pi-daddy 0.13.0 → 0.15.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 +113 -0
- package/README.md +113 -7
- package/dist/catalog.d.ts +9 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +90 -0
- package/dist/catalog.js.map +1 -1
- package/dist/cli.d.ts +31 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +234 -0
- package/dist/cli.js.map +1 -0
- package/dist/delegate.d.ts.map +1 -1
- package/dist/delegate.js +14 -2
- package/dist/delegate.js.map +1 -1
- package/dist/grant-env.d.ts +73 -0
- package/dist/grant-env.d.ts.map +1 -0
- package/dist/grant-env.js +131 -0
- package/dist/grant-env.js.map +1 -0
- package/dist/grant-store.d.ts +68 -0
- package/dist/grant-store.d.ts.map +1 -0
- package/dist/grant-store.js +142 -0
- package/dist/grant-store.js.map +1 -0
- package/dist/init.d.ts +117 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +286 -0
- package/dist/init.js.map +1 -0
- package/dist/skill-packages.d.ts +128 -0
- package/dist/skill-packages.d.ts.map +1 -0
- package/dist/skill-packages.js +245 -0
- package/dist/skill-packages.js.map +1 -0
- package/extensions/grants-command.ts +31 -0
- package/extensions/grants.ts +57 -5
- package/extensions/init-command.ts +132 -0
- package/extensions/session.ts +75 -6
- package/extensions/spawn-summary.ts +214 -0
- package/package.json +12 -1
- package/src/catalog.ts +92 -0
- package/src/cli.ts +266 -0
- package/src/delegate.ts +14 -2
- package/src/grant-env.ts +188 -0
- package/src/grant-store.ts +151 -0
- package/src/init.ts +344 -0
- package/src/skill-packages.ts +282 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which installed npm packages ship `SKILL.md` definitions — read from their own manifests.
|
|
3
|
+
*
|
|
4
|
+
* `pi-daddy init` scaffolds a governed project from whatever skill packages are already installed, and
|
|
5
|
+
* this is how it finds them: **a package declares its skills in `package.json`'s `pi.skills` array**, which
|
|
6
|
+
* is pi's own convention and how pi itself loads them. Measured against `principal-pi-skills@2.3.1`:
|
|
7
|
+
*
|
|
8
|
+
* ```json
|
|
9
|
+
* "pi": { "skills": ["./decide", "./architect", "./plan", "./build", "./review", "./debug", "./git-ops"] }
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* **A declaration, never a heuristic.** Walking `node_modules` looking for files called `SKILL.md` would
|
|
13
|
+
* find a package's test fixtures, its examples, and its vendored copies of someone else's skills — and
|
|
14
|
+
* would then offer to install them as spawnable sub-agents. A package that says which of its files are
|
|
15
|
+
* skills has said so on purpose, and that is the only list this reads.
|
|
16
|
+
*
|
|
17
|
+
* What it deliberately does NOT do: scan `~/.pi/agent/skills/`. Definitions already in a skill root are
|
|
18
|
+
* discovered by `loadDefinitions` and governed as they stand; copying them into a project would duplicate
|
|
19
|
+
* them under a name that shadows the original (project wins on collision), which is a change nobody asked
|
|
20
|
+
* for.
|
|
21
|
+
*
|
|
22
|
+
* **Everything read here comes from a third party**, so this module is also where the refusals live: a
|
|
23
|
+
* name, a declared capability id, or a path that cannot safely be written into a generated file is refused
|
|
24
|
+
* with a reason rather than passed on (R-77, R-78, R-80). `init` generates a shell file an operator
|
|
25
|
+
* `source`s; the only strings that may reach it are ones that survived a whitelist here.
|
|
26
|
+
*/
|
|
27
|
+
import { type SkillDefinition } from "./definitions.ts";
|
|
28
|
+
import { type Capability } from "./resolve.ts";
|
|
29
|
+
export interface DiscoveredSkill {
|
|
30
|
+
definition: SkillDefinition;
|
|
31
|
+
/** The file verbatim. `init` copies it rather than regenerating it, so nothing is lost in a round trip. */
|
|
32
|
+
text: string;
|
|
33
|
+
path: string;
|
|
34
|
+
}
|
|
35
|
+
/** Why a declared skill was refused before it could be planned. Each has a different fix. */
|
|
36
|
+
export type RefusalReason =
|
|
37
|
+
/** The name cannot be a capability id, a line in a sourced file, or a path segment. */
|
|
38
|
+
"unsafe-name"
|
|
39
|
+
/** A declared `allowed-tools` entry cannot be one either — R-78, the sibling of R-77. */
|
|
40
|
+
| "unsafe-capability"
|
|
41
|
+
/** The declaration claims `tool:*` or `agent:*`: root authority, which a package may not hand itself. */
|
|
42
|
+
| "wildcard"
|
|
43
|
+
/** The bytes are not valid UTF-8, so "copied verbatim" could not be honoured. */
|
|
44
|
+
| "not-utf8";
|
|
45
|
+
export interface RefusedSkill {
|
|
46
|
+
/** The `pi.skills` entry or the definition name, whichever the operator can act on. */
|
|
47
|
+
subject: string;
|
|
48
|
+
reason: RefusalReason;
|
|
49
|
+
/** The offending id(s), when the reason names any. */
|
|
50
|
+
detail: string[];
|
|
51
|
+
}
|
|
52
|
+
export interface SkillPackage {
|
|
53
|
+
name: string;
|
|
54
|
+
version: string;
|
|
55
|
+
/** Paths `pi.skills` named that could not be read as a `SKILL.md`, so the report can say so out loud. */
|
|
56
|
+
unreadable: string[];
|
|
57
|
+
/** Skills refused for a reason that would otherwise reach a generated file. Never silently dropped. */
|
|
58
|
+
refused: RefusedSkill[];
|
|
59
|
+
skills: DiscoveredSkill[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* May this definition's name be written into a capability id, a shell file and a path?
|
|
63
|
+
*
|
|
64
|
+
* **Measured before it was written, and it was a defect in this module's first version (R-77).** A
|
|
65
|
+
* definition's identity is its directory name, and `init` interpolates that name into three places at once:
|
|
66
|
+
* `agent:<name>` inside a **comma-separated** `PI_GRANTS_GRANT`, a `.pi/grants.env` an operator **sources**,
|
|
67
|
+
* and the path it writes the copy to. An installed package with a directory called `a,tool:bash` produced
|
|
68
|
+
*
|
|
69
|
+
* ```
|
|
70
|
+
* export PI_GRANTS_GRANT="agent:a,tool:bash,tool:delegate,tool:read"
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* — `tool:bash` in an operator's grant, declared by nobody. A quote character reaches a file that gets
|
|
74
|
+
* `source`d, and a name of `..` writes outside `.pi/skills/`. One rule closes all three, and it is
|
|
75
|
+
* deliberately a **whitelist**: the safe set here is small and the unsafe set is the rest of Unicode.
|
|
76
|
+
*
|
|
77
|
+
* The first character must be alphanumeric, so `..` and dotfiles are refused along with everything else.
|
|
78
|
+
*/
|
|
79
|
+
export declare function isSafeName(name: string): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* May this DECLARED capability id be written into the generated grant? — R-78.
|
|
82
|
+
*
|
|
83
|
+
* **R-77's other half, and the reason a whitelist beats a blocklist twice.** R-77 was closed on the name
|
|
84
|
+
* channel; the `allowed-tools` *value* travels to the identical interpolation site and was unchecked, so a
|
|
85
|
+
* package declaring
|
|
86
|
+
*
|
|
87
|
+
* ```yaml
|
|
88
|
+
* allowed-tools: Read,ext:x";touch /tmp/pwned;PI_GRANTS_GRANT="
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* produced a `.pi/grants.env` that executed arbitrary code the moment the operator ran the `source` line
|
|
92
|
+
* `init` itself prints. Reproduced end to end before this existed. `ceilingForDefinition` passes `ext:`,
|
|
93
|
+
* `skill:` and `agent:` entries through **as written** by design (a translation table would invent or drop
|
|
94
|
+
* grants), which is right for the enforcement path — the catalog refuses what it does not know — and is
|
|
95
|
+
* exactly why the check has to be here, at the boundary that *generates* rather than the one that enforces.
|
|
96
|
+
*
|
|
97
|
+
* The grammar is the one `docs/SPEC.md` documents: `tool:<name>`, `skill:<name>`, `agent:<name>`, and
|
|
98
|
+
* `ext:<pkg>/<tool>` where `<pkg>` may be npm-scoped. No wildcards — those are refused separately and
|
|
99
|
+
* loudly, because "you tried to grant yourself everything" is a different fact from "that is not a name".
|
|
100
|
+
*/
|
|
101
|
+
export declare function isSafeCapability(id: Capability): boolean;
|
|
102
|
+
/** Read one installed package, if it declares skills. `null` means "not a skill package", not an error. */
|
|
103
|
+
export declare function readSkillPackage(packageDir: string): Promise<SkillPackage | null>;
|
|
104
|
+
/**
|
|
105
|
+
* Every installed package under `<cwd>/node_modules` that declares `pi.skills`, sorted by name.
|
|
106
|
+
*
|
|
107
|
+
* Top level and one scope deep, which is what npm's layout has. Nothing recurses into a dependency's own
|
|
108
|
+
* `node_modules`: a transitive skill package is not something an operator asked to install definitions
|
|
109
|
+
* from, and scaffolding one into their project would be a surprise wearing a helpful face.
|
|
110
|
+
*/
|
|
111
|
+
/**
|
|
112
|
+
* Every place a pi package can be installed, most specific first (R-75).
|
|
113
|
+
*
|
|
114
|
+
* **Two roots, because pi has two install paths and only one of them is `npm install`.** `pi install
|
|
115
|
+
* npm:principal-pi-skills` — the documented, pi-native way, and the only one that registers the package so
|
|
116
|
+
* pi will auto-load its extension — puts it in `$PI_CODING_AGENT_DIR/npm/node_modules`, and leaves the
|
|
117
|
+
* project without a `node_modules` at all. Searching only `<cwd>/node_modules` therefore found **nothing**
|
|
118
|
+
* for an operator who followed pi's own instructions, and said "install principal-pi-skills" to somebody
|
|
119
|
+
* who just had.
|
|
120
|
+
*
|
|
121
|
+
* Measured, in a fresh `PI_CODING_AGENT_DIR` with an empty project: `pi install` populates the agent root
|
|
122
|
+
* and creates no project root.
|
|
123
|
+
*
|
|
124
|
+
* Project first when both exist, because a package pinned in the repository is the one the team agreed on.
|
|
125
|
+
*/
|
|
126
|
+
export declare function skillPackageRoots(cwd: string): string[];
|
|
127
|
+
export declare function discoverSkillPackages(cwd: string): Promise<SkillPackage[]>;
|
|
128
|
+
//# sourceMappingURL=skill-packages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-packages.d.ts","sourceRoot":"","sources":["../src/skill-packages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAKH,OAAO,EAA8C,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEpG,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/D,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,eAAe,CAAC;IAC5B,2GAA2G;IAC3G,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,6FAA6F;AAC7F,MAAM,MAAM,aAAa;AACvB,uFAAuF;AACrF,aAAa;AACf,yFAAyF;GACvF,mBAAmB;AACrB,yGAAyG;GACvG,UAAU;AACZ,iFAAiF;GAC/E,UAAU,CAAC;AAEf,MAAM,WAAW,YAAY;IAC3B,uFAAuF;IACvF,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,aAAa,CAAC;IACtB,sDAAsD;IACtD,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,yGAAyG;IACzG,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,uGAAuG;IACvG,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,UAAU,GAAG,OAAO,CAMxD;AA2DD,2GAA2G;AAC3G,wBAAsB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAkCvF;AAED;;;;;;GAMG;AACH;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAGvD;AAED,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAgBhF"}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which installed npm packages ship `SKILL.md` definitions — read from their own manifests.
|
|
3
|
+
*
|
|
4
|
+
* `pi-daddy init` scaffolds a governed project from whatever skill packages are already installed, and
|
|
5
|
+
* this is how it finds them: **a package declares its skills in `package.json`'s `pi.skills` array**, which
|
|
6
|
+
* is pi's own convention and how pi itself loads them. Measured against `principal-pi-skills@2.3.1`:
|
|
7
|
+
*
|
|
8
|
+
* ```json
|
|
9
|
+
* "pi": { "skills": ["./decide", "./architect", "./plan", "./build", "./review", "./debug", "./git-ops"] }
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* **A declaration, never a heuristic.** Walking `node_modules` looking for files called `SKILL.md` would
|
|
13
|
+
* find a package's test fixtures, its examples, and its vendored copies of someone else's skills — and
|
|
14
|
+
* would then offer to install them as spawnable sub-agents. A package that says which of its files are
|
|
15
|
+
* skills has said so on purpose, and that is the only list this reads.
|
|
16
|
+
*
|
|
17
|
+
* What it deliberately does NOT do: scan `~/.pi/agent/skills/`. Definitions already in a skill root are
|
|
18
|
+
* discovered by `loadDefinitions` and governed as they stand; copying them into a project would duplicate
|
|
19
|
+
* them under a name that shadows the original (project wins on collision), which is a change nobody asked
|
|
20
|
+
* for.
|
|
21
|
+
*
|
|
22
|
+
* **Everything read here comes from a third party**, so this module is also where the refusals live: a
|
|
23
|
+
* name, a declared capability id, or a path that cannot safely be written into a generated file is refused
|
|
24
|
+
* with a reason rather than passed on (R-77, R-78, R-80). `init` generates a shell file an operator
|
|
25
|
+
* `source`s; the only strings that may reach it are ones that survived a whitelist here.
|
|
26
|
+
*/
|
|
27
|
+
import { readdir, readFile, realpath } from "node:fs/promises";
|
|
28
|
+
import { homedir } from "node:os";
|
|
29
|
+
import { join, resolve, sep } from "node:path";
|
|
30
|
+
import { ceilingForDefinition, parseSkillDefinition } from "./definitions.js";
|
|
31
|
+
import { WILDCARD } from "./pi-tools.js";
|
|
32
|
+
import { AGENT_WILDCARD } from "./resolve.js";
|
|
33
|
+
/**
|
|
34
|
+
* May this definition's name be written into a capability id, a shell file and a path?
|
|
35
|
+
*
|
|
36
|
+
* **Measured before it was written, and it was a defect in this module's first version (R-77).** A
|
|
37
|
+
* definition's identity is its directory name, and `init` interpolates that name into three places at once:
|
|
38
|
+
* `agent:<name>` inside a **comma-separated** `PI_GRANTS_GRANT`, a `.pi/grants.env` an operator **sources**,
|
|
39
|
+
* and the path it writes the copy to. An installed package with a directory called `a,tool:bash` produced
|
|
40
|
+
*
|
|
41
|
+
* ```
|
|
42
|
+
* export PI_GRANTS_GRANT="agent:a,tool:bash,tool:delegate,tool:read"
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* — `tool:bash` in an operator's grant, declared by nobody. A quote character reaches a file that gets
|
|
46
|
+
* `source`d, and a name of `..` writes outside `.pi/skills/`. One rule closes all three, and it is
|
|
47
|
+
* deliberately a **whitelist**: the safe set here is small and the unsafe set is the rest of Unicode.
|
|
48
|
+
*
|
|
49
|
+
* The first character must be alphanumeric, so `..` and dotfiles are refused along with everything else.
|
|
50
|
+
*/
|
|
51
|
+
export function isSafeName(name) {
|
|
52
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* May this DECLARED capability id be written into the generated grant? — R-78.
|
|
56
|
+
*
|
|
57
|
+
* **R-77's other half, and the reason a whitelist beats a blocklist twice.** R-77 was closed on the name
|
|
58
|
+
* channel; the `allowed-tools` *value* travels to the identical interpolation site and was unchecked, so a
|
|
59
|
+
* package declaring
|
|
60
|
+
*
|
|
61
|
+
* ```yaml
|
|
62
|
+
* allowed-tools: Read,ext:x";touch /tmp/pwned;PI_GRANTS_GRANT="
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* produced a `.pi/grants.env` that executed arbitrary code the moment the operator ran the `source` line
|
|
66
|
+
* `init` itself prints. Reproduced end to end before this existed. `ceilingForDefinition` passes `ext:`,
|
|
67
|
+
* `skill:` and `agent:` entries through **as written** by design (a translation table would invent or drop
|
|
68
|
+
* grants), which is right for the enforcement path — the catalog refuses what it does not know — and is
|
|
69
|
+
* exactly why the check has to be here, at the boundary that *generates* rather than the one that enforces.
|
|
70
|
+
*
|
|
71
|
+
* The grammar is the one `docs/SPEC.md` documents: `tool:<name>`, `skill:<name>`, `agent:<name>`, and
|
|
72
|
+
* `ext:<pkg>/<tool>` where `<pkg>` may be npm-scoped. No wildcards — those are refused separately and
|
|
73
|
+
* loudly, because "you tried to grant yourself everything" is a different fact from "that is not a name".
|
|
74
|
+
*/
|
|
75
|
+
export function isSafeCapability(id) {
|
|
76
|
+
const segment = "[A-Za-z0-9][A-Za-z0-9._-]*";
|
|
77
|
+
return (new RegExp(`^(tool|skill|agent):${segment}$`).test(id) ||
|
|
78
|
+
new RegExp(`^ext:(@${segment}/)?${segment}/${segment}$`).test(id));
|
|
79
|
+
}
|
|
80
|
+
/** The two ids that confer root authority. A package declaring one is claiming it, not describing a need. */
|
|
81
|
+
function wildcardsIn(capabilities) {
|
|
82
|
+
return capabilities.filter((c) => c === WILDCARD || c === AGENT_WILDCARD);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Everything about one declared skill that would make it unsafe to scaffold. `null` when it is fine.
|
|
86
|
+
*
|
|
87
|
+
* Ordered so the operator is told the most actionable thing: a wildcard is a deliberate claim, an unsafe id
|
|
88
|
+
* is probably a typo or an attack, and a bad name is neither.
|
|
89
|
+
*/
|
|
90
|
+
function refusalFor(skill) {
|
|
91
|
+
const name = skill.definition.name;
|
|
92
|
+
if (!isSafeName(name))
|
|
93
|
+
return { subject: name, reason: "unsafe-name", detail: [name] };
|
|
94
|
+
const ceiling = ceilingForDefinition(skill.definition);
|
|
95
|
+
const wildcards = wildcardsIn(ceiling.capabilities);
|
|
96
|
+
if (wildcards.length > 0)
|
|
97
|
+
return { subject: name, reason: "wildcard", detail: wildcards };
|
|
98
|
+
const unsafe = ceiling.capabilities.filter((c) => !isSafeCapability(c));
|
|
99
|
+
if (unsafe.length > 0)
|
|
100
|
+
return { subject: name, reason: "unsafe-capability", detail: unsafe };
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
/** One `pi.skills` entry: a directory holding `SKILL.md`, or a `.md` file — the same two shapes pi allows. */
|
|
104
|
+
async function readSkill(packageDir, entry) {
|
|
105
|
+
const target = resolve(packageDir, entry);
|
|
106
|
+
// A manifest is data from another package, so an entry escaping its own directory is refused rather than
|
|
107
|
+
// followed. **`realpath`, not a lexical prefix test** (R-80): `resolve()` normalises `..` and knows
|
|
108
|
+
// nothing about symlinks, so a packaged symlink walked straight past the first version of this check and
|
|
109
|
+
// a definition from outside the package was copied in, with its `allowed-tools` landing in the operator's
|
|
110
|
+
// grant. Measured. This is the same lesson as the `realpathSync` fix in `cli.ts`, which was found by the
|
|
111
|
+
// smoke test one day earlier and not applied here.
|
|
112
|
+
const realPackageDir = await realpath(packageDir).catch(() => packageDir);
|
|
113
|
+
for (const path of [join(target, "SKILL.md"), ...(target.endsWith(".md") ? [target] : [])]) {
|
|
114
|
+
let bytes;
|
|
115
|
+
try {
|
|
116
|
+
bytes = await readFile(path);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const realPath = await realpath(path).catch(() => path);
|
|
122
|
+
if (!realPath.startsWith(realPackageDir + sep))
|
|
123
|
+
return null;
|
|
124
|
+
// "The file verbatim … nothing is lost in a round trip" is a claim this module makes, so bytes that
|
|
125
|
+
// cannot survive the round trip are refused rather than silently replaced. A latin-1 `0xE9` used to come
|
|
126
|
+
// back as U+FFFD, changing the file's length and its digest, with no warning.
|
|
127
|
+
const text = bytes.toString("utf8");
|
|
128
|
+
if (!Buffer.from(text, "utf8").equals(bytes))
|
|
129
|
+
return "not-utf8";
|
|
130
|
+
const definition = parseSkillDefinition(path, text);
|
|
131
|
+
return definition ? { definition, text, path } : null;
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
/** Read one installed package, if it declares skills. `null` means "not a skill package", not an error. */
|
|
136
|
+
export async function readSkillPackage(packageDir) {
|
|
137
|
+
let manifest;
|
|
138
|
+
try {
|
|
139
|
+
manifest = JSON.parse(await readFile(join(packageDir, "package.json"), "utf8"));
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
const declared = manifest.pi?.skills;
|
|
145
|
+
if (!Array.isArray(declared) || declared.length === 0)
|
|
146
|
+
return null;
|
|
147
|
+
const skills = [];
|
|
148
|
+
const unreadable = [];
|
|
149
|
+
const refused = [];
|
|
150
|
+
for (const entry of declared) {
|
|
151
|
+
if (typeof entry !== "string")
|
|
152
|
+
continue;
|
|
153
|
+
const skill = await readSkill(packageDir, entry);
|
|
154
|
+
if (skill === null) {
|
|
155
|
+
unreadable.push(entry);
|
|
156
|
+
}
|
|
157
|
+
else if (skill === "not-utf8") {
|
|
158
|
+
refused.push({ subject: entry, reason: "not-utf8", detail: [] });
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
const refusal = refusalFor(skill);
|
|
162
|
+
if (refusal)
|
|
163
|
+
refused.push(refusal);
|
|
164
|
+
else
|
|
165
|
+
skills.push(skill);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
name: manifest.name ?? packageDir.split(sep).pop() ?? "(unnamed)",
|
|
170
|
+
version: manifest.version ?? "(no version)",
|
|
171
|
+
unreadable,
|
|
172
|
+
refused,
|
|
173
|
+
skills,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Every installed package under `<cwd>/node_modules` that declares `pi.skills`, sorted by name.
|
|
178
|
+
*
|
|
179
|
+
* Top level and one scope deep, which is what npm's layout has. Nothing recurses into a dependency's own
|
|
180
|
+
* `node_modules`: a transitive skill package is not something an operator asked to install definitions
|
|
181
|
+
* from, and scaffolding one into their project would be a surprise wearing a helpful face.
|
|
182
|
+
*/
|
|
183
|
+
/**
|
|
184
|
+
* Every place a pi package can be installed, most specific first (R-75).
|
|
185
|
+
*
|
|
186
|
+
* **Two roots, because pi has two install paths and only one of them is `npm install`.** `pi install
|
|
187
|
+
* npm:principal-pi-skills` — the documented, pi-native way, and the only one that registers the package so
|
|
188
|
+
* pi will auto-load its extension — puts it in `$PI_CODING_AGENT_DIR/npm/node_modules`, and leaves the
|
|
189
|
+
* project without a `node_modules` at all. Searching only `<cwd>/node_modules` therefore found **nothing**
|
|
190
|
+
* for an operator who followed pi's own instructions, and said "install principal-pi-skills" to somebody
|
|
191
|
+
* who just had.
|
|
192
|
+
*
|
|
193
|
+
* Measured, in a fresh `PI_CODING_AGENT_DIR` with an empty project: `pi install` populates the agent root
|
|
194
|
+
* and creates no project root.
|
|
195
|
+
*
|
|
196
|
+
* Project first when both exist, because a package pinned in the repository is the one the team agreed on.
|
|
197
|
+
*/
|
|
198
|
+
export function skillPackageRoots(cwd) {
|
|
199
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
|
200
|
+
return [join(cwd, "node_modules"), join(agentDir, "npm", "node_modules")];
|
|
201
|
+
}
|
|
202
|
+
export async function discoverSkillPackages(cwd) {
|
|
203
|
+
const dirs = [];
|
|
204
|
+
const seenNames = new Set();
|
|
205
|
+
for (const root of skillPackageRoots(cwd))
|
|
206
|
+
await collectFrom(root, dirs);
|
|
207
|
+
const packages = [];
|
|
208
|
+
for (const dir of dirs) {
|
|
209
|
+
const found = await readSkillPackage(dir);
|
|
210
|
+
// First root wins on a name collision: the project's pinned copy outranks the machine-wide one, and
|
|
211
|
+
// silently preferring the other would make a committed lockfile stop meaning anything.
|
|
212
|
+
if (found && !seenNames.has(found.name)) {
|
|
213
|
+
seenNames.add(found.name);
|
|
214
|
+
packages.push(found);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return packages.sort((a, b) => a.name.localeCompare(b.name));
|
|
218
|
+
}
|
|
219
|
+
/** Append every package directory under one `node_modules`, scoped packages included. */
|
|
220
|
+
async function collectFrom(root, dirs) {
|
|
221
|
+
let entries;
|
|
222
|
+
try {
|
|
223
|
+
entries = await readdir(root);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return; // a root that does not exist is a normal state, not a failure
|
|
227
|
+
}
|
|
228
|
+
for (const entry of entries.sort()) {
|
|
229
|
+
if (entry.startsWith("."))
|
|
230
|
+
continue; // .bin, .package-lock.json
|
|
231
|
+
if (entry.startsWith("@")) {
|
|
232
|
+
try {
|
|
233
|
+
for (const scoped of (await readdir(join(root, entry))).sort())
|
|
234
|
+
dirs.push(join(root, entry, scoped));
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
dirs.push(join(root, entry));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
//# sourceMappingURL=skill-packages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-packages.js","sourceRoot":"","sources":["../src/skill-packages.ts"],"names":[],"mappings":"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"}
|
|
@@ -43,6 +43,15 @@ export interface GrantsCommandContext {
|
|
|
43
43
|
* R-28 discipline where it belongs: this command asks what would happen instead of working it out.
|
|
44
44
|
*/
|
|
45
45
|
previewDelegation: (name: string) => Promise<GatedPlan>;
|
|
46
|
+
/**
|
|
47
|
+
* Run `/grants init`: scaffold definitions, ask about the withheld capabilities only, store the answer
|
|
48
|
+
* outside the workspace, and apply it to this session (ADR-0030).
|
|
49
|
+
*
|
|
50
|
+
* Injected rather than imported so this module stays what it is — a read-only diagnostic that cannot
|
|
51
|
+
* become a governance path by accident. The one command here that *writes* takes its ability to do so
|
|
52
|
+
* from the caller, visibly.
|
|
53
|
+
*/
|
|
54
|
+
runInit: () => Promise<void>;
|
|
46
55
|
}
|
|
47
56
|
|
|
48
57
|
/**
|
|
@@ -51,6 +60,9 @@ export interface GrantsCommandContext {
|
|
|
51
60
|
*/
|
|
52
61
|
const PREVIEW_LIMIT = 12;
|
|
53
62
|
|
|
63
|
+
/** The verbs `/grants` answers to. Anything else is refused rather than silently treated as no verb. */
|
|
64
|
+
const KNOWN_SUBCOMMANDS: readonly string[] = ["init", "ledger", "approvals", "revoke"];
|
|
65
|
+
|
|
54
66
|
export const grantsCommand = {
|
|
55
67
|
description:
|
|
56
68
|
"Show this session's capability grant, delegation depth, and known agent-type ceilings; " +
|
|
@@ -65,6 +77,11 @@ handler: async (args: string, ctx: any) => {
|
|
|
65
77
|
|
|
66
78
|
const [sub, target] = args.trim().split(/\s+/).filter(Boolean);
|
|
67
79
|
|
|
80
|
+
if (sub === "init") {
|
|
81
|
+
await ctx.grants.runInit();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
68
85
|
if (sub === "ledger") {
|
|
69
86
|
// The detector, made reachable. `verifyLedger` exists because nothing in this package had ever read
|
|
70
87
|
// a ledger back, so a torn line was indistinguishable from a spawn that never happened — and a
|
|
@@ -265,6 +282,20 @@ handler: async (args: string, ctx: any) => {
|
|
|
265
282
|
return;
|
|
266
283
|
}
|
|
267
284
|
|
|
285
|
+
// **An unrecognised subcommand is refused, not ignored (R-74).** Every `sub` that is not a known verb
|
|
286
|
+
// fell through to this status screen with the word silently dropped, so `/grants init` — a command that
|
|
287
|
+
// does not exist — printed a healthy-looking report and read as though it had run. `/grants ledgr` did
|
|
288
|
+
// the same. A diagnostic that answers a question nobody asked is worse than one that refuses: it is
|
|
289
|
+
// indistinguishable from success, which is the shape this package spends most of its risk register on.
|
|
290
|
+
if (sub !== undefined && !KNOWN_SUBCOMMANDS.includes(sub)) {
|
|
291
|
+
ctx.ui.notify(
|
|
292
|
+
`grants: unknown subcommand "${sub}" — did nothing. Known: ${KNOWN_SUBCOMMANDS.join(", ")}. ` +
|
|
293
|
+
`Plain /grants shows this session's status.`,
|
|
294
|
+
"warning",
|
|
295
|
+
);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
268
299
|
const { valid } = await loadApprovals({ cwd, now: new Date(), snapshotOf });
|
|
269
300
|
const lines = [
|
|
270
301
|
governed ? "grants: ACTIVE" : "grants: inactive (set PI_GRANTS_GRANT to govern this session)",
|
package/extensions/grants.ts
CHANGED
|
@@ -30,12 +30,17 @@ import { legacyApprovalsPath, sharedApprovalsPath } from "../src/approval-store.
|
|
|
30
30
|
import { buildCatalog } from "../src/catalog.ts";
|
|
31
31
|
import { loadDefinitions } from "../src/definitions.ts";
|
|
32
32
|
import { appendRecord, buildRecord, verifyLedger } from "../src/ledger.ts";
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
ENV_GRANT, deriveOwnGrant, observeToolNames } from "../src/propagation.ts";
|
|
34
35
|
import { snapshotOf } from "./approvals.ts";
|
|
35
36
|
import { registerDelegationTools } from "./delegation.ts";
|
|
36
37
|
import { grantsCommand } from "./grants-command.ts";
|
|
38
|
+
import { runInit } from "./init-command.ts";
|
|
39
|
+
import type { Capability } from "../src/resolve.ts";
|
|
40
|
+
|
|
37
41
|
import { planWithApprovals } from "./run-delegation.ts";
|
|
38
|
-
import { createGrantsSession } from "./session.ts";
|
|
42
|
+
import { createGrantsSession, loadProjectDefinitions, type GrantsSession } from "./session.ts";
|
|
43
|
+
import { renderSpawnableSummary, summariseSpawnable } from "./spawn-summary.ts";
|
|
39
44
|
|
|
40
45
|
const SPAWN_TOOLS = new Set(["Agent", "subagent", "spawn_agent"]);
|
|
41
46
|
|
|
@@ -59,6 +64,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
59
64
|
|
|
60
65
|
pi.on("session_start", async (_event, ctx) => {
|
|
61
66
|
session.cwd = ctx.cwd;
|
|
67
|
+
// The one case the stored-grant lookup can get wrong (ADR-0030). The factory reads the store keyed by
|
|
68
|
+
// `process.cwd()` because it runs before any hook and therefore before `ctx` exists — and S-5 forces
|
|
69
|
+
// that ordering, since whether `delegate` is registered is decided there. Almost always the two agree.
|
|
70
|
+
// When they do not, the session is governed by a different directory's decision than the one it is
|
|
71
|
+
// working in, which is exactly the confusion a grant must never cause, so it is said out loud rather
|
|
72
|
+
// than left to be inferred from a surprising refusal. Sync, so it is not the R-60 shape.
|
|
73
|
+
if (session.storeCwd !== ctx.cwd && process.env[ENV_GRANT] === undefined) {
|
|
74
|
+
ctx.ui.notify(
|
|
75
|
+
`grants: this session's stored grant was read for ${session.storeCwd}, but pi is working in ` +
|
|
76
|
+
`${ctx.cwd}. A grant belongs to a directory, so run /grants init here, or set PI_GRANTS_GRANT ` +
|
|
77
|
+
`explicitly — the environment always wins.`,
|
|
78
|
+
"warning",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
62
81
|
try {
|
|
63
82
|
// Guarded together, and guarded at all because of R-60 rather than because either one throws today:
|
|
64
83
|
// both loaders swallow their own filesystem errors, so this catch is currently unreachable. The point
|
|
@@ -68,9 +87,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
68
87
|
// delegate by `tools:`, and an operator whose `agent:` spawns have all started failing deserves to
|
|
69
88
|
// know it was the *scan* that broke rather than the grant.
|
|
70
89
|
try {
|
|
71
|
-
|
|
72
|
-
session.catalogReady = buildCatalog({ cwd: ctx.cwd, observedTools: session.observedTools });
|
|
73
|
-
session.catalog = await session.catalogReady;
|
|
90
|
+
await loadProjectDefinitions(session, ctx.cwd);
|
|
74
91
|
} catch (error) {
|
|
75
92
|
ctx.ui.notify(
|
|
76
93
|
`grants: could not read this project's definitions or capability catalog ` +
|
|
@@ -207,6 +224,35 @@ export default function (pi: ExtensionAPI) {
|
|
|
207
224
|
`grants: depth ${session.depth}/${session.maxDepth}, holding [${session.ownGrant.join(", ") || "nothing"}]`,
|
|
208
225
|
"info",
|
|
209
226
|
);
|
|
227
|
+
// B1 / P4. The grant alone never named the definitions, never said where they came from, and never
|
|
228
|
+
// said which ones were being WITHHELD — so an operator who had just installed a package of
|
|
229
|
+
// `SKILL.md` files could not tell governance-is-working from did-the-install-fail. Classified by the
|
|
230
|
+
// real planner (see `./spawn-summary.ts`), never by a second reading of the rules.
|
|
231
|
+
//
|
|
232
|
+
// Its own try/catch, and not because `summariseSpawnable` throws today: this is the R-60 shape
|
|
233
|
+
// exactly — one added `await` inside the blanket catch cancelling every control below it in
|
|
234
|
+
// silence. There is nothing below it now; there will be.
|
|
235
|
+
try {
|
|
236
|
+
const line = renderSpawnableSummary(
|
|
237
|
+
await summariseSpawnable(
|
|
238
|
+
session.definitions,
|
|
239
|
+
(name) => planWithApprovals(session, { task: "(preview)", agent: name }, {}, null),
|
|
240
|
+
// The session facts that make every per-definition verdict identical. `mayDelegate` in
|
|
241
|
+
// particular: without `tool:delegate` there is no delegate tool at all, and the line used to
|
|
242
|
+
// report definitions as spawnable in the one session where nothing can ever be spawned.
|
|
243
|
+
{ mayDelegate: session.mayDelegate, depth: session.depth, maxDepth: session.maxDepth },
|
|
244
|
+
),
|
|
245
|
+
session.definitions.size,
|
|
246
|
+
);
|
|
247
|
+
if (line) ctx.ui.notify(line, "info");
|
|
248
|
+
} catch (error) {
|
|
249
|
+
ctx.ui.notify(
|
|
250
|
+
`grants: could not work out which definitions are spawnable ` +
|
|
251
|
+
`(${error instanceof Error ? error.message : String(error)}) — run /grants for the per-definition ` +
|
|
252
|
+
`verdict. Nothing about the grant or its enforcement depends on this line.`,
|
|
253
|
+
"warning",
|
|
254
|
+
);
|
|
255
|
+
}
|
|
210
256
|
}
|
|
211
257
|
} catch (error) {
|
|
212
258
|
// Rule 8 — fail closed, and be LOUD about it. Swallowing is still right: a startup fault must not
|
|
@@ -338,9 +384,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
338
384
|
// says so: stored approvals count exactly as they would for a spawn, and no human is asked
|
|
339
385
|
// (R-38). Passing `ctx` here would let `/grants` raise a dialog, and passing `hasUI: false` would
|
|
340
386
|
// make every gated definition report "no interactive user" instead of what actually blocks it.
|
|
387
|
+
runInit: () =>
|
|
388
|
+
runInit(session, ctx, async () => {
|
|
389
|
+
await loadProjectDefinitions(session, ctx.cwd);
|
|
390
|
+
delegation.refreshSpawnable();
|
|
391
|
+
}),
|
|
341
392
|
previewDelegation: (name: string) =>
|
|
342
393
|
planWithApprovals(session, { task: "(preview)", agent: name }, {}, null),
|
|
343
394
|
},
|
|
344
395
|
}),
|
|
345
396
|
});
|
|
346
397
|
}
|
|
398
|
+
|