Skip to content

feat: pass each project's trust to pi and name what trusting it loads - #690

Merged
danyaberezun merged 15 commits into
mainfrom
mcp-stack/2-project-trust
Oct 11, 2026
Merged

danyaberezun merged 15 commits into
mainfrom
mcp-stack/2-project-trust

Conversation

@danyaberezun

@danyaberezun danyaberezun commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Every session passes projectTrusted: true to pi, so pi loads every repository's own project resources
the moment a chat opens, trusted or not:

  • .pi/settings.json;
  • project extensions, which run code on this machine;
  • skills, prompts, themes and SYSTEM.md;
  • subagent definitions.

Only ThinkRail's committed skill aliases wait for the per-project trust grant.

The trust notice only counts those aliases. A repository whose risky content is a .pi/ extension or a
subagent definition therefore shows no notice at all.

Approach

  • A separate pi-level trust. The project record gets a second trust, piResourceTrust (granted /
    untrusted).
    • Where it applies. It is what reaches pi's SettingsManager wherever the host builds one for
      a project: sessions, pre-session loaders and skill listings, and subagent children.
    • Children. A child takes its trust from its parent context (ParentContext.isProjectTrusted in
      pi-delegation). ThinkRail answers the parent's loaded trust AND the current record, so a child
      never holds more than its parent or the record.
    • Alias trust. The existing grant for skill aliases stays as it is.
  • No grandfathering. A record without piResourceTrust reads as untrusted and asks once through the
    trust notice, instead of keeping code-running resources nobody consented to.
    • New projects start from pi's own decision: its trust.json (nearest ancestor), else its global
      defaultProjectTrust: "always"; otherwise, and when the store is unreadable, untrusted.
  • Disclosed consent.
    • project.setTrust({ trusted: true, resources: true }) is the only way to grant pi-level trust.
      Only screens that name what it loads send it: the Welcome notice, New Workspace and the Skills dialog
      (the in-chat place to grant it), all built on one shared derivation.
    • Older clients grant alias trust only. A grant on an already alias-trusted project keeps the
      acknowledged set, so aliases that arrived since still wait for review.
    • trusted: false revokes both; the notice offers Stop trusting whenever either grant is held.
  • Live chats follow. Trust applies before every load. A grant or revoke asks feat: upgrade pi to 1.1.0, gate session reloads and teardown, import the login env #689's reload gate to
    reload only the chats whose trust is stale: an idle chat now; a busy one finishes its turn and reloads
    at settlement. A revoke stops live subagents first. A chat still opening re-checks its trust when it
    registers. Every trust change publishes the project record, so clients stay in sync even when a reload
    fails and project.setTrust reports it.
  • A notice that names what it loads. project.trustSummary (PROJECT_TRUST_PROTOCOL_VERSION = 81)
    reports the alias skills and the kinds of pi resources present (piResources: ProjectResourceKind[]:
    extensions, settings, skills, prompts, themes, system prompt, subagents).
    • The notice. ProjectTrustNotice replaces ProjectSkillsNotice on Welcome and in New
      Workspace. For example: "This project ships 1 skill and its own Pi resources — off until you trust
      it", then exactly what trust loads ("Trust loads its prompts."), saying it runs code only when
      extensions or settings are present.
    • Older hosts. On an older host, the client falls back to project.aliasSkills, which means
      aliases only.

