external-subagent 0.5.2 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/bin/external-subagent-mcp.mjs +3 -2
- package/cli/commands/agents.mjs +4 -9
- package/cli/commands/config.mjs +1 -1
- package/cli/commands/daemon.mjs +8 -3
- package/cli/commands/maintenance.mjs +158 -1
- package/cli/commands/plugin.mjs +28 -3
- package/cli/commands/tasks.mjs +7 -1
- package/cli/commands/update.mjs +18 -7
- package/cli/config/write.mjs +195 -14
- package/cli/constants.mjs +2 -2
- package/cli/install/codex.mjs +1 -1
- package/cli/install/dsh.mjs +606 -0
- package/cli/install/init.mjs +23 -33
- package/cli/install/layout.mjs +10 -2
- package/cli/install/payload.mjs +59 -12
- package/cli/install/service-activation.mjs +10 -2
- package/cli/install/service-linux.mjs +1 -1
- package/cli/install/service-macos.mjs +40 -14
- package/cli/install/service-windows.mjs +463 -0
- package/cli/install/update.mjs +6 -5
- package/cli/main.mjs +10 -14
- package/cli/maintenance.mjs +7 -1
- package/cli/paths.mjs +152 -8
- package/npm/native/README.md +2 -2
- package/npm/native/darwin-arm64/external-subagent-mcp +0 -0
- package/npm/native/darwin-arm64/external-subagentd +0 -0
- package/npm/native/darwin-arm64/payload.json +5 -5
- package/npm/native/linux-x64/external-subagent-mcp +0 -0
- package/npm/native/linux-x64/external-subagentd +0 -0
- package/npm/native/linux-x64/payload.json +5 -5
- package/package.json +3 -2
- package/plugins/codex/external-subagent/skills/external-subagent/SKILL.md +5 -3
- package/schema/external-subagent-public-api.json +3 -3
- package/scripts/release/build-native-payload.mjs +22 -5
- package/scripts/release/check-native-tarball.mjs +34 -14
- package/scripts/release/stage-debug-plugin.mjs +9 -3
- package/scripts/release/test-installed-tarball.mjs +1 -1
- package/plugins/codex/external-subagent/hooks/audit-bash-result.mjs +0 -55
- package/plugins/codex/external-subagent/hooks/check-agent-files.mjs +0 -19
- package/plugins/codex/external-subagent/hooks/guard-bash.mjs +0 -15
- package/plugins/codex/external-subagent/lib/agent-file-policy.mjs +0 -206
- package/plugins/codex/external-subagent/scripts/install-agent-hooks.mjs +0 -149
- package/plugins/codex/external-subagent/scripts/policy-verifier.mjs +0 -58
package/cli/paths.mjs
CHANGED
|
@@ -30,7 +30,7 @@ function darwinProductPaths(home) {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
// XDG Base Directory resolution
|
|
33
|
+
// XDG Base Directory resolution. A variable selects its directory only
|
|
34
34
|
// when it is a non-empty absolute path; relative or empty values are ignored and
|
|
35
35
|
// the spec fallback under $HOME applies. Only the three XDG variables the
|
|
36
36
|
// product owns are read — no other user XDG directory is probed.
|
|
@@ -46,7 +46,7 @@ function xdgDirectory(env, variable, home, fallback) {
|
|
|
46
46
|
// state, database, socket, hook provenance, plugin staging, profiles all live
|
|
47
47
|
// inside the product data directory) so the two platforms stay structurally
|
|
48
48
|
// parallel. Data and logs follow XDG; the systemd user unit directory is
|
|
49
|
-
// provided for the
|
|
49
|
+
// provided for the service backend.
|
|
50
50
|
function linuxProductPaths(home, env) {
|
|
51
51
|
const data = path.join(xdgDirectory(env, 'XDG_DATA_HOME', home, ['.local', 'share']), PRODUCT_NAME);
|
|
52
52
|
const logs = path.join(xdgDirectory(env, 'XDG_STATE_HOME', home, ['.local', 'state']), PRODUCT_NAME);
|
|
@@ -67,13 +67,55 @@ function linuxProductPaths(home, env) {
|
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// Windows base directory resolution: %LOCALAPPDATA% is used only when it is a
|
|
71
|
+
// non-empty absolute path; relative or empty values are ignored and the fallback
|
|
72
|
+
// under %USERPROFILE%\AppData\Local applies. APPDATA (Roaming) is deliberately
|
|
73
|
+
// not consulted as the frozen field set has no roaming consumers.
|
|
74
|
+
function localAppDataDirectory(env, home) {
|
|
75
|
+
const configured = env.LOCALAPPDATA;
|
|
76
|
+
if (typeof configured === 'string' && configured !== '' && path.isAbsolute(configured)) {
|
|
77
|
+
return configured;
|
|
78
|
+
}
|
|
79
|
+
return path.join(home, 'AppData', 'Local');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Windows product layout: structurally parallel to darwin/linux.
|
|
83
|
+
// Data, database, config, state, hook provenance, plugin staging, and profiles
|
|
84
|
+
// live under %LOCALAPPDATA%\<product>.
|
|
85
|
+
// Socket uses the Windows named pipe namespace (\\.\pipe\<product>-daemon).
|
|
86
|
+
// launchAgent is null as Windows uses SCM rather than a file-based unit definition.
|
|
87
|
+
function windowsProductPaths(home, env) {
|
|
88
|
+
const localAppData = localAppDataDirectory(env, home);
|
|
89
|
+
const data = path.join(localAppData, PRODUCT_NAME);
|
|
90
|
+
return {
|
|
91
|
+
home,
|
|
92
|
+
data,
|
|
93
|
+
config: path.join(data, 'config.json'),
|
|
94
|
+
state: path.join(data, 'install-state.json'),
|
|
95
|
+
hookProvenance: path.join(data, 'zcode-agent-hook-provenance.json'),
|
|
96
|
+
database: path.join(data, `${PRODUCT_NAME}.sqlite3`),
|
|
97
|
+
// Named pipe namespace placeholder (pending phase 1 real-machine implementation,
|
|
98
|
+
// not a filesystem path; prefix matches the windows.rs \\.\pipe\ literal).
|
|
99
|
+
socket: `\\\\.\\pipe\\${PRODUCT_NAME}-daemon`,
|
|
100
|
+
logs: path.join(localAppData, PRODUCT_NAME, 'logs'),
|
|
101
|
+
// Windows has no file-based service definition; pending phase 3 SCM integration decision.
|
|
102
|
+
launchAgent: null,
|
|
103
|
+
zcodeConfig: path.join(home, '.zcode', 'cli', 'config.json'),
|
|
104
|
+
zcodePlugin: path.join(data, 'zcode-plugin', PLUGIN_NAME),
|
|
105
|
+
profiles: path.join(data, 'profiles'),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
70
109
|
// State paths derive from PRODUCT_NAME, so the debug variant gets a fully
|
|
71
110
|
// parallel tree (its own data directory, database, socket, and logs) and
|
|
72
111
|
// never touches the released product's state. The layout is selected by the
|
|
73
|
-
// process platform: macOS `~/Library`, every other
|
|
74
|
-
// baseline. `zcodeConfig` is `~/.zcode/cli/config.json
|
|
112
|
+
// process platform: macOS `~/Library`, Windows `%LOCALAPPDATA%`, every other
|
|
113
|
+
// supported host the XDG baseline. `zcodeConfig` is `~/.zcode/cli/config.json`.
|
|
75
114
|
export function productPaths(home = os.homedir(), env = process.env) {
|
|
76
|
-
|
|
115
|
+
const currentPlatform = platform(env);
|
|
116
|
+
if (currentPlatform === 'darwin') return darwinProductPaths(home);
|
|
117
|
+
if (currentPlatform === 'win32') return windowsProductPaths(home, env);
|
|
118
|
+
return linuxProductPaths(home, env);
|
|
77
119
|
}
|
|
78
120
|
|
|
79
121
|
// Mirror the daemon's `profiles_directory()` exactly (rpc/profiles.rs):
|
|
@@ -92,7 +134,7 @@ export function productPaths(home = os.homedir(), env = process.env) {
|
|
|
92
134
|
// separators and any `.` component that is not the first, so a trailing `.` is
|
|
93
135
|
// not a component of its own: `a/.` has parent `Some("")`, `a/b/.` has parent
|
|
94
136
|
// `Some("a")`, and `/.` is the root with no parent (`None`). `..` is retained.
|
|
95
|
-
function
|
|
137
|
+
function rustPathParentUnix(value) {
|
|
96
138
|
if (value === '') return null;
|
|
97
139
|
let end = value.length;
|
|
98
140
|
for (;;) {
|
|
@@ -121,6 +163,107 @@ function joinProfiles(parent) {
|
|
|
121
163
|
return `${parent}/profiles`;
|
|
122
164
|
}
|
|
123
165
|
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Windows twin of the two helpers above.
|
|
168
|
+
//
|
|
169
|
+
// Rust compiles `std::path` against the TARGET platform's grammar, so the
|
|
170
|
+
// daemon running on Windows reads config paths as Win32 paths: both `\` and
|
|
171
|
+
// `/` are separators, a Disk/UNC/Verbatim prefix is part of the path, and
|
|
172
|
+
// `Path::parent()` never returns a parent for a root. The functions below are
|
|
173
|
+
// derived case-by-case from real `Path::parent()` / `Path::join("profiles")`
|
|
174
|
+
// output on Windows (see tests/platform/windows-paths.test.mjs); they preserve
|
|
175
|
+
// the original bytes (mixed separators included) and never lexically fold
|
|
176
|
+
// `..`, exactly like the Unix implementation above.
|
|
177
|
+
//
|
|
178
|
+
// The Unix helpers stay untouched: `profilesDir` selects the pair by the same
|
|
179
|
+
// process-platform seam that selects the product layout.
|
|
180
|
+
function isWindowsSeparator(char) {
|
|
181
|
+
return char === '\\' || char === '/';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Prefix grammar (the subset the product can encounter):
|
|
185
|
+
// VerbatimDisk \\?\C:
|
|
186
|
+
// VerbatimUNC \\?\UNC\server\share
|
|
187
|
+
// Verbatim \\?\...
|
|
188
|
+
// UNC \\server\share (requires both components)
|
|
189
|
+
// Disk C:
|
|
190
|
+
// A path with fewer UNC components (`\\server`, `//foo`) is NOT a prefix in
|
|
191
|
+
// Rust: the leading separator is the root instead.
|
|
192
|
+
function windowsPrefixEnd(value) {
|
|
193
|
+
if (value.startsWith('\\\\?\\') || value.startsWith('//?/')) {
|
|
194
|
+
let index = 4;
|
|
195
|
+
if (value.slice(index, index + 3).toUpperCase() === 'UNC'
|
|
196
|
+
&& index + 3 < value.length && isWindowsSeparator(value[index + 3])) {
|
|
197
|
+
index += 4;
|
|
198
|
+
// server
|
|
199
|
+
while (index < value.length && !isWindowsSeparator(value[index])) index += 1;
|
|
200
|
+
while (index < value.length && isWindowsSeparator(value[index])) index += 1;
|
|
201
|
+
// share
|
|
202
|
+
while (index < value.length && !isWindowsSeparator(value[index])) index += 1;
|
|
203
|
+
return index;
|
|
204
|
+
}
|
|
205
|
+
if (index + 1 < value.length && /[A-Za-z]/u.test(value[index]) && value[index + 1] === ':') {
|
|
206
|
+
return index + 2;
|
|
207
|
+
}
|
|
208
|
+
return 4;
|
|
209
|
+
}
|
|
210
|
+
if (value.length >= 2 && isWindowsSeparator(value[0]) && isWindowsSeparator(value[1])) {
|
|
211
|
+
let index = 2;
|
|
212
|
+
const serverStart = index;
|
|
213
|
+
while (index < value.length && !isWindowsSeparator(value[index])) index += 1;
|
|
214
|
+
if (index === serverStart) return 0; // repeated separator: root-relative
|
|
215
|
+
while (index < value.length && isWindowsSeparator(value[index])) index += 1;
|
|
216
|
+
const shareStart = index;
|
|
217
|
+
while (index < value.length && !isWindowsSeparator(value[index])) index += 1;
|
|
218
|
+
if (index === shareStart) return 0; // no share: not a UNC prefix
|
|
219
|
+
return index;
|
|
220
|
+
}
|
|
221
|
+
if (value.length >= 2 && /[A-Za-z]/u.test(value[0]) && value[1] === ':') return 2;
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function windowsComponents(value, start) {
|
|
226
|
+
const components = [];
|
|
227
|
+
let index = start;
|
|
228
|
+
while (index < value.length) {
|
|
229
|
+
while (index < value.length && isWindowsSeparator(value[index])) index += 1;
|
|
230
|
+
if (index >= value.length) break;
|
|
231
|
+
const componentStart = index;
|
|
232
|
+
while (index < value.length && !isWindowsSeparator(value[index])) index += 1;
|
|
233
|
+
// Rust's Path::components drops a non-leading "." (a leading "." with no
|
|
234
|
+
// prefix/root is CurDir and stays a component).
|
|
235
|
+
if (value.slice(componentStart, index) === '.' && componentStart !== 0) continue;
|
|
236
|
+
components.push({ start: componentStart, end: index });
|
|
237
|
+
}
|
|
238
|
+
return components;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function rustPathParentWindows(value) {
|
|
242
|
+
if (value === '') return null;
|
|
243
|
+
const prefixEnd = windowsPrefixEnd(value);
|
|
244
|
+
let rootEnd = prefixEnd;
|
|
245
|
+
if (rootEnd < value.length && isWindowsSeparator(value[rootEnd])) rootEnd += 1;
|
|
246
|
+
const components = windowsComponents(value, rootEnd);
|
|
247
|
+
if (components.length === 0) return null; // root, prefix-only, and empty paths
|
|
248
|
+
if (components.length === 1) return rootEnd === 0 ? '' : value.slice(0, rootEnd);
|
|
249
|
+
return value.slice(0, components[components.length - 2].end);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Platform-selected Rust `Path::parent()` (the target platform's grammar, not
|
|
253
|
+
// the host JS module's). Exported so the path contract is directly testable
|
|
254
|
+
// without going through the product layout.
|
|
255
|
+
export function rustPathParent(value, currentPlatform = process.platform) {
|
|
256
|
+
return currentPlatform === 'win32' ? rustPathParentWindows(value) : rustPathParentUnix(value);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Rust `Path::join` on Windows appends with `\`; a trailing separator (roots)
|
|
260
|
+
// and a drive-relative `C:` prefix append directly.
|
|
261
|
+
function joinProfilesWindows(parent) {
|
|
262
|
+
if (parent === '') return 'profiles';
|
|
263
|
+
if (/[\\/]$/u.test(parent) || /^[A-Za-z]:$/u.test(parent)) return `${parent}profiles`;
|
|
264
|
+
return `${parent}\\profiles`;
|
|
265
|
+
}
|
|
266
|
+
|
|
124
267
|
export function profilesDir(env = {}, home = os.homedir()) {
|
|
125
268
|
const names = ['EXTERNAL_SUBAGENT_CONFIG', 'ZCODE_AGENT_CONFIG'];
|
|
126
269
|
let envPath;
|
|
@@ -133,7 +276,8 @@ export function profilesDir(env = {}, home = os.homedir()) {
|
|
|
133
276
|
}
|
|
134
277
|
}
|
|
135
278
|
if (!exported) return path.join(productPaths(home, env).data, 'profiles');
|
|
136
|
-
const
|
|
279
|
+
const windows = platform(env) === 'win32';
|
|
280
|
+
const parent = rustPathParent(envPath, platform(env));
|
|
137
281
|
if (parent === null) return null;
|
|
138
|
-
return joinProfiles(parent);
|
|
282
|
+
return windows ? joinProfilesWindows(parent) : joinProfiles(parent);
|
|
139
283
|
}
|
package/npm/native/README.md
CHANGED
|
@@ -14,6 +14,6 @@ node scripts/release/build-native-payload.mjs --if-stale
|
|
|
14
14
|
|
|
15
15
|
Supported platforms install without compiling Rust; the installer verifies
|
|
16
16
|
the payload manifest, digest, permissions (755), and Mach-O arm64 image
|
|
17
|
-
before any `init` step runs. There is no payload for other platforms
|
|
18
|
-
|
|
17
|
+
before any `init` step runs. There is no payload for other platforms yet;
|
|
18
|
+
unsupported platforms keep `help`/`version` working and reject business
|
|
19
19
|
commands without writing HOME.
|
|
Binary file
|
|
Binary file
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
3
|
"product": "external-subagent",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.3",
|
|
5
5
|
"platform": "darwin-arm64",
|
|
6
6
|
"files": [
|
|
7
7
|
{
|
|
8
8
|
"name": "external-subagentd",
|
|
9
|
-
"bytes":
|
|
9
|
+
"bytes": 10169696,
|
|
10
10
|
"mode": "755",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "2a253cc82755453ce7e56a625871f7fc2144d853f37dda1498daa18d0800dfc0"
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
14
|
"name": "external-subagent-mcp",
|
|
15
|
-
"bytes":
|
|
15
|
+
"bytes": 935776,
|
|
16
16
|
"mode": "755",
|
|
17
|
-
"sha256": "
|
|
17
|
+
"sha256": "f941106af0c3135087a1122d3e6075223996ab09831768a1a495facb91ca6ad7"
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
20
|
}
|
|
Binary file
|
|
Binary file
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
3
|
"product": "external-subagent",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.3",
|
|
5
5
|
"platform": "linux-x64",
|
|
6
6
|
"files": [
|
|
7
7
|
{
|
|
8
8
|
"name": "external-subagentd",
|
|
9
|
-
"bytes":
|
|
9
|
+
"bytes": 11488752,
|
|
10
10
|
"mode": "755",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "d960dc25d6f4ac77027f81575b91428bb521c8f75b5b39f30e148b15707f1ebb"
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
14
|
"name": "external-subagent-mcp",
|
|
15
|
-
"bytes":
|
|
15
|
+
"bytes": 1025512,
|
|
16
16
|
"mode": "755",
|
|
17
|
-
"sha256": "
|
|
17
|
+
"sha256": "44717d7bada5ed71066f93348fa26f74391475fa147c816c3574a91b3ad65f22"
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
20
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "external-subagent",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
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",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
],
|
|
23
23
|
"os": [
|
|
24
24
|
"darwin",
|
|
25
|
-
"linux"
|
|
25
|
+
"linux",
|
|
26
|
+
"win32"
|
|
26
27
|
],
|
|
27
28
|
"cpu": [
|
|
28
29
|
"arm64",
|
|
@@ -31,7 +31,7 @@ existing wire default; zcode and dsh send no effort field or call at all).
|
|
|
31
31
|
pass it through to the runtime, because the supported set is only known at
|
|
32
32
|
runtime. A malformed or unsupported token is rejected before dispatch, so no
|
|
33
33
|
task is created. Example: spawn with
|
|
34
|
-
`{"subagent": "zcode", "repository": "/absolute/path", "prompt": "...", "effort": "high"}`.
|
|
34
|
+
`{"subagent": "zcode", "model": "provider/model", "repository": "/absolute/path", "prompt": "...", "effort": "high"}`.
|
|
35
35
|
|
|
36
36
|
The caller is a `host`. Any unregistered local MCP client can act as `custom` and
|
|
37
37
|
call status or spawn without a host record or Codex home. Built-in `host.codex`
|
|
@@ -47,8 +47,10 @@ the configuration only on success (a daemon restart is required for `dsh`/`codex
|
|
|
47
47
|
Actual per-subagent limitations (from the accepted code):
|
|
48
48
|
|
|
49
49
|
- `zcode` — disabled until explicitly enabled; all four permission modes
|
|
50
|
-
(build/edit/plan/yolo); an explicit
|
|
51
|
-
|
|
50
|
+
(build/edit/plan/yolo); spawn requires an explicit `{provider}/{model}`
|
|
51
|
+
token from the spawn request or a named profile. A bare token and an
|
|
52
|
+
omitted model are rejected at admission, and `agents.zcode.default_model`
|
|
53
|
+
is rejected with `model_selection_unsupported`.
|
|
52
54
|
- `dsh` — disabled until explicitly enabled and configured
|
|
53
55
|
(`runtime_path`/`home`/`profile`/`version`); admits only `build` and strict
|
|
54
56
|
`plan`.
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"repository": { "type": "string", "minLength": 1 },
|
|
69
69
|
"permission_mode": { "$ref": "#/properties/permission_mode", "description": "codex supports build/edit (workspace-write), plan (read-only), and yolo (danger-full-access), always with approvalPolicy=never; agy supports only build (--mode accept-edits) and yolo (--dangerously-skip-permissions)." },
|
|
70
70
|
"prompt": { "type": "string", "minLength": 1 },
|
|
71
|
-
"model": { "type": "string", "minLength": 1, "description": "Task model. dsh requires provider:model, split at the first colon (the model side may contain further colons; a missing colon or an empty side is rejected before any task). codex uses its own model id; zcode
|
|
71
|
+
"model": { "type": "string", "minLength": 1, "description": "Task model. dsh requires provider:model, split at the first colon (the model side may contain further colons; a missing colon or an empty side is rejected before any task). codex uses its own model id; zcode requires an explicit provider/model token from the spawn request or a named profile, and rejects a bare token, an omitted model, and agents.zcode.default_model at admission. agy uses a bare model slug with no ':' or '/' separator or whitespace; admission validates only that shape and the daemon does not verify catalog membership, so an unknown slug fails loudly at session start when the agy CLI rejects it. Omission falls back to the configured default_model for dsh and agy, then to the provider-native model; codex requires an explicit model or agents.codex.default_model; zcode and codex never fall back to a provider default." },
|
|
72
72
|
"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; agy admits only low, medium, high or max; zcode and dsh pass the bounded token through. Omission keeps the runtime default." },
|
|
73
73
|
"write_manifest": { "type": "array", "items": { "type": "string", "minLength": 1 }, "description": "codex requires an empty or omitted write_manifest; non-empty input returns codex_write_manifest_unsupported before task creation. agy likewise requires an empty or omitted write_manifest; non-empty input returns agy_write_manifest_unsupported before task creation. dsh admits a non-empty manifest in build through the guarded manifest-build composition (workspace-write sandbox, only tool-fs writable, out-of-manifest paths rejected as FS_WRITE_MANIFEST_DENIED), at most 256 entries; an explicit [\".\"] keeps the existing build composition. plan requires an empty manifest for every subagent." }
|
|
74
74
|
},
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
},
|
|
105
105
|
"wait": {
|
|
106
106
|
"type": "object",
|
|
107
|
-
"description": "Wait up to 290 seconds by default. Returns early only when an actionable pending request exists (permission requests need allow/deny; user-input requests carry the full embedded question and need answer with non-empty content) or the terminal result is available; ordinary progress, message receipts, already-responded and unsupported requests never wake it, and its timeout still returns timed_out=true. When the final result is embedded with complete=true no further result call is needed; a partial page directs you to external_subagent_result with next_offset. A failed terminal result also carries failure_message: usually the daemon's bounded single-line JSON failure detail, but an early fail_claim row may be plain text, so treat it as an opaque bounded string and attempt JSON parsing defensively (null when the task has no persisted detail or the outcome is not a failure). Read failure_message before falling back to daemon logs or sqlite. The pending_requests projection stays capped at 100 records while the wake decision scans the full pending set, and the request that woke the wait is always part of the returned projection so its request_id can be answered directly. activity carries the text tail incrementally: each wait returns only the text appended since the previous wait (an empty tail means nothing new), every byte is delivered by wait at most once, and latest_text_truncated flags the case where undelivered bytes rolled out of the bounded window so this response degraded to the whole window (on the terminal response, bytes repeating the embedded result page verbatim are stripped so they are not shipped twice),
|
|
107
|
+
"description": "Wait up to 290 seconds by default. Returns early only when an actionable pending request exists (permission requests need allow/deny; user-input requests carry the full embedded question and need answer with non-empty content) or the terminal result is available; ordinary progress, message receipts, already-responded and unsupported requests never wake it, and its timeout still returns timed_out=true. When the final result is embedded with complete=true no further result call is needed; a partial page directs you to external_subagent_result with next_offset. A failed terminal result also carries failure_message: usually the daemon's bounded single-line JSON failure detail, but an early fail_claim row may be plain text, so treat it as an opaque bounded string and attempt JSON parsing defensively (null when the task has no persisted detail or the outcome is not a failure). Read failure_message before falling back to daemon logs or sqlite. The pending_requests projection stays capped at 100 records while the wake decision scans the full pending set, and the request that woke the wait is always part of the returned projection so its request_id can be answered directly. activity carries the text tail incrementally: each wait returns only the text appended since the previous wait (an empty tail means nothing new), every byte is delivered by wait at most once, and latest_text_truncated flags the case where undelivered bytes rolled out of the bounded window so this response degraded to the whole window (on the terminal response, bytes repeating the embedded result page verbatim are stripped so they are not shipped twice), tool calls started in the last 60 seconds, and telemetry status; activity also exposes last_activity_age_ms (null before any activity) and model_request_active; additionally in_flight_tool_count counts tracked in-flight tool calls (scheduled or started without a matching result, error or permission resolution; 0 when there is no snapshot or the adapter has no per-call lifecycle tracking, such as codex) and oldest_in_flight_tool_age_ms is the age since the oldest tracked in-flight call first appeared (null when none is tracked); use external_subagent_observe only when these liveness signals suggest a meaningless loop or a stuck command, to inspect recent tool-call detail.",
|
|
108
108
|
"required": ["agent_id"],
|
|
109
109
|
"properties": {
|
|
110
110
|
"agent_id": { "type": "integer", "minimum": 10000000, "maximum": 99999999 },
|
|
@@ -184,7 +184,7 @@
|
|
|
184
184
|
"additionalProperties": false,
|
|
185
185
|
"properties": {
|
|
186
186
|
"external_subagent_status": { "input": [], "output": ["mcp_version", "components", "capabilities", "subagents"] },
|
|
187
|
-
"external_subagent_spawn": { "input": ["profile", "subagent", "repository", "permission_mode", "prompt", "model", "effort", "write_manifest"], "output": ["agent_id", "status", "session_id"], "idempotent": false },
|
|
187
|
+
"external_subagent_spawn": { "input": ["profile", "subagent", "repository", "permission_mode", "prompt", "model", "effort", "write_manifest"], "output": ["agent_id", "status", "session_id", "instruction"], "idempotent": false },
|
|
188
188
|
"external_subagent_wait": { "input": ["agent_id", "wait_time", "message_id"], "output": ["task", "pending_requests", "result_available", "activity", "result", "instruction", "timed_out", "message_receipt"] },
|
|
189
189
|
"external_subagent_observe": { "input": ["agent_id"], "output": ["tools", "reasoning", "coverage"], "idempotent": true },
|
|
190
190
|
"external_subagent_list": { "input": ["subagent", "repository", "phase", "outcome", "cursor", "limit"], "output": ["tasks", "next_cursor"] },
|
|
@@ -29,11 +29,19 @@ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '
|
|
|
29
29
|
// correct tuple everywhere (release: darwin-arm64, linux-x64) and an
|
|
30
30
|
// unsupported host fails before touching the tree. The debug variant reuses
|
|
31
31
|
// the same tuple under its parallel native-debug directory.
|
|
32
|
-
|
|
32
|
+
//
|
|
33
|
+
// win32-x64 is staged as a controlled test artifact before NATIVE_PLATFORMS
|
|
34
|
+
// is opened for Windows distribution: the tuple is explicit here, never resolved by
|
|
35
|
+
// nativePlatform(), so `npm pack` can build and inspect the Windows payload
|
|
36
|
+
// on a real Windows host without activating distribution. Windows cargo
|
|
37
|
+
// output carries the `.exe` suffix, and the manifest records the same names.
|
|
38
|
+
const platform = nativePlatform()
|
|
39
|
+
?? ((process.platform === 'win32' && process.arch === 'x64') ? 'win32-x64' : null);
|
|
33
40
|
if (platform === null) {
|
|
34
41
|
process.stderr.write(`no native payload for ${process.platform}-${process.arch}\n`);
|
|
35
42
|
process.exit(2);
|
|
36
43
|
}
|
|
44
|
+
const executableSuffix = platform === 'win32-x64' ? '.exe' : '';
|
|
37
45
|
const variantIndex = process.argv.indexOf('--variant');
|
|
38
46
|
const variant = variantIndex === -1 ? '' : (process.argv[variantIndex + 1] ?? '');
|
|
39
47
|
if (variant !== '' && variant !== 'debug') {
|
|
@@ -51,9 +59,10 @@ if (cargoProfile !== 'release' && cargoProfile !== 'debug') {
|
|
|
51
59
|
}
|
|
52
60
|
const nativeDirName = isDebug ? 'native-debug' : 'native';
|
|
53
61
|
const productName = isDebug ? 'external-subagent-debug' : 'external-subagent';
|
|
54
|
-
const binaries = isDebug
|
|
62
|
+
const binaries = (isDebug
|
|
55
63
|
? [['external-subagentd', 'external-subagent-debugd'], ['external-subagent-mcp', 'external-subagent-debug-mcp']]
|
|
56
|
-
: [['external-subagentd', 'external-subagentd'], ['external-subagent-mcp', 'external-subagent-mcp']]
|
|
64
|
+
: [['external-subagentd', 'external-subagentd'], ['external-subagent-mcp', 'external-subagent-mcp']])
|
|
65
|
+
.map(([source, name]) => [source, `${name}${executableSuffix}`]);
|
|
57
66
|
const platformDir = path.join(packageRoot, 'npm', nativeDirName, platform);
|
|
58
67
|
const manifestPath = path.join(platformDir, 'payload.json');
|
|
59
68
|
const sourceRoots = ['crates', 'profiles'].map((dir) => path.join(packageRoot, dir))
|
|
@@ -121,7 +130,7 @@ if (build.status !== 0) {
|
|
|
121
130
|
fs.mkdirSync(platformDir, { recursive: true });
|
|
122
131
|
const files = [];
|
|
123
132
|
for (const [source, name] of binaries) {
|
|
124
|
-
const sourcePath = path.join(packageRoot, 'target', cargoProfile, source);
|
|
133
|
+
const sourcePath = path.join(packageRoot, 'target', cargoProfile, `${source}${executableSuffix}`);
|
|
125
134
|
if (!fs.existsSync(sourcePath)) {
|
|
126
135
|
process.stderr.write(`${cargoProfile} binary missing: ${sourcePath}\n`);
|
|
127
136
|
process.exit(1);
|
|
@@ -130,7 +139,15 @@ for (const [source, name] of binaries) {
|
|
|
130
139
|
fs.copyFileSync(sourcePath, target);
|
|
131
140
|
fs.chmodSync(target, 0o755);
|
|
132
141
|
const bytes = fs.readFileSync(target);
|
|
133
|
-
|
|
142
|
+
// The POSIX mode is part of the darwin/linux manifest contract (and the
|
|
143
|
+
// install verifier enforces it there). Windows has no POSIX mode; omit the
|
|
144
|
+
// field instead of recording a meaningless "755".
|
|
145
|
+
files.push({
|
|
146
|
+
name,
|
|
147
|
+
bytes: bytes.length,
|
|
148
|
+
...(executableSuffix === '' ? { mode: '755' } : {}),
|
|
149
|
+
sha256: sha256(bytes),
|
|
150
|
+
});
|
|
134
151
|
}
|
|
135
152
|
|
|
136
153
|
const manifest = { schema_version: 1, product: productName, version, platform };
|
|
@@ -26,23 +26,32 @@
|
|
|
26
26
|
import fs from 'node:fs';
|
|
27
27
|
import path from 'node:path';
|
|
28
28
|
import { spawnSync } from 'node:child_process';
|
|
29
|
-
import { elfArch, machoArch } from '../../cli/install/payload.mjs';
|
|
29
|
+
import { elfArch, machoArch, peArch } from '../../cli/install/payload.mjs';
|
|
30
30
|
import { nativePlatform } from '../../cli/install/layout.mjs';
|
|
31
31
|
|
|
32
32
|
const stagedOnly = process.argv.includes('--staged');
|
|
33
33
|
const explicit = process.argv[2] && !process.argv[2].startsWith('--') ? process.argv[2] : null;
|
|
34
34
|
const dryRun = process.env.npm_config_dry_run === 'true';
|
|
35
35
|
|
|
36
|
-
//
|
|
36
|
+
// A Windows host packs a CONTROLLED win32-x64 artifact, not the
|
|
37
|
+
// multi-platform release bundle: only the Windows payload is required and
|
|
38
|
+
// checked. Windows npm publishing is intentionally not activated yet, so
|
|
39
|
+
// macOS/Linux hosts keep the existing dual-platform publish gate; the merged
|
|
40
|
+
// release tarball is assembled there from the platform artifacts.
|
|
41
|
+
const win32Controlled = process.platform === 'win32' && process.arch === 'x64';
|
|
42
|
+
const hostPlatform = nativePlatform() ?? (win32Controlled ? 'win32-x64' : null);
|
|
43
|
+
// Every platform the packed artifact carries, and the image identity each
|
|
37
44
|
// payload must carry. Keeping both lists here (rather than a fixed
|
|
38
|
-
// darwin-arm64 path) is what makes the gate
|
|
39
|
-
const RELEASE_PLATFORMS = Object.freeze(['darwin-arm64', 'linux-x64']);
|
|
40
|
-
const
|
|
45
|
+
// darwin-arm64 path) is what makes the gate multi-platform.
|
|
46
|
+
const RELEASE_PLATFORMS = Object.freeze(win32Controlled ? ['win32-x64'] : ['darwin-arm64', 'linux-x64']);
|
|
47
|
+
const binariesFor = (platform) => (platform === 'win32-x64'
|
|
48
|
+
? Object.freeze(['external-subagentd.exe', 'external-subagent-mcp.exe'])
|
|
49
|
+
: Object.freeze(['external-subagentd', 'external-subagent-mcp']));
|
|
41
50
|
const IMAGE_CHECKS = Object.freeze({
|
|
42
51
|
'darwin-arm64': { describe: 'Mach-O arm64 executable', probe: machoArch, arch: 'arm64' },
|
|
43
52
|
'linux-x64': { describe: 'ELF x86-64 executable', probe: elfArch, arch: 'x64' },
|
|
53
|
+
'win32-x64': { describe: 'PE32+ x86-64 executable', probe: peArch, arch: 'x64' },
|
|
44
54
|
});
|
|
45
|
-
const hostPlatform = nativePlatform();
|
|
46
55
|
|
|
47
56
|
function findTarball() {
|
|
48
57
|
if (explicit) return explicit;
|
|
@@ -87,15 +96,22 @@ if (!stagedOnly) {
|
|
|
87
96
|
// rejected here (and by the --staged publish gate) before it can be
|
|
88
97
|
// published.
|
|
89
98
|
for (const platform of RELEASE_PLATFORMS) {
|
|
90
|
-
for (const name of [...
|
|
99
|
+
for (const name of [...binariesFor(platform), 'payload.json']) required.push(`package/npm/native/${platform}/${name}`);
|
|
91
100
|
}
|
|
92
101
|
|
|
93
|
-
|
|
102
|
+
// GNU tar (git-bash on Windows) reads a drive-letter path such as
|
|
103
|
+
// `C:\...` as a remote host. A cwd-relative path is unambiguous on every
|
|
104
|
+
// platform and names the same file the pack just produced.
|
|
105
|
+
const tarArgument = path.relative(process.cwd(), path.resolve(tgz)) || tgz;
|
|
106
|
+
const listing = spawnSync('tar', ['-tzf', tarArgument], { encoding: 'utf8' });
|
|
94
107
|
if (listing.status !== 0) {
|
|
95
108
|
process.stderr.write(`cannot list tarball: ${listing.stderr}\n`);
|
|
96
109
|
process.exit(1);
|
|
97
110
|
}
|
|
98
|
-
|
|
111
|
+
// Windows' bundled System32 tar (libarchive bsdtar) lists entries with CRLF
|
|
112
|
+
// line endings while GNU tar writes LF; accept both so entry matching does
|
|
113
|
+
// not depend on which tar the host resolves.
|
|
114
|
+
const entries = listing.stdout.split(/\r?\n/).filter(Boolean);
|
|
99
115
|
|
|
100
116
|
for (const entry of required) {
|
|
101
117
|
if (!entries.includes(entry)) { process.stderr.write(`missing release entry: ${entry}\n`); failed = true; }
|
|
@@ -108,9 +124,9 @@ if (!stagedOnly) {
|
|
|
108
124
|
if (forbidden.test(entry)) { process.stderr.write(`forbidden release material: ${entry}\n`); failed = true; }
|
|
109
125
|
}
|
|
110
126
|
|
|
111
|
-
const packedPackage = JSON.parse(spawnSync('tar', ['-xzf',
|
|
127
|
+
const packedPackage = JSON.parse(spawnSync('tar', ['-xzf', tarArgument, '-O', 'package/package.json'], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }).stdout);
|
|
112
128
|
for (const platform of RELEASE_PLATFORMS) {
|
|
113
|
-
const manifestText = spawnSync('tar', ['-xzf',
|
|
129
|
+
const manifestText = spawnSync('tar', ['-xzf', tarArgument, '-O', `package/npm/native/${platform}/payload.json`], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }).stdout;
|
|
114
130
|
let manifest;
|
|
115
131
|
try {
|
|
116
132
|
manifest = JSON.parse(manifestText);
|
|
@@ -162,7 +178,7 @@ function checkStagedPlatform(platform) {
|
|
|
162
178
|
failed = true;
|
|
163
179
|
}
|
|
164
180
|
const image = IMAGE_CHECKS[platform];
|
|
165
|
-
for (const name of
|
|
181
|
+
for (const name of binariesFor(platform)) {
|
|
166
182
|
const local = path.join(dir, name);
|
|
167
183
|
let bytes;
|
|
168
184
|
try {
|
|
@@ -176,8 +192,12 @@ function checkStagedPlatform(platform) {
|
|
|
176
192
|
process.stderr.write(`${platform}/${name} is not a ${image.describe}\n`);
|
|
177
193
|
failed = true;
|
|
178
194
|
}
|
|
179
|
-
|
|
180
|
-
|
|
195
|
+
// Windows has no POSIX execute bit; the PE image check above is the
|
|
196
|
+
// executable identity there, exactly as verifyPayload reasons.
|
|
197
|
+
if (platform !== 'win32-x64') {
|
|
198
|
+
const mode = (fs.statSync(local).mode & 0o777).toString(8);
|
|
199
|
+
if (mode !== '755') { process.stderr.write(`${platform}/${name} has release mode ${mode}, expected 755\n`); failed = true; }
|
|
200
|
+
}
|
|
181
201
|
}
|
|
182
202
|
}
|
|
183
203
|
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
// from the release source at plugins/codex/external-subagent: a byte-level copy
|
|
4
4
|
// with the plugin identity renamed (plugin.json name/displayName, the skill
|
|
5
5
|
// directory and its frontmatter name). The .mcp.json placeholder and the
|
|
6
|
-
//
|
|
6
|
+
// scripts payload stay identical — the host bindings rewrite the
|
|
7
7
|
// placeholder at staging time, and tool names are the daemon's, not the
|
|
8
8
|
// plugin's. The generated tree is a build artifact (gitignored) owned by
|
|
9
9
|
// build-native-payload.mjs --variant debug.
|
|
10
10
|
import fs from 'node:fs';
|
|
11
11
|
import path from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
12
13
|
|
|
13
14
|
const DEBUG_PLUGIN_NAME = 'external-subagent-debug';
|
|
14
15
|
const SOURCE_PLUGIN_NAME = 'external-subagent';
|
|
@@ -43,13 +44,18 @@ export function stageDebugPlugin(packageRoot) {
|
|
|
43
44
|
fs.renameSync(path.join(target, 'skills', SOURCE_PLUGIN_NAME), path.join(target, 'skills', DEBUG_PLUGIN_NAME));
|
|
44
45
|
const skillPath = path.join(target, 'skills', DEBUG_PLUGIN_NAME, 'SKILL.md');
|
|
45
46
|
const skill = fs.readFileSync(skillPath, 'utf8');
|
|
46
|
-
|
|
47
|
+
// Preserve the source file's line endings: a Windows checkout has CRLF and
|
|
48
|
+
// the debug rename must not normalize the whole document.
|
|
49
|
+
const rewritten = skill.replace(
|
|
50
|
+
new RegExp(`name: ${SOURCE_PLUGIN_NAME}(\\r?\\n)`, 'u'),
|
|
51
|
+
(_, newline) => `name: ${DEBUG_PLUGIN_NAME}${newline}`,
|
|
52
|
+
);
|
|
47
53
|
fs.writeFileSync(skillPath, rewritten);
|
|
48
54
|
return target;
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
52
|
-
const packageRoot = path.resolve(path.dirname(
|
|
58
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
53
59
|
try {
|
|
54
60
|
const target = stageDebugPlugin(packageRoot);
|
|
55
61
|
process.stdout.write(`debug plugin source staged: ${target}\n`);
|
|
@@ -39,7 +39,7 @@ try {
|
|
|
39
39
|
run('npm', ['install', '--global', `--prefix=${prefix}`, '--no-audit', '--no-fund', tgz], { cwd: work, env });
|
|
40
40
|
const cli = path.join(prefix, 'bin', 'external-subagent');
|
|
41
41
|
const packageRoot = path.join(prefix, 'lib', 'node_modules', 'external-subagent');
|
|
42
|
-
// The packed payload directory is the running host's tuple
|
|
42
|
+
// The packed payload directory is the running host's tuple, so the
|
|
43
43
|
// smoke checks the platform it is actually installing rather than a fixed
|
|
44
44
|
// darwin-arm64 path.
|
|
45
45
|
const payloadPlatform = nativePlatform() ?? `${process.platform}-${process.arch}`;
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import crypto from 'node:crypto';
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
|
|
6
|
-
let raw = '';
|
|
7
|
-
process.stdin.setEncoding('utf8');
|
|
8
|
-
for await (const chunk of process.stdin) raw += chunk;
|
|
9
|
-
|
|
10
|
-
function boundedText(value, maxBytes = 1_048_576) {
|
|
11
|
-
if (typeof value !== 'string') return '';
|
|
12
|
-
const bytes = Buffer.from(value, 'utf8');
|
|
13
|
-
return bytes.length <= maxBytes ? value : bytes.subarray(0, maxBytes).toString('utf8');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
const input = JSON.parse(raw);
|
|
18
|
-
const tool = input?.tool_name ?? input?.toolName ?? '';
|
|
19
|
-
if (tool !== 'Bash') process.exit(0);
|
|
20
|
-
|
|
21
|
-
const toolInput = input?.tool_input ?? input?.input ?? {};
|
|
22
|
-
const command = String(toolInput?.command ?? '');
|
|
23
|
-
const cwd = String(input?.cwd ?? '');
|
|
24
|
-
const response = input?.tool_response ?? input?.toolResponse ?? {};
|
|
25
|
-
const stdout = boundedText(response?.stdout ?? response?.output ?? '');
|
|
26
|
-
const stderr = boundedText(response?.stderr ?? input?.error ?? '');
|
|
27
|
-
const record = {
|
|
28
|
-
schema: 'zcode-agent-bash-fact/v2',
|
|
29
|
-
at: new Date().toISOString(),
|
|
30
|
-
session_id: input?.session_id ?? input?.sessionId ?? null,
|
|
31
|
-
tool_use_id: input?.tool_use_id ?? input?.toolUseId ?? null,
|
|
32
|
-
hook_event_name: input?.hook_event_name ?? input?.hookEventName ?? null,
|
|
33
|
-
cwd_sha256: crypto.createHash('sha256').update(cwd).digest('hex'),
|
|
34
|
-
command_sha256: crypto.createHash('sha256').update(command).digest('hex'),
|
|
35
|
-
status_code: Number.isInteger(response?.status_code) ? response.status_code : (Number.isInteger(response?.exitCode) ? response.exitCode : null),
|
|
36
|
-
duration_ms: Number.isFinite(input?.duration_ms) ? Math.max(0, Math.floor(input.duration_ms))
|
|
37
|
-
: (Number.isFinite(response?.duration_ms) ? Math.max(0, Math.floor(response.duration_ms)) : null),
|
|
38
|
-
stdout_sha256: crypto.createHash('sha256').update(stdout).digest('hex'),
|
|
39
|
-
stderr_sha256: crypto.createHash('sha256').update(stderr).digest('hex'),
|
|
40
|
-
stdout_bytes_observed: Buffer.byteLength(stdout),
|
|
41
|
-
stderr_bytes_observed: Buffer.byteLength(stderr),
|
|
42
|
-
failed: Boolean(input?.error) || input?.hook_event_name === 'PostToolUseFailure' || input?.hookEventName === 'PostToolUseFailure',
|
|
43
|
-
permission_mode: ['build', 'edit', 'plan', 'yolo'].includes(process.env.ZCODE_PERMISSION_MODE || process.env.ZCODE_AGENT_PERMISSION_MODE)
|
|
44
|
-
? (process.env.ZCODE_PERMISSION_MODE || process.env.ZCODE_AGENT_PERMISSION_MODE)
|
|
45
|
-
: null,
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const dataRoot = process.env.ZCODE_PLUGIN_DATA;
|
|
49
|
-
if (!dataRoot) process.exit(0);
|
|
50
|
-
fs.mkdirSync(dataRoot, { recursive: true, mode: 0o700 });
|
|
51
|
-
const logPath = path.join(dataRoot, 'readonly-bash-audit.jsonl');
|
|
52
|
-
fs.appendFileSync(logPath, `${JSON.stringify(record)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'a' });
|
|
53
|
-
} catch (error) {
|
|
54
|
-
process.stderr.write(`[zcode-agent-bash-audit] ${error instanceof Error ? error.message : String(error)}\n`);
|
|
55
|
-
}
|