Skip to content

Canonical Identity Binding Cutover: Remove isTrusted#11006

Merged
noanflaherty merged 13 commits into
mainfrom
feature/canonical-id-binding-cutover
Mar 1, 2026
Merged

Canonical Identity Binding Cutover: Remove isTrusted#11006
noanflaherty merged 13 commits into
mainfrom
feature/canonical-id-binding-cutover

Conversation

@noanflaherty
Copy link
Copy Markdown
Contributor

@noanflaherty noanflaherty commented Mar 1, 2026

Summary

Remove the isTrusted compatibility bypass from all guardian decision authorization paths and replace it with canonical identity binding using guardianPrincipalId. All decisions are now authorized purely by principal matching: actor.guardianPrincipalId === request.guardianPrincipalId, with a cross-channel fallback via the assistant's canonical vellum principal.

Changes

  • Added guardianPrincipalId column to channel_guardian_bindings and canonical_guardian_requests tables (M1)
  • Backfilled existing rows with principal IDs and expired unresolvable pending requests (M2)
  • Propagated principal through all runtime contexts: actor-trust-resolver, guardian-context-resolver, local-actor-identity, session-runtime-assembly (M3)
  • Bound principal at all canonical request creation sites and all createBinding callsites, with IntegrityError guard for decisionable kinds (M4)
  • Replaced isTrusted bypass in applyCanonicalGuardianDecision with 3-step principal validation, extracted isAuthorizedGuardianPrincipal shared helper (M5)
  • Added guard test to prevent isTrusted reintroduction, updated all tests to principal-match pattern (M6)

Milestone PRs (merged into feature branch)

Project issue

Closes #10955

Test plan

  • Verify isTrusted does not appear in production code: grep -rn "isTrusted" assistant/src/ --include="*.ts" | grep -v __tests__ | grep -v node_modules
  • Verify all decisionable canonical requests persist guardianPrincipalId
  • Verify cross-surface approvals: request created in channel, decision applied from vellum/guardian thread
  • Verify principal mismatch correctly blocks unauthorized decisions
  • Run full test suite: cd assistant && bun test

Generated with Claude Code


Open with Devin

noanflaherty and others added 6 commits February 28, 2026 22:26
Co-authored-by: Claude <noreply@anthropic.com>
* 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>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* 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>
* 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>
) (#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>
@noanflaherty
Copy link
Copy Markdown
Contributor Author

@codex review

devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

…xempt, backfill expiry, code lookup guard

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 db6ddac

devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

…pproval

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 4b482a8

@noanflaherty
Copy link
Copy Markdown
Contributor Author

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

devin-ai-integration[bot]

This comment was marked as resolved.

@noanflaherty
Copy link
Copy Markdown
Contributor Author

PR Review: Canonical Identity Binding Cutover

I did a focused review of the identity-binding + isTrusted removal path. Two issues stood out.

1) [P1] Authorization is still effectively a compatibility shim in one direction

  • File: assistant/src/approvals/guardian-decision-primitive.ts (isAuthorizedGuardianPrincipal, around lines 299-317)
  • Issue: isAuthorizedGuardianPrincipal returns true when either side matches the vellum canonical principal, even when actorPrincipalId !== requestPrincipalId.

Current logic:

  • allow on direct principal match
  • allow if actor principal is canonical vellum principal
  • allow if request principal is canonical vellum principal

That third case means a request bound to vellum canonical principal can be approved by any other channel principal, even if principals do not match. This weakens the strict principal-binding invariant and keeps a compatibility bypass in place.

Why this matters:

  • The PR goal is to rely on canonical identity binding and retire isTrusted behavior.
  • This helper preserves implicit trust bridging rather than enforcing a single bound identity.

Suggested fix:

  • Prefer strict principal equality in the authorization gate.
  • If cross-channel approvals are required, unify all bindings for the same guardian onto one principal (migration/backfill) instead of runtime OR-based fallback.

2) [P2] New principal requirement broke multiple touched tests

The new createCanonicalGuardianRequest guard requires guardianPrincipalId for decisionable kinds (tool_approval, tool_grant_request, pending_question). Several test fixtures in this PR still create tool_approval rows without it.

  • File: assistant/src/__tests__/guardian-actions-endpoint.test.ts
    • createTestCanonicalRequest() (around lines 90-114) defaults to kind: 'tool_approval' but does not set guardianPrincipalId.
  • File: assistant/src/__tests__/guardian-principal-id-roundtrip.test.ts
    • multiple tests create tool_approval without guardianPrincipalId (e.g. around lines 129-143, 178-182).

Repro:

cd assistant
bun test src/__tests__/guardian-actions-endpoint.test.ts

This currently fails with IntegrityError: Cannot create decisionable canonical request ... without guardianPrincipalId.


What I ran

  • bun test src/__tests__/guardian-decision-primitive-canonical.test.ts
  • bun test src/__tests__/guardian-routing-invariants.test.ts
  • bun test src/__tests__/guardian-actions-endpoint.test.ts (fails as above)

…gration 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>
devin-ai-integration[bot]