Changes

  • Contracts:
    • Project.piResourceTrust and ProjectTrustSummary;
    • the project.trustSummary method and the project.setTrust resources parameter;
    • ProjectResourceKind;
    • protocol v81 (+ pin; v79/v80 are main's subagent concurrency and turn changes), and a "Project trust" section in the contracts SPEC.
  • packages/server/src/projects: setPiTrustSeed, the seeding in openProject, and the two grants
    in setProjectTrust, which publishes the record.
  • packages/server/src/agent:
    • projectTrust.ts: piTrustsProjectPath and projectTrustSummary;
    • skillAdmission.ts: piResourceTrusted and admissionContextFor;
    • extensions.ts: trust applied before every load, and listing trust;
    • agentSessionManager.ts: buildSessionSettings(cwd, projectTrusted), applyPiResourceTrust, the
      registration re-check, and liveParentContext.isProjectTrusted;
    • delegation.ts: stopSessionChildren.
  • packages/pi-delegation: optional isProjectTrusted on ParentContext and ResourceContextInput;
    buildChildSettings receives that trust (building on fix(delegation): deliver the image autoResize override to subagent children #657's hook), and a child prepared across a trust
    change is refused. The pure-pi fallback in pi-subagents passes its ctx, so vanilla-pi children now
    inherit the session's trust too.
  • Host: project.setTrust with resources plus the gated reload; project.trustSummary; the trust
    seed; admissionContextFor in place of four hand-built contexts.
  • Web:
    • ProjectTrustNotice; chat/projectTrust.ts and chat/useProjectTrustSummary.ts, shared by Welcome,
      New Workspace and the Skills dialog (in chat/ because chat may not depend on panels);
    • selectSupportsProjectTrust.
  • Tests: a project's .pi/extensions code runs only under pi-level trust (both loader branches);
    e2e for subagent-definition-only projects (new-workspace), the Welcome grant/Stop-trusting flow
    (welcome) and the Skills dialog grant (skills-switches).
  • Docs: goal-and-requirements.md ("Trust is explicit").

Notes for reviewers:

  • The alias-name listing passes projectTrusted: false (alias roots are explicit paths), and pre-session skill listings
    now hide a repository's .pi/skills until pi-level trust is granted — hence the changed expectation in
    extensions.test.ts.
  • ThinkRail's own template picker still lists a repository's .pi/prompts regardless of trust: it reads text the user
    chooses to send and runs nothing. pi's own prompt-template loading follows pi-level trust.
  • The Stop-trusting confirmation names what actually stops: Pi resources and the reload of open chats only when pi-level
    trust was granted on a host that supports it; otherwise only new chats lose the project's skills.
  • The summary's kinds mirror pi's private list of trust-requiring .pi/ entries (minus mcp.json, which no ThinkRail
    session loads before feat(server): run the MCP servers from mcp.json in chats on pi's built-in engine #691); a test asserts each detected kind is also gated by pi's hasTrustRequiringProjectResources.
  • Stack note: feat(server): run the MCP servers from mcp.json in chats on pi's built-in engine #691 must adapt to piResources (it adds an MCP server count to the summary) and to the removed
    projectTrusted delegation binding.

Screenshots

Captured with the e2e harness's hermetic fixture project (one committed skill alias plus a .pi/agents subagent definition), 1440×900 at 2×, dark theme; the same throwaway scenario ran on main (before) and on this PR's tree (after).

Welcome trust notice — untrusted project

Before After
Welcome trust notice, before Welcome trust notice, after

Welcome trust notice — after trusting

Before After
Welcome after trusting, before Welcome after trusting, after

Welcome — pi already trusts the folder, alias skills still pending (Stop trusting stays reachable)

Before After
Welcome with pi-level trust from pi's store, before Welcome with pi-level trust from pi's store, after

Chat Skills dialog trust banner

Before After
Skills dialog trust banner, before Skills dialog trust banner, after

Related issues

No tracked issue. This is part of the MCP support stack (milestone 1); review and merge in order:

  1. feat: upgrade pi to 1.1.0, gate session reloads and teardown, import the login env #689 feat: upgrade pi to 1.1.0, gate session reloads and teardown, import the login env
  2. feat: pass each project's trust to pi and name what trusting it loads #690 feat: pass each project's trust to pi and name what trusting it loads ← this PR
  3. feat(server): run the MCP servers from mcp.json in chats on pi's built-in engine #691 feat(server): run the MCP servers from mcp.json in chats on pi's built-in engine
  4. feat(server): serve MCP server management, status and sign-in to clients #692 feat(server): serve MCP server management, status and sign-in to clients
  5. feat(web): track MCP status and list each chat's MCP servers in its resources #693 feat(web): track MCP status and list each chat's MCP servers in its resources
  6. feat: manage MCP servers in Settings #694 feat: manage MCP servers in Settings
  7. feat(web): render MCP tool calls, resources and tool_search as cards #695 feat(web): render MCP tool calls, resources and tool_search as cards

#689 has merged; this PR is rebased onto main. #691–#695 still sit on the previous head of this branch and need a restack.

Testing

On this PR's tree (after the rebase onto main and the review fixes):

  • bunx turbo run typecheck --filter='!@thinkrail/desktop' → 19/19; each review-fix commit also typechecks on its own
  • bun run test → 20/20 tasks
  • bun run lint → clean; check:deps, check:boundaries, check:seams, check:spec-surface, check:compiler → OK
  • bun run e2e → run twice; each run had one unrelated chat-rendering failure (chat-scroll.spec.ts:616, then the known activity-breadcrumbs.spec.ts:240 flake) that passes on focused reruns (--repeat-each=3: 6/6 and 12/12)
  • Focused live specs (real Central, bun run e2e:full e2e/subagents.live.spec.ts e2e/skills-reload.live.spec.ts e2e/new-workspace.live.spec.ts) → 6 passed. The complete bun run e2e:full suite was not rerun.

Desktop: @thinkrail/desktop's typecheck still cannot run here: its electrobun prepare step waits on a cache lock held by a running electrobun dev. The same lock blocks the pre-commit hook, so the review-fix commits were made with --no-verify after running the hook's other checks by hand. This change touches no desktop file.

Checklist

  • Fast gates pass: bun run lint, bun run typecheck, bun run test
  • E2E suite passes for app-affecting changes (bun run e2e, or bun run e2e:full when touching agent behavior) — see Testing: one unrelated flake per full run
  • Before/after screenshots are included for frontend changes, or marked not applicable
  • Relevant SPEC.md / top-level specs updated to reflect any boundary, contract, or behavior change
  • I have read the Contributing guide and agree to the Code of Conduct

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Requesting changes — please address the blocking inline finding.

Comment thread packages/server/src/host/handlers.ts Resolved
@danyaberezun
danyaberezun force-pushed the mcp-stack/2-project-trust branch from aaa9701 to f187c9d Compare October 9, 2026 10:28

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved — ready to merge.

@danyaberezun
danyaberezun force-pushed the mcp-stack/2-project-trust branch from f187c9d to e80df86 Compare October 9, 2026 12:12

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Requesting changes — please address the blocking inline finding.

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Requesting changes — please address the blocking inline finding.

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Requesting changes — please address the blocking inline findings.

@OLavrik

Copy link
Copy Markdown
Collaborator

Additional observations (non-blocking)

Beyond the existing review threads, here are a few more considerations:

1. New sessions during applyPiResourceTrust await

File: packages/server/src/agent/agentSessionManager.ts — applyPiResourceTrust()

While stopSessionChildren().settled and requestSessionReload() are awaited in the loop, createSession() can complete with the OLD trust (the project record is already updated, but we haven't returned yet). A session created during this window will have stale trust.

Consider capturing a generation token before the loop or re-checking new sessions after.


2. Abort reason "user" is semantically incorrect

File: packages/server/src/agent/agentSessionManager.ts — stopSessionChildren(..., "user")

Child transcripts will show "Stopped by user" rather than indicating trust was revoked. Consider a distinct reason like "trust-revoked" for clarity.


3. piResourceTrustFor closure may return stale value

File: packages/server/src/agent/delegation.ts — projectTrusted: () => piResourceTrustFor(workspaceId)

This is captured in a closure. If skillAdmissionResolver caches the old value until the parent reloads, a child created between setProjectTrust and the parent reload will get stale trust.


4. Partial failure leaves inconsistent state

File: packages/server/src/agent/agentSessionManager.ts — applyPiResourceTrust()

If session 1 reloads successfully but session 2's reload throws, the function returns dispositions without indicating which sessions failed. The caller (project.setTrust) reports success while session 2 still runs with old trust.


5. projectTrustSummary scans FS on every call

File: packages/server/src/agent/projectTrust.ts — projectTrustSummary()

No caching — each call scans the filesystem. If .pi/ changes between calls, the UI may show inconsistent state. Not critical but worth noting for future optimization.

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Requesting changes — please address the blocking inline finding.

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Requesting changes — please address the blocking inline finding.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

All blocking issues resolved. Non-blocking observations acknowledged — they don't block the merge.

Fixed:

  • Child preparation race (revalidation after assemble)
  • Trust store read failure (undefined vs null distinction)
  • Reload failure handling (error notify to user)

Non-blocking (noted for future):

  • Abort reason semantics
  • Sessions created during await window
  • Partial failure reporting
  • No caching in trustSummary

LGTM ✅

Base automatically changed from mcp-stack/1-pi-lifecycle to main October 11, 2026 07:13
@danyaberezun
danyaberezun force-pushed the mcp-stack/2-project-trust branch from 92ee01b to f43e715 Compare October 11, 2026 07:13

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved — ready to merge.

- Project records carry pi-level trust (piResourceTrust), applied at every
  SettingsManager site including subagent children; existing projects are
  grandfathered unless pi's trust.json denies them.
- project.setTrust({ resources: true }) is the only pi-level grant, sent by
  surfaces that name what it loads; trusted: false revokes both grants.
- Grants and revokes reach live chats through the reload gate.
- project.trustSummary (protocol v78) backs ProjectTrustNotice, which
  replaces ProjectSkillsNotice and gains Stop trusting.
…is revoked

Children capture the project's pi-level trust at creation and pi-subagents
retains them across a parent reload, so a revoke now aborts every live child
of a session whose trust went stale, before that session's reload is
requested. The Stop-trusting confirmation says so.
…t store

piProjectTrustDecision distinguishes no saved decision (null) from a store
it could not read (undefined); the migration leaves such a record without
the field, which reads as untrusted, and seeds it on a later load.
…t change reports

stopSessionChildren now exposes the settlement of every abort (a failed
abort disposes the child); the trust revoke awaits it before requesting the
parent reload, while Stop all keeps returning its count at once.
… trust change could not reload

pi-delegation revalidates the projectTrusted binding after assembling a
child and disposes one whose settings captured a trust that has since
changed; applyPiResourceTrust reports a failed reload instead of swallowing
it, and project.setTrust fails naming the chats still on the previous trust.
…r a settings change

A trust change on a busy chat defers its reload; if that reload later
fails, the chat now receives an error notify asking to close or reload it
instead of silently keeping the previous resources.
@danyaberezun
danyaberezun force-pushed the mcp-stack/2-project-trust branch from f43e715 to 7dbc52a Compare October 11, 2026 20:01
@danyaberezun

Copy link
Copy Markdown
Collaborator Author

Review round: rebase + fixes

Rebased onto main (protocol is now v81: main took v79/v80; compiler census baseline and #703's rounded-sm carried over), then addressed a fresh review of the whole change plus Olga Lavrichenko (@OLavrik)'s observations (each answered on its own thread above):

  • de661c5 — a pi-level grant on an already alias-trusted project no longer re-snapshots acknowledgedSkills, so skills that arrived after the first trust stay pending review instead of being approved by the "its own Pi resources" grant.
  • 9ecb953 — a subagent's trust comes from its parent context (ParentContext.isProjectTrusted = parent's loaded trust AND the record); the projectTrusted binding is removed. Closes the busy-parent race where a child created during a deferred grant kept running through a later revoke. Pure-pi children now inherit their session's trust too.
  • 0cd6d82 — a chat that finishes opening while trust changes reloads into the new trust before createSession returns.
  • 082c669 — trust changes publish the project record, so a project.setTrust that fails on a chat reload no longer leaves clients showing the old trust.
  • bc8d7ef — no grandfathering: a record without piResourceTrust reads as untrusted and asks once. New projects seed from pi's trust.json, else its defaultProjectTrust: "always".
  • 21a0790 — the agent SPEC no longer claims nothing runs under revoked trust once setTrust returns: a busy chat finishes its turn and reloads at settlement (as the Stop-trusting copy already says).
  • e0e609a — ProjectTrustSummary.piResources: ProjectResourceKind[] replaces nativeResources: boolean; the notice names exactly what is there ("Trust loads its prompts.") and mentions code only for extensions/settings.
  • f5b57bf — the Skills dialog uses the same notice and grants resources: true (the in-chat path to pi-level trust); Stop trusting shows whenever either grant is held.
  • 7dbc52a — tests: .pi/extensions code runs only under pi-level trust (both loader branches); e2e for the Welcome grant/revoke flow and the Skills dialog grant.

Body, screenshots and Testing are updated. Heads-up for the stack: #691 needs to adapt to piResources and the removed delegation binding when it is restacked.

@danyaberezun
danyaberezun added this pull request to the merge queue Oct 11, 2026
Merged via the queue into main with commit 9518635 Oct 11, 2026
17 checks passed
@danyaberezun
danyaberezun deleted the mcp-stack/2-project-trust branch October 11, 2026 20:22
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.

2 participants