agents-can-communicate 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/bin/entrypoints/acc-bootstrap.mjs +6 -48
- package/bin/entrypoints/acc-claude-channel.mjs +47 -157
- package/bin/entrypoints/acc-mcp.mjs +0 -1
- package/bin/entrypoints/acc.mjs +0 -1
- package/docs/ADAPTER_AUTHORING.md +74 -24
- package/docs/ARCHITECTURE.md +31 -18
- package/docs/CAPABILITIES.md +38 -25
- package/docs/CLI.md +51 -13
- package/docs/CONCEPTS.md +5 -4
- package/docs/CONFIGURATION.md +18 -11
- package/docs/GETTING_STARTED.md +15 -12
- package/docs/GLOSSARY.md +2 -1
- package/docs/HOW_IT_WORKS.md +8 -4
- package/docs/PROTOCOL.md +25 -6
- package/docs/RELEASING.md +8 -5
- package/docs/SECURITY_MODEL.md +14 -1
- package/docs/TROUBLESHOOTING.md +66 -29
- package/docs/UPGRADING.md +49 -8
- package/docs/WHY_ACC.md +5 -3
- package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +2 -3
- package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +14 -73
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +29 -71
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.282-product-evidence.json +161 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.282.json +28 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/inbox-wake-2.1.282.json +44 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +5 -2
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +26 -30
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/hooks.mjs +6 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-contract.mjs +10 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-delivery.mjs +249 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-endpoint.mjs +166 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-registry.mjs +71 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-settings.mjs +109 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +25 -31
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +3 -4
- package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +10 -13
- package/node_modules/@agents-can-communicate/adapter-codex/src/live-permissions.mjs +5 -5
- package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance-host.mjs +46 -10
- package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance.mjs +5 -7
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +16 -12
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +12 -2
- package/node_modules/@agents-can-communicate/adapter-codex/src/service-setup.mjs +26 -9
- package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +12 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +2 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/src/events.mjs +16 -3
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +4 -3
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +4 -3
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-activation.mjs +4 -14
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +104 -84
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +8 -4
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +8 -6
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +8 -23
- package/node_modules/@agents-can-communicate/cli/src/install-delivery-consent.mjs +3 -4
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +24 -3
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +7 -43
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +5 -3
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/maintenance.mjs +9 -15
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/pins.mjs +13 -4
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/refresh.mjs +3 -4
- package/node_modules/@agents-can-communicate/cli/src/native-delivery-status.mjs +2 -2
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/receipts.mjs +17 -2
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +25 -2
- package/node_modules/@agents-can-communicate/core/src/status.mjs +31 -1
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/delivery-router/src/refresh-binding.mjs +2 -2
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +33 -48
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/client-pid.mjs +30 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/native-attempt.mjs +6 -8
- package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +27 -15
- package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +30 -14
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +4 -2
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +19 -5
- package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +0 -2
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +6 -11
- package/node_modules/@agents-can-communicate/installer/src/index.mjs +4 -7
- package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +23 -61
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +7 -1
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +15 -5
- package/node_modules/@agents-can-communicate/installer/src/shell-bootstrap.mjs +10 -133
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +6 -1
- package/package.json +1 -1
- package/bin/acc-bootstrap.mjs +0 -7
- package/bin/acc-claude-channel.mjs +0 -10
- package/bin/entrypoints/claude-channel-binding.mjs +0 -46
- package/bin/entrypoints/claude-channel-stdio.mjs +0 -29
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/.mcp.json +0 -8
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/channel.mjs +0 -381
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +0 -232
- package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +0 -150
|
@@ -8,27 +8,43 @@ const run = promisify(execFile);
|
|
|
8
8
|
const DEFAULT_TIMEOUT_MS = 1_000;
|
|
9
9
|
|
|
10
10
|
const LINE = /^\s*(\d+)\s+(\d+)\s+(.+?)\s*$/;
|
|
11
|
+
// A pid, then the whole rest of the line: a command line has spaces anywhere.
|
|
12
|
+
const ARGS_LINE = /^\s*(\d+)\s+(.+?)\s*$/;
|
|
13
|
+
// Command lines are long; measured at 231 KB for 1,137 processes on a desktop
|
|
14
|
+
// Mac, a quarter of execFile's default buffer.
|
|
15
|
+
const ARGS_BUFFER = 16 * 1024 * 1024;
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
|
-
* Every process on this machine, as pid -> parent and
|
|
18
|
+
* Every process on this machine, as pid -> parent, executable and, when it
|
|
19
|
+
* could be read, command line.
|
|
14
20
|
*
|
|
15
21
|
* Returns an empty map rather than throwing when the platform has no `ps`
|
|
16
22
|
* (Windows) or the call fails. An empty table resolves no client, which is the
|
|
17
|
-
* "nobody knows" answer the caller already handles.
|
|
23
|
+
* "nobody knows" answer the caller already handles. The command lines are read
|
|
24
|
+
* at the same time and within the same timeout; they only name a client that
|
|
25
|
+
* is a script, so when that read fails every entry keeps what it had before.
|
|
18
26
|
*/
|
|
19
27
|
export async function readProcessTable({ run: exec = run,
|
|
20
28
|
timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return new Map();
|
|
29
|
+
const options = { timeout: timeoutMs, killSignal: "SIGKILL" };
|
|
30
|
+
const [tree, commandLines] = await Promise.all([
|
|
31
|
+
exec("ps", ["-o", "pid=,ppid=,comm=", "-A"], options).then(({ stdout }) => stdout, () => null),
|
|
32
|
+
exec("ps", ["-A", "-o", "pid=,args="], { ...options, maxBuffer: ARGS_BUFFER })
|
|
33
|
+
.then(({ stdout }) => stdout, () => ""),
|
|
34
|
+
]);
|
|
35
|
+
if (typeof tree !== "string") return new Map();
|
|
36
|
+
const args = new Map();
|
|
37
|
+
for (const line of String(commandLines).split("\n")) {
|
|
38
|
+
const match = ARGS_LINE.exec(line);
|
|
39
|
+
if (match !== null) args.set(Number(match[1]), match[2]);
|
|
33
40
|
}
|
|
41
|
+
const table = new Map();
|
|
42
|
+
for (const line of tree.split("\n")) {
|
|
43
|
+
const match = LINE.exec(line);
|
|
44
|
+
if (match === null) continue;
|
|
45
|
+
const pid = Number(match[1]);
|
|
46
|
+
table.set(pid, { ppid: Number(match[2]), comm: match[3],
|
|
47
|
+
...(args.has(pid) ? { args: args.get(pid) } : {}) });
|
|
48
|
+
}
|
|
49
|
+
return table;
|
|
34
50
|
}
|
|
@@ -400,8 +400,10 @@ const HANDLERS = {
|
|
|
400
400
|
const facts = await runtimeFacts(import.meta.url);
|
|
401
401
|
await storeSessionBinding({ runtimeDir: paths.root, deadlineAt: deadline, harnessSessionId: event.sessionId,
|
|
402
402
|
accSessionId: binding.accSessionId, generation: binding.generation, ...facts });
|
|
403
|
+
// The pid this binding already knew keeps the refreshed pin reapable if
|
|
404
|
+
// the lookup below cannot find the client.
|
|
403
405
|
await writePin({ root: pinRoot, harnessSessionId: event.sessionId, runtimeRoot: facts.runtimeRoot,
|
|
404
|
-
version: facts.version, storeVersion: facts.storeVersion });
|
|
406
|
+
version: facts.version, storeVersion: facts.storeVersion, clientPid: binding.clientPid });
|
|
405
407
|
}
|
|
406
408
|
const clientVersion = await probeClientVersion(adapter,
|
|
407
409
|
{ timeoutMs: Math.max(1, Math.min(1_000, deadline - Date.now())) });
|
|
@@ -506,7 +508,7 @@ const HANDLERS = {
|
|
|
506
508
|
const owner = { sessionId: binding.accSessionId, generation: binding.generation };
|
|
507
509
|
const current = await context.service.locateSession(binding.accSessionId);
|
|
508
510
|
if (current?.record.generation === binding.generation && current.record.state === "open") {
|
|
509
|
-
await context.service.closeSession(owner);
|
|
511
|
+
await context.service.closeSession({ ...owner, endReason: event.endReason ?? null });
|
|
510
512
|
} else {
|
|
511
513
|
// Close may have committed before this hook died. Retire only the old
|
|
512
514
|
// endpoint and binding; do not close a replacement or repeat a close event.
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { readdir, rm } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
1
4
|
import { applyServiceSetup } from "./service-setup.mjs";
|
|
2
5
|
import { applyNativeActivation, deactivateNative } from "./native-activation.mjs";
|
|
3
6
|
import { finalizeRemoval, missingArtifactParents, recordInstall,
|
|
@@ -52,12 +55,10 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
|
|
|
52
55
|
retainedMechanisms = report.retainedMechanisms;
|
|
53
56
|
}
|
|
54
57
|
let native = operation.retainedNativeActivation ?? null;
|
|
55
|
-
let appendedRcBlock = false;
|
|
56
58
|
if (operation.nativeActivation !== undefined) {
|
|
57
|
-
const applied = await applyNativeActivation({
|
|
58
|
-
activation: operation.nativeActivation,
|
|
59
|
+
const applied = await applyNativeActivation({
|
|
60
|
+
activation: operation.nativeActivation, ...activation });
|
|
59
61
|
native = applied.nativeActivation;
|
|
60
|
-
appendedRcBlock = applied.appendedRcBlock;
|
|
61
62
|
}
|
|
62
63
|
if (retainedMechanisms.length > 0) {
|
|
63
64
|
native = { ...(native ?? operation.deactivation),
|
|
@@ -71,7 +72,7 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
|
|
|
71
72
|
artifacts: operation.artifacts, createdDirectories,
|
|
72
73
|
deliveryPolicy: operation.livePolicy, deliveryDecision: operation.deliveryDecision,
|
|
73
74
|
nativeActivation: native });
|
|
74
|
-
results.operations.push({ ...operation, applied: true,
|
|
75
|
+
results.operations.push({ ...operation, applied: true,
|
|
75
76
|
needsAction: outcome.needsAction ?? [],
|
|
76
77
|
changes: outcome.changes ?? [], diagnostics: [
|
|
77
78
|
...(operation.deliveryDiagnostic === undefined
|
|
@@ -118,6 +119,19 @@ export async function applyPlan({ plan, adapters, context, dataHome, dryRun = fa
|
|
|
118
119
|
results.failed.push({ adapterId: operation.adapterId, error: error.message });
|
|
119
120
|
}
|
|
120
121
|
}
|
|
122
|
+
// The eligibility cache a 0.7.x `claude` shim read before every launch. No
|
|
123
|
+
// shim reads it any more, and it holds nothing a user wrote.
|
|
124
|
+
if (!dryRun && typeof dataHome === "string") {
|
|
125
|
+
await rm(path.join(dataHome, "acc", "native-bootstrap"), { recursive: true, force: true })
|
|
126
|
+
.catch(() => {});
|
|
127
|
+
// A 0.7.x Claude Channel unlinked its own registration on a clean exit;
|
|
128
|
+
// one that crashed left it in its workspace, naming a dead socket.
|
|
129
|
+
const workspaces = path.join(dataHome, "acc", "workspaces");
|
|
130
|
+
for (const workspace of await readdir(workspaces).catch(() => [])) {
|
|
131
|
+
await rm(path.join(workspaces, workspace, "native", "claude"), { recursive: true, force: true })
|
|
132
|
+
.catch(() => {});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
121
135
|
return results;
|
|
122
136
|
}
|
|
123
137
|
|
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
// reasons; installation and doctor must not turn a version floor into readiness.
|
|
3
3
|
const REASONS = Object.freeze({
|
|
4
4
|
native_delivery_unsupported: "this adapter has no native delivery channel",
|
|
5
|
-
platform_not_captured: "native delivery is not verified on this platform",
|
|
6
5
|
version_unavailable: "the client version could not be verified",
|
|
7
|
-
prerelease_not_captured: "this client version is not a verified stable release",
|
|
8
6
|
below_minimum_version: "the client is below the native delivery minimum version",
|
|
9
7
|
known_bad_version: "this client version has a known native delivery failure",
|
|
10
8
|
native_endpoint_unavailable: "the client's local delivery service is unavailable",
|
|
@@ -5,7 +5,7 @@ import { capabilityEvidence, effectiveCapabilities, evaluateNativeEligibility,
|
|
|
5
5
|
validateNativeActivationPlan }
|
|
6
6
|
from "@agents-can-communicate/adapter-sdk";
|
|
7
7
|
|
|
8
|
-
import { resolveExecutable,
|
|
8
|
+
import { resolveExecutable, shimDirFor } from "./native-activation.mjs";
|
|
9
9
|
|
|
10
10
|
import { describeDeliveryFallback, describeNativeReason } from "./delivery-diagnostics.mjs";
|
|
11
11
|
|
|
@@ -13,9 +13,8 @@ const run = promisify(execFile);
|
|
|
13
13
|
|
|
14
14
|
// Reasons the client itself will not change within an install: unsupported,
|
|
15
15
|
// as opposed to degraded, which a repaired probe or shell may lift.
|
|
16
|
-
const STATIC_REASONS = new Set(["native_delivery_unsupported", "
|
|
17
|
-
"
|
|
18
|
-
"known_bad_version"]);
|
|
16
|
+
const STATIC_REASONS = new Set(["native_delivery_unsupported", "version_unavailable",
|
|
17
|
+
"below_minimum_version", "known_bad_version"]);
|
|
19
18
|
|
|
20
19
|
const DEFAULT_PROBE_TIMEOUT_MS = 3_000;
|
|
21
20
|
|
|
@@ -76,15 +75,14 @@ async function detectNative(adapter, entry, { context, platform, probeTimeoutMs,
|
|
|
76
75
|
}
|
|
77
76
|
try {
|
|
78
77
|
facts.eligibility = evaluateNativeEligibility(adapter,
|
|
79
|
-
{ clientVersion: entry.version,
|
|
78
|
+
{ clientVersion: entry.version, probe: facts.probe });
|
|
80
79
|
} catch {
|
|
81
80
|
facts.eligibility = { eligible: false, reasonCode: "feature_probe_failed" };
|
|
82
81
|
}
|
|
83
82
|
if (facts.eligibility.eligible !== true) {
|
|
84
83
|
const reasonCode = facts.eligibility.reasonCode ?? "feature_probe_failed";
|
|
85
84
|
// A stored policy can be consented to before a service/session exists.
|
|
86
|
-
// No activation is planned until the full probe passes
|
|
87
|
-
// clients still need their verified plan to disclose launch changes.
|
|
85
|
+
// No activation is planned until the full probe passes.
|
|
88
86
|
const consentAvailable = adapter.nativeDelivery.policySource === "installation-record"
|
|
89
87
|
&& ["native_endpoint_unavailable", "native_session_unavailable"].includes(reasonCode);
|
|
90
88
|
return STATIC_REASONS.has(reasonCode)
|
|
@@ -100,10 +98,6 @@ async function detectNative(adapter, entry, { context, platform, probeTimeoutMs,
|
|
|
100
98
|
return degraded("feature_probe_failed", facts);
|
|
101
99
|
}
|
|
102
100
|
if (!activationPlan.eligible) return degraded(activationPlan.reasonCode, facts);
|
|
103
|
-
const shell = context?.shell ?? null;
|
|
104
|
-
if (activationPlan.mechanisms.some(item => item.kind === "shell-bootstrap") && shell !== "zsh") {
|
|
105
|
-
return { ...degraded("unsupported_shell", facts), activationPlan };
|
|
106
|
-
}
|
|
107
101
|
return { state: "eligible", reasonCode: null, ...facts, activationPlan };
|
|
108
102
|
} catch {
|
|
109
103
|
return degraded("feature_probe_failed", { realExecutable: null, probe: null, eligibility: null });
|
|
@@ -165,6 +159,7 @@ export async function detectInstallation({ adapters, context, probe = spawnProbe
|
|
|
165
159
|
// on the machine down with it.
|
|
166
160
|
if (detected.blocked) entry.blocked = detected.blocked;
|
|
167
161
|
if (detected.outgoingDelivery) entry.outgoingDelivery = detected.outgoingDelivery;
|
|
162
|
+
if (detected.inboundDelivery) entry.inboundDelivery = detected.inboundDelivery;
|
|
168
163
|
if (detected.nativeSetup) entry.nativeSetup = detected.nativeSetup;
|
|
169
164
|
entry.diagnostics = [...(detected.diagnostics ?? [])];
|
|
170
165
|
// What a person has to do, as opposed to what is true. Adapters that
|
|
@@ -5,13 +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 {
|
|
9
|
-
|
|
10
|
-
export {
|
|
11
|
-
|
|
12
|
-
shellLiteral, uninstallShellBootstrap, validateShimEntry } from "./shell-bootstrap.mjs";
|
|
13
|
-
export { LIVE_POLICIES, describeActivation, describeDeactivation, livePolicyOf, rcFileFor,
|
|
14
|
-
resolveExecutable, shellOf, shimDirFor } from "./native-activation.mjs";
|
|
8
|
+
export { BLOCK_BEGIN, BLOCK_END, SHIM_MARKER, locateBlock, uninstallShellBootstrap }
|
|
9
|
+
from "./shell-bootstrap.mjs";
|
|
10
|
+
export { LIVE_POLICIES, describeActivation, describeDeactivation, livePolicyOf,
|
|
11
|
+
resolveExecutable, shimDirFor } from "./native-activation.mjs";
|
|
15
12
|
export { readInstalledLivePolicy, readInstalledLivePolicyState } from "./live-policy.mjs";
|
|
16
13
|
export { describeDeliveryFallback, describeInstallDelivery, describeNativeReason }
|
|
17
14
|
from "./delivery-diagnostics.mjs";
|
|
@@ -2,10 +2,7 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { access, constants } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
import { installShellBootstrap, planShellBootstrap, uninstallShellBootstrap }
|
|
8
|
-
from "./shell-bootstrap.mjs";
|
|
5
|
+
import { uninstallShellBootstrap } from "./shell-bootstrap.mjs";
|
|
9
6
|
import { LIVE_POLICIES, livePolicyOf } from "./live-policy.mjs";
|
|
10
7
|
|
|
11
8
|
// The installer's side of a native activation: which owned mechanisms an
|
|
@@ -16,26 +13,15 @@ import { LIVE_POLICIES, livePolicyOf } from "./live-policy.mjs";
|
|
|
16
13
|
// apply, and only when it did not already exist.
|
|
17
14
|
|
|
18
15
|
export { LIVE_POLICIES, livePolicyOf };
|
|
19
|
-
const MECHANISM_ORDER = ["native-config", "native-service"
|
|
16
|
+
const MECHANISM_ORDER = ["native-config", "native-service"];
|
|
20
17
|
const SERVICE_TIMEOUT_MS = 15_000;
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return typeof shell === "string" && shell !== "" ? path.basename(shell) : null;
|
|
25
|
-
};
|
|
19
|
+
// Where ACC 0.7.x put its `claude` shim. An edited shim is kept rather than
|
|
20
|
+
// deleted, so detection still has to look past this directory.
|
|
26
21
|
export const shimDirFor = stateRoot => path.join(stateRoot, "bin");
|
|
27
|
-
// `.zshrc`, deliberately, and that makes the shim interactive-only: zsh reads
|
|
28
|
-
// this file for interactive shells and not for `zsh -lc` or a script. An
|
|
29
|
-
// interactive launch is where a client session comes from, and putting the PATH
|
|
30
|
-
// entry somewhere every shell reads would put ACC in front of a vendor command
|
|
31
|
-
// in scripts and CI that never asked for it. Worth knowing when checking an
|
|
32
|
-
// install: a non-interactive shell resolving the vendor binary directly is this
|
|
33
|
-
// choice working, not a broken bootstrap.
|
|
34
|
-
export const rcFileFor = (home, shell) => (shell === "zsh" && typeof home === "string"
|
|
35
|
-
? path.join(home, ".zshrc") : null);
|
|
36
22
|
|
|
37
|
-
// The vendor executable
|
|
38
|
-
//
|
|
23
|
+
// The vendor executable to probe: the first executable on PATH outside the
|
|
24
|
+
// excluded directories, so a kept 0.7.x shim is never probed as the client.
|
|
39
25
|
export async function resolveExecutable(command, { pathEnv = "", exclude = [] } = {}) {
|
|
40
26
|
const excluded = exclude.filter(Boolean).map(directory => path.resolve(directory));
|
|
41
27
|
for (const directory of String(pathEnv).split(path.delimiter).filter(Boolean)) {
|
|
@@ -65,11 +51,7 @@ const renderCommand = command => [command.executable, ...command.args].join(" ")
|
|
|
65
51
|
export function describeActivation(activation) {
|
|
66
52
|
const lines = [];
|
|
67
53
|
for (const mechanism of ordered(activation.mechanisms)) {
|
|
68
|
-
if (mechanism.kind === "
|
|
69
|
-
lines.push(`create shim ${path.join(activation.shimDir, mechanism.command)} for `
|
|
70
|
-
+ `${mechanism.command} (${activation.livePolicy} live delivery)`);
|
|
71
|
-
lines.push(`add a PATH block to ${activation.rcFile}`);
|
|
72
|
-
} else if (mechanism.kind === "native-config") {
|
|
54
|
+
if (mechanism.kind === "native-config") {
|
|
73
55
|
lines.push(`write native config ${mechanism.artifactIds.join(", ")}`);
|
|
74
56
|
} else if (mechanism.preExisting) {
|
|
75
57
|
lines.push(`use the existing ${mechanism.serviceId} service`);
|
|
@@ -96,47 +78,27 @@ export function describeDeactivation(nativeActivation) {
|
|
|
96
78
|
return lines;
|
|
97
79
|
}
|
|
98
80
|
|
|
99
|
-
export async function applyNativeActivation({
|
|
100
|
-
node = process.execPath, bootstrap = defaultBootstrap(), exec = defaultExec }) {
|
|
81
|
+
export async function applyNativeActivation({ activation, exec = defaultExec }) {
|
|
101
82
|
const record = { livePolicy: activation.livePolicy,
|
|
102
83
|
protocolContract: activation.protocolContract, mechanisms: [] };
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
await exec(mechanism.applyCommand.executable, mechanism.applyCommand.args);
|
|
112
|
-
createdByAcc = true;
|
|
113
|
-
}
|
|
114
|
-
record.mechanisms.push({ kind: mechanism.kind, serviceId: mechanism.serviceId,
|
|
115
|
-
createdByAcc, teardownCommand: mechanism.teardownCommand });
|
|
116
|
-
} else {
|
|
117
|
-
const plan = planShellBootstrap({ shell: activation.shell, rcFile: activation.rcFile,
|
|
118
|
-
shimDir: activation.shimDir, runtime: { node, bootstrap, dataHome },
|
|
119
|
-
entries: [{ adapterId: adapter.id, command: mechanism.command,
|
|
120
|
-
realExecutable: mechanism.realExecutable, prefixArgs: mechanism.prefixArgs,
|
|
121
|
-
livePolicy: activation.livePolicy }] });
|
|
122
|
-
const result = await installShellBootstrap({ plan });
|
|
123
|
-
if (!result.ok) throw new Error(`shell bootstrap refused: ${result.reasonCode}`);
|
|
124
|
-
shell = result;
|
|
125
|
-
record.mechanisms.push({ kind: mechanism.kind, command: mechanism.command, shimDir: activation.shimDir,
|
|
126
|
-
ownedFiles: result.shims.map(shim => ({ path: shim.path, sha256: shim.sha256 })),
|
|
127
|
-
rcFile: result.rcFile });
|
|
84
|
+
for (const mechanism of ordered(activation.mechanisms)) {
|
|
85
|
+
if (mechanism.kind === "native-config") {
|
|
86
|
+
record.mechanisms.push({ kind: mechanism.kind, artifactIds: [...mechanism.artifactIds] });
|
|
87
|
+
} else if (mechanism.kind === "native-service") {
|
|
88
|
+
let createdByAcc = false;
|
|
89
|
+
if (!mechanism.preExisting && mechanism.applyCommand !== null) {
|
|
90
|
+
await exec(mechanism.applyCommand.executable, mechanism.applyCommand.args);
|
|
91
|
+
createdByAcc = true;
|
|
128
92
|
}
|
|
93
|
+
record.mechanisms.push({ kind: mechanism.kind, serviceId: mechanism.serviceId,
|
|
94
|
+
createdByAcc, teardownCommand: mechanism.teardownCommand });
|
|
95
|
+
} else {
|
|
96
|
+
// validateNativeActivationPlan refuses a shell bootstrap before any plan
|
|
97
|
+
// gets here; this is the same refusal for a hand-built one.
|
|
98
|
+
throw new Error("shell-bootstrap activation was removed");
|
|
129
99
|
}
|
|
130
|
-
return { nativeActivation: record, appendedRcBlock: shell?.rcFile.appended === true };
|
|
131
|
-
} catch (error) {
|
|
132
|
-
// Only bytes this operation wrote are taken back; a pre-existing service
|
|
133
|
-
// or a user's own file is never touched on the way out.
|
|
134
|
-
if (shell !== null) {
|
|
135
|
-
await uninstallShellBootstrap({ ownership: { shims: shell.shims, shimDir: activation.shimDir,
|
|
136
|
-
rcFile: shell.rcFile } }).catch(() => null);
|
|
137
|
-
}
|
|
138
|
-
throw error;
|
|
139
100
|
}
|
|
101
|
+
return { nativeActivation: record };
|
|
140
102
|
}
|
|
141
103
|
|
|
142
104
|
// Compare planned mechanisms with the deletion authority saved by older installs.
|
|
@@ -28,6 +28,8 @@ export async function fingerprint(file) {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
const CLIENT_ORPHAN_MARKER = ".orphaned_at";
|
|
32
|
+
|
|
31
33
|
/**
|
|
32
34
|
* Fingerprint of a whole directory ACC created.
|
|
33
35
|
*
|
|
@@ -45,7 +47,11 @@ export async function treeFingerprint(root) {
|
|
|
45
47
|
if (error.code === "ENOTDIR") return fingerprint(root);
|
|
46
48
|
throw error;
|
|
47
49
|
}
|
|
48
|
-
|
|
50
|
+
// Claude Code marks a plugin cache version its registry no longer names with
|
|
51
|
+
// `.orphaned_at`, and removes the marker again if the version comes back. That
|
|
52
|
+
// is the client's own bookkeeping inside a copy ACC keeps on purpose, never an
|
|
53
|
+
// edit, and ACC never writes a file by that name.
|
|
54
|
+
const files = entries.filter(entry => entry.isFile() && entry.name !== CLIENT_ORPHAN_MARKER)
|
|
49
55
|
.map(entry => path.relative(root, path.join(entry.parentPath ?? entry.path, entry.name)))
|
|
50
56
|
.sort();
|
|
51
57
|
const hash = createHash("sha256");
|
|
@@ -2,7 +2,7 @@ import { describeInstallDelivery } from "./delivery-diagnostics.mjs";
|
|
|
2
2
|
|
|
3
3
|
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
4
4
|
|
|
5
|
-
import { LIVE_POLICIES, describeActivation, describeDeactivation, planActivationRetirements
|
|
5
|
+
import { LIVE_POLICIES, describeActivation, describeDeactivation, planActivationRetirements }
|
|
6
6
|
from "./native-activation.mjs";
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -122,8 +122,18 @@ export function planInstallation({ adapters, detected, context, action = "instal
|
|
|
122
122
|
&& native.activationPlan?.eligible === true;
|
|
123
123
|
const effectiveLivePolicy = liveDeliverySupported ? delivery : "off";
|
|
124
124
|
const previous = recordedById.get(entry.adapterId)?.nativeActivation ?? null;
|
|
125
|
+
// What a failed readiness probe may keep. Not a shell bootstrap: no adapter
|
|
126
|
+
// can plan one any more, so keeping it would keep a launch path nothing
|
|
127
|
+
// maintains - the 0.7.x `claude` shim. Not an activation recorded under a
|
|
128
|
+
// contract the adapter no longer declares: it describes a transport that is
|
|
129
|
+
// gone, the Claude Channel among them.
|
|
130
|
+
const declaredContracts = new Set((adapter.nativeDelivery?.anchors ?? [])
|
|
131
|
+
.map(anchor => anchor.protocolContract));
|
|
132
|
+
const retainable = previous !== null && declaredContracts.has(previous.protocolContract)
|
|
133
|
+
? { ...previous, mechanisms: (previous.mechanisms ?? []).filter(item => item.kind !== "shell-bootstrap") }
|
|
134
|
+
: null;
|
|
125
135
|
const retainedActivation = action === "install" && delivery !== "off"
|
|
126
|
-
&& !liveDeliverySupported &&
|
|
136
|
+
&& !liveDeliverySupported && retainable?.livePolicy === delivery ? retainable : null;
|
|
127
137
|
const configuredLivePolicy = retainedActivation ? delivery : effectiveLivePolicy;
|
|
128
138
|
const deliveryDiagnostic = action === "install" && delivery !== "off"
|
|
129
139
|
&& !liveDeliverySupported
|
|
@@ -144,13 +154,13 @@ export function planInstallation({ adapters, detected, context, action = "instal
|
|
|
144
154
|
// creates one.
|
|
145
155
|
const nativeActivation = action === "install" && effectiveLivePolicy !== "off"
|
|
146
156
|
? { livePolicy: effectiveLivePolicy, protocolContract: native.eligibility.protocolContract,
|
|
147
|
-
shell: context?.shell ?? null, rcFile: rcFileFor(context?.home, context?.shell),
|
|
148
|
-
shimDir: typeof context?.stateRoot === "string" ? shimDirFor(context.stateRoot) : null,
|
|
149
157
|
mechanisms: native.activationPlan.mechanisms }
|
|
150
158
|
: null;
|
|
151
159
|
// A failed readiness probe is not revocation. Keep existing guarded launch
|
|
152
160
|
// setup without applying service commands or claiming it is available.
|
|
153
|
-
const retirements = retainedActivation
|
|
161
|
+
const retirements = retainedActivation
|
|
162
|
+
? (previous.mechanisms ?? []).filter(item => item.kind === "shell-bootstrap")
|
|
163
|
+
: planActivationRetirements({ previous, desired: nativeActivation });
|
|
154
164
|
const deactivation = retirements.length > 0 ? { ...previous, mechanisms: retirements } : null;
|
|
155
165
|
const artifacts = (record?.artifacts ?? adapter.planInstall(installContext))
|
|
156
166
|
.map(artifact => ({ path: artifact.path, kind: artifact.kind ?? "file" }))
|
|
@@ -1,115 +1,19 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { chmod,
|
|
2
|
+
import { chmod, readdir, readFile, rename, rm, rmdir, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// replaces itself with the real vendor executable through `exec`; ACC is never
|
|
11
|
-
// the parent of a model session. Every byte ACC writes is recorded with its
|
|
12
|
-
// hash so uninstall removes only what is still ACC's and refuses a block or
|
|
13
|
-
// shim someone has edited.
|
|
5
|
+
// Retirement of the shell half of 0.7.x native delivery: one marked PATH block
|
|
6
|
+
// in the user's zsh rc and one `claude` shim in an ACC-owned directory. ACC no
|
|
7
|
+
// longer writes either; `acc install`, `acc update` and `acc uninstall` remove
|
|
8
|
+
// what an older version wrote. Every byte was recorded with its hash, so only
|
|
9
|
+
// what is still ACC's is removed, and an edited block or shim is kept.
|
|
14
10
|
|
|
15
11
|
export const BLOCK_BEGIN = "# >>> agents-can-communicate native delivery >>>";
|
|
16
12
|
export const BLOCK_END = "# <<< agents-can-communicate native delivery <<<";
|
|
17
13
|
export const SHIM_MARKER = "# agents-can-communicate native delivery shim";
|
|
18
|
-
export const SUPPORTED_SHELLS = Object.freeze(["zsh"]);
|
|
19
|
-
export const SHIM_POLICIES = Object.freeze(["actionable", "all"]);
|
|
20
|
-
const COMMAND_NAME = /^[a-z][a-z0-9_.-]*$/;
|
|
21
14
|
|
|
22
|
-
const usage = (message, details = {}) => { throw new AccError(EXIT.USAGE, message, details); };
|
|
23
15
|
const sha256 = text => createHash("sha256").update(text).digest("hex");
|
|
24
|
-
|
|
25
|
-
// byte of a path or argument is ever interpreted by the shell.
|
|
26
|
-
export const shellLiteral = value => `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
27
|
-
const isAbsolute = value => typeof value === "string" && path.isAbsolute(value);
|
|
28
|
-
const inside = (root, candidate) => {
|
|
29
|
-
const relative = path.relative(root, candidate);
|
|
30
|
-
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export function renderPathBlock(shimDir) {
|
|
34
|
-
return `${BLOCK_BEGIN}\nexport PATH=${shellLiteral(shimDir)}:"$PATH"\n${BLOCK_END}\n`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function validateShimEntry(entry) {
|
|
38
|
-
if (entry === null || typeof entry !== "object") usage("a shim entry must be an object");
|
|
39
|
-
for (const key of Object.keys(entry)) {
|
|
40
|
-
if (!["adapterId", "command", "realExecutable", "prefixArgs", "livePolicy"].includes(key)) {
|
|
41
|
-
usage(`unknown shim entry field ${key}`, { key });
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
assertPortableId(entry.adapterId, "shim adapter id");
|
|
45
|
-
if (typeof entry.command !== "string" || !COMMAND_NAME.test(entry.command)) {
|
|
46
|
-
usage("a shim command must be a bare command name", { command: entry.command });
|
|
47
|
-
}
|
|
48
|
-
if (!isAbsolute(entry.realExecutable)) usage("a shim realExecutable must be an absolute path");
|
|
49
|
-
if (!Array.isArray(entry.prefixArgs) || entry.prefixArgs.some(arg => typeof arg !== "string"
|
|
50
|
-
|| arg === "" || /[\0\n]/.test(arg))) {
|
|
51
|
-
usage("shim prefixArgs must be non-empty argument strings without NUL or newline");
|
|
52
|
-
}
|
|
53
|
-
if (!SHIM_POLICIES.includes(entry.livePolicy)) {
|
|
54
|
-
usage("a shim livePolicy must be actionable or all; off means no shim", { livePolicy: entry.livePolicy });
|
|
55
|
-
}
|
|
56
|
-
return Object.freeze({ ...entry, prefixArgs: Object.freeze([...entry.prefixArgs]) });
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function renderCommandShim({ node, bootstrap, dataHome, entry }) {
|
|
60
|
-
const shim = validateShimEntry(entry);
|
|
61
|
-
for (const [name, value] of Object.entries({ node, bootstrap, dataHome })) {
|
|
62
|
-
if (!isAbsolute(value)) usage(`shim ${name} must be an absolute path`, { [name]: value });
|
|
63
|
-
}
|
|
64
|
-
const real = shellLiteral(shim.realExecutable);
|
|
65
|
-
const check = [node, bootstrap, "--adapter", shim.adapterId, "--real-executable",
|
|
66
|
-
shim.realExecutable, "--data-home", dataHome].map(shellLiteral).join(" ");
|
|
67
|
-
const prefix = shim.prefixArgs.map(shellLiteral).join(" ");
|
|
68
|
-
return [
|
|
69
|
-
"#!/bin/sh",
|
|
70
|
-
`${SHIM_MARKER} for ${shellLiteral(shim.command)}. Generated; do not edit.`,
|
|
71
|
-
"# ACC_BYPASS=1 runs the vendor command untouched. A failed or missing check",
|
|
72
|
-
"# does the same: the vendor command is never blocked on ACC.",
|
|
73
|
-
'if [ "${ACC_BYPASS-}" = "1" ]; then',
|
|
74
|
-
" unset ACC_NATIVE_DELIVERY_POLICY",
|
|
75
|
-
` exec ${real} "$@"`,
|
|
76
|
-
"fi",
|
|
77
|
-
`if ${check} </dev/null >/dev/null 2>&1; then`,
|
|
78
|
-
` ACC_NATIVE_DELIVERY_POLICY=${shellLiteral(shim.livePolicy)}`,
|
|
79
|
-
" export ACC_NATIVE_DELIVERY_POLICY",
|
|
80
|
-
` exec ${real}${prefix === "" ? "" : ` ${prefix}`} "$@"`,
|
|
81
|
-
"fi",
|
|
82
|
-
"unset ACC_NATIVE_DELIVERY_POLICY",
|
|
83
|
-
`exec ${real} "$@"`,
|
|
84
|
-
"",
|
|
85
|
-
].join("\n");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function planShellBootstrap({ shell, rcFile, shimDir, entries = [], runtime = null }) {
|
|
89
|
-
if (!SUPPORTED_SHELLS.includes(shell)) {
|
|
90
|
-
return Object.freeze({ eligible: false, reasonCode: "unsupported_shell", shell: shell ?? null,
|
|
91
|
-
rcFile: null, shimDir: null, block: null, shims: Object.freeze([]), runtime: null });
|
|
92
|
-
}
|
|
93
|
-
if (!isAbsolute(rcFile) || !isAbsolute(shimDir)) usage("rcFile and shimDir must be absolute");
|
|
94
|
-
const validated = entries.map(validateShimEntry);
|
|
95
|
-
const commands = new Set();
|
|
96
|
-
for (const entry of validated) {
|
|
97
|
-
if (commands.has(entry.command)) usage(`duplicate shim command ${entry.command}`);
|
|
98
|
-
commands.add(entry.command);
|
|
99
|
-
// A shim that resolved to itself would loop forever; the real executable
|
|
100
|
-
// is resolved before the shim directory is ever on PATH.
|
|
101
|
-
if (inside(shimDir, entry.realExecutable)) {
|
|
102
|
-
usage("a shim realExecutable cannot live inside the shim directory",
|
|
103
|
-
{ realExecutable: entry.realExecutable });
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return Object.freeze({ eligible: true, reasonCode: null, shell, rcFile, shimDir,
|
|
107
|
-
block: renderPathBlock(shimDir), runtime,
|
|
108
|
-
shims: Object.freeze(validated.map(entry => Object.freeze({
|
|
109
|
-
path: path.join(shimDir, entry.command), entry }))) });
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const defaultIo = Object.freeze({ readFile, writeFile, mkdir, chmod, rename, rm, rmdir, readdir });
|
|
16
|
+
const defaultIo = Object.freeze({ readFile, writeFile, chmod, rename, rm, rmdir, readdir, stat });
|
|
113
17
|
|
|
114
18
|
async function readText(io, file) {
|
|
115
19
|
try {
|
|
@@ -139,35 +43,6 @@ export function locateBlock(text) {
|
|
|
139
43
|
return { start: begin, end: end === -1 ? text.length : end + 1 };
|
|
140
44
|
}
|
|
141
45
|
|
|
142
|
-
export async function installShellBootstrap({ plan, io = defaultIo }) {
|
|
143
|
-
if (!plan?.eligible) return { ok: false, reasonCode: plan?.reasonCode ?? "unsupported_shell" };
|
|
144
|
-
const { node, bootstrap, dataHome } = plan.runtime ?? {};
|
|
145
|
-
const current = await readText(io, plan.rcFile);
|
|
146
|
-
const span = locateBlock(current);
|
|
147
|
-
const existing = span === null ? null : current.slice(span.start, span.end);
|
|
148
|
-
if (existing !== null && existing !== plan.block) {
|
|
149
|
-
return { ok: false, reasonCode: "rc_block_modified", rcFile: plan.rcFile };
|
|
150
|
-
}
|
|
151
|
-
await io.mkdir(plan.shimDir, { recursive: true, mode: 0o700 });
|
|
152
|
-
await io.chmod(plan.shimDir, 0o700);
|
|
153
|
-
const shims = [];
|
|
154
|
-
for (const shim of plan.shims) {
|
|
155
|
-
const content = renderCommandShim({ node, bootstrap, dataHome, entry: shim.entry });
|
|
156
|
-
await writeAtomic(io, shim.path, content, 0o700);
|
|
157
|
-
shims.push({ path: shim.path, command: shim.entry.command, sha256: sha256(content) });
|
|
158
|
-
}
|
|
159
|
-
let appended = false;
|
|
160
|
-
if (existing === null) {
|
|
161
|
-
const base = current ?? "";
|
|
162
|
-
const separator = base === "" || base.endsWith("\n") ? "" : "\n";
|
|
163
|
-
await writeAtomic(io, plan.rcFile, `${base}${separator}${plan.block}`, 0o600);
|
|
164
|
-
appended = true;
|
|
165
|
-
}
|
|
166
|
-
return { ok: true, reasonCode: null, shell: plan.shell, shimDir: plan.shimDir,
|
|
167
|
-
rcFile: { path: plan.rcFile, blockSha256: sha256(plan.block), appended },
|
|
168
|
-
shims };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
46
|
export async function uninstallShellBootstrap({ ownership, io = defaultIo }) {
|
|
172
47
|
const result = { ok: true, reasonCode: null, removedShims: [], keptShims: [],
|
|
173
48
|
missingShims: [], rcBlock: "absent" };
|
|
@@ -199,7 +74,9 @@ export async function uninstallShellBootstrap({ ownership, io = defaultIo }) {
|
|
|
199
74
|
} else if (remaining.length > 0) {
|
|
200
75
|
result.rcBlock = "kept";
|
|
201
76
|
} else {
|
|
202
|
-
|
|
77
|
+
// The user's file keeps the permissions it has now.
|
|
78
|
+
const mode = ((await io.stat?.(rcFile.path).catch(() => null))?.mode ?? 0o600) & 0o777;
|
|
79
|
+
await writeAtomic(io, rcFile.path, text.slice(0, span.start) + text.slice(span.end), mode);
|
|
203
80
|
result.rcBlock = "removed";
|
|
204
81
|
}
|
|
205
82
|
}
|
|
@@ -72,11 +72,16 @@ export async function publishAtomic(destination, bytes,
|
|
|
72
72
|
// active-journal.mjs defaults tmpDir, so every caller keeps working.
|
|
73
73
|
stageDir = stageDir ?? path.join(root, "stage");
|
|
74
74
|
const destinationDir = path.dirname(destination);
|
|
75
|
-
|
|
75
|
+
// Every check settles before a refusal is reported: Promise.all would reject
|
|
76
|
+
// on the first one while the others still create directories under a root
|
|
77
|
+
// the caller may already be removing.
|
|
78
|
+
const prepared = await Promise.allSettled([
|
|
76
79
|
ensureManagedDirectory(root, tmpDir),
|
|
77
80
|
ensureManagedDirectory(root, stageDir),
|
|
78
81
|
ensureManagedDirectory(root, destinationDir),
|
|
79
82
|
]);
|
|
83
|
+
const refused = prepared.find(result => result.status === "rejected");
|
|
84
|
+
if (refused) throw refused.reason;
|
|
80
85
|
if (!replace) {
|
|
81
86
|
const existing = await bytesIfPresent(destination, root);
|
|
82
87
|
if (existing !== null) {
|