Skip to content

M2: Backfill + startup invariants for guardian principal#10969

Merged
noanflaherty merged 2 commits into
feature/canonical-id-binding-cutoverfrom
swarm/canonical-id-bind/task-2
Mar 1, 2026
Merged

M2: Backfill + startup invariants for guardian principal#10969
noanflaherty merged 2 commits into
feature/canonical-id-binding-cutoverfrom
swarm/canonical-id-bind/task-2

Conversation

@noanflaherty
Copy link
Copy Markdown
Contributor

@noanflaherty noanflaherty commented Mar 1, 2026

Backfill guardianPrincipalId for existing channel_guardian_bindings and canonical_guardian_requests rows. Update guardian-vellum-migration to set principal on binding creation. Expire unresolvable pending requests. Part of #10955.


Open with Devin

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@noanflaherty noanflaherty self-assigned this Mar 1, 2026
@noanflaherty
Copy link
Copy Markdown
Contributor Author

👀 Where to focus your review

  • assistant/src/memory/migrations/126-backfill-guardian-principal-id.ts: Data migration that backfills guardianPrincipalId and expires unresolvable pending requests. The binding backfill sets principal = externalUserId which is the correct canonical mapping. The request backfill logic for step 3b (desktop-originated requests) uses source_type = 'desktop' OR source_channel = 'vellum' — verify these are the correct discriminators for local desktop requests.
  • assistant/src/runtime/guardian-vellum-migration.ts: Now sets guardianPrincipalId on new binding creation and backfills it on existing bindings that lack it. The startup-time backfill is a belt-and-suspenders complement to migration 126.

Risk level: Medium — data migration that expires pending requests that can't be deterministically bound to a principal. This is the intended behavior per the task spec, but worth confirming no edge case leaves a legitimately actionable request expired.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 4 additional findings.

Open in Devin Review

…ktop rebinding predicate

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@noanflaherty
Copy link
Copy Markdown
Contributor Author

@codex review this PR again — the previous issues have been fixed in commit cdd4e2e

@noanflaherty
Copy link
Copy Markdown
Contributor Author

