@twin3-ai/agent-id 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/agent-id-sync.js +80 -0
- package/bin/agent-id.js +3397 -0
- package/browser-sdk.js +149 -0
- package/cloudflare-worker-adapter.js +100 -0
- package/domain-proof.js +208 -0
- package/enterprise-identity.js +202 -0
- package/installer.js +722 -0
- package/local-policy.js +118 -0
- package/package.json +44 -0
- package/production-preflight.js +123 -0
- package/release-verifier.js +83 -0
- package/repository-connector.js +306 -0
- package/runtime-config.js +10 -0
- package/site-agent.js +1010 -0
- package/sync-service.js +312 -0
- package/task-executor.js +129 -0
- package/telemetry-collector.js +163 -0
- package/trust-verifier.js +206 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
|
|
5
|
+
const CLAIM_SCHEMA = "agentx-trust-claim-v1";
|
|
6
|
+
const EVIDENCE_SCHEMA = "agentx-evidence-attestation-v1";
|
|
7
|
+
const RECEIPT_SCHEMA = "agentx-capability-runtime-receipt-v1";
|
|
8
|
+
const BUNDLE_SCHEMA = "agentx-portable-trust-bundle-v1";
|
|
9
|
+
const RUNTIME_RECEIPT_TTL = 24 * 60 * 60;
|
|
10
|
+
|
|
11
|
+
function sorted(value) {
|
|
12
|
+
if (Array.isArray(value)) return value.map(sorted);
|
|
13
|
+
if (value && typeof value === "object") {
|
|
14
|
+
return Object.fromEntries(Object.keys(value).sort().map(key => [key, sorted(value[key])]));
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function canonical(value) {
|
|
20
|
+
return Buffer.from(JSON.stringify(sorted(value)), "utf8");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function contentHash(value) {
|
|
24
|
+
return `sha256:${crypto.createHash("sha256").update(canonical(value)).digest("hex")}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function without(value, keys) {
|
|
28
|
+
return Object.fromEntries(Object.entries(value || {}).filter(([key]) => !keys.has(key)));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function verifySignature(publicKey, payload, proofValue) {
|
|
32
|
+
try {
|
|
33
|
+
return crypto.verify(null, canonical(payload), publicKey, Buffer.from(String(proofValue || ""), "base64url"));
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function evidencePayload(evidence) {
|
|
40
|
+
return without(evidence, new Set(["proof"]));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function evidenceIdPayload(evidence) {
|
|
44
|
+
return without(evidence, new Set(["evidence_id", "proof"]));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function verifyEvidenceAttestation(evidence, claimSubject, publicKeys, now) {
|
|
48
|
+
if (!["customer_signed", "independent_public", "external_approval"].includes(evidence?.origin)) return [];
|
|
49
|
+
if (evidence?.schema !== EVIDENCE_SCHEMA) return ["evidence_signature_missing"];
|
|
50
|
+
const failures = [];
|
|
51
|
+
if (JSON.stringify(sorted(evidence?.subject || {})) !== JSON.stringify(sorted(claimSubject || {}))) failures.push("evidence_subject_mismatch");
|
|
52
|
+
const verifierId = evidence?.verifier?.id || "";
|
|
53
|
+
if (!verifierId || evidence?.verifier_id !== verifierId) failures.push("evidence_verifier_mismatch");
|
|
54
|
+
const expectedId = `evd_${contentHash(evidenceIdPayload(evidence)).slice(7, 31)}`;
|
|
55
|
+
if (evidence?.evidence_id !== expectedId) failures.push("evidence_id_mismatch");
|
|
56
|
+
const publicKey = publicKeys?.[verifierId];
|
|
57
|
+
if (!publicKey) failures.push("evidence_verifier_key_missing");
|
|
58
|
+
else if (!verifySignature(publicKey, evidencePayload(evidence), evidence?.proof?.proof_value)) failures.push("evidence_signature_invalid");
|
|
59
|
+
const observedAt = Number(evidence?.observed_at || 0);
|
|
60
|
+
if (observedAt <= 0 || observedAt > now) failures.push("evidence_time_invalid");
|
|
61
|
+
return failures;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function evidenceLevel(evidence, verifiedEvidenceIds = []) {
|
|
65
|
+
const rows = Array.isArray(evidence) ? evidence : [];
|
|
66
|
+
const verified = new Set(verifiedEvidenceIds);
|
|
67
|
+
if (rows.some(row => row?.origin === "external_approval" && verified.has(row?.evidence_id))) return "externally_approved";
|
|
68
|
+
if (rows.some(row => row?.origin === "independent_public" && verified.has(row?.evidence_id))) return "independently_verified";
|
|
69
|
+
if (rows.some(row => row?.origin === "customer_signed" && verified.has(row?.evidence_id))) return "customer_attested";
|
|
70
|
+
if (rows.some(row => row?.origin === "issuer_verified")) return "issuer_verified";
|
|
71
|
+
return "declared";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function verifyClaim(claim, publicKeys, now, revocations = [], conflicts = []) {
|
|
75
|
+
const failureCodes = [];
|
|
76
|
+
if (!claim || claim.schema !== CLAIM_SCHEMA) {
|
|
77
|
+
return { status: "blocked", integrity_ok: false, failure_codes: ["invalid_claim_schema"], evidence_level: "declared" };
|
|
78
|
+
}
|
|
79
|
+
const unsigned = without(claim, new Set(["claim_hash", "proof"]));
|
|
80
|
+
if (claim.claim_hash !== contentHash(unsigned)) failureCodes.push("claim_hash_mismatch");
|
|
81
|
+
const issuerId = claim?.issuer?.id || "";
|
|
82
|
+
const publicKey = publicKeys?.[issuerId];
|
|
83
|
+
if (!publicKey) failureCodes.push("issuer_key_missing");
|
|
84
|
+
else if (!verifySignature(publicKey, { ...unsigned, claim_hash: claim.claim_hash }, claim?.proof?.proof_value)) failureCodes.push("claim_signature_invalid");
|
|
85
|
+
const issuedAt = Number(claim.issued_at || 0);
|
|
86
|
+
const expiresAt = Number(claim.expires_at || 0);
|
|
87
|
+
if (issuedAt <= 0 || expiresAt <= issuedAt || issuedAt > now) failureCodes.push("claim_lifetime_invalid");
|
|
88
|
+
if (revocations.some(row => row?.claim_id === claim.claim_id && Number(row?.revoked_at || 0) <= now)) failureCodes.push("claim_revoked");
|
|
89
|
+
if (conflicts.some(row => Array.isArray(row?.claim_ids) && row.claim_ids.includes(claim.claim_id))) failureCodes.push("claim_conflicted");
|
|
90
|
+
const verifiedEvidenceIds = [];
|
|
91
|
+
for (const evidence of claim.evidence || []) {
|
|
92
|
+
const evidenceFailures = verifyEvidenceAttestation(evidence, claim.subject, publicKeys, now);
|
|
93
|
+
if (["independent_public", "external_approval"].includes(evidence?.origin) && evidence?.verifier_id === issuerId) {
|
|
94
|
+
evidenceFailures.push("evidence_verifier_not_independent");
|
|
95
|
+
}
|
|
96
|
+
failureCodes.push(...evidenceFailures);
|
|
97
|
+
if (!evidenceFailures.length && evidence?.schema === EVIDENCE_SCHEMA) verifiedEvidenceIds.push(evidence.evidence_id);
|
|
98
|
+
}
|
|
99
|
+
const blocking = new Set([
|
|
100
|
+
"claim_hash_mismatch", "issuer_key_missing", "claim_signature_invalid", "claim_lifetime_invalid",
|
|
101
|
+
"claim_revoked", "claim_conflicted", "evidence_signature_missing", "evidence_subject_mismatch",
|
|
102
|
+
"evidence_attestation_invalid", "evidence_verifier_mismatch", "evidence_id_mismatch",
|
|
103
|
+
"evidence_verifier_key_missing", "evidence_signature_invalid", "evidence_time_invalid",
|
|
104
|
+
"evidence_verifier_not_independent"
|
|
105
|
+
]);
|
|
106
|
+
let status;
|
|
107
|
+
if (failureCodes.some(code => blocking.has(code))) status = "blocked";
|
|
108
|
+
else if (now >= expiresAt) {
|
|
109
|
+
failureCodes.push("claim_expired");
|
|
110
|
+
status = "review";
|
|
111
|
+
} else {
|
|
112
|
+
const level = evidenceLevel(claim.evidence, verifiedEvidenceIds);
|
|
113
|
+
status = ["independently_verified", "externally_approved"].includes(level) ? "verified" : "observed";
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
claim_id: claim.claim_id,
|
|
117
|
+
claim_type: claim.claim_type,
|
|
118
|
+
status,
|
|
119
|
+
integrity_ok: !failureCodes.some(code => blocking.has(code)),
|
|
120
|
+
evidence_level: evidenceLevel(claim.evidence, verifiedEvidenceIds),
|
|
121
|
+
failure_codes: failureCodes,
|
|
122
|
+
expires_at: expiresAt
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function verifyRuntimeReceipt(receipt, claim, publicKeys, now) {
|
|
127
|
+
const failures = [];
|
|
128
|
+
if (!receipt || receipt.schema !== RECEIPT_SCHEMA) return ["runtime_receipt_schema_invalid"];
|
|
129
|
+
if (receipt.claim_id !== claim.claim_id) return ["runtime_claim_mismatch"];
|
|
130
|
+
if (receipt.endpoint !== claim?.statement?.endpoint) failures.push("runtime_endpoint_mismatch");
|
|
131
|
+
const unsigned = without(receipt, new Set(["receipt_hash", "proof"]));
|
|
132
|
+
if (receipt.receipt_hash !== contentHash(unsigned)) failures.push("runtime_receipt_hash_mismatch");
|
|
133
|
+
const verifierId = receipt?.verifier?.id || "";
|
|
134
|
+
if (verifierId === (claim?.issuer?.id || "")) failures.push("runtime_verifier_not_independent");
|
|
135
|
+
const publicKey = publicKeys?.[verifierId];
|
|
136
|
+
if (!publicKey) failures.push("runtime_verifier_key_missing");
|
|
137
|
+
else if (!verifySignature(publicKey, { ...unsigned, receipt_hash: receipt.receipt_hash }, receipt?.proof?.proof_value)) failures.push("runtime_receipt_signature_invalid");
|
|
138
|
+
const observedAt = Number(receipt.observed_at || 0);
|
|
139
|
+
if (observedAt <= 0 || observedAt > now) failures.push("runtime_receipt_time_invalid");
|
|
140
|
+
else if (now - observedAt > RUNTIME_RECEIPT_TTL) failures.push("runtime_receipt_stale");
|
|
141
|
+
const httpStatus = Number(receipt.http_status || 0);
|
|
142
|
+
if (httpStatus < 200 || httpStatus >= 300) failures.push("runtime_call_failed");
|
|
143
|
+
if (!/^sha256:[0-9a-f]{64}$/.test(receipt.request_hash || "") || !/^sha256:[0-9a-f]{64}$/.test(receipt.response_hash || "")) failures.push("runtime_hash_invalid");
|
|
144
|
+
return failures;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function verifyCapabilityRuntime(claim, receipts, publicKeys, now, revocations = [], conflicts = []) {
|
|
148
|
+
const claimResult = verifyClaim(claim, publicKeys, now, revocations, conflicts);
|
|
149
|
+
if (claimResult.status === "blocked") return claimResult;
|
|
150
|
+
const matching = (receipts || []).filter(row => row?.claim_id === claim.claim_id);
|
|
151
|
+
if (!matching.length) return { ...claimResult, status: "review", failure_codes: [...claimResult.failure_codes, "runtime_receipt_missing"] };
|
|
152
|
+
const checked = matching.map(row => ({ row, failures: verifyRuntimeReceipt(row, claim, publicKeys, now) }));
|
|
153
|
+
const valid = checked.filter(item => item.failures.length === 0).map(item => item.row);
|
|
154
|
+
if (valid.length) {
|
|
155
|
+
const latest = valid.sort((a, b) => Number(b.observed_at) - Number(a.observed_at))[0];
|
|
156
|
+
return { ...claimResult, status: "verified", evidence_level: "independently_verified", failure_codes: [], runtime_receipt_id: latest.receipt_id, runtime_observed_at: latest.observed_at };
|
|
157
|
+
}
|
|
158
|
+
const failures = [...new Set(checked.flatMap(item => item.failures))].sort();
|
|
159
|
+
const blocking = new Set(["runtime_receipt_hash_mismatch", "runtime_receipt_signature_invalid", "runtime_verifier_key_missing", "runtime_verifier_not_independent", "runtime_endpoint_mismatch", "runtime_claim_mismatch"]);
|
|
160
|
+
return { ...claimResult, status: failures.some(code => blocking.has(code)) ? "blocked" : "review", failure_codes: failures };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function verifyPortableBundle(bundle, { now = Math.floor(Date.now() / 1000), requiredClaimTypes = [], revocations = [], conflicts = [] } = {}) {
|
|
164
|
+
const failureCodes = [];
|
|
165
|
+
if (!bundle || bundle.schema !== BUNDLE_SCHEMA) {
|
|
166
|
+
return { verdict: "block", offline_verifiable: false, failure_codes: ["invalid_bundle_schema"], verified_claims: 0 };
|
|
167
|
+
}
|
|
168
|
+
if (!Array.isArray(bundle.claims) || bundle.claims.some(row => !row || typeof row !== "object" || Array.isArray(row))) {
|
|
169
|
+
return { verdict: "block", offline_verifiable: false, failure_codes: ["invalid_bundle_claims"], verified_claims: 0 };
|
|
170
|
+
}
|
|
171
|
+
if (!Array.isArray(bundle.runtime_receipts) || bundle.runtime_receipts.some(row => !row || typeof row !== "object" || Array.isArray(row))) {
|
|
172
|
+
return { verdict: "block", offline_verifiable: false, failure_codes: ["invalid_bundle_receipts"], verified_claims: 0 };
|
|
173
|
+
}
|
|
174
|
+
if (!bundle.public_keys || typeof bundle.public_keys !== "object" || Array.isArray(bundle.public_keys)) {
|
|
175
|
+
return { verdict: "block", offline_verifiable: false, failure_codes: ["invalid_bundle_public_keys"], verified_claims: 0 };
|
|
176
|
+
}
|
|
177
|
+
if (!Array.isArray(requiredClaimTypes) || !Array.isArray(revocations) || !Array.isArray(conflicts)) {
|
|
178
|
+
return { verdict: "block", offline_verifiable: false, failure_codes: ["invalid_verification_options"], verified_claims: 0 };
|
|
179
|
+
}
|
|
180
|
+
if (!Number.isFinite(Number(now))) {
|
|
181
|
+
return { verdict: "block", offline_verifiable: false, failure_codes: ["invalid_verification_time"], verified_claims: 0 };
|
|
182
|
+
}
|
|
183
|
+
if (bundle.bundle_hash !== contentHash(without(bundle, new Set(["bundle_hash"])))) failureCodes.push("bundle_hash_mismatch");
|
|
184
|
+
if (!(bundle.claims || []).length) failureCodes.push("trust_claim_required");
|
|
185
|
+
const publicKeys = bundle.public_keys || {};
|
|
186
|
+
const results = (bundle.claims || []).map(claim => claim.claim_type === "capability"
|
|
187
|
+
? verifyCapabilityRuntime(claim, bundle.runtime_receipts || [], publicKeys, Number(now), revocations, conflicts)
|
|
188
|
+
: verifyClaim(claim, publicKeys, Number(now), revocations, conflicts));
|
|
189
|
+
const present = new Set(results.map(row => row.claim_type));
|
|
190
|
+
for (const type of requiredClaimTypes) if (!present.has(type)) failureCodes.push(`required_claim_missing:${type}`);
|
|
191
|
+
const required = results.filter(row => !requiredClaimTypes.length || requiredClaimTypes.includes(row.claim_type));
|
|
192
|
+
let verdict;
|
|
193
|
+
if (failureCodes.length || required.some(row => row.status === "blocked")) verdict = "block";
|
|
194
|
+
else if (required.some(row => row.status !== "verified")) verdict = "review";
|
|
195
|
+
else verdict = "pass";
|
|
196
|
+
return {
|
|
197
|
+
verdict,
|
|
198
|
+
offline_verifiable: failureCodes.length === 0 && Object.keys(publicKeys).length > 0,
|
|
199
|
+
bundle_hash: bundle.bundle_hash,
|
|
200
|
+
verified_claims: results.filter(row => row.status === "verified").length,
|
|
201
|
+
claim_results: results,
|
|
202
|
+
failure_codes: failureCodes
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
module.exports = { canonical, contentHash, verifyClaim, verifyCapabilityRuntime, verifyPortableBundle };
|