agents-can-communicate 0.7.1 → 0.8.1
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/README.md +3 -1
- package/bin/entrypoints/acc-bootstrap.mjs +6 -48
- package/bin/entrypoints/acc-claude-channel.mjs +47 -157
- package/bin/entrypoints/acc-mcp.mjs +0 -1
- package/bin/entrypoints/acc.mjs +0 -1
- package/docs/ADAPTER_AUTHORING.md +74 -24
- package/docs/ARCHITECTURE.md +31 -18
- package/docs/CAPABILITIES.md +38 -25
- package/docs/CLI.md +51 -13
- package/docs/CONCEPTS.md +5 -4
- package/docs/CONFIGURATION.md +18 -11
- package/docs/GETTING_STARTED.md +15 -12
- package/docs/GLOSSARY.md +2 -1
- package/docs/HOW_IT_WORKS.md +8 -4
- package/docs/PROTOCOL.md +25 -6
- package/docs/RELEASING.md +8 -5
- package/docs/SECURITY_MODEL.md +14 -1
- package/docs/TROUBLESHOOTING.md +66 -29
- package/docs/UPGRADING.md +49 -8
- package/docs/WHY_ACC.md +5 -3
- 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/src/adapter.mjs +2 -3
- package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +14 -73
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +29 -71
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.282-product-evidence.json +161 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.282.json +28 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/inbox-wake-2.1.282.json +44 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +5 -2
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +26 -30
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/hooks.mjs +6 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-contract.mjs +10 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-delivery.mjs +249 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-endpoint.mjs +166 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-registry.mjs +71 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-settings.mjs +109 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +25 -31
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +3 -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/live-permissions.mjs +5 -5
- 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/certification.mjs +2 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/src/events.mjs +16 -3
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +4 -3
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +4 -3
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-activation.mjs +4 -14
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +104 -84
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +8 -4
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +8 -6
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +8 -23
- package/node_modules/@agents-can-communicate/cli/src/install-delivery-consent.mjs +3 -4
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +24 -3
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +7 -43
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +5 -3
- 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/managed-runtime/refresh.mjs +3 -4
- package/node_modules/@agents-can-communicate/cli/src/native-delivery-status.mjs +2 -2
- 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/sessions.mjs +25 -2
- package/node_modules/@agents-can-communicate/core/src/status.mjs +31 -1
- 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 +33 -48
- 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-attempt.mjs +6 -8
- package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +27 -15
- 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 +4 -2
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +19 -5
- package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +0 -2
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +6 -11
- package/node_modules/@agents-can-communicate/installer/src/index.mjs +4 -7
- package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +23 -61
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +7 -1
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +15 -5
- package/node_modules/@agents-can-communicate/installer/src/shell-bootstrap.mjs +10 -133
- 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 +6 -1
- package/package.json +1 -1
- package/bin/acc-bootstrap.mjs +0 -7
- package/bin/acc-claude-channel.mjs +0 -10
- package/bin/entrypoints/claude-channel-binding.mjs +0 -46
- package/bin/entrypoints/claude-channel-stdio.mjs +0 -29
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/.mcp.json +0 -8
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/channel.mjs +0 -381
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +0 -232
- package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +0 -150
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"source": "real-client-capture",
|
|
4
|
+
"client": "claude-code",
|
|
5
|
+
"phase": "product",
|
|
6
|
+
"clientVersion": "2.1.282",
|
|
7
|
+
"platform": "darwin-arm64",
|
|
8
|
+
"packageSha256": "fa81c55588988ed7274636c564a0adb5c442110f014f82e7a3f94da26fdd8891",
|
|
9
|
+
"startedAt": "2026-09-25T21:31:56.000Z",
|
|
10
|
+
"finishedAt": "2026-09-25T21:35:56.792Z",
|
|
11
|
+
"scenarioCount": 6,
|
|
12
|
+
"passedCount": 6,
|
|
13
|
+
"failedCount": 0,
|
|
14
|
+
"cleanup": {
|
|
15
|
+
"attempted": true,
|
|
16
|
+
"outcome": "passed",
|
|
17
|
+
"ownedProcesses": "stopped",
|
|
18
|
+
"temporaryState": "removed"
|
|
19
|
+
},
|
|
20
|
+
"scenarios": [
|
|
21
|
+
{
|
|
22
|
+
"caseId": "C01",
|
|
23
|
+
"outcome": "passed",
|
|
24
|
+
"startedAt": "2026-09-25T21:31:56.000Z",
|
|
25
|
+
"finishedAt": "2026-09-25T21:32:44.339Z",
|
|
26
|
+
"messageId": "message_EaLKeCerBFIR_o0gQpT4vQ",
|
|
27
|
+
"observations": [
|
|
28
|
+
{
|
|
29
|
+
"kind": "delivery",
|
|
30
|
+
"at": "2026-09-25T21:32:44.339Z",
|
|
31
|
+
"outcome": "woken"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"kind": "wake",
|
|
35
|
+
"at": "2026-09-25T21:31:56.373Z",
|
|
36
|
+
"outcome": "started-turn"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"kind": "projection",
|
|
40
|
+
"at": "2026-09-25T21:31:56.808Z",
|
|
41
|
+
"outcome": "body-shown"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"kind": "offer",
|
|
45
|
+
"at": "2026-09-25T21:31:56.724Z",
|
|
46
|
+
"outcome": "next-turn"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"caseId": "C02",
|
|
52
|
+
"outcome": "passed",
|
|
53
|
+
"startedAt": "2026-09-25T21:33:05.000Z",
|
|
54
|
+
"finishedAt": "2026-09-25T21:34:19.560Z",
|
|
55
|
+
"messageId": "message_YFDLjj4jtNLDsC67GV9Y1g",
|
|
56
|
+
"observations": [
|
|
57
|
+
{
|
|
58
|
+
"kind": "delivery",
|
|
59
|
+
"at": "2026-09-25T21:34:19.560Z",
|
|
60
|
+
"outcome": "woken"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"kind": "wake",
|
|
64
|
+
"at": "2026-09-25T21:33:05.316Z",
|
|
65
|
+
"outcome": "between-tool-calls"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"kind": "projection",
|
|
69
|
+
"at": "2026-09-25T21:33:17.285Z",
|
|
70
|
+
"outcome": "body-shown"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"kind": "offer",
|
|
74
|
+
"at": "2026-09-25T21:33:17.207Z",
|
|
75
|
+
"outcome": "next-turn"
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"caseId": "C03",
|
|
81
|
+
"outcome": "passed",
|
|
82
|
+
"startedAt": "2026-09-25T21:31:56.000Z",
|
|
83
|
+
"finishedAt": "2026-09-25T21:32:44.370Z",
|
|
84
|
+
"messageId": "message_EaLKeCerBFIR_o0gQpT4vQ",
|
|
85
|
+
"observations": [
|
|
86
|
+
{
|
|
87
|
+
"kind": "answer",
|
|
88
|
+
"at": "2026-09-25T21:32:16.968Z",
|
|
89
|
+
"outcome": "recorded"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"kind": "receipt",
|
|
93
|
+
"at": "2026-09-25T21:32:16.968Z",
|
|
94
|
+
"outcome": "acknowledged"
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"caseId": "C04",
|
|
100
|
+
"outcome": "passed",
|
|
101
|
+
"startedAt": "2026-09-25T21:34:28.000Z",
|
|
102
|
+
"finishedAt": "2026-09-25T21:34:56.510Z",
|
|
103
|
+
"messageId": "message_zzo8wi26wFyq8hfGvAsWIw",
|
|
104
|
+
"observations": [
|
|
105
|
+
{
|
|
106
|
+
"kind": "logical-message",
|
|
107
|
+
"at": "2026-09-25T21:34:56.510Z",
|
|
108
|
+
"outcome": "same-message-id"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"kind": "wake",
|
|
112
|
+
"at": "2026-09-25T21:34:28.385Z",
|
|
113
|
+
"outcome": "delivered-once"
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"caseId": "C05",
|
|
119
|
+
"outcome": "passed",
|
|
120
|
+
"startedAt": "2026-09-25T21:35:44.000Z",
|
|
121
|
+
"finishedAt": "2026-09-25T21:35:56.792Z",
|
|
122
|
+
"messageId": "message_owEZDIuuC0KygynZk-b0Hg",
|
|
123
|
+
"observations": [
|
|
124
|
+
{
|
|
125
|
+
"kind": "delivery",
|
|
126
|
+
"at": "2026-09-25T21:35:56.792Z",
|
|
127
|
+
"outcome": "queued"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"kind": "client",
|
|
131
|
+
"outcome": "exited",
|
|
132
|
+
"at": "2026-09-25T21:35:47.000Z"
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"caseId": "C06",
|
|
138
|
+
"outcome": "passed",
|
|
139
|
+
"startedAt": "2026-09-25T21:35:05.000Z",
|
|
140
|
+
"finishedAt": "2026-09-25T21:35:34.783Z",
|
|
141
|
+
"messageId": "message_tDm0K7jQWFVKmVhs_t_29Q",
|
|
142
|
+
"observations": [
|
|
143
|
+
{
|
|
144
|
+
"kind": "delivery",
|
|
145
|
+
"at": "2026-09-25T21:35:34.783Z",
|
|
146
|
+
"outcome": "woken"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"kind": "wake",
|
|
150
|
+
"at": "2026-09-25T21:35:05.754Z",
|
|
151
|
+
"outcome": "started-turn"
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"kind": "other-session",
|
|
155
|
+
"at": "2026-09-25T21:35:34.783Z",
|
|
156
|
+
"outcome": "not-woken"
|
|
157
|
+
}
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"client": "claude-code",
|
|
3
|
+
"version": "2.1.282",
|
|
4
|
+
"platform": "darwin-arm64",
|
|
5
|
+
"observedAt": "2026-09-25T21:35:56.792Z",
|
|
6
|
+
"capability": "native_delivery",
|
|
7
|
+
"result": "pass",
|
|
8
|
+
"fixture": "claude-code-2.1.282",
|
|
9
|
+
"launchMode": "ordinary-command-with-installed-hooks",
|
|
10
|
+
"protocolContract": "claude-code-inbox-socket-v1",
|
|
11
|
+
"idle": "offered",
|
|
12
|
+
"busy": "presented_between_tool_calls",
|
|
13
|
+
"reply": "routed",
|
|
14
|
+
"duplicate": "same_message_id",
|
|
15
|
+
"fallback": "queued",
|
|
16
|
+
"limitations": [
|
|
17
|
+
"Observed on darwin-arm64 with Claude Code 2.1.282 in the TUI, model Sonnet 5, auto permission mode, through a private candidate built from this branch and installed with an isolated ACC data home; the plugin registration in the real Claude config directory was restored byte for byte afterwards.",
|
|
18
|
+
"Every wake carried ACC wording and the message id only. The body reached the model through the UserPromptSubmit projection that each delivered wake fired, inside the untrusted acc-peer-message block.",
|
|
19
|
+
"An idle session started a turn with no user input. A wake sent while a Bash tool call ran was taken after that call returned and before the next one, and the turn continued.",
|
|
20
|
+
"The observed reply loop uses the installed acc reply CLI; native delivery.replyRoute remains false. The router reports a wake as outcome woken via claude-inbox and leaves the receipt queued until the hook's output carried the body, which records it offered via next-turn.",
|
|
21
|
+
"A repeated logical send kept its message id, took the durable path because the receipt was already acknowledged, and produced exactly one wake.",
|
|
22
|
+
"Two sessions in one workspace each bound their own inbox; a message addressed to one never woke the other.",
|
|
23
|
+
"An exited session left no registry entry; the next message stayed queued as recipient_unavailable.",
|
|
24
|
+
"A receiving session in bypassPermissions mode was not captured; Claude Code documents that such a session holds each wake whose sender does not attest bypass, and ACC never attests a mode.",
|
|
25
|
+
"Linux and native Windows remain uncaptured."
|
|
26
|
+
],
|
|
27
|
+
"packageSha256": "fa81c55588988ed7274636c564a0adb5c442110f014f82e7a3f94da26fdd8891"
|
|
28
|
+
}
|
package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/inbox-wake-2.1.282.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"note": "Transport capture of the Claude Code session inbox, run on Claude Code 2.1.282, macOS arm64, 2026-09-25, before any ACC code used it. A receiving interactive TUI session ran from the real executable in auto mode with ACC 0.7.0 hooks installed and no Channel. A separate process, not a child of the receiver, wrote one JSON line per connection to the socket the session registry named, with no auth line and no token. Session ids, paths, pids and message text are redacted; outcomes are read from the receiver's own transcript records and ACC's store events.",
|
|
3
|
+
"clientVersion": "2.1.282",
|
|
4
|
+
"platform": "darwin-arm64",
|
|
5
|
+
"observedAt": "2026-09-25T19:40:41.000Z",
|
|
6
|
+
"registry": {
|
|
7
|
+
"file": "<config>/sessions/<pid>.json",
|
|
8
|
+
"fieldsRead": ["pid", "sessionId", "messagingSocketPath"],
|
|
9
|
+
"sessionIdEqualsHookSessionId": true,
|
|
10
|
+
"statusValuesSeen": ["idle", "busy", "waiting"]
|
|
11
|
+
},
|
|
12
|
+
"frame": {
|
|
13
|
+
"shape": { "type": "user", "message": { "role": "user", "content": "<text>" }, "msg_id": "<id>" },
|
|
14
|
+
"authLine": "absent",
|
|
15
|
+
"socketAnswer": "none"
|
|
16
|
+
},
|
|
17
|
+
"idle": {
|
|
18
|
+
"result": "new turn started without user input",
|
|
19
|
+
"transcriptRecord": "user, isMeta, origin.kind peer, verifiedPeerPid set",
|
|
20
|
+
"userPromptSubmitFired": true
|
|
21
|
+
},
|
|
22
|
+
"busy": {
|
|
23
|
+
"result": "queued while a Bash tool call ran; delivered after its tool_result, before the next tool call; the turn continued",
|
|
24
|
+
"transcriptRecord": "attachment queued_command, origin.kind peer",
|
|
25
|
+
"userPromptSubmitFired": true
|
|
26
|
+
},
|
|
27
|
+
"accWake": {
|
|
28
|
+
"frameCarried": "ACC wording and the message id only",
|
|
29
|
+
"idle": "the beforeTurn projection showed the queued ACC question in its untrusted block; the model answered with acc reply; the store recorded the answer and acknowledged",
|
|
30
|
+
"midTurn": "the same projection arrived with the UserPromptSubmit that the mid-turn frame fired; the model answered after its running commands",
|
|
31
|
+
"offerRecordedAs": "next-turn"
|
|
32
|
+
},
|
|
33
|
+
"inbound": {
|
|
34
|
+
"promptingReceiverUnattestedSender": "delivered",
|
|
35
|
+
"promptingReceiverSenderAttestsBypass": "held with an approval dialog; registry status waiting; deny dropped it",
|
|
36
|
+
"bypassReceiver": "unobserved"
|
|
37
|
+
},
|
|
38
|
+
"burst": {
|
|
39
|
+
"framesFromSeparateProcesses": 6,
|
|
40
|
+
"delivered": 6,
|
|
41
|
+
"queuedFramesDeliveredInOneTurn": 5,
|
|
42
|
+
"userPromptSubmitPerFrame": true
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agents-can-communicate/adapter-claude-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/adapter.mjs",
|
|
8
|
-
"./
|
|
8
|
+
"./inbox-delivery": "./src/inbox-delivery.mjs"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"certification.json",
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
"fixtures/delivery/claude-code-2.1.252.json",
|
|
19
19
|
"fixtures/delivery/claude-code-2.1.258.json",
|
|
20
20
|
"fixtures/delivery/claude-code-2.1.260.json",
|
|
21
|
+
"fixtures/delivery/claude-code-2.1.282.json",
|
|
22
|
+
"fixtures/delivery/claude-code-2.1.282-product-evidence.json",
|
|
23
|
+
"fixtures/inbox-wake-2.1.282.json",
|
|
21
24
|
"src/",
|
|
22
25
|
"plugin/"
|
|
23
26
|
]
|
|
@@ -2,18 +2,15 @@ import { defineAdapter, projectContext, projectContextResult }
|
|
|
2
2
|
from "@agents-can-communicate/adapter-sdk";
|
|
3
3
|
import certification from "../certification.json" with { type: "json" };
|
|
4
4
|
|
|
5
|
-
import { PROTOCOL_CONTRACT } from "./channel.mjs";
|
|
6
5
|
import { denyOutcome, injectOutcome, injectStartOwnerOutcome, normalizeClaudeHook } from "./hooks.mjs";
|
|
6
|
+
import { MIN_VERSION, PROTOCOL_CONTRACT, bindNativeSession, offerMessage, planNativeActivation,
|
|
7
|
+
probeNativeDelivery, refreshNativeSession, retireNativeSession } from "./inbox-delivery.mjs";
|
|
7
8
|
import { planClaudeInstall, detectClaude, installClaudePlugin, uninstallClaudePlugin } from "./install.mjs";
|
|
8
|
-
import { bindNativeSession, offerMessage, planNativeActivation, probeNativeDelivery, routeReply }
|
|
9
|
-
from "./native-delivery.mjs";
|
|
10
9
|
|
|
11
10
|
export const CLAUDE_CODE_VERSION = "2.1.233";
|
|
12
|
-
export const CLAUDE_CHANNEL_MINIMUM = "2.1.258";
|
|
13
11
|
export const CLAUDE_DELIVERY_FALLBACK = Object.freeze({
|
|
14
|
-
diagnostic:
|
|
15
|
-
+ "
|
|
16
|
-
+ "is enabled. Messages remain durable for certified next-turn delivery or acc inbox",
|
|
12
|
+
diagnostic: `Claude Code live delivery wakes a session through its inbox socket from ${MIN_VERSION}; `
|
|
13
|
+
+ "fallback: next-turn hooks or acc inbox",
|
|
17
14
|
});
|
|
18
15
|
|
|
19
16
|
/**
|
|
@@ -25,7 +22,9 @@ export const CLAUDE_DELIVERY_FALLBACK = Object.freeze({
|
|
|
25
22
|
* and a parent/child mapping claimed without observation is the kind of thing
|
|
26
23
|
* that quietly maps every child onto its parent. `startupInjection` and
|
|
27
24
|
* `safePointInjection` were not exercised - only the before-turn path was.
|
|
28
|
-
* Native live delivery
|
|
25
|
+
* Native live delivery is the inbox wake, certified from 2.1.282 (captured on
|
|
26
|
+
* darwin-arm64, applied on every platform the probe admits); reply routing
|
|
27
|
+
* through the transport is not claimed.
|
|
29
28
|
*/
|
|
30
29
|
export function createClaudeCodeAdapter() {
|
|
31
30
|
return defineAdapter({
|
|
@@ -44,28 +43,22 @@ export function createClaudeCodeAdapter() {
|
|
|
44
43
|
context: { beforeTurnInjection: true },
|
|
45
44
|
// PreToolUse denied both a Write and a Bash call; neither ran.
|
|
46
45
|
guards: { beforeWrite: true, beforeShell: true },
|
|
47
|
-
// nextTurn is the certified 2.1.233 hook projection
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
delivery: { nextTurn: true, livePush: true, replyRoute: true },
|
|
46
|
+
// nextTurn is the certified 2.1.233 hook projection. livePush rests on
|
|
47
|
+
// the 2.1.282 product capture of the inbox wake; the reply is the
|
|
48
|
+
// ordinary `acc reply` every adapter has, so replyRoute stays false.
|
|
49
|
+
delivery: { nextTurn: true, livePush: true },
|
|
52
50
|
},
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
// not a second anchor: an anchor is the minimum's proof, one per platform,
|
|
58
|
-
// and this contract has no maximum precisely so a newer stable client is
|
|
59
|
-
// admitted by probe and a generation-bound handshake instead of by another
|
|
60
|
-
// anchor. That capture is what exercised the rule - every branch observed
|
|
61
|
-
// on 2.1.260 while the minimum stayed here. Two minor versions of drift,
|
|
62
|
-
// served without a capture at either of them, is the rule working.
|
|
51
|
+
|
|
52
|
+
// The first passing capture is the minimum, and there is no maximum: a
|
|
53
|
+
// newer stable client is admitted by the probe and the per-session
|
|
54
|
+
// handshake. The offer is a wake; see src/inbox-delivery.mjs.
|
|
63
55
|
nativeDelivery: {
|
|
64
|
-
|
|
65
|
-
anchors: [{
|
|
66
|
-
protocolContract: PROTOCOL_CONTRACT }],
|
|
56
|
+
minimum: MIN_VERSION,
|
|
57
|
+
anchors: [{ version: MIN_VERSION, protocolContract: PROTOCOL_CONTRACT }],
|
|
67
58
|
knownBad: [],
|
|
68
|
-
activationKinds: ["
|
|
59
|
+
activationKinds: ["native-service"],
|
|
60
|
+
policySource: "installation-record",
|
|
61
|
+
offerKind: "wake",
|
|
69
62
|
},
|
|
70
63
|
|
|
71
64
|
startSession: async () => ({ ok: true, changes: [], diagnostics: [] }),
|
|
@@ -75,8 +68,7 @@ export function createClaudeCodeAdapter() {
|
|
|
75
68
|
|
|
76
69
|
planInstall: context => planClaudeInstall(context),
|
|
77
70
|
detect: context => detectClaude(context),
|
|
78
|
-
install: context => installClaudePlugin(
|
|
79
|
-
livePolicy: context.livePolicy ?? "off" }),
|
|
71
|
+
install: context => installClaudePlugin(context),
|
|
80
72
|
uninstall: context => uninstallClaudePlugin(context),
|
|
81
73
|
|
|
82
74
|
doctor: async context => {
|
|
@@ -85,6 +77,9 @@ export function createClaudeCodeAdapter() {
|
|
|
85
77
|
...detected.diagnostics,
|
|
86
78
|
"hook payloads captured from Claude Code 2.1.233",
|
|
87
79
|
CLAUDE_DELIVERY_FALLBACK.diagnostic,
|
|
80
|
+
// Claude Code's own inbound control decides whether a wake reaches the
|
|
81
|
+
// model; ACC never attests a permission mode. Said only when it matters.
|
|
82
|
+
...(detected.inboundDelivery.diagnostic === null ? [] : [detected.inboundDelivery.diagnostic]),
|
|
88
83
|
// SessionEnd is advisory and cannot summarise a conversation that has
|
|
89
84
|
// already stopped, so the handoff is written from Stop or the skill.
|
|
90
85
|
"handoff is written while the model is active, not at SessionEnd",
|
|
@@ -94,8 +89,9 @@ export function createClaudeCodeAdapter() {
|
|
|
94
89
|
probeNativeDelivery,
|
|
95
90
|
planNativeActivation,
|
|
96
91
|
bindNativeSession,
|
|
92
|
+
refreshNativeSession,
|
|
93
|
+
retireNativeSession,
|
|
97
94
|
offerMessage,
|
|
98
|
-
routeReply,
|
|
99
95
|
|
|
100
96
|
denyOutcome,
|
|
101
97
|
injectOutcome,
|
|
@@ -50,6 +50,12 @@ export function normalizeClaudeHook(payload) {
|
|
|
50
50
|
parentSessionId: typeof payload.agent_id === "string" ? payload.session_id : null,
|
|
51
51
|
tool,
|
|
52
52
|
targets: writeTargets(tool, payload.tool_input),
|
|
53
|
+
// Documented as conditional; SessionStart carried none on 2.1.233.
|
|
54
|
+
permissionMode: typeof payload.permission_mode === "string" && payload.permission_mode !== ""
|
|
55
|
+
? payload.permission_mode : null,
|
|
56
|
+
// SessionEnd says why: "clear", "logout", "prompt_input_exit", "other".
|
|
57
|
+
endReason: event === "SessionEnd" && typeof payload.reason === "string" && payload.reason !== ""
|
|
58
|
+
? payload.reason : null,
|
|
53
59
|
});
|
|
54
60
|
}
|
|
55
61
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The facts of the inbox transport every module of it shares, kept apart so
|
|
2
|
+
// the endpoint record and the delivery methods import them without a cycle.
|
|
3
|
+
|
|
4
|
+
export const PROTOCOL_CONTRACT = "claude-code-inbox-socket-v1";
|
|
5
|
+
// The first release the inbox wake was captured on. 2.1.224 binds the socket,
|
|
6
|
+
// but only 2.1.282 was observed firing UserPromptSubmit for a delivered frame,
|
|
7
|
+
// and that is what brings the message body in with the wake.
|
|
8
|
+
export const MIN_VERSION = "2.1.282";
|
|
9
|
+
export const INBOX_MODES = Object.freeze(["livePush", "idleWake", "busyQueue"]);
|
|
10
|
+
export const TRANSPORT = "claude-inbox";
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import { compareVersionOrder, versionOrder } from "@agents-can-communicate/adapter-sdk";
|
|
3
|
+
|
|
4
|
+
import { INBOX_MODES, MIN_VERSION, PROTOCOL_CONTRACT, TRANSPORT } from "./inbox-contract.mjs";
|
|
5
|
+
import { claimWake, newEndpointId, readInboxEndpoint, releaseWake, removeInboxEndpoint,
|
|
6
|
+
sweepDeadEndpoints, writeInboxEndpoint } from "./inbox-endpoint.mjs";
|
|
7
|
+
import { claudeConfigDir, readSessionRecord, verifyInbox } from "./inbox-registry.mjs";
|
|
8
|
+
import { MANAGED_SETTINGS, permissionModeFromArgs, readInboundSettings, readProcessArgs, receptionOf }
|
|
9
|
+
from "./inbox-settings.mjs";
|
|
10
|
+
|
|
11
|
+
// Live delivery into a Claude Code session through the inbox socket the
|
|
12
|
+
// session itself binds (2.1.224 and later, no flag, every provider).
|
|
13
|
+
//
|
|
14
|
+
// The offer is a wake, never the message. Measured on 2.1.282: every frame a
|
|
15
|
+
// session accepts on its inbox fires UserPromptSubmit, idle or between two tool
|
|
16
|
+
// calls, and ACC's beforeTurn hook then projects the queued message inside its
|
|
17
|
+
// untrusted block, with receipts and the `acc reply` route. So the frame
|
|
18
|
+
// carries fixed ACC wording and the ACC message id, and no byte a peer wrote:
|
|
19
|
+
// Claude Code frames every inbox message as a teammate's request to act on,
|
|
20
|
+
// and the only thing a wake gives it to act on is ACC's own notice.
|
|
21
|
+
//
|
|
22
|
+
// The connection sends no auth line, and this module never reads the
|
|
23
|
+
// session's messaging token or key file. A frame carrying that token would
|
|
24
|
+
// pass as the session's own child and skip the inbound controls its user set.
|
|
25
|
+
|
|
26
|
+
const LEASE_MS = 120_000;
|
|
27
|
+
const MESSAGE_ID = /^[A-Za-z0-9_-]{1,200}$/;
|
|
28
|
+
const PROBE_NEEDLE = Buffer.from("messagingSocketPath");
|
|
29
|
+
const PROBE_MAX_BYTES = 256 * 1024 * 1024;
|
|
30
|
+
|
|
31
|
+
export { INBOX_MODES, MIN_VERSION, PROTOCOL_CONTRACT, TRANSPORT };
|
|
32
|
+
|
|
33
|
+
// Claude Code shows the wake as a message from another Claude session, so the
|
|
34
|
+
// text names the ACC route: a model otherwise tries SendMessage first.
|
|
35
|
+
export const wakeText = messageId => `ACC: new peer message ${messageId} for this session. `
|
|
36
|
+
+ "This turn's ACC context shows it. If it does not, it was already shown, or read it with "
|
|
37
|
+
+ `acc inbox --message ${messageId}. Answer it through ACC with acc reply --message ${messageId}; `
|
|
38
|
+
+ "SendMessage cannot deliver to an ACC participant.";
|
|
39
|
+
|
|
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;
|
|
45
|
+
|
|
46
|
+
// The registry field only a build with the inbox writes. Read-only, bounded.
|
|
47
|
+
async function executableHasInbox(realExecutable) {
|
|
48
|
+
let handle;
|
|
49
|
+
try {
|
|
50
|
+
handle = await (await import("node:fs/promises")).open(realExecutable, "r");
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const chunk = Buffer.alloc(1024 * 1024);
|
|
56
|
+
let carry = Buffer.alloc(0);
|
|
57
|
+
let position = 0;
|
|
58
|
+
for (;;) {
|
|
59
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, position);
|
|
60
|
+
if (bytesRead === 0) return false;
|
|
61
|
+
const window = Buffer.concat([carry, chunk.subarray(0, bytesRead)]);
|
|
62
|
+
if (window.includes(PROBE_NEEDLE)) return true;
|
|
63
|
+
carry = window.subarray(Math.max(0, window.length - PROBE_NEEDLE.length));
|
|
64
|
+
position += bytesRead;
|
|
65
|
+
if (position >= PROBE_MAX_BYTES) return false;
|
|
66
|
+
}
|
|
67
|
+
} finally {
|
|
68
|
+
await handle.close().catch(() => null);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function defaultReadVersion(realExecutable, timeoutMs) {
|
|
73
|
+
return new Promise(resolve => {
|
|
74
|
+
import("node:child_process").then(({ execFile }) => {
|
|
75
|
+
execFile(realExecutable, ["--version"], { timeout: timeoutMs, windowsHide: true },
|
|
76
|
+
(error, stdout, stderr) => {
|
|
77
|
+
if (error !== null) return resolve(null);
|
|
78
|
+
resolve(/(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)/.exec(`${stdout}${stderr}`)?.[1] ?? null);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function withTimeout(work, ms) {
|
|
85
|
+
let timer = null;
|
|
86
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
87
|
+
timer = setTimeout(() => reject(Object.assign(new Error("native probe timed out"),
|
|
88
|
+
{ code: "ETIMEDOUT" })), ms);
|
|
89
|
+
});
|
|
90
|
+
return Promise.race([work, deadline]).finally(() => clearTimeout(timer));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
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
|
|
96
|
+
* registry. Never launches a session. Native Windows serves a named pipe that
|
|
97
|
+
* demands an auth line; nothing there is captured, so it keeps hook delivery.
|
|
98
|
+
*/
|
|
99
|
+
export async function probeNativeDelivery({ realExecutable, timeoutMs = 750, platform = process.platform,
|
|
100
|
+
hasInbox = executableHasInbox, readVersion = defaultReadVersion } = {}) {
|
|
101
|
+
const unsupported = (reasonCode, clientVersion = null) => ({ supported: false, clientVersion,
|
|
102
|
+
protocolContract: PROTOCOL_CONTRACT, executableFingerprint: null, modes: [], reasonCode });
|
|
103
|
+
if (platform === "win32") return unsupported("native_delivery_unsupported");
|
|
104
|
+
if (typeof realExecutable !== "string" || realExecutable === "") return unsupported("feature_probe_failed");
|
|
105
|
+
const clientVersion = await withTimeout(Promise.resolve(readVersion(realExecutable, timeoutMs)), timeoutMs)
|
|
106
|
+
.catch(() => null);
|
|
107
|
+
if (versionOrder(clientVersion) === null) return unsupported("feature_probe_failed", clientVersion);
|
|
108
|
+
if (belowMinimum(clientVersion)) return unsupported("below_minimum_version", clientVersion);
|
|
109
|
+
const present = await withTimeout(Promise.resolve(hasInbox(realExecutable)), timeoutMs).catch(() => false);
|
|
110
|
+
if (!present) return unsupported("protocol_mismatch", clientVersion);
|
|
111
|
+
return { supported: true, clientVersion, protocolContract: PROTOCOL_CONTRACT,
|
|
112
|
+
executableFingerprint: null, modes: [...INBOX_MODES], reasonCode: null };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Claude Code runs the inbox itself. ACC starts nothing and rewrites no argument.
|
|
116
|
+
export function planNativeActivation({ detection }) {
|
|
117
|
+
if (typeof detection?.realExecutable !== "string" || detection.realExecutable === "") {
|
|
118
|
+
return { eligible: false, reasonCode: "feature_probe_failed", mechanisms: [] };
|
|
119
|
+
}
|
|
120
|
+
return { eligible: true, reasonCode: null, mechanisms: [{ kind: "native-service",
|
|
121
|
+
serviceId: "claude-code-inbox", preExisting: true, applyCommand: null, teardownCommand: null }] };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const closed = (clientVersion, reasonCode) => ({ supported: false, clientVersion: clientVersion ?? null,
|
|
125
|
+
protocolContract: PROTOCOL_CONTRACT, modes: [], opaqueEndpointRef: null, leaseUntil: null, reasonCode });
|
|
126
|
+
const handshake = (endpoint, now) => ({ supported: true, clientVersion: endpoint.clientVersion,
|
|
127
|
+
protocolContract: PROTOCOL_CONTRACT, modes: [...INBOX_MODES], opaqueEndpointRef: endpoint.endpointId,
|
|
128
|
+
leaseUntil: new Date(now() + LEASE_MS).toISOString(), reasonCode: null });
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Bind this ACC session to the inbox of the Claude process whose hook is
|
|
132
|
+
* running. The socket comes from the hook's own environment, which Claude Code
|
|
133
|
+
* exports per session and never inherits from a parent; the registry must
|
|
134
|
+
* name this process and this socket before anything is published. The session
|
|
135
|
+
* id is checked by every offer and refresh, since after /resume the registry
|
|
136
|
+
* catches up only once the hook has run.
|
|
137
|
+
*/
|
|
138
|
+
export async function bindNativeSession({ event, clientPid, clientVersion, runtimeDir,
|
|
139
|
+
env = process.env, now = Date.now, managedSettingsPath = MANAGED_SETTINGS[process.platform],
|
|
140
|
+
readClientArgs = readProcessArgs } = {}) {
|
|
141
|
+
if (!Number.isInteger(clientPid) || clientPid <= 0) return closed(clientVersion, "client_process_unknown");
|
|
142
|
+
if (typeof event?.sessionId !== "string" || event.sessionId === "") {
|
|
143
|
+
return closed(clientVersion, "handshake_failed");
|
|
144
|
+
}
|
|
145
|
+
if (versionOrder(clientVersion) === null) return closed(clientVersion, "version_unavailable");
|
|
146
|
+
// The contract refuses it anyway, after this returns. Refusing here keeps an
|
|
147
|
+
// older client - which binds again on every turn - from writing an endpoint
|
|
148
|
+
// record each time.
|
|
149
|
+
if (belowMinimum(clientVersion)) return closed(clientVersion, "below_minimum_version");
|
|
150
|
+
const socketPath = env?.CLAUDE_CODE_MESSAGING_SOCKET;
|
|
151
|
+
if (typeof socketPath !== "string" || socketPath === "") {
|
|
152
|
+
return closed(clientVersion, "native_endpoint_unavailable");
|
|
153
|
+
}
|
|
154
|
+
await sweepDeadEndpoints({ runtimeDir });
|
|
155
|
+
const configDir = claudeConfigDir(env);
|
|
156
|
+
const refused = await verifyInbox({ configDir, clientPid, sessionId: event.sessionId, socketPath,
|
|
157
|
+
anyConversation: true });
|
|
158
|
+
if (refused !== null) return closed(clientVersion, refused);
|
|
159
|
+
// Project settings sit where the session was started, which the registry
|
|
160
|
+
// records; the hook's cwd follows the shell.
|
|
161
|
+
const record = await readSessionRecord({ configDir, clientPid });
|
|
162
|
+
const settings = await readInboundSettings({ configDir, projectDir: record?.cwd ?? event.cwd,
|
|
163
|
+
managedSettingsPath });
|
|
164
|
+
// The hook's mode is current. SessionStart carries none, so until the first
|
|
165
|
+
// prompt the launch flag stands in, then the configured default.
|
|
166
|
+
const launchMode = event.permissionMode == null
|
|
167
|
+
? permissionModeFromArgs(await readClientArgs(clientPid).catch(() => null)) : null;
|
|
168
|
+
const reception = receptionOf({
|
|
169
|
+
permissionMode: event.permissionMode ?? launchMode ?? settings.defaultMode,
|
|
170
|
+
crossSessionInbound: settings.crossSessionInbound });
|
|
171
|
+
const endpoint = { schemaVersion: 1, endpointId: newEndpointId(), socketPath, configDir, clientPid,
|
|
172
|
+
sessionId: event.sessionId, clientVersion, protocolContract: PROTOCOL_CONTRACT,
|
|
173
|
+
leaseUntil: new Date(now() + LEASE_MS).toISOString(), reception };
|
|
174
|
+
try {
|
|
175
|
+
await writeInboxEndpoint({ runtimeDir, record: endpoint });
|
|
176
|
+
} catch {
|
|
177
|
+
return closed(clientVersion, "handshake_failed");
|
|
178
|
+
}
|
|
179
|
+
return handshake(endpoint, now);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function verifiedEndpoint(binding, runtimeDir) {
|
|
183
|
+
const endpoint = await readInboxEndpoint({ runtimeDir, endpointId: binding?.opaqueEndpointRef });
|
|
184
|
+
if (endpoint === null) return null;
|
|
185
|
+
const refused = await verifyInbox({ configDir: endpoint.configDir, clientPid: endpoint.clientPid,
|
|
186
|
+
sessionId: endpoint.sessionId, socketPath: endpoint.socketPath });
|
|
187
|
+
return refused === null ? endpoint : null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// The router calls this when a lease ran out, so a session that sits idle
|
|
191
|
+
// between turns stays reachable. The id never changes on a refresh.
|
|
192
|
+
export async function refreshNativeSession({ binding, runtimeDir, now = Date.now } = {}) {
|
|
193
|
+
const endpoint = await verifiedEndpoint(binding, runtimeDir);
|
|
194
|
+
return endpoint === null ? closed(binding?.clientVersion, "handshake_failed") : handshake(endpoint, now);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export const retireNativeSession = ({ binding, runtimeDir }) =>
|
|
198
|
+
removeInboxEndpoint({ runtimeDir, endpointId: binding?.opaqueEndpointRef });
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Sender side: re-verify the receiver, then write one wake line. The socket
|
|
202
|
+
* answers nothing, so acceptance is a line written to a verified inbox without
|
|
203
|
+
* an error inside the timeout. What the model then sees is recorded by the
|
|
204
|
+
* receiver's own hook.
|
|
205
|
+
*/
|
|
206
|
+
export async function offerMessage({ binding, message, runtimeDir, timeoutMs = 2_000,
|
|
207
|
+
connect = net.createConnection } = {}) {
|
|
208
|
+
const rejected = safeErrorCode => ({ accepted: false, transport: TRANSPORT,
|
|
209
|
+
clientVersion: binding?.clientVersion ?? null, safeErrorCode });
|
|
210
|
+
if (typeof message?.messageId !== "string" || !MESSAGE_ID.test(message.messageId)) {
|
|
211
|
+
return rejected("transport_rejected");
|
|
212
|
+
}
|
|
213
|
+
const endpoint = await verifiedEndpoint(binding, runtimeDir);
|
|
214
|
+
if (endpoint === null) return rejected("recipient_unavailable");
|
|
215
|
+
// The receiver's own crossSessionInbound refuses unattested wakes; one would
|
|
216
|
+
// be dropped, so none is sent and the message waits for its next turn.
|
|
217
|
+
if (endpoint.reception === "refused") return rejected("delivery_disabled");
|
|
218
|
+
const accepted = { accepted: true, transport: TRANSPORT, clientVersion: endpoint.clientVersion,
|
|
219
|
+
...(endpoint.reception === "held" ? { pendingApproval: true } : {}) };
|
|
220
|
+
const wake = { runtimeDir, endpointId: endpoint.endpointId, messageId: message.messageId };
|
|
221
|
+
// Claude Code delivers a repeated msg_id again, so the dedupe is ours.
|
|
222
|
+
let first;
|
|
223
|
+
try {
|
|
224
|
+
first = await claimWake(wake);
|
|
225
|
+
} catch {
|
|
226
|
+
return rejected("transport_error");
|
|
227
|
+
}
|
|
228
|
+
if (!first) return accepted;
|
|
229
|
+
const frame = `${JSON.stringify({ type: "user",
|
|
230
|
+
message: { role: "user", content: wakeText(message.messageId) },
|
|
231
|
+
msg_id: `acc-wake-${message.messageId}` })}\n`;
|
|
232
|
+
const result = await new Promise(resolve => {
|
|
233
|
+
const socket = connect(endpoint.socketPath);
|
|
234
|
+
let settled = false;
|
|
235
|
+
const finish = value => {
|
|
236
|
+
if (settled) return;
|
|
237
|
+
settled = true;
|
|
238
|
+
clearTimeout(timer);
|
|
239
|
+
socket.destroy();
|
|
240
|
+
resolve(value);
|
|
241
|
+
};
|
|
242
|
+
const timer = setTimeout(() => finish(rejected("transport_error")), timeoutMs);
|
|
243
|
+
socket.once("error", error => finish(rejected(["EPERM", "EACCES"].includes(error?.code)
|
|
244
|
+
? "transport_permission_denied" : "recipient_unavailable")));
|
|
245
|
+
socket.once("connect", () => socket.end(frame, () => finish(accepted)));
|
|
246
|
+
});
|
|
247
|
+
if (!result.accepted) await releaseWake(wake);
|
|
248
|
+
return result;
|
|
249
|
+
}
|