@counterfact-labs/cli 0.1.2 → 0.2.3

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 (184) hide show
  1. package/README.md +178 -38
  2. package/bundle/client-release.json +7 -0
  3. package/bundle/sealed.json +908 -0
  4. package/dist/adapters/anthropic.js +151 -0
  5. package/dist/adapters/completion.js +112 -0
  6. package/dist/adapters/durable-write.js +34 -0
  7. package/dist/adapters/files.js +140 -0
  8. package/dist/adapters/folder-path.js +6 -0
  9. package/dist/adapters/git-ignore.js +49 -0
  10. package/dist/adapters/git.js +44 -0
  11. package/dist/adapters/ignored-files.js +36 -0
  12. package/dist/adapters/instructions.js +35 -0
  13. package/dist/adapters/models.js +51 -0
  14. package/dist/adapters/output-paths.js +92 -0
  15. package/dist/adapters/prompt-cache.js +28 -0
  16. package/dist/adapters/public-instructions.js +36 -0
  17. package/dist/adapters/quality.js +6 -0
  18. package/dist/adapters/relay-plan.js +15 -0
  19. package/dist/adapters/release.js +57 -0
  20. package/dist/adapters/report.js +29 -0
  21. package/dist/adapters/repository-files.js +199 -0
  22. package/dist/adapters/repository-root.js +120 -0
  23. package/dist/adapters/results.js +9 -0
  24. package/dist/adapters/settings.js +25 -0
  25. package/dist/adapters/snapshot.js +68 -0
  26. package/dist/analysis-core/analysis/analysis-flags.js +1 -0
  27. package/dist/analysis-core/analysis/code-agents/skipped-files.js +1 -0
  28. package/dist/analysis-core/analysis/finding-quality/config.js +1 -0
  29. package/dist/analysis-core/analysis/finding-quality/policy.js +1 -0
  30. package/dist/analysis-core/analysis/headlines/attachment.js +1 -0
  31. package/dist/analysis-core/analysis/headlines/input.js +1 -0
  32. package/dist/analysis-core/analysis/headlines/types.js +1 -0
  33. package/dist/analysis-core/analysis/schemas.js +1 -0
  34. package/dist/analysis-core/analysis/source-notes/types.js +1 -0
  35. package/dist/analysis-core/chunks/2PSNOSJ2.js +1 -0
  36. package/dist/analysis-core/chunks/3227KXHF.js +1 -0
  37. package/dist/analysis-core/chunks/326F4XNG.js +1 -0
  38. package/dist/analysis-core/chunks/3MTXB4MK.js +6 -0
  39. package/dist/analysis-core/chunks/4F6ZIHXU.js +1 -0
  40. package/dist/analysis-core/chunks/57M4OQJM.js +1 -0
  41. package/dist/analysis-core/chunks/6YCABS5T.js +1 -0
  42. package/dist/analysis-core/chunks/724WZMYW.js +2 -0
  43. package/dist/analysis-core/chunks/74FPPPIG.js +1 -0
  44. package/dist/analysis-core/chunks/7QDOHWSY.js +27 -0
  45. package/dist/analysis-core/chunks/BZNHHLNP.js +1 -0
  46. package/dist/analysis-core/chunks/DTMHXKP6.js +7 -0
  47. package/dist/analysis-core/chunks/E7TFIHC5.js +1 -0
  48. package/dist/analysis-core/chunks/EH3QQ3T4.js +1 -0
  49. package/dist/analysis-core/chunks/EQTIRIMU.js +37 -0
  50. package/dist/analysis-core/chunks/FSLWD2PE.js +1 -0
  51. package/dist/analysis-core/chunks/G322LST6.js +1 -0
  52. package/dist/analysis-core/chunks/HTLIRVP7.js +1 -0
  53. package/dist/analysis-core/chunks/IBCFGUHI.js +1 -0
  54. package/dist/analysis-core/chunks/IL2KRP3T.js +1 -0
  55. package/dist/analysis-core/chunks/J4AXGE5W.js +1 -0
  56. package/dist/analysis-core/chunks/JF4GYMOZ.js +1 -0
  57. package/dist/analysis-core/chunks/KEKTZDIK.js +1 -0
  58. package/dist/analysis-core/chunks/KU5DINLW.js +1 -0
  59. package/dist/analysis-core/chunks/M7MMAUAK.js +1 -0
  60. package/dist/analysis-core/chunks/OFBJNHC6.js +1 -0
  61. package/dist/analysis-core/chunks/ORCYIIHB.js +60 -0
  62. package/dist/analysis-core/chunks/PVPEY7NF.js +1 -0
  63. package/dist/analysis-core/chunks/PYKAERZZ.js +1 -0
  64. package/dist/analysis-core/chunks/QDYMR6KS.js +1 -0
  65. package/dist/analysis-core/chunks/QPXNXMLC.js +1 -0
  66. package/dist/analysis-core/chunks/SL27GMEN.js +1 -0
  67. package/dist/analysis-core/chunks/STZOC7LN.js +1 -0
  68. package/dist/analysis-core/chunks/T5TREZCQ.js +1 -0
  69. package/dist/analysis-core/chunks/TDNTU4TF.js +1 -0
  70. package/dist/analysis-core/chunks/U5HOEVPE.js +1 -0
  71. package/dist/analysis-core/chunks/UEWL3GMQ.js +1 -0
  72. package/dist/analysis-core/chunks/VER355VN.js +1 -0
  73. package/dist/analysis-core/chunks/VFENG4MR.js +1 -0
  74. package/dist/analysis-core/chunks/VGYWI4X2.js +1 -0
  75. package/dist/analysis-core/chunks/Y5AUKKYG.js +1 -0
  76. package/dist/analysis-core/chunks/Y7JGSUOA.js +1 -0
  77. package/dist/analysis-core/chunks/YVDCJN7D.js +14 -0
  78. package/dist/analysis-core/chunks/ZAA44GPI.js +1 -0
  79. package/dist/analysis-core/chunks/ZMUBMYWN.js +1 -0
  80. package/dist/analysis-core/contracts/requests.js +1 -0
  81. package/dist/analysis-core/conventions/file-admission.js +1 -0
  82. package/dist/analysis-core/data/finding-types.js +1 -0
  83. package/dist/analysis-core/errors.js +1 -0
  84. package/dist/analysis-core/findings/presentation.js +1 -0
  85. package/dist/analysis-core/github/repo-key.js +1 -0
  86. package/dist/analysis-core/import/outside-loader-sources.js +1 -0
  87. package/dist/analysis-core/index.js +6 -0
  88. package/dist/analysis-core/llm/cache-policy.js +1 -0
  89. package/dist/analysis-core/llm/cost.js +1 -0
  90. package/dist/analysis-core/llm/execution.js +1 -0
  91. package/dist/analysis-core/llm/output-limit.js +1 -0
  92. package/dist/analysis-core/llm/slots.js +1 -0
  93. package/dist/analysis-core/llm/stable-json.js +1 -0
  94. package/dist/analysis-core/llm/structured-output-error.js +1 -0
  95. package/dist/analysis-core/map-limit.js +1 -0
  96. package/dist/analysis-core/requests/answer-formats.js +1 -0
  97. package/dist/analysis-core/requests/check-diagnostics.js +1 -0
  98. package/dist/analysis-core/requests/headline-request.js +1 -0
  99. package/dist/analysis-core/requests/instruction-text.js +1 -0
  100. package/dist/analysis-core/requests/local-plan.js +1 -0
  101. package/dist/analysis-core/requests/registry.js +1 -0
  102. package/dist/analysis-core/requests/schema-descriptions.js +1 -0
  103. package/dist/analysis-core/requests/shape-finding-input.js +1 -0
  104. package/dist/analysis-core/requests/template-version.js +1 -0
  105. package/dist/analysis-core/requests/tools.js +1 -0
  106. package/dist/analysis-core/settings.js +1 -0
  107. package/dist/bundle/seal/deserialise.js +75 -0
  108. package/dist/bundle/seal/encryption.js +25 -0
  109. package/dist/bundle/seal/metadata.js +69 -0
  110. package/dist/bundle/seal/open.js +38 -0
  111. package/dist/bundle/seal/planned-calls.js +20 -0
  112. package/dist/bundle/seal/signature.js +51 -0
  113. package/dist/cli/agent-folder.js +17 -0
  114. package/dist/cli/api-types.js +2 -0
  115. package/dist/cli/api.js +1 -0
  116. package/dist/cli/browser.js +23 -0
  117. package/dist/cli/claude-code-permission.js +20 -0
  118. package/dist/cli/commands/analyze.js +73 -0
  119. package/dist/cli/commands/login.js +51 -0
  120. package/dist/cli/completion-recovery.js +18 -0
  121. package/dist/cli/context.js +22 -0
  122. package/dist/cli/email-sign-in.js +56 -0
  123. package/dist/cli/git.js +39 -0
  124. package/dist/cli/intake-payload.js +18 -0
  125. package/dist/{main.js → cli/main.js} +23 -12
  126. package/dist/cli/mcp.js +297 -0
  127. package/dist/cli/output.js +10 -0
  128. package/dist/cli/relay-check.js +239 -0
  129. package/dist/cli/relay-preparation.js +31 -0
  130. package/dist/cli/sign-in-input.js +97 -0
  131. package/dist/cli/sign-in.js +37 -0
  132. package/dist/cli/suggested-email.js +10 -0
  133. package/dist/cli/summary.js +30 -0
  134. package/dist/contract/cli-api-messages.js +5 -0
  135. package/dist/contract/cli-refusals.js +22 -0
  136. package/dist/contract/cli-version.js +1 -0
  137. package/dist/contract/console-url.js +8 -0
  138. package/dist/contract/console.js +111 -0
  139. package/dist/contract/email-sign-in-messages.js +18 -0
  140. package/dist/contract/messages.js +252 -0
  141. package/dist/contract/release-key.js +2 -0
  142. package/dist/contract/runtime-environment.js +37 -0
  143. package/dist/core/analysis/finding-quality/config.js +4 -0
  144. package/dist/core/analysis/run-to-end.js +55 -0
  145. package/dist/core/analysis/structural-run.js +16 -0
  146. package/dist/gatekeeper/attestation.js +67 -0
  147. package/dist/gatekeeper/certificate-algorithms.js +18 -0
  148. package/dist/gatekeeper/certificate-path.js +99 -0
  149. package/dist/gatekeeper/cli-api.js +174 -0
  150. package/dist/gatekeeper/client.js +76 -0
  151. package/dist/gatekeeper/connection-exchange.js +60 -0
  152. package/dist/gatekeeper/connection.js +140 -0
  153. package/dist/gatekeeper/console.js +168 -0
  154. package/dist/gatekeeper/loopback.js +5 -0
  155. package/dist/gatekeeper/model.js +70 -0
  156. package/dist/gatekeeper/production-attestation.js +149 -0
  157. package/dist/gatekeeper/trust/hardware.js +21 -0
  158. package/dist/gatekeeper/trust/policy.js +42 -0
  159. package/dist/gatekeeper/trust/production.json +25 -0
  160. package/dist/setup-pings/client.js +31 -0
  161. package/dist/setup-pings/contract.js +2 -0
  162. package/dist/setup-pings/policy.js +3 -0
  163. package/dist/setup-pings/state.js +261 -0
  164. package/dist/setup-pings/worker.js +95 -0
  165. package/dist/transport/base64url.js +9 -0
  166. package/dist/transport/http-response.js +89 -0
  167. package/dist/transport/tls-exporter.js +5 -0
  168. package/package.json +57 -11
  169. package/LICENSE +0 -1
  170. package/dist/api-types.js +0 -2
  171. package/dist/api.js +0 -97
  172. package/dist/commands/check.js +0 -104
  173. package/dist/commands/login.js +0 -21
  174. package/dist/context.js +0 -27
  175. package/dist/files.js +0 -99
  176. package/dist/git.js +0 -21
  177. package/dist/gitignore.js +0 -70
  178. package/dist/mcp.js +0 -165
  179. package/dist/output.js +0 -23
  180. package/dist/secret-files.js +0 -5
  181. /package/dist/{commands → cli/commands}/balance.js +0 -0
  182. /package/dist/{commands → cli/commands}/finding.js +0 -0
  183. /package/dist/{commands → cli/commands}/logout.js +0 -0
  184. /package/dist/{credentials.js → cli/credentials.js} +0 -0
