@tranhoangnguyen0310/pi-flow-external 5.4.0-external.0 → 5.5.0-external.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +1 -1
- package/docs/orchestration-playbook.md +3 -1
- package/docs/public-contract.md +5 -2
- package/package.json +1 -1
- package/skills/pi-flow-orchestration/SKILL.md +2 -2
- package/skills/pi-flow-orchestration/recipes/bounded-repair.js +5 -1
- package/skills/pi-flow-orchestration/references/runtime-rules.md +1 -1
- package/src/contract/help.ts +104 -7
- package/src/core/capabilities.ts +615 -0
- package/src/core/parent-context.ts +3 -1
- package/src/core/spawn.ts +3 -2
- package/src/core/subagent-render.ts +3 -2
- package/src/external-help.ts +23 -34
- package/src/prompts.ts +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to pi-flow external are documented here.
|
|
4
4
|
|
|
5
|
+
## [5.5.0-external.0] - 2026-10-24
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Read-only harness capability discovery through `external_help({ topic: "capabilities", harness? })` (#79, #95). Omit `harness` for a compact summary or select a built-in CLI or registered `pi-*` harness for details about permissions, structured child answers, resume, budgets, cost reporting, retries, context sharing, and Pi resources.
|
|
9
|
+
- Capability records distinguish configured/enabled state from `readiness: "not_probed"`, and Flow's structured run receipts from support for schema-constrained child answers. Discovery launches no processes, reads no credentials, calls no providers, and writes no files.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Permission help is generated from shared per-tier facts, including each Pi tier's exact tools. Budget-support disclosures use shared backend metadata. Existing execution permissions and budget enforcement are unchanged.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- Capability discovery respects valid OpenCode role overrides instead of marking the whole harness invalid when a binding supplies a compatible model or effort setting.
|
|
16
|
+
- Claude permission help uses the current effective UID and correctly reports `--permission-mode auto` under root.
|
|
17
|
+
- Active in-process Pi child rows show `Pi SDK child · host access` instead of `external host access`.
|
|
18
|
+
|
|
19
|
+
### Release
|
|
20
|
+
- Publishes the capability changes merged in #95 under a fresh version. That merge retained the already-published `5.4.1-external.0`, so its release workflow reused the existing tag and skipped publishing; this release includes those changes without rewriting any published version or tag.
|
|
21
|
+
|
|
22
|
+
## [5.4.1-external.0] - 2026-10-23
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- Bounded-repair now rejects non-Codex harnesses before starting any editor. Supporting edit and readonly permissions alone does not provide the shell access and structured output required by QA.
|
|
26
|
+
- Repair guidance now states the Codex-only limit and requires checks compatible with its read-only sandbox. Regression tests cover rejection before edits and default/explicit Codex execution.
|
|
27
|
+
|
|
5
28
|
## [5.4.0-external.0] - 2026-10-23
|
|
6
29
|
|
|
7
30
|
### Added
|
package/README.md
CHANGED
|
@@ -229,5 +229,5 @@ The direct delegation tool `Agent` is now `external_delegate`, with no alias. Up
|
|
|
229
229
|
- **[Workflows](docs/workflows.md):** `external_workflow`, plain-language plans, live phases, error handling (`ChildRunError`), and replay cache.
|
|
230
230
|
- **[Orchestration Playbook](docs/orchestration-playbook.md):** When to choose patterns, how to invoke the bundled skill, and tested example recipes.
|
|
231
231
|
- **[Workflow Tool Rename](docs/migration-workflow-to-external-workflow.md):** `workflow` → `external_workflow`, with no alias.
|
|
232
|
-
- **[Public Contract](docs/public-contract.md):** Contract envelope definitions (`external_delegate`, `external_workflow`, `external_runs`, `external_help`), `external_help({ topic: "contracts"
|
|
232
|
+
- **[Public Contract](docs/public-contract.md):** Contract envelope definitions (`external_delegate`, `external_workflow`, `external_runs`, `external_help`), `external_help({ topic: "contracts" | "capabilities" })` discovery, and explicit context-handoff guidance for Pi codemode consumers.
|
|
233
233
|
- **[Troubleshooting](docs/troubleshooting.md):** Common errors, authentication checks, and `pi-cc-extensions` renderer compatibility.
|
|
@@ -44,7 +44,9 @@ Example for parallel-sweep.js:
|
|
|
44
44
|
|
|
45
45
|
Before launch the parent should explain the split, selected harnesses, permissions, which checks run in parallel, and what evidence will come back. Foreground is the default when the result is needed next.
|
|
46
46
|
|
|
47
|
-
The example pins Codex because it supports the explicit readonly tier
|
|
47
|
+
The example pins Codex because it supports the explicit readonly tier; **it does not automatically switch backends**. For the read-only recipes, set harness to a verified available backend that enforces the required tier. Antigravity supports only danger, so these restricted examples do not work on it.
|
|
48
|
+
|
|
49
|
+
**Bounded repair is Codex-only.** It rejects other harnesses before any editor starts. Supporting edit and readonly tiers is not enough: QA must also have shell access and structured output. OpenCode rejects the required schema, while Claude, Muse, and named Pi harnesses cannot shell-run tests at readonly. Before launch, confirm Codex is available and the checks can run in its read-only sandbox; checks that need workspace writes or network access may fail. Do not silently raise permissions to run them.
|
|
48
50
|
|
|
49
51
|
## What good behavior looks like
|
|
50
52
|
|
package/docs/public-contract.md
CHANGED
|
@@ -25,7 +25,7 @@ Every result carries:
|
|
|
25
25
|
| `external_delegate` | `delegate` | `{run}` | `{run}`, or `{run: null}` before registration |
|
|
26
26
|
| `external_workflow` | `run` | `{run}` | `{run}`, or `{run: null}` before registration |
|
|
27
27
|
| `external_runs` | the requested action | the action's payload (below) | `null` |
|
|
28
|
-
| `external_help` | `help` | `{topic, harness?, tool?, text}
|
|
28
|
+
| `external_help` | `help` | `{topic, harness?, tool?, text}`, `{topic: "contracts", index \| tool, contract}`, or `{topic: "capabilities", harness?, harnesses}` | `null` |
|
|
29
29
|
|
|
30
30
|
A failure that has a run keeps that run's evidence. Read-only supervision and help never return partial data on failure.
|
|
31
31
|
|
|
@@ -117,9 +117,12 @@ A host redaction hook that replaces content drops structured content unless it e
|
|
|
117
117
|
|
|
118
118
|
Pi generates TypeScript input and discriminated output declarations from the registered schemas through native `searchTools` and `describeTool`. For `external_delegate`, each `anyOf` selector branch (`role` vs `subagent_type`) repeats the shared property definitions and required fields so `describeTool("external_delegate")` renders both branches; JSON Schema `not` exclusions (`role` + `subagent_type` and `subagent_type` + `harness`) are enforced by `validateToolArguments` even though Pi's TypeScript renderer omits `not`.
|
|
119
119
|
|
|
120
|
-
For Flow-specific semantics, raw JSON schemas, validated example payloads,
|
|
120
|
+
For Flow-specific semantics, raw JSON schemas, validated example payloads, workflow helper signatures, and per-harness capabilities, call `external_help`:
|
|
121
121
|
- `external_help({ topic: "contracts" })` returns a compact index (≤ 1.5 KiB) listing `external_delegate`, `external_workflow`, `external_runs`, and `external_help`, whether each is registered (`false` for `external_workflow` when disabled), core contract rules, and a pointer to `describeTool`.
|
|
122
122
|
- `external_help({ topic: "contracts", tool: "external_delegate" })` (or `"external_workflow"`, `"external_runs"`, `"external_help"`) returns `{ tool, contractVersion, registered, actions, purpose, input, output, semantics, examples, helpers? }`, where `input` and `output` come directly from the executable tool definitions and `helpers` on `external_workflow` exposes `WORKFLOW_HELPERS` (`functions`, `globals`, `agentOptions`).
|
|
123
|
+
- `external_help({ topic: "capabilities", harness? })` returns `{ topic: "capabilities", harness?, harnesses: CapabilityRecord[] }` without spawning processes, reading credentials, calling providers, or writing files. Omitting `harness` renders a compact one-line-per-harness summary in `content` (`enabled`, supported `tiers`, `workflowChildSchema`, `resume`, `budget`, `cost`) across all selectable harnesses (or the six built-in harnesses with `configuration.state: "invalid"` and `diagnostics` when settings are blocked, without guessing registered `pi-*` names). Passing `harness` renders the full capability record for that harness: `configuration` (`built_in | registered | invalid`, `enabled`, `diagnostics?`, `model?`, `thinking?`, `preset?`), `readiness: "not_probed"` (configuration does not prove installation or authentication; use `/external config harness test` or `/external doctor`), `workflowChildSchema` (schema-constrained workflow `agent({ schema })` task output, distinct from Flow's operational `PublicRun` receipts which every harness returns), `resume`, `budget` (`"enforced" | "not_enforced"`), `cost` (`"native" | "estimated" | "partial" | "unknown"`), `retries`, `nestedAgentDetection`, `permissions` (`default`, per-tier `supported`/`enforced`/`caveat?`/`tools?`, and backend `caveats`), `context` (`modes`, `maxBytes`, `resumeConflict`), and `resources` (for `pi-*` harnesses, reflecting `preset` and `ctx.isProjectTrusted()`).
|
|
124
|
+
|
|
125
|
+
Capability configuration describes the harness and its defaults, not every role binding. Role-specific model and effort overrides are validated after resolution; a binding error does not disable the entire harness.
|
|
123
126
|
|
|
124
127
|
## Explicit context handoff
|
|
125
128
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tranhoangnguyen0310/pi-flow-external",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.5.0-external.0",
|
|
4
4
|
"description": "External Claude Code, Codex CLI, Antigravity, Grok Build CLI, Muse Code, and OpenCode delegation for pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -34,9 +34,9 @@ Read [pattern-selection.md](references/pattern-selection.md) for failure modes a
|
|
|
34
34
|
- [parallel-sweep.js](recipes/parallel-sweep.js) — independent scopes, partial failures kept.
|
|
35
35
|
- [branch-and-converge.js](recipes/branch-and-converge.js) — parallel discovery with explicit evidence handoff into a final synthesis.
|
|
36
36
|
- [adversarial-review.js](recipes/adversarial-review.js) — draft and independent critique (not a self-review).
|
|
37
|
-
- [bounded-repair.js](recipes/bounded-repair.js) — authorized edits, structured checks, limited iterations.
|
|
37
|
+
- [bounded-repair.js](recipes/bounded-repair.js) — Codex-only authorized edits, structured checks, limited iterations. Other harnesses are rejected before any child starts.
|
|
38
38
|
|
|
39
|
-
These are adaptable **examples**, not new runtime functions. Copy a recipe into a trusted saved workflow root or pass its text as the script to external_workflow, and pass JSON inputs through the tool's args. A scriptPath must be inside an allowed workflow root; a package skill directory is not automatically such a root. Recipes assume Codex as a restricted-permission demonstration backend, not an automatic fallback.
|
|
39
|
+
These are adaptable **examples**, not new runtime functions. Copy a recipe into a trusted saved workflow root or pass its text as the script to external_workflow, and pass JSON inputs through the tool's args. A scriptPath must be inside an allowed workflow root; a package skill directory is not automatically such a root. Recipes assume Codex as a restricted-permission demonstration backend, not an automatic fallback. For the read-only recipes, set harness inputs to an available backend that enforces the required tier. Bounded repair requires Codex: QA needs both shell access at readonly and structured output. Before launch, confirm Codex is available and the specified checks can run in its read-only sandbox; checks that need workspace writes or network access may fail.
|
|
40
40
|
|
|
41
41
|
## Flow rules that prevent common failures
|
|
42
42
|
|
|
@@ -18,7 +18,11 @@ if (typeof input.checks !== "string" || !input.checks.trim()) throw new Error("a
|
|
|
18
18
|
if (!Number.isInteger(input.maxAttempts) || input.maxAttempts < 1 || input.maxAttempts > 3) {
|
|
19
19
|
throw new Error("args.maxAttempts must be an authorized number in 1-3");
|
|
20
20
|
}
|
|
21
|
-
|
|
21
|
+
// shortcut: Codex-only QA; expand after verifying shell access and structured output at readonly.
|
|
22
|
+
const harness = input.harness === undefined ? "codex" : input.harness;
|
|
23
|
+
if (harness !== "codex") {
|
|
24
|
+
throw new Error('bounded-repair requires harness "codex" for shell checks and structured QA; no agents started');
|
|
25
|
+
}
|
|
22
26
|
const attempts = [];
|
|
23
27
|
let feedback = "";
|
|
24
28
|
for (let attempt = 1; attempt <= input.maxAttempts; attempt++) {
|
|
@@ -19,7 +19,7 @@ Use the installed version's external_help({topic:"workflow"}) and external_help(
|
|
|
19
19
|
- Claude readonly and edit have native tool restrictions, not kernel isolation; headless shell is denied at those tiers. Codex supports read-only and workspace-write OS sandboxes. Do not promise the same security guarantee across backends.
|
|
20
20
|
- For inspection-only work, prefer a harness that can enforce readonly. A prompt saying "don't edit" alone is insufficient.
|
|
21
21
|
- Do not execute parallel edits in the same working tree unless the parent has verified disjoint ownership and no shared-file changes. No built-in worktree isolation or merge arbitration exists in Flow.
|
|
22
|
-
- A repair recipe needs the user to authorize edits and the expected fix/test cycle. Do not auto-retry failed children or expand permission without authorization.
|
|
22
|
+
- A repair recipe needs the user to authorize edits and the expected fix/test cycle. The bundled bounded-repair recipe accepts only Codex and rejects other harnesses before starting children: QA requires shell access at readonly plus structured output, not just support for the permission tiers. Confirm the checks can run in Codex's read-only sandbox before editing. Do not auto-retry failed children or expand permission without authorization.
|
|
23
23
|
|
|
24
24
|
## Context and handoff
|
|
25
25
|
|
package/src/contract/help.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
2
|
import { Type, type Static, type TSchema } from "typebox";
|
|
3
|
+
import { buildCapabilityRecord } from "../core/capabilities.ts";
|
|
3
4
|
import { EXTERNAL_DELEGATE_TOOL, EXTERNAL_WORKFLOW_TOOL } from "../tool-names.ts";
|
|
4
5
|
import { USAGE_ONE_AGENT, usageWorkflowScript } from "../prompts.ts";
|
|
5
6
|
import { workflowToolParameters } from "../workflow/source.ts";
|
|
@@ -9,7 +10,7 @@ import { EXTERNAL_RUNS_ACTION_SPECS, EXTERNAL_RUNS_EXAMPLES, externalRunsOutputS
|
|
|
9
10
|
import { formatWorkflowHelpersLine, HANDLED_CHILD_FAILURES, WORKFLOW_HELPERS, workflowOutputSchema } from "./workflow.ts";
|
|
10
11
|
|
|
11
12
|
export const EXTERNAL_HELP_TOOL = "external_help" as const;
|
|
12
|
-
export const EXTERNAL_HELP_TOPICS = ["usage", "roles", "permissions", "workflow", "contracts"] as const;
|
|
13
|
+
export const EXTERNAL_HELP_TOPICS = ["usage", "roles", "permissions", "workflow", "contracts", "capabilities"] as const;
|
|
13
14
|
export const PUBLIC_TOOL_NAMES = [
|
|
14
15
|
EXTERNAL_DELEGATE_TOOL,
|
|
15
16
|
EXTERNAL_WORKFLOW_TOOL,
|
|
@@ -25,11 +26,11 @@ export const CONTRACT_INDEX_MAX_BYTES = 1536;
|
|
|
25
26
|
export const externalHelpParameters = Type.Object({
|
|
26
27
|
topic: StringEnum(EXTERNAL_HELP_TOPICS, {
|
|
27
28
|
description:
|
|
28
|
-
"Help topic: usage playbook, role descriptions/configured profile availability, harness permissions, workflow syntax and saved workflows,
|
|
29
|
+
"Help topic: usage playbook, role descriptions/configured profile availability, harness permissions, workflow syntax and saved workflows, public tool contracts, or per-harness capabilities.",
|
|
29
30
|
}),
|
|
30
31
|
harness: Type.Optional(Type.String({
|
|
31
32
|
description:
|
|
32
|
-
"Optional harness filter for roles or
|
|
33
|
+
"Optional harness filter for roles, permissions, or capabilities: agy, claude, codex, grok, muse, opencode, or a registered pi-* harness. Omit on capabilities for the compact index. usage, workflow, and contracts describe every harness.",
|
|
33
34
|
})),
|
|
34
35
|
tool: Type.Optional(Type.String({
|
|
35
36
|
description:
|
|
@@ -112,10 +113,81 @@ const contractsTopicDataSchema = Type.Union([
|
|
|
112
113
|
}),
|
|
113
114
|
]);
|
|
114
115
|
|
|
116
|
+
export const permissionTierCapabilitySchema = Type.Object({
|
|
117
|
+
supported: Type.Boolean(),
|
|
118
|
+
enforced: Type.Boolean(),
|
|
119
|
+
caveat: Type.Optional(Type.String()),
|
|
120
|
+
tools: Type.Optional(Type.Array(Type.String())),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
export const capabilityRecordSchema = Type.Object({
|
|
124
|
+
harness: Type.String(),
|
|
125
|
+
backend: strings("agy", "claude", "codex", "grok", "muse", "opencode", "pi"),
|
|
126
|
+
boundary: strings("external_cli", "pi_sdk"),
|
|
127
|
+
configuration: Type.Object({
|
|
128
|
+
state: strings("built_in", "registered", "invalid"),
|
|
129
|
+
enabled: Type.Boolean(),
|
|
130
|
+
diagnostics: Type.Optional(Type.Array(Type.String())),
|
|
131
|
+
model: Type.Optional(Type.String()),
|
|
132
|
+
thinking: Type.Optional(Type.String()),
|
|
133
|
+
preset: Type.Optional(strings("minimal", "skills")),
|
|
134
|
+
}),
|
|
135
|
+
readiness: Type.Literal("not_probed"),
|
|
136
|
+
workflowChildSchema: Type.Object({
|
|
137
|
+
support: strings("supported", "unsupported"),
|
|
138
|
+
mechanism: strings("cli_flag", "schema_file", "injected_tool", "none"),
|
|
139
|
+
}),
|
|
140
|
+
resume: Type.Object({
|
|
141
|
+
support: strings("supported", "unsupported"),
|
|
142
|
+
mechanism: Type.Optional(Type.String()),
|
|
143
|
+
conditions: Type.Array(Type.String()),
|
|
144
|
+
}),
|
|
145
|
+
budget: strings("enforced", "not_enforced"),
|
|
146
|
+
cost: Type.Object({
|
|
147
|
+
kind: strings("native", "estimated", "partial", "unknown"),
|
|
148
|
+
conditions: Type.Array(Type.String()),
|
|
149
|
+
}),
|
|
150
|
+
retries: Type.Object({
|
|
151
|
+
extension: strings("none", "once_on_infrastructure"),
|
|
152
|
+
native: strings("none", "internal", "unknown"),
|
|
153
|
+
}),
|
|
154
|
+
nestedAgentDetection: Type.Boolean(),
|
|
155
|
+
permissions: Type.Object({
|
|
156
|
+
default: strings("readonly", "edit", "danger"),
|
|
157
|
+
tiers: Type.Object({
|
|
158
|
+
readonly: permissionTierCapabilitySchema,
|
|
159
|
+
edit: permissionTierCapabilitySchema,
|
|
160
|
+
danger: permissionTierCapabilitySchema,
|
|
161
|
+
}),
|
|
162
|
+
caveats: Type.Array(Type.String()),
|
|
163
|
+
}),
|
|
164
|
+
context: Type.Object({
|
|
165
|
+
modes: Type.Tuple([Type.Literal("none"), Type.Literal("recent"), Type.Literal("full")]),
|
|
166
|
+
maxBytes: Type.Integer({ minimum: 1 }),
|
|
167
|
+
resumeConflict: Type.Literal(true),
|
|
168
|
+
}),
|
|
169
|
+
resources: Type.Optional(
|
|
170
|
+
Type.Object({
|
|
171
|
+
skills: strings("none", "user", "user_and_project"),
|
|
172
|
+
extensions: Type.Literal("none"),
|
|
173
|
+
promptTemplates: Type.Literal("none"),
|
|
174
|
+
themes: Type.Literal("none"),
|
|
175
|
+
}),
|
|
176
|
+
),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
export type CapabilityRecordSchema = Static<typeof capabilityRecordSchema>;
|
|
180
|
+
|
|
181
|
+
const capabilitiesTopicDataSchema = Type.Object({
|
|
182
|
+
topic: Type.Literal("capabilities"),
|
|
183
|
+
harness: Type.Optional(Type.String()),
|
|
184
|
+
harnesses: Type.Array(capabilityRecordSchema),
|
|
185
|
+
});
|
|
186
|
+
|
|
115
187
|
export const externalHelpOutputSchema = contractEnvelope(
|
|
116
188
|
EXTERNAL_HELP_TOOL,
|
|
117
189
|
Type.Literal("help"),
|
|
118
|
-
Type.Union([proseTopicDataSchema, contractsTopicDataSchema]),
|
|
190
|
+
Type.Union([proseTopicDataSchema, contractsTopicDataSchema, capabilitiesTopicDataSchema]),
|
|
119
191
|
Type.Null(),
|
|
120
192
|
);
|
|
121
193
|
|
|
@@ -372,14 +444,15 @@ export const PUBLIC_TOOLS: Record<PublicToolName, PublicToolSpec> = {
|
|
|
372
444
|
tool: EXTERNAL_HELP_TOOL,
|
|
373
445
|
contractVersion: 1,
|
|
374
446
|
actions: ["help"],
|
|
375
|
-
purpose: "Read-only help for usage, roles, permissions, workflows, and
|
|
447
|
+
purpose: "Read-only help for usage, roles, permissions, workflows, contracts, and capabilities.",
|
|
376
448
|
input: externalHelpParameters,
|
|
377
449
|
output: externalHelpOutputSchema,
|
|
378
450
|
semantics: [
|
|
379
451
|
"Read-only discovery: spawns no CLIs, reads no credentials, calls no providers, and writes no files.",
|
|
380
452
|
"Blank/whitespace harness and tool placeholders normalize to omitted (#62).",
|
|
381
|
-
"Unknown harness on roles/permissions or unknown tool on contracts returns ok:false with code 'selection_invalid' and data:null.",
|
|
382
|
-
"Prose topics (usage, roles, permissions, workflow) return data:{topic, harness?, text}; contracts returns data:{topic:'contracts', index} or data:{topic:'contracts', tool, contract}.",
|
|
453
|
+
"Unknown harness on roles/permissions/capabilities or unknown tool on contracts returns ok:false with code 'selection_invalid' and data:null.",
|
|
454
|
+
"Prose topics (usage, roles, permissions, workflow) return data:{topic, harness?, text}; contracts returns data:{topic:'contracts', index} or data:{topic:'contracts', tool, contract}; capabilities returns data:{topic:'capabilities', harness?, harnesses}.",
|
|
455
|
+
"Capability readiness is always 'not_probed' (configuration does not prove installation or authentication); every harness returns PublicRun receipts while workflowChildSchema describes workflow agent({ schema }) support.",
|
|
383
456
|
],
|
|
384
457
|
examples: [
|
|
385
458
|
{
|
|
@@ -411,6 +484,30 @@ export const PUBLIC_TOOLS: Record<PublicToolName, PublicToolSpec> = {
|
|
|
411
484
|
warnings: [],
|
|
412
485
|
},
|
|
413
486
|
},
|
|
487
|
+
{
|
|
488
|
+
label: "capabilities_detail",
|
|
489
|
+
input: { topic: "capabilities", harness: "claude" },
|
|
490
|
+
output: {
|
|
491
|
+
contractVersion: 1,
|
|
492
|
+
tool: EXTERNAL_HELP_TOOL,
|
|
493
|
+
action: "help",
|
|
494
|
+
observedAt: EXAMPLE_OBSERVED_AT,
|
|
495
|
+
ok: true,
|
|
496
|
+
data: {
|
|
497
|
+
topic: "capabilities",
|
|
498
|
+
harness: "claude",
|
|
499
|
+
harnesses: [
|
|
500
|
+
buildCapabilityRecord({
|
|
501
|
+
harness: "claude",
|
|
502
|
+
backend: "claude",
|
|
503
|
+
configuration: { state: "built_in", enabled: true },
|
|
504
|
+
options: { effectiveUid: 501, platform: "linux", defaultPermission: "danger" },
|
|
505
|
+
}),
|
|
506
|
+
],
|
|
507
|
+
},
|
|
508
|
+
warnings: [],
|
|
509
|
+
},
|
|
510
|
+
},
|
|
414
511
|
{
|
|
415
512
|
label: "unknown_tool_selector",
|
|
416
513
|
input: { topic: "contracts", tool: "unknown_tool" },
|
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
import { effectivePiResourcePreset } from "../harnesses.ts";
|
|
2
|
+
import { loadExternalCatalog } from "../profiles.ts";
|
|
3
|
+
import { loadExternalSettings } from "../settings.ts";
|
|
4
|
+
import {
|
|
5
|
+
EXTERNAL_HARNESSES,
|
|
6
|
+
type ExternalHarness,
|
|
7
|
+
type PermissionTier,
|
|
8
|
+
type PiResourcePreset,
|
|
9
|
+
type SubagentBackend,
|
|
10
|
+
} from "../types.ts";
|
|
11
|
+
import { ExpectedFlowError } from "./errors.ts";
|
|
12
|
+
import { PARENT_CONTEXT_MAX_BYTES } from "./parent-context.ts";
|
|
13
|
+
import {
|
|
14
|
+
buildPermissionArgs,
|
|
15
|
+
PI_TIER_ACTIVE_TOOLS,
|
|
16
|
+
resolvePermission,
|
|
17
|
+
unsupportedPermissionReason,
|
|
18
|
+
} from "./permissions.ts";
|
|
19
|
+
|
|
20
|
+
export interface BackendCapabilityDescriptor {
|
|
21
|
+
boundary: "external_cli" | "pi_sdk";
|
|
22
|
+
workflowChildSchema: {
|
|
23
|
+
support: "supported" | "unsupported";
|
|
24
|
+
mechanism: "cli_flag" | "schema_file" | "injected_tool" | "none";
|
|
25
|
+
};
|
|
26
|
+
resume: {
|
|
27
|
+
support: "supported" | "unsupported";
|
|
28
|
+
mechanism?: string;
|
|
29
|
+
conditions: readonly string[];
|
|
30
|
+
};
|
|
31
|
+
budget: "enforced" | "not_enforced";
|
|
32
|
+
cost: {
|
|
33
|
+
kind: "native" | "estimated" | "partial" | "unknown";
|
|
34
|
+
conditions: readonly string[];
|
|
35
|
+
};
|
|
36
|
+
retries: {
|
|
37
|
+
extension: "none" | "once_on_infrastructure";
|
|
38
|
+
native: "none" | "internal" | "unknown";
|
|
39
|
+
};
|
|
40
|
+
nestedAgentDetection: boolean;
|
|
41
|
+
/** Backend-level permission caveats that no per-tier helper computes. */
|
|
42
|
+
permissionCaveats: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const PERMISSION_TIERS: readonly PermissionTier[] = ["readonly", "edit", "danger"];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Single metadata source for backend capability facts that no existing helper
|
|
49
|
+
* computes. Per-tier support/enforcement/caveats and Pi tool lists are derived
|
|
50
|
+
* from `src/core/permissions.ts`, and the context byte ceiling is imported from
|
|
51
|
+
* `src/core/parent-context.ts`. Conformance tests in `test/capabilities.test.ts`
|
|
52
|
+
* verify every field here against the actual adapter implementations.
|
|
53
|
+
*/
|
|
54
|
+
export const BACKEND_CAPABILITIES: Readonly<Record<SubagentBackend, BackendCapabilityDescriptor>> = {
|
|
55
|
+
claude: {
|
|
56
|
+
boundary: "external_cli",
|
|
57
|
+
workflowChildSchema: { support: "supported", mechanism: "cli_flag" },
|
|
58
|
+
resume: {
|
|
59
|
+
support: "supported",
|
|
60
|
+
mechanism: "--resume",
|
|
61
|
+
conditions: [
|
|
62
|
+
"Requires a completed claude run record with a sessionId.",
|
|
63
|
+
"Cannot be combined with context sharing (recent/full).",
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
budget: "enforced",
|
|
67
|
+
cost: {
|
|
68
|
+
kind: "native",
|
|
69
|
+
conditions: [
|
|
70
|
+
"Reported natively by Claude Code (total_cost_usd or modelUsage costUSD); costKnown is false when absent.",
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
retries: { extension: "none", native: "unknown" },
|
|
74
|
+
nestedAgentDetection: true,
|
|
75
|
+
permissionCaveats: [
|
|
76
|
+
"danger uses --dangerously-skip-permissions (--permission-mode auto under effective UID 0).",
|
|
77
|
+
"The requested tier is used as-is; role names do not raise it.",
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
codex: {
|
|
81
|
+
boundary: "external_cli",
|
|
82
|
+
workflowChildSchema: { support: "supported", mechanism: "schema_file" },
|
|
83
|
+
resume: {
|
|
84
|
+
support: "supported",
|
|
85
|
+
mechanism: "exec resume",
|
|
86
|
+
conditions: [
|
|
87
|
+
"Requires a completed codex run record with a sessionId.",
|
|
88
|
+
"Cannot be combined with context sharing (recent/full).",
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
budget: "not_enforced",
|
|
92
|
+
cost: {
|
|
93
|
+
kind: "estimated",
|
|
94
|
+
conditions: [
|
|
95
|
+
"Estimated from local price table for mapped models (gpt-5.5, gpt-5.4, gpt-5.4-mini) with costEstimated: true; costKnown is false when the model is unmapped.",
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
retries: { extension: "none", native: "unknown" },
|
|
99
|
+
nestedAgentDetection: true,
|
|
100
|
+
permissionCaveats: [
|
|
101
|
+
"Sandbox flags govern model-generated shell commands, not MCP, plugins, or hooks.",
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
agy: {
|
|
105
|
+
boundary: "external_cli",
|
|
106
|
+
workflowChildSchema: { support: "supported", mechanism: "cli_flag" },
|
|
107
|
+
resume: {
|
|
108
|
+
support: "supported",
|
|
109
|
+
mechanism: "--conversation",
|
|
110
|
+
conditions: [
|
|
111
|
+
"Requires a completed agy run record with a sessionId.",
|
|
112
|
+
"Cannot be combined with context sharing (recent/full).",
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
budget: "not_enforced",
|
|
116
|
+
cost: {
|
|
117
|
+
kind: "unknown",
|
|
118
|
+
conditions: [
|
|
119
|
+
"Antigravity reports token counts only; costKnown is always false.",
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
retries: { extension: "once_on_infrastructure", native: "unknown" },
|
|
123
|
+
nestedAgentDetection: true,
|
|
124
|
+
permissionCaveats: [
|
|
125
|
+
"Autonomous only: every accepted run uses --dangerously-skip-permissions and is unsandboxed.",
|
|
126
|
+
"readonly and edit are rejected instead of being broadened to danger.",
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
grok: {
|
|
130
|
+
boundary: "external_cli",
|
|
131
|
+
workflowChildSchema: { support: "supported", mechanism: "cli_flag" },
|
|
132
|
+
resume: {
|
|
133
|
+
support: "supported",
|
|
134
|
+
mechanism: "--resume",
|
|
135
|
+
conditions: [
|
|
136
|
+
"Requires a completed grok run record with a sessionId.",
|
|
137
|
+
"Cannot be combined with context sharing (recent/full).",
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
budget: "not_enforced",
|
|
141
|
+
cost: {
|
|
142
|
+
kind: "native",
|
|
143
|
+
conditions: [
|
|
144
|
+
"Reported natively by Grok CLI (total_cost_usd or modelUsage costUSD); costKnown is false when absent.",
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
retries: { extension: "none", native: "unknown" },
|
|
148
|
+
nestedAgentDetection: true,
|
|
149
|
+
permissionCaveats: [
|
|
150
|
+
"Enforced by a kernel sandbox alongside --permission-mode bypassPermissions, but readonly network blocking is Linux-only.",
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
muse: {
|
|
154
|
+
boundary: "external_cli",
|
|
155
|
+
workflowChildSchema: { support: "supported", mechanism: "schema_file" },
|
|
156
|
+
resume: {
|
|
157
|
+
support: "supported",
|
|
158
|
+
mechanism: "--session-id",
|
|
159
|
+
conditions: [
|
|
160
|
+
"Requires a completed muse run record with a sessionId.",
|
|
161
|
+
"Cannot be combined with context sharing (recent/full).",
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
budget: "not_enforced",
|
|
165
|
+
cost: {
|
|
166
|
+
kind: "unknown",
|
|
167
|
+
conditions: [
|
|
168
|
+
"Muse has not been observed to report token usage or cost; costKnown is always false.",
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
retries: { extension: "none", native: "internal" },
|
|
172
|
+
nestedAgentDetection: false,
|
|
173
|
+
permissionCaveats: [
|
|
174
|
+
"danger (--yolo) disables approval and the sandbox and additionally trusts this workspace (loads its skills/rules) for this run — a broader grant than an unsandboxed run alone.",
|
|
175
|
+
"Native provider retries (up to 10 attempts) are disclosed via activity narration, not performed by this extension.",
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
opencode: {
|
|
179
|
+
boundary: "external_cli",
|
|
180
|
+
workflowChildSchema: { support: "unsupported", mechanism: "none" },
|
|
181
|
+
resume: {
|
|
182
|
+
support: "supported",
|
|
183
|
+
mechanism: "--session",
|
|
184
|
+
conditions: [
|
|
185
|
+
"Requires a completed opencode run record with a sessionId.",
|
|
186
|
+
"Refused at readonly/edit when the saved session carries its own permission rules.",
|
|
187
|
+
"Refused at danger when the session was last run by an injected restricted agent.",
|
|
188
|
+
"Cannot be combined with context sharing (recent/full).",
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
budget: "not_enforced",
|
|
192
|
+
cost: {
|
|
193
|
+
kind: "partial",
|
|
194
|
+
conditions: [
|
|
195
|
+
"Summed from exported turn assistant messages; costKnown is false when any message lacks usage/cost or ran subagent.",
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
retries: { extension: "none", native: "internal" },
|
|
199
|
+
nestedAgentDetection: true,
|
|
200
|
+
permissionCaveats: [
|
|
201
|
+
"Every run is opencode run --standalone, a private server this run owns, never the shared background service.",
|
|
202
|
+
"readonly and edit run under a per-run agent injected through OPENCODE_CONFIG_CONTENT and selected with --agent on every run, resumes included; shell, subagent, web, MCP, and plugin tools are hidden, and edits outside the workspace are denied.",
|
|
203
|
+
"Resuming a session with its own permission rules at readonly/edit, or a restricted session at danger, is refused.",
|
|
204
|
+
"These are OpenCode application rules, not an OS sandbox, and project or user plugins and MCP servers still load and run their code.",
|
|
205
|
+
"Existing OPENCODE_CONFIG_CONTENT in the environment blocks restricted (readonly/edit) launches.",
|
|
206
|
+
"danger uses --auto, which approves every permission ask except explicit denies.",
|
|
207
|
+
"OpenCode retries transient provider errors itself (up to 10 retries); this extension does not retry.",
|
|
208
|
+
],
|
|
209
|
+
},
|
|
210
|
+
pi: {
|
|
211
|
+
boundary: "pi_sdk",
|
|
212
|
+
workflowChildSchema: { support: "supported", mechanism: "injected_tool" },
|
|
213
|
+
resume: {
|
|
214
|
+
support: "unsupported",
|
|
215
|
+
conditions: [
|
|
216
|
+
"Pi children use an in-memory session and store no resumable sessionId.",
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
budget: "not_enforced",
|
|
220
|
+
cost: {
|
|
221
|
+
kind: "native",
|
|
222
|
+
conditions: [
|
|
223
|
+
"Reported from Pi SDK session usage stats.",
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
retries: { extension: "none", native: "none" },
|
|
227
|
+
nestedAgentDetection: false,
|
|
228
|
+
permissionCaveats: [
|
|
229
|
+
"Run in-process via the Pi SDK, not as a CLI.",
|
|
230
|
+
"That curated built-in tool list is not an OS sandbox; danger-tier bash has host access.",
|
|
231
|
+
"Each registration selects preset minimal (the default when the field is absent; skills stay unloaded) or skills (installed skills load; project skills load only when the project is trusted). Extensions, prompt templates, and themes stay unloaded.",
|
|
232
|
+
"Retry is disabled per child regardless of Pi's own settings.",
|
|
233
|
+
],
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export interface PermissionTierCapability {
|
|
238
|
+
supported: boolean;
|
|
239
|
+
enforced: boolean;
|
|
240
|
+
caveat?: string;
|
|
241
|
+
tools?: string[];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export interface CapabilityConfiguration {
|
|
245
|
+
state: "built_in" | "registered" | "invalid";
|
|
246
|
+
enabled: boolean;
|
|
247
|
+
diagnostics?: string[];
|
|
248
|
+
model?: string;
|
|
249
|
+
thinking?: string;
|
|
250
|
+
preset?: PiResourcePreset;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface PiCapabilityResources {
|
|
254
|
+
skills: "none" | "user" | "user_and_project";
|
|
255
|
+
extensions: "none";
|
|
256
|
+
promptTemplates: "none";
|
|
257
|
+
themes: "none";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export interface CapabilityRecord {
|
|
261
|
+
harness: string;
|
|
262
|
+
backend: SubagentBackend;
|
|
263
|
+
boundary: "external_cli" | "pi_sdk";
|
|
264
|
+
configuration: CapabilityConfiguration;
|
|
265
|
+
readiness: "not_probed";
|
|
266
|
+
workflowChildSchema: {
|
|
267
|
+
support: "supported" | "unsupported";
|
|
268
|
+
mechanism: "cli_flag" | "schema_file" | "injected_tool" | "none";
|
|
269
|
+
};
|
|
270
|
+
resume: {
|
|
271
|
+
support: "supported" | "unsupported";
|
|
272
|
+
mechanism?: string;
|
|
273
|
+
conditions: string[];
|
|
274
|
+
};
|
|
275
|
+
budget: "enforced" | "not_enforced";
|
|
276
|
+
cost: {
|
|
277
|
+
kind: "native" | "estimated" | "partial" | "unknown";
|
|
278
|
+
conditions: string[];
|
|
279
|
+
};
|
|
280
|
+
retries: {
|
|
281
|
+
extension: "none" | "once_on_infrastructure";
|
|
282
|
+
native: "none" | "internal" | "unknown";
|
|
283
|
+
};
|
|
284
|
+
nestedAgentDetection: boolean;
|
|
285
|
+
permissions: {
|
|
286
|
+
default: PermissionTier;
|
|
287
|
+
tiers: Record<PermissionTier, PermissionTierCapability>;
|
|
288
|
+
caveats: string[];
|
|
289
|
+
};
|
|
290
|
+
context: {
|
|
291
|
+
modes: ["none", "recent", "full"];
|
|
292
|
+
maxBytes: number;
|
|
293
|
+
resumeConflict: true;
|
|
294
|
+
};
|
|
295
|
+
resources?: PiCapabilityResources;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export interface CapabilityEnvironmentOptions {
|
|
299
|
+
effectiveUid?: number;
|
|
300
|
+
platform?: NodeJS.Platform;
|
|
301
|
+
projectTrusted?: boolean;
|
|
302
|
+
defaultPermission?: PermissionTier;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function buildPermissionTierCapabilities(
|
|
306
|
+
backend: SubagentBackend,
|
|
307
|
+
options: Pick<CapabilityEnvironmentOptions, "effectiveUid" | "platform"> = {},
|
|
308
|
+
): Record<PermissionTier, PermissionTierCapability> {
|
|
309
|
+
const effectiveUid = options.effectiveUid ?? process.geteuid?.();
|
|
310
|
+
const platform = options.platform ?? process.platform;
|
|
311
|
+
const buildTier = (tier: PermissionTier): PermissionTierCapability => {
|
|
312
|
+
const unsupported = unsupportedPermissionReason(tier, backend);
|
|
313
|
+
const resolved = resolvePermission(tier, backend);
|
|
314
|
+
let caveat = resolved.caveat;
|
|
315
|
+
if (backend === "claude" && tier === "danger" && effectiveUid === 0) {
|
|
316
|
+
caveat = "uses --permission-mode auto under effective UID 0";
|
|
317
|
+
} else if (backend === "grok" && tier === "readonly" && platform !== "linux") {
|
|
318
|
+
caveat = `${resolved.caveat} (inactive on ${platform})`;
|
|
319
|
+
}
|
|
320
|
+
const tools = backend === "pi" ? [...PI_TIER_ACTIVE_TOOLS[tier]] : undefined;
|
|
321
|
+
return {
|
|
322
|
+
supported: unsupported === undefined,
|
|
323
|
+
enforced: resolved.enforced,
|
|
324
|
+
...(caveat ? { caveat } : {}),
|
|
325
|
+
...(tools ? { tools } : {}),
|
|
326
|
+
};
|
|
327
|
+
};
|
|
328
|
+
return {
|
|
329
|
+
readonly: buildTier("readonly"),
|
|
330
|
+
edit: buildTier("edit"),
|
|
331
|
+
danger: buildTier("danger"),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function buildPiCapabilityResources(
|
|
336
|
+
preset: PiResourcePreset | undefined,
|
|
337
|
+
projectTrusted = false,
|
|
338
|
+
): PiCapabilityResources {
|
|
339
|
+
const effectivePreset = effectivePiResourcePreset(preset);
|
|
340
|
+
return {
|
|
341
|
+
skills: effectivePreset === "minimal" ? "none" : projectTrusted ? "user_and_project" : "user",
|
|
342
|
+
extensions: "none",
|
|
343
|
+
promptTemplates: "none",
|
|
344
|
+
themes: "none",
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function harnessLevelDiagnostics(
|
|
349
|
+
harness: string,
|
|
350
|
+
catalogDiagnostics: readonly string[],
|
|
351
|
+
): string[] {
|
|
352
|
+
const problems: string[] = [];
|
|
353
|
+
for (const diag of catalogDiagnostics) {
|
|
354
|
+
if (diag.startsWith("Invalid override directory ") && diag.endsWith(`/${harness}`)) {
|
|
355
|
+
problems.push(diag);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return problems;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function buildCapabilityRecord(params: {
|
|
362
|
+
harness: string;
|
|
363
|
+
backend: SubagentBackend;
|
|
364
|
+
configuration: CapabilityConfiguration;
|
|
365
|
+
options?: CapabilityEnvironmentOptions;
|
|
366
|
+
}): CapabilityRecord {
|
|
367
|
+
const { harness, backend, configuration, options = {} } = params;
|
|
368
|
+
const descriptor = BACKEND_CAPABILITIES[backend];
|
|
369
|
+
const defaultPermission = options.defaultPermission ?? "danger";
|
|
370
|
+
const resources =
|
|
371
|
+
backend === "pi"
|
|
372
|
+
? buildPiCapabilityResources(configuration.preset, options.projectTrusted === true)
|
|
373
|
+
: undefined;
|
|
374
|
+
return {
|
|
375
|
+
harness,
|
|
376
|
+
backend,
|
|
377
|
+
boundary: descriptor.boundary,
|
|
378
|
+
configuration,
|
|
379
|
+
readiness: "not_probed",
|
|
380
|
+
workflowChildSchema: { ...descriptor.workflowChildSchema },
|
|
381
|
+
resume: {
|
|
382
|
+
support: descriptor.resume.support,
|
|
383
|
+
...(descriptor.resume.mechanism ? { mechanism: descriptor.resume.mechanism } : {}),
|
|
384
|
+
conditions: [...descriptor.resume.conditions],
|
|
385
|
+
},
|
|
386
|
+
budget: descriptor.budget,
|
|
387
|
+
cost: {
|
|
388
|
+
kind: descriptor.cost.kind,
|
|
389
|
+
conditions: [...descriptor.cost.conditions],
|
|
390
|
+
},
|
|
391
|
+
retries: { ...descriptor.retries },
|
|
392
|
+
nestedAgentDetection: descriptor.nestedAgentDetection,
|
|
393
|
+
permissions: {
|
|
394
|
+
default: defaultPermission,
|
|
395
|
+
tiers: buildPermissionTierCapabilities(backend, options),
|
|
396
|
+
caveats: [...descriptor.permissionCaveats],
|
|
397
|
+
},
|
|
398
|
+
context: {
|
|
399
|
+
modes: ["none", "recent", "full"],
|
|
400
|
+
maxBytes: PARENT_CONTEXT_MAX_BYTES,
|
|
401
|
+
resumeConflict: true,
|
|
402
|
+
},
|
|
403
|
+
...(resources ? { resources } : {}),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export interface BuildCapabilitiesOptions extends CapabilityEnvironmentOptions {
|
|
408
|
+
agentDir: string;
|
|
409
|
+
harness?: string;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function buildCapabilityRecords(options: BuildCapabilitiesOptions): CapabilityRecord[] {
|
|
413
|
+
const loadedSettings = loadExternalSettings(options.agentDir);
|
|
414
|
+
const catalog = loadExternalCatalog(options.agentDir);
|
|
415
|
+
const defaultPermission = options.defaultPermission ?? loadedSettings.settings.defaultPermission;
|
|
416
|
+
const envOptions: CapabilityEnvironmentOptions = {
|
|
417
|
+
effectiveUid: options.effectiveUid,
|
|
418
|
+
platform: options.platform,
|
|
419
|
+
projectTrusted: options.projectTrusted,
|
|
420
|
+
defaultPermission,
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// When the catalog is blocked, registered pi-* names may be unreadable or
|
|
424
|
+
// untrustworthy, so none are guessed. Only built-in harnesses are selectable.
|
|
425
|
+
const configuredPiHarnesses = catalog.blocked ? new Set<string>() : new Set(catalog.harnessConfigs.keys());
|
|
426
|
+
if (options.harness !== undefined) {
|
|
427
|
+
if (
|
|
428
|
+
!(EXTERNAL_HARNESSES as readonly string[]).includes(options.harness) &&
|
|
429
|
+
!configuredPiHarnesses.has(options.harness)
|
|
430
|
+
) {
|
|
431
|
+
const configured = [...EXTERNAL_HARNESSES, ...configuredPiHarnesses].sort();
|
|
432
|
+
throw new ExpectedFlowError(
|
|
433
|
+
"selection_invalid",
|
|
434
|
+
`Unknown harness "${options.harness}". Configured harnesses: ${configured.join(", ") || "none"}.`,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const targetHarnesses = options.harness
|
|
440
|
+
? [options.harness]
|
|
441
|
+
: [...EXTERNAL_HARNESSES, ...[...configuredPiHarnesses].sort()];
|
|
442
|
+
|
|
443
|
+
return targetHarnesses.map((harness) => {
|
|
444
|
+
const isBuiltIn = (EXTERNAL_HARNESSES as readonly string[]).includes(harness);
|
|
445
|
+
const backend: SubagentBackend = isBuiltIn ? (harness as ExternalHarness) : "pi";
|
|
446
|
+
if (catalog.blocked) {
|
|
447
|
+
const diagnostic = catalog.diagnostics.join(" ").trim() || "Configuration is invalid.";
|
|
448
|
+
return buildCapabilityRecord({
|
|
449
|
+
harness,
|
|
450
|
+
backend,
|
|
451
|
+
configuration: {
|
|
452
|
+
state: "invalid",
|
|
453
|
+
enabled: false,
|
|
454
|
+
diagnostics: [diagnostic],
|
|
455
|
+
},
|
|
456
|
+
options: envOptions,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const hSettings = loadedSettings.settings.harnessSettings?.[harness];
|
|
461
|
+
// Binding overrides can repair defaults; the catalog validates each resolved binding.
|
|
462
|
+
const problems = harnessLevelDiagnostics(harness, catalog.diagnostics);
|
|
463
|
+
if (isBuiltIn) {
|
|
464
|
+
const model = hSettings?.model && hSettings.model !== "native" ? hSettings.model : undefined;
|
|
465
|
+
const thinking = hSettings?.thinking;
|
|
466
|
+
const configuration: CapabilityConfiguration = problems.length
|
|
467
|
+
? {
|
|
468
|
+
state: "invalid",
|
|
469
|
+
enabled: false,
|
|
470
|
+
diagnostics: [problems.join(" ")],
|
|
471
|
+
...(model ? { model } : {}),
|
|
472
|
+
...(thinking ? { thinking } : {}),
|
|
473
|
+
}
|
|
474
|
+
: {
|
|
475
|
+
state: "built_in",
|
|
476
|
+
enabled: !catalog.disabledHarnesses.has(harness),
|
|
477
|
+
...(model ? { model } : {}),
|
|
478
|
+
...(thinking ? { thinking } : {}),
|
|
479
|
+
};
|
|
480
|
+
return buildCapabilityRecord({ harness, backend, configuration, options: envOptions });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const piConfig = catalog.harnessConfigs.get(harness)!;
|
|
484
|
+
const preset = effectivePiResourcePreset(piConfig.preset);
|
|
485
|
+
const thinking = hSettings?.thinking ?? piConfig.thinking;
|
|
486
|
+
const configuration: CapabilityConfiguration = problems.length
|
|
487
|
+
? {
|
|
488
|
+
state: "invalid",
|
|
489
|
+
enabled: false,
|
|
490
|
+
diagnostics: [problems.join(" ")],
|
|
491
|
+
model: piConfig.model,
|
|
492
|
+
thinking,
|
|
493
|
+
preset,
|
|
494
|
+
}
|
|
495
|
+
: {
|
|
496
|
+
state: "registered",
|
|
497
|
+
enabled: !catalog.disabledHarnesses.has(harness),
|
|
498
|
+
model: piConfig.model,
|
|
499
|
+
thinking,
|
|
500
|
+
preset,
|
|
501
|
+
};
|
|
502
|
+
return buildCapabilityRecord({ harness, backend, configuration, options: envOptions });
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export function formatCapabilitiesIndex(records: readonly CapabilityRecord[]): string {
|
|
507
|
+
const invalidDiag = records.find((r) => r.configuration.state === "invalid" && r.configuration.diagnostics?.length)
|
|
508
|
+
?.configuration.diagnostics?.join(" ");
|
|
509
|
+
const rows = records.map((record) => {
|
|
510
|
+
const cfg = record.configuration;
|
|
511
|
+
const stateLabel =
|
|
512
|
+
cfg.state === "invalid"
|
|
513
|
+
? "invalid (disabled)"
|
|
514
|
+
: `${cfg.state} (${cfg.enabled ? "enabled" : "disabled"})${cfg.model ? `, model=${cfg.model}` : ""}${cfg.preset ? `, preset=${cfg.preset}` : ""}`;
|
|
515
|
+
const supportedTiers = PERMISSION_TIERS.filter((t) => record.permissions.tiers[t].supported).join("/");
|
|
516
|
+
const schemaLabel =
|
|
517
|
+
record.workflowChildSchema.support === "supported"
|
|
518
|
+
? `supported(${record.workflowChildSchema.mechanism})`
|
|
519
|
+
: "unsupported";
|
|
520
|
+
const resumeLabel =
|
|
521
|
+
record.resume.support === "supported"
|
|
522
|
+
? `supported(${record.resume.mechanism})`
|
|
523
|
+
: "unsupported";
|
|
524
|
+
return `- ${record.harness} [${record.boundary}, ${stateLabel}]: tiers=${supportedTiers} · childSchema=${schemaLabel} · resume=${resumeLabel} · budget=${record.budget} · cost=${record.cost.kind}`;
|
|
525
|
+
});
|
|
526
|
+
return [
|
|
527
|
+
"Harness capabilities (readiness: not_probed; every harness returns structured PublicRun receipts; pass harness:\"<name>\" for full details):",
|
|
528
|
+
...(invalidDiag ? [`Configuration diagnostics: ${invalidDiag}`] : []),
|
|
529
|
+
...rows,
|
|
530
|
+
"",
|
|
531
|
+
"Readiness is not_probed (configuration is not authentication). Run /external config harness test <pi-harness> or /external doctor to check readiness.",
|
|
532
|
+
].join("\n");
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export function formatCapabilityDetail(record: CapabilityRecord): string {
|
|
536
|
+
const cfg = record.configuration;
|
|
537
|
+
const cfgParts = [
|
|
538
|
+
`state=${cfg.state}`,
|
|
539
|
+
`enabled=${cfg.enabled}`,
|
|
540
|
+
...(cfg.model ? [`model=${cfg.model}`] : []),
|
|
541
|
+
...(cfg.thinking ? [`thinking=${cfg.thinking}`] : []),
|
|
542
|
+
...(cfg.preset ? [`preset=${cfg.preset}`] : []),
|
|
543
|
+
];
|
|
544
|
+
const tierLines = PERMISSION_TIERS.map((tier) => {
|
|
545
|
+
const t = record.permissions.tiers[tier];
|
|
546
|
+
const status = !t.supported ? "unsupported" : t.enforced ? "supported (enforced)" : "supported (advisory)";
|
|
547
|
+
const tools = t.tools ? ` · tools: ${t.tools.join("/")}` : "";
|
|
548
|
+
const caveat = t.caveat ? ` · ${t.caveat}` : "";
|
|
549
|
+
return `- ${tier}: ${status}${tools}${caveat}`;
|
|
550
|
+
});
|
|
551
|
+
return [
|
|
552
|
+
`${record.harness} (backend: ${record.backend}, boundary: ${record.boundary})`,
|
|
553
|
+
`Configuration: ${cfgParts.join(", ")}`,
|
|
554
|
+
...(cfg.diagnostics?.length ? [`Diagnostics: ${cfg.diagnostics.join("; ")}`] : []),
|
|
555
|
+
`Readiness: ${record.readiness} (configuration does not prove CLI installation or authentication; use /external config harness test or /external doctor).`,
|
|
556
|
+
"Operational receipts: PublicRun structuredContent supported on external_delegate, external_workflow, and external_runs.",
|
|
557
|
+
`Workflow child schema (agent({ schema })): ${record.workflowChildSchema.support} (mechanism: ${record.workflowChildSchema.mechanism}).`,
|
|
558
|
+
`Resume: ${record.resume.support}${record.resume.mechanism ? ` (${record.resume.mechanism})` : ""}. ${record.resume.conditions.join(" ")}`,
|
|
559
|
+
`Budget enforcement: ${record.budget}${record.budget === "enforced" ? " (enforced mid-run via --max-budget-usd when > 0)." : " (recorded in receipt with budgetEnforceable: false when set; not enforced mid-run)."}`,
|
|
560
|
+
`Cost reporting: ${record.cost.kind}. ${record.cost.conditions.join(" ")}`,
|
|
561
|
+
`Retries: extension=${record.retries.extension}, native=${record.retries.native}.`,
|
|
562
|
+
`Nested-agent activity detection: ${record.nestedAgentDetection ? "supported" : "disabled"}.`,
|
|
563
|
+
`Permissions (default: ${record.permissions.default}):`,
|
|
564
|
+
...tierLines,
|
|
565
|
+
"Permission caveats:",
|
|
566
|
+
...record.permissions.caveats.map((c) => `- ${c}`),
|
|
567
|
+
`Context: modes=${record.context.modes.join(", ")}, maxBytes=${record.context.maxBytes}, resumeConflict=${record.context.resumeConflict}.`,
|
|
568
|
+
...(record.resources
|
|
569
|
+
? [
|
|
570
|
+
`Resources (Pi SDK child): skills=${record.resources.skills}, extensions=${record.resources.extensions}, promptTemplates=${record.resources.promptTemplates}, themes=${record.resources.themes}.`,
|
|
571
|
+
]
|
|
572
|
+
: []),
|
|
573
|
+
].join("\n");
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
export function formatBackendPermissionHelp(
|
|
577
|
+
backend: SubagentBackend,
|
|
578
|
+
options: Pick<CapabilityEnvironmentOptions, "effectiveUid" | "platform"> = {},
|
|
579
|
+
): string {
|
|
580
|
+
const descriptor = BACKEND_CAPABILITIES[backend];
|
|
581
|
+
const effectiveUid = options.effectiveUid ?? process.geteuid?.();
|
|
582
|
+
const tiers = buildPermissionTierCapabilities(backend, { ...options, effectiveUid });
|
|
583
|
+
if (backend === "pi") {
|
|
584
|
+
const tierTools = PERMISSION_TIERS.map((tier) => `${tier} (${tiers[tier].tools!.join("/")})`).join(", ");
|
|
585
|
+
return `pi-* (named Pi harness configs): Tiers gate a curated built-in tool list per tier: ${tierTools}. ${descriptor.permissionCaveats.join(" ")}`;
|
|
586
|
+
}
|
|
587
|
+
const tierSummaries = PERMISSION_TIERS.map((tier) => {
|
|
588
|
+
const info = tiers[tier];
|
|
589
|
+
if (!info.supported) {
|
|
590
|
+
return `${tier} is unsupported (${info.caveat ?? "rejected before launch"})`;
|
|
591
|
+
}
|
|
592
|
+
const args = buildPermissionArgs(tier, backend, { effectiveUid });
|
|
593
|
+
const flagPart = args.length ? `uses ${args.join(" ")}` : "is enforced";
|
|
594
|
+
const caveatPart = info.caveat ? ` (${info.caveat})` : "";
|
|
595
|
+
return `${tier} ${flagPart}${caveatPart}`;
|
|
596
|
+
});
|
|
597
|
+
return `${backend}: ${tierSummaries.join(", ")}. ${descriptor.permissionCaveats.join(" ")}`;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
export function formatPermissionHelp(
|
|
601
|
+
harness: string | undefined,
|
|
602
|
+
configuredPiHarnesses: ReadonlySet<string>,
|
|
603
|
+
options: Pick<CapabilityEnvironmentOptions, "effectiveUid" | "platform"> = {},
|
|
604
|
+
): string {
|
|
605
|
+
const resolveBackend = (name: string): SubagentBackend =>
|
|
606
|
+
(EXTERNAL_HARNESSES as readonly string[]).includes(name) ? (name as ExternalHarness) : "pi";
|
|
607
|
+
const backends: SubagentBackend[] = harness
|
|
608
|
+
? [resolveBackend(harness)]
|
|
609
|
+
: [...EXTERNAL_HARNESSES, ...(configuredPiHarnesses.size ? (["pi"] as const) : [])];
|
|
610
|
+
return [
|
|
611
|
+
"External CLIs and pi-* harnesses run with host access; use them only in trusted repositories. Permission is the call's explicit tier, otherwise the global defaultPermission (danger unless changed). A role describes intent and does not change that tier. Antigravity rejects readonly and edit.",
|
|
612
|
+
...backends.map((backend) => formatBackendPermissionHelp(backend, options)),
|
|
613
|
+
"Failed or aborted runs are not silently retried. Only agy may retry once for an infrastructure-classified auth, eligibility, or network failure, and the receipt discloses it.",
|
|
614
|
+
].join("\n\n");
|
|
615
|
+
}
|
|
@@ -2,6 +2,8 @@ import { ExpectedFlowError } from "./errors.ts";
|
|
|
2
2
|
import { buildSessionContext, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Type, type Static } from "typebox";
|
|
4
4
|
|
|
5
|
+
export const PARENT_CONTEXT_MAX_BYTES = 1024 * 1024;
|
|
6
|
+
|
|
5
7
|
export const parentContextSchema = Type.Union([
|
|
6
8
|
Type.Object({ mode: Type.Literal("none") }, { additionalProperties: false }),
|
|
7
9
|
Type.Object({ mode: Type.Literal("recent"), turns: Type.Integer({ minimum: 1 }) }, { additionalProperties: false }),
|
|
@@ -104,7 +106,7 @@ export function prepareParentContext(
|
|
|
104
106
|
const text = JSON.stringify(transcript);
|
|
105
107
|
const bytes = Buffer.byteLength(text, "utf8");
|
|
106
108
|
// ponytail: fixed transport ceiling, backend-specific token budgeting if needed.
|
|
107
|
-
if (bytes >
|
|
109
|
+
if (bytes > PARENT_CONTEXT_MAX_BYTES) throw new ExpectedFlowError("context_invalid", "Parent context is too large (over 1 MiB); share fewer turns or provide a text briefing. Nothing was truncated.");
|
|
108
110
|
const receipt: ParentContextReceipt = { mode: context.mode, ...(context.mode === "recent" ? { requestedTurns: context.turns } : {}), sharedTurns, messages: transcript.length, bytes, compacted };
|
|
109
111
|
return {
|
|
110
112
|
context: receipt,
|
package/src/core/spawn.ts
CHANGED
|
@@ -38,6 +38,7 @@ import type {
|
|
|
38
38
|
} from "../types.ts";
|
|
39
39
|
import { selectorHarness } from "../profiles.ts";
|
|
40
40
|
import { PI_TIER_ACTIVE_TOOLS, resolvePermission, resolveEffectivePermissionTier, unsupportedPermissionReason } from "./permissions.ts";
|
|
41
|
+
import { BACKEND_CAPABILITIES } from "./capabilities.ts";
|
|
41
42
|
import { resolveResume } from "./resume.ts";
|
|
42
43
|
import { formatParentContext, type ParentContextReceipt } from "./parent-context.ts";
|
|
43
44
|
import { createRunRecord, type RunRecord } from "./run-record.ts";
|
|
@@ -259,7 +260,7 @@ function resolvedRunReceipt(input: {
|
|
|
259
260
|
},
|
|
260
261
|
...(input.maxBudgetUsd !== undefined ? { maxBudgetUsd: input.maxBudgetUsd } : {}),
|
|
261
262
|
// Budgets are enforced mid-run only where the backend supports a native cap (claude).
|
|
262
|
-
...(input.maxBudgetUsd !== undefined && input.backend !== "
|
|
263
|
+
...(input.maxBudgetUsd !== undefined && BACKEND_CAPABILITIES[input.backend].budget !== "enforced" ? { budgetEnforceable: false as const } : {}),
|
|
263
264
|
};
|
|
264
265
|
}
|
|
265
266
|
|
|
@@ -430,7 +431,7 @@ export async function spawnSubagent(params: SpawnSubagentParams): Promise<AgentT
|
|
|
430
431
|
permissionEffective: permission,
|
|
431
432
|
timeoutMs: params.timeoutMs,
|
|
432
433
|
maxBudgetUsd: params.maxBudgetUsd ?? null,
|
|
433
|
-
budgetEnforced: (params.maxBudgetUsd ?? 0) > 0 && params.profile.backend === "
|
|
434
|
+
budgetEnforced: (params.maxBudgetUsd ?? 0) > 0 && BACKEND_CAPABILITIES[params.profile.backend].budget === "enforced",
|
|
434
435
|
resumeRunId: params.resumeRunId ?? null,
|
|
435
436
|
},
|
|
436
437
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getMarkdownTheme, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Container, Markdown, Text, TruncatedText, type Component } from "@earendil-works/pi-tui";
|
|
3
3
|
import { getBackendAgentLabel } from "./display.ts";
|
|
4
|
+
import { BACKEND_CAPABILITIES } from "./capabilities.ts";
|
|
4
5
|
import { formatParentContext, type ParentContextReceipt } from "./parent-context.ts";
|
|
5
6
|
import { SPINNER_FRAMES } from "./spinner.ts";
|
|
6
7
|
import type { PermissionTier, SubagentAssistantOutput, SubagentBackend, SubagentRunStatus, SubagentUsage } from "../types.ts";
|
|
@@ -173,7 +174,7 @@ function richTitle(node: RenderableSubagentNode): string {
|
|
|
173
174
|
function formatRuntimeAndUsage(node: RenderableSubagentNode, now: number, showAccess = true): string {
|
|
174
175
|
const parts: string[] = [];
|
|
175
176
|
if (showAccess && isActiveSubagentStatus(node.status)) {
|
|
176
|
-
parts.push("external host access");
|
|
177
|
+
parts.push(node.backend === "pi" ? "Pi SDK child · host access" : "external host access");
|
|
177
178
|
}
|
|
178
179
|
if (node.context) {
|
|
179
180
|
const shared = node.context;
|
|
@@ -187,7 +188,7 @@ function formatRuntimeAndUsage(node: RenderableSubagentNode, now: number, showAc
|
|
|
187
188
|
if (node.permissionDenials && node.permissionDenials > 0) {
|
|
188
189
|
parts.push(`${node.permissionDenials} permission denials`);
|
|
189
190
|
}
|
|
190
|
-
if (node.maxBudgetUsd !== undefined && node.backend && node.backend !== "
|
|
191
|
+
if (node.maxBudgetUsd !== undefined && node.backend && BACKEND_CAPABILITIES[node.backend].budget !== "enforced") {
|
|
191
192
|
parts.push("budget unenforceable");
|
|
192
193
|
}
|
|
193
194
|
const startedAt = node.startedAt;
|
package/src/external-help.ts
CHANGED
|
@@ -19,6 +19,12 @@ import {
|
|
|
19
19
|
} from "./contract/help.ts";
|
|
20
20
|
import { formatExternalRunsActionGuidance } from "./contract/runs.ts";
|
|
21
21
|
import { formatWorkflowHelpersLine } from "./contract/workflow.ts";
|
|
22
|
+
import {
|
|
23
|
+
buildCapabilityRecords,
|
|
24
|
+
formatCapabilitiesIndex,
|
|
25
|
+
formatCapabilityDetail,
|
|
26
|
+
formatPermissionHelp,
|
|
27
|
+
} from "./core/capabilities.ts";
|
|
22
28
|
import { ExpectedFlowError, type FlowErrorCode } from "./core/errors.ts";
|
|
23
29
|
import { disabledHarnessMessage, loadExternalCatalog } from "./profiles.ts";
|
|
24
30
|
import {
|
|
@@ -28,7 +34,7 @@ import {
|
|
|
28
34
|
formatUsagePlaybook,
|
|
29
35
|
} from "./prompts.ts";
|
|
30
36
|
import { listSavedWorkflows } from "./workflow/registry.ts";
|
|
31
|
-
import { EXTERNAL_HARNESSES
|
|
37
|
+
import { EXTERNAL_HARNESSES } from "./types.ts";
|
|
32
38
|
|
|
33
39
|
export { externalHelpParameters, type ExternalHelpParams } from "./contract/help.ts";
|
|
34
40
|
|
|
@@ -49,37 +55,6 @@ function isProjectTrusted(ctx: ExtensionContext): boolean {
|
|
|
49
55
|
}
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
const PERMISSION_HELP_BY_BACKEND: Record<ExternalHarness | "pi", string> = {
|
|
53
|
-
agy: "agy: autonomous only. Every accepted run uses --dangerously-skip-permissions and is unsandboxed. readonly and edit are rejected instead of being broadened to that mode.",
|
|
54
|
-
claude: "claude: readonly uses --permission-mode plan, edit uses --permission-mode acceptEdits, and danger uses --dangerously-skip-permissions (--permission-mode auto under effective UID 0). Headless readonly/edit deny Bash. The requested tier is used as-is; role names do not raise it.",
|
|
55
|
-
codex: "codex: readonly, edit, and danger map to read-only, workspace-write, and danger-full-access sandboxes. This governs model-generated shell commands, not MCP/plugins/hooks.",
|
|
56
|
-
grok: "grok: readonly uses --sandbox read-only, edit uses --sandbox workspace, and danger uses --sandbox off, always alongside --permission-mode bypassPermissions. Enforced by a kernel sandbox, but network blocking is Linux-only; edit limits writes to the workspace.",
|
|
57
|
-
muse: "muse: readonly uses --disable-approval --disable-write --disable-shell (approval, non-shell writes, and shell execution all disabled), edit uses --disable-approval alone (the sandbox stays enabled by default; writes and shell remain available within it), and danger uses --yolo, which disables approval and the sandbox and additionally trusts this workspace (loads its skills/rules) for this run — a broader grant than an unsandboxed run alone. Native provider retries (up to 10 attempts) are disclosed via activity narration, not performed by this extension.",
|
|
58
|
-
opencode: "opencode (OpenCode 2): every run is `opencode run --standalone`, a private server this run owns, never the shared background service. readonly and edit run under a per-run agent injected through OPENCODE_CONFIG_CONTENT and selected with --agent on every run, resumes included, whose permission rules deny every tool except read/grep/glob (readonly) or those plus edit/write/patch (edit); shell, subagent, web, MCP, and plugin tools are hidden, and edits outside the workspace are denied. Resuming a session with its own permission rules at readonly/edit, or a restricted session at danger, is refused. These are OpenCode application rules, not an OS sandbox, and project or user plugins and MCP servers still load and run their code. danger uses --auto, which approves every permission ask except explicit denies. OpenCode retries transient provider errors itself (up to 10 retries); this extension does not retry.",
|
|
59
|
-
pi: "pi-* (named Pi harness configs): run in-process, not as a CLI. Tiers gate a curated built-in tool list (read/bash/edit/write/grep/find/ls). That list is not an OS sandbox; danger-tier bash has host access. Each registration selects preset minimal (the default when the field is absent; skills stay unloaded) or skills (installed skills load; project skills load only when the project is trusted). Extensions, prompt templates, and themes stay unloaded. Retry is disabled per child regardless of Pi's own settings.",
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
/** Resolve a requested, already-validated harness name to its permission-semantics backend. */
|
|
63
|
-
function permissionHelpBackend(harness: string, configuredPiHarnesses: ReadonlySet<string>): ExternalHarness | "pi" {
|
|
64
|
-
if ((EXTERNAL_HARNESSES as readonly string[]).includes(harness)) return harness as ExternalHarness;
|
|
65
|
-
// Callers only reach here after validateHarnessFilter, so any name that is
|
|
66
|
-
// not one of the external harnesses is guaranteed to be a registered
|
|
67
|
-
// pi-* harness at this point — never an unrecognized string silently
|
|
68
|
-
// treated as "pi".
|
|
69
|
-
return "pi";
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function permissionHelp(harness: string | undefined, configuredPiHarnesses: ReadonlySet<string>): string {
|
|
73
|
-
const backends: (ExternalHarness | "pi")[] = harness
|
|
74
|
-
? [permissionHelpBackend(harness, configuredPiHarnesses)]
|
|
75
|
-
: [...EXTERNAL_HARNESSES, ...(configuredPiHarnesses.size ? (["pi"] as const) : [])];
|
|
76
|
-
return [
|
|
77
|
-
"External CLIs and pi-* harnesses run with host access; use them only in trusted repositories. Permission is the call's explicit tier, otherwise the global defaultPermission (danger unless changed). A role describes intent and does not change that tier. Antigravity rejects readonly and edit.",
|
|
78
|
-
...backends.map((name) => PERMISSION_HELP_BY_BACKEND[name]),
|
|
79
|
-
"Failed or aborted runs are not silently retried. Only agy may retry once for an infrastructure-classified auth, eligibility, or network failure, and the receipt discloses it.",
|
|
80
|
-
].join("\n\n");
|
|
81
|
-
}
|
|
82
|
-
|
|
83
58
|
/**
|
|
84
59
|
* Reject an unknown harness filter up front, listing the live configured
|
|
85
60
|
* set, rather than letting it silently fall through to "pi" (permissions)
|
|
@@ -151,7 +126,7 @@ export function createExternalHelpTool(
|
|
|
151
126
|
return defineTool({
|
|
152
127
|
name: EXTERNAL_HELP_TOOL,
|
|
153
128
|
label: "External Help",
|
|
154
|
-
description: "Read-only help on demand: usage playbook, role details, permission behavior, workflow usage (including background runs, external_runs supervision syntax, and replay) or saved-workflow discovery,
|
|
129
|
+
description: "Read-only help on demand: usage playbook, role details, permission behavior, workflow usage (including background runs, external_runs supervision syntax, and replay) or saved-workflow discovery, public tool contracts, and per-harness capabilities.",
|
|
155
130
|
promptSnippet: EXTERNAL_HELP_PROMPT_SNIPPET,
|
|
156
131
|
parameters: externalHelpParameters,
|
|
157
132
|
outputSchema: externalHelpOutputSchema,
|
|
@@ -183,6 +158,20 @@ export function createExternalHelpTool(
|
|
|
183
158
|
};
|
|
184
159
|
}
|
|
185
160
|
|
|
161
|
+
if (params.topic === "capabilities") {
|
|
162
|
+
const harnesses = buildCapabilityRecords({
|
|
163
|
+
agentDir: getAgentDir(),
|
|
164
|
+
harness,
|
|
165
|
+
projectTrusted: isProjectTrusted(ctx),
|
|
166
|
+
});
|
|
167
|
+
const text = harness ? formatCapabilityDetail(harnesses[0]!) : formatCapabilitiesIndex(harnesses);
|
|
168
|
+
return {
|
|
169
|
+
content: [{ type: "text" as const, text }],
|
|
170
|
+
details: { topic: "capabilities" as const, ...(harness ? { harness } : {}) },
|
|
171
|
+
data: { topic: "capabilities" as const, ...(harness ? { harness } : {}), harnesses },
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
186
175
|
let text: string;
|
|
187
176
|
const catalog = loadExternalCatalog(getAgentDir());
|
|
188
177
|
const harnessConfigs = catalog.harnessConfigs;
|
|
@@ -199,7 +188,7 @@ export function createExternalHelpTool(
|
|
|
199
188
|
: harness && catalog.disabledHarnesses.has(harness) ? disabledHarnessMessage(harness)
|
|
200
189
|
: formatExternalRoleHelp(catalog.profiles, options.getDefaultHarness(ctx), harness);
|
|
201
190
|
} else if (params.topic === "permissions") {
|
|
202
|
-
text =
|
|
191
|
+
text = formatPermissionHelp(harness, configuredPiHarnesses);
|
|
203
192
|
} else {
|
|
204
193
|
const wfText = workflowHelp(options.workflowEnabled, listSavedWorkflows({
|
|
205
194
|
agentDir: getAgentDir(),
|
package/src/prompts.ts
CHANGED
|
@@ -10,7 +10,7 @@ export const WORKFLOW_PROMPT_SNIPPET =
|
|
|
10
10
|
"Run requested multi-agent orchestration with external roles; when useful read the bundled pi-flow-orchestration skill to select the smallest fitting pattern. Use external_help for exact syntax and saved workflows.";
|
|
11
11
|
|
|
12
12
|
export const EXTERNAL_HELP_PROMPT_SNIPPET =
|
|
13
|
-
"Show the delegation playbook, role details, permission behavior, workflow guidance,
|
|
13
|
+
"Show the delegation playbook, role details, permission behavior, workflow guidance, public contracts, or harness capabilities on demand.";
|
|
14
14
|
|
|
15
15
|
export const EXTERNAL_RUNS_PROMPT_SNIPPET =
|
|
16
16
|
"Supervise session-owned external runs (supply only fields for the chosen action): list (optional workflowRunId, limit, cursor, workflowCursor), inspect (runIds: 1 ID with any view, or 2-20 IDs for summary batch), wait (runIds 1-100, optional mode/limitBytes), or cancel (runIds: 1 ID, optional reason). view: final returns only a verified terminal answer, empty until one exists.";
|
|
@@ -105,7 +105,7 @@ Choose a fresh child or resume on purpose. Omit context for independent work. co
|
|
|
105
105
|
|
|
106
106
|
Default to foreground (blocking) when you need the result next, including parallel workflows. When you have independent local work (read, edit, bash) that does not touch files the delegated run may change, or the user asks for background execution, pass background:true, do that other work first, and only call external_runs wait once you actually need the result; never background then immediately wait. Ending a turn does not stop a background run: if the user asks to leave it running, switch tasks, or check back later, report the runId and end your turn or work on the other task without waiting. Otherwise retain the runId and collect it in this turn after your independent work finishes. Foreground has a live card; a background receipt is not a live monitor, though UI sessions show active background runs in the status bar and wake the session with a follow-up message when an uncollected background run settles across turns. Use external_runs wait only when needed; check each run's outcome and pending IDs. Read inline successful output or inspect view final; inspect failures with diagnostics. Interrupting foreground cancels that call; interrupting wait only stops waiting. Explicit cancel stops background work. Keep the owning session open: orderly shutdown requests cancellation; restart does not adopt work. Prefer one external_workflow with parallel() for coordinated parallel tasks; all children share maxConcurrentSubagents. Sequential awaits stay serial. Report milestones. Retry a failed run, or switch its model or harness, only when the user authorized that step. agy alone may make one disclosed infrastructure retry. Nested agents may start in another workspace.
|
|
107
107
|
|
|
108
|
-
Catalog availability reflects enabled harnesses and built-in/authored roles, not CLI installation or authentication. Disabled harnesses reject calls without fallback; manage them through /external config. Harness and role lifecycle commands live under /external config harness and /external config role. Role selection never falls back. external_help topic usage is the playbook; roles, permissions, workflow, and
|
|
108
|
+
Catalog availability reflects enabled harnesses and built-in/authored roles, not CLI installation or authentication. Disabled harnesses reject calls without fallback; manage them through /external config. Harness and role lifecycle commands live under /external config harness and /external config role. Role selection never falls back. external_help topic usage is the playbook; roles, permissions, workflow, contracts, and capabilities are the references.`;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
export interface UsageAgentExample {
|
|
@@ -210,7 +210,7 @@ export function formatUsagePlaybook(): string {
|
|
|
210
210
|
|
|
211
211
|
A named pi-* harness runs in-process on the SDK's built-in tools. Preset minimal leaves skills unloaded. Preset skills loads installed skills, and project skills only when the project is trusted. Those presets select skill loading. They are not this extension's tools and they are not web search. Resume does not continue a pi-* child, and a budget there is recorded without enforcement.
|
|
212
212
|
|
|
213
|
-
Tell each child the objective, the context it needs, what it may change, the deliverable, and how you will verify it. Use absolute paths. Say read-only or edit in the prompt. Omit permission to keep defaultPermission (danger unless changed), including a reviewer that may use a shell and still must not edit. Pass permission only to confine the call below that default. A danger tier stays inside the authorization the user already gave. Antigravity accepts only danger. Topic permissions is the harness-boundary reference. Topic roles lists descriptions. Topic workflow is the syntax and saved-workflow reference. Topic contracts is the public schema and error-code reference (pass tool for one tool's schemas and examples).
|
|
213
|
+
Tell each child the objective, the context it needs, what it may change, the deliverable, and how you will verify it. Use absolute paths. Say read-only or edit in the prompt. Omit permission to keep defaultPermission (danger unless changed), including a reviewer that may use a shell and still must not edit. Pass permission only to confine the call below that default. A danger tier stays inside the authorization the user already gave. Antigravity accepts only danger. Topic permissions is the harness-boundary reference. Topic roles lists descriptions. Topic workflow is the syntax and saved-workflow reference. Topic contracts is the public schema and error-code reference (pass tool for one tool's schemas and examples). Topic capabilities is the per-harness feature, permission, and resource reference (pass harness for one harness's full record; readiness is not_probed).
|
|
214
214
|
|
|
215
215
|
Choose fresh context or resume deliberately. Omit context, or pass {"mode":"none"}, for independent work such as a review. {"mode":"recent","turns":N} and {"mode":"full"} share parent conversation and cannot be combined with resume. resume continues one prior CLI run that recorded a session id, on the same backend: ${EXTERNAL_HARNESSES.join(", ")}. A named pi-* harness has no persisted session, so resume does not continue it. Prefer resume for the same task and a new child when you want an independent judgment.
|
|
216
216
|
|