mtech-ai-local-runner 0.1.8 → 0.1.9

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 (54) hide show
  1. package/README.md +45 -4
  2. package/dist/src/adapters/claude-code/adapter.js +157 -98
  3. package/dist/src/adapters/claude-code/completion-report.js +2 -64
  4. package/dist/src/adapters/claude-code/prompt-compiler.js +1 -60
  5. package/dist/src/adapters/claude-code/runtime-options.js +72 -0
  6. package/dist/src/adapters/codex/adapter.js +170 -0
  7. package/dist/src/adapters/codex/app-server-events.js +84 -0
  8. package/dist/src/adapters/codex/compatibility.js +45 -0
  9. package/dist/src/adapters/codex/config-builder.js +40 -0
  10. package/dist/src/adapters/codex/host-admission.js +3 -0
  11. package/dist/src/adapters/codex/inspection-rpc.js +48 -0
  12. package/dist/src/adapters/codex/job-rpc.js +189 -0
  13. package/dist/src/adapters/codex/native-policy.js +112 -0
  14. package/dist/src/adapters/codex/native-proof.js +14 -0
  15. package/dist/src/adapters/codex/native-session.js +375 -0
  16. package/dist/src/adapters/codex/plugin.js +26 -0
  17. package/dist/src/adapters/codex/pre-model-gate.js +112 -0
  18. package/dist/src/adapters/codex/prompt-compiler.js +13 -0
  19. package/dist/src/adapters/codex/runtime-options.js +163 -0
  20. package/dist/src/adapters/codex/scratch.js +28 -0
  21. package/dist/src/adapters/codex/stdio-rpc.js +113 -0
  22. package/dist/src/adapters/codex/stream-parser.js +85 -0
  23. package/dist/src/adapters/codex/supported-admission.js +97 -0
  24. package/dist/src/adapters/codex/tool-dispatcher.js +20 -0
  25. package/dist/src/adapters/completion-report.js +64 -0
  26. package/dist/src/adapters/discovery.js +76 -0
  27. package/dist/src/adapters/prompt-compiler.js +60 -0
  28. package/dist/src/adapters/registry.js +10 -5
  29. package/dist/src/cli.js +50 -58
  30. package/dist/src/commands/doctor.js +7 -1
  31. package/dist/src/commands/setup.js +3 -0
  32. package/dist/src/daemon/execution-coordinator.js +19 -2
  33. package/dist/src/daemon/runtime.js +34 -41
  34. package/dist/src/process/node-process-group-host.js +74 -0
  35. package/dist/src/safety/cli-host-admission.js +74 -0
  36. package/dist/src/safety/cli-security-modes.js +4 -0
  37. package/dist/src/safety/codex-production-host.js +267 -0
  38. package/dist/src/safety/codex-supported-releases.js +1 -0
  39. package/dist/src/safety/hook-cli.js +21 -7
  40. package/dist/src/safety/hook-policy-builder.js +4 -11
  41. package/dist/src/safety/job-security-policy.js +62 -0
  42. package/dist/src/safety/job-security-receipt.js +20 -0
  43. package/dist/src/safety/job-security-session.js +133 -0
  44. package/dist/src/safety/job-tool-dispatcher.js +52 -0
  45. package/dist/src/safety/job-tool-policy.js +61 -0
  46. package/dist/src/safety/job-tool-session.js +82 -0
  47. package/dist/src/safety/local-tool-policy.js +101 -0
  48. package/dist/src/safety/local-tool-server.js +174 -0
  49. package/dist/src/safety/local-tool-worker.js +57 -0
  50. package/dist/src/safety/pre-tool-use-hook.js +3 -39
  51. package/dist/src/safety/registered-command-integrity.js +52 -0
  52. package/dist/src/safety/security-requirements.js +38 -0
  53. package/dist/src/safety/subprocess-sandbox.js +62 -0
  54. package/package.json +1 -1
