@twin3-ai/agent-id 0.1.0 → 0.2.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.
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+
3
+ const crypto = require("node:crypto");
4
+ const { AUTO_MANAGED_PATHS } = require("./repository-connector.js");
5
+
6
+ const SCHEMA = "agentx-signed-artifact-bundle-v0.2";
7
+ const MAX_LIFETIME_SECONDS = 7 * 24 * 60 * 60;
8
+ 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;
9
+
10
+ function bundleError(code) {
11
+ const error = new Error(code);
12
+ error.code = code;
13
+ return error;
14
+ }
15
+ function stableValue(value) {
16
+ if (Array.isArray(value)) return value.map(stableValue);
17
+ if (!value || typeof value !== "object") return value;
18
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])]));
19
+ }
20
+ function canonical(value) { return Buffer.from(JSON.stringify(stableValue(value)), "utf8"); }
21
+ function hash(value) { return `sha256:${crypto.createHash("sha256").update(value).digest("hex")}`; }
22
+ function sameSubject(left, right) {
23
+ return ["tenant_id", "host", "environment", "agent_id"].every((field) => String(left && left[field] || "") === String(right && right[field] || ""));
24
+ }
25
+ function keyId(key) {
26
+ const publicKey = key && key.type === "public" ? key : crypto.createPublicKey(key);
27
+ const der = publicKey.export({ type: "spki", format: "der" });
28
+ return `ed25519:${crypto.createHash("sha256").update(der).digest("hex").slice(0, 24)}`;
29
+ }
30
+ function normalizeArtifacts(artifacts) {
31
+ if (!Array.isArray(artifacts) || artifacts.length === 0) throw bundleError("ARTIFACT_BUNDLE_ARTIFACTS_REQUIRED");
32
+ const seen = new Set();
33
+ return artifacts.map((artifact) => {
34
+ const path = String(artifact && artifact.path || "").replace(/^\/+/, "");
35
+ const content = artifact && artifact.content;
36
+ if (!AUTO_MANAGED_PATHS.has(path)) throw bundleError("ARTIFACT_BUNDLE_PATH_NOT_ALLOWED");
37
+ if (seen.has(path)) throw bundleError("ARTIFACT_BUNDLE_DUPLICATE_PATH");
38
+ if (typeof content !== "string" || Buffer.byteLength(content, "utf8") > 1024 * 1024 || SECRET_CONTENT.test(content)) throw bundleError("ARTIFACT_BUNDLE_INVALID_CONTENT");
39
+ if (path.endsWith(".json")) {
40
+ try { JSON.parse(content); } catch (_error) { throw bundleError("ARTIFACT_BUNDLE_INVALID_JSON"); }
41
+ }
42
+ seen.add(path);
43
+ return {
44
+ path,
45
+ content_type: String(artifact.content_type || "text/plain; charset=utf-8").slice(0, 120),
46
+ content,
47
+ content_hash: hash(Buffer.from(content, "utf8"))
48
+ };
49
+ });
50
+ }
51
+ function unsignedBundle(bundle) {
52
+ const copy = { ...bundle };
53
+ delete copy.signature;
54
+ return copy;
55
+ }
56
+ function createArtifactBundle({ subject, artifacts, issuerPrivateKey, issuerPublicKey, source = {}, issuedAt = Math.floor(Date.now() / 1000), expiresAt = issuedAt + 3600 } = {}) {
57
+ if (!subject || !issuerPrivateKey || !issuerPublicKey) throw bundleError("ARTIFACT_BUNDLE_CONFIGURATION_INVALID");
58
+ if (!["tenant_id", "host", "environment", "agent_id"].every((field) => typeof subject[field] === "string" && subject[field])) throw bundleError("ARTIFACT_BUNDLE_SUBJECT_INVALID");
59
+ if (typeof source.run_id !== "string" || !source.run_id || typeof source.receipt_hash !== "string" || !/^sha256:[a-f0-9]{6,}$/i.test(source.receipt_hash)) throw bundleError("ARTIFACT_BUNDLE_SOURCE_INVALID");
60
+ if (!Number.isInteger(issuedAt) || !Number.isInteger(expiresAt) || expiresAt <= issuedAt || expiresAt - issuedAt > MAX_LIFETIME_SECONDS) throw bundleError("ARTIFACT_BUNDLE_TIME_INVALID");
61
+ const bundle = {
62
+ schema: SCHEMA,
63
+ bundle_version: 2,
64
+ subject: stableValue(subject),
65
+ source: { run_id: source.run_id, receipt_hash: source.receipt_hash },
66
+ issued_at: issuedAt,
67
+ expires_at: expiresAt,
68
+ issuer_key_id: keyId(issuerPublicKey),
69
+ artifacts: normalizeArtifacts(artifacts)
70
+ };
71
+ bundle.bundle_hash = hash(canonical(bundle));
72
+ bundle.signature = crypto.sign(null, canonical(bundle), issuerPrivateKey).toString("base64url");
73
+ return bundle;
74
+ }
75
+ function verifyArtifactBundle(bundle, { issuerPublicKey, expectedSubject, now = Math.floor(Date.now() / 1000) } = {}) {
76
+ if (!bundle || bundle.schema !== SCHEMA || bundle.bundle_version !== 2 || !issuerPublicKey || !bundle.signature) throw bundleError("ARTIFACT_BUNDLE_INVALID");
77
+ if (!sameSubject(bundle.subject, expectedSubject)) throw bundleError("ARTIFACT_BUNDLE_SUBJECT_MISMATCH");
78
+ if (!Number.isInteger(bundle.issued_at) || !Number.isInteger(bundle.expires_at) || now < bundle.issued_at || now >= bundle.expires_at || bundle.expires_at - bundle.issued_at > MAX_LIFETIME_SECONDS) throw bundleError("ARTIFACT_BUNDLE_EXPIRED");
79
+ if (bundle.issuer_key_id !== keyId(issuerPublicKey)) throw bundleError("ARTIFACT_BUNDLE_SIGNATURE_INVALID");
80
+ const artifacts = normalizeArtifacts(bundle.artifacts);
81
+ if (artifacts.some((artifact, index) => artifact.content_hash !== bundle.artifacts[index].content_hash)) throw bundleError("ARTIFACT_BUNDLE_SIGNATURE_INVALID");
82
+ const unsigned = unsignedBundle(bundle);
83
+ const claimedHash = unsigned.bundle_hash;
84
+ delete unsigned.bundle_hash;
85
+ if (claimedHash !== hash(canonical(unsigned))) throw bundleError("ARTIFACT_BUNDLE_SIGNATURE_INVALID");
86
+ const verificationKey = issuerPublicKey && issuerPublicKey.type === "public" ? issuerPublicKey : crypto.createPublicKey(issuerPublicKey);
87
+ const verified = crypto.verify(null, canonical({ ...unsigned, bundle_hash: claimedHash }), verificationKey, Buffer.from(bundle.signature, "base64url"));
88
+ if (!verified) throw bundleError("ARTIFACT_BUNDLE_SIGNATURE_INVALID");
89
+ return { verified: true, bundle_hash: claimedHash, subject: bundle.subject, artifacts: artifacts.map(({ content, ...item }) => item) };
90
+ }
91
+
92
+ module.exports = { SCHEMA, MAX_LIFETIME_SECONDS, createArtifactBundle, verifyArtifactBundle };
package/bin/agent-id.js CHANGED
@@ -3,11 +3,15 @@ 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, resumeInstall, uninstallInstall, enterpriseInstallStatus, prepareEnterpriseSecretDirectory, writeEnterpriseBootstrapCredential, readEnterpriseBootstrapCredential, replaceEnterpriseBootstrapCredential, removeEnterpriseBootstrapCredential, writeEnterpriseEnrollmentState } = require("../installer.js");
6
+ const { buildInstallPlan, applyInstallPlan, resumeInstall, uninstallInstall, enterpriseInstallStatus, prepareEnterpriseSecretDirectory, writeEnterpriseBootstrapCredential, readEnterpriseBootstrapCredential, replaceEnterpriseBootstrapCredential, removeEnterpriseBootstrapCredential, writeEnterpriseEnrollmentState } = require("../installer.js");
7
7
  const { buildRepositoryPatch, applyRepositoryPatch, rollbackRepositoryPatch } = require("../repository-connector.js");
