@cr1ms0n/pi-subagent 0.8.7 → 0.8.8
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 +8 -0
- package/README.md +20 -7
- package/docs/ARCHITECTURE.md +9 -2
- package/docs/SECURITY.md +13 -4
- package/package.json +1 -1
- package/skills/subagent/SKILL.md +14 -6
- package/src/agents.ts +8 -2
- package/src/context-policy.ts +169 -0
- package/src/extension.ts +33 -6
- package/src/orchestrator.ts +67 -2
- package/src/policy.ts +56 -26
- package/src/schema.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.8.8 — 2026-09-16
|
|
6
|
+
|
|
7
|
+
### Remote Context tools are target-model gated
|
|
8
|
+
|
|
9
|
+
- Restrict `new_context`, `get_context_remaining`, `history`, and `notes` to Pi children whose target model is an exact `compaction.gatewayContextModels` entry of the operator-owned `pi-openai-toolkit` config while `compaction.contextManagement` is `remote`. The previous 0.8.6 behavior appended these tools to every Pi child whenever the parent exposed them, so a non-allowlisted child could receive context tools simply because the parent session had them.
|
|
10
|
+
- Matching is exact and case-sensitive on the full `provider/model` string; prefixes, bare model ids, and native `openai-codex` identity do not match. Explicit requests for a context tool on a non-eligible target are rejected as unavailable, inherited/default lists are filtered, and retry fallback attempts are re-filtered per attempt so a disallowed fallback cannot inherit them. The internally constructed synthesis child uses the same gate.
|
|
11
|
+
- Missing, unreadable, invalid, or non-`remote` toolkit configuration now fails closed to no context tools. Non-Pi backends, read-only/write classification, and model-policy routing are unchanged. The package reads the existing operator-owned allowlist and does not duplicate it in `~/.pi/subagent.json`.
|
|
12
|
+
|
|
5
13
|
## 0.8.7 — 2026-09-14
|
|
6
14
|
|
|
7
15
|
### Provider-safe tool schemas
|
package/README.md
CHANGED
|
@@ -193,17 +193,30 @@ Set a persona's backend in agent frontmatter with `backend: codex`.
|
|
|
193
193
|
|
|
194
194
|
| Profile | Tools | Writes |
|
|
195
195
|
| ---------------------------- | ------------------------------------------------------- | ------------------------------------------- |
|
|
196
|
-
| `explore` (parallel default) | read/grep/find/ls + safe extras +
|
|
196
|
+
| `explore` (parallel default) | read/grep/find/ls + safe extras + allowlisted context tools | no project-file writes |
|
|
197
197
|
| `review` | same as explore | no project-file writes |
|
|
198
|
-
| `general` | inherited active tools +
|
|
198
|
+
| `general` | inherited active tools + allowlisted context tools | yes if tools include bash/edit/write |
|
|
199
199
|
|
|
200
200
|
For the Pi backend, the context-management tools `new_context`,
|
|
201
201
|
`get_context_remaining`, `history`, and `notes` are retained in child tool
|
|
202
|
-
allowlists when
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
202
|
+
allowlists only when all three hold: the toolkit config at
|
|
203
|
+
`$PI_CODING_AGENT_DIR/extensions/pi-openai-toolkit/config.json` (default:
|
|
204
|
+
`~/.pi/agent/extensions/pi-openai-toolkit/config.json`) has
|
|
205
|
+
`compaction.contextManagement: "remote"`, the child's target model exactly
|
|
206
|
+
equals one `compaction.gatewayContextModels` entry, and the parent exposes the
|
|
207
|
+
tool. Eligibility is exact and case-sensitive on the full `provider/model`
|
|
208
|
+
string — prefixes, bare model ids, and native `openai-codex` identity do not
|
|
209
|
+
match. The package reads that operator-owned list and never duplicates it.
|
|
210
|
+
|
|
211
|
+
Context tools are control-plane tools: they may update continuity notes or the
|
|
212
|
+
remote context window, but cannot modify the child checkout or run a shell
|
|
213
|
+
command. For an eligible target they are appended even when the task supplies a
|
|
214
|
+
narrower tool list; for every other Pi target they are removed from inherited
|
|
215
|
+
tools, never passed to `--tools`, and an explicit request for one is rejected as
|
|
216
|
+
unavailable. Retry fallback attempts are filtered per attempt, so a disallowed
|
|
217
|
+
fallback cannot inherit them. If the toolkit file is missing, unreadable,
|
|
218
|
+
invalid, or not in Remote Context mode, the effective allowlist is empty (fail
|
|
219
|
+
closed). Non-Pi backends are unchanged.
|
|
207
220
|
|
|
208
221
|
Parallel write-capable tasks sharing one checkout are rejected unless each uses
|
|
209
222
|
`isolation: "worktree"`, distinct `cwd`, or explicit `allow_shared_writes: true`.
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -14,7 +14,14 @@
|
|
|
14
14
|
PID; transcript joins happen only on message boundaries, not per-chunk ticks.
|
|
15
15
|
- Retry with model fallback lives in `orchestrator.ts` (`isTransientFailure`): queue
|
|
16
16
|
timeouts, stalls, spawn errors, and provider errors re-run the same spec on the next
|
|
17
|
-
fallback model with accumulated usage; task-quality failures never retry.
|
|
17
|
+
fallback model with accumulated usage; task-quality failures never retry. Each attempt
|
|
18
|
+
re-derives its Pi context-tool membership from the attempt's target model via
|
|
19
|
+
`context-policy.ts`, so an ineligible fallback cannot inherit the primary's tools.
|
|
20
|
+
- `context-policy.ts`: sole reader/parser of the operator-owned `pi-openai-toolkit`
|
|
21
|
+
Remote Context allowlist (`<Pi agent dir>/extensions/pi-openai-toolkit/config.json`).
|
|
22
|
+
Exposes the exact `gatewayContextModels` match plus the four context-tool names, and
|
|
23
|
+
fails closed to an empty allowlist on any missing/unreadable/invalid/non-remote
|
|
24
|
+
configuration. The toolkit's native `openai-codex` rule is intentionally not mirrored.
|
|
18
25
|
- `context: "fork"` spawns the child with `--fork <parent session file>` so it starts
|
|
19
26
|
from a real branched copy of the parent conversation. Fail-fast when the parent
|
|
20
27
|
session is not persisted; single-task only.
|
|
@@ -29,7 +36,7 @@
|
|
|
29
36
|
- `persistence.ts`: versioned active-branch event folding and bounded child transcript metadata.
|
|
30
37
|
- `maintenance.ts`: filesystem GC (session files) and abort-race helpers; kept out of persistence.
|
|
31
38
|
- `usage.ts`: provider-reported root/subagent/combined accounting.
|
|
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
|
|
39
|
+
- `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 are granted per target model: `context-policy.ts` reads the operator-owned `pi-openai-toolkit` Remote Context allowlist (`compaction.gatewayContextModels` with `contextManagement: "remote"`), `policy.ts` removes the four context names from every non-eligible Pi child, `orchestrator.ts` re-filters them per fallback attempt, and both fail closed to no context tools when the config is missing or invalid.
|
|
33
40
|
- `config.ts`: defaults ← `~/.pi/subagent.json` ← `PI_SUBAGENT_*` env overrides.
|
|
34
41
|
- `structured.ts`: structured-output contract (dependency-free JSON-Schema subset
|
|
35
42
|
validation, fenced json:result extraction, contract/repair prompts) and
|
package/docs/SECURITY.md
CHANGED
|
@@ -8,9 +8,17 @@ 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` (+ safe extras and Pi context tools) | No project-file writes |
|
|
11
|
+
| `explore` | `read`, `grep`, `find`, `ls` (+ safe extras and allowlisted Pi context tools) | No project-file writes |
|
|
12
12
|
| `review` | same as explore | No project-file writes |
|
|
13
|
-
| `general` | inherited active tools (+ Pi context tools) | Yes if `bash`/`edit`/`write` are active |
|
|
13
|
+
| `general` | inherited active tools (+ allowlisted Pi context tools) | Yes if `bash`/`edit`/`write` are active |
|
|
14
|
+
|
|
15
|
+
Pi context tools are allowlisted by **target model**, not by parent session: a
|
|
16
|
+
child receives `new_context`, `get_context_remaining`, `history`, or `notes`
|
|
17
|
+
only when the toolkit config has `compaction.contextManagement: "remote"`, the
|
|
18
|
+
child's target model exactly equals a `compaction.gatewayContextModels` entry,
|
|
19
|
+
and the parent exposes the tool. Missing/unreadable/invalid/disabled toolkit
|
|
20
|
+
configuration yields no context tools. Retry fallback attempts are re-filtered
|
|
21
|
+
per attempt, so a disallowed fallback cannot inherit them.
|
|
14
22
|
|
|
15
23
|
Parallel mode defaults to `explore` to avoid concurrent shared writes.
|
|
16
24
|
|
|
@@ -19,8 +27,9 @@ Parallel mode defaults to `explore` to avoid concurrent shared writes.
|
|
|
19
27
|
1. **Read-only means no project-file mutation.** `bash` can rewrite the disk and is never part of
|
|
20
28
|
an explore/review profile. Pi context-management tools (`new_context`,
|
|
21
29
|
`get_context_remaining`, `history`, `notes`) are an explicit control-plane
|
|
22
|
-
exception
|
|
23
|
-
the project write
|
|
30
|
+
exception, but only for the exact target models allowlisted above: they may
|
|
31
|
+
update continuity notes/window state but cannot access the project write
|
|
32
|
+
tools, and they are rejected as unavailable for every other target.
|
|
24
33
|
2. **Parallel writers** require `isolation: "worktree"`, distinct `cwd` values,
|
|
25
34
|
or an explicit `allow_shared_writes: true` opt-in.
|
|
26
35
|
3. **Depth is capped** (`maxDepth`, default 2). Nested children at the ceiling do
|
package/package.json
CHANGED
package/skills/subagent/SKILL.md
CHANGED
|
@@ -59,15 +59,23 @@ const routeModel = "<exact model from current modelPolicy route>";
|
|
|
59
59
|
|
|
60
60
|
| Profile | Tools | Writes |
|
|
61
61
|
| --------- | --------------------------------------------------------- | ------------------------------------------- |
|
|
62
|
-
| `explore` | read/search/ls (+safe) +
|
|
62
|
+
| `explore` | read/search/ls (+safe) + allowlisted context tools | no project-file writes |
|
|
63
63
|
| `review` | same as explore | no project-file writes |
|
|
64
|
-
| `general` | inherited active tools +
|
|
64
|
+
| `general` | inherited active tools + allowlisted context tools | yes if tools include bash/edit/write |
|
|
65
65
|
|
|
66
66
|
For Pi children, `new_context`, `get_context_remaining`, `history`, and
|
|
67
|
-
`notes` are control-plane tools.
|
|
68
|
-
the
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
`notes` are control-plane tools. They are kept in the child allowlist only when
|
|
68
|
+
the target model exactly matches a `compaction.gatewayContextModels` entry in
|
|
69
|
+
the Remote Context toolkit config
|
|
70
|
+
(`$PI_CODING_AGENT_DIR/extensions/pi-openai-toolkit/config.json`, default:
|
|
71
|
+
`~/.pi/agent/extensions/pi-openai-toolkit/config.json`, with
|
|
72
|
+
`contextManagement: "remote"`), and only when the parent exposes them. For an
|
|
73
|
+
eligible target they stay even if a narrower tool list was requested; for any
|
|
74
|
+
other Pi target they are dropped from inherited tools, never passed to
|
|
75
|
+
`--tools`, and an explicit request for one is rejected as unavailable. Fallback
|
|
76
|
+
attempts are filtered per attempt. Missing/unreadable/invalid/disabled toolkit
|
|
77
|
+
configuration means no context tools at all. They may update context
|
|
78
|
+
notes/window state, but never grant `bash`, `edit`, or `write` access.
|
|
71
79
|
|
|
72
80
|
Parallel write-capable tasks sharing one checkout are rejected unless each uses
|
|
73
81
|
`isolation: "worktree"`, a distinct `cwd`, or `allow_shared_writes: true`.
|
package/src/agents.ts
CHANGED
|
@@ -64,7 +64,13 @@ export interface AgentDefinition {
|
|
|
64
64
|
const MAX_AGENT_FILE_BYTES = 64 * 1024;
|
|
65
65
|
const NAME_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Resolve the Pi agent directory: `$PI_CODING_AGENT_DIR` (with `~` expansion)
|
|
69
|
+
* else `~/.pi/agent`. Exported so other configuration readers (for example the
|
|
70
|
+
* Remote Context allowlist in `context-policy.ts`) share one path convention
|
|
71
|
+
* instead of inventing a second one.
|
|
72
|
+
*/
|
|
73
|
+
export function piAgentDir(): string {
|
|
68
74
|
const envDir = process.env.PI_CODING_AGENT_DIR?.trim();
|
|
69
75
|
if (envDir) {
|
|
70
76
|
return envDir.startsWith("~") ? path.join(os.homedir(), envDir.slice(1)) : envDir;
|
|
@@ -76,7 +82,7 @@ export function discoveryRoots(cwd: string): Array<{ dir: string; scope: AgentDe
|
|
|
76
82
|
return [
|
|
77
83
|
{ dir: path.join(cwd, ".pi", "agents"), scope: "project" },
|
|
78
84
|
{ dir: path.join(cwd, ".agents", "agents"), scope: "shared" },
|
|
79
|
-
{ dir: path.join(
|
|
85
|
+
{ dir: path.join(piAgentDir(), "agents"), scope: "global" },
|
|
80
86
|
];
|
|
81
87
|
}
|
|
82
88
|
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote Context eligibility boundary for the four Pi context-management tools.
|
|
3
|
+
*
|
|
4
|
+
* `pi-subagent` does not own the Remote Context model list: the operator-owned
|
|
5
|
+
* `pi-openai-toolkit` configuration owns it. This module reads that file once
|
|
6
|
+
* per dispatch and fails closed to an empty allowlist whenever the file is
|
|
7
|
+
* missing, unreadable, malformed, or not in `contextManagement: "remote"` mode.
|
|
8
|
+
*
|
|
9
|
+
* Matching is exact and case-sensitive on the complete `provider/model` string:
|
|
10
|
+
* entries are never trimmed, prefix-matched, or inferred from a bare model id.
|
|
11
|
+
* The toolkit's separate native `openai-codex` provider rule is intentionally
|
|
12
|
+
* not mirrored here — only configured `gatewayContextModels` entries are
|
|
13
|
+
* eligible, because the boundary is "models outside the allowlist receive no
|
|
14
|
+
* context-manager tools".
|
|
15
|
+
*
|
|
16
|
+
* Nothing in this module writes to stdout: that channel is the child RPC
|
|
17
|
+
* protocol and the parent TUI.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import * as fs from "node:fs/promises";
|
|
21
|
+
import * as path from "node:path";
|
|
22
|
+
import { piAgentDir } from "./agents.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Pi context-management tools are control-plane capabilities: they may update
|
|
26
|
+
* continuity notes or the remote context window, but they cannot modify the
|
|
27
|
+
* child checkout. Keep them separate from ordinary source-inspection tools so
|
|
28
|
+
* the read-only profile's exception remains explicit.
|
|
29
|
+
*/
|
|
30
|
+
export const CONTEXT_MANAGEMENT_TOOLS: ReadonlySet<string> = new Set([
|
|
31
|
+
"new_context",
|
|
32
|
+
"get_context_remaining",
|
|
33
|
+
"history",
|
|
34
|
+
"notes",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
/** Canonical enumeration order when appending context tools to a child allowlist. */
|
|
38
|
+
export const CONTEXT_MANAGEMENT_TOOL_NAMES: readonly string[] = Object.freeze([
|
|
39
|
+
...CONTEXT_MANAGEMENT_TOOLS,
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
/** Toolkit config path relative to the Pi agent directory. */
|
|
43
|
+
export const TOOLKIT_CONFIG_PATH_PARTS: readonly string[] = Object.freeze([
|
|
44
|
+
"extensions",
|
|
45
|
+
"pi-openai-toolkit",
|
|
46
|
+
"config.json",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
/** Immutable per-dispatch snapshot of the operator-owned Remote Context allowlist. */
|
|
50
|
+
export interface ContextManagementPolicy {
|
|
51
|
+
/** Exact `provider/model` strings eligible for Remote Context. */
|
|
52
|
+
readonly gatewayModels: readonly string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Fail-closed policy used for missing/unreadable/invalid/disabled configuration. */
|
|
56
|
+
export const EMPTY_CONTEXT_MANAGEMENT_POLICY: ContextManagementPolicy = Object.freeze({
|
|
57
|
+
gatewayModels: Object.freeze([]),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
61
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
62
|
+
? (value as Record<string, unknown>)
|
|
63
|
+
: undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Pure parser over the toolkit config JSON.
|
|
68
|
+
*
|
|
69
|
+
* Returns the exact gateway model set only when `compaction.contextManagement`
|
|
70
|
+
* is `"remote"` and every `compaction.gatewayContextModels` entry is a non-empty
|
|
71
|
+
* string. Any other shape — including a partly malformed list — yields the
|
|
72
|
+
* empty policy so a compromised/edited file cannot widen the boundary.
|
|
73
|
+
*/
|
|
74
|
+
export function parseContextManagementPolicy(raw: unknown): ContextManagementPolicy {
|
|
75
|
+
const compaction = asRecord(asRecord(raw)?.compaction);
|
|
76
|
+
if (!compaction || compaction.contextManagement !== "remote") return EMPTY_CONTEXT_MANAGEMENT_POLICY;
|
|
77
|
+
const listed = compaction.gatewayContextModels;
|
|
78
|
+
if (!Array.isArray(listed)) return EMPTY_CONTEXT_MANAGEMENT_POLICY;
|
|
79
|
+
const gatewayModels: string[] = [];
|
|
80
|
+
for (const entry of listed) {
|
|
81
|
+
if (typeof entry !== "string" || entry.trim() === "") return EMPTY_CONTEXT_MANAGEMENT_POLICY;
|
|
82
|
+
if (!gatewayModels.includes(entry)) gatewayModels.push(entry);
|
|
83
|
+
}
|
|
84
|
+
return Object.freeze({ gatewayModels: Object.freeze(gatewayModels) });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Default toolkit config path, using the shared Pi agent-directory precedence. */
|
|
88
|
+
export function contextManagementConfigPath(agentDir = piAgentDir()): string {
|
|
89
|
+
return path.join(agentDir, ...TOOLKIT_CONFIG_PATH_PARTS);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Read one Remote Context snapshot. The injectable path keeps checks offline.
|
|
94
|
+
* Every failure mode (missing, unreadable, invalid JSON, wrong shape) returns
|
|
95
|
+
* the empty policy, so an ineligible target never inherits context tools.
|
|
96
|
+
*/
|
|
97
|
+
export async function readContextManagementPolicy(
|
|
98
|
+
file = contextManagementConfigPath(),
|
|
99
|
+
): Promise<ContextManagementPolicy> {
|
|
100
|
+
try {
|
|
101
|
+
return parseContextManagementPolicy(JSON.parse(await fs.readFile(file, "utf8")));
|
|
102
|
+
} catch {
|
|
103
|
+
return EMPTY_CONTEXT_MANAGEMENT_POLICY; // fail closed; stdout is the RPC channel
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** True when `model` exactly equals a configured `gatewayContextModels` entry. */
|
|
108
|
+
export function isGatewayContextModel(
|
|
109
|
+
policy: ContextManagementPolicy | undefined,
|
|
110
|
+
model: string | undefined,
|
|
111
|
+
): boolean {
|
|
112
|
+
if (!policy || !model) return false;
|
|
113
|
+
return policy.gatewayModels.includes(model);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Context-tool names a target model may receive: the parent-exposed subset,
|
|
118
|
+
* and only for an exact allowlisted model. Empty for every other target.
|
|
119
|
+
*/
|
|
120
|
+
export function contextToolsForModel(
|
|
121
|
+
policy: ContextManagementPolicy | undefined,
|
|
122
|
+
model: string | undefined,
|
|
123
|
+
parentExposed: readonly string[],
|
|
124
|
+
): string[] {
|
|
125
|
+
if (!isGatewayContextModel(policy, model)) return [];
|
|
126
|
+
const exposed = new Set(parentExposed);
|
|
127
|
+
return CONTEXT_MANAGEMENT_TOOL_NAMES.filter((tool) => exposed.has(tool));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Tool list for the internally constructed read-only synthesis child. */
|
|
131
|
+
export function synthesisToolsForModel(
|
|
132
|
+
policy: ContextManagementPolicy | undefined,
|
|
133
|
+
model: string | undefined,
|
|
134
|
+
parentExposed: readonly string[],
|
|
135
|
+
): string[] {
|
|
136
|
+
return ["read", ...contextToolsForModel(policy, model, parentExposed)];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Re-derive a child tool allowlist for one target model (primary request or a
|
|
141
|
+
* fallback attempt). Non-context names keep their existing order/deduplication;
|
|
142
|
+
* all four context names are removed first and re-appended only when the
|
|
143
|
+
* effective backend is Pi and the attempt model is exactly allowlisted.
|
|
144
|
+
*
|
|
145
|
+
* Non-Pi backends are returned unchanged. Absent a dispatch policy, a Pi list
|
|
146
|
+
* loses every context name (fail closed) — low-level `runTasks` callers that
|
|
147
|
+
* provide a validated `TaskSpec` but no snapshot cannot reintroduce them. A Pi
|
|
148
|
+
* task with no explicit list becomes `--no-tools` at the backend boundary
|
|
149
|
+
* rather than silently falling back to Pi's unrestricted default tool set.
|
|
150
|
+
*/
|
|
151
|
+
export function filterContextToolsForModel(
|
|
152
|
+
tools: readonly string[] | undefined,
|
|
153
|
+
options: {
|
|
154
|
+
backend: string;
|
|
155
|
+
model?: string;
|
|
156
|
+
policy?: ContextManagementPolicy;
|
|
157
|
+
parentExposed?: readonly string[];
|
|
158
|
+
},
|
|
159
|
+
): string[] | undefined {
|
|
160
|
+
if (tools === undefined) return options.backend === "pi" ? [] : undefined;
|
|
161
|
+
if (options.backend !== "pi") return [...tools];
|
|
162
|
+
const kept = tools.filter((tool) => !CONTEXT_MANAGEMENT_TOOLS.has(tool));
|
|
163
|
+
return [
|
|
164
|
+
...new Set([
|
|
165
|
+
...kept,
|
|
166
|
+
...contextToolsForModel(options.policy, options.model, options.parentExposed ?? []),
|
|
167
|
+
]),
|
|
168
|
+
];
|
|
169
|
+
}
|
package/src/extension.ts
CHANGED
|
@@ -22,9 +22,15 @@ import {
|
|
|
22
22
|
import { createGetPiCommand, getLaunchResolution } from "./launch.js";
|
|
23
23
|
import { abortAsPromise } from "./maintenance.js";
|
|
24
24
|
import { sweepSessionsLifecycle } from "./distill.js";
|
|
25
|
-
import {
|
|
25
|
+
import { runTasksWithContextPolicy } from "./orchestrator.js";
|
|
26
26
|
import { OutputManager } from "./output.js";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
CONTEXT_MANAGEMENT_TOOL_NAMES,
|
|
29
|
+
readContextManagementPolicy,
|
|
30
|
+
synthesisToolsForModel,
|
|
31
|
+
type ContextManagementPolicy,
|
|
32
|
+
} from "./context-policy.js";
|
|
33
|
+
import { parseDepth, parseSpawnPolicy, SPAWNS_ENV_VAR, validateSubagentRequest, type ResolvedTask } from "./policy.js";
|
|
28
34
|
import type { ChildRunner } from "./runner.js";
|
|
29
35
|
import { ProcessLockManager } from "./process-lock.js";
|
|
30
36
|
import { SessionScopedRunRegistry, snapshotFromLiveRun } from "./registry.js";
|
|
@@ -523,7 +529,13 @@ async function runSynthesis(
|
|
|
523
529
|
runtime: SessionRuntime,
|
|
524
530
|
instruction: string,
|
|
525
531
|
results: TaskResult[],
|
|
526
|
-
options: {
|
|
532
|
+
options: {
|
|
533
|
+
runId: string;
|
|
534
|
+
modelPolicy: ModelPolicySnapshot;
|
|
535
|
+
contextPolicy: ContextManagementPolicy;
|
|
536
|
+
parentContextTools: readonly string[];
|
|
537
|
+
signal: AbortSignal;
|
|
538
|
+
},
|
|
527
539
|
): Promise<TaskResult | undefined> {
|
|
528
540
|
const sections = results.map((result, index) => {
|
|
529
541
|
// Typed handoff: validated structured results feed the synthesis child
|
|
@@ -570,7 +582,9 @@ async function runSynthesis(
|
|
|
570
582
|
label: "synthesis",
|
|
571
583
|
profile: "review",
|
|
572
584
|
canWrite: false,
|
|
573
|
-
|
|
585
|
+
// Same exact target-model gate as ordinary children: a disallowed
|
|
586
|
+
// synthesis model receives no context-manager tools at all.
|
|
587
|
+
tools: synthesisToolsForModel(options.contextPolicy, approved.route.model, options.parentContextTools),
|
|
574
588
|
model: approved.route.model,
|
|
575
589
|
fallbackModels: [...approved.route.fallbackModels],
|
|
576
590
|
thinking: approved.route.thinking ?? "low",
|
|
@@ -976,12 +990,19 @@ export default function registerSubagent(pi: ExtensionAPI): void {
|
|
|
976
990
|
// restarting the parent session; no provider catalog or credentials are
|
|
977
991
|
// loaded by this path.
|
|
978
992
|
const dispatchConfig = loadConfig(await readConfigFile());
|
|
993
|
+
// One Remote Context snapshot per dispatch: the same operator-owned
|
|
994
|
+
// gateway allowlist gates initial validation, every fallback attempt,
|
|
995
|
+
// and the internally constructed synthesis child. Missing/invalid/off
|
|
996
|
+
// toolkit configuration fails closed to an empty allowlist.
|
|
997
|
+
const contextPolicy = await readContextManagementPolicy();
|
|
998
|
+
const parentToolNames = pi.getAllTools().map((tool) => tool.name);
|
|
999
|
+
const parentContextTools = CONTEXT_MANAGEMENT_TOOL_NAMES.filter((tool) => parentToolNames.includes(tool));
|
|
979
1000
|
const model = ctx.model;
|
|
980
1001
|
const validated = validateSubagentRequest(params, {
|
|
981
1002
|
cwd: ctx.cwd,
|
|
982
1003
|
model: model ? `${model.provider}/${model.id}` : undefined,
|
|
983
1004
|
thinking: pi.getThinkingLevel() as TaskSpec["thinking"],
|
|
984
|
-
availableTools:
|
|
1005
|
+
availableTools: parentToolNames,
|
|
985
1006
|
activeTools: pi.getActiveTools(),
|
|
986
1007
|
depth: parseDepth(),
|
|
987
1008
|
sessionFile: ctx.sessionManager.getSessionFile() ?? undefined,
|
|
@@ -993,6 +1014,7 @@ export default function registerSubagent(pi: ExtensionAPI): void {
|
|
|
993
1014
|
agents: agentCatalog(runtime),
|
|
994
1015
|
modelPolicy: dispatchConfig.modelPolicy,
|
|
995
1016
|
modelPolicyError: dispatchConfig.modelPolicyError,
|
|
1017
|
+
contextPolicy,
|
|
996
1018
|
});
|
|
997
1019
|
if (!validated.ok) fail(validated.error);
|
|
998
1020
|
|
|
@@ -1174,6 +1196,7 @@ export default function registerSubagent(pi: ExtensionAPI): void {
|
|
|
1174
1196
|
}
|
|
1175
1197
|
|
|
1176
1198
|
const specs: TaskSpec[] = validated.tasks.map((task: ResolvedTask) => ({
|
|
1199
|
+
backend: task.backend,
|
|
1177
1200
|
task: task.task,
|
|
1178
1201
|
label: task.label,
|
|
1179
1202
|
systemPrompt: task.systemPrompt,
|
|
@@ -1262,7 +1285,7 @@ export default function registerSubagent(pi: ExtensionAPI): void {
|
|
|
1262
1285
|
|
|
1263
1286
|
const work = (async () => {
|
|
1264
1287
|
try {
|
|
1265
|
-
const result = await
|
|
1288
|
+
const result = await runTasksWithContextPolicy(specs, {
|
|
1266
1289
|
semaphore: runtime.semaphore,
|
|
1267
1290
|
getPiCommand: runtime.getPiCommand,
|
|
1268
1291
|
sessionDir: runtime.config.sessionDir,
|
|
@@ -1276,6 +1299,8 @@ export default function registerSubagent(pi: ExtensionAPI): void {
|
|
|
1276
1299
|
stallAfterMs: runtime.config.stallAfterMs,
|
|
1277
1300
|
stallKillAfterMs: runtime.config.stallKillAfterMs,
|
|
1278
1301
|
maxRetries: runtime.config.maxRetries,
|
|
1302
|
+
contextPolicy,
|
|
1303
|
+
parentContextTools,
|
|
1279
1304
|
onRunnerCreated: (index, runner) => {
|
|
1280
1305
|
let runners = runtime.liveRunners.get(runId);
|
|
1281
1306
|
if (!runners) runtime.liveRunners.set(runId, (runners = new Map()));
|
|
@@ -1322,6 +1347,8 @@ export default function registerSubagent(pi: ExtensionAPI): void {
|
|
|
1322
1347
|
const synthesized = await runSynthesis(runtime, validated.synthesis, result.results, {
|
|
1323
1348
|
runId,
|
|
1324
1349
|
modelPolicy: dispatchConfig.modelPolicy!,
|
|
1350
|
+
contextPolicy,
|
|
1351
|
+
parentContextTools,
|
|
1325
1352
|
signal: controller.signal,
|
|
1326
1353
|
});
|
|
1327
1354
|
if (synthesized) result.results = [synthesized, ...result.results];
|
package/src/orchestrator.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { defaultConfig } from "./config.js";
|
|
4
|
+
import { filterContextToolsForModel, type ContextManagementPolicy } from "./context-policy.js";
|
|
4
5
|
import { createGetPiCommand } from "./launch.js";
|
|
5
6
|
import type { ProcessLockManager } from "./process-lock.js";
|
|
6
7
|
import { ChildRunner, type GetPiCommand } from "./runner.js";
|
|
@@ -30,6 +31,40 @@ export interface OrchestratorDeps {
|
|
|
30
31
|
maxRetries?: number;
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
/** Internal-only capability snapshot; deliberately absent from the stable SDK type. */
|
|
35
|
+
interface ContextOrchestratorDeps {
|
|
36
|
+
/** Remote Context allowlist captured once for the parent dispatch. */
|
|
37
|
+
contextPolicy?: ContextManagementPolicy;
|
|
38
|
+
/** Parent-exposed context-manager tool names in canonical order. */
|
|
39
|
+
parentContextTools?: readonly string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type RunTasksOptions = OrchestratorDeps & { signal?: AbortSignal };
|
|
43
|
+
type InternalRunTasksOptions = RunTasksOptions & ContextOrchestratorDeps;
|
|
44
|
+
|
|
45
|
+
/** Internal projection used by the stable public entry and offline checks. */
|
|
46
|
+
export function stripContextOrchestratorOptions(options: RunTasksOptions): InternalRunTasksOptions {
|
|
47
|
+
return {
|
|
48
|
+
...options,
|
|
49
|
+
contextPolicy: undefined,
|
|
50
|
+
parentContextTools: undefined,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Resolve the actual tool list that one retry attempt will receive. */
|
|
55
|
+
export function toolsForAttempt(
|
|
56
|
+
spec: Pick<TaskSpec, "backend" | "tools">,
|
|
57
|
+
model: string | undefined,
|
|
58
|
+
options: ContextOrchestratorDeps,
|
|
59
|
+
): string[] | undefined {
|
|
60
|
+
return filterContextToolsForModel(spec.tools, {
|
|
61
|
+
backend: spec.backend ?? "pi",
|
|
62
|
+
model,
|
|
63
|
+
policy: options.contextPolicy,
|
|
64
|
+
parentExposed: options.parentContextTools,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
33
68
|
/**
|
|
34
69
|
* Transient failures are infrastructure problems, not task problems: the same
|
|
35
70
|
* spec is safe to retry without duplicating side effects because no meaningful
|
|
@@ -88,7 +123,29 @@ async function writeArtifact(spec: TaskSpec, result: TaskResult): Promise<void>
|
|
|
88
123
|
|
|
89
124
|
export async function runTasks(
|
|
90
125
|
specs: TaskSpec[],
|
|
91
|
-
options:
|
|
126
|
+
options: RunTasksOptions = {},
|
|
127
|
+
): Promise<OrchestratedRun> {
|
|
128
|
+
// Deliberately erase any runtime-only fields supplied by JavaScript callers
|
|
129
|
+
// or `as any` casts. Only the extension-only entry below may carry the
|
|
130
|
+
// operator-owned context snapshot into the attempt loop.
|
|
131
|
+
return runTasksInternal(specs, stripContextOrchestratorOptions(options));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Extension-only orchestration entry that carries the trusted, per-dispatch
|
|
136
|
+
* Remote Context snapshot. It is intentionally not re-exported from src/index.ts;
|
|
137
|
+
* stable SDK callers cannot forge operator-owned context authorization.
|
|
138
|
+
*/
|
|
139
|
+
export async function runTasksWithContextPolicy(
|
|
140
|
+
specs: TaskSpec[],
|
|
141
|
+
options: InternalRunTasksOptions,
|
|
142
|
+
): Promise<OrchestratedRun> {
|
|
143
|
+
return runTasksInternal(specs, options);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function runTasksInternal(
|
|
147
|
+
specs: TaskSpec[],
|
|
148
|
+
options: InternalRunTasksOptions,
|
|
92
149
|
): Promise<OrchestratedRun> {
|
|
93
150
|
const semaphore = options.semaphore ?? new Semaphore(defaultConfig.maxActiveProcesses, defaultConfig.maxQueuedTasks);
|
|
94
151
|
const worktrees = options.worktrees ?? new WorktreeManager();
|
|
@@ -163,7 +220,15 @@ export async function runTasks(
|
|
|
163
220
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
164
221
|
const model = attempt === 1 ? spec.model : (fallbacks[attempt - 2] ?? spec.model);
|
|
165
222
|
if (model) attemptedModels.push(model);
|
|
166
|
-
const attemptSpec: TaskSpec = {
|
|
223
|
+
const attemptSpec: TaskSpec = {
|
|
224
|
+
...spec,
|
|
225
|
+
model,
|
|
226
|
+
// Retry safety: a fallback model may differ in Remote Context
|
|
227
|
+
// eligibility, so context tools are re-derived per attempt from the
|
|
228
|
+
// original validated list. Without a dispatch snapshot this removes
|
|
229
|
+
// them entirely (fail closed) instead of leaking the primary's.
|
|
230
|
+
tools: toolsForAttempt(spec, model, options),
|
|
231
|
+
};
|
|
167
232
|
const runner = new ChildRunner(
|
|
168
233
|
semaphore,
|
|
169
234
|
options.getPiCommand ?? createGetPiCommand(),
|
package/src/policy.ts
CHANGED
|
@@ -9,6 +9,13 @@ import { BACKEND_NAMES, checkCapabilities, type BackendName } from "./backend.js
|
|
|
9
9
|
import { resolveBackend } from "./backends/index.js";
|
|
10
10
|
import { validateModelRequest, type ModelPolicySnapshot } from "./model-policy.js";
|
|
11
11
|
import { isThinkingLevel } from "./thinking.js";
|
|
12
|
+
import {
|
|
13
|
+
CONTEXT_MANAGEMENT_TOOLS,
|
|
14
|
+
EMPTY_CONTEXT_MANAGEMENT_POLICY,
|
|
15
|
+
contextToolsForModel,
|
|
16
|
+
isGatewayContextModel,
|
|
17
|
+
type ContextManagementPolicy,
|
|
18
|
+
} from "./context-policy.js";
|
|
12
19
|
|
|
13
20
|
export const DEPTH_ENV_VAR = "PI_SUBAGENT_DEPTH";
|
|
14
21
|
export const SPAWNS_ENV_VAR = "PI_SUBAGENT_SPAWNS";
|
|
@@ -27,19 +34,8 @@ export const READ_ONLY_TOOLS = new Set([
|
|
|
27
34
|
"web_search",
|
|
28
35
|
"web_fetch",
|
|
29
36
|
]);
|
|
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
37
|
const NON_WRITING_TOOLS = new Set([...READ_ONLY_TOOLS, ...CONTEXT_MANAGEMENT_TOOLS]);
|
|
38
|
+
export { CONTEXT_MANAGEMENT_TOOLS };
|
|
43
39
|
export const KNOWN_WRITE_TOOLS = new Set(["bash", "edit", "write"]);
|
|
44
40
|
/** Backward-compatible export; policy uses fail-closed classification above. */
|
|
45
41
|
export const WRITE_TOOLS = KNOWN_WRITE_TOOLS;
|
|
@@ -89,24 +85,43 @@ function resolveTools(
|
|
|
89
85
|
availableTools: string[],
|
|
90
86
|
activeTools: string[],
|
|
91
87
|
backend: BackendName,
|
|
88
|
+
contextPolicy: ContextManagementPolicy = EMPTY_CONTEXT_MANAGEMENT_POLICY,
|
|
89
|
+
targetModel: string | undefined = undefined,
|
|
92
90
|
): { tools?: string[]; canWrite?: boolean; error?: string } {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
91
|
+
const isPi = backend === "pi";
|
|
92
|
+
// Remote Context eligibility belongs to the exact target model, never to the
|
|
93
|
+
// parent session: a parent that exposes context tools is not authorization
|
|
94
|
+
// for a different child model.
|
|
95
|
+
const eligible = isPi && isGatewayContextModel(contextPolicy, targetModel);
|
|
96
|
+
const contextTools = eligible ? contextToolsForModel(contextPolicy, targetModel, availableTools) : [];
|
|
97
|
+
const nonWritingTools = isPi ? NON_WRITING_TOOLS : READ_ONLY_TOOLS;
|
|
98
|
+
// A non-eligible Pi target must not see context names at all: not inherited
|
|
99
|
+
// from the parent's active tools, not in the read-only default set, and not
|
|
100
|
+
// as a valid explicit request.
|
|
101
|
+
const availableForTarget = new Set(availableTools);
|
|
102
|
+
if (isPi && !eligible) {
|
|
103
|
+
for (const tool of CONTEXT_MANAGEMENT_TOOLS) availableForTarget.delete(tool);
|
|
104
|
+
}
|
|
100
105
|
const addContextTools = (tools: readonly string[]): string[] =>
|
|
101
106
|
[...new Set([...tools, ...contextTools])];
|
|
107
|
+
const dropContextTools = (tools: readonly string[]): string[] => {
|
|
108
|
+
if (!isPi || eligible) return [...tools];
|
|
109
|
+
return tools.filter((tool) => !CONTEXT_MANAGEMENT_TOOLS.has(tool));
|
|
110
|
+
};
|
|
102
111
|
|
|
103
112
|
if (requested) {
|
|
104
|
-
|
|
113
|
+
// An explicit context name on a non-eligible target is unavailable, not
|
|
114
|
+
// silently dropped — the caller gets the existing rejection channel.
|
|
115
|
+
const unknown = requested.filter((tool) => !availableForTarget.has(tool));
|
|
105
116
|
if (unknown.length) return { error: `Unknown or unavailable tools: ${unknown.join(", ")}` };
|
|
106
117
|
}
|
|
107
118
|
|
|
108
119
|
if (profile === "explore" || profile === "review") {
|
|
109
|
-
|
|
120
|
+
// Non-writing defaults come from READ_ONLY_TOOLS; context tools are added
|
|
121
|
+
// back only through the eligibility-gated addContextTools above.
|
|
122
|
+
const source = addContextTools(
|
|
123
|
+
requested ?? [...READ_ONLY_TOOLS].filter((tool) => availableForTarget.has(tool)),
|
|
124
|
+
);
|
|
110
125
|
const unsafe = source.filter((tool) => !nonWritingTools.has(tool));
|
|
111
126
|
if (unsafe.length) {
|
|
112
127
|
return {
|
|
@@ -116,8 +131,8 @@ function resolveTools(
|
|
|
116
131
|
return { tools: source, canWrite: false };
|
|
117
132
|
}
|
|
118
133
|
|
|
119
|
-
const source = addContextTools(requested ?? activeTools);
|
|
120
|
-
const unknown = source.filter((tool) => !
|
|
134
|
+
const source = dropContextTools(addContextTools(requested ?? activeTools));
|
|
135
|
+
const unknown = source.filter((tool) => !availableForTarget.has(tool));
|
|
121
136
|
if (unknown.length) return { error: `Active tools are unavailable: ${unknown.join(", ")}` };
|
|
122
137
|
// General-profile custom tools are conservatively write-capable unless explicitly known non-writing.
|
|
123
138
|
return {
|
|
@@ -158,7 +173,7 @@ function normalizeTask(
|
|
|
158
173
|
index: number,
|
|
159
174
|
parent: ParentContext,
|
|
160
175
|
defaultProfile: TaskProfile,
|
|
161
|
-
defaults: { timeoutMs?: number; taskDefaults?: TaskDefaultsByProfile; agents?: Map<string, AgentDefinition>; modelPolicy?: ModelPolicySnapshot; modelPolicyError?: string } = {},
|
|
176
|
+
defaults: { timeoutMs?: number; taskDefaults?: TaskDefaultsByProfile; agents?: Map<string, AgentDefinition>; modelPolicy?: ModelPolicySnapshot; modelPolicyError?: string; contextPolicy?: ContextManagementPolicy } = {},
|
|
162
177
|
): { task?: ResolvedTask; error?: string } {
|
|
163
178
|
if (!item.task?.trim()) return { error: `Task ${index + 1} must not be blank` };
|
|
164
179
|
|
|
@@ -222,8 +237,20 @@ function normalizeTask(
|
|
|
222
237
|
}
|
|
223
238
|
const profile = item.profile ?? agent?.profile ?? defaultProfile;
|
|
224
239
|
const requestedTools = item.tools ?? agent?.tools;
|
|
225
|
-
const resolved = resolveTools(
|
|
226
|
-
|
|
240
|
+
const resolved = resolveTools(
|
|
241
|
+
profile,
|
|
242
|
+
requestedTools,
|
|
243
|
+
parent.availableTools,
|
|
244
|
+
parent.activeTools ?? parent.availableTools,
|
|
245
|
+
backend,
|
|
246
|
+
defaults.contextPolicy ?? EMPTY_CONTEXT_MANAGEMENT_POLICY,
|
|
247
|
+
// The target model is the policy-routed model; the parent's model is not
|
|
248
|
+
// authorization for a different child.
|
|
249
|
+
modelRoute.model,
|
|
250
|
+
);
|
|
251
|
+
if (resolved.error || !resolved.tools || resolved.canWrite === undefined) {
|
|
252
|
+
return { error: `Task ${index + 1}: ${resolved.error ?? "Tool resolution failed"}` };
|
|
253
|
+
}
|
|
227
254
|
const cwd = resolvePath(parent.cwd, item.cwd);
|
|
228
255
|
const output = item.output ? resolvePath(cwd, item.output) : undefined;
|
|
229
256
|
// Non-model fields retain the existing precedence: explicit request > agent
|
|
@@ -403,6 +430,8 @@ export function validateSubagentRequest(
|
|
|
403
430
|
agents?: Map<string, AgentDefinition>;
|
|
404
431
|
modelPolicy?: ModelPolicySnapshot;
|
|
405
432
|
modelPolicyError?: string;
|
|
433
|
+
/** Remote Context allowlist snapshot; omitted means fail closed (no context tools). */
|
|
434
|
+
contextPolicy?: ContextManagementPolicy;
|
|
406
435
|
} = {},
|
|
407
436
|
): ValidationResult {
|
|
408
437
|
const defaults = {
|
|
@@ -411,6 +440,7 @@ export function validateSubagentRequest(
|
|
|
411
440
|
agents: options.agents,
|
|
412
441
|
modelPolicy: options.modelPolicy,
|
|
413
442
|
modelPolicyError: options.modelPolicyError,
|
|
443
|
+
contextPolicy: options.contextPolicy,
|
|
414
444
|
};
|
|
415
445
|
const depth = parent.depth ?? parseDepth();
|
|
416
446
|
const maxDepth = options.maxDepth ?? defaultConfig.maxDepth;
|
package/src/schema.ts
CHANGED
|
@@ -51,8 +51,8 @@ export const TaskFields = {
|
|
|
51
51
|
system_prompt: Type.Optional(Type.String({ description: "Extra system prompt appended to the child's prompt (does not replace it)." })),
|
|
52
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." })),
|
|
53
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." }),
|
|
54
|
-
tools: Type.Optional(Type.Array(Type.String(), { description: "Optional tool allowlist. explore/review profiles reject project-writing tools
|
|
55
|
-
profile: Type.Optional({ ...Profile, description: "Capability profile: explore/review cannot write project files
|
|
54
|
+
tools: Type.Optional(Type.Array(Type.String(), { description: "Optional tool allowlist. explore/review profiles reject project-writing tools. Pi context-management tools are added only when the target model is an exact `compaction.gatewayContextModels` entry of the Remote Context toolkit config and the parent exposes them; requesting one for any other target is rejected as unavailable." })),
|
|
55
|
+
profile: Type.Optional({ ...Profile, description: "Capability profile: explore/review cannot write project files; Pi context-management tools are retained only for an exact allowlisted Remote Context target and never grant bash/edit/write. general inherits the parent's active tools and may write." }),
|
|
56
56
|
cwd: Type.Optional(Type.String({ description: "Working directory for the child process." })),
|
|
57
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." })),
|
|
58
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." })),
|