fix(actors): reject foreign-authority ids at POST /actors/; add POST /actors/{id}/peers/ - #2916
Conversation
…/actors/{id}/peers/
Closes #2549
Root cause: POST /actors/ accepted any actor_id, including ids under foreign
authorities. actor_slug() drops scheme and netloc, so two ids that share only
the final path segment (e.g. finder:7999/.../alice and vendor:7999/.../alice)
mapped to the same SQLite file. hosted_actor_ids() then reported remote actors
as locally hosted, and subsequent delivery failed in unexpected ways.
Fix (ADR-0081):
- _is_foreign_authority() helper compares the request id's netloc against the
serving node's base_url; POST /actors/ returns 422 for any id whose authority
does not match.
- New POST /actors/{actor_id}/peers/ endpoint stores a peer as a CoreActor
record inside the named hosted actor's own store (address-book entry, not a
phantom hosted actor).
- Demo seeding helpers (seed_containers*, cli.py seed command) updated to use
seed_peer() for Phase 2 cross-container registrations.
- Engine-level collision guard remains a WARNING; the route-level rejection is
the primary guard. Warning-not-raise preserves the multi-node test harness
where two nodes in one process legitimately share a slug.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sei-ahouseholder
left a comment
There was a problem hiding this comment.
PR Triage: #2916 — fix(actors): reject foreign-authority ids at POST /actors/; add POST /actors/{id}/peers/
Linked issues: #2549 (foreign-authority actor store collision)
Changed files: 18 files — adapters/driven, adapters/driving/fastapi, demo
CI status: ❌ failing (Tests/pytest — 20 failures; all other checks pass)
Merge state:
Base branch: main
Needs integration tests: yes
Findings
| # | Phase | Severity | Description | Outcome |
|---|---|---|---|---|
| phase11-test-suite-failing-0 | ci-status | ❌ FAIL | 20 tests fail in 7 test/demo/ files: _create_actor() helpers post foreign-authority IDs to POST /actors/ (now 422). test_case_proposal_round_trip.py, test_multi_actor_seed.py, test_pcr_bootstrap.py, test_pcr_engage_case.py, test_pcr_late_joiner.py, test_seed.py, test_sync_ledger_replication.py were not migrated to seed_peer(). |
fix-now |
| phase8-docstring-raises-vs-warns-0 | code-review | ❌ FAIL | actor_slug() docstring (engine.py:102) says :func:\get_actor_engine` raises if it ever occursbut the function logs a WARNING, not an exception. Contradicts both actual behavior andget_actor_engine()`'s own docstring. |
fix-now |
| phase3-pr-body-format-0 | pr-body-format | Closes #2549 is at the bottom after ## Test plan — must precede any ## header. Also missing Changes section and Verification section with actual test counts. |
fix-now | |
| phase9-mkdocs-title-inconsistency-0 | notes-docs-currency | mkdocs.yml line 319: ADR-0081 title missing "Own" ("Hosted Actor Store" vs "Hosted Actor's Own Store"). |
fix-now |
Total: 2 FAIL · 2 IMPROVE · 0 NEW-ISSUE
Triage artifact: .claude/pr-2916-triage.json
Next step: run /pr-execute or /pr-ship to apply fixes.
PR Triage: #2916 — fix(actors): reject foreign-authority ids at POST /actors/; add POST /actors/{id}/peers/Linked issues: #2549 (foreign-authority actor store collision) Findings
Total: 2 FAIL · 2 IMPROVE · 0 NEW-ISSUE Triage artifact: |
- ci-status — migrate 7 test/demo/ files from _create_actor(wrong_client, foreign_base_api) to _register_peer() (ADR-0081); update TestSeedCLIWith- DeterministicId and TestSeedCommand to mock seed_peer alongside seed_actor - code-review — actor_slug() docstring: "raises" → "warns" (engine.py:102) - notes-docs-currency — mkdocs.yml ADR-0081 title: add missing "Own" - pr-body-format — Closes #2549 moved to top; added Changes + Verification Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sei-ahouseholder
left a comment
There was a problem hiding this comment.
PR Execute: #2916 — fix(actors): reject foreign-authority ids at POST /actors/; add POST /actors/{id}/peers/
Fixes applied: 1 commit (32e7719b)
Issues filed: 0
Deferred (awaiting your input): 0
Tests run: unit + integration
CI status: ✅ passing (22/22 checks)
Base sync: ✅ merged main @ 8d40c313 — 0 conflicts resolved
Fixed
| Finding | Commit |
|---|---|
| phase11-test-suite-failing-0: 20 failing tests — _create_actor() used foreign-authority client in 7 demo test files | 32e7719b |
| phase8-docstring-raises-vs-warns-0: actor_slug() docstring said "raises" but get_actor_engine() warns | 32e7719b |
| phase3-pr-body-format-0: PR body missing Closes at top, Changes section, and Verification counts | 32e7719b |
| phase9-mkdocs-title-inconsistency-0: mkdocs.yml ADR-0081 title missing "Own" | 32e7719b |
Execute artifact: .claude/pr-2916-execute.json
Next step: run /pr-verify, or /pr-ship will continue automatically.
sei-ahouseholder
left a comment
There was a problem hiding this comment.
PR Verify: #2916 — fix(actors): reject foreign-authority ids at POST /actors/; add POST /actors/{id}/peers/
Overall verdict: ✅ READY-TO-MERGE
CI: ✅ 22/22 checks passing
Merge state: MERGEABLE / BLOCKED (pending required review — repo policy, not a defect)
Conflict markers: clean
Finding Verdicts
| Finding | Severity | Outcome | Verdict |
|---|---|---|---|
| phase11-test-suite-failing-0: 20 failing tests across 7 demo test files | FAIL | fixed @ 32e7719b |
✅ CONFIRMED |
| phase8-docstring-raises-vs-warns-0: actor_slug() docstring said "raises" | FAIL | fixed @ 32e7719b |
✅ CONFIRMED |
| phase3-pr-body-format-0: PR body format violations | IMPROVE | fixed @ 32e7719b |
✅ CONFIRMED |
| phase9-mkdocs-title-inconsistency-0: mkdocs.yml ADR-0081 title missing "Own" | IMPROVE | fixed @ 32e7719b |
✅ CONFIRMED |
All FAIL findings confirmed at HEAD. CI green. Branch synced with main @ 8d40c313. No unresolved items.
Verify artifact cleanup complete.
PR #2916 is READY-TO-MERGE.
… PRs Replace two `ADR-0073 decision 5` references introduced by PRs #2916 and #2927 while resolving merge conflicts with origin/main: - test/adapters/driving/fastapi/routers/test_actors.py:447 - vultron/adapters/driving/fastapi/routers/actors/_routes.py:166 Both are replaced with ADR-0073#peer-records-in-knowers-store. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Renumbers this branch's ADR 0081 -> 0082. PR #2916 landed its own ADR-0081 ("Peer Knowledge Lives in the Hosted Actor's Own Store, Not as a Hosted Actor") on main first, and that number already has inbound references across vultron/ and test/, so the unlanded ADR is the one that moves. Conflicts, both in adr-index generator output, resolved by taking main's side and regenerating: - docs/adr/index.md — regenerated with `adr-index --write`; now lists both ADR-0081 (peer knowledge) and ADR-0082 (wire/core boundary). - mkdocs.yml — nav labels are hand-crafted and only checked for completeness, so the ADR-0082 entry was added by hand in number order. All 51 references to the wire/core-boundary ADR were rewritten across 22 files (specs, notes, docs, AGENTS.md, and the ARCH-22 ratchet test docstring). Every occurrence was inspected individually first: references to main's ADR-0081 in vultron/ and test/ are deliberately untouched. Known exception: plan/history/2608/learning/CONCERN-2830.md still cites docs/adr/0081-wire-core-boundary-pairing-registry.md. HM-01-005 makes history entries write-once (MUST NOT be modified), and that spec has no sanctioned amendment path for an external renumbering, so the stale path is left in place rather than violating the rule. Recorded on #2952. Verification after the merge: - Full unit suite: no failures, no errors (pre-existing xfails only). - mypy 1315 files clean; pyright 0 errors; black clean; flake8 vultron/ test/ clean (CI scope). - adr-index --check: index and nav in sync. - mkdocs build --strict: 101 warnings on this branch and 101 on origin/main — identical, pre-existing, tracked as #2904. This branch adds none. - test/architecture/test_codebase_docs_paths.py (new ratchet from main) passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes #2549
Summary
POST /actors/accepted foreign-authority ids.actor_slug()drops scheme and netloc, sofinder:7999/.../aliceandvendor:7999/.../alicemapped to the same SQLite file.hosted_actor_ids()then reported remote actors as locally hosted, breaking outbound delivery._is_foreign_authority()compares the request id's netloc against the serving node'sbase_url;POST /actors/returns 422 for any id whose authority does not match.POST /actors/{actor_id}/peers/stores a peer as aCoreActorrecord inside the named hosted actor's own store — an address-book entry, not a phantom hosted actor.seed_containers*helpers andcli.py seedcommand now useseed_peer()for Phase 2 cross-container peer registrations.get_actor_enginecollision guard remains a WARNING (not exception); the route-level rejection is the primary guard. Warning-not-raise preserves the multi-node test harness where two nodes in one process legitimately share a slug.Changes
vultron/adapters/driving/fastapi/routers/actors/_routes.py—_is_foreign_authority(), 422 guard increate_actor(), newPOST /{actor_id:path}/peers/endpointvultron/demo/utils.py—seed_peer()utility functionvultron/demo/helpers/seeding.py— allseed_containers*helpers updated to useseed_peer()for Phase 2vultron/demo/cli.py—seedcommand usesseed_peer()for peer registrationvultron/adapters/driven/datalayer_sqlite/engine.py—actor_slug()docstring updatedvultron/adapters/driven/actor_hosts.py—canonical_actor_uri()docstring updateddocs/adr/0081-peer-knowledge-in-hosted-actor-store.md— new ADRVerification
uv run pytest— 7899 passed, 0 failed (+103 new tests net)docs/adr/0081-peer-knowledge-in-hosted-actor-store.mdadded ✅POST /actors/with foreign-authority id → 422 (test_a_foreign_absolute_id_is_rejected) ✅hosted_actor_ids()enumerates only hosted actors (peers live in host stores, not as hosted actors) ✅create_actorrejects foreign-authority ids with clear error message ✅TestRegisterPeer— 5 new tests: 201 on creation, 200 on repeat, peer absent fromGET /actors/, 422 for non-http peer id, 404 for unknown host ✅TestGetActorEngineCollisionGuard— warning (not raise) on cross-authority slug collision ✅🤖 Generated with Claude Code