@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 +186 -3
- package/enterprise-identity.js +20 -0
- package/installer.js +91 -1
- package/package.json +1 -1
- package/production-preflight.js +13 -1
- package/release-verifier.js +5 -0
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
|
|
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
|
}
|
package/enterprise-identity.js
CHANGED
|
@@ -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
package/production-preflight.js
CHANGED
|
@@ -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
|
|
package/release-verifier.js
CHANGED
|
@@ -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");
|