@@ -0,0 +1,97 @@
1
+ import { createInterface } from "node:readline";
2
+ import { PassThrough } from "node:stream";
3
+ export function terminalSignInInput(input = process.stdin, output = process.stdout) {
4
+ const adapter = {
5
+ get isTerminal() { return input.isTTY === true && output.isTTY === true; },
6
+ async readCode(prompt, signal) {
7
+ if (signal?.aborted || !adapter.isTerminal || input.destroyed || input.readableEnded || output.destroyed || output.writableEnded)
8
+ return undefined;
9
+ if (!input.setRawMode)
10
+ return undefined;
11
+ const setRawMode = input.setRawMode.bind(input);
12
+ return new Promise(resolve => {
13
+ const wasRaw = input.isRaw === true;
14
+ const wasFlowing = input.readableFlowing === true;
15
+ // Keep readline's editing and keypress listeners off stdin, with no output to echo a code.
16
+ const hidden = new PassThrough();
17
+ const lines = createInterface({ input: hidden, terminal: true, historySize: 0 });
18
+ let finished = false;
19
+ let result;
20
+ const cancel = () => { result = undefined; finish(); };
21
+ const outputFailed = () => { cancel(); complete(); };
22
+ const forward = (chunk) => { hidden.write(chunk); };
23
+ function complete() {
24
+ process.removeListener("SIGINT", cancel);
25
+ signal?.removeEventListener("abort", cancel);
26
+ output.removeListener("error", outputFailed);
27
+ output.removeListener("close", outputFailed);
28
+ output.removeListener("finish", outputFailed);
29
+ resolve(result);
30
+ }
31
+ function finish(code) {
32
+ if (finished)
33
+ return;
34
+ finished = true;
35
+ result = code;
36
+ lines.close();
37
+ lines.removeAllListeners();
38
+ hidden.destroy();
39
+ hidden.removeAllListeners();
40
+ input.removeListener("data", forward);
41
+ input.removeListener("end", cancel);
42
+ input.removeListener("close", cancel);
43
+ input.removeListener("error", cancel);
44
+ try {
45
+ if (!wasRaw)
46
+ setRawMode(false);
47
+ }
48
+ catch {
49
+ result = undefined;
50
+ }
51
+ if (!wasFlowing)
52
+ input.pause();
53
+ if (output.destroyed || output.writableEnded) {
54
+ result = undefined;
55
+ return;
56
+ }
57
+ // Keep the error handler until the write finishes: stream errors usually arrive later.
58
+ try {
59
+ output.write("\n", error => { if (!error)
60
+ complete(); });
61
+ }
62
+ catch {
63
+ result = undefined;
64
+ complete();
65
+ }
66
+ }
67
+ lines.once("line", finish);
68
+ lines.once("close", finish);
69
+ lines.once("SIGINT", cancel);
70
+ lines.once("SIGTSTP", cancel);
71
+ input.once("end", cancel);
72
+ input.once("close", cancel);
73
+ input.once("error", cancel);
74
+ process.once("SIGINT", cancel);
75
+ signal?.addEventListener("abort", cancel, { once: true });
76
+ output.once("error", outputFailed);
77
+ output.once("close", outputFailed);
78
+ output.once("finish", outputFailed);
79
+ try {
80
+ if (!wasRaw)
81
+ setRawMode(true);
82
+ if (finished)
83
+ return;
84
+ output.write(prompt);
85
+ if (finished)
86
+ return;
87
+ input.on("data", forward);
88
+ input.resume();
89
+ }
90
+ catch {
91
+ finish();
92
+ }
93
+ });
94
+ },
95
+ };
96
+ return adapter;
97
+ }
@@ -0,0 +1,37 @@
1
+ import { ApiError } from "./api.js";
2
+ import { readCredentials, saveCredentials } from "./credentials.js";
3
+ import { emailSignInMessages } from "../contract/email-sign-in-messages.js";
4
+ async function pollSignIn(ctx, device) {
5
+ const until = ctx.now() + device.expiresIn * 1000;
6
+ while (ctx.now() < until) {
7
+ await ctx.sleep(device.interval * 1000);
8
+ const poll = await ctx.api.request("device/token", { deviceCode: device.deviceCode }, false);
9
+ if (poll.status === "ok") {
10
+ await saveCredentials(ctx.homeDir, ctx.api.baseUrl, poll.token);
11
+ return "signed_in";
12
+ }
13
+ if (poll.status === "expired")
14
+ break;
15
+ if (poll.status === "denied")
16
+ throw new Error(emailSignInMessages.cancelled);
17
+ }
18
+ throw new Error("The sign-in code expired. Run counterfact-labs login to try again.");
19
+ }
20
+ export async function startSignIn(ctx) {
21
+ const device = await ctx.api.request("device", {}, false);
22
+ return { userCode: device.userCode, verificationUri: device.verificationUri,
23
+ pageUrl: `${device.verificationUri}?code=${encodeURIComponent(device.userCode)}`, done: pollSignIn(ctx, device) };
24
+ }
25
+ export async function signInStatus(ctx) {
26
+ if (!await readCredentials(ctx.homeDir, ctx.api.baseUrl))
27
+ return { signedIn: false };
28
+ try {
29
+ await ctx.api.request("balance");
30
+ return { signedIn: true };
31
+ }
32
+ catch (error) {
33
+ if (error instanceof ApiError && error.status === 401)
34
+ return { signedIn: false };
35
+ throw error;
36
+ }
37
+ }
@@ -0,0 +1,10 @@
1
+ import { normalizeSignInEmail } from "./email-sign-in.js";
2
+ import { userEmail } from "./git.js";
3
+ export async function suggestedEmail(folder, readEmail = userEmail) {
4
+ try {
5
+ return normalizeSignInEmail(await readEmail(folder));
6
+ }
7
+ catch {
8
+ return undefined;
9
+ }
10
+ }
@@ -0,0 +1,30 @@
1
+ import { findingPresentation } from "../analysis-core/findings/presentation.js";
2
+ export function formatSummary(report) {
3
+ const lines = ["# Counterfact analysis", "",
4
+ `${report.findings.length} findings; ${report.structures.length} structural proposals.`, ""];
5
+ if (!report.findings.length)
6
+ lines.push("No findings.", "");
7
+ for (const [index, finding] of report.findings.entries()) {
8
+ const presentation = findingPresentation(finding);
9
+ lines.push(`## ${index + 1}. ${presentation.displayTitle}`, "");
10
+ if (presentation.ifLeft)
11
+ lines.push(`If left: ${presentation.ifLeft}`, "");
12
+ lines.push(`Severity: ${finding.severity}; category: ${finding.category ?? "unclassified"}.`, "", finding.cause, "");
13
+ const sites = [...(finding.site ? [finding.site] : []), ...finding.otherSites];
14
+ if (sites.length)
15
+ lines.push(`Sources: ${sites.map(site => `${site.path}:${site.line ?? "?"}`).join(", ")}`, "");
16
+ if (finding.question)
17
+ lines.push(finding.question, "");
18
+ if (finding.fixes.length)
19
+ lines.push(...finding.fixes.map(fix => `- ${fix.title}`), "");
20
+ }
21
+ if (report.structures.length)
22
+ lines.push("## Structural proposals", "", ...report.structures.flatMap(proposal => {
23
+ const presentation = findingPresentation({ ...proposal, title: proposal.body.title });
24
+ return [presentation.ifLeft ? `- ${presentation.displayTitle}` : `- ${presentation.displayTitle}: ${proposal.body.cause}`, "",
25
+ ...(presentation.ifLeft ? [`If left: ${presentation.ifLeft}`, ""] : [])];
26
+ }));
27
+ if (report.warnings.length)
28
+ lines.push("## Reading and analysis notices", "", ...report.warnings.map(warning => `- ${warning}`), "");
29
+ return lines.join("\n");
30
+ }
@@ -0,0 +1,5 @@
1
+ export const cliApiMessages = Object.freeze({
2
+ unreachable: "Can't reach Counterfact. Try again shortly.",
3
+ invalid_response: "Counterfact returned an invalid response. Try again shortly.",
4
+ request_failed: "The request could not be completed. Try again shortly.",
5
+ });
@@ -0,0 +1,22 @@
1
+ export const START_FAILURE_MESSAGE = "This couldn't start. Try again.";
2
+ export const RESERVATION_EXPIRED_MESSAGE = "This reservation expired. Start a new analysis.";
3
+ export const FINISH_FAILURE_MESSAGE = "The analysis couldn't finish. Try again.";
4
+ export const RESULTS_TOO_LARGE_MESSAGE = "The results were too large to save. Try again.";
5
+ export const COMMITTED_FILES_MESSAGE = "Counterfact analyzes the committed version of the agent's files. Commit your changes, then try again.";
6
+ const unsupportedVersion = "This version of Counterfact can't run analyses. Update it: npm install -g @counterfact-labs/cli@latest.";
7
+ /** Shared public refusals keep mode admission and reservation errors consistent. */
8
+ export const cliRefusals = {
9
+ low_balance: "Your balance doesn't cover this analysis. Add credit at https://app.counterfactlabs.com/account?status=low_balance#balance",
10
+ local_checks_paused: "Analyses are paused right now. Try again later.",
11
+ cli_update_required: "Update Counterfact: run npm install -g @counterfact-labs/cli@latest.",
12
+ release_unavailable: unsupportedVersion,
13
+ invalid_plan: unsupportedVersion,
14
+ invalid_body: "Counterfact couldn't read this request. Update Counterfact, then try again.",
15
+ body_too_large: "This request is too large. The limit is 2 MB.",
16
+ free_cap_reached: "Free analyses are paused for today. Try again tomorrow, or top up to run a paid analysis.",
17
+ org_spend_limit: "The workspace spending limit was reached. Ask Counterfact to raise it.",
18
+ not_reserved: "Start a new analysis.",
19
+ reservation_expired: "Start a new analysis.",
20
+ stale_quote: START_FAILURE_MESSAGE,
21
+ };
22
+ export function refusalExitCode(code) { return code === "low_balance" ? 2 : 1; }
@@ -0,0 +1 @@
1
+ export const CLI_VERSION = "0.2.3";
@@ -0,0 +1,8 @@
1
+ export function savedAnalysisUrl(projectId, consoleOrigin) {
2
+ return new URL(`/projects/${projectId}/findings`, consoleOrigin).href;
3
+ }
4
+ import { isLoopbackHost } from "../gatekeeper/loopback.js";
5
+ /** Bearer credentials may use plain HTTP only on this machine. */
6
+ export function consoleSchemeAllowed(url) {
7
+ return url.protocol === "https:" || url.protocol === "http:" && isLoopbackHost(url.hostname);
8
+ }
@@ -0,0 +1,111 @@
1
+ import { z } from "zod";
2
+ import { fixDeltaSchema, fixFactsSchema } from "../analysis-core/data/finding-types.js";
3
+ import { inspectFixSchema, otherSiteSchema, rawFindingSchema } from "../analysis-core/analysis/schemas.js";
4
+ import { sourceNotesSchema } from "../analysis-core/analysis/source-notes/types.js";
5
+ export const id = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/);
6
+ const uuid = z.uuid().transform(value => value.toLowerCase());
7
+ const money = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
8
+ const count = (max) => z.number().int().positive().max(max);
9
+ // These data-only boundaries follow the SaaS local start, reservation and findings owners; see docs/provenance.md.
10
+ export const LOCAL_START_EXECUTION_HEADER = { name: "x-counterfact-execution", value: "local" };
11
+ export const LOCAL_START_BODY_LIMIT = 2 * 1024 * 1024;
12
+ const localStartPath = z.string().min(1).max(4096).refine(value => !value.includes("\\") && !value.includes("\0") && !value.startsWith("/")
13
+ && value.split("/").every(part => part !== ".." && part !== "." && part !== ""));
14
+ export const localStartRequestSchema = z.strictObject({
15
+ execution: z.literal("local"), remoteUrl: z.string().max(4096).nullable(), relPath: z.string().max(4096),
16
+ folderName: z.string().trim().min(1).max(255), branch: z.string().max(4096).nullable().optional(),
17
+ commit: z.string().max(4096).nullable().optional(), dirty: z.boolean().nullable().optional(),
18
+ files: z.array(z.strictObject({ path: localStartPath, size: money })).min(1)
19
+ .refine(files => new Set(files.map(file => file.path)).size === files.length),
20
+ });
21
+ export const localStartResponseSchema = z.strictObject({ jobId: uuid, projectId: uuid });
22
+ export const relayPlanSchema = z.strictObject({
23
+ bundleVersion: id, bundleHash: z.string().regex(/^[a-f0-9]{64}$/), relayVersion: id,
24
+ calls: z.array(z.strictObject({ routeId: id, model: id, calls: count(512), maxInputBytes: count(800_000),
25
+ maxInputTokens: count(200_000), maxOutputTokens: count(64_000) })).min(1).max(64)
26
+ .refine(calls => new Set(calls.map(call => call.routeId)).size === calls.length && calls.reduce((sum, call) => sum + call.calls, 0) <= 512),
27
+ });
28
+ export const cliRelayReservationRequestSchema = z.discriminatedUnion("action", [
29
+ z.strictObject({ action: z.literal("create"), jobId: uuid, checkId: uuid, projectId: uuid, plan: relayPlanSchema }),
30
+ z.strictObject({ action: z.literal("reserve"), reservationId: uuid }),
31
+ z.strictObject({ action: z.literal("status"), reservationId: uuid }),
32
+ z.strictObject({ action: z.literal("heartbeat"), reservationId: uuid }),
33
+ z.strictObject({ action: z.literal("fail"), reservationId: uuid }),
34
+ ]);
35
+ export const consoleCreatedSchema = z.strictObject({ reservationId: uuid, jobId: uuid, projectId: uuid, checkId: uuid, state: z.literal("quoted") });
36
+ export const consoleStatusSchema = z.strictObject({
37
+ reservationId: uuid, checkId: uuid, projectId: uuid,
38
+ state: z.enum(["quoted", "reserved", "closing", "closed", "expired"]),
39
+ settlement: z.enum(["unheld", "held", "charged", "released"]),
40
+ reason: z.enum(["service_failure", "required_failure", "authorization_refused", "our_deadline_failure",
41
+ "findings_saved", "required_delivered", "no_required_delivery"]).nullable(),
42
+ uploadState: z.enum(["saved", "missing"]), analysisId: uuid.optional(), findingIds: z.array(uuid).max(20_000).optional(),
43
+ quoteId: z.string().min(1).max(256).optional(), priceMicro: money.nullable(), providerBudgetMicro: money,
44
+ expiresAt: z.iso.datetime(), absoluteExpiresAt: z.iso.datetime().nullable(),
45
+ deadlineClassification: z.enum(["caller_timeout", "our_deadline_failure"]).nullable(),
46
+ }).refine(value => value.uploadState === "saved"
47
+ ? value.analysisId !== undefined && value.findingIds !== undefined
48
+ : value.analysisId === undefined && value.findingIds === undefined);
49
+ export const consoleReservationSchema = consoleStatusSchema.safeExtend({ reservationToken: z.string().min(1).max(256),
50
+ quoteId: z.string().min(1).max(256) });
51
+ export const consoleIntakeResultSchema = z.strictObject({
52
+ checkId: uuid, projectId: uuid, analysisId: uuid, findingIds: z.array(uuid).max(20_000),
53
+ settlement: z.enum(["unheld", "held", "charged", "released"]).optional(),
54
+ });
55
+ export const LOCAL_FINDINGS_BODY_LIMIT = 2 * 1024 * 1024;
56
+ export const LOCAL_FINDINGS_QUOTE_LIMIT = 2000;
57
+ export const LOCAL_FINDINGS_TEXT_LIMIT = 4096;
58
+ function shortFindingFields(value) {
59
+ if (typeof value === "string")
60
+ return Buffer.byteLength(value, "utf8") <= LOCAL_FINDINGS_TEXT_LIMIT;
61
+ return value === null || typeof value !== "object" || Object.values(value).every(shortFindingFields);
62
+ }
63
+ function validIntakeText(value) {
64
+ if (typeof value === "string")
65
+ return !value.includes(String.fromCharCode(0)) && !/[\uD800-\uDFFF]/u.test(value);
66
+ return value === null || typeof value !== "object" || Object.values(value).every(validIntakeText);
67
+ }
68
+ const quote = z.string().refine(value => Buffer.byteLength(value, "utf8") <= LOCAL_FINDINGS_QUOTE_LIMIT);
69
+ const text = z.string().min(1).refine(value => value.trim().length > 0);
70
+ const delta = fixDeltaSchema.extend({
71
+ nodes: z.array(fixDeltaSchema.shape.nodes.element.strict()),
72
+ edges: z.array(fixDeltaSchema.shape.edges.element.strict()),
73
+ }).strict();
74
+ const fix = inspectFixSchema.extend({
75
+ ...fixFactsSchema.shape,
76
+ targetFiles: z.array(inspectFixSchema.shape.targetFiles.element.strict()),
77
+ delta: delta.nullable(),
78
+ }).strict();
79
+ const notes = sourceNotesSchema.extend({
80
+ sites: z.array(sourceNotesSchema.shape.sites.element.extend({ keyPhrase: quote }).strict()),
81
+ counterEvidence: z.array(sourceNotesSchema.shape.counterEvidence.element.extend({ excerpt: quote }).strict()),
82
+ }).strict();
83
+ const finding = rawFindingSchema.extend({
84
+ summary: z.string().nullable(),
85
+ category: rawFindingSchema.shape.category.removeCatch(),
86
+ question: rawFindingSchema.shape.question.removeCatch().nullable(),
87
+ siteRepo: text.nullable(),
88
+ site: rawFindingSchema.shape.site.unwrap().strict().nullable(),
89
+ excerpt: quote.nullable(),
90
+ otherSites: z.array(otherSiteSchema.extend({ repo: text, excerpt: otherSiteSchema.shape.excerpt.pipe(quote) }).strict()),
91
+ fixes: z.array(fix).min(1),
92
+ sourceNotes: notes.nullable().optional(),
93
+ }).strict().refine(shortFindingFields);
94
+ const payload = z.strictObject({
95
+ checkId: z.uuid(), projectId: z.uuid(), reservationId: uuid.optional(),
96
+ agent: z.strictObject({
97
+ repository: text.max(4096), commit: z.string().regex(/^(?:[a-f\d]{40}|[a-f\d]{64})$/i),
98
+ files: z.array(z.strictObject({ path: text.max(4096), hash: z.string().regex(/^[a-f\d]{64}$/i) })).min(1),
99
+ }),
100
+ findings: z.array(finding), bundleVersion: text.max(128), relayVersion: text.max(128), counterfactLocalVersion: text.max(128),
101
+ }).refine(validIntakeText);
102
+ // Original JSON size is checked before a shared schema can trim text or apply defaults.
103
+ export const localFindingsPayloadSchema = z.unknown().refine(value => {
104
+ try {
105
+ const json = JSON.stringify(value);
106
+ return typeof json === "string" && Buffer.byteLength(json, "utf8") <= LOCAL_FINDINGS_BODY_LIMIT;
107
+ }
108
+ catch {
109
+ return false;
110
+ }
111
+ }).pipe(payload);
@@ -0,0 +1,18 @@
1
+ import { cliApiMessages } from "./cli-api-messages.js";
2
+ const rateLimited = "Too many sign-in requests. Try again later.";
3
+ export const emailSignInMessages = Object.freeze({
4
+ invalid_email: "Enter a valid email address.",
5
+ invalid_code: "Enter the code from your email.",
6
+ wrong_code: "That code didn't work. Try the code from your email again.",
7
+ expired: "That code expired. Request a new sign-in code.",
8
+ too_many_attempts: "Too many incorrect codes. Request a new sign-in code.",
9
+ rate_limited: rateLimited,
10
+ rate_limited_with_cooldown: (seconds) => seconds === 0 ? rateLimited
11
+ : `Too many sign-in requests. Try again in ${seconds} ${seconds === 1 ? "second" : "seconds"}.`,
12
+ email_unavailable: "Email sign-in isn't available. Use browser sign-in instead.",
13
+ browser_sign_in_pending: "A browser sign-in is still in progress. Finish it before starting email sign-in.",
14
+ unreachable: cliApiMessages.unreachable,
15
+ invalid_response: cliApiMessages.invalid_response,
16
+ request_failed: cliApiMessages.request_failed,
17
+ cancelled: "Sign-in was cancelled. Run counterfact-labs login to try again.",
18
+ });
@@ -0,0 +1,252 @@
1
+ import { z } from "zod";
2
+ import { executionRequest } from "../analysis-core/contracts/requests.js";
3
+ import { checkRuntimeEnvironment } from "./runtime-environment.js";
4
+ /**
5
+ * The messages that cross between the customer's machine and the sealed relay. This module is the only owner of
6
+ * their shape: the relay, the gatekeeper and the bundle tooling all import these schemas and never redefine them.
7
+ * Design: docs/design/sealed-model-relay.md and docs/design/relay-contract.md.
8
+ */
9
+ const compactJwtPattern = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
10
+ const hexChallenge = z.string().length(64).regex(/^[0-9a-f]+$/);
11
+ /** The wire adds routing identity to the core's data-only execution envelope. */
12
+ export const relayRequest = executionRequest.extend({
13
+ bundleVersion: z.string().min(1).max(128),
14
+ reservationId: z.string().min(1).max(128),
15
+ reservationToken: z.string().min(1).max(256),
16
+ /** Stable paid-call identity; required by the production delivery transport. */
17
+ callId: z.uuid(),
18
+ /** Fresh per call, echoed in the answer, so an answer cannot be replayed into another call. */
19
+ nonce: z.string().min(16).max(256),
20
+ template: z.string().min(1).max(128),
21
+ answerFormat: z.string().min(1).max(128),
22
+ limits: executionRequest.shape.limits.unwrap().extend({ maxTokens: z.number().int().positive().safe().max(64_000).optional() }).optional(),
23
+ }).strict();
24
+ export const relayUsage = z.object({
25
+ inputTokens: z.number().int().nonnegative().safe(),
26
+ outputTokens: z.number().int().nonnegative().safe(),
27
+ cacheReadTokens: z.number().int().nonnegative().safe(),
28
+ cacheWriteTokens: z.number().int().nonnegative().safe(),
29
+ cacheWrite5mTokens: z.number().int().nonnegative().safe().optional(),
30
+ cacheWrite1hTokens: z.number().int().nonnegative().safe().optional(),
31
+ }).strict();
32
+ export const relayAnswer = z.discriminatedUnion("ok", [
33
+ z.object({
34
+ ok: z.literal(true),
35
+ nonce: z.string().max(256),
36
+ /** Validated against the shared format; local parsing applies defaults, fallbacks and cleanup. */
37
+ answer: z.unknown(),
38
+ usage: relayUsage,
39
+ /** True when the answer quoted our instruction text and the relay removed it. */
40
+ redacted: z.boolean(),
41
+ }).strict(),
42
+ z.object({
43
+ ok: z.literal(false),
44
+ nonce: z.string().max(256),
45
+ code: z.enum(["bad_request", "unknown_template", "bundle_version", "reservation", "model_failed", "leak_refused", "call_in_progress", "call_status", "busy"]),
46
+ }).strict(),
47
+ ]);
48
+ export const RELAY_TLS_EXPORTER_LABEL = "EXPORTER-counterfact-relay-v1";
49
+ export const RELAY_MAX_FRAME_BYTES = 2 * 1024 * 1024;
50
+ export const deliveryConfirmation = z.object({
51
+ callId: z.uuid(), attemptId: z.uuid(), connectionId: z.uuid(),
52
+ challenge: hexChallenge,
53
+ }).strict();
54
+ /** The challenge is released with the complete answer, never in headers or a separate preliminary frame. */
55
+ export const relayAnswerFrame = z.object({
56
+ answer: relayAnswer,
57
+ delivery: z.object({ confirmation: deliveryConfirmation, deadlineMs: z.number().int().positive().safe() }).strict().optional(),
58
+ }).strict().refine(frame => frame.answer.ok === (frame.delivery !== undefined));
59
+ export const deliveryReceipt = z.object({ receiptId: z.uuid() }).strict();
60
+ // This is the count-only server boundary; it never accepts arbitrary diagnostics or request content.
61
+ const billingId = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/);
62
+ const billingScope = { actionId: z.uuid(), reservationId: z.uuid(), reservationToken: z.string().min(1).max(256), callId: z.uuid() };
63
+ const inputCount = z.number().int().nonnegative().max(200_000);
64
+ const outputCount = z.number().int().nonnegative().max(64_000);
65
+ /** A reservation lookup establishes the owner without reserving spend or contacting the model provider. */
66
+ export const reservationCheck = z.object({ action: z.literal("check"), ...billingScope }).strict();
67
+ export const reservationIdentity = z.object({ clientId: z.string().min(1).max(128) }).strict();
68
+ export const callCounts = z.object({ inputBytes: z.number().int().positive().max(800_000),
69
+ inputTokens: inputCount.positive(), maxOutputTokens: outputCount.positive() }).strict();
70
+ export const authorizationRequest = z.object({ action: z.literal("authorize"), ...billingScope,
71
+ routeId: billingId, templateId: billingId, answerFormatId: billingId, model: billingId, ...callCounts.shape }).strict();
72
+ export const callAuthorization = z.object({ attemptId: z.uuid(), model: billingId, role: z.enum(["required", "optional"]),
73
+ /** Stable opaque owner supplied only by the authenticated authorization service. */
74
+ clientId: reservationIdentity.shape.clientId,
75
+ deadline: z.iso.datetime(), countLimits: z.object({ maxInputBytes: z.number().int().positive().max(800_000),
76
+ maxInputTokens: inputCount.positive(), maxOutputTokens: outputCount.positive() }).strict() }).strict();
77
+ export const providerUsage = z.object({ kind: z.enum(["actual", "estimated", "unpaid"]),
78
+ outcome: z.enum(["success", "unknown", "retryable_failure", "terminal_provider_failure", "schema_refusal", "leak_refusal"]),
79
+ inputTokens: inputCount, outputTokens: outputCount, cacheReadTokens: inputCount, cacheWriteTokens: inputCount,
80
+ cacheWrite5mTokens: inputCount.optional(), cacheWrite1hTokens: inputCount.optional() }).strict();
81
+ export const serviceTiming = z.object({ kind: z.enum(["relay", "provider"]), startedAt: z.number().int().nonnegative().safe(),
82
+ endedAt: z.number().int().nonnegative().safe() }).strict().refine(timing => timing.endedAt >= timing.startedAt);
83
+ export const usageReport = z.object({ action: z.literal("usage"), ...billingScope, attemptId: z.uuid(),
84
+ usage: providerUsage, timing: z.array(serviceTiming).max(2) }).strict();
85
+ export const deliveryReport = z.object({ action: z.literal("delivery"), ...billingScope, attemptId: z.uuid(),
86
+ delivery: z.discriminatedUnion("status", [
87
+ z.object({ status: z.literal("confirmed"), confirmation: deliveryConfirmation.extend({
88
+ challenge: z.string().regex(/^[A-Za-z0-9_-]{43}$/),
89
+ }) }).strict(),
90
+ z.object({ status: z.enum(["unknown", "failed"]) }).strict(),
91
+ ]) }).strict();
92
+ export const reservationAction = z.discriminatedUnion("action", [reservationCheck, authorizationRequest, usageReport, deliveryReport]);
93
+ export const RELAY_HEALTH_VERSION = 2;
94
+ export const RELAY_HEALTH_AUDIENCE = "counterfact-relay-health";
95
+ export const RELAY_HEALTH_MAX_REQUEST_BYTES = 1024;
96
+ /** Billing supplies a fresh random challenge; this route accepts no analysis data. */
97
+ export const relayHealthRequest = z.object({
98
+ version: z.literal(RELAY_HEALTH_VERSION),
99
+ challenge: hexChallenge,
100
+ }).strict();
101
+ export const relayHealthRelease = z.object({
102
+ releaseId: z.string().min(1).max(128),
103
+ relayVersion: z.string().min(1).max(128),
104
+ bundleVersion: z.string().min(1).max(128),
105
+ workloadPolicySha256: z.string().regex(/^[0-9a-f]{64}$/),
106
+ }).strict();
107
+ export const relayHealthFilesystem = z.object({
108
+ readOnlyRootFilesystem: z.boolean(),
109
+ tmpFilesystemType: z.string().min(1).max(128).regex(/^[a-zA-Z0-9._-]+$/),
110
+ }).strict();
111
+ export const relayHealthResponse = z.discriminatedUnion("ok", [
112
+ z.object({
113
+ version: z.literal(RELAY_HEALTH_VERSION),
114
+ ok: z.literal(true),
115
+ bootId: z.uuid(),
116
+ challenge: hexChallenge,
117
+ release: relayHealthRelease,
118
+ filesystem: relayHealthFilesystem,
119
+ /** The evidence owner authenticates the audience and all three nonces before release. */
120
+ evidence: z.string().max(32 * 1024).regex(compactJwtPattern),
121
+ }).strict(),
122
+ z.object({
123
+ version: z.literal(RELAY_HEALTH_VERSION),
124
+ ok: z.literal(false),
125
+ code: z.enum(["bad_request", "unavailable"]),
126
+ }).strict(),
127
+ ]);
128
+ export const PRODUCTION_EVIDENCE_VERSION = 1;
129
+ export const RELAY_CHANNEL_AUDIENCE = "https://relay.counterfactlabs.com";
130
+ // Passthrough forwarding keeps the non-root workload's listener port unchanged.
131
+ export const RELAY_PORT = 8443;
132
+ export const RELAY_URL = `${RELAY_CHANNEL_AUDIENCE}:${RELAY_PORT}`;
133
+ export const RELAY_HEALTH_PATH = "/health";
134
+ export const RELAY_HEALTH_URL = `${RELAY_URL}${RELAY_HEALTH_PATH}`;
135
+ export const GOOGLE_ATTESTATION_ISSUER = "https://confidentialcomputing.googleapis.com";
136
+ export const MAX_ATTESTATION_BYTES = 32 * 1024;
137
+ export const attestationChallenge = z.object({
138
+ version: z.literal(PRODUCTION_EVIDENCE_VERSION),
139
+ challenge: hexChallenge,
140
+ }).strict();
141
+ export const productionEvidence = z.object({
142
+ version: z.literal(PRODUCTION_EVIDENCE_VERSION),
143
+ token: z.string().max(MAX_ATTESTATION_BYTES).regex(compactJwtPattern),
144
+ }).strict();
145
+ /** Google's launcher accepts up to six caller nonces, each at most 74 characters. */
146
+ export const evidenceBinding = z.object({
147
+ audience: z.string().min(1).max(256).regex(/^[A-Za-z0-9:/._-]+$/),
148
+ nonces: z.array(z.string().min(1).max(74).regex(/^[A-Za-z0-9_:-]+$/)).min(1).max(6),
149
+ }).strict();
150
+ /** Native claims are interpreted only after the PKI chain and token signature are authenticated. */
151
+ export const googleAttestationClaims = z.object({
152
+ iss: z.literal(GOOGLE_ATTESTATION_ISSUER),
153
+ aud: z.string(),
154
+ sub: z.string(),
155
+ iat: z.number().int(),
156
+ nbf: z.number().int(),
157
+ exp: z.number().int(),
158
+ eat_nonce: z.union([z.string(), z.array(z.string()).min(1).max(6)]),
159
+ secboot: z.literal(true),
160
+ dbgstat: z.literal("disabled-since-boot"),
161
+ oemid: z.literal(11129),
162
+ hwmodel: z.string(),
163
+ swname: z.literal("CONFIDENTIAL_SPACE"),
164
+ swversion: z.array(z.string()).length(1),
165
+ google_service_accounts: z.array(z.string()).min(1),
166
+ development: z.never().optional(),
167
+ identity: z.never().optional(),
168
+ submods: z.object({
169
+ confidential_space: z.object({
170
+ support_attributes: z.array(z.string()),
171
+ monitoring_enabled: z.object({ memory: z.literal(false) }).passthrough(),
172
+ }).passthrough(),
173
+ container: z.object({
174
+ image_digest: z.string().regex(/^sha256:[0-9a-f]{64}$/),
175
+ image_signatures: z.array(z.object({
176
+ key_id: z.string(), signature: z.string().min(1),
177
+ signature_algorithm: z.literal("RSASSA_PSS_SHA256"),
178
+ }).passthrough()).min(1),
179
+ // Google omits both override claims when nothing was overridden; when present they must still be empty.
180
+ cmd_override: z.array(z.unknown()).length(0).optional(),
181
+ env_override: z.object({}).strict().optional(),
182
+ // Real tokens list the container's own environment; the relay's start-up rule decides which settings are allowed.
183
+ env: z.record(z.string(), z.string()).refine(env => {
184
+ try {
185
+ checkRuntimeEnvironment(env);
186
+ return true;
187
+ }
188
+ catch {
189
+ return false;
190
+ }
191
+ }),
192
+ }).passthrough(),
193
+ }).passthrough(),
194
+ }).passthrough();
195
+ /** Authenticated observations describe a refused workload without granting it an admitted identity. */
196
+ export const observedAttestationClaims = googleAttestationClaims.extend({
197
+ dbgstat: z.string(),
198
+ submods: googleAttestationClaims.shape.submods.extend({
199
+ container: googleAttestationClaims.shape.submods.shape.container.extend({
200
+ image_signatures: z.array(googleAttestationClaims.shape.submods.shape.container.shape.image_signatures.element),
201
+ cmd_override: z.array(z.unknown()).optional(),
202
+ env_override: z.record(z.string(), z.unknown()).optional(),
203
+ }),
204
+ }),
205
+ });
206
+ /**
207
+ * Local development claims. These cannot establish production identity; production uses native Google claims.
208
+ */
209
+ export const attestationClaims = z.object({
210
+ iss: z.string().url(),
211
+ /** The relay image's digest. Must be one we published. */
212
+ imageDigest: z.string().regex(/^sha256:[0-9a-f]{64}$/),
213
+ /** Hash of the relay's channel public key, so the token is bound to this connection. */
214
+ channelKeyHash: z.string().regex(/^[0-9a-f]{64}$/),
215
+ exp: z.number().int(),
216
+ iat: z.number().int(),
217
+ }).passthrough();
218
+ const assetMetadata = z.object({
219
+ /** HMAC-SHA256 keyed by a secret stored only in the encrypted payload. */
220
+ fingerprint: z.string().regex(/^[0-9a-f]{64}$/),
221
+ size: z.number().int().nonnegative(),
222
+ }).strict();
223
+ export const bundleMetadata = z.object({
224
+ templates: z.record(z.string().min(1), assetMetadata.extend({
225
+ role: z.enum(["required", "optional"]),
226
+ answerFormats: z.array(z.string().min(1)).min(1),
227
+ }).strict()),
228
+ includes: z.record(z.string().min(1), assetMetadata.extend({
229
+ expandedSize: z.number().int().nonnegative(),
230
+ expandedFingerprint: z.string().regex(/^[0-9a-f]{64}$/),
231
+ }).strict()),
232
+ descriptions: z.record(z.string().min(1), assetMetadata),
233
+ headlinePlan: assetMetadata.extend({ inputOverheadBytes: z.number().int().nonnegative(), evidenceCopies: z.number().int().nonnegative(),
234
+ resultVersion: z.number().int().positive().optional() }).strict().optional(),
235
+ }).strict();
236
+ /** The sealed instruction bundle as shipped with the CLI. Only the relay can open `sealed`. */
237
+ export const sealedBundle = z.object({
238
+ version: z.string().min(1),
239
+ /** The relay versions able to open this bundle. */
240
+ relayVersions: z.array(z.string().min(1)).min(1),
241
+ /** Template names and answer formats are public; the wording is not (design section 9). */
242
+ templates: z.array(z.string().min(1)),
243
+ answerFormats: z.array(z.string().min(1)),
244
+ metadata: bundleMetadata,
245
+ /** AES-256-GCM over the serialised registry and private fingerprint key, base64. */
246
+ sealed: z.string().min(1),
247
+ iv: z.string().min(1),
248
+ /** The data key, wrapped by the key service so it is released only to an attested relay image. */
249
+ wrappedKey: z.string().min(1),
250
+ /** Ed25519 signature by our release key over every field above, base64. */
251
+ signature: z.string().min(1),
252
+ }).strict();
@@ -0,0 +1,2 @@
1
+ // Changing the release key requires a reviewed CLI release, independent of bundle metadata.
2
+ export const RELEASE_KEY_SPKI_SHA256 = "470601058e910027392fca337233926b6ec38311472307b44c78c5b21d5dfc9c";
@@ -0,0 +1,37 @@
1
+ // The pinned distroless base image sets SSL_CERT_FILE to its bundled certificates. The relay accepts exactly that
2
+ // value, so the image works while no operator setting can point the relay at a different trust store.
3
+ export const BASE_CERTIFICATE_FILE = "/etc/ssl/certs/ca-certificates.crt";
4
+ const fixedValues = Object.freeze({ SSL_CERT_FILE: BASE_CERTIFICATE_FILE });
5
+ // Native startup consumes these data, so loader checks have the same owner as runtime checks.
6
+ export const runtimeEnvironmentPolicy = Object.freeze({
7
+ fixedValues,
8
+ forbiddenPrefixes: ["CF_RELAY_", "ANTHROPIC_", "GOOGLE_AUTH_", "LD_"],
9
+ forbiddenNames: ["GOOGLE_APPLICATION_CREDENTIALS", "NODE_OPTIONS", "NODE_USE_ENV_PROXY", "NODE_EXTRA_CA_CERTS",
10
+ "NODE_TLS_REJECT_UNAUTHORIZED", "SSL_CERT_DIR", "GLIBC_TUNABLES", "GCONV_PATH"],
11
+ forbiddenNamesIgnoringCase: ["http_proxy", "https_proxy", "all_proxy"],
12
+ });
13
+ export const reviewedNativeImageEnvironment = Object.freeze([
14
+ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
15
+ `SSL_CERT_FILE=${BASE_CERTIFICATE_FILE}`, "NODE_ENV=production", "TMPDIR=/tmp",
16
+ ]);
17
+ /** Refuses settings that could redirect credentials, trust or traffic; the image checker reads the same fixed values. */
18
+ export function checkRuntimeEnvironment(env) {
19
+ for (const name of Object.keys(env)) {
20
+ const value = env[name];
21
+ if (value === undefined || value === "")
22
+ continue;
23
+ if (fixedValues[name] !== undefined) {
24
+ if (value !== fixedValues[name])
25
+ throw new Error();
26
+ continue;
27
+ }
28
+ if (runtimeEnvironmentPolicy.forbiddenPrefixes.some(prefix => name.startsWith(prefix)) ||
29
+ runtimeEnvironmentPolicy.forbiddenNamesIgnoringCase.includes(name.toLowerCase()) ||
30
+ runtimeEnvironmentPolicy.forbiddenNames.includes(name))
31
+ throw new Error();
32
+ }
33
+ }
34
+ /** True when an image configuration Env entry pins one of the fixed values the runtime accepts. */
35
+ export function isFixedImageEnvironment(entry) {
36
+ return Object.entries(fixedValues).some(([name, value]) => entry === `${name}=${value}`);
37
+ }
@@ -0,0 +1,4 @@
1
+ export { readLocalQualityConfig as readFindingQualityConfig } from "../../../adapters/quality.js";
2
+ import { assertFindingQualityPrompts as assertSharedPrompts } from "../../../analysis-core/analysis/finding-quality/config.js";
3
+ import { readableRequestTools } from "../../../adapters/instructions.js";
4
+ export const assertFindingQualityPrompts = (config) => assertSharedPrompts(config, readableRequestTools());