8
+ const { createRepositoryConnector } = require("../repository-connector.js");
8
9
  const { generateSiteAgentIdentity, createEnterpriseSession } = require("../enterprise-identity.js");
9
10
  const { buildWellKnownProofDocument, resolveProofTarget, writeWellKnownProofDocument } = require("../domain-proof.js");
10
11
  const { initializeLocalPolicy, loadLocalPolicy, approvePolicyRule } = require("../local-policy.js");
12
+ const { createTaskExecutor } = require("../task-executor.js");
13
+ const { createOptimizationLoop } = require("../optimization-loop.js");
14
+ const crypto = require("node:crypto");
11
15
  const { verifyPortableBundle } = require("../trust-verifier.js");
12
16
  const { verifyReleaseManifest } = require("../release-verifier.js");
13
17
  const { runProductionPreflight } = require("../production-preflight.js");
@@ -62,7 +66,17 @@ async function enterpriseSessionForProject(projectRoot) {
62
66
  credential: existing.credential,
63
67
  identity
64
68
  });
65
- return { root, state, session };
69
+ return { root, state, session, identity, credential: existing.credential };
70
+ }
71
+
72
+ async function trustedIssuerPublicKey() {
73
+ const response = await fetch(endpoint + "/.well-known/agentx-issuer-key.json");
74
+ if (!response.ok) throw new Error("enterprise issuer key unavailable");
75
+ const descriptor = await response.json();
76
+ const publicKey = String(descriptor.public_key_pem || "");
77
+ const actual = `sha256:${crypto.createHash("sha256").update(publicKey).digest("hex")}`;
78
+ if (!publicKey || actual !== TRUSTED_ISSUER_KEY_SHA256) throw new Error("enterprise issuer key pin mismatch");
79
+ return publicKey;
66
80
  }
67
81
 
