agents-can-communicate 0.3.0 → 0.4.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.
Files changed (157) hide show
  1. package/README.md +112 -91
  2. package/SECURITY.md +8 -5
  3. package/bin/acc-bootstrap.mjs +5 -54
  4. package/bin/acc-claude-channel.mjs +8 -175
  5. package/bin/acc-hook.mjs +4 -133
  6. package/bin/acc-mcp.mjs +4 -59
  7. package/bin/acc-update-worker.mjs +8 -0
  8. package/bin/acc.mjs +5 -39
  9. package/bin/entrypoints/acc-bootstrap.mjs +47 -0
  10. package/bin/entrypoints/acc-claude-channel.mjs +149 -0
  11. package/bin/entrypoints/acc-hook.mjs +58 -0
  12. package/bin/entrypoints/acc-mcp.mjs +70 -0
  13. package/bin/entrypoints/acc.mjs +51 -0
  14. package/bin/entrypoints/claude-channel-stdio.mjs +29 -0
  15. package/bin/entrypoints/hook-output.mjs +73 -0
  16. package/docs/ADAPTER_AUTHORING.md +62 -24
  17. package/docs/ARCHITECTURE.md +79 -20
  18. package/docs/CAPABILITIES.md +52 -30
  19. package/docs/CLI.md +164 -35
  20. package/docs/CONCEPTS.md +97 -93
  21. package/docs/CONFIGURATION.md +66 -45
  22. package/docs/DESIGN_DECISIONS.md +24 -15
  23. package/docs/GETTING_STARTED.md +124 -118
  24. package/docs/GLOSSARY.md +12 -6
  25. package/docs/HOW_IT_WORKS.md +136 -229
  26. package/docs/MCP.md +98 -31
  27. package/docs/PROTOCOL.md +76 -14
  28. package/docs/RELEASING.md +128 -86
  29. package/docs/SECURITY_MODEL.md +52 -15
  30. package/docs/TROUBLESHOOTING.md +88 -18
  31. package/docs/UPGRADING.md +119 -0
  32. package/docs/WHY_ACC.md +19 -16
  33. package/docs/index.md +40 -24
  34. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
  35. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +112 -9
  36. package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +6 -6
  37. package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +2 -1
  38. package/node_modules/@agents-can-communicate/adapter-codex/certification.json +38 -13
  39. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +220 -0
  40. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-current-binding-product-evidence.json +1549 -0
  41. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-current-binding-product.json +27 -0
  42. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-product-evidence.json +1549 -0
  43. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-product.json +27 -0
  44. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-transport-evidence.json +259 -0
  45. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-transport.json +21 -0
  46. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-current-binding-product-evidence.json +1549 -0
  47. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-current-binding-product.json +27 -0
  48. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-product-evidence.json +1549 -0
  49. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-product.json +27 -0
  50. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-transport-evidence.json +259 -0
  51. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-transport.json +21 -0
  52. package/node_modules/@agents-can-communicate/adapter-codex/package.json +14 -2
  53. package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +111 -8
  54. package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +26 -44
  55. package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +70 -17
  56. package/node_modules/@agents-can-communicate/adapter-codex/src/config-block.mjs +231 -0
  57. package/node_modules/@agents-can-communicate/adapter-codex/src/hooks.mjs +1 -1
  58. package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +71 -63
  59. package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +107 -103
  60. package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +96 -0
  61. package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +3 -2
  62. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  63. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +111 -8
  64. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  65. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +2 -2
  66. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  67. package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +111 -8
  68. package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +2 -2
  69. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  70. package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +111 -8
  71. package/node_modules/@agents-can-communicate/adapter-kimi/src/hooks.mjs +1 -1
  72. package/node_modules/@agents-can-communicate/adapter-kimi/src/install.mjs +2 -2
  73. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  74. package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +5 -0
  75. package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +26 -4
  76. package/node_modules/@agents-can-communicate/adapter-sdk/src/decision-text.mjs +19 -0
  77. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +5 -3
  78. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -0
  79. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +9 -2
  80. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +2 -6
  81. package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +29 -9
  82. package/node_modules/@agents-can-communicate/adapter-sdk/src/toml-block.mjs +8 -6
  83. package/node_modules/@agents-can-communicate/cli/package.json +4 -2
  84. package/node_modules/@agents-can-communicate/cli/src/args.mjs +49 -22
  85. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +11 -4
  86. package/node_modules/@agents-can-communicate/cli/src/git-probe.mjs +9 -3
  87. package/node_modules/@agents-can-communicate/cli/src/help.mjs +109 -14
  88. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +66 -18
  89. package/node_modules/@agents-can-communicate/cli/src/main.mjs +35 -12
  90. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/activation.mjs +110 -0
  91. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/command.mjs +63 -0
  92. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/download.mjs +71 -0
  93. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +110 -0
  94. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation.mjs +167 -0
  95. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/install.mjs +73 -0
  96. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +60 -0
  97. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/leases.mjs +60 -0
  98. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/location.mjs +55 -0
  99. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/mutex.mjs +145 -0
  100. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/policy.mjs +4 -0
  101. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/refresh.mjs +40 -0
  102. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/schedule.mjs +21 -0
  103. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/state.mjs +122 -0
  104. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/worker.mjs +65 -0
  105. package/node_modules/@agents-can-communicate/cli/src/session-owner.mjs +33 -108
  106. package/node_modules/@agents-can-communicate/cli/src/update-command.mjs +25 -7
  107. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  108. package/node_modules/@agents-can-communicate/core/src/attention.mjs +3 -0
  109. package/node_modules/@agents-can-communicate/core/src/claims.mjs +13 -8
  110. package/node_modules/@agents-can-communicate/core/src/conversations.mjs +48 -24
  111. package/node_modules/@agents-can-communicate/core/src/decision-changes.mjs +36 -0
  112. package/node_modules/@agents-can-communicate/core/src/decision-state.mjs +42 -0
  113. package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +11 -4
  114. package/node_modules/@agents-can-communicate/core/src/inbox.mjs +48 -17
  115. package/node_modules/@agents-can-communicate/core/src/intents.mjs +35 -15
  116. package/node_modules/@agents-can-communicate/core/src/materialisation.mjs +7 -5
  117. package/node_modules/@agents-can-communicate/core/src/message-pages.mjs +75 -0
  118. package/node_modules/@agents-can-communicate/core/src/next-turn-delivery.mjs +31 -0
  119. package/node_modules/@agents-can-communicate/core/src/session-opening.mjs +128 -0
  120. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +58 -132
  121. package/node_modules/@agents-can-communicate/core/src/sync.mjs +31 -2
  122. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  123. package/node_modules/@agents-can-communicate/delivery-router/src/refresh-binding.mjs +38 -0
  124. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +86 -10
  125. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  126. package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +1 -0
  127. package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +6 -4
  128. package/node_modules/@agents-can-communicate/hook-runner/src/native-retirement.mjs +48 -0
  129. package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +1 -1
  130. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +175 -67
  131. package/node_modules/@agents-can-communicate/hook-runner/src/session-lifecycle.mjs +13 -0
  132. package/node_modules/@agents-can-communicate/installer/package.json +4 -2
  133. package/node_modules/@agents-can-communicate/installer/src/apply.mjs +17 -2
  134. package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +11 -5
  135. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +1 -1
  136. package/node_modules/@agents-can-communicate/installer/src/index.mjs +1 -0
  137. package/node_modules/@agents-can-communicate/installer/src/live-policy.mjs +21 -0
  138. package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +20 -6
  139. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +10 -4
  140. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +3 -3
  141. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  142. package/node_modules/@agents-can-communicate/mcp-server/src/protocol.mjs +85 -0
  143. package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +3 -2
  144. package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +59 -57
  145. package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +47 -33
  146. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  147. package/node_modules/@agents-can-communicate/protocol/src/conversations.mjs +3 -0
  148. package/node_modules/@agents-can-communicate/protocol/src/decision-changes.mjs +15 -0
  149. package/node_modules/@agents-can-communicate/protocol/src/index.mjs +3 -1
  150. package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +14 -5
  151. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  152. package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +5 -1
  153. package/node_modules/@agents-can-communicate/storage-filesystem/src/deadline.mjs +7 -0
  154. package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +2 -0
  155. package/node_modules/@agents-can-communicate/storage-filesystem/src/session-read.mjs +25 -0
  156. package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +28 -25
  157. package/package.json +2 -1
