agentcert 0.2.0 → 0.2.2
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 +11 -0
- package/dist/cli.js +131 -1
- package/dist/control-plane.js +143 -0
- package/dist/credentials.js +115 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,6 +34,17 @@ Default outputs:
|
|
|
34
34
|
- `.agentcert/latest/reviewed-failure-dataset.jsonl`
|
|
35
35
|
- `.agentcert/latest/monitor.json`
|
|
36
36
|
|
|
37
|
+
Push the validated evidence bundle into a hosted AgentCert project:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx agentcert connect --server https://agentcert-control-plane.onrender.com --project your-project-id
|
|
41
|
+
npx agentcert push --evidence .agentcert/latest/agentcert-evidence.json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Add `--push` to `agentcert run` to run locally and upload the resulting bundle
|
|
45
|
+
in one command. Project API keys can create runs, record events, and upload
|
|
46
|
+
evidence, but cannot approve their own runtime actions.
|
|
47
|
+
|
|
37
48
|
Review/export helpers:
|
|
38
49
|
|
|
39
50
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
4
5
|
import { validateEvidenceArtifacts } from "./artifact-validation.js";
|
|
5
6
|
import { renderAgentCertBadge } from "./badge.js";
|
|
6
7
|
import { buildEvidenceBundle } from "./bundle.js";
|
|
7
8
|
import { recordsFromAgentCertResult, evaluateFailureClassifier, renderCorpusSummary, summarizeCorpus, writeReviewedFailureDataset, } from "./corpus.js";
|
|
8
9
|
import { openCorpusStore, parseCorpusStoreKind } from "./corpus-store.js";
|
|
10
|
+
import { pushEvidenceToControlPlane, verifyControlPlaneConnection } from "./control-plane.js";
|
|
11
|
+
import { DEFAULT_AGENTCERT_SERVER, resolveConnection, saveConnection } from "./credentials.js";
|
|
9
12
|
import { applyFailureReviews, appendFailureReview, createFailureReview, findFailurePattern, parseFailureReviewStatus, parseFailureType, parseReviewConfidence, readFailureReviews, } from "./failure-review.js";
|
|
10
13
|
import { buildMonitorSnapshot, writeMonitorSnapshot } from "./monitor.js";
|
|
11
14
|
import { normalizeMcpBenchResult, normalizeOnegentAuditPacket, normalizeTripwireResult } from "./normalizers.js";
|
|
@@ -81,6 +84,33 @@ Next:
|
|
|
81
84
|
npx agentcert run --tripwire .tripwire/latest/tripwire-result.json --subject ${JSON.stringify(subject)} --fail-on-verdict
|
|
82
85
|
`);
|
|
83
86
|
}
|
|
87
|
+
else if (command === "connect") {
|
|
88
|
+
if (readBoolFlag("--help")) {
|
|
89
|
+
process.stdout.write(`Usage:
|
|
90
|
+
agentcert connect --server https://agentcert-control-plane.onrender.com --project <project-id>
|
|
91
|
+
agentcert connect --name staging --server https://staging.example.com --project <project-id>
|
|
92
|
+
|
|
93
|
+
The API key is read from AGENTCERT_API_KEY or requested with hidden input.
|
|
94
|
+
Saved connections are reused by agentcert push and agentcert run --push.
|
|
95
|
+
`);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
const name = readFlag("--name") ?? "default";
|
|
99
|
+
const server = readFlag("--server") ?? process.env.AGENTCERT_BASE_URL ?? DEFAULT_AGENTCERT_SERVER;
|
|
100
|
+
const projectId = readFlag("--project") ?? process.env.AGENTCERT_PROJECT_ID ?? await promptValue("Project ID: ");
|
|
101
|
+
const apiKey = readFlag("--api-key") ?? process.env.AGENTCERT_API_KEY ?? await promptSecret("Project API key: ");
|
|
102
|
+
const connection = await resolveConnection({ server, projectId, apiKey, env: {} });
|
|
103
|
+
const verified = await verifyControlPlaneConnection({
|
|
104
|
+
baseUrl: connection.server,
|
|
105
|
+
projectId: connection.projectId,
|
|
106
|
+
apiKey: connection.apiKey,
|
|
107
|
+
});
|
|
108
|
+
const path = await saveConnection(name, connection);
|
|
109
|
+
process.stdout.write(`Connected ${JSON.stringify(name)} to ${connection.server}\n`);
|
|
110
|
+
process.stdout.write(`Project: ${verified.projectId} (${verified.runs} runs, ${verified.evidence} evidence objects)\n`);
|
|
111
|
+
process.stdout.write(`Credentials: ${path}\n`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
84
114
|
else if (command === "report") {
|
|
85
115
|
const config = await loadConfig(readFlag("--config"));
|
|
86
116
|
const subject = readFlag("--subject") ?? config?.subject.name ?? "agentcert-subject";
|
|
@@ -269,8 +299,27 @@ else if (command === "run") {
|
|
|
269
299
|
overrides,
|
|
270
300
|
});
|
|
271
301
|
process.stdout.write(renderRunSummary(outcome));
|
|
302
|
+
if (readBoolFlag("--push")) {
|
|
303
|
+
await pushHostedEvidence(outcome.bundle, Buffer.from(`${JSON.stringify(outcome.bundle, null, 2)}\n`), "agentcert-evidence.json");
|
|
304
|
+
}
|
|
272
305
|
process.exitCode = outcome.exitCode;
|
|
273
306
|
}
|
|
307
|
+
else if (command === "push") {
|
|
308
|
+
const evidencePath = resolve(readFlag("--evidence") ?? ".agentcert/latest/agentcert-evidence.json");
|
|
309
|
+
const evidenceBytes = await readFile(evidencePath);
|
|
310
|
+
let bundle;
|
|
311
|
+
try {
|
|
312
|
+
bundle = JSON.parse(evidenceBytes.toString("utf8"));
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
throw new Error(`Evidence file is not valid JSON: ${evidencePath}`);
|
|
316
|
+
}
|
|
317
|
+
const validation = validateAgentCertSchema("evidence-bundle", bundle);
|
|
318
|
+
if (!validation.valid) {
|
|
319
|
+
throw new Error(`Evidence bundle is invalid:\n${validation.errors.map((error) => `- ${error}`).join("\n")}`);
|
|
320
|
+
}
|
|
321
|
+
await pushHostedEvidence(bundle, evidenceBytes, basename(evidencePath));
|
|
322
|
+
}
|
|
274
323
|
else if (command === "release-gate") {
|
|
275
324
|
const overrides = readRunOverrides();
|
|
276
325
|
const profileName = readFlag("--profile");
|
|
@@ -461,6 +510,7 @@ else if (command === "validate") {
|
|
|
461
510
|
else {
|
|
462
511
|
process.stdout.write(`Usage:
|
|
463
512
|
agentcert init --subject my-browser-agent
|
|
513
|
+
agentcert connect --server https://agentcert-control-plane.onrender.com --project <project-id>
|
|
464
514
|
agentcert init --out agentcert.config.json --tripwire-config tripwire.yml --force
|
|
465
515
|
agentcert init --subject my-browser-agent --github-action
|
|
466
516
|
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
|
|
@@ -471,6 +521,8 @@ else {
|
|
|
471
521
|
agentcert monitor build --corpus .agentcert/corpus/corpus.jsonl --out .agentcert/latest/monitor.json --subject my-agent
|
|
472
522
|
agentcert monitor build --store sqlite --sqlite .agentcert/corpus/agentcert.sqlite --out .agentcert/latest/monitor.json --subject my-agent
|
|
473
523
|
agentcert run --profile public-demo
|
|
524
|
+
agentcert push --evidence .agentcert/latest/agentcert-evidence.json --server https://agentcert.example.com --project <project-id>
|
|
525
|
+
agentcert run --tripwire .tripwire/latest/tripwire-result.json --push
|
|
474
526
|
agentcert run --mcpbench .mcpbench/latest/results.json --tripwire .tripwire/latest/tripwire-result.json --onegent .onegent/procurement/audit-packet.json --out .agentcert/latest --corpus .agentcert/corpus/corpus.jsonl --monitor-out .agentcert/latest/monitor.json --reviewed-dataset-out .agentcert/latest/reviewed-failure-dataset.jsonl
|
|
475
527
|
agentcert release-gate --config agentcert.config.json --strict
|
|
476
528
|
agentcert release-gate --evidence .agentcert/latest/agentcert-evidence.json --baseline .agentcert/baselines/main.json
|
|
@@ -492,6 +544,78 @@ async function loadConfig(path) {
|
|
|
492
544
|
}
|
|
493
545
|
return (await readJson(path));
|
|
494
546
|
}
|
|
547
|
+
async function pushHostedEvidence(bundle, bytes, fileName) {
|
|
548
|
+
const connection = await resolveConnection({
|
|
549
|
+
name: readFlag("--connection"),
|
|
550
|
+
server: readFlag("--server"),
|
|
551
|
+
projectId: readFlag("--project"),
|
|
552
|
+
apiKey: readFlag("--api-key"),
|
|
553
|
+
});
|
|
554
|
+
const result = await pushEvidenceToControlPlane({
|
|
555
|
+
baseUrl: connection.server,
|
|
556
|
+
projectId: connection.projectId,
|
|
557
|
+
apiKey: connection.apiKey,
|
|
558
|
+
bundle,
|
|
559
|
+
evidenceBytes: bytes,
|
|
560
|
+
fileName,
|
|
561
|
+
externalId: readFlag("--external-id"),
|
|
562
|
+
});
|
|
563
|
+
process.stdout.write(`Hosted run: ${result.runId}\nHosted evidence: ${result.evidenceId}\n`);
|
|
564
|
+
}
|
|
565
|
+
async function promptValue(label) {
|
|
566
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
567
|
+
throw new Error(`Missing ${label.trim().replace(/:$/, "").toLowerCase()}. Pass it as a flag or environment variable.`);
|
|
568
|
+
}
|
|
569
|
+
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
570
|
+
try {
|
|
571
|
+
return (await prompt.question(label)).trim();
|
|
572
|
+
}
|
|
573
|
+
finally {
|
|
574
|
+
prompt.close();
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
async function promptSecret(label) {
|
|
578
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY || !process.stdin.setRawMode) {
|
|
579
|
+
throw new Error("Missing project API key. Set AGENTCERT_API_KEY or pass --api-key in a trusted environment.");
|
|
580
|
+
}
|
|
581
|
+
process.stdout.write(label);
|
|
582
|
+
process.stdin.setRawMode(true);
|
|
583
|
+
process.stdin.resume();
|
|
584
|
+
process.stdin.setEncoding("utf8");
|
|
585
|
+
return new Promise((resolveSecret, rejectSecret) => {
|
|
586
|
+
let value = "";
|
|
587
|
+
const cleanup = () => {
|
|
588
|
+
process.stdin.off("data", onData);
|
|
589
|
+
process.stdin.setRawMode?.(false);
|
|
590
|
+
process.stdin.pause();
|
|
591
|
+
process.stdout.write("\n");
|
|
592
|
+
};
|
|
593
|
+
const onData = (chunk) => {
|
|
594
|
+
for (const character of String(chunk)) {
|
|
595
|
+
if (character === "\u0003") {
|
|
596
|
+
cleanup();
|
|
597
|
+
rejectSecret(new Error("AgentCert connection cancelled."));
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (character === "\r" || character === "\n") {
|
|
601
|
+
cleanup();
|
|
602
|
+
resolveSecret(value.trim());
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (character === "\u007f" || character === "\b") {
|
|
606
|
+
if (value) {
|
|
607
|
+
value = value.slice(0, -1);
|
|
608
|
+
process.stdout.write("\b \b");
|
|
609
|
+
}
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
value += character;
|
|
613
|
+
process.stdout.write("*");
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
process.stdin.on("data", onData);
|
|
617
|
+
});
|
|
618
|
+
}
|
|
495
619
|
async function readJson(path) {
|
|
496
620
|
const raw = await readFile(resolve(path), "utf8");
|
|
497
621
|
return JSON.parse(raw);
|
|
@@ -683,6 +807,12 @@ function errorHint(message, code) {
|
|
|
683
807
|
if (message.includes("already exists")) {
|
|
684
808
|
return "Hint: re-run with `--force` to overwrite starter files, or choose a different `--out` path.";
|
|
685
809
|
}
|
|
810
|
+
if (message.includes("Hosted push requires")) {
|
|
811
|
+
return "Hint: create a project API key in AgentCert Integrations, then set AGENTCERT_BASE_URL, AGENTCERT_PROJECT_ID, and AGENTCERT_API_KEY.";
|
|
812
|
+
}
|
|
813
|
+
if (message.includes("Authentication required") || message.includes("API key")) {
|
|
814
|
+
return "Hint: check AGENTCERT_API_KEY and confirm the key belongs to AGENTCERT_PROJECT_ID.";
|
|
815
|
+
}
|
|
686
816
|
return undefined;
|
|
687
817
|
}
|
|
688
818
|
function starterTripwireConfig(subject) {
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
export class ControlPlaneRequestError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
constructor(message, status) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.name = "ControlPlaneRequestError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export async function verifyControlPlaneConnection(options) {
|
|
10
|
+
const baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
11
|
+
const request = options.fetch ?? fetch;
|
|
12
|
+
try {
|
|
13
|
+
const overview = await requestJson(request, `${baseUrl}/v1/projects/${encodeURIComponent(options.projectId)}/overview`, {
|
|
14
|
+
headers: { authorization: `Bearer ${options.apiKey}` },
|
|
15
|
+
});
|
|
16
|
+
return {
|
|
17
|
+
projectId: overview.projectId,
|
|
18
|
+
runs: overview.summary.runs ?? 0,
|
|
19
|
+
evidence: overview.summary.evidence ?? 0,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error instanceof ControlPlaneRequestError && error.status === 401) {
|
|
24
|
+
throw new Error("AgentCert API key was rejected. Create a new project API key and try again.");
|
|
25
|
+
}
|
|
26
|
+
if (error instanceof ControlPlaneRequestError && error.status === 403) {
|
|
27
|
+
throw new Error(`AgentCert API key cannot access project ${options.projectId}. Check the project ID and key scope.`);
|
|
28
|
+
}
|
|
29
|
+
if (error instanceof ControlPlaneRequestError && error.status === 404) {
|
|
30
|
+
throw new Error(`AgentCert project ${options.projectId} was not found.`);
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function pushEvidenceToControlPlane(options) {
|
|
36
|
+
const baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
37
|
+
if (!baseUrl || !options.projectId || !options.apiKey) {
|
|
38
|
+
throw new Error("baseUrl, projectId, and apiKey are required to push evidence.");
|
|
39
|
+
}
|
|
40
|
+
const request = options.fetch ?? fetch;
|
|
41
|
+
const projectUrl = `${baseUrl}/v1/projects/${encodeURIComponent(options.projectId)}`;
|
|
42
|
+
const externalId = options.externalId ?? options.bundle.runId;
|
|
43
|
+
const kind = hostedRunKind(options.bundle);
|
|
44
|
+
const headers = { authorization: `Bearer ${options.apiKey}` };
|
|
45
|
+
const run = await requestJson(request, `${projectUrl}/runs`, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: { ...headers, "content-type": "application/json" },
|
|
48
|
+
body: JSON.stringify({
|
|
49
|
+
externalId,
|
|
50
|
+
kind,
|
|
51
|
+
schemaVersion: options.bundle.schemaVersion,
|
|
52
|
+
startedAt: options.bundle.generatedAt,
|
|
53
|
+
metadata: {
|
|
54
|
+
subject: options.bundle.subject,
|
|
55
|
+
products: options.bundle.summary.products,
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
await requestJson(request, `${projectUrl}/runs/${encodeURIComponent(run.id)}/events`, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: { ...headers, "content-type": "application/json" },
|
|
62
|
+
body: JSON.stringify({
|
|
63
|
+
events: [{
|
|
64
|
+
sequence: 0,
|
|
65
|
+
type: "agentcert.evidence.created",
|
|
66
|
+
actor: "agentcert-cli",
|
|
67
|
+
occurredAt: options.bundle.generatedAt,
|
|
68
|
+
payload: {
|
|
69
|
+
verdict: options.bundle.verdict,
|
|
70
|
+
totalEvidence: options.bundle.summary.totalEvidence,
|
|
71
|
+
criticalEvidence: options.bundle.summary.criticalEvidence,
|
|
72
|
+
highEvidence: options.bundle.summary.highEvidence,
|
|
73
|
+
},
|
|
74
|
+
}],
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
const query = new URLSearchParams({
|
|
78
|
+
fileName: options.fileName ?? "agentcert-evidence.json",
|
|
79
|
+
kind: "evidence_bundle",
|
|
80
|
+
schemaVersion: options.bundle.schemaVersion,
|
|
81
|
+
runId: run.id,
|
|
82
|
+
});
|
|
83
|
+
const evidence = await requestJson(request, `${projectUrl}/evidence?${query}`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: { ...headers, "content-type": "application/json" },
|
|
86
|
+
body: new Uint8Array(options.evidenceBytes).buffer,
|
|
87
|
+
});
|
|
88
|
+
const firstDivergence = options.bundle.evidence.find((item) => item.severity === "critical" || item.severity === "high")?.message;
|
|
89
|
+
await requestJson(request, `${projectUrl}/runs/${encodeURIComponent(run.id)}/complete`, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: { ...headers, "content-type": "application/json" },
|
|
92
|
+
body: JSON.stringify({
|
|
93
|
+
status: options.bundle.verdict.passed ? "passed" : "failed",
|
|
94
|
+
score: options.bundle.verdict.score,
|
|
95
|
+
summary: `${options.bundle.subject.name}: ${options.bundle.verdict.level}`,
|
|
96
|
+
firstDivergence,
|
|
97
|
+
completedAt: options.bundle.generatedAt,
|
|
98
|
+
metadata: {
|
|
99
|
+
evidenceId: evidence.id,
|
|
100
|
+
evidenceSchemaVersion: options.bundle.schemaVersion,
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
return { runId: run.id, evidenceId: evidence.id, externalId };
|
|
105
|
+
}
|
|
106
|
+
function hostedRunKind(bundle) {
|
|
107
|
+
const products = new Set(bundle.summary.products);
|
|
108
|
+
if (products.size > 1)
|
|
109
|
+
return "release_gate";
|
|
110
|
+
if (products.has("mcpbench"))
|
|
111
|
+
return "mcpbench";
|
|
112
|
+
if (products.has("tripwire-ci"))
|
|
113
|
+
return "tripwire";
|
|
114
|
+
if (products.has("onegent-runtime"))
|
|
115
|
+
return "runtime";
|
|
116
|
+
return "custom";
|
|
117
|
+
}
|
|
118
|
+
async function requestJson(request, url, init) {
|
|
119
|
+
let response;
|
|
120
|
+
try {
|
|
121
|
+
response = await request(url, { ...init, signal: init.signal ?? AbortSignal.timeout(30_000) });
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
125
|
+
throw new ControlPlaneRequestError(`AgentCert control plane request failed: ${message}`);
|
|
126
|
+
}
|
|
127
|
+
const text = await response.text();
|
|
128
|
+
let value = {};
|
|
129
|
+
if (text) {
|
|
130
|
+
try {
|
|
131
|
+
value = JSON.parse(text);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
if (!response.ok)
|
|
135
|
+
throw new ControlPlaneRequestError(`AgentCert control plane returned HTTP ${response.status}.`, response.status);
|
|
136
|
+
throw new ControlPlaneRequestError("AgentCert control plane returned invalid JSON.", response.status);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
throw new ControlPlaneRequestError(typeof value.error === "string" ? value.error : `AgentCert control plane returned HTTP ${response.status}.`, response.status);
|
|
141
|
+
}
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
export const DEFAULT_AGENTCERT_SERVER = "https://agentcert-control-plane.onrender.com";
|
|
6
|
+
export async function saveConnection(name, input, options = {}) {
|
|
7
|
+
const connectionName = validateConnectionName(name);
|
|
8
|
+
const connection = validateConnection(input);
|
|
9
|
+
const path = credentialsPath(options);
|
|
10
|
+
const current = await readCredentialFile(path);
|
|
11
|
+
const next = {
|
|
12
|
+
schemaVersion: "agentcert.credentials.v1",
|
|
13
|
+
defaultConnection: connectionName,
|
|
14
|
+
connections: { ...current?.connections, [connectionName]: connection },
|
|
15
|
+
};
|
|
16
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
17
|
+
const temporaryPath = `${path}.${randomUUID()}.tmp`;
|
|
18
|
+
await writeFile(temporaryPath, `${JSON.stringify(next, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
19
|
+
await rename(temporaryPath, path);
|
|
20
|
+
await chmod(path, 0o600).catch(() => undefined);
|
|
21
|
+
return path;
|
|
22
|
+
}
|
|
23
|
+
export async function loadConnection(name, options = {}) {
|
|
24
|
+
const file = await readCredentialFile(credentialsPath(options));
|
|
25
|
+
if (!file)
|
|
26
|
+
return undefined;
|
|
27
|
+
const connectionName = name ? validateConnectionName(name) : file.defaultConnection;
|
|
28
|
+
const connection = file.connections[connectionName];
|
|
29
|
+
return connection ? validateConnection(connection) : undefined;
|
|
30
|
+
}
|
|
31
|
+
export async function resolveConnection(options = {}) {
|
|
32
|
+
const env = options.env ?? process.env;
|
|
33
|
+
const stored = options.server && options.projectId && options.apiKey
|
|
34
|
+
? undefined
|
|
35
|
+
: await loadConnection(options.name, options);
|
|
36
|
+
const candidate = {
|
|
37
|
+
server: options.server ?? env.AGENTCERT_BASE_URL ?? stored?.server,
|
|
38
|
+
projectId: options.projectId ?? env.AGENTCERT_PROJECT_ID ?? stored?.projectId,
|
|
39
|
+
apiKey: options.apiKey ?? env.AGENTCERT_API_KEY ?? stored?.apiKey,
|
|
40
|
+
};
|
|
41
|
+
const missing = Object.entries(candidate).filter(([, value]) => !value).map(([key]) => key);
|
|
42
|
+
if (missing.length > 0) {
|
|
43
|
+
throw new Error(`Hosted connection is incomplete (${missing.join(", ")} missing). Run \`npx agentcert connect\` or set AGENTCERT_BASE_URL, AGENTCERT_PROJECT_ID, and AGENTCERT_API_KEY.`);
|
|
44
|
+
}
|
|
45
|
+
return validateConnection(candidate);
|
|
46
|
+
}
|
|
47
|
+
export function credentialsPath(options = {}) {
|
|
48
|
+
const configHome = options.configHome ?? process.env.AGENTCERT_CONFIG_HOME ?? join(homedir(), ".agentcert");
|
|
49
|
+
return join(configHome, "credentials.json");
|
|
50
|
+
}
|
|
51
|
+
function validateConnection(input) {
|
|
52
|
+
const projectId = input.projectId.trim();
|
|
53
|
+
const apiKey = input.apiKey.trim();
|
|
54
|
+
if (!projectId)
|
|
55
|
+
throw new Error("AgentCert project ID is required.");
|
|
56
|
+
if (!apiKey.startsWith("ac_live_"))
|
|
57
|
+
throw new Error("AgentCert API key must start with ac_live_.");
|
|
58
|
+
return { server: normalizeServer(input.server), projectId, apiKey };
|
|
59
|
+
}
|
|
60
|
+
function normalizeServer(value) {
|
|
61
|
+
let url;
|
|
62
|
+
try {
|
|
63
|
+
url = new URL(value.trim());
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
throw new Error("AgentCert server must be a valid HTTP or HTTPS URL.");
|
|
67
|
+
}
|
|
68
|
+
const loopback = new Set(["localhost", "127.0.0.1", "[::1]"]).has(url.hostname);
|
|
69
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
|
|
70
|
+
throw new Error("AgentCert server must use HTTPS. Plain HTTP is allowed only for localhost development.");
|
|
71
|
+
}
|
|
72
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
73
|
+
throw new Error("AgentCert server URL cannot contain credentials, query parameters, or a fragment.");
|
|
74
|
+
}
|
|
75
|
+
return url.toString().replace(/\/$/, "");
|
|
76
|
+
}
|
|
77
|
+
function validateConnectionName(value) {
|
|
78
|
+
const name = value.trim();
|
|
79
|
+
if (!/^[A-Za-z0-9._-]{1,64}$/.test(name)) {
|
|
80
|
+
throw new Error("Connection name must contain 1-64 letters, numbers, dots, underscores, or hyphens.");
|
|
81
|
+
}
|
|
82
|
+
return name;
|
|
83
|
+
}
|
|
84
|
+
async function readCredentialFile(path) {
|
|
85
|
+
let raw;
|
|
86
|
+
try {
|
|
87
|
+
raw = await readFile(path, "utf8");
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (error.code === "ENOENT")
|
|
91
|
+
return undefined;
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
let value;
|
|
95
|
+
try {
|
|
96
|
+
value = JSON.parse(raw);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new Error(`AgentCert credentials file is not valid JSON: ${path}`);
|
|
100
|
+
}
|
|
101
|
+
if (!isCredentialFile(value)) {
|
|
102
|
+
throw new Error(`AgentCert credentials file has an unsupported format: ${path}`);
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
function isCredentialFile(value) {
|
|
107
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
108
|
+
return false;
|
|
109
|
+
const file = value;
|
|
110
|
+
return file.schemaVersion === "agentcert.credentials.v1"
|
|
111
|
+
&& typeof file.defaultConnection === "string"
|
|
112
|
+
&& Boolean(file.connections)
|
|
113
|
+
&& typeof file.connections === "object"
|
|
114
|
+
&& !Array.isArray(file.connections);
|
|
115
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from "./bundle.js";
|
|
2
2
|
export * from "./corpus.js";
|
|
3
3
|
export * from "./corpus-store.js";
|
|
4
|
+
export * from "./control-plane.js";
|
|
5
|
+
export * from "./credentials.js";
|
|
4
6
|
export * from "./failure-review.js";
|
|
5
7
|
export * from "./evidence-signing.js";
|
|
6
8
|
export * from "./local-server.js";
|