@the-open-engine/zeroshot 6.28.0 → 6.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +3 -3
  2. package/cli/index.js +21 -223
  3. package/docker/zeroshot-cluster/Dockerfile +2 -3
  4. package/docker/zeroshot-oecp/Cargo.toml +12 -0
  5. package/docker/zeroshot-oecp/Dockerfile +65 -0
  6. package/docker/zeroshot-oecp/src/main.rs +32 -0
  7. package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
  8. package/lib/agent-cli-provider/adapters/codex.js +1 -0
  9. package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
  10. package/lib/cluster-worker/engine-adapter.js +11 -15
  11. package/lib/cluster-worker/engine-input.js +14 -0
  12. package/lib/cluster-worker/profiles.js +42 -1
  13. package/lib/start-cluster.js +3 -3
  14. package/lib/target/bounded-json.d.ts +6 -0
  15. package/lib/target/bounded-json.js +43 -0
  16. package/lib/target/device-flow.d.ts +5 -1
  17. package/lib/target/device-flow.js +6 -1
  18. package/lib/target/discovery.d.ts +1 -0
  19. package/lib/target/discovery.js +12 -35
  20. package/lib/target/hosted-run/client.d.ts +26 -0
  21. package/lib/target/hosted-run/client.js +158 -0
  22. package/lib/target/hosted-run/commands.d.ts +4 -0
  23. package/lib/target/hosted-run/commands.js +113 -0
  24. package/lib/target/hosted-run/contracts.d.ts +49 -0
  25. package/lib/target/hosted-run/contracts.js +3 -0
  26. package/lib/target/hosted-run/input.d.ts +5 -0
  27. package/lib/target/hosted-run/input.js +200 -0
  28. package/lib/target/hosted-run.d.ts +4 -0
  29. package/lib/target/hosted-run.js +12 -0
  30. package/lib/target/target-session.d.ts +1 -0
  31. package/lib/target/target-session.js +2 -1
  32. package/package.json +16 -3
  33. package/scripts/audit-production-dependencies.js +150 -0
  34. package/scripts/opcore-agent-gate.js +159 -0
  35. package/scripts/opcore-agent-tool-overlays.js +154 -0
  36. package/scripts/opcore-introduced-check.js +290 -0
  37. package/src/agent-cli-provider/adapters/codex.ts +1 -0
  38. package/src/isolation-manager.js +16 -1
  39. package/src/target/bounded-json.ts +48 -0
  40. package/src/target/device-flow.ts +14 -3
  41. package/src/target/discovery.ts +17 -35
  42. package/src/target/hosted-run/client.ts +198 -0
  43. package/src/target/hosted-run/commands.ts +140 -0
  44. package/src/target/hosted-run/contracts.ts +53 -0
  45. package/src/target/hosted-run/input.ts +199 -0
  46. package/src/target/hosted-run.ts +8 -0
  47. package/src/target/target-session.ts +5 -1
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveHostedInput = resolveHostedInput;
7
+ exports.githubToken = githubToken;
8
+ exports.providerKey = providerKey;
9
+ exports.validateHostedOptions = validateHostedOptions;
10
+ const node_fs_1 = __importDefault(require("node:fs"));
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ const node_child_process_1 = require("node:child_process");
13
+ const SUBMISSION_KEY = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{12}$/i;
14
+ const CAPSULE_SIZES = new Set(['tiny', 'small', 'standard', 'large']);
15
+ function validRepository(value) {
16
+ if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(value))
17
+ return false;
18
+ return value.split('/').every((segment) => segment !== '.' && segment !== '..');
19
+ }
20
+ function validModel(value) {
21
+ return (value.length <= 256 && /^[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(value));
22
+ }
23
+ function repositoryFromRemote(cwd = process.cwd()) {
24
+ let remote;
25
+ try {
26
+ remote = (0, node_child_process_1.execFileSync)('git', ['remote', 'get-url', 'origin'], {
27
+ cwd,
28
+ encoding: 'utf8',
29
+ stdio: ['ignore', 'pipe', 'ignore'],
30
+ }).trim();
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ const match = remote.match(/^(?:git@github\.com:|ssh:\/\/git@github\.com\/|https?:\/\/github\.com\/)([^/]+\/[^/]+?)(?:\.git)?$/);
36
+ const repository = match?.[1];
37
+ return repository && validRepository(repository) ? repository : null;
38
+ }
39
+ function isolationProfile(options) {
40
+ return options.pr ? 'isolation.pr@1' : 'isolation.worktree@1';
41
+ }
42
+ function providerProfile(options) {
43
+ return options.pr ? 'provider.codex-openrouter-pr@1' : 'provider.codex-openrouter@1';
44
+ }
45
+ function promptRequest(prompt, options) {
46
+ return {
47
+ source: 'prompt',
48
+ prompt,
49
+ artifacts: [],
50
+ isolationProfile: isolationProfile(options),
51
+ providerProfile: providerProfile(options),
52
+ };
53
+ }
54
+ function issueRequest(issue, options) {
55
+ return {
56
+ source: 'issue',
57
+ issue,
58
+ artifacts: [],
59
+ isolationProfile: isolationProfile(options),
60
+ providerProfile: providerProfile(options),
61
+ };
62
+ }
63
+ function issueInput(value, options) {
64
+ const shorthand = value.match(/^([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)#([1-9][0-9]*)$/);
65
+ if (shorthand?.[1] && shorthand[2] && validRepository(shorthand[1])) {
66
+ const issue = `https://github.com/${shorthand[1]}/issues/${shorthand[2]}`;
67
+ return { repository: shorthand[1], request: issueRequest(issue, options) };
68
+ }
69
+ let url;
70
+ try {
71
+ url = new URL(value);
72
+ }
73
+ catch {
74
+ return null;
75
+ }
76
+ const match = url.pathname.match(/^\/([^/]+)\/([^/]+)\/issues\/([1-9][0-9]*)\/?$/);
77
+ const repository = match?.[1] && match[2] ? `${match[1]}/${match[2]}` : '';
78
+ if (url.hostname !== 'github.com' || !match || !validRepository(repository))
79
+ return null;
80
+ return { repository, request: issueRequest(url.href, options) };
81
+ }
82
+ async function readStdin() {
83
+ if (process.stdin.isTTY)
84
+ throw new Error('zeroshot run - requires piped input');
85
+ const chunks = [];
86
+ let bytes = 0;
87
+ for await (const chunk of process.stdin) {
88
+ const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
89
+ bytes += value.length;
90
+ if (bytes > 1024 * 1024)
91
+ throw new Error('hosted task input exceeds 1 MiB');
92
+ chunks.push(value);
93
+ }
94
+ const value = Buffer.concat(chunks).toString('utf8').trim();
95
+ if (!value)
96
+ throw new Error('hosted task input is empty');
97
+ return value;
98
+ }
99
+ function readTaskFile(filename) {
100
+ const flags = node_fs_1.default.constants.O_RDONLY | (node_fs_1.default.constants.O_NOFOLLOW ?? 0) | (node_fs_1.default.constants.O_NONBLOCK ?? 0);
101
+ let descriptor;
102
+ try {
103
+ descriptor = node_fs_1.default.openSync(filename, flags);
104
+ }
105
+ catch (error) {
106
+ const code = error.code;
107
+ if (code === 'ENOENT' || code === 'ENOTDIR' || code === 'EISDIR')
108
+ return null;
109
+ if (code === 'ELOOP')
110
+ throw new Error(`hosted task file must not be a symlink: ${filename}`);
111
+ throw error;
112
+ }
113
+ try {
114
+ if (!node_fs_1.default.fstatSync(descriptor).isFile())
115
+ return null;
116
+ return node_fs_1.default.readFileSync(descriptor, 'utf8');
117
+ }
118
+ finally {
119
+ node_fs_1.default.closeSync(descriptor);
120
+ }
121
+ }
122
+ async function resolveHostedInput(input, options, environment = process.env) {
123
+ const explicitIssue = issueInput(input, options);
124
+ if (explicitIssue)
125
+ return explicitIssue;
126
+ const repository = options.repository ?? environment['ZEROSHOT_REPOSITORY'] ?? repositoryFromRemote();
127
+ if (!validRepository(repository ?? '')) {
128
+ throw new Error('hosted runs need a GitHub repository; use org/repo#123, --repository owner/name, ' +
129
+ 'ZEROSHOT_REPOSITORY, or run inside a GitHub checkout');
130
+ }
131
+ if (/^[1-9][0-9]*$/.test(input)) {
132
+ return { repository: repository, request: issueRequest(input, options) };
133
+ }
134
+ const prompt = input === '-' ? await readStdin() : (readTaskFile(node_path_1.default.resolve(input)) ?? input);
135
+ if (!prompt.trim())
136
+ throw new Error('hosted task input is empty');
137
+ return { repository: repository, request: promptRequest(prompt.trim(), options) };
138
+ }
139
+ function githubToken(environment) {
140
+ const configured = environment['GH_TOKEN'] ?? environment['GITHUB_TOKEN'];
141
+ if (configured?.trim())
142
+ return configured.trim();
143
+ try {
144
+ const token = (0, node_child_process_1.execFileSync)('gh', ['auth', 'token'], {
145
+ encoding: 'utf8',
146
+ stdio: ['ignore', 'pipe', 'ignore'],
147
+ }).trim();
148
+ if (token)
149
+ return token;
150
+ }
151
+ catch {
152
+ // The actionable error below covers missing and unauthenticated gh alike.
153
+ }
154
+ throw new Error('hosted runs require GH_TOKEN/GITHUB_TOKEN or an authenticated gh CLI');
155
+ }
156
+ function providerKey(environment) {
157
+ const key = environment['OPENROUTER_API_KEY'];
158
+ if (!key?.trim())
159
+ throw new Error('hosted Codex runs require OPENROUTER_API_KEY');
160
+ return key.trim();
161
+ }
162
+ function validateHostedOptions(options) {
163
+ const unsupported = [
164
+ ['config', '--config'],
165
+ ['docker', '--docker'],
166
+ ['worktree', '--worktree'],
167
+ ['dockerImage', '--docker-image'],
168
+ ['strictSchema', '--strict-schema'],
169
+ ['ship', '--ship'],
170
+ ['prBase', '--pr-base'],
171
+ ['mergeQueue', '--merge-queue'],
172
+ ['closeIssue', '--close-issue'],
173
+ ['workers', '--workers'],
174
+ ['gitlab', '--gitlab'],
175
+ ['jira', '--jira'],
176
+ ['devops', '--devops'],
177
+ ['linear', '--linear'],
178
+ ['mount', '--mount'],
179
+ ['noMounts', '--no-mounts'],
180
+ ['containerHome', '--container-home'],
181
+ ];
182
+ const selected = unsupported
183
+ .filter(([name]) => options[name] !== undefined && options[name] !== false)
184
+ .map(([, flag]) => flag);
185
+ if (options.provider && options.provider !== 'codex')
186
+ selected.push('--provider');
187
+ if (selected.length)
188
+ throw new Error(`hosted runs do not support ${selected.join(', ')}`);
189
+ if (!options.target)
190
+ throw new Error('hosted runs require --target');
191
+ if (options.model !== undefined && !validModel(options.model)) {
192
+ throw new Error('hosted runs require an exact provider/model slug');
193
+ }
194
+ if (!CAPSULE_SIZES.has(options.size ?? 'standard')) {
195
+ throw new Error('hosted runs require --size tiny, small, standard, or large');
196
+ }
197
+ if (options.submissionKey !== undefined && !SUBMISSION_KEY.test(options.submissionKey)) {
198
+ throw new Error('hosted runs require --submission-key to be a random UUID');
199
+ }
200
+ }
@@ -0,0 +1,4 @@
1
+ export { HostedRunHttpError } from './hosted-run/client.ts';
2
+ export { cancelHostedRun, runHosted, statusHostedRun } from './hosted-run/commands.ts';
3
+ export { resolveHostedInput, validateHostedOptions } from './hosted-run/input.ts';
4
+ export type { HostedOptions, HostedRunDependencies, HostedRunIntent, } from './hosted-run/contracts.ts';
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateHostedOptions = exports.resolveHostedInput = exports.statusHostedRun = exports.runHosted = exports.cancelHostedRun = exports.HostedRunHttpError = void 0;
4
+ var client_ts_1 = require("./hosted-run/client.js");
5
+ Object.defineProperty(exports, "HostedRunHttpError", { enumerable: true, get: function () { return client_ts_1.HostedRunHttpError; } });
6
+ var commands_ts_1 = require("./hosted-run/commands.js");
7
+ Object.defineProperty(exports, "cancelHostedRun", { enumerable: true, get: function () { return commands_ts_1.cancelHostedRun; } });
8
+ Object.defineProperty(exports, "runHosted", { enumerable: true, get: function () { return commands_ts_1.runHosted; } });
9
+ Object.defineProperty(exports, "statusHostedRun", { enumerable: true, get: function () { return commands_ts_1.statusHostedRun; } });
10
+ var input_ts_1 = require("./hosted-run/input.js");
11
+ Object.defineProperty(exports, "resolveHostedInput", { enumerable: true, get: function () { return input_ts_1.resolveHostedInput; } });
12
+ Object.defineProperty(exports, "validateHostedOptions", { enumerable: true, get: function () { return input_ts_1.validateHostedOptions; } });
@@ -20,6 +20,7 @@ export interface TargetSessionDeps {
20
20
  readonly tokenEndpoint: string;
21
21
  readonly revocationEndpoint?: string;
22
22
  readonly clientId: string;
23
+ readonly capsuleApiBaseUrl: string;
23
24
  };
24
25
  }
25
26
  export declare function targetLogin(targetName: string, target: TargetRecord, credentialStore: TargetCredentialStore, acquireLock: () => Promise<() => Promise<void>>, settings: SettingsPort, deps: TargetSessionDeps): Promise<{
@@ -30,7 +30,7 @@ async function targetLogin(targetName, target, credentialStore, acquireLock, set
30
30
  // Browser open is best-effort
31
31
  }
32
32
  }
33
- const tokenResponse = await (0, device_flow_ts_1.pollForToken)(tokenEndpoint, clientId, codeResponse.device_code, codeResponse.interval, codeResponse.expires_in, http, clock);
33
+ const tokenResponse = await (0, device_flow_ts_1.pollForToken)(tokenEndpoint, clientId, codeResponse.device_code, codeResponse.interval, codeResponse.expires_in, http, clock, undefined, { token: target.deviceToken, label: 'Zeroshot CLI' });
34
34
  if (!tokenResponse.organization) {
35
35
  throw new device_flow_ts_1.UnboundSessionError(codeResponse.verification_uri);
36
36
  }
@@ -59,6 +59,7 @@ async function refreshAccessToken(targetName, target, credentialStore, acquireLo
59
59
  grant_type: 'refresh_token',
60
60
  refresh_token: currentRefreshToken,
61
61
  client_id: clientId,
62
+ audience: 'capsule',
62
63
  });
63
64
  let tokenResponse;
64
65
  const response = await http.fetch(tokenEndpoint, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@the-open-engine/zeroshot",
3
- "version": "6.28.0",
3
+ "version": "6.29.0",
4
4
  "description": "Multi-agent orchestration engine for Claude, Codex, and Gemini",
5
5
  "main": "src/orchestrator.js",
6
6
  "bin": {
@@ -60,6 +60,15 @@
60
60
  "test:cluster-client": "npm run build:cluster && node --test tests/cluster/client.test.js tests/cluster/parity.test.js tests/cluster/architecture.test.js tests/cluster/verifier-regressions.test.js tests/cluster/request-validation.test.js tests/hosted-session/coordinator.test.js",
61
61
  "test:cluster-package": "npm run build:agent-cli-provider && npm run build:cluster && npm run build:target && node --test tests/cluster/package.test.js",
62
62
  "dev:link": "npm link",
63
+ "opcore:status": "opcore status --repo . --json",
64
+ "opcore:scan": "opcore --repo . --json",
65
+ "opcore:check": "node scripts/opcore-introduced-check.js",
66
+ "opcore:check:staged": "node scripts/opcore-introduced-check.js --staged",
67
+ "opcore:install:rust-metrics": "cargo install rust-code-analysis-cli --version 0.0.25 --locked",
68
+ "opcore:graph:build": "opcore graph build --repo . --json",
69
+ "opcore:graph:update": "opcore graph update --repo . --json",
70
+ "opcore:measure": "opcore measure --repo . --json",
71
+ "audit:production": "node scripts/audit-production-dependencies.js",
63
72
  "lint": "eslint .",
64
73
  "lint:fix": "eslint . --fix",
65
74
  "validate:templates": "node scripts/validate-templates.js",
@@ -122,7 +131,7 @@
122
131
  "url": "https://github.com/the-open-engine/zeroshot/issues"
123
132
  },
124
133
  "engines": {
125
- "node": ">=18.0.0"
134
+ "node": ">=22.0.0"
126
135
  },
127
136
  "publishConfig": {
128
137
  "access": "public",
@@ -148,6 +157,9 @@
148
157
  "files": [
149
158
  "src/",
150
159
  "lib/",
160
+ "!src/target/register-hosted-commands.ts",
161
+ "!lib/target/register-hosted-commands.js",
162
+ "!lib/target/register-hosted-commands.d.ts",
151
163
  "bin/",
152
164
  "cli/",
153
165
  "task-lib/",
@@ -169,8 +181,8 @@
169
181
  "commander": "^14.0.2",
170
182
  "node-pty": "^1.1.0",
171
183
  "omelette": "^0.4.17",
172
- "pidusage": "^4.0.1",
173
184
  "open": "^10.1.0",
185
+ "pidusage": "^4.0.1",
174
186
  "proper-lockfile": "^4.1.2"
175
187
  },
176
188
  "optionalDependencies": {
@@ -196,6 +208,7 @@
196
208
  "jscpd": "^3.5.10",
197
209
  "lint-staged": "^16.2.7",
198
210
  "mocha": "^11.7.5",
211
+ "opcore": "0.2.1",
199
212
  "prettier": "^3.7.4",
200
213
  "semantic-release": "^25.0.2",
201
214
  "sinon": "^21.0.0",
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { spawnSync } = require('node:child_process');
5
+
6
+ const severityRank = new Map([
7
+ ['info', 0],
8
+ ['low', 1],
9
+ ['moderate', 2],
10
+ ['high', 3],
11
+ ['critical', 4],
12
+ ]);
13
+ const approvedLeafAdvisories = new Map([
14
+ ['brace-expansion', new Set(['1123898', '1130591'])],
15
+ ['smol-toml', new Set(['1115393'])],
16
+ ]);
17
+ const approvedLeafPaths = new Map([
18
+ ['brace-expansion', 'node_modules/opcore/node_modules/brace-expansion'],
19
+ ['smol-toml', 'node_modules/opcore/node_modules/smol-toml'],
20
+ ]);
21
+ const approvedVersions = new Map([
22
+ ['node_modules/opcore', '0.2.1'],
23
+ ['node_modules/opcore/node_modules/brace-expansion', '5.0.6'],
24
+ ['node_modules/opcore/node_modules/smol-toml', '1.6.0'],
25
+ ]);
26
+
27
+ function isRelevant(vulnerability) {
28
+ return (severityRank.get(vulnerability.severity) ?? Number.POSITIVE_INFINITY) >= 2;
29
+ }
30
+
31
+ function hasExpectedNodes(name, vulnerability) {
32
+ const nodes = vulnerability.nodes;
33
+ if (!Array.isArray(nodes) || nodes.length === 0) return false;
34
+ const leafPath = approvedLeafPaths.get(name);
35
+ if (leafPath !== undefined) return nodes.length === 1 && nodes[0] === leafPath;
36
+ if (name === 'opcore') return nodes.length === 1 && nodes[0] === 'node_modules/opcore';
37
+ return nodes.every((node) => node.startsWith('node_modules/opcore/node_modules/'));
38
+ }
39
+
40
+ function isApprovedLeaf(name, vulnerability) {
41
+ const approvedSources = approvedLeafAdvisories.get(name);
42
+ if (approvedSources === undefined || !hasExpectedNodes(name, vulnerability)) return false;
43
+ if (!Array.isArray(vulnerability.via) || vulnerability.via.length === 0) return false;
44
+ return vulnerability.via.every(
45
+ (advisory) =>
46
+ advisory !== null &&
47
+ typeof advisory === 'object' &&
48
+ approvedSources.has(String(advisory.source))
49
+ );
50
+ }
51
+
52
+ function isApprovedVulnerability(name, vulnerabilities, memo, active) {
53
+ if (memo.has(name)) return memo.get(name);
54
+ if (active.has(name)) return false;
55
+ const vulnerability = vulnerabilities[name];
56
+ if (vulnerability === undefined || !hasExpectedNodes(name, vulnerability)) return false;
57
+ if (isApprovedLeaf(name, vulnerability)) {
58
+ memo.set(name, true);
59
+ return true;
60
+ }
61
+ if (!Array.isArray(vulnerability.via) || vulnerability.via.length === 0) return false;
62
+ if (!vulnerability.via.every((dependency) => typeof dependency === 'string')) return false;
63
+
64
+ active.add(name);
65
+ const approved = vulnerability.via.every((dependency) =>
66
+ isApprovedVulnerability(dependency, vulnerabilities, memo, active)
67
+ );
68
+ active.delete(name);
69
+ memo.set(name, approved);
70
+ return approved;
71
+ }
72
+
73
+ function exceptionVersionsMatch(lock) {
74
+ if (lock?.packages?.['']?.dependencies?.opcore !== '0.2.1') return false;
75
+ return Array.from(approvedVersions).every(
76
+ ([packagePath, version]) => lock?.packages?.[packagePath]?.version === version
77
+ );
78
+ }
79
+
80
+ function evaluateAudit(payload, lock) {
81
+ if (payload === null || typeof payload !== 'object' || payload.error !== undefined) {
82
+ throw new Error('npm audit did not return a valid vulnerability report');
83
+ }
84
+ const vulnerabilities = payload.vulnerabilities;
85
+ if (vulnerabilities === null || typeof vulnerabilities !== 'object') {
86
+ throw new Error('npm audit report is missing vulnerabilities');
87
+ }
88
+
89
+ const relevant = Object.entries(vulnerabilities).filter(([, vulnerability]) =>
90
+ isRelevant(vulnerability)
91
+ );
92
+ const memo = new Map();
93
+ const allowed = relevant.filter(([name]) =>
94
+ isApprovedVulnerability(name, vulnerabilities, memo, new Set())
95
+ );
96
+ if (allowed.length > 0 && !exceptionVersionsMatch(lock)) {
97
+ return { allowed: [], blocked: relevant, versionMismatch: true };
98
+ }
99
+ const allowedNames = new Set(allowed.map(([name]) => name));
100
+ return {
101
+ allowed,
102
+ blocked: relevant.filter(([name]) => !allowedNames.has(name)),
103
+ versionMismatch: false,
104
+ };
105
+ }
106
+
107
+ function runAudit() {
108
+ const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
109
+ const result = spawnSync(npm, ['audit', '--audit-level=moderate', '--omit=dev', '--json'], {
110
+ encoding: 'utf8',
111
+ maxBuffer: 16 * 1024 * 1024,
112
+ });
113
+ if (result.error) throw result.error;
114
+
115
+ let payload;
116
+ try {
117
+ payload = JSON.parse(result.stdout);
118
+ } catch {
119
+ throw new Error(
120
+ result.stderr || result.stdout || `npm audit exited with status ${result.status}`
121
+ );
122
+ }
123
+ const lock = require('../package-lock.json');
124
+ return evaluateAudit(payload, lock);
125
+ }
126
+
127
+ function main() {
128
+ const result = runAudit();
129
+ if (result.blocked.length > 0) {
130
+ const suffix = result.versionMismatch ? ' (Opcore exception version mismatch)' : '';
131
+ console.error(
132
+ `Production dependency audit failed${suffix}: ${result.blocked.map(([name]) => name).join(', ')}`
133
+ );
134
+ process.exitCode = 1;
135
+ return;
136
+ }
137
+ if (result.allowed.length > 0) {
138
+ console.warn(
139
+ `Production dependency audit passed with pinned Opcore advisories: ${result.allowed
140
+ .map(([name]) => name)
141
+ .join(', ')}`
142
+ );
143
+ return;
144
+ }
145
+ console.log('Production dependency audit passed');
146
+ }
147
+
148
+ if (require.main === module) main();
149
+
150
+ module.exports = { evaluateAudit };
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawnSync } = require('node:child_process');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const { extractToolRequest, isRecord, overlaysForTool } = require('./opcore-agent-tool-overlays');
8
+
9
+ const validationTimeoutMs = 120_000;
10
+ const maxFeedbackChars = 4000;
11
+ const preWriteChecks = [
12
+ 'typescript.syntax',
13
+ 'typescript.types',
14
+ 'typescript.lint',
15
+ 'typescript.function-metrics',
16
+ 'typescript.file-length',
17
+ 'rust.source-hygiene',
18
+ 'rust.fmt',
19
+ 'rust.file-length',
20
+ 'rust.function-metrics',
21
+ 'docs.staleness',
22
+ 'docs.freshness',
23
+ 'docs.length',
24
+ 'docs.dry',
25
+ 'docs.content-quality',
26
+ 'docs.code-blocks',
27
+ 'docs.rules-why',
28
+ ];
29
+
30
+ function parseArgs(argv) {
31
+ const args = { harness: 'unknown' };
32
+ let index = 0;
33
+ while (index < argv.length) {
34
+ const arg = argv[index];
35
+ const next = argv[index + 1];
36
+ if (arg === '--harness') args.harness = next || 'unknown';
37
+ else if (arg.startsWith('--harness=')) args.harness = arg.slice('--harness='.length);
38
+ else if (arg === '--repo') args.repo = next;
39
+ else if (arg.startsWith('--repo=')) args.repo = arg.slice('--repo='.length);
40
+ index += arg === '--harness' || arg === '--repo' ? 2 : 1;
41
+ }
42
+ return args;
43
+ }
44
+
45
+ function resolveRepoRoot(explicitRepo, cwd) {
46
+ if (explicitRepo) return path.resolve(explicitRepo);
47
+ const start = path.resolve(cwd || process.cwd());
48
+ const result = spawnSync('git', ['rev-parse', '--show-toplevel'], {
49
+ cwd: start,
50
+ encoding: 'utf8',
51
+ stdio: ['ignore', 'pipe', 'ignore'],
52
+ });
53
+ return result.status === 0 && result.stdout.trim() ? path.resolve(result.stdout.trim()) : start;
54
+ }
55
+
56
+ function runValidation(request) {
57
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'zeroshot-opcore-prewrite-'));
58
+ const requestPath = path.join(tempDir, 'validation-request.json');
59
+ try {
60
+ fs.writeFileSync(requestPath, `${JSON.stringify(request)}\n`);
61
+ const opcoreEntrypoint = require.resolve('opcore');
62
+ const result = spawnSync(
63
+ process.execPath,
64
+ [
65
+ opcoreEntrypoint,
66
+ 'validate',
67
+ 'pre-write',
68
+ '--request-file',
69
+ requestPath,
70
+ '--timeout-ms',
71
+ String(validationTimeoutMs),
72
+ '--json',
73
+ ],
74
+ {
75
+ cwd: request.repo.repoRoot,
76
+ encoding: 'utf8',
77
+ stdio: ['ignore', 'pipe', 'pipe'],
78
+ }
79
+ );
80
+ if (result.error) throw result.error;
81
+ const payload = JSON.parse((result.stdout || '').trim());
82
+ if (!payload.receipt) {
83
+ throw new Error(result.stderr || result.stdout || `Opcore exited ${result.status}`);
84
+ }
85
+ return payload.receipt;
86
+ } finally {
87
+ fs.rmSync(tempDir, { recursive: true, force: true });
88
+ }
89
+ }
90
+
91
+ function feedback(toolName, receipt) {
92
+ const summary = receipt.failureSummary?.message || 'Pre-write validation failed';
93
+ const checks = receipt.checks?.length ? ` checks=${receipt.checks.join(',')}` : '';
94
+ const paths = receipt.overlays?.paths?.length ? ` paths=${receipt.overlays.paths.join(',')}` : '';
95
+ const message = `Opcore write gate blocked ${toolName}: ${summary} status=${receipt.validationStatus}${checks}${paths}\n`;
96
+ return message.length <= maxFeedbackChars
97
+ ? message
98
+ : `${message.slice(0, maxFeedbackChars - 15).trimEnd()} [truncated]\n`;
99
+ }
100
+
101
+ async function readStdin() {
102
+ let input = '';
103
+ for await (const chunk of process.stdin) input += chunk.toString();
104
+ return input;
105
+ }
106
+
107
+ async function main() {
108
+ const args = parseArgs(process.argv.slice(2));
109
+ let mapped;
110
+ try {
111
+ const raw = await readStdin();
112
+ if (!raw.trim()) return 0;
113
+ const envelope = JSON.parse(raw);
114
+ if (!isRecord(envelope)) throw new Error('hook payload must be a JSON object');
115
+ const tool = extractToolRequest(envelope);
116
+ if (!tool) return 0;
117
+ const repoRoot = resolveRepoRoot(args.repo, tool.cwd);
118
+ const overlays = await overlaysForTool(repoRoot, tool);
119
+ if (overlays.length === 0) return 0;
120
+ mapped = {
121
+ toolName: tool.toolName,
122
+ request: {
123
+ requestId: `zeroshot-opcore-agent-gate-${Date.now()}`,
124
+ repo: { repoRoot },
125
+ scope: { kind: 'files', files: overlays.map((overlay) => overlay.path) },
126
+ graph: { mode: 'optional', provider: 'opcore-graph' },
127
+ overlays,
128
+ checks: preWriteChecks,
129
+ reportMode: 'introduced',
130
+ },
131
+ };
132
+ } catch (error) {
133
+ process.stderr.write(
134
+ `Opcore write gate skipped: ${error instanceof Error ? error.message : String(error)}\n`
135
+ );
136
+ return 0;
137
+ }
138
+
139
+ try {
140
+ const receipt = runValidation(mapped.request);
141
+ if (receipt.ok) return 0;
142
+ process.stderr.write(feedback(mapped.toolName, receipt));
143
+ return 2;
144
+ } catch (error) {
145
+ process.stderr.write(
146
+ `Opcore write gate blocked ${mapped.toolName}: validation command failed: ${error instanceof Error ? error.message : String(error)}\n`
147
+ );
148
+ return 2;
149
+ }
150
+ }
151
+
152
+ main()
153
+ .then((exitCode) => {
154
+ process.exitCode = exitCode;
155
+ })
156
+ .catch((error) => {
157
+ console.error(error instanceof Error ? error.message : String(error));
158
+ process.exitCode = 2;
159
+ });