external-subagent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/bin/external-subagent-debug-mcp.mjs +5 -0
- package/bin/external-subagent-debug.mjs +13 -0
- package/bin/external-subagent-mcp.mjs +20 -0
- package/bin/external-subagent.mjs +8 -0
- package/bin/zas.mjs +8 -0
- package/cli/commands/agents.mjs +117 -0
- package/cli/commands/config.mjs +117 -0
- package/cli/commands/daemon.mjs +36 -0
- package/cli/commands/maintenance.mjs +27 -0
- package/cli/commands/plugin.mjs +96 -0
- package/cli/commands/tasks.mjs +64 -0
- package/cli/commands/update.mjs +219 -0
- package/cli/config/read.mjs +24 -0
- package/cli/config/schema.mjs +75 -0
- package/cli/config/write.mjs +39 -0
- package/cli/constants.mjs +24 -0
- package/cli/errors.mjs +7 -0
- package/cli/fs-atomic.mjs +36 -0
- package/cli/install/codex.mjs +352 -0
- package/cli/install/init.mjs +206 -0
- package/cli/install/layout.mjs +54 -0
- package/cli/install/npm-hook.mjs +49 -0
- package/cli/install/path.mjs +43 -0
- package/cli/install/payload.mjs +84 -0
- package/cli/install/plugin-stage.mjs +232 -0
- package/cli/install/reconcile.mjs +209 -0
- package/cli/install/recovery.mjs +79 -0
- package/cli/install/service-activation.mjs +133 -0
- package/cli/install/service-macos.mjs +146 -0
- package/cli/install/update.mjs +170 -0
- package/cli/install/zcode.mjs +239 -0
- package/cli/main.mjs +429 -0
- package/cli/maintenance.mjs +104 -0
- package/cli/paths.mjs +40 -0
- package/cli/rpc.mjs +261 -0
- package/launchd/com.external-subagent.daemon.plist.template +37 -0
- package/npm/native/README.md +19 -0
- package/npm/native/darwin-arm64/external-subagent-mcp +0 -0
- package/npm/native/darwin-arm64/external-subagentd +0 -0
- package/npm/native/darwin-arm64/payload.json +20 -0
- package/package.json +60 -0
- package/plugins/codex/external-subagent/.codex-plugin/plugin.json +17 -0
- package/plugins/codex/external-subagent/.mcp.json +9 -0
- package/plugins/codex/external-subagent/hooks/audit-bash-result.mjs +55 -0
- package/plugins/codex/external-subagent/hooks/check-agent-files.mjs +19 -0
- package/plugins/codex/external-subagent/hooks/guard-bash.mjs +15 -0
- package/plugins/codex/external-subagent/lib/agent-file-policy.mjs +206 -0
- package/plugins/codex/external-subagent/scripts/install-agent-hooks.mjs +149 -0
- package/plugins/codex/external-subagent/scripts/mcp-stdio-bridge.mjs +36 -0
- package/plugins/codex/external-subagent/scripts/policy-verifier.mjs +44 -0
- package/plugins/codex/external-subagent/skills/external-subagent/SKILL.md +55 -0
- package/schema/external-subagent-public-api.json +185 -0
- package/schema/zas-observation-v1.1.schema.json +58 -0
- package/schema/zas-public-reasoning-source.json +14 -0
- package/scripts/release/build-native-payload.mjs +119 -0
- package/scripts/release/check-native-tarball.mjs +125 -0
- package/scripts/release/stage-debug-plugin.mjs +60 -0
- package/scripts/release/test-installed-tarball.mjs +73 -0
|
@@ -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
|
+
}
|