mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 16:45:22 +00:00
292 lines
30 KiB
Markdown
292 lines
30 KiB
Markdown
---
|
|
issue: 635
|
|
issue_title: "Forwarded accessIntent is dropped before Authorizer Chain escalation"
|
|
---
|
|
|
|
# Carry the forwarded access facts through to Authorizer Chain escalation
|
|
|
|
## Release Recommendation
|
|
|
|
**Release:** ship independently
|
|
|
|
Issue #635 is not a numbered step in `docs/architecture/architecture.md`'s improvement roadmap, and that roadmap carries no `Release:` batch annotations, so there is no batch to hold this behind.
|
|
It is a self-contained defect fix in the serving-node escalation path.
|
|
|
|
It ships as a **breaking** change (`fix!:`), so it cuts a major release: `23.0.3` → `24.0.0`.
|
|
|
|
## Problem Statement
|
|
|
|
A subagent child that cannot answer an `ask` forwards the request to its parent (serving) session.
|
|
Since [#596], the request carries a structured `ForwardedAccessIntent` — the child-fixed access facts (`surface`, `matchValues`, `boundaryValue`) plus requester identity (`requesterCwd`, `principal`).
|
|
Since [#597], `ForwardedRequestServer.resolveDecision()` resolves that intent against the serving node's own composed ruleset.
|
|
|
|
But when resolution lands on `ask`, `buildForwardedAskDetails()` reconstructs `PromptPermissionDetails` from the request's **display** fields only — `message`, `surface`, `value`, `forwarding`, `sessionApproval` — and drops `request.accessIntent` on the floor.
|
|
`PromptPermissionDetails` already declares an optional `accessIntent?: ForwardedAccessFacts` field, which every local gate populates; only the serving-node reconstruction leaves it empty.
|
|
|
|
Two consequences follow, both defects against already-accepted design:
|
|
|
|
1. **An Authorizer Chain link sees no structured evidence for a forwarded ask.**
|
|
[ADR 0008]'s composition section states that once both tracks land, "a serving node's chain links … review forwarded asks against the child-fixed fact set — honest evidence, not a parent-side re-derivation."
|
|
Today a link receives display strings and a formatted UI message.
|
|
It cannot safely parse display text, and it must not reconstruct the child's path from the parent's cwd — the exact re-derivation [ADR 0008] forbids.
|
|
So a path-aware link has no option but to `defer` every forwarded `write`/`edit`, even when canonical path evidence would let it decide conservatively.
|
|
|
|
2. **The bounded-delegation checkpoint silently under-applies to forwarded asks.**
|
|
`delegation-envelope.ts` decides exclusion with `details.accessIntent?.surface ?? details.surface`, preferring the gate-authoritative surface and falling back to the display surface.
|
|
For a forwarded ask the gate surface is absent, so the checkpoint reads the display surface — the child's **tool name** (`write`), never `path`/`external_directory`.
|
|
A registered allow-capable link's `allow` on a forwarded `path`-gate ask is therefore honored, where the identical ask made locally in the parent is capped to `defer` ([ADR 0007] §5).
|
|
Forwarding is currently an escalation path around the operator's own delegation boundary.
|
|
|
|
The same line of code causes both: populating `accessIntent` fixes the evidence gap and closes the checkpoint escape together.
|
|
|
|
## Goals
|
|
|
|
- Carry the child-fixed access facts (`surface`, `matchValues`, `boundaryValue`) from `request.accessIntent` onto the `PromptPermissionDetails` the serving node escalates.
|
|
- Keep the disclosure boundary explicit: `requesterCwd` and `principal` do **not** cross onto the prompt details, and neither do raw tool arguments or change bodies.
|
|
The requester identity a link legitimately needs is already on `details.forwarding`.
|
|
- Accept, pin, and document the resulting bounded-delegation tightening: a chain link's `allow` on a forwarded `path` / `external_directory` ask is now capped to `defer`.
|
|
This is a **breaking change** — it ships as `fix(pi-permission-system)!:` with a `BREAKING CHANGE:` footer and a migration note.
|
|
- Record the cross-issue principle this fix instantiates — *high-fidelity in-process seams, minimal-but-correlatable broadcasts* — so [#610]'s planning session inherits a decided frame rather than re-deliberating it.
|
|
|
|
## Non-Goals
|
|
|
|
- **[#610] (make UI prompt decisions correlatable in the serving session).**
|
|
It shares this issue's root cause — the serving node reconstructs a degraded projection of the forwarded request — but it changes the public `permissions:decision` event contract, adds a parent-side emit point, and carries a `pkg:pi-subagents` label.
|
|
This plan contributes only the shared principle note; the event work stays with [#610].
|
|
- **[#620] (allow-capable opaque-bash adjudicator).**
|
|
[#620] will relax the whole-`path` exclusion to a secret-shaped one, which narrows the tightening this plan lands.
|
|
That refinement is [#620]'s to make; nothing here changes `DELEGATION_EXCLUDED_SURFACES`.
|
|
- **The `permissions:ui_prompt` broadcast payload.**
|
|
`buildUiPrompt` reads only `surface`/`value`/`forwarding` and never touches `accessIntent`, so the forwarded broadcast is byte-identical after this change.
|
|
`details.surface` is not repointed at the gate surface — that would degrade the [#292] non-degraded-broadcast contract.
|
|
- **`ServingPolicy.resolve` / recorded-authority resolution.**
|
|
`resolveDecision`'s `request.accessIntent`-presence gate and its `ask` floor on absence ([ADR 0008] §4) are untouched.
|
|
- **The multi-surface fact set.**
|
|
[ADR 0008] records it as an explicitly deferred edge; a multi-surface child decision still floors to `ask`.
|
|
|
|
## Background
|
|
|
|
Relevant modules, in the order a forwarded ask traverses them:
|
|
|
|
| Module | Role |
|
|
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| `src/handlers/gates/*.ts` | Every gate populates `promptDetails.accessIntent` via `accessFactsFromPath` / `accessFactsFromValue` (`gates/helpers.ts`). |
|
|
| `src/authority/approval-escalator.ts` | `ParentAuthorizer.authorize()` reads `details.accessIntent` and `buildForwardedRequest` completes it into a `ForwardedAccessIntent` by stamping `requesterCwd` + `principal`. |
|
|
| `src/authority/permission-forwarding.ts` | Declares `ForwardedAccessFacts` (the three fields) and `ForwardedAccessIntent extends ForwardedAccessFacts` (plus identity). |
|
|
| `src/authority/forwarding-io.ts` | `asForwardedAccessIntent` tolerantly narrows the wire field on read. |
|
|
| `src/authority/forwarded-request-server.ts` | `resolveDecision` resolves the intent; `buildForwardedAskDetails` reconstructs the ask details — **the defect site**. |
|
|
| `src/authority/authorizer-selection.ts` | `escalate` composes the chain, wrapping each link in `encloseInDelegationEnvelope`. |
|
|
| `src/authority/delegation-envelope.ts` | The checkpoint; reads `details.accessIntent?.surface ?? details.surface`. |
|
|
|
|
Which gate raises a forwarded ask determines whether the tightening applies at all:
|
|
|
|
| Raising gate | `accessIntent.surface` | Display `surface` | Envelope before | Envelope after |
|
|
| ------------------------------------- | ---------------------- | ----------------- | ---------------- | -------------- |
|
|
| `bash` command gate | `bash` | `bash` | not excluded | not excluded |
|
|
| Per-tool gate (a `write:` rule fired) | `write` | `write` | not excluded | not excluded |
|
|
| Cross-cutting `path` gate | `path` | `write` | **not excluded** | **excluded** |
|
|
| `external_directory` gate | `external_directory` | tool name | **not excluded** | **excluded** |
|
|
|
|
Only the last two rows change, and only once an allow-capable link is registered and named in `authorizerChain`.
|
|
The shipped first-party link (`packages/pi-permission-model-judge`) is deny-first; [ADR 0007] describes the checkpoint as dormant today.
|
|
|
|
Constraints from `AGENTS.md` and the package skill that bear on this plan:
|
|
|
|
- Breaking commits use `!` **after** the scope: `fix(pi-permission-system)!:`.
|
|
- `docs/architecture/` and `docs/decisions/` are **not** in the package's `files` allowlist, so a link from a shipped doc (`README.md`, `docs/configuration.md`, `docs/migration/*.md`) into either must be an absolute GitHub URL.
|
|
- Architecture module-tree entries describe current behavior; cite an issue only when the ref encodes an active constraint.
|
|
- Do not put `Closes #N` in commit messages; use `Refs #635`.
|
|
|
|
## Design Overview
|
|
|
|
### The projection
|
|
|
|
`buildForwardedAskDetails` gains one field, produced by an explicitly-typed module-private projection rather than a spread:
|
|
|
|
```typescript
|
|
/**
|
|
* Project the request's `ForwardedAccessIntent` down to the child-fixed access
|
|
* facts an Authorizer may see.
|
|
*
|
|
* The disclosure boundary is deliberate and is why this is a field-by-field
|
|
* projection, not a spread: `requesterCwd` and `principal` stay off the ask
|
|
* details. A link that needs requester identity reads `details.forwarding`,
|
|
* which already carries the agent name and session id.
|
|
*
|
|
* The explicit `ForwardedAccessFacts` return type makes the boundary
|
|
* compile-checked: a field added to `ForwardedAccessFacts` fails `tsc` here
|
|
* until it is deliberately projected or deliberately withheld.
|
|
*/
|
|
function toAccessFacts(intent: ForwardedAccessIntent): ForwardedAccessFacts {
|
|
return {
|
|
surface: intent.surface,
|
|
matchValues: intent.matchValues,
|
|
boundaryValue: intent.boundaryValue,
|
|
};
|
|
}
|
|
```
|
|
|
|
and the details builder adds a conditional spread matching the existing `sessionApproval` idiom, so a version-skew request without the field carries no `accessIntent` key at all (not an explicit `undefined`):
|
|
|
|
```typescript
|
|
...(request.accessIntent
|
|
? { accessIntent: toAccessFacts(request.accessIntent) }
|
|
: {}),
|
|
```
|
|
|
|
Absence must stay absence: `delegation-envelope.ts` fail-safes an undetermined surface to *excluded*, and the `?? details.surface` fallback depends on `accessIntent?.surface` being `undefined` rather than a half-populated object.
|
|
|
|
### Consumer call site
|
|
|
|
A registered chain link's `authorize` is the consumer this exists for.
|
|
The interaction is Tell-Don't-Ask in the direction that matters — the link is *handed* the facts and *asks the injected query* for the policy; it never reaches back through the details for a collaborator:
|
|
|
|
```typescript
|
|
async function authorize(details, query, log) {
|
|
const facts = details.accessIntent;
|
|
if (facts?.surface !== "external_directory") return { kind: "defer" };
|
|
const verdict = query.checkPermission("external_directory", facts.boundaryValue ?? undefined);
|
|
log.review("model_judge.reviewed", { requestId: details.requestId, surface: facts.surface });
|
|
return verdict.state === "deny" ? { kind: "deny", reason: "outside policy" } : { kind: "defer" };
|
|
}
|
|
```
|
|
|
|
`facts.matchValues` and `facts.boundaryValue` are plain strings fixed at the child, honoring the `path-values` string boundary (`docs/decisions/0002-path-values-string-boundary.md`) — the wire never carries an `AccessPath`, and the link never rebuilds one.
|
|
|
|
### The bounded-delegation consequence
|
|
|
|
No code change in `delegation-envelope.ts`.
|
|
Its existing `details.accessIntent?.surface ?? details.surface` already prefers the gate-authoritative surface; it simply never had one for a forwarded ask.
|
|
After this change the forwarded path is treated exactly like the local path, which is what [ADR 0007] §5 already specifies.
|
|
|
|
The checkpoint still only ever *tightens* — it converts `allow` → `defer`, never the reverse — so the invariant [#599] landed is preserved, not merely unbroken.
|
|
|
|
### The shared principle (the [#610] frame)
|
|
|
|
A new short subsection in `docs/architecture/architecture.md`, under `## The authority model` immediately after `### The recursion` (where the courier hop is already described):
|
|
|
|
> **Reconstruction fidelity at the serving node.**
|
|
> The courier hop carries facts, not judgment — but what the serving node reconstructs from the forwarded request differs by audience.
|
|
> An **in-process seam** (the `Authorizer` chain, reached through `PromptPermissionDetails`) receives the full child-fixed fact set, because a chain link is operator-opted-in via `authorizerChain` and must decide from evidence rather than parsed display text.
|
|
> A **cross-extension broadcast** (`permissions:ui_prompt` / `permissions:decision` on `pi.events`) receives the minimum needed to be correlatable, because any loaded extension can observe it.
|
|
> Fidelity up, disclosure down: the two directions are the same rule applied to different trust boundaries.
|
|
> Requester identity (`requesterCwd`, `principal`) crosses to neither — it stays on the wire object, with the ask details carrying only the `forwarding` provenance.
|
|
|
|
This is descriptive of decided architecture ([ADR 0007] §5, [ADR 0008] §2) rather than a new decision, so it belongs in the living architecture doc, not a new ADR.
|
|
[#610] can cite it for the broadcast half.
|
|
|
|
## Module-Level Changes
|
|
|
|
### Source
|
|
|
|
- **`src/authority/forwarded-request-server.ts`** — add the module-private `toAccessFacts(intent: ForwardedAccessIntent): ForwardedAccessFacts` helper; add the conditional `accessIntent` spread to `buildForwardedAskDetails`; extend that function's doc comment to name the disclosure boundary and the checkpoint coupling.
|
|
`ForwardedAccessFacts` joins the existing type import from `#src/authority/permission-forwarding` (`ForwardedAccessIntent` is already imported).
|
|
- **`src/authority/permission-prompter.ts`** — correct the `PromptPermissionDetails.accessIntent` doc comment.
|
|
Its current final sentence, "Absent for a serving-node local prompt reconstructed from a forwarded request," becomes false with this change; replace it with the version-skew condition (absent only when the forwarded request carried no intent).
|
|
|
|
Greps run to bound the file list:
|
|
|
|
- `grep -rn "accessIntent" packages/pi-permission-system/src` — 8 gate sites (producers, unchanged), `approval-escalator.ts` (child side, unchanged), `delegation-envelope.ts` (consumer, unchanged), `forwarding-io.ts` (wire read, unchanged), `permission-forwarding.ts` (type, unchanged), plus the two files above.
|
|
- No export is removed or renamed, so no cross-package or `docs/` symbol sweep is triggered.
|
|
- `grep -rn "Absent for a serving-node"` — matches `src/authority/permission-prompter.ts` and `dist/public.d.ts`; `dist/` is gitignored and regenerated by `build:types`, so only the source comment is edited.
|
|
|
|
### Tests
|
|
|
|
- **`test/authority/forwarded-request-server.test.ts`** — the exact-object assertion in "escalates an ask through the AskEscalator with the forwarded provenance details" (currently `expect(escalate).toHaveBeenCalledWith({ … })`) gains the projected `accessIntent`; this is the only exact-match call-site assertion on `escalate` in the suite (`grep -rn "escalate).toHaveBeenCalledWith({" test/` → 1 hit).
|
|
New cases cover the `path`-surface projection, the exact-key disclosure boundary, version-skew absence, and the bounded-delegation composition.
|
|
- **`test/helpers/forwarding-fixtures.ts`** — no change; `makeForwardedAccessIntent` already accepts `Partial<ForwardedAccessIntent>` overrides and defaults to a `bash` surface with a worktree-shaped `requesterCwd`.
|
|
|
|
### Docs
|
|
|
|
- **`docs/architecture/architecture.md`** — (a) the `forwarded-request-server.ts` module-tree entry gains a clause that the serving reconstruction projects the request's access facts onto the escalated ask; the clause carries the `#635` ref because the projection is an active constraint (the bounded-delegation checkpoint's exclusion decision depends on it), per the architecture-doc convention. (b) The new `### Reconstruction fidelity at the serving node` subsection under `## The authority model`.
|
|
The `permission-prompter.ts` entry already reads "carries the child-fixed `accessIntent` facts a forwarded ask relays" and stays accurate.
|
|
- **`docs/configuration.md`** — the bounded-delegation checkpoint paragraph (in the `Authorizer chain — case-by-case decision links` section) gains one sentence: the cap applies to forwarded subagent asks on the gate surface that raised them, not the tool name displayed.
|
|
- **`docs/migration/0635-forwarded-ask-delegation-envelope.md`** — new, following the shape of `docs/migration/0644-project-trust-gating.md`: what changed, who is affected (only operators running a third-party *allow*-capable link named in `authorizerChain`), what to do (nothing for the shipped deny-first judge; expect a prompt where a link previously auto-allowed a forwarded path ask), and the forward pointer to [#620]'s secret-shaped refinement.
|
|
`docs/migration` is in the package's `files` allowlist, so any link it makes into `docs/decisions/` must be an absolute GitHub URL.
|
|
- **`.pi/skills/package-pi-permission-system/SKILL.md`** — the `AuthorizerSelection.escalate` paragraph's bounded-delegation sentence gains a clause that a forwarded ask carries the child-fixed `accessIntent`, so the checkpoint reads the gate surface for forwarded and local asks alike.
|
|
This is a reworded-behavior case with no removed symbol, so the skill grep is required by convention.
|
|
|
|
No `README.md` change: its one-sentence chain summary ("caps any link's `allow` on `external_directory`/`path`") stays true and is the right granularity.
|
|
No config schema, example-config, or `permissions.schema.json` change: no config field is added.
|
|
|
|
## Test Impact Analysis
|
|
|
|
1. **Newly enabled tests.**
|
|
This is a defect fix, not an extraction, so it enables no structurally-new test surface.
|
|
It does make one assertion newly *meaningful*: that a forwarded `path`-gate ask reaches the chain with `accessIntent.surface === "path"`, which is the observable difference between the two envelope outcomes.
|
|
The bounded-delegation composition test is new coverage of a real gap — `delegation-envelope.test.ts` covers the envelope over synthetic details, and `forwarded-request-server.test.ts` covers the server over a fake escalator, but nothing composed the two, which is precisely why the forwarded escape went unnoticed.
|
|
2. **Redundant tests.**
|
|
None.
|
|
No existing test is subsumed; the exact-match escalate assertion is *extended*, not replaced, because pinning the whole details object is exactly what guards the disclosure boundary.
|
|
3. **Tests that must stay as-is.**
|
|
`test/authority/forwarding-io.test.ts`'s `asForwardedAccessIntent` narrowing cases (the wire read is untouched), `forwarded-request-server.test.ts`'s recorded-authority and version-skew cases (`resolveDecision` is untouched), and `composition-root.test.ts`'s forwarded non-degraded-broadcast and grant-scope round-trips (the display projection is untouched).
|
|
|
|
## Invariants at Risk
|
|
|
|
| Invariant | Origin | Pinned by | Risk and handling |
|
|
| ---------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| A forwarded ask emits a **non-degraded** `permissions:ui_prompt`: the parent's broadcast carries the child's original `surface`/`value`. | [#292] | `test/composition-root.test.ts`, "forwarded non-degraded broadcast" | Repointing `details.surface` at the gate surface would regress it. Measured, not argued: `buildUiPrompt` (`src/permission-ui-prompt.ts`) reads only `requestId`/`source`/`surface`/`value`/`agentName`/`message`/`forwarding` and never `accessIntent`, so the emitted payload is byte-identical after this change. The plan adds a field and repoints nothing. |
|
|
| `ServingPolicy` resolution is intent-only; a request without `accessIntent` floors to `ask` without consulting the policy. | [#597], [ADR 0008] §4 | `forwarded-request-server.test.ts`, "floors a request with no fields at all (fully legacy) to escalation without consulting the policy" | `resolveDecision` is not edited. The version-skew test is extended to also assert the escalated details carry **no** `accessIntent` key, so absence stays absence. |
|
|
| The wire read is tolerant: a malformed `accessIntent` narrows to `undefined` rather than throwing. | [#596] | `test/authority/forwarding-io.test.ts` | Unchanged; the projection runs only on an already-narrowed value, so it can never see a partial object. |
|
|
| The bounded-delegation checkpoint only ever **tightens** a verdict. | [#599], [ADR 0007] §5 | `test/authority/delegation-envelope.test.ts` | Preserved and strengthened: the change converts one `allow` into a `defer` and creates no path from `defer`/`deny` to `allow`. The new composition test pins it end to end. |
|
|
| Requester identity (`requesterCwd`, `principal`) never crosses onto the prompt details. | [ADR 0008] §2 disclosure framing; the issue's own explicit ask | *New* — this invariant lives only in prose today | Add the pin: an exact-keys assertion (`Object.keys(details.accessIntent).sort()` equals `["boundaryValue", "matchValues", "surface"]`) plus the compile-checked `ForwardedAccessFacts` return type on `toAccessFacts`. |
|
|
|
|
## TDD Order
|
|
|
|
1. **Red → Green: project the child-fixed facts onto the escalated ask, and pin the delegation consequence.**
|
|
Test surface: `test/authority/forwarded-request-server.test.ts`.
|
|
Red (four assertions, all failing against current `main`):
|
|
- Extend the existing exact-object assertion in "escalates an ask through the AskEscalator with the forwarded provenance details" to include `accessIntent: { surface: "bash", matchValues: ["git push"], boundaryValue: null }`.
|
|
- New: a forwarded request whose `accessIntent` is a `path`-surface intent (multi-alias `matchValues`, non-null `boundaryValue`, and a `requesterCwd`/`principal` distinct from the serving session) escalates with all three fact fields intact **and** with exactly those three keys — the disclosure-boundary pin.
|
|
- New: a version-skew request with no `accessIntent` escalates with the key absent (`expect(details).not.toHaveProperty("accessIntent")`).
|
|
- New `describe` composing the two real units: run the details captured from `escalate` through `encloseInDelegationEnvelope` over an allow-returning link, and assert `{ kind: "defer" }` for a forwarded `path`-surface ask and `{ kind: "allow" }` for a forwarded `bash`-surface ask (the scenario-A regression guard).
|
|
|
|
Green: add `toAccessFacts` and the conditional spread in `src/authority/forwarded-request-server.ts`; correct the `PromptPermissionDetails.accessIntent` doc comment in `src/authority/permission-prompter.ts`.
|
|
Land the user-facing breaking-change docs in the same commit so the `BREAKING CHANGE:` footer points at a note that exists: `docs/migration/0635-forwarded-ask-delegation-envelope.md` and the `docs/configuration.md` sentence.
|
|
Verify: `pnpm --filter @gotgenes/pi-permission-system run test`, `run check`, `run lint`.
|
|
|
|
Commit: `fix(pi-permission-system)!: carry forwarded access facts to the Authorizer Chain (#635)`, with a `BREAKING CHANGE:` footer describing the checkpoint tightening and pointing at the migration note, plus `Refs #635`.
|
|
|
|
2. **Docs: architecture module tree, the fidelity principle, and the package skill.**
|
|
No test cycle — documentation only.
|
|
Edit `docs/architecture/architecture.md` (the `forwarded-request-server.ts` module-tree clause and the new `### Reconstruction fidelity at the serving node` subsection) and `.pi/skills/package-pi-permission-system/SKILL.md` (the bounded-delegation sentence).
|
|
Verify: `pnpm exec rumdl check` on the edited files.
|
|
|
|
Commit: `docs(pi-permission-system): record serving-node reconstruction fidelity (#635)`.
|
|
|
|
## Risks and Mitigations
|
|
|
|
- **Risk: a spread leaks `requesterCwd`/`principal` onto the ask details.**
|
|
`ForwardedAccessIntent` is structurally assignable to `ForwardedAccessFacts`, so `accessIntent: request.accessIntent` type-checks cleanly while carrying two extra fields at runtime — a silent disclosure widening that `tsc` cannot catch.
|
|
Mitigation: the field-by-field `toAccessFacts` projection with an explicit `ForwardedAccessFacts` return type, plus the exact-keys test assertion.
|
|
- **Risk: a future field added to `ForwardedAccessFacts` is silently not projected.**
|
|
Mitigation: the explicit return type makes the object literal incomplete, so `tsc` fails at `toAccessFacts` until the new field is deliberately projected or deliberately withheld with a comment.
|
|
- **Risk: an operator running an allow-capable link is surprised by new prompts.**
|
|
Mitigation: `fix!:` + `BREAKING CHANGE:` footer + a migration note naming exactly who is affected.
|
|
The blast radius is narrow by construction: it requires a third-party allow-capable link, named in `authorizerChain`, on a forwarded ask raised by the `path` / `external_directory` gate.
|
|
The shipped first-party judge is deny-first.
|
|
- **Risk: a major version bump (`23.0.3` → `24.0.0`) for a small change.**
|
|
Accepted deliberately (operator decision at planning): the tightening changes an authorization outcome, which is the category that most warrants a loud signal.
|
|
- **Risk: the tightening is later reversed by [#620].**
|
|
Not a regression — [#620] replaces the whole-surface exclusion with a secret-shaped one *by design*, for local and forwarded asks alike.
|
|
The migration note names this so an operator reading it understands the trajectory.
|
|
- **Risk: the new architecture subsection re-inflates the doc.**
|
|
Mitigation: it is one short paragraph under an existing section, stating current behavior, with no per-issue provenance trail in the module tree.
|
|
|
|
## Open Questions
|
|
|
|
- Should the fidelity principle eventually graduate from the architecture doc into an ADR?
|
|
Deferred until [#610] lands: an ADR is warranted if the broadcast half turns out to need real deliberation (e.g. whether `permissions:decision` may carry a value projection at all), and premature if [#610] is a mechanical `requestId` addition.
|
|
- Should `ForwardedAccessFacts` grow a `requesterCwd` for *display* (letting a prompt or a link show "the child resolved this against `/worktree/issue-42`")?
|
|
Out of scope; no consumer asks for it, and adding it would widen the disclosure boundary this plan deliberately draws.
|
|
Revisit only when a concrete consumer exists.
|
|
|
|
[ADR 0007]: https://github.com/gotgenes/pi-packages/blob/main/packages/pi-permission-system/docs/decisions/0007-model-judge-authorizer-chain-adr.md
|
|
[ADR 0008]: https://github.com/gotgenes/pi-packages/blob/main/packages/pi-permission-system/docs/decisions/0008-cross-session-access-intent.md
|
|
[#292]: https://github.com/gotgenes/pi-packages/issues/292
|
|
[#596]: https://github.com/gotgenes/pi-packages/issues/596
|
|
[#597]: https://github.com/gotgenes/pi-packages/issues/597
|
|
[#599]: https://github.com/gotgenes/pi-packages/issues/599
|
|
[#610]: https://github.com/gotgenes/pi-packages/issues/610
|
|
[#620]: https://github.com/gotgenes/pi-packages/issues/620
|