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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/bin/external-subagent-debug-mcp.mjs +5 -0
  4. package/bin/external-subagent-debug.mjs +13 -0
  5. package/bin/external-subagent-mcp.mjs +20 -0
  6. package/bin/external-subagent.mjs +8 -0
  7. package/bin/zas.mjs +8 -0
  8. package/cli/commands/agents.mjs +117 -0
  9. package/cli/commands/config.mjs +117 -0
  10. package/cli/commands/daemon.mjs +36 -0
  11. package/cli/commands/maintenance.mjs +27 -0
  12. package/cli/commands/plugin.mjs +96 -0
  13. package/cli/commands/tasks.mjs +64 -0
  14. package/cli/commands/update.mjs +219 -0
  15. package/cli/config/read.mjs +24 -0
  16. package/cli/config/schema.mjs +75 -0
  17. package/cli/config/write.mjs +39 -0
  18. package/cli/constants.mjs +24 -0
  19. package/cli/errors.mjs +7 -0
  20. package/cli/fs-atomic.mjs +36 -0
  21. package/cli/install/codex.mjs +352 -0
  22. package/cli/install/init.mjs +206 -0
  23. package/cli/install/layout.mjs +54 -0
  24. package/cli/install/npm-hook.mjs +49 -0
  25. package/cli/install/path.mjs +43 -0
  26. package/cli/install/payload.mjs +84 -0
  27. package/cli/install/plugin-stage.mjs +232 -0
  28. package/cli/install/reconcile.mjs +209 -0
  29. package/cli/install/recovery.mjs +79 -0
  30. package/cli/install/service-activation.mjs +133 -0
  31. package/cli/install/service-macos.mjs +146 -0
  32. package/cli/install/update.mjs +170 -0
  33. package/cli/install/zcode.mjs +239 -0
  34. package/cli/main.mjs +429 -0
  35. package/cli/maintenance.mjs +104 -0
  36. package/cli/paths.mjs +40 -0
  37. package/cli/rpc.mjs +261 -0
  38. package/launchd/com.external-subagent.daemon.plist.template +37 -0
  39. package/npm/native/README.md +19 -0
  40. package/npm/native/darwin-arm64/external-subagent-mcp +0 -0
  41. package/npm/native/darwin-arm64/external-subagentd +0 -0
  42. package/npm/native/darwin-arm64/payload.json +20 -0
  43. package/package.json +60 -0
  44. package/plugins/codex/external-subagent/.codex-plugin/plugin.json +17 -0
  45. package/plugins/codex/external-subagent/.mcp.json +9 -0
  46. package/plugins/codex/external-subagent/hooks/audit-bash-result.mjs +55 -0
  47. package/plugins/codex/external-subagent/hooks/check-agent-files.mjs +19 -0
  48. package/plugins/codex/external-subagent/hooks/guard-bash.mjs +15 -0
  49. package/plugins/codex/external-subagent/lib/agent-file-policy.mjs +206 -0
  50. package/plugins/codex/external-subagent/scripts/install-agent-hooks.mjs +149 -0
  51. package/plugins/codex/external-subagent/scripts/mcp-stdio-bridge.mjs +36 -0
  52. package/plugins/codex/external-subagent/scripts/policy-verifier.mjs +44 -0
  53. package/plugins/codex/external-subagent/skills/external-subagent/SKILL.md +55 -0
  54. package/schema/external-subagent-public-api.json +185 -0
  55. package/schema/zas-observation-v1.1.schema.json +58 -0
  56. package/schema/zas-public-reasoning-source.json +14 -0
  57. package/scripts/release/build-native-payload.mjs +119 -0
  58. package/scripts/release/check-native-tarball.mjs +125 -0
  59. package/scripts/release/stage-debug-plugin.mjs +60 -0
  60. package/scripts/release/test-installed-tarball.mjs +73 -0
