@counterfact-labs/cli 0.2.4 → 0.2.6
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/agent-folder.js +3 -3
- package/dist/cli/commands/analyze.js +39 -6
- package/dist/cli/commands/balance.js +6 -3
- package/dist/cli/commands/finding.js +9 -3
- package/dist/cli/context.js +6 -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-failure.js +8 -0
- package/dist/cli/mcp.js +125 -37
- package/dist/cli/relay-check.js +47 -14
- package/dist/cli/relay-preparation.js +28 -5
- package/dist/cli/result-check.js +68 -0
- package/dist/cli/sign-in.js +61 -2
- 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-api-messages.js +2 -0
- package/dist/contract/cli-refusals.js +69 -1
- package/dist/contract/cli-version.js +1 -1
- package/dist/contract/console.js +21 -16
- package/dist/contract/email-sign-in-messages.js +4 -0
- 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 +28 -9
- package/dist/gatekeeper/connection-exchange.js +17 -12
- package/dist/gatekeeper/console.js +31 -17
- 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
package/dist/cli/sign-in.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { ApiError } from "./api.js";
|
|
2
2
|
import { readCredentials, saveCredentials } from "./credentials.js";
|
|
3
3
|
import { emailSignInMessages } from "../contract/email-sign-in-messages.js";
|
|
4
|
+
import { cliApiMessages } from "../contract/cli-api-messages.js";
|
|
5
|
+
export class SignInExpiredError extends ApiError {
|
|
6
|
+
constructor() { super(400, "expired", emailSignInMessages.browser_expired); }
|
|
7
|
+
}
|
|
8
|
+
export class SignInDeniedError extends Error {
|
|
9
|
+
constructor() { super(emailSignInMessages.cancelled); }
|
|
10
|
+
}
|
|
4
11
|
async function pollSignIn(ctx, device) {
|
|
5
12
|
const until = ctx.now() + device.expiresIn * 1000;
|
|
6
13
|
while (ctx.now() < until) {
|
|
@@ -15,12 +22,64 @@ async function pollSignIn(ctx, device) {
|
|
|
15
22
|
if (poll.status === "denied")
|
|
16
23
|
throw new Error(emailSignInMessages.cancelled);
|
|
17
24
|
}
|
|
18
|
-
throw new
|
|
25
|
+
throw new SignInExpiredError();
|
|
26
|
+
}
|
|
27
|
+
async function pollSignInWithRetry(ctx, device) {
|
|
28
|
+
const until = ctx.now() + device.expiresIn * 1000;
|
|
29
|
+
const interval = device.interval * 1000;
|
|
30
|
+
let delay = interval;
|
|
31
|
+
while (ctx.now() < until) {
|
|
32
|
+
await ctx.sleep(Math.min(delay, until - ctx.now()));
|
|
33
|
+
if (ctx.now() >= until)
|
|
34
|
+
break;
|
|
35
|
+
let poll;
|
|
36
|
+
try {
|
|
37
|
+
poll = await ctx.api.request("device/token", { deviceCode: device.deviceCode }, false, { timeoutMs: Math.min(120_000, until - ctx.now()) });
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
ctx.signal?.throwIfAborted();
|
|
41
|
+
if (!(error instanceof ApiError) || !(error.status === 0 || error.status === 408 || error.status === 429
|
|
42
|
+
|| (error.status >= 500 && error.status < 600)))
|
|
43
|
+
throw error;
|
|
44
|
+
// A temporary failure cannot tell us whether the user approved the code.
|
|
45
|
+
delay = Math.max(interval, error.retryAfterMs);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
delay = interval;
|
|
49
|
+
if (!poll)
|
|
50
|
+
throw new Error(cliApiMessages.invalid_response);
|
|
51
|
+
if (poll.status === "ok") {
|
|
52
|
+
// The server has consumed the code, so keep its token even after local expiry.
|
|
53
|
+
await saveCredentials(ctx.homeDir, ctx.api.baseUrl, poll.token);
|
|
54
|
+
return "signed_in";
|
|
55
|
+
}
|
|
56
|
+
if (poll.status === "expired")
|
|
57
|
+
break;
|
|
58
|
+
if (poll.status === "denied")
|
|
59
|
+
throw new SignInDeniedError();
|
|
60
|
+
}
|
|
61
|
+
throw new SignInExpiredError();
|
|
19
62
|
}
|
|
20
63
|
export async function startSignIn(ctx) {
|
|
21
64
|
const device = await ctx.api.request("device", {}, false);
|
|
22
65
|
return { userCode: device.userCode, verificationUri: device.verificationUri,
|
|
23
|
-
pageUrl: `${device.verificationUri}?code=${encodeURIComponent(device.userCode)}`,
|
|
66
|
+
pageUrl: `${device.verificationUri}?code=${encodeURIComponent(device.userCode)}`, retryEnabled: ctx.signInRetry,
|
|
67
|
+
done: ctx.signInRetry ? pollSignInWithRetry(ctx, device) : pollSignIn(ctx, device) };
|
|
68
|
+
}
|
|
69
|
+
export async function waitForSignIn(flow, signal) {
|
|
70
|
+
signal.throwIfAborted();
|
|
71
|
+
let cancel;
|
|
72
|
+
const cancelled = new Promise((_resolve, reject) => {
|
|
73
|
+
cancel = () => reject(signal.reason);
|
|
74
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
75
|
+
});
|
|
76
|
+
// A cancelled tool wait must leave the shared browser sign-in running.
|
|
77
|
+
try {
|
|
78
|
+
return await Promise.race([flow.done, cancelled]);
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
signal.removeEventListener("abort", cancel);
|
|
82
|
+
}
|
|
24
83
|
}
|
|
25
84
|
export async function signInStatus(ctx) {
|
|
26
85
|
if (!await readCredentials(ctx.homeDir, ctx.api.baseUrl))
|
package/dist/cli/summary.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { findingPresentation } from "../analysis-core/findings/presentation.js";
|
|
2
|
-
|
|
2
|
+
import { findingDisplayRows, TEAM_WORKFLOW_HEADING } from "./finding-display.js";
|
|
3
|
+
export function formatSummary(report, options) {
|
|
3
4
|
const lines = ["# Counterfact analysis", "",
|
|
4
5
|
`${report.findings.length} findings; ${report.structures.length} structural proposals.`, ""];
|
|
5
6
|
if (!report.findings.length)
|
|
6
7
|
lines.push("No findings.", "");
|
|
7
|
-
|
|
8
|
+
const findings = report.findings.map(finding => {
|
|
8
9
|
const presentation = findingPresentation(finding);
|
|
9
|
-
|
|
10
|
+
return { ...finding, presentation, title: presentation.displayTitle };
|
|
11
|
+
});
|
|
12
|
+
for (const { finding, label, startsTeamGroup } of findingDisplayRows(findings, { ...options, labels: "position" })) {
|
|
13
|
+
const { presentation } = finding;
|
|
14
|
+
if (startsTeamGroup)
|
|
15
|
+
lines.push(`## ${TEAM_WORKFLOW_HEADING}`, "");
|
|
16
|
+
lines.push(`## ${label}`, "");
|
|
10
17
|
if (presentation.ifLeft)
|
|
11
18
|
lines.push(`If left: ${presentation.ifLeft}`, "");
|
|
12
19
|
lines.push(`Severity: ${finding.severity}; category: ${finding.category ?? "unclassified"}.`, "", finding.cause, "");
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const CHOOSE_AGENT_MESSAGE = "Counterfact couldn't tell which folder holds your agent. Run it again on that folder, for example";
|
|
2
|
+
export const NO_AGENT_MESSAGE = "Counterfact couldn't find an agent prompt in this folder. Pass a folder containing an agent prompt or a model call with a system prompt.";
|
|
3
|
+
/** The local importer supplies the choice; transport keeps its data and wording together. */
|
|
4
|
+
export class AgentChoiceError extends Error {
|
|
5
|
+
agentChoice;
|
|
6
|
+
constructor(agentChoice) {
|
|
7
|
+
const folders = [...new Set(agentChoice.candidates.map(candidate => candidate.folder))];
|
|
8
|
+
super(agentChoice.kind === "choose_agent" ? [`${CHOOSE_AGENT_MESSAGE} counterfact-labs analyze ${shellArgument(folders[0])}.`, "These folders look like agents:", ...folders].join("\n") : NO_AGENT_MESSAGE);
|
|
9
|
+
this.agentChoice = agentChoice;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** Folder names must remain literal when a user pastes the suggested command. */
|
|
13
|
+
function shellArgument(folder) {
|
|
14
|
+
return /^[a-zA-Z0-9_./-]+$/.test(folder) ? folder : `'${folder.replaceAll("'", "'\\''")}'`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { importProposalEnabled } from "../analysis-core/index.js";
|
|
3
|
+
export const INVALID_PROPOSAL_MESSAGE = "The proposed agent files couldn't be used, because a path is outside the folder or isn't a file Counterfact can read.";
|
|
4
|
+
export const INVALID_PROPOSAL_FORMAT_MESSAGE = "The proposed agent files couldn't be used, because the proposal needs an agent name and a list of files with supported roles.";
|
|
5
|
+
// Check both path separators so a proposal has the same meaning on every host.
|
|
6
|
+
export const agentProposalSchema = z.object({
|
|
7
|
+
agentName: z.string().min(1).max(200).regex(/^[^\p{C}\p{Zl}\p{Zp}]+$/u),
|
|
8
|
+
files: z.array(z.object({
|
|
9
|
+
path: z.string().min(1).max(512).regex(/^(?![A-Za-z]:)(?![/\\])(?!.*(?:^|[/\\])\.\.(?:[/\\]|$))[^\p{C}\p{Zl}\p{Zp}]+$/u)
|
|
10
|
+
.describe("Paths are relative to the agent folder."),
|
|
11
|
+
role: z.enum(["prompt", "tools", "model-calling code", "config", "other"]),
|
|
12
|
+
})).max(200),
|
|
13
|
+
});
|
|
14
|
+
export const agentProposalInputSchema = z.toJSONSchema(agentProposalSchema);
|
|
15
|
+
delete agentProposalInputSchema.$schema;
|
|
16
|
+
function validateAgentProposal(value) {
|
|
17
|
+
if (value === undefined)
|
|
18
|
+
return undefined;
|
|
19
|
+
const parsed = agentProposalSchema.safeParse(value);
|
|
20
|
+
if (!parsed.success) {
|
|
21
|
+
const invalidPath = parsed.error.issues.some(issue => issue.path[0] === "files" && issue.path[2] === "path");
|
|
22
|
+
throw new Error(invalidPath ? INVALID_PROPOSAL_MESSAGE : INVALID_PROPOSAL_FORMAT_MESSAGE);
|
|
23
|
+
}
|
|
24
|
+
return parsed.data;
|
|
25
|
+
}
|
|
26
|
+
/** Check the switch before invoking a loader so disabled proposals never open files. */
|
|
27
|
+
export async function loadAgentProposal(load, settings) {
|
|
28
|
+
if (!importProposalEnabled(settings))
|
|
29
|
+
return undefined;
|
|
30
|
+
let value;
|
|
31
|
+
try {
|
|
32
|
+
value = await load();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw new Error("The proposal file couldn't be read as JSON.");
|
|
36
|
+
}
|
|
37
|
+
return validateAgentProposal(value);
|
|
38
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { allowsRuntimeOverrides, checkRuntimeEnvironment } from "./runtime-environment.js";
|
|
3
|
+
// Identity contracts stay independent of analysis requests so probes cannot load private rules.
|
|
4
|
+
export const compactJwtPattern = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
5
|
+
export const hexChallenge = z.string().length(64).regex(/^[0-9a-f]+$/);
|
|
6
|
+
export const PRODUCTION_EVIDENCE_VERSION = 1;
|
|
7
|
+
export const RELAY_CHANNEL_AUDIENCE = "https://relay.counterfactlabs.com";
|
|
8
|
+
// Passthrough forwarding keeps the non-root workload's listener port unchanged.
|
|
9
|
+
export const RELAY_PORT = 8443;
|
|
10
|
+
export const RELAY_URL = `${RELAY_CHANNEL_AUDIENCE}:${RELAY_PORT}`;
|
|
11
|
+
export const RELAY_HEALTH_PATH = "/health";
|
|
12
|
+
export const RELAY_HEALTH_URL = `${RELAY_URL}${RELAY_HEALTH_PATH}`;
|
|
13
|
+
export const GOOGLE_ATTESTATION_ISSUER = "https://confidentialcomputing.googleapis.com";
|
|
14
|
+
export const MAX_ATTESTATION_BYTES = 32 * 1024;
|
|
15
|
+
export const attestationChallenge = z.object({
|
|
16
|
+
version: z.literal(PRODUCTION_EVIDENCE_VERSION),
|
|
17
|
+
challenge: hexChallenge,
|
|
18
|
+
}).strict();
|
|
19
|
+
export const productionEvidence = z.object({
|
|
20
|
+
version: z.literal(PRODUCTION_EVIDENCE_VERSION),
|
|
21
|
+
token: z.string().max(MAX_ATTESTATION_BYTES).regex(compactJwtPattern),
|
|
22
|
+
}).strict();
|
|
23
|
+
/** Google's launcher accepts up to six caller nonces, each at most 74 characters. */
|
|
24
|
+
export const evidenceBinding = z.object({
|
|
25
|
+
audience: z.string().min(1).max(256).regex(/^[A-Za-z0-9:/._-]+$/),
|
|
26
|
+
nonces: z.array(z.string().min(1).max(74).regex(/^[A-Za-z0-9_:-]+$/)).min(1).max(6),
|
|
27
|
+
}).strict();
|
|
28
|
+
/** Native claims are interpreted only after the PKI chain and token signature are authenticated. */
|
|
29
|
+
export const googleAttestationClaims = z.object({
|
|
30
|
+
iss: z.literal(GOOGLE_ATTESTATION_ISSUER),
|
|
31
|
+
aud: z.string(),
|
|
32
|
+
sub: z.string(),
|
|
33
|
+
iat: z.number().int(),
|
|
34
|
+
nbf: z.number().int(),
|
|
35
|
+
exp: z.number().int(),
|
|
36
|
+
eat_nonce: z.union([z.string(), z.array(z.string()).min(1).max(6)]),
|
|
37
|
+
secboot: z.literal(true),
|
|
38
|
+
dbgstat: z.literal("disabled-since-boot"),
|
|
39
|
+
oemid: z.literal(11129),
|
|
40
|
+
hwmodel: z.string(),
|
|
41
|
+
swname: z.literal("CONFIDENTIAL_SPACE"),
|
|
42
|
+
swversion: z.array(z.string()).length(1),
|
|
43
|
+
google_service_accounts: z.array(z.string()).min(1),
|
|
44
|
+
development: z.never().optional(),
|
|
45
|
+
identity: z.never().optional(),
|
|
46
|
+
submods: z.object({
|
|
47
|
+
confidential_space: z.object({
|
|
48
|
+
support_attributes: z.array(z.string()),
|
|
49
|
+
monitoring_enabled: z.object({ memory: z.literal(false) }).passthrough(),
|
|
50
|
+
}).passthrough(),
|
|
51
|
+
container: z.object({
|
|
52
|
+
image_digest: z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
53
|
+
image_signatures: z.array(z.object({
|
|
54
|
+
key_id: z.string(), signature: z.string().min(1),
|
|
55
|
+
signature_algorithm: z.literal("RSASSA_PSS_SHA256"),
|
|
56
|
+
}).passthrough()).min(1),
|
|
57
|
+
// Google omits override claims when nothing was overridden; only the owned clock rollback setting is allowed.
|
|
58
|
+
cmd_override: z.array(z.unknown()).length(0).optional(),
|
|
59
|
+
env_override: z.record(z.string(), z.string()).refine(allowsRuntimeOverrides).optional(),
|
|
60
|
+
// Real tokens list the container's own environment; the relay's start-up rule decides which settings are allowed.
|
|
61
|
+
env: z.record(z.string(), z.string()).refine(env => {
|
|
62
|
+
try {
|
|
63
|
+
checkRuntimeEnvironment(env);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}),
|
|
70
|
+
}).passthrough(),
|
|
71
|
+
}).passthrough(),
|
|
72
|
+
}).passthrough();
|
|
73
|
+
/** Authenticated observations describe a refused workload without granting it an admitted identity. */
|
|
74
|
+
export const observedAttestationClaims = googleAttestationClaims.extend({
|
|
75
|
+
dbgstat: z.string(),
|
|
76
|
+
submods: googleAttestationClaims.shape.submods.extend({
|
|
77
|
+
container: googleAttestationClaims.shape.submods.shape.container.extend({
|
|
78
|
+
image_signatures: z.array(googleAttestationClaims.shape.submods.shape.container.shape.image_signatures.element),
|
|
79
|
+
cmd_override: z.array(z.unknown()).optional(),
|
|
80
|
+
env_override: z.record(z.string(), z.unknown()).optional(),
|
|
81
|
+
}),
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
// Intel TDX omits these arrays for unsigned/debug images; only probes may read absence as "none".
|
|
85
|
+
export const probeAttestationClaims = observedAttestationClaims.extend({
|
|
86
|
+
submods: observedAttestationClaims.shape.submods.extend({
|
|
87
|
+
confidential_space: observedAttestationClaims.shape.submods.shape.confidential_space.extend({
|
|
88
|
+
support_attributes: observedAttestationClaims.shape.submods.shape.confidential_space.shape.support_attributes.default([]),
|
|
89
|
+
}),
|
|
90
|
+
container: observedAttestationClaims.shape.submods.shape.container.extend({
|
|
91
|
+
image_signatures: observedAttestationClaims.shape.submods.shape.container.shape.image_signatures.default([]),
|
|
92
|
+
}),
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
/**
|
|
96
|
+
* Local development claims. These cannot establish production identity; production uses native Google claims.
|
|
97
|
+
*/
|
|
98
|
+
export const attestationClaims = z.object({
|
|
99
|
+
iss: z.string().url(),
|
|
100
|
+
/** The relay image's digest. Must be one we published. */
|
|
101
|
+
imageDigest: z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
102
|
+
/** Hash of the relay's channel public key, so the token is bound to this connection. */
|
|
103
|
+
channelKeyHash: z.string().regex(/^[0-9a-f]{64}$/),
|
|
104
|
+
exp: z.number().int(),
|
|
105
|
+
iat: z.number().int(),
|
|
106
|
+
}).passthrough();
|
|
@@ -2,4 +2,6 @@ export const cliApiMessages = Object.freeze({
|
|
|
2
2
|
unreachable: "Can't reach Counterfact. Try again shortly.",
|
|
3
3
|
invalid_response: "Counterfact returned an invalid response. Try again shortly.",
|
|
4
4
|
request_failed: "The request could not be completed. Try again shortly.",
|
|
5
|
+
unauthorized: "Not signed in. Run counterfact-labs login.",
|
|
6
|
+
invalid_console_url: "COUNTERFACT_URL must be an HTTP or HTTPS address without credentials or query fields.",
|
|
5
7
|
});
|
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
import { FOLDER_TOO_LARGE_MESSAGE } from "../analysis-core/errors.js";
|
|
2
|
+
import { cliApiMessages } from "./cli-api-messages.js";
|
|
3
|
+
import { emailSignInMessages } from "./email-sign-in-messages.js";
|
|
2
4
|
export const START_FAILURE_MESSAGE = "This couldn't start. Try again.";
|
|
3
5
|
export const RESERVATION_EXPIRED_MESSAGE = "This reservation expired. Start a new analysis.";
|
|
4
6
|
export const FINISH_FAILURE_MESSAGE = "The analysis couldn't finish. Try again.";
|
|
5
7
|
export const RESULTS_TOO_LARGE_MESSAGE = "The results were too large to save. Try again.";
|
|
6
8
|
export const COMMITTED_FILES_MESSAGE = "Counterfact analyzes the committed version of the agent's files. Commit your changes, then try again.";
|
|
7
9
|
const unsupportedVersion = "This version of Counterfact can't run analyses. Update it: npm install -g @counterfact-labs/cli@latest.";
|
|
8
|
-
|
|
10
|
+
// The console separates over-limit costs from version or route mismatches, so each code gets its own recovery guidance.
|
|
11
|
+
const analysisTooLarge = "This analysis is too large to run at once. Choose a smaller folder, such as one agent's folder.";
|
|
12
|
+
export const localRefusals = {
|
|
13
|
+
outside_project: "Choose a folder inside this project, or start Claude Code or Codex in the folder you want to analyze.",
|
|
14
|
+
launch_folder_unavailable: START_FAILURE_MESSAGE,
|
|
15
|
+
folder_unavailable: START_FAILURE_MESSAGE,
|
|
16
|
+
repeat_too_soon: FINISH_FAILURE_MESSAGE,
|
|
17
|
+
};
|
|
18
|
+
/** Both console clients use the same public messages for server refusals. */
|
|
9
19
|
export const cliRefusals = {
|
|
10
20
|
folder_too_large: FOLDER_TOO_LARGE_MESSAGE,
|
|
11
21
|
low_balance: "Your balance doesn't cover this analysis. Add credit at https://app.counterfactlabs.com/account?status=low_balance#balance",
|
|
12
22
|
local_checks_paused: "Analyses are paused right now. Try again later.",
|
|
13
23
|
cli_update_required: "Update Counterfact: run npm install -g @counterfact-labs/cli@latest.",
|
|
14
24
|
release_unavailable: unsupportedVersion,
|
|
25
|
+
plan_over_limit: analysisTooLarge,
|
|
15
26
|
invalid_plan: unsupportedVersion,
|
|
16
27
|
invalid_body: "Counterfact couldn't read this request. Update Counterfact, then try again.",
|
|
17
28
|
body_too_large: "This request is too large. The limit is 2 MB.",
|
|
@@ -22,3 +33,60 @@ export const cliRefusals = {
|
|
|
22
33
|
stale_quote: START_FAILURE_MESSAGE,
|
|
23
34
|
};
|
|
24
35
|
export function refusalExitCode(code) { return code === "low_balance" ? 2 : 1; }
|
|
36
|
+
export class CliRefusalError extends Error {
|
|
37
|
+
code;
|
|
38
|
+
constructor(code) {
|
|
39
|
+
super(Object.hasOwn(localRefusals, code) ? localRefusals[code] : cliRefusals[code]);
|
|
40
|
+
this.code = code;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const otherReasons = {
|
|
44
|
+
unavailable: "console_unavailable",
|
|
45
|
+
invalid_configuration: "invalid_configuration",
|
|
46
|
+
invalid_request: "invalid_request",
|
|
47
|
+
unauthorized: "unauthorized",
|
|
48
|
+
not_allowed: "not_allowed",
|
|
49
|
+
not_found: "not_found",
|
|
50
|
+
rate_limited: "rate_limited",
|
|
51
|
+
reservation_closed: "reservation_closed",
|
|
52
|
+
reservation_closing: "reservation_closing",
|
|
53
|
+
delivery_incomplete: "delivery_incomplete",
|
|
54
|
+
check_conflict: "check_conflict",
|
|
55
|
+
invalid_email: "invalid_email",
|
|
56
|
+
wrong_code: "wrong_code",
|
|
57
|
+
expired: "expired",
|
|
58
|
+
too_many_attempts: "too_many_attempts",
|
|
59
|
+
email_unavailable: "email_unavailable",
|
|
60
|
+
request_failed: "request_failed",
|
|
61
|
+
};
|
|
62
|
+
const messageReasons = {
|
|
63
|
+
[cliRefusals.release_unavailable]: "release_unavailable",
|
|
64
|
+
[cliApiMessages.invalid_response]: "invalid_response",
|
|
65
|
+
[emailSignInMessages.browser_sign_in_pending]: "browser_sign_in_pending",
|
|
66
|
+
[emailSignInMessages.cancelled]: "cancelled",
|
|
67
|
+
};
|
|
68
|
+
const reasonValues = [
|
|
69
|
+
...Object.keys(cliRefusals),
|
|
70
|
+
...Object.keys(localRefusals),
|
|
71
|
+
...Object.values(otherReasons), ...Object.values(messageReasons),
|
|
72
|
+
"console_unreachable", "invalid_console_url", "relay_unreachable", "results_not_saved",
|
|
73
|
+
];
|
|
74
|
+
const ownedReasons = new Set(reasonValues);
|
|
75
|
+
/** Only fixed reasons may cross the MCP boundary; diagnostics can contain customer data. */
|
|
76
|
+
export function failureReason(error) {
|
|
77
|
+
if (!(error instanceof Error))
|
|
78
|
+
return "request_failed";
|
|
79
|
+
if (error instanceof CliRefusalError)
|
|
80
|
+
return error.code;
|
|
81
|
+
if ("reason" in error && typeof error.reason === "string" && ownedReasons.has(error.reason))
|
|
82
|
+
return error.reason;
|
|
83
|
+
const code = "code" in error ? error.code : undefined;
|
|
84
|
+
if (typeof code === "string") {
|
|
85
|
+
if (Object.hasOwn(cliRefusals, code))
|
|
86
|
+
return code;
|
|
87
|
+
if (Object.hasOwn(otherReasons, code))
|
|
88
|
+
return otherReasons[code];
|
|
89
|
+
return "request_failed";
|
|
90
|
+
}
|
|
91
|
+
return Object.hasOwn(messageReasons, error.message) ? messageReasons[error.message] : "request_failed";
|
|
92
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "0.2.
|
|
1
|
+
export const CLI_VERSION = "0.2.6";
|
package/dist/contract/console.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { fixDeltaSchema, fixFactsSchema } from "../analysis-core/data/finding-types.js";
|
|
2
|
+
import { findingAudienceSchema, fixDeltaSchema, fixFactsSchema } from "../analysis-core/data/finding-types.js";
|
|
3
3
|
import { inspectFixSchema, otherSiteSchema, rawFindingSchema } from "../analysis-core/analysis/schemas.js";
|
|
4
4
|
import { sourceNotesSchema } from "../analysis-core/analysis/source-notes/types.js";
|
|
5
5
|
import { parseAnalysisSize } from "../analysis-core/conventions/analysis-fit.js";
|
|
6
|
+
import { canonicalUuidSchema as uuid, matchingFindingNumbers, numberedFindingsSchema } from "./findings.js";
|
|
7
|
+
import { RESULT_CHECK_LIMITS, validIntakeText } from "../analysis-core/contracts/relay-analysis.js";
|
|
6
8
|
export const id = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/);
|
|
7
|
-
const uuid = z.uuid().transform(value => value.toLowerCase());
|
|
8
9
|
const money = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
|
|
9
10
|
const count = (max) => z.number().int().positive().max(max);
|
|
10
11
|
// These data-only boundaries follow the SaaS local start, reservation and findings owners; see docs/provenance.md.
|
|
@@ -59,32 +60,35 @@ export const consoleStatusSchema = z.strictObject({
|
|
|
59
60
|
settlement: z.enum(["unheld", "held", "charged", "released"]),
|
|
60
61
|
reason: z.enum(["service_failure", "required_failure", "authorization_refused", "our_deadline_failure",
|
|
61
62
|
"findings_saved", "required_delivered", "no_required_delivery"]).nullable(),
|
|
62
|
-
uploadState: z.enum(["saved", "missing"]), analysisId: uuid.optional(), findingIds: z.array(uuid).max(
|
|
63
|
+
uploadState: z.enum(["saved", "missing"]), analysisId: uuid.optional(), findingIds: z.array(uuid).max(RESULT_CHECK_LIMITS.findings).optional(),
|
|
64
|
+
findings: numberedFindingsSchema.optional(),
|
|
63
65
|
quoteId: z.string().min(1).max(256).optional(), priceMicro: money.nullable(), providerBudgetMicro: money,
|
|
64
66
|
expiresAt: z.iso.datetime(), absoluteExpiresAt: z.iso.datetime().nullable(),
|
|
65
67
|
deadlineClassification: z.enum(["caller_timeout", "our_deadline_failure"]).nullable(),
|
|
66
68
|
}).refine(value => value.uploadState === "saved"
|
|
67
69
|
? value.analysisId !== undefined && value.findingIds !== undefined
|
|
68
|
-
: value.analysisId === undefined && value.findingIds === undefined)
|
|
70
|
+
: value.analysisId === undefined && value.findingIds === undefined && value.findings === undefined)
|
|
71
|
+
.refine(matchingFindingNumbers);
|
|
69
72
|
export const consoleReservationSchema = consoleStatusSchema.safeExtend({ reservationToken: z.string().min(1).max(256),
|
|
70
73
|
quoteId: z.string().min(1).max(256) });
|
|
74
|
+
/**
|
|
75
|
+
* One findingId per payload finding, in payload order; optional findings maps those ids to project sequences in any order.
|
|
76
|
+
* Console must send findings in intake replies and saved status (including seq in status) only when
|
|
77
|
+
* x-counterfact-cli-version is 0.2.5 or later: older CLIs use strict schemas and reject these fields after a paid save.
|
|
78
|
+
*/
|
|
71
79
|
export const consoleIntakeResultSchema = z.strictObject({
|
|
72
|
-
checkId: uuid, projectId: uuid, analysisId: uuid, findingIds: z.array(uuid).max(
|
|
80
|
+
checkId: uuid, projectId: uuid, analysisId: uuid, findingIds: z.array(uuid).max(RESULT_CHECK_LIMITS.findings),
|
|
81
|
+
findings: numberedFindingsSchema.optional(),
|
|
73
82
|
settlement: z.enum(["unheld", "held", "charged", "released"]).optional(),
|
|
74
|
-
});
|
|
83
|
+
}).refine(matchingFindingNumbers);
|
|
75
84
|
export const LOCAL_FINDINGS_BODY_LIMIT = 2 * 1024 * 1024;
|
|
76
|
-
export const LOCAL_FINDINGS_QUOTE_LIMIT =
|
|
77
|
-
export const LOCAL_FINDINGS_TEXT_LIMIT =
|
|
85
|
+
export const LOCAL_FINDINGS_QUOTE_LIMIT = RESULT_CHECK_LIMITS.quoteBytes;
|
|
86
|
+
export const LOCAL_FINDINGS_TEXT_LIMIT = RESULT_CHECK_LIMITS.textBytes;
|
|
78
87
|
function shortFindingFields(value) {
|
|
79
88
|
if (typeof value === "string")
|
|
80
89
|
return Buffer.byteLength(value, "utf8") <= LOCAL_FINDINGS_TEXT_LIMIT;
|
|
81
90
|
return value === null || typeof value !== "object" || Object.values(value).every(shortFindingFields);
|
|
82
91
|
}
|
|
83
|
-
function validIntakeText(value) {
|
|
84
|
-
if (typeof value === "string")
|
|
85
|
-
return !value.includes(String.fromCharCode(0)) && !/[\uD800-\uDFFF]/u.test(value);
|
|
86
|
-
return value === null || typeof value !== "object" || Object.values(value).every(validIntakeText);
|
|
87
|
-
}
|
|
88
92
|
const quote = z.string().refine(value => Buffer.byteLength(value, "utf8") <= LOCAL_FINDINGS_QUOTE_LIMIT);
|
|
89
93
|
const text = z.string().min(1).refine(value => value.trim().length > 0);
|
|
90
94
|
const delta = fixDeltaSchema.extend({
|
|
@@ -100,7 +104,8 @@ const notes = sourceNotesSchema.extend({
|
|
|
100
104
|
sites: z.array(sourceNotesSchema.shape.sites.element.extend({ keyPhrase: quote }).strict()),
|
|
101
105
|
counterEvidence: z.array(sourceNotesSchema.shape.counterEvidence.element.extend({ excerpt: quote }).strict()),
|
|
102
106
|
}).strict();
|
|
103
|
-
const
|
|
107
|
+
export const localFindingSchema = rawFindingSchema.extend({
|
|
108
|
+
audience: findingAudienceSchema.optional(),
|
|
104
109
|
summary: z.string().nullable(),
|
|
105
110
|
category: rawFindingSchema.shape.category.removeCatch(),
|
|
106
111
|
question: rawFindingSchema.shape.question.removeCatch().nullable(),
|
|
@@ -114,10 +119,10 @@ const finding = rawFindingSchema.extend({
|
|
|
114
119
|
const payload = z.strictObject({
|
|
115
120
|
checkId: z.uuid(), projectId: z.uuid(), reservationId: uuid.optional(),
|
|
116
121
|
agent: z.strictObject({
|
|
117
|
-
repository: text.max(4096), commit: z.string().regex(/^(?:[a-f\d]{40}|[a-f\d]{64})$/i),
|
|
122
|
+
repository: text.max(4096), commit: z.string().regex(/^(?:[a-f\d]{40}|[a-f\d]{64})$/i).nullable(),
|
|
118
123
|
files: z.array(z.strictObject({ path: text.max(4096), hash: z.string().regex(/^[a-f\d]{64}$/i) })).min(1),
|
|
119
124
|
}),
|
|
120
|
-
findings: z.array(
|
|
125
|
+
findings: z.array(localFindingSchema), bundleVersion: text.max(128), relayVersion: text.max(128), counterfactLocalVersion: text.max(128),
|
|
121
126
|
}).refine(validIntakeText);
|
|
122
127
|
// Original JSON size is checked before a shared schema can trim text or apply defaults.
|
|
123
128
|
export const localFindingsPayloadSchema = z.unknown().refine(value => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cliApiMessages } from "./cli-api-messages.js";
|
|
2
2
|
const rateLimited = "Too many sign-in requests. Try again later.";
|
|
3
|
+
// Holds the sentences both sign-in flows share, so browser expiry lives here too.
|
|
3
4
|
export const emailSignInMessages = Object.freeze({
|
|
4
5
|
invalid_email: "Enter a valid email address.",
|
|
5
6
|
invalid_code: "Enter the code from your email.",
|
|
@@ -11,8 +12,11 @@ export const emailSignInMessages = Object.freeze({
|
|
|
11
12
|
: `Too many sign-in requests. Try again in ${seconds} ${seconds === 1 ? "second" : "seconds"}.`,
|
|
12
13
|
email_unavailable: "Email sign-in isn't available. Use browser sign-in instead.",
|
|
13
14
|
browser_sign_in_pending: "A browser sign-in is still in progress. Finish it before starting email sign-in.",
|
|
15
|
+
browser_expired: "The sign-in code expired. Run counterfact-labs login to try again.",
|
|
14
16
|
unreachable: cliApiMessages.unreachable,
|
|
15
17
|
invalid_response: cliApiMessages.invalid_response,
|
|
16
18
|
request_failed: cliApiMessages.request_failed,
|
|
17
19
|
cancelled: "Sign-in was cancelled. Run counterfact-labs login to try again.",
|
|
20
|
+
mcp_expired: "The sign-in code expired. Call sign_in to try again.",
|
|
21
|
+
mcp_cancelled: "Sign-in was cancelled. Call sign_in to try again.",
|
|
18
22
|
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ANALYSIS_PROTOCOL, analysisCapabilities } from "../analysis-core/contracts/relay-analysis.js";
|
|
3
|
+
import { stableStringify } from "../analysis-core/llm/stable-json.js";
|
|
4
|
+
import { workloadHardware } from "../gatekeeper/trust/schema.js";
|
|
5
|
+
import { kmsKeyName } from "../transport/kms.js";
|
|
6
|
+
export const ENGINE_ABI = 1;
|
|
7
|
+
export const ENGINE_SPAWNER_CONTRACT = 1;
|
|
8
|
+
const hash = z.string().regex(/^[0-9a-f]{64}$/);
|
|
9
|
+
const version = z.string().min(1).max(128).regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/);
|
|
10
|
+
export const engineIdentity = z.strictObject({
|
|
11
|
+
abi: z.literal(ENGINE_ABI), protocol: z.literal(ANALYSIS_PROTOCOL), spawnerContract: z.literal(ENGINE_SPAWNER_CONTRACT),
|
|
12
|
+
engineVersion: version, coreVersion: version, logicCommit: z.string().regex(/^[0-9a-f]{40}$/),
|
|
13
|
+
programSha256: hash, closureSha256: hash,
|
|
14
|
+
});
|
|
15
|
+
export const engineKeyPolicy = z.strictObject({
|
|
16
|
+
wrappingKey: z.string().refine(value => { try {
|
|
17
|
+
kmsKeyName(value);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
} }),
|
|
23
|
+
workloadPolicySha256: hash, hardware: workloadHardware,
|
|
24
|
+
});
|
|
25
|
+
export const enginePin = engineIdentity.extend({ artifactSha256: hash, keyPolicy: engineKeyPolicy }).strict();
|
|
26
|
+
export const analysisRelease = z.strictObject({ engine: enginePin, wire: analysisCapabilities }).refine(value => value.wire.protocol === value.engine.protocol && value.wire.engineVersion === value.engine.engineVersion);
|
|
27
|
+
export function matchEngineKeyPolicy(keyPolicy, release) {
|
|
28
|
+
const policy = engineKeyPolicy.parse(keyPolicy);
|
|
29
|
+
if (release.bundleKey !== undefined && policy.wrappingKey !== kmsKeyName(release.bundleKey) ||
|
|
30
|
+
release.workloadPolicySha256 !== undefined && policy.workloadPolicySha256 !== release.workloadPolicySha256 ||
|
|
31
|
+
release.workloadPolicy !== undefined && !release.workloadPolicy.hardwareModels.includes(policy.hardware))
|
|
32
|
+
throw new Error("Invalid engine key policy");
|
|
33
|
+
}
|
|
34
|
+
export function samePin(left, right) {
|
|
35
|
+
if (!left || !right)
|
|
36
|
+
return left === right;
|
|
37
|
+
return stableStringify(enginePin.parse(left)) === stableStringify(enginePin.parse(right));
|
|
38
|
+
}
|
|
39
|
+
/** Historical releases carry no engine. New analysis evidence must be complete and agree at every boundary. */
|
|
40
|
+
export function matchAnalysisRelease(value, pin, bundleVersion) {
|
|
41
|
+
const parsed = analysisRelease.parse(value);
|
|
42
|
+
if (!samePin(parsed.engine, pin) || parsed.wire.bundleVersion !== bundleVersion)
|
|
43
|
+
throw new Error("Analysis release differs");
|
|
44
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const findingSequenceSchema = z.number().int().positive().max(Number.MAX_SAFE_INTEGER);
|
|
3
|
+
export const canonicalUuidSchema = z.uuid().transform(value => value.toLowerCase());
|
|
4
|
+
export const numberedFindingSchema = z.strictObject({
|
|
5
|
+
id: canonicalUuidSchema, seq: findingSequenceSchema,
|
|
6
|
+
});
|
|
7
|
+
export const numberedFindingsSchema = z.array(numberedFindingSchema).max(20_000)
|
|
8
|
+
.refine(values => new Set(values.map(value => value.id)).size === values.length
|
|
9
|
+
&& new Set(values.map(value => value.seq)).size === values.length);
|
|
10
|
+
/** Both intake and recovery status must refer only to their committed finding ids. */
|
|
11
|
+
export function matchingFindingNumbers(value) {
|
|
12
|
+
if (value.findings === undefined)
|
|
13
|
+
return true;
|
|
14
|
+
const ids = new Set(value.findingIds);
|
|
15
|
+
return value.findings.every(finding => ids.has(finding.id));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Console route to implement: GET /api/cli/projects/{projectId}/findings/{seq}.
|
|
19
|
+
* Requires the signed-in bearer token and project access; no body and no charge.
|
|
20
|
+
* Params: { projectId: UUID, seq: positive decimal integer }. 200: { id: UUID, seq: number }.
|
|
21
|
+
* 404 means that finding does not exist in this project. The caller then loads
|
|
22
|
+
* the explanation from the existing GET /api/cli/findings/{id} route, whose
|
|
23
|
+
* response may add seq: number. Numbers are never resolved across projects.
|
|
24
|
+
*/
|
|
25
|
+
export const findingNumberParamsSchema = z.strictObject({
|
|
26
|
+
projectId: canonicalUuidSchema, seq: z.string().regex(/^[1-9][0-9]*$/).transform(Number).pipe(findingSequenceSchema),
|
|
27
|
+
});
|
|
28
|
+
export const findingNumberResponseSchema = numberedFindingSchema;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { canonicalUuidSchema, findingSequenceSchema } from "./findings.js";
|
|
3
|
+
/**
|
|
4
|
+
* Console routes to implement; both require the signed-in CLI bearer token.
|
|
5
|
+
* GET /api/cli/fix-requests has no body and returns 200 with a JSON array:
|
|
6
|
+
* [{ findingId: UUID, seq: positive integer, title: string, projectId: UUID,
|
|
7
|
+
* requestedAt: ISO-8601 UTC timestamp, repoKey?: string }]. Empty is [].
|
|
8
|
+
* repoKey is the finding's console repository key, including its scoped folder
|
|
9
|
+
* when present (for example fictional/support/agents/fern or local:fern).
|
|
10
|
+
* Older consoles may omit it. Only this user's
|
|
11
|
+
* unclaimed requests for accessible findings are returned. No charge or writes.
|
|
12
|
+
*
|
|
13
|
+
* POST /api/cli/fix-requests/{id}/claim takes JSON {} and returns 200 with
|
|
14
|
+
* { findingId: UUID, claimed: true }. The path id is the findingId from the list;
|
|
15
|
+
* there is one waiting request per user and finding, so no separate request id.
|
|
16
|
+
* Claiming is idempotent for that user, removes it from their waiting list and
|
|
17
|
+
* never starts analysis, applies edits or charges. Re-requesting a finding makes
|
|
18
|
+
* it waiting again. A missing or inaccessible request returns 404; unauthenticated
|
|
19
|
+
* requests return 401. The console owns authorization and atomic claiming.
|
|
20
|
+
*/
|
|
21
|
+
export const waitingFixSchema = z.object({
|
|
22
|
+
findingId: canonicalUuidSchema, seq: findingSequenceSchema, title: z.string().min(1).max(4096),
|
|
23
|
+
projectId: canonicalUuidSchema, requestedAt: z.iso.datetime(),
|
|
24
|
+
repoKey: z.string().min(1).max(4096).refine(value => value.trim().length > 0 && !/[\r\n\u2028\u2029]/.test(value)).optional(),
|
|
25
|
+
});
|
|
26
|
+
export const waitingFixesResponseSchema = z.array(waitingFixSchema).max(20_000)
|
|
27
|
+
.refine(values => new Set(values.map(value => value.findingId)).size === values.length);
|
|
28
|
+
export const claimFixRequestParamsSchema = z.strictObject({ id: canonicalUuidSchema });
|
|
29
|
+
export const claimFixRequestBodySchema = z.strictObject({});
|
|
30
|
+
export const claimFixRequestResponseSchema = z.strictObject({ findingId: canonicalUuidSchema, claimed: z.literal(true) });
|