@counterfact-labs/cli 0.1.3 → 0.2.4

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 (186) hide show
  1. package/README.md +176 -42
  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 +144 -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 +62 -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 +26 -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/3CKW5JIF.js +1 -0
  39. package/dist/analysis-core/chunks/3GUKUEEJ.js +1 -0
  40. package/dist/analysis-core/chunks/3MTXB4MK.js +6 -0
  41. package/dist/analysis-core/chunks/4F6ZIHXU.js +1 -0
  42. package/dist/analysis-core/chunks/57M4OQJM.js +1 -0
  43. package/dist/analysis-core/chunks/6YCABS5T.js +1 -0
  44. package/dist/analysis-core/chunks/724WZMYW.js +2 -0
  45. package/dist/analysis-core/chunks/74FPPPIG.js +1 -0
  46. package/dist/analysis-core/chunks/7OYIBWX3.js +1 -0
  47. package/dist/analysis-core/chunks/D6AWHLRI.js +1 -0
  48. package/dist/analysis-core/chunks/DRGDIIFA.js +1 -0
  49. package/dist/analysis-core/chunks/E7TFIHC5.js +1 -0
  50. package/dist/analysis-core/chunks/EH3QQ3T4.js +1 -0
  51. package/dist/analysis-core/chunks/FSLWD2PE.js +1 -0
  52. package/dist/analysis-core/chunks/FTODBFQW.js +1 -0
  53. package/dist/analysis-core/chunks/HPIAEQHE.js +37 -0
  54. package/dist/analysis-core/chunks/HTLIRVP7.js +1 -0
  55. package/dist/analysis-core/chunks/IBCFGUHI.js +1 -0
  56. package/dist/analysis-core/chunks/IVDDV6CU.js +14 -0
  57. package/dist/analysis-core/chunks/J4AXGE5W.js +1 -0
  58. package/dist/analysis-core/chunks/JF4GYMOZ.js +1 -0
  59. package/dist/analysis-core/chunks/JKWTHRUR.js +1 -0
  60. package/dist/analysis-core/chunks/K5OTWBTT.js +22 -0
  61. package/dist/analysis-core/chunks/KU5DINLW.js +1 -0
  62. package/dist/analysis-core/chunks/MJGB25S5.js +1 -0
  63. package/dist/analysis-core/chunks/MXXNED6N.js +7 -0
  64. package/dist/analysis-core/chunks/NCWEOJVJ.js +6 -0
  65. package/dist/analysis-core/chunks/O652FQQH.js +1 -0
  66. package/dist/analysis-core/chunks/OFBJNHC6.js +1 -0
  67. package/dist/analysis-core/chunks/PYKAERZZ.js +1 -0
  68. package/dist/analysis-core/chunks/QDYMR6KS.js +1 -0
  69. package/dist/analysis-core/chunks/QMIXCT62.js +1 -0
  70. package/dist/analysis-core/chunks/QV2EOS7U.js +1 -0
  71. package/dist/analysis-core/chunks/SL27GMEN.js +1 -0
  72. package/dist/analysis-core/chunks/STZOC7LN.js +1 -0
  73. package/dist/analysis-core/chunks/T2SAP7OV.js +60 -0
  74. package/dist/analysis-core/chunks/T5TREZCQ.js +1 -0
  75. package/dist/analysis-core/chunks/T66VDFXF.js +1 -0
  76. package/dist/analysis-core/chunks/TDNTU4TF.js +1 -0
  77. package/dist/analysis-core/chunks/U5HOEVPE.js +1 -0
  78. package/dist/analysis-core/chunks/UEWL3GMQ.js +1 -0
  79. package/dist/analysis-core/chunks/V7BFUTM7.js +1 -0
  80. package/dist/analysis-core/chunks/VGYWI4X2.js +1 -0
  81. package/dist/analysis-core/chunks/Y7JGSUOA.js +1 -0
  82. package/dist/analysis-core/chunks/ZAA44GPI.js +1 -0
  83. package/dist/analysis-core/contracts/requests.js +1 -0
  84. package/dist/analysis-core/conventions/analysis-fit.js +1 -0
  85. package/dist/analysis-core/conventions/file-admission.js +1 -0
  86. package/dist/analysis-core/data/finding-types.js +1 -0
  87. package/dist/analysis-core/errors.js +1 -0
  88. package/dist/analysis-core/findings/presentation.js +1 -0
  89. package/dist/analysis-core/github/repo-key.js +1 -0
  90. package/dist/analysis-core/import/outside-loader-sources.js +1 -0
  91. package/dist/analysis-core/index.js +6 -0
  92. package/dist/analysis-core/llm/cache-policy.js +1 -0
  93. package/dist/analysis-core/llm/cost.js +1 -0
  94. package/dist/analysis-core/llm/execution.js +1 -0
  95. package/dist/analysis-core/llm/output-limit.js +1 -0
  96. package/dist/analysis-core/llm/slots.js +1 -0
  97. package/dist/analysis-core/llm/stable-json.js +1 -0
  98. package/dist/analysis-core/llm/structured-output-error.js +1 -0
  99. package/dist/analysis-core/map-limit.js +1 -0
  100. package/dist/analysis-core/requests/answer-formats.js +1 -0
  101. package/dist/analysis-core/requests/check-diagnostics.js +1 -0
  102. package/dist/analysis-core/requests/headline-request.js +1 -0
  103. package/dist/analysis-core/requests/instruction-text.js +1 -0
  104. package/dist/analysis-core/requests/local-plan.js +1 -0
  105. package/dist/analysis-core/requests/registry.js +1 -0
  106. package/dist/analysis-core/requests/schema-descriptions.js +1 -0
  107. package/dist/analysis-core/requests/shape-finding-input.js +1 -0
  108. package/dist/analysis-core/requests/template-version.js +1 -0
  109. package/dist/analysis-core/requests/tools.js +1 -0
  110. package/dist/analysis-core/settings.js +1 -0
  111. package/dist/bundle/seal/deserialise.js +75 -0
  112. package/dist/bundle/seal/encryption.js +25 -0
  113. package/dist/bundle/seal/metadata.js +69 -0
  114. package/dist/bundle/seal/open.js +38 -0
  115. package/dist/bundle/seal/planned-calls.js +20 -0
  116. package/dist/bundle/seal/signature.js +51 -0
  117. package/dist/cli/agent-folder.js +17 -0
  118. package/dist/cli/api-types.js +2 -0
  119. package/dist/cli/api.js +1 -0
  120. package/dist/{claude-code-permission.js → cli/claude-code-permission.js} +4 -3
  121. package/dist/cli/commands/analyze.js +75 -0
  122. package/dist/cli/commands/login.js +51 -0
  123. package/dist/cli/completion-recovery.js +18 -0
  124. package/dist/{context.js → cli/context.js} +8 -14
  125. package/dist/cli/development-relay.js +34 -0
  126. package/dist/cli/email-sign-in.js +56 -0
  127. package/dist/{git.js → cli/git.js} +12 -8
  128. package/dist/cli/intake-payload.js +18 -0
  129. package/dist/{main.js → cli/main.js} +23 -12
  130. package/dist/{mcp.js → cli/mcp.js} +109 -40
  131. package/dist/cli/output.js +10 -0
  132. package/dist/cli/relay-check.js +239 -0
  133. package/dist/cli/relay-preparation.js +33 -0
  134. package/dist/cli/sign-in-input.js +97 -0
  135. package/dist/{sign-in.js → cli/sign-in.js} +2 -1
  136. package/dist/cli/suggested-email.js +10 -0
  137. package/dist/cli/summary.js +30 -0
  138. package/dist/contract/cli-api-messages.js +5 -0
  139. package/dist/contract/cli-refusals.js +24 -0
  140. package/dist/contract/cli-version.js +1 -0
  141. package/dist/contract/console-url.js +8 -0
  142. package/dist/contract/console.js +131 -0
  143. package/dist/contract/email-sign-in-messages.js +18 -0
  144. package/dist/contract/messages.js +252 -0
  145. package/dist/contract/release-key.js +2 -0
  146. package/dist/contract/runtime-environment.js +37 -0
  147. package/dist/core/analysis/finding-quality/config.js +4 -0
  148. package/dist/core/analysis/run-to-end.js +55 -0
  149. package/dist/core/analysis/structural-run.js +16 -0
  150. package/dist/gatekeeper/attestation.js +67 -0
  151. package/dist/gatekeeper/certificate-algorithms.js +18 -0
  152. package/dist/gatekeeper/certificate-path.js +99 -0
  153. package/dist/gatekeeper/cli-api.js +174 -0
  154. package/dist/gatekeeper/client.js +76 -0
  155. package/dist/gatekeeper/connection-exchange.js +61 -0
  156. package/dist/gatekeeper/connection.js +140 -0
  157. package/dist/gatekeeper/console.js +175 -0
  158. package/dist/gatekeeper/loopback.js +5 -0
  159. package/dist/gatekeeper/model.js +70 -0
  160. package/dist/gatekeeper/production-attestation.js +149 -0
  161. package/dist/gatekeeper/trust/hardware.js +21 -0
  162. package/dist/gatekeeper/trust/policy.js +42 -0
  163. package/dist/gatekeeper/trust/production.json +25 -0
  164. package/dist/setup-pings/client.js +31 -0
  165. package/dist/setup-pings/contract.js +2 -0
  166. package/dist/setup-pings/policy.js +3 -0
  167. package/dist/setup-pings/state.js +261 -0
  168. package/dist/setup-pings/worker.js +95 -0
  169. package/dist/transport/base64url.js +9 -0
  170. package/dist/transport/http-response.js +89 -0
  171. package/dist/transport/tls-exporter.js +5 -0
  172. package/package.json +57 -11
  173. package/LICENSE +0 -1
  174. package/dist/api-types.js +0 -2
  175. package/dist/api.js +0 -97
  176. package/dist/commands/check.js +0 -104
  177. package/dist/commands/login.js +0 -9
  178. package/dist/files.js +0 -99
  179. package/dist/gitignore.js +0 -70
  180. package/dist/output.js +0 -23
  181. package/dist/secret-files.js +0 -5
  182. /package/dist/{browser.js → cli/browser.js} +0 -0
  183. /package/dist/{commands → cli/commands}/balance.js +0 -0
  184. /package/dist/{commands → cli/commands}/finding.js +0 -0
  185. /package/dist/{commands → cli/commands}/logout.js +0 -0
  186. /package/dist/{credentials.js → cli/credentials.js} +0 -0
@@ -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
+ }
@@ -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
+ }