@twin3-ai/agent-id 0.3.13 → 0.3.14

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/bin/agent-id.js CHANGED
@@ -3,7 +3,7 @@ const { configuredEndpoint, TRUSTED_ISSUER_KEY_SHA256 } = require("../runtime-co
3
3
  const endpoint = configuredEndpoint();
4
4
  const fs = require("fs");
5
5
  const path = require("path");
6
- const { buildInstallPlan, preflightInstallPlan, applyInstallPlan, resumeInstall, uninstallInstall, enterpriseInstallStatus, prepareEnterpriseSecretDirectory, writeEnterpriseBootstrapCredential, readEnterpriseBootstrapCredential, replaceEnterpriseBootstrapCredential, removeEnterpriseBootstrapCredential, writeEnterpriseEnrollmentState } = require("../installer.js");
6
+ const { buildInstallPlan, preflightInstallPlan, applyInstallPlan, resumeInstall, uninstallInstall, enterpriseInstallStatus, prepareEnterpriseSecretDirectory, writeEnterpriseVerificationCapability, readEnterpriseVerificationCapability, removeEnterpriseVerificationCapability, writeEnterpriseBootstrapCredential, readEnterpriseBootstrapCredential, replaceEnterpriseBootstrapCredential, removeEnterpriseBootstrapCredential, writeEnterpriseEnrollmentState } = require("../installer.js");
7
7
  const { buildRepositoryPatch, applyRepositoryPatch, rollbackRepositoryPatch } = require("../repository-connector.js");
8
8
  const { createRepositoryConnector } = require("../repository-connector.js");
9
9
  const { generateSiteAgentIdentity, createEnterpriseSession } = require("../enterprise-identity.js");
@@ -67,6 +67,42 @@ function readCsvFlag(argv, flag) {
67
67
  return value ? value.split(",").map((item) => item.trim()).filter(Boolean) : undefined;
68
68
  }
69
69
 
70
+ function readSecretFileOrEnv(argv, flag, envName, label) {
71
+ const file = readFlagValue(argv, flag);
72
+ const value = file
73
+ ? fs.readFileSync(path.resolve(file), "utf8").trim()
74
+ : String(process.env[envName] || "").trim();
75
+ if (!value) throw new Error(`${label} is required via ${flag} or ${envName}`);
76
+ return value;
77
+ }
78
+
79
+ function reserveSecretOutput(target, label) {
80
+ if (!target) throw new Error(`${label} requires an explicit output file`);
81
+ const absolute = path.resolve(target);
82
+ const parent = path.dirname(absolute);
83
+ fs.mkdirSync(parent, { recursive: true, mode: 0o700 });
84
+ const descriptor = fs.openSync(absolute, "wx", 0o600);
85
+ fs.closeSync(descriptor);
86
+ fs.chmodSync(absolute, 0o600);
87
+ let committed = false;
88
+ return {
89
+ path: absolute,
90
+ commit(value) {
91
+ fs.writeFileSync(absolute, String(value), { encoding: "utf8", mode: 0o600 });
92
+ fs.chmodSync(absolute, 0o600);
93
+ committed = true;
94
+ },
95
+ abort() {
96
+ if (!committed) fs.rmSync(absolute, { force: true });
97
+ },
98
+ };
99
+ }
100
+
101
+ function printSiteKeyLifecycle(value, lines) {
102
+ if (process.argv.includes("--json")) return print(value);
103
+ console.log([...lines, "Use --json for the complete non-secret receipt."].join("\n"));
104
+ }
105
+
70
106
  async function enterpriseSessionForProject(projectRoot) {
71
107
  const root = path.resolve(projectRoot || process.cwd());
72
108
  const state = JSON.parse(fs.readFileSync(path.join(root, ".agent-id", "enterprise-state.json"), "utf8"));
@@ -183,6 +219,7 @@ function enrollmentArtifactSnapshot(projectRoot, publicRoot) {
183
219
  const proof = resolveProofTarget({ projectRoot: project, publicRoot }).target;
184
220
  const files = [
185
221
  path.join(project, ".agent-id", "secrets", "enterprise-ed25519.pem"),
222
+ path.join(project, ".agent-id", "secrets", "domain-verification-capability.json"),
186
223
  path.join(project, ".agent-id", "secrets", ".gitignore"),
187
224
  path.join(project, ".agent-id", "enterprise-state.json"),
188
225
  proof,
@@ -261,6 +298,13 @@ async function createEnterpriseEnrollment({ siteUrl, projectRoot, publicRoot, en
261
298
  });
262
299
  const challenge = response && response.challenge;
263
300
  if (!challenge || !challenge.challenge_id) throw new Error(response && response.error || "enterprise_enrollment_failed");
301
+ const verificationCapability = String(response && response.verification_capability || "");
302
+ if (!verificationCapability.startsWith("dvc_")) throw new Error("enterprise_verification_capability_missing");
303
+ const capabilityReceipt = await writeEnterpriseVerificationCapability(
304
+ projectRoot,
305
+ challenge.challenge_id,
306
+ verificationCapability,
307
+ );
264
308
  const domainProof = method === "well_known" ? buildWellKnownProofDocument(challenge) : null;
265
309
  await writeEnterpriseEnrollmentState(projectRoot, {
266
310
  status: "challenge_pending",
@@ -288,6 +332,11 @@ async function createEnterpriseEnrollment({ siteUrl, projectRoot, publicRoot, en
288
332
  challenge,
289
333
  domain_proof: domainProof,
290
334
  proof_receipt: proofReceipt,
335
+ verification_capability: {
336
+ sealed_locally: true,
337
+ encrypted_at_rest: capabilityReceipt.encrypted_at_rest,
338
+ mode: capabilityReceipt.mode,
339
+ },
291
340
  identity: {
292
341
  algorithm: identity.algorithm,
293
342
  public_key_pem: identity.public_key_pem,
@@ -1357,6 +1406,9 @@ async function runCli(argv) {
1357
1406
  " agent-id google-marketplace-runtime-ledger [receipt-json-file]",
1358
1407
  " agent-id google-marketplace-runtime-evidence [url|evidence-json-file]",
1359
1408
  " agent-id google-marketplace-test-billing",
1409
+ " agent-id google-operations-evidence <source-manifest.json> --operator-token-file <oidc-token-file>",
1410
+ " agent-id google-a2a-conformance --operator-token-file <oidc-token-file>",
1411
+ " agent-id google-marketplace-submit-ready <evidence-manifest.json> --operator-token-file <oidc-token-file>",
1360
1412
  " agent-id google-marketplace-agent-card-validate [evidence-json-file]",
1361
1413
  " agent-id google-marketplace-agent-card-gcs-pack [evidence-json-file]",
1362
1414
  " agent-id google-marketplace-product-details-review [evidence-json-file]",
@@ -1461,6 +1513,9 @@ async function runCli(argv) {
1461
1513
  " agent-id job-submit <job-id>",
1462
1514
  " agent-id job-evaluate <job-id>",
1463
1515
  " agent-id register <url>",
1516
+ " agent-id site-key-recovery-start <url> --capability-out <secret-file>",
1517
+ " agent-id site-key-recovery-complete <url> --challenge <id> --capability-file <secret-file> --key-out <secret-file>",
1518
+ " agent-id site-key-rotate <url> --key-file <secret-file> --key-out <secret-file> --approve",
1464
1519
  " agent-id install-guide <url>",
1465
1520
  " agent-id install <url> [--modules identity,audit,measurement,optimization,automation] [--grant identity_publish,public_read,telemetry_submit,insights_read,ai_files_write] [--project-root <dir>] [--public-root <dir>] [--apply]",
1466
1521
  " agent-id install-resume [--project-root <dir>]",
@@ -1930,6 +1985,49 @@ async function runCli(argv) {
1930
1985
  }
1931
1986
  return print(await get("/api/agent/google_marketplace_test_billing_gate"));
1932
1987
  }
1988
+ if (cmd === "google-operations-evidence") {
1989
+ if (!url || String(url).startsWith("--")) throw new Error("source manifest JSON file is required");
1990
+ const payload = JSON.parse(fs.readFileSync(path.resolve(url), "utf8"));
1991
+ const token = readSecretFileOrEnv(
1992
+ argv,
1993
+ "--operator-token-file",
1994
+ "AGENT_ID_OPERATOR_ID_TOKEN",
1995
+ "Google operator OIDC token",
1996
+ );
1997
+ return print(await post(
1998
+ "/api/agent/google_operations_evidence_collect",
1999
+ payload,
2000
+ { authorization: `Bearer ${token}` },
2001
+ ));
2002
+ }
2003
+ if (cmd === "google-a2a-conformance") {
2004
+ const token = readSecretFileOrEnv(
2005
+ argv,
2006
+ "--operator-token-file",
2007
+ "AGENT_ID_OPERATOR_ID_TOKEN",
2008
+ "Google operator OIDC token",
2009
+ );
2010
+ return print(await post(
2011
+ "/api/agent/google_a2a_local_conformance",
2012
+ {},
2013
+ { authorization: `Bearer ${token}` },
2014
+ ));
2015
+ }
2016
+ if (cmd === "google-marketplace-submit-ready") {
2017
+ if (!url || String(url).startsWith("--")) throw new Error("evidence manifest JSON file is required");
2018
+ const payload = JSON.parse(fs.readFileSync(path.resolve(url), "utf8"));
2019
+ const token = readSecretFileOrEnv(
2020
+ argv,
2021
+ "--operator-token-file",
2022
+ "AGENT_ID_OPERATOR_ID_TOKEN",
2023
+ "Google operator OIDC token",
2024
+ );
2025
+ return print(await post(
2026
+ "/api/agent/google_marketplace_submit_ready_decision",
2027
+ payload,
2028
+ { authorization: `Bearer ${token}` },
2029
+ ));
2030
+ }
1933
2031
  if (cmd === "google-marketplace-agent-card-validate") {
1934
2032
  const evidenceFile = url && !String(url).startsWith("--") ? url : "";
1935
2033
  if (evidenceFile) {
@@ -2625,6 +2723,85 @@ async function runCli(argv) {
2625
2723
  }));
2626
2724
  }
2627
2725
  if (cmd === "checkout") return print(await post("/api/agent/checkout", { plan: url }));
2726
+ if (cmd === "site-key-recovery-start") {
2727
+ if (!url) throw new Error("site-key-recovery-start requires a site URL");
2728
+ const output = reserveSecretOutput(readFlagValue(argv, "--capability-out"), "site-key-recovery-start");
2729
+ try {
2730
+ const result = await post("/api/site_agents/key_recovery/start", { url });
2731
+ if (!result.ok || !result.recovery_capability) throw new Error(result.error || "site_key_recovery_start_failed");
2732
+ output.commit(result.recovery_capability);
2733
+ const safe = { ...result, recovery_capability: undefined, recovery_capability_file: output.path };
2734
+ delete safe.recovery_capability;
2735
+ return printSiteKeyLifecycle(safe, [
2736
+ "Site Agent Key recovery: CHALLENGE CREATED",
2737
+ `Site: ${safe.host}`,
2738
+ `Challenge: ${safe.challenge_id}`,
2739
+ `DNS TXT name: ${safe.dns_txt.name}`,
2740
+ `DNS TXT value: ${safe.dns_txt.value}`,
2741
+ `Private capability saved: ${output.path}`,
2742
+ `Complete: ${pinnedCliCommand("site-key-recovery-complete", [url, "--challenge", safe.challenge_id, "--capability-file", output.path, "--key-out", "<new-secret-file>"])}`,
2743
+ ]);
2744
+ } catch (error) {
2745
+ output.abort();
2746
+ throw error;
2747
+ }
2748
+ }
2749
+ if (cmd === "site-key-recovery-complete") {
2750
+ if (!url) throw new Error("site-key-recovery-complete requires a site URL");
2751
+ const challengeId = readFlagValue(argv, "--challenge");
2752
+ if (!challengeId) throw new Error("site-key-recovery-complete requires --challenge");
2753
+ const capability = readSecretFileOrEnv(
2754
+ argv, "--capability-file", "AGENT_ID_RECOVERY_CAPABILITY", "recovery capability",
2755
+ );
2756
+ const output = reserveSecretOutput(readFlagValue(argv, "--key-out"), "site-key-recovery-complete");
2757
+ try {
2758
+ const result = await post("/api/site_agents/key_recovery/complete", {
2759
+ url,
2760
+ challenge_id: challengeId,
2761
+ recovery_capability: capability,
2762
+ });
2763
+ if (!result.ok || !result.agent_key) throw new Error(result.error || "site_key_recovery_complete_failed");
2764
+ output.commit(result.agent_key);
2765
+ const safe = { ...result, agent_key: undefined, agent_key_file: output.path };
2766
+ delete safe.agent_key;
2767
+ return printSiteKeyLifecycle(safe, [
2768
+ "Site Agent Key recovery: COMPLETE",
2769
+ `Site: ${safe.host}`,
2770
+ `New key saved: ${output.path}`,
2771
+ `Receipt: ${(safe.receipt || {}).receipt_id || "recorded"}`,
2772
+ `Old credentials: ${safe.old_credential_status}`,
2773
+ ]);
2774
+ } catch (error) {
2775
+ output.abort();
2776
+ throw error;
2777
+ }
2778
+ }
2779
+ if (cmd === "site-key-rotate") {
2780
+ if (!url) throw new Error("site-key-rotate requires a site URL");
2781
+ if (!argv.includes("--approve")) throw new Error("site-key-rotate requires --approve");
2782
+ const currentKey = readSecretFileOrEnv(argv, "--key-file", "AEO_AGENT_KEY", "current Site Agent Key");
2783
+ const output = reserveSecretOutput(readFlagValue(argv, "--key-out"), "site-key-rotate");
2784
+ try {
2785
+ const result = await post(
2786
+ "/api/site_agents/key_rotate",
2787
+ { url, approved: true },
2788
+ { Authorization: `Bearer ${currentKey}` },
2789
+ );
2790
+ if (!result.ok || !result.agent_key) throw new Error(result.error || "site_key_rotation_failed");
2791
+ output.commit(result.agent_key);
2792
+ const safe = { ...result, agent_key: undefined, agent_key_file: output.path };
2793
+ delete safe.agent_key;
2794
+ return printSiteKeyLifecycle(safe, [
2795
+ "Site Agent Key rotation: COMPLETE",
2796
+ `Site: ${safe.host}`,
2797
+ `New key saved: ${output.path}`,
2798
+ `Receipt: ${(safe.receipt || {}).receipt_id || "recorded"}`,
2799
+ ]);
2800
+ } catch (error) {
2801
+ output.abort();
2802
+ throw error;
2803
+ }
2804
+ }
2628
2805
  if (cmd === "register") return print(await post("/api/site_agents/register", { url }));
2629
2806
  if (cmd === "install-guide") return print(await get(url ? `/api/site_agents/install_guide?url=${encodeURIComponent(url)}` : "/api/site_agents/install_guide"));
2630
2807
  if (cmd === "install") {
@@ -2648,8 +2825,9 @@ async function runCli(argv) {
2648
2825
  }
2649
2826
  const publicProofPath = path.posix.join(...relativePublicRoot.split(path.sep).filter(Boolean), ".well-known", "agent-id.json");
2650
2827
  const privateKeyPath = ".agent-id/secrets/enterprise-ed25519.pem";
2828
+ const capabilityPath = ".agent-id/secrets/domain-verification-capability.json";
2651
2829
  const managedFiles = detectedPlan.files.map(({ path: filePath, mode, sha256 }) => ({ path: filePath, mode, sha256 }));
2652
- const plannedWrites = [...new Set([privateKeyPath, ".agent-id/enterprise-state.json", publicProofPath, ...managedFiles.map((item) => item.path)])];
2830
+ const plannedWrites = [...new Set([privateKeyPath, capabilityPath, ".agent-id/enterprise-state.json", publicProofPath, ...managedFiles.map((item) => item.path)])];
2653
2831
  const plan = {
2654
2832
  schema: "agentx-signed-install-plan-v1",
2655
2833
  site_url: detectedPlan.site_url,
@@ -2803,7 +2981,11 @@ async function runCli(argv) {
2803
2981
  next_command: pinnedCliCommand("enterprise-identity-publish", ["--project-root", projectRoot, "--public-root", projectRoot])
2804
2982
  });
2805
2983
  }
2806
- const response = await post("/api/site_agents/v1/challenges/verify", { challenge_id: state.challenge_id });
2984
+ const sealedCapability = await readEnterpriseVerificationCapability(projectRoot, state.challenge_id);
2985
+ const response = await post("/api/site_agents/v1/challenges/verify", {
2986
+ challenge_id: state.challenge_id,
2987
+ verification_capability: sealedCapability.capability,
2988
+ });
2807
2989
  if (!response || response.ok !== true) return print(response || { ok: false, error: "domain_verification_failed" });
2808
2990
  let secretReceipt;
2809
2991
  try {
@@ -2812,6 +2994,7 @@ async function runCli(argv) {
2812
2994
  response.credential,
2813
2995
  response.bootstrap_secret
2814
2996
  );
2997
+ await removeEnterpriseVerificationCapability(projectRoot, state.challenge_id);
2815
2998
  } catch (_error) {
2816
2999
  throw new Error(`enterprise_credential_delivery_failed; request a new single-use challenge with: ${pinnedCliCommand("enterprise-enroll", [state.site_url, "--environment", state.environment, "--method", "well_known", "--project-root", projectRoot])}`);
2817
3000
  }
@@ -159,6 +159,14 @@ function createEnterpriseSession({ serviceOrigin, siteUrl, environment, credenti
159
159
  return responseJson(await fetcher(`${origin}/api/site_agents/v1/dashboard${pathname}`, request));
160
160
  }
161
161
 
162
+ async function decisionRequest(pathname = "", { method = "GET", body = null } = {}) {
163
+ const scope = method === "GET" ? "insights:read" : "evidence:write";
164
+ const accessToken = await session.getAccessToken([scope]);
165
+ const request = { method, headers: { "content-type": "application/json", authorization: `Bearer ${accessToken}` } };
166
+ if (body !== null) request.body = JSON.stringify(body);
167
+ return responseJson(await fetcher(`${origin}/api/site_agents/v1/decision${pathname}`, request));
168
+ }
169
+
162
170
  const session = {
163
171
  schema: "agentx-enterprise-session-v1",
164
172
  site_url: site.url,
@@ -211,6 +219,18 @@ function createEnterpriseSession({ serviceOrigin, siteUrl, environment, credenti
211
219
  async createDashboardPairing() {
212
220
  return dashboardRequest("/pairings", { method: "POST", body: {} });
213
221
  },
222
+ async getDecisionIntelligence() {
223
+ return decisionRequest();
224
+ },
225
+ async collectPublicAudit() {
226
+ return decisionRequest("/observations", { method: "POST", body: { kind: "public_audit" } });
227
+ },
228
+ async submitDecisionEvidence(kind, evidence) {
229
+ const normalizedKind = String(kind || "").trim();
230
+ if (!["connector", "provider_decision"].includes(normalizedKind)) throw enterpriseError("decision_evidence_kind_unsupported");
231
+ if (!evidence || typeof evidence !== "object" || Array.isArray(evidence)) throw enterpriseError("decision_evidence_mapping_required");
232
+ return decisionRequest("/observations", { method: "POST", body: { kind: normalizedKind, evidence } });
233
+ },
214
234
  async listTasks() {
215
235
  return taskRequest("");
216
236
  },
package/installer.js CHANGED
@@ -17,6 +17,8 @@ const TRANSACTION_JOURNAL = `${TRANSACTION_DIR}/active.json`;
17
17
  const TRANSACTION_LOCK = `${TRANSACTION_DIR}/active.lock`;
18
18
  const COMMIT_MARKER = `${MANAGED_DIR}/install-commit.json`;
19
19
  const SECRET_IGNORE = "*\n!.gitignore\n";
20
+ const VERIFICATION_CAPABILITY_SCHEMA = "agentx-domain-verification-capability-sealed-v1";
21
+ const VERIFICATION_CAPABILITY_AAD = "agent-id-domain-verification-capability-local-wrap-v1";
20
22
  const INSTALL_MODULES = Object.freeze({
21
23
  identity: Object.freeze({ permission: "identity_publish", label: "Domain-bound Agent identity" }),
22
24
  audit: Object.freeze({ permission: "public_read", label: "SEO / AEO / GEO / AGO public audit" }),
@@ -641,10 +643,95 @@ async function prepareEnterpriseSecretDirectory(projectRoot = process.cwd()) {
641
643
  return {
642
644
  secret_directory: secretDirectory,
643
645
  key_path: path.join(secretDirectory, "enterprise-ed25519.pem"),
644
- credential_path: path.join(secretDirectory, "enterprise-credential.json")
646
+ credential_path: path.join(secretDirectory, "enterprise-credential.json"),
647
+ verification_capability_path: path.join(secretDirectory, "domain-verification-capability.json")
645
648
  };
646
649
  }
647
650
 
651
+ async function enterpriseCapabilityWrapKey(paths) {
652
+ const stat = await fs.lstat(paths.key_path).catch((error) => {
653
+ if (error.code === "ENOENT") throw installError("ENTERPRISE_PRIVATE_KEY_MISSING", "Enterprise private key is required to protect the verification capability.");
654
+ throw error;
655
+ });
656
+ if (stat.isSymbolicLink() || !stat.isFile() || (stat.mode & 0o777) !== 0o600) {
657
+ throw installError("ENTERPRISE_PRIVATE_KEY_INVALID", "Enterprise private key must be a regular 0600 file.");
658
+ }
659
+ const privateKey = await fs.readFile(paths.key_path);
660
+ return crypto.createHash("sha256").update(VERIFICATION_CAPABILITY_AAD).update("\0").update(privateKey).digest();
661
+ }
662
+
663
+ async function writeEnterpriseVerificationCapability(projectRoot, challengeId, capability) {
664
+ const challenge = String(challengeId || "");
665
+ const secret = String(capability || "");
666
+ if (!challenge.startsWith("dch_") || !secret.startsWith("dvc_") || secret.length < 48) {
667
+ throw installError("INVALID_VERIFICATION_CAPABILITY", "A complete one-time verification capability is required.");
668
+ }
669
+ const paths = await prepareEnterpriseSecretDirectory(projectRoot);
670
+ const key = await enterpriseCapabilityWrapKey(paths);
671
+ const iv = crypto.randomBytes(12);
672
+ const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
673
+ cipher.setAAD(Buffer.from(`${VERIFICATION_CAPABILITY_AAD}\0${challenge}`, "utf8"));
674
+ const ciphertext = Buffer.concat([cipher.update(secret, "utf8"), cipher.final()]);
675
+ const sealed = {
676
+ schema: VERIFICATION_CAPABILITY_SCHEMA,
677
+ challenge_id: challenge,
678
+ algorithm: "aes-256-gcm",
679
+ iv: iv.toString("base64url"),
680
+ ciphertext: ciphertext.toString("base64url"),
681
+ auth_tag: cipher.getAuthTag().toString("base64url"),
682
+ };
683
+ const serialized = JSON.stringify(sealed, null, 2) + "\n";
684
+ if (serialized.includes(secret)) {
685
+ throw installError("VERIFICATION_CAPABILITY_SEAL_FAILED", "The verification capability must not be persisted in plaintext.");
686
+ }
687
+ await writeAtomic(paths.verification_capability_path, serialized, "private");
688
+ return {
689
+ schema: "agentx-domain-verification-capability-write-receipt-v1",
690
+ challenge_id: challenge,
691
+ path: paths.verification_capability_path,
692
+ encrypted_at_rest: true,
693
+ mode: "0600",
694
+ };
695
+ }
696
+
697
+ async function readEnterpriseVerificationCapability(projectRoot, expectedChallengeId) {
698
+ const challenge = String(expectedChallengeId || "");
699
+ const paths = await prepareEnterpriseSecretDirectory(projectRoot);
700
+ let stat;
701
+ try { stat = await fs.lstat(paths.verification_capability_path); }
702
+ catch (error) {
703
+ if (error.code === "ENOENT") throw installError("VERIFICATION_CAPABILITY_MISSING", "This enrollment predates private verification capabilities; run enterprise-enroll again.");
704
+ throw error;
705
+ }
706
+ if (stat.isSymbolicLink() || !stat.isFile() || (stat.mode & 0o777) !== 0o600) {
707
+ throw installError("VERIFICATION_CAPABILITY_FILE_INVALID", "The sealed verification capability must be a regular 0600 file.");
708
+ }
709
+ const sealed = await readJson(paths.verification_capability_path);
710
+ if (!sealed || sealed.schema !== VERIFICATION_CAPABILITY_SCHEMA || sealed.algorithm !== "aes-256-gcm" || sealed.challenge_id !== challenge) {
711
+ throw installError("VERIFICATION_CAPABILITY_FILE_INVALID", "The sealed verification capability does not match this enrollment.");
712
+ }
713
+ try {
714
+ const key = await enterpriseCapabilityWrapKey(paths);
715
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, Buffer.from(sealed.iv, "base64url"));
716
+ decipher.setAAD(Buffer.from(`${VERIFICATION_CAPABILITY_AAD}\0${challenge}`, "utf8"));
717
+ decipher.setAuthTag(Buffer.from(sealed.auth_tag, "base64url"));
718
+ const capability = Buffer.concat([
719
+ decipher.update(Buffer.from(sealed.ciphertext, "base64url")),
720
+ decipher.final(),
721
+ ]).toString("utf8");
722
+ if (!capability.startsWith("dvc_") || capability.length < 48) throw new Error("invalid capability");
723
+ return { capability, path: paths.verification_capability_path };
724
+ } catch (_error) {
725
+ throw installError("VERIFICATION_CAPABILITY_DECRYPT_FAILED", "The verification capability could not be opened with this site's private key.");
726
+ }
727
+ }
728
+
729
+ async function removeEnterpriseVerificationCapability(projectRoot, expectedChallengeId) {
730
+ const opened = await readEnterpriseVerificationCapability(projectRoot, expectedChallengeId);
731
+ await fs.rm(opened.path, { force: true });
732
+ return { removed: true, challenge_id: String(expectedChallengeId || "") };
733
+ }
734
+
648
735
  async function writeEnterpriseBootstrapCredential(projectRoot, credential, bootstrapSecret) {
649
736
  const details = credential && typeof credential === "object" ? credential : {};
650
737
  const secret = String(bootstrapSecret || "");
@@ -1346,6 +1433,9 @@ module.exports = {
1346
1433
  uninstallInstall,
1347
1434
  enterpriseInstallStatus,
1348
1435
  prepareEnterpriseSecretDirectory,
1436
+ writeEnterpriseVerificationCapability,
1437
+ readEnterpriseVerificationCapability,
1438
+ removeEnterpriseVerificationCapability,
1349
1439
  writeEnterpriseBootstrapCredential,
1350
1440
  replaceEnterpriseBootstrapCredential,
1351
1441
  removeEnterpriseBootstrapCredential,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twin3-ai/agent-id",
3
- "version": "0.3.13",
3
+ "version": "0.3.14",
4
4
  "description": "Domain-bound AgentX portable alias, AEO evidence, and website-Agent integration SDK.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  const net = require("node:net");
4
- const { verifyReleaseManifest, verifyProductionReleaseReceipt, sha256 } = require("./release-verifier.js");
4
+ const { verifyReleaseManifest, verifyProductionReleaseReceipt, canonical, sha256 } = require("./release-verifier.js");
5
5
 
6
6
  const PACKAGE_NAME = "@twin3-ai/agent-id";
7
7
 
@@ -83,6 +83,16 @@ async function runProductionPreflight({ endpoint, packageVersion, expectedIssuer
83
83
  const health = healthResponse.ok && healthResponse.body && healthResponse.body.ok !== false;
84
84
  const agentCard = cardResponse.ok && typeof cardResponse.body?.name === "string" && cardResponse.body.name.length > 0;
85
85
  const installManifest = installResponse.ok && installResponse.body?.schema === "xagent-install-manifest-v1";
86
+ const installRelease = installResponse.body?.release || {};
87
+ const installReleaseBinding = installManifest
88
+ && installRelease.package === manifest?.package?.name
89
+ && installRelease.version === manifest?.package?.version
90
+ && installRelease.integrity === manifest?.package?.integrity
91
+ && installRelease.commit === manifest?.release_commit
92
+ && installRelease.api_origin === manifest?.api_origin
93
+ && installRelease.manifest_hash === manifest?.content_hash
94
+ && canonical(installRelease.compatibility || {}) === canonical(manifest?.compatibility || {})
95
+ && canonical(installRelease.rollback || {}) === canonical(manifest?.rollback || {});
86
96
  const openapi = openapiResponse.ok && openapiResponse.body?.openapi === "3.1.0";
87
97
  const productionReceipt = receiptResponse.ok && receiptResponse.body
88
98
  ? verifyProductionReleaseReceipt({
@@ -116,6 +126,7 @@ async function runProductionPreflight({ endpoint, packageVersion, expectedIssuer
116
126
  health_endpoint: health,
117
127
  agent_card: agentCard,
118
128
  install_manifest: installManifest,
129
+ install_release_binding: installReleaseBinding,
119
130
  openapi,
120
131
  };
121
132
  const blockers = [...release.blockers, ...productionReceipt.blockers];
@@ -128,6 +139,7 @@ async function runProductionPreflight({ endpoint, packageVersion, expectedIssuer
128
139
  if (apiReachable && !health) blockers.push("health_endpoint_unavailable");
129
140
  if (apiReachable && !agentCard) blockers.push("agent_card_unavailable");
130
141
  if (apiReachable && !installManifest) blockers.push("install_manifest_unavailable");
142
+ if (installManifest && !installReleaseBinding) blockers.push("install_manifest_release_truth_mismatch");
131
143
  if (apiReachable && !openapi) blockers.push("openapi_unavailable");
132
144
  if (productionReceipt.ok && !artifactBindings) blockers.push("production_artifact_hash_mismatch");
133
145
 
@@ -47,6 +47,10 @@ function verifyReleaseManifest({ manifest, issuerDescriptor, expectedOrigin, pac
47
47
  const expectedVersionUrl = `https://www.npmjs.com/package/${PACKAGE_NAME}/v/${String(packageVersion || "")}`;
48
48
  if (manifest?.package?.version_url !== expectedVersionUrl) blockers.push("npm_version_url_mismatch");
49
49
  if (manifest?.supply_chain?.npm_provenance !== "not_claimed_private_repository") blockers.push("npm_provenance_claim_mismatch");
50
+ if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(String(manifest?.rollback?.version || ""))) blockers.push("rollback_version_missing");
51
+ if (manifest?.rollback?.version === manifest?.package?.version) blockers.push("rollback_version_must_differ");
52
+ if (!/^sha512-[A-Za-z0-9+/=]{80,120}$/.test(String(manifest?.rollback?.integrity || ""))) blockers.push("rollback_integrity_missing");
53
+ if (manifest?.rollback?.package !== PACKAGE_NAME || manifest?.rollback?.source !== "previous_production_release") blockers.push("rollback_contract_mismatch");
50
54
 
51
55
  const proof = manifest && typeof manifest.proof === "object" ? manifest.proof : {};
52
56
  const unsigned = manifest && typeof manifest === "object"
@@ -89,6 +93,7 @@ function verifyProductionReleaseReceipt({ receipt, issuerDescriptor, releaseMani
89
93
  if (!expected || normalizedOrigin(receipt?.api_origin) !== expected) blockers.push("production_receipt_origin_mismatch");
90
94
  if (receipt?.package?.name !== PACKAGE_NAME || receipt?.package?.version !== String(packageVersion || "")) blockers.push("production_receipt_package_mismatch");
91
95
  if (receipt?.package?.integrity !== releaseManifest?.package?.integrity) blockers.push("production_receipt_integrity_mismatch");
96
+ if (canonical(receipt?.rollback || {}) !== canonical(releaseManifest?.rollback || {})) blockers.push("production_receipt_rollback_mismatch");
92
97
  if (receipt?.source?.release_commit !== releaseManifest?.release_commit) blockers.push("production_receipt_commit_mismatch");
93
98
  if (receipt?.source?.release_manifest_sha256 !== releaseManifest?.content_hash) blockers.push("production_receipt_manifest_hash_mismatch");
94
99
  if (!/^sha256:[0-9a-f]{64}$/.test(String(receipt?.deployment?.image_digest || ""))) blockers.push("production_receipt_image_digest_missing");