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:
@@ -141,3 +141,7 @@ dist
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
.vite/
|
||||
|
||||
# Vibe coding
|
||||
.pi-subagents/
|
||||
.codegraph/
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"packages": [
|
||||
"../packages/pi-permission-ai-judge"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# AGENTS.md
|
||||
|
||||
Guidance for AI agents working in this repo.
|
||||
|
||||
## Agent skills
|
||||
|
||||
### Issue tracker
|
||||
|
||||
Issues are tracked as Plane work items in the `pi-extensions` (`PIEXTENSIO`) project, via the Plane MCP tools. See `docs/agents/issue-tracker.md`.
|
||||
|
||||
### Triage labels
|
||||
|
||||
Five canonical triage roles map to labels of the same name (`needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`). See `docs/agents/triage-labels.md`.
|
||||
|
||||
### Domain docs
|
||||
|
||||
Single-context: one `CONTEXT.md` + `docs/adr/` at the repo root. See `docs/agents/domain.md`.
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
# Permission Authorization
|
||||
|
||||
This context describes how ambiguous coding-agent operations are reviewed before they may execute.
|
||||
|
||||
## Language
|
||||
|
||||
**Deterministic Permission Policy**:
|
||||
The rule-based authority that classifies an operation as allowed, denied, or requiring a decision.
|
||||
_Avoid_: Static judge
|
||||
|
||||
**Authorization Judge**:
|
||||
An independent reviewer that proposes a verdict for an operation the Deterministic Permission Policy could not decide.
|
||||
_Avoid_: Bash parser, safety classifier
|
||||
|
||||
**Shadow Mode**:
|
||||
An observation mode in which an Authorization Judge records a verdict without changing whether the operation executes.
|
||||
_Avoid_: Dry run
|
||||
|
||||
**Enforce Mode**:
|
||||
An authority mode in which selected Authorization Judge verdicts may directly determine whether an operation executes.
|
||||
_Avoid_: Production mode
|
||||
|
||||
**Defer**:
|
||||
A verdict stating that the available information or the judge itself is insufficient to decide, leaving the decision to the next authority.
|
||||
_Avoid_: Deny, error
|
||||
|
||||
**False Allow**:
|
||||
A Shadow Mode outcome in which the Authorization Judge proposes approval and the human reviewer rejects the same permission request.
|
||||
_Avoid_: False positive
|
||||
|
||||
**Requesting Session**:
|
||||
The session in which the operation requiring authorization originated. For a forwarded request, this is the child session.
|
||||
_Avoid_: Current session
|
||||
|
||||
**Serving Session**:
|
||||
The authority-bearing session that resolves a forwarded request and runs its configured Authorization Judge before the terminal human authority.
|
||||
_Avoid_: Parent context, current session
|
||||
|
||||
**Conversation Owner**:
|
||||
The session whose conversation entries are supplied to an Authorization Judge. It may differ from the Requesting Session for forwarded requests.
|
||||
_Avoid_: Requester
|
||||
@@ -0,0 +1,37 @@
|
||||
# Domain Docs
|
||||
|
||||
How the engineering skills should consume this repo's domain documentation when exploring the codebase.
|
||||
|
||||
## Before exploring, read these
|
||||
|
||||
- **`CONTEXT.md`** at the repo root.
|
||||
- **`docs/adr/`** — read ADRs that touch the area you're about to work in.
|
||||
|
||||
If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved.
|
||||
|
||||
## File structure
|
||||
|
||||
Single-context repo (this repo):
|
||||
|
||||
```
|
||||
/
|
||||
├── CONTEXT.md
|
||||
├── docs/adr/
|
||||
│ ├── 0001-<decision>.md
|
||||
│ └── 0002-<decision>.md
|
||||
└── packages/
|
||||
```
|
||||
|
||||
(If this repo ever grows into a genuinely large multi-package monorepo, switch to the multi-context layout: a root `CONTEXT-MAP.md` pointing at a `CONTEXT.md` per package, with `docs/adr/` for system-wide decisions and per-package ADR dirs. For now one context covers everything.)
|
||||
|
||||
## Use the glossary's vocabulary
|
||||
|
||||
When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.
|
||||
|
||||
If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`).
|
||||
|
||||
## Flag ADR conflicts
|
||||
|
||||
If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
|
||||
|
||||
> _Contradicts ADR-0001 (<decision>) — but worth reopening because…_
|
||||
@@ -0,0 +1,56 @@
|
||||
# Issue tracker: Plane
|
||||
|
||||
Issues and specs for this repo live as **work items** in the **`pi-extensions`** project (`PIEXTENSIO`) on Plane. Use the **Plane MCP tools** for all operations — these are available to skills running in this repo. There is no `gh` / git-remote coupling; Plane is independent of this repo's GitHub remote.
|
||||
|
||||
## Project & workspace
|
||||
|
||||
- **Project**: `pi-extensions` — identifier `PIEXTENSIO`, id `270cee3f-cf84-48c8-bd4f-0882689f2a87`.
|
||||
- All new issues go into this project. Look the id up with `plane_list_projects` if it ever changes.
|
||||
|
||||
## State vocabulary
|
||||
|
||||
States in this project (reference by name; resolve the id with `plane_list_states`):
|
||||
|
||||
| State | Group | Use for |
|
||||
| --------- | ---------- | --------------------------------------------- |
|
||||
| Backlog | backlog | default landing state for new issues |
|
||||
| Todo | unstarted | accepted, queued for work |
|
||||
| In Progress | started | actively being worked |
|
||||
| Done | completed | finished |
|
||||
| Cancelled | cancelled | discarded / wontfix-via-state |
|
||||
|
||||
A "closed" issue = `Done` or `Cancelled` (group `completed` / `cancelled`). "Open" = everything else (`plane_get_pql_reference` → `stateGroup IN openStates()`).
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Create an issue**: `plane_create_work_item(project_id, name, description_html=...)`. It lands in `Backlog` by default. For triage, also attach the `needs-triage` label (create the label with `plane_create_label` on first use — none exist yet).
|
||||
- **Read an issue**: `plane_retrieve_work_item(project_id, work_item_id, expand="assignees,labels,state")` — or `plane_retrieve_work_item_by_identifier("PIEXTENSIO-<n>")` when you only have the `PIEXTENSIO-N` id.
|
||||
- **List issues**: `plane_list_work_items(project_id, pql=..., expand="labels,state")`. Resolve label/state names to ids first (`plane_list_labels`, `plane_list_states`). PQL examples: `stateGroup IN openStates()`; `priority = "high"`; `labels = "<label-uuid>"`. Call `plane_get_pql_reference` for full syntax.
|
||||
- **Search by text**: `plane_search_work_items(query="...")` — matches name, sequence id, and project identifier (not the description body).
|
||||
- **Comment on an issue**: `plane_create_work_item_comment(project_id, work_item_id, comment_html="...")`.
|
||||
- **Apply / remove a label**: `plane_manage_work_item_label(project_id, work_item_id, add_label_id=..., remove_label_id=...)` — add/remove one label without replacing the list.
|
||||
- **Change state**: `plane_update_work_item(project_id, work_item_id, state=<state-id>)`.
|
||||
- **Close**: set state to `Done` (or `Cancelled` for wontfix).
|
||||
|
||||
## Pull requests as a triage surface
|
||||
|
||||
**PRs as a request surface: no.** PRs live on GitHub (`SikongJueluo/pi-extensions`) but are **not** treated as triage tickets. Only Plane work items go through triage. Flip this to `yes` and describe the GitHub-PR flow if you ever want external PRs in the queue.
|
||||
|
||||
## When a skill says "publish to the issue tracker"
|
||||
|
||||
Create a Plane work item via `plane_create_work_item` in the `pi-extensions` project.
|
||||
|
||||
## When a skill says "fetch the relevant ticket"
|
||||
|
||||
`plane_retrieve_work_item_by_identifier("PIEXTENSIO-<n>", expand="assignees,labels,state")`, plus `plane_list_work_item_comments` to read the discussion.
|
||||
|
||||
## Wayfinding operations
|
||||
|
||||
Used by `/wayfinder`. The **map** is a single work item with **child** work items as tickets.
|
||||
|
||||
- **Map**: a single work item labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. Create with `plane_create_work_item`, then `plane_create_label(project_id, "wayfinder:map", ...)` if the label doesn't exist, then attach it.
|
||||
- **Child ticket**: a work item whose `parent` is the map — `plane_create_work_item(..., parent=<map-id>)` or `plane_update_work_item(parent=<map-id>)`. Labels: `wayfinder:<type>` (`research`/`prototype`/`grilling`/`task`). Once claimed, assign the driving dev via `plane_manage_work_item_assignee`.
|
||||
- **Blocking**: native work-item relations are gated on this workspace's plan, so use a **body-text fallback** — put `Blocked by: PIEXTENSIO-<n>, PIEXTENSIO-<n>` at the top of the child body. A ticket is unblocked when every blocker is `Done`/`Cancelled`.
|
||||
- **Frontier query**: list the map's open children via `plane_list_work_items` filtered to the map's children and `stateGroup IN openStates()`; drop any with an open `Blocked by` line or an assignee; first in created order wins.
|
||||
- **Claim**: `plane_manage_work_item_assignee(project_id, work_item_id, add_user_id=<me>)` — resolve `<me>` with `plane_get_me`. The session's first write.
|
||||
- **Resolve**: `plane_create_work_item_comment` with the answer, then `plane_update_work_item(state=<Done-id>)`, then append a context pointer (gist + link) to the map's Decisions-so-far (edit the map's description via `plane_update_work_item`).
|
||||
@@ -0,0 +1,19 @@
|
||||
# Triage Labels
|
||||
|
||||
The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker (Plane, project `pi-extensions` / `PIEXTENSIO`).
|
||||
|
||||
| Label in mattpocock/skills | Label in our tracker | Meaning |
|
||||
| -------------------------- | -------------------- | ---------------------------------------- |
|
||||
| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
|
||||
| `needs-info` | `needs-info` | Waiting on reporter for more information |
|
||||
| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
|
||||
| `ready-for-human` | `ready-for-human` | Requires human implementation |
|
||||
| `wontfix` | `wontfix` | Will not be actioned |
|
||||
|
||||
When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.
|
||||
|
||||
## Plane notes
|
||||
|
||||
- These labels **do not exist yet** in the `pi-extensions` project. The `triage` skill should create each label with `plane_create_label(project_id, name=...)` on first use (Plane has no predefined triage labels), then attach it with `plane_manage_work_item_label`. Resolve label names to ids via `plane_list_labels`.
|
||||
- `wontfix` can be expressed either as a label **or** by moving the work item to the `Cancelled` state — prefer the label for explicit triage intent, and state for final closure.
|
||||
- Edit the right-hand column to match any other vocabulary you adopt later.
|
||||
@@ -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