agentcert 0.2.1 → 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 CHANGED
@@ -37,9 +37,7 @@ Default outputs:
37
37
  Push the validated evidence bundle into a hosted AgentCert project:
38
38
 
39
39
  ```bash
40
- export AGENTCERT_BASE_URL="https://agentcert.example.com"
41
- export AGENTCERT_PROJECT_ID="your-project-id"
42
- export AGENTCERT_API_KEY="ac_live_..."
40
+ npx agentcert connect --server https://agentcert-control-plane.onrender.com --project your-project-id
43
41
  npx agentcert push --evidence .agentcert/latest/agentcert-evidence.json
44
42
  ```
45
43
 
package/dist/cli.js CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3
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";
9
- import { pushEvidenceToControlPlane } from "./control-plane.js";
10
+ import { pushEvidenceToControlPlane, verifyControlPlaneConnection } from "./control-plane.js";
11
+ import { DEFAULT_AGENTCERT_SERVER, resolveConnection, saveConnection } from "./credentials.js";
10
12
  import { applyFailureReviews, appendFailureReview, createFailureReview, findFailurePattern, parseFailureReviewStatus, parseFailureType, parseReviewConfidence, readFailureReviews, } from "./failure-review.js";
11
13
  import { buildMonitorSnapshot, writeMonitorSnapshot } from "./monitor.js";
12
14
  import { normalizeMcpBenchResult, normalizeOnegentAuditPacket, normalizeTripwireResult } from "./normalizers.js";
@@ -82,6 +84,33 @@ Next:
82
84
  npx agentcert run --tripwire .tripwire/latest/tripwire-result.json --subject ${JSON.stringify(subject)} --fail-on-verdict
83
85
  `);
84
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
+ }
85
114
  else if (command === "report") {
86
115
  const config = await loadConfig(readFlag("--config"));
87
116
  const subject = readFlag("--subject") ?? config?.subject.name ?? "agentcert-subject";
@@ -481,6 +510,7 @@ else if (command === "validate") {
481
510
  else {
482
511
  process.stdout.write(`Usage:
483
512
  agentcert init --subject my-browser-agent
513
+ agentcert connect --server https://agentcert-control-plane.onrender.com --project <project-id>
484
514
  agentcert init --out agentcert.config.json --tripwire-config tripwire.yml --force
485
515
  agentcert init --subject my-browser-agent --github-action
486
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
@@ -515,16 +545,16 @@ async function loadConfig(path) {
515
545
  return (await readJson(path));
516
546
  }
517
547
  async function pushHostedEvidence(bundle, bytes, fileName) {
518
- const baseUrl = readFlag("--server") ?? process.env.AGENTCERT_BASE_URL;
519
- const projectId = readFlag("--project") ?? process.env.AGENTCERT_PROJECT_ID;
520
- const apiKey = readFlag("--api-key") ?? process.env.AGENTCERT_API_KEY;
521
- if (!baseUrl || !projectId || !apiKey) {
522
- throw new Error("Hosted push requires AGENTCERT_BASE_URL, AGENTCERT_PROJECT_ID, and AGENTCERT_API_KEY (or --server, --project, and --api-key).");
523
- }
548
+ const connection = await resolveConnection({
549
+ name: readFlag("--connection"),
550
+ server: readFlag("--server"),
551
+ projectId: readFlag("--project"),
552
+ apiKey: readFlag("--api-key"),
553
+ });
524
554
  const result = await pushEvidenceToControlPlane({
525
- baseUrl,
526
- projectId,
527
- apiKey,
555
+ baseUrl: connection.server,
556
+ projectId: connection.projectId,
557
+ apiKey: connection.apiKey,
528
558
  bundle,
529
559
  evidenceBytes: bytes,
530
560
  fileName,
@@ -532,6 +562,60 @@ async function pushHostedEvidence(bundle, bytes, fileName) {
532
562
  });
533
563
  process.stdout.write(`Hosted run: ${result.runId}\nHosted evidence: ${result.evidenceId}\n`);
534
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
+ }
535
619
  async function readJson(path) {
536
620
  const raw = await readFile(resolve(path), "utf8");
537
621
  return JSON.parse(raw);
@@ -1,3 +1,37 @@
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
+ }
1
35
  export async function pushEvidenceToControlPlane(options) {
2
36
  const baseUrl = options.baseUrl.replace(/\/$/, "");
3
37
  if (!baseUrl || !options.projectId || !options.apiKey) {
@@ -88,7 +122,7 @@ async function requestJson(request, url, init) {
88
122
  }
89
123
  catch (error) {
90
124
  const message = error instanceof Error ? error.message : String(error);
91
- throw new Error(`AgentCert control plane request failed: ${message}`);
125
+ throw new ControlPlaneRequestError(`AgentCert control plane request failed: ${message}`);
92
126
  }
93
127
  const text = await response.text();
94
128
  let value = {};
@@ -98,12 +132,12 @@ async function requestJson(request, url, init) {
98
132
  }
99
133
  catch {
100
134
  if (!response.ok)
101
- throw new Error(`AgentCert control plane returned HTTP ${response.status}.`);
102
- throw new Error("AgentCert control plane returned invalid JSON.");
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);
103
137
  }
104
138
  }
105
139
  if (!response.ok) {
106
- throw new Error(typeof value.error === "string" ? value.error : `AgentCert control plane returned HTTP ${response.status}.`);
140
+ throw new ControlPlaneRequestError(typeof value.error === "string" ? value.error : `AgentCert control plane returned HTTP ${response.status}.`, response.status);
107
141
  }
108
142
  return value;
109
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
@@ -2,6 +2,7 @@ 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 "./credentials.js";
5
6
  export * from "./failure-review.js";
6
7
  export * from "./evidence-signing.js";
7
8
  export * from "./local-server.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentcert",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Assurance release gates, runtime evidence, and regression CI for AI agents.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",