external-subagent 0.5.2 → 0.5.4

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 (44) hide show
  1. package/README.md +8 -5
  2. package/bin/external-subagent-mcp.mjs +3 -2
  3. package/cli/commands/agents.mjs +4 -9
  4. package/cli/commands/config.mjs +1 -1
  5. package/cli/commands/daemon.mjs +8 -3
  6. package/cli/commands/maintenance.mjs +158 -1
  7. package/cli/commands/plugin.mjs +28 -3
  8. package/cli/commands/tasks.mjs +7 -1
  9. package/cli/commands/update.mjs +18 -7
  10. package/cli/config/write.mjs +195 -14
  11. package/cli/constants.mjs +2 -2
  12. package/cli/install/codex.mjs +1 -1
  13. package/cli/install/dsh.mjs +613 -0
  14. package/cli/install/init.mjs +23 -33
  15. package/cli/install/layout.mjs +10 -2
  16. package/cli/install/payload.mjs +59 -12
  17. package/cli/install/service-activation.mjs +10 -2
  18. package/cli/install/service-linux.mjs +1 -1
  19. package/cli/install/service-macos.mjs +40 -14
  20. package/cli/install/service-windows.mjs +463 -0
  21. package/cli/install/update.mjs +6 -5
  22. package/cli/main.mjs +10 -14
  23. package/cli/maintenance.mjs +7 -1
  24. package/cli/paths.mjs +152 -8
  25. package/npm/native/README.md +2 -2
  26. package/npm/native/darwin-arm64/external-subagent-mcp +0 -0
  27. package/npm/native/darwin-arm64/external-subagentd +0 -0
  28. package/npm/native/darwin-arm64/payload.json +5 -5
  29. package/npm/native/linux-x64/external-subagent-mcp +0 -0
  30. package/npm/native/linux-x64/external-subagentd +0 -0
  31. package/npm/native/linux-x64/payload.json +5 -5
  32. package/package.json +3 -2
  33. package/plugins/codex/external-subagent/skills/external-subagent/SKILL.md +5 -3
  34. package/schema/external-subagent-public-api.json +3 -3
  35. package/scripts/release/build-native-payload.mjs +22 -5
  36. package/scripts/release/check-native-tarball.mjs +34 -14
  37. package/scripts/release/stage-debug-plugin.mjs +9 -3
  38. package/scripts/release/test-installed-tarball.mjs +1 -1
  39. package/plugins/codex/external-subagent/hooks/audit-bash-result.mjs +0 -55
  40. package/plugins/codex/external-subagent/hooks/check-agent-files.mjs +0 -19
  41. package/plugins/codex/external-subagent/hooks/guard-bash.mjs +0 -15
  42. package/plugins/codex/external-subagent/lib/agent-file-policy.mjs +0 -206
  43. package/plugins/codex/external-subagent/scripts/install-agent-hooks.mjs +0 -149
  44. package/plugins/codex/external-subagent/scripts/policy-verifier.mjs +0 -58