68
82
  function printManagedResult(value) {
@@ -1364,6 +1378,8 @@ async function runCli(argv) {
1364
1378
  " agent-id enterprise-managed-pause [--project-root <dir>] [--json]",
1365
1379
  " agent-id enterprise-managed-resume [--project-root <dir>] [--json]",
1366
1380
  " agent-id enterprise-managed-runs [--project-root <dir>] [--limit <1-100>] [--json]",
1381
+ " agent-id enterprise-optimize-once [--project-root <dir>] [--approve-task <task-id>] [--json]",
1382
+ " agent-id enterprise-rollback-once --task-id <task-id> --project-root <dir> --approve [--json]",
1367
1383
  " agent-id enterprise-contract",
1368
1384
  " agent-id repository-patch-plan <root> <changes-json-file>",
1369
1385
  " agent-id repository-patch-apply <plan-json-file> --approve",
@@ -2533,12 +2549,19 @@ async function runCli(argv) {
2533
2549
  applyProof: true,
2534
2550
  });
2535
2551
  if (!enrollment.ok) throw new Error(enrollment.error || "enterprise_enrollment_failed");
2552
+ // Enrollment creates the customer's key and publishes the proof, but the
2553
+ // runtime that polls for work is a separate set of files. Without this the
2554
+ // installer that writes them was never called from the CLI, so nothing was
2555
+ // installed, install-resume always reported not_installed, and
2556
+ // install-uninstall removed nothing because there was no state to find.
2557
+ const receipt = await applyInstallPlan(detectedPlan, { approved: true });
2536
2558
  return printSignedInstallResult({
2537
2559
  ok: true,
2538
2560
  dry_run: false,
2539
2561
  plan,
2540
2562
  release: gate,
2541
2563
  enrollment,
2564
+ runtime_install: receipt,
2542
2565
  non_claims: ["domain_verification_still_required", "shared_site_agent_key_not_used", "does_not_modify_customer_content"]
2543
2566
  });
2544
2567
  }
@@ -2737,6 +2760,55 @@ async function runCli(argv) {
2737
2760
  session.clear();
2738
2761
  }
2739
2762
  }
