@twin3-ai/agent-id 0.1.0 → 0.3.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/artifact-bundle.js +92 -0
- package/b1-sync.js +85 -0
- package/bin/agent-id-b1-sync.js +44 -0
- package/bin/agent-id-cloudflare-a1.js +53 -0
- package/bin/agent-id.js +400 -75
- package/cloudflare-a1-sync.js +105 -0
- package/domain-proof.js +193 -0
- package/edge-html-injection.js +258 -0
- package/enterprise-identity.js +48 -0
- package/installer.js +679 -46
- package/local-policy.js +4 -2
- package/optimization-loop.js +48 -0
- package/package.json +15 -3
- package/production-preflight.js +35 -4
- package/release-verifier.js +46 -1
- package/repository-connector.js +150 -5
- package/site-agent.js +55 -3
- package/static-edge-adapter.js +115 -0
- package/sync-service.js +1 -1
- package/task-executor.js +80 -10
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("node:crypto");
|
|
4
|
+
const { AUTO_MANAGED_PATHS } = require("./repository-connector.js");
|
|
5
|
+
const SECRET_CONTENT = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|\bak_aeo_[A-Za-z0-9_-]{6,}\b|\bav_[A-Za-z0-9_-]{8,}\b|\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b)/i;
|
|
6
|
+
|
|
7
|
+
function edgeError(code) { const error = new Error(code); error.code = code; return error; }
|
|
8
|
+
function hashText(value) { return `sha256:${crypto.createHash("sha256").update(value).digest("hex")}`; }
|
|
9
|
+
function normalizePath(value) {
|
|
10
|
+
const path = String(value || "").replace(/^\/+/, "");
|
|
11
|
+
if (!AUTO_MANAGED_PATHS.has(path)) throw edgeError("EDGE_PATH_NOT_ALLOWED");
|
|
12
|
+
return path;
|
|
13
|
+
}
|
|
14
|
+
function assertPublicAsset(path, content) {
|
|
15
|
+
if (typeof content !== "string" || Buffer.byteLength(content, "utf8") > 1024 * 1024 || SECRET_CONTENT.test(content)) throw edgeError("EDGE_CONTENT_INVALID");
|
|
16
|
+
if (path.endsWith(".json")) {
|
|
17
|
+
try { JSON.parse(content); } catch (_error) { throw edgeError("EDGE_JSON_INVALID"); }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function createMemoryAssetStore(initial = {}) {
|
|
21
|
+
const values = new Map(Object.entries(initial));
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
async get(key) { return values.has(key) ? structuredClone(values.get(key)) : null; },
|
|
24
|
+
async put(key, value) { values.set(key, structuredClone(value)); },
|
|
25
|
+
async delete(key) { values.delete(key); },
|
|
26
|
+
async list() { return [...values.keys()].sort(); }
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function createStaticEdgeConnector({ store, verifiedRoutes = [...AUTO_MANAGED_PATHS] } = {}) {
|
|
30
|
+
if (!store || !["get", "put", "delete"].every((name) => typeof store[name] === "function")) throw edgeError("EDGE_STORE_REQUIRED");
|
|
31
|
+
const allowed = new Set(verifiedRoutes.map(normalizePath));
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
describeCapabilities() {
|
|
34
|
+
return { connector: "static_edge", operations: ["upsert", "replace", "delete"], paths: [...allowed].sort(), customer_site_write_access: false };
|
|
35
|
+
},
|
|
36
|
+
async readResource(resource) {
|
|
37
|
+
const path = normalizePath(resource && resource.path);
|
|
38
|
+
if (!allowed.has(path)) throw edgeError("EDGE_ROUTE_NOT_VERIFIED");
|
|
39
|
+
const current = await store.get(path);
|
|
40
|
+
return { connector: "static_edge", environment: String(resource.environment || "production"), resource_type: "route", resource_id: String(resource.resource_id || path), path, content: current && current.content || null, sha256: current && current.sha256 || "" };
|
|
41
|
+
},
|
|
42
|
+
async planOperations(task, currentResource) {
|
|
43
|
+
if (!task || !Array.isArray(task.operations) || !task.operations.length) throw edgeError("EDGE_OPERATIONS_REQUIRED");
|
|
44
|
+
const changes = task.operations.map((operation) => {
|
|
45
|
+
const path = normalizePath(operation.path);
|
|
46
|
+
if (!allowed.has(path) || path !== currentResource.path) throw edgeError("EDGE_ROUTE_NOT_VERIFIED");
|
|
47
|
+
if (!["upsert", "replace", "delete"].includes(operation.operation)) throw edgeError("EDGE_OPERATION_NOT_SUPPORTED");
|
|
48
|
+
const content = operation.operation === "delete" ? "" : operation.value;
|
|
49
|
+
assertPublicAsset(path, content);
|
|
50
|
+
if (operation.expected_hash !== currentResource.sha256) throw edgeError("EDGE_STALE_RESOURCE");
|
|
51
|
+
return { path, operation: operation.operation, before_sha256: currentResource.sha256, after_sha256: operation.operation === "delete" ? "" : hashText(content), after_content: content, content_type: path.endsWith(".json") ? "application/json; charset=utf-8" : "text/plain; charset=utf-8" };
|
|
52
|
+
});
|
|
53
|
+
return { schema: "agentx-static-edge-plan-v1", approval_required: true, customer_site_write_access: false, changes };
|
|
54
|
+
},
|
|
55
|
+
async applyOperations(plan) {
|
|
56
|
+
if (!plan || plan.schema !== "agentx-static-edge-plan-v1") throw edgeError("EDGE_PLAN_INVALID");
|
|
57
|
+
const rollbackEntries = [];
|
|
58
|
+
for (const change of plan.changes) {
|
|
59
|
+
const current = await store.get(change.path);
|
|
60
|
+
const currentHash = current && current.sha256 || "";
|
|
61
|
+
if (currentHash !== change.before_sha256) throw edgeError("EDGE_STALE_RESOURCE");
|
|
62
|
+
rollbackEntries.push({ path: change.path, before: current, after_sha256: change.after_sha256 });
|
|
63
|
+
}
|
|
64
|
+
const applied = [];
|
|
65
|
+
try {
|
|
66
|
+
for (const change of plan.changes) {
|
|
67
|
+
if (change.operation === "delete") await store.delete(change.path);
|
|
68
|
+
else await store.put(change.path, { content: change.after_content, content_type: change.content_type, sha256: change.after_sha256, updated_at: Date.now() });
|
|
69
|
+
applied.push(change.path);
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
for (const item of rollbackEntries.filter((entry) => applied.includes(entry.path)).reverse()) {
|
|
73
|
+
if (item.before) await store.put(item.path, item.before);
|
|
74
|
+
else await store.delete(item.path);
|
|
75
|
+
}
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
return { schema: "agentx-static-edge-receipt-v1", connector: "static_edge", applied_changes: plan.changes.map(({ after_content, ...change }) => change), rollback_entries: rollbackEntries, customer_site_write_access: false };
|
|
79
|
+
},
|
|
80
|
+
async verifyLocalResult(receipt) {
|
|
81
|
+
for (const change of receipt.applied_changes || []) {
|
|
82
|
+
const current = await store.get(change.path);
|
|
83
|
+
const currentHash = current && current.sha256 || "";
|
|
84
|
+
if (currentHash !== change.after_sha256) throw edgeError("EDGE_VERIFY_FAILED");
|
|
85
|
+
}
|
|
86
|
+
return { verified: true };
|
|
87
|
+
},
|
|
88
|
+
async rollback(receipt) {
|
|
89
|
+
if (!receipt || receipt.schema !== "agentx-static-edge-receipt-v1") throw edgeError("EDGE_ROLLBACK_RECEIPT_INVALID");
|
|
90
|
+
for (const item of receipt.rollback_entries || []) {
|
|
91
|
+
const current = await store.get(item.path);
|
|
92
|
+
if ((current && current.sha256 || "") !== item.after_sha256) throw edgeError("EDGE_ROLLBACK_CONFLICT");
|
|
93
|
+
}
|
|
94
|
+
for (const item of [...(receipt.rollback_entries || [])].reverse()) {
|
|
95
|
+
if (item.before) await store.put(item.path, item.before);
|
|
96
|
+
else await store.delete(item.path);
|
|
97
|
+
}
|
|
98
|
+
return { schema: "agentx-static-edge-rollback-receipt-v1", connector: "static_edge", rolled_back: true, paths: (receipt.rollback_entries || []).map((item) => item.path) };
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function createStaticEdgeHandler({ store } = {}) {
|
|
103
|
+
if (!store || typeof store.get !== "function") throw edgeError("EDGE_STORE_REQUIRED");
|
|
104
|
+
return async function handle(request) {
|
|
105
|
+
const method = String(request && request.method || "GET").toUpperCase();
|
|
106
|
+
if (!['GET', 'HEAD'].includes(method)) return new Response("Method Not Allowed", { status: 405, headers: { Allow: "GET, HEAD" } });
|
|
107
|
+
let path;
|
|
108
|
+
try { path = normalizePath(new URL(request.url).pathname); } catch (_error) { return new Response("Not Found", { status: 404 }); }
|
|
109
|
+
const asset = await store.get(path);
|
|
110
|
+
if (!asset) return new Response("Not Found", { status: 404 });
|
|
111
|
+
return new Response(method === "HEAD" ? null : asset.content, { status: 200, headers: { "Content-Type": asset.content_type, "Cache-Control": "public, max-age=300", "ETag": `\"${asset.sha256}\"`, "X-Agent-ID-Managed": "true", "X-Content-Type-Options": "nosniff" } });
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = { createMemoryAssetStore, createStaticEdgeConnector, createStaticEdgeHandler };
|
package/sync-service.js
CHANGED
|
@@ -125,7 +125,7 @@ function createSiteAgentSyncService(options = {}) {
|
|
|
125
125
|
const intervalMs = Math.max(1000, Number(options.intervalMs || process.env.AGENT_ID_SYNC_INTERVAL_MS || 900000));
|
|
126
126
|
const heartbeatIntervalMs = Math.max(300000, Number(options.heartbeatIntervalMs || process.env.AGENT_ID_HEARTBEAT_INTERVAL_MS || options.intervalMs || 900000));
|
|
127
127
|
const collector = options.collector || null;
|
|
128
|
-
const runtimeVersion = String(options.runtimeVersion || process.env.AGENT_ID_RUNTIME_VERSION || "0.
|
|
128
|
+
const runtimeVersion = String(options.runtimeVersion || process.env.AGENT_ID_RUNTIME_VERSION || "0.3.0");
|
|
129
129
|
const capabilities = Array.from(new Set((Array.isArray(options.capabilities) ? options.capabilities : ["heartbeat", "insights_feed"]).map((item) => String(item).trim()).filter(Boolean))).sort();
|
|
130
130
|
const now = options.now || Date.now;
|
|
131
131
|
const sleep = options.sleep || ((ms, signal) => new Promise((resolve) => {
|
package/task-executor.js
CHANGED
|
@@ -15,6 +15,44 @@ function stableValue(value) {
|
|
|
15
15
|
}
|
|
16
16
|
function canonical(value) { return Buffer.from(JSON.stringify(stableValue(value)), "utf8"); }
|
|
17
17
|
function hash(value) { return `sha256:${crypto.createHash("sha256").update(value).digest("hex")}`; }
|
|
18
|
+
function rollbackEvidence(connectorReceipt) {
|
|
19
|
+
if (!connectorReceipt || typeof connectorReceipt !== "object" || connectorReceipt.rolled_back !== true) {
|
|
20
|
+
throw executorError("rollback_not_confirmed");
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
schema: String(connectorReceipt.schema || ""),
|
|
24
|
+
connector: String(connectorReceipt.connector || ""),
|
|
25
|
+
plan_id: String(connectorReceipt.plan_id || ""),
|
|
26
|
+
artifact_bundle_hash: String(connectorReceipt.artifact_bundle_hash || ""),
|
|
27
|
+
rolled_back: true,
|
|
28
|
+
files: Array.isArray(connectorReceipt.files)
|
|
29
|
+
? connectorReceipt.files.map((item) => ({
|
|
30
|
+
path: String(item && (item.path || item.relative_path) || ""),
|
|
31
|
+
before_sha256: String(item && item.before_sha256 || "")
|
|
32
|
+
}))
|
|
33
|
+
: [],
|
|
34
|
+
paths: Array.isArray(connectorReceipt.paths) ? connectorReceipt.paths.map((item) => String(item)) : []
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function signedRollbackReceipt({ task, subject, originalReceipt, connectorReceipt, privateKey, now }) {
|
|
38
|
+
const connectorRollbackReceipt = rollbackEvidence(connectorReceipt);
|
|
39
|
+
const payload = {
|
|
40
|
+
schema: "agentx-customer-rollback-receipt-v1",
|
|
41
|
+
receipt_nonce: crypto.randomUUID(),
|
|
42
|
+
task_id: task.task_id,
|
|
43
|
+
subject,
|
|
44
|
+
connector: task.connector,
|
|
45
|
+
policy_digest: task.policy_digest,
|
|
46
|
+
resource: task.resource,
|
|
47
|
+
original_receipt_hash: String(originalReceipt && originalReceipt.receipt_hash || ""),
|
|
48
|
+
connector_rollback_hash: hash(canonical(connectorRollbackReceipt)),
|
|
49
|
+
connector_rollback_receipt: connectorRollbackReceipt,
|
|
50
|
+
rolled_back: true,
|
|
51
|
+
rolled_back_at: Number(now),
|
|
52
|
+
};
|
|
53
|
+
const signature = crypto.sign(null, canonical(payload), privateKey).toString("base64url");
|
|
54
|
+
return { ...payload, signature, receipt_hash: hash(canonical({ ...payload, signature })) };
|
|
55
|
+
}
|
|
18
56
|
function exactSubject(left, right) {
|
|
19
57
|
return ["tenant_id","host","environment","agent_id"].every((field) => String(left && left[field] || "") === String(right && right[field] || ""));
|
|
20
58
|
}
|
|
@@ -69,23 +107,28 @@ function createTaskExecutor({ issuerPublicKey, identity, policy, connectors, sta
|
|
|
69
107
|
environment: task.subject.environment,
|
|
70
108
|
operations: task.operations.map((operation) => ({ ...operation, operation: operation.op }))
|
|
71
109
|
};
|
|
72
|
-
authorizeTask(policy, policyTask, current);
|
|
110
|
+
const authorization = authorizeTask(policy, policyTask, current);
|
|
73
111
|
const plan = await connector.planOperations(policyTask, current);
|
|
74
|
-
return { connector, current, plan, policyTask };
|
|
112
|
+
return { connector, current, plan, policyTask, authorization };
|
|
75
113
|
}
|
|
76
114
|
return Object.freeze({
|
|
77
115
|
async plan(task) {
|
|
78
116
|
const prepared = await prepare(task);
|
|
79
117
|
runtime.set(task.task_id, { task, ...prepared });
|
|
80
|
-
state.tasks[task.task_id] = { task_id: task.task_id, connector: task.connector, idempotency_key: task.idempotency_key, state: "planned", policy_digest: task.policy_digest, envelope_hash: hash(canonical(Object.fromEntries(SIGNED_FIELDS.map((field) => [field, task[field]])))) };
|
|
118
|
+
state.tasks[task.task_id] = { task_id: task.task_id, connector: task.connector, idempotency_key: task.idempotency_key, resource: task.resource, state: "planned", policy_digest: task.policy_digest, envelope_hash: hash(canonical(Object.fromEntries(SIGNED_FIELDS.map((field) => [field, task[field]])))) };
|
|
81
119
|
persist();
|
|
82
|
-
return { schema: "agentx-local-execution-plan-v1", task_id: task.task_id, connector: task.connector, approval_required:
|
|
120
|
+
return { schema: "agentx-local-execution-plan-v1", task_id: task.task_id, connector: task.connector, decision: prepared.authorization.decision, approval_required: prepared.authorization.decision !== "auto", plan_hash: hash(canonical(prepared.plan)), changes: prepared.plan.changes.map(({ after_content, rollback_entries, ...item }) => item) };
|
|
83
121
|
},
|
|
84
122
|
async apply(task, approval = {}) {
|
|
85
123
|
const existing = state.tasks[task.task_id];
|
|
86
|
-
if (existing && existing.state === "deployed" && existing.result)
|
|
87
|
-
|
|
124
|
+
if (existing && existing.state === "deployed" && existing.result) {
|
|
125
|
+
const connector = connectors && connectors[task.connector];
|
|
126
|
+
if (!connector) throw executorError("unknown_connector");
|
|
127
|
+
if (existing.connector_receipt) runtime.set(task.task_id, { task, connector, connectorReceipt: existing.connector_receipt });
|
|
128
|
+
return existing.result;
|
|
129
|
+
}
|
|
88
130
|
const prepared = runtime.get(task.task_id) || { task, ...(await prepare(task)) };
|
|
131
|
+
if (prepared.authorization.decision !== "auto" && (approval.approved !== true || approval.task_id !== task.task_id || approval.policy_digest !== policy.digest)) throw executorError("approval_required");
|
|
89
132
|
const receipt = await prepared.connector.applyOperations(prepared.plan);
|
|
90
133
|
await prepared.connector.verifyLocalResult(receipt);
|
|
91
134
|
const appliedResource = (receipt.applied_changes || []).find((item) => item.path === task.resource.path) || {};
|
|
@@ -105,20 +148,47 @@ function createTaskExecutor({ issuerPublicKey, identity, policy, connectors, sta
|
|
|
105
148
|
const privateKey = crypto.createPrivateKey(fs.readFileSync(identity.key_path, "utf8"));
|
|
106
149
|
publicReceipt.signature = crypto.sign(null, canonical(publicReceipt), privateKey).toString("base64url");
|
|
107
150
|
publicReceipt.receipt_hash = hash(canonical(publicReceipt));
|
|
108
|
-
state.tasks[task.task_id] = {
|
|
151
|
+
state.tasks[task.task_id] = {
|
|
152
|
+
...state.tasks[task.task_id],
|
|
153
|
+
state: "deployed",
|
|
154
|
+
receipt_hash: publicReceipt.receipt_hash,
|
|
155
|
+
result: publicReceipt,
|
|
156
|
+
connector_receipt: receipt,
|
|
157
|
+
};
|
|
109
158
|
runtime.set(task.task_id, { ...prepared, connectorReceipt: receipt });
|
|
110
159
|
persist();
|
|
111
160
|
return publicReceipt;
|
|
112
161
|
},
|
|
113
162
|
async rollback(taskId, approval = {}) {
|
|
114
163
|
if (approval.approved !== true || approval.task_id !== taskId) throw executorError("approval_required");
|
|
115
|
-
const prepared = runtime.get(taskId);
|
|
116
164
|
const taskState = state.tasks[taskId];
|
|
165
|
+
const prepared = runtime.get(taskId) || {
|
|
166
|
+
task: {
|
|
167
|
+
task_id: taskId,
|
|
168
|
+
connector: taskState && taskState.connector,
|
|
169
|
+
policy_digest: taskState && taskState.policy_digest,
|
|
170
|
+
subject: identity.subject,
|
|
171
|
+
resource: taskState && taskState.resource,
|
|
172
|
+
},
|
|
173
|
+
connector: connectors && connectors[taskState && taskState.connector],
|
|
174
|
+
connectorReceipt: taskState && taskState.connector_receipt,
|
|
175
|
+
};
|
|
117
176
|
if (!taskState || taskState.state !== "deployed") throw executorError("rollback_receipt_unavailable");
|
|
177
|
+
if (!taskState.connector_receipt) throw executorError("rollback_receipt_unavailable");
|
|
118
178
|
const connector = prepared && prepared.connector || connectors && connectors[taskState.connector];
|
|
119
179
|
if (!connector) throw executorError("unknown_connector");
|
|
120
|
-
const
|
|
121
|
-
|
|
180
|
+
const connectorReceipt = await connector.rollback(prepared && prepared.connectorReceipt);
|
|
181
|
+
rollbackEvidence(connectorReceipt);
|
|
182
|
+
const privateKey = crypto.createPrivateKey(fs.readFileSync(identity.key_path, "utf8"));
|
|
183
|
+
const receipt = signedRollbackReceipt({
|
|
184
|
+
task: prepared.task,
|
|
185
|
+
subject: identity.subject,
|
|
186
|
+
originalReceipt: taskState.result,
|
|
187
|
+
connectorReceipt,
|
|
188
|
+
privateKey,
|
|
189
|
+
now: now(),
|
|
190
|
+
});
|
|
191
|
+
state.tasks[taskId] = { ...taskState, state: "rolled_back", rollback_receipt_hash: receipt.receipt_hash };
|
|
122
192
|
persist();
|
|
123
193
|
return receipt;
|
|
124
194
|
},
|