@@ -0,0 +1,267 @@
1
+ import path from 'node:path';
2
+ import { constants } from 'node:fs';
3
+ import { open, lstat } from 'node:fs/promises';
4
+ import { requireCodexNativeProof } from '../adapters/codex/native-proof.js';
5
+ import { admitSupportedCodexPolicy } from '../adapters/codex/supported-admission.js';
6
+ import { admitJobSecurity } from './job-security-policy.js';
7
+ import { commandRegistryFingerprint } from './job-security-session.js';
8
+ import { fingerprint } from '../daemon/execution-coordinator.js';
9
+ import { requireSecurityRequirements } from './security-requirements.js';
10
+ import { SUPPORTED_CLI_GATES } from './cli-security-modes.js';
11
+ import { verifyCommandIntegrity, measureCommandFile } from './registered-command-integrity.js';
12
+ import { SUPPORTED_CLI_RISKS } from './cli-security-modes.js';
13
+ import { CODEX_SUPPORTED_RELEASES } from './codex-supported-releases.js';
14
+ import { securityRequirementsFingerprint } from './security-requirements.js';
15
+ const reject = () => new Error('CODEX_PRODUCTION_HOST_REJECTED');
16
+ /** Read original file backend only. No CLI, Keychain, HTTP, refresh or persistence.
17
+ * This is local credential availability, not remote entitlement/validity proof. */
18
+ export async function readCodexCredentialStatus(accountRoot) {
19
+ const fd = await open(path.join(accountRoot, 'auth.json'), constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
20
+ try {
21
+ const stat = await fd.stat();
22
+ if (!stat.isFile() || stat.size > 1_048_576)
23
+ throw reject();
24
+ const chunks = [];
25
+ let size = 0;
26
+ for await (const chunk of fd.createReadStream({ autoClose: false })) {
27
+ size += chunk.length;
28
+ if (size > 1_048_576)
29
+ throw reject();
30
+ chunks.push(chunk);
31
+ }
32
+ const raw = Buffer.concat(chunks).toString('utf8');
33
+ const a = JSON.parse(raw), token = a?.tokens?.access_token;
34
+ if (a.auth_mode !== 'chatgpt' || typeof token !== 'string' || token.length > 32768)
35
+ throw reject();
36
+ const parts = token.split('.');
37
+ if (parts.length !== 3)
38
+ throw reject();
39
+ const exp = JSON.parse(Buffer.from(parts[1], 'base64url').toString()).exp;
40
+ if (!Number.isSafeInteger(exp) || exp <= 0)
41
+ throw reject();
42
+ return { backend: 'file', available: true, expiresAt: exp * 1000 };
43
+ }
44
+ catch {
45
+ throw reject();
46
+ }
47
+ finally {
48
+ await fd.close();
49
+ }
50
+ }
51
+ export const codexReleaseFingerprint = (release) => securityRequirementsFingerprint(release);
52
+ function registeredCommandValid(c) {
53
+ try {
54
+ const fields = ['id', 'executable', 'argv', 'integrity', ...(c.readPaths === undefined ? [] : ['readPaths'])];
55
+ if (JSON.stringify(Object.keys(c).sort()) !== JSON.stringify(fields.sort()) || typeof c.id !== 'string' || !c.id || c.id.length > 256 || /[\r\n\0]/.test(c.id)
56
+ || typeof c.executable !== 'string' || !path.isAbsolute(c.executable) || !Array.isArray(c.argv) || c.argv.some(a => typeof a !== 'string' || /[\r\n\0]/.test(a)))
57
+ return false;
58
+ const paths = c.readPaths ?? [], integrity = c.integrity;
59
+ return Array.isArray(paths) && new Set(paths).size === paths.length && paths.every(p => typeof p === 'string' && !!p && !path.isAbsolute(p) && !p.split(/[\\/]/).some(s => !s || s === '.' || s === '..') && !/[\0*?\[\]{}]/.test(p))
60
+ && !!integrity && JSON.stringify(Object.keys(integrity).sort()) === JSON.stringify(['executableSha256', 'readFiles'])
61
+ && /^[a-f0-9]{64}$/.test(integrity.executableSha256) && !!integrity.readFiles && !Array.isArray(integrity.readFiles)
62
+ && JSON.stringify(Object.keys(integrity.readFiles).sort()) === JSON.stringify([...paths].sort())
63
+ && Object.values(integrity.readFiles).every(s => typeof s === 'string' && /^[a-f0-9]{64}$/.test(s));
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }
69
+ function reviewed(value) {
70
+ try {
71
+ return JSON.stringify(Object.keys(value).sort()) === JSON.stringify(['version', 'adapterVersion', 'executableVersion', 'binarySha256', 'modelIds', 'decisionId', 'acceptedResidualRisks', 'commands'].sort())
72
+ && value.version === 'codex-supported-release-v1' && value.adapterVersion === '1' && /^[a-f0-9]{64}$/.test(value.binarySha256)
73
+ && typeof value.executableVersion === 'string' && !!value.executableVersion && !/[\r\n\0]/.test(value.executableVersion)
74
+ && /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(value.decisionId)
75
+ && Array.isArray(value.modelIds) && value.modelIds.length > 0 && new Set(value.modelIds).size === value.modelIds.length
76
+ && value.modelIds.every(v => typeof v === 'string' && v.length > 0 && v.length < 256 && !/[\r\n\0]/.test(v))
77
+ && JSON.stringify(value.acceptedResidualRisks) === JSON.stringify(SUPPORTED_CLI_RISKS) && Array.isArray(value.commands) && new Set(value.commands.map(c => c.id)).size === value.commands.length && value.commands.every(registeredCommandValid);
78
+ }
79
+ catch {
80
+ return false;
81
+ }
82
+ }
83
+ const capabilities = ['repository_read', 'repository_edit', 'shell_test', 'git_diff'];
84
+ export function createCodexProductionHost(input) {
85
+ input = { ...input };
86
+ const releases = structuredClone(input.releases ?? CODEX_SUPPORTED_RELEASES);
87
+ const claims = new Map();
88
+ const scopes = new Map();
89
+ const account = path.resolve(input.codexHome ?? path.join(input.home, '.codex'));
90
+ async function inspect() {
91
+ if (!path.isAbsolute(input.home) || !input.runnerId || !releases.length)
92
+ throw reject();
93
+ const paths = path.isAbsolute(input.binary) ? [input.binary] : input.binary === 'codex' && input.searchPath?.split(path.delimiter).every(p => path.isAbsolute(p)) ? input.searchPath.split(path.delimiter).map(p => path.join(p, 'codex')) : [];
94
+ for (const binary of paths) {
95
+ let stat;
96
+ try {
97
+ stat = await lstat(binary);
98
+ }
99
+ catch (e) {
100
+ if (e.code === 'ENOENT')
101
+ continue;
102
+ throw reject();
103
+ }
104
+ // Preserve PATH precedence: never skip a present unreviewed first executable.
105
+ if (!stat.isFile() || !(stat.mode & 0o111) || (stat.mode & 0o022))
106
+ throw reject();
107
+ const sha = await measureCommandFile(binary), matches = releases.filter(r => reviewed(r) && r.binarySha256 === sha);
108
+ if (matches.length !== 1)
109
+ throw reject();
110
+ const credential = await (input.readCredentialStatus ?? readCodexCredentialStatus)(account);
111
+ if (credential.backend !== 'file' || !credential.available || !Number.isFinite(credential.expiresAt) || credential.expiresAt <= Date.now())
112
+ throw reject();
113
+ return { binary, release: matches[0] };
114
+ }
115
+ throw reject();
116
+ }
117
+ return {
118
+ async probe() {
119
+ try {
120
+ const { release } = await inspect();
121
+ return { available: true, authenticated: true, executableVersion: release.executableVersion, capabilities: [...capabilities] };
122
+ }
123
+ catch {
124
+ return { available: false, authenticated: false, capabilities: [], reason: 'CODEX_REVIEWED_RELEASE_OR_LOCAL_CREDENTIALS_UNAVAILABLE' };
125
+ }
126
+ },
127
+ registerClaim(value) {
128
+ const claim = structuredClone(value), job = claim.job, pkg = claim.task_package, context = claim.security_context;
129
+ if (claim.status !== 'claimed' || !job || !pkg || !context || context.runner_id !== input.runnerId
130
+ || typeof context.actor_id !== 'string' || !context.actor_id || /[\r\n\0]/.test(context.actor_id)
131
+ || !job.id || !Number.isSafeInteger(job.generation) || job.generation < 1 || !['claimed', 'shadowing'].includes(job.status)
132
+ || !/^[a-f0-9]{64}$/.test(job.launch_fingerprint) || !/^[a-f0-9]{64}$/.test(claim.contract_fingerprint ?? '')
133
+ || pkg.contract_fingerprint !== claim.contract_fingerprint || fingerprint(pkg) !== claim.task_package_fingerprint
134
+ || pkg.runtime_adapter !== 'codex_cli' || pkg.runtime_adapter_version !== '1' || pkg.resolved_provider !== 'openai'
135
+ || claims.size > 0 || scopes.size > 0)
136
+ throw reject();
137
+ requireSecurityRequirements(pkg.security_requirements, { adapterId: 'codex_cli', adapterVersion: '1' });
138
+ claims.set(job.id, claim);
139
+ },
140
+ job(id, sourceRoot) {
141
+ const claim = claims.get(id);
142
+ if (!claim || !path.isAbsolute(sourceRoot) || scopes.has(id))
143
+ throw reject();
144
+ const pkg = claim.task_package, job = claim.job, context = claim.security_context;
145
+ let prepared = false, revoked = false, used = false;
146
+ let selected;
147
+ let policy, supported, native;
148
+ let request;
149
+ const revoke = () => { revoked = true; policy?.revoke(); supported?.policy.revoke(); native?.close(); };
150
+ const assertPrepared = async () => {
151
+ if (revoked || !prepared || !selected || !request)
152
+ throw reject();
153
+ const current = await inspect();
154
+ if (revoked || current.binary !== selected.binary || codexReleaseFingerprint(current.release) !== codexReleaseFingerprint(selected.release))
155
+ throw reject();
156
+ for (const c of selected.release.commands) {
157
+ const paths = c.readPaths ?? [];
158
+ if (paths.some(p => path.isAbsolute(p) || p.split(/[\\/]/).includes('..')))
159
+ throw reject();
160
+ await verifyCommandIntegrity(c.integrity, c.executable, paths, paths.map(p => path.join(sourceRoot, p)));
161
+ }
162
+ if (revoked)
163
+ throw reject();
164
+ };
165
+ const scope = {
166
+ async prepare(value) {
167
+ if (prepared || revoked)
168
+ throw reject();
169
+ prepared = true;
170
+ try {
171
+ const r = Object.freeze(structuredClone(value));
172
+ selected = await inspect();
173
+ const requirements = requireSecurityRequirements(r.requirements, { adapterId: 'codex_cli', adapterVersion: '1' });
174
+ if (revoked || requirements.security_mode !== 'supported_cli_policy_v1'
175
+ || requirements.release_manifest_sha256 !== codexReleaseFingerprint(selected.release)
176
+ || r.adapterId !== 'codex_cli' || r.adapterVersion !== '1' || r.jobId !== job.id || r.generation !== job.generation
177
+ || r.modelId !== pkg.resolved_model_id || !selected.release.modelIds.includes(r.modelId)
178
+ || r.sourceSha !== pkg.repository?.base_commit_sha
179
+ || fingerprint(r.requirements) !== fingerprint(pkg.security_requirements)
180
+ || r.launchFingerprint !== job.launch_fingerprint || r.contractFingerprint !== claim.contract_fingerprint
181
+ || r.taskPackageFingerprint !== claim.task_package_fingerprint)
182
+ throw reject();
183
+ request = r;
184
+ await assertPrepared();
185
+ }
186
+ catch {
187
+ revoke();
188
+ throw reject();
189
+ }
190
+ },
191
+ async assertPrepared() { try {
192
+ await assertPrepared();
193
+ }
194
+ catch {
195
+ revoke();
196
+ throw reject();
197
+ } },
198
+ commands() { if (revoked || !selected || !request)
199
+ throw reject(); return structuredClone(selected.release.commands); },
200
+ binaryPath() { if (revoked || !selected || !request)
201
+ throw reject(); return selected.binary; },
202
+ policy: () => policy,
203
+ binding() {
204
+ try {
205
+ if (revoked || !native || !policy)
206
+ return null;
207
+ const b = native.binding();
208
+ return b ? { jobId: job.id, generation: job.generation, ...b } : null;
209
+ }
210
+ catch {
211
+ return null;
212
+ }
213
+ },
214
+ async admit(proof) {
215
+ if (used || revoked)
216
+ throw reject();
217
+ used = true;
218
+ try {
219
+ await assertPrepared();
220
+ const n = requireCodexNativeProof(proof), r = request, release = selected.release;
221
+ if (n.binaryPath !== selected.binary || n.sourceRoot !== sourceRoot || n.home !== input.home || n.codexHome !== account
222
+ || n.expected.model !== r.modelId || n.expected.provider !== 'openai')
223
+ throw reject();
224
+ const pin = { adapterId: r.adapterId, adapterVersion: r.adapterVersion, jobId: r.jobId, generation: r.generation, modelId: r.modelId, sourceSha: r.sourceSha,
225
+ securityMode: 'supported_cli_policy_v1', requirementsSha256: r.requirements.fingerprint,
226
+ binarySha256: release.binarySha256, configSha256: n.expected.configFingerprint, managedPolicySha256: n.managedPolicySha256,
227
+ toolsSha256: n.expected.toolSchemaFingerprint, commandsSha256: commandRegistryFingerprint(release.commands),
228
+ osProfileSha256: fingerprint({ mode: 'supported_cli_policy_v1', config: n.expected.configFingerprint, sourceRoot }) };
229
+ const binding = { jobId: r.jobId, generation: r.generation, actorId: context.actor_id, runnerId: context.runner_id,
230
+ repositoryKey: String(pkg.repository.key), sourceSha: r.sourceSha,
231
+ launchFingerprint: r.launchFingerprint, contractFingerprint: r.contractFingerprint, taskPackageFingerprint: r.taskPackageFingerprint,
232
+ binarySha256: release.binarySha256, requirementsFingerprint: pin.requirementsSha256, managedPolicyFingerprint: n.managedPolicySha256,
233
+ commandsFingerprint: pin.commandsSha256, decisionId: release.decisionId };
234
+ // All checks originate in this host and the retained transport validators.
235
+ // No caller-supplied passed/check array is consumed.
236
+ policy = await admitJobSecurity(pin, async (frozen) => {
237
+ requireCodexNativeProof(proof);
238
+ if (revoked)
239
+ throw reject();
240
+ return { version: 2, pin: frozen, checks: SUPPORTED_CLI_GATES.map(gate => ({ gate, status: 'passed' })) };
241
+ });
242
+ supported = { binding, policy: await admitSupportedCodexPolicy({ ...n.expected, ...binding, mode: 'supported_cli_policy_v1' }, async (frozen) => {
243
+ requireCodexNativeProof(proof);
244
+ if (revoked)
245
+ throw reject();
246
+ return { version: 'codex-supported-policy-evidence-v1', mode: 'supported_cli_policy_v1', pin: frozen, acceptedResidualRisks: release.acceptedResidualRisks, checks: SUPPORTED_CLI_GATES.map(gate => ({ gate, status: 'passed' })) };
247
+ }) };
248
+ requireCodexNativeProof(proof);
249
+ if (revoked)
250
+ throw reject();
251
+ native = n;
252
+ return supported;
253
+ }
254
+ catch {
255
+ revoke();
256
+ throw reject();
257
+ }
258
+ }, revoke,
259
+ };
260
+ scopes.set(id, scope);
261
+ return scope;
262
+ },
263
+ close() { for (const scope of scopes.values())
264
+ scope.revoke(); scopes.clear(); claims.clear(); },
265
+ releaseJob(id) { scopes.get(id)?.revoke(); scopes.delete(id); claims.delete(id); },
266
+ };
267
+ }
@@ -0,0 +1 @@
1
+ export const CODEX_SUPPORTED_RELEASES = Object.freeze([]);
@@ -1,5 +1,6 @@
1
1
  import { evaluateHookEvent } from "./pre-tool-use-hook.js";
2
2
  import path from "node:path";
3
+ export const MAX_HOOK_INPUT_BYTES = 262144;
3
4
  export const HOOK_POLICY_ENV_VAR = "AI_GATEWAY_RUNNER_HOOK_POLICY_B64";
4
5
  function denyMalformed() {
5
6
  return {
@@ -23,7 +24,7 @@ function isStringArray(value) {
23
24
  return Array.isArray(value) && value.every((item) => typeof item === "string");
24
25
  }
25
26
  export function parsePolicy(encoded) {
26
- if (!encoded)
27
+ if (!encoded || encoded.length > MAX_HOOK_INPUT_BYTES)
27
28
  return null;
28
29
  try {
29
30
  if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(encoded)) {
@@ -55,9 +56,11 @@ export function parsePolicy(encoded) {
55
56
  }
56
57
  }
57
58
  export function parseHookInput(raw) {
59
+ if (Buffer.byteLength(raw, "utf8") > MAX_HOOK_INPUT_BYTES)
60
+ return null;
58
61
  try {
59
62
  const parsed = JSON.parse(raw);
60
- if (typeof parsed !== "object" || parsed === null)
63
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
61
64
  return null;
62
65
  return parsed;
63
66
  }
@@ -75,15 +78,26 @@ export function decideFromRaw(rawInput, policyEnvValue) {
75
78
  return evaluateHookEvent(input, policy);
76
79
  }
77
80
  export async function runHookCli(readStdin, env, writeStdout) {
78
- const rawInput = await readStdin();
79
- const decision = decideFromRaw(rawInput, env[HOOK_POLICY_ENV_VAR]);
80
- writeStdout(JSON.stringify(decision));
81
- return 0;
81
+ try {
82
+ const rawInput = await readStdin();
83
+ const decision = decideFromRaw(rawInput, env[HOOK_POLICY_ENV_VAR]);
84
+ writeStdout(JSON.stringify(decision));
85
+ return 0;
86
+ }
87
+ catch {
88
+ writeStdout(JSON.stringify(denyMalformed()));
89
+ return 2;
90
+ }
82
91
  }
83
92
  async function readAllStdin() {
84
93
  const chunks = [];
94
+ let size = 0;
85
95
  for await (const chunk of process.stdin) {
86
- chunks.push(Buffer.from(chunk));
96
+ const bytes = Buffer.from(chunk);
97
+ size += bytes.length;
98
+ if (size > MAX_HOOK_INPUT_BYTES)
99
+ throw new Error("HOOK_INPUT_TOO_LARGE");
100
+ chunks.push(bytes);
87
101
  }
88
102
  return Buffer.concat(chunks).toString("utf8");
89
103
  }
@@ -1,3 +1,4 @@
1
+ import { CLAUDE_LOCAL_MCP_TOOLS } from "./pre-tool-use-hook.js";
1
2
  const DEFAULT_BLOCKED_PATHS = [".git", ".env", ".aws", ".ssh"];
2
3
  function requiredTestsFromTaskPackage(taskPackage) {
3
4
  const pkg = taskPackage;
@@ -9,21 +10,13 @@ function requiredTestsFromTaskPackage(taskPackage) {
9
10
  }
10
11
  return requiredTests.map((item) => String(item));
11
12
  }
12
- function allowedGatewayToolsFromTaskPackage(taskPackage) {
13
- const pkg = taskPackage;
14
- const contract = pkg.contract;
15
- const policy = contract?.policy;
16
- const allowed = policy?.allowed_gateway_tools;
17
- return new Set(Array.isArray(allowed) ? allowed.filter((tool) => typeof tool === "string") : []);
18
- }
19
13
  /**
20
- * The hook policy is derived only from the canonical task package and the
21
- * runner's own generated paths — never from anything Claude writes.
14
+ * Only the four job-local MCP names may pass the hook. Source and command
15
+ * authorization is enforced by the scoped worker, never by native Claude tools.
22
16
  */
23
17
  export function buildHookPolicy(options) {
24
18
  const requiredCommands = requiredTestsFromTaskPackage(options.taskPackage);
25
- const allowedGatewayTools = allowedGatewayToolsFromTaskPackage(options.taskPackage);
26
- const allowedMcpTools = [...new Set(options.registeredReadTools.filter((tool) => allowedGatewayTools.has(tool)))];
19
+ const allowedMcpTools = [...CLAUDE_LOCAL_MCP_TOOLS];
27
20
  const blockedPaths = [...new Set([...DEFAULT_BLOCKED_PATHS, ...(options.additionalBlockedPaths ?? []), ...options.generatedPaths])];
28
21
  return {
29
22
  requiredCommands,
@@ -0,0 +1,62 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { SUPPORTED_CLI_MODE, SUPPORTED_CLI_GATES } from './cli-security-modes.js';
3
+ /** Mandatory for every CLI. New adapter IDs do not alter this gate set. */
4
+ export const JOB_SECURITY_GATES = Object.freeze(['binary_binding', 'native_tools', 'managed_policy', 'credential_boundary', 'source_command_scope', 'request_binding', 'provider_egress', 'owned_lifecycle']);
5
+ const issued = new WeakMap();
6
+ const rejected = () => new Error('JOB_SECURITY_REJECTED');
7
+ const keys = ['adapterId', 'adapterVersion', 'jobId', 'generation', 'modelId', 'sourceSha', 'binarySha256', 'configSha256', 'managedPolicySha256', 'toolsSha256', 'commandsSha256', 'osProfileSha256', 'requirementsSha256'];
8
+ const object = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
9
+ const exact = (v, names) => JSON.stringify(Object.keys(v).sort()) === JSON.stringify([...names].sort());
10
+ function validPin(value) {
11
+ if (!object(value) || !exact(value, value.securityMode === undefined ? keys : [...keys, 'securityMode'])
12
+ || value.securityMode !== undefined && (value.securityMode !== SUPPORTED_CLI_MODE || value.adapterId !== 'codex_cli') || !Number.isSafeInteger(value.generation) || Number(value.generation) < 1)
13
+ return false;
14
+ for (const name of keys.filter(k => k !== 'generation')) {
15
+ const v = value[name];
16
+ if (typeof v !== 'string' || !v || v.length > 256 || /[\r\n\0]/.test(v))
17
+ return false;
18
+ }
19
+ if (!/^[a-f0-9]{40}$/.test(String(value.sourceSha)))
20
+ return false;
21
+ return keys.filter(k => k.endsWith('Sha256')).every(k => /^[a-f0-9]{64}$/.test(String(value[k])));
22
+ }
23
+ export const jobSecurityPinKeys = (pin) => pin.securityMode ? [...keys, 'securityMode'] : [...keys];
24
+ const encoded = (pin) => JSON.stringify(Object.fromEntries(jobSecurityPinKeys(pin).map(key => [key, pin[key]])));
25
+ export async function admitJobSecurity(expected, verify) {
26
+ try {
27
+ if (!validPin(expected))
28
+ throw rejected();
29
+ const pin = Object.freeze(structuredClone(expected));
30
+ const receipt = await verify(pin);
31
+ const gates = pin.securityMode === SUPPORTED_CLI_MODE ? SUPPORTED_CLI_GATES : JOB_SECURITY_GATES;
32
+ if (!object(receipt) || !exact(receipt, ['version', 'pin', 'checks']) || receipt.version !== (pin.securityMode ? 2 : 1)
33
+ || !validPin(receipt.pin) || encoded(receipt.pin) !== encoded(pin) || !Array.isArray(receipt.checks)
34
+ || receipt.checks.length !== gates.length)
35
+ throw rejected();
36
+ const seen = new Set();
37
+ for (const check of receipt.checks) {
38
+ if (!object(check) || !exact(check, ['gate', 'status']) || check.status !== 'passed' || typeof check.gate !== 'string'
39
+ || !gates.includes(check.gate) || seen.has(check.gate))
40
+ throw rejected();
41
+ seen.add(check.gate);
42
+ }
43
+ let revoked = false;
44
+ const policy = Object.freeze({ pin, fingerprint: createHash('sha256').update(encoded(pin)).digest('hex'), revoke: () => { revoked = true; } });
45
+ issued.set(policy, () => !revoked);
46
+ return policy;
47
+ }
48
+ catch {
49
+ throw rejected();
50
+ }
51
+ }
52
+ export function requireJobSecurity(value, expected) {
53
+ if (!object(value) || !issued.get(value)?.())
54
+ throw rejected();
55
+ const pin = value.pin;
56
+ for (const key of ['adapterId', 'adapterVersion', 'jobId', 'modelId'])
57
+ if (pin[key] !== expected[key])
58
+ throw rejected();
59
+ for (const key of ['sourceSha', 'generation'])
60
+ if (expected[key] !== undefined && pin[key] !== expected[key])
61
+ throw rejected();
62
+ }
@@ -0,0 +1,20 @@
1
+ import { requireJobSecurity } from './job-security-policy.js';
2
+ import { requireSecurityRequirements } from './security-requirements.js';
3
+ /** Audit linkage only. Gateway must preserve employee_runner provenance; serialized
4
+ * JSON never mints host authority or native isolation certification. */
5
+ export function createJobSecurityReceipt(policy, input) {
6
+ try {
7
+ requireJobSecurity(policy, { adapterId: policy.pin.adapterId, adapterVersion: policy.pin.adapterVersion, jobId: input.jobId, generation: input.generation, modelId: input.modelId, sourceSha: input.sourceSha });
8
+ if (!Number.isSafeInteger(input.generation) || input.generation < 1 || [input.launchFingerprint, input.contractFingerprint, input.taskPackageFingerprint].some(v => !/^[a-f0-9]{64}$/.test(v)))
9
+ throw Error();
10
+ const r = requireSecurityRequirements(input.requirements, { adapterId: policy.pin.adapterId, adapterVersion: policy.pin.adapterVersion, requirementsSha256: policy.pin.requirementsSha256 });
11
+ if (policy.pin.securityMode !== r.security_mode)
12
+ throw Error();
13
+ const supported = !!policy.pin.securityMode;
14
+ return Object.freeze({ ...(supported ? { security_mode: r.security_mode, accepted_residual_risks: Object.freeze([...r.accepted_residual_risks]) } : {}), receipt_version: supported ? 'cli-job-security-receipt-v2' : 'cli-job-security-receipt-v1', evidence_origin: 'employee_runner', phase: 'pre_execution', job_id: input.jobId, generation: input.generation,
15
+ requirements_fingerprint: r.fingerprint, launch_fingerprint: input.launchFingerprint, contract_fingerprint: input.contractFingerprint, task_package_fingerprint: input.taskPackageFingerprint, policy_fingerprint: policy.fingerprint, pin: Object.freeze({ ...policy.pin }) });
16
+ }
17
+ catch {
18
+ throw new Error('JOB_SECURITY_RECEIPT_REJECTED');
19
+ }
20
+ }
@@ -0,0 +1,133 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { requireSupportedCodexPolicy } from '../adapters/codex/supported-admission.js';
3
+ import { admitJobSecurity, requireJobSecurity } from './job-security-policy.js';
4
+ import { requireSecurityRequirements } from './security-requirements.js';
5
+ const fail = () => new Error('JOB_SECURITY_SESSION_REJECTED');
6
+ function canonical(v) {
7
+ if (Array.isArray(v))
8
+ return v.map(canonical);
9
+ if (v && typeof v === 'object')
10
+ return Object.fromEntries(Object.keys(v).sort().map(k => [k, canonical(v[k])]));
11
+ return v;
12
+ }
13
+ export const commandRegistryFingerprint = (commands) => createHash('sha256').update(JSON.stringify(canonical(commands))).digest('hex');
14
+ function freeze(value) {
15
+ if (value && typeof value === 'object') {
16
+ for (const item of Object.values(value))
17
+ freeze(item);
18
+ Object.freeze(value);
19
+ }
20
+ return value;
21
+ }
22
+ /** One issuance per job attempt, revoked on cancellation including async races.
23
+ * This wires verified authority; it does not establish native isolation itself. */
24
+ export class JobSecuritySession {
25
+ host;
26
+ used = false;
27
+ revoked = false;
28
+ policy;
29
+ request;
30
+ supportedFactory;
31
+ supportedContext;
32
+ supportedUsed = false;
33
+ registry = [];
34
+ nativeBinding;
35
+ constructor(host) {
36
+ this.host = host;
37
+ }
38
+ async authorize(input) {
39
+ if (this.used || this.revoked)
40
+ throw fail();
41
+ this.used = true;
42
+ let issued;
43
+ try {
44
+ if (!this.host)
45
+ throw fail();
46
+ const request = freeze(structuredClone(input));
47
+ if (!Number.isSafeInteger(request.generation) || request.generation < 1 || !/^[a-f0-9]{40}$/.test(request.sourceSha)
48
+ || [request.adapterId, request.adapterVersion, request.jobId, request.modelId].some(v => typeof v !== 'string' || !v || v.length > 256 || /[\r\n\0]/.test(v)))
49
+ throw fail();
50
+ const requirements = requireSecurityRequirements(request.requirements, request);
51
+ if ([request.launchFingerprint, request.contractFingerprint, request.taskPackageFingerprint].some(s => !/^[a-f0-9]{64}$/.test(s)))
52
+ throw fail();
53
+ const plan = await this.host(request);
54
+ if (this.revoked)
55
+ throw fail();
56
+ // Snapshot authority before awaiting a verifier; neither commands nor pin may
57
+ // change underneath it. Keep only the host callback for live native identity.
58
+ const pin = freeze(structuredClone(plan.pin)), commands = freeze(structuredClone(plan.commandRegistry));
59
+ for (const key of ['adapterId', 'adapterVersion', 'jobId', 'generation', 'modelId', 'sourceSha'])
60
+ if (pin[key] !== request[key])
61
+ throw fail();
62
+ if (pin.requirementsSha256 !== requirements.fingerprint || !Array.isArray(commands)
63
+ || pin.commandsSha256 !== commandRegistryFingerprint(commands) || typeof plan.nativeBinding !== 'function')
64
+ throw fail();
65
+ if (pin.securityMode !== requirements.security_mode || pin.securityMode && typeof plan.prepareSupportedAdmission !== 'function')
66
+ throw fail();
67
+ const nativeBinding = plan.nativeBinding.bind(plan);
68
+ issued = await admitJobSecurity(pin, plan.verify.bind(plan));
69
+ if (this.revoked)
70
+ throw fail();
71
+ requireJobSecurity(issued, request);
72
+ this.policy = issued;
73
+ this.registry = commands;
74
+ this.nativeBinding = nativeBinding;
75
+ this.request = request;
76
+ this.supportedFactory = pin.securityMode ? plan.prepareSupportedAdmission.bind(plan) : undefined;
77
+ return issued;
78
+ }
79
+ catch {
80
+ issued?.revoke();
81
+ this.revoke();
82
+ throw fail();
83
+ }
84
+ }
85
+ async supportedAdmission(expected) {
86
+ let context;
87
+ try {
88
+ if (this.revoked || this.supportedUsed || !this.policy?.pin.securityMode || !this.request || !this.supportedFactory)
89
+ throw fail();
90
+ this.supportedUsed = true;
91
+ requireJobSecurity(this.policy, this.request);
92
+ const captured = freeze(structuredClone(expected));
93
+ context = await this.supportedFactory(captured);
94
+ // Snapshot callback data before validation; only the capability retains identity.
95
+ context = { policy: context.policy, binding: freeze(structuredClone(context.binding)) };
96
+ if (this.revoked)
97
+ throw fail();
98
+ const p = this.policy.pin, r = this.request, b = context.binding;
99
+ if (b.jobId !== p.jobId || b.generation !== p.generation || b.sourceSha !== p.sourceSha || b.binarySha256 !== p.binarySha256
100
+ || b.requirementsFingerprint !== p.requirementsSha256 || b.managedPolicyFingerprint !== p.managedPolicySha256 || b.commandsFingerprint !== p.commandsSha256
101
+ || b.launchFingerprint !== r.launchFingerprint || b.contractFingerprint !== r.contractFingerprint || b.taskPackageFingerprint !== r.taskPackageFingerprint
102
+ || captured.model !== p.modelId || captured.configFingerprint !== p.configSha256 || captured.toolSchemaFingerprint !== p.toolsSha256)
103
+ throw fail();
104
+ requireSupportedCodexPolicy(context.policy, captured, b);
105
+ requireJobSecurity(this.policy, r);
106
+ this.supportedContext = context;
107
+ return context;
108
+ }
109
+ catch {
110
+ context?.policy?.revoke();
111
+ this.revoke();
112
+ throw fail();
113
+ }
114
+ }
115
+ commands() {
116
+ return !this.revoked && this.policy ? structuredClone(this.registry) : [];
117
+ }
118
+ binding() {
119
+ try {
120
+ if (this.revoked || !this.policy)
121
+ return null;
122
+ requireJobSecurity(this.policy, this.policy.pin);
123
+ const b = this.nativeBinding?.(), p = this.policy.pin;
124
+ if (!b || b.jobId !== p.jobId || b.generation !== p.generation)
125
+ return null;
126
+ return { ...b };
127
+ }
128
+ catch {
129
+ return null;
130
+ }
131
+ }
132
+ revoke() { this.revoked = true; this.policy?.revoke(); this.supportedContext?.policy.revoke(); this.supportedFactory = undefined; this.request = undefined; this.registry = []; this.nativeBinding = undefined; }
133
+ }
@@ -0,0 +1,52 @@
1
+ import { localToolInventory } from './local-tool-server.js';
2
+ const rejected = () => new Error('JOB_TOOL_DISPATCH_REJECTED');
3
+ function record(value) { return !!value && typeof value === 'object' && !Array.isArray(value); }
4
+ const id = (value) => typeof value === 'string' && value.length > 0 && value.length <= 256 && !/[\r\n\0]/.test(value);
5
+ const keys = (value, expected) => JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...expected].sort());
6
+ /** Host-owned validation before the existing scoped policy. Not a capability
7
+ * certificate: no production wiring until native authority is actually proven.
8
+ * Host-assigned decimal request sequence is strictly increasing on its ordered
9
+ * channel. A high-water mark rejects replay without an execution-count limit
10
+ * or an ever-growing cache. This sequence is not a model-selected capability. */
11
+ export class JobToolDispatcher {
12
+ policy;
13
+ active;
14
+ #binding;
15
+ #revoked = false;
16
+ #lastRequest = 0n;
17
+ constructor(binding, policy, active) {
18
+ this.policy = policy;
19
+ this.active = active;
20
+ if (![binding.jobId, binding.threadId, binding.turnId].every(id) || !Number.isSafeInteger(binding.generation) || binding.generation < 1)
21
+ throw rejected();
22
+ this.#binding = Object.freeze({ ...binding });
23
+ }
24
+ revoke() { if (this.#revoked)
25
+ return; this.#revoked = true; this.policy.revoke?.(); }
26
+ async dispatch(input) {
27
+ try {
28
+ if (this.#revoked || !this.active() || !record(input)
29
+ || !keys(input, ['jobId', 'threadId', 'turnId', 'generation', 'requestId', 'tool', 'args']))
30
+ throw rejected();
31
+ const envelope = structuredClone(input), b = this.#binding;
32
+ if (envelope.jobId !== b.jobId || envelope.threadId !== b.threadId || envelope.turnId !== b.turnId || envelope.generation !== b.generation
33
+ || !id(envelope.requestId) || !/^[1-9][0-9]*$/.test(envelope.requestId) || BigInt(envelope.requestId) <= this.#lastRequest
34
+ || typeof envelope.tool !== 'string' || !record(envelope.args))
35
+ throw rejected();
36
+ const tool = localToolInventory().find(t => t.name === envelope.tool);
37
+ const properties = tool?.inputSchema.properties;
38
+ if (!record(properties) || !keys(envelope.args, Object.keys(properties))
39
+ || Object.values(envelope.args).some(value => typeof value !== 'string')
40
+ || Buffer.byteLength(JSON.stringify(envelope.args)) > 1024 * 1024)
41
+ throw rejected();
42
+ this.#lastRequest = BigInt(envelope.requestId);
43
+ const result = await this.policy.call(envelope.tool, envelope.args);
44
+ if (this.#revoked || !this.active())
45
+ throw rejected();
46
+ return result;
47
+ }
48
+ catch {
49
+ throw rejected();
50
+ }
51
+ }
52
+ }