@llblab/pi-actors 0.34.1 → 0.36.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/AGENTS.md +3 -1
- package/BACKLOG.md +4 -90
- package/CHANGELOG.md +14 -2
- package/README.md +12 -2
- package/dist/lib/command-templates.d.ts +2 -0
- package/dist/lib/command-templates.js +76 -0
- package/dist/lib/prompts.d.ts +1 -0
- package/dist/lib/prompts.js +1 -0
- package/dist/lib/recipes-discovery.d.ts +2 -0
- package/dist/lib/recipes-discovery.js +77 -5
- package/dist/lib/registry.d.ts +3 -0
- package/dist/lib/registry.js +60 -1
- package/dist/lib/runtime-notifier.js +8 -3
- package/dist/lib/tools-inspect.js +172 -4
- package/dist/lib/tools-register.js +1 -0
- package/dist/lib/tools-response.js +13 -2
- package/dist/scripts/validate-recipe.mjs +164 -7
- package/dist/skills/actors/SKILL.md +28 -11
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/recipe-library.md +2 -0
- package/docs/template-recipes.md +2 -0
- package/docs/tool-registry.md +17 -12
- package/lib/command-templates.ts +124 -0
- package/lib/prompts.ts +2 -0
- package/lib/recipes-discovery.ts +98 -6
- package/lib/registry.ts +94 -1
- package/lib/runtime-notifier.ts +9 -3
- package/lib/tools-inspect.ts +186 -4
- package/lib/tools-register.ts +1 -0
- package/lib/tools-response.ts +16 -2
- package/package.json +3 -2
- package/scripts/validate-recipe.mjs +164 -7
- package/skills/actors/SKILL.md +28 -11
- package/skills/swarm/SKILL.md +1 -1
package/AGENTS.md
CHANGED
|
@@ -113,9 +113,11 @@ Pi host
|
|
|
113
113
|
- `~/.pi/agent/recipes/*.json` is executable muscle memory: recipes there become persistent tools by location.
|
|
114
114
|
- Preserve filename identity, atomic writes, explicit operator-gated changes, and local transportability.
|
|
115
115
|
- Packaged/ad hoc recipes outside the agent root are components, not user tools.
|
|
116
|
+
- Register existing recipes by importing them from the user-root wrapper and using a `{ "name": "alias" }` template node; do not duplicate a ready recipe's script command, defaults, mailbox, or artifact contract in the wrapper.
|
|
117
|
+
- Skill-owned scripts must be exposed through skill-owned recipes first. If a local tool needs that capability, import the skill recipe via `{agent}/skills/<skill>/recipes/<recipe>.json` instead of calling `{agent}/skills/<skill>/scripts/*` directly.
|
|
116
118
|
- Tool definitions use `template`, not `script`, and built-in/core tool names must not be shadowed.
|
|
117
119
|
- Packaged recipe growth is demand-driven: prefer reusable components over speculative scenario catalogs.
|
|
118
|
-
- Recipe templates may point directly at executable helper scripts; keep script executable bits and avoid unnecessary `node` prefixes.
|
|
120
|
+
- Recipe templates may point directly at executable helper scripts when the recipe owns that script boundary; keep script executable bits and avoid unnecessary `node` prefixes.
|
|
119
121
|
|
|
120
122
|
## Command And Recipe Layers
|
|
121
123
|
|
package/BACKLOG.md
CHANGED
|
@@ -51,91 +51,9 @@ No open hotfix items.
|
|
|
51
51
|
|
|
52
52
|
## Minor Backlog
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
No open minor items.
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
- Priority: High.
|
|
59
|
-
- Status: Planned.
|
|
60
|
-
- Goal: Make successful ad hoc actor patterns easy to promote manually from draft memory into active user recipe memory.
|
|
61
|
-
- Why now: Draft recipes under `~/.pi/agent/recipes/drafts` are replayable but intentionally not active tools, and the two-stage memory model needs an explicit operator-gated promotion path.
|
|
62
|
-
- Direction:
|
|
63
|
-
- List draft recipes with source run, timestamp, fingerprint, description/template preview, and validation status.
|
|
64
|
-
- Promote a selected draft to `~/.pi/agent/recipes/<name>.json` only through an explicit action or explicit tool argument.
|
|
65
|
-
- Run recipe validation/doctor before writing and expose collision/shadowing diagnostics.
|
|
66
|
-
- Preserve draft files unless deletion is explicitly requested.
|
|
67
|
-
- Prefer extending existing registry/tool surfaces over adding a new public noun.
|
|
68
|
-
- Acceptance:
|
|
69
|
-
- Draft recipes remain non-tools until promotion.
|
|
70
|
-
- Promotion writes atomically and never auto-promotes.
|
|
71
|
-
- Tests cover valid promotion, invalid draft, name collision, and packaged-recipe shadowing.
|
|
72
|
-
- Docs explain draft memory vs active tool memory in one compact section.
|
|
73
|
-
|
|
74
|
-
### M-19 Recipe Doctor Risk Labels v2
|
|
75
|
-
|
|
76
|
-
- Priority: Medium.
|
|
77
|
-
- Status: Planned.
|
|
78
|
-
- Goal: Evolve recipe doctor into a compact capability-risk membrane without pretending to sandbox trusted local execution.
|
|
79
|
-
- Why now: Recipe doctor already has remediation UX; the next useful slice is deterministic advisory risk classification for local capabilities.
|
|
80
|
-
- Direction:
|
|
81
|
-
- Add advisory labels such as `risk.shell`, `risk.eval`, `risk.broad_fs_write`, `risk.destructive_fs`, `risk.network`, `risk.external_side_effect`, `risk.long_running`, `risk.platform_specific`, and `risk.secret_touching`.
|
|
82
|
-
- Keep labels advisory and deterministic; do not block execution unless existing validation already blocks it.
|
|
83
|
-
- Expose compact risk summaries in `inspect target=recipes view=doctor` and verbose per-recipe labels.
|
|
84
|
-
- Keep launch-time warnings quiet except for already-failing or clearly dangerous cases.
|
|
85
|
-
- Preserve honest wording: trusted local execution, not isolation.
|
|
86
|
-
- Acceptance:
|
|
87
|
-
- Risk labels are deterministic and tested.
|
|
88
|
-
- Existing risky shell-boundary diagnostics remain intact.
|
|
89
|
-
- Doctor output stays compact by default.
|
|
90
|
-
- README/docs do not introduce sandbox or security-boundary claims.
|
|
91
|
-
|
|
92
|
-
### M-20 Runtime Triage Surface
|
|
93
|
-
|
|
94
|
-
- Priority: Medium.
|
|
95
|
-
- Status: Planned.
|
|
96
|
-
- Goal: Add one compact operator triage view that answers what needs attention right now without performing repairs.
|
|
97
|
-
- Why now: Runtime status, recipe doctor, drafts, stale claims, session mismatches, failed runs, and other-session counts are currently separate bounded surfaces.
|
|
98
|
-
- Direction:
|
|
99
|
-
- Add `inspect target=tool:pi-actors view=triage` or an equivalent existing inspect surface.
|
|
100
|
-
- Summarize runtime version/mode, active runs, other-session runs, invalid or blocking recipes, high-risk recipes, draft recipes, stale worker claims, recent failed runs, attention messages, and suggested next inspect actions.
|
|
101
|
-
- Keep every warning tied to a next inspect/action hint.
|
|
102
|
-
- Do not auto-repair, auto-prune, relax ownership, or hide detailed source-of-truth views.
|
|
103
|
-
- Acceptance:
|
|
104
|
-
- Triage output is compact enough for agent context.
|
|
105
|
-
- Healthy and degraded states are covered by tests.
|
|
106
|
-
- Detailed inspect/doctor/status views remain source of truth.
|
|
107
|
-
|
|
108
|
-
### M-21 Packaged Recipe QA Matrix
|
|
109
|
-
|
|
110
|
-
- Priority: Medium.
|
|
111
|
-
- Status: Planned.
|
|
112
|
-
- Goal: Prevent packaged recipes from drifting into inconsistent mailbox, artifact, platform, or package-root behavior.
|
|
113
|
-
- Why now: Packaged recipes are standard-library components; they should be boringly consistent before operators copy or register them as durable local capabilities.
|
|
114
|
-
- Direction:
|
|
115
|
-
- Add an internal QA check over `recipes/*.json` for descriptions, async mailbox contracts, termination vocabulary, artifact declarations, platform notes, installed-package-safe helper paths, and compiled shim coverage.
|
|
116
|
-
- Keep `control.kill` as generic runtime termination and allow `control.stop` / `control.cancel` only as actor-domain vocabulary.
|
|
117
|
-
- Fail with exact recipe/path/key diagnostics.
|
|
118
|
-
- Avoid a broad recipe-library rewrite beyond violations discovered by the check.
|
|
119
|
-
- Acceptance:
|
|
120
|
-
- QA runs under an existing validation command or a clearly named subcheck used by `npm run validate`.
|
|
121
|
-
- Tests/fixtures cover at least one positive and one negative case.
|
|
122
|
-
- Packaged recipes remain optional components, not policy workflows.
|
|
123
|
-
|
|
124
|
-
### M-22 Wake and Watcher Chaos Fixtures
|
|
125
|
-
|
|
126
|
-
- Priority: Medium.
|
|
127
|
-
- Status: Planned.
|
|
128
|
-
- Goal: Harden the invariant that durable files are canonical and wake notifications are advisory acceleration.
|
|
129
|
-
- Why now: Wake, watcher, line-counter, and JSONL resilience are central to operator trust as actor counts grow.
|
|
130
|
-
- Direction:
|
|
131
|
-
- Add deterministic fixtures for watcher restart, line-counter reset, duplicate terminal events, missing wake with present inbox record, wake before file catch-up, corrupt JSONL with later valid records, and killed run with stale progress phase.
|
|
132
|
-
- Preserve event-driven observability without reintroducing polling-first coordination examples.
|
|
133
|
-
- Keep tests fast and local.
|
|
134
|
-
- Acceptance:
|
|
135
|
-
- Duplicate follow-ups do not reappear.
|
|
136
|
-
- Missing wake does not lose durable messages.
|
|
137
|
-
- Corrupt records degrade inspect but do not kill it.
|
|
138
|
-
- Killed/stale progress states remain diagnosable.
|
|
56
|
+
The backlog is intentionally pruned to the 20% of work most likely to deliver 80% of value for `pi-actors` as a local actor kernel. Bias toward consolidation, smaller public surface area, and reliability over new feature breadth.
|
|
139
57
|
|
|
140
58
|
## Explicitly Deferred
|
|
141
59
|
|
|
@@ -145,7 +63,7 @@ These are valid ideas but not current focus. Reintroduce only with concrete evid
|
|
|
145
63
|
- Run restart/reattach policy: risky for isolation; defer until corruption recovery and protocol fixtures are stronger.
|
|
146
64
|
- Cross-session force kill or attach/adopt/reparent: useful later, but ownership policy should not change until observability makes current boundaries clear.
|
|
147
65
|
- Actor address helper CLI: keep diagnostics improving opportunistically inside existing parser/tests.
|
|
148
|
-
- Golden flow docs and flow conformance runner: useful after
|
|
66
|
+
- Golden flow docs and flow conformance runner: useful after the diagnostic and promotion surfaces are stable.
|
|
149
67
|
- Documentation refactor: defer until the canonical mailbox loop and worker recipe exist; avoid rewriting docs twice.
|
|
150
68
|
- Host-level tool unregistration: blocked on host API support.
|
|
151
69
|
- Branch-local checkpoint semantics: wait for real collaborative branch-runner experiments.
|
|
@@ -153,8 +71,4 @@ These are valid ideas but not current focus. Reintroduce only with concrete evid
|
|
|
153
71
|
|
|
154
72
|
## Suggested Milestone Order
|
|
155
73
|
|
|
156
|
-
|
|
157
|
-
Next milestone: M-18 Draft Recipe Promotion UX.
|
|
158
|
-
Then: M-19 Recipe Doctor Risk Labels v2.
|
|
159
|
-
Small cleanup lane: continue opportunistic domain polish only when a real ownership boundary appears.
|
|
160
|
-
```
|
|
74
|
+
No current milestone. Reassess runtime evidence before adding the next reliability slice.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,11 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.36.0: Recipe Diagnostics And Runtime Triage
|
|
6
|
+
|
|
7
|
+
- `[Recipe Doctor]` Added deterministic advisory risk labels for discovered recipes, including shell, eval, filesystem mutation, network, external side effect, long-running, platform-specific, and secret-touching signals in verbose recipe inspection plus compact doctor risk counts.
|
|
8
|
+
- `[Runtime]` Added `inspect target=tool:pi-actors view=triage` as a compact read-only operator attention surface covering runtime mode, active and other-session runs, invalid or blocking recipes, exposed tool recipes with non-lifecycle risk labels, drafts, stale claims, failed runs, attention messages, and next inspect actions.
|
|
9
|
+
- `[Recipes]` Added packaged recipe QA through `validate-recipe.mjs --qa` and `npm run recipes:qa`, with exact diagnostics for async mailbox contracts, termination vocabulary, artifact paths, platform scope, installed-package-safe helper paths, and missing helper scripts.
|
|
10
|
+
- `[Runtime]` Hardened wake/watch chaos behavior with deterministic coverage for watcher restarts, partial wake records before file catch-up, corrupt wake JSONL with later valid records, missing wake records with durable inbox work, and killed runs with stale progress state.
|
|
11
|
+
- `[Docs]` Documented recipe-doctor risk labels, runtime triage, packaged recipe QA, and import-first ready-recipe registration as operator review aids and source-of-truth preservation, not sandbox, repair, or security-boundary claims.
|
|
12
|
+
|
|
13
|
+
## 0.35.0: Draft Recipe Promotion UX
|
|
14
|
+
|
|
15
|
+
- `[Recipes]` Added draft recipe promotion UX: `inspect target=recipes view=summary verbose=true` now exposes draft timestamps, fingerprints, validation state, source run when known, and template previews, while `register_tool name=<tool> draft=<path>` promotes a validated draft into active recipe memory without deleting the draft and rejects collisions unless `update=true` is explicit.
|
|
16
|
+
|
|
5
17
|
## 0.34.1: Message Delivery Outcome Hotfix
|
|
6
18
|
|
|
7
19
|
- `[Dogfood]` Added a deterministic actor-worker stale-claim smoke covering an intentionally claimed branch inbox record; the worker now reports stale claim counts in both `worker-status.json` and its awaiting-assignment room event without adding auto-recovery or scheduler policy.
|
|
8
|
-
- `[Messages]`
|
|
9
|
-
- `[Backlog]` Closed
|
|
20
|
+
- `[Messages]` Normalized public message results with `delivered`, `persisted`, `queued`, `forwarded`, `consumer`, and `reason` fields across run, branch, room, coordinator, session, and tool destinations; room multicast now also exposes per-recipient branch delivery outcomes.
|
|
21
|
+
- `[Backlog]` Closed the worker stale-claim dogfood and message delivery outcome work after adding delivery-result coverage for run, branch, room, coordinator, session, tool, and ownership-denied paths.
|
|
10
22
|
|
|
11
23
|
## 0.34.0: Actor Kernel Domain Compression
|
|
12
24
|
|
package/README.md
CHANGED
|
@@ -132,6 +132,7 @@ docs_review scope="README.md" model="current-review-model" run_id=docs_review
|
|
|
132
132
|
Inspect only when there is a reason:
|
|
133
133
|
|
|
134
134
|
```text
|
|
135
|
+
inspect target=tool:pi-actors view=triage
|
|
135
136
|
inspect target=run:docs_review view=status
|
|
136
137
|
inspect target=run:docs_review view=tail lines=80
|
|
137
138
|
inspect target=run:docs_review view=messages
|
|
@@ -207,7 +208,8 @@ Rules:
|
|
|
207
208
|
- User recipes override same-name lower-priority recipes;
|
|
208
209
|
- Same-id JSON recipes shadow Markdown recipes in the same priority layer;
|
|
209
210
|
- Packaged recipes are standard-library components, not automatically installed operator policy;
|
|
210
|
-
-
|
|
211
|
+
- Draft recipes in `~/.pi/agent/recipes/drafts/` are replayable memory, not active tools;
|
|
212
|
+
- `register_tool` creates, updates, lists, deletes, or explicitly promotes draft recipe files through the normal agent interface.
|
|
211
213
|
|
|
212
214
|
Example foreground tool:
|
|
213
215
|
|
|
@@ -226,6 +228,14 @@ register_tool name=docs_review \
|
|
|
226
228
|
args="scope:path,model:string"
|
|
227
229
|
```
|
|
228
230
|
|
|
231
|
+
Promote a successful captured draft only after an explicit operator decision:
|
|
232
|
+
|
|
233
|
+
```text
|
|
234
|
+
register_tool name=docs_review draft=~/.pi/agent/recipes/drafts/spawned-run.json
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Promotion validates the draft, writes `~/.pi/agent/recipes/<name>.json`, preserves the draft, and rejects collisions unless `update=true` is supplied.
|
|
238
|
+
|
|
229
239
|
Inspect the discovered registry:
|
|
230
240
|
|
|
231
241
|
```text
|
|
@@ -298,7 +308,7 @@ Packaged recipes should prefer mailbox/wake behavior for portable control. Recip
|
|
|
298
308
|
|
|
299
309
|
Commands execute directly without shell evaluation where possible, but trusted executables still run with the same system permissions as Pi. Only register commands, scripts, recipes, and paths you trust.
|
|
300
310
|
|
|
301
|
-
High-risk templates such as shells, interpreter eval modes, and broad filesystem mutation may surface warnings, but the runtime is not a security boundary.
|
|
311
|
+
High-risk templates such as shells, interpreter eval modes, and broad filesystem mutation may surface warnings, but the runtime is not a security boundary. Recipe doctor also exposes advisory labels like `risk.shell`, `risk.eval`, `risk.destructive_fs`, `risk.network`, and `risk.external_side_effect` for operator review.
|
|
302
312
|
|
|
303
313
|
Prefer:
|
|
304
314
|
|
|
@@ -50,6 +50,7 @@ export interface CommandTemplateExecResult {
|
|
|
50
50
|
code: number;
|
|
51
51
|
killed: boolean;
|
|
52
52
|
}
|
|
53
|
+
export type CommandTemplateRiskLabel = "risk.shell" | "risk.eval" | "risk.broad_fs_write" | "risk.destructive_fs" | "risk.network" | "risk.external_side_effect" | "risk.long_running" | "risk.platform_specific" | "risk.secret_touching";
|
|
53
54
|
export type CommandTemplateExecCommand = (command: string, args: string[], options?: CommandTemplateExecOptions) => Promise<CommandTemplateExecResult>;
|
|
54
55
|
export declare function normalizeCommandTemplateConfig(config: CommandTemplateConfig): CommandTemplateObjectConfig;
|
|
55
56
|
export declare function resolveInheritedDefaultReferences(ownDefaults: Record<string, unknown> | undefined, inheritedDefaults: Record<string, unknown> | undefined, runtimeValues?: Record<string, unknown>): Record<string, unknown> | undefined;
|
|
@@ -58,6 +59,7 @@ export declare function isCommandTemplateRepeatPlaceholder(name: string): boolea
|
|
|
58
59
|
export declare function getCommandTemplateRepeatDefaults(index: number, repeat: number): Record<string, string>;
|
|
59
60
|
export declare function expandCommandTemplateConfigs(config: CommandTemplateConfig, inherited?: Pick<CommandTemplateObjectConfig, "args" | "defaults">): CommandTemplateLeafConfig[];
|
|
60
61
|
export declare function getCommandTemplateWarnings(config: CommandTemplateConfig): string[];
|
|
62
|
+
export declare function getCommandTemplateRiskLabels(config: CommandTemplateConfig): CommandTemplateRiskLabel[];
|
|
61
63
|
export declare function getCommandTemplateDefaults(config: CommandTemplateConfig | undefined): Record<string, string>;
|
|
62
64
|
export declare function splitCommandTemplate(input: string): string[];
|
|
63
65
|
export declare function expandCommandTemplateExecutable(command: string, cwd: string): string;
|
|
@@ -6,6 +6,17 @@
|
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { isAbsolute, resolve } from "node:path";
|
|
9
|
+
const COMMAND_TEMPLATE_RISK_LABEL_ORDER = [
|
|
10
|
+
"risk.shell",
|
|
11
|
+
"risk.eval",
|
|
12
|
+
"risk.destructive_fs",
|
|
13
|
+
"risk.broad_fs_write",
|
|
14
|
+
"risk.external_side_effect",
|
|
15
|
+
"risk.secret_touching",
|
|
16
|
+
"risk.network",
|
|
17
|
+
"risk.long_running",
|
|
18
|
+
"risk.platform_specific",
|
|
19
|
+
];
|
|
9
20
|
function normalizeCommandTemplateArgs(value) {
|
|
10
21
|
if (!Array.isArray(value))
|
|
11
22
|
return [];
|
|
@@ -93,6 +104,68 @@ function hasRiskyPathArg(args) {
|
|
|
93
104
|
arg.startsWith("~/") ||
|
|
94
105
|
arg.startsWith("/"));
|
|
95
106
|
}
|
|
107
|
+
function sortRiskLabels(labels) {
|
|
108
|
+
const unique = new Set(labels);
|
|
109
|
+
return COMMAND_TEMPLATE_RISK_LABEL_ORDER.filter((label) => unique.has(label));
|
|
110
|
+
}
|
|
111
|
+
function hasAnyArg(args, values) {
|
|
112
|
+
return args.some((arg) => values.includes(arg.toLowerCase()));
|
|
113
|
+
}
|
|
114
|
+
function hasSecretTouchingText(parts) {
|
|
115
|
+
return parts.some((part) => /(^|[{}._\-\s/])(?:secret|token|password|passwd|credential|api[_-]?key|private[_-]?key|\.env|ssh[_-]?key)(?:[{}._\-\s/]|$)/i.test(part));
|
|
116
|
+
}
|
|
117
|
+
function getLeafCommandTemplateRiskLabels(config) {
|
|
118
|
+
const parts = splitCommandTemplate(config.template);
|
|
119
|
+
const command = getExecutableName(parts[0]);
|
|
120
|
+
const args = parts.slice(1);
|
|
121
|
+
const labels = new Set();
|
|
122
|
+
if (["bash", "sh", "zsh", "fish"].includes(command)) {
|
|
123
|
+
labels.add("risk.shell");
|
|
124
|
+
if (hasAnyFlag(args, ["-c"]))
|
|
125
|
+
labels.add("risk.eval");
|
|
126
|
+
}
|
|
127
|
+
if (["node", "deno", "bun"].includes(command) &&
|
|
128
|
+
hasAnyFlag(args, ["-e", "--eval"])) {
|
|
129
|
+
labels.add("risk.eval");
|
|
130
|
+
}
|
|
131
|
+
if (["python", "python3", "perl", "ruby"].includes(command) &&
|
|
132
|
+
hasAnyFlag(args, ["-c", "-e"])) {
|
|
133
|
+
labels.add("risk.eval");
|
|
134
|
+
}
|
|
135
|
+
if (command === "rm" &&
|
|
136
|
+
(args.some((arg) => /^-[^-]*r/.test(arg) || /^-[^-]*f/.test(arg)) ||
|
|
137
|
+
hasRiskyPathArg(args))) {
|
|
138
|
+
labels.add("risk.destructive_fs");
|
|
139
|
+
}
|
|
140
|
+
if (["mv", "cp", "rsync"].includes(command) && hasRiskyPathArg(args)) {
|
|
141
|
+
labels.add("risk.broad_fs_write");
|
|
142
|
+
}
|
|
143
|
+
if (["curl", "wget", "ssh", "scp", "sftp", "rsync", "nc", "ncat", "telnet", "ftp"].includes(command) ||
|
|
144
|
+
(command === "git" &&
|
|
145
|
+
hasAnyArg(args, ["clone", "fetch", "pull", "push", "ls-remote"])) ||
|
|
146
|
+
["npm", "pnpm", "yarn", "pip", "cargo"].includes(command)) {
|
|
147
|
+
labels.add("risk.network");
|
|
148
|
+
}
|
|
149
|
+
if (["gh", "glab", "hub", "kubectl", "terraform"].includes(command) ||
|
|
150
|
+
(command === "git" && hasAnyArg(args, ["push"])) ||
|
|
151
|
+
(["npm", "pnpm", "yarn"].includes(command) &&
|
|
152
|
+
hasAnyArg(args, ["publish", "login", "logout", "deprecate"]))) {
|
|
153
|
+
labels.add("risk.external_side_effect");
|
|
154
|
+
}
|
|
155
|
+
if (command === "sleep" ||
|
|
156
|
+
command === "watch" ||
|
|
157
|
+
(command === "tail" && hasAnyFlag(args, ["-f"])) ||
|
|
158
|
+
hasAnyArg(args, ["--watch", "--serve", "serve"])) {
|
|
159
|
+
labels.add("risk.long_running");
|
|
160
|
+
}
|
|
161
|
+
if (["systemctl", "launchctl", "osascript", "open", "xdg-open", "powershell", "pwsh", "cmd.exe", "apt", "apt-get", "dnf", "yum", "brew", "pacman", "apk", "xclip", "wl-copy"].includes(command)) {
|
|
162
|
+
labels.add("risk.platform_specific");
|
|
163
|
+
}
|
|
164
|
+
if (["pass", "gpg", "ssh-add"].includes(command) || hasSecretTouchingText(parts)) {
|
|
165
|
+
labels.add("risk.secret_touching");
|
|
166
|
+
}
|
|
167
|
+
return sortRiskLabels(labels);
|
|
168
|
+
}
|
|
96
169
|
function getLeafCommandTemplateWarnings(config) {
|
|
97
170
|
const parts = splitCommandTemplate(config.template);
|
|
98
171
|
const command = getExecutableName(parts[0]);
|
|
@@ -206,6 +279,9 @@ export function getCommandTemplateWarnings(config) {
|
|
|
206
279
|
...new Set(expandCommandTemplateConfigs(config).flatMap((leaf) => getLeafCommandTemplateWarnings(leaf))),
|
|
207
280
|
];
|
|
208
281
|
}
|
|
282
|
+
export function getCommandTemplateRiskLabels(config) {
|
|
283
|
+
return sortRiskLabels(expandCommandTemplateConfigs(config).flatMap((leaf) => getLeafCommandTemplateRiskLabels(leaf)));
|
|
284
|
+
}
|
|
209
285
|
function parseCommandTemplateArgToken(value) {
|
|
210
286
|
const separatorIndex = value.indexOf("=");
|
|
211
287
|
const rawName = separatorIndex === -1 ? value : value.slice(0, separatorIndex);
|
package/dist/lib/prompts.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export declare const ONBOARDING_SYSTEM_PROMPT = "pi-actors quick model:\n- Local
|
|
|
10
10
|
export declare const REGISTER_TOOL_PARAM_DESCRIPTIONS: {
|
|
11
11
|
readonly name: "Tool name in snake_case (e.g., 'transcribe')";
|
|
12
12
|
readonly description: "Describe what the tool does for the LLM. Required unless deleting; omitted updates keep the old description.";
|
|
13
|
+
readonly draft: "Promote a draft recipe path from ~/.pi/agent/recipes/drafts into an active named recipe under ~/.pi/agent/recipes. Requires name; use update=true to overwrite.";
|
|
13
14
|
readonly async: "Set true for a co-located async template recipe. Omit for ordinary command templates or file-backed recipe references.";
|
|
14
15
|
readonly state_dir: "Optional async run state directory for a co-located template recipe.";
|
|
15
16
|
readonly template: "Command template with {arg} or {arg=default} placeholders, or a template recipe JSON path/name. With async, this is the co-located recipe body. Bare recipe names resolve under ~/.pi/agent/recipes. Omitted updates keep the old template. Empty string deletes the tool.";
|
package/dist/lib/prompts.js
CHANGED
|
@@ -30,6 +30,7 @@ export const ONBOARDING_SYSTEM_PROMPT = `pi-actors quick model:
|
|
|
30
30
|
export const REGISTER_TOOL_PARAM_DESCRIPTIONS = {
|
|
31
31
|
name: "Tool name in snake_case (e.g., 'transcribe')",
|
|
32
32
|
description: "Describe what the tool does for the LLM. Required unless deleting; omitted updates keep the old description.",
|
|
33
|
+
draft: "Promote a draft recipe path from ~/.pi/agent/recipes/drafts into an active named recipe under ~/.pi/agent/recipes. Requires name; use update=true to overwrite.",
|
|
33
34
|
async: "Set true for a co-located async template recipe. Omit for ordinary command templates or file-backed recipe references.",
|
|
34
35
|
state_dir: "Optional async run state directory for a co-located template recipe.",
|
|
35
36
|
template: "Command template with {arg} or {arg=default} placeholders, or a template recipe JSON path/name. With async, this is the co-located recipe body. Bare recipe names resolve under ~/.pi/agent/recipes. Omitted updates keep the old template. Empty string deletes the tool.",
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Zones: recipe discovery, tool exposure, registry diagnostics
|
|
4
4
|
* Owns filename identity discovery across prioritized recipe roots
|
|
5
5
|
*/
|
|
6
|
+
import * as CommandTemplates from "./command-templates.ts";
|
|
6
7
|
import type { RegisteredTool } from "./config.ts";
|
|
7
8
|
import type { TemplateRecipeConfig } from "./recipes-references.ts";
|
|
8
9
|
export interface DiscoveredRecipe {
|
|
@@ -18,6 +19,7 @@ export interface DiscoveredRecipe {
|
|
|
18
19
|
tool: boolean;
|
|
19
20
|
mutableUsage: boolean;
|
|
20
21
|
diagnostics: string[];
|
|
22
|
+
riskLabels: CommandTemplates.CommandTemplateRiskLabel[];
|
|
21
23
|
shadows: string[];
|
|
22
24
|
}
|
|
23
25
|
export interface RecipeIntegrityManifestEntry {
|
|
@@ -45,15 +45,35 @@ function listRecipeFiles(root) {
|
|
|
45
45
|
.localeCompare(b.replace(/\.md$/, ".json")) ||
|
|
46
46
|
(a.endsWith(".json") ? -1 : 1));
|
|
47
47
|
}
|
|
48
|
+
function getRecipeCommandTemplateConfig(config) {
|
|
49
|
+
return (typeof config.template === "object" && config.template !== null
|
|
50
|
+
? config.template
|
|
51
|
+
: config);
|
|
52
|
+
}
|
|
48
53
|
function getRecipeConfigDiagnostics(file, config) {
|
|
49
54
|
if (!config) {
|
|
50
55
|
const reason = RecipesReferences.diagnoseRawRecipeConfigFailure(file);
|
|
51
56
|
return [`Invalid recipe: ${file}${reason ? `: ${reason}` : ""}`];
|
|
52
57
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
return CommandTemplates.getCommandTemplateWarnings(getRecipeCommandTemplateConfig(config)).map((warning) => `Recipe ${file}: ${warning}`);
|
|
59
|
+
}
|
|
60
|
+
function getRecipeRiskLabels(config) {
|
|
61
|
+
if (!config)
|
|
62
|
+
return [];
|
|
63
|
+
const labels = new Set(CommandTemplates.getCommandTemplateRiskLabels(getRecipeCommandTemplateConfig(config)));
|
|
64
|
+
if (config.async === true)
|
|
65
|
+
labels.add("risk.long_running");
|
|
66
|
+
return [
|
|
67
|
+
"risk.shell",
|
|
68
|
+
"risk.eval",
|
|
69
|
+
"risk.destructive_fs",
|
|
70
|
+
"risk.broad_fs_write",
|
|
71
|
+
"risk.external_side_effect",
|
|
72
|
+
"risk.secret_touching",
|
|
73
|
+
"risk.network",
|
|
74
|
+
"risk.long_running",
|
|
75
|
+
"risk.platform_specific",
|
|
76
|
+
].filter((label) => labels.has(label));
|
|
57
77
|
}
|
|
58
78
|
function readDiscoveredRecipe(root, file, priority, defaultTool = false, mutableUsage = false) {
|
|
59
79
|
const id = RecipesReferences.getRecipeIdFromPath(file);
|
|
@@ -74,6 +94,7 @@ function readDiscoveredRecipe(root, file, priority, defaultTool = false, mutable
|
|
|
74
94
|
tool: defaultTool && !disabled && !invalid,
|
|
75
95
|
mutableUsage,
|
|
76
96
|
diagnostics: getRecipeConfigDiagnostics(file, config),
|
|
97
|
+
riskLabels: getRecipeRiskLabels(config),
|
|
77
98
|
shadows: [],
|
|
78
99
|
};
|
|
79
100
|
}
|
|
@@ -92,6 +113,7 @@ function readDiscoveredRecipe(root, file, priority, defaultTool = false, mutable
|
|
|
92
113
|
diagnostics: [
|
|
93
114
|
`Failed to load recipe ${file}: ${error instanceof Error ? error.message : String(error)}`,
|
|
94
115
|
],
|
|
116
|
+
riskLabels: [],
|
|
95
117
|
shadows: [],
|
|
96
118
|
};
|
|
97
119
|
}
|
|
@@ -266,7 +288,7 @@ function diagnosticSeverity(message) {
|
|
|
266
288
|
if (/invalid|failed to load|not found|cyclic|exceeds|must define|repeat must/i.test(message)) {
|
|
267
289
|
return "error";
|
|
268
290
|
}
|
|
269
|
-
if (/world-writable|group-writable|invokes bash|eval|destructive|unsafe/i.test(message)) {
|
|
291
|
+
if (/world-writable|group-writable|invokes bash|eval|destructive|broad filesystem|unsafe/i.test(message)) {
|
|
270
292
|
return "warning";
|
|
271
293
|
}
|
|
272
294
|
return "info";
|
|
@@ -304,6 +326,7 @@ function diagnosticDetails(result) {
|
|
|
304
326
|
seen.add(key);
|
|
305
327
|
details.push({
|
|
306
328
|
...(entry ? { id: entry.id, path: entry.path } : {}),
|
|
329
|
+
...(entry?.riskLabels.length ? { risk_labels: entry.riskLabels } : {}),
|
|
307
330
|
action: diagnosticSuggestedAction(message),
|
|
308
331
|
message,
|
|
309
332
|
severity: diagnosticSeverity(message),
|
|
@@ -357,6 +380,7 @@ function remediationForEntry(entry, activePath) {
|
|
|
357
380
|
kind: "risky_shell_boundary",
|
|
358
381
|
severity: "warning",
|
|
359
382
|
path: entry.path,
|
|
383
|
+
...(entry.riskLabels.length ? { risk_labels: entry.riskLabels } : {}),
|
|
360
384
|
reason: riskyDiagnostics[0],
|
|
361
385
|
action: "audit trusted command boundary; keep only if the recipe is local and intentional",
|
|
362
386
|
};
|
|
@@ -400,6 +424,25 @@ function discoveryRemediations(result) {
|
|
|
400
424
|
String(a.id).localeCompare(String(b.id)) ||
|
|
401
425
|
String(a.path).localeCompare(String(b.path)));
|
|
402
426
|
}
|
|
427
|
+
function summarizeRiskLabels(entries) {
|
|
428
|
+
const counts = new Map();
|
|
429
|
+
for (const entry of entries) {
|
|
430
|
+
for (const label of entry.riskLabels) {
|
|
431
|
+
const current = counts.get(label) ?? { count: 0, ids: new Set() };
|
|
432
|
+
current.count += 1;
|
|
433
|
+
current.ids.add(entry.id);
|
|
434
|
+
counts.set(label, current);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return [...counts.entries()]
|
|
438
|
+
.map(([label, value]) => ({
|
|
439
|
+
label,
|
|
440
|
+
count: value.count,
|
|
441
|
+
recipes: [...value.ids].sort(),
|
|
442
|
+
}))
|
|
443
|
+
.sort((a, b) => Number(b.count) - Number(a.count) ||
|
|
444
|
+
String(a.label).localeCompare(String(b.label)));
|
|
445
|
+
}
|
|
403
446
|
function recommendationForEntry(entry, activePath) {
|
|
404
447
|
const recommendation = cleanupRecommendation(entry);
|
|
405
448
|
if (!recommendation)
|
|
@@ -425,14 +468,41 @@ export function getShadowedLaunchDiagnostic(result, id) {
|
|
|
425
468
|
reason: active.invalid ? "shadowed_invalid" : "shadowed_disabled",
|
|
426
469
|
};
|
|
427
470
|
}
|
|
471
|
+
function templatePreview(value) {
|
|
472
|
+
const rendered = typeof value === "string"
|
|
473
|
+
? value
|
|
474
|
+
: value === undefined
|
|
475
|
+
? undefined
|
|
476
|
+
: JSON.stringify(value);
|
|
477
|
+
return rendered && rendered.length > 120
|
|
478
|
+
? `${rendered.slice(0, 117)}...`
|
|
479
|
+
: rendered;
|
|
480
|
+
}
|
|
428
481
|
export function listDraftRecipes(root) {
|
|
429
482
|
return listRecipeFiles(root).map((path) => {
|
|
430
483
|
const id = RecipesReferences.getRecipeIdFromPath(path);
|
|
484
|
+
const bytes = readFileSync(path);
|
|
485
|
+
const stat = statSync(path);
|
|
431
486
|
const config = RecipesReferences.readRawRecipeConfig(path);
|
|
487
|
+
const resolved = RecipesReferences.readResolvedRecipeConfig(path);
|
|
488
|
+
const diagnostics = getRecipeConfigDiagnostics(path, resolved);
|
|
489
|
+
const sourceRun = String(config?.description ?? "").match(/spawn run ([^\s]+)/)?.[1];
|
|
490
|
+
const preview = templatePreview(config?.template);
|
|
491
|
+
const riskLabels = getRecipeRiskLabels(resolved);
|
|
432
492
|
return {
|
|
433
493
|
id,
|
|
434
494
|
path,
|
|
495
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
496
|
+
size: bytes.byteLength,
|
|
497
|
+
created_at: stat.birthtime.toISOString(),
|
|
498
|
+
modified_at: stat.mtime.toISOString(),
|
|
499
|
+
valid: Boolean(resolved),
|
|
500
|
+
diagnostics,
|
|
501
|
+
...(riskLabels.length ? { risk_labels: riskLabels } : {}),
|
|
435
502
|
...(config?.description ? { description: config.description } : {}),
|
|
503
|
+
...(sourceRun ? { source_run: sourceRun } : {}),
|
|
504
|
+
...(config?.async !== undefined ? { async: config.async } : {}),
|
|
505
|
+
...(preview ? { template_preview: preview } : {}),
|
|
436
506
|
};
|
|
437
507
|
});
|
|
438
508
|
}
|
|
@@ -453,6 +523,7 @@ export function summarizeDiscovery(result) {
|
|
|
453
523
|
disabled: entry.disabled,
|
|
454
524
|
invalid: entry.invalid,
|
|
455
525
|
shadows: entry.shadows,
|
|
526
|
+
...(entry.riskLabels.length ? { risk_labels: entry.riskLabels } : {}),
|
|
456
527
|
...(entry.config?.imports ? { imports: entry.config.imports } : {}),
|
|
457
528
|
...(recipeUsage(entry.config)
|
|
458
529
|
? { usage: recipeUsage(entry.config) }
|
|
@@ -479,6 +550,7 @@ export function summarizeDiscovery(result) {
|
|
|
479
550
|
.filter((entry) => entry.disabled)
|
|
480
551
|
.map((entry) => ({ id: entry.id, path: entry.path }))
|
|
481
552
|
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
553
|
+
risk_summary: summarizeRiskLabels(result.entries),
|
|
482
554
|
recommendations,
|
|
483
555
|
remediations,
|
|
484
556
|
top_action: remediations[0],
|
package/dist/lib/registry.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface RegisterToolInput {
|
|
|
11
11
|
async?: boolean;
|
|
12
12
|
state_dir?: string;
|
|
13
13
|
template?: CommandTemplates.CommandTemplateValue | null;
|
|
14
|
+
draft?: string;
|
|
14
15
|
args?: string;
|
|
15
16
|
update?: boolean;
|
|
16
17
|
values?: Record<string, unknown>;
|
|
@@ -20,6 +21,8 @@ export interface RegisterToolResultDetails {
|
|
|
20
21
|
async?: boolean;
|
|
21
22
|
config?: string;
|
|
22
23
|
defaults?: Record<string, string>;
|
|
24
|
+
draft?: string;
|
|
25
|
+
promoted?: boolean;
|
|
23
26
|
recipeName?: string;
|
|
24
27
|
state_dir?: string;
|
|
25
28
|
template?: CommandTemplates.CommandTemplateValue;
|
package/dist/lib/registry.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Owns register/update/delete validation, persistence, runtime side effects, and result payloads
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync, mkdirSync, unlinkSync } from "node:fs";
|
|
7
|
-
import { dirname, join } from "node:path";
|
|
7
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
8
8
|
import * as CommandTemplates from "./command-templates.js";
|
|
9
9
|
import * as ExecutionOutput from "./execution-output.js";
|
|
10
10
|
import { writeJsonAtomic } from "./file-state.js";
|
|
@@ -32,6 +32,62 @@ function getRecipeRoot(deps) {
|
|
|
32
32
|
function getToolRecipePath(deps, name) {
|
|
33
33
|
return join(getRecipeRoot(deps), `${name}.json`);
|
|
34
34
|
}
|
|
35
|
+
function assertDraftPath(deps, draft) {
|
|
36
|
+
const draftRoot = resolve(getRecipeRoot(deps), "drafts");
|
|
37
|
+
const path = resolve(draft);
|
|
38
|
+
const relation = relative(draftRoot, path);
|
|
39
|
+
if (relation === "" ||
|
|
40
|
+
relation.startsWith("..") ||
|
|
41
|
+
resolve(relation) === relation) {
|
|
42
|
+
throw new Error(ExecutionOutput.formatToolText(`Draft must be under ${draftRoot}. Use inspect target=recipes view=summary to list drafts.`));
|
|
43
|
+
}
|
|
44
|
+
if (!existsSync(path)) {
|
|
45
|
+
throw new Error(ExecutionOutput.formatToolText(`Draft not found: ${path}`));
|
|
46
|
+
}
|
|
47
|
+
return path;
|
|
48
|
+
}
|
|
49
|
+
function promoteDraftRecipe(name, input, ctx, deps) {
|
|
50
|
+
const draftPath = assertDraftPath(deps, String(input.draft ?? ""));
|
|
51
|
+
const targetPath = getToolRecipePath(deps, name);
|
|
52
|
+
const tools = deps.getTools();
|
|
53
|
+
const existing = tools.get(name);
|
|
54
|
+
const conflict = deps.getExternalToolConflict(name);
|
|
55
|
+
if (conflict)
|
|
56
|
+
throw new Error(ExecutionOutput.formatToolText(conflict));
|
|
57
|
+
if ((existing || existsSync(targetPath)) && !input.update) {
|
|
58
|
+
throw new Error(ExecutionOutput.formatToolText(`Tool "${name}" already registered. Use update=true to overwrite.`));
|
|
59
|
+
}
|
|
60
|
+
const config = RecipesReferences.readResolvedRecipeConfig(draftPath);
|
|
61
|
+
if (!config) {
|
|
62
|
+
const reason = RecipesReferences.diagnoseRawRecipeConfigFailure(draftPath);
|
|
63
|
+
throw new Error(ExecutionOutput.formatToolText(`Draft recipe is invalid${reason ? `: ${reason}` : "."}`));
|
|
64
|
+
}
|
|
65
|
+
const promoted = buildConfig(name, {
|
|
66
|
+
...input,
|
|
67
|
+
description: input.description ?? config.description,
|
|
68
|
+
template: draftPath,
|
|
69
|
+
}, existing);
|
|
70
|
+
const raw = RecipesReferences.readRawRecipeConfig(draftPath);
|
|
71
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
72
|
+
writeJsonAtomic(targetPath, raw);
|
|
73
|
+
promoted.template = targetPath;
|
|
74
|
+
promoted.sourcePath = targetPath;
|
|
75
|
+
tools.set(name, promoted);
|
|
76
|
+
deps.registerRuntimeTool(promoted);
|
|
77
|
+
deps.notify(ctx, `Promoted draft recipe: ${name}`, "info");
|
|
78
|
+
return {
|
|
79
|
+
content: [
|
|
80
|
+
textContent(ExecutionOutput.formatToolText(`${existing ? "Updated" : "Registered"} tool "${name}" from draft recipe.`)),
|
|
81
|
+
],
|
|
82
|
+
details: {
|
|
83
|
+
args: promoted.args,
|
|
84
|
+
config: targetPath,
|
|
85
|
+
draft: draftPath,
|
|
86
|
+
promoted: true,
|
|
87
|
+
tool: name,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
35
91
|
function persistToolRecipe(deps, cfg) {
|
|
36
92
|
const path = getToolRecipePath(deps, cfg.name);
|
|
37
93
|
mkdirSync(dirname(path), { recursive: true });
|
|
@@ -173,6 +229,9 @@ export async function executeRegisterTool(params, ctx, deps) {
|
|
|
173
229
|
if (deps.reservedToolNames.has(name)) {
|
|
174
230
|
throw new Error(ExecutionOutput.formatToolText(`Reserved tool name: ${name}`));
|
|
175
231
|
}
|
|
232
|
+
if (typeof input.draft === "string" && input.draft.trim()) {
|
|
233
|
+
return promoteDraftRecipe(name, input, ctx, deps);
|
|
234
|
+
}
|
|
176
235
|
const templateProvided = Object.hasOwn(input, "template");
|
|
177
236
|
const template = getInputTemplate(input.template);
|
|
178
237
|
if (templateProvided && (template === null || template === ""))
|
|
@@ -74,6 +74,7 @@ export function createFileRuntimeNotifier(stateDir, options = {}) {
|
|
|
74
74
|
subscribe: (actor, onWake, subscribeOptions = {}) => {
|
|
75
75
|
mkdirSync(dirname(file), { recursive: true });
|
|
76
76
|
let position = options.replay || !existsSync(file) ? 0 : statSync(file).size;
|
|
77
|
+
let pending = "";
|
|
77
78
|
let closed = false;
|
|
78
79
|
const reconcile = (reason) => {
|
|
79
80
|
if (closed)
|
|
@@ -89,11 +90,15 @@ export function createFileRuntimeNotifier(stateDir, options = {}) {
|
|
|
89
90
|
if (closed || !existsSync(file))
|
|
90
91
|
return;
|
|
91
92
|
const buffer = readFileSync(file);
|
|
92
|
-
if (position > buffer.length)
|
|
93
|
+
if (position > buffer.length) {
|
|
93
94
|
position = 0;
|
|
94
|
-
|
|
95
|
+
pending = "";
|
|
96
|
+
}
|
|
97
|
+
const chunk = pending + buffer.subarray(position).toString("utf8");
|
|
95
98
|
position = buffer.length;
|
|
96
|
-
|
|
99
|
+
const lines = chunk.split("\n");
|
|
100
|
+
pending = chunk.endsWith("\n") ? "" : (lines.pop() ?? "");
|
|
101
|
+
for (const line of lines) {
|
|
97
102
|
if (!line.trim())
|
|
98
103
|
continue;
|
|
99
104
|
const event = parseRuntimeWakeEventLine(line);
|