external-subagent 0.1.2 → 0.1.3
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/README.md +43 -25
- package/cli/commands/agents.mjs +50 -2
- package/cli/commands/config.mjs +2 -2
- package/cli/commands/maintenance.mjs +2 -2
- package/cli/commands/update.mjs +1 -1
- package/cli/config/schema.mjs +1 -1
- package/cli/config/write.mjs +1 -1
- package/cli/constants.mjs +2 -2
- package/cli/install/codex.mjs +4 -4
- package/cli/install/init.mjs +3 -3
- package/cli/install/plugin-stage.mjs +22 -2
- package/cli/install/service-macos.mjs +9 -0
- package/cli/install/zcode.mjs +2 -2
- package/cli/main.mjs +10 -14
- package/cli/maintenance.mjs +1 -6
- package/cli/paths.mjs +1 -13
- package/cli/rpc.mjs +1 -0
- package/npm/native/darwin-arm64/external-subagent-mcp +0 -0
- package/npm/native/darwin-arm64/external-subagentd +0 -0
- package/npm/native/darwin-arm64/payload.json +4 -4
- package/package.json +1 -1
- package/plugins/codex/external-subagent/.codex-plugin/plugin.json +1 -1
- package/plugins/codex/external-subagent/.mcp.json +1 -1
- package/plugins/codex/external-subagent/scripts/mcp-stdio-bridge.mjs +3 -3
- package/plugins/codex/external-subagent/skills/external-subagent/SKILL.md +14 -7
- package/schema/external-subagent-public-api.json +11 -5
- package/schema/{zas-observation-v1.1.schema.json → observation.schema.json} +1 -1
- package/schema/public-reasoning-source.json +36 -0
- package/scripts/release/build-native-payload.mjs +23 -7
- package/schema/zas-public-reasoning-source.json +0 -14
package/README.md
CHANGED
|
@@ -15,32 +15,43 @@ independent axes:
|
|
|
15
15
|
|
|
16
16
|
## Status
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
productization closeout below is the historical record of that stage. DSH and
|
|
20
|
-
ZCode installed-artifact lifecycles, active-task upgrade, and standalone
|
|
21
|
-
initialization are implemented and reviewed. `init` installs the standalone
|
|
22
|
-
daemon service only — no host is bound implicitly; hosts are bound through the
|
|
23
|
-
explicit `install-plugin` (Codex or ZCode) / `install-mcp` (Codex-only TOML
|
|
24
|
-
binding) commands. Installing from a packed artifact and explicitly
|
|
25
|
-
initializing to a launchd-resident service (idempotent repeat init/start) is
|
|
26
|
-
live-verified on a real GUI session. Both productization
|
|
27
|
-
candidates passed the four-cell consumer matrix — DSH/ZCode × public CLI /
|
|
28
|
-
real Codex CLI via the managed plugin over MCP, each against its own tarball:
|
|
29
|
-
the pre-fix candidate C1 (`0.1.1`, historical) and the post-installer-fix
|
|
30
|
-
final candidate C2 (`0.1.2`), whose fresh consumer verification passed
|
|
31
|
-
through the public install surface — all four cells COMPLETED with the
|
|
32
|
-
plugin cache verified against the staged binding (`cache_verified: true`;
|
|
33
|
-
see [docs/acceptance/productization.md](docs/acceptance/productization.md)).
|
|
34
|
-
The managed plugin manifest is versioned per released candidate because its
|
|
35
|
-
version is codex's plugin-cache identity, and codex resolves the reserved
|
|
36
|
-
`personal` marketplace name machine-globally to the real user root
|
|
37
|
-
(regardless of `CODEX_HOME`), so each candidate's consumer runs pin that
|
|
38
|
-
identity's bytes there and any later candidate bumps again. The installer
|
|
39
|
-
re-verified on 0.154.0; DSH admission currently supports `build` and strict
|
|
40
|
-
`plan`; its model selection is explicit spawn model, configured default, then
|
|
41
|
-
the native default. The package is published to the public npm registry as
|
|
18
|
+
The package is published to the public npm registry as
|
|
42
19
|
`external-subagent@0.1.0` (macOS arm64 only; `os`/`cpu` in `package.json`
|
|
43
|
-
reject installation on other platforms at npm install time).
|
|
20
|
+
reject installation on other platforms at npm install time). `init` installs
|
|
21
|
+
the standalone daemon service only — no host is bound implicitly; hosts are
|
|
22
|
+
bound through the explicit `install-plugin` (Codex or ZCode) /
|
|
23
|
+
`install-mcp` (Codex-only TOML binding) commands. Installing from a packed
|
|
24
|
+
artifact and explicitly initializing to a launchd-resident service
|
|
25
|
+
(idempotent repeat init/start) is live-verified on a real GUI session;
|
|
26
|
+
active-task upgrade and standalone initialization are implemented and
|
|
27
|
+
reviewed.
|
|
28
|
+
|
|
29
|
+
All three subagents are **disabled by default** and are enabled per name with
|
|
30
|
+
`external-subagent agents enable <zcode|dsh|codex>`: a successful local probe
|
|
31
|
+
writes the configuration (no write on failure), and `dsh`/`codex` need a
|
|
32
|
+
daemon restart to take effect. ZCode admits all four permission modes
|
|
33
|
+
(build/edit/plan/yolo) and rejects an explicit spawn `model`; DSH admits
|
|
34
|
+
`build` and strict `plan`, with model selection as explicit spawn model,
|
|
35
|
+
configured default, then the native default, and its `build` mode admits a
|
|
36
|
+
non-empty caller `write_manifest` through the guarded manifest-build
|
|
37
|
+
composition (workspace-write sandbox, `tool-fs`-only writes, out-of-manifest
|
|
38
|
+
paths rejected as `FS_WRITE_MANIFEST_DENIED`; an explicit `["."]` keeps the
|
|
39
|
+
legacy build composition and `plan` still requires an empty manifest); Codex
|
|
40
|
+
admits all four modes
|
|
41
|
+
(build/edit → workspace-write, plan → read-only, yolo → danger-full-access,
|
|
42
|
+
all pinning `approvalPolicy=never`) and rejects a non-empty `write_manifest`
|
|
43
|
+
with `codex_write_manifest_unsupported`. `observe` works on all three: ZCode
|
|
44
|
+
and DSH expose the public reasoning tail (at most 200 characters) and Codex
|
|
45
|
+
reports `reasoning: null`.
|
|
46
|
+
|
|
47
|
+
The daemon socket environment variable is now the neutral
|
|
48
|
+
`EXTERNAL_SUBAGENT_SOCKET`, with no fallback to the retired name. An existing
|
|
49
|
+
installation upgrading to this version must run the public
|
|
50
|
+
`external-subagent update` (or `reconcile`) once to rewrite the host
|
|
51
|
+
bindings, then restart the service; see
|
|
52
|
+
[docs/operations.md](docs/operations.md). The historical productization
|
|
53
|
+
acceptance matrix is recorded in
|
|
54
|
+
[docs/acceptance/productization.md](docs/acceptance/productization.md).
|
|
44
55
|
|
|
45
56
|
## Versioning
|
|
46
57
|
|
|
@@ -122,3 +133,10 @@ cargo test --workspace
|
|
|
122
133
|
node --test tests/install/*.test.mjs tests/platform/*.test.mjs tests/cli/*.test.mjs tests/contract/*.test.mjs
|
|
123
134
|
node --test tests/integration/*.test.mjs tools/probes/dsh-acp/probe.test.mjs
|
|
124
135
|
```
|
|
136
|
+
|
|
137
|
+
The JavaScript install/upgrade suites build the native facade through
|
|
138
|
+
`scripts/release/build-native-payload.mjs`; set
|
|
139
|
+
`EXTERNAL_SUBAGENT_CARGO_PROFILE=debug` to make that entry build from
|
|
140
|
+
`target/debug` (no `--release`) and record `profile: debug` in the staged
|
|
141
|
+
manifest. The default stays a release build for `prepack`/publishing, and
|
|
142
|
+
`npm run check` is the JavaScript syntax gate.
|
package/cli/commands/agents.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { readConfig } from '../config/read.mjs';
|
|
2
|
+
import { updateConfig } from '../config/write.mjs';
|
|
3
|
+
import { SUBAGENT_IDS } from '../config/schema.mjs';
|
|
4
|
+
import path from 'node:path';
|
|
2
5
|
import { CliError } from '../errors.mjs';
|
|
3
6
|
|
|
4
7
|
const INPUT_FIELDS = new Set(['operation', 'subagent', 'through', 'workspace', 'home']);
|
|
@@ -60,7 +63,11 @@ function parseModelsArgs(rest) {
|
|
|
60
63
|
export function parseSubagentsArgs(args) {
|
|
61
64
|
if (args.length === 0) return { operation: 'list' };
|
|
62
65
|
const [operation, ...rest] = args;
|
|
63
|
-
if (!['list', 'status', 'probe', 'models'].includes(operation)) throw new CliError('INVALID_ARGUMENT', `unsupported subagents operation: ${operation}`, 2);
|
|
66
|
+
if (!['list', 'status', 'probe', 'models', 'enable'].includes(operation)) throw new CliError('INVALID_ARGUMENT', `unsupported subagents operation: ${operation}`, 2);
|
|
67
|
+
if (operation === 'enable') {
|
|
68
|
+
if (rest.length !== 1 || !SUBAGENT_IDS.includes(rest[0])) throw new CliError('INVALID_ARGUMENT', 'usage: agents enable <zcode|dsh|codex>', 2);
|
|
69
|
+
return { operation, subagent: rest[0] };
|
|
70
|
+
}
|
|
64
71
|
if (operation === 'probe') return parseProbeArgs(rest);
|
|
65
72
|
if (operation === 'models') return parseModelsArgs(rest);
|
|
66
73
|
if (operation === 'list' && rest.length !== 0) throw new CliError('INVALID_ARGUMENT', 'usage: subagents list', 2);
|
|
@@ -72,6 +79,44 @@ export async function subagentsCommand(paths, input = {}, options = {}) {
|
|
|
72
79
|
validateInput(input);
|
|
73
80
|
const operation = input.operation ?? 'list';
|
|
74
81
|
const config = readConfig(paths.config);
|
|
82
|
+
if (operation === 'enable') {
|
|
83
|
+
const agent = input.subagent;
|
|
84
|
+
if (!SUBAGENT_IDS.includes(agent) || ['through', 'workspace', 'home'].some((key) => input[key] !== undefined)) {
|
|
85
|
+
throw new CliError('INVALID_ARGUMENT', 'usage: agents enable <zcode|dsh|codex>', 2);
|
|
86
|
+
}
|
|
87
|
+
if (typeof options.callDaemon !== 'function' || typeof options.socket !== 'string') throw new CliError('INTERNAL_ERROR', 'agents enable requires a daemon connection');
|
|
88
|
+
const entry = config.subagents[agent];
|
|
89
|
+
const scope = Object.fromEntries(['home', 'profile'].filter((key) => entry[key] != null).map((key) => [key, entry[key]]));
|
|
90
|
+
const result = await options.callDaemon(options.socket, 'agent-probe', { subagent: agent, through: 'local', scope });
|
|
91
|
+
const local = result.evidence?.local;
|
|
92
|
+
if (local?.state !== 'READY') throw new CliError('agent_probe_failed', `Cannot enable ${agent}: ${local?.reason ?? 'local runtime probe did not succeed'}`, 2);
|
|
93
|
+
const requiredVersion = result.status?.required_version;
|
|
94
|
+
if (agent === 'dsh' && (typeof requiredVersion !== 'string' || !requiredVersion.trim())) {
|
|
95
|
+
throw new CliError('agent_probe_failed', 'Cannot enable dsh: daemon did not report required_version; update and restart the daemon, then retry', 2);
|
|
96
|
+
}
|
|
97
|
+
if (requiredVersion != null && local.version !== requiredVersion) {
|
|
98
|
+
throw new CliError('agent_probe_failed', `Cannot enable ${agent}: expected version ${requiredVersion}, observed ${local.version ?? 'unknown'}; install the required runtime version and retry`, 2);
|
|
99
|
+
}
|
|
100
|
+
const patch = { enabled: true, spawn_supported: true };
|
|
101
|
+
if (agent !== 'zcode') {
|
|
102
|
+
if (typeof local.runtime_path !== 'string' || !path.isAbsolute(local.runtime_path) || typeof local.version !== 'string' || !local.version.trim()) {
|
|
103
|
+
throw new CliError('agent_probe_failed', 'Probe did not return an absolute runtime_path and observed version', 2);
|
|
104
|
+
}
|
|
105
|
+
const home = local.scope?.home;
|
|
106
|
+
if (typeof home !== 'string' || !path.isAbsolute(home)) throw new CliError('agent_probe_failed', `Cannot enable ${agent}: configure subagents.${agent}.home or export ${agent.toUpperCase()}_HOME before starting the daemon`, 2);
|
|
107
|
+
Object.assign(patch, { runtime_path: local.runtime_path, home, profile: agent === 'dsh' ? 'acp' : (entry.profile ?? null), version: local.version });
|
|
108
|
+
}
|
|
109
|
+
const updated = updateConfig(paths.config, (latest) => {
|
|
110
|
+
if (Object.entries(patch).every(([key, value]) => latest.subagents[agent][key] === value)) return latest;
|
|
111
|
+
return { ...latest, subagents: { ...latest.subagents, [agent]: { ...latest.subagents[agent], ...patch } } };
|
|
112
|
+
});
|
|
113
|
+
const restart = agent !== 'zcode';
|
|
114
|
+
return {
|
|
115
|
+
subagent: agent, enabled: true, config_revision: updated.revision,
|
|
116
|
+
config: updated, evidence: subagentView(result.evidence), restart_required: restart,
|
|
117
|
+
message: restart ? '配置已保存,重启 daemon 后生效。' : '配置已保存,新的 zcode 任务即时生效,无需重启。',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
75
120
|
if (operation === 'list') {
|
|
76
121
|
return { default_subagent: config.default_subagent, config_revision: config.revision, subagents: Object.entries(config.subagents).map(([id, value]) => ({ subagent: id, ...value })) };
|
|
77
122
|
}
|
|
@@ -99,7 +144,10 @@ export async function subagentsCommand(paths, input = {}, options = {}) {
|
|
|
99
144
|
const entries = status.subagents ?? status.agents;
|
|
100
145
|
if (!Array.isArray(entries)) throw new CliError('PROTOCOL_ERROR', 'daemon status did not include subagent status');
|
|
101
146
|
const subagents = entries.filter((entry) => input.subagent === undefined || (entry.subagent ?? entry.agent) === input.subagent).map(subagentView);
|
|
102
|
-
if (input.subagent !== undefined && subagents.length === 0)
|
|
147
|
+
if (input.subagent !== undefined && subagents.length === 0) {
|
|
148
|
+
const roster = entries.map((entry) => entry.subagent ?? entry.agent).filter(Boolean).map((name) => `"${name}"`).join(', ');
|
|
149
|
+
throw new CliError('subagent_unknown', `daemon did not report subagent: ${input.subagent}, available subagents are [${roster}]`, 2);
|
|
150
|
+
}
|
|
103
151
|
return { service_generation: status.service_generation ?? null, subagents };
|
|
104
152
|
}
|
|
105
153
|
|
package/cli/commands/config.mjs
CHANGED
|
@@ -36,8 +36,8 @@ function unsetPatch(key) {
|
|
|
36
36
|
if (key === 'default_subagent') return { default_subagent: null };
|
|
37
37
|
const [, agent, field] = key.split('.');
|
|
38
38
|
const defaults = {
|
|
39
|
-
enabled:
|
|
40
|
-
spawn_supported:
|
|
39
|
+
enabled: false,
|
|
40
|
+
spawn_supported: false,
|
|
41
41
|
default_model: null, runtime_path: null, home: null, profile: null, version: null,
|
|
42
42
|
};
|
|
43
43
|
return { subagents: { [agent]: { [field]: defaults[field] } } };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { backupData,
|
|
1
|
+
import { backupData, purge, restoreData, uninstall as removeService } from '../maintenance.mjs';
|
|
2
2
|
import { unregisterAllCodexHomes } from '../install/reconcile.mjs';
|
|
3
3
|
import { uninstallZcodePlugin } from '../install/zcode.mjs';
|
|
4
4
|
|
|
@@ -24,4 +24,4 @@ export function uninstall(paths, options = {}) {
|
|
|
24
24
|
return { ...removed, codex_homes_unregistered: released.unregistered, zcode_binding: zcode };
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export { backupData,
|
|
27
|
+
export { backupData, purge, restoreData };
|
package/cli/commands/update.mjs
CHANGED
|
@@ -40,7 +40,7 @@ export async function updateCommand(paths, args = [], daemon = {}) {
|
|
|
40
40
|
const registryPath = paths.data ? path.join(paths.data, 'codex-homes.json') : null;
|
|
41
41
|
const priorRegistryBytes = (() => { try { return fs.readFileSync(registryPath); } catch { return null; } })();
|
|
42
42
|
const requestedVersion = args.find((arg) => arg.startsWith('--version='))?.slice(10) || 'current';
|
|
43
|
-
const socket = daemon.socket || process.env.
|
|
43
|
+
const socket = daemon.socket || process.env.EXTERNAL_SUBAGENT_SOCKET || paths.socket;
|
|
44
44
|
const rpc = daemon.callDaemon || callDaemon;
|
|
45
45
|
const drainDeadline = Number.isFinite(daemon.drainTimeoutMs) && daemon.drainTimeoutMs > 0
|
|
46
46
|
? Date.now() + daemon.drainTimeoutMs : null;
|
package/cli/config/schema.mjs
CHANGED
|
@@ -21,7 +21,7 @@ export function defaultConfig() {
|
|
|
21
21
|
revision: 0,
|
|
22
22
|
default_subagent: null,
|
|
23
23
|
subagents: {
|
|
24
|
-
zcode: { enabled:
|
|
24
|
+
zcode: { enabled: false, spawn_supported: false, default_model: null },
|
|
25
25
|
dsh: { enabled: false, spawn_supported: false, default_model: null, runtime_path: null, home: null, profile: null, version: null },
|
|
26
26
|
codex: { enabled: false, spawn_supported: false, default_model: null, runtime_path: null, home: null, profile: null, version: null },
|
|
27
27
|
},
|
package/cli/config/write.mjs
CHANGED
|
@@ -31,7 +31,7 @@ export function updateConfig(file, updater) {
|
|
|
31
31
|
try {
|
|
32
32
|
const current = readConfig(file);
|
|
33
33
|
const next = updater(current);
|
|
34
|
-
return writeConfig(file, next);
|
|
34
|
+
return next === current ? current : writeConfig(file, next);
|
|
35
35
|
} finally {
|
|
36
36
|
try { process.kill(-helper.pid, 'SIGTERM'); } catch {}
|
|
37
37
|
try { fs.unlinkSync(ready); } catch {}
|
package/cli/constants.mjs
CHANGED
|
@@ -8,7 +8,7 @@ const IS_DEBUG_VARIANT = process.env.EXTERNAL_SUBAGENT_VARIANT === 'debug';
|
|
|
8
8
|
|
|
9
9
|
export const PRODUCT_NAME = IS_DEBUG_VARIANT ? 'external-subagent-debug' : 'external-subagent';
|
|
10
10
|
export const PRODUCT_ID = IS_DEBUG_VARIANT ? 'external_subagent_debug' : 'external_subagent';
|
|
11
|
-
export const VERSION = '0.1.
|
|
11
|
+
export const VERSION = '0.1.3';
|
|
12
12
|
export const LAUNCH_AGENT_LABEL = IS_DEBUG_VARIANT ? 'com.external-subagent-debug.daemon' : 'com.external-subagent.daemon';
|
|
13
13
|
export const DAEMON_BIN_NAME = IS_DEBUG_VARIANT ? 'external-subagent-debugd' : 'external-subagentd';
|
|
14
14
|
export const MCP_BIN_NAME = IS_DEBUG_VARIANT ? 'external-subagent-debug-mcp' : 'external-subagent-mcp';
|
|
@@ -18,7 +18,7 @@ export const ZCODE_RUNTIME = '/Applications/ZCode.app/Contents/Resources/glm/zco
|
|
|
18
18
|
|
|
19
19
|
export const BUSINESS_COMMANDS = new Set([
|
|
20
20
|
'init', 'hooks', 'status', 'diagnose', 'backup', 'restore', 'start', 'stop',
|
|
21
|
-
'uninstall', 'purge', '
|
|
21
|
+
'uninstall', 'purge', 'install-plugin', 'install-mcp', 'create', 'spawn', 'wait', 'list', 'send',
|
|
22
22
|
'respond', 'cancel', 'result', 'close', 'config', 'agents', 'subagents',
|
|
23
23
|
'observe', 'update', 'reconcile',
|
|
24
24
|
]);
|
package/cli/install/codex.mjs
CHANGED
|
@@ -97,10 +97,10 @@ function updateMarketplace(file, staging) {
|
|
|
97
97
|
function stagedManagedBinding(staging) {
|
|
98
98
|
const manifest = JSON.parse(fs.readFileSync(path.join(staging, '.codex-plugin', 'plugin.json'), 'utf8'));
|
|
99
99
|
const server = JSON.parse(fs.readFileSync(path.join(staging, '.mcp.json'), 'utf8')).mcpServers?.external_subagent;
|
|
100
|
-
if (!server || typeof server.command !== 'string' || typeof server.env?.
|
|
100
|
+
if (!server || typeof server.command !== 'string' || typeof server.env?.EXTERNAL_SUBAGENT_SOCKET !== 'string') {
|
|
101
101
|
throw new CliError('INVALID_PLUGIN_SOURCE', 'staged plugin MCP binding is incomplete');
|
|
102
102
|
}
|
|
103
|
-
return { manifest, command: server.command, socket: server.env.
|
|
103
|
+
return { manifest, command: server.command, socket: server.env.EXTERNAL_SUBAGENT_SOCKET };
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
function readCacheJson(file, cache, identity) {
|
|
@@ -152,7 +152,7 @@ function verifyCodexCache(add, { codexHome, staging, marketplaceName }) {
|
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
const server = readCacheJson(path.join(cache, '.mcp.json'), cache, identity).mcpServers?.external_subagent;
|
|
155
|
-
const socket = server?.env?.
|
|
155
|
+
const socket = server?.env?.EXTERNAL_SUBAGENT_SOCKET;
|
|
156
156
|
if (!server || server.command !== expected.command || socket !== expected.socket) {
|
|
157
157
|
throw new CliError('CODEX_CACHE_BINDING_MISMATCH', `codex plugin cache for ${identity} carries a different managed binding (command=${server?.command}, socket=${socket}); expected this install's staged binding (command=${expected.command}, socket=${expected.socket}). The cache resolved to another installation's bytes (${cache}); either the machine-global content store reused them for the same identity (release a distinct plugin version instead of accepting them), or the reserved marketplace name personal resolved machine-globally to the real user root regardless of CODEX_HOME (refresh or re-register that root, or use a non-reserved marketplace name in isolated homes)`);
|
|
158
158
|
}
|
|
@@ -309,7 +309,7 @@ export function uninstallPlugin(paths, options = {}) {
|
|
|
309
309
|
function codexMcpConfig(paths) {
|
|
310
310
|
const command = nativeBinary(MCP_BIN_NAME);
|
|
311
311
|
const tools = CODEX_TOOLS.map((tool) => ` "${tool}",`).join('\n');
|
|
312
|
-
return `[${CODEX_MCP_SECTION}]\ncommand = ${JSON.stringify(command)}\nenabled = true\nrequired = true\nstartup_timeout_sec = 10\ntool_timeout_sec = 304\nenabled_tools = [\n${tools}\n]\ndefault_tools_approval_mode = "prompt"\n\n[${CODEX_MCP_SECTION}.env]\
|
|
312
|
+
return `[${CODEX_MCP_SECTION}]\ncommand = ${JSON.stringify(command)}\nenabled = true\nrequired = true\nstartup_timeout_sec = 10\ntool_timeout_sec = 304\nenabled_tools = [\n${tools}\n]\ndefault_tools_approval_mode = "prompt"\n\n[${CODEX_MCP_SECTION}.env]\nEXTERNAL_SUBAGENT_SOCKET = ${JSON.stringify(paths.socket)}\n\n[${CODEX_MCP_SECTION}.tools.external_subagent_status]\napproval_mode = "auto"\n\n[${CODEX_MCP_SECTION}.tools.external_subagent_list]\napproval_mode = "auto"\n\n[${CODEX_MCP_SECTION}.tools.external_subagent_wait]\napproval_mode = "auto"\n\n[${CODEX_MCP_SECTION}.tools.external_subagent_result]\napproval_mode = "auto"\n`;
|
|
313
313
|
}
|
|
314
314
|
|
|
315
315
|
function removeTomlSection(text, section) {
|
package/cli/install/init.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
|
-
import { LAUNCH_AGENT_LABEL
|
|
4
|
+
import { LAUNCH_AGENT_LABEL } from '../constants.mjs';
|
|
5
5
|
import { CliError } from '../errors.mjs';
|
|
6
6
|
import { parseConfig } from '../config/read.mjs';
|
|
7
7
|
import { writeConfig } from '../config/write.mjs';
|
|
@@ -10,7 +10,7 @@ import { pathReport } from './path.mjs';
|
|
|
10
10
|
import { packageRoot, payloadManifestPath } from './layout.mjs';
|
|
11
11
|
import { updateInstallation } from './update.mjs';
|
|
12
12
|
import { loadInstallState, markInstallStep, removeCreatedDirectories, rollbackFiles, snapshotFile } from './recovery.mjs';
|
|
13
|
-
import { bootstrapService, bootoutService, installLaunchAgent } from './service-macos.mjs';
|
|
13
|
+
import { bootstrapService, bootoutService, installLaunchAgent, runtimeObservations } from './service-macos.mjs';
|
|
14
14
|
|
|
15
15
|
// Standalone-install coordination (S05, AUD-005 decision D1). A plain npm
|
|
16
16
|
// install only stages the package and payload; an explicit `init` installs
|
|
@@ -197,7 +197,7 @@ export function runInit(options = {}) {
|
|
|
197
197
|
// Honest runtime report instead of a hard dependency: the pinned ZCode
|
|
198
198
|
// runtime is an adapter capability, observed and forwarded by the service
|
|
199
199
|
// template only when present.
|
|
200
|
-
|
|
200
|
+
runtimes: runtimeObservations(paths),
|
|
201
201
|
payload,
|
|
202
202
|
baseline,
|
|
203
203
|
path_report: pathFindings,
|
|
@@ -75,6 +75,16 @@ export function treeDigest(root) {
|
|
|
75
75
|
// The publish protocol keeps two private sibling trees beside the target,
|
|
76
76
|
// named `.<target>.candidate.<pid>.<uuid>` and `.<target>.prior.<pid>.<uuid>`.
|
|
77
77
|
|
|
78
|
+
// The socket environment key every managed binding pins. Runtime consumers
|
|
79
|
+
// (the stdio bridge, the native MCP facade, the CLI, and the daemon) read
|
|
80
|
+
// exactly this name, with no fallback to any retired spelling.
|
|
81
|
+
export const SOCKET_ENV = 'EXTERNAL_SUBAGENT_SOCKET';
|
|
82
|
+
// The retired key earlier installers pinned into staged `.mcp.json` files.
|
|
83
|
+
// It is recognized ONLY here, to refresh a managed staging tree in place, and
|
|
84
|
+
// is never a runtime fallback. The literal is assembled from fragments so the
|
|
85
|
+
// retired name never appears as a searchable token (the rg gate stays clean).
|
|
86
|
+
export const LEGACY_SOCKET_ENV = ['ZCODE', 'AGENTD', 'SOCKET'].join('_');
|
|
87
|
+
|
|
78
88
|
const siblingSuffix = () => `${process.pid}.${crypto.randomUUID()}`;
|
|
79
89
|
|
|
80
90
|
function discardTree(dir) {
|
|
@@ -97,7 +107,10 @@ function bindCandidate(candidate, paths, command, args, timeoutMs) {
|
|
|
97
107
|
server.command = command;
|
|
98
108
|
if (args !== null) server.args = args;
|
|
99
109
|
if (timeoutMs !== null) server.timeoutMs = timeoutMs;
|
|
100
|
-
server.env = { ...(server.env || {}),
|
|
110
|
+
server.env = { ...(server.env || {}), [SOCKET_ENV]: paths.socket };
|
|
111
|
+
// Rewrite, never accumulate: a managed prior tree bound under the retired
|
|
112
|
+
// key is refreshed to carry the new key and nothing else.
|
|
113
|
+
delete server.env[LEGACY_SOCKET_ENV];
|
|
101
114
|
fs.writeFileSync(mcpPath, `${JSON.stringify(mcp, null, 2)}\n`, { mode: 0o600 });
|
|
102
115
|
}
|
|
103
116
|
|
|
@@ -138,7 +151,14 @@ export function preparePluginStage(source, staging, paths, binding = {}) {
|
|
|
138
151
|
// the staged bridge); a prior native-form binding carried none, and the
|
|
139
152
|
// rewrite below installs the requested form regardless.
|
|
140
153
|
const argsMatch = args === null || priorCommand !== command || JSON.stringify(priorArgs) === JSON.stringify(args);
|
|
141
|
-
|
|
154
|
+
// Ownership of an existing tree is proved by the requested binding, the
|
|
155
|
+
// native facade, or a staged script. Its socket may be pinned under the
|
|
156
|
+
// current key OR the retired one this installer used before the rename:
|
|
157
|
+
// either one pointing at paths.socket is a managed product binding that
|
|
158
|
+
// gets rewritten to the current key. Any other binding stays foreign.
|
|
159
|
+
const priorEnv = priorServer?.env || {};
|
|
160
|
+
const priorSocketManaged = priorEnv[SOCKET_ENV] === paths.socket || priorEnv[LEGACY_SOCKET_ENV] === paths.socket;
|
|
161
|
+
if (!priorServer || (!commandMatches && !argsOwned) || !argsMatch || !priorSocketManaged) {
|
|
142
162
|
throw new CliError('PLUGIN_STAGING_CONFLICT', 'staging MCP binding differs from the managed product endpoint');
|
|
143
163
|
}
|
|
144
164
|
}
|
|
@@ -18,6 +18,15 @@ function escapeXml(value) {
|
|
|
18
18
|
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// Presence is diagnostic only; enabling is an explicit probe + config operation.
|
|
22
|
+
export function runtimeObservations(paths, options = {}) {
|
|
23
|
+
const config = readConfig(paths.config);
|
|
24
|
+
return Object.fromEntries(['zcode', 'dsh', 'codex'].map((agent) => {
|
|
25
|
+
const runtime = agent === 'zcode' ? (options.zcodeRuntime ?? ZCODE_RUNTIME) : config.subagents[agent].runtime_path;
|
|
26
|
+
return [agent, { path: runtime, present: Boolean(runtime && fs.existsSync(runtime)), enabled: config.subagents[agent].enabled }];
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
export function launchAgentPlist(paths, options = {}) {
|
|
22
31
|
const daemon = nativeBinary(DAEMON_BIN_NAME);
|
|
23
32
|
const config = readConfig(paths.config);
|
package/cli/install/zcode.mjs
CHANGED
|
@@ -105,7 +105,7 @@ function verifyZcodeBinding({ config, staging, dir }) {
|
|
|
105
105
|
const binding = zcodeMcpBinding(staging);
|
|
106
106
|
if (!server || server.command !== binding.command || JSON.stringify(server.args || []) !== JSON.stringify(binding.args)
|
|
107
107
|
|| server.timeoutMs !== binding.timeoutMs
|
|
108
|
-
|| server.env?.
|
|
108
|
+
|| server.env?.EXTERNAL_SUBAGENT_SOCKET === undefined) {
|
|
109
109
|
throw new CliError('ZCODE_BINDING_UNVERIFIABLE', `the staged plugin MCP binding is not the managed endpoint (${staging})`);
|
|
110
110
|
}
|
|
111
111
|
return server;
|
|
@@ -148,7 +148,7 @@ export function installZcodePlugin(paths, options = {}) {
|
|
|
148
148
|
plugin_id: ZCODE_PLUGIN_ID,
|
|
149
149
|
digest: treeDigest(staging),
|
|
150
150
|
config_verified: true,
|
|
151
|
-
binding: { command: server.command, socket: server.env.
|
|
151
|
+
binding: { command: server.command, socket: server.env.EXTERNAL_SUBAGENT_SOCKET },
|
|
152
152
|
};
|
|
153
153
|
if (prior.doc.plugins?.enabled === false) {
|
|
154
154
|
result.warning = 'plugins.enabled is false in the ZCode config; the binding will not load until plugins are enabled';
|
package/cli/main.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { BUSINESS_COMMANDS, PRODUCT_NAME, VERSION, ZCODE_RUNTIME } from './const
|
|
|
5
5
|
import { CliError } from './errors.mjs';
|
|
6
6
|
import { installHooks, installPlan, runInit } from './install/init.mjs';
|
|
7
7
|
import { nativeBinary } from './install/layout.mjs';
|
|
8
|
-
import {
|
|
8
|
+
import { purge, restoreData, backupData, uninstall } from './commands/maintenance.mjs';
|
|
9
9
|
import { updateCommand } from './commands/update.mjs';
|
|
10
10
|
import { localInstallStatus, startDaemon, stopDaemon } from './commands/daemon.mjs';
|
|
11
11
|
import { serviceRegistrationStatus } from './install/service-macos.mjs';
|
|
@@ -17,7 +17,7 @@ import { parseConfigArgs } from './commands/config.mjs';
|
|
|
17
17
|
import { subagentsCommand, parseSubagentsArgs } from './commands/agents.mjs';
|
|
18
18
|
import { parseSpawnArgs, prepareSpawnInput } from './commands/tasks.mjs';
|
|
19
19
|
|
|
20
|
-
const HELP = `external-subagent ${VERSION}\n\nUsage: external-subagent <command> [options]\n\nCommands:\n help, version Show basic product information\n init [--dry-run] [--resume] [--install-hooks] [--skip-service-start]\n Install the standalone daemon service only; bind a\n host afterwards with install-plugin or install-mcp\n hooks install [--dry-run] Install ZCode policy hooks explicitly\n install-plugin [codex|zcode] [--dry-run|--uninstall] [--codex-home <path>]\n Install or remove the managed host plugin (MCP + skill);\n the host defaults to codex, --codex-home is codex-only\n install-mcp [--dry-run|--uninstall] [--codex-home <path>]\n Install or remove the direct Codex MCP TOML binding\n status, diagnose Inspect local service and runtime state\n start, stop Bootstrap or boot out the daemon LaunchAgent\n backup --output <dir> Back up retained product data\n restore --input <dir> Verify and restore product data\n uninstall Release Codex/ZCode bindings; remove service registration; retain data\n purge --yes Explicitly delete new product data\n
|
|
20
|
+
const HELP = `external-subagent ${VERSION}\n\nUsage: external-subagent <command> [options]\n\nCommands:\n help, version Show basic product information\n init [--dry-run] [--resume] [--install-hooks] [--skip-service-start]\n Install the standalone daemon service only; bind a\n host afterwards with install-plugin or install-mcp\n hooks install [--dry-run] Install ZCode policy hooks explicitly\n install-plugin [codex|zcode] [--dry-run|--uninstall] [--codex-home <path>]\n Install or remove the managed host plugin (MCP + skill);\n the host defaults to codex, --codex-home is codex-only\n install-mcp [--dry-run|--uninstall] [--codex-home <path>]\n Install or remove the direct Codex MCP TOML binding\n status, diagnose Inspect local service and runtime state\n start, stop Bootstrap or boot out the daemon LaunchAgent\n backup --output <dir> Back up retained product data\n restore --input <dir> Verify and restore product data\n uninstall Release Codex/ZCode bindings; remove service registration; retain data\n purge --yes Explicitly delete new product data\n`;
|
|
21
21
|
const DAEMON_HELP = ` config get [key] | config set <key> <value>\n subagents list | subagents status [subagent] | subagents probe/models [subagent]\n create/spawn, wait, list, send, respond, cancel, result, close, observe\n Daemon calls accept --json '<object>' or JSON stdin\n list JSON requires repository (workspace is an alias)\n observe JSON requires only agent_id\n`;
|
|
22
22
|
|
|
23
23
|
function structuredInput(args, parser) {
|
|
@@ -117,7 +117,7 @@ function boundedFailureRecord(record) {
|
|
|
117
117
|
function diagnosticTail(text) {
|
|
118
118
|
let incomplete = false;
|
|
119
119
|
const decoded = text.split('\n').map((line) => {
|
|
120
|
-
const match = line.match(/^(\[
|
|
120
|
+
const match = line.match(/^(\[external-subagentd\] failure agent=[^:\r\n]+: )(.+)$/u);
|
|
121
121
|
if (match && match[2].startsWith('{')) {
|
|
122
122
|
try {
|
|
123
123
|
const record = JSON.parse(match[2]);
|
|
@@ -166,7 +166,7 @@ function diagnosticLogs(logDirectory) {
|
|
|
166
166
|
let decodeStart = Math.max(0, start - 256) - readStart;
|
|
167
167
|
if (decodeStart > 0) {
|
|
168
168
|
const lineStart = buffer.lastIndexOf(0x0a, decodeStart - 1) + 1;
|
|
169
|
-
if (buffer.subarray(lineStart, read).toString('utf8').startsWith('[
|
|
169
|
+
if (buffer.subarray(lineStart, read).toString('utf8').startsWith('[external-subagentd] failure agent=')) decodeStart = lineStart;
|
|
170
170
|
}
|
|
171
171
|
// Legacy text keeps its original lookbehind; known records are decoded
|
|
172
172
|
// (or marked incomplete) before any display clipping can hide the prefix.
|
|
@@ -211,7 +211,7 @@ function agentDiagnosticLogs(logDirectory, agentId) {
|
|
|
211
211
|
if (lines.pop()) report.incomplete.push(`record_incomplete:${name}`); // The writer may still be appending.
|
|
212
212
|
for (const line of lines.reverse()) {
|
|
213
213
|
if (Buffer.byteLength(line) > DIAGNOSTIC_RECORD_BYTES) { report.incomplete.push(`record_truncated:${name}`); continue; }
|
|
214
|
-
const prefix = `[
|
|
214
|
+
const prefix = `[external-subagentd] failure agent=${agentId}: `;
|
|
215
215
|
if (!line.startsWith(prefix)) continue;
|
|
216
216
|
const raw = line.slice(prefix.length);
|
|
217
217
|
// JSON records repeat the identifier; reject misleading prefix matches.
|
|
@@ -256,7 +256,7 @@ function diagnoseInput(args) {
|
|
|
256
256
|
|
|
257
257
|
async function diagnose(paths, args) {
|
|
258
258
|
const { agent, outputDirectory } = diagnoseInput(args);
|
|
259
|
-
const socket = process.env.
|
|
259
|
+
const socket = process.env.EXTERNAL_SUBAGENT_SOCKET || paths.socket;
|
|
260
260
|
const report = {
|
|
261
261
|
schema_version: 1,
|
|
262
262
|
scope: agent ? { agent_id: agent } : { kind: 'global' },
|
|
@@ -381,7 +381,7 @@ export async function main(args) {
|
|
|
381
381
|
// keep it out of the ordinary status projection.
|
|
382
382
|
const { pid: _pid, ...service } = serviceRaw;
|
|
383
383
|
try {
|
|
384
|
-
output({ ...local, service, daemon_status: publicDaemonStatus(await callDaemon(process.env.
|
|
384
|
+
output({ ...local, service, daemon_status: publicDaemonStatus(await callDaemon(process.env.EXTERNAL_SUBAGENT_SOCKET || paths.socket, 'status', {}), { verbose }) });
|
|
385
385
|
} catch (error) {
|
|
386
386
|
output({ ...local, service, daemon_status: null, daemon_error: { code: error.code || 'DAEMON_ERROR', message: error.message } });
|
|
387
387
|
}
|
|
@@ -391,7 +391,7 @@ export async function main(args) {
|
|
|
391
391
|
const input = structuredInput(args.slice(1), command === 'config' ? parseConfigArgs : parseSubagentsArgs);
|
|
392
392
|
output(command === 'config'
|
|
393
393
|
? configCommand(paths, input)
|
|
394
|
-
: await subagentsCommand(paths, input, { callDaemon, socket: process.env.
|
|
394
|
+
: await subagentsCommand(paths, input, { callDaemon, socket: process.env.EXTERNAL_SUBAGENT_SOCKET || paths.socket }));
|
|
395
395
|
return;
|
|
396
396
|
}
|
|
397
397
|
if (command === 'diagnose') {
|
|
@@ -405,16 +405,12 @@ export async function main(args) {
|
|
|
405
405
|
if (command === 'restore') { output(restoreData(value(args, '--input'), paths)); return; }
|
|
406
406
|
if (command === 'start') { output(startDaemon(paths)); return; }
|
|
407
407
|
if (command === 'stop') { output(stopDaemon(paths)); return; }
|
|
408
|
-
if (command === 'update' || command === 'reconcile') { output(await updateCommand(paths, command === 'reconcile' ? ['reconcile', ...args.slice(1)] : args.slice(1), { socket: process.env.
|
|
408
|
+
if (command === 'update' || command === 'reconcile') { output(await updateCommand(paths, command === 'reconcile' ? ['reconcile', ...args.slice(1)] : args.slice(1), { socket: process.env.EXTERNAL_SUBAGENT_SOCKET || paths.socket })); return; }
|
|
409
409
|
if (command === 'uninstall') { output(uninstall(paths)); return; }
|
|
410
410
|
if (command === 'purge') {
|
|
411
411
|
if (!args.includes('--yes')) throw new CliError('CONFIRMATION_REQUIRED', 'purge requires --yes');
|
|
412
412
|
output(purge(paths)); return;
|
|
413
413
|
}
|
|
414
|
-
if (command === 'cleanup-legacy') {
|
|
415
|
-
if (!args.includes('--yes')) throw new CliError('CONFIRMATION_REQUIRED', 'cleanup-legacy requires --yes');
|
|
416
|
-
output(cleanupLegacy(paths.home)); return;
|
|
417
|
-
}
|
|
418
414
|
let input;
|
|
419
415
|
if (command === 'create' || command === 'spawn') {
|
|
420
416
|
const spawnArgs = args.slice(1);
|
|
@@ -422,7 +418,7 @@ export async function main(args) {
|
|
|
422
418
|
? prepareSpawnInput(parseDaemonInput(spawnArgs))
|
|
423
419
|
: parseSpawnArgs(spawnArgs);
|
|
424
420
|
} else input = parseDaemonInput(args.slice(1));
|
|
425
|
-
const result = await callDaemon(process.env.
|
|
421
|
+
const result = await callDaemon(process.env.EXTERNAL_SUBAGENT_SOCKET || paths.socket, command, input);
|
|
426
422
|
output({ command, result });
|
|
427
423
|
}
|
|
428
424
|
|
package/cli/maintenance.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { CliError } from './errors.mjs';
|
|
4
4
|
import { jsonBytes, sha256 } from './fs-atomic.mjs';
|
|
5
|
-
import {
|
|
5
|
+
import { productPaths } from './paths.mjs';
|
|
6
6
|
import { bootoutService } from './install/service-macos.mjs';
|
|
7
7
|
|
|
8
8
|
function copyTree(source, destination, records, root = source) {
|
|
@@ -97,8 +97,3 @@ export function uninstall(paths = productPaths(), options = {}) {
|
|
|
97
97
|
export function purge(paths = productPaths()) {
|
|
98
98
|
return { purged: removeOne(paths.data), logs_purged: removeOne(paths.logs) };
|
|
99
99
|
}
|
|
100
|
-
|
|
101
|
-
export function cleanupLegacy(home) {
|
|
102
|
-
const removed = legacyPaths(home).filter(removeOne);
|
|
103
|
-
return { migration: false, aliases_created: [], removed };
|
|
104
|
-
}
|
package/cli/paths.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { LAUNCH_AGENT_LABEL, PRODUCT_NAME } from './constants.mjs';
|
|
|
4
4
|
import { PLUGIN_NAME } from './install/layout.mjs';
|
|
5
5
|
|
|
6
6
|
export function platform() {
|
|
7
|
-
return process.env.
|
|
7
|
+
return process.env.EXTERNAL_SUBAGENT_TEST_PLATFORM || process.platform;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
// State paths derive from PRODUCT_NAME, so the debug variant gets a fully
|
|
@@ -26,15 +26,3 @@ export function productPaths(home = os.homedir()) {
|
|
|
26
26
|
zcodePlugin: path.join(data, 'zcode-plugin', PLUGIN_NAME),
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
-
|
|
30
|
-
export function legacyPaths(home = os.homedir()) {
|
|
31
|
-
return [
|
|
32
|
-
path.join(home, 'Library', 'LaunchAgents', 'com.zcode-reviewd.plist'),
|
|
33
|
-
path.join(home, 'Library', 'LaunchAgents', 'com.zcode-review-mcp.plist'),
|
|
34
|
-
path.join(home, 'Library', 'Logs', 'zcode-reviewd'),
|
|
35
|
-
path.join(home, 'Library', 'Application Support', 'zcode-review-mcp'),
|
|
36
|
-
path.join(home, 'Library', 'Application Support', 'zcode-reviewd'),
|
|
37
|
-
path.join(home, '.local', 'bin', 'zcode-reviewd'),
|
|
38
|
-
path.join(home, '.local', 'bin', 'zcode-review-mcp'),
|
|
39
|
-
];
|
|
40
|
-
}
|
package/cli/rpc.mjs
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
3
|
"product": "external-subagent",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.3",
|
|
5
5
|
"platform": "darwin-arm64",
|
|
6
6
|
"files": [
|
|
7
7
|
{
|
|
8
8
|
"name": "external-subagentd",
|
|
9
|
-
"bytes":
|
|
9
|
+
"bytes": 9331504,
|
|
10
10
|
"mode": "755",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "8f8625ccf600f6a4c252318ab7b67cc6f111d0e19282edd759f02a3267fb7a14"
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
14
|
"name": "external-subagent-mcp",
|
|
15
15
|
"bytes": 937840,
|
|
16
16
|
"mode": "755",
|
|
17
|
-
"sha256": "
|
|
17
|
+
"sha256": "5d933f567572c570b76733cb8eccae6eb08d3a4e84c239cc06b334bd760f4008"
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
20
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "external-subagent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Manage bounded ZCode, DeepSeek Harness (DSH), and Codex subagent tasks through a local MCP service for any local host (Codex, ZCode, or a custom MCP client)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Mirrors crates/external-mcp (the native facade) byte for byte in
|
|
5
5
|
// behavior: resolve the daemon MCP stream endpoint from
|
|
6
|
-
//
|
|
6
|
+
// EXTERNAL_SUBAGENT_SOCKET, connect, and pipe client stdio <-> socket until
|
|
7
7
|
// either side reaches EOF. Hosts that spawn MCP servers inside
|
|
8
8
|
// restricted execution contexts (ZCode kills ad-hoc-signed native
|
|
9
9
|
// binaries there, while node processes and unix sockets are allowed)
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
import net from 'node:net';
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
|
|
15
|
-
const socket = process.env.
|
|
15
|
+
const socket = process.env.EXTERNAL_SUBAGENT_SOCKET;
|
|
16
16
|
if (!socket || !path.isAbsolute(socket)) {
|
|
17
|
-
console.error('mcp-stdio-bridge:
|
|
17
|
+
console.error('mcp-stdio-bridge: EXTERNAL_SUBAGENT_SOCKET must be an absolute path');
|
|
18
18
|
process.exit(1);
|
|
19
19
|
}
|
|
20
20
|
// The stable daemon RPC socket keeps its name; MCP is exposed by the
|
|
@@ -25,7 +25,7 @@ Spawn accepts an optional `effort` token that steers the task's reasoning
|
|
|
25
25
|
effort. Omit it to keep each agent's current default (codex keeps its existing
|
|
26
26
|
wire default; zcode and dsh send no effort field or call at all). `codex`
|
|
27
27
|
admits only the closed set
|
|
28
|
-
`low | medium | high | xhigh` — `minimal` and `
|
|
28
|
+
`low | medium | high | xhigh | max` — `minimal` and `ultra` are rejected;
|
|
29
29
|
`zcode` and `dsh` accept any bounded token (1..24 bytes of `[a-z0-9_]`) and
|
|
30
30
|
pass it through to the runtime, because the supported set is only known at
|
|
31
31
|
runtime. A malformed or unsupported token is rejected before dispatch, so no
|
|
@@ -40,16 +40,23 @@ Codex home instances, and offers plugin or direct MCP bindings; built-in
|
|
|
40
40
|
dirs with a single per-user instance. This is separate
|
|
41
41
|
from `subagent.codex`. Protocol implementations are adapters.
|
|
42
42
|
|
|
43
|
+
All three subagents (`zcode`, `dsh`, `codex`) are disabled by default; enable one
|
|
44
|
+
with `external-subagent agents enable <name>`, which runs a local probe and writes
|
|
45
|
+
the configuration only on success (a daemon restart is required for `dsh`/`codex`).
|
|
43
46
|
Actual per-subagent limitations (from the accepted code):
|
|
44
47
|
|
|
45
|
-
- `zcode` —
|
|
48
|
+
- `zcode` — disabled until explicitly enabled; all four permission modes
|
|
46
49
|
(build/edit/plan/yolo); an explicit spawn `model` is rejected
|
|
47
50
|
(`model_selection_unsupported`).
|
|
48
51
|
- `dsh` — disabled until explicitly enabled and configured
|
|
49
52
|
(`runtime_path`/`home`/`profile`/`version`); admits only `build` and strict
|
|
50
53
|
`plan`.
|
|
51
|
-
- `codex` — disabled
|
|
52
|
-
|
|
53
|
-
`
|
|
54
|
-
- `
|
|
55
|
-
|
|
54
|
+
- `codex` — disabled until explicitly enabled; all four permission modes:
|
|
55
|
+
`build`/`edit` map to sandbox=workspace-write, `plan` to sandbox=read-only,
|
|
56
|
+
and `yolo` to sandbox=danger-full-access, all pinning approvalPolicy=never.
|
|
57
|
+
A non-empty spawn `write_manifest` is rejected before task creation with
|
|
58
|
+
`codex_write_manifest_unsupported`.
|
|
59
|
+
- `observe` — available for all three subagents; `zcode` and `dsh` return the
|
|
60
|
+
public reasoning tail (at most 200 characters), while `codex` does not collect
|
|
61
|
+
reasoning and returns `reasoning: null`. Tool history and coverage are reported
|
|
62
|
+
truthfully per adapter capability.
|
|
@@ -62,15 +62,19 @@
|
|
|
62
62
|
"type": "object",
|
|
63
63
|
"required": ["repository", "prompt"],
|
|
64
64
|
"properties": {
|
|
65
|
-
"subagent": { "type": "string", "minLength": 1, "description": "Subagent route key; omission uses configured default_subagent or returns subagent_required. Null is invalid; dsh production spawn
|
|
65
|
+
"subagent": { "type": "string", "minLength": 1, "description": "Subagent route key; omission uses configured default_subagent or returns subagent_required. Null is invalid; dsh production spawn requires its enabled + spawn_supported + pinned-runtime configuration." },
|
|
66
66
|
"repository": { "type": "string", "minLength": 1 },
|
|
67
|
-
"permission_mode": { "$ref": "#/properties/permission_mode" },
|
|
67
|
+
"permission_mode": { "$ref": "#/properties/permission_mode", "description": "codex supports build/edit (workspace-write), plan (read-only), and yolo (danger-full-access), always with approvalPolicy=never." },
|
|
68
68
|
"prompt": { "type": "string", "minLength": 1 },
|
|
69
69
|
"model": { "type": "string", "minLength": 1, "description": "Task model. dsh requires provider:model, split at the first colon (the model side may contain further colons; a missing colon or an empty side is rejected before any task). codex uses its own model id; zcode rejects model selection." },
|
|
70
70
|
"effort": { "type": "string", "minLength": 1, "maxLength": 24, "pattern": "^[a-z0-9_]+$", "description": "Reasoning-effort token: 1..24 bytes of [a-z0-9_]. codex admits only low, medium, high or xhigh; zcode and dsh pass the bounded token through. Omission keeps the runtime default." },
|
|
71
|
-
"write_manifest": { "type": "array", "items": { "type": "string", "minLength": 1 } }
|
|
71
|
+
"write_manifest": { "type": "array", "items": { "type": "string", "minLength": 1 }, "description": "codex requires an empty or omitted write_manifest; non-empty input returns codex_write_manifest_unsupported before task creation. dsh admits a non-empty manifest in build through the guarded manifest-build composition (workspace-write sandbox, only tool-fs writable, out-of-manifest paths rejected as FS_WRITE_MANIFEST_DENIED), at most 256 entries; an explicit [\".\"] keeps the existing build composition. plan requires an empty manifest for every subagent." }
|
|
72
72
|
},
|
|
73
73
|
"allOf": [
|
|
74
|
+
{ "if": { "required": ["subagent"], "properties": { "subagent": { "const": "codex" } } },
|
|
75
|
+
"then": { "properties": { "write_manifest": { "maxItems": 0 } } } },
|
|
76
|
+
{ "if": { "required": ["subagent"], "properties": { "subagent": { "const": "dsh" } } },
|
|
77
|
+
"then": { "properties": { "write_manifest": { "maxItems": 256 } } } },
|
|
74
78
|
{ "if": { "required": ["permission_mode"], "properties": { "permission_mode": { "const": "plan" } } },
|
|
75
79
|
"then": { "properties": { "write_manifest": { "maxItems": 0 } } } }
|
|
76
80
|
],
|
|
@@ -101,6 +105,7 @@
|
|
|
101
105
|
"additionalProperties": false
|
|
102
106
|
},
|
|
103
107
|
"observe": {
|
|
108
|
+
"description": "Read captured task observations for zcode, dsh or codex. zcode and dsh expose at most 200 Unicode characters of public reasoning; codex reasoning is null. Coverage reports capture gaps and unsupported history truthfully.",
|
|
104
109
|
"type": "object",
|
|
105
110
|
"required": ["agent_id"],
|
|
106
111
|
"properties": {
|
|
@@ -121,7 +126,7 @@
|
|
|
121
126
|
},
|
|
122
127
|
"result_projection": {
|
|
123
128
|
"type": "object",
|
|
124
|
-
"required": ["outcome", "final_text", "partial", "offset", "total_bytes", "next_offset", "complete"],
|
|
129
|
+
"required": ["outcome", "final_text", "partial", "offset", "total_bytes", "next_offset", "complete", "reason_code"],
|
|
125
130
|
"properties": {
|
|
126
131
|
"outcome": { "enum": ["COMPLETED", "FAILED", "CANCELLED", "TIMED_OUT", "RUNTIME_LOST", "RESULT_INVALID"] },
|
|
127
132
|
"final_text": { "type": "string" },
|
|
@@ -129,7 +134,8 @@
|
|
|
129
134
|
"offset": { "type": "integer", "minimum": 0 },
|
|
130
135
|
"total_bytes": { "type": "integer", "minimum": 0 },
|
|
131
136
|
"next_offset": { "type": ["integer", "null"], "minimum": 0 },
|
|
132
|
-
"complete": { "type": "boolean" }
|
|
137
|
+
"complete": { "type": "boolean" },
|
|
138
|
+
"reason_code": { "type": ["string", "null"] }
|
|
133
139
|
},
|
|
134
140
|
"additionalProperties": false
|
|
135
141
|
},
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "external-subagent-public-reasoning-source/1",
|
|
3
|
+
"subagents": {
|
|
4
|
+
"zcode": {
|
|
5
|
+
"public": true,
|
|
6
|
+
"status": "VERIFIED_RUNTIME_PUBLIC",
|
|
7
|
+
"runtime_path": "/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs",
|
|
8
|
+
"runtime_version": "3.11.2",
|
|
9
|
+
"runtime_sha256": "e9f1868c0fdb863537ed910ee3828b9be96b8c2fd805473f63b439e1113266b8",
|
|
10
|
+
"wire_method": "session/event",
|
|
11
|
+
"event_type_pointer": "/params/type",
|
|
12
|
+
"event_type": "model.streaming",
|
|
13
|
+
"event_discriminator_pointer": "/params/payload/kind",
|
|
14
|
+
"event_discriminator_value": "reasoning_delta",
|
|
15
|
+
"delta_pointer": "/params/payload/delta",
|
|
16
|
+
"explicitly_excluded_key": "encrypted_content"
|
|
17
|
+
},
|
|
18
|
+
"dsh": {
|
|
19
|
+
"public": true,
|
|
20
|
+
"status": "VERIFIED_PROTOCOL_PUBLIC",
|
|
21
|
+
"runtime_version": "acp/1",
|
|
22
|
+
"wire_method": "session/update",
|
|
23
|
+
"event_type": "agent_thought_chunk",
|
|
24
|
+
"delta_pointer": "/params/update/content/text",
|
|
25
|
+
"canonical_event_type": "model.streaming",
|
|
26
|
+
"canonical_delta_pointer": "/params/payload/delta",
|
|
27
|
+
"explicitly_excluded_key": "encrypted_content"
|
|
28
|
+
},
|
|
29
|
+
"codex": {
|
|
30
|
+
"public": false,
|
|
31
|
+
"status": "HIDDEN",
|
|
32
|
+
"reasoning": null,
|
|
33
|
+
"collected": false
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -29,6 +29,14 @@ if (variant !== '' && variant !== 'debug') {
|
|
|
29
29
|
process.exit(2);
|
|
30
30
|
}
|
|
31
31
|
const isDebug = variant === 'debug';
|
|
32
|
+
// The cargo profile is release by default so prepack/prepublish keep shipping
|
|
33
|
+
// release bytes; EXTERNAL_SUBAGENT_CARGO_PROFILE=debug selects the development
|
|
34
|
+
// profile without changing the payload/product naming.
|
|
35
|
+
const cargoProfile = process.env.EXTERNAL_SUBAGENT_CARGO_PROFILE || 'release';
|
|
36
|
+
if (cargoProfile !== 'release' && cargoProfile !== 'debug') {
|
|
37
|
+
process.stderr.write(`unknown cargo profile: ${cargoProfile} (supported: release, debug)\n`);
|
|
38
|
+
process.exit(2);
|
|
39
|
+
}
|
|
32
40
|
const nativeDirName = isDebug ? 'native-debug' : 'native';
|
|
33
41
|
const productName = isDebug ? 'external-subagent-debug' : 'external-subagent';
|
|
34
42
|
const binaries = isDebug
|
|
@@ -66,6 +74,7 @@ function stagedIsCurrent(version) {
|
|
|
66
74
|
let manifest;
|
|
67
75
|
try { manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); } catch { return false; }
|
|
68
76
|
if (manifest.version !== version || manifest.platform !== 'darwin-arm64' || manifest.product !== productName) return false;
|
|
77
|
+
if ((manifest.profile ?? 'release') !== cargoProfile) return false;
|
|
69
78
|
const inputs = newestInputMs();
|
|
70
79
|
for (const file of manifest.files) {
|
|
71
80
|
const target = path.join(platformDir, file.name);
|
|
@@ -82,24 +91,27 @@ const version = packageVersion();
|
|
|
82
91
|
// diagnostics go to stderr there so pack output stays machine-parseable.
|
|
83
92
|
const note = (text) => (process.env.npm_lifecycle_event ? process.stderr : process.stdout).write(text);
|
|
84
93
|
if (process.argv.includes('--if-stale') && stagedIsCurrent(version)) {
|
|
85
|
-
note(`native payload already current at ${version}${isDebug ? ' (debug)' : ''}\n`);
|
|
94
|
+
note(`native payload already current at ${version}${isDebug ? ' (debug)' : ''}${cargoProfile === 'release' ? '' : ` [${cargoProfile}]`}\n`);
|
|
86
95
|
process.exit(0);
|
|
87
96
|
}
|
|
88
97
|
|
|
89
|
-
const
|
|
98
|
+
const cargoArguments = ['build'];
|
|
99
|
+
if (cargoProfile === 'release') cargoArguments.push('--release');
|
|
100
|
+
cargoArguments.push('-p', 'external-daemon', '-p', 'external-mcp');
|
|
101
|
+
const build = spawnSync('cargo', cargoArguments, {
|
|
90
102
|
cwd: packageRoot, stdio: 'inherit',
|
|
91
103
|
});
|
|
92
104
|
if (build.status !== 0) {
|
|
93
|
-
process.stderr.write(
|
|
105
|
+
process.stderr.write(`cargo ${cargoProfile} build failed\n`);
|
|
94
106
|
process.exit(build.status ?? 1);
|
|
95
107
|
}
|
|
96
108
|
|
|
97
109
|
fs.mkdirSync(platformDir, { recursive: true });
|
|
98
110
|
const files = [];
|
|
99
111
|
for (const [source, name] of binaries) {
|
|
100
|
-
const sourcePath = path.join(packageRoot, 'target',
|
|
112
|
+
const sourcePath = path.join(packageRoot, 'target', cargoProfile, source);
|
|
101
113
|
if (!fs.existsSync(sourcePath)) {
|
|
102
|
-
process.stderr.write(
|
|
114
|
+
process.stderr.write(`${cargoProfile} binary missing: ${sourcePath}\n`);
|
|
103
115
|
process.exit(1);
|
|
104
116
|
}
|
|
105
117
|
const target = path.join(platformDir, name);
|
|
@@ -109,11 +121,15 @@ for (const [source, name] of binaries) {
|
|
|
109
121
|
files.push({ name, bytes: bytes.length, mode: '755', sha256: sha256(bytes) });
|
|
110
122
|
}
|
|
111
123
|
|
|
112
|
-
const manifest = { schema_version: 1, product: productName, version, platform: 'darwin-arm64'
|
|
124
|
+
const manifest = { schema_version: 1, product: productName, version, platform: 'darwin-arm64' };
|
|
125
|
+
// The debug profile is recorded so --if-stale can tell a debug-staged payload
|
|
126
|
+
// from a release one; the release manifest stays byte-identical to before.
|
|
127
|
+
if (cargoProfile !== 'release') manifest.profile = cargoProfile;
|
|
128
|
+
manifest.files = files;
|
|
113
129
|
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o644 });
|
|
114
130
|
if (isDebug) {
|
|
115
131
|
const stagedPlugin = stageDebugPlugin(packageRoot);
|
|
116
132
|
note(`native payload ${version} (debug) staged: ${files.map((file) => file.name).join(', ')}\nplugin source staged: ${stagedPlugin}\n`);
|
|
117
133
|
} else {
|
|
118
|
-
note(`native payload ${version} staged: ${files.map((file) => file.name).join(', ')}\n`);
|
|
134
|
+
note(`native payload ${version} staged: ${files.map((file) => file.name).join(', ')}${cargoProfile === 'release' ? '' : ` [${cargoProfile}]`}\n`);
|
|
119
135
|
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"schema": "zas-public-reasoning-source/1",
|
|
3
|
-
"status": "VERIFIED_RUNTIME_PUBLIC",
|
|
4
|
-
"runtime_path": "/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs",
|
|
5
|
-
"runtime_version": "3.11.2",
|
|
6
|
-
"runtime_sha256": "e9f1868c0fdb863537ed910ee3828b9be96b8c2fd805473f63b439e1113266b8",
|
|
7
|
-
"wire_method": "session/event",
|
|
8
|
-
"event_type_pointer": "/params/type",
|
|
9
|
-
"event_type": "model.streaming",
|
|
10
|
-
"event_discriminator_pointer": "/params/payload/kind",
|
|
11
|
-
"event_discriminator_value": "reasoning_delta",
|
|
12
|
-
"delta_pointer": "/params/payload/delta",
|
|
13
|
-
"explicitly_excluded_key": "encrypted_content"
|
|
14
|
-
}
|