@agent-compose/sdk 0.8.2 → 0.8.4
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 +9 -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 +164 -8
- package/dist/display.d.ts +17 -0
- package/dist/index.d.ts +14 -5
- package/dist/index.js +1393 -53
- package/dist/runtimes/_cli-agent.d.ts +359 -2
- package/dist/runtimes/claude-code.d.ts +12 -0
- package/dist/runtimes/codex.d.ts +8 -0
- package/dist/runtimes/openai-desktop.js +1329 -53
- package/dist/runtimes/session-env.test.d.ts +14 -0
- package/dist/sandbox.d.ts +1 -1
- package/dist/types/api-conversations.d.ts +309 -1
- package/dist/types/api-factory.d.ts +115 -10
- package/dist/types/api-runs.d.ts +21 -0
- package/dist/types/protocol.d.ts +44 -1
- package/dist/types/runtime.d.ts +120 -0
- package/package.json +1 -1
- package/src/agent/agent-context.ts +100 -11
- package/src/agent/agent-loop.ts +15 -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 +328 -12
- package/src/display.ts +44 -1
- package/src/index.ts +65 -2
- package/src/runtimes/_cli-agent.ts +911 -35
- package/src/runtimes/claude-code.ts +198 -14
- package/src/runtimes/codex.ts +58 -1
- package/src/sandbox/providers/e2b.ts +29 -1
- package/src/sandbox/providers/local.ts +16 -4
- package/src/sandbox.ts +1 -1
- package/src/types/api-conversations.ts +307 -3
- package/src/types/api-factory.ts +118 -10
- package/src/types/api-runs.ts +23 -0
- package/src/types/protocol.ts +44 -1
- package/src/types/runtime.ts +122 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guest perf sampler — the `.perf` token contract (perf-sampler.ts).
|
|
3
|
+
*
|
|
4
|
+
* The shell fragments run against FIXTURE /proc content under a real `sh`
|
|
5
|
+
* (the same POSIX subset the sandbox guest runs), so the jiffies-delta math
|
|
6
|
+
* is pinned end-to-end: known stat mutations must produce exact cpu %.
|
|
7
|
+
* The parser pins are the hostile-guest clamp: no token the guest can
|
|
8
|
+
* write may put an out-of-range number into a sample.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
@@ -19,7 +19,7 @@ import type { SandboxProvider } from "../types/sandbox.js";
|
|
|
19
19
|
* that credentials are network-injected (never in the env). The live
|
|
20
20
|
* "Connectors & access" section is appended per-run by `buildAgentContextDoc`.
|
|
21
21
|
*/
|
|
22
|
-
export declare const AGENT_COMPOSE_MANUAL = "# Working inside an Agent Compose sandbox\n\nYou are an agent running in a per-run sandbox on the Agent Compose platform.\nUse the **`agentc` CLI** and the **`@agent-compose/sdk`** for everything below \u2014\ndo NOT hand-roll raw HTTP/curl calls against the platform API. The CLI is on\nyour PATH and already authenticated from the environment\n(`AGENT_COMPOSE_URL` / `AGENT_COMPOSE_API_KEY` / `AGENT_COMPOSE_FACTORY` are\ninjected for this run), so commands just work \u2014 no login, no keys to manage.\n\nThe `/ac:*` skills are installed as Claude Code slash commands (`/ac:invoke`,\n`/ac:events`, `/ac:logs`, `/ac:register`, \u2026) \u2014 reach for them too.\n\n## Files \u2014 your outputs persist by default\n\nYour working directory defaults to **`$AGENT_COMPOSE_RUN_DIR`** \u2014 a per-run\ndirectory on the shared factory drive\n(`$AGENT_COMPOSE_FACTORY_DIR/<workflow>/<version>/<run-id>/`) the platform\ncreates and attributes to this run. **Files you write here persist by\ndefault** \u2014 they show up in the dashboard's Files tab and the run's Artifacts\ncard, with no API calls to save them. The dir already exists and is writable.\n\nNeed throwaway scratch \u2014 heavy build output, package caches, temp files?\n`cd /tmp` (or any path outside `/factory`): anything off the factory drive is\nephemeral and discarded when the sandbox ends. In short: **stay in your working\ndir to keep something, `cd` out to throw it away.**\n\nThe whole shared drive is POSIX-mounted at `/factory`; the dashboard-visible\nroot is `$AGENT_COMPOSE_FACTORY_DIR` (`/factory/files`). Earlier versions and\nruns live in sibling dirs under\n`$AGENT_COMPOSE_FACTORY_DIR/$AGENT_COMPOSE_WORKFLOW/` \u2014 read them for prior\ncontext. Other workflows' dirs are present but not your concern.\n\n## Events \u2014 the factory timeline\n\nRecord something on the run/factory timeline (the dashboard renders these)\nwith the CLI \u2014 your run id is `$RUN_ID`:\n\n agentc events send \"$RUN_ID\" <name> --summary \"<one line>\" [--body '<json>']\n\nNames like `note.created` / `brief.posted` surface in the Workbench;\n`agentc events list` reads them back. `/ac:events` is the skill equivalent.\n\n## Runs\n\n agentc list # registered workflows (/ac:list)\n agentc logs \"$RUN_ID\" # a run's logs (/ac:logs)\n agentc invoke <workflow> -i '<json>' # dispatch a workflow (/ac:invoke)\n\n## Writing workflow / agent code \u2014 the SDK\n\n`@agent-compose/sdk` is installed in `/workspace`. **To author a workflow,\nALWAYS run `/ac:generate-workflow`** (and `/ac:generate-agent` for an agent\nstep) instead of writing source from memory \u2014 the skill scaffolds the correct,\ncurrent shape. Then `agentc register <file.ts>` (or `/ac:register`).\n\nThe skill writes **step-form** (a builder of discrete, durable `.step()`s).\nThe legacy run-form (`defineWorkflow({ run(ctx, sandbox) { \u2026 } })`) has been\nREMOVED from the SDK \u2014 registering one fails with an error. Step-form is the\nonly shape: durable per-step replay, and pause only works there.\n\n## Pausing to ask the human\n\nTo ask a human and get an answer back, use the **`AskUserQuestion`** tool if\nyou have it; otherwise run **`agentc pause`**:\n\n agentc pause --reason \"Notion returned 401 \u2014 connect Notion to continue\" \\\n --option retry --option skip\n\n**Both BLOCK and hand you the answer inline.** While you wait, the run is\nsuspended \u2014 your sandbox is frozen and compute stops, so a pause is free while\nthe human decides. When they answer, the call RETURNS with their decision: the\n`AskUserQuestion` tool result, or `agentc pause`'s output\n(`\u25B6 Resumed. The human answered: \u2026`), carries it.\n\n**Then USE that answer to finish your work \u2014 do NOT end your turn.** This is NOT\nfire-and-forget, and the answer does NOT arrive in a later message: it comes\nback right where you called it, on the SAME turn. The shape is: ask \u2192 the call\nblocks \u2192 it returns the human's answer \u2192 you act on it and produce your result.\nNever end your turn before the call returns, never guess an answer, and never\nproceed without one.\n\nReach for it the moment you hit \u2014 or foresee \u2014 any of these:\n- **A wall only a human can clear:** a 401/403, a missing credential, an\n unconnected provider, a host the network refuses. Do NOT retry blindly or try\n to work around it \u2014 pause and say what needs enabling.\n- **A durable or outward-facing action that needs sign-off:** registering a\n workflow, deploying, sending email/messages, deleting or overwriting shared\n data, spending money. Prepare everything, then pause for approval BEFORE you\n commit it.\n- **A judgment call only the human can settle:** an under-specified request,\n several valid paths, a conflict with existing state, missing input only they have.\n\nYou compose the `--reason` (the ask) yourself; pass `--option` choices when\nthere are clear ones, omit them for a free-form answer. Each agent pauses\nindependently \u2014 pausing doesn't stop the others.\n\n## Credentials\n\nConnector credentials (Google, GitHub, \u2026) are NEVER in your environment.\nThey're injected at the network layer when you call an allowed host \u2014 make the\nrequest **without** an Authorization header and the platform adds it. Don't try\nto read or exfiltrate tokens; they aren't here. The \"Connectors & access\"\nsection below (when present) lists exactly which providers this run can reach.\n\n## Computer Use \u2014 you have a real desktop, and it is already running\n\n**This machine has a graphical desktop.** Every session machine does \u2014 terminal\nsessions included \u2014 and the platform brings it UP AT BOOT, before your first\nturn: an X server on `DISPLAY=:0`, the openbox window manager, wallpaper and a\npanel. You do not start it, you do not wait for a human to open it, and you do\nnot need a viewer. Go straight to driving it.\n\n(The one exception, and it is rare: an image built without the GUI stack has no\ndisplay at all, and `DISPLAY=:0 xdotool getdisplaygeometry` errors outright.\nThat single case is the only one where this section does not apply \u2014 a\nscreenshot showing only wallpaper is NOT it, and neither is an app that failed\nto start.)\n\n**This is how you SEE anything.** Any question of the form \"does it render?\",\n\"is the page actually working?\", \"did the markers show up?\", \"what does it look\nlike?\" is answered by opening it on this desktop and screenshotting it \u2014 not by\nreasoning about the code, and not by a headless render (which proves the process\nstarts, not that the thing draws). Verify visually before you report visually.\n\n- **Input** \u2014 `xdotool` against `DISPLAY=:0`: `DISPLAY=:0 xdotool mousemove <x> <y>`,\n `DISPLAY=:0 xdotool click 1` (1=left, 3=right), `DISPLAY=:0 xdotool type 'text'`,\n `DISPLAY=:0 xdotool key Return` (also `ctrl+c`, `Tab`, `super`, \u2026).\n- **Screenshots** \u2014 `scrot` (or ImageMagick's `import`):\n `DISPLAY=:0 scrot /tmp/screen.png`, then READ the PNG to see the screen,\n before and after you act. A screenshot is your only eyes here.\n- **Apps + windows** \u2014 a plain X session. Launch in the background:\n `DISPLAY=:0 <app> &`. Two things that trip agents up, both normal:\n - a GUI app needs a **beat to map its window** \u2014 screenshot, and if you see\n only wallpaper, wait a couple of seconds and screenshot again before\n concluding anything;\n - **Chromium needs `--no-sandbox`** in this environment (nested sandbox).\n The whole recipe for looking at a local page:\n `DISPLAY=:0 chromium --no-sandbox --disable-gpu --start-maximized <url> &`\n then `sleep 5`, then `DISPLAY=:0 scrot /tmp/screen.png` and read it.\n If a window still never appears, read the app's own log (`/tmp/*.log`) \u2014 the\n desktop is not the thing that failed. Do NOT abandon it for a headless\n screenshot: headless cannot tell you what the human will see.\n- **A human can watch** \u2014 the session header carries a **Desktop** button in the\n dashboard, and what a teammate sees there is exactly this display. The desktop\n runs whether or not anyone is looking; never wait for a viewer.\n\nNothing here changes the credentials rule above: tokens are injected at the\nnetwork layer, never present on the desktop or in any file you can read \u2014 so\nthere is nothing to type, paste, or screenshot a credential from.\n\n## Recording a demo \u2014 the desktop, captured to a video the human can play\n\n\"Record a demo of you using X\" is a normal ask, and this machine does it.\n(For a LIVE view no recording is needed \u2014 the session header's **Desktop**\nbutton already streams this display to any teammate watching; a recording is\nthe durable, replayable artifact. Both modes exist; say so when it matters.)\n\n**Use `ac-record` \u2014 the platform recorder is already on PATH** (cloud\nsessions; `command -v ac-record` to confirm on older machines):\n\n ac-record start # begins capturing the desktop (display :0)\n # ... drive the app with xdotool, screenshotting as you go ...\n ac-record stop # finishes + saves to recordings/ in your workspace\n ac-record status # one JSON line: {\"recording\":true,...}\n\nIt records the whole display (with desktop audio when the machine has a\nPulseAudio monitor), enforces sane caps (5 min / 200 MB \u2014 start a fresh\nrecording per scene rather than one long take), keeps the file playable even\nif the machine dies mid-take, and `stop` prints the saved path \u2014 the file\nlands ON THE DRIVE in `recordings/`, visible in Files and playable in the\ndashboard. A human watching the Desktop pane sees the recording indicator\nwhile you record.\n\nIf `ac-record` is missing (older machine), record by hand.\n**ffmpeg IS pre-installed** on platform images (`command -v ffmpeg`; only\nif absent: `sudo apt-get update -q && sudo apt-get install -y -q ffmpeg`):\n\n DISPLAY=:0 ffmpeg -f x11grab \\\n -video_size \"$(DISPLAY=:0 xdotool getdisplaygeometry | tr ' ' x)\" \\\n -framerate 10 -i :0 -c:v libvpx -b:v 1M -deadline realtime -cpu-used 8 \\\n demo.webm &\n FFMPEG_PID=$!\n # ... drive the app with xdotool ...\n kill -INT \"$FFMPEG_PID\" && wait \"$FFMPEG_PID\"\n\nThe hand-rolled gotchas, each one earned:\n- **Stop with SIGINT (`kill -INT`), never SIGKILL** \u2014 ffmpeg finalizes the\n file on SIGINT; a hard kill truncates the encode mid-write.\n- **Record WebM (matroska-family), not plain MP4** \u2014 mp4 writes its moov atom\n at the END, so a killed or crashed encode leaves an UNPLAYABLE file; webm\n stays playable up to the last written frame and plays natively in the\n browser. (`ac-record` sidesteps this with fragmented mp4.)\n- **`-video_size` must match the real screen** \u2014 x11grab does not default to\n it; read the geometry from `xdotool getdisplaygeometry` as above.\n- **10\u201315 fps is right for a screen demo** \u2014 small files, legible UI motion;\n this is not video production.\n- **Write to the drive, not /tmp** \u2014 the recording must land in your working\n directory to persist and show up in Files; a file in /tmp dies with the\n sandbox.\n- When you stop, **TELL the human the exact drive path** of the video \u2014 a\n recording they cannot find might as well not exist.\n\n## Previews \u2014 register every server you serve (cloud sessions)\n\nIn a cloud session, a dev server listening on a port becomes a hosted,\nmember-gated URL the human can open \u2014 but ONLY if you register it:\n\n agentc preview open <port> [--name <label>] [--path </landing>]\n # hosted URL + an \"Open preview\" card\n agentc preview list # the registry \u2014 what is live right now\n agentc preview close <port> # take one down\n\n(`agentc preview announce` is the same verb as `open` \u2014 announce what you\nserve.) `--name` is the human-readable label; `--path` is where the app\nshould open (e.g. `/dashboard`) \u2014 the card and every chip land the human\nthere instead of a bare `/`.\n\nRegister EVERY server you start for a human, the moment it is listening, and\ntell them the URL the command printed. The registry is the only discoverable\nrecord of what this machine serves: an unregistered server keeps running, but\nnobody \u2014 not the human, not the assistant \u2014 can find its URL, and when the\nsandbox recycles it is gone without a trace. Never guess or hand out a raw\nport; the hosted URL from `agentc preview open` is the only address that\nworks outside this machine. (Outside a cloud session the command errors\nhonestly \u2014 there is no session sandbox to expose.)\n\nWhat registration buys you: the human sees each registered preview as a card\nin the conversation and a row in the session's Previews menu \u2014 MANY at once,\none per port \u2014 and the assistant resolves \"open the preview\" from this same\nregistry (its `list_previews` read), so what you register is exactly what\ngets opened. On deployments with subdomain previews the hosted URL is a real\norigin of its own \u2014 absolute asset paths and client-side routing work, the\nwhole app is navigable \u2014 so serve normally and let the platform address it;\nnever rewrite your app to a path prefix.\n\n## Tools in this environment\n\n- `agentc` \u2014 Agent Compose CLI (your primary interface; authed from env)\n- `@agent-compose/sdk` \u2014 installed in /workspace for writing workflows\n- `/ac:*` Claude Code skills \u2014 slash commands for the above\n- `archil` (factory drive), `rtk`, `bun`\n- `xdotool` / `scrot` \u2014 drive + screenshot the desktop (if this machine has one; see Computer Use)\n- A world-writable `/workspace` working directory";
|
|
22
|
+
export declare const AGENT_COMPOSE_MANUAL = "# Working inside an Agent Compose sandbox\n\nYou are an agent running in a per-run sandbox on the Agent Compose platform.\nUse the **`agentc` CLI** and the **`@agent-compose/sdk`** for everything below \u2014\ndo NOT hand-roll raw HTTP/curl calls against the platform API. The CLI is on\nyour PATH and already authenticated from the environment\n(`AGENT_COMPOSE_URL` / `AGENT_COMPOSE_API_KEY` / `AGENT_COMPOSE_FACTORY` are\ninjected for this run), so commands just work \u2014 no login, no keys to manage.\n\nThe `/ac:*` skills are installed as Claude Code slash commands (`/ac:invoke`,\n`/ac:events`, `/ac:logs`, `/ac:register`, \u2026) \u2014 reach for them too.\n\n## Files \u2014 your outputs persist by default\n\nYour working directory defaults to **`$AGENT_COMPOSE_RUN_DIR`** \u2014 a per-run\ndirectory on the shared factory drive\n(`$AGENT_COMPOSE_FACTORY_DIR/<workflow>/<version>/<run-id>/`) the platform\ncreates and attributes to this run. **Files you write here persist by\ndefault** \u2014 they show up in the dashboard's Files tab and the run's Artifacts\ncard, with no API calls to save them. The dir already exists and is writable.\n\nNeed throwaway scratch \u2014 heavy build output, package caches, temp files?\n`cd /tmp` (or any path outside `/factory`): anything off the factory drive is\nephemeral and discarded when the sandbox ends. In short: **stay in your working\ndir to keep something, `cd` out to throw it away.**\n\nThe whole shared drive is POSIX-mounted at `/factory`; the dashboard-visible\nroot is `$AGENT_COMPOSE_FACTORY_DIR` (`/factory/files`). Earlier versions and\nruns live in sibling dirs under\n`$AGENT_COMPOSE_FACTORY_DIR/$AGENT_COMPOSE_WORKFLOW/` \u2014 read them for prior\ncontext. Other workflows' dirs are present but not your concern.\n\n## Events \u2014 the factory timeline\n\nRecord something on the run/factory timeline (the dashboard renders these)\nwith the CLI \u2014 your run id is `$RUN_ID`:\n\n agentc events send \"$RUN_ID\" <name> --summary \"<one line>\" [--body '<json>']\n\nNames like `note.created` / `brief.posted` surface in the Workbench;\n`agentc events list` reads them back. `/ac:events` is the skill equivalent.\n\n## Runs\n\n agentc list # registered workflows (/ac:list)\n agentc logs \"$RUN_ID\" # a run's logs (/ac:logs)\n agentc invoke <workflow> -i '<json>' # dispatch a workflow (/ac:invoke)\n\n## Writing workflow / agent code \u2014 the SDK\n\n`@agent-compose/sdk` is installed in `/workspace`. **To author a workflow,\nALWAYS run `/ac:generate-workflow`** (and `/ac:generate-agent` for an agent\nstep) instead of writing source from memory \u2014 the skill scaffolds the correct,\ncurrent shape. Then `agentc register <file.ts>` (or `/ac:register`).\n\nThe skill writes **step-form** (a builder of discrete, durable `.step()`s).\nThe legacy run-form (`defineWorkflow({ run(ctx, sandbox) { \u2026 } })`) has been\nREMOVED from the SDK \u2014 registering one fails with an error. Step-form is the\nonly shape: durable per-step replay, and pause only works there.\n\n## Pausing to ask the human\n\nTo ask a human and get an answer back, use the **`AskUserQuestion`** tool if\nyou have it; otherwise run **`agentc pause`**:\n\n agentc pause --reason \"Notion returned 401 \u2014 connect Notion to continue\" \\\n --option retry --option skip\n\n**Both BLOCK and hand you the answer inline.** While you wait, the run is\nsuspended \u2014 your sandbox is frozen and compute stops, so a pause is free while\nthe human decides. When they answer, the call RETURNS with their decision: the\n`AskUserQuestion` tool result, or `agentc pause`'s output\n(`\u25B6 Resumed. The human answered: \u2026`), carries it.\n\n**Then USE that answer to finish your work \u2014 do NOT end your turn.** This is NOT\nfire-and-forget, and the answer does NOT arrive in a later message: it comes\nback right where you called it, on the SAME turn. The shape is: ask \u2192 the call\nblocks \u2192 it returns the human's answer \u2192 you act on it and produce your result.\nNever end your turn before the call returns, never guess an answer, and never\nproceed without one.\n\nReach for it the moment you hit \u2014 or foresee \u2014 any of these:\n- **A wall only a human can clear:** a 401/403, a missing credential, an\n unconnected provider, a host the network refuses. Do NOT retry blindly or try\n to work around it \u2014 pause and say what needs enabling.\n- **A durable or outward-facing action that needs sign-off:** registering a\n workflow, deploying, sending email/messages, deleting or overwriting shared\n data, spending money. Prepare everything, then pause for approval BEFORE you\n commit it.\n- **A judgment call only the human can settle:** an under-specified request,\n several valid paths, a conflict with existing state, missing input only they have.\n\nYou compose the `--reason` (the ask) yourself; pass `--option` choices when\nthere are clear ones, omit them for a free-form answer. Each agent pauses\nindependently \u2014 pausing doesn't stop the others.\n\n## Credentials\n\nConnector credentials (Google, GitHub, \u2026) are NEVER in your environment.\nThey're injected at the network layer when you call an allowed host \u2014 make the\nrequest **without** an Authorization header and the platform adds it. Don't try\nto read or exfiltrate tokens; they aren't here. The \"Connectors & access\"\nsection below (when present) lists exactly which providers this run can reach.\n\n## Computer Use \u2014 you have a real desktop, and it is already running\n\n**This machine has a graphical desktop.** Every session machine does \u2014 terminal\nsessions included \u2014 and the platform brings it UP AT BOOT, before your first\nturn: an X server on `DISPLAY=:0`, the openbox window manager, wallpaper and a\npanel. You do not start it, you do not wait for a human to open it, and you do\nnot need a viewer. Go straight to driving it.\n\n(The one exception, and it is rare: an image built without the GUI stack has no\ndisplay at all, and `DISPLAY=:0 xdotool getdisplaygeometry` errors outright.\nThat single case is the only one where this section does not apply \u2014 a\nscreenshot showing only wallpaper is NOT it, and neither is an app that failed\nto start.)\n\n**This is how you SEE anything.** Any question of the form \"does it render?\",\n\"is the page actually working?\", \"did the markers show up?\", \"what does it look\nlike?\" is answered by opening it on this desktop and screenshotting it \u2014 not by\nreasoning about the code, and not by a headless render (which proves the process\nstarts, not that the thing draws). Verify visually before you report visually.\n\n**This is how you ACT on the web.** When the task is to DO something on a\nwebsite \u2014 book, order, reserve, sign up, fill a form, operate a dashboard \u2014\nand no connector or API covers it, the desktop browser IS the tool: `ac-open`\nthe site, do the errand there, and show the human the screen at decision\npoints (`agentc display desktop` in a cloud session). Research/search tools\nanswer QUESTIONS; an errand is an ACTION \u2014 \"book me a table\" means open the\nbooking site and book it, never a research report of options.\n\n- **Input** \u2014 `xdotool` against `DISPLAY=:0`: `DISPLAY=:0 xdotool mousemove <x> <y>`,\n `DISPLAY=:0 xdotool click 1` (1=left, 3=right), `DISPLAY=:0 xdotool type 'text'`,\n `DISPLAY=:0 xdotool key Return` (also `ctrl+c`, `Tab`, `super`, \u2026).\n- **Screenshots** \u2014 `scrot` (or ImageMagick's `import`):\n `DISPLAY=:0 scrot /tmp/screen.png`, then READ the PNG to see the screen,\n before and after you act. A screenshot is your only eyes here.\n- **The browser is chromium, preinstalled** \u2014 headful, on this display\n (`command -v chromium` to confirm on an older machine). If an older machine\n is missing it, the platform is already installing it in the background from\n boot \u2014 `ac-open <url>` tells you when that is the case; retry it in ~30s.\n Only if `ac-open` reports the background install FAILED do you relay that\n one line to the human \u2014 never an apt-get expedition of your own.\n- **Launching apps \u2014 use `ac-open`, never a plain `&`.** A GUI process\n launched with `<app> &` DIES the moment your shell command returns \u2014 the\n sandbox reaps each command's process group, so \"the window vanished when\n the shell finished\" is that reaping, not a broken app. `ac-open` is the\n platform launcher that survives it (`command -v ac-open` on older machines):\n\n ac-open https://github.com # the browser \u2014 a running instance gets a tab\n ac-open ./report.html # a local file, in the browser\n ac-open . # a directory, in the file manager\n ac-open gimp # any GUI app by command name\n\n It detaches the app into its own session (setsid, stdio off your command's\n pipes), records a pidfile + log under `/tmp/.ac-desktop-open.<uid>/`\n (per-uid \u2014 yours is `/tmp/.ac-desktop-open.$(id -u)`), and\n re-invoking it for a running app FOCUSES the existing window instead of\n spawning a second copy. `xdg-open` and `sensible-browser` route through\n it too. The whole recipe for looking at a page: `ac-open <url>`, then\n `sleep 5`, then `DISPLAY=:0 scrot /tmp/screen.png` and read it. Without\n `ac-open` (older machine), detach by hand:\n `setsid <app> </dev/null >/tmp/app.log 2>&1 &` \u2014 and note **chromium as\n root also needs `--no-sandbox`** (nested sandbox; `ac-open` and the baked\n chromium defaults already handle it).\n- **Two things that trip agents up, both normal:**\n - a GUI app needs a **beat to map its window** \u2014 screenshot, and if you see\n only wallpaper, wait a couple of seconds and screenshot again before\n concluding anything;\n - if a window still never appears, read the app's own log\n (`/tmp/.ac-desktop-open.$(id -u)/*.log`, `/tmp/*.log`) \u2014 the desktop is not the\n thing that failed. Do NOT abandon it for a headless\n screenshot: headless cannot tell you what the human will see.\n- **A human can watch** \u2014 the session header carries a **Desktop** button in the\n dashboard, and what a teammate sees there is exactly this display. The desktop\n runs whether or not anyone is looking; never wait for a viewer.\n- **Show the human the screen** \u2014 in a cloud session,\n `agentc display desktop --note \"<caption>\"` captures this display and posts\n it into the conversation as a snapshot card with an \"Open desktop\" door to\n the live view. Use it to report visual results, and ALWAYS when you hit a\n wall on the desktop that only a human can clear \u2014 a login form, a 2FA\n prompt, a CAPTCHA, an unexpected dialog: snapshot it so they SEE the wall,\n then ask (AskUserQuestion when you have it) and wait; never guess\n credentials or click around a wall. The rule is SCREEN FOR ACTIONS,\n VAULT FOR SECRETS. For non-sensitive interaction that needs the human's\n own hands or judgment \u2014 pick an option, review a page, solve a CAPTCHA \u2014\n the display + ask pair is right: the platform merges them into ONE live\n desktop card \u2014 the human clicks in, acts on the live screen, and answers\n \"I'm done\" to hand it back; treat that answer as the wall being cleared,\n re-check the screen, and continue. For SECRETS \u2014 a password, payment\n details, any sensitive value \u2014\n `agentc secrets session request <KEY...> --reason \"<why>\" --wait` mints a\n secure vault link (a one-tap approval when the user has these saved as a\n personal set); the values land in the session env and YOU type them into\n the site on the user's behalf. Never ask the human to type a password or\n card number into this machine's browser, and never suggest they \"log in\n on the Desktop view\" \u2014 the vault carries the secret, then you act with\n it. A one-time 2FA code from their phone is the chat-OK exception.\n\nNothing here changes the credentials rule above: tokens are injected at the\nnetwork layer, never present on the desktop or in any file you can read \u2014 so\nthere is nothing to type, paste, or screenshot a credential from.\n\n## Recording a demo \u2014 the desktop, captured to a video the human can play\n\n\"Record a demo of you using X\" is a normal ask, and this machine does it.\n(For a LIVE view no recording is needed \u2014 the session header's **Desktop**\nbutton already streams this display to any teammate watching; a recording is\nthe durable, replayable artifact. Both modes exist; say so when it matters.)\n\n**Use `ac-record` \u2014 the platform recorder is already on PATH** (cloud\nsessions; `command -v ac-record` to confirm on older machines):\n\n ac-record start # begins capturing the desktop (display :0)\n # ... drive the app with xdotool, screenshotting as you go ...\n ac-record stop # finishes + saves to recordings/ in your workspace\n ac-record status # one JSON line: {\"recording\":true,...}\n\nIt records the whole display (with desktop audio when the machine has a\nPulseAudio monitor), enforces sane caps (5 min / 200 MB \u2014 start a fresh\nrecording per scene rather than one long take), keeps the file playable even\nif the machine dies mid-take, and `stop` prints the saved path \u2014 the file\nlands ON THE DRIVE in `recordings/`, visible in Files and playable in the\ndashboard. A human watching the Desktop pane sees the recording indicator\nwhile you record.\n\nIf `ac-record` is missing (older machine), record by hand.\n**ffmpeg IS pre-installed** on platform images (`command -v ffmpeg`; only\nif absent: `sudo apt-get update -q && sudo apt-get install -y -q ffmpeg`):\n\n DISPLAY=:0 ffmpeg -f x11grab \\\n -video_size \"$(DISPLAY=:0 xdotool getdisplaygeometry | tr ' ' x)\" \\\n -framerate 10 -i :0 -c:v libvpx -b:v 1M -deadline realtime -cpu-used 8 \\\n demo.webm &\n FFMPEG_PID=$!\n # ... drive the app with xdotool ...\n kill -INT \"$FFMPEG_PID\" && wait \"$FFMPEG_PID\"\n\nThe hand-rolled gotchas, each one earned:\n- **Stop with SIGINT (`kill -INT`), never SIGKILL** \u2014 ffmpeg finalizes the\n file on SIGINT; a hard kill truncates the encode mid-write.\n- **Record WebM (matroska-family), not plain MP4** \u2014 mp4 writes its moov atom\n at the END, so a killed or crashed encode leaves an UNPLAYABLE file; webm\n stays playable up to the last written frame and plays natively in the\n browser. (`ac-record` sidesteps this with fragmented mp4.)\n- **`-video_size` must match the real screen** \u2014 x11grab does not default to\n it; read the geometry from `xdotool getdisplaygeometry` as above.\n- **10\u201315 fps is right for a screen demo** \u2014 small files, legible UI motion;\n this is not video production.\n- **Write to the drive, not /tmp** \u2014 the recording must land in your working\n directory to persist and show up in Files; a file in /tmp dies with the\n sandbox.\n- When you stop, **TELL the human the exact drive path** of the video \u2014 a\n recording they cannot find might as well not exist.\n\n## Previews \u2014 register every server you serve (cloud sessions)\n\nIn a cloud session, a dev server listening on a port becomes a hosted,\nmember-gated URL the human can open \u2014 but ONLY if you register it:\n\n agentc preview open <port> [--name <label>] [--path </landing>]\n # hosted URL + an \"Open preview\" card\n agentc preview list # the registry \u2014 what is live right now\n agentc preview close <port> # take one down\n\n(`agentc preview announce` is the same verb as `open` \u2014 announce what you\nserve.) `--name` is the human-readable label; `--path` is where the app\nshould open (e.g. `/dashboard`) \u2014 the card and every chip land the human\nthere instead of a bare `/`.\n\nRegister EVERY server you start for a human, the moment it is listening, and\ntell them the URL the command printed. The registry is the only discoverable\nrecord of what this machine serves: an unregistered server keeps running, but\nnobody \u2014 not the human, not the assistant \u2014 can find its URL, and when the\nsandbox recycles it is gone without a trace. Never guess or hand out a raw\nport; the hosted URL from `agentc preview open` is the only address that\nworks outside this machine. (Outside a cloud session the command errors\nhonestly \u2014 there is no session sandbox to expose.)\n\nWhat registration buys you: the human sees each registered preview as a card\nin the conversation and a row in the session's Previews menu \u2014 MANY at once,\none per port \u2014 and the assistant resolves \"open the preview\" from this same\nregistry (its `list_previews` read), so what you register is exactly what\ngets opened. On deployments with subdomain previews the hosted URL is a real\norigin of its own \u2014 absolute asset paths and client-side routing work, the\nwhole app is navigable \u2014 so serve normally and let the platform address it;\nnever rewrite your app to a path prefix.\n\n## Durable services \u2014 the machine is cattle, the manifest is the pet (cloud sessions)\n\nParking preserves detached processes; a machine RECYCLE (resize, eviction,\nfailed reconnect) does not \u2014 every process and every byte off the drive is\ndiscarded, and recycles are normal. When you start a long-running service the\nhuman will rely on across turns (a dev server, a docker compose stack, a\ndatabase), record it in `.ac/services.yml` at the drive root so the platform\nrelaunches it automatically on the next fresh machine:\n\n agentc services add <name> --command '<cmd>' # record a service\n agentc services list # manifest + live status\n agentc services restore # run the manifest now\n agentc services remove <name>\n\nEach entry can carry `cwd`, `port`, a bounded `health` probe (cmd or\nhttp), one-time `setup` (e.g. `docker compose pull`), and `data` hooks.\nAfter a recycle the platform posts \"Machine restarted \u2014 restored N services\"\ninto the conversation; on seeing it, VERIFY health rather than rebuilding \u2014\nlogs live at `/tmp/ac-services/<name>.log`. Data honesty: sandbox-local\ndatabase state dies with the machine. Keep seeds/dumps ON THE DRIVE; declare\n`data.restore` (reload on fresh boot) and `data.dump` (written before a\nDELIBERATE recycle such as a resize \u2014 evictions give no warning, so treat the\ndrive copy as the truth).\n\n## Tools in this environment\n\n- `agentc` \u2014 Agent Compose CLI (your primary interface; authed from env)\n- `@agent-compose/sdk` \u2014 installed in /workspace for writing workflows\n- `/ac:*` Claude Code skills \u2014 slash commands for the above\n- `rtk`, `bun`\n- `xdotool` / `scrot` \u2014 drive + screenshot the desktop (if this machine has one; see Computer Use)\n- `chromium` \u2014 the desktop browser; `ac-open <url|file|app>` \u2014 open it on the\n desktop, detached (survives your command; see Computer Use)\n- A world-writable `/workspace` working directory\n\nIf a system capability you need is genuinely missing \u2014 no browser, no display,\nno `ac-open`, a daemon that isn't there \u2014 say so to the human in ONE honest\nline (what is missing and what it blocks) instead of mounting a\npackage-manager expedition. An in-session `apt-get install` dies with the\nsandbox, burns turns, and hides the real gap; missing platform capabilities\nare the platform's to bake in, and `agentc pause` is the door to ask through.\n(Your own project's dependencies are different \u2014 installing those is normal\nwork.)";
|
|
23
23
|
/** Parameters for the `agentc session add` education brief (ADR-0055 §8). */
|
|
24
24
|
export interface AddedSessionBriefParams {
|
|
25
25
|
conversationId: string;
|
|
@@ -78,11 +78,19 @@ export type AgentMessageSummary = {
|
|
|
78
78
|
};
|
|
79
79
|
/** Everything but the live-only streaming chunk: `text_delta` never becomes
|
|
80
80
|
* an agent.message event (the terminating `text` carries the whole block) —
|
|
81
|
-
* the loop filters it before summarizing.
|
|
81
|
+
* the loop filters it before summarizing. `task_notification` is filtered
|
|
82
|
+
* too: it is session-transport metadata (a parent harness's background-task
|
|
83
|
+
* completion echo), not the agent's own output. `harness_notice` likewise:
|
|
84
|
+
* harness-composed advisory text (synthetic assistant messages), never the
|
|
85
|
+
* agent speaking. */
|
|
82
86
|
type DurableAgentMessage = Exclude<AgentMessage, {
|
|
83
87
|
type: "text_delta";
|
|
84
88
|
} | {
|
|
85
89
|
type: "usage_delta";
|
|
90
|
+
} | {
|
|
91
|
+
type: "task_notification";
|
|
92
|
+
} | {
|
|
93
|
+
type: "harness_notice";
|
|
86
94
|
}>;
|
|
87
95
|
export declare function summarizeAgentMessage(msg: DurableAgentMessage): AgentMessageSummary;
|
|
88
96
|
export type AgentLifecycleEvent = {
|
|
@@ -0,0 +1,184 @@
|
|
|
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
|
+
/** Where the launcher lands on the guest PATH. */
|
|
37
|
+
export declare const AC_OPEN_PATH = "/usr/local/bin/ac-open";
|
|
38
|
+
/** SHARED in-guest state dir: the browser-backfill markers, written by ROOT
|
|
39
|
+
* (the backfill runs sudo) and only ever READ by ac-open. /tmp (ext4) —
|
|
40
|
+
* machine-tier state that dies with the VM, exactly like the recorder's.
|
|
41
|
+
*
|
|
42
|
+
* ac-open's own pidfiles + logs do NOT live here — they live in a PER-UID
|
|
43
|
+
* sibling (`${AC_OPEN_STATE_DIR}.<uid>`, composed in the script). The
|
|
44
|
+
* 2026-08-21 owner transcript is why: the backfill's root-owned `mkdir -p`
|
|
45
|
+
* landed this dir 0755 root:root, the session user's `>>"$LOG"` redirect
|
|
46
|
+
* then failed, and the spawned subshell died before chromium ever ran —
|
|
47
|
+
* surfacing as "chromium exited immediately: see …/browser.log" with an
|
|
48
|
+
* EMPTY log (the redirect that would have written it is what failed). A
|
|
49
|
+
* per-uid dir makes the launcher's writes collision-free for every
|
|
50
|
+
* identity (root workflow runner AND the default session user). */
|
|
51
|
+
export declare const AC_OPEN_STATE_DIR = "/tmp/.ac-desktop-open";
|
|
52
|
+
/** Generic-open entrypoints routed through the launcher. */
|
|
53
|
+
export declare const AC_OPEN_SHIM_PATHS: readonly ["/usr/local/bin/xdg-open", "/usr/local/bin/sensible-browser"];
|
|
54
|
+
/**
|
|
55
|
+
* Browser-backfill marker protocol (self-healing browser presence). The
|
|
56
|
+
* server's boot-time desktop-ensure detects a desktop-carrying image WITHOUT
|
|
57
|
+
* chromium — only possible on GRANDFATHERED bakes, where the chromium install
|
|
58
|
+
* was tolerant (`|| true`) before the 2026-08-16 hard-install — and
|
|
59
|
+
* apt-installs it in the BACKGROUND, with the same setsid + stdio-redirect +
|
|
60
|
+
* pidfile detach discipline ac-open itself uses, so no boot path ever blocks
|
|
61
|
+
* on apt (20-40s). These markers are the shared contract: `browserBackfillCmd`
|
|
62
|
+
* writes them, `browserBackfillStatusCmd` and the ac-open script read them —
|
|
63
|
+
* that's how ac-open answers the THIRD state honestly (browser missing but
|
|
64
|
+
* ARRIVING — retry) instead of "no browser, give up".
|
|
65
|
+
*/
|
|
66
|
+
export declare const BROWSER_BACKFILL_PIDFILE = "/tmp/.ac-desktop-open/browser-install.pid";
|
|
67
|
+
export declare const BROWSER_BACKFILL_LOG = "/tmp/.ac-desktop-open/browser-install.log";
|
|
68
|
+
export declare const BROWSER_BACKFILL_FAILED = "/tmp/.ac-desktop-open/browser-install.failed";
|
|
69
|
+
/** The PINNED third-state line ac-open prints while the background install
|
|
70
|
+
* runs: honest, actionable, and never an invitation to give up or to mount
|
|
71
|
+
* an apt expedition of the agent's own. */
|
|
72
|
+
export declare const BROWSER_INSTALL_IN_PROGRESS_MSG = "ac-open: no browser YET \u2014 the platform is installing chromium in the background right now; retry this exact command in ~30s";
|
|
73
|
+
/**
|
|
74
|
+
* The `ac-open` script body. POSIX sh (dash is /bin/sh on the Debian base).
|
|
75
|
+
*
|
|
76
|
+
* Contract, pinned by the unit tests:
|
|
77
|
+
* - every spawn is `setsid <cmd> </dev/null >>"$LOG" 2>&1 &` — its own
|
|
78
|
+
* session (survives the exec's process-group reap), stdio OFF the exec's
|
|
79
|
+
* pipes (so the exec's stream can settle), a durable pidfile;
|
|
80
|
+
* - pidfiles + logs live in a PER-UID state dir (`$D`), so the root runner
|
|
81
|
+
* and the session user never fight over one root-owned dir (the
|
|
82
|
+
* 2026-08-21 "exited immediately, empty log" failure — see
|
|
83
|
+
* AC_OPEN_STATE_DIR's header); the shared dir (`$M`) is read-only here,
|
|
84
|
+
* for the backfill markers root writes;
|
|
85
|
+
* - IDEMPOTENT: re-invoking for a running app FOCUSES its window
|
|
86
|
+
* (xdotool windowactivate) instead of spawning a second copy; the
|
|
87
|
+
* browser and file manager are singletons, so a re-invoke hands the
|
|
88
|
+
* target to the existing instance (a new tab / window) by design.
|
|
89
|
+
* Liveness checks are SAME-UID only (`pgrep -u`): another identity's
|
|
90
|
+
* instance shares neither profile nor single-instance socket, so it can
|
|
91
|
+
* neither take a hand-off nor make this spawn redundant. And a fresh
|
|
92
|
+
* spawn that exits within the liveness window while a same-uid instance
|
|
93
|
+
* is alive IS the hand-off (single-instance apps hand the target over
|
|
94
|
+
* and exit 0) — reported as success, never as a crash. N calls =
|
|
95
|
+
* N tabs, zero crashes;
|
|
96
|
+
* - HONEST degrades, one line each: no display → say so; no browser /
|
|
97
|
+
* unknown app → name the missing tool and tell the agent to REPORT it
|
|
98
|
+
* rather than mount a package-manager expedition. A REAL immediate
|
|
99
|
+
* death carries the app's own log tail as the cause.
|
|
100
|
+
*/
|
|
101
|
+
export declare const AC_OPEN_SCRIPT = "#!/bin/sh\n# ac-open \u2014 open a URL, file, or GUI app on this machine's desktop, DETACHED.\n#\n# ac-open https://github.com the browser (a running instance gets a tab)\n# ac-open ./report.html a local file, in the browser\n# ac-open . a directory, in the file manager\n# ac-open gimp [args...] any GUI app by command name\n#\n# Why: the sandbox reaps each exec'd command's process group when the command\n# returns, so a GUI app launched with a plain `&` dies the moment the\n# launching shell finishes. ac-open detaches the app into its own session\n# (setsid, stdio off the exec's pipes), records a pidfile, and returns at\n# once; re-invoking it for a running app focuses the existing window instead\n# of spawning a second copy. Logs + pidfiles: /tmp/.ac-desktop-open.<uid>/.\n# Written by agent-compose (sdk/src/agent/desktop-open.ts) \u2014 do not edit in place.\nset -u\nexport DISPLAY=\"${DISPLAY:-:0}\"\n# Per-UID state (pidfiles + logs): the shared dir is root-owned when the\n# backfill created it, and a user-side >>$LOG into a root 0755 dir fails\n# BEFORE the app runs \u2014 the false \"exited immediately\" with an empty log.\nD=\"/tmp/.ac-desktop-open.$(id -u)\"\n# Shared, root-written backfill markers \u2014 READ-ONLY here.\nM=/tmp/.ac-desktop-open\nmkdir -p \"$D\" 2>/dev/null || true\n[ -w \"$D\" ] || { echo \"ac-open: state dir $D is not writable by uid $(id -u)\" >&2; exit 1; }\n\n[ $# -ge 1 ] || { echo 'usage: ac-open <url|file|dir|app> [args...]' >&2; exit 2; }\nTARGET=$1; shift\n\n# No display = no desktop on this image. One honest line, no expedition.\nif ! timeout 3 xdotool getdisplaygeometry >/dev/null 2>&1; then\n echo \"ac-open: no desktop display on $DISPLAY \u2014 this machine has no GUI session up\" >&2\n exit 1\nfi\n\npick_browser() {\n for b in chromium chromium-browser x-www-browser google-chrome; do\n command -v \"$b\" >/dev/null 2>&1 && { echo \"$b\"; return 0; }\n done\n return 1\n}\n\n# Best-effort: raise the newest visible window whose class matches $1.\nfocus_class() {\n WID=$(xdotool search --onlyvisible --class \"$1\" 2>/dev/null | tail -1) || WID=''\n [ -n \"${WID:-}\" ] && xdotool windowactivate \"$WID\" 2>/dev/null || true\n}\n\n# Same-UID instance probe: another identity's instance shares neither profile\n# nor single-instance socket, so only our own uid's counts as \"running\".\nalive_same_uid() { pgrep -x -u \"$(id -u)\" \"$1\" >/dev/null 2>&1; }\n\n# Singleton apps (browser, file manager): the second invocation hands the\n# target to the running instance and exits \u2014 that IS the idempotent path, so\n# spawn detached every time and only liveness-check a FRESH instance.\nsingleton_open() { # $1 = state key, rest = command + target\n NAME=$1; shift\n BIN=$(basename \"$1\")\n PIDFILE=$D/$NAME.pid; LOG=$D/$NAME.log\n RUNNING=0\n alive_same_uid \"$BIN\" && RUNNING=1\n [ \"$RUNNING\" = 0 ] && [ -f \"$PIDFILE\" ] && kill -0 \"$(cat \"$PIDFILE\" 2>/dev/null)\" 2>/dev/null && RUNNING=1\n setsid \"$@\" </dev/null >>\"$LOG\" 2>&1 &\n PID=$!\n if [ \"$RUNNING\" = 1 ]; then\n sleep 1\n focus_class \"$BIN\"\n echo \"ac-open: handed off to the running $BIN \u2014 no second window\"\n return 0\n fi\n echo \"$PID\" > \"$PIDFILE\"\n sleep 1\n if ! kill -0 \"$PID\" 2>/dev/null; then\n # Died within the liveness window \u2014 but if OUR instance of $BIN is alive\n # NOW, this was the single-instance HAND-OFF: the pre-check raced a\n # still-starting instance, the fresh process handed the target over and\n # exited 0. That is success (N calls = N tabs), never a crash report.\n if alive_same_uid \"$BIN\"; then\n focus_class \"$BIN\"\n echo \"ac-open: handed off to the running $BIN \u2014 no second window\"\n return 0\n fi\n TAIL=$(tail -c 300 \"$LOG\" 2>/dev/null | tr '\\n' ' ')\n echo \"ac-open: $BIN exited immediately: ${TAIL:-no output captured \u2014 see $LOG}\" >&2\n exit 1\n fi\n focus_class \"$BIN\"\n echo \"ac-open: launched $BIN (pid $PID), detached \u2014 it survives this command\"\n}\n\nopen_url() { # $1 = url\n B=$(pick_browser) || {\n # Grandfathered image mid-heal: the boot-time desktop-ensure found no\n # browser and detached an apt install (browserBackfillCmd \u2014 the pidfile\n # below is ITS single-flight marker, root-written in the SHARED dir).\n # Third honest state: arriving.\n if [ -f \"$M/browser-install.pid\" ] && kill -0 \"$(cat \"$M/browser-install.pid\" 2>/dev/null)\" 2>/dev/null; then\n echo \"ac-open: no browser YET \u2014 the platform is installing chromium in the background right now; retry this exact command in ~30s\" >&2\n exit 75\n fi\n if [ -f \"$M/browser-install.failed\" ]; then\n TAIL=$(tail -c 300 \"$M/browser-install.log\" 2>/dev/null | tr '\\n' ' ')\n echo \"ac-open: the platform's background chromium install FAILED: ${TAIL:-see $M/browser-install.log} \u2014 report that cause in one line; do not apt-get one yourself.\" >&2\n exit 1\n fi\n echo \"ac-open: no graphical browser on this image (expected chromium) \u2014 cannot open $1. Report the missing browser in one line; do not apt-get one mid-session.\" >&2\n exit 1\n }\n singleton_open browser \"$B\" \"$1\"\n}\n\nopen_app() { # $1 = command, rest = args\n command -v \"$1\" >/dev/null 2>&1 || {\n echo \"ac-open: '$1' is not installed on this machine \u2014 report the missing tool in one line instead of package-hunting for it\" >&2\n exit 127\n }\n BIN=$(basename \"$1\"); PIDFILE=$D/app-$BIN.pid; LOG=$D/app-$BIN.log\n OLD=$(cat \"$PIDFILE\" 2>/dev/null || true)\n if [ -n \"${OLD:-}\" ] && kill -0 \"$OLD\" 2>/dev/null; then\n WID=$(xdotool search --onlyvisible --pid \"$OLD\" 2>/dev/null | tail -1) || WID=''\n [ -z \"${WID:-}\" ] && { WID=$(xdotool search --onlyvisible --class \"$BIN\" 2>/dev/null | tail -1) || WID=''; }\n [ -n \"${WID:-}\" ] && xdotool windowactivate \"$WID\" 2>/dev/null || true\n echo \"ac-open: $BIN is already running (pid $OLD) \u2014 focused it instead of starting a second copy\"\n exit 0\n fi\n CMD=$1; shift\n setsid \"$CMD\" \"$@\" </dev/null >>\"$LOG\" 2>&1 &\n PID=$!\n echo \"$PID\" > \"$PIDFILE\"\n sleep 1\n if ! kill -0 \"$PID\" 2>/dev/null; then\n # Same hand-off grace as the singletons: VS Code and friends are\n # single-instance too \u2014 a fresh spawn that exits at once while our own\n # instance is alive HANDED OFF, it did not crash.\n if alive_same_uid \"$BIN\"; then\n focus_class \"$BIN\"\n echo \"ac-open: handed off to the running $BIN \u2014 no second window\"\n exit 0\n fi\n TAIL=$(tail -c 300 \"$LOG\" 2>/dev/null | tr '\\n' ' ')\n echo \"ac-open: $BIN exited immediately: ${TAIL:-no output captured \u2014 see $LOG}\" >&2\n exit 1\n fi\n echo \"ac-open: launched $BIN (pid $PID), detached \u2014 it survives this command\"\n}\n\ncase \"$TARGET\" in\n http://*|https://*|file://*|about:*|chrome://*)\n open_url \"$TARGET\" ;;\n localhost|localhost:*|localhost/*|127.0.0.1|127.0.0.1:*|127.0.0.1/*)\n open_url \"http://$TARGET\" ;;\n *)\n if [ -d \"$TARGET\" ]; then\n command -v pcmanfm >/dev/null 2>&1 || {\n echo \"ac-open: no file manager on this image (expected pcmanfm) \u2014 report it in one line\" >&2\n exit 1\n }\n case \"$TARGET\" in /*) ABS=$TARGET ;; *) ABS=$PWD/$TARGET ;; esac\n singleton_open files pcmanfm \"$ABS\"\n elif [ -f \"$TARGET\" ]; then\n case \"$TARGET\" in /*) ABS=$TARGET ;; *) ABS=$PWD/$TARGET ;; esac\n open_url \"file://$ABS\"\n else\n open_app \"$TARGET\" \"$@\"\n fi ;;\nesac\n";
|
|
102
|
+
/** Shim body for the generic-open entrypoints (`xdg-open`,
|
|
103
|
+
* `sensible-browser`): exec straight into the detaching launcher, so any
|
|
104
|
+
* tool that "opens a URL" survives its exec too. */
|
|
105
|
+
export declare const AC_OPEN_SHIM = "#!/bin/sh\n# agent-compose: generic open routes through ac-open (the detaching launcher).\nexec /usr/local/bin/ac-open \"$@\"\n";
|
|
106
|
+
/**
|
|
107
|
+
* Install (or refresh) `ac-open` + the generic-open shims — run as root.
|
|
108
|
+
* The script carries `$`/quotes/backslashes, so it rides the base64 idiom
|
|
109
|
+
* (the WRITE_AGENTS_MD / installRecorderCmd pattern): escaping-proof through
|
|
110
|
+
* the sandbox command channel. Always a plain truncating rewrite — the files
|
|
111
|
+
* are wholly platform-owned and tiny, so idempotence is free and a contract
|
|
112
|
+
* change reaches grandfathered VMs on the next session boot. Each write is
|
|
113
|
+
* `sh -n`-checked so a corrupted transit fails the install loudly, never the
|
|
114
|
+
* agent's first `ac-open`.
|
|
115
|
+
*/
|
|
116
|
+
export declare function installDesktopOpenCmd(): string;
|
|
117
|
+
/**
|
|
118
|
+
* Chromium session-desktop defaults — SINGLE-SOURCED here (the ac-open
|
|
119
|
+
* two-door pattern): baked into fresh images by infra/e2b-template/parts.ts
|
|
120
|
+
* (CHROMIUM_DEFAULTS_INSTALL), re-installed at session desktop-ensure by the
|
|
121
|
+
* server (persistent.ts), and written by the browser backfill right after it
|
|
122
|
+
* lands chromium on a grandfathered image — so a backfilled browser behaves
|
|
123
|
+
* exactly like a baked one. Before this, a backfilled chromium had NO flags
|
|
124
|
+
* file: a root launch died on the missing --no-sandbox ("exited
|
|
125
|
+
* immediately"), and every launch paid the first-run wizard.
|
|
126
|
+
*
|
|
127
|
+
* Debian's /usr/bin/chromium wrapper dot-sources every /etc/chromium.d/*
|
|
128
|
+
* fragment, so these flags apply to EVERY door into the browser — the dock
|
|
129
|
+
* launcher, the openbox menu, ac-open, an agent's bare `chromium`.
|
|
130
|
+
*
|
|
131
|
+
* Flag notes:
|
|
132
|
+
* --disable-gpu — the session display is Xkasmvnc, which exposes NO GLX
|
|
133
|
+
* (probe-verified in a live sandbox, 2026-08-21). Without the flag,
|
|
134
|
+
* chromium's GPU process walks both EGL display types (OpenGL, then
|
|
135
|
+
* OpenGLES) through ANGLE, fails both ("Initialization of all (2) EGL
|
|
136
|
+
* display types failed"), respawns, and only then falls back to the
|
|
137
|
+
* software raster it was always going to use — measured at 200-500ms of
|
|
138
|
+
* pure startup waste per launch on an idle host, worse on a shared
|
|
139
|
+
* 2-vCPU guest. Software WebGL (SwiftShader) still works.
|
|
140
|
+
* --disable-session-crashed-bubble — a paused/killed VM otherwise greets
|
|
141
|
+
* every reattach with the restore bubble.
|
|
142
|
+
* --no-sandbox only under root — chromium refuses to start as root
|
|
143
|
+
* without it; the per-session microVM is the isolation boundary.
|
|
144
|
+
*/
|
|
145
|
+
export declare const CHROMIUM_DEFAULTS_PATH = "/etc/chromium.d/00-ac-defaults";
|
|
146
|
+
export declare const CHROMIUM_DEFAULTS = "# agent-compose session-desktop chromium defaults\n# (single-sourced in sdk/src/agent/desktop-open.ts \u2014 sourced by /usr/bin/chromium)\nCHROMIUM_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\"\nif [ \"$(id -u)\" = \"0\" ]; then CHROMIUM_FLAGS=\"$CHROMIUM_FLAGS --no-sandbox\"; fi\n";
|
|
147
|
+
/**
|
|
148
|
+
* Install (or refresh) the chromium defaults fragment — run as root.
|
|
149
|
+
* QUOTE-FREE by construction (base64 payload, no quoting anywhere): the
|
|
150
|
+
* string embeds verbatim inside browserBackfillCmd's single-quoted `sh -c`
|
|
151
|
+
* payload as well as standing alone in a template runCmd / an ensure exec.
|
|
152
|
+
*/
|
|
153
|
+
export declare function installChromiumDefaultsCmd(): string;
|
|
154
|
+
/**
|
|
155
|
+
* Probe-then-heal for the browser on a GRANDFATHERED image — run as root at
|
|
156
|
+
* session desktop-ensure (server/src/sandbox/attach/browser-backfill.ts).
|
|
157
|
+
*
|
|
158
|
+
* Scope is exactly chromium, deliberately not a package set: every desktop
|
|
159
|
+
* bake since the first (8329c9c4) hard-installs xdotool/scrot/pcmanfm in the
|
|
160
|
+
* SAME `&&`-joined apt line as the desktop stack itself, so any image that
|
|
161
|
+
* probes a desktop necessarily carries them — chromium alone rode a tolerant
|
|
162
|
+
* `|| true` until the 2026-08-16 hard-install, making it the only
|
|
163
|
+
* manual-promised desktop tool that can be missing. (ac-open and ac-record
|
|
164
|
+
* are re-installed on every fresh boot by ensureSessionDesktop already.)
|
|
165
|
+
*
|
|
166
|
+
* Shape: a three-way verdict on stdout (the desktop-capability-probe idiom —
|
|
167
|
+
* `backfill=present` / `backfill=in-progress` / `backfill=started`), and on
|
|
168
|
+
* `started` the apt install runs DETACHED — `setsid … </dev/null >>log 2>&1 &`
|
|
169
|
+
* plus a pidfile, the exact ac-open/cli-agent discipline — so this command
|
|
170
|
+
* returns in milliseconds while apt takes its 20-40s. Single-flight is the
|
|
171
|
+
* pidfile liveness gate (a second ensure sees `in-progress` and starts
|
|
172
|
+
* nothing); apt's own dpkg lock backstops the millisecond race window. The
|
|
173
|
+
* installer's last act is its verdict: a failure touches the `.failed` marker
|
|
174
|
+
* (cause in the log) and either way the pidfile is removed, so a machine
|
|
175
|
+
* suspended mid-install simply retries on its next fresh boot.
|
|
176
|
+
*/
|
|
177
|
+
export declare function browserBackfillCmd(): string;
|
|
178
|
+
/**
|
|
179
|
+
* The watcher's status probe (read-only, safe from any user): `backfill=ok` /
|
|
180
|
+
* `backfill=failed` (+ the log tail as the cause, one line) /
|
|
181
|
+
* `backfill=in-progress` / `backfill=lost` (installer died without a verdict
|
|
182
|
+
* — suspend/recycle mid-apt; the next fresh boot's ensure retries).
|
|
183
|
+
*/
|
|
184
|
+
export declare function browserBackfillStatusCmd(): string;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guest-side session perf sampling — the `<promptPath>.perf` token contract.
|
|
3
|
+
*
|
|
4
|
+
* The session guest has NO push channel: every durable signal is a flat file
|
|
5
|
+
* next to the turn's prompt path, read by bounded server execs (the durable
|
|
6
|
+
* trio — see tailer.ts / _cli-agent.ts). Perf sampling rides that exact
|
|
7
|
+
* plane:
|
|
8
|
+
*
|
|
9
|
+
* - LIVE turn: the runner's 10s heartbeat subshell calls `ac_perf_tick`
|
|
10
|
+
* each beat; every 6th beat (~60s) it burst-reads /proc (stat jiffies
|
|
11
|
+
* delta, loadavg, meminfo) plus one `df -P`, and rewrites the perf
|
|
12
|
+
* token file atomically. The subshell dies with the wrapper, so
|
|
13
|
+
* sampling stops within one beat of turn end — it can never keep the
|
|
14
|
+
* setsid group alive (the 2026-08-17 spurious-wake class).
|
|
15
|
+
* - The token then rides the EXISTING durable-trio probe exec as one
|
|
16
|
+
* trailing field (`turnLivenessProbeCommand`) — no new exec, no new
|
|
17
|
+
* socket, ~70 extra bytes on a line the server already pulls.
|
|
18
|
+
* - PARKED-AWAKE: the server's 5-min compute-span checkpoint (which
|
|
19
|
+
* already holds the provider's RUNNING list — a suspended machine is
|
|
20
|
+
* structurally never probed) runs `standalonePerfProbeCommand`: the
|
|
21
|
+
* same read burst twice across a short window, one bounded exec.
|
|
22
|
+
*
|
|
23
|
+
* Token format (ONE whitespace-free field, so it can ride a space-split
|
|
24
|
+
* probe line): `v=1,cpu=12,l1=0.42,mem=37,dsk=52,vc=2,ram=4096,ts=1755…`
|
|
25
|
+
* cpu — busy % of all vcpus over the sampling window (-1 = no window yet)
|
|
26
|
+
* l1 — 1-min loadavg (-1 = unreadable)
|
|
27
|
+
* mem — used % of MemTotal, via MemAvailable (-1 = unreadable)
|
|
28
|
+
* dsk — used % of the root filesystem (-1 = unreadable)
|
|
29
|
+
* vc — vcpu count (cpuN lines in /proc/stat)
|
|
30
|
+
* ram — MemTotal in MB (the sandbox-size context the fold tags by)
|
|
31
|
+
* ts — guest clock, epoch seconds (dedupe only, never arithmetic)
|
|
32
|
+
*
|
|
33
|
+
* The guest is hostile by doctrine (registry-activities.ts house rule):
|
|
34
|
+
* `parsePerfToken` clamps every field to its closed range and nulls
|
|
35
|
+
* anything out of bounds — server folds re-use this parser, so no guest
|
|
36
|
+
* number reaches a metric or a Temporal payload unclamped.
|
|
37
|
+
*/
|
|
38
|
+
/** Sample every Nth heartbeat beat (10s beats → ~60s cadence). */
|
|
39
|
+
export declare const PERF_SAMPLE_EVERY_BEATS = 6;
|
|
40
|
+
/** CPU window of the standalone (parked-awake) probe, seconds. */
|
|
41
|
+
export declare const PERF_PROBE_WINDOW_SECONDS = 2;
|
|
42
|
+
/** The standalone probe's output line leads with this prefix. */
|
|
43
|
+
export declare const PERF_PROBE_LINE_PREFIX = "perf ";
|
|
44
|
+
/** Longest token the parsers accept — anything bigger is garbage. */
|
|
45
|
+
export declare const PERF_TOKEN_MAX_CHARS = 200;
|
|
46
|
+
/** One clamped guest perf sample. Null fields = unreadable/absent on the
|
|
47
|
+
* guest — never zero-filled (a zero is a claim; null is honesty). */
|
|
48
|
+
export interface GuestPerfSample {
|
|
49
|
+
/** Busy % of all vcpus over the sample window, 0–100. */
|
|
50
|
+
cpuBusyPct: number | null;
|
|
51
|
+
/** 1-minute load average. */
|
|
52
|
+
load1: number | null;
|
|
53
|
+
/** Used % of MemTotal (MemAvailable-based), 0–100. */
|
|
54
|
+
memUsedPct: number | null;
|
|
55
|
+
/** Used % of the root filesystem, 0–100. */
|
|
56
|
+
diskUsedPct: number | null;
|
|
57
|
+
/** vcpu count — the sandbox-size context tag. */
|
|
58
|
+
vcpus: number | null;
|
|
59
|
+
/** MemTotal in MB — the other half of the size context. */
|
|
60
|
+
ramMb: number | null;
|
|
61
|
+
/** Guest clock at sample time, epoch seconds. Dedupe only. */
|
|
62
|
+
sampledAtS: number | null;
|
|
63
|
+
}
|
|
64
|
+
export interface PerfSamplerPaths {
|
|
65
|
+
/** Durable token file (`<promptPath>.perf`). */
|
|
66
|
+
perfPath: string;
|
|
67
|
+
/** Override for tests ONLY — fixture dir standing in for /proc. */
|
|
68
|
+
procRoot?: string;
|
|
69
|
+
/** Filesystem to `df` (default "/"). */
|
|
70
|
+
diskPath?: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The heartbeat-loop member: function definitions plus `ac_perf_tick`,
|
|
74
|
+
* which gates the burst to every `PERF_SAMPLE_EVERY_BEATS`th beat (beat 1
|
|
75
|
+
* establishes the jiffies baseline — its token carries cpu=-1; beat 7 is
|
|
76
|
+
* the first real cpu window) and rewrites the token file atomically
|
|
77
|
+
* (tmp + `mv -f`, so a concurrent probe `head` reads old-or-new, never a
|
|
78
|
+
* torn write). Place the returned fragment INSIDE the heartbeat subshell,
|
|
79
|
+
* before its `while`; call `ac_perf_tick` once per beat in the body.
|
|
80
|
+
*/
|
|
81
|
+
export declare function perfSamplerFunctionFragment(paths: PerfSamplerPaths): string;
|
|
82
|
+
/**
|
|
83
|
+
* Self-contained one-exec probe for the parked-awake lane (the server's
|
|
84
|
+
* 5-min compute-span checkpoint): two read bursts across a short window
|
|
85
|
+
* give a real cpu delta, then the token prints to stdout as
|
|
86
|
+
* `perf <token>`. Bounded by the caller's exec timeout; ~window+ε runtime.
|
|
87
|
+
*/
|
|
88
|
+
export declare function standalonePerfProbeCommand(opts?: {
|
|
89
|
+
procRoot?: string;
|
|
90
|
+
diskPath?: string;
|
|
91
|
+
windowSeconds?: number;
|
|
92
|
+
}): string;
|
|
93
|
+
/** Parse + CLAMP one perf token. Null = no usable sample (absent file, a
|
|
94
|
+
* garbled write, a hostile guest). Out-of-range fields null out
|
|
95
|
+
* individually; a token with no usable utilization field at all is null. */
|
|
96
|
+
export declare function parsePerfToken(token: string): GuestPerfSample | null;
|
|
97
|
+
/** Parse the standalone probe's stdout (the LAST `perf ` line wins — envd
|
|
98
|
+
* occasionally prepends shell noise). */
|
|
99
|
+
export declare function parsePerfProbeOutput(stdout: string): GuestPerfSample | null;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The durable services manifest — `.ac/services.yml` on the session's drive
|
|
3
|
+
* branch.
|
|
4
|
+
*
|
|
5
|
+
* Sessions run on cattle machines: a VM recycle (resize, eviction, failed
|
|
6
|
+
* reconnect) discards every process and every byte off the drive. The manifest
|
|
7
|
+
* is the pet — a small, versioned description of the long-running services a
|
|
8
|
+
* session depends on (dev servers, docker compose stacks, built dashboards) so
|
|
9
|
+
* the platform can rebuild the stack on a fresh machine without being asked.
|
|
10
|
+
*
|
|
11
|
+
* This module is pure string/data work: a parser for a deliberately RESTRICTED
|
|
12
|
+
* YAML subset (no yaml dependency — the schema is small and fixed, and a
|
|
13
|
+
* hand-rolled parser matches house style, cf. parseRecorderStatus /
|
|
14
|
+
* parsePerfToken), a serializer the CLI uses for `agentc services add/remove`,
|
|
15
|
+
* and the shared types. Script generation lives in ./services-restore.ts.
|
|
16
|
+
*
|
|
17
|
+
* Supported YAML subset (anything else is a loud parse error, never a guess):
|
|
18
|
+
*
|
|
19
|
+
* version: 1
|
|
20
|
+
* services:
|
|
21
|
+
* - name: postgres
|
|
22
|
+
* command: docker compose up postgres # required, foreground shell
|
|
23
|
+
* cwd: myapp # optional, relative to drive root
|
|
24
|
+
* port: 5432 # optional, informational + docs
|
|
25
|
+
* env: reads DATABASE_URL from .env # optional free-text note
|
|
26
|
+
* setup: docker compose pull postgres # optional one-time prep per machine
|
|
27
|
+
* health: # optional, at most one of:
|
|
28
|
+
* cmd: pg_isready -h localhost # shell probe (exit 0 = healthy)
|
|
29
|
+
* http: http://localhost:5432/ # or an HTTP 2xx probe
|
|
30
|
+
* data: # optional data hooks
|
|
31
|
+
* dump: pg_dump app > .ac/seeds/dev.sql # before a DELIBERATE recycle
|
|
32
|
+
* restore: psql app < .ac/seeds/dev.sql # after setup on a fresh boot
|
|
33
|
+
*
|
|
34
|
+
* Scalars only, one nesting level (`health:` / `data:`), full-line `#` comments
|
|
35
|
+
* only (an inline ` # ...` would be ambiguous inside shell commands and is kept
|
|
36
|
+
* as part of the value), no multi-line block scalars (`|` / `>`), no anchors,
|
|
37
|
+
* no flow collections.
|
|
38
|
+
*/
|
|
39
|
+
export declare const SERVICES_MANIFEST_RELPATH = ".ac/services.yml";
|
|
40
|
+
/** Hard cap on manifest entries — a manifest is a stack, not a fleet. */
|
|
41
|
+
export declare const SERVICES_MAX = 16;
|
|
42
|
+
export declare const SERVICE_NAME_RE: RegExp;
|
|
43
|
+
export interface ServiceHealth {
|
|
44
|
+
/** Shell probe; exit 0 means healthy. Mutually exclusive with `http`. */
|
|
45
|
+
cmd?: string;
|
|
46
|
+
/** HTTP probe; any 2xx means healthy. Mutually exclusive with `cmd`. */
|
|
47
|
+
http?: string;
|
|
48
|
+
}
|
|
49
|
+
export interface ServiceDataHooks {
|
|
50
|
+
/**
|
|
51
|
+
* Run before a DELIBERATE recycle (resize). Evictions and dead-VM
|
|
52
|
+
* fresh-acquires cannot run it — the machine is already gone — so dumps are
|
|
53
|
+
* a courtesy, not a guarantee; durable data belongs on the drive.
|
|
54
|
+
*/
|
|
55
|
+
dump?: string;
|
|
56
|
+
/** Run after `setup` on a fresh boot, before the service launches. */
|
|
57
|
+
restore?: string;
|
|
58
|
+
}
|
|
59
|
+
export interface ServiceEntry {
|
|
60
|
+
name: string;
|
|
61
|
+
/** Foreground shell command; launched detached with durable log + pidfile. */
|
|
62
|
+
command: string;
|
|
63
|
+
/** Working directory, relative to the drive root (absolute rejected). */
|
|
64
|
+
cwd?: string;
|
|
65
|
+
/** Informational — surfaced in `agentc services list` and the restore line. */
|
|
66
|
+
port?: number;
|
|
67
|
+
/** Free-text note about env the service expects (documentation only). */
|
|
68
|
+
env?: string;
|
|
69
|
+
/** One-time per-machine prep (e.g. `docker compose up -d`). */
|
|
70
|
+
setup?: string;
|
|
71
|
+
health?: ServiceHealth;
|
|
72
|
+
data?: ServiceDataHooks;
|
|
73
|
+
}
|
|
74
|
+
export interface ServicesManifest {
|
|
75
|
+
version: 1;
|
|
76
|
+
services: ServiceEntry[];
|
|
77
|
+
}
|
|
78
|
+
export interface ParseServicesManifestResult {
|
|
79
|
+
/** Present iff `errors` is empty. All-or-nothing: a broken manifest restores nothing, loudly. */
|
|
80
|
+
manifest: ServicesManifest | null;
|
|
81
|
+
errors: string[];
|
|
82
|
+
}
|
|
83
|
+
export declare function parseServicesManifest(text: string): ParseServicesManifestResult;
|
|
84
|
+
/**
|
|
85
|
+
* Serialize a manifest in the canonical shape the parser reads back.
|
|
86
|
+
* Values containing newlines are a caller bug (the CLI rejects them first).
|
|
87
|
+
*/
|
|
88
|
+
export declare function renderServicesManifest(manifest: ServicesManifest): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Restore-script generation for the durable services manifest.
|
|
3
|
+
*
|
|
4
|
+
* Pure string composition — nothing here touches a sandbox (the desktop-open
|
|
5
|
+
* posture). The server's fresh-boot ensure and `agentc services restore` both
|
|
6
|
+
* feed a parsed manifest through these generators, so the launch semantics are
|
|
7
|
+
* one implementation:
|
|
8
|
+
*
|
|
9
|
+
* - every service is launched DETACHED (`setsid sh -c … </dev/null &`) with a
|
|
10
|
+
* durable log and pidfile under /tmp/ac-services/ — the same contract the
|
|
11
|
+
* background-work doctrine teaches, so a restored service freezes while the
|
|
12
|
+
* machine parks and resumes on wake exactly like an agent-launched job;
|
|
13
|
+
* - `setup` and `data.restore` run first, sequentially, in the service's cwd;
|
|
14
|
+
* - health is gated with a bounded poll, never an unbounded wait;
|
|
15
|
+
* - every outcome is reported as a machine-readable status line
|
|
16
|
+
* (`##ac-service <name> <ok|unchecked|failed|dumped|dump-failed> [detail]`)
|
|
17
|
+
* — per-service honesty, never a fatal abort of the whole restore.
|
|
18
|
+
*
|
|
19
|
+
* The generated script is POSIX sh, installed via base64 + `sh -n` (the
|
|
20
|
+
* desktop-open install idiom) so a syntax regression fails loudly at install
|
|
21
|
+
* time, not silently at boot.
|
|
22
|
+
*/
|
|
23
|
+
import type { ServicesManifest } from "./services-manifest.js";
|
|
24
|
+
/** Durable per-service logs + pidfiles live here (machine-local, recreated each boot). */
|
|
25
|
+
export declare const SERVICES_STATE_DIR = "/tmp/ac-services";
|
|
26
|
+
/** Marker prefix for machine-readable per-service outcome lines. */
|
|
27
|
+
export declare const SERVICE_STATUS_PREFIX = "##ac-service";
|
|
28
|
+
/** Bound on each service's health poll (1s beats). */
|
|
29
|
+
export declare const SERVICE_HEALTH_TIMEOUT_S = 45;
|
|
30
|
+
export type ServiceRestoreStatus = "ok" | "unchecked" | "failed" | "dumped" | "dump-failed";
|
|
31
|
+
export interface ServiceRestoreOutcome {
|
|
32
|
+
name: string;
|
|
33
|
+
status: ServiceRestoreStatus;
|
|
34
|
+
detail?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The auto-restore script: sequential, per-service honest, never exits nonzero
|
|
38
|
+
* because one service failed (the caller reads status lines, not the exit code).
|
|
39
|
+
*
|
|
40
|
+
* @param driveRoot absolute session working dir (the drive root, e.g. /factory/files)
|
|
41
|
+
*/
|
|
42
|
+
export declare function servicesRestoreScript(manifest: ServicesManifest, driveRoot: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* The pre-recycle dump script: runs each service's `data.dump` hook in its cwd,
|
|
45
|
+
* bounded by the caller's command timeout. Only DELIBERATE recycles (resize)
|
|
46
|
+
* get to run this — an evicted or dead machine cannot.
|
|
47
|
+
*/
|
|
48
|
+
export declare function servicesDumpScript(manifest: ServicesManifest, driveRoot: string): string | null;
|
|
49
|
+
/**
|
|
50
|
+
* Parse the status lines out of a restore/dump run's stdout. Non-status lines
|
|
51
|
+
* are ignored; malformed status lines are dropped rather than guessed at.
|
|
52
|
+
*/
|
|
53
|
+
export declare function parseServicesRestoreOutput(stdout: string): ServiceRestoreOutcome[];
|
|
54
|
+
/**
|
|
55
|
+
* One human line for the conversation notice, e.g.
|
|
56
|
+
* `postgres ✓ redis ✓ web ✗ (health-timeout — see /tmp/ac-services/web.log)`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function formatServicesRestoreSummary(outcomes: ServiceRestoreOutcome[]): string;
|