@counterfact-labs/cli 0.2.9 → 0.2.11
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 +4 -5
- package/bundle/client-release.json +1 -1
- package/dist/adapters/analysis-results.js +10 -5
- package/dist/adapters/completion.js +75 -41
- package/dist/adapters/config-directory.js +5 -0
- package/dist/adapters/files.js +25 -10
- package/dist/adapters/legacy-reports.js +48 -0
- package/dist/adapters/output-paths.js +85 -5
- package/dist/adapters/relay-plan.js +7 -5
- package/dist/adapters/result-receipt.js +78 -0
- package/dist/adapters/results.js +5 -8
- package/dist/adapters/settings.js +10 -4
- package/dist/adapters/storage-identity.js +27 -0
- package/dist/analysis-core/analysis/code-agents/skipped-files.js +1 -1
- package/dist/analysis-core/analysis/file-analysis-plan.js +1 -1
- package/dist/analysis-core/analysis/file-dependencies.js +1 -1
- package/dist/analysis-core/analysis/finding-quality/policy.js +1 -1
- package/dist/analysis-core/analysis/folder-analysis-plan.js +1 -1
- package/dist/analysis-core/analysis/passes/full-file-passes.js +1 -1
- package/dist/analysis-core/analysis/project-architecture.js +1 -1
- package/dist/analysis-core/analysis/schemas.js +1 -1
- package/dist/analysis-core/chunks/{5B5UCWD3.js → 2IQN26TC.js} +1 -1
- package/dist/analysis-core/chunks/{MZOKZ3N6.js → 2J3PWUPG.js} +1 -1
- package/dist/analysis-core/chunks/33EUZF25.js +1 -0
- package/dist/analysis-core/chunks/{3GUKUEEJ.js → 5JG3ZRNJ.js} +1 -1
- package/dist/analysis-core/chunks/{FRXOQ2M6.js → 6E4K7JTI.js} +1 -1
- package/dist/analysis-core/chunks/{4RXRAB5S.js → 6JTHFDH3.js} +1 -1
- package/dist/analysis-core/chunks/{4UDQBDDE.js → 6W6B7VSP.js} +1 -1
- package/dist/analysis-core/chunks/{4PT5C4IT.js → AIS3FHQR.js} +7 -7
- package/dist/analysis-core/chunks/CNQZLSAP.js +1 -0
- package/dist/analysis-core/chunks/{YWL52FVW.js → EJ2IZUN2.js} +2 -2
- package/dist/analysis-core/chunks/{LOBIDUCA.js → GOBOE2SC.js} +1 -1
- package/dist/analysis-core/chunks/GYXDN5W4.js +1 -0
- package/dist/analysis-core/chunks/GZSUTDGX.js +1 -0
- package/dist/analysis-core/chunks/HB5BTGTI.js +6 -0
- package/dist/analysis-core/chunks/HH3HDT5K.js +73 -0
- package/dist/analysis-core/chunks/I4F56VMG.js +1 -0
- package/dist/analysis-core/chunks/{RQTNTIR4.js → I5F7S63K.js} +1 -1
- package/dist/analysis-core/chunks/ICJV6KIZ.js +11 -0
- package/dist/analysis-core/chunks/JAPVLU4Z.js +1 -0
- package/dist/analysis-core/chunks/{33IGHOYT.js → JCVB6IC4.js} +1 -1
- package/dist/analysis-core/chunks/{33HRXJ5Q.js → JLW44GIU.js} +1 -1
- package/dist/analysis-core/chunks/JS6HTVYX.js +1 -0
- package/dist/analysis-core/chunks/{JGK37AWN.js → JWQXG5RB.js} +1 -1
- package/dist/analysis-core/chunks/LOODGUMW.js +1 -0
- package/dist/analysis-core/chunks/{HVQJKPQ4.js → NWYNJHY4.js} +1 -1
- package/dist/analysis-core/chunks/{HHFDNY6V.js → OHDC3L6V.js} +1 -1
- package/dist/analysis-core/chunks/OXGKALKM.js +1 -0
- package/dist/analysis-core/chunks/{MJGB25S5.js → P5FCHJ7W.js} +1 -1
- package/dist/analysis-core/chunks/PISOGSJF.js +1 -0
- package/dist/analysis-core/chunks/{CTLHKFPH.js → QENYFKG3.js} +8 -8
- package/dist/analysis-core/chunks/{2YREYGOG.js → QY57UR6E.js} +1 -1
- package/dist/analysis-core/chunks/QYYN4YKB.js +35 -0
- package/dist/analysis-core/chunks/UYG422QR.js +0 -0
- package/dist/analysis-core/chunks/V7GZHSVR.js +1 -0
- package/dist/analysis-core/chunks/{JEWBPIQI.js → VBGQKYIT.js} +1 -1
- package/dist/analysis-core/chunks/{S27Y2TP5.js → VVSHUUWX.js} +1 -1
- package/dist/analysis-core/chunks/VX6LYNXV.js +1 -0
- package/dist/analysis-core/chunks/{JKWTHRUR.js → YDSB7UOR.js} +1 -1
- package/dist/analysis-core/chunks/ZLUI3EHB.js +1 -0
- package/dist/analysis-core/contracts/execution-request.js +1 -1
- package/dist/analysis-core/contracts/intake-finding.js +1 -1
- package/dist/analysis-core/contracts/model-errors.js +1 -0
- package/dist/analysis-core/contracts/relay-analysis.js +1 -1
- package/dist/analysis-core/contracts/requests.js +1 -1
- package/dist/analysis-core/conventions/analysis-fit.js +1 -1
- package/dist/analysis-core/conventions/file-admission.js +1 -1
- package/dist/analysis-core/errors.js +1 -1
- package/dist/analysis-core/import/calls/modules.js +1 -1
- package/dist/analysis-core/import/entry-selection.js +1 -1
- package/dist/analysis-core/import/outside-loader-sources.js +1 -1
- package/dist/analysis-core/index.js +1 -1
- package/dist/analysis-core/llm/cache-policy.js +1 -1
- package/dist/analysis-core/llm/cost.js +1 -1
- package/dist/analysis-core/llm/structured-output-error.js +1 -1
- package/dist/analysis-core/requests/answer-formats.js +1 -1
- package/dist/analysis-core/requests/frame-budget.js +1 -0
- package/dist/analysis-core/requests/local-plan.js +1 -1
- package/dist/analysis-core/requests/planned-limits.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 -1
- package/dist/analysis-core/wording.js +1 -0
- package/dist/cli/analysis-preview.js +22 -0
- package/dist/cli/analysis-selection.js +6 -8
- package/dist/cli/commands/analyze.js +29 -26
- package/dist/cli/commands/login.js +5 -0
- package/dist/cli/compatibility.js +39 -0
- package/dist/cli/completion-delivery.js +4 -3
- package/dist/cli/completion-recovery.js +25 -23
- package/dist/cli/credentials.js +3 -2
- package/dist/cli/finding-display.js +3 -2
- package/dist/cli/git.js +5 -2
- package/dist/cli/intake-payload.js +1 -2
- package/dist/cli/main.js +8 -2
- package/dist/cli/mcp.js +35 -10
- package/dist/cli/prior-completion.js +1 -1
- package/dist/cli/relay-check.js +88 -30
- package/dist/cli/relay-failure.js +19 -11
- package/dist/cli/relay-preparation.js +9 -6
- package/dist/cli/scope-choices.js +5 -4
- package/dist/cli/skipped-files.js +2 -1
- package/dist/cli/suggested-email.js +6 -1
- package/dist/cli/summary.js +2 -1
- package/dist/contract/agent-choice.js +2 -0
- package/dist/contract/cli-refusals.js +105 -9
- package/dist/contract/cli-version.js +1 -1
- package/dist/contract/console.js +2 -1
- package/dist/contract/email-sign-in-messages.js +2 -1
- package/dist/contract/scope-choice.js +4 -1
- package/dist/gatekeeper/client.js +11 -2
- package/dist/gatekeeper/connection-exchange.js +19 -4
- package/dist/gatekeeper/connection.js +15 -6
- package/dist/gatekeeper/console.js +10 -17
- package/dist/gatekeeper/model.js +13 -3
- package/package.json +1 -1
- package/dist/adapters/gitignore-output.js +0 -41
- package/dist/adapters/source-relay-plan.js +0 -7
- package/dist/analysis-core/chunks/2AASSZRO.js +0 -35
- package/dist/analysis-core/chunks/CQQ4TTXE.js +0 -1
- package/dist/analysis-core/chunks/F54DOWKN.js +0 -6
- package/dist/analysis-core/chunks/HCPO6QPU.js +0 -1
- package/dist/analysis-core/chunks/J4AXGE5W.js +0 -1
- package/dist/analysis-core/chunks/LFYTD7JF.js +0 -1
- package/dist/analysis-core/chunks/LMUXOKIM.js +0 -1
- package/dist/analysis-core/chunks/M5YWJY7P.js +0 -1
- package/dist/analysis-core/chunks/MGVIKA74.js +0 -1
- package/dist/analysis-core/chunks/NXHVBJL4.js +0 -1
- package/dist/analysis-core/chunks/OKBK3JL3.js +0 -1
- package/dist/analysis-core/chunks/SJESFYBA.js +0 -10
- package/dist/analysis-core/chunks/W24SFYZM.js +0 -67
- package/dist/analysis-core/chunks/YTHF2E2S.js +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { countForm } from "../analysis-core/wording.js";
|
|
1
2
|
import { skippedFilesEnabled } from "../analysis-core/settings.js";
|
|
2
3
|
import { skippedFilesNotice, ANALYZE_SKIPPED_FILES_OFFER } from "../analysis-core/analysis/code-agents/skipped-files.js";
|
|
3
4
|
import { analyzeCommand } from "../contract/agent-choice.js";
|
|
@@ -15,7 +16,7 @@ export function skippedResultText(files, settings = {}, options = {}) {
|
|
|
15
16
|
}
|
|
16
17
|
export function followupPlanText(plan) {
|
|
17
18
|
const count = plan.groups.length;
|
|
18
|
-
return [count ? `This will run ${count} ${count
|
|
19
|
+
return [count ? `This will run ${count} ${countForm(count, "analysis", "analyses")}.` : "No skipped files remain to analyze.",
|
|
19
20
|
...plan.tooLarge.map(path => `${path}: too large to analyze.`),
|
|
20
21
|
...plan.unavailable.map(path => `${path}: could not be read.`),
|
|
21
22
|
...(plan.missingContext ?? []).map(file => `${file.path}: some imported files could not be included: ${file.imports.join(", ")}.`), ""].join("\n");
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { normalizeSignInEmail } from "./email-sign-in.js";
|
|
2
2
|
import { userEmail } from "./git.js";
|
|
3
|
+
function isNoreplyEmail(email) {
|
|
4
|
+
// These addresses cannot receive the sign-in code.
|
|
5
|
+
return /(?:^|[.@+_-])(?:no[._-]?reply|do[._-]?not[._-]?reply)(?:$|[.@+_-])/.test(email);
|
|
6
|
+
}
|
|
3
7
|
export async function suggestedEmail(folder, readEmail = userEmail) {
|
|
4
8
|
try {
|
|
5
|
-
|
|
9
|
+
const email = normalizeSignInEmail(await readEmail(folder));
|
|
10
|
+
return email && !isNoreplyEmail(email) ? email : undefined;
|
|
6
11
|
}
|
|
7
12
|
catch {
|
|
8
13
|
return undefined;
|
package/dist/cli/summary.js
CHANGED
|
@@ -2,9 +2,10 @@ import { skippedResultText } from "./skipped-files.js";
|
|
|
2
2
|
import { findingPresentation } from "../analysis-core/findings/presentation.js";
|
|
3
3
|
import { activityAdvice, findingDisplayRows, notActiveCountLine, TEAM_WORKFLOW_HEADING } from "./finding-display.js";
|
|
4
4
|
import { groupByActivity } from "../analysis-core/analysis/schemas.js";
|
|
5
|
+
import { countForm } from "../analysis-core/wording.js";
|
|
5
6
|
export function formatSummary(report, options) {
|
|
6
7
|
const lines = ["# Counterfact analysis", "",
|
|
7
|
-
`${report.findings.length} findings; ${report.structures.length} structural
|
|
8
|
+
`${report.findings.length} ${countForm(report.findings.length, "finding")}; ${report.structures.length} ${countForm(report.structures.length, "structural proposal")}.`, ""];
|
|
8
9
|
const skipped = skippedResultText(report.refusedFiles ?? [], { CF_SKIPPED_FILES: options.skippedFilesEnabled === false ? "0" : "1" }, { offer: options.skippedOffer, folder: options.analyzedFolder });
|
|
9
10
|
if (skipped)
|
|
10
11
|
lines.push(skipped.trimEnd(), "");
|
|
@@ -8,9 +8,11 @@ export function analyzeCommand(folder) {
|
|
|
8
8
|
/** The local importer supplies the choice; transport keeps its data and wording together. */
|
|
9
9
|
export class AgentChoiceError extends Error {
|
|
10
10
|
agentChoice;
|
|
11
|
+
reason;
|
|
11
12
|
constructor(agentChoice) {
|
|
12
13
|
super(agentChoiceMessage(agentChoice));
|
|
13
14
|
this.agentChoice = agentChoice;
|
|
15
|
+
this.reason = agentChoice.kind;
|
|
14
16
|
}
|
|
15
17
|
}
|
|
16
18
|
export function agentChoiceMessage(agentChoice, surface = "terminal") {
|
|
@@ -5,15 +5,80 @@ import { ANALYSIS_TOO_LARGE_MESSAGE, INVALID_SELECTION_MESSAGE } from "./scope-c
|
|
|
5
5
|
export const START_FAILURE_MESSAGE = "This couldn't start. Try again.";
|
|
6
6
|
export const RESERVATION_EXPIRED_MESSAGE = "This reservation expired. Start a new analysis.";
|
|
7
7
|
export const FINISH_FAILURE_MESSAGE = "The analysis couldn't finish. Try again.";
|
|
8
|
+
export const REPORT_HOME_IN_REPOSITORY_MESSAGE = "Counterfact can't store reports outside this repository because it contains your home folder. Run the analysis from a copy of the repository outside your home folder.";
|
|
9
|
+
export const REPORT_OUTPUT_IN_REPOSITORY_MESSAGE = "Counterfact can't save reports inside the analyzed repository. Use --output with a directory outside the repository.";
|
|
10
|
+
export const OUTPUT_PATH_MESSAGE = "Counterfact couldn't safely write results in the output folder. Check its permissions and remove any links in that folder, then try again.";
|
|
11
|
+
export const LOCAL_REPORT_WRITE_MESSAGE = "The local report couldn't be written.";
|
|
12
|
+
export const COMPLETION_STORAGE_MESSAGE = "Counterfact couldn't save the recovery record on this computer. Check the output folder's permissions and available disk space.";
|
|
13
|
+
export const INVALID_CONSOLE_REQUEST_MESSAGE = "Counterfact couldn't read this request. Update Counterfact, then try again.";
|
|
14
|
+
export function repeatAnalysisMessage(retryAt, url, charged = false) {
|
|
15
|
+
const time = new Date(Math.ceil(retryAt / 60_000) * 60_000).toLocaleTimeString("en-US", { timeZone: "America/Los_Angeles", hour: "numeric", minute: "2-digit", timeZoneName: "short" });
|
|
16
|
+
if (charged && !url)
|
|
17
|
+
return `This folder's last analysis ended without saved results. You can run it again after ${time}. ${retryChargeWarning}`;
|
|
18
|
+
return `This folder was analyzed a few minutes ago. You can run it again after ${time}.${url ? ` Earlier results: ${url}` : ""}`;
|
|
19
|
+
}
|
|
20
|
+
const retryChargeWarning = "Running the analysis again may charge you.";
|
|
21
|
+
const safeRetryMessage = "It is safe to run the analysis again.";
|
|
22
|
+
const relayChargeMessages = {
|
|
23
|
+
charged: "This analysis was charged.", free_used: "This run used a free analysis.", not_charged: "Nothing was charged.",
|
|
24
|
+
unknown: "Counterfact couldn't confirm whether anything was charged.",
|
|
25
|
+
};
|
|
26
|
+
/** Settlement owns retry certainty; a zero-price charge still consumed a free analysis. */
|
|
27
|
+
export function relayChargeStatus(status, noReservation) {
|
|
28
|
+
return status?.settlement === "charged" ? status.priceMicro === 0 ? "free_used" : "charged"
|
|
29
|
+
: status?.settlement === "released" || status?.settlement === "unheld" || noReservation ? "not_charged" : "unknown";
|
|
30
|
+
}
|
|
31
|
+
export function relayClosedMessage(charge) {
|
|
32
|
+
return `The connection to Counterfact closed before the analysis finished. ${relayChargeMessages[charge]} ${charge === "not_charged" ? safeRetryMessage : retryChargeWarning}`;
|
|
33
|
+
}
|
|
34
|
+
function chargeWarning(message, charge) {
|
|
35
|
+
return charge === "not_charged" ? message : `${message} ${relayChargeMessages[charge]} ${retryChargeWarning}`;
|
|
36
|
+
}
|
|
37
|
+
export const relayFailureMessages = {
|
|
38
|
+
relay_frame_too_large: "Part of this agent was too large to send in one request, so the analysis stopped. Nothing was charged. Try again, or choose a smaller folder.",
|
|
39
|
+
model_answer_invalid: "The model's answer couldn't be read, so the analysis stopped. Nothing was charged. Try again.",
|
|
40
|
+
relay_protocol_invalid: "Counterfact's server sent an answer this version can't read. Update Counterfact, then try again.",
|
|
41
|
+
};
|
|
42
|
+
export const analysisFailureMessages = {
|
|
43
|
+
...relayFailureMessages,
|
|
44
|
+
relay_unreachable: "Counterfact couldn't establish or keep a valid relay connection. Try the analysis again.",
|
|
45
|
+
relay_local_closed: "Counterfact closed this analysis connection on this computer before the analysis finished. Try the analysis again.",
|
|
46
|
+
console_unavailable: "Counterfact's analysis service is unavailable. Try again later.",
|
|
47
|
+
reservation: "Counterfact couldn't authorize this analysis. Start a new analysis.",
|
|
48
|
+
leak_refused: "Counterfact stopped this analysis because its response failed a safety check.",
|
|
49
|
+
cancelled: "The analysis was cancelled.",
|
|
50
|
+
};
|
|
51
|
+
/** Analysis failures use the same cause and settlement wording in terminal runs and MCP. */
|
|
52
|
+
export function relayFailureMessage(reason, charge, noReservation = false) {
|
|
53
|
+
if (reason === "relay_closed")
|
|
54
|
+
return relayClosedMessage(charge);
|
|
55
|
+
if (typeof reason !== "string" || !Object.hasOwn(analysisFailureMessages, reason))
|
|
56
|
+
return undefined;
|
|
57
|
+
const message = analysisFailureMessages[reason];
|
|
58
|
+
if (message === undefined)
|
|
59
|
+
return undefined;
|
|
60
|
+
if (reason === "relay_frame_too_large" || reason === "model_answer_invalid")
|
|
61
|
+
return noReservation || charge === "not_charged" ? message : FINISH_FAILURE_MESSAGE;
|
|
62
|
+
// Protocol guidance makes no charge claim and applies to every settlement.
|
|
63
|
+
if (reason === "relay_protocol_invalid")
|
|
64
|
+
return message;
|
|
65
|
+
if (charge !== "not_charged")
|
|
66
|
+
return chargeWarning(message, charge);
|
|
67
|
+
return reason === "leak_refused" || reason === "cancelled" ? `${message} ${safeRetryMessage}` : message;
|
|
68
|
+
}
|
|
8
69
|
export const NOT_ALLOWED_MESSAGE = "Your account can't run analyses. Ask Counterfact for access.";
|
|
9
70
|
export const SIGN_IN_EXPIRED_MESSAGE = "Your sign-in has expired. Run counterfact-labs login, then try again.";
|
|
10
71
|
export const FOLDER_BUSY_MESSAGE = "Another analysis is running on this folder. Try again when it finishes.";
|
|
11
72
|
export const CHECK_CONFLICT_MESSAGE = "Saved results from an earlier run don't match this analysis. Start a new analysis.";
|
|
12
73
|
export const UNSAVED_RESULTS_MESSAGE = "The analysis ended before its results were saved. Start a new analysis.";
|
|
13
|
-
export const
|
|
74
|
+
export const RETAINED_RESULTS_MESSAGE = "Your results are kept on this computer.";
|
|
75
|
+
export const PENDING_RESULTS_MESSAGE = `${RETAINED_RESULTS_MESSAGE} Run the same command on the same folder again to save them at no extra charge.`;
|
|
14
76
|
export const RESULTS_TOO_LARGE_MESSAGE = "The results were too large to save. Try again.";
|
|
15
77
|
export const COMMITTED_FILES_MESSAGE = "Counterfact analyzes the committed version of the agent's files. Commit your changes, then try again.";
|
|
16
78
|
const unsupportedVersion = "This version of Counterfact can't run analyses. Update it: npm install -g @counterfact-labs/cli@latest.";
|
|
79
|
+
const updateCli = "Update Counterfact: run npm install -g @counterfact-labs/cli@latest.\n\n"
|
|
80
|
+
+ "Claude Code plugin users: run claude plugin update counterfact@counterfact-labs, then restart Claude Code. "
|
|
81
|
+
+ "/reload-plugins does not restart the running Counterfact server.";
|
|
17
82
|
// The console separates over-limit costs from version or route mismatches, so each code gets its own recovery guidance.
|
|
18
83
|
const localReadFailure = "Counterfact couldn't read the selected file or folder. Check that it exists and is readable, then try again.";
|
|
19
84
|
export const localRefusals = {
|
|
@@ -22,18 +87,19 @@ export const localRefusals = {
|
|
|
22
87
|
folder_unavailable: localReadFailure,
|
|
23
88
|
local_read_failed: localReadFailure,
|
|
24
89
|
local_git_failed: "Counterfact couldn't read this folder's Git information. Check that Git works in this folder, then try again.",
|
|
25
|
-
repeat_too_soon:
|
|
90
|
+
repeat_too_soon: "This folder was analyzed a few minutes ago. Wait 10 minutes after it finished, then try again.",
|
|
26
91
|
};
|
|
27
92
|
/** Both console clients use the same public messages for server refusals. */
|
|
28
93
|
export const cliRefusals = {
|
|
29
94
|
folder_too_large: FOLDER_TOO_LARGE_MESSAGE,
|
|
30
95
|
low_balance: "Your balance doesn't cover this analysis. Add credit at https://app.counterfactlabs.com/account?status=low_balance#balance",
|
|
31
96
|
local_checks_paused: "Analyses are paused right now. Try again later.",
|
|
32
|
-
cli_update_required:
|
|
97
|
+
cli_update_required: updateCli,
|
|
98
|
+
cli_version_too_old: updateCli,
|
|
33
99
|
release_unavailable: unsupportedVersion,
|
|
34
100
|
plan_over_limit: ANALYSIS_TOO_LARGE_MESSAGE,
|
|
35
101
|
invalid_plan: unsupportedVersion,
|
|
36
|
-
invalid_body:
|
|
102
|
+
invalid_body: INVALID_CONSOLE_REQUEST_MESSAGE,
|
|
37
103
|
body_too_large: "This request is too large. The limit is 2 MB.",
|
|
38
104
|
free_cap_reached: "Free analyses are paused for today. Try again tomorrow, or top up to run a paid analysis.",
|
|
39
105
|
org_spend_limit: "The workspace spending limit was reached. Ask Counterfact to raise it.",
|
|
@@ -44,8 +110,8 @@ export const cliRefusals = {
|
|
|
44
110
|
export function refusalExitCode(code) { return code === "low_balance" ? 2 : 1; }
|
|
45
111
|
export class CliRefusalError extends Error {
|
|
46
112
|
code;
|
|
47
|
-
constructor(code) {
|
|
48
|
-
super(Object.hasOwn(localRefusals, code) ? localRefusals[code] : cliRefusals[code]);
|
|
113
|
+
constructor(code, repeat) {
|
|
114
|
+
super(code === "repeat_too_soon" && repeat ? repeatAnalysisMessage(repeat.retryAt, repeat.url, repeat.charged) : Object.hasOwn(localRefusals, code) ? localRefusals[code] : cliRefusals[code]);
|
|
49
115
|
this.code = code;
|
|
50
116
|
}
|
|
51
117
|
}
|
|
@@ -67,6 +133,10 @@ const otherReasons = {
|
|
|
67
133
|
too_many_attempts: "too_many_attempts",
|
|
68
134
|
email_unavailable: "email_unavailable",
|
|
69
135
|
request_failed: "request_failed",
|
|
136
|
+
bad_request: "relay_protocol_invalid",
|
|
137
|
+
reservation: "reservation",
|
|
138
|
+
leak_refused: "leak_refused",
|
|
139
|
+
cancelled: "cancelled",
|
|
70
140
|
};
|
|
71
141
|
const messageReasons = {
|
|
72
142
|
[cliRefusals.release_unavailable]: "release_unavailable",
|
|
@@ -78,10 +148,13 @@ const reasonValues = [
|
|
|
78
148
|
...Object.keys(cliRefusals),
|
|
79
149
|
...Object.keys(localRefusals),
|
|
80
150
|
...Object.values(otherReasons), ...Object.values(messageReasons),
|
|
81
|
-
"console_unreachable", "invalid_console_url", "relay_unreachable", "results_not_saved",
|
|
82
|
-
"results_saved_report_failed", "results_pending", "results_discarded",
|
|
151
|
+
"console_unreachable", "invalid_console_url", "relay_unreachable", "relay_closed", "relay_local_closed", "results_not_saved",
|
|
152
|
+
"results_saved_report_failed", "results_pending", "results_discarded", "results_kept",
|
|
83
153
|
"folder_busy", "completion_settings_mismatch",
|
|
154
|
+
"no_agent", "choose_agent", "choose_scope",
|
|
155
|
+
"relay_frame_too_large", "model_answer_invalid", "relay_protocol_invalid",
|
|
84
156
|
];
|
|
157
|
+
export const FAILURE_REASONS = [...new Set(reasonValues)];
|
|
85
158
|
const ownedReasons = new Set(reasonValues);
|
|
86
159
|
const readErrorCodes = new Set(["ENOENT", "EACCES", "EPERM", "ENOTDIR", "EISDIR", "ELOOP", "EMFILE", "ENFILE", "EIO", "ESTALE", "EBADF", "EINVAL", "ENOMEM", "EAGAIN", "EBUSY"]);
|
|
87
160
|
/** Classify local failures without copying paths, command output or file contents into public errors. */
|
|
@@ -90,9 +163,11 @@ export function localFailure(error) {
|
|
|
90
163
|
return undefined;
|
|
91
164
|
if ("reason" in error && error.reason === "local_git_failed")
|
|
92
165
|
return { message: localRefusals.local_git_failed, reason: "local_git_failed" };
|
|
93
|
-
//
|
|
166
|
+
// The reader boundary can also identify operations that failed after initial selection.
|
|
94
167
|
if (!("localOperation" in error) || error.localOperation !== "read")
|
|
95
168
|
return undefined;
|
|
169
|
+
if ("reason" in error && error.reason === "local_read_failed")
|
|
170
|
+
return { message: localRefusals.local_read_failed, reason: "local_read_failed" };
|
|
96
171
|
const syscall = "syscall" in error ? error.syscall : undefined;
|
|
97
172
|
const code = "code" in error ? error.code : undefined;
|
|
98
173
|
if (typeof code === "string" && readErrorCodes.has(code) && typeof syscall === "string"
|
|
@@ -103,6 +178,27 @@ export function localFailure(error) {
|
|
|
103
178
|
}
|
|
104
179
|
return undefined;
|
|
105
180
|
}
|
|
181
|
+
/** Analysis failures expose fixed causes without private reader diagnostics. */
|
|
182
|
+
export function analysisFailureDetails(error, charge, context = {}) {
|
|
183
|
+
if (!(error instanceof Error) || error instanceof CliRefusalError)
|
|
184
|
+
return undefined;
|
|
185
|
+
const reason = failureReason(error);
|
|
186
|
+
const interrupted = context.cancelled && (reason === "relay_unreachable" || reason === "relay_closed" || reason === "relay_local_closed"
|
|
187
|
+
|| "localOperation" in error && error.localOperation === "read");
|
|
188
|
+
if (interrupted || error.name === "AbortError")
|
|
189
|
+
return { message: relayFailureMessage("cancelled", charge), reason: "cancelled" };
|
|
190
|
+
const local = localFailure(error);
|
|
191
|
+
if (local)
|
|
192
|
+
return { ...local, message: chargeWarning(local.message, charge) };
|
|
193
|
+
if ("localOperation" in error && error.localOperation === "read") {
|
|
194
|
+
return { message: chargeWarning(FINISH_FAILURE_MESSAGE, charge), reason: "request_failed" };
|
|
195
|
+
}
|
|
196
|
+
if (reason === "folder_busy" || reason === "reservation_expired") {
|
|
197
|
+
return { message: chargeWarning(reason === "folder_busy" ? FOLDER_BUSY_MESSAGE : RESERVATION_EXPIRED_MESSAGE, charge), reason };
|
|
198
|
+
}
|
|
199
|
+
const message = relayFailureMessage(reason, charge, context.noReservation);
|
|
200
|
+
return message ? { message, reason } : undefined;
|
|
201
|
+
}
|
|
106
202
|
/** Only fixed reasons may cross the MCP boundary; diagnostics can contain customer data. */
|
|
107
203
|
export function failureReason(error) {
|
|
108
204
|
if (!(error instanceof Error))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "0.2.
|
|
1
|
+
export const CLI_VERSION = "0.2.11";
|
package/dist/contract/console.js
CHANGED
|
@@ -7,6 +7,7 @@ import { projectArchitectureSchema } from "./project-architecture.js";
|
|
|
7
7
|
import { intakeFinding as localFindingSchema, nonblankText as text, RESULT_CHECK_LIMITS, validIntakeText } from "../analysis-core/contracts/intake-finding.js";
|
|
8
8
|
export { localFindingSchema };
|
|
9
9
|
import { selectionStartIdentitySchema } from "./selection-metadata.js";
|
|
10
|
+
import { FAILURE_REASONS } from "./cli-refusals.js";
|
|
10
11
|
export const id = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/);
|
|
11
12
|
const money = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
|
|
12
13
|
const count = (max) => z.number().int().positive().max(max);
|
|
@@ -60,7 +61,7 @@ export const cliRelayReservationRequestSchema = z.discriminatedUnion("action", [
|
|
|
60
61
|
z.strictObject({ action: z.literal("reserve"), reservationId: uuid }),
|
|
61
62
|
z.strictObject({ action: z.literal("status"), reservationId: uuid }),
|
|
62
63
|
z.strictObject({ action: z.literal("heartbeat"), reservationId: uuid }),
|
|
63
|
-
z.strictObject({ action: z.literal("fail"), reservationId: uuid }),
|
|
64
|
+
z.strictObject({ action: z.literal("fail"), reservationId: uuid, reason: z.enum(FAILURE_REASONS).optional() }),
|
|
64
65
|
]);
|
|
65
66
|
export const consoleCreatedSchema = z.strictObject({ reservationId: uuid, jobId: uuid, projectId: uuid, checkId: uuid, state: z.literal("quoted") });
|
|
66
67
|
export const consoleStatusSchema = z.strictObject({
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cliApiMessages } from "./cli-api-messages.js";
|
|
2
|
+
import { countForm } from "../analysis-core/wording.js";
|
|
2
3
|
const rateLimited = "Too many sign-in requests. Try again later.";
|
|
3
4
|
// Holds the sentences both sign-in flows share, so browser expiry lives here too.
|
|
4
5
|
export const emailSignInMessages = Object.freeze({
|
|
@@ -9,7 +10,7 @@ export const emailSignInMessages = Object.freeze({
|
|
|
9
10
|
too_many_attempts: "Too many incorrect codes. Request a new sign-in code.",
|
|
10
11
|
rate_limited: rateLimited,
|
|
11
12
|
rate_limited_with_cooldown: (seconds) => seconds === 0 ? rateLimited
|
|
12
|
-
: `Too many sign-in requests. Try again in ${seconds} ${seconds
|
|
13
|
+
: `Too many sign-in requests. Try again in ${seconds} ${countForm(seconds, "second")}.`,
|
|
13
14
|
email_unavailable: "Email sign-in isn't available. Use browser sign-in instead.",
|
|
14
15
|
browser_sign_in_pending: "A browser sign-in is still in progress. Finish it before starting email sign-in.",
|
|
15
16
|
browser_expired: "The sign-in code expired. Run counterfact-labs login to try again.",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { countForm } from "../analysis-core/wording.js";
|
|
2
3
|
import { ANALYSIS_SELECTION_LIMITS, provenScopeChoiceSchema } from "../analysis-core/contracts/analysis-selection.js";
|
|
3
4
|
import { refusedImportSchema } from "../analysis-core/contracts/module-resolution.js";
|
|
4
5
|
import { selectionPathSchema, snapshotIdSchema } from "../analysis-core/contracts/selection-paths.js";
|
|
@@ -52,7 +53,9 @@ export function progressLine(selection, surface = "source") {
|
|
|
52
53
|
export function failureLine(selection) {
|
|
53
54
|
return selection?.kind === "file" ? SELECTED_PATH_FAILURE : "Analysis could not finish. Check the folder, provider key and connection.";
|
|
54
55
|
}
|
|
55
|
-
export
|
|
56
|
+
export function selectionCoverageDescription(entry, count) {
|
|
57
|
+
return `This analysis covers ${entry} and ${count} selected ${countForm(count, "import")}. Other imports were left out.`;
|
|
58
|
+
}
|
|
56
59
|
export const SCOPE_RETRY_TEMPLATE = "Run again: counterfact-labs analyze {quoted-entry} --scope {quoted-scope-file}";
|
|
57
60
|
export const FILE_SCOPE_PUBLIC_USAGE = "Usage: counterfact-labs analyze [path] [--proposal <json-file>] [--scope <json-file>]\n counterfact-labs finding <id>\n counterfact-labs balance | login | logout | mcp\n";
|
|
58
61
|
export const FILE_SCOPE_SOURCE_USAGE = "Usage: npx tsx cli/check.ts <path> [--output <directory>] [--scope <json-file>]\n";
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { publicRelayConnectionError } from "./connection-exchange.js";
|
|
2
|
+
import { assertFrameBytes } from "../analysis-core/requests/frame-budget.js";
|
|
3
|
+
import { isPublicModelError, RelayProtocolError } from "../analysis-core/contracts/model-errors.js";
|
|
1
4
|
import { randomBytes } from "node:crypto";
|
|
2
5
|
import { relayAnswer, relayRequest } from "../contract/messages.js";
|
|
3
6
|
import { verifyAttestation } from "./attestation.js";
|
|
@@ -21,6 +24,7 @@ export class GatekeeperClient {
|
|
|
21
24
|
const outgoing = relayRequest.safeParse({ ...request, nonce: randomBytes(32).toString("hex") });
|
|
22
25
|
if (!outgoing.success)
|
|
23
26
|
throw new GatekeeperClientError("Invalid relay request");
|
|
27
|
+
assertFrameBytes(Buffer.byteLength(JSON.stringify(outgoing.data)));
|
|
24
28
|
const options = this.options;
|
|
25
29
|
if (options.mode === "development") {
|
|
26
30
|
let token;
|
|
@@ -39,13 +43,18 @@ export class GatekeeperClient {
|
|
|
39
43
|
beforeSend();
|
|
40
44
|
incoming = await options.connection.send(outgoing.data, { ...sendOptions, beforeSend });
|
|
41
45
|
}
|
|
42
|
-
catch {
|
|
46
|
+
catch (error) {
|
|
43
47
|
// Transport errors can include request bodies, so do not retain their text or cause.
|
|
48
|
+
if (isPublicModelError(error))
|
|
49
|
+
throw error;
|
|
50
|
+
const connectionError = publicRelayConnectionError(error);
|
|
51
|
+
if (connectionError)
|
|
52
|
+
throw connectionError;
|
|
44
53
|
throw new GatekeeperClientError("Unable to send relay request");
|
|
45
54
|
}
|
|
46
55
|
const answer = relayAnswer.safeParse(incoming);
|
|
47
56
|
if (!answer.success)
|
|
48
|
-
throw new
|
|
57
|
+
throw new RelayProtocolError();
|
|
49
58
|
if (answer.data.nonce !== outgoing.data.nonce)
|
|
50
59
|
throw new GatekeeperClientError("Relay answer nonce mismatch");
|
|
51
60
|
if (answer.data.ok || answer.data.code !== "busy" || !options.deadlineMs)
|
|
@@ -1,9 +1,19 @@
|
|
|
1
|
+
import { isPublicModelError, RelayProtocolError } from "../analysis-core/contracts/model-errors.js";
|
|
1
2
|
import { RELAY_MAX_FRAME_BYTES, deliveryReceipt, relayAnswer, relayAnswerFrame, relayRequest } from "../contract/messages.js";
|
|
2
3
|
import { unredactedAnswer } from "../contract/public-report.js";
|
|
3
4
|
export class RelayConnectionError extends Error {
|
|
5
|
+
reason;
|
|
4
6
|
name = "RelayConnectionError";
|
|
5
|
-
reason = "relay_unreachable"
|
|
6
|
-
|
|
7
|
+
constructor(reason = "relay_unreachable") {
|
|
8
|
+
super("Relay connection failed");
|
|
9
|
+
this.reason = reason;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** Keep only transport-owned categories when crossing a boundary that discards diagnostics. */
|
|
13
|
+
export function publicRelayConnectionError(error) {
|
|
14
|
+
if (!(error instanceof RelayConnectionError) || !["relay_unreachable", "relay_closed", "relay_local_closed"].includes(error.reason))
|
|
15
|
+
return undefined;
|
|
16
|
+
return new RelayConnectionError(error.reason);
|
|
7
17
|
}
|
|
8
18
|
/** Both carriers retry the same confirmation, never another paid request. */
|
|
9
19
|
export async function acknowledgeDelivery(options, confirmation) {
|
|
@@ -29,7 +39,10 @@ export function createRelayExchange(options) {
|
|
|
29
39
|
// Provider work can exceed a network-body timeout; evidence expiry still closes this exact socket.
|
|
30
40
|
const response = await options.exchange("/relay", request, RELAY_MAX_FRAME_BYTES, options.remainingEvidenceMs());
|
|
31
41
|
const legacy = options.development ? relayAnswer.safeParse(response.body) : undefined;
|
|
32
|
-
const
|
|
42
|
+
const parsed = legacy?.success ? { success: true, data: { answer: legacy.data, delivery: undefined } } : relayAnswerFrame.safeParse(response.body);
|
|
43
|
+
if (!parsed.success)
|
|
44
|
+
throw new RelayProtocolError();
|
|
45
|
+
const frame = parsed.data;
|
|
33
46
|
if (frame.answer.nonce !== request.nonce)
|
|
34
47
|
throw new Error();
|
|
35
48
|
if (frame.answer.ok)
|
|
@@ -43,8 +56,10 @@ export function createRelayExchange(options) {
|
|
|
43
56
|
}
|
|
44
57
|
return frame.answer;
|
|
45
58
|
}
|
|
46
|
-
catch {
|
|
59
|
+
catch (error) {
|
|
47
60
|
options.close();
|
|
61
|
+
if (isPublicModelError(error))
|
|
62
|
+
throw error;
|
|
48
63
|
throw new RelayConnectionError();
|
|
49
64
|
}
|
|
50
65
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { assertFrameBytes } from "../analysis-core/requests/frame-budget.js";
|
|
2
|
+
import { isPublicModelError, RelayProtocolError } from "../analysis-core/contracts/model-errors.js";
|
|
1
3
|
import { isLoopbackHost } from "./loopback.js";
|
|
2
4
|
import { randomBytes, X509Certificate } from "node:crypto";
|
|
3
5
|
import { Agent, request as httpsRequest } from "node:https";
|
|
4
6
|
import { connect } from "node:tls";
|
|
5
|
-
import { PRODUCTION_EVIDENCE_VERSION,
|
|
7
|
+
import { PRODUCTION_EVIDENCE_VERSION, RELAY_URL } from "../contract/messages.js";
|
|
6
8
|
import { requestBoundedResponse } from "../transport/http-response.js";
|
|
7
9
|
import { relayExportedKeyMaterial } from "../transport/tls-exporter.js";
|
|
8
10
|
import { createRelayExchange, RelayConnectionError } from "./connection-exchange.js";
|
|
@@ -84,8 +86,7 @@ export async function connectRelay(url = RELAY_URL, policy) {
|
|
|
84
86
|
if (closed || actualSocket.destroyed)
|
|
85
87
|
throw new RelayConnectionError();
|
|
86
88
|
const body = JSON.stringify(input);
|
|
87
|
-
|
|
88
|
-
throw new RelayConnectionError();
|
|
89
|
+
assertFrameBytes(Buffer.byteLength(body));
|
|
89
90
|
const developmentAttestation = !!policy.development && path === "/attestation";
|
|
90
91
|
try {
|
|
91
92
|
const result = await requestBoundedResponse(receive => {
|
|
@@ -103,12 +104,20 @@ export async function connectRelay(url = RELAY_URL, policy) {
|
|
|
103
104
|
} });
|
|
104
105
|
if (authenticated)
|
|
105
106
|
valid();
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
let answer;
|
|
108
|
+
try {
|
|
109
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(result.body);
|
|
110
|
+
answer = developmentAttestation ? text : JSON.parse(text);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
throw new RelayProtocolError();
|
|
114
|
+
}
|
|
108
115
|
return { status: result.response.statusCode ?? 0, body: answer };
|
|
109
116
|
}
|
|
110
|
-
catch {
|
|
117
|
+
catch (error) {
|
|
111
118
|
close();
|
|
119
|
+
if (isPublicModelError(error))
|
|
120
|
+
throw error;
|
|
112
121
|
throw new RelayConnectionError();
|
|
113
122
|
}
|
|
114
123
|
}
|
|
@@ -3,14 +3,14 @@ import { request as httpRequest } from "node:http";
|
|
|
3
3
|
import { request as httpsRequest } from "node:https";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { CLI_VERSION } from "../contract/cli-version.js";
|
|
6
|
-
import { cliRefusals, NOT_ALLOWED_MESSAGE, SIGN_IN_EXPIRED_MESSAGE, CHECK_CONFLICT_MESSAGE, UNSAVED_RESULTS_MESSAGE,
|
|
6
|
+
import { cliRefusals, NOT_ALLOWED_MESSAGE, SIGN_IN_EXPIRED_MESSAGE, CHECK_CONFLICT_MESSAGE, UNSAVED_RESULTS_MESSAGE, INVALID_CONSOLE_REQUEST_MESSAGE, RESERVATION_EXPIRED_MESSAGE, RESULTS_TOO_LARGE_MESSAGE, START_FAILURE_MESSAGE } from "../contract/cli-refusals.js";
|
|
7
7
|
import { cliRelayReservationRequestSchema, consoleCreatedSchema, consoleIntakeResultSchema, consoleReservationSchema, consoleStatusSchema, LOCAL_FINDINGS_BODY_LIMIT, localFindingsPayloadSchema, LOCAL_START_BODY_LIMIT, LOCAL_START_EXECUTION_HEADER, localStartRequestSchema, localStartResponseSchema, serializeLocalStartMetadata } from "../contract/console.js";
|
|
8
8
|
const RESPONSE_LIMIT = 64 * 1024;
|
|
9
9
|
const RESERVATION_BODY_LIMIT = 64 * 1024;
|
|
10
10
|
export const consoleErrorMessages = {
|
|
11
11
|
...cliRefusals,
|
|
12
12
|
invalid_configuration: START_FAILURE_MESSAGE,
|
|
13
|
-
invalid_request:
|
|
13
|
+
invalid_request: INVALID_CONSOLE_REQUEST_MESSAGE,
|
|
14
14
|
unavailable: "The results couldn't be saved yet. Run the analysis on the same folder again to save them; you won't be charged again.",
|
|
15
15
|
unauthorized: SIGN_IN_EXPIRED_MESSAGE,
|
|
16
16
|
not_allowed: NOT_ALLOWED_MESSAGE,
|
|
@@ -50,14 +50,6 @@ export class ConsoleError extends Error {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
export const isConsoleToken = (token) => /^[\x21-\x7e]{1,4096}$/.test(token);
|
|
53
|
-
/** Strict older consoles accept optional metadata only after explicitly advertising it. */
|
|
54
|
-
export function consoleCapabilities(mode) {
|
|
55
|
-
return {
|
|
56
|
-
skippedFiles: "skippedFiles" in mode && mode.skippedFiles === true,
|
|
57
|
-
findingActivity: "findingActivity" in mode && mode.findingActivity === true,
|
|
58
|
-
architecture: "architecture" in mode && mode.architecture === true,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
53
|
/** Only this client may send repository metadata, content-free lifecycle or findings-only intake payloads to the console. */
|
|
62
54
|
export class ConsoleClient {
|
|
63
55
|
#url;
|
|
@@ -93,12 +85,13 @@ export class ConsoleClient {
|
|
|
93
85
|
}
|
|
94
86
|
return this.#send("/api/cli/checks", input, localStartRequestSchema, localStartResponseSchema, LOCAL_START_BODY_LIMIT, { statuses: [201], headers: { ...headers, [LOCAL_START_EXECUTION_HEADER.name]: LOCAL_START_EXECUTION_HEADER.value }, context: "start" });
|
|
95
87
|
}
|
|
96
|
-
async capabilities() {
|
|
97
|
-
const mode = await this.#send("/api/cli/checks/mode", {}, z.object({}), z.object({ skippedFiles: z.boolean().optional() }).passthrough(), 1024, { method: "GET", context: "start" });
|
|
98
|
-
return consoleCapabilities(mode);
|
|
99
|
-
}
|
|
100
88
|
async supportsSkippedFiles() {
|
|
101
|
-
|
|
89
|
+
const mode = await this.#send("/api/cli/checks/mode", {}, z.object({}), z.object({ skippedFiles: z.boolean().optional() }), 1024, { method: "GET", context: "start" });
|
|
90
|
+
return mode.skippedFiles === true;
|
|
91
|
+
}
|
|
92
|
+
async supportsFailureReason() {
|
|
93
|
+
const mode = await this.#send("/api/cli/checks/mode", {}, z.object({}), z.object({ failureReason: z.boolean().optional() }), 1024, { method: "GET", context: "start" });
|
|
94
|
+
return mode.failureReason === true;
|
|
102
95
|
}
|
|
103
96
|
create(input) {
|
|
104
97
|
return this.#lifecycle({ ...input, action: "create" }, consoleCreatedSchema);
|
|
@@ -112,8 +105,8 @@ export class ConsoleClient {
|
|
|
112
105
|
heartbeat(reservationId) {
|
|
113
106
|
return this.#lifecycle({ action: "heartbeat", reservationId }, consoleStatusSchema);
|
|
114
107
|
}
|
|
115
|
-
fail(reservationId) {
|
|
116
|
-
return this.#lifecycle({ action: "fail", reservationId }, consoleStatusSchema);
|
|
108
|
+
fail(reservationId, reason) {
|
|
109
|
+
return this.#lifecycle({ action: "fail", reservationId, ...(reason === undefined ? {} : { reason }) }, consoleStatusSchema);
|
|
117
110
|
}
|
|
118
111
|
intake(input, options = {}) {
|
|
119
112
|
return this.#send("/api/cli/checks/local", input, localFindingsPayloadSchema, consoleIntakeResultSchema, LOCAL_FINDINGS_BODY_LIMIT, { statuses: [200, 201], context: "intake", ...options });
|
package/dist/gatekeeper/model.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { publicRelayConnectionError } from "./connection-exchange.js";
|
|
2
|
+
import { isPublicModelError } from "../analysis-core/contracts/model-errors.js";
|
|
1
3
|
import { randomUUID } from "node:crypto";
|
|
2
4
|
import { unredactedAnswer } from "../contract/public-report.js";
|
|
3
5
|
import { executionRequest } from "../analysis-core/contracts/execution-request.js";
|
|
@@ -34,11 +36,19 @@ export function relayModel(options) {
|
|
|
34
36
|
const signals = [options.signal, execution.signal].filter((signal) => !!signal);
|
|
35
37
|
reply = await client.call({ ...snapshot, bundleVersion, reservationId, reservationToken, callId: identity }, { signal: signals.length ? AbortSignal.any(signals) : undefined });
|
|
36
38
|
}
|
|
37
|
-
catch {
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (isPublicModelError(error))
|
|
41
|
+
throw error;
|
|
42
|
+
const connectionError = publicRelayConnectionError(error);
|
|
43
|
+
if (connectionError)
|
|
44
|
+
throw connectionError;
|
|
38
45
|
throw new Error("Relay request could not finish");
|
|
39
46
|
}
|
|
40
|
-
if (!reply.ok)
|
|
41
|
-
|
|
47
|
+
if (!reply.ok) {
|
|
48
|
+
const refused = new Error(`Relay refused the request: ${reply.code}`);
|
|
49
|
+
refused.reason = reply.code === "bad_request" ? "relay_protocol_invalid" : reply.code === "reservation" || reply.code === "leak_refused" ? reply.code : undefined;
|
|
50
|
+
throw refused;
|
|
51
|
+
}
|
|
42
52
|
try {
|
|
43
53
|
onUsage?.({ ...reply.usage, template: snapshot.template, model: snapshot.model });
|
|
44
54
|
}
|
package/package.json
CHANGED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { constants } from "node:fs";
|
|
2
|
-
import { open } from "node:fs/promises";
|
|
3
|
-
import { createGitIgnoreFactsQuery } from "./git-ignore.js";
|
|
4
|
-
import { runGit } from "./git.js";
|
|
5
|
-
import { analysisReadRoots, gitRepositoryRoot } from "./repository-root.js";
|
|
6
|
-
import { DEFAULT_OUTPUT_DIR, outputPaths } from "./output-paths.js";
|
|
7
|
-
const ignoreRule = `${DEFAULT_OUTPUT_DIR}/`;
|
|
8
|
-
export const GITIGNORE_OUTPUT_NOTICE = `Added ${ignoreRule} to .gitignore, so the local report stays out of Git.\n`;
|
|
9
|
-
export const gitignoreOutputEnabled = (settings = process.env) => settings.CF_LOCAL_GITIGNORE_OUTPUT !== "0";
|
|
10
|
-
/** Keep local excerpts out of Git after a successful default report write. */
|
|
11
|
-
export async function ignoreDefaultOutput(folder, outputDir, options = {}) {
|
|
12
|
-
if (outputDir !== undefined || !gitignoreOutputEnabled(options.settings))
|
|
13
|
-
return false;
|
|
14
|
-
try {
|
|
15
|
-
// Automatic edits need confirmed checkout boundaries even when ignore reads can trust Git alone.
|
|
16
|
-
const reported = await gitRepositoryRoot(async (args) => (await runGit(folder, args)).toString("utf8"), folder);
|
|
17
|
-
if (!(await analysisReadRoots(folder, reported)).repository)
|
|
18
|
-
return false;
|
|
19
|
-
const facts = await createGitIgnoreFactsQuery(folder)([ignoreRule]);
|
|
20
|
-
if (!facts || facts.has(ignoreRule))
|
|
21
|
-
return false;
|
|
22
|
-
// Appending preserves existing rules, and refusing links keeps this write in the selected folder.
|
|
23
|
-
const [path] = await outputPaths(folder, [".gitignore"], folder);
|
|
24
|
-
const file = await open(path, constants.O_RDWR | constants.O_CREAT | constants.O_APPEND | constants.O_NOFOLLOW | constants.O_NONBLOCK, 0o644);
|
|
25
|
-
try {
|
|
26
|
-
const info = await file.stat();
|
|
27
|
-
if (!info.isFile())
|
|
28
|
-
return false;
|
|
29
|
-
const last = Buffer.alloc(1);
|
|
30
|
-
if (info.size)
|
|
31
|
-
await file.read(last, 0, 1, info.size - 1);
|
|
32
|
-
await file.writeFile(`${info.size && last[0] !== 10 ? "\n" : ""}${ignoreRule}\n`);
|
|
33
|
-
}
|
|
34
|
-
finally {
|
|
35
|
-
await file.close();
|
|
36
|
-
}
|
|
37
|
-
return true;
|
|
38
|
-
}
|
|
39
|
-
catch { /* Report delivery must survive an unavailable Git command or ignore file. */ }
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { localSourcePlan } from "../analysis-core/requests/local-plan.js";
|
|
2
|
-
import { prepareRelayPlanFromCalls } from "./relay-plan.js";
|
|
3
|
-
/** Source preparation plans the captured view once and shares the public paid-plan owner. */
|
|
4
|
-
export function prepareRelayPlan({ bundle, relayVersion, settings, source }) {
|
|
5
|
-
const { files, calls } = localSourcePlan(settings, source);
|
|
6
|
-
return { files, plan: prepareRelayPlanFromCalls({ bundle, relayVersion, calls }) };
|
|
7
|
-
}
|