external-subagent 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -19
- package/bin/external-subagent-debug-mcp.mjs +2 -1
- package/bin/external-subagent-mcp.mjs +36 -11
- package/cli/commands/config.mjs +6 -1
- package/cli/commands/daemon.mjs +12 -2
- package/cli/config/write.mjs +21 -3
- package/cli/constants.mjs +35 -1
- package/cli/install/init.mjs +30 -11
- package/cli/install/layout.mjs +11 -4
- package/cli/install/path.mjs +35 -10
- package/cli/install/payload.mjs +86 -6
- package/cli/install/service-activation.mjs +133 -1
- package/cli/install/service-linux.mjs +327 -0
- package/cli/install/service-macos.mjs +59 -18
- package/cli/install/update.mjs +7 -1
- package/cli/main.mjs +25 -11
- package/cli/maintenance.mjs +16 -11
- package/cli/paths.mjs +57 -8
- 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/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 +20 -0
- package/package.json +6 -3
- package/plugins/codex/external-subagent/.codex-plugin/plugin.json +1 -1
- package/plugins/codex/external-subagent/scripts/policy-verifier.mjs +16 -2
- package/scripts/release/build-native-payload.mjs +18 -6
- package/scripts/release/check-native-tarball.mjs +99 -31
- package/scripts/release/test-installed-tarball.mjs +6 -1
package/cli/main.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
|
-
import { BUSINESS_COMMANDS, PRODUCT_NAME, VERSION,
|
|
4
|
+
import { BUSINESS_COMMANDS, PRODUCT_NAME, VERSION, zcodeRuntimePath } from './constants.mjs';
|
|
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';
|
|
@@ -17,7 +17,12 @@ import { parseConfigArgs } from './commands/config.mjs';
|
|
|
17
17
|
import { subagentsCommand, parseSubagentsArgs } from './commands/agents.mjs';
|
|
18
18
|
import { parseProfileArgs, parseSpawnArgs, prepareSpawnInput, profileCommand } from './commands/tasks.mjs';
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
// Hosts that ship a supported service backend and payload: macOS (launchd)
|
|
21
|
+
// and Linux (systemd user services). The gate is structural, so any other
|
|
22
|
+
// platform is rejected before product paths are resolved or written.
|
|
23
|
+
const SUPPORTED_PLATFORMS = new Set(['darwin', 'linux']);
|
|
24
|
+
|
|
25
|
+
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 service\n profile list | profile show <name>\n Inspect global spawn profiles (profiles/*.json);\n invalid files are reported as path-based warnings,\n and warned or duplicate files are excluded from the\n available name set\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
26
|
const DAEMON_HELP = ` config get [key] | config set <key> <value>\n subagents list | subagents status [subagent] | subagents probe/models [subagent]\n create/spawn [--profile <name>], 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
27
|
|
|
23
28
|
function structuredInput(args, parser) {
|
|
@@ -262,7 +267,11 @@ async function diagnose(paths, args) {
|
|
|
262
267
|
scope: agent ? { agent_id: agent } : { kind: 'global' },
|
|
263
268
|
platform: platform(),
|
|
264
269
|
runtime: {
|
|
265
|
-
|
|
270
|
+
// The same platform-aware resolution the service generators use
|
|
271
|
+
// (constants.mjs): explicit ZCODE_RUNTIME_PATH, then the platform's
|
|
272
|
+
// conventional installation, then the packaged pin — reported as an
|
|
273
|
+
// artifact observation, never a probe.
|
|
274
|
+
configured_artifact: fileArtifact(zcodeRuntimePath(paths.home), 'cli_resolved_configuration'),
|
|
266
275
|
running_identity: null,
|
|
267
276
|
running_identity_source: 'not_observed_by_cli',
|
|
268
277
|
},
|
|
@@ -332,7 +341,10 @@ export async function main(args) {
|
|
|
332
341
|
process.stdout.write(`${VERSION}\n`); return;
|
|
333
342
|
}
|
|
334
343
|
if (command !== 'profile' && !BUSINESS_COMMANDS.has(command)) throw new CliError('UNKNOWN_COMMAND', `unknown command: ${command}`, 2);
|
|
335
|
-
|
|
344
|
+
// macOS and Linux are supported hosts; every other platform (Windows, BSD,
|
|
345
|
+
// an unknown tuple) is refused structurally before any filesystem side
|
|
346
|
+
// effect, so an unsupported host never writes HOME state.
|
|
347
|
+
if (!SUPPORTED_PLATFORMS.has(platform())) throw new CliError('UNSUPPORTED_PLATFORM', `${command} is supported only on macOS and Linux`);
|
|
336
348
|
|
|
337
349
|
const paths = productPaths();
|
|
338
350
|
if (command === 'init') {
|
|
@@ -373,13 +385,15 @@ export async function main(args) {
|
|
|
373
385
|
if (command === 'status') {
|
|
374
386
|
const verbose = args.includes('--verbose');
|
|
375
387
|
const local = localInstallStatus(paths, { verbose });
|
|
376
|
-
// `service` is the read-only
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
|
|
388
|
+
// `service` is the read-only backend view (launchd registered job or the
|
|
389
|
+
// systemd user unit's real state — `failed` included, never a fake
|
|
390
|
+
// healthy) selected by the platform dispatch in service-macos.mjs; a Linux
|
|
391
|
+
// host without a reachable user manager degrades to an explicit
|
|
392
|
+
// `unavailable` view with the linger hint instead of failing the whole
|
|
393
|
+
// read-only command. `daemon_status` stays the RPC view, so a
|
|
394
|
+
// loaded-but-unready or ready-but-unregistered install reads differently
|
|
395
|
+
// instead of blurring.
|
|
396
|
+
const { pid: _pid, ...service } = serviceRegistrationStatus();
|
|
383
397
|
try {
|
|
384
398
|
output({ ...local, service, daemon_status: publicDaemonStatus(await callDaemon(process.env.EXTERNAL_SUBAGENT_SOCKET || paths.socket, 'status', {}), { verbose }) });
|
|
385
399
|
} catch (error) {
|
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 { productPaths } from './paths.mjs';
|
|
5
|
+
import { platform, productPaths } from './paths.mjs';
|
|
6
6
|
import { bootoutService } from './install/service-macos.mjs';
|
|
7
7
|
|
|
8
8
|
function copyTree(source, destination, records, root = source) {
|
|
@@ -75,20 +75,25 @@ function removeOne(target) {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
// Deleting the
|
|
79
|
-
// the next logout (observed live: uninstall reported
|
|
80
|
-
// process, the launchd registration, and the RPC
|
|
81
|
-
// The ES-owned service is therefore booted out —
|
|
82
|
-
// removal
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
78
|
+
// Deleting the definition alone leaves a job the service manager already
|
|
79
|
+
// loaded running until the next logout (observed live: uninstall reported
|
|
80
|
+
// success while the daemon process, the launchd registration, and the RPC
|
|
81
|
+
// socket all stayed alive). The ES-owned service is therefore booted out —
|
|
82
|
+
// with the bounded removal confirmation bootoutService owns on BOTH backends
|
|
83
|
+
// (launchd bootout on macOS, systemctl --user disable --now plus the inactive
|
|
84
|
+
// confirmation on Linux) — before its definition file is removed. A service
|
|
85
|
+
// that is not registered is not an error (idempotent uninstall); a stop that
|
|
86
|
+
// cannot complete fails the command rather than deleting the definition out
|
|
87
|
+
// from under a still-running service. `removed_launch_agent` is macOS-only;
|
|
88
|
+
// Linux reports the same removal as `removed_service_definition`.
|
|
86
89
|
export function uninstall(paths = productPaths(), options = {}) {
|
|
87
|
-
const
|
|
90
|
+
const macos = platform() === 'darwin';
|
|
91
|
+
const service = bootoutService(paths, process.getuid(), { launchctl: options.launchctl, systemctl: options.systemctl });
|
|
92
|
+
const removed = removeOne(paths.launchAgent);
|
|
88
93
|
return {
|
|
89
94
|
service_stopped: true,
|
|
90
95
|
service_already_stopped: Boolean(service.already_stopped),
|
|
91
|
-
removed_launch_agent:
|
|
96
|
+
...(macos ? { removed_launch_agent: removed } : { removed_service_definition: removed }),
|
|
92
97
|
data_retained: true,
|
|
93
98
|
data: paths.data,
|
|
94
99
|
};
|
package/cli/paths.mjs
CHANGED
|
@@ -3,14 +3,16 @@ import path from 'node:path';
|
|
|
3
3
|
import { LAUNCH_AGENT_LABEL, PRODUCT_NAME } from './constants.mjs';
|
|
4
4
|
import { PLUGIN_NAME } from './install/layout.mjs';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
// The process platform, overridable by the EXTERNAL_SUBAGENT_TEST_PLATFORM seam
|
|
7
|
+
// so both layouts are exercisable on any host. The seam keeps its original
|
|
8
|
+
// meaning: it is a *process platform* override, nothing else.
|
|
9
|
+
export function platform(env = process.env) {
|
|
10
|
+
return env.EXTERNAL_SUBAGENT_TEST_PLATFORM || process.platform;
|
|
8
11
|
}
|
|
9
12
|
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
export function productPaths(home = os.homedir()) {
|
|
13
|
+
// macOS derives the whole tree from `~/Library`; the bytes of every field below
|
|
14
|
+
// are contractually frozen (existing installs must keep resolving their state).
|
|
15
|
+
function darwinProductPaths(home) {
|
|
14
16
|
const data = path.join(home, 'Library', 'Application Support', PRODUCT_NAME);
|
|
15
17
|
return {
|
|
16
18
|
home,
|
|
@@ -28,6 +30,52 @@ export function productPaths(home = os.homedir()) {
|
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
// XDG Base Directory resolution (D-S02). A variable selects its directory only
|
|
34
|
+
// when it is a non-empty absolute path; relative or empty values are ignored and
|
|
35
|
+
// the spec fallback under $HOME applies. Only the three XDG variables the
|
|
36
|
+
// product owns are read — no other user XDG directory is probed.
|
|
37
|
+
function xdgDirectory(env, variable, home, fallback) {
|
|
38
|
+
const configured = env[variable];
|
|
39
|
+
if (typeof configured === 'string' && configured !== '' && path.isAbsolute(configured)) {
|
|
40
|
+
return configured;
|
|
41
|
+
}
|
|
42
|
+
return path.join(home, ...fallback);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Linux keeps the same internal relative layout as macOS (config, install
|
|
46
|
+
// state, database, socket, hook provenance, plugin staging, profiles all live
|
|
47
|
+
// inside the product data directory) so the two platforms stay structurally
|
|
48
|
+
// parallel. Data and logs follow XDG; the systemd user unit directory is
|
|
49
|
+
// provided for the S03 service backend.
|
|
50
|
+
function linuxProductPaths(home, env) {
|
|
51
|
+
const data = path.join(xdgDirectory(env, 'XDG_DATA_HOME', home, ['.local', 'share']), PRODUCT_NAME);
|
|
52
|
+
const logs = path.join(xdgDirectory(env, 'XDG_STATE_HOME', home, ['.local', 'state']), PRODUCT_NAME);
|
|
53
|
+
const configHome = xdgDirectory(env, 'XDG_CONFIG_HOME', home, ['.config']);
|
|
54
|
+
return {
|
|
55
|
+
home,
|
|
56
|
+
data,
|
|
57
|
+
config: path.join(data, 'config.json'),
|
|
58
|
+
state: path.join(data, 'install-state.json'),
|
|
59
|
+
hookProvenance: path.join(data, 'zcode-agent-hook-provenance.json'),
|
|
60
|
+
database: path.join(data, `${PRODUCT_NAME}.sqlite3`),
|
|
61
|
+
socket: path.join(data, `${PRODUCT_NAME}.sock`),
|
|
62
|
+
logs,
|
|
63
|
+
launchAgent: path.join(configHome, 'systemd', 'user', `${PRODUCT_NAME}.service`),
|
|
64
|
+
zcodeConfig: path.join(home, '.zcode', 'cli', 'config.json'),
|
|
65
|
+
zcodePlugin: path.join(data, 'zcode-plugin', PLUGIN_NAME),
|
|
66
|
+
profiles: path.join(data, 'profiles'),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// State paths derive from PRODUCT_NAME, so the debug variant gets a fully
|
|
71
|
+
// parallel tree (its own data directory, database, socket, and logs) and
|
|
72
|
+
// never touches the released product's state. The layout is selected by the
|
|
73
|
+
// process platform: macOS `~/Library`, every other supported host the XDG
|
|
74
|
+
// baseline. `zcodeConfig` is `~/.zcode/cli/config.json` on both platforms.
|
|
75
|
+
export function productPaths(home = os.homedir(), env = process.env) {
|
|
76
|
+
return platform(env) === 'darwin' ? darwinProductPaths(home) : linuxProductPaths(home, env);
|
|
77
|
+
}
|
|
78
|
+
|
|
31
79
|
// Mirror the daemon's `profiles_directory()` exactly (rpc/profiles.rs):
|
|
32
80
|
// `EXTERNAL_SUBAGENT_CONFIG` wins when it is *exported*, even when exported as
|
|
33
81
|
// an empty string, and only then does `ZCODE_AGENT_CONFIG` apply; presence, not
|
|
@@ -35,7 +83,8 @@ export function productPaths(home = os.homedir()) {
|
|
|
35
83
|
// exported path has no parent in Rust (`Path::new("").parent() == None`), so the
|
|
36
84
|
// daemon loads no profiles directory at all; return null to represent that.
|
|
37
85
|
// Otherwise take the sibling `profiles/` of the config file, or fall back to the
|
|
38
|
-
// product data directory
|
|
86
|
+
// product data directory (the daemon's data fallback is the same XDG/macOS data
|
|
87
|
+
// root selected here, so both sides resolve one directory).
|
|
39
88
|
|
|
40
89
|
// Rust `Path::parent()` semantics on Unix, implemented with plain string
|
|
41
90
|
// slicing (never `path.dirname`/`path.join`, which lexically normalise and would
|
|
@@ -83,7 +132,7 @@ export function profilesDir(env = {}, home = os.homedir()) {
|
|
|
83
132
|
break;
|
|
84
133
|
}
|
|
85
134
|
}
|
|
86
|
-
if (!exported) return path.join(productPaths(home).data, 'profiles');
|
|
135
|
+
if (!exported) return path.join(productPaths(home, env).data, 'profiles');
|
|
87
136
|
const parent = rustPathParent(envPath);
|
|
88
137
|
if (parent === null) return null;
|
|
89
138
|
return joinProfiles(parent);
|
|
Binary file
|
|
Binary file
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
3
|
"product": "external-subagent",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.1",
|
|
5
5
|
"platform": "darwin-arm64",
|
|
6
6
|
"files": [
|
|
7
7
|
{
|
|
8
8
|
"name": "external-subagentd",
|
|
9
|
-
"bytes":
|
|
9
|
+
"bytes": 10075248,
|
|
10
10
|
"mode": "755",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "eb0b1c4ffb4ab44678adf5aa39ee96f61cf09ebdb03b74da9e12cebad2b868f5"
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
14
|
"name": "external-subagent-mcp",
|
|
15
15
|
"bytes": 935888,
|
|
16
16
|
"mode": "755",
|
|
17
|
-
"sha256": "
|
|
17
|
+
"sha256": "1dce8a0c5566af2d9b4a12a39ad43c819bcb7f395a1a7c738389e5b0bbd24346"
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
20
|
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"product": "external-subagent",
|
|
4
|
+
"version": "0.5.1",
|
|
5
|
+
"platform": "linux-x64",
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"name": "external-subagentd",
|
|
9
|
+
"bytes": 11452160,
|
|
10
|
+
"mode": "755",
|
|
11
|
+
"sha256": "c4f410496c2f9390288cb168e447127c9ee7ec0b47c591bcd84f25f688cad3af"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "external-subagent-mcp",
|
|
15
|
+
"bytes": 1025360,
|
|
16
|
+
"mode": "755",
|
|
17
|
+
"sha256": "6553125c48dcd1a21554a6dd1181a7cb55d84f874118a1bd5136b5b29e916e60"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "external-subagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Manage bounded ZCode, DeepSeek Harness (DSH), Codex, and Google Antigravity (agy) 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",
|
|
@@ -21,10 +21,12 @@
|
|
|
21
21
|
"agent-tasks"
|
|
22
22
|
],
|
|
23
23
|
"os": [
|
|
24
|
-
"darwin"
|
|
24
|
+
"darwin",
|
|
25
|
+
"linux"
|
|
25
26
|
],
|
|
26
27
|
"cpu": [
|
|
27
|
-
"arm64"
|
|
28
|
+
"arm64",
|
|
29
|
+
"x64"
|
|
28
30
|
],
|
|
29
31
|
"bin": {
|
|
30
32
|
"external-subagent": "bin/external-subagent.mjs",
|
|
@@ -52,6 +54,7 @@
|
|
|
52
54
|
"postpack": "node scripts/release/check-native-tarball.mjs",
|
|
53
55
|
"prepublishOnly": "node scripts/release/check-native-tarball.mjs --staged",
|
|
54
56
|
"test:install": "node --test tests/install/*.test.mjs",
|
|
57
|
+
"test:release": "node --test tests/release/*.test.mjs",
|
|
55
58
|
"test:affected": "node scripts/test/affected.mjs",
|
|
56
59
|
"check": "node --check bin/external-subagent.mjs && node --check bin/external-subagent-mcp.mjs && node --check bin/external-subagent-debug.mjs && node --check bin/external-subagent-debug-mcp.mjs && node --check cli/main.mjs"
|
|
57
60
|
},
|
|
@@ -25,8 +25,22 @@ const script = hook?.args?.find((candidate) => typeof candidate === 'string' &&
|
|
|
25
25
|
const guard = events.PreToolUse.find((entry) => entry?.matcher === 'Bash')?.hooks?.find((entry) => entry?.type === 'process')?.args?.[0];
|
|
26
26
|
const audit = events.PostToolUse?.find((entry) => entry?.matcher === 'Bash')?.hooks?.find((entry) => entry?.type === 'process')?.args?.[0];
|
|
27
27
|
if (!script || !fs.statSync(script).isFile() || hook.command !== process.execPath || !guard || !fs.statSync(guard).isFile() || !audit || !fs.statSync(audit).isFile()) process.exit(2);
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// The provenance is staged by install-agent-hooks.mjs beside this verifier,
|
|
29
|
+
// inside the product data directory of the probe home. The candidates mirror
|
|
30
|
+
// the daemon's policy_verifier_candidates exactly: the frozen macOS
|
|
31
|
+
// `~/Library/Application Support` bytes on darwin; elsewhere an exported
|
|
32
|
+
// absolute $XDG_DATA_HOME is probed first, then the `~/.local/share` XDG
|
|
33
|
+
// fallback for the scope home.
|
|
34
|
+
const provenanceCandidates = process.platform === 'darwin'
|
|
35
|
+
? [path.join(home, 'Library', 'Application Support', 'external-subagent', 'zcode-agent-hook-provenance.json')]
|
|
36
|
+
: [
|
|
37
|
+
...(typeof process.env.XDG_DATA_HOME === 'string' && path.isAbsolute(process.env.XDG_DATA_HOME)
|
|
38
|
+
? [path.join(process.env.XDG_DATA_HOME, 'external-subagent', 'zcode-agent-hook-provenance.json')]
|
|
39
|
+
: []),
|
|
40
|
+
path.join(home, '.local', 'share', 'external-subagent', 'zcode-agent-hook-provenance.json'),
|
|
41
|
+
];
|
|
42
|
+
const provenancePath = provenanceCandidates.find((candidate) => fs.existsSync(candidate));
|
|
43
|
+
if (!provenancePath) process.exit(2);
|
|
30
44
|
const provenance = JSON.parse(fs.readFileSync(provenancePath, 'utf8'));
|
|
31
45
|
const hash = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
32
46
|
const policy = path.resolve(path.dirname(script), '../lib/agent-file-policy.mjs');
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Build the versioned native payload for the npm package: release binaries
|
|
3
3
|
// for external-subagentd and external-subagent-mcp are copied into
|
|
4
|
-
// npm/native
|
|
5
|
-
// (version, platform, per-file bytes/sha256/mode)
|
|
4
|
+
// npm/native/<platform> together with a deterministic payload.json manifest
|
|
5
|
+
// (version, platform, per-file bytes/sha256/mode), where <platform> is derived
|
|
6
|
+
// from the running host (darwin-arm64, linux-x64). Installed-package code
|
|
6
7
|
// verifies against this manifest, so it must never contain volatile fields.
|
|
7
8
|
//
|
|
8
9
|
// `--variant debug` stages the parallel development payload instead: the same
|
|
9
|
-
// cargo artifacts are copied under debug names into npm/native-debug
|
|
10
|
+
// cargo artifacts are copied under debug names into npm/native-debug/<platform>
|
|
10
11
|
// (manifest product external-subagent-debug), and the debug plugin source is
|
|
11
12
|
// staged at plugins/codex/external-subagent-debug. The debug payload is a
|
|
12
13
|
// development checkout product and is rejected from release tarballs by
|
|
@@ -20,8 +21,19 @@ import crypto from 'node:crypto';
|
|
|
20
21
|
import { spawnSync } from 'node:child_process';
|
|
21
22
|
import { fileURLToPath } from 'node:url';
|
|
22
23
|
import { stageDebugPlugin } from './stage-debug-plugin.mjs';
|
|
24
|
+
import { nativePlatform } from '../../cli/install/layout.mjs';
|
|
23
25
|
|
|
24
26
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
27
|
+
// The staged directory and the manifest `platform` field derive from the
|
|
28
|
+
// running host through the shared platform table, so one script stages the
|
|
29
|
+
// correct tuple everywhere (release: darwin-arm64, linux-x64) and an
|
|
30
|
+
// unsupported host fails before touching the tree. The debug variant reuses
|
|
31
|
+
// the same tuple under its parallel native-debug directory.
|
|
32
|
+
const platform = nativePlatform();
|
|
33
|
+
if (platform === null) {
|
|
34
|
+
process.stderr.write(`no native payload for ${process.platform}-${process.arch}\n`);
|
|
35
|
+
process.exit(2);
|
|
36
|
+
}
|
|
25
37
|
const variantIndex = process.argv.indexOf('--variant');
|
|
26
38
|
const variant = variantIndex === -1 ? '' : (process.argv[variantIndex + 1] ?? '');
|
|
27
39
|
if (variant !== '' && variant !== 'debug') {
|
|
@@ -42,7 +54,7 @@ const productName = isDebug ? 'external-subagent-debug' : 'external-subagent';
|
|
|
42
54
|
const binaries = isDebug
|
|
43
55
|
? [['external-subagentd', 'external-subagent-debugd'], ['external-subagent-mcp', 'external-subagent-debug-mcp']]
|
|
44
56
|
: [['external-subagentd', 'external-subagentd'], ['external-subagent-mcp', 'external-subagent-mcp']];
|
|
45
|
-
const platformDir = path.join(packageRoot, 'npm', nativeDirName,
|
|
57
|
+
const platformDir = path.join(packageRoot, 'npm', nativeDirName, platform);
|
|
46
58
|
const manifestPath = path.join(platformDir, 'payload.json');
|
|
47
59
|
const sourceRoots = ['crates', 'profiles'].map((dir) => path.join(packageRoot, dir))
|
|
48
60
|
.concat([path.join(packageRoot, 'Cargo.toml'), path.join(packageRoot, 'Cargo.lock')]);
|
|
@@ -73,7 +85,7 @@ function stagedIsCurrent(version) {
|
|
|
73
85
|
if (!fs.existsSync(manifestPath)) return false;
|
|
74
86
|
let manifest;
|
|
75
87
|
try { manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); } catch { return false; }
|
|
76
|
-
if (manifest.version !== version || manifest.platform !==
|
|
88
|
+
if (manifest.version !== version || manifest.platform !== platform || manifest.product !== productName) return false;
|
|
77
89
|
if ((manifest.profile ?? 'release') !== cargoProfile) return false;
|
|
78
90
|
const inputs = newestInputMs();
|
|
79
91
|
for (const file of manifest.files) {
|
|
@@ -121,7 +133,7 @@ for (const [source, name] of binaries) {
|
|
|
121
133
|
files.push({ name, bytes: bytes.length, mode: '755', sha256: sha256(bytes) });
|
|
122
134
|
}
|
|
123
135
|
|
|
124
|
-
const manifest = { schema_version: 1, product: productName, version, platform
|
|
136
|
+
const manifest = { schema_version: 1, product: productName, version, platform };
|
|
125
137
|
// The debug profile is recorded so --if-stale can tell a debug-staged payload
|
|
126
138
|
// from a release one; the release manifest stays byte-identical to before.
|
|
127
139
|
if (cargoProfile !== 'release') manifest.profile = cargoProfile;
|
|
@@ -3,26 +3,47 @@
|
|
|
3
3
|
// check-native-tarball.mjs [package.tgz] verify a packed tarball (entries,
|
|
4
4
|
// payload manifest consistency,
|
|
5
5
|
// forbidden development material)
|
|
6
|
-
// check-native-tarball.mjs --staged verify only the staged
|
|
7
|
-
// (npm/native
|
|
6
|
+
// check-native-tarball.mjs --staged verify only the staged payloads
|
|
7
|
+
// (npm/native/<platform>) against
|
|
8
8
|
// the package version — the publish
|
|
9
9
|
// gate, since `npm publish` packs
|
|
10
10
|
// into a private temp directory no
|
|
11
11
|
// lifecycle script can inspect.
|
|
12
|
+
// A released tarball carries every supported platform payload (darwin-arm64
|
|
13
|
+
// and linux-x64), so both entry sets and both manifests are required in either
|
|
14
|
+
// mode: a tarball missing one platform is rejected. Version agreement with the
|
|
15
|
+
// packed package is enforced for the running host's payload (prepack rebuilds
|
|
16
|
+
// only the host payload from source) and, in --staged mode, for every staged
|
|
17
|
+
// platform — the merged publish tree where the foreign payload comes from CI's
|
|
18
|
+
// other platform job. Image checks (Mach-O arm64 / ELF x86-64) and the 755
|
|
19
|
+
// mode check use the same probes and layout table as cli/install/payload.mjs,
|
|
20
|
+
// so this gate cannot drift from what an installed package verifies.
|
|
12
21
|
// Without --staged the tarball is resolved from: the explicit argument, the
|
|
13
22
|
// npm --pack-destination directory (set during `npm pack` lifecycles), or the
|
|
14
23
|
// newest matching tgz in the working directory. A dry run (npm pack --dry-run)
|
|
15
24
|
// or a publish (no local tarball) skips the tarball checks and still runs the
|
|
16
25
|
// staged payload checks.
|
|
17
26
|
import fs from 'node:fs';
|
|
18
|
-
import os from 'node:os';
|
|
19
27
|
import path from 'node:path';
|
|
20
28
|
import { spawnSync } from 'node:child_process';
|
|
29
|
+
import { elfArch, machoArch } from '../../cli/install/payload.mjs';
|
|
30
|
+
import { nativePlatform } from '../../cli/install/layout.mjs';
|
|
21
31
|
|
|
22
32
|
const stagedOnly = process.argv.includes('--staged');
|
|
23
33
|
const explicit = process.argv[2] && !process.argv[2].startsWith('--') ? process.argv[2] : null;
|
|
24
34
|
const dryRun = process.env.npm_config_dry_run === 'true';
|
|
25
35
|
|
|
36
|
+
// Every platform the single release tarball ships, and the image identity each
|
|
37
|
+
// payload must carry. Keeping both lists here (rather than a fixed
|
|
38
|
+
// darwin-arm64 path) is what makes the gate dual-platform.
|
|
39
|
+
const RELEASE_PLATFORMS = Object.freeze(['darwin-arm64', 'linux-x64']);
|
|
40
|
+
const BINARIES = Object.freeze(['external-subagentd', 'external-subagent-mcp']);
|
|
41
|
+
const IMAGE_CHECKS = Object.freeze({
|
|
42
|
+
'darwin-arm64': { describe: 'Mach-O arm64 executable', probe: machoArch, arch: 'arm64' },
|
|
43
|
+
'linux-x64': { describe: 'ELF x86-64 executable', probe: elfArch, arch: 'x64' },
|
|
44
|
+
});
|
|
45
|
+
const hostPlatform = nativePlatform();
|
|
46
|
+
|
|
26
47
|
function findTarball() {
|
|
27
48
|
if (explicit) return explicit;
|
|
28
49
|
const candidates = [];
|
|
@@ -57,14 +78,17 @@ if (!stagedOnly) {
|
|
|
57
78
|
'package/bin/external-subagent-debug.mjs',
|
|
58
79
|
'package/bin/external-subagent-debug-mcp.mjs',
|
|
59
80
|
'package/cli/main.mjs',
|
|
60
|
-
'package/npm/native/darwin-arm64/external-subagentd',
|
|
61
|
-
'package/npm/native/darwin-arm64/external-subagent-mcp',
|
|
62
|
-
'package/npm/native/darwin-arm64/payload.json',
|
|
63
81
|
'package/plugins/codex/external-subagent/.codex-plugin/plugin.json',
|
|
64
82
|
'package/plugins/codex/external-subagent/.mcp.json',
|
|
65
83
|
'package/launchd/com.external-subagent.daemon.plist.template',
|
|
66
84
|
'package/LICENSE',
|
|
67
85
|
];
|
|
86
|
+
// Both platform payloads are mandatory: a tarball that ships only one is
|
|
87
|
+
// rejected here (and by the --staged publish gate) before it can be
|
|
88
|
+
// published.
|
|
89
|
+
for (const platform of RELEASE_PLATFORMS) {
|
|
90
|
+
for (const name of [...BINARIES, 'payload.json']) required.push(`package/npm/native/${platform}/${name}`);
|
|
91
|
+
}
|
|
68
92
|
|
|
69
93
|
const listing = spawnSync('tar', ['-tzf', tgz], { encoding: 'utf8' });
|
|
70
94
|
if (listing.status !== 0) {
|
|
@@ -85,38 +109,82 @@ if (!stagedOnly) {
|
|
|
85
109
|
}
|
|
86
110
|
|
|
87
111
|
const packedPackage = JSON.parse(spawnSync('tar', ['-xzf', tgz, '-O', 'package/package.json'], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }).stdout);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
112
|
+
for (const platform of RELEASE_PLATFORMS) {
|
|
113
|
+
const manifestText = spawnSync('tar', ['-xzf', tgz, '-O', `package/npm/native/${platform}/payload.json`], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }).stdout;
|
|
114
|
+
let manifest;
|
|
115
|
+
try {
|
|
116
|
+
manifest = JSON.parse(manifestText);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
process.stderr.write(`payload manifest for ${platform} is not valid JSON: ${error.message}\n`);
|
|
119
|
+
failed = true;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (manifest.product !== 'external-subagent' || manifest.platform !== platform) {
|
|
123
|
+
process.stderr.write(`payload manifest for ${platform} has an inconsistent identity (${manifest.product}/${manifest.platform})\n`);
|
|
124
|
+
failed = true;
|
|
125
|
+
}
|
|
126
|
+
if (platform === hostPlatform && manifest.version !== packedPackage.version) {
|
|
127
|
+
process.stderr.write(`payload version ${manifest.version} (${platform}) does not match package version ${packedPackage.version}\n`);
|
|
128
|
+
failed = true;
|
|
129
|
+
}
|
|
93
130
|
}
|
|
94
131
|
}
|
|
95
132
|
}
|
|
96
133
|
|
|
97
|
-
// Staged payload checks
|
|
98
|
-
//
|
|
99
|
-
//
|
|
134
|
+
// Staged payload checks: the staged native binaries and the package version
|
|
135
|
+
// must agree right where the tarball is built from. The publish gate
|
|
136
|
+
// (--staged) checks every platform because CI merges both artifacts into one
|
|
137
|
+
// tree before packing; a plain `npm pack` checks the running host's platform,
|
|
138
|
+
// whose payload prepack rebuilds from source (the foreign platform is required
|
|
139
|
+
// to be present by the tarball entry checks above).
|
|
100
140
|
const repoPackage = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8'));
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
141
|
+
function checkStagedPlatform(platform) {
|
|
142
|
+
const dir = path.join(process.cwd(), 'npm', 'native', platform);
|
|
143
|
+
const manifestPath = path.join(dir, 'payload.json');
|
|
144
|
+
let manifest;
|
|
145
|
+
try {
|
|
146
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
147
|
+
} catch (error) {
|
|
148
|
+
process.stderr.write(`staged ${platform} payload manifest is unavailable: ${error.message}\n`);
|
|
149
|
+
failed = true;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (manifest.version !== repoPackage.version) {
|
|
153
|
+
process.stderr.write(`staged ${platform} payload version ${manifest.version} does not match package version ${repoPackage.version}\n`);
|
|
154
|
+
failed = true;
|
|
155
|
+
}
|
|
156
|
+
if (manifest.product !== 'external-subagent') {
|
|
157
|
+
process.stderr.write(`staged ${platform} payload product ${manifest.product} is not the release payload\n`);
|
|
116
158
|
failed = true;
|
|
117
159
|
}
|
|
118
|
-
|
|
119
|
-
|
|
160
|
+
if (manifest.platform !== platform) {
|
|
161
|
+
process.stderr.write(`staged ${platform} payload manifest declares ${manifest.platform}\n`);
|
|
162
|
+
failed = true;
|
|
163
|
+
}
|
|
164
|
+
const image = IMAGE_CHECKS[platform];
|
|
165
|
+
for (const name of BINARIES) {
|
|
166
|
+
const local = path.join(dir, name);
|
|
167
|
+
let bytes;
|
|
168
|
+
try {
|
|
169
|
+
bytes = fs.readFileSync(local);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
process.stderr.write(`${platform}/${name} is missing from the staged payload: ${error.code}\n`);
|
|
172
|
+
failed = true;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (image.probe(bytes) !== image.arch) {
|
|
176
|
+
process.stderr.write(`${platform}/${name} is not a ${image.describe}\n`);
|
|
177
|
+
failed = true;
|
|
178
|
+
}
|
|
179
|
+
const mode = (fs.statSync(local).mode & 0o777).toString(8);
|
|
180
|
+
if (mode !== '755') { process.stderr.write(`${platform}/${name} has release mode ${mode}, expected 755\n`); failed = true; }
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const stagedPlatforms = stagedOnly ? RELEASE_PLATFORMS : (hostPlatform ? [hostPlatform] : []);
|
|
185
|
+
for (const platform of stagedPlatforms) checkStagedPlatform(platform);
|
|
186
|
+
if (stagedPlatforms.length === 0) {
|
|
187
|
+
note(`no staged release platform for ${process.platform}-${process.arch}; staged payload checks skipped\n`);
|
|
120
188
|
}
|
|
121
189
|
|
|
122
190
|
if (failed) process.exit(1);
|
|
@@ -10,6 +10,7 @@ import fs from 'node:fs';
|
|
|
10
10
|
import os from 'node:os';
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import { spawnSync } from 'node:child_process';
|
|
13
|
+
import { nativePlatform } from '../../cli/install/layout.mjs';
|
|
13
14
|
|
|
14
15
|
function run(command, args, options = {}) {
|
|
15
16
|
const result = spawnSync(command, args, { encoding: 'utf8', ...options });
|
|
@@ -38,7 +39,11 @@ try {
|
|
|
38
39
|
run('npm', ['install', '--global', `--prefix=${prefix}`, '--no-audit', '--no-fund', tgz], { cwd: work, env });
|
|
39
40
|
const cli = path.join(prefix, 'bin', 'external-subagent');
|
|
40
41
|
const packageRoot = path.join(prefix, 'lib', 'node_modules', 'external-subagent');
|
|
41
|
-
|
|
42
|
+
// The packed payload directory is the running host's tuple (S01), so the
|
|
43
|
+
// smoke checks the platform it is actually installing rather than a fixed
|
|
44
|
+
// darwin-arm64 path.
|
|
45
|
+
const payloadPlatform = nativePlatform() ?? `${process.platform}-${process.arch}`;
|
|
46
|
+
for (const target of [cli, path.join(prefix, 'bin', 'external-subagent-mcp'), path.join(packageRoot, 'npm', 'native', payloadPlatform, 'external-subagentd')]) {
|
|
42
47
|
fs.accessSync(target, fs.constants.X_OK);
|
|
43
48
|
}
|
|
44
49
|
run(cli, ['version'], { env });
|