agents-can-communicate 0.5.9 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +2 -1
  2. package/bin/acc-antigravity-relay.mjs +7 -0
  3. package/bin/entrypoints/acc-antigravity-relay.mjs +116 -0
  4. package/bin/entrypoints/acc-bootstrap.mjs +4 -0
  5. package/bin/entrypoints/acc-hook.mjs +11 -7
  6. package/bin/entrypoints/antigravity-relay-binding.mjs +43 -0
  7. package/docs/ADAPTER_AUTHORING.md +33 -0
  8. package/docs/ARCHITECTURE.md +27 -4
  9. package/docs/CAPABILITIES.md +22 -16
  10. package/docs/CLI.md +21 -3
  11. package/docs/CONCEPTS.md +7 -0
  12. package/docs/CONFIGURATION.md +1 -0
  13. package/docs/GETTING_STARTED.md +10 -4
  14. package/docs/HOW_IT_WORKS.md +7 -1
  15. package/docs/TROUBLESHOOTING.md +99 -0
  16. package/node_modules/@agents-can-communicate/adapter-antigravity/certification.json +91 -0
  17. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/PreInvocation-1.2.7.json +11 -0
  18. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/SessionStart-1.2.7.json +9 -0
  19. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/agentapi-error-answers-1.2.7.json +19 -0
  20. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/agentapi-live-push-1.2.7.json +31 -0
  21. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/certification-provenance.json +217 -0
  22. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/delivery/antigravity-cli-1.2.7-relay-product-evidence.json +132 -0
  23. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/delivery/antigravity-cli-1.2.7-relay-product.json +29 -0
  24. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/hooks-first-trust-no-workspace-1.2.7.json +37 -0
  25. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/live-push-surfaces-1.2.7.json +38 -0
  26. package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +28 -0
  27. package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +5 -0
  28. package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/skills/acc/SKILL.md +329 -0
  29. package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +119 -0
  30. package/node_modules/@agents-can-communicate/adapter-antigravity/src/agentapi.mjs +98 -0
  31. package/node_modules/@agents-can-communicate/adapter-antigravity/src/hooks.mjs +201 -0
  32. package/node_modules/@agents-can-communicate/adapter-antigravity/src/install.mjs +708 -0
  33. package/node_modules/@agents-can-communicate/adapter-antigravity/src/native-delivery.mjs +170 -0
  34. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-endpoint.mjs +116 -0
  35. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-start.mjs +123 -0
  36. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay.mjs +195 -0
  37. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relays.mjs +64 -0
  38. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
  39. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +14 -2
  40. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +2 -1
  41. package/node_modules/@agents-can-communicate/adapter-claude-code/src/hooks.mjs +5 -0
  42. package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
  43. package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +14 -2
  44. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  45. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +14 -2
  46. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  47. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  48. package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +14 -2
  49. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  50. package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +14 -2
  51. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  52. package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +37 -1
  53. package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +25 -5
  54. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +1 -0
  55. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -1
  56. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-attempt.mjs +1 -1
  57. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  58. package/node_modules/@agents-can-communicate/cli/src/args.mjs +6 -2
  59. package/node_modules/@agents-can-communicate/cli/src/help.mjs +1 -1
  60. package/node_modules/@agents-can-communicate/cli/src/hook-workspace.mjs +118 -0
  61. package/node_modules/@agents-can-communicate/cli/src/index.mjs +1 -0
  62. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +17 -3
  63. package/node_modules/@agents-can-communicate/cli/src/main.mjs +8 -4
  64. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/command-prefix.mjs +26 -0
  65. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +6 -4
  66. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +3 -1
  67. package/node_modules/@agents-can-communicate/cli/src/session-owner.mjs +27 -6
  68. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  69. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  70. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  71. package/node_modules/@agents-can-communicate/hook-runner/src/native-attempt.mjs +31 -0
  72. package/node_modules/@agents-can-communicate/hook-runner/src/owner-context.mjs +16 -3
  73. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +70 -17
  74. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  75. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +4 -0
  76. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +10 -0
  77. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  78. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  79. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  80. package/package.json +3 -1
