@attest-ai/core 1.0.0-rc.2 → 1.0.0-rc.4
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 +1002 -120
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +315 -6
- package/dist/cjs/proto/index.cjs +7 -0
- package/dist/cjs/proto/index.cjs.map +1 -1
- package/dist/cjs/proto/index.d.cts +31 -2
- package/dist/esm/cli.d.ts +8 -1
- package/dist/esm/cli.js +536 -47
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/index.d.ts +315 -6
- package/dist/esm/index.js +978 -120
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/proto/index.d.ts +31 -2
- package/dist/esm/proto/index.js +5 -0
- package/dist/esm/proto/index.js.map +1 -1
- package/package.json +3 -3
package/dist/esm/cli.js
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
5
|
-
import * as
|
|
5
|
+
import * as fs4 from "fs";
|
|
6
6
|
import * as os2 from "os";
|
|
7
|
-
import
|
|
7
|
+
import * as path4 from "path";
|
|
8
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8
9
|
|
|
9
10
|
// src/version.ts
|
|
10
|
-
var VERSION = "1.0.0-rc.
|
|
11
|
-
var ENGINE_VERSION = "1.0.0-rc.
|
|
11
|
+
var VERSION = "1.0.0-rc.4";
|
|
12
|
+
var ENGINE_VERSION = "1.0.0-rc.4";
|
|
12
13
|
|
|
13
14
|
// src/engine-manager.ts
|
|
14
15
|
import { spawn, spawnSync } from "child_process";
|
|
@@ -76,7 +77,7 @@ function httpsGet(url, redirectCount = 0) {
|
|
|
76
77
|
new Error(`Too many redirects (>${MAX_REDIRECTS}) following ${url}`)
|
|
77
78
|
);
|
|
78
79
|
}
|
|
79
|
-
return new Promise((
|
|
80
|
+
return new Promise((resolve2, reject) => {
|
|
80
81
|
const client = url.startsWith("https:") ? https : http;
|
|
81
82
|
client.get(url, (res) => {
|
|
82
83
|
const status = res.statusCode ?? 0;
|
|
@@ -87,7 +88,7 @@ function httpsGet(url, redirectCount = 0) {
|
|
|
87
88
|
return;
|
|
88
89
|
}
|
|
89
90
|
res.resume();
|
|
90
|
-
|
|
91
|
+
resolve2(httpsGet(location, redirectCount + 1));
|
|
91
92
|
return;
|
|
92
93
|
}
|
|
93
94
|
if (status < 200 || status >= 300) {
|
|
@@ -97,7 +98,7 @@ function httpsGet(url, redirectCount = 0) {
|
|
|
97
98
|
}
|
|
98
99
|
const chunks = [];
|
|
99
100
|
res.on("data", (chunk) => chunks.push(chunk));
|
|
100
|
-
res.on("end", () =>
|
|
101
|
+
res.on("end", () => resolve2(Buffer.concat(chunks)));
|
|
101
102
|
res.on("error", reject);
|
|
102
103
|
}).on("error", reject);
|
|
103
104
|
});
|
|
@@ -228,8 +229,8 @@ Install options:
|
|
|
228
229
|
}
|
|
229
230
|
return [await downloadEngine(), "download"];
|
|
230
231
|
}
|
|
231
|
-
async function resolveEngine(explicitPath,
|
|
232
|
-
const [enginePath, source] = await locateEngine(explicitPath,
|
|
232
|
+
async function resolveEngine(explicitPath, options2 = {}) {
|
|
233
|
+
const [enginePath, source] = await locateEngine(explicitPath, options2.allowDownload ?? true);
|
|
233
234
|
const found = engineVersion(enginePath);
|
|
234
235
|
if (found !== ENGINE_VERSION) {
|
|
235
236
|
throw new EngineVersionMismatchError(enginePath, source, found, ENGINE_VERSION);
|
|
@@ -288,8 +289,8 @@ not_evaluated: warn
|
|
|
288
289
|
`;
|
|
289
290
|
var GITHUB_WORKFLOW_TEMPLATE = `# Written by \`attest init --ci github\`.
|
|
290
291
|
# The test step records its outcome and continues so the report is always uploaded,
|
|
291
|
-
# the policy evaluated and the pull request
|
|
292
|
-
# when the tests failed. A policy warning (exit 2) does not fail the job.
|
|
292
|
+
# the JUnit file written, the policy evaluated and the pull request summarised; the last
|
|
293
|
+
# step fails the job when the tests failed. A policy warning (exit 2) does not fail the job.
|
|
293
294
|
name: attest
|
|
294
295
|
|
|
295
296
|
on:
|
|
@@ -299,11 +300,13 @@ on:
|
|
|
299
300
|
|
|
300
301
|
permissions:
|
|
301
302
|
contents: read
|
|
302
|
-
pull-requests: write
|
|
303
303
|
|
|
304
304
|
jobs:
|
|
305
305
|
attest:
|
|
306
306
|
runs-on: ubuntu-latest
|
|
307
|
+
permissions:
|
|
308
|
+
contents: read
|
|
309
|
+
pull-requests: write
|
|
307
310
|
steps:
|
|
308
311
|
- uses: actions/checkout@v6
|
|
309
312
|
with:
|
|
@@ -317,12 +320,17 @@ jobs:
|
|
|
317
320
|
id: tests
|
|
318
321
|
continue-on-error: true
|
|
319
322
|
run: npx vitest run ${REPORT_ARGS}
|
|
323
|
+
- name: Write the JUnit report
|
|
324
|
+
if: always()
|
|
325
|
+
run: npx attest report junit --report attest-report.json --out attest-junit.xml
|
|
320
326
|
- name: Upload the Attest report
|
|
321
327
|
if: always()
|
|
322
328
|
uses: actions/upload-artifact@v7
|
|
323
329
|
with:
|
|
324
330
|
name: attest-report
|
|
325
|
-
path:
|
|
331
|
+
path: |
|
|
332
|
+
attest-report.json
|
|
333
|
+
attest-junit.xml
|
|
326
334
|
if-no-files-found: error
|
|
327
335
|
- name: Evaluate the merge policy
|
|
328
336
|
if: always()
|
|
@@ -336,8 +344,9 @@ jobs:
|
|
|
336
344
|
fi
|
|
337
345
|
exit "$code"
|
|
338
346
|
- name: Annotate the pull request
|
|
339
|
-
#
|
|
340
|
-
#
|
|
347
|
+
# Posts one summary comment (updated on every run) and annotates failures at their
|
|
348
|
+
# file and line. Fork pull requests get a read-only token that cannot comment; the
|
|
349
|
+
# last step still fails the job when tests failed.
|
|
341
350
|
if: >-
|
|
342
351
|
always() && github.event_name == 'pull_request' &&
|
|
343
352
|
github.event.pull_request.head.repo.full_name == github.repository
|
|
@@ -350,49 +359,264 @@ jobs:
|
|
|
350
359
|
echo "::error::vitest failed; see the Run tests step"
|
|
351
360
|
exit 1
|
|
352
361
|
`;
|
|
353
|
-
function write(
|
|
362
|
+
function write(targetDir, rel, content, result, quiet) {
|
|
363
|
+
const file = path2.join(targetDir, ...rel.split("/"));
|
|
354
364
|
if (fs2.existsSync(file)) {
|
|
355
|
-
|
|
365
|
+
result.skipped.push(rel);
|
|
366
|
+
if (!quiet) process.stderr.write(`Skipped (exists): ${file}
|
|
356
367
|
`);
|
|
357
368
|
return;
|
|
358
369
|
}
|
|
359
370
|
fs2.mkdirSync(path2.dirname(file), { recursive: true });
|
|
360
371
|
fs2.writeFileSync(file, content);
|
|
361
|
-
|
|
372
|
+
result.created.push(rel);
|
|
373
|
+
if (!quiet) process.stdout.write(`Created: ${file}
|
|
362
374
|
`);
|
|
363
375
|
}
|
|
364
|
-
function scaffoldProject(targetDir,
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
376
|
+
function scaffoldProject(targetDir, options2 = {}) {
|
|
377
|
+
const quiet = options2.quiet ?? false;
|
|
378
|
+
const result = { created: [], skipped: [] };
|
|
379
|
+
write(targetDir, "tests/my-agent.test.ts", SAMPLE_TEST_TEMPLATE, result, quiet);
|
|
380
|
+
write(targetDir, "attest.policy.yaml", POLICY_TEMPLATE, result, quiet);
|
|
381
|
+
if (options2.ci === "github") {
|
|
382
|
+
write(targetDir, ".github/workflows/attest.yml", GITHUB_WORKFLOW_TEMPLATE, result, quiet);
|
|
369
383
|
}
|
|
370
|
-
|
|
371
|
-
|
|
384
|
+
if (!quiet) {
|
|
385
|
+
process.stdout.write(
|
|
386
|
+
`
|
|
372
387
|
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
388
|
`
|
|
374
|
-
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
return result;
|
|
375
392
|
}
|
|
376
393
|
|
|
377
|
-
// src/
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
394
|
+
// src/capture.ts
|
|
395
|
+
import { spawn as spawn2 } from "child_process";
|
|
396
|
+
import * as fs3 from "fs";
|
|
397
|
+
import * as path3 from "path";
|
|
398
|
+
import { fileURLToPath } from "url";
|
|
399
|
+
var FIXTURE_VERSION = 1;
|
|
400
|
+
var DEFAULT_FIXTURE_DIR = path3.join("tests", "attest", "fixtures");
|
|
401
|
+
var CaptureError = class extends Error {
|
|
402
|
+
/** The fixture written, when the engine wrote one. */
|
|
403
|
+
fixture;
|
|
404
|
+
constructor(message, fixture = null) {
|
|
405
|
+
super(message);
|
|
406
|
+
this.name = "CaptureError";
|
|
407
|
+
this.fixture = fixture;
|
|
386
408
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
409
|
+
};
|
|
410
|
+
function toPath(p) {
|
|
411
|
+
return p instanceof URL ? fileURLToPath(p) : p;
|
|
412
|
+
}
|
|
413
|
+
function loadFixture(file) {
|
|
414
|
+
const p = toPath(file);
|
|
415
|
+
const doc = JSON.parse(fs3.readFileSync(p, "utf8"));
|
|
416
|
+
if (!isObject(doc) || doc["fixture_version"] !== FIXTURE_VERSION) {
|
|
417
|
+
throw new CaptureError(`${p}: not a fixture_version ${FIXTURE_VERSION} capture fixture`);
|
|
392
418
|
}
|
|
393
|
-
|
|
394
|
-
return
|
|
419
|
+
validate(p, doc);
|
|
420
|
+
return doc;
|
|
421
|
+
}
|
|
422
|
+
var NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
423
|
+
function isObject(v) {
|
|
424
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
425
|
+
}
|
|
426
|
+
function kindOf(v) {
|
|
427
|
+
if (Array.isArray(v)) return "array";
|
|
428
|
+
if (isObject(v)) return "object";
|
|
429
|
+
if (typeof v === "string" || typeof v === "boolean") return typeof v;
|
|
430
|
+
return "other";
|
|
431
|
+
}
|
|
432
|
+
function validate(p, doc) {
|
|
433
|
+
const need = (where, obj, fields) => {
|
|
434
|
+
if (!isObject(obj)) throw new CaptureError(`${p}: ${where} is not an object`);
|
|
435
|
+
for (const [key, kind] of Object.entries(fields)) {
|
|
436
|
+
if (kindOf(obj[key]) !== kind) throw new CaptureError(`${p}: ${where}.${key} is missing or not a ${kind}`);
|
|
437
|
+
}
|
|
438
|
+
return obj;
|
|
439
|
+
};
|
|
440
|
+
need("fixture", doc, { name: "string", source: "object", trace: "object", exchanges: "array", contracts: "array", verification: "object" });
|
|
441
|
+
if (!("world" in doc)) throw new CaptureError(`${p}: fixture.world is missing`);
|
|
442
|
+
if (!NAME_PATTERN.test(doc["name"])) {
|
|
443
|
+
throw new CaptureError(`${p}: fixture name ${JSON.stringify(doc["name"])} is not a valid fixture name`);
|
|
444
|
+
}
|
|
445
|
+
const v = need("fixture.verification", doc["verification"], {
|
|
446
|
+
verified: "boolean",
|
|
447
|
+
expect_fail: "boolean",
|
|
448
|
+
results: "array",
|
|
449
|
+
expected_failures: "array"
|
|
450
|
+
});
|
|
451
|
+
doc["contracts"].forEach(
|
|
452
|
+
(c, i) => need(`fixture.contracts[${i}]`, c, { id: "string", kind: "string", description: "string", type: "string", spec: "object" })
|
|
453
|
+
);
|
|
454
|
+
v["results"].forEach(
|
|
455
|
+
(r, i) => need(`fixture.verification.results[${i}]`, r, { contract: "string", status: "string", explanation: "string" })
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
function objectKey(k) {
|
|
459
|
+
if (k === "__proto__") return `[${JSON.stringify(k)}]`;
|
|
460
|
+
return /^[A-Za-z_$][\w$]*$/.test(k) ? k : JSON.stringify(k);
|
|
461
|
+
}
|
|
462
|
+
function comment(text) {
|
|
463
|
+
return text.replace(/[\p{Cc}\p{Zl}\p{Zp}]/gu, " ");
|
|
464
|
+
}
|
|
465
|
+
function ts(value) {
|
|
466
|
+
if (value === null || typeof value === "boolean" || typeof value === "number") {
|
|
467
|
+
return JSON.stringify(value);
|
|
468
|
+
}
|
|
469
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
470
|
+
if (Array.isArray(value)) return `[${value.map(ts).join(", ")}]`;
|
|
471
|
+
if (typeof value === "object") {
|
|
472
|
+
const entries = Object.entries(value).map(
|
|
473
|
+
([k, v]) => `${objectKey(k)}: ${ts(v)}`
|
|
474
|
+
);
|
|
475
|
+
return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
|
|
476
|
+
}
|
|
477
|
+
throw new Error(`value ${String(value)} has no literal`);
|
|
478
|
+
}
|
|
479
|
+
function str(spec, key) {
|
|
480
|
+
const v = spec[key];
|
|
481
|
+
if (typeof v !== "string") throw new Error(`missing ${key}`);
|
|
482
|
+
return v;
|
|
483
|
+
}
|
|
484
|
+
function options(entries) {
|
|
485
|
+
const set = entries.filter(([, v]) => v !== void 0 && v !== null && v !== false);
|
|
486
|
+
return set.length === 0 ? "" : `{ ${set.map(([k, v]) => `${k}: ${ts(v)}`).join(", ")} }`;
|
|
487
|
+
}
|
|
488
|
+
function call(method, args, opts) {
|
|
489
|
+
return `.${method}(${[...args, ...opts ? [opts] : []].join(", ")})`;
|
|
395
490
|
}
|
|
491
|
+
function dslCall(contract) {
|
|
492
|
+
const s = contract.spec;
|
|
493
|
+
const soft = s["soft"] === true ? true : void 0;
|
|
494
|
+
try {
|
|
495
|
+
if (contract.type === "trace") {
|
|
496
|
+
const check = s["check"];
|
|
497
|
+
if (check === "trajectory" && s["mode"] !== "partial_order") {
|
|
498
|
+
const mode = s["mode"] === "strict" || s["mode"] === void 0 ? void 0 : s["mode"];
|
|
499
|
+
if (!Array.isArray(s["tools"])) throw new Error("missing tools");
|
|
500
|
+
return call("trajectory", [ts(s["tools"])], options([["mode", mode], ["soft", soft]]));
|
|
501
|
+
}
|
|
502
|
+
if (check === "tool_order") {
|
|
503
|
+
return call("toolCalledBefore", [ts(str(s, "before")), ts(str(s, "after"))], options([["soft", soft]]));
|
|
504
|
+
}
|
|
505
|
+
if (check === "tool_cardinality") {
|
|
506
|
+
return call(
|
|
507
|
+
"toolCardinality",
|
|
508
|
+
[ts(str(s, "tool"))],
|
|
509
|
+
options([
|
|
510
|
+
["per", s["per"]],
|
|
511
|
+
["atLeast", s["min"]],
|
|
512
|
+
["atMost", s["max"]],
|
|
513
|
+
["soft", soft]
|
|
514
|
+
])
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (contract.type === "schema" && s["target"] === "output.structured") {
|
|
519
|
+
if (typeof s["schema"] !== "object" || s["schema"] === null) throw new Error("missing schema");
|
|
520
|
+
return call("outputMatchesSchema", [ts(s["schema"])], "");
|
|
521
|
+
}
|
|
522
|
+
if (contract.type === "constraint" && s["field"] === "metadata.cost_usd" && s["operator"] === "lte") {
|
|
523
|
+
if (typeof s["value"] !== "number") throw new Error("missing value");
|
|
524
|
+
return call("costUnder", [ts(s["value"])], options([["soft", soft]]));
|
|
525
|
+
}
|
|
526
|
+
} catch (err) {
|
|
527
|
+
throw new Error(`contract ${contract.id}: malformed spec (${err.message})`);
|
|
528
|
+
}
|
|
529
|
+
throw new Error(`contract ${contract.id}: no TypeScript DSL call for ${contract.type} spec`);
|
|
530
|
+
}
|
|
531
|
+
function testPathFor(fixturePath) {
|
|
532
|
+
const name = path3.basename(fixturePath).replace(/\.attest\.json$/, "");
|
|
533
|
+
return path3.join(path3.dirname(path3.dirname(fixturePath)), `${name}.test.ts`);
|
|
534
|
+
}
|
|
535
|
+
function renderVitest(fixture, fixturePath, testPath) {
|
|
536
|
+
const calls = [];
|
|
537
|
+
const skipped = [];
|
|
538
|
+
for (const c of fixture.contracts) {
|
|
539
|
+
try {
|
|
540
|
+
calls.push([c, dslCall(c)]);
|
|
541
|
+
} catch (err) {
|
|
542
|
+
skipped.push(err.message);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const rel = path3.relative(path3.dirname(testPath), fixturePath).split(path3.sep).join("/");
|
|
546
|
+
const status = new Map(fixture.verification.results.map((r) => [r.contract, r.status]));
|
|
547
|
+
const expectFail = fixture.verification.expect_fail;
|
|
548
|
+
const lines = [
|
|
549
|
+
`// Contracts drafted by \`attest capture\` from fixture ${comment(ts(fixture.name))}.`,
|
|
550
|
+
"//",
|
|
551
|
+
"// Review and edit before committing: they check the captured trace without running",
|
|
552
|
+
"// the agent. Re-capturing does not overwrite this file.",
|
|
553
|
+
`import { attestExpect, loadFixture, type ExpectChain } from "@attest-ai/core";`,
|
|
554
|
+
`import { ${expectFail ? "inspect" : "evaluate"} } from "@attest-ai/vitest";`,
|
|
555
|
+
`import { describe, ${expectFail ? "expect, " : ""}it } from "vitest";`,
|
|
556
|
+
"",
|
|
557
|
+
`const FIXTURE = loadFixture(new URL(${ts(rel.startsWith(".") ? rel : `./${rel}`)}, import.meta.url));`
|
|
558
|
+
];
|
|
559
|
+
if (skipped.length > 0) {
|
|
560
|
+
lines.push("", "// Drafted contracts without a TypeScript DSL call (left out):");
|
|
561
|
+
for (const s of skipped) lines.push(`// - ${comment(s)}`);
|
|
562
|
+
}
|
|
563
|
+
lines.push("", "function contracts(): ExpectChain {", " return attestExpect(FIXTURE.trace)");
|
|
564
|
+
for (const [c, dsl] of calls) {
|
|
565
|
+
lines.push(` // ${comment(`${c.id}: ${c.description}`)}`, ` ${dsl}`);
|
|
566
|
+
}
|
|
567
|
+
lines[lines.length - 1] += ";";
|
|
568
|
+
lines.push("}");
|
|
569
|
+
if (expectFail) {
|
|
570
|
+
lines.push("", "// Status of each contract on the captured incident, in contracts() order.");
|
|
571
|
+
lines.push("const EXPECTED: Array<[string, string]> = [");
|
|
572
|
+
for (const [c] of calls) lines.push(` [${ts(c.id)}, ${ts(status.get(c.id) ?? "pass")}],`);
|
|
573
|
+
lines.push(
|
|
574
|
+
"];",
|
|
575
|
+
"",
|
|
576
|
+
`describe(${ts(fixture.name)}, () => {`,
|
|
577
|
+
` it("reproduces the incident", async () => {`,
|
|
578
|
+
" const result = await inspect(contracts());",
|
|
579
|
+
"",
|
|
580
|
+
" expect(result.assertionResults.map((row) => row.status)).toEqual(EXPECTED.map(([, s]) => s));",
|
|
581
|
+
" });",
|
|
582
|
+
"});"
|
|
583
|
+
);
|
|
584
|
+
} else {
|
|
585
|
+
lines.push(
|
|
586
|
+
"",
|
|
587
|
+
`describe(${ts(fixture.name)}, () => {`,
|
|
588
|
+
` it("keeps its drafted contracts", async () => {`,
|
|
589
|
+
" await evaluate(contracts());",
|
|
590
|
+
" });",
|
|
591
|
+
"});"
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
return `${lines.join("\n")}
|
|
595
|
+
`;
|
|
596
|
+
}
|
|
597
|
+
function emitVitest(fixturePath) {
|
|
598
|
+
const testPath = testPathFor(fixturePath);
|
|
599
|
+
const existing = fs3.lstatSync(testPath, { throwIfNoEntry: false });
|
|
600
|
+
if (existing?.isSymbolicLink()) {
|
|
601
|
+
throw new CaptureError(`${testPath} is a symbolic link; refusing to draft a test through it`);
|
|
602
|
+
}
|
|
603
|
+
if (existing !== void 0) return null;
|
|
604
|
+
const source = renderVitest(loadFixture(fixturePath), fixturePath, testPath);
|
|
605
|
+
fs3.mkdirSync(path3.dirname(testPath), { recursive: true });
|
|
606
|
+
try {
|
|
607
|
+
fs3.writeFileSync(testPath, source, { encoding: "utf8", flag: "wx" });
|
|
608
|
+
} catch (err) {
|
|
609
|
+
if (err.code === "EEXIST") {
|
|
610
|
+
throw new CaptureError(`${testPath} appeared while drafting; left unchanged`);
|
|
611
|
+
}
|
|
612
|
+
throw err;
|
|
613
|
+
}
|
|
614
|
+
return testPath;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// src/cli.ts
|
|
618
|
+
var HELP_FLAGS = /* @__PURE__ */ new Set(["-h", "--help"]);
|
|
619
|
+
var SDK_USAGE = "usage: attest <command> [args]\n\nSDK commands:\n init [--ci github] [--skills] [--mcp] [--json]\n scaffold a sample test and merge policy here\n (--ci github also writes .github/workflows/attest.yml;\n --skills installs the agent skills with attest-engine;\n --mcp writes the MCP client configs (.mcp.json, .cursor/mcp.json,\n .codex/config.toml) with attest-engine;\n --json prints {created, updated, skipped} paths as JSON)\n\n`attest capture` (engine) also drafts tests/attest/<name>.test.ts from a verified\nfixture, unless that file exists.\n\noptions:\n --version print the SDK version and the engine version it requires\n --help print this help\n";
|
|
396
620
|
var HELP_PROBE_TIMEOUT_MS = 1e4;
|
|
397
621
|
function errorMessage(err) {
|
|
398
622
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -450,28 +674,291 @@ function exitStatus(status, signal) {
|
|
|
450
674
|
if (signal !== null) return 128 + (os2.constants.signals[signal] ?? 0);
|
|
451
675
|
return 1;
|
|
452
676
|
}
|
|
453
|
-
async function
|
|
677
|
+
async function engineProviding(command) {
|
|
454
678
|
let engine;
|
|
455
679
|
try {
|
|
456
680
|
engine = await resolveEngine();
|
|
457
681
|
} catch (err) {
|
|
458
682
|
process.stderr.write(`attest: ${errorMessage(err)}
|
|
459
683
|
`);
|
|
460
|
-
return
|
|
684
|
+
return null;
|
|
461
685
|
}
|
|
462
|
-
const command = args[0] ?? "";
|
|
463
686
|
const commands = engineCommands(engine);
|
|
464
687
|
if (commands === null || command !== "help" && !commands.has(command)) {
|
|
465
688
|
process.stderr.write(
|
|
466
689
|
`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.
|
|
467
690
|
`
|
|
468
691
|
);
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
return engine;
|
|
695
|
+
}
|
|
696
|
+
var INIT_HELP_ENTRY = {
|
|
697
|
+
name: "init",
|
|
698
|
+
summary: "scaffold a sample test and policy; --ci github, --skills, --mcp, --json",
|
|
699
|
+
schema: "cli/init.schema.json",
|
|
700
|
+
exit_codes: [
|
|
701
|
+
{ code: 0, meaning: "success" },
|
|
702
|
+
{
|
|
703
|
+
code: 1,
|
|
704
|
+
meaning: "--skills or --mcp could not install: the engine is missing, too old or failed"
|
|
705
|
+
},
|
|
706
|
+
{ code: 2, meaning: "usage error" }
|
|
707
|
+
]
|
|
708
|
+
};
|
|
709
|
+
function helpJson(engine) {
|
|
710
|
+
const proc = spawnSync2(engine, ["help", "--json"], {
|
|
711
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
712
|
+
encoding: "utf-8"
|
|
713
|
+
});
|
|
714
|
+
if (proc.error) throw proc.error;
|
|
715
|
+
if (proc.status !== 0) {
|
|
716
|
+
process.stderr.write(proc.stderr);
|
|
717
|
+
return exitStatus(proc.status, proc.signal);
|
|
718
|
+
}
|
|
719
|
+
let doc;
|
|
720
|
+
try {
|
|
721
|
+
doc = JSON.parse(proc.stdout);
|
|
722
|
+
if (!Array.isArray(doc.commands)) throw new Error("no commands array");
|
|
723
|
+
} catch (err) {
|
|
724
|
+
process.stderr.write(`attest: \`${engine} help --json\` printed an unexpected document: ${errorMessage(err)}
|
|
725
|
+
`);
|
|
469
726
|
return 1;
|
|
470
727
|
}
|
|
728
|
+
doc.commands = [...doc.commands, INIT_HELP_ENTRY].sort(
|
|
729
|
+
(a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0
|
|
730
|
+
);
|
|
731
|
+
process.stdout.write(`${JSON.stringify(doc, null, 2)}
|
|
732
|
+
`);
|
|
733
|
+
return 0;
|
|
734
|
+
}
|
|
735
|
+
async function forward(args) {
|
|
736
|
+
const command = args[0] ?? "";
|
|
737
|
+
const engine = await engineProviding(command);
|
|
738
|
+
if (engine === null) return 1;
|
|
739
|
+
if (args.length === 2 && command === "help" && args[1] === "--json") return helpJson(engine);
|
|
740
|
+
if (command === "capture" && captureWritesFixture(args.slice(1))) {
|
|
741
|
+
return captureAndEmit(engine, args.slice(1));
|
|
742
|
+
}
|
|
471
743
|
const proc = spawnSync2(engine, args, { stdio: "inherit" });
|
|
472
744
|
if (proc.error) throw proc.error;
|
|
473
745
|
return exitStatus(proc.status, proc.signal);
|
|
474
746
|
}
|
|
747
|
+
var CAPTURE_NO_TEST = /* @__PURE__ */ new Set(["check-redaction", "serve", "h", "help"]);
|
|
748
|
+
function flagName(arg) {
|
|
749
|
+
if (!arg.startsWith("-") || arg === "-" || arg === "--") return null;
|
|
750
|
+
return arg.replace(/^-+/, "").split("=", 1)[0] ?? null;
|
|
751
|
+
}
|
|
752
|
+
function captureWritesFixture(args) {
|
|
753
|
+
for (const a of args) {
|
|
754
|
+
if (a === "--") break;
|
|
755
|
+
const name = flagName(a);
|
|
756
|
+
if (name !== null && CAPTURE_NO_TEST.has(name)) return false;
|
|
757
|
+
}
|
|
758
|
+
return true;
|
|
759
|
+
}
|
|
760
|
+
function userResultFile(args) {
|
|
761
|
+
for (const a of args) {
|
|
762
|
+
if (a === "--") break;
|
|
763
|
+
if (flagName(a) === "result-file") return true;
|
|
764
|
+
}
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
function captureAndEmit(engine, args) {
|
|
768
|
+
if (userResultFile(args)) {
|
|
769
|
+
process.stderr.write(
|
|
770
|
+
"attest: capture --result-file is set by the attest CLI; run attest-engine capture directly to choose it\n"
|
|
771
|
+
);
|
|
772
|
+
return 2;
|
|
773
|
+
}
|
|
774
|
+
const tmp = fs4.mkdtempSync(path4.join(os2.tmpdir(), "attest-capture-"));
|
|
775
|
+
const resultFile = path4.join(tmp, "result.json");
|
|
776
|
+
let result;
|
|
777
|
+
let code;
|
|
778
|
+
try {
|
|
779
|
+
const proc = spawnSync2(engine, ["capture", "--result-file", resultFile, ...args], { stdio: "inherit" });
|
|
780
|
+
if (proc.error) throw proc.error;
|
|
781
|
+
code = exitStatus(proc.status, proc.signal);
|
|
782
|
+
if (!fs4.existsSync(resultFile)) return code;
|
|
783
|
+
result = JSON.parse(fs4.readFileSync(resultFile, "utf8"));
|
|
784
|
+
} finally {
|
|
785
|
+
fs4.rmSync(tmp, { recursive: true, force: true });
|
|
786
|
+
}
|
|
787
|
+
const fixture = result.fixture;
|
|
788
|
+
if (!result.verified) {
|
|
789
|
+
process.stderr.write(` test: none drafted for unverified ${fixture}
|
|
790
|
+
`);
|
|
791
|
+
return code;
|
|
792
|
+
}
|
|
793
|
+
const written = emitVitest(fixture);
|
|
794
|
+
process.stderr.write(
|
|
795
|
+
written === null ? ` test: ${testPathFor(fixture)} exists; left unchanged
|
|
796
|
+
` : ` test: drafted ${written}
|
|
797
|
+
`
|
|
798
|
+
);
|
|
799
|
+
return code;
|
|
800
|
+
}
|
|
801
|
+
var INIT_USAGE = "usage: attest init [--ci github] [--skills] [--mcp] [--json] (scaffolds into the current directory)";
|
|
802
|
+
var INIT_KINDS = ["created", "updated", "skipped"];
|
|
803
|
+
var INIT_KINDS_WITH_CONFLICT = [...INIT_KINDS, "conflict"];
|
|
804
|
+
var SKILLS_KINDS = [
|
|
805
|
+
["created", "created"],
|
|
806
|
+
["updated", "updated"],
|
|
807
|
+
["unchanged", "skipped"]
|
|
808
|
+
];
|
|
809
|
+
var MCP_KINDS = [
|
|
810
|
+
["created", "created"],
|
|
811
|
+
["updated", "updated"],
|
|
812
|
+
["unchanged", "skipped"],
|
|
813
|
+
["conflict", "conflict"]
|
|
814
|
+
];
|
|
815
|
+
function parseInitArgs(args) {
|
|
816
|
+
const parsed = { ci: false, skills: false, mcp: false, json: false };
|
|
817
|
+
for (let i = 0; i < args.length; i++) {
|
|
818
|
+
const arg = args[i];
|
|
819
|
+
if (arg === "--ci" && args[i + 1] === "github" && !parsed.ci) {
|
|
820
|
+
parsed.ci = true;
|
|
821
|
+
i++;
|
|
822
|
+
} else if (arg === "--ci=github" && !parsed.ci) {
|
|
823
|
+
parsed.ci = true;
|
|
824
|
+
} else if (arg === "--skills" && !parsed.skills) {
|
|
825
|
+
parsed.skills = true;
|
|
826
|
+
} else if (arg === "--mcp" && !parsed.mcp) {
|
|
827
|
+
parsed.mcp = true;
|
|
828
|
+
} else if (arg === "--json" && !parsed.json) {
|
|
829
|
+
parsed.json = true;
|
|
830
|
+
} else {
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return parsed;
|
|
835
|
+
}
|
|
836
|
+
function emptyInitResult() {
|
|
837
|
+
return { created: [], updated: [], skipped: [], conflict: [] };
|
|
838
|
+
}
|
|
839
|
+
function runEngineInstall(engine, subcommand, kinds, what) {
|
|
840
|
+
const argv = [...subcommand, "--json"];
|
|
841
|
+
const shown = [engine, ...argv].join(" ");
|
|
842
|
+
const proc = spawnSync2(engine, argv, {
|
|
843
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
844
|
+
encoding: "utf-8"
|
|
845
|
+
});
|
|
846
|
+
if (proc.error) throw proc.error;
|
|
847
|
+
if (proc.status !== 0) {
|
|
848
|
+
const detail = proc.stderr.trim();
|
|
849
|
+
throw new Error(
|
|
850
|
+
`${what} failed (\`${shown}\` exited ${String(exitStatus(proc.status, proc.signal))})` + (detail === "" ? "" : `: ${detail}`)
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
process.stderr.write(proc.stderr);
|
|
854
|
+
let doc;
|
|
855
|
+
try {
|
|
856
|
+
doc = JSON.parse(proc.stdout);
|
|
857
|
+
} catch (err) {
|
|
858
|
+
throw new Error(`\`${shown}\` printed invalid JSON: ${errorMessage(err)}`);
|
|
859
|
+
}
|
|
860
|
+
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
|
|
861
|
+
throw new Error(`\`${shown}\` printed ${JSON.stringify(doc)}, not an object`);
|
|
862
|
+
}
|
|
863
|
+
const record = doc;
|
|
864
|
+
const result = emptyInitResult();
|
|
865
|
+
for (const [engineKind, initKind] of kinds) {
|
|
866
|
+
const paths = record[engineKind];
|
|
867
|
+
if (!Array.isArray(paths) || !paths.every((p) => typeof p === "string")) {
|
|
868
|
+
throw new Error(`\`${shown}\` printed no \`${engineKind}\` list of paths: ${proc.stdout}`);
|
|
869
|
+
}
|
|
870
|
+
result[initKind].push(...paths);
|
|
871
|
+
}
|
|
872
|
+
return result;
|
|
873
|
+
}
|
|
874
|
+
function installSkills(engine, root) {
|
|
875
|
+
return runEngineInstall(engine, ["skills", "install", "--dir", root], SKILLS_KINDS, "skills install");
|
|
876
|
+
}
|
|
877
|
+
function mcpLaunchCommand(root) {
|
|
878
|
+
return fs4.existsSync(path4.join(root, "pnpm-lock.yaml")) ? ["pnpm", "exec", "attest", "mcp"] : ["npx", "--no", "attest", "mcp"];
|
|
879
|
+
}
|
|
880
|
+
function installMcp(engine, root) {
|
|
881
|
+
return runEngineInstall(
|
|
882
|
+
engine,
|
|
883
|
+
["mcp", "install", "--dir", root, "--command", JSON.stringify(mcpLaunchCommand(root))],
|
|
884
|
+
MCP_KINDS,
|
|
885
|
+
"mcp install"
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
function printSkills(root, skills) {
|
|
889
|
+
const abs = (rel) => path4.join(root, ...rel.split("/"));
|
|
890
|
+
for (const rel of skills.created) process.stdout.write(`Created: ${abs(rel)}
|
|
891
|
+
`);
|
|
892
|
+
for (const rel of skills.updated) process.stdout.write(`Updated: ${abs(rel)}
|
|
893
|
+
`);
|
|
894
|
+
for (const rel of skills.skipped) process.stderr.write(`Skipped (unchanged): ${abs(rel)}
|
|
895
|
+
`);
|
|
896
|
+
for (const rel of skills.conflict) {
|
|
897
|
+
process.stderr.write(
|
|
898
|
+
`Conflict (a different attest entry, left as it is; \`attest-engine mcp install --force\` replaces it): ${abs(rel)}
|
|
899
|
+
`
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
async function runInit(parsed, root) {
|
|
904
|
+
const result = emptyInitResult();
|
|
905
|
+
const installs = [
|
|
906
|
+
[parsed.skills, "skills", installSkills],
|
|
907
|
+
[parsed.mcp, "mcp", installMcp]
|
|
908
|
+
];
|
|
909
|
+
for (const [wanted, command, install] of installs) {
|
|
910
|
+
if (!wanted) continue;
|
|
911
|
+
const engine = await engineProviding(command);
|
|
912
|
+
if (engine === null) return null;
|
|
913
|
+
let installed;
|
|
914
|
+
try {
|
|
915
|
+
installed = install(engine, root);
|
|
916
|
+
} catch (err) {
|
|
917
|
+
process.stderr.write(`attest: ${errorMessage(err)}
|
|
918
|
+
`);
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
if (!parsed.json) printSkills(root, installed);
|
|
922
|
+
for (const kind of INIT_KINDS_WITH_CONFLICT) result[kind].push(...installed[kind]);
|
|
923
|
+
}
|
|
924
|
+
const scaffolded = scaffoldProject(root, {
|
|
925
|
+
...parsed.ci ? { ci: "github" } : {},
|
|
926
|
+
quiet: parsed.json
|
|
927
|
+
});
|
|
928
|
+
result.created.push(...scaffolded.created);
|
|
929
|
+
result.skipped.push(...scaffolded.skipped);
|
|
930
|
+
return result;
|
|
931
|
+
}
|
|
932
|
+
async function cmdInit(args) {
|
|
933
|
+
if (args.some((a) => HELP_FLAGS.has(a))) {
|
|
934
|
+
process.stdout.write(`${INIT_USAGE}
|
|
935
|
+
`);
|
|
936
|
+
return 0;
|
|
937
|
+
}
|
|
938
|
+
const parsed = parseInitArgs(args);
|
|
939
|
+
if (parsed === null) {
|
|
940
|
+
process.stderr.write(`${INIT_USAGE}
|
|
941
|
+
`);
|
|
942
|
+
return 2;
|
|
943
|
+
}
|
|
944
|
+
const root = process.cwd();
|
|
945
|
+
if (!parsed.json) return await runInit(parsed, root) === null ? 1 : 0;
|
|
946
|
+
const stdoutWrite = process.stdout.write;
|
|
947
|
+
process.stdout.write = process.stderr.write.bind(process.stderr);
|
|
948
|
+
let result;
|
|
949
|
+
try {
|
|
950
|
+
result = await runInit(parsed, root);
|
|
951
|
+
} finally {
|
|
952
|
+
process.stdout.write = stdoutWrite;
|
|
953
|
+
}
|
|
954
|
+
if (result === null) return 1;
|
|
955
|
+
const done = result;
|
|
956
|
+
const kinds = parsed.mcp ? INIT_KINDS_WITH_CONFLICT : INIT_KINDS;
|
|
957
|
+
const sorted = Object.fromEntries(kinds.map((k) => [k, [...done[k]].sort()]));
|
|
958
|
+
process.stdout.write(`${JSON.stringify(sorted)}
|
|
959
|
+
`);
|
|
960
|
+
return 0;
|
|
961
|
+
}
|
|
475
962
|
async function main(argv = process.argv.slice(2)) {
|
|
476
963
|
const [command] = argv;
|
|
477
964
|
if (command === void 0 || HELP_FLAGS.has(command) || command === "help" && argv.length === 1) {
|
|
@@ -495,8 +982,8 @@ ${SDK_USAGE}`);
|
|
|
495
982
|
}
|
|
496
983
|
function isDirectExecution() {
|
|
497
984
|
const script = process.argv[1];
|
|
498
|
-
if (script === void 0 || !
|
|
499
|
-
return
|
|
985
|
+
if (script === void 0 || !fs4.existsSync(script)) return false;
|
|
986
|
+
return fs4.realpathSync(script) === fs4.realpathSync(fileURLToPath2(import.meta.url));
|
|
500
987
|
}
|
|
501
988
|
if (isDirectExecution()) {
|
|
502
989
|
main().then(
|
|
@@ -509,8 +996,10 @@ if (isDirectExecution()) {
|
|
|
509
996
|
);
|
|
510
997
|
}
|
|
511
998
|
export {
|
|
999
|
+
captureWritesFixture,
|
|
512
1000
|
engineCommands,
|
|
513
1001
|
exitStatus,
|
|
1002
|
+
helpJson,
|
|
514
1003
|
main
|
|
515
1004
|
};
|
|
516
1005
|
//# sourceMappingURL=cli.js.map
|