agents-can-communicate 0.8.1 → 0.8.3
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/docs/ADAPTER_AUTHORING.md +14 -0
- package/docs/CAPABILITIES.md +1 -1
- package/docs/CLI.md +16 -0
- package/docs/CONFIGURATION.md +73 -8
- package/docs/GETTING_STARTED.md +6 -3
- package/docs/HOW_IT_WORKS.md +3 -1
- package/docs/SECURITY_MODEL.md +12 -1
- package/docs/TROUBLESHOOTING.md +32 -5
- package/docs/UPGRADING.md +40 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/skills/acc/SKILL.md +7 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +3 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/allow-rule.mjs +204 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/install.mjs +45 -10
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +9 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-endpoint.mjs +3 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-observed-directories.mjs +62 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-socket-directories.mjs +41 -0
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +3 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +5 -4
- package/node_modules/@agents-can-communicate/adapter-codex/src/live-permissions.mjs +64 -12
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +7 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +16 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -1
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +9 -4
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +11 -4
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +44 -6
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/download.mjs +3 -1
- package/node_modules/@agents-can-communicate/cli/src/native-session-diagnostics.mjs +20 -10
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/status.mjs +12 -0
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +9 -0
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +10 -3
- package/node_modules/@agents-can-communicate/installer/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/installer/src/receiver-sockets.mjs +19 -0
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +30 -4
- package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-directory.mjs +56 -25
- package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +6 -2
- package/package.json +1 -1
|
@@ -5,11 +5,13 @@ import path from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
|
|
8
|
-
import { bakeSkillCommand, blankJson, defaultAntigravityRelay,
|
|
9
|
-
stampPluginVersion, writeCliShim, writeForeignJson, writeHookShim }
|
|
8
|
+
import { bakeSkillCommand, blankJson, defaultAntigravityRelay, isShellWord, ownVersion,
|
|
9
|
+
removeIfEmpty, stampPluginVersion, writeCliShim, writeForeignJson, writeHookShim }
|
|
10
10
|
from "@agents-can-communicate/adapter-sdk";
|
|
11
11
|
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
12
12
|
|
|
13
|
+
import { agySettingsPath, cliWrapperPath, ensureAllowRule, inspectAllowRule,
|
|
14
|
+
withdrawAllowRule } from "./allow-rule.mjs";
|
|
13
15
|
import { relayShimPath, runningRelays, stopRelays } from "./relays.mjs";
|
|
14
16
|
|
|
15
17
|
const run = promisify(execFile);
|
|
@@ -349,7 +351,9 @@ async function installSkillPlugin(context) {
|
|
|
349
351
|
await cp(bundle, plugin, { recursive: true });
|
|
350
352
|
await stampPluginVersion({ file: path.join(plugin, "plugin.json"),
|
|
351
353
|
version: await ownVersion(import.meta.url), io: { readFile, writeFile } });
|
|
352
|
-
|
|
354
|
+
// Bare when the path allows it: 1.2.12 matched no allow rule against a
|
|
355
|
+
// command whose first word was quoted (issue #214).
|
|
356
|
+
await bakeSkillCommand({ root: plugin, cliShim, bareWhenSafe: true });
|
|
353
357
|
await agyFor(context)(["plugin", "install", plugin], { home, env: context.env });
|
|
354
358
|
} finally {
|
|
355
359
|
await rm(stage, { recursive: true, force: true });
|
|
@@ -399,10 +403,13 @@ export async function installAntigravity(context) {
|
|
|
399
403
|
await writeFile(createdMarker(context, file), `${file}\n`);
|
|
400
404
|
}
|
|
401
405
|
await installSkillPlugin(context);
|
|
402
|
-
|
|
406
|
+
// After the skill, whose wrapper the rule names. Always on (#214, 2026-09-27).
|
|
407
|
+
const allow = await ensureAllowRule(context);
|
|
408
|
+
const changes = [shimDir(home), file, pluginInstallPath(home), vendorManifestPath(home),
|
|
409
|
+
...allow.changes];
|
|
403
410
|
|
|
404
|
-
const diagnostics = [];
|
|
405
|
-
const needsAction = [];
|
|
411
|
+
const diagnostics = [...allow.diagnostics];
|
|
412
|
+
const needsAction = [...allow.needsAction];
|
|
406
413
|
if (locationOf(context) === "workspace") {
|
|
407
414
|
diagnostics.push("a workspace registration loads only while this project is an open "
|
|
408
415
|
+ `Antigravity workspace; in print mode pass --add-dir ${context.antigravityWorkspace}`);
|
|
@@ -524,8 +531,14 @@ export async function uninstallAntigravity(context) {
|
|
|
524
531
|
await rm(manifestMarker, { force: true });
|
|
525
532
|
}
|
|
526
533
|
|
|
534
|
+
// The allow rule ACC recorded adding, and nothing else in the operator's file.
|
|
535
|
+
const allow = await withdrawAllowRule(context);
|
|
536
|
+
changes.push(...allow.changes);
|
|
537
|
+
diagnostics.push(...allow.diagnostics);
|
|
538
|
+
|
|
527
539
|
if (!keep.includes(shimDir(home))) await rm(shimDir(home), { recursive: true, force: true });
|
|
528
|
-
return { ok: true, changes, diagnostics
|
|
540
|
+
return { ok: true, changes, diagnostics,
|
|
541
|
+
...(allow.needsAction.length > 0 ? { needsAction: allow.needsAction } : {}) };
|
|
529
542
|
}
|
|
530
543
|
|
|
531
544
|
/**
|
|
@@ -560,6 +573,12 @@ export async function detectAntigravity(context) {
|
|
|
560
573
|
const needsAction = [];
|
|
561
574
|
const blocked = locationChoice(context);
|
|
562
575
|
const imported = await exists(importedPluginPath(context.home));
|
|
576
|
+
// Read from files only, so it is known for a client ACC did not run. It is
|
|
577
|
+
// the client's own approval control over what a woken session can answer.
|
|
578
|
+
const commandApproval = await inspectAllowRule(context);
|
|
579
|
+
const approval = { commandApproval,
|
|
580
|
+
inboundDelivery: { state: commandApproval.state, diagnostic: commandApproval.diagnostic } };
|
|
581
|
+
diagnostics.push(commandApproval.diagnostic);
|
|
563
582
|
// The installer runs every adapter's detect and passes a null version for a
|
|
564
583
|
// client it did not find. Asking agy then would spawn a binary the installer
|
|
565
584
|
// could not run a moment ago, or - where it does exist but did not answer -
|
|
@@ -568,7 +587,8 @@ export async function detectAntigravity(context) {
|
|
|
568
587
|
diagnostics.push("acc hooks not registered: Antigravity CLI was not found, so it was "
|
|
569
588
|
+ "not asked what it has loaded");
|
|
570
589
|
if (blocked !== null) diagnostics.push(blocked.reason);
|
|
571
|
-
return { ok: true, changes: [], diagnostics, ...
|
|
590
|
+
return { ok: true, changes: [], diagnostics, ...approval,
|
|
591
|
+
...(blocked === null ? {} : { blocked }) };
|
|
572
592
|
}
|
|
573
593
|
let readback = null;
|
|
574
594
|
try {
|
|
@@ -622,11 +642,22 @@ export async function detectAntigravity(context) {
|
|
|
622
642
|
+ "first run and loads its skill, but none of its hook names, so no ACC hook runs here "
|
|
623
643
|
+ "(issue #176)");
|
|
624
644
|
}
|
|
645
|
+
// Every install writes the rule, so where ACC's wrapper is on disk a missing
|
|
646
|
+
// or unreadable rule is an install to finish, whatever the delivery policy.
|
|
647
|
+
if (await exists(cliWrapperPath(context.home))) {
|
|
648
|
+
if (commandApproval.state === "prompts") {
|
|
649
|
+
needsAction.push(`acc install --adapter antigravity # ${commandApproval.rule} is missing `
|
|
650
|
+
+ `from permissions.allow in ${commandApproval.file}, so each ACC command waits for `
|
|
651
|
+
+ "approval");
|
|
652
|
+
} else if (commandApproval.state === "unreadable") {
|
|
653
|
+
needsAction.push(commandApproval.diagnostic);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
625
656
|
if (blocked !== null) {
|
|
626
657
|
diagnostics.push(blocked.reason);
|
|
627
658
|
needsAction.push(blocked.reason);
|
|
628
659
|
}
|
|
629
|
-
return { ok: true, changes: [], diagnostics,
|
|
660
|
+
return { ok: true, changes: [], diagnostics, ...approval,
|
|
630
661
|
...(blocked === null ? {} : { blocked }),
|
|
631
662
|
...(needsAction.length > 0 ? { needsAction } : {}) };
|
|
632
663
|
}
|
|
@@ -678,8 +709,12 @@ export function planAntigravityInstall(context) {
|
|
|
678
709
|
// installer leaves to the adapter - so the installer never deletes the copy
|
|
679
710
|
// ahead of `agy plugin uninstall` and strands its manifest entry. That exact
|
|
680
711
|
// ordering, with the shim directory, once left `{}` behind in the user's home.
|
|
712
|
+
// The operator's own settings, edited for ACC's allow rule wherever one can
|
|
713
|
+
// match: a wrapper path that needs quotes gets none.
|
|
681
714
|
const plugin = [{ path: pluginInstallPath(context.home), kind: "merge" },
|
|
682
|
-
{ path: vendorManifestPath(context.home), kind: "merge" }
|
|
715
|
+
{ path: vendorManifestPath(context.home), kind: "merge" },
|
|
716
|
+
...(isShellWord(cliWrapperPath(context.home))
|
|
717
|
+
? [{ path: agySettingsPath(context.home), kind: "merge" }] : [])];
|
|
683
718
|
if (locationChoice(context) !== null) {
|
|
684
719
|
return [shim, ...plugin,
|
|
685
720
|
{ path: globalHooksPath(context.home), kind: "merge" },
|
|
@@ -5,6 +5,8 @@ import certification from "../certification.json" with { type: "json" };
|
|
|
5
5
|
import { denyOutcome, injectOutcome, injectStartOwnerOutcome, normalizeClaudeHook } from "./hooks.mjs";
|
|
6
6
|
import { MIN_VERSION, PROTOCOL_CONTRACT, bindNativeSession, offerMessage, planNativeActivation,
|
|
7
7
|
probeNativeDelivery, refreshNativeSession, retireNativeSession } from "./inbox-delivery.mjs";
|
|
8
|
+
import { observedInboxDirectories } from "./inbox-observed-directories.mjs";
|
|
9
|
+
import { inboxSocketDirectories } from "./inbox-socket-directories.mjs";
|
|
8
10
|
import { planClaudeInstall, detectClaude, installClaudePlugin, uninstallClaudePlugin } from "./install.mjs";
|
|
9
11
|
|
|
10
12
|
export const CLAUDE_CODE_VERSION = "2.1.233";
|
|
@@ -92,6 +94,13 @@ export function createClaudeCodeAdapter() {
|
|
|
92
94
|
refreshNativeSession,
|
|
93
95
|
retireNativeSession,
|
|
94
96
|
offerMessage,
|
|
97
|
+
// Where a sender connects to wake a session; a sandboxed sender is allowed these:
|
|
98
|
+
// the directories this environment implies, and those of every inbox bound here.
|
|
99
|
+
inboundSockets: context => [
|
|
100
|
+
...inboxSocketDirectories({ env: context?.env, platform: context?.platform, uid: context?.uid }),
|
|
101
|
+
// Ownership is this process's own uid, never the context's naming override.
|
|
102
|
+
...observedInboxDirectories({ stateRoot: context?.stateRoot }),
|
|
103
|
+
],
|
|
95
104
|
|
|
96
105
|
denyOutcome,
|
|
97
106
|
injectOutcome,
|
|
@@ -21,6 +21,8 @@ const own = info => typeof process.getuid !== "function" || info.uid === process
|
|
|
21
21
|
const invalid = () => new Error("invalid Claude inbox endpoint registration");
|
|
22
22
|
|
|
23
23
|
export const newEndpointId = () => `claude_inbox_${randomBytes(16).toString("hex")}`;
|
|
24
|
+
// Where, under a workspace runtime directory, the records live.
|
|
25
|
+
export const ENDPOINTS_DIRECTORY = "claude-inbox-endpoints";
|
|
24
26
|
|
|
25
27
|
function valid(record) {
|
|
26
28
|
return record !== null && typeof record === "object" && !Array.isArray(record)
|
|
@@ -41,7 +43,7 @@ async function directory(runtimeDir, create = false) {
|
|
|
41
43
|
if (!absolute(runtimeDir)) throw invalid();
|
|
42
44
|
if (create) await mkdir(runtimeDir, { recursive: true, mode: 0o700 });
|
|
43
45
|
const root = await realpath(runtimeDir);
|
|
44
|
-
const dir = path.join(root,
|
|
46
|
+
const dir = path.join(root, ENDPOINTS_DIRECTORY);
|
|
45
47
|
if (create) await mkdir(dir, { mode: 0o700 }).catch(error => {
|
|
46
48
|
if (error.code !== "EEXIST") throw error;
|
|
47
49
|
});
|
package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-observed-directories.mjs
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, openSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { ENDPOINTS_DIRECTORY } from "./inbox-endpoint.mjs";
|
|
5
|
+
|
|
6
|
+
// The inbox directories of Claude Code sessions ACC has bound on this machine,
|
|
7
|
+
// read from the adapter's own endpoint records in every workspace under ACC's
|
|
8
|
+
// state root (review of #217). The environment-derived directories cover
|
|
9
|
+
// sessions started from the installer's environment; a session started with
|
|
10
|
+
// its own absolute CLAUDE_CODE_TMPDIR or XDG_RUNTIME_DIR binds elsewhere, and
|
|
11
|
+
// the hook that bound it wrote down where.
|
|
12
|
+
//
|
|
13
|
+
// Synchronous: install, doctor and the managed refresh compose the grant list
|
|
14
|
+
// while building one context, once per command. Every workspace and every
|
|
15
|
+
// matching record is read - a count cap dropped whatever sorted past it, and
|
|
16
|
+
// doctor then called the profile configured (review of #217) - and each read
|
|
17
|
+
// is bounded to one small record. Nothing here throws; a record that cannot be
|
|
18
|
+
// read or cannot be a Claude Code inbox adds nothing. A grant widens the Codex sandbox, so a directory is taken only when
|
|
19
|
+
// it is named as Claude Code names its inbox directories (2.1.283:
|
|
20
|
+
// `cc-socks` or `cc-socks-<uid>`), whatever a file on disk says.
|
|
21
|
+
|
|
22
|
+
const RECORD = /^claude_inbox_[a-f0-9]{32}\.json$/;
|
|
23
|
+
const INBOX_DIRECTORY = /^cc-socks(?:-\d+)?$/;
|
|
24
|
+
const MAX_BYTES = 8_192;
|
|
25
|
+
|
|
26
|
+
const list = directory => {
|
|
27
|
+
try {
|
|
28
|
+
return readdirSync(directory).sort();
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function socketPathIn(file, uid) {
|
|
35
|
+
let fd;
|
|
36
|
+
try {
|
|
37
|
+
fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
38
|
+
const info = fstatSync(fd);
|
|
39
|
+
if (!info.isFile() || (uid !== null && info.uid !== uid) || info.size > MAX_BYTES) return null;
|
|
40
|
+
return JSON.parse(readFileSync(fd, "utf8"))?.socketPath ?? null;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
} finally {
|
|
44
|
+
if (fd !== undefined) try { closeSync(fd); } catch { /* already closed */ }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function observedInboxDirectories({ stateRoot, uid = process.getuid?.() ?? null } = {}) {
|
|
49
|
+
if (typeof stateRoot !== "string" || !path.isAbsolute(stateRoot)) return [];
|
|
50
|
+
const workspaces = path.join(stateRoot, "workspaces");
|
|
51
|
+
const found = new Set();
|
|
52
|
+
for (const workspace of list(workspaces)) {
|
|
53
|
+
const records = path.join(workspaces, workspace, ENDPOINTS_DIRECTORY);
|
|
54
|
+
for (const name of list(records).filter(item => RECORD.test(item))) {
|
|
55
|
+
const socketPath = socketPathIn(path.join(records, name), uid);
|
|
56
|
+
if (typeof socketPath !== "string" || !path.isAbsolute(socketPath) || socketPath.includes("\0")) continue;
|
|
57
|
+
const directory = path.dirname(path.normalize(socketPath));
|
|
58
|
+
if (INBOX_DIRECTORY.test(path.basename(directory))) found.add(directory);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return [...found].sort();
|
|
62
|
+
}
|
package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-socket-directories.mjs
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
// Where Claude Code binds a session's inbox socket, read from the 2.1.283
|
|
4
|
+
// binary (2026-09-27):
|
|
5
|
+
//
|
|
6
|
+
// `${XDG_RUNTIME_DIR || CLAUDE_CODE_TMPDIR || "/tmp"}/cc-socks/<pid>.sock`
|
|
7
|
+
//
|
|
8
|
+
// and, when that path is longer than 103 bytes, `/tmp/cc-socks-<uid>/<pid>.sock`
|
|
9
|
+
// (`$PREFIX/tmp` when TERMUX_VERSION is set). Claude Code's own list of default
|
|
10
|
+
// socket directories adds `/run/user/<uid>/cc-socks` on Linux. Observed on
|
|
11
|
+
// macOS: `/tmp/cc-socks/<pid>.sock`.
|
|
12
|
+
//
|
|
13
|
+
// A sender in a sandbox - a Codex shell running `acc reply` - has to be allowed
|
|
14
|
+
// to connect here, or every live offer to a Claude Code session fails with
|
|
15
|
+
// EPERM and waits for the recipient's next turn. The grant is a directory; the
|
|
16
|
+
// adapter still checks Claude Code's session registry before every write, so a
|
|
17
|
+
// wider grant cannot wake a reused pid.
|
|
18
|
+
//
|
|
19
|
+
// A relative override is resolved against the Claude Code process's own working
|
|
20
|
+
// directory, which no installer can know, so it is left out. Native Windows
|
|
21
|
+
// serves the inbox on a named pipe, which no Unix socket grant covers.
|
|
22
|
+
|
|
23
|
+
const hostOf = platform => String(platform ?? process.platform).split("-")[0];
|
|
24
|
+
const set = value => typeof value === "string" && value !== "";
|
|
25
|
+
|
|
26
|
+
export function inboxSocketDirectories({ env = {}, platform, uid } = {}) {
|
|
27
|
+
const host = hostOf(platform);
|
|
28
|
+
if (host === "win32") return [];
|
|
29
|
+
const user = Number.isInteger(uid) ? uid : (process.getuid?.() ?? 0);
|
|
30
|
+
// Claude Code's `||`: an empty value falls through to the next one.
|
|
31
|
+
const root = [env.XDG_RUNTIME_DIR, env.CLAUDE_CODE_TMPDIR].find(set) ?? "/tmp";
|
|
32
|
+
const fallbackRoot = set(env.TERMUX_VERSION) && set(env.PREFIX)
|
|
33
|
+
? path.join(env.PREFIX, "tmp") : "/tmp";
|
|
34
|
+
const directories = [
|
|
35
|
+
path.join(root, "cc-socks"),
|
|
36
|
+
"/tmp/cc-socks",
|
|
37
|
+
...(host === "linux" ? [`/run/user/${user}/cc-socks`] : []),
|
|
38
|
+
path.join(fallbackRoot, `cc-socks-${user}`),
|
|
39
|
+
];
|
|
40
|
+
return [...new Set(directories.filter(directory => path.isAbsolute(directory)))];
|
|
41
|
+
}
|
|
@@ -11,6 +11,7 @@ import { allowOutcome, denyOutcome, injectOutcome, normalizeCodexHook }
|
|
|
11
11
|
from "./hooks.mjs";
|
|
12
12
|
import { planCodexInstall, detectCodex, installCodexPlugin, preflightCodexUninstall,
|
|
13
13
|
uninstallCodexPlugin } from "./install.mjs";
|
|
14
|
+
import { controlSocket } from "./live-permissions.mjs";
|
|
14
15
|
// Public native wiring is enabled only with installed-product capture evidence.
|
|
15
16
|
|
|
16
17
|
// Re-exported so existing importers of CODEX_QUEUE_MINIMUM keep resolving; the
|
|
@@ -67,6 +68,8 @@ export function createCodexAdapter() {
|
|
|
67
68
|
},
|
|
68
69
|
probeNativeDelivery, planNativeActivation, bindNativeSession,
|
|
69
70
|
refreshNativeSession, retireNativeSession, offerMessage,
|
|
71
|
+
// A Codex sender reaches another Codex session through its control socket.
|
|
72
|
+
inboundSockets: context => [controlSocket(context)],
|
|
70
73
|
...createCodexMaintenance(),
|
|
71
74
|
...createCodexServiceSetup(),
|
|
72
75
|
startSession: async () => ({ ok: true, changes: [], diagnostics: [] }),
|
|
@@ -161,7 +161,7 @@ const sandboxReview = (config, file, stateRoot) =>
|
|
|
161
161
|
|
|
162
162
|
export async function installCodexPlugin({ home, agentsHome = home,
|
|
163
163
|
codexHome = path.join(home, ".codex"), dataHome, stateRoot, runner, node, cli, keepPreviousVersion = null,
|
|
164
|
-
requestedLivePolicy, livePolicy, clientVersion, platform }) {
|
|
164
|
+
requestedLivePolicy, livePolicy, clientVersion, platform, receiverSockets }) {
|
|
165
165
|
// Read before writing, so a manifest that will not parse is found before a
|
|
166
166
|
// plugin tree is laid down that nothing will then be able to remove.
|
|
167
167
|
const existing = await readJson(marketplacePath(agentsHome), { name: MARKETPLACE,
|
|
@@ -182,7 +182,7 @@ export async function installCodexPlugin({ home, agentsHome = home,
|
|
|
182
182
|
+ "remove it and install again", { config });
|
|
183
183
|
}
|
|
184
184
|
const permissions = prepareLivePermissions(foreign.source, { home, codexHome, stateRoot,
|
|
185
|
-
file: config, requestedLivePolicy, livePolicy, clientVersion, platform });
|
|
185
|
+
file: config, requestedLivePolicy, livePolicy, clientVersion, platform, receiverSockets });
|
|
186
186
|
const theirSandbox = permissions.skipLegacy || inspectConfig(permissions.source, config).sandbox;
|
|
187
187
|
const permissionActions = permissions.status?.state === "unverified" ? [permissions.status.diagnostic] : [];
|
|
188
188
|
|
|
@@ -338,7 +338,8 @@ export async function uninstallCodexPlugin({ home, agentsHome = home,
|
|
|
338
338
|
}
|
|
339
339
|
|
|
340
340
|
export async function detectCodex({ home, agentsHome = home,
|
|
341
|
-
codexHome = path.join(home, ".codex"), stateRoot, clientVersion, platform, nativeDelivery
|
|
341
|
+
codexHome = path.join(home, ".codex"), stateRoot, clientVersion, platform, nativeDelivery,
|
|
342
|
+
receiverSockets }) {
|
|
342
343
|
const marketplace = await readJson(marketplacePath(agentsHome), null);
|
|
343
344
|
const published = (marketplace?.plugins ?? []).some(entry => entry.name === PLUGIN_NAME);
|
|
344
345
|
const config = await readFile(configPath(codexHome), "utf8").catch(() => "");
|
|
@@ -347,7 +348,7 @@ export async function detectCodex({ home, agentsHome = home,
|
|
|
347
348
|
const cached = await stat(cachePath(codexHome))
|
|
348
349
|
.then(() => true).catch(() => false);
|
|
349
350
|
const outgoingDelivery = outgoingStatus(config, { home, codexHome, stateRoot,
|
|
350
|
-
file: configPath(codexHome), clientVersion, platform });
|
|
351
|
+
file: configPath(codexHome), clientVersion, platform, receiverSockets });
|
|
351
352
|
// Saved trust is not readiness. Codex compares every current definition's
|
|
352
353
|
// hash and can disable a trusted hook. Even a commented or stale single record
|
|
353
354
|
// previously suppressed this check. Leave verification to Codex's /hooks;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
2
|
-
import {
|
|
3
|
+
import { tomlString } from "@agents-can-communicate/adapter-sdk";
|
|
3
4
|
import { compareVersions } from "./app-server-client.mjs";
|
|
4
5
|
import { addPermissions, inspectPermissions } from "./permission-ownership.mjs";
|
|
5
6
|
import { scanConfig } from "./toml-scan.mjs";
|
|
@@ -23,8 +24,12 @@ const configured = (source, context) => {
|
|
|
23
24
|
&& value(["features", "network_proxy"]) === "true"
|
|
24
25
|
&& value(["permissions", profile, "network", "enabled"]) === "true"
|
|
25
26
|
&& literal(value(["permissions", profile, "filesystem", context.stateRoot])) === "write"
|
|
26
|
-
&&
|
|
27
|
+
&& missingSockets(entries, profile, context).length === 0;
|
|
27
28
|
};
|
|
29
|
+
// The grants a profile lacks for the sockets ACC delivers to.
|
|
30
|
+
const missingSockets = (entries, profile, context) => sockets(context).filter(socket =>
|
|
31
|
+
literal(entries.find(entry => !entry.header
|
|
32
|
+
&& equal(entry.keys, ["permissions", profile, "network", "unix_sockets", socket]))?.value) !== "allow");
|
|
28
33
|
|
|
29
34
|
const hasCustomPolicy = (source, context) => {
|
|
30
35
|
const ownership = inspectPermissions(source);
|
|
@@ -48,10 +53,37 @@ const hasCustomPolicy = (source, context) => {
|
|
|
48
53
|
|| proxies.length > 1 || proxies.some(entry => entry.header || !["true", "false"].includes(entry.value));
|
|
49
54
|
};
|
|
50
55
|
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
const
|
|
54
|
-
|
|
56
|
+
// Where this adapter's own sessions receive: the app-server control socket in
|
|
57
|
+
// Codex's actual home.
|
|
58
|
+
export const controlSocket = context => path.join(context.codexHome ?? path.join(context.home, ".codex"),
|
|
59
|
+
"app-server-control", "app-server-control.sock");
|
|
60
|
+
|
|
61
|
+
// Codex 0.157.1 on macOS, measured with `codex sandbox -P` (2026-09-27): a
|
|
62
|
+
// granted path that exists when Codex builds the sandbox policy is compared in
|
|
63
|
+
// its resolved form, and one that does not is kept as written, while Seatbelt
|
|
64
|
+
// checks the path the kernel resolved. A `/tmp/cc-socks-501` grant made before
|
|
65
|
+
// that directory existed never matched `/private/tmp/cc-socks-501/<pid>.sock`;
|
|
66
|
+
// `/private/tmp/cc-socks-501` did, both before and after. So the existing
|
|
67
|
+
// parent is resolved the way the kernel will, and the last component is kept as
|
|
68
|
+
// spelled: a socket that is a link - Codex's own control socket - must keep
|
|
69
|
+
// naming the link, because its target changes with every daemon.
|
|
70
|
+
export function grantPath(target, realpath = realpathSync.native) {
|
|
71
|
+
const tail = [path.basename(target)];
|
|
72
|
+
for (let parent = path.dirname(target); ; parent = path.dirname(parent)) {
|
|
73
|
+
try {
|
|
74
|
+
return path.join(realpath(parent), ...tail);
|
|
75
|
+
} catch {
|
|
76
|
+
if (parent === path.dirname(parent)) return target;
|
|
77
|
+
tail.unshift(path.basename(parent));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Every socket ACC delivers to - Codex's own and each receiving adapter's
|
|
83
|
+
// declaration, composed by the installer as `receiverSockets` - and nothing
|
|
84
|
+
// else: grants cover ACC's local receivers, never arbitrary Unix sockets.
|
|
85
|
+
const sockets = context => [...new Set([controlSocket(context), ...(context.receiverSockets ?? [])]
|
|
86
|
+
.map(socket => grantPath(socket, context.realpath)))].sort();
|
|
55
87
|
|
|
56
88
|
export function outgoingStatus(source, context) {
|
|
57
89
|
const state = inspectPermissions(source).state;
|
|
@@ -62,8 +94,10 @@ export function outgoingStatus(source, context) {
|
|
|
62
94
|
: ready ? null : "sender_permissions_unverified";
|
|
63
95
|
return { state: reasonCode === null ? "configured" : "unverified", reasonCode,
|
|
64
96
|
setup: supported(context)
|
|
65
|
-
? "Codex outgoing setup: default workspace settings gain ACC state write access and an
|
|
66
|
-
+ "
|
|
97
|
+
? "Codex outgoing setup: default workspace settings gain ACC state write access and an allowlist of the "
|
|
98
|
+
+ "local sockets ACC delivers to - ACC's own channel, the Codex control socket and other clients' session "
|
|
99
|
+
+ "inboxes - through the network proxy (external network stays denied); custom policies are preserved. "
|
|
100
|
+
+ "Start a new session after installation."
|
|
67
101
|
: null,
|
|
68
102
|
diagnostic: reasonCode === null
|
|
69
103
|
? "outgoing live delivery: local socket permissions configured for new sessions; active session overrides remain unverified"
|
|
@@ -73,8 +107,21 @@ export function outgoingStatus(source, context) {
|
|
|
73
107
|
: custom
|
|
74
108
|
? "custom permission policy was preserved; manually allow ACC state and local sockets "
|
|
75
109
|
+ "through the network proxy in the effective workspace profile"
|
|
76
|
-
:
|
|
77
|
-
|
|
110
|
+
: state === "owned"
|
|
111
|
+
? `${incomplete(source, context)}; run acc install --adapter codex, then start a new session`
|
|
112
|
+
: "ACC outgoing grants are absent; run acc install --adapter codex --delivery actionable, "
|
|
113
|
+
+ "then start a new session") };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// An owned profile ACC wrote for fewer receivers - 0.8.1 predates the Claude
|
|
117
|
+
// Code inbox grant - or for another state root. Reinstalling rewrites it.
|
|
118
|
+
function incomplete(source, context) {
|
|
119
|
+
const entries = scanConfig(source);
|
|
120
|
+
const profile = literal(entries.find(entry => !entry.header && equal(entry.keys, ["default_permissions"]))?.value);
|
|
121
|
+
const missing = typeof profile === "string" ? missingSockets(entries, profile, context) : [];
|
|
122
|
+
return missing.length > 0
|
|
123
|
+
? `ACC outgoing grants do not cover every socket ACC delivers to; missing ${missing.join(", ")}`
|
|
124
|
+
: "ACC outgoing grants are out of date";
|
|
78
125
|
}
|
|
79
126
|
|
|
80
127
|
export function prepareLivePermissions(source, context) {
|
|
@@ -85,10 +132,15 @@ export function prepareLivePermissions(source, context) {
|
|
|
85
132
|
// Once ACC owns a valid outgoing profile, turning incoming requests off must
|
|
86
133
|
// not revoke the already approved ability to use ACC from the sandbox. A
|
|
87
134
|
// fresh off install still creates no grants because it has no owned unit.
|
|
88
|
-
|
|
135
|
+
// An owned unit written for fewer receivers is brought up to date even so:
|
|
136
|
+
// the approval was for ACC's local sockets, and without this `acc install`
|
|
137
|
+
// under a recorded off could never repair a profile an older ACC wrote.
|
|
138
|
+
const stale = owned.state === "owned" && supported(context) && Boolean(context.stateRoot)
|
|
139
|
+
&& !configured(source, context) && !hasCustomPolicy(owned.source, context);
|
|
140
|
+
if (!requested && owned.state === "owned" && !stale) {
|
|
89
141
|
return { source, skipLegacy: true, status: outgoingStatus(source, context) };
|
|
90
142
|
}
|
|
91
|
-
if (!requested) return { source: owned.source, skipLegacy: hasPermissions(scanConfig(owned.source)) };
|
|
143
|
+
if (!requested && !stale) return { source: owned.source, skipLegacy: hasPermissions(scanConfig(owned.source)) };
|
|
92
144
|
if (!supported(context) || !context.stateRoot) {
|
|
93
145
|
return { source, skipLegacy: hasPermissions(scanConfig(source)), status: outgoingStatus(source, context) };
|
|
94
146
|
}
|
|
@@ -129,6 +129,13 @@ export function defineAdapter(manifest) {
|
|
|
129
129
|
usage(`optional native method ${method} must be a function`, { id: manifest.id, method });
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
|
+
// `inboundSockets(context)` names the Unix socket paths - directories or
|
|
133
|
+
// sockets - a sender connects to when this adapter's sessions receive live
|
|
134
|
+
// delivery. A sandboxed sender's installer allows exactly these, so a value
|
|
135
|
+
// that is not a function would silently read as "nothing to allow".
|
|
136
|
+
if (manifest.inboundSockets !== undefined && typeof manifest.inboundSockets !== "function") {
|
|
137
|
+
usage("optional inboundSockets must be a function", { id: manifest.id });
|
|
138
|
+
}
|
|
132
139
|
if (manifest.nativeDelivery !== undefined) {
|
|
133
140
|
const client = manifest.client.certificationName ?? manifest.client.command;
|
|
134
141
|
native.nativeDelivery = validateNativeDeliveryContract(manifest.nativeDelivery,
|
|
@@ -84,6 +84,15 @@ const PLACEHOLDER = "{{ACC}}";
|
|
|
84
84
|
// shell's to interpret.
|
|
85
85
|
const quote = value => `"${String(value).replace(/(["\\$`])/g, "\\$1")}"`;
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Whether a path is one shell word exactly as written: absolute, and made only
|
|
89
|
+
* of the POSIX portable filename characters and slashes. Nothing in it is
|
|
90
|
+
* expanded, split or quoted by a shell, so the unquoted text and the word a
|
|
91
|
+
* shell runs are the same string.
|
|
92
|
+
*/
|
|
93
|
+
export const isShellWord = value => typeof value === "string"
|
|
94
|
+
&& /^\/[A-Za-z0-9._/-]*$/.test(value);
|
|
95
|
+
|
|
87
96
|
/**
|
|
88
97
|
* Write the small shim a client's hook command points at.
|
|
89
98
|
*
|
|
@@ -231,13 +240,18 @@ export const defaultCli = () => ownBinary("acc.mjs");
|
|
|
231
240
|
* JSON, worked out the schema, and wrote records and events by hand - inventing
|
|
232
241
|
* an event type, a harness name, and its own generation tokens. So the command
|
|
233
242
|
* is pinned here for the same reason it is pinned in the shim.
|
|
243
|
+
*
|
|
244
|
+
* `bareWhenSafe` names the shim without quotes when it is one shell word. A
|
|
245
|
+
* client that matches allow rules against the command's first word needs it:
|
|
246
|
+
* Antigravity CLI 1.2.12 never matched a command whose program word was quoted,
|
|
247
|
+
* whatever rule was loaded (issue #214). A path that needs quotes keeps them.
|
|
234
248
|
*/
|
|
235
|
-
export async function bakeSkillCommand({ root, cliShim }) {
|
|
249
|
+
export async function bakeSkillCommand({ root, cliShim, bareWhenSafe = false }) {
|
|
236
250
|
if (typeof cliShim !== "string" || cliShim === "") {
|
|
237
251
|
throw new AccError(EXIT.DATA, "refusing to bake an empty skill command", { cliShim });
|
|
238
252
|
}
|
|
239
253
|
// A managed data home is "Application Support"; an unquoted path splits.
|
|
240
|
-
const command = quote(cliShim);
|
|
254
|
+
const command = bareWhenSafe && isShellWord(cliShim) ? cliShim : quote(cliShim);
|
|
241
255
|
const baked = [];
|
|
242
256
|
const walk = async directory => {
|
|
243
257
|
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
@@ -9,7 +9,7 @@ export { NATIVE_ACTIVATION_KINDS, NATIVE_BINDING_MODES, NATIVE_REASON_CODES,
|
|
|
9
9
|
validateNativeActivationPlan, validateNativeDeliveryContract, validateNativeHandshake }
|
|
10
10
|
from "./native-delivery.mjs";
|
|
11
11
|
export { EVENT_KINDS, NORMALIZED_EVENT_KEYS, normalizedEvent } from "./events.mjs";
|
|
12
|
-
export { assertRunner, bakeSkillCommand, defaultAntigravityRelay, defaultCli,
|
|
12
|
+
export { assertRunner, bakeSkillCommand, defaultAntigravityRelay, defaultCli, isShellWord,
|
|
13
13
|
defaultRunner, removeInstalledTree, runnerExists, writeCliShim, writeHookShim }
|
|
14
14
|
from "./hook-shim.mjs";
|
|
15
15
|
export { BEGIN, END, removeTomlBlock, renderBlock, stripBlock, tomlString, writeTomlBlock }
|
|
@@ -12,7 +12,7 @@ export { describeNative } from "./native-delivery-status.mjs";
|
|
|
12
12
|
import { nativeSessionLines, updateNativeSessions } from "./native-session-diagnostics.mjs";
|
|
13
13
|
import { ALL_ADAPTERS, clientContext, probeTimeout } from "./install-command.mjs";
|
|
14
14
|
import { decisionOf } from "./install-delivery-consent.mjs";
|
|
15
|
-
import { describePresence } from "./main.mjs";
|
|
15
|
+
import { describePresence, presenceBreakdown } from "./main.mjs";
|
|
16
16
|
import { platformPaths } from "./platform-paths.mjs";
|
|
17
17
|
import { noticeUpdate } from "./update-check.mjs";
|
|
18
18
|
import { managedUpdateDiagnostic } from "./managed-runtime/diagnostics.mjs";
|
|
@@ -116,7 +116,7 @@ export async function diagnoseAdapters({ options, runtime, detect = detectInstal
|
|
|
116
116
|
env: runtime?.env ?? {} });
|
|
117
117
|
// The same environment install reads, so detection probes the same client.
|
|
118
118
|
const clients = clientContext(home, path.join(dataHome, "acc"),
|
|
119
|
-
{ env: runtime?.env ?? {} });
|
|
119
|
+
{ env: runtime?.env ?? {}, platform: runtime?.platform });
|
|
120
120
|
const adapters = ALL_ADAPTERS();
|
|
121
121
|
const detected = await detect({ adapters, context: clients,
|
|
122
122
|
probeTimeoutMs: probeTimeout(runtime?.env) });
|
|
@@ -192,8 +192,12 @@ export async function runDoctor({ options, context, runtime }) {
|
|
|
192
192
|
// "unreadable protocol.json" and a person's first `acc doctor` in a new
|
|
193
193
|
// project would answer that their store is broken.
|
|
194
194
|
const started = await stat(path.join(root, "protocol.json")).then(() => true, () => false);
|
|
195
|
+
// Shaped like the storage report, so a reader of one finds every field of
|
|
196
|
+
// the other: without `trimmedThrough` the summary said "history from
|
|
197
|
+
// undefined" on every first doctor in a new project.
|
|
195
198
|
const report = !started
|
|
196
|
-
? { healthy: true, blocked: [], corrupt: [], repaired: []
|
|
199
|
+
? { healthy: true, blocked: [], corrupt: [], repaired: [], swept: 0, staged: 0, partials: 0,
|
|
200
|
+
retired: 0, trimmedThrough: null }
|
|
197
201
|
: options.repair === true
|
|
198
202
|
? await repairFilesystemStore({ root, clock })
|
|
199
203
|
: await diagnoseFilesystemStore({ root });
|
|
@@ -276,7 +280,8 @@ export async function runDoctor({ options, context, runtime }) {
|
|
|
276
280
|
report.trimmedThrough === null ? null : `history from ${report.trimmedThrough}`,
|
|
277
281
|
].filter(Boolean);
|
|
278
282
|
const staging = held.length === 0 ? "" : ` (${held.join(", ")})`;
|
|
279
|
-
const text = [`store healthy${staging};
|
|
283
|
+
const text = [`store healthy${staging}; `
|
|
284
|
+
+ `${describePresence(status.counts, presenceBreakdown(status.participants, ALL_ADAPTERS()))}; `
|
|
280
285
|
+ `protection ${status.protection}; ${installed} of ${adapters.length} adapter(s) installed`,
|
|
281
286
|
...adapters.filter(adapter => (adapter.present || adapter.installed)
|
|
282
287
|
&& adapter.nativeDelivery.reasonCode === "native_delivery_unsupported"
|