@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/installer.js ADDED
@@ -0,0 +1,722 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs/promises");
4
+ const path = require("node:path");
5
+ const crypto = require("node:crypto");
6
+ const { DEFAULT_ENDPOINT } = require("./runtime-config.js");
7
+
8
+ const INSTALL_SCHEMA = "agentx-install-plan-v1";
9
+ const STATE_SCHEMA = "agentx-site-agent-install-state-v1";
10
+ const MANAGED_DIR = ".agent-id";
11
+ const SECRET_IGNORE = "*\n!.gitignore\n";
12
+
13
+ function installError(code, message) {
14
+ const error = new Error(message);
15
+ error.code = code;
16
+ return error;
17
+ }
18
+
19
+ function normalizeProjectRoot(projectRoot = process.cwd()) {
20
+ const root = path.resolve(String(projectRoot));
21
+ if (!path.isAbsolute(root)) throw installError("INVALID_PROJECT_ROOT", "Project root must be an absolute path.");
22
+ return root;
23
+ }
24
+
25
+ function normalizeSiteUrl(value) {
26
+ let parsed;
27
+ try {
28
+ parsed = new URL(String(value || ""));
29
+ } catch (_err) {
30
+ throw installError("INVALID_SITE_URL", "Site URL must be a valid http or https URL.");
31
+ }
32
+ if (!["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password || !parsed.hostname) {
33
+ throw installError("INVALID_SITE_URL", "Site URL must be a public http or https URL without credentials.");
34
+ }
35
+ if (parsed.search || parsed.hash) throw installError("INVALID_SITE_URL", "Site URL must not contain a query string or fragment.");
36
+ parsed.pathname = parsed.pathname.replace(/\/{2,}/g, "/").replace(/\/$/, "") || "/";
37
+ return parsed.toString().replace(/\/$/, "") || parsed.origin;
38
+ }
39
+
40
+ async function readJson(filePath) {
41
+ try {
42
+ return JSON.parse(await fs.readFile(filePath, "utf8"));
43
+ } catch (error) {
44
+ if (error.code === "ENOENT") return null;
45
+ throw error;
46
+ }
47
+ }
48
+
49
+ async function detectProject({ projectRoot = process.cwd(), fsImpl = fs } = {}) {
50
+ const root = normalizeProjectRoot(projectRoot);
51
+ const packageJson = await (async () => {
52
+ try { return JSON.parse(await fsImpl.readFile(path.join(root, "package.json"), "utf8")); }
53
+ catch (error) { return error.code === "ENOENT" ? null : (() => { throw error; })(); }
54
+ })();
55
+ const dependencies = {
56
+ ...(packageJson && packageJson.dependencies ? packageJson.dependencies : {}),
57
+ ...(packageJson && packageJson.devDependencies ? packageJson.devDependencies : {})
58
+ };
59
+ const exists = async (name) => {
60
+ try { await fsImpl.access(path.join(root, name)); return true; }
61
+ catch (_err) { return false; }
62
+ };
63
+ const dependencyNames = Object.keys(dependencies);
64
+ const hasStaticEntry = await exists("index.html") || await exists("public/index.html");
65
+ const cliOnlyStaticSite = Boolean(
66
+ packageJson
67
+ && hasStaticEntry
68
+ && dependencyNames.length > 0
69
+ && dependencyNames.every((name) => name === "@twin3-ai/agent-id")
70
+ );
71
+ const framework = dependencies.next ? "next"
72
+ : dependencies.nuxt ? "nuxt"
73
+ : dependencies.astro ? "astro"
74
+ : dependencies.gatsby ? "gatsby"
75
+ : dependencies.svelte || dependencies["@sveltejs/kit"] ? "svelte"
76
+ : dependencies.vite ? "vite"
77
+ : cliOnlyStaticSite ? "static"
78
+ : packageJson ? "node" : "static";
79
+ const packageManager = await exists("pnpm-lock.yaml") ? "pnpm"
80
+ : await exists("yarn.lock") ? "yarn"
81
+ : await exists("bun.lockb") || await exists("bun.lock") ? "bun"
82
+ : packageJson ? "npm" : "none";
83
+ const detectedFiles = [];
84
+ for (const name of ["package.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "package-lock.json"]) {
85
+ if (await exists(name)) detectedFiles.push(name);
86
+ }
87
+ return {
88
+ project_root: root,
89
+ framework,
90
+ package_manager: packageManager,
91
+ package_name: packageJson && typeof packageJson.name === "string" ? packageJson.name : "",
92
+ detected_files: detectedFiles
93
+ };
94
+ }
95
+
96
+ function stableValue(value) {
97
+ if (Array.isArray(value)) return value.map(stableValue);
98
+ if (!value || typeof value !== "object") return value;
99
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])]));
100
+ }
101
+
102
+ function stableJson(value) {
103
+ return JSON.stringify(stableValue(value));
104
+ }
105
+
106
+ function hashText(value) {
107
+ return `sha256:${crypto.createHash("sha256").update(value).digest("hex")}`;
108
+ }
109
+
110
+ function runtimeTemplate() {
111
+ return `"use strict";
112
+
113
+ // Server-side only helper. Keep SITE_AGENT_KEY in the host secret store.
114
+ const { createSiteAgentClient } = require("@twin3-ai/agent-id");
115
+
116
+ function createAgentIdRuntime(options = {}) {
117
+ return createSiteAgentClient({
118
+ endpoint: options.endpoint || process.env.AGENT_ID_ENDPOINT,
119
+ agentKey: options.agentKey || process.env.SITE_AGENT_KEY
120
+ });
121
+ }
122
+
123
+ module.exports = { createAgentIdRuntime };
124
+ `;
125
+ }
126
+
127
+ function staticRunnerTemplate(siteUrl) {
128
+ return `"use strict";
129
+
130
+ const fs = require("node:fs/promises");
131
+ const path = require("node:path");
132
+
133
+ const SITE_URL = process.env.SITE_URL || ${JSON.stringify(siteUrl)};
134
+ const AGENT_ID_ENDPOINT = String(process.env.AGENT_ID_ENDPOINT || "").replace(/\\/+$/, "");
135
+ const SITE_AGENT_KEY = process.env.SITE_AGENT_KEY || "";
136
+ const SYNC_MODE = process.env.AGENT_ID_SYNC_MODE || "full";
137
+
138
+ function redact(value) {
139
+ if (Array.isArray(value)) return value.map(redact);
140
+ if (!value || typeof value !== "object") return value;
141
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
142
+ key,
143
+ /key|token|secret|authorization/i.test(key) ? "[REDACTED]" : redact(item)
144
+ ]));
145
+ }
146
+
147
+ async function call(action, payload = {}) {
148
+ const response = await fetch(AGENT_ID_ENDPOINT + "/api/site_agents/call", {
149
+ method: "POST",
150
+ headers: { "content-type": "application/json" },
151
+ body: JSON.stringify({ ...payload, url: SITE_URL, action, agent_key: SITE_AGENT_KEY })
152
+ });
153
+ const text = await response.text();
154
+ let body;
155
+ try { body = JSON.parse(text || "{}"); } catch (_error) { body = { text }; }
156
+ if (!response.ok || body.ok === false) {
157
+ const error = new Error("Agent ID sync failed: " + response.status);
158
+ error.status = response.status;
159
+ error.body = redact(body);
160
+ throw error;
161
+ }
162
+ return body;
163
+ }
164
+
165
+ async function main() {
166
+ if (!AGENT_ID_ENDPOINT) throw new Error("AGENT_ID_ENDPOINT is required. Use the stable Agent ID API domain supplied by the service owner.");
167
+ if (!SITE_AGENT_KEY) throw new Error("SITE_AGENT_KEY is required in the customer-owned runner environment.");
168
+ if (!["heartbeat", "monthly", "full"].includes(SYNC_MODE)) throw new Error("AGENT_ID_SYNC_MODE must be heartbeat, monthly, or full.");
169
+ const observedAt = new Date().toISOString();
170
+ const heartbeat = SYNC_MODE === "monthly" ? { skipped: true, reason: "monthly_insights_run" } : await call("site_agent_events", {
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" } }]
173
+ });
174
+ const insights = SYNC_MODE === "heartbeat" ? { skipped: true, reason: "daily_heartbeat_only" } : await call("insights_feed", {
175
+ source: "customer_owned_github_actions_runner",
176
+ audit_url: true,
177
+ cadence: "monthly"
178
+ });
179
+ const report = redact({
180
+ schema: "agentx-customer-owned-sync-report-v1",
181
+ site_url: SITE_URL,
182
+ parent_endpoint: AGENT_ID_ENDPOINT,
183
+ observed_at: observedAt,
184
+ sync_mode: SYNC_MODE,
185
+ heartbeat,
186
+ insights,
187
+ authority: "advisory_only",
188
+ customer_approval_required_for_site_changes: true
189
+ });
190
+ const target = path.join(process.cwd(), ".agent-id/reports/latest.json");
191
+ await fs.mkdir(path.dirname(target), { recursive: true });
192
+ await fs.writeFile(target, JSON.stringify(report, null, 2) + "\\n", "utf8");
193
+ process.stdout.write(JSON.stringify({ ok: true, report: ".agent-id/reports/latest.json", observed_at: observedAt }) + "\\n");
194
+ }
195
+
196
+ main().catch((error) => {
197
+ console.error(JSON.stringify({ ok: false, error: error.message, status: error.status || 0, details: error.body || null }));
198
+ process.exit(1);
199
+ });
200
+ `;
201
+ }
202
+
203
+ function staticWorkflowTemplate(siteUrl) {
204
+ return [
205
+ "name: Agent ID website sync",
206
+ "",
207
+ "on:",
208
+ " workflow_dispatch:",
209
+ " schedule:",
210
+ " - cron: '17 2 * * *'",
211
+ " - cron: '23 3 1 * *'",
212
+ "",
213
+ "permissions:",
214
+ " contents: read",
215
+ "",
216
+ "jobs:",
217
+ " sync:",
218
+ " runs-on: ubuntu-latest",
219
+ " timeout-minutes: 10",
220
+ " steps:",
221
+ " - uses: actions/checkout@v4",
222
+ " - uses: actions/setup-node@v4",
223
+ " with:",
224
+ " node-version: '20'",
225
+ " - name: Pull verified insights from Agent ID",
226
+ " run: node .agent-id/runner.cjs",
227
+ " env:",
228
+ ` SITE_URL: ${siteUrl}`,
229
+ " AGENT_ID_ENDPOINT: ${{ vars.AGENT_ID_ENDPOINT }}",
230
+ " AGENT_ID_SYNC_MODE: ${{ github.event_name == 'workflow_dispatch' && 'full' || github.event.schedule == '23 3 1 * *' && 'monthly' || 'heartbeat' }}",
231
+ " SITE_AGENT_KEY: ${{ secrets.SITE_AGENT_KEY }}",
232
+ " - name: Upload redacted sync report",
233
+ " if: always()",
234
+ " uses: actions/upload-artifact@v4",
235
+ " with:",
236
+ " name: agent-id-sync-report",
237
+ " path: .agent-id/reports/latest.json",
238
+ " if-no-files-found: error",
239
+ " retention-days: 30",
240
+ ""
241
+ ].join("\n");
242
+ }
243
+
244
+ function readmeTemplate(siteUrl, framework, packageManager, endpoint) {
245
+ if (framework === "static") {
246
+ return `# Agent ID Site Agent installation
247
+
248
+ - Site URL: ${siteUrl}
249
+ - Parent service: ${endpoint}
250
+ - Runtime: customer-owned GitHub Actions Runner
251
+
252
+ ## Required owner actions
253
+
254
+ 1. Publish the domain verification document returned by Agent ID.
255
+ 2. Verify domain ownership and obtain the Site Agent Key.
256
+ 3. Save the stable API origin as the repository variable \`AGENT_ID_ENDPOINT\`; do not use a Cloud Run revision URL.
257
+ 4. Save the key as the repository secret \`SITE_AGENT_KEY\`.
258
+ 5. Run the \`Agent ID website sync\` workflow once. Keep the daily heartbeat and monthly insights schedules enabled.
259
+ 6. Review the uploaded \`agent-id-sync-report\` artifact before approving website changes.
260
+
261
+ The Runner sends a daily connection heartbeat. It pulls evidence-bound optimization insights on the first day of each month; a manual run performs both. It does not expose the key, commit code, deploy the site, or give Agent ID direct write access to this repository.
262
+ `;
263
+ }
264
+ return `# Agent ID Site Agent installation
265
+
266
+ - Site URL: ${siteUrl}
267
+ - Detected framework: ${framework}
268
+ - Detected package manager: ${packageManager}
269
+
270
+ ## Next steps
271
+
272
+ 1. Publish the domain verification file or DNS record returned by Agent ID.
273
+ 2. Run domain verification from the website owner environment.
274
+ 3. Install \`@twin3-ai/agent-id\` in the website server, Worker, CMS connector, or Agent runtime.
275
+ 4. Store the issued Site Agent Key as \`SITE_AGENT_KEY\` in the server secret store.
276
+ 5. Start \`agent-id-sync --url ${siteUrl} --once\` for a first evidence exchange.
277
+
278
+ This directory contains only the local install state and a server-side helper template. It never contains a Site Agent Key, browser token, customer content, or deployment credentials. The Agent ID service does not receive direct write access to this website by installing this package.
279
+ `;
280
+ }
281
+
282
+ function stateTemplate(plan) {
283
+ return {
284
+ schema: STATE_SCHEMA,
285
+ install_id: plan.install_id,
286
+ site_url: plan.site_url,
287
+ host: new URL(plan.site_url).hostname,
288
+ parent_endpoint: plan.parent_endpoint,
289
+ framework: plan.framework,
290
+ package_manager: plan.package_manager,
291
+ status: "installed",
292
+ verification_status: "pending_domain_verification",
293
+ requires_site_agent_key: true,
294
+ site_agent_key_storage: plan.framework === "static"
295
+ ? "github_actions_secret:SITE_AGENT_KEY"
296
+ : "server_secret_store:SITE_AGENT_KEY",
297
+ managed_paths: plan.files.map((file) => file.path),
298
+ no_direct_customer_site_mutation: true,
299
+ no_secret_in_state: true
300
+ };
301
+ }
302
+
303
+ function buildInstallPlan({ url, projectRoot = process.cwd(), endpoint = DEFAULT_ENDPOINT, framework, packageManager, detected } = {}) {
304
+ return Promise.resolve().then(async () => {
305
+ const siteUrl = normalizeSiteUrl(url);
306
+ const root = normalizeProjectRoot(projectRoot);
307
+ const detectedProject = detected || await detectProject({ projectRoot: root });
308
+ const selectedFramework = framework || detectedProject.framework;
309
+ const selectedPackageManager = packageManager || detectedProject.package_manager;
310
+ if (!/^[a-z][a-z0-9._-]{0,31}$/.test(String(selectedFramework))) throw installError("INVALID_FRAMEWORK", "Framework identifier is invalid.");
311
+ if (!/^[a-z][a-z0-9._-]{0,31}$/.test(String(selectedPackageManager))) throw installError("INVALID_PACKAGE_MANAGER", "Package manager identifier is invalid.");
312
+ const parentEndpoint = String(endpoint || DEFAULT_ENDPOINT).replace(/\/+$/, "");
313
+ const files = selectedFramework === "static" ? [
314
+ { path: `${MANAGED_DIR}/install-state.json`, mode: "private", content: "" },
315
+ { path: `${MANAGED_DIR}/runner.cjs`, mode: "customer_runner", content: staticRunnerTemplate(siteUrl, parentEndpoint) },
316
+ { path: `${MANAGED_DIR}/README.md`, mode: "documentation", content: readmeTemplate(siteUrl, selectedFramework, selectedPackageManager, parentEndpoint) },
317
+ { path: ".github/workflows/agent-id-sync.yml", mode: "customer_runner_workflow", content: staticWorkflowTemplate(siteUrl, parentEndpoint) }
318
+ ] : [
319
+ { path: `${MANAGED_DIR}/install-state.json`, mode: "private", content: "" },
320
+ { path: `${MANAGED_DIR}/site-agent.cjs`, mode: "server_helper", content: runtimeTemplate() },
321
+ { path: `${MANAGED_DIR}/README.md`, mode: "documentation", content: readmeTemplate(siteUrl, selectedFramework, selectedPackageManager, parentEndpoint) }
322
+ ];
323
+ const base = {
324
+ schema: INSTALL_SCHEMA,
325
+ version: 1,
326
+ site_url: siteUrl,
327
+ parent_endpoint: parentEndpoint,
328
+ project_root: root,
329
+ framework: selectedFramework,
330
+ package_manager: selectedPackageManager,
331
+ approval_required: true,
332
+ side_effects: selectedFramework === "static"
333
+ ? ["write_agent_id_namespace", "write_dedicated_github_actions_workflow"]
334
+ : ["write_only_agent_id_namespace"],
335
+ excluded_side_effects: ["package_install", "customer_content_change", "deployment", "secret_creation", "database_change", "default_branch_push"],
336
+ next_steps: selectedFramework === "static"
337
+ ? ["publish_domain_proof", "verify_domain_ownership", "store_SITE_AGENT_KEY_as_GitHub_Actions_secret", "run_customer_owned_sync_workflow"]
338
+ : ["verify_domain_ownership", "install_server_package", "store_SITE_AGENT_KEY_in_server_secret_store", "start_site_agent_sync"],
339
+ files: files.map(({ path: filePath, mode, content }) => ({ path: filePath, mode, sha256: hashText(content) }))
340
+ };
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 })) });
344
+ });
345
+ }
346
+
347
+ function validatePlan(plan) {
348
+ if (!plan || plan.schema !== INSTALL_SCHEMA || plan.approval_required !== true) throw installError("INVALID_INSTALL_PLAN", "The installation plan is invalid or missing approval metadata.");
349
+ const expectedCount = plan.framework === "static" ? 4 : 3;
350
+ if (!Array.isArray(plan.files) || plan.files.length !== expectedCount) throw installError("INVALID_INSTALL_PLAN", `The installation plan must contain exactly ${expectedCount} managed files.`);
351
+ const root = normalizeProjectRoot(plan.project_root);
352
+ for (const file of plan.files) {
353
+ const allowedWorkflow = plan.framework === "static" && file && file.path === ".github/workflows/agent-id-sync.yml";
354
+ if (!file || typeof file.path !== "string" || path.isAbsolute(file.path) || file.path.split(/[\\/]+/).includes("..") || (!file.path.startsWith(`${MANAGED_DIR}/`) && !allowedWorkflow)) {
355
+ throw installError("UNSAFE_INSTALL_PATH", "Installation can only write managed Agent ID files and its dedicated GitHub Actions workflow.");
356
+ }
357
+ }
358
+ return root;
359
+ }
360
+
361
+ async function writeAtomic(filePath, content, mode) {
362
+ await fs.mkdir(path.dirname(filePath), { recursive: true, mode: mode === "private" ? 0o700 : 0o755 });
363
+ const temp = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
364
+ try {
365
+ await fs.writeFile(temp, content, { encoding: "utf8", mode: mode === "private" ? 0o600 : 0o644 });
366
+ await fs.rename(temp, filePath);
367
+ await fs.chmod(filePath, mode === "private" ? 0o600 : 0o644);
368
+ } finally {
369
+ await fs.rm(temp, { force: true }).catch(() => {});
370
+ }
371
+ }
372
+
373
+ async function prepareEnterpriseSecretDirectory(projectRoot = process.cwd()) {
374
+ const root = normalizeProjectRoot(projectRoot);
375
+ const realRoot = await fs.realpath(root);
376
+ const managedDirectory = path.join(root, MANAGED_DIR);
377
+ let managedStat;
378
+ try { managedStat = await fs.lstat(managedDirectory); }
379
+ catch (error) {
380
+ if (error.code !== "ENOENT") throw error;
381
+ await fs.mkdir(managedDirectory, { mode: 0o700 });
382
+ managedStat = await fs.lstat(managedDirectory);
383
+ }
384
+ if (managedStat.isSymbolicLink() || !managedStat.isDirectory()) {
385
+ throw installError("UNSAFE_ENTERPRISE_SECRET_DIRECTORY", "The .agent-id path must be a real directory.");
386
+ }
387
+ const realManagedDirectory = await fs.realpath(managedDirectory);
388
+ if (realManagedDirectory !== realRoot && !realManagedDirectory.startsWith(realRoot + path.sep)) {
389
+ throw installError("UNSAFE_ENTERPRISE_SECRET_DIRECTORY", "The .agent-id path must remain in the customer project.");
390
+ }
391
+ await fs.chmod(managedDirectory, 0o700);
392
+ const secretDirectory = path.join(managedDirectory, "secrets");
393
+ let directoryStat;
394
+ try { directoryStat = await fs.lstat(secretDirectory); }
395
+ catch (error) {
396
+ if (error.code !== "ENOENT") throw error;
397
+ await fs.mkdir(secretDirectory, { mode: 0o700 });
398
+ directoryStat = await fs.lstat(secretDirectory);
399
+ }
400
+ const realSecretDirectory = await fs.realpath(secretDirectory);
401
+ if (
402
+ directoryStat.isSymbolicLink()
403
+ || !directoryStat.isDirectory()
404
+ || (realSecretDirectory !== realRoot && !realSecretDirectory.startsWith(realRoot + path.sep))
405
+ ) {
406
+ throw installError("UNSAFE_ENTERPRISE_SECRET_DIRECTORY", "Enterprise secrets must remain in the customer project.");
407
+ }
408
+ await fs.chmod(secretDirectory, 0o700);
409
+ const ignorePath = path.join(secretDirectory, ".gitignore");
410
+ let existingIgnore = null;
411
+ try { existingIgnore = await fs.readFile(ignorePath, "utf8"); }
412
+ catch (error) { if (error.code !== "ENOENT") throw error; }
413
+ if (existingIgnore !== null && existingIgnore !== SECRET_IGNORE) {
414
+ throw installError("ENTERPRISE_SECRET_IGNORE_CONFLICT", "Refusing to replace a customer-managed secret ignore file.");
415
+ }
416
+ if (existingIgnore === null) await writeAtomic(ignorePath, SECRET_IGNORE, "documentation");
417
+ return {
418
+ secret_directory: secretDirectory,
419
+ key_path: path.join(secretDirectory, "enterprise-ed25519.pem"),
420
+ credential_path: path.join(secretDirectory, "enterprise-credential.json")
421
+ };
422
+ }
423
+
424
+ async function writeEnterpriseBootstrapCredential(projectRoot, credential, bootstrapSecret) {
425
+ const details = credential && typeof credential === "object" ? credential : {};
426
+ const secret = String(bootstrapSecret || "");
427
+ const required = ["credential_id", "tenant_id", "host", "environment", "agent_id"];
428
+ if (required.some((field) => typeof details[field] !== "string" || !details[field]) || !secret.startsWith("bsc_")) {
429
+ throw installError("INVALID_ENTERPRISE_CREDENTIAL", "A complete bootstrap credential is required.");
430
+ }
431
+ const paths = await prepareEnterpriseSecretDirectory(projectRoot);
432
+ try {
433
+ await fs.lstat(paths.credential_path);
434
+ throw installError("ENTERPRISE_CREDENTIAL_CONFLICT", "Refusing to overwrite an existing enterprise credential.");
435
+ } catch (error) {
436
+ if (error.code !== "ENOENT") throw error;
437
+ }
438
+ const stored = {
439
+ schema: "agentx-customer-bootstrap-credential-v1",
440
+ credential_id: details.credential_id,
441
+ client_id: String(details.client_id || ""),
442
+ key_id: String(details.key_id || ""),
443
+ tenant_id: details.tenant_id,
444
+ host: details.host,
445
+ environment: details.environment,
446
+ agent_id: details.agent_id,
447
+ public_key_thumbprint: String(details.public_key_thumbprint || ""),
448
+ maximum_scopes: Array.isArray(details.maximum_scopes) ? [...details.maximum_scopes] : [],
449
+ status: String(details.status || "active"),
450
+ issued_at: Number(details.issued_at || 0),
451
+ expires_at: Number(details.expires_at || 0),
452
+ bootstrap_secret: secret
453
+ };
454
+ const serialized = JSON.stringify(stored, null, 2) + "\n";
455
+ if (/access_token|PRIVATE KEY/.test(serialized)) {
456
+ throw installError("ENTERPRISE_CREDENTIAL_CONTENT_INVALID", "Credential storage cannot contain access tokens or private keys.");
457
+ }
458
+ await writeAtomic(paths.credential_path, serialized, "private");
459
+ return {
460
+ schema: "agentx-enterprise-credential-write-receipt-v1",
461
+ credential_id: stored.credential_id,
462
+ credential_path: paths.credential_path,
463
+ mode: "0600",
464
+ secret_persisted_locally: true
465
+ };
466
+ }
467
+
468
+ async function replaceEnterpriseBootstrapCredential(projectRoot, credential, bootstrapSecret, expectedCredentialId) {
469
+ const existing = await readEnterpriseBootstrapCredential(projectRoot);
470
+ if (!existing || existing.credential.credential_id !== String(expectedCredentialId || "")) {
471
+ throw installError("ENTERPRISE_CREDENTIAL_REPLACEMENT_MISMATCH", "The local credential changed before rotation could be committed.");
472
+ }
473
+ const details = credential && typeof credential === "object" ? credential : {};
474
+ const secret = String(bootstrapSecret || "");
475
+ const required = ["credential_id", "tenant_id", "host", "environment", "agent_id", "public_key_thumbprint"];
476
+ if (required.some((field) => typeof details[field] !== "string" || !details[field]) || !secret.startsWith("bsc_")) {
477
+ throw installError("INVALID_ENTERPRISE_CREDENTIAL", "A complete replacement credential is required.");
478
+ }
479
+ for (const field of ["tenant_id", "host", "environment", "agent_id", "public_key_thumbprint"]) {
480
+ if (details[field] !== existing.credential[field]) {
481
+ throw installError("ENTERPRISE_CREDENTIAL_REPLACEMENT_MISMATCH", `Replacement credential changed ${field}.`);
482
+ }
483
+ }
484
+ const stored = {
485
+ schema: "agentx-customer-bootstrap-credential-v1",
486
+ credential_id: details.credential_id,
487
+ client_id: String(details.client_id || ""),
488
+ key_id: String(details.key_id || ""),
489
+ tenant_id: details.tenant_id,
490
+ host: details.host,
491
+ environment: details.environment,
492
+ agent_id: details.agent_id,
493
+ public_key_thumbprint: details.public_key_thumbprint,
494
+ maximum_scopes: Array.isArray(details.maximum_scopes) ? [...details.maximum_scopes] : [],
495
+ status: String(details.status || "active"),
496
+ issued_at: Number(details.issued_at || 0),
497
+ expires_at: Number(details.expires_at || 0),
498
+ bootstrap_secret: secret
499
+ };
500
+ await writeAtomic(existing.credential_path, JSON.stringify(stored, null, 2) + "\n", "private");
501
+ return {
502
+ schema: "agentx-enterprise-credential-rotation-receipt-v1",
503
+ previous_credential_id: existing.credential.credential_id,
504
+ credential_id: stored.credential_id,
505
+ credential_path: existing.credential_path,
506
+ mode: "0600",
507
+ secret_replaced_atomically: true
508
+ };
509
+ }
510
+
511
+ async function removeEnterpriseBootstrapCredential(projectRoot, expectedCredentialId) {
512
+ const existing = await readEnterpriseBootstrapCredential(projectRoot);
513
+ if (!existing || existing.credential.credential_id !== String(expectedCredentialId || "")) {
514
+ throw installError("ENTERPRISE_CREDENTIAL_REVOKE_MISMATCH", "The local credential changed before revocation could be finalized.");
515
+ }
516
+ await fs.rm(existing.credential_path, { force: true });
517
+ return {
518
+ schema: "agentx-enterprise-credential-revocation-receipt-v1",
519
+ credential_id: existing.credential.credential_id,
520
+ credential_removed: true,
521
+ private_key_retained: true
522
+ };
523
+ }
524
+
525
+ async function readEnterpriseBootstrapCredential(projectRoot = process.cwd()) {
526
+ const paths = await prepareEnterpriseSecretDirectory(projectRoot);
527
+ const keyStat = await fs.lstat(paths.key_path).catch((error) => {
528
+ if (error.code === "ENOENT") throw installError("ENTERPRISE_PRIVATE_KEY_MISSING", "Enterprise private key is required before domain verification.");
529
+ throw error;
530
+ });
531
+ if (keyStat.isSymbolicLink() || !keyStat.isFile() || (keyStat.mode & 0o777) !== 0o600) {
532
+ throw installError("ENTERPRISE_PRIVATE_KEY_INVALID", "Enterprise private key must be a regular 0600 file.");
533
+ }
534
+ let stat;
535
+ try { stat = await fs.lstat(paths.credential_path); }
536
+ catch (error) {
537
+ if (error.code === "ENOENT") return null;
538
+ throw error;
539
+ }
540
+ if (stat.isSymbolicLink() || !stat.isFile() || (stat.mode & 0o777) !== 0o600) {
541
+ throw installError("ENTERPRISE_CREDENTIAL_FILE_INVALID", "Enterprise credential must be a regular 0600 file.");
542
+ }
543
+ const credential = await readJson(paths.credential_path);
544
+ const required = ["credential_id", "tenant_id", "host", "environment", "agent_id", "public_key_thumbprint"];
545
+ if (
546
+ !credential
547
+ || credential.schema !== "agentx-customer-bootstrap-credential-v1"
548
+ || required.some((field) => typeof credential[field] !== "string" || !credential[field])
549
+ || !String(credential.bootstrap_secret || "").startsWith("bsc_")
550
+ ) {
551
+ throw installError("ENTERPRISE_CREDENTIAL_FILE_INVALID", "Enterprise credential is incomplete or invalid.");
552
+ }
553
+ return { credential, credential_path: paths.credential_path, key_path: paths.key_path };
554
+ }
555
+
556
+ function installContents(plan) {
557
+ const state = stateTemplate(plan);
558
+ return {
559
+ state,
560
+ contents: {
561
+ [`${MANAGED_DIR}/install-state.json`]: JSON.stringify(state, null, 2) + "\n",
562
+ [`${MANAGED_DIR}/site-agent.cjs`]: runtimeTemplate(),
563
+ [`${MANAGED_DIR}/runner.cjs`]: staticRunnerTemplate(plan.site_url),
564
+ [`${MANAGED_DIR}/README.md`]: readmeTemplate(plan.site_url, plan.framework, plan.package_manager, plan.parent_endpoint),
565
+ ".github/workflows/agent-id-sync.yml": staticWorkflowTemplate(plan.site_url)
566
+ }
567
+ };
568
+ }
569
+
570
+ async function inspectInstallPlan(plan) {
571
+ const root = validatePlan(plan);
572
+ const { state, contents } = installContents(plan);
573
+ const prepared = [];
574
+ for (const file of plan.files) {
575
+ const target = path.resolve(root, file.path);
576
+ if (target !== root && !target.startsWith(root + path.sep)) throw installError("UNSAFE_INSTALL_PATH", "Installation path escaped the project root.");
577
+ const content = contents[file.path];
578
+ if (typeof content !== "string" || hashText(content) !== file.sha256) throw installError("PLAN_CONTENT_MISMATCH", `Install plan content mismatch for ${file.path}.`);
579
+ let existing = null;
580
+ try { existing = await fs.readFile(target, "utf8"); } catch (error) { if (error.code !== "ENOENT") throw error; }
581
+ if (existing !== null && existing !== content) throw installError("INSTALL_CONFLICT", `Refusing to overwrite an existing file: ${file.path}`);
582
+ prepared.push({ file, target, content, existing });
583
+ }
584
+ return { root, state, prepared };
585
+ }
586
+
587
+ async function preflightInstallPlan(plan) {
588
+ const { prepared } = await inspectInstallPlan(plan);
589
+ return {
590
+ schema: "agentx-install-preflight-v1",
591
+ ok: true,
592
+ install_id: plan.install_id,
593
+ files: prepared.map(({ file, existing }) => ({ path: file.path, action: existing === null ? "create" : "unchanged" })),
594
+ side_effects: [],
595
+ };
596
+ }
597
+
598
+ async function applyInstallPlan(plan, { approved = false } = {}) {
599
+ if (approved !== true) throw installError("INSTALL_APPROVAL_REQUIRED", "Installation requires explicit approved=true.");
600
+ const { state, prepared } = await inspectInstallPlan(plan);
601
+ const result = { schema: "agentx-install-receipt-v1", install_id: plan.install_id, changed: false, files: [], state: state.status };
602
+ for (const { file, target, content, existing } of prepared) {
603
+ if (existing === null) {
604
+ await writeAtomic(target, content, file.mode);
605
+ result.changed = true;
606
+ }
607
+ result.files.push({ path: file.path, changed: existing === null, sha256: file.sha256 });
608
+ }
609
+ return result;
610
+ }
611
+
612
+ async function resumeInstall(projectRoot = process.cwd()) {
613
+ const root = normalizeProjectRoot(projectRoot);
614
+ const statePath = path.join(root, MANAGED_DIR, "install-state.json");
615
+ const state = await readJson(statePath);
616
+ if (!state || state.schema !== STATE_SCHEMA) return { schema: "agentx-install-status-v1", state: "not_installed", project_root: root, next_steps: ["build_install_plan"] };
617
+ return {
618
+ schema: "agentx-install-status-v1",
619
+ state: state.status || "unknown",
620
+ verification_status: state.verification_status || "pending_domain_verification",
621
+ site_url: state.site_url || "",
622
+ install_id: state.install_id || "",
623
+ project_root: root,
624
+ next_steps: state.framework === "static"
625
+ ? (state.verification_status === "verified"
626
+ ? ["store_SITE_AGENT_KEY_as_GitHub_Actions_secret", "run_customer_owned_sync_workflow"]
627
+ : ["publish_domain_proof", "verify_domain_ownership", "store_SITE_AGENT_KEY_as_GitHub_Actions_secret"])
628
+ : (state.verification_status === "verified" ? ["install_server_package", "start_site_agent_sync"] : ["verify_domain_ownership", "install_server_package", "store_SITE_AGENT_KEY_in_server_secret_store"])
629
+ };
630
+ }
631
+
632
+ async function enterpriseInstallStatus(projectRoot = process.cwd()) {
633
+ const root = normalizeProjectRoot(projectRoot);
634
+ const state = await readJson(path.join(root, MANAGED_DIR, "enterprise-state.json"));
635
+ const policy = await readJson(path.join(root, MANAGED_DIR, "enterprise-policy.json"));
636
+ return {
637
+ schema: "agentx-enterprise-install-status-v1",
638
+ state: state && state.status ? state.status : "not_enrolled",
639
+ site_url: state && state.site_url ? state.site_url : "",
640
+ environment: state && state.environment ? state.environment : "",
641
+ agent_id: state && state.agent_id ? state.agent_id : "",
642
+ credential_id: state && state.credential_id ? state.credential_id : "",
643
+ challenge_id: state && state.challenge_id ? state.challenge_id : "",
644
+ domain_proof_ready: Boolean(state && state.domain_proof && state.domain_proof.schema === "agentx-domain-proof-v1"),
645
+ public_key_thumbprint: state && state.public_key_thumbprint ? state.public_key_thumbprint : "",
646
+ policy_default: policy && policy.default === "deny" ? "deny" : "uninitialized",
647
+ policy_version: policy && Number.isInteger(policy.version) ? policy.version : 0,
648
+ policy_digest: policy && typeof policy.digest === "string" ? policy.digest : "",
649
+ short_lived_credential_storage: "memory_only",
650
+ mutation_authority: "local_policy_only"
651
+ };
652
+ }
653
+
654
+ async function writeEnterpriseEnrollmentState(projectRoot, state) {
655
+ const root = normalizeProjectRoot(projectRoot);
656
+ const proof = state && state.domain_proof && state.domain_proof.schema === "agentx-domain-proof-v1"
657
+ ? {
658
+ schema: "agentx-domain-proof-v1",
659
+ challenge_id: String(state.domain_proof.challenge_id || ""),
660
+ proof: String(state.domain_proof.proof || ""),
661
+ agent_id: String(state.domain_proof.agent_id || ""),
662
+ tenant_id: String(state.domain_proof.tenant_id || ""),
663
+ host: String(state.domain_proof.host || ""),
664
+ environment: String(state.domain_proof.environment || ""),
665
+ public_key_thumbprint: String(state.domain_proof.public_key_thumbprint || ""),
666
+ expires_at: Number(state.domain_proof.expires_at || 0)
667
+ }
668
+ : null;
669
+ const allowed = {
670
+ schema: "agentx-enterprise-enrollment-state-v1",
671
+ status: state.status || "challenge_pending",
672
+ site_url: normalizeSiteUrl(state.site_url),
673
+ environment: state.environment,
674
+ agent_id: state.agent_id || "",
675
+ credential_id: state.credential_id || "",
676
+ challenge_id: state.challenge_id || "",
677
+ public_key_thumbprint: state.public_key_thumbprint || ""
678
+ };
679
+ if (proof) allowed.domain_proof = proof;
680
+ const serialized = JSON.stringify(allowed, null, 2) + "\n";
681
+ if (/access_token|bootstrap_secret|PRIVATE KEY/.test(serialized)) throw installError("ENTERPRISE_SECRET_IN_STATE", "Enterprise secrets cannot be stored in installer state.");
682
+ await writeAtomic(path.join(root, MANAGED_DIR, "enterprise-state.json"), serialized, "private");
683
+ return allowed;
684
+ }
685
+
686
+ async function uninstallInstall(projectRoot = process.cwd(), { approved = false } = {}) {
687
+ if (approved !== true) throw installError("INSTALL_APPROVAL_REQUIRED", "Uninstall requires explicit approved=true.");
688
+ const root = normalizeProjectRoot(projectRoot);
689
+ const statePath = path.join(root, MANAGED_DIR, "install-state.json");
690
+ const state = await readJson(statePath);
691
+ if (!state || state.schema !== STATE_SCHEMA) return { schema: "agentx-uninstall-receipt-v1", removed: false, files: [] };
692
+ const files = Array.isArray(state.managed_paths) ? state.managed_paths : [];
693
+ const removed = [];
694
+ for (const relative of files) {
695
+ const allowedWorkflow = relative === ".github/workflows/agent-id-sync.yml";
696
+ if (typeof relative !== "string" || path.isAbsolute(relative) || (!relative.startsWith(`${MANAGED_DIR}/`) && !allowedWorkflow) || relative.split(/[\\/]+/).includes("..")) throw installError("UNSAFE_INSTALL_PATH", "Refusing to remove a file outside managed Agent ID paths.");
697
+ const target = path.resolve(root, relative);
698
+ await fs.rm(target, { force: true });
699
+ removed.push(relative);
700
+ }
701
+ await fs.rm(path.join(root, MANAGED_DIR), { recursive: false, force: true }).catch(() => {});
702
+ return { schema: "agentx-uninstall-receipt-v1", removed: true, files: removed, install_id: state.install_id || "" };
703
+ }
704
+
705
+ module.exports = {
706
+ INSTALL_SCHEMA,
707
+ STATE_SCHEMA,
708
+ buildInstallPlan,
709
+ preflightInstallPlan,
710
+ detectProject,
711
+ applyInstallPlan,
712
+ resumeInstall,
713
+ uninstallInstall,
714
+ enterpriseInstallStatus,
715
+ prepareEnterpriseSecretDirectory,
716
+ writeEnterpriseBootstrapCredential,
717
+ replaceEnterpriseBootstrapCredential,
718
+ removeEnterpriseBootstrapCredential,
719
+ readEnterpriseBootstrapCredential,
720
+ writeEnterpriseEnrollmentState,
721
+ normalizeSiteUrl
722
+ };