@coreplane/switchboard 1.235.0 → 1.236.0
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/assets/Dockerfile +11 -0
- package/dist/assets/config/config.example.yaml +9 -4
- package/dist/assets/deploy/cloudflare-memory/worker.ts +12 -0
- package/dist/assets/deploy/cloudflare-resident/Dockerfile +18 -0
- package/dist/assets/deploy/cloudflare-resident/worker.ts +115 -56
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +11 -0
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +68 -7
- package/dist/assets/package-lock.json +212 -3
- package/dist/assets/package.json +4 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/core/coordinator/contract.ts +24 -4
- package/dist/assets/src/core/coordinator/driver.ts +31 -3
- package/dist/assets/src/core/runEvents.ts +36 -1
- package/dist/assets/src/core/runFriction.ts +1 -0
- package/dist/assets/src/core/ship/contract.ts +3 -1
- package/dist/assets/src/core/ship/coordinator.ts +213 -29
- package/dist/assets/src/core/trace/types.ts +5 -0
- package/dist/assets/src/execution/residentDisk.ts +21 -10
- package/dist/assets/src/execution/sandboxErrors.ts +72 -0
- package/dist/assets/src/execution/sandboxStart.ts +90 -0
- package/dist/assets/web/dist/.vite/manifest.json +437 -423
- package/dist/assets/web/dist/assets/{AppShell-lYVcj-k7.js → AppShell-DZdz_sl7.js} +1 -1
- package/dist/assets/web/dist/assets/{CostsPage-D-v8am88.js → CostsPage-BfJsol9y.js} +1 -1
- package/dist/assets/web/dist/assets/{DeliveryPage-CvlWP7Eq.js → DeliveryPage-CvInfu7y.js} +1 -1
- package/dist/assets/web/dist/assets/{NotFoundPage-BzWS4Ca1.js → NotFoundPage-d37EP8hS.js} +1 -1
- package/dist/assets/web/dist/assets/ResidentDetailPage-DQSD0SZD.js +1 -0
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CZUGLen6.js +1 -0
- package/dist/assets/web/dist/assets/RunFoldRow-Bngn4GjF.js +9 -0
- package/dist/assets/web/dist/assets/{RunRoutePage-oN9GkVEA.js → RunRoutePage-a-tMrirB.js} +3 -3
- package/dist/assets/web/dist/assets/RunsIndexPage-s0VKmRda.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-DEICQ4FY.js → RunsTabs-CyCdOhJ2.js} +1 -1
- package/dist/assets/web/dist/assets/ScheduledPage-ByCmyrDs.js +1 -0
- package/dist/assets/web/dist/assets/SettingsPage-DhKEokGK.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-D14iJMy7.js → StatusDot-D-BPSPpw.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-B0Ob5MQ4.js → Tooltip-DPDx5tXT.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-0eiekTHQ.js +1 -0
- package/dist/assets/web/dist/assets/{angular-html-Cw130Zyi.js → angular-html-Ni1Xil2G.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-DY1m4C2s.js → angular-ts-oCgo6p05.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-W6Ty7v05.js → apl-CiWYl0gh.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-DlLVHwVk.js → astro-B-VeKZ_J.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-Bco3tDh4.js → blade-BIWEISKd.js} +1 -1
- package/dist/assets/web/dist/assets/{c-1LaFY_cj.js → c-DHxA5Byp.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-B-OvrOL5.js → chapel-D4qGLw-Q.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-Ch3EPlHu.js → cobol-BznX5Q-u.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-hZuITnto.js → coffee-CVr8Idlj.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-B8iRVo8m.js → cpp-wrk6NfW4.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-DKveI7lr.js → crystal-CGYWMeH1.js} +1 -1
- package/dist/assets/web/dist/assets/{css-_2_CD-rr.js → css-BgGytMsy.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-pOdUzj6D.js → dist-DSkVB16z.js} +2 -2
- package/dist/assets/web/dist/assets/durationTone-DYogEPEk.js +1 -0
- package/dist/assets/web/dist/assets/{edge-BenMDPct.js → edge-CdR6AU9v.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-CBbdEqG6.js → elixir-CQRb3PZx.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-DMub_2lY.js → elm-CpuA6W8B.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-CI9He_Up.js → erb-CvlrblqH.js} +1 -1
- package/dist/assets/web/dist/assets/{favicon-BQsePYv5.js → favicon-CSt-qDvc.js} +1 -1
- package/dist/assets/web/dist/assets/{git-rebase-BBfhnyF8.js → git-rebase-COnovCVc.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-CR9Fxrau.js → glimmer-js-C-DS4g_J.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-CwFwX_XS.js → glimmer-ts-DTkorWRI.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-Daiodp1r.js → glsl-CH51-JW7.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-DZvrsEkB.js → graphql-BO-XymNv.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-BaXOzdg7.js → hack-C-N0oicf.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-CMskq_VA.js → haml-BfmdtWoP.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-Dhw_f7jD.js → handlebars-BqhO_te-.js} +1 -1
- package/dist/assets/web/dist/assets/{html-C9k99z0z.js → html-DUEZlTKV.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-VckPItXN.js → html-derivative-DOUJTS8A.js} +1 -1
- package/dist/assets/web/dist/assets/{http-DXU_3h9v.js → http-j7UH0Cqv.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-KU24JNdp.js → hurl-D6ZaBLJk.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-BjXBzI5Z.js +1 -0
- package/dist/assets/web/dist/assets/{java-BbZjTNgf.js → java-dugWx3wi.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-DXCdqcZZ.js → javascript-CK4GC4JO.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-pDlsjTVp.js → jinja-DrLKIyKG.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-DsXDk66e.js → jison-CS1Th-TO.js} +1 -1
- package/dist/assets/web/dist/assets/{json-tCxXfRgG.js → json-CkBqdVys.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-B0ZYkCmi.js → jsx-CCxdg7n3.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-BLzzeI6x.js → julia-BalKW2hA.js} +1 -1
- package/dist/assets/web/dist/assets/{just-DvD2_60c.js → just-BKw1G40D.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-511zn3h7.js → latex-Dfad6ZN-.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-BD0BkMox.js → liquid-f_CMGBvd.js} +1 -1
- package/dist/assets/web/dist/assets/{indexFormat-B-pd4r7Y.js → localIso-CNA-bhS-.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-NFicKm6N.js → lua-BcMK_uKM.js} +1 -1
- package/dist/assets/web/dist/assets/main-B77vAZVg.css +1 -0
- package/dist/assets/web/dist/assets/main-DR8iKpNt.js +28 -0
- package/dist/assets/web/dist/assets/{marko-1-G_nEXK.js → marko-B0oNLx2i.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-DUe3AQLp.js → mdc-Bc_MmIyq.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-Dg379_bA.js → nginx-Cmxv8RIt.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-BnFq97ZO.js → nim-BtgRfaY0.js} +1 -1
- package/dist/assets/web/dist/assets/{org-F0uiwGvq.js → org-Dl9EzlHh.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-B0euczkl.js → perl-BmF7x7BB.js} +1 -1
- package/dist/assets/web/dist/assets/{php-DkL3k_n7.js → php-rH6CVptQ.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-L_OjjZP4.js → pug-CIN5Ccck.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-BMjB00Zz.js → qml-BpLS3RFr.js} +1 -1
- package/dist/assets/web/dist/assets/{r-DoeLdnqR.js → r-BUMF3B-H.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-BzYNkAWy.js → razor-B7sTBPdT.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-CPmElMk3.js → regexp-h2SvwrsJ.js} +1 -1
- package/dist/assets/web/dist/assets/residentsModel-VSJEepL1.js +1 -0
- package/dist/assets/web/dist/assets/{rst-DrsjgfI7.js → rst-DLOWEsD2.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-cB42ppvx.js → ruby-Cl_-I4k3.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-R5Y3NM_I.js → sas-Cm4M-BvV.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-C-zWxV9s.js → scss-CEFCEQUF.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-oJF96aAU.js → shellscript-CE6zb5eS.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-CznECKyi.js → shellsession-B4fDVzrM.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-wjHLSRag.js → soy-DwXCMpjE.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-C_BM8IOW.js → sql-DrnAKnyD.js} +1 -1
- package/dist/assets/web/dist/assets/{stata-D31HO8aQ.js → stata-BzlwPWD6.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-CKCLyplA.js → surrealql-CeUieZXd.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-3geWk2iu.js → svelte-BERIpIKR.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-DiJr_hTD.js → templ-6FSuUDwg.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-BEJGWyeF.js → tex-SWCyMwNX.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-C8Mzdpxx.js → ts-tags-VWj7gWgY.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-UQiSm_p3.js → tsx-B6dzfL8L.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-_Pvzkeo9.js → twig-BDXsr482.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-BWMkINKK.js → typescript-s100Dt8y.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-Bdg9m-e7.js → typst-WfyEAUdt.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-CDZrrAZ8.js → vue-CkXOOJ-6.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-tAlpNhfN.js → vue-html-CEpzdcau.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-C27UF_rh.js → vue-vine-D9dsfiFA.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-CxeDr9Zh.js → xml-iBDofbJq.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-Bpi0uUnM.js → xsl-CqbJF4hD.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-BtUgZ1GN.js → yaml-D9i6OxYe.js} +1 -1
- package/dist/cli.js +2255 -1170
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/ResidentDetailPage-DSpsye5e.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CT8fI1M3.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-Cqh8pTXC.js +0 -9
- package/dist/assets/web/dist/assets/RunsIndexPage-sjXr8iCV.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-Tz__zLzi.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-CbKCL58v.js +0 -1
- package/dist/assets/web/dist/assets/indexRow-Bnj883ii.js +0 -1
- package/dist/assets/web/dist/assets/localIso-L06jV29p.js +0 -1
- package/dist/assets/web/dist/assets/main-D3lG-yQ9.js +0 -28
- package/dist/assets/web/dist/assets/main-Dm11o0hc.css +0 -1
- package/dist/assets/web/dist/assets/residentDiskBudget-BMBKlYRH.js +0 -1
package/dist/assets/Dockerfile
CHANGED
|
@@ -87,6 +87,17 @@ RUN npm install -g @earendil-works/pi-coding-agent@0.85.1 \
|
|
|
87
87
|
&& pi --version | grep -qx '0.85.1' \
|
|
88
88
|
&& pi --help | grep -q -- '--mode <mode>'
|
|
89
89
|
|
|
90
|
+
# OpenCode, the second harness (docs/reference/specs/harness.md): `@opencode/cli`
|
|
91
|
+
# at the EXACT pin held by src/deploy/imageOpenCodeHarness.test.ts, beside pi.
|
|
92
|
+
# The wrapper's postinstall hard-links the platform binary into place, so it
|
|
93
|
+
# is the one install script allowed by name; the grep proves the binary on
|
|
94
|
+
# PATH prints the pin's version text (`opencode v2.0.3`, exactly); the npm
|
|
95
|
+
# cache (a 200 MB tarball) is dropped in the same layer. Dark until a
|
|
96
|
+
# deployment names it: nothing here starts it on its own.
|
|
97
|
+
RUN npm install -g --allow-scripts=@opencode/cli @opencode/cli@2.0.3 \
|
|
98
|
+
&& npm cache clean --force \
|
|
99
|
+
&& opencode --version | grep -qx 'opencode v2.0.3'
|
|
100
|
+
|
|
90
101
|
# Non-root user; agents run bash with this user's (container-scoped) permissions.
|
|
91
102
|
RUN useradd -m -u 1001 switchboard
|
|
92
103
|
WORKDIR /app
|
|
@@ -311,10 +311,15 @@ workspaceDir: ./workspaces
|
|
|
311
311
|
# them the request is refused naming what is missing. Per unit: at most
|
|
312
312
|
# `maxRounds` review rounds and `maxMinutes` minutes of wall clock — whichever
|
|
313
313
|
# hits first ends the unit, and each child round's own budget is clipped to the
|
|
314
|
-
# remaining time.
|
|
315
|
-
#
|
|
316
|
-
#
|
|
317
|
-
#
|
|
314
|
+
# remaining time. The pipeline is budgeted for the loop, not one round: the
|
|
315
|
+
# coding child's directive is additionally capped to leave two review rounds and
|
|
316
|
+
# one merge poll (11 minutes) on the clock, so a child that uses its whole
|
|
317
|
+
# budget still hands the review its time — which is why `maxMinutes` must be an
|
|
318
|
+
# integer >= 14 (the loop's reserve plus one round). `maxMinutes` is the ship
|
|
319
|
+
# preset's declared budget: a channel or user `boundary.maxMinutes` and a
|
|
320
|
+
# per-message `budget:` directive clip it like any preset's, and the card says
|
|
321
|
+
# so. Defaults shown; `maxRounds` must be an integer >= 1; any other key under
|
|
322
|
+
# `ship` fails the load by name.
|
|
318
323
|
# ship:
|
|
319
324
|
# maxRounds: 3 # review rounds per unit
|
|
320
325
|
# maxMinutes: 120 # the ship preset's wall-clock budget in minutes (the registry's default)
|
|
@@ -1807,6 +1807,18 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
1807
1807
|
this.ctx.storage.transactionSync(() => {
|
|
1808
1808
|
result = this.upsertInTransaction(record, proposal);
|
|
1809
1809
|
});
|
|
1810
|
+
// A coordinator child closed OUTSIDE the ledger's finish — the run loop or
|
|
1811
|
+
// a reclaim writing an `interrupted` record, the pi harness's typed restart,
|
|
1812
|
+
// a resume abandoning a lost workspace — still wakes its parent's wait at
|
|
1813
|
+
// once (run-history item 47): the same `run-finished-<runId>` event rides
|
|
1814
|
+
// this commit. The tombstone a run writes at its start is excluded (its
|
|
1815
|
+
// `finishedAt` equals `startedAt`); the parent confirms by `read-record`
|
|
1816
|
+
// before it acts, so a duplicate send is harmless.
|
|
1817
|
+
if (result.stored && record.parentInstanceId !== undefined && record.finishedAt > record.startedAt) {
|
|
1818
|
+
const event = await sendRunFinished(this.env.SHIP_COORDINATOR, record);
|
|
1819
|
+
if (event.kind === "failed")
|
|
1820
|
+
console.warn(`[runs/put] ${record.id} → ${event.type} not delivered to ${event.instance}: ${event.reason}`);
|
|
1821
|
+
}
|
|
1810
1822
|
if ((await this.ctx.storage.getAlarm()) === null)
|
|
1811
1823
|
await this.ctx.storage.setAlarm(systemClock() + RUN_SWEEP_INTERVAL_MS);
|
|
1812
1824
|
return result;
|
|
@@ -201,6 +201,18 @@ RUN npm install -g @earendil-works/pi-coding-agent@0.85.1 \
|
|
|
201
201
|
&& pi --version | grep -qx '0.85.1' \
|
|
202
202
|
&& pi --help | grep -q -- '--mode <mode>'
|
|
203
203
|
|
|
204
|
+
# OpenCode, the second harness (docs/reference/specs/harness.md): `@opencode/cli`
|
|
205
|
+
# at the EXACT pin held by src/deploy/imageOpenCodeHarness.test.ts, beside pi.
|
|
206
|
+
# The wrapper's postinstall hard-links the platform binary into place, so it
|
|
207
|
+
# is the one install script allowed by name; the grep proves the binary on
|
|
208
|
+
# PATH prints the pin's version text (`opencode v2.0.3`, exactly); the npm
|
|
209
|
+
# cache (a 200 MB tarball) is dropped in the same layer, and the proof runs
|
|
210
|
+
# again as worker1 below. Dark until a deployment names it: nothing here
|
|
211
|
+
# starts it on its own.
|
|
212
|
+
RUN npm install -g --allow-scripts=@opencode/cli @opencode/cli@2.0.3 \
|
|
213
|
+
&& npm cache clean --force \
|
|
214
|
+
&& opencode --version | grep -qx 'opencode v2.0.3'
|
|
215
|
+
|
|
204
216
|
# Agent commands never run as root (repo code runs unprivileged, one user per
|
|
205
217
|
# thread — docs/reference/specs/resident-repos.md). The SDK's exec has no user/uid option
|
|
206
218
|
# (verified against 0.13.0-next.751.1), so the container server itself runs as
|
|
@@ -230,6 +242,12 @@ RUN su -s /bin/bash worker1 -c "playwright screenshot --viewport-size=640,480 'd
|
|
|
230
242
|
# own, named by PI_CODING_AGENT_DIR at start).
|
|
231
243
|
RUN su -s /bin/bash worker1 -c "pi --version | grep -qx '0.85.1'"
|
|
232
244
|
|
|
245
|
+
# OpenCode once more, as worker1: a run's server is started as the thread's
|
|
246
|
+
# user through the same `su`, so the BUILD proves an unprivileged user finds
|
|
247
|
+
# the pin on PATH and the binary runs without a writable home (its roots are
|
|
248
|
+
# the run's own, named by the XDG variables at start).
|
|
249
|
+
RUN su -s /bin/bash worker1 -c "opencode --version | grep -qx 'opencode v2.0.3'"
|
|
250
|
+
|
|
233
251
|
# bun once more, as worker1: a thread's `bun install` runs as the thread's user
|
|
234
252
|
# through the same `su`, whose PATH is /etc/environment's rather than this
|
|
235
253
|
# file's — so the BUILD proves that PATH resolves the pin, not the base's bun.
|
|
@@ -505,16 +505,18 @@ const FORCE_DETACH_KILL_TIMEOUT_MS = 2_000;
|
|
|
505
505
|
* binding record is KEPT so the next attach recreates with the same
|
|
506
506
|
* ref. Overridable per resident via the onboard-time `worktreeTtlDays`. */
|
|
507
507
|
const WORKTREE_TTL_DAYS_DEFAULT = 7;
|
|
508
|
-
/** A live binding whose last attach is older than this
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
* release call was lost.
|
|
508
|
+
/** A live binding whose last attach is older than this and has no op in
|
|
509
|
+
* flight is released by the sweep step (every refresh instance runs one),
|
|
510
|
+
* whatever its tree holds (item 17) — runs that ended before /detach existed,
|
|
511
|
+
* or whose release call was lost. */
|
|
512
512
|
const CLEAN_IDLE_RELEASE_S = 60 * 60;
|
|
513
|
-
/** Idle sleep: when no
|
|
514
|
-
*
|
|
515
|
-
* next instance to the idle cadence, so the container can
|
|
516
|
-
* (SLEEP_AFTER); the next attach refreshes first if the mirror
|
|
517
|
-
* (refresh-on-attach).
|
|
513
|
+
/** Idle sleep: when no live binding was attached to or used within this
|
|
514
|
+
* window and nothing is in flight, the refresh cycle skips the fetch and the
|
|
515
|
+
* cron holds the next instance to the idle cadence, so the container can
|
|
516
|
+
* actually sleep (SLEEP_AFTER); the next attach refreshes first if the mirror
|
|
517
|
+
* is stale (refresh-on-attach). The disk-full recycle (item 54) reads the
|
|
518
|
+
* same window through the same predicate (`recentlyUsed`): a sleep and a
|
|
519
|
+
* recycle destroy the same disk, so one rule says when it may go away. */
|
|
518
520
|
const IDLE_AFTER_S = 60 * 60;
|
|
519
521
|
/** LRU eviction floor: an over-cap onboard with `evictColdest:true` may
|
|
520
522
|
* offboard the coldest eligible warm resident, but never one whose last
|
|
@@ -1037,6 +1039,15 @@ interface ThreadBinding {
|
|
|
1037
1039
|
/** Why the tree could not be measured before that eviction (git could not
|
|
1038
1040
|
* read it) — so an unreadable tree is never recorded as clean. */
|
|
1039
1041
|
evictedUnmeasured?: string;
|
|
1042
|
+
/** What the tree held when the last disk-full recycle (item 54) discarded
|
|
1043
|
+
* it with the disk — the same measurement as `evictedLeftBehind`, under its
|
|
1044
|
+
* own name because the binding is not evicted: its user is kept and the
|
|
1045
|
+
* next attach recreates the tree (`worktree-missing`). Both rewritten on
|
|
1046
|
+
* every recycle; absent when the tree was clean or already gone. */
|
|
1047
|
+
recycledLeftBehind?: LeftBehind;
|
|
1048
|
+
/** Why the tree could not be measured before that recycle (git could not
|
|
1049
|
+
* read it) — still discarded with the disk, never recorded as clean. */
|
|
1050
|
+
recycledUnmeasured?: string;
|
|
1040
1051
|
/** How deps were last materialized (evidence that the per-branch reconciliation ran). */
|
|
1041
1052
|
deps?: ThreadDepsMechanism;
|
|
1042
1053
|
/** Commit the worktree was last attached at (the ref's tip in the mirror
|
|
@@ -2945,12 +2956,11 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
2945
2956
|
throw new CycleRestartError("image-stale-restart");
|
|
2946
2957
|
}
|
|
2947
2958
|
|
|
2948
|
-
// Idle sleep:
|
|
2949
|
-
//
|
|
2959
|
+
// Idle sleep: no live binding used within IDLE_AFTER_S and nothing in
|
|
2960
|
+
// flight → skip this fetch; the cron holds the next instance to the idle
|
|
2950
2961
|
// cadence, so SLEEP_AFTER can elapse. Staleness is repaid at the next
|
|
2951
|
-
// attach (refreshIfStale).
|
|
2952
|
-
//
|
|
2953
|
-
// uncommitted work is not snapshotted.
|
|
2962
|
+
// attach (refreshIfStale). What a live tree holds is not asked (item 17):
|
|
2963
|
+
// sleep destroys the disk, and a tree no run is using protects nothing.
|
|
2954
2964
|
// Only a SETTLED resident may park: a cycle that finds `refreshing`/
|
|
2955
2965
|
// `restoring` at entry is looking at a marker left by a cycle that died
|
|
2956
2966
|
// mid-flight (a deploy evicting the DO: stuck `refreshing` + parked →
|
|
@@ -3002,7 +3012,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3002
3012
|
await this.ctx.storage.delete(DEGRADED_STREAK_KEY);
|
|
3003
3013
|
}
|
|
3004
3014
|
if (settled && (await this.isIdle())) {
|
|
3005
|
-
// isIdle awaited (
|
|
3015
|
+
// isIdle awaited (the bindings listing) — re-read before writing.
|
|
3006
3016
|
const now = (await this.ctx.storage.get<RepoFacts>(FACTS_KEY)) ?? facts;
|
|
3007
3017
|
if (!now.idleSince)
|
|
3008
3018
|
await this.ctx.storage.put(FACTS_KEY, {
|
|
@@ -3798,15 +3808,24 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3798
3808
|
});
|
|
3799
3809
|
}
|
|
3800
3810
|
|
|
3801
|
-
/**
|
|
3802
|
-
*
|
|
3803
|
-
*
|
|
3804
|
-
*
|
|
3805
|
-
*
|
|
3806
|
-
|
|
3807
|
-
|
|
3811
|
+
/** Whether a run may be using this disk: a live binding attached to or
|
|
3812
|
+
* used (an exec bumps `lastAttachAt` too, item 21) within IDLE_AFTER_S.
|
|
3813
|
+
* The op counter is 0 between a run's tool calls, so this floor is what
|
|
3814
|
+
* stands for a run mid-flight. The one predicate behind "the disk may go
|
|
3815
|
+
* away": the idle-sleep gate (item 16b — a platform sleep destroys the
|
|
3816
|
+
* disk) and the disk-full recycle (item 54) both read it, and neither asks
|
|
3817
|
+
* what a tree holds (item 17): a dirty tree no run is using protects
|
|
3818
|
+
* nothing — the next attach wipes it. Bindings are storage, so this needs
|
|
3819
|
+
* no container. */
|
|
3820
|
+
private recentlyUsed(live: ThreadBinding[]): boolean {
|
|
3808
3821
|
const recent = systemClock() - IDLE_AFTER_S * 1000;
|
|
3809
|
-
|
|
3822
|
+
return live.some((b) => Date.parse(b.lastAttachAt) >= recent);
|
|
3823
|
+
}
|
|
3824
|
+
|
|
3825
|
+
/** Idle = no live binding used within the floor and nothing in flight; the
|
|
3826
|
+
* sweep's release records what an idle tree held. */
|
|
3827
|
+
private async isIdle(): Promise<boolean> {
|
|
3828
|
+
if (this.recentlyUsed(await this.liveBindings())) return false;
|
|
3810
3829
|
return this.inFlightCount() === 0;
|
|
3811
3830
|
}
|
|
3812
3831
|
|
|
@@ -3816,17 +3835,6 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3816
3835
|
return [...all.values()].filter((b) => !b.evicted && b.user);
|
|
3817
3836
|
}
|
|
3818
3837
|
|
|
3819
|
-
/** The disk-full recycle's tree guard (item 54), its one reader: true when
|
|
3820
|
-
* every live tree is clean as its thread user, or the runtime is already
|
|
3821
|
-
* down — a sleep has destroyed the disk, so there is nothing left to lose. */
|
|
3822
|
-
private async liveTreesClean(live: ThreadBinding[]): Promise<boolean> {
|
|
3823
|
-
if (!(await this.isRuntimeActive().catch(() => false))) return true;
|
|
3824
|
-
// Concurrent: each check touches only its own (disjoint) tree, and one
|
|
3825
|
-
// spawn each — no serial 3-probe round-trip per live binding.
|
|
3826
|
-
const checks = await Promise.all(live.map((b) => this.worktreeCleanliness(b)));
|
|
3827
|
-
return checks.every((c) => c.clean); // any dirty or unknown → not clean
|
|
3828
|
-
}
|
|
3829
|
-
|
|
3830
3838
|
// -- disk-full (docs/reference/specs/resident-repos.md item 54) ---------------------------
|
|
3831
3839
|
|
|
3832
3840
|
/** Free space on the workspace mount in KiB; `null` when df cannot answer.
|
|
@@ -3852,28 +3860,54 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3852
3860
|
|
|
3853
3861
|
/** The disk is a cache: stop the container so the next cycle attempt
|
|
3854
3862
|
* restores mirror + checkout from R2 onto an empty disk — the same wake
|
|
3855
|
-
* path as a platform sleep. Only when the pure plan allows it:
|
|
3856
|
-
* flight (`selfInFlight` excludes the
|
|
3857
|
-
*
|
|
3858
|
-
*
|
|
3859
|
-
*
|
|
3860
|
-
*
|
|
3863
|
+
* path as a platform sleep. Only when the pure plan allows it: no recycle
|
|
3864
|
+
* within the cooldown, nothing in flight (`selfInFlight` excludes the
|
|
3865
|
+
* calling refresh cycle from the count) and no live binding used within
|
|
3866
|
+
* the idle floor — the idle-sleep gate's own predicate (`recentlyUsed`),
|
|
3867
|
+
* because a sleep and a recycle destroy the same disk: it may go away when
|
|
3868
|
+
* no run is using it, never for what the trees hold (item 17). A recycle
|
|
3869
|
+
* discards every live tree the way an eviction does, so each is measured
|
|
3870
|
+
* first, as its thread user, and its binding records what went
|
|
3871
|
+
* (`recordRecycledTree`); the plan is decided again after those awaits,
|
|
3872
|
+
* right before the stop. A refused recycle is written to
|
|
3873
|
+
* `lastRefreshError` with its why, so `/residents` says what an operator
|
|
3874
|
+
* must do; the `degraded` reason stays the clean `disk-full: …`. Answers
|
|
3875
|
+
* whether the container was recycled. */
|
|
3861
3876
|
private async recoverFromDiskFull(reason: string, selfInFlight: number): Promise<boolean> {
|
|
3862
3877
|
const lastRecycleAt = await this.ctx.storage.get<number>(DISK_FULL_RECYCLE_KEY);
|
|
3863
|
-
const plan =
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3878
|
+
const plan = (live: ThreadBinding[]) =>
|
|
3879
|
+
planDiskFullRecovery({
|
|
3880
|
+
now: systemClock(),
|
|
3881
|
+
lastRecycleAt,
|
|
3882
|
+
inFlight: this.inFlightCount() - selfInFlight,
|
|
3883
|
+
recentlyUsed: this.recentlyUsed(live),
|
|
3884
|
+
idleFloorS: IDLE_AFTER_S,
|
|
3885
|
+
});
|
|
3886
|
+
const kept = async (why: string) => {
|
|
3887
|
+
console.log(`disk-full: container kept — ${why}`);
|
|
3888
|
+
await this.recordRefreshError(`${reason} — container kept: ${why}`);
|
|
3872
3889
|
return false;
|
|
3873
|
-
}
|
|
3890
|
+
};
|
|
3891
|
+
const live = await this.liveBindings();
|
|
3892
|
+
const first = plan(live);
|
|
3893
|
+
if (first.action === "wait") return kept(first.why);
|
|
3894
|
+
// What each live tree holds, for its record — never a reason to keep the
|
|
3895
|
+
// container. Concurrent: each probe touches only its own tree, one spawn
|
|
3896
|
+
// each. Runtime down: the disk, and every tree with it, is already gone.
|
|
3897
|
+
const active = await this.isRuntimeActive().catch(() => false);
|
|
3898
|
+
const trees = active
|
|
3899
|
+
? await Promise.all(
|
|
3900
|
+
live.map(async (binding) => [binding, await this.measureTreeBeforeEviction(binding)] as const),
|
|
3901
|
+
)
|
|
3902
|
+
: live.map((binding) => [binding, undefined] as const);
|
|
3903
|
+
// The measurement awaited (the DO yields at each await): decide again over
|
|
3904
|
+
// fresh facts — an attach or an op that landed meanwhile keeps the container.
|
|
3905
|
+
const verdict = plan(await this.liveBindings());
|
|
3906
|
+
if (verdict.action === "wait") return kept(verdict.why);
|
|
3874
3907
|
console.log(
|
|
3875
3908
|
`disk-full: recycling the container — the next cycle restores mirror + checkout from R2 onto an empty disk (${reason})`,
|
|
3876
3909
|
);
|
|
3910
|
+
for (const [binding, tree] of trees) await this.recordRecycledTree(binding, tree);
|
|
3877
3911
|
await this.ctx.storage.put(DISK_FULL_RECYCLE_KEY, systemClock());
|
|
3878
3912
|
await this.recordRefreshError(`${reason} — container recycled; restoring from R2 on the next cycle`);
|
|
3879
3913
|
this.swapIncarnation(); // deliberate incarnation swap
|
|
@@ -3881,6 +3915,27 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3881
3915
|
return true;
|
|
3882
3916
|
}
|
|
3883
3917
|
|
|
3918
|
+
/** The disk-full recycle's record for one live tree (item 54), written the
|
|
3919
|
+
* way `evictBinding` writes an eviction's — but the binding stays live, its
|
|
3920
|
+
* pool user kept and its tree recreated on the next attach, so this is not
|
|
3921
|
+
* an eviction and the record sits under its own name beside
|
|
3922
|
+
* `evictedLeftBehind`: the counts when something was there, the probe's
|
|
3923
|
+
* first error line when git could not read the tree, both rewritten on
|
|
3924
|
+
* every recycle so nothing lingers from an earlier one, and one log line
|
|
3925
|
+
* when the tree held something. Re-read first: a binding evicted or
|
|
3926
|
+
* re-attached during the measurement is left as is. */
|
|
3927
|
+
private async recordRecycledTree(binding: ThreadBinding, tree: EvictedTree | undefined): Promise<void> {
|
|
3928
|
+
const now = await this.ctx.storage.get<ThreadBinding>(threadBindingKey(binding.threadKey));
|
|
3929
|
+
if (!now || now.evicted || now.lastAttachAt !== binding.lastAttachAt) return;
|
|
3930
|
+
await this.ctx.storage.put(threadBindingKey(binding.threadKey), {
|
|
3931
|
+
...now,
|
|
3932
|
+
recycledLeftBehind: tree && "leftBehind" in tree ? tree.leftBehind : undefined,
|
|
3933
|
+
recycledUnmeasured: tree && "unmeasured" in tree ? tree.unmeasured : undefined,
|
|
3934
|
+
} satisfies ThreadBinding);
|
|
3935
|
+
if (tree)
|
|
3936
|
+
console.log(`disk-full: ${binding.threadKey} tree discarded with the disk — ${evictedTreeSentence(tree)}`);
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3884
3939
|
// -- disk budget (docs/reference/specs/resident-repos.md item 55) -------------------------
|
|
3885
3940
|
|
|
3886
3941
|
/** The `df` half: total/used/free of the workspace mount; null when df
|
|
@@ -6202,11 +6257,10 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6202
6257
|
* (su), never as root: the worktree is thread-owned, so root git in it
|
|
6203
6258
|
* would be refused by safe.directory and would be the exact repo-local-
|
|
6204
6259
|
* config execution vector safe.directory exists to block. Read by
|
|
6205
|
-
* `measureTreeBeforeEviction` for the
|
|
6206
|
-
*
|
|
6207
|
-
*
|
|
6208
|
-
*
|
|
6209
|
-
* measured: there was nothing to discard.
|
|
6260
|
+
* `measureTreeBeforeEviction` alone, for the record of an eviction or of
|
|
6261
|
+
* the disk-full recycle — never a reason to keep a tree or a container
|
|
6262
|
+
* (item 17). A tree that no longer exists (disk recycled by a sleep/wake)
|
|
6263
|
+
* is clean with nothing measured: there was nothing to discard.
|
|
6210
6264
|
*
|
|
6211
6265
|
* ONE spawn: the presence test and both git probes fold
|
|
6212
6266
|
* into the pure `worktreeCleanlinessScript` (test -d as root, both git
|
|
@@ -6222,7 +6276,8 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6222
6276
|
return parseWorktreeCleanliness(r);
|
|
6223
6277
|
}
|
|
6224
6278
|
|
|
6225
|
-
/** What a tree holds right before its eviction
|
|
6279
|
+
/** What a tree holds right before its eviction — or before the disk-full
|
|
6280
|
+
* recycle discards it with the disk (item 54) — for the record and the log
|
|
6226
6281
|
* (item 17): the one-spawn probe as the thread user, read once into
|
|
6227
6282
|
* `evictedTreeOf` — never into a keep decision. Callers skip it when the
|
|
6228
6283
|
* runtime is down: the disk, and the tree with it, is already gone. */
|
|
@@ -6782,6 +6837,8 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6782
6837
|
evictedWhy,
|
|
6783
6838
|
evictedLeftBehind,
|
|
6784
6839
|
evictedUnmeasured,
|
|
6840
|
+
recycledLeftBehind,
|
|
6841
|
+
recycledUnmeasured,
|
|
6785
6842
|
}) => ({
|
|
6786
6843
|
threadKey,
|
|
6787
6844
|
ref,
|
|
@@ -6795,6 +6852,8 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
6795
6852
|
evictedWhy: evictedWhy ?? null,
|
|
6796
6853
|
evictedLeftBehind: evictedLeftBehind ?? null,
|
|
6797
6854
|
evictedUnmeasured: evictedUnmeasured ?? null,
|
|
6855
|
+
recycledLeftBehind: recycledLeftBehind ?? null,
|
|
6856
|
+
recycledUnmeasured: recycledUnmeasured ?? null,
|
|
6798
6857
|
}),
|
|
6799
6858
|
);
|
|
6800
6859
|
return {
|
|
@@ -176,6 +176,17 @@ RUN npm install -g @earendil-works/pi-coding-agent@0.85.1 \
|
|
|
176
176
|
&& pi --version | grep -qx '0.85.1' \
|
|
177
177
|
&& pi --help | grep -q -- '--mode <mode>'
|
|
178
178
|
|
|
179
|
+
# OpenCode, the second harness (docs/reference/specs/harness.md): `@opencode/cli`
|
|
180
|
+
# at the EXACT pin held by src/deploy/imageOpenCodeHarness.test.ts, beside pi.
|
|
181
|
+
# The wrapper's postinstall hard-links the platform binary into place, so it
|
|
182
|
+
# is the one install script allowed by name; the grep proves the binary on
|
|
183
|
+
# PATH prints the pin's version text (`opencode v2.0.3`, exactly); the npm
|
|
184
|
+
# cache (a 200 MB tarball) is dropped in the same layer. Dark until a
|
|
185
|
+
# deployment names it: nothing here starts it on its own.
|
|
186
|
+
RUN npm install -g --allow-scripts=@opencode/cli @opencode/cli@2.0.3 \
|
|
187
|
+
&& npm cache clean --force \
|
|
188
|
+
&& opencode --version | grep -qx 'opencode v2.0.3'
|
|
189
|
+
|
|
179
190
|
# The container outlives a crash of the SDK's container server (the
|
|
180
191
|
# supervisor's header says why). The base image's ENTRYPOINT is tini running
|
|
181
192
|
# the server, so one uncaught exception there ended the container; here tini
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
} from "../../src/execution/sandboxLifecycle.js";
|
|
45
45
|
import { envFromRequest } from "../../src/execution/sandboxEnv.js";
|
|
46
46
|
import { IdleGuard, type IdleGuardHost } from "../../src/execution/sandboxIdle.js";
|
|
47
|
+
import { StartGate, type StartGateHost, type StartingCause } from "../../src/execution/sandboxStart.js";
|
|
47
48
|
import { RUNTIME_REPLACEMENT_WORDING, isRuntimeUnreachableSignal } from "../../src/execution/residentRefresh.js";
|
|
48
49
|
import {
|
|
49
50
|
fleetBusyAnswer,
|
|
@@ -53,6 +54,8 @@ import {
|
|
|
53
54
|
runtimeUnreachableAnswer,
|
|
54
55
|
runtimeUnreachableExecAnswer,
|
|
55
56
|
SandboxRuntimeUnreachableError,
|
|
57
|
+
sandboxStartingAnswer,
|
|
58
|
+
sandboxStartingExecAnswer,
|
|
56
59
|
thrownShape,
|
|
57
60
|
thrownText,
|
|
58
61
|
} from "../../src/execution/sandboxErrors.js";
|
|
@@ -167,6 +170,12 @@ interface FileRefusal {
|
|
|
167
170
|
reason?: string;
|
|
168
171
|
}
|
|
169
172
|
|
|
173
|
+
/** The SDK's own limit on the warm-up command (`true`): generous, since the
|
|
174
|
+
* SDK's start — an instance grant up to 30 s, the runtime's port up to 90 s
|
|
175
|
+
* by its defaults — happens inside this one call. The executor's wait for
|
|
176
|
+
* the `sandbox-starting` token is bounded on its own side. */
|
|
177
|
+
const WARM_UP_BACKSTOP_MS = 4 * 60_000;
|
|
178
|
+
|
|
170
179
|
/** Durable Object storage key of the idle ledger's served-time. */
|
|
171
180
|
const IDLE_LEDGER_KEY = "switchboard.idle.lastServedAt";
|
|
172
181
|
/** The scheduled-callback name of the idle sweep (a method below). */
|
|
@@ -185,10 +194,12 @@ export class SwitchboardSandbox extends Sandbox<Env> {
|
|
|
185
194
|
sleepAfter = SANDBOX_SLEEP_AFTER;
|
|
186
195
|
|
|
187
196
|
private readonly idle: IdleGuard;
|
|
197
|
+
private readonly gate: StartGate;
|
|
188
198
|
|
|
189
199
|
constructor(...args: ConstructorParameters<typeof Sandbox<Env>>) {
|
|
190
200
|
super(...args);
|
|
191
201
|
this.idle = new IdleGuard(this.idleHost());
|
|
202
|
+
this.gate = new StartGate(this.startHost());
|
|
192
203
|
// Every wake, including the one the SDK's own alarm causes on a leaked
|
|
193
204
|
// container: the baseline is read and a sweep armed before any request.
|
|
194
205
|
this.ctx.blockConcurrencyWhile(() => this.idle.wake());
|
|
@@ -215,6 +226,25 @@ export class SwitchboardSandbox extends Sandbox<Env> {
|
|
|
215
226
|
};
|
|
216
227
|
}
|
|
217
228
|
|
|
229
|
+
/** The start gate's view of this object (docs/reference/specs/execution.md
|
|
230
|
+
* item 23): the platform's running flag, and a warm-up that is one trivial
|
|
231
|
+
* command through the SDK — which does the start (instance grant, image
|
|
232
|
+
* pull, boot, the runtime's port) and answers when the runtime does. The
|
|
233
|
+
* warm-up runs inside the idle ledger so the guard sees a request in flight
|
|
234
|
+
* for as long as the start takes. */
|
|
235
|
+
private startHost(): StartGateHost {
|
|
236
|
+
return {
|
|
237
|
+
now: systemClock,
|
|
238
|
+
containerRunning: () => this.ctx.container?.running,
|
|
239
|
+
warmUp: () =>
|
|
240
|
+
this.idle.served(async () => {
|
|
241
|
+
const proc = await createExtensionProcessSandbox(this).exec(["true"], { timeout: WARM_UP_BACKSTOP_MS });
|
|
242
|
+
await proc.output({ encoding: "utf8", timeout: WARM_UP_BACKSTOP_MS });
|
|
243
|
+
}),
|
|
244
|
+
log: (event) => console.log(JSON.stringify({ ...event, thread: this.ctx.id.name ?? this.ctx.id.toString() })),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
218
248
|
/** The idle sweep, scheduled by the guard through the SDK's schedule table
|
|
219
249
|
* and re-armed by it while a container is running. */
|
|
220
250
|
async idleSweep(): Promise<void> {
|
|
@@ -240,7 +270,19 @@ export class SwitchboardSandbox extends Sandbox<Env> {
|
|
|
240
270
|
execTimeoutSecs: number,
|
|
241
271
|
envVars: Record<string, string>,
|
|
242
272
|
): Promise<ExecAnswer | ExecFailure> {
|
|
243
|
-
|
|
273
|
+
const startedAt = systemClock();
|
|
274
|
+
return this.idle.served(async () => {
|
|
275
|
+
try {
|
|
276
|
+
return await this.gate.through(
|
|
277
|
+
() => this.execute(command, execTimeoutSecs, envVars),
|
|
278
|
+
(cause) => sandboxStartingExecAnswer(cause),
|
|
279
|
+
);
|
|
280
|
+
} catch (err) {
|
|
281
|
+
// The warm-up's own failure, handed on by the gate: a full fleet or a
|
|
282
|
+
// silent control port keeps its name; anything else propagates.
|
|
283
|
+
return this.execFailure(err, startedAt);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
244
286
|
}
|
|
245
287
|
|
|
246
288
|
/** `runCommand` without the ledger entry: the body, and the internal caller
|
|
@@ -337,9 +379,19 @@ export class SwitchboardSandbox extends Sandbox<Env> {
|
|
|
337
379
|
}
|
|
338
380
|
}
|
|
339
381
|
|
|
382
|
+
/** A file route's answer while the container starts: HTTP 503 with the
|
|
383
|
+
* token, like a full fleet's. */
|
|
384
|
+
private startingRefusal(cause: StartingCause): FileRefusal {
|
|
385
|
+
const { error, reason } = sandboxStartingAnswer(cause);
|
|
386
|
+
return { error, status: 503, reason };
|
|
387
|
+
}
|
|
388
|
+
|
|
340
389
|
async readText(path: string): Promise<{ content: string } | FileRefusal> {
|
|
341
390
|
return this.idle.served(() =>
|
|
342
|
-
this.
|
|
391
|
+
this.gate.through(
|
|
392
|
+
() => this.fileOp(async () => ({ content: (await this.readFile(path, { encoding: "utf-8" })).content })),
|
|
393
|
+
(cause) => this.startingRefusal(cause),
|
|
394
|
+
),
|
|
343
395
|
);
|
|
344
396
|
}
|
|
345
397
|
|
|
@@ -348,7 +400,12 @@ export class SwitchboardSandbox extends Sandbox<Env> {
|
|
|
348
400
|
* decoded bytes to it — an SDK read that came back short would otherwise
|
|
349
401
|
* pass as the file. A file over the cap is refused by name inside a 200. */
|
|
350
402
|
async readBase64(path: string): Promise<Base64ReadAnswer | FileRefusal> {
|
|
351
|
-
return this.idle.served(() =>
|
|
403
|
+
return this.idle.served(() =>
|
|
404
|
+
this.gate.through(
|
|
405
|
+
() => this.readBase64Now(path),
|
|
406
|
+
(cause) => this.startingRefusal(cause),
|
|
407
|
+
),
|
|
408
|
+
);
|
|
352
409
|
}
|
|
353
410
|
|
|
354
411
|
private async readBase64Now(path: string): Promise<Base64ReadAnswer | FileRefusal> {
|
|
@@ -377,10 +434,14 @@ export class SwitchboardSandbox extends Sandbox<Env> {
|
|
|
377
434
|
|
|
378
435
|
async write(path: string, content: string): Promise<{ ok: true } | FileRefusal> {
|
|
379
436
|
return this.idle.served(() =>
|
|
380
|
-
this.
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
437
|
+
this.gate.through(
|
|
438
|
+
() =>
|
|
439
|
+
this.fileOp(async () => {
|
|
440
|
+
await this.writeFile(path, content);
|
|
441
|
+
return { ok: true as const };
|
|
442
|
+
}),
|
|
443
|
+
(cause) => this.startingRefusal(cause),
|
|
444
|
+
),
|
|
384
445
|
);
|
|
385
446
|
}
|
|
386
447
|
}
|