@attest-ai/core 0.9.0 → 1.0.0-rc.1
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/dist/cjs/index.cjs +272 -114
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +47 -16
- package/dist/cjs/proto/index.cjs +36 -0
- package/dist/cjs/proto/index.cjs.map +1 -1
- package/dist/cjs/proto/index.d.cts +34 -1
- package/dist/esm/cli.d.ts +19 -4
- package/dist/esm/cli.js +450 -407
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/index.d.ts +47 -16
- package/dist/esm/index.js +263 -115
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/proto/index.d.ts +34 -1
- package/dist/esm/proto/index.js +26 -0
- package/dist/esm/proto/index.js.map +1 -1
- package/package.json +4 -3
package/dist/esm/cli.js
CHANGED
|
@@ -1,473 +1,516 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
5
|
-
import * as
|
|
6
|
-
import * as
|
|
4
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
5
|
+
import * as fs3 from "fs";
|
|
6
|
+
import * as os2 from "os";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
7
8
|
|
|
8
9
|
// src/version.ts
|
|
9
|
-
var VERSION = "0.
|
|
10
|
+
var VERSION = "1.0.0-rc.1";
|
|
11
|
+
var ENGINE_VERSION = "1.0.0-rc.1";
|
|
10
12
|
|
|
11
|
-
// src/
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
// src/engine-manager.ts
|
|
14
|
+
import { spawn, spawnSync } from "child_process";
|
|
15
|
+
import { createInterface } from "readline";
|
|
16
|
+
import { existsSync, accessSync, statSync as statSync2, constants as fsConstants } from "fs";
|
|
17
|
+
import { delimiter, join as join2 } from "path";
|
|
18
|
+
|
|
19
|
+
// src/engine-downloader.ts
|
|
20
|
+
import * as https from "https";
|
|
21
|
+
import * as http from "http";
|
|
22
|
+
import * as fs from "fs";
|
|
23
|
+
import * as path from "path";
|
|
24
|
+
import * as os from "os";
|
|
25
|
+
import * as crypto from "crypto";
|
|
26
|
+
var RELEASE_BASE = `https://github.com/attest-framework/attest/releases/download/v${ENGINE_VERSION}`;
|
|
27
|
+
var CHECKSUMS_URL = `${RELEASE_BASE}/checksums-sha256.txt`;
|
|
28
|
+
var MAX_REDIRECTS = 5;
|
|
29
|
+
function versionedBinDir() {
|
|
30
|
+
return path.join(os.homedir(), ".attest", "bin", ENGINE_VERSION);
|
|
31
|
+
}
|
|
32
|
+
function platformKey() {
|
|
33
|
+
const platform2 = os.platform();
|
|
34
|
+
const arch2 = os.arch();
|
|
35
|
+
const mapping = {
|
|
36
|
+
darwin: { arm64: "darwin-arm64", x64: "darwin-amd64" },
|
|
37
|
+
linux: { arm64: "linux-arm64", x64: "linux-amd64" },
|
|
38
|
+
win32: { x64: "windows-amd64", arm64: "windows-arm64" }
|
|
39
|
+
};
|
|
40
|
+
const osMap = mapping[platform2];
|
|
41
|
+
if (!osMap) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Unsupported platform: ${platform2}. Supported: darwin, linux, win32.`
|
|
44
|
+
);
|
|
19
45
|
}
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
for (const p of pairs) {
|
|
26
|
-
const hPos = p.human >= threshold;
|
|
27
|
-
const jPos = p.judge >= threshold;
|
|
28
|
-
if (hPos) humanPos++;
|
|
29
|
-
if (jPos) judgePos++;
|
|
30
|
-
if (hPos && jPos) bothPos++;
|
|
31
|
-
if (!hPos && !jPos) bothNeg++;
|
|
46
|
+
const key = osMap[arch2];
|
|
47
|
+
if (!key) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Unsupported architecture '${arch2}' on platform '${platform2}'. Supported architectures: ${Object.keys(osMap).join(", ")}.`
|
|
50
|
+
);
|
|
32
51
|
}
|
|
33
|
-
|
|
34
|
-
const judgeNeg = n - judgePos;
|
|
35
|
-
const agreement = (bothPos + bothNeg) / n;
|
|
36
|
-
const expected = (humanPos * judgePos + humanNeg * judgeNeg) / (n * n);
|
|
37
|
-
const cohenKappa = expected < 1 ? (agreement - expected) / (1 - expected) : 0;
|
|
38
|
-
const rocAuc = rocAUC(pairs, threshold);
|
|
39
|
-
return { threshold, n, agreement, cohenKappa, rocAuc };
|
|
52
|
+
return key;
|
|
40
53
|
}
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
const avgRank = (i + j + 1) / 2;
|
|
58
|
-
for (let k = i; k < j; k++) {
|
|
59
|
-
if (rows[k].pos) rankSumPos += avgRank;
|
|
54
|
+
function binaryFilename() {
|
|
55
|
+
return os.platform() === "win32" ? "attest-engine.exe" : "attest-engine";
|
|
56
|
+
}
|
|
57
|
+
function cachedEnginePath() {
|
|
58
|
+
const binPath = path.join(versionedBinDir(), binaryFilename());
|
|
59
|
+
return fs.statSync(binPath, { throwIfNoEntry: false })?.isFile() ? binPath : null;
|
|
60
|
+
}
|
|
61
|
+
function parseChecksums(text) {
|
|
62
|
+
const checksums = /* @__PURE__ */ new Map();
|
|
63
|
+
for (const line of text.split("\n")) {
|
|
64
|
+
const trimmed = line.trim();
|
|
65
|
+
if (!trimmed) continue;
|
|
66
|
+
const match = trimmed.match(/^([a-f0-9]{64})\s+(.+)$/);
|
|
67
|
+
if (match) {
|
|
68
|
+
checksums.set(match[2], match[1]);
|
|
60
69
|
}
|
|
61
|
-
i = j;
|
|
62
70
|
}
|
|
63
|
-
|
|
64
|
-
if (Number.isNaN(auc) || !Number.isFinite(auc)) return 0;
|
|
65
|
-
return auc;
|
|
71
|
+
return checksums;
|
|
66
72
|
}
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
let start = 0;
|
|
73
|
-
if (rows[0].length >= 2) {
|
|
74
|
-
const firstHuman = parseFloatStrict(rows[0][1]);
|
|
75
|
-
if (firstHuman === null) start = 1;
|
|
73
|
+
function httpsGet(url, redirectCount = 0) {
|
|
74
|
+
if (redirectCount > MAX_REDIRECTS) {
|
|
75
|
+
return Promise.reject(
|
|
76
|
+
new Error(`Too many redirects (>${MAX_REDIRECTS}) following ${url}`)
|
|
77
|
+
);
|
|
76
78
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
throw new Error(`CSV line ${lineNum}: human_label not a float: ${row[1]}`);
|
|
91
|
-
}
|
|
92
|
-
const rec = {
|
|
93
|
-
input: row[0],
|
|
94
|
-
humanLabel: human,
|
|
95
|
-
judgeScore: 0,
|
|
96
|
-
judgeKnown: false
|
|
97
|
-
};
|
|
98
|
-
if (row.length >= 3 && row[2].trim() !== "") {
|
|
99
|
-
const judge = parseFloatStrict(row[2]);
|
|
100
|
-
if (judge === null) {
|
|
101
|
-
throw new Error(
|
|
102
|
-
`CSV line ${lineNum}: judge_score not a float: ${row[2]}`
|
|
103
|
-
);
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const client = url.startsWith("https:") ? https : http;
|
|
81
|
+
client.get(url, (res) => {
|
|
82
|
+
const status = res.statusCode ?? 0;
|
|
83
|
+
if (status === 301 || status === 302) {
|
|
84
|
+
const location = res.headers.location;
|
|
85
|
+
if (!location) {
|
|
86
|
+
reject(new Error(`Redirect ${status} without Location header from ${url}`));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
res.resume();
|
|
90
|
+
resolve(httpsGet(location, redirectCount + 1));
|
|
91
|
+
return;
|
|
104
92
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
if (out.length === 0) {
|
|
111
|
-
throw new Error("CSV contained no labeled rows");
|
|
112
|
-
}
|
|
113
|
-
return out;
|
|
114
|
-
}
|
|
115
|
-
function loadLabelsJSONL(text) {
|
|
116
|
-
const out = [];
|
|
117
|
-
const lines = text.split(/\r?\n/);
|
|
118
|
-
for (let i = 0; i < lines.length; i++) {
|
|
119
|
-
const stripped = lines[i].trim();
|
|
120
|
-
if (!stripped) continue;
|
|
121
|
-
let obj;
|
|
122
|
-
try {
|
|
123
|
-
obj = JSON.parse(stripped);
|
|
124
|
-
} catch (err) {
|
|
125
|
-
throw new Error(`JSONL line ${i + 1}: ${err.message}`);
|
|
126
|
-
}
|
|
127
|
-
if (Object.keys(obj).length === 0) continue;
|
|
128
|
-
if (obj["human_label"] === void 0) {
|
|
129
|
-
throw new Error(`JSONL line ${i + 1}: missing human_label`);
|
|
130
|
-
}
|
|
131
|
-
const human = parseJSONNumber(obj["human_label"]);
|
|
132
|
-
if (human === null) {
|
|
133
|
-
throw new Error(`JSONL line ${i + 1}: human_label is not a number`);
|
|
134
|
-
}
|
|
135
|
-
const rec = {
|
|
136
|
-
input: typeof obj["input"] === "string" ? obj["input"] : "",
|
|
137
|
-
humanLabel: human,
|
|
138
|
-
judgeScore: 0,
|
|
139
|
-
judgeKnown: false
|
|
140
|
-
};
|
|
141
|
-
if (obj["judge_score"] !== void 0 && obj["judge_score"] !== null) {
|
|
142
|
-
const judge = parseJSONNumber(obj["judge_score"]);
|
|
143
|
-
if (judge === null) {
|
|
144
|
-
throw new Error(`JSONL line ${i + 1}: judge_score is not a number`);
|
|
93
|
+
if (status < 200 || status >= 300) {
|
|
94
|
+
res.resume();
|
|
95
|
+
reject(new Error(`HTTP ${status} fetching ${url}`));
|
|
96
|
+
return;
|
|
145
97
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
98
|
+
const chunks = [];
|
|
99
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
100
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
101
|
+
res.on("error", reject);
|
|
102
|
+
}).on("error", reject);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
async function downloadEngine() {
|
|
106
|
+
const key = platformKey();
|
|
107
|
+
const filename = `attest-engine-${key}${os.platform() === "win32" ? ".exe" : ""}`;
|
|
108
|
+
const binaryUrl = `${RELEASE_BASE}/${filename}`;
|
|
109
|
+
process.stderr.write(`[attest] Downloading engine v${ENGINE_VERSION} for ${key}...
|
|
110
|
+
`);
|
|
111
|
+
process.stderr.write(`[attest] Fetching checksums...
|
|
112
|
+
`);
|
|
113
|
+
const checksumsText = (await httpsGet(CHECKSUMS_URL)).toString("utf-8");
|
|
114
|
+
const checksums = parseChecksums(checksumsText);
|
|
115
|
+
const expectedHash = checksums.get(filename);
|
|
116
|
+
if (!expectedHash) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`No checksum found for '${filename}' in checksums-sha256.txt. Available files: ${[...checksums.keys()].join(", ")}`
|
|
119
|
+
);
|
|
150
120
|
}
|
|
151
|
-
|
|
152
|
-
|
|
121
|
+
process.stderr.write(`[attest] Downloading ${binaryUrl}...
|
|
122
|
+
`);
|
|
123
|
+
const data = await httpsGet(binaryUrl);
|
|
124
|
+
const actualHash = crypto.createHash("sha256").update(data).digest("hex");
|
|
125
|
+
if (actualHash !== expectedHash) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`SHA256 mismatch for ${filename}. Expected: ${expectedHash}, got: ${actualHash}. The download may be corrupted. Retry or download manually.`
|
|
128
|
+
);
|
|
153
129
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
130
|
+
process.stderr.write(`[attest] Checksum verified.
|
|
131
|
+
`);
|
|
132
|
+
const dir = versionedBinDir();
|
|
133
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
134
|
+
const destPath = path.join(dir, binaryFilename());
|
|
135
|
+
const suffix = `${String(process.pid)}.${crypto.randomBytes(8).toString("hex")}`;
|
|
136
|
+
const tmpPath = path.join(dir, `.${binaryFilename()}.tmp.${suffix}`);
|
|
137
|
+
try {
|
|
138
|
+
fs.writeFileSync(tmpPath, data, { flag: "wx", mode: 493 });
|
|
139
|
+
fs.chmodSync(tmpPath, 493);
|
|
140
|
+
fs.renameSync(tmpPath, destPath);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
if (err.code !== "EEXIST") fs.rmSync(tmpPath, { force: true });
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
process.stderr.write(`[attest] Engine installed at ${destPath}
|
|
146
|
+
`);
|
|
147
|
+
return destPath;
|
|
162
148
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
149
|
+
|
|
150
|
+
// src/engine-manager.ts
|
|
151
|
+
var ENGINE_BINARY_NAME = "attest-engine";
|
|
152
|
+
var VERSION_PROBE_TIMEOUT_MS = 1e4;
|
|
153
|
+
var EngineVersionMismatchError = class extends Error {
|
|
154
|
+
enginePath;
|
|
155
|
+
source;
|
|
156
|
+
found;
|
|
157
|
+
required;
|
|
158
|
+
constructor(enginePath, source, found, required) {
|
|
159
|
+
const reported = found === null ? "reports no version" : `reports version ${found}`;
|
|
160
|
+
super(
|
|
161
|
+
`attest-engine at ${enginePath} (from ${source}) ${reported}; this SDK requires attest-engine ${required}. Point ATTEST_ENGINE_PATH at a matching engine, or remove the mismatched one from PATH so the SDK can download ${required} into ~/.attest/bin/${required}/.`
|
|
162
|
+
);
|
|
163
|
+
this.name = "EngineVersionMismatchError";
|
|
164
|
+
this.enginePath = enginePath;
|
|
165
|
+
this.source = source;
|
|
166
|
+
this.found = found;
|
|
167
|
+
this.required = required;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
function engineVersion(enginePath) {
|
|
171
|
+
const proc = spawnSync(enginePath, ["version"], {
|
|
172
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
173
|
+
encoding: "utf-8",
|
|
174
|
+
timeout: VERSION_PROBE_TIMEOUT_MS
|
|
175
|
+
});
|
|
176
|
+
if (proc.error) throw proc.error;
|
|
177
|
+
const out = proc.stdout.trim();
|
|
178
|
+
const prefix = `${ENGINE_BINARY_NAME} `;
|
|
179
|
+
if (proc.status !== 0 || !out.startsWith(prefix)) return null;
|
|
180
|
+
return out.slice(prefix.length).trim();
|
|
167
181
|
}
|
|
168
|
-
function
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const ch = text[i];
|
|
176
|
-
if (inQuotes) {
|
|
177
|
-
if (ch === '"') {
|
|
178
|
-
if (text[i + 1] === '"') {
|
|
179
|
-
field += '"';
|
|
180
|
-
i += 2;
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
inQuotes = false;
|
|
184
|
-
i++;
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
field += ch;
|
|
188
|
-
i++;
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
if (ch === '"') {
|
|
192
|
-
inQuotes = true;
|
|
193
|
-
i++;
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
if (ch === ",") {
|
|
197
|
-
current.push(field);
|
|
198
|
-
field = "";
|
|
199
|
-
i++;
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
if (ch === "\n") {
|
|
203
|
-
current.push(field);
|
|
204
|
-
rows.push(current);
|
|
205
|
-
current = [];
|
|
206
|
-
field = "";
|
|
207
|
-
i++;
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
if (ch === "\r") {
|
|
211
|
-
i++;
|
|
182
|
+
function onPath() {
|
|
183
|
+
for (const dir of (process.env["PATH"] ?? "").split(delimiter)) {
|
|
184
|
+
if (!dir) continue;
|
|
185
|
+
const candidate = join2(dir, binaryFilename());
|
|
186
|
+
try {
|
|
187
|
+
accessSync(candidate, fsConstants.X_OK);
|
|
188
|
+
} catch {
|
|
212
189
|
continue;
|
|
213
190
|
}
|
|
214
|
-
|
|
215
|
-
i++;
|
|
216
|
-
}
|
|
217
|
-
if (field.length > 0 || current.length > 0) {
|
|
218
|
-
current.push(field);
|
|
219
|
-
rows.push(current);
|
|
191
|
+
if (statSync2(candidate).isFile()) return candidate;
|
|
220
192
|
}
|
|
221
|
-
return
|
|
193
|
+
return null;
|
|
222
194
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (envOverride) return envOverride;
|
|
228
|
-
return path.join(os.homedir(), ".attest", "cache");
|
|
229
|
-
}
|
|
230
|
-
function cacheDbPath() {
|
|
231
|
-
return path.join(cacheDir(), "attest.db");
|
|
195
|
+
function requireFile(enginePath, label) {
|
|
196
|
+
if (!existsSync(enginePath) || !statSync2(enginePath).isFile()) {
|
|
197
|
+
throw new Error(`${label}=${enginePath} does not exist or is not a file.`);
|
|
198
|
+
}
|
|
232
199
|
}
|
|
233
|
-
function
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
200
|
+
async function locateEngine(explicitPath, allowDownload) {
|
|
201
|
+
if (explicitPath) {
|
|
202
|
+
requireFile(explicitPath, "Engine path");
|
|
203
|
+
return [explicitPath, "explicit engine path"];
|
|
204
|
+
}
|
|
205
|
+
const envPath = process.env["ATTEST_ENGINE_PATH"];
|
|
206
|
+
if (envPath) {
|
|
207
|
+
requireFile(envPath, "ATTEST_ENGINE_PATH");
|
|
208
|
+
return [envPath, "ATTEST_ENGINE_PATH"];
|
|
209
|
+
}
|
|
210
|
+
const found = onPath();
|
|
211
|
+
if (found) return [found, "PATH"];
|
|
212
|
+
const cached = cachedEnginePath();
|
|
213
|
+
if (cached) return [cached, "engine cache"];
|
|
214
|
+
if (!allowDownload) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`No '${ENGINE_BINARY_NAME}' ${ENGINE_VERSION} in ATTEST_ENGINE_PATH, PATH or ~/.attest/bin/${ENGINE_VERSION}/.`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
const noDownload = process.env["ATTEST_ENGINE_NO_DOWNLOAD"]?.toLowerCase();
|
|
220
|
+
if (noDownload === "1" || noDownload === "true" || noDownload === "yes") {
|
|
221
|
+
throw new Error(
|
|
222
|
+
`Cannot find '${ENGINE_BINARY_NAME}' ${ENGINE_VERSION} and ATTEST_ENGINE_NO_DOWNLOAD is set.
|
|
223
|
+
Install options:
|
|
224
|
+
1. Build from source: make engine, then set ATTEST_ENGINE_PATH
|
|
225
|
+
2. Download release v${ENGINE_VERSION}: https://github.com/attest-framework/attest/releases
|
|
226
|
+
3. Allow auto-download by unsetting ATTEST_ENGINE_NO_DOWNLOAD`
|
|
227
|
+
);
|
|
239
228
|
}
|
|
240
|
-
|
|
241
|
-
process.stdout.write(JSON.stringify(stats) + "\n");
|
|
229
|
+
return [await downloadEngine(), "download"];
|
|
242
230
|
}
|
|
243
|
-
function
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
`);
|
|
249
|
-
} else {
|
|
250
|
-
process.stdout.write(`No cache to clear: ${dbPath}
|
|
251
|
-
`);
|
|
231
|
+
async function resolveEngine(explicitPath, options = {}) {
|
|
232
|
+
const [enginePath, source] = await locateEngine(explicitPath, options.allowDownload ?? true);
|
|
233
|
+
const found = engineVersion(enginePath);
|
|
234
|
+
if (found !== ENGINE_VERSION) {
|
|
235
|
+
throw new EngineVersionMismatchError(enginePath, source, found, ENGINE_VERSION);
|
|
252
236
|
}
|
|
237
|
+
return enginePath;
|
|
253
238
|
}
|
|
254
|
-
var CONFTEST_TEMPLATE = `import { describe, it, expect } from "vitest";
|
|
255
|
-
import { attestExpect, TraceBuilder } from "@attest-ai/core";
|
|
256
239
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
240
|
+
// src/scaffold.ts
|
|
241
|
+
import * as fs2 from "fs";
|
|
242
|
+
import * as path2 from "path";
|
|
243
|
+
var DEMO_MARKER = "ATTEST-DEMO";
|
|
244
|
+
var SAMPLE_TEST_TEMPLATE = `// Sample Attest test written by \`attest init\`. Run it with \`npx vitest run\`.
|
|
245
|
+
import { it } from "vitest";
|
|
246
|
+
import { attestExpect, TraceBuilder, type Trace } from "@attest-ai/core";
|
|
247
|
+
import { evaluate } from "@attest-ai/vitest";
|
|
263
248
|
|
|
264
|
-
|
|
265
|
-
|
|
249
|
+
// The trace of one agent run, built by hand so the sample needs no model, API key
|
|
250
|
+
// or network. Replace it with a trace from your agent: wrap the agent with
|
|
251
|
+
// \`new Agent(...)\` or record it with an adapter such as OpenAIAdapter.
|
|
252
|
+
function myAgentResult(): Trace {
|
|
253
|
+
return new TraceBuilder("refund-agent")
|
|
254
|
+
.setInput({ user_message: "Please refund order ORD-1001." })
|
|
255
|
+
.addToolCall("lookup_order", {
|
|
256
|
+
args: { order_id: "ORD-1001" },
|
|
257
|
+
result: { status: "delivered", amount: 42.0 },
|
|
258
|
+
})
|
|
259
|
+
.addToolCall("issue_refund", {
|
|
260
|
+
args: { order_id: "ORD-1001", amount: 42.0 },
|
|
261
|
+
result: { refund_id: "RF-1001" },
|
|
262
|
+
})
|
|
263
|
+
.setOutput({
|
|
264
|
+
message: "Your refund of $42.00 has been issued.",
|
|
265
|
+
structured: { refund_id: "RF-1001", amount: 42.0 },
|
|
266
|
+
})
|
|
267
|
+
.setMetadata({ total_tokens: 180, cost_usd: 0.002, latency_ms: 900 })
|
|
268
|
+
.build();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
it("my agent issues the refund", async (context) => {
|
|
272
|
+
const chain = attestExpect(myAgentResult())
|
|
273
|
+
.outputMatchesSchema({ type: "object", required: ["refund_id"] }) // layer 1: schema
|
|
274
|
+
.costUnder(0.01) // layer 2: constraint
|
|
275
|
+
.toolsCalledInOrder(["lookup_order", "issue_refund"]) // layer 3: trace
|
|
276
|
+
.outputContains("refund"); // ${DEMO_MARKER}: change "refund" to "store credit"
|
|
277
|
+
await evaluate(chain, { context });
|
|
266
278
|
});
|
|
267
279
|
`;
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
+
var REPORT_ARGS = "--reporter=default --reporter=@attest-ai/vitest/reporter --outputFile.attest=attest-report.json";
|
|
281
|
+
var POLICY_TEMPLATE = `# Merge policy for \`attest policy evaluate --report <report> --policy attest.policy.yaml\`.
|
|
282
|
+
# Exit 0: pass. Exit 1: a blocking rule failed. Exit 2: warnings only.
|
|
283
|
+
layers:
|
|
284
|
+
0: # layer 0 caps every layer together
|
|
285
|
+
max_hard_fails: 0
|
|
286
|
+
# Assertions that could not run (no judge configured, simulation) warn instead of block.
|
|
287
|
+
not_evaluated: warn
|
|
288
|
+
`;
|
|
289
|
+
var GITHUB_WORKFLOW_TEMPLATE = `# Written by \`attest init --ci github\`.
|
|
290
|
+
# The test step records its outcome and continues so the report is always uploaded,
|
|
291
|
+
# the policy evaluated and the pull request annotated; the last step fails the job
|
|
292
|
+
# when the tests failed. A policy warning (exit 2) does not fail the job.
|
|
293
|
+
name: attest
|
|
294
|
+
|
|
295
|
+
on:
|
|
296
|
+
pull_request:
|
|
297
|
+
push:
|
|
298
|
+
branches: [main]
|
|
299
|
+
|
|
300
|
+
permissions:
|
|
301
|
+
contents: read
|
|
302
|
+
pull-requests: write
|
|
303
|
+
|
|
304
|
+
jobs:
|
|
305
|
+
attest:
|
|
306
|
+
runs-on: ubuntu-latest
|
|
307
|
+
steps:
|
|
308
|
+
- uses: actions/checkout@v6
|
|
309
|
+
with:
|
|
310
|
+
persist-credentials: false
|
|
311
|
+
- uses: actions/setup-node@v6
|
|
312
|
+
with:
|
|
313
|
+
node-version: 22
|
|
314
|
+
- name: Install dependencies
|
|
315
|
+
run: npm ci
|
|
316
|
+
- name: Run tests
|
|
317
|
+
id: tests
|
|
318
|
+
continue-on-error: true
|
|
319
|
+
run: npx vitest run ${REPORT_ARGS}
|
|
320
|
+
- name: Upload the Attest report
|
|
321
|
+
if: always()
|
|
322
|
+
uses: actions/upload-artifact@v7
|
|
323
|
+
with:
|
|
324
|
+
name: attest-report
|
|
325
|
+
path: attest-report.json
|
|
326
|
+
if-no-files-found: error
|
|
327
|
+
- name: Evaluate the merge policy
|
|
328
|
+
if: always()
|
|
329
|
+
run: |
|
|
330
|
+
set +e
|
|
331
|
+
npx attest policy evaluate --report attest-report.json --policy attest.policy.yaml
|
|
332
|
+
code=$?
|
|
333
|
+
if [ "$code" -eq 2 ]; then
|
|
334
|
+
echo "::warning::attest policy: warnings only (exit 2)"
|
|
335
|
+
exit 0
|
|
336
|
+
fi
|
|
337
|
+
exit "$code"
|
|
338
|
+
- name: Annotate the pull request
|
|
339
|
+
# Fork pull requests get a read-only token that cannot post reviews; the last step
|
|
340
|
+
# still fails the job when tests failed.
|
|
341
|
+
if: >-
|
|
342
|
+
always() && github.event_name == 'pull_request' &&
|
|
343
|
+
github.event.pull_request.head.repo.full_name == github.repository
|
|
344
|
+
uses: attest-framework/attest/.github/actions/pr-annotate@v${VERSION}
|
|
345
|
+
with:
|
|
346
|
+
report: attest-report.json
|
|
347
|
+
- name: Fail the job when tests failed
|
|
348
|
+
if: always() && steps.tests.outcome == 'failure'
|
|
349
|
+
run: |
|
|
350
|
+
echo "::error::vitest failed; see the Run tests step"
|
|
351
|
+
exit 1
|
|
352
|
+
`;
|
|
353
|
+
function write(file, content) {
|
|
354
|
+
if (fs2.existsSync(file)) {
|
|
355
|
+
process.stderr.write(`Skipped (exists): ${file}
|
|
280
356
|
`);
|
|
357
|
+
return;
|
|
281
358
|
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
let testFiles = [];
|
|
286
|
-
if (fs.existsSync(testsDir)) {
|
|
287
|
-
testFiles = fs.readdirSync(testsDir).filter((f) => f.endsWith(".test.ts") || f.endsWith(".test.js"));
|
|
288
|
-
}
|
|
289
|
-
if (testFiles.length === 0) {
|
|
290
|
-
process.stderr.write(
|
|
291
|
-
`Warning: no test files (*.test.ts / *.test.js) found in ${testsDir}.
|
|
292
|
-
`
|
|
293
|
-
);
|
|
294
|
-
} else {
|
|
295
|
-
process.stdout.write(`Found ${testFiles.length} test file(s) in ${testsDir}
|
|
359
|
+
fs2.mkdirSync(path2.dirname(file), { recursive: true });
|
|
360
|
+
fs2.writeFileSync(file, content);
|
|
361
|
+
process.stdout.write(`Created: ${file}
|
|
296
362
|
`);
|
|
297
|
-
}
|
|
298
|
-
const STALE_DAYS = 30;
|
|
299
|
-
const now = Date.now();
|
|
300
|
-
const goldenFiles = findGoldenFiles(targetDir);
|
|
301
|
-
for (const golden of goldenFiles) {
|
|
302
|
-
const mtime = fs.statSync(golden).mtimeMs;
|
|
303
|
-
const ageDays = Math.floor((now - mtime) / (1e3 * 60 * 60 * 24));
|
|
304
|
-
if (ageDays > STALE_DAYS) {
|
|
305
|
-
process.stderr.write(
|
|
306
|
-
`Warning: golden trace ${golden} is ${ageDays} days old (>${STALE_DAYS} days). Consider regenerating.
|
|
307
|
-
`
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
363
|
}
|
|
312
|
-
function
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const fullPath = path.join(dir, entry.name);
|
|
318
|
-
if (entry.isDirectory() && entry.name !== "node_modules") {
|
|
319
|
-
results.push(...findGoldenFiles(fullPath));
|
|
320
|
-
} else if (entry.isFile() && entry.name.endsWith(".golden")) {
|
|
321
|
-
results.push(fullPath);
|
|
322
|
-
}
|
|
364
|
+
function scaffoldProject(targetDir, options = {}) {
|
|
365
|
+
write(path2.join(targetDir, "tests", "my-agent.test.ts"), SAMPLE_TEST_TEMPLATE);
|
|
366
|
+
write(path2.join(targetDir, "attest.policy.yaml"), POLICY_TEMPLATE);
|
|
367
|
+
if (options.ci === "github") {
|
|
368
|
+
write(path2.join(targetDir, ".github", "workflows", "attest.yml"), GITHUB_WORKFLOW_TEMPLATE);
|
|
323
369
|
}
|
|
324
|
-
|
|
370
|
+
process.stdout.write(
|
|
371
|
+
`
|
|
372
|
+
Next: run \`npx vitest run\`. Then change the line marked ${DEMO_MARKER} in tests/my-agent.test.ts and run it again to see a failure.
|
|
373
|
+
`
|
|
374
|
+
);
|
|
325
375
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
376
|
+
|
|
377
|
+
// src/cli.ts
|
|
378
|
+
var HELP_FLAGS = /* @__PURE__ */ new Set(["-h", "--help"]);
|
|
379
|
+
var SDK_USAGE = "usage: attest <command> [args]\n\nSDK commands:\n init [--ci github] scaffold a sample test and merge policy here\n (--ci github also writes .github/workflows/attest.yml)\n\noptions:\n --version print the SDK version and the engine version it requires\n --help print this help\n";
|
|
380
|
+
var INIT_USAGE = "usage: attest init [--ci github] (scaffolds into the current directory)";
|
|
381
|
+
function cmdInit(args) {
|
|
382
|
+
if (args.some((a) => HELP_FLAGS.has(a))) {
|
|
383
|
+
process.stdout.write(`${INIT_USAGE}
|
|
330
384
|
`);
|
|
331
|
-
|
|
385
|
+
return 0;
|
|
332
386
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
process.exit(0);
|
|
337
|
-
} else if (args[1] === "clear") {
|
|
338
|
-
cmdCacheClear();
|
|
339
|
-
process.exit(0);
|
|
340
|
-
} else {
|
|
341
|
-
process.stderr.write(`Unknown cache subcommand: ${args[1]}
|
|
387
|
+
const joined = args.join(" ");
|
|
388
|
+
if (args.length > 0 && joined !== "--ci github" && joined !== "--ci=github") {
|
|
389
|
+
process.stderr.write(`${INIT_USAGE}
|
|
342
390
|
`);
|
|
343
|
-
|
|
344
|
-
process.exit(1);
|
|
345
|
-
}
|
|
391
|
+
return 2;
|
|
346
392
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
process.exit(0);
|
|
350
|
-
}
|
|
351
|
-
if (args[0] === "validate") {
|
|
352
|
-
cmdValidate(process.cwd());
|
|
353
|
-
process.exit(0);
|
|
354
|
-
}
|
|
355
|
-
if (args[0] === "calibrate") {
|
|
356
|
-
cmdCalibrate(args.slice(1));
|
|
357
|
-
process.exit(0);
|
|
358
|
-
}
|
|
359
|
-
process.stderr.write(
|
|
360
|
-
"Usage: attest <command>\n\nCommands:\n --version Print version\n init Scaffold a test project\n validate Validate test suite\n cache stats Show cache statistics\n cache clear Clear cache database\n calibrate Compute judge agreement metrics over labeled data\n"
|
|
361
|
-
);
|
|
362
|
-
process.exit(args.length === 0 ? 0 : 1);
|
|
393
|
+
scaffoldProject(process.cwd(), args.length > 0 ? { ci: "github" } : {});
|
|
394
|
+
return 0;
|
|
363
395
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
} else if (arg === "--rubric-version") {
|
|
383
|
-
if (next === void 0) throw new Error("--rubric-version requires a value");
|
|
384
|
-
opts.rubricVersion = next;
|
|
385
|
-
i++;
|
|
386
|
-
} else if (arg === "--threshold") {
|
|
387
|
-
if (next === void 0) throw new Error("--threshold requires a number");
|
|
388
|
-
const parsed = Number(next);
|
|
389
|
-
if (next.trim() === "" || Number.isNaN(parsed)) {
|
|
390
|
-
throw new Error(`--threshold value ${JSON.stringify(next)} is not a number`);
|
|
391
|
-
}
|
|
392
|
-
opts.threshold = parsed;
|
|
393
|
-
i++;
|
|
394
|
-
} else {
|
|
395
|
-
throw new Error(`unknown calibrate flag: ${arg}`);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
if (!opts.labels) {
|
|
399
|
-
throw new Error("--labels is required");
|
|
396
|
+
var HELP_PROBE_TIMEOUT_MS = 1e4;
|
|
397
|
+
function errorMessage(err) {
|
|
398
|
+
return err instanceof Error ? err.message : String(err);
|
|
399
|
+
}
|
|
400
|
+
function engineCommands(engine, timeoutMs = HELP_PROBE_TIMEOUT_MS) {
|
|
401
|
+
const proc = spawnSync2(engine, ["help"], {
|
|
402
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
403
|
+
encoding: "utf-8",
|
|
404
|
+
timeout: timeoutMs
|
|
405
|
+
});
|
|
406
|
+
if (proc.error || proc.status !== 0) return null;
|
|
407
|
+
const lines = proc.stdout.split("\n");
|
|
408
|
+
const start = lines.indexOf("commands:");
|
|
409
|
+
if (start === -1) return null;
|
|
410
|
+
const commands = /* @__PURE__ */ new Map();
|
|
411
|
+
for (const line of lines.slice(start + 1)) {
|
|
412
|
+
const match = /^\s*(\S+)\s*(.*)$/.exec(line);
|
|
413
|
+
if (match?.[1]) commands.set(match[1], (match[2] ?? "").trim());
|
|
400
414
|
}
|
|
401
|
-
return
|
|
415
|
+
return commands;
|
|
402
416
|
}
|
|
403
|
-
function
|
|
404
|
-
|
|
417
|
+
async function printHelp() {
|
|
418
|
+
process.stdout.write(`${SDK_USAGE}
|
|
419
|
+
`);
|
|
420
|
+
let engine;
|
|
405
421
|
try {
|
|
406
|
-
|
|
422
|
+
engine = await resolveEngine(void 0, { allowDownload: false });
|
|
407
423
|
} catch (err) {
|
|
408
|
-
|
|
424
|
+
const firstLine = (errorMessage(err).split("\n")[0] ?? "").replace(/\.$/, "");
|
|
425
|
+
process.stdout.write(
|
|
426
|
+
`engine commands: not available (${firstLine}). Run \`attest doctor\` to download attest-engine ${ENGINE_VERSION}, or set ATTEST_ENGINE_PATH.
|
|
427
|
+
`
|
|
428
|
+
);
|
|
429
|
+
return 0;
|
|
430
|
+
}
|
|
431
|
+
const commands = engineCommands(engine);
|
|
432
|
+
if (commands === null) {
|
|
433
|
+
process.stdout.write(
|
|
434
|
+
`engine commands: not available (attest-engine at ${engine} lists no commands; it predates the command registry).
|
|
435
|
+
`
|
|
436
|
+
);
|
|
437
|
+
return 0;
|
|
438
|
+
}
|
|
439
|
+
const width = Math.max(0, ...[...commands.keys()].map((name) => name.length));
|
|
440
|
+
process.stdout.write(`engine commands (attest-engine ${ENGINE_VERSION}, ${engine}):
|
|
441
|
+
`);
|
|
442
|
+
for (const [name, summary] of commands) {
|
|
443
|
+
process.stdout.write(` ${name.padEnd(width)} ${summary}
|
|
409
444
|
`);
|
|
410
|
-
process.exit(2);
|
|
411
|
-
return;
|
|
412
445
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
446
|
+
return 0;
|
|
447
|
+
}
|
|
448
|
+
function exitStatus(status, signal) {
|
|
449
|
+
if (status !== null) return status;
|
|
450
|
+
if (signal !== null) return 128 + (os2.constants.signals[signal] ?? 0);
|
|
451
|
+
return 1;
|
|
452
|
+
}
|
|
453
|
+
async function forward(args) {
|
|
454
|
+
let engine;
|
|
416
455
|
try {
|
|
417
|
-
|
|
418
|
-
records = loadLabelsJSONL(text);
|
|
419
|
-
} else if (ext === ".csv") {
|
|
420
|
-
records = loadLabelsCSV(text);
|
|
421
|
-
} else if (text.trimStart().startsWith("{")) {
|
|
422
|
-
records = loadLabelsJSONL(text);
|
|
423
|
-
} else {
|
|
424
|
-
records = loadLabelsCSV(text);
|
|
425
|
-
}
|
|
456
|
+
engine = await resolveEngine();
|
|
426
457
|
} catch (err) {
|
|
427
|
-
process.stderr.write(`
|
|
458
|
+
process.stderr.write(`attest: ${errorMessage(err)}
|
|
428
459
|
`);
|
|
429
|
-
|
|
430
|
-
return;
|
|
460
|
+
return 1;
|
|
431
461
|
}
|
|
432
|
-
const
|
|
433
|
-
const
|
|
434
|
-
if (
|
|
462
|
+
const command = args[0] ?? "";
|
|
463
|
+
const commands = engineCommands(engine);
|
|
464
|
+
if (commands === null || command !== "help" && !commands.has(command)) {
|
|
435
465
|
process.stderr.write(
|
|
436
|
-
`
|
|
466
|
+
`attest: attest-engine at ${engine} (version ${ENGINE_VERSION}) predates \`attest ${command}\`: its help does not list that command. Set ATTEST_ENGINE_PATH to an engine built from this release (make engine), or install a release whose engine provides it.
|
|
437
467
|
`
|
|
438
468
|
);
|
|
439
|
-
|
|
440
|
-
return;
|
|
469
|
+
return 1;
|
|
441
470
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
471
|
+
const proc = spawnSync2(engine, args, { stdio: "inherit" });
|
|
472
|
+
if (proc.error) throw proc.error;
|
|
473
|
+
return exitStatus(proc.status, proc.signal);
|
|
474
|
+
}
|
|
475
|
+
async function main(argv = process.argv.slice(2)) {
|
|
476
|
+
const [command] = argv;
|
|
477
|
+
if (command === void 0 || HELP_FLAGS.has(command) || command === "help" && argv.length === 1) {
|
|
478
|
+
return printHelp();
|
|
479
|
+
}
|
|
480
|
+
if (command === "--version") {
|
|
481
|
+
process.stdout.write(`attest ${VERSION} (requires attest-engine ${ENGINE_VERSION})
|
|
447
482
|
`);
|
|
448
|
-
|
|
449
|
-
return;
|
|
483
|
+
return 0;
|
|
450
484
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
process.stdout.write(JSON.stringify(output, null, 2) + "\n");
|
|
485
|
+
if (command === "init") {
|
|
486
|
+
return cmdInit(argv.slice(1));
|
|
487
|
+
}
|
|
488
|
+
if (command.startsWith("-")) {
|
|
489
|
+
process.stderr.write(`attest: unknown option ${command}
|
|
490
|
+
|
|
491
|
+
${SDK_USAGE}`);
|
|
492
|
+
return 2;
|
|
493
|
+
}
|
|
494
|
+
return forward(argv);
|
|
462
495
|
}
|
|
463
|
-
function
|
|
464
|
-
|
|
496
|
+
function isDirectExecution() {
|
|
497
|
+
const script = process.argv[1];
|
|
498
|
+
if (script === void 0 || !fs3.existsSync(script)) return false;
|
|
499
|
+
return fs3.realpathSync(script) === fs3.realpathSync(fileURLToPath(import.meta.url));
|
|
465
500
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
501
|
+
if (isDirectExecution()) {
|
|
502
|
+
main().then(
|
|
503
|
+
(code) => process.exit(code),
|
|
504
|
+
(err) => {
|
|
505
|
+
process.stderr.write(`attest: ${errorMessage(err)}
|
|
506
|
+
`);
|
|
507
|
+
process.exit(1);
|
|
508
|
+
}
|
|
509
|
+
);
|
|
469
510
|
}
|
|
470
511
|
export {
|
|
512
|
+
engineCommands,
|
|
513
|
+
exitStatus,
|
|
471
514
|
main
|
|
472
515
|
};
|
|
473
516
|
//# sourceMappingURL=cli.js.map
|