agents-can-communicate 0.8.0 → 0.8.2
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/bin/entrypoints/acc-mcp.mjs +0 -1
- package/bin/entrypoints/acc.mjs +0 -1
- package/docs/ADAPTER_AUTHORING.md +50 -17
- package/docs/ARCHITECTURE.md +9 -6
- package/docs/CAPABILITIES.md +24 -14
- package/docs/CLI.md +22 -3
- package/docs/CONFIGURATION.md +73 -8
- package/docs/GETTING_STARTED.md +6 -3
- package/docs/HOW_IT_WORKS.md +3 -1
- package/docs/PROTOCOL.md +2 -2
- package/docs/RELEASING.md +6 -4
- package/docs/SECURITY_MODEL.md +12 -1
- package/docs/TROUBLESHOOTING.md +34 -7
- package/docs/UPGRADING.md +29 -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 +5 -4
- 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 +14 -4
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-delivery.mjs +12 -17
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-endpoint.mjs +5 -3
- 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 +6 -4
- package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +10 -13
- 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 +69 -17
- package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance-host.mjs +46 -10
- package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance.mjs +5 -7
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +16 -12
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +12 -2
- package/node_modules/@agents-can-communicate/adapter-codex/src/service-setup.mjs +26 -9
- package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +12 -0
- 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/certification.mjs +2 -2
- 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 +4 -3
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +85 -77
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +4 -4
- 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 +49 -12
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/maintenance.mjs +9 -15
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/pins.mjs +13 -4
- 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/receipts.mjs +17 -2
- package/node_modules/@agents-can-communicate/core/src/status.mjs +26 -2
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/delivery-router/src/refresh-binding.mjs +2 -2
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +9 -41
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/client-pid.mjs +30 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +3 -4
- package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +30 -14
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +12 -1
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +0 -2
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +13 -7
- 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
package/docs/UPGRADING.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Upgrading to 0.5.0
|
|
2
2
|
|
|
3
|
+
## From 0.8.x
|
|
4
|
+
|
|
5
|
+
Update as usual:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
acc update
|
|
9
|
+
acc version
|
|
10
|
+
acc doctor
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The update refreshes every installed integration with its recorded policy. Two of them change
|
|
14
|
+
a client's own settings:
|
|
15
|
+
|
|
16
|
+
- **Codex.** ACC's permission profile in `~/.codex/config.toml` also allows the Claude Code
|
|
17
|
+
inbox socket directories, so a Codex session can wake a Claude Code session live. Grants are
|
|
18
|
+
written in their resolved form, such as `/private/tmp/cc-socks` on macOS. A profile you
|
|
19
|
+
edited stays yours: doctor then names the missing grants and `acc install --adapter codex`.
|
|
20
|
+
Start a new Codex session after the update; a session already open keeps its old policy.
|
|
21
|
+
- **Antigravity CLI.** The install adds `command(<wrapper>)` to `permissions.allow` in
|
|
22
|
+
`~/.gemini/antigravity-cli/settings.json`, and the skill calls the wrapper without quotes, so
|
|
23
|
+
a woken session runs its ACC commands without an approval prompt. Restart `agy`: it reads
|
|
24
|
+
the file only at startup. `acc uninstall` removes every copy of that rule.
|
|
25
|
+
|
|
26
|
+
`acc doctor` and `acc status` describe an idle session by what a message sent to it meets
|
|
27
|
+
(`idle, wake on send`, `idle until next turn`, `idle, inbox only`), and each status row carries
|
|
28
|
+
`processTracked`, `liveDelivery` and `liveLeaseCurrent`. This release adds no store event type
|
|
29
|
+
or field, so a store it writes stays readable by 0.8.x.
|
|
30
|
+
|
|
3
31
|
## From 0.7.x
|
|
4
32
|
|
|
5
33
|
Claude Code live delivery wakes each session through the inbox socket that Claude Code opens
|
|
@@ -165,7 +193,7 @@ keeps holding activation across an uninstall until it exits.
|
|
|
165
193
|
|
|
166
194
|
## Confirmed client service maintenance
|
|
167
195
|
|
|
168
|
-
|
|
196
|
+
`acc update` can ask once to restart a verified Codex service that holds a
|
|
169
197
|
native binding blocking activation, or that is serving a version outside the captured
|
|
170
198
|
native-delivery contract. A serving version that satisfies that contract is left alone,
|
|
171
199
|
even when the installed CLI has moved to a newer build. The captured maintenance contract
|
|
@@ -41,6 +41,13 @@ work. An MCP connection can have a different participant from the hook session;
|
|
|
41
41
|
use inbox/reply only for the participant the message addresses. Do not borrow a
|
|
42
42
|
peer's ID, read runtime bindings, or improvise credentials.
|
|
43
43
|
|
|
44
|
+
### Run each command as written
|
|
45
|
+
|
|
46
|
+
Start every ACC command with `{{ACC}}` exactly as this skill writes it: add no
|
|
47
|
+
quotes around it and no `sh -c` wrapper. Antigravity CLI decides from a command's
|
|
48
|
+
first word whether it needs approval, and the rule that lets ACC commands run
|
|
49
|
+
without a prompt matches only that form.
|
|
50
|
+
|
|
44
51
|
## Start shared work once
|
|
45
52
|
|
|
46
53
|
After understanding the request, publish one concise intent:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineAdapter, projectContext, projectContextResult }
|
|
1
|
+
import { channelSocketDirectory, defineAdapter, projectContext, projectContextResult }
|
|
2
2
|
from "@agents-can-communicate/adapter-sdk";
|
|
3
3
|
import certification from "../certification.json" with { type: "json" };
|
|
4
4
|
|
|
@@ -76,13 +76,14 @@ export function createAntigravityAdapter() {
|
|
|
76
76
|
// no retireNativeSession here: the hook runner retires and re-publishes the
|
|
77
77
|
// binding on every turn, and must not take the relay with it.
|
|
78
78
|
nativeDelivery: {
|
|
79
|
-
|
|
80
|
-
anchors: [{
|
|
81
|
-
protocolContract: PROTOCOL_CONTRACT }],
|
|
79
|
+
minimum: ANTIGRAVITY_CLI_VERSION,
|
|
80
|
+
anchors: [{ version: ANTIGRAVITY_CLI_VERSION, protocolContract: PROTOCOL_CONTRACT }],
|
|
82
81
|
knownBad: [], activationKinds: ["native-config"], policySource: "installation-record",
|
|
83
82
|
},
|
|
84
83
|
probeNativeDelivery, planNativeActivation, bindNativeSession, refreshNativeSession,
|
|
85
84
|
offerMessage,
|
|
85
|
+
// The relay binds in ACC's own channel directory; a sandboxed sender is allowed it.
|
|
86
|
+
inboundSockets: () => [channelSocketDirectory()],
|
|
86
87
|
deliveryFallback: { diagnostic:
|
|
87
88
|
`Antigravity CLI next-turn and live delivery are certified from ${ANTIGRAVITY_CLI_VERSION} `
|
|
88
89
|
+ "onward; live delivery also needs the agent to start ACC's relay once per conversation, "
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { blankJson, isShellWord, removeIfEmpty, writeForeignJson }
|
|
5
|
+
from "@agents-can-communicate/adapter-sdk";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The one allow rule ACC may add to Antigravity CLI's settings, with consent.
|
|
9
|
+
*
|
|
10
|
+
* Antigravity CLI asks before every shell command. On 1.2.12 a live push woke an
|
|
11
|
+
* idle session and its first ACC command waited at an approval prompt with
|
|
12
|
+
* nobody there, so the peer never got an answer (issue #214). A rule under
|
|
13
|
+
* `permissions.allow` removes the prompt. What 1.2.12 was captured doing:
|
|
14
|
+
*
|
|
15
|
+
* - The file is `~/.gemini/antigravity-cli/settings.json`, read once at startup.
|
|
16
|
+
* `~/.gemini/settings.json` belongs to Gemini CLI and is never touched here.
|
|
17
|
+
* - A one-word rule is a prefix: `command("<wrapper>")` let `<wrapper> help`
|
|
18
|
+
* run unasked.
|
|
19
|
+
* - A command whose first word is quoted matched no rule at all, quoted or not;
|
|
20
|
+
* quotes around an argument are fine. So the skill names the wrapper bare,
|
|
21
|
+
* and a wrapper path that only stays one word inside quotes cannot be allowed.
|
|
22
|
+
*
|
|
23
|
+
* ACC writes the bare form, `command(<wrapper>)`, on every install of this
|
|
24
|
+
* adapter: the user decided on 2026-09-27 that it is always on, with no
|
|
25
|
+
* question and no dependence on the delivery policy. The file is still the
|
|
26
|
+
* operator's: one that does not parse, or whose `permissions.allow` is not a
|
|
27
|
+
* list, is never rewritten. What ACC added is recorded in its own data home -
|
|
28
|
+
* the rule and the containers it had to create - and uninstall takes back
|
|
29
|
+
* exactly that, and always ends the claim, so the same words added later by
|
|
30
|
+
* the operator stay theirs.
|
|
31
|
+
*/
|
|
32
|
+
export const agySettingsPath = home => path.join(home, ".gemini", "antigravity-cli",
|
|
33
|
+
"settings.json");
|
|
34
|
+
export const cliWrapperPath = home => path.join(home, ".gemini", "config", "acc", "acc-cli.sh");
|
|
35
|
+
export const accAllowRule = wrapper => `command(${wrapper})`;
|
|
36
|
+
|
|
37
|
+
// Forms that already allow every command starting with the bare wrapper: the
|
|
38
|
+
// one ACC writes, and the quoted one-word form 1.2.12 was captured matching.
|
|
39
|
+
const allowingForms = wrapper => [accAllowRule(wrapper), `command("${wrapper}")`];
|
|
40
|
+
|
|
41
|
+
// Beside the `created-*` markers, for the same reason: the installer removes the
|
|
42
|
+
// shim directory before uninstall runs. `acc doctor` passes the state root.
|
|
43
|
+
const claimDir = ({ dataHome, stateRoot, home }) => typeof dataHome === "string" && dataHome !== ""
|
|
44
|
+
? path.join(dataHome, "acc", "adapter-antigravity")
|
|
45
|
+
: typeof stateRoot === "string" && stateRoot !== ""
|
|
46
|
+
? path.join(stateRoot, "adapter-antigravity")
|
|
47
|
+
: path.join(home, ".gemini", "config", "acc");
|
|
48
|
+
const claimPath = (context, file) => path.join(claimDir(context),
|
|
49
|
+
`allow-rule-${Buffer.from(file).toString("base64url")}`);
|
|
50
|
+
|
|
51
|
+
const isObject = value => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
52
|
+
|
|
53
|
+
async function readClaim(file) {
|
|
54
|
+
try {
|
|
55
|
+
const claim = JSON.parse(await readFile(file, "utf8"));
|
|
56
|
+
return typeof claim?.rule === "string" ? claim : null;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The settings as the client would read them, or why ACC must not edit them. */
|
|
63
|
+
async function readSettings(file) {
|
|
64
|
+
let text;
|
|
65
|
+
try {
|
|
66
|
+
text = await readFile(file, "utf8");
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error.code === "ENOENT") return { state: "missing", value: {}, allow: [] };
|
|
69
|
+
return { state: "unreadable", reason: `could not be read (${error.code ?? error.message})` };
|
|
70
|
+
}
|
|
71
|
+
let value;
|
|
72
|
+
try {
|
|
73
|
+
value = JSON.parse(text);
|
|
74
|
+
} catch {
|
|
75
|
+
return { state: "unreadable", reason: "is not valid JSON" };
|
|
76
|
+
}
|
|
77
|
+
if (!isObject(value)) return { state: "unreadable", reason: "does not hold a JSON object" };
|
|
78
|
+
if (value.permissions !== undefined && !isObject(value.permissions)) {
|
|
79
|
+
return { state: "unreadable", reason: "has a permissions value that is not an object" };
|
|
80
|
+
}
|
|
81
|
+
const allow = value.permissions?.allow;
|
|
82
|
+
if (allow !== undefined && !Array.isArray(allow)) {
|
|
83
|
+
return { state: "unreadable", reason: "has a permissions.allow value that is not a list" };
|
|
84
|
+
}
|
|
85
|
+
return { state: "read", value, allow: allow ?? [] };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const RESTART = "Antigravity CLI reads it at startup, so a session started before it was "
|
|
89
|
+
+ "added still asks until agy restarts";
|
|
90
|
+
|
|
91
|
+
/** What the rule's state is on this machine, in words doctor can print. */
|
|
92
|
+
export async function inspectAllowRule(context) {
|
|
93
|
+
const file = agySettingsPath(context.home);
|
|
94
|
+
const wrapper = cliWrapperPath(context.home);
|
|
95
|
+
const rule = accAllowRule(wrapper);
|
|
96
|
+
const base = { file, wrapper, rule, owned: false };
|
|
97
|
+
if (!isShellWord(wrapper)) {
|
|
98
|
+
return { ...base, state: "unmatchable", diagnostic: "each ACC command waits for approval, "
|
|
99
|
+
+ "so a live wake stops at the agent's first ACC command until someone answers the "
|
|
100
|
+
+ `approval prompt. No allow rule can remove it here: the wrapper ${wrapper} stays one `
|
|
101
|
+
+ "shell word only inside quotes, and Antigravity CLI matches no rule against a command "
|
|
102
|
+
+ "whose first word is quoted. ACC adds the prefix rule to permissions.allow in "
|
|
103
|
+
+ `${file} when the wrapper path holds only letters, digits and . _ - /` };
|
|
104
|
+
}
|
|
105
|
+
const settings = await readSettings(file);
|
|
106
|
+
if (settings.state === "unreadable") {
|
|
107
|
+
return { ...base, state: "unreadable", diagnostic: `${file} ${settings.reason}, so ACC `
|
|
108
|
+
+ "leaves it as it is and cannot tell whether ACC commands wait for approval; a live "
|
|
109
|
+
+ "wake stops at the agent's first ACC command while they do. Repair the file, then run "
|
|
110
|
+
+ `acc install --adapter antigravity to add ${rule} to permissions.allow` };
|
|
111
|
+
}
|
|
112
|
+
const matched = settings.allow.find(entry => allowingForms(wrapper).includes(entry));
|
|
113
|
+
if (matched !== undefined) {
|
|
114
|
+
const claim = await readClaim(claimPath(context, file));
|
|
115
|
+
const owned = claim?.rule === matched;
|
|
116
|
+
return { ...base, state: "allowed", owned, diagnostic: "ACC commands run without an "
|
|
117
|
+
+ `approval prompt: permissions.allow in ${file} holds ${matched}`
|
|
118
|
+
+ `${owned ? ", added by ACC" : ""}. ${RESTART}` };
|
|
119
|
+
}
|
|
120
|
+
// Every install writes the rule, so its absence means one did not run here, or
|
|
121
|
+
// the rule was taken out since.
|
|
122
|
+
return { ...base, state: "prompts",
|
|
123
|
+
diagnostic: "each ACC command waits for approval, so a live wake stops at the agent's "
|
|
124
|
+
+ "first ACC command until someone answers the approval prompt. ACC's install adds "
|
|
125
|
+
+ `${rule} to permissions.allow in ${file}, and it is not there: run acc install `
|
|
126
|
+
+ "--adapter antigravity, then restart agy" };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function addRule(context, found) {
|
|
130
|
+
const settings = await readSettings(found.file);
|
|
131
|
+
const claimFile = claimPath(context, found.file);
|
|
132
|
+
// Only what this write creates. A file or container that is already there is
|
|
133
|
+
// the operator's as far as ACC can tell: one ACC made earlier cannot be told
|
|
134
|
+
// from one they put in its place, and carrying the old flags over deleted
|
|
135
|
+
// their file at uninstall (review of #218). The cost is an empty container
|
|
136
|
+
// ACC once made staying after uninstall, never the operator's file going.
|
|
137
|
+
const created = {
|
|
138
|
+
file: settings.state === "missing",
|
|
139
|
+
permissions: settings.value.permissions === undefined,
|
|
140
|
+
allow: settings.value.permissions?.allow === undefined,
|
|
141
|
+
};
|
|
142
|
+
// The claim first: a crash between the two leaves a claim with no rule, which
|
|
143
|
+
// uninstall drops, rather than a rule nobody can tell is ACC's.
|
|
144
|
+
await mkdir(path.dirname(claimFile), { recursive: true });
|
|
145
|
+
await writeFile(claimFile, `${JSON.stringify({ file: found.file, rule: found.rule,
|
|
146
|
+
created })}\n`);
|
|
147
|
+
await writeForeignJson(found.file, { ...settings.value, permissions: {
|
|
148
|
+
...settings.value.permissions, allow: [...settings.allow, found.rule] } },
|
|
149
|
+
{ readFile, writeFile, mkdir });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Take back the rule ACC recorded adding, and the containers it created if they
|
|
154
|
+
* are empty. Every copy of it goes: one in the same words cannot be told from
|
|
155
|
+
* ACC's, and the client itself has duplicated grants across reloads, so taking
|
|
156
|
+
* one back left ACC commands running unasked after uninstall (review of #218).
|
|
157
|
+
* Anything else in the file stays, and a rule the operator had before ACC came
|
|
158
|
+
* was never claimed.
|
|
159
|
+
*/
|
|
160
|
+
export async function withdrawAllowRule(context) {
|
|
161
|
+
const file = agySettingsPath(context.home);
|
|
162
|
+
const claimFile = claimPath(context, file);
|
|
163
|
+
const claim = await readClaim(claimFile);
|
|
164
|
+
if (claim === null) return { changes: [], diagnostics: [], needsAction: [] };
|
|
165
|
+
const settings = await readSettings(file);
|
|
166
|
+
if (settings.state === "unreadable") {
|
|
167
|
+
// Kept, so the next install or uninstall can finish once the file reads.
|
|
168
|
+
return { changes: [], diagnostics: [], needsAction: [`${file} ${settings.reason}; ACC `
|
|
169
|
+
+ `left it as it is and still has to remove its ${claim.rule} from permissions.allow`] };
|
|
170
|
+
}
|
|
171
|
+
if (!settings.allow.includes(claim.rule)) {
|
|
172
|
+
await rm(claimFile, { force: true });
|
|
173
|
+
return { changes: [], diagnostics: [], needsAction: [] };
|
|
174
|
+
}
|
|
175
|
+
const permissions = { ...settings.value.permissions,
|
|
176
|
+
allow: settings.allow.filter(rule => rule !== claim.rule) };
|
|
177
|
+
if (claim.created?.allow === true && permissions.allow.length === 0) delete permissions.allow;
|
|
178
|
+
const next = { ...settings.value, permissions };
|
|
179
|
+
if (claim.created?.permissions === true && Object.keys(permissions).length === 0) {
|
|
180
|
+
delete next.permissions;
|
|
181
|
+
}
|
|
182
|
+
await writeForeignJson(file, next, { readFile, writeFile, mkdir });
|
|
183
|
+
if (claim.created?.file === true) {
|
|
184
|
+
await removeIfEmpty(file, { readFile, rm, isEmpty: blankJson() });
|
|
185
|
+
}
|
|
186
|
+
await rm(claimFile, { force: true });
|
|
187
|
+
return { changes: [file], diagnostics: [`removed ACC's ${claim.rule} from ${file}`],
|
|
188
|
+
needsAction: [] };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Put ACC's rule in place, at every install. */
|
|
192
|
+
export async function ensureAllowRule(context) {
|
|
193
|
+
const found = await inspectAllowRule(context);
|
|
194
|
+
if (found.state === "unmatchable" || found.state === "unreadable") {
|
|
195
|
+
return { changes: [], diagnostics: [found.diagnostic], needsAction: [found.diagnostic] };
|
|
196
|
+
}
|
|
197
|
+
if (found.state === "allowed") {
|
|
198
|
+
return { changes: [], diagnostics: [found.diagnostic], needsAction: [] };
|
|
199
|
+
}
|
|
200
|
+
await addRule(context, found);
|
|
201
|
+
return { changes: [found.file], needsAction: [], diagnostics: [`added ${found.rule} to `
|
|
202
|
+
+ `permissions.allow in ${found.file}: ACC commands run without an approval prompt in `
|
|
203
|
+
+ "Antigravity sessions started from now on; restart agy for a session already open"] };
|
|
204
|
+
}
|
|
@@ -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";
|
|
@@ -22,8 +24,9 @@ export const CLAUDE_DELIVERY_FALLBACK = Object.freeze({
|
|
|
22
24
|
* and a parent/child mapping claimed without observation is the kind of thing
|
|
23
25
|
* that quietly maps every child onto its parent. `startupInjection` and
|
|
24
26
|
* `safePointInjection` were not exercised - only the before-turn path was.
|
|
25
|
-
* Native live delivery is the inbox wake, certified from 2.1.282 on
|
|
26
|
-
* darwin-arm64
|
|
27
|
+
* Native live delivery is the inbox wake, certified from 2.1.282 (captured on
|
|
28
|
+
* darwin-arm64, applied on every platform the probe admits); reply routing
|
|
29
|
+
* through the transport is not claimed.
|
|
27
30
|
*/
|
|
28
31
|
export function createClaudeCodeAdapter() {
|
|
29
32
|
return defineAdapter({
|
|
@@ -52,8 +55,8 @@ export function createClaudeCodeAdapter() {
|
|
|
52
55
|
// newer stable client is admitted by the probe and the per-session
|
|
53
56
|
// handshake. The offer is a wake; see src/inbox-delivery.mjs.
|
|
54
57
|
nativeDelivery: {
|
|
55
|
-
|
|
56
|
-
anchors: [{
|
|
58
|
+
minimum: MIN_VERSION,
|
|
59
|
+
anchors: [{ version: MIN_VERSION, protocolContract: PROTOCOL_CONTRACT }],
|
|
57
60
|
knownBad: [],
|
|
58
61
|
activationKinds: ["native-service"],
|
|
59
62
|
policySource: "installation-record",
|
|
@@ -91,6 +94,13 @@ export function createClaudeCodeAdapter() {
|
|
|
91
94
|
refreshNativeSession,
|
|
92
95
|
retireNativeSession,
|
|
93
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
|
+
],
|
|
94
104
|
|
|
95
105
|
denyOutcome,
|
|
96
106
|
injectOutcome,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import net from "node:net";
|
|
2
|
+
import { compareVersionOrder, versionOrder } from "@agents-can-communicate/adapter-sdk";
|
|
2
3
|
|
|
3
4
|
import { INBOX_MODES, MIN_VERSION, PROTOCOL_CONTRACT, TRANSPORT } from "./inbox-contract.mjs";
|
|
4
5
|
import { claimWake, newEndpointId, readInboxEndpoint, releaseWake, removeInboxEndpoint,
|
|
@@ -26,7 +27,6 @@ const LEASE_MS = 120_000;
|
|
|
26
27
|
const MESSAGE_ID = /^[A-Za-z0-9_-]{1,200}$/;
|
|
27
28
|
const PROBE_NEEDLE = Buffer.from("messagingSocketPath");
|
|
28
29
|
const PROBE_MAX_BYTES = 256 * 1024 * 1024;
|
|
29
|
-
const STABLE_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z.-]+)?$/;
|
|
30
30
|
|
|
31
31
|
export { INBOX_MODES, MIN_VERSION, PROTOCOL_CONTRACT, TRANSPORT };
|
|
32
32
|
|
|
@@ -37,12 +37,11 @@ export const wakeText = messageId => `ACC: new peer message ${messageId} for thi
|
|
|
37
37
|
+ `acc inbox --message ${messageId}. Answer it through ACC with acc reply --message ${messageId}; `
|
|
38
38
|
+ "SendMessage cannot deliver to an ACC participant.";
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
40
|
+
// A version is judged by its release triple: a prerelease of a version above
|
|
41
|
+
// the minimum is not an older client, and the inbox in the executable is what
|
|
42
|
+
// admits it. An unreadable version orders as null.
|
|
43
|
+
const belowMinimum = clientVersion => compareVersionOrder(versionOrder(clientVersion),
|
|
44
|
+
versionOrder(MIN_VERSION)) < 0;
|
|
46
45
|
|
|
47
46
|
// The registry field only a build with the inbox writes. Read-only, bounded.
|
|
48
47
|
async function executableHasInbox(realExecutable) {
|
|
@@ -92,8 +91,8 @@ function withTimeout(work, ms) {
|
|
|
92
91
|
}
|
|
93
92
|
|
|
94
93
|
/**
|
|
95
|
-
* Read-only: a
|
|
96
|
-
*
|
|
94
|
+
* Read-only: a version at or above the capture, a platform whose inbox is a
|
|
95
|
+
* Unix socket, and an executable that writes the inbox into its session
|
|
97
96
|
* registry. Never launches a session. Native Windows serves a named pipe that
|
|
98
97
|
* demands an auth line; nothing there is captured, so it keeps hook delivery.
|
|
99
98
|
*/
|
|
@@ -105,10 +104,8 @@ export async function probeNativeDelivery({ realExecutable, timeoutMs = 750, pla
|
|
|
105
104
|
if (typeof realExecutable !== "string" || realExecutable === "") return unsupported("feature_probe_failed");
|
|
106
105
|
const clientVersion = await withTimeout(Promise.resolve(readVersion(realExecutable, timeoutMs)), timeoutMs)
|
|
107
106
|
.catch(() => null);
|
|
108
|
-
if (clientVersion === null
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
if (compare(clientVersion, MIN_VERSION) < 0) return unsupported("below_minimum_version", clientVersion);
|
|
107
|
+
if (versionOrder(clientVersion) === null) return unsupported("feature_probe_failed", clientVersion);
|
|
108
|
+
if (belowMinimum(clientVersion)) return unsupported("below_minimum_version", clientVersion);
|
|
112
109
|
const present = await withTimeout(Promise.resolve(hasInbox(realExecutable)), timeoutMs).catch(() => false);
|
|
113
110
|
if (!present) return unsupported("protocol_mismatch", clientVersion);
|
|
114
111
|
return { supported: true, clientVersion, protocolContract: PROTOCOL_CONTRACT,
|
|
@@ -145,13 +142,11 @@ export async function bindNativeSession({ event, clientPid, clientVersion, runti
|
|
|
145
142
|
if (typeof event?.sessionId !== "string" || event.sessionId === "") {
|
|
146
143
|
return closed(clientVersion, "handshake_failed");
|
|
147
144
|
}
|
|
148
|
-
if (
|
|
149
|
-
return closed(clientVersion, "version_unavailable");
|
|
150
|
-
}
|
|
145
|
+
if (versionOrder(clientVersion) === null) return closed(clientVersion, "version_unavailable");
|
|
151
146
|
// The contract refuses it anyway, after this returns. Refusing here keeps an
|
|
152
147
|
// older client - which binds again on every turn - from writing an endpoint
|
|
153
148
|
// record each time.
|
|
154
|
-
if (
|
|
149
|
+
if (belowMinimum(clientVersion)) return closed(clientVersion, "below_minimum_version");
|
|
155
150
|
const socketPath = env?.CLAUDE_CODE_MESSAGING_SOCKET;
|
|
156
151
|
if (typeof socketPath !== "string" || socketPath === "") {
|
|
157
152
|
return closed(clientVersion, "native_endpoint_unavailable");
|
|
@@ -2,6 +2,7 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
3
|
import { lstat, mkdir, open, readdir, realpath, rename, rm } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { versionOrder } from "@agents-can-communicate/adapter-sdk";
|
|
5
6
|
|
|
6
7
|
import { PROTOCOL_CONTRACT } from "./inbox-contract.mjs";
|
|
7
8
|
import { inboxSocketIsSafe } from "./inbox-registry.mjs";
|
|
@@ -14,13 +15,14 @@ import { isReception } from "./inbox-settings.mjs";
|
|
|
14
15
|
const KEYS = ["schemaVersion", "endpointId", "socketPath", "configDir", "clientPid", "sessionId",
|
|
15
16
|
"clientVersion", "protocolContract", "leaseUntil", "reception"];
|
|
16
17
|
const ENDPOINT = /^claude_inbox_[a-f0-9]{32}$/;
|
|
17
|
-
const STABLE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z.-]+)?$/;
|
|
18
18
|
const MAX_BYTES = 8_192;
|
|
19
19
|
const absolute = value => typeof value === "string" && path.isAbsolute(value) && !value.includes("\0");
|
|
20
20
|
const own = info => typeof process.getuid !== "function" || info.uid === process.getuid();
|
|
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)
|
|
@@ -29,7 +31,7 @@ function valid(record) {
|
|
|
29
31
|
&& absolute(record.socketPath) && absolute(record.configDir)
|
|
30
32
|
&& Number.isInteger(record.clientPid) && record.clientPid > 0
|
|
31
33
|
&& typeof record.sessionId === "string" && record.sessionId !== "" && record.sessionId.length <= 200
|
|
32
|
-
&& typeof record.clientVersion === "string" &&
|
|
34
|
+
&& typeof record.clientVersion === "string" && versionOrder(record.clientVersion) !== null
|
|
33
35
|
&& record.protocolContract === PROTOCOL_CONTRACT
|
|
34
36
|
&& typeof record.leaseUntil === "string"
|
|
35
37
|
&& /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(record.leaseUntil)
|
|
@@ -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
|
+
}
|