@coreplane/switchboard 1.227.0 → 1.229.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/config/config.example.yaml +7 -13
- package/dist/assets/deploy/cloudflare-memory/worker.ts +157 -3
- package/dist/assets/deploy/cloudflare-resident/worker.ts +170 -30
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +22 -18
- package/dist/assets/deploy/cloudflare-sandbox/package.json +1 -1
- package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.sh +11 -10
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +332 -299
- package/dist/assets/deploy/cloudflare-sandbox/wrangler.template.jsonc +2 -2
- package/dist/assets/package-lock.json +2542 -212
- package/dist/assets/package.json +2 -2
- package/dist/assets/project.json +2 -2
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +19 -34
- package/dist/assets/src/core/authz/policy.ts +8 -0
- package/dist/assets/src/core/authz/resource.ts +3 -1
- package/dist/assets/src/core/authz/types.ts +10 -1
- package/dist/assets/src/core/chatMessage.ts +1 -1
- package/dist/assets/src/core/coordinator/contract.ts +6 -0
- package/dist/assets/src/core/coordinator/driver.ts +8 -6
- package/dist/assets/src/core/runEvents.ts +53 -14
- package/dist/assets/src/core/runFriction.ts +3 -2
- package/dist/assets/src/core/runRecord.ts +46 -3
- package/dist/assets/src/core/runUsage.ts +199 -0
- package/dist/assets/src/core/ship/coordinator.ts +4 -2
- package/dist/assets/src/execution/residentDepCache.ts +34 -8
- package/dist/assets/src/execution/residentRebind.ts +84 -7
- package/dist/assets/src/execution/sandboxErrors.ts +14 -38
- package/dist/assets/src/execution/sandboxLifecycle.ts +78 -0
- package/dist/assets/web/dist/.vite/manifest.json +20 -20
- package/dist/assets/web/dist/assets/CostsPage-DQg30mHr.js +2 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-Chvll3wy.js → ResidentDetailPage-CkVYktwT.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-B5f8IwGF.js → ResidentsIndexPage-BLanxf27.js} +1 -1
- package/dist/assets/web/dist/assets/RunRoutePage-U3nwL8Df.js +13 -0
- package/dist/assets/web/dist/assets/{RunsIndexPage-BTJuKFTv.js → RunsIndexPage-DrIVxmpl.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-BVfgUBvP.js → ScheduledPage-DpqubmIm.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-CFXbAw7S.js → StatusDot-BpD9MRge.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DcHMtbHJ.js → Tooltip-BYv0WSrA.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-DKhqHu0V.js → dist-BZmA5qTt.js} +1 -1
- package/dist/assets/web/dist/assets/{main-CveRd2yk.js → main-C4GOEklV.js} +2 -2
- package/dist/assets/web/dist/assets/main-CAVqMbiX.css +1 -0
- package/dist/cli.js +9094 -9223
- package/package.json +1 -2
- package/dist/assets/src/execution/sandboxKeepalive.ts +0 -118
- package/dist/assets/web/dist/assets/CostsPage-5pl3HB2F.js +0 -2
- package/dist/assets/web/dist/assets/RunRoutePage-CRvmCuXh.js +0 -12
- package/dist/assets/web/dist/assets/main-xLAsdkfB.css +0 -1
|
@@ -317,19 +317,13 @@ workspaceDir: ./workspaces
|
|
|
317
317
|
# maxRounds: 3 # review rounds per unit
|
|
318
318
|
# maxMinutes: 120 # the ship preset's wall-clock budget in minutes (the registry's default)
|
|
319
319
|
|
|
320
|
-
#
|
|
321
|
-
#
|
|
322
|
-
# inside the run's own execution container
|
|
323
|
-
#
|
|
324
|
-
#
|
|
325
|
-
#
|
|
326
|
-
#
|
|
327
|
-
# pi in the run's container; a preset without one (general, research,
|
|
328
|
-
# conductor) runs it as a child of the bot, with none of pi's own shell or
|
|
329
|
-
# file tools. A preset the registry does not know, or a value that is
|
|
330
|
-
# neither harness, fails the load by name.
|
|
331
|
-
# harness:
|
|
332
|
-
# coding: pi
|
|
320
|
+
# `harness` is a retired key (docs/reference/specs/harness-pi.md item 1). It
|
|
321
|
+
# moved the presets onto pi one at a time; every preset runs on pi now — the pi
|
|
322
|
+
# coding agent started inside the run's own execution container (or as a child
|
|
323
|
+
# of the bot for a preset without a workspace) with the run's model-proxy
|
|
324
|
+
# bearer as its only key — and there is no other loop. A block written during
|
|
325
|
+
# the series still loads when every value is `pi`; `native` names the deleted
|
|
326
|
+
# loop and fails the load by name, as does an unknown preset. Remove the block.
|
|
333
327
|
|
|
334
328
|
# What the harness writes into pi's per-run settings for every run on pi
|
|
335
329
|
# (docs/reference/specs/harness-pi.md item 4): pi's compaction thresholds, in
|
|
@@ -56,6 +56,14 @@ import {
|
|
|
56
56
|
textOfStoredRow,
|
|
57
57
|
} from "../../src/core/runLedger/sessionLog.ts";
|
|
58
58
|
import type { RunEvent } from "../../src/core/runEvents.ts";
|
|
59
|
+
import {
|
|
60
|
+
aggregateUsageByUser,
|
|
61
|
+
isRunUsage,
|
|
62
|
+
usageOfEvents,
|
|
63
|
+
type RunUsage,
|
|
64
|
+
type RunUsageReport,
|
|
65
|
+
type UsageRun,
|
|
66
|
+
} from "../../src/core/runUsage.ts";
|
|
59
67
|
import {
|
|
60
68
|
checkFence,
|
|
61
69
|
decideClaim,
|
|
@@ -1160,6 +1168,10 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
1160
1168
|
// The session a run was a range of (session-log item 7), so the sweep can
|
|
1161
1169
|
// tell which sessions still have a kept run; null for a record without one.
|
|
1162
1170
|
if (!columns.has("session_key")) this.sql.exec(`ALTER TABLE runs ADD COLUMN session_key TEXT`);
|
|
1171
|
+
// What the run cost in tokens (costs.md, cost by user): the record's `usage`
|
|
1172
|
+
// as JSON; NULL for a record written before the field existed, until the
|
|
1173
|
+
// by-user aggregate fills it in from the run's stored events.
|
|
1174
|
+
if (!columns.has("usage_json")) this.sql.exec(`ALTER TABLE runs ADD COLUMN usage_json TEXT`);
|
|
1163
1175
|
this.sql.exec(`
|
|
1164
1176
|
CREATE INDEX IF NOT EXISTS runs_channel_finished ON runs(channel_id, finished_at DESC, run_id DESC);
|
|
1165
1177
|
CREATE INDEX IF NOT EXISTS runs_visibility_finished ON runs(channel_visibility, finished_at DESC, run_id DESC);
|
|
@@ -1820,8 +1832,8 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
1820
1832
|
);
|
|
1821
1833
|
this.sql.exec(
|
|
1822
1834
|
`INSERT INTO runs (run_id, label, agent, model, channel_id, user_id, thread_key, channel_visibility, repo, started_at, finished_at, stored_at, status,
|
|
1823
|
-
event_count, stored_event_count, truncated, bytes, diagnosis_json, summary_json, session_key)
|
|
1824
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1835
|
+
event_count, stored_event_count, truncated, bytes, diagnosis_json, summary_json, session_key, usage_json)
|
|
1836
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1825
1837
|
ON CONFLICT(run_id) DO UPDATE SET
|
|
1826
1838
|
label = excluded.label, agent = excluded.agent, model = excluded.model, channel_id = excluded.channel_id,
|
|
1827
1839
|
user_id = excluded.user_id, thread_key = excluded.thread_key, channel_visibility = excluded.channel_visibility,
|
|
@@ -1829,7 +1841,8 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
1829
1841
|
finished_at = excluded.finished_at, stored_at = excluded.stored_at, status = excluded.status,
|
|
1830
1842
|
event_count = excluded.event_count, stored_event_count = excluded.stored_event_count, truncated = excluded.truncated,
|
|
1831
1843
|
bytes = excluded.bytes, diagnosis_json = excluded.diagnosis_json, summary_json = excluded.summary_json,
|
|
1832
|
-
session_key = excluded.session_key
|
|
1844
|
+
session_key = excluded.session_key,
|
|
1845
|
+
usage_json = COALESCE(excluded.usage_json, runs.usage_json)`,
|
|
1833
1846
|
stored.id,
|
|
1834
1847
|
stored.label ?? null,
|
|
1835
1848
|
stored.agent ?? null,
|
|
@@ -1850,6 +1863,7 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
1850
1863
|
JSON.stringify(stored.diagnosis),
|
|
1851
1864
|
JSON.stringify(summary),
|
|
1852
1865
|
stored.session?.key ?? null,
|
|
1866
|
+
stored.usage ? JSON.stringify(stored.usage) : null,
|
|
1853
1867
|
);
|
|
1854
1868
|
// The session's registry row learns its newest finish (session-log item
|
|
1855
1869
|
// 7); a record that reaches the store without a claim (the plain put
|
|
@@ -2053,6 +2067,83 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
2053
2067
|
return summary ? { ...summary, bytes: row.bytes } : null;
|
|
2054
2068
|
}
|
|
2055
2069
|
|
|
2070
|
+
/**
|
|
2071
|
+
* Who spent what, per UTC day (costs.md, cost by user), over the runs that
|
|
2072
|
+
* finished in [sinceMs, untilMs) and are inside the retention window. A row
|
|
2073
|
+
* written before `usage_json` existed is filled in here from its stored
|
|
2074
|
+
* `model.turn` events — up to USAGE_BACKFILL_PER_CALL a call, the rest
|
|
2075
|
+
* reported as `pending` — so the history heals as it is read, with no
|
|
2076
|
+
* operator step; a run without turns is written back as the zero usage so it
|
|
2077
|
+
* is not re-read. A child is billed to its parent's requester; a parent
|
|
2078
|
+
* outside the range is looked up by id.
|
|
2079
|
+
*/
|
|
2080
|
+
usageByUser(sinceMs: number, untilMs: number): RunUsageReport {
|
|
2081
|
+
const now = systemClock();
|
|
2082
|
+
const { policy } = this.policyState();
|
|
2083
|
+
const cutoff = now - policy.retentionDays * 86_400_000;
|
|
2084
|
+
const rows = this.sql
|
|
2085
|
+
.exec<UsageRow>(
|
|
2086
|
+
`SELECT run_id, user_id, started_at, finished_at, usage_json, summary_json FROM runs
|
|
2087
|
+
WHERE finished_at >= ? AND finished_at < ? ORDER BY finished_at ASC, run_id ASC`,
|
|
2088
|
+
Math.max(sinceMs, cutoff),
|
|
2089
|
+
untilMs,
|
|
2090
|
+
)
|
|
2091
|
+
.toArray();
|
|
2092
|
+
let backfilled = 0;
|
|
2093
|
+
const runs: UsageRun[] = rows.map((row) => {
|
|
2094
|
+
let usage = parseUsageJson(row.usage_json);
|
|
2095
|
+
if (usage === undefined && backfilled < USAGE_BACKFILL_PER_CALL) {
|
|
2096
|
+
usage = usageOfEvents(this.modelTurnEvents(row.run_id));
|
|
2097
|
+
this.sql.exec(`UPDATE runs SET usage_json = ? WHERE run_id = ?`, JSON.stringify(usage), row.run_id);
|
|
2098
|
+
backfilled += 1;
|
|
2099
|
+
}
|
|
2100
|
+
const who = identityOfSummary(row.summary_json);
|
|
2101
|
+
return {
|
|
2102
|
+
id: row.run_id,
|
|
2103
|
+
userId: row.user_id,
|
|
2104
|
+
...(who.userName ? { userName: who.userName } : {}),
|
|
2105
|
+
...(who.parentRunId ? { parentRunId: who.parentRunId } : {}),
|
|
2106
|
+
startedAt: row.started_at,
|
|
2107
|
+
finishedAt: row.finished_at,
|
|
2108
|
+
...(usage ? { usage } : {}),
|
|
2109
|
+
};
|
|
2110
|
+
});
|
|
2111
|
+
const lookupParent = (id: string) => {
|
|
2112
|
+
const p = this.sql
|
|
2113
|
+
.exec<{ user_id: string; summary_json: string }>(`SELECT user_id, summary_json FROM runs WHERE run_id = ?`, id)
|
|
2114
|
+
.toArray()[0];
|
|
2115
|
+
if (!p) return undefined;
|
|
2116
|
+
const who = identityOfSummary(p.summary_json);
|
|
2117
|
+
return { userId: p.user_id, ...(who.userName ? { userName: who.userName } : {}) };
|
|
2118
|
+
};
|
|
2119
|
+
const { rows: out, pending } = aggregateUsageByUser(runs, lookupParent);
|
|
2120
|
+
const earliest = this.sql
|
|
2121
|
+
.exec<{ m: number | null }>(`SELECT MIN(finished_at) AS m FROM runs WHERE finished_at >= ?`, cutoff)
|
|
2122
|
+
.one().m;
|
|
2123
|
+
return {
|
|
2124
|
+
rows: out,
|
|
2125
|
+
pending,
|
|
2126
|
+
...(earliest !== null ? { earliestFinishedAt: earliest } : {}),
|
|
2127
|
+
retentionDays: policy.retentionDays,
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
/** The stored `model.turn` span ends of one run — the rows a backfill prices. */
|
|
2132
|
+
private modelTurnEvents(runId: string): RunEvent[] {
|
|
2133
|
+
const out: RunEvent[] = [];
|
|
2134
|
+
for (const r of this.sql
|
|
2135
|
+
.exec<{ json: string }>(`SELECT json FROM run_events WHERE run_id = ? AND json LIKE '%"model.turn"%'`, runId)
|
|
2136
|
+
.toArray()) {
|
|
2137
|
+
try {
|
|
2138
|
+
const e = JSON.parse(r.json) as RunEvent;
|
|
2139
|
+
if (e && e.type === "span_end") out.push(e);
|
|
2140
|
+
} catch {
|
|
2141
|
+
// an unparsable event row prices nothing
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
return out;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2056
2147
|
/**
|
|
2057
2148
|
* Newest first (finished_at desc, run_id desc) among the rows the policy
|
|
2058
2149
|
* keeps, filtered, capped at RUN_LIST_MAX_LIMIT. The `before`/`beforeId`
|
|
@@ -2181,6 +2272,43 @@ function parseEventRows(rows: readonly EventRow[]): StoredRunEvent[] {
|
|
|
2181
2272
|
}
|
|
2182
2273
|
|
|
2183
2274
|
/** The stored summary (record minus events); null when the row is unreadable. */
|
|
2275
|
+
/** How many usage-less rows one by-user read prices from their events before
|
|
2276
|
+
* reporting the rest as pending: a page load stays quick while the history heals. */
|
|
2277
|
+
const USAGE_BACKFILL_PER_CALL = 200;
|
|
2278
|
+
|
|
2279
|
+
type UsageRow = {
|
|
2280
|
+
run_id: string;
|
|
2281
|
+
user_id: string;
|
|
2282
|
+
started_at: number;
|
|
2283
|
+
finished_at: number;
|
|
2284
|
+
usage_json: string | null;
|
|
2285
|
+
summary_json: string;
|
|
2286
|
+
};
|
|
2287
|
+
|
|
2288
|
+
/** A stored `usage_json`, or undefined when absent or unreadable (then it is recomputed). */
|
|
2289
|
+
function parseUsageJson(raw: string | null): RunUsage | undefined {
|
|
2290
|
+
if (raw === null) return undefined;
|
|
2291
|
+
try {
|
|
2292
|
+
const parsed: unknown = JSON.parse(raw);
|
|
2293
|
+
return isRunUsage(parsed) ? parsed : undefined;
|
|
2294
|
+
} catch {
|
|
2295
|
+
return undefined;
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
/** The two identity fields the by-user aggregate needs off a summary, read leniently. */
|
|
2300
|
+
function identityOfSummary(raw: string): { userName?: string; parentRunId?: string } {
|
|
2301
|
+
try {
|
|
2302
|
+
const s = JSON.parse(raw) as Record<string, unknown>;
|
|
2303
|
+
return {
|
|
2304
|
+
...(typeof s.userName === "string" && s.userName ? { userName: s.userName } : {}),
|
|
2305
|
+
...(typeof s.parentRunId === "string" && s.parentRunId ? { parentRunId: s.parentRunId } : {}),
|
|
2306
|
+
};
|
|
2307
|
+
} catch {
|
|
2308
|
+
return {};
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2184
2312
|
function parseSummary(row: RunRow): Omit<RunRecord, "events"> | null {
|
|
2185
2313
|
try {
|
|
2186
2314
|
const parsed: unknown = JSON.parse(row.summary_json);
|
|
@@ -2237,6 +2365,23 @@ function parseRunPut(body: unknown): Validated<{ storeKey: string; record: RunRe
|
|
|
2237
2365
|
}
|
|
2238
2366
|
|
|
2239
2367
|
/** `{storeKey, id}` — the body of /runs/get, /runs/summary and /runs/delete, and the base of /runs/events. */
|
|
2368
|
+
/** At most a year of runs a call — the page asks for 90 days at most. */
|
|
2369
|
+
const USAGE_QUERY_MAX_SPAN_MS = 366 * 86_400_000;
|
|
2370
|
+
|
|
2371
|
+
function parseRunUsageQuery(body: unknown): Validated<{ storeKey: string; sinceMs: number; untilMs: number }> {
|
|
2372
|
+
if (typeof body !== "object" || body === null) return invalid("body must be a JSON object");
|
|
2373
|
+
const b = body as Record<string, unknown>;
|
|
2374
|
+
const key = parseStoreKey(b);
|
|
2375
|
+
if (!key.ok) return key;
|
|
2376
|
+
const { sinceMs, untilMs } = b;
|
|
2377
|
+
if (typeof sinceMs !== "number" || !Number.isFinite(sinceMs) || sinceMs < 0)
|
|
2378
|
+
return invalid("sinceMs must be a non-negative number");
|
|
2379
|
+
if (typeof untilMs !== "number" || !Number.isFinite(untilMs) || untilMs <= sinceMs)
|
|
2380
|
+
return invalid("untilMs must be a number after sinceMs");
|
|
2381
|
+
if (untilMs - sinceMs > USAGE_QUERY_MAX_SPAN_MS) return invalid("the range may span at most 366 days");
|
|
2382
|
+
return { ok: true, value: { storeKey: key.value, sinceMs, untilMs } };
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2240
2385
|
function parseRunTarget(body: unknown): Validated<{ storeKey: string; id: string }> {
|
|
2241
2386
|
if (typeof body !== "object" || body === null) return invalid("body must be a JSON object");
|
|
2242
2387
|
const b = body as Record<string, unknown>;
|
|
@@ -3504,6 +3649,14 @@ async function handleRuns(pathname: string, body: unknown, env: Env): Promise<Re
|
|
|
3504
3649
|
);
|
|
3505
3650
|
return json(result ?? { events: null });
|
|
3506
3651
|
}
|
|
3652
|
+
if (pathname === "/runs/usage-by-user") {
|
|
3653
|
+
const parsed = parseRunUsageQuery(body);
|
|
3654
|
+
if (!parsed.ok) return json({ error: parsed.error }, 400);
|
|
3655
|
+
const { storeKey, sinceMs, untilMs } = parsed.value;
|
|
3656
|
+
const report = await stub(storeKey).usageByUser(sinceMs, untilMs);
|
|
3657
|
+
console.log(`[runs/usage-by-user] ${storeKey} -> ${report.rows.length} rows, ${report.pending} pending`);
|
|
3658
|
+
return json(report);
|
|
3659
|
+
}
|
|
3507
3660
|
// /runs/delete
|
|
3508
3661
|
const parsed = parseRunTarget(body);
|
|
3509
3662
|
if (!parsed.ok) return json({ error: parsed.error }, 400);
|
|
@@ -3526,6 +3679,7 @@ const ROUTES = new Set([
|
|
|
3526
3679
|
"/runs/summary",
|
|
3527
3680
|
"/runs/list",
|
|
3528
3681
|
"/runs/events",
|
|
3682
|
+
"/runs/usage-by-user",
|
|
3529
3683
|
"/runs/delete",
|
|
3530
3684
|
...LEDGER_ROUTES,
|
|
3531
3685
|
]);
|
|
@@ -88,12 +88,16 @@ import { decideWorktree, parseReuse, type WorktreeFacts } from "../../src/execut
|
|
|
88
88
|
import {
|
|
89
89
|
boundByFor,
|
|
90
90
|
parseOwnPr,
|
|
91
|
+
parsePushed,
|
|
91
92
|
parseRefByDefault,
|
|
92
93
|
rebindPlan,
|
|
93
94
|
rebindRefused,
|
|
94
95
|
rebindVerdict,
|
|
96
|
+
rememberOwnBranches,
|
|
95
97
|
type BoundBy,
|
|
98
|
+
type OwnBranch,
|
|
96
99
|
type OwnPr,
|
|
100
|
+
type PushedBranch,
|
|
97
101
|
type Rebound,
|
|
98
102
|
type RebindRefused,
|
|
99
103
|
type RebindTreeFacts,
|
|
@@ -994,6 +998,13 @@ interface ThreadBinding {
|
|
|
994
998
|
* to, onto the branch the thread's own run opened a pull request on. Set,
|
|
995
999
|
* the binding never moves again. */
|
|
996
1000
|
rebound?: Rebound;
|
|
1001
|
+
/** The branches the thread's own runs pushed and the pull requests they
|
|
1002
|
+
* head, as each run's release handed them over (`/detach` `pushed`, item
|
|
1003
|
+
* 16a) — written before any eviction decision, so the fact outlives the
|
|
1004
|
+
* tree: a clean tree is released the moment a run ends, and this is what
|
|
1005
|
+
* lets the follow-up's attach move onto the branch and recreate the tree
|
|
1006
|
+
* there. Newest last; the oldest fall off past `OWN_BRANCHES_MAX`. */
|
|
1007
|
+
ownBranches?: OwnBranch[];
|
|
997
1008
|
/** Allocated OS user (worker2..worker17); "" once evicted (pool released). */
|
|
998
1009
|
user: string;
|
|
999
1010
|
worktreePath: string;
|
|
@@ -1092,6 +1103,15 @@ interface RefHintReason {
|
|
|
1092
1103
|
/** The body every bot always sent: a hint with no reason attached. */
|
|
1093
1104
|
const NO_REF_HINT_REASON: RefHintReason = { ownPr: null, refByDefault: false };
|
|
1094
1105
|
|
|
1106
|
+
/** What the rebind decided under the mutex (item 16): nothing to move (the
|
|
1107
|
+
* row is already on the branch), a named refusal, or the move — written on
|
|
1108
|
+
* the row, whether a checkout in the live tree or a ref the attach will
|
|
1109
|
+
* recreate the tree at. */
|
|
1110
|
+
type RebindOutcome =
|
|
1111
|
+
| { kind: "none"; binding: ThreadBinding }
|
|
1112
|
+
| { kind: "refuse"; refused: RebindRefused }
|
|
1113
|
+
| { kind: "rebound"; moved: ThreadBinding; rebound: Rebound };
|
|
1114
|
+
|
|
1095
1115
|
/** Result of one /op test/build execution. `ok` is the command's
|
|
1096
1116
|
* verdict — a failing test run is a RESULT with ok:false, never an error. */
|
|
1097
1117
|
interface OpRunOk {
|
|
@@ -4358,6 +4378,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4358
4378
|
? {
|
|
4359
4379
|
...(existing.boundBy !== undefined ? { boundBy: existing.boundBy } : {}),
|
|
4360
4380
|
...(existing.rebound !== undefined ? { rebound: existing.rebound } : {}),
|
|
4381
|
+
...(existing.ownBranches !== undefined ? { ownBranches: existing.ownBranches } : {}),
|
|
4361
4382
|
}
|
|
4362
4383
|
: { boundBy }),
|
|
4363
4384
|
};
|
|
@@ -4497,6 +4518,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4497
4518
|
reason.ownPr,
|
|
4498
4519
|
reuse,
|
|
4499
4520
|
facts.defaultRef,
|
|
4521
|
+
slug,
|
|
4500
4522
|
);
|
|
4501
4523
|
if ("error" in rebind) return rebind;
|
|
4502
4524
|
const prior = rebind.binding;
|
|
@@ -4579,19 +4601,26 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4579
4601
|
ownPr: OwnPr | null,
|
|
4580
4602
|
reuse: boolean,
|
|
4581
4603
|
defaultRef: string,
|
|
4604
|
+
slug: string,
|
|
4582
4605
|
): Promise<{ binding: ThreadBinding | undefined; rebound?: Rebound; rebindRefused?: RebindRefused } | ThreadErr> {
|
|
4583
4606
|
const plan = rebindPlan({ ownPr, reuse, binding: prior, defaultRef });
|
|
4584
4607
|
if (plan.kind === "none" || prior === undefined) return { binding: prior };
|
|
4585
4608
|
if (plan.kind === "refuse") return { binding: prior, rebindRefused: plan.refused };
|
|
4586
4609
|
const key = threadBindingKey(prior.threadKey);
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4610
|
+
// A tree that is gone is recreated at the branch by the attach (item 16):
|
|
4611
|
+
// the mirror must hold the branch first, and a branch pushed since the
|
|
4612
|
+
// last refresh cycle is fetched for — with a token minted here, before
|
|
4613
|
+
// the mutex, like the attach's own recovery fetch. Only when the binding
|
|
4614
|
+
// allows a recreate at all: an evicted tree, or a live one whose runs
|
|
4615
|
+
// pushed the branch (the tree may turn out missing).
|
|
4616
|
+
const fetchToken =
|
|
4617
|
+
(plan.kind === "recreate" || plan.own) && githubAppConfigured(this.env)
|
|
4618
|
+
? ((await mintRepoScopedToken(this.env, slug).catch(() => null))?.token ?? null)
|
|
4619
|
+
: null;
|
|
4620
|
+
let outcome: RebindOutcome;
|
|
4592
4621
|
try {
|
|
4593
4622
|
outcome = (
|
|
4594
|
-
await this.withMirrorLock(async (): Promise<
|
|
4623
|
+
await this.withMirrorLock(async (): Promise<RebindOutcome> => {
|
|
4595
4624
|
// The binding as it stands NOW, under the mutex — not the snapshot
|
|
4596
4625
|
// the plan read before the wait: the move is judged on, and written
|
|
4597
4626
|
// over, the row's current fields (its last attach time, its
|
|
@@ -4602,6 +4631,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4602
4631
|
const again = rebindPlan({ ownPr, reuse, binding: current, defaultRef });
|
|
4603
4632
|
if (again.kind === "none") return { kind: "none", binding: current };
|
|
4604
4633
|
if (again.kind === "refuse") return again;
|
|
4634
|
+
if (again.kind === "recreate") return this.recreateAtOwnBranch(current, again, fetchToken);
|
|
4605
4635
|
const wt = current.worktreePath;
|
|
4606
4636
|
// The facts, as the thread user in the thread's own tree: is the
|
|
4607
4637
|
// branch a local branch here (the physical trace of this thread's
|
|
@@ -4612,7 +4642,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4612
4642
|
const branch = await this.threadRun(
|
|
4613
4643
|
current.user,
|
|
4614
4644
|
wt,
|
|
4615
|
-
`git rev-parse --verify --quiet ${shellQuote(`refs/heads/${
|
|
4645
|
+
`git rev-parse --verify --quiet ${shellQuote(`refs/heads/${again.to}`)}`,
|
|
4616
4646
|
DEFAULT_EXEC_TIMEOUT_MS,
|
|
4617
4647
|
);
|
|
4618
4648
|
tree.branchExists = branch.exitCode === 0;
|
|
@@ -4625,13 +4655,16 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4625
4655
|
tree.readable = status.exitCode === 0;
|
|
4626
4656
|
if (tree.readable) tree.dirty = status.stdout.trim() !== "";
|
|
4627
4657
|
}
|
|
4628
|
-
|
|
4658
|
+
// Judged on the re-read plan: it carries whether the branch is the
|
|
4659
|
+
// thread's own, which the pre-lock plan lacks when it was a recreate.
|
|
4660
|
+
const verdict = rebindVerdict(again, tree);
|
|
4661
|
+
if (verdict.kind === "recreate") return this.recreateAtOwnBranch(current, again, fetchToken);
|
|
4629
4662
|
if (verdict.kind !== "rebind") return verdict;
|
|
4630
4663
|
const startedAt = systemClock();
|
|
4631
4664
|
const checkout = await this.threadRun(
|
|
4632
4665
|
current.user,
|
|
4633
4666
|
wt,
|
|
4634
|
-
`git checkout --quiet ${shellQuote(
|
|
4667
|
+
`git checkout --quiet ${shellQuote(again.to)}`,
|
|
4635
4668
|
DEFAULT_EXEC_TIMEOUT_MS,
|
|
4636
4669
|
);
|
|
4637
4670
|
this.stepTrace.getStore()?.record("rebind-checkout", {
|
|
@@ -4643,15 +4676,15 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4643
4676
|
if (checkout.exitCode !== 0) {
|
|
4644
4677
|
const detail =
|
|
4645
4678
|
checkout.stderr.trim().split("\n")[0]?.slice(0, 200) || `git checkout exited ${checkout.exitCode}`;
|
|
4646
|
-
return { kind: "refuse", refused: rebindRefused(
|
|
4679
|
+
return { kind: "refuse", refused: rebindRefused(again, "checkout-failed", detail) };
|
|
4647
4680
|
}
|
|
4648
4681
|
const rebound: Rebound = {
|
|
4649
4682
|
from: current.ref,
|
|
4650
|
-
to:
|
|
4651
|
-
pr:
|
|
4683
|
+
to: again.to,
|
|
4684
|
+
pr: again.pr,
|
|
4652
4685
|
at: new Date(systemClock()).toISOString(),
|
|
4653
4686
|
};
|
|
4654
|
-
const moved: ThreadBinding = { ...current, ref:
|
|
4687
|
+
const moved: ThreadBinding = { ...current, ref: again.to, rebound };
|
|
4655
4688
|
await this.ctx.storage.put(key, moved);
|
|
4656
4689
|
return { kind: "rebound", moved, rebound };
|
|
4657
4690
|
}, ATTACH_MUTEX_WAIT_MS)
|
|
@@ -4677,6 +4710,77 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4677
4710
|
return { binding: moved, rebound };
|
|
4678
4711
|
}
|
|
4679
4712
|
|
|
4713
|
+
/** The move when the thread's tree is gone (item 16): the binding's ref
|
|
4714
|
+
* becomes the branch its own run pushed, and the attach that follows
|
|
4715
|
+
* recreates the tree at it — `ensureThreadWorktree` finds no tree and
|
|
4716
|
+
* clones the branch from the mirror. So the mirror must hold the branch:
|
|
4717
|
+
* one pushed since the last refresh cycle is fetched for here, under the
|
|
4718
|
+
* mutex; a branch still missing after the fetch (deleted after a merge, or
|
|
4719
|
+
* never pushed) is a `branch-absent` refusal and the binding stands — the
|
|
4720
|
+
* attach then goes on at the ref it had, never `unknown-ref`. Nothing on
|
|
4721
|
+
* disk is touched: no checkout (there is no tree), no clone (the attach's). */
|
|
4722
|
+
private async recreateAtOwnBranch(
|
|
4723
|
+
current: ThreadBinding,
|
|
4724
|
+
plan: { to: string; pr: number },
|
|
4725
|
+
fetchToken: string | null,
|
|
4726
|
+
): Promise<RebindOutcome> {
|
|
4727
|
+
let present = await this.refExists(plan.to);
|
|
4728
|
+
if (!present) {
|
|
4729
|
+
// A fetch that fails — GitHub unreachable, no token for a private repo —
|
|
4730
|
+
// is not the attach's failure: the branch is then simply not in the
|
|
4731
|
+
// mirror, and the refusal below names it, so the binding stands and the
|
|
4732
|
+
// run goes on at the ref it had instead of the attach ending 500.
|
|
4733
|
+
try {
|
|
4734
|
+
await this.gitWithCred(
|
|
4735
|
+
fetchToken,
|
|
4736
|
+
["-C", MIRROR_DIR, "fetch", "--prune", "origin"],
|
|
4737
|
+
"fetch",
|
|
4738
|
+
GIT_NETWORK_TIMEOUT_MS,
|
|
4739
|
+
);
|
|
4740
|
+
} catch (err) {
|
|
4741
|
+
console.log(
|
|
4742
|
+
`attach ${current.threadKey}: the fetch for ${plan.to} failed (${errMsg(err)}) — the rebind is refused, the attach goes on`,
|
|
4743
|
+
);
|
|
4744
|
+
}
|
|
4745
|
+
present = await this.refExists(plan.to);
|
|
4746
|
+
}
|
|
4747
|
+
if (!present) {
|
|
4748
|
+
return {
|
|
4749
|
+
kind: "refuse",
|
|
4750
|
+
refused: rebindRefused(
|
|
4751
|
+
plan,
|
|
4752
|
+
"branch-absent",
|
|
4753
|
+
`the thread's worktree is gone and the mirror does not hold ${JSON.stringify(plan.to)} (deleted after a merge, or never pushed); the tree cannot be recreated at it`,
|
|
4754
|
+
),
|
|
4755
|
+
};
|
|
4756
|
+
}
|
|
4757
|
+
const rebound: Rebound = {
|
|
4758
|
+
from: current.ref,
|
|
4759
|
+
to: plan.to,
|
|
4760
|
+
pr: plan.pr,
|
|
4761
|
+
at: new Date(systemClock()).toISOString(),
|
|
4762
|
+
};
|
|
4763
|
+
const moved: ThreadBinding = { ...current, ref: plan.to, rebound };
|
|
4764
|
+
await this.ctx.storage.put(threadBindingKey(current.threadKey), moved);
|
|
4765
|
+
return { kind: "rebound", moved, rebound };
|
|
4766
|
+
}
|
|
4767
|
+
|
|
4768
|
+
/** The release's word on what the run pushed (`/detach` `pushed`, item
|
|
4769
|
+
* 16a), remembered on the binding BEFORE the detach decides anything: a
|
|
4770
|
+
* clean tree is evicted at once, a dirty one kept, an already-evicted
|
|
4771
|
+
* binding answered as such — and in every case the fact must outlive the
|
|
4772
|
+
* tree, since it is what a follow-up's rebind onto the thread's own pull
|
|
4773
|
+
* request branch reads once the tree is gone. No binding → nothing to
|
|
4774
|
+
* remember on (the detach answers its 404 next). */
|
|
4775
|
+
private async rememberOwnBranches(threadKey: string, pushed: readonly PushedBranch[]): Promise<void> {
|
|
4776
|
+
const binding = await this.ctx.storage.get<ThreadBinding>(threadBindingKey(threadKey));
|
|
4777
|
+
if (!binding) return;
|
|
4778
|
+
await this.ctx.storage.put(threadBindingKey(threadKey), {
|
|
4779
|
+
...binding,
|
|
4780
|
+
ownBranches: rememberOwnBranches(binding.ownBranches, pushed, new Date(systemClock()).toISOString()),
|
|
4781
|
+
} satisfies ThreadBinding);
|
|
4782
|
+
}
|
|
4783
|
+
|
|
4680
4784
|
/** The second half of an attach, past disk admission: mint, fetch + clone
|
|
4681
4785
|
* under the mirror lock, deps, credentials, the binding write. Split from
|
|
4682
4786
|
* `attachThreadBody` only so the admission's commitment is released on every
|
|
@@ -5313,14 +5417,13 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5313
5417
|
"deps-seed",
|
|
5314
5418
|
REFRESH_BUILD_TIMEOUT_MS,
|
|
5315
5419
|
);
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
}
|
|
5420
|
+
await this.swapMutableCaches(
|
|
5421
|
+
seedNm,
|
|
5422
|
+
`${scratch}/node_modules`,
|
|
5423
|
+
BUILD_USER,
|
|
5424
|
+
parsed.mutableListing,
|
|
5425
|
+
"deps-seed-swap",
|
|
5426
|
+
);
|
|
5324
5427
|
console.log(`deps: ${key.slice(0, 8)} seeded from ${seed.slice(0, 8)} (${parsed.mech})`);
|
|
5325
5428
|
}
|
|
5326
5429
|
// Unprivileged and token-free, in a tree only this attempt
|
|
@@ -5423,16 +5526,42 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5423
5526
|
"deps-materialize",
|
|
5424
5527
|
REFRESH_BUILD_TIMEOUT_MS,
|
|
5425
5528
|
);
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5529
|
+
await this.swapMutableCaches(
|
|
5530
|
+
entryNodeModules ?? `${CHECKOUT_DIR}/node_modules`,
|
|
5531
|
+
`${tree}/node_modules`,
|
|
5532
|
+
user,
|
|
5533
|
+
parsed.mutableListing,
|
|
5534
|
+
"deps-mutable-swap",
|
|
5535
|
+
);
|
|
5536
|
+
return parsed.mech;
|
|
5537
|
+
}
|
|
5538
|
+
|
|
5539
|
+
/** The tool-cache swap for a freshly hardlinked node_modules: the listing
|
|
5540
|
+
* through `mutableCachePaths`, every swap in one fork
|
|
5541
|
+
* (`mutableCacheSwapScript`). A path the source has no counterpart for is
|
|
5542
|
+
* a tree-private entry the script leaves in place and names on a
|
|
5543
|
+
* `skipped=` line; the log carries those names so a refresh that read warm
|
|
5544
|
+
* still says what it left alone (the failure path already quotes the
|
|
5545
|
+
* script's stdout). */
|
|
5546
|
+
private async swapMutableCaches(
|
|
5547
|
+
srcNodeModules: string,
|
|
5548
|
+
dstNodeModules: string,
|
|
5549
|
+
user: string,
|
|
5550
|
+
mutableListing: string[],
|
|
5551
|
+
step: "deps-mutable-swap" | "deps-seed-swap",
|
|
5552
|
+
): Promise<void> {
|
|
5553
|
+
const paths = mutableCachePaths(dstNodeModules, mutableListing);
|
|
5554
|
+
if (paths.length === 0) return;
|
|
5555
|
+
const swapped = await this.runDepScript(
|
|
5556
|
+
mutableCacheSwapScript(srcNodeModules, dstNodeModules, user, paths),
|
|
5557
|
+
step,
|
|
5558
|
+
GIT_NETWORK_TIMEOUT_MS,
|
|
5559
|
+
);
|
|
5560
|
+
if (swapped.skipped.length > 0) {
|
|
5561
|
+
console.log(
|
|
5562
|
+
`deps: ${step} left ${swapped.skipped.length} tree-private node_modules entr${swapped.skipped.length === 1 ? "y" : "ies"} with no store counterpart in place: ${swapped.skipped.join(", ")}`,
|
|
5433
5563
|
);
|
|
5434
5564
|
}
|
|
5435
|
-
return parsed.mech;
|
|
5436
5565
|
}
|
|
5437
5566
|
|
|
5438
5567
|
/** Adopt a checkout that already holds node_modules but whose key has no
|
|
@@ -5910,7 +6039,10 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
5910
6039
|
async detachThread(
|
|
5911
6040
|
threadKey: string,
|
|
5912
6041
|
force: boolean,
|
|
6042
|
+
/** What the ending run pushed (item 16a): remembered on the binding first, whatever the detach then decides. */
|
|
6043
|
+
pushed: readonly PushedBranch[] = [],
|
|
5913
6044
|
): Promise<{ released: boolean; reason?: string; user?: string } | ThreadErr> {
|
|
6045
|
+
if (pushed.length > 0) await this.rememberOwnBranches(threadKey, pushed);
|
|
5914
6046
|
const binding = await this.ctx.storage.get<ThreadBinding>(threadBindingKey(threadKey));
|
|
5915
6047
|
if (!binding) return { error: `no-binding: ${threadKey} has never attached to this resident`, status: 404 };
|
|
5916
6048
|
if (binding.evicted || !binding.user) return { released: false, reason: "already-evicted" };
|
|
@@ -7785,7 +7917,15 @@ async function handleAttach(env: Env, body: Record<string, unknown>, traceparent
|
|
|
7785
7917
|
async function handleDetach(env: Env, body: Record<string, unknown>): Promise<Response> {
|
|
7786
7918
|
const ctx = await resolveThreadRoute(env, body);
|
|
7787
7919
|
if (ctx instanceof Response) return ctx;
|
|
7788
|
-
|
|
7920
|
+
// What the run pushed (item 16a), each ref through the one ref pattern
|
|
7921
|
+
// before it can be stored or become a git argument.
|
|
7922
|
+
const pushed = parsePushed(body.pushed);
|
|
7923
|
+
if ("error" in pushed) return json({ error: pushed.error }, 400);
|
|
7924
|
+
for (const entry of pushed.pushed) {
|
|
7925
|
+
const ref = parseRef(entry.ref, "pushed[].ref");
|
|
7926
|
+
if ("error" in ref) return json({ error: ref.error }, 400);
|
|
7927
|
+
}
|
|
7928
|
+
const result = await ctx.stub.detachThread(ctx.threadKey, body.force === true, pushed.pushed);
|
|
7789
7929
|
if ("error" in result) return threadErrResponse(result);
|
|
7790
7930
|
return json(result);
|
|
7791
7931
|
}
|
|
@@ -13,19 +13,21 @@
|
|
|
13
13
|
FROM --platform=linux/amd64 docker.io/library/node:24.21.0-slim AS node
|
|
14
14
|
|
|
15
15
|
# The tag MUST match the @cloudflare/sandbox version in package.json exactly
|
|
16
|
-
# (no 'latest' tag exists) — bump both together.
|
|
17
|
-
|
|
16
|
+
# (no 'latest' tag exists on the @next line) — bump both together. The same
|
|
17
|
+
# base as the resident image: its container server is compiled with a Bun
|
|
18
|
+
# whose directory watcher no longer dies on an entry removed under it, which
|
|
19
|
+
# ended the 0.12.9 line's server mid-command (docs/reference/specs/execution.md
|
|
20
|
+
# item 21).
|
|
21
|
+
FROM docker.io/cloudflare/sandbox:0.13.0-next.751.1
|
|
18
22
|
USER root
|
|
19
|
-
# The SDK's container server kills any command after COMMAND_TIMEOUT_MS
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
# through the normal result path. This sits 40s above the 20-min ceiling so it
|
|
28
|
-
# never wins the race. Bump alongside BASH_TIMEOUT_MAX_MS
|
|
23
|
+
# The SDK's container server kills any command after COMMAND_TIMEOUT_MS
|
|
24
|
+
# (unset → no limit). Pure BACKSTOP behind two closer ones: the real
|
|
25
|
+
# per-command limit is coreutils `timeout` in worker.ts (280s default,
|
|
26
|
+
# raisable per call via the bash tool's timeoutMs up to the 20-min ceiling
|
|
27
|
+
# BASH_TIMEOUT_MAX_MS), which yields a genuine exit 124 through the normal
|
|
28
|
+
# result path, and worker.ts launches each process with the SDK's own
|
|
29
|
+
# per-process `timeout` a margin above that. This sits 40s above the 20-min
|
|
30
|
+
# ceiling so it never wins the race. Bump alongside BASH_TIMEOUT_MAX_MS
|
|
29
31
|
# (src/execution/bashTimeout.ts); changing it requires a container rebuild +
|
|
30
32
|
# redeploy.
|
|
31
33
|
ENV COMMAND_TIMEOUT_MS=1240000
|
|
@@ -53,8 +55,8 @@ RUN apt-get update \
|
|
|
53
55
|
# execs the real client. src/deploy/sandboxDocker.test.ts holds its shape.
|
|
54
56
|
COPY --chmod=0755 docker-wrapper.sh /usr/local/bin/docker
|
|
55
57
|
|
|
56
|
-
# Node 24, not the base's. The
|
|
57
|
-
# (copied from node:22-slim: the binary at /usr/local/bin/node, npm and
|
|
58
|
+
# Node 24, not the base's. The 0.13.0-next base ships Node 22.23.2 like the
|
|
59
|
+
# 0.12.9 one (copied from node:22-slim: the binary at /usr/local/bin/node, npm and
|
|
58
60
|
# corepack under /usr/local/lib/node_modules, npm/npx symlinked into it), while
|
|
59
61
|
# this repository builds and tests on Node 24 (.nvmrc, CI) and its published
|
|
60
62
|
# CLI requires `>=24`. A run that exercised the suite inside this image read a
|
|
@@ -174,9 +176,11 @@ RUN npm install -g @earendil-works/pi-coding-agent@0.85.1 \
|
|
|
174
176
|
&& pi --version | grep -qx '0.85.1' \
|
|
175
177
|
&& pi --help | grep -q -- '--mode <mode>'
|
|
176
178
|
|
|
177
|
-
#
|
|
178
|
-
#
|
|
179
|
-
# the server
|
|
179
|
+
# The container outlives a crash of the SDK's container server (the
|
|
180
|
+
# supervisor's header says why). The base image's ENTRYPOINT is tini running
|
|
181
|
+
# the server, so one uncaught exception there ended the container; here tini
|
|
182
|
+
# stays PID 1 — it reaps orphans and forwards the platform's signals — and
|
|
183
|
+
# runs the supervisor, which starts the server again when it exits.
|
|
180
184
|
# src/deploy/sandboxRuntimeSupervisor.test.ts holds this pair of lines.
|
|
181
185
|
COPY --chmod=0755 runtime-supervisor.sh /usr/local/bin/sandbox-runtime-supervisor
|
|
182
|
-
ENTRYPOINT ["/usr/local/bin/sandbox-runtime-supervisor"]
|
|
186
|
+
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/sandbox-runtime-supervisor"]
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"secrets": "npm --prefix ../.. run --silent cli -- deploy secrets sandbox"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@cloudflare/sandbox": "0.
|
|
19
|
+
"@cloudflare/sandbox": "0.13.0-next.751.1"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@cloudflare/workers-types": "^5.20260904.1",
|