agents-can-communicate 0.2.0 → 0.3.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.
- package/README.md +17 -7
- package/bin/acc-bootstrap.mjs +56 -0
- package/bin/acc-claude-channel.mjs +177 -0
- package/bin/acc.mjs +7 -2
- package/docs/ADAPTER_AUTHORING.md +34 -2
- package/docs/CAPABILITIES.md +25 -10
- package/docs/CLI.md +10 -3
- package/docs/CONFIGURATION.md +4 -0
- package/docs/HOW_IT_WORKS.md +277 -0
- package/docs/PROTOCOL.md +1 -1
- package/docs/RELEASING.md +7 -1
- package/docs/TROUBLESHOOTING.md +7 -1
- package/docs/index.md +3 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +196 -46
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +237 -45
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.252.json +3 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.258.json +23 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.260.json +23 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +5 -2
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/.mcp.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +4 -2
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +34 -2
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/channel.mjs +377 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +27 -7
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +229 -0
- package/node_modules/@agents-can-communicate/adapter-codex/certification.json +38 -5
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +171 -38
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.0.json +3 -1
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-remote-workspace.json +25 -0
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +4 -2
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +4 -2
- package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +42 -7
- package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +121 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +151 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +192 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/certification.json +44 -28
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +4 -2
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/{BeforeAgent.json → BeforeAgent-0.57.0.json} +2 -2
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/{BeforeTool.json → BeforeTool-0.57.0.json} +2 -2
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/{BeforeTool-shell.json → BeforeTool-shell-0.57.0.json} +2 -2
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/{SessionEnd.json → SessionEnd-0.57.0.json} +2 -2
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/{SessionStart.json → SessionStart-0.57.0.json} +2 -2
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/certification-provenance.json +266 -39
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +6 -6
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +25 -11
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +4 -2
- package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +3 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +4 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +18 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +2 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +6 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-activation.mjs +76 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +202 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +101 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +17 -2
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/args.mjs +2 -2
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +67 -5
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +103 -11
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +52 -2
- package/node_modules/@agents-can-communicate/core/src/service.mjs +10 -0
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +26 -12
- package/node_modules/@agents-can-communicate/hook-runner/package.json +4 -2
- package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +90 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +123 -90
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +48 -4
- package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +144 -0
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +72 -3
- package/node_modules/@agents-can-communicate/installer/src/index.mjs +7 -0
- package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +161 -0
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +6 -2
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +41 -6
- package/node_modules/@agents-can-communicate/installer/src/shell-bootstrap.mjs +210 -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/protocol/src/fields.mjs +17 -0
- package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +18 -3
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/package.json +2 -1
|
@@ -12,6 +12,7 @@ import { createGitProbe, discoverWorkspace, platformDataHome, runtimePaths }
|
|
|
12
12
|
|
|
13
13
|
import { resolveClientPid } from "./client-pid.mjs";
|
|
14
14
|
import { probeClientVersion as defaultProbeClientVersion } from "./client-version.mjs";
|
|
15
|
+
import { establishNativeBinding, livePolicyFrom } from "./native-binding.mjs";
|
|
15
16
|
import { readProcessTable as defaultReadProcessTable } from "./process-table.mjs";
|
|
16
17
|
|
|
17
18
|
// Kept cohesive above 300 lines because every handler shares one fail-open
|
|
@@ -184,6 +185,107 @@ async function openContext({ cwd, dataHome, runtime, env }) {
|
|
|
184
185
|
service: createCoordinationService({ store, clock: runtime.clock, ids: runtime.ids }) };
|
|
185
186
|
}
|
|
186
187
|
|
|
188
|
+
// What this turn is shown and which complete groups it may commit as
|
|
189
|
+
// offered. Runs after the heartbeat and the bounded native retry.
|
|
190
|
+
async function projectTurn({ binding, context, adapter, adapterId }) {
|
|
191
|
+
const sync = await context.service.sync({ sessionId: binding.accSessionId,
|
|
192
|
+
cursor: null, scope: "delta" });
|
|
193
|
+
|
|
194
|
+
// Sync already carries the roster. Calling collectStatus here used to read
|
|
195
|
+
// the entire materialised store a second time on every prompt merely to
|
|
196
|
+
// rediscover this session's participant id.
|
|
197
|
+
const mine = sync.roster
|
|
198
|
+
.find(participant => participant.sessionId === binding.accSessionId);
|
|
199
|
+
|
|
200
|
+
// What peers have said to this participant and no model has been shown yet.
|
|
201
|
+
// Without this the projector's peer block never runs in production: an
|
|
202
|
+
// agent sees only the obligation attention line, not the durable body that
|
|
203
|
+
// may be offered after the stdout transport succeeds.
|
|
204
|
+
const delivery = await context.service.nextTurnDelivery({
|
|
205
|
+
workspaceId: context.descriptor.id,
|
|
206
|
+
participantId: mine?.participantId,
|
|
207
|
+
exceptSessionId: binding.accSessionId });
|
|
208
|
+
const messages = delivery.queuedMessages;
|
|
209
|
+
|
|
210
|
+
// Solo costs nothing: nothing to say means nothing printed, not a banner
|
|
211
|
+
// announcing that nobody else is here. But something already said to you is
|
|
212
|
+
// not nothing - the check used to run before the inbox was read, so the
|
|
213
|
+
// answer to your own request vanished the moment the agent working on it
|
|
214
|
+
// closed and left you as the only session.
|
|
215
|
+
if (sync.solo && messages.length === 0) return { stdout: "" };
|
|
216
|
+
|
|
217
|
+
// The ceiling a team agreed on in `acc.workspace.json`, or the default when
|
|
218
|
+
// there is no config. Validated by the protocol and, until now, never read:
|
|
219
|
+
// the projector was always called with its own default.
|
|
220
|
+
const effective = effectiveCapabilities(adapter, binding);
|
|
221
|
+
const hasStructuredRenderer = typeof adapter.renderContextResult === "function";
|
|
222
|
+
const canOfferNextTurn = effective.delivery.nextTurn === true && hasStructuredRenderer;
|
|
223
|
+
const projectionInput = { ...sync, messages: canOfferNextTurn ? messages : [],
|
|
224
|
+
liveOfferedMessageIds: delivery.liveOfferedMessageIds,
|
|
225
|
+
roomMessageIds: delivery.roomMessageIds,
|
|
226
|
+
currentParticipantId: mine?.participantId };
|
|
227
|
+
const projectionOptions = {
|
|
228
|
+
budgetBytes: context.descriptor.policy?.contextBudgetBytes };
|
|
229
|
+
// Delivery is state, not text parsing. Peer-controlled bodies can imitate
|
|
230
|
+
// another message's visible header, so only projector metadata proves
|
|
231
|
+
// which complete groups survived the byte budget. A custom adapter without
|
|
232
|
+
// metadata may still inject text, but cannot advance a receipt from it.
|
|
233
|
+
const projection = !hasStructuredRenderer
|
|
234
|
+
? { text: await adapter.renderContext?.(projectionInput, projectionOptions) ?? "",
|
|
235
|
+
offeredMessageIds: [], includedAttentionIds: [] }
|
|
236
|
+
: await adapter.renderContextResult(projectionInput, projectionOptions);
|
|
237
|
+
const clientFactsKnown = typeof binding.clientVersion === "string"
|
|
238
|
+
&& typeof binding.platform === "string";
|
|
239
|
+
const reason = !effective.delivery.nextTurn
|
|
240
|
+
? clientFactsKnown
|
|
241
|
+
? `client ${binding.clientVersion} on ${binding.platform} is not certified for nextTurn`
|
|
242
|
+
: "the client version or platform is unknown"
|
|
243
|
+
: !hasStructuredRenderer ? "this adapter lacks structured delivery metadata" : null;
|
|
244
|
+
const degradation = reason !== null && messages.length > 0
|
|
245
|
+
? `acc: ${messages.length} pending message(s) withheld because ${reason}; read `
|
|
246
|
+
+ `${messages[0].messageId} with acc inbox --message ${messages[0].messageId}` : null;
|
|
247
|
+
const visibleDegradation = degradation === null ? "" : `ACC: ${degradation.slice(5)}`;
|
|
248
|
+
const budgetBytes = projectionOptions.budgetBytes ?? 6_000;
|
|
249
|
+
const projected = degradation === null ? projection.text
|
|
250
|
+
: fitDegradation(projection.text, visibleDegradation, messages, budgetBytes);
|
|
251
|
+
if (projected === "") {
|
|
252
|
+
return degradation === null ? { stdout: "" } : { stdout: "", stderr: degradation };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// The renderer returns ids as metadata, never as text to parse. A peer body
|
|
256
|
+
// can imitate every visible label, so only a complete group selected by the
|
|
257
|
+
// projector is eligible for the post-write offer commit.
|
|
258
|
+
const offered = new Set(canOfferNextTurn ? projection.offeredMessageIds ?? [] : []);
|
|
259
|
+
const offerInputs = messages.filter(message => offered.has(message.messageId))
|
|
260
|
+
.map(message => ({ messageId: message.messageId,
|
|
261
|
+
recipientParticipantId: mine.participantId,
|
|
262
|
+
targetSessionId: binding.accSessionId, targetGeneration: binding.generation,
|
|
263
|
+
transport: "next-turn", adapterId,
|
|
264
|
+
clientVersion: binding.clientVersion }));
|
|
265
|
+
// Same again: Kimi Code shows the model a hook's raw stdout, while Gemini
|
|
266
|
+
// and Claude Code want an envelope and drop a bare string.
|
|
267
|
+
// The entry point owns the transport boundary. This handler only prepares
|
|
268
|
+
// offer inputs; recording them here would claim delivery before stdout's
|
|
269
|
+
// callback proves that the bytes crossed.
|
|
270
|
+
const outcome = { stdout: "", ...adapter.injectOutcome?.(projected) };
|
|
271
|
+
const writableOffers = outcome.stdout === "" ? [] : offerInputs;
|
|
272
|
+
if (degradation === null) return { ...outcome, offerInputs: writableOffers };
|
|
273
|
+
return { ...outcome,
|
|
274
|
+
stderr: [outcome.stderr, degradation].filter(Boolean).join("\n"),
|
|
275
|
+
offerInputs: writableOffers };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// One bounded, fail-open native handshake for this exact session generation.
|
|
279
|
+
// The policy comes only from the environment an owned shell bootstrap
|
|
280
|
+
// exported; an ordinary launch has none and stays durable.
|
|
281
|
+
async function bindNative({ adapter, event, hookBinding, clientVersion, platform, context, paths,
|
|
282
|
+
deadline }) {
|
|
283
|
+
return establishNativeBinding({ adapter, event, hookBinding, clientVersion, platform,
|
|
284
|
+
livePolicy: livePolicyFrom(context.env), service: context.service, runtimeDir: paths.root,
|
|
285
|
+
clock: context.service.clock,
|
|
286
|
+
timeoutMs: Math.max(1, Math.min(750, deadline - Date.now())) });
|
|
287
|
+
}
|
|
288
|
+
|
|
187
289
|
const HANDLERS = {
|
|
188
290
|
async sessionStart({ event, context, adapter, adapterId, binding, paths,
|
|
189
291
|
readProcessTable, probeClientVersion, platform, deadline }) {
|
|
@@ -204,6 +306,9 @@ const HANDLERS = {
|
|
|
204
306
|
const command = adapter.client?.command ?? null;
|
|
205
307
|
const pid = command === null ? null
|
|
206
308
|
: resolveClientPid({ table: await readProcessTable(), from: process.pid, command });
|
|
309
|
+
const clientPid = Number.isInteger(pid) && pid > 0 ? pid : undefined;
|
|
310
|
+
const native = hookBinding => bindNative({ adapter, event, hookBinding, ...clientFacts,
|
|
311
|
+
context, paths, deadline });
|
|
207
312
|
const metadata = {
|
|
208
313
|
pid,
|
|
209
314
|
enforcement: capabilities.guards?.beforeWrite === true ? "guarded" : "advisory",
|
|
@@ -219,10 +324,12 @@ const HANDLERS = {
|
|
|
219
324
|
...metadata,
|
|
220
325
|
});
|
|
221
326
|
if (resumed !== null) {
|
|
327
|
+
const hookBinding = { accSessionId: resumed.sessionId, generation: resumed.generation,
|
|
328
|
+
...clientFacts, clientPid };
|
|
222
329
|
await storeSessionBinding({ runtimeDir: paths.root, harnessSessionId: event.sessionId,
|
|
223
|
-
|
|
330
|
+
...hookBinding });
|
|
224
331
|
return { accSessionId: resumed.sessionId, generation: resumed.generation,
|
|
225
|
-
...clientFacts, capabilities };
|
|
332
|
+
...clientFacts, capabilities, nativeBinding: await native(hookBinding) };
|
|
226
333
|
}
|
|
227
334
|
}
|
|
228
335
|
const session = await context.service.openSession({
|
|
@@ -239,10 +346,12 @@ const HANDLERS = {
|
|
|
239
346
|
...metadata,
|
|
240
347
|
descriptor: context.descriptor,
|
|
241
348
|
});
|
|
349
|
+
const hookBinding = { accSessionId: session.sessionId, generation: session.generation,
|
|
350
|
+
...clientFacts, clientPid };
|
|
242
351
|
await storeSessionBinding({ runtimeDir: paths.root, harnessSessionId: event.sessionId,
|
|
243
|
-
|
|
352
|
+
...hookBinding });
|
|
244
353
|
return { accSessionId: session.sessionId, generation: session.generation,
|
|
245
|
-
...clientFacts, capabilities };
|
|
354
|
+
...clientFacts, capabilities, nativeBinding: await native(hookBinding) };
|
|
246
355
|
},
|
|
247
356
|
|
|
248
357
|
async heartbeat({ binding, context }) {
|
|
@@ -260,97 +369,21 @@ const HANDLERS = {
|
|
|
260
369
|
return {};
|
|
261
370
|
},
|
|
262
371
|
|
|
263
|
-
async beforeTurn(
|
|
372
|
+
async beforeTurn(input) {
|
|
373
|
+
const { binding, context, adapter, event, paths, deadline } = input;
|
|
264
374
|
if (binding === null) return {};
|
|
265
375
|
// A turn is the clearest sign a session is alive. Never a reason to fail:
|
|
266
376
|
// this runs in front of somebody's prompt.
|
|
267
377
|
await context.service.heartbeatSession({ sessionId: binding.accSessionId,
|
|
268
378
|
generation: binding.generation }).catch(() => null);
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
// What peers have said to this participant and no model has been shown yet.
|
|
279
|
-
// Without this the projector's peer block never runs in production: an
|
|
280
|
-
// agent sees only the obligation attention line, not the durable body that
|
|
281
|
-
// may be offered after the stdout transport succeeds.
|
|
282
|
-
const delivery = await context.service.nextTurnDelivery({
|
|
283
|
-
workspaceId: context.descriptor.id,
|
|
284
|
-
participantId: mine?.participantId,
|
|
285
|
-
exceptSessionId: binding.accSessionId });
|
|
286
|
-
const messages = delivery.queuedMessages;
|
|
287
|
-
|
|
288
|
-
// Solo costs nothing: nothing to say means nothing printed, not a banner
|
|
289
|
-
// announcing that nobody else is here. But something already said to you is
|
|
290
|
-
// not nothing - the check used to run before the inbox was read, so the
|
|
291
|
-
// answer to your own request vanished the moment the agent working on it
|
|
292
|
-
// closed and left you as the only session.
|
|
293
|
-
if (sync.solo && messages.length === 0) return { stdout: "" };
|
|
294
|
-
|
|
295
|
-
// The ceiling a team agreed on in `acc.workspace.json`, or the default when
|
|
296
|
-
// there is no config. Validated by the protocol and, until now, never read:
|
|
297
|
-
// the projector was always called with its own default.
|
|
298
|
-
const effective = effectiveCapabilities(adapter, binding);
|
|
299
|
-
const hasStructuredRenderer = typeof adapter.renderContextResult === "function";
|
|
300
|
-
const canOfferNextTurn = effective.delivery.nextTurn === true && hasStructuredRenderer;
|
|
301
|
-
const projectionInput = { ...sync, messages: canOfferNextTurn ? messages : [],
|
|
302
|
-
liveOfferedMessageIds: delivery.liveOfferedMessageIds,
|
|
303
|
-
roomMessageIds: delivery.roomMessageIds,
|
|
304
|
-
currentParticipantId: mine?.participantId };
|
|
305
|
-
const projectionOptions = {
|
|
306
|
-
budgetBytes: context.descriptor.policy?.contextBudgetBytes };
|
|
307
|
-
// Delivery is state, not text parsing. Peer-controlled bodies can imitate
|
|
308
|
-
// another message's visible header, so only projector metadata proves
|
|
309
|
-
// which complete groups survived the byte budget. A custom adapter without
|
|
310
|
-
// metadata may still inject text, but cannot advance a receipt from it.
|
|
311
|
-
const projection = !hasStructuredRenderer
|
|
312
|
-
? { text: await adapter.renderContext?.(projectionInput, projectionOptions) ?? "",
|
|
313
|
-
offeredMessageIds: [], includedAttentionIds: [] }
|
|
314
|
-
: await adapter.renderContextResult(projectionInput, projectionOptions);
|
|
315
|
-
const clientFactsKnown = typeof binding.clientVersion === "string"
|
|
316
|
-
&& typeof binding.platform === "string";
|
|
317
|
-
const reason = !effective.delivery.nextTurn
|
|
318
|
-
? clientFactsKnown
|
|
319
|
-
? `client ${binding.clientVersion} on ${binding.platform} is not certified for nextTurn`
|
|
320
|
-
: "the client version or platform is unknown"
|
|
321
|
-
: !hasStructuredRenderer ? "this adapter lacks structured delivery metadata" : null;
|
|
322
|
-
const degradation = reason !== null && messages.length > 0
|
|
323
|
-
? `acc: ${messages.length} pending message(s) withheld because ${reason}; read `
|
|
324
|
-
+ `${messages[0].messageId} with acc inbox --message ${messages[0].messageId}` : null;
|
|
325
|
-
const visibleDegradation = degradation === null ? "" : `ACC: ${degradation.slice(5)}`;
|
|
326
|
-
const budgetBytes = projectionOptions.budgetBytes ?? 6_000;
|
|
327
|
-
const projected = degradation === null ? projection.text
|
|
328
|
-
: fitDegradation(projection.text, visibleDegradation, messages, budgetBytes);
|
|
329
|
-
if (projected === "") {
|
|
330
|
-
return degradation === null ? { stdout: "" } : { stdout: "", stderr: degradation };
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// The renderer returns ids as metadata, never as text to parse. A peer body
|
|
334
|
-
// can imitate every visible label, so only a complete group selected by the
|
|
335
|
-
// projector is eligible for the post-write offer commit.
|
|
336
|
-
const offered = new Set(canOfferNextTurn ? projection.offeredMessageIds ?? [] : []);
|
|
337
|
-
const offerInputs = messages.filter(message => offered.has(message.messageId))
|
|
338
|
-
.map(message => ({ messageId: message.messageId,
|
|
339
|
-
recipientParticipantId: mine.participantId,
|
|
340
|
-
targetSessionId: binding.accSessionId, targetGeneration: binding.generation,
|
|
341
|
-
transport: "next-turn", adapterId,
|
|
342
|
-
clientVersion: binding.clientVersion }));
|
|
343
|
-
// Same again: Kimi Code shows the model a hook's raw stdout, while Gemini
|
|
344
|
-
// and Claude Code want an envelope and drop a bare string.
|
|
345
|
-
// The entry point owns the transport boundary. This handler only prepares
|
|
346
|
-
// offer inputs; recording them here would claim delivery before stdout's
|
|
347
|
-
// callback proves that the bytes crossed.
|
|
348
|
-
const outcome = { stdout: "", ...adapter.injectOutcome?.(projected) };
|
|
349
|
-
const writableOffers = outcome.stdout === "" ? [] : offerInputs;
|
|
350
|
-
if (degradation === null) return { ...outcome, offerInputs: writableOffers };
|
|
351
|
-
return { ...outcome,
|
|
352
|
-
stderr: [outcome.stderr, degradation].filter(Boolean).join("\n"),
|
|
353
|
-
offerInputs: writableOffers };
|
|
379
|
+
// A native transport that became ready only after SessionStart is picked
|
|
380
|
+
// up here and a live lease is renewed: bounded, fail-open, never on a guard.
|
|
381
|
+
const nativeBinding = livePolicyFrom(context.env) === "off" ? undefined
|
|
382
|
+
: await bindNative({ adapter, event, hookBinding: binding,
|
|
383
|
+
clientVersion: binding.clientVersion, platform: binding.platform, context, paths,
|
|
384
|
+
deadline });
|
|
385
|
+
const turn = await projectTurn(input);
|
|
386
|
+
return nativeBinding === undefined ? turn : { ...turn, nativeBinding };
|
|
354
387
|
},
|
|
355
388
|
|
|
356
389
|
async beforeTool({ binding, context, event, adapter }) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { applyNativeActivation, deactivateNative } from "./native-activation.mjs";
|
|
1
2
|
import { finalizeRemoval, missingArtifactParents, recordInstall,
|
|
2
3
|
removeEmptyOwnedDirectories, removeOwnedArtifacts } from "./ownership.mjs";
|
|
3
4
|
|
|
@@ -14,7 +15,7 @@ import { finalizeRemoval, missingArtifactParents, recordInstall,
|
|
|
14
15
|
* three that work, plus the name of the one that did not.
|
|
15
16
|
*/
|
|
16
17
|
export async function applyPlan({ plan, adapters, context, dataHome, dryRun = false,
|
|
17
|
-
accVersion = null }) {
|
|
18
|
+
accVersion = null, activation = {} }) {
|
|
18
19
|
const byId = new Map(adapters.map(adapter => [adapter.id, adapter]));
|
|
19
20
|
const results = { action: plan.action, dryRun, operations: [], skipped: plan.skipped,
|
|
20
21
|
failed: [] };
|
|
@@ -37,22 +38,46 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
|
|
|
37
38
|
requestedLivePolicy: operation.livePolicy ?? "off",
|
|
38
39
|
livePolicy: operation.effectiveLivePolicy ?? "off" };
|
|
39
40
|
const outcome = await adapter.install(installContext);
|
|
41
|
+
// A consented activation is applied after the adapter's own wiring, in
|
|
42
|
+
// a fixed order; an explicit off takes a recorded one back first so the
|
|
43
|
+
// record written below describes the machine as it now is.
|
|
44
|
+
const notes = [];
|
|
45
|
+
if (operation.deactivation !== undefined) {
|
|
46
|
+
const report = await deactivateNative({ nativeActivation: operation.deactivation,
|
|
47
|
+
...activation });
|
|
48
|
+
notes.push(...describeTeardown(report));
|
|
49
|
+
}
|
|
50
|
+
let native = null;
|
|
51
|
+
let appendedRcBlock = false;
|
|
52
|
+
if (operation.nativeActivation !== undefined) {
|
|
53
|
+
const applied = await applyNativeActivation({ adapter,
|
|
54
|
+
activation: operation.nativeActivation, dataHome, ...activation });
|
|
55
|
+
native = applied.nativeActivation;
|
|
56
|
+
appendedRcBlock = applied.appendedRcBlock;
|
|
57
|
+
}
|
|
40
58
|
// Recorded after the write, so a record never claims an install that
|
|
41
59
|
// did not happen. The reverse order would leave uninstall trying to
|
|
42
60
|
// remove files nothing created.
|
|
43
61
|
await recordInstall({ dataHome, adapterId: adapter.id,
|
|
44
62
|
version: operation.clientVersion ?? null, accVersion,
|
|
45
|
-
artifacts: operation.artifacts, createdDirectories });
|
|
46
|
-
results.operations.push({ ...operation, applied: true,
|
|
63
|
+
artifacts: operation.artifacts, createdDirectories, nativeActivation: native });
|
|
64
|
+
results.operations.push({ ...operation, applied: true, appendedRcBlock,
|
|
47
65
|
changes: outcome.changes ?? [], diagnostics: [
|
|
48
66
|
...(operation.deliveryDiagnostic === undefined
|
|
49
67
|
? [] : [operation.deliveryDiagnostic]),
|
|
50
68
|
...(outcome.diagnostics ?? []),
|
|
69
|
+
...notes,
|
|
51
70
|
] });
|
|
52
71
|
} else {
|
|
53
72
|
// Keep the record until every cleanup step succeeds. It is both the
|
|
54
73
|
// authority for deletion and the only durable recipe a retry has when
|
|
55
74
|
// the client or one of ACC's own artifacts is already gone.
|
|
75
|
+
const notes = [];
|
|
76
|
+
if (operation.deactivation !== undefined) {
|
|
77
|
+
const report = await deactivateNative({ nativeActivation: operation.deactivation,
|
|
78
|
+
...activation });
|
|
79
|
+
notes.push(...describeTeardown(report));
|
|
80
|
+
}
|
|
56
81
|
const owned = await removeOwnedArtifacts({ dataHome, adapterId: adapter.id });
|
|
57
82
|
// What ownership held back is passed on, because the adapter would
|
|
58
83
|
// otherwise remove its own layout unconditionally and undo the decision.
|
|
@@ -66,7 +91,7 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
|
|
|
66
91
|
changes: outcome.changes ?? [], removed: owned.removed, kept: owned.kept,
|
|
67
92
|
removedDirectories: directories.removed, keptDirectories: directories.kept,
|
|
68
93
|
missingDirectories: directories.missing,
|
|
69
|
-
diagnostics: outcome.diagnostics ?? [] });
|
|
94
|
+
diagnostics: [...(outcome.diagnostics ?? []), ...notes] });
|
|
70
95
|
}
|
|
71
96
|
} catch (error) {
|
|
72
97
|
results.failed.push({ adapterId: operation.adapterId, error: error.message });
|
|
@@ -74,3 +99,22 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
|
|
|
74
99
|
}
|
|
75
100
|
return results;
|
|
76
101
|
}
|
|
102
|
+
|
|
103
|
+
// What a deactivation actually did, said truthfully: a retained service is
|
|
104
|
+
// named as retained, a modified PATH block as kept.
|
|
105
|
+
function describeTeardown(report) {
|
|
106
|
+
const lines = [];
|
|
107
|
+
if (report.shell !== null) {
|
|
108
|
+
for (const file of report.shell.removedShims) lines.push(`removed shim ${file}`);
|
|
109
|
+
for (const file of report.shell.keptShims) lines.push(`kept shim ${file} - changed since ACC wrote it`);
|
|
110
|
+
if (report.shell.rcBlock === "removed") lines.push("removed the ACC PATH block");
|
|
111
|
+
if (report.shell.rcBlock === "modified") lines.push("kept the ACC PATH block - changed since ACC wrote it");
|
|
112
|
+
if (report.shell.rcBlock === "kept") lines.push("kept the ACC PATH block - another ACC shim still uses it");
|
|
113
|
+
}
|
|
114
|
+
for (const service of report.services) {
|
|
115
|
+
lines.push(service.outcome === "stopped" ? `stopped the ${service.serviceId} service`
|
|
116
|
+
: `retained the ${service.serviceId} service (${service.outcome === "retained_pre_existing"
|
|
117
|
+
? "it existed before ACC" : "no vendor teardown exists"})`);
|
|
118
|
+
}
|
|
119
|
+
return lines;
|
|
120
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { createReadStream } from "node:fs";
|
|
4
|
+
import { mkdir, readFile, realpath, rename, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
import { evaluateNativeEligibility } from "@agents-can-communicate/adapter-sdk";
|
|
8
|
+
|
|
9
|
+
// The launch-time check behind an owned shell shim. It answers one closed
|
|
10
|
+
// question - may this exact executable receive native delivery? - from the
|
|
11
|
+
// static minimum, a read-only adapter probe, and a keyed cache under ACC's own
|
|
12
|
+
// data home. It never throws: a shim that cannot decide launches the vendor
|
|
13
|
+
// command untouched, and this module is what makes "cannot decide" cheap.
|
|
14
|
+
//
|
|
15
|
+
// The cache key is the executable's identity - resolved path, symlink target,
|
|
16
|
+
// inode, size, mtime, and full sha256 - so an upgrade or replacement is a miss
|
|
17
|
+
// and an unchanged binary skips the version spawn and the probe. A cached
|
|
18
|
+
// failure is short-lived; a repaired client is never disabled for long.
|
|
19
|
+
|
|
20
|
+
export const BOOTSTRAP_CACHE_SCHEMA = 1;
|
|
21
|
+
export const SUPPORTED_TTL_MS = 6 * 60 * 60 * 1_000;
|
|
22
|
+
export const FAILED_TTL_MS = 5 * 60 * 1_000;
|
|
23
|
+
const DEFAULT_TIMEOUT_MS = 750;
|
|
24
|
+
const VERSION = /(?:^|[^0-9A-Za-z])v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)(?:\b|$)/;
|
|
25
|
+
|
|
26
|
+
export const cachePathFor = (dataHome, adapterId) =>
|
|
27
|
+
path.join(dataHome, "acc", "native-bootstrap", `${adapterId}.json`);
|
|
28
|
+
|
|
29
|
+
function sha256File(file) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const hash = createHash("sha256");
|
|
32
|
+
createReadStream(file).on("data", chunk => hash.update(chunk))
|
|
33
|
+
.on("error", reject).on("end", () => resolve(`sha256:${hash.digest("hex")}`));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function executableIdentity(realExecutable) {
|
|
38
|
+
const target = await realpath(realExecutable);
|
|
39
|
+
const facts = await stat(target);
|
|
40
|
+
if (!facts.isFile()) throw new Error("not a file");
|
|
41
|
+
return { path: realExecutable, target, inode: facts.ino, size: facts.size,
|
|
42
|
+
mtimeMs: Math.floor(facts.mtimeMs), executableFingerprint: await sha256File(target) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function withTimeout(work, ms, label) {
|
|
46
|
+
let timer = null;
|
|
47
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
48
|
+
timer = setTimeout(() => reject(Object.assign(new Error(`${label} timed out`),
|
|
49
|
+
{ code: "ETIMEDOUT" })), ms);
|
|
50
|
+
});
|
|
51
|
+
return Promise.race([work, deadline]).finally(() => clearTimeout(timer));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Read-only: the client's own version command, nothing else.
|
|
55
|
+
export function defaultReadVersion({ realExecutable, versionArgs, timeoutMs }) {
|
|
56
|
+
return new Promise(resolve => {
|
|
57
|
+
execFile(realExecutable, versionArgs, { timeout: timeoutMs, windowsHide: true },
|
|
58
|
+
(error, stdout, stderr) => {
|
|
59
|
+
if (error !== null) return resolve(null);
|
|
60
|
+
resolve(VERSION.exec(`${stdout}${stderr}`)?.[1] ?? null);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const sameIdentity = (left, right) => left !== undefined && right !== undefined
|
|
66
|
+
&& ["path", "target", "inode", "size", "mtimeMs", "executableFingerprint"]
|
|
67
|
+
.every(key => left[key] === right[key]);
|
|
68
|
+
|
|
69
|
+
async function loadCache(file) {
|
|
70
|
+
try {
|
|
71
|
+
const record = JSON.parse(await readFile(file, "utf8"));
|
|
72
|
+
return record?.schemaVersion === BOOTSTRAP_CACHE_SCHEMA ? record : null;
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function saveCache(file, record) {
|
|
79
|
+
await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
80
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
81
|
+
await writeFile(temporary, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 });
|
|
82
|
+
await rename(temporary, file);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const closedOutcome = (supported, reasonCode) => Object.freeze({ supported, reasonCode });
|
|
86
|
+
|
|
87
|
+
export async function checkNativeBootstrap({ adapter, realExecutable, platform, dataHome,
|
|
88
|
+
timeoutMs = DEFAULT_TIMEOUT_MS, clock = { now: () => new Date().toISOString() },
|
|
89
|
+
readVersion = defaultReadVersion }) {
|
|
90
|
+
try {
|
|
91
|
+
if (adapter?.nativeDelivery === undefined || typeof adapter.probeNativeDelivery !== "function") {
|
|
92
|
+
return closedOutcome(false, "native_delivery_unsupported");
|
|
93
|
+
}
|
|
94
|
+
const identity = await executableIdentity(realExecutable);
|
|
95
|
+
const file = cachePathFor(dataHome, adapter.id);
|
|
96
|
+
const now = Date.parse(clock.now());
|
|
97
|
+
const cached = await loadCache(file);
|
|
98
|
+
if (cached !== null && cached.adapterId === adapter.id && cached.platform === platform
|
|
99
|
+
&& sameIdentity(cached.identity, identity) && Date.parse(cached.expiresAt) > now) {
|
|
100
|
+
return closedOutcome(cached.supported, cached.reasonCode);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const clientVersion = await withTimeout(readVersion({ realExecutable: identity.target,
|
|
104
|
+
versionArgs: adapter.client?.versionArgs ?? ["--version"], timeoutMs }), timeoutMs,
|
|
105
|
+
"version probe").catch(() => null);
|
|
106
|
+
let probe = null;
|
|
107
|
+
let reasonCode = null;
|
|
108
|
+
if (clientVersion !== null) {
|
|
109
|
+
try {
|
|
110
|
+
probe = await withTimeout(adapter.probeNativeDelivery({ realExecutable: identity.target,
|
|
111
|
+
timeoutMs }), timeoutMs, "native probe");
|
|
112
|
+
} catch (error) {
|
|
113
|
+
probe = null;
|
|
114
|
+
reasonCode = error?.code === "ETIMEDOUT" ? "probe_timeout" : "feature_probe_failed";
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
let eligibility;
|
|
118
|
+
try {
|
|
119
|
+
eligibility = evaluateNativeEligibility(adapter, { clientVersion, platform, probe });
|
|
120
|
+
} catch {
|
|
121
|
+
// A malformed probe is an adapter bug, and still not a reason to change
|
|
122
|
+
// the user's launch: it fails closed like any other probe failure.
|
|
123
|
+
eligibility = { eligible: false, reasonCode: "feature_probe_failed" };
|
|
124
|
+
}
|
|
125
|
+
const supported = eligibility.eligible === true;
|
|
126
|
+
// A probe that timed out or threw is reported as such rather than as the
|
|
127
|
+
// generic verdict the missing probe would otherwise produce.
|
|
128
|
+
const outcomeReason = supported ? null : (reasonCode ?? eligibility.reasonCode
|
|
129
|
+
?? "feature_probe_failed");
|
|
130
|
+
// Only closed facts are stored: identity, versions, the eligibility verdict.
|
|
131
|
+
// Never command output, never a message body.
|
|
132
|
+
await saveCache(file, {
|
|
133
|
+
schemaVersion: BOOTSTRAP_CACHE_SCHEMA, adapterId: adapter.id, platform, identity,
|
|
134
|
+
clientVersion, supported, reasonCode: outcomeReason,
|
|
135
|
+
protocolContract: supported ? eligibility.protocolContract : null,
|
|
136
|
+
modes: supported ? [...eligibility.modes] : [],
|
|
137
|
+
checkedAt: new Date(now).toISOString(),
|
|
138
|
+
expiresAt: new Date(now + (supported ? SUPPORTED_TTL_MS : FAILED_TTL_MS)).toISOString(),
|
|
139
|
+
}).catch(() => null);
|
|
140
|
+
return closedOutcome(supported, outcomeReason);
|
|
141
|
+
} catch {
|
|
142
|
+
return closedOutcome(false, "feature_probe_failed");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
|
|
4
|
-
import { effectiveCapabilities
|
|
4
|
+
import { effectiveCapabilities, evaluateNativeEligibility, validateNativeActivationPlan }
|
|
5
|
+
from "@agents-can-communicate/adapter-sdk";
|
|
6
|
+
|
|
7
|
+
import { resolveExecutable, shellOf, shimDirFor } from "./native-activation.mjs";
|
|
5
8
|
|
|
6
9
|
const run = promisify(execFile);
|
|
7
10
|
|
|
11
|
+
// Reasons the client itself will not change within an install: unsupported,
|
|
12
|
+
// as opposed to degraded, which a repaired probe or shell may lift.
|
|
13
|
+
const STATIC_REASONS = new Set(["native_delivery_unsupported", "platform_not_captured",
|
|
14
|
+
"version_unavailable", "prerelease_not_captured", "below_minimum_version",
|
|
15
|
+
"known_bad_version"]);
|
|
16
|
+
|
|
8
17
|
const DEFAULT_PROBE_TIMEOUT_MS = 3_000;
|
|
9
18
|
|
|
10
19
|
// Clients print their version in their own shape: "codex-cli 0.147.0", a bare
|
|
@@ -37,9 +46,65 @@ const withTimeout = (work, ms, label) => new Promise((resolve, reject) => {
|
|
|
37
46
|
* exactly the case someone is running this command to find out about, and
|
|
38
47
|
* letting it throw would hide the other three behind it.
|
|
39
48
|
*/
|
|
49
|
+
const unsupported = reasonCode => ({ state: "unsupported", reasonCode, realExecutable: null,
|
|
50
|
+
probe: null, eligibility: null, activationPlan: null });
|
|
51
|
+
const degraded = (reasonCode, facts) => ({ state: "degraded", reasonCode, activationPlan: null,
|
|
52
|
+
...facts });
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The read-only native-delivery report for one detected client: which
|
|
56
|
+
* executable a shim would exec, what the adapter's probe saw, the static
|
|
57
|
+
* verdict, and the activation the adapter would ask for. Version, help, and
|
|
58
|
+
* protocol probes only; never a service mutation.
|
|
59
|
+
*/
|
|
60
|
+
async function detectNative(adapter, entry, { context, platform, probeTimeoutMs, pathEnv }) {
|
|
61
|
+
if (adapter.nativeDelivery === undefined) return unsupported("native_delivery_unsupported");
|
|
62
|
+
try {
|
|
63
|
+
const realExecutable = await resolveExecutable(adapter.client.command, { pathEnv,
|
|
64
|
+
exclude: [typeof context?.stateRoot === "string" ? shimDirFor(context.stateRoot) : null] });
|
|
65
|
+
if (realExecutable === null) return unsupported("version_unavailable");
|
|
66
|
+
const facts = { realExecutable, probe: null, eligibility: null };
|
|
67
|
+
try {
|
|
68
|
+
facts.probe = await withTimeout(Promise.resolve(adapter.probeNativeDelivery({
|
|
69
|
+
realExecutable, timeoutMs: probeTimeoutMs })), probeTimeoutMs,
|
|
70
|
+
`${adapter.id} native probe`);
|
|
71
|
+
} catch {
|
|
72
|
+
facts.probe = null;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
facts.eligibility = evaluateNativeEligibility(adapter,
|
|
76
|
+
{ clientVersion: entry.version, platform, probe: facts.probe });
|
|
77
|
+
} catch {
|
|
78
|
+
facts.eligibility = { eligible: false, reasonCode: "feature_probe_failed" };
|
|
79
|
+
}
|
|
80
|
+
if (facts.eligibility.eligible !== true) {
|
|
81
|
+
const reasonCode = facts.eligibility.reasonCode ?? "feature_probe_failed";
|
|
82
|
+
return STATIC_REASONS.has(reasonCode)
|
|
83
|
+
? { ...unsupported(reasonCode), ...facts } : degraded(reasonCode, facts);
|
|
84
|
+
}
|
|
85
|
+
let activationPlan;
|
|
86
|
+
try {
|
|
87
|
+
activationPlan = validateNativeActivationPlan(await adapter.planNativeActivation({
|
|
88
|
+
detection: { realExecutable, version: entry.version, platform, probe: facts.probe },
|
|
89
|
+
context, livePolicy: null }));
|
|
90
|
+
} catch {
|
|
91
|
+
return degraded("feature_probe_failed", facts);
|
|
92
|
+
}
|
|
93
|
+
if (!activationPlan.eligible) return degraded(activationPlan.reasonCode, facts);
|
|
94
|
+
const shell = context?.shell ?? null;
|
|
95
|
+
if (activationPlan.mechanisms.some(item => item.kind === "shell-bootstrap") && shell !== "zsh") {
|
|
96
|
+
return { ...degraded("unsupported_shell", facts), activationPlan };
|
|
97
|
+
}
|
|
98
|
+
return { state: "eligible", reasonCode: null, ...facts, activationPlan };
|
|
99
|
+
} catch {
|
|
100
|
+
return degraded("feature_probe_failed", { realExecutable: null, probe: null, eligibility: null });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
40
104
|
export async function detectInstallation({ adapters, context, probe = spawnProbe,
|
|
41
105
|
probeTimeoutMs = DEFAULT_PROBE_TIMEOUT_MS,
|
|
42
|
-
platform = `${process.platform}-${process.arch}
|
|
106
|
+
platform = `${process.platform}-${process.arch}`,
|
|
107
|
+
pathEnv = context?.env?.PATH ?? process.env.PATH ?? "" }) {
|
|
43
108
|
const entries = await Promise.all([...adapters]
|
|
44
109
|
// Ordered by id so two runs can be diffed, and so a plan built from this is
|
|
45
110
|
// deterministic rather than dependent on registry order.
|
|
@@ -67,6 +132,10 @@ export async function detectInstallation({ adapters, context, probe = spawnProbe
|
|
|
67
132
|
}
|
|
68
133
|
entry.capabilities = effectiveCapabilities(adapter,
|
|
69
134
|
{ clientVersion: entry.version, platform });
|
|
135
|
+
entry.nativeDelivery = entry.present
|
|
136
|
+
? await detectNative(adapter, entry, { context, platform, probeTimeoutMs, pathEnv })
|
|
137
|
+
: unsupported(adapter.nativeDelivery === undefined
|
|
138
|
+
? "native_delivery_unsupported" : "version_unavailable");
|
|
70
139
|
|
|
71
140
|
try {
|
|
72
141
|
const detected = await adapter.detect(context);
|
|
@@ -82,7 +151,7 @@ export async function detectInstallation({ adapters, context, probe = spawnProbe
|
|
|
82
151
|
} catch (error) {
|
|
83
152
|
entry.error = entry.error ?? error.message;
|
|
84
153
|
}
|
|
85
|
-
if (entry.
|
|
154
|
+
if (entry.nativeDelivery.state !== "eligible"
|
|
86
155
|
&& typeof adapter.deliveryFallback?.diagnostic === "string") {
|
|
87
156
|
const nextTurnDowngraded = adapter.capabilities?.delivery?.nextTurn === true
|
|
88
157
|
&& entry.capabilities?.delivery?.nextTurn !== true;
|
|
@@ -5,3 +5,10 @@ export { applyPlan } from "./apply.mjs";
|
|
|
5
5
|
export { finalizeRemoval, fingerprint, loadOwnership, missingArtifactParents, recordInstall,
|
|
6
6
|
removeEmptyOwnedDirectories, removeOwned, removeOwnedArtifacts, treeFingerprint, verifyOwned }
|
|
7
7
|
from "./ownership.mjs";
|
|
8
|
+
export { BOOTSTRAP_CACHE_SCHEMA, FAILED_TTL_MS, SUPPORTED_TTL_MS, cachePathFor,
|
|
9
|
+
checkNativeBootstrap } from "./bootstrap-runtime.mjs";
|
|
10
|
+
export { BLOCK_BEGIN, BLOCK_END, SHIM_MARKER, SHIM_POLICIES, SUPPORTED_SHELLS,
|
|
11
|
+
installShellBootstrap, locateBlock, planShellBootstrap, renderCommandShim, renderPathBlock,
|
|
12
|
+
shellLiteral, uninstallShellBootstrap, validateShimEntry } from "./shell-bootstrap.mjs";
|
|
13
|
+
export { LIVE_POLICIES, describeActivation, describeDeactivation, livePolicyOf, rcFileFor,
|
|
14
|
+
resolveExecutable, shellOf, shimDirFor } from "./native-activation.mjs";
|