mirror of
https://github.com/SikongJueluo/pi-extensions.git
synced 2026-10-05 11:52:55 +08:00
docs: add agent guidance and permission research docs
- 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
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
# Research: context ownership for forwarded and subagent Bash asks
|
||||
|
||||
## Scope
|
||||
|
||||
This report describes the installed runtime inspected on 2026-08-08:
|
||||
|
||||
- `@gotgenes/pi-permission-system` 24.0.0
|
||||
- `pi-subagents` 0.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:
|
||||
|
||||
1. The child's own permission session composes child-local links before its terminal `ParentAuthorizer`.
|
||||
2. If the child reaches `ParentAuthorizer`, the request is written to the serving session. The serving `ForwardedRequestServer` resolves serving policy and, when the result is still `ask`, 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
|
||||
|
||||
1. The primary tool call reaches the primary permission gate.
|
||||
2. Deterministic policy returns `ask`.
|
||||
3. `AuthorizerSelection.escalate()` resolves configured links and composes them before the selected terminal.
|
||||
4. The root Judge link may return `allow`, `deny`, or `defer`.
|
||||
5. `defer` continues to `LocalUserAuthorizer`; 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
|
||||
|
||||
1. The child's deterministic policy returns `ask`.
|
||||
2. The child's `AuthorizerSelection` first runs links registered in that child session, if any.
|
||||
3. If all links defer or no link exists, the child terminal `ParentAuthorizer` resolves a target session, writes a `ForwardedPermissionRequest`, and polls for a response.
|
||||
4. The UI-present root's `ForwardingManager` drains the target session inbox.
|
||||
5. `ForwardedRequestServer` resolves the child-fixed access intent against serving policy.
|
||||
6. A serving-policy `allow` or `deny` completes without the root chain. A serving-policy `ask` is projected into `PromptPermissionDetails` and passed to the root `AuthorizerSelection`.
|
||||
7. Root links run before `LocalUserAuthorizer`. A root Judge therefore sees root-captured conversation plus forwarded provenance, not child conversation.
|
||||
8. 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 `globalThis` state 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
|
||||
|
||||
1. 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.
|
||||
2. Snapshot and retain that root context/session identity as registration provenance. The callback must not infer ownership merely from the process-global service lookup.
|
||||
3. For `details.forwarding` absent, treat the captured root conversation as the requester's current conversation.
|
||||
4. For `details.forwarding` present, 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.
|
||||
5. Preserve `details.forwarding.requesterSessionId` and `requesterAgentName` as provenance. If forwarded provenance is missing, blank, or `unknown`, the Judge must return `{ kind: "defer" }`.
|
||||
6. If registration provenance is not demonstrably the UI root, the captured session changed unexpectedly, or context ownership otherwise cannot be established, return `{ kind: "defer" }`.
|
||||
7. Judge `defer` means continue through the authorizer chain to human or terminal authority. The Judge must not synthesize `confirmationUnavailable`; only terminal authority owns unavailable-authority denial.
|
||||
8. 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 `local` or `forwarded_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 raw `ForwardedPermissionRequest`, `principal`, or `requesterCwd`.
|
||||
- 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.
|
||||
Reference in New Issue
Block a user