Skip to content

Support trustworthy per-member invocation identity for authored apps #15

Open

Description

@shuv1337

Problem

A native family app needs notes and other authored records to carry trustworthy per-member authorship when invoked through Executor. We could not establish a supported contract that binds an authenticated caller to the effective app invocation identity. Caller-supplied member IDs cannot be the authority, and saved execution profiles must remain distinct from people.

This is an integration-contract gap, not a demonstrated authorization vulnerability. If a supported account-binding pattern already satisfies it, document and prove that pattern instead of adding another identity mechanism.

Evidence

During CrommettCrew Phase 0 discovery on October 12, 2026 (PDT), the personal self-hosted instance reported apps@0.0.1-beta.74. Its published BoundContext exposed account bindings and services but no immutable caller identity. Local source inspected by that integration was a61cbe30cdd42402dce8fd7b2416575a2fdaa6a0; relevant SDK surface: packages/apps/src/contracts/app.ts (BoundContext). Local source is not proof of identical deployed behavior.

The integration kept real family profiles and notes disabled. A separate loopback-only canvas proof uses synthetic profiles and does not solve Executor identity.

Requested outcome

Provide and document either a supported per-member account binding or host-established immutable invocation identity. Keep Executor principal, application member, organization, and saved execution profile explicitly distinct. Define behavior for native requests, browser app tools, MCP, delegated calls, and background work wherever supported; unsupported contexts must fail explicitly rather than inherit operator authority.

Acceptance

  • A real self-host E2E scenario uses two synthetic principals in one organization to invoke the same note query/mutation and records the correct authenticated author.
  • Substituting member, organization, or profile identifiers cannot impersonate another principal or expand authority.
  • Revoked membership/sessions and hidden or unselected tools fail closed; adding a privileged capability does not silently grant it to existing selections.
  • The public SDK and authoring documentation explain the supported identity binding and its limitations, with an executable example.
  • Tests drive public HTTP/MCP/browser surfaces under e2e/, following repository conventions.

Related: native-to-browser session exchange is tracked in #16. This issue does not authorize enabling production family data or deployment.

