- add AGENTS.md and docs for issue tracker, triage labels, and domain docs - add CONTEXT.md glossary for the permission authorization domain - add research report on context ownership for forwarded bash asks - ignore .pi-subagents and .codegraph directories - remove pi-permission-ai-judge from settings packages
12 KiB
Research: context ownership for forwarded and subagent Bash asks
Scope
This report describes the installed runtime inspected on 2026-08-08:
@gotgenes/pi-permission-system24.0.0pi-subagents0.43.0- the current
packages/pi-permission-ai-judge/src/index.ts
Only first-party source, package documentation, and Pi API declarations are used. Product implementation remains out of scope.
Executive answer
An external permission-system Authorizer callback receives only (details, query, log). It receives no ask-time ExtensionContext. Any conversation it reads therefore belongs to the extension lifecycle context captured when that callback was registered.
For a direct primary-session Bash ask, the root permission-system composes its configured authorizer links before LocalUserAuthorizer. A Judge registered by the UI-present root may safely identify the captured sessionManager as the primary conversation.
A child ask has two distinct possible authorizer-chain phases:
- The child's own permission session composes child-local links before its terminal
ParentAuthorizer. - If the child reaches
ParentAuthorizer, the request is written to the serving session. The servingForwardedRequestServerresolves serving policy and, when the result is stillask, composes the serving session's links before its terminal authorizer.
A future Judge must therefore bind only from the UI-present root/service-owning lifecycle context. Otherwise a separately registered child link could judge with child context, or a registered in-process child could capture child context while accidentally registering its callback into the parent's process-global service.
For a forwarded ask handled by the root link, the captured conversation is the serving root's conversation, not the requester's child transcript. Link-visible child provenance is details.forwarding; there is no cross-session API for reading the child's conversation.
Proven call flows
Direct primary Bash ask
- The primary tool call reaches the primary permission gate.
- Deterministic policy returns
ask. AuthorizerSelection.escalate()resolves configured links and composes them before the selected terminal.- The root Judge link may return
allow,deny, ordefer. defercontinues toLocalUserAuthorizer; it is not a denial.
The Judge callback may use a root ExtensionContext captured at session_start, but that context is closure state rather than an argument to authorize().
Child ask forwarded to the root
- The child's deterministic policy returns
ask. - The child's
AuthorizerSelectionfirst runs links registered in that child session, if any. - If all links defer or no link exists, the child terminal
ParentAuthorizerresolves a target session, writes aForwardedPermissionRequest, and polls for a response. - The UI-present root's
ForwardingManagerdrains the target session inbox. ForwardedRequestServerresolves the child-fixed access intent against serving policy.- A serving-policy
allowordenycompletes without the root chain. A serving-policyaskis projected intoPromptPermissionDetailsand passed to the rootAuthorizerSelection. - Root links run before
LocalUserAuthorizer. A root Judge therefore sees root-captured conversation plus forwarded provenance, not child conversation. - The root writes the decision; the child receives it and resumes or blocks.
ParentAuthorizer always uses filesystem request/response transport, including registered in-process children. The registry resolves identity and parentage; it is not an in-memory response fast path.
Nested children
Depth alone does not determine the result. A request targeting a headless immediate parent will normally remain unserved and time out. A request routed directly to the UI-present root can still be served. The server warns on a detected multi-hop mismatch but deliberately continues processing a correctly targeted request.
What an authorizer link can actually see
| Evidence | Direct primary ask | Parent-side forwarded ask |
|---|---|---|
details.forwarding |
absent | requester agent/session, possibly nullable for version-skew data |
details.agentName |
active primary agent | requesting child agent |
details.surface / details.value |
usually derived fields are absent on the internal details object | child's explicit display projection |
details.accessIntent |
gate surface, match values, boundary value | same child-fixed facts projected by the server |
details.command |
winning Bash command unit | absent |
details.message |
formatted prompt; includes full Bash input text when it differs from the winning unit | same child-formatted prompt, prefixed with child identity by the server |
| raw tool input | unavailable | unavailable |
wire principal / requesterCwd |
unavailable | deliberately withheld from chain links |
ask-time ExtensionContext |
unavailable | unavailable |
For a Bash ask, details.accessIntent.matchValues[0] and direct details.command identify the permission-system command unit that caused ask. The full original Bash input is not preserved as a structured authorizer field; when it differs, it survives only inside the formatted details.message text. The later input-contract decision must not pretend that details.command is the full raw Bash invocation or silently parse an authorization-critical command from prose.
Registration and context-ownership hazards
The permissions service is stored under Symbol.for(...) on globalThis and registration stores an authorizer callback in the service owner's AuthorizerRegistry.
- A normal UI root publishes its service.
- A registered in-process child does not publish its own service, but still emits the ready event.
- Consequently, a consumer extension loaded in that child can resolve the parent's process-global service. If it registers, its child-captured closure is stored in the parent's registry. If the root already registered the same name, duplicate registration throws.
- A separate child process has separate
globalThisstate and may have its own service lifecycle; registering there would create a child-local link with child-captured context.
Therefore successful getPermissionsService() lookup does not prove that the current extension instance owns that service's session context.
Safe v0.1 ownership contract
- Register the AI Judge only from a lifecycle context proven to be the UI-present, non-subagent root. Do not register from headless or subagent lifecycle contexts.
- Snapshot and retain that root context/session identity as registration provenance. The callback must not infer ownership merely from the process-global service lookup.
- For
details.forwardingabsent, treat the captured root conversation as the requester's current conversation. - For
details.forwardingpresent, label the origin as forwarded and treat the captured conversation only as serving-root user-intent context. Never describe it as the child's conversation and never infer child-local dialogue that is not present. - Preserve
details.forwarding.requesterSessionIdandrequesterAgentNameas provenance. If forwarded provenance is missing, blank, orunknown, the Judge must return{ kind: "defer" }. - If registration provenance is not demonstrably the UI root, the captured session changed unexpectedly, or context ownership otherwise cannot be established, return
{ kind: "defer" }. - Judge
defermeans continue through the authorizer chain to human or terminal authority. The Judge must not synthesizeconfirmationUnavailable; only terminal authority owns unavailable-authority denial. - Ensure a logical forwarded request produces at most one Judge prediction. Root-only registration is the v0.1 mechanism that avoids child-then-parent duplicate judgments.
Whether serving-root conversation is sufficient evidence to grant a later Enforce-mode allow for a forwarded child request remains a product decision for Define the exact authorization input and conversation contract. The facts established here are that root user intent is available, child conversation is not, and the two must not be conflated.
Implications for the later input-contract ticket
- Add an explicit origin such as
localorforwarded_subagent. - Record the conversation owner separately from requester identity.
- For forwarded asks, carry only the link-visible projection:
details.forwarding,details.surface/value,details.accessIntent, and the formatted message. Do not claim access to the rawForwardedPermissionRequest,principal, orrequesterCwd. - Define structured handling for both the triggering Bash command unit and the unavailable full raw Bash invocation. If v0.1 requires the full invocation as structured data, permission-system must expose it; prose parsing is not an equivalent contract.
- Keep the user-intent priority explicit: root user messages are authoritative; child agent identity and the command are evidence, not instructions.
Evidence index
| Claim | Primary source |
|---|---|
Link callback receives only details, query, and log |
@gotgenes/pi-permission-system/src/authority/authorizer.ts, Authorizer.authorize |
| Links compose before each session's terminal | src/authority/authorizer-selection.ts, AuthorizerSelection.escalate; src/authority/authorizer-chain.ts, composeAuthorizerChain |
defer continues; deny is decisive |
src/authority/authorizer-chain.ts, decideFromVerdict |
| Child terminal writes and polls forwarded request | src/authority/approval-escalator.ts, ParentAuthorizer.waitForForwardedApproval and pollForForwardedResponse |
| Root policy resolves before root escalation | src/authority/forwarded-request-server.ts, resolveDecision |
| Forwarded details projection and disclosure boundary | src/authority/forwarded-request-server.ts, buildForwardedAskDetails and toAccessFacts |
| Link-visible details fields | src/authority/permission-prompter.ts, PromptPermissionDetails |
| Full Bash command appears only in formatted message when distinct | src/permission-prompts.ts, formatAskPrompt; src/handlers/gates/tool.ts, describeToolGate |
UI display derives from winning details.command |
src/permission-ui-prompt.ts, buildUiPrompt and directValue |
| Service is process-global | src/service.ts, SERVICE_KEY, publishPermissionsService, getPermissionsService |
| Registered in-process child skips publication but emits ready | src/service-lifecycle.ts, PermissionServiceLifecycle.activate |
| Duplicate authorizer names throw | src/authority/authorizer-registry.ts, AuthorizerRegistry.register |
| Only UI non-subagent sessions poll forwarded inboxes | src/authority/forwarding-manager.ts, ForwardingManager.start |
| Registry-first, environment-second target resolution | src/authority/permission-forwarding.ts, resolvePermissionForwardingTargetSessionId |
| Multi-hop mismatch warns but continues | src/authority/forwarded-request-server.ts, warnOnMultiHop |
| Current Judge captures no context | packages/pi-permission-ai-judge/src/index.ts, current session_start handler and registered callback |
Residual verification gap
The source establishes the registration hazard, but no runtime probe was run to observe extension lifecycle ordering when the future AI Judge is loaded into a registered in-process child. Before implementation acceptance, run one focused probe that logs, without conversation content:
- registering extension session ID and
hasUI; - service-owning session ID if exposed;
- callback invocation origin (
local/forwarded); - forwarded requester session ID.
The probe should confirm that the root-only guard prevents child registration and that one forwarded Bash ask creates exactly one Judge prediction at the serving root.