@@ -0,0 +1,167 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm }
3
+ from "node:fs/promises";
4
+ import path from "node:path";
5
+
6
+ const NAME = "agents-can-communicate";
7
+ const STABLE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
8
+
9
+ async function canonicalFuturePath(file) {
10
+ let current = path.resolve(file);
11
+ const trailing = [];
12
+ for (;;) {
13
+ try { return path.join(await realpath(current), ...trailing); }
14
+ catch (error) {
15
+ if (error.code !== "ENOENT") throw error;
16
+ const parent = path.dirname(current);
17
+ if (parent === current) throw error;
18
+ trailing.unshift(path.basename(current));
19
+ current = parent;
20
+ }
21
+ }
22
+ }
23
+
24
+ async function directory(file) {
25
+ await mkdir(file, { recursive: true, mode: 0o700 });
26
+ const stat = await lstat(file);
27
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("managed runtime directory is not a regular directory");
28
+ }
29
+
30
+ function declaredPath(value) {
31
+ if (typeof value !== "string" || value === "" || /[\\*?\[\]{}\0]/.test(value)
32
+ || path.isAbsolute(value) || value.split("/").includes("..")) {
33
+ throw new Error("invalid declared runtime path");
34
+ }
35
+ return value.replace(/\/+$/, "");
36
+ }
37
+
38
+ async function collectFile(source, relative, collected) {
39
+ const stat = await lstat(source);
40
+ if (stat.isSymbolicLink()) throw new Error(`symbolic link inside runtime files: ${relative}`);
41
+ if (stat.isDirectory()) {
42
+ for (const entry of (await readdir(source)).sort()) {
43
+ await collectFile(path.join(source, entry), path.posix.join(relative, entry), collected);
44
+ }
45
+ } else if (stat.isFile()) {
46
+ collected.set(relative, { bytes: await readFile(source), mode: stat.mode & 0o777 });
47
+ } else throw new Error(`unsupported runtime file: ${relative}`);
48
+ }
49
+
50
+ async function checkDeclaredComponents(source, relative) {
51
+ let current = source;
52
+ for (const component of relative.split("/")) {
53
+ current = path.join(current, component);
54
+ if ((await lstat(current)).isSymbolicLink()) {
55
+ throw new Error(`symbolic link inside declared runtime path: ${relative}`);
56
+ }
57
+ }
58
+ }
59
+
60
+ function requireOutside(source, root) {
61
+ const relative = path.relative(source, root);
62
+ if (relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative))) {
63
+ throw new Error("managed runtime must live outside its source package");
64
+ }
65
+ }
66
+
67
+ async function collectPackage(source, prefix, collected, expectedName, version) {
68
+ await checkDeclaredComponents(source, "package.json");
69
+ const manifestFile = path.join(source, "package.json");
70
+ const manifest = JSON.parse(await readFile(manifestFile, "utf8"));
71
+ if (manifest.name !== expectedName || manifest.version !== version
72
+ || !Array.isArray(manifest.files)) throw new Error("runtime package identity or declared files are invalid");
73
+ const declared = new Set(["package.json", ...manifest.files.map(declaredPath)]);
74
+ // npm includes these public package files regardless of the files whitelist.
75
+ for (const file of await readdir(source)) {
76
+ if (/^(readme|licen[cs]e)(\..*)?$/i.test(file)) declared.add(file);
77
+ }
78
+ for (const relative of declared) {
79
+ await checkDeclaredComponents(source, relative);
80
+ await collectFile(path.join(source, relative), path.posix.join(prefix, relative), collected);
81
+ }
82
+ return manifest;
83
+ }
84
+
85
+ function fingerprint(files) {
86
+ const hash = createHash("sha256");
87
+ for (const [name, file] of [...files].sort(([a], [b]) => a.localeCompare(b))) {
88
+ hash.update(JSON.stringify([name, file.mode, file.bytes.length]));
89
+ hash.update(file.bytes);
90
+ }
91
+ return hash.digest("hex");
92
+ }
93
+
94
+ async function existingMatches(target, files) {
95
+ try {
96
+ const actual = new Map();
97
+ await collectFile(target, "", actual);
98
+ if (fingerprint(actual) !== fingerprint(files)) throw new Error("managed runtime generation integrity changed");
99
+ return true;
100
+ } catch (error) {
101
+ if (error.code === "ENOENT") {
102
+ // Only an absent generation is new. A damaged existing tree is not a
103
+ // staging directory we are entitled to overwrite.
104
+ const exists = await lstat(target).then(() => true, e => {
105
+ if (e.code === "ENOENT") return false;
106
+ throw e;
107
+ });
108
+ if (!exists) return false;
109
+ }
110
+ throw error;
111
+ }
112
+ }
113
+
114
+ /** Materialize the package's shipped files; never copy a checkout or workspace symlink. */
115
+ export async function stageOwnGeneration({ packageRoot, managerRoot }) {
116
+ const source = await realpath(packageRoot);
117
+ const root = await canonicalFuturePath(managerRoot);
118
+ requireOutside(source, root);
119
+ await checkDeclaredComponents(source, "package.json");
120
+ const manifest = JSON.parse(await readFile(path.join(source, "package.json"), "utf8"));
121
+ if (manifest.name !== NAME || !STABLE.test(manifest.version)) throw new Error("invalid ACC runtime version");
122
+ const files = new Map();
123
+ await collectPackage(source, "", files, NAME, manifest.version);
124
+ if (!Array.isArray(manifest.bundleDependencies)) throw new Error("runtime must declare its bundled workspaces");
125
+ for (const name of manifest.bundleDependencies) {
126
+ if (!/^@agents-can-communicate\/[a-z0-9-]+$/.test(name)) throw new Error("invalid bundled runtime package");
127
+ const location = await realpath(path.join(source, "node_modules", name));
128
+ requireOutside(location, root);
129
+ await collectPackage(location, `node_modules/${name}`, files, name, manifest.version);
130
+ }
131
+ const digest = fingerprint(files);
132
+ await directory(root);
133
+ const generations = path.join(root, "generations");
134
+ await directory(generations);
135
+ const target = path.join(generations, `${manifest.version}-${digest.slice(0, 24)}`);
136
+ const result = { version: manifest.version, root: target, digest };
137
+ if (await existingMatches(target, files)) return result;
138
+ const staging = await mkdtemp(path.join(generations, ".staging-"));
139
+ try {
140
+ const directories = new Set([staging]);
141
+ for (const [name, file] of files) {
142
+ const destination = path.join(staging, name);
143
+ await mkdir(path.dirname(destination), { recursive: true });
144
+ for (let parent = path.dirname(destination); parent !== staging; parent = path.dirname(parent)) directories.add(parent);
145
+ const handle = await open(destination, "wx", file.mode);
146
+ try {
147
+ await handle.writeFile(file.bytes);
148
+ await handle.chmod(file.mode);
149
+ await handle.sync();
150
+ } finally { await handle.close(); }
151
+ }
152
+ for (const directory of [...directories].sort((a, b) => b.length - a.length)) {
153
+ const handle = await open(directory, "r");
154
+ try { await handle.sync(); } finally { await handle.close(); }
155
+ }
156
+ try {
157
+ await rename(staging, target);
158
+ } catch (error) {
159
+ if (!["EEXIST", "ENOTEMPTY"].includes(error.code) || !await existingMatches(target, files)) throw error;
160
+ }
161
+ const handle = await open(generations, "r");
162
+ try { await handle.sync(); } finally { await handle.close(); }
163
+ return result;
164
+ } finally {
165
+ await rm(staging, { recursive: true, force: true });
166
+ }
167
+ }
@@ -0,0 +1,73 @@
1
+ import { execFile } from "node:child_process";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { validateManagedLocation } from "./location.mjs";
7
+ import { stageOwnGeneration } from "./generation.mjs";
8
+ import { writeLaunchers } from "./launchers.mjs";
9
+ import { canonicalManagerRoot, readControl, writeControl } from "./state.mjs";
10
+ import { scheduleWorker } from "./schedule.mjs";
11
+ import { withManagerLock } from "./mutex.mjs";
12
+
13
+ const exec = promisify(execFile);
14
+ export async function verifyGeneration(runtime, { env = process.env } = {}) {
15
+ const temporary = await mkdtemp(path.join(tmpdir(), "acc-runtime-check-"));
16
+ try {
17
+ const { stdout } = await exec(process.execPath,
18
+ [path.join(runtime.root, "bin", "acc.mjs"), "version", "--json"], {
19
+ env: { ...env, ACC_DATA_HOME: path.join(temporary, "data"), ACC_NO_UPDATE_CHECK: "1" },
20
+ cwd: temporary, timeout: 10_000, maxBuffer: 64 * 1024,
21
+ });
22
+ if (JSON.parse(stdout)?.data?.version !== runtime.version) throw new Error("runtime version check failed");
23
+ } finally { await rm(temporary, { recursive: true, force: true }); }
24
+ }
25
+
26
+ /** Only the actual CLI composition supplies packageRoot; dry runs never enroll. */
27
+ export async function installManaged({ packageRoot, managerRoot, dataHome, home, targets, env, apply, cwd }) {
28
+ const root = await validateManagedLocation({ managerRoot, dataHome, home, cwd, env });
29
+ const candidate = await stageOwnGeneration({ packageRoot, managerRoot: root });
30
+ await verifyGeneration(candidate, { env });
31
+ return withManagerLock(root, async () => {
32
+ const previous = await readControl(root);
33
+ if (previous !== null && previous.home !== home) {
34
+ throw new Error("this ACC data home manages another client home; use a separate ACC_DATA_HOME");
35
+ }
36
+ if (previous !== null && previous.active.root !== candidate.root) {
37
+ throw new Error("install from the active managed runtime; use acc update to switch versions");
38
+ }
39
+ const runtime = { version: candidate.version, root: candidate.root };
40
+ const control = { schemaVersion: 1, active: runtime, pending: runtime, phase: "activating",
41
+ auto: previous?.auto ?? true, pin: previous?.pin ?? null,
42
+ checkedAt: previous?.checkedAt ?? null, home,
43
+ targets: [...new Set([...(previous?.targets ?? []), ...targets])], notice: null };
44
+ await writeControl(root, control);
45
+ const paths = await writeLaunchers(root, runtime.root);
46
+ const result = await apply({ ...paths, preserveVersions: true });
47
+ if (result.failed.length === 0) {
48
+ const ready = await writeControl(root, { ...control, phase: "ready", pending: null });
49
+ await scheduleWorker(root, ready, { env });
50
+ } else {
51
+ await writeControl(root, { ...control, notice: "Integration refresh is incomplete; run acc update to recover." });
52
+ }
53
+ return result;
54
+ });
55
+ }
56
+
57
+ /** Removal updates enrollment under the same fence, so an updater cannot reinstall it. */
58
+ export async function uninstallManaged({ managerRoot, apply }) {
59
+ const root = await canonicalManagerRoot(managerRoot);
60
+ return withManagerLock(root, async () => {
61
+ const control = await readControl(root);
62
+ if (!control) return apply();
63
+ if (control.phase !== "ready") throw new Error("finish the interrupted update with acc update before uninstalling");
64
+ const result = await apply();
65
+ const removed = new Set(result.operations.filter(operation => operation.applied)
66
+ .map(operation => operation.adapterId));
67
+ const targets = control.targets.filter(id => !removed.has(id));
68
+ await writeControl(root, { ...control, targets,
69
+ auto: targets.length > 0 && result.failed.length === 0 && control.auto,
70
+ pending: targets.length > 0 ? control.pending : null, notice: null });
71
+ return result;
72
+ });
73
+ }
@@ -0,0 +1,60 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { lstat, mkdtemp, open, readFile, rename, rm } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { ENTRY_KINDS } from "./entry.mjs";
5
+ import { managedDirectory, syncDirectory } from "./state.mjs";
6
+
7
+ const MODULES = ["entry.mjs", "state.mjs", "mutex.mjs", "leases.mjs", "schedule.mjs", "policy.mjs"];
8
+ async function durableFile(file, bytes, mode = 0o600) {
9
+ const handle = await open(file, "wx", mode);
10
+ try { await handle.writeFile(bytes); await handle.sync(); }
11
+ finally { await handle.close(); }
12
+ }
13
+
14
+ export const stablePaths = root => ({
15
+ cli: path.join(root, "bin", "acc.mjs"), runner: path.join(root, "bin", "acc-hook.mjs"),
16
+ bootstrap: path.join(root, "bin", "acc-bootstrap.mjs"),
17
+ channel: path.join(root, "bin", "acc-claude-channel.mjs"),
18
+ });
19
+
20
+ /** Publish immutable launcher modules, then atomically replace each tiny entry.
21
+ * Call while admission is fenced. Old launchers are kept for already-loaded
22
+ * entry modules, whose dependency imports may still be pending.
23
+ */
24
+ export async function writeLaunchers(root, packageRoot) {
25
+ const modules = await Promise.all(MODULES.map(async name => [name,
26
+ await readFile(new URL(name, import.meta.url))]));
27
+ const hash = createHash("sha256");
28
+ for (const [name, bytes] of modules) hash.update(name).update(bytes);
29
+ const id = hash.digest("hex");
30
+ const parent = path.join(root, "launchers");
31
+ await managedDirectory(parent, { create: true });
32
+ const directory = path.join(parent, id);
33
+ const staging = await mkdtemp(path.join(parent, ".staging-"));
34
+ try {
35
+ for (const [name, bytes] of modules) await durableFile(path.join(staging, name), bytes);
36
+ await syncDirectory(staging);
37
+ try { await rename(staging, directory); }
38
+ catch (error) { if (!["EEXIST", "ENOTEMPTY"].includes(error.code)) throw error; }
39
+ await managedDirectory(directory);
40
+ for (const [name, bytes] of modules) {
41
+ const file = path.join(directory, name);
42
+ if (!(await lstat(file)).isFile() || (await lstat(file)).isSymbolicLink()
43
+ || !(await readFile(file)).equals(bytes)) throw new Error("managed launcher integrity changed");
44
+ }
45
+ await syncDirectory(parent);
46
+ } finally { await rm(staging, { recursive: true, force: true }); }
47
+ const bin = path.join(root, "bin");
48
+ await managedDirectory(bin, { create: true });
49
+ for (const kind of ENTRY_KINDS) {
50
+ const bytes = `#!/usr/bin/env node\nimport { runEntry } from "../launchers/${id}/entry.mjs";\n`
51
+ + `await runEntry(${JSON.stringify({ kind, packageRoot, managerRoot: root, managedRequired: true })});\n`;
52
+ const temporary = path.join(bin, `.${randomUUID()}.tmp`);
53
+ try {
54
+ await durableFile(temporary, bytes, 0o700);
55
+ await rename(temporary, path.join(bin, `${kind}.mjs`));
56
+ } finally { await rm(temporary, { force: true }); }
57
+ }
58
+ await syncDirectory(bin);
59
+ return stablePaths(root);
60
+ }
@@ -0,0 +1,60 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readdir, rm } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { canonicalManagerRoot, managedDirectory, readControl, readManagedJson,
5
+ syncDirectory, validateRuntime, writeManagedJson } from "./state.mjs";
6
+ import { confirmedDead, defaultPidIsAlive, withManagerLock } from "./mutex.mjs";
7
+
8
+ /** Admission and lease publication must finish inside the pointer's mutex. */
9
+ export async function acquireRuntime(root, { pid = process.pid, kind = "cli" } = {}) {
10
+ if (!Number.isSafeInteger(pid) || pid <= 0 || typeof kind !== "string" || !kind) {
11
+ throw new Error("invalid runtime lease identity");
12
+ }
13
+ root = await canonicalManagerRoot(root);
14
+ return withManagerLock(root, async () => {
15
+ const control = await readControl(root);
16
+ if (!control) throw new Error("managed runtime is not initialized");
17
+ if (control.phase !== "ready") throw new Error("managed runtime activation in progress");
18
+ // Ordinary admission also reaps dead processes when updates are off or no
19
+ // new version exists. Corrupt unrelated records remain a management hold,
20
+ // but must not introduce a new failure for ordinary runtime selection.
21
+ try { await listRuntimeHolds(root); } catch { /* Defer uncertain housekeeping. */ }
22
+ const lease = { schemaVersion: 1, token: randomUUID(), pid, kind,
23
+ runtime: control.active, createdAt: new Date().toISOString() };
24
+ const directory = path.join(root, "leases");
25
+ await managedDirectory(directory, { create: true });
26
+ await writeManagedJson(path.join(directory, `${lease.token}.json`), lease);
27
+ return lease;
28
+ });
29
+ }
30
+
31
+ /** Management decisions call this under withManagerLock to exclude admissions.
32
+ * Records are immutable and uniquely named; confirmed-dead cleanup also works
33
+ * outside a transaction. No release-on-return or timestamp expiry exists.
34
+ */
35
+ export async function listRuntimeHolds(root, { pidIsAlive = defaultPidIsAlive } = {}) {
36
+ root = await canonicalManagerRoot(root);
37
+ if (!await managedDirectory(root)) return [];
38
+ const directory = path.join(root, "leases");
39
+ if (!await managedDirectory(directory)) return [];
40
+ const holds = [];
41
+ let removed = false;
42
+ for (const name of (await readdir(directory)).sort()) {
43
+ if (name.startsWith(".record-") && name.endsWith(".tmp")) continue;
44
+ if (!name.endsWith(".json")) throw new Error("invalid runtime lease file");
45
+ const file = path.join(directory, name);
46
+ const lease = await readManagedJson(file);
47
+ if (lease === undefined) continue;
48
+ if (!lease || lease.schemaVersion !== 1 || !Number.isSafeInteger(lease.pid) || lease.pid <= 0
49
+ || typeof lease.token !== "string" || name !== `${lease.token}.json`
50
+ || typeof lease.kind !== "string" || !lease.kind
51
+ || !Number.isFinite(Date.parse(lease.createdAt))) throw new Error("invalid runtime lease");
52
+ await validateRuntime(root, lease.runtime);
53
+ if (await confirmedDead(lease.pid, pidIsAlive)) {
54
+ await rm(file, { force: true });
55
+ removed = true;
56
+ } else holds.push(lease);
57
+ }
58
+ if (removed) await syncDirectory(directory);
59
+ return holds;
60
+ }
@@ -0,0 +1,55 @@
1
+ import { lstat, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { canonicalManagerRoot } from "./state.mjs";
4
+
5
+ const contains = (root, target) => {
6
+ const relative = path.relative(root, target);
7
+ return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
8
+ };
9
+ async function exists(file) {
10
+ try { await lstat(file); return true; }
11
+ catch (error) { if (error.code === "ENOENT") return false; throw error; }
12
+ }
13
+ async function markedRoots(start) {
14
+ const roots = new Set();
15
+ for (let directory = start;; directory = path.dirname(directory)) {
16
+ if (await exists(path.join(directory, ".git"))) roots.add(directory);
17
+ const config = path.join(directory, "acc.workspace.json");
18
+ if (await exists(config)) {
19
+ roots.add(directory);
20
+ // Installation does not consume workspace config. A malformed optional
21
+ // file cannot prevent an external install, but its marker still counts.
22
+ let declared;
23
+ try { declared = JSON.parse(await readFile(config, "utf8")); } catch { /* Marker only. */ }
24
+ for (const root of Array.isArray(declared?.roots) ? declared.roots : []) {
25
+ if (typeof root === "string") roots.add(await canonicalManagerRoot(path.resolve(directory, root)));
26
+ }
27
+ }
28
+ if (path.dirname(directory) === directory) return roots;
29
+ }
30
+ }
31
+
32
+ /** Read-only admission for installation, independent of Git or workspace stores. */
33
+ export async function validateManagedLocation({ managerRoot, dataHome, cwd = process.cwd(), home, env = {} }) {
34
+ const root = await canonicalManagerRoot(managerRoot);
35
+ // The install ledger uses the configured data home, while runtime staging
36
+ // uses the resolved manager. A symlink can put either destination elsewhere.
37
+ dataHome = await canonicalManagerRoot(dataHome ?? path.dirname(path.dirname(managerRoot)));
38
+ const start = await canonicalManagerRoot(env.ACC_WORKSPACE_ROOT || path.resolve(cwd));
39
+ const roots = await markedRoots(dataHome);
40
+ for (const directory of [root, start]) {
41
+ for (const marked of await markedRoots(directory)) roots.add(marked);
42
+ }
43
+ const homes = await Promise.all([home, env.HOME].filter(value => typeof value === "string")
44
+ .map(value => canonicalManagerRoot(path.resolve(value))));
45
+ // HOME and filesystem root are installation launch locations, unless the
46
+ // user explicitly declares them as workspaces or a filesystem marker does.
47
+ if (env.ACC_WORKSPACE_ROOT || !homes.includes(start) && path.dirname(start) !== start) roots.add(start);
48
+ if ([...roots].some(workspace => contains(workspace, dataHome) || contains(workspace, root))) {
49
+ throw new Error("ACC data home must be outside every repository or workspace");
50
+ }
51
+ if (root !== path.join(dataHome, "acc", "runtime")) {
52
+ throw new Error("ACC cannot relocate only its runtime; relocate the entire data home with ACC_DATA_HOME");
53
+ }
54
+ return root;
55
+ }
@@ -0,0 +1,145 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { lstat, mkdir, readdir, rename, rm } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { performance } from "node:perf_hooks";
5
+ import { canonicalManagerRoot, managedDirectory, readManagedJson, syncDirectory, writeManagedJson } from "./state.mjs";
6
+
7
+ export function defaultPidIsAlive(pid) {
8
+ try { process.kill(pid, 0); return true; }
9
+ catch (error) { return error.code !== "ESRCH"; }
10
+ }
11
+
12
+ export async function confirmedDead(pid, pidIsAlive = defaultPidIsAlive) {
13
+ try { return await pidIsAlive(pid) === false; }
14
+ catch (error) { return error.code === "ESRCH"; }
15
+ }
16
+
17
+ const identity = owner => createHash("sha256")
18
+ .update(JSON.stringify([owner.pid, owner.token, owner.acquiredAt])).digest("hex");
19
+
20
+ async function lockDirectoryIdentity(directory) {
21
+ try {
22
+ const info = await lstat(directory);
23
+ if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("manager lock is not a regular directory");
24
+ return info;
25
+ } catch (error) {
26
+ if (error.code === "ENOENT") return null;
27
+ throw error;
28
+ }
29
+ }
30
+
31
+ function validOwner(owner) {
32
+ return owner && Number.isSafeInteger(owner.pid) && owner.pid > 0
33
+ && typeof owner.token === "string" && owner.token
34
+ && typeof owner.acquiredAt === "string" && Number.isFinite(Date.parse(owner.acquiredAt));
35
+ }
36
+
37
+ async function readOwner(directory) {
38
+ const before = await lockDirectoryIdentity(directory);
39
+ if (!before) return null;
40
+ const owner = await readManagedJson(path.join(directory, "owner.json"));
41
+ const after = await lockDirectoryIdentity(directory);
42
+ // Publication and retirement replace the entire directory. Missing owner
43
+ // bytes only prove corruption when both observations refer to the same lock.
44
+ if (!after || before.dev !== after.dev || before.ino !== after.ino) return null;
45
+ if (!validOwner(owner)) {
46
+ throw new Error("invalid manager lock owner; refusing to reclaim");
47
+ }
48
+ return owner;
49
+ }
50
+
51
+ async function retire(directory, target) {
52
+ try { await rename(directory, target); return true; }
53
+ catch (error) {
54
+ if (["ENOENT", "EEXIST", "ENOTEMPTY"].includes(error.code)) return false;
55
+ throw error;
56
+ }
57
+ }
58
+
59
+ /** Caller holds and has synced manager.lock. Every old-identity observer keeps
60
+ * its candidate through all awaited reads, probes and retirements. Validate the
61
+ * entire snapshot before deleting anything: a single unknown/live observer may
62
+ * still need any historical fence. Candidates published after admission can only
63
+ * observe this holder, whose future release tombstone is absent from the snapshot.
64
+ */
65
+ async function compactQuiescentLocks(root, pidIsAlive) {
66
+ try {
67
+ const names = await readdir(root);
68
+ const dead = [];
69
+ for (const name of names.filter(name => name.startsWith("manager.candidate-"))) {
70
+ const match = /^manager\.candidate-([1-9]\d*)-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.lock$/.exec(name);
71
+ if (!match || !Number.isSafeInteger(Number(match[1]))) return;
72
+ const candidate = path.join(root, name);
73
+ if (!await lockDirectoryIdentity(candidate)) return;
74
+ const owner = await readManagedJson(path.join(candidate, "owner.json"));
75
+ if (owner !== undefined && (!validOwner(owner)
76
+ || owner.pid !== Number(match[1]) || owner.token !== match[2])) return;
77
+ if (!await confirmedDead(Number(match[1]), pidIsAlive)) return;
78
+ dead.push(candidate);
79
+ }
80
+ for (const name of names.filter(name => /^manager\.reclaimed-[0-9a-f]{64}\.lock$/.test(name))) {
81
+ const tombstone = path.join(root, name);
82
+ const owner = await readOwner(tombstone);
83
+ if (owner && name === `manager.reclaimed-${identity(owner)}.lock`) {
84
+ await rm(tombstone, { recursive: true, force: true });
85
+ }
86
+ }
87
+ for (const candidate of dead) await rm(candidate, { recursive: true, force: true });
88
+ await syncDirectory(root);
89
+ } catch {
90
+ // Maintenance is best effort. Unreadable/malformed preparation keeps the
91
+ // fences; housekeeping must never turn successful admission into failure.
92
+ }
93
+ }
94
+
95
+ /** Process death is the only expiry. Nonquiescent tombstones fence observers. */
96
+ export async function withManagerLock(root, operation, { timeoutMs = 1000, pidIsAlive = defaultPidIsAlive } = {}) {
97
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0) throw new Error("invalid manager lock timeout");
98
+ root = await canonicalManagerRoot(root);
99
+ await managedDirectory(root, { create: true });
100
+ const directory = path.join(root, "manager.lock");
101
+ const owner = { pid: process.pid, token: randomUUID(), acquiredAt: new Date().toISOString() };
102
+ const candidate = path.join(root, `manager.candidate-${owner.pid}-${owner.token}.lock`);
103
+ await mkdir(candidate, { mode: 0o700 });
104
+ let owned = false;
105
+ try {
106
+ await writeManagedJson(path.join(candidate, "owner.json"), owner);
107
+ await syncDirectory(candidate);
108
+ // Budget contention after durable preparation; slow fsync is not another worker.
109
+ const deadline = performance.now() + timeoutMs;
110
+ while (performance.now() < deadline) {
111
+ const current = await readOwner(directory);
112
+ if (current) {
113
+ if (await confirmedDead(current.pid, pidIsAlive)) {
114
+ // Every observer of this dead owner uses the same nonempty target;
115
+ // retain it until quiescence so a late observer cannot retire a successor.
116
+ const tombstone = path.join(root, `manager.reclaimed-${identity(current)}.lock`);
117
+ if (await retire(directory, tombstone)) await syncDirectory(root);
118
+ }
119
+ } else {
120
+ try {
121
+ await rename(candidate, directory);
122
+ owned = true;
123
+ await syncDirectory(root);
124
+ await compactQuiescentLocks(root, pidIsAlive);
125
+ return await operation();
126
+ } catch (error) {
127
+ if (owned || !["EEXIST", "ENOTEMPTY"].includes(error.code)) throw error;
128
+ }
129
+ }
130
+ const remaining = deadline - performance.now();
131
+ if (remaining > 0) await new Promise(resolve => setTimeout(resolve, Math.min(10, remaining)));
132
+ }
133
+ throw new Error("manager lock held; acquisition timeout");
134
+ } finally {
135
+ if (owned) {
136
+ const current = await readOwner(directory);
137
+ if (current?.token === owner.token) {
138
+ // A release tombstone also fences observers that inspected this owner
139
+ // before release and resume after a successor has been published.
140
+ const released = path.join(root, `manager.reclaimed-${identity(owner)}.lock`);
141
+ if (await retire(directory, released)) await syncDirectory(root);
142
+ }
143
+ } else await rm(candidate, { recursive: true, force: true });
144
+ }
145
+ }
@@ -0,0 +1,4 @@
1
+ export const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
2
+ export const networkDisabled = env => !!env?.ACC_NO_UPDATE_CHECK && env.ACC_NO_UPDATE_CHECK !== "0";
3
+ export const checkDue = control => !Number.isFinite(Date.parse(control.checkedAt))
4
+ || Date.now() - Date.parse(control.checkedAt) >= CHECK_INTERVAL_MS;
@@ -0,0 +1,40 @@
1
+ import path from "node:path";
2
+ import { applyPlan, detectInstallation, livePolicyOf, loadOwnership, planInstallation, shellOf }
3
+ from "@agents-can-communicate/installer";
4
+ import { ALL_ADAPTERS, clientContext, probeTimeout } from "../install-command.mjs";
5
+ import { stablePaths, writeLaunchers } from "./launchers.mjs";
6
+
7
+ /** Loaded from the candidate, so its own adapters supply its integration bytes. */
8
+ export async function prepareRefresh({ control, root, env = process.env }) {
9
+ const dataHome = path.dirname(path.dirname(root));
10
+ const wanted = new Set(control.targets);
11
+ const adapters = ALL_ADAPTERS().filter(adapter => wanted.has(adapter.id));
12
+ if (adapters.length !== wanted.size) throw new Error("candidate cannot refresh every installed integration");
13
+ const context = { ...clientContext(control.home, path.join(dataHome, "acc"),
14
+ { env, shell: shellOf(env), dataHome }), ...stablePaths(root), preserveVersions: true };
15
+ const recorded = (await loadOwnership({ dataHome })).installs;
16
+ const detected = await detectInstallation({ adapters, context, probeTimeoutMs: probeTimeout(env) });
17
+ const deliveryByAdapter = Object.fromEntries(adapters.map(adapter => [adapter.id,
18
+ livePolicyOf(recorded.find(record => record.adapterId === adapter.id))]));
19
+ // An unavailable probe cannot silently revoke an existing native opt-in.
20
+ for (const detection of detected) {
21
+ if (deliveryByAdapter[detection.adapterId] !== "off" && detection.nativeDelivery?.state !== "eligible") {
22
+ throw new Error("native integration could not be verified; update remains pending");
23
+ }
24
+ }
25
+ const plan = planInstallation({ adapters, detected, context, recorded,
26
+ accVersion: control.pending.version, requested: control.targets, deliveryByAdapter });
27
+ if (plan.skipped.length || plan.operations.length !== wanted.size) {
28
+ throw new Error("candidate did not plan every installed integration");
29
+ }
30
+ return async () => {
31
+ await writeLaunchers(root, control.pending.root);
32
+ const result = await applyPlan({ plan, adapters, context, dataHome, accVersion: control.pending.version,
33
+ activation: { bootstrap: context.bootstrap } });
34
+ return { ...result, failed: result.failed.map(failure => ({ ...failure,
35
+ paths: plan.operations.find(operation => operation.adapterId === failure.adapterId)
36
+ ?.artifacts?.map(artifact => artifact.path).filter(file => typeof file === "string") ?? [],
37
+ })) };
38
+
39
+ };
40
+ }
@@ -0,0 +1,21 @@
1
+ import { spawn } from "node:child_process";
2
+ import path from "node:path";
3
+ import { confirmedDead } from "./mutex.mjs";
4
+ import { readManagedJson } from "./state.mjs";
5
+ import { checkDue, networkDisabled } from "./policy.mjs";
6
+
7
+ /** No network waits here: this only starts an independent background process. */
8
+ export async function scheduleWorker(root, control, { env = process.env } = {}) {
9
+ if (!control?.auto || networkDisabled(env) || !control.pending && !checkDue(control)) return false;
10
+ try {
11
+ const owner = await readManagedJson(path.join(root, "worker", "manager.lock", "owner.json"));
12
+ if (Number.isSafeInteger(owner?.pid) && !await confirmedDead(owner.pid)) return false;
13
+ const child = spawn(process.execPath,
14
+ [path.join(control.active.root, "bin", "acc-update-worker.mjs"), root], {
15
+ env, detached: true, stdio: "ignore", cwd: root,
16
+ });
17
+ child.on("error", () => {});
18
+ child.unref();
19
+ return true;
20
+ } catch { return false; }
21
+ }