@coreplane/switchboard 1.241.0 → 1.243.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 +45 -13
- package/dist/assets/deploy/cloudflare-memory/worker.ts +86 -29
- package/dist/assets/deploy/cloudflare-resident/worker.ts +298 -24
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +38 -5
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/project.json +5 -5
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +61 -48
- package/dist/assets/src/config/profile.ts +68 -3
- package/dist/assets/src/core/authz/actor.ts +12 -9
- package/dist/assets/src/core/authz/authorize.ts +17 -4
- package/dist/assets/src/core/authz/policy.ts +4 -0
- package/dist/assets/src/core/authz/types.ts +12 -0
- package/dist/assets/src/core/budgets.ts +313 -0
- package/dist/assets/src/core/coordinator/contract.ts +3 -1
- package/dist/assets/src/core/coordinator/driver.ts +0 -10
- package/dist/assets/src/core/costs.ts +21 -80
- package/dist/assets/src/core/harness/scope.ts +21 -0
- package/dist/assets/src/core/modelPricing.ts +212 -0
- package/dist/assets/src/core/prDescriptionTypes.ts +29 -24
- package/dist/assets/src/core/reviewVerdict.ts +7 -0
- package/dist/assets/src/core/runEvents.ts +48 -5
- package/dist/assets/src/core/runFriction.ts +2 -1
- package/dist/assets/src/core/runLedger/types.ts +2 -0
- package/dist/assets/src/core/runRecord.ts +51 -0
- package/dist/assets/src/core/runUsage.ts +158 -47
- package/dist/assets/src/core/schedules.ts +3 -0
- package/dist/assets/src/core/ship/contract.ts +4 -2
- package/dist/assets/src/core/ship/coordinator.ts +101 -84
- package/dist/assets/src/core/ship/handoff.ts +9 -0
- package/dist/assets/src/execution/bashTimeout.ts +8 -5
- 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 +43 -4
- 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 +443 -446
- package/dist/assets/web/dist/assets/{AppShell-DEy5jRuS.js → AppShell-IDMGG6yi.js} +1 -1
- package/dist/assets/web/dist/assets/CostsPage-CwXOmkeQ.js +2 -0
- package/dist/assets/web/dist/assets/{DeliveryPage-BGpPr5xr.js → DeliveryPage-n1tz5I_v.js} +1 -1
- package/dist/assets/web/dist/assets/HomePage-PRxjQiGG.js +2 -0
- package/dist/assets/web/dist/assets/{NotFoundPage-DtmQM2gD.js → NotFoundPage-C97EhJcQ.js} +1 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-BT9RhFZ7.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-C-48fxat.js → ResidentDetailPage-DACalNVF.js} +1 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-SHPdu6uZ.js +1 -0
- package/dist/assets/web/dist/assets/RunFoldRow-0SdOmOr5.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-BaFS2p8I.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-DYI-iALj.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-CtNGNnhN.js → RunsTabs-BUfdk0lH.js} +1 -1
- package/dist/assets/web/dist/assets/ScheduledPage-DJ8HiCPt.js +1 -0
- package/dist/assets/web/dist/assets/SettingsPage-DLiN5IgY.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-DPWE1JmO.js → StatusDot-Dw0T1M-P.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DKhSRH9t.js → Tooltip-BbLuIAiS.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-DicUG96U.js +1 -0
- package/dist/assets/web/dist/assets/{angular-html-DgSK1qvr.js → angular-html-oBNfPJR0.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-D3gNdiSG.js → angular-ts-BvNwsyWA.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-CkHCYM8I.js → apl-CNUdRlYf.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-DLm45axt.js → astro-Zb0NriSe.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-BKa-VE-c.js → blade-qPRVheqq.js} +1 -1
- package/dist/assets/web/dist/assets/{c-C8NCNWai.js → c-D8Awx4YO.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-DqDQ7IKH.js → chapel-Bt72Mhsx.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-j4vCD6AJ.js → cobol-BOBacexg.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-C6oDVH-0.js → coffee-E4u0liHW.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-Bd3A1Rtc.js → cpp-q2sLNlul.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-CUOgOw0d.js → crystal-DyWqUnlb.js} +1 -1
- package/dist/assets/web/dist/assets/{css-RYljyv7G.js → css-CQY0hFsD.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-BnJXKHCY.js → dist-twkFmSUY.js} +2 -2
- package/dist/assets/web/dist/assets/durationTone-BobbycC-.js +1 -0
- package/dist/assets/web/dist/assets/{edge-BJhNdQDc.js → edge-C1MwhJkX.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-g1AWOAfT.js → elixir-Bb3YbHfn.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-CER5e4Dz.js → elm-DAN9IGQw.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-D5aOmMQf.js → erb-BEB8Xlsj.js} +1 -1
- package/dist/assets/web/dist/assets/format-BldUwl_R.js +1 -0
- package/dist/assets/web/dist/assets/{git-rebase-D6rfV8jp.js → git-rebase-tqpjRfxO.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-BvDH3-mC.js → glimmer-js-Ccbo65zR.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-DChtNs9V.js → glimmer-ts-B6WBVMpE.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-BHlsWrxf.js → glsl-tRec3Fcu.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-BsaBHb53.js → graphql-P8kbxT4F.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-0lbDosCd.js → hack-H9Zhkagy.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-BKtco2vr.js → haml-DtEnpn7Z.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-BE_cj04z.js → handlebars-DkgPfoAz.js} +1 -1
- package/dist/assets/web/dist/assets/{html-BNcr8EVd.js → html-D30RXpIs.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-BsJt2Kei.js → html-derivative-DwozLrEx.js} +1 -1
- package/dist/assets/web/dist/assets/{http-BR5P8ER_.js → http-DXuzBAPm.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-CtHalpWS.js → hurl-d1UUJIt_.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-B_s5tKyq.js +1 -0
- package/dist/assets/web/dist/assets/{java-BXVdhN11.js → java-DL0gWf34.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-CyoShgNQ.js → javascript-Cl7vavnS.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-BQYZzHex.js → jinja-CfQOWMX9.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-DqrUa1Eq.js → jison-KdYirlqm.js} +1 -1
- package/dist/assets/web/dist/assets/{json-CQiTF9Jj.js → json-C9cDQ-Qj.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-Y16MkCYQ.js → jsx-CRx5NItd.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-zn16YONe.js → julia-CmsQsZQl.js} +1 -1
- package/dist/assets/web/dist/assets/{just-DcVeN_MN.js → just-CsM3Q8TE.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-BNNoF-WP.js → latex-BXCh5YRX.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-DAi6qHzn.js → liquid-BF2vwK8p.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-BswS0axw.js → lua-DcMBATrl.js} +1 -1
- package/dist/assets/web/dist/assets/main-B4kEF3Sg.css +1 -0
- package/dist/assets/web/dist/assets/{main-B3is4sk6.js → main-d-w-tIKt.js} +2 -2
- package/dist/assets/web/dist/assets/{marko-C-4hkmQZ.js → marko-88MndKvG.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-0ZaHUkS7.js → mdc-DJ4kVAd8.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-DBDg5tOR.js → nginx-CP6mRgtV.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-BVMz49Yw.js → nim-QQfj3fpF.js} +1 -1
- package/dist/assets/web/dist/assets/{org-1hO_K6ya.js → org-Bke3eBzc.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-CVRQLvVk.js → perl-Bkh0N7Kz.js} +1 -1
- package/dist/assets/web/dist/assets/{php-K9nCrB9n.js → php-Dnv1Piya.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-CTxOcmO3.js → pug-D6peFFI7.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-CmCkfG5q.js → qml-D8PEs-C-.js} +1 -1
- package/dist/assets/web/dist/assets/{r-pP47Xn1X.js → r-B1EL9b_j.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-BpW6r3tC.js → razor-BsMTIh2b.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-CINgdY4N.js → regexp-HhvC8spD.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-BZIUEq2Q.js → rst-D5paAxpg.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-nzAOxz6r.js → ruby-BrQwhLrl.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-QIS1bFth.js → sas-afot2B1o.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-BuhBBVNt.js → scss-Efm-mwuG.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-CQc1vXbk.js → shellscript-BK0Vv5fT.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-CoubCAUv.js → shellsession-BGqlMC7N.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-CPRzlder.js → soy-M0b4UwGM.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-W9krb8-9.js → sql-sxe6IE9j.js} +1 -1
- package/dist/assets/web/dist/assets/sseReplay-C9m_EB8J.js +9 -0
- package/dist/assets/web/dist/assets/{stata-CuISJEC0.js → stata-nPF_ddLP.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-CSet7584.js → surrealql-D_GrC6u7.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-ChXTSYwK.js → svelte-DwL1AtNP.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-Bm55v62k.js → templ-7s7LTDkc.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-CICMX8Gj.js → tex-Bx-5fMxe.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-CA1UzWyB.js → ts-tags-DUMJnke_.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-Dy04HNbv.js → tsx-BN8biPDe.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-BBHsVnVD.js → twig-8nIu84TN.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-Dvc-wVBT.js → typescript-BooSPq_S.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-Dvlqx3_q.js → typst-CTBiBsem.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-zWi1MNU2.js → vue-C6Ft4Lea.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-D4YxT3An.js → vue-html-Ba36dD5D.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-0YE7uQlH.js → vue-vine-NkFexVo2.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-gm-iksZB.js → xml-C_THnHXZ.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-D_W5BcrY.js → xsl-D8G5xqjY.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-CHmZ21wZ.js → yaml-jAMIJzge.js} +1 -1
- package/dist/cli.js +4121 -1902
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/CostsPage-BuHD-Bv7.js +0 -2
- package/dist/assets/web/dist/assets/HomePage-dKBvWh-E.js +0 -2
- package/dist/assets/web/dist/assets/PendingTurnRow-CrEAWx5b.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CBBIjPOa.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-B6vxjcBI.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-CQjd3Cz-.js +0 -6
- package/dist/assets/web/dist/assets/RunsIndexPage-Cy9G6D0p.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-Ci1Ygqnc.js +0 -1
- package/dist/assets/web/dist/assets/SettingsPage-DxmJvAmT.js +0 -1
- package/dist/assets/web/dist/assets/SlackMark-VDNs7Vjh.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-DEN5bSQf.js +0 -1
- package/dist/assets/web/dist/assets/durationTone-DXG-3R7_.js +0 -1
- package/dist/assets/web/dist/assets/indexRow-BD1VT8o8.js +0 -1
- package/dist/assets/web/dist/assets/localIso-L06jV29p.js +0 -1
- package/dist/assets/web/dist/assets/main-DP_zSemY.css +0 -1
- package/dist/assets/web/dist/assets/sseReplay-yji8a1wM.js +0 -9
package/dist/assets/project.json
CHANGED
|
@@ -168,11 +168,11 @@
|
|
|
168
168
|
"when": "Part of `check:consistency`."
|
|
169
169
|
},
|
|
170
170
|
"clock:gen": {
|
|
171
|
-
"does": "Regenerates
|
|
171
|
+
"does": "Regenerates both clock allowlists from the tree: wall-clock reads (empty) and duration literals outside `src/core/budgets.ts`.",
|
|
172
172
|
"when": "Part of `fix`."
|
|
173
173
|
},
|
|
174
174
|
"clock:check": {
|
|
175
|
-
"does": "No production file reads the wall clock directly
|
|
175
|
+
"does": "No production file reads the wall clock directly or gained a duration literal outside `src/core/budgets.ts`; both allowlists match the tree.",
|
|
176
176
|
"when": "Part of `check:consistency`."
|
|
177
177
|
},
|
|
178
178
|
"docs:changed": {
|
|
@@ -204,15 +204,15 @@
|
|
|
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": {
|
|
215
|
-
"does": "Load harness: `-- history|resident|sandbox|e2e|cards|provider|pi|route`.",
|
|
215
|
+
"does": "Load harness: `-- history|resident|sandbox|e2e|cards|provider|pi|route|door`.",
|
|
216
216
|
"when": "Capacity receipts (docs/reference/specs/load-harness.md)."
|
|
217
217
|
}
|
|
218
218
|
}
|
package/dist/assets/source.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Agent definitions. An agent is a system prompt + toolset + machine class + wall-clock budget.
|
|
2
2
|
import type { Effort } from "../effort.js";
|
|
3
3
|
import { BASH_TIMEOUT_MAX_MS } from "../execution/bashTimeout.js";
|
|
4
|
+
import { ASKS, RUNAWAY_TURNS_PER_MINUTE, runawayTurnCap, type LoopPreset } from "../core/budgets.js";
|
|
4
5
|
import { CONTRACT_HEADING, CONTRACT_SECTION_HEADINGS, PR_TITLE_GUARD } from "../core/ship/contract.js";
|
|
5
6
|
// Which model runs it is resolved separately by the config layers, so any
|
|
6
7
|
// agent can run on any configured provider/model.
|
|
@@ -41,24 +42,16 @@ export function machineNeedsRepo(machine: MachineClass): boolean {
|
|
|
41
42
|
export const IDENTITIES = ["none", "read", "write"] as const;
|
|
42
43
|
export type Identity = (typeof IDENTITIES)[number];
|
|
43
44
|
|
|
44
|
-
/** The
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
|
|
49
|
-
* says so (docs/reference/specs/harness-pi.md item 15). */
|
|
50
|
-
export const RUNAWAY_TURNS_PER_MINUTE = 6;
|
|
51
|
-
|
|
52
|
-
/** The turn cap a wall clock implies: `maxMinutes × RUNAWAY_TURNS_PER_MINUTE`.
|
|
53
|
-
* Every preset that runs the loop derives its `maxTurns` from this, so the
|
|
54
|
-
* cap is never a number a good run reaches — the minutes are the budget. */
|
|
55
|
-
export function runawayTurnCap(maxMinutes: number): number {
|
|
56
|
-
return maxMinutes * RUNAWAY_TURNS_PER_MINUTE;
|
|
57
|
-
}
|
|
45
|
+
/** The wall clocks live in `src/core/budgets.ts` (docs/decisions/0046): a
|
|
46
|
+
* preset's ask, the turn cap derived from it and every allowance are rows
|
|
47
|
+
* there, and this registry reads them. Re-exported for the readers that
|
|
48
|
+
* learned them here. */
|
|
49
|
+
export { RUNAWAY_TURNS_PER_MINUTE, runawayTurnCap };
|
|
58
50
|
|
|
59
|
-
/** A loop-running preset's budget as one fact
|
|
60
|
-
* guard derived from it. */
|
|
61
|
-
function loopBudget(
|
|
51
|
+
/** A loop-running preset's budget as one fact read from the module: the wall
|
|
52
|
+
* clock it asks for, and the runaway guard derived from it. */
|
|
53
|
+
function loopBudget(preset: LoopPreset): Pick<AgentDef, "maxMinutes" | "maxTurns"> {
|
|
54
|
+
const maxMinutes = ASKS[preset];
|
|
62
55
|
return { maxMinutes, maxTurns: runawayTurnCap(maxMinutes) };
|
|
63
56
|
}
|
|
64
57
|
|
|
@@ -150,15 +143,18 @@ export function statusCardRule(examples = '"Implement the fix", "Run the test su
|
|
|
150
143
|
);
|
|
151
144
|
}
|
|
152
145
|
|
|
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.
|
|
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
|
|
146
|
+
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. The body is a fixed-size MAP for the reader with everything for agents collapsed under it; every field is capped in visible characters (a link's URL is not counted) and the tool refuses an object over a cap naming the field and the count — cut and resubmit. 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. BEFORE authoring the pointers, load the \`pr-description\` skill with use_skill — it defines how to choose at most seven pointers, the mechanical anchor rules and what goes below the fold; follow it for every PR.
|
|
147
|
+
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 why), add what you changed, and anchor the pointers 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.
|
|
155
148
|
- **title**: the PR title — one line naming the change, specific enough to pick out of a PR list.
|
|
156
|
-
- **TL;DR** (\`tldr\`, rendered first): two sentences for a
|
|
157
|
-
- **
|
|
158
|
-
- **
|
|
159
|
-
- **
|
|
160
|
-
- **
|
|
161
|
-
- **
|
|
149
|
+
- **TL;DR** (\`tldr\`, rendered first, ≤300): two sentences for a reader with zero context — what this PR does and why it matters.
|
|
150
|
+
- **Why** (\`why\`, ≤400): the problem and the motivation, with the triggering issue/request, the record and the stack position hyperlinked. Why, never what: the diff shows what.
|
|
151
|
+
- **Where to look** (\`pointers\`, 1 to 7): the files a reviewer would open first, in reading order, each { label ≤60, text ≤160, optional risk ≤100, anchor } with the anchor a { path, from, to } line range at your pushed head, rendered as a link (never embedded code). One pointer per idea, never per file; when the change has more ideas than seven, keep the seven whose mistake would cost most.
|
|
152
|
+
- **Feedback wanted** (\`feedbackWanted\`, ≤200): the one or two things you want the reviewer's judgement on.
|
|
153
|
+
- **Risk** (\`risk\`, ≤300): what breaks if this is wrong, the blast radius, the rollback; over 400 changed lines, say so and name the split you considered.
|
|
154
|
+
- **Verified** (\`verified\`, ≤200): one line for a person — which suites ran and passed, what is still human-gated.
|
|
155
|
+
- **Decisions** (\`decisions\`, 0 to 10, collapsed): non-obvious choices as { title, rationale ≤400 } — the alternative rejected and the fact that decided it.
|
|
156
|
+
- **Validation** (\`validation\`, 1 to 30 criteria, collapsed): what you tested and the actual results as { criterion ≤200, proof ≤300 } rows (test ids, commands run, pass/fail), plus how the reviewer can verify it.
|
|
157
|
+
- **For agents** (\`agentNotes\`, optional, ≤2000, collapsed): what a reviewing agent needs that a person does not — the rebase you did, generated files to skip, the command that reproduces the bug.`;
|
|
162
158
|
|
|
163
159
|
// Both coding prompts carry this verbatim: coding runs hold a write-scoped
|
|
164
160
|
// token where a merge is one command away, so the boundary is spelled out the
|
|
@@ -223,6 +219,14 @@ export const FENCED_CONTENT_RULE =
|
|
|
223
219
|
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.
|
|
224
220
|
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.`;
|
|
225
221
|
|
|
222
|
+
// Every coding prompt carries this verbatim (docs/reference/specs/agent-coding.md
|
|
223
|
+
// item 13): the order of checks and the push. Three plan children died at their
|
|
224
|
+
// budget in one evening with finished work unpushed because each ran the
|
|
225
|
+
// project's most expensive checks first; the rule is the runner's to hold, not
|
|
226
|
+
// a line every requester remembers to paste. Stack-agnostic on purpose — the
|
|
227
|
+
// classes are by duration, the project's own scripts and CI say which is which.
|
|
228
|
+
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.`;
|
|
229
|
+
|
|
226
230
|
const CODING_SYSTEM = `You are Switchboard's coding agent, operating from a Slack request.
|
|
227
231
|
|
|
228
232
|
You work inside a dedicated workspace directory with bash, read_file, and write_file tools. ${SANDBOX_TOOLCHAIN}
|
|
@@ -237,10 +241,13 @@ Workflow for shipping a PR:
|
|
|
237
241
|
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.
|
|
238
242
|
2. Create a branch with a descriptive name.
|
|
239
243
|
3. Implement the change. Match the surrounding code's style and conventions.
|
|
240
|
-
4.
|
|
241
|
-
5. Commit with a clear message and push the branch.
|
|
242
|
-
6.
|
|
243
|
-
7.
|
|
244
|
+
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.
|
|
245
|
+
5. Commit with a clear message and push the branch — before any full suite, build or full verification.
|
|
246
|
+
6. Then, if the budget allows, run the project's expensive checks once and fix forward with further commits and pushes.
|
|
247
|
+
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.
|
|
248
|
+
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.
|
|
249
|
+
|
|
250
|
+
${CHECKS_BY_COST}
|
|
244
251
|
|
|
245
252
|
${NEVER_MERGE}
|
|
246
253
|
|
|
@@ -279,11 +286,14 @@ Environment notes:
|
|
|
279
286
|
Workflow for shipping a change:
|
|
280
287
|
1. Create a branch with a descriptive name off the bound branch.
|
|
281
288
|
2. Implement the change. Match the surrounding code's style and conventions.
|
|
282
|
-
3.
|
|
283
|
-
4. Commit with a clear message and push the branch with \`git push -u origin <branch
|
|
284
|
-
5.
|
|
285
|
-
6. Call the
|
|
286
|
-
7.
|
|
289
|
+
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).
|
|
290
|
+
4. Commit with a clear message and push the branch with \`git push -u origin <branch>\` — before any full suite, build or full verification.
|
|
291
|
+
5. Then, if the budget allows, run the project's expensive checks once and fix forward with further commits and pushes.
|
|
292
|
+
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.
|
|
293
|
+
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.
|
|
294
|
+
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.
|
|
295
|
+
|
|
296
|
+
${CHECKS_BY_COST}
|
|
287
297
|
|
|
288
298
|
${NEVER_MERGE}
|
|
289
299
|
|
|
@@ -318,11 +328,14 @@ THE REPOSITORY IS ALREADY CLONED at \`/workspace/checkout\` — seeded from the
|
|
|
318
328
|
Workflow for shipping a change:
|
|
319
329
|
1. Create a branch with a descriptive name off the current branch.
|
|
320
330
|
2. Implement the change. Match the surrounding code's style and conventions.
|
|
321
|
-
3.
|
|
322
|
-
4. Commit with a clear message and push the branch with \`git push -u origin <branch
|
|
323
|
-
5.
|
|
324
|
-
6. Call the
|
|
325
|
-
7.
|
|
331
|
+
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).
|
|
332
|
+
4. Commit with a clear message and push the branch with \`git push -u origin <branch>\` — before any full suite, build or full verification.
|
|
333
|
+
5. Then, if the budget allows, run the project's expensive checks once and fix forward with further commits and pushes.
|
|
334
|
+
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.
|
|
335
|
+
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.
|
|
336
|
+
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.
|
|
337
|
+
|
|
338
|
+
${CHECKS_BY_COST}
|
|
326
339
|
|
|
327
340
|
${NEVER_MERGE}
|
|
328
341
|
|
|
@@ -504,7 +517,7 @@ Your tools work without a workspace: the GitHub tools — \`github_repos\` (the
|
|
|
504
517
|
|
|
505
518
|
${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
519
|
|
|
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
|
|
520
|
+
You cannot run commands, clone repositories, edit code, or review pull requests, and you cannot search the web. Other Switchboard agents can: for a code change or a pull request tell the user to re-send with \`agent:ship\` (it makes the change, opens the PR and loops review); for a PR review, \`agent:review\`; for a web-research question, \`agent:research\` (e.g. "\`agent:ship in acme/api: fix the failing login test\`", "\`agent:research compare X and Y\`"). Delete an issue only when the user explicitly asked to delete it (closing is an update).`;
|
|
508
521
|
|
|
509
522
|
// The explore agent (docs/reference/specs/agent-explore.md): a long, read-only
|
|
510
523
|
// investigation — "run our CI locally and validate the claims", "how long does
|
|
@@ -527,7 +540,7 @@ THE DELIVERABLE IS A CLAIM TABLE. Turn the request into the claims it makes or a
|
|
|
527
540
|
|
|
528
541
|
TIME. Your budget is up to two hours — less when a boundary or the request's \`budget:\` directive clipped it, which the runtime-config block above says — and the wrap-up warning tells you when to stop starting new checks. A single command is capped at ${BASH_TIMEOUT_MAX_MS / 60_000} minutes (pass the bash tool's \`timeoutMs\`, up to ${BASH_TIMEOUT_MAX_MS} ms, for a long one). A job that needs longer — a full suite, a build, a pipeline run — is started detached and polled across tool calls: \`setsid -f sh -c '<command> > /tmp/job.log 2>&1; echo $? > /tmp/job.exit'\`, then \`tail -n 40 /tmp/job.log\` and \`cat /tmp/job.exit\` on later calls (a plain background job dies with the command that started it; a \`setsid -f\` job outlives it). Batch commands into few tool calls; never explore file by file.
|
|
529
542
|
|
|
530
|
-
READ-ONLY: NEVER open a pull request, and never commit or push — no branch, no \`gh pr create\`, no PR or issue write of any kind. You hold a read credential and your job is to find out, not to change. If the investigation shows a change is needed, say exactly what and where in your write-up and point the user at \`agent:
|
|
543
|
+
READ-ONLY: NEVER open a pull request, and never commit or push — no branch, no \`gh pr create\`, no PR or issue write of any kind. You hold a read credential and your job is to find out, not to change. If the investigation shows a change is needed, say exactly what and where in your write-up and point the user at \`agent:ship\` (it makes the change, opens the PR and loops review).
|
|
531
544
|
|
|
532
545
|
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.
|
|
533
546
|
|
|
@@ -621,7 +634,7 @@ const WORK_PRESETS = {
|
|
|
621
634
|
machine: "none",
|
|
622
635
|
identity: "none",
|
|
623
636
|
maxTokens: 16000,
|
|
624
|
-
...loopBudget(
|
|
637
|
+
...loopBudget("general"),
|
|
625
638
|
},
|
|
626
639
|
coding: {
|
|
627
640
|
name: "coding",
|
|
@@ -631,7 +644,7 @@ const WORK_PRESETS = {
|
|
|
631
644
|
seededSystem: CODING_SYSTEM_SEEDED,
|
|
632
645
|
toolset: "full",
|
|
633
646
|
maxTokens: 64000,
|
|
634
|
-
...loopBudget(
|
|
647
|
+
...loopBudget("coding"),
|
|
635
648
|
// No built-in effort: the deployment decides (`defaults.efforts.coding`,
|
|
636
649
|
// `config set channel efforts.coding=…`, or `effort:` per request).
|
|
637
650
|
machine: "repo-resident",
|
|
@@ -652,7 +665,7 @@ const WORK_PRESETS = {
|
|
|
652
665
|
machine: "repo-resident",
|
|
653
666
|
identity: "read", // a read-scoped token and a read-only worktree: it cannot post or push from inside
|
|
654
667
|
maxTokens: 64000,
|
|
655
|
-
...loopBudget(
|
|
668
|
+
...loopBudget("review"), // a safety net — typical reviews land in ~5 minutes
|
|
656
669
|
effort: "medium", // fast turns; one big-context pass does the deep work
|
|
657
670
|
},
|
|
658
671
|
ship: {
|
|
@@ -681,7 +694,7 @@ const WORK_PRESETS = {
|
|
|
681
694
|
// reviewed pull request, never code landing on main.
|
|
682
695
|
maxTurns: 1,
|
|
683
696
|
maxTokens: 16000,
|
|
684
|
-
maxMinutes:
|
|
697
|
+
maxMinutes: ASKS.ship,
|
|
685
698
|
},
|
|
686
699
|
research: {
|
|
687
700
|
name: "research",
|
|
@@ -692,7 +705,7 @@ const WORK_PRESETS = {
|
|
|
692
705
|
machine: "none", // web I/O only; no workspace is provisioned
|
|
693
706
|
identity: "none",
|
|
694
707
|
maxTokens: 24000,
|
|
695
|
-
...loopBudget(
|
|
708
|
+
...loopBudget("research"),
|
|
696
709
|
effort: "medium",
|
|
697
710
|
},
|
|
698
711
|
explore: {
|
|
@@ -706,7 +719,7 @@ const WORK_PRESETS = {
|
|
|
706
719
|
machine: "repo-cold",
|
|
707
720
|
identity: "read", // a read-scoped token: it can clone and read, never push — whatever the caller holds
|
|
708
721
|
maxTokens: 64000,
|
|
709
|
-
...loopBudget(
|
|
722
|
+
...loopBudget("explore"),
|
|
710
723
|
// No built-in effort: the deployment decides, as for coding.
|
|
711
724
|
},
|
|
712
725
|
} satisfies Record<string, AgentDef>;
|
|
@@ -731,7 +744,7 @@ export const AGENTS: Record<string, AgentDef> = {
|
|
|
731
744
|
// spawn exactly those — so no child runs that the record did not name.
|
|
732
745
|
routable: false,
|
|
733
746
|
maxTokens: 32000,
|
|
734
|
-
...loopBudget(
|
|
747
|
+
...loopBudget("conductor"), // long enough to outlast a coding child; every child is capped by what remains of it
|
|
735
748
|
// No built-in effort: the deployment decides, as for coding.
|
|
736
749
|
},
|
|
737
750
|
};
|
|
@@ -73,10 +73,40 @@ export function budgetedAgent(agent: AgentDef, profile: RunProfile): AgentDef {
|
|
|
73
73
|
|
|
74
74
|
// ---- boundaries: a scope caps, never grants -----------------------------------
|
|
75
75
|
|
|
76
|
+
/** The blast-radius classes a scope may name as its `confirm`
|
|
77
|
+
* (docs/decisions/0044-a-routed-write-is-confirmed-in-proportion-to-its-blast-radius.md):
|
|
78
|
+
* the first class on the ladder the door hands back instead of running.
|
|
79
|
+
* `exec` is on the ladder for the comparison but not settable — a test or
|
|
80
|
+
* build never asks — and `never` is refused until the door's write misbind
|
|
81
|
+
* rate has been measured; the validator names both reasons. */
|
|
82
|
+
export type ConfirmClass = "write" | "destructive";
|
|
83
|
+
export const CONFIRM_CLASSES: readonly ConfirmClass[] = ["write", "destructive"];
|
|
84
|
+
|
|
85
|
+
/** The classes on the door's ladder — the command registry's `BlastRadius`,
|
|
86
|
+
* spelled here rather than imported: a type import still drags the registry's
|
|
87
|
+
* whole graph into every program that compiles this near-leaf module, the
|
|
88
|
+
* Workers included. The door indexes the ladder with the registry's type
|
|
89
|
+
* (`routedRunsAtOnce`), so a class added there without a rung here fails to
|
|
90
|
+
* compile at the one place the two vocabularies meet. */
|
|
91
|
+
export type ConfirmLadderClass = "read" | "exec" | "write" | "destructive";
|
|
92
|
+
|
|
93
|
+
/** The ladder the door compares on, `read < exec < write < destructive`: among
|
|
94
|
+
* the last three, from asking most to asking least — a `confirm` of `write`
|
|
95
|
+
* hands back every write and every destructive write, `destructive` only the
|
|
96
|
+
* destructive ones. A read is on the order so the comparison is total, but the
|
|
97
|
+
* door never asks for one. */
|
|
98
|
+
export const CONFIRM_ORDER: Record<ConfirmLadderClass, number> = { read: 0, exec: 1, write: 2, destructive: 3 };
|
|
99
|
+
|
|
100
|
+
/** The door's confirm class when no scope sets one: every routed write is
|
|
101
|
+
* handed back, a read or a test run at once — the door as it was before the
|
|
102
|
+
* axis existed. Attributed to `built-in`, a word that appears in no config. */
|
|
103
|
+
export const BUILT_IN_CONFIRM: ConfirmClass = "write";
|
|
104
|
+
|
|
76
105
|
/** A cap on the three axes that any scope may set (`defaults`, `channels.<id>`,
|
|
77
|
-
* `users.<id>`; docs/reference/specs/routing-and-config.md item 2)
|
|
78
|
-
* axis caps nothing. A boundary never
|
|
79
|
-
* and the policy table's one question
|
|
106
|
+
* `users.<id>`; docs/reference/specs/routing-and-config.md item 2), and the
|
|
107
|
+
* door's `confirm` beside them. An absent axis caps nothing. A boundary never
|
|
108
|
+
* grants: it is not a fourth grants axis, and the policy table's one question
|
|
109
|
+
* (who may run a preset) is unchanged. */
|
|
80
110
|
export interface Boundary {
|
|
81
111
|
/** The most a run may have, in minutes; at least 2 (the bash tool keeps a 60 s reserve). */
|
|
82
112
|
maxMinutes?: number;
|
|
@@ -84,6 +114,11 @@ export interface Boundary {
|
|
|
84
114
|
maxIdentity?: Identity;
|
|
85
115
|
/** The machine classes a run may execute on; a preset's class must be in every layer's set. */
|
|
86
116
|
machines?: MachineClass[];
|
|
117
|
+
/** The first blast-radius class a command the router bound is handed back
|
|
118
|
+
* at instead of run (record 0044). Not a run cap: it rides the boundary for
|
|
119
|
+
* its scopes and its intersection-toward-caution, is read by the door alone
|
|
120
|
+
* (`effectiveConfirm`), and never enters `intersectBoundaries` or a profile. */
|
|
121
|
+
confirm?: ConfirmClass;
|
|
87
122
|
}
|
|
88
123
|
|
|
89
124
|
/** One layer's boundary with the scope it came from, in resolution order:
|
|
@@ -143,6 +178,36 @@ export function intersectBoundaries(layers: readonly ScopedBoundary[]): Effectiv
|
|
|
143
178
|
return out.maxMinutes || out.maxIdentity || out.machines ? out : undefined;
|
|
144
179
|
}
|
|
145
180
|
|
|
181
|
+
/** Where the door's confirm class came from: a scope that set it, or the
|
|
182
|
+
* built-in default when none did. Local to the confirm axis — `BoundaryScope`
|
|
183
|
+
* itself is unchanged, since no run cap is ever attributed to `built-in`. */
|
|
184
|
+
export type ConfirmScope = BoundaryScope | "built-in";
|
|
185
|
+
|
|
186
|
+
/** The door's decision on the confirm axis: the class and the scope it names. */
|
|
187
|
+
export interface EffectiveConfirm {
|
|
188
|
+
value: ConfirmClass;
|
|
189
|
+
scope: ConfirmScope;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The confirm axis intersected over a request's path, apart from the run caps:
|
|
194
|
+
* the earliest class on `CONFIRM_ORDER` any layer named — the most cautious,
|
|
195
|
+
* since a scope's value is the most permissive answer it allows and the org's
|
|
196
|
+
* is therefore a floor no layer below it can loosen — attributed to the layer
|
|
197
|
+
* that set it (on a tie the first layer named keeps it, the least specific
|
|
198
|
+
* scope, as `intersectBoundaries` does). No layer set one: the built-in
|
|
199
|
+
* `write`, attributed to `built-in`. Pure over the same layers `intersectBoundaries` takes.
|
|
200
|
+
*/
|
|
201
|
+
export function effectiveConfirm(layers: readonly ScopedBoundary[]): EffectiveConfirm {
|
|
202
|
+
let out: EffectiveConfirm | undefined;
|
|
203
|
+
for (const { scope, boundary } of layers) {
|
|
204
|
+
if (boundary.confirm !== undefined && (!out || CONFIRM_ORDER[boundary.confirm] < CONFIRM_ORDER[out.value])) {
|
|
205
|
+
out = { value: boundary.confirm, scope };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return out ?? { value: BUILT_IN_CONFIRM, scope: "built-in" };
|
|
209
|
+
}
|
|
210
|
+
|
|
146
211
|
/**
|
|
147
212
|
* The boundaries on a child's path with its parent's remaining wall clock as
|
|
148
213
|
* one more layer (docs/reference/specs/routing-and-config.md item 20): the
|
|
@@ -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>> = {
|
|
@@ -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
|
}
|
|
@@ -82,6 +82,10 @@ export const POLICY: readonly Rule[] = [
|
|
|
82
82
|
{ action: "friction:write", resource: "command", when: [grant("friction:write")] },
|
|
83
83
|
|
|
84
84
|
// ── costs ────────────────────────────────────────────────────────────────
|
|
85
|
+
// `costs by` reads the snapshot's arithmetic: what every browser session
|
|
86
|
+
// holds (every group's read) and what a Slack user or a token is granted —
|
|
87
|
+
// never a chat baseline, since a by-user table names who spent what.
|
|
88
|
+
{ action: "costs:read", resource: "command", when: [grant("costs:read")] },
|
|
85
89
|
// `costs snapshot` reads both billing providers and replaces what every
|
|
86
90
|
// viewer of the costs page sees: the grant, never a baseline (the admins'
|
|
87
91
|
// `all` and a named `grants` entry hold it).
|
|
@@ -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
|
}
|