2763
+ if (cmd === "enterprise-optimize-once") {
2764
+ const projectRoot = readFlagValue(argv, "--project-root") || process.cwd();
2765
+ const { root, state, session, identity, credential } = await enterpriseSessionForProject(projectRoot);
2766
+ const policy = loadLocalPolicy(path.join(root, ".agent-id", "enterprise-policy.json"));
2767
+ identity.subject = { tenant_id: credential.tenant_id, host: credential.host, environment: state.environment, agent_id: credential.agent_id };
2768
+ const executor = createTaskExecutor({
2769
+ issuerPublicKey: await trustedIssuerPublicKey(),
2770
+ identity,
2771
+ policy,
2772
+ connectors: { repository_patch: createRepositoryConnector({ repositoryRoot: root }) },
2773
+ statePath: path.join(root, ".agent-id", "executor-state.json")
2774
+ });
2775
+ const approvedTask = readFlagValue(argv, "--approve-task");
2776
+ try {
2777
+ return print(await createOptimizationLoop({ session, executor }).runOnce({ approvedTaskIds: approvedTask ? [approvedTask] : [] }));
2778
+ } finally {
2779
+ session.clear();
2780
+ }
2781
+ }
2782
+ if (cmd === "enterprise-rollback-once") {
2783
+ const projectRoot = readFlagValue(argv, "--project-root") || process.cwd();
2784
+ const taskId = readFlagValue(argv, "--task-id");
2785
+ if (!taskId) throw new Error("enterprise-rollback-once requires --task-id");
2786
+ if (!argv.includes("--approve")) throw new Error("enterprise rollback requires --approve");
2787
+ const { root, state, session, identity, credential } = await enterpriseSessionForProject(projectRoot);
2788
+ const policy = loadLocalPolicy(path.join(root, ".agent-id", "enterprise-policy.json"));
2789
+ identity.subject = { tenant_id: credential.tenant_id, host: credential.host, environment: state.environment, agent_id: credential.agent_id };
2790
+ const executor = createTaskExecutor({
2791
+ issuerPublicKey: await trustedIssuerPublicKey(),
2792
+ identity,
2793
+ policy,
2794
+ connectors: { repository_patch: createRepositoryConnector({ repositoryRoot: root }) },
2795
+ statePath: path.join(root, ".agent-id", "executor-state.json")
2796
+ });
2797
+ try {
2798
+ const listed = await session.listTasks();
2799
+ let task = (listed.tasks || []).find((item) => item.task_id === taskId);
2800
+ if (!task) throw new Error("enterprise_task_not_found");
2801
+ if (["deployed", "failed"].includes(task.state)) {
2802
+ task = (await session.transitionTask(taskId, "rollback", task.version)).task;
2803
+ }
2804
+ if (task.state !== "rollback_pending") throw new Error(`enterprise_task_not_rollbackable:${task.state}`);
2805
+ const rollbackReceipt = await executor.rollback(taskId, { approved: true, task_id: taskId });
2806
+ const completed = await session.transitionTask(taskId, "rollback-receipt", task.version, { rollback_receipt: rollbackReceipt });
2807
+ return print({ ok: true, task: completed.task, rollback_receipt: rollbackReceipt });
2808
+ } finally {
2809
+ session.clear();
2810
+ }
2811
+ }
2740
2812
  if (cmd === "repository-patch-plan") {
2741
2813
  const root = url;
2742
2814
  const changesFile = argv[2];
@@ -2790,7 +2862,7 @@ function runMcp() {
2790
2862
 
2791
2863
  async function handleMcpLine(line) {
2792
2864
  const msg = JSON.parse(line);
2793
- if (msg.method === "initialize") return send(msg.id, { protocolVersion: "2024-11-05", serverInfo: { name: "agent-id", version: "0.1.0" } });
2865
+ if (msg.method === "initialize") return send(msg.id, { protocolVersion: "2024-11-05", serverInfo: { name: "agent-id", version: "0.2.0" } });
2794
2866
  if (msg.method === "tools/list") {
2795
2867
  const tools = [
2796
2868
  { name: "audit_url", description: "Audit a website for SEO/AEO/GEO/Agent readiness.", inputSchema: { type: "object", properties: { url: { type: "string" } }, required: ["url"] } },
@@ -145,6 +145,13 @@ function createEnterpriseSession({ serviceOrigin, siteUrl, environment, credenti
145
145
  return responseJson(await fetcher(`${origin}/api/site_agents/v1/managed/${pathname}`, request));
146
146
  }
147
147
 
148
+ async function taskRequest(pathname, { method = "GET", body = null, scopes = ["tasks:read"] } = {}) {
149
+ const accessToken = await session.getAccessToken(scopes);
150
+ const request = { method, headers: { "content-type": "application/json", authorization: `Bearer ${accessToken}` } };
151
+ if (body !== null) request.body = JSON.stringify(body);
152
+ return responseJson(await fetcher(`${origin}/api/site_agents/v1/tasks${pathname}`, request));
153
+ }
154
+
148
155
  const session = {
149
156
  schema: "agentx-enterprise-session-v1",
150
157
  site_url: site.url,
@@ -194,6 +201,26 @@ function createEnterpriseSession({ serviceOrigin, siteUrl, environment, credenti
194
201
  const bounded = Math.max(1, Math.min(Number(limit) || 20, 100));
195
202
  return managedRequest(`runs?limit=${encodeURIComponent(Math.trunc(bounded))}`);
196
203
  },
204
+ async listTasks() {
205
+ return taskRequest("");
206
+ },
207
+ async getTaskComparison(taskId) {
208
+ const id = encodeURIComponent(String(taskId || ""));
209
+ if (!id) throw enterpriseError("enterprise_task_id_required");
210
+ return taskRequest(`/${id}/comparison`);
211
+ },
212
+ async createTaskComparison(taskId) {
213
+ const id = encodeURIComponent(String(taskId || ""));
214
+ if (!id) throw enterpriseError("enterprise_task_id_required");
215
+ return taskRequest(`/${id}/comparison`, { method: "POST", scopes: ["tasks:read"], body: {} });
216
+ },
217
+ async transitionTask(taskId, action, expectedVersion, payload = {}) {
218
+ const id = encodeURIComponent(String(taskId || ""));
219
+ const supported = new Set(["propose", "approve", "execute", "reject", "receipt", "verify", "complete", "rollback", "rollback-receipt"]);
220
+ if (!id || !supported.has(action) || !Number.isInteger(expectedVersion)) throw enterpriseError("enterprise_task_transition_invalid");
221
+ const scopes = ["receipt", "rollback-receipt"].includes(action) ? ["evidence:write"] : (["verify", "complete"].includes(action) ? ["tasks:read"] : ["tasks:execute"]);
222
+ return taskRequest(`/${id}/${action}`, { method: "POST", scopes, body: { ...payload, expected_version: expectedVersion } });
223
+ },
197
224
  clear() { cachedToken = ""; cachedExpiry = 0; cachedScopes = []; }
198
225
  };
199
226
  return Object.freeze(session);
package/installer.js CHANGED
@@ -169,7 +169,7 @@ async function main() {
169
169
  const observedAt = new Date().toISOString();
170
170
  const heartbeat = SYNC_MODE === "monthly" ? { skipped: true, reason: "monthly_insights_run" } : await call("site_agent_events", {
171
171
  source: "customer_owned_github_actions_runner",
172
- events: [{ event: "heartbeat", url: SITE_URL, ts: Math.floor(Date.now() / 1000), data: { runtime: "github_actions", sdk_version: "0.1.0" } }]
172
+ events: [{ event: "heartbeat", url: SITE_URL, ts: Math.floor(Date.now() / 1000), data: { runtime: "github_actions", sdk_version: "0.2.0" } }]
173
173
  });
174
174
  const insights = SYNC_MODE === "heartbeat" ? { skipped: true, reason: "daily_heartbeat_only" } : await call("insights_feed", {
175
175
  source: "customer_owned_github_actions_runner",
@@ -300,6 +300,90 @@ function stateTemplate(plan) {
300
300
  };
301
301
  }
302
302
 
303
+ const OPPORTUNITY_CHECKS = Object.freeze([
304
+ { id: "package_manifest", paths: ["package.json"] },
305
+ { id: "llms_txt", paths: ["llms.txt", "public/llms.txt"] },
306
+ { id: "robots_txt", paths: ["robots.txt", "public/robots.txt"] },
307
+ { id: "agent_card", paths: [".well-known/agent-card.json", "public/.well-known/agent-card.json"] },
308
+ { id: "agent_id", paths: [".well-known/agent-id.json", "public/.well-known/agent-id.json"] },
309
+ { id: "aeo_agent", paths: [".well-known/aeo-agent.json", "public/.well-known/aeo-agent.json"] },
310
+ { id: "agent_knowledge", paths: [".well-known/agent-knowledge.json", "public/.well-known/agent-knowledge.json"] }
311
+ ]);
312
+
313
+ async function inspectAgentIdOpportunities({ projectRoot = process.cwd(), fsImpl = fs, checks = OPPORTUNITY_CHECKS } = {}) {
314
+ const root = normalizeProjectRoot(projectRoot);
315
+ const reportChecks = [];
316
+ for (const definition of checks) {
317
+ const presentPaths = [];
318
+ const unavailablePaths = [];
319
+ const unavailableErrors = {};
320
+ const symlinkPaths = [];
321
+ for (const relativePath of definition.paths) {
322
+ if (typeof fsImpl.lstat !== "function") {
323
+ unavailablePaths.push(relativePath);
324
+ unavailableErrors[relativePath] = "LSTAT_UNAVAILABLE";
325
+ continue;
326
+ }
327
+ try {
328
+ const candidate = path.resolve(root, relativePath);
329
+ const withinRoot = path.relative(root, candidate);
330
+ if (withinRoot === ".." || withinRoot.startsWith(`..${path.sep}`) || path.isAbsolute(withinRoot)) {
331
+ unavailablePaths.push(relativePath);
332
+ unavailableErrors[relativePath] = "PATH_ESCAPE";
333
+ continue;
334
+ }
335
+ const stat = await fsImpl.lstat(candidate);
336
+ if (stat.isSymbolicLink()) {
337
+ symlinkPaths.push(relativePath);
338
+ unavailablePaths.push(relativePath);
339
+ unavailableErrors[relativePath] = "SYMLINK_REJECTED";
340
+ continue;
341
+ }
342
+ if (typeof stat.isFile === "function" && !stat.isFile()) {
343
+ unavailablePaths.push(relativePath);
344
+ unavailableErrors[relativePath] = "NOT_REGULAR_FILE";
345
+ continue;
346
+ }
347
+ presentPaths.push(relativePath);
348
+ } catch (error) {
349
+ if (!error || error.code !== "ENOENT") {
350
+ unavailablePaths.push(relativePath);
351
+ unavailableErrors[relativePath] = (error && ["EACCES", "EPERM"].includes(error.code))
352
+ ? "PERMISSION_DENIED"
353
+ : "UNAVAILABLE";
354
+ }
355
+ }
356
+ }
357
+ reportChecks.push({
358
+ id: definition.id,
359
+ status: presentPaths.length ? "present" : unavailablePaths.length ? "unavailable" : "missing",
360
+ present_paths: presentPaths,
361
+ unavailable_paths: unavailablePaths,
362
+ unavailable_errors: unavailableErrors,
363
+ symlink_paths: symlinkPaths,
364
+ candidate_paths: [...definition.paths]
365
+ });
366
+ }
367
+ const present = reportChecks.filter((item) => item.status === "present").length;
368
+ const unavailable = reportChecks.filter((item) => item.status === "unavailable").length;
369
+ return {
370
+ schema: "agentx-local-opportunity-report-v1",
371
+ local_only: true,
372
+ source_uploaded: false,
373
+ secrets_scanned: false,
374
+ content_validated: false,
375
+ verified: 0,
376
+ checks: reportChecks,
377
+ symlink_policy: "skip_and_exclude",
378
+ summary: {
379
+ total: reportChecks.length,
380
+ present,
381
+ missing: reportChecks.length - present - unavailable,
382
+ unavailable
383
+ }
384
+ };
385
+ }
386
+
303
387
  function buildInstallPlan({ url, projectRoot = process.cwd(), endpoint = DEFAULT_ENDPOINT, framework, packageManager, detected } = {}) {
304
388
  return Promise.resolve().then(async () => {
305
389
  const siteUrl = normalizeSiteUrl(url);
@@ -307,6 +391,7 @@ function buildInstallPlan({ url, projectRoot = process.cwd(), endpoint = DEFAULT
307
391
  const detectedProject = detected || await detectProject({ projectRoot: root });
308
392
  const selectedFramework = framework || detectedProject.framework;
309
393
  const selectedPackageManager = packageManager || detectedProject.package_manager;
394
+ const opportunityReport = await inspectAgentIdOpportunities({ projectRoot: root });
310
395
  if (!/^[a-z][a-z0-9._-]{0,31}$/.test(String(selectedFramework))) throw installError("INVALID_FRAMEWORK", "Framework identifier is invalid.");
311
396
  if (!/^[a-z][a-z0-9._-]{0,31}$/.test(String(selectedPackageManager))) throw installError("INVALID_PACKAGE_MANAGER", "Package manager identifier is invalid.");
312
397
  const parentEndpoint = String(endpoint || DEFAULT_ENDPOINT).replace(/\/+$/, "");
@@ -328,6 +413,7 @@ function buildInstallPlan({ url, projectRoot = process.cwd(), endpoint = DEFAULT
328
413
  project_root: root,
329
414
  framework: selectedFramework,
330
415
  package_manager: selectedPackageManager,
416
+ opportunity_report: opportunityReport,
331
417
  approval_required: true,
332
418
  side_effects: selectedFramework === "static"
333
419
  ? ["write_agent_id_namespace", "write_dedicated_github_actions_workflow"]
@@ -338,9 +424,33 @@ function buildInstallPlan({ url, projectRoot = process.cwd(), endpoint = DEFAULT
338
424
  : ["verify_domain_ownership", "install_server_package", "store_SITE_AGENT_KEY_in_server_secret_store", "start_site_agent_sync"],
339
425
  files: files.map(({ path: filePath, mode, content }) => ({ path: filePath, mode, sha256: hashText(content) }))
340
426
  };
341
- base.install_id = hashText(stableJson(base)).slice(0, 24);
342
- base.files[0].sha256 = hashText(JSON.stringify(stateTemplate({ ...base, files: base.files }), null, 2) + "\n");
343
- return Object.freeze({ ...base, files: files.map((file, index) => ({ ...base.files[index], content: file.content })) });
427
+ // install_id identifies the deterministic install plan, not the mutable
428
+ // state file that is generated from that plan at apply time.
429
+ const installIdentity = {
430
+ schema: base.schema,
431
+ version: base.version,
432
+ site_url: base.site_url,
433
+ parent_endpoint: base.parent_endpoint,
434
+ framework: base.framework,
435
+ package_manager: base.package_manager,
436
+ approval_required: base.approval_required,
437
+ side_effects: base.side_effects,
438
+ excluded_side_effects: base.excluded_side_effects,
439
+ next_steps: base.next_steps,
440
+ files: base.files.filter((file) => file.path !== `${MANAGED_DIR}/install-state.json`)
441
+ };
442
+ base.install_id = hashText(stableJson(installIdentity)).slice(0, 24);
443
+ const stateContent = JSON.stringify(stateTemplate({ ...base, files: base.files }), null, 2) + "\n";
444
+ const stateFileIndex = base.files.findIndex((file) => file.path === `${MANAGED_DIR}/install-state.json`);
445
+ if (stateFileIndex < 0) throw installError("INSTALL_STATE_FILE_MISSING", "The install plan must include its managed state file.");
446
+ base.files[stateFileIndex].sha256 = hashText(stateContent);
447
+ return Object.freeze({
448
+ ...base,
449
+ files: files.map((file, index) => ({
450
+ ...base.files[index],
451
+ content: file.path === `${MANAGED_DIR}/install-state.json` ? stateContent : file.content
452
+ }))
453
+ });
344
454
  });
345
455
  }
346
456
 
@@ -708,6 +818,7 @@ module.exports = {
708
818
  buildInstallPlan,
709
819
  preflightInstallPlan,
710
820
  detectProject,
821
+ inspectAgentIdOpportunities,
711
822
  applyInstallPlan,
712
823
  resumeInstall,
713
824
  uninstallInstall,
package/local-policy.js CHANGED
@@ -7,6 +7,7 @@ const crypto = require("node:crypto");
7
7
  const POLICY_SCHEMA = "agentx-local-policy-v1";
8
8
  const MAX_BYTES = 1024 * 1024;
9
9
  const OPERATIONS = new Set(["upsert", "replace", "delete"]);
10
+ const APPROVAL_MODES = new Set(["auto", "review", "explicit"]);
10
11
  const SENSITIVE_PATHS = /(^|\/)(\.env(?:\.|$)|\.git|id_rsa|id_ed25519|secrets?|credentials?|private[-_.]?key|package-lock\.json)(\/|$)/i;
11
12
  const EXECUTABLE_PATHS = /\.(?:sh|bash|zsh|fish|cmd|bat|ps1|exe|dll|so|dylib|js|mjs|cjs|py|rb|php|pl)$/i;
12
13
  const SECRET_CONTENT = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:SITE_AGENT_KEY|AEO_AGENT_KEY|API_KEY|ACCESS_TOKEN|SECRET)\s*[:=]|\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b)/i;
@@ -57,7 +58,7 @@ function validRule(rule) {
57
58
  && safeRelative(rule.path_prefix) && !rule.path_prefix.includes("*")
58
59
  && Array.isArray(rule.allowed_operations) && rule.allowed_operations.length > 0 && rule.allowed_operations.every((item) => OPERATIONS.has(item))
59
60
  && Number.isInteger(rule.max_bytes) && rule.max_bytes > 0 && rule.max_bytes <= MAX_BYTES
60
- && rule.approval_mode === "explicit" && Number.isInteger(rule.version) && rule.version > 0;
61
+ && APPROVAL_MODES.has(rule.approval_mode) && Number.isInteger(rule.version) && rule.version > 0;
61
62
  }
62
63
  function approvePolicyRule(policy, rule, { approved = false, approvedBy = "", now = Math.floor(Date.now() / 1000) } = {}) {
63
64
  if (approved !== true || typeof approvedBy !== "string" || !approvedBy.trim()) throw policyError("policy_approval_required");
@@ -112,7 +113,8 @@ function authorizeTask(policy, task, currentResource) {
112
113
  for (const operation of task.operations) {
113
114
  if (!rule.allowed_operations.includes(operation.operation) || !pathWithinPrefix(operation.path, rule.path_prefix) || Buffer.byteLength(operation.value || "", "utf8") > rule.max_bytes) throw policyError("policy_denied");
114
115
  }
115
- return { authorized: true, policy_version: policy.version, policy_digest: policy.digest, rule_version: rule.version, approval_mode: rule.approval_mode };
116
+ const decision = rule.approval_mode === "auto" ? "auto" : "review";
117
+ return { authorized: true, decision, policy_version: policy.version, policy_digest: policy.digest, rule_version: rule.version, approval_mode: rule.approval_mode };
116
118
  }
117
119
 
118
120
  module.exports = { POLICY_SCHEMA, initializeLocalPolicy, loadLocalPolicy, approvePolicyRule, authorizeTask };
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+
3
+ function loopError(code) { const error = new Error(code); error.code = code; return error; }
4
+
5
+ function createOptimizationLoop({ session, executor } = {}) {
6
+ if (!session || !executor || typeof session.listTasks !== "function") throw loopError("OPTIMIZATION_LOOP_CONFIGURATION_INVALID");
7
+ async function advance(input, approvals) {
8
+ let task = input;
9
+ if (task.state === "draft") task = (await session.transitionTask(task.task_id, "propose", task.version)).task;
10
+ if (task.state === "proposed") {
11
+ const plan = await executor.plan(task);
12
+ if (plan.approval_required && !approvals.has(task.task_id)) return { task_id: task.task_id, status: "pending_customer_review", plan };
13
+ task = (await session.transitionTask(task.task_id, "approve", task.version)).task;
14
+ }
15
+ if (task.state === "approved") task = (await session.transitionTask(task.task_id, "execute", task.version)).task;
16
+ if (task.state === "executing") {
17
+ const approval = { approved: true, task_id: task.task_id, policy_digest: task.policy_digest };
18
+ const receipt = await executor.apply(task, approval);
19
+ task = (await session.transitionTask(task.task_id, "receipt", task.version, { receipt })).task;
20
+ }
21
+ if (task.state === "deployed") {
22
+ const result = await session.transitionTask(task.task_id, "verify", task.version, { attempt: 0 });
23
+ if (result.pending) return { task_id: task.task_id, status: "public_verification_pending", retry_after_seconds: result.public_result && result.public_result.retry_after_seconds || 30 };
24
+ task = result.task;
25
+ }
26
+ if (task.state === "verifying") task = (await session.transitionTask(task.task_id, "complete", task.version)).task;
27
+ let comparison = null;
28
+ if (task.state === "completed" && typeof session.createTaskComparison === "function") {
29
+ comparison = await session.createTaskComparison(task.task_id);
30
+ }
31
+ return { task_id: task.task_id, status: task.state, version: task.version, comparison_receipt_id: comparison && comparison.receipt && comparison.receipt.receipt_id || null };
32
+ }
33
+ return Object.freeze({
34
+ async runOnce({ approvedTaskIds = [] } = {}) {
35
+ const payload = await session.listTasks();
36
+ const approvals = new Set(approvedTaskIds.map(String));
37
+ const results = [];
38
+ for (const task of payload.tasks || []) {
39
+ if (["rejected", "expired", "rolled_back", "rollback_failed", "failed"].includes(task.state)) continue;
40
+ try { results.push(await advance(task, approvals)); }
41
+ catch (error) { results.push({ task_id: task.task_id, status: "error", error: String(error.code || error.message || "optimization_loop_failed") }); }
42
+ }
43
+ return { schema: "agentx-optimization-loop-run-v0.2", processed: results.length, results };
44
+ }
45
+ });
46
+ }
47
+
48
+ module.exports = { createOptimizationLoop };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twin3-ai/agent-id",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Domain-bound Agent identity, AEO evidence, and website-Agent integration SDK.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -16,7 +16,7 @@
16
16
  "npm": ">=10"
17
17
  },
18
18
  "scripts": {
19
- "prepublishOnly": "node --check bin/agent-id.js && node --check installer.js && node --check enterprise-identity.js && node --check release-verifier.js && node --check production-preflight.js"
19
+ "prepublishOnly": "node --check bin/agent-id.js && node --check installer.js && node --check enterprise-identity.js && node --check release-verifier.js && node --check production-preflight.js && node --check static-edge-adapter.js && node --check optimization-loop.js && node --check artifact-bundle.js"
20
20
  },
21
21
  "main": "site-agent.js",
22
22
  "exports": {
@@ -34,6 +34,9 @@
34
34
  "./trust-verifier": "./trust-verifier.js",
35
35
  "./repository-connector": "./repository-connector.js",
36
36
  "./cloudflare-worker-adapter": "./cloudflare-worker-adapter.js",
37
+ "./static-edge-adapter": "./static-edge-adapter.js",
38
+ "./optimization-loop": "./optimization-loop.js",
39
+ "./artifact-bundle": "./artifact-bundle.js",
37
40
  "./release-verifier": "./release-verifier.js",
38
41
  "./production-preflight": "./production-preflight.js"
39
42
  },
@@ -4,14 +4,15 @@ const fs = require("node:fs/promises");
4
4
  const path = require("node:path");
5
5
  const crypto = require("node:crypto");
6
6
 
7
- const ALLOWED_PATHS = new Set([
7
+ const AUTO_MANAGED_PATHS = new Set([
8
8
  "llms.txt",
9
- "robots.txt",
10
9
  ".well-known/agent-card.json",
11
10
  ".well-known/agent-id.json",
12
11
  ".well-known/aeo-agent.json",
13
12
  ".well-known/agent-knowledge.json"
14
13
  ]);
14
+ const REVIEW_ONLY_PATHS = new Set(["robots.txt"]);
15
+ const ALLOWED_PATHS = AUTO_MANAGED_PATHS;
15
16
  const SECRET_PATTERNS = [
16
17
  /\bak_aeo_[A-Za-z0-9_-]{6,}\b/,
17
18
  /\bav_[A-Za-z0-9_-]{8,}\b/,
@@ -35,6 +36,7 @@ function rootPath(value = process.cwd()) {
35
36
 
36
37
  function normalizeRelativePath(value) {
37
38
  const relative = String(value || "").replace(/\\/g, "/").replace(/^\.\//, "");
39
+ if (REVIEW_ONLY_PATHS.has(relative)) throw connectorError("CONNECTOR_REVIEW_ONLY_PATH", `Path requires customer-owned review and is not automatically managed: ${relative}`);
38
40
  if (!ALLOWED_PATHS.has(relative)) throw connectorError("CONNECTOR_PATH_NOT_ALLOWED", `Path is not on the connector allowlist: ${relative}`);
39
41
  return relative;
40
42
  }
@@ -157,6 +159,25 @@ async function buildImplementationPatch({ repositoryRoot = process.cwd(), implem
157
159
  });
158
160
  }
159
161
 
162
+ async function buildVerifiedArtifactPatch({ repositoryRoot = process.cwd(), artifactBundle, issuerPublicKey, expectedSubject, now } = {}) {
163
+ const { verifyArtifactBundle } = require("./artifact-bundle.js");
164
+ const verification = verifyArtifactBundle(artifactBundle, { issuerPublicKey, expectedSubject, now });
165
+ const plan = await buildRepositoryPatch({
166
+ repositoryRoot,
167
+ changes: artifactBundle.artifacts.map((artifact) => ({
168
+ path: artifact.path,
169
+ content: artifact.content,
170
+ reason: `Signed Agent ID artifact bundle ${verification.bundle_hash}`
171
+ }))
172
+ });
173
+ return {
174
+ ...plan,
175
+ artifact_bundle_hash: verification.bundle_hash,
176
+ artifact_bundle_source: { ...artifactBundle.source },
177
+ artifact_bundle_expires_at: artifactBundle.expires_at
178
+ };
179
+ }
180
+
160
181
  function validatePlan(plan, expectedRoot) {
161
182
  if (!plan || plan.schema !== "agentx-repository-patch-plan-v1" || plan.approval_required !== true) throw connectorError("CONNECTOR_INVALID_PLAN", "Invalid repository patch plan.");
162
183
  const root = rootPath(plan.repository_root);
@@ -216,6 +237,9 @@ async function applyRepositoryPatch(plan, { approved = false } = {}) {
216
237
  repository_root: root,
217
238
  applied: true,
218
239
  default_branch_push: false,
240
+ artifact_bundle_hash: String(plan.artifact_bundle_hash || ""),
241
+ artifact_bundle_source: plan.artifact_bundle_source ? { ...plan.artifact_bundle_source } : null,
242
+ artifact_bundle_expires_at: Number(plan.artifact_bundle_expires_at || 0),
219
243
  applied_changes: appliedChanges,
220
244
  rollback_entries: rollbackEntries
221
245
  };
@@ -249,13 +273,16 @@ async function rollbackRepositoryPatch(receiptOrRoot, { approved = false } = {})
249
273
  else await fs.rm(absolute, { force: true });
250
274
  restored.push({ path: relative, before_sha256: item.before_sha256 });
251
275
  }
252
- return { schema: "agentx-repository-rollback-receipt-v1", connector: "repository_patch", plan_id: receipt.plan_id || "", rolled_back: true, files: restored };
276
+ return { schema: "agentx-repository-rollback-receipt-v1", connector: "repository_patch", plan_id: receipt.plan_id || "", artifact_bundle_hash: String(receipt.artifact_bundle_hash || ""), rolled_back: true, files: restored };
253
277
  }
254
278
 
255
279
  module.exports = {
256
280
  ALLOWED_PATHS,
281
+ AUTO_MANAGED_PATHS,
282
+ REVIEW_ONLY_PATHS,
257
283
  buildRepositoryPatch,
258
284
  buildImplementationPatch,
285
+ buildVerifiedArtifactPatch,
259
286
  deploymentChanges,
260
287
  applyRepositoryPatch,
261
288
  rollbackRepositoryPatch,
package/site-agent.js CHANGED
@@ -362,7 +362,7 @@ function createSiteAgentClient(options = {}) {
362
362
  ts: payload.ts == null ? Math.floor(Date.now() / 1000) : payload.ts,
363
363
  data: {
364
364
  runtime: payload.runtime || "server_side_site_agent",
365
- sdk_version: payload.sdk_version || payload.sdkVersion || "0.1.0"
365
+ sdk_version: payload.sdk_version || payload.sdkVersion || "0.2.0"
366
366
  }
367
367
  }]
368
368
  });
@@ -379,7 +379,7 @@ function createSiteAgentClient(options = {}) {
379
379
  ts: payload.ts == null ? Math.floor(Date.now() / 1000) : payload.ts,
380
380
  data: {
381
381
  runtime: payload.runtime || "server_side_site_agent",
382
- sdk_version: payload.sdk_version || payload.sdkVersion || "0.1.0"
382
+ sdk_version: payload.sdk_version || payload.sdkVersion || "0.2.0"
383
383
  }
384
384
  }]
385
385
  });
@@ -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.1.0");
128
+ const runtimeVersion = String(options.runtimeVersion || process.env.AGENT_ID_RUNTIME_VERSION || "0.2.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: true, plan_hash: hash(canonical(prepared.plan)), changes: prepared.plan.changes.map(({ after_content, rollback_entries, ...item }) => item) };
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) return existing.result;
87
- if (approval.approved !== true || approval.task_id !== task.task_id || approval.policy_digest !== policy.digest) throw executorError("approval_required");
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] = { ...state.tasks[task.task_id], state: "deployed", receipt_hash: publicReceipt.receipt_hash, result: publicReceipt };
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 receipt = await connector.rollback(prepared && prepared.connectorReceipt);
121
- state.tasks[taskId] = { ...taskState, state: "rolled_back", rollback_receipt_hash: hash(canonical(receipt)) };
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
  },