agentcert 0.2.6 → 0.3.0
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 +25 -0
- package/dist/cli.js +40 -3
- package/dist/command-help.js +8 -0
- package/dist/conformance.js +115 -0
- package/dist/index.js +2 -0
- package/dist/sandbox.js +258 -0
- package/dist/schema-validator.js +114 -1
- package/dist/vendor/onegent-runtime/mock-procurement.d.ts +6 -0
- package/dist/vendor/onegent-runtime/mock-procurement.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/mock-procurement.js +33 -0
- package/dist/vendor/onegent-runtime/policies.d.ts +6 -0
- package/dist/vendor/onegent-runtime/policies.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/policies.js +109 -0
- package/dist/vendor/onegent-runtime/risk.d.ts +3 -0
- package/dist/vendor/onegent-runtime/risk.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/risk.js +54 -0
- package/dist/vendor/onegent-runtime/sandbox-adapter-kit.d.ts +45 -0
- package/dist/vendor/onegent-runtime/sandbox-adapter-kit.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/sandbox-adapter-kit.js +148 -0
- package/dist/vendor/onegent-runtime/sandbox-harness.d.ts +167 -0
- package/dist/vendor/onegent-runtime/sandbox-harness.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/sandbox-harness.js +729 -0
- package/dist/vendor/onegent-runtime/sandbox-hosted.d.ts +76 -0
- package/dist/vendor/onegent-runtime/sandbox-hosted.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/sandbox-hosted.js +129 -0
- package/dist/vendor/onegent-runtime/sdk.d.ts +29 -0
- package/dist/vendor/onegent-runtime/sdk.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/sdk.js +140 -0
- package/dist/vendor/onegent-runtime/service.d.ts +27 -0
- package/dist/vendor/onegent-runtime/service.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/service.js +421 -0
- package/dist/vendor/onegent-runtime/store.d.ts +16 -0
- package/dist/vendor/onegent-runtime/store.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/store.js +29 -0
- package/dist/vendor/onegent-runtime/types.d.ts +284 -0
- package/dist/vendor/onegent-runtime/types.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/types.js +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -57,6 +57,31 @@ or mismatched artifacts remain observable as `partial` or `rejected`.
|
|
|
57
57
|
Project API keys can create runs, record events, and upload evidence, but
|
|
58
58
|
cannot approve their own runtime actions.
|
|
59
59
|
|
|
60
|
+
## Sandbox onboarding
|
|
61
|
+
|
|
62
|
+
Create and certify a synthetic SandboxSystem adapter with the same public CLI:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx agentcert sandbox init
|
|
66
|
+
npx agentcert sandbox certify --adapter ./agentcert.sandbox.mjs
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The first command writes one dependency-free JavaScript file. The second runs
|
|
70
|
+
the bundled deterministic adapter contract and writes
|
|
71
|
+
`.agentcert/sandbox/sandbox-adapter-conformance.json`. No `@agentcert` scoped
|
|
72
|
+
package is required.
|
|
73
|
+
|
|
74
|
+
After `agentcert connect`, certify and upload in one command:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npx agentcert sandbox push --adapter ./agentcert.sandbox.mjs
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Passing and failing reports are both retained for review, while failed
|
|
81
|
+
certifications return a non-zero exit status. The generated template is
|
|
82
|
+
synthetic and network-denied; it must not contain production credentials or
|
|
83
|
+
connect to live systems.
|
|
84
|
+
|
|
60
85
|
Review/export helpers:
|
|
61
86
|
|
|
62
87
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import { collectCompanionArtifacts, MAX_REPORTED_COMPANION_ARTIFACT_SKIPS } from
|
|
|
9
9
|
import { recordsFromAgentCertResult, evaluateFailureClassifier, renderCorpusSummary, summarizeCorpus, writeReviewedFailureDataset, } from "./corpus.js";
|
|
10
10
|
import { openCorpusStore, parseCorpusStoreKind } from "./corpus-store.js";
|
|
11
11
|
import { pushEvidenceToControlPlane, verifyControlPlaneConnection } from "./control-plane.js";
|
|
12
|
+
import { runEvidenceConformance } from "./conformance.js";
|
|
12
13
|
import { DEFAULT_AGENTCERT_SERVER, resolveConnection, saveConnection } from "./credentials.js";
|
|
13
14
|
import { applyFailureReviews, appendFailureReview, createFailureReview, findFailurePattern, parseFailureReviewStatus, parseFailureType, parseReviewConfidence, readFailureReviews, } from "./failure-review.js";
|
|
14
15
|
import { buildMonitorSnapshot, writeMonitorSnapshot } from "./monitor.js";
|
|
@@ -21,6 +22,7 @@ import { parseSchemaId, validateAgentCertSchema } from "./schema-validator.js";
|
|
|
21
22
|
import { applyRunOverrides, loadRunProfile, profileFromArtifactFlags, renderRunSummary, runAgentCertProfile, } from "./runner.js";
|
|
22
23
|
import { buildRobustnessLabSnapshot, readRobustnessLabConfig, renderRobustnessLabSummary, writeRobustnessLabSnapshot } from "./lab.js";
|
|
23
24
|
import { renderCommandHelp } from "./command-help.js";
|
|
25
|
+
import { runSandboxCommand } from "./sandbox.js";
|
|
24
26
|
process.on("uncaughtException", reportFatalError);
|
|
25
27
|
process.on("unhandledRejection", reportFatalError);
|
|
26
28
|
const command = process.argv[2] ?? "help";
|
|
@@ -119,6 +121,10 @@ Saved connections are reused by agentcert push and agentcert run --push.
|
|
|
119
121
|
process.stdout.write(`Credentials: ${path}\n`);
|
|
120
122
|
}
|
|
121
123
|
}
|
|
124
|
+
else if (command === "sandbox") {
|
|
125
|
+
const result = await runSandboxCommand(process.argv.slice(3));
|
|
126
|
+
process.exitCode = result.exitCode;
|
|
127
|
+
}
|
|
122
128
|
else if (command === "report") {
|
|
123
129
|
const config = await loadConfig(readFlag("--config"));
|
|
124
130
|
const subject = readFlag("--subject") ?? config?.subject.name ?? "agentcert-subject";
|
|
@@ -515,10 +521,39 @@ else if (command === "validate") {
|
|
|
515
521
|
}
|
|
516
522
|
}
|
|
517
523
|
}
|
|
524
|
+
else if (command === "conformance") {
|
|
525
|
+
const file = readFlag("--file") ?? readFirstPositionalAfterCommand();
|
|
526
|
+
if (!file)
|
|
527
|
+
throw new Error("Missing evidence file. Usage: agentcert conformance <file> --artifact-root <directory>.");
|
|
528
|
+
const artifactRoot = resolve(readFlag("--artifact-root") ?? dirname(resolve(file)));
|
|
529
|
+
const report = await runEvidenceConformance(await readJson(file), {
|
|
530
|
+
evidenceFile: file,
|
|
531
|
+
artifactRoot,
|
|
532
|
+
implementation: readFlag("--implementation"),
|
|
533
|
+
});
|
|
534
|
+
const output = `${JSON.stringify(report, null, 2)}\n`;
|
|
535
|
+
const outPath = readFlag("--out");
|
|
536
|
+
if (outPath) {
|
|
537
|
+
await mkdir(dirname(resolve(outPath)), { recursive: true });
|
|
538
|
+
await writeFile(outPath, output);
|
|
539
|
+
process.stdout.write(`Wrote conformance report: ${resolve(outPath)}\n`);
|
|
540
|
+
}
|
|
541
|
+
process.stdout.write(`${report.valid ? "Conformant" : "Non-conformant"} evidence implementation: ${report.implementation}\n`);
|
|
542
|
+
for (const item of report.checks) {
|
|
543
|
+
process.stdout.write(`- ${item.status === "passed" ? "PASS" : "FAIL"} ${item.id}: ${item.message}\n`);
|
|
544
|
+
for (const error of item.errors)
|
|
545
|
+
process.stdout.write(` - ${error}\n`);
|
|
546
|
+
}
|
|
547
|
+
if (!report.valid)
|
|
548
|
+
process.exitCode = 1;
|
|
549
|
+
}
|
|
518
550
|
else {
|
|
519
551
|
process.stdout.write(`Usage:
|
|
520
552
|
agentcert init --subject my-browser-agent
|
|
521
553
|
agentcert connect --server https://agentcert-control-plane.onrender.com --project <project-id>
|
|
554
|
+
agentcert sandbox init
|
|
555
|
+
agentcert sandbox certify --adapter ./agentcert.sandbox.mjs
|
|
556
|
+
agentcert sandbox push --adapter ./agentcert.sandbox.mjs
|
|
522
557
|
agentcert init --out agentcert.config.json --tripwire-config tripwire.yml --force
|
|
523
558
|
agentcert init --subject my-browser-agent --github-action
|
|
524
559
|
agentcert report --mcpbench .mcpbench/latest/results.json --tripwire .tripwire/latest/tripwire-result.json --onegent .onegent/procurement/audit-packet.json --out .agentcert/latest --subject my-agent
|
|
@@ -543,6 +578,7 @@ else {
|
|
|
543
578
|
agentcert serve --corpus .agentcert/corpus/corpus.jsonl --static public-demo/agentcert-monitor --artifact-root public-demo/browser-agent-robustness/evidence/tripwire-public-demo
|
|
544
579
|
agentcert validate .agentcert/latest/agentcert-evidence.json
|
|
545
580
|
agentcert validate .agentcert/latest/agentcert-evidence.json --check-artifacts
|
|
581
|
+
agentcert conformance .agentcert/latest/agentcert-evidence.json --artifact-root . --implementation my-agent
|
|
546
582
|
agentcert schema validate --schema evidence-bundle --file .agentcert/latest/agentcert-evidence.json
|
|
547
583
|
`);
|
|
548
584
|
}
|
|
@@ -911,12 +947,13 @@ jobs:
|
|
|
911
947
|
# permissions:
|
|
912
948
|
# contents: write
|
|
913
949
|
steps:
|
|
914
|
-
- uses: actions/checkout@
|
|
915
|
-
- uses: actions/setup-node@
|
|
950
|
+
- uses: actions/checkout@v7
|
|
951
|
+
- uses: actions/setup-node@v6
|
|
916
952
|
with:
|
|
917
953
|
node-version: "20"
|
|
918
954
|
|
|
919
|
-
-
|
|
955
|
+
- id: agentcert
|
|
956
|
+
uses: Kakarottoooo/agentcert/actions/tripwire@v0
|
|
920
957
|
with:
|
|
921
958
|
config: tripwire.yml
|
|
922
959
|
out: .tripwire/latest
|
package/dist/command-help.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
export function renderCommandHelp(command) {
|
|
2
|
+
if (command === "sandbox")
|
|
3
|
+
return undefined;
|
|
4
|
+
if (command === "conformance")
|
|
5
|
+
return `Usage:
|
|
6
|
+
agentcert conformance <evidence.json> --artifact-root <directory> [--implementation <name>] [--out <report.json>]
|
|
7
|
+
|
|
8
|
+
Checks schema identity, v0.1 compatibility, artifact manifest structure, and exact artifact hashes and sizes.
|
|
9
|
+
`;
|
|
2
10
|
if (command !== "push")
|
|
3
11
|
return undefined;
|
|
4
12
|
return `Usage:
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { normalizeManifestPath } from "./artifact-manifest.js";
|
|
5
|
+
import { validateAgentCertSchema } from "./schema-validator.js";
|
|
6
|
+
const SUPPORTED_TOP_LEVEL_FIELDS = new Set([
|
|
7
|
+
"schemaName", "schemaVersion", "schemaSemver", "kind", "runId", "generatedAt", "subject", "verdict", "summary",
|
|
8
|
+
"results", "evidence", "artifacts", "artifactManifest", "standards",
|
|
9
|
+
]);
|
|
10
|
+
export async function runEvidenceConformance(input, options) {
|
|
11
|
+
const checks = [];
|
|
12
|
+
const schema = validateAgentCertSchema("evidence-bundle", input);
|
|
13
|
+
checks.push(check("schema", "Evidence bundle satisfies the AgentCert v0.1 semantic contract.", schema.errors));
|
|
14
|
+
const bundle = object(input);
|
|
15
|
+
const compatibilityErrors = bundle
|
|
16
|
+
? Object.keys(bundle).filter((key) => !SUPPORTED_TOP_LEVEL_FIELDS.has(key)).map((key) => `Unsupported top-level field: ${key}.`)
|
|
17
|
+
: ["Evidence bundle must be an object."];
|
|
18
|
+
checks.push(check("compatibility", "Evidence bundle uses only v0.1-compatible top-level fields.", compatibilityErrors));
|
|
19
|
+
const manifestErrors = [];
|
|
20
|
+
const entries = parseManifest(bundle?.artifactManifest, manifestErrors);
|
|
21
|
+
checks.push(check("manifest", "Artifact manifest declares unique normalized paths, SHA-256, byte size, and kind.", manifestErrors));
|
|
22
|
+
const byteErrors = [];
|
|
23
|
+
if (manifestErrors.length === 0) {
|
|
24
|
+
const root = resolve(options.artifactRoot);
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
const path = resolve(root, entry.path);
|
|
27
|
+
if (path !== root && !path.startsWith(`${root}\\`) && !path.startsWith(`${root}/`)) {
|
|
28
|
+
byteErrors.push(`${entry.path}: resolved path escapes artifact root.`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const bytes = await readFile(path);
|
|
33
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
34
|
+
if (bytes.byteLength !== entry.sizeBytes)
|
|
35
|
+
byteErrors.push(`${entry.path}: expected ${entry.sizeBytes} bytes, observed ${bytes.byteLength}.`);
|
|
36
|
+
if (digest !== entry.sha256)
|
|
37
|
+
byteErrors.push(`${entry.path}: SHA-256 does not match the declared digest.`);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : "read_failed";
|
|
41
|
+
byteErrors.push(`${entry.path}: artifact could not be read (${code}).`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
byteErrors.push("Artifact bytes were not checked because the manifest is invalid.");
|
|
47
|
+
}
|
|
48
|
+
checks.push(check("artifact-bytes", "Every declared artifact matches its exact runtime bytes.", byteErrors));
|
|
49
|
+
const passed = checks.filter((item) => item.status === "passed").length;
|
|
50
|
+
const failed = checks.length - passed;
|
|
51
|
+
return {
|
|
52
|
+
schemaVersion: "agentcert.conformance.v0.1",
|
|
53
|
+
kind: "agentcert.evidence_conformance_report",
|
|
54
|
+
implementation: options.implementation?.trim() || "third-party",
|
|
55
|
+
evidenceFile: resolve(options.evidenceFile),
|
|
56
|
+
artifactRoot: resolve(options.artifactRoot),
|
|
57
|
+
generatedAt: (options.now ?? new Date()).toISOString(),
|
|
58
|
+
valid: failed === 0,
|
|
59
|
+
summary: { passed, failed },
|
|
60
|
+
checks,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function parseManifest(input, errors) {
|
|
64
|
+
const manifest = object(input);
|
|
65
|
+
if (!manifest) {
|
|
66
|
+
errors.push("artifactManifest is required for conformance.");
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
if (manifest.schemaVersion !== "agentcert.artifact_manifest.v0.1")
|
|
70
|
+
errors.push("artifactManifest.schemaVersion must be agentcert.artifact_manifest.v0.1.");
|
|
71
|
+
if (!Array.isArray(manifest.entries)) {
|
|
72
|
+
errors.push("artifactManifest.entries must be an array.");
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
if (manifest.entries.length > 500)
|
|
76
|
+
errors.push("artifactManifest.entries cannot exceed 500 entries.");
|
|
77
|
+
const paths = new Set();
|
|
78
|
+
const entries = [];
|
|
79
|
+
manifest.entries.forEach((inputEntry, index) => {
|
|
80
|
+
const entry = object(inputEntry);
|
|
81
|
+
if (!entry) {
|
|
82
|
+
errors.push(`artifactManifest.entries[${index}] must be an object.`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
let path;
|
|
86
|
+
try {
|
|
87
|
+
path = typeof entry.path === "string" ? normalizeManifestPath(entry.path) : undefined;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
errors.push(`artifactManifest.entries[${index}].path: ${error.message}`);
|
|
91
|
+
}
|
|
92
|
+
if (!path)
|
|
93
|
+
errors.push(`artifactManifest.entries[${index}].path must be a normalized relative path.`);
|
|
94
|
+
else if (paths.has(path))
|
|
95
|
+
errors.push(`artifactManifest contains duplicate path ${path}.`);
|
|
96
|
+
else
|
|
97
|
+
paths.add(path);
|
|
98
|
+
if (typeof entry.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(entry.sha256))
|
|
99
|
+
errors.push(`artifactManifest.entries[${index}].sha256 must be 64 lowercase hex characters.`);
|
|
100
|
+
if (!Number.isInteger(entry.sizeBytes) || entry.sizeBytes < 0)
|
|
101
|
+
errors.push(`artifactManifest.entries[${index}].sizeBytes must be a non-negative integer.`);
|
|
102
|
+
if (typeof entry.kind !== "string" || !/^[a-z][a-z0-9_-]{0,63}$/.test(entry.kind))
|
|
103
|
+
errors.push(`artifactManifest.entries[${index}].kind must be a stable lowercase identifier.`);
|
|
104
|
+
if (path && typeof entry.sha256 === "string" && Number.isInteger(entry.sizeBytes) && typeof entry.kind === "string") {
|
|
105
|
+
entries.push({ path, sha256: entry.sha256, sizeBytes: entry.sizeBytes, kind: entry.kind });
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
return entries;
|
|
109
|
+
}
|
|
110
|
+
function check(id, message, errors) {
|
|
111
|
+
return { id, status: errors.length === 0 ? "passed" : "failed", message, errors };
|
|
112
|
+
}
|
|
113
|
+
function object(input) {
|
|
114
|
+
return input && typeof input === "object" && !Array.isArray(input) ? input : undefined;
|
|
115
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,8 @@ export * from "./bundle.js";
|
|
|
2
2
|
export * from "./corpus.js";
|
|
3
3
|
export * from "./corpus-store.js";
|
|
4
4
|
export * from "./control-plane.js";
|
|
5
|
+
export * from "./sandbox.js";
|
|
6
|
+
export * from "./conformance.js";
|
|
5
7
|
export * from "./credentials.js";
|
|
6
8
|
export * from "./failure-review.js";
|
|
7
9
|
export * from "./evidence-signing.js";
|
package/dist/sandbox.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { resolveConnection } from "./credentials.js";
|
|
5
|
+
const DEFAULT_ADAPTER_PATH = "agentcert.sandbox.mjs";
|
|
6
|
+
const DEFAULT_REPORT_PATH = ".agentcert/sandbox/sandbox-adapter-conformance.json";
|
|
7
|
+
export async function runSandboxCommand(args) {
|
|
8
|
+
const action = args[0] ?? "help";
|
|
9
|
+
if (action === "help" || args.includes("--help") || args.includes("-h")) {
|
|
10
|
+
process.stdout.write(renderSandboxHelp(action === "help" ? undefined : action));
|
|
11
|
+
return { exitCode: 0 };
|
|
12
|
+
}
|
|
13
|
+
if (action === "init")
|
|
14
|
+
return initializeSandboxAdapter(args.slice(1));
|
|
15
|
+
if (action === "certify")
|
|
16
|
+
return certifySandboxAdapter(args.slice(1));
|
|
17
|
+
if (action === "push")
|
|
18
|
+
return pushSandboxCertification(args.slice(1));
|
|
19
|
+
throw new Error(`Unknown sandbox command ${JSON.stringify(action)}. Run \`npx agentcert sandbox --help\`.`);
|
|
20
|
+
}
|
|
21
|
+
export async function initializeSandboxAdapter(args) {
|
|
22
|
+
const requestedPath = flag(args, "--adapter") ?? flag(args, "--out") ?? DEFAULT_ADAPTER_PATH;
|
|
23
|
+
const outPath = resolve(requestedPath);
|
|
24
|
+
await writeStarterFile(outPath, sandboxAdapterTemplate(), args.includes("--force"));
|
|
25
|
+
process.stdout.write(`Wrote ${outPath}\n\nNext:\n`);
|
|
26
|
+
process.stdout.write(` npx agentcert sandbox certify --adapter ${JSON.stringify(requestedPath)}\n`);
|
|
27
|
+
process.stdout.write("\nReplace the in-memory handlers with your sandbox implementation. Keep production credentials and live systems out of this adapter.\n");
|
|
28
|
+
return { exitCode: 0 };
|
|
29
|
+
}
|
|
30
|
+
export async function certifySandboxAdapter(args, runtimeOverride) {
|
|
31
|
+
const runtime = runtimeOverride ?? await loadSandboxRuntime();
|
|
32
|
+
const adapterPath = resolve(flag(args, "--adapter") ?? DEFAULT_ADAPTER_PATH);
|
|
33
|
+
const reportPath = resolve(flag(args, "--out") ?? DEFAULT_REPORT_PATH);
|
|
34
|
+
const system = await loadSandboxAdapter(adapterPath);
|
|
35
|
+
const report = await runtime.runSandboxAdapterConformanceSuite({
|
|
36
|
+
system,
|
|
37
|
+
implementation: flag(args, "--implementation"),
|
|
38
|
+
targetSystem: flag(args, "--target-system"),
|
|
39
|
+
});
|
|
40
|
+
await runtime.writeSandboxAdapterConformanceReport(report, reportPath);
|
|
41
|
+
renderCertificationResult(report, reportPath);
|
|
42
|
+
return { exitCode: report.verdict.passed ? 0 : 1, reportPath, report };
|
|
43
|
+
}
|
|
44
|
+
export async function pushSandboxCertification(args, runtimeOverride) {
|
|
45
|
+
const runtime = runtimeOverride ?? await loadSandboxRuntime();
|
|
46
|
+
const certified = await certifySandboxAdapter(args, runtime);
|
|
47
|
+
if (!certified.report || !certified.reportPath)
|
|
48
|
+
throw new Error("Sandbox certification did not produce a report.");
|
|
49
|
+
const connection = await resolveConnection({
|
|
50
|
+
name: flag(args, "--connection"),
|
|
51
|
+
server: flag(args, "--server"),
|
|
52
|
+
projectId: flag(args, "--project"),
|
|
53
|
+
apiKey: flag(args, "--api-key"),
|
|
54
|
+
});
|
|
55
|
+
const uploaded = await runtime.uploadSandboxCertificationReport(certified.report, {
|
|
56
|
+
baseUrl: connection.server,
|
|
57
|
+
projectId: connection.projectId,
|
|
58
|
+
apiKey: connection.apiKey,
|
|
59
|
+
externalId: flag(args, "--external-id"),
|
|
60
|
+
});
|
|
61
|
+
process.stdout.write(`Hosted sandbox run: ${String(uploaded.run.id ?? "created")}\n`);
|
|
62
|
+
process.stdout.write(`Hosted evidence: ${String(uploaded.evidence.id ?? "created")}\n`);
|
|
63
|
+
return certified;
|
|
64
|
+
}
|
|
65
|
+
export async function loadSandboxAdapter(adapterPath) {
|
|
66
|
+
let module;
|
|
67
|
+
try {
|
|
68
|
+
module = await import(pathToFileURL(adapterPath).href);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
72
|
+
throw new Error(`Could not load sandbox adapter ${adapterPath}: ${message}`);
|
|
73
|
+
}
|
|
74
|
+
const candidate = module.sandboxSystem ?? module.default ?? module.system;
|
|
75
|
+
if (!isSandboxSystem(candidate)) {
|
|
76
|
+
throw new Error(`Sandbox adapter ${adapterPath} must export \`sandboxSystem\`, \`system\`, or a default SandboxSystem object.`);
|
|
77
|
+
}
|
|
78
|
+
return candidate;
|
|
79
|
+
}
|
|
80
|
+
export function renderSandboxHelp(action) {
|
|
81
|
+
if (action === "init")
|
|
82
|
+
return `Usage:
|
|
83
|
+
agentcert sandbox init [--adapter agentcert.sandbox.mjs] [--force]
|
|
84
|
+
|
|
85
|
+
Writes one dependency-free synthetic SandboxSystem adapter template.
|
|
86
|
+
`;
|
|
87
|
+
if (action === "certify")
|
|
88
|
+
return `Usage:
|
|
89
|
+
agentcert sandbox certify --adapter ./my-sandbox-adapter.js [--out .agentcert/sandbox/report.json]
|
|
90
|
+
|
|
91
|
+
Runs the deterministic sandbox adapter conformance suite locally.
|
|
92
|
+
`;
|
|
93
|
+
if (action === "push")
|
|
94
|
+
return `Usage:
|
|
95
|
+
agentcert sandbox push --adapter ./my-sandbox-adapter.js
|
|
96
|
+
|
|
97
|
+
Runs certification, writes the local report, and uploads it through the saved AgentCert connection.
|
|
98
|
+
Use \`agentcert connect\` first, or pass --server, --project, and --api-key.
|
|
99
|
+
`;
|
|
100
|
+
return `Usage:
|
|
101
|
+
agentcert sandbox init [--adapter agentcert.sandbox.mjs]
|
|
102
|
+
agentcert sandbox certify --adapter ./my-sandbox-adapter.js
|
|
103
|
+
agentcert sandbox push --adapter ./my-sandbox-adapter.js
|
|
104
|
+
|
|
105
|
+
Commands:
|
|
106
|
+
init Write one dependency-free adapter template
|
|
107
|
+
certify Run deterministic local conformance checks
|
|
108
|
+
push Certify and upload the report to AgentCert Hosted
|
|
109
|
+
|
|
110
|
+
Common options:
|
|
111
|
+
--adapter <path> Adapter module (default: agentcert.sandbox.mjs)
|
|
112
|
+
--out <path> Local report path
|
|
113
|
+
--implementation <id> Stable adapter implementation name
|
|
114
|
+
--target-system <name> Target system exercised by the suite
|
|
115
|
+
`;
|
|
116
|
+
}
|
|
117
|
+
async function loadSandboxRuntime() {
|
|
118
|
+
const adapterKitUrl = new URL("./vendor/onegent-runtime/sandbox-adapter-kit.js", import.meta.url);
|
|
119
|
+
const hostedUrl = new URL("./vendor/onegent-runtime/sandbox-hosted.js", import.meta.url);
|
|
120
|
+
try {
|
|
121
|
+
const [adapterKit, hosted] = await Promise.all([import(adapterKitUrl.href), import(hostedUrl.href)]);
|
|
122
|
+
return { ...adapterKit, ...hosted };
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : undefined;
|
|
126
|
+
if (code === "ERR_MODULE_NOT_FOUND") {
|
|
127
|
+
throw new Error("AgentCert sandbox runtime is missing. Reinstall the agentcert package and retry.");
|
|
128
|
+
}
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function isSandboxSystem(value) {
|
|
133
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
134
|
+
return false;
|
|
135
|
+
const system = value;
|
|
136
|
+
const safety = system.safety;
|
|
137
|
+
return typeof system.name === "string"
|
|
138
|
+
&& safety?.mode === "sandbox"
|
|
139
|
+
&& safety.networkAccess === false
|
|
140
|
+
&& safety.syntheticDataOnly === true
|
|
141
|
+
&& Array.isArray(safety.allowedTargetSystems)
|
|
142
|
+
&& ["createTenant", "deleteTenant", "resetTenant", "seedTenant", "hasTenant", "snapshotTenant", "adapterForTenant"]
|
|
143
|
+
.every((method) => typeof system[method] === "function");
|
|
144
|
+
}
|
|
145
|
+
function flag(args, name) {
|
|
146
|
+
const index = args.indexOf(name);
|
|
147
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
148
|
+
}
|
|
149
|
+
async function writeStarterFile(path, content, force) {
|
|
150
|
+
await mkdir(dirname(path), { recursive: true });
|
|
151
|
+
try {
|
|
152
|
+
await writeFile(path, content, { flag: force ? "w" : "wx" });
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
|
|
156
|
+
throw new Error(`${path} already exists. Re-run with --force to overwrite it.`);
|
|
157
|
+
}
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function renderCertificationResult(report, reportPath) {
|
|
162
|
+
process.stdout.write(`${report.verdict.passed ? "PASS" : "FAIL"} ${report.verdict.score}/100 sandbox adapter conformance\n`);
|
|
163
|
+
for (const check of report.checks) {
|
|
164
|
+
process.stdout.write(`- ${check.status === "passed" ? "PASS" : "FAIL"} ${check.id}: ${check.message}\n`);
|
|
165
|
+
}
|
|
166
|
+
process.stdout.write(`Report: ${reportPath}\n`);
|
|
167
|
+
}
|
|
168
|
+
function sandboxAdapterTemplate() {
|
|
169
|
+
return `// Synthetic local state only. Do not import production credentials or call live systems here.
|
|
170
|
+
const tenants = new Map();
|
|
171
|
+
|
|
172
|
+
export const sandboxSystem = {
|
|
173
|
+
name: "my-sandbox-adapter",
|
|
174
|
+
safety: {
|
|
175
|
+
mode: "sandbox",
|
|
176
|
+
networkAccess: false,
|
|
177
|
+
syntheticDataOnly: true,
|
|
178
|
+
allowedTargetSystems: ["MySandboxSystem"],
|
|
179
|
+
},
|
|
180
|
+
createTenant(input) {
|
|
181
|
+
if (input.synthetic !== true) throw new Error("Synthetic tenants only.");
|
|
182
|
+
if (tenants.has(input.id)) throw new Error(\`Tenant \${input.id} already exists.\`);
|
|
183
|
+
const seed = syntheticSeed(input.seed ?? {});
|
|
184
|
+
tenants.set(input.id, { seed, state: structuredClone(seed) });
|
|
185
|
+
},
|
|
186
|
+
deleteTenant(tenantId) {
|
|
187
|
+
tenants.delete(tenantId);
|
|
188
|
+
},
|
|
189
|
+
resetTenant(tenantId) {
|
|
190
|
+
const tenant = requiredTenant(tenantId);
|
|
191
|
+
tenant.state = structuredClone(tenant.seed);
|
|
192
|
+
},
|
|
193
|
+
seedTenant(tenantId, seed) {
|
|
194
|
+
const tenant = requiredTenant(tenantId);
|
|
195
|
+
tenant.seed = syntheticSeed(seed);
|
|
196
|
+
tenant.state = structuredClone(seed);
|
|
197
|
+
},
|
|
198
|
+
hasTenant(tenantId) {
|
|
199
|
+
return tenants.has(tenantId);
|
|
200
|
+
},
|
|
201
|
+
snapshotTenant(tenantId) {
|
|
202
|
+
return structuredClone(requiredTenant(tenantId).state);
|
|
203
|
+
},
|
|
204
|
+
adapterForTenant(tenantId) {
|
|
205
|
+
requiredTenant(tenantId);
|
|
206
|
+
return {
|
|
207
|
+
name: \`my-sandbox-adapter:\${tenantId}\`,
|
|
208
|
+
safety: { mode: "sandbox", networkAccess: false, allowedTargetSystems: ["MySandboxSystem"] },
|
|
209
|
+
execute(action) {
|
|
210
|
+
const tenant = requiredTenant(tenantId);
|
|
211
|
+
const previousState = structuredClone(tenant.state[action.businessObjectId] ?? action.beforeState);
|
|
212
|
+
const observedState = structuredClone(action.proposedAfterState);
|
|
213
|
+
tenant.state[action.businessObjectId] = observedState;
|
|
214
|
+
return { method: "SYNTHETIC_SANDBOX", targetSystem: action.targetSystem, previousState, observedState };
|
|
215
|
+
},
|
|
216
|
+
rollback(action, execution) {
|
|
217
|
+
const restored = structuredClone(execution.previousState ?? action.beforeState);
|
|
218
|
+
requiredTenant(tenantId).state[action.businessObjectId] = restored;
|
|
219
|
+
return { success: true, observedState: restored };
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export default sandboxSystem;
|
|
226
|
+
|
|
227
|
+
function requiredTenant(tenantId) {
|
|
228
|
+
const tenant = tenants.get(tenantId);
|
|
229
|
+
if (!tenant) throw new Error(\`Tenant \${tenantId} does not exist.\`);
|
|
230
|
+
return tenant;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function syntheticSeed(value) {
|
|
234
|
+
assertSynthetic(value, "seed", new Set());
|
|
235
|
+
return structuredClone(value);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function assertSynthetic(value, path, seen) {
|
|
239
|
+
if (value === null || ["boolean", "number"].includes(typeof value)) return;
|
|
240
|
+
if (typeof value === "string") {
|
|
241
|
+
if (/^(?:sk-(?:proj-)?|npm_|gh[pousr]_)[A-Za-z0-9_-]{16,}$/.test(value)) {
|
|
242
|
+
throw new Error(\`Credential-like value is not allowed at \${path}.\`);
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (typeof value !== "object") throw new Error(\`Synthetic data must be JSON-compatible at \${path}.\`);
|
|
247
|
+
if (seen.has(value)) throw new Error(\`Synthetic data cannot contain cycles at \${path}.\`);
|
|
248
|
+
seen.add(value);
|
|
249
|
+
for (const [key, item] of Object.entries(value)) {
|
|
250
|
+
if (/(?:password|passwd|secret|token|api[_-]?key|credential|authorization|cookie)/i.test(key)) {
|
|
251
|
+
throw new Error(\`Credential-like field \${path}.\${key} is not allowed.\`);
|
|
252
|
+
}
|
|
253
|
+
assertSynthetic(item, \`\${path}.\${key}\`, seen);
|
|
254
|
+
}
|
|
255
|
+
seen.delete(value);
|
|
256
|
+
}
|
|
257
|
+
`;
|
|
258
|
+
}
|
package/dist/schema-validator.js
CHANGED
|
@@ -59,10 +59,33 @@ function validateEvidenceBundle(value, errors) {
|
|
|
59
59
|
requiredArray(value, "evidence", errors);
|
|
60
60
|
requiredObject(value, "artifacts", errors);
|
|
61
61
|
requiredArray(value, "standards", errors);
|
|
62
|
+
const subject = recordValue(value.subject);
|
|
63
|
+
if (subject) {
|
|
64
|
+
requiredStringAt(subject, "name", "subject.name", errors);
|
|
65
|
+
requiredEnumAt(subject, "type", ["agent", "mcp-server", "tool", "application", "unknown"], "subject.type", errors);
|
|
66
|
+
}
|
|
67
|
+
const verdict = recordValue(value.verdict);
|
|
68
|
+
if (verdict) {
|
|
69
|
+
requiredBooleanAt(verdict, "passed", "verdict.passed", errors);
|
|
70
|
+
requiredNumberRange(verdict, "score", "verdict.score", 0, 100, errors);
|
|
71
|
+
requiredStringAt(verdict, "level", "verdict.level", errors);
|
|
72
|
+
}
|
|
73
|
+
const summary = recordValue(value.summary);
|
|
74
|
+
if (summary) {
|
|
75
|
+
stringArray(summary.products, "summary.products", errors);
|
|
76
|
+
requiredNonNegativeInteger(summary, "criticalEvidence", "summary.criticalEvidence", errors);
|
|
77
|
+
requiredNonNegativeInteger(summary, "highEvidence", "summary.highEvidence", errors);
|
|
78
|
+
requiredNonNegativeInteger(summary, "totalEvidence", "summary.totalEvidence", errors);
|
|
79
|
+
}
|
|
80
|
+
validateTimestamp(value.generatedAt, "generatedAt", errors);
|
|
81
|
+
validateArtifactMap(value.artifacts, "artifacts", errors);
|
|
82
|
+
validateResultArray(value.results, "results", errors);
|
|
83
|
+
validateEvidenceArray(value.evidence, "evidence", errors);
|
|
84
|
+
validateStandards(value.standards, errors);
|
|
62
85
|
}
|
|
63
86
|
function validateResult(value, errors) {
|
|
64
87
|
requiredConst(value, "schemaVersion", "1", errors);
|
|
65
|
-
|
|
88
|
+
requiredString(value, "product", errors);
|
|
66
89
|
requiredString(value, "runId", errors);
|
|
67
90
|
requiredString(value, "timestamp", errors);
|
|
68
91
|
requiredEnum(value, "phase", ["pre-release", "runtime"], errors);
|
|
@@ -70,6 +93,62 @@ function validateResult(value, errors) {
|
|
|
70
93
|
requiredBoolean(value, "passed", errors);
|
|
71
94
|
requiredObject(value, "artifacts", errors);
|
|
72
95
|
requiredArray(value, "evidence", errors);
|
|
96
|
+
requiredNumberRange(value, "score", "score", 0, 100, errors);
|
|
97
|
+
validateTimestamp(value.timestamp, "timestamp", errors);
|
|
98
|
+
validateArtifactMap(value.artifacts, "artifacts", errors);
|
|
99
|
+
validateEvidenceArray(value.evidence, "evidence", errors);
|
|
100
|
+
}
|
|
101
|
+
function validateResultArray(input, path, errors) {
|
|
102
|
+
if (!Array.isArray(input))
|
|
103
|
+
return;
|
|
104
|
+
input.forEach((item, index) => {
|
|
105
|
+
const value = object(item, `${path}[${index}]`, errors);
|
|
106
|
+
if (!value)
|
|
107
|
+
return;
|
|
108
|
+
const nested = [];
|
|
109
|
+
validateResult(value, nested);
|
|
110
|
+
errors.push(...nested.map((error) => `${path}[${index}].${error}`));
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
function validateEvidenceArray(input, path, errors) {
|
|
114
|
+
if (!Array.isArray(input))
|
|
115
|
+
return;
|
|
116
|
+
input.forEach((item, index) => {
|
|
117
|
+
const value = object(item, `${path}[${index}]`, errors);
|
|
118
|
+
if (!value)
|
|
119
|
+
return;
|
|
120
|
+
requiredStringAt(value, "id", `${path}[${index}].id`, errors);
|
|
121
|
+
requiredStringAt(value, "kind", `${path}[${index}].kind`, errors);
|
|
122
|
+
requiredEnumAt(value, "severity", ["critical", "high", "medium", "low", "info"], `${path}[${index}].severity`, errors);
|
|
123
|
+
requiredStringAt(value, "message", `${path}[${index}].message`, errors);
|
|
124
|
+
for (const field of ["source", "artifactPath", "suggestedFix"]) {
|
|
125
|
+
if (value[field] !== undefined && typeof value[field] !== "string")
|
|
126
|
+
errors.push(`${path}[${index}].${field} must be a string.`);
|
|
127
|
+
}
|
|
128
|
+
if (value.metadata !== undefined && !recordValue(value.metadata))
|
|
129
|
+
errors.push(`${path}[${index}].metadata must be an object.`);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function validateArtifactMap(input, path, errors) {
|
|
133
|
+
const value = recordValue(input);
|
|
134
|
+
if (!value)
|
|
135
|
+
return;
|
|
136
|
+
for (const [key, item] of Object.entries(value))
|
|
137
|
+
if (typeof item !== "string")
|
|
138
|
+
errors.push(`${path}.${key} must be a string.`);
|
|
139
|
+
}
|
|
140
|
+
function validateStandards(input, errors) {
|
|
141
|
+
if (!Array.isArray(input))
|
|
142
|
+
return;
|
|
143
|
+
input.forEach((item, index) => {
|
|
144
|
+
const value = object(item, `standards[${index}]`, errors);
|
|
145
|
+
if (!value)
|
|
146
|
+
return;
|
|
147
|
+
requiredStringAt(value, "id", `standards[${index}].id`, errors);
|
|
148
|
+
requiredStringAt(value, "name", `standards[${index}].name`, errors);
|
|
149
|
+
requiredEnumAt(value, "status", ["mapped", "planned"], `standards[${index}].status`, errors);
|
|
150
|
+
requiredStringAt(value, "note", `standards[${index}].note`, errors);
|
|
151
|
+
});
|
|
73
152
|
}
|
|
74
153
|
function validateCorpusRecord(value, errors) {
|
|
75
154
|
requiredConst(value, "schemaVersion", "1", errors);
|
|
@@ -161,3 +240,37 @@ function requiredEnum(input, key, values, errors) {
|
|
|
161
240
|
errors.push(`${key} must be one of: ${values.join(", ")}.`);
|
|
162
241
|
}
|
|
163
242
|
}
|
|
243
|
+
function recordValue(input) {
|
|
244
|
+
return input && typeof input === "object" && !Array.isArray(input) ? input : undefined;
|
|
245
|
+
}
|
|
246
|
+
function requiredStringAt(input, key, path, errors) {
|
|
247
|
+
if (typeof input[key] !== "string" || input[key] === "")
|
|
248
|
+
errors.push(`${path} must be a non-empty string.`);
|
|
249
|
+
}
|
|
250
|
+
function requiredBooleanAt(input, key, path, errors) {
|
|
251
|
+
if (typeof input[key] !== "boolean")
|
|
252
|
+
errors.push(`${path} must be a boolean.`);
|
|
253
|
+
}
|
|
254
|
+
function requiredEnumAt(input, key, values, path, errors) {
|
|
255
|
+
if (typeof input[key] !== "string" || !values.includes(input[key]))
|
|
256
|
+
errors.push(`${path} must be one of: ${values.join(", ")}.`);
|
|
257
|
+
}
|
|
258
|
+
function requiredNumberRange(input, key, path, minimum, maximum, errors) {
|
|
259
|
+
const value = input[key];
|
|
260
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < minimum || value > maximum) {
|
|
261
|
+
errors.push(`${path} must be a finite number from ${minimum} to ${maximum}.`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function requiredNonNegativeInteger(input, key, path, errors) {
|
|
265
|
+
const value = input[key];
|
|
266
|
+
if (!Number.isInteger(value) || value < 0)
|
|
267
|
+
errors.push(`${path} must be a non-negative integer.`);
|
|
268
|
+
}
|
|
269
|
+
function stringArray(input, path, errors) {
|
|
270
|
+
if (!Array.isArray(input) || input.some((item) => typeof item !== "string"))
|
|
271
|
+
errors.push(`${path} must be an array of strings.`);
|
|
272
|
+
}
|
|
273
|
+
function validateTimestamp(input, path, errors) {
|
|
274
|
+
if (typeof input === "string" && !Number.isFinite(Date.parse(input)))
|
|
275
|
+
errors.push(`${path} must be a valid date-time string.`);
|
|
276
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ActionIntent, MockPurchaseOrder } from "./types.js";
|
|
2
|
+
export declare const PROCUREMENT_DEMO_PO_ID = "PO-DEMO-4850";
|
|
3
|
+
export declare function createProcurementDemoPurchaseOrder(): MockPurchaseOrder;
|
|
4
|
+
export declare function getPurchaseOrder(id: string): MockPurchaseOrder | undefined;
|
|
5
|
+
export declare function submitMockPurchaseOrder(action: ActionIntent): MockPurchaseOrder;
|
|
6
|
+
//# sourceMappingURL=mock-procurement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock-procurement.d.ts","sourceRoot":"","sources":["../../../../onegent-runtime/src/mock-procurement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAGlE,eAAO,MAAM,sBAAsB,iBAAiB,CAAC;AAErD,wBAAgB,kCAAkC,IAAI,iBAAiB,CActE;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAE1E;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,YAAY,GAAG,iBAAiB,CAc/E"}
|