@sjawhar/claude-envoy 1.11.2 → 1.11.3
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/.omp-plugin/plugin.json
CHANGED
package/dist/envoy-channel.js
CHANGED
|
@@ -55133,7 +55133,7 @@ var import_nats2 = __toESM(require_mod4(), 1);
|
|
|
55133
55133
|
|
|
55134
55134
|
// package.json
|
|
55135
55135
|
var name = "@sjawhar/claude-envoy";
|
|
55136
|
-
var version2 = "1.11.
|
|
55136
|
+
var version2 = "1.11.3";
|
|
55137
55137
|
var legion = {
|
|
55138
55138
|
daemonApiVersion: 54
|
|
55139
55139
|
};
|
|
@@ -14,7 +14,10 @@ limit, and one command that fills it ends every role's turn, not only yours.
|
|
|
14
14
|
In the pod you compile and test the packages your change touches, build the program a check of
|
|
15
15
|
your issue drives (`go build ./cmd/legion`), vet the module you changed (`go vet -tags e2e ./...`,
|
|
16
16
|
which compiles and runs no test), and, in a package whose tests need Postgres, run only the tests
|
|
17
|
-
your change touches by name (`go test -run`).
|
|
17
|
+
your change touches by name (`go test -run`). To run Go's race detector, prefix the command with
|
|
18
|
+
`CGO_ENABLED=1`: `CGO_ENABLED=1 go test -race -count=1 -run '<TestName>' ./internal/<package>/`.
|
|
19
|
+
The worker image sets `CGO_ENABLED=0` and carries gcc (both in
|
|
20
|
+
`packages/daemon/docker/worker.Dockerfile`), and Go refuses `-race` without cgo.
|
|
18
21
|
|
|
19
22
|
A whole module's or whole package's test run goes to CI on the pull request, where a job of that
|
|
20
23
|
pull request runs it: `go test ./...`, a whole Postgres-backed package such as
|
|
@@ -28,14 +31,31 @@ heavy run goes where your deployment's instructions say.
|
|
|
28
31
|
file `OMP_SESSION_SQL_DSN_FILE`: that is the live sessions database every pod shares, and it
|
|
29
32
|
holds every role's transcript.
|
|
30
33
|
- Give every Postgres variable a test reads `$LEGION_TEST_PG_DSN`, your pod's own server:
|
|
31
|
-
`LEGION_TEST_PG_DSN`, `DISPATCH_TEST_DATABASE_URL` and `BROKER_TEST_DATABASE_URL`.
|
|
32
|
-
and
|
|
34
|
+
`LEGION_TEST_PG_DSN`, `DISPATCH_TEST_DATABASE_URL` and `BROKER_TEST_DATABASE_URL`. Most suites
|
|
35
|
+
make and drop one of their own per test on that server — a database through `internal/testpg`,
|
|
36
|
+
a schema through `internal/broker/store/storetest`'s `URL` — so your pod's roles can share the
|
|
37
|
+
server. Four write the database the URL itself names instead, and take a database of their own
|
|
38
|
+
from the next bullet's `createdb` line: `packages/envoy/internal/broker/store`, whose
|
|
39
|
+
`store_test.go` helpers call `Open(ctx, testDatabaseURL(t))` and then `Migrate` on the database
|
|
40
|
+
`BROKER_TEST_DATABASE_URL` names; and three that open the database `DISPATCH_TEST_DATABASE_URL`
|
|
41
|
+
names and migrate it, `packages/envoy/internal/dispatch/store` (`openTestStore`, in its
|
|
42
|
+
`store_test.go`), `packages/envoy/internal/dispatch/delivery` (`deliveryTestPool`, which also
|
|
43
|
+
deletes every delivery table's rows) and `packages/envoy/internal/dispatch/redeliver`
|
|
44
|
+
(`redeliverTestLeases`). In an empty database of its own, one run of the broker's left 24 tables
|
|
45
|
+
on 2026-11-06, and one whole run of the Dispatch store's left 104 on 2026-11-07, a count that
|
|
46
|
+
grows with Dispatch's migrations.
|
|
33
47
|
- Anything that migrates into the database its DSN names, rather than making one per test, needs
|
|
34
48
|
a database of your own, because a sibling role writing the same one collides with you: the
|
|
35
49
|
Dispatch browser suite's `DATABASE_URL`, which resets what it is given, and the stage scripts'
|
|
36
50
|
`LEGION_E2E_PG_DSN`, which a daemon's `postgres_dsn` becomes and migrates Legion's schema into.
|
|
37
51
|
Make one and point the variable at it:
|
|
38
52
|
`createdb -h 127.0.0.1 -p 5433 -U legion_test "e2e_$(date +%s)"`.
|
|
53
|
+
The DSN's own database is owned by your login and writable, so what keeps two roles out of it is
|
|
54
|
+
this rule rather than the server. `packages/daemon/internal/daemon` is the third case, whose
|
|
55
|
+
`TestMain` takes one database for the package's run through `internal/testpg/testdb`.
|
|
56
|
+
Empty the served database by dropping your own suite's schemas and your own objects in `public`,
|
|
57
|
+
never `public` itself: `vector` goes with it, your login may not create it again, and
|
|
58
|
+
`ensure` only ever installs it at a database's creation.
|
|
39
59
|
- The server is empty in every new pod and dies with it. Every database made from `template1`
|
|
40
60
|
has pgvector.
|
|
41
61
|
- `LEGION_TEST_PG_DSN` set and the connection refused: that is not yours to repair. Starting the
|
|
@@ -48,12 +68,14 @@ heavy run goes where your deployment's instructions say.
|
|
|
48
68
|
is starting one and nothing will appear while you wait. With `legion-testpg` on `PATH`, start
|
|
49
69
|
one yourself on the issue's volume: the directory it defaults to is a 1 MiB filesystem your
|
|
50
70
|
role's own mounts cover, so `export LEGION_TESTPG_DIR=$XDG_DATA_HOME/testpg` first, then
|
|
51
|
-
`export LEGION_TEST_PG_DSN=postgres://legion_test@127.0.0.1:5433/
|
|
52
|
-
and `legion-testpg ensure`, which makes the cluster, the role
|
|
71
|
+
`export LEGION_TEST_PG_DSN=postgres://legion_test@127.0.0.1:5433/legion_test?sslmode=disable`
|
|
72
|
+
and `legion-testpg ensure`, which makes the cluster, the role, `vector` in `template1` and the
|
|
73
|
+
database the DSN names, owned by that role. That
|
|
53
74
|
server lives in your generation: it dies when your generation ends, which takes a sibling
|
|
54
75
|
role's connections and databases with it mid-test, and the next `ensure` of either role finds
|
|
55
|
-
the same cluster on disk and serves it again. With no
|
|
56
|
-
`legion-
|
|
76
|
+
the same cluster on disk and serves it again. With no `legion-testpg` either, stand up one with
|
|
77
|
+
the harness of `skill://legion-worker/references/withheld-capability.md` (**The worked case**)
|
|
78
|
+
and set the DSN.
|
|
57
79
|
|
|
58
80
|
## Reading a shared database
|
|
59
81
|
|
|
@@ -160,169 +182,23 @@ So that writable `$HOME`, and the issue's disk, are where an install for yoursel
|
|
|
160
182
|
the mechanics the answer is not a flat no: `redis-cli`, `sqlite3` and `dig` did run from an
|
|
161
183
|
extraction prefix under `$HOME` with `LD_LIBRARY_PATH` set, and `psql` did not — Debian's
|
|
162
184
|
`/usr/bin/psql` is a Perl wrapper that loads `PgCommon.pm` through Perl's own search path, which no
|
|
163
|
-
extraction prefix is on. What rules such an attempt out is not the unpacking: it is the
|
|
164
|
-
|
|
165
|
-
`$HOME` fails that test's first condition whatever you scope it to,
|
|
166
|
-
pod's disk rather than the issue's volume — the placement
|
|
167
|
-
(`packages/daemon/internal/runtime/sandbox/manifest.go`) exists to move writes
|
|
168
|
-
extraction is the harness's: onto the issue's volume, and exported around its
|
|
169
|
-
further (the Dispatch e2e stack
|
|
170
|
-
carry, the one documented step is a pinned run through one of the
|
|
171
|
-
— plus `uv run --with` when what you need is a library rather than
|
|
172
|
-
no runner reaches is an image change ("When no runner reaches it", below).
|
|
185
|
+
extraction prefix is on. What rules such an attempt out is not the unpacking: it is the test of
|
|
186
|
+
`skill://legion-worker/references/withheld-capability.md#the-test-where-it-lands-and-what-it-leaves-resolving`,
|
|
187
|
+
and an extraction under `$HOME` fails that test's first condition whatever you scope it to,
|
|
188
|
+
because `/home/legion` is the pod's disk rather than the issue's volume — the placement
|
|
189
|
+
`roleEnvironment` (`packages/daemon/internal/runtime/sandbox/manifest.go`) exists to move writes
|
|
190
|
+
off. The sanctioned extraction is the harness's: onto the issue's volume, and exported around its
|
|
191
|
+
own commands and no further (the Dispatch e2e stack of that reference is the worked case). For
|
|
192
|
+
everything the image does not carry, the one documented step is a pinned run through one of the
|
|
193
|
+
three package runners it does — plus `uv run --with` when what you need is a library rather than
|
|
194
|
+
a command — and a tool no runner reaches is an image change ("When no runner reaches it", below).
|
|
173
195
|
|
|
174
196
|
## What the withheld capability is, and what it is not
|
|
175
197
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
three artefacts in this repository are what draw the line — not a claim about what the ruling meant:
|
|
181
|
-
|
|
182
|
-
- **What the image ships for a role to use.** `packages/daemon/docker/worker.Dockerfile` installs
|
|
183
|
-
`uv` and `uvx`, `bun` with its `bunx` link, and the Go toolchain, and the `toolchain` capability
|
|
184
|
-
row (`packages/daemon/internal/capabilities/capabilities.go`) grants them by name. A runner put
|
|
185
|
-
in the image deliberately and granted by a capability row is not a route around a withheld one.
|
|
186
|
-
- **What the pod configures for those runners to write.** A role's environment relocates
|
|
187
|
-
`UV_CACHE_DIR` and `UV_PYTHON_INSTALL_DIR` (`podkind.go`) and `BUN_INSTALL_CACHE_DIR`, `GOCACHE`
|
|
188
|
-
and `GOMODCACHE` (`roleEnvironment`, `packages/daemon/internal/runtime/sandbox/manifest.go`)
|
|
189
|
-
onto the issue's volume, shared by the issue's roles. Legion provisions the caches a pinned run
|
|
190
|
-
fills; filling them is what they are for.
|
|
191
|
-
- **What Legion itself does at launch.** `installBlock`
|
|
192
|
-
(`packages/daemon/internal/runtime/sandbox/plugins.go`) renders a `bun add` under
|
|
193
|
-
`/usr/bin/flock` into that shared bun install cache on every installing launch. Fetching a pinned
|
|
194
|
-
package at run time is something Legion does, not something it withholds.
|
|
195
|
-
|
|
196
|
-
So the withheld row is about a dependency **the pod carries**: something that outlives the command
|
|
197
|
-
that created it, and that a later launch or another role's process then resolves *without asking
|
|
198
|
-
for it*.
|
|
199
|
-
|
|
200
|
-
### The test: where it lands, and what it leaves resolving
|
|
201
|
-
|
|
202
|
-
An install of your own — an interpreter, a browser, a second toolchain, an extraction prefix — is
|
|
203
|
-
on the sanctioned side when both of these hold: what it writes lands on the issue's own volume,
|
|
204
|
-
under `/legion`, which outlives a role's relaunch in its pod and is shared by the issue's roles —
|
|
205
|
-
whether through a variable the runtime sets for you there or under a path on that volume your
|
|
206
|
-
harness names itself — but outside the issue's workspace (`$LEGION_WORKSPACE`, under
|
|
207
|
-
`/legion/workspaces`) and the runtime's own directories (`/legion/repos`, `/legion/feed`); and
|
|
208
|
-
after the command exits nothing else resolves differently. The workspace is excluded because its jj
|
|
209
|
-
working copy tracks every new file under jj's `snapshot.max-new-file-size` (1 MiB for a role), so
|
|
210
|
-
a root written there is added to the change every role shares, and a larger new
|
|
211
|
-
file is left untracked, every jj command in the workspace then warning `Refused to snapshot some
|
|
212
|
-
files`. The one write into it that is sanctioned is a repository's own dependency install into
|
|
213
|
-
its ignored directories, below. The variables the runtime sets on the volume, where a tool's own
|
|
214
|
-
cache already goes, are
|
|
215
|
-
`XDG_CACHE_HOME` (`/legion/scratch/cache`), `XDG_DATA_HOME`, `XDG_STATE_HOME`, `TMPDIR`, `GOCACHE`,
|
|
216
|
-
`GOMODCACHE`, `BUN_INSTALL_CACHE_DIR`, `UV_CACHE_DIR` (`/legion/uv/cache`) and
|
|
217
|
-
`UV_PYTHON_INSTALL_DIR` (`/legion/uv/python/<issue>`). (`XDG_CONFIG_HOME` is set too and is not
|
|
218
|
-
one of them: it is your role's own config home, a copy of the pod's made for your role on the
|
|
219
|
-
pod's disk, which no other role reads.) `$HOME` and `/tmp` are not on the volume:
|
|
220
|
-
both are your role's own, on the pod's disk.
|
|
221
|
-
|
|
222
|
-
It is the withheld capability when either condition fails — anything that writes into a
|
|
223
|
-
system prefix, and anything that leaves a toolchain, shim or binary which a later launch or
|
|
224
|
-
another role's process will resolve without asking for it. Between two installs that both land in
|
|
225
|
-
the right place, the test is resolution, not disk, and it reads as scope: a second toolchain
|
|
226
|
-
exported for the commands of your own run — by absolute path, or on a `PATH` your harness's own
|
|
227
|
-
script sets around its own invocations — is sanctioned; the same toolchain left where a later
|
|
228
|
-
launch, another role or a subagent picks it up without naming it is operator setup, whoever typed
|
|
229
|
-
the command.
|
|
230
|
-
|
|
231
|
-
What each shape leaves resolving:
|
|
232
|
-
|
|
233
|
-
- A pinned runner run: nothing. `uvx --from httpie@3.2.4 http --version` leaves a cache entry
|
|
234
|
-
addressed by that coordinate and that version, which the next command reaches only by writing the
|
|
235
|
-
same pin; no `PATH`, no interpreter and no library search of any other process changes. That is
|
|
236
|
-
the sanctioned side, and it is the side the forms below are on.
|
|
237
|
-
- A repository's own dependency set: nothing either, which is why the caches are provisioned at
|
|
238
|
-
all. `uv sync` or `bun install` in your workspace materialises what that project already
|
|
239
|
-
declares, into that project's own directory, which a process reaches by asking for that project
|
|
240
|
-
and no other way — and the runtime puts `UV_PYTHON_INSTALL_DIR` and the workspace itself on the
|
|
241
|
-
issue's volume exactly so every role of the issue's pod reuses it.
|
|
242
|
-
- An extraction prefix: whatever you leave resolving through it, which is yours to decide. Unpack
|
|
243
|
-
Debian packages under `$HOME`, export `LD_LIBRARY_PATH` in the shell you go on working in, and
|
|
244
|
-
you change what every later command in that shell — and every subagent that inherits the
|
|
245
|
-
environment — resolves `psql` or its libraries to, asked for or not, on a prefix nothing records,
|
|
246
|
-
pins or rebuilds: that fails both conditions, and it is the withheld capability. The harness
|
|
247
|
-
below is the shape that passes. The opposite shape is in the tree too, and superseded: telling
|
|
248
|
-
every subagent to prepend a directory to `PATH` because a subagent's shell starts without it
|
|
249
|
-
hands over an environment to processes that never named it. That is the difference between
|
|
250
|
-
standing a harness up and getting yourself a CLI.
|
|
251
|
-
|
|
252
|
-
The test is about what a command leaves for other processes, never about getting the same bytes
|
|
253
|
-
twice — because a pinned line does not promise the same bytes. What a pin binds is the named
|
|
254
|
-
distribution at that version; its dependency closure is resolved when the command runs. With
|
|
255
|
-
`UV_CACHE_DIR` emptied, `uvx -v --from httpie@3.2.4 http --version` logs `Selecting:
|
|
256
|
-
requests==2.34.2 [compatible]` and resolves `urllib3` and `certifi` from ranges, where the warm run
|
|
257
|
-
prints `3.2.4` at once off the cache that first run filled. When your work needs the closure pinned
|
|
258
|
-
too — a comparison that has to hold across days or across pods — that is a project's own lockfile
|
|
259
|
-
with `uv run` or `bun install` in a workspace, not a one-off runner line.
|
|
260
|
-
|
|
261
|
-
A pinned run whose tool then installs something of its own — an interpreter, a browser, a second
|
|
262
|
-
toolchain — is where the first condition does the work. `uv run --with pyyaml==6.0.2 --no-project`
|
|
263
|
-
passes both: the wheel lands in `UV_CACHE_DIR`, under `wheels-v6/pypi/pyyaml/6.0.2-…`, and
|
|
264
|
-
`UV_PYTHON_INSTALL_DIR` is never created, because the image's own `python3` served the run. The
|
|
265
|
-
interpreter is the other run: a project whose `.python-version` names an interpreter uv must fetch
|
|
266
|
-
puts about 70 MB under `UV_PYTHON_INSTALL_DIR`, where every role of the issue's pod finds it, while
|
|
267
|
-
the cache holds no wheel. A browser download passes too, and for Playwright — the browser driver this
|
|
268
|
-
repository's own Dispatch suite installs (`playwright install`, `packages/dispatch`'s
|
|
269
|
-
`e2e:install`) — it passes as it stands, with nothing pointed anywhere: playwright-core resolves
|
|
270
|
-
its browser registry under `XDG_CACHE_HOME`, which the runtime has already put on the issue's
|
|
271
|
-
volume, so the issue's roles share one registry under the exclusive install lock that
|
|
272
|
-
`docs/kubernetes.md` writes down in "Anatomy of a Sandbox pod", the paragraph on sharing the cache
|
|
273
|
-
home — worth reading before an install, for the two things that follow from sharing. Pointing
|
|
274
|
-
`PLAYWRIGHT_BROWSERS_PATH` elsewhere is what costs you: that shared registry, and the lock that
|
|
275
|
-
makes a second role installing at the same time wait rather than race. The condition is for a tool
|
|
276
|
-
whose cache the runtime does not already place — Puppeteer is one, resolving its browser cache to
|
|
277
|
-
`join(homedir(), '.cache', 'puppeteer')` and so to your role's own home on the pod's disk whatever
|
|
278
|
-
`XDG_CACHE_HOME` says. For that one, point the tool's own cache variable (`PUPPETEER_CACHE_DIR`
|
|
279
|
-
here) at a directory under one of the paths above yourself, since an unset one leaves the download
|
|
280
|
-
wherever the tool defaults, which is nowhere this runtime chose.
|
|
281
|
-
|
|
282
|
-
Where it lands is what you pay. Under one of those variables it is on the issue's volume: it
|
|
283
|
-
outlives a role's relaunch in its pod, and every role of the issue is told the same path — under
|
|
284
|
-
every one of those variables but
|
|
285
|
-
`XDG_STATE_HOME`, which is `/legion/scratch/state/<role>`, a directory named for your role
|
|
286
|
-
(`roleEnvironment`) because Oh My Pi names its browser broker's lock from the state root and an
|
|
287
|
-
abstract socket name is one name across every role of the pod, which share its network. It is not
|
|
288
|
-
private: every role of the issue can write it, as it can every path above — though it does not
|
|
289
|
-
outlive the pod: the issue's disk is the pod's own and is deleted with it, so a later pod of the
|
|
290
|
-
issue starts without it. Under
|
|
291
|
-
`$HOME` — `/home/legion`, which no volume is mounted on — it is your role's own home, an overlay
|
|
292
|
-
over the image's whose writes land on the pod's disk (an `emptyDir` every role's home and `/tmp`
|
|
293
|
-
share): one copy per role, per pod, kept across the role's relaunches in that pod and gone when
|
|
294
|
-
the pod is replaced, and charged to the pod's ephemeral-storage limit, which every role of the pod
|
|
295
|
-
shares and which is smaller than the volume. Moving exactly these writes off that
|
|
296
|
-
filesystem is what `roleEnvironment` (`packages/daemon/internal/runtime/sandbox/manifest.go`)
|
|
297
|
-
exists for, after GKE Autopilot evicted issue pods that charged their caches to it — so an install
|
|
298
|
-
under `$HOME` fails the first condition and is outside the sanctioned form, however little else it
|
|
299
|
-
changes. The three artefacts above are the whole of that line's ground. Disk is not: every cache is
|
|
300
|
-
already relocated onto the issue's volume, which has the room for them (`docs/kubernetes.md`, "The
|
|
301
|
-
tools a role's own work needs", carries the figure), and a sanctioned install is paid once per
|
|
302
|
-
issue pod, on the issue's own disk, by the issue's roles together.
|
|
303
|
-
|
|
304
|
-
**The worked case, and the one a role actually meets: the Dispatch e2e stack.** A change to
|
|
305
|
-
`packages/dispatch` is proved in the pod by driving the specs it touches, one spec a command
|
|
306
|
-
(`bun run e2e -- e2e/<one spec file>`); the whole browser suite runs in CI's `dispatch` job on the
|
|
307
|
-
pull request. One spec needs the same stack as all of them: a Postgres with pgvector. On a digest
|
|
308
|
-
carrying `legion-testpg`, use the pod's own server ("The database your tests use", above) and
|
|
309
|
-
unpack nothing. On a digest without it, the way
|
|
310
|
-
that stack is stood up here is Debian packages unpacked with `dpkg-deb -x` into one root on the
|
|
311
|
-
issue's volume — the five packages and the start sequence are in
|
|
312
|
-
`docs/solutions/testing/the-real-dispatch-stack-runs-inside-a-legion-pod-with-no-postgres-no-docker-and-no-root-INFRA-42.md`,
|
|
313
|
-
and
|
|
314
|
-
`docs/solutions/testing/a-browser-harness-in-an-issue-pod-lives-on-the-issue-volume-INFRA-43.md`
|
|
315
|
-
puts it on the volume, moves the caches with it, and writes it as one idempotent script the next
|
|
316
|
-
role re-runs. That passes both conditions, and the second is the one to get right: export the
|
|
317
|
-
root's `PATH` and `LD_LIBRARY_PATH` around the harness's own commands — `initdb`, `pg_ctl`, the
|
|
318
|
-
server, the Playwright run — rather than into the shell you go on working in. Handing the harness
|
|
319
|
-
to the next role is the same test passed again, and
|
|
320
|
-
`docs/solutions/legion/a-port-held-in-an-issue-pod-is-a-sibling-roles-harness-move-your-run-with-the-five-port-variables-and-your-own-database-and-never-stop-the-holder-LEGION-695.md`
|
|
321
|
-
is the illustration: it leaves the server running and records the `LD_LIBRARY_PATH`, `PATH` and
|
|
322
|
-
`DATABASE_URL` in its handoff's `trickyParts`, so what crosses to the next role is a note that
|
|
323
|
-
role sets deliberately rather than an environment it inherits. The suite shells out to `psql`,
|
|
324
|
-
and `psql` being in the image is what makes that harness work now without your extracting a
|
|
325
|
-
client for yourself at all.
|
|
198
|
+
What the withheld row says, the three artefacts that draw the line between it and a pinned runner
|
|
199
|
+
form, and the worked harness case are in
|
|
200
|
+
`skill://legion-worker/references/withheld-capability.md`: read it before you unpack, extract or
|
|
201
|
+
install anything the image does not carry.
|
|
326
202
|
|
|
327
203
|
## The three pinned runner forms
|
|
328
204
|
|
|
@@ -528,8 +404,9 @@ to its role topic), with the tool, what your phase needs it for, and the runner
|
|
|
528
404
|
ruled out. What is not the step is improvising a CLI for your own convenience — a vendored binary,
|
|
529
405
|
a hand-rolled substitute, or an extraction prefix dropped on your own `PATH`: the architect decides
|
|
530
406
|
whether the image change is work to schedule. An extraction a harness builds for its own runs is
|
|
531
|
-
not improvising and is not what this rules out (the Dispatch e2e stack
|
|
532
|
-
|
|
407
|
+
not improvising and is not what this rules out (the Dispatch e2e stack of
|
|
408
|
+
`skill://legion-worker/references/withheld-capability.md`): the two cases part on its test,
|
|
409
|
+
"The test: where it lands, and what it leaves resolving", which the convenience case fails.
|
|
533
410
|
|
|
534
411
|
The operator's side of this — why the image carries those five clients rather than anything else,
|
|
535
412
|
the five priced options with their measurements, and what the clients cost the image — is in
|
|
@@ -716,8 +593,9 @@ something to count.
|
|
|
716
593
|
`go test -count=1 -timeout 40m ./internal/workflow/` runs whole in the pod, with no docker
|
|
717
594
|
panic.
|
|
718
595
|
- **The rest, by package**, for a named run the pod does make. A Postgres unpacked from Debian
|
|
719
|
-
packages (the harness under **The worked case**) has only the
|
|
720
|
-
locales, so its default collation is byte order, and
|
|
596
|
+
packages (the harness under **The worked case** of `withheld-capability.md`) has only the
|
|
597
|
+
`C`, `C.utf8` and `POSIX` locales, so its default collation is byte order, and
|
|
598
|
+
`internal/dispatch/api`'s
|
|
721
599
|
`TestIssueRanksStayOrderedPastLowercaseDigitsAndProductionShapedMoves` refuses such a database
|
|
722
600
|
by its own message. The pod's own `legion-testpg` server is made with ICU `en-US`, so that
|
|
723
601
|
test runs there, as it does in CI's `envoy-go` job, whose Postgres has `en_US.utf8`.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# What the withheld capability is, and what it is not
|
|
2
|
+
|
|
3
|
+
Part of `skill://legion-worker`, read from `skill://legion-worker/references/tools.md` before you
|
|
4
|
+
unpack, extract or install anything the worker image does not carry. Every path it cites is in
|
|
5
|
+
cybermonklabs/legion.
|
|
6
|
+
|
|
7
|
+
The withheld row reads `installs or configures its own dependencies in the pod: Legion owns its
|
|
8
|
+
dependencies` (`packages/daemon/internal/capabilities/capabilities.go`, ruling
|
|
9
|
+
dispatch://LEGION-200). Read as a sentence on its own, that wording would also forbid `uvx`,
|
|
10
|
+
`bunx --package` and `go run`, each of which fetches a package and writes a cache. It does not, and
|
|
11
|
+
three artefacts in this repository are what draw the line — not a claim about what the ruling meant:
|
|
12
|
+
|
|
13
|
+
- **What the image ships for a role to use.** `packages/daemon/docker/worker.Dockerfile` installs
|
|
14
|
+
`uv` and `uvx`, `bun` with its `bunx` link, and the Go toolchain, and the `toolchain` capability
|
|
15
|
+
row (`packages/daemon/internal/capabilities/capabilities.go`) grants them by name. A runner put
|
|
16
|
+
in the image deliberately and granted by a capability row is not a route around a withheld one.
|
|
17
|
+
- **What the pod configures for those runners to write.** A role's environment relocates
|
|
18
|
+
`UV_CACHE_DIR` and `UV_PYTHON_INSTALL_DIR` (`podkind.go`) and `BUN_INSTALL_CACHE_DIR`, `GOCACHE`
|
|
19
|
+
and `GOMODCACHE` (`roleEnvironment`, `packages/daemon/internal/runtime/sandbox/manifest.go`)
|
|
20
|
+
onto the issue's volume, shared by the issue's roles. Legion provisions the caches a pinned run
|
|
21
|
+
fills; filling them is what they are for.
|
|
22
|
+
- **What Legion itself does at launch.** `installBlock`
|
|
23
|
+
(`packages/daemon/internal/runtime/sandbox/plugins.go`) renders a `bun add` under
|
|
24
|
+
`/usr/bin/flock` into that shared bun install cache on every installing launch. Fetching a pinned
|
|
25
|
+
package at run time is something Legion does, not something it withholds.
|
|
26
|
+
|
|
27
|
+
So the withheld row is about a dependency **the pod carries**: something that outlives the command
|
|
28
|
+
that created it, and that a later launch or another role's process then resolves *without asking
|
|
29
|
+
for it*.
|
|
30
|
+
|
|
31
|
+
### The test: where it lands, and what it leaves resolving
|
|
32
|
+
|
|
33
|
+
An install of your own — an interpreter, a browser, a second toolchain, an extraction prefix — is
|
|
34
|
+
on the sanctioned side when both of these hold: what it writes lands on the issue's own volume,
|
|
35
|
+
under `/legion`, which outlives a role's relaunch in its pod and is shared by the issue's roles —
|
|
36
|
+
whether through a variable the runtime sets for you there or under a path on that volume your
|
|
37
|
+
harness names itself — but outside the issue's workspace (`$LEGION_WORKSPACE`, under
|
|
38
|
+
`/legion/workspaces`) and the runtime's own directories (`/legion/repos`, `/legion/feed`); and
|
|
39
|
+
after the command exits nothing else resolves differently. The workspace is excluded because its jj
|
|
40
|
+
working copy tracks every new file under jj's `snapshot.max-new-file-size` (1 MiB for a role), so
|
|
41
|
+
a root written there is added to the change every role shares, and a larger new
|
|
42
|
+
file is left untracked, every jj command in the workspace then warning `Refused to snapshot some
|
|
43
|
+
files`. The one write into it that is sanctioned is a repository's own dependency install into
|
|
44
|
+
its ignored directories, below. The variables the runtime sets on the volume, where a tool's own
|
|
45
|
+
cache already goes, are
|
|
46
|
+
`XDG_CACHE_HOME` (`/legion/scratch/cache`), `XDG_DATA_HOME`, `XDG_STATE_HOME`, `TMPDIR`, `GOCACHE`,
|
|
47
|
+
`GOMODCACHE`, `BUN_INSTALL_CACHE_DIR`, `UV_CACHE_DIR` (`/legion/uv/cache`) and
|
|
48
|
+
`UV_PYTHON_INSTALL_DIR` (`/legion/uv/python/<issue>`). (`XDG_CONFIG_HOME` is set too and is not
|
|
49
|
+
one of them: it is your role's own config home, a copy of the pod's made for your role on the
|
|
50
|
+
pod's disk, which no other role reads.) `$HOME` and `/tmp` are not on the volume:
|
|
51
|
+
both are your role's own, on the pod's disk.
|
|
52
|
+
|
|
53
|
+
It is the withheld capability when either condition fails — anything that writes into a
|
|
54
|
+
system prefix, and anything that leaves a toolchain, shim or binary which a later launch or
|
|
55
|
+
another role's process will resolve without asking for it. Between two installs that both land in
|
|
56
|
+
the right place, the test is resolution, not disk, and it reads as scope: a second toolchain
|
|
57
|
+
exported for the commands of your own run — by absolute path, or on a `PATH` your harness's own
|
|
58
|
+
script sets around its own invocations — is sanctioned; the same toolchain left where a later
|
|
59
|
+
launch, another role or a subagent picks it up without naming it is operator setup, whoever typed
|
|
60
|
+
the command.
|
|
61
|
+
|
|
62
|
+
What each shape leaves resolving:
|
|
63
|
+
|
|
64
|
+
- A pinned runner run: nothing. `uvx --from httpie@3.2.4 http --version` leaves a cache entry
|
|
65
|
+
addressed by that coordinate and that version, which the next command reaches only by writing the
|
|
66
|
+
same pin; no `PATH`, no interpreter and no library search of any other process changes. That is
|
|
67
|
+
the sanctioned side, and it is the side the runner forms of
|
|
68
|
+
`skill://legion-worker/references/tools.md` are on.
|
|
69
|
+
- A repository's own dependency set: nothing either, which is why the caches are provisioned at
|
|
70
|
+
all. `uv sync` or `bun install` in your workspace materialises what that project already
|
|
71
|
+
declares, into that project's own directory, which a process reaches by asking for that project
|
|
72
|
+
and no other way — and the runtime puts `UV_PYTHON_INSTALL_DIR` and the workspace itself on the
|
|
73
|
+
issue's volume exactly so every role of the issue's pod reuses it.
|
|
74
|
+
- An extraction prefix: whatever you leave resolving through it, which is yours to decide. Unpack
|
|
75
|
+
Debian packages under `$HOME`, export `LD_LIBRARY_PATH` in the shell you go on working in, and
|
|
76
|
+
you change what every later command in that shell — and every subagent that inherits the
|
|
77
|
+
environment — resolves `psql` or its libraries to, asked for or not, on a prefix nothing records,
|
|
78
|
+
pins or rebuilds: that fails both conditions, and it is the withheld capability. The harness
|
|
79
|
+
below is the shape that passes. The opposite shape is in the tree too, and superseded: telling
|
|
80
|
+
every subagent to prepend a directory to `PATH` because a subagent's shell starts without it
|
|
81
|
+
hands over an environment to processes that never named it. That is the difference between
|
|
82
|
+
standing a harness up and getting yourself a CLI.
|
|
83
|
+
|
|
84
|
+
The test is about what a command leaves for other processes, never about getting the same bytes
|
|
85
|
+
twice — because a pinned line does not promise the same bytes. What a pin binds is the named
|
|
86
|
+
distribution at that version; its dependency closure is resolved when the command runs. With
|
|
87
|
+
`UV_CACHE_DIR` emptied, `uvx -v --from httpie@3.2.4 http --version` logs `Selecting:
|
|
88
|
+
requests==2.34.2 [compatible]` and resolves `urllib3` and `certifi` from ranges, where the warm run
|
|
89
|
+
prints `3.2.4` at once off the cache that first run filled. When your work needs the closure pinned
|
|
90
|
+
too — a comparison that has to hold across days or across pods — that is a project's own lockfile
|
|
91
|
+
with `uv run` or `bun install` in a workspace, not a one-off runner line.
|
|
92
|
+
|
|
93
|
+
A pinned run whose tool then installs something of its own — an interpreter, a browser, a second
|
|
94
|
+
toolchain — is where the first condition does the work. `uv run --with pyyaml==6.0.2 --no-project`
|
|
95
|
+
passes both: the wheel lands in `UV_CACHE_DIR`, under `wheels-v6/pypi/pyyaml/6.0.2-…`, and
|
|
96
|
+
`UV_PYTHON_INSTALL_DIR` is never created, because the image's own `python3` served the run. The
|
|
97
|
+
interpreter is the other run: a project whose `.python-version` names an interpreter uv must fetch
|
|
98
|
+
puts about 70 MB under `UV_PYTHON_INSTALL_DIR`, where every role of the issue's pod finds it, while
|
|
99
|
+
the cache holds no wheel. A browser download passes too, and for Playwright — the browser driver this
|
|
100
|
+
repository's own Dispatch suite installs (`playwright install`, `packages/dispatch`'s
|
|
101
|
+
`e2e:install`) — it passes as it stands, with nothing pointed anywhere: playwright-core resolves
|
|
102
|
+
its browser registry under `XDG_CACHE_HOME`, which the runtime has already put on the issue's
|
|
103
|
+
volume, so the issue's roles share one registry under the exclusive install lock that
|
|
104
|
+
`docs/kubernetes.md` writes down in "Anatomy of a Sandbox pod", the paragraph on sharing the cache
|
|
105
|
+
home — worth reading before an install, for the two things that follow from sharing. Pointing
|
|
106
|
+
`PLAYWRIGHT_BROWSERS_PATH` elsewhere is what costs you: that shared registry, and the lock that
|
|
107
|
+
makes a second role installing at the same time wait rather than race. The condition is for a tool
|
|
108
|
+
whose cache the runtime does not already place — Puppeteer is one, resolving its browser cache to
|
|
109
|
+
`join(homedir(), '.cache', 'puppeteer')` and so to your role's own home on the pod's disk whatever
|
|
110
|
+
`XDG_CACHE_HOME` says. For that one, point the tool's own cache variable (`PUPPETEER_CACHE_DIR`
|
|
111
|
+
here) at a directory under one of the paths above yourself, since an unset one leaves the download
|
|
112
|
+
wherever the tool defaults, which is nowhere this runtime chose.
|
|
113
|
+
|
|
114
|
+
Where it lands is what you pay. Under one of those variables it is on the issue's volume: it
|
|
115
|
+
outlives a role's relaunch in its pod, and every role of the issue is told the same path — under
|
|
116
|
+
every one of those variables but
|
|
117
|
+
`XDG_STATE_HOME`, which is `/legion/scratch/state/<role>`, a directory named for your role
|
|
118
|
+
(`roleEnvironment`) because Oh My Pi names its browser broker's lock from the state root and an
|
|
119
|
+
abstract socket name is one name across every role of the pod, which share its network. It is not
|
|
120
|
+
private: every role of the issue can write it, as it can every path above — though it does not
|
|
121
|
+
outlive the pod: the issue's disk is the pod's own and is deleted with it, so a later pod of the
|
|
122
|
+
issue starts without it. Under
|
|
123
|
+
`$HOME` — `/home/legion`, which no volume is mounted on — it is your role's own home, an overlay
|
|
124
|
+
over the image's whose writes land on the pod's disk (an `emptyDir` every role's home and `/tmp`
|
|
125
|
+
share): one copy per role, per pod, kept across the role's relaunches in that pod and gone when
|
|
126
|
+
the pod is replaced, and charged to the pod's ephemeral-storage limit, which every role of the pod
|
|
127
|
+
shares and which is smaller than the volume. Moving exactly these writes off that
|
|
128
|
+
filesystem is what `roleEnvironment` (`packages/daemon/internal/runtime/sandbox/manifest.go`)
|
|
129
|
+
exists for, after GKE Autopilot evicted issue pods that charged their caches to it — so an install
|
|
130
|
+
under `$HOME` fails the first condition and is outside the sanctioned form, however little else it
|
|
131
|
+
changes. The three artefacts above are the whole of that line's ground. Disk is not: every cache is
|
|
132
|
+
already relocated onto the issue's volume, which has the room for them (`docs/kubernetes.md`, "The
|
|
133
|
+
tools a role's own work needs", carries the figure), and a sanctioned install is paid once per
|
|
134
|
+
issue pod, on the issue's own disk, by the issue's roles together.
|
|
135
|
+
|
|
136
|
+
**The worked case, and the one a role actually meets: the Dispatch e2e stack.** A change to
|
|
137
|
+
`packages/dispatch` is proved in the pod by driving the specs it touches, one spec a command
|
|
138
|
+
(`bun run e2e -- e2e/<one spec file>`); the whole browser suite runs in CI's `dispatch` job on the
|
|
139
|
+
pull request. One spec needs the same stack as all of them: a Postgres with pgvector. On a digest
|
|
140
|
+
carrying `legion-testpg`, use the pod's own server
|
|
141
|
+
(`skill://legion-worker/references/tools.md#the-database-your-tests-use`) and
|
|
142
|
+
unpack nothing. On a digest without it, the way
|
|
143
|
+
that stack is stood up here is Debian packages unpacked with `dpkg-deb -x` into one root on the
|
|
144
|
+
issue's volume — the five packages and the start sequence are in
|
|
145
|
+
`docs/solutions/testing/the-real-dispatch-stack-runs-inside-a-legion-pod-with-no-postgres-no-docker-and-no-root-INFRA-42.md`,
|
|
146
|
+
and
|
|
147
|
+
`docs/solutions/testing/a-browser-harness-in-an-issue-pod-lives-on-the-issue-volume-INFRA-43.md`
|
|
148
|
+
puts it on the volume, moves the caches with it, and writes it as one idempotent script the next
|
|
149
|
+
role re-runs. That passes both conditions, and the second is the one to get right: export the
|
|
150
|
+
root's `PATH` and `LD_LIBRARY_PATH` around the harness's own commands — `initdb`, `pg_ctl`, the
|
|
151
|
+
server, the Playwright run — rather than into the shell you go on working in. Handing the harness
|
|
152
|
+
to the next role is the same test passed again, and
|
|
153
|
+
`docs/solutions/legion/a-port-held-in-an-issue-pod-is-a-sibling-roles-harness-move-your-run-with-the-five-port-variables-and-your-own-database-and-never-stop-the-holder-LEGION-695.md`
|
|
154
|
+
is the illustration: it leaves the server running and records the `LD_LIBRARY_PATH`, `PATH` and
|
|
155
|
+
`DATABASE_URL` in its handoff's `trickyParts`, so what crosses to the next role is a note that
|
|
156
|
+
role sets deliberately rather than an environment it inherits. The suite shells out to `psql`,
|
|
157
|
+
and `psql` being in the image is what makes that harness work now without your extracting a
|
|
158
|
+
client for yourself at all.
|