@@ -0,0 +1,185 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://external-subagent.invalid/schema/public-api.json",
4
+ "title": "external-subagent MCP public API",
5
+ "description": "The intentionally small external_subagent_* wire catalog.",
6
+ "type": "object",
7
+ "required": ["tools"],
8
+ "properties": {
9
+ "tools": {
10
+ "type": "array",
11
+ "const": ["external_subagent_cancel", "external_subagent_close", "external_subagent_list", "external_subagent_observe", "external_subagent_wait", "external_subagent_respond", "external_subagent_result", "external_subagent_send", "external_subagent_spawn", "external_subagent_status"]
12
+ },
13
+ "permission_mode": { "enum": ["build", "edit", "plan", "yolo"], "default": "build" },
14
+ "subagent_status": {
15
+ "type": "object",
16
+ "required": ["subagent", "configured", "enabled", "spawn_supported", "permission_modes", "model_selection", "effort_selection", "local", "auth", "hi"],
17
+ "properties": {
18
+ "subagent": { "enum": ["zcode", "dsh", "codex"] },
19
+ "configured": { "type": "boolean" },
20
+ "enabled": { "type": "boolean" },
21
+ "spawn_supported": { "type": "boolean" },
22
+ "permission_modes": {
23
+ "type": "array",
24
+ "items": { "enum": ["build", "edit", "plan", "yolo"] },
25
+ "uniqueItems": true
26
+ },
27
+ "model_selection": {
28
+ "type": "object",
29
+ "required": ["supported", "mode"],
30
+ "properties": {
31
+ "supported": { "type": "boolean" },
32
+ "mode": { "enum": ["native_only", "catalog_token"] }
33
+ },
34
+ "additionalProperties": false
35
+ },
36
+ "effort_selection": {
37
+ "type": "object",
38
+ "description": "Reasoning-effort steering capability. codex admits only its closed set; zcode and dsh pass a bounded [a-z0-9_] token through to the runtime.",
39
+ "required": ["supported", "mode"],
40
+ "properties": {
41
+ "supported": { "type": "boolean" },
42
+ "mode": { "enum": ["closed_set", "passthrough_token"] }
43
+ },
44
+ "additionalProperties": false
45
+ },
46
+ "local": { "$ref": "#/properties/subagent_scope_status" },
47
+ "auth": { "$ref": "#/properties/subagent_scope_status" },
48
+ "hi": { "$ref": "#/properties/subagent_scope_status" }
49
+ },
50
+ "additionalProperties": false
51
+ },
52
+ "subagent_scope_status": {
53
+ "type": "object",
54
+ "description": "Readiness conclusion only. Probe evidence (scope paths, version, checked_at_ms, reason) stays on the daemon RPC view for the CLI diagnose owner.",
55
+ "required": ["state"],
56
+ "properties": {
57
+ "state": { "enum": ["READY", "DEGRADED", "UNAVAILABLE", "UNKNOWN"] }
58
+ },
59
+ "additionalProperties": false
60
+ },
61
+ "spawn": {
62
+ "type": "object",
63
+ "required": ["repository", "prompt"],
64
+ "properties": {
65
+ "subagent": { "type": "string", "minLength": 1, "description": "Subagent route key; omission uses configured default_subagent or returns subagent_required. Null is invalid; dsh production spawn remains gated." },
66
+ "repository": { "type": "string", "minLength": 1 },
67
+ "permission_mode": { "$ref": "#/properties/permission_mode" },
68
+ "prompt": { "type": "string", "minLength": 1 },
69
+ "model": { "type": "string", "minLength": 1 },
70
+ "effort": { "type": "string", "minLength": 1, "maxLength": 24, "pattern": "^[a-z0-9_]+$", "description": "Reasoning-effort token: 1..24 bytes of [a-z0-9_]. codex admits only low, medium, high or xhigh; zcode and dsh pass the bounded token through. Omission keeps the runtime default." },
71
+ "write_manifest": { "type": "array", "items": { "type": "string", "minLength": 1 } }
72
+ },
73
+ "allOf": [
74
+ { "if": { "required": ["permission_mode"], "properties": { "permission_mode": { "const": "plan" } } },
75
+ "then": { "properties": { "write_manifest": { "maxItems": 0 } } } }
76
+ ],
77
+ "additionalProperties": false
78
+ },
79
+ "list": {
80
+ "type": "object",
81
+ "required": ["repository"],
82
+ "properties": {
83
+ "subagent": { "type": "string", "minLength": 1 },
84
+ "repository": { "type": "string", "minLength": 1 },
85
+ "phase": { "type": ["string", "null"], "default": null },
86
+ "outcome": { "type": ["string", "null"], "default": null },
87
+ "cursor": { "type": ["string", "null"], "default": null },
88
+ "limit": { "type": "integer", "minimum": 1, "maximum": 100, "default": 100 }
89
+ },
90
+ "additionalProperties": false
91
+ },
92
+ "wait": {
93
+ "type": "object",
94
+ "description": "Wait returns early only for an actionable pending request (permission requests need allow/deny; user-input requests carry the full embedded question and need answer with non-empty content) or a terminal result; ordinary progress, message receipts, already-responded and unsupported requests never wake it. The pending_requests output projection is capped at 100 actionable records, while the wake decision scans the full pending set. activity carries the latest text tail, a 200-char verified reasoning tail, tool calls started in the last 60 seconds, and telemetry status.",
95
+ "required": ["agent_id"],
96
+ "properties": {
97
+ "agent_id": { "type": "integer", "minimum": 10000000, "maximum": 99999999 },
98
+ "wait_time": { "type": "integer", "minimum": 0, "maximum": 299, "default": 290 }
99
+ ,"message_id": { "type": ["string", "null"] }
100
+ },
101
+ "additionalProperties": false
102
+ },
103
+ "observe": {
104
+ "type": "object",
105
+ "required": ["agent_id"],
106
+ "properties": {
107
+ "agent_id": { "type": "integer", "minimum": 10000000, "maximum": 99999999 }
108
+ },
109
+ "additionalProperties": false
110
+ },
111
+ "result": {
112
+ "type": "object",
113
+ "description": "Pages bounded terminal task-owned text with the offset and limit continuation contract. Questions from user-input requests are embedded in full in wait projections, not paged here.",
114
+ "required": ["agent_id"],
115
+ "properties": {
116
+ "agent_id": { "type": "integer", "minimum": 10000000, "maximum": 99999999 },
117
+ "offset": { "type": "integer", "minimum": 0, "default": 0 },
118
+ "limit": { "type": "integer", "minimum": 1, "maximum": 262144, "default": 262144 }
119
+ },
120
+ "additionalProperties": false
121
+ },
122
+ "result_projection": {
123
+ "type": "object",
124
+ "required": ["outcome", "final_text", "partial", "offset", "total_bytes", "next_offset", "complete"],
125
+ "properties": {
126
+ "outcome": { "enum": ["COMPLETED", "FAILED", "CANCELLED", "TIMED_OUT", "RUNTIME_LOST", "RESULT_INVALID"] },
127
+ "final_text": { "type": "string" },
128
+ "partial": { "type": "boolean" },
129
+ "offset": { "type": "integer", "minimum": 0 },
130
+ "total_bytes": { "type": "integer", "minimum": 0 },
131
+ "next_offset": { "type": ["integer", "null"], "minimum": 0 },
132
+ "complete": { "type": "boolean" }
133
+ },
134
+ "additionalProperties": false
135
+ },
136
+ "question_projection": {
137
+ "type": "object",
138
+ "description": "Embedded question of an answerable user-input request in the wait projection's pending_requests entries; wait is the only retrieval surface, so a question beyond the embed cap is marked truncated instead of paged.",
139
+ "required": ["text", "truncated"],
140
+ "properties": {
141
+ "text": { "type": "string" },
142
+ "truncated": { "type": "boolean" }
143
+ },
144
+ "additionalProperties": false
145
+ },
146
+ "error_projection": {
147
+ "type": "object",
148
+ "required": ["error"],
149
+ "properties": {
150
+ "error": {
151
+ "type": "object",
152
+ "required": ["code", "message"],
153
+ "properties": {
154
+ "code": { "type": "string", "minLength": 1 },
155
+ "message": { "type": "string", "minLength": 1 },
156
+ "component": { "type": "string", "minLength": 1 },
157
+ "operation": { "type": "string", "minLength": 1 },
158
+ "request_id": { "type": "string", "minLength": 1 },
159
+ "agent_id": { "type": "integer", "minimum": 10000000, "maximum": 99999999 },
160
+ "prompt_count": { "type": "integer", "minimum": 0 }
161
+ },
162
+ "additionalProperties": false
163
+ }
164
+ },
165
+ "additionalProperties": false
166
+ },
167
+ "contracts": {
168
+ "type": "object",
169
+ "additionalProperties": false,
170
+ "properties": {
171
+ "external_subagent_status": { "input": [], "output": ["mcp_version", "components", "capabilities", "subagents"] },
172
+ "external_subagent_spawn": { "input": ["subagent", "repository", "permission_mode", "prompt", "model", "effort", "write_manifest"], "output": ["agent_id", "status"], "idempotent": false },
173
+ "external_subagent_wait": { "input": ["agent_id", "wait_time", "message_id"], "output": ["task", "pending_requests", "result_available", "activity", "result", "instruction", "timed_out", "message_receipt"] },
174
+ "external_subagent_observe": { "input": ["agent_id"], "output": ["tools", "reasoning", "coverage"], "idempotent": true },
175
+ "external_subagent_list": { "input": ["subagent", "repository", "phase", "outcome", "cursor", "limit"], "output": ["tasks", "next_cursor"] },
176
+ "external_subagent_send": { "input": ["agent_id", "message_id", "content"], "output": ["message_id", "disposition"] },
177
+ "external_subagent_respond": { "input": ["agent_id", "request_id", "decision", "content"], "output": ["disposition", "requested_decision", "effective_decision", "policy_overrode", "policy_reason_code"] },
178
+ "external_subagent_cancel": { "input": ["agent_id"], "output": ["task"] },
179
+ "external_subagent_result": { "input": ["agent_id", "offset", "limit"], "output": ["task", "result"] },
180
+ "external_subagent_close": { "input": ["agent_id"], "output": ["task"] }
181
+ }
182
+ }
183
+ },
184
+ "additionalProperties": false
185
+ }
@@ -0,0 +1,58 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "external-subagent suspicion-only observation",
4
+ "type": "object",
5
+ "additionalProperties": false,
6
+ "required": ["tools", "reasoning", "coverage"],
7
+ "properties": {
8
+ "tools": {
9
+ "type": "array",
10
+ "maxItems": 3,
11
+ "items": {
12
+ "type": "object",
13
+ "additionalProperties": false,
14
+ "required": ["tool_name", "call_count", "recent_calls"],
15
+ "properties": {
16
+ "tool_name": { "type": "string", "minLength": 1 },
17
+ "call_count": { "type": "integer", "minimum": 1 },
18
+ "recent_calls": {
19
+ "type": "array",
20
+ "minItems": 1,
21
+ "maxItems": 5,
22
+ "items": {
23
+ "type": "object",
24
+ "additionalProperties": false,
25
+ "required": ["seq", "tool_call_id", "arguments", "arguments_truncated", "redacted_fields"],
26
+ "properties": {
27
+ "seq": { "type": "integer", "minimum": 1 },
28
+ "tool_call_id": { "type": "string", "minLength": 1 },
29
+ "arguments": { "type": "object" },
30
+ "arguments_truncated": { "type": "boolean" },
31
+ "redacted_fields": { "type": "integer", "minimum": 0 }
32
+ }
33
+ }
34
+ }
35
+ }
36
+ }
37
+ },
38
+ "reasoning": {
39
+ "type": "object",
40
+ "additionalProperties": false,
41
+ "required": ["text", "truncated"],
42
+ "properties": {
43
+ "text": { "type": "string", "maxLength": 200 },
44
+ "truncated": { "type": "boolean" }
45
+ }
46
+ },
47
+ "coverage": {
48
+ "type": "object",
49
+ "additionalProperties": false,
50
+ "required": ["tool_history_complete", "reasoning_complete", "dropped_events"],
51
+ "properties": {
52
+ "tool_history_complete": { "type": "boolean" },
53
+ "reasoning_complete": { "type": "boolean" },
54
+ "dropped_events": { "type": "integer", "minimum": 0 }
55
+ }
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "schema": "zas-public-reasoning-source/1",
3
+ "status": "VERIFIED_RUNTIME_PUBLIC",
4
+ "runtime_path": "/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs",
5
+ "runtime_version": "3.11.2",
6
+ "runtime_sha256": "e9f1868c0fdb863537ed910ee3828b9be96b8c2fd805473f63b439e1113266b8",
7
+ "wire_method": "session/event",
8
+ "event_type_pointer": "/params/type",
9
+ "event_type": "model.streaming",
10
+ "event_discriminator_pointer": "/params/payload/kind",
11
+ "event_discriminator_value": "reasoning_delta",
12
+ "delta_pointer": "/params/payload/delta",
13
+ "explicitly_excluded_key": "encrypted_content"
14
+ }
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ // Build the versioned native payload for the npm package: release binaries
3
+ // for external-subagentd and external-subagent-mcp are copied into
4
+ // npm/native/darwin-arm64 together with a deterministic payload.json manifest
5
+ // (version, platform, per-file bytes/sha256/mode). Installed-package code
6
+ // verifies against this manifest, so it must never contain volatile fields.
7
+ //
8
+ // `--variant debug` stages the parallel development payload instead: the same
9
+ // cargo artifacts are copied under debug names into npm/native-debug/darwin-arm64
10
+ // (manifest product external-subagent-debug), and the debug plugin source is
11
+ // staged at plugins/codex/external-subagent-debug. The debug payload is a
12
+ // development checkout product and is rejected from release tarballs by
13
+ // scripts/release/check-native-tarball.mjs.
14
+ //
15
+ // --if-stale skips the cargo build when the staged binaries are newer than
16
+ // every source input and the manifest already matches the package version.
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import crypto from 'node:crypto';
20
+ import { spawnSync } from 'node:child_process';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { stageDebugPlugin } from './stage-debug-plugin.mjs';
23
+
24
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
25
+ const variantIndex = process.argv.indexOf('--variant');
26
+ const variant = variantIndex === -1 ? '' : (process.argv[variantIndex + 1] ?? '');
27
+ if (variant !== '' && variant !== 'debug') {
28
+ process.stderr.write(`unknown variant: ${variant} (supported: debug)\n`);
29
+ process.exit(2);
30
+ }
31
+ const isDebug = variant === 'debug';
32
+ const nativeDirName = isDebug ? 'native-debug' : 'native';
33
+ const productName = isDebug ? 'external-subagent-debug' : 'external-subagent';
34
+ const binaries = isDebug
35
+ ? [['external-subagentd', 'external-subagent-debugd'], ['external-subagent-mcp', 'external-subagent-debug-mcp']]
36
+ : [['external-subagentd', 'external-subagentd'], ['external-subagent-mcp', 'external-subagent-mcp']];
37
+ const platformDir = path.join(packageRoot, 'npm', nativeDirName, 'darwin-arm64');
38
+ const manifestPath = path.join(platformDir, 'payload.json');
39
+ const sourceRoots = ['crates', 'profiles'].map((dir) => path.join(packageRoot, dir))
40
+ .concat([path.join(packageRoot, 'Cargo.toml'), path.join(packageRoot, 'Cargo.lock')]);
41
+
42
+ function packageVersion() {
43
+ return JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')).version;
44
+ }
45
+
46
+ function sha256(bytes) {
47
+ return crypto.createHash('sha256').update(bytes).digest('hex');
48
+ }
49
+
50
+ function newestInputMs() {
51
+ let newest = 0;
52
+ const walk = (entry) => {
53
+ const stat = fs.statSync(entry);
54
+ if (stat.isFile()) { newest = Math.max(newest, stat.mtimeMs); return; }
55
+ for (const child of fs.readdirSync(entry)) {
56
+ if (child === 'target' || child.startsWith('.')) continue;
57
+ walk(path.join(entry, child));
58
+ }
59
+ };
60
+ for (const root of sourceRoots) if (fs.existsSync(root)) walk(root);
61
+ return newest;
62
+ }
63
+
64
+ function stagedIsCurrent(version) {
65
+ if (!fs.existsSync(manifestPath)) return false;
66
+ let manifest;
67
+ try { manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); } catch { return false; }
68
+ if (manifest.version !== version || manifest.platform !== 'darwin-arm64' || manifest.product !== productName) return false;
69
+ const inputs = newestInputMs();
70
+ for (const file of manifest.files) {
71
+ const target = path.join(platformDir, file.name);
72
+ if (!fs.existsSync(target)) return false;
73
+ const stat = fs.statSync(target);
74
+ if (stat.mtimeMs < inputs) return false;
75
+ if (stat.size !== file.bytes || sha256(fs.readFileSync(target)) !== file.sha256) return false;
76
+ }
77
+ return true;
78
+ }
79
+
80
+ const version = packageVersion();
81
+ // Under an npm lifecycle (prepack) stdout belongs to npm's `--json` protocol;
82
+ // diagnostics go to stderr there so pack output stays machine-parseable.
83
+ const note = (text) => (process.env.npm_lifecycle_event ? process.stderr : process.stdout).write(text);
84
+ if (process.argv.includes('--if-stale') && stagedIsCurrent(version)) {
85
+ note(`native payload already current at ${version}${isDebug ? ' (debug)' : ''}\n`);
86
+ process.exit(0);
87
+ }
88
+
89
+ const build = spawnSync('cargo', ['build', '--release', '-p', 'external-daemon', '-p', 'external-mcp'], {
90
+ cwd: packageRoot, stdio: 'inherit',
91
+ });
92
+ if (build.status !== 0) {
93
+ process.stderr.write('cargo release build failed\n');
94
+ process.exit(build.status ?? 1);
95
+ }
96
+
97
+ fs.mkdirSync(platformDir, { recursive: true });
98
+ const files = [];
99
+ for (const [source, name] of binaries) {
100
+ const sourcePath = path.join(packageRoot, 'target', 'release', source);
101
+ if (!fs.existsSync(sourcePath)) {
102
+ process.stderr.write(`release binary missing: ${sourcePath}\n`);
103
+ process.exit(1);
104
+ }
105
+ const target = path.join(platformDir, name);
106
+ fs.copyFileSync(sourcePath, target);
107
+ fs.chmodSync(target, 0o755);
108
+ const bytes = fs.readFileSync(target);
109
+ files.push({ name, bytes: bytes.length, mode: '755', sha256: sha256(bytes) });
110
+ }
111
+
112
+ const manifest = { schema_version: 1, product: productName, version, platform: 'darwin-arm64', files };
113
+ fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o644 });
114
+ if (isDebug) {
115
+ const stagedPlugin = stageDebugPlugin(packageRoot);
116
+ note(`native payload ${version} (debug) staged: ${files.map((file) => file.name).join(', ')}\nplugin source staged: ${stagedPlugin}\n`);
117
+ } else {
118
+ note(`native payload ${version} staged: ${files.map((file) => file.name).join(', ')}\n`);
119
+ }
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+ // Release pack static checks. Two modes:
3
+ // check-native-tarball.mjs [package.tgz] verify a packed tarball (entries,
4
+ // payload manifest consistency,
5
+ // forbidden development material)
6
+ // check-native-tarball.mjs --staged verify only the staged payload
7
+ // (npm/native/darwin-arm64) against
8
+ // the package version — the publish
9
+ // gate, since `npm publish` packs
10
+ // into a private temp directory no
11
+ // lifecycle script can inspect.
12
+ // Without --staged the tarball is resolved from: the explicit argument, the
13
+ // npm --pack-destination directory (set during `npm pack` lifecycles), or the
14
+ // newest matching tgz in the working directory. A dry run (npm pack --dry-run)
15
+ // or a publish (no local tarball) skips the tarball checks and still runs the
16
+ // staged payload checks.
17
+ import fs from 'node:fs';
18
+ import os from 'node:os';
19
+ import path from 'node:path';
20
+ import { spawnSync } from 'node:child_process';
21
+
22
+ const stagedOnly = process.argv.includes('--staged');
23
+ const explicit = process.argv[2] && !process.argv[2].startsWith('--') ? process.argv[2] : null;
24
+ const dryRun = process.env.npm_config_dry_run === 'true';
25
+
26
+ function findTarball() {
27
+ if (explicit) return explicit;
28
+ const candidates = [];
29
+ const packDestination = process.env.npm_config_pack_destination;
30
+ if (packDestination) candidates.push(packDestination);
31
+ candidates.push(process.cwd());
32
+ for (const dir of candidates) {
33
+ const staged = fs.readdirSync(dir).filter((name) => /^external-subagent-\d+\.\d+\.\d+\.tgz$/.test(name)).sort();
34
+ if (staged.length > 0) return path.join(dir, staged[staged.length - 1]);
35
+ }
36
+ return null;
37
+ }
38
+
39
+ let failed = false;
40
+ let tgz = null;
41
+ // Under an npm lifecycle (postpack/prepublishOnly) stdout belongs to npm's
42
+ // `--json` protocol; diagnostics go to stderr there.
43
+ const note = (text) => (process.env.npm_lifecycle_event ? process.stderr : process.stdout).write(text);
44
+ if (!stagedOnly) {
45
+ tgz = findTarball();
46
+ if (tgz === null && !dryRun && explicit === null) {
47
+ // `npm publish` packs into a private temp directory: no tarball to check
48
+ // here. The prepublishOnly staged gate covers the payload; the tarball
49
+ // entry checks run on every `npm pack`.
50
+ note('no local packed tarball to check (publish or explicit run without one); running staged payload checks only\n');
51
+ }
52
+ if (tgz !== null) {
53
+ const required = [
54
+ 'package/package.json',
55
+ 'package/bin/external-subagent.mjs',
56
+ 'package/bin/external-subagent-mcp.mjs',
57
+ 'package/bin/external-subagent-debug.mjs',
58
+ 'package/bin/external-subagent-debug-mcp.mjs',
59
+ '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
+ 'package/plugins/codex/external-subagent/.codex-plugin/plugin.json',
64
+ 'package/plugins/codex/external-subagent/.mcp.json',
65
+ 'package/launchd/com.external-subagent.daemon.plist.template',
66
+ 'package/LICENSE',
67
+ ];
68
+
69
+ const listing = spawnSync('tar', ['-tzf', tgz], { encoding: 'utf8' });
70
+ if (listing.status !== 0) {
71
+ process.stderr.write(`cannot list tarball: ${listing.stderr}\n`);
72
+ process.exit(1);
73
+ }
74
+ const entries = listing.stdout.split('\n').filter(Boolean);
75
+
76
+ for (const entry of required) {
77
+ if (!entries.includes(entry)) { process.stderr.write(`missing release entry: ${entry}\n`); failed = true; }
78
+ }
79
+ // The debug payload and debug plugin source are development-checkout build
80
+ // artifacts; a tarball carrying them would ship ~10 MB of bytes no
81
+ // installed release consumer can verify.
82
+ const forbidden = /(^|\/)(\.agent-work|\.git|target|tests|node_modules|\.npm|workspace)(\/|$)|(^|\/)npm\/native-debug(\/|$)|(^|\/)plugins\/codex\/external-subagent-debug(\/|$)|\.sqlite3$|\.log$|credentials|\.DS_Store$/u;
83
+ for (const entry of entries) {
84
+ if (forbidden.test(entry)) { process.stderr.write(`forbidden release material: ${entry}\n`); failed = true; }
85
+ }
86
+
87
+ const packedPackage = JSON.parse(spawnSync('tar', ['-xzf', tgz, '-O', 'package/package.json'], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }).stdout);
88
+ const manifestText = spawnSync('tar', ['-xzf', tgz, '-O', 'package/npm/native/darwin-arm64/payload.json'], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }).stdout;
89
+ const manifest = JSON.parse(manifestText);
90
+ if (manifest.version !== packedPackage.version) {
91
+ process.stderr.write(`payload version ${manifest.version} does not match package version ${packedPackage.version}\n`);
92
+ failed = true;
93
+ }
94
+ }
95
+ }
96
+
97
+ // Staged payload checks run in every mode (pack, publish, dry run): the
98
+ // package version and the staged native binaries must agree right where the
99
+ // tarball is built from.
100
+ const repoPackage = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8'));
101
+ const stagedManifest = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'npm', 'native', 'darwin-arm64', 'payload.json'), 'utf8'));
102
+ if (stagedManifest.version !== repoPackage.version) {
103
+ process.stderr.write(`staged payload version ${stagedManifest.version} does not match package version ${repoPackage.version}\n`);
104
+ failed = true;
105
+ }
106
+ if (stagedManifest.product !== 'external-subagent') {
107
+ process.stderr.write(`staged payload product ${stagedManifest.product} is not the release payload\n`);
108
+ failed = true;
109
+ }
110
+
111
+ for (const name of ['external-subagentd', 'external-subagent-mcp']) {
112
+ const local = path.join(process.cwd(), 'npm', 'native', 'darwin-arm64', name);
113
+ const file = spawnSync('file', ['-b', local], { encoding: 'utf8' });
114
+ if (!/Mach-O 64-bit executable arm64/u.test(file.stdout)) {
115
+ process.stderr.write(`${name} is not a Mach-O arm64 executable: ${file.stdout.trim()}\n`);
116
+ failed = true;
117
+ }
118
+ const mode = spawnSync('stat', ['-f', '%Lp', local], { encoding: 'utf8' }).stdout.trim();
119
+ if (mode !== '755') { process.stderr.write(`${name} has release mode ${mode}, expected 755\n`); failed = true; }
120
+ }
121
+
122
+ if (failed) process.exit(1);
123
+ note(stagedOnly
124
+ ? `staged release payload checks passed (${repoPackage.version})\n`
125
+ : `release tarball static checks passed: ${tgz ? path.basename(tgz) : 'staged payload only'}\n`);
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ // Stage the debug-variant plugin source at plugins/codex/external-subagent-debug
3
+ // from the release source at plugins/codex/external-subagent: a byte-level copy
4
+ // with the plugin identity renamed (plugin.json name/displayName, the skill
5
+ // directory and its frontmatter name). The .mcp.json placeholder and the
6
+ // hooks/scripts/lib payloads stay identical — the host bindings rewrite the
7
+ // placeholder at staging time, and tool names are the daemon's, not the
8
+ // plugin's. The generated tree is a build artifact (gitignored) owned by
9
+ // build-native-payload.mjs --variant debug.
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+
13
+ const DEBUG_PLUGIN_NAME = 'external-subagent-debug';
14
+ const SOURCE_PLUGIN_NAME = 'external-subagent';
15
+
16
+ export function stageDebugPlugin(packageRoot) {
17
+ const source = path.join(packageRoot, 'plugins', 'codex', SOURCE_PLUGIN_NAME);
18
+ const target = path.join(packageRoot, 'plugins', 'codex', DEBUG_PLUGIN_NAME);
19
+ if (!fs.existsSync(path.join(source, '.codex-plugin', 'plugin.json'))) {
20
+ throw new Error(`release plugin source is missing: ${source}`);
21
+ }
22
+ fs.rmSync(target, { recursive: true, force: true });
23
+ fs.cpSync(source, target, { recursive: true });
24
+ const rmJunk = (dir) => {
25
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
26
+ const child = path.join(dir, entry.name);
27
+ if (entry.isDirectory()) rmJunk(child);
28
+ else if (entry.name === '.DS_Store') fs.rmSync(child);
29
+ }
30
+ };
31
+ rmJunk(target);
32
+
33
+ const manifestPath = path.join(target, '.codex-plugin', 'plugin.json');
34
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
35
+ manifest.name = DEBUG_PLUGIN_NAME;
36
+ manifest.interface = {
37
+ ...manifest.interface,
38
+ displayName: `${manifest.interface?.displayName ?? SOURCE_PLUGIN_NAME} Debug`,
39
+ shortDescription: `Debug instance: ${manifest.interface?.shortDescription ?? ''}`.trim(),
40
+ };
41
+ fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
42
+
43
+ fs.renameSync(path.join(target, 'skills', SOURCE_PLUGIN_NAME), path.join(target, 'skills', DEBUG_PLUGIN_NAME));
44
+ const skillPath = path.join(target, 'skills', DEBUG_PLUGIN_NAME, 'SKILL.md');
45
+ const skill = fs.readFileSync(skillPath, 'utf8');
46
+ const rewritten = skill.replace(`name: ${SOURCE_PLUGIN_NAME}\n`, `name: ${DEBUG_PLUGIN_NAME}\n`, 1);
47
+ fs.writeFileSync(skillPath, rewritten);
48
+ return target;
49
+ }
50
+
51
+ if (import.meta.url === `file://${process.argv[1]}`) {
52
+ const packageRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..');
53
+ try {
54
+ const target = stageDebugPlugin(packageRoot);
55
+ process.stdout.write(`debug plugin source staged: ${target}\n`);
56
+ } catch (error) {
57
+ process.stderr.write(`${error.message}\n`);
58
+ process.exit(1);
59
+ }
60
+ }
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ // Controlled-prefix install check for the packed artifact: install the tgz
3
+ // into a throwaway npm prefix with an isolated HOME and npm cache, then verify
4
+ // the staged bin entries, install-only HOME purity, an explicit init (with
5
+ // launchd neutralized), and the resulting LaunchAgent/binding/registry files.
6
+ // This never touches the real user HOME, the real ~/.codex, or launchd.
7
+ // Usage: test-installed-tarball.mjs [package.tgz]
8
+ import fs from 'node:fs';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+ import { spawnSync } from 'node:child_process';
12
+
13
+ function run(command, args, options = {}) {
14
+ const result = spawnSync(command, args, { encoding: 'utf8', ...options });
15
+ if (result.status !== 0) {
16
+ process.stderr.write(`command failed: ${command} ${args.join(' ')}\n${result.stderr || result.stdout}\n`);
17
+ process.exit(result.status ?? 1);
18
+ }
19
+ return result;
20
+ }
21
+
22
+ const tgzArgument = process.argv[2] || path.join(process.cwd(), (fs.readdirSync(process.cwd()).filter((name) => /^external-subagent-\d+\.\d+\.\d+\.tgz$/.test(name)).sort().slice(-1)[0] || ''));
23
+ const tgz = tgzArgument ? path.resolve(tgzArgument) : '';
24
+ if (!tgz || !fs.existsSync(tgz)) {
25
+ process.stderr.write('no packed tarball found; run npm pack first or pass the tgz path\n');
26
+ process.exit(2);
27
+ }
28
+
29
+ const work = fs.mkdtempSync(path.join(os.tmpdir(), 'external-subagent-prefix-'));
30
+ const prefix = path.join(work, 'prefix');
31
+ const home = path.join(work, 'home');
32
+ fs.mkdirSync(home, { recursive: true });
33
+ const cache = path.join(work, 'npm-cache');
34
+ const env = { ...process.env, HOME: home, CODEX_HOME: path.join(home, '.codex'), EXTERNAL_SUBAGENT_TEST_NO_LAUNCHCTL: '1', npm_config_cache: cache };
35
+
36
+ try {
37
+ run('npm', ['install', '--global', `--prefix=${prefix}`, '--no-audit', '--no-fund', tgz], { cwd: work, env });
38
+ const cli = path.join(prefix, 'bin', 'external-subagent');
39
+ const packageRoot = path.join(prefix, 'lib', 'node_modules', 'external-subagent');
40
+ for (const target of [cli, path.join(prefix, 'bin', 'external-subagent-mcp'), path.join(packageRoot, 'npm', 'native', 'darwin-arm64', 'external-subagentd')]) {
41
+ fs.accessSync(target, fs.constants.X_OK);
42
+ }
43
+ run(cli, ['version'], { env });
44
+ const homeEntries = fs.readdirSync(home).filter((entry) => entry !== '.npm' && entry !== '.cache');
45
+ if (homeEntries.length !== 0) {
46
+ process.stderr.write(`install-only must not write the home; found: ${homeEntries.join(', ')}\n`);
47
+ process.exit(1);
48
+ }
49
+
50
+ // Use the real codex CLI when present (it only ever sees the throwaway
51
+ // CODEX_HOME); otherwise fall back to an explicit skip so the check still
52
+ // verifies the install/service layer without pretending Codex ran.
53
+ const codexAvailable = spawnSync('codex', ['--version'], { encoding: 'utf8' }).status === 0;
54
+ const initArgs = ['init', '--skip-runtime-probe'];
55
+ if (!codexAvailable) initArgs.push('--skip-codex-plugin');
56
+ const init = JSON.parse(run(cli, initArgs, { env }).stdout);
57
+ if (!init.ok || !init.installed) {
58
+ process.stderr.write(`init did not complete: ${JSON.stringify(init)}\n`);
59
+ process.exit(1);
60
+ }
61
+ const expectedArtifacts = [
62
+ path.join(home, 'Library', 'LaunchAgents', 'com.external-subagent.daemon.plist'),
63
+ path.join(home, 'Library', 'Application Support', 'external-subagent', 'config.json'),
64
+ path.join(home, 'plugins', 'external-subagent', '.mcp.json'),
65
+ ];
66
+ if (codexAvailable) expectedArtifacts.push(path.join(home, '.agents', 'plugins', 'marketplace.json'));
67
+ for (const target of expectedArtifacts) {
68
+ if (!fs.existsSync(target)) { process.stderr.write(`init artifact missing: ${target}\n`); process.exit(1); }
69
+ }
70
+ process.stdout.write(`${JSON.stringify({ installed: true, codex_cli: codexAvailable ? 'real' : 'skipped', prefix: `${prefix} (removed)`, payload: init.payload, service: init.service }, null, 2)}\n`);
71
+ } finally {
72
+ fs.rmSync(work, { recursive: true, force: true });
73
+ }