@devin review this PR again — the previous issues have been fixed in commit cdd4e2e

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@noanflaherty noanflaherty merged commit 78e7990 into feature/canonical-id-binding-cutover Mar 1, 2026
1 check passed
@noanflaherty noanflaherty deleted the swarm/canonical-id-bind/task-2 branch March 1, 2026 03:45
noanflaherty added a commit that referenced this pull request Mar 1, 2026
* feat: add guardianPrincipalId to schema and storage plumbing (#10965)

Co-authored-by: Claude <noreply@anthropic.com>

* M2: Backfill + startup invariants for guardian principal (#10969)

* feat: backfill guardianPrincipalId and enforce startup invariants

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: restrict backfill expiration to expired requests and tighten desktop rebinding predicate

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat: propagate guardianPrincipalId through runtime contexts (#10978)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* M4: Bind principal at all canonical request creation sites (#10980)

* feat: bind guardianPrincipalId at all canonical request creation sites

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add try/catch guards around createCanonicalGuardianRequest in all caller paths

Wrap createCanonicalGuardianRequest calls in try/catch in three locations
to handle IntegrityError when guardianPrincipalId is missing:

1. daemon/server.ts (makePendingInteractionRegistrar) - prevents crash when
   session.guardianContext is undefined during tool approval events
2. runtime/routes/conversation-routes.ts (makeHubPublisher) - same pattern
   for the HTTP hub publisher path
3. runtime/access-request-helper.ts - preserves the intentional no-binding
   fallback path (documented at file header) where access requests proceed
   without guardian identity

All catch blocks log at debug level matching the existing pattern in
daemon/handlers/sessions.ts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add try/catch in inbound-message-handler and fix misleading notified:true in access-request-helper

- Wrap createCanonicalGuardianRequest call in inbound-message-handler.ts
  ingress escalation path with try/catch to prevent unhandled IntegrityError
  from crashing the HTTP handler with a 500. On failure, logs a warning and
  continues to the notification pipeline.

- Fix catch block in access-request-helper.ts to return notified: false
  instead of notified: true, since the emitNotificationSignal call is never
  reached when the error is caught.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: pass guardianPrincipalId to all createBinding callsites

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* M5: Cutover decision authorization + remove isTrusted (#10990)

* feat: cutover decision authorization to principal-based and remove isTrusted

Replace the isTrusted compatibility bypass in all runtime types and callsites
with principal-based authorization. Decision authorization is now purely
principal-based: actor.guardianPrincipalId must match request.guardianPrincipalId
for any applied decision. There is no longer a trusted bypass path.

Key changes:
- Remove isTrusted from ActorContext type, replace with guardianPrincipalId
- Add three-step principal validation in applyCanonicalGuardianDecision()
- Add guardianPrincipalId filter to canonical guardian store queries
- Update guardian-reply-router to use principal-based request discovery
- Update all callsites (HTTP routes, daemon handlers, session process,
  inbound message handler) to resolve and pass guardianPrincipalId
- Replace isTrusted-based guardianReplyText conditionals with channel checks

Closes #10960

* fix: resolve cross-channel principal mismatch and non-vellum channel fallback

- conversation-routes.ts: Fall back to session.guardianContext for
  verifiedActorExternalUserId and verifiedActorPrincipalId when
  actorVerification is null (non-vellum channels).
- guardian-decision-primitive.ts: Allow cross-channel guardian decisions
  by checking actor principal against the assistant's canonical vellum
  binding principal when direct principal match fails.
- guardian-reply-router.ts: Add guardianPrincipalId filter to the
  conversation fallback query in findPendingCanonicalRequests so
  guardians only see requests they are authorized to act on.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add cross-channel principal fallback to code-only clarification check

Extract isAuthorizedGuardianPrincipal() shared helper from the inline
cross-channel fallback logic in applyCanonicalGuardianDecision, and use
it in the router's code-only clarification identity check. This ensures
a desktop guardian entering a request code for a cross-channel request
(e.g. Telegram-originated) sees the clarification context instead of
getting "Request not found".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat: M6 — tests + guardrails + cleanup for principal-based auth (#10961) (#11002)

* feat: M6 — tests + guardrails + cleanup for principal-based auth (#10961)

- Update 6 test files to use guardianPrincipalId principal-match pattern
  instead of legacy isTrusted boolean
- Add guard test (no-is-trusted-guard.test.ts) to prevent isTrusted
  reintroduction in production code
- Clean up 3 stale comments referencing trusted bypass and
  desktop/trusted with principal-based terminology
- Verify no production isTrusted references remain (only allowed
  trust-class variable names: isTrustedActor, isTrustedContact,
  isTrustedTrustClass)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: correct guard test pipe and identity mismatch test field

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use JS-level allowlist filtering in guard test to prevent masking

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address holistic review — decidedByPrincipalId, access_request exempt, backfill expiry, code lookup guard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: make isAuthorizedGuardianPrincipal symmetric for cross-channel approval

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address reviewer feedback — binding check, pre-bootstrap IPC, migration access_request exclusion

- isAuthorizedGuardianPrincipal: verify actor's principal belongs to an
  active guardian binding before allowing cross-channel approval, closing
  the asymmetric authorization gap (Devin #3/#4)
- local-actor-identity: eagerly create vellum binding via
  ensureVellumGuardianBinding in pre-bootstrap IPC path so downstream
  decisionable requests always have a guardianPrincipalId (Devin #5)
- Migration 126: exclude access_request from expiry sweep in steps 3a
  and 3c since access_request is non-decisionable and proceeds via the
  invite flow (Codex P2 #2)
- Update roundtrip tests to supply guardianPrincipalId for decisionable
  tool_approval requests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: unify channel binding principals + fix test fixtures for IntegrityError guard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: align migration registry checkpoint key with migration function (_v1 -> _v2)

* fix: narrow CanonicalDecisionResult union before accessing resolverReplyText

---------

Co-authored-by: Claude <noreply@anthropic.com>
tkheyfets pushed a commit that referenced this pull request Mar 2, 2026
* feat: add guardianPrincipalId to schema and storage plumbing (#10965)

Co-authored-by: Claude <noreply@anthropic.com>

* M2: Backfill + startup invariants for guardian principal (#10969)

* feat: backfill guardianPrincipalId and enforce startup invariants

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: restrict backfill expiration to expired requests and tighten desktop rebinding predicate

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat: propagate guardianPrincipalId through runtime contexts (#10978)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* M4: Bind principal at all canonical request creation sites (#10980)

* feat: bind guardianPrincipalId at all canonical request creation sites

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add try/catch guards around createCanonicalGuardianRequest in all caller paths

Wrap createCanonicalGuardianRequest calls in try/catch in three locations
to handle IntegrityError when guardianPrincipalId is missing:

1. daemon/server.ts (makePendingInteractionRegistrar) - prevents crash when
   session.guardianContext is undefined during tool approval events
2. runtime/routes/conversation-routes.ts (makeHubPublisher) - same pattern
   for the HTTP hub publisher path
3. runtime/access-request-helper.ts - preserves the intentional no-binding
   fallback path (documented at file header) where access requests proceed
   without guardian identity

All catch blocks log at debug level matching the existing pattern in
daemon/handlers/sessions.ts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add try/catch in inbound-message-handler and fix misleading notified:true in access-request-helper

- Wrap createCanonicalGuardianRequest call in inbound-message-handler.ts
  ingress escalation path with try/catch to prevent unhandled IntegrityError
  from crashing the HTTP handler with a 500. On failure, logs a warning and
  continues to the notification pipeline.

- Fix catch block in access-request-helper.ts to return notified: false
  instead of notified: true, since the emitNotificationSignal call is never
  reached when the error is caught.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: pass guardianPrincipalId to all createBinding callsites

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* M5: Cutover decision authorization + remove isTrusted (#10990)

* feat: cutover decision authorization to principal-based and remove isTrusted

Replace the isTrusted compatibility bypass in all runtime types and callsites
with principal-based authorization. Decision authorization is now purely
principal-based: actor.guardianPrincipalId must match request.guardianPrincipalId
for any applied decision. There is no longer a trusted bypass path.

Key changes:
- Remove isTrusted from ActorContext type, replace with guardianPrincipalId
- Add three-step principal validation in applyCanonicalGuardianDecision()
- Add guardianPrincipalId filter to canonical guardian store queries
- Update guardian-reply-router to use principal-based request discovery
- Update all callsites (HTTP routes, daemon handlers, session process,
  inbound message handler) to resolve and pass guardianPrincipalId
- Replace isTrusted-based guardianReplyText conditionals with channel checks

Closes #10960

* fix: resolve cross-channel principal mismatch and non-vellum channel fallback

- conversation-routes.ts: Fall back to session.guardianContext for
  verifiedActorExternalUserId and verifiedActorPrincipalId when
  actorVerification is null (non-vellum channels).
- guardian-decision-primitive.ts: Allow cross-channel guardian decisions
  by checking actor principal against the assistant's canonical vellum
  binding principal when direct principal match fails.
- guardian-reply-router.ts: Add guardianPrincipalId filter to the
  conversation fallback query in findPendingCanonicalRequests so
  guardians only see requests they are authorized to act on.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add cross-channel principal fallback to code-only clarification check

Extract isAuthorizedGuardianPrincipal() shared helper from the inline
cross-channel fallback logic in applyCanonicalGuardianDecision, and use
it in the router's code-only clarification identity check. This ensures
a desktop guardian entering a request code for a cross-channel request
(e.g. Telegram-originated) sees the clarification context instead of
getting "Request not found".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat: M6 — tests + guardrails + cleanup for principal-based auth (#10961) (#11002)

* feat: M6 — tests + guardrails + cleanup for principal-based auth (#10961)

- Update 6 test files to use guardianPrincipalId principal-match pattern
  instead of legacy isTrusted boolean
- Add guard test (no-is-trusted-guard.test.ts) to prevent isTrusted
  reintroduction in production code
- Clean up 3 stale comments referencing trusted bypass and
  desktop/trusted with principal-based terminology
- Verify no production isTrusted references remain (only allowed
  trust-class variable names: isTrustedActor, isTrustedContact,
  isTrustedTrustClass)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: correct guard test pipe and identity mismatch test field

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use JS-level allowlist filtering in guard test to prevent masking

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address holistic review — decidedByPrincipalId, access_request exempt, backfill expiry, code lookup guard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: make isAuthorizedGuardianPrincipal symmetric for cross-channel approval

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address reviewer feedback — binding check, pre-bootstrap IPC, migration access_request exclusion

- isAuthorizedGuardianPrincipal: verify actor's principal belongs to an
  active guardian binding before allowing cross-channel approval, closing
  the asymmetric authorization gap (Devin #3/#4)
- local-actor-identity: eagerly create vellum binding via
  ensureVellumGuardianBinding in pre-bootstrap IPC path so downstream
  decisionable requests always have a guardianPrincipalId (Devin #5)
- Migration 126: exclude access_request from expiry sweep in steps 3a
  and 3c since access_request is non-decisionable and proceeds via the
  invite flow (Codex P2 #2)
- Update roundtrip tests to supply guardianPrincipalId for decisionable
  tool_approval requests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: unify channel binding principals + fix test fixtures for IntegrityError guard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: align migration registry checkpoint key with migration function (_v1 -> _v2)

* fix: narrow CanonicalDecisionResult union before accessing resolverReplyText

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant