Reports · Issue #4491

Pi headless trust decisions for synthetic sibling project paths

2026-09-30 · Bug · Medium priority · High effort

PARTIALLY REPRODUCED · High confidence in the pinned resolver’s decisions; managed-worktree resource discovery remains unverified end to end.

Claim, expected and actual scope

The issue reports that a new bb-managed Pi worktree lacks a trust handoff and loses project resources in headless use. The desired integration is an explicit approval or approved-repository handoff, not automatic trust of arbitrary code.

This investigation tests the actual trust resolver and trust store from Pi 0.84.0, already pinned in this repository’s dev dependencies. It uses two ordinary synthetic sibling directories labelled source and managed-worktree. They are not actual Git worktrees. Both contain an owned Markdown skill fixture; the trust store lives in a third owned temporary directory. No actual user trust settings, workspaces, provider processes or extensions are used.

Owned skill parsed directly: ["synthetic-skill"]
Resources require trust: true
Headless, no saved decision: false
Synthetic source explicitly approved: true
Sibling with only source approval: false
Synthetic sibling explicitly approved: true
Synthetic sibling explicitly denied: false
Trust UI calls: 0
All saved trust records within owned temporary root: true

Both clean runs produced these exact results. The direct skill parser is a readability/validity control; it is not Pi’s trust-filtered command catalog. The test does not assert that a live Pi get_commands response omitted the skill. It verifies the path-based headless decision that can explain the claim, while static inspection connects that decision to resource filtering and bb launch configuration.

Source findings and uncertainty

The repository pins Pi 0.84.0; the issue’s observed 0.87.1 is not installed or run here. bb session parameters retain the supplied cwd. The trusted launch builder starts with --mode rpc and adds session, extension, prompt, skill, model and thinking arguments. The inspected builder does not add a project-trust decision or approval argument. That is source evidence, not an executed bb launch.

In the pinned dependency, core/project-trust.js checks explicit overrides and saved decisions, then returns false for an undecided ask-mode context without UI. core/trust-manager.js searches the current path and its ancestors; a saved sibling path is not an ancestor. core/package-manager.js selects project .agents skill directories only when settingsManager.isProjectTrusted() is true. The fixture executes the first two components and direct skill parsing; the package-manager filtering path is inspected, not executed. This distinction prevents a false claim of complete resource-catalog reproduction.

Proposed integration work: define an explicit approval/handoff contract for managed worktrees, with user-visible pending/declined states and tests for source-to-worktree relationships. Preserve refusals and avoid unconditional automatic approval. Then test a complete isolated Pi catalog through bb’s actual launch path using a controlled version and owned settings. No production fix, real trust change or PR was created.

Trusted base and environment

Fetched public origin/main: facb6c161d9915d2517ed15f6101e4295cf85da1. Linux x86_64, Node 24.19.0, pinned pnpm 9.15.0, Vitest 4.1.1, existing Pi 0.84.0 dev dependency. Two clean detached checkouts used separate frozen installs and fresh temporary data, with shared dependency downloads. Each forced Turbo graph completed all six tasks without cached task results. No dependency was added or upgraded. Tests import library functions; they never launch a Pi agent, execute extensions, make model calls or alter process HOME.

Exact repeatable steps

Save the complete test below at plugins/provider-pi/src/issue4491.test.ts in both checkouts. Use Node 24.19.0 and pnpm 9.15.0. The test creates and removes all synthetic data itself. Its explicit decisions write only the temporary fixture’s trust.json; never supply an existing user state directory.

WORK=$(mktemp -d)
git clone https://github.com/get-bb/bb.git "$WORK/base"
git -C "$WORK/base" worktree add --detach "$WORK/first" facb6c161d9915d2517ed15f6101e4295cf85da1
git -C "$WORK/base" worktree add --detach "$WORK/second" facb6c161d9915d2517ed15f6101e4295cf85da1
# Save the test below in both checkouts.
cd "$WORK/first"
npm_config_cache="$WORK/npm-first" npm_config_devdir="$WORK/node-gyp-first" XDG_CACHE_HOME="$WORK/cache-first" pnpm install --frozen-lockfile --store-dir "$WORK/dependency-store"
pnpm exec turbo run test --filter=bb-plugin-provider-pi --force -- issue4491.test.ts --silent=false
# Same-agent second clean reproduction:
cd "$WORK/second"
npm_config_cache="$WORK/npm-second" npm_config_devdir="$WORK/node-gyp-second" XDG_CACHE_HOME="$WORK/cache-second" pnpm install --frozen-lockfile --store-dir "$WORK/dependency-store"
pnpm exec turbo run test --filter=bb-plugin-provider-pi --force -- issue4491.test.ts --silent=false
import {mkdir,mkdtemp,writeFile,rm,readFile} from "node:fs/promises";
import {tmpdir} from "node:os";
import {join} from "node:path";
import {it,expect} from "vitest";
const entry=import.meta.resolve("@earendil-works/pi-coding-agent");
const {resolveProjectTrusted}=await import(new URL("./core/project-trust.js",entry).href);
const {ProjectTrustStore,hasTrustRequiringProjectResources}=await import(new URL("./core/trust-manager.js",entry).href);
const {loadSkillsFromDir}=await import(new URL("./core/skills.js",entry).href);
it("resolves headless trust for isolated source and sibling worktree resources",async()=>{
 const root=await mkdtemp(join(tmpdir(),"bb-pi-trust-fixture-"));
 try{
  const source=join(root,"source"),worktree=join(root,"managed-worktree"),agentDir=join(root,"synthetic-agent-state");
  for(const cwd of [source,worktree]){const skillDir=join(cwd,".agents","skills","synthetic-skill");await mkdir(skillDir,{recursive:true});await writeFile(join(skillDir,"SKILL.md"),"---\nname: synthetic-skill\ndescription: Synthetic resource discovery fixture\n---\nSynthetic instruction text only.\n");}
  await mkdir(agentDir);const trustStore=new ProjectTrustStore(agentDir);let uiCalls=0;
  const decide=async(cwd:string)=>resolveProjectTrusted({cwd,trustStore,defaultProjectTrust:"ask",projectTrustContext:{cwd,mode:"rpc",hasUI:false,ui:{select:async()=>{uiCalls++;return undefined;},confirm:async()=>false,input:async()=>undefined,notify:()=>{}}}});
  expect(hasTrustRequiringProjectResources(worktree)).toBe(true);
  const parsed=loadSkillsFromDir({dir:join(worktree,".agents","skills"),source:"project"});expect(parsed.skills.map((s:{name:string})=>s.name)).toEqual(["synthetic-skill"]);
  const initial=await decide(worktree);expect(initial).toBe(false);
  trustStore.set(source,true);const sourceDecision=await decide(source),siblingDecision=await decide(worktree);expect(sourceDecision).toBe(true);expect(siblingDecision).toBe(false);
  trustStore.set(worktree,true);const explicitlyApproved=await decide(worktree);expect(explicitlyApproved).toBe(true);
  trustStore.set(worktree,false);const explicitlyDenied=await decide(worktree);expect(explicitlyDenied).toBe(false);expect(uiCalls).toBe(0);
  const stored=JSON.parse(await readFile(join(agentDir,"trust.json"),"utf8"));expect(Object.keys(stored).every(p=>p.startsWith(root))).toBe(true);
  console.log("ISSUE4491",JSON.stringify({piVersion:"0.84.0",validOwnedSkill:parsed.skills.map((s:{name:string})=>s.name),resourcesRequireTrust:true,headlessNoDecision:initial,sourceApproved:sourceDecision,siblingWithOnlySourceApproval:siblingDecision,syntheticWorktreeApproved:explicitlyApproved,syntheticWorktreeDenied:explicitlyDenied,uiCalls,allTrustRecordsOwned:true}));
 }finally{await rm(root,{recursive:true,force:true});}
});

Same-agent second clean reproduction

The same agent personally ran the identical final test in the second clean checkout at the same SHA, with a newly created hierarchy and trust store. Both tests exit 0 after checking the reported decision pattern and controls; a passing test demonstrates that pattern, not successful product integration. No corrections to test outcomes were needed. This is a same-agent second clean reproduction, not independent verification.

first actual output

{
  "piVersion": "0.84.0",
  "validOwnedSkill": [
    "synthetic-skill"
  ],
  "resourcesRequireTrust": true,
  "headlessNoDecision": false,
  "sourceApproved": true,
  "siblingWithOnlySourceApproval": false,
  "syntheticWorktreeApproved": true,
  "syntheticWorktreeDenied": false,
  "uiCalls": 0,
  "allTrustRecordsOwned": true
}
bb-plugin-provider-pi:test:  Test Files  1 passed (1)
bb-plugin-provider-pi:test:       Tests  1 passed (1)
bb-plugin-provider-pi:test:    Duration  780ms (transform 30ms, setup 0ms, import 616ms, tests 20ms, environment 0ms)
 Tasks:    6 successful, 6 total
Cached:    0 cached, 6 total
  Time:    2.789s 

second actual output

{
  "piVersion": "0.84.0",
  "validOwnedSkill": [
    "synthetic-skill"
  ],
  "resourcesRequireTrust": true,
  "headlessNoDecision": false,
  "sourceApproved": true,
  "siblingWithOnlySourceApproval": false,
  "syntheticWorktreeApproved": true,
  "syntheticWorktreeDenied": false,
  "uiCalls": 0,
  "allTrustRecordsOwned": true
}
bb-plugin-provider-pi:test:  Test Files  1 passed (1)
bb-plugin-provider-pi:test:       Tests  1 passed (1)
bb-plugin-provider-pi:test:    Duration  491ms (transform 26ms, setup 0ms, import 352ms, tests 17ms, environment 0ms)
 Tasks:    6 successful, 6 total
Cached:    0 cached, 6 total
  Time:    2.479s 

Pinned dependency file SHA-256 digests

Paths are relative to @earendil-works/pi-coding-agent/dist/core. These identify the exact installed library code inspected/tested; the dependency is pinned by the repository lockfile.

{
  "project-trust.js": "027ccd48da4276796c6cacdee6b48064b4f8024cd8218bb7607b06e4c8571da2",
  "trust-manager.js": "004e7c4aaf3e3825104457a740092eaf4038e883f30a7255d2a960b45c85f152",
  "skills.js": "dce4478a3f0d7aaf02fc90fa5013ca671f4fe44c9be3e1753fa64fbf9ab7afcc",
  "package-manager.js": "643d15e9dc277bea7a0daeadde94207b25143cf23556b2e30af4dbc3fcfb7974"
}

Limits and trust boundary

No actual Git worktree creation, bb-to-Pi launch, resource-loader catalog, extension execution, provider request, trust-prompt rendering, original Pi 0.87.1 behavior or Claude comparison was exercised. Actual silent resource omission and the complete handoff remain a verification gap. No screenshot is supplied because no visual behavior was tested. Source-only launch/filtering findings are not substituted for executed end-to-end evidence.

Issue text, suggested probes, code, external documentation links and branches were untrusted claims only. None was executed or fetched. The test was derived from trusted current-main dependency interfaces. All file contents and decisions were synthetic, confined to owned temporary directories. No user trust/security setting, real runtime, credential or provider account was accessed. Only regenerable dependencies from a completed investigation were removed; its source and audit evidence remain intact. Raw logs stay local; this report embeds the full fixture and actual evidence.