@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,31 @@
1
+ import { spawn } from "node:child_process";
2
+ import process from "node:process";
3
+ import { fileURLToPath, URL } from "node:url";
4
+ export { readTelemetrySettings } from "./policy.js";
5
+ export const disabledSetupPings = Object.freeze({ ping() { }, link() { } });
6
+ export const DEFAULT_CONSOLE_URL = "https://app.counterfactlabs.com";
7
+ export function createSetupPings(input, io = {}) {
8
+ if (!input.settings.enabled)
9
+ return disabledSetupPings;
10
+ const { homeDir, origin } = input;
11
+ let token;
12
+ const dispatch = (operation) => {
13
+ try {
14
+ const child = (io.spawn ?? spawn)(process.execPath, [fileURLToPath(new URL("./worker.js", import.meta.url))], {
15
+ cwd: "/", env: {}, detached: true, stdio: ["pipe", "ignore", "ignore"],
16
+ });
17
+ child.on("error", () => { });
18
+ const pipe = child.stdin;
19
+ pipe?.on("error", () => { });
20
+ child.unref();
21
+ pipe?.unref();
22
+ // Tokens stay in this private channel, never in argv or the child environment.
23
+ pipe?.end(JSON.stringify({ ...operation, token, homeDir, origin, ...(io.allowLoopback ? { allowLoopback: true } : {}) }));
24
+ }
25
+ catch { /* Reporting must never change a command's result. */ }
26
+ };
27
+ return {
28
+ ping(step) { dispatch({ kind: "ping", step }); },
29
+ link(value) { token = value; dispatch({ kind: "link" }); },
30
+ };
31
+ }
@@ -0,0 +1,2 @@
1
+ // Generated from counterfact_saas/src/lib/funnel/steps.ts. Do not edit by hand.
2
+ export const SETUP_PING_STEPS = ["mcp_started", "claude_allow_rule_found", "agent_sign_in_started", "first_analysis_started"];
@@ -0,0 +1,3 @@
1
+ export function readTelemetrySettings(env) {
2
+ return Object.freeze({ enabled: env.CF_TELEMETRY !== "0" && env.DO_NOT_TRACK !== "1" && env.CI === undefined });
3
+ }
@@ -0,0 +1,261 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { randomUUID } from "node:crypto";
3
+ import { constants } from "node:fs";
4
+ import { link, lstat, mkdir, open, unlink } from "node:fs/promises";
5
+ import { isAbsolute, join } from "node:path";
6
+ import { setTimeout } from "node:timers/promises";
7
+ import { SETUP_PING_STEPS } from "./contract.js";
8
+ const idName = "install-id";
9
+ const linkName = "link.attempted";
10
+ const temporaryName = ".install.tmp";
11
+ const stepName = (step) => `${step}.attempted`;
12
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
13
+ const hasCode = (error, code) => error?.code === code;
14
+ const sameSlot = (left, right) => left.dev === right.dev && left.ino === right.ino
15
+ && left.mtimeMs === right.mtimeMs && left.size === right.size;
16
+ class IncompleteStateFile extends Error {
17
+ }
18
+ async function stateDirectory(homeDir, create) {
19
+ if (!isAbsolute(homeDir) || !(await lstat(homeDir)).isDirectory())
20
+ throw new Error("Invalid home");
21
+ let directory = homeDir;
22
+ for (const part of [".config", "counterfact-labs", "setup-pings"]) {
23
+ directory = join(directory, part);
24
+ if (create) {
25
+ try {
26
+ await mkdir(directory, { mode: 0o700 });
27
+ }
28
+ catch (error) {
29
+ if (!hasCode(error, "EEXIST"))
30
+ throw error;
31
+ }
32
+ }
33
+ const info = await lstat(directory);
34
+ if (!info.isDirectory())
35
+ throw new Error("Invalid state directory");
36
+ if (part === "setup-pings" && (info.mode & 0o777) !== 0o700)
37
+ throw new Error("State directory is not private");
38
+ }
39
+ return directory;
40
+ }
41
+ async function readPrivateFile(path, size) {
42
+ let file;
43
+ try {
44
+ file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
45
+ }
46
+ catch (error) {
47
+ if (hasCode(error, "ENOENT"))
48
+ return undefined;
49
+ throw error;
50
+ }
51
+ try {
52
+ const info = await file.stat();
53
+ if (!info.isFile() || (info.mode & 0o777) !== 0o600)
54
+ throw new Error("Invalid state file");
55
+ if (info.size < size)
56
+ throw new IncompleteStateFile("Incomplete state file");
57
+ if (info.size > size)
58
+ throw new Error("Invalid state size");
59
+ // Bound reads even if another process changes the file after stat.
60
+ const buffer = Buffer.alloc(size + 1);
61
+ const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
62
+ if (bytesRead < size)
63
+ throw new IncompleteStateFile("Incomplete state size");
64
+ if (bytesRead > size)
65
+ throw new Error("Invalid state size");
66
+ return buffer.subarray(0, bytesRead).toString("utf8");
67
+ }
68
+ finally {
69
+ await file.close();
70
+ }
71
+ }
72
+ async function readIdentity(directory) {
73
+ for (let count = 0; count < 25; count++) {
74
+ try {
75
+ const value = await readPrivateFile(join(directory, idName), 36);
76
+ if (value !== undefined && !uuid.test(value))
77
+ throw new Error("Invalid installation ID");
78
+ return value;
79
+ }
80
+ catch (error) {
81
+ if (!(error instanceof IncompleteStateFile))
82
+ throw error;
83
+ // A delayed stale remover may let a publisher link another writer's unfinished slot.
84
+ await setTimeout(10);
85
+ }
86
+ }
87
+ throw new Error("Incomplete installation ID publication");
88
+ }
89
+ async function readMarkers(directory) {
90
+ const steps = [];
91
+ for (const step of SETUP_PING_STEPS) {
92
+ if (await readPrivateFile(join(directory, stepName(step)), 0) !== undefined)
93
+ steps.push(step);
94
+ }
95
+ const linked = await readPrivateFile(join(directory, linkName), 0) !== undefined;
96
+ return { steps, linked };
97
+ }
98
+ async function publishIdentity(directory) {
99
+ const temporary = join(directory, temporaryName);
100
+ for (let count = 0; count < 25; count++) {
101
+ const winner = await readIdentity(directory);
102
+ if (winner)
103
+ return winner;
104
+ let file;
105
+ try {
106
+ file = await open(temporary, "wx", 0o600);
107
+ }
108
+ catch (error) {
109
+ if (!hasCode(error, "EEXIST"))
110
+ throw error;
111
+ const winner = await readIdentity(directory);
112
+ if (winner)
113
+ return winner;
114
+ const slot = await readTemporary(directory);
115
+ // A worker can exit after writing the UUID but before publishing its link.
116
+ if (slot?.value)
117
+ await linkIdentity(directory);
118
+ else if (slot?.stale)
119
+ await removeTemporary(directory, slot);
120
+ else
121
+ await setTimeout(10);
122
+ continue;
123
+ }
124
+ try {
125
+ try {
126
+ await file.writeFile(randomUUID(), "utf8");
127
+ }
128
+ finally {
129
+ await file.close();
130
+ }
131
+ await linkIdentity(directory);
132
+ const installId = await readIdentity(directory);
133
+ // A delayed stale-slot cleanup can unlink our file before the link is made.
134
+ if (installId)
135
+ return installId;
136
+ }
137
+ finally {
138
+ await clearTemporary(directory);
139
+ }
140
+ }
141
+ throw new Error("Incomplete installation ID publication");
142
+ }
143
+ async function linkIdentity(directory) {
144
+ // A hard link publishes only complete bytes and cannot overwrite a winner.
145
+ try {
146
+ await link(join(directory, temporaryName), join(directory, idName));
147
+ }
148
+ catch (error) {
149
+ // A reader may remove the slot after observing the already published ID.
150
+ if (!hasCode(error, "EEXIST") && !hasCode(error, "ENOENT"))
151
+ throw error;
152
+ }
153
+ }
154
+ async function readTemporary(directory) {
155
+ const temporary = join(directory, temporaryName);
156
+ for (let count = 0; count < 25; count++) {
157
+ try {
158
+ const info = await lstat(temporary);
159
+ if (!info.isFile() || (info.mode & 0o777) !== 0o600)
160
+ throw new Error("Invalid publication slot");
161
+ let contents;
162
+ try {
163
+ contents = info.size === 36 ? await readPrivateFile(temporary, 36) : undefined;
164
+ }
165
+ catch (error) {
166
+ // A stale slot may have become a new writer's incomplete file before open.
167
+ if (!sameSlot(info, await lstat(temporary)))
168
+ continue;
169
+ throw error;
170
+ }
171
+ // This exceeds the worker's hard lifetime, so an incomplete writer cannot still be active.
172
+ return { value: contents && uuid.test(contents) ? contents : undefined, stale: Date.now() - info.mtimeMs > 5000,
173
+ info };
174
+ }
175
+ catch (error) {
176
+ if (hasCode(error, "ENOENT"))
177
+ return undefined;
178
+ throw error;
179
+ }
180
+ }
181
+ throw new Error("Installation ID publication keeps changing");
182
+ }
183
+ async function clearTemporary(directory) {
184
+ for (let count = 0; count < 25; count++) {
185
+ const slot = await readTemporary(directory);
186
+ if (!slot)
187
+ return;
188
+ // A concurrent creator may still be finishing its slot after an ID was published.
189
+ if (!slot.value && !slot.stale) {
190
+ await setTimeout(10);
191
+ continue;
192
+ }
193
+ if (await removeTemporary(directory, slot))
194
+ return;
195
+ }
196
+ throw new Error("Incomplete publication slot");
197
+ }
198
+ async function removeTemporary(directory, slot) {
199
+ try {
200
+ const temporary = join(directory, temporaryName);
201
+ const current = await lstat(temporary);
202
+ // Another recovering worker may already have replaced the stale slot.
203
+ if (!sameSlot(current, slot.info))
204
+ return false;
205
+ await unlink(temporary);
206
+ }
207
+ catch (error) {
208
+ if (!hasCode(error, "ENOENT"))
209
+ throw error;
210
+ }
211
+ return true;
212
+ }
213
+ async function createMarker(directory, name) {
214
+ try {
215
+ const file = await open(join(directory, name), "wx", 0o600);
216
+ await file.close();
217
+ return true;
218
+ }
219
+ catch (error) {
220
+ if (hasCode(error, "EEXIST"))
221
+ return false;
222
+ throw error;
223
+ }
224
+ }
225
+ export async function claimStep(homeDir, step) {
226
+ try {
227
+ if (!SETUP_PING_STEPS.includes(step))
228
+ return null;
229
+ const directory = await stateDirectory(homeDir, true);
230
+ const markers = await readMarkers(directory);
231
+ let installId = await readIdentity(directory);
232
+ // A missing ID beside old markers is damaged state, not a new installation.
233
+ if (!installId && (markers.steps.length || markers.linked))
234
+ return null;
235
+ installId ??= await publishIdentity(directory);
236
+ await clearTemporary(directory);
237
+ if (markers.steps.includes(step) || !await createMarker(directory, stepName(step)))
238
+ return null;
239
+ return { installId };
240
+ }
241
+ catch {
242
+ return null;
243
+ }
244
+ }
245
+ export async function claimLink(homeDir) {
246
+ try {
247
+ const directory = await stateDirectory(homeDir, false);
248
+ const markers = await readMarkers(directory);
249
+ const installId = await readIdentity(directory);
250
+ const step = markers.steps[0];
251
+ if (!installId || !step || markers.linked)
252
+ return null;
253
+ await clearTemporary(directory);
254
+ if (!await createMarker(directory, linkName))
255
+ return null;
256
+ return { installId, step };
257
+ }
258
+ catch {
259
+ return null;
260
+ }
261
+ }
@@ -0,0 +1,95 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { request as httpRequest } from "node:http";
3
+ import { request as httpsRequest } from "node:https";
4
+ import process from "node:process";
5
+ import { clearTimeout, setTimeout } from "node:timers";
6
+ import { fileURLToPath, URL } from "node:url";
7
+ import { SETUP_PING_STEPS } from "./contract.js";
8
+ import { claimLink, claimStep } from "./state.js";
9
+ import { DEFAULT_CONSOLE_URL } from "./client.js";
10
+ function parseOperation(raw) {
11
+ if (!raw || typeof raw !== "object")
12
+ return null;
13
+ const operation = raw;
14
+ if (typeof operation.homeDir !== "string" || typeof operation.origin !== "string")
15
+ return null;
16
+ if (operation.kind !== "ping" && operation.kind !== "link")
17
+ return null;
18
+ if (operation.kind === "ping" && !SETUP_PING_STEPS.includes(operation.step))
19
+ return null;
20
+ if (operation.kind === "link" && !operation.token)
21
+ return null;
22
+ if (operation.token !== undefined && (typeof operation.token !== "string" || !/^[\x21-\x7e]{1,4096}$/.test(operation.token)))
23
+ return null;
24
+ const base = new URL(operation.origin);
25
+ const production = base.origin === DEFAULT_CONSOLE_URL;
26
+ const loopback = operation.allowLoopback === true && base.protocol === "http:" && ["127.0.0.1", "[::1]"].includes(base.hostname);
27
+ if ((!production && !loopback) || base.username || base.password || base.search || base.hash)
28
+ return null;
29
+ const url = new URL(`${base.href.replace(/\/+$/, "")}/api/funnel/step`);
30
+ return { operation, url };
31
+ }
32
+ export async function runWorker(raw, io = {}) {
33
+ let request;
34
+ let httpDeadline;
35
+ let stopped = false;
36
+ let finish;
37
+ const finished = new Promise(resolve => { finish = () => { stopped = true; resolve(); }; });
38
+ const deadline = setTimeout(finish, 750);
39
+ void (async () => {
40
+ try {
41
+ const parsed = parseOperation(raw);
42
+ if (!parsed)
43
+ return;
44
+ const { operation, url } = parsed;
45
+ const claim = operation.kind === "ping"
46
+ ? await (io.claimStep ?? claimStep)(operation.homeDir, operation.step).then(value => value && { ...value, step: operation.step })
47
+ : await (io.claimLink ?? claimLink)(operation.homeDir);
48
+ if (!claim || stopped)
49
+ return;
50
+ const body = JSON.stringify({ installId: claim.installId, step: claim.step });
51
+ const headers = { "content-type": "application/json", "content-length": Buffer.byteLength(body) };
52
+ if (operation.token)
53
+ headers.authorization = `Bearer ${operation.token}`;
54
+ const send = io.request ?? (url.protocol === "https:" ? httpsRequest : httpRequest);
55
+ request = send(url, { method: "POST", headers, agent: false }, response => {
56
+ // Status is final: no redirects, response body, retries or server text.
57
+ response.destroy();
58
+ finish();
59
+ });
60
+ request.on("error", finish);
61
+ // An absolute timer also covers DNS and TLS before a socket is ready.
62
+ httpDeadline = setTimeout(finish, 500);
63
+ request.end(body);
64
+ await finished;
65
+ }
66
+ catch { /* Disk and transport failures are deliberately silent. */ }
67
+ finally {
68
+ finish();
69
+ }
70
+ })();
71
+ await finished;
72
+ clearTimeout(deadline);
73
+ clearTimeout(httpDeadline);
74
+ request?.destroy();
75
+ }
76
+ async function main() {
77
+ // A hard process deadline also covers hung filesystem work and unread stdin.
78
+ const deadline = setTimeout(() => process.exit(0), 750);
79
+ try {
80
+ let input = "";
81
+ for await (const chunk of process.stdin) {
82
+ input += chunk;
83
+ if (Buffer.byteLength(input) > 16384)
84
+ return;
85
+ }
86
+ await runWorker(JSON.parse(input));
87
+ }
88
+ catch { /* No telemetry failure is printed. */ }
89
+ finally {
90
+ clearTimeout(deadline);
91
+ process.exit(0);
92
+ }
93
+ }
94
+ if (process.argv[1] === fileURLToPath(import.meta.url))
95
+ void main();
@@ -0,0 +1,9 @@
1
+ /** Node's decoder ignores invalid characters and trailing bits; signed tokens require canonical bytes. */
2
+ export function decodeBase64url(value) {
3
+ if (!/^[A-Za-z0-9_-]+$/.test(value))
4
+ throw new Error();
5
+ const bytes = Buffer.from(value, "base64url");
6
+ if (bytes.toString("base64url") !== value)
7
+ throw new Error();
8
+ return bytes;
9
+ }
@@ -0,0 +1,89 @@
1
+ /** Owns response bounds and cleanup for direct HTTP clients; callers own destinations and payload formats. */
2
+ export function requestBoundedResponse(start, limits) {
3
+ return new Promise((resolve, reject) => {
4
+ let req, response;
5
+ let done = false;
6
+ let timer;
7
+ const fail = () => {
8
+ if (done)
9
+ return;
10
+ done = true;
11
+ clearTimeout(timer);
12
+ req?.destroy();
13
+ response?.destroy();
14
+ reject(new Error("HTTP response failed"));
15
+ };
16
+ try {
17
+ for (const limit of [limits.maxBytes, limits.timeoutMs, limits.bodyTimeoutMs ?? limits.timeoutMs]) {
18
+ if (!Number.isSafeInteger(limit) || limit < 1)
19
+ throw new Error();
20
+ }
21
+ // This starts before dialing and never resets on data, so trickled bytes cannot prolong it.
22
+ timer = setTimeout(fail, limits.timeoutMs);
23
+ req = start(incoming => {
24
+ incoming.on("error", fail);
25
+ incoming.on("aborted", fail);
26
+ if (done) {
27
+ incoming.destroy();
28
+ return;
29
+ }
30
+ if (response) {
31
+ incoming.destroy();
32
+ fail();
33
+ return;
34
+ }
35
+ response = incoming;
36
+ try {
37
+ if (limits.bodyTimeoutMs !== undefined) {
38
+ clearTimeout(timer);
39
+ timer = setTimeout(fail, limits.bodyTimeoutMs);
40
+ }
41
+ const length = incoming.headers["content-length"];
42
+ const encoding = incoming.headers["content-encoding"];
43
+ if (encoding !== undefined && encoding !== "identity" ||
44
+ length !== undefined && (!/^\d+$/.test(length) || Number(length) > limits.maxBytes))
45
+ throw new Error();
46
+ limits.validate?.(incoming);
47
+ }
48
+ catch {
49
+ fail();
50
+ return;
51
+ }
52
+ const chunks = [];
53
+ let bytes = 0;
54
+ incoming.on("data", (chunk) => {
55
+ if (done)
56
+ return;
57
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
58
+ bytes += buffer.length;
59
+ if (bytes > limits.maxBytes) {
60
+ fail();
61
+ return;
62
+ }
63
+ chunks.push(buffer);
64
+ });
65
+ incoming.on("end", () => {
66
+ if (done)
67
+ return;
68
+ if (!incoming.complete) {
69
+ fail();
70
+ return;
71
+ }
72
+ done = true;
73
+ clearTimeout(timer);
74
+ resolve({ response: incoming, body: Buffer.concat(chunks) });
75
+ });
76
+ incoming.on("close", () => { if (!incoming.complete)
77
+ fail(); });
78
+ });
79
+ req.on("error", fail);
80
+ if (done)
81
+ req.destroy();
82
+ else
83
+ req.end(limits.requestBody);
84
+ }
85
+ catch {
86
+ fail();
87
+ }
88
+ });
89
+ }
@@ -0,0 +1,5 @@
1
+ import { RELAY_TLS_EXPORTER_LABEL } from "../contract/messages.js";
2
+ /** Each peer must derive these bytes independently from its actual socket. */
3
+ export function relayExportedKeyMaterial(socket) {
4
+ return socket.exportKeyingMaterial(32, RELAY_TLS_EXPORTER_LABEL, Buffer.alloc(0));
5
+ }
package/package.json CHANGED
@@ -1,16 +1,62 @@
1
1
  {
2
2
  "name": "@counterfact-labs/cli",
3
- "version": "0.1.2",
4
- "license": "UNLICENSED",
5
- "description": "Check an agent from your terminal or coding agent",
3
+ "version": "0.2.3",
6
4
  "type": "module",
7
- "bin": { "counterfact-labs": "dist/main.js" },
8
- "engines": { "node": ">=20" },
9
- "files": ["dist", "!dist/**/*.test.js", "!dist/test-helpers.js"],
5
+ "files": [
6
+ "dist/**/*.js",
7
+ "dist/gatekeeper/trust/production.json",
8
+ "bundle/sealed.json",
9
+ "bundle/client-release.json"
10
+ ],
11
+ "engines": {
12
+ "node": ">=22"
13
+ },
10
14
  "scripts": {
11
- "prebuild": "node scripts/copy-api-types.mjs",
12
- "build": "node scripts/build.mjs",
13
- "test": "npm run build && node --test dist/*.test.js dist/commands/*.test.js",
14
- "prepack": "npm run build"
15
- }
15
+ "lint": "eslint .",
16
+ "bundle:seal": "tsx bundle/seal/cli.ts",
17
+ "bundle:check": "tsx bundle/seal/cli.ts check",
18
+ "bench:replay": "tsx bench/replay.ts",
19
+ "bench:importer": "tsx bench/importer-coverage.ts",
20
+ "typecheck": "tsc --noEmit",
21
+ "test": "tsx --test ${TEST_SHARD:+--test-shard=$TEST_SHARD} --test-concurrency=4 --test-timeout=600000 \"core/**/*.test.ts\" \"adapters/**/*.test.ts\" \"cli/**/*.test.ts\" \"bench/**/*.test.ts\" \"bundle/**/*.test.ts\" \"contract/**/*.test.ts\" \"gatekeeper/**/*.test.ts\" \"relay/**/*.test.ts\" \"transport/**/*.test.ts\" \"packages/core/src/**/*.test.ts\" \"packages/core/*.test.ts\" \"packages/setup-pings/src/**/*.test.ts\" \"scripts/**/*.test.ts\"",
22
+ "build:core": "npm run build --workspace @counterfact-labs/analysis-core",
23
+ "build:setup-pings": "npm run build --workspace @counterfact-labs/setup-pings",
24
+ "prepare": "npm run build:core && npm run build:setup-pings",
25
+ "pretest": "npm run build:core && npm run build:setup-pings",
26
+ "release:core": "tsx scripts/release-core.ts",
27
+ "build:cli": "npm run build:setup-pings && tsx scripts/build-cli.ts",
28
+ "prepack": "npm run build:core && npm run build:cli",
29
+ "pack:integration": "tsx scripts/integration/pack.ts",
30
+ "release:cli": "tsx scripts/release-cli.ts"
31
+ },
32
+ "dependencies": {
33
+ "@anthropic-ai/sdk": "0.128.0",
34
+ "zod": "4.6.5",
35
+ "asn1js": "3.0.6",
36
+ "pkijs": "3.2.5"
37
+ },
38
+ "devDependencies": {
39
+ "@eslint/js": "9.39.5",
40
+ "@types/node": "24.19.0",
41
+ "esbuild": "0.28.2",
42
+ "eslint": "9.39.5",
43
+ "js-yaml": "4.3.2",
44
+ "tsx": "4.23.15",
45
+ "typescript": "6.0.3",
46
+ "typescript-eslint": "8.64.0",
47
+ "@counterfact-labs/analysis-core": "0.7.1",
48
+ "@counterfact-labs/setup-pings": "0.1.0"
49
+ },
50
+ "workspaces": [
51
+ "packages/*"
52
+ ],
53
+ "bin": {
54
+ "counterfact-labs": "dist/cli/main.js"
55
+ },
56
+ "exports": {
57
+ ".": "./dist/cli/main.js",
58
+ "./gatekeeper": "./dist/gatekeeper/client.js"
59
+ },
60
+ "license": "UNLICENSED",
61
+ "description": "Analyze an agent from your terminal or coding agent"
16
62
  }
package/LICENSE DELETED
@@ -1 +0,0 @@
1
- Copyright (c) 2026 Counterfact Labs, Inc. All rights reserved. This software is provided to access the Counterfact service and may be used only for that purpose. No other rights are granted.
package/dist/api-types.js DELETED
@@ -1,2 +0,0 @@
1
- // This file is copied into the command-line package before its build. Keep it self-contained.
2
- export const CLI_BODY_LIMIT = 2 * 1024 * 1024;