Activity

  1. shuv1337 commented on Oct 23, 2026

    @shuv1337
    OwnerAuthor

    Status 2026-10-23 (post-cutover audit): mostly delivered; three acceptance items are still unproven, so this stays open.

    Delivered and live (deployed cff076a98; ctx.caller / ctx.approvedBy since #27, behind the callerIdentity capability):

    • Host-set caller. ctx.caller (kind, subject, issuer, email, organizationId, channel, provenance) is set by the host and never by the caller. Documented with an example in packages/app-templates/executor/skills/app-authoring/tools.md ("ctx.caller is the invocation's verified caller…").
    • Two principals, one tool, distinct identities. e2e/tests/caller-identity.spec.ts (callerIdentity) has a member and an admin, each over MCP with a PAT, call the same tool and get distinct verified callers. It also covers the dashboard person and workflow runs (system caller, owner subject), caller-scoped cache, no secret in caller data, and an approval recorded as approvedBy while the requester stays the caller. Revoking one user's key stops only that user.
    • A person must approve. personApproval in the same file: a tool that requires a person refuses the agent's own approval. Per-caller identity also holds in a shared runtime (shared-runtime.spec.ts), and app-page calls have the ui channel (app-page-approvals.spec.ts).
    • In production: the Mesh cutover (Oct 22) uses ctx.caller / ctx.approvedBy for its operator and console gates.

    Not yet proven by a scenario:

    1. Substituting member, organization or profile identifiers in the request can't impersonate another principal. The design makes it impossible, since input never sets ctx.caller, but no scenario sends forged fields to show it.
    2. Removing someone's organization membership, or ending their sessions, makes their calls fail closed. Only key revocation is covered here.
    3. Adding a privileged capability doesn't silently grant it to existing selections.

    Possibly out of scope: the native-request context overlaps #16, which is still open.

  2. shuv1337 commented on Oct 23, 2026

    @shuv1337
    OwnerAuthor

    Status 2026-10-23 01:15 PDT: #58 merged (cbaeeb998). Every acceptance item except two is now proven by a scenario, so this stays open.

    New in #58 (test-only): caller-identity.spec.ts, scenario "apps see each MCP user, the dashboard person and workflow work as distinct verified callers" (callerIdentity). Its test app adds a claim tool whose input takes caller and approvedBy as plain JSON. Alice calls it naming Bob's caller with organizationId: "org_forged" and an owner email, plus an approvedBy naming Bob as a human browser approver. The scenario asserts forged.caller equals Alice's own verified caller and forged.approvedBy is null. So member, organization and approver identifiers in the input can't impersonate another principal.

    Existing scenarios that cover the rest of acceptance item 3:

    • Membership removal fails closed: member-api-keys.spec.ts, "Organization membership removal permanently revokes pinned API keys" (memberApiKeys), step "Membership lifecycle". After POST /api/auth/organization/remove-member:
      • the organization-pinned key returns 401 and is dropped from the key list;
      • the member's full-account key returns 403 in that organization;
      • another user's key still returns 200;
      • "Cannot replace removed key" is refused with 403;
      • after the member rejoins, the pinned key still returns 401.
    • Ending sessions fails closed: warm-request-auth.spec.ts, "Dashboard requests refuse signed-out sessions and removed members immediately" (warmRequestAuth):
      • step "A signed-out session is refused on its next request": a second cookie jar holding the same session gets 401 right after sign-out;
      • step "A removed member's live session is refused on its next request": a live session gets 403 right after remove-member.
    • Hidden and unselected tools, and capability expansion: mcp-grant-restrictions.spec.ts, "MCP grants retain exact selections through deploys and live narrowing" (liveGrantRestrictions).
      • Fixture step "Adding a tool never expands an exact selection" redeploys the app with a new later mutation after consent.
      • Calls to later and to a hidden app's echo both return execution.ok === false, while the selected echo succeeds.
      • Step "Version changes retain selections and later narrowing affects active clients" narrows the grant, and the active client's next echo fails.

    Targeted run: the four scenarios above all passed on self-host E2E against the #58 head (19d875d3c). On the fork, #58's CI ran check and Socket, both green; it has no E2E jobs. Prod (a4f6f0b84, deployed 00:52 PDT) has the same app/runtime code; #58 changes only the spec.

    Still unproven, so this stays open:

    1. Profile substitution: no scenario substitutes a saved execution-profile identifier (a connection's runsAs account) to show it can't impersonate another principal. The member, organization and approver fields are covered by Prove that identity named in a tool's input never becomes ctx.caller #58.
    2. Native requests: behavior for native requests isn't defined or proven; that overlaps Support scoped native-to-web session exchange for private app UIs #16, which stays separate. Browser app tools (ui channel), MCP, delegated approval (approvedBy) and background work (workflow system caller) are covered.
  3. added a commit that references this issue on Oct 23, 2026
  4. shuv1337 commented on Oct 23, 2026

    @shuv1337
    OwnerAuthor

    Update 2026-10-23: saved-profile (runsAs) substitution is now proven by #60 (merged as 991910d29, test-only).

    #60: mcp-connections.spec.ts, scenario "Scoped connections run apps only as the selected accounts and link bare accounts" (scopedConnectionAccounts).

    • The owner shares the app with everyone.
    • A member saves an MCP connection whose runsAs names the owner's personal account.
    • POST /api/organizations/:org/mcp-connections returns 400 ConnectionAccessInvalid with reason account (not app, so app access passed and the account check refused it).
    • linkedAccountIds(member) stays [].
    • Passed on self-host E2E. bun run check passed. Fork CI (check, Socket) passed with no review comments. The scenario's Cloud target runs in deployed tests.

    Already in the same scenario:

    • Naming another account inside a call ($accounts) fails with AccountChoiceUnsupported (400).
    • An account linked in place of the connection's pinned account is refused, and its data never appears.

    Related scenarios:

    • scopedConnectionLinkSwappedMidExecution: an account swapped mid-execution is refused with a target refusal.
    • hostedMcpLinkHeldAccess: when account or app access is withdrawn while a save waits, the save gets ConnectionAccessInvalid and nothing is linked.

    Acceptance status. All five listed acceptance items now have passing E2E scenarios:

    • two principals;
    • substitution of member, organization, approver and saved profile;
    • revoked membership and sessions, hidden and unselected tools, and no capability expansion;
    • docs with an example;
    • public HTTP/MCP/browser surfaces.

    Not covered: native requests. The requested outcome also asks for defined behavior for native requests. There is no native-request surface yet; the native-to-browser session exchange is #16, which stays open and separate. I'm leaving this open for the owner to decide whether that part is satisfied by #16's tracking or must land first.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions