external-subagent 0.5.2 → 0.5.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 +8 -5
- package/bin/external-subagent-mcp.mjs +3 -2
- package/cli/commands/agents.mjs +4 -9
- package/cli/commands/config.mjs +1 -1
- package/cli/commands/daemon.mjs +8 -3
- package/cli/commands/maintenance.mjs +158 -1
- package/cli/commands/plugin.mjs +28 -3
- package/cli/commands/tasks.mjs +7 -1
- package/cli/commands/update.mjs +18 -7
- package/cli/config/write.mjs +195 -14
- package/cli/constants.mjs +2 -2
- package/cli/install/codex.mjs +1 -1
- package/cli/install/dsh.mjs +606 -0
- package/cli/install/init.mjs +23 -33
- package/cli/install/layout.mjs +10 -2
- package/cli/install/payload.mjs +59 -12
- package/cli/install/service-activation.mjs +10 -2
- package/cli/install/service-linux.mjs +1 -1
- package/cli/install/service-macos.mjs +40 -14
- package/cli/install/service-windows.mjs +463 -0
- package/cli/install/update.mjs +6 -5
- package/cli/main.mjs +10 -14
- package/cli/maintenance.mjs +7 -1
- package/cli/paths.mjs +152 -8
- package/npm/native/README.md +2 -2
- 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 +5 -5
- package/npm/native/linux-x64/external-subagent-mcp +0 -0
- package/npm/native/linux-x64/external-subagentd +0 -0
- package/npm/native/linux-x64/payload.json +5 -5
- package/package.json +3 -2
- package/plugins/codex/external-subagent/skills/external-subagent/SKILL.md +5 -3
- package/schema/external-subagent-public-api.json +3 -3
- package/scripts/release/build-native-payload.mjs +22 -5
- package/scripts/release/check-native-tarball.mjs +34 -14
- package/scripts/release/stage-debug-plugin.mjs +9 -3
- package/scripts/release/test-installed-tarball.mjs +1 -1
- package/plugins/codex/external-subagent/hooks/audit-bash-result.mjs +0 -55
- package/plugins/codex/external-subagent/hooks/check-agent-files.mjs +0 -19
- package/plugins/codex/external-subagent/hooks/guard-bash.mjs +0 -15
- package/plugins/codex/external-subagent/lib/agent-file-policy.mjs +0 -206
- package/plugins/codex/external-subagent/scripts/install-agent-hooks.mjs +0 -149
- package/plugins/codex/external-subagent/scripts/policy-verifier.mjs +0 -58
package/README.md
CHANGED
|
@@ -6,8 +6,9 @@ tools) and subagents (the execution targets that run the tasks) are two
|
|
|
6
6
|
independent axes:
|
|
7
7
|
|
|
8
8
|
- **Hosts** — Codex (`install-plugin codex` or `install-mcp`), ZCode
|
|
9
|
-
(`install-plugin zcode`),
|
|
10
|
-
directly to the facade without
|
|
9
|
+
(`install-plugin zcode`), DSH (`install-plugin dsh --profile <name>`), and
|
|
10
|
+
any custom local MCP client that connects directly to the facade without
|
|
11
|
+
registration.
|
|
11
12
|
- **Subagents** — ZCode, DeepSeek Harness (DSH), Codex, and Google
|
|
12
13
|
Antigravity (`agy`), each routed through an internal adapter and subject to
|
|
13
14
|
the per-subagent limitations listed in [docs/product.md](docs/product.md)
|
|
@@ -38,7 +39,8 @@ All four subagents are **disabled by default** and are enabled per name with
|
|
|
38
39
|
`external-subagent agents enable <zcode|dsh|codex|agy>`: a successful local probe
|
|
39
40
|
writes the configuration (no write on failure), and `dsh`/`codex`/`agy` need a
|
|
40
41
|
daemon restart to take effect. ZCode admits all four permission modes
|
|
41
|
-
(build/edit/plan/yolo) and
|
|
42
|
+
(build/edit/plan/yolo) and requires an explicit `provider/model` spawn
|
|
43
|
+
selection; DSH admits
|
|
42
44
|
`build` and strict `plan`, with model selection as explicit spawn model,
|
|
43
45
|
configured default, then the native default, and its `build` mode admits a
|
|
44
46
|
non-empty caller `write_manifest` through the guarded manifest-build
|
|
@@ -90,7 +92,8 @@ intentionally diverge; see
|
|
|
90
92
|
```
|
|
91
93
|
npm install -g external-subagent # stages package + payload only (macOS arm64, Linux x64)
|
|
92
94
|
external-subagent init # explicit: standalone daemon service only
|
|
93
|
-
external-subagent install-plugin zcode # optional host binding (or: install-plugin codex /
|
|
95
|
+
external-subagent install-plugin zcode # optional host binding (or: install-plugin codex /
|
|
96
|
+
# install-plugin dsh --profile web / install-mcp)
|
|
94
97
|
```
|
|
95
98
|
|
|
96
99
|
Packing and publishing are gated by npm lifecycle scripts: `prepack` always
|
|
@@ -151,7 +154,7 @@ from the available set.
|
|
|
151
154
|
|---|---|
|
|
152
155
|
| `name` | Required, unique across files; trimmed, non-empty, at most 128 bytes |
|
|
153
156
|
| `subagent` | `zcode`, `dsh`, `codex`, or `agy` |
|
|
154
|
-
| `model` | Model token, named per target: `dsh` wants `provider:model`, `zcode`
|
|
157
|
+
| `model` | Model token, named per target: `dsh` wants `provider:model`, `zcode` requires `provider/model` (from the spawn request or a named profile; a bare token and omission are rejected), `codex` and `agy` take a bare slug |
|
|
155
158
|
| `effort` | Effort token; some runtime models require one (for example `agy` rejects `gemini-3.8-flash` without `--effort`) and the spawn error surfaces the runtime's message verbatim |
|
|
156
159
|
| `permission_mode` | `build`, `edit`, `plan`, or `yolo`; defaults to `build` when unset |
|
|
157
160
|
| `developer_instructions` | Free-form instructions, delivered as described below |
|
|
@@ -4,7 +4,7 @@ import { spawn } from 'node:child_process';
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import { nativePlatform } from '../cli/install/layout.mjs';
|
|
7
|
+
import { nativeExecutableName, nativePlatform } from '../cli/install/layout.mjs';
|
|
8
8
|
|
|
9
9
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
10
|
|
|
@@ -18,8 +18,9 @@ export function mcpFacadePath(
|
|
|
18
18
|
platform = nativePlatform() ?? `${process.platform}-${process.arch}`,
|
|
19
19
|
debug = process.env.EXTERNAL_SUBAGENT_VARIANT === 'debug',
|
|
20
20
|
) {
|
|
21
|
+
const name = debug ? 'external-subagent-debug-mcp' : 'external-subagent-mcp';
|
|
21
22
|
return path.join(packageRoot, 'npm', debug ? 'native-debug' : 'native', platform,
|
|
22
|
-
|
|
23
|
+
nativeExecutableName(name, platform));
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export function runMcpFacade() {
|
package/cli/commands/agents.mjs
CHANGED
|
@@ -90,13 +90,8 @@ export async function subagentsCommand(paths, input = {}, options = {}) {
|
|
|
90
90
|
const result = await options.callDaemon(options.socket, 'agent-probe', { subagent: agent, through: 'local', scope });
|
|
91
91
|
const local = result.evidence?.local;
|
|
92
92
|
if (local?.state !== 'READY') throw new CliError('agent_probe_failed', `Cannot enable ${agent}: ${local?.reason ?? 'local runtime probe did not succeed'}`, 2);
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
}
|
|
93
|
+
// Version differences are advisory (surfaced by status and failure
|
|
94
|
+
// instructions); enable records the observed version without rejecting it.
|
|
100
95
|
const patch = { enabled: true, spawn_supported: true };
|
|
101
96
|
if (agent !== 'zcode') {
|
|
102
97
|
if (typeof local.runtime_path !== 'string' || !path.isAbsolute(local.runtime_path) || typeof local.version !== 'string' || !local.version.trim()) {
|
|
@@ -158,11 +153,11 @@ export async function subagentsCommand(paths, input = {}, options = {}) {
|
|
|
158
153
|
}
|
|
159
154
|
|
|
160
155
|
// Source compatibility for internal callers while the public CLI name is
|
|
161
|
-
// `subagents`; wire-level agent fields remain
|
|
156
|
+
// `subagents`; wire-level agent fields remain the daemon wire contract's responsibility.
|
|
162
157
|
export const agentsCommand = subagentsCommand;
|
|
163
158
|
export const parseAgentsArgs = parseSubagentsArgs;
|
|
164
159
|
|
|
165
|
-
// The daemon wire contract is
|
|
160
|
+
// The daemon wire contract is owned by the daemon; the CLI already exposes the
|
|
166
161
|
// canonical execution-target name at its own boundary.
|
|
167
162
|
function subagentView(value) {
|
|
168
163
|
if (!value) return value;
|
package/cli/commands/config.mjs
CHANGED
|
@@ -96,7 +96,7 @@ export function parseConfigArgs(args) {
|
|
|
96
96
|
// regenerates the service definition from the new config. Only the file is
|
|
97
97
|
// rewritten — launchd picks the environment up at its next load, so a running
|
|
98
98
|
// daemon is never restarted or disturbed here. The launchd plist is macOS
|
|
99
|
-
// only; the Linux systemd unit is re-rendered by the
|
|
99
|
+
// only; the Linux systemd unit is re-rendered by the service backend, so a
|
|
100
100
|
// Linux config write refreshes nothing yet instead of writing a plist to the
|
|
101
101
|
// unit path.
|
|
102
102
|
function refreshServiceDefinition(paths) {
|
package/cli/commands/daemon.mjs
CHANGED
|
@@ -28,11 +28,16 @@ export function localInstallStatus(paths, { verbose = false } = {}) {
|
|
|
28
28
|
? payload
|
|
29
29
|
: { status: payload.status, platform: payload.platform, version: payload.version };
|
|
30
30
|
// The service-definition path is the macOS LaunchAgent plist or the Linux
|
|
31
|
-
// systemd user unit (paths.mjs).
|
|
31
|
+
// systemd user unit (paths.mjs). Windows has no file-based definition (the
|
|
32
|
+
// SCM registration is the definition), so `launchAgent` is null there and
|
|
33
|
+
// must not reach fs.existsSync. The reported field stays platform-truthful
|
|
32
34
|
// so a Linux install never reads as a launchd agent.
|
|
33
|
-
const
|
|
35
|
+
const currentPlatform = platform();
|
|
36
|
+
const serviceDefinition = currentPlatform === 'darwin'
|
|
34
37
|
? { launch_agent: fs.existsSync(paths.launchAgent) }
|
|
35
|
-
:
|
|
38
|
+
: currentPlatform === 'win32'
|
|
39
|
+
? { service_definition: false }
|
|
40
|
+
: { service_definition: fs.existsSync(paths.launchAgent) };
|
|
36
41
|
return {
|
|
37
42
|
installed: fs.existsSync(paths.state),
|
|
38
43
|
...serviceDefinition,
|
|
@@ -1,6 +1,10 @@
|
|
|
1
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
|
+
import { uninstallAllDshBindings } from '../install/dsh.mjs';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { atomicWrite, jsonBytes, readOptional } from '../fs-atomic.mjs';
|
|
4
8
|
|
|
5
9
|
// Maintenance command coordination. The ES-owned service is booted out and
|
|
6
10
|
// its definition removed before any D08 claim is released: a bootout that
|
|
@@ -12,6 +16,149 @@ import { uninstallZcodePlugin } from '../install/zcode.mjs';
|
|
|
12
16
|
// released the same way: the product-owned staging tree and its single
|
|
13
17
|
// plugins.dirs entry go away together.
|
|
14
18
|
|
|
19
|
+
// Event-bound legacy shapes. The retired installer wrote each script under
|
|
20
|
+
// exactly these events and always placed the script as the single process
|
|
21
|
+
// argument.
|
|
22
|
+
const MANAGED_HOOK_EVENTS = {
|
|
23
|
+
PreToolUse: new Map([
|
|
24
|
+
['guard-bash.mjs', 'Bash'],
|
|
25
|
+
['check-agent-files.mjs', '^(Read|Grep|Glob|Write|Edit|Delete|Move)$'],
|
|
26
|
+
]),
|
|
27
|
+
PostToolUse: new Map([['audit-bash-result.mjs', 'Bash']]),
|
|
28
|
+
PostToolUseFailure: new Map([['audit-bash-result.mjs', 'Bash']]),
|
|
29
|
+
};
|
|
30
|
+
const PROVENANCE_WRAPPER_KEYS = [
|
|
31
|
+
'effective_guard_wrapper_path',
|
|
32
|
+
'effective_audit_wrapper_path',
|
|
33
|
+
'effective_file_wrapper_path',
|
|
34
|
+
];
|
|
35
|
+
const POLICY_VERIFIER_NAME = 'external-subagent-policy-verifier';
|
|
36
|
+
|
|
37
|
+
function exactWrapperPaths(provenance) {
|
|
38
|
+
const paths = new Set();
|
|
39
|
+
if (!provenance || typeof provenance !== 'object' || Array.isArray(provenance)) return paths;
|
|
40
|
+
for (const key of PROVENANCE_WRAPPER_KEYS) {
|
|
41
|
+
const value = provenance[key];
|
|
42
|
+
if (typeof value === 'string' && value.length > 0) paths.add(path.resolve(value));
|
|
43
|
+
}
|
|
44
|
+
return paths;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// A managed entry is exactly the installer's written shape: a process hook
|
|
48
|
+
// whose script position (`args[0]`) is either a provenance-recorded exact
|
|
49
|
+
// wrapper path or an event-bound legacy `<plugin root>/hooks/<managed
|
|
50
|
+
// script>` with the matching matcher. Arguments after the script position
|
|
51
|
+
// are data, never executable positions.
|
|
52
|
+
function managedHookEntry(entry, exactPaths, managedScripts) {
|
|
53
|
+
if (!entry || typeof entry !== 'object' || !Array.isArray(entry.hooks)) return false;
|
|
54
|
+
const hooks = entry.hooks.filter((hook) => hook?.type === 'process' && Array.isArray(hook.args));
|
|
55
|
+
for (const hook of hooks) {
|
|
56
|
+
const script = hook.args[0];
|
|
57
|
+
if (typeof script === 'string' && exactPaths.has(path.resolve(script))) return true;
|
|
58
|
+
}
|
|
59
|
+
if (typeof entry.matcher !== 'string') return false;
|
|
60
|
+
for (const hook of hooks) {
|
|
61
|
+
const script = hook.args[0];
|
|
62
|
+
if (typeof script !== 'string') continue;
|
|
63
|
+
const expectedMatcher = managedScripts.get(path.basename(script));
|
|
64
|
+
if (expectedMatcher !== undefined
|
|
65
|
+
&& path.basename(path.dirname(script)) === 'hooks'
|
|
66
|
+
&& entry.matcher === expectedMatcher) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Only a genuinely absent record routes to the empty path. A real I/O read
|
|
74
|
+
// failure propagates so the caller treats the config phase as failed and
|
|
75
|
+
// retains the exact-path evidence a retry needs. An unparseable record cannot
|
|
76
|
+
// contribute exact paths, so it is treated as absent.
|
|
77
|
+
function readProvenance(file) {
|
|
78
|
+
if (typeof file !== 'string') return {};
|
|
79
|
+
const bytes = readOptional(file);
|
|
80
|
+
if (bytes === null) return {};
|
|
81
|
+
try {
|
|
82
|
+
const doc = JSON.parse(bytes.toString('utf8'));
|
|
83
|
+
return doc && typeof doc === 'object' && !Array.isArray(doc) ? doc : {};
|
|
84
|
+
} catch {
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function removeFile(file) {
|
|
90
|
+
try {
|
|
91
|
+
fs.unlinkSync(file);
|
|
92
|
+
return true;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (error?.code === 'ENOENT') return false;
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Inverse of the retired hook installer, bounded to the references it
|
|
100
|
+
// actually wrote: recognized managed entries are removed from the three
|
|
101
|
+
// event lists, while every foreign entry, unknown hooks key, and unrelated
|
|
102
|
+
// top-level key stays untouched. The write is atomic. Afterwards the
|
|
103
|
+
// provenance record and the verifier staged beside it are deleted — but only
|
|
104
|
+
// when the config phase completed, so a failed read/parse/write retains the
|
|
105
|
+
// exact-path evidence a retry needs.
|
|
106
|
+
function removeManagedHooks(paths) {
|
|
107
|
+
const configPath = paths.zcodeConfig;
|
|
108
|
+
const provenancePath = paths.hookProvenance;
|
|
109
|
+
const result = {
|
|
110
|
+
config: configPath ?? null,
|
|
111
|
+
entries_removed: 0,
|
|
112
|
+
config_updated: false,
|
|
113
|
+
provenance_removed: false,
|
|
114
|
+
verifier_removed: false,
|
|
115
|
+
};
|
|
116
|
+
let configPhaseFailed = false;
|
|
117
|
+
try {
|
|
118
|
+
const exactPaths = exactWrapperPaths(readProvenance(provenancePath));
|
|
119
|
+
const bytes = typeof configPath === 'string' ? readOptional(configPath) : null;
|
|
120
|
+
if (bytes !== null) {
|
|
121
|
+
const config = JSON.parse(bytes.toString('utf8'));
|
|
122
|
+
const events = config?.hooks && typeof config.hooks === 'object' && !Array.isArray(config.hooks)
|
|
123
|
+
? config.hooks.events
|
|
124
|
+
: undefined;
|
|
125
|
+
if (events && typeof events === 'object' && !Array.isArray(events)) {
|
|
126
|
+
let removed = 0;
|
|
127
|
+
for (const [event, managedScripts] of Object.entries(MANAGED_HOOK_EVENTS)) {
|
|
128
|
+
const entries = events[event];
|
|
129
|
+
if (!Array.isArray(entries)) continue;
|
|
130
|
+
const kept = entries.filter((entry) => !managedHookEntry(entry, exactPaths, managedScripts));
|
|
131
|
+
removed += entries.length - kept.length;
|
|
132
|
+
if (kept.length !== entries.length) events[event] = kept;
|
|
133
|
+
}
|
|
134
|
+
if (removed > 0) {
|
|
135
|
+
atomicWrite(configPath, jsonBytes(config));
|
|
136
|
+
result.entries_removed = removed;
|
|
137
|
+
result.config_updated = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} catch (error) {
|
|
142
|
+
configPhaseFailed = true;
|
|
143
|
+
result.error = { code: error?.code || 'ZCODE_HOOKS_CLEANUP_FAILED', message: error.message };
|
|
144
|
+
}
|
|
145
|
+
if (!configPhaseFailed) {
|
|
146
|
+
try {
|
|
147
|
+
if (typeof provenancePath === 'string') result.provenance_removed = removeFile(provenancePath);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
result.error ??= { code: error?.code || 'ZCODE_HOOKS_CLEANUP_FAILED', message: error.message };
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
if (typeof provenancePath === 'string') {
|
|
153
|
+
result.verifier_removed = removeFile(path.join(path.dirname(provenancePath), POLICY_VERIFIER_NAME));
|
|
154
|
+
}
|
|
155
|
+
} catch (error) {
|
|
156
|
+
result.error ??= { code: error?.code || 'ZCODE_HOOKS_CLEANUP_FAILED', message: error.message };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
|
|
15
162
|
export function uninstall(paths, options = {}) {
|
|
16
163
|
const removed = removeService(paths, options);
|
|
17
164
|
const released = unregisterAllCodexHomes(paths);
|
|
@@ -21,7 +168,17 @@ export function uninstall(paths, options = {}) {
|
|
|
21
168
|
try { zcode = uninstallZcodePlugin(paths); } catch (error) {
|
|
22
169
|
zcode = { uninstalled: false, error: { code: error.code || 'ZCODE_BINDING_FAILED', message: error.message } };
|
|
23
170
|
}
|
|
24
|
-
|
|
171
|
+
// DSH bindings are released by scanning every profile's patch sentinel;
|
|
172
|
+
// the same per-item reporting posture as the codex homes release.
|
|
173
|
+
let dsh;
|
|
174
|
+
try { dsh = uninstallAllDshBindings(paths); } catch (error) {
|
|
175
|
+
dsh = { released: 0, error: { code: error.code || 'DSH_BINDING_RELEASE_FAILED', message: error.message } };
|
|
176
|
+
}
|
|
177
|
+
// Retained installations may still carry managed hooks pointing at the
|
|
178
|
+
// removed scripts. Cleanup is bounded to recognized entries and reports
|
|
179
|
+
// its own failure without failing the rest of the uninstall.
|
|
180
|
+
const zcode_hooks = removeManagedHooks(paths);
|
|
181
|
+
return { ...removed, codex_homes_unregistered: released.unregistered, zcode_binding: zcode, dsh_bindings: dsh, zcode_hooks };
|
|
25
182
|
}
|
|
26
183
|
|
|
27
184
|
export { backupData, purge, restoreData };
|
package/cli/commands/plugin.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CliError } from '../errors.mjs';
|
|
2
2
|
import { installMcp, installPlugin, uninstallPlugin } from '../install/codex.mjs';
|
|
3
3
|
import { installZcodePlugin, uninstallZcodePlugin } from '../install/zcode.mjs';
|
|
4
|
+
import { installDshPlugin, uninstallDshPlugin } from '../install/dsh.mjs';
|
|
4
5
|
import { registerCodexHome, unregisterCodexHome } from '../install/reconcile.mjs';
|
|
5
6
|
import { verifyPayload } from '../install/payload.mjs';
|
|
6
7
|
|
|
@@ -9,10 +10,12 @@ import { verifyPayload } from '../install/payload.mjs';
|
|
|
9
10
|
// stages the plugin and goes through the official codex CLI, claiming the
|
|
10
11
|
// Codex home in the D08 registry; zcode stages the same plugin into a
|
|
11
12
|
// product-owned tree and registers it through the ZCode config's inline
|
|
12
|
-
// plugin dirs
|
|
13
|
-
//
|
|
13
|
+
// plugin dirs; dsh appends one managed mcp-client entry to the profile's
|
|
14
|
+
// cordis.patch.yml and stages the skill into the DSH user skill root. The
|
|
15
|
+
// MCP TOML binding stays an explicit codex-only alternative for hosts
|
|
16
|
+
// without plugin support.
|
|
14
17
|
|
|
15
|
-
const PLUGIN_HOSTS = ['codex', 'zcode'];
|
|
18
|
+
const PLUGIN_HOSTS = ['codex', 'zcode', 'dsh'];
|
|
16
19
|
|
|
17
20
|
function parseCommon(args, flags) {
|
|
18
21
|
const options = {};
|
|
@@ -29,6 +32,16 @@ function parseCommon(args, flags) {
|
|
|
29
32
|
if (!value || value.startsWith('--')) throw new CliError('INVALID_ARGUMENT', '--codex-home requires a value', 2);
|
|
30
33
|
options.codexHome = value;
|
|
31
34
|
index += 1;
|
|
35
|
+
} else if (arg === '--dsh-home') {
|
|
36
|
+
const value = args[index + 1];
|
|
37
|
+
if (!value || value.startsWith('--')) throw new CliError('INVALID_ARGUMENT', '--dsh-home requires a value', 2);
|
|
38
|
+
options.dshHome = value;
|
|
39
|
+
index += 1;
|
|
40
|
+
} else if (arg === '--profile') {
|
|
41
|
+
const value = args[index + 1];
|
|
42
|
+
if (!value || value.startsWith('--')) throw new CliError('INVALID_ARGUMENT', '--profile requires a value', 2);
|
|
43
|
+
options.profile = value;
|
|
44
|
+
index += 1;
|
|
32
45
|
} else {
|
|
33
46
|
throw new CliError('INVALID_ARGUMENT', `unsupported plugin option: ${arg}`, 2);
|
|
34
47
|
}
|
|
@@ -45,8 +58,20 @@ export function pluginCommand(paths, args) {
|
|
|
45
58
|
const installerOptions = withInstallerFlags(options);
|
|
46
59
|
if (options.host === 'zcode') {
|
|
47
60
|
if (options.codexHome) throw new CliError('INVALID_ARGUMENT', '--codex-home applies to the codex host only', 2);
|
|
61
|
+
if (options.dshHome) throw new CliError('INVALID_ARGUMENT', '--dsh-home applies to the dsh host only', 2);
|
|
62
|
+
if (options.profile) throw new CliError('INVALID_ARGUMENT', '--profile applies to the dsh host only', 2);
|
|
48
63
|
return options.uninstall ? uninstallZcodePlugin(paths, installerOptions) : installZcodePlugin(paths, installerOptions);
|
|
49
64
|
}
|
|
65
|
+
if (options.host === 'dsh') {
|
|
66
|
+
if (options.codexHome) throw new CliError('INVALID_ARGUMENT', '--codex-home applies to the codex host only', 2);
|
|
67
|
+
// installDshPlugin re-requires --profile, but failing here keeps the
|
|
68
|
+
// error at the CLI parsing boundary with the other flag-combination
|
|
69
|
+
// errors instead of deep inside the installer.
|
|
70
|
+
if (!options.profile) throw new CliError('INVALID_ARGUMENT', 'the dsh host requires --profile <name>', 2);
|
|
71
|
+
return options.uninstall ? uninstallDshPlugin(paths, installerOptions) : installDshPlugin(paths, installerOptions);
|
|
72
|
+
}
|
|
73
|
+
if (options.profile) throw new CliError('INVALID_ARGUMENT', '--profile applies to the dsh host only', 2);
|
|
74
|
+
if (options.dshHome) throw new CliError('INVALID_ARGUMENT', '--dsh-home applies to the dsh host only', 2);
|
|
50
75
|
if (options.uninstall) {
|
|
51
76
|
const result = uninstallPlugin(paths, installerOptions);
|
|
52
77
|
if (!options.dry_run && result.uninstalled) {
|
package/cli/commands/tasks.mjs
CHANGED
|
@@ -114,7 +114,7 @@ function trimRust(value) {
|
|
|
114
114
|
// outside a string is therefore rejected iff `Number(token)` is not finite.
|
|
115
115
|
// This uses correctly-rounded IEEE rounding; serde_json's default
|
|
116
116
|
// accumulator can round to infinity within one ulp of `f64::MAX` (a
|
|
117
|
-
// recorded residual
|
|
117
|
+
// recorded residual).
|
|
118
118
|
// 3. `JSON.parse` has no container-depth limit (it is iterative), while
|
|
119
119
|
// serde_json starts with `remaining_depth = 128` (de.rs:63) and
|
|
120
120
|
// `check_recursion` decrements before descending and errors when it reaches
|
|
@@ -344,6 +344,12 @@ function rustExtension(fileName) {
|
|
|
344
344
|
// by `profilesDir` (an empty directory yields the bare name).
|
|
345
345
|
function joinChild(dir, name) {
|
|
346
346
|
if (dir === '') return name;
|
|
347
|
+
if (process.platform === 'win32') {
|
|
348
|
+
// Rust Path::join uses '\' on Windows and treats both separators as
|
|
349
|
+
// already-separated components.
|
|
350
|
+
if (dir.endsWith('/') || dir.endsWith('\\')) return dir + name;
|
|
351
|
+
return `${dir}\\${name}`;
|
|
352
|
+
}
|
|
347
353
|
if (dir.endsWith('/')) return dir + name;
|
|
348
354
|
return `${dir}/${name}`;
|
|
349
355
|
}
|
package/cli/commands/update.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { npmUpdateCoordination, reconcileCodexHomes } from '../install/reconcile.mjs';
|
|
2
2
|
import { reconcileZcodeBinding } from '../install/zcode.mjs';
|
|
3
|
+
import { reconcileDshBindings } from '../install/dsh.mjs';
|
|
3
4
|
import { preflightUpdate, reconcileInstallation, updateInstallation } from '../install/update.mjs';
|
|
4
5
|
import { callDaemon, canonicalDaemonCode } from '../rpc.mjs';
|
|
5
6
|
import { CliError } from '../errors.mjs';
|
|
@@ -28,7 +29,16 @@ function partialHostSync(result) {
|
|
|
28
29
|
const { error } = result.zcode;
|
|
29
30
|
failures.push(`zcode: ${error ? `${error.code}: ${error.message}` : 'binding refresh failed'}`);
|
|
30
31
|
}
|
|
31
|
-
|
|
32
|
+
// DSH reconciles per profile; a bound-but-failed profile is a partial
|
|
33
|
+
// sync (retryable through reconcile), an absent binding is not.
|
|
34
|
+
const dshFailed = Array.isArray(result?.dsh?.profiles) && result.dsh.profiles.some((entry) => entry.status === 'failed');
|
|
35
|
+
if (dshFailed) {
|
|
36
|
+
for (const entry of result.dsh.profiles.filter((item) => item.status === 'failed')) {
|
|
37
|
+
const { error } = entry;
|
|
38
|
+
failures.push(`dsh ${entry.profile}: ${error ? `${error.code}: ${error.message}` : 'binding refresh failed'}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return failures.length > 0 ? { zcodeFailed, dshFailed, failures } : null;
|
|
32
42
|
}
|
|
33
43
|
|
|
34
44
|
export async function updateCommand(paths, args = [], daemon = {}) {
|
|
@@ -115,13 +125,14 @@ export async function updateCommand(paths, args = [], daemon = {}) {
|
|
|
115
125
|
const serviceInstalled = typeof daemon.hasInstalledService === 'function' ? daemon.hasInstalledService(paths) : hasInstalledService(paths);
|
|
116
126
|
const serviceDue = serviceInstalled && !daemon.skipServiceActivation;
|
|
117
127
|
result = (reconciling && !coordinate)
|
|
118
|
-
? { ...reconcileInstallation(paths, { cancelActive, yes }), homes: reconcileCodexHomes(paths), zcode: reconcileZcodeBinding(paths) }
|
|
128
|
+
? { ...reconcileInstallation(paths, { cancelActive, yes }), homes: reconcileCodexHomes(paths), zcode: reconcileZcodeBinding(paths), dsh: reconcileDshBindings(paths) }
|
|
119
129
|
: await (daemon.updateInstallation || updateInstallation)(paths, { version: requestedVersion, deferCodexSync: serviceDue });
|
|
120
|
-
// The zcode
|
|
121
|
-
//
|
|
122
|
-
// installs, where the codex homes sync runs inside
|
|
123
|
-
// instead of the service activation block below.
|
|
130
|
+
// The zcode and dsh bindings have neither a registry nor a service
|
|
131
|
+
// dependency, so they refresh on every activation attempt — including
|
|
132
|
+
// service-less installs, where the codex homes sync runs inside
|
|
133
|
+
// updateInstallation instead of the service activation block below.
|
|
124
134
|
if (!result.zcode) result.zcode = reconcileZcodeBinding(paths);
|
|
135
|
+
if (!result.dsh) result.dsh = reconcileDshBindings(paths);
|
|
125
136
|
if (!result || typeof result !== 'object' || result.phase !== 'active') {
|
|
126
137
|
throw new Error(`installation update did not activate payload (phase=${result?.phase ?? 'none'})`);
|
|
127
138
|
}
|
|
@@ -214,6 +225,6 @@ export async function updateCommand(paths, args = [], daemon = {}) {
|
|
|
214
225
|
// error, the receipt, and the per-host registries the reconcile owners
|
|
215
226
|
// already persisted — and the public reconcile command finishes the
|
|
216
227
|
// remaining hosts idempotently. The activation itself stays published.
|
|
217
|
-
throw new CliError(partialHosts.zcodeFailed ? 'ZCODE_SYNC_PARTIAL' : 'CODEX_SYNC_PARTIAL',
|
|
228
|
+
throw new CliError(partialHosts.zcodeFailed ? 'ZCODE_SYNC_PARTIAL' : (partialHosts.dshFailed ? 'DSH_SYNC_PARTIAL' : 'CODEX_SYNC_PARTIAL'),
|
|
218
229
|
`activation completed at ${result?.active?.version || requestedVersion}, but host binding reconciliation is partial — ${partialHosts.failures.join('; ')}; run 'external-subagent reconcile' after fixing the listed hosts`);
|
|
219
230
|
}
|