@the-open-engine/zeroshot 6.27.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.
- package/README.md +3 -3
- package/cli/index.js +28 -21
- package/docker/zeroshot-cluster/Dockerfile +2 -3
- package/docker/zeroshot-oecp/Cargo.toml +12 -0
- package/docker/zeroshot-oecp/Dockerfile +65 -0
- package/docker/zeroshot-oecp/src/main.rs +32 -0
- package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.js +1 -0
- package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
- package/lib/cluster-worker/engine-adapter.js +11 -15
- package/lib/cluster-worker/engine-input.js +14 -0
- package/lib/cluster-worker/profiles.js +42 -1
- package/lib/start-cluster.js +3 -3
- package/lib/target/bounded-json.d.ts +6 -0
- package/lib/target/bounded-json.js +43 -0
- package/lib/target/credential-lock.d.ts +1 -0
- package/lib/target/credential-lock.js +38 -0
- package/lib/target/credential-store.d.ts +27 -0
- package/lib/target/credential-store.js +113 -0
- package/lib/target/device-flow.d.ts +42 -0
- package/lib/target/device-flow.js +109 -0
- package/lib/target/discovery.d.ts +12 -0
- package/lib/target/discovery.js +97 -0
- package/lib/target/hosted-run/client.d.ts +26 -0
- package/lib/target/hosted-run/client.js +158 -0
- package/lib/target/hosted-run/commands.d.ts +4 -0
- package/lib/target/hosted-run/commands.js +113 -0
- package/lib/target/hosted-run/contracts.d.ts +49 -0
- package/lib/target/hosted-run/contracts.js +3 -0
- package/lib/target/hosted-run/input.d.ts +5 -0
- package/lib/target/hosted-run/input.js +200 -0
- package/lib/target/hosted-run.d.ts +4 -0
- package/lib/target/hosted-run.js +12 -0
- package/lib/target/index.d.ts +6 -0
- package/lib/target/index.js +38 -0
- package/lib/target/target-registry.d.ts +45 -0
- package/lib/target/target-registry.js +132 -0
- package/lib/target/target-session.d.ts +40 -0
- package/lib/target/target-session.js +163 -0
- package/package.json +25 -7
- package/scripts/audit-production-dependencies.js +150 -0
- package/scripts/opcore-agent-gate.js +159 -0
- package/scripts/opcore-agent-tool-overlays.js +154 -0
- package/scripts/opcore-introduced-check.js +290 -0
- package/src/agent-cli-provider/adapters/codex.ts +1 -0
- package/src/isolation-manager.js +16 -1
- package/src/target/bounded-json.ts +48 -0
- package/src/target/credential-lock.ts +35 -0
- package/src/target/credential-store.ts +107 -0
- package/src/target/device-flow.ts +168 -0
- package/src/target/discovery.ts +131 -0
- package/src/target/hosted-run/client.ts +198 -0
- package/src/target/hosted-run/commands.ts +140 -0
- package/src/target/hosted-run/contracts.ts +53 -0
- package/src/target/hosted-run/input.ts +199 -0
- package/src/target/hosted-run.ts +8 -0
- package/src/target/index.ts +55 -0
- package/src/target/target-registry.ts +174 -0
- package/src/target/target-session.ts +253 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LoginRequiredError = void 0;
|
|
4
|
+
exports.targetLogin = targetLogin;
|
|
5
|
+
exports.refreshAccessToken = refreshAccessToken;
|
|
6
|
+
exports.getAccessTokenProvider = getAccessTokenProvider;
|
|
7
|
+
exports.revokeAndCleanup = revokeAndCleanup;
|
|
8
|
+
const device_flow_ts_1 = require("./device-flow.js");
|
|
9
|
+
const credential_store_ts_1 = require("./credential-store.js");
|
|
10
|
+
const target_registry_ts_1 = require("./target-registry.js");
|
|
11
|
+
class LoginRequiredError extends Error {
|
|
12
|
+
targetName;
|
|
13
|
+
constructor(targetName) {
|
|
14
|
+
super(`Login required. Run: zeroshot target login ${targetName}`);
|
|
15
|
+
this.name = 'LoginRequiredError';
|
|
16
|
+
this.targetName = targetName;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.LoginRequiredError = LoginRequiredError;
|
|
20
|
+
async function targetLogin(targetName, target, credentialStore, acquireLock, settings, deps) {
|
|
21
|
+
const { http, clock, browserOpener, stderr, discoveryEndpoints } = deps;
|
|
22
|
+
const { deviceAuthorizationEndpoint, tokenEndpoint, clientId } = discoveryEndpoints;
|
|
23
|
+
const codeResponse = await (0, device_flow_ts_1.requestDeviceCode)(deviceAuthorizationEndpoint, clientId, http);
|
|
24
|
+
stderr.write(`\nOpen this URL to authorize:\n ${codeResponse.verification_uri}\n\nEnter code: ${codeResponse.user_code}\n\n`);
|
|
25
|
+
if (codeResponse.verification_uri_complete) {
|
|
26
|
+
try {
|
|
27
|
+
await browserOpener.open(codeResponse.verification_uri_complete);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Browser open is best-effort
|
|
31
|
+
}
|
|
32
|
+
}
|
|
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
|
+
if (!tokenResponse.organization) {
|
|
35
|
+
throw new device_flow_ts_1.UnboundSessionError(codeResponse.verification_uri);
|
|
36
|
+
}
|
|
37
|
+
const serviceKey = (0, credential_store_ts_1.targetServiceKey)(target.id);
|
|
38
|
+
const release = await acquireLock();
|
|
39
|
+
try {
|
|
40
|
+
await credentialStore.set(serviceKey, credential_store_ts_1.TARGET_ACCOUNT, tokenResponse.refresh_token);
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await release();
|
|
44
|
+
}
|
|
45
|
+
(0, target_registry_ts_1.updateTargetOrganization)(targetName, tokenResponse.organization, settings);
|
|
46
|
+
return { organization: tokenResponse.organization };
|
|
47
|
+
}
|
|
48
|
+
async function refreshAccessToken(targetName, target, credentialStore, acquireLock, deps) {
|
|
49
|
+
const { http, discoveryEndpoints } = deps;
|
|
50
|
+
const { tokenEndpoint, revocationEndpoint, clientId } = discoveryEndpoints;
|
|
51
|
+
const serviceKey = (0, credential_store_ts_1.targetServiceKey)(target.id);
|
|
52
|
+
const release = await acquireLock();
|
|
53
|
+
try {
|
|
54
|
+
const currentRefreshToken = await credentialStore.get(serviceKey, credential_store_ts_1.TARGET_ACCOUNT);
|
|
55
|
+
if (!currentRefreshToken) {
|
|
56
|
+
throw new LoginRequiredError(targetName);
|
|
57
|
+
}
|
|
58
|
+
const body = new URLSearchParams({
|
|
59
|
+
grant_type: 'refresh_token',
|
|
60
|
+
refresh_token: currentRefreshToken,
|
|
61
|
+
client_id: clientId,
|
|
62
|
+
audience: 'capsule',
|
|
63
|
+
});
|
|
64
|
+
let tokenResponse;
|
|
65
|
+
const response = await http.fetch(tokenEndpoint, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
68
|
+
body: body.toString(),
|
|
69
|
+
redirect: 'error',
|
|
70
|
+
});
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
const errorBody = (await response.json().catch(() => ({ error: 'unknown' })));
|
|
73
|
+
if (errorBody.error === 'invalid_grant') {
|
|
74
|
+
await credentialStore.delete(serviceKey, credential_store_ts_1.TARGET_ACCOUNT);
|
|
75
|
+
throw new LoginRequiredError(targetName);
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`Token refresh failed (${response.status}): ${errorBody.error}`);
|
|
78
|
+
}
|
|
79
|
+
tokenResponse = (await response.json());
|
|
80
|
+
try {
|
|
81
|
+
await credentialStore.set(serviceKey, credential_store_ts_1.TARGET_ACCOUNT, tokenResponse.refresh_token);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
await bestEffortRevoke(tokenResponse.refresh_token, revocationEndpoint, clientId, http);
|
|
85
|
+
await credentialStore.delete(serviceKey, credential_store_ts_1.TARGET_ACCOUNT);
|
|
86
|
+
throw new LoginRequiredError(targetName);
|
|
87
|
+
}
|
|
88
|
+
return { accessToken: tokenResponse.access_token, expiresIn: tokenResponse.expires_in };
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
await release();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function bestEffortRevoke(token, revocationEndpoint, clientId, http) {
|
|
95
|
+
if (!revocationEndpoint)
|
|
96
|
+
return;
|
|
97
|
+
try {
|
|
98
|
+
const body = new URLSearchParams({
|
|
99
|
+
token,
|
|
100
|
+
client_id: clientId,
|
|
101
|
+
token_type_hint: 'refresh_token',
|
|
102
|
+
});
|
|
103
|
+
await http.fetch(revocationEndpoint, {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
106
|
+
body: body.toString(),
|
|
107
|
+
redirect: 'error',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Best-effort
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function getAccessTokenProvider(targetName, target, credentialStore, acquireLock, deps, clock = { now: () => Date.now() }) {
|
|
115
|
+
let cachedToken = null;
|
|
116
|
+
let expiresAt = 0;
|
|
117
|
+
return {
|
|
118
|
+
async getAccessToken(_signal) {
|
|
119
|
+
if (cachedToken && clock.now() < expiresAt - 30_000) {
|
|
120
|
+
return cachedToken;
|
|
121
|
+
}
|
|
122
|
+
const result = await refreshAccessToken(targetName, target, credentialStore, acquireLock, deps);
|
|
123
|
+
cachedToken = result.accessToken;
|
|
124
|
+
expiresAt = clock.now() + result.expiresIn * 1000;
|
|
125
|
+
return result.accessToken;
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
async function revokeAndCleanup(target, credentialStore, acquireLock, deps, force) {
|
|
130
|
+
const { http, discoveryEndpoints } = deps;
|
|
131
|
+
const { revocationEndpoint, clientId } = discoveryEndpoints;
|
|
132
|
+
const serviceKey = (0, credential_store_ts_1.targetServiceKey)(target.id);
|
|
133
|
+
const release = await acquireLock();
|
|
134
|
+
try {
|
|
135
|
+
const refreshToken = await credentialStore.get(serviceKey, credential_store_ts_1.TARGET_ACCOUNT);
|
|
136
|
+
if (refreshToken && revocationEndpoint) {
|
|
137
|
+
const body = new URLSearchParams({
|
|
138
|
+
token: refreshToken,
|
|
139
|
+
client_id: clientId,
|
|
140
|
+
token_type_hint: 'refresh_token',
|
|
141
|
+
});
|
|
142
|
+
try {
|
|
143
|
+
const response = await http.fetch(revocationEndpoint, {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
146
|
+
body: body.toString(),
|
|
147
|
+
redirect: 'error',
|
|
148
|
+
});
|
|
149
|
+
if (!response.ok && !force) {
|
|
150
|
+
throw new Error(`Remote revocation failed (${response.status}). Use --force to remove anyway.`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
if (!force)
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
await credentialStore.delete(serviceKey, credential_store_ts_1.TARGET_ACCOUNT);
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
await release();
|
|
162
|
+
}
|
|
163
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@the-open-engine/zeroshot",
|
|
3
|
-
"version": "6.
|
|
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": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
]
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
|
-
"pretest": "npm run build:agent-cli-provider",
|
|
32
|
+
"pretest": "npm run build:agent-cli-provider && npm run build:target",
|
|
33
33
|
"test": "node tests/run-tests.js",
|
|
34
34
|
"test:unit": "node tests/run-tests.js",
|
|
35
35
|
"test:e2e": "mocha 'tests/e2e/**/*.test.js' --timeout 120000",
|
|
@@ -40,14 +40,17 @@
|
|
|
40
40
|
"test:coverage:report": "c8 --reporter=html npm run test:unit && echo 'Coverage report generated at coverage/index.html'",
|
|
41
41
|
"postinstall": "node scripts/fix-node-pty-permissions.js && node scripts/check-path.js",
|
|
42
42
|
"start": "node cli/index.js",
|
|
43
|
-
"typecheck": "tsc --noEmit && npm run typecheck:cluster && npm run typecheck:hosted-target && npm run typecheck:hosted-session",
|
|
43
|
+
"typecheck": "tsc --noEmit && npm run typecheck:cluster && npm run typecheck:hosted-target && npm run typecheck:hosted-session && npm run typecheck:target",
|
|
44
44
|
"typecheck:agent-cli-provider": "tsc --project tsconfig.agent-cli-provider.json",
|
|
45
45
|
"typecheck:hosted-target": "tsc --project tsconfig.hosted-target.json",
|
|
46
46
|
"typecheck:hosted-session": "tsc --project tsconfig.hosted-session.json",
|
|
47
47
|
"test:hosted-target": "node --test tests/hosted-target/*.test.ts",
|
|
48
|
+
"test:target": "npm run build:agent-cli-provider && npm run build:target && node --test tests/target/*.test.ts",
|
|
49
|
+
"typecheck:target": "tsc --project tsconfig.target.json",
|
|
48
50
|
"typecheck:cluster": "tsc --project tsconfig.cluster.json",
|
|
49
51
|
"lint:agent-cli-provider": "eslint \"src/agent-cli-provider/**/*.ts\" \"tests/agent-cli-provider/**/*.ts\"",
|
|
50
52
|
"build:agent-cli-provider": "tsc --project tsconfig.agent-cli-provider.build.json",
|
|
53
|
+
"build:target": "tsc --project tsconfig.target.build.json",
|
|
51
54
|
"build:cluster": "npm run protocol:generate && tsc --project tsconfig.cluster.cjs.json && tsc --project tsconfig.cluster.esm.json && tsc --project tsconfig.hosted-session.cjs.json && tsc --project tsconfig.hosted-session.esm.json && node scripts/build-cluster.js",
|
|
52
55
|
"test:agent-cli-provider": "node --test tests/agent-cli-provider/*.test.js",
|
|
53
56
|
"test:omp": "npm run build:agent-cli-provider && node --test tests/agent-cli-provider/omp-*.test.js && node tests/run-tests.js tests/omp-rpc-watcher.test.js tests/omp-isolation-capability.test.js tests/omp-docker-auth.test.js tests/omp-docker-fresh-only.test.js",
|
|
@@ -55,8 +58,17 @@
|
|
|
55
58
|
"check:agent-cli-provider": "npm run check:agent-cli-provider:ci",
|
|
56
59
|
"check:agent-cli-provider:ci": "npm run typecheck:agent-cli-provider && npm run lint:agent-cli-provider && npm run build:agent-cli-provider && npm run test:agent-cli-provider",
|
|
57
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",
|
|
58
|
-
"test:cluster-package": "npm run build:agent-cli-provider && npm run build:cluster && node --test tests/cluster/package.test.js",
|
|
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",
|
|
59
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",
|
|
60
72
|
"lint": "eslint .",
|
|
61
73
|
"lint:fix": "eslint . --fix",
|
|
62
74
|
"validate:templates": "node scripts/validate-templates.js",
|
|
@@ -77,9 +89,9 @@
|
|
|
77
89
|
"release:preflight": "node scripts/release-preflight.js",
|
|
78
90
|
"release:recover": "node scripts/release-recovery.js",
|
|
79
91
|
"release:assert-published": "node scripts/assert-release-published.js",
|
|
80
|
-
"prepack": "npm run build:cluster",
|
|
92
|
+
"prepack": "npm run build:cluster && npm run build:target",
|
|
81
93
|
"prepublishOnly": "npm run lint && npm run typecheck && npm run check:agent-cli-provider:ci",
|
|
82
|
-
"prepare": "npm run build:agent-cli-provider && husky"
|
|
94
|
+
"prepare": "npm run build:agent-cli-provider && npm run build:target && husky"
|
|
83
95
|
},
|
|
84
96
|
"c8": {
|
|
85
97
|
"reporter": [
|
|
@@ -119,7 +131,7 @@
|
|
|
119
131
|
"url": "https://github.com/the-open-engine/zeroshot/issues"
|
|
120
132
|
},
|
|
121
133
|
"engines": {
|
|
122
|
-
"node": ">=
|
|
134
|
+
"node": ">=22.0.0"
|
|
123
135
|
},
|
|
124
136
|
"publishConfig": {
|
|
125
137
|
"access": "public",
|
|
@@ -145,6 +157,9 @@
|
|
|
145
157
|
"files": [
|
|
146
158
|
"src/",
|
|
147
159
|
"lib/",
|
|
160
|
+
"!src/target/register-hosted-commands.ts",
|
|
161
|
+
"!lib/target/register-hosted-commands.js",
|
|
162
|
+
"!lib/target/register-hosted-commands.d.ts",
|
|
148
163
|
"bin/",
|
|
149
164
|
"cli/",
|
|
150
165
|
"task-lib/",
|
|
@@ -166,10 +181,12 @@
|
|
|
166
181
|
"commander": "^14.0.2",
|
|
167
182
|
"node-pty": "^1.1.0",
|
|
168
183
|
"omelette": "^0.4.17",
|
|
184
|
+
"open": "^10.1.0",
|
|
169
185
|
"pidusage": "^4.0.1",
|
|
170
186
|
"proper-lockfile": "^4.1.2"
|
|
171
187
|
},
|
|
172
188
|
"optionalDependencies": {
|
|
189
|
+
"@napi-rs/keyring": "^1.1.6",
|
|
173
190
|
"ws": "^8.18.3"
|
|
174
191
|
},
|
|
175
192
|
"devDependencies": {
|
|
@@ -191,6 +208,7 @@
|
|
|
191
208
|
"jscpd": "^3.5.10",
|
|
192
209
|
"lint-staged": "^16.2.7",
|
|
193
210
|
"mocha": "^11.7.5",
|
|
211
|
+
"opcore": "0.2.1",
|
|
194
212
|
"prettier": "^3.7.4",
|
|
195
213
|
"semantic-release": "^25.0.2",
|
|
196
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
|
+
});
|