@coreplane/switchboard 1.244.0 → 1.245.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 +6 -3
- package/dist/assets/deploy/cloudflare/worker.ts +2 -0
- package/dist/assets/deploy/cloudflare-resident/threadErr.ts +337 -0
- package/dist/assets/deploy/cloudflare-resident/tsconfig.json +1 -1
- package/dist/assets/deploy/cloudflare-resident/worker.ts +31 -262
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +5 -10
- package/dist/assets/deploy/secrets.manifest.json +12 -0
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +4 -4
- package/dist/assets/src/config/profile.ts +17 -1
- package/dist/assets/src/core/authz/types.ts +8 -0
- package/dist/assets/src/core/budgets.ts +25 -0
- package/dist/assets/src/core/reviewVerdict.ts +101 -24
- package/dist/assets/src/core/runEvents.ts +26 -1
- package/dist/assets/src/core/runRecord.ts +34 -0
- package/dist/assets/src/core/ship/coordinator.ts +25 -21
- package/dist/assets/src/execution/residentRefresh.ts +25 -6
- package/dist/assets/web/dist/.vite/manifest.json +481 -462
- package/dist/assets/web/dist/assets/AppShell-qqs_p96-.js +1 -0
- package/dist/assets/web/dist/assets/CostsPage-Dd9aXxxI.js +2 -0
- package/dist/assets/web/dist/assets/DeliveryPage-D90FTjYq.js +1 -0
- package/dist/assets/web/dist/assets/HomePage-VLTFopnl.js +2 -0
- package/dist/assets/web/dist/assets/InputMenu-lQP3ia9D.js +1 -0
- package/dist/assets/web/dist/assets/NotFoundPage-8ViskpN3.js +1 -0
- package/dist/assets/web/dist/assets/PendingTurnRow-D2gHef5v.js +1 -0
- package/dist/assets/web/dist/assets/ResidentDetailPage-nWLN8BOd.js +1 -0
- package/dist/assets/web/dist/assets/ResidentsIndexPage-D3Q48CJW.js +1 -0
- package/dist/assets/web/dist/assets/{RunFoldRow-BPMQxTcl.js → RunFoldRow-CHy_IfZG.js} +1 -1
- package/dist/assets/web/dist/assets/RunRoutePage-C6kX5om4.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-DjxGj1Q5.js +1 -0
- package/dist/assets/web/dist/assets/RunsTabs-WyeG5tKb.js +1 -0
- package/dist/assets/web/dist/assets/ScheduledPage-B8D5AU4d.js +1 -0
- package/dist/assets/web/dist/assets/SettingSelect-UTHsqUUS.js +1 -0
- package/dist/assets/web/dist/assets/SettingsPage-BoW7Zjzt.js +1 -0
- package/dist/assets/web/dist/assets/StatusDot-BGHureCJ.js +1 -0
- package/dist/assets/web/dist/assets/Tooltip-CiP8qPd7.js +1 -0
- package/dist/assets/web/dist/assets/UnitRoutePage-2zEeRBP1.js +1 -0
- package/dist/assets/web/dist/assets/{angular-html-Dail07rr.js → angular-html-qXhb9ti8.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-BReExio8.js → angular-ts-DzvdyoUZ.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-DhNO6Yyx.js → apl-Ck7Ru2Sm.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-BYotTY4C.js → astro-CwehvReb.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-mUxTtD1X.js → blade-WSyVbSFK.js} +1 -1
- package/dist/assets/web/dist/assets/{c-DhuiwcSQ.js → c-DMHI1wDz.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-GD2vFtRi.js → chapel-BF8AYK8_.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-BhFDwkes.js → cobol-UAfImuWu.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-DWUgbE0r.js → coffee-D54hi96d.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-CO9XFjGW.js → cpp-BRWfLgBc.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-B9pVurpn.js → crystal-CSqC5deT.js} +1 -1
- package/dist/assets/web/dist/assets/{css-CFVVV6lI.js → css-D4ZuRFGk.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-BtPs2AkQ.js → dist-73ZMn_Mg.js} +2 -2
- package/dist/assets/web/dist/assets/{durationTone-BobbycC-.js → durationTone-D51enaGR.js} +1 -1
- package/dist/assets/web/dist/assets/{edge-BDhIDQIb.js → edge-DDPtNbZc.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-DG3J8WoD.js → elixir-BTQkQfsJ.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-FiOdiaOE.js → elm-B_PkLzGZ.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-vxbtW503.js → erb-DfjIMVM_.js} +1 -1
- package/dist/assets/web/dist/assets/{eventSource-tRprhqm7.js → eventSource-By0B_w4S.js} +1 -1
- package/dist/assets/web/dist/assets/{git-rebase-DINz8M3H.js → git-rebase-DP8wmn7K.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-yann6r9i.js → glimmer-js-G12VK2gK.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-CXTbrZn_.js → glimmer-ts-D3XiSI8E.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-DNYC-hq0.js → glsl-DOKAhMj-.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-CwzhHzi9.js → graphql-CJB_fmON.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-BBXF1cwR.js → hack-BHqtRWap.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-oEqBcsTy.js → haml-DwotyarS.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-YE5thkT1.js → handlebars-B6fDaA3U.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-DCT1GYtQ.js → html-derivative-C5bpQmmI.js} +1 -1
- package/dist/assets/web/dist/assets/{html-Dp4jRQ1j.js → html-s52taj36.js} +1 -1
- package/dist/assets/web/dist/assets/{http-BPcGbmzJ.js → http-99zC935r.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-CQnNHwyd.js → hurl-Bgk-4-0x.js} +1 -1
- package/dist/assets/web/dist/assets/{java-Dm8Ovj6W.js → java-CuV_c5I0.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-Btm9oJK9.js → javascript-CZlOvSMQ.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-VZYjWF_U.js → jinja-BuXeZ4Tu.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-Dcmjxa9x.js → jison-DIuP0hkG.js} +1 -1
- package/dist/assets/web/dist/assets/{json-DoWiOGZ8.js → json-CZ74vfoD.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-Dvmo55ma.js → jsx-BNjRs8Vj.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-d8AtfV26.js → julia-OCE-fFOg.js} +1 -1
- package/dist/assets/web/dist/assets/{just-Cc5EStA-.js → just-CtvD87WF.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-B5fKKIH7.js → latex-C3kf86OR.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-DUYI9JND.js → liquid-B1WI--Kp.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-BhMwKWpw.js → lua-DtoT0gKg.js} +1 -1
- package/dist/assets/web/dist/assets/main-ClluP-Qf.js +28 -0
- package/dist/assets/web/dist/assets/main-CpkwzHX6.css +1 -0
- package/dist/assets/web/dist/assets/{marko-Cnf2cPAi.js → marko-BE2MC6Yt.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-FDDXoir9.js → mdc-BwO4weXJ.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-q-8U_1jR.js → nginx-BTuOgGVC.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-DIA0TUWs.js → nim-CiIUM-oV.js} +1 -1
- package/dist/assets/web/dist/assets/{org-CR88hy7S.js → org-L78DEWB-.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-Bkd8N7ja.js → perl-KqHOm4Ft.js} +1 -1
- package/dist/assets/web/dist/assets/{php-BOs84mvy.js → php-C41XbEkn.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-BbpZBb3w.js → pug-B12-4i-k.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-D2jQpjFE.js → qml--fw2Up5c.js} +1 -1
- package/dist/assets/web/dist/assets/{r-DKzcAQ22.js → r-Db1JBXrC.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-SOQMWcC9.js → razor-BYpVBgbu.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-B6KK4W8d.js → regexp-BMqGLSPc.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-kPSg-NPf.js → rst-B63n7-Sa.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-cKS-JYLH.js → ruby-D3VI8UYv.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-DNxzE5gs.js → sas-DxRf8ZAR.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-DGcHQ5OO.js → scss-DRRA6d6A.js} +1 -1
- package/dist/assets/web/dist/assets/seed-DFfT5S_N.js +6 -0
- package/dist/assets/web/dist/assets/{settingsApi-priOPiwu.js → settingsApi-Dm6eaQcu.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-CRZeZBa5.js → shellscript-CoOBe3_y.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-DSNEL0HX.js → shellsession-DOV1HYxf.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-zisNbkDO.js → soy-Acpmpp1W.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-MG06KMMb.js → sql-DeT9N-fA.js} +1 -1
- package/dist/assets/web/dist/assets/{sseReplay-DaRtc7nI.js → sseReplay-DPXVxhL5.js} +3 -3
- package/dist/assets/web/dist/assets/{stata-CB_wKwv9.js → stata-CbxXAMO4.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-DYdSt7re.js → surrealql-BdKbi4hc.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-DHO-F7c7.js → svelte-tAcjDM7R.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-CUL5L4Lr.js → templ-CObINNJj.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-_JNEKjVW.js → tex-BYwpZT1J.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags--9EpEV_8.js → ts-tags-BciREL5R.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-BcNq2x2u.js → tsx-CRiv3Pr3.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-EsY5Dk_D.js → twig-DuZZ1Ej0.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-CyA_HiUH.js → typescript-jkHz_5HZ.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-SzaHlDt4.js → typst-DOac4Gal.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-D3cn5acV.js → vue-CgOX5wXA.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-6MxhUxOa.js → vue-html-Duu99VNw.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-qJnqhgo9.js → vue-vine-Bmj_wvRJ.js} +1 -1
- package/dist/assets/web/dist/assets/wallClock-DD_fPuSY.js +1 -0
- package/dist/assets/web/dist/assets/{xml-DyRpC7xv.js → xml-BtUIVeWH.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-BXMRPAsg.js → xsl-wX71n1E5.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-BNmPCtAN.js → yaml-CdLdXvmz.js} +1 -1
- package/dist/cli.js +1474 -684
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/AppShell-Ck4hSZDi.js +0 -1
- package/dist/assets/web/dist/assets/CostsPage-DvhjDMka.js +0 -2
- package/dist/assets/web/dist/assets/DeliveryPage-DBlI_Yq2.js +0 -1
- package/dist/assets/web/dist/assets/HomePage-XFWPxDiJ.js +0 -2
- package/dist/assets/web/dist/assets/NotFoundPage-CiGrVOyv.js +0 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-BHCSpsPf.js +0 -1
- package/dist/assets/web/dist/assets/ResidentDetailPage-C5USV_8P.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CSKisWnX.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-CZe9Yodw.js +0 -9
- package/dist/assets/web/dist/assets/RunsIndexPage-BfA333Qy.js +0 -1
- package/dist/assets/web/dist/assets/RunsTabs-BWRHzhtT.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-DBaChcJZ.js +0 -1
- package/dist/assets/web/dist/assets/SettingsPage-CqVi9-Ac.js +0 -1
- package/dist/assets/web/dist/assets/StatusDot-_Q_bmReh.js +0 -1
- package/dist/assets/web/dist/assets/Tooltip-Cafsgd4m.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-CdoBZwtf.js +0 -1
- package/dist/assets/web/dist/assets/main-C4366PZF.css +0 -1
- package/dist/assets/web/dist/assets/main-DlTNAC6c.js +0 -28
- package/dist/assets/web/dist/assets/seed-DeKSHrt2.js +0 -6
- package/dist/assets/web/dist/assets/wallClock-61NpVkY6.js +0 -1
package/dist/cli.js
CHANGED
|
@@ -1501,6 +1501,9 @@ function postStepLease(preset, remainingMs) {
|
|
|
1501
1501
|
if (remainingMs === void 0) return allowance;
|
|
1502
1502
|
return Math.min(allowance, Math.max(1, remainingMs / MINUTE_MS));
|
|
1503
1503
|
}
|
|
1504
|
+
function leaseMinimum(preset) {
|
|
1505
|
+
return ALLOWANCES.writeUp + postStepMinutes(preset) + 1;
|
|
1506
|
+
}
|
|
1504
1507
|
function turnLeaseMs(askMinutes, remainingMs) {
|
|
1505
1508
|
return Math.min(askMinutes * MINUTE_MS, Math.max(MINUTE_MS, remainingMs));
|
|
1506
1509
|
}
|
|
@@ -1547,13 +1550,14 @@ function fit(pipeline) {
|
|
|
1547
1550
|
const need = ALLOWANCES.provision + ASKS.coding + reserveMinutes({ kind: "coding", index: 0 }, pipeline);
|
|
1548
1551
|
return { ok: pipeline.maxMinutes >= need, need, have: pipeline.maxMinutes };
|
|
1549
1552
|
}
|
|
1550
|
-
var MINUTE_MS, DAY_MS, CONFIRMATION_TTL_MS, LOOP_PRESETS, ASKS, PRESET_FLOORS, FLOORS, SHIP_WAIT, ALLOWANCES, POST_STEP_MINUTES, WRAP_UP_WARNING, BASH_COMMAND, RUNAWAY_TURNS_PER_MINUTE, DEFAULT_GRANT, GRANT_RENEWALS_MAX;
|
|
1553
|
+
var MINUTE_MS, DAY_MS, CONFIRMATION_TTL_MS, FIRST_ATTACH_WAIT_MS, LOOP_PRESETS, ASKS, PRESET_FLOORS, FLOORS, SHIP_WAIT, ALLOWANCES, POST_STEP_MINUTES, WRAP_UP_WARNING, BASH_COMMAND, RUNAWAY_TURNS_PER_MINUTE, DEFAULT_GRANT, GRANT_RENEWALS_MAX;
|
|
1551
1554
|
var init_budgets = __esm({
|
|
1552
1555
|
"../../src/core/budgets.ts"() {
|
|
1553
1556
|
"use strict";
|
|
1554
1557
|
MINUTE_MS = 6e4;
|
|
1555
1558
|
DAY_MS = 24 * 60 * MINUTE_MS;
|
|
1556
1559
|
CONFIRMATION_TTL_MS = 10 * MINUTE_MS;
|
|
1560
|
+
FIRST_ATTACH_WAIT_MS = MINUTE_MS;
|
|
1557
1561
|
LOOP_PRESETS = ["general", "coding", "review", "research", "explore", "conductor"];
|
|
1558
1562
|
ASKS = {
|
|
1559
1563
|
general: 5,
|
|
@@ -2017,7 +2021,7 @@ var init_registry = __esm({
|
|
|
2017
2021
|
IDENTITIES = ["none", "read", "write"];
|
|
2018
2022
|
PR_DESCRIPTION_TEMPLATE = `PR description \u2014 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 \u2014 \`npm run check:pr-title -- "<title>"\` \u2014 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 \u2014 cut and resubmit. Prose is unwrapped \u2014 no hard line breaks inside a paragraph. Always hyperlink the triggering issue/request. Never fabricate validation \u2014 state exactly what you ran and the real result. BEFORE authoring the pointers, load the \`pr-description\` skill with use_skill \u2014 it defines how to choose at most seven pointers, the mechanical anchor rules and what goes below the fold; follow it for every PR.
|
|
2019
2023
|
EVERY PR includes one that already exists when you push \u2014 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 \u2014 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.
|
|
2020
|
-
- **title
|
|
2024
|
+
- **title** (\u226472 characters in all): the changelog line \u2014 \`type(scope): what a reader can now do or expect\`, one change, one clause, present tense. The type and the scope spend the same budget as the description, so the description is short; the body carries the rest. The tool refuses a longer title naming the count, and so does the gate in CI.
|
|
2021
2025
|
- **TL;DR** (\`tldr\`, rendered first, \u2264300): two sentences for a reader with zero context \u2014 what this PR does and why it matters.
|
|
2022
2026
|
- **Why** (\`why\`, \u2264400): the problem and the motivation, with the triggering issue/request, the record and the stack position hyperlinked. Why, never what: the diff shows what.
|
|
2023
2027
|
- **Where to look** (\`pointers\`, 1 to 7): the files a reviewer would open first, in reading order, each { label \u226460, text \u2264160, optional risk \u2264100, 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.
|
|
@@ -2157,7 +2161,7 @@ ${statusCardRule()}
|
|
|
2157
2161
|
Report outcomes faithfully: if tests fail or a step was skipped, say so plainly.
|
|
2158
2162
|
${FENCED_CONTENT_RULE}
|
|
2159
2163
|
Your final message is posted to Slack \u2014 keep it readable, lead with the outcome.`;
|
|
2160
|
-
REVIEW_VERDICT_INSTRUCTION = `VERDICT: before your final message, call the submit_verdict tool exactly once with \`approve\` (no
|
|
2164
|
+
REVIEW_VERDICT_INSTRUCTION = `VERDICT: before your final message, call the submit_verdict tool exactly once with \`approve\` (no finding at or above the severity to address remains \u2014 the level in force for this run, \`minor\` by default: a major or a minor finding means \`request_changes\`; nits alone never block) or \`request_changes\`, a one-line summary, \`head\` = the output of \`git rev-parse HEAD\` in the checkout you reviewed, and \`findings\` \u2014 every issue you report as a structured entry with a stable id you assign in order (F1, F2, \u2026), a severity of exactly blocking|major|minor|nit, the file (plus line when it points at one), and a one-line title. The findings array is the index of your review: the full explanation of each finding stays in your prose, keyed by the same ids. Switchboard writes the verdict as the first line of the GitHub comment itself and lists the findings under it; a review with no submitted verdict is posted as not approving, so never skip it. An \`approve\` carrying a finding at or above the severity to address is downgraded to \`request_changes\` and the tool's ack says so \u2014 approve only when every finding sits below the level. Do not write "LGTM" in your own text \u2014 the verdict line carries it.`;
|
|
2161
2165
|
REVIEW_SPEC_CHECK = `3. SPEC CONTRADICTION CHECK, when the repository has \`docs/reference/specs/\`: list the specs the change touches \u2014 \`npm run --silent specs:coverage -- --changed origin/<base>...HEAD\` (\`<base>\` is the PR's base branch) when the repository's package.json has that script, otherwise match the changed paths against each spec's \`- **Code**:\` / \`- **Tests**:\` header lines (a header path covers itself and everything beneath it). If the command fails for any reason \u2014 dependencies not installed, tsx missing, a cold checkout \u2014 fall back to matching the header lines by hand; never install dependencies or build to make it run. Then read ONLY those specs, never the whole specs tree \u2014 fold the reads into your gather batch where you can. For each touched spec, judge whether the diff contradicts a numbered behavior statement or a validation criterion: code that now does what the spec says it does not, a criterion whose named test the diff removed or retitled, a behavior the diff deleted that the spec still promises. A contradiction is a finding of severity \`minor\` or higher titled \`Spec contradiction \u2014 <spec file> item <n>: <what the code now does vs what the spec says>\`; a spec updated in the same diff to match the code is not a finding. A repository with no \`docs/reference/specs/\` has nothing to check \u2014 skip this step silently.
|
|
2162
2166
|
3a. TEST GUARD, in the same repositories: run \`npm run --silent specs:coverage -- --changed origin/<base>...HEAD --test-guard\` (fold it into the same batch). It compares every test file the diff touches at the base and at the head and prints one line per thing lost, in two classes. A \`test-guard: <file> \u2014 removed: \u2026\` line is deterministic \u2014 the test file deleted, an it/test/describe title gone with no new title to pair with, a skip/only/todo marker (\`.skip(\`, \`.only(\`, \`xit(\`, \`xdescribe(\`, \`it.todo(\`, \`test.todo(\`) on a test the base ran \u2014 and each one is a finding of severity \`minor\` or higher titled \`Test removed \u2014 <file>: <what>\` whose explanation quotes the guard's line exactly as printed. A \`test-guard: <file> \u2014 check: \u2026\` line is a heuristic \u2014 fewer \`expect(\` calls in the file, a title gone while another arrived (a rename or a split) \u2014 and you dispose of every one of them explicitly in your review, never silently: either "weakened", which makes it a finding at \`minor\`, or "refactor, verification intact" with one clause saying why. A line ending \`\u2014 allowed by <spec>\` is licensed by a spec change in the same diff and is neither; \`test-guard ok\` is nothing to report. If the command fails for any reason, judge the same facts from the diff by hand \u2014 a deleted test file, a removed title, a new skip marker, fewer assertions \u2014 and file each one the diff does not license the same way; never install or build to make it run.`;
|
|
2163
2167
|
REVIEW_UNIT_CONTRACT = ` 3b. UNIT CONTRACT, when this prompt carries a \`${CONTRACT_HEADING}\` block after the REVIEW TARGET block (its sub-headings, in order: ${CONTRACT_HEADINGS_LIST}): it is what the coding child was handed for this plan unit, rendered by Switchboard from the plan, and the diff is judged against it. Read the unit's Test scenarios and find each one in the diff: a test scenario the unit listed and the diff did not add is a finding of severity \`minor\` titled \`Contract \u2014 test scenario missing: <the scenario>\` \u2014 the same severity as a spec contradiction. For each spec row the block names, check its proof binding resolves in the diff; a named row the diff leaves untouched is disposed of out loud, as in 3a: one clause on why it needed no change, or a finding at \`minor\` titled \`Contract \u2014 spec row not updated: <spec> item <n>\`. A guard the block names that the diff weakens is the guard's own finding (3a). No \`${CONTRACT_HEADING}\` block in this prompt \u2192 nothing to check; skip this step silently.`;
|
|
@@ -2598,33 +2602,45 @@ var init_redact = __esm({
|
|
|
2598
2602
|
});
|
|
2599
2603
|
|
|
2600
2604
|
// ../../src/core/reviewVerdict.ts
|
|
2601
|
-
function
|
|
2605
|
+
function severityAtOrAbove(severity, level) {
|
|
2606
|
+
return isAddressSeverity(severity) && severityRank(severity) <= severityRank(level);
|
|
2607
|
+
}
|
|
2608
|
+
function resolveAddressSeverity(layers) {
|
|
2609
|
+
if (layers.run !== void 0) return { level: layers.run, source: "run" };
|
|
2610
|
+
if (layers.user !== void 0) return { level: layers.user, source: "user" };
|
|
2611
|
+
if (layers.channel !== void 0) return { level: layers.channel, source: "channel" };
|
|
2612
|
+
return { level: layers.org ?? DEFAULT_ADDRESS_SEVERITY, source: "org" };
|
|
2613
|
+
}
|
|
2614
|
+
function downgradeNote(d) {
|
|
2615
|
+
return `finding${d.findings.length === 1 ? "" : "s"} ${d.findings.join(", ")} at or above ${d.level}, the severity to address`;
|
|
2616
|
+
}
|
|
2617
|
+
function parseVerdictInput(input, opts = {}) {
|
|
2602
2618
|
const verdict = input.verdict;
|
|
2603
2619
|
if (verdict !== "approve" && verdict !== "request_changes") return null;
|
|
2620
|
+
const level = opts.addressSeverity ?? DEFAULT_ADDRESS_SEVERITY;
|
|
2604
2621
|
const summary = typeof input.summary === "string" ? oneLine2(input.summary) : "";
|
|
2605
2622
|
const head = normalizeHead(input.head);
|
|
2606
2623
|
const out = { verdict, summary };
|
|
2607
2624
|
if (head) out.head = head;
|
|
2608
2625
|
if (input.findings !== void 0) {
|
|
2609
|
-
const parsed = parseFindings(input.findings);
|
|
2626
|
+
const parsed = parseFindings(input.findings, level);
|
|
2610
2627
|
if (parsed.findings) out.findings = parsed.findings;
|
|
2611
2628
|
if (parsed.dropped.length) out.droppedFindings = parsed.dropped;
|
|
2612
|
-
if (out.verdict === "approve" && parsed.
|
|
2629
|
+
if (out.verdict === "approve" && parsed.gated.length > 0) {
|
|
2613
2630
|
out.verdict = "request_changes";
|
|
2614
|
-
out.
|
|
2615
|
-
|
|
2616
|
-
);
|
|
2631
|
+
out.downgraded = { findings: parsed.gated, level };
|
|
2632
|
+
out.summary = oneLine2(`${out.summary} [downgraded from approve: ${downgradeNote(out.downgraded)}]`);
|
|
2617
2633
|
}
|
|
2618
2634
|
}
|
|
2619
2635
|
return out;
|
|
2620
2636
|
}
|
|
2621
|
-
function parseFindings(value) {
|
|
2637
|
+
function parseFindings(value, level) {
|
|
2622
2638
|
if (!Array.isArray(value)) {
|
|
2623
|
-
return { dropped: ["findings: dropped \u2014 not an array"],
|
|
2639
|
+
return { dropped: ["findings: dropped \u2014 not an array"], gated: [] };
|
|
2624
2640
|
}
|
|
2625
2641
|
const findings = [];
|
|
2626
2642
|
const dropped = [];
|
|
2627
|
-
const
|
|
2643
|
+
const gated = [];
|
|
2628
2644
|
value.forEach((raw, i) => {
|
|
2629
2645
|
const parsed = parseFinding(raw);
|
|
2630
2646
|
if (parsed.finding) {
|
|
@@ -2634,9 +2650,9 @@ function parseFindings(value) {
|
|
|
2634
2650
|
}
|
|
2635
2651
|
const label = parsed.finding?.id ?? parsed.id ?? `findings[${i}]`;
|
|
2636
2652
|
const r = typeof raw === "object" && raw !== null ? raw : void 0;
|
|
2637
|
-
if (r?.severity
|
|
2653
|
+
if (severityAtOrAbove(r?.severity, level)) gated.push(`${label} (${r?.severity})`);
|
|
2638
2654
|
});
|
|
2639
|
-
return { findings, dropped,
|
|
2655
|
+
return { findings, dropped, gated };
|
|
2640
2656
|
}
|
|
2641
2657
|
function parseFinding(raw) {
|
|
2642
2658
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return { reason: "not an object" };
|
|
@@ -2745,13 +2761,17 @@ function redactVerdict(v, redact = redactSecrets) {
|
|
|
2745
2761
|
function redactDispositions(dispositions, redact = redactSecrets) {
|
|
2746
2762
|
return dispositions.map((d) => ({ findingId: d.findingId, disposition: d.disposition, note: redact(d.note) }));
|
|
2747
2763
|
}
|
|
2748
|
-
var FINDING_SEVERITIES, LGTM_TOKEN, CHANGES_TOKEN, NO_VERDICT_LINE, VERDICT_KINDS, DISPOSITION_KINDS, isRecordLike, REVIEW_POST_HEAD;
|
|
2764
|
+
var FINDING_SEVERITIES, ADDRESS_SEVERITIES, DEFAULT_ADDRESS_SEVERITY, isAddressSeverity, severityRank, LGTM_TOKEN, CHANGES_TOKEN, NO_VERDICT_LINE, VERDICT_KINDS, DISPOSITION_KINDS, isRecordLike, REVIEW_POST_HEAD;
|
|
2749
2765
|
var init_reviewVerdict = __esm({
|
|
2750
2766
|
"../../src/core/reviewVerdict.ts"() {
|
|
2751
2767
|
"use strict";
|
|
2752
2768
|
init_reviewedHead();
|
|
2753
2769
|
init_redact();
|
|
2754
2770
|
FINDING_SEVERITIES = ["blocking", "major", "minor", "nit"];
|
|
2771
|
+
ADDRESS_SEVERITIES = FINDING_SEVERITIES;
|
|
2772
|
+
DEFAULT_ADDRESS_SEVERITY = "minor";
|
|
2773
|
+
isAddressSeverity = (v) => ADDRESS_SEVERITIES.includes(v);
|
|
2774
|
+
severityRank = (s) => ADDRESS_SEVERITIES.indexOf(s);
|
|
2755
2775
|
LGTM_TOKEN = "LGTM:";
|
|
2756
2776
|
CHANGES_TOKEN = "Changes requested:";
|
|
2757
2777
|
NO_VERDICT_LINE = "No verdict submitted \u2014 not approving.";
|
|
@@ -2993,7 +3013,7 @@ function matchDispositions(findings, dispositions) {
|
|
|
2993
3013
|
const dropped = [...new Set(dispositions.filter((d) => !issued.has(d.findingId)).map((d) => d.findingId))];
|
|
2994
3014
|
return { matched, dropped };
|
|
2995
3015
|
}
|
|
2996
|
-
var MIN, PLAN_ID_PATTERN, PLAN_BRANCH, UNIT_REF, SLUG_MAX, INSTANCE_ID_MAX,
|
|
3016
|
+
var MIN, PLAN_ID_PATTERN, PLAN_BRANCH, UNIT_REF, SLUG_MAX, INSTANCE_ID_MAX, WAIT_MARGIN_MS, WAIT_CHUNK_MS, MERGE_WAIT_CHUNK_MS, BUSY_RETRY_MS;
|
|
2997
3017
|
var init_coordinator = __esm({
|
|
2998
3018
|
"../../src/core/ship/coordinator.ts"() {
|
|
2999
3019
|
"use strict";
|
|
@@ -3008,9 +3028,6 @@ var init_coordinator = __esm({
|
|
|
3008
3028
|
UNIT_REF = /\bU(\d+)\s+to\s+U(\d+)\b|\bU\d+\b/g;
|
|
3009
3029
|
SLUG_MAX = 24;
|
|
3010
3030
|
INSTANCE_ID_MAX = 100;
|
|
3011
|
-
ADDRESS_SEVERITIES = ["blocking", "major", "minor", "nit"];
|
|
3012
|
-
DEFAULT_ADDRESS_SEVERITY = "minor";
|
|
3013
|
-
isAddressSeverity = (v) => ADDRESS_SEVERITIES.includes(v);
|
|
3014
3031
|
WAIT_MARGIN_MS = SHIP_WAIT.marginMinutes * MIN;
|
|
3015
3032
|
WAIT_CHUNK_MS = SHIP_WAIT.chunkMinutes * MIN;
|
|
3016
3033
|
MERGE_WAIT_CHUNK_MS = SHIP_WAIT.mergeChunkMinutes * MIN;
|
|
@@ -3019,12 +3036,6 @@ var init_coordinator = __esm({
|
|
|
3019
3036
|
});
|
|
3020
3037
|
|
|
3021
3038
|
// ../../src/core/shipPipeline.ts
|
|
3022
|
-
function resolveAddressSeverity(layers) {
|
|
3023
|
-
if (layers.run !== void 0) return { level: layers.run, source: "run" };
|
|
3024
|
-
if (layers.user !== void 0) return { level: layers.user, source: "user" };
|
|
3025
|
-
if (layers.channel !== void 0) return { level: layers.channel, source: "channel" };
|
|
3026
|
-
return { level: layers.org ?? DEFAULT_ADDRESS_SEVERITY, source: "org" };
|
|
3027
|
-
}
|
|
3028
3039
|
function resolveGrant2(layers) {
|
|
3029
3040
|
const scoped = layers.user !== void 0 ? { grant: layers.user, source: "user" } : layers.channel !== void 0 ? { grant: layers.channel, source: "channel" } : { grant: layers.org ?? DEFAULT_GRANT, source: "org" };
|
|
3030
3041
|
if (layers.run === void 0) return scoped;
|
|
@@ -3052,7 +3063,7 @@ var init_shipPipeline = __esm({
|
|
|
3052
3063
|
init_registry();
|
|
3053
3064
|
init_budgets();
|
|
3054
3065
|
init_coordinator();
|
|
3055
|
-
|
|
3066
|
+
init_reviewVerdict();
|
|
3056
3067
|
SHIP_DEFAULT_MAX_ROUNDS = 3;
|
|
3057
3068
|
SHIP_DEFAULT_MAX_MINUTES = AGENTS.ship.maxMinutes;
|
|
3058
3069
|
}
|
|
@@ -3189,7 +3200,7 @@ function boundedByParent(boundary, parentRemainingMs) {
|
|
|
3189
3200
|
if (boundary?.maxMinutes && boundary.maxMinutes.value <= minutes) return boundary;
|
|
3190
3201
|
return { ...boundary, maxMinutes: { value: minutes, scope: "parent" } };
|
|
3191
3202
|
}
|
|
3192
|
-
function effectiveProfile(preset, directives, boundary) {
|
|
3203
|
+
function effectiveProfile(preset, directives, boundary, minimum) {
|
|
3193
3204
|
if (boundary?.maxIdentity && !identityWithin(preset.identity, boundary.maxIdentity.value)) {
|
|
3194
3205
|
return {
|
|
3195
3206
|
kind: "refused",
|
|
@@ -3222,6 +3233,12 @@ function effectiveProfile(preset, directives, boundary) {
|
|
|
3222
3233
|
minutes = boundary.maxMinutes.value;
|
|
3223
3234
|
boundedBy = boundary.maxMinutes.scope;
|
|
3224
3235
|
}
|
|
3236
|
+
if (minimum !== void 0 && minutes < minimum) {
|
|
3237
|
+
return {
|
|
3238
|
+
kind: "refused",
|
|
3239
|
+
refusal: { axis: "minutes", needs: minimum, have: minutes, scope: boundedBy ?? "defaults" }
|
|
3240
|
+
};
|
|
3241
|
+
}
|
|
3225
3242
|
return {
|
|
3226
3243
|
kind: "profile",
|
|
3227
3244
|
profile: {
|
|
@@ -3838,7 +3855,7 @@ function validateConfig(cfg) {
|
|
|
3838
3855
|
for (const key of unknownKeys2(cfg, CONFIG_KEYS)) throw new Error(`config.yaml: unknown key \`${key}\``);
|
|
3839
3856
|
validateScopeEfforts(cfg, "config.yaml");
|
|
3840
3857
|
validateBoundaries(cfg, "config.yaml");
|
|
3841
|
-
|
|
3858
|
+
validateScopeBlocks(cfg, "config.yaml");
|
|
3842
3859
|
validateHarnessWords(cfg, "config.yaml");
|
|
3843
3860
|
validateMcpServers(cfg, "config.yaml");
|
|
3844
3861
|
if (typeof cfg.organization !== "string" || cfg.organization.trim() === "") {
|
|
@@ -3862,6 +3879,7 @@ function validateConfig(cfg) {
|
|
|
3862
3879
|
if (cfg.runHistory !== void 0) validateRunHistory(cfg.runHistory);
|
|
3863
3880
|
if (cfg.tracing !== void 0) validateTracing(cfg.tracing);
|
|
3864
3881
|
validateRuntimeOverrides(cfg.runtimeOverrides);
|
|
3882
|
+
if (cfg.review !== void 0) validateReview(cfg.review);
|
|
3865
3883
|
if (cfg.ship !== void 0) validateShip(cfg.ship);
|
|
3866
3884
|
if (cfg.spawn !== void 0) validateSpawn(cfg.spawn);
|
|
3867
3885
|
if (cfg.routing !== void 0) validateRouting(cfg.routing, cfg.providers);
|
|
@@ -3978,12 +3996,23 @@ function grantProblem(path, raw) {
|
|
|
3978
3996
|
return `${path}.costCapUsd must be a positive number of dollars`;
|
|
3979
3997
|
return void 0;
|
|
3980
3998
|
}
|
|
3981
|
-
function
|
|
3999
|
+
function addressSeverityProblem(path, value) {
|
|
4000
|
+
return value !== void 0 && !isAddressSeverity(value) ? `${path} must be one of ${ADDRESS_SEVERITIES.join(", ")} (docs/reference/specs/agent-review.md item 5a)` : void 0;
|
|
4001
|
+
}
|
|
4002
|
+
function validateReview(review) {
|
|
4003
|
+
const problem = addressSeverityProblem("review.addressSeverity", review.addressSeverity);
|
|
4004
|
+
if (problem) throw new Error(`config.yaml: ${problem}`);
|
|
4005
|
+
}
|
|
4006
|
+
function validateScopeBlocks(layer, source2) {
|
|
3982
4007
|
for (const [kind, scopes] of [
|
|
3983
4008
|
["channels", layer.channels],
|
|
3984
4009
|
["users", layer.users]
|
|
3985
4010
|
]) {
|
|
3986
4011
|
for (const [id, scope] of Object.entries(scopes ?? {})) {
|
|
4012
|
+
const level = addressSeverityProblem(`${kind}.${id}.review.addressSeverity`, scope.review?.addressSeverity);
|
|
4013
|
+
if (level) throw new Error(`${source2}: ${level}`);
|
|
4014
|
+
if (scope.ship !== void 0 && "addressSeverity" in scope.ship)
|
|
4015
|
+
throw new Error(`${source2}: ${ADDRESS_SEVERITY_MOVED(`${kind}.${id}.ship.addressSeverity`)}`);
|
|
3987
4016
|
const grant2 = scope.ship?.grant;
|
|
3988
4017
|
if (grant2 === void 0) continue;
|
|
3989
4018
|
const problem = grantProblem(`${kind}.${id}.ship.grant`, grant2);
|
|
@@ -3998,6 +4027,7 @@ function validateShip(ship) {
|
|
|
3998
4027
|
throw new Error(
|
|
3999
4028
|
"config.yaml: ship.coordinator is no longer a key \u2014 every agent:ship request runs on the plan runner; remove it (docs/reference/migrations.md)"
|
|
4000
4029
|
);
|
|
4030
|
+
if (key === "addressSeverity") throw new Error(`config.yaml: ${ADDRESS_SEVERITY_MOVED("ship.addressSeverity")}`);
|
|
4001
4031
|
throw new Error(`config.yaml: ship.${key} is not a known key`);
|
|
4002
4032
|
}
|
|
4003
4033
|
const rounds = ship.maxRounds;
|
|
@@ -4015,10 +4045,6 @@ function validateShip(ship) {
|
|
|
4015
4045
|
throw new Error(
|
|
4016
4046
|
`config.yaml: ship.maxMinutes ${pipeline.maxMinutes} cannot hold the loop ship.maxRounds ${pipeline.maxRounds} allows \u2014 ${held2.need} minutes are needed (${ALLOWANCES.provision} to provision, the coding child's ${ASKS.coding}, and the reserve for ${pipeline.maxRounds} review rounds at their floors); raise ship.maxMinutes or lower ship.maxRounds (docs/reference/specs/agent-ship.md item 8)`
|
|
4017
4047
|
);
|
|
4018
|
-
if (ship.addressSeverity !== void 0 && !isAddressSeverity(ship.addressSeverity))
|
|
4019
|
-
throw new Error(
|
|
4020
|
-
`config.yaml: ship.addressSeverity must be one of ${ADDRESS_SEVERITIES.join(", ")} (docs/reference/specs/agent-ship.md item 9)`
|
|
4021
|
-
);
|
|
4022
4048
|
if (ship.grant !== void 0) {
|
|
4023
4049
|
const problem = grantProblem("ship.grant", ship.grant);
|
|
4024
4050
|
if (problem) throw new Error(`config.yaml: ${problem}`);
|
|
@@ -4089,7 +4115,7 @@ function validateRuntimeOverrides(ro) {
|
|
|
4089
4115
|
}
|
|
4090
4116
|
}
|
|
4091
4117
|
}
|
|
4092
|
-
var MAX_INSTRUCTIONS_LENGTH, CONFIG_KEYS, SELF_IMPROVEMENT_KEYS, BOUNDARY_KEYS, MIN_BOUNDARY_MINUTES, PI_KEYS, PI_COMPACTION_KEYS, harnessWords, OPENCODE_KEYS, OPENCODE_COMPACTION_KEYS, ROUTE_ANSWER_MODES, REFERENCES_KEYS, ROUTING_KEYS, SHIP_KEYS, GRANT_KEYS, SPAWN_KEYS;
|
|
4118
|
+
var MAX_INSTRUCTIONS_LENGTH, CONFIG_KEYS, SELF_IMPROVEMENT_KEYS, BOUNDARY_KEYS, MIN_BOUNDARY_MINUTES, PI_KEYS, PI_COMPACTION_KEYS, harnessWords, OPENCODE_KEYS, OPENCODE_COMPACTION_KEYS, ROUTE_ANSWER_MODES, REFERENCES_KEYS, ROUTING_KEYS, SHIP_KEYS, GRANT_KEYS, ADDRESS_SEVERITY_MOVED, SPAWN_KEYS;
|
|
4093
4119
|
var init_validate = __esm({
|
|
4094
4120
|
"../../src/config/validate.ts"() {
|
|
4095
4121
|
"use strict";
|
|
@@ -4161,10 +4187,10 @@ var init_validate = __esm({
|
|
|
4161
4187
|
SHIP_KEYS = {
|
|
4162
4188
|
maxRounds: true,
|
|
4163
4189
|
maxMinutes: true,
|
|
4164
|
-
addressSeverity: true,
|
|
4165
4190
|
grant: true
|
|
4166
4191
|
};
|
|
4167
4192
|
GRANT_KEYS = { renewals: true, costCapUsd: true };
|
|
4193
|
+
ADDRESS_SEVERITY_MOVED = (path) => `${path} moved to ${path.replace(/ship\.addressSeverity$/, "review.addressSeverity")} \u2014 the severity to address now gates every review's verdict, not ship alone (docs/reference/migrations.md)`;
|
|
4168
4194
|
SPAWN_KEYS = { maxChildren: true };
|
|
4169
4195
|
}
|
|
4170
4196
|
});
|
|
@@ -4443,7 +4469,7 @@ var init_config2 = __esm({
|
|
|
4443
4469
|
validateInstructions(doc, `overrides (${this.backing.describe()})`);
|
|
4444
4470
|
validateScopeEfforts(doc, `overrides (${this.backing.describe()})`);
|
|
4445
4471
|
validateBoundaries(doc, `overrides (${this.backing.describe()})`);
|
|
4446
|
-
|
|
4472
|
+
validateScopeBlocks(doc, `overrides (${this.backing.describe()})`);
|
|
4447
4473
|
validateHarnessWords(doc, `overrides (${this.backing.describe()})`);
|
|
4448
4474
|
validateMcpServers(
|
|
4449
4475
|
{ channels: doc.channels, users: doc.users, defaults: doc.org },
|
|
@@ -9159,6 +9185,7 @@ var init_runEvents = __esm({
|
|
|
9159
9185
|
RUN_NOTE_KINDS = [
|
|
9160
9186
|
"wrap_up",
|
|
9161
9187
|
"time_budget_exhausted",
|
|
9188
|
+
"tool_cut",
|
|
9162
9189
|
"turn_budget_exhausted",
|
|
9163
9190
|
"sandbox_dead",
|
|
9164
9191
|
"fleet_busy",
|
|
@@ -9177,6 +9204,7 @@ var init_runEvents = __esm({
|
|
|
9177
9204
|
"cold_sandbox",
|
|
9178
9205
|
"rebind_refused",
|
|
9179
9206
|
"work_left_behind",
|
|
9207
|
+
"workspace_torn_down",
|
|
9180
9208
|
"pr_not_opened",
|
|
9181
9209
|
"review_not_posted",
|
|
9182
9210
|
"compacted",
|
|
@@ -9185,6 +9213,7 @@ var init_runEvents = __esm({
|
|
|
9185
9213
|
"tool_refused",
|
|
9186
9214
|
"settle_set_aside",
|
|
9187
9215
|
"tool_unnamed",
|
|
9216
|
+
"directory_reached",
|
|
9188
9217
|
"budget_salvage",
|
|
9189
9218
|
"stuck_loop"
|
|
9190
9219
|
];
|
|
@@ -9595,6 +9624,9 @@ function execDeadline(timeoutMs, signal2) {
|
|
|
9595
9624
|
signal2.addEventListener("abort", onStop, { once: true });
|
|
9596
9625
|
return AbortSignal.any([deadline.signal, signal2]);
|
|
9597
9626
|
}
|
|
9627
|
+
function isRunStopError(err2) {
|
|
9628
|
+
return err2 instanceof ExecInfraError && err2.reason === "aborted";
|
|
9629
|
+
}
|
|
9598
9630
|
function infraMayClear(err2) {
|
|
9599
9631
|
return EXEC_INFRA_WAITABLE.has(err2.reason);
|
|
9600
9632
|
}
|
|
@@ -10433,7 +10465,7 @@ var init_residentRefresh = __esm({
|
|
|
10433
10465
|
"use strict";
|
|
10434
10466
|
init_residentDisk();
|
|
10435
10467
|
RUNTIME_MOVED_WORDING = /previous runtime incarnation|interrupted because the runtime changed|runtime identity is no longer active|sandbox lifetime is no longer current|platform was updating the sandbox runtime|no longer identifies pid/i;
|
|
10436
|
-
STOPPED_CONTAINER_WORDING = /process supervisor is closed|container is not running, consider calling start/i;
|
|
10468
|
+
STOPPED_CONTAINER_WORDING = /process supervisor is closed|container is not running, consider calling start|the container just exited/i;
|
|
10437
10469
|
RUNTIME_REPLACEMENT_WORDING = new RegExp(
|
|
10438
10470
|
`${RUNTIME_MOVED_WORDING.source}|${STOPPED_CONTAINER_WORDING.source}`,
|
|
10439
10471
|
"i"
|
|
@@ -10655,34 +10687,45 @@ function residentAnswerReason(status3, data) {
|
|
|
10655
10687
|
function answeredStatus(httpStatus, data) {
|
|
10656
10688
|
return httpStatus === 200 && typeof data.error === "string" && typeof data.status === "number" ? data.status : httpStatus;
|
|
10657
10689
|
}
|
|
10690
|
+
function isTransientRefusal(answer) {
|
|
10691
|
+
return answer.status >= 500 && answer.status <= 599 && answer.data.transient === true;
|
|
10692
|
+
}
|
|
10658
10693
|
function lifecycleReasonOf(data) {
|
|
10659
10694
|
if (typeof data.stateReason === "string") return data.stateReason;
|
|
10660
10695
|
if (typeof data.reason === "string" && !ANSWER_OWN_WORDS.has(data.reason)) return data.reason;
|
|
10661
10696
|
return "";
|
|
10662
10697
|
}
|
|
10663
|
-
function wakeStrikeReason(last) {
|
|
10698
|
+
function wakeStrikeReason(last, transient = false) {
|
|
10699
|
+
if (transient && isUnansweredProbe(last)) return "worker-unavailable";
|
|
10664
10700
|
return last.kind === "status" && isWakeable(last.state, last.reason) ? "worker-unavailable" : "refused";
|
|
10665
10701
|
}
|
|
10666
|
-
function
|
|
10702
|
+
function isUnansweredProbe(view) {
|
|
10703
|
+
return view.kind === "unreachable" && (view.transport || view.status !== void 0 && view.status >= 500);
|
|
10704
|
+
}
|
|
10705
|
+
function residentWakeBudgetStrike(route3, refusal, spentMs, last, wait = {}) {
|
|
10706
|
+
const origin = wait.origin ?? "container-exited";
|
|
10707
|
+
const transient = wait.transient ?? origin === "transient-refusal";
|
|
10708
|
+
const waitedFor = origin === "container-exited" ? "to wake" : "to come back";
|
|
10667
10709
|
return residentWakeStrike(
|
|
10668
10710
|
route3,
|
|
10669
10711
|
refusal,
|
|
10670
|
-
`waited ${Math.round(spentMs / 1e3)}s for the resident
|
|
10671
|
-
wakeStrikeReason(last)
|
|
10712
|
+
`waited ${Math.round(spentMs / 1e3)}s for the resident ${waitedFor} (last seen ${describeProbe(last)}) and gave up`,
|
|
10713
|
+
wakeStrikeReason(last, transient),
|
|
10714
|
+
origin
|
|
10672
10715
|
);
|
|
10673
10716
|
}
|
|
10674
|
-
function residentWakeStrike(route3, refusal, why, reason2) {
|
|
10717
|
+
function residentWakeStrike(route3, refusal, why, reason2, origin = "container-exited") {
|
|
10675
10718
|
return classifyError(new ExecInfraError(`resident ${route3}: ${refusal}; ${why}`, reason2), {
|
|
10676
10719
|
kind: "infra",
|
|
10677
|
-
code:
|
|
10720
|
+
code: origin
|
|
10678
10721
|
});
|
|
10679
10722
|
}
|
|
10680
|
-
function wakePause(ms, signal2) {
|
|
10723
|
+
function wakePause(ms, signal2, route3) {
|
|
10681
10724
|
return new Promise((resolve13, reject) => {
|
|
10682
|
-
if (signal2?.aborted) return reject(wakeStopped());
|
|
10725
|
+
if (signal2?.aborted) return reject(wakeStopped(route3));
|
|
10683
10726
|
const onAbort = () => {
|
|
10684
10727
|
clearTimeout(timer);
|
|
10685
|
-
reject(wakeStopped());
|
|
10728
|
+
reject(wakeStopped(route3));
|
|
10686
10729
|
};
|
|
10687
10730
|
const timer = setTimeout(() => {
|
|
10688
10731
|
signal2?.removeEventListener("abort", onAbort);
|
|
@@ -10756,7 +10799,10 @@ var init_resident = __esm({
|
|
|
10756
10799
|
init_executor();
|
|
10757
10800
|
DETACH_TIMEOUT_MS = 1e4;
|
|
10758
10801
|
ANSWER_OWN_WORDS = /* @__PURE__ */ new Set(["mirror-busy", DISK_PRESSURE_REASON, "image-stale"]);
|
|
10759
|
-
wakeStopped = () =>
|
|
10802
|
+
wakeStopped = (route3) => classifyError(
|
|
10803
|
+
new ExecInfraError(`resident ${route3}: stopped waiting for the resident to wake: the run was stopped`, "aborted"),
|
|
10804
|
+
{ kind: "transport" }
|
|
10805
|
+
);
|
|
10760
10806
|
ResidentNeedsRefError = class extends Error {
|
|
10761
10807
|
/** The resident's default branch when the Worker names one in the 409 body:
|
|
10762
10808
|
* the factory binds the thread to it (loudly) instead of asking. Undefined
|
|
@@ -10772,6 +10818,11 @@ var init_resident = __esm({
|
|
|
10772
10818
|
resource;
|
|
10773
10819
|
defaultRef;
|
|
10774
10820
|
needs = "ref";
|
|
10821
|
+
/** How long the wake wait ran before this refusal, when the attach met one
|
|
10822
|
+
* (a transient refusal cleared, then the resident asked for a ref): the
|
|
10823
|
+
* factory's retry by default draws on one budget across both attaches and
|
|
10824
|
+
* the card names the total. */
|
|
10825
|
+
wokeAfterMs;
|
|
10775
10826
|
};
|
|
10776
10827
|
ResidentReuseRefusedError = class extends Error {
|
|
10777
10828
|
constructor(resource, why) {
|
|
@@ -10817,7 +10868,13 @@ var init_resident = __esm({
|
|
|
10817
10868
|
const data = await parseResidentBody(res);
|
|
10818
10869
|
const err2 = typeof data.error === "string" ? data.error : "";
|
|
10819
10870
|
if (err2.startsWith("op-refused")) return { kind: "refused", reason: err2 };
|
|
10820
|
-
if (err2)
|
|
10871
|
+
if (err2) {
|
|
10872
|
+
return {
|
|
10873
|
+
kind: "error",
|
|
10874
|
+
message: `resident /op: ${err2}`,
|
|
10875
|
+
...data.transient === true ? { transient: true } : {}
|
|
10876
|
+
};
|
|
10877
|
+
}
|
|
10821
10878
|
if (!res.ok) return { kind: "error", message: `resident /op HTTP ${res.status}` };
|
|
10822
10879
|
const ok2 = data.ok === true;
|
|
10823
10880
|
const summary = typeof data.summary === "string" && data.summary ? data.summary : `${op} ${ok2 ? "succeeded" : "failed"}`;
|
|
@@ -10888,7 +10945,13 @@ var init_resident = __esm({
|
|
|
10888
10945
|
}
|
|
10889
10946
|
const data = sanitizeResidentBody(await res.json().catch(() => ({})));
|
|
10890
10947
|
if (!res.ok) {
|
|
10891
|
-
return {
|
|
10948
|
+
return {
|
|
10949
|
+
kind: "unreachable",
|
|
10950
|
+
error: `probe HTTP ${res.status}: ${String(data.error ?? "")}`,
|
|
10951
|
+
transport: false,
|
|
10952
|
+
status: res.status,
|
|
10953
|
+
...res.status >= 500 && data.transient === true ? { transient: true } : {}
|
|
10954
|
+
};
|
|
10892
10955
|
}
|
|
10893
10956
|
const seed = seedHandleOf(data.snapshot);
|
|
10894
10957
|
return {
|
|
@@ -10898,6 +10961,31 @@ var init_resident = __esm({
|
|
|
10898
10961
|
...seed ? { seed } : {}
|
|
10899
10962
|
};
|
|
10900
10963
|
}
|
|
10964
|
+
/** The selection probe, waited through a blip the Worker typed transient
|
|
10965
|
+
* (execution.md item 9): a 5xx carrying `transient: true` — the Durable
|
|
10966
|
+
* Object reset or lost under the probe, which the attach's own wait would
|
|
10967
|
+
* have waited through a moment later — is re-probed every `WAKE_POLL_MS`
|
|
10968
|
+
* under `budgetMs`, the run's stop ending the wait at once with its typed
|
|
10969
|
+
* error. Starts from the view the caller already has (`first`), re-probes
|
|
10970
|
+
* at once — a blip the Durable Object has already recovered from costs no
|
|
10971
|
+
* pause, as the wake wait's first view costs none — then every poll; answers
|
|
10972
|
+
* the last view and how long was waited. A transport failure or any other
|
|
10973
|
+
* answer is returned at once — untyped, it is the caller's outage breaker's
|
|
10974
|
+
* case, never a wait. */
|
|
10975
|
+
static async probeStatusThroughBlip(baseUrl, token2, resource, timeoutMs, first, wait) {
|
|
10976
|
+
const t0 = systemClock();
|
|
10977
|
+
let probe = first;
|
|
10978
|
+
let reprobed = false;
|
|
10979
|
+
for (; ; ) {
|
|
10980
|
+
if (probe.kind !== "unreachable" || !probe.transient) return { probe, waitedMs: systemClock() - t0 };
|
|
10981
|
+
const spent = systemClock() - t0;
|
|
10982
|
+
if (spent >= wait.budgetMs) return { probe, waitedMs: spent };
|
|
10983
|
+
if (reprobed) await wakePause(Math.min(WAKE_POLL_MS, wait.budgetMs - spent), wait.signal, "/status");
|
|
10984
|
+
reprobed = true;
|
|
10985
|
+
probe = await _ResidentExecutor.probeStatus(baseUrl, token2, resource, timeoutMs, wait.span, wait.signal);
|
|
10986
|
+
if (wait.signal?.aborted) throw wakeStopped("/status");
|
|
10987
|
+
}
|
|
10988
|
+
}
|
|
10901
10989
|
/** The one held request while the resident restores (item 27): POST
|
|
10902
10990
|
* /await-restore, which the resident's Durable Object holds until its state
|
|
10903
10991
|
* leaves `restoring` and the transition publishes — event-driven, no
|
|
@@ -10972,10 +11060,32 @@ var init_resident = __esm({
|
|
|
10972
11060
|
* request and the resident moves a default-bound thread onto it (item 16;
|
|
10973
11061
|
* `rebound` / `rebindRefused` say which) — and the sha the worktree is at. A
|
|
10974
11062
|
* 200 without both fields is a malformed resident (the attach contract
|
|
10975
|
-
* always carries them) and is an error, never a half-bound executor.
|
|
10976
|
-
|
|
10977
|
-
|
|
11063
|
+
* always carries them) and is an error, never a half-bound executor. A
|
|
11064
|
+
* refusal the Worker typed as the platform's transient (`isTransientRefusal`:
|
|
11065
|
+
* the Durable Object reset or lost under the attach) is waited through
|
|
11066
|
+
* here, for the run's first attach and every re-attach alike: the wake wait
|
|
11067
|
+
* (item 65) probes `/status`, re-attaches as soon as the engine says it
|
|
11068
|
+
* serves, and is bounded by `opts.budgetMs` under the wake ceiling; past the
|
|
11069
|
+
* budget it throws the wake's strike (`worker-unavailable`), so a caller
|
|
11070
|
+
* with a longer clock may still wait and the factory falls back cold naming
|
|
11071
|
+
* the wait. A deterministic refusal is judged at once, with no probe.
|
|
11072
|
+
* `attachTimeoutMs` is how long one attach request may take — the exec
|
|
11073
|
+
* default when absent (a first attach or a head move may clone and install
|
|
11074
|
+
* deps); an operation passes its own call bound, so its recovery never
|
|
11075
|
+
* waits past the operation's wall clock. */
|
|
11076
|
+
async attach(span, opts = {}) {
|
|
11077
|
+
const answer = await this.attachOnce(span, opts.attachTimeoutMs, opts.signal);
|
|
10978
11078
|
if (answer.ok) return answer.binding;
|
|
11079
|
+
if (isTransientRefusal(answer)) {
|
|
11080
|
+
const woke = await this.awaitWake("/attach", String(answer.data.error), {
|
|
11081
|
+
origin: "transient-refusal",
|
|
11082
|
+
signal: opts.signal,
|
|
11083
|
+
budgetMs: opts.budgetMs,
|
|
11084
|
+
attachTimeoutMs: opts.attachTimeoutMs,
|
|
11085
|
+
span
|
|
11086
|
+
});
|
|
11087
|
+
return { ...woke.binding, wokeAfterMs: woke.waitedMs };
|
|
11088
|
+
}
|
|
10979
11089
|
throw this.attachRefusal(answer);
|
|
10980
11090
|
}
|
|
10981
11091
|
/** One `/attach`, answered rather than thrown: the binding on a 200, else
|
|
@@ -11036,7 +11146,7 @@ var init_resident = __esm({
|
|
|
11036
11146
|
if (status3 === 409 && data.needs === "recreate")
|
|
11037
11147
|
return traced(new ResidentReuseRefusedError(this.opts.resource, err2));
|
|
11038
11148
|
if (status3 === 404) return traced(new Error(`resident attach: ${this.opts.resource} is not onboarded (${err2})`));
|
|
11039
|
-
if (
|
|
11149
|
+
if (isTransientRefusal(answer)) {
|
|
11040
11150
|
return traced(
|
|
11041
11151
|
classifyError(
|
|
11042
11152
|
new ExecInfraError(`resident attach failed for ${this.opts.resource}: ${err2}`, "worker-unavailable"),
|
|
@@ -11052,11 +11162,13 @@ var init_resident = __esm({
|
|
|
11052
11162
|
* mechanism a re-review after a push uses, applied mid-run. The move's own
|
|
11053
11163
|
* attach carries the new sha; a later recovery re-attach names none (item
|
|
11054
11164
|
* 51: the run's own pushes may move the tip). Answers the sha the worktree
|
|
11055
|
-
* is at; throws like attach() on a refusal.
|
|
11165
|
+
* is at; throws like attach() on a refusal. The run's stop (`opts.signal`)
|
|
11166
|
+
* rides into the attach and the wake wait a transient refusal begins, so a
|
|
11167
|
+
* stopped round never sits out the wake ceiling. */
|
|
11056
11168
|
async moveTo(sha, opts) {
|
|
11057
11169
|
this.opts = { ...this.opts, sha };
|
|
11058
11170
|
this.shaPending = true;
|
|
11059
|
-
const binding = await this.attach(opts?.span);
|
|
11171
|
+
const binding = await this.attach(opts?.span, { signal: opts?.signal });
|
|
11060
11172
|
return { sha: binding.sha };
|
|
11061
11173
|
}
|
|
11062
11174
|
/** POST /detach: return this thread's pool user (and remove its worktree)
|
|
@@ -11125,10 +11237,27 @@ var init_resident = __esm({
|
|
|
11125
11237
|
if (!CONTROL_RESET_REISSUE_ROUTES.has(route3) && saysControlReset(data))
|
|
11126
11238
|
throw new ExecControlResetError(String(data.error).trim());
|
|
11127
11239
|
};
|
|
11240
|
+
let waitStarted;
|
|
11241
|
+
const waitBudget = wakeWaitBudget(opts.waitBudgetMs);
|
|
11242
|
+
const waitLeft = () => {
|
|
11243
|
+
waitStarted ??= systemClock();
|
|
11244
|
+
return Math.max(0, waitBudget - (systemClock() - waitStarted));
|
|
11245
|
+
};
|
|
11246
|
+
const recoveryAttachTimeoutMs = Math.max(callTimeoutMs, BASH_TIMEOUT_MS);
|
|
11128
11247
|
let r = await this.call(route3, body, callTimeoutMs, signal2, span);
|
|
11129
11248
|
if (isContainerRolling(r.data.error)) {
|
|
11130
|
-
const woke = await this.awaitWake(route3, String(r.data.error), {
|
|
11131
|
-
|
|
11249
|
+
const woke = await this.awaitWake(route3, String(r.data.error), {
|
|
11250
|
+
origin: "container-exited",
|
|
11251
|
+
signal: signal2,
|
|
11252
|
+
budgetMs: waitLeft(),
|
|
11253
|
+
attachTimeoutMs: callTimeoutMs,
|
|
11254
|
+
span
|
|
11255
|
+
});
|
|
11256
|
+
if (route3 === "/exec")
|
|
11257
|
+
throw new ExecSandboxRestartedError(
|
|
11258
|
+
sandboxRestartedMessage({ waitedMs: woke.waitedMs, ref: woke.binding.ref, sha: woke.binding.sha }),
|
|
11259
|
+
woke.waitedMs
|
|
11260
|
+
);
|
|
11132
11261
|
r = await this.call(route3, body, callTimeoutMs, signal2, span);
|
|
11133
11262
|
if (isContainerRolling(r.data.error)) {
|
|
11134
11263
|
throw classifyError(
|
|
@@ -11143,21 +11272,21 @@ var init_resident = __esm({
|
|
|
11143
11272
|
goneUnderThread(r.data);
|
|
11144
11273
|
controlResetUnderThread(r.data);
|
|
11145
11274
|
if (r.data.needs === "attach") {
|
|
11146
|
-
await this.attach(span);
|
|
11275
|
+
await this.attach(span, { signal: signal2, budgetMs: waitLeft(), attachTimeoutMs: recoveryAttachTimeoutMs });
|
|
11147
11276
|
r = await this.call(route3, body, callTimeoutMs, signal2, span);
|
|
11148
11277
|
goneUnderThread(r.data);
|
|
11149
11278
|
controlResetUnderThread(r.data);
|
|
11150
11279
|
if (r.data.needs === "attach") throw stillGone(r.data);
|
|
11151
11280
|
}
|
|
11152
11281
|
if (saysControlReset(r.data)) {
|
|
11153
|
-
await this.attach(span);
|
|
11282
|
+
await this.attach(span, { signal: signal2, budgetMs: waitLeft(), attachTimeoutMs: recoveryAttachTimeoutMs });
|
|
11154
11283
|
r = await this.call(route3, body, callTimeoutMs, signal2, span);
|
|
11155
11284
|
if (saysControlReset(r.data)) throw new ExecControlResetError(String(r.data.error).trim());
|
|
11156
11285
|
if (r.data.needs === "attach") throw stillGone(r.data);
|
|
11157
11286
|
}
|
|
11158
11287
|
if (r.data.reason === "runtime-replaced") {
|
|
11159
11288
|
this.noteRuntimeReplaced(route3, r.data);
|
|
11160
|
-
await this.attach(span);
|
|
11289
|
+
await this.attach(span, { signal: signal2, budgetMs: waitLeft(), attachTimeoutMs: recoveryAttachTimeoutMs });
|
|
11161
11290
|
r = await this.call(route3, body, callTimeoutMs, signal2, span);
|
|
11162
11291
|
if (r.data.reason === "runtime-replaced") this.noteRuntimeReplaced(route3, r.data);
|
|
11163
11292
|
if (r.data.needs === "attach") throw stillGone(r.data);
|
|
@@ -11185,13 +11314,28 @@ var init_resident = __esm({
|
|
|
11185
11314
|
* the wait going). Answers the wait and the fresh binding. Throws the
|
|
11186
11315
|
* strike (an `ExecInfraError` the tracker counts) when nothing is
|
|
11187
11316
|
* recovering, when the resident goes down mid-wait, when a re-attach does
|
|
11188
|
-
* not answer, or when the budget is spent; a hard stop rejects at once
|
|
11189
|
-
*
|
|
11190
|
-
* attach's own
|
|
11317
|
+
* not answer, or when the budget is spent; a hard stop rejects at once with
|
|
11318
|
+
* the stop's one typed shape (`wakeStopped`: `aborted`, the transport) from
|
|
11319
|
+
* every stop point — the pause, the probe, the re-attach's own call; any
|
|
11320
|
+
* other attach refusal is the attach's own legible error.
|
|
11321
|
+
*
|
|
11322
|
+
* A wait a TRANSIENT refusal began (`opts.transient`, or a re-attach's
|
|
11323
|
+
* transient 500 met inside it) reads an unreachable Worker differently: the
|
|
11324
|
+
* Durable Object that reset or lost its connection under the attach loses
|
|
11325
|
+
* `/status` too, so an unanswered probe is the same blip, waited through
|
|
11326
|
+
* under the budget — never the definite "nothing says the container is
|
|
11327
|
+
* coming back" a container's exit is judged by — and a wait spent with the
|
|
11328
|
+
* Worker unreachable throughout ends in `worker-unavailable`, never
|
|
11329
|
+
* `refused`. */
|
|
11191
11330
|
async awaitWake(route3, refusal, opts) {
|
|
11192
11331
|
const t0 = systemClock();
|
|
11193
11332
|
const waited = () => systemClock() - t0;
|
|
11194
|
-
|
|
11333
|
+
let transient = opts.origin === "transient-refusal";
|
|
11334
|
+
const definite = (why) => residentWakeStrike(route3, refusal, why, "refused", opts.origin);
|
|
11335
|
+
const decide = (view) => transient && isUnansweredProbe(view) ? {
|
|
11336
|
+
wait: true,
|
|
11337
|
+
why: `the resident Worker did not answer /status (${view.error}) after a transient refusal: the same blip, waited through`
|
|
11338
|
+
} : wakeDecision(view);
|
|
11195
11339
|
const probe = async () => {
|
|
11196
11340
|
const seen2 = await _ResidentExecutor.probeStatus(
|
|
11197
11341
|
this.opts.baseUrl,
|
|
@@ -11201,37 +11345,47 @@ var init_resident = __esm({
|
|
|
11201
11345
|
opts.span,
|
|
11202
11346
|
opts.signal
|
|
11203
11347
|
);
|
|
11204
|
-
if (opts.signal?.aborted) throw wakeStopped();
|
|
11348
|
+
if (opts.signal?.aborted) throw wakeStopped(route3);
|
|
11205
11349
|
return seen2;
|
|
11206
11350
|
};
|
|
11207
11351
|
let seen = await probe();
|
|
11208
|
-
const decision =
|
|
11352
|
+
const decision = decide(seen);
|
|
11209
11353
|
if (!decision.wait) throw definite(decision.why);
|
|
11210
11354
|
const budget = wakeWaitBudget(opts.budgetMs);
|
|
11355
|
+
let firstView = true;
|
|
11211
11356
|
for (; ; ) {
|
|
11357
|
+
if (seen.kind === "status" && isServiceable(seen.state, seen.reason) && (transient || !firstView)) {
|
|
11358
|
+
let answer;
|
|
11359
|
+
try {
|
|
11360
|
+
const bound = opts.attachTimeoutMs === void 0 ? Math.max(budget - waited(), BASH_TIMEOUT_MS) : Math.min(WAKE_WAIT_MAX_MS, Math.max(budget - waited(), opts.attachTimeoutMs));
|
|
11361
|
+
answer = await this.attachOnce(opts.span, bound, opts.signal);
|
|
11362
|
+
} catch (err2) {
|
|
11363
|
+
if (!(err2 instanceof ExecInfraError)) throw err2;
|
|
11364
|
+
if (err2.reason === "aborted") throw err2;
|
|
11365
|
+
throw residentWakeStrike(
|
|
11366
|
+
route3,
|
|
11367
|
+
refusal,
|
|
11368
|
+
`the re-attach after ${Math.round(waited() / 1e3)}s did not answer (${err2.message})`,
|
|
11369
|
+
wakeStrikeReason(seen, transient),
|
|
11370
|
+
opts.origin
|
|
11371
|
+
);
|
|
11372
|
+
}
|
|
11373
|
+
if (answer.ok) return { waitedMs: waited(), binding: answer.binding };
|
|
11374
|
+
if (isTransientRefusal(answer)) transient = true;
|
|
11375
|
+
else if (!isContainerRolling(answer.data.error)) {
|
|
11376
|
+
const refused7 = this.attachRefusal(answer);
|
|
11377
|
+
if (refused7 instanceof ResidentNeedsRefError) refused7.wokeAfterMs = waited();
|
|
11378
|
+
throw refused7;
|
|
11379
|
+
}
|
|
11380
|
+
}
|
|
11381
|
+
firstView = false;
|
|
11212
11382
|
const spent = waited();
|
|
11213
|
-
if (spent >= budget)
|
|
11214
|
-
|
|
11383
|
+
if (spent >= budget)
|
|
11384
|
+
throw residentWakeBudgetStrike(route3, refusal, spent, seen, { origin: opts.origin, transient });
|
|
11385
|
+
await wakePause(Math.min(WAKE_POLL_MS, budget - spent), opts.signal, route3);
|
|
11215
11386
|
seen = await probe();
|
|
11216
|
-
const again =
|
|
11387
|
+
const again = decide(seen);
|
|
11217
11388
|
if (!again.wait) throw definite(again.why);
|
|
11218
|
-
if (seen.kind !== "status" || !isServiceable(seen.state, seen.reason)) continue;
|
|
11219
|
-
let answer;
|
|
11220
|
-
try {
|
|
11221
|
-
answer = await this.attachOnce(opts.span, Math.max(budget - waited(), 1e3), opts.signal);
|
|
11222
|
-
} catch (err2) {
|
|
11223
|
-
if (!(err2 instanceof ExecInfraError)) throw err2;
|
|
11224
|
-
if (err2.reason === "aborted") throw err2;
|
|
11225
|
-
throw residentWakeStrike(
|
|
11226
|
-
route3,
|
|
11227
|
-
refusal,
|
|
11228
|
-
`the re-attach after ${Math.round(waited() / 1e3)}s did not answer (${err2.message})`,
|
|
11229
|
-
wakeStrikeReason(seen)
|
|
11230
|
-
);
|
|
11231
|
-
}
|
|
11232
|
-
if (answer.ok) return { waitedMs: waited(), ref: answer.binding.ref, sha: answer.binding.sha };
|
|
11233
|
-
if (answer.data.transient === true) continue;
|
|
11234
|
-
if (!isContainerRolling(answer.data.error)) throw this.attachRefusal(answer);
|
|
11235
11389
|
}
|
|
11236
11390
|
}
|
|
11237
11391
|
async exec(command2, opts) {
|
|
@@ -11489,6 +11643,19 @@ async function makeExecutor(opts, ctx, span) {
|
|
|
11489
11643
|
if (!token2) throw new Error(`execution.resident is configured but ${tokenEnv} is not set`);
|
|
11490
11644
|
const resource = repoResourceId(ctx.repo);
|
|
11491
11645
|
let probe = await probeResident(resident, token2, resource, span);
|
|
11646
|
+
let probeWaitMs = 0;
|
|
11647
|
+
if (probe.kind === "unreachable" && probe.transient) {
|
|
11648
|
+
const through = await ResidentExecutor.probeStatusThroughBlip(
|
|
11649
|
+
resident.baseUrl,
|
|
11650
|
+
token2.reveal(),
|
|
11651
|
+
resource,
|
|
11652
|
+
resident.probeTimeoutMs ?? 2e3,
|
|
11653
|
+
probe,
|
|
11654
|
+
{ budgetMs: FIRST_ATTACH_WAIT_MS, signal: ctx.stopSignal, span }
|
|
11655
|
+
);
|
|
11656
|
+
probe = through.probe;
|
|
11657
|
+
probeWaitMs = through.waitedMs;
|
|
11658
|
+
}
|
|
11492
11659
|
let waitedForRestore = false;
|
|
11493
11660
|
if (probe.kind === "status" && probe.state === "restoring") {
|
|
11494
11661
|
const wait = await ResidentExecutor.awaitRestore(
|
|
@@ -11526,7 +11693,15 @@ async function makeExecutor(opts, ctx, span) {
|
|
|
11526
11693
|
...ctx.ownPr !== void 0 ? { ownPr: ctx.ownPr } : {}
|
|
11527
11694
|
},
|
|
11528
11695
|
nonWarm,
|
|
11529
|
-
span
|
|
11696
|
+
span,
|
|
11697
|
+
// A refusal the Worker typed transient is waited through here, under
|
|
11698
|
+
// what the probe's wait left of the factory's own budget and the
|
|
11699
|
+
// run's stop (item 9); the card names both waits' total.
|
|
11700
|
+
{
|
|
11701
|
+
signal: ctx.stopSignal,
|
|
11702
|
+
budgetMs: Math.max(0, FIRST_ATTACH_WAIT_MS - probeWaitMs),
|
|
11703
|
+
waitedMs: probeWaitMs
|
|
11704
|
+
}
|
|
11530
11705
|
);
|
|
11531
11706
|
return waitedForRestore ? {
|
|
11532
11707
|
...selection,
|
|
@@ -11534,12 +11709,14 @@ async function makeExecutor(opts, ctx, span) {
|
|
|
11534
11709
|
} : selection;
|
|
11535
11710
|
} catch (err2) {
|
|
11536
11711
|
if (err2 instanceof ResidentNeedsRefError) throw err2;
|
|
11712
|
+
if (isRunStopError(err2)) throw err2;
|
|
11537
11713
|
failedAttach = residentTraceOf(err2);
|
|
11538
11714
|
const wait = waitedForRestore ? " after waiting for the resident's restore" : "";
|
|
11539
11715
|
reason2 = oneLine(`resident attach failed (${err2 instanceof Error ? err2.message : String(err2)})${wait}`);
|
|
11540
11716
|
}
|
|
11541
11717
|
} else if (probe.kind === "unreachable") {
|
|
11542
|
-
|
|
11718
|
+
const waited = probeWaitMs > 0 ? ` after waiting ${Math.round(probeWaitMs / 1e3)}s` : "";
|
|
11719
|
+
reason2 = oneLine(`resident unreachable (${probe.error})${waited}`);
|
|
11543
11720
|
} else if (probe.state !== "not-onboarded") {
|
|
11544
11721
|
const wait = waitedForRestore ? " after waiting for the resident's restore" : "";
|
|
11545
11722
|
reason2 = oneLine(`resident ${probe.state}${probe.reason ? ` (${probe.reason})` : ""}${wait}`);
|
|
@@ -11623,10 +11800,24 @@ async function reattachWorkspace(opts, ctx, recorded, span) {
|
|
|
11623
11800
|
const token2 = processSecrets.named(tokenEnv);
|
|
11624
11801
|
if (!token2) throw new Error(`execution.resident is configured but ${tokenEnv} is not set`);
|
|
11625
11802
|
const resource = repoResourceId(ctx.repo);
|
|
11626
|
-
|
|
11627
|
-
|
|
11803
|
+
let probe = await probeResident(resident, token2, resource, span);
|
|
11804
|
+
let probeWaitMs = 0;
|
|
11805
|
+
if (probe.kind === "unreachable" && probe.transient) {
|
|
11806
|
+
const through = await ResidentExecutor.probeStatusThroughBlip(
|
|
11807
|
+
resident.baseUrl,
|
|
11808
|
+
token2.reveal(),
|
|
11809
|
+
resource,
|
|
11810
|
+
resident.probeTimeoutMs ?? 2e3,
|
|
11811
|
+
probe,
|
|
11812
|
+
{ budgetMs: FIRST_ATTACH_WAIT_MS, signal: ctx.stopSignal, span }
|
|
11813
|
+
);
|
|
11814
|
+
probe = through.probe;
|
|
11815
|
+
probeWaitMs = through.waitedMs;
|
|
11816
|
+
}
|
|
11817
|
+
const waitedNote = probeWaitMs > 0 ? ` after waiting ${Math.round(probeWaitMs / 1e3)}s` : "";
|
|
11818
|
+
if (probe.kind === "unreachable") throw refuse3(`resident unreachable (${probe.error})${waitedNote}`);
|
|
11628
11819
|
if (!isServiceable(probe.state, probe.reason))
|
|
11629
|
-
throw refuse3(`resident ${probe.state}${probe.reason ? ` (${probe.reason})` : ""}`);
|
|
11820
|
+
throw refuse3(`resident ${probe.state}${probe.reason ? ` (${probe.reason})` : ""}${waitedNote}`);
|
|
11630
11821
|
const nonWarm = probe.state === "warm" ? void 0 : oneLine(`${probe.state}${probe.reason ? ` (${probe.reason})` : ""}`);
|
|
11631
11822
|
const executor = new ResidentExecutor({
|
|
11632
11823
|
baseUrl: resident.baseUrl,
|
|
@@ -11640,8 +11831,12 @@ async function reattachWorkspace(opts, ctx, recorded, span) {
|
|
|
11640
11831
|
});
|
|
11641
11832
|
let binding;
|
|
11642
11833
|
try {
|
|
11643
|
-
binding = await executor.attach(span
|
|
11834
|
+
binding = await executor.attach(span, {
|
|
11835
|
+
signal: ctx.stopSignal,
|
|
11836
|
+
budgetMs: Math.max(0, FIRST_ATTACH_WAIT_MS - probeWaitMs)
|
|
11837
|
+
});
|
|
11644
11838
|
} catch (err2) {
|
|
11839
|
+
if (isRunStopError(err2)) throw err2;
|
|
11645
11840
|
throw refuse3(err2 instanceof Error ? err2.message : String(err2));
|
|
11646
11841
|
}
|
|
11647
11842
|
const same = (recordedValue, answered) => recordedValue === void 0 || answered === void 0 || recordedValue === answered;
|
|
@@ -11652,6 +11847,8 @@ async function reattachWorkspace(opts, ctx, recorded, span) {
|
|
|
11652
11847
|
);
|
|
11653
11848
|
}
|
|
11654
11849
|
const where = `${resource.replace(/^repo:/, "")} \xB7 ${binding.ref}@${binding.sha.slice(0, 7)}`;
|
|
11850
|
+
const wokeSeconds = Math.round((probeWaitMs + (binding.wokeAfterMs ?? 0)) / 1e3);
|
|
11851
|
+
const woke = wokeSeconds > 0 ? ` \xB7 after waiting ${wokeSeconds}s for the resident` : "";
|
|
11655
11852
|
return {
|
|
11656
11853
|
executor,
|
|
11657
11854
|
resident: true,
|
|
@@ -11659,21 +11856,25 @@ async function reattachWorkspace(opts, ctx, recorded, span) {
|
|
|
11659
11856
|
...binding.trace ? { trace: binding.trace } : {},
|
|
11660
11857
|
...binding.attachMs !== void 0 ? { attachMs: binding.attachMs } : {},
|
|
11661
11858
|
binding,
|
|
11662
|
-
note: nonWarm ? `resident ${nonWarm} \xB7 ${where} \xB7 re-attached to the run's worktree` : `resident \xB7 ${where} \xB7 re-attached to the run's worktree`
|
|
11859
|
+
note: nonWarm ? `resident ${nonWarm} \xB7 ${where} \xB7 re-attached to the run's worktree${woke}` : `resident \xB7 ${where} \xB7 re-attached to the run's worktree${woke}`
|
|
11663
11860
|
};
|
|
11664
11861
|
}
|
|
11665
|
-
async function openResident(opts, nonWarm, span) {
|
|
11862
|
+
async function openResident(opts, nonWarm, span, wait = {}) {
|
|
11666
11863
|
let executor = new ResidentExecutor(opts);
|
|
11667
11864
|
let binding;
|
|
11668
11865
|
let byDefault = false;
|
|
11669
11866
|
try {
|
|
11670
|
-
binding = await executor.attach(span);
|
|
11867
|
+
binding = await executor.attach(span, wait);
|
|
11671
11868
|
} catch (err2) {
|
|
11672
11869
|
if (!(err2 instanceof ResidentNeedsRefError) || !err2.defaultRef) throw err2;
|
|
11673
11870
|
byDefault = true;
|
|
11871
|
+
const firstWaitMs = err2.wokeAfterMs ?? 0;
|
|
11872
|
+
const budgetLeft = wait.budgetMs === void 0 ? void 0 : Math.max(0, wait.budgetMs - firstWaitMs);
|
|
11674
11873
|
executor = new ResidentExecutor({ ...opts, refHint: err2.defaultRef, refByDefault: true });
|
|
11675
11874
|
try {
|
|
11676
|
-
binding = await executor.attach(span);
|
|
11875
|
+
binding = await executor.attach(span, { ...wait, budgetMs: budgetLeft });
|
|
11876
|
+
const totalWaitMs = firstWaitMs + (binding.wokeAfterMs ?? 0);
|
|
11877
|
+
if (totalWaitMs > 0) binding = { ...binding, wokeAfterMs: totalWaitMs };
|
|
11677
11878
|
} catch (again) {
|
|
11678
11879
|
if (again instanceof ResidentNeedsRefError) {
|
|
11679
11880
|
throw new Error(
|
|
@@ -11687,6 +11888,8 @@ async function openResident(opts, nonWarm, span) {
|
|
|
11687
11888
|
const where = `${opts.resource.replace(/^repo:/, "")} \xB7 ${binding.ref}@${binding.sha.slice(0, 7)}`;
|
|
11688
11889
|
const why = byDefault ? " (repo default \u2014 no branch named)" : "";
|
|
11689
11890
|
const moved = rebindLabel(binding);
|
|
11891
|
+
const wokeSeconds = Math.round(((wait.waitedMs ?? 0) + (binding.wokeAfterMs ?? 0)) / 1e3);
|
|
11892
|
+
const woke = wokeSeconds > 0 ? ` \xB7 after waiting ${wokeSeconds}s for the resident` : "";
|
|
11690
11893
|
return {
|
|
11691
11894
|
executor,
|
|
11692
11895
|
resident: true,
|
|
@@ -11694,7 +11897,7 @@ async function openResident(opts, nonWarm, span) {
|
|
|
11694
11897
|
...binding.trace ? { trace: binding.trace } : {},
|
|
11695
11898
|
...binding.attachMs !== void 0 ? { attachMs: binding.attachMs } : {},
|
|
11696
11899
|
binding,
|
|
11697
|
-
note: nonWarm ? `resident ${nonWarm} \xB7 ${where}${why}${moved} \u2014 attached to the last snapshot` : `resident \xB7 ${where}${why}${moved}`
|
|
11900
|
+
note: nonWarm ? `resident ${nonWarm} \xB7 ${where}${why}${moved} \u2014 attached to the last snapshot${woke}` : `resident \xB7 ${where}${why}${moved}${woke}`
|
|
11698
11901
|
};
|
|
11699
11902
|
}
|
|
11700
11903
|
function rebindLabel(binding) {
|
|
@@ -11829,6 +12032,7 @@ var BACKENDS, WorkspaceReattachRefusedError, PROBE_OUTAGE_WINDOW_MS, AWAIT_RESTO
|
|
|
11829
12032
|
var init_factory = __esm({
|
|
11830
12033
|
"../../src/execution/factory.ts"() {
|
|
11831
12034
|
"use strict";
|
|
12035
|
+
init_budgets();
|
|
11832
12036
|
init_redact();
|
|
11833
12037
|
init_residentTrace();
|
|
11834
12038
|
init_executor();
|
|
@@ -14707,6 +14911,19 @@ function pushedBranchesOf(events) {
|
|
|
14707
14911
|
}
|
|
14708
14912
|
return [...byRef].map(([ref, pr]) => ({ ref, pr }));
|
|
14709
14913
|
}
|
|
14914
|
+
function callsInFlight(events, status3) {
|
|
14915
|
+
const calls = /* @__PURE__ */ new Map();
|
|
14916
|
+
for (const e of events) {
|
|
14917
|
+
if (e.type === "tool_call" && e.callId !== void 0) {
|
|
14918
|
+
if (calls.get(e.callId)?.state !== "cut") calls.set(e.callId, { call: e, state: "open" });
|
|
14919
|
+
} else if (e.type === "tool_result" && e.callId !== void 0) {
|
|
14920
|
+
const seen = calls.get(e.callId);
|
|
14921
|
+
if (seen !== void 0 && seen.state !== "cut") seen.state = e.cut === true ? "cut" : "settled";
|
|
14922
|
+
}
|
|
14923
|
+
}
|
|
14924
|
+
const orderly = status3 === "completed" || status3 === "stopped_soft";
|
|
14925
|
+
return [...calls.values()].filter((c) => c.state === "cut" || c.state === "open" && !orderly).map((c) => c.call);
|
|
14926
|
+
}
|
|
14710
14927
|
function routeOfEvents(events) {
|
|
14711
14928
|
const meta = events.find((e) => e.type === "run_meta");
|
|
14712
14929
|
if (meta?.type !== "run_meta" || meta.agentSource !== "route") return void 0;
|
|
@@ -16354,6 +16571,22 @@ var init_runDuration = __esm({
|
|
|
16354
16571
|
}
|
|
16355
16572
|
});
|
|
16356
16573
|
|
|
16574
|
+
// ../../src/core/authz/viewAs.ts
|
|
16575
|
+
function holdsAll(actor) {
|
|
16576
|
+
return actor.grants.actions === "all" && actor.grants.channels === "all" && actor.grants.repos === "all";
|
|
16577
|
+
}
|
|
16578
|
+
function isViewablePerson(id) {
|
|
16579
|
+
return /^slack:U[A-Z0-9]+$/.test(id);
|
|
16580
|
+
}
|
|
16581
|
+
function viewingRefusal(person) {
|
|
16582
|
+
return `You are viewing as ${person.name ?? person.id}; writes are your own to make \u2014 exit view-as to write.`;
|
|
16583
|
+
}
|
|
16584
|
+
var init_viewAs = __esm({
|
|
16585
|
+
"../../src/core/authz/viewAs.ts"() {
|
|
16586
|
+
"use strict";
|
|
16587
|
+
}
|
|
16588
|
+
});
|
|
16589
|
+
|
|
16357
16590
|
// ../../src/mcp/config.ts
|
|
16358
16591
|
function parseMcpSettings(raw) {
|
|
16359
16592
|
if (raw === void 0 || raw === null) return void 0;
|
|
@@ -16661,6 +16894,7 @@ var init_commandRegistry = __esm({
|
|
|
16661
16894
|
init_clock();
|
|
16662
16895
|
init_formatDuration();
|
|
16663
16896
|
init_authorize();
|
|
16897
|
+
init_viewAs();
|
|
16664
16898
|
init_capabilities();
|
|
16665
16899
|
init_untrusted();
|
|
16666
16900
|
COMMAND_ID = /^[a-z][a-z0-9]*\.[a-z][a-z0-9]*$/;
|
|
@@ -16753,6 +16987,7 @@ var init_commandRegistry = __esm({
|
|
|
16753
16987
|
callerKind: caller.kind,
|
|
16754
16988
|
callerId: caller.id,
|
|
16755
16989
|
...caller.actor.asUser ? { asUser: caller.actor.asUser.id } : {},
|
|
16990
|
+
...caller.actor.viewingAs ? { viewingAs: caller.actor.viewingAs.id } : {},
|
|
16756
16991
|
effect: cmd.effect,
|
|
16757
16992
|
outcome: res.ok ? "ok" : res.error,
|
|
16758
16993
|
...reason2 === void 0 ? {} : { reason: reason2 },
|
|
@@ -16760,6 +16995,8 @@ var init_commandRegistry = __esm({
|
|
|
16760
16995
|
});
|
|
16761
16996
|
return res;
|
|
16762
16997
|
};
|
|
16998
|
+
if (caller.actor.viewingAs && cmd.effect !== "read")
|
|
16999
|
+
return done(fail("unauthorized", viewingRefusal(caller.actor.viewingAs)), "viewing");
|
|
16763
17000
|
const decision = authorize(caller.actor, cmd.action, resourceOf(cmd, input, caller));
|
|
16764
17001
|
if (!decision.allow)
|
|
16765
17002
|
return done(fail("unauthorized", `${caller.id} is not allowed to run ${cmd.id}`), decision.reason);
|
|
@@ -17119,8 +17356,10 @@ var init_config4 = __esm({
|
|
|
17119
17356
|
`per-agent harness: --harness.<agent> ${harnessWords2} \u2014 which process drives that agent's runs (under me, your own runs only)`
|
|
17120
17357
|
),
|
|
17121
17358
|
boundary: boundaryOption,
|
|
17122
|
-
|
|
17123
|
-
addressSeverity: z6.enum(ADDRESS_SEVERITIES).optional().describe(
|
|
17359
|
+
review: z6.object({
|
|
17360
|
+
addressSeverity: z6.enum(ADDRESS_SEVERITIES).optional().describe(
|
|
17361
|
+
"the severity to address: a review's approve carrying a finding at or above it is a request_changes, and ship's rounds are held to it (--review.addressSeverity <level>)"
|
|
17362
|
+
)
|
|
17124
17363
|
}).optional(),
|
|
17125
17364
|
channel: channelOption
|
|
17126
17365
|
}),
|
|
@@ -17159,7 +17398,8 @@ var init_config4 = __esm({
|
|
|
17159
17398
|
if (options.effort !== void 0) patch.effort = options.effort;
|
|
17160
17399
|
if (options.efforts) patch.efforts = options.efforts;
|
|
17161
17400
|
if (options.harness) patch.harness = options.harness;
|
|
17162
|
-
if (options.
|
|
17401
|
+
if (options.review?.addressSeverity !== void 0)
|
|
17402
|
+
patch.review = { addressSeverity: options.review.addressSeverity };
|
|
17163
17403
|
if (options.boundary) {
|
|
17164
17404
|
const { machines, ...fields } = options.boundary;
|
|
17165
17405
|
const raw = { ...fields };
|
|
@@ -19520,7 +19760,7 @@ var init_help = __esm({
|
|
|
19520
19760
|
init_docsLink();
|
|
19521
19761
|
defineCommand10 = commandDefiner();
|
|
19522
19762
|
COMMANDS_REFERENCE_URL = `${PROJECT_DOCS_URL}/reference/slack-commands`;
|
|
19523
|
-
DIRECTIVES_HELP = "`agent:review model:anthropic/claude-opus-5 effort:low budget:20 look at PR 42` (effort: low | medium | high \u2014 lower = faster turns; budget: whole minutes
|
|
19763
|
+
DIRECTIVES_HELP = "`agent:review model:anthropic/claude-opus-5 effort:low budget:20 look at PR 42` (effort: low | medium | high \u2014 lower = faster turns; budget: whole minutes \u2014 narrows this run's wall clock, never widens it; a budget too short for a turn plus the write-up is refused by name)";
|
|
19524
19764
|
helpShow = defineCommand10({
|
|
19525
19765
|
id: "help.show",
|
|
19526
19766
|
action: "help:read",
|
|
@@ -23029,7 +23269,10 @@ function defineOp(op) {
|
|
|
23029
23269
|
`\`${args.slug}\` is not onboarded as a resident, so \`repo ${op}\` has nothing to run against \u2014 \`repo onboard ${args.slug}\` first, or ask the coding agent directly.`
|
|
23030
23270
|
);
|
|
23031
23271
|
case "error":
|
|
23032
|
-
throw new CommandError(
|
|
23272
|
+
throw new CommandError(
|
|
23273
|
+
"unavailable",
|
|
23274
|
+
result.transient === true ? `${result.message} \u2014 the resident was unavailable for a moment; re-run the command` : result.message
|
|
23275
|
+
);
|
|
23033
23276
|
}
|
|
23034
23277
|
}
|
|
23035
23278
|
});
|
|
@@ -27585,10 +27828,10 @@ async function closeRestartRow(adopted, restart, why) {
|
|
|
27585
27828
|
);
|
|
27586
27829
|
}
|
|
27587
27830
|
}
|
|
27588
|
-
async function closeResumedRow(adopted, resume, why) {
|
|
27831
|
+
async function closeResumedRow(adopted, resume, why, status3 = "interrupted") {
|
|
27589
27832
|
try {
|
|
27590
27833
|
await adopted.sink.put(
|
|
27591
|
-
reclaimedRunRecord({ row: resume.row, events: resume.events, status:
|
|
27834
|
+
reclaimedRunRecord({ row: resume.row, events: resume.events, status: status3, finishedAt: systemClock() })
|
|
27592
27835
|
);
|
|
27593
27836
|
} catch (err2) {
|
|
27594
27837
|
console.warn(
|
|
@@ -31527,6 +31770,12 @@ function capped2(base, max) {
|
|
|
31527
31770
|
if (n2 > max) ctx.addIssue({ code: z21.ZodIssueCode.custom, message: `at most ${max} visible characters (got ${n2})` });
|
|
31528
31771
|
});
|
|
31529
31772
|
}
|
|
31773
|
+
function cappedRaw(base, max) {
|
|
31774
|
+
return base.superRefine((s, ctx) => {
|
|
31775
|
+
const n2 = s.length;
|
|
31776
|
+
if (n2 > max) ctx.addIssue({ code: z21.ZodIssueCode.custom, message: `at most ${max} characters (got ${n2})` });
|
|
31777
|
+
});
|
|
31778
|
+
}
|
|
31530
31779
|
function boundedArray(item, min, max, what) {
|
|
31531
31780
|
return z21.array(item).superRefine((arr, ctx) => {
|
|
31532
31781
|
if (arr.length < min)
|
|
@@ -31933,6 +32182,10 @@ var init_prDescription = __esm({
|
|
|
31933
32182
|
"use strict";
|
|
31934
32183
|
init_redact();
|
|
31935
32184
|
PR_DESCRIPTION_CAPS = {
|
|
32185
|
+
/** The squash subject and the changelog line, the whole line counted: the
|
|
32186
|
+
* number the title gate (`scripts/check-pr-title.mjs`, `TITLE_MAX_LENGTH`)
|
|
32187
|
+
* holds it to; a test keeps the two equal. */
|
|
32188
|
+
title: 72,
|
|
31936
32189
|
tldr: 300,
|
|
31937
32190
|
why: 400,
|
|
31938
32191
|
pointers: 7,
|
|
@@ -31971,8 +32224,9 @@ var init_prDescription = __esm({
|
|
|
31971
32224
|
});
|
|
31972
32225
|
PrDescriptionSchema = z21.object({
|
|
31973
32226
|
/** The PR title's single source. Metadata for the PR's own title field —
|
|
31974
|
-
* never rendered into the body (GitHub shows the title itself)
|
|
31975
|
-
|
|
32227
|
+
* never rendered into the body (GitHub shows the title itself) — and the
|
|
32228
|
+
* squash subject, so it is capped like every other field. */
|
|
32229
|
+
title: cappedRaw(line2, PR_DESCRIPTION_CAPS.title),
|
|
31976
32230
|
tldr: capped2(prose, PR_DESCRIPTION_CAPS.tldr),
|
|
31977
32231
|
why: capped2(prose, PR_DESCRIPTION_CAPS.why),
|
|
31978
32232
|
pointers: boundedArray(PointerSchema, 1, PR_DESCRIPTION_CAPS.pointers, "pointers"),
|
|
@@ -32054,7 +32308,7 @@ var init_submit = __esm({
|
|
|
32054
32308
|
submitVerdictTool = {
|
|
32055
32309
|
name: "submit_verdict",
|
|
32056
32310
|
failsInText: true,
|
|
32057
|
-
description: "Record your review verdict. REQUIRED before your final message when reviewing a PR: `approve` when
|
|
32311
|
+
description: "Record your review verdict. REQUIRED before your final message when reviewing a PR: `approve` when no finding at or above the severity to address remains (the level in force for this run, `minor` by default: a major or minor finding means `request_changes`; nits alone never block), `request_changes` otherwise. Switchboard writes the verdict as the first line of the GitHub comment itself (`LGTM:` only for approve); a review with no submitted verdict is posted as NOT approving. Call it once, after your analysis; a later call replaces the earlier one. `head` is the commit you reviewed \u2014 run `git rev-parse HEAD` in the checkout you read and tested and pass its output; Switchboard posts to the PR only if that commit IS the PR's head, so a review of the wrong branch can never land on a PR. Enumerate EVERY issue you report in `findings` with STABLE ids assigned in order (F1, F2, \u2026) \u2014 a fix round references findings by these ids, so never renumber them. Severity is exactly one of blocking|major|minor|nit; the entry carries the file (plus line when it points at one) and a one-line title, while the full explanation stays in your review text keyed by the same ids. An `approve` carrying a finding at or above the severity to address is downgraded to `request_changes` and the ack names the finding and the level \u2014 approve only when every finding sits below it.",
|
|
32058
32312
|
inputSchema: {
|
|
32059
32313
|
type: "object",
|
|
32060
32314
|
properties: {
|
|
@@ -32093,10 +32347,13 @@ var init_submit = __esm({
|
|
|
32093
32347
|
required: ["verdict", "summary", "head"]
|
|
32094
32348
|
},
|
|
32095
32349
|
async run(input, ctx) {
|
|
32096
|
-
const verdict = parseVerdictInput(input
|
|
32350
|
+
const verdict = parseVerdictInput(input, {
|
|
32351
|
+
...ctx.addressSeverity !== void 0 ? { addressSeverity: ctx.addressSeverity } : {}
|
|
32352
|
+
});
|
|
32097
32353
|
if (!verdict) return "error: verdict must be exactly `approve` or `request_changes`";
|
|
32098
32354
|
ctx.onVerdict?.(verdict);
|
|
32099
32355
|
const notes = [];
|
|
32356
|
+
if (verdict.downgraded) notes.push(`downgraded from approve: ${downgradeNote(verdict.downgraded)}`);
|
|
32100
32357
|
if (verdict.findings) notes.push(`${verdict.findings.length} finding${verdict.findings.length === 1 ? "" : "s"}`);
|
|
32101
32358
|
if (verdict.droppedFindings?.length) notes.push(`dropped: ${verdict.droppedFindings.join("; ")}`);
|
|
32102
32359
|
return notes.length ? `verdict recorded: ${verdict.verdict} (${notes.join("; ")})` : `verdict recorded: ${verdict.verdict}`;
|
|
@@ -32141,7 +32398,10 @@ var init_submit = __esm({
|
|
|
32141
32398
|
inputSchema: {
|
|
32142
32399
|
type: "object",
|
|
32143
32400
|
properties: {
|
|
32144
|
-
title: {
|
|
32401
|
+
title: {
|
|
32402
|
+
type: "string",
|
|
32403
|
+
description: "The PR title \u2014 one line naming the change, `type(scope): what a reader can now do or expect`, at most 72 characters in all"
|
|
32404
|
+
},
|
|
32145
32405
|
tldr: {
|
|
32146
32406
|
type: "string",
|
|
32147
32407
|
description: "Two sentences for a reader with zero context: what this PR does and why it matters (\u2264300 visible chars)"
|
|
@@ -33597,7 +33857,7 @@ function parseDirectives(input) {
|
|
|
33597
33857
|
} else if (f.key === "severity") {
|
|
33598
33858
|
if (!isAddressSeverity(f.value)) {
|
|
33599
33859
|
throw new Error(
|
|
33600
|
-
`Unknown severity "${f.value}". severity:<level> takes one of ${ADDRESS_SEVERITIES.join(", ")} \u2014 the
|
|
33860
|
+
`Unknown severity "${f.value}". severity:<level> takes one of ${ADDRESS_SEVERITIES.join(", ")} \u2014 the severity to address: a review's approve carrying a finding at or above it is a request_changes.`
|
|
33601
33861
|
);
|
|
33602
33862
|
}
|
|
33603
33863
|
out.severity = f.value;
|
|
@@ -33693,7 +33953,12 @@ function resolveProfile(ctx) {
|
|
|
33693
33953
|
const carried = ctx.resume?.row.meta.profile;
|
|
33694
33954
|
const declared = carried ? { machine: carried.machine, identity: carried.identity, maxMinutes: carried.minutes } : ctx.agent;
|
|
33695
33955
|
const boundary = ctx.parentRemainingMs !== void 0 ? boundedByParent(ctx.resolved.boundary, ctx.parentRemainingMs) : ctx.resolved.boundary;
|
|
33696
|
-
const resolution = effectiveProfile(
|
|
33956
|
+
const resolution = effectiveProfile(
|
|
33957
|
+
declared,
|
|
33958
|
+
ctx.budget !== void 0 ? { budget: ctx.budget } : {},
|
|
33959
|
+
boundary,
|
|
33960
|
+
leaseMinimum(ctx.agent.name)
|
|
33961
|
+
);
|
|
33697
33962
|
if (resolution.kind === "profile" && carried?.boundedBy && !resolution.profile.boundedBy) {
|
|
33698
33963
|
return { kind: "profile", profile: { ...resolution.profile, boundedBy: carried.boundedBy } };
|
|
33699
33964
|
}
|
|
@@ -33730,6 +33995,7 @@ function repoVetFor(profile, resident) {
|
|
|
33730
33995
|
var init_resolve = __esm({
|
|
33731
33996
|
"../../src/core/dispatch/resolve.ts"() {
|
|
33732
33997
|
"use strict";
|
|
33998
|
+
init_budgets();
|
|
33733
33999
|
init_registry();
|
|
33734
34000
|
init_profile();
|
|
33735
34001
|
init_directives();
|
|
@@ -33953,7 +34219,7 @@ var init_digestCoverage = __esm({
|
|
|
33953
34219
|
|
|
33954
34220
|
// ../../src/core/reviewRound.ts
|
|
33955
34221
|
function releaseModeFor(identity2, opts) {
|
|
33956
|
-
return identity2 === "read" || opts.hardStopped ? "always" : "if-idle";
|
|
34222
|
+
return identity2 === "read" || opts.hardStopped || opts.commandInFlight === true || opts.gateBypassed === true ? "always" : "if-idle";
|
|
33957
34223
|
}
|
|
33958
34224
|
async function attachRoundWorkspace(input) {
|
|
33959
34225
|
const { agent, profile } = input.round;
|
|
@@ -33967,7 +34233,8 @@ async function attachRoundWorkspace(input) {
|
|
|
33967
34233
|
ref: input.round.ref,
|
|
33968
34234
|
headSha: input.round.headSha,
|
|
33969
34235
|
...input.round.ownPr !== void 0 ? { ownPr: input.round.ownPr } : {},
|
|
33970
|
-
...input.round.reattach !== void 0 ? { reattach: input.round.reattach } : {}
|
|
34236
|
+
...input.round.reattach !== void 0 ? { reattach: input.round.reattach } : {},
|
|
34237
|
+
...input.round.stopSignal !== void 0 ? { stopSignal: input.round.stopSignal } : {}
|
|
33971
34238
|
},
|
|
33972
34239
|
input.span
|
|
33973
34240
|
);
|
|
@@ -34109,7 +34376,8 @@ async function settle(input, span) {
|
|
|
34109
34376
|
let worktreeMoved = false;
|
|
34110
34377
|
if (executor.moveTo) {
|
|
34111
34378
|
try {
|
|
34112
|
-
const
|
|
34379
|
+
const moved = await executor.moveTo(current, { ...trace, signal: turn.control.hardSignal });
|
|
34380
|
+
const at = normalizeHead(moved.sha);
|
|
34113
34381
|
worktreeMoved = at !== void 0 && sameCommit(at, current);
|
|
34114
34382
|
console.log(
|
|
34115
34383
|
`[review] ${logKey} worktree moved to ${at?.slice(0, 7) ?? "?"}${worktreeMoved ? "" : " (not the expected head)"}`
|
|
@@ -34313,7 +34581,14 @@ async function authorizeProfile(deps, ctx) {
|
|
|
34313
34581
|
return { kind: "refused", reason: "profile_bounded" };
|
|
34314
34582
|
}
|
|
34315
34583
|
function refusalSummary(refusal) {
|
|
34316
|
-
|
|
34584
|
+
switch (refusal.axis) {
|
|
34585
|
+
case "identity":
|
|
34586
|
+
return `identity ${refusal.needs} > ${refusal.cap} (${refusal.scope})`;
|
|
34587
|
+
case "machine":
|
|
34588
|
+
return `machine ${refusal.needs} not in ${refusal.allowed.join(",")} (${refusal.scopes.join(",")})`;
|
|
34589
|
+
case "minutes":
|
|
34590
|
+
return `minutes ${refusal.have} < ${refusal.needs} (${refusal.scope})`;
|
|
34591
|
+
}
|
|
34317
34592
|
}
|
|
34318
34593
|
function boundaryOf(scope) {
|
|
34319
34594
|
switch (scope) {
|
|
@@ -34340,7 +34615,21 @@ function wayForward(scope, axis, needs, adminsHint) {
|
|
|
34340
34615
|
case "directive":
|
|
34341
34616
|
return "send the message again without the budget directive";
|
|
34342
34617
|
case "parent":
|
|
34343
|
-
return
|
|
34618
|
+
return `spawn it from a run with at least ${needs} left`;
|
|
34619
|
+
}
|
|
34620
|
+
}
|
|
34621
|
+
function minutesWayForward(scope, needs, adminsHint) {
|
|
34622
|
+
switch (scope) {
|
|
34623
|
+
case "directive":
|
|
34624
|
+
return `send the message again with \`budget:${needs}\` or more`;
|
|
34625
|
+
case "user":
|
|
34626
|
+
return `raise your own boundary with \`config set me --boundary.maxMinutes ${needs}\`, or drop your overrides with \`config clear me\``;
|
|
34627
|
+
case "channel":
|
|
34628
|
+
return `run it in a channel whose boundary allows ${needs} minutes, or ask ${adminsHint} to raise this channel's boundary`;
|
|
34629
|
+
case "defaults":
|
|
34630
|
+
return `ask ${adminsHint} to raise \`defaults.boundary\` in the configuration`;
|
|
34631
|
+
case "parent":
|
|
34632
|
+
return `spawn it from a run with at least ${needs} minutes left`;
|
|
34344
34633
|
}
|
|
34345
34634
|
}
|
|
34346
34635
|
function profileRefusalReply(agentName, refusal, adminsHint) {
|
|
@@ -34349,6 +34638,10 @@ function profileRefusalReply(agentName, refusal, adminsHint) {
|
|
|
34349
34638
|
const how2 = wayForward(refusal.scope, "maxIdentity", refusal.needs, adminsHint);
|
|
34350
34639
|
return `\u{1F6AB} \`${agentName}\` needs a \`${refusal.needs}\` credential; ${boundaryOf(refusal.scope)} caps runs at \`${refusal.cap}\`. ${capitalize(how2)}.`;
|
|
34351
34640
|
}
|
|
34641
|
+
if (refusal.axis === "minutes") {
|
|
34642
|
+
const how2 = minutesWayForward(refusal.scope, refusal.needs, adminsHint);
|
|
34643
|
+
return `\u{1F6AB} \`${agentName}\` needs at least ${refusal.needs} minutes \u2014 a turn, then its write-up and post-step \u2014 and ${boundaryOf(refusal.scope)} gives it ${refusal.have}. ${capitalize(how2)}.`;
|
|
34644
|
+
}
|
|
34352
34645
|
const scopes = refusal.scopes.map(boundaryOf);
|
|
34353
34646
|
const who2 = scopes.length > 1 ? `${scopes.join(" and ")} allow` : `${scopes[0] ?? "the boundary"} allows`;
|
|
34354
34647
|
const allowed2 = refusal.allowed.map((m) => `\`${m}\``).join(", ");
|
|
@@ -35225,7 +35518,7 @@ async function reserveRun(deps, ctx) {
|
|
|
35225
35518
|
return void 0;
|
|
35226
35519
|
}
|
|
35227
35520
|
async function attachRound(deps, ctx) {
|
|
35228
|
-
const { threadKey, agent, profile, repoCtx, root, clock: clock4, reattach } = ctx;
|
|
35521
|
+
const { threadKey, agent, profile, repoCtx, root, clock: clock4, reattach, stopSignal } = ctx;
|
|
35229
35522
|
const ownPr = ownPrOf(repoCtx);
|
|
35230
35523
|
return root.span("dispatch.workspace.attach", async (span) => {
|
|
35231
35524
|
const graft = (steps, residentTotalMs) => graftResidentSteps(steps, {
|
|
@@ -35253,7 +35546,8 @@ async function attachRound(deps, ctx) {
|
|
|
35253
35546
|
// The thread's own pull request, when the ref is its head (resident-
|
|
35254
35547
|
// repos item 16): the one reason the resident may move a binding.
|
|
35255
35548
|
...ownPr !== void 0 ? { ownPr } : {},
|
|
35256
|
-
...reattach !== void 0 ? { reattach } : {}
|
|
35549
|
+
...reattach !== void 0 ? { reattach } : {},
|
|
35550
|
+
...stopSignal !== void 0 ? { stopSignal } : {}
|
|
35257
35551
|
},
|
|
35258
35552
|
logKey: threadKey,
|
|
35259
35553
|
span
|
|
@@ -35273,11 +35567,12 @@ async function reattachWorkspace2(deps, ctx) {
|
|
|
35273
35567
|
return { kind: "attached", round: await attachRound(deps, ctx) };
|
|
35274
35568
|
} catch (err2) {
|
|
35275
35569
|
if (err2 instanceof WorkspaceReattachRefusedError) return { kind: "reattach_refused", why: err2.why };
|
|
35570
|
+
if (isRunStopError(err2)) return { kind: "stopped" };
|
|
35276
35571
|
throw err2;
|
|
35277
35572
|
}
|
|
35278
35573
|
}
|
|
35279
35574
|
async function attachWorkspace(deps, ctx) {
|
|
35280
|
-
const { msg, io, refuse: refuse3, card, shell, closeLines, clock: clock4, agent, profile, repoCtx, root, reattach } = ctx;
|
|
35575
|
+
const { msg, io, refuse: refuse3, card, shell, closeLines, clock: clock4, agent, profile, repoCtx, root, reattach, stopSignal } = ctx;
|
|
35281
35576
|
let round2;
|
|
35282
35577
|
try {
|
|
35283
35578
|
round2 = await attachRound(deps, {
|
|
@@ -35287,7 +35582,8 @@ async function attachWorkspace(deps, ctx) {
|
|
|
35287
35582
|
repoCtx,
|
|
35288
35583
|
root,
|
|
35289
35584
|
clock: clock4,
|
|
35290
|
-
...reattach !== void 0 ? { reattach } : {}
|
|
35585
|
+
...reattach !== void 0 ? { reattach } : {},
|
|
35586
|
+
...stopSignal !== void 0 ? { stopSignal } : {}
|
|
35291
35587
|
});
|
|
35292
35588
|
} catch (err2) {
|
|
35293
35589
|
if (err2 instanceof ResidentNeedsRefError) {
|
|
@@ -35303,6 +35599,7 @@ async function attachWorkspace(deps, ctx) {
|
|
|
35303
35599
|
return { kind: "refused", reason: "which_branch" };
|
|
35304
35600
|
}
|
|
35305
35601
|
if (err2 instanceof WorkspaceReattachRefusedError) return { kind: "reattach_refused", why: err2.why };
|
|
35602
|
+
if (isRunStopError(err2)) return { kind: "stopped" };
|
|
35306
35603
|
throw err2;
|
|
35307
35604
|
}
|
|
35308
35605
|
return { kind: "attached", round: round2 };
|
|
@@ -35449,6 +35746,7 @@ var init_provision = __esm({
|
|
|
35449
35746
|
init_actor();
|
|
35450
35747
|
init_profile();
|
|
35451
35748
|
init_factory();
|
|
35749
|
+
init_executor();
|
|
35452
35750
|
init_residentTrace();
|
|
35453
35751
|
init_resident();
|
|
35454
35752
|
init_memory();
|
|
@@ -35679,7 +35977,7 @@ function openThroughSeam(harness, deps, run2) {
|
|
|
35679
35977
|
}
|
|
35680
35978
|
return harness.open(deps, run2);
|
|
35681
35979
|
}
|
|
35682
|
-
var isRecord4, relaunchesOf, isPiFacts, HarnessInterruptedError, HarnessMismatchError, RELAUNCH_CEILING, HarnessContainerReplacedError;
|
|
35980
|
+
var isRecord4, relaunchesOf, isPiFacts, HarnessInterruptedError, HarnessMismatchError, HarnessGateBypassedError, RELAUNCH_CEILING, HarnessContainerReplacedError;
|
|
35683
35981
|
var init_contract4 = __esm({
|
|
35684
35982
|
"../../src/core/harness/contract.ts"() {
|
|
35685
35983
|
"use strict";
|
|
@@ -35710,6 +36008,12 @@ var init_contract4 = __esm({
|
|
|
35710
36008
|
expected;
|
|
35711
36009
|
found;
|
|
35712
36010
|
};
|
|
36011
|
+
HarnessGateBypassedError = class extends Error {
|
|
36012
|
+
constructor(message4) {
|
|
36013
|
+
super(message4);
|
|
36014
|
+
this.name = "HarnessGateBypassedError";
|
|
36015
|
+
}
|
|
36016
|
+
};
|
|
35713
36017
|
RELAUNCH_CEILING = 2;
|
|
35714
36018
|
HarnessContainerReplacedError = class extends HarnessInterruptedError {
|
|
35715
36019
|
constructor(message4, said2, was, now, record2, condition = "word") {
|
|
@@ -35766,8 +36070,10 @@ async function prepareRelaunch(deps, ctx) {
|
|
|
35766
36070
|
repoCtx: ctx.repoCtx,
|
|
35767
36071
|
root: ctx.root,
|
|
35768
36072
|
clock: ctx.clock,
|
|
35769
|
-
reattach: ctx.binding
|
|
36073
|
+
reattach: ctx.binding,
|
|
36074
|
+
...ctx.stopSignal !== void 0 ? { stopSignal: ctx.stopSignal } : {}
|
|
35770
36075
|
});
|
|
36076
|
+
if (reattached.kind === "stopped") return { kind: "stopped" };
|
|
35771
36077
|
if (reattached.kind === "reattach_refused")
|
|
35772
36078
|
return refuse3(
|
|
35773
36079
|
`the run's workspace could not be re-attached in the replacement container (${reattached.why}); the run restarts from its request as a new run in this thread`,
|
|
@@ -35976,9 +36282,25 @@ async function identityOrNothing(container) {
|
|
|
35976
36282
|
function saysContainerDown(err2) {
|
|
35977
36283
|
return err2 instanceof Error && !isContainerGone(err2) && !RUNTIME_WORD.test(err2.message) && CONTAINER_DOWN_WORDING.test(err2.message);
|
|
35978
36284
|
}
|
|
36285
|
+
function* selfAndCauses(err2) {
|
|
36286
|
+
const seen = /* @__PURE__ */ new Set();
|
|
36287
|
+
for (let e = err2; e !== void 0 && e !== null && !seen.has(e); e = e.cause) {
|
|
36288
|
+
seen.add(e);
|
|
36289
|
+
yield e;
|
|
36290
|
+
}
|
|
36291
|
+
}
|
|
36292
|
+
function infraErrorOf(err2) {
|
|
36293
|
+
for (const e of selfAndCauses(err2)) if (e instanceof ExecInfraError) return e;
|
|
36294
|
+
return void 0;
|
|
36295
|
+
}
|
|
35979
36296
|
function saysTransportLost(err2) {
|
|
35980
36297
|
if (!(err2 instanceof Error) || isContainerGone(err2) || RUNTIME_WORD.test(err2.message)) return false;
|
|
35981
36298
|
if (err2 instanceof HarnessControlFileLostError) return false;
|
|
36299
|
+
const typed = infraErrorOf(err2);
|
|
36300
|
+
if (typed !== void 0) {
|
|
36301
|
+
if (infraMayClear(typed)) return true;
|
|
36302
|
+
if (typed.reason !== "answered") return false;
|
|
36303
|
+
}
|
|
35982
36304
|
return err2 instanceof HarnessContainerDownError || CONTAINER_DOWN_WORDING.test(err2.message);
|
|
35983
36305
|
}
|
|
35984
36306
|
function isControlReset(err2) {
|
|
@@ -36141,7 +36463,7 @@ var init_container = __esm({
|
|
|
36141
36463
|
}
|
|
36142
36464
|
};
|
|
36143
36465
|
CONTAINER_DOWN_WORDING = new RegExp(
|
|
36144
|
-
`${STOPPED_CONTAINER_WORDING.source}|${CONTAINER_GONE_WORDING.source}|peer closed websocket|without sending close frame|\\bECONNRESET\\b|connection reset|socket hang up`,
|
|
36466
|
+
`${STOPPED_CONTAINER_WORDING.source}|${CONTAINER_GONE_WORDING.source}|peer closed websocket|without sending close frame|\\bECONNRESET\\b|connection reset|socket hang up|network connection lost`,
|
|
36145
36467
|
"i"
|
|
36146
36468
|
);
|
|
36147
36469
|
HarnessContainerDownError = class extends HarnessContainerError {
|
|
@@ -36683,7 +37005,7 @@ function elapsedMinutes(ms) {
|
|
|
36683
37005
|
if (minutes < 1) return "under a minute";
|
|
36684
37006
|
return `${minutes} minute${minutes === 1 ? "" : "s"}`;
|
|
36685
37007
|
}
|
|
36686
|
-
var HARD_STOP_MESSAGE, CONTINUE_PROMPT, wrapUpNote, wrapUpInstruction, WRITE_UP_REQUEST, timeBudgetInstruction, turnGuardInstruction, SOFT_STOP_INSTRUCTION, turnGuardPace, MODEL_CALL_IN_FLIGHT, timeBudgetNote, turnGuardNote, softStopNote, hardStopNote, windDownFailureNote, finaleTimedOutNote, wrapUpUndeliveredNote, wrapUpWriteFailedNote, wrapUpName, finaleAbortReason, noWriteUp, timeBudgetAnswer, turnGuardAnswer, softStopAnswer;
|
|
37008
|
+
var HARD_STOP_MESSAGE, CONTINUE_PROMPT, wrapUpNote, wrapUpInstruction, WRITE_UP_REQUEST, timeBudgetInstruction, turnGuardInstruction, SOFT_STOP_INSTRUCTION, turnGuardPace, MODEL_CALL_IN_FLIGHT, timeBudgetNote, toolCutNote, turnGuardNote, softStopNote, hardStopNote, windDownFailureNote, finaleTimedOutNote, wrapUpUndeliveredNote, wrapUpWriteFailedNote, wrapUpName, finaleAbortReason, noWriteUp, timeBudgetAnswer, turnGuardAnswer, softStopAnswer;
|
|
36687
37009
|
var init_windDown = __esm({
|
|
36688
37010
|
"../../src/core/harness/windDown.ts"() {
|
|
36689
37011
|
"use strict";
|
|
@@ -36698,6 +37020,7 @@ var init_windDown = __esm({
|
|
|
36698
37020
|
turnGuardPace = (turns, elapsedMs) => `${turns} model turn${turns === 1 ? "" : "s"} in ${elapsedMinutes(elapsedMs)}`;
|
|
36699
37021
|
MODEL_CALL_IN_FLIGHT = "a model call was in flight";
|
|
36700
37022
|
timeBudgetNote = (doing) => doing ? `the loop's time is up while ${doing} \u2014 writing up findings so far inside the lease` : "the loop's time is up \u2014 writing up findings so far inside the lease";
|
|
37023
|
+
toolCutNote = (doing) => `the loop's end cut the command in flight (${doing}) so the write-up keeps its allowance`;
|
|
36701
37024
|
turnGuardNote = (pace) => `turn guard fired: ${pace}, a pace that looks like a loop \u2014 writing up findings so far`;
|
|
36702
37025
|
softStopNote = () => "soft stop \u2014 no further steps, writing up findings so far";
|
|
36703
37026
|
hardStopNote = () => "hard stop \u2014 run aborted, no summary written";
|
|
@@ -37205,6 +37528,10 @@ var init_bridge2 = __esm({
|
|
|
37205
37528
|
/** The calls under way: their span, their tool, and whether their end is
|
|
37206
37529
|
* judged against the gate (harness-pi item 7). */
|
|
37207
37530
|
openTools = /* @__PURE__ */ new Map();
|
|
37531
|
+
/** The open calls an abort was sent for (`markOpenCallsCut`): their end, when
|
|
37532
|
+
* pi answers it, is the abort's cut and not a settle — the result is marked
|
|
37533
|
+
* `cut`, and the workspace's release reads the command as one that may run on. */
|
|
37534
|
+
cutCalls = /* @__PURE__ */ new Set();
|
|
37208
37535
|
/** The calls the gate saw — the extension asked `/harness/authorize` for
|
|
37209
37536
|
* them (`gateSaw`) — not yet ended. */
|
|
37210
37537
|
vetted = /* @__PURE__ */ new Set();
|
|
@@ -37364,6 +37691,7 @@ var init_bridge2 = __esm({
|
|
|
37364
37691
|
const callId = str5(event.toolCallId);
|
|
37365
37692
|
const open2 = this.openTools.get(callId);
|
|
37366
37693
|
this.openTools.delete(callId);
|
|
37694
|
+
const cut = this.cutCalls.delete(callId);
|
|
37367
37695
|
const tool = open2?.tool ?? str5(event.toolName);
|
|
37368
37696
|
const isError = event.isError === true;
|
|
37369
37697
|
const text = piResultText(event.result);
|
|
@@ -37376,7 +37704,8 @@ var init_bridge2 = __esm({
|
|
|
37376
37704
|
callId,
|
|
37377
37705
|
...exit.exitCode !== void 0 ? { exitCode: exit.exitCode } : {},
|
|
37378
37706
|
...prepareToolResult(text),
|
|
37379
|
-
...open2?.span ? { spanId: open2.span.id } : {}
|
|
37707
|
+
...open2?.span ? { spanId: open2.span.id } : {},
|
|
37708
|
+
...cut ? { cut: true } : {}
|
|
37380
37709
|
});
|
|
37381
37710
|
open2?.span?.end(ok2 ? "ok" : "error", {
|
|
37382
37711
|
callId,
|
|
@@ -37452,17 +37781,35 @@ var init_bridge2 = __esm({
|
|
|
37452
37781
|
if (open2.length > 0) return `running ${open2.join(", ")}`;
|
|
37453
37782
|
return this.modelCallOpen ? MODEL_CALL_IN_FLIGHT : void 0;
|
|
37454
37783
|
}
|
|
37784
|
+
/** An abort is being sent to pi: every call open now is cut by it, not
|
|
37785
|
+
* settled — its end, when pi answers, lands marked `cut` (harness.md item 13:
|
|
37786
|
+
* the command behind it may run on, and the workspace's release reads it so). */
|
|
37787
|
+
markOpenCallsCut() {
|
|
37788
|
+
for (const callId of this.openTools.keys()) this.cutCalls.add(callId);
|
|
37789
|
+
}
|
|
37455
37790
|
/** End whatever tool spans a stopped pi left open, so no span outlives the
|
|
37456
37791
|
* run; `reason` is each call's result summary — one for all, or the note
|
|
37457
37792
|
* each call is settled with (harness-pi item 16: the restart note, said of
|
|
37458
|
-
* the container).
|
|
37459
|
-
|
|
37793
|
+
* the container). `cut` marks each result when the caller is the session's
|
|
37794
|
+
* end, where pi is gone but its tree persists and a relayed command may run
|
|
37795
|
+
* on — the workspace's release reads it as in flight (harness.md item 13);
|
|
37796
|
+
* the container-replaced verdict leaves it unmarked, the old container's
|
|
37797
|
+
* calls gone and the relayed ones taken over by the relaunch. */
|
|
37798
|
+
closeOpenSpans(reason2, opts = {}) {
|
|
37460
37799
|
for (const [callId, open2] of this.openTools) {
|
|
37461
37800
|
open2.span?.end("error", { callId, ok: false });
|
|
37462
37801
|
const summary = typeof reason2 === "string" ? reason2 : reason2({ callId, tool: open2.tool });
|
|
37463
|
-
this.emit({
|
|
37802
|
+
this.emit({
|
|
37803
|
+
type: "tool_result",
|
|
37804
|
+
tool: open2.tool,
|
|
37805
|
+
ok: false,
|
|
37806
|
+
callId,
|
|
37807
|
+
summary: redactAndCap(summary),
|
|
37808
|
+
...opts.cut ? { cut: true } : {}
|
|
37809
|
+
});
|
|
37464
37810
|
}
|
|
37465
37811
|
this.openTools.clear();
|
|
37812
|
+
this.cutCalls.clear();
|
|
37466
37813
|
this.vetted.clear();
|
|
37467
37814
|
}
|
|
37468
37815
|
};
|
|
@@ -37529,54 +37876,48 @@ var init_transport = __esm({
|
|
|
37529
37876
|
send(command2) {
|
|
37530
37877
|
void this.write(command2);
|
|
37531
37878
|
}
|
|
37532
|
-
/** Send, and say how the write came out once the chain reaches it (
|
|
37533
|
-
* item 16)
|
|
37534
|
-
*
|
|
37535
|
-
*
|
|
37536
|
-
* `
|
|
37537
|
-
*
|
|
37879
|
+
/** Send, and say how the write came out once the chain reaches it (`Landing`,
|
|
37880
|
+
* harness-pi item 16). What the gate's `onLanded` waits on: a clock stamped
|
|
37881
|
+
* at the landing (the finale's) starts when pi has the steer, not when a
|
|
37882
|
+
* chain that only held it was handed it. A write to a closed transport is
|
|
37883
|
+
* `dropped`: nothing keeps it, so nothing will re-send it.
|
|
37884
|
+
*
|
|
37885
|
+
* An abort takes the same step with three differences (the one way in: the
|
|
37886
|
+
* gate passes it by type, `HeldSends.send`). It is never queued, so no
|
|
37887
|
+
* re-attach can replay it from `takeUnsent`; a spent chain does not stop it
|
|
37888
|
+
* — the reset that failed a prompt a moment after the abort was asked for
|
|
37889
|
+
* must not swallow the stop, and a teardown's abort still reaches pi past
|
|
37890
|
+
* an earlier failure; and its own failure is nobody's — the transport is
|
|
37891
|
+
* being left and the process ended by `kill`. Behind whatever is in flight,
|
|
37892
|
+
* never a second writer: a line is a separate exec into the FIFO and a line
|
|
37893
|
+
* over PIPE_BUF is several `write(2)`s. On a transport abandoned to a
|
|
37894
|
+
* re-attach by the time its step runs it is `dropped`, not held: never
|
|
37895
|
+
* queued, nothing re-sends it, and its sender is told — the pi harness
|
|
37896
|
+
* notes a dropped abort as its own error, so a stop swallowed there is
|
|
37897
|
+
* seen, never assumed. Nothing chains one there today: every abort's
|
|
37898
|
+
* sender in the pi harness is the loop or the turn, both suspended in the
|
|
37899
|
+
* recovery that abandons, and the OpenCode bridge never writes on its
|
|
37900
|
+
* transport. */
|
|
37538
37901
|
write(command2) {
|
|
37539
|
-
if (this.closed) return Promise.resolve("
|
|
37540
|
-
|
|
37541
|
-
this.sendAbort();
|
|
37542
|
-
return this.sending.then(() => "landed");
|
|
37543
|
-
}
|
|
37902
|
+
if (this.closed) return Promise.resolve("dropped");
|
|
37903
|
+
const abort = command2.type === "abort";
|
|
37544
37904
|
const line3 = JSON.stringify(command2);
|
|
37545
|
-
this.queued.push(command2);
|
|
37905
|
+
if (!abort) this.queued.push(command2);
|
|
37546
37906
|
const step = this.sending.then(() => {
|
|
37547
|
-
if (this.
|
|
37548
|
-
this.
|
|
37907
|
+
if (this.heldForReattach) return abort ? "dropped" : "held";
|
|
37908
|
+
if (!abort && this.sendError !== void 0) return "held";
|
|
37909
|
+
if (!abort) this.queued.shift();
|
|
37549
37910
|
return this.deps.container.writeLine(this.deps.paths, line3).then(() => "landed");
|
|
37550
37911
|
}).catch((err2) => {
|
|
37551
|
-
|
|
37552
|
-
|
|
37912
|
+
if (!abort) {
|
|
37913
|
+
this.sendError ??= err2 instanceof Error ? err2 : new Error(String(err2));
|
|
37914
|
+
this.pendingSend ??= command2;
|
|
37915
|
+
}
|
|
37553
37916
|
return "failed";
|
|
37554
37917
|
});
|
|
37555
37918
|
this.sending = step.then(() => void 0);
|
|
37556
37919
|
return step;
|
|
37557
37920
|
}
|
|
37558
|
-
/** The abort — a hard stop's, the finale's, a gate bypass's — the one write
|
|
37559
|
-
* that never waits for the gate and never waits for a re-attach (harness-pi
|
|
37560
|
-
* item 16): ONE step on the transport's chain that a spent chain does not
|
|
37561
|
-
* stop. Nothing in flight — an idle chain, or one a failure already spent —
|
|
37562
|
-
* and the step runs at once: that is the direct write. A write in flight or
|
|
37563
|
-
* queued on a live chain, and the abort follows it: whichever prompt the
|
|
37564
|
-
* stop was asked for during, pi has the prompt first and the abort after,
|
|
37565
|
-
* never an idle session aborted and then handed the prompt to run unwatched.
|
|
37566
|
-
* Never two writers: a line is a separate exec into the FIFO and a line over
|
|
37567
|
-
* PIPE_BUF is several `write(2)`s, so a direct write beside an in-flight one
|
|
37568
|
-
* could land inside pi's line and cost both. The step writes even when the
|
|
37569
|
-
* write before it spent the chain (the reset that failed a prompt a moment
|
|
37570
|
-
* after the abort was asked for must not swallow the stop), and skips only
|
|
37571
|
-
* on a transport abandoned to a re-attach — its fresh transport is the one
|
|
37572
|
-
* writer then; never queued for `takeUnsent`, so no re-attach replays it; a
|
|
37573
|
-
* failed write is nobody's — the transport is being left, the process
|
|
37574
|
-
* ended by `kill`. */
|
|
37575
|
-
sendAbort() {
|
|
37576
|
-
if (this.closed) return;
|
|
37577
|
-
const line3 = JSON.stringify({ type: "abort" });
|
|
37578
|
-
this.sending = this.sending.then(() => this.heldForReattach ? void 0 : this.deps.container.writeLine(this.deps.paths, line3)).catch(() => void 0);
|
|
37579
|
-
}
|
|
37580
37921
|
/** Whether the reader is at the log's end as far as it knows: every record
|
|
37581
37922
|
* read has been handed out and the last read was short. The gate reads its
|
|
37582
37923
|
* clock only then (`HeldSends.quiet`): a catch-up burst still being handed
|
|
@@ -37608,7 +37949,7 @@ var init_transport = __esm({
|
|
|
37608
37949
|
/** Close for teardown: a write already queued still lands — unless an
|
|
37609
37950
|
* earlier write failed, when it is held (`takeUnsent`) and this transport
|
|
37610
37951
|
* delivers nothing more of the chain. An abort is the one exception: its
|
|
37611
|
-
* step ignores the spent chain (`
|
|
37952
|
+
* step ignores the spent chain (`write`'s abort step), so a teardown's abort still
|
|
37612
37953
|
* reaches pi even then; the process is ended by `kill` regardless, so
|
|
37613
37954
|
* teardown owes nothing to a held write. */
|
|
37614
37955
|
close() {
|
|
@@ -37716,7 +38057,7 @@ function resolveControlResetWrite(command2, echoed = () => false) {
|
|
|
37716
38057
|
message: `a ${type} command was in flight when the resident's control plane reset under the run; its outcome is unknown and it has no echo to resolve it by, so the run cannot continue safely`
|
|
37717
38058
|
};
|
|
37718
38059
|
}
|
|
37719
|
-
var MAX_INPLACE_REATTACHES, PROMPT_ECHO_WAIT_MS, WORD_ALIVE_REATTACH_NOTE, CONTROL_RESET_RESUMED_NOTE, RESEND_AS_IS, HeldSends;
|
|
38060
|
+
var MAX_INPLACE_REATTACHES, PROMPT_ECHO_WAIT_MS, WORD_ALIVE_REATTACH_NOTE, CONTROL_RESET_RESUMED_NOTE, RESEND_AS_IS, PASSES_HOLD, HeldSends;
|
|
37720
38061
|
var init_reattach = __esm({
|
|
37721
38062
|
"../../src/core/harness/reattach.ts"() {
|
|
37722
38063
|
"use strict";
|
|
@@ -37727,6 +38068,7 @@ var init_reattach = __esm({
|
|
|
37727
38068
|
WORD_ALIVE_REATTACH_NOTE = "the executor said replaced; the row's pi answers alive in this container; re-attached";
|
|
37728
38069
|
CONTROL_RESET_RESUMED_NOTE = "the resident's control plane reset under the run (a deploy); the container and pi are unchanged; re-attached";
|
|
37729
38070
|
RESEND_AS_IS = /* @__PURE__ */ new Set(["set_auto_retry", "get_state", "extension_ui_response"]);
|
|
38071
|
+
PASSES_HOLD = /* @__PURE__ */ new Set(["extension_ui_response", "abort"]);
|
|
37730
38072
|
HeldSends = class {
|
|
37731
38073
|
constructor(deliver) {
|
|
37732
38074
|
this.deliver = deliver;
|
|
@@ -37747,13 +38089,14 @@ var init_reattach = __esm({
|
|
|
37747
38089
|
return this.queue.length > 0;
|
|
37748
38090
|
}
|
|
37749
38091
|
/** Send now — or, while a prompt in doubt holds the gate, hold behind it in
|
|
37750
|
-
* order. A gate reply
|
|
37751
|
-
* type, so no caller can get it wrong.
|
|
37752
|
-
*
|
|
37753
|
-
* never at the hand-off to a chain
|
|
38092
|
+
* order. A gate reply and an abort never hold (`PASSES_HOLD`): the rule
|
|
38093
|
+
* lives here, by the command's type, so no caller can get it wrong.
|
|
38094
|
+
* `onLanded` runs when the write has settled at the transport — told
|
|
38095
|
+
* `landed`, or `failed` with the reset — never at the hand-off to a chain
|
|
38096
|
+
* that only holds it, and never for a write nothing kept. */
|
|
37754
38097
|
send(command2, onLanded) {
|
|
37755
38098
|
if (onLanded) this.onLanded.set(command2, onLanded);
|
|
37756
|
-
if (this.holding && command2.type
|
|
38099
|
+
if (this.holding && !PASSES_HOLD.has(String(command2.type))) {
|
|
37757
38100
|
this.queue.push({ command: command2, state: "send" });
|
|
37758
38101
|
return;
|
|
37759
38102
|
}
|
|
@@ -37814,7 +38157,11 @@ var init_reattach = __esm({
|
|
|
37814
38157
|
}
|
|
37815
38158
|
/** Hand a write to the transport; its `onLanded`, if any, runs once the write
|
|
37816
38159
|
* settled there. A `held` landing leaves the callback in place: the fresh
|
|
37817
|
-
* transport's re-send of the same object (through `send`) runs it.
|
|
38160
|
+
* transport's re-send of the same object (through `send`) runs it. Every
|
|
38161
|
+
* other landing is the write's end and the callback is told which —
|
|
38162
|
+
* `dropped` included: a closed transport kept nothing, no re-send will come,
|
|
38163
|
+
* and a clock or a label waiting on the write must give up now rather than
|
|
38164
|
+
* wait for ever. */
|
|
37818
38165
|
dispatch(command2) {
|
|
37819
38166
|
void this.deliver(command2).then((landing) => {
|
|
37820
38167
|
if (landing === "held") return;
|
|
@@ -38314,7 +38661,8 @@ var init_tracingExecutor = __esm({
|
|
|
38314
38661
|
const innerRelease = inner.release?.bind(inner);
|
|
38315
38662
|
if (innerRelease) this.release = (mode) => this.timed("exec.release", (s) => innerRelease(mode, { span: s }));
|
|
38316
38663
|
const innerMoveTo = inner.moveTo?.bind(inner);
|
|
38317
|
-
if (innerMoveTo)
|
|
38664
|
+
if (innerMoveTo)
|
|
38665
|
+
this.moveTo = (sha, opts) => this.timed("exec.move_to", (s) => innerMoveTo(sha, { ...opts, span: s }));
|
|
38318
38666
|
const innerReadBytes = inner.readBytes?.bind(inner);
|
|
38319
38667
|
if (innerReadBytes)
|
|
38320
38668
|
this.readBytes = (path) => this.timed("exec.read_bytes", (s) => innerReadBytes(path, { span: s }));
|
|
@@ -38906,6 +39254,8 @@ async function runPiHarnessOpen(deps, run2) {
|
|
|
38906
39254
|
let windingDown = "run";
|
|
38907
39255
|
let hardStopped = false;
|
|
38908
39256
|
let bypass;
|
|
39257
|
+
let abortSent = false;
|
|
39258
|
+
let settled = false;
|
|
38909
39259
|
let replaced;
|
|
38910
39260
|
const toolsBlocked = () => {
|
|
38911
39261
|
if (!writeUp) return void 0;
|
|
@@ -38990,7 +39340,8 @@ async function runPiHarnessOpen(deps, run2) {
|
|
|
38990
39340
|
sessionEnded = true;
|
|
38991
39341
|
transport?.close();
|
|
38992
39342
|
bridge.closeOpenSpans(
|
|
38993
|
-
hardStopped ? "the run was hard-stopped" : bypass ? "the run was stopped: a tool call bypassed the gate" : "the run ended"
|
|
39343
|
+
hardStopped ? "the run was hard-stopped" : bypass ? "the run was stopped: a tool call bypassed the gate" : "the run ended",
|
|
39344
|
+
{ cut: abortSent || !settled }
|
|
38994
39345
|
);
|
|
38995
39346
|
save();
|
|
38996
39347
|
if (replaced !== void 0) return;
|
|
@@ -39168,14 +39519,21 @@ async function runPiHarnessOpen(deps, run2) {
|
|
|
39168
39519
|
else if (run2.resume) sends.send({ id: ids.prompt, type: "prompt", message: CONTINUE_PROMPT });
|
|
39169
39520
|
else sends.send({ id: ids.prompt, type: "prompt", ...promptOf(run2.messages) });
|
|
39170
39521
|
let warned = false;
|
|
39171
|
-
let settled = false;
|
|
39172
39522
|
let stopMode;
|
|
39173
39523
|
let finaleAborted = false;
|
|
39174
39524
|
let writeUpFailed;
|
|
39525
|
+
const abortPi = () => {
|
|
39526
|
+
abortSent = true;
|
|
39527
|
+
bridge.markOpenCallsCut();
|
|
39528
|
+
sends.send({ type: "abort" }, (landing) => {
|
|
39529
|
+
if (landing === "dropped")
|
|
39530
|
+
note("harness_error", "the abort was dropped: no transport kept it, so pi was never told to stop");
|
|
39531
|
+
});
|
|
39532
|
+
};
|
|
39175
39533
|
const hardStop = () => {
|
|
39176
39534
|
if (hardStopped) return;
|
|
39177
39535
|
hardStopped = true;
|
|
39178
|
-
|
|
39536
|
+
abortPi();
|
|
39179
39537
|
};
|
|
39180
39538
|
const startWriteUp = (kind, instruction) => {
|
|
39181
39539
|
writeUp = kind;
|
|
@@ -39185,20 +39543,26 @@ async function runPiHarnessOpen(deps, run2) {
|
|
|
39185
39543
|
const steer = { type: "steer", message: instruction };
|
|
39186
39544
|
writeUpSteer = steer;
|
|
39187
39545
|
sends.send(steer, (landing) => {
|
|
39546
|
+
if (writeUpSteer !== steer) return;
|
|
39188
39547
|
if (landing === "landed") {
|
|
39189
39548
|
writeUpAt = now();
|
|
39190
39549
|
return;
|
|
39191
39550
|
}
|
|
39192
|
-
|
|
39193
|
-
|
|
39551
|
+
if (landing === "failed") {
|
|
39552
|
+
note("wrap_up", wrapUpWriteFailedNote(kind.kind, windingDown));
|
|
39553
|
+
ask();
|
|
39554
|
+
return;
|
|
39555
|
+
}
|
|
39194
39556
|
});
|
|
39195
39557
|
};
|
|
39196
39558
|
ask();
|
|
39197
39559
|
};
|
|
39198
39560
|
const dropHeld = (closes) => {
|
|
39199
39561
|
const dropped = sends.dropHeld();
|
|
39200
|
-
|
|
39201
|
-
|
|
39562
|
+
const undelivered = writeUp !== void 0 && writeUpSteer !== void 0 && dropped.includes(writeUpSteer) ? writeUp : void 0;
|
|
39563
|
+
writeUpSteer = void 0;
|
|
39564
|
+
if (undelivered === void 0) return;
|
|
39565
|
+
note("wrap_up", wrapUpUndeliveredNote(undelivered.kind, closes));
|
|
39202
39566
|
clearWriteUp();
|
|
39203
39567
|
};
|
|
39204
39568
|
let steers = Promise.resolve();
|
|
@@ -39232,7 +39596,7 @@ async function runPiHarnessOpen(deps, run2) {
|
|
|
39232
39596
|
note("follow_up", `staging the follow-up's files failed: ${redactSecrets(stagedLine)}`);
|
|
39233
39597
|
}
|
|
39234
39598
|
}
|
|
39235
|
-
if (
|
|
39599
|
+
if (loopEnded) {
|
|
39236
39600
|
run2.inbox?.requeue(inputs);
|
|
39237
39601
|
return;
|
|
39238
39602
|
}
|
|
@@ -39269,7 +39633,7 @@ ${stagedLine}` : prompt;
|
|
|
39269
39633
|
writeUpAt = void 0;
|
|
39270
39634
|
finaleAborted = true;
|
|
39271
39635
|
run2.onProgress?.(finaleTimedOutNote());
|
|
39272
|
-
|
|
39636
|
+
abortPi();
|
|
39273
39637
|
}
|
|
39274
39638
|
return;
|
|
39275
39639
|
}
|
|
@@ -39280,8 +39644,13 @@ ${stagedLine}` : prompt;
|
|
|
39280
39644
|
return;
|
|
39281
39645
|
}
|
|
39282
39646
|
if (now() >= loopEnd) {
|
|
39283
|
-
|
|
39647
|
+
const doing = bridge.doingNow();
|
|
39648
|
+
note("time_budget_exhausted", timeBudgetNote(doing));
|
|
39284
39649
|
startWriteUp({ kind: "time" }, timeBudgetInstruction());
|
|
39650
|
+
if (doing !== void 0 && doing.startsWith("running ")) {
|
|
39651
|
+
note("tool_cut", toolCutNote(doing));
|
|
39652
|
+
abortPi();
|
|
39653
|
+
}
|
|
39285
39654
|
return;
|
|
39286
39655
|
}
|
|
39287
39656
|
if (bridge.turns >= run2.agent.maxTurns) {
|
|
@@ -39350,7 +39719,7 @@ ${stagedLine}` : prompt;
|
|
|
39350
39719
|
if (now() >= until) {
|
|
39351
39720
|
const msg = "the write in flight when the resident's control plane reset did not settle before the deadline; the run cannot continue";
|
|
39352
39721
|
note("harness_error", msg);
|
|
39353
|
-
|
|
39722
|
+
abortPi();
|
|
39354
39723
|
throw new Error(msg, { cause: err2 });
|
|
39355
39724
|
}
|
|
39356
39725
|
}
|
|
@@ -39426,7 +39795,7 @@ ${stagedLine}` : prompt;
|
|
|
39426
39795
|
if (obs.gateBypassed) {
|
|
39427
39796
|
bypass = new GateBypassed(obs.gateBypassed.tool, obs.gateBypassed.callId);
|
|
39428
39797
|
note("harness_error", `${bypass.message} \u2014 the run is stopped`);
|
|
39429
|
-
|
|
39798
|
+
abortPi();
|
|
39430
39799
|
break;
|
|
39431
39800
|
}
|
|
39432
39801
|
if (obs.response) {
|
|
@@ -39598,7 +39967,7 @@ ${stagedLine}` : prompt;
|
|
|
39598
39967
|
writeUpAt = void 0;
|
|
39599
39968
|
finaleAborted = true;
|
|
39600
39969
|
run2.onProgress?.(finaleTimedOutNote("turn"));
|
|
39601
|
-
|
|
39970
|
+
abortPi();
|
|
39602
39971
|
}
|
|
39603
39972
|
return;
|
|
39604
39973
|
}
|
|
@@ -39666,7 +40035,7 @@ ${stagedLine}` : prompt;
|
|
|
39666
40035
|
if (obs.gateBypassed) {
|
|
39667
40036
|
bypass = new GateBypassed(obs.gateBypassed.tool, obs.gateBypassed.callId);
|
|
39668
40037
|
note("harness_error", `${bypass.message} \u2014 the turn is stopped`);
|
|
39669
|
-
|
|
40038
|
+
abortPi();
|
|
39670
40039
|
break;
|
|
39671
40040
|
}
|
|
39672
40041
|
if (obs.response) noteEcho(obs.response);
|
|
@@ -39784,7 +40153,7 @@ var init_harness = __esm({
|
|
|
39784
40153
|
this.name = "PromptRefused";
|
|
39785
40154
|
}
|
|
39786
40155
|
};
|
|
39787
|
-
GateBypassed = class extends
|
|
40156
|
+
GateBypassed = class extends HarnessGateBypassedError {
|
|
39788
40157
|
constructor(tool, callId) {
|
|
39789
40158
|
super(`the gate was bypassed: pi ran ${tool} (call ${callId}) without asking the bot`);
|
|
39790
40159
|
this.name = "GateBypassed";
|
|
@@ -40824,7 +41193,9 @@ async function runLoop(deps, ctx) {
|
|
|
40824
41193
|
});
|
|
40825
41194
|
}
|
|
40826
41195
|
let answer;
|
|
40827
|
-
let verdict = typeof restored.verdict === "object" && restored.verdict !== null ? parseVerdictInput(restored.verdict
|
|
41196
|
+
let verdict = typeof restored.verdict === "object" && restored.verdict !== null ? parseVerdictInput(restored.verdict, {
|
|
41197
|
+
addressSeverity: ctx.addressSeverity.level
|
|
41198
|
+
}) ?? void 0 : void 0;
|
|
40828
41199
|
const onVerdict = (v) => {
|
|
40829
41200
|
verdict = v;
|
|
40830
41201
|
ledgerRun?.setState({ verdict: v });
|
|
@@ -40863,14 +41234,38 @@ async function runLoop(deps, ctx) {
|
|
|
40863
41234
|
let reviewPost;
|
|
40864
41235
|
let carried;
|
|
40865
41236
|
let runFailed = false;
|
|
41237
|
+
let gateBypassed = false;
|
|
41238
|
+
let commandInFlight = false;
|
|
41239
|
+
const recordEvents = () => registry.snapshot(run2.id, run2.token)?.events ?? [];
|
|
40866
41240
|
let failure2;
|
|
40867
41241
|
let interrupted;
|
|
41242
|
+
const statusNow = () => {
|
|
41243
|
+
const stopped = run2.control.requested;
|
|
41244
|
+
return interrupted ? "interrupted" : runFailed ? "failed" : stopped === "hard" ? "stopped_hard" : stopped === "soft" ? "stopped_soft" : "completed";
|
|
41245
|
+
};
|
|
41246
|
+
const readCallsInFlight = () => {
|
|
41247
|
+
const calls = callsInFlight(recordEvents(), statusNow());
|
|
41248
|
+
commandInFlight = calls.length > 0;
|
|
41249
|
+
if (commandInFlight)
|
|
41250
|
+
registry.publish(run2.id, {
|
|
41251
|
+
type: "run_note",
|
|
41252
|
+
kind: "workspace_torn_down",
|
|
41253
|
+
summary: redactAndCap(
|
|
41254
|
+
`a command may still be running in the workspace, so it is torn down rather than paired: ${calls.map((c) => c.summary).join("; ")}`,
|
|
41255
|
+
ENDING_NOTE_MAX
|
|
41256
|
+
),
|
|
41257
|
+
at: clock4()
|
|
41258
|
+
});
|
|
41259
|
+
};
|
|
40868
41260
|
let runDiagnosis;
|
|
40869
41261
|
let harnessSession;
|
|
40870
41262
|
const releaseWorkspace = (span) => {
|
|
40871
|
-
const
|
|
41263
|
+
const events = recordEvents();
|
|
41264
|
+
const pushed = pushedBranchesOf(events);
|
|
40872
41265
|
return round2.release({
|
|
40873
41266
|
hardStopped: run2.control.requested === "hard",
|
|
41267
|
+
commandInFlight,
|
|
41268
|
+
gateBypassed,
|
|
40874
41269
|
...span ? { span } : {},
|
|
40875
41270
|
...pushed.length > 0 ? { pushed } : {}
|
|
40876
41271
|
});
|
|
@@ -40916,6 +41311,7 @@ async function runLoop(deps, ctx) {
|
|
|
40916
41311
|
...wait ? { wait } : {},
|
|
40917
41312
|
...session ? { session } : {},
|
|
40918
41313
|
onVerdict,
|
|
41314
|
+
addressSeverity: ctx.addressSeverity.level,
|
|
40919
41315
|
onDigest,
|
|
40920
41316
|
onPrDescription,
|
|
40921
41317
|
onHandoff,
|
|
@@ -41088,6 +41484,7 @@ async function runLoop(deps, ctx) {
|
|
|
41088
41484
|
replaced: err2,
|
|
41089
41485
|
facts: lastFacts,
|
|
41090
41486
|
binding: workspaceBinding,
|
|
41487
|
+
stopSignal: run2.control.hardSignal,
|
|
41091
41488
|
saveFacts
|
|
41092
41489
|
});
|
|
41093
41490
|
} catch (failed) {
|
|
@@ -41099,6 +41496,15 @@ async function runLoop(deps, ctx) {
|
|
|
41099
41496
|
onEvent({ type: "run_note", kind: "sandbox_restarted", summary: decision.interruption.message });
|
|
41100
41497
|
throw decision.interruption;
|
|
41101
41498
|
}
|
|
41499
|
+
if (decision.kind === "stopped") {
|
|
41500
|
+
harnessDeps.registry.forget(run2.id);
|
|
41501
|
+
onEvent({
|
|
41502
|
+
type: "run_note",
|
|
41503
|
+
kind: "sandbox_restarted",
|
|
41504
|
+
summary: "the container was replaced under the run, and the run was stopped while its workspace was being re-attached in the replacement"
|
|
41505
|
+
});
|
|
41506
|
+
break;
|
|
41507
|
+
}
|
|
41102
41508
|
if (decision.round !== void 0) {
|
|
41103
41509
|
executor = decision.round.selection.executor;
|
|
41104
41510
|
toolContext.executor = executor;
|
|
@@ -41112,7 +41518,7 @@ async function runLoop(deps, ctx) {
|
|
|
41112
41518
|
harnessResume = decision.resume;
|
|
41113
41519
|
}
|
|
41114
41520
|
}
|
|
41115
|
-
answer = harnessSession
|
|
41521
|
+
answer = harnessSession?.answer ?? HARD_STOP_MESSAGE;
|
|
41116
41522
|
}
|
|
41117
41523
|
if (isPrReview && repoCtx.repo && repoCtx.pr !== void 0 && run2.control.requested !== "hard" && postedBefore === void 0) {
|
|
41118
41524
|
const settled = await settleReviewedHead({
|
|
@@ -41255,6 +41661,7 @@ async function runLoop(deps, ctx) {
|
|
|
41255
41661
|
}
|
|
41256
41662
|
}
|
|
41257
41663
|
await harnessSession?.end();
|
|
41664
|
+
readCallsInFlight();
|
|
41258
41665
|
const leftBehind = isCodingPrRun && run2.control.requested !== "hard" ? workLeftBehindOf({ uncommittedChanges: observedUncommitted, unpushedCommits: observedUnpushed }) : void 0;
|
|
41259
41666
|
if (leftBehind) {
|
|
41260
41667
|
registry.publish(run2.id, {
|
|
@@ -41328,14 +41735,16 @@ async function runLoop(deps, ctx) {
|
|
|
41328
41735
|
if (err2 instanceof HarnessInterruptedError) interrupted = err2;
|
|
41329
41736
|
else {
|
|
41330
41737
|
runFailed = true;
|
|
41738
|
+
gateBypassed = err2 instanceof HarnessGateBypassedError;
|
|
41331
41739
|
if (err2 instanceof ModelPolicyRefusedError) failure2 = { kind: "policy_refusal" };
|
|
41332
41740
|
registry.publish(run2.id, {
|
|
41333
41741
|
type: "run_note",
|
|
41334
41742
|
kind: "run_failed",
|
|
41335
|
-
summary: redactAndCap(err2 instanceof Error ? err2.message : String(err2),
|
|
41743
|
+
summary: redactAndCap(err2 instanceof Error ? err2.message : String(err2), ENDING_NOTE_MAX)
|
|
41336
41744
|
});
|
|
41337
41745
|
}
|
|
41338
41746
|
await harnessSession?.end();
|
|
41747
|
+
readCallsInFlight();
|
|
41339
41748
|
await root.span("post.workspace_release", (span) => releaseWorkspace(span));
|
|
41340
41749
|
if (!interrupted) throw err2;
|
|
41341
41750
|
return {
|
|
@@ -41347,8 +41756,7 @@ async function runLoop(deps, ctx) {
|
|
|
41347
41756
|
};
|
|
41348
41757
|
} finally {
|
|
41349
41758
|
clearInterval(heartbeat);
|
|
41350
|
-
const
|
|
41351
|
-
const status3 = interrupted ? "interrupted" : runFailed ? "failed" : stopped === "hard" ? "stopped_hard" : stopped === "soft" ? "stopped_soft" : "completed";
|
|
41759
|
+
const status3 = statusNow();
|
|
41352
41760
|
registry.finish(run2.id, status3);
|
|
41353
41761
|
const snap = registry.snapshot(run2.id, run2.token);
|
|
41354
41762
|
const events = snap?.events ?? [];
|
|
@@ -41447,7 +41855,7 @@ function unknownHarnessWordOf(state) {
|
|
|
41447
41855
|
container: typeof container === "string" ? container : "unknown"
|
|
41448
41856
|
};
|
|
41449
41857
|
}
|
|
41450
|
-
var
|
|
41858
|
+
var ENDING_NOTE_MAX;
|
|
41451
41859
|
var init_runLoop = __esm({
|
|
41452
41860
|
"../../src/core/dispatch/runLoop.ts"() {
|
|
41453
41861
|
"use strict";
|
|
@@ -41490,7 +41898,7 @@ var init_runLoop = __esm({
|
|
|
41490
41898
|
init_reply();
|
|
41491
41899
|
init_staging();
|
|
41492
41900
|
init_run2();
|
|
41493
|
-
|
|
41901
|
+
ENDING_NOTE_MAX = 500;
|
|
41494
41902
|
}
|
|
41495
41903
|
});
|
|
41496
41904
|
|
|
@@ -42207,9 +42615,9 @@ async function runShipBranch(deps, msg, io, ctx) {
|
|
|
42207
42615
|
}
|
|
42208
42616
|
const scopes = deps.config.scopes(msg.channelId, msg.userId);
|
|
42209
42617
|
const addressSeverity = resolveAddressSeverity({
|
|
42210
|
-
org: deps.config.config.
|
|
42211
|
-
channel: scopes.channel.
|
|
42212
|
-
user: scopes.user.
|
|
42618
|
+
org: deps.config.config.review?.addressSeverity,
|
|
42619
|
+
channel: scopes.channel.review?.addressSeverity,
|
|
42620
|
+
user: scopes.user.review?.addressSeverity,
|
|
42213
42621
|
run: directives.severity
|
|
42214
42622
|
});
|
|
42215
42623
|
const grant2 = resolveGrant2({
|
|
@@ -42341,14 +42749,14 @@ var init_codingChild = __esm({
|
|
|
42341
42749
|
|
|
42342
42750
|
// ../../src/core/dispatch/settle.ts
|
|
42343
42751
|
function settleThread(deps, ctx) {
|
|
42344
|
-
const { msg, admitted,
|
|
42752
|
+
const { msg, admitted, stopCounts, control } = ctx;
|
|
42345
42753
|
const admission = deps.admission ?? defaultAdmission;
|
|
42346
42754
|
const released = admitted ? admission.release(msg.threadKey, admitted) : [];
|
|
42347
42755
|
const pending2 = released.filter(fromPerson);
|
|
42348
42756
|
const fromRuns = released.length - pending2.length;
|
|
42349
42757
|
if (fromRuns > 0)
|
|
42350
42758
|
console.log(`[dispatch] ${msg.threadKey} ${fromRuns} steer(s) from a parent run never read \u2014 not run fresh`);
|
|
42351
|
-
const stopMode =
|
|
42759
|
+
const stopMode = stopCounts ? control?.requested : void 0;
|
|
42352
42760
|
if (pending2.length > 0 && stopMode) {
|
|
42353
42761
|
console.log(`[dispatch] ${msg.threadKey} ${pending2.length} follow-up(s) dropped: run stopped (${stopMode})`);
|
|
42354
42762
|
return { kind: "dropped", stopMode, pending: pending2 };
|
|
@@ -43007,6 +43415,7 @@ async function dispatch(deps, msg, io, opts = {}) {
|
|
|
43007
43415
|
let reserved;
|
|
43008
43416
|
let requestRow;
|
|
43009
43417
|
let fencedWhileAttaching = false;
|
|
43418
|
+
let stoppedWhileAttaching;
|
|
43010
43419
|
let resumeRowClosed = false;
|
|
43011
43420
|
let restartRequest;
|
|
43012
43421
|
const reservationHooks = {
|
|
@@ -43321,9 +43730,30 @@ async function dispatch(deps, msg, io, opts = {}) {
|
|
|
43321
43730
|
profile,
|
|
43322
43731
|
repoCtx,
|
|
43323
43732
|
root,
|
|
43324
|
-
...reattach !== void 0 ? { reattach } : {}
|
|
43733
|
+
...reattach !== void 0 ? { reattach } : {},
|
|
43734
|
+
// The run's control exists from the registry row above: a stop relayed
|
|
43735
|
+
// during the attach ends its wake wait at once (execution.md item 9).
|
|
43736
|
+
...registered ? { stopSignal: registered.control.hardSignal } : {}
|
|
43325
43737
|
});
|
|
43326
43738
|
if (attach.kind === "refused") return ended;
|
|
43739
|
+
if (attach.kind === "stopped") {
|
|
43740
|
+
if (!fencedWhileAttaching) {
|
|
43741
|
+
stoppedWhileAttaching = registered?.control.requested ?? "hard";
|
|
43742
|
+
await root.span(
|
|
43743
|
+
"dispatch.stop",
|
|
43744
|
+
() => card.done(
|
|
43745
|
+
shell.close({
|
|
43746
|
+
kind: "not_started",
|
|
43747
|
+
icon: "\u26D4",
|
|
43748
|
+
reason: "stopped before the run started",
|
|
43749
|
+
...closeLines(clock4(), false)
|
|
43750
|
+
})
|
|
43751
|
+
),
|
|
43752
|
+
{ attrs: { outcome: "stopped_while_attaching" } }
|
|
43753
|
+
);
|
|
43754
|
+
}
|
|
43755
|
+
return ended;
|
|
43756
|
+
}
|
|
43327
43757
|
if (attach.kind === "reattach_refused") {
|
|
43328
43758
|
if (resume) {
|
|
43329
43759
|
const request2 = await abandonLostWorkspace({
|
|
@@ -43520,9 +43950,17 @@ async function dispatch(deps, msg, io, opts = {}) {
|
|
|
43520
43950
|
inbox: admitted.inbox
|
|
43521
43951
|
}
|
|
43522
43952
|
);
|
|
43953
|
+
const scopes = deps.config.scopes(msg.channelId, msg.userId);
|
|
43954
|
+
const addressSeverity = resolveAddressSeverity({
|
|
43955
|
+
org: deps.config.config.review?.addressSeverity,
|
|
43956
|
+
channel: scopes.channel.review?.addressSeverity,
|
|
43957
|
+
user: scopes.user.review?.addressSeverity,
|
|
43958
|
+
run: directives.severity
|
|
43959
|
+
});
|
|
43523
43960
|
const ran = await runLoop(deps, {
|
|
43524
43961
|
msg,
|
|
43525
43962
|
io,
|
|
43963
|
+
addressSeverity,
|
|
43526
43964
|
agent,
|
|
43527
43965
|
profile,
|
|
43528
43966
|
resolved,
|
|
@@ -43644,17 +44082,21 @@ async function dispatch(deps, msg, io, opts = {}) {
|
|
|
43644
44082
|
if (registered) deps.runBearers?.revoke(registered.id);
|
|
43645
44083
|
if (resume && ledgerRun && !runLoopStarted && !resumeRowClosed) {
|
|
43646
44084
|
const adopted = ledgerRun;
|
|
43647
|
-
|
|
43648
|
-
|
|
43649
|
-
|
|
43650
|
-
);
|
|
43651
|
-
console.log(
|
|
43652
|
-
`[resume] ${msg.threadKey} run ${resume.row.runId} closed interrupted: the resumed dispatch ended before the run started`
|
|
43653
|
-
);
|
|
44085
|
+
const why = stoppedWhileAttaching !== void 0 ? "the resumed run was stopped before it started" : "the resumed dispatch ended before the run started";
|
|
44086
|
+
const status3 = stoppedWhileAttaching === "hard" ? "stopped_hard" : stoppedWhileAttaching === "soft" ? "stopped_soft" : "interrupted";
|
|
44087
|
+
await root.span("post.history_write", () => closeResumedRow(adopted, resume, why, status3));
|
|
44088
|
+
console.log(`[resume] ${msg.threadKey} run ${resume.row.runId} closed ${status3}: ${why}`);
|
|
43654
44089
|
}
|
|
43655
|
-
const settled = settleThread(deps, {
|
|
44090
|
+
const settled = settleThread(deps, {
|
|
44091
|
+
msg,
|
|
44092
|
+
admitted,
|
|
44093
|
+
// A stop that ended the attach counts as the loop's stop would: the
|
|
44094
|
+
// request ends `stopped`, and a follow-up queued during the wait is told.
|
|
44095
|
+
stopCounts: runLoopStarted || stoppedWhileAttaching !== void 0,
|
|
44096
|
+
control: registered?.control
|
|
44097
|
+
});
|
|
43656
44098
|
if (settled.kind === "dropped") await tellDropped(root, settled.pending);
|
|
43657
|
-
const stopMode = settled.kind === "handed-on" ? void 0 : settled.stopMode;
|
|
44099
|
+
const stopMode = (settled.kind === "handed-on" ? void 0 : settled.stopMode) ?? stoppedWhileAttaching;
|
|
43658
44100
|
ended.status = caught ? "failed" : refused7 ? "refused" : stopMode ? "stopped" : "completed";
|
|
43659
44101
|
root.end(caught ? "error" : "ok", { status: ended.status });
|
|
43660
44102
|
if (restartRequest) {
|
|
@@ -43787,6 +44229,7 @@ var init_dispatcher = __esm({
|
|
|
43787
44229
|
init_record();
|
|
43788
44230
|
init_ship();
|
|
43789
44231
|
init_shipPipeline();
|
|
44232
|
+
init_reviewVerdict();
|
|
43790
44233
|
init_contract();
|
|
43791
44234
|
init_codingChild();
|
|
43792
44235
|
init_settle();
|
|
@@ -45565,6 +46008,38 @@ let pending = Promise.resolve();
|
|
|
45565
46008
|
let backoffMs = MIN_BACKOFF_MS;
|
|
45566
46009
|
let connections = 0;
|
|
45567
46010
|
|
|
46011
|
+
// A write the feed refuses is not thrown from the write: node reports it on the
|
|
46012
|
+
// stream's error event \u2014 for the file the seam appends the feed to (EBADF: a
|
|
46013
|
+
// descriptor opened without write) as for a pipe whose reader is gone (EPIPE)
|
|
46014
|
+
// \u2014 and an error event nobody listens for is an uncaught exception that kills
|
|
46015
|
+
// the tailer with no note anywhere. The listener is the guard: the refused
|
|
46016
|
+
// record is lost to the feed and said on stderr. A feed gone for good \u2014 its
|
|
46017
|
+
// descriptor bad (EBADF), its reader gone (EPIPE): every later write fails the
|
|
46018
|
+
// same way and the stream is not destroyed \u2014 ends the tailer non-zero once the
|
|
46019
|
+
// note is out, so its death is visible to the seam and a re-attach restarts it
|
|
46020
|
+
// (a mute tailer alive at its pid would be waited on to the run's silence
|
|
46021
|
+
// bound): the exit code is set first, the exit follows the note's write or a
|
|
46022
|
+
// short unref'd timer, whichever comes first, so a stderr that never drains
|
|
46023
|
+
// cannot keep the mute tailer alive either. Any other refusal \u2014 a full disk
|
|
46024
|
+
// (ENOSPC), which space can cure \u2014 is noted and the tailer goes on. A write on
|
|
46025
|
+
// stderr that fails has nothing left to say it to.
|
|
46026
|
+
const FEED_GONE = new Set(["EBADF", "EPIPE"]);
|
|
46027
|
+
const FEED_GONE_EXIT_MS = 1000;
|
|
46028
|
+
process.stdout.on("error", (err) => {
|
|
46029
|
+
const gone = FEED_GONE.has(err && err.code);
|
|
46030
|
+
if (gone) {
|
|
46031
|
+
process.exitCode = 1;
|
|
46032
|
+
setTimeout(() => process.exit(1), FEED_GONE_EXIT_MS).unref();
|
|
46033
|
+
}
|
|
46034
|
+
process.stderr.write(
|
|
46035
|
+
"tailer: a record could not be written" + (gone ? "; the feed is gone, exiting" : "") + ": " + detailOf(err) + "\\n",
|
|
46036
|
+
() => {
|
|
46037
|
+
if (gone) process.exit(1);
|
|
46038
|
+
},
|
|
46039
|
+
);
|
|
46040
|
+
});
|
|
46041
|
+
process.stderr.on("error", () => {});
|
|
46042
|
+
|
|
45568
46043
|
function emit(record) {
|
|
45569
46044
|
process.stdout.write(JSON.stringify(record) + "\\n");
|
|
45570
46045
|
}
|
|
@@ -45601,15 +46076,21 @@ async function getJson(path) {
|
|
|
45601
46076
|
return JSON.parse(res.body);
|
|
45602
46077
|
}
|
|
45603
46078
|
|
|
45604
|
-
/** The session's pending asks, whole
|
|
46079
|
+
/** The session's pending asks, whole, emitted the moment their read answers;
|
|
46080
|
+
* its messages, the changed ones only, after the store's pages. Both reads go
|
|
46081
|
+
* out at once: an unanswered ask holds the server's turn, so it waits on
|
|
46082
|
+
* nothing \u2014 least of all a long store's pagination. An ask read ahead of the
|
|
46083
|
+
* rows that name its call is the bridge's to hold until they come. */
|
|
45605
46084
|
async function refill(sessionID, reason) {
|
|
45606
46085
|
const at = Date.now();
|
|
45607
|
-
|
|
45608
|
-
|
|
45609
|
-
|
|
45610
|
-
|
|
45611
|
-
|
|
45612
|
-
|
|
46086
|
+
// Neither read nor what is done with its answer can reject out of here: the
|
|
46087
|
+
// asks' chain catches its read's refusal and a malformed answer alike (null
|
|
46088
|
+
// is JSON with no data), the messages' try its pages and its record \u2014 so
|
|
46089
|
+
// refill never rejects and the queue below never ends (a write cannot throw:
|
|
46090
|
+
// emit).
|
|
46091
|
+
const asks = getJson("/api/session/" + sessionID + "/permission")
|
|
46092
|
+
.then((answer) => emit({ feed: "permissions", at, sessionID, reason, data: Array.isArray(answer.data) ? answer.data : [] }))
|
|
46093
|
+
.catch((err) => note("permission refill failed", { sessionID, reason, detail: detailOf(err) }));
|
|
45613
46094
|
try {
|
|
45614
46095
|
const all = [];
|
|
45615
46096
|
let cursor;
|
|
@@ -45638,10 +46119,16 @@ async function refill(sessionID, reason) {
|
|
|
45638
46119
|
if (!known.has(sessionID)) known.set(sessionID, new Map());
|
|
45639
46120
|
note("message refill failed", { sessionID, reason, detail: detailOf(err) });
|
|
45640
46121
|
}
|
|
46122
|
+
await asks;
|
|
45641
46123
|
}
|
|
45642
46124
|
|
|
46125
|
+
// One refill after another; a refill that still managed to reject is noted
|
|
46126
|
+
// and the chain goes on \u2014 a rejected chain would skip every later refill for
|
|
46127
|
+
// every session, silently, for the rest of the tailer's life.
|
|
45643
46128
|
function queueRefill(sessionID, reason) {
|
|
45644
|
-
pending = pending
|
|
46129
|
+
pending = pending
|
|
46130
|
+
.then(() => refill(sessionID, reason))
|
|
46131
|
+
.catch((err) => note("refill failed", { sessionID, reason, detail: detailOf(err) }));
|
|
45645
46132
|
}
|
|
45646
46133
|
|
|
45647
46134
|
function onEvent(event) {
|
|
@@ -46105,6 +46592,12 @@ function isBudgetRefusal(error) {
|
|
|
46105
46592
|
return TURN_BUDGET_REFUSAL.test(error.message);
|
|
46106
46593
|
}
|
|
46107
46594
|
function judgeOpenCodeAsk(action, resources, rules, relayedToolNames) {
|
|
46595
|
+
if (action === "doom_loop")
|
|
46596
|
+
return {
|
|
46597
|
+
reply: "reject",
|
|
46598
|
+
tool: "doom_loop",
|
|
46599
|
+
message: "OpenCode's repeat guard tripped: the same call has been made over and over, and the run does not go on looping \u2014 change approach, or write up what stands"
|
|
46600
|
+
};
|
|
46108
46601
|
const word = openCodeToolWord(action);
|
|
46109
46602
|
const tool = TOOL_NAME_WORD[action] ?? word;
|
|
46110
46603
|
if (relayedToolNames.has(action) || relayedToolNames.has(word)) return { reply: "once", tool: action };
|
|
@@ -46128,14 +46621,42 @@ function judgeOpenCodeAsk(action, resources, rules, relayedToolNames) {
|
|
|
46128
46621
|
if (verdict.verdict !== "allowed") return refuse3(verdict.reason);
|
|
46129
46622
|
return { reply: "once", tool };
|
|
46130
46623
|
}
|
|
46131
|
-
function
|
|
46624
|
+
function foldInputs(asks, own) {
|
|
46625
|
+
let input = own;
|
|
46626
|
+
for (const ask of asks) input = openInput(ask, input);
|
|
46627
|
+
return input;
|
|
46628
|
+
}
|
|
46629
|
+
function openInput(request2, own) {
|
|
46132
46630
|
const resources = Array.isArray(request2.resources) ? request2.resources.filter((r) => typeof r === "string") : [];
|
|
46133
|
-
if (
|
|
46631
|
+
if (request2.action === "doom_loop") return own;
|
|
46632
|
+
if (resources.length === 0) return own;
|
|
46633
|
+
if (request2.action === "external_directory") {
|
|
46634
|
+
const ownPath = typeof own?.path === "string" ? own.path : typeof own?.filePath === "string" ? own.filePath : void 0;
|
|
46635
|
+
const before = Array.isArray(own?.directories) ? own.directories.filter((d) => typeof d === "string") : [];
|
|
46636
|
+
const directories = [.../* @__PURE__ */ new Set([...before, ...resources])];
|
|
46637
|
+
return { ...own ?? {}, path: ownPath ?? directories[0], directory: directories[0], directories };
|
|
46638
|
+
}
|
|
46134
46639
|
return openCodeToolNameWord(request2.action) === "bash" ? { command: resources.join(" ") } : { path: resources[0] };
|
|
46135
46640
|
}
|
|
46641
|
+
function describeOpenCodeToolCall(tool, input) {
|
|
46642
|
+
const line3 = describePiToolCall(tool, input);
|
|
46643
|
+
const reached = Array.isArray(input?.directories) ? input.directories.filter((d) => typeof d === "string" && d !== input.path) : [];
|
|
46644
|
+
return reached.length === 0 ? line3 : `${line3} (reaching ${reached.join(", ")})`;
|
|
46645
|
+
}
|
|
46136
46646
|
function unattributableDetail(tool, callId) {
|
|
46137
46647
|
return `${tool} (call ${callId}): an ask pending at the bot's death was answered while the bot was away; the run cannot tell by whom`;
|
|
46138
46648
|
}
|
|
46649
|
+
function toolPartsOf(message4) {
|
|
46650
|
+
if (message4.type !== "assistant") return [];
|
|
46651
|
+
const content = message4.content;
|
|
46652
|
+
const parts = [];
|
|
46653
|
+
for (const part of Array.isArray(content) ? content : []) {
|
|
46654
|
+
if (!isRecord9(part) || part.type !== "tool") continue;
|
|
46655
|
+
const input = isRecord9(part.state) && isRecord9(part.state.input) ? part.state.input : void 0;
|
|
46656
|
+
parts.push({ id: str6(part.id), name: str6(part.name), ...input ? { input } : {} });
|
|
46657
|
+
}
|
|
46658
|
+
return parts;
|
|
46659
|
+
}
|
|
46139
46660
|
function ledgerTurnCount(ledger) {
|
|
46140
46661
|
let turns = 0;
|
|
46141
46662
|
for (const row2 of ledger) {
|
|
@@ -46307,6 +46828,7 @@ async function driveOpenCode(deps, run2, conn, kind = "loop") {
|
|
|
46307
46828
|
let providerError;
|
|
46308
46829
|
let writeUpFailed;
|
|
46309
46830
|
let settled = false;
|
|
46831
|
+
let interruptPosted = false;
|
|
46310
46832
|
let replacedBy;
|
|
46311
46833
|
let transportLost;
|
|
46312
46834
|
const probe = {
|
|
@@ -46352,13 +46874,24 @@ async function driveOpenCode(deps, run2, conn, kind = "loop") {
|
|
|
46352
46874
|
post("the write-up steer", sessionRoutes["session.prompt"], { text: instruction, delivery: "steer" });
|
|
46353
46875
|
};
|
|
46354
46876
|
const turnCount = () => deps.bearers?.grantOf(run2.runId)?.turns ?? bridge.turns;
|
|
46877
|
+
const interrupt = () => {
|
|
46878
|
+
interruptPosted = true;
|
|
46879
|
+
post("the interrupt", sessionRoutes["session.interrupt"]);
|
|
46880
|
+
};
|
|
46355
46881
|
const check = () => {
|
|
46356
46882
|
const requested = run2.control?.requested;
|
|
46357
46883
|
if (requested === "hard") {
|
|
46358
46884
|
if (ended === void 0) {
|
|
46359
46885
|
ended = "hard";
|
|
46360
46886
|
note("stopped", hardStopNote(), "hard");
|
|
46361
|
-
|
|
46887
|
+
interrupt();
|
|
46888
|
+
}
|
|
46889
|
+
return;
|
|
46890
|
+
}
|
|
46891
|
+
if (conn.failure.error !== void 0) {
|
|
46892
|
+
if (ended === void 0) {
|
|
46893
|
+
ended = "failed";
|
|
46894
|
+
interrupt();
|
|
46362
46895
|
}
|
|
46363
46896
|
return;
|
|
46364
46897
|
}
|
|
@@ -46374,7 +46907,7 @@ async function driveOpenCode(deps, run2, conn, kind = "loop") {
|
|
|
46374
46907
|
writeUpFailed ??= reason2;
|
|
46375
46908
|
run2.onProgress?.(finaleTimedOutNote());
|
|
46376
46909
|
note("harness_error", windDownFailureNote(reason2));
|
|
46377
|
-
|
|
46910
|
+
interrupt();
|
|
46378
46911
|
}
|
|
46379
46912
|
return;
|
|
46380
46913
|
}
|
|
@@ -46480,7 +47013,7 @@ async function driveOpenCode(deps, run2, conn, kind = "loop") {
|
|
|
46480
47013
|
if (failure2 !== void 0) {
|
|
46481
47014
|
replyFailed = new OpenCodeReplyFailedError(reply.requestID, reply.callId, reply.reply, failure2);
|
|
46482
47015
|
note("harness_error", `${replyFailed.message} \u2014 the run is stopped`);
|
|
46483
|
-
|
|
47016
|
+
interrupt();
|
|
46484
47017
|
return replyFailed;
|
|
46485
47018
|
}
|
|
46486
47019
|
}
|
|
@@ -46607,7 +47140,7 @@ async function driveOpenCode(deps, run2, conn, kind = "loop") {
|
|
|
46607
47140
|
if (await postReplies(obs) !== void 0) break;
|
|
46608
47141
|
if (obs.bypass) {
|
|
46609
47142
|
bypass = obs.bypass;
|
|
46610
|
-
|
|
47143
|
+
interrupt();
|
|
46611
47144
|
break;
|
|
46612
47145
|
}
|
|
46613
47146
|
if (bridge.observing === "catching-up") {
|
|
@@ -46636,7 +47169,7 @@ async function driveOpenCode(deps, run2, conn, kind = "loop") {
|
|
|
46636
47169
|
conn.boundaries?.left();
|
|
46637
47170
|
transport.close();
|
|
46638
47171
|
agentSpan?.end(
|
|
46639
|
-
ended === "hard" || ended === "silent" || bypass || replyFailed || replacedBy || transportLost || refused7 || providerError !== void 0 ? "error" : "ok"
|
|
47172
|
+
ended === "hard" || ended === "silent" || ended === "failed" || bypass || replyFailed || replacedBy || transportLost || refused7 || providerError !== void 0 ? "error" : "ok"
|
|
46640
47173
|
);
|
|
46641
47174
|
}
|
|
46642
47175
|
if (replacedBy !== void 0) {
|
|
@@ -46651,6 +47184,11 @@ async function driveOpenCode(deps, run2, conn, kind = "loop") {
|
|
|
46651
47184
|
note("sandbox_restarted", replaced.message);
|
|
46652
47185
|
throw replaced;
|
|
46653
47186
|
}
|
|
47187
|
+
if (interruptPosted)
|
|
47188
|
+
bridge.closeOpenSpans(
|
|
47189
|
+
(open2) => `${open2.tool} was still running when the loop left on its interrupt; its outcome never reached the record`,
|
|
47190
|
+
{ cut: true }
|
|
47191
|
+
);
|
|
46654
47192
|
if (transportLost !== void 0 && ended !== "hard") {
|
|
46655
47193
|
note(
|
|
46656
47194
|
"harness_error",
|
|
@@ -46674,18 +47212,19 @@ async function driveOpenCode(deps, run2, conn, kind = "loop") {
|
|
|
46674
47212
|
boundMs: FIRST_EVENT_BOUND_MS
|
|
46675
47213
|
});
|
|
46676
47214
|
note("harness_error", `${silent.message} \u2014 the run is stopped`);
|
|
46677
|
-
|
|
47215
|
+
interrupt();
|
|
46678
47216
|
throw silent;
|
|
46679
47217
|
}
|
|
46680
47218
|
const remaining = () => deadline - now();
|
|
46681
47219
|
const handOver = () => ({ storeIds: bridge.storeIds() });
|
|
46682
|
-
if (ended === "
|
|
47220
|
+
if (ended === "failed" && conn.failure.error !== void 0) throw conn.failure.error;
|
|
47221
|
+
if (ended === "hard") return { answer: HARD_STOP_MESSAGE, remainingMs: remaining, hardStopped: true, ...handOver() };
|
|
46683
47222
|
if (providerError !== void 0) throw new Error(`the model call failed: ${providerError}`);
|
|
46684
47223
|
if (!settled && ended !== "finale") throw new Error("the OpenCode run ended before its execution settled");
|
|
46685
47224
|
await bridge.flush();
|
|
46686
47225
|
const text = bridge.answer() ?? "";
|
|
46687
47226
|
const answer = writeUpAnswer(writeUp, text, run2.agent.maxMinutes, writeUpFailed);
|
|
46688
|
-
return { answer, remainingMs: remaining, ...handOver() };
|
|
47227
|
+
return { answer, remainingMs: remaining, hardStopped: false, ...handOver() };
|
|
46689
47228
|
}
|
|
46690
47229
|
function writeUpAnswer(writeUp, text, maxMinutes, writeUpFailed) {
|
|
46691
47230
|
if (writeUp?.kind === "time") return timeBudgetAnswer(text, maxMinutes, writeUpFailed);
|
|
@@ -46711,7 +47250,7 @@ var init_bridge3 = __esm({
|
|
|
46711
47250
|
init_dispositions();
|
|
46712
47251
|
init_session2();
|
|
46713
47252
|
init_process2();
|
|
46714
|
-
OpenCodeGateBypassedError = class extends
|
|
47253
|
+
OpenCodeGateBypassedError = class extends HarnessGateBypassedError {
|
|
46715
47254
|
constructor(detail) {
|
|
46716
47255
|
super(`the gate was bypassed: ${detail}`);
|
|
46717
47256
|
this.detail = detail;
|
|
@@ -46845,10 +47384,17 @@ var init_bridge3 = __esm({
|
|
|
46845
47384
|
* the fake sends the whole store — and the projection reads the whole map,
|
|
46846
47385
|
* so a turn lands once whichever shape a refill has (the join by message id). */
|
|
46847
47386
|
store = /* @__PURE__ */ new Map();
|
|
46848
|
-
/**
|
|
46849
|
-
*
|
|
46850
|
-
*
|
|
46851
|
-
|
|
47387
|
+
/** Every tool part the store has shown, by the part's id (the call id): the
|
|
47388
|
+
* tool's name and its own input — filled the moment a refill is read, ahead
|
|
47389
|
+
* of the mirror's chain, for what must be named at once: the call a
|
|
47390
|
+
* permission's `source` points at. */
|
|
47391
|
+
partTools = /* @__PURE__ */ new Map();
|
|
47392
|
+
/** Resource permissions answered for a call the store has not named yet, by
|
|
47393
|
+
* call id, each call's asks in the order they came — two can name one call
|
|
47394
|
+
* (`external_directory`, then `doom_loop`) and each is kept: the call is
|
|
47395
|
+
* opened once, when a refill names its part, with every held ask's input
|
|
47396
|
+
* folded in, or at its settle under the first ask's own name if none did. */
|
|
47397
|
+
unnamedAsks = /* @__PURE__ */ new Map();
|
|
46852
47398
|
/** How many store messages the projection skips: the seed and the request's
|
|
46853
47399
|
* echo on a fresh run, the import on a rebuild; none on a re-attach, where
|
|
46854
47400
|
* the ledger's rows are skipped by turn instead (`adoptStore`). */
|
|
@@ -46934,18 +47480,14 @@ var init_bridge3 = __esm({
|
|
|
46934
47480
|
const pendingCalls = new Set(pending2.map((r) => r.source?.id ?? r.id));
|
|
46935
47481
|
for (const row2 of ledger)
|
|
46936
47482
|
for (const part of row2.content) if (part.type === "tool_result") this.ledgerResults.add(part.toolUseId);
|
|
46937
|
-
for (const message4 of messages)
|
|
46938
|
-
|
|
46939
|
-
|
|
46940
|
-
for (const part of Array.isArray(content) ? content : []) {
|
|
46941
|
-
if (!isRecord9(part) || part.type !== "tool") continue;
|
|
46942
|
-
const id = str6(part.id);
|
|
46943
|
-
const name = str6(part.name);
|
|
47483
|
+
for (const message4 of messages)
|
|
47484
|
+
for (const part of toolPartsOf(message4)) {
|
|
47485
|
+
const { id, name } = part;
|
|
46944
47486
|
this.toolNames.set(id, name);
|
|
47487
|
+
this.partTools.set(id, part);
|
|
46945
47488
|
if (this.ledgerResults.has(id) || pendingCalls.has(id) || this.deps.relayedToolNames.has(name)) continue;
|
|
46946
47489
|
this.unattributableCalls.add(id);
|
|
46947
47490
|
}
|
|
46948
|
-
}
|
|
46949
47491
|
this.storeSeed = 0;
|
|
46950
47492
|
this.fedTurns = Math.min(ledgerTurnCount(ledger), projectStore(messages, 0).length);
|
|
46951
47493
|
this.mirrorChain = this.mirrorChain.then(() => this.syncMirror(messages));
|
|
@@ -46966,12 +47508,17 @@ var init_bridge3 = __esm({
|
|
|
46966
47508
|
storeIds() {
|
|
46967
47509
|
return [...this.store.keys()];
|
|
46968
47510
|
}
|
|
46969
|
-
/** Settle every call still open when the
|
|
46970
|
-
*
|
|
46971
|
-
*
|
|
46972
|
-
*
|
|
46973
|
-
*
|
|
46974
|
-
|
|
47511
|
+
/** Settle every call still open when the loop is done with the session: each
|
|
47512
|
+
* open span ends `error` and its call is put on the record as a failed
|
|
47513
|
+
* `tool_result` carrying `reason`, so no span outlives the run and the record
|
|
47514
|
+
* holds a result for every call, exactly as pi's `closeOpenSpans` does. On
|
|
47515
|
+
* the container-replaced verdict (the restart note, said of the container)
|
|
47516
|
+
* the results are unmarked: the old container's calls are gone and the
|
|
47517
|
+
* relaunch takes the relayed ones over, nothing the workspace's release must
|
|
47518
|
+
* hold for. At every other exit the loop left the calls interrupted or
|
|
47519
|
+
* unread, and `cut` marks each result as a call ended and not settled, whose
|
|
47520
|
+
* command may still be running (harness.md item 13). */
|
|
47521
|
+
closeOpenSpans(reason2, opts = {}) {
|
|
46975
47522
|
for (const [callId, open2] of this.openTools) {
|
|
46976
47523
|
open2.span?.end("error", { callId, ok: false });
|
|
46977
47524
|
this.emit({
|
|
@@ -46979,7 +47526,8 @@ var init_bridge3 = __esm({
|
|
|
46979
47526
|
tool: open2.tool,
|
|
46980
47527
|
ok: false,
|
|
46981
47528
|
callId,
|
|
46982
|
-
summary: redactAndCap(reason2({ callId, tool: open2.tool }), COMMAND_CAP)
|
|
47529
|
+
summary: redactAndCap(reason2({ callId, tool: open2.tool }), COMMAND_CAP),
|
|
47530
|
+
...opts.cut ? { cut: true } : {}
|
|
46983
47531
|
});
|
|
46984
47532
|
}
|
|
46985
47533
|
this.openTools.clear();
|
|
@@ -47141,6 +47689,7 @@ ${data.text}` : data.text;
|
|
|
47141
47689
|
}
|
|
47142
47690
|
onToolCalled(data) {
|
|
47143
47691
|
const callId = str6(data.id);
|
|
47692
|
+
if (this.openTools.has(callId)) return;
|
|
47144
47693
|
this.openCall(callId, this.toolNames.get(callId) ?? "tool", isRecord9(data.input) ? data.input : void 0);
|
|
47145
47694
|
}
|
|
47146
47695
|
/** The call opened on the record — its span, its `tool_call` with the
|
|
@@ -47161,7 +47710,7 @@ ${data.text}` : data.text;
|
|
|
47161
47710
|
this.emit({
|
|
47162
47711
|
type: "tool_call",
|
|
47163
47712
|
tool,
|
|
47164
|
-
summary: redactAndCap(
|
|
47713
|
+
summary: redactAndCap(describeOpenCodeToolCall(tool, input)),
|
|
47165
47714
|
callId,
|
|
47166
47715
|
...command2,
|
|
47167
47716
|
...span ? { spanId: span.id } : {}
|
|
@@ -47179,6 +47728,16 @@ ${data.text}` : data.text;
|
|
|
47179
47728
|
);
|
|
47180
47729
|
return;
|
|
47181
47730
|
}
|
|
47731
|
+
const held2 = this.takeHeldAsks(callId);
|
|
47732
|
+
const first = held2[0];
|
|
47733
|
+
if (first !== void 0 && !this.openTools.has(callId) && !this.toolNames.has(callId)) {
|
|
47734
|
+
this.note(
|
|
47735
|
+
"tool_unnamed",
|
|
47736
|
+
`OpenCode asked ${redactAndCap(held2.map((h) => h.action).join(", "), 60)} for call ${callId} of step ${redactAndCap(first.source?.messageID ?? "", 80)}, and the store showed no part naming the call's tool before it settled; the record opens the call under the permission's name`
|
|
47737
|
+
);
|
|
47738
|
+
this.toolNames.set(callId, first.action);
|
|
47739
|
+
this.openCall(callId, first.action, foldInputs(held2, void 0));
|
|
47740
|
+
}
|
|
47182
47741
|
const open2 = this.openTools.get(callId);
|
|
47183
47742
|
this.openTools.delete(callId);
|
|
47184
47743
|
const tool = open2?.tool ?? openCodeToolNameWord(this.toolNames.get(callId) ?? "tool");
|
|
@@ -47225,16 +47784,45 @@ ${data.text}` : data.text;
|
|
|
47225
47784
|
);
|
|
47226
47785
|
}
|
|
47227
47786
|
}
|
|
47228
|
-
/**
|
|
47229
|
-
*
|
|
47230
|
-
|
|
47231
|
-
|
|
47232
|
-
|
|
47233
|
-
|
|
47234
|
-
|
|
47235
|
-
|
|
47236
|
-
|
|
47237
|
-
|
|
47787
|
+
/** Take the resource permissions held for a call, in the order they came:
|
|
47788
|
+
* the hold is over once they are read, whoever reads them. */
|
|
47789
|
+
takeHeldAsks(callId) {
|
|
47790
|
+
const held2 = this.unnamedAsks.get(callId) ?? [];
|
|
47791
|
+
this.unnamedAsks.delete(callId);
|
|
47792
|
+
return held2;
|
|
47793
|
+
}
|
|
47794
|
+
/** An `external_directory` ask answered once the call's line is already on
|
|
47795
|
+
* the record: the line cannot be amended, so the directory the call reached
|
|
47796
|
+
* is said in a note naming the call (harness.md item 13) rather than lost. */
|
|
47797
|
+
noteDirectoryReached(callId, asks) {
|
|
47798
|
+
const directories = [
|
|
47799
|
+
...new Set(
|
|
47800
|
+
asks.filter((ask) => ask.action === "external_directory").flatMap((ask) => Array.isArray(ask.resources) ? ask.resources : []).filter((r) => typeof r === "string")
|
|
47801
|
+
)
|
|
47802
|
+
];
|
|
47803
|
+
if (directories.length === 0) return;
|
|
47804
|
+
const tool = openCodeToolNameWord(this.toolNames.get(callId) ?? "tool");
|
|
47805
|
+
this.note(
|
|
47806
|
+
"directory_reached",
|
|
47807
|
+
`${tool} (call ${callId}) reached ${redactAndCap(directories.join(", "), 200)} after its line was written; the line does not name it`
|
|
47808
|
+
);
|
|
47809
|
+
}
|
|
47810
|
+
/** A resource permission's call, opened once the store names its part: under
|
|
47811
|
+
* the tool's word, with the tool's own input and the resources of every ask
|
|
47812
|
+
* held before the open — and of `ask`, one arriving with the part already in
|
|
47813
|
+
* hand — folded in (every directory the `external_directory` asks named, the
|
|
47814
|
+
* first as `directory`, all in `directories`); an ask that arrives after the
|
|
47815
|
+
* call opened is answered but cannot amend the `tool_call` already on the
|
|
47816
|
+
* record — the directory it named is said in a `directory_reached` note. */
|
|
47817
|
+
openNamedCall(callId, part, ask) {
|
|
47818
|
+
const held2 = this.takeHeldAsks(callId);
|
|
47819
|
+
if (ask !== void 0) held2.push(ask);
|
|
47820
|
+
if (this.openTools.has(callId) || this.toolNames.has(callId)) {
|
|
47821
|
+
this.noteDirectoryReached(callId, held2);
|
|
47822
|
+
return;
|
|
47823
|
+
}
|
|
47824
|
+
this.toolNames.set(callId, part.name);
|
|
47825
|
+
this.openCall(callId, part.name, foldInputs(held2, part.input));
|
|
47238
47826
|
}
|
|
47239
47827
|
bypass(out, detail) {
|
|
47240
47828
|
out.bypass = new OpenCodeGateBypassedError(detail);
|
|
@@ -47243,20 +47831,24 @@ ${data.text}` : data.text;
|
|
|
47243
47831
|
onPermissionAsked(request2, out) {
|
|
47244
47832
|
const callId = request2.source?.id ?? request2.id;
|
|
47245
47833
|
if (typeof request2.source?.messageID === "string") this.ownSteps.add(request2.source.messageID);
|
|
47246
|
-
if (this.answered.has(callId)) return;
|
|
47247
47834
|
if (this.decidedReplies.has(request2.id)) return;
|
|
47248
47835
|
const source2 = request2.source;
|
|
47249
47836
|
if (source2?.type === "tool" && this.observing === "own" && !this.openTools.has(callId) && !this.toolNames.has(callId)) {
|
|
47250
|
-
|
|
47251
|
-
|
|
47252
|
-
|
|
47253
|
-
|
|
47254
|
-
|
|
47255
|
-
|
|
47256
|
-
|
|
47257
|
-
|
|
47258
|
-
|
|
47259
|
-
|
|
47837
|
+
if (RESOURCE_PERMISSIONS.has(request2.action)) {
|
|
47838
|
+
const part = this.partTools.get(source2.id);
|
|
47839
|
+
if (part !== void 0) this.openNamedCall(callId, part, request2);
|
|
47840
|
+
else {
|
|
47841
|
+
const held2 = this.unnamedAsks.get(callId);
|
|
47842
|
+
if (held2 === void 0) this.unnamedAsks.set(callId, [request2]);
|
|
47843
|
+
else held2.push(request2);
|
|
47844
|
+
}
|
|
47845
|
+
} else {
|
|
47846
|
+
const raw = OPENCODE_ACTION_TO_TOOL_WORD[request2.action] !== void 0 ? request2.action : openCodeToolWord(request2.action);
|
|
47847
|
+
this.toolNames.set(callId, raw);
|
|
47848
|
+
this.openCall(callId, raw, openInput(request2, void 0));
|
|
47849
|
+
}
|
|
47850
|
+
} else if (source2?.type === "tool" && this.observing === "own" && request2.action === "external_directory") {
|
|
47851
|
+
this.noteDirectoryReached(callId, [request2]);
|
|
47260
47852
|
}
|
|
47261
47853
|
if (this.observing === "catching-up" && !this.pendingAtReattach.has(request2.id)) {
|
|
47262
47854
|
const name = this.toolNames.get(callId) ?? request2.action;
|
|
@@ -47271,7 +47863,7 @@ ${data.text}` : data.text;
|
|
|
47271
47863
|
this.deps.rules,
|
|
47272
47864
|
this.deps.relayedToolNames
|
|
47273
47865
|
);
|
|
47274
|
-
this.answered.set(callId, verdict.reply);
|
|
47866
|
+
if (this.answered.get(callId) !== "reject") this.answered.set(callId, verdict.reply);
|
|
47275
47867
|
this.decidedReplies.set(request2.id, { reply: verdict.reply, callId, echoed: false });
|
|
47276
47868
|
if (verdict.reply === "reject")
|
|
47277
47869
|
this.note("tool_refused", `${verdict.tool} refused: ${redactAndCap(verdict.message ?? "", 300)}`);
|
|
@@ -47326,7 +47918,11 @@ ${data.text}` : data.text;
|
|
|
47326
47918
|
return out;
|
|
47327
47919
|
}
|
|
47328
47920
|
onMessagesRefill(messages) {
|
|
47329
|
-
for (const message4 of messages)
|
|
47921
|
+
for (const message4 of messages)
|
|
47922
|
+
for (const part of toolPartsOf(message4)) {
|
|
47923
|
+
this.partTools.set(part.id, part);
|
|
47924
|
+
if (this.unnamedAsks.has(part.id)) this.openNamedCall(part.id, part);
|
|
47925
|
+
}
|
|
47330
47926
|
this.mirrorChain = this.mirrorChain.then(() => this.syncMirror(messages));
|
|
47331
47927
|
return { replies: [], settled: false };
|
|
47332
47928
|
}
|
|
@@ -47618,6 +48214,7 @@ async function openOpenCodeRun(deps, run2, settings) {
|
|
|
47618
48214
|
knownMessageIds: known,
|
|
47619
48215
|
writes: { seq: 0, ownPrompts: /* @__PURE__ */ new Map(), imported },
|
|
47620
48216
|
boundaries: new StepBoundaries(),
|
|
48217
|
+
failure: {},
|
|
47621
48218
|
paths: server.paths,
|
|
47622
48219
|
port: server.port,
|
|
47623
48220
|
password: server.password,
|
|
@@ -47641,16 +48238,17 @@ async function openOpenCodeRun(deps, run2, settings) {
|
|
|
47641
48238
|
);
|
|
47642
48239
|
let answer;
|
|
47643
48240
|
let remainingMs;
|
|
48241
|
+
let hardStopped;
|
|
47644
48242
|
let drained;
|
|
47645
48243
|
try {
|
|
47646
48244
|
let storeIds;
|
|
47647
|
-
({ answer, remainingMs, storeIds } = await driveOpenCode(deps, run2, conn));
|
|
48245
|
+
({ answer, remainingMs, storeIds, hardStopped } = await driveOpenCode(deps, run2, conn));
|
|
47648
48246
|
for (const id of storeIds) known.add(id);
|
|
47649
48247
|
} finally {
|
|
47650
48248
|
draining = false;
|
|
47651
48249
|
drained = await drainer;
|
|
47652
48250
|
}
|
|
47653
|
-
if (drained instanceof OpenCodeWriteUnresolvedError) throw drained;
|
|
48251
|
+
if (drained instanceof OpenCodeWriteUnresolvedError && !hardStopped) throw drained;
|
|
47654
48252
|
return {
|
|
47655
48253
|
answer,
|
|
47656
48254
|
followUp: async (input) => {
|
|
@@ -47788,9 +48386,18 @@ function drainFollowUps(deps, run2, conn, running, emit, note) {
|
|
|
47788
48386
|
const routes = openCodeSessionRoutes(conn.sessionID);
|
|
47789
48387
|
return (async () => {
|
|
47790
48388
|
const deferred = [];
|
|
48389
|
+
const senderOf = (input) => input.from !== void 0 ? `run:${input.from.runId}` : `user:${input.userId}`;
|
|
47791
48390
|
while (running()) {
|
|
47792
48391
|
const inputs = run2.inbox?.drain() ?? [];
|
|
47793
48392
|
for (const input of inputs) {
|
|
48393
|
+
if (input.from === void 0 && deferred.some((held2) => senderOf(held2) === senderOf(input))) {
|
|
48394
|
+
deferred.push(input);
|
|
48395
|
+
note(
|
|
48396
|
+
"follow_up",
|
|
48397
|
+
`follow-up held for a fresh turn behind an earlier one from the same sender the server did not take: ${redactSecrets(followUpSnippet(input))}`
|
|
48398
|
+
);
|
|
48399
|
+
continue;
|
|
48400
|
+
}
|
|
47794
48401
|
let stagedLine = "";
|
|
47795
48402
|
if (run2.stageFollowUps) {
|
|
47796
48403
|
try {
|
|
@@ -47891,8 +48498,8 @@ ${stagedLine}` : prompt;
|
|
|
47891
48498
|
} catch (err2) {
|
|
47892
48499
|
if (err2 instanceof OpenCodeWriteUnresolvedError) {
|
|
47893
48500
|
note("harness_error", `${err2.message} \u2014 the run is stopped`);
|
|
47894
|
-
|
|
47895
|
-
run2.inbox?.requeue([...deferred.splice(0), ...inputs.slice(inputs.indexOf(input)
|
|
48501
|
+
conn.failure.error = err2;
|
|
48502
|
+
run2.inbox?.requeue([...deferred.splice(0), ...inputs.slice(inputs.indexOf(input))]);
|
|
47896
48503
|
}
|
|
47897
48504
|
throw err2;
|
|
47898
48505
|
}
|
|
@@ -47906,8 +48513,8 @@ ${stagedLine}` : prompt;
|
|
|
47906
48513
|
run2.inbox?.requeue([...deferred.splice(0), ...rest]);
|
|
47907
48514
|
return;
|
|
47908
48515
|
}
|
|
47909
|
-
deferred.push(
|
|
47910
|
-
|
|
48516
|
+
deferred.push(input);
|
|
48517
|
+
continue;
|
|
47911
48518
|
}
|
|
47912
48519
|
const source2 = {
|
|
47913
48520
|
...input.sourceUrl ? { url: input.sourceUrl } : {},
|
|
@@ -50623,6 +51230,17 @@ function clickSlackIO(client, click, opts = {}) {
|
|
|
50623
51230
|
{ ...opts, offerMessage: click.offer }
|
|
50624
51231
|
);
|
|
50625
51232
|
}
|
|
51233
|
+
function takenOfferNote(kind, userId) {
|
|
51234
|
+
const button = kind === "confirm" ? "Run" : "Cancel";
|
|
51235
|
+
const doing = kind === "confirm" ? "running\u2026" : "cancelling\u2026";
|
|
51236
|
+
return { mrkdwn: `*${button}* clicked by <@${userId}> \xB7 ${doing}`, plain: `${button} clicked \xB7 ${doing}` };
|
|
51237
|
+
}
|
|
51238
|
+
function takenOfferBlocks(blocks, kind, userId) {
|
|
51239
|
+
return [
|
|
51240
|
+
...blocks.filter((b) => b.type !== "actions"),
|
|
51241
|
+
{ type: "context", elements: [{ type: "mrkdwn", text: takenOfferNote(kind, userId).mrkdwn }] }
|
|
51242
|
+
];
|
|
51243
|
+
}
|
|
50626
51244
|
async function handleConfirmClick(deps, { client, statusClient }, { ack, body, action }, botUserId) {
|
|
50627
51245
|
await ack();
|
|
50628
51246
|
const kind = action.action_id === "confirm.run" ? "confirm" : action.action_id === "confirm.cancel" ? "cancel" : void 0;
|
|
@@ -50637,6 +51255,19 @@ async function handleConfirmClick(deps, { client, statusClient }, { ack, body, a
|
|
|
50637
51255
|
return;
|
|
50638
51256
|
}
|
|
50639
51257
|
const threadTs = message4.thread_ts ?? container?.thread_ts ?? message4.ts;
|
|
51258
|
+
try {
|
|
51259
|
+
await client.chat.update({
|
|
51260
|
+
channel,
|
|
51261
|
+
ts: message4.ts,
|
|
51262
|
+
text: `${message4.text ?? ""}
|
|
51263
|
+
${takenOfferNote(kind, body.user.id).plain}`,
|
|
51264
|
+
blocks: takenOfferBlocks(message4.blocks ?? [], kind, body.user.id)
|
|
51265
|
+
});
|
|
51266
|
+
} catch (err2) {
|
|
51267
|
+
console.error(
|
|
51268
|
+
`[confirm] ${kind} click on ${id} in ${channel}:${message4.ts}: the buttons could not be taken down \u2014 ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
51269
|
+
);
|
|
51270
|
+
}
|
|
50640
51271
|
let io;
|
|
50641
51272
|
try {
|
|
50642
51273
|
const requester = await resolveSlackRequester(client, {
|
|
@@ -51683,6 +52314,500 @@ var init_scheduledPanel = __esm({
|
|
|
51683
52314
|
}
|
|
51684
52315
|
});
|
|
51685
52316
|
|
|
52317
|
+
// ../../src/channels/accessAuth.ts
|
|
52318
|
+
import { createPublicKey, verify as cryptoVerify } from "node:crypto";
|
|
52319
|
+
function isServiceToken(identity2) {
|
|
52320
|
+
return identity2.sub === "" && typeof identity2.commonName === "string" && identity2.commonName !== "";
|
|
52321
|
+
}
|
|
52322
|
+
function decodeBase64Url(part) {
|
|
52323
|
+
if (part === "" || /[^A-Za-z0-9_-]/.test(part)) return null;
|
|
52324
|
+
return Buffer.from(part, "base64url");
|
|
52325
|
+
}
|
|
52326
|
+
function decodeJsonSegment(part) {
|
|
52327
|
+
const buf2 = decodeBase64Url(part);
|
|
52328
|
+
if (!buf2) return null;
|
|
52329
|
+
try {
|
|
52330
|
+
const parsed = JSON.parse(buf2.toString("utf8"));
|
|
52331
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
52332
|
+
return parsed;
|
|
52333
|
+
} catch {
|
|
52334
|
+
return null;
|
|
52335
|
+
}
|
|
52336
|
+
}
|
|
52337
|
+
async function resolveKey(kid, config, deps) {
|
|
52338
|
+
const cache2 = deps.cache ?? new JwksCache();
|
|
52339
|
+
const cached = cache2.get(kid, deps.now());
|
|
52340
|
+
if (cached) return cached;
|
|
52341
|
+
const ttlMs = (deps.ttlSeconds ?? DEFAULT_JWKS_TTL_SECONDS) * 1e3;
|
|
52342
|
+
const minRefetchIntervalMs = deps.minRefetchIntervalMs ?? DEFAULT_MIN_REFETCH_INTERVAL_MS;
|
|
52343
|
+
const certsUrl = `https://${config.teamDomain}${CERTS_PATH}`;
|
|
52344
|
+
await cache2.refresh(() => deps.fetchJwks(certsUrl), deps.now, ttlMs, minRefetchIntervalMs);
|
|
52345
|
+
return cache2.get(kid, deps.now());
|
|
52346
|
+
}
|
|
52347
|
+
function claimsValid(claims, config, now) {
|
|
52348
|
+
const nowSec = Math.floor(now / 1e3);
|
|
52349
|
+
if (claims.iss !== `https://${config.teamDomain}`) return false;
|
|
52350
|
+
const aud = claims.aud;
|
|
52351
|
+
const audOk = typeof aud === "string" ? aud === config.aud : Array.isArray(aud) && aud.includes(config.aud);
|
|
52352
|
+
if (!audOk) return false;
|
|
52353
|
+
if (typeof claims.exp !== "number" || claims.exp <= nowSec) return false;
|
|
52354
|
+
if (typeof claims.nbf === "number" && claims.nbf > nowSec + SKEW_SECONDS) return false;
|
|
52355
|
+
if (typeof claims.iat === "number" && claims.iat > nowSec + SKEW_SECONDS) return false;
|
|
52356
|
+
return hasSubject(claims);
|
|
52357
|
+
}
|
|
52358
|
+
function hasSubject(claims) {
|
|
52359
|
+
if (typeof claims.sub === "string" && claims.sub !== "") return true;
|
|
52360
|
+
const subEmpty = claims.sub === void 0 || claims.sub === "";
|
|
52361
|
+
return subEmpty && typeof claims.common_name === "string" && claims.common_name !== "";
|
|
52362
|
+
}
|
|
52363
|
+
async function verifyAccessJwt(token2, config, deps) {
|
|
52364
|
+
if (typeof token2 !== "string") return null;
|
|
52365
|
+
const parts = token2.split(".");
|
|
52366
|
+
if (parts.length !== 3) return null;
|
|
52367
|
+
const [rawHeader, rawPayload, rawSignature] = parts;
|
|
52368
|
+
const header4 = decodeJsonSegment(rawHeader);
|
|
52369
|
+
const payload = decodeJsonSegment(rawPayload);
|
|
52370
|
+
if (!header4 || !payload) return null;
|
|
52371
|
+
if (header4.alg !== "RS256") return null;
|
|
52372
|
+
if (typeof header4.kid !== "string" || header4.kid === "") return null;
|
|
52373
|
+
const signature = decodeBase64Url(rawSignature);
|
|
52374
|
+
if (!signature) return null;
|
|
52375
|
+
const jwk = await resolveKey(header4.kid, config, deps);
|
|
52376
|
+
if (!jwk) return null;
|
|
52377
|
+
let signatureOk;
|
|
52378
|
+
try {
|
|
52379
|
+
const key = createPublicKey({ format: "jwk", key: jwk });
|
|
52380
|
+
signatureOk = cryptoVerify("RSA-SHA256", Buffer.from(`${rawHeader}.${rawPayload}`), key, signature);
|
|
52381
|
+
} catch {
|
|
52382
|
+
return null;
|
|
52383
|
+
}
|
|
52384
|
+
if (!signatureOk) return null;
|
|
52385
|
+
if (!claimsValid(payload, config, deps.now())) return null;
|
|
52386
|
+
if (typeof payload.sub !== "string" || payload.sub === "") {
|
|
52387
|
+
return { sub: "", commonName: payload.common_name };
|
|
52388
|
+
}
|
|
52389
|
+
return {
|
|
52390
|
+
sub: payload.sub,
|
|
52391
|
+
email: typeof payload.email === "string" ? payload.email : void 0
|
|
52392
|
+
};
|
|
52393
|
+
}
|
|
52394
|
+
function normalizeTeamDomain(raw) {
|
|
52395
|
+
return raw.trim().replace(/^https?:\/\//i, "").replace(/\/+$/, "");
|
|
52396
|
+
}
|
|
52397
|
+
function parseAccessConfig(env) {
|
|
52398
|
+
const teamDomain = normalizeTeamDomain(env.ACCESS_TEAM_DOMAIN ?? "");
|
|
52399
|
+
const aud = (env.ACCESS_AUD ?? "").trim();
|
|
52400
|
+
if (teamDomain === "" || aud === "") return null;
|
|
52401
|
+
return { teamDomain, aud };
|
|
52402
|
+
}
|
|
52403
|
+
var CERTS_PATH, DEFAULT_JWKS_TTL_SECONDS, DEFAULT_MIN_REFETCH_INTERVAL_MS, SKEW_SECONDS, httpJwksFetcher, JwksCache;
|
|
52404
|
+
var init_accessAuth = __esm({
|
|
52405
|
+
"../../src/channels/accessAuth.ts"() {
|
|
52406
|
+
"use strict";
|
|
52407
|
+
CERTS_PATH = "/cdn-cgi/access/certs";
|
|
52408
|
+
DEFAULT_JWKS_TTL_SECONDS = 3600;
|
|
52409
|
+
DEFAULT_MIN_REFETCH_INTERVAL_MS = 3e4;
|
|
52410
|
+
SKEW_SECONDS = 60;
|
|
52411
|
+
httpJwksFetcher = async (certsUrl) => {
|
|
52412
|
+
const res = await fetch(certsUrl);
|
|
52413
|
+
if (!res.ok) throw new Error(`JWKS fetch failed: HTTP ${res.status}`);
|
|
52414
|
+
const body = await res.json();
|
|
52415
|
+
const keys = body?.keys;
|
|
52416
|
+
if (!Array.isArray(keys)) throw new Error("JWKS response missing keys[]");
|
|
52417
|
+
return keys;
|
|
52418
|
+
};
|
|
52419
|
+
JwksCache = class {
|
|
52420
|
+
entries = /* @__PURE__ */ new Map();
|
|
52421
|
+
/** Ms of the last SUCCESSFUL JWKS fetch, or null before the first one. Drives
|
|
52422
|
+
* the negative-cache interval in `refresh`; a failed fetch does not set it. */
|
|
52423
|
+
lastFetchAt = null;
|
|
52424
|
+
/** The in-flight refresh shared by concurrent callers (single-flight); null
|
|
52425
|
+
* when no fetch is running. It never rejects — `runFetch` swallows failures to
|
|
52426
|
+
* fail closed — so both the owner and any joiners resolve cleanly. */
|
|
52427
|
+
inFlight = null;
|
|
52428
|
+
/** The fresh JWK for `kid`, or null if absent/expired (expired entries are
|
|
52429
|
+
* evicted lazily). `now` is milliseconds. */
|
|
52430
|
+
get(kid, now) {
|
|
52431
|
+
const hit = this.entries.get(kid);
|
|
52432
|
+
if (!hit) return null;
|
|
52433
|
+
if (hit.expiresAt <= now) {
|
|
52434
|
+
this.entries.delete(kid);
|
|
52435
|
+
return null;
|
|
52436
|
+
}
|
|
52437
|
+
return hit.jwk;
|
|
52438
|
+
}
|
|
52439
|
+
/** Store every kid'd key from a freshly fetched JWKS with the given absolute
|
|
52440
|
+
* expiry (ms). Keyless entries are skipped. */
|
|
52441
|
+
put(keys, expiresAt) {
|
|
52442
|
+
for (const jwk of keys) {
|
|
52443
|
+
if (typeof jwk.kid === "string" && jwk.kid !== "") {
|
|
52444
|
+
this.entries.set(jwk.kid, { jwk, expiresAt });
|
|
52445
|
+
}
|
|
52446
|
+
}
|
|
52447
|
+
}
|
|
52448
|
+
/**
|
|
52449
|
+
* Refresh the JWKS to (try to) learn an unknown kid, with two guards against
|
|
52450
|
+
* the unknown-kid amplification vector:
|
|
52451
|
+
* - single-flight: if a refresh is already running, join it instead of
|
|
52452
|
+
* firing another — N concurrent unknown kids ⇒ at most one `fetch` call.
|
|
52453
|
+
* - negative cache: once a fetch has succeeded, another is suppressed until
|
|
52454
|
+
* `minRefetchIntervalMs` has elapsed, so an unknown kid resolves to null
|
|
52455
|
+
* WITHOUT a network round-trip inside that window. A genuinely rotated kid
|
|
52456
|
+
* is still picked up on the first refresh after the interval.
|
|
52457
|
+
* Fail-closed: a fetch failure leaves the store unchanged and does not advance
|
|
52458
|
+
* the last-fetch time. `now` is read lazily (ms) so the injected clock can move
|
|
52459
|
+
* across the await.
|
|
52460
|
+
*/
|
|
52461
|
+
async refresh(fetch2, now, ttlMs, minRefetchIntervalMs) {
|
|
52462
|
+
if (this.inFlight) {
|
|
52463
|
+
await this.inFlight;
|
|
52464
|
+
return;
|
|
52465
|
+
}
|
|
52466
|
+
if (this.lastFetchAt !== null && now() - this.lastFetchAt < minRefetchIntervalMs) {
|
|
52467
|
+
return;
|
|
52468
|
+
}
|
|
52469
|
+
this.inFlight = this.runFetch(fetch2, now, ttlMs);
|
|
52470
|
+
try {
|
|
52471
|
+
await this.inFlight;
|
|
52472
|
+
} finally {
|
|
52473
|
+
this.inFlight = null;
|
|
52474
|
+
}
|
|
52475
|
+
}
|
|
52476
|
+
/** Perform one JWKS fetch and fold the result into the store. Never rejects:
|
|
52477
|
+
* on any failure the store is left unchanged (fail closed) and `lastFetchAt`
|
|
52478
|
+
* is not advanced, so `resolveKey` returns null and a later verify may retry. */
|
|
52479
|
+
async runFetch(fetch2, now, ttlMs) {
|
|
52480
|
+
try {
|
|
52481
|
+
const keys = await fetch2();
|
|
52482
|
+
const fetchedAt = now();
|
|
52483
|
+
this.put(keys, fetchedAt + ttlMs);
|
|
52484
|
+
this.lastFetchAt = fetchedAt;
|
|
52485
|
+
} catch {
|
|
52486
|
+
}
|
|
52487
|
+
}
|
|
52488
|
+
};
|
|
52489
|
+
}
|
|
52490
|
+
});
|
|
52491
|
+
|
|
52492
|
+
// ../../src/channels/commandHttp.ts
|
|
52493
|
+
function normalizePath2(pathname) {
|
|
52494
|
+
let decoded = pathname;
|
|
52495
|
+
try {
|
|
52496
|
+
decoded = decodeURIComponent(pathname);
|
|
52497
|
+
} catch {
|
|
52498
|
+
}
|
|
52499
|
+
return decoded.replace(/\/{2,}/g, "/");
|
|
52500
|
+
}
|
|
52501
|
+
function isCommandPath(pathname) {
|
|
52502
|
+
const p = normalizePath2(pathname).toLowerCase();
|
|
52503
|
+
return p === API_ROOT || p === `${API_ROOT}/` || p.startsWith(`${API_ROOT}/`);
|
|
52504
|
+
}
|
|
52505
|
+
function commandIdFromPath(pathname) {
|
|
52506
|
+
const p = normalizePath2(pathname);
|
|
52507
|
+
if (!p.startsWith(`${API_ROOT}/`)) return void 0;
|
|
52508
|
+
const rest = p.slice(API_ROOT.length + 1);
|
|
52509
|
+
return COMMAND_ID.test(rest) ? rest : void 0;
|
|
52510
|
+
}
|
|
52511
|
+
function callerIdFor(identity2) {
|
|
52512
|
+
return isServiceToken(identity2) ? `access:svc:${identity2.commonName}` : `access:${identity2.sub}`;
|
|
52513
|
+
}
|
|
52514
|
+
function accessActor(identity2, grantsFor) {
|
|
52515
|
+
return isServiceToken(identity2) ? resolveActor({ surface: "access-service", subjectId: identity2.commonName }, grantsFor) : resolveActor({ surface: "access-browser", subjectId: identity2.sub }, grantsFor);
|
|
52516
|
+
}
|
|
52517
|
+
function serviceTokenAllowed(pathname, identity2) {
|
|
52518
|
+
return !isServiceToken(identity2) || isCommandPath(pathname);
|
|
52519
|
+
}
|
|
52520
|
+
async function resolveAccessActor(identity2, opts) {
|
|
52521
|
+
const actor = accessActor(identity2, opts.grantsFor);
|
|
52522
|
+
if (isServiceToken(identity2)) return actor;
|
|
52523
|
+
if (identity2.viewAs !== void 0 && holdsAll(actor) && isViewablePerson(identity2.viewAs))
|
|
52524
|
+
return viewingAs(actor, identity2.viewAs, opts);
|
|
52525
|
+
if (!identity2.email || !opts.personByEmail) return actor;
|
|
52526
|
+
const person = await opts.personByEmail(identity2.email).catch(() => void 0);
|
|
52527
|
+
if (!person || !person.id.startsWith("slack:")) return actor;
|
|
52528
|
+
const memberOf = opts.channelsOf ? await boundedChannels(opts.channelsOf(person.id), opts.channelsOfTimeoutMs ?? CHANNEL_DIRECTORY_TIMEOUT_MS) : "unknown";
|
|
52529
|
+
return {
|
|
52530
|
+
...actor,
|
|
52531
|
+
self: [actor.id, person.id],
|
|
52532
|
+
asUser: { id: person.id, ...person.name ? { name: person.name } : {} },
|
|
52533
|
+
...memberOf === "unknown" ? {} : { memberOf }
|
|
52534
|
+
};
|
|
52535
|
+
}
|
|
52536
|
+
async function viewingAs(admin, personId, opts) {
|
|
52537
|
+
const [memberOf, name] = await Promise.all([
|
|
52538
|
+
opts.channelsOf ? boundedChannels(opts.channelsOf(personId), opts.channelsOfTimeoutMs ?? CHANNEL_DIRECTORY_TIMEOUT_MS) : Promise.resolve("unknown"),
|
|
52539
|
+
opts.personName ? opts.personName(personId).catch(() => void 0) : Promise.resolve(void 0)
|
|
52540
|
+
]);
|
|
52541
|
+
const person = {
|
|
52542
|
+
kind: "user",
|
|
52543
|
+
id: personId,
|
|
52544
|
+
// The browser baseline under a subject no config names — what the person's own session
|
|
52545
|
+
// holds, whose `sub` this process does not know (an entry keyed to that sub is the one
|
|
52546
|
+
// thing view-as cannot see; record 0053 names the boundary).
|
|
52547
|
+
grants: opts.grantsFor(`access:${personId}`),
|
|
52548
|
+
self: [personId],
|
|
52549
|
+
asUser: { id: personId, ...name ? { name } : {} },
|
|
52550
|
+
...memberOf === "unknown" ? {} : { memberOf }
|
|
52551
|
+
};
|
|
52552
|
+
return {
|
|
52553
|
+
...admin,
|
|
52554
|
+
onBehalfOf: person,
|
|
52555
|
+
asUser: person.asUser,
|
|
52556
|
+
viewingAs: { id: personId, ...name ? { name } : {} }
|
|
52557
|
+
};
|
|
52558
|
+
}
|
|
52559
|
+
async function boundedChannels(lookup2, timeoutMs) {
|
|
52560
|
+
let timer;
|
|
52561
|
+
try {
|
|
52562
|
+
const answer = await Promise.race([
|
|
52563
|
+
lookup2.catch(() => "unknown"),
|
|
52564
|
+
new Promise((resolve13) => {
|
|
52565
|
+
timer = setTimeout(() => resolve13(LOOKUP_TIMED_OUT), timeoutMs);
|
|
52566
|
+
})
|
|
52567
|
+
]);
|
|
52568
|
+
return answer === LOOKUP_TIMED_OUT ? "unknown" : answer;
|
|
52569
|
+
} finally {
|
|
52570
|
+
if (timer) clearTimeout(timer);
|
|
52571
|
+
}
|
|
52572
|
+
}
|
|
52573
|
+
async function callerFor(identity2, opts) {
|
|
52574
|
+
return {
|
|
52575
|
+
kind: "access",
|
|
52576
|
+
id: callerIdFor(identity2),
|
|
52577
|
+
actor: await resolveAccessActor(identity2, opts),
|
|
52578
|
+
...identity2.email && !isServiceToken(identity2) ? { email: identity2.email } : {}
|
|
52579
|
+
};
|
|
52580
|
+
}
|
|
52581
|
+
function hostOf(url) {
|
|
52582
|
+
if (!url) return void 0;
|
|
52583
|
+
try {
|
|
52584
|
+
return new URL(url).host.toLowerCase();
|
|
52585
|
+
} catch {
|
|
52586
|
+
return void 0;
|
|
52587
|
+
}
|
|
52588
|
+
}
|
|
52589
|
+
function header2(req, name) {
|
|
52590
|
+
const v = req.headers[name];
|
|
52591
|
+
return Array.isArray(v) ? v[0] : v;
|
|
52592
|
+
}
|
|
52593
|
+
function originAllowed(req, opts) {
|
|
52594
|
+
const site = header2(req, "sec-fetch-site");
|
|
52595
|
+
if (site !== void 0 && site !== "same-origin" && site !== "none") return false;
|
|
52596
|
+
const origin = header2(req, "origin");
|
|
52597
|
+
if (origin === void 0) return true;
|
|
52598
|
+
const expected = originOf(opts.publicBaseUrl);
|
|
52599
|
+
if (expected !== void 0) return originOf(origin) === expected;
|
|
52600
|
+
const host = header2(req, "host")?.toLowerCase();
|
|
52601
|
+
const actual = hostOf(origin);
|
|
52602
|
+
return host !== void 0 && actual !== void 0 && actual === host;
|
|
52603
|
+
}
|
|
52604
|
+
function originOf(url) {
|
|
52605
|
+
if (!url) return void 0;
|
|
52606
|
+
try {
|
|
52607
|
+
const o = new URL(url).origin;
|
|
52608
|
+
return o === "null" ? void 0 : o.toLowerCase();
|
|
52609
|
+
} catch {
|
|
52610
|
+
return void 0;
|
|
52611
|
+
}
|
|
52612
|
+
}
|
|
52613
|
+
function isJsonContentType(req) {
|
|
52614
|
+
const ct = header2(req, "content-type");
|
|
52615
|
+
return ct !== void 0 && ct.split(";")[0].trim().toLowerCase() === "application/json";
|
|
52616
|
+
}
|
|
52617
|
+
function createCommandHttpHandler(commands, opts) {
|
|
52618
|
+
const maxBytes = opts.maxBodyBytes ?? MAX_BODY_BYTES2;
|
|
52619
|
+
const send = (res, status3, body, extra = {}) => {
|
|
52620
|
+
res.writeHead(status3, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", ...extra });
|
|
52621
|
+
res.end(JSON.stringify(body));
|
|
52622
|
+
};
|
|
52623
|
+
const refuse3 = (res, status3, code, error, extra = {}) => send(res, status3, { error, code }, extra);
|
|
52624
|
+
return async (req, res, identity2) => {
|
|
52625
|
+
const url = new URL(req.url ?? "/", "http://placeholder.invalid");
|
|
52626
|
+
const id = commandIdFromPath(url.pathname);
|
|
52627
|
+
const cmd = id === void 0 ? void 0 : commands.get(id);
|
|
52628
|
+
if (!cmd || !CommandRegistry.exposedTo(cmd, "access")) {
|
|
52629
|
+
refuse3(res, 404, "not_found", "unknown command");
|
|
52630
|
+
return;
|
|
52631
|
+
}
|
|
52632
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
52633
|
+
const allow = cmd.effect === "write" ? "POST" : "GET, POST";
|
|
52634
|
+
if (method !== "POST" && (cmd.effect === "write" || method !== "GET")) {
|
|
52635
|
+
refuse3(res, 405, "method_not_allowed", `${cmd.id} accepts ${allow}`, { allow });
|
|
52636
|
+
return;
|
|
52637
|
+
}
|
|
52638
|
+
if (method === "POST" && !isJsonContentType(req)) {
|
|
52639
|
+
refuse3(res, 415, "unsupported_media_type", "content-type must be application/json");
|
|
52640
|
+
return;
|
|
52641
|
+
}
|
|
52642
|
+
if (cmd.effect === "write" && !originAllowed(req, opts)) {
|
|
52643
|
+
refuse3(res, 403, "forbidden_origin", "cross-origin writes are refused");
|
|
52644
|
+
return;
|
|
52645
|
+
}
|
|
52646
|
+
const caller = await callerFor(identity2, opts);
|
|
52647
|
+
if (CommandRegistry.refuses(cmd, caller)) {
|
|
52648
|
+
refuse3(res, ERROR_STATUS.unauthorized, "unauthorized", `${caller.id} is not allowed to run ${cmd.id}`);
|
|
52649
|
+
return;
|
|
52650
|
+
}
|
|
52651
|
+
let named;
|
|
52652
|
+
let keys;
|
|
52653
|
+
if (method === "GET") {
|
|
52654
|
+
named = Object.fromEntries(url.searchParams);
|
|
52655
|
+
keys = "kebab";
|
|
52656
|
+
} else {
|
|
52657
|
+
const read = await readBody(req, maxBytes);
|
|
52658
|
+
if (!read.ok) {
|
|
52659
|
+
refuse3(res, 413, "payload_too_large", "request body too large");
|
|
52660
|
+
req.destroy();
|
|
52661
|
+
return;
|
|
52662
|
+
}
|
|
52663
|
+
let body;
|
|
52664
|
+
try {
|
|
52665
|
+
body = read.body.trim() === "" ? {} : JSON.parse(read.body);
|
|
52666
|
+
} catch {
|
|
52667
|
+
refuse3(res, ERROR_STATUS.invalid_input, "invalid_input", "body must be a JSON object");
|
|
52668
|
+
return;
|
|
52669
|
+
}
|
|
52670
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
52671
|
+
refuse3(res, ERROR_STATUS.invalid_input, "invalid_input", "body must be a JSON object");
|
|
52672
|
+
return;
|
|
52673
|
+
}
|
|
52674
|
+
named = body;
|
|
52675
|
+
keys = "camel";
|
|
52676
|
+
}
|
|
52677
|
+
const input = namedToInput(cmd, named, keys);
|
|
52678
|
+
if ("error" in input) {
|
|
52679
|
+
refuse3(res, ERROR_STATUS.invalid_input, "invalid_input", input.error);
|
|
52680
|
+
return;
|
|
52681
|
+
}
|
|
52682
|
+
const result = await commands.invoke(cmd.id, input, caller);
|
|
52683
|
+
if (!result.ok) {
|
|
52684
|
+
refuse3(res, result.status, result.error, result.message);
|
|
52685
|
+
return;
|
|
52686
|
+
}
|
|
52687
|
+
send(res, 200, result.value);
|
|
52688
|
+
};
|
|
52689
|
+
}
|
|
52690
|
+
var API_ROOT, LOOKUP_TIMED_OUT;
|
|
52691
|
+
var init_commandHttp = __esm({
|
|
52692
|
+
"../../src/channels/commandHttp.ts"() {
|
|
52693
|
+
"use strict";
|
|
52694
|
+
init_actor();
|
|
52695
|
+
init_commandRegistry();
|
|
52696
|
+
init_commandSurface();
|
|
52697
|
+
init_record();
|
|
52698
|
+
init_accessAuth();
|
|
52699
|
+
init_viewAs();
|
|
52700
|
+
init_http();
|
|
52701
|
+
API_ROOT = "/api";
|
|
52702
|
+
LOOKUP_TIMED_OUT = /* @__PURE__ */ Symbol("channelsOf timed out");
|
|
52703
|
+
}
|
|
52704
|
+
});
|
|
52705
|
+
|
|
52706
|
+
// ../../src/channels/viewAs.ts
|
|
52707
|
+
function viewAsFromCookie(header4) {
|
|
52708
|
+
const raw = Array.isArray(header4) ? header4.join("; ") : header4;
|
|
52709
|
+
if (!raw) return void 0;
|
|
52710
|
+
for (const part of raw.split(";")) {
|
|
52711
|
+
const eq = part.indexOf("=");
|
|
52712
|
+
if (eq < 0) continue;
|
|
52713
|
+
if (part.slice(0, eq).trim() !== VIEW_AS_COOKIE) continue;
|
|
52714
|
+
try {
|
|
52715
|
+
const value = decodeURIComponent(part.slice(eq + 1).trim());
|
|
52716
|
+
if (value !== "") return value;
|
|
52717
|
+
} catch {
|
|
52718
|
+
}
|
|
52719
|
+
}
|
|
52720
|
+
return void 0;
|
|
52721
|
+
}
|
|
52722
|
+
function isViewAsPath(pathname) {
|
|
52723
|
+
return pathname === VIEW_AS_PATH || pathname === VIEW_AS_EXIT_PATH;
|
|
52724
|
+
}
|
|
52725
|
+
function requestersOf(rows) {
|
|
52726
|
+
const seen = /* @__PURE__ */ new Map();
|
|
52727
|
+
for (const r of rows) {
|
|
52728
|
+
if (!r.userId || !isViewablePerson(r.userId) || seen.has(r.userId)) continue;
|
|
52729
|
+
seen.set(r.userId, { id: r.userId, ...r.userName ? { name: r.userName } : {} });
|
|
52730
|
+
}
|
|
52731
|
+
return [...seen.values()].sort((a, b) => (a.name ?? a.id).localeCompare(b.name ?? b.id));
|
|
52732
|
+
}
|
|
52733
|
+
function refuseWhileViewing(res, person) {
|
|
52734
|
+
res.writeHead(403, JSON_NO_STORE);
|
|
52735
|
+
res.end(JSON.stringify({ error: "unauthorized", message: viewingRefusal(person) }));
|
|
52736
|
+
return true;
|
|
52737
|
+
}
|
|
52738
|
+
function cookieHeader(value, deps, clear) {
|
|
52739
|
+
const attrs = [
|
|
52740
|
+
"Path=/",
|
|
52741
|
+
"HttpOnly",
|
|
52742
|
+
"SameSite=Strict",
|
|
52743
|
+
...deps.secure ? ["Secure"] : [],
|
|
52744
|
+
...clear ? ["Max-Age=0"] : []
|
|
52745
|
+
];
|
|
52746
|
+
return `${VIEW_AS_COOKIE}=${encodeURIComponent(value)}; ${attrs.join("; ")}`;
|
|
52747
|
+
}
|
|
52748
|
+
function createViewAsHandler(deps) {
|
|
52749
|
+
const json3 = (res, status3, body, extra = {}) => {
|
|
52750
|
+
res.writeHead(status3, { ...JSON_NO_STORE, ...extra });
|
|
52751
|
+
res.end(JSON.stringify(body));
|
|
52752
|
+
};
|
|
52753
|
+
return (req, res, ctx) => {
|
|
52754
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
52755
|
+
if (!isViewAsPath(url.pathname)) return false;
|
|
52756
|
+
if ((req.method ?? "GET").toUpperCase() !== "POST") {
|
|
52757
|
+
json3(res, 405, { error: "method_not_allowed" }, { allow: "POST" });
|
|
52758
|
+
return true;
|
|
52759
|
+
}
|
|
52760
|
+
if (!originAllowed(req, { publicBaseUrl: deps.publicBaseUrl })) {
|
|
52761
|
+
json3(res, 403, { error: "forbidden_origin" });
|
|
52762
|
+
req.destroy();
|
|
52763
|
+
return true;
|
|
52764
|
+
}
|
|
52765
|
+
if (!holdsAll(ctx.actor)) {
|
|
52766
|
+
json3(res, 403, { error: "unauthorized", message: "Only a session holding every grant may view as a person." });
|
|
52767
|
+
req.destroy();
|
|
52768
|
+
return true;
|
|
52769
|
+
}
|
|
52770
|
+
if (url.pathname === VIEW_AS_EXIT_PATH) {
|
|
52771
|
+
res.writeHead(204, { "set-cookie": cookieHeader("", deps, true), "cache-control": "no-store" });
|
|
52772
|
+
res.end();
|
|
52773
|
+
return true;
|
|
52774
|
+
}
|
|
52775
|
+
readBody(req, MAX_BODY_BYTES3).then((read) => {
|
|
52776
|
+
if (!read.ok) {
|
|
52777
|
+
json3(res, 413, { error: "invalid_input", message: "body too large" });
|
|
52778
|
+
return;
|
|
52779
|
+
}
|
|
52780
|
+
let person;
|
|
52781
|
+
try {
|
|
52782
|
+
person = JSON.parse(read.body).person;
|
|
52783
|
+
} catch {
|
|
52784
|
+
person = void 0;
|
|
52785
|
+
}
|
|
52786
|
+
if (typeof person !== "string" || !isViewablePerson(person)) {
|
|
52787
|
+
json3(res, 400, { error: "invalid_input", message: "person must be a Slack person id" });
|
|
52788
|
+
return;
|
|
52789
|
+
}
|
|
52790
|
+
res.writeHead(204, { "set-cookie": cookieHeader(person, deps, false), "cache-control": "no-store" });
|
|
52791
|
+
res.end();
|
|
52792
|
+
}).catch(() => json3(res, 400, { error: "invalid_input", message: "body unreadable" }));
|
|
52793
|
+
return true;
|
|
52794
|
+
};
|
|
52795
|
+
}
|
|
52796
|
+
var VIEW_AS_COOKIE, VIEW_AS_PATH, VIEW_AS_EXIT_PATH, MAX_BODY_BYTES3, JSON_NO_STORE;
|
|
52797
|
+
var init_viewAs2 = __esm({
|
|
52798
|
+
"../../src/channels/viewAs.ts"() {
|
|
52799
|
+
"use strict";
|
|
52800
|
+
init_viewAs();
|
|
52801
|
+
init_commandHttp();
|
|
52802
|
+
init_http();
|
|
52803
|
+
VIEW_AS_COOKIE = "sb-view-as";
|
|
52804
|
+
VIEW_AS_PATH = "/runs/view-as";
|
|
52805
|
+
VIEW_AS_EXIT_PATH = "/runs/view-as/exit";
|
|
52806
|
+
MAX_BODY_BYTES3 = 1024;
|
|
52807
|
+
JSON_NO_STORE = { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" };
|
|
52808
|
+
}
|
|
52809
|
+
});
|
|
52810
|
+
|
|
51686
52811
|
// ../../src/channels/liveView/html.ts
|
|
51687
52812
|
function escapeHtml(s) {
|
|
51688
52813
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -51808,7 +52933,11 @@ function pageFavicon(seed) {
|
|
|
51808
52933
|
}
|
|
51809
52934
|
}
|
|
51810
52935
|
function makeShellRenderer(assets, capabilities) {
|
|
51811
|
-
return (title, seed) => renderShell(
|
|
52936
|
+
return (viewer, title, seed) => renderShell(
|
|
52937
|
+
title,
|
|
52938
|
+
{ ...seed, capabilities, ...viewer?.viewingAs ? { viewingAs: viewer.viewingAs } : {} },
|
|
52939
|
+
assets
|
|
52940
|
+
);
|
|
51812
52941
|
}
|
|
51813
52942
|
var PAGE_CSP, FORM_PAGE_CSP, WEB_HTML_HEADERS;
|
|
51814
52943
|
var init_webShell = __esm({
|
|
@@ -52166,10 +53295,10 @@ function createLiveViewHandler(deps) {
|
|
|
52166
53295
|
return token2 === void 0 ? v : { ...v, token: token2 };
|
|
52167
53296
|
};
|
|
52168
53297
|
const liveTokens = () => new Map(index.listActive().map((s) => [s.id, s.token]));
|
|
52169
|
-
const notFoundPage = (res) => {
|
|
53298
|
+
const notFoundPage = (res, viewer) => {
|
|
52170
53299
|
res.writeHead(404, WEB_HTML_HEADERS);
|
|
52171
53300
|
res.end(
|
|
52172
|
-
deps.shell("Run not found", {
|
|
53301
|
+
deps.shell(viewer, "Run not found", {
|
|
52173
53302
|
page: "runNotFound",
|
|
52174
53303
|
retentionDays: deps.retention ? deps.retention.retentionDays : null
|
|
52175
53304
|
})
|
|
@@ -52224,6 +53353,9 @@ function createLiveViewHandler(deps) {
|
|
|
52224
53353
|
all,
|
|
52225
53354
|
mine,
|
|
52226
53355
|
...ctx.actor.asUser ? { asUser: ctx.actor.asUser } : {},
|
|
53356
|
+
// A session holding `all` may view the dashboard as a person (record 0053): the
|
|
53357
|
+
// picker offers the requesters this page names; the seed says nothing to anyone else.
|
|
53358
|
+
...holdsAll(ctx.actor) ? { viewAs: { people: requestersOf(page2.rows) } } : {},
|
|
52227
53359
|
retentionDays: deps.retention ? deps.retention.retentionDays : null,
|
|
52228
53360
|
now: now(),
|
|
52229
53361
|
rows: [...page2.rows],
|
|
@@ -52232,7 +53364,7 @@ function createLiveViewHandler(deps) {
|
|
|
52232
53364
|
...page2.olderThan !== void 0 ? { olderThan: page2.olderThan } : {}
|
|
52233
53365
|
};
|
|
52234
53366
|
res.writeHead(200, WEB_HTML_HEADERS);
|
|
52235
|
-
res.end(deps.shell(title, seed));
|
|
53367
|
+
res.end(deps.shell(ctx.actor, title, seed));
|
|
52236
53368
|
};
|
|
52237
53369
|
if (!all) {
|
|
52238
53370
|
run2(
|
|
@@ -52248,7 +53380,7 @@ function createLiveViewHandler(deps) {
|
|
|
52248
53380
|
const scheduled = deps.scheduled;
|
|
52249
53381
|
if (!scheduled) {
|
|
52250
53382
|
res.writeHead(200, WEB_HTML_HEADERS);
|
|
52251
|
-
res.end(deps.shell("Scheduled runs", { page: "scheduled", now: now(), rows: null }));
|
|
53383
|
+
res.end(deps.shell(ctx.actor, "Scheduled runs", { page: "scheduled", now: now(), rows: null }));
|
|
52252
53384
|
return true;
|
|
52253
53385
|
}
|
|
52254
53386
|
const visibleTo = readableRuns(ctx.actor);
|
|
@@ -52262,7 +53394,7 @@ function createLiveViewHandler(deps) {
|
|
|
52262
53394
|
...firings.ok ? {} : { firingsUnavailable: firings.reason }
|
|
52263
53395
|
};
|
|
52264
53396
|
res.writeHead(200, WEB_HTML_HEADERS);
|
|
52265
|
-
res.end(deps.shell("Scheduled runs", seed));
|
|
53397
|
+
res.end(deps.shell(ctx.actor, "Scheduled runs", seed));
|
|
52266
53398
|
};
|
|
52267
53399
|
if (!scheduled.store) {
|
|
52268
53400
|
render3({ ok: false, reason: NO_STORE_REASON });
|
|
@@ -52276,7 +53408,7 @@ function createLiveViewHandler(deps) {
|
|
|
52276
53408
|
run2(res, async () => {
|
|
52277
53409
|
const found = await service.listUnitRuns(route3.key, visibleTo);
|
|
52278
53410
|
if (!found.ok) {
|
|
52279
|
-
notFoundPage(res);
|
|
53411
|
+
notFoundPage(res, ctx.actor);
|
|
52280
53412
|
return;
|
|
52281
53413
|
}
|
|
52282
53414
|
const findings = found.value.pr ? await service.listFindings({ repo: found.value.instance.repo, number: found.value.pr.number }, visibleTo) : void 0;
|
|
@@ -52292,7 +53424,7 @@ function createLiveViewHandler(deps) {
|
|
|
52292
53424
|
retentionDays: deps.retention ? deps.retention.retentionDays : null
|
|
52293
53425
|
};
|
|
52294
53426
|
res.writeHead(200, WEB_HTML_HEADERS);
|
|
52295
|
-
res.end(deps.shell(`Unit ${found.value.id}`, seed));
|
|
53427
|
+
res.end(deps.shell(ctx.actor, `Unit ${found.value.id}`, seed));
|
|
52296
53428
|
});
|
|
52297
53429
|
return true;
|
|
52298
53430
|
}
|
|
@@ -52307,7 +53439,7 @@ function createLiveViewHandler(deps) {
|
|
|
52307
53439
|
});
|
|
52308
53440
|
res.writeHead(200, WEB_HTML_HEADERS);
|
|
52309
53441
|
res.end(
|
|
52310
|
-
deps.shell("Live run", {
|
|
53442
|
+
deps.shell(ctx.actor, "Live run", {
|
|
52311
53443
|
page: "run",
|
|
52312
53444
|
mode: "live",
|
|
52313
53445
|
id: route3.id,
|
|
@@ -52349,11 +53481,12 @@ function createLiveViewHandler(deps) {
|
|
|
52349
53481
|
finished: snap.finished,
|
|
52350
53482
|
window: { start: snap.receivedAt ?? snap.startedAt, end: snap.finishedAt ?? now() }
|
|
52351
53483
|
});
|
|
52352
|
-
res.writeHead(200,
|
|
53484
|
+
res.writeHead(200, JSON_NO_STORE2);
|
|
52353
53485
|
res.end(JSON.stringify({ id: route3.id, finished: snap.finished, diagnosis }));
|
|
52354
53486
|
return true;
|
|
52355
53487
|
}
|
|
52356
53488
|
if (route3.kind === "stop") {
|
|
53489
|
+
if (ctx.actor.viewingAs) return refuseWhileViewing(res, ctx.actor.viewingAs);
|
|
52357
53490
|
const mode = parseStopMode(url.searchParams.get("mode"));
|
|
52358
53491
|
if (!mode) {
|
|
52359
53492
|
text(res, 400, "mode must be soft or hard");
|
|
@@ -52365,7 +53498,7 @@ function createLiveViewHandler(deps) {
|
|
|
52365
53498
|
else text(res, 404, NOT_FOUND2);
|
|
52366
53499
|
return true;
|
|
52367
53500
|
}
|
|
52368
|
-
res.writeHead(200,
|
|
53501
|
+
res.writeHead(200, JSON_NO_STORE2);
|
|
52369
53502
|
res.end(JSON.stringify({ id: route3.id, mode: result.mode, state: "stopping" }));
|
|
52370
53503
|
return true;
|
|
52371
53504
|
}
|
|
@@ -52380,6 +53513,7 @@ function createLiveViewHandler(deps) {
|
|
|
52380
53513
|
const actor = ctx.actor;
|
|
52381
53514
|
const servable = (view) => view.finished || view.ownerGen !== void 0;
|
|
52382
53515
|
if (route3.kind === "stop") {
|
|
53516
|
+
if (ctx.actor.viewingAs) return refuseWhileViewing(res, ctx.actor.viewingAs);
|
|
52383
53517
|
const mode = parseStopMode(url.searchParams.get("mode"));
|
|
52384
53518
|
if (!mode) {
|
|
52385
53519
|
text(res, 400, "mode must be soft or hard");
|
|
@@ -52401,7 +53535,7 @@ function createLiveViewHandler(deps) {
|
|
|
52401
53535
|
else text(res, 404, NOT_FOUND2);
|
|
52402
53536
|
return;
|
|
52403
53537
|
}
|
|
52404
|
-
res.writeHead(200,
|
|
53538
|
+
res.writeHead(200, JSON_NO_STORE2);
|
|
52405
53539
|
res.end(JSON.stringify(stopped.value));
|
|
52406
53540
|
});
|
|
52407
53541
|
return true;
|
|
@@ -52414,7 +53548,7 @@ function createLiveViewHandler(deps) {
|
|
|
52414
53548
|
text(res, 404, NOT_FOUND2);
|
|
52415
53549
|
return;
|
|
52416
53550
|
}
|
|
52417
|
-
res.writeHead(200,
|
|
53551
|
+
res.writeHead(200, JSON_NO_STORE2);
|
|
52418
53552
|
res.end(JSON.stringify(friction.value));
|
|
52419
53553
|
});
|
|
52420
53554
|
return true;
|
|
@@ -52428,7 +53562,7 @@ function createLiveViewHandler(deps) {
|
|
|
52428
53562
|
sink.write(NOT_FOUND2);
|
|
52429
53563
|
sink.end();
|
|
52430
53564
|
} else if (route3.kind === "page") {
|
|
52431
|
-
notFoundPage(res);
|
|
53565
|
+
notFoundPage(res, ctx.actor);
|
|
52432
53566
|
} else text(res, 404, NOT_FOUND2);
|
|
52433
53567
|
return;
|
|
52434
53568
|
}
|
|
@@ -52456,7 +53590,7 @@ function createLiveViewHandler(deps) {
|
|
|
52456
53590
|
const tokens = liveTokens();
|
|
52457
53591
|
res.writeHead(200, WEB_HTML_HEADERS);
|
|
52458
53592
|
res.end(
|
|
52459
|
-
deps.shell("Run", {
|
|
53593
|
+
deps.shell(ctx.actor, "Run", {
|
|
52460
53594
|
page: "run",
|
|
52461
53595
|
mode: "history",
|
|
52462
53596
|
id: route3.id,
|
|
@@ -52492,7 +53626,7 @@ function createLiveViewHandler(deps) {
|
|
|
52492
53626
|
return true;
|
|
52493
53627
|
};
|
|
52494
53628
|
}
|
|
52495
|
-
var RESERVED_IDS, INDEX_PAGE_SIZE, RUNS_GET, RUNS_STOP, NOT_FOUND2, TEXT,
|
|
53629
|
+
var RESERVED_IDS, INDEX_PAGE_SIZE, RUNS_GET, RUNS_STOP, NOT_FOUND2, TEXT, JSON_NO_STORE2, NO_STORE_REASON;
|
|
52496
53630
|
var init_liveView = __esm({
|
|
52497
53631
|
"../../src/channels/liveView.ts"() {
|
|
52498
53632
|
"use strict";
|
|
@@ -52508,19 +53642,21 @@ var init_liveView = __esm({
|
|
|
52508
53642
|
init_runsService();
|
|
52509
53643
|
init_commandRegistry();
|
|
52510
53644
|
init_scheduledPanel();
|
|
53645
|
+
init_viewAs();
|
|
53646
|
+
init_viewAs2();
|
|
52511
53647
|
init_webShell();
|
|
52512
53648
|
init_favicon();
|
|
52513
53649
|
init_sse();
|
|
52514
53650
|
init_html();
|
|
52515
53651
|
init_sse();
|
|
52516
53652
|
init_webSeed();
|
|
52517
|
-
RESERVED_IDS = /* @__PURE__ */ new Set(["scheduled", "artifacts", "unit"]);
|
|
53653
|
+
RESERVED_IDS = /* @__PURE__ */ new Set(["scheduled", "artifacts", "unit", "view-as"]);
|
|
52518
53654
|
INDEX_PAGE_SIZE = 25;
|
|
52519
53655
|
RUNS_GET = { type: "command", id: "runs.get" };
|
|
52520
53656
|
RUNS_STOP = { type: "command", id: "runs.stop" };
|
|
52521
53657
|
NOT_FOUND2 = "run not found";
|
|
52522
53658
|
TEXT = { "content-type": "text/plain; charset=utf-8" };
|
|
52523
|
-
|
|
53659
|
+
JSON_NO_STORE2 = { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" };
|
|
52524
53660
|
NO_STORE_REASON = "schedules.worker is not configured";
|
|
52525
53661
|
}
|
|
52526
53662
|
});
|
|
@@ -52792,7 +53928,7 @@ function createResidentsViewHandler(deps) {
|
|
|
52792
53928
|
runs: live
|
|
52793
53929
|
};
|
|
52794
53930
|
res.writeHead(200, WEB_HTML_HEADERS);
|
|
52795
|
-
res.end(shell("Resident repos", seed));
|
|
53931
|
+
res.end(shell(ctx.actor, "Resident repos", seed));
|
|
52796
53932
|
finish2(200);
|
|
52797
53933
|
return;
|
|
52798
53934
|
}
|
|
@@ -52802,7 +53938,7 @@ function createResidentsViewHandler(deps) {
|
|
|
52802
53938
|
return;
|
|
52803
53939
|
}
|
|
52804
53940
|
res.writeHead(200, WEB_HTML_HEADERS);
|
|
52805
|
-
res.end(shell(route3.slug, { page: "resident", slug: route3.slug, record: record2 }));
|
|
53941
|
+
res.end(shell(ctx.actor, route3.slug, { page: "resident", slug: route3.slug, record: record2 }));
|
|
52806
53942
|
finish2(200);
|
|
52807
53943
|
});
|
|
52808
53944
|
return true;
|
|
@@ -52827,368 +53963,6 @@ var init_residentsView = __esm({
|
|
|
52827
53963
|
}
|
|
52828
53964
|
});
|
|
52829
53965
|
|
|
52830
|
-
// ../../src/channels/accessAuth.ts
|
|
52831
|
-
import { createPublicKey, verify as cryptoVerify } from "node:crypto";
|
|
52832
|
-
function isServiceToken(identity2) {
|
|
52833
|
-
return identity2.sub === "" && typeof identity2.commonName === "string" && identity2.commonName !== "";
|
|
52834
|
-
}
|
|
52835
|
-
function decodeBase64Url(part) {
|
|
52836
|
-
if (part === "" || /[^A-Za-z0-9_-]/.test(part)) return null;
|
|
52837
|
-
return Buffer.from(part, "base64url");
|
|
52838
|
-
}
|
|
52839
|
-
function decodeJsonSegment(part) {
|
|
52840
|
-
const buf2 = decodeBase64Url(part);
|
|
52841
|
-
if (!buf2) return null;
|
|
52842
|
-
try {
|
|
52843
|
-
const parsed = JSON.parse(buf2.toString("utf8"));
|
|
52844
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
52845
|
-
return parsed;
|
|
52846
|
-
} catch {
|
|
52847
|
-
return null;
|
|
52848
|
-
}
|
|
52849
|
-
}
|
|
52850
|
-
async function resolveKey(kid, config, deps) {
|
|
52851
|
-
const cache2 = deps.cache ?? new JwksCache();
|
|
52852
|
-
const cached = cache2.get(kid, deps.now());
|
|
52853
|
-
if (cached) return cached;
|
|
52854
|
-
const ttlMs = (deps.ttlSeconds ?? DEFAULT_JWKS_TTL_SECONDS) * 1e3;
|
|
52855
|
-
const minRefetchIntervalMs = deps.minRefetchIntervalMs ?? DEFAULT_MIN_REFETCH_INTERVAL_MS;
|
|
52856
|
-
const certsUrl = `https://${config.teamDomain}${CERTS_PATH}`;
|
|
52857
|
-
await cache2.refresh(() => deps.fetchJwks(certsUrl), deps.now, ttlMs, minRefetchIntervalMs);
|
|
52858
|
-
return cache2.get(kid, deps.now());
|
|
52859
|
-
}
|
|
52860
|
-
function claimsValid(claims, config, now) {
|
|
52861
|
-
const nowSec = Math.floor(now / 1e3);
|
|
52862
|
-
if (claims.iss !== `https://${config.teamDomain}`) return false;
|
|
52863
|
-
const aud = claims.aud;
|
|
52864
|
-
const audOk = typeof aud === "string" ? aud === config.aud : Array.isArray(aud) && aud.includes(config.aud);
|
|
52865
|
-
if (!audOk) return false;
|
|
52866
|
-
if (typeof claims.exp !== "number" || claims.exp <= nowSec) return false;
|
|
52867
|
-
if (typeof claims.nbf === "number" && claims.nbf > nowSec + SKEW_SECONDS) return false;
|
|
52868
|
-
if (typeof claims.iat === "number" && claims.iat > nowSec + SKEW_SECONDS) return false;
|
|
52869
|
-
return hasSubject(claims);
|
|
52870
|
-
}
|
|
52871
|
-
function hasSubject(claims) {
|
|
52872
|
-
if (typeof claims.sub === "string" && claims.sub !== "") return true;
|
|
52873
|
-
const subEmpty = claims.sub === void 0 || claims.sub === "";
|
|
52874
|
-
return subEmpty && typeof claims.common_name === "string" && claims.common_name !== "";
|
|
52875
|
-
}
|
|
52876
|
-
async function verifyAccessJwt(token2, config, deps) {
|
|
52877
|
-
if (typeof token2 !== "string") return null;
|
|
52878
|
-
const parts = token2.split(".");
|
|
52879
|
-
if (parts.length !== 3) return null;
|
|
52880
|
-
const [rawHeader, rawPayload, rawSignature] = parts;
|
|
52881
|
-
const header4 = decodeJsonSegment(rawHeader);
|
|
52882
|
-
const payload = decodeJsonSegment(rawPayload);
|
|
52883
|
-
if (!header4 || !payload) return null;
|
|
52884
|
-
if (header4.alg !== "RS256") return null;
|
|
52885
|
-
if (typeof header4.kid !== "string" || header4.kid === "") return null;
|
|
52886
|
-
const signature = decodeBase64Url(rawSignature);
|
|
52887
|
-
if (!signature) return null;
|
|
52888
|
-
const jwk = await resolveKey(header4.kid, config, deps);
|
|
52889
|
-
if (!jwk) return null;
|
|
52890
|
-
let signatureOk;
|
|
52891
|
-
try {
|
|
52892
|
-
const key = createPublicKey({ format: "jwk", key: jwk });
|
|
52893
|
-
signatureOk = cryptoVerify("RSA-SHA256", Buffer.from(`${rawHeader}.${rawPayload}`), key, signature);
|
|
52894
|
-
} catch {
|
|
52895
|
-
return null;
|
|
52896
|
-
}
|
|
52897
|
-
if (!signatureOk) return null;
|
|
52898
|
-
if (!claimsValid(payload, config, deps.now())) return null;
|
|
52899
|
-
if (typeof payload.sub !== "string" || payload.sub === "") {
|
|
52900
|
-
return { sub: "", commonName: payload.common_name };
|
|
52901
|
-
}
|
|
52902
|
-
return {
|
|
52903
|
-
sub: payload.sub,
|
|
52904
|
-
email: typeof payload.email === "string" ? payload.email : void 0
|
|
52905
|
-
};
|
|
52906
|
-
}
|
|
52907
|
-
function normalizeTeamDomain(raw) {
|
|
52908
|
-
return raw.trim().replace(/^https?:\/\//i, "").replace(/\/+$/, "");
|
|
52909
|
-
}
|
|
52910
|
-
function parseAccessConfig(env) {
|
|
52911
|
-
const teamDomain = normalizeTeamDomain(env.ACCESS_TEAM_DOMAIN ?? "");
|
|
52912
|
-
const aud = (env.ACCESS_AUD ?? "").trim();
|
|
52913
|
-
if (teamDomain === "" || aud === "") return null;
|
|
52914
|
-
return { teamDomain, aud };
|
|
52915
|
-
}
|
|
52916
|
-
var CERTS_PATH, DEFAULT_JWKS_TTL_SECONDS, DEFAULT_MIN_REFETCH_INTERVAL_MS, SKEW_SECONDS, httpJwksFetcher, JwksCache;
|
|
52917
|
-
var init_accessAuth = __esm({
|
|
52918
|
-
"../../src/channels/accessAuth.ts"() {
|
|
52919
|
-
"use strict";
|
|
52920
|
-
CERTS_PATH = "/cdn-cgi/access/certs";
|
|
52921
|
-
DEFAULT_JWKS_TTL_SECONDS = 3600;
|
|
52922
|
-
DEFAULT_MIN_REFETCH_INTERVAL_MS = 3e4;
|
|
52923
|
-
SKEW_SECONDS = 60;
|
|
52924
|
-
httpJwksFetcher = async (certsUrl) => {
|
|
52925
|
-
const res = await fetch(certsUrl);
|
|
52926
|
-
if (!res.ok) throw new Error(`JWKS fetch failed: HTTP ${res.status}`);
|
|
52927
|
-
const body = await res.json();
|
|
52928
|
-
const keys = body?.keys;
|
|
52929
|
-
if (!Array.isArray(keys)) throw new Error("JWKS response missing keys[]");
|
|
52930
|
-
return keys;
|
|
52931
|
-
};
|
|
52932
|
-
JwksCache = class {
|
|
52933
|
-
entries = /* @__PURE__ */ new Map();
|
|
52934
|
-
/** Ms of the last SUCCESSFUL JWKS fetch, or null before the first one. Drives
|
|
52935
|
-
* the negative-cache interval in `refresh`; a failed fetch does not set it. */
|
|
52936
|
-
lastFetchAt = null;
|
|
52937
|
-
/** The in-flight refresh shared by concurrent callers (single-flight); null
|
|
52938
|
-
* when no fetch is running. It never rejects — `runFetch` swallows failures to
|
|
52939
|
-
* fail closed — so both the owner and any joiners resolve cleanly. */
|
|
52940
|
-
inFlight = null;
|
|
52941
|
-
/** The fresh JWK for `kid`, or null if absent/expired (expired entries are
|
|
52942
|
-
* evicted lazily). `now` is milliseconds. */
|
|
52943
|
-
get(kid, now) {
|
|
52944
|
-
const hit = this.entries.get(kid);
|
|
52945
|
-
if (!hit) return null;
|
|
52946
|
-
if (hit.expiresAt <= now) {
|
|
52947
|
-
this.entries.delete(kid);
|
|
52948
|
-
return null;
|
|
52949
|
-
}
|
|
52950
|
-
return hit.jwk;
|
|
52951
|
-
}
|
|
52952
|
-
/** Store every kid'd key from a freshly fetched JWKS with the given absolute
|
|
52953
|
-
* expiry (ms). Keyless entries are skipped. */
|
|
52954
|
-
put(keys, expiresAt) {
|
|
52955
|
-
for (const jwk of keys) {
|
|
52956
|
-
if (typeof jwk.kid === "string" && jwk.kid !== "") {
|
|
52957
|
-
this.entries.set(jwk.kid, { jwk, expiresAt });
|
|
52958
|
-
}
|
|
52959
|
-
}
|
|
52960
|
-
}
|
|
52961
|
-
/**
|
|
52962
|
-
* Refresh the JWKS to (try to) learn an unknown kid, with two guards against
|
|
52963
|
-
* the unknown-kid amplification vector:
|
|
52964
|
-
* - single-flight: if a refresh is already running, join it instead of
|
|
52965
|
-
* firing another — N concurrent unknown kids ⇒ at most one `fetch` call.
|
|
52966
|
-
* - negative cache: once a fetch has succeeded, another is suppressed until
|
|
52967
|
-
* `minRefetchIntervalMs` has elapsed, so an unknown kid resolves to null
|
|
52968
|
-
* WITHOUT a network round-trip inside that window. A genuinely rotated kid
|
|
52969
|
-
* is still picked up on the first refresh after the interval.
|
|
52970
|
-
* Fail-closed: a fetch failure leaves the store unchanged and does not advance
|
|
52971
|
-
* the last-fetch time. `now` is read lazily (ms) so the injected clock can move
|
|
52972
|
-
* across the await.
|
|
52973
|
-
*/
|
|
52974
|
-
async refresh(fetch2, now, ttlMs, minRefetchIntervalMs) {
|
|
52975
|
-
if (this.inFlight) {
|
|
52976
|
-
await this.inFlight;
|
|
52977
|
-
return;
|
|
52978
|
-
}
|
|
52979
|
-
if (this.lastFetchAt !== null && now() - this.lastFetchAt < minRefetchIntervalMs) {
|
|
52980
|
-
return;
|
|
52981
|
-
}
|
|
52982
|
-
this.inFlight = this.runFetch(fetch2, now, ttlMs);
|
|
52983
|
-
try {
|
|
52984
|
-
await this.inFlight;
|
|
52985
|
-
} finally {
|
|
52986
|
-
this.inFlight = null;
|
|
52987
|
-
}
|
|
52988
|
-
}
|
|
52989
|
-
/** Perform one JWKS fetch and fold the result into the store. Never rejects:
|
|
52990
|
-
* on any failure the store is left unchanged (fail closed) and `lastFetchAt`
|
|
52991
|
-
* is not advanced, so `resolveKey` returns null and a later verify may retry. */
|
|
52992
|
-
async runFetch(fetch2, now, ttlMs) {
|
|
52993
|
-
try {
|
|
52994
|
-
const keys = await fetch2();
|
|
52995
|
-
const fetchedAt = now();
|
|
52996
|
-
this.put(keys, fetchedAt + ttlMs);
|
|
52997
|
-
this.lastFetchAt = fetchedAt;
|
|
52998
|
-
} catch {
|
|
52999
|
-
}
|
|
53000
|
-
}
|
|
53001
|
-
};
|
|
53002
|
-
}
|
|
53003
|
-
});
|
|
53004
|
-
|
|
53005
|
-
// ../../src/channels/commandHttp.ts
|
|
53006
|
-
function normalizePath2(pathname) {
|
|
53007
|
-
let decoded = pathname;
|
|
53008
|
-
try {
|
|
53009
|
-
decoded = decodeURIComponent(pathname);
|
|
53010
|
-
} catch {
|
|
53011
|
-
}
|
|
53012
|
-
return decoded.replace(/\/{2,}/g, "/");
|
|
53013
|
-
}
|
|
53014
|
-
function isCommandPath(pathname) {
|
|
53015
|
-
const p = normalizePath2(pathname).toLowerCase();
|
|
53016
|
-
return p === API_ROOT || p === `${API_ROOT}/` || p.startsWith(`${API_ROOT}/`);
|
|
53017
|
-
}
|
|
53018
|
-
function commandIdFromPath(pathname) {
|
|
53019
|
-
const p = normalizePath2(pathname);
|
|
53020
|
-
if (!p.startsWith(`${API_ROOT}/`)) return void 0;
|
|
53021
|
-
const rest = p.slice(API_ROOT.length + 1);
|
|
53022
|
-
return COMMAND_ID.test(rest) ? rest : void 0;
|
|
53023
|
-
}
|
|
53024
|
-
function callerIdFor(identity2) {
|
|
53025
|
-
return isServiceToken(identity2) ? `access:svc:${identity2.commonName}` : `access:${identity2.sub}`;
|
|
53026
|
-
}
|
|
53027
|
-
function accessActor(identity2, grantsFor) {
|
|
53028
|
-
return isServiceToken(identity2) ? resolveActor({ surface: "access-service", subjectId: identity2.commonName }, grantsFor) : resolveActor({ surface: "access-browser", subjectId: identity2.sub }, grantsFor);
|
|
53029
|
-
}
|
|
53030
|
-
function serviceTokenAllowed(pathname, identity2) {
|
|
53031
|
-
return !isServiceToken(identity2) || isCommandPath(pathname);
|
|
53032
|
-
}
|
|
53033
|
-
async function resolveAccessActor(identity2, opts) {
|
|
53034
|
-
const actor = accessActor(identity2, opts.grantsFor);
|
|
53035
|
-
if (isServiceToken(identity2) || !identity2.email || !opts.personByEmail) return actor;
|
|
53036
|
-
const person = await opts.personByEmail(identity2.email).catch(() => void 0);
|
|
53037
|
-
if (!person || !person.id.startsWith("slack:")) return actor;
|
|
53038
|
-
const memberOf = opts.channelsOf ? await boundedChannels(opts.channelsOf(person.id), opts.channelsOfTimeoutMs ?? CHANNEL_DIRECTORY_TIMEOUT_MS) : "unknown";
|
|
53039
|
-
return {
|
|
53040
|
-
...actor,
|
|
53041
|
-
self: [actor.id, person.id],
|
|
53042
|
-
asUser: { id: person.id, ...person.name ? { name: person.name } : {} },
|
|
53043
|
-
...memberOf === "unknown" ? {} : { memberOf }
|
|
53044
|
-
};
|
|
53045
|
-
}
|
|
53046
|
-
async function boundedChannels(lookup2, timeoutMs) {
|
|
53047
|
-
let timer;
|
|
53048
|
-
try {
|
|
53049
|
-
const answer = await Promise.race([
|
|
53050
|
-
lookup2.catch(() => "unknown"),
|
|
53051
|
-
new Promise((resolve13) => {
|
|
53052
|
-
timer = setTimeout(() => resolve13(LOOKUP_TIMED_OUT), timeoutMs);
|
|
53053
|
-
})
|
|
53054
|
-
]);
|
|
53055
|
-
return answer === LOOKUP_TIMED_OUT ? "unknown" : answer;
|
|
53056
|
-
} finally {
|
|
53057
|
-
if (timer) clearTimeout(timer);
|
|
53058
|
-
}
|
|
53059
|
-
}
|
|
53060
|
-
async function callerFor(identity2, opts) {
|
|
53061
|
-
return {
|
|
53062
|
-
kind: "access",
|
|
53063
|
-
id: callerIdFor(identity2),
|
|
53064
|
-
actor: await resolveAccessActor(identity2, opts),
|
|
53065
|
-
...identity2.email && !isServiceToken(identity2) ? { email: identity2.email } : {}
|
|
53066
|
-
};
|
|
53067
|
-
}
|
|
53068
|
-
function hostOf(url) {
|
|
53069
|
-
if (!url) return void 0;
|
|
53070
|
-
try {
|
|
53071
|
-
return new URL(url).host.toLowerCase();
|
|
53072
|
-
} catch {
|
|
53073
|
-
return void 0;
|
|
53074
|
-
}
|
|
53075
|
-
}
|
|
53076
|
-
function header2(req, name) {
|
|
53077
|
-
const v = req.headers[name];
|
|
53078
|
-
return Array.isArray(v) ? v[0] : v;
|
|
53079
|
-
}
|
|
53080
|
-
function originAllowed(req, opts) {
|
|
53081
|
-
const site = header2(req, "sec-fetch-site");
|
|
53082
|
-
if (site !== void 0 && site !== "same-origin" && site !== "none") return false;
|
|
53083
|
-
const origin = header2(req, "origin");
|
|
53084
|
-
if (origin === void 0) return true;
|
|
53085
|
-
const expected = originOf(opts.publicBaseUrl);
|
|
53086
|
-
if (expected !== void 0) return originOf(origin) === expected;
|
|
53087
|
-
const host = header2(req, "host")?.toLowerCase();
|
|
53088
|
-
const actual = hostOf(origin);
|
|
53089
|
-
return host !== void 0 && actual !== void 0 && actual === host;
|
|
53090
|
-
}
|
|
53091
|
-
function originOf(url) {
|
|
53092
|
-
if (!url) return void 0;
|
|
53093
|
-
try {
|
|
53094
|
-
const o = new URL(url).origin;
|
|
53095
|
-
return o === "null" ? void 0 : o.toLowerCase();
|
|
53096
|
-
} catch {
|
|
53097
|
-
return void 0;
|
|
53098
|
-
}
|
|
53099
|
-
}
|
|
53100
|
-
function isJsonContentType(req) {
|
|
53101
|
-
const ct = header2(req, "content-type");
|
|
53102
|
-
return ct !== void 0 && ct.split(";")[0].trim().toLowerCase() === "application/json";
|
|
53103
|
-
}
|
|
53104
|
-
function createCommandHttpHandler(commands, opts) {
|
|
53105
|
-
const maxBytes = opts.maxBodyBytes ?? MAX_BODY_BYTES2;
|
|
53106
|
-
const send = (res, status3, body, extra = {}) => {
|
|
53107
|
-
res.writeHead(status3, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", ...extra });
|
|
53108
|
-
res.end(JSON.stringify(body));
|
|
53109
|
-
};
|
|
53110
|
-
const refuse3 = (res, status3, code, error, extra = {}) => send(res, status3, { error, code }, extra);
|
|
53111
|
-
return async (req, res, identity2) => {
|
|
53112
|
-
const url = new URL(req.url ?? "/", "http://placeholder.invalid");
|
|
53113
|
-
const id = commandIdFromPath(url.pathname);
|
|
53114
|
-
const cmd = id === void 0 ? void 0 : commands.get(id);
|
|
53115
|
-
if (!cmd || !CommandRegistry.exposedTo(cmd, "access")) {
|
|
53116
|
-
refuse3(res, 404, "not_found", "unknown command");
|
|
53117
|
-
return;
|
|
53118
|
-
}
|
|
53119
|
-
const method = (req.method ?? "GET").toUpperCase();
|
|
53120
|
-
const allow = cmd.effect === "write" ? "POST" : "GET, POST";
|
|
53121
|
-
if (method !== "POST" && (cmd.effect === "write" || method !== "GET")) {
|
|
53122
|
-
refuse3(res, 405, "method_not_allowed", `${cmd.id} accepts ${allow}`, { allow });
|
|
53123
|
-
return;
|
|
53124
|
-
}
|
|
53125
|
-
if (method === "POST" && !isJsonContentType(req)) {
|
|
53126
|
-
refuse3(res, 415, "unsupported_media_type", "content-type must be application/json");
|
|
53127
|
-
return;
|
|
53128
|
-
}
|
|
53129
|
-
if (cmd.effect === "write" && !originAllowed(req, opts)) {
|
|
53130
|
-
refuse3(res, 403, "forbidden_origin", "cross-origin writes are refused");
|
|
53131
|
-
return;
|
|
53132
|
-
}
|
|
53133
|
-
const caller = await callerFor(identity2, opts);
|
|
53134
|
-
if (CommandRegistry.refuses(cmd, caller)) {
|
|
53135
|
-
refuse3(res, ERROR_STATUS.unauthorized, "unauthorized", `${caller.id} is not allowed to run ${cmd.id}`);
|
|
53136
|
-
return;
|
|
53137
|
-
}
|
|
53138
|
-
let named;
|
|
53139
|
-
let keys;
|
|
53140
|
-
if (method === "GET") {
|
|
53141
|
-
named = Object.fromEntries(url.searchParams);
|
|
53142
|
-
keys = "kebab";
|
|
53143
|
-
} else {
|
|
53144
|
-
const read = await readBody(req, maxBytes);
|
|
53145
|
-
if (!read.ok) {
|
|
53146
|
-
refuse3(res, 413, "payload_too_large", "request body too large");
|
|
53147
|
-
req.destroy();
|
|
53148
|
-
return;
|
|
53149
|
-
}
|
|
53150
|
-
let body;
|
|
53151
|
-
try {
|
|
53152
|
-
body = read.body.trim() === "" ? {} : JSON.parse(read.body);
|
|
53153
|
-
} catch {
|
|
53154
|
-
refuse3(res, ERROR_STATUS.invalid_input, "invalid_input", "body must be a JSON object");
|
|
53155
|
-
return;
|
|
53156
|
-
}
|
|
53157
|
-
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
53158
|
-
refuse3(res, ERROR_STATUS.invalid_input, "invalid_input", "body must be a JSON object");
|
|
53159
|
-
return;
|
|
53160
|
-
}
|
|
53161
|
-
named = body;
|
|
53162
|
-
keys = "camel";
|
|
53163
|
-
}
|
|
53164
|
-
const input = namedToInput(cmd, named, keys);
|
|
53165
|
-
if ("error" in input) {
|
|
53166
|
-
refuse3(res, ERROR_STATUS.invalid_input, "invalid_input", input.error);
|
|
53167
|
-
return;
|
|
53168
|
-
}
|
|
53169
|
-
const result = await commands.invoke(cmd.id, input, caller);
|
|
53170
|
-
if (!result.ok) {
|
|
53171
|
-
refuse3(res, result.status, result.error, result.message);
|
|
53172
|
-
return;
|
|
53173
|
-
}
|
|
53174
|
-
send(res, 200, result.value);
|
|
53175
|
-
};
|
|
53176
|
-
}
|
|
53177
|
-
var API_ROOT, LOOKUP_TIMED_OUT;
|
|
53178
|
-
var init_commandHttp = __esm({
|
|
53179
|
-
"../../src/channels/commandHttp.ts"() {
|
|
53180
|
-
"use strict";
|
|
53181
|
-
init_actor();
|
|
53182
|
-
init_commandRegistry();
|
|
53183
|
-
init_commandSurface();
|
|
53184
|
-
init_record();
|
|
53185
|
-
init_accessAuth();
|
|
53186
|
-
init_http();
|
|
53187
|
-
API_ROOT = "/api";
|
|
53188
|
-
LOOKUP_TIMED_OUT = /* @__PURE__ */ Symbol("channelsOf timed out");
|
|
53189
|
-
}
|
|
53190
|
-
});
|
|
53191
|
-
|
|
53192
53966
|
// ../../src/channels/web.ts
|
|
53193
53967
|
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
53194
53968
|
function parseThreadsRoute(pathname) {
|
|
@@ -53428,6 +54202,11 @@ function createWebChatHandler(deps) {
|
|
|
53428
54202
|
req.destroy();
|
|
53429
54203
|
return;
|
|
53430
54204
|
}
|
|
54205
|
+
if (ctx.actor.viewingAs) {
|
|
54206
|
+
answer(403, { error: "unauthorized", message: viewingRefusal(ctx.actor.viewingAs) });
|
|
54207
|
+
req.destroy();
|
|
54208
|
+
return;
|
|
54209
|
+
}
|
|
53431
54210
|
const sub = subOf(ctx.actor);
|
|
53432
54211
|
const threadKey = threadKeyFor(sub, id);
|
|
53433
54212
|
if (!ownLane(sub, threadKey)) {
|
|
@@ -53512,7 +54291,7 @@ function createWebChatHandler(deps) {
|
|
|
53512
54291
|
}
|
|
53513
54292
|
const render3 = (title, seed) => {
|
|
53514
54293
|
res.writeHead(200, WEB_HTML_HEADERS);
|
|
53515
|
-
res.end(deps.shell(title, seed));
|
|
54294
|
+
res.end(deps.shell(ctx.actor, title, seed));
|
|
53516
54295
|
};
|
|
53517
54296
|
const sub = subOf(ctx.actor);
|
|
53518
54297
|
if (route3.kind === "new") {
|
|
@@ -53524,7 +54303,7 @@ function createWebChatHandler(deps) {
|
|
|
53524
54303
|
turnsOf(threadKey, ctx.actor).then(async (open2) => {
|
|
53525
54304
|
if (open2.runs.length === 0 && !ownLane(sub, threadKey)) {
|
|
53526
54305
|
res.writeHead(404, WEB_HTML_HEADERS);
|
|
53527
|
-
res.end(deps.shell("Run not found", { page: "runNotFound", retentionDays }));
|
|
54306
|
+
res.end(deps.shell(ctx.actor, "Run not found", { page: "runNotFound", retentionDays }));
|
|
53528
54307
|
return;
|
|
53529
54308
|
}
|
|
53530
54309
|
const seed = await seedFor(ctx, route3.id, threadKey, open2);
|
|
@@ -53549,6 +54328,7 @@ var init_web2 = __esm({
|
|
|
53549
54328
|
init_http();
|
|
53550
54329
|
init_viewer();
|
|
53551
54330
|
init_webShell();
|
|
54331
|
+
init_viewAs();
|
|
53552
54332
|
PLATFORM5 = "web";
|
|
53553
54333
|
CONVERSATION_ID_BYTES = 10;
|
|
53554
54334
|
CONVERSATION_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
@@ -53681,7 +54461,7 @@ function createCostsViewHandler(service, shell, opts = {}) {
|
|
|
53681
54461
|
Promise.all([orNone(service.report(group, days)), by]).then(([report2, byReport2]) => {
|
|
53682
54462
|
res.writeHead(200, WEB_HTML_HEADERS);
|
|
53683
54463
|
res.end(
|
|
53684
|
-
shell(`${report2?.label ?? group} spend`, {
|
|
54464
|
+
shell(ctx.actor, `${report2?.label ?? group} spend`, {
|
|
53685
54465
|
page: "costs",
|
|
53686
54466
|
group,
|
|
53687
54467
|
report: report2,
|
|
@@ -53786,7 +54566,7 @@ function createDeliveryViewHandler(deps, shell) {
|
|
|
53786
54566
|
res.end(body);
|
|
53787
54567
|
return;
|
|
53788
54568
|
}
|
|
53789
|
-
const html = shell(`${report2.repo} delivery`, { page: "delivery", report: report2, repos });
|
|
54569
|
+
const html = shell(ctx.actor, `${report2.repo} delivery`, { page: "delivery", report: report2, repos });
|
|
53790
54570
|
res.writeHead(200, WEB_HTML_HEADERS);
|
|
53791
54571
|
res.end(html);
|
|
53792
54572
|
}).catch((err2) => {
|
|
@@ -53963,9 +54743,9 @@ function createSettingsViewHandler(deps, shell) {
|
|
|
53963
54743
|
}
|
|
53964
54744
|
plain4(res, 502, `settings unavailable: ${reason2}`);
|
|
53965
54745
|
};
|
|
53966
|
-
const render3 = (seed) => {
|
|
54746
|
+
const render3 = (viewer, seed) => {
|
|
53967
54747
|
res.writeHead(200, WEB_HTML_HEADERS);
|
|
53968
|
-
res.end(shell("Settings", seed));
|
|
54748
|
+
res.end(shell(viewer, "Settings", seed));
|
|
53969
54749
|
};
|
|
53970
54750
|
const channel = "channel" in route3 ? route3.channel : void 0;
|
|
53971
54751
|
deps.callerFor(ctx.identity).then(async (caller) => {
|
|
@@ -53981,11 +54761,12 @@ function createSettingsViewHandler(deps, shell) {
|
|
|
53981
54761
|
plain4(res, 403, "forbidden");
|
|
53982
54762
|
return;
|
|
53983
54763
|
}
|
|
53984
|
-
render3({ ...base, installation: deps.installation() });
|
|
54764
|
+
render3(caller.actor, { ...base, installation: deps.installation() });
|
|
53985
54765
|
return;
|
|
53986
54766
|
}
|
|
53987
54767
|
const part = tab === "mcps" ? await mcpsSeed(caller, channel) : await channelsSeed(caller, channel);
|
|
53988
54768
|
render3(
|
|
54769
|
+
caller.actor,
|
|
53989
54770
|
tab === "mcps" ? { ...base, mcps: part } : { ...base, channels: part }
|
|
53990
54771
|
);
|
|
53991
54772
|
}).catch(failed);
|
|
@@ -55148,7 +55929,7 @@ function handleAdminModelProxyBearer(req, res, deps) {
|
|
|
55148
55929
|
req.destroy();
|
|
55149
55930
|
return;
|
|
55150
55931
|
}
|
|
55151
|
-
const read = await readBody(req,
|
|
55932
|
+
const read = await readBody(req, MAX_BODY_BYTES4);
|
|
55152
55933
|
if (!read.ok) {
|
|
55153
55934
|
json3(413, { ok: false, error: "request body too large" });
|
|
55154
55935
|
return;
|
|
@@ -55189,7 +55970,7 @@ function handleAdminModelProxyBearer(req, res, deps) {
|
|
|
55189
55970
|
});
|
|
55190
55971
|
})();
|
|
55191
55972
|
}
|
|
55192
|
-
var MODEL_PROXY_BEARER_PATH, MODEL_PROXY_BEARER_SCOPE,
|
|
55973
|
+
var MODEL_PROXY_BEARER_PATH, MODEL_PROXY_BEARER_SCOPE, MAX_BODY_BYTES4;
|
|
55193
55974
|
var init_adminModelProxy = __esm({
|
|
55194
55975
|
"../../src/channels/adminModelProxy.ts"() {
|
|
55195
55976
|
"use strict";
|
|
@@ -55199,7 +55980,7 @@ var init_adminModelProxy = __esm({
|
|
|
55199
55980
|
init_modelProxy();
|
|
55200
55981
|
MODEL_PROXY_BEARER_PATH = "/admin/model-proxy/bearer";
|
|
55201
55982
|
MODEL_PROXY_BEARER_SCOPE = "deploy:write";
|
|
55202
|
-
|
|
55983
|
+
MAX_BODY_BYTES4 = 4096;
|
|
55203
55984
|
}
|
|
55204
55985
|
});
|
|
55205
55986
|
|
|
@@ -55473,7 +56254,8 @@ ${task}` : task;
|
|
|
55473
56254
|
...prior ? { prior } : {}
|
|
55474
56255
|
});
|
|
55475
56256
|
const contract = await contractFor(instance, unit, readers);
|
|
55476
|
-
|
|
56257
|
+
const severity = `severity:${instance.addressSeverity ?? DEFAULT_ADDRESS_SEVERITY}`;
|
|
56258
|
+
return { preset: "review", prompt: `${prUrl(instance.repo, brief.pr)} ${severity}
|
|
55477
56259
|
|
|
55478
56260
|
${turn}`, contract };
|
|
55479
56261
|
}
|
|
@@ -56758,7 +57540,7 @@ function createGithubWebhookHandler(deps) {
|
|
|
56758
57540
|
res.end(JSON.stringify(body2));
|
|
56759
57541
|
};
|
|
56760
57542
|
if (req.method !== "POST") return answer(405, { error: "method_not_allowed" });
|
|
56761
|
-
const body = await readBody(req,
|
|
57543
|
+
const body = await readBody(req, MAX_BODY_BYTES5);
|
|
56762
57544
|
if (!body.ok) return answer(413, { error: "too_large" });
|
|
56763
57545
|
const result = await handleCheckRunIntake(
|
|
56764
57546
|
{ event: one(req.headers[GITHUB_EVENT_HEADER]), signature: one(req.headers[GITHUB_SIGNATURE_HEADER]) },
|
|
@@ -56768,14 +57550,14 @@ function createGithubWebhookHandler(deps) {
|
|
|
56768
57550
|
answer(result.status, result.body);
|
|
56769
57551
|
};
|
|
56770
57552
|
}
|
|
56771
|
-
var GITHUB_WEBHOOK_PATH,
|
|
57553
|
+
var GITHUB_WEBHOOK_PATH, MAX_BODY_BYTES5, one;
|
|
56772
57554
|
var init_githubWebhook = __esm({
|
|
56773
57555
|
"../../src/channels/githubWebhook.ts"() {
|
|
56774
57556
|
"use strict";
|
|
56775
57557
|
init_checksIntake();
|
|
56776
57558
|
init_http();
|
|
56777
57559
|
GITHUB_WEBHOOK_PATH = "/webhooks/github";
|
|
56778
|
-
|
|
57560
|
+
MAX_BODY_BYTES5 = 1024 * 1024;
|
|
56779
57561
|
one = (v) => Array.isArray(v) ? v[0] : v;
|
|
56780
57562
|
}
|
|
56781
57563
|
});
|
|
@@ -56892,6 +57674,7 @@ async function runBot() {
|
|
|
56892
57674
|
};
|
|
56893
57675
|
let slackChannelsOf;
|
|
56894
57676
|
const channelsOf = (actorId) => slackChannelsOf ? slackChannelsOf(actorId) : Promise.resolve("unknown");
|
|
57677
|
+
const personName = (personId) => names.person(personId);
|
|
56895
57678
|
const mcpWiring = buildMcp(config, processSecrets, {
|
|
56896
57679
|
publicBaseUrl: process.env.PUBLIC_BASE_URL,
|
|
56897
57680
|
resolveEmail: (userId) => slackEmailLookup ? slackEmailLookup(userId) : Promise.resolve(void 0),
|
|
@@ -57228,7 +58011,7 @@ async function runBot() {
|
|
|
57228
58011
|
{
|
|
57229
58012
|
commands,
|
|
57230
58013
|
names,
|
|
57231
|
-
callerFor: (identity2) => callerFor(identity2, { grantsFor: (id) => config.grantsFor(id), personByEmail, channelsOf }),
|
|
58014
|
+
callerFor: (identity2) => callerFor(identity2, { grantsFor: (id) => config.grantsFor(id), personByEmail, channelsOf, personName }),
|
|
57232
58015
|
installation: () => installationSettings(config.config, capabilities),
|
|
57233
58016
|
vocabulary: {
|
|
57234
58017
|
agents: Object.keys(AGENTS),
|
|
@@ -57292,8 +58075,10 @@ async function runBot() {
|
|
|
57292
58075
|
grantsFor: (id) => config.grantsFor(id),
|
|
57293
58076
|
personByEmail,
|
|
57294
58077
|
channelsOf,
|
|
58078
|
+
personName,
|
|
57295
58079
|
publicBaseUrl
|
|
57296
58080
|
});
|
|
58081
|
+
const viewAsRoutes = createViewAsHandler({ publicBaseUrl, secure: publicBaseUrl?.startsWith("https://") ?? false });
|
|
57297
58082
|
const commandHttpState = `GET|POST /api/<group>.<verb> (${commands.list().length} commands)`;
|
|
57298
58083
|
const mcpConnectView = createMcpConnectViewHandler({
|
|
57299
58084
|
registry: () => mcpWiring.service,
|
|
@@ -57374,19 +58159,23 @@ async function runBot() {
|
|
|
57374
58159
|
res.end("forbidden");
|
|
57375
58160
|
return;
|
|
57376
58161
|
}
|
|
57377
|
-
|
|
57378
|
-
const
|
|
58162
|
+
const viewAs = viewAsFromCookie(req.headers.cookie);
|
|
58163
|
+
const identity2 = viewAs !== void 0 ? { ...gate.identity, viewAs } : gate.identity;
|
|
58164
|
+
if (isCommandPath(path)) return commandHttp(req, res, identity2);
|
|
58165
|
+
const actor = await resolveAccessActor(identity2, {
|
|
57379
58166
|
grantsFor: (id) => config.grantsFor(id),
|
|
57380
58167
|
personByEmail,
|
|
57381
|
-
channelsOf
|
|
58168
|
+
channelsOf,
|
|
58169
|
+
personName
|
|
57382
58170
|
});
|
|
58171
|
+
if (viewAsRoutes(req, res, { actor })) return;
|
|
57383
58172
|
if (liveView2(req, res, { actor })) return;
|
|
57384
|
-
if (webChat(req, res, { actor, identity:
|
|
58173
|
+
if (webChat(req, res, { actor, identity: identity2 })) return;
|
|
57385
58174
|
if (mcpConnectView(req, res, gate.identity)) return;
|
|
57386
58175
|
if (residentsView(req, res, { actor })) return;
|
|
57387
|
-
if (costsView(req, res, { identity:
|
|
58176
|
+
if (costsView(req, res, { identity: identity2, actor })) return;
|
|
57388
58177
|
if (deliveryView(req, res, { actor })) return;
|
|
57389
|
-
if (settingsView(req, res, { identity:
|
|
58178
|
+
if (settingsView(req, res, { identity: identity2 })) return;
|
|
57390
58179
|
res.writeHead(200, { "content-type": "text/plain" });
|
|
57391
58180
|
res.end("ok");
|
|
57392
58181
|
}).catch((err2) => {
|
|
@@ -57629,6 +58418,7 @@ var init_index = __esm({
|
|
|
57629
58418
|
init_source();
|
|
57630
58419
|
init_confirmations();
|
|
57631
58420
|
init_mcpConnectView();
|
|
58421
|
+
init_viewAs2();
|
|
57632
58422
|
init_lookups();
|
|
57633
58423
|
init_slackNames();
|
|
57634
58424
|
init_names();
|