agents-can-communicate 0.1.18 → 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 +87 -69
- package/SECURITY.md +31 -0
- package/bin/acc-bootstrap.mjs +56 -0
- package/bin/acc-claude-channel.mjs +177 -0
- package/bin/acc-hook.mjs +94 -12
- package/bin/acc-mcp.mjs +6 -2
- package/bin/acc.mjs +13 -3
- package/docs/ADAPTER_AUTHORING.md +204 -0
- package/docs/ARCHITECTURE.md +131 -0
- package/docs/CAPABILITIES.md +117 -214
- package/docs/CLI.md +164 -0
- package/docs/CONCEPTS.md +134 -0
- package/docs/CONFIGURATION.md +147 -0
- package/docs/DESIGN_DECISIONS.md +89 -0
- package/docs/GETTING_STARTED.md +145 -0
- package/docs/GLOSSARY.md +26 -0
- package/docs/HOW_IT_WORKS.md +277 -0
- package/docs/MCP.md +94 -0
- package/docs/PROTOCOL.md +200 -0
- package/docs/RELEASING.md +115 -0
- package/docs/SECURITY_MODEL.md +131 -0
- package/docs/TROUBLESHOOTING.md +108 -0
- package/docs/WHY_ACC.md +61 -0
- package/docs/index.md +44 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +228 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse-Edit.json +19 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse.json +17 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionEnd.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionStart.json +7 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/UserPromptSubmit.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +269 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.252.json +21 -0
- 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 +13 -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 +22 -22
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +45 -5
- 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 +150 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/PreToolUse.json +14 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionEnd.json +7 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionStart.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/UserPromptSubmit.json +10 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +199 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.0.json +21 -0
- 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 +11 -2
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +22 -22
- package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +54 -12
- 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 +68 -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/extension/skills/acc/SKILL.md +22 -22
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeAgent-0.57.0.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool-0.57.0.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool-shell-0.57.0.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionEnd-0.57.0.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionStart-0.57.0.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/certification-provenance.json +293 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +8 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +31 -13
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +4 -2
- package/node_modules/@agents-can-communicate/adapter-grok/certification.json +3 -0
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +2 -1
- package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +22 -22
- package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +11 -9
- package/node_modules/@agents-can-communicate/adapter-kimi/certification.json +52 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Bash.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Write.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionHeartbeat.json +7 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionStart.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/UserPromptSubmit.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/certification-provenance.json +66 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +8 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +22 -22
- package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +7 -3
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +52 -18
- package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +158 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +36 -17
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +9 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +7 -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 +28 -4
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/args.mjs +12 -31
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +70 -5
- package/node_modules/@agents-can-communicate/cli/src/help.mjs +2 -5
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +111 -12
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +100 -121
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/attention.mjs +106 -0
- package/node_modules/@agents-can-communicate/core/src/conversations.mjs +276 -0
- package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +131 -0
- package/node_modules/@agents-can-communicate/core/src/finish-retries.mjs +97 -0
- package/node_modules/@agents-can-communicate/core/src/inbox.mjs +91 -107
- package/node_modules/@agents-can-communicate/core/src/index.mjs +3 -3
- package/node_modules/@agents-can-communicate/core/src/intents.mjs +0 -1
- package/node_modules/@agents-can-communicate/core/src/ports.mjs +2 -1
- package/node_modules/@agents-can-communicate/core/src/receipts.mjs +109 -0
- package/node_modules/@agents-can-communicate/core/src/service.mjs +21 -10
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +22 -20
- package/node_modules/@agents-can-communicate/core/src/status.mjs +11 -9
- package/node_modules/@agents-can-communicate/core/src/sync.mjs +3 -294
- package/node_modules/@agents-can-communicate/delivery-router/package.json +12 -0
- package/node_modules/@agents-can-communicate/delivery-router/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +131 -0
- package/node_modules/@agents-can-communicate/hook-runner/package.json +4 -2
- package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +20 -0
- 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 +190 -105
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +70 -10
- package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +144 -0
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +89 -5
- package/node_modules/@agents-can-communicate/installer/src/index.mjs +10 -2
- package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +161 -0
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +112 -12
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +54 -2
- 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/mcp-server/src/input-validator.mjs +79 -0
- package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +23 -28
- package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +102 -72
- package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +54 -97
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/config.mjs +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/conversations.mjs +64 -0
- package/node_modules/@agents-can-communicate/protocol/src/fields.mjs +17 -0
- package/node_modules/@agents-can-communicate/protocol/src/index.mjs +4 -1
- package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +64 -90
- package/node_modules/@agents-can-communicate/protocol/src/states.mjs +13 -40
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/active-journal.mjs +230 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +77 -28
- package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +83 -35
- package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +112 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +18 -8
- package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +68 -26
- package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +113 -35
- package/package.json +20 -1
- package/node_modules/@agents-can-communicate/core/src/communication.mjs +0 -334
- package/node_modules/@agents-can-communicate/core/src/message-signals.mjs +0 -41
- package/node_modules/@agents-can-communicate/core/src/notify.mjs +0 -95
- package/node_modules/@agents-can-communicate/core/src/tasks.mjs +0 -244
- package/node_modules/@agents-can-communicate/core/src/workstreams.mjs +0 -109
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
|
|
4
|
+
import { effectiveCapabilities, evaluateNativeEligibility, validateNativeActivationPlan }
|
|
5
|
+
from "@agents-can-communicate/adapter-sdk";
|
|
6
|
+
|
|
7
|
+
import { resolveExecutable, shellOf, shimDirFor } from "./native-activation.mjs";
|
|
8
|
+
|
|
4
9
|
const run = promisify(execFile);
|
|
5
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
|
+
|
|
6
17
|
const DEFAULT_PROBE_TIMEOUT_MS = 3_000;
|
|
7
18
|
|
|
8
19
|
// Clients print their version in their own shape: "codex-cli 0.147.0", a bare
|
|
9
20
|
// "0.36.1", a banner with the number somewhere inside. The number is extracted
|
|
10
21
|
// where it can be, and the raw line is kept either way - "present, version
|
|
11
22
|
// unreadable" is a real state and hiding it would make the client look absent.
|
|
12
|
-
const VERSION =
|
|
23
|
+
const VERSION = /(?:^|[^0-9A-Za-z])v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)(?:\b|$)/;
|
|
13
24
|
|
|
14
25
|
/**
|
|
15
26
|
* Ask the operating system what a client reports as its version.
|
|
@@ -35,8 +46,65 @@ const withTimeout = (work, ms, label) => new Promise((resolve, reject) => {
|
|
|
35
46
|
* exactly the case someone is running this command to find out about, and
|
|
36
47
|
* letting it throw would hide the other three behind it.
|
|
37
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
|
+
|
|
38
104
|
export async function detectInstallation({ adapters, context, probe = spawnProbe,
|
|
39
|
-
probeTimeoutMs = DEFAULT_PROBE_TIMEOUT_MS
|
|
105
|
+
probeTimeoutMs = DEFAULT_PROBE_TIMEOUT_MS,
|
|
106
|
+
platform = `${process.platform}-${process.arch}`,
|
|
107
|
+
pathEnv = context?.env?.PATH ?? process.env.PATH ?? "" }) {
|
|
40
108
|
const entries = await Promise.all([...adapters]
|
|
41
109
|
// Ordered by id so two runs can be diffed, and so a plan built from this is
|
|
42
110
|
// deterministic rather than dependent on registry order.
|
|
@@ -44,8 +112,8 @@ export async function detectInstallation({ adapters, context, probe = spawnProbe
|
|
|
44
112
|
.map(async adapter => {
|
|
45
113
|
const entry = { adapterId: adapter.id, displayName: adapter.displayName,
|
|
46
114
|
present: false, version: null, versionOutput: null, installed: false,
|
|
47
|
-
diagnostics: [], needsAction: [], capabilities: adapter
|
|
48
|
-
error: null };
|
|
115
|
+
diagnostics: [], needsAction: [], capabilities: effectiveCapabilities(adapter),
|
|
116
|
+
deliveryDiagnostic: null, error: null };
|
|
49
117
|
|
|
50
118
|
try {
|
|
51
119
|
const output = await withTimeout(
|
|
@@ -62,10 +130,16 @@ export async function detectInstallation({ adapters, context, probe = spawnProbe
|
|
|
62
130
|
} catch (error) {
|
|
63
131
|
entry.error = error.message;
|
|
64
132
|
}
|
|
133
|
+
entry.capabilities = effectiveCapabilities(adapter,
|
|
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");
|
|
65
139
|
|
|
66
140
|
try {
|
|
67
141
|
const detected = await adapter.detect(context);
|
|
68
|
-
entry.diagnostics = detected.diagnostics ?? [];
|
|
142
|
+
entry.diagnostics = [...(detected.diagnostics ?? [])];
|
|
69
143
|
// What a person has to do, as opposed to what is true. Adapters that
|
|
70
144
|
// have nothing to ask for say nothing.
|
|
71
145
|
entry.needsAction = detected.needsAction ?? [];
|
|
@@ -77,6 +151,16 @@ export async function detectInstallation({ adapters, context, probe = spawnProbe
|
|
|
77
151
|
} catch (error) {
|
|
78
152
|
entry.error = entry.error ?? error.message;
|
|
79
153
|
}
|
|
154
|
+
if (entry.nativeDelivery.state !== "eligible"
|
|
155
|
+
&& typeof adapter.deliveryFallback?.diagnostic === "string") {
|
|
156
|
+
const nextTurnDowngraded = adapter.capabilities?.delivery?.nextTurn === true
|
|
157
|
+
&& entry.capabilities?.delivery?.nextTurn !== true;
|
|
158
|
+
entry.deliveryDiagnostic = nextTurnDowngraded
|
|
159
|
+
? `${adapter.displayName} ${entry.version ?? "unknown version"} has no certified `
|
|
160
|
+
+ `next-turn delivery on ${platform}; ${adapter.deliveryFallback.diagnostic}`
|
|
161
|
+
: adapter.deliveryFallback.diagnostic;
|
|
162
|
+
entry.diagnostics.push(entry.deliveryDiagnostic);
|
|
163
|
+
}
|
|
80
164
|
return entry;
|
|
81
165
|
}));
|
|
82
166
|
return entries;
|
|
@@ -2,5 +2,13 @@
|
|
|
2
2
|
export { detectInstallation, spawnProbe } from "./detect.mjs";
|
|
3
3
|
export { planInstallation } from "./plan.mjs";
|
|
4
4
|
export { applyPlan } from "./apply.mjs";
|
|
5
|
-
export { fingerprint, loadOwnership,
|
|
6
|
-
verifyOwned }
|
|
5
|
+
export { finalizeRemoval, fingerprint, loadOwnership, missingArtifactParents, recordInstall,
|
|
6
|
+
removeEmptyOwnedDirectories, removeOwned, removeOwnedArtifacts, treeFingerprint, verifyOwned }
|
|
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";
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { access, constants } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { defaultBootstrap } from "@agents-can-communicate/adapter-sdk";
|
|
6
|
+
|
|
7
|
+
import { installShellBootstrap, planShellBootstrap, uninstallShellBootstrap }
|
|
8
|
+
from "./shell-bootstrap.mjs";
|
|
9
|
+
|
|
10
|
+
// The installer's side of a native activation: which owned mechanisms an
|
|
11
|
+
// eligible adapter asked for, how they are applied in a fixed order, what is
|
|
12
|
+
// recorded so uninstall removes only ACC's bytes, and how a recorded
|
|
13
|
+
// activation is taken back. Adapter-owned native config is written by the
|
|
14
|
+
// adapter's own install; a vendor service is started only here, only during
|
|
15
|
+
// apply, and only when it did not already exist.
|
|
16
|
+
|
|
17
|
+
export const LIVE_POLICIES = Object.freeze(["off", "actionable", "all"]);
|
|
18
|
+
const MECHANISM_ORDER = ["native-config", "native-service", "shell-bootstrap"];
|
|
19
|
+
const SERVICE_TIMEOUT_MS = 15_000;
|
|
20
|
+
|
|
21
|
+
export const livePolicyOf = install => (LIVE_POLICIES.includes(install?.nativeActivation?.livePolicy)
|
|
22
|
+
? install.nativeActivation.livePolicy : "off");
|
|
23
|
+
|
|
24
|
+
export const shellOf = env => {
|
|
25
|
+
const shell = env?.SHELL;
|
|
26
|
+
return typeof shell === "string" && shell !== "" ? path.basename(shell) : null;
|
|
27
|
+
};
|
|
28
|
+
export const shimDirFor = stateRoot => path.join(stateRoot, "bin");
|
|
29
|
+
// `.zshrc`, deliberately, and that makes the shim interactive-only: zsh reads
|
|
30
|
+
// this file for interactive shells and not for `zsh -lc` or a script. An
|
|
31
|
+
// interactive launch is where a client session comes from, and putting the PATH
|
|
32
|
+
// entry somewhere every shell reads would put ACC in front of a vendor command
|
|
33
|
+
// in scripts and CI that never asked for it. Worth knowing when checking an
|
|
34
|
+
// install: a non-interactive shell resolving the vendor binary directly is this
|
|
35
|
+
// choice working, not a broken bootstrap.
|
|
36
|
+
export const rcFileFor = (home, shell) => (shell === "zsh" && typeof home === "string"
|
|
37
|
+
? path.join(home, ".zshrc") : null);
|
|
38
|
+
|
|
39
|
+
// The vendor executable a shim will exec: the first executable on PATH that is
|
|
40
|
+
// not ACC's own shim directory, so a shim never resolves itself.
|
|
41
|
+
export async function resolveExecutable(command, { pathEnv = "", exclude = [] } = {}) {
|
|
42
|
+
const excluded = exclude.filter(Boolean).map(directory => path.resolve(directory));
|
|
43
|
+
for (const directory of String(pathEnv).split(path.delimiter).filter(Boolean)) {
|
|
44
|
+
if (excluded.includes(path.resolve(directory))) continue;
|
|
45
|
+
const candidate = path.join(directory, command);
|
|
46
|
+
try {
|
|
47
|
+
await access(candidate, constants.X_OK);
|
|
48
|
+
return candidate;
|
|
49
|
+
} catch {
|
|
50
|
+
// not here; keep walking PATH
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const defaultExec = (executable, args) => new Promise((resolve, reject) => {
|
|
57
|
+
execFile(executable, args, { timeout: SERVICE_TIMEOUT_MS, windowsHide: true },
|
|
58
|
+
error => (error === null ? resolve() : reject(error)));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const ordered = mechanisms => [...mechanisms].sort((left, right) =>
|
|
62
|
+
MECHANISM_ORDER.indexOf(left.kind) - MECHANISM_ORDER.indexOf(right.kind));
|
|
63
|
+
|
|
64
|
+
const renderCommand = command => [command.executable, ...command.args].join(" ");
|
|
65
|
+
|
|
66
|
+
/** What an activation would touch, in the operator's words. */
|
|
67
|
+
export function describeActivation(activation) {
|
|
68
|
+
const lines = [];
|
|
69
|
+
for (const mechanism of ordered(activation.mechanisms)) {
|
|
70
|
+
if (mechanism.kind === "shell-bootstrap") {
|
|
71
|
+
lines.push(`create shim ${path.join(activation.shimDir, mechanism.command)} for `
|
|
72
|
+
+ `${mechanism.command} (${activation.livePolicy} live delivery)`);
|
|
73
|
+
lines.push(`add a PATH block to ${activation.rcFile}`);
|
|
74
|
+
} else if (mechanism.kind === "native-config") {
|
|
75
|
+
lines.push(`write native config ${mechanism.artifactIds.join(", ")}`);
|
|
76
|
+
} else if (mechanism.preExisting) {
|
|
77
|
+
lines.push(`use the existing ${mechanism.serviceId} service`);
|
|
78
|
+
} else if (mechanism.applyCommand !== null) {
|
|
79
|
+
lines.push(`start the ${mechanism.serviceId} service: ${renderCommand(mechanism.applyCommand)}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return lines;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function describeDeactivation(nativeActivation) {
|
|
86
|
+
const lines = [];
|
|
87
|
+
for (const mechanism of nativeActivation?.mechanisms ?? []) {
|
|
88
|
+
if (mechanism.kind === "shell-bootstrap") {
|
|
89
|
+
for (const file of mechanism.ownedFiles) lines.push(`remove shim ${file.path}`);
|
|
90
|
+
lines.push(`remove the PATH block from ${mechanism.rcFile.path} once no ACC shim remains`);
|
|
91
|
+
} else if (mechanism.kind === "native-service") {
|
|
92
|
+
lines.push(mechanism.createdByAcc && mechanism.teardownCommand !== null
|
|
93
|
+
? `stop the ${mechanism.serviceId} service: ${renderCommand(mechanism.teardownCommand)}`
|
|
94
|
+
: `leave the ${mechanism.serviceId} service in place (${mechanism.createdByAcc
|
|
95
|
+
? "no vendor teardown exists" : "it existed before ACC"})`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return lines;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function applyNativeActivation({ adapter, activation, dataHome,
|
|
102
|
+
node = process.execPath, bootstrap = defaultBootstrap(), exec = defaultExec }) {
|
|
103
|
+
const record = { livePolicy: activation.livePolicy,
|
|
104
|
+
protocolContract: activation.protocolContract, mechanisms: [] };
|
|
105
|
+
let shell = null;
|
|
106
|
+
try {
|
|
107
|
+
for (const mechanism of ordered(activation.mechanisms)) {
|
|
108
|
+
if (mechanism.kind === "native-config") {
|
|
109
|
+
record.mechanisms.push({ kind: mechanism.kind, artifactIds: [...mechanism.artifactIds] });
|
|
110
|
+
} else if (mechanism.kind === "native-service") {
|
|
111
|
+
let createdByAcc = false;
|
|
112
|
+
if (!mechanism.preExisting && mechanism.applyCommand !== null) {
|
|
113
|
+
await exec(mechanism.applyCommand.executable, mechanism.applyCommand.args);
|
|
114
|
+
createdByAcc = true;
|
|
115
|
+
}
|
|
116
|
+
record.mechanisms.push({ kind: mechanism.kind, serviceId: mechanism.serviceId,
|
|
117
|
+
createdByAcc, teardownCommand: mechanism.teardownCommand });
|
|
118
|
+
} else {
|
|
119
|
+
const plan = planShellBootstrap({ shell: activation.shell, rcFile: activation.rcFile,
|
|
120
|
+
shimDir: activation.shimDir, runtime: { node, bootstrap, dataHome },
|
|
121
|
+
entries: [{ adapterId: adapter.id, command: mechanism.command,
|
|
122
|
+
realExecutable: mechanism.realExecutable, prefixArgs: mechanism.prefixArgs,
|
|
123
|
+
livePolicy: activation.livePolicy }] });
|
|
124
|
+
const result = await installShellBootstrap({ plan });
|
|
125
|
+
if (!result.ok) throw new Error(`shell bootstrap refused: ${result.reasonCode}`);
|
|
126
|
+
shell = result;
|
|
127
|
+
record.mechanisms.push({ kind: mechanism.kind, shimDir: activation.shimDir,
|
|
128
|
+
ownedFiles: result.shims.map(shim => ({ path: shim.path, sha256: shim.sha256 })),
|
|
129
|
+
rcFile: result.rcFile });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { nativeActivation: record, appendedRcBlock: shell?.rcFile.appended === true };
|
|
133
|
+
} catch (error) {
|
|
134
|
+
// Only bytes this operation wrote are taken back; a pre-existing service
|
|
135
|
+
// or a user's own file is never touched on the way out.
|
|
136
|
+
if (shell !== null) {
|
|
137
|
+
await uninstallShellBootstrap({ ownership: { shims: shell.shims, shimDir: activation.shimDir,
|
|
138
|
+
rcFile: shell.rcFile } }).catch(() => null);
|
|
139
|
+
}
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function deactivateNative({ nativeActivation, exec = defaultExec }) {
|
|
145
|
+
const report = { shell: null, services: [] };
|
|
146
|
+
for (const mechanism of nativeActivation?.mechanisms ?? []) {
|
|
147
|
+
if (mechanism.kind === "shell-bootstrap") {
|
|
148
|
+
report.shell = await uninstallShellBootstrap({ ownership: { shims: mechanism.ownedFiles,
|
|
149
|
+
shimDir: mechanism.shimDir, rcFile: mechanism.rcFile } });
|
|
150
|
+
} else if (mechanism.kind === "native-service") {
|
|
151
|
+
if (mechanism.createdByAcc && mechanism.teardownCommand !== null) {
|
|
152
|
+
await exec(mechanism.teardownCommand.executable, mechanism.teardownCommand.args);
|
|
153
|
+
report.services.push({ serviceId: mechanism.serviceId, outcome: "stopped" });
|
|
154
|
+
} else {
|
|
155
|
+
report.services.push({ serviceId: mechanism.serviceId,
|
|
156
|
+
outcome: mechanism.createdByAcc ? "retained_no_teardown" : "retained_pre_existing" });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return report;
|
|
161
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdir, readdir, readFile, rename, rm, writeFile }
|
|
2
|
+
import { lstat, mkdir, readdir, readFile, rename, rm, rmdir, writeFile }
|
|
3
|
+
from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
|
|
5
6
|
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
@@ -109,7 +110,7 @@ async function saveOwnership({ dataHome, record }) {
|
|
|
109
110
|
* runtime, and leaves the bundle inside the client exactly where it was.
|
|
110
111
|
*/
|
|
111
112
|
export async function recordInstall({ dataHome, adapterId, version, accVersion = null,
|
|
112
|
-
artifacts }) {
|
|
113
|
+
artifacts, createdDirectories = [], nativeActivation = null }) {
|
|
113
114
|
const stamped = await Promise.all(artifacts.map(async artifact => ({
|
|
114
115
|
path: artifact.path,
|
|
115
116
|
kind: artifact.kind ?? "file",
|
|
@@ -118,14 +119,103 @@ export async function recordInstall({ dataHome, adapterId, version, accVersion =
|
|
|
118
119
|
sha256: artifact.kind === "merge" ? null : await fingerprintFor(artifact),
|
|
119
120
|
})));
|
|
120
121
|
const record = await loadOwnership({ dataHome });
|
|
122
|
+
const previous = record.installs.find(install => install.adapterId === adapterId);
|
|
123
|
+
const directories = [...new Set([
|
|
124
|
+
...(previous?.createdDirectories ?? []), ...createdDirectories,
|
|
125
|
+
])].sort((left, right) => left.split(path.sep).length - right.split(path.sep).length
|
|
126
|
+
|| left.localeCompare(right));
|
|
121
127
|
await saveOwnership({ dataHome, record: { schemaVersion: SCHEMA_VERSION,
|
|
122
128
|
installs: [...record.installs.filter(install => install.adapterId !== adapterId),
|
|
123
|
-
{ adapterId, version, accVersion, artifacts: stamped
|
|
129
|
+
{ adapterId, version, accVersion, artifacts: stamped,
|
|
130
|
+
...(directories.length === 0 ? {} : { createdDirectories: directories }),
|
|
131
|
+
// Present only for a consented native activation. A record without it
|
|
132
|
+
// - every 0.2 install - reads as live policy off and is never rewritten
|
|
133
|
+
// merely to add the field.
|
|
134
|
+
...(nativeActivation === null ? {} : { nativeActivation }) }] } });
|
|
124
135
|
}
|
|
125
136
|
|
|
126
137
|
const installFor = (record, adapterId) =>
|
|
127
138
|
record.installs.find(install => install.adapterId === adapterId) ?? null;
|
|
128
139
|
|
|
140
|
+
const inside = (home, candidate) => {
|
|
141
|
+
const relative = path.relative(home, candidate);
|
|
142
|
+
return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`)
|
|
143
|
+
&& !path.isAbsolute(relative);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
async function hasSymlinkAncestor(home, candidate) {
|
|
147
|
+
let current = candidate;
|
|
148
|
+
while (inside(home, current)) {
|
|
149
|
+
const stat = await lstat(current).catch(error => {
|
|
150
|
+
if (error.code === "ENOENT") return null;
|
|
151
|
+
throw error;
|
|
152
|
+
});
|
|
153
|
+
if (stat?.isSymbolicLink()) return true;
|
|
154
|
+
current = path.dirname(current);
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Return planned artifact parents that do not yet exist under the client home. */
|
|
160
|
+
export async function missingArtifactParents({ home, artifacts }) {
|
|
161
|
+
if (typeof home !== "string") return [];
|
|
162
|
+
const root = path.resolve(home);
|
|
163
|
+
const missing = new Set();
|
|
164
|
+
for (const artifact of artifacts) {
|
|
165
|
+
let directory = path.dirname(path.resolve(artifact.path));
|
|
166
|
+
while (inside(root, directory)) {
|
|
167
|
+
try {
|
|
168
|
+
await lstat(directory);
|
|
169
|
+
break;
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (error.code !== "ENOENT") throw error;
|
|
172
|
+
missing.add(directory);
|
|
173
|
+
directory = path.dirname(directory);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return [...missing].sort((left, right) =>
|
|
178
|
+
left.split(path.sep).length - right.split(path.sep).length || left.localeCompare(right));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Remove recorded parents deepest-first, but only while each remains an empty directory. */
|
|
182
|
+
export async function removeEmptyOwnedDirectories({ home, directories = [] }) {
|
|
183
|
+
const result = { removed: [], kept: [], missing: [] };
|
|
184
|
+
const root = typeof home === "string" ? path.resolve(home) : null;
|
|
185
|
+
const ordered = [...new Set(directories)].sort((left, right) =>
|
|
186
|
+
right.split(path.sep).length - left.split(path.sep).length || left.localeCompare(right));
|
|
187
|
+
for (const directory of ordered) {
|
|
188
|
+
if (root === null || !inside(root, path.resolve(directory))) {
|
|
189
|
+
result.kept.push(directory);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
let stat;
|
|
193
|
+
try {
|
|
194
|
+
stat = await lstat(directory);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (error.code === "ENOENT") { result.missing.push(directory); continue; }
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
200
|
+
|| await hasSymlinkAncestor(root, path.dirname(directory))) {
|
|
201
|
+
result.kept.push(directory);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
await rmdir(directory);
|
|
206
|
+
result.removed.push(directory);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (["ENOTEMPTY", "EEXIST"].includes(error.code)) {
|
|
209
|
+
result.kept.push(directory);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (error.code === "ENOENT") { result.missing.push(directory); continue; }
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
|
|
129
219
|
/** Compare what was written against what is there now. Read-only. */
|
|
130
220
|
export async function verifyOwned({ dataHome, adapterId }) {
|
|
131
221
|
const install = installFor(await loadOwnership({ dataHome }), adapterId);
|
|
@@ -141,17 +231,12 @@ export async function verifyOwned({ dataHome, adapterId }) {
|
|
|
141
231
|
return result;
|
|
142
232
|
}
|
|
143
233
|
|
|
144
|
-
/**
|
|
145
|
-
|
|
146
|
-
*
|
|
147
|
-
* A modified file is kept and reported. A merge artifact is never deleted at
|
|
148
|
-
* all: the user owns that file and ACC owns some entries inside it, which is the
|
|
149
|
-
* adapter's own uninstall to unpick because it knows the format.
|
|
150
|
-
*/
|
|
151
|
-
export async function removeOwned({ dataHome, adapterId }) {
|
|
234
|
+
/** Remove owned artifacts while retaining the record as retry authority. */
|
|
235
|
+
export async function removeOwnedArtifacts({ dataHome, adapterId }) {
|
|
152
236
|
const record = await loadOwnership({ dataHome });
|
|
153
237
|
const install = installFor(record, adapterId);
|
|
154
|
-
const result = { adapterId, removed: [], kept: [], missing: [], delegated: []
|
|
238
|
+
const result = { adapterId, removed: [], kept: [], missing: [], delegated: [],
|
|
239
|
+
createdDirectories: install?.createdDirectories ?? [] };
|
|
155
240
|
if (install === null) return result;
|
|
156
241
|
|
|
157
242
|
for (const artifact of install.artifacts) {
|
|
@@ -163,7 +248,22 @@ export async function removeOwned({ dataHome, adapterId }) {
|
|
|
163
248
|
result.removed.push(artifact.path);
|
|
164
249
|
}
|
|
165
250
|
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Forget one install only after every adapter-owned cleanup step succeeded. */
|
|
255
|
+
export async function finalizeRemoval({ dataHome, adapterId }) {
|
|
256
|
+
const record = await loadOwnership({ dataHome });
|
|
257
|
+
if (installFor(record, adapterId) === null) return false;
|
|
258
|
+
|
|
166
259
|
await saveOwnership({ dataHome, record: { schemaVersion: SCHEMA_VERSION,
|
|
167
260
|
installs: record.installs.filter(entry => entry.adapterId !== adapterId) } });
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Remove and finalize for callers that perform no delegated adapter cleanup. */
|
|
265
|
+
export async function removeOwned(options) {
|
|
266
|
+
const result = await removeOwnedArtifacts(options);
|
|
267
|
+
await finalizeRemoval(options);
|
|
168
268
|
return result;
|
|
169
269
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
2
2
|
|
|
3
|
+
import { LIVE_POLICIES, describeActivation, describeDeactivation, rcFileFor, shimDirFor }
|
|
4
|
+
from "./native-activation.mjs";
|
|
5
|
+
|
|
3
6
|
/**
|
|
4
7
|
* Turn a detection report into exactly what would happen.
|
|
5
8
|
*
|
|
@@ -8,11 +11,27 @@ import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
|
8
11
|
* it previews is a decoration, and the operator would find out only afterwards.
|
|
9
12
|
*/
|
|
10
13
|
export function planInstallation({ adapters, detected, context, action = "install",
|
|
11
|
-
recorded = [], accVersion = null, allowDowngrade = false, requested = []
|
|
14
|
+
recorded = [], accVersion = null, allowDowngrade = false, requested = [],
|
|
15
|
+
deliveryByAdapter = {} }) {
|
|
12
16
|
if (!["install", "uninstall"].includes(action)) {
|
|
13
17
|
throw new AccError(EXIT.USAGE, `unknown installation action: ${action}`, { action });
|
|
14
18
|
}
|
|
15
19
|
const byId = new Map(adapters.map(adapter => [adapter.id, adapter]));
|
|
20
|
+
// One explicit answer per selected client. A missing entry is off; a policy
|
|
21
|
+
// for a client that is not part of this run is a mistake to say out loud,
|
|
22
|
+
// not something to store for later.
|
|
23
|
+
if (deliveryByAdapter === null || typeof deliveryByAdapter !== "object") {
|
|
24
|
+
throw new AccError(EXIT.USAGE, "deliveryByAdapter must map adapter ids to policies");
|
|
25
|
+
}
|
|
26
|
+
for (const [adapterId, policy] of Object.entries(deliveryByAdapter)) {
|
|
27
|
+
if (!byId.has(adapterId)) {
|
|
28
|
+
throw new AccError(EXIT.USAGE,
|
|
29
|
+
`delivery policy names ${adapterId}, which is not part of this install`, { adapterId });
|
|
30
|
+
}
|
|
31
|
+
if (!LIVE_POLICIES.includes(policy)) {
|
|
32
|
+
throw new AccError(EXIT.USAGE, `unknown delivery policy: ${policy}`, { adapterId, policy });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
16
35
|
// What ACC recorded writing, by client. For an uninstall this is the
|
|
17
36
|
// authority rather than detection: the record is the only account of what was
|
|
18
37
|
// written, and a client's configuration directory outlives the client.
|
|
@@ -82,7 +101,32 @@ export function planInstallation({ adapters, detected, context, action = "instal
|
|
|
82
101
|
// From the record when the client is gone, because that is what was written
|
|
83
102
|
// and so what will be removed. Asking the adapter instead would describe an
|
|
84
103
|
// install for a machine this one no longer is.
|
|
85
|
-
const
|
|
104
|
+
const delivery = deliveryByAdapter[entry.adapterId] ?? "off";
|
|
105
|
+
const native = entry.nativeDelivery ?? null;
|
|
106
|
+
const liveDeliverySupported = native?.state === "eligible"
|
|
107
|
+
&& native.activationPlan?.eligible === true;
|
|
108
|
+
const effectiveLivePolicy = liveDeliverySupported ? delivery : "off";
|
|
109
|
+
const deliveryDiagnostic = action === "install" && delivery !== "off"
|
|
110
|
+
&& !liveDeliverySupported
|
|
111
|
+
? entry.deliveryDiagnostic ?? adapter.deliveryFallback?.diagnostic
|
|
112
|
+
?? `${adapter.displayName ?? adapter.id} cannot receive native delivery `
|
|
113
|
+
+ `(${native?.reasonCode ?? "native_delivery_unsupported"}); durable fallback remains active`
|
|
114
|
+
: null;
|
|
115
|
+
const installContext = { ...context, requestedLivePolicy: delivery,
|
|
116
|
+
livePolicy: effectiveLivePolicy };
|
|
117
|
+
// A consented activation that this run keeps, activates, or takes back.
|
|
118
|
+
// Only an explicit off or an uninstall removes one; an absent record never
|
|
119
|
+
// creates one.
|
|
120
|
+
const previous = recordedById.get(entry.adapterId)?.nativeActivation ?? null;
|
|
121
|
+
const nativeActivation = action === "install" && effectiveLivePolicy !== "off"
|
|
122
|
+
? { livePolicy: effectiveLivePolicy, protocolContract: native.eligibility.protocolContract,
|
|
123
|
+
shell: context?.shell ?? null, rcFile: rcFileFor(context?.home, context?.shell),
|
|
124
|
+
shimDir: typeof context?.stateRoot === "string" ? shimDirFor(context.stateRoot) : null,
|
|
125
|
+
mechanisms: native.activationPlan.mechanisms }
|
|
126
|
+
: null;
|
|
127
|
+
const deactivation = previous !== null
|
|
128
|
+
&& (action === "uninstall" || effectiveLivePolicy === "off") ? previous : null;
|
|
129
|
+
const artifacts = (record?.artifacts ?? adapter.planInstall(installContext))
|
|
86
130
|
.map(artifact => ({ path: artifact.path, kind: artifact.kind ?? "file" }))
|
|
87
131
|
.sort((a, b) => a.path.localeCompare(b.path));
|
|
88
132
|
|
|
@@ -96,16 +140,24 @@ export function planInstallation({ adapters, detected, context, action = "instal
|
|
|
96
140
|
// to be inferred from a client version that is null.
|
|
97
141
|
clientPresent: entry.present === true,
|
|
98
142
|
alreadyInstalled: entry.installed === true,
|
|
143
|
+
livePolicy: delivery,
|
|
144
|
+
effectiveLivePolicy,
|
|
145
|
+
...(deliveryDiagnostic === null ? {} : { deliveryDiagnostic }),
|
|
146
|
+
...(nativeActivation === null ? {} : { nativeActivation }),
|
|
147
|
+
...(deactivation === null ? {} : { deactivation }),
|
|
99
148
|
artifacts,
|
|
100
149
|
// Said in the operator's terms, not in paths: which files ACC creates
|
|
101
150
|
// outright and which belong to the user and are only edited.
|
|
102
151
|
summary: [
|
|
103
152
|
...(entry.present ? [] : [`${adapter.displayName ?? adapter.id} is no longer on `
|
|
104
153
|
+ "this machine; removing what ACC recorded writing"]),
|
|
154
|
+
...(deliveryDiagnostic === null ? [] : [deliveryDiagnostic]),
|
|
105
155
|
...artifacts.filter(a => a.kind === "tree")
|
|
106
156
|
.map(a => `${action === "install" ? "create" : "remove"} ${a.path}`),
|
|
107
157
|
...artifacts.filter(a => a.kind === "merge")
|
|
108
158
|
.map(a => `${action === "install" ? "add ACC entries to" : "remove ACC entries from"} ${a.path}`),
|
|
159
|
+
...(nativeActivation === null ? [] : describeActivation(nativeActivation)),
|
|
160
|
+
...(deactivation === null ? [] : describeDeactivation(deactivation)),
|
|
109
161
|
],
|
|
110
162
|
});
|
|
111
163
|
}
|