@cr1ms0n/pi-subagent 0.8.5 → 0.8.7
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 +14 -0
- package/README.md +13 -5
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/SECURITY.md +8 -5
- package/package.json +1 -1
- package/skills/subagent/SKILL.md +11 -5
- package/src/extension.ts +12 -5
- package/src/policy.ts +35 -12
- package/src/schema.ts +28 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.8.7 — 2026-09-14
|
|
6
|
+
|
|
7
|
+
### Provider-safe tool schemas
|
|
8
|
+
|
|
9
|
+
- Strip internal TypeBox `~...` metadata from the `subagent` and `subagent_wait` schemas before Pi provider adapters receive them, while retaining the original schemas for local runtime validation.
|
|
10
|
+
|
|
11
|
+
## 0.8.6 — 2026-09-14
|
|
12
|
+
|
|
13
|
+
### Pi context management in child profiles
|
|
14
|
+
|
|
15
|
+
- Keep Pi context-management tools available to child tool allowlists without granting project-file write access, so configured gateway models such as `uwoacrimson/gpt-5.6-luna` and `uwoacrimson/gpt-5.6-sol` can use `contextManagement` from `explore` and `review` profiles.
|
|
16
|
+
|
|
3
17
|
## 0.8.5 — 2026-09-10
|
|
4
18
|
|
|
5
19
|
### Read-only FFF search tools
|
package/README.md
CHANGED
|
@@ -191,11 +191,19 @@ Set a persona's backend in agent frontmatter with `backend: codex`.
|
|
|
191
191
|
|
|
192
192
|
## Profiles
|
|
193
193
|
|
|
194
|
-
| Profile | Tools
|
|
195
|
-
| ---------------------------- |
|
|
196
|
-
| `explore` (parallel default) | read/grep/find/ls + safe extras | no
|
|
197
|
-
| `review` | same as explore
|
|
198
|
-
| `general` | inherited active tools
|
|
194
|
+
| Profile | Tools | Writes |
|
|
195
|
+
| ---------------------------- | ------------------------------------------------------- | ------------------------------------------- |
|
|
196
|
+
| `explore` (parallel default) | read/grep/find/ls + safe extras + Pi context tools | no project-file writes |
|
|
197
|
+
| `review` | same as explore | no project-file writes |
|
|
198
|
+
| `general` | inherited active tools + Pi context tools | yes if tools include bash/edit/write |
|
|
199
|
+
|
|
200
|
+
For the Pi backend, the context-management tools `new_context`,
|
|
201
|
+
`get_context_remaining`, `history`, and `notes` are retained in child tool
|
|
202
|
+
allowlists when the parent exposes them. They are control-plane tools: they may
|
|
203
|
+
update continuity notes or the remote context window, but cannot modify the
|
|
204
|
+
child checkout or run a shell command. This exception also applies when a task
|
|
205
|
+
supplies a narrower tool list, so Pi's `contextManagement` remains usable for
|
|
206
|
+
configured gateway models.
|
|
199
207
|
|
|
200
208
|
Parallel write-capable tasks sharing one checkout are rejected unless each uses
|
|
201
209
|
`isolation: "worktree"`, distinct `cwd`, or explicit `allow_shared_writes: true`.
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
- `persistence.ts`: versioned active-branch event folding and bounded child transcript metadata.
|
|
30
30
|
- `maintenance.ts`: filesystem GC (session files) and abort-race helpers; kept out of persistence.
|
|
31
31
|
- `usage.ts`: provider-reported root/subagent/combined accounting.
|
|
32
|
-
- `policy.ts` / `schema.ts`: discriminated request validation and safe capability profiles.
|
|
32
|
+
- `policy.ts` / `schema.ts`: discriminated request validation and safe capability profiles. `schema.ts` retains the canonical TypeBox validators and derives provider-safe tool-schema projections; `extension.ts` registers those projections while validating calls with the originals. Pi context-management control-plane tools remain available to child allowlists without granting project-file write access.
|
|
33
33
|
- `config.ts`: defaults ← `~/.pi/subagent.json` ← `PI_SUBAGENT_*` env overrides.
|
|
34
34
|
- `structured.ts`: structured-output contract (dependency-free JSON-Schema subset
|
|
35
35
|
validation, fenced json:result extraction, contract/repair prompts) and
|
package/docs/SECURITY.md
CHANGED
|
@@ -8,16 +8,19 @@ and can use tools according to their capability profile.
|
|
|
8
8
|
|
|
9
9
|
| Profile | Default tools | Writes? |
|
|
10
10
|
|---------|---------------|---------|
|
|
11
|
-
| `explore` | `read`, `grep`, `find`, `ls` (+
|
|
12
|
-
| `review` | same as explore | No |
|
|
13
|
-
| `general` | inherited active tools (
|
|
11
|
+
| `explore` | `read`, `grep`, `find`, `ls` (+ safe extras and Pi context tools) | No project-file writes |
|
|
12
|
+
| `review` | same as explore | No project-file writes |
|
|
13
|
+
| `general` | inherited active tools (+ Pi context tools) | Yes if `bash`/`edit`/`write` are active |
|
|
14
14
|
|
|
15
15
|
Parallel mode defaults to `explore` to avoid concurrent shared writes.
|
|
16
16
|
|
|
17
17
|
## Hard rules
|
|
18
18
|
|
|
19
|
-
1. **Read-only means no
|
|
20
|
-
an explore/review profile.
|
|
19
|
+
1. **Read-only means no project-file mutation.** `bash` can rewrite the disk and is never part of
|
|
20
|
+
an explore/review profile. Pi context-management tools (`new_context`,
|
|
21
|
+
`get_context_remaining`, `history`, `notes`) are an explicit control-plane
|
|
22
|
+
exception: they may update continuity notes/window state but cannot access
|
|
23
|
+
the project write tools.
|
|
21
24
|
2. **Parallel writers** require `isolation: "worktree"`, distinct `cwd` values,
|
|
22
25
|
or an explicit `allow_shared_writes: true` opt-in.
|
|
23
26
|
3. **Depth is capped** (`maxDepth`, default 2). Nested children at the ceiling do
|
package/package.json
CHANGED
package/skills/subagent/SKILL.md
CHANGED
|
@@ -57,11 +57,17 @@ const routeModel = "<exact model from current modelPolicy route>";
|
|
|
57
57
|
|
|
58
58
|
## Profiles
|
|
59
59
|
|
|
60
|
-
| Profile | Tools
|
|
61
|
-
| --------- |
|
|
62
|
-
| `explore` | read/search/ls (+safe) | no
|
|
63
|
-
| `review` | same as explore
|
|
64
|
-
| `general` | inherited active tools | yes if tools include bash/edit/write |
|
|
60
|
+
| Profile | Tools | Writes |
|
|
61
|
+
| --------- | --------------------------------------------------------- | ------------------------------------------- |
|
|
62
|
+
| `explore` | read/search/ls (+safe) + Pi context tools | no project-file writes |
|
|
63
|
+
| `review` | same as explore | no project-file writes |
|
|
64
|
+
| `general` | inherited active tools + Pi context tools | yes if tools include bash/edit/write |
|
|
65
|
+
|
|
66
|
+
For Pi children, `new_context`, `get_context_remaining`, `history`, and
|
|
67
|
+
`notes` are control-plane tools. When available in the parent they remain in
|
|
68
|
+
the child allowlist—even if a narrower tool list was requested—so Pi's remote
|
|
69
|
+
`contextManagement` can stay active. They may update context notes/window
|
|
70
|
+
state, but never grant `bash`, `edit`, or `write` access.
|
|
65
71
|
|
|
66
72
|
Parallel write-capable tasks sharing one checkout are rejected unless each uses
|
|
67
73
|
`isolation: "worktree"`, a distinct `cwd`, or `allow_shared_writes: true`.
|
package/src/extension.ts
CHANGED
|
@@ -24,11 +24,18 @@ import { abortAsPromise } from "./maintenance.js";
|
|
|
24
24
|
import { sweepSessionsLifecycle } from "./distill.js";
|
|
25
25
|
import { runTasks } from "./orchestrator.js";
|
|
26
26
|
import { OutputManager } from "./output.js";
|
|
27
|
-
import { parseDepth, parseSpawnPolicy, SPAWNS_ENV_VAR, validateSubagentRequest, type ResolvedTask } from "./policy.js";
|
|
27
|
+
import { CONTEXT_MANAGEMENT_TOOLS, parseDepth, parseSpawnPolicy, SPAWNS_ENV_VAR, validateSubagentRequest, type ResolvedTask } from "./policy.js";
|
|
28
28
|
import type { ChildRunner } from "./runner.js";
|
|
29
29
|
import { ProcessLockManager } from "./process-lock.js";
|
|
30
30
|
import { SessionScopedRunRegistry, snapshotFromLiveRun } from "./registry.js";
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
ProviderSubagentParamsSchema,
|
|
33
|
+
ProviderSubagentWaitParamsSchema,
|
|
34
|
+
SubagentParamsSchema,
|
|
35
|
+
SubagentWaitParamsSchema,
|
|
36
|
+
type SubagentParams,
|
|
37
|
+
type SubagentWaitParams,
|
|
38
|
+
} from "./schema.js";
|
|
32
39
|
import { BTW_ENTRY_TYPE, btwLabel, type BtwEntry } from "./btw.js";
|
|
33
40
|
import { Semaphore } from "./semaphore.js";
|
|
34
41
|
import type { RunSnapshot, TaskResult, TaskSpec, UsageStats } from "./types.js";
|
|
@@ -563,7 +570,7 @@ async function runSynthesis(
|
|
|
563
570
|
label: "synthesis",
|
|
564
571
|
profile: "review",
|
|
565
572
|
canWrite: false,
|
|
566
|
-
tools: ["read"],
|
|
573
|
+
tools: ["read", ...CONTEXT_MANAGEMENT_TOOLS],
|
|
567
574
|
model: approved.route.model,
|
|
568
575
|
fallbackModels: [...approved.route.fallbackModels],
|
|
569
576
|
thinking: approved.route.thinking ?? "low",
|
|
@@ -944,7 +951,7 @@ export default function registerSubagent(pi: ExtensionAPI): void {
|
|
|
944
951
|
// usable immediately via agent:'name' (execute-time refresh); only the
|
|
945
952
|
// system-prompt advertisement waits for the next session.
|
|
946
953
|
promptGuidelines: guidelines(discoverAgents(process.cwd())),
|
|
947
|
-
parameters:
|
|
954
|
+
parameters: ProviderSubagentParamsSchema,
|
|
948
955
|
async execute(_id, params: SubagentParams, signal, onUpdate, ctx) {
|
|
949
956
|
// `subagent_wait` delegates here with a synthesized action:"wait" params
|
|
950
957
|
// object, carrying its timeout through this non-schema field so the two
|
|
@@ -1507,7 +1514,7 @@ export default function registerSubagent(pi: ExtensionAPI): void {
|
|
|
1507
1514
|
label: "Subagent wait",
|
|
1508
1515
|
description:
|
|
1509
1516
|
"Block until a background subagent run (async:true) settles, then deliver its output. Equivalent to subagent { action: 'wait', id }. Aborting or timing out leaves the run alive and collectable; use subagent { action: 'cancel' } to stop it.",
|
|
1510
|
-
parameters:
|
|
1517
|
+
parameters: ProviderSubagentWaitParamsSchema,
|
|
1511
1518
|
async execute(id, params: SubagentWaitParams, signal, onUpdate, ctx) {
|
|
1512
1519
|
return subagentTool.execute(
|
|
1513
1520
|
id,
|
package/src/policy.ts
CHANGED
|
@@ -27,6 +27,19 @@ export const READ_ONLY_TOOLS = new Set([
|
|
|
27
27
|
"web_search",
|
|
28
28
|
"web_fetch",
|
|
29
29
|
]);
|
|
30
|
+
/**
|
|
31
|
+
* Pi context-management tools are control-plane capabilities: they may update
|
|
32
|
+
* continuity notes or the remote context window, but they cannot modify the
|
|
33
|
+
* child checkout. Keep them separate from ordinary source-inspection tools so
|
|
34
|
+
* the read-only profile's exception remains explicit.
|
|
35
|
+
*/
|
|
36
|
+
export const CONTEXT_MANAGEMENT_TOOLS = new Set([
|
|
37
|
+
"new_context",
|
|
38
|
+
"get_context_remaining",
|
|
39
|
+
"history",
|
|
40
|
+
"notes",
|
|
41
|
+
]);
|
|
42
|
+
const NON_WRITING_TOOLS = new Set([...READ_ONLY_TOOLS, ...CONTEXT_MANAGEMENT_TOOLS]);
|
|
30
43
|
export const KNOWN_WRITE_TOOLS = new Set(["bash", "edit", "write"]);
|
|
31
44
|
/** Backward-compatible export; policy uses fail-closed classification above. */
|
|
32
45
|
export const WRITE_TOOLS = KNOWN_WRITE_TOOLS;
|
|
@@ -75,31 +88,41 @@ function resolveTools(
|
|
|
75
88
|
requested: string[] | undefined,
|
|
76
89
|
availableTools: string[],
|
|
77
90
|
activeTools: string[],
|
|
91
|
+
backend: BackendName,
|
|
78
92
|
): { tools?: string[]; canWrite?: boolean; error?: string } {
|
|
79
93
|
const available = new Set(availableTools);
|
|
94
|
+
const contextTools = backend === "pi"
|
|
95
|
+
? [...CONTEXT_MANAGEMENT_TOOLS].filter((tool) => available.has(tool))
|
|
96
|
+
: [];
|
|
97
|
+
const nonWritingTools = backend === "pi" ? NON_WRITING_TOOLS : READ_ONLY_TOOLS;
|
|
98
|
+
// Keep Pi's context-management control plane available to every child when
|
|
99
|
+
// the parent exposes it, even if the task requested a narrower tool subset.
|
|
100
|
+
const addContextTools = (tools: readonly string[]): string[] =>
|
|
101
|
+
[...new Set([...tools, ...contextTools])];
|
|
102
|
+
|
|
80
103
|
if (requested) {
|
|
81
104
|
const unknown = requested.filter((tool) => !available.has(tool));
|
|
82
105
|
if (unknown.length) return { error: `Unknown or unavailable tools: ${unknown.join(", ")}` };
|
|
83
106
|
}
|
|
84
107
|
|
|
85
108
|
if (profile === "explore" || profile === "review") {
|
|
86
|
-
const source = requested ?? [...
|
|
87
|
-
const unsafe = source.filter((tool) => !
|
|
109
|
+
const source = addContextTools(requested ?? [...nonWritingTools].filter((tool) => available.has(tool)));
|
|
110
|
+
const unsafe = source.filter((tool) => !nonWritingTools.has(tool));
|
|
88
111
|
if (unsafe.length) {
|
|
89
112
|
return {
|
|
90
113
|
error: `${profile} is strictly read-only. Unclassified or writable tools are not allowed: ${unsafe.join(", ")}`,
|
|
91
114
|
};
|
|
92
115
|
}
|
|
93
|
-
return { tools:
|
|
116
|
+
return { tools: source, canWrite: false };
|
|
94
117
|
}
|
|
95
118
|
|
|
96
|
-
const source = requested ?? activeTools;
|
|
119
|
+
const source = addContextTools(requested ?? activeTools);
|
|
97
120
|
const unknown = source.filter((tool) => !available.has(tool));
|
|
98
121
|
if (unknown.length) return { error: `Active tools are unavailable: ${unknown.join(", ")}` };
|
|
99
|
-
// General-profile custom tools are conservatively write-capable unless explicitly known
|
|
122
|
+
// General-profile custom tools are conservatively write-capable unless explicitly known non-writing.
|
|
100
123
|
return {
|
|
101
|
-
tools:
|
|
102
|
-
canWrite: source.some((tool) => KNOWN_WRITE_TOOLS.has(tool) || !
|
|
124
|
+
tools: source,
|
|
125
|
+
canWrite: source.some((tool) => KNOWN_WRITE_TOOLS.has(tool) || !nonWritingTools.has(tool)),
|
|
103
126
|
};
|
|
104
127
|
}
|
|
105
128
|
|
|
@@ -193,9 +216,13 @@ function normalizeTask(
|
|
|
193
216
|
}
|
|
194
217
|
}
|
|
195
218
|
|
|
219
|
+
const backend: BackendName = item.backend ?? agent?.backend ?? "pi";
|
|
220
|
+
if (!BACKEND_NAMES.includes(backend)) {
|
|
221
|
+
return { error: `Task ${index + 1}: unknown backend '${backend}' (expected ${BACKEND_NAMES.join(", ")})` };
|
|
222
|
+
}
|
|
196
223
|
const profile = item.profile ?? agent?.profile ?? defaultProfile;
|
|
197
224
|
const requestedTools = item.tools ?? agent?.tools;
|
|
198
|
-
const resolved = resolveTools(profile, requestedTools, parent.availableTools, parent.activeTools ?? parent.availableTools);
|
|
225
|
+
const resolved = resolveTools(profile, requestedTools, parent.availableTools, parent.activeTools ?? parent.availableTools, backend);
|
|
199
226
|
if (resolved.error || !resolved.tools || resolved.canWrite === undefined) return { error: resolved.error ?? "Tool resolution failed" };
|
|
200
227
|
const cwd = resolvePath(parent.cwd, item.cwd);
|
|
201
228
|
const output = item.output ? resolvePath(cwd, item.output) : undefined;
|
|
@@ -216,10 +243,6 @@ function normalizeTask(
|
|
|
216
243
|
|
|
217
244
|
// Backend capability gate. Refuse combinations the backend cannot honor
|
|
218
245
|
// rather than silently dropping a budget or a read-only guarantee.
|
|
219
|
-
const backend: BackendName = item.backend ?? agent?.backend ?? "pi";
|
|
220
|
-
if (!BACKEND_NAMES.includes(backend)) {
|
|
221
|
-
return { error: `Task ${index + 1}: unknown backend '${backend}' (expected ${BACKEND_NAMES.join(", ")})` };
|
|
222
|
-
}
|
|
223
246
|
const capabilities = resolveBackend(backend).capabilities;
|
|
224
247
|
const problems = checkCapabilities(
|
|
225
248
|
{
|
package/src/schema.ts
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
import { Type, type Static } from "typebox";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Remove TypeBox's internal compositor metadata before a schema crosses into
|
|
5
|
+
* Pi's provider-facing tool catalog. The original schema is kept intact for
|
|
6
|
+
* local Value.Check/Value.Errors validation.
|
|
7
|
+
*/
|
|
8
|
+
export function sanitizeProviderSchema<T>(value: T): T {
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
return value.map((item) => sanitizeProviderSchema(item)) as T;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (value && typeof value === "object") {
|
|
14
|
+
const record = value as Record<string, unknown>;
|
|
15
|
+
return Object.fromEntries(
|
|
16
|
+
Object.entries(record)
|
|
17
|
+
.filter(([key]) => !key.startsWith("~"))
|
|
18
|
+
.map(([key, entry]) => [key, sanitizeProviderSchema(entry)] as const),
|
|
19
|
+
) as T;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
3
25
|
const ThinkingLevel = Type.String({
|
|
4
26
|
minLength: 1,
|
|
5
27
|
maxLength: 64,
|
|
@@ -29,8 +51,8 @@ export const TaskFields = {
|
|
|
29
51
|
system_prompt: Type.Optional(Type.String({ description: "Extra system prompt appended to the child's prompt (does not replace it)." })),
|
|
30
52
|
model: Type.Optional(Type.String({ description: "**REQUIRED for every spawn call (task/tasks).** Exact model id from modelPolicy, in provider/model-id form. Calls without an explicit model are rejected; agent-file model, taskDefaults, and parent-session inheritance are ignored. Management actions (status/wait/cancel/steer/diff/apply/discard) do not need it." })),
|
|
31
53
|
thinking: Type.Optional({ ...ThinkingLevel, description: "Opaque Pi thinking level for the child. Values such as max are passed through unchanged; Pi/model support decides validity. Defaults to agent thinking, profile taskDefaults.thinking, modelPolicy route thinking, then the parent's level." }),
|
|
32
|
-
tools: Type.Optional(Type.Array(Type.String(), { description: "Optional tool allowlist. explore/review profiles reject
|
|
33
|
-
profile: Type.Optional({ ...Profile, description: "Capability profile: explore/review
|
|
54
|
+
tools: Type.Optional(Type.Array(Type.String(), { description: "Optional tool allowlist. explore/review profiles reject project-writing tools; Pi context-management tools remain available for context continuity." })),
|
|
55
|
+
profile: Type.Optional({ ...Profile, description: "Capability profile: explore/review cannot write project files but retain Pi context-management tools; general inherits the parent's active tools and may write." }),
|
|
34
56
|
cwd: Type.Optional(Type.String({ description: "Working directory for the child process." })),
|
|
35
57
|
timeout_ms: Type.Optional(Type.Number({ minimum: 1, maximum: 24 * 60 * 60_000, description: "Total budget in milliseconds including queue time. Timed-out runs report which phase timed out." })),
|
|
36
58
|
max_turns: Type.Optional(Type.Number({ minimum: 1, maximum: 500, description: "Budget: at this many turns the child is steered to wrap up and given grace turns for a final answer; ends as 'partial' with output preserved." })),
|
|
@@ -152,6 +174,10 @@ export const SubagentWaitParamsSchema = Type.Object(
|
|
|
152
174
|
},
|
|
153
175
|
);
|
|
154
176
|
|
|
177
|
+
/** Provider-facing projections without TypeBox's internal `~...` metadata. */
|
|
178
|
+
export const ProviderSubagentParamsSchema = sanitizeProviderSchema(SubagentParamsSchema);
|
|
179
|
+
export const ProviderSubagentWaitParamsSchema = sanitizeProviderSchema(SubagentWaitParamsSchema);
|
|
180
|
+
|
|
155
181
|
export type SubagentWaitParams = Static<typeof SubagentWaitParamsSchema>;
|
|
156
182
|
|
|
157
183
|
/** Runtime guard used by tests/docs to assert provider compatibility. */
|