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.
- package/README.md +45 -4
- package/dist/src/adapters/claude-code/adapter.js +157 -98
- package/dist/src/adapters/claude-code/completion-report.js +2 -64
- package/dist/src/adapters/claude-code/prompt-compiler.js +1 -60
- package/dist/src/adapters/claude-code/runtime-options.js +72 -0
- package/dist/src/adapters/codex/adapter.js +170 -0
- package/dist/src/adapters/codex/app-server-events.js +84 -0
- package/dist/src/adapters/codex/compatibility.js +45 -0
- package/dist/src/adapters/codex/config-builder.js +40 -0
- package/dist/src/adapters/codex/host-admission.js +3 -0
- package/dist/src/adapters/codex/inspection-rpc.js +48 -0
- package/dist/src/adapters/codex/job-rpc.js +189 -0
- package/dist/src/adapters/codex/native-policy.js +112 -0
- package/dist/src/adapters/codex/native-proof.js +14 -0
- package/dist/src/adapters/codex/native-session.js +375 -0
- package/dist/src/adapters/codex/plugin.js +26 -0
- package/dist/src/adapters/codex/pre-model-gate.js +112 -0
- package/dist/src/adapters/codex/prompt-compiler.js +13 -0
- package/dist/src/adapters/codex/runtime-options.js +163 -0
- package/dist/src/adapters/codex/scratch.js +28 -0
- package/dist/src/adapters/codex/stdio-rpc.js +113 -0
- package/dist/src/adapters/codex/stream-parser.js +85 -0
- package/dist/src/adapters/codex/supported-admission.js +97 -0
- package/dist/src/adapters/codex/tool-dispatcher.js +20 -0
- package/dist/src/adapters/completion-report.js +64 -0
- package/dist/src/adapters/discovery.js +76 -0
- package/dist/src/adapters/prompt-compiler.js +60 -0
- package/dist/src/adapters/registry.js +10 -5
- package/dist/src/cli.js +50 -58
- package/dist/src/commands/doctor.js +7 -1
- package/dist/src/commands/setup.js +3 -0
- package/dist/src/daemon/execution-coordinator.js +19 -2
- package/dist/src/daemon/runtime.js +34 -41
- package/dist/src/process/node-process-group-host.js +74 -0
- package/dist/src/safety/cli-host-admission.js +74 -0
- package/dist/src/safety/cli-security-modes.js +4 -0
- package/dist/src/safety/codex-production-host.js +267 -0
- package/dist/src/safety/codex-supported-releases.js +1 -0
- package/dist/src/safety/hook-cli.js +21 -7
- package/dist/src/safety/hook-policy-builder.js +4 -11
- package/dist/src/safety/job-security-policy.js +62 -0
- package/dist/src/safety/job-security-receipt.js +20 -0
- package/dist/src/safety/job-security-session.js +133 -0
- package/dist/src/safety/job-tool-dispatcher.js +52 -0
- package/dist/src/safety/job-tool-policy.js +61 -0
- package/dist/src/safety/job-tool-session.js +82 -0
- package/dist/src/safety/local-tool-policy.js +101 -0
- package/dist/src/safety/local-tool-server.js +174 -0
- package/dist/src/safety/local-tool-worker.js +57 -0
- package/dist/src/safety/pre-tool-use-hook.js +3 -39
- package/dist/src/safety/registered-command-integrity.js +52 -0
- package/dist/src/safety/security-requirements.js +38 -0
- package/dist/src/safety/subprocess-sandbox.js +62 -0
- package/package.json +1 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { COMPLETION_REPORT_NOTICE } from "./completion-report.js";
|
|
2
|
+
const STOP_BEFORE_PUBLICATION_RULE = "Stop before publication. Never run git commit, git push, create a pull request, merge, " +
|
|
3
|
+
"deploy, access production, send an external message, or write data outside this worktree.";
|
|
4
|
+
const EVENT_SCHEMA_NOTICE = "Report progress only through the normalized event lifecycle (started, progress, file_changed, question, " +
|
|
5
|
+
"test_started, test_result, budget_warning, checkpoint_ready, blocked, completed, failed). " +
|
|
6
|
+
"Checkpoint your work with checkpoint_ready before stopping whenever it is safe to do so.";
|
|
7
|
+
function requiredArray(value, label) {
|
|
8
|
+
if (!Array.isArray(value)) {
|
|
9
|
+
throw new Error(`task package is missing ${label}`);
|
|
10
|
+
}
|
|
11
|
+
return value.map((item) => String(item));
|
|
12
|
+
}
|
|
13
|
+
function record(value) {
|
|
14
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
15
|
+
? value
|
|
16
|
+
: undefined;
|
|
17
|
+
}
|
|
18
|
+
export function compilePrompt(taskPackage, options) {
|
|
19
|
+
const contract = record(taskPackage.contract);
|
|
20
|
+
if (!contract) {
|
|
21
|
+
throw new Error("task package is missing a contract");
|
|
22
|
+
}
|
|
23
|
+
const agentExecution = record(taskPackage.agent_execution);
|
|
24
|
+
if (!agentExecution) {
|
|
25
|
+
throw new Error("task package is missing the implementation brief (agent_execution section)");
|
|
26
|
+
}
|
|
27
|
+
const brief = record(agentExecution.implementation_brief);
|
|
28
|
+
if (!brief) {
|
|
29
|
+
throw new Error("task package is missing the implementation brief");
|
|
30
|
+
}
|
|
31
|
+
const unresolvedQuestions = requiredArray(brief.unresolved_questions ?? [], "unresolved_questions");
|
|
32
|
+
if (unresolvedQuestions.length > 0) {
|
|
33
|
+
throw new Error(`task package has unresolved questions that must be resolved before dispatch: ${unresolvedQuestions.join("; ")}`);
|
|
34
|
+
}
|
|
35
|
+
const contractObjective = record(contract.objective);
|
|
36
|
+
const objective = String(brief.objective ?? contractObjective?.summary ?? "");
|
|
37
|
+
if (!objective) {
|
|
38
|
+
throw new Error("task package is missing an objective");
|
|
39
|
+
}
|
|
40
|
+
const acceptanceCriteria = requiredArray(brief.acceptance_criteria ?? contractObjective?.acceptance_criteria ?? [], "acceptance_criteria");
|
|
41
|
+
const prohibitedActions = requiredArray(agentExecution.prohibited_actions ?? [], "prohibited_actions");
|
|
42
|
+
const requiredChanges = requiredArray(brief.required_changes ?? [], "required_changes");
|
|
43
|
+
const invariants = requiredArray(brief.invariants ?? [], "invariants");
|
|
44
|
+
const nonGoals = requiredArray(brief.non_goals ?? [], "non_goals");
|
|
45
|
+
const requiredTests = requiredArray(brief.required_tests ?? [], "required_tests");
|
|
46
|
+
const sections = [
|
|
47
|
+
`# Bounded implementation brief: ${options.workItemLabel}`,
|
|
48
|
+
`## Objective\n${objective}`,
|
|
49
|
+
`## Required changes\n${requiredChanges.map((item) => `- ${item}`).join("\n")}`,
|
|
50
|
+
`## Invariants\n${invariants.map((item) => `- ${item}`).join("\n")}`,
|
|
51
|
+
`## Non-goals\n${nonGoals.map((item) => `- ${item}`).join("\n")}`,
|
|
52
|
+
`## Acceptance criteria\n${acceptanceCriteria.map((item) => `- ${item}`).join("\n")}`,
|
|
53
|
+
`## Required tests\n${requiredTests.map((item) => `- ${item}`).join("\n")}`,
|
|
54
|
+
`## Prohibited actions\n${prohibitedActions.map((item) => `- ${item}`).join("\n")}`,
|
|
55
|
+
`## Stop-before-publication rule\n${STOP_BEFORE_PUBLICATION_RULE}`,
|
|
56
|
+
`## Event and checkpoint obligations\n${EVENT_SCHEMA_NOTICE}`,
|
|
57
|
+
`## Final completion report\n${COMPLETION_REPORT_NOTICE}`,
|
|
58
|
+
];
|
|
59
|
+
return sections.join("\n\n");
|
|
60
|
+
}
|
|
@@ -9,11 +9,16 @@ export class AdapterRegistry {
|
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
async probeAll() {
|
|
12
|
-
return Promise.all([...this.plugins.values()].map(async (plugin) =>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
return Promise.all([...this.plugins.values()].map(async (plugin) => {
|
|
13
|
+
let probe;
|
|
14
|
+
try {
|
|
15
|
+
probe = await plugin.probe();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
probe = { available: false, authenticated: false, capabilities: [], reason: "Adapter discovery failed; check the local executable and login" };
|
|
19
|
+
}
|
|
20
|
+
return { id: plugin.id, version: plugin.version, ...probe };
|
|
21
|
+
}));
|
|
17
22
|
}
|
|
18
23
|
async select(id, version, input) {
|
|
19
24
|
const plugin = this.plugins.get(id);
|
package/dist/src/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { loadConfig, redact, isAuthConfig, isExecutionConfig, isServiceConfig }
|
|
|
6
6
|
import { McpGatewayClient } from "./gateway-client.js";
|
|
7
7
|
import { Runner } from "./runner.js";
|
|
8
8
|
import { FileRunnerStateStore } from "./state-store.js";
|
|
9
|
-
import {
|
|
9
|
+
import { createClaudeCodeAdapterOptions } from "./adapters/claude-code/runtime-options.js";
|
|
10
10
|
import { ClaudeCodeAdapter } from "./adapters/claude-code/adapter.js";
|
|
11
11
|
import { verifyCheckout } from "./safety/checkout-guard.js";
|
|
12
12
|
import { buildNodeCheckoutInspectorDeps, realNodeCheckoutInspectorDeps } from "./safety/node-checkout-inspector.js";
|
|
@@ -15,15 +15,12 @@ import { FileOAuthCredentialStore } from "./auth/credential-store.js";
|
|
|
15
15
|
import { OAuthClient } from "./auth/oauth-client.js";
|
|
16
16
|
import { LoopbackLogin } from "./auth/loopback-login.js";
|
|
17
17
|
import { OAuthTokenProvider, LoginRequiredError } from "./auth/token-provider.js";
|
|
18
|
-
import { McpRelay } from "./auth/mcp-relay.js";
|
|
19
|
-
import { McpRelayConfig } from "./auth/mcp-relay-config.js";
|
|
20
18
|
import { createBrowserOpener } from "./auth/browser-opener.js";
|
|
21
19
|
import { FileLocalConfigStore } from "./registration/local-config.js";
|
|
22
20
|
import { LaunchdService } from "./platform/macos/launchd.js";
|
|
23
21
|
import { buildDaemon, runnerDisplayName } from "./daemon/runtime.js";
|
|
24
22
|
import { createHash } from "node:crypto";
|
|
25
|
-
import {
|
|
26
|
-
import { promisify } from "node:util";
|
|
23
|
+
import { discoverEmployeeAdapters, hasUsableAdapter, adapterDiagnosticsWithRemedies } from "./adapters/discovery.js";
|
|
27
24
|
import { mkdir, rm } from "node:fs/promises";
|
|
28
25
|
export async function cleanupOnceResources(configPath, closeRelay, unlinkFile = unlinkSync) {
|
|
29
26
|
const errors = [];
|
|
@@ -53,45 +50,17 @@ function buildCheckoutPreflight() {
|
|
|
53
50
|
};
|
|
54
51
|
}
|
|
55
52
|
async function buildOnceAdapter(config, env, tokenProvider, fetchFn) {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
const options = createClaudeCodeAdapterOptions({
|
|
54
|
+
worktreePath: config.checkoutPath, jobId: config.workLoopId, stateRoot: config.stateRoot, env,
|
|
55
|
+
binaryPath: config.claudeBinaryPath, hookExecutablePath: config.hookExecutablePath,
|
|
56
|
+
maxBudgetUsd: Number.MAX_SAFE_INTEGER, workItemLabel: config.workLoopId,
|
|
60
57
|
});
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
worktreeRoot: config.checkoutPath,
|
|
68
|
-
maxBudgetUsd: Number.MAX_SAFE_INTEGER,
|
|
69
|
-
workItemLabel: config.workLoopId,
|
|
70
|
-
env,
|
|
71
|
-
registeredReadTools: config.registeredReadTools,
|
|
72
|
-
writeMcpConfig: (cfg) => configs.write(cfg),
|
|
73
|
-
mcpServerName: "ai_gateway",
|
|
74
|
-
binaryPath: config.claudeBinaryPath,
|
|
75
|
-
hookExecutablePath: config.hookExecutablePath,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
catch (err) {
|
|
79
|
-
// Close relay if adapter construction fails
|
|
80
|
-
try {
|
|
81
|
-
await relay.close();
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
// Suppress relay close error; propagate adapter error
|
|
85
|
-
}
|
|
86
|
-
throw err;
|
|
87
|
-
}
|
|
88
|
-
// Cleanup function: always attempts both config unlink and relay close
|
|
89
|
-
const cleanup = async () => cleanupOnceResources(null, async () => { try {
|
|
90
|
-
configs.clear();
|
|
91
|
-
}
|
|
92
|
-
finally {
|
|
93
|
-
await relay.close();
|
|
94
|
-
} });
|
|
58
|
+
// Do not create a Gateway binding through the legacy once path while the
|
|
59
|
+
// shared native release evidence is absent. Injected test adapters are separate.
|
|
60
|
+
if (!options.securityPolicy)
|
|
61
|
+
throw new Error('CLI_SECURITY_ADMISSION_UNAVAILABLE: verified CLI isolation is required');
|
|
62
|
+
const adapter = new ClaudeCodeAdapter(options);
|
|
63
|
+
const cleanup = async () => adapter.cancel();
|
|
95
64
|
return { adapter, cleanup };
|
|
96
65
|
}
|
|
97
66
|
async function handleLogin(config, stdout, stderr, fetchFn = fetch, opener) {
|
|
@@ -168,6 +137,11 @@ export async function main(argv, env, io = {}) {
|
|
|
168
137
|
if (config.mode === "setup") {
|
|
169
138
|
if (process.platform !== "darwin")
|
|
170
139
|
throw new Error("Local runner daemon setup requires macOS");
|
|
140
|
+
const detected = await (io.discoverAdapters?.() ?? discoverEmployeeAdapters(env));
|
|
141
|
+
if (!hasUsableAdapter(detected)) {
|
|
142
|
+
stdout(JSON.stringify({ ok: false, configured: false, adapters: adapterDiagnosticsWithRemedies(detected), action: "Resolve adapter diagnostics, then rerun setup." }));
|
|
143
|
+
throw new Error("No usable authenticated runtime adapter is available");
|
|
144
|
+
}
|
|
171
145
|
const loginCode = await handleLogin(config, stdout, stderr, fetchFn, io.browserOpener);
|
|
172
146
|
if (loginCode !== 0)
|
|
173
147
|
return loginCode;
|
|
@@ -176,21 +150,29 @@ export async function main(argv, env, io = {}) {
|
|
|
176
150
|
const oauthClient = new OAuthClient({ resource: config.gatewayMcpUrl, fetchFn });
|
|
177
151
|
const tokenProvider = new OAuthTokenProvider({ store: credentialStore, oauthClient, resource: config.gatewayMcpUrl, clock: () => Date.now() });
|
|
178
152
|
const gateway = new McpGatewayClient({ mcpUrl: config.gatewayMcpUrl, tokenProvider, runnerId: "setup", fetchFn });
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
153
|
+
const adapters = await (io.discoverAdapters?.() ?? discoverEmployeeAdapters(env));
|
|
154
|
+
if (!hasUsableAdapter(adapters)) {
|
|
155
|
+
stdout(JSON.stringify({ ok: false, configured: false, adapters: adapterDiagnosticsWithRemedies(adapters), action: "Resolve adapter diagnostics, then rerun setup." }));
|
|
156
|
+
throw new Error("No usable authenticated runtime adapter is available. Run doctor for adapter diagnostics.");
|
|
157
|
+
}
|
|
183
158
|
const registered = await gateway.registerLocalRunner({
|
|
184
159
|
deviceFingerprint: createHash("sha256").update(`${process.getuid?.() ?? 0}|${runnerDisplayName()}|${config.gatewayMcpUrl}`).digest("hex"),
|
|
185
|
-
displayName: runnerDisplayName(), packageVersion: "0.1.
|
|
186
|
-
capabilities:
|
|
160
|
+
displayName: runnerDisplayName(), packageVersion: "0.1.9",
|
|
161
|
+
capabilities: adapters.map(probe => ({ adapter: probe.id, version: probe.version, capabilities: probe.capabilities, authenticated: probe.available && probe.authenticated })),
|
|
187
162
|
});
|
|
188
163
|
await localStore.save({ schemaVersion: 1, gatewayMcpUrl: config.gatewayMcpUrl, runnerId: registered.runner.id, workspaceRoot: config.workspaceRoot, concurrency: 1 });
|
|
189
164
|
await launchd.install(process.argv[1], config.stateRoot);
|
|
190
|
-
stdout(JSON.stringify({ configured: true, runnerId: registered.runner.id,
|
|
191
|
-
return
|
|
165
|
+
stdout(JSON.stringify({ configured: true, runnerId: registered.runner.id, adapters }));
|
|
166
|
+
return 0;
|
|
192
167
|
}
|
|
168
|
+
const doctorAdapters = config.mode === "doctor" ? adapterDiagnosticsWithRemedies(await (io.discoverAdapters?.() ?? discoverEmployeeAdapters(env))) : undefined;
|
|
193
169
|
const local = await localStore.load();
|
|
170
|
+
if (!local && config.mode === "doctor") {
|
|
171
|
+
stdout(JSON.stringify({ ok: false, configured: false, adapters: doctorAdapters,
|
|
172
|
+
checks: { oauth: false, gateway: false, runner_registration: false, workspace: false, launchd: false },
|
|
173
|
+
action: "Resolve adapter diagnostics, then run setup to register this runner." }));
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
194
176
|
if (!local)
|
|
195
177
|
throw new Error("Local runner is not configured. Run npx mtech-ai-local-runner setup.");
|
|
196
178
|
if (config.mode === "start") {
|
|
@@ -214,13 +196,23 @@ export async function main(argv, env, io = {}) {
|
|
|
214
196
|
const oauthClient = new OAuthClient({ resource: local.gatewayMcpUrl, fetchFn });
|
|
215
197
|
const tokenProvider = new OAuthTokenProvider({ store: credentialStore, oauthClient, resource: local.gatewayMcpUrl, tokenOverride: env.AI_GATEWAY_LOCAL_RUNNER_TOKEN, clock: () => Date.now() });
|
|
216
198
|
if (config.mode === "doctor") {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
199
|
+
const adapters = doctorAdapters;
|
|
200
|
+
const localChecks = Promise.allSettled([
|
|
201
|
+
launchd.status(),
|
|
202
|
+
mkdir(local.workspaceRoot, { recursive: true, mode: 0o700 }),
|
|
203
|
+
]);
|
|
204
|
+
// Refresh rotating credentials once before the Gateway client asks for a token.
|
|
205
|
+
const [oauth] = await Promise.allSettled([tokenProvider.getToken()]);
|
|
206
|
+
const [gateway] = oauth.status === "fulfilled"
|
|
207
|
+
? await Promise.allSettled([new McpGatewayClient({ mcpUrl: local.gatewayMcpUrl, tokenProvider, runnerId: local.runnerId, fetchFn }).localRunnerStatus()])
|
|
208
|
+
: [{ status: "rejected", reason: "Authentication is unavailable" }];
|
|
209
|
+
const [service, workspace] = await localChecks;
|
|
210
|
+
const checks = [oauth, gateway, service, workspace];
|
|
211
|
+
const loaded = service.status === "fulfilled" && service.value.loaded;
|
|
212
|
+
const ok = checks.every(check => check.status === "fulfilled") && loaded && hasUsableAdapter(adapters);
|
|
213
|
+
stdout(JSON.stringify({ ok, configured: true, adapters, checks: { runner_registration: true, oauth: oauth.status === "fulfilled", gateway: gateway.status === "fulfilled", workspace: workspace.status === "fulfilled", launchd: loaded },
|
|
214
|
+
...(gateway.status === "fulfilled" ? { runner: gateway.value } : {}), ...(service.status === "fulfilled" ? { launchd: service.value } : {}) }));
|
|
215
|
+
return ok ? 0 : 1;
|
|
224
216
|
}
|
|
225
217
|
const runtime = await buildDaemon({ gatewayMcpUrl: local.gatewayMcpUrl, runnerId: local.runnerId, workspaceRoot: local.workspaceRoot, stateRoot: config.stateRoot, tokenProvider, fetchFn, env });
|
|
226
218
|
try {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasUsableAdapter } from "../adapters/discovery.js";
|
|
1
2
|
export class Doctor {
|
|
2
3
|
deps;
|
|
3
4
|
constructor(deps) {
|
|
@@ -24,6 +25,11 @@ export class Doctor {
|
|
|
24
25
|
checks.push({ key, ok: false, detail: error instanceof Error ? error.message : String(error) });
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
+
const adapters = await this.deps.adapterDiagnostics?.();
|
|
29
|
+
if (adapters) {
|
|
30
|
+
const check = checks.find(check => check.key === "adapters");
|
|
31
|
+
check.ok = hasUsableAdapter(adapters);
|
|
32
|
+
}
|
|
33
|
+
return { ok: checks.every((check) => check.ok), checks, ...(adapters ? { adapters } : {}) };
|
|
28
34
|
}
|
|
29
35
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasUsableAdapter } from "../adapters/discovery.js";
|
|
1
2
|
export class SetupCommand {
|
|
2
3
|
deps;
|
|
3
4
|
constructor(deps) {
|
|
@@ -8,6 +9,8 @@ export class SetupCommand {
|
|
|
8
9
|
throw new Error("Local runner daemon setup requires macOS");
|
|
9
10
|
await this.deps.login();
|
|
10
11
|
const adapters = await this.deps.probeAdapters();
|
|
12
|
+
if (!hasUsableAdapter(adapters))
|
|
13
|
+
throw new Error("No usable authenticated runtime adapter is available");
|
|
11
14
|
const registration = await this.deps.register({ workspaceRoot: input.workspaceRoot, adapters });
|
|
12
15
|
await this.deps.installService();
|
|
13
16
|
const doctor = await this.deps.doctor();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { requireSecurityRequirements } from '../safety/security-requirements.js';
|
|
1
2
|
import { GatewayToolError } from "../gateway-client.js";
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import { PROTOCOL_VERSION, isTerminalEventType, validateEvent, MAX_EVENT_SUMMARY_CHARS } from "../protocol.js";
|
|
@@ -29,6 +30,14 @@ function text(value, label) {
|
|
|
29
30
|
throw new Error(`task package is missing ${label}`);
|
|
30
31
|
return value;
|
|
31
32
|
}
|
|
33
|
+
function immutable(value) {
|
|
34
|
+
if (value && typeof value === "object") {
|
|
35
|
+
for (const child of Object.values(value))
|
|
36
|
+
immutable(child);
|
|
37
|
+
Object.freeze(value);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
32
41
|
class PreparationDeadlineError extends Error {
|
|
33
42
|
}
|
|
34
43
|
export class ExecutionCoordinator {
|
|
@@ -37,6 +46,7 @@ export class ExecutionCoordinator {
|
|
|
37
46
|
this.deps = deps;
|
|
38
47
|
}
|
|
39
48
|
async execute(job) {
|
|
49
|
+
job = { ...job, taskPackage: immutable(structuredClone(job.taskPackage)) };
|
|
40
50
|
let stage = "package";
|
|
41
51
|
let workspace;
|
|
42
52
|
let adapter;
|
|
@@ -63,6 +73,10 @@ export class ExecutionCoordinator {
|
|
|
63
73
|
const adapterId = text(pkg.runtime_adapter, "runtime adapter");
|
|
64
74
|
const adapterVersion = text(pkg.runtime_adapter_version, "runtime adapter version");
|
|
65
75
|
const expectedContractFingerprint = text(pkg.contract_fingerprint, "contract fingerprint");
|
|
76
|
+
requireSecurityRequirements(pkg.security_requirements, { adapterId, adapterVersion });
|
|
77
|
+
if (!/^[a-f0-9]{64}$/.test(job.launchFingerprint ?? '') || job.gatewayContractFingerprint !== expectedContractFingerprint
|
|
78
|
+
|| job.gatewayTaskPackageFingerprint !== fingerprint(pkg))
|
|
79
|
+
throw new Error("Claimed launch fingerprints differ");
|
|
66
80
|
stage = "workspace";
|
|
67
81
|
workspace = await step(() => this.deps.workspaces.create({
|
|
68
82
|
jobId: job.id,
|
|
@@ -177,7 +191,10 @@ export class ExecutionCoordinator {
|
|
|
177
191
|
await this.deps.state.save({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, phase: "running", bindingId: binding.bindingId, eventCursor: sequence, ...(isTerminalEventType(event.eventType) ? { terminalEventType: event.eventType } : {}), workspaceManifest: workspace.manifest });
|
|
178
192
|
};
|
|
179
193
|
try {
|
|
180
|
-
|
|
194
|
+
if (!job.taskPackage.resolved_model_id
|
|
195
|
+
|| binding.resolvedModelId !== job.taskPackage.resolved_model_id)
|
|
196
|
+
throw new Error("Binding model differs from approved launch");
|
|
197
|
+
await adapter.start({ securityBinding: { launchFingerprint: job.launchFingerprint, contractFingerprint: job.gatewayContractFingerprint, taskPackageFingerprint: job.gatewayTaskPackageFingerprint }, generation: job.generation, approvedTaskPackage: job.taskPackage, bindingId: binding.bindingId, sessionId: binding.bindingId, taskPackage: binding.taskPackage, resolvedModelId: binding.resolvedModelId });
|
|
181
198
|
await readControls();
|
|
182
199
|
if (adapter.events && !requestedTerminal) {
|
|
183
200
|
for await (const event of adapter.events()) {
|
|
@@ -230,7 +247,7 @@ export class ExecutionCoordinator {
|
|
|
230
247
|
clearInterval(heartbeatTimer);
|
|
231
248
|
}
|
|
232
249
|
const status = terminal.eventType === "completed" ? "completed" : terminal.eventType === "cancelled" ? "cancelled" : terminal.eventType === "blocked" || terminal.eventType === "scope_conflict" ? "blocked" : "failed";
|
|
233
|
-
await this.deps.gateway.complete({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, status, result: { binding_id: binding.bindingId, event_type: terminal.eventType, summary: terminal.summary } });
|
|
250
|
+
await this.deps.gateway.complete({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, status, result: { binding_id: binding.bindingId, event_type: terminal.eventType, summary: terminal.summary, ...(adapter.securityReceipt?.() ? { security_receipt: adapter.securityReceipt() } : {}) } });
|
|
234
251
|
await this.deps.state.clear();
|
|
235
252
|
}
|
|
236
253
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { promisify } from "node:util";
|
|
1
|
+
import { createCodexProductionHost } from '../safety/codex-production-host.js';
|
|
3
2
|
import { hostname } from "node:os";
|
|
4
3
|
import { createHash } from "node:crypto";
|
|
5
4
|
import { join } from "node:path";
|
|
6
5
|
import { McpGatewayClient } from "../gateway-client.js";
|
|
6
|
+
import { discoverAdapters } from "../adapters/discovery.js";
|
|
7
|
+
import { CodexPlugin } from "../adapters/codex/plugin.js";
|
|
8
|
+
import { buildCodexJobOptions } from "../adapters/codex/runtime-options.js";
|
|
7
9
|
import { AdapterRegistry } from "../adapters/registry.js";
|
|
8
10
|
import { ClaudeCodePlugin } from "../adapters/claude-code/adapter.js";
|
|
9
|
-
import {
|
|
11
|
+
import { createClaudeCodeAdapterOptions } from "../adapters/claude-code/runtime-options.js";
|
|
10
12
|
import { WorkspaceManager } from "../workspace/workspace-manager.js";
|
|
11
13
|
import { GitClient } from "../workspace/git-client.js";
|
|
12
14
|
import { FileDaemonStateStore } from "../state-store.js";
|
|
@@ -15,35 +17,26 @@ import { JobPoller } from "./job-poller.js";
|
|
|
15
17
|
import { ProcessLock } from "./process-lock.js";
|
|
16
18
|
import { Daemon } from "./daemon.js";
|
|
17
19
|
import { buildTerminalEvidenceCollector } from "../safety/terminal-evidence.js";
|
|
18
|
-
import { McpRelay } from "../auth/mcp-relay.js";
|
|
19
|
-
import { McpRelayConfig } from "../auth/mcp-relay-config.js";
|
|
20
|
-
const execFileAsync = promisify(execFile);
|
|
21
20
|
export async function buildDaemon(input) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
21
|
+
if (input.codexSupportedProbe && !input.codexSecurityHost)
|
|
22
|
+
throw new Error('CODEX_SUPPORTED_HOST_REQUIRED');
|
|
23
|
+
const production = !input.codexSecurityHost && !input.codexSupportedProbe ? createCodexProductionHost({ runnerId: input.runnerId, binary: input.env.AI_GATEWAY_LOCAL_RUNNER_CODEX_BINARY ?? 'codex', home: input.env.HOME ?? '', codexHome: input.env.CODEX_HOME, searchPath: input.env.PATH }) : undefined;
|
|
24
|
+
let pendingCodexClaim;
|
|
25
25
|
const gateway = new McpGatewayClient({ mcpUrl: input.gatewayMcpUrl, tokenProvider: input.tokenProvider, runnerId: input.runnerId, fetchFn: input.fetchFn });
|
|
26
26
|
const hookPath = new URL("../safety/hook-cli.js", import.meta.url).pathname;
|
|
27
27
|
const plugin = new ClaudeCodePlugin({
|
|
28
|
-
probe: async () =>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return { available: true, authenticated: auth, executableVersion: version.stdout.trim() };
|
|
33
|
-
}
|
|
34
|
-
catch (error) {
|
|
35
|
-
return { available: false, authenticated: false, reason: error instanceof Error ? error.message : String(error) };
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
options: ({ worktreePath, jobId }) => ({
|
|
39
|
-
processHost: new NodeProcessHost(), worktreeRoot: worktreePath, maxBudgetUsd: Number.MAX_SAFE_INTEGER,
|
|
40
|
-
workItemLabel: jobId, env: input.env, registeredReadTools: [], hookExecutablePath: hookPath,
|
|
28
|
+
probe: async () => (await discoverAdapters(input.env, undefined, ['claude_code_cli']))[0],
|
|
29
|
+
options: ({ worktreePath, jobId }) => createClaudeCodeAdapterOptions({
|
|
30
|
+
worktreePath, jobId, stateRoot: input.stateRoot, env: input.env,
|
|
31
|
+
maxBudgetUsd: Number.MAX_SAFE_INTEGER, hookExecutablePath: hookPath,
|
|
41
32
|
binaryPath: input.env.AI_GATEWAY_LOCAL_RUNNER_CLAUDE_BINARY,
|
|
42
|
-
mcpServerName: "ai_gateway",
|
|
43
|
-
writeMcpConfig: (config) => configs.write(config),
|
|
44
33
|
}),
|
|
45
34
|
});
|
|
46
|
-
const
|
|
35
|
+
const codex = new CodexPlugin({
|
|
36
|
+
probe: async () => production ? production.probe() : input.codexSupportedProbe ? input.codexSupportedProbe() : (await discoverAdapters(input.env, undefined, ['codex_cli']))[0],
|
|
37
|
+
options: ({ worktreePath, jobId }) => buildCodexJobOptions({ worktreePath, jobId, stateRoot: input.stateRoot, env: input.env, ...(production ? { productionHostFactory: () => production.job(jobId, worktreePath) } : { securityHost: input.codexSecurityHost }) }),
|
|
38
|
+
});
|
|
39
|
+
const registry = new AdapterRegistry([plugin, codex]);
|
|
47
40
|
const probes = await registry.probeAll();
|
|
48
41
|
const versions = Object.fromEntries(probes.filter((probe) => probe.available && probe.authenticated).map((probe) => [probe.id, probe.version]));
|
|
49
42
|
const state = new FileDaemonStateStore(input.stateRoot);
|
|
@@ -64,9 +57,16 @@ export async function buildDaemon(input) {
|
|
|
64
57
|
heartbeat: async (bindingId, job) => {
|
|
65
58
|
await Promise.all([gateway.heartbeat(bindingId), gateway.heartbeatLocalJob({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken })]);
|
|
66
59
|
},
|
|
67
|
-
complete: (result) => gateway.completeLocalJob(result),
|
|
60
|
+
complete: async (result) => { await gateway.completeLocalJob(result); production?.releaseJob(result.jobId); pendingCodexClaim = undefined; },
|
|
68
61
|
},
|
|
69
|
-
registry,
|
|
62
|
+
registry: { select: async (id, version, options) => {
|
|
63
|
+
if (production && id === 'codex_cli') {
|
|
64
|
+
if (!pendingCodexClaim || pendingCodexClaim.job?.id !== options.jobId)
|
|
65
|
+
throw new Error('CODEX_TRUSTED_CLAIM_REQUIRED');
|
|
66
|
+
production.registerClaim(pendingCodexClaim);
|
|
67
|
+
}
|
|
68
|
+
return registry.select(id, version, options);
|
|
69
|
+
} },
|
|
70
70
|
workspaces: new WorkspaceManager({ root: input.workspaceRoot, git: new GitClient() }),
|
|
71
71
|
state,
|
|
72
72
|
});
|
|
@@ -74,7 +74,7 @@ export async function buildDaemon(input) {
|
|
|
74
74
|
reportError: (diagnostic) => { process.stderr.write(`${JSON.stringify({ event: "poller_failed", ...diagnostic })}\n`); },
|
|
75
75
|
heartbeat: () => gateway.heartbeatLocalRunner({
|
|
76
76
|
deviceFingerprint: createHash("sha256").update(`${process.getuid?.() ?? 0}|${runnerDisplayName()}|${input.gatewayMcpUrl}`).digest("hex"),
|
|
77
|
-
displayName: runnerDisplayName(), packageVersion: "0.1.
|
|
77
|
+
displayName: runnerDisplayName(), packageVersion: "0.1.9",
|
|
78
78
|
capabilities: probes.map((probe) => ({ adapter: probe.id, version: probe.version,
|
|
79
79
|
capabilities: probe.capabilities, authenticated: probe.available && probe.authenticated })),
|
|
80
80
|
}).then(() => undefined),
|
|
@@ -82,22 +82,19 @@ export async function buildDaemon(input) {
|
|
|
82
82
|
const claim = await gateway.waitForLocalJob(versions, 25);
|
|
83
83
|
if (claim.status !== "claimed" || !claim.job || !claim.task_package || !claim.lease_token || !claim.launch_grant)
|
|
84
84
|
return { status: "no_job" };
|
|
85
|
+
if (production && claim.task_package.runtime_adapter === 'codex_cli')
|
|
86
|
+
pendingCodexClaim = claim;
|
|
85
87
|
const job = {
|
|
86
88
|
id: claim.job.id, generation: claim.job.generation, leaseToken: claim.lease_token, launchGrant: claim.launch_grant,
|
|
87
89
|
workLoopId: claim.job.work_loop_id, taskPackage: claim.task_package,
|
|
88
|
-
gatewayContractFingerprint: claim.contract_fingerprint,
|
|
90
|
+
launchFingerprint: claim.job.launch_fingerprint, gatewayContractFingerprint: claim.contract_fingerprint,
|
|
89
91
|
gatewayTaskPackageFingerprint: claim.task_package_fingerprint,
|
|
90
92
|
};
|
|
91
93
|
await state.save({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, phase: "claimed" });
|
|
92
94
|
return { status: "claimed", job };
|
|
93
95
|
},
|
|
94
96
|
execute: async (job) => {
|
|
95
|
-
|
|
96
|
-
await coordinator.execute(job);
|
|
97
|
-
}
|
|
98
|
-
finally {
|
|
99
|
-
configs.clear();
|
|
100
|
-
}
|
|
97
|
+
await coordinator.execute(job);
|
|
101
98
|
},
|
|
102
99
|
delay: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
103
100
|
});
|
|
@@ -129,12 +126,8 @@ export async function buildDaemon(input) {
|
|
|
129
126
|
},
|
|
130
127
|
poller,
|
|
131
128
|
});
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
finally {
|
|
136
|
-
await relay.close();
|
|
137
|
-
} } };
|
|
129
|
+
// Model tools have no Gateway relay. Each adapter owns and closes its job endpoint.
|
|
130
|
+
return { daemon, close: async () => { poller.stop(); production?.close(); } };
|
|
138
131
|
}
|
|
139
132
|
export function runnerDisplayName() { return `${hostname()} local AI`; }
|
|
140
133
|
/** Unknown mutation outcomes halt polling; known failures replay only their terminal receipt. */
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
/** Each invocation owns one new process group; no name-based process sweeping. */
|
|
3
|
+
export class NodeProcessGroupHost {
|
|
4
|
+
spawn(command, args, options) {
|
|
5
|
+
if (process.platform !== "darwin")
|
|
6
|
+
throw new Error("Codex process isolation requires macOS");
|
|
7
|
+
const child = spawn(command, args, { ...options, shell: false, detached: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
8
|
+
let failure;
|
|
9
|
+
let owned = true;
|
|
10
|
+
const kill = (signal = "SIGKILL") => {
|
|
11
|
+
if (!owned || !child.pid)
|
|
12
|
+
return;
|
|
13
|
+
try {
|
|
14
|
+
process.kill(-child.pid, signal);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
const code = error.code;
|
|
18
|
+
if (code === "EPERM" && child.exitCode === null && child.signalCode === null) {
|
|
19
|
+
// macOS can expose the root PID after stdout EOF while its process
|
|
20
|
+
// group is already exiting. Signal only this ChildProcess and retry
|
|
21
|
+
// group cleanup from its exit callback after it has been reaped.
|
|
22
|
+
child.kill(signal);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (code !== "ESRCH")
|
|
26
|
+
throw new Error("Provider process group cleanup could not be confirmed");
|
|
27
|
+
}
|
|
28
|
+
if (signal === "SIGKILL")
|
|
29
|
+
owned = false;
|
|
30
|
+
};
|
|
31
|
+
child.on("error", () => { failure = new Error("Provider process could not start"); });
|
|
32
|
+
child.stdin.on("error", () => { failure = new Error("Provider process input closed"); });
|
|
33
|
+
// Provider stderr can contain account details. Consume without publishing it.
|
|
34
|
+
child.stderr.resume();
|
|
35
|
+
child.once("exit", () => {
|
|
36
|
+
try {
|
|
37
|
+
kill();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
failure = new Error("Provider process group cleanup could not be confirmed");
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return {
|
|
44
|
+
stdin: { write: (text) => { child.stdin.write(text); }, end: () => { child.stdin.end(); } },
|
|
45
|
+
stdout: { async *[Symbol.asyncIterator]() {
|
|
46
|
+
let pending = Buffer.alloc(0);
|
|
47
|
+
try {
|
|
48
|
+
for await (const chunk of child.stdout) {
|
|
49
|
+
pending = Buffer.concat([pending, Buffer.from(chunk)]);
|
|
50
|
+
let end;
|
|
51
|
+
while ((end = pending.indexOf(10)) !== -1) {
|
|
52
|
+
if (end > 256_000)
|
|
53
|
+
throw new Error("Provider event exceeded the allowed size");
|
|
54
|
+
const line = pending.subarray(0, end).toString("utf8").replace(/\r$/, "");
|
|
55
|
+
pending = pending.subarray(end + 1);
|
|
56
|
+
yield line;
|
|
57
|
+
}
|
|
58
|
+
if (pending.length > 256_000)
|
|
59
|
+
throw new Error("Provider event exceeded the allowed size");
|
|
60
|
+
}
|
|
61
|
+
if (pending.length)
|
|
62
|
+
yield pending.toString("utf8");
|
|
63
|
+
if (failure)
|
|
64
|
+
throw failure;
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
kill();
|
|
68
|
+
}
|
|
69
|
+
} },
|
|
70
|
+
kill: (signal) => kill((signal ?? "SIGKILL")),
|
|
71
|
+
once: (_event, callback) => { child.once("exit", callback); },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { open, lstat, stat, access } from 'node:fs/promises';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
// This digest identifies historical offline evidence only. It grants no capability.
|
|
6
|
+
const OFFLINE_OBSERVED_BINARY = '0d5ac7aae20c4eda6242428d5e218c55ba96d8763fda2a271088ff37104b9d28';
|
|
7
|
+
/** A byte measurement, not a spawn certificate. No candidate executable is run.
|
|
8
|
+
* Reject nonregular/symlink/mutable inputs. Check both descriptor and pathname;
|
|
9
|
+
* these checks do not purport to make a later path-based spawn atomic. */
|
|
10
|
+
async function measure(file) {
|
|
11
|
+
const fd = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
12
|
+
try {
|
|
13
|
+
const before = await fd.stat({ bigint: true });
|
|
14
|
+
if (!before.isFile() || !(before.mode & 73n) || before.size > 512n * 1024n * 1024n)
|
|
15
|
+
throw Error('binary');
|
|
16
|
+
const hash = createHash('sha256');
|
|
17
|
+
let bytes = 0n;
|
|
18
|
+
for await (const chunk of fd.createReadStream({ autoClose: false })) {
|
|
19
|
+
bytes += BigInt(chunk.length);
|
|
20
|
+
if (bytes > 512n * 1024n * 1024n)
|
|
21
|
+
throw Error('binary');
|
|
22
|
+
hash.update(chunk);
|
|
23
|
+
}
|
|
24
|
+
const after = await fd.stat({ bigint: true }), named = await lstat(file, { bigint: true });
|
|
25
|
+
for (const stat of [after, named])
|
|
26
|
+
if (!stat.isFile() || stat.dev !== before.dev || stat.ino !== before.ino
|
|
27
|
+
|| stat.size !== before.size || stat.mtimeNs !== before.mtimeNs || stat.ctimeNs !== before.ctimeNs)
|
|
28
|
+
throw Error('binary');
|
|
29
|
+
if (bytes !== before.size)
|
|
30
|
+
throw Error('binary');
|
|
31
|
+
return hash.digest('hex');
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
await fd.close();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Reports independent prerequisites together, never converting a version/hash,
|
|
38
|
+
* fixture result, environment flag or task-package claim into release admission.
|
|
39
|
+
* No reviewed native policy-binding/outer-isolation verifier exists yet. */
|
|
40
|
+
export async function diagnoseCliHostAdmission(adapterId, binary, env) {
|
|
41
|
+
const checks = [];
|
|
42
|
+
let digest;
|
|
43
|
+
const lookup = (env.PATH ?? '').split(path.delimiter);
|
|
44
|
+
const candidates = path.isAbsolute(binary) ? [binary] : /^[A-Za-z0-9_-]+$/.test(binary) && lookup.every(part => path.isAbsolute(part))
|
|
45
|
+
? lookup.map(part => path.join(part, binary)) : [];
|
|
46
|
+
for (const candidate of candidates) {
|
|
47
|
+
try {
|
|
48
|
+
await access(candidate, constants.X_OK);
|
|
49
|
+
if (!(await stat(candidate)).isFile())
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
// The first executable is what exec lookup selects. Never hash a substitute.
|
|
56
|
+
try {
|
|
57
|
+
digest = await measure(candidate);
|
|
58
|
+
}
|
|
59
|
+
catch { /* Fixed diagnostic only. */ }
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
checks.push(digest ? { id: 'binary_identity', status: 'observed', code: adapterId === 'codex_cli' && digest === OFFLINE_OBSERVED_BINARY ? 'BINARY_OFFLINE_OBSERVED_ONLY' : 'BINARY_NOT_OFFLINE_REVIEWED' }
|
|
63
|
+
: { id: 'binary_identity', status: 'blocked', code: 'BINARY_IDENTITY_UNAVAILABLE' });
|
|
64
|
+
for (const [id, code] of [
|
|
65
|
+
['policy_binding', 'NATIVE_POLICY_BINDING_UNAVAILABLE'],
|
|
66
|
+
['native_isolation', 'NATIVE_ISOLATION_UNVERIFIED'],
|
|
67
|
+
['managed_sources', 'MANAGED_SOURCE_COVERAGE_UNVERIFIED'],
|
|
68
|
+
['provider_network', 'PROVIDER_NETWORK_BOUNDARY_UNVERIFIED'],
|
|
69
|
+
['release_admission', 'RELEASE_ADMISSION_UNAVAILABLE'],
|
|
70
|
+
])
|
|
71
|
+
checks.push({ id, status: 'blocked', code });
|
|
72
|
+
return { status: 'unsupported', available: false, executable: false, capabilities: [],
|
|
73
|
+
...(digest ? { observedBinarySha256: digest } : {}), checks, reasonCodes: checks.map(check => check.code) };
|
|
74
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Explicitly narrower authority; never presented as native isolation. */
|
|
2
|
+
export const SUPPORTED_CLI_MODE = 'supported_cli_policy_v1';
|
|
3
|
+
export const SUPPORTED_CLI_GATES = Object.freeze(['binary_identity', 'supported_permissions', 'managed_policy_compatibility', 'restricted_mcp', 'rpc_anti_reattachment', 'login_preservation', 'owned_lifecycle', 'gateway_launch_binding']);
|
|
4
|
+
export const SUPPORTED_CLI_RISKS = Object.freeze(['vendor_native_runtime', 'shared_account_authentication', 'extensions_and_hosted_services', 'ipc_network_outside_command_sandbox']);
|