@coreplane/switchboard 1.240.0 → 1.241.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 +26 -7
- package/dist/assets/deploy/cloudflare/worker.ts +77 -18
- package/dist/assets/deploy/cloudflare-resident/worker.ts +1 -0
- package/dist/assets/deploy/secrets.manifest.json +6 -0
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +27 -8
- package/dist/assets/src/core/authz/actor.ts +5 -0
- package/dist/assets/src/core/authz/grants.ts +20 -9
- package/dist/assets/src/core/coordinator/contract.ts +37 -0
- package/dist/assets/src/core/coordinator/driver.ts +17 -1
- package/dist/assets/src/core/coordinator/instancesRoute.ts +72 -0
- package/dist/assets/src/core/costs.ts +10 -5
- package/dist/assets/src/core/ship/coordinator.ts +22 -7
- package/dist/assets/src/core/trace/attrs.ts +1 -1
- package/dist/assets/src/execution/bindingPurge.ts +2 -2
- package/dist/assets/web/dist/.vite/manifest.json +422 -420
- package/dist/assets/web/dist/assets/AppShell-DEy5jRuS.js +1 -0
- package/dist/assets/web/dist/assets/{CostsPage-BD3w5j_S.js → CostsPage-BuHD-Bv7.js} +2 -2
- package/dist/assets/web/dist/assets/{DeliveryPage-BtvzKA1m.js → DeliveryPage-BGpPr5xr.js} +1 -1
- package/dist/assets/web/dist/assets/HomePage-dKBvWh-E.js +2 -0
- package/dist/assets/web/dist/assets/{NotFoundPage-DwOX1HaZ.js → NotFoundPage-DtmQM2gD.js} +1 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-CrEAWx5b.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-CUAOh5d9.js → ResidentDetailPage-C-48fxat.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-B3KUlYy2.js → ResidentsIndexPage-CBBIjPOa.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-B6vxjcBI.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-CQjd3Cz-.js +6 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-Cy9G6D0p.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-BnkvCBrM.js → RunsTabs-CtNGNnhN.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-C1y9jgAk.js → ScheduledPage-Ci1Ygqnc.js} +1 -1
- package/dist/assets/web/dist/assets/SettingsPage-DxmJvAmT.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-CiByEtoR.js → StatusDot-DPWE1JmO.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DyRxk8A8.js → Tooltip-DKhSRH9t.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-CnWyOizV.js → UnitRoutePage-DEN5bSQf.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-html-DAQ_1T1v.js → angular-html-DgSK1qvr.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-D4kxRVp1.js → angular-ts-D3gNdiSG.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-Dy07l64y.js → apl-CkHCYM8I.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-DZIbTlc7.js → astro-DLm45axt.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-CmSXWX3l.js → blade-BKa-VE-c.js} +1 -1
- package/dist/assets/web/dist/assets/{c-ggzVdJLK.js → c-C8NCNWai.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-Bo1wktsG.js → chapel-DqDQ7IKH.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-DJUmtKM2.js → cobol-j4vCD6AJ.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-BZWOVz4C.js → coffee-C6oDVH-0.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-Dtk39yMw.js → cpp-Bd3A1Rtc.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-CGf_-Zoj.js → crystal-CUOgOw0d.js} +1 -1
- package/dist/assets/web/dist/assets/{css-CX2fcL4u.js → css-RYljyv7G.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-D8TaC961.js → dist-BnJXKHCY.js} +2 -2
- package/dist/assets/web/dist/assets/{edge-BYS8FtgQ.js → edge-BJhNdQDc.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-fhUpi3nO.js → elixir-g1AWOAfT.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-DAQNiwsN.js → elm-CER5e4Dz.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-CWzEys6U.js → erb-D5aOmMQf.js} +1 -1
- package/dist/assets/web/dist/assets/{git-rebase-9rY3p-Rw.js → git-rebase-D6rfV8jp.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-tY7hy0nE.js → glimmer-js-BvDH3-mC.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-CTrLv5JF.js → glimmer-ts-DChtNs9V.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-BRJbiDfq.js → glsl-BHlsWrxf.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-B-MtVLh2.js → graphql-BsaBHb53.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-BtIkOypQ.js → hack-0lbDosCd.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-CoOH6gfa.js → haml-BKtco2vr.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-DgrRu-GP.js → handlebars-BE_cj04z.js} +1 -1
- package/dist/assets/web/dist/assets/{html-DZaaI7ca.js → html-BNcr8EVd.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-DgHvT-qZ.js → html-derivative-BsJt2Kei.js} +1 -1
- package/dist/assets/web/dist/assets/{http-CvAd51yz.js → http-BR5P8ER_.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-CXGqAfzx.js → hurl-CtHalpWS.js} +1 -1
- package/dist/assets/web/dist/assets/{indexRow-Cse3JGb0.js → indexRow-BD1VT8o8.js} +1 -1
- package/dist/assets/web/dist/assets/{java-CIQStIXF.js → java-BXVdhN11.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-WbH8s7E3.js → javascript-CyoShgNQ.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-DzyWEVKC.js → jinja-BQYZzHex.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-CrT-zCa-.js → jison-DqrUa1Eq.js} +1 -1
- package/dist/assets/web/dist/assets/{json-lHjpjXMQ.js → json-CQiTF9Jj.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-WTwYCHW6.js → jsx-Y16MkCYQ.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-Bqpk9H7E.js → julia-zn16YONe.js} +1 -1
- package/dist/assets/web/dist/assets/{just-LdXk1r3m.js → just-DcVeN_MN.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-h_y2A5KD.js → latex-BNNoF-WP.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-CY7bF2_p.js → liquid-DAi6qHzn.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-oQ7XVZOY.js → lua-BswS0axw.js} +1 -1
- package/dist/assets/web/dist/assets/{main-CGO6fQ8k.js → main-B3is4sk6.js} +2 -2
- package/dist/assets/web/dist/assets/main-DP_zSemY.css +1 -0
- package/dist/assets/web/dist/assets/{marko-zv_qZ4QV.js → marko-C-4hkmQZ.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-Dl4kL_YK.js → mdc-0ZaHUkS7.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-CAHCZj4F.js → nginx-DBDg5tOR.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-BOtmwCJH.js → nim-BVMz49Yw.js} +1 -1
- package/dist/assets/web/dist/assets/{org-DspQk3HQ.js → org-1hO_K6ya.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-CAXmvVl0.js → perl-CVRQLvVk.js} +1 -1
- package/dist/assets/web/dist/assets/{php-CMUx4VyM.js → php-K9nCrB9n.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-B55xVJTv.js → pug-CTxOcmO3.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-DsNKCCRk.js → qml-CmCkfG5q.js} +1 -1
- package/dist/assets/web/dist/assets/{r-DbhP0VvO.js → r-pP47Xn1X.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-BCpE6P-p.js → razor-BpW6r3tC.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-BllsjiN4.js → regexp-CINgdY4N.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-CO5Rpq9V.js → rst-BZIUEq2Q.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-C2A4OgQq.js → ruby-nzAOxz6r.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-Butzoyql.js → sas-QIS1bFth.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-PEUwJBpf.js → scss-BuhBBVNt.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-3Yhq0khE.js → shellscript-CQc1vXbk.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-04Hkb7m3.js → shellsession-CoubCAUv.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-BnId73cP.js → soy-CPRzlder.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-BKwdc0ch.js → sql-W9krb8-9.js} +1 -1
- package/dist/assets/web/dist/assets/{sseReplay-DutHNghE.js → sseReplay-yji8a1wM.js} +1 -1
- package/dist/assets/web/dist/assets/{stata-BPPtNkay.js → stata-CuISJEC0.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-CDPChcYP.js → surrealql-CSet7584.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-B15DIQ-z.js → svelte-ChXTSYwK.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-D-rp_AiB.js → templ-Bm55v62k.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-BnVDCD7p.js → tex-CICMX8Gj.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-cpujdyAR.js → ts-tags-CA1UzWyB.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-D1vO4L72.js → tsx-Dy04HNbv.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-oQGnQGRc.js → twig-BBHsVnVD.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-CKSFpXiH.js → typescript-Dvc-wVBT.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-DKHFo8BF.js → typst-Dvlqx3_q.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-DpCp0_Sz.js → vue-html-D4YxT3An.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-y5d5wUb1.js → vue-vine-0YE7uQlH.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-BstokpiS.js → vue-zWi1MNU2.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-Br2hx5Ax.js → xml-gm-iksZB.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-Bc8Cobap.js → xsl-D_W5BcrY.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-LOITy_I1.js → yaml-CHmZ21wZ.js} +1 -1
- package/dist/cli.js +3411 -502
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/AppShell-CM_fh-WW.js +0 -1
- package/dist/assets/web/dist/assets/HomePage-BU6aRCSe.js +0 -2
- package/dist/assets/web/dist/assets/RunFoldRow-Dulcbg25.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-DGoGrQZq.js +0 -6
- package/dist/assets/web/dist/assets/RunsIndexPage-37W7OZbC.js +0 -1
- package/dist/assets/web/dist/assets/SettingsPage-ggfB-Rqr.js +0 -1
- package/dist/assets/web/dist/assets/Slideover-CX-LEklF.js +0 -1
- package/dist/assets/web/dist/assets/main-O8UXRuEv.css +0 -1
|
@@ -327,13 +327,20 @@ workspaceDir: ./workspaces
|
|
|
327
327
|
# # a finding at or above it sends the round into a fix step instead of merge-ready.
|
|
328
328
|
# # Per channel/user: config set … --ship.addressSeverity; per run: severity:<level>.
|
|
329
329
|
|
|
330
|
-
#
|
|
331
|
-
#
|
|
332
|
-
# coding agent
|
|
333
|
-
#
|
|
334
|
-
#
|
|
335
|
-
#
|
|
336
|
-
#
|
|
330
|
+
# Which harness each preset's runs are driven by (docs/reference/specs/harness.md
|
|
331
|
+
# item 8): a mapping of preset to a harness's name. Two exist — `pi` (the pi
|
|
332
|
+
# coding agent, the default for every preset) and `opencode` (OpenCode's server,
|
|
333
|
+
# `opencode serve`) — each started inside the run's own execution container (or
|
|
334
|
+
# as a child of the bot for a preset without a workspace) with the run's
|
|
335
|
+
# model-proxy bearer as its only key, every tool call decided in the bot, every
|
|
336
|
+
# event on the run's record. A preset the block does not name runs on pi;
|
|
337
|
+
# nothing defaults to OpenCode. A run keeps the harness it started on, so
|
|
338
|
+
# changing a word moves the next run, never one in flight. Any other word
|
|
339
|
+
# (`native`, the deleted loop; `codex`) fails the load naming the two, as does
|
|
340
|
+
# an unknown preset. How-to: docs/how-to/put-a-preset-on-opencode.md.
|
|
341
|
+
# harness:
|
|
342
|
+
# coding: opencode # coding runs on OpenCode
|
|
343
|
+
# review: pi # the default, written out
|
|
337
344
|
|
|
338
345
|
# What the harness writes into pi's per-run settings for every run on pi
|
|
339
346
|
# (docs/reference/specs/harness-pi.md item 4): pi's compaction thresholds, in
|
|
@@ -349,6 +356,18 @@ workspaceDir: ./workspaces
|
|
|
349
356
|
# reserveTokens: 150000
|
|
350
357
|
# keepRecentTokens: 8000
|
|
351
358
|
|
|
359
|
+
# What the harness writes into OpenCode's per-run configuration for every run
|
|
360
|
+
# on OpenCode (docs/reference/specs/harness.md item 8): OpenCode's compaction
|
|
361
|
+
# thresholds, in tokens, under OpenCode's own names — `buffer`, the tokens kept
|
|
362
|
+
# free of the model's window before a compaction runs; `keepTokens`, how much
|
|
363
|
+
# of the newest turns it keeps. Unset, OpenCode's defaults stand and the file
|
|
364
|
+
# names no compaction. The OpenCode twin of the `pi` block above; positive
|
|
365
|
+
# integers only, any other shape or key fails the load by name.
|
|
366
|
+
# opencode:
|
|
367
|
+
# compaction:
|
|
368
|
+
# buffer: 20000
|
|
369
|
+
# keepTokens: 8000
|
|
370
|
+
|
|
352
371
|
# The fan-out cap a spawning run meets (docs/reference/specs/agent-conductor.md).
|
|
353
372
|
# `agent:conductor` starts child runs as the person who asked — each an
|
|
354
373
|
# ordinary run in a thread of its own, under that person's permissions — and
|
|
@@ -17,8 +17,11 @@ import {
|
|
|
17
17
|
createInstanceResponse,
|
|
18
18
|
instanceStatusResponse,
|
|
19
19
|
isInstanceNotFound,
|
|
20
|
+
parseInstanceEventPath,
|
|
20
21
|
parseInstanceStatusPath,
|
|
21
22
|
parseCreateInstanceRequest,
|
|
23
|
+
parseSendEventRequest,
|
|
24
|
+
sendEventResponse,
|
|
22
25
|
parseSubjectAuthorization,
|
|
23
26
|
type CreateInstanceOutcome,
|
|
24
27
|
} from "../../src/core/coordinator/instancesRoute.ts";
|
|
@@ -87,6 +90,7 @@ export interface Env {
|
|
|
87
90
|
DASHBOARD_TOKEN?: string; // dashboard auth `token` strategy: the bearer (the default env name; config may name another)
|
|
88
91
|
SWITCHBOARD_INGRESS_TOKENS?: string; // enables HTTP /ingress + MCP /mcp (JSON token→identity map); the `cron` entry is what scheduled runs present; an entry whose `http:<subject>` actor holds `deploy:write` in the bot's config may POST /admin/restart
|
|
89
92
|
BRAVE_SEARCH_API_KEY?: string; // web_search backend (Brave); web_fetch works without it
|
|
93
|
+
GITHUB_WEBHOOK_SECRET?: string; // check-run intake: signs POST /webhooks/github; absent, the intake answers 503 disabled
|
|
90
94
|
CF_ANALYTICS_TOKEN?: string; // costs dash: Cloudflare API token, Account Analytics:Read only
|
|
91
95
|
ANTHROPIC_ADMIN_KEY?: string; // costs dash (optional): Anthropic Admin API key for the LLM cost report
|
|
92
96
|
MEMORY_TOKEN?: string; // durable memory + friction ledger + schedule firings + MCP registry: bearer for the state Worker
|
|
@@ -125,6 +129,7 @@ const FORWARDED_OPTIONAL = [
|
|
|
125
129
|
"ANTHROPIC_ADMIN_KEY",
|
|
126
130
|
"SWITCHBOARD_INGRESS_TOKENS",
|
|
127
131
|
"BRAVE_SEARCH_API_KEY",
|
|
132
|
+
"GITHUB_WEBHOOK_SECRET",
|
|
128
133
|
"MEMORY_TOKEN",
|
|
129
134
|
"MCP_CREDENTIAL_KEY",
|
|
130
135
|
"MCP_ACCESS_CLIENT_ID",
|
|
@@ -407,6 +412,57 @@ async function handleCoordinatorInstanceStatus(request: Request, env: Env, id: s
|
|
|
407
412
|
return json(res.status, res.body);
|
|
408
413
|
}
|
|
409
414
|
|
|
415
|
+
/** `POST /admin/coordinator/instances/<id>/events` — the shim's event relay
|
|
416
|
+
* (docs/reference/specs/http-ingress.md item 12): the bot's check-run intake
|
|
417
|
+
* holds no Workflow binding, so its `checks-settled-<head>` send crosses here
|
|
418
|
+
* and this Worker's own `SHIP_COORDINATOR` binding delivers it. The same door
|
|
419
|
+
* as the create and the status: the bearer in the map, the bot's `authorize`
|
|
420
|
+
* answer. The engine's `instance.not_found` is 404; any other refusal (an
|
|
421
|
+
* instance that already ended) is 502 by reason — the sender treats both as a
|
|
422
|
+
* failed send and the driver's bounded wait is the fallback. */
|
|
423
|
+
async function handleCoordinatorInstanceEvent(request: Request, env: Env, id: string): Promise<Response> {
|
|
424
|
+
const json = (status: number, body: Record<string, unknown>) =>
|
|
425
|
+
new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
426
|
+
if (request.method !== "POST")
|
|
427
|
+
return json(405, {
|
|
428
|
+
ok: false,
|
|
429
|
+
error: `method not allowed: ${request.method} — ${COORDINATOR_INSTANCES_PATH}/<id>/events answers POST only`,
|
|
430
|
+
});
|
|
431
|
+
const authorization = request.headers.get("authorization") ?? undefined;
|
|
432
|
+
const authn = authenticateIngressBearer(authorization, env.SWITCHBOARD_INGRESS_TOKENS, "coordinator");
|
|
433
|
+
if (!authn.ok) {
|
|
434
|
+
console.warn(`[coordinator] instance event ${authn.status} — ${authn.reason}`);
|
|
435
|
+
return json(authn.status, { ok: false, error: authn.reason });
|
|
436
|
+
}
|
|
437
|
+
let answer: Response;
|
|
438
|
+
try {
|
|
439
|
+
answer = await getContainer(env.SWITCHBOARD, INSTANCE).fetch(
|
|
440
|
+
new Request(`${INTERNAL}${COORDINATOR_AUTHORIZE_PATH}`, {
|
|
441
|
+
method: "POST",
|
|
442
|
+
headers: { authorization: authorization ?? "" },
|
|
443
|
+
}),
|
|
444
|
+
);
|
|
445
|
+
} catch (err) {
|
|
446
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
447
|
+
return json(503, { ok: false, error: `coordinator disabled: the bot could not be asked (${reason})` });
|
|
448
|
+
}
|
|
449
|
+
const auth = parseSubjectAuthorization(answer.status, await answer.text().catch(() => ""));
|
|
450
|
+
if (!auth.ok) return json(auth.status, { ok: false, error: auth.reason });
|
|
451
|
+
const parsed = parseSendEventRequest(await request.text().catch(() => ""));
|
|
452
|
+
if (!parsed.ok) return json(400, { ok: false, error: parsed.reason });
|
|
453
|
+
let outcome: Parameters<typeof sendEventResponse>[0];
|
|
454
|
+
try {
|
|
455
|
+
await (await env.SHIP_COORDINATOR.get(id)).sendEvent({ type: parsed.type, payload: parsed.payload });
|
|
456
|
+
outcome = { kind: "sent", id };
|
|
457
|
+
} catch (err) {
|
|
458
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
459
|
+
outcome = isInstanceNotFound(reason) ? { kind: "absent", id } : { kind: "failed", id, reason };
|
|
460
|
+
}
|
|
461
|
+
const res = sendEventResponse(outcome);
|
|
462
|
+
console.log(`[coordinator] ${auth.subject} → instance ${id} event ${parsed.type}: ${outcome.kind}`);
|
|
463
|
+
return json(res.status, res.body);
|
|
464
|
+
}
|
|
465
|
+
|
|
410
466
|
/** Record a firing on the state Worker's ScheduleDO (the /runs Scheduled panel
|
|
411
467
|
* reads it). Best-effort: a failure here is a log line — the run itself (if
|
|
412
468
|
* any) already happened and is its own record. */
|
|
@@ -440,29 +496,32 @@ export default {
|
|
|
440
496
|
// The routes the Worker answers itself — the restart, the coordinator's
|
|
441
497
|
// instance creation and an instance's status, all over bindings only this
|
|
442
498
|
// Worker holds; everything else is the container's.
|
|
443
|
-
const
|
|
499
|
+
const eventsId = parseInstanceEventPath(pathname);
|
|
500
|
+
const statusId = eventsId === undefined ? parseInstanceStatusPath(pathname) : undefined;
|
|
444
501
|
const res =
|
|
445
502
|
pathname === "/admin/restart"
|
|
446
503
|
? await handleAdminRestart(forwarded, env)
|
|
447
504
|
: pathname === COORDINATOR_INSTANCES_PATH
|
|
448
505
|
? await handleCoordinatorInstances(forwarded, env)
|
|
449
|
-
:
|
|
450
|
-
? await
|
|
451
|
-
:
|
|
452
|
-
?
|
|
453
|
-
|
|
454
|
-
//
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
506
|
+
: eventsId !== undefined
|
|
507
|
+
? await handleCoordinatorInstanceEvent(forwarded, env, eventsId)
|
|
508
|
+
: statusId !== undefined
|
|
509
|
+
? await handleCoordinatorInstanceStatus(forwarded, env, statusId)
|
|
510
|
+
: pathname === COPY_PATH
|
|
511
|
+
? // The artifact copy (artifactsCopy.ts): the R2 binding and the Slack
|
|
512
|
+
// token are this Worker's; the bot only asks, with its copy bearer.
|
|
513
|
+
// `forwarded`, never `inbound`: `withTraceContext` rebuilt the request
|
|
514
|
+
// with `new Request(inbound, …)`, which takes the body stream with it —
|
|
515
|
+
// `inbound.text()` is empty afterwards and the route read "not JSON" live.
|
|
516
|
+
await handleArtifactsCopy(forwarded, {
|
|
517
|
+
bucket: env.ARTIFACTS,
|
|
518
|
+
bucketName: env.ARTIFACTS_BUCKET_NAME,
|
|
519
|
+
copyToken: env.ARTIFACTS_COPY_TOKEN,
|
|
520
|
+
slackToken: env.SLACK_BOT_TOKEN,
|
|
521
|
+
fetch: (input, init) => fetch(input, init),
|
|
522
|
+
lengthPipe: (size) => new FixedLengthStream(size),
|
|
523
|
+
})
|
|
524
|
+
: withLength(await getContainer(env.SWITCHBOARD, INSTANCE).fetch(forwarded));
|
|
466
525
|
root.end(res.status >= 500 ? "error" : "ok", { httpStatus: res.status });
|
|
467
526
|
return res;
|
|
468
527
|
} catch (err) {
|
|
@@ -7067,6 +7067,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7067
7067
|
LIFECYCLE_KEY,
|
|
7068
7068
|
REFRESH_INSTANCE_KEY,
|
|
7069
7069
|
RUNTIME_UNREACHABLE_KEY,
|
|
7070
|
+
AUTO_REBUILDS_KEY,
|
|
7070
7071
|
]);
|
|
7071
7072
|
const facts = map.get(FACTS_KEY) as RepoFacts | undefined;
|
|
7072
7073
|
const snap = map.get(SNAPSHOT_KEY) as SnapshotRecord | undefined;
|
|
@@ -40,6 +40,12 @@
|
|
|
40
40
|
"optional": true,
|
|
41
41
|
"note": "JSON bearer→identity map for POST /ingress and POST /mcp: `{ subject, channel?, email? }` per entry. Self-minted (openssl rand -hex 32 per entry). Optional: without it /ingress and /mcp are disabled and `deploy restart` has no bearer. The `cron` entry fires schedules; a `coordinator` entry (granted `coordinator:step`) is the ship coordinator's, rotated apart from cron. An `email` binds the token to a person: their runs, their grants untouched (authorization.md item 15)."
|
|
42
42
|
},
|
|
43
|
+
{
|
|
44
|
+
"name": "GITHUB_WEBHOOK_SECRET",
|
|
45
|
+
"workers": ["bot"],
|
|
46
|
+
"optional": true,
|
|
47
|
+
"note": "GitHub webhook signing secret for POST /webhooks/github (the check-run intake). Set the same value on the repo's webhook. Optional: without it the intake answers 503 disabled and the merge wait falls back to its bounded timeout."
|
|
48
|
+
},
|
|
43
49
|
{
|
|
44
50
|
"name": "SANDBOX_TOKEN",
|
|
45
51
|
"workers": ["bot", "sandbox"],
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.241.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.241.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"workspaces": [
|
|
12
12
|
"web",
|
|
@@ -20445,7 +20445,7 @@
|
|
|
20445
20445
|
},
|
|
20446
20446
|
"packages/switchboard": {
|
|
20447
20447
|
"name": "@coreplane/switchboard",
|
|
20448
|
-
"version": "1.
|
|
20448
|
+
"version": "1.241.0",
|
|
20449
20449
|
"license": "Apache-2.0",
|
|
20450
20450
|
"dependencies": {
|
|
20451
20451
|
"@earendil-works/pi-ai": "0.85.1",
|
package/dist/assets/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.241.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
|
|
6
6
|
"license": "Apache-2.0",
|
package/dist/assets/source.json
CHANGED
|
@@ -133,6 +133,23 @@ export interface AgentDef {
|
|
|
133
133
|
// and rendered against the head sha at render time, so a repush is a
|
|
134
134
|
// re-render by Switchboard — the agent only resubmits when the CONTENT (line
|
|
135
135
|
// numbers included) changed.
|
|
136
|
+
/** The status-card rule every tool-running preset carries (docs/reference/specs/run-visibility.md
|
|
137
|
+
* item 2). One sentence in one place: the card shows the command running right
|
|
138
|
+
* now beside the checklist, so the checklist's markers must be facts — ✱ from
|
|
139
|
+
* the item's first command, ✓ only once its result has been read — or the two
|
|
140
|
+
* contradict each other on the card. `examples` are the preset's own outcome
|
|
141
|
+
* phrasings; the rule itself never varies by preset. */
|
|
142
|
+
export function statusCardRule(examples = '"Implement the fix", "Run the test suite"'): string {
|
|
143
|
+
return (
|
|
144
|
+
"Maintain the user-facing status card with the update_status tool. Post your plan as a checklist (○ pending) as soon as you have it, " +
|
|
145
|
+
"then keep it truthful at every moment: the markers are facts, not intentions. Mark an item ✱ when you issue the first command that does it, " +
|
|
146
|
+
"and ✓ only after you have read the result that proves it happened — never in the same turn as the command, never because you intend to run it next, " +
|
|
147
|
+
"never for a reporting or posting step you have not done. The order is: mark the item ✱, run its commands, read the result, then mark it ✓ and the next item ✱. " +
|
|
148
|
+
"The card shows the command running right now beside your checklist, so a ✓ item whose command is still running reads as a lie. " +
|
|
149
|
+
`Items are short outcomes (${examples}), never commands. This is the only progress the user sees while you work.`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
136
153
|
const PR_DESCRIPTION_TEMPLATE = `PR description — submit it with the submit_pr_description tool for EVERY PR (this is the default, not something to wait to be asked for). Switchboard renders the GitHub body from the object you submit, so never author PR-body markdown yourself. Before submitting, judge your title with the ${PR_TITLE_GUARD} gate — \`npm run check:pr-title -- "<title>"\` — and submit only a title it accepts; the same gate refuses the PR in CI. Content contract per field (each renders as its own section): prose is unwrapped — no hard line breaks inside a paragraph. Always hyperlink the triggering issue/request. Never fabricate validation — state exactly what you ran and the real result. Keep each field concise, not padded.
|
|
137
154
|
EVERY PR includes one that already exists when you push — opened by a person, by dependabot, or by an earlier run. After EVERY push to such a PR: read its current title and body (\`github_issue_get\` with the PR number works for pull requests; \`gh pr view\` where gh exists), judge them against the change as it now stands at the pushed head, and submit the object that describes the PR as it is NOW — carry forward what the existing body says that is still true (a dependency bump's release notes belong in whatWhy), add what you changed, and anchor the Tour at the new head. Switchboard replaces the PR's title and body with your rendering. A description that describes an earlier state of its branch is a bug; "it is someone else's PR" is never a reason to leave it.
|
|
138
155
|
- **title**: the PR title — one line naming the change, specific enough to pick out of a PR list.
|
|
@@ -237,7 +254,7 @@ ${SHOW_FILES}
|
|
|
237
254
|
|
|
238
255
|
${NOTEPAD}
|
|
239
256
|
|
|
240
|
-
|
|
257
|
+
${statusCardRule('"Clone repo and read the diff", "Run the test suite"')}
|
|
241
258
|
|
|
242
259
|
If the request doesn't name a repository and you can't infer it, ask for it instead of guessing.
|
|
243
260
|
Report outcomes faithfully: if tests fail or a step was skipped, say so plainly.
|
|
@@ -280,7 +297,7 @@ ${SHOW_FILES}
|
|
|
280
297
|
|
|
281
298
|
${NOTEPAD}
|
|
282
299
|
|
|
283
|
-
|
|
300
|
+
${statusCardRule()}
|
|
284
301
|
|
|
285
302
|
Report outcomes faithfully: if tests fail or a step was skipped, say so plainly.
|
|
286
303
|
${FENCED_CONTENT_RULE}
|
|
@@ -319,7 +336,7 @@ ${SHOW_FILES}
|
|
|
319
336
|
|
|
320
337
|
${NOTEPAD}
|
|
321
338
|
|
|
322
|
-
|
|
339
|
+
${statusCardRule()}
|
|
323
340
|
|
|
324
341
|
Report outcomes faithfully: if tests fail or a step was skipped, say so plainly.
|
|
325
342
|
${FENCED_CONTENT_RULE}
|
|
@@ -384,7 +401,7 @@ ${REVIEW_VERDICT_INSTRUCTION}
|
|
|
384
401
|
|
|
385
402
|
${NOTEPAD}
|
|
386
403
|
|
|
387
|
-
|
|
404
|
+
${statusCardRule('"Gather the diff and the files", "Analyze the change", "Post the verdict"')}
|
|
388
405
|
|
|
389
406
|
${FENCED_CONTENT_RULE}
|
|
390
407
|
Your final message is posted to Slack. Lead with a one-line verdict, then the findings.`;
|
|
@@ -417,7 +434,7 @@ ${REVIEW_VERDICT_INSTRUCTION}
|
|
|
417
434
|
|
|
418
435
|
${NOTEPAD}
|
|
419
436
|
|
|
420
|
-
|
|
437
|
+
${statusCardRule('"Gather the diff and the files", "Analyze the change", "Post the verdict"')}
|
|
421
438
|
|
|
422
439
|
${FENCED_CONTENT_RULE}
|
|
423
440
|
Your final message is posted to Slack. Lead with a one-line verdict, then the findings.`;
|
|
@@ -451,7 +468,7 @@ ${REVIEW_VERDICT_INSTRUCTION}
|
|
|
451
468
|
|
|
452
469
|
${NOTEPAD}
|
|
453
470
|
|
|
454
|
-
|
|
471
|
+
${statusCardRule('"Gather the diff and the files", "Analyze the change", "Post the verdict"')}
|
|
455
472
|
|
|
456
473
|
${FENCED_CONTENT_RULE}
|
|
457
474
|
Your final message is posted to Slack. Lead with a one-line verdict, then the findings.`;
|
|
@@ -469,7 +486,7 @@ How to work:
|
|
|
469
486
|
2. Prefer primary sources; corroborate a surprising claim with a second source.
|
|
470
487
|
3. Answer concisely and cite the URLs (or repo paths) you used. If sources conflict or you couldn't verify something, say so plainly. If web search is unconfigured, use web_fetch / the GitHub tools on what you have and say search was unavailable.
|
|
471
488
|
|
|
472
|
-
|
|
489
|
+
${statusCardRule('"Search the sources", "Write the answer"')}
|
|
473
490
|
|
|
474
491
|
${FENCED_CONTENT_RULE}
|
|
475
492
|
Use Slack-friendly formatting (no markdown headers; *bold*, bullets, code blocks). Your final message is posted to Slack — lead with the answer, then supporting detail and sources.`;
|
|
@@ -485,6 +502,8 @@ Answer directly and concisely. Use Slack-friendly formatting (no markdown header
|
|
|
485
502
|
|
|
486
503
|
Your tools work without a workspace: the GitHub tools — \`github_repos\` (the org repositories you can reach), \`github_tree\` / \`github_file\` / \`github_search_code\` (browse, read, search their code and docs, private repos included), \`github_issue_list\` / \`github_issue_get\` (read issues), \`github_issue_create\` / \`github_issue_update\` / \`github_issue_comment\` / \`github_issue_delete\` (act on issues) — and \`web_fetch\` (read a public URL). Use them: when the user names a repo loosely ("the switchboard app"), resolve it with github_repos (or the thread) rather than asking; when asked about one of our repos, read it before answering. Report exactly what a tool did (issue number + URL) — never claim an action you did not perform, and never fabricate file contents, URLs, or command output.
|
|
487
504
|
|
|
505
|
+
${statusCardRule('"Read the issue and its thread", "Post the comment"')} A one-step answer needs no checklist; post one when the request has steps the person would wait on.
|
|
506
|
+
|
|
488
507
|
You cannot run commands, clone repositories, edit code, or review pull requests, and you cannot search the web. Other Switchboard agents can: for code changes or PRs tell the user to re-send with \`agent:coding\`; for a PR review, \`agent:review\`; for a web-research question, \`agent:research\` (e.g. "\`agent:coding fix the failing login test in acme/api\`", "\`agent:research compare X and Y\`"). Delete an issue only when the user explicitly asked to delete it (closing is an update).`;
|
|
489
508
|
|
|
490
509
|
// The explore agent (docs/reference/specs/agent-explore.md): a long, read-only
|
|
@@ -512,7 +531,7 @@ READ-ONLY: NEVER open a pull request, and never commit or push — no branch, no
|
|
|
512
531
|
|
|
513
532
|
You cannot attach or post files: your whole answer is text. Never say a file is attached or below — name its path in the workspace and describe it (what it shows, its size) instead; a person who needs the file itself asks \`agent:coding\`, which can attach.
|
|
514
533
|
|
|
515
|
-
|
|
534
|
+
${statusCardRule('"Clone and install", "Time the full suite"')}
|
|
516
535
|
|
|
517
536
|
${NOTEPAD}
|
|
518
537
|
|
|
@@ -100,6 +100,11 @@ const CHAT_SURFACES: Readonly<Record<string, ActorSurface>> = {
|
|
|
100
100
|
mcp: "mcp",
|
|
101
101
|
cli: "cli",
|
|
102
102
|
schedule: "schedule",
|
|
103
|
+
// The web chat (record 0043): a browser session speaks as text, so its
|
|
104
|
+
// `access:<sub>` is a chat credential too — the browser actor, its baseline
|
|
105
|
+
// and its entry. A service token never reaches the chat (the dashboard gate
|
|
106
|
+
// serves it `/api/*` only), so `access:svc:` is no chat surface.
|
|
107
|
+
access: "access-browser",
|
|
103
108
|
};
|
|
104
109
|
|
|
105
110
|
/** A chat message's `userId` is already namespaced by its adapter (`slack:U…`,
|
|
@@ -62,10 +62,20 @@ export const CHAT_OPEN_ACTIONS: readonly string[] = [
|
|
|
62
62
|
"mcp:write",
|
|
63
63
|
];
|
|
64
64
|
|
|
65
|
-
/**
|
|
66
|
-
*
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
/** The two writes a browser session holds beside its reads (record 0043): the
|
|
66
|
+
* same personal writes every Slack user holds (`CHAT_OPEN_ACTIONS`), because
|
|
67
|
+
* the web chat makes a browser session a chat user — `memory remember` and
|
|
68
|
+
* `mcp add` for its own tier. The tier rows still decide the target: a shared
|
|
69
|
+
* tier needs the grant it always needed. */
|
|
70
|
+
export const BROWSER_WRITE_ACTIONS: readonly string[] = ["memory:write", "mcp:write"];
|
|
71
|
+
|
|
72
|
+
/** What an Access browser session holds implicitly: every registered group's
|
|
73
|
+
* read plus the two personal chat writes — never another write, never an exec.
|
|
74
|
+
* No groups known (a process that registered no commands) → nothing at all,
|
|
75
|
+
* fail-closed: the writes ride with the catalogue they belong to. */
|
|
76
|
+
export function browserActions(commandGroups: readonly string[]): Set<string> {
|
|
77
|
+
if (commandGroups.length === 0) return new Set();
|
|
78
|
+
return new Set([...commandGroups.map((g) => `${g}:read`), ...BROWSER_WRITE_ACTIONS]);
|
|
69
79
|
}
|
|
70
80
|
|
|
71
81
|
// ---- the native `grants` block ----------------------------------------------
|
|
@@ -243,8 +253,9 @@ export interface GrantsTable {
|
|
|
243
253
|
surfaces: Map<string, Grants>;
|
|
244
254
|
/** What every `slack:` user holds, listed or not: the open chat commands and `agent:run:<name>` for every unrestricted agent. */
|
|
245
255
|
everyone: Grants;
|
|
246
|
-
/** What every Access browser session (`access:<sub>`, never `access:svc:`) holds:
|
|
247
|
-
|
|
256
|
+
/** What every Access browser session (`access:<sub>`, never `access:svc:`) holds:
|
|
257
|
+
* each registered group's read and the two personal chat writes. */
|
|
258
|
+
browser: Grants;
|
|
248
259
|
restrict: Restriction;
|
|
249
260
|
}
|
|
250
261
|
|
|
@@ -253,9 +264,9 @@ export interface GrantsTable {
|
|
|
253
264
|
* `<group>:read`. Every other namespace (`schedule:`, `access:svc:`, `http:`,
|
|
254
265
|
* `mcp:`) is a credential or a job that holds exactly what names it — an
|
|
255
266
|
* unlisted one is `NO_GRANTS` (fail-closed). */
|
|
256
|
-
export function namespaceBaseline(actorId: string, table: Pick<GrantsTable, "everyone" | "
|
|
267
|
+
export function namespaceBaseline(actorId: string, table: Pick<GrantsTable, "everyone" | "browser">): Grants {
|
|
257
268
|
if (actorId.startsWith("slack:")) return table.everyone;
|
|
258
|
-
if (actorId.startsWith("access:") && !actorId.startsWith("access:svc:")) return table.
|
|
269
|
+
if (actorId.startsWith("access:") && !actorId.startsWith("access:svc:")) return table.browser;
|
|
259
270
|
return NO_GRANTS;
|
|
260
271
|
}
|
|
261
272
|
|
|
@@ -271,7 +282,7 @@ export function grantsTable(source: GrantsSource): GrantsTable {
|
|
|
271
282
|
const openAgents = (source.agentNames ?? []).filter((a) => !restrict.agents.has(a)).map(agentRunAction);
|
|
272
283
|
const baselines = {
|
|
273
284
|
everyone: { ...NO_GRANTS, actions: new Set([...CHAT_OPEN_ACTIONS, ...openAgents]) },
|
|
274
|
-
|
|
285
|
+
browser: { ...NO_GRANTS, actions: browserActions(source.commandGroups ?? []) },
|
|
275
286
|
};
|
|
276
287
|
const grants = new Map<string, Grants>();
|
|
277
288
|
const surfaces = new Map<string, Grants>();
|
|
@@ -80,6 +80,22 @@ export function runFinishedEventType(runId: string): string {
|
|
|
80
80
|
return `${RUN_FINISHED_EVENT_PREFIX}${runId}`;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/** The event the bot's GitHub check-run intake sends a merge-waiting parent:
|
|
84
|
+
* the type carries the head sha (hex — inside the platform's alphabet), so a
|
|
85
|
+
* driver waiting at that head matches its own event and any other head's is
|
|
86
|
+
* buffered harmlessly. */
|
|
87
|
+
export const CHECKS_SETTLED_EVENT_PREFIX = "checks-settled-";
|
|
88
|
+
export function checksSettledEventType(headSha: string): string {
|
|
89
|
+
return `${CHECKS_SETTLED_EVENT_PREFIX}${headSha}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** What the checks-settled event carries — the head and a clock; the parent
|
|
93
|
+
* re-asks the merge door before it acts, so nothing more rides here. */
|
|
94
|
+
export interface ChecksSettledPayload {
|
|
95
|
+
headSha: string;
|
|
96
|
+
settledAt: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
83
99
|
/** What the event carries — ids, a status and a clock; the parent confirms
|
|
84
100
|
* through `read-record` before it acts, so nothing more rides here. */
|
|
85
101
|
export interface RunFinishedPayload {
|
|
@@ -310,6 +326,27 @@ export type RunFinishedSend =
|
|
|
310
326
|
| { kind: "no-binding"; instance: string }
|
|
311
327
|
| { kind: "failed"; instance: string; type: string; reason: string };
|
|
312
328
|
|
|
329
|
+
/** The one send per settled head (http-ingress.md item 12): best effort like
|
|
330
|
+
* `sendRunFinished` — a refusal is answered, never thrown, and the parent's
|
|
331
|
+
* bounded merge wait times out on its own. */
|
|
332
|
+
export async function sendChecksSettled(
|
|
333
|
+
workflow: WorkflowSender | undefined,
|
|
334
|
+
instance: string,
|
|
335
|
+
headSha: string,
|
|
336
|
+
settledAt: number,
|
|
337
|
+
): Promise<RunFinishedSend> {
|
|
338
|
+
if (!workflow) return { kind: "no-binding", instance };
|
|
339
|
+
const type = checksSettledEventType(headSha);
|
|
340
|
+
const payload: ChecksSettledPayload = { headSha, settledAt };
|
|
341
|
+
try {
|
|
342
|
+
const handle = await workflow.get(instance);
|
|
343
|
+
await handle.sendEvent({ type, payload });
|
|
344
|
+
return { kind: "sent", instance, type };
|
|
345
|
+
} catch (err) {
|
|
346
|
+
return { kind: "failed", instance, type, reason: err instanceof Error ? err.message : String(err) };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
313
350
|
/** The one send per committed terminal record (run-history item 47). */
|
|
314
351
|
export async function sendRunFinished(
|
|
315
352
|
workflow: WorkflowSender | undefined,
|
|
@@ -57,7 +57,7 @@ import {
|
|
|
57
57
|
type UnitPipelineInput,
|
|
58
58
|
type UnitPipelineState,
|
|
59
59
|
} from "../ship/coordinator.js";
|
|
60
|
-
import { isCoordinatorUnit, runFinishedEventType, type CoordinatorUnit } from "./contract.js";
|
|
60
|
+
import { checksSettledEventType, isCoordinatorUnit, runFinishedEventType, type CoordinatorUnit } from "./contract.js";
|
|
61
61
|
|
|
62
62
|
const MIN = 60_000;
|
|
63
63
|
|
|
@@ -429,6 +429,22 @@ async function perform(
|
|
|
429
429
|
case "sleep":
|
|
430
430
|
await step.sleep(action.step, action.ms);
|
|
431
431
|
return { type: "sleep", step: action.step };
|
|
432
|
+
case "wait-checks": {
|
|
433
|
+
// The intake's checks-settled event at the approved head (http-ingress.md
|
|
434
|
+
// item 12), with the machine's bounded timeout as the fallback: either
|
|
435
|
+
// way the machine re-asks the merge door, which is the guard.
|
|
436
|
+
let outcome: "event" | "timeout";
|
|
437
|
+
try {
|
|
438
|
+
await step.waitForEvent(action.step, {
|
|
439
|
+
type: checksSettledEventType(action.headSha),
|
|
440
|
+
timeout: action.timeoutMs,
|
|
441
|
+
});
|
|
442
|
+
outcome = "event";
|
|
443
|
+
} catch {
|
|
444
|
+
outcome = "timeout";
|
|
445
|
+
}
|
|
446
|
+
return { type: "wait-checks", step: action.step, outcome };
|
|
447
|
+
}
|
|
432
448
|
case "merge":
|
|
433
449
|
return mergeReturn(
|
|
434
450
|
action.step,
|
|
@@ -131,6 +131,78 @@ export function parseInstanceStatusPath(pathname: string): string | undefined {
|
|
|
131
131
|
return INSTANCE_ID_PATTERN.test(id) ? id : undefined;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/** `POST /admin/coordinator/instances/<id>/events` — the shim's event relay
|
|
135
|
+
* (docs/reference/specs/http-ingress.md item 12): the bot holds no Workflow
|
|
136
|
+
* binding, so the check-run intake sends `checks-settled-<head>` through this
|
|
137
|
+
* route and the shim's own `SHIP_COORDINATOR.get(id).sendEvent` delivers it. */
|
|
138
|
+
export const COORDINATOR_INSTANCE_EVENTS_SUFFIX = "/events";
|
|
139
|
+
|
|
140
|
+
/** The instance id an event path names, or undefined for any other path. */
|
|
141
|
+
export function parseInstanceEventPath(pathname: string): string | undefined {
|
|
142
|
+
if (!pathname.startsWith(COORDINATOR_INSTANCE_STATUS_PREFIX)) return undefined;
|
|
143
|
+
if (!pathname.endsWith(COORDINATOR_INSTANCE_EVENTS_SUFFIX)) return undefined;
|
|
144
|
+
const id = pathname.slice(
|
|
145
|
+
COORDINATOR_INSTANCE_STATUS_PREFIX.length,
|
|
146
|
+
pathname.length - COORDINATOR_INSTANCE_EVENTS_SUFFIX.length,
|
|
147
|
+
);
|
|
148
|
+
return INSTANCE_ID_PATTERN.test(id) ? id : undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export type ParsedSendEvent = { ok: true; type: string; payload: unknown } | { ok: false; reason: string };
|
|
152
|
+
|
|
153
|
+
/** The Workflow event alphabet — what the engine's `sendEvent` accepts as a type. */
|
|
154
|
+
const EVENT_TYPE_PATTERN = /^[A-Za-z0-9_-]{1,100}$/;
|
|
155
|
+
|
|
156
|
+
/** The body: `{ type, payload? }` — the typed event the instance's `waitForEvent` waits on. */
|
|
157
|
+
export function parseSendEventRequest(text: string): ParsedSendEvent {
|
|
158
|
+
let parsed: unknown;
|
|
159
|
+
try {
|
|
160
|
+
parsed = JSON.parse(text);
|
|
161
|
+
} catch {
|
|
162
|
+
return { ok: false, reason: "body is not valid JSON" };
|
|
163
|
+
}
|
|
164
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
165
|
+
return { ok: false, reason: "body must be a JSON object" };
|
|
166
|
+
const b = parsed as Record<string, unknown>;
|
|
167
|
+
if (typeof b.type !== "string" || !EVENT_TYPE_PATTERN.test(b.type))
|
|
168
|
+
return { ok: false, reason: "`type` must be a Workflow event type: letters, digits, `_` and `-`, at most 100" };
|
|
169
|
+
return { ok: true, type: b.type, payload: b.payload };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** How the send ended on the shim: delivered, no such instance (the engine's
|
|
173
|
+
* own `instance.not_found`), or the engine refusing by reason (an instance
|
|
174
|
+
* that already ended is a refusal, not an absence). */
|
|
175
|
+
export type SendEventOutcome =
|
|
176
|
+
{ kind: "sent"; id: string } | { kind: "absent"; id: string } | { kind: "failed"; id: string; reason: string };
|
|
177
|
+
|
|
178
|
+
export function sendEventResponse(outcome: SendEventOutcome): { status: number; body: Record<string, unknown> } {
|
|
179
|
+
switch (outcome.kind) {
|
|
180
|
+
case "sent":
|
|
181
|
+
return { status: 200, body: { ok: true, id: outcome.id, sent: true } };
|
|
182
|
+
case "absent":
|
|
183
|
+
return { status: 404, body: { ok: false, error: "no_instance", id: outcome.id } };
|
|
184
|
+
case "failed":
|
|
185
|
+
return { status: 502, body: { ok: false, error: "send_failed", id: outcome.id, message: outcome.reason } };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The send answer as the bot reads it back: sent, absent, or unanswered by reason. */
|
|
190
|
+
export type SendEventAnswer = { kind: "sent" } | { kind: "absent" } | { kind: "unanswered"; reason: string };
|
|
191
|
+
|
|
192
|
+
export function readSendEventAnswer(status: number, text: string): SendEventAnswer {
|
|
193
|
+
let parsed: unknown;
|
|
194
|
+
try {
|
|
195
|
+
parsed = JSON.parse(text);
|
|
196
|
+
} catch {
|
|
197
|
+
parsed = undefined;
|
|
198
|
+
}
|
|
199
|
+
const body = typeof parsed === "object" && parsed !== null ? (parsed as Record<string, unknown>) : undefined;
|
|
200
|
+
if (status === 200 && body?.ok === true && body.sent === true) return { kind: "sent" };
|
|
201
|
+
if (status === 404 && body?.error === "no_instance") return { kind: "absent" };
|
|
202
|
+
const detail = typeof body?.error === "string" ? body.error : text.slice(0, 200);
|
|
203
|
+
return { kind: "unanswered", reason: `HTTP ${status} — ${detail}` };
|
|
204
|
+
}
|
|
205
|
+
|
|
134
206
|
/** How the status read ended on the shim: the platform's status word, no such
|
|
135
207
|
* instance, or the engine failing by reason. */
|
|
136
208
|
export type InstanceStatusOutcome =
|
|
@@ -762,10 +762,15 @@ export function anthropicTokensCostUsd(modelId: string, t: AnthropicTokens): num
|
|
|
762
762
|
// ---- range ----------------------------------------------------------------------------
|
|
763
763
|
|
|
764
764
|
const DEFAULT_DAYS = 30;
|
|
765
|
-
/** The widest range the page offers — and the window a costs snapshot is read for.
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
765
|
+
/** The widest range the page offers — and the window a costs snapshot is read for.
|
|
766
|
+
* Cloudflare's analytics on a Workers account answer no range wider than 4w4d
|
|
767
|
+
* (32 days) and hold no data older than that (`cannot request a time range
|
|
768
|
+
* wider than 4w4d`, `cannot request data older than 4w4d` — measured live), so
|
|
769
|
+
* a wider window is a refused read, not more history: 31 UTC days is the most
|
|
770
|
+
* a take can ask for. */
|
|
771
|
+
export const MAX_DAYS = 31;
|
|
772
|
+
|
|
773
|
+
/** `?days=N` → a UTC date range ending today. Garbage → default; clamped 1..31. */
|
|
769
774
|
export function resolveRange(daysParam: string | null, now: Date = new Date(systemClock())): DateRange {
|
|
770
775
|
const parsed = daysParam === null ? NaN : Number(daysParam);
|
|
771
776
|
const days = Number.isInteger(parsed) ? Math.min(MAX_DAYS, Math.max(1, parsed)) : DEFAULT_DAYS;
|
|
@@ -1070,7 +1075,7 @@ export class AnthropicCostReportSource implements LlmCostSource {
|
|
|
1070
1075
|
if (range.from >= endExclusive) return { rows, closedThrough };
|
|
1071
1076
|
let page: string | null = null;
|
|
1072
1077
|
for (let i = 0; ; i++) {
|
|
1073
|
-
// Like the non-USD check: refuse rather than mis-sum. A ≤
|
|
1078
|
+
// Like the non-USD check: refuse rather than mis-sum. A ≤31-day range at
|
|
1074
1079
|
// limit=31 is at most 3 pages, so hitting the cap means the API changed.
|
|
1075
1080
|
if (i >= MAX_COST_PAGES)
|
|
1076
1081
|
throw new Error(
|