@counterfact-labs/cli 0.2.3 → 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 +29 -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 +4 -7
- package/dist/adapters/release.js +10 -4
- 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 +27 -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/3GUKUEEJ.js +1 -0
- package/dist/analysis-core/chunks/{Y5AUKKYG.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/JKWTHRUR.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/{VFENG4MR.js → MJGB25S5.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/{QPXNXMLC.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/{VER355VN.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 -0
- package/dist/analysis-core/conventions/file-admission.js +1 -1
- package/dist/analysis-core/data/finding-types.js +1 -1
- package/dist/analysis-core/errors.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/cache-policy.js +1 -1
- package/dist/analysis-core/llm/cost.js +1 -1
- 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/bundle/seal/signature.js +2 -2
- package/dist/cli/commands/analyze.js +39 -5
- package/dist/cli/commands/finding.js +9 -3
- package/dist/cli/context.js +4 -1
- package/dist/cli/development-relay.js +34 -0
- 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 +47 -14
- package/dist/cli/relay-preparation.js +31 -6
- 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 +5 -0
- package/dist/contract/cli-version.js +1 -1
- package/dist/contract/console.js +41 -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 +18 -13
- package/dist/gatekeeper/console.js +11 -4
- 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/6YCABS5T.js +0 -1
- package/dist/analysis-core/chunks/7QDOHWSY.js +0 -27
- package/dist/analysis-core/chunks/BZNHHLNP.js +0 -1
- package/dist/analysis-core/chunks/DTMHXKP6.js +0 -7
- package/dist/analysis-core/chunks/EQTIRIMU.js +0 -37
- package/dist/analysis-core/chunks/G322LST6.js +0 -1
- package/dist/analysis-core/chunks/IL2KRP3T.js +0 -1
- package/dist/analysis-core/chunks/JF4GYMOZ.js +0 -1
- package/dist/analysis-core/chunks/KEKTZDIK.js +0 -1
- package/dist/analysis-core/chunks/M7MMAUAK.js +0 -1
- package/dist/analysis-core/chunks/OFBJNHC6.js +0 -1
- package/dist/analysis-core/chunks/ORCYIIHB.js +0 -60
- package/dist/analysis-core/chunks/PVPEY7NF.js +0 -1
- package/dist/analysis-core/chunks/STZOC7LN.js +0 -1
- 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/YVDCJN7D.js +0 -14
- package/dist/analysis-core/chunks/ZMUBMYWN.js +0 -1
- package/dist/core/analysis/finding-quality/config.js +0 -4
- package/dist/core/analysis/structural-run.js +0 -16
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ Use Node.js 22 or later and install the pinned dependencies:
|
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
npm ci
|
|
19
|
+
npm run build:core
|
|
19
20
|
```
|
|
20
21
|
|
|
21
22
|
Set `ANTHROPIC_API_KEY` in your shell to your Anthropic development key, then run these commands from this checkout:
|
|
@@ -141,8 +142,9 @@ or add both to the sandbox's allowed domains.
|
|
|
141
142
|
The command uses the existing saved sign-in and sends `X-Counterfact-CLI-Version: 0.2.0`. It asks
|
|
142
143
|
`/api/cli/checks/mode` before reading source or starting paid work. Upload, paused, update-required and unavailable
|
|
143
144
|
responses stop the analysis; the command never falls back to uploading files. Settings are captured at command start.
|
|
144
|
-
|
|
145
|
+
An analysis reads the agent's files as they are on disk, including uncommitted changes, and also works in folders without Git.
|
|
145
146
|
The console owns prices, expressed in millionths of a dollar.
|
|
147
|
+
By default, the CLI adds .counterfact/ to the folder's .gitignore when Git doesn't already ignore it. Set CF_LOCAL_GITIGNORE_OUTPUT=0 to turn this off.
|
|
146
148
|
|
|
147
149
|
The installed command uses the relay address in `bundle/client-release.json` and saved sign-in. The relay runner sends
|
|
148
150
|
repository facts and the admitted file list (paths and sizes, without contents or absolute checkout paths) to
|
|
@@ -154,6 +156,10 @@ Release stays blocked while `relayUrl` and `projectContext` are unconfigured in
|
|
|
154
156
|
`projectContext` to `checks` only after local start support is reviewed. The release validator requires both settings
|
|
155
157
|
along with the signed bundle and trust pins; an environment override cannot bypass the project-context pin.
|
|
156
158
|
|
|
159
|
+
## Relay privacy
|
|
160
|
+
|
|
161
|
+
The sealed relay's source is public at [github.com/counterfact-labs/sealed-relay](https://github.com/counterfact-labs/sealed-relay), with a reproducible build of its JavaScript, and the CLI checks the relay's attestation against a pinned image digest before sending it any analysis input.
|
|
162
|
+
|
|
157
163
|
## Build and release the customer CLI archive
|
|
158
164
|
|
|
159
165
|
Generate `bundle/sealed.json` with the existing release tooling and externally supplied release keys. Install the
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"verificationKey": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAbBab1PYdmBKXxCvoJ47toJhQvxPevo9nlZnb0zbDWoc=\n-----END PUBLIC KEY-----\n",
|
|
3
3
|
"relayVersion": "relay-1.0.1",
|
|
4
|
-
"localVersion": "0.2.
|
|
4
|
+
"localVersion": "0.2.5",
|
|
5
5
|
"relayUrl": "https://relay.counterfactlabs.com:8443",
|
|
6
6
|
"projectContext": "checks"
|
|
7
7
|
}
|
|
@@ -5,6 +5,7 @@ import { estimateTokensFromBytes } from "../analysis-core/llm/cost.js";
|
|
|
5
5
|
import { anthropicStructuredLimit, supportsReasoningControls } from "../analysis-core/llm/output-limit.js";
|
|
6
6
|
import { requestCachesInput, structuredPromptOf } from "./prompt-cache.js";
|
|
7
7
|
import { StructuredOutputError } from "../analysis-core/llm/structured-output-error.js";
|
|
8
|
+
import { THINKING_MODES } from "../analysis-core/llm/types.js";
|
|
8
9
|
export function anthropicLlm(settings, opts = {}) {
|
|
9
10
|
const client = opts.client ?? new Anthropic({ ...(opts.apiKey === undefined ? {} : { apiKey: opts.apiKey }) });
|
|
10
11
|
return { structured: call => structuredCall(settings, client, call, opts.onUsage, opts.sourceRetries) };
|
|
@@ -19,6 +20,10 @@ export function anthropicCaptureBackend(client = new Anthropic({ maxRetries: 0 }
|
|
|
19
20
|
function canceled() {
|
|
20
21
|
return new JobError("canceled", "The model call was canceled.");
|
|
21
22
|
}
|
|
23
|
+
export class ProviderRequestRefused extends Error {
|
|
24
|
+
name = "ProviderRequestRefused";
|
|
25
|
+
constructor() { super("Invalid model request"); }
|
|
26
|
+
}
|
|
22
27
|
export class ProviderCallError extends JobError {
|
|
23
28
|
payment;
|
|
24
29
|
constructor(code, message, retryable, payment) {
|
|
@@ -34,6 +39,10 @@ function providerFailure(error, signal) {
|
|
|
34
39
|
return new ProviderCallError("rate_limited", "The model provider is busy. Wait a minute and try again.", true, "unpaid");
|
|
35
40
|
}
|
|
36
41
|
if (error instanceof Anthropic.APIConnectionError) {
|
|
42
|
+
// The guard rejects before network access, so no provider work can have been paid.
|
|
43
|
+
if (error.cause instanceof ProviderRequestRefused) {
|
|
44
|
+
return new ProviderCallError("llm", "Invalid model request", false, "unpaid");
|
|
45
|
+
}
|
|
37
46
|
return new ProviderCallError("llm", "The model could not be reached. Try again.", true, "unknown");
|
|
38
47
|
}
|
|
39
48
|
const directive = error instanceof Anthropic.APIError ? error.headers?.get("x-should-retry") : undefined;
|
|
@@ -66,13 +75,13 @@ function providerRequest(settings, call) {
|
|
|
66
75
|
const streamed = supportsReasoningControls(call.model);
|
|
67
76
|
const params = {
|
|
68
77
|
model: call.model, max_tokens: anthropicStructuredLimit(call),
|
|
69
|
-
...(streamed ? { thinking: { type: call.thinking ??
|
|
78
|
+
...(streamed ? { thinking: { type: call.thinking ?? THINKING_MODES.adaptive } } : {}),
|
|
70
79
|
...(call.system ? { system: call.system } : {}),
|
|
71
80
|
messages: [{ role: "user", content: structuredPromptOf(settings, call) }],
|
|
72
81
|
output_config: {
|
|
73
82
|
// SDK stream parsing can throw before returning usage for incomplete JSON.
|
|
74
83
|
format: { type: format.type, schema: format.schema },
|
|
75
|
-
...(call.effort && streamed && call.thinking !==
|
|
84
|
+
...(call.effort && streamed && call.thinking !== THINKING_MODES.disabled ? { effort: call.effort } : {}),
|
|
76
85
|
},
|
|
77
86
|
};
|
|
78
87
|
return { params, format, streamed };
|
|
@@ -104,6 +104,9 @@ export async function loadCompletion(folder) {
|
|
|
104
104
|
return null;
|
|
105
105
|
throw new Error(INVALID_COMPLETION_MESSAGE);
|
|
106
106
|
}
|
|
107
|
+
// Released folder-local records always had a Git commit; hashes alone cannot establish their provenance.
|
|
108
|
+
if (legacy.payload.agent.commit === null)
|
|
109
|
+
return null;
|
|
107
110
|
if (!await matchesSnapshotProvenance(folder, legacy.payload.agent))
|
|
108
111
|
throw new Error(INVALID_COMPLETION_MESSAGE);
|
|
109
112
|
// Attempt imported recovery once automatically; uncertain legacy data must not block later analyses.
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
|
-
import { open, rename, rm } from "node:fs/promises";
|
|
3
|
+
import { chmod, mkdir, open, rename, rm } from "node:fs/promises";
|
|
4
4
|
import { dirname } from "node:path";
|
|
5
|
+
export async function preparePrivateDirectory(path) {
|
|
6
|
+
await mkdir(path, { recursive: true, mode: 0o700 });
|
|
7
|
+
await chmod(path, 0o700);
|
|
8
|
+
}
|
|
5
9
|
export async function durableWrite(path, bytes) {
|
|
6
10
|
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
11
|
+
let created = false;
|
|
7
12
|
const permission = process.permission !== undefined;
|
|
8
13
|
// The permission model disables fsync, so sync data on each write and skip the unavailable directory sync.
|
|
9
14
|
const flags = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW | (permission ? constants.O_DSYNC : 0);
|
|
10
15
|
try {
|
|
11
16
|
const file = await open(temporary, flags, 0o600);
|
|
17
|
+
created = true;
|
|
12
18
|
try {
|
|
13
19
|
await file.writeFile(bytes);
|
|
14
20
|
if (!permission)
|
|
@@ -29,6 +35,7 @@ export async function durableWrite(path, bytes) {
|
|
|
29
35
|
}
|
|
30
36
|
}
|
|
31
37
|
finally {
|
|
32
|
-
|
|
38
|
+
if (created)
|
|
39
|
+
await rm(temporary, { force: true });
|
|
33
40
|
}
|
|
34
41
|
}
|
package/dist/adapters/files.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { opendir, realpath, stat, readFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join, posix } from "node:path";
|
|
3
|
-
import { admitFile, fileRefusalWarning, localSymlinkGuardEnabled, respectGitignore, readRepoFile, resolveLocalExclusions } from "../analysis-core/conventions/file-admission.js";
|
|
3
|
+
import { admitFile, FileReadRefusal, fileRefusalWarning, localSymlinkGuardEnabled, respectGitignore, readRepoFile, resolveLocalExclusions } from "../analysis-core/conventions/file-admission.js";
|
|
4
|
+
import { assessAnalysisSize, measureAnalysisSize } from "../analysis-core/conventions/analysis-fit.js";
|
|
4
5
|
import { localRepoKeyFor } from "../analysis-core/github/repo-key.js";
|
|
5
6
|
import { fetchLocalSources } from "../analysis-core/index.js";
|
|
6
7
|
import { repoPathFrom } from "../analysis-core/import/outside-loader-sources.js";
|
|
@@ -33,12 +34,15 @@ export async function readLocalFolder(settings, folder, admission = {}, options
|
|
|
33
34
|
}
|
|
34
35
|
});
|
|
35
36
|
const targets = new Map();
|
|
36
|
-
const snapshot =
|
|
37
|
+
const snapshot = Object.create(null);
|
|
38
|
+
const snapshotTargets = new Map();
|
|
37
39
|
let visited = 0;
|
|
38
40
|
const targetOf = async (path, purpose, directory = false) => {
|
|
39
41
|
const target = await realpath(join(root, path));
|
|
40
42
|
const result = admitFile(settings, { path, directory }, { purpose, route: "local-folder", ...admission, localTarget: { root, path: target, excludedTargets } });
|
|
41
43
|
if (!result.admitted) {
|
|
44
|
+
if (purpose === "proposal")
|
|
45
|
+
throw new FileReadRefusal(result.reason, path);
|
|
42
46
|
refused.push({ path, reason: result.reason });
|
|
43
47
|
if (result.reason === "secret" || result.reason === "out-of-scope")
|
|
44
48
|
warnings.push({ path, reason: result.reason });
|
|
@@ -49,6 +53,8 @@ export async function readLocalFolder(settings, folder, admission = {}, options
|
|
|
49
53
|
const allowed = admitFile(settings, { path, directory }, { purpose, route: "local-folder", ...admission,
|
|
50
54
|
gitIgnored, localTarget: { root, path: target, parent, excludedTargets } });
|
|
51
55
|
if (!allowed.admitted) {
|
|
56
|
+
if (purpose === "proposal")
|
|
57
|
+
throw new FileReadRefusal(allowed.reason, path);
|
|
52
58
|
if (directory)
|
|
53
59
|
gitSkipped.push(...await ignoredFiles(join(root, path), path));
|
|
54
60
|
else
|
|
@@ -117,23 +123,33 @@ export async function readLocalFolder(settings, folder, admission = {}, options
|
|
|
117
123
|
await walk(root, "", new Set());
|
|
118
124
|
else
|
|
119
125
|
gitSkipped.push(...await ignoredFiles(root, ""));
|
|
126
|
+
const analysisSize = measureAnalysisSize(settings, entries, admission);
|
|
127
|
+
// The service makes the final decision; local prediction never changes retained files.
|
|
128
|
+
const analysisFit = settings.CF_REFUSE_TOO_LARGE === "0" ? undefined : assessAnalysisSize(analysisSize);
|
|
120
129
|
const listing = new Set(entries.map(entry => entry.path));
|
|
121
130
|
const repoKey = localRepoKeyFor(root);
|
|
122
131
|
const repository = createRepositoryFiles(settings, root, admission, options);
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
132
|
+
const read = async (entry, purpose) => {
|
|
133
|
+
const target = guarded || purpose === "proposal" ? await targetOf(entry.path, purpose) : targets.get(entry.path);
|
|
134
|
+
return target ? readRepoFile(settings, entry, async () => {
|
|
135
|
+
if (!options.captureSnapshot)
|
|
136
|
+
return readFile(target, "utf8");
|
|
137
|
+
const cached = purpose !== "proposal" || snapshotTargets.get(entry.path) === target ? snapshot[entry.path] : undefined;
|
|
138
|
+
const bytes = cached ?? await readFile(target);
|
|
139
|
+
snapshot[entry.path] = bytes;
|
|
140
|
+
snapshotTargets.set(entry.path, target);
|
|
141
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
142
|
+
}, { ...admission, listing, route: "local-folder", localTarget: { root, path: target, excludedTargets } }) : null;
|
|
143
|
+
};
|
|
144
|
+
const discovered = await fetchLocalSources(settings, { repoKey, entries, admission, repository,
|
|
145
|
+
proposal: options.proposal, proposalRefusals: [...gitSkipped, ...refused],
|
|
146
|
+
read: entry => read(entry, "override"), proposalRead: entry => read(entry, "proposal"),
|
|
147
|
+
});
|
|
133
148
|
const expanded = discovered.root !== undefined;
|
|
134
149
|
const ignored = mergeSkippedFiles(expanded ? gitSkipped.map(file => ({ ...file, path: repoPathFrom(file.path, discovered.root) })) : gitSkipped, repository.skippedFiles.map(file => ({ ...file, path: expanded ? file.path : posix.relative(repository.selectedRoot ?? "", file.path) })));
|
|
135
150
|
const captured = { ...Object.fromEntries(Object.entries(snapshot).map(([path, bytes]) => [repoPathFrom(path, discovered.root), bytes])), ...repository.snapshot };
|
|
136
|
-
return { repoKey, files: discovered.files, ...(expanded ? { root: discovered.root, sourceBase: repository.sourceBase } : {}),
|
|
151
|
+
return { repoKey, ...(discovered.agentChoice ? { agentChoice: discovered.agentChoice } : {}), files: discovered.files, analysisSize, ...(analysisFit ? { analysisFit } : {}), ...(expanded ? { root: discovered.root, sourceBase: repository.sourceBase } : {}),
|
|
152
|
+
...(discovered.proposalCounts ? { proposalCounts: discovered.proposalCounts } : {}),
|
|
137
153
|
...(options.captureSnapshot ? { snapshot: Object.fromEntries(Object.keys(discovered.files).map(path => [path, captured[path]])) } : {}), fragment: discovered.fragment, selection: discovered.selection,
|
|
138
154
|
skippedFiles: mergeSkippedFiles(discovered.skippedFiles, ignored, expanded ? refused.map(file => ({ ...file, path: repoPathFrom(file.path, discovered.root) })) : refused),
|
|
139
155
|
warnings: [...new Set(mergeUnlistedFileWarnings([...warnings.map(file => fileRefusalWarning(repoPathFrom(file.path, discovered.root), file.reason)), ...discovered.warnings], ignored.length))] };
|
|
@@ -2,21 +2,28 @@ import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
|
2
2
|
import { mapLimit } from "../analysis-core/map-limit.js";
|
|
3
3
|
import { respectGitignore } from "../analysis-core/conventions/file-admission.js";
|
|
4
4
|
import { runGit } from "./git.js";
|
|
5
|
-
import { gitRepositoryRoot
|
|
5
|
+
import { gitRepositoryRoot } from "./repository-root.js";
|
|
6
6
|
/** Query path facts only; admission owns whether an ignored path can be read. */
|
|
7
7
|
export function createGitIgnore(settings, folder) {
|
|
8
|
+
const query = createGitIgnoreQuery(folder);
|
|
9
|
+
return (paths) => respectGitignore(settings) ? query(paths) : Promise.resolve(undefined);
|
|
10
|
+
}
|
|
11
|
+
export function createGitIgnoreQuery(folder) {
|
|
12
|
+
const query = createGitIgnoreFactsQuery(folder);
|
|
13
|
+
return async (paths) => {
|
|
14
|
+
const facts = await query(paths);
|
|
15
|
+
return facts && new Set([...facts].filter(([, fact]) => fact === "ignored").map(([path]) => path));
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Git supplies the last matching rule, including a deliberate negation. */
|
|
19
|
+
export function createGitIgnoreFactsQuery(folder) {
|
|
8
20
|
const cache = new Map();
|
|
9
21
|
let root;
|
|
10
22
|
return async (paths) => {
|
|
11
|
-
if (!respectGitignore(settings))
|
|
12
|
-
return undefined;
|
|
13
23
|
if (!paths.length)
|
|
14
|
-
return new
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
gitRepositoryRoot(async (args) => (await runGit(folder, args)).toString("utf8"))]);
|
|
18
|
-
return boundary && await searchBaseMatches(boundary.base, reported) ? boundary.base : null;
|
|
19
|
-
})());
|
|
24
|
+
return new Map();
|
|
25
|
+
// Git's root owns ignore rules even when discovery stops at a different marker.
|
|
26
|
+
const base = await (root ??= gitRepositoryRoot(async (args) => (await runGit(folder, args)).toString("utf8"), folder));
|
|
20
27
|
if (!base)
|
|
21
28
|
return undefined;
|
|
22
29
|
const candidates = paths.filter(path => {
|
|
@@ -30,20 +37,27 @@ export function createGitIgnore(settings, folder) {
|
|
|
30
37
|
const input = Buffer.from(batch.map(path => resolve(folder, path)).join("\0") + "\0");
|
|
31
38
|
let output;
|
|
32
39
|
try {
|
|
33
|
-
output = await runGit(folder, ["check-ignore", "-z", "--stdin"], { userConfig: true, input, maxBuffer: input.length + 1024 });
|
|
40
|
+
output = await runGit(folder, ["check-ignore", "--verbose", "-z", "--stdin"], { userConfig: true, input, maxBuffer: input.length + 1024 * 1024 });
|
|
34
41
|
}
|
|
35
42
|
catch (error) {
|
|
36
43
|
if (error.code === 1) {
|
|
37
44
|
for (const path of batch)
|
|
38
|
-
cache.set(path,
|
|
45
|
+
cache.set(path, null);
|
|
39
46
|
return;
|
|
40
47
|
}
|
|
41
48
|
throw error;
|
|
42
49
|
}
|
|
43
|
-
const
|
|
50
|
+
const fields = output.toString("utf8").split("\0");
|
|
51
|
+
const matches = new Map();
|
|
52
|
+
for (let index = 0; index + 3 < fields.length; index += 4) {
|
|
53
|
+
matches.set(fields[index + 3], fields[index + 2].startsWith("!") ? "included" : "ignored");
|
|
54
|
+
}
|
|
44
55
|
for (const path of batch)
|
|
45
|
-
cache.set(path, matches.
|
|
56
|
+
cache.set(path, matches.get(path) ?? null);
|
|
46
57
|
});
|
|
47
|
-
return new
|
|
58
|
+
return new Map(candidates.flatMap(path => {
|
|
59
|
+
const fact = cache.get(resolve(folder, path));
|
|
60
|
+
return fact ? [[path, fact]] : [];
|
|
61
|
+
}));
|
|
48
62
|
};
|
|
49
63
|
}
|
package/dist/adapters/git.js
CHANGED
|
@@ -8,7 +8,7 @@ export async function runGit(folder, args, options = {}) {
|
|
|
8
8
|
"-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "-c", "core.sshCommand=",
|
|
9
9
|
"-c", "protocol.ext.allow=never", "-c", "diff.external=", "-c", "core.attributesFile=/dev/null",
|
|
10
10
|
...overrides, "-C", folder, ...command], {
|
|
11
|
-
encoding: "buffer", timeout: options.timeout ?? 3000, maxBuffer,
|
|
11
|
+
encoding: "buffer", timeout: options.timeout ?? 3000, maxBuffer, signal: options.signal,
|
|
12
12
|
env: { ...env, GIT_ATTR_NOSYSTEM: "1", GIT_NO_REPLACE_OBJECTS: "1", GIT_NO_LAZY_FETCH: "1", GIT_TERMINAL_PROMPT: "0", GIT_CONFIG_NOSYSTEM: "1",
|
|
13
13
|
GIT_CONFIG_GLOBAL: options.userConfig ? process.env.GIT_CONFIG_GLOBAL : "/dev/null", GIT_PAGER: "cat" },
|
|
14
14
|
});
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
}
|
package/dist/adapters/models.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { executeStructuredRequest } from "../analysis-core/llm/execution.js";
|
|
2
|
-
import { findingHeadlinesEnabled } from "../analysis-core/settings.js";
|
|
3
|
-
import { headlineRequest } from "../analysis-core/analysis/headlines/input.js";
|
|
4
2
|
import { retryCalls } from "../analysis-core/llm/slots.js";
|
|
5
3
|
import { relayModel, ModelConfigurationError } from "../gatekeeper/model.js";
|
|
6
4
|
import { readableRequestTools, readableInstructions } from "./instructions.js";
|
|
@@ -42,10 +40,7 @@ export async function selectAnalysisExecution(transport, source, relay) {
|
|
|
42
40
|
return { model: relayModel({ ...relay, bundleVersion: requestTools.bundleVersion }), requestTools,
|
|
43
41
|
instructions: async () => { throw new Error("Readable instructions are unavailable in relay mode"); } };
|
|
44
42
|
}
|
|
45
|
-
export async function analysisExecution(model, options = {}
|
|
43
|
+
export async function analysisExecution(model, options = {}) {
|
|
46
44
|
const execution = options.execution ?? await selectAnalysisExecution(options.transport ?? {}, () => "execute" in model ? model : localModel(model), options.relay);
|
|
47
|
-
// Validate enabled result support before earlier stages can spend, even for supplied executions.
|
|
48
|
-
if (findingHeadlinesEnabled(settings))
|
|
49
|
-
execution.requestTools.headlinePlan(headlineRequest([]));
|
|
50
45
|
return execution;
|
|
51
46
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { lstat, mkdir, realpath } from "node:fs/promises";
|
|
1
|
+
import { chmod, lstat, mkdir, realpath } from "node:fs/promises";
|
|
2
2
|
import { dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { FINISH_FAILURE_MESSAGE } from "../contract/cli-refusals.js";
|
|
4
4
|
import { resolveFolderPath } from "./folder-path.js";
|
|
5
5
|
import { durableWrite } from "./durable-write.js";
|
|
6
|
+
export const DEFAULT_OUTPUT_DIR = ".counterfact";
|
|
6
7
|
export class OutputPathError extends Error {
|
|
7
8
|
constructor() {
|
|
8
9
|
super(FINISH_FAILURE_MESSAGE);
|
|
@@ -16,7 +17,7 @@ const within = (base, path) => {
|
|
|
16
17
|
/** Validate the selected anchor and every output component before creating or replacing files. */
|
|
17
18
|
export async function outputPaths(folder, names, outputDir, create = false) {
|
|
18
19
|
const selected = resolve(folder), root = await resolveFolderPath(folder);
|
|
19
|
-
const requested = resolve(outputDir ?? join(selected,
|
|
20
|
+
const requested = resolve(outputDir ?? join(selected, DEFAULT_OUTPUT_DIR));
|
|
20
21
|
let directory = within(selected, requested) ? resolve(root, relative(selected, requested)) : requested;
|
|
21
22
|
const external = !within(root, directory);
|
|
22
23
|
const userUid = process.getuid?.();
|
|
@@ -84,6 +85,8 @@ export async function outputPaths(folder, names, outputDir, create = false) {
|
|
|
84
85
|
export async function writeOutputFiles(folder, files, outputDir) {
|
|
85
86
|
const names = Object.keys(files);
|
|
86
87
|
const paths = await outputPaths(folder, names, outputDir, true);
|
|
88
|
+
if (paths.length)
|
|
89
|
+
await chmod(dirname(paths[0]), 0o700);
|
|
87
90
|
for (let index = 0; index < paths.length; index++) {
|
|
88
91
|
await outputPaths(folder, names, outputDir);
|
|
89
92
|
await durableWrite(paths[index], files[names[index]]);
|
|
@@ -13,7 +13,7 @@ export function loadPublicRequestTools(value, verificationKey, settings = {}) {
|
|
|
13
13
|
const renderingUnavailable = () => { throw new Error("Instruction rendering requires the sealed relay"); };
|
|
14
14
|
if (findingHeadlinesEnabled(settings) && bundle.metadata.headlinePlan?.resultVersion !== HEADLINE_RESULT_VERSION)
|
|
15
15
|
renderingUnavailable();
|
|
16
|
-
return { ...versions, bundleVersion: bundle.version, modelReaderPrompt: renderingUnavailable, callInputBytes, checkDiagnosticsText,
|
|
16
|
+
return { ...versions, bundleVersion: bundle.version, answerFormats: bundle.answerFormats, modelReaderPrompt: renderingUnavailable, callInputBytes, checkDiagnosticsText,
|
|
17
17
|
buildAnalysisCall: renderingUnavailable,
|
|
18
18
|
headlinePlan(request) {
|
|
19
19
|
const plan = bundle.metadata.headlinePlan;
|
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { bundleContentHash } from "../bundle/seal/signature.js";
|
|
2
2
|
import { checkPlannedCallsAgainstBundle } from "../bundle/seal/planned-calls.js";
|
|
3
|
-
import {
|
|
3
|
+
import { localSourcePlan } from "../analysis-core/requests/local-plan.js";
|
|
4
4
|
import { relayPlanSchema } from "../contract/console.js";
|
|
5
5
|
/** Callers authenticate the release before reading files; only this owner turns source facts into a paid plan. */
|
|
6
6
|
export function prepareRelayPlan({ bundle, tools, relayVersion, settings, source }) {
|
|
7
|
-
const
|
|
8
|
-
const calls = localCallPlan(settings, files, {
|
|
9
|
-
structureState: { repos: [{ key: source.repoKey, kind: "fs", files: source.files, ...(source.root !== undefined ? { root: source.root } : {}) }], mapped: [source.fragment] },
|
|
10
|
-
});
|
|
7
|
+
const { files, calls } = localSourcePlan(settings, source);
|
|
11
8
|
checkPlannedCallsAgainstBundle(bundle, calls);
|
|
12
9
|
return { files, plan: relayPlanSchema.parse({ bundleVersion: tools.bundleVersion,
|
|
13
|
-
bundleHash:
|
|
10
|
+
bundleHash: bundleContentHash(bundle.sealed), relayVersion,
|
|
14
11
|
calls: calls.map(({ routeId, model, calls, maxInputBytes, maxInputTokens, maxOutputTokens }) => ({ routeId, model, calls, maxInputBytes, maxInputTokens, maxOutputTokens })) }) };
|
|
15
12
|
}
|
package/dist/adapters/release.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash, createPublicKey } from "node:crypto";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
2
3
|
import { readFile } from "node:fs/promises";
|
|
3
4
|
import { sealedBundle } from "../contract/messages.js";
|
|
4
5
|
import { loadPublicRequestTools } from "./public-instructions.js";
|
|
@@ -6,6 +7,7 @@ import { relayOrigin } from "../gatekeeper/connection.js";
|
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
import { cliRefusals } from "../contract/cli-refusals.js";
|
|
8
9
|
import { RELEASE_KEY_SPKI_SHA256 } from "../contract/release-key.js";
|
|
10
|
+
import { findingAudienceSettings } from "../analysis-core/analysis/finding-audience-switch.js";
|
|
9
11
|
const releasePins = z.object({ verificationKey: z.string().min(1), relayVersion: z.string().min(1), localVersion: z.string().min(1),
|
|
10
12
|
relayUrl: z.string().min(1), projectContext: z.literal("checks") });
|
|
11
13
|
export function readClientReleasePins(value) {
|
|
@@ -19,7 +21,7 @@ export function validateClientRelease(release, settings = {}) {
|
|
|
19
21
|
const tools = loadPublicRequestTools(bundle, release.verificationKey, settings);
|
|
20
22
|
if (!bundle.relayVersions.includes(release.relayVersion))
|
|
21
23
|
throw new Error();
|
|
22
|
-
return { bundle, tools };
|
|
24
|
+
return { bundle, tools, settings: findingAudienceSettings(settings, bundle) };
|
|
23
25
|
}
|
|
24
26
|
export function readClientReleaseKey(pem) {
|
|
25
27
|
const key = createPublicKey(pem);
|
|
@@ -28,6 +30,10 @@ export function readClientReleaseKey(pem) {
|
|
|
28
30
|
throw new Error(cliRefusals.release_unavailable);
|
|
29
31
|
return key;
|
|
30
32
|
}
|
|
33
|
+
export function installedReleasePaths() {
|
|
34
|
+
const base = new URL(import.meta.url.endsWith(".ts") ? "../bundle/" : "../../bundle/", import.meta.url);
|
|
35
|
+
return { bundle: fileURLToPath(new URL("sealed.json", base)), metadata: fileURLToPath(new URL("client-release.json", base)) };
|
|
36
|
+
}
|
|
31
37
|
/** Installed releases always use the separately reviewed signing key. */
|
|
32
38
|
export function loadInstalledRelease(paths) {
|
|
33
39
|
return loadRelease(paths, false);
|
|
@@ -38,14 +44,14 @@ export function loadDevelopmentRelease(paths) {
|
|
|
38
44
|
}
|
|
39
45
|
async function loadRelease(paths, development) {
|
|
40
46
|
try {
|
|
41
|
-
const
|
|
42
|
-
const metadata = JSON.parse(await readFile(
|
|
47
|
+
const location = paths ?? installedReleasePaths();
|
|
48
|
+
const metadata = JSON.parse(await readFile(location.metadata, "utf8"));
|
|
43
49
|
const pins = paths ? metadata : readClientReleasePins(metadata);
|
|
44
50
|
if (typeof pins.verificationKey !== "string" || typeof pins.relayVersion !== "string" || !pins.relayVersion ||
|
|
45
51
|
typeof pins.localVersion !== "string" || !pins.localVersion)
|
|
46
52
|
throw new Error();
|
|
47
53
|
const verificationKey = development ? createPublicKey(pins.verificationKey) : readClientReleaseKey(pins.verificationKey);
|
|
48
|
-
const bundle = JSON.parse(await readFile(
|
|
54
|
+
const bundle = JSON.parse(await readFile(location.bundle, "utf8"));
|
|
49
55
|
const release = { bundle, verificationKey, relayVersion: pins.relayVersion, localVersion: pins.localVersion,
|
|
50
56
|
relayUrl: pins.relayUrl, projectContext: pins.projectContext };
|
|
51
57
|
validateClientRelease(release);
|