@counterfact-labs/cli 0.2.6 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/bundle/client-release.json +1 -1
- package/dist/adapters/analysis-results.js +87 -0
- package/dist/adapters/combined-results.js +27 -0
- package/dist/adapters/completion-lock.js +95 -0
- package/dist/adapters/completion.js +132 -17
- package/dist/adapters/entry-files.js +129 -0
- package/dist/adapters/entry-packing.js +161 -0
- package/dist/adapters/entry-selection.js +57 -0
- package/dist/adapters/files.js +111 -5
- package/dist/adapters/git.js +10 -1
- package/dist/adapters/relay-plan.js +5 -7
- package/dist/adapters/release.js +4 -1
- package/dist/adapters/results.js +18 -3
- package/dist/adapters/selection.js +114 -0
- package/dist/adapters/settings.js +62 -4
- package/dist/adapters/source-relay-plan.js +7 -0
- package/dist/analysis-core/analysis/code-agents/skipped-files.js +1 -1
- package/dist/analysis-core/analysis/file-analysis-plan.js +1 -0
- package/dist/analysis-core/analysis/file-dependencies.js +1 -0
- package/dist/analysis-core/analysis/finding-activity-switch.js +1 -0
- package/dist/analysis-core/analysis/finding-identity.js +1 -0
- package/dist/analysis-core/analysis/finding-quality/policy.js +1 -0
- package/dist/analysis-core/analysis/folder-analysis-plan.js +1 -0
- package/dist/analysis-core/analysis/passes/full-file-passes.js +1 -0
- package/dist/analysis-core/analysis/project-architecture.js +1 -0
- package/dist/analysis-core/analysis/schemas.js +1 -1
- package/dist/analysis-core/chunks/2AASSZRO.js +35 -0
- package/dist/analysis-core/chunks/2YREYGOG.js +1 -0
- package/dist/analysis-core/chunks/33HRXJ5Q.js +1 -0
- package/dist/analysis-core/chunks/33IGHOYT.js +1 -0
- package/dist/analysis-core/chunks/4PT5C4IT.js +7 -0
- package/dist/analysis-core/chunks/4RXRAB5S.js +1 -0
- package/dist/analysis-core/chunks/4UDQBDDE.js +1 -0
- package/dist/analysis-core/chunks/{OPMA32A6.js → 5B5UCWD3.js} +1 -1
- package/dist/analysis-core/chunks/6YCABS5T.js +1 -0
- package/dist/analysis-core/chunks/CQQ4TTXE.js +1 -0
- package/dist/analysis-core/chunks/CTLHKFPH.js +19 -0
- package/dist/analysis-core/chunks/F54DOWKN.js +6 -0
- package/dist/analysis-core/chunks/FRXOQ2M6.js +3 -0
- package/dist/analysis-core/chunks/HCPO6QPU.js +1 -0
- package/dist/analysis-core/chunks/HHFDNY6V.js +14 -0
- package/dist/analysis-core/chunks/{3JWEFCTJ.js → HVQJKPQ4.js} +1 -1
- package/dist/analysis-core/chunks/INVTU73D.js +1 -0
- package/dist/analysis-core/chunks/JEWBPIQI.js +1 -0
- package/dist/analysis-core/chunks/JGK37AWN.js +1 -0
- package/dist/analysis-core/chunks/LFYTD7JF.js +1 -0
- package/dist/analysis-core/chunks/LMUXOKIM.js +1 -0
- package/dist/analysis-core/chunks/LOBIDUCA.js +1 -0
- package/dist/analysis-core/chunks/M5YWJY7P.js +1 -0
- package/dist/analysis-core/chunks/MGVIKA74.js +1 -0
- package/dist/analysis-core/chunks/MKYLK6DB.js +1 -0
- package/dist/analysis-core/chunks/MZOKZ3N6.js +1 -0
- package/dist/analysis-core/chunks/NXHVBJL4.js +1 -0
- package/dist/analysis-core/chunks/OKBK3JL3.js +1 -0
- package/dist/analysis-core/chunks/PBHHWFR6.js +1 -0
- package/dist/analysis-core/chunks/Q5WR6B2H.js +2 -0
- package/dist/analysis-core/chunks/QVXARKP7.js +1 -0
- package/dist/analysis-core/chunks/RQTNTIR4.js +1 -0
- package/dist/analysis-core/chunks/RUVMFAST.js +1 -0
- package/dist/analysis-core/chunks/S27VIQMZ.js +1 -0
- package/dist/analysis-core/chunks/{YDBO7P4X.js → S27Y2TP5.js} +1 -1
- package/dist/analysis-core/chunks/SJESFYBA.js +10 -0
- package/dist/analysis-core/chunks/SP6XFHNH.js +1 -0
- package/dist/analysis-core/chunks/VDUFPTDD.js +1 -0
- package/dist/analysis-core/chunks/W24SFYZM.js +67 -0
- package/dist/analysis-core/chunks/YTHF2E2S.js +1 -0
- package/dist/analysis-core/chunks/YWL52FVW.js +2 -0
- package/dist/analysis-core/contracts/analysis-selection.js +1 -0
- package/dist/analysis-core/contracts/analysis-size.js +1 -0
- package/dist/analysis-core/contracts/execution-request.js +1 -0
- package/dist/analysis-core/contracts/intake-finding.js +1 -0
- package/dist/analysis-core/contracts/model-call-identity.js +1 -0
- package/dist/analysis-core/contracts/module-resolution.js +1 -0
- package/dist/analysis-core/contracts/relay-analysis.js +1 -1
- package/dist/analysis-core/contracts/requests.js +1 -1
- package/dist/analysis-core/contracts/selection-paths.js +1 -0
- package/dist/analysis-core/conventions/analysis-fit.js +1 -1
- package/dist/analysis-core/conventions/file-admission.js +1 -1
- package/dist/analysis-core/conventions/read-limits.js +1 -0
- package/dist/analysis-core/import/calls/modules.js +1 -0
- package/dist/analysis-core/import/entry-selection.js +1 -0
- package/dist/analysis-core/import/outside-loader-sources.js +1 -1
- package/dist/analysis-core/index.js +3 -3
- package/dist/analysis-core/requests/answer-formats.js +1 -1
- package/dist/analysis-core/requests/local-plan.js +1 -1
- package/dist/analysis-core/requests/planned-limits.js +1 -0
- package/dist/analysis-core/requests/registry.js +1 -1
- package/dist/analysis-core/requests/template-version.js +1 -1
- package/dist/analysis-core/requests/tools.js +1 -1
- package/dist/analysis-core/settings.js +1 -1
- package/dist/analysis-core/stages/engine-entry.js +1 -1
- package/dist/bundle/seal/engine.js +1 -1
- package/dist/cli/analysis-selection.js +133 -0
- package/dist/cli/api.js +1 -1
- package/dist/cli/commands/analyze.js +82 -14
- package/dist/cli/commands/skipped.js +65 -0
- package/dist/cli/completion-delivery.js +63 -0
- package/dist/cli/completion-recovery.js +68 -14
- package/dist/cli/context.js +4 -2
- package/dist/cli/finding-display.js +102 -7
- package/dist/cli/finding-settings.js +3 -1
- package/dist/cli/intake-payload.js +26 -2
- package/dist/cli/main.js +37 -11
- package/dist/cli/mcp-failure.js +6 -5
- package/dist/cli/mcp.js +191 -26
- package/dist/cli/prior-completion.js +35 -0
- package/dist/cli/relay-check.js +260 -110
- package/dist/cli/relay-failure.js +32 -0
- package/dist/cli/relay-preparation.js +72 -21
- package/dist/cli/result-check.js +31 -12
- package/dist/cli/results-upload.js +131 -0
- package/dist/cli/saved-results.js +10 -0
- package/dist/cli/scope-choices.js +57 -0
- package/dist/cli/sign-in.js +2 -2
- package/dist/cli/skipped-files.js +22 -0
- package/dist/cli/summary.js +15 -1
- package/dist/contract/agent-choice.js +25 -2
- package/dist/contract/cli-api-messages.js +3 -0
- package/dist/contract/cli-refusals.js +39 -5
- package/dist/contract/cli-version.js +1 -1
- package/dist/contract/console-url.js +3 -0
- package/dist/contract/console.js +24 -40
- package/dist/contract/engine.js +1 -1
- package/dist/contract/messages.js +4 -3
- package/dist/contract/project-architecture.js +1 -0
- package/dist/contract/scope-choice.js +98 -0
- package/dist/contract/selection-metadata.js +9 -0
- package/dist/contract/selection-offer.js +40 -0
- package/dist/gatekeeper/cli-api.js +70 -4
- package/dist/gatekeeper/console.js +55 -28
- package/dist/gatekeeper/model.js +5 -23
- package/dist/gatekeeper/trust/policy.js +2 -1
- package/package.json +5 -3
- package/dist/analysis-core/analysis/source-notes/types.js +0 -1
- package/dist/analysis-core/chunks/2PSNOSJ2.js +0 -1
- package/dist/analysis-core/chunks/377MLK4M.js +0 -21
- package/dist/analysis-core/chunks/5DGPLXYB.js +0 -1
- package/dist/analysis-core/chunks/6B2HILSY.js +0 -1
- package/dist/analysis-core/chunks/6QY5BV5B.js +0 -14
- package/dist/analysis-core/chunks/6U2IR4XB.js +0 -1
- package/dist/analysis-core/chunks/DLPVYWHZ.js +0 -1
- package/dist/analysis-core/chunks/FOKDE64Y.js +0 -1
- package/dist/analysis-core/chunks/FSLWD2PE.js +0 -1
- package/dist/analysis-core/chunks/K2GNDX3W.js +0 -1
- package/dist/analysis-core/chunks/KTNA2KTN.js +0 -7
- package/dist/analysis-core/chunks/KWECQJ6X.js +0 -1
- package/dist/analysis-core/chunks/NIOS7U6C.js +0 -41
- package/dist/analysis-core/chunks/PECX3OXB.js +0 -1
- package/dist/analysis-core/chunks/PYKAERZZ.js +0 -1
- package/dist/analysis-core/chunks/QFY6A3MS.js +0 -66
- package/dist/analysis-core/chunks/SKVEBOGA.js +0 -1
- package/dist/analysis-core/chunks/STI4JE7L.js +0 -1
- package/dist/analysis-core/chunks/SVMEWGD3.js +0 -1
- package/dist/analysis-core/chunks/X7WZAA7S.js +0 -1
- package/dist/analysis-core/chunks/ZTOVBAG7.js +0 -1
- package/dist/analysis-core/data/finding-types.js +0 -1
package/dist/contract/console.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
import { inspectFixSchema, otherSiteSchema, rawFindingSchema } from "../analysis-core/analysis/schemas.js";
|
|
4
|
-
import { sourceNotesSchema } from "../analysis-core/analysis/source-notes/types.js";
|
|
5
|
-
import { parseAnalysisSize } from "../analysis-core/conventions/analysis-fit.js";
|
|
2
|
+
import { parseAnalysisSize } from "../analysis-core/contracts/analysis-size.js";
|
|
6
3
|
import { canonicalUuidSchema as uuid, matchingFindingNumbers, numberedFindingsSchema } from "./findings.js";
|
|
7
|
-
import {
|
|
4
|
+
import { publicReportSchema } from "../analysis-core/contracts/relay-analysis.js";
|
|
5
|
+
import { PLANNED_INPUT_BYTES } from "../analysis-core/requests/planned-limits.js";
|
|
6
|
+
import { projectArchitectureSchema } from "./project-architecture.js";
|
|
7
|
+
import { intakeFinding as localFindingSchema, nonblankText as text, RESULT_CHECK_LIMITS, validIntakeText } from "../analysis-core/contracts/intake-finding.js";
|
|
8
|
+
export { localFindingSchema };
|
|
9
|
+
import { selectionStartIdentitySchema } from "./selection-metadata.js";
|
|
8
10
|
export const id = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/);
|
|
9
11
|
const money = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
|
|
10
12
|
const count = (max) => z.number().int().positive().max(max);
|
|
@@ -13,13 +15,17 @@ export const LOCAL_START_EXECUTION_HEADER = { name: "x-counterfact-execution", v
|
|
|
13
15
|
export const LOCAL_START_BODY_LIMIT = 2 * 1024 * 1024;
|
|
14
16
|
export const ANALYSIS_SIZE_HEADER = "x-counterfact-analysis-size";
|
|
15
17
|
export const START_ATTEMPT_HEADER = "x-counterfact-start-attempt";
|
|
18
|
+
export const SELECTION_IDENTITY_HEADER = "x-counterfact-selection";
|
|
16
19
|
export const ANALYSIS_SIZE_HEADER_LIMIT = 256;
|
|
20
|
+
export const ADD_TO_ANALYSIS_HEADER = "x-counterfact-add-to-analysis";
|
|
17
21
|
/** Additive headers let older consoles keep accepting the unchanged, strict start body. */
|
|
18
22
|
export function serializeLocalStartMetadata(metadata) {
|
|
19
23
|
const size = JSON.stringify(parseAnalysisSize(metadata.analysisSize));
|
|
20
24
|
if (Buffer.byteLength(size, "utf8") > ANALYSIS_SIZE_HEADER_LIMIT)
|
|
21
25
|
throw new Error("Invalid analysis size header");
|
|
22
|
-
return { [ANALYSIS_SIZE_HEADER]: size, [START_ATTEMPT_HEADER]: uuid.parse(metadata.startAttempt)
|
|
26
|
+
return { [ANALYSIS_SIZE_HEADER]: size, [START_ATTEMPT_HEADER]: uuid.parse(metadata.startAttempt),
|
|
27
|
+
...(metadata.addToAnalysisId ? { [ADD_TO_ANALYSIS_HEADER]: uuid.parse(metadata.addToAnalysisId) } : {}),
|
|
28
|
+
...(metadata.selection === undefined ? {} : { [SELECTION_IDENTITY_HEADER]: JSON.stringify(selectionStartIdentitySchema.parse(metadata.selection)) }) };
|
|
23
29
|
}
|
|
24
30
|
/** Missing measurements preserve old clients; present measurements must be complete. */
|
|
25
31
|
export function parseLocalStartMetadata(headers) {
|
|
@@ -28,7 +34,10 @@ export function parseLocalStartMetadata(headers) {
|
|
|
28
34
|
return undefined;
|
|
29
35
|
if (Buffer.byteLength(raw, "utf8") > ANALYSIS_SIZE_HEADER_LIMIT)
|
|
30
36
|
throw new Error("Invalid analysis size header");
|
|
31
|
-
|
|
37
|
+
const selection = headers.get(SELECTION_IDENTITY_HEADER);
|
|
38
|
+
return { analysisSize: parseAnalysisSize(JSON.parse(raw)), startAttempt: uuid.parse(headers.get(START_ATTEMPT_HEADER)),
|
|
39
|
+
...(headers.get(ADD_TO_ANALYSIS_HEADER) === null ? {} : { addToAnalysisId: uuid.parse(headers.get(ADD_TO_ANALYSIS_HEADER)) }),
|
|
40
|
+
...(selection === null ? {} : { selection: selectionStartIdentitySchema.parse(JSON.parse(z.string().max(256).parse(selection))) }) };
|
|
32
41
|
}
|
|
33
42
|
const localStartPath = z.string().min(1).max(4096).refine(value => !value.includes("\\") && !value.includes("\0") && !value.startsWith("/")
|
|
34
43
|
&& value.split("/").every(part => part !== ".." && part !== "." && part !== ""));
|
|
@@ -42,7 +51,7 @@ export const localStartRequestSchema = z.strictObject({
|
|
|
42
51
|
export const localStartResponseSchema = z.strictObject({ jobId: uuid, projectId: uuid });
|
|
43
52
|
export const relayPlanSchema = z.strictObject({
|
|
44
53
|
bundleVersion: id, bundleHash: z.string().regex(/^[a-f0-9]{64}$/), relayVersion: id,
|
|
45
|
-
calls: z.array(z.strictObject({ routeId: id, model: id, calls: count(512), maxInputBytes: count(
|
|
54
|
+
calls: z.array(z.strictObject({ routeId: id, model: id, calls: count(512), maxInputBytes: count(PLANNED_INPUT_BYTES),
|
|
46
55
|
maxInputTokens: count(200_000), maxOutputTokens: count(64_000) })).min(1).max(64)
|
|
47
56
|
.refine(calls => new Set(calls.map(call => call.routeId)).size === calls.length && calls.reduce((sum, call) => sum + call.calls, 0) <= 512),
|
|
48
57
|
});
|
|
@@ -84,44 +93,19 @@ export const consoleIntakeResultSchema = z.strictObject({
|
|
|
84
93
|
export const LOCAL_FINDINGS_BODY_LIMIT = 2 * 1024 * 1024;
|
|
85
94
|
export const LOCAL_FINDINGS_QUOTE_LIMIT = RESULT_CHECK_LIMITS.quoteBytes;
|
|
86
95
|
export const LOCAL_FINDINGS_TEXT_LIMIT = RESULT_CHECK_LIMITS.textBytes;
|
|
87
|
-
function shortFindingFields(value) {
|
|
88
|
-
if (typeof value === "string")
|
|
89
|
-
return Buffer.byteLength(value, "utf8") <= LOCAL_FINDINGS_TEXT_LIMIT;
|
|
90
|
-
return value === null || typeof value !== "object" || Object.values(value).every(shortFindingFields);
|
|
91
|
-
}
|
|
92
|
-
const quote = z.string().refine(value => Buffer.byteLength(value, "utf8") <= LOCAL_FINDINGS_QUOTE_LIMIT);
|
|
93
|
-
const text = z.string().min(1).refine(value => value.trim().length > 0);
|
|
94
|
-
const delta = fixDeltaSchema.extend({
|
|
95
|
-
nodes: z.array(fixDeltaSchema.shape.nodes.element.strict()),
|
|
96
|
-
edges: z.array(fixDeltaSchema.shape.edges.element.strict()),
|
|
97
|
-
}).strict();
|
|
98
|
-
const fix = inspectFixSchema.extend({
|
|
99
|
-
...fixFactsSchema.shape,
|
|
100
|
-
targetFiles: z.array(inspectFixSchema.shape.targetFiles.element.strict()),
|
|
101
|
-
delta: delta.nullable(),
|
|
102
|
-
}).strict();
|
|
103
|
-
const notes = sourceNotesSchema.extend({
|
|
104
|
-
sites: z.array(sourceNotesSchema.shape.sites.element.extend({ keyPhrase: quote }).strict()),
|
|
105
|
-
counterEvidence: z.array(sourceNotesSchema.shape.counterEvidence.element.extend({ excerpt: quote }).strict()),
|
|
106
|
-
}).strict();
|
|
107
|
-
export const localFindingSchema = rawFindingSchema.extend({
|
|
108
|
-
audience: findingAudienceSchema.optional(),
|
|
109
|
-
summary: z.string().nullable(),
|
|
110
|
-
category: rawFindingSchema.shape.category.removeCatch(),
|
|
111
|
-
question: rawFindingSchema.shape.question.removeCatch().nullable(),
|
|
112
|
-
siteRepo: text.nullable(),
|
|
113
|
-
site: rawFindingSchema.shape.site.unwrap().strict().nullable(),
|
|
114
|
-
excerpt: quote.nullable(),
|
|
115
|
-
otherSites: z.array(otherSiteSchema.extend({ repo: text, excerpt: otherSiteSchema.shape.excerpt.pipe(quote) }).strict()),
|
|
116
|
-
fixes: z.array(fix).min(1),
|
|
117
|
-
sourceNotes: notes.nullable().optional(),
|
|
118
|
-
}).strict().refine(shortFindingFields);
|
|
119
96
|
const payload = z.strictObject({
|
|
120
97
|
checkId: z.uuid(), projectId: z.uuid(), reservationId: uuid.optional(),
|
|
98
|
+
addToAnalysisId: uuid.optional(),
|
|
99
|
+
coverage: z.strictObject({ skippedFiles: z.array(z.strictObject({ path: localStartPath,
|
|
100
|
+
reason: publicReportSchema.shape.refusedFiles.element.shape.reason })),
|
|
101
|
+
readPaths: z.array(localStartPath) }).optional(),
|
|
102
|
+
findingAssessments: z.array(z.strictObject({ fingerprint: z.string().max(8192), support: z.enum(["complete", "incomplete"]),
|
|
103
|
+
confidence: z.enum(["sure", "not sure"]), weak: z.boolean(), reason: z.string().max(4096) })).optional(),
|
|
121
104
|
agent: z.strictObject({
|
|
122
105
|
repository: text.max(4096), commit: z.string().regex(/^(?:[a-f\d]{40}|[a-f\d]{64})$/i).nullable(),
|
|
123
106
|
files: z.array(z.strictObject({ path: text.max(4096), hash: z.string().regex(/^[a-f\d]{64}$/i) })).min(1),
|
|
124
107
|
}),
|
|
108
|
+
architecture: projectArchitectureSchema.optional(),
|
|
125
109
|
findings: z.array(localFindingSchema), bundleVersion: text.max(128), relayVersion: text.max(128), counterfactLocalVersion: text.max(128),
|
|
126
110
|
}).refine(validIntakeText);
|
|
127
111
|
// Original JSON size is checked before a shared schema can trim text or apply defaults.
|
package/dist/contract/engine.js
CHANGED
|
@@ -3,7 +3,7 @@ import { ANALYSIS_PROTOCOL, analysisCapabilities } from "../analysis-core/contra
|
|
|
3
3
|
import { stableStringify } from "../analysis-core/llm/stable-json.js";
|
|
4
4
|
import { workloadHardware } from "../gatekeeper/trust/schema.js";
|
|
5
5
|
import { kmsKeyName } from "../transport/kms.js";
|
|
6
|
-
export const ENGINE_ABI =
|
|
6
|
+
export const ENGINE_ABI = 2;
|
|
7
7
|
export const ENGINE_SPAWNER_CONTRACT = 1;
|
|
8
8
|
const hash = z.string().regex(/^[0-9a-f]{64}$/);
|
|
9
9
|
const version = z.string().min(1).max(128).regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { analysisRelease } from "./engine.js";
|
|
3
|
-
import { executionRequest } from "../analysis-core/contracts/
|
|
3
|
+
import { executionRequest } from "../analysis-core/contracts/execution-request.js";
|
|
4
|
+
import { PLANNED_INPUT_BYTES } from "../analysis-core/requests/planned-limits.js";
|
|
4
5
|
import { compactJwtPattern, hexChallenge } from "./attestation.js";
|
|
5
6
|
export { PRODUCTION_EVIDENCE_VERSION, RELAY_CHANNEL_AUDIENCE, RELAY_PORT, RELAY_URL, RELAY_HEALTH_PATH, RELAY_HEALTH_URL, GOOGLE_ATTESTATION_ISSUER, MAX_ATTESTATION_BYTES, attestationChallenge, productionEvidence, evidenceBinding, googleAttestationClaims, observedAttestationClaims, probeAttestationClaims, attestationClaims } from "./attestation.js";
|
|
6
7
|
/**
|
|
@@ -66,14 +67,14 @@ const outputCount = z.number().int().nonnegative().max(64_000);
|
|
|
66
67
|
/** A reservation lookup establishes the owner without reserving spend or contacting the model provider. */
|
|
67
68
|
export const reservationCheck = z.object({ action: z.literal("check"), ...billingScope }).strict();
|
|
68
69
|
export const reservationIdentity = z.object({ clientId: z.string().min(1).max(128) }).strict();
|
|
69
|
-
export const callCounts = z.object({ inputBytes: z.number().int().positive().max(
|
|
70
|
+
export const callCounts = z.object({ inputBytes: z.number().int().positive().max(PLANNED_INPUT_BYTES),
|
|
70
71
|
inputTokens: inputCount.positive(), maxOutputTokens: outputCount.positive() }).strict();
|
|
71
72
|
export const authorizationRequest = z.object({ action: z.literal("authorize"), ...billingScope,
|
|
72
73
|
routeId: billingId, templateId: billingId, answerFormatId: billingId, model: billingId, ...callCounts.shape }).strict();
|
|
73
74
|
export const callAuthorization = z.object({ attemptId: z.uuid(), model: billingId, role: z.enum(["required", "optional"]),
|
|
74
75
|
/** Stable opaque owner supplied only by the authenticated authorization service. */
|
|
75
76
|
clientId: reservationIdentity.shape.clientId,
|
|
76
|
-
deadline: z.iso.datetime(), countLimits: z.object({ maxInputBytes: z.number().int().positive().max(
|
|
77
|
+
deadline: z.iso.datetime(), countLimits: z.object({ maxInputBytes: z.number().int().positive().max(PLANNED_INPUT_BYTES),
|
|
77
78
|
maxInputTokens: inputCount.positive(), maxOutputTokens: outputCount.positive() }).strict() }).strict();
|
|
78
79
|
export const providerUsage = z.object({ kind: z.enum(["actual", "estimated", "unpaid"]),
|
|
79
80
|
outcome: z.enum(["success", "unknown", "retryable_failure", "terminal_provider_failure", "schema_refusal", "leak_refusal"]),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PROJECT_ARCHITECTURE_BYTES, projectArchitectureSchema, projectArchitecture } from "../analysis-core/analysis/project-architecture.js";
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ANALYSIS_SELECTION_LIMITS, provenScopeChoiceSchema } from "../analysis-core/contracts/analysis-selection.js";
|
|
3
|
+
import { refusedImportSchema } from "../analysis-core/contracts/module-resolution.js";
|
|
4
|
+
import { selectionPathSchema, snapshotIdSchema } from "../analysis-core/contracts/selection-paths.js";
|
|
5
|
+
import { FileReadRefusal } from "../analysis-core/conventions/file-admission.js";
|
|
6
|
+
export { analysisScopeSchema } from "../analysis-core/contracts/analysis-selection.js";
|
|
7
|
+
// Coverage wording stays here so the CLI and MCP cannot make different promises about coverage.
|
|
8
|
+
export const CHOOSE_SCOPE_MESSAGE = "This analysis is too large to run at once. Choose what Counterfact should read.";
|
|
9
|
+
export const MULTI_ENTRY_CHOOSE_SCOPE_MESSAGE = "These agent files are too many or too large for one analysis. Choose what Counterfact should read.";
|
|
10
|
+
export const ENTRY_ONLY_LABEL = "Only the selected file";
|
|
11
|
+
export const ENTRY_ONLY_DESCRIPTION = "Read {entry}. Leave its imports out of this analysis.";
|
|
12
|
+
export const SELECTED_IMPORTS_LABEL = "The selected file and some imports";
|
|
13
|
+
export const SELECTED_IMPORTS_DESCRIPTION = "Read {entry} and the imports listed below. Leave the other imports out of this analysis.";
|
|
14
|
+
export const ENTRY_IMPORTS_LABEL = "One agent file and its imports";
|
|
15
|
+
export const ENTRY_IMPORTS_DESCRIPTION = "Read {entry} and its project imports inside the launch folder.";
|
|
16
|
+
export const ENTRY_GROUP_LABEL = "Several agent files and their imports";
|
|
17
|
+
export const ENTRY_GROUP_DESCRIPTION = "Read the agent files and their project imports listed below. Leave the other selected files out of this analysis.";
|
|
18
|
+
export const SINGLE_FILE_TOO_LARGE_MESSAGE = "This file is too large to analyze on its own. Choose another agent file or split this file into smaller files.";
|
|
19
|
+
export const INVALID_SELECTION_MESSAGE = "Choose an agent file or folder inside the launch folder.";
|
|
20
|
+
export const ANALYSIS_TOO_LARGE_MESSAGE = "This analysis is too large to run at once. Choose one agent file or a smaller folder, such as one agent's folder.";
|
|
21
|
+
/** Recovery may offer a file only when this run accepts file selections. */
|
|
22
|
+
export function selectionRefusalMessage(message, fileMode) {
|
|
23
|
+
if (!fileMode && message === INVALID_SELECTION_MESSAGE)
|
|
24
|
+
return "Choose a folder inside this project, or start Claude Code or Codex in the folder you want to analyze.";
|
|
25
|
+
if (!fileMode && message === ANALYSIS_TOO_LARGE_MESSAGE)
|
|
26
|
+
return "This analysis is too large to run at once. Choose a smaller folder, such as one agent's folder.";
|
|
27
|
+
return message;
|
|
28
|
+
}
|
|
29
|
+
export const INVALID_SCOPE_MESSAGE = "This scope could not be used. Choose one of the returned scopes and try again.";
|
|
30
|
+
export const STALE_SCOPE_MESSAGE = "The selected files changed. Run the analysis again to get updated scope choices.";
|
|
31
|
+
export const SCOPE_PROPOSAL_CONFLICT_MESSAGE = "The selected scope and proposed files do not match. Keep the selected file and only its listed imports.";
|
|
32
|
+
export const SELECTION_REFUSAL_MESSAGES = Object.freeze([
|
|
33
|
+
INVALID_SELECTION_MESSAGE, INVALID_SCOPE_MESSAGE, STALE_SCOPE_MESSAGE, SCOPE_PROPOSAL_CONFLICT_MESSAGE,
|
|
34
|
+
SINGLE_FILE_TOO_LARGE_MESSAGE,
|
|
35
|
+
]);
|
|
36
|
+
export function isSelectionRefusal(error, fileMode) {
|
|
37
|
+
return error instanceof Error && (SELECTION_REFUSAL_MESSAGES.includes(error.message)
|
|
38
|
+
|| fileMode && error instanceof FileReadRefusal);
|
|
39
|
+
}
|
|
40
|
+
export const INCLUDED_FILES_CAPTION = "Included files:";
|
|
41
|
+
export const OMITTED_IMPORTS_CAPTION = "Imports left out:";
|
|
42
|
+
export const OMITTED_FILES_CAPTION = "Files left out:";
|
|
43
|
+
export const SCOPE_CHOICE_INSTRUCTION = "Choose a scope, then run the analysis again with that scope.";
|
|
44
|
+
export const INCOMPLETE_IMPORTS_MESSAGE = "Some imports could not be resolved. This analysis covers only the files listed below.";
|
|
45
|
+
export const IMPORT_DISCOVERY_LIMIT_MESSAGE = "Counterfact reached the import discovery limit. The list of imports is incomplete.";
|
|
46
|
+
export const SELECTED_FILES_PROGRESS = "Reading the selected files and running analysis.\n";
|
|
47
|
+
export const SELECTED_PATH_FAILURE = "Analysis could not finish. Check the selected path, provider key and connection.";
|
|
48
|
+
export function progressLine(selection, surface = "source") {
|
|
49
|
+
return selection?.kind === "file" ? SELECTED_FILES_PROGRESS
|
|
50
|
+
: surface === "terminal" ? "Analyzing the agent…" : "Reading the folder and running analysis.\n";
|
|
51
|
+
}
|
|
52
|
+
export function failureLine(selection) {
|
|
53
|
+
return selection?.kind === "file" ? SELECTED_PATH_FAILURE : "Analysis could not finish. Check the folder, provider key and connection.";
|
|
54
|
+
}
|
|
55
|
+
export const SELECTION_COVERAGE_DESCRIPTION = "This analysis covers {entry} and {count} selected imports. Other imports were left out.";
|
|
56
|
+
export const SCOPE_RETRY_TEMPLATE = "Run again: counterfact-labs analyze {quoted-entry} --scope {quoted-scope-file}";
|
|
57
|
+
export const FILE_SCOPE_PUBLIC_USAGE = "Usage: counterfact-labs analyze [path] [--proposal <json-file>] [--scope <json-file>]\n counterfact-labs finding <id>\n counterfact-labs balance | login | logout | mcp\n";
|
|
58
|
+
export const FILE_SCOPE_SOURCE_USAGE = "Usage: npx tsx cli/check.ts <path> [--output <directory>] [--scope <json-file>]\n";
|
|
59
|
+
export function scopeChoiceWording(choice, fits) {
|
|
60
|
+
const group = (choice.scope.entries ?? []).filter(entry => choice.includes.includes(entry)).length > 1;
|
|
61
|
+
const entryOnly = choice.scope.imports === "none" || Array.isArray(choice.scope.imports) && choice.scope.imports.length === 0;
|
|
62
|
+
const fullImports = choice.scope.imports === "all" || choice.coverage === "entry-imports";
|
|
63
|
+
return {
|
|
64
|
+
label: group ? ENTRY_GROUP_LABEL : entryOnly ? ENTRY_ONLY_LABEL : fullImports ? ENTRY_IMPORTS_LABEL : SELECTED_IMPORTS_LABEL,
|
|
65
|
+
description: entryOnly && fits === false ? SINGLE_FILE_TOO_LARGE_MESSAGE
|
|
66
|
+
: (group ? ENTRY_GROUP_DESCRIPTION : entryOnly ? ENTRY_ONLY_DESCRIPTION : fullImports ? ENTRY_IMPORTS_DESCRIPTION : SELECTED_IMPORTS_DESCRIPTION)
|
|
67
|
+
.replace("{entry}", () => choice.scope.entry),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export const scopeChoiceSchema = provenScopeChoiceSchema.safeExtend({
|
|
71
|
+
label: z.enum([ENTRY_ONLY_LABEL, SELECTED_IMPORTS_LABEL, ENTRY_IMPORTS_LABEL, ENTRY_GROUP_LABEL]),
|
|
72
|
+
description: z.string().min(1).max(8192),
|
|
73
|
+
// Local measurement can prove a group fits; ordinary run admission checks it again.
|
|
74
|
+
fits: z.union([z.boolean(), z.literal("unknown")]),
|
|
75
|
+
}).refine(choice => {
|
|
76
|
+
const { label, description } = scopeChoiceWording(choice, choice.fits);
|
|
77
|
+
return choice.label === label && choice.description === description;
|
|
78
|
+
});
|
|
79
|
+
export const chooseScopeResultSchema = z.strictObject({
|
|
80
|
+
kind: z.literal("choose_scope"), text: z.enum([CHOOSE_SCOPE_MESSAGE, MULTI_ENTRY_CHOOSE_SCOPE_MESSAGE]), snapshot: snapshotIdSchema,
|
|
81
|
+
entry: selectionPathSchema.nullable(), coverageComplete: z.boolean(),
|
|
82
|
+
frontierCount: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
83
|
+
choices: z.array(scopeChoiceSchema).min(1).max(ANALYSIS_SELECTION_LIMITS.choices),
|
|
84
|
+
imports: z.array(z.strictObject({ path: selectionPathSchema, role: z.literal("imported-helper"),
|
|
85
|
+
importedBy: z.array(selectionPathSchema).min(1).max(ANALYSIS_SELECTION_LIMITS.files)
|
|
86
|
+
.refine(paths => new Set(paths).size === paths.length) })).max(ANALYSIS_SELECTION_LIMITS.files),
|
|
87
|
+
refusedEdges: z.array(refusedImportSchema).max(ANALYSIS_SELECTION_LIMITS.edges),
|
|
88
|
+
}).refine(value => (!value.coverageComplete || value.frontierCount === 0 && value.refusedEdges.every(edge => edge.reason === "external"))
|
|
89
|
+
&& new Set(value.choices.map(choice => choice.id)).size === value.choices.length
|
|
90
|
+
&& new Set(value.imports.map(file => file.path)).size === value.imports.length
|
|
91
|
+
&& value.choices.every(choice => choice.scope.snapshot === value.snapshot && (value.entry === null || choice.scope.entry === value.entry
|
|
92
|
+
&& (choice.scope.imports !== "all" || choice.omitted.length === 0))));
|
|
93
|
+
export const chooseScopeMcpResultSchema = z.strictObject({
|
|
94
|
+
content: z.tuple([z.strictObject({ type: z.literal("text"), text: z.string()
|
|
95
|
+
.refine(text => [CHOOSE_SCOPE_MESSAGE, MULTI_ENTRY_CHOOSE_SCOPE_MESSAGE].some(message => text.startsWith(message))) })]),
|
|
96
|
+
isError: z.literal(false), structuredContent: chooseScopeResultSchema.safeExtend({ exitCode: z.literal(0) }),
|
|
97
|
+
});
|
|
98
|
+
export const SCOPE_CHOICE_EXIT_CODE = 3;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
const digest = z.string().regex(/^[a-f0-9]{64}$/);
|
|
4
|
+
export const selectionStartIdentitySchema = z.strictObject({ version: z.literal(1), key: digest, snapshot: digest });
|
|
5
|
+
/** The console can distinguish selected runs without receiving local coordinates or snapshot contents. */
|
|
6
|
+
export function selectionStartIdentity(key, snapshot) {
|
|
7
|
+
const hash = (value) => createHash("sha256").update(value).digest("hex");
|
|
8
|
+
return { version: 1, key: hash(key), snapshot: hash(snapshot) };
|
|
9
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { CHOOSE_SCOPE_MESSAGE, MULTI_ENTRY_CHOOSE_SCOPE_MESSAGE, scopeChoiceWording, SINGLE_FILE_TOO_LARGE_MESSAGE } from "./scope-choice.js";
|
|
2
|
+
export class ScopeChoiceError extends Error {
|
|
3
|
+
choice;
|
|
4
|
+
constructor(choice) {
|
|
5
|
+
super(choice.text);
|
|
6
|
+
this.choice = choice;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/** A known nonfitting choice must never invite another paid attempt. */
|
|
10
|
+
export function scopeChoiceRefusal(choice) {
|
|
11
|
+
return choice.choices.every(option => option.fits === false) ? SINGLE_FILE_TOO_LARGE_MESSAGE : undefined;
|
|
12
|
+
}
|
|
13
|
+
/** Membership choices are proven by core; fewer files alone never prove that a run fits. */
|
|
14
|
+
export function selectionScopeChoices(membership, options = {}) {
|
|
15
|
+
const current = new Set(options.currentPaths ?? membership.files.map(file => file.path));
|
|
16
|
+
const oversized = new Set(options.oversizedPaths ?? []);
|
|
17
|
+
const seen = new Set();
|
|
18
|
+
const choices = membership.choices.flatMap(choice => {
|
|
19
|
+
const entryOnly = choice.scope.imports === "none" || Array.isArray(choice.scope.imports) && choice.scope.imports.length === 0;
|
|
20
|
+
const knownOversized = entryOnly && oversized.has(choice.scope.entry);
|
|
21
|
+
const identical = choice.includes.length === current.size && choice.includes.every(path => current.has(path));
|
|
22
|
+
const key = JSON.stringify([...choice.includes].sort());
|
|
23
|
+
if (seen.has(key) || choice.includes.some(path => !current.has(path)) || identical && !knownOversized && !options.allowCurrent)
|
|
24
|
+
return [];
|
|
25
|
+
seen.add(key);
|
|
26
|
+
const { label, description } = scopeChoiceWording(choice, knownOversized ? false : "unknown");
|
|
27
|
+
return [{ ...choice, label, description, fits: knownOversized ? false : "unknown" }];
|
|
28
|
+
});
|
|
29
|
+
if (!choices.length)
|
|
30
|
+
return undefined;
|
|
31
|
+
const imports = membership.files.flatMap(file => {
|
|
32
|
+
if (file.role !== "imported-helper")
|
|
33
|
+
return [];
|
|
34
|
+
const importedBy = [...new Set(membership.edges.filter(edge => edge.path === file.path).map(edge => edge.from))];
|
|
35
|
+
return importedBy.length ? [{ path: file.path, role: "imported-helper", importedBy }] : [];
|
|
36
|
+
});
|
|
37
|
+
return { kind: "choose_scope", text: membership.kind === "file" && membership.entries ? MULTI_ENTRY_CHOOSE_SCOPE_MESSAGE : CHOOSE_SCOPE_MESSAGE, snapshot: membership.snapshot,
|
|
38
|
+
entry: membership.kind === "file" && !membership.entries ? membership.entry : null, coverageComplete: membership.completeness.complete,
|
|
39
|
+
frontierCount: membership.completeness.frontierCount, choices, imports, refusedEdges: membership.refusedEdges };
|
|
40
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { cliRefusals, refusalExitCode } from "../contract/cli-refusals.js";
|
|
1
|
+
import { cliRefusals, localFailure, refusalExitCode } from "../contract/cli-refusals.js";
|
|
2
2
|
import { CLI_VERSION } from "../contract/cli-version.js";
|
|
3
3
|
import { consoleSchemeAllowed } from "../contract/console-url.js";
|
|
4
4
|
import { cliApiMessages } from "../contract/cli-api-messages.js";
|
|
5
|
+
import { selectionRefusalMessage } from "../contract/scope-choice.js";
|
|
5
6
|
import { emailSignInMessages } from "../contract/email-sign-in-messages.js";
|
|
6
7
|
import { CLI_BODY_LIMIT } from "../cli/api-types.js";
|
|
7
8
|
import { findingNumberParamsSchema } from "../contract/findings.js";
|
|
@@ -20,6 +21,9 @@ export class ApiError extends Error {
|
|
|
20
21
|
this.reason = reason;
|
|
21
22
|
}
|
|
22
23
|
}
|
|
24
|
+
export class SavedSignInError extends ApiError {
|
|
25
|
+
constructor() { super(0, "invalid_request", cliApiMessages.unreadable_sign_in_terminal, 0, "invalid_request"); }
|
|
26
|
+
}
|
|
23
27
|
const emailRoutes = {
|
|
24
28
|
"email/start": { fields: ["email"], successStatus: 202, failures: { invalid_email: 400 }, jsonCooldown: true },
|
|
25
29
|
"email/verify": { fields: ["email", "code", "clientName"], successStatus: 200,
|
|
@@ -27,6 +31,49 @@ const emailRoutes = {
|
|
|
27
31
|
};
|
|
28
32
|
// Longer cooldowns keep the server's delay but use the sentence without a duration.
|
|
29
33
|
const MAX_DISPLAYED_EMAIL_COOLDOWN_MS = 24 * 60 * 60 * 1000;
|
|
34
|
+
const transportErrorCodes = new Set([
|
|
35
|
+
"ECONNREFUSED", "ECONNRESET", "ECONNABORTED", "EPIPE", "ENETUNREACH", "ENETDOWN", "EHOSTUNREACH", "EHOSTDOWN",
|
|
36
|
+
"ENETRESET", "EADDRNOTAVAIL", "EPROTO", "ENOTFOUND", "EAI_AGAIN", "EAI_FAIL", "ETIMEDOUT", "ETIMEOUT", "ESERVFAIL", "ENODATA",
|
|
37
|
+
// Certificate verification can fail without an SSL or TLS prefix.
|
|
38
|
+
"UNABLE_TO_GET_ISSUER_CERT", "UNABLE_TO_GET_CRL", "UNABLE_TO_DECRYPT_CERT_SIGNATURE",
|
|
39
|
+
"UNABLE_TO_DECRYPT_CRL_SIGNATURE", "UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY", "CERT_SIGNATURE_FAILURE",
|
|
40
|
+
"CRL_SIGNATURE_FAILURE", "CERT_NOT_YET_VALID", "CERT_HAS_EXPIRED", "CRL_NOT_YET_VALID", "CRL_HAS_EXPIRED",
|
|
41
|
+
"ERROR_IN_CERT_NOT_BEFORE_FIELD", "ERROR_IN_CERT_NOT_AFTER_FIELD", "ERROR_IN_CRL_LAST_UPDATE_FIELD",
|
|
42
|
+
"ERROR_IN_CRL_NEXT_UPDATE_FIELD", "DEPTH_ZERO_SELF_SIGNED_CERT", "SELF_SIGNED_CERT_IN_CHAIN",
|
|
43
|
+
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY", "UNABLE_TO_VERIFY_LEAF_SIGNATURE", "CERT_CHAIN_TOO_LONG", "CERT_REVOKED",
|
|
44
|
+
"NO_ISSUER_PUBLIC_KEY", "PATH_LENGTH_EXCEEDED", "INVALID_PURPOSE", "CERT_UNTRUSTED", "CERT_REJECTED",
|
|
45
|
+
"SUBJECT_ISSUER_MISMATCH", "AKID_SKID_MISMATCH", "AKID_ISSUER_SERIAL_MISMATCH", "KEYUSAGE_NO_CERTSIGN",
|
|
46
|
+
"UNABLE_TO_GET_CRL_ISSUER", "UNHANDLED_CRITICAL_EXTENSION", "KEYUSAGE_NO_CRL_SIGN",
|
|
47
|
+
"UNHANDLED_CRITICAL_CRL_EXTENSION", "INVALID_NON_CA", "PROXY_PATH_LENGTH_EXCEEDED",
|
|
48
|
+
"KEYUSAGE_NO_DIGITAL_SIGNATURE", "PROXY_CERTIFICATES_NOT_ALLOWED", "INVALID_EXTENSION",
|
|
49
|
+
"INVALID_POLICY_EXTENSION", "NO_EXPLICIT_POLICY", "DIFFERENT_CRL_SCOPE", "UNSUPPORTED_EXTENSION_FEATURE",
|
|
50
|
+
"UNNESTED_RESOURCE", "PERMITTED_VIOLATION", "EXCLUDED_VIOLATION", "SUBTREE_MINMAX", "APPLICATION_VERIFICATION",
|
|
51
|
+
"UNSUPPORTED_CONSTRAINT_TYPE", "UNSUPPORTED_CONSTRAINT_SYNTAX", "UNSUPPORTED_NAME_SYNTAX",
|
|
52
|
+
"CRL_PATH_VALIDATION_ERROR", "PATH_LOOP", "SUITE_B_INVALID_VERSION", "SUITE_B_INVALID_ALGORITHM",
|
|
53
|
+
"SUITE_B_INVALID_CURVE", "SUITE_B_INVALID_SIGNATURE_ALGORITHM", "SUITE_B_LOS_NOT_ALLOWED",
|
|
54
|
+
"SUITE_B_CANNOT_SIGN_P_384_WITH_P_256", "HOSTNAME_MISMATCH", "EMAIL_MISMATCH", "IP_ADDRESS_MISMATCH",
|
|
55
|
+
"DANE_NO_MATCH", "EE_KEY_TOO_SMALL", "CA_KEY_TOO_SMALL", "CA_MD_TOO_WEAK", "NO_VALID_SCTS",
|
|
56
|
+
"PROXY_SUBJECT_NAME_VIOLATION", "OCSP_VERIFY_NEEDED", "OCSP_VERIFY_FAILED", "OCSP_CERT_UNKNOWN",
|
|
57
|
+
"UNSUPPORTED_SIGNATURE_ALGORITHM", "SIGNATURE_ALGORITHM_MISMATCH", "SIGNATURE_ALGORITHM_INCONSISTENCY",
|
|
58
|
+
"INVALID_CA", "PATHLEN_INVALID_FOR_NON_CA", "PATHLEN_WITHOUT_KU_KEY_CERT_SIGN",
|
|
59
|
+
"KU_KEY_CERT_SIGN_INVALID_FOR_NON_CA", "ISSUER_NAME_EMPTY", "SUBJECT_NAME_EMPTY",
|
|
60
|
+
"MISSING_AUTHORITY_KEY_IDENTIFIER", "MISSING_SUBJECT_KEY_IDENTIFIER", "EMPTY_SUBJECT_ALT_NAME",
|
|
61
|
+
"EMPTY_SUBJECT_SAN_NOT_CRITICAL", "CA_BCONS_NOT_CRITICAL", "AUTHORITY_KEY_IDENTIFIER_CRITICAL",
|
|
62
|
+
"SUBJECT_KEY_IDENTIFIER_CRITICAL", "CA_CERT_MISSING_KEY_USAGE", "EXTENSIONS_REQUIRE_VERSION_3",
|
|
63
|
+
"EC_KEY_EXPLICIT_PARAMS", "RPK_UNTRUSTED",
|
|
64
|
+
]);
|
|
65
|
+
function transportFailed(error, depth = 0) {
|
|
66
|
+
if (!(error instanceof Error) || depth > 4)
|
|
67
|
+
return false;
|
|
68
|
+
if ("code" in error && typeof error.code === "string") {
|
|
69
|
+
if (error.code === "ERR_INVALID_URL" || error.code === "UND_ERR_INVALID_ARG")
|
|
70
|
+
return false;
|
|
71
|
+
if (transportErrorCodes.has(error.code) || /^(?:UND_ERR_|ERR_SSL_|ERR_TLS_|EAI_|CERT_|CRL_)/.test(error.code))
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return transportFailed(error.cause, depth + 1)
|
|
75
|
+
|| error instanceof AggregateError && error.errors.some((cause) => transportFailed(cause, depth + 1));
|
|
76
|
+
}
|
|
30
77
|
function retryAfterMs(value, rejectNegativeSeconds = false) {
|
|
31
78
|
if (!value)
|
|
32
79
|
return undefined;
|
|
@@ -97,11 +144,13 @@ export class ApiClient {
|
|
|
97
144
|
token;
|
|
98
145
|
fetcher;
|
|
99
146
|
signal;
|
|
147
|
+
classifyTransportErrors;
|
|
100
148
|
baseUrl;
|
|
101
|
-
constructor(baseUrl, token, fetcher = fetch, signal) {
|
|
149
|
+
constructor(baseUrl, token, fetcher = fetch, signal, classifyTransportErrors = true) {
|
|
102
150
|
this.token = token;
|
|
103
151
|
this.fetcher = fetcher;
|
|
104
152
|
this.signal = signal;
|
|
153
|
+
this.classifyTransportErrors = classifyTransportErrors;
|
|
105
154
|
let url;
|
|
106
155
|
try {
|
|
107
156
|
url = new URL(baseUrl);
|
|
@@ -132,6 +181,15 @@ export class ApiClient {
|
|
|
132
181
|
}
|
|
133
182
|
if (body !== undefined)
|
|
134
183
|
headers["content-type"] = "application/json";
|
|
184
|
+
// Fetch also validates headers locally. Keep that failure outside the transport catch.
|
|
185
|
+
if (this.classifyTransportErrors) {
|
|
186
|
+
try {
|
|
187
|
+
new Headers(headers);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
throw new SavedSignInError();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
135
193
|
const serialized = body === undefined ? undefined : JSON.stringify(safeBody);
|
|
136
194
|
if (serialized !== undefined && Buffer.byteLength(serialized, "utf8") > CLI_BODY_LIMIT) {
|
|
137
195
|
throw new ApiError(413, "body_too_large", cliRefusals.body_too_large);
|
|
@@ -151,8 +209,14 @@ export class ApiClient {
|
|
|
151
209
|
redirect: "error", signal: controller.signal,
|
|
152
210
|
});
|
|
153
211
|
}
|
|
154
|
-
catch {
|
|
212
|
+
catch (error) {
|
|
155
213
|
throwIfAborted();
|
|
214
|
+
// Fetch can refuse locally before creating a request, so rejection alone is not transport evidence.
|
|
215
|
+
if (this.classifyTransportErrors && !controller.signal.aborted && !transportFailed(error)) {
|
|
216
|
+
if (localFailure(error))
|
|
217
|
+
throw error;
|
|
218
|
+
throw new ApiError(0, "request_failed", cliApiMessages.request_failed);
|
|
219
|
+
}
|
|
156
220
|
throw new ApiError(0, "unreachable", cliApiMessages.unreachable, 0, "console_unreachable");
|
|
157
221
|
}
|
|
158
222
|
throwIfAborted();
|
|
@@ -190,4 +254,6 @@ export class ApiClient {
|
|
|
190
254
|
}
|
|
191
255
|
}
|
|
192
256
|
export function errorCode(error) { return refusalExitCode(error instanceof ApiError ? error.code : undefined); }
|
|
193
|
-
export function errorText(error
|
|
257
|
+
export function errorText(error, fileMode = true) {
|
|
258
|
+
return selectionRefusalMessage(localFailure(error)?.message ?? (error instanceof Error ? error.message : cliApiMessages.request_failed), fileMode);
|
|
259
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { consoleSchemeAllowed } from "../contract/console-url.js";
|
|
2
2
|
import { request as httpRequest } from "node:http";
|
|
3
3
|
import { request as httpsRequest } from "node:https";
|
|
4
|
+
import { z } from "zod";
|
|
4
5
|
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 { cliRefusals, NOT_ALLOWED_MESSAGE, SIGN_IN_EXPIRED_MESSAGE, CHECK_CONFLICT_MESSAGE, UNSAVED_RESULTS_MESSAGE, FINISH_FAILURE_MESSAGE, RESERVATION_EXPIRED_MESSAGE, RESULTS_TOO_LARGE_MESSAGE, START_FAILURE_MESSAGE } from "../contract/cli-refusals.js";
|
|
6
7
|
import { cliRelayReservationRequestSchema, consoleCreatedSchema, consoleIntakeResultSchema, consoleReservationSchema, consoleStatusSchema, LOCAL_FINDINGS_BODY_LIMIT, localFindingsPayloadSchema, LOCAL_START_BODY_LIMIT, LOCAL_START_EXECUTION_HEADER, localStartRequestSchema, localStartResponseSchema, serializeLocalStartMetadata } from "../contract/console.js";
|
|
7
8
|
const RESPONSE_LIMIT = 64 * 1024;
|
|
8
9
|
const RESERVATION_BODY_LIMIT = 64 * 1024;
|
|
@@ -11,19 +12,22 @@ export const consoleErrorMessages = {
|
|
|
11
12
|
invalid_configuration: START_FAILURE_MESSAGE,
|
|
12
13
|
invalid_request: FINISH_FAILURE_MESSAGE,
|
|
13
14
|
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:
|
|
15
|
-
not_allowed:
|
|
15
|
+
unauthorized: SIGN_IN_EXPIRED_MESSAGE,
|
|
16
|
+
not_allowed: NOT_ALLOWED_MESSAGE,
|
|
16
17
|
not_found: "Counterfact couldn't find this analysis. Start a new analysis.",
|
|
17
18
|
rate_limited: "Counterfact is busy. Try again in a minute.",
|
|
18
|
-
reservation_closed:
|
|
19
|
+
reservation_closed: UNSAVED_RESULTS_MESSAGE,
|
|
19
20
|
reservation_closing: "The results are still being saved. Run the analysis on the same folder again in a minute.",
|
|
20
|
-
delivery_incomplete:
|
|
21
|
-
check_conflict:
|
|
21
|
+
delivery_incomplete: UNSAVED_RESULTS_MESSAGE,
|
|
22
|
+
check_conflict: CHECK_CONFLICT_MESSAGE,
|
|
22
23
|
};
|
|
23
24
|
export class ConsoleError extends Error {
|
|
24
25
|
code;
|
|
25
26
|
reason;
|
|
26
|
-
|
|
27
|
+
localReason;
|
|
28
|
+
responseCode;
|
|
29
|
+
httpStatus;
|
|
30
|
+
constructor(code = "unavailable", context, options = {}) {
|
|
27
31
|
let message = consoleErrorMessages[code];
|
|
28
32
|
let reason = code === "unavailable" ? "results_not_saved" : code;
|
|
29
33
|
if (context === "start" && (code === "unavailable" || code === "invalid_request" || code === "not_found")) {
|
|
@@ -39,10 +43,21 @@ export class ConsoleError extends Error {
|
|
|
39
43
|
super(message);
|
|
40
44
|
this.name = "ConsoleError";
|
|
41
45
|
this.code = code;
|
|
42
|
-
this.reason = localReason ?? reason;
|
|
46
|
+
this.reason = options.localReason ?? reason;
|
|
47
|
+
this.localReason = options.localReason;
|
|
48
|
+
this.responseCode = options.responseCode;
|
|
49
|
+
this.httpStatus = options.httpStatus;
|
|
43
50
|
}
|
|
44
51
|
}
|
|
45
52
|
export const isConsoleToken = (token) => /^[\x21-\x7e]{1,4096}$/.test(token);
|
|
53
|
+
/** Strict older consoles accept optional metadata only after explicitly advertising it. */
|
|
54
|
+
export function consoleCapabilities(mode) {
|
|
55
|
+
return {
|
|
56
|
+
skippedFiles: "skippedFiles" in mode && mode.skippedFiles === true,
|
|
57
|
+
findingActivity: "findingActivity" in mode && mode.findingActivity === true,
|
|
58
|
+
architecture: "architecture" in mode && mode.architecture === true,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
46
61
|
/** Only this client may send repository metadata, content-free lifecycle or findings-only intake payloads to the console. */
|
|
47
62
|
export class ConsoleClient {
|
|
48
63
|
#url;
|
|
@@ -54,15 +69,15 @@ export class ConsoleClient {
|
|
|
54
69
|
url = new URL(options.url);
|
|
55
70
|
if (!consoleSchemeAllowed(url)
|
|
56
71
|
|| url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
57
|
-
throw new ConsoleError("invalid_configuration", undefined, "invalid_console_url");
|
|
72
|
+
throw new ConsoleError("invalid_configuration", undefined, { localReason: "invalid_console_url" });
|
|
58
73
|
}
|
|
59
74
|
}
|
|
60
75
|
catch {
|
|
61
|
-
throw new ConsoleError("invalid_configuration", undefined, "invalid_console_url");
|
|
76
|
+
throw new ConsoleError("invalid_configuration", undefined, { localReason: "invalid_console_url" });
|
|
62
77
|
}
|
|
63
78
|
const timeout = options.timeoutMs ?? 15_000;
|
|
64
79
|
if (!isConsoleToken(options.token) || !Number.isSafeInteger(timeout) || timeout <= 0 || timeout > 120_000) {
|
|
65
|
-
throw new ConsoleError("invalid_configuration", undefined, "request_failed");
|
|
80
|
+
throw new ConsoleError("invalid_configuration", undefined, { localReason: "request_failed" });
|
|
66
81
|
}
|
|
67
82
|
this.#url = url;
|
|
68
83
|
this.#token = options.token;
|
|
@@ -78,6 +93,13 @@ export class ConsoleClient {
|
|
|
78
93
|
}
|
|
79
94
|
return this.#send("/api/cli/checks", input, localStartRequestSchema, localStartResponseSchema, LOCAL_START_BODY_LIMIT, { statuses: [201], headers: { ...headers, [LOCAL_START_EXECUTION_HEADER.name]: LOCAL_START_EXECUTION_HEADER.value }, context: "start" });
|
|
80
95
|
}
|
|
96
|
+
async capabilities() {
|
|
97
|
+
const mode = await this.#send("/api/cli/checks/mode", {}, z.object({}), z.object({ skippedFiles: z.boolean().optional() }).passthrough(), 1024, { method: "GET", context: "start" });
|
|
98
|
+
return consoleCapabilities(mode);
|
|
99
|
+
}
|
|
100
|
+
async supportsSkippedFiles() {
|
|
101
|
+
return (await this.capabilities()).skippedFiles;
|
|
102
|
+
}
|
|
81
103
|
create(input) {
|
|
82
104
|
return this.#lifecycle({ ...input, action: "create" }, consoleCreatedSchema);
|
|
83
105
|
}
|
|
@@ -93,8 +115,8 @@ export class ConsoleClient {
|
|
|
93
115
|
fail(reservationId) {
|
|
94
116
|
return this.#lifecycle({ action: "fail", reservationId }, consoleStatusSchema);
|
|
95
117
|
}
|
|
96
|
-
intake(input) {
|
|
97
|
-
return this.#send("/api/cli/checks/local", input, localFindingsPayloadSchema, consoleIntakeResultSchema, LOCAL_FINDINGS_BODY_LIMIT, { statuses: [200, 201], context: "intake" });
|
|
118
|
+
intake(input, options = {}) {
|
|
119
|
+
return this.#send("/api/cli/checks/local", input, localFindingsPayloadSchema, consoleIntakeResultSchema, LOCAL_FINDINGS_BODY_LIMIT, { statuses: [200, 201], context: "intake", ...options });
|
|
98
120
|
}
|
|
99
121
|
#lifecycle(input, response) {
|
|
100
122
|
// Starting work has no saved results to retry; recovery keeps its saving guidance.
|
|
@@ -120,7 +142,8 @@ export class ConsoleClient {
|
|
|
120
142
|
throw new ConsoleError("invalid_request", options.context);
|
|
121
143
|
}
|
|
122
144
|
return new Promise((resolve, reject) => {
|
|
123
|
-
let settled = false;
|
|
145
|
+
let settled = false, responseStatus;
|
|
146
|
+
const interrupted = () => new ConsoleError("unavailable", undefined, { httpStatus: responseStatus });
|
|
124
147
|
const finish = (error, result) => {
|
|
125
148
|
if (settled)
|
|
126
149
|
return;
|
|
@@ -133,18 +156,22 @@ export class ConsoleClient {
|
|
|
133
156
|
};
|
|
134
157
|
const destination = new URL(path, this.#url);
|
|
135
158
|
const request = (destination.protocol === "https:" ? httpsRequest : httpRequest)(destination, {
|
|
136
|
-
method: "POST", agent: false, rejectUnauthorized: true, maxHeaderSize: 16 * 1024,
|
|
159
|
+
method: options.method ?? "POST", agent: false, rejectUnauthorized: true, maxHeaderSize: 16 * 1024,
|
|
137
160
|
headers: { "x-counterfact-cli-version": CLI_VERSION, authorization: `Bearer ${this.#token}`, "content-type": "application/json", accept: "application/json",
|
|
138
|
-
"content-length": body.length, "accept-encoding": "identity",
|
|
161
|
+
"content-length": options.method === "GET" ? 0 : body.length, "accept-encoding": "identity",
|
|
139
162
|
...options.headers },
|
|
140
163
|
}, response => {
|
|
141
164
|
const status = response.statusCode ?? 0;
|
|
165
|
+
responseStatus = status;
|
|
166
|
+
const accepted = options.acceptAnySuccess ? status >= 200 && status < 300 : (options.statuses ?? [200]).includes(status);
|
|
167
|
+
// Keep delivery status separate from validated receipt details.
|
|
168
|
+
const responseError = (code) => new ConsoleError(typeof code === "string" && Object.hasOwn(consoleErrorMessages, code) ? code : "unavailable", undefined, { responseCode: typeof code === "string" ? code : undefined, httpStatus: status });
|
|
142
169
|
const declared = response.headers["content-length"];
|
|
143
170
|
const contentType = response.headers["content-type"] ?? "";
|
|
144
171
|
if (status >= 300 && status < 400 || !/^application\/json(?:\s*;|$)/i.test(contentType)
|
|
145
172
|
|| response.headers["content-encoding"] && response.headers["content-encoding"] !== "identity"
|
|
146
173
|
|| declared && (!/^\d+$/.test(declared) || Number(declared) > RESPONSE_LIMIT)) {
|
|
147
|
-
finish(
|
|
174
|
+
finish(responseError());
|
|
148
175
|
response.destroy();
|
|
149
176
|
return;
|
|
150
177
|
}
|
|
@@ -153,37 +180,37 @@ export class ConsoleClient {
|
|
|
153
180
|
response.on("data", (chunk) => {
|
|
154
181
|
size += chunk.length;
|
|
155
182
|
if (size > RESPONSE_LIMIT) {
|
|
156
|
-
finish(
|
|
183
|
+
finish(responseError());
|
|
157
184
|
response.destroy();
|
|
158
185
|
}
|
|
159
186
|
else
|
|
160
187
|
chunks.push(chunk);
|
|
161
188
|
});
|
|
162
|
-
response.on("error", () => finish(
|
|
163
|
-
response.on("aborted", () => finish(
|
|
189
|
+
response.on("error", () => finish(interrupted()));
|
|
190
|
+
response.on("aborted", () => finish(interrupted()));
|
|
164
191
|
response.on("end", () => {
|
|
165
192
|
try {
|
|
166
193
|
const decoded = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
167
|
-
if (!
|
|
194
|
+
if (!accepted) {
|
|
168
195
|
const code = decoded && typeof decoded === "object" && "code" in decoded ? decoded.code : undefined;
|
|
169
|
-
throw
|
|
196
|
+
throw responseError(code);
|
|
170
197
|
}
|
|
171
198
|
const result = responseSchema.parse(decoded);
|
|
172
199
|
for (const [field, expected] of Object.entries(identities)) {
|
|
173
200
|
if (result[field] !== expected)
|
|
174
|
-
throw
|
|
201
|
+
throw responseError();
|
|
175
202
|
}
|
|
176
203
|
finish(undefined, result);
|
|
177
204
|
}
|
|
178
205
|
catch (error) {
|
|
179
|
-
finish(error instanceof ConsoleError ? error :
|
|
206
|
+
finish(error instanceof ConsoleError ? error : responseError());
|
|
180
207
|
}
|
|
181
208
|
});
|
|
182
209
|
});
|
|
183
210
|
// Use one wall-clock deadline: an idle timeout alone lets a trickling response run forever.
|
|
184
|
-
const timer = setTimeout(() => { finish(
|
|
185
|
-
request.on("error", () => finish(
|
|
186
|
-
request.end(body);
|
|
187
|
-
}).catch(error => { throw new ConsoleError(error instanceof ConsoleError ? error.code : "unavailable", options.context); });
|
|
211
|
+
const timer = setTimeout(() => { finish(interrupted()); request.destroy(); }, this.#timeoutMs);
|
|
212
|
+
request.on("error", () => finish(interrupted()));
|
|
213
|
+
request.end(options.method === "GET" ? undefined : body);
|
|
214
|
+
}).catch(error => { throw new ConsoleError(error instanceof ConsoleError ? error.code : "unavailable", options.context, error instanceof ConsoleError ? { localReason: error.localReason, responseCode: error.responseCode, httpStatus: error.httpStatus } : {}); });
|
|
188
215
|
}
|
|
189
216
|
}
|