@henryqw/pi-subagent 24.0.1 → 25.0.1
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/CONTEXT.md +1 -1
- package/README.md +3 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.js +24 -8
- package/dist/mcp-role.d.ts +7 -5
- package/dist/mcp-role.js +98 -8
- package/docs/orchestration.md +1 -1
- package/extensions/role-mcp.ts +6 -10
- package/package.json +7 -7
package/CONTEXT.md
CHANGED
|
@@ -62,7 +62,7 @@ Own Role-configured Pi delegation through one `delegate_task` surface:
|
|
|
62
62
|
### Resource and launch policy
|
|
63
63
|
|
|
64
64
|
- Ambient child extensions and Skills stay disabled. Only Role/caller resources plus required internal adapters load.
|
|
65
|
-
- Every Role requires `tools`, `extensions`, and `skills` arrays. Omitted or empty `mcps` denies MCP access. Direct `pi-mcp-adapter` loading is rejected because it bypasses the allowlist.
|
|
65
|
+
- Every Role requires `tools`, `extensions`, and `skills` arrays. `extensions` may name Pi built-in extensions as `builtin:<name>`. `mcps` names servers from the global `~/.pi/agent/mcp.json`; their tools load in the child with direct exposure. Omitted or empty `mcps` denies MCP access. Direct `pi-mcp-adapter` loading is rejected because it bypasses the allowlist.
|
|
66
66
|
- Selected extensions are trusted executable bundles, not a sandbox. All tools and lifecycle behavior they register load together.
|
|
67
67
|
- Role Skill names resolve through Main's effective Pi registry. Missing Roles, Skills, tools, MCP servers, routes, models, or thinking levels fail before the first productive turn.
|
|
68
68
|
- Route precedence is call model class, then Role default, then registered Model Task assignment/default. A direct model replaces only the route model and must support its thinking level.
|
package/README.md
CHANGED
|
@@ -13,18 +13,11 @@ pi install npm:@henryqw/pi-subagent
|
|
|
13
13
|
|
|
14
14
|
Run `/task-models` and configure the `fast`, `balanced`, `frontier`, and `fav` routes you use.
|
|
15
15
|
|
|
16
|
-
Install `pi-mcp-adapter` only when a Role declares an MCP server allowlist:
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
pi install npm:pi-mcp-adapter
|
|
20
|
-
```
|
|
21
|
-
|
|
22
16
|
## Works with
|
|
23
17
|
|
|
24
18
|
| Package | Relationship | Purpose |
|
|
25
19
|
| --- | --- | --- |
|
|
26
20
|
| [`@henryqw/pi-task-models`](https://pi.henry.wang/extensions/pi-task-models) | Required | Provides the configured model routes for delegated Roles. |
|
|
27
|
-
| [`pi-mcp-adapter`](https://github.com/nicobailon/pi-mcp-adapter) | Required | Loads the MCP servers named in a Role's `mcps` allowlist, only for Roles that declare one. |
|
|
28
21
|
|
|
29
22
|
## Use
|
|
30
23
|
|
|
@@ -181,14 +174,14 @@ Role Markdown lives in `~/.pi/agent/config/pi-subagent/` and requires frontmatte
|
|
|
181
174
|
| `name`, `description` | Required non-empty text without terminal control characters |
|
|
182
175
|
| `modelClass` | Optional `fast`, `balanced`, `frontier`, or `fav` default |
|
|
183
176
|
| `tools` | Required array of base tool names; `[]` selects none |
|
|
184
|
-
| `extensions` | Required array of trusted absolute paths
|
|
177
|
+
| `extensions` | Required array of trusted absolute paths, supported package sources, or Pi built-in extensions such as `builtin:codemode` |
|
|
185
178
|
| `skills` | Required array of effective Pi Skill names |
|
|
186
|
-
| `mcps` | Optional exact
|
|
179
|
+
| `mcps` | Optional exact server names from `~/.pi/agent/mcp.json` in Pi's native `mcpServers` format; omitted or `[]` denies MCP access |
|
|
187
180
|
| body | Required system instructions |
|
|
188
181
|
|
|
189
182
|
Roles describe responsibility and capabilities. They do not choose isolation; each request does. A same-named user Role overrides a built-in Role. The package ships `implementer`, `reviewer`, and `scout`.
|
|
190
183
|
|
|
191
|
-
Children
|
|
184
|
+
Children run with `--no-extensions`, so a Role must list the extension that registers its route model's provider, and only declared resources plus required internal policy adapters load. `builtin:<name>` entries load Pi built-in extensions (`builtin:codemode`, `builtin:tool-search`, `builtin:llama.cpp`) explicitly; `builtin:mcp` is rejected because it would connect every configured server and bypass `mcps`. The servers named in `mcps` come from the global `~/.pi/agent/mcp.json` only; their tools load in the child with direct exposure and are declared to the model as `mcp__<server>__<tool>` once the server connects. Pi replaces `-` in server and tool names with `_` and appends a hash suffix when two tool names collide or a name exceeds 64 characters. Each selected entry is checked against Pi's documented `mcpServers` rules before launch, in Main and again in the child: a disabled, malformed, or unknown-transport entry, an invalid server name, two selected names that differ only in `-` and `_`, `auth.provider` over plain HTTP outside `localhost`, `127.0.0.1`, or `[::1]`, or a non-HTTPS `oauth.authServerMetadataUrl` outside those hosts fails the launch with the file path and field. The file is never modified; fix the entry or choose another server. Valid `auth`, `oauth`, and `description` fields are passed through so Pi's native provider auth and OAuth apply. Missing Skills, tools, MCP servers, Roles, or routes fail before productive work starts. Main-only delegation and recovery tools plus `ask_question` are excluded from children.
|
|
192
185
|
|
|
193
186
|
## API
|
|
194
187
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type AvailableModel, type ModelTask, type ProfileName, type ResolvedTaskRoute, type ThinkingLevel } from "@henryqw/pi-task-models";
|
|
3
3
|
export { DISPLAY_TEXT_CONTRACT, hasDisplayControlCharacters } from "./display-text.ts";
|
|
4
|
-
export {
|
|
4
|
+
export { loadRoleMcpConfig, parseRoleMcpAllowlist } from "./mcp-role.ts";
|
|
5
5
|
export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, DEFAULT_MAX_TURNS, EphemeralSubagentError, EXECUTION_BUDGET_ENV, formatDuration, type EphemeralSubagentActivityEvent, type EphemeralSubagentErrorCode, type EphemeralSubagentExecutionBudget, type EphemeralSubagentExecutor, type EphemeralSubagentExecutorOptions, type EphemeralSubagentResult, type EphemeralSubagentRunInput, type EphemeralSubagentTimeout, } from "./ephemeral.ts";
|
|
6
6
|
export { createChildWorktree, finalizeChildWorktree, inspectIndexFlags, inspectWorktreeDirty, WorktreeSetupError, type WorktreeDirtyInspection, type WorktreeInfo, type WorktreePayload, } from "./worktree.ts";
|
|
7
7
|
export { prepareExactReviewEvidence, REVIEW_MAX_PATCH_BYTES, REVIEW_MAX_PATHS, type PreparedReviewEvidence, type PrepareExactReviewEvidenceInput, } from "./review-evidence.ts";
|
package/dist/index.js
CHANGED
|
@@ -4,10 +4,10 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { DefaultPackageManager, getAgentDir, parseFrontmatter, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { extensionConfigDir } from "@henryqw/pi-config-store";
|
|
6
6
|
import { hasDisplayControlCharacters } from "./display-text.js";
|
|
7
|
-
import {
|
|
7
|
+
import { loadRoleMcpConfig, parseRoleMcpAllowlist } from "./mcp-role.js";
|
|
8
8
|
import { loadTaskModelsConfig, modelReference, orderedProfileRoutes, PROFILE_NAMES, resolveConfiguredTaskRoute, resolveTaskModelRoute, } from "@henryqw/pi-task-models";
|
|
9
9
|
export { DISPLAY_TEXT_CONTRACT, hasDisplayControlCharacters } from "./display-text.js";
|
|
10
|
-
export {
|
|
10
|
+
export { loadRoleMcpConfig, parseRoleMcpAllowlist } from "./mcp-role.js";
|
|
11
11
|
export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, DEFAULT_MAX_TURNS, EphemeralSubagentError, EXECUTION_BUDGET_ENV, formatDuration, } from "./ephemeral.js";
|
|
12
12
|
export { createChildWorktree, finalizeChildWorktree, inspectIndexFlags, inspectWorktreeDirty, WorktreeSetupError, } from "./worktree.js";
|
|
13
13
|
export { prepareExactReviewEvidence, REVIEW_MAX_PATCH_BYTES, REVIEW_MAX_PATHS, } from "./review-evidence.js";
|
|
@@ -66,12 +66,25 @@ const stringList = (value, field, source) => {
|
|
|
66
66
|
}
|
|
67
67
|
return value.map((item) => item.trim());
|
|
68
68
|
};
|
|
69
|
+
// Pi 0.99 built-in extensions (`builtin:<name>`, see Pi's settings docs); Pi exports no list of them.
|
|
70
|
+
// `builtin:mcp` is excluded: it connects every server in mcp.json and would bypass the Role `mcps` allowlist.
|
|
71
|
+
const BUILTIN_EXTENSION_NAMES = ["codemode", "tool-search", "llama.cpp"];
|
|
72
|
+
const BUILTIN_EXTENSION_PREFIX = "builtin:";
|
|
69
73
|
function validateExtension(extension, source) {
|
|
70
74
|
const value = cleanText(extension, "extension", source);
|
|
75
|
+
if (value.startsWith(BUILTIN_EXTENSION_PREFIX)) {
|
|
76
|
+
if (value === `${BUILTIN_EXTENSION_PREFIX}mcp`) {
|
|
77
|
+
throw new Error(`${source} must select MCP servers with mcps instead of loading builtin:mcp directly.`);
|
|
78
|
+
}
|
|
79
|
+
if (!BUILTIN_EXTENSION_NAMES.includes(value.slice(BUILTIN_EXTENSION_PREFIX.length))) {
|
|
80
|
+
throw new Error(`${source}: unknown built-in extension ${value}; use ${BUILTIN_EXTENSION_NAMES.map((name) => `${BUILTIN_EXTENSION_PREFIX}${name}`).join(", ")}.`);
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
71
84
|
const packageSource = /^(?:npm|git|github|https?|ssh):/.test(value);
|
|
72
85
|
const userPath = isAbsolute(value) || value.startsWith("~/") || value.startsWith("~\\") || value.startsWith("file://");
|
|
73
86
|
if (!packageSource && !userPath) {
|
|
74
|
-
throw new Error(`${source}: extensions entries must be absolute paths
|
|
87
|
+
throw new Error(`${source}: extensions entries must be absolute paths, package sources, or builtin:<name>.`);
|
|
75
88
|
}
|
|
76
89
|
return value;
|
|
77
90
|
}
|
|
@@ -231,7 +244,9 @@ export async function resolveRolePackageResources(role, ctx) {
|
|
|
231
244
|
const resolved = await packageManager(ctx).resolveExtensionSources([...sources]);
|
|
232
245
|
const resourceGroups = [resolved.extensions, resolved.skills, resolved.prompts, resolved.themes]
|
|
233
246
|
.map((resources) => resources.filter((resource) => resource.enabled));
|
|
234
|
-
|
|
247
|
+
// Built-in extensions resolve to their `builtin:<name>` path with the bare source `builtin`.
|
|
248
|
+
const resolvedSources = new Set(resourceGroups.flat()
|
|
249
|
+
.map((resource) => resource.metadata.source === "builtin" ? resource.path : resource.metadata.source));
|
|
235
250
|
const missing = sources.filter((source) => !resolvedSources.has(source));
|
|
236
251
|
if (missing.length)
|
|
237
252
|
throw new Error(`Role extension sources resolved no resources: ${missing.join(", ")}.`);
|
|
@@ -317,6 +332,9 @@ export function prepareRoleLaunch(pi, ctx, input) {
|
|
|
317
332
|
? prepareRoleLaunchFromSkills(ctx, input, resolveRoleSkills(pi, input.role))
|
|
318
333
|
: prepareResolvedRoleLaunch(pi, ctx, input);
|
|
319
334
|
assertNoMissingRoleSkills(input.role, prepared);
|
|
335
|
+
// Validate the mcp.json the child reads: its environment inherits Main's, with launch overrides on top.
|
|
336
|
+
if (input.role.mcps?.length)
|
|
337
|
+
loadRoleMcpConfig(prepared.env.PI_CODING_AGENT_DIR ?? getAgentDir(), input.role.mcps);
|
|
320
338
|
return { ...prepared, role };
|
|
321
339
|
}
|
|
322
340
|
/** Resolve and prepare a configured Role with its package-owned resources. */
|
|
@@ -328,10 +346,8 @@ export async function resolveConfiguredRoleLaunch(pi, ctx, input) {
|
|
|
328
346
|
if (matches.length !== 1)
|
|
329
347
|
throw new Error(`Required configured Role ${roleName} is missing or ambiguous.`);
|
|
330
348
|
const role = matches[0];
|
|
331
|
-
if (role.mcps?.length)
|
|
332
|
-
|
|
333
|
-
selectRoleMcpConfig(loadMcpConfig(join(getAgentDir(), "mcp.json"), ctx.cwd), role.mcps);
|
|
334
|
-
}
|
|
349
|
+
if (role.mcps?.length)
|
|
350
|
+
loadRoleMcpConfig(getAgentDir(), role.mcps);
|
|
335
351
|
const resources = await resolveRolePackageResources(role, ctx);
|
|
336
352
|
const effectiveRole = { ...role, extensions: resources.extensions };
|
|
337
353
|
const namedSkills = resolveRoleSkills(pi, effectiveRole);
|
package/dist/mcp-role.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
mcpServers: Record<string, unknown>;
|
|
3
|
-
settings?: Record<string, unknown>;
|
|
4
|
-
}
|
|
1
|
+
import type { LoadedMcpConfig } from "@earendil-works/pi-coding-agent";
|
|
5
2
|
export declare function parseRoleMcpAllowlist(value: unknown): string[];
|
|
6
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Select the Role's allowlisted servers from the global `mcp.json` in Pi's native `mcpServers`
|
|
5
|
+
* shape; the project `.pi/mcp.json` is ignored. A `--no-extensions` child has no codemode tool, so
|
|
6
|
+
* every selected server is forced to direct exposure and codemode is never activated.
|
|
7
|
+
*/
|
|
8
|
+
export declare function loadRoleMcpConfig(agentDir: string, allowlist: readonly string[]): LoadedMcpConfig;
|
package/dist/mcp-role.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
export function parseRoleMcpAllowlist(value) {
|
|
2
4
|
if (typeof value !== "string")
|
|
3
5
|
throw new Error("The Role MCP policy flag must contain a JSON array of MCP server names.");
|
|
@@ -17,14 +19,102 @@ export function parseRoleMcpAllowlist(value) {
|
|
|
17
19
|
throw new Error("The Role MCP policy flag contains duplicate MCP server names.");
|
|
18
20
|
return names;
|
|
19
21
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
function isRecord(value) {
|
|
23
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
const isString = (value) => typeof value === "string";
|
|
26
|
+
const isStringRecord = (value) => isRecord(value) && Object.values(value).every(isString);
|
|
27
|
+
const parseUrl = (value) => (typeof value === "string" && URL.canParse(value) ? new URL(value) : undefined);
|
|
28
|
+
// Pi's native MCP rules (core/mcp-servers.ts); `validateMcpServerConfig` is not exported, so they are mirrored here.
|
|
29
|
+
const SERVER_NAME = /^[A-Za-z0-9_-]+$/;
|
|
30
|
+
const LOOPBACK_HOSTS = ["localhost", "127.0.0.1", "[::1]"];
|
|
31
|
+
const isHttpsOrLoopback = (url) => url !== undefined && (url.protocol === "https:" || (url.protocol === "http:" && LOOPBACK_HOSTS.includes(url.hostname)));
|
|
32
|
+
/** Tool namespace `mcp__<server>` replaces `-` with `_`, so names differing only there collide. */
|
|
33
|
+
const mcpNamespace = (name) => name.replace(/-/g, "_");
|
|
34
|
+
/** Documented `mcpServers` fields (Pi's docs/mcp.md) and their shapes, by transport; dotted keys are nested. */
|
|
35
|
+
const COMMON_CHECKS = {
|
|
36
|
+
enabled: [(value) => typeof value === "boolean", "a boolean"],
|
|
37
|
+
timeout: [(value) => typeof value === "number" && Number.isFinite(value) && value > 0, "a positive number"],
|
|
38
|
+
description: [isString, "a string"],
|
|
39
|
+
};
|
|
40
|
+
const STDIO_CHECKS = {
|
|
41
|
+
args: [(value) => Array.isArray(value) && value.every(isString), "an array of strings"],
|
|
42
|
+
env: [isStringRecord, "an object of strings"],
|
|
43
|
+
cwd: [isString, "a string"],
|
|
44
|
+
};
|
|
45
|
+
const HTTP_CHECKS = {
|
|
46
|
+
url: [(value) => /^https?:$/.test(parseUrl(value)?.protocol ?? ""), "an http or https URL"],
|
|
47
|
+
headers: [isStringRecord, "an object of strings"],
|
|
48
|
+
oauth: [isRecord, "an object"],
|
|
49
|
+
"oauth.clientId": [isString, "a string"],
|
|
50
|
+
"oauth.clientSecret": [isString, "a string"],
|
|
51
|
+
"oauth.callbackPort": [(value) => Number.isInteger(value) && value >= 1 && value <= 65535, "a port number"],
|
|
52
|
+
"oauth.callbackUrl": [(value, server) => {
|
|
53
|
+
const url = parseUrl(value);
|
|
54
|
+
const port = server.oauth.callbackPort;
|
|
55
|
+
return url !== undefined && url.protocol === "http:" && LOOPBACK_HOSTS.includes(url.hostname) && url.search === "" && url.hash === ""
|
|
56
|
+
&& (url.port === "" || port === undefined || Number(url.port) === port);
|
|
57
|
+
}, "an http URL on localhost, 127.0.0.1, or [::1] without query or fragment, on the \"oauth.callbackPort\" port"],
|
|
58
|
+
"oauth.scope": [isString, "a string"],
|
|
59
|
+
"oauth.clientName": [(value) => typeof value === "string" && value.trim() !== "", "a non-empty string"],
|
|
60
|
+
"oauth.authServerMetadataUrl": [(value) => isHttpsOrLoopback(parseUrl(value)), "an https URL, or http on localhost, 127.0.0.1, or [::1]"],
|
|
61
|
+
auth: [(value) => isRecord(value) && typeof value.provider === "string" && value.provider !== "", "an object with a non-empty \"provider\" string"],
|
|
62
|
+
};
|
|
63
|
+
/** Validate one selected server against Pi's documented `mcpServers` rules; the native loader is bypassed by `loadConfig`. */
|
|
64
|
+
function validateRoleMcpServer(path, name, value) {
|
|
65
|
+
const fail = (message) => { throw new Error(`${path}: MCP server "${name}" ${message}`); };
|
|
66
|
+
if (!SERVER_NAME.test(name))
|
|
67
|
+
fail("has an invalid name (use letters, digits, \"_\" and \"-\").");
|
|
68
|
+
if (!isRecord(value))
|
|
69
|
+
fail("must be an object.");
|
|
70
|
+
if (value.enabled === false)
|
|
71
|
+
throw new Error(`${path}: Role MCP server "${name}" is disabled.`);
|
|
72
|
+
const stdio = typeof value.command === "string" && value.command.trim() !== "" && (value.type === undefined || value.type === "stdio");
|
|
73
|
+
const http = typeof value.url === "string" && value.url.trim() !== "" && (value.type === undefined || value.type === "http" || value.type === "streamable-http");
|
|
74
|
+
if (stdio === http)
|
|
75
|
+
fail("needs either a \"command\" (stdio) or a \"url\" (http or streamable-http).");
|
|
76
|
+
for (const [field, [valid, shape]] of Object.entries({ ...COMMON_CHECKS, ...(stdio ? STDIO_CHECKS : HTTP_CHECKS) })) {
|
|
77
|
+
const fieldValue = field.split(".").reduce((parent, key) => (isRecord(parent) ? parent[key] : undefined), value);
|
|
78
|
+
if (fieldValue !== undefined && !valid(fieldValue, value))
|
|
79
|
+
fail(`field "${field}" must be ${shape}.`);
|
|
80
|
+
}
|
|
81
|
+
// The native transport sends the provider's token to `url`, so Pi only allows it over https or loopback.
|
|
82
|
+
if (value.auth !== undefined && !isHttpsOrLoopback(parseUrl(value.url))) {
|
|
83
|
+
fail("field \"auth\" requires \"url\" to use https, or http on localhost, 127.0.0.1, or [::1].");
|
|
84
|
+
}
|
|
85
|
+
const { toolExposure: _toolExposure, ...config } = value;
|
|
86
|
+
return { ...config, exposure: "direct" };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Select the Role's allowlisted servers from the global `mcp.json` in Pi's native `mcpServers`
|
|
90
|
+
* shape; the project `.pi/mcp.json` is ignored. A `--no-extensions` child has no codemode tool, so
|
|
91
|
+
* every selected server is forced to direct exposure and codemode is never activated.
|
|
92
|
+
*/
|
|
93
|
+
export function loadRoleMcpConfig(agentDir, allowlist) {
|
|
94
|
+
const path = join(agentDir, "mcp.json");
|
|
95
|
+
let parsed = {};
|
|
96
|
+
try {
|
|
97
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
|
|
101
|
+
throw new Error(`${path}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!isRecord(parsed) || (parsed.mcpServers !== undefined && !isRecord(parsed.mcpServers))) {
|
|
105
|
+
throw new Error(`${path}: expected an object with an "mcpServers" object.`);
|
|
106
|
+
}
|
|
107
|
+
const configured = parsed.mcpServers ?? {};
|
|
108
|
+
const missing = allowlist.filter((name) => !Object.hasOwn(configured, name));
|
|
22
109
|
if (missing.length)
|
|
23
110
|
throw new Error(`Role MCP servers are not configured: ${missing.join(", ")}.`);
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
111
|
+
const namespaces = new Map();
|
|
112
|
+
const servers = allowlist.map((name) => {
|
|
113
|
+
const clash = namespaces.get(mcpNamespace(name));
|
|
114
|
+
if (clash)
|
|
115
|
+
throw new Error(`${path}: Role MCP server "${name}" conflicts with "${clash}": names that differ only in "-" and "_" share one tool namespace.`);
|
|
116
|
+
namespaces.set(mcpNamespace(name), name);
|
|
117
|
+
return { name, config: validateRoleMcpServer(path, name, configured[name]), source: path, scope: "global" };
|
|
118
|
+
});
|
|
119
|
+
return { servers, autoEnableCodemode: false, errors: [] };
|
|
30
120
|
}
|
package/docs/orchestration.md
CHANGED
|
@@ -139,7 +139,7 @@ Every child launch:
|
|
|
139
139
|
5. excludes Main-only delegation/recovery tools and `ask_question`; and
|
|
140
140
|
6. prepends the delegated-subagent identity contract.
|
|
141
141
|
|
|
142
|
-
An empty `tools` list adds no base tools, though selected extension tools can still activate. Omitted or empty `mcps` denies MCP access. Loading `pi-mcp-adapter` directly is rejected because it bypasses the allowlist.
|
|
142
|
+
An empty `tools` list adds no base tools, though selected extension tools can still activate. Children run with `--no-extensions`, so a Role must list the extension that registers its route model's provider; `builtin:<name>` entries load Pi built-in extensions. `mcps` names servers from the global `~/.pi/agent/mcp.json` in Pi's native format, and their tools load in the child with direct exposure. Omitted or empty `mcps` denies MCP access. Loading `pi-mcp-adapter` directly is rejected because it bypasses the allowlist.
|
|
143
143
|
|
|
144
144
|
Isolated Herdr Role launches receive the same turn, token, and child-runtime budget metadata as ephemeral launches. Retained workers therefore keep one non-refilling session budget across follow-ups rather than receiving a new request budget.
|
|
145
145
|
|
package/extensions/role-mcp.ts
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { createMcpAdapter } from "pi-mcp-adapter";
|
|
4
|
-
import { loadMcpConfig } from "pi-mcp-adapter/config";
|
|
5
|
-
import { roleMcpAllowlistFromArgv, ROLE_MCP_POLICY_FLAG, selectRoleMcpConfig } from "@henryqw/pi-subagent";
|
|
1
|
+
import { createMcpExtension, getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { loadRoleMcpConfig, roleMcpAllowlistFromArgv, ROLE_MCP_POLICY_FLAG } from "@henryqw/pi-subagent";
|
|
6
3
|
|
|
7
|
-
export default function roleMcp(pi: ExtensionAPI): void {
|
|
4
|
+
export default function roleMcp(pi: ExtensionAPI): void | Promise<void> {
|
|
8
5
|
pi.registerFlag(ROLE_MCP_POLICY_FLAG, {
|
|
9
6
|
description: "Internal Pi Subagent Role MCP policy",
|
|
10
7
|
type: "string",
|
|
11
8
|
});
|
|
12
|
-
// Pi binds extension flag values after factories load.
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
createMcpAdapter({ config })(pi);
|
|
9
|
+
// Pi binds extension flag values after factories load. Throwing here exits the child non-zero.
|
|
10
|
+
const config = loadRoleMcpConfig(getAgentDir(), roleMcpAllowlistFromArgv(process.argv));
|
|
11
|
+
return createMcpExtension({ loadConfig: () => config })(pi);
|
|
16
12
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-subagent",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "25.0.1",
|
|
4
4
|
"description": "Delegate bounded direct work or durable checked isolated task graphs to Pi roles.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
"pack:check": "npm pack --dry-run"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@earendil-works/pi-ai": ">=0.
|
|
44
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
45
|
-
"@earendil-works/pi-tui": ">=0.
|
|
43
|
+
"@earendil-works/pi-ai": ">=0.99.1 <0.100.0 || ^1.0.0",
|
|
44
|
+
"@earendil-works/pi-coding-agent": ">=0.99.1 <0.100.0 || ^1.0.0",
|
|
45
|
+
"@earendil-works/pi-tui": ">=0.99.1 <0.100.0 || ^1.0.0",
|
|
46
46
|
"typebox": "^1.3.15"
|
|
47
47
|
},
|
|
48
48
|
"repository": {
|
|
@@ -74,9 +74,9 @@
|
|
|
74
74
|
"proper-lockfile": "^4.1.2"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
|
-
"@earendil-works/pi-ai": "0.
|
|
78
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
79
|
-
"@earendil-works/pi-tui": "0.
|
|
77
|
+
"@earendil-works/pi-ai": "1.0.0",
|
|
78
|
+
"@earendil-works/pi-coding-agent": "1.0.0",
|
|
79
|
+
"@earendil-works/pi-tui": "1.0.0",
|
|
80
80
|
"@types/proper-lockfile": "^4.1.4"
|
|
81
81
|
}
|
|
82
82
|
}
|