@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,140 @@
1
+ import { isLoopbackHost } from "./loopback.js";
2
+ import { randomBytes, X509Certificate } from "node:crypto";
3
+ import { Agent, request as httpsRequest } from "node:https";
4
+ import { connect } from "node:tls";
5
+ import { PRODUCTION_EVIDENCE_VERSION, RELAY_MAX_FRAME_BYTES, RELAY_URL } from "../contract/messages.js";
6
+ import { requestBoundedResponse } from "../transport/http-response.js";
7
+ import { relayExportedKeyMaterial } from "../transport/tls-exporter.js";
8
+ import { createRelayExchange, RelayConnectionError } from "./connection-exchange.js";
9
+ export { RelayConnectionError } from "./connection-exchange.js";
10
+ import { verifyProductionAttestation } from "./production-attestation.js";
11
+ import { verifyAttestation } from "./attestation.js";
12
+ import { validateProductionTrust } from "./trust/policy.js";
13
+ export function relayOrigin(value) {
14
+ const origin = new URL(value);
15
+ if (origin.protocol !== "https:" || origin.username || origin.password || origin.pathname !== "/" || origin.search || origin.hash)
16
+ throw new Error();
17
+ return origin;
18
+ }
19
+ /** A self-signed leaf establishes encryption provisionally; the selected attestation policy authenticates the workload. */
20
+ export async function connectRelay(url = RELAY_URL, policy) {
21
+ let socket, agent;
22
+ let closed = false, expiryTimer;
23
+ const close = () => {
24
+ closed = true;
25
+ clearTimeout(expiryTimer);
26
+ agent?.destroy();
27
+ socket?.destroy();
28
+ };
29
+ try {
30
+ const trust = policy.development ? undefined : validateProductionTrust(policy.trust);
31
+ const origin = relayOrigin(url);
32
+ const timeoutMs = policy.timeoutMs ?? 30_000;
33
+ const nowMs = policy.now ? () => policy.now() * 1000 : Date.now;
34
+ const now = () => Math.floor(nowMs() / 1000);
35
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 120_000)
36
+ throw new Error();
37
+ let hostname = origin.hostname.replace(/^\[|\]$/g, "");
38
+ if (policy.development) {
39
+ if (!policy.development.allowDevelopment || policy.trust ||
40
+ !isLoopbackHost(hostname))
41
+ throw new Error();
42
+ // Resolve the conventional name locally so DNS cannot move development trust off the machine.
43
+ if (hostname === "localhost")
44
+ hostname = "127.0.0.1";
45
+ }
46
+ socket = await new Promise((resolve, reject) => {
47
+ // Attestation authenticates this exact TLS socket before any customer request is sent.
48
+ // nosemgrep: problem-based-packs.insecure-transport.js-node.bypass-tls-verification.bypass-tls-verification
49
+ const provisional = connect({ host: hostname, port: Number(origin.port || 443),
50
+ rejectUnauthorized: false, minVersion: "TLSv1.3", ALPNProtocols: ["http/1.1"] });
51
+ // Install ownership before connection establishment so even a handshake timeout destroys the socket.
52
+ socket = provisional;
53
+ const timer = setTimeout(() => { provisional.destroy(); reject(new RelayConnectionError()); }, timeoutMs);
54
+ provisional.once("error", () => { clearTimeout(timer); reject(new RelayConnectionError()); });
55
+ provisional.once("close", () => { clearTimeout(timer); reject(new RelayConnectionError()); });
56
+ provisional.once("secureConnect", () => { clearTimeout(timer); resolve(provisional); });
57
+ });
58
+ socket.on("error", close);
59
+ socket.once("close", close);
60
+ socket.disableRenegotiation();
61
+ // A new handshake must never replace exporter material on an authenticated socket.
62
+ socket.on("secure", close);
63
+ if (closed || socket.destroyed || (socket.alpnProtocol && socket.alpnProtocol !== "http/1.1"))
64
+ throw new Error();
65
+ const actualSocket = socket;
66
+ const exportedKeyMaterial = relayExportedKeyMaterial(actualSocket);
67
+ // The agent may hand out this socket once. It has no path that dials a replacement connection.
68
+ agent = new Agent({ keepAlive: true, maxSockets: 1, maxCachedSessions: 0 });
69
+ let assigned = false;
70
+ agent.createConnection = () => {
71
+ if (assigned || closed || actualSocket.destroyed)
72
+ throw new RelayConnectionError();
73
+ assigned = true;
74
+ return actualSocket;
75
+ };
76
+ let expires = 0;
77
+ const valid = () => {
78
+ if (closed || actualSocket.destroyed || expires <= now())
79
+ throw new RelayConnectionError();
80
+ };
81
+ async function exchange(path, input, maxBytes, authenticated, waitMs = timeoutMs) {
82
+ if (authenticated)
83
+ valid();
84
+ if (closed || actualSocket.destroyed)
85
+ throw new RelayConnectionError();
86
+ const body = JSON.stringify(input);
87
+ if (Buffer.byteLength(body) > RELAY_MAX_FRAME_BYTES)
88
+ throw new RelayConnectionError();
89
+ const developmentAttestation = !!policy.development && path === "/attestation";
90
+ try {
91
+ const result = await requestBoundedResponse(receive => {
92
+ const req = httpsRequest({ hostname, port: origin.port || 443, method: developmentAttestation ? "GET" : "POST", path, agent,
93
+ maxHeaderSize: 16 * 1024,
94
+ headers: { "content-type": "application/json", "content-length": developmentAttestation ? 0 : Buffer.byteLength(body), "cache-control": "no-store" } }, receive);
95
+ req.on("socket", supplied => { if (supplied !== actualSocket)
96
+ req.destroy(new RelayConnectionError()); });
97
+ return req;
98
+ }, { maxBytes, timeoutMs: waitMs, bodyTimeoutMs: timeoutMs, requestBody: developmentAttestation ? "" : body, validate: response => {
99
+ const status = response.statusCode ?? 0;
100
+ if (response.socket !== actualSocket || (status !== 200 && !(path === "/delivery" && status === 503)) ||
101
+ !(developmentAttestation ? /^application\/jwt(?:;|$)/i : /^application\/json(?:;|$)/i).test(response.headers["content-type"] ?? ""))
102
+ throw new Error();
103
+ } });
104
+ if (authenticated)
105
+ valid();
106
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(result.body);
107
+ const answer = developmentAttestation ? text : JSON.parse(text);
108
+ return { status: result.response.statusCode ?? 0, body: answer };
109
+ }
110
+ catch {
111
+ close();
112
+ throw new RelayConnectionError();
113
+ }
114
+ }
115
+ const challenge = randomBytes(32).toString("hex");
116
+ const challengeStarted = nowMs();
117
+ const challengeStartedMonotonic = performance.now();
118
+ const result = await exchange("/attestation", { version: PRODUCTION_EVIDENCE_VERSION, challenge }, 40 * 1024, false);
119
+ const verified = policy.development
120
+ ? verifyAttestation(result.body, new X509Certificate(actualSocket.getPeerCertificate().raw).publicKey, policy.development, now())
121
+ : await verifyProductionAttestation(result.body, { exportedKeyMaterial, challenge }, trust, now());
122
+ if (nowMs() < challengeStarted || nowMs() - challengeStarted >= timeoutMs ||
123
+ performance.now() - challengeStartedMonotonic >= timeoutMs)
124
+ throw new Error();
125
+ expires = verified.exp;
126
+ valid();
127
+ expiryTimer = setTimeout(close, Math.max(1, (expires - now()) * 1000));
128
+ expiryTimer.unref();
129
+ return {
130
+ get attestedImageDigest() { return verified.imageDigest; },
131
+ send: createRelayExchange({ development: !!policy.development, valid, close, remainingEvidenceMs: () => Math.max(1, (expires - now()) * 1000),
132
+ exchange: (path, input, maxBytes, waitMs) => exchange(path, input, maxBytes, true, waitMs) }),
133
+ close,
134
+ };
135
+ }
136
+ catch {
137
+ close();
138
+ throw new RelayConnectionError();
139
+ }
140
+ }
@@ -0,0 +1,168 @@
1
+ import { consoleSchemeAllowed } from "../contract/console-url.js";
2
+ import { request as httpRequest } from "node:http";
3
+ import { request as httpsRequest } from "node:https";
4
+ import { CLI_VERSION } from "../contract/cli-version.js";
5
+ import { cliRefusals, FINISH_FAILURE_MESSAGE, RESERVATION_EXPIRED_MESSAGE, RESULTS_TOO_LARGE_MESSAGE, START_FAILURE_MESSAGE } from "../contract/cli-refusals.js";
6
+ import { cliRelayReservationRequestSchema, consoleCreatedSchema, consoleIntakeResultSchema, consoleReservationSchema, consoleStatusSchema, LOCAL_FINDINGS_BODY_LIMIT, localFindingsPayloadSchema, LOCAL_START_BODY_LIMIT, LOCAL_START_EXECUTION_HEADER, localStartRequestSchema, localStartResponseSchema } from "../contract/console.js";
7
+ const RESPONSE_LIMIT = 64 * 1024;
8
+ const RESERVATION_BODY_LIMIT = 64 * 1024;
9
+ const messages = {
10
+ ...cliRefusals,
11
+ invalid_configuration: START_FAILURE_MESSAGE,
12
+ invalid_request: FINISH_FAILURE_MESSAGE,
13
+ unavailable: "The results couldn't be saved yet. Run the analysis on the same folder again to save them; you won't be charged again.",
14
+ unauthorized: "Your sign-in has expired. Run counterfact-labs login, then try again.",
15
+ not_allowed: "Your account can't run analyses. Ask Counterfact for access.",
16
+ not_found: "Counterfact couldn't find this analysis. Start a new analysis.",
17
+ rate_limited: "Counterfact is busy. Try again in a minute.",
18
+ reservation_closed: "The analysis ended before its results were saved. Start a new analysis.",
19
+ reservation_closing: "The results are still being saved. Run the analysis on the same folder again in a minute.",
20
+ delivery_incomplete: "The analysis ended before its results were saved. Start a new analysis.",
21
+ check_conflict: "Saved results from an earlier run don't match this analysis. Start a new analysis.",
22
+ };
23
+ export class ConsoleError extends Error {
24
+ code;
25
+ constructor(code = "unavailable", context) {
26
+ super(context === "start" && (code === "unavailable" || code === "invalid_request" || code === "not_found") ? START_FAILURE_MESSAGE
27
+ : context === "intake" && code === "reservation_expired" ? RESERVATION_EXPIRED_MESSAGE
28
+ // Only saving results can be too large; a rejected status, heartbeat or fail request just could not finish.
29
+ : context === "intake" && code === "invalid_request" ? RESULTS_TOO_LARGE_MESSAGE : messages[code]);
30
+ this.name = "ConsoleError";
31
+ this.code = code;
32
+ }
33
+ }
34
+ export const isConsoleToken = (token) => /^[\x21-\x7e]{1,4096}$/.test(token);
35
+ /** Only this client may send repository metadata, content-free lifecycle or findings-only intake payloads to the console. */
36
+ export class ConsoleClient {
37
+ #url;
38
+ #token;
39
+ #timeoutMs;
40
+ constructor(options) {
41
+ try {
42
+ const url = new URL(options.url);
43
+ const timeout = options.timeoutMs ?? 15_000;
44
+ if (!consoleSchemeAllowed(url)
45
+ || url.username || url.password || url.pathname !== "/" || url.search || url.hash
46
+ || !isConsoleToken(options.token)
47
+ || !Number.isSafeInteger(timeout) || timeout <= 0 || timeout > 120_000)
48
+ throw new ConsoleError("invalid_configuration");
49
+ this.#url = url;
50
+ this.#token = options.token;
51
+ this.#timeoutMs = timeout;
52
+ }
53
+ catch {
54
+ throw new ConsoleError("invalid_configuration");
55
+ }
56
+ }
57
+ start(input) {
58
+ return this.#send("/api/cli/checks", input, localStartRequestSchema, localStartResponseSchema, LOCAL_START_BODY_LIMIT, { statuses: [201], header: LOCAL_START_EXECUTION_HEADER, context: "start" });
59
+ }
60
+ create(input) {
61
+ return this.#lifecycle({ ...input, action: "create" }, consoleCreatedSchema);
62
+ }
63
+ reserve(input) {
64
+ return this.#lifecycle({ ...input, action: "reserve" }, consoleReservationSchema);
65
+ }
66
+ status(reservationId) {
67
+ return this.#lifecycle({ action: "status", reservationId }, consoleStatusSchema);
68
+ }
69
+ heartbeat(reservationId) {
70
+ return this.#lifecycle({ action: "heartbeat", reservationId }, consoleStatusSchema);
71
+ }
72
+ fail(reservationId) {
73
+ return this.#lifecycle({ action: "fail", reservationId }, consoleStatusSchema);
74
+ }
75
+ intake(input) {
76
+ return this.#send("/api/cli/checks/local", input, localFindingsPayloadSchema, consoleIntakeResultSchema, LOCAL_FINDINGS_BODY_LIMIT, { statuses: [200, 201], context: "intake" });
77
+ }
78
+ #lifecycle(input, response) {
79
+ // Starting work has no saved results to retry; recovery keeps its saving guidance.
80
+ const context = input.action === "create" || input.action === "reserve" ? "start" : undefined;
81
+ return this.#send("/api/cli/relay-reservations", input, cliRelayReservationRequestSchema, response, RESERVATION_BODY_LIMIT, { context });
82
+ }
83
+ async #send(path, input, schema, responseSchema, limit, options = {}) {
84
+ let body;
85
+ const identities = {};
86
+ try {
87
+ const parsed = schema.parse(input);
88
+ for (const field of ["jobId", "checkId", "projectId", "reservationId"]) {
89
+ const value = parsed[field];
90
+ if (typeof value === "string" && !(options.context === "intake" && field === "reservationId")) {
91
+ identities[field] = value.toLowerCase();
92
+ }
93
+ }
94
+ body = Buffer.from(JSON.stringify(parsed), "utf8");
95
+ if (body.length > limit)
96
+ throw new ConsoleError("invalid_request", options.context);
97
+ }
98
+ catch {
99
+ throw new ConsoleError("invalid_request", options.context);
100
+ }
101
+ return new Promise((resolve, reject) => {
102
+ let settled = false;
103
+ const finish = (error, result) => {
104
+ if (settled)
105
+ return;
106
+ settled = true;
107
+ clearTimeout(timer);
108
+ if (error)
109
+ reject(error);
110
+ else
111
+ resolve(result);
112
+ };
113
+ const destination = new URL(path, this.#url);
114
+ const request = (destination.protocol === "https:" ? httpsRequest : httpRequest)(destination, {
115
+ method: "POST", agent: false, rejectUnauthorized: true, maxHeaderSize: 16 * 1024,
116
+ headers: { "x-counterfact-cli-version": CLI_VERSION, authorization: `Bearer ${this.#token}`, "content-type": "application/json", accept: "application/json",
117
+ "content-length": body.length, "accept-encoding": "identity",
118
+ ...(options.header ? { [options.header.name]: options.header.value } : {}) },
119
+ }, response => {
120
+ const status = response.statusCode ?? 0;
121
+ const declared = response.headers["content-length"];
122
+ const contentType = response.headers["content-type"] ?? "";
123
+ if (status >= 300 && status < 400 || !/^application\/json(?:\s*;|$)/i.test(contentType)
124
+ || response.headers["content-encoding"] && response.headers["content-encoding"] !== "identity"
125
+ || declared && (!/^\d+$/.test(declared) || Number(declared) > RESPONSE_LIMIT)) {
126
+ finish(new ConsoleError());
127
+ response.destroy();
128
+ return;
129
+ }
130
+ let size = 0;
131
+ const chunks = [];
132
+ response.on("data", (chunk) => {
133
+ size += chunk.length;
134
+ if (size > RESPONSE_LIMIT) {
135
+ finish(new ConsoleError());
136
+ response.destroy();
137
+ }
138
+ else
139
+ chunks.push(chunk);
140
+ });
141
+ response.on("error", () => finish(new ConsoleError()));
142
+ response.on("aborted", () => finish(new ConsoleError()));
143
+ response.on("end", () => {
144
+ try {
145
+ const decoded = JSON.parse(Buffer.concat(chunks).toString("utf8"));
146
+ if (!(options.statuses ?? [200]).includes(status)) {
147
+ const code = decoded && typeof decoded === "object" && "code" in decoded ? decoded.code : undefined;
148
+ throw new ConsoleError(typeof code === "string" && Object.hasOwn(messages, code) ? code : "unavailable");
149
+ }
150
+ const result = responseSchema.parse(decoded);
151
+ for (const [field, expected] of Object.entries(identities)) {
152
+ if (result[field] !== expected)
153
+ throw new ConsoleError();
154
+ }
155
+ finish(undefined, result);
156
+ }
157
+ catch (error) {
158
+ finish(error instanceof ConsoleError ? error : new ConsoleError());
159
+ }
160
+ });
161
+ });
162
+ // Use one wall-clock deadline: an idle timeout alone lets a trickling response run forever.
163
+ const timer = setTimeout(() => { finish(new ConsoleError()); request.destroy(); }, this.#timeoutMs);
164
+ request.on("error", () => finish(new ConsoleError()));
165
+ request.end(body);
166
+ }).catch(error => { throw new ConsoleError(error instanceof ConsoleError ? error.code : "unavailable", options.context); });
167
+ }
168
+ }
@@ -0,0 +1,5 @@
1
+ /** Whether a URL hostname names this machine, so development trust and plain HTTP never leave it. */
2
+ export function isLoopbackHost(hostname) {
3
+ const host = hostname.replace(/^\[|\]$/g, "");
4
+ return host === "localhost" || host === "::1" || /^127\.\d+\.\d+\.\d+$/.test(host);
5
+ }
@@ -0,0 +1,70 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { executionRequest } from "../analysis-core/contracts/requests.js";
3
+ export class ModelConfigurationError extends Error {
4
+ name = "ModelConfigurationError";
5
+ }
6
+ export function relayModel(options) {
7
+ const { client, bundleVersion, reservationId, reservationToken, onUsage } = options;
8
+ if (!bundleVersion.trim() || !reservationId.trim() || !reservationToken?.trim() || reservationToken.length > 256)
9
+ throw new ModelConfigurationError("Invalid relay configuration");
10
+ const identities = new Map();
11
+ const originals = new WeakMap();
12
+ let callCount = 0;
13
+ function view(retry) {
14
+ return {
15
+ settleStartedOnError: true,
16
+ withRetry: view,
17
+ async execute(request, execution = {}) {
18
+ options.signal?.throwIfAborted();
19
+ execution.signal?.throwIfAborted();
20
+ let snapshot;
21
+ let inputBytes;
22
+ try {
23
+ // Match JSON transport semantics, including omitted optional data fields.
24
+ snapshot = executionRequest.parse(JSON.parse(JSON.stringify(request)));
25
+ inputBytes = Buffer.byteLength(JSON.stringify(snapshot.inputs));
26
+ }
27
+ catch {
28
+ throw new Error("Invalid relay request data");
29
+ }
30
+ execution.onSend?.(inputBytes);
31
+ const key = createHash("sha256").update(JSON.stringify(snapshot)).digest("hex");
32
+ const matching = identities.get(key);
33
+ let identity;
34
+ if (retry > 0) {
35
+ const original = originals.get(request);
36
+ const candidates = original ? [...original].filter(id => matching?.has(id)) : [...matching ?? []];
37
+ if (candidates.length !== 1)
38
+ throw new Error("Ambiguous relay call identity");
39
+ identity = candidates[0];
40
+ }
41
+ else {
42
+ if (callCount >= 4096)
43
+ throw new Error("Relay call limit reached");
44
+ identity = randomUUID();
45
+ callCount++;
46
+ identities.set(key, new Set([...matching ?? [], identity]));
47
+ originals.set(request, new Set([...originals.get(request) ?? [], identity]));
48
+ }
49
+ let reply;
50
+ try {
51
+ const signals = [options.signal, execution.signal].filter((signal) => !!signal);
52
+ reply = await client.call({ ...snapshot, bundleVersion, reservationId, reservationToken, callId: identity }, { signal: signals.length ? AbortSignal.any(signals) : undefined });
53
+ }
54
+ catch {
55
+ throw new Error("Relay request could not finish");
56
+ }
57
+ if (!reply.ok)
58
+ throw new Error(`Relay refused the request: ${reply.code}`);
59
+ try {
60
+ onUsage?.({ ...reply.usage, template: snapshot.template, model: snapshot.model });
61
+ }
62
+ catch {
63
+ // A failed counter must not discard an answer or expose the sink's error text.
64
+ }
65
+ return reply.answer;
66
+ },
67
+ };
68
+ }
69
+ return view(0);
70
+ }
@@ -0,0 +1,149 @@
1
+ import { constants, verify, X509Certificate } from "node:crypto";
2
+ import { z } from "zod";
3
+ import { GOOGLE_ATTESTATION_ISSUER, MAX_ATTESTATION_BYTES, PRODUCTION_EVIDENCE_VERSION, RELAY_CHANNEL_AUDIENCE, attestationChallenge, evidenceBinding, googleAttestationClaims, observedAttestationClaims, productionEvidence } from "../contract/messages.js";
4
+ import { decodeBase64url } from "../transport/base64url.js";
5
+ import { isApprovedHardware } from "./trust/hardware.js";
6
+ import { AttestationError } from "./attestation.js";
7
+ import { validateCertificatePath } from "./certificate-path.js";
8
+ import { validateProductionTrust, validateWorkloadPolicy } from "./trust/policy.js";
9
+ /** A published client needs every trust constraint configured before it can connect to production. */
10
+ export function assertProductionTrustConfigured(value) {
11
+ try {
12
+ validateProductionTrust(value);
13
+ }
14
+ catch {
15
+ throw new Error("Production trust is not configured for a CLI release.");
16
+ }
17
+ }
18
+ const ISSUER_CLOCK_SKEW_SECONDS = 60;
19
+ const headerSchema = z.object({
20
+ alg: z.literal("RS256"), typ: z.literal("JWT").optional(), kid: z.string().max(256).optional(),
21
+ x5c: z.array(z.string().min(1).max(8192)).min(1).max(5),
22
+ }).strict();
23
+ /** Both peers supply bytes from their actual TLS socket; the transport owns freshness and socket reuse. */
24
+ export function channelEvidenceBinding(binding) {
25
+ try {
26
+ attestationChallenge.parse({ version: PRODUCTION_EVIDENCE_VERSION, challenge: binding.challenge });
27
+ if (!(binding.exportedKeyMaterial instanceof Uint8Array) || binding.exportedKeyMaterial.byteLength !== 32)
28
+ throw new Error();
29
+ return { audience: RELAY_CHANNEL_AUDIENCE,
30
+ nonces: [Buffer.from(binding.exportedKeyMaterial).toString("base64url"), binding.challenge] };
31
+ }
32
+ catch {
33
+ throw new AttestationError("Invalid production attestation channel binding");
34
+ }
35
+ }
36
+ /** The caller's binding comes from the channel/health/action owner, never from the token being verified. */
37
+ export async function verifyProductionEvidence(token, binding, trust, now = Math.floor(Date.now() / 1000)) {
38
+ let validated;
39
+ try {
40
+ validated = validateProductionTrust(trust);
41
+ }
42
+ catch {
43
+ throw new AttestationError("Malformed production attestation evidence");
44
+ }
45
+ const verified = await verifyEvidence(token, binding, validated, now);
46
+ if (!validated.imageDigests.includes(verified.imageDigest))
47
+ throw new AttestationError("Invalid production attestation claims or binding");
48
+ return verified;
49
+ }
50
+ export async function verifyWorkloadEvidence(token, binding, policy, now = Math.floor(Date.now() / 1000)) {
51
+ let validated;
52
+ try {
53
+ validated = validateWorkloadPolicy(policy);
54
+ }
55
+ catch {
56
+ throw new AttestationError("Malformed production attestation evidence");
57
+ }
58
+ return verifyEvidence(token, binding, validated, now);
59
+ }
60
+ async function authenticate(token, binding, roots, now) {
61
+ try {
62
+ productionEvidence.parse({ version: PRODUCTION_EVIDENCE_VERSION, token });
63
+ if (!Number.isSafeInteger(now))
64
+ throw new Error();
65
+ evidenceBinding.parse(binding);
66
+ const [encodedHeader, encodedPayload, signature] = token.split(".");
67
+ const header = headerSchema.parse(JSON.parse(decodeBase64url(encodedHeader).toString("utf8")));
68
+ const payload = decodeBase64url(encodedPayload);
69
+ const chain = header.x5c.map(encoded => {
70
+ const der = Buffer.from(encoded, "base64");
71
+ if (der.toString("base64") !== encoded)
72
+ throw new Error();
73
+ const cert = new X509Certificate(der);
74
+ if (!cert.raw.equals(der))
75
+ throw new Error();
76
+ return cert;
77
+ });
78
+ const key = await validateCertificatePath(chain, roots, now);
79
+ if (!verify("RSA-SHA256", Buffer.from(`${encodedHeader}.${encodedPayload}`), { key, padding: constants.RSA_PKCS1_PADDING }, decodeBase64url(signature))) {
80
+ throw new AttestationError("Invalid production attestation signature");
81
+ }
82
+ return payload;
83
+ }
84
+ catch (error) {
85
+ if (error instanceof AttestationError)
86
+ throw error;
87
+ throw new AttestationError("Malformed production attestation evidence");
88
+ }
89
+ }
90
+ function validateClaimsBinding(claims, binding, now) {
91
+ // The challenge bounds freshness; issuer clock drift gets a small allowance without extending expiry.
92
+ if (claims.exp <= now || claims.iat > now + ISSUER_CLOCK_SKEW_SECONDS ||
93
+ claims.nbf > now + ISSUER_CLOCK_SKEW_SECONDS || claims.exp <= claims.iat)
94
+ throw new Error();
95
+ const nonces = typeof claims.eat_nonce === "string" ? [claims.eat_nonce] : claims.eat_nonce;
96
+ if (claims.aud !== binding.audience || nonces.length !== binding.nonces.length ||
97
+ nonces.some((nonce, index) => nonce !== binding.nonces[index]))
98
+ throw new Error();
99
+ }
100
+ /**
101
+ * Reviewed roots authenticate probe facts; this result cannot substitute for workload admission.
102
+ * The default schema is the strict observed shape; a probe owner can supply its own shape for claims
103
+ * that Google omits on non-compliant workloads, but authentication and binding are always enforced here.
104
+ */
105
+ export async function observeGoogleEvidence(token, binding, roots, now = Math.floor(Date.now() / 1000), claimsSchema = observedAttestationClaims) {
106
+ const payload = await authenticate(token, binding, roots, now);
107
+ try {
108
+ const claims = claimsSchema.parse(JSON.parse(payload.toString("utf8")));
109
+ validateClaimsBinding(claims, binding, now);
110
+ return { claims };
111
+ }
112
+ catch {
113
+ throw new AttestationError("Invalid production attestation claims or binding");
114
+ }
115
+ }
116
+ async function verifyEvidence(token, binding, trust, now = Math.floor(Date.now() / 1000)) {
117
+ const payload = await authenticate(token, binding, trust.roots, now);
118
+ try {
119
+ // Claims cannot influence trust selection or the internal identity before authentication above.
120
+ const claims = googleAttestationClaims.parse(JSON.parse(payload.toString("utf8")));
121
+ validateClaimsBinding(claims, binding, now);
122
+ const { container, confidential_space: space } = claims.submods;
123
+ if (!trust.softwareVersions.includes(claims.swversion[0]) ||
124
+ !isApprovedHardware(claims.hwmodel, trust.hardwareModels) || !trust.subjects.includes(claims.sub) ||
125
+ claims.google_service_accounts.some(account => !trust.serviceAccounts.includes(account)) ||
126
+ !space.support_attributes.includes("STABLE") || !space.support_attributes.includes("USABLE") ||
127
+ space.support_attributes.some(attribute => !["STABLE", "USABLE"].includes(attribute)))
128
+ throw new Error();
129
+ if (!container.image_signatures.some(signature => trust.imageSigningKeyIds.includes(signature.key_id)) ||
130
+ container.image_signatures.some(signature => Buffer.from(signature.signature, "base64").toString("base64") !== signature.signature))
131
+ throw new Error();
132
+ return { iss: GOOGLE_ATTESTATION_ISSUER, imageDigest: container.image_digest, iat: claims.iat, exp: claims.exp };
133
+ }
134
+ catch {
135
+ throw new AttestationError("Invalid production attestation claims or binding");
136
+ }
137
+ }
138
+ export async function verifyProductionAttestation(evidence, binding, trust, now = Math.floor(Date.now() / 1000)) {
139
+ return verifyProductionEvidence(evidenceToken(evidence), channelEvidenceBinding(binding), trust, now);
140
+ }
141
+ export async function verifyWorkloadAttestation(evidence, binding, policy, now = Math.floor(Date.now() / 1000)) {
142
+ return verifyWorkloadEvidence(evidenceToken(evidence), channelEvidenceBinding(binding), policy, now);
143
+ }
144
+ function evidenceToken(evidence) {
145
+ const parsed = productionEvidence.safeParse(evidence);
146
+ if (!parsed.success || parsed.data.token.length > MAX_ATTESTATION_BYTES)
147
+ throw new AttestationError("Malformed production attestation evidence");
148
+ return parsed.data.token;
149
+ }
@@ -0,0 +1,21 @@
1
+ // Target and preserved rollback hardware are chosen only here, and callers never accept both together; rollback keeps AMD SEV on its original machine.
2
+ const target = Object.freeze({ hwmodel: "GCP_INTEL_TDX", computeType: "TDX", machine: "c3-standard-4",
3
+ confidentialImage: "projects/confidential-space-images/global/images/confidential-space-260800" });
4
+ const rollback = Object.freeze({ hwmodel: "GCP_AMD_SEV", computeType: "SEV", machine: "n2d-standard-2",
5
+ confidentialImage: target.confidentialImage });
6
+ export const hardwareModels = [target.hwmodel, rollback.hwmodel];
7
+ export function confidentialHardware(profile = "rollback") {
8
+ if (profile === "target")
9
+ return target;
10
+ if (profile === "rollback")
11
+ return rollback;
12
+ throw new Error("Invalid confidential hardware policy");
13
+ }
14
+ export function isApprovedHardware(model, approved) {
15
+ return approved.length === 1 && hardwareModels.some(value => value === model) && approved[0] === model;
16
+ }
17
+ export function requireSealedRootHardware(config, policy) {
18
+ if (config.hardwareProfile !== "target" || config.confidentialImage !== target.confidentialImage ||
19
+ policy && !isApprovedHardware(target.hwmodel, policy.hardwareModels))
20
+ throw new Error("Sealed root requires reviewed target hardware and host image");
21
+ }
@@ -0,0 +1,42 @@
1
+ import { X509Certificate } from "node:crypto";
2
+ import { z } from "zod";
3
+ import { hardwareModels } from "./hardware.js";
4
+ export const workloadZones = ["us-central1-a", "us-central1-b", "us-central1-c", "us-central1-f"];
5
+ const values = (item) => z.array(item).min(1).max(8).refine(list => new Set(list).size === list.length);
6
+ /** The one rule for an acceptable Google root: a self-signed CA, RSA of at least 2048 bits, valid at `now`. */
7
+ export function acceptableGoogleRoot(cert, now = Date.now()) {
8
+ return cert.ca && cert.checkIssued(cert) && cert.verify(cert.publicKey) &&
9
+ cert.publicKey.asymmetricKeyType === "rsa" && (cert.publicKey.asymmetricKeyDetails?.modulusLength ?? 0) >= 2048 &&
10
+ Date.parse(cert.validFrom) <= now && Date.parse(cert.validTo) > now;
11
+ }
12
+ const root = z.string().max(16384).refine(pem => {
13
+ try {
14
+ const cert = new X509Certificate(pem);
15
+ return pem.trim() === cert.toString().trim() && acceptableGoogleRoot(cert);
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ });
21
+ // Fixed security claims remain owned by the native token contract. This policy selects reviewed allow-lists.
22
+ export const workloadPolicy = z.object({
23
+ version: z.literal(1), roots: values(root),
24
+ softwareVersions: values(z.string().regex(/^[0-9]{6,8}$/)),
25
+ hardwareModels: z.array(z.enum(hardwareModels)).length(1),
26
+ serviceAccounts: values(z.string().regex(/^[a-z][a-z0-9-]*@[a-z][a-z0-9-]*\.iam\.gserviceaccount\.com$/)),
27
+ subjects: values(z.string().regex(/^https:\/\/www\.googleapis\.com\/compute\/v1\/projects\/[a-z][a-z0-9-]*\/zones\/[a-z0-9-]+\/instances\/[a-z0-9][a-z0-9-]*$/)
28
+ .refine(subject => workloadZones.some(zone => subject.includes(`/zones/${zone}/`)))),
29
+ imageSigningKeyIds: values(z.string().regex(/^[0-9a-f]{64}$/)),
30
+ }).strict();
31
+ const productionTrust = workloadPolicy.extend({
32
+ imageDigests: z.array(z.string().regex(/^sha256:[0-9a-f]{64}$/)).min(1),
33
+ });
34
+ export function validateWorkloadPolicy(value) {
35
+ return workloadPolicy.parse(value);
36
+ }
37
+ export function workloadPolicyBytes(value) {
38
+ return `${JSON.stringify(validateWorkloadPolicy(value), null, 2)}\n`;
39
+ }
40
+ export function validateProductionTrust(value) {
41
+ return productionTrust.parse(value);
42
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "version": 1,
3
+ "roots": [
4
+ "-----BEGIN CERTIFICATE-----\n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\nuyT5CnJulxSC5CT1\n-----END CERTIFICATE-----"
5
+ ],
6
+ "softwareVersions": [
7
+ "260800"
8
+ ],
9
+ "hardwareModels": [
10
+ "GCP_INTEL_TDX"
11
+ ],
12
+ "serviceAccounts": [
13
+ "relay-runtime@hosted-web-504419.iam.gserviceaccount.com"
14
+ ],
15
+ "subjects": [
16
+ "https://www.googleapis.com/compute/v1/projects/hosted-web-504419/zones/us-central1-a/instances/relay-vm",
17
+ "https://www.googleapis.com/compute/v1/projects/hosted-web-504419/zones/us-central1-a/instances/relay-staging-vm"
18
+ ],
19
+ "imageSigningKeyIds": [
20
+ "47caa7c1d5184e83548e604e4d74e20ffa3d4682031abfe826267a96e15f7fda"
21
+ ],
22
+ "imageDigests": [
23
+ "sha256:1d7a87dcb421afffba20a563e478801972b48fd9569bfb9377aac29e6b589e79"
24
+ ]
25
+ }