@coreplane/switchboard 1.240.0 → 1.242.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 +40 -8
- package/dist/assets/deploy/cloudflare/worker.ts +77 -18
- package/dist/assets/deploy/cloudflare-resident/worker.ts +198 -15
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +38 -5
- 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/project.json +2 -2
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +58 -22
- package/dist/assets/src/core/authz/actor.ts +17 -9
- package/dist/assets/src/core/authz/authorize.ts +17 -4
- package/dist/assets/src/core/authz/grants.ts +20 -9
- package/dist/assets/src/core/authz/types.ts +12 -0
- package/dist/assets/src/core/coordinator/contract.ts +40 -1
- 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 +22 -9
- package/dist/assets/src/core/harness/scope.ts +21 -0
- package/dist/assets/src/core/runEvents.ts +20 -1
- package/dist/assets/src/core/runLedger/types.ts +2 -0
- package/dist/assets/src/core/runRecord.ts +4 -0
- package/dist/assets/src/core/ship/contract.ts +4 -2
- 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/src/execution/residentAutoRebuild.ts +4 -2
- package/dist/assets/src/execution/residentHead.ts +23 -10
- package/dist/assets/src/execution/residentInfraStreak.ts +89 -0
- package/dist/assets/src/execution/residentRefresh.ts +15 -1
- package/dist/assets/src/execution/residentSteps.ts +1 -0
- package/dist/assets/src/execution/sandboxErrors.ts +6 -0
- package/dist/assets/src/execution/seedPlan.ts +17 -0
- package/dist/assets/web/dist/.vite/manifest.json +435 -439
- package/dist/assets/web/dist/assets/AppShell-IDMGG6yi.js +1 -0
- package/dist/assets/web/dist/assets/{CostsPage-BD3w5j_S.js → CostsPage-CtmKhhOF.js} +2 -2
- package/dist/assets/web/dist/assets/{DeliveryPage-BtvzKA1m.js → DeliveryPage-n1tz5I_v.js} +1 -1
- package/dist/assets/web/dist/assets/HomePage-QtH1EwYF.js +2 -0
- package/dist/assets/web/dist/assets/{NotFoundPage-DwOX1HaZ.js → NotFoundPage-C97EhJcQ.js} +1 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-BZA_vQt3.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-CUAOh5d9.js → ResidentDetailPage-DTMBgnIW.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-B3KUlYy2.js → ResidentsIndexPage-CaDvXhzJ.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-mgyLW0oV.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-DypJYMQa.js +6 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-DYraPoWD.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-BnkvCBrM.js → RunsTabs-BUfdk0lH.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-C1y9jgAk.js → ScheduledPage-DXD2gLJk.js} +1 -1
- package/dist/assets/web/dist/assets/SettingsPage-BIGio8Y0.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-CiByEtoR.js → StatusDot-ELoXHlFt.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DyRxk8A8.js → Tooltip-BoeFwYP2.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-jhCrWW3i.js +1 -0
- package/dist/assets/web/dist/assets/{angular-html-DAQ_1T1v.js → angular-html-oBNfPJR0.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-D4kxRVp1.js → angular-ts-BvNwsyWA.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-Dy07l64y.js → apl-CNUdRlYf.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-DZIbTlc7.js → astro-Zb0NriSe.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-CmSXWX3l.js → blade-qPRVheqq.js} +1 -1
- package/dist/assets/web/dist/assets/{c-ggzVdJLK.js → c-D8Awx4YO.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-Bo1wktsG.js → chapel-Bt72Mhsx.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-DJUmtKM2.js → cobol-BOBacexg.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-BZWOVz4C.js → coffee-E4u0liHW.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-Dtk39yMw.js → cpp-q2sLNlul.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-CGf_-Zoj.js → crystal-DyWqUnlb.js} +1 -1
- package/dist/assets/web/dist/assets/{css-CX2fcL4u.js → css-CQY0hFsD.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-D8TaC961.js → dist-BU5UivXC.js} +2 -2
- package/dist/assets/web/dist/assets/durationTone-BobbycC-.js +1 -0
- package/dist/assets/web/dist/assets/{edge-BYS8FtgQ.js → edge-C1MwhJkX.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-fhUpi3nO.js → elixir-Bb3YbHfn.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-DAQNiwsN.js → elm-DAN9IGQw.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-CWzEys6U.js → erb-BEB8Xlsj.js} +1 -1
- package/dist/assets/web/dist/assets/{git-rebase-9rY3p-Rw.js → git-rebase-tqpjRfxO.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-tY7hy0nE.js → glimmer-js-Ccbo65zR.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-CTrLv5JF.js → glimmer-ts-B6WBVMpE.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-BRJbiDfq.js → glsl-tRec3Fcu.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-B-MtVLh2.js → graphql-P8kbxT4F.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-BtIkOypQ.js → hack-H9Zhkagy.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-CoOH6gfa.js → haml-DtEnpn7Z.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-DgrRu-GP.js → handlebars-DkgPfoAz.js} +1 -1
- package/dist/assets/web/dist/assets/{html-DZaaI7ca.js → html-D30RXpIs.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-DgHvT-qZ.js → html-derivative-DwozLrEx.js} +1 -1
- package/dist/assets/web/dist/assets/{http-CvAd51yz.js → http-DXuzBAPm.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-CXGqAfzx.js → hurl-d1UUJIt_.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-DL0gWf34.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-WbH8s7E3.js → javascript-Cl7vavnS.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-DzyWEVKC.js → jinja-CfQOWMX9.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-CrT-zCa-.js → jison-KdYirlqm.js} +1 -1
- package/dist/assets/web/dist/assets/{json-lHjpjXMQ.js → json-C9cDQ-Qj.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-WTwYCHW6.js → jsx-CRx5NItd.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-Bqpk9H7E.js → julia-CmsQsZQl.js} +1 -1
- package/dist/assets/web/dist/assets/{just-LdXk1r3m.js → just-CsM3Q8TE.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-h_y2A5KD.js → latex-BXCh5YRX.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-CY7bF2_p.js → liquid-BF2vwK8p.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-oQ7XVZOY.js → lua-DcMBATrl.js} +1 -1
- package/dist/assets/web/dist/assets/main-B2fX10aW.css +1 -0
- package/dist/assets/web/dist/assets/{main-CGO6fQ8k.js → main-D4EA1g6n.js} +2 -2
- package/dist/assets/web/dist/assets/{marko-zv_qZ4QV.js → marko-88MndKvG.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-Dl4kL_YK.js → mdc-DJ4kVAd8.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-CAHCZj4F.js → nginx-CP6mRgtV.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-BOtmwCJH.js → nim-QQfj3fpF.js} +1 -1
- package/dist/assets/web/dist/assets/{org-DspQk3HQ.js → org-Bke3eBzc.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-CAXmvVl0.js → perl-Bkh0N7Kz.js} +1 -1
- package/dist/assets/web/dist/assets/{php-CMUx4VyM.js → php-Dnv1Piya.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-B55xVJTv.js → pug-D6peFFI7.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-DsNKCCRk.js → qml-D8PEs-C-.js} +1 -1
- package/dist/assets/web/dist/assets/{r-DbhP0VvO.js → r-B1EL9b_j.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-BCpE6P-p.js → razor-BsMTIh2b.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-BllsjiN4.js → regexp-HhvC8spD.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-CO5Rpq9V.js → rst-D5paAxpg.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-C2A4OgQq.js → ruby-BrQwhLrl.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-Butzoyql.js → sas-afot2B1o.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-PEUwJBpf.js → scss-Efm-mwuG.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-3Yhq0khE.js → shellscript-BK0Vv5fT.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-04Hkb7m3.js → shellsession-BGqlMC7N.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-BnId73cP.js → soy-M0b4UwGM.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-BKwdc0ch.js → sql-sxe6IE9j.js} +1 -1
- package/dist/assets/web/dist/assets/sseReplay-g7ml86LM.js +9 -0
- package/dist/assets/web/dist/assets/{stata-BPPtNkay.js → stata-nPF_ddLP.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-CDPChcYP.js → surrealql-D_GrC6u7.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-B15DIQ-z.js → svelte-DwL1AtNP.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-D-rp_AiB.js → templ-7s7LTDkc.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-BnVDCD7p.js → tex-Bx-5fMxe.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-cpujdyAR.js → ts-tags-DUMJnke_.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-D1vO4L72.js → tsx-BN8biPDe.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-oQGnQGRc.js → twig-8nIu84TN.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-CKSFpXiH.js → typescript-BooSPq_S.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-DKHFo8BF.js → typst-CTBiBsem.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-BstokpiS.js → vue-C6Ft4Lea.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-DpCp0_Sz.js → vue-html-Ba36dD5D.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-y5d5wUb1.js → vue-vine-NkFexVo2.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-Br2hx5Ax.js → xml-C_THnHXZ.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-Bc8Cobap.js → xsl-D8G5xqjY.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-LOITy_I1.js → yaml-jAMIJzge.js} +1 -1
- package/dist/cli.js +4632 -817
- 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/SlackMark-VDNs7Vjh.js +0 -1
- package/dist/assets/web/dist/assets/Slideover-CX-LEklF.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-CnWyOizV.js +0 -1
- package/dist/assets/web/dist/assets/durationTone-DXG-3R7_.js +0 -1
- package/dist/assets/web/dist/assets/main-O8UXRuEv.css +0 -1
- package/dist/assets/web/dist/assets/sseReplay-DutHNghE.js +0 -9
|
@@ -87,8 +87,10 @@ import {
|
|
|
87
87
|
SEED_ABANDONED_RESTORE_WAIT_MS,
|
|
88
88
|
seedFixupScript,
|
|
89
89
|
seedMarkerText,
|
|
90
|
+
type RestorePhases,
|
|
90
91
|
type SandboxSeed,
|
|
91
92
|
type SeedAnswer,
|
|
93
|
+
type SeedPhases,
|
|
92
94
|
type SeedStep,
|
|
93
95
|
} from "../../src/execution/seedPlan.js";
|
|
94
96
|
import { shellQuote } from "../../src/execution/shellQuote.js";
|
|
@@ -450,12 +452,13 @@ export class SwitchboardSandbox extends Sandbox<Env> {
|
|
|
450
452
|
// marker for another handle.
|
|
451
453
|
await this.runRoot(["sh", "-c", `${unmountAllRestoresScript()}\n${this.seedSweep()}`], 60_000);
|
|
452
454
|
let t = systemClock();
|
|
453
|
-
await this.restoreSeedInto(seed.checkoutBackupId, SEED_CHECKOUT_DIR, deadline, "checkout");
|
|
455
|
+
const checkout = await this.restoreSeedInto(seed.checkoutBackupId, SEED_CHECKOUT_DIR, deadline, "checkout");
|
|
454
456
|
steps.restore = systemClock() - t;
|
|
457
|
+
const phases: SeedPhases = { checkout, deps: null };
|
|
455
458
|
if (seed.depsBackupId) {
|
|
456
459
|
step = "deps";
|
|
457
460
|
t = systemClock();
|
|
458
|
-
await this.restoreSeedInto(seed.depsBackupId, SEED_DEPS_STAGING_DIR, deadline, "deps");
|
|
461
|
+
phases.deps = await this.restoreSeedInto(seed.depsBackupId, SEED_DEPS_STAGING_DIR, deadline, "deps");
|
|
459
462
|
steps.deps = systemClock() - t;
|
|
460
463
|
}
|
|
461
464
|
step = "fixup";
|
|
@@ -480,9 +483,27 @@ export class SwitchboardSandbox extends Sandbox<Env> {
|
|
|
480
483
|
);
|
|
481
484
|
const ms = systemClock() - t0;
|
|
482
485
|
console.log(
|
|
483
|
-
JSON.stringify({
|
|
486
|
+
JSON.stringify({
|
|
487
|
+
event: "sandbox.seeded",
|
|
488
|
+
slug: seed.slug,
|
|
489
|
+
ref: seed.fetchRef ?? seed.ref,
|
|
490
|
+
sha,
|
|
491
|
+
steps,
|
|
492
|
+
phases,
|
|
493
|
+
ms,
|
|
494
|
+
}),
|
|
484
495
|
);
|
|
485
|
-
return {
|
|
496
|
+
return {
|
|
497
|
+
seeded: true,
|
|
498
|
+
cached: false,
|
|
499
|
+
slug: seed.slug,
|
|
500
|
+
ref: seed.fetchRef ?? seed.ref,
|
|
501
|
+
sha,
|
|
502
|
+
from,
|
|
503
|
+
steps,
|
|
504
|
+
phases,
|
|
505
|
+
ms,
|
|
506
|
+
};
|
|
486
507
|
} catch (err) {
|
|
487
508
|
const shape = thrownShape(err);
|
|
488
509
|
// A half seed never survives either: the run that follows goes cold
|
|
@@ -510,7 +531,12 @@ export class SwitchboardSandbox extends Sandbox<Env> {
|
|
|
510
531
|
* a staging sibling, the wait is judged by bytes arriving (the SDK's call
|
|
511
532
|
* takes no timeout) against the seed's one deadline, then the extract
|
|
512
533
|
* script puts a real tree in place and the target appears last. */
|
|
513
|
-
private async restoreSeedInto(
|
|
534
|
+
private async restoreSeedInto(
|
|
535
|
+
id: string,
|
|
536
|
+
targetDir: string,
|
|
537
|
+
deadlineMs: number,
|
|
538
|
+
what: string,
|
|
539
|
+
): Promise<RestorePhases> {
|
|
514
540
|
const attempt = crypto.randomUUID().slice(0, 8);
|
|
515
541
|
const mountDir = restoreMountDir(targetDir, attempt);
|
|
516
542
|
const backup: DirectoryBackup = { id, dir: mountDir };
|
|
@@ -535,11 +561,18 @@ export class SwitchboardSandbox extends Sandbox<Env> {
|
|
|
535
561
|
const verdict = judgeRestoreProgress({ startedMs, nowMs: systemClock(), samples, deadlineMs });
|
|
536
562
|
if (verdict.verdict !== "wait") throw new Error(`${what} restore ${verdict.verdict}: ${verdict.detail}`);
|
|
537
563
|
}
|
|
564
|
+
const download = systemClock() - startedMs;
|
|
565
|
+
const extractStartedMs = systemClock();
|
|
538
566
|
const r = await this.runRoot(
|
|
539
567
|
["sh", "-c", extractRestoreScript({ mountDir, backupId: id, archivePath: restoreArchivePath(id), targetDir })],
|
|
540
568
|
Math.max(60_000, deadlineMs - systemClock()),
|
|
541
569
|
);
|
|
542
570
|
if (r.exitCode !== 0) throw new Error(`${what} extract exited ${r.exitCode}: ${tail(r.stderr || r.stdout)}`);
|
|
571
|
+
const extract = systemClock() - extractStartedMs;
|
|
572
|
+
console.log(
|
|
573
|
+
JSON.stringify({ event: "sandbox.seed-restore", what, id, download, extract, method: r.stdout.trim() }),
|
|
574
|
+
);
|
|
575
|
+
return { download, extract };
|
|
543
576
|
}
|
|
544
577
|
|
|
545
578
|
/** Restores this object started that have not settled: what a failure
|
|
@@ -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.242.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.242.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.242.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.242.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/project.json
CHANGED
|
@@ -204,11 +204,11 @@
|
|
|
204
204
|
"when": "After a `web/` change."
|
|
205
205
|
},
|
|
206
206
|
"screenshots:gen": {
|
|
207
|
-
"does": "Renders the dashboard
|
|
207
|
+
"does": "Renders the dashboard screenshots whose inputs changed, both themes, recording each surface's input hashes in `docs/public/screenshots/manifest/` (`--force`: all).",
|
|
208
208
|
"when": "After a `web/` or fixture change, once `screenshots:check` names it; needs `npx playwright-core install chromium`, so it is not part of `fix`."
|
|
209
209
|
},
|
|
210
210
|
"screenshots:check": {
|
|
211
|
-
"does": "
|
|
211
|
+
"does": "Each surface's inputs still hash to what its screenshots were rendered from — no browser.",
|
|
212
212
|
"when": "Part of `check:consistency`."
|
|
213
213
|
},
|
|
214
214
|
"load": {
|
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.
|
|
@@ -206,6 +223,14 @@ export const FENCED_CONTENT_RULE =
|
|
|
206
223
|
const NOTEPAD = `YOUR NOTES AND YOUR REACH BACK. This thread's conversation outlives your context window and this run: every turn — yours, the person's, every tool call and its output, from this run and the runs before it in this thread — is kept in a log you can search with the \`recall\` tool (words → the matching turns with their numbers; a turn number → that turn whole). When something you need is no longer in front of you, recall it instead of redoing the work or guessing.
|
|
207
224
|
Keep notes with the \`notes\` tool: one short document, replaced whole each time, at most 8 KiB — decisions and their reasons, the names of things you found (files, tests, commits, the head your tests were green at), what is not yet proven. They are the one thing sure to survive a compaction and to reach the next run in this thread: they ride your system prompt at its start and come back to you right after a compaction. A person reads them too, on the run's page, so write them as a document and never as one paragraph: Markdown, a \`##\` heading per section — \`Done\`, \`In progress\`, \`Next\`, \`Facts\` (names, ids, heads, the reasons behind decisions), leaving out a section with nothing in it — one bullet per item, one line per bullet, no prose walls. Write them when you decide something worth keeping, not only at the end.`;
|
|
208
225
|
|
|
226
|
+
// Every coding prompt carries this verbatim (docs/reference/specs/agent-coding.md
|
|
227
|
+
// item 13): the order of checks and the push. Three plan children died at their
|
|
228
|
+
// budget in one evening with finished work unpushed because each ran the
|
|
229
|
+
// project's most expensive checks first; the rule is the runner's to hold, not
|
|
230
|
+
// a line every requester remembers to paste. Stack-agnostic on purpose — the
|
|
231
|
+
// classes are by duration, the project's own scripts and CI say which is which.
|
|
232
|
+
export const CHECKS_BY_COST = `CHECKS BY COST — push before the expensive ones. Every check you might run has a cost class: seconds (a formatter or a linter on the files you touched, one test file, a docs, link or spec check, the typecheck of one package) or minutes (the whole test suite, a build, a dependency install, an end-to-end or full verification). Know a command's class before you run it — from the project's own scripts and CI configuration, from how long it took last time, or by the class above when you have nothing better. Prove each change with the cheapest check that can prove it, matched to the change's scope: a documentation change gets the documentation checks, one module gets its own tests, a shared type gets the typecheck. As soon as the change exists and those checks pass, commit and push — the pushed branch is the deliverable, and an unpushed tree does not survive the run's end. Only then run the expensive checks, once, and fix forward with further commits and pushes. Never start an operation whose expected duration does not fit the time you have left minus what a commit, a push and the description need: push what there is and say plainly what is unverified instead. The description's validation names exactly what ran; what did not run is CI's to gate, and you say so.`;
|
|
233
|
+
|
|
209
234
|
const CODING_SYSTEM = `You are Switchboard's coding agent, operating from a Slack request.
|
|
210
235
|
|
|
211
236
|
You work inside a dedicated workspace directory with bash, read_file, and write_file tools. ${SANDBOX_TOOLCHAIN}
|
|
@@ -220,10 +245,13 @@ Workflow for shipping a PR:
|
|
|
220
245
|
1. Clone the repo into the workspace if it's not already there (use gh or git; both are authenticated on this host). Orient with a few BATCHED commands (tree + the relevant files in one call), not file-by-file exploration.
|
|
221
246
|
2. Create a branch with a descriptive name.
|
|
222
247
|
3. Implement the change. Match the surrounding code's style and conventions.
|
|
223
|
-
4.
|
|
224
|
-
5. Commit with a clear message and push the branch.
|
|
225
|
-
6.
|
|
226
|
-
7.
|
|
248
|
+
4. Prove the change with the cheapest checks that can (CHECKS BY COST below): the linter and the tests nearest the files you touched, the documentation checks for a documentation change.
|
|
249
|
+
5. Commit with a clear message and push the branch — before any full suite, build or full verification.
|
|
250
|
+
6. Then, if the budget allows, run the project's expensive checks once and fix forward with further commits and pushes.
|
|
251
|
+
7. Call the submit_pr_description tool with the typed description object (content contract below) — every time, bringing forward the context you gained while implementing. Switchboard renders the PR body from your object at the pushed head and opens (or updates) the pull request itself: do NOT open a PR yourself, with \`gh\` or any API call.
|
|
252
|
+
8. Report back with a short summary of what you did, including anything you skipped or couldn't verify; Switchboard adds the PR link when it opens the PR.
|
|
253
|
+
|
|
254
|
+
${CHECKS_BY_COST}
|
|
227
255
|
|
|
228
256
|
${NEVER_MERGE}
|
|
229
257
|
|
|
@@ -237,7 +265,7 @@ ${SHOW_FILES}
|
|
|
237
265
|
|
|
238
266
|
${NOTEPAD}
|
|
239
267
|
|
|
240
|
-
|
|
268
|
+
${statusCardRule('"Clone repo and read the diff", "Run the test suite"')}
|
|
241
269
|
|
|
242
270
|
If the request doesn't name a repository and you can't infer it, ask for it instead of guessing.
|
|
243
271
|
Report outcomes faithfully: if tests fail or a step was skipped, say so plainly.
|
|
@@ -262,11 +290,14 @@ Environment notes:
|
|
|
262
290
|
Workflow for shipping a change:
|
|
263
291
|
1. Create a branch with a descriptive name off the bound branch.
|
|
264
292
|
2. Implement the change. Match the surrounding code's style and conventions.
|
|
265
|
-
3.
|
|
266
|
-
4. Commit with a clear message and push the branch with \`git push -u origin <branch
|
|
267
|
-
5.
|
|
268
|
-
6. Call the
|
|
269
|
-
7.
|
|
293
|
+
3. Prove the change with the cheapest checks that can (CHECKS BY COST below): the linter and the tests nearest the files you touched, the documentation checks for a documentation change (dependencies are already present).
|
|
294
|
+
4. Commit with a clear message and push the branch with \`git push -u origin <branch>\` — before any full suite, build or full verification.
|
|
295
|
+
5. Then, if the budget allows, run the project's expensive checks once and fix forward with further commits and pushes.
|
|
296
|
+
6. Call the \`diff_digest\` tool to get a distilled summary of your change — per-file churn, totals, and risky-file flags. It is a distilled summary, not the raw diff: use it to shape the description you submit next — which files the Tour must walk, what belongs in risks.
|
|
297
|
+
7. Call the submit_pr_description tool with the typed description object (content contract below) — every time. Switchboard renders the PR body from your object at the pushed head and opens (or updates) the pull request itself: do NOT open a PR yourself, with any API call.
|
|
298
|
+
8. Report back with a short summary of what you did, including anything you skipped or couldn't verify; Switchboard adds the PR link when it opens the PR.
|
|
299
|
+
|
|
300
|
+
${CHECKS_BY_COST}
|
|
270
301
|
|
|
271
302
|
${NEVER_MERGE}
|
|
272
303
|
|
|
@@ -280,7 +311,7 @@ ${SHOW_FILES}
|
|
|
280
311
|
|
|
281
312
|
${NOTEPAD}
|
|
282
313
|
|
|
283
|
-
|
|
314
|
+
${statusCardRule()}
|
|
284
315
|
|
|
285
316
|
Report outcomes faithfully: if tests fail or a step was skipped, say so plainly.
|
|
286
317
|
${FENCED_CONTENT_RULE}
|
|
@@ -301,11 +332,14 @@ THE REPOSITORY IS ALREADY CLONED at \`/workspace/checkout\` — seeded from the
|
|
|
301
332
|
Workflow for shipping a change:
|
|
302
333
|
1. Create a branch with a descriptive name off the current branch.
|
|
303
334
|
2. Implement the change. Match the surrounding code's style and conventions.
|
|
304
|
-
3.
|
|
305
|
-
4. Commit with a clear message and push the branch with \`git push -u origin <branch
|
|
306
|
-
5.
|
|
307
|
-
6. Call the
|
|
308
|
-
7.
|
|
335
|
+
3. Prove the change with the cheapest checks that can (CHECKS BY COST below): the linter and the tests nearest the files you touched, the documentation checks for a documentation change (dependencies are already present).
|
|
336
|
+
4. Commit with a clear message and push the branch with \`git push -u origin <branch>\` — before any full suite, build or full verification.
|
|
337
|
+
5. Then, if the budget allows, run the project's expensive checks once and fix forward with further commits and pushes.
|
|
338
|
+
6. Call the \`diff_digest\` tool to get a distilled summary of your change — per-file churn, totals, and risky-file flags. It is a distilled summary, not the raw diff: use it to shape the description you submit next — which files the Tour must walk, what belongs in risks.
|
|
339
|
+
7. Call the submit_pr_description tool with the typed description object (content contract below) — every time. Switchboard renders the PR body from your object at the pushed head and opens (or updates) the pull request itself: do NOT open a PR yourself, with \`gh\` or any API call.
|
|
340
|
+
8. Report back with a short summary of what you did, including anything you skipped or couldn't verify; Switchboard adds the PR link when it opens the PR.
|
|
341
|
+
|
|
342
|
+
${CHECKS_BY_COST}
|
|
309
343
|
|
|
310
344
|
${NEVER_MERGE}
|
|
311
345
|
|
|
@@ -319,7 +353,7 @@ ${SHOW_FILES}
|
|
|
319
353
|
|
|
320
354
|
${NOTEPAD}
|
|
321
355
|
|
|
322
|
-
|
|
356
|
+
${statusCardRule()}
|
|
323
357
|
|
|
324
358
|
Report outcomes faithfully: if tests fail or a step was skipped, say so plainly.
|
|
325
359
|
${FENCED_CONTENT_RULE}
|
|
@@ -384,7 +418,7 @@ ${REVIEW_VERDICT_INSTRUCTION}
|
|
|
384
418
|
|
|
385
419
|
${NOTEPAD}
|
|
386
420
|
|
|
387
|
-
|
|
421
|
+
${statusCardRule('"Gather the diff and the files", "Analyze the change", "Post the verdict"')}
|
|
388
422
|
|
|
389
423
|
${FENCED_CONTENT_RULE}
|
|
390
424
|
Your final message is posted to Slack. Lead with a one-line verdict, then the findings.`;
|
|
@@ -417,7 +451,7 @@ ${REVIEW_VERDICT_INSTRUCTION}
|
|
|
417
451
|
|
|
418
452
|
${NOTEPAD}
|
|
419
453
|
|
|
420
|
-
|
|
454
|
+
${statusCardRule('"Gather the diff and the files", "Analyze the change", "Post the verdict"')}
|
|
421
455
|
|
|
422
456
|
${FENCED_CONTENT_RULE}
|
|
423
457
|
Your final message is posted to Slack. Lead with a one-line verdict, then the findings.`;
|
|
@@ -451,7 +485,7 @@ ${REVIEW_VERDICT_INSTRUCTION}
|
|
|
451
485
|
|
|
452
486
|
${NOTEPAD}
|
|
453
487
|
|
|
454
|
-
|
|
488
|
+
${statusCardRule('"Gather the diff and the files", "Analyze the change", "Post the verdict"')}
|
|
455
489
|
|
|
456
490
|
${FENCED_CONTENT_RULE}
|
|
457
491
|
Your final message is posted to Slack. Lead with a one-line verdict, then the findings.`;
|
|
@@ -469,7 +503,7 @@ How to work:
|
|
|
469
503
|
2. Prefer primary sources; corroborate a surprising claim with a second source.
|
|
470
504
|
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
505
|
|
|
472
|
-
|
|
506
|
+
${statusCardRule('"Search the sources", "Write the answer"')}
|
|
473
507
|
|
|
474
508
|
${FENCED_CONTENT_RULE}
|
|
475
509
|
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 +519,8 @@ Answer directly and concisely. Use Slack-friendly formatting (no markdown header
|
|
|
485
519
|
|
|
486
520
|
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
521
|
|
|
522
|
+
${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.
|
|
523
|
+
|
|
488
524
|
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
525
|
|
|
490
526
|
// The explore agent (docs/reference/specs/agent-explore.md): a long, read-only
|
|
@@ -512,7 +548,7 @@ READ-ONLY: NEVER open a pull request, and never commit or push — no branch, no
|
|
|
512
548
|
|
|
513
549
|
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
550
|
|
|
515
|
-
|
|
551
|
+
${statusCardRule('"Clone and install", "Time the full suite"')}
|
|
516
552
|
|
|
517
553
|
${NOTEPAD}
|
|
518
554
|
|
|
@@ -83,15 +83,18 @@ interface BoundFields {
|
|
|
83
83
|
userName?: string;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
/** The actor
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
export function
|
|
94
|
-
|
|
86
|
+
/** The actor every gate in the dispatch path decides on: `resolveChatActor`
|
|
87
|
+
* over the message, with config's grants lookup. Every `canRunAgent` /
|
|
88
|
+
* `canUseRepo` / `canManageRepos` / `canEditChannelConfig` question asks
|
|
89
|
+
* about THIS actor, never `msg.userId`: a relayed message (`postedBy`,
|
|
90
|
+
* authorization.md item 14) decides on the app ∩ the person, a bound
|
|
91
|
+
* credential (`authenticatedAs`, item 15) on the credential alone — naming
|
|
92
|
+
* a person on a run never lends the run the person's grants. */
|
|
93
|
+
export function chatActorOf(
|
|
94
|
+
config: { grantsFor: GrantsLookup },
|
|
95
|
+
msg: { userId: string; channelId: string; threadKey: string; postedBy?: string } & BoundFields,
|
|
96
|
+
): Actor {
|
|
97
|
+
return resolveChatActor(msg, (id) => config.grantsFor(id));
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
const CHAT_SURFACES: Readonly<Record<string, ActorSurface>> = {
|
|
@@ -100,6 +103,11 @@ const CHAT_SURFACES: Readonly<Record<string, ActorSurface>> = {
|
|
|
100
103
|
mcp: "mcp",
|
|
101
104
|
cli: "cli",
|
|
102
105
|
schedule: "schedule",
|
|
106
|
+
// The web chat (record 0043): a browser session speaks as text, so its
|
|
107
|
+
// `access:<sub>` is a chat credential too — the browser actor, its baseline
|
|
108
|
+
// and its entry. A service token never reaches the chat (the dashboard gate
|
|
109
|
+
// serves it `/api/*` only), so `access:svc:` is no chat surface.
|
|
110
|
+
access: "access-browser",
|
|
103
111
|
};
|
|
104
112
|
|
|
105
113
|
/** A chat message's `userId` is already namespaced by its adapter (`slack:U…`,
|
|
@@ -63,6 +63,14 @@ export function principalOf(actor: Actor): Actor {
|
|
|
63
63
|
return current;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/** The channels the decision's person is in: the root principal's
|
|
67
|
+
* `memberOf`, as `selfIdsOf` reads its `self` — a fact from the channel
|
|
68
|
+
* directory, never a grant; absent → the empty set. */
|
|
69
|
+
export function memberChannelsOf(actor: Actor): ReadonlySet<string> {
|
|
70
|
+
return principalOf(actor).memberOf ?? EMPTY_SET;
|
|
71
|
+
}
|
|
72
|
+
const EMPTY_SET: ReadonlySet<string> = new Set();
|
|
73
|
+
|
|
66
74
|
/** The ids that mean "me" for a decision (record 0042): the root principal's
|
|
67
75
|
* `self` when it carries one — its own id and the person a dashboard session
|
|
68
76
|
* is linked to — else its id alone. Never read by a grant check. */
|
|
@@ -117,6 +125,7 @@ export function evaluateCondition(
|
|
|
117
125
|
grants: Grants,
|
|
118
126
|
selfIds: readonly string[],
|
|
119
127
|
attributes: ResourceAttributes,
|
|
128
|
+
memberOf: ReadonlySet<string> = EMPTY_SET,
|
|
120
129
|
): boolean {
|
|
121
130
|
switch (condition.kind) {
|
|
122
131
|
case "has-grant": {
|
|
@@ -124,11 +133,13 @@ export function evaluateCondition(
|
|
|
124
133
|
return grant !== undefined && hasAction(grants.actions, grant);
|
|
125
134
|
}
|
|
126
135
|
case "member-of":
|
|
127
|
-
// Granted the channel,
|
|
136
|
+
// Granted the channel, in the channel (the directory's fact on the
|
|
137
|
+
// actor), or the channel is public (a run's stamped
|
|
128
138
|
// visibility). `unknown` — no stamp, a directory failure — is never
|
|
129
139
|
// public: fail-closed.
|
|
130
140
|
return (
|
|
131
|
-
(attributes.channelId !== undefined &&
|
|
141
|
+
(attributes.channelId !== undefined &&
|
|
142
|
+
(holds(grants.channels, attributes.channelId) || memberOf.has(attributes.channelId))) ||
|
|
132
143
|
attributes.channelVisibility === "public"
|
|
133
144
|
);
|
|
134
145
|
case "is-self":
|
|
@@ -154,7 +165,8 @@ export function evaluateRule(rule: Rule, actor: Actor, resource: Resource): bool
|
|
|
154
165
|
if (rule.originVisibility && !rule.originVisibility.includes(attributes.visibility)) return false;
|
|
155
166
|
const grants = effectiveGrants(actor);
|
|
156
167
|
const selfIds = selfIdsOf(actor);
|
|
157
|
-
|
|
168
|
+
const memberOf = memberChannelsOf(actor);
|
|
169
|
+
return rule.when.every((condition) => evaluateCondition(condition, grants, selfIds, attributes, memberOf));
|
|
158
170
|
}
|
|
159
171
|
|
|
160
172
|
/** `authorize` over an explicit (validated) table. Tests use it to drive
|
|
@@ -173,9 +185,10 @@ export function authorizeWith(rules: readonly Rule[], actor: Actor, action: Acti
|
|
|
173
185
|
if (forOrigin.length === 0) return deny("origin-visibility");
|
|
174
186
|
const grants = effectiveGrants(actor);
|
|
175
187
|
const selfIds = selfIdsOf(actor);
|
|
188
|
+
const memberOf = memberChannelsOf(actor);
|
|
176
189
|
let reason: DenyReason | undefined;
|
|
177
190
|
for (const rule of forOrigin) {
|
|
178
|
-
const failed = rule.when.find((condition) => !evaluateCondition(condition, grants, selfIds, attributes));
|
|
191
|
+
const failed = rule.when.find((condition) => !evaluateCondition(condition, grants, selfIds, attributes, memberOf));
|
|
179
192
|
if (!failed) return ALLOW;
|
|
180
193
|
reason ??= FAILURE_REASON[failed.kind];
|
|
181
194
|
}
|
|
@@ -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>();
|
|
@@ -57,6 +57,15 @@ export interface Actor {
|
|
|
57
57
|
readonly self?: readonly string[];
|
|
58
58
|
/** The linked person, for display and the audit line (`asUser`); absent when unlinked. */
|
|
59
59
|
readonly asUser?: { readonly id: string; readonly name?: string };
|
|
60
|
+
/**
|
|
61
|
+
* The channels the platform says the actor's person is in: the
|
|
62
|
+
* channel directory's `channelsOf`, resolved once with the actor — a fact
|
|
63
|
+
* about the person, never a grant from config. `member-of` reads it beside
|
|
64
|
+
* `grants.channels`, so a private channel's runs and config open to the
|
|
65
|
+
* people in it. Absent (no directory, a lookup failure, an unlinked session)
|
|
66
|
+
* → nothing: fail-closed, exactly today's behaviour.
|
|
67
|
+
*/
|
|
68
|
+
readonly memberOf?: ReadonlySet<string>;
|
|
60
69
|
}
|
|
61
70
|
|
|
62
71
|
/** `<group>:<read|write|exec>` plus the non-command actions. A plain
|
|
@@ -184,4 +193,7 @@ export type Predicate =
|
|
|
184
193
|
export interface ChannelDirectory {
|
|
185
194
|
info(channelId: string): Promise<{ visibility: ChannelVisibility }>;
|
|
186
195
|
isMember(actorId: string, channelId: string): Promise<boolean | "unknown">;
|
|
196
|
+
/** Every channel the actor is in, platform-namespaced — what the resolver puts on
|
|
197
|
+
* `Actor.memberOf`; `unknown` when the adapter cannot say (fail-closed). */
|
|
198
|
+
channelsOf(actorId: string): Promise<ReadonlySet<string> | "unknown">;
|
|
187
199
|
}
|
|
@@ -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 {
|
|
@@ -134,6 +150,8 @@ export interface CoordinatorInstance {
|
|
|
134
150
|
/** The bound credential behind the person (authorization.md item 15), when
|
|
135
151
|
* there was one: every child is authorized under ITS grants, as the request was. */
|
|
136
152
|
authenticatedAs?: string;
|
|
153
|
+
/** The app that relayed the request for the person (authorization.md item 14), when one did: every child is authorized under app ∩ person, as the request was. */
|
|
154
|
+
postedBy?: string;
|
|
137
155
|
channelId: string;
|
|
138
156
|
channelName?: string;
|
|
139
157
|
/** The requesting thread: where the card lives and where a generated plan's
|
|
@@ -244,7 +262,7 @@ export function isCoordinatorInstance(v: unknown): v is CoordinatorInstance {
|
|
|
244
262
|
if (r.kind !== "ship") return false;
|
|
245
263
|
if (!isText(r.userId) || !isText(r.channelId) || !isText(r.threadKey)) return false;
|
|
246
264
|
if (!isOptionalText(r.userName) || !isOptionalText(r.channelName) || !isOptionalText(r.sourceUrl)) return false;
|
|
247
|
-
if (!isOptionalText(r.authenticatedAs)) return false;
|
|
265
|
+
if (!isOptionalText(r.authenticatedAs) || !isOptionalText(r.postedBy)) return false;
|
|
248
266
|
if (typeof r.repo !== "string" || !REPO_SLUG.test(r.repo)) return false;
|
|
249
267
|
if (!isText(r.branch) || !isOptionalText(r.base)) return false;
|
|
250
268
|
if (!isFinite(r.createdAt)) return false;
|
|
@@ -310,6 +328,27 @@ export type RunFinishedSend =
|
|
|
310
328
|
| { kind: "no-binding"; instance: string }
|
|
311
329
|
| { kind: "failed"; instance: string; type: string; reason: string };
|
|
312
330
|
|
|
331
|
+
/** The one send per settled head (http-ingress.md item 12): best effort like
|
|
332
|
+
* `sendRunFinished` — a refusal is answered, never thrown, and the parent's
|
|
333
|
+
* bounded merge wait times out on its own. */
|
|
334
|
+
export async function sendChecksSettled(
|
|
335
|
+
workflow: WorkflowSender | undefined,
|
|
336
|
+
instance: string,
|
|
337
|
+
headSha: string,
|
|
338
|
+
settledAt: number,
|
|
339
|
+
): Promise<RunFinishedSend> {
|
|
340
|
+
if (!workflow) return { kind: "no-binding", instance };
|
|
341
|
+
const type = checksSettledEventType(headSha);
|
|
342
|
+
const payload: ChecksSettledPayload = { headSha, settledAt };
|
|
343
|
+
try {
|
|
344
|
+
const handle = await workflow.get(instance);
|
|
345
|
+
await handle.sendEvent({ type, payload });
|
|
346
|
+
return { kind: "sent", instance, type };
|
|
347
|
+
} catch (err) {
|
|
348
|
+
return { kind: "failed", instance, type, reason: err instanceof Error ? err.message : String(err) };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
313
352
|
/** The one send per committed terminal record (run-history item 47). */
|
|
314
353
|
export async function sendRunFinished(
|
|
315
354
|
workflow: WorkflowSender | undefined,
|