external-subagent 0.1.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/LICENSE +21 -0
- package/README.md +124 -0
- package/bin/external-subagent-debug-mcp.mjs +5 -0
- package/bin/external-subagent-debug.mjs +13 -0
- package/bin/external-subagent-mcp.mjs +20 -0
- package/bin/external-subagent.mjs +8 -0
- package/bin/zas.mjs +8 -0
- package/cli/commands/agents.mjs +117 -0
- package/cli/commands/config.mjs +117 -0
- package/cli/commands/daemon.mjs +36 -0
- package/cli/commands/maintenance.mjs +27 -0
- package/cli/commands/plugin.mjs +96 -0
- package/cli/commands/tasks.mjs +64 -0
- package/cli/commands/update.mjs +219 -0
- package/cli/config/read.mjs +24 -0
- package/cli/config/schema.mjs +75 -0
- package/cli/config/write.mjs +39 -0
- package/cli/constants.mjs +24 -0
- package/cli/errors.mjs +7 -0
- package/cli/fs-atomic.mjs +36 -0
- package/cli/install/codex.mjs +352 -0
- package/cli/install/init.mjs +206 -0
- package/cli/install/layout.mjs +54 -0
- package/cli/install/npm-hook.mjs +49 -0
- package/cli/install/path.mjs +43 -0
- package/cli/install/payload.mjs +84 -0
- package/cli/install/plugin-stage.mjs +232 -0
- package/cli/install/reconcile.mjs +209 -0
- package/cli/install/recovery.mjs +79 -0
- package/cli/install/service-activation.mjs +133 -0
- package/cli/install/service-macos.mjs +146 -0
- package/cli/install/update.mjs +170 -0
- package/cli/install/zcode.mjs +239 -0
- package/cli/main.mjs +429 -0
- package/cli/maintenance.mjs +104 -0
- package/cli/paths.mjs +40 -0
- package/cli/rpc.mjs +261 -0
- package/launchd/com.external-subagent.daemon.plist.template +37 -0
- package/npm/native/README.md +19 -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 +20 -0
- package/package.json +60 -0
- package/plugins/codex/external-subagent/.codex-plugin/plugin.json +17 -0
- package/plugins/codex/external-subagent/.mcp.json +9 -0
- package/plugins/codex/external-subagent/hooks/audit-bash-result.mjs +55 -0
- package/plugins/codex/external-subagent/hooks/check-agent-files.mjs +19 -0
- package/plugins/codex/external-subagent/hooks/guard-bash.mjs +15 -0
- package/plugins/codex/external-subagent/lib/agent-file-policy.mjs +206 -0
- package/plugins/codex/external-subagent/scripts/install-agent-hooks.mjs +149 -0
- package/plugins/codex/external-subagent/scripts/mcp-stdio-bridge.mjs +36 -0
- package/plugins/codex/external-subagent/scripts/policy-verifier.mjs +44 -0
- package/plugins/codex/external-subagent/skills/external-subagent/SKILL.md +55 -0
- package/schema/external-subagent-public-api.json +185 -0
- package/schema/zas-observation-v1.1.schema.json +58 -0
- package/schema/zas-public-reasoning-source.json +14 -0
- package/scripts/release/build-native-payload.mjs +119 -0
- package/scripts/release/check-native-tarball.mjs +125 -0
- package/scripts/release/stage-debug-plugin.mjs +60 -0
- package/scripts/release/test-installed-tarball.mjs +73 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 iBobby
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# external-subagent
|
|
2
|
+
|
|
3
|
+
A local service that manages bounded, repository-scoped external subagent
|
|
4
|
+
tasks and exposes them over MCP. Hosts (the applications that call the MCP
|
|
5
|
+
tools) and subagents (the execution targets that run the tasks) are two
|
|
6
|
+
independent axes:
|
|
7
|
+
|
|
8
|
+
- **Hosts** — Codex (`install-plugin codex` or `install-mcp`), ZCode
|
|
9
|
+
(`install-plugin zcode`), and any custom local MCP client that connects
|
|
10
|
+
directly to the facade without registration.
|
|
11
|
+
- **Subagents** — ZCode, DeepSeek Harness (DSH), and Codex, each routed
|
|
12
|
+
through an internal adapter and subject to the per-subagent limitations
|
|
13
|
+
listed in [docs/product.md](docs/product.md) and
|
|
14
|
+
[docs/mcp-api.md](docs/mcp-api.md).
|
|
15
|
+
|
|
16
|
+
## Status
|
|
17
|
+
|
|
18
|
+
Current work happens on the `audit/external-subagent-20260917` branch; the
|
|
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
|
|
42
|
+
`external-subagent@0.1.0` (macOS arm64 only; `os`/`cpu` in `package.json`
|
|
43
|
+
reject installation on other platforms at npm install time).
|
|
44
|
+
|
|
45
|
+
## Versioning
|
|
46
|
+
|
|
47
|
+
The product version has a single source: `package.json` (`0.1.0`). The CLI
|
|
48
|
+
constant (`cli/constants.mjs`), the daemon crates, and the native payload
|
|
49
|
+
manifest all carry the same version, and `scripts/release/check-native-tarball.mjs`
|
|
50
|
+
enforces payload/package agreement. The managed Codex plugin manifest
|
|
51
|
+
(`plugins/codex/external-subagent/.codex-plugin/plugin.json`, currently
|
|
52
|
+
`0.1.3`) is deliberately **not** tied to the product version: it is codex's
|
|
53
|
+
plugin-cache identity (`plugin@marketplace@version`; under the reserved
|
|
54
|
+
`personal` marketplace name codex resolves the real user root
|
|
55
|
+
machine-globally, ignoring `CODEX_HOME`), which
|
|
56
|
+
must carry a fresh version per released candidate (`0.1.1` for C1, `0.1.2`
|
|
57
|
+
for C2, `0.1.3` for the boundary-fixes candidate) so no home silently
|
|
58
|
+
receives another installation's cached bytes. The two numbers therefore
|
|
59
|
+
intentionally diverge; see
|
|
60
|
+
[docs/operations.md](docs/operations.md) for the full rule.
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
npm install -g external-subagent # stages package + payload only (macOS arm64)
|
|
66
|
+
external-subagent init # explicit: standalone daemon service only
|
|
67
|
+
external-subagent install-plugin zcode # optional host binding (or: install-plugin codex / install-mcp)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Packing and publishing are gated by npm lifecycle scripts: `prepack` always
|
|
71
|
+
rebuilds the native payload from source (so a tarball can never silently ship
|
|
72
|
+
stale or missing binaries) and `postpack` runs the static tarball checks. A
|
|
73
|
+
development checkout packs the same way (`npm pack` after `cargo` is
|
|
74
|
+
available); see [docs/operations.md](docs/operations.md) for release checks.
|
|
75
|
+
|
|
76
|
+
## Publishing
|
|
77
|
+
|
|
78
|
+
Releases publish from CI via npm Trusted Publishing (OIDC): pushing a `vX.Y.Z`
|
|
79
|
+
tag runs [.github/workflows/npm-publish.yml](.github/workflows/npm-publish.yml)
|
|
80
|
+
on an Apple Silicon runner, which rebuilds the payload, runs the pack gates,
|
|
81
|
+
and publishes with no npm token in the environment — the runner's OIDC
|
|
82
|
+
identity is the credential. npm does not support trusted publishing for a
|
|
83
|
+
package's first release, so the initial version was published manually once
|
|
84
|
+
and the trusted publisher was then linked on npmjs.com; the workflow filename
|
|
85
|
+
is part of that link and must not be renamed.
|
|
86
|
+
|
|
87
|
+
A plain install stages the package and payload only. `init` never binds a
|
|
88
|
+
host or touches Codex state; host binding is a separate explicit step
|
|
89
|
+
(`install-plugin codex|zcode`, or `install-mcp` for the direct Codex TOML
|
|
90
|
+
binding), and any custom local MCP client may connect without either. On an
|
|
91
|
+
already initialized installation, npm's postinstall hook detects a local
|
|
92
|
+
version drift and reuses
|
|
93
|
+
the existing reconcile/update owner; it never initializes a fresh home or
|
|
94
|
+
probes a provider. A same-version reinstall does not drift and therefore
|
|
95
|
+
does not trigger that update path — rebind hosts explicitly with
|
|
96
|
+
`external-subagent reconcile` and restart the service per
|
|
97
|
+
[docs/operations.md](docs/operations.md) instead of expecting npm reinstall
|
|
98
|
+
to refresh a running daemon. `init` remains the explicit first activation
|
|
99
|
+
step. A
|
|
100
|
+
completed update whose registered Codex homes only partially rebind reports
|
|
101
|
+
`CODEX_SYNC_PARTIAL` per home and keeps the verified activation; the public
|
|
102
|
+
`stop` confirms launchd removal before returning, and `uninstall` boots out
|
|
103
|
+
the ES-owned service before removing its registration while retaining all
|
|
104
|
+
data. Supported platform: macOS arm64 — `os`/`cpu` in `package.json` make npm
|
|
105
|
+
refuse installation elsewhere up front; the CLI additionally keeps
|
|
106
|
+
`help`/`version` working on other platforms and rejects business commands
|
|
107
|
+
without writing HOME.
|
|
108
|
+
|
|
109
|
+
See [docs/operations.md](docs/operations.md) for service control, PATH
|
|
110
|
+
behavior, the Codex homes registry, backup/removal, and release checks; and
|
|
111
|
+
[docs/compatibility/codex.md](docs/compatibility/codex.md) for the verified
|
|
112
|
+
codex-cli interface. See [docs/product.md](docs/product.md) for the product
|
|
113
|
+
workflow, provider configuration, and CLI/MCP acceptance boundary.
|
|
114
|
+
|
|
115
|
+
See [docs/mcp-api.md](docs/mcp-api.md) for the complete MCP tool reference,
|
|
116
|
+
field usage, defaults, design rationale, and omission/removal impacts.
|
|
117
|
+
|
|
118
|
+
## Development
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
cargo test --workspace
|
|
122
|
+
node --test tests/install/*.test.mjs tests/platform/*.test.mjs tests/cli/*.test.mjs tests/contract/*.test.mjs
|
|
123
|
+
node --test tests/integration/*.test.mjs tools/probes/dsh-acp/probe.test.mjs
|
|
124
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Debug-variant CLI entry: selects the parallel external-subagent-debug
|
|
3
|
+
// installation (own payload, state directory, LaunchAgent, plugin identity,
|
|
4
|
+
// and socket) before the CLI derives any name from the variant token. The
|
|
5
|
+
// release installation is never touched by commands run through this shim.
|
|
6
|
+
process.env.EXTERNAL_SUBAGENT_VARIANT = 'debug';
|
|
7
|
+
const { main } = await import('../cli/main.mjs');
|
|
8
|
+
|
|
9
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
10
|
+
const code = typeof error?.code === 'string' ? error.code : 'INTERNAL_ERROR';
|
|
11
|
+
process.stderr.write(`${JSON.stringify({ ok: false, error: { code, message: error.message, ...(error.agentId ? { agent_id: error.agentId } : {}), ...(Number.isInteger(error.promptCount) ? { prompt_count: error.promptCount } : {}) } })}\n`);
|
|
12
|
+
process.exitCode = Number.isInteger(error?.exitCode) ? error.exitCode : 1;
|
|
13
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
8
|
+
// The debug shims set EXTERNAL_SUBAGENT_VARIANT before re-entering this file,
|
|
9
|
+
// so the facade resolves to the variant payload staged beside the release one.
|
|
10
|
+
const debug = process.env.EXTERNAL_SUBAGENT_VARIANT === 'debug';
|
|
11
|
+
const facade = path.join(packageRoot, 'npm', debug ? 'native-debug' : 'native', 'darwin-arm64',
|
|
12
|
+
debug ? 'external-subagent-debug-mcp' : 'external-subagent-mcp');
|
|
13
|
+
const child = spawn(facade, process.argv.slice(2), { stdio: 'inherit', env: process.env });
|
|
14
|
+
child.on('error', (error) => {
|
|
15
|
+
console.error(error.message);
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
});
|
|
18
|
+
child.on('exit', (code, signal) => {
|
|
19
|
+
process.exitCode = code ?? (signal ? 1 : 0);
|
|
20
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { main } from '../cli/main.mjs';
|
|
3
|
+
|
|
4
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
5
|
+
const code = typeof error?.code === 'string' ? error.code : 'INTERNAL_ERROR';
|
|
6
|
+
process.stderr.write(`${JSON.stringify({ ok: false, error: { code, message: error.message, ...(error.agentId ? { agent_id: error.agentId } : {}), ...(Number.isInteger(error.promptCount) ? { prompt_count: error.promptCount } : {}) } })}\n`);
|
|
7
|
+
process.exitCode = Number.isInteger(error?.exitCode) ? error.exitCode : 1;
|
|
8
|
+
});
|
package/bin/zas.mjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { main } from '../cli/main.mjs';
|
|
3
|
+
|
|
4
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
5
|
+
const code = typeof error?.code === 'string' ? error.code : 'INTERNAL_ERROR';
|
|
6
|
+
process.stderr.write(`${JSON.stringify({ ok: false, error: { code, message: error.message, ...(error.agentId ? { agent_id: error.agentId } : {}) } })}\n`);
|
|
7
|
+
process.exitCode = Number.isInteger(error?.exitCode) ? error.exitCode : 1;
|
|
8
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { readConfig } from '../config/read.mjs';
|
|
2
|
+
import { CliError } from '../errors.mjs';
|
|
3
|
+
|
|
4
|
+
const INPUT_FIELDS = new Set(['operation', 'subagent', 'through', 'workspace', 'home']);
|
|
5
|
+
|
|
6
|
+
function validateInput(input) {
|
|
7
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new CliError('INVALID_ARGUMENT', 'subagents input must be an object', 2);
|
|
8
|
+
for (const key of Object.keys(input)) {
|
|
9
|
+
if (!INPUT_FIELDS.has(key)) throw new CliError('INVALID_ARGUMENT', `subagents contains unsupported field: ${key}`, 2);
|
|
10
|
+
}
|
|
11
|
+
if (Object.prototype.hasOwnProperty.call(input, 'subagent')) {
|
|
12
|
+
if (input.subagent === null) throw new CliError('INVALID_ARGUMENT', 'subagent must be omitted or a supported subagent id; null is invalid', 2);
|
|
13
|
+
if (typeof input.subagent !== 'string' || input.subagent.length === 0) throw new CliError('INVALID_ARGUMENT', 'subagent must be a non-empty string', 2);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseProbeArgs(rest) {
|
|
18
|
+
const subagent = rest.shift();
|
|
19
|
+
if (!subagent || subagent.startsWith('--')) throw new CliError('subagent_required', 'subagents probe requires a subagent', 2);
|
|
20
|
+
let through = 'local';
|
|
21
|
+
let selectedLayer = false;
|
|
22
|
+
const input = { operation: 'probe', subagent };
|
|
23
|
+
while (rest.length > 0) {
|
|
24
|
+
const option = rest.shift();
|
|
25
|
+
if (['--local', '--auth', '--hi'].includes(option)) {
|
|
26
|
+
if (selectedLayer) throw new CliError('INVALID_ARGUMENT', 'subagents probe accepts exactly one of --local, --auth, or --hi', 2);
|
|
27
|
+
through = option.slice(2);
|
|
28
|
+
selectedLayer = true;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (option === '--workspace' || option === '--home') {
|
|
32
|
+
const name = option.slice(2);
|
|
33
|
+
const value = rest.shift();
|
|
34
|
+
if (!value || value.startsWith('--')) throw new CliError('INVALID_ARGUMENT', `${option} requires a value`, 2);
|
|
35
|
+
if (input[name] !== undefined) throw new CliError('INVALID_ARGUMENT', `${option} may be provided only once`, 2);
|
|
36
|
+
input[name] = value;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
throw new CliError('INVALID_ARGUMENT', `unsupported subagents probe option: ${option}`, 2);
|
|
40
|
+
}
|
|
41
|
+
return { ...input, through };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseModelsArgs(rest) {
|
|
45
|
+
const subagent = rest.shift();
|
|
46
|
+
if (!subagent || subagent.startsWith('--')) throw new CliError('subagent_required', 'subagents models requires a subagent', 2);
|
|
47
|
+
const input = { operation: 'models', subagent };
|
|
48
|
+
while (rest.length > 0) {
|
|
49
|
+
const option = rest.shift();
|
|
50
|
+
if (option !== '--workspace' && option !== '--home') throw new CliError('INVALID_ARGUMENT', `unsupported subagents models option: ${option}`, 2);
|
|
51
|
+
const name = option.slice(2);
|
|
52
|
+
const value = rest.shift();
|
|
53
|
+
if (!value || value.startsWith('--')) throw new CliError('INVALID_ARGUMENT', `${option} requires a value`, 2);
|
|
54
|
+
if (input[name] !== undefined) throw new CliError('INVALID_ARGUMENT', `${option} may be provided only once`, 2);
|
|
55
|
+
input[name] = value;
|
|
56
|
+
}
|
|
57
|
+
return input;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function parseSubagentsArgs(args) {
|
|
61
|
+
if (args.length === 0) return { operation: 'list' };
|
|
62
|
+
const [operation, ...rest] = args;
|
|
63
|
+
if (!['list', 'status', 'probe', 'models'].includes(operation)) throw new CliError('INVALID_ARGUMENT', `unsupported subagents operation: ${operation}`, 2);
|
|
64
|
+
if (operation === 'probe') return parseProbeArgs(rest);
|
|
65
|
+
if (operation === 'models') return parseModelsArgs(rest);
|
|
66
|
+
if (operation === 'list' && rest.length !== 0) throw new CliError('INVALID_ARGUMENT', 'usage: subagents list', 2);
|
|
67
|
+
if (operation !== 'list' && rest.length > 1) throw new CliError('INVALID_ARGUMENT', `usage: subagents ${operation} [subagent]`, 2);
|
|
68
|
+
return { operation, ...(rest[0] ? { subagent: rest[0] } : {}) };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function subagentsCommand(paths, input = {}, options = {}) {
|
|
72
|
+
validateInput(input);
|
|
73
|
+
const operation = input.operation ?? 'list';
|
|
74
|
+
const config = readConfig(paths.config);
|
|
75
|
+
if (operation === 'list') {
|
|
76
|
+
return { default_subagent: config.default_subagent, config_revision: config.revision, subagents: Object.entries(config.subagents).map(([id, value]) => ({ subagent: id, ...value })) };
|
|
77
|
+
}
|
|
78
|
+
if (operation === 'models') {
|
|
79
|
+
if (typeof options.callDaemon !== 'function' || typeof options.socket !== 'string') throw new CliError('INTERNAL_ERROR', 'subagents models requires a daemon connection');
|
|
80
|
+
if (!input.subagent) throw new CliError('subagent_required', 'subagents models requires a subagent', 2);
|
|
81
|
+
const scope = {};
|
|
82
|
+
if (input.workspace !== undefined) scope.workspace = input.workspace;
|
|
83
|
+
if (input.home !== undefined) scope.home = input.home;
|
|
84
|
+
const result = await options.callDaemon(options.socket, 'agent-models', { subagent: input.subagent, scope });
|
|
85
|
+
return subagentView(result);
|
|
86
|
+
}
|
|
87
|
+
if (operation === 'probe') {
|
|
88
|
+
if (typeof options.callDaemon !== 'function' || typeof options.socket !== 'string') throw new CliError('INTERNAL_ERROR', 'subagents probe requires a daemon connection');
|
|
89
|
+
if (!input.subagent) throw new CliError('subagent_required', 'subagents probe requires a subagent', 2);
|
|
90
|
+
const scope = {};
|
|
91
|
+
if (input.workspace !== undefined) scope.workspace = input.workspace;
|
|
92
|
+
if (input.home !== undefined) scope.home = input.home;
|
|
93
|
+
const result = await options.callDaemon(options.socket, 'agent-probe', { subagent: input.subagent, through: input.through ?? 'local', scope });
|
|
94
|
+
return { ...result, evidence: subagentView(result.evidence), status: subagentView(result.status) };
|
|
95
|
+
}
|
|
96
|
+
if (operation !== 'status') throw new CliError('INVALID_ARGUMENT', `unsupported subagents operation: ${operation}`, 2);
|
|
97
|
+
if (typeof options.callDaemon !== 'function' || typeof options.socket !== 'string') throw new CliError('INTERNAL_ERROR', 'subagents status requires a daemon connection');
|
|
98
|
+
const status = await options.callDaemon(options.socket, 'status', {});
|
|
99
|
+
const entries = status.subagents ?? status.agents;
|
|
100
|
+
if (!Array.isArray(entries)) throw new CliError('PROTOCOL_ERROR', 'daemon status did not include subagent status');
|
|
101
|
+
const subagents = entries.filter((entry) => input.subagent === undefined || (entry.subagent ?? entry.agent) === input.subagent).map(subagentView);
|
|
102
|
+
if (input.subagent !== undefined && subagents.length === 0) throw new CliError('subagent_unknown', `daemon did not report subagent: ${input.subagent}`, 2);
|
|
103
|
+
return { service_generation: status.service_generation ?? null, subagents };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Source compatibility for internal callers while the public CLI name is
|
|
107
|
+
// `subagents`; wire-level agent fields remain S02's responsibility.
|
|
108
|
+
export const agentsCommand = subagentsCommand;
|
|
109
|
+
export const parseAgentsArgs = parseSubagentsArgs;
|
|
110
|
+
|
|
111
|
+
// The daemon wire contract is migrated by S02; the CLI already exposes the
|
|
112
|
+
// canonical execution-target name at its own boundary.
|
|
113
|
+
function subagentView(value) {
|
|
114
|
+
if (!value) return value;
|
|
115
|
+
const { agent, ...rest } = value;
|
|
116
|
+
return { subagent: value.subagent ?? agent, ...rest };
|
|
117
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { readConfig } from '../config/read.mjs';
|
|
2
|
+
import { updateConfig, writeConfig } from '../config/write.mjs';
|
|
3
|
+
import { SUBAGENT_IDS } from '../config/schema.mjs';
|
|
4
|
+
import { CliError } from '../errors.mjs';
|
|
5
|
+
|
|
6
|
+
const CONFIG_INPUT_FIELDS = new Set(['operation', 'patch', 'key']);
|
|
7
|
+
const GET_KEYS = new Set([
|
|
8
|
+
'default_subagent',
|
|
9
|
+
...SUBAGENT_IDS.flatMap((agent) => [`subagents.${agent}.enabled`, `subagents.${agent}.spawn_supported`, `subagents.${agent}.default_model`, `subagents.${agent}.runtime_path`, `subagents.${agent}.home`, `subagents.${agent}.profile`, `subagents.${agent}.version`]),
|
|
10
|
+
]);
|
|
11
|
+
const SET_KEYS = GET_KEYS;
|
|
12
|
+
|
|
13
|
+
function rejectUnknownInput(input, allowed = CONFIG_INPUT_FIELDS) {
|
|
14
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new CliError('INVALID_ARGUMENT', 'config input must be an object', 2);
|
|
15
|
+
for (const key of Object.keys(input)) {
|
|
16
|
+
if (!allowed.has(key)) throw new CliError('INVALID_ARGUMENT', `config contains unsupported field: ${key}`, 2);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseValue(raw) {
|
|
21
|
+
if (raw === 'null') return null;
|
|
22
|
+
if (raw === 'true') return true;
|
|
23
|
+
if (raw === 'false') return false;
|
|
24
|
+
return raw;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function patchFor(key, value) {
|
|
28
|
+
if (!SET_KEYS.has(key)) throw new CliError('INVALID_ARGUMENT', `unsupported config key: ${key}`, 2);
|
|
29
|
+
const parts = key.split('.');
|
|
30
|
+
if (parts.length === 1) return { [key]: value };
|
|
31
|
+
return { subagents: { [parts[1]]: { [parts[2]]: value } } };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function unsetPatch(key) {
|
|
35
|
+
if (!SET_KEYS.has(key)) throw new CliError('INVALID_ARGUMENT', `unsupported config key: ${key}`, 2);
|
|
36
|
+
if (key === 'default_subagent') return { default_subagent: null };
|
|
37
|
+
const [, agent, field] = key.split('.');
|
|
38
|
+
const defaults = {
|
|
39
|
+
enabled: agent === 'zcode',
|
|
40
|
+
spawn_supported: agent === 'zcode',
|
|
41
|
+
default_model: null, runtime_path: null, home: null, profile: null, version: null,
|
|
42
|
+
};
|
|
43
|
+
return { subagents: { [agent]: { [field]: defaults[field] } } };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function valueFor(config, key) {
|
|
47
|
+
if (!GET_KEYS.has(key)) throw new CliError('INVALID_ARGUMENT', `unsupported config key: ${key}`, 2);
|
|
48
|
+
return key.split('.').reduce((value, part) => value?.[part], config);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function mergePatch(current, patch) {
|
|
52
|
+
return {
|
|
53
|
+
...current,
|
|
54
|
+
...patch,
|
|
55
|
+
subagents: {
|
|
56
|
+
...current.subagents,
|
|
57
|
+
...Object.fromEntries(Object.entries(patch.subagents || {}).map(([agent, value]) => [
|
|
58
|
+
agent,
|
|
59
|
+
{ ...current.subagents[agent], ...value },
|
|
60
|
+
])),
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function parseConfigArgs(args) {
|
|
66
|
+
if (args.length === 0) return { operation: 'get' };
|
|
67
|
+
const [operation, ...rest] = args;
|
|
68
|
+
if (operation === 'get') {
|
|
69
|
+
if (rest.length > 1) throw new CliError('INVALID_ARGUMENT', 'usage: config get [key]', 2);
|
|
70
|
+
return { operation, ...(rest[0] ? { key: rest[0] } : {}) };
|
|
71
|
+
}
|
|
72
|
+
if (operation === 'set') {
|
|
73
|
+
if (rest.length !== 2) throw new CliError('INVALID_ARGUMENT', 'usage: config set <key> <value>', 2);
|
|
74
|
+
return { operation, patch: patchFor(rest[0], parseValue(rest[1])) };
|
|
75
|
+
}
|
|
76
|
+
if (operation === 'show') {
|
|
77
|
+
if (rest.length !== 0) throw new CliError('INVALID_ARGUMENT', 'usage: config show', 2);
|
|
78
|
+
return { operation };
|
|
79
|
+
}
|
|
80
|
+
if (operation === 'unset') {
|
|
81
|
+
if (rest.length !== 1) throw new CliError('INVALID_ARGUMENT', 'usage: config unset <key>', 2);
|
|
82
|
+
unsetPatch(rest[0]);
|
|
83
|
+
return { operation, key: rest[0] };
|
|
84
|
+
}
|
|
85
|
+
throw new CliError('INVALID_ARGUMENT', `unsupported config operation: ${operation}`, 2);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function configCommand(paths, input = {}) {
|
|
89
|
+
rejectUnknownInput(input);
|
|
90
|
+
const operation = input.operation ?? 'get';
|
|
91
|
+
const current = readConfig(paths.config);
|
|
92
|
+
if (operation === 'set') {
|
|
93
|
+
if (!input.patch || typeof input.patch !== 'object' || Array.isArray(input.patch)) throw new CliError('INVALID_ARGUMENT', 'config set requires an object patch', 2);
|
|
94
|
+
if (Object.hasOwn(input.patch, 'revision')) throw new CliError('INVALID_ARGUMENT', 'config revision is managed by the writer', 2);
|
|
95
|
+
if (input.patch.subagents !== undefined && (!input.patch.subagents || typeof input.patch.subagents !== 'object' || Array.isArray(input.patch.subagents))) {
|
|
96
|
+
throw new CliError('INVALID_ARGUMENT', 'config subagents patch must be an object', 2);
|
|
97
|
+
}
|
|
98
|
+
for (const [agent, value] of Object.entries(input.patch.subagents || {})) {
|
|
99
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new CliError('CONFIG_INVALID', `subagents.${agent} must be an object`, 2);
|
|
100
|
+
}
|
|
101
|
+
return { config: updateConfig(paths.config, (latest) => mergePatch(latest, input.patch)) };
|
|
102
|
+
}
|
|
103
|
+
if (operation === 'show') {
|
|
104
|
+
if (input.patch !== undefined || input.key !== undefined) throw new CliError('INVALID_ARGUMENT', 'config show does not accept key or patch', 2);
|
|
105
|
+
return { config: current };
|
|
106
|
+
}
|
|
107
|
+
if (operation === 'unset') {
|
|
108
|
+
if (input.patch !== undefined || typeof input.key !== 'string') throw new CliError('INVALID_ARGUMENT', 'config unset requires exactly one supported key', 2);
|
|
109
|
+
return { config: updateConfig(paths.config, (latest) => mergePatch(latest, unsetPatch(input.key))) };
|
|
110
|
+
}
|
|
111
|
+
if (operation === 'get') {
|
|
112
|
+
if (input.patch !== undefined) throw new CliError('INVALID_ARGUMENT', 'config get does not accept patch', 2);
|
|
113
|
+
if (input.key === undefined) return { config: current };
|
|
114
|
+
return { key: input.key, value: valueFor(current, input.key), revision: current.revision };
|
|
115
|
+
}
|
|
116
|
+
throw new CliError('INVALID_ARGUMENT', `unsupported config operation: ${operation}`, 2);
|
|
117
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { bootoutService, bootstrapService } from '../install/service-macos.mjs';
|
|
3
|
+
import { loadCodexHomes } from '../install/reconcile.mjs';
|
|
4
|
+
import { verifyPayload } from '../install/payload.mjs';
|
|
5
|
+
|
|
6
|
+
// Daemon/service command surface: explicit start/stop through the managed
|
|
7
|
+
// LaunchAgent, and the local installation summary consumed by `status`.
|
|
8
|
+
|
|
9
|
+
export function startDaemon(paths) {
|
|
10
|
+
return bootstrapService(paths);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function stopDaemon(paths) {
|
|
14
|
+
return bootoutService(paths);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function localInstallStatus(paths, { verbose = false } = {}) {
|
|
18
|
+
let payload = { status: 'unverified' };
|
|
19
|
+
try { payload = verifyPayload(); } catch (error) {
|
|
20
|
+
payload = { status: 'invalid', error: { code: error.code || 'PAYLOAD_INVALID', message: error.message } };
|
|
21
|
+
}
|
|
22
|
+
const registry = loadCodexHomes(paths);
|
|
23
|
+
const publicPayload = verbose
|
|
24
|
+
? payload
|
|
25
|
+
: { status: payload.status, platform: payload.platform, version: payload.version };
|
|
26
|
+
return {
|
|
27
|
+
installed: fs.existsSync(paths.state),
|
|
28
|
+
launch_agent: fs.existsSync(paths.launchAgent),
|
|
29
|
+
data: fs.existsSync(paths.data),
|
|
30
|
+
payload: publicPayload,
|
|
31
|
+
// Home paths are installation internals; status reports only the
|
|
32
|
+
// aggregate binding state. Detailed paths remain available to diagnose.
|
|
33
|
+
codex_homes: registry.registry.homes.map((entry) => ({ version: entry.version, last_status: entry.last_status })),
|
|
34
|
+
...(verbose && registry.recovery ? { codex_homes_recovery: registry.recovery } : {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { backupData, cleanupLegacy, purge, restoreData, uninstall as removeService } from '../maintenance.mjs';
|
|
2
|
+
import { unregisterAllCodexHomes } from '../install/reconcile.mjs';
|
|
3
|
+
import { uninstallZcodePlugin } from '../install/zcode.mjs';
|
|
4
|
+
|
|
5
|
+
// Maintenance command coordination. The ES-owned service is booted out and
|
|
6
|
+
// its definition removed before any D08 claim is released: a bootout that
|
|
7
|
+
// cannot complete fails the command with the registry exactly as it was, so
|
|
8
|
+
// an uninstall retry (or a reconcile) still sees every claimed home instead
|
|
9
|
+
// of a wiped registry. Only a fully removed service releases the claims;
|
|
10
|
+
// product data is retained by contract, and the emptied registry travels
|
|
11
|
+
// with it so no home remains claimed. The stateless ZCode binding is
|
|
12
|
+
// released the same way: the product-owned staging tree and its single
|
|
13
|
+
// plugins.dirs entry go away together.
|
|
14
|
+
|
|
15
|
+
export function uninstall(paths, options = {}) {
|
|
16
|
+
const removed = removeService(paths, options);
|
|
17
|
+
const released = unregisterAllCodexHomes(paths);
|
|
18
|
+
// An unreadable ZCode config is foreign state, not a product claim: report
|
|
19
|
+
// the release failure instead of failing the whole uninstall over it.
|
|
20
|
+
let zcode;
|
|
21
|
+
try { zcode = uninstallZcodePlugin(paths); } catch (error) {
|
|
22
|
+
zcode = { uninstalled: false, error: { code: error.code || 'ZCODE_BINDING_FAILED', message: error.message } };
|
|
23
|
+
}
|
|
24
|
+
return { ...removed, codex_homes_unregistered: released.unregistered, zcode_binding: zcode };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { backupData, cleanupLegacy, purge, restoreData };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { CliError } from '../errors.mjs';
|
|
2
|
+
import { installMcp, installPlugin, uninstallPlugin } from '../install/codex.mjs';
|
|
3
|
+
import { installZcodePlugin, uninstallZcodePlugin } from '../install/zcode.mjs';
|
|
4
|
+
import { registerCodexHome, unregisterCodexHome } from '../install/reconcile.mjs';
|
|
5
|
+
import { verifyPayload } from '../install/payload.mjs';
|
|
6
|
+
|
|
7
|
+
// Command surface for the managed host bindings. `install-plugin` takes an
|
|
8
|
+
// optional host argument: codex (the default, for backward compatibility)
|
|
9
|
+
// stages the plugin and goes through the official codex CLI, claiming the
|
|
10
|
+
// Codex home in the D08 registry; zcode stages the same plugin into a
|
|
11
|
+
// product-owned tree and registers it through the ZCode config's inline
|
|
12
|
+
// plugin dirs. The MCP TOML binding stays an explicit codex-only
|
|
13
|
+
// alternative for hosts without plugin support.
|
|
14
|
+
|
|
15
|
+
const PLUGIN_HOSTS = ['codex', 'zcode'];
|
|
16
|
+
|
|
17
|
+
function parseCommon(args, flags) {
|
|
18
|
+
const options = {};
|
|
19
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
20
|
+
const arg = args[index];
|
|
21
|
+
if (!arg.startsWith('--')) {
|
|
22
|
+
if (options.host) throw new CliError('INVALID_ARGUMENT', `plugin host was already given: ${options.host}`, 2);
|
|
23
|
+
if (!PLUGIN_HOSTS.includes(arg)) throw new CliError('INVALID_ARGUMENT', `unsupported plugin host: ${arg} (expected ${PLUGIN_HOSTS.join(' or ')})`, 2);
|
|
24
|
+
options.host = arg;
|
|
25
|
+
} else if (flags.includes(arg)) {
|
|
26
|
+
options[flagName(arg)] = true;
|
|
27
|
+
} else if (arg === '--codex-home') {
|
|
28
|
+
const value = args[index + 1];
|
|
29
|
+
if (!value || value.startsWith('--')) throw new CliError('INVALID_ARGUMENT', '--codex-home requires a value', 2);
|
|
30
|
+
options.codexHome = value;
|
|
31
|
+
index += 1;
|
|
32
|
+
} else {
|
|
33
|
+
throw new CliError('INVALID_ARGUMENT', `unsupported plugin option: ${arg}`, 2);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return options;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function flagName(flag) {
|
|
40
|
+
return flag.replace(/^--/u, '').replaceAll('-', '_');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function pluginCommand(paths, args) {
|
|
44
|
+
const options = parseCommon(args, ['--dry-run', '--uninstall']);
|
|
45
|
+
const installerOptions = withInstallerFlags(options);
|
|
46
|
+
if (options.host === 'zcode') {
|
|
47
|
+
if (options.codexHome) throw new CliError('INVALID_ARGUMENT', '--codex-home applies to the codex host only', 2);
|
|
48
|
+
return options.uninstall ? uninstallZcodePlugin(paths, installerOptions) : installZcodePlugin(paths, installerOptions);
|
|
49
|
+
}
|
|
50
|
+
if (options.uninstall) {
|
|
51
|
+
const result = uninstallPlugin(paths, installerOptions);
|
|
52
|
+
if (!options.dry_run && result.uninstalled) {
|
|
53
|
+
const claim = unregisterCodexHome(paths, result.codex_home, 'plugin');
|
|
54
|
+
result.claim_released = claim.unregistered;
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
const result = installPlugin(paths, installerOptions);
|
|
59
|
+
if (!options.dry_run && result.installed) {
|
|
60
|
+
const payload = safePayloadVersion();
|
|
61
|
+
const claim = registerCodexHome(paths, result.codex_home, { version: payload, digest: result.digest, binding_mode: 'plugin' });
|
|
62
|
+
result.claim = { registered: claim.registered, deduplicated: claim.deduplicated, homes: claim.homes };
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function mcpCommand(paths, args) {
|
|
68
|
+
const options = parseCommon(args, ['--dry-run', '--uninstall']);
|
|
69
|
+
// The MCP TOML binding is codex-only (see the header note): a non-codex
|
|
70
|
+
// host must fail here, before any installer or registry call, instead of
|
|
71
|
+
// being silently ignored while the codex config is rewritten.
|
|
72
|
+
if (options.host && options.host !== 'codex') {
|
|
73
|
+
throw new CliError('INVALID_ARGUMENT', `unsupported mcp host: ${options.host} (install-mcp binds the codex host only)`, 2);
|
|
74
|
+
}
|
|
75
|
+
const installerOptions = withInstallerFlags(options);
|
|
76
|
+
const result = installMcp(paths, installerOptions);
|
|
77
|
+
if (!options.dry_run && result.installed) {
|
|
78
|
+
const claim = registerCodexHome(paths, result.codex_home || installerOptions.codexHome, { binding_mode: 'mcp', status: 'claimed' });
|
|
79
|
+
result.claim = { registered: claim.registered, deduplicated: claim.deduplicated, homes: claim.homes };
|
|
80
|
+
} else if (!options.dry_run && options.uninstall) {
|
|
81
|
+
const claim = unregisterCodexHome(paths, result.codex_home || installerOptions.codexHome, 'mcp');
|
|
82
|
+
result.claim_released = claim.unregistered;
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The install layer speaks camelCase (dryRun); CLI parsing produces snake_case
|
|
88
|
+
// flags. Normalize at the boundary so --dry-run actually holds the installers
|
|
89
|
+
// back instead of running a real install/uninstall.
|
|
90
|
+
function withInstallerFlags(options) {
|
|
91
|
+
return { ...options, dryRun: Boolean(options.dry_run) };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function safePayloadVersion() {
|
|
95
|
+
try { return verifyPayload().version; } catch { return null; }
|
|
96
|
+
}
|