@gotgenes/pi-permission-system 23.0.3 → 25.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/dist/public.d.ts +4 -2
- package/docs/configuration.md +24 -7
- package/docs/migration/0635-forwarded-ask-delegation-envelope.md +42 -0
- package/docs/opencode-compatibility.md +1 -1
- package/package.json +1 -1
- package/src/access-intent/bash/node-text.ts +5 -1
- package/src/access-intent/bash/shell-variable-expansion.ts +77 -0
- package/src/authority/forwarded-request-server.ts +36 -1
- package/src/authority/permission-prompter.ts +4 -2
- package/src/expand-home.ts +30 -16
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,38 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [25.0.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v24.0.0...pi-permission-system-v25.0.0) (2026-08-11)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### ⚠ BREAKING CHANGES
|
|
12
|
+
|
|
13
|
+
* **pi-permission-system:** a bash command referencing `$HOME` or `${HOME}` now reaches the `external_directory` gate whether or not the target exists, so a policy with `external_directory: {"*": "ask"}` prompts for commands that previously ran silently. Allow the directory explicitly to restore the old behavior — e.g. `"external_directory": {"~/.cargo/registry/*": "allow"}`. The token shown in prompts, review-log entries, and derived session-approval patterns is now the expanded path rather than the `$HOME/...` spelling; this makes the prompt agree with the approval pattern, which was already derived from the expanded form.
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **pi-permission-system:** expand ${HOME} alongside $HOME in path patterns ([3ebbd41](https://github.com/gotgenes/pi-packages/commit/3ebbd418e5b85a6dfbc1865c69f62c1f9a3cb144)), closes [#694](https://github.com/gotgenes/pi-packages/issues/694)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* **pi-permission-system:** resolve $HOME and $PWD expansions in bash path tokens ([8cbc7ee](https://github.com/gotgenes/pi-packages/commit/8cbc7ee011a3e06dcf047bfe8a467db3aa487721)), closes [#694](https://github.com/gotgenes/pi-packages/issues/694)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Documentation
|
|
26
|
+
|
|
27
|
+
* **pi-permission-system:** record resolved shell expansions in ADR 0009 and user docs ([caaf5ec](https://github.com/gotgenes/pi-packages/commit/caaf5ec02ef2f7df1e2a922dba0a9afdc121b191)), closes [#694](https://github.com/gotgenes/pi-packages/issues/694)
|
|
28
|
+
|
|
29
|
+
## [24.0.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v23.0.3...pi-permission-system-v24.0.0) (2026-07-26)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### ⚠ BREAKING CHANGES
|
|
33
|
+
|
|
34
|
+
* **pi-permission-system:** an authorizer chain link's `allow` on a forwarded subagent ask raised by the `path` or `external_directory` gate is now downgraded to `defer`, so the request falls through to an interactive prompt. This affects only an operator running an allow-capable third-party link named in `authorizerChain`; the first-party model judge is deny-first and is unaffected, as are forwarded `bash` asks and per-tool-gated asks. See packages/pi-permission-system/docs/migration/0635-forwarded-ask-delegation-envelope.md
|
|
35
|
+
|
|
36
|
+
### Bug Fixes
|
|
37
|
+
|
|
38
|
+
* **pi-permission-system:** carry forwarded access facts to the Authorizer Chain ([#635](https://github.com/gotgenes/pi-packages/issues/635)) ([c0790ad](https://github.com/gotgenes/pi-packages/commit/c0790ad6d1d15defaba60097d803618b4d8c461c))
|
|
39
|
+
|
|
8
40
|
## [23.0.3](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v23.0.2...pi-permission-system-v23.0.3) (2026-07-26)
|
|
9
41
|
|
|
10
42
|
|
package/dist/public.d.ts
CHANGED
|
@@ -195,8 +195,10 @@ interface PromptPermissionDetails {
|
|
|
195
195
|
* The child-fixed access facts the raising gate computed (surface + match
|
|
196
196
|
* set). Rides through the runner to the escalation edge, which completes
|
|
197
197
|
* them into a `ForwardedAccessIntent` by stamping `requesterCwd` and
|
|
198
|
-
* `principal`.
|
|
199
|
-
* forwarded request
|
|
198
|
+
* `principal`. On a serving node these facts are projected back off the
|
|
199
|
+
* forwarded request, so a forwarded ask reaches the `Authorizer` chain with
|
|
200
|
+
* the same evidence as a local one; only a version-skew request that carried
|
|
201
|
+
* no intent leaves this absent.
|
|
200
202
|
*/
|
|
201
203
|
accessIntent?: ForwardedAccessFacts;
|
|
202
204
|
}
|
package/docs/configuration.md
CHANGED
|
@@ -106,7 +106,7 @@ This clamp is deny-preserving and, like `yoloMode`, applied at composition; when
|
|
|
106
106
|
| `doublePressToConfirm` | `true` | Requires a confirming second press of a decision hotkey in the inline TUI dialog (see below). TUI sessions only; set to `false` for single-press. |
|
|
107
107
|
| `toolInputPreviewMaxLength` | `200` | Max characters of inline JSON shown in permission prompts for tool inputs. Omit to use the default. Set to a large value to disable truncation. |
|
|
108
108
|
| `toolTextSummaryMaxLength` | `80` | Max characters of inline pattern/path summaries (grep patterns, find globs, ls paths) in permission prompts. Omit to use the default. |
|
|
109
|
-
| `piInfrastructureReadPaths` | `[]` | Extra directories to auto-allow for reads, bypassing the `external_directory` gate. Supports `~`/`$HOME` expansion and wildcard patterns (`*`, `?`).
|
|
109
|
+
| `piInfrastructureReadPaths` | `[]` | Extra directories to auto-allow for reads, bypassing the `external_directory` gate. Supports `~`/`$HOME`/`${HOME}` expansion and wildcard patterns (`*`, `?`). |
|
|
110
110
|
| `authorizerChain` | `[]` | Ordered names of registered live-authority chain links to consult before the terminal authorizer (see [Authorizer chain](#authorizer-chain--case-by-case-decision-links)). |
|
|
111
111
|
|
|
112
112
|
Both logs write to `~/.pi/agent/extensions/pi-permission-system/logs/`.
|
|
@@ -136,7 +136,7 @@ Non-TUI contexts (RPC / frontend-driven sessions) keep the single-select prompt
|
|
|
136
136
|
### `piInfrastructureReadPaths` patterns
|
|
137
137
|
|
|
138
138
|
Each entry is either a plain directory prefix or a wildcard pattern.
|
|
139
|
-
Plain entries match any path that starts with the given directory (after `~`/`$HOME` expansion).
|
|
139
|
+
Plain entries match any path that starts with the given directory (after `~`/`$HOME`/`${HOME}` expansion).
|
|
140
140
|
Wildcard entries use `*` (any characters, including `/`) and `?` (exactly one character).
|
|
141
141
|
`*` and `**` are equivalent — both cross directory boundaries.
|
|
142
142
|
|
|
@@ -209,6 +209,9 @@ Three invariants govern the chain:
|
|
|
209
209
|
|
|
210
210
|
The chain owner caps every link with a **bounded-delegation checkpoint**: a link's `allow` on an excluded surface (`external_directory` or the `path` surface) is downgraded to `defer`, so a buggy or over-eager judge can never approve access outside your policy.
|
|
211
211
|
Deny and defer are never capped.
|
|
212
|
+
The excluded surface is the **gate** surface the rule fired on, not the tool name displayed in the prompt — so a `write` blocked by a `path` rule is capped.
|
|
213
|
+
This holds for an ask forwarded up from a subagent exactly as it does for a local one.
|
|
214
|
+
See [migration/0635-forwarded-ask-delegation-envelope.md](migration/0635-forwarded-ask-delegation-envelope.md).
|
|
212
215
|
|
|
213
216
|
Extension authors: register a link from a `permissions:ready` handler via `getPermissionsService().registerAuthorizer(name, authorize)`; the callback receives the ask details and a narrow, session-scoped `PermissionQuery` (`checkPermission` / `getToolPermission`) so it can consult the deterministic engine at gate parity.
|
|
214
217
|
Registration returns a disposer, and only one link may hold a given name.
|
|
@@ -584,9 +587,22 @@ The pattern is stored and displayed as written (`~/.cargo/registry/*`) in logs a
|
|
|
584
587
|
For caches you only ever **read**, `piInfrastructureReadPaths` is a lighter alternative — it auto-allows read-only tools (`read`, `find`, `grep`, `ls`) and bypasses the gate entirely, but it does not cover `write`/`edit` or bash.
|
|
585
588
|
Use `external_directory` when the allowance must apply to every tool.
|
|
586
589
|
|
|
587
|
-
Bash commands are also covered: the extension
|
|
588
|
-
|
|
589
|
-
|
|
590
|
+
Bash commands are also covered: the extension parses the command and applies the same gate to every token that resolves outside `ctx.cwd`.
|
|
591
|
+
Quoting is understood, so `ls "$HOME/x"` and `ls $HOME/x` are treated alike.
|
|
592
|
+
|
|
593
|
+
What the bash projection resolves:
|
|
594
|
+
|
|
595
|
+
- Absolute, home-relative (`~/`), parent-traversal (`../`), and separator-bearing tokens, plus redirect targets (`> out.txt`) and values embedded in long options (`--file=/tmp/patterns`).
|
|
596
|
+
- The plain shell variables `$HOME` / `${HOME}` and `$PWD` / `${PWD}`, so `$HOME/x` is gated exactly as `~/x` and the literal absolute spelling, whether or not the target exists.
|
|
597
|
+
- Relative tokens, against the working directory produced by folding literal current-shell `cd` commands.
|
|
598
|
+
- A bare token (`cat id_rsa`) when it names an existing filesystem entry.
|
|
599
|
+
|
|
600
|
+
What it deliberately does not resolve: any other variable (`$CONFIG_DIR`), a command substitution (`$(cmd)`), an expansion carrying an operator (`${HOME:-/tmp}`), and a variable reached through an assignment (`CURRENT="$HOME"; ls "$CURRENT"`).
|
|
601
|
+
A non-literal `cd` (`cd "$DIR"`) makes the working directory unknown, after which relative tokens are kept literal rather than resolved against a guess.
|
|
602
|
+
Commands whose payload is opaque (`bash -c`, `eval`, `sudo`, `xargs`) are floored to `ask` instead of projected.
|
|
603
|
+
The governing record is [ADR 0009](https://github.com/gotgenes/pi-packages/blob/main/packages/pi-permission-system/docs/decisions/0009-bash-path-projection-completeness-contract.md), which states what the projection guarantees and which gaps are accepted residuals rather than bugs.
|
|
604
|
+
|
|
605
|
+
(The separate `bash` command-pattern surface does evaluate commands nested inside substitutions and subshells; see that section.) OS device paths (`/dev/null`, `/dev/stdin`, `/dev/stdout`, `/dev/stderr`) are always excluded.
|
|
590
606
|
|
|
591
607
|
#### Symlinked paths
|
|
592
608
|
|
|
@@ -641,8 +657,9 @@ To allow-list such a path, write the rule using the path as typed — for exampl
|
|
|
641
657
|
|
|
642
658
|
### Home Directory Expansion in Patterns
|
|
643
659
|
|
|
644
|
-
Pattern keys in any permission surface can start with
|
|
660
|
+
Pattern keys in any permission surface can start with `~/`, `$HOME/`, or `${HOME}/` (or be exactly `~`, `$HOME`, or `${HOME}`).
|
|
645
661
|
They are expanded to the OS home directory at match time, so configs are portable across machines and users.
|
|
662
|
+
A prefix is recognized only when it stands alone or precedes a separator, so a longer name (`~username`, `$HOMEDIR`) and a braced expansion carrying an operator (`${HOME:-/tmp}`) are left alone.
|
|
646
663
|
|
|
647
664
|
```jsonc
|
|
648
665
|
{
|
|
@@ -658,7 +675,7 @@ They are expanded to the OS home directory at match time, so configs are portabl
|
|
|
658
675
|
The pattern is stored and displayed as written (e.g. `~/development/*`) in logs and approval dialogs.
|
|
659
676
|
|
|
660
677
|
Path **values** supplied by tool calls and bash commands are expanded the same way.
|
|
661
|
-
This means `~/...`, `$HOME/...`, and the fully-expanded absolute form all match a single home-anchored pattern: a `read` tool called with path `~/.ssh/config`, `$HOME/.ssh/config`, or `/Users/me/.ssh/config` is all caught by a `"~/.ssh/*": "deny"` rule.
|
|
678
|
+
This means `~/...`, `$HOME/...`, `${HOME}/...`, and the fully-expanded absolute form all match a single home-anchored pattern: a `read` tool called with path `~/.ssh/config`, `$HOME/.ssh/config`, `${HOME}/.ssh/config`, or `/Users/me/.ssh/config` is all caught by a `"~/.ssh/*": "deny"` rule.
|
|
662
679
|
|
|
663
680
|
---
|
|
664
681
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Migration guide: forwarded asks and the bounded-delegation checkpoint
|
|
2
|
+
|
|
3
|
+
Starting with the release that closes #635, a permission request forwarded up from a subagent carries the **child-fixed access facts** through to the authorizer chain.
|
|
4
|
+
As a side effect, the bounded-delegation checkpoint now applies to forwarded asks the same way it already applied to local ones.
|
|
5
|
+
This is a **breaking change** for one narrow configuration.
|
|
6
|
+
|
|
7
|
+
## Who is affected
|
|
8
|
+
|
|
9
|
+
You are affected only if **all** of the following hold:
|
|
10
|
+
|
|
11
|
+
- You name an authorizer chain link in `authorizerChain`, and
|
|
12
|
+
- that link is **allow-capable** (it can return `allow`, not only `deny` / `defer`), and
|
|
13
|
+
- a subagent forwards an `ask` to your session that was raised by the `path` or `external_directory` gate.
|
|
14
|
+
|
|
15
|
+
If you run no chain link, or run only a deny-first link — including the first-party [`@gotgenes/pi-permission-model-judge`](https://github.com/gotgenes/pi-packages/tree/main/packages/pi-permission-model-judge), which only ever denies or defers — **nothing changes for you**.
|
|
16
|
+
|
|
17
|
+
## What changed
|
|
18
|
+
|
|
19
|
+
The checkpoint downgrades a link's `allow` to `defer` on the excluded surfaces (`path`, `external_directory`), so the request falls through to an interactive prompt instead.
|
|
20
|
+
It selects the surface from the **gate** surface — the surface the matching rule lives on — falling back to the displayed surface when no gate surface is available.
|
|
21
|
+
|
|
22
|
+
A forwarded request previously arrived with no gate surface, so the checkpoint fell back to the child's *display* surface, which is the tool name (`write`, `edit`, `read`).
|
|
23
|
+
A tool name is not an excluded surface, so a link's `allow` on a forwarded `path`-gated request was honored.
|
|
24
|
+
The identical request made directly in the same session was capped, because a local request always carries its gate surface.
|
|
25
|
+
|
|
26
|
+
Forwarding was therefore a way around your own delegation boundary: a link could approve `~/.ssh/config` for a subagent that it could not approve for the main session.
|
|
27
|
+
Now the gate surface crosses the forwarding hop, and both paths behave identically.
|
|
28
|
+
|
|
29
|
+
Unchanged: a forwarded `bash` ask, and a forwarded tool ask gated by a per-tool rule (`write: ask`), are both already on non-excluded surfaces and are not capped.
|
|
30
|
+
`deny` and `defer` verdicts were never capped and still are not.
|
|
31
|
+
|
|
32
|
+
## What you need to do
|
|
33
|
+
|
|
34
|
+
Nothing, unless you run an allow-capable link.
|
|
35
|
+
|
|
36
|
+
If you do, expect an interactive prompt where that link previously auto-approved a forwarded subagent request for a path.
|
|
37
|
+
To keep such a request from prompting, grant it in policy rather than through the link — the `path` and `external_directory` rules in your config are consulted before the chain runs, so an `allow` there resolves the request without reaching an authorizer at all.
|
|
38
|
+
|
|
39
|
+
## Related
|
|
40
|
+
|
|
41
|
+
Issue #620 will replace the whole-`path` exclusion with a narrower secret-shaped one, letting a link allow a non-secret path again while keeping secret-shaped paths capped.
|
|
42
|
+
That refinement applies to local and forwarded asks alike, so the two paths stay aligned.
|
|
@@ -20,7 +20,7 @@ The following concepts are shared between OpenCode and this extension:
|
|
|
20
20
|
| Last-match-wins | When multiple patterns match, the last one in config order wins |
|
|
21
21
|
| `*` wildcard | Matches zero or more of any character (including path separators) |
|
|
22
22
|
| `?` wildcard | Matches exactly one character |
|
|
23
|
-
| Home directory expansion |
|
|
23
|
+
| Home directory expansion | `~/`, `$HOME/`, and `${HOME}/` expand to the OS home directory in patterns |
|
|
24
24
|
| `external_directory` surface | Gates access to paths outside the working directory |
|
|
25
25
|
| `bash` surface | Command patterns matched against shell commands |
|
|
26
26
|
| `skill` surface | Skill name patterns matched against skill invocations |
|
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TSNode } from "#src/access-intent/bash/parser";
|
|
2
|
+
import { resolvePlainVariableExpansion } from "#src/access-intent/bash/shell-variable-expansion";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Node types whose subtrees must never be descended into for
|
|
@@ -29,6 +30,8 @@ export const ARG_NODE_TYPES = new Set([
|
|
|
29
30
|
* - `raw_string` → strip surrounding single quotes
|
|
30
31
|
* - `string` → strip surrounding double quotes, concatenate children text
|
|
31
32
|
* - `concatenation` → concatenate resolved children
|
|
33
|
+
* - expansions → the resolved value of a plain `$HOME`/`$PWD` reference,
|
|
34
|
+
* else `.text` (see `shell-variable-expansion.ts`)
|
|
32
35
|
* - other → `.text` as fallback
|
|
33
36
|
*/
|
|
34
37
|
export function resolveNodeText(node: TSNode): string {
|
|
@@ -57,9 +60,10 @@ export function resolveNodeText(node: TSNode): string {
|
|
|
57
60
|
return result;
|
|
58
61
|
}
|
|
59
62
|
case "string_content":
|
|
63
|
+
return node.text;
|
|
60
64
|
case "simple_expansion":
|
|
61
65
|
case "expansion":
|
|
62
|
-
return node.text;
|
|
66
|
+
return resolvePlainVariableExpansion(node) ?? node.text;
|
|
63
67
|
case "concatenation": {
|
|
64
68
|
let result = "";
|
|
65
69
|
for (let i = 0; i < node.childCount; i++) {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolution of the shell variable references the bash path projection can
|
|
3
|
+
* settle statically.
|
|
4
|
+
*
|
|
5
|
+
* Runs at token collection, upstream of classification: by the time a token
|
|
6
|
+
* reaches `classifyTokenAsPathCandidate` it already carries the expanded path,
|
|
7
|
+
* so `$HOME/x` is accepted by the ordinary absolute-shape branch and needs no
|
|
8
|
+
* per-variable knowledge in the classifiers (#694). Keeping the vocabulary here
|
|
9
|
+
* — rather than teaching each classifier a `$HOME` prefix — is what stops the
|
|
10
|
+
* two from drifting apart, which is the defect this module closes.
|
|
11
|
+
*
|
|
12
|
+
* The resolvable set is deliberately tiny and closed. `HOME` is the spelling
|
|
13
|
+
* `expandHomePath` already resolves for config patterns and path literals, so
|
|
14
|
+
* resolving it here removes an inconsistency rather than widening the
|
|
15
|
+
* determinism boundary; `PWD` reads no environment at all. Every other name
|
|
16
|
+
* keeps its literal text, so ADR 0003's exclusion of ambient host state stands.
|
|
17
|
+
* See `docs/decisions/0009-bash-path-projection-completeness-contract.md`.
|
|
18
|
+
*/
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
|
|
21
|
+
import type { TSNode } from "#src/access-intent/bash/parser";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The value of a plain `$NAME` / `${NAME}` reference, or `null` when the node
|
|
25
|
+
* is not a plain reference or names a variable outside the resolvable set.
|
|
26
|
+
*
|
|
27
|
+
* Plainness is decided structurally, not by matching the node's text: a plain
|
|
28
|
+
* reference carries exactly one `variable_name` child and nothing else but
|
|
29
|
+
* delimiters. An operator form (`${HOME:-/tmp}`, `${#HOME}`, `${HOME%/*}`)
|
|
30
|
+
* carries additional children and is therefore rejected without this module
|
|
31
|
+
* needing to enumerate bash's expansion operators.
|
|
32
|
+
*/
|
|
33
|
+
export function resolvePlainVariableExpansion(node: TSNode): string | null {
|
|
34
|
+
const name = plainVariableName(node);
|
|
35
|
+
return name === null ? null : (RESOLVABLE_VARIABLES.get(name)?.() ?? null);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* How each resolvable variable is spelled as a path.
|
|
40
|
+
*
|
|
41
|
+
* `PWD` resolves to the base-relative marker rather than a directory: the
|
|
42
|
+
* shell's working directory at a given point *is* the projection's effective
|
|
43
|
+
* base, which the resolver already applies via `resolveBase`. Handing back `.`
|
|
44
|
+
* therefore lands `$PWD/x` on the same footing as `./x` — correct after any
|
|
45
|
+
* `cd` folding, conservative under an unknown base (#393), and free of both a
|
|
46
|
+
* threaded base parameter and a platform branch.
|
|
47
|
+
*/
|
|
48
|
+
const RESOLVABLE_VARIABLES: ReadonlyMap<string, () => string> = new Map([
|
|
49
|
+
["HOME", homedir],
|
|
50
|
+
["PWD", () => "."],
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
/** Node types that delimit an expansion without altering what it evaluates to. */
|
|
54
|
+
const EXPANSION_DELIMITERS: ReadonlySet<string> = new Set(["$", "${", "}"]);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The variable a node plainly references, or `null` when it references none —
|
|
58
|
+
* because it has no `variable_name` child, has more than one, or carries a
|
|
59
|
+
* child that is neither the name nor a delimiter (an expansion operator and its
|
|
60
|
+
* operand, or an assignment's `=` and value).
|
|
61
|
+
*/
|
|
62
|
+
function plainVariableName(node: TSNode): string | null {
|
|
63
|
+
let name: string | null = null;
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
66
|
+
const child = node.child(i);
|
|
67
|
+
if (!child) continue;
|
|
68
|
+
if (child.type === "variable_name") {
|
|
69
|
+
if (name !== null) return null;
|
|
70
|
+
name = child.text;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (!EXPANSION_DELIMITERS.has(child.type)) return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return name;
|
|
77
|
+
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "#src/authority/forwarder-context";
|
|
6
6
|
import type { PermissionPromptDecision } from "#src/authority/permission-dialog";
|
|
7
7
|
import {
|
|
8
|
+
type ForwardedAccessFacts,
|
|
8
9
|
type ForwardedAccessIntent,
|
|
9
10
|
type ForwardedPermissionRequest,
|
|
10
11
|
type ForwardedPermissionResponse,
|
|
@@ -93,7 +94,14 @@ function formatForwardedPermissionPrompt(
|
|
|
93
94
|
/**
|
|
94
95
|
* Map a forwarded request onto the escalated ask's details, carrying the
|
|
95
96
|
* forwarded provenance (requester agent/session + the child's original display
|
|
96
|
-
* projection) so `LocalUserAuthorizer` emits a non-degraded broadcast (#292)
|
|
97
|
+
* projection) so `LocalUserAuthorizer` emits a non-degraded broadcast (#292),
|
|
98
|
+
* plus the child-fixed access facts so the serving node's `Authorizer` chain
|
|
99
|
+
* judges a forwarded ask on the same evidence as a local one (ADR 0008; #635).
|
|
100
|
+
*
|
|
101
|
+
* The display `surface` and the fact `surface` are distinct and both belong
|
|
102
|
+
* here: the former is the child's tool name (what the UI shows), the latter the
|
|
103
|
+
* gate surface the rule fired on (what the bounded-delegation checkpoint
|
|
104
|
+
* excludes on).
|
|
97
105
|
*/
|
|
98
106
|
function buildForwardedAskDetails(
|
|
99
107
|
request: ForwardedPermissionRequest,
|
|
@@ -114,6 +122,33 @@ function buildForwardedAskDetails(
|
|
|
114
122
|
...(request.sessionApproval
|
|
115
123
|
? { sessionApproval: request.sessionApproval }
|
|
116
124
|
: {}),
|
|
125
|
+
// Absent for a version-skew request that carried no intent — which the
|
|
126
|
+
// delegation envelope reads as "surface undetermined" and fail-safes to
|
|
127
|
+
// excluded, so absence must stay absence rather than become `undefined`.
|
|
128
|
+
...(request.accessIntent
|
|
129
|
+
? { accessIntent: toAccessFacts(request.accessIntent) }
|
|
130
|
+
: {}),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Project the wire intent down to the child-fixed access facts an `Authorizer`
|
|
136
|
+
* may see.
|
|
137
|
+
*
|
|
138
|
+
* Field-by-field rather than a spread, because this is a disclosure boundary:
|
|
139
|
+
* `requesterCwd` and `principal` are requester identity for the serving node's
|
|
140
|
+
* own resolution (ADR 0008 §3) and stay off the ask details. A link that needs
|
|
141
|
+
* requester identity reads `details.forwarding`. `ForwardedAccessIntent`
|
|
142
|
+
* extends `ForwardedAccessFacts`, so a spread would type-check while widening
|
|
143
|
+
* disclosure at runtime; the explicit return type makes any future field on
|
|
144
|
+
* `ForwardedAccessFacts` a compile error here until it is deliberately
|
|
145
|
+
* projected or deliberately withheld.
|
|
146
|
+
*/
|
|
147
|
+
function toAccessFacts(intent: ForwardedAccessIntent): ForwardedAccessFacts {
|
|
148
|
+
return {
|
|
149
|
+
surface: intent.surface,
|
|
150
|
+
matchValues: intent.matchValues,
|
|
151
|
+
boundaryValue: intent.boundaryValue,
|
|
117
152
|
};
|
|
118
153
|
}
|
|
119
154
|
|
|
@@ -53,8 +53,10 @@ export interface PromptPermissionDetails {
|
|
|
53
53
|
* The child-fixed access facts the raising gate computed (surface + match
|
|
54
54
|
* set). Rides through the runner to the escalation edge, which completes
|
|
55
55
|
* them into a `ForwardedAccessIntent` by stamping `requesterCwd` and
|
|
56
|
-
* `principal`.
|
|
57
|
-
* forwarded request
|
|
56
|
+
* `principal`. On a serving node these facts are projected back off the
|
|
57
|
+
* forwarded request, so a forwarded ask reaches the `Authorizer` chain with
|
|
58
|
+
* the same evidence as a local one; only a version-skew request that carried
|
|
59
|
+
* no intent leaves this absent.
|
|
58
60
|
*/
|
|
59
61
|
accessIntent?: ForwardedAccessFacts;
|
|
60
62
|
}
|
package/src/expand-home.ts
CHANGED
|
@@ -2,27 +2,41 @@ import { homedir } from "node:os";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* The spellings of the home directory this package resolves, in every pattern
|
|
6
|
+
* and path literal.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
* `$HOME` and `${HOME}` are the two spellings of the same shell variable and
|
|
9
|
+
* must stay interchangeable: a rule keyed on one form has to match a path
|
|
10
|
+
* written in the other, and the bash path projection classifies a token by the
|
|
11
|
+
* shape it has *after* this expansion (#694).
|
|
12
|
+
*/
|
|
13
|
+
const HOME_PREFIXES = ["~", "$HOME", "${HOME}"] as const;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Expand a home-directory prefix in a pattern or path value to the OS home
|
|
17
|
+
* directory.
|
|
18
|
+
*
|
|
19
|
+
* A prefix is recognized only when it stands alone or is followed by a path
|
|
20
|
+
* separator, so a longer name (`~username`, `$HOMEDIR`, `${HOMEDIR}`) and a
|
|
21
|
+
* braced parameter expansion carrying an operator (`${HOME:-/tmp}`,
|
|
22
|
+
* `${HOME%/*}`) are both left untouched.
|
|
23
|
+
*
|
|
24
|
+
* Supported forms, for each prefix in {@link HOME_PREFIXES}:
|
|
25
|
+
* - `<prefix>` → `homedir()`
|
|
26
|
+
* - `<prefix>/path` → `homedir()/path`
|
|
27
|
+
* - `<prefix>\path` → `homedir()\path` (Windows)
|
|
14
28
|
*
|
|
15
29
|
* All other patterns are returned unchanged.
|
|
16
30
|
*/
|
|
17
31
|
export function expandHomePath(pattern: string): string {
|
|
18
|
-
|
|
19
|
-
return homedir();
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
for (const prefix of HOME_PREFIXES) {
|
|
33
|
+
if (pattern === prefix) return homedir();
|
|
34
|
+
if (!pattern.startsWith(prefix)) continue;
|
|
35
|
+
|
|
36
|
+
const rest = pattern.slice(prefix.length);
|
|
37
|
+
if (rest.startsWith("/") || rest.startsWith("\\")) {
|
|
38
|
+
return join(homedir(), rest.slice(1));
|
|
39
|
+
}
|
|
26
40
|
}
|
|
27
41
|
return pattern;
|
|
28
42
|
}
|