@@ -0,0 +1,98 @@
1
+ import { execFile } from "node:child_process";
2
+ import { accessSync, constants, statSync } from "node:fs";
3
+ import path from "node:path";
4
+
5
+ /**
6
+ * The one way ACC talks to a running Antigravity session: `agy agentapi`, run as
7
+ * a child whose environment - and only whose environment - carries the session
8
+ * endpoint. argv is readable by every process on the machine, so the token is
9
+ * never an argument; the environment of a short-lived child is the exposure
10
+ * every command the agent itself runs already has.
11
+ *
12
+ * Answer shapes are the ones captured on 1.2.7: a delivered message comes back
13
+ * as `response.sendMessage.recipientId`; a gone session as
14
+ * `code = Unavailable`; a refused credential as `code = Unauthenticated`.
15
+ */
16
+ export const ENDPOINT_VARIABLES = Object.freeze(["ANTIGRAVITY_LS_ADDRESS",
17
+ "ANTIGRAVITY_CSRF_TOKEN", "ANTIGRAVITY_CONVERSATION_ID"]);
18
+ const TITLE = "ACC peer message";
19
+
20
+ export function endpointFrom(env) {
21
+ const [lsAddress, csrfToken, conversationId] = ENDPOINT_VARIABLES.map(name => env?.[name]);
22
+ return [lsAddress, csrfToken, conversationId].every(value => typeof value === "string"
23
+ && value !== "") ? { lsAddress, csrfToken, conversationId } : null;
24
+ }
25
+
26
+ export function childEnv(baseEnv, endpoint) {
27
+ const env = { ...baseEnv };
28
+ for (const name of ENDPOINT_VARIABLES) delete env[name];
29
+ return { ...env, ANTIGRAVITY_LS_ADDRESS: endpoint.lsAddress,
30
+ ANTIGRAVITY_CSRF_TOKEN: endpoint.csrfToken,
31
+ ANTIGRAVITY_CONVERSATION_ID: endpoint.conversationId };
32
+ }
33
+
34
+ export function classifyAnswer(stdout, conversationId) {
35
+ let answer;
36
+ try {
37
+ answer = JSON.parse(stdout);
38
+ } catch {
39
+ return { ok: false, reasonCode: "transport_error" };
40
+ }
41
+ const sent = answer?.response?.sendMessage;
42
+ if (sent !== undefined) {
43
+ return sent.recipientId === conversationId ? { ok: true, reasonCode: null }
44
+ : { ok: false, reasonCode: "transport_rejected" };
45
+ }
46
+ if (answer?.response?.conversationMetadata !== undefined) return { ok: true, reasonCode: null };
47
+ const error = typeof answer?.error === "string" ? answer.error : "";
48
+ if (/code = Unavailable/.test(error)) return { ok: false, reasonCode: "recipient_unavailable" };
49
+ if (/code = (Unauthenticated|PermissionDenied)/.test(error)) {
50
+ return { ok: false, reasonCode: "transport_rejected" };
51
+ }
52
+ return { ok: false, reasonCode: "transport_error" };
53
+ }
54
+
55
+ export function run(command, args, { env, timeout }) {
56
+ return new Promise(resolve => {
57
+ execFile(command, args, { env, timeout, windowsHide: true, maxBuffer: 1024 * 1024 },
58
+ (_error, stdout) => resolve({ stdout: String(stdout ?? "") }));
59
+ });
60
+ }
61
+
62
+ export function createAgentApi({ endpoint, baseEnv = process.env, run: runner = run,
63
+ command = "agy", timeoutMs = 4_000 }) {
64
+ const call = async args => {
65
+ try {
66
+ const { stdout } = await runner(command, ["agentapi", ...args],
67
+ { env: childEnv(baseEnv, endpoint), timeout: timeoutMs });
68
+ return classifyAnswer(stdout, endpoint.conversationId);
69
+ } catch {
70
+ return { ok: false, reasonCode: "transport_error" };
71
+ }
72
+ };
73
+ return {
74
+ sendMessage: text => call(["send-message", "--title", TITLE, endpoint.conversationId, text]),
75
+ conversationMetadata: () => call(["get-conversation-metadata", endpoint.conversationId]),
76
+ };
77
+ }
78
+
79
+ /**
80
+ * The agy the relay runs. The agent's shell names the client's own binary in
81
+ * ANTIGRAVITY_AGENTAPI_EXE - captured as the same file as the agy on its PATH -
82
+ * and preferring it keeps a push working for a client started by a full path
83
+ * that is not on the PATH. Only an absolute, executable file named agy is
84
+ * taken, because the relay calls it as `agy agentapi ...`; anything else falls
85
+ * back to the PATH.
86
+ */
87
+ export function agentApiCommand(env) {
88
+ const named = env?.ANTIGRAVITY_AGENTAPI_EXE;
89
+ if (typeof named !== "string" || !path.isAbsolute(named) || path.basename(named) !== "agy") {
90
+ return "agy";
91
+ }
92
+ try {
93
+ accessSync(named, constants.X_OK);
94
+ return statSync(named).isFile() ? named : "agy";
95
+ } catch {
96
+ return "agy";
97
+ }
98
+ }
@@ -0,0 +1,201 @@
1
+ import { normalizedEvent } from "@agents-can-communicate/adapter-sdk";
2
+ import { AccError, EXIT } from "@agents-can-communicate/protocol";
3
+
4
+ /**
5
+ * The events that load on this client. Captured on 1.2.7 by registering each
6
+ * one alone and then together, and reading the effective list back from
7
+ * `agy -p "/hooks" --output-format json`.
8
+ *
9
+ * The list is short because most of what an adapter would want is not here.
10
+ * There is no tool event, so no guard; and no session end, so no lifecycle
11
+ * deregistration. See COMPATIBILITY.md.
12
+ */
13
+ export const ANTIGRAVITY_HOOK_EVENTS = Object.freeze(["SessionStart", "PreInvocation",
14
+ "PostInvocation", "Stop"]);
15
+
16
+ /**
17
+ * Names this client accepts into a config file and then silently drops.
18
+ *
19
+ * Nothing is logged, no error is produced, and `/hooks` reports an empty list.
20
+ * That is the single most important failure mode here, so the names are kept
21
+ * rather than merely absent: an install that writes one of them succeeds on
22
+ * disk and registers nothing, and the only defence is to refuse the name in
23
+ * both directions - never write it, and never normalise it if it somehow
24
+ * arrives.
25
+ *
26
+ * `SessionEnd`, `PreToolUse`, `PostToolUse`, `Notification` and
27
+ * `turn-completion` were each registered and never fired; `turn-completion`
28
+ * does not appear in the 1.2.7 binary at all. The last four are the Gemini CLI
29
+ * names, which matter because both clients share `~/.gemini`.
30
+ */
31
+ export const ANTIGRAVITY_INERT_EVENTS = Object.freeze(["SessionEnd", "PreToolUse",
32
+ "PostToolUse", "Notification", "turn-completion",
33
+ "BeforeAgent", "AfterAgent", "BeforeTool", "AfterTool"]);
34
+
35
+ /**
36
+ * How many times this adapter will continue one turn.
37
+ *
38
+ * One. The vendor caps consecutive continuations itself - changelog 1.1.9 fixed
39
+ * "stop hooks that always block hanging the agent forever" - but the cap is
40
+ * configurable and its value was not captured, so relying on it would be
41
+ * relying on a number nobody here has read. One is also the whole of what was
42
+ * observed: `executionNum` 0 continued into `executionNum` 1, and that pair is
43
+ * the evidence this ceiling rests on.
44
+ *
45
+ * A continuation is a nudge, not a gate. Nothing about it guarantees a peer is
46
+ * answered, and the sender-facing story must not say otherwise.
47
+ */
48
+ export const STOP_CONTINUATION_CEILING = 1;
49
+
50
+ const KIND_BY_EVENT = Object.freeze({
51
+ SessionStart: "sessionStart",
52
+ PreInvocation: "beforeTurn",
53
+ // Recorded rather than used. It fires after each invocation with an envelope
54
+ // byte-identical to PreInvocation's, and there is nothing ACC does at that
55
+ // point that PreInvocation has not already done.
56
+ PostInvocation: "other",
57
+ Stop: "turnEnd",
58
+ });
59
+
60
+ const data = (message, details) => {
61
+ throw new AccError(EXIT.DATA, message, details);
62
+ };
63
+
64
+ /**
65
+ * Normalise an Antigravity CLI hook payload.
66
+ *
67
+ * The event name is an argument, never a payload field. This client sends no
68
+ * `hook_event_name`, and `PreInvocation` and `PostInvocation` hand over
69
+ * byte-identical envelopes for the same invocation - so a payload alone cannot
70
+ * say which of the two ran. The command registered for each event carries its
71
+ * own name, and that is the only thing that knows.
72
+ *
73
+ * A whitelist, for the reason every adapter uses one: the payload carries
74
+ * `transcriptPath` and `artifactDirectoryPath`, both of which lead to the
75
+ * conversation. Neither survives this function.
76
+ */
77
+ export function normalizeAntigravityHook(payload, { args = [] } = {}) {
78
+ const event = args[0];
79
+ if (typeof event !== "string" || !ANTIGRAVITY_HOOK_EVENTS.includes(event)) {
80
+ data("unrecognised Antigravity hook event", { event: event ?? null });
81
+ }
82
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
83
+ data("Antigravity hook payload is not an object", { event });
84
+ }
85
+ if (typeof payload.conversationId !== "string" || payload.conversationId === "") {
86
+ data("hook payload has no conversation id",
87
+ { event, received: Object.keys(payload) });
88
+ }
89
+ // The only working directory this client offers. `workspacePaths` is an array
90
+ // because a session can hold several; the first is the one the conversation
91
+ // was opened against, and ACC's workspace identity needs exactly one.
92
+ //
93
+ // It is empty whenever the session has no open workspace, which an ordinary
94
+ // `agy -p` in a project directory is: captured on 1.2.7 as
95
+ // `fixtures/SessionStart-no-workspace-1.2.7.json`. Nothing else in the
96
+ // payload can stand in for it. `transcriptPath` and `artifactDirectoryPath`
97
+ // both point inside `~/.gemini/antigravity-cli/brain/<conversationId>/`,
98
+ // which is the client's own state and is per conversation - adopting it would
99
+ // give every conversation a private ACC workspace and two agents in one
100
+ // project would never see each other. Nor does the hook process inherit the
101
+ // client's directory: its working directory is the directory of the
102
+ // hooks.json it was registered from, `~/.gemini/config` for a global
103
+ // registration (`fixtures/hook-process-environment-1.2.7.json`).
104
+ //
105
+ // So this refuses, and it refuses *by name*. A hook that fails open is
106
+ // invisible by design, and an anonymous refusal here is indistinguishable
107
+ // from ACC not being installed - which is the state issue #176 describes.
108
+ const paths = payload.workspacePaths;
109
+ if (Array.isArray(paths) && paths.length === 0) {
110
+ data("this Antigravity session has no open workspace, so the hook was given no "
111
+ + "project directory and ACC cannot tell which workspace to join; open the project "
112
+ + "as an Antigravity workspace, or pass it with --add-dir",
113
+ { event, reasonCode: "antigravity_no_open_workspace" });
114
+ }
115
+ const cwd = Array.isArray(paths) ? paths[0] : undefined;
116
+ if (typeof cwd !== "string" || cwd === "") {
117
+ data("hook payload has no workspace path", { event, received: Object.keys(payload) });
118
+ }
119
+ return normalizedEvent({
120
+ kind: KIND_BY_EVENT[event],
121
+ sessionId: payload.conversationId,
122
+ cwd,
123
+ model: typeof payload.modelName === "string" ? payload.modelName : null,
124
+ parentSessionId: null,
125
+ // This client runs no tool hook, so there is never a tool to name and never
126
+ // a target to guard. Declared empty rather than left to a default, because
127
+ // a future reader will want to know it was measured.
128
+ tool: null,
129
+ targets: [],
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Context for the next invocation.
135
+ *
136
+ * Measured, not copied: returning this from `PreInvocation` put the text where
137
+ * the model read it, and the model reproduced the probe token in its reply. The
138
+ * client's `userMessage` and `toolCall` injection types exist in the binary and
139
+ * were not exercised, so neither is used here.
140
+ */
141
+ export function injectResponse(context) {
142
+ return context === "" || context === undefined || context === null
143
+ ? {} : { injectSteps: [{ ephemeralMessage: String(context) }] };
144
+ }
145
+
146
+ /**
147
+ * Whether to hold this turn open, and why.
148
+ *
149
+ * `continue` is the exact string; any other value, including `block` and
150
+ * `stop`, permits shutdown. The `reason` reaches the model - the model
151
+ * reproduced a token carried in it - so this is a delivery path as well as a
152
+ * decision. It arrives inside a `<SYSTEM_MESSAGE>` prefixed
153
+ * `Stop hook blocked termination:`, so a peer body carried here sits in a
154
+ * system-framed block, and the untrusted-peer fence the projector draws around
155
+ * it is what tells the model it is data
156
+ * (`fixtures/stop-continuation-live-1.2.7.json`).
157
+ *
158
+ * Everything that is not a good reason below the ceiling releases the turn.
159
+ * That is deliberate and it is the whole fail-open rule in one expression: a
160
+ * hook error, an expired budget and an unreachable store all arrive here as an
161
+ * absent reason, and none of them may be the cause of a session that will not
162
+ * finish.
163
+ */
164
+ export function stopResponse(input) {
165
+ const reason = input?.reason;
166
+ const executionNum = input?.executionNum;
167
+ if (typeof reason !== "string" || reason.trim() === "") return {};
168
+ if (!Number.isInteger(executionNum) || executionNum < 0) return {};
169
+ if (executionNum >= STOP_CONTINUATION_CEILING) return {};
170
+ return { decision: "continue", reason };
171
+ }
172
+
173
+ export function injectOutcome(context) {
174
+ const response = injectResponse(context);
175
+ return { stdout: Object.keys(response).length === 0 ? "" : `${JSON.stringify(response)}\n`,
176
+ stderr: "", exitCode: 0 };
177
+ }
178
+
179
+ export function stopOutcome(input) {
180
+ const response = stopResponse(input);
181
+ // Printing nothing is how a turn is allowed to end. An empty object would do
182
+ // the same thing, but silence cannot be mistaken later for a decision.
183
+ return { stdout: Object.keys(response).length === 0 ? "" : `${JSON.stringify(response)}\n`,
184
+ stderr: "", exitCode: 0 };
185
+ }
186
+
187
+ /**
188
+ * There is no deny on this client.
189
+ *
190
+ * No tool event loads, so nothing ever reaches a guard, and no shape was ever
191
+ * found that stops a tool call. This exists because the hook runtime asks every
192
+ * adapter how it denies, and the honest answer here is that it cannot: the call
193
+ * is allowed and the reason goes to stderr where an operator can read it.
194
+ *
195
+ * It must never return a deny envelope copied from another client. A shape that
196
+ * does nothing, printed as though it did something, is exactly the silent
197
+ * failure this package documents.
198
+ */
199
+ export function denyOutcome(reason) {
200
+ return { stdout: "", stderr: `acc: ${reason} (this client has no tool guard)`, exitCode: 0 };
201
+ }