@agent-compose/sdk 0.8.1 → 0.8.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/dist/agent/__tests__/perf-sampler.test.d.ts +10 -0
- package/dist/agent/agent-context.d.ts +1 -1
- package/dist/agent/agent-loop.d.ts +5 -1
- package/dist/agent/desktop-open.d.ts +184 -0
- package/dist/agent/perf-sampler.d.ts +99 -0
- package/dist/agent/services-manifest.d.ts +88 -0
- package/dist/agent/services-restore.d.ts +58 -0
- package/dist/client.d.ts +189 -15
- package/dist/display.d.ts +17 -0
- package/dist/index.d.ts +14 -5
- package/dist/index.js +1625 -120
- package/dist/runtimes/_cli-agent.d.ts +372 -2
- package/dist/runtimes/claude-code.d.ts +12 -0
- package/dist/runtimes/codex.buildcommand.test.d.ts +9 -0
- package/dist/runtimes/codex.d.ts +8 -0
- package/dist/runtimes/openai-desktop.js +1555 -120
- package/dist/runtimes/session-env.test.d.ts +14 -0
- package/dist/sandbox/sizes.d.ts +120 -30
- package/dist/sandbox.d.ts +1 -1
- package/dist/types/api-conversations.d.ts +476 -1
- package/dist/types/api-factory.d.ts +164 -7
- package/dist/types/api-runs.d.ts +23 -1
- package/dist/types/protocol.d.ts +32 -1
- package/dist/types/runtime.d.ts +120 -0
- package/dist/types/workflow-metadata.d.ts +6 -5
- package/package.json +1 -1
- package/src/agent/agent-context.ts +128 -28
- package/src/agent/agent-loop.ts +10 -3
- package/src/agent/desktop-open.ts +418 -0
- package/src/agent/perf-sampler.ts +202 -0
- package/src/agent/services-manifest.ts +356 -0
- package/src/agent/services-restore.ts +195 -0
- package/src/client.ts +384 -32
- package/src/display.ts +44 -1
- package/src/index.ts +74 -7
- package/src/runtimes/_cli-agent.ts +1160 -67
- package/src/runtimes/claude-code.ts +187 -12
- package/src/runtimes/codex.ts +65 -2
- package/src/sandbox/providers/e2b.ts +8 -4
- package/src/sandbox/providers/local.ts +16 -4
- package/src/sandbox/sizes.ts +127 -44
- package/src/sandbox.ts +8 -0
- package/src/types/api-conversations.ts +461 -2
- package/src/types/api-factory.ts +165 -7
- package/src/types/api-runs.ts +25 -1
- package/src/types/protocol.ts +30 -1
- package/src/types/runtime.ts +122 -0
- package/src/types/workflow-metadata.ts +6 -5
|
@@ -145,27 +145,81 @@ like?" is answered by opening it on this desktop and screenshotting it — not b
|
|
|
145
145
|
reasoning about the code, and not by a headless render (which proves the process
|
|
146
146
|
starts, not that the thing draws). Verify visually before you report visually.
|
|
147
147
|
|
|
148
|
+
**This is how you ACT on the web.** When the task is to DO something on a
|
|
149
|
+
website — book, order, reserve, sign up, fill a form, operate a dashboard —
|
|
150
|
+
and no connector or API covers it, the desktop browser IS the tool: \`ac-open\`
|
|
151
|
+
the site, do the errand there, and show the human the screen at decision
|
|
152
|
+
points (\`agentc display desktop\` in a cloud session). Research/search tools
|
|
153
|
+
answer QUESTIONS; an errand is an ACTION — "book me a table" means open the
|
|
154
|
+
booking site and book it, never a research report of options.
|
|
155
|
+
|
|
148
156
|
- **Input** — \`xdotool\` against \`DISPLAY=:0\`: \`DISPLAY=:0 xdotool mousemove <x> <y>\`,
|
|
149
157
|
\`DISPLAY=:0 xdotool click 1\` (1=left, 3=right), \`DISPLAY=:0 xdotool type 'text'\`,
|
|
150
158
|
\`DISPLAY=:0 xdotool key Return\` (also \`ctrl+c\`, \`Tab\`, \`super\`, …).
|
|
151
159
|
- **Screenshots** — \`scrot\` (or ImageMagick's \`import\`):
|
|
152
160
|
\`DISPLAY=:0 scrot /tmp/screen.png\`, then READ the PNG to see the screen,
|
|
153
161
|
before and after you act. A screenshot is your only eyes here.
|
|
154
|
-
- **
|
|
155
|
-
\`
|
|
162
|
+
- **The browser is chromium, preinstalled** — headful, on this display
|
|
163
|
+
(\`command -v chromium\` to confirm on an older machine). If an older machine
|
|
164
|
+
is missing it, the platform is already installing it in the background from
|
|
165
|
+
boot — \`ac-open <url>\` tells you when that is the case; retry it in ~30s.
|
|
166
|
+
Only if \`ac-open\` reports the background install FAILED do you relay that
|
|
167
|
+
one line to the human — never an apt-get expedition of your own.
|
|
168
|
+
- **Launching apps — use \`ac-open\`, never a plain \`&\`.** A GUI process
|
|
169
|
+
launched with \`<app> &\` DIES the moment your shell command returns — the
|
|
170
|
+
sandbox reaps each command's process group, so "the window vanished when
|
|
171
|
+
the shell finished" is that reaping, not a broken app. \`ac-open\` is the
|
|
172
|
+
platform launcher that survives it (\`command -v ac-open\` on older machines):
|
|
173
|
+
|
|
174
|
+
ac-open https://github.com # the browser — a running instance gets a tab
|
|
175
|
+
ac-open ./report.html # a local file, in the browser
|
|
176
|
+
ac-open . # a directory, in the file manager
|
|
177
|
+
ac-open gimp # any GUI app by command name
|
|
178
|
+
|
|
179
|
+
It detaches the app into its own session (setsid, stdio off your command's
|
|
180
|
+
pipes), records a pidfile + log under \`/tmp/.ac-desktop-open.<uid>/\`
|
|
181
|
+
(per-uid — yours is \`/tmp/.ac-desktop-open.$(id -u)\`), and
|
|
182
|
+
re-invoking it for a running app FOCUSES the existing window instead of
|
|
183
|
+
spawning a second copy. \`xdg-open\` and \`sensible-browser\` route through
|
|
184
|
+
it too. The whole recipe for looking at a page: \`ac-open <url>\`, then
|
|
185
|
+
\`sleep 5\`, then \`DISPLAY=:0 scrot /tmp/screen.png\` and read it. Without
|
|
186
|
+
\`ac-open\` (older machine), detach by hand:
|
|
187
|
+
\`setsid <app> </dev/null >/tmp/app.log 2>&1 &\` — and note **chromium as
|
|
188
|
+
root also needs \`--no-sandbox\`** (nested sandbox; \`ac-open\` and the baked
|
|
189
|
+
chromium defaults already handle it).
|
|
190
|
+
- **Two things that trip agents up, both normal:**
|
|
156
191
|
- a GUI app needs a **beat to map its window** — screenshot, and if you see
|
|
157
192
|
only wallpaper, wait a couple of seconds and screenshot again before
|
|
158
193
|
concluding anything;
|
|
159
|
-
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
If a window still never appears, read the app's own log (\`/tmp/*.log\`) — the
|
|
164
|
-
desktop is not the thing that failed. Do NOT abandon it for a headless
|
|
165
|
-
screenshot: headless cannot tell you what the human will see.
|
|
194
|
+
- if a window still never appears, read the app's own log
|
|
195
|
+
(\`/tmp/.ac-desktop-open.$(id -u)/*.log\`, \`/tmp/*.log\`) — the desktop is not the
|
|
196
|
+
thing that failed. Do NOT abandon it for a headless
|
|
197
|
+
screenshot: headless cannot tell you what the human will see.
|
|
166
198
|
- **A human can watch** — the session header carries a **Desktop** button in the
|
|
167
199
|
dashboard, and what a teammate sees there is exactly this display. The desktop
|
|
168
200
|
runs whether or not anyone is looking; never wait for a viewer.
|
|
201
|
+
- **Show the human the screen** — in a cloud session,
|
|
202
|
+
\`agentc display desktop --note "<caption>"\` captures this display and posts
|
|
203
|
+
it into the conversation as a snapshot card with an "Open desktop" door to
|
|
204
|
+
the live view. Use it to report visual results, and ALWAYS when you hit a
|
|
205
|
+
wall on the desktop that only a human can clear — a login form, a 2FA
|
|
206
|
+
prompt, a CAPTCHA, an unexpected dialog: snapshot it so they SEE the wall,
|
|
207
|
+
then ask (AskUserQuestion when you have it) and wait; never guess
|
|
208
|
+
credentials or click around a wall. The rule is SCREEN FOR ACTIONS,
|
|
209
|
+
VAULT FOR SECRETS. For non-sensitive interaction that needs the human's
|
|
210
|
+
own hands or judgment — pick an option, review a page, solve a CAPTCHA —
|
|
211
|
+
the display + ask pair is right: the platform merges them into ONE live
|
|
212
|
+
desktop card — the human clicks in, acts on the live screen, and answers
|
|
213
|
+
"I'm done" to hand it back; treat that answer as the wall being cleared,
|
|
214
|
+
re-check the screen, and continue. For SECRETS — a password, payment
|
|
215
|
+
details, any sensitive value —
|
|
216
|
+
\`agentc secrets session request <KEY...> --reason "<why>" --wait\` mints a
|
|
217
|
+
secure vault link (a one-tap approval when the user has these saved as a
|
|
218
|
+
personal set); the values land in the session env and YOU type them into
|
|
219
|
+
the site on the user's behalf. Never ask the human to type a password or
|
|
220
|
+
card number into this machine's browser, and never suggest they "log in
|
|
221
|
+
on the Desktop view" — the vault carries the secret, then you act with
|
|
222
|
+
it. A one-time 2FA code from their phone is the chat-OK exception.
|
|
169
223
|
|
|
170
224
|
Nothing here changes the credentials rule above: tokens are injected at the
|
|
171
225
|
network layer, never present on the desktop or in any file you can read — so
|
|
@@ -173,38 +227,49 @@ there is nothing to type, paste, or screenshot a credential from.
|
|
|
173
227
|
|
|
174
228
|
## Recording a demo — the desktop, captured to a video the human can play
|
|
175
229
|
|
|
176
|
-
"Record a demo of you using X" is a normal ask, and this machine does it
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
230
|
+
"Record a demo of you using X" is a normal ask, and this machine does it.
|
|
231
|
+
(For a LIVE view no recording is needed — the session header's **Desktop**
|
|
232
|
+
button already streams this display to any teammate watching; a recording is
|
|
233
|
+
the durable, replayable artifact. Both modes exist; say so when it matters.)
|
|
234
|
+
|
|
235
|
+
**Use \`ac-record\` — the platform recorder is already on PATH** (cloud
|
|
236
|
+
sessions; \`command -v ac-record\` to confirm on older machines):
|
|
182
237
|
|
|
183
|
-
|
|
238
|
+
ac-record start # begins capturing the desktop (display :0)
|
|
239
|
+
# ... drive the app with xdotool, screenshotting as you go ...
|
|
240
|
+
ac-record stop # finishes + saves to recordings/ in your workspace
|
|
241
|
+
ac-record status # one JSON line: {"recording":true,...}
|
|
184
242
|
|
|
185
|
-
|
|
243
|
+
It records the whole display (with desktop audio when the machine has a
|
|
244
|
+
PulseAudio monitor), enforces sane caps (5 min / 200 MB — start a fresh
|
|
245
|
+
recording per scene rather than one long take), keeps the file playable even
|
|
246
|
+
if the machine dies mid-take, and \`stop\` prints the saved path — the file
|
|
247
|
+
lands ON THE DRIVE in \`recordings/\`, visible in Files and playable in the
|
|
248
|
+
dashboard. A human watching the Desktop pane sees the recording indicator
|
|
249
|
+
while you record.
|
|
186
250
|
|
|
187
|
-
|
|
251
|
+
If \`ac-record\` is missing (older machine), record by hand.
|
|
252
|
+
**ffmpeg IS pre-installed** on platform images (\`command -v ffmpeg\`; only
|
|
253
|
+
if absent: \`sudo apt-get update -q && sudo apt-get install -y -q ffmpeg\`):
|
|
188
254
|
|
|
189
255
|
DISPLAY=:0 ffmpeg -f x11grab \\
|
|
190
256
|
-video_size "$(DISPLAY=:0 xdotool getdisplaygeometry | tr ' ' x)" \\
|
|
191
257
|
-framerate 10 -i :0 -c:v libvpx -b:v 1M -deadline realtime -cpu-used 8 \\
|
|
192
258
|
demo.webm &
|
|
193
259
|
FFMPEG_PID=$!
|
|
194
|
-
# ... drive the app with xdotool
|
|
260
|
+
# ... drive the app with xdotool ...
|
|
195
261
|
kill -INT "$FFMPEG_PID" && wait "$FFMPEG_PID"
|
|
196
262
|
|
|
197
|
-
The gotchas, each one earned:
|
|
263
|
+
The hand-rolled gotchas, each one earned:
|
|
198
264
|
- **Stop with SIGINT (\`kill -INT\`), never SIGKILL** — ffmpeg finalizes the
|
|
199
265
|
file on SIGINT; a hard kill truncates the encode mid-write.
|
|
200
|
-
- **Record WebM (matroska-family), not MP4** — mp4 writes its moov atom
|
|
201
|
-
END, so a killed or crashed encode leaves an UNPLAYABLE file; webm
|
|
202
|
-
playable up to the last written frame and plays natively in the
|
|
203
|
-
|
|
204
|
-
afterwards if you truly need it, never record straight to it.
|
|
266
|
+
- **Record WebM (matroska-family), not plain MP4** — mp4 writes its moov atom
|
|
267
|
+
at the END, so a killed or crashed encode leaves an UNPLAYABLE file; webm
|
|
268
|
+
stays playable up to the last written frame and plays natively in the
|
|
269
|
+
browser. (\`ac-record\` sidesteps this with fragmented mp4.)
|
|
205
270
|
- **\`-video_size\` must match the real screen** — x11grab does not default to
|
|
206
271
|
it; read the geometry from \`xdotool getdisplaygeometry\` as above.
|
|
207
|
-
- **10–
|
|
272
|
+
- **10–15 fps is right for a screen demo** — small files, legible UI motion;
|
|
208
273
|
this is not video production.
|
|
209
274
|
- **Write to the drive, not /tmp** — the recording must land in your working
|
|
210
275
|
directory to persist and show up in Files; a file in /tmp dies with the
|
|
@@ -245,14 +310,49 @@ origin of its own — absolute asset paths and client-side routing work, the
|
|
|
245
310
|
whole app is navigable — so serve normally and let the platform address it;
|
|
246
311
|
never rewrite your app to a path prefix.
|
|
247
312
|
|
|
313
|
+
## Durable services — the machine is cattle, the manifest is the pet (cloud sessions)
|
|
314
|
+
|
|
315
|
+
Parking preserves detached processes; a machine RECYCLE (resize, eviction,
|
|
316
|
+
failed reconnect) does not — every process and every byte off the drive is
|
|
317
|
+
discarded, and recycles are normal. When you start a long-running service the
|
|
318
|
+
human will rely on across turns (a dev server, a docker compose stack, a
|
|
319
|
+
database), record it in \`.ac/services.yml\` at the drive root so the platform
|
|
320
|
+
relaunches it automatically on the next fresh machine:
|
|
321
|
+
|
|
322
|
+
agentc services add <name> --command '<cmd>' # record a service
|
|
323
|
+
agentc services list # manifest + live status
|
|
324
|
+
agentc services restore # run the manifest now
|
|
325
|
+
agentc services remove <name>
|
|
326
|
+
|
|
327
|
+
Each entry can carry \`cwd\`, \`port\`, a bounded \`health\` probe (cmd or
|
|
328
|
+
http), one-time \`setup\` (e.g. \`docker compose pull\`), and \`data\` hooks.
|
|
329
|
+
After a recycle the platform posts "Machine restarted — restored N services"
|
|
330
|
+
into the conversation; on seeing it, VERIFY health rather than rebuilding —
|
|
331
|
+
logs live at \`/tmp/ac-services/<name>.log\`. Data honesty: sandbox-local
|
|
332
|
+
database state dies with the machine. Keep seeds/dumps ON THE DRIVE; declare
|
|
333
|
+
\`data.restore\` (reload on fresh boot) and \`data.dump\` (written before a
|
|
334
|
+
DELIBERATE recycle such as a resize — evictions give no warning, so treat the
|
|
335
|
+
drive copy as the truth).
|
|
336
|
+
|
|
248
337
|
## Tools in this environment
|
|
249
338
|
|
|
250
339
|
- \`agentc\` — Agent Compose CLI (your primary interface; authed from env)
|
|
251
340
|
- \`@agent-compose/sdk\` — installed in /workspace for writing workflows
|
|
252
341
|
- \`/ac:*\` Claude Code skills — slash commands for the above
|
|
253
|
-
- \`
|
|
342
|
+
- \`rtk\`, \`bun\`
|
|
254
343
|
- \`xdotool\` / \`scrot\` — drive + screenshot the desktop (if this machine has one; see Computer Use)
|
|
255
|
-
-
|
|
344
|
+
- \`chromium\` — the desktop browser; \`ac-open <url|file|app>\` — open it on the
|
|
345
|
+
desktop, detached (survives your command; see Computer Use)
|
|
346
|
+
- A world-writable \`/workspace\` working directory
|
|
347
|
+
|
|
348
|
+
If a system capability you need is genuinely missing — no browser, no display,
|
|
349
|
+
no \`ac-open\`, a daemon that isn't there — say so to the human in ONE honest
|
|
350
|
+
line (what is missing and what it blocks) instead of mounting a
|
|
351
|
+
package-manager expedition. An in-session \`apt-get install\` dies with the
|
|
352
|
+
sandbox, burns turns, and hides the real gap; missing platform capabilities
|
|
353
|
+
are the platform's to bake in, and \`agentc pause\` is the door to ask through.
|
|
354
|
+
(Your own project's dependencies are different — installing those is normal
|
|
355
|
+
work.)`;
|
|
256
356
|
|
|
257
357
|
/** Parameters for the `agentc session add` education brief (ADR-0055 §8). */
|
|
258
358
|
export interface AddedSessionBriefParams {
|
package/src/agent/agent-loop.ts
CHANGED
|
@@ -91,8 +91,13 @@ function preview(value: unknown): string {
|
|
|
91
91
|
|
|
92
92
|
/** Everything but the live-only streaming chunk: `text_delta` never becomes
|
|
93
93
|
* an agent.message event (the terminating `text` carries the whole block) —
|
|
94
|
-
* the loop filters it before summarizing.
|
|
95
|
-
|
|
94
|
+
* the loop filters it before summarizing. `task_notification` is filtered
|
|
95
|
+
* too: it is session-transport metadata (a parent harness's background-task
|
|
96
|
+
* completion echo), not the agent's own output. */
|
|
97
|
+
type DurableAgentMessage = Exclude<
|
|
98
|
+
AgentMessage,
|
|
99
|
+
{ type: "text_delta" } | { type: "usage_delta" } | { type: "task_notification" }
|
|
100
|
+
>;
|
|
96
101
|
|
|
97
102
|
export function summarizeAgentMessage(msg: DurableAgentMessage): AgentMessageSummary {
|
|
98
103
|
switch (msg.type) {
|
|
@@ -478,7 +483,9 @@ export async function agentLoop<TResponse = unknown>(opts: AgentLoopOpts<TRespon
|
|
|
478
483
|
continue;
|
|
479
484
|
}
|
|
480
485
|
const msg = outputVerdict.value;
|
|
481
|
-
|
|
486
|
+
// A processor cannot re-introduce a live-only chunk; task
|
|
487
|
+
// notifications are transport metadata, never loop output.
|
|
488
|
+
if (msg.type === "text_delta" || msg.type === "usage_delta" || msg.type === "task_notification") continue;
|
|
482
489
|
opts.onAgentEvent?.(iteration, msg);
|
|
483
490
|
// Usage summaries carry the resolved model so the server can price
|
|
484
491
|
// token rows per model without correlating back to agent.spawned.
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ac-open` — the in-guest desktop launcher that SURVIVES the command that
|
|
3
|
+
* invoked it.
|
|
4
|
+
*
|
|
5
|
+
* Why it exists (the 2026-08-16 cloud-desktop transcript): the sandbox exec
|
|
6
|
+
* layer (E2B envd) runs each command as its own session/process group and
|
|
7
|
+
* reaps that whole group when the command returns — and a child that inherits
|
|
8
|
+
* the exec's stdout/stderr pipes tethers the exec's stream to its own
|
|
9
|
+
* lifetime. So a GUI app launched with a plain `&` dies the moment the
|
|
10
|
+
* launching shell finishes ("It exited when the shell finished" — the agent's
|
|
11
|
+
* own words, after 8 turns of desktop archaeology). This is the SAME failure
|
|
12
|
+
* class as the v0.10.39/40 runner regression, fixed there with
|
|
13
|
+
* `setsid + >/dev/null 2>&1 </dev/null + pidfile` (sdk/src/runtimes/
|
|
14
|
+
* _cli-agent.ts — see its LOAD-BEARING comment). `ac-open` packages that
|
|
15
|
+
* exact discipline as a one-word verb so no agent ever has to rediscover it.
|
|
16
|
+
*
|
|
17
|
+
* ONE script, TWO delivery doors (the ac-record pattern,
|
|
18
|
+
* server/src/sandbox/attach/desktop-recorder.ts):
|
|
19
|
+
* - BAKED into the desktop-carrying E2B images by the template recipe
|
|
20
|
+
* (infra/e2b-template/parts.ts DESKTOP_OPEN_INSTALL) — covers workflow
|
|
21
|
+
* runs and fresh session images;
|
|
22
|
+
* - INSTALLED at session desktop-ensure by the server
|
|
23
|
+
* (server/src/sandbox/persistent.ts ensureSessionDesktop) — covers
|
|
24
|
+
* GRANDFATHERED session images that predate the bake, on every fresh boot.
|
|
25
|
+
* Both doors run `installDesktopOpenCmd()`, a plain truncating rewrite, so a
|
|
26
|
+
* contract change reaches old VMs on the next boot.
|
|
27
|
+
*
|
|
28
|
+
* The install also shims `xdg-open` and `sensible-browser` at /usr/local/bin
|
|
29
|
+
* (which precedes /usr/bin on PATH — the VS Code wrapper precedent), so
|
|
30
|
+
* generic "open a URL" flows inherit the detach semantics instead of dying
|
|
31
|
+
* with the exec.
|
|
32
|
+
*
|
|
33
|
+
* This module composes STRINGS only (pure + unit-tested, the
|
|
34
|
+
* desktop-recorder.ts posture); nothing here touches a sandbox.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/** Where the launcher lands on the guest PATH. */
|
|
38
|
+
export const AC_OPEN_PATH = "/usr/local/bin/ac-open";
|
|
39
|
+
|
|
40
|
+
/** SHARED in-guest state dir: the browser-backfill markers, written by ROOT
|
|
41
|
+
* (the backfill runs sudo) and only ever READ by ac-open. /tmp (ext4) —
|
|
42
|
+
* machine-tier state that dies with the VM, exactly like the recorder's.
|
|
43
|
+
*
|
|
44
|
+
* ac-open's own pidfiles + logs do NOT live here — they live in a PER-UID
|
|
45
|
+
* sibling (`${AC_OPEN_STATE_DIR}.<uid>`, composed in the script). The
|
|
46
|
+
* 2026-08-21 owner transcript is why: the backfill's root-owned `mkdir -p`
|
|
47
|
+
* landed this dir 0755 root:root, the session user's `>>"$LOG"` redirect
|
|
48
|
+
* then failed, and the spawned subshell died before chromium ever ran —
|
|
49
|
+
* surfacing as "chromium exited immediately: see …/browser.log" with an
|
|
50
|
+
* EMPTY log (the redirect that would have written it is what failed). A
|
|
51
|
+
* per-uid dir makes the launcher's writes collision-free for every
|
|
52
|
+
* identity (root workflow runner AND the default session user). */
|
|
53
|
+
export const AC_OPEN_STATE_DIR = "/tmp/.ac-desktop-open";
|
|
54
|
+
|
|
55
|
+
/** Generic-open entrypoints routed through the launcher. */
|
|
56
|
+
export const AC_OPEN_SHIM_PATHS = [
|
|
57
|
+
"/usr/local/bin/xdg-open",
|
|
58
|
+
"/usr/local/bin/sensible-browser",
|
|
59
|
+
] as const;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Browser-backfill marker protocol (self-healing browser presence). The
|
|
63
|
+
* server's boot-time desktop-ensure detects a desktop-carrying image WITHOUT
|
|
64
|
+
* chromium — only possible on GRANDFATHERED bakes, where the chromium install
|
|
65
|
+
* was tolerant (`|| true`) before the 2026-08-16 hard-install — and
|
|
66
|
+
* apt-installs it in the BACKGROUND, with the same setsid + stdio-redirect +
|
|
67
|
+
* pidfile detach discipline ac-open itself uses, so no boot path ever blocks
|
|
68
|
+
* on apt (20-40s). These markers are the shared contract: `browserBackfillCmd`
|
|
69
|
+
* writes them, `browserBackfillStatusCmd` and the ac-open script read them —
|
|
70
|
+
* that's how ac-open answers the THIRD state honestly (browser missing but
|
|
71
|
+
* ARRIVING — retry) instead of "no browser, give up".
|
|
72
|
+
*/
|
|
73
|
+
export const BROWSER_BACKFILL_PIDFILE = `${AC_OPEN_STATE_DIR}/browser-install.pid`;
|
|
74
|
+
export const BROWSER_BACKFILL_LOG = `${AC_OPEN_STATE_DIR}/browser-install.log`;
|
|
75
|
+
export const BROWSER_BACKFILL_FAILED = `${AC_OPEN_STATE_DIR}/browser-install.failed`;
|
|
76
|
+
|
|
77
|
+
/** The PINNED third-state line ac-open prints while the background install
|
|
78
|
+
* runs: honest, actionable, and never an invitation to give up or to mount
|
|
79
|
+
* an apt expedition of the agent's own. */
|
|
80
|
+
export const BROWSER_INSTALL_IN_PROGRESS_MSG =
|
|
81
|
+
"ac-open: no browser YET — the platform is installing chromium in the background right now; retry this exact command in ~30s";
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The `ac-open` script body. POSIX sh (dash is /bin/sh on the Debian base).
|
|
85
|
+
*
|
|
86
|
+
* Contract, pinned by the unit tests:
|
|
87
|
+
* - every spawn is `setsid <cmd> </dev/null >>"$LOG" 2>&1 &` — its own
|
|
88
|
+
* session (survives the exec's process-group reap), stdio OFF the exec's
|
|
89
|
+
* pipes (so the exec's stream can settle), a durable pidfile;
|
|
90
|
+
* - pidfiles + logs live in a PER-UID state dir (`$D`), so the root runner
|
|
91
|
+
* and the session user never fight over one root-owned dir (the
|
|
92
|
+
* 2026-08-21 "exited immediately, empty log" failure — see
|
|
93
|
+
* AC_OPEN_STATE_DIR's header); the shared dir (`$M`) is read-only here,
|
|
94
|
+
* for the backfill markers root writes;
|
|
95
|
+
* - IDEMPOTENT: re-invoking for a running app FOCUSES its window
|
|
96
|
+
* (xdotool windowactivate) instead of spawning a second copy; the
|
|
97
|
+
* browser and file manager are singletons, so a re-invoke hands the
|
|
98
|
+
* target to the existing instance (a new tab / window) by design.
|
|
99
|
+
* Liveness checks are SAME-UID only (`pgrep -u`): another identity's
|
|
100
|
+
* instance shares neither profile nor single-instance socket, so it can
|
|
101
|
+
* neither take a hand-off nor make this spawn redundant. And a fresh
|
|
102
|
+
* spawn that exits within the liveness window while a same-uid instance
|
|
103
|
+
* is alive IS the hand-off (single-instance apps hand the target over
|
|
104
|
+
* and exit 0) — reported as success, never as a crash. N calls =
|
|
105
|
+
* N tabs, zero crashes;
|
|
106
|
+
* - HONEST degrades, one line each: no display → say so; no browser /
|
|
107
|
+
* unknown app → name the missing tool and tell the agent to REPORT it
|
|
108
|
+
* rather than mount a package-manager expedition. A REAL immediate
|
|
109
|
+
* death carries the app's own log tail as the cause.
|
|
110
|
+
*/
|
|
111
|
+
export const AC_OPEN_SCRIPT = `#!/bin/sh
|
|
112
|
+
# ac-open — open a URL, file, or GUI app on this machine's desktop, DETACHED.
|
|
113
|
+
#
|
|
114
|
+
# ac-open https://github.com the browser (a running instance gets a tab)
|
|
115
|
+
# ac-open ./report.html a local file, in the browser
|
|
116
|
+
# ac-open . a directory, in the file manager
|
|
117
|
+
# ac-open gimp [args...] any GUI app by command name
|
|
118
|
+
#
|
|
119
|
+
# Why: the sandbox reaps each exec'd command's process group when the command
|
|
120
|
+
# returns, so a GUI app launched with a plain \`&\` dies the moment the
|
|
121
|
+
# launching shell finishes. ac-open detaches the app into its own session
|
|
122
|
+
# (setsid, stdio off the exec's pipes), records a pidfile, and returns at
|
|
123
|
+
# once; re-invoking it for a running app focuses the existing window instead
|
|
124
|
+
# of spawning a second copy. Logs + pidfiles: /tmp/.ac-desktop-open.<uid>/.
|
|
125
|
+
# Written by agent-compose (sdk/src/agent/desktop-open.ts) — do not edit in place.
|
|
126
|
+
set -u
|
|
127
|
+
export DISPLAY="\${DISPLAY:-:0}"
|
|
128
|
+
# Per-UID state (pidfiles + logs): the shared dir is root-owned when the
|
|
129
|
+
# backfill created it, and a user-side >>$LOG into a root 0755 dir fails
|
|
130
|
+
# BEFORE the app runs — the false "exited immediately" with an empty log.
|
|
131
|
+
D="/tmp/.ac-desktop-open.$(id -u)"
|
|
132
|
+
# Shared, root-written backfill markers — READ-ONLY here.
|
|
133
|
+
M=/tmp/.ac-desktop-open
|
|
134
|
+
mkdir -p "$D" 2>/dev/null || true
|
|
135
|
+
[ -w "$D" ] || { echo "ac-open: state dir $D is not writable by uid $(id -u)" >&2; exit 1; }
|
|
136
|
+
|
|
137
|
+
[ $# -ge 1 ] || { echo 'usage: ac-open <url|file|dir|app> [args...]' >&2; exit 2; }
|
|
138
|
+
TARGET=$1; shift
|
|
139
|
+
|
|
140
|
+
# No display = no desktop on this image. One honest line, no expedition.
|
|
141
|
+
if ! timeout 3 xdotool getdisplaygeometry >/dev/null 2>&1; then
|
|
142
|
+
echo "ac-open: no desktop display on $DISPLAY — this machine has no GUI session up" >&2
|
|
143
|
+
exit 1
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
pick_browser() {
|
|
147
|
+
for b in chromium chromium-browser x-www-browser google-chrome; do
|
|
148
|
+
command -v "$b" >/dev/null 2>&1 && { echo "$b"; return 0; }
|
|
149
|
+
done
|
|
150
|
+
return 1
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# Best-effort: raise the newest visible window whose class matches $1.
|
|
154
|
+
focus_class() {
|
|
155
|
+
WID=$(xdotool search --onlyvisible --class "$1" 2>/dev/null | tail -1) || WID=''
|
|
156
|
+
[ -n "\${WID:-}" ] && xdotool windowactivate "$WID" 2>/dev/null || true
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# Same-UID instance probe: another identity's instance shares neither profile
|
|
160
|
+
# nor single-instance socket, so only our own uid's counts as "running".
|
|
161
|
+
alive_same_uid() { pgrep -x -u "$(id -u)" "$1" >/dev/null 2>&1; }
|
|
162
|
+
|
|
163
|
+
# Singleton apps (browser, file manager): the second invocation hands the
|
|
164
|
+
# target to the running instance and exits — that IS the idempotent path, so
|
|
165
|
+
# spawn detached every time and only liveness-check a FRESH instance.
|
|
166
|
+
singleton_open() { # $1 = state key, rest = command + target
|
|
167
|
+
NAME=$1; shift
|
|
168
|
+
BIN=$(basename "$1")
|
|
169
|
+
PIDFILE=$D/$NAME.pid; LOG=$D/$NAME.log
|
|
170
|
+
RUNNING=0
|
|
171
|
+
alive_same_uid "$BIN" && RUNNING=1
|
|
172
|
+
[ "$RUNNING" = 0 ] && [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE" 2>/dev/null)" 2>/dev/null && RUNNING=1
|
|
173
|
+
setsid "$@" </dev/null >>"$LOG" 2>&1 &
|
|
174
|
+
PID=$!
|
|
175
|
+
if [ "$RUNNING" = 1 ]; then
|
|
176
|
+
sleep 1
|
|
177
|
+
focus_class "$BIN"
|
|
178
|
+
echo "ac-open: handed off to the running $BIN — no second window"
|
|
179
|
+
return 0
|
|
180
|
+
fi
|
|
181
|
+
echo "$PID" > "$PIDFILE"
|
|
182
|
+
sleep 1
|
|
183
|
+
if ! kill -0 "$PID" 2>/dev/null; then
|
|
184
|
+
# Died within the liveness window — but if OUR instance of $BIN is alive
|
|
185
|
+
# NOW, this was the single-instance HAND-OFF: the pre-check raced a
|
|
186
|
+
# still-starting instance, the fresh process handed the target over and
|
|
187
|
+
# exited 0. That is success (N calls = N tabs), never a crash report.
|
|
188
|
+
if alive_same_uid "$BIN"; then
|
|
189
|
+
focus_class "$BIN"
|
|
190
|
+
echo "ac-open: handed off to the running $BIN — no second window"
|
|
191
|
+
return 0
|
|
192
|
+
fi
|
|
193
|
+
TAIL=$(tail -c 300 "$LOG" 2>/dev/null | tr '\\n' ' ')
|
|
194
|
+
echo "ac-open: $BIN exited immediately: \${TAIL:-no output captured — see $LOG}" >&2
|
|
195
|
+
exit 1
|
|
196
|
+
fi
|
|
197
|
+
focus_class "$BIN"
|
|
198
|
+
echo "ac-open: launched $BIN (pid $PID), detached — it survives this command"
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
open_url() { # $1 = url
|
|
202
|
+
B=$(pick_browser) || {
|
|
203
|
+
# Grandfathered image mid-heal: the boot-time desktop-ensure found no
|
|
204
|
+
# browser and detached an apt install (browserBackfillCmd — the pidfile
|
|
205
|
+
# below is ITS single-flight marker, root-written in the SHARED dir).
|
|
206
|
+
# Third honest state: arriving.
|
|
207
|
+
if [ -f "$M/browser-install.pid" ] && kill -0 "$(cat "$M/browser-install.pid" 2>/dev/null)" 2>/dev/null; then
|
|
208
|
+
echo "${BROWSER_INSTALL_IN_PROGRESS_MSG}" >&2
|
|
209
|
+
exit 75
|
|
210
|
+
fi
|
|
211
|
+
if [ -f "$M/browser-install.failed" ]; then
|
|
212
|
+
TAIL=$(tail -c 300 "$M/browser-install.log" 2>/dev/null | tr '\\n' ' ')
|
|
213
|
+
echo "ac-open: the platform's background chromium install FAILED: \${TAIL:-see $M/browser-install.log} — report that cause in one line; do not apt-get one yourself." >&2
|
|
214
|
+
exit 1
|
|
215
|
+
fi
|
|
216
|
+
echo "ac-open: no graphical browser on this image (expected chromium) — cannot open $1. Report the missing browser in one line; do not apt-get one mid-session." >&2
|
|
217
|
+
exit 1
|
|
218
|
+
}
|
|
219
|
+
singleton_open browser "$B" "$1"
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
open_app() { # $1 = command, rest = args
|
|
223
|
+
command -v "$1" >/dev/null 2>&1 || {
|
|
224
|
+
echo "ac-open: '$1' is not installed on this machine — report the missing tool in one line instead of package-hunting for it" >&2
|
|
225
|
+
exit 127
|
|
226
|
+
}
|
|
227
|
+
BIN=$(basename "$1"); PIDFILE=$D/app-$BIN.pid; LOG=$D/app-$BIN.log
|
|
228
|
+
OLD=$(cat "$PIDFILE" 2>/dev/null || true)
|
|
229
|
+
if [ -n "\${OLD:-}" ] && kill -0 "$OLD" 2>/dev/null; then
|
|
230
|
+
WID=$(xdotool search --onlyvisible --pid "$OLD" 2>/dev/null | tail -1) || WID=''
|
|
231
|
+
[ -z "\${WID:-}" ] && { WID=$(xdotool search --onlyvisible --class "$BIN" 2>/dev/null | tail -1) || WID=''; }
|
|
232
|
+
[ -n "\${WID:-}" ] && xdotool windowactivate "$WID" 2>/dev/null || true
|
|
233
|
+
echo "ac-open: $BIN is already running (pid $OLD) — focused it instead of starting a second copy"
|
|
234
|
+
exit 0
|
|
235
|
+
fi
|
|
236
|
+
CMD=$1; shift
|
|
237
|
+
setsid "$CMD" "$@" </dev/null >>"$LOG" 2>&1 &
|
|
238
|
+
PID=$!
|
|
239
|
+
echo "$PID" > "$PIDFILE"
|
|
240
|
+
sleep 1
|
|
241
|
+
if ! kill -0 "$PID" 2>/dev/null; then
|
|
242
|
+
# Same hand-off grace as the singletons: VS Code and friends are
|
|
243
|
+
# single-instance too — a fresh spawn that exits at once while our own
|
|
244
|
+
# instance is alive HANDED OFF, it did not crash.
|
|
245
|
+
if alive_same_uid "$BIN"; then
|
|
246
|
+
focus_class "$BIN"
|
|
247
|
+
echo "ac-open: handed off to the running $BIN — no second window"
|
|
248
|
+
exit 0
|
|
249
|
+
fi
|
|
250
|
+
TAIL=$(tail -c 300 "$LOG" 2>/dev/null | tr '\\n' ' ')
|
|
251
|
+
echo "ac-open: $BIN exited immediately: \${TAIL:-no output captured — see $LOG}" >&2
|
|
252
|
+
exit 1
|
|
253
|
+
fi
|
|
254
|
+
echo "ac-open: launched $BIN (pid $PID), detached — it survives this command"
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
case "$TARGET" in
|
|
258
|
+
http://*|https://*|file://*|about:*|chrome://*)
|
|
259
|
+
open_url "$TARGET" ;;
|
|
260
|
+
localhost|localhost:*|localhost/*|127.0.0.1|127.0.0.1:*|127.0.0.1/*)
|
|
261
|
+
open_url "http://$TARGET" ;;
|
|
262
|
+
*)
|
|
263
|
+
if [ -d "$TARGET" ]; then
|
|
264
|
+
command -v pcmanfm >/dev/null 2>&1 || {
|
|
265
|
+
echo "ac-open: no file manager on this image (expected pcmanfm) — report it in one line" >&2
|
|
266
|
+
exit 1
|
|
267
|
+
}
|
|
268
|
+
case "$TARGET" in /*) ABS=$TARGET ;; *) ABS=$PWD/$TARGET ;; esac
|
|
269
|
+
singleton_open files pcmanfm "$ABS"
|
|
270
|
+
elif [ -f "$TARGET" ]; then
|
|
271
|
+
case "$TARGET" in /*) ABS=$TARGET ;; *) ABS=$PWD/$TARGET ;; esac
|
|
272
|
+
open_url "file://$ABS"
|
|
273
|
+
else
|
|
274
|
+
open_app "$TARGET" "$@"
|
|
275
|
+
fi ;;
|
|
276
|
+
esac
|
|
277
|
+
`;
|
|
278
|
+
|
|
279
|
+
/** Shim body for the generic-open entrypoints (`xdg-open`,
|
|
280
|
+
* `sensible-browser`): exec straight into the detaching launcher, so any
|
|
281
|
+
* tool that "opens a URL" survives its exec too. */
|
|
282
|
+
export const AC_OPEN_SHIM = `#!/bin/sh
|
|
283
|
+
# agent-compose: generic open routes through ac-open (the detaching launcher).
|
|
284
|
+
exec ${AC_OPEN_PATH} "$@"
|
|
285
|
+
`;
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Install (or refresh) `ac-open` + the generic-open shims — run as root.
|
|
289
|
+
* The script carries `$`/quotes/backslashes, so it rides the base64 idiom
|
|
290
|
+
* (the WRITE_AGENTS_MD / installRecorderCmd pattern): escaping-proof through
|
|
291
|
+
* the sandbox command channel. Always a plain truncating rewrite — the files
|
|
292
|
+
* are wholly platform-owned and tiny, so idempotence is free and a contract
|
|
293
|
+
* change reaches grandfathered VMs on the next session boot. Each write is
|
|
294
|
+
* `sh -n`-checked so a corrupted transit fails the install loudly, never the
|
|
295
|
+
* agent's first `ac-open`.
|
|
296
|
+
*/
|
|
297
|
+
export function installDesktopOpenCmd(): string {
|
|
298
|
+
const scriptB64 = Buffer.from(AC_OPEN_SCRIPT, "utf8").toString("base64");
|
|
299
|
+
const shimB64 = Buffer.from(AC_OPEN_SHIM, "utf8").toString("base64");
|
|
300
|
+
return [
|
|
301
|
+
`printf %s '${scriptB64}' | base64 -d > ${AC_OPEN_PATH}`,
|
|
302
|
+
`chmod 0755 ${AC_OPEN_PATH}`,
|
|
303
|
+
`sh -n ${AC_OPEN_PATH}`,
|
|
304
|
+
...AC_OPEN_SHIM_PATHS.flatMap((p) => [
|
|
305
|
+
`printf %s '${shimB64}' | base64 -d > ${p}`,
|
|
306
|
+
`chmod 0755 ${p}`,
|
|
307
|
+
`sh -n ${p}`,
|
|
308
|
+
]),
|
|
309
|
+
].join(" && ");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Chromium session-desktop defaults — SINGLE-SOURCED here (the ac-open
|
|
314
|
+
* two-door pattern): baked into fresh images by infra/e2b-template/parts.ts
|
|
315
|
+
* (CHROMIUM_DEFAULTS_INSTALL), re-installed at session desktop-ensure by the
|
|
316
|
+
* server (persistent.ts), and written by the browser backfill right after it
|
|
317
|
+
* lands chromium on a grandfathered image — so a backfilled browser behaves
|
|
318
|
+
* exactly like a baked one. Before this, a backfilled chromium had NO flags
|
|
319
|
+
* file: a root launch died on the missing --no-sandbox ("exited
|
|
320
|
+
* immediately"), and every launch paid the first-run wizard.
|
|
321
|
+
*
|
|
322
|
+
* Debian's /usr/bin/chromium wrapper dot-sources every /etc/chromium.d/*
|
|
323
|
+
* fragment, so these flags apply to EVERY door into the browser — the dock
|
|
324
|
+
* launcher, the openbox menu, ac-open, an agent's bare `chromium`.
|
|
325
|
+
*
|
|
326
|
+
* Flag notes:
|
|
327
|
+
* --disable-gpu — the session display is Xkasmvnc, which exposes NO GLX
|
|
328
|
+
* (probe-verified in a live sandbox, 2026-08-21). Without the flag,
|
|
329
|
+
* chromium's GPU process walks both EGL display types (OpenGL, then
|
|
330
|
+
* OpenGLES) through ANGLE, fails both ("Initialization of all (2) EGL
|
|
331
|
+
* display types failed"), respawns, and only then falls back to the
|
|
332
|
+
* software raster it was always going to use — measured at 200-500ms of
|
|
333
|
+
* pure startup waste per launch on an idle host, worse on a shared
|
|
334
|
+
* 2-vCPU guest. Software WebGL (SwiftShader) still works.
|
|
335
|
+
* --disable-session-crashed-bubble — a paused/killed VM otherwise greets
|
|
336
|
+
* every reattach with the restore bubble.
|
|
337
|
+
* --no-sandbox only under root — chromium refuses to start as root
|
|
338
|
+
* without it; the per-session microVM is the isolation boundary.
|
|
339
|
+
*/
|
|
340
|
+
export const CHROMIUM_DEFAULTS_PATH = "/etc/chromium.d/00-ac-defaults";
|
|
341
|
+
|
|
342
|
+
export const CHROMIUM_DEFAULTS = `# agent-compose session-desktop chromium defaults
|
|
343
|
+
# (single-sourced in sdk/src/agent/desktop-open.ts — sourced by /usr/bin/chromium)
|
|
344
|
+
CHROMIUM_FLAGS="$CHROMIUM_FLAGS --no-first-run --no-default-browser-check --password-store=basic --disable-session-crashed-bubble --disable-dev-shm-usage --start-maximized --disable-gpu"
|
|
345
|
+
if [ "$(id -u)" = "0" ]; then CHROMIUM_FLAGS="$CHROMIUM_FLAGS --no-sandbox"; fi
|
|
346
|
+
`;
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Install (or refresh) the chromium defaults fragment — run as root.
|
|
350
|
+
* QUOTE-FREE by construction (base64 payload, no quoting anywhere): the
|
|
351
|
+
* string embeds verbatim inside browserBackfillCmd's single-quoted `sh -c`
|
|
352
|
+
* payload as well as standing alone in a template runCmd / an ensure exec.
|
|
353
|
+
*/
|
|
354
|
+
export function installChromiumDefaultsCmd(): string {
|
|
355
|
+
const b64 = Buffer.from(CHROMIUM_DEFAULTS, "utf8").toString("base64");
|
|
356
|
+
return [
|
|
357
|
+
"mkdir -p /etc/chromium.d",
|
|
358
|
+
`printf %s ${b64} | base64 -d > ${CHROMIUM_DEFAULTS_PATH}`,
|
|
359
|
+
`chmod 0644 ${CHROMIUM_DEFAULTS_PATH}`,
|
|
360
|
+
`sh -n ${CHROMIUM_DEFAULTS_PATH}`,
|
|
361
|
+
].join(" && ");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Probe-then-heal for the browser on a GRANDFATHERED image — run as root at
|
|
366
|
+
* session desktop-ensure (server/src/sandbox/attach/browser-backfill.ts).
|
|
367
|
+
*
|
|
368
|
+
* Scope is exactly chromium, deliberately not a package set: every desktop
|
|
369
|
+
* bake since the first (8329c9c4) hard-installs xdotool/scrot/pcmanfm in the
|
|
370
|
+
* SAME `&&`-joined apt line as the desktop stack itself, so any image that
|
|
371
|
+
* probes a desktop necessarily carries them — chromium alone rode a tolerant
|
|
372
|
+
* `|| true` until the 2026-08-16 hard-install, making it the only
|
|
373
|
+
* manual-promised desktop tool that can be missing. (ac-open and ac-record
|
|
374
|
+
* are re-installed on every fresh boot by ensureSessionDesktop already.)
|
|
375
|
+
*
|
|
376
|
+
* Shape: a three-way verdict on stdout (the desktop-capability-probe idiom —
|
|
377
|
+
* `backfill=present` / `backfill=in-progress` / `backfill=started`), and on
|
|
378
|
+
* `started` the apt install runs DETACHED — `setsid … </dev/null >>log 2>&1 &`
|
|
379
|
+
* plus a pidfile, the exact ac-open/cli-agent discipline — so this command
|
|
380
|
+
* returns in milliseconds while apt takes its 20-40s. Single-flight is the
|
|
381
|
+
* pidfile liveness gate (a second ensure sees `in-progress` and starts
|
|
382
|
+
* nothing); apt's own dpkg lock backstops the millisecond race window. The
|
|
383
|
+
* installer's last act is its verdict: a failure touches the `.failed` marker
|
|
384
|
+
* (cause in the log) and either way the pidfile is removed, so a machine
|
|
385
|
+
* suspended mid-install simply retries on its next fresh boot.
|
|
386
|
+
*/
|
|
387
|
+
export function browserBackfillCmd(): string {
|
|
388
|
+
const pid = BROWSER_BACKFILL_PIDFILE;
|
|
389
|
+
// The defaults install rides INSIDE the success chain: a backfilled
|
|
390
|
+
// chromium without its flags fragment still crashes every root launch
|
|
391
|
+
// (missing --no-sandbox — the 2026-08-21 transcript's other half), so a
|
|
392
|
+
// browser without flags is a failed heal, reported as one.
|
|
393
|
+
// installChromiumDefaultsCmd() is quote-free by construction, so it embeds
|
|
394
|
+
// verbatim in this single-quoted payload.
|
|
395
|
+
return (
|
|
396
|
+
`mkdir -p ${AC_OPEN_STATE_DIR} && ` +
|
|
397
|
+
`if command -v chromium >/dev/null 2>&1; then echo backfill=present; ` +
|
|
398
|
+
`elif [ -f ${pid} ] && kill -0 "$(cat ${pid} 2>/dev/null)" 2>/dev/null; then echo backfill=in-progress; ` +
|
|
399
|
+
`else rm -f ${BROWSER_BACKFILL_FAILED}; ` +
|
|
400
|
+
`setsid sh -c 'if apt-get update -q && DEBIAN_FRONTEND=noninteractive apt-get install -y -q chromium && command -v chromium >/dev/null 2>&1 && ${installChromiumDefaultsCmd()}; then echo backfill-install=ok; else echo backfill-install=failed; touch ${BROWSER_BACKFILL_FAILED}; fi; rm -f ${pid}' </dev/null >>${BROWSER_BACKFILL_LOG} 2>&1 & ` +
|
|
401
|
+
`echo $! > ${pid}; echo backfill=started; fi`
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* The watcher's status probe (read-only, safe from any user): `backfill=ok` /
|
|
407
|
+
* `backfill=failed` (+ the log tail as the cause, one line) /
|
|
408
|
+
* `backfill=in-progress` / `backfill=lost` (installer died without a verdict
|
|
409
|
+
* — suspend/recycle mid-apt; the next fresh boot's ensure retries).
|
|
410
|
+
*/
|
|
411
|
+
export function browserBackfillStatusCmd(): string {
|
|
412
|
+
return (
|
|
413
|
+
`if command -v chromium >/dev/null 2>&1; then echo backfill=ok; ` +
|
|
414
|
+
`elif [ -f ${BROWSER_BACKFILL_FAILED} ]; then echo backfill=failed; tail -c 300 ${BROWSER_BACKFILL_LOG} 2>/dev/null | tr '\\n' ' '; ` +
|
|
415
|
+
`elif [ -f ${BROWSER_BACKFILL_PIDFILE} ] && kill -0 "$(cat ${BROWSER_BACKFILL_PIDFILE} 2>/dev/null)" 2>/dev/null; then echo backfill=in-progress; ` +
|
|
416
|
+
`else echo backfill=lost; fi`
|
|
417
|
+
);
|
|
418
|
+
}
|