This comment was marked as resolved.

…tyError guard

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

Both issues addressed in commit 59c9010:

P1: Replaced the isAuthorizedGuardianPrincipal runtime OR-based fallback with strict principal equality. All channel bindings are now unified onto the canonical vellum principal at creation time (channel-guardian-service.ts) and in the backfill migration (126, re-keyed to v2). The isAuthorizedGuardianPrincipal function has been removed entirely — authorization is now a single actorContext.guardianPrincipalId !== request.guardianPrincipalId check.

P2: Added guardianPrincipalId to the createTestCanonicalRequest helper in guardian-actions-endpoint.test.ts (defaults to 'test-principal'). All 4 guardian test files pass (111 tests, 0 failures).

devin-ai-integration[bot]

This comment was marked as resolved.

@noanflaherty
Copy link
Copy Markdown
Contributor Author

Follow-up review on 59c9010 (merge readiness)

I re-reviewed after the latest fix commit and reran the affected suites.

✅ Previously raised issues are addressed

  • Principal authorization is now strict equality in the canonical decision path.
  • Channel principal unification/backfill is implemented in migration/service paths.
  • Test fixture IntegrityError failures were resolved (missing guardianPrincipalId in decisionable request fixtures).

⚠️ One remaining issue before merge

[P2] TypeScript union narrowing regression

  • File: assistant/src/runtime/guardian-reply-router.ts (around line 507)
  • Issue: resolverReplyText is read from canonicalResult without narrowing on applied.

Current expression:

const hasResolverReplyText = Boolean(result.canonicalResult?.resolverReplyText);

CanonicalDecisionResult is a union where resolverReplyText only exists on the applied: true branch, so this causes a TS error.

Suggested fix (matches the safe pattern already used on main):

const hasResolverReplyText = Boolean(
  result.canonicalResult?.applied && result.canonicalResult.resolverReplyText,
);

Test signal from this pass

I reran targeted suites on the PR head:

  • guardian-actions-endpoint.test.ts
  • guardian-principal-id-roundtrip.test.ts
  • guardian-decision-primitive-canonical.test.ts
  • guardian-routing-invariants.test.ts
  • no-is-trusted-guard.test.ts
  • actor-token-service.test.ts
  • channel-guardian.test.ts

Merge recommendation

This is very close, but I would fix the single guardian-reply-router.ts narrowing issue first, then merge.

@noanflaherty noanflaherty merged commit ab2923c into main Mar 1, 2026
1 check passed
@noanflaherty noanflaherty deleted the feature/canonical-id-binding-cutover branch March 1, 2026 13:52
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 found 1 new potential issue.

View 15 additional findings in Devin Review.

Open in Devin Review

Comment thread assistant/src/approvals/guardian-decision-primitive.ts
noanflaherty added a commit that referenced this pull request Mar 1, 2026
…aps from #11006

- Fix local IPC fallback context field names (conversationExternalId, actorExternalId)
- Make voice guardian dispatch binding-self-healing via ensureVellumGuardianBinding
- Add access_request to DECISIONABLE_KINDS with self-healing and migration update
- Rename residual isTrusted symbol to trustedAudience in call-pointer-messages
- Update all test fixtures to supply guardianPrincipalId for decisionable kinds

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
noanflaherty added a commit that referenced this pull request Mar 1, 2026
…aps (#11054)

* fix: harden canonical identity binding — close remaining post-merge gaps from #11006

- Fix local IPC fallback context field names (conversationExternalId, actorExternalId)
- Make voice guardian dispatch binding-self-healing via ensureVellumGuardianBinding
- Add access_request to DECISIONABLE_KINDS with self-healing and migration update
- Rename residual isTrusted symbol to trustedAudience in call-pointer-messages
- Update all test fixtures to supply guardianPrincipalId for decisionable kinds

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

* fix: bump migration checkpoint to v3 so access_request backfill runs on upgraded databases

Addresses Codex review: the withCrashRecovery checkpoint key was still v2,
meaning databases that already completed v2 would skip the new access_request
principal-binding and expiration logic.

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

---------

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

Addressed in #11084

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>
tkheyfets pushed a commit that referenced this pull request Mar 2, 2026
…aps (#11054)

* fix: harden canonical identity binding — close remaining post-merge gaps from #11006

- Fix local IPC fallback context field names (conversationExternalId, actorExternalId)
- Make voice guardian dispatch binding-self-healing via ensureVellumGuardianBinding
- Add access_request to DECISIONABLE_KINDS with self-healing and migration update
- Rename residual isTrusted symbol to trustedAudience in call-pointer-messages
- Update all test fixtures to supply guardianPrincipalId for decisionable kinds

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

* fix: bump migration checkpoint to v3 so access_request backfill runs on upgraded databases

Addresses Codex review: the withCrashRecovery checkpoint key was still v2,
meaning databases that already completed v2 would skip the new access_request
principal-binding and expiration logic.

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

---------

Co-authored-by: Claude Opus 4.6 <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.

Canonical Identity Binding Cutover: Remove isTrusted Compatibility Shim

1 participant