@@ -1,19 +0,0 @@
1
- #!/usr/bin/env node
2
- import { createAgentFileHookOutput, evaluateAgentFileInput } from '../lib/agent-file-policy.mjs';
3
-
4
- let raw = '';
5
- process.stdin.setEncoding('utf8');
6
- for await (const chunk of process.stdin) raw += chunk;
7
- try {
8
- const input = JSON.parse(raw);
9
- // Hooks are optional and must not claim ordinary, unmanaged ZCode sessions.
10
- if (process.env.ZCODE_AGENT_POLICY !== '1') process.exit(0);
11
- process.stdout.write(`${JSON.stringify(createAgentFileHookOutput(evaluateAgentFileInput(input)))}\n`);
12
- } catch {
13
- if (process.env.ZCODE_AGENT_POLICY !== '1') process.exit(0);
14
- // Never echo input, paths, or parser details: hook failures are denied and redacted.
15
- process.stdout.write(`${JSON.stringify(createAgentFileHookOutput({
16
- decision: 'deny',
17
- reason: 'zcode-agent-file-policy/v1.0.0: hook_internal_error',
18
- }))}\n`);
19
- }
@@ -1,15 +0,0 @@
1
- #!/usr/bin/env node
2
- let raw = '';
3
- process.stdin.setEncoding('utf8');
4
- for await (const chunk of process.stdin) raw += chunk;
5
- try {
6
- const input = JSON.parse(raw);
7
- const managed = process.env.ZCODE_AGENT_POLICY === '1';
8
- const mode = process.env.ZCODE_AGENT_PERMISSION_MODE || process.env.ZCODE_PERMISSION_MODE;
9
- if (!managed || mode !== 'plan' || (input?.tool_name ?? input?.toolName) !== 'Bash') process.exit(0);
10
- process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: 'external-subagent policy: Bash is disabled in plan probes' } })}\n`);
11
- } catch {
12
- if (process.env.ZCODE_AGENT_POLICY === '1') {
13
- process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: 'external-subagent policy: malformed hook input' } })}\n`);
14
- }
15
- }
@@ -1,206 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
-
4
- export const AGENT_FILE_POLICY_VERSION = 'zcode-agent-file-policy/v1.0.0';
5
- const MAX_MANIFEST_BYTES = 64 * 1024;
6
- const MAX_MANIFEST_ENTRIES = 256;
7
- const MAX_BOOTSTRAP_ROOTS = 8;
8
- const MAX_PATH_BYTES = 4096;
9
- const WRITE_TOOLS = new Set(['Write', 'Edit', 'Delete', 'Move']);
10
- const READ_TOOLS = new Set(['Read', 'Grep', 'Glob']);
11
- const PATH_KEYS = new Set([
12
- 'path', 'file_path', 'filePath', 'directory', 'directory_path', 'directoryPath',
13
- 'source', 'source_path', 'sourcePath', 'destination', 'destination_path', 'destinationPath',
14
- 'old_path', 'oldPath', 'new_path', 'newPath', 'from', 'to',
15
- ]);
16
- const PROTECTED_PART = /^(?:\.git|\.gitmodules|\.zcode|\.codex|\.agent-work)$/u;
17
-
18
- function deny(code) {
19
- return { decision: 'deny', code, reason: `${AGENT_FILE_POLICY_VERSION}: ${code}` };
20
- }
21
-
22
- function allow() {
23
- return { decision: 'allow', code: 'ok', reason: `${AGENT_FILE_POLICY_VERSION}: allowed` };
24
- }
25
-
26
- function ask() {
27
- return { decision: 'ask', code: 'edit_requires_approval', reason: `${AGENT_FILE_POLICY_VERSION}: edit requires external approval` };
28
- }
29
-
30
- function envRequired(env) {
31
- if (env?.ZCODE_AGENT_POLICY !== '1') return { decision: 'skip', code: 'policy_not_managed', reason: `${AGENT_FILE_POLICY_VERSION}: unmanaged session` };
32
- const rawRoot = env?.ZCODE_AGENT_WORKSPACE_ROOT;
33
- if (typeof rawRoot !== 'string' || rawRoot.length === 0 || !path.isAbsolute(rawRoot)) {
34
- return deny('workspace_root_missing');
35
- }
36
- let root;
37
- try {
38
- root = fs.realpathSync.native(rawRoot);
39
- if (!fs.statSync(root).isDirectory()) return deny('workspace_root_invalid');
40
- } catch {
41
- return deny('workspace_root_invalid');
42
- }
43
- let manifest = [];
44
- const rawManifest = env?.ZCODE_AGENT_WRITE_MANIFEST;
45
- if (typeof rawManifest !== 'string' || Buffer.byteLength(rawManifest, 'utf8') > MAX_MANIFEST_BYTES) {
46
- return deny('write_manifest_invalid');
47
- }
48
- try {
49
- const parsed = rawManifest.length === 0 ? [] : JSON.parse(rawManifest);
50
- if (!Array.isArray(parsed) || parsed.length > MAX_MANIFEST_ENTRIES) return deny('write_manifest_invalid');
51
- manifest = parsed.map((item) => {
52
- if (typeof item !== 'string' || item.length === 0 || Buffer.byteLength(item, 'utf8') > MAX_PATH_BYTES) {
53
- throw new Error('manifest path');
54
- }
55
- const candidate = path.posix.normalize(item.replaceAll('\\', '/'));
56
- if (path.posix.isAbsolute(candidate) || candidate === '..' || candidate.startsWith('../') || candidate.includes('\0')) {
57
- throw new Error('manifest traversal');
58
- }
59
- if (candidate.split('/').some((part) => PROTECTED_PART.test(part))) {
60
- throw new Error('manifest protected');
61
- }
62
- return candidate === '.' ? '' : candidate;
63
- });
64
- } catch {
65
- return deny('write_manifest_invalid');
66
- }
67
- let bootstrapRoots = [];
68
- const rawBootstrapRoots = env?.ZCODE_AGENT_BOOTSTRAP_ROOTS;
69
- if (rawBootstrapRoots !== undefined) {
70
- if (typeof rawBootstrapRoots !== 'string' || Buffer.byteLength(rawBootstrapRoots, 'utf8') > MAX_MANIFEST_BYTES) {
71
- return deny('bootstrap_roots_invalid');
72
- }
73
- try {
74
- const parsed = rawBootstrapRoots.trimStart().startsWith('[')
75
- ? JSON.parse(rawBootstrapRoots)
76
- : [rawBootstrapRoots];
77
- if (!Array.isArray(parsed) || parsed.length > MAX_BOOTSTRAP_ROOTS) throw new Error('bootstrap roots');
78
- bootstrapRoots = parsed.map((item) => {
79
- if (typeof item !== 'string' || !path.isAbsolute(item) || item.length > MAX_PATH_BYTES || item.includes('\0') || hasParentTraversal(item)) {
80
- throw new Error('bootstrap root path');
81
- }
82
- if (item.split(/[\\/]/u).some((part) => PROTECTED_PART.test(part))) {
83
- throw new Error('bootstrap protected');
84
- }
85
- try {
86
- if (!fs.statSync(item).isDirectory()) throw new Error('bootstrap not directory');
87
- return fs.realpathSync.native(item);
88
- } catch {
89
- // A missing optional bootstrap root is inert; reads under it remain denied.
90
- return item;
91
- }
92
- });
93
- } catch {
94
- return deny('bootstrap_roots_invalid');
95
- }
96
- }
97
- return { root, manifest, bootstrapRoots };
98
- }
99
-
100
- function collectPaths(value, key = '') {
101
- if (typeof value === 'string') return PATH_KEYS.has(key) ? [value] : [];
102
- if (!value || typeof value !== 'object') return [];
103
- if (Array.isArray(value)) return value.flatMap((item) => collectPaths(item, key));
104
- return Object.entries(value).flatMap(([childKey, child]) => collectPaths(child, childKey));
105
- }
106
-
107
- function hasParentTraversal(value) {
108
- return value.split(/[\\/]/u).some((part) => part === '..');
109
- }
110
-
111
- function confinedPath(root, value) {
112
- if (typeof value !== 'string' || value.length === 0 || Buffer.byteLength(value, 'utf8') > MAX_PATH_BYTES) {
113
- return false;
114
- }
115
- if (value.includes('\0') || hasParentTraversal(value)) return false;
116
- const canonicalCandidate = resolveCanonicalPath(root, value);
117
- if (!canonicalCandidate) return false;
118
- return canonicalCandidate === root || canonicalCandidate.startsWith(`${root}${path.sep}`);
119
- }
120
-
121
- function resolveCanonicalPath(root, value) {
122
- if (typeof value !== 'string' || value.length === 0 || Buffer.byteLength(value, 'utf8') > MAX_PATH_BYTES) {
123
- return null;
124
- }
125
- if (value.includes('\0') || hasParentTraversal(value)) return null;
126
- const candidate = path.isAbsolute(value) ? value : path.join(root, value);
127
- const lexical = path.normalize(candidate);
128
- let existing = lexical;
129
- const missing = [];
130
- while (!fs.existsSync(existing)) {
131
- const parent = path.dirname(existing);
132
- if (parent === existing) return null;
133
- missing.unshift(path.basename(existing));
134
- existing = parent;
135
- }
136
- try {
137
- const canonical = fs.realpathSync.native(existing);
138
- return missing.length > 0 ? path.join(canonical, ...missing) : canonical;
139
- } catch {
140
- return null;
141
- }
142
- }
143
-
144
- function protectedPath(root, value) {
145
- const relative = path.isAbsolute(value) ? path.relative(root, value) : value;
146
- const lexicalProtected = relative.split(/[\\/]/u).some((part) => PROTECTED_PART.test(part));
147
- if (lexicalProtected) return true;
148
- const canonical = resolveCanonicalPath(root, value);
149
- if (!canonical) return false;
150
- return path.relative(root, canonical)
151
- .split(/[\\/]/u)
152
- .some((part) => PROTECTED_PART.test(part));
153
- }
154
-
155
- function pathAllowedForRead(state, value) {
156
- // Relative paths are daemon-worktree paths. Never reinterpret a relative
157
- // payload against a bootstrap root (that would turn a worktree symlink into
158
- // an apparently safe bootstrap path). Bootstrap roots are absolute-only.
159
- if (!path.isAbsolute(value)) {
160
- return confinedPath(state.root, value) && !protectedPath(state.root, value);
161
- }
162
- return [state.root, ...state.bootstrapRoots].some(
163
- (root) => confinedPath(root, value) && !protectedPath(root, value),
164
- );
165
- }
166
-
167
- function withinManifest(root, value, manifest) {
168
- const candidate = resolveCanonicalPath(root, value);
169
- if (!candidate) return false;
170
- return manifest.some((entry) => {
171
- const target = resolveCanonicalPath(root, entry);
172
- if (!target) return false;
173
- return candidate === target || candidate.startsWith(`${target}${path.sep}`);
174
- });
175
- }
176
-
177
- export function evaluateAgentFileInput(input, env = process.env) {
178
- const state = envRequired(env);
179
- if (state.decision) return state;
180
- const tool = input?.tool_name ?? input?.toolName ?? input?.name;
181
- if (typeof tool !== 'string' || (!READ_TOOLS.has(tool) && !WRITE_TOOLS.has(tool))) {
182
- return deny('unsupported_tool');
183
- }
184
- const toolInput = input?.tool_input ?? input?.toolInput ?? input?.input ?? {};
185
- const paths = collectPaths(toolInput);
186
- // An omitted path is valid for a search rooted at cwd, but a file mutation must name a target.
187
- if (WRITE_TOOLS.has(tool) && paths.length === 0) return deny('path_missing');
188
- const cwd = input?.cwd ?? input?.working_directory ?? input?.workingDirectory ?? state.root;
189
- if (!confinedPath(state.root, cwd) || protectedPath(state.root, cwd)) return deny('cwd_outside_root');
190
- for (const value of paths.length ? paths : [cwd]) {
191
- if (!pathAllowedForRead(state, value)) return deny('path_outside_root');
192
- if (WRITE_TOOLS.has(tool) && !withinManifest(state.root, value, state.manifest)) return deny('write_not_allowlisted');
193
- }
194
- if (WRITE_TOOLS.has(tool) && env.ZCODE_AGENT_PERMISSION_MODE === 'edit') return ask();
195
- return allow();
196
- }
197
-
198
- export function createAgentFileHookOutput(result) {
199
- return {
200
- hookSpecificOutput: {
201
- hookEventName: 'PreToolUse',
202
- permissionDecision: result.decision,
203
- permissionDecisionReason: result.reason,
204
- },
205
- };
206
- }
@@ -1,149 +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
- const pluginRoot = path.resolve(new URL('..', import.meta.url).pathname);
7
- const hookRoot = pluginRoot;
8
- const configPath = process.argv[process.argv.indexOf('--config') + 1];
9
- if (!configPath || configPath.startsWith('--')) {
10
- console.error('usage: node install-agent-hooks.mjs --config /absolute/config.json [--provenance /absolute/provenance.json]');
11
- process.exit(2);
12
- }
13
- const provenanceIndex = process.argv.indexOf('--provenance');
14
- const provenancePath = provenanceIndex >= 0
15
- ? process.argv[provenanceIndex + 1]
16
- : path.join(path.dirname(configPath), 'zcode-agent-hook-provenance.json');
17
-
18
- function readJson(file, fallback) {
19
- try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (error) {
20
- if (error?.code === 'ENOENT') return fallback;
21
- throw error;
22
- }
23
- }
24
-
25
- function atomicWriteBytes(file, bytes) {
26
- fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
27
- const temporary = `${file}.tmp-${process.pid}`;
28
- try {
29
- fs.writeFileSync(temporary, bytes, { mode: 0o600 });
30
- fs.renameSync(temporary, file);
31
- } finally {
32
- try { fs.unlinkSync(temporary); } catch (error) {
33
- if (error?.code !== 'ENOENT') throw error;
34
- }
35
- }
36
- }
37
-
38
- const encodeJson = (value) => `${JSON.stringify(value, null, 2)}\n`;
39
- const atomicWrite = (file, value) => atomicWriteBytes(file, encodeJson(value));
40
-
41
- const config = readJson(configPath, {});
42
- if (!config || typeof config !== 'object' || Array.isArray(config)) throw new Error('config must be a JSON object');
43
- if (fs.existsSync(provenancePath)) {
44
- const prior = readJson(provenancePath, null);
45
- if (!prior || prior.effective_config_path !== path.resolve(configPath)) {
46
- throw new Error(`HOOK_PROVENANCE_INVALID: effective_config_path does not match ${path.resolve(configPath)}`);
47
- }
48
- }
49
- const next = structuredClone(config);
50
- next.hooks ??= {};
51
- next.hooks.enabled = true;
52
- next.hooks.events ??= {};
53
- const hashFile = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
54
- // Resolve every shipped decision owner before mutating the caller's config.
55
- // A copied or incomplete plugin must fail without leaving a partial install.
56
- const effectiveConfigPath = path.resolve(configPath);
57
- const effectiveFilePolicyPath = path.join(hookRoot, 'lib', 'agent-file-policy.mjs');
58
- const auditWrapperPath = path.join(hookRoot, 'hooks', 'audit-bash-result.mjs');
59
- const fileWrapperPath = path.join(hookRoot, 'hooks', 'check-agent-files.mjs');
60
- const guardWrapperPath = path.join(hookRoot, 'hooks', 'guard-bash.mjs');
61
- const verifierSourcePath = path.join(pluginRoot, 'scripts', 'policy-verifier.mjs');
62
- const filePolicySha256 = hashFile(effectiveFilePolicyPath);
63
- if (effectiveConfigPath === path.resolve(provenancePath)) throw new Error('config and provenance paths must differ');
64
- const events = {
65
- PreToolUse: [
66
- { matcher: 'Bash', script: 'hooks/guard-bash.mjs' },
67
- { matcher: '^(Read|Grep|Glob|Write|Edit|Delete|Move)$', script: 'hooks/check-agent-files.mjs' },
68
- ],
69
- PostToolUse: [{ matcher: 'Bash', script: 'hooks/audit-bash-result.mjs' }],
70
- PostToolUseFailure: [{ matcher: 'Bash', script: 'hooks/audit-bash-result.mjs' }],
71
- };
72
-
73
- function processHookArgs(candidate) {
74
- if (!candidate || !Array.isArray(candidate.hooks)) return [];
75
- return candidate.hooks
76
- .filter((hook) => hook?.type === 'process' && Array.isArray(hook.args))
77
- .flatMap((hook) => hook.args)
78
- .filter((arg) => typeof arg === 'string');
79
- }
80
-
81
- function isRecognizedAgentHook(candidate, matcher, expectedScript) {
82
- if (candidate?.matcher !== matcher) return false;
83
- const args = processHookArgs(candidate);
84
- return args.some((arg) => path.resolve(arg) === expectedScript);
85
- }
86
-
87
- for (const [event, expectedEntries] of Object.entries(events)) {
88
- const existing = Array.isArray(next.hooks.events[event]) ? next.hooks.events[event] : [];
89
- for (const { matcher, script } of expectedEntries) {
90
- const expectedScript = path.join(hookRoot, script);
91
- const managedEntries = existing.filter((candidate) => candidate?.matcher === matcher);
92
- const unknownEntries = managedEntries.filter((candidate) => !isRecognizedAgentHook(candidate, matcher, expectedScript));
93
- if (unknownEntries.length > 0) {
94
- throw new Error(`${event} contains an unknown managed hook; refusing to modify configuration`);
95
- }
96
- }
97
- const unrelated = existing.filter((candidate) => !expectedEntries.some(({ matcher }) => candidate?.matcher === matcher));
98
- next.hooks.events[event] = [
99
- ...unrelated,
100
- ...expectedEntries.map(({ matcher, script }) => ({
101
- matcher,
102
- hooks: [{ type: 'process', command: process.execPath, args: [path.join(hookRoot, script)], timeoutMs: 5000 }],
103
- })),
104
- ];
105
- }
106
- const nextConfigBytes = encodeJson(next);
107
- const nextProvenance = {
108
- effective_file_policy_version: 'zcode-agent-file-policy/v1.0.0',
109
- effective_file_policy_sha256: filePolicySha256,
110
- effective_file_policy_path: effectiveFilePolicyPath,
111
- effective_guard_wrapper_path: guardWrapperPath,
112
- effective_guard_wrapper_sha256: hashFile(guardWrapperPath),
113
- effective_config_path: effectiveConfigPath,
114
- effective_config_sha256: crypto.createHash('sha256').update(nextConfigBytes).digest('hex'),
115
- effective_audit_wrapper_path: auditWrapperPath,
116
- effective_audit_wrapper_sha256: hashFile(auditWrapperPath),
117
- effective_file_wrapper_path: fileWrapperPath,
118
- effective_file_wrapper_sha256: hashFile(fileWrapperPath),
119
- hook_activation_verified: true,
120
- activation_method: 'outer-plugin-install',
121
- activation_generation: `${Date.now()}-${filePolicySha256.slice(0, 12)}`,
122
- };
123
- const previousProvenance = fs.existsSync(provenancePath) ? fs.readFileSync(provenancePath) : null;
124
- const verifierPath = path.join(path.dirname(provenancePath), 'external-subagent-policy-verifier');
125
- const previousVerifier = fs.existsSync(verifierPath)
126
- ? { bytes: fs.readFileSync(verifierPath), mode: fs.statSync(verifierPath).mode & 0o777 }
127
- : null;
128
- try {
129
- atomicWrite(provenancePath, nextProvenance);
130
- atomicWriteBytes(verifierPath, fs.readFileSync(verifierSourcePath));
131
- fs.chmodSync(verifierPath, 0o700);
132
- atomicWriteBytes(configPath, nextConfigBytes);
133
- } catch (error) {
134
- if (previousProvenance === null) {
135
- try { fs.unlinkSync(provenancePath); } catch (unlinkError) {
136
- if (unlinkError?.code !== 'ENOENT') throw unlinkError;
137
- }
138
- } else {
139
- atomicWriteBytes(provenancePath, previousProvenance);
140
- }
141
- if (previousVerifier === null) {
142
- try { fs.unlinkSync(verifierPath); } catch (unlinkError) { if (unlinkError?.code !== 'ENOENT') throw unlinkError; }
143
- } else {
144
- atomicWriteBytes(verifierPath, previousVerifier.bytes);
145
- fs.chmodSync(verifierPath, previousVerifier.mode);
146
- }
147
- throw error;
148
- }
149
- console.log(JSON.stringify({ config: effectiveConfigPath, provenance: path.resolve(provenancePath), file_policy_sha256: filePolicySha256 }));
@@ -1,58 +0,0 @@
1
- #!/usr/bin/env node
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import crypto from 'node:crypto';
5
-
6
- function value(name) {
7
- const index = process.argv.indexOf(name);
8
- const result = index >= 0 ? process.argv[index + 1] : undefined;
9
- if (!result || result.startsWith('--')) process.exit(2);
10
- return result;
11
- }
12
-
13
- const workspace = path.resolve(value('--workspace'));
14
- const home = path.resolve(value('--home'));
15
- const mode = value('--permission-mode');
16
- const manifest = value('--write-manifest');
17
- if (mode !== 'plan' || manifest !== '[]' || !fs.statSync(workspace).isDirectory()) process.exit(2);
18
- const configPath = path.join(home, '.zcode', 'cli', 'config.json');
19
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
20
- const events = config?.hooks?.events;
21
- if (config?.hooks?.enabled !== true || !events || !Array.isArray(events.PreToolUse)) process.exit(2);
22
- const pre = events.PreToolUse.find((entry) => entry?.matcher === '^(Read|Grep|Glob|Write|Edit|Delete|Move)$');
23
- const hook = pre?.hooks?.find((entry) => entry?.type === 'process' && Array.isArray(entry.args));
24
- const script = hook?.args?.find((candidate) => typeof candidate === 'string' && candidate.endsWith('/hooks/check-agent-files.mjs'));
25
- const guard = events.PreToolUse.find((entry) => entry?.matcher === 'Bash')?.hooks?.find((entry) => entry?.type === 'process')?.args?.[0];
26
- const audit = events.PostToolUse?.find((entry) => entry?.matcher === 'Bash')?.hooks?.find((entry) => entry?.type === 'process')?.args?.[0];
27
- if (!script || !fs.statSync(script).isFile() || hook.command !== process.execPath || !guard || !fs.statSync(guard).isFile() || !audit || !fs.statSync(audit).isFile()) process.exit(2);
28
- // The provenance is staged by install-agent-hooks.mjs beside this verifier,
29
- // inside the product data directory of the probe home. The candidates mirror
30
- // the daemon's policy_verifier_candidates exactly: the frozen macOS
31
- // `~/Library/Application Support` bytes on darwin; elsewhere an exported
32
- // absolute $XDG_DATA_HOME is probed first, then the `~/.local/share` XDG
33
- // fallback for the scope home.
34
- const provenanceCandidates = process.platform === 'darwin'
35
- ? [path.join(home, 'Library', 'Application Support', 'external-subagent', 'zcode-agent-hook-provenance.json')]
36
- : [
37
- ...(typeof process.env.XDG_DATA_HOME === 'string' && path.isAbsolute(process.env.XDG_DATA_HOME)
38
- ? [path.join(process.env.XDG_DATA_HOME, 'external-subagent', 'zcode-agent-hook-provenance.json')]
39
- : []),
40
- path.join(home, '.local', 'share', 'external-subagent', 'zcode-agent-hook-provenance.json'),
41
- ];
42
- const provenancePath = provenanceCandidates.find((candidate) => fs.existsSync(candidate));
43
- if (!provenancePath) process.exit(2);
44
- const provenance = JSON.parse(fs.readFileSync(provenancePath, 'utf8'));
45
- const hash = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
46
- const policy = path.resolve(path.dirname(script), '../lib/agent-file-policy.mjs');
47
- if (provenance.effective_config_path !== path.resolve(configPath)
48
- || provenance.hook_activation_verified !== true
49
- || provenance.effective_file_policy_version !== 'zcode-agent-file-policy/v1.0.0'
50
- || provenance.effective_file_policy_path !== policy
51
- || provenance.effective_file_policy_sha256 !== hash(policy)
52
- || provenance.effective_guard_wrapper_path !== path.resolve(guard)
53
- || provenance.effective_guard_wrapper_sha256 !== hash(guard)
54
- || provenance.effective_file_wrapper_path !== path.resolve(script)
55
- || provenance.effective_file_wrapper_sha256 !== hash(script)
56
- || provenance.effective_audit_wrapper_path !== path.resolve(audit)
57
- || provenance.effective_audit_wrapper_sha256 !== hash(audit)) process.exit(2);
58
- process.exit(0);