@counterfact-labs/cli 0.2.4 → 0.2.5
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 +7 -1
- package/bundle/client-release.json +1 -1
- package/dist/adapters/anthropic.js +11 -2
- package/dist/adapters/completion.js +3 -0
- package/dist/adapters/durable-write.js +9 -2
- package/dist/adapters/files.js +25 -13
- package/dist/adapters/git-ignore.js +28 -14
- package/dist/adapters/git.js +1 -1
- package/dist/adapters/gitignore-output.js +41 -0
- package/dist/adapters/models.js +1 -6
- package/dist/adapters/output-paths.js +5 -2
- package/dist/adapters/public-instructions.js +1 -1
- package/dist/adapters/relay-plan.js +2 -5
- package/dist/adapters/release.js +2 -1
- package/dist/adapters/repository-files.js +164 -4
- package/dist/adapters/repository-root.js +18 -2
- package/dist/adapters/results.js +8 -4
- package/dist/adapters/settings.js +26 -1
- package/dist/adapters/snapshot.js +86 -15
- package/dist/analysis-core/analysis/finding-audience-switch.js +1 -0
- package/dist/analysis-core/analysis/schemas.js +1 -1
- package/dist/analysis-core/analysis/source-notes/types.js +1 -1
- package/dist/analysis-core/chunks/377MLK4M.js +21 -0
- package/dist/analysis-core/chunks/{D6AWHLRI.js → 3JWEFCTJ.js} +1 -1
- package/dist/analysis-core/chunks/{3227KXHF.js → 4H23CSUM.js} +1 -1
- package/dist/analysis-core/chunks/5DGPLXYB.js +1 -0
- package/dist/analysis-core/chunks/5TATKWLG.js +1 -0
- package/dist/analysis-core/chunks/{EH3QQ3T4.js → 6B2HILSY.js} +1 -1
- package/dist/analysis-core/chunks/6QY5BV5B.js +14 -0
- package/dist/analysis-core/chunks/6U2IR4XB.js +1 -0
- package/dist/analysis-core/chunks/A7WJJMRY.js +1 -0
- package/dist/analysis-core/chunks/C6X5GDG4.js +1 -0
- package/dist/analysis-core/chunks/DLPVYWHZ.js +1 -0
- package/dist/analysis-core/chunks/DZZBQDGO.js +1 -0
- package/dist/analysis-core/chunks/F3R4J3ND.js +1 -0
- package/dist/analysis-core/chunks/FOKDE64Y.js +1 -0
- package/dist/analysis-core/chunks/K2GNDX3W.js +1 -0
- package/dist/analysis-core/chunks/KTNA2KTN.js +7 -0
- package/dist/analysis-core/chunks/KWECQJ6X.js +1 -0
- package/dist/analysis-core/chunks/{SL27GMEN.js → M6HYSTQD.js} +1 -1
- package/dist/analysis-core/chunks/{ZAA44GPI.js → N7U5N42V.js} +1 -1
- package/dist/analysis-core/chunks/NIOS7U6C.js +41 -0
- package/dist/analysis-core/chunks/{QMIXCT62.js → OPMA32A6.js} +1 -1
- package/dist/analysis-core/chunks/PECX3OXB.js +1 -0
- package/dist/analysis-core/chunks/QFY6A3MS.js +66 -0
- package/dist/analysis-core/chunks/SKVEBOGA.js +1 -0
- package/dist/analysis-core/chunks/{T66VDFXF.js → STI4JE7L.js} +1 -1
- package/dist/analysis-core/chunks/SVMEWGD3.js +1 -0
- package/dist/analysis-core/chunks/X7WZAA7S.js +1 -0
- package/dist/analysis-core/chunks/YDBO7P4X.js +1 -0
- package/dist/analysis-core/chunks/YUY436SM.js +1 -0
- package/dist/analysis-core/chunks/ZTOVBAG7.js +1 -0
- package/dist/analysis-core/contracts/file-safety.js +1 -0
- package/dist/analysis-core/contracts/relay-analysis.js +1 -0
- package/dist/analysis-core/contracts/requests.js +1 -1
- package/dist/analysis-core/conventions/analysis-fit.js +1 -1
- package/dist/analysis-core/conventions/file-admission.js +1 -1
- package/dist/analysis-core/data/finding-types.js +1 -1
- package/dist/analysis-core/findings/sites.js +1 -0
- package/dist/analysis-core/github/repo-key.js +1 -1
- package/dist/analysis-core/import/outside-loader-sources.js +1 -1
- package/dist/analysis-core/index.js +3 -3
- package/dist/analysis-core/llm/output-limit.js +1 -1
- package/dist/analysis-core/llm/types.js +1 -0
- package/dist/analysis-core/requests/answer-formats.js +1 -1
- package/dist/analysis-core/requests/headline-request.js +1 -1
- package/dist/analysis-core/requests/local-plan.js +1 -1
- 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 -0
- package/dist/analysis-core/stages/execution-ports.js +1 -0
- package/dist/bundle/seal/deserialise.js +9 -1
- package/dist/bundle/seal/engine.js +76 -0
- package/dist/bundle/seal/freeze.js +9 -0
- package/dist/bundle/seal/metadata.js +3 -0
- package/dist/bundle/seal/open-engine.js +26 -0
- package/dist/bundle/seal/open.js +2 -8
- package/dist/cli/commands/analyze.js +36 -4
- package/dist/cli/commands/finding.js +9 -3
- package/dist/cli/context.js +4 -1
- package/dist/cli/finding-display.js +39 -0
- package/dist/cli/finding-lookup.js +92 -0
- package/dist/cli/finding-settings.js +5 -0
- package/dist/cli/fix-requests.js +42 -0
- package/dist/cli/intake-payload.js +19 -11
- package/dist/cli/main.js +9 -4
- package/dist/cli/mcp.js +65 -18
- package/dist/cli/relay-check.js +46 -13
- package/dist/cli/relay-preparation.js +28 -5
- package/dist/cli/result-check.js +68 -0
- package/dist/cli/summary.js +10 -3
- package/dist/contract/agent-choice.js +15 -0
- package/dist/contract/agent-proposal.js +38 -0
- package/dist/contract/attestation.js +106 -0
- package/dist/contract/cli-refusals.js +3 -0
- package/dist/contract/cli-version.js +1 -1
- package/dist/contract/console.js +21 -16
- package/dist/contract/engine.js +44 -0
- package/dist/contract/findings.js +28 -0
- package/dist/contract/fix-requests.js +30 -0
- package/dist/contract/messages.js +6 -94
- package/dist/contract/public-report.js +1 -0
- package/dist/contract/runtime-environment.js +12 -1
- package/dist/core/analysis/run-to-end.js +7 -52
- package/dist/gatekeeper/attestation.js +1 -1
- package/dist/gatekeeper/channel-binding.js +15 -0
- package/dist/gatekeeper/cli-api.js +12 -2
- package/dist/gatekeeper/connection-exchange.js +16 -12
- package/dist/gatekeeper/model.js +2 -1
- package/dist/gatekeeper/production-attestation.js +23 -17
- package/dist/gatekeeper/trust/policy.js +4 -1
- package/dist/gatekeeper/trust/schema.js +4 -0
- package/dist/transport/kms.js +25 -0
- package/package.json +4 -3
- package/dist/adapters/quality.js +0 -6
- package/dist/adapters/report.js +0 -29
- package/dist/analysis-core/analysis/analysis-flags.js +0 -1
- package/dist/analysis-core/analysis/finding-quality/config.js +0 -1
- package/dist/analysis-core/analysis/finding-quality/policy.js +0 -1
- package/dist/analysis-core/analysis/headlines/attachment.js +0 -1
- package/dist/analysis-core/analysis/headlines/input.js +0 -1
- package/dist/analysis-core/chunks/3CKW5JIF.js +0 -1
- package/dist/analysis-core/chunks/6YCABS5T.js +0 -1
- package/dist/analysis-core/chunks/7OYIBWX3.js +0 -1
- package/dist/analysis-core/chunks/DRGDIIFA.js +0 -1
- package/dist/analysis-core/chunks/FTODBFQW.js +0 -1
- package/dist/analysis-core/chunks/HPIAEQHE.js +0 -37
- package/dist/analysis-core/chunks/IVDDV6CU.js +0 -14
- package/dist/analysis-core/chunks/JF4GYMOZ.js +0 -1
- package/dist/analysis-core/chunks/K5OTWBTT.js +0 -22
- package/dist/analysis-core/chunks/MXXNED6N.js +0 -7
- package/dist/analysis-core/chunks/NCWEOJVJ.js +0 -6
- package/dist/analysis-core/chunks/O652FQQH.js +0 -1
- package/dist/analysis-core/chunks/OFBJNHC6.js +0 -1
- package/dist/analysis-core/chunks/QV2EOS7U.js +0 -1
- package/dist/analysis-core/chunks/STZOC7LN.js +0 -1
- package/dist/analysis-core/chunks/T2SAP7OV.js +0 -60
- package/dist/analysis-core/chunks/T5TREZCQ.js +0 -1
- package/dist/analysis-core/chunks/TDNTU4TF.js +0 -1
- package/dist/analysis-core/chunks/U5HOEVPE.js +0 -1
- package/dist/analysis-core/chunks/UEWL3GMQ.js +0 -1
- package/dist/analysis-core/chunks/V7BFUTM7.js +0 -1
- package/dist/core/analysis/finding-quality/config.js +0 -4
- package/dist/core/analysis/structural-run.js +0 -16
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { loadFinding } from "../finding-lookup.js";
|
|
2
|
+
import { formatFindingNumber } from "../finding-display.js";
|
|
3
|
+
import { findingSequenceSchema } from "../../contract/findings.js";
|
|
4
|
+
export async function finding(id, ctx, options = {}) {
|
|
5
|
+
const value = await loadFinding(id, ctx.api, options.projectId, options.numbers ?? false);
|
|
6
|
+
const showNumber = ctx.findingNumbers !== false && findingSequenceSchema.safeParse(value.seq).success;
|
|
7
|
+
options.onLoaded?.(value);
|
|
8
|
+
const number = showNumber ? `${formatFindingNumber(value.seq)} ` : "";
|
|
9
|
+
ctx.write(`${number}${value.title}\n${value.explanation}\n`);
|
|
4
10
|
for (const quote of value.quotes) {
|
|
5
11
|
ctx.write(`\n${quote.path}${quote.line === null ? "" : `:${quote.line}`}\n`);
|
|
6
12
|
if (quote.text !== null)
|
package/dist/cli/context.js
CHANGED
|
@@ -6,15 +6,18 @@ import { openBrowser } from "./browser.js";
|
|
|
6
6
|
import { readCredentials } from "./credentials.js";
|
|
7
7
|
import { userEmail } from "./git.js";
|
|
8
8
|
import { terminalSignInInput } from "./sign-in-input.js";
|
|
9
|
+
import { readFindingSettings } from "./finding-settings.js";
|
|
9
10
|
export function commandContext(options = {}) {
|
|
10
11
|
const startup = readCliSettings(options.env, options.emailSignIn);
|
|
11
12
|
const homeDir = options.homeDir ?? homedir();
|
|
12
13
|
const baseUrl = (options.baseUrl ?? startup.baseUrl ?? DEFAULT_URL).replace(/\/+$/, "");
|
|
13
14
|
const api = new ApiClient(baseUrl, () => readCredentials(homeDir, api.baseUrl), undefined, options.signal);
|
|
14
15
|
const write = options.write ?? (text => { process.stdout.write(text); });
|
|
16
|
+
const findingSettings = options.findingSettings ?? readFindingSettings(options.env);
|
|
15
17
|
return {
|
|
18
|
+
findingNumbers: findingSettings.numbersEnabled,
|
|
16
19
|
startup, signal: options.signal, relayRuntime: options.relayRuntime, cwd: options.cwd ?? process.cwd(),
|
|
17
|
-
token: () => readCredentials(homeDir, api.baseUrl), onCheckComplete: options.onCheckComplete, onCheckStart: options.onCheckStart,
|
|
20
|
+
token: () => readCredentials(homeDir, api.baseUrl), onCheckComplete: options.onCheckComplete, onCheckStart: options.onCheckStart, onAgentChoice: options.onAgentChoice,
|
|
18
21
|
api, homeDir, write, openBrowser: options.openBrowser ?? openBrowser,
|
|
19
22
|
signInInput: options.signInInput ?? terminalSignInInput(), readEmail: options.readEmail ?? userEmail,
|
|
20
23
|
sleep: options.sleep ?? (async (ms) => { await delay(ms, undefined, { signal: options.signal }); }), now: options.now ?? Date.now,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { groupByAudience } from "../analysis-core/analysis/schemas.js";
|
|
2
|
+
export const TEAM_WORKFLOW_HEADING = "Team workflow";
|
|
3
|
+
export const formatFindingNumber = (seq) => `F-${seq}`;
|
|
4
|
+
/** Intake returns one id per payload finding in order; the optional number mapping is joined by id. */
|
|
5
|
+
export function savedFindings(result, report) {
|
|
6
|
+
const numbers = new Map(result.findings?.map(value => [value.id, value]));
|
|
7
|
+
return result.findingIds.map((id, index) => {
|
|
8
|
+
const numbered = numbers.get(id), local = report[index];
|
|
9
|
+
return { id, ...(numbered ? { seq: numbered.seq, number: formatFindingNumber(numbered.seq) } : {}),
|
|
10
|
+
title: local?.title ?? "", ...(local?.audience === undefined ? {} : { audience: local.audience }) };
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
function findingLine(value, enabled, index, labels, audienceEnabled) {
|
|
14
|
+
// The off switch preserves the original Markdown summary, including whitespace.
|
|
15
|
+
if (labels === "position" && !audienceEnabled)
|
|
16
|
+
return `${index + 1}. ${value.title}`;
|
|
17
|
+
const title = value.title.replace(/[\r\n\u2028\u2029]+/g, " ");
|
|
18
|
+
const label = labels === "position" ? `${index + 1}.` : enabled && value.seq !== undefined ? formatFindingNumber(value.seq) : value.id ?? `${index + 1}.`;
|
|
19
|
+
return `${label}${title ? ` ${title}` : ""}`;
|
|
20
|
+
}
|
|
21
|
+
export function findingDisplayRows(findings, { numbersEnabled = true, audienceEnabled, labels }) {
|
|
22
|
+
const groups = audienceEnabled ? groupByAudience(findings) : { users: findings, team: [] };
|
|
23
|
+
return [...groups.users, ...groups.team].map((finding, index) => ({ finding,
|
|
24
|
+
label: findingLine(finding, numbersEnabled, index, labels, audienceEnabled), startsTeamGroup: groups.team.length > 0 && index === groups.users.length }));
|
|
25
|
+
}
|
|
26
|
+
export function formatSavedFindings(result, options) {
|
|
27
|
+
if (options.numbersEnabled !== false && result.findings?.some(value => value.seq !== undefined)) {
|
|
28
|
+
return "Findings:\n" + findingDisplayRows(result.findings, { ...options, labels: "reference" }).map(row => `${row.startsTeamGroup ? `${TEAM_WORKFLOW_HEADING}:\n` : ""}${row.label}\n`).join("");
|
|
29
|
+
}
|
|
30
|
+
return result.findingIds?.length ? `Finding ids: ${result.findingIds.join(", ")}\n` : "";
|
|
31
|
+
}
|
|
32
|
+
export function formatWaitingFixes(items, enabled, needsProject) {
|
|
33
|
+
if (!items.length)
|
|
34
|
+
return "No waiting fixes.";
|
|
35
|
+
return "Waiting fixes:\n" + findingDisplayRows(items.map(item => ({ ...item, id: item.findingId })), { numbersEnabled: enabled, audienceEnabled: false, labels: "reference" }).map(({ finding: item, label }) => {
|
|
36
|
+
const scope = [item.repoKey, ...(enabled && needsProject(item) ? [`project ${item.projectId}`] : [])].filter(Boolean);
|
|
37
|
+
return `${label}${scope.length ? ` (${scope.join("; ")})` : ""}\n`;
|
|
38
|
+
}).join("");
|
|
39
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { canonicalUuidSchema, findingNumberParamsSchema, findingNumberResponseSchema, findingSequenceSchema } from "../contract/findings.js";
|
|
2
|
+
import { cliApiMessages } from "../contract/cli-api-messages.js";
|
|
3
|
+
export const FINDING_PROJECT_MESSAGE = "Give the project for this finding number, which analysis results and the waiting_fixes tool list.";
|
|
4
|
+
export const numberedFindingInputSchema = { type: "object", properties: {
|
|
5
|
+
id: { type: "string", description: "A finding id, or a number such as F-10." },
|
|
6
|
+
projectId: { type: "string", description: "The project the finding number belongs to, needed unless this session knows only one project." },
|
|
7
|
+
}, required: ["id"], additionalProperties: false };
|
|
8
|
+
export function findingNumber(value) {
|
|
9
|
+
const match = /^F-?([1-9][0-9]*)$/i.exec(value.trim());
|
|
10
|
+
const seq = match ? Number(match[1]) : undefined;
|
|
11
|
+
return seq !== undefined && Number.isSafeInteger(seq) ? seq : undefined;
|
|
12
|
+
}
|
|
13
|
+
export class FindingReferences {
|
|
14
|
+
projects = new Set();
|
|
15
|
+
analysedProjects = new Set();
|
|
16
|
+
remember(projectId, source = "analysis") {
|
|
17
|
+
const canonical = projectId.toLowerCase();
|
|
18
|
+
this.projects.add(canonical);
|
|
19
|
+
if (source === "analysis")
|
|
20
|
+
this.analysedProjects.add(canonical);
|
|
21
|
+
}
|
|
22
|
+
// Observed findings cannot prove another project lacks the same historical number.
|
|
23
|
+
project() { return this.projects.size === 1 ? [...this.projects][0] : undefined; }
|
|
24
|
+
async resolve(reference, projectId, readWaiting) {
|
|
25
|
+
const seq = findingNumber(reference);
|
|
26
|
+
if (projectId || seq === undefined)
|
|
27
|
+
return projectId;
|
|
28
|
+
let waiting = [];
|
|
29
|
+
try {
|
|
30
|
+
waiting = await readWaiting?.() ?? [];
|
|
31
|
+
}
|
|
32
|
+
catch { /* A known project can still resolve a finding when the queue is unavailable. */ }
|
|
33
|
+
return this.resolveNumber(this.waitingByNumber(waiting).get(seq) ?? []);
|
|
34
|
+
}
|
|
35
|
+
projectsNeeded(waiting) {
|
|
36
|
+
const needed = new Set();
|
|
37
|
+
for (const matches of this.waitingByNumber(waiting).values()) {
|
|
38
|
+
const project = this.resolveNumber(matches);
|
|
39
|
+
for (const item of matches)
|
|
40
|
+
if (project !== item.projectId.toLowerCase())
|
|
41
|
+
needed.add(item.findingId);
|
|
42
|
+
}
|
|
43
|
+
return needed;
|
|
44
|
+
}
|
|
45
|
+
waitingByNumber(waiting) {
|
|
46
|
+
const numbers = new Map();
|
|
47
|
+
for (const item of waiting) {
|
|
48
|
+
this.remember(item.projectId, "waiting");
|
|
49
|
+
const matches = numbers.get(item.seq);
|
|
50
|
+
if (matches)
|
|
51
|
+
matches.push(item);
|
|
52
|
+
else
|
|
53
|
+
numbers.set(item.seq, [item]);
|
|
54
|
+
}
|
|
55
|
+
return numbers;
|
|
56
|
+
}
|
|
57
|
+
resolveNumber(matches) {
|
|
58
|
+
if (!matches.length && this.analysedProjects.size === 1)
|
|
59
|
+
return [...this.analysedProjects][0];
|
|
60
|
+
if (matches.length !== 1)
|
|
61
|
+
return this.project();
|
|
62
|
+
const candidate = matches[0].projectId.toLowerCase();
|
|
63
|
+
// A queued fix may be older than the project the user just analysed.
|
|
64
|
+
if (this.analysedProjects.size > 0 && (this.analysedProjects.size !== 1 || !this.analysedProjects.has(candidate)))
|
|
65
|
+
return undefined;
|
|
66
|
+
return candidate;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export async function loadFinding(reference, api, projectId, numbered = true) {
|
|
70
|
+
const seq = numbered ? findingNumber(reference) : undefined;
|
|
71
|
+
let id = reference;
|
|
72
|
+
if (seq !== undefined) {
|
|
73
|
+
if (!projectId)
|
|
74
|
+
throw new Error(FINDING_PROJECT_MESSAGE);
|
|
75
|
+
const params = findingNumberParamsSchema.safeParse({ projectId, seq: String(seq) });
|
|
76
|
+
if (!params.success)
|
|
77
|
+
throw new Error(FINDING_PROJECT_MESSAGE);
|
|
78
|
+
const result = findingNumberResponseSchema.safeParse(await api.request(`projects/${params.data.projectId}/findings/${params.data.seq}`));
|
|
79
|
+
if (!result.success || result.data.seq !== seq)
|
|
80
|
+
throw new Error(cliApiMessages.invalid_response);
|
|
81
|
+
id = result.data.id;
|
|
82
|
+
}
|
|
83
|
+
const value = await api.request(`findings/${encodeURIComponent(id)}`);
|
|
84
|
+
const canonicalId = canonicalUuidSchema.safeParse(value.id);
|
|
85
|
+
if (seq !== undefined && (!canonicalId.success || canonicalId.data !== id || value.seq !== undefined && value.seq !== seq)) {
|
|
86
|
+
throw new Error(cliApiMessages.invalid_response);
|
|
87
|
+
}
|
|
88
|
+
if (seq !== undefined)
|
|
89
|
+
return { ...value, id, seq };
|
|
90
|
+
const { seq: detailSeq, ...detail } = value;
|
|
91
|
+
return numbered && detailSeq !== undefined && !findingSequenceSchema.safeParse(detailSeq).success ? detail : value;
|
|
92
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { findingAudienceEnabled } from "../analysis-core/analysis/finding-audience-switch.js";
|
|
2
|
+
export function readFindingSettings(env = process.env, bundle) {
|
|
3
|
+
return { numbersEnabled: env.CF_FINDING_NUMBERS !== "0", fixesEnabled: env.CF_WAITING_FIXES !== "0",
|
|
4
|
+
audienceEnabled: findingAudienceEnabled(env, bundle) };
|
|
5
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { waitingFixesResponseSchema, claimFixRequestParamsSchema, claimFixRequestResponseSchema } from "../contract/fix-requests.js";
|
|
2
|
+
import { cliApiMessages } from "../contract/cli-api-messages.js";
|
|
3
|
+
import { ApiError } from "./api.js";
|
|
4
|
+
export const FIX_DISCOVERY_TIMEOUT_MS = 2_000;
|
|
5
|
+
export const fixRequestTools = [
|
|
6
|
+
{ name: "waiting_fixes", description: "Lists the fixes you asked for in the console that are still waiting. It is read-only and free.",
|
|
7
|
+
annotations: { readOnlyHint: true },
|
|
8
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
9
|
+
{ name: "claim_fix", description: "Marks a waiting fix as taken when you start it, without editing files or running an analysis.",
|
|
10
|
+
inputSchema: { type: "object", properties: { id: { type: "string", description: "The findingId returned by waiting_fixes." } }, required: ["id"], additionalProperties: false } },
|
|
11
|
+
];
|
|
12
|
+
export async function waitingFixes(api, options = {}) {
|
|
13
|
+
try {
|
|
14
|
+
const result = waitingFixesResponseSchema.safeParse(await api.request("fix-requests", undefined, true, options));
|
|
15
|
+
if (!result.success)
|
|
16
|
+
throw new Error(cliApiMessages.invalid_response);
|
|
17
|
+
return result.data;
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
// An older console has no queue; read-only discovery still works during rollout.
|
|
21
|
+
if (error instanceof ApiError && error.status === 404)
|
|
22
|
+
return [];
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function waitingFixCount(api) {
|
|
27
|
+
try {
|
|
28
|
+
return (await waitingFixes(api, { timeoutMs: FIX_DISCOVERY_TIMEOUT_MS })).length;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function claimFix(api, id) {
|
|
35
|
+
const params = claimFixRequestParamsSchema.safeParse({ id });
|
|
36
|
+
if (!params.success)
|
|
37
|
+
throw new Error("Provide a finding id.");
|
|
38
|
+
const result = claimFixRequestResponseSchema.safeParse(await api.request(`fix-requests/${params.data.id}/claim`, {}));
|
|
39
|
+
if (!result.success || result.data.findingId !== params.data.id)
|
|
40
|
+
throw new Error(cliApiMessages.invalid_response);
|
|
41
|
+
return result.data;
|
|
42
|
+
}
|
|
@@ -1,18 +1,26 @@
|
|
|
1
|
+
import { resultSafetyCheckEnabled } from "../analysis-core/settings.js";
|
|
1
2
|
import { localFindingsPayloadSchema } from "../contract/console.js";
|
|
3
|
+
import { projectPublicFinding } from "../contract/public-report.js";
|
|
4
|
+
import { checkIntakeFindings } from "./result-check.js";
|
|
2
5
|
import { RESULTS_TOO_LARGE_MESSAGE } from "../contract/cli-refusals.js";
|
|
3
|
-
export function buildIntakePayload(identity, agent, report) {
|
|
6
|
+
export function buildIntakePayload(identity, agent, report, options = {}) {
|
|
4
7
|
const repo = (key) => key === null || key === report.repoKey ? agent.repository : key;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
let findings = report.findings.map(finding => projectPublicFinding(finding, repo));
|
|
9
|
+
let resultCheck = null;
|
|
10
|
+
let findingIndexes = report.findings.map((_, index) => index);
|
|
11
|
+
if (resultSafetyCheckEnabled(options.settings ?? {})) {
|
|
12
|
+
const captured = options.files;
|
|
13
|
+
if (!captured)
|
|
14
|
+
throw new Error("Result check requires a captured file snapshot");
|
|
15
|
+
const admitted = Object.fromEntries(agent.files.filter(file => Object.hasOwn(captured, file.path))
|
|
16
|
+
.map(file => [file.path, captured[file.path]]));
|
|
17
|
+
const { findings: accepted, acceptedIndexes, ...counts } = checkIntakeFindings(findings, { repoKey: agent.repository, files: admitted });
|
|
18
|
+
findings = accepted;
|
|
19
|
+
resultCheck = counts;
|
|
20
|
+
findingIndexes = acceptedIndexes;
|
|
21
|
+
}
|
|
14
22
|
const result = localFindingsPayloadSchema.safeParse({ ...identity, agent, findings });
|
|
15
23
|
if (!result.success)
|
|
16
24
|
throw new Error(RESULTS_TOO_LARGE_MESSAGE);
|
|
17
|
-
return result.data;
|
|
25
|
+
return { payload: result.data, resultCheck, findingIndexes };
|
|
18
26
|
}
|
package/dist/cli/main.js
CHANGED
|
@@ -4,13 +4,13 @@ import { realpath } from "node:fs/promises";
|
|
|
4
4
|
import { errorCode, errorText } from "./api.js";
|
|
5
5
|
import { readEmailSignIn } from "../adapters/settings.js";
|
|
6
6
|
import { commandContext } from "./context.js";
|
|
7
|
-
import { analyze } from "./commands/analyze.js";
|
|
7
|
+
import { analyze, parseAnalyzeOptions } from "./commands/analyze.js";
|
|
8
8
|
import { finding } from "./commands/finding.js";
|
|
9
9
|
import { balance } from "./commands/balance.js";
|
|
10
10
|
import { login, parseLoginOptions } from "./commands/login.js";
|
|
11
11
|
import { logout } from "./commands/logout.js";
|
|
12
12
|
import { serveMcp } from "./mcp.js";
|
|
13
|
-
const usage = "Usage: counterfact-labs analyze [path]\n counterfact-labs finding <id>\n counterfact-labs balance | login | logout | mcp\n";
|
|
13
|
+
const usage = "Usage: counterfact-labs analyze [path] [--proposal <json-file>]\n counterfact-labs finding <id>\n counterfact-labs balance | login | logout | mcp\n";
|
|
14
14
|
export async function run(args, options = {}) {
|
|
15
15
|
const write = options.write ?? (text => { process.stdout.write(text); });
|
|
16
16
|
const emailSignIn = options.emailSignIn ?? readEmailSignIn(options.env);
|
|
@@ -22,9 +22,14 @@ export async function run(args, options = {}) {
|
|
|
22
22
|
return 0;
|
|
23
23
|
}
|
|
24
24
|
if (command === "analyze") {
|
|
25
|
-
|
|
25
|
+
let parsed;
|
|
26
|
+
try {
|
|
27
|
+
parsed = parseAnalyzeOptions(rest);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
26
30
|
throw new Error(usage.trimEnd());
|
|
27
|
-
|
|
31
|
+
}
|
|
32
|
+
return await analyze(parsed.path, parsed.options, commandContext(commandOptions));
|
|
28
33
|
}
|
|
29
34
|
if (command === "finding" && rest.length === 1 && !rest[0].startsWith("-"))
|
|
30
35
|
return await finding(rest[0], commandContext(commandOptions));
|
package/dist/cli/mcp.js
CHANGED
|
@@ -15,11 +15,16 @@ import { emailSignInMessages } from "../contract/email-sign-in-messages.js";
|
|
|
15
15
|
import { EMAIL_SIGN_IN_TERMS, normalizeSignInEmail, validSignInCode, startEmailSignIn, signInWithCode } from "./email-sign-in.js";
|
|
16
16
|
import { resolveAgentFolder, resolveMcpAgentFolder } from "./agent-folder.js";
|
|
17
17
|
import { suggestedEmail } from "./suggested-email.js";
|
|
18
|
+
import { agentProposalInputSchema } from "../contract/agent-proposal.js";
|
|
19
|
+
import { fixRequestTools, waitingFixes, waitingFixCount, claimFix, FIX_DISCOVERY_TIMEOUT_MS } from "./fix-requests.js";
|
|
20
|
+
import { FINDING_PROJECT_MESSAGE, FindingReferences, numberedFindingInputSchema } from "./finding-lookup.js";
|
|
21
|
+
import { findingDisplayRows, formatFindingNumber, formatWaitingFixes } from "./finding-display.js";
|
|
22
|
+
import { readFindingSettings } from "./finding-settings.js";
|
|
18
23
|
const legacyTools = [
|
|
19
24
|
{
|
|
20
25
|
name: ANALYZE_TOOL_NAME,
|
|
21
26
|
description: "Analyze the agent in a folder on this computer. Each call runs one analysis and charges the user's balance, so call it only when the user asks for an analysis. After edits, suggest an analysis and wait for the user to ask. Prices are listed on the top-up page and website. If the balance doesn't cover the analysis, show the returned message and top-up link.",
|
|
22
|
-
inputSchema: { type: "object", properties: { path: { type: "string", description: "The agent folder on this computer." } }, required: ["path"], additionalProperties: false },
|
|
27
|
+
inputSchema: { type: "object", properties: { path: { type: "string", description: "The agent folder on this computer." }, proposal: { ...agentProposalInputSchema, description: "Optional: the files you believe make up the agent, with each file's role. Paths are relative to the agent folder." } }, required: ["path"], additionalProperties: false },
|
|
23
28
|
},
|
|
24
29
|
{ name: "get_finding", description: "Get a finding's explanation, quoted lines, and suggested fixes.", inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"], additionalProperties: false } },
|
|
25
30
|
{ name: "get_balance", description: "Get the balance and top-up link.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
@@ -35,12 +40,16 @@ class ProtocolError extends Error {
|
|
|
35
40
|
}
|
|
36
41
|
function object(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
|
|
37
42
|
export async function serveMcp(input, output, options = {}) {
|
|
43
|
+
const audienceChoice = (options.env ?? process.env).CF_FINDING_AUDIENCE;
|
|
38
44
|
const cwd = options.cwd ?? process.cwd();
|
|
39
45
|
const guarded = (options.env ?? process.env).CF_MCP_PATH_GUARD !== "0";
|
|
40
46
|
const launch = guarded ? resolveFolderPath(cwd).catch(() => null) : Promise.resolve(cwd);
|
|
41
47
|
const lastAnalysis = new Map();
|
|
48
|
+
const findingSettings = readFindingSettings(options.env);
|
|
49
|
+
const { numbersEnabled, fixesEnabled } = findingSettings;
|
|
50
|
+
const references = new FindingReferences();
|
|
42
51
|
const emailSignIn = options.emailSignIn ?? readEmailSignIn(options.env);
|
|
43
|
-
const
|
|
52
|
+
const signInTools = emailSignIn ? [...legacyTools.slice(0, 3),
|
|
44
53
|
{
|
|
45
54
|
name: "sign_in_status",
|
|
46
55
|
description: "Check whether this computer is signed in to Counterfact and whether the user has allowed the analysis tool in Claude Code. Use path only to choose the agent folder for the local git email suggestion; otherwise use the launch folder. Analysis permission is always checked in the launch folder. A valid local git email is returned as suggestedEmail. Prefer the signed-in user's email already in your context; otherwise offer suggestedEmail. Ask the user to confirm an address before sending a code. With wait: true, wait only for browser sign-in to finish.",
|
|
@@ -65,6 +74,9 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
65
74
|
}, required: ["email", "code"], additionalProperties: false },
|
|
66
75
|
},
|
|
67
76
|
] : legacyTools;
|
|
77
|
+
const tools = signInTools.map(tool => tool.name === "get_finding" && numbersEnabled ? {
|
|
78
|
+
...tool, inputSchema: numberedFindingInputSchema,
|
|
79
|
+
} : tool).concat(fixesEnabled ? fixRequestTools : []);
|
|
68
80
|
let pendingSignIn;
|
|
69
81
|
const requests = new Map();
|
|
70
82
|
let queue = Promise.resolve();
|
|
@@ -76,15 +88,18 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
76
88
|
if (!object(args))
|
|
77
89
|
throw new ProtocolError(-32602, "Tool arguments must be an object.");
|
|
78
90
|
const name = params.name;
|
|
79
|
-
const allowed = name === ANALYZE_TOOL_NAME ? ["path"] : name === "get_finding" ? ["id"]
|
|
80
|
-
: name === "
|
|
81
|
-
:
|
|
91
|
+
const allowed = name === ANALYZE_TOOL_NAME ? ["path", "proposal"] : name === "get_finding" ? (numbersEnabled ? Object.keys(numberedFindingInputSchema.properties) : ["id"])
|
|
92
|
+
: name === "claim_fix" ? ["id"]
|
|
93
|
+
: name === "sign_in_status" ? (emailSignIn ? ["wait", "path"] : ["wait"])
|
|
94
|
+
: emailSignIn && name === "sign_in" ? ["email"] : emailSignIn && name === "sign_in_with_code" ? ["email", "code"] : [];
|
|
82
95
|
if (!tools.some(tool => tool.name === name) || Object.keys(args).some(key => !allowed.includes(key)))
|
|
83
96
|
throw new ProtocolError(-32602, "Unknown tool or argument.");
|
|
84
97
|
if (name === ANALYZE_TOOL_NAME && (typeof args.path !== "string" || !args.path.trim()))
|
|
85
98
|
throw new ProtocolError(-32602, "Provide a path.");
|
|
86
99
|
if (name === "get_finding" && (typeof args.id !== "string" || !args.id.trim()))
|
|
87
100
|
throw new ProtocolError(-32602, "Provide a finding id.");
|
|
101
|
+
if (name === "get_finding" && args.projectId !== undefined && (typeof args.projectId !== "string" || !args.projectId.trim()))
|
|
102
|
+
throw new ProtocolError(-32602, FINDING_PROJECT_MESSAGE);
|
|
88
103
|
if (name === "sign_in_status" && args.wait !== undefined && typeof args.wait !== "boolean")
|
|
89
104
|
throw new ProtocolError(-32602, "Provide an optional boolean wait.");
|
|
90
105
|
if (name === "sign_in_status" && "path" in args && (typeof args.path !== "string" || !args.path.trim()))
|
|
@@ -96,12 +111,30 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
96
111
|
if (name === "sign_in_with_code" && !validSignInCode(args.code))
|
|
97
112
|
throw new ProtocolError(-32602, emailSignInMessages.invalid_code);
|
|
98
113
|
let text = "", exitCode = 0;
|
|
114
|
+
let agentChoice;
|
|
99
115
|
let findingIds;
|
|
100
|
-
let
|
|
116
|
+
let findings, projectId;
|
|
117
|
+
let loadedFinding;
|
|
118
|
+
let fixRequests, claimed;
|
|
119
|
+
let waitingCount;
|
|
120
|
+
let url, analysisFolder;
|
|
121
|
+
let started = false;
|
|
101
122
|
let signIn;
|
|
102
123
|
try {
|
|
103
|
-
const ctx = commandContext({ ...options,
|
|
124
|
+
const ctx = commandContext({ ...options, env: { ...(options.env ?? process.env), CF_FINDING_AUDIENCE: audienceChoice },
|
|
125
|
+
emailSignIn, findingSettings, signal, onCheckStart: () => {
|
|
126
|
+
started = true;
|
|
127
|
+
if (guarded && analysisFolder)
|
|
128
|
+
lastAnalysis.set(analysisFolder, ctx.now());
|
|
129
|
+
options.onCheckStart?.();
|
|
130
|
+
}, onAgentChoice: choice => { agentChoice = choice; options.onAgentChoice?.(choice); }, onCheckComplete: result => {
|
|
104
131
|
findingIds = result.findingIds ?? [];
|
|
132
|
+
if (numbersEnabled) {
|
|
133
|
+
findings = findingDisplayRows(result.findings ?? [], { numbersEnabled,
|
|
134
|
+
audienceEnabled: result.audienceEnabled, labels: "reference" }).map(row => row.finding);
|
|
135
|
+
projectId = result.projectId;
|
|
136
|
+
references.remember(result.projectId);
|
|
137
|
+
}
|
|
105
138
|
url = result.url;
|
|
106
139
|
options.onCheckComplete?.(result);
|
|
107
140
|
}, write: chunk => {
|
|
@@ -109,8 +142,23 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
109
142
|
if (!chunk.startsWith("\r"))
|
|
110
143
|
text += chunk;
|
|
111
144
|
} });
|
|
112
|
-
if (name === "
|
|
113
|
-
await
|
|
145
|
+
if (name === "waiting_fixes") {
|
|
146
|
+
const waiting = await waitingFixes(ctx.api);
|
|
147
|
+
fixRequests = waiting;
|
|
148
|
+
const projectsNeeded = references.projectsNeeded(waiting);
|
|
149
|
+
text = formatWaitingFixes(waiting, numbersEnabled, item => projectsNeeded.has(item.findingId));
|
|
150
|
+
}
|
|
151
|
+
else if (name === "claim_fix") {
|
|
152
|
+
claimed = await claimFix(ctx.api, args.id);
|
|
153
|
+
text = "The fix request is marked as taken.";
|
|
154
|
+
}
|
|
155
|
+
else if (name === "get_finding") {
|
|
156
|
+
const project = numbersEnabled ? await references.resolve(args.id, args.projectId, fixesEnabled ? () => waitingFixes(ctx.api, { timeoutMs: FIX_DISCOVERY_TIMEOUT_MS }) : undefined) : undefined;
|
|
157
|
+
await finding(args.id, ctx, { projectId: project, numbers: numbersEnabled, onLoaded: value => {
|
|
158
|
+
if (numbersEnabled)
|
|
159
|
+
loadedFinding = { ...value, ...(value.seq !== undefined ? { number: formatFindingNumber(value.seq) } : {}) };
|
|
160
|
+
} });
|
|
161
|
+
}
|
|
114
162
|
else if (name === "get_balance")
|
|
115
163
|
await balance(ctx);
|
|
116
164
|
else if (name === "sign_in_status") {
|
|
@@ -129,6 +177,8 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
129
177
|
}
|
|
130
178
|
}
|
|
131
179
|
signIn = { ...await signInStatus(ctx), analyzeToolAllowed: allowed };
|
|
180
|
+
if (fixesEnabled)
|
|
181
|
+
waitingCount = signIn.signedIn ? await waitingFixCount(ctx.api) : 0;
|
|
132
182
|
text = signIn.signedIn ? "Signed in." : "Not signed in.";
|
|
133
183
|
if (emailSignIn) {
|
|
134
184
|
const suggestion = await suggestedEmail(resolveAgentFolder(args.path ?? ".", cwd), ctx.readEmail);
|
|
@@ -162,7 +212,7 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
162
212
|
signal.addEventListener("abort", cancel, { once: true });
|
|
163
213
|
try {
|
|
164
214
|
signal.throwIfAborted();
|
|
165
|
-
const flow = await startSignIn(commandContext({ ...options, emailSignIn, homeDir: ctx.homeDir, baseUrl: ctx.api.baseUrl, signal: controller.signal }));
|
|
215
|
+
const flow = await startSignIn(commandContext({ ...options, emailSignIn, findingSettings, homeDir: ctx.homeDir, baseUrl: ctx.api.baseUrl, signal: controller.signal }));
|
|
166
216
|
pending = { flow, controller, opened: false };
|
|
167
217
|
pendingSignIn = pending;
|
|
168
218
|
const current = pending;
|
|
@@ -190,14 +240,9 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
190
240
|
if (guarded && last !== undefined && ctx.now() - last < 10 * 60 * 1000) {
|
|
191
241
|
throw new Error(FINISH_FAILURE_MESSAGE);
|
|
192
242
|
}
|
|
193
|
-
|
|
243
|
+
analysisFolder = folder;
|
|
194
244
|
try {
|
|
195
|
-
exitCode = await analyze(folder, {},
|
|
196
|
-
started = true;
|
|
197
|
-
if (guarded)
|
|
198
|
-
lastAnalysis.set(folder, ctx.now());
|
|
199
|
-
ctx.onCheckStart?.();
|
|
200
|
-
} });
|
|
245
|
+
exitCode = await analyze(folder, { proposal: args.proposal }, ctx);
|
|
201
246
|
}
|
|
202
247
|
finally {
|
|
203
248
|
if (guarded && started)
|
|
@@ -212,7 +257,9 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
212
257
|
// Some clients (Claude Code among them) give the model only the structured result when one is present, so it
|
|
213
258
|
// repeats the text: without it the model cannot read the result.
|
|
214
259
|
return { content: [{ type: "text", text }], isError: exitCode !== 0,
|
|
215
|
-
structuredContent: { text, exitCode, ...(findingIds ? { findingIds } : {}), ...(url ? { url } : {}), ...signIn
|
|
260
|
+
structuredContent: { text, exitCode, ...agentChoice, ...(findingIds ? { findingIds } : {}), ...(url ? { url } : {}), ...signIn,
|
|
261
|
+
...(findings ? { findings, projectId } : {}), ...(loadedFinding ? { finding: loadedFinding } : {}),
|
|
262
|
+
...(fixRequests ? { fixRequests } : {}), ...claimed, ...(waitingCount !== undefined ? { waitingFixes: waitingCount } : {}) } };
|
|
216
263
|
}
|
|
217
264
|
async function respond(message, id, signal) {
|
|
218
265
|
if (signal.aborted)
|
package/dist/cli/relay-check.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { resolveFolderPath } from "../adapters/folder-path.js";
|
|
2
|
-
import { savedAnalysisUrl } from "../contract/console-url.js";
|
|
3
1
|
import { randomUUID } from "node:crypto";
|
|
4
2
|
import { disabledSetupPings } from "../setup-pings/client.js";
|
|
5
3
|
import { saveCompletion, loadCompletion } from "../adapters/completion.js";
|
|
6
4
|
import { saveLocalResults } from "../adapters/results.js";
|
|
5
|
+
import { GITIGNORE_OUTPUT_NOTICE } from "../adapters/gitignore-output.js";
|
|
7
6
|
import { OutputPathError, outputPaths } from "../adapters/output-paths.js";
|
|
8
7
|
import { validateClientRelease } from "../adapters/release.js";
|
|
9
8
|
import { selectAnalysisExecution } from "../adapters/models.js";
|
|
9
|
+
import { resolveFolderPath } from "../adapters/folder-path.js";
|
|
10
|
+
import { AgentChoiceError } from "../contract/agent-choice.js";
|
|
11
|
+
import { savedAnalysisUrl } from "../contract/console-url.js";
|
|
10
12
|
import { prepareRelayCheck } from "./relay-preparation.js";
|
|
11
13
|
import { cliRefusals, COMMITTED_FILES_MESSAGE, FINISH_FAILURE_MESSAGE, RESULTS_TOO_LARGE_MESSAGE, refusalExitCode } from "../contract/cli-refusals.js";
|
|
12
14
|
import { ConsoleClient, ConsoleError } from "../gatekeeper/console.js";
|
|
@@ -14,9 +16,13 @@ import { GatekeeperClient } from "../gatekeeper/client.js";
|
|
|
14
16
|
import { connectRelay, relayOrigin } from "../gatekeeper/connection.js";
|
|
15
17
|
import productionTrust from "../gatekeeper/trust/production.json" with { type: "json" };
|
|
16
18
|
import { runAnalysisToEnd } from "../core/analysis/run-to-end.js";
|
|
19
|
+
import { findingPresentation } from "../analysis-core/findings/presentation.js";
|
|
17
20
|
import { formatSummary } from "./summary.js";
|
|
18
21
|
import { buildIntakePayload } from "./intake-payload.js";
|
|
19
22
|
import { completionRecoveryDecision, COMPLETION_SETTINGS_MISMATCH_MESSAGE, matchesCheck } from "./completion-recovery.js";
|
|
23
|
+
import { savedFindings } from "./finding-display.js";
|
|
24
|
+
import { INVALID_PROPOSAL_MESSAGE } from "../contract/agent-proposal.js";
|
|
25
|
+
import { readFindingSettings } from "./finding-settings.js";
|
|
20
26
|
const stateLine = (status) => `Reservation ${status.state}; settlement ${status.settlement}; lease ${new Date(status.expiresAt).toLocaleString("en-US", { timeZone: "America/Los_Angeles", timeZoneName: "short" })}.\n`;
|
|
21
27
|
function requireIntakeReservation(status) {
|
|
22
28
|
if (status.state === "reserved")
|
|
@@ -31,7 +37,8 @@ function requireIntakeReservation(status) {
|
|
|
31
37
|
async function submit(client, payload, signal) {
|
|
32
38
|
signal.throwIfAborted();
|
|
33
39
|
const status = await client.status(payload.reservationId);
|
|
34
|
-
const committed = (value) => ({ checkId: value.checkId, projectId: value.projectId, analysisId: value.analysisId, findingIds: value.findingIds,
|
|
40
|
+
const committed = (value) => ({ checkId: value.checkId, projectId: value.projectId, analysisId: value.analysisId, findingIds: value.findingIds,
|
|
41
|
+
...(value.findings ? { findings: value.findings } : {}), settlement: value.settlement });
|
|
35
42
|
if (!matchesCheck(status, payload))
|
|
36
43
|
throw new ConsoleError("check_conflict");
|
|
37
44
|
if (status.uploadState === "saved")
|
|
@@ -65,11 +72,14 @@ export function runReservedRelayCheck(folder, options) {
|
|
|
65
72
|
return runRelayLifecycle(folder, options, options);
|
|
66
73
|
}
|
|
67
74
|
async function runRelayLifecycle(folder, options, existing) {
|
|
68
|
-
const { config,
|
|
75
|
+
const { config, stdout, stderr } = options;
|
|
76
|
+
let settings = options.settings;
|
|
77
|
+
const summaryOptions = { audienceEnabled: false };
|
|
69
78
|
let client, reservationId, projectId;
|
|
70
79
|
let connection = existing?.connection;
|
|
71
80
|
let identity;
|
|
72
81
|
let completedPayload;
|
|
82
|
+
let displayFindings;
|
|
73
83
|
let completionOrigin;
|
|
74
84
|
let completed = false, uploaded = false, heartbeat;
|
|
75
85
|
let renewal;
|
|
@@ -77,7 +87,8 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
77
87
|
const signal = options.signal ? AbortSignal.any([controller.signal, options.signal]) : controller.signal;
|
|
78
88
|
const showSaved = (result) => {
|
|
79
89
|
const url = savedAnalysisUrl(result.projectId, config.CF_CONSOLE_URL);
|
|
80
|
-
|
|
90
|
+
const findings = savedFindings({ findingIds: result.findingIds ?? [], findings: result.findings }, displayFindings ?? completedPayload?.findings ?? []);
|
|
91
|
+
options.onSaved?.({ ...result, findings, url, audienceEnabled: summaryOptions.audienceEnabled });
|
|
81
92
|
stdout(`Saved analysis ${result.analysisId}: ${url}\n`);
|
|
82
93
|
};
|
|
83
94
|
try {
|
|
@@ -100,6 +111,8 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
100
111
|
// Signature verification precedes all folder reads, status calls and spending.
|
|
101
112
|
const validated = validateClientRelease(options.release, settings);
|
|
102
113
|
const { bundle } = validated;
|
|
114
|
+
settings = validated.settings;
|
|
115
|
+
summaryOptions.audienceEnabled = readFindingSettings(settings).audienceEnabled;
|
|
103
116
|
folder = await resolveFolderPath(folder);
|
|
104
117
|
signal.throwIfAborted();
|
|
105
118
|
const prior = await loadCompletion(folder);
|
|
@@ -134,7 +147,7 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
134
147
|
if (error.code !== "ENOENT")
|
|
135
148
|
throw error;
|
|
136
149
|
}
|
|
137
|
-
const { source, provenance, plan, start, startMetadata } = await prepareRelayCheck(folder, { ...options, signal, expectedCommit: existing?.expectedCommit }, validated);
|
|
150
|
+
const { source, provenance, plan, start, startMetadata } = await prepareRelayCheck(folder, { ...options, signal, expectedCommit: existing?.expectedCommit, expectedFiles: existing?.expectedFiles }, validated);
|
|
138
151
|
let reservation = existing?.reservation;
|
|
139
152
|
if (!reservation) {
|
|
140
153
|
const started = await client.start(start, startMetadata);
|
|
@@ -171,9 +184,28 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
171
184
|
(options.setupPings ?? disabledSetupPings).ping("first_analysis_started");
|
|
172
185
|
const report = await runAnalysisToEnd(source, execution.model, settings, { execution });
|
|
173
186
|
signal.throwIfAborted();
|
|
174
|
-
const paths = await saveLocalResults(folder, report, source.skippedFiles, options.outputDir);
|
|
175
|
-
|
|
176
|
-
|
|
187
|
+
const paths = await saveLocalResults(folder, report, source.skippedFiles, options.outputDir, { ...summaryOptions, settings });
|
|
188
|
+
try {
|
|
189
|
+
options.onLocalSaved?.(paths);
|
|
190
|
+
if (paths.ignoreAdded)
|
|
191
|
+
stderr(GITIGNORE_OUTPUT_NOTICE);
|
|
192
|
+
}
|
|
193
|
+
catch { /* Output failure must not change a server outcome. */ }
|
|
194
|
+
const { payload, resultCheck, findingIndexes } = buildIntakePayload({ checkId: reservation.checkId, projectId: projectId, reservationId,
|
|
195
|
+
bundleVersion: bundle.version, relayVersion: options.release.relayVersion, counterfactLocalVersion: options.release.localVersion }, provenance, report, { settings, files: source.snapshot });
|
|
196
|
+
displayFindings = findingIndexes.map(index => {
|
|
197
|
+
const finding = report.findings[index];
|
|
198
|
+
return { title: findingPresentation(finding).displayTitle, audience: finding.audience };
|
|
199
|
+
});
|
|
200
|
+
if (resultCheck?.droppedFindings) {
|
|
201
|
+
const count = resultCheck.droppedFindings;
|
|
202
|
+
const reason = resultCheck.schemaDroppedFindings || resultCheck.capDroppedFindings
|
|
203
|
+
? `${count === 1 ? "it was" : "they were"} too long or not in the expected format` : `${count === 1 ? "its" : "their"} quotes did not match your files`;
|
|
204
|
+
try {
|
|
205
|
+
stderr(`${count} ${count === 1 ? "finding was" : "findings were"} not uploaded because ${reason}.\n`);
|
|
206
|
+
}
|
|
207
|
+
catch { /* Output failure must not change a server outcome. */ }
|
|
208
|
+
}
|
|
177
209
|
completionOrigin = new URL(config.CF_CONSOLE_URL).origin;
|
|
178
210
|
await saveCompletion(folder, payload, completionOrigin);
|
|
179
211
|
completedPayload = payload;
|
|
@@ -181,7 +213,7 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
181
213
|
const saved = await submit(client, payload, signal);
|
|
182
214
|
uploaded = true;
|
|
183
215
|
await saveCompletion(folder, payload, completionOrigin, true);
|
|
184
|
-
stdout(formatSummary(report));
|
|
216
|
+
stdout(formatSummary(report, summaryOptions));
|
|
185
217
|
stdout(`\nSaved ${paths.findingsPath}\nSaved ${paths.summaryPath}\n`);
|
|
186
218
|
showSaved(saved);
|
|
187
219
|
return 0;
|
|
@@ -204,7 +236,7 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
204
236
|
catch { /* Committed server state still survives a local write failure. */ }
|
|
205
237
|
}
|
|
206
238
|
try {
|
|
207
|
-
showSaved({ projectId: status.projectId, analysisId: status.analysisId, findingIds: status.findingIds });
|
|
239
|
+
showSaved({ projectId: status.projectId, analysisId: status.analysisId, findingIds: status.findingIds, findings: status.findings });
|
|
208
240
|
}
|
|
209
241
|
catch { /* Output failure must not change a server outcome. */ }
|
|
210
242
|
return 0;
|
|
@@ -216,11 +248,12 @@ async function runRelayLifecycle(folder, options, existing) {
|
|
|
216
248
|
}
|
|
217
249
|
catch { /* Keep the pending record if the durable write cannot be confirmed. */ }
|
|
218
250
|
}
|
|
219
|
-
const known = error instanceof ConsoleError || error instanceof OutputPathError || error instanceof Error && ([cliRefusals.release_unavailable, COMMITTED_FILES_MESSAGE, RESULTS_TOO_LARGE_MESSAGE].includes(error.message)
|
|
251
|
+
const known = error instanceof AgentChoiceError || error instanceof ConsoleError || error instanceof OutputPathError || error instanceof Error && ([cliRefusals.release_unavailable, COMMITTED_FILES_MESSAGE, RESULTS_TOO_LARGE_MESSAGE, INVALID_PROPOSAL_MESSAGE].includes(error.message)
|
|
220
252
|
|| error.message.startsWith("The saved completion") || error.message === COMPLETION_SETTINGS_MISMATCH_MESSAGE);
|
|
221
253
|
const message = known ? error.message : FINISH_FAILURE_MESSAGE;
|
|
222
254
|
try {
|
|
223
|
-
options.onError?.({ code: error instanceof ConsoleError ? error.code : undefined, message
|
|
255
|
+
options.onError?.({ code: error instanceof ConsoleError ? error.code : undefined, message,
|
|
256
|
+
...(error instanceof AgentChoiceError ? { agentChoice: error.agentChoice } : {}) });
|
|
224
257
|
stderr(`${message}\n`);
|
|
225
258
|
if (status && !(error instanceof ConsoleError && error.code === "low_balance"))
|
|
226
259
|
stderr(stateLine(status));
|