@coreplane/switchboard 1.240.0 → 1.241.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/config/config.example.yaml +26 -7
- package/dist/assets/deploy/cloudflare/worker.ts +77 -18
- package/dist/assets/deploy/cloudflare-resident/worker.ts +1 -0
- package/dist/assets/deploy/secrets.manifest.json +6 -0
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +27 -8
- package/dist/assets/src/core/authz/actor.ts +5 -0
- package/dist/assets/src/core/authz/grants.ts +20 -9
- package/dist/assets/src/core/coordinator/contract.ts +37 -0
- package/dist/assets/src/core/coordinator/driver.ts +17 -1
- package/dist/assets/src/core/coordinator/instancesRoute.ts +72 -0
- package/dist/assets/src/core/costs.ts +10 -5
- package/dist/assets/src/core/ship/coordinator.ts +22 -7
- package/dist/assets/src/core/trace/attrs.ts +1 -1
- package/dist/assets/src/execution/bindingPurge.ts +2 -2
- package/dist/assets/web/dist/.vite/manifest.json +422 -420
- package/dist/assets/web/dist/assets/AppShell-DEy5jRuS.js +1 -0
- package/dist/assets/web/dist/assets/{CostsPage-BD3w5j_S.js → CostsPage-BuHD-Bv7.js} +2 -2
- package/dist/assets/web/dist/assets/{DeliveryPage-BtvzKA1m.js → DeliveryPage-BGpPr5xr.js} +1 -1
- package/dist/assets/web/dist/assets/HomePage-dKBvWh-E.js +2 -0
- package/dist/assets/web/dist/assets/{NotFoundPage-DwOX1HaZ.js → NotFoundPage-DtmQM2gD.js} +1 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-CrEAWx5b.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-CUAOh5d9.js → ResidentDetailPage-C-48fxat.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-B3KUlYy2.js → ResidentsIndexPage-CBBIjPOa.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-B6vxjcBI.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-CQjd3Cz-.js +6 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-Cy9G6D0p.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-BnkvCBrM.js → RunsTabs-CtNGNnhN.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-C1y9jgAk.js → ScheduledPage-Ci1Ygqnc.js} +1 -1
- package/dist/assets/web/dist/assets/SettingsPage-DxmJvAmT.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-CiByEtoR.js → StatusDot-DPWE1JmO.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DyRxk8A8.js → Tooltip-DKhSRH9t.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-CnWyOizV.js → UnitRoutePage-DEN5bSQf.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-html-DAQ_1T1v.js → angular-html-DgSK1qvr.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-D4kxRVp1.js → angular-ts-D3gNdiSG.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-Dy07l64y.js → apl-CkHCYM8I.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-DZIbTlc7.js → astro-DLm45axt.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-CmSXWX3l.js → blade-BKa-VE-c.js} +1 -1
- package/dist/assets/web/dist/assets/{c-ggzVdJLK.js → c-C8NCNWai.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-Bo1wktsG.js → chapel-DqDQ7IKH.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-DJUmtKM2.js → cobol-j4vCD6AJ.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-BZWOVz4C.js → coffee-C6oDVH-0.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-Dtk39yMw.js → cpp-Bd3A1Rtc.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-CGf_-Zoj.js → crystal-CUOgOw0d.js} +1 -1
- package/dist/assets/web/dist/assets/{css-CX2fcL4u.js → css-RYljyv7G.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-D8TaC961.js → dist-BnJXKHCY.js} +2 -2
- package/dist/assets/web/dist/assets/{edge-BYS8FtgQ.js → edge-BJhNdQDc.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-fhUpi3nO.js → elixir-g1AWOAfT.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-DAQNiwsN.js → elm-CER5e4Dz.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-CWzEys6U.js → erb-D5aOmMQf.js} +1 -1
- package/dist/assets/web/dist/assets/{git-rebase-9rY3p-Rw.js → git-rebase-D6rfV8jp.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-tY7hy0nE.js → glimmer-js-BvDH3-mC.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-CTrLv5JF.js → glimmer-ts-DChtNs9V.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-BRJbiDfq.js → glsl-BHlsWrxf.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-B-MtVLh2.js → graphql-BsaBHb53.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-BtIkOypQ.js → hack-0lbDosCd.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-CoOH6gfa.js → haml-BKtco2vr.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-DgrRu-GP.js → handlebars-BE_cj04z.js} +1 -1
- package/dist/assets/web/dist/assets/{html-DZaaI7ca.js → html-BNcr8EVd.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-DgHvT-qZ.js → html-derivative-BsJt2Kei.js} +1 -1
- package/dist/assets/web/dist/assets/{http-CvAd51yz.js → http-BR5P8ER_.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-CXGqAfzx.js → hurl-CtHalpWS.js} +1 -1
- package/dist/assets/web/dist/assets/{indexRow-Cse3JGb0.js → indexRow-BD1VT8o8.js} +1 -1
- package/dist/assets/web/dist/assets/{java-CIQStIXF.js → java-BXVdhN11.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-WbH8s7E3.js → javascript-CyoShgNQ.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-DzyWEVKC.js → jinja-BQYZzHex.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-CrT-zCa-.js → jison-DqrUa1Eq.js} +1 -1
- package/dist/assets/web/dist/assets/{json-lHjpjXMQ.js → json-CQiTF9Jj.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-WTwYCHW6.js → jsx-Y16MkCYQ.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-Bqpk9H7E.js → julia-zn16YONe.js} +1 -1
- package/dist/assets/web/dist/assets/{just-LdXk1r3m.js → just-DcVeN_MN.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-h_y2A5KD.js → latex-BNNoF-WP.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-CY7bF2_p.js → liquid-DAi6qHzn.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-oQ7XVZOY.js → lua-BswS0axw.js} +1 -1
- package/dist/assets/web/dist/assets/{main-CGO6fQ8k.js → main-B3is4sk6.js} +2 -2
- package/dist/assets/web/dist/assets/main-DP_zSemY.css +1 -0
- package/dist/assets/web/dist/assets/{marko-zv_qZ4QV.js → marko-C-4hkmQZ.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-Dl4kL_YK.js → mdc-0ZaHUkS7.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-CAHCZj4F.js → nginx-DBDg5tOR.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-BOtmwCJH.js → nim-BVMz49Yw.js} +1 -1
- package/dist/assets/web/dist/assets/{org-DspQk3HQ.js → org-1hO_K6ya.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-CAXmvVl0.js → perl-CVRQLvVk.js} +1 -1
- package/dist/assets/web/dist/assets/{php-CMUx4VyM.js → php-K9nCrB9n.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-B55xVJTv.js → pug-CTxOcmO3.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-DsNKCCRk.js → qml-CmCkfG5q.js} +1 -1
- package/dist/assets/web/dist/assets/{r-DbhP0VvO.js → r-pP47Xn1X.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-BCpE6P-p.js → razor-BpW6r3tC.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-BllsjiN4.js → regexp-CINgdY4N.js} +1 -1
- package/dist/assets/web/dist/assets/{rst-CO5Rpq9V.js → rst-BZIUEq2Q.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-C2A4OgQq.js → ruby-nzAOxz6r.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-Butzoyql.js → sas-QIS1bFth.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-PEUwJBpf.js → scss-BuhBBVNt.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-3Yhq0khE.js → shellscript-CQc1vXbk.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-04Hkb7m3.js → shellsession-CoubCAUv.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-BnId73cP.js → soy-CPRzlder.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-BKwdc0ch.js → sql-W9krb8-9.js} +1 -1
- package/dist/assets/web/dist/assets/{sseReplay-DutHNghE.js → sseReplay-yji8a1wM.js} +1 -1
- package/dist/assets/web/dist/assets/{stata-BPPtNkay.js → stata-CuISJEC0.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-CDPChcYP.js → surrealql-CSet7584.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-B15DIQ-z.js → svelte-ChXTSYwK.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-D-rp_AiB.js → templ-Bm55v62k.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-BnVDCD7p.js → tex-CICMX8Gj.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-cpujdyAR.js → ts-tags-CA1UzWyB.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-D1vO4L72.js → tsx-Dy04HNbv.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-oQGnQGRc.js → twig-BBHsVnVD.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-CKSFpXiH.js → typescript-Dvc-wVBT.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-DKHFo8BF.js → typst-Dvlqx3_q.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-DpCp0_Sz.js → vue-html-D4YxT3An.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-y5d5wUb1.js → vue-vine-0YE7uQlH.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-BstokpiS.js → vue-zWi1MNU2.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-Br2hx5Ax.js → xml-gm-iksZB.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-Bc8Cobap.js → xsl-D_W5BcrY.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-LOITy_I1.js → yaml-CHmZ21wZ.js} +1 -1
- package/dist/cli.js +3411 -502
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/AppShell-CM_fh-WW.js +0 -1
- package/dist/assets/web/dist/assets/HomePage-BU6aRCSe.js +0 -2
- package/dist/assets/web/dist/assets/RunFoldRow-Dulcbg25.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-DGoGrQZq.js +0 -6
- package/dist/assets/web/dist/assets/RunsIndexPage-37W7OZbC.js +0 -1
- package/dist/assets/web/dist/assets/SettingsPage-ggfB-Rqr.js +0 -1
- package/dist/assets/web/dist/assets/Slideover-CX-LEklF.js +0 -1
- package/dist/assets/web/dist/assets/main-O8UXRuEv.css +0 -1
package/dist/cli.js
CHANGED
|
@@ -1032,8 +1032,9 @@ function surfaceKeyFor(actorId) {
|
|
|
1032
1032
|
function agentRunAction(agent) {
|
|
1033
1033
|
return `agent:run:${agent}`;
|
|
1034
1034
|
}
|
|
1035
|
-
function
|
|
1036
|
-
|
|
1035
|
+
function browserActions(commandGroups) {
|
|
1036
|
+
if (commandGroups.length === 0) return /* @__PURE__ */ new Set();
|
|
1037
|
+
return /* @__PURE__ */ new Set([...commandGroups.map((g) => `${g}:read`), ...BROWSER_WRITE_ACTIONS]);
|
|
1037
1038
|
}
|
|
1038
1039
|
function toSet(v) {
|
|
1039
1040
|
if (v === "all") return "all";
|
|
@@ -1111,7 +1112,7 @@ function parseRestrictConfig(raw, agentNames) {
|
|
|
1111
1112
|
}
|
|
1112
1113
|
function namespaceBaseline(actorId, table) {
|
|
1113
1114
|
if (actorId.startsWith("slack:")) return table.everyone;
|
|
1114
|
-
if (actorId.startsWith("access:") && !actorId.startsWith("access:svc:")) return table.
|
|
1115
|
+
if (actorId.startsWith("access:") && !actorId.startsWith("access:svc:")) return table.browser;
|
|
1115
1116
|
return NO_GRANTS;
|
|
1116
1117
|
}
|
|
1117
1118
|
function grantsTable(source2) {
|
|
@@ -1119,7 +1120,7 @@ function grantsTable(source2) {
|
|
|
1119
1120
|
const openAgents = (source2.agentNames ?? []).filter((a) => !restrict.agents.has(a)).map(agentRunAction);
|
|
1120
1121
|
const baselines = {
|
|
1121
1122
|
everyone: { ...NO_GRANTS, actions: /* @__PURE__ */ new Set([...CHAT_OPEN_ACTIONS, ...openAgents]) },
|
|
1122
|
-
|
|
1123
|
+
browser: { ...NO_GRANTS, actions: browserActions(source2.commandGroups ?? []) }
|
|
1123
1124
|
};
|
|
1124
1125
|
const grants = /* @__PURE__ */ new Map();
|
|
1125
1126
|
const surfaces = /* @__PURE__ */ new Map();
|
|
@@ -1164,7 +1165,7 @@ function unionGrants(a, b) {
|
|
|
1164
1165
|
function isEmpty(g) {
|
|
1165
1166
|
return g.actions !== "all" && g.actions.size === 0 && g.channels !== "all" && g.channels.size === 0 && g.repos !== "all" && g.repos.size === 0;
|
|
1166
1167
|
}
|
|
1167
|
-
var ALL_GRANTS, GRANT_ACTOR_PREFIXES, SURFACE_GRANT_PREFIXES, CHAT_OPEN_ACTIONS, grantList, grantsEntrySchema, NO_RESTRICTION, REPO_SLUG_RE, restrictSchema;
|
|
1168
|
+
var ALL_GRANTS, GRANT_ACTOR_PREFIXES, SURFACE_GRANT_PREFIXES, CHAT_OPEN_ACTIONS, BROWSER_WRITE_ACTIONS, grantList, grantsEntrySchema, NO_RESTRICTION, REPO_SLUG_RE, restrictSchema;
|
|
1168
1169
|
var init_grants = __esm({
|
|
1169
1170
|
"../../src/core/authz/grants.ts"() {
|
|
1170
1171
|
"use strict";
|
|
@@ -1185,6 +1186,7 @@ var init_grants = __esm({
|
|
|
1185
1186
|
"memory:write",
|
|
1186
1187
|
"mcp:write"
|
|
1187
1188
|
];
|
|
1189
|
+
BROWSER_WRITE_ACTIONS = ["memory:write", "mcp:write"];
|
|
1188
1190
|
grantList = z.union([z.literal("all"), z.array(z.string().min(1))]);
|
|
1189
1191
|
grantsEntrySchema = z.object({ actions: grantList.optional(), channels: grantList.optional(), repos: grantList.optional() }).strict();
|
|
1190
1192
|
NO_RESTRICTION = Object.freeze({ agents: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set() });
|
|
@@ -1843,6 +1845,9 @@ function runawayTurnCap(maxMinutes) {
|
|
|
1843
1845
|
function loopBudget(maxMinutes) {
|
|
1844
1846
|
return { maxMinutes, maxTurns: runawayTurnCap(maxMinutes) };
|
|
1845
1847
|
}
|
|
1848
|
+
function statusCardRule(examples = '"Implement the fix", "Run the test suite"') {
|
|
1849
|
+
return `Maintain the user-facing status card with the update_status tool. Post your plan as a checklist (\u25CB pending) as soon as you have it, then keep it truthful at every moment: the markers are facts, not intentions. Mark an item \u2731 when you issue the first command that does it, and \u2713 only after you have read the result that proves it happened \u2014 never in the same turn as the command, never because you intend to run it next, never for a reporting or posting step you have not done. The order is: mark the item \u2731, run its commands, read the result, then mark it \u2713 and the next item \u2731. The card shows the command running right now beside your checklist, so a \u2713 item whose command is still running reads as a lie. Items are short outcomes (${examples}), never commands. This is the only progress the user sees while you work.`;
|
|
1850
|
+
}
|
|
1846
1851
|
function conductorSystem(siblings) {
|
|
1847
1852
|
const readers = siblings.filter((a) => a.identity !== "write");
|
|
1848
1853
|
const writers = siblings.filter((a) => a.identity === "write");
|
|
@@ -1946,7 +1951,7 @@ ${SHOW_FILES}
|
|
|
1946
1951
|
|
|
1947
1952
|
${NOTEPAD}
|
|
1948
1953
|
|
|
1949
|
-
|
|
1954
|
+
${statusCardRule('"Clone repo and read the diff", "Run the test suite"')}
|
|
1950
1955
|
|
|
1951
1956
|
If the request doesn't name a repository and you can't infer it, ask for it instead of guessing.
|
|
1952
1957
|
Report outcomes faithfully: if tests fail or a step was skipped, say so plainly.
|
|
@@ -1983,7 +1988,7 @@ ${SHOW_FILES}
|
|
|
1983
1988
|
|
|
1984
1989
|
${NOTEPAD}
|
|
1985
1990
|
|
|
1986
|
-
|
|
1991
|
+
${statusCardRule()}
|
|
1987
1992
|
|
|
1988
1993
|
Report outcomes faithfully: if tests fail or a step was skipped, say so plainly.
|
|
1989
1994
|
${FENCED_CONTENT_RULE}
|
|
@@ -2015,7 +2020,7 @@ ${SHOW_FILES}
|
|
|
2015
2020
|
|
|
2016
2021
|
${NOTEPAD}
|
|
2017
2022
|
|
|
2018
|
-
|
|
2023
|
+
${statusCardRule()}
|
|
2019
2024
|
|
|
2020
2025
|
Report outcomes faithfully: if tests fail or a step was skipped, say so plainly.
|
|
2021
2026
|
${FENCED_CONTENT_RULE}
|
|
@@ -2050,7 +2055,7 @@ ${REVIEW_VERDICT_INSTRUCTION}
|
|
|
2050
2055
|
|
|
2051
2056
|
${NOTEPAD}
|
|
2052
2057
|
|
|
2053
|
-
|
|
2058
|
+
${statusCardRule('"Gather the diff and the files", "Analyze the change", "Post the verdict"')}
|
|
2054
2059
|
|
|
2055
2060
|
${FENCED_CONTENT_RULE}
|
|
2056
2061
|
Your final message is posted to Slack. Lead with a one-line verdict, then the findings.`;
|
|
@@ -2079,7 +2084,7 @@ ${REVIEW_VERDICT_INSTRUCTION}
|
|
|
2079
2084
|
|
|
2080
2085
|
${NOTEPAD}
|
|
2081
2086
|
|
|
2082
|
-
|
|
2087
|
+
${statusCardRule('"Gather the diff and the files", "Analyze the change", "Post the verdict"')}
|
|
2083
2088
|
|
|
2084
2089
|
${FENCED_CONTENT_RULE}
|
|
2085
2090
|
Your final message is posted to Slack. Lead with a one-line verdict, then the findings.`;
|
|
@@ -2108,7 +2113,7 @@ ${REVIEW_VERDICT_INSTRUCTION}
|
|
|
2108
2113
|
|
|
2109
2114
|
${NOTEPAD}
|
|
2110
2115
|
|
|
2111
|
-
|
|
2116
|
+
${statusCardRule('"Gather the diff and the files", "Analyze the change", "Post the verdict"')}
|
|
2112
2117
|
|
|
2113
2118
|
${FENCED_CONTENT_RULE}
|
|
2114
2119
|
Your final message is posted to Slack. Lead with a one-line verdict, then the findings.`;
|
|
@@ -2121,7 +2126,7 @@ How to work:
|
|
|
2121
2126
|
2. Prefer primary sources; corroborate a surprising claim with a second source.
|
|
2122
2127
|
3. Answer concisely and cite the URLs (or repo paths) you used. If sources conflict or you couldn't verify something, say so plainly. If web search is unconfigured, use web_fetch / the GitHub tools on what you have and say search was unavailable.
|
|
2123
2128
|
|
|
2124
|
-
|
|
2129
|
+
${statusCardRule('"Search the sources", "Write the answer"')}
|
|
2125
2130
|
|
|
2126
2131
|
${FENCED_CONTENT_RULE}
|
|
2127
2132
|
Use Slack-friendly formatting (no markdown headers; *bold*, bullets, code blocks). Your final message is posted to Slack \u2014 lead with the answer, then supporting detail and sources.`;
|
|
@@ -2131,6 +2136,8 @@ Answer directly and concisely. Use Slack-friendly formatting (no markdown header
|
|
|
2131
2136
|
|
|
2132
2137
|
Your tools work without a workspace: the GitHub tools \u2014 \`github_repos\` (the org repositories you can reach), \`github_tree\` / \`github_file\` / \`github_search_code\` (browse, read, search their code and docs, private repos included), \`github_issue_list\` / \`github_issue_get\` (read issues), \`github_issue_create\` / \`github_issue_update\` / \`github_issue_comment\` / \`github_issue_delete\` (act on issues) \u2014 and \`web_fetch\` (read a public URL). Use them: when the user names a repo loosely ("the switchboard app"), resolve it with github_repos (or the thread) rather than asking; when asked about one of our repos, read it before answering. Report exactly what a tool did (issue number + URL) \u2014 never claim an action you did not perform, and never fabricate file contents, URLs, or command output.
|
|
2133
2138
|
|
|
2139
|
+
${statusCardRule('"Read the issue and its thread", "Post the comment"')} A one-step answer needs no checklist; post one when the request has steps the person would wait on.
|
|
2140
|
+
|
|
2134
2141
|
You cannot run commands, clone repositories, edit code, or review pull requests, and you cannot search the web. Other Switchboard agents can: for code changes or PRs tell the user to re-send with \`agent:coding\`; for a PR review, \`agent:review\`; for a web-research question, \`agent:research\` (e.g. "\`agent:coding fix the failing login test in acme/api\`", "\`agent:research compare X and Y\`"). Delete an issue only when the user explicitly asked to delete it (closing is an update).`;
|
|
2135
2142
|
EXPLORE_SYSTEM = `You are Switchboard's explore agent: a long, read-only investigation of a repository, answering a request from Slack.
|
|
2136
2143
|
|
|
@@ -2144,7 +2151,7 @@ READ-ONLY: NEVER open a pull request, and never commit or push \u2014 no branch,
|
|
|
2144
2151
|
|
|
2145
2152
|
You cannot attach or post files: your whole answer is text. Never say a file is attached or below \u2014 name its path in the workspace and describe it (what it shows, its size) instead; a person who needs the file itself asks \`agent:coding\`, which can attach.
|
|
2146
2153
|
|
|
2147
|
-
|
|
2154
|
+
${statusCardRule('"Clone and install", "Time the full suite"')}
|
|
2148
2155
|
|
|
2149
2156
|
${NOTEPAD}
|
|
2150
2157
|
|
|
@@ -2843,7 +2850,7 @@ function matchDispositions(findings, dispositions) {
|
|
|
2843
2850
|
const dropped = [...new Set(dispositions.filter((d) => !issued.has(d.findingId)).map((d) => d.findingId))];
|
|
2844
2851
|
return { matched, dropped };
|
|
2845
2852
|
}
|
|
2846
|
-
var MIN, SHIP_ROUND_RESERVE_MS, SHIP_LOOP_RESERVE_MS, SHIP_FIX_RESERVE_MS, SHIP_MIN_MAX_MINUTES, PLAN_ID_PATTERN, PLAN_BRANCH, UNIT_REF, SLUG_MAX, INSTANCE_ID_MAX, ADDRESS_SEVERITIES, DEFAULT_ADDRESS_SEVERITY, isAddressSeverity, WAIT_MARGIN_MS, WAIT_CHUNK_MS,
|
|
2853
|
+
var MIN, SHIP_ROUND_RESERVE_MS, SHIP_LOOP_RESERVE_MS, SHIP_FIX_RESERVE_MS, SHIP_MIN_MAX_MINUTES, PLAN_ID_PATTERN, PLAN_BRANCH, UNIT_REF, SLUG_MAX, INSTANCE_ID_MAX, ADDRESS_SEVERITIES, DEFAULT_ADDRESS_SEVERITY, isAddressSeverity, WAIT_MARGIN_MS, WAIT_CHUNK_MS, MERGE_WAIT_MAX_MS, MERGE_WAIT_CHUNK_MS, BUSY_RETRY_MS;
|
|
2847
2854
|
var init_coordinator = __esm({
|
|
2848
2855
|
"../../src/core/ship/coordinator.ts"() {
|
|
2849
2856
|
"use strict";
|
|
@@ -2865,8 +2872,8 @@ var init_coordinator = __esm({
|
|
|
2865
2872
|
isAddressSeverity = (v) => ADDRESS_SEVERITIES.includes(v);
|
|
2866
2873
|
WAIT_MARGIN_MS = 5 * MIN;
|
|
2867
2874
|
WAIT_CHUNK_MS = 5 * MIN;
|
|
2868
|
-
MERGE_POLL_MS = 5 * MIN;
|
|
2869
2875
|
MERGE_WAIT_MAX_MS = 60 * MIN;
|
|
2876
|
+
MERGE_WAIT_CHUNK_MS = 5 * MIN;
|
|
2870
2877
|
BUSY_RETRY_MS = 2 * MIN;
|
|
2871
2878
|
}
|
|
2872
2879
|
});
|
|
@@ -2963,6 +2970,22 @@ var init_dashboardAuthConfig = __esm({
|
|
|
2963
2970
|
}
|
|
2964
2971
|
});
|
|
2965
2972
|
|
|
2973
|
+
// ../../src/core/harness/roster.ts
|
|
2974
|
+
function isHarnessName(word) {
|
|
2975
|
+
return typeof word === "string" && HARNESS_NAMES.includes(word);
|
|
2976
|
+
}
|
|
2977
|
+
function harnessForPreset(roster, words, preset) {
|
|
2978
|
+
return roster[words?.[preset] ?? DEFAULT_HARNESS];
|
|
2979
|
+
}
|
|
2980
|
+
var HARNESS_NAMES, DEFAULT_HARNESS;
|
|
2981
|
+
var init_roster = __esm({
|
|
2982
|
+
"../../src/core/harness/roster.ts"() {
|
|
2983
|
+
"use strict";
|
|
2984
|
+
HARNESS_NAMES = ["pi", "opencode"];
|
|
2985
|
+
DEFAULT_HARNESS = "pi";
|
|
2986
|
+
}
|
|
2987
|
+
});
|
|
2988
|
+
|
|
2966
2989
|
// ../../src/tools/web.ts
|
|
2967
2990
|
import { lookup as dnsLookup } from "node:dns/promises";
|
|
2968
2991
|
import { Agent, fetch as undiciFetch } from "undici";
|
|
@@ -3578,6 +3601,7 @@ function validateConfig(cfg) {
|
|
|
3578
3601
|
if (cfg.artifacts !== void 0) validateArtifacts(cfg.artifacts);
|
|
3579
3602
|
if (cfg.harness !== void 0) validateHarness(cfg.harness);
|
|
3580
3603
|
if (cfg.pi !== void 0) validatePi(cfg.pi);
|
|
3604
|
+
if (cfg.opencode !== void 0) validateOpenCode(cfg.opencode);
|
|
3581
3605
|
validateDashboardConfig(cfg.dashboard);
|
|
3582
3606
|
}
|
|
3583
3607
|
function validatePi(pi) {
|
|
@@ -3597,15 +3621,32 @@ function validatePi(pi) {
|
|
|
3597
3621
|
}
|
|
3598
3622
|
function validateHarness(harness) {
|
|
3599
3623
|
if (typeof harness !== "object" || harness === null || Array.isArray(harness))
|
|
3600
|
-
throw new Error(
|
|
3624
|
+
throw new Error(`config.yaml: harness must be a mapping of preset to a harness name (${harnessWords(" or ")})`);
|
|
3601
3625
|
for (const [preset, value] of Object.entries(harness)) {
|
|
3602
3626
|
if (!Object.hasOwn(AGENTS, preset)) throw new Error(`config.yaml: harness.${preset} is not a known agent`);
|
|
3603
|
-
if (value
|
|
3627
|
+
if (!isHarnessName(value))
|
|
3604
3628
|
throw new Error(
|
|
3605
|
-
`config.yaml: harness.${preset}
|
|
3629
|
+
`config.yaml: harness.${preset}: ${typeof value === "string" ? value : JSON.stringify(value)} is not a harness; the harnesses are ${harnessWords(" and ")}`
|
|
3606
3630
|
);
|
|
3607
3631
|
}
|
|
3608
3632
|
}
|
|
3633
|
+
function validateOpenCode(opencode) {
|
|
3634
|
+
if (typeof opencode !== "object" || opencode === null || Array.isArray(opencode))
|
|
3635
|
+
throw new Error("config.yaml: opencode must be a mapping");
|
|
3636
|
+
for (const key of unknownKeys2(opencode, OPENCODE_KEYS))
|
|
3637
|
+
throw new Error(`config.yaml: opencode.${key} is not a known key`);
|
|
3638
|
+
const { compaction } = opencode;
|
|
3639
|
+
if (compaction === void 0) return;
|
|
3640
|
+
if (typeof compaction !== "object" || compaction === null || Array.isArray(compaction))
|
|
3641
|
+
throw new Error("config.yaml: opencode.compaction must be a mapping");
|
|
3642
|
+
for (const key of unknownKeys2(compaction, OPENCODE_COMPACTION_KEYS))
|
|
3643
|
+
throw new Error(`config.yaml: opencode.compaction.${key} is not a known key`);
|
|
3644
|
+
for (const key of Object.keys(OPENCODE_COMPACTION_KEYS)) {
|
|
3645
|
+
const value = compaction[key];
|
|
3646
|
+
if (value !== void 0 && (typeof value !== "number" || !Number.isInteger(value) || value <= 0))
|
|
3647
|
+
throw new Error(`config.yaml: opencode.compaction.${key} must be a positive integer (tokens)`);
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3609
3650
|
function validateReferences(references) {
|
|
3610
3651
|
if (typeof references !== "object" || references === null || Array.isArray(references))
|
|
3611
3652
|
throw new Error("config.yaml: references must be a mapping");
|
|
@@ -3729,7 +3770,7 @@ function validateRuntimeOverrides(ro) {
|
|
|
3729
3770
|
}
|
|
3730
3771
|
}
|
|
3731
3772
|
}
|
|
3732
|
-
var MAX_INSTRUCTIONS_LENGTH, CONFIG_KEYS, SELF_IMPROVEMENT_KEYS, BOUNDARY_KEYS, MIN_BOUNDARY_MINUTES, PI_KEYS, PI_COMPACTION_KEYS, ROUTE_ANSWER_MODES, REFERENCES_KEYS, ROUTING_KEYS, SHIP_KEYS, SPAWN_KEYS;
|
|
3773
|
+
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, SPAWN_KEYS;
|
|
3733
3774
|
var init_validate = __esm({
|
|
3734
3775
|
"../../src/config/validate.ts"() {
|
|
3735
3776
|
"use strict";
|
|
@@ -3740,6 +3781,7 @@ var init_validate = __esm({
|
|
|
3740
3781
|
init_config();
|
|
3741
3782
|
init_grants();
|
|
3742
3783
|
init_registry();
|
|
3784
|
+
init_roster();
|
|
3743
3785
|
init_web();
|
|
3744
3786
|
init_registry2();
|
|
3745
3787
|
MAX_INSTRUCTIONS_LENGTH = 2e3;
|
|
@@ -3767,6 +3809,7 @@ var init_validate = __esm({
|
|
|
3767
3809
|
references: true,
|
|
3768
3810
|
harness: true,
|
|
3769
3811
|
pi: true,
|
|
3812
|
+
opencode: true,
|
|
3770
3813
|
slack: true,
|
|
3771
3814
|
runHistory: true,
|
|
3772
3815
|
runtimeOverrides: true,
|
|
@@ -3783,6 +3826,9 @@ var init_validate = __esm({
|
|
|
3783
3826
|
MIN_BOUNDARY_MINUTES = 2;
|
|
3784
3827
|
PI_KEYS = { compaction: true };
|
|
3785
3828
|
PI_COMPACTION_KEYS = { reserveTokens: true, keepRecentTokens: true };
|
|
3829
|
+
harnessWords = (joiner) => HARNESS_NAMES.join(joiner);
|
|
3830
|
+
OPENCODE_KEYS = { compaction: true };
|
|
3831
|
+
OPENCODE_COMPACTION_KEYS = { buffer: true, keepTokens: true };
|
|
3786
3832
|
ROUTE_ANSWER_MODES = ["tool", "text"];
|
|
3787
3833
|
REFERENCES_KEYS = { enabled: true };
|
|
3788
3834
|
ROUTING_KEYS = { auto: true, model: true, answer: true };
|
|
@@ -4284,15 +4330,15 @@ var init_config2 = __esm({
|
|
|
4284
4330
|
const us = this.userScope(opts.userId);
|
|
4285
4331
|
const [agentName, agentLayer] = opts.request.agent !== void 0 ? [opts.request.agent, "request"] : us.agent !== void 0 ? [us.agent, "user"] : ch.agent !== void 0 ? [ch.agent, "channel"] : [this.config.defaults.agent, "default"];
|
|
4286
4332
|
const boundary = intersectBoundaries(this.boundaryLayers(ch, us));
|
|
4287
|
-
const
|
|
4288
|
-
if (!
|
|
4333
|
+
const modelRef3 = opts.request.model ?? us.model ?? ch.model ?? us.models?.[agentName] ?? ch.models?.[agentName] ?? this.config.defaults.models[agentName] ?? this.config.defaults.models["general"];
|
|
4334
|
+
if (!modelRef3) {
|
|
4289
4335
|
throw new Error(`No model configured for agent "${agentName}" \u2014 set defaults.models.${agentName} in config.yaml`);
|
|
4290
4336
|
}
|
|
4291
4337
|
const effort2 = opts.request.effort ?? us.effort ?? ch.effort ?? us.efforts?.[agentName] ?? ch.efforts?.[agentName] ?? this.config.defaults.efforts?.[agentName];
|
|
4292
4338
|
return {
|
|
4293
4339
|
agentName,
|
|
4294
4340
|
agentLayer,
|
|
4295
|
-
modelRef:
|
|
4341
|
+
modelRef: modelRef3,
|
|
4296
4342
|
...effort2 !== void 0 ? { effort: effort2 } : {},
|
|
4297
4343
|
...boundary !== void 0 ? { boundary } : {}
|
|
4298
4344
|
};
|
|
@@ -4339,8 +4385,9 @@ var init_config2 = __esm({
|
|
|
4339
4385
|
}
|
|
4340
4386
|
/** The one grants lookup: what `grants[<actorId>]` declares
|
|
4341
4387
|
* on top of its namespace's baseline (the chat `open` commands and every
|
|
4342
|
-
* unrestricted agent for a Slack user, every group's read
|
|
4343
|
-
* session), else that baseline alone,
|
|
4388
|
+
* unrestricted agent for a Slack user, every group's read and the two
|
|
4389
|
+
* personal chat writes for a browser session), else that baseline alone,
|
|
4390
|
+
* else nothing — unioned with the
|
|
4344
4391
|
* surface's `<ns>:*` entry when config has one. Attached to every
|
|
4345
4392
|
* `Caller.actor`: the ONLY thing `authorize` reads about a caller. */
|
|
4346
4393
|
grantsFor(actorId) {
|
|
@@ -12420,7 +12467,7 @@ var init_costs = __esm({
|
|
|
12420
12467
|
};
|
|
12421
12468
|
DATED_RELEASE_SUFFIX = /^-\d{8}$/;
|
|
12422
12469
|
DEFAULT_DAYS = 30;
|
|
12423
|
-
MAX_DAYS =
|
|
12470
|
+
MAX_DAYS = 31;
|
|
12424
12471
|
NullLlmCostSource = class {
|
|
12425
12472
|
fetchDailyCost(_range) {
|
|
12426
12473
|
return Promise.resolve(null);
|
|
@@ -13062,6 +13109,9 @@ function unitOfIdempotencyKey(key) {
|
|
|
13062
13109
|
const head = step.split("/")[0];
|
|
13063
13110
|
return UNIT_PATTERN.test(head) ? head : void 0;
|
|
13064
13111
|
}
|
|
13112
|
+
function checksSettledEventType(headSha) {
|
|
13113
|
+
return `${CHECKS_SETTLED_EVENT_PREFIX}${headSha}`;
|
|
13114
|
+
}
|
|
13065
13115
|
function coordinatorFields(tag) {
|
|
13066
13116
|
return tag ? { parentInstanceId: tag.parentInstanceId, idempotencyKey: tag.idempotencyKey } : {};
|
|
13067
13117
|
}
|
|
@@ -13105,7 +13155,19 @@ function isCoordinatorUnit(v) {
|
|
|
13105
13155
|
if (r.startedAt !== void 0 && !isFinite(r.startedAt)) return false;
|
|
13106
13156
|
return true;
|
|
13107
13157
|
}
|
|
13108
|
-
|
|
13158
|
+
async function sendChecksSettled(workflow, instance, headSha, settledAt) {
|
|
13159
|
+
if (!workflow) return { kind: "no-binding", instance };
|
|
13160
|
+
const type = checksSettledEventType(headSha);
|
|
13161
|
+
const payload = { headSha, settledAt };
|
|
13162
|
+
try {
|
|
13163
|
+
const handle2 = await workflow.get(instance);
|
|
13164
|
+
await handle2.sendEvent({ type, payload });
|
|
13165
|
+
return { kind: "sent", instance, type };
|
|
13166
|
+
} catch (err2) {
|
|
13167
|
+
return { kind: "failed", instance, type, reason: err2 instanceof Error ? err2.message : String(err2) };
|
|
13168
|
+
}
|
|
13169
|
+
}
|
|
13170
|
+
var COORDINATOR_IDENTITY, COORDINATOR_STEP_ACTION, PLAN_MERGE_ACTION, COORDINATOR_STEP_PATH_PREFIX, INSTANCE_ID_PATTERN, STEP_NAME_PATTERN, IDEMPOTENCY_KEY_PATTERN, UNIT_PATTERN, UNIT_KEY_PATTERN, CHECKS_SETTLED_EVENT_PREFIX, REPO_SLUG2, MAX_TEXT, MAX_REPORT, MAX_ROUNDS, isText, isOptionalText, isFinite, isObject, isPr, isResume, isThread;
|
|
13109
13171
|
var init_contract2 = __esm({
|
|
13110
13172
|
"../../src/core/coordinator/contract.ts"() {
|
|
13111
13173
|
"use strict";
|
|
@@ -13118,6 +13180,7 @@ var init_contract2 = __esm({
|
|
|
13118
13180
|
IDEMPOTENCY_KEY_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9_-]{0,99}:[A-Za-z0-9_][A-Za-z0-9_./-]{0,119}$/;
|
|
13119
13181
|
UNIT_PATTERN = /^[A-Za-z0-9_-]{1,32}$/;
|
|
13120
13182
|
UNIT_KEY_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9_-]{0,99}:[A-Za-z0-9_-]{1,32}$/;
|
|
13183
|
+
CHECKS_SETTLED_EVENT_PREFIX = "checks-settled-";
|
|
13121
13184
|
REPO_SLUG2 = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
13122
13185
|
MAX_TEXT = 512;
|
|
13123
13186
|
MAX_REPORT = 2e4;
|
|
@@ -16187,13 +16250,22 @@ var init_config4 = __esm({
|
|
|
16187
16250
|
options: z6.object({ channel: channelOption }),
|
|
16188
16251
|
action: "config:read",
|
|
16189
16252
|
effect: "read",
|
|
16190
|
-
describe: "The effective agent/model/effort for you in this channel, the defaults, both scopes, and what is restricted.",
|
|
16253
|
+
describe: "The effective agent/model/effort for you in this channel, the defaults, both scopes, and what is restricted; without a channel (a browser, a token, the CLI), your settings outside any channel.",
|
|
16191
16254
|
render: (output) => formatConfigDescription(output),
|
|
16192
16255
|
handler: async ({ options, caller, deps }) => {
|
|
16193
|
-
const
|
|
16256
|
+
const me = meIdOf(caller) ?? caller.id;
|
|
16257
|
+
const channel = options.channel ?? caller.origin?.channelId;
|
|
16258
|
+
if (!channel) {
|
|
16259
|
+
const description2 = {
|
|
16260
|
+
...await deps.config.describeConfig(`none:${me}`, me),
|
|
16261
|
+
channel: {},
|
|
16262
|
+
channelConfigRestricted: true
|
|
16263
|
+
};
|
|
16264
|
+
return description2;
|
|
16265
|
+
}
|
|
16194
16266
|
await assertMayReadChannel(caller, channel, deps);
|
|
16195
16267
|
const description = {
|
|
16196
|
-
...await deps.config.describeConfig(channel,
|
|
16268
|
+
...await deps.config.describeConfig(channel, me),
|
|
16197
16269
|
channelConfigRestricted: !mayEditChannel(caller, channel)
|
|
16198
16270
|
};
|
|
16199
16271
|
return description;
|
|
@@ -20961,7 +21033,7 @@ var init_stores = __esm({
|
|
|
20961
21033
|
// ../../src/core/authz/channelDirectory.ts
|
|
20962
21034
|
function visibilityOf(channelId) {
|
|
20963
21035
|
if (channelId.startsWith("http:") || channelId.startsWith("mcp:")) return "machine";
|
|
20964
|
-
if (channelId.startsWith("slack:D")) return "dm";
|
|
21036
|
+
if (channelId.startsWith("slack:D") || channelId.startsWith("web:")) return "dm";
|
|
20965
21037
|
if (channelId.startsWith("slack:G")) return "private";
|
|
20966
21038
|
return "unknown";
|
|
20967
21039
|
}
|
|
@@ -25607,7 +25679,12 @@ var init_actor = __esm({
|
|
|
25607
25679
|
http: "http",
|
|
25608
25680
|
mcp: "mcp",
|
|
25609
25681
|
cli: "cli",
|
|
25610
|
-
schedule: "schedule"
|
|
25682
|
+
schedule: "schedule",
|
|
25683
|
+
// The web chat (record 0043): a browser session speaks as text, so its
|
|
25684
|
+
// `access:<sub>` is a chat credential too — the browser actor, its baseline
|
|
25685
|
+
// and its entry. A service token never reaches the chat (the dashboard gate
|
|
25686
|
+
// serves it `/api/*` only), so `access:svc:` is no chat surface.
|
|
25687
|
+
access: "access-browser"
|
|
25611
25688
|
};
|
|
25612
25689
|
}
|
|
25613
25690
|
});
|
|
@@ -26048,19 +26125,19 @@ function stagedFromInbox(v) {
|
|
|
26048
26125
|
}
|
|
26049
26126
|
function messageFromInbox(stored, fallbackAt) {
|
|
26050
26127
|
const m = stored;
|
|
26051
|
-
const
|
|
26052
|
-
const text =
|
|
26053
|
-
const userId =
|
|
26054
|
-
const threadKey =
|
|
26055
|
-
const channelId =
|
|
26128
|
+
const str8 = (k) => typeof m[k] === "string" ? m[k] : void 0;
|
|
26129
|
+
const text = str8("text");
|
|
26130
|
+
const userId = str8("userId");
|
|
26131
|
+
const threadKey = str8("threadKey");
|
|
26132
|
+
const channelId = str8("channelId");
|
|
26056
26133
|
if (text === void 0 || userId === void 0 || threadKey === void 0 || channelId === void 0)
|
|
26057
26134
|
return void 0;
|
|
26058
|
-
const userName =
|
|
26059
|
-
const authenticatedAs =
|
|
26060
|
-
const sourceUrl =
|
|
26061
|
-
const channelName =
|
|
26062
|
-
const messageId =
|
|
26063
|
-
const fromRunId =
|
|
26135
|
+
const userName = str8("userName");
|
|
26136
|
+
const authenticatedAs = str8("authenticatedAs");
|
|
26137
|
+
const sourceUrl = str8("sourceUrl");
|
|
26138
|
+
const channelName = str8("channelName");
|
|
26139
|
+
const messageId = str8("messageId");
|
|
26140
|
+
const fromRunId = str8("fromRunId");
|
|
26064
26141
|
const at = typeof m.at === "number" && Number.isFinite(m.at) ? m.at : fallbackAt;
|
|
26065
26142
|
const images = attachmentsFromInbox(m.images);
|
|
26066
26143
|
const documents = attachmentsFromInbox(m.documents);
|
|
@@ -29524,18 +29601,18 @@ var init_textTurns = __esm({
|
|
|
29524
29601
|
function maxChildrenOf(cfg) {
|
|
29525
29602
|
return cfg?.maxChildren ?? DEFAULT_MAX_CHILDREN;
|
|
29526
29603
|
}
|
|
29527
|
-
function childRequestText(
|
|
29528
|
-
const directives = [`agent:${
|
|
29529
|
-
const branch2 =
|
|
29530
|
-
const repo =
|
|
29531
|
-
return `${directives.join(" ")}${repo} ${
|
|
29604
|
+
function childRequestText(request2) {
|
|
29605
|
+
const directives = [`agent:${request2.preset}`, ...request2.budget !== void 0 ? [`budget:${request2.budget}`] : []];
|
|
29606
|
+
const branch2 = request2.ref !== void 0 ? ` on branch ${request2.ref}` : "";
|
|
29607
|
+
const repo = request2.repo !== void 0 ? ` in ${request2.repo}${branch2}:` : "";
|
|
29608
|
+
return `${directives.join(" ")}${repo} ${request2.prompt}`;
|
|
29532
29609
|
}
|
|
29533
|
-
function childThreadLead(parent,
|
|
29610
|
+
function childThreadLead(parent, request2) {
|
|
29534
29611
|
const who2 = parent.msg.userName ?? parent.msg.userId;
|
|
29535
29612
|
const from = parent.msg.sourceUrl !== void 0 ? `[the *${parent.agentName}* run](${parent.msg.sourceUrl})` : `the *${parent.agentName}* run`;
|
|
29536
|
-
const line3 = (
|
|
29613
|
+
const line3 = (request2.prompt.split("\n")[0] ?? "").trim();
|
|
29537
29614
|
const snippet = line3.length > 140 ? `${line3.slice(0, 137)}\u2026` : line3;
|
|
29538
|
-
return `\u21B3 *${
|
|
29615
|
+
return `\u21B3 *${request2.preset}* run for ${who2}, spawned by ${from}: ${snippet}`;
|
|
29539
29616
|
}
|
|
29540
29617
|
function platformOf(channelId) {
|
|
29541
29618
|
const colon = channelId.indexOf(":");
|
|
@@ -29561,7 +29638,7 @@ function watchedChild(io, on) {
|
|
|
29561
29638
|
if (io.openThread) watched2.openThread = (lead) => io.openThread(lead);
|
|
29562
29639
|
return watched2;
|
|
29563
29640
|
}
|
|
29564
|
-
async function spawnChild(deps, parent,
|
|
29641
|
+
async function spawnChild(deps, parent, request2) {
|
|
29565
29642
|
const refused6 = (reason3, message4) => ({ kind: "refused", reason: reason3, message: message4 });
|
|
29566
29643
|
if (parent.depth >= MAX_SPAWN_DEPTH) {
|
|
29567
29644
|
return refused6(
|
|
@@ -29569,10 +29646,10 @@ async function spawnChild(deps, parent, request) {
|
|
|
29569
29646
|
`a child run cannot spawn: this run is itself a child, and a tree is ${MAX_SPAWN_DEPTH} level deep \u2014 the run that started it is the one to ask`
|
|
29570
29647
|
);
|
|
29571
29648
|
}
|
|
29572
|
-
if (AGENTS[
|
|
29649
|
+
if (AGENTS[request2.preset]?.identity === "write") {
|
|
29573
29650
|
return refused6(
|
|
29574
29651
|
"spawn_identity",
|
|
29575
|
-
`\`${
|
|
29652
|
+
`\`${request2.preset}\` runs as a \`write\` identity \u2014 it pushes branches and opens pull requests \u2014 and a spawned child never writes: it reads this conversation and reports; the person who asked starts that work by hand with \`agent:${request2.preset}\``
|
|
29576
29653
|
);
|
|
29577
29654
|
}
|
|
29578
29655
|
const minutesLeft = Math.floor(parent.remainingMs / 6e4);
|
|
@@ -29598,7 +29675,7 @@ async function spawnChild(deps, parent, request) {
|
|
|
29598
29675
|
}
|
|
29599
29676
|
let opened;
|
|
29600
29677
|
try {
|
|
29601
|
-
opened = await parent.io.openThread(childThreadLead(parent,
|
|
29678
|
+
opened = await parent.io.openThread(childThreadLead(parent, request2));
|
|
29602
29679
|
} catch (err2) {
|
|
29603
29680
|
return refused6("spawn_failed", `the channel could not open the child's thread: ${describe3(err2)}`);
|
|
29604
29681
|
}
|
|
@@ -29610,7 +29687,7 @@ async function spawnChild(deps, parent, request) {
|
|
|
29610
29687
|
...parent.msg.channelName !== void 0 ? { channelName: parent.msg.channelName } : {},
|
|
29611
29688
|
threadKey: opened.thread.threadKey,
|
|
29612
29689
|
...opened.thread.sourceUrl !== void 0 ? { sourceUrl: opened.thread.sourceUrl } : {},
|
|
29613
|
-
text: childRequestText(
|
|
29690
|
+
text: childRequestText(request2),
|
|
29614
29691
|
receivedAt: deps.clock()
|
|
29615
29692
|
};
|
|
29616
29693
|
let startedId;
|
|
@@ -29647,7 +29724,7 @@ async function spawnChild(deps, parent, request) {
|
|
|
29647
29724
|
if (first.kind === "started" || startedId !== void 0) {
|
|
29648
29725
|
const runId3 = first.kind === "started" ? first.id : startedId;
|
|
29649
29726
|
console.log(
|
|
29650
|
-
`[spawn] ${parent.msg.threadKey} run ${parent.runId} spawned ${
|
|
29727
|
+
`[spawn] ${parent.msg.threadKey} run ${parent.runId} spawned ${request2.preset} run ${runId3} in ${child.threadKey}`
|
|
29651
29728
|
);
|
|
29652
29729
|
return {
|
|
29653
29730
|
kind: "spawned",
|
|
@@ -29658,7 +29735,7 @@ async function spawnChild(deps, parent, request) {
|
|
|
29658
29735
|
}
|
|
29659
29736
|
if (first.kind === "threw") return refused6("spawn_failed", describe3(first.err));
|
|
29660
29737
|
const reason2 = first.outcome.refusal ?? "spawn_failed";
|
|
29661
|
-
console.log(`[spawn] ${parent.msg.threadKey} run ${parent.runId}: ${
|
|
29738
|
+
console.log(`[spawn] ${parent.msg.threadKey} run ${parent.runId}: ${request2.preset} child not started (${reason2})`);
|
|
29662
29739
|
return refused6(reason2, lastReply ?? `the child ended (${first.outcome.status}) before it started`);
|
|
29663
29740
|
}
|
|
29664
29741
|
function spawnCapabilityFor(deps, parent) {
|
|
@@ -29672,8 +29749,8 @@ function spawnCapabilityFor(deps, parent) {
|
|
|
29672
29749
|
};
|
|
29673
29750
|
let previous = Promise.resolve();
|
|
29674
29751
|
return {
|
|
29675
|
-
spawn: (
|
|
29676
|
-
const turn = previous.then(() => spawnChild(withHook, { ...parent, ...at },
|
|
29752
|
+
spawn: (request2, at) => {
|
|
29753
|
+
const turn = previous.then(() => spawnChild(withHook, { ...parent, ...at }, request2));
|
|
29677
29754
|
previous = turn.catch(() => {
|
|
29678
29755
|
});
|
|
29679
29756
|
return turn;
|
|
@@ -29898,6 +29975,14 @@ async function nextTick(wait, ids, ms, signal2) {
|
|
|
29898
29975
|
signal2?.removeEventListener("abort", end);
|
|
29899
29976
|
}
|
|
29900
29977
|
}
|
|
29978
|
+
function endsAwaited(watch) {
|
|
29979
|
+
return new Set(
|
|
29980
|
+
watch.pending().map((id) => {
|
|
29981
|
+
const state = watch.get(id);
|
|
29982
|
+
return state?.kind === "running" && state.continuedBy !== void 0 ? state.continuedBy : id;
|
|
29983
|
+
})
|
|
29984
|
+
);
|
|
29985
|
+
}
|
|
29901
29986
|
var UNAVAILABLE2, NOT_FOUND, CHILD_PRESETS, WRITE_PRESETS, REPO_PRESETS, CHILDREN_PAGES_MAX, spawnRunTool, sendToRunTool, AWAIT_IDS_MAX, awaitRunsTool, listRunsTool, getRunStatusTool, RUN_TOOLS;
|
|
29902
29987
|
var init_runs2 = __esm({
|
|
29903
29988
|
"../../src/tools/runs.ts"() {
|
|
@@ -29943,17 +30028,17 @@ var init_runs2 = __esm({
|
|
|
29943
30028
|
return `error: unknown preset "${preset}" \u2014 one of ${CHILD_PRESETS().join(", ")}`;
|
|
29944
30029
|
const prompt = String(input.prompt ?? "").trim();
|
|
29945
30030
|
if (!prompt) return "error: prompt is required \u2014 what the child should do, after this conversation's text";
|
|
29946
|
-
const
|
|
29947
|
-
if (input.repo !== void 0 && String(input.repo).trim())
|
|
30031
|
+
const request2 = { preset, prompt };
|
|
30032
|
+
if (input.repo !== void 0 && String(input.repo).trim()) request2.repo = String(input.repo).trim();
|
|
29948
30033
|
if (input.budget !== void 0) {
|
|
29949
30034
|
const budget = input.budget;
|
|
29950
30035
|
if (typeof budget !== "number" || !Number.isInteger(budget) || budget < MIN_BOUNDARY_MINUTES)
|
|
29951
30036
|
return `error: budget takes a whole number of minutes, at least ${MIN_BOUNDARY_MINUTES}`;
|
|
29952
|
-
|
|
30037
|
+
request2.budget = budget;
|
|
29953
30038
|
}
|
|
29954
30039
|
const spawn7 = ctx.spawn ?? nullSpawnCapability;
|
|
29955
30040
|
const conversation = ctx.conversation ? await ctx.conversation() : void 0;
|
|
29956
|
-
const out = await spawn7.spawn(
|
|
30041
|
+
const out = await spawn7.spawn(request2, {
|
|
29957
30042
|
remainingMs: ctx.remainingMs?.() ?? Number.POSITIVE_INFINITY,
|
|
29958
30043
|
...conversation ? { conversation } : {}
|
|
29959
30044
|
});
|
|
@@ -30063,7 +30148,7 @@ var init_runs2 = __esm({
|
|
|
30063
30148
|
});
|
|
30064
30149
|
}
|
|
30065
30150
|
const ms = Math.min(AWAIT_POLL_MS, Math.max(0, decision.until - now));
|
|
30066
|
-
await nextTick(wait,
|
|
30151
|
+
await nextTick(wait, endsAwaited(watch), ms, ctx.signal);
|
|
30067
30152
|
}
|
|
30068
30153
|
}
|
|
30069
30154
|
};
|
|
@@ -30593,13 +30678,14 @@ ${skill.body}${footer}`;
|
|
|
30593
30678
|
});
|
|
30594
30679
|
|
|
30595
30680
|
// ../../src/tools/status.ts
|
|
30596
|
-
var updateStatusTool;
|
|
30681
|
+
var STATUS_RESULT, updateStatusTool;
|
|
30597
30682
|
var init_status2 = __esm({
|
|
30598
30683
|
"../../src/tools/status.ts"() {
|
|
30599
30684
|
"use strict";
|
|
30685
|
+
STATUS_RESULT = "status updated \u2014 keep it truthful: \u2713 only for items whose result you have already read, \u2731 on the item whose command runs next.";
|
|
30600
30686
|
updateStatusTool = {
|
|
30601
30687
|
name: "update_status",
|
|
30602
|
-
description: "Update the short user-facing status checklist shown while you work. Call it right after planning (all items pending) and again whenever an item's state changes. Format: one item per line, prefixed with a state marker: \u2713 done, \u2731 in progress, \u25CB pending. Keep it to 3-6 short outcome-oriented items (what, not how \u2014 never raw commands).",
|
|
30688
|
+
description: "Update the short user-facing status checklist shown while you work. Call it right after planning (all items pending) and again whenever an item's state changes. Format: one item per line, prefixed with a state marker: \u2713 done, \u2731 in progress, \u25CB pending. The markers are facts, not intentions: mark an item \u2731 when you issue the first command that does it, and \u2713 only after you have read its result \u2014 never in the same turn as the command, never because you plan to run it next. The card shows the command running right now beside this list, so the two must agree. Keep it to 3-6 short outcome-oriented items (what, not how \u2014 never raw commands).",
|
|
30603
30689
|
inputSchema: {
|
|
30604
30690
|
type: "object",
|
|
30605
30691
|
properties: {
|
|
@@ -30612,7 +30698,7 @@ var init_status2 = __esm({
|
|
|
30612
30698
|
},
|
|
30613
30699
|
async run(input, ctx) {
|
|
30614
30700
|
ctx.reportProgress?.(String(input.checklist ?? ""));
|
|
30615
|
-
return
|
|
30701
|
+
return STATUS_RESULT;
|
|
30616
30702
|
}
|
|
30617
30703
|
};
|
|
30618
30704
|
}
|
|
@@ -31185,7 +31271,7 @@ var init_submit = __esm({
|
|
|
31185
31271
|
return "no run is recording a handoff here \u2014 it was not recorded (it applies to a coding run started for a plan unit)";
|
|
31186
31272
|
ctx.onHandoff(parsed.handoff);
|
|
31187
31273
|
const h = parsed.handoff;
|
|
31188
|
-
const count = (n2,
|
|
31274
|
+
const count = (n2, one2, many) => `${n2} ${n2 === 1 ? one2 : many}`;
|
|
31189
31275
|
return `handoff recorded: ${count(h.deviations.length, "deviation", "deviations")}, ${count(h.followUps.length, "follow-up", "follow-ups")}, ${h.unproven.length} unproven; a later call replaces this one`;
|
|
31190
31276
|
}
|
|
31191
31277
|
};
|
|
@@ -31690,8 +31776,8 @@ async function routeRequest(deps, ctx) {
|
|
|
31690
31776
|
return { kind: "routed", resolved: resolveRouted(deps, msg, directives, sticky, carried.preset), route: carried };
|
|
31691
31777
|
}
|
|
31692
31778
|
if (!routingOn(cfg) || agentSource !== "default" || threadLive) return { kind: "unrouted" };
|
|
31693
|
-
const
|
|
31694
|
-
if (!
|
|
31779
|
+
const modelRef3 = cfg.routing?.model ?? cfg.defaults.models["general"];
|
|
31780
|
+
if (!modelRef3) {
|
|
31695
31781
|
console.log(`[route] ${msg.threadKey} not routed: no routing.model and no defaults.models.general`);
|
|
31696
31782
|
return { kind: "unrouted" };
|
|
31697
31783
|
}
|
|
@@ -31699,7 +31785,7 @@ async function routeRequest(deps, ctx) {
|
|
|
31699
31785
|
try {
|
|
31700
31786
|
if (deps.routeModel) model = deps.routeModel;
|
|
31701
31787
|
else {
|
|
31702
|
-
const ref = parseModelRef(
|
|
31788
|
+
const ref = parseModelRef(modelRef3);
|
|
31703
31789
|
model = providerRouteModel(deps.completions.get(ref.provider), ref.model, {
|
|
31704
31790
|
...cfg.routing?.answer ? { answer: cfg.routing.answer } : {}
|
|
31705
31791
|
});
|
|
@@ -31741,15 +31827,15 @@ async function routeRequest(deps, ctx) {
|
|
|
31741
31827
|
)
|
|
31742
31828
|
);
|
|
31743
31829
|
if (decision.preset === void 0 && decision.command && ctx.command && deps.commands)
|
|
31744
|
-
return answerCommand(deps.commands, ctx.command, msg, decision.command,
|
|
31830
|
+
return answerCommand(deps.commands, ctx.command, msg, decision.command, modelRef3, root);
|
|
31745
31831
|
if (decision.preset === void 0) {
|
|
31746
31832
|
console.log(`[route] ${msg.threadKey} not routed (${decision.reason}) \u2014 running ${cfg.defaults.agent}`);
|
|
31747
|
-
return decision.compoundRejected ? { kind: "unrouted", rejected: { preset: cfg.defaults.agent, reason: decision.reason, model:
|
|
31833
|
+
return decision.compoundRejected ? { kind: "unrouted", rejected: { preset: cfg.defaults.agent, reason: decision.reason, model: modelRef3 } } : { kind: "unrouted" };
|
|
31748
31834
|
}
|
|
31749
31835
|
const resolved = resolveRouted(deps, msg, directives, sticky, decision.preset);
|
|
31750
31836
|
const { parts, collapsed } = decision;
|
|
31751
31837
|
console.log(
|
|
31752
|
-
`[route] ${msg.threadKey} routed to ${decision.preset} on ${
|
|
31838
|
+
`[route] ${msg.threadKey} routed to ${decision.preset} on ${modelRef3}${parts ? ` (${parts.length} parts)` : ""}${collapsed ? ` (compound collapsed: ${collapsed.presets.join("+")})` : ""}: ${decision.reason}`
|
|
31753
31839
|
);
|
|
31754
31840
|
return {
|
|
31755
31841
|
kind: "routed",
|
|
@@ -31757,13 +31843,13 @@ async function routeRequest(deps, ctx) {
|
|
|
31757
31843
|
route: {
|
|
31758
31844
|
preset: decision.preset,
|
|
31759
31845
|
reason: decision.reason,
|
|
31760
|
-
model:
|
|
31846
|
+
model: modelRef3,
|
|
31761
31847
|
...parts ? { parts } : {},
|
|
31762
31848
|
...collapsed ? { collapsed } : {}
|
|
31763
31849
|
}
|
|
31764
31850
|
};
|
|
31765
31851
|
}
|
|
31766
|
-
async function answerCommand(commands, branch2, msg, decided,
|
|
31852
|
+
async function answerCommand(commands, branch2, msg, decided, modelRef3, root) {
|
|
31767
31853
|
const { deps, io, ending, trace } = branch2;
|
|
31768
31854
|
const def = commands.get(decided.id);
|
|
31769
31855
|
if (!def) {
|
|
@@ -31773,7 +31859,7 @@ async function answerCommand(commands, branch2, msg, decided, modelRef2, root) {
|
|
|
31773
31859
|
const receipt = redactAndCap(chatInvocation(def, decided.input), ROUTE_RECEIPT_CAP);
|
|
31774
31860
|
const receiptLine = `${ROUTED_RECEIPT_PREFIX} ${receipt}`;
|
|
31775
31861
|
if (def.effect === "write") {
|
|
31776
|
-
console.log(`[route] ${msg.threadKey} handed back ${def.id} on ${
|
|
31862
|
+
console.log(`[route] ${msg.threadKey} handed back ${def.id} on ${modelRef3}: ${receipt}`);
|
|
31777
31863
|
await ending.sealAfterReply(
|
|
31778
31864
|
async () => {
|
|
31779
31865
|
},
|
|
@@ -31781,11 +31867,11 @@ async function answerCommand(commands, branch2, msg, decided, modelRef2, root) {
|
|
|
31781
31867
|
);
|
|
31782
31868
|
return { kind: "command", command: def.id, outcome: "hand_back" };
|
|
31783
31869
|
}
|
|
31784
|
-
console.log(`[route] ${msg.threadKey} routed to command ${def.id} on ${
|
|
31870
|
+
console.log(`[route] ${msg.threadKey} routed to command ${def.id} on ${modelRef3}: ${receipt}`);
|
|
31785
31871
|
const route3 = {
|
|
31786
31872
|
preset: COMMAND_RUN_AGENT,
|
|
31787
31873
|
reason: `command ${def.id}`,
|
|
31788
|
-
model:
|
|
31874
|
+
model: modelRef3,
|
|
31789
31875
|
command: def.id,
|
|
31790
31876
|
input: redactedInput(decided.input),
|
|
31791
31877
|
receipt
|
|
@@ -31814,13 +31900,13 @@ function redactedInput(input) {
|
|
|
31814
31900
|
return v === void 0 ? null : text(JSON.stringify(v) ?? String(v));
|
|
31815
31901
|
};
|
|
31816
31902
|
const leafOrList = (v) => Array.isArray(v) ? v.map(leaf) : leaf(v);
|
|
31817
|
-
const
|
|
31903
|
+
const isRecord9 = (v) => typeof v === "object" && v !== null && (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);
|
|
31818
31904
|
const entries = (v, of) => Object.fromEntries(
|
|
31819
31905
|
Object.entries(v).filter(([, x]) => x !== void 0).map(([k, x]) => [k, of(x)])
|
|
31820
31906
|
);
|
|
31821
31907
|
const object1 = (v) => entries(v, leafOrList);
|
|
31822
|
-
const object2 = (v) => entries(v, (x) =>
|
|
31823
|
-
const object3 = (v) => entries(v, (x) =>
|
|
31908
|
+
const object2 = (v) => entries(v, (x) => isRecord9(x) ? object1(x) : leafOrList(x));
|
|
31909
|
+
const object3 = (v) => entries(v, (x) => isRecord9(x) ? object2(x) : leafOrList(x));
|
|
31824
31910
|
return {
|
|
31825
31911
|
...input.args ? { args: input.args.map(leaf) } : {},
|
|
31826
31912
|
...input.options ? { options: object3(input.options) } : {}
|
|
@@ -33443,10 +33529,12 @@ async function registerRun(deps, ctx) {
|
|
|
33443
33529
|
agentSource,
|
|
33444
33530
|
model: resolved.modelRef,
|
|
33445
33531
|
traceId: root.traceId,
|
|
33446
|
-
// The harness the run is driven by (harness.md
|
|
33447
|
-
//
|
|
33448
|
-
//
|
|
33449
|
-
|
|
33532
|
+
// The harness the run is driven by (harness.md items 8 and 10): the
|
|
33533
|
+
// object the preset's configuration word picks off the roster, by its
|
|
33534
|
+
// own name, so a record can be told from another harness's; none in a
|
|
33535
|
+
// process that starts no run. A resumed run publishes no meta (above),
|
|
33536
|
+
// and its harness is its row's, not the word's.
|
|
33537
|
+
...deps.harness ? { harness: harnessForPreset(deps.harness.harnesses, deps.config.config.harness, agent.name).name } : {},
|
|
33450
33538
|
...resolved.effort !== void 0 ? { effort: resolved.effort } : {},
|
|
33451
33539
|
...repoCtx2.repo !== void 0 ? { repo: repoCtx2.repo } : {},
|
|
33452
33540
|
...repoCtx2.ref !== void 0 ? { ref: repoCtx2.ref } : {},
|
|
@@ -33766,6 +33854,7 @@ var init_provision = __esm({
|
|
|
33766
33854
|
init_reply();
|
|
33767
33855
|
init_messages();
|
|
33768
33856
|
init_route();
|
|
33857
|
+
init_roster();
|
|
33769
33858
|
STATUS_UPDATE_MIN_MS = 3e3;
|
|
33770
33859
|
}
|
|
33771
33860
|
});
|
|
@@ -35788,6 +35877,12 @@ function reachFor(identity2) {
|
|
|
35788
35877
|
if (identity2 === "read") return READ_REACH;
|
|
35789
35878
|
return NONE_REACH;
|
|
35790
35879
|
}
|
|
35880
|
+
function openCodeToolWord(action) {
|
|
35881
|
+
const mapped = OPENCODE_ACTION_TO_TOOL_WORD[action];
|
|
35882
|
+
if (mapped !== void 0) return mapped;
|
|
35883
|
+
const underscore = action.indexOf("_");
|
|
35884
|
+
return underscore > 0 ? action.slice(underscore + 1) : action;
|
|
35885
|
+
}
|
|
35791
35886
|
function judgeToolCall(tool, input, ctx) {
|
|
35792
35887
|
const bundle = PI_TOOL_BUNDLES[tool];
|
|
35793
35888
|
if (bundle === void 0) return outside(`${tool} is not in any bundle the ${ctx.identity} identity reaches`);
|
|
@@ -35851,7 +35946,7 @@ function judgePath(path, ctx) {
|
|
|
35851
35946
|
if (rel.startsWith("..") || isAbsolute5(rel)) return refused3(`path \u2014 \`${path}\` resolves outside the checkout`);
|
|
35852
35947
|
return allowed;
|
|
35853
35948
|
}
|
|
35854
|
-
var PI_TOOL_BUNDLES, CODING_REACH, READ_REACH, NONE_REACH, allowed, refused3, outside, isRecord6, CREDENTIAL_FILE, ENV_DUMP, CREDENTIAL_VAR, MERGE_OR_APPROVE, GIT_THEN_PUSH, GIT_PUSH, ANY_PUSH, GH_WRITE_VERB, WRITE_METHOD, GH_API_WRITE, CURL_GITHUB_WRITE;
|
|
35949
|
+
var PI_TOOL_BUNDLES, CODING_REACH, READ_REACH, NONE_REACH, allowed, refused3, outside, isRecord6, CREDENTIAL_FILE, ENV_DUMP, CREDENTIAL_VAR, MERGE_OR_APPROVE, GIT_THEN_PUSH, GIT_PUSH, ANY_PUSH, GH_WRITE_VERB, WRITE_METHOD, GH_API_WRITE, CURL_GITHUB_WRITE, OPENCODE_ACTION_TO_TOOL_WORD;
|
|
35855
35950
|
var init_toolRules = __esm({
|
|
35856
35951
|
"../../src/core/harness/pi/toolRules.ts"() {
|
|
35857
35952
|
"use strict";
|
|
@@ -35896,6 +35991,19 @@ var init_toolRules = __esm({
|
|
|
35896
35991
|
CURL_GITHUB_WRITE = new RegExp(
|
|
35897
35992
|
`\\bcurl\\b(?=[^;&|]*api\\.github\\.com)(?=[^;&|]*(?:\\s(?:-X|--request)[\\s=]*${WRITE_METHOD}\\b|\\s(?:-d|--data(?:-\\w+)?|--json|-T|--upload-file)[\\s=]))`
|
|
35898
35993
|
);
|
|
35994
|
+
OPENCODE_ACTION_TO_TOOL_WORD = {
|
|
35995
|
+
shell: "bash",
|
|
35996
|
+
edit: "edit",
|
|
35997
|
+
// OpenCode's `write` and `patch` tools both assert the `edit` action (their
|
|
35998
|
+
// `permission: "edit"`), so `write` is never an action here; a call to the
|
|
35999
|
+
// write tool arrives as `edit`.
|
|
36000
|
+
patch: "edit",
|
|
36001
|
+
read: "read",
|
|
36002
|
+
glob: "find",
|
|
36003
|
+
grep: "grep",
|
|
36004
|
+
webfetch: "web_fetch",
|
|
36005
|
+
websearch: "web_search"
|
|
36006
|
+
};
|
|
35899
36007
|
}
|
|
35900
36008
|
});
|
|
35901
36009
|
|
|
@@ -36667,7 +36775,7 @@ async function runPiHarnessOpen(deps, run2) {
|
|
|
36667
36775
|
requeueUnechoed = () => {
|
|
36668
36776
|
run2.inbox?.requeue(unechoed.splice(0).flatMap((u) => u.inputs));
|
|
36669
36777
|
};
|
|
36670
|
-
const
|
|
36778
|
+
const drainFollowUps2 = () => {
|
|
36671
36779
|
const inputs = run2.inbox?.drain() ?? [];
|
|
36672
36780
|
if (inputs.length === 0) return;
|
|
36673
36781
|
const images = inputs.flatMap(
|
|
@@ -36749,7 +36857,7 @@ ${stagedLine}` : prompt;
|
|
|
36749
36857
|
note("wrap_up", wrapUpNote(minutesLeft));
|
|
36750
36858
|
transport.send({ type: "steer", message: wrapUpInstruction(minutesLeft) });
|
|
36751
36859
|
}
|
|
36752
|
-
|
|
36860
|
+
drainFollowUps2();
|
|
36753
36861
|
};
|
|
36754
36862
|
const iterator = transport.lines[Symbol.asyncIterator]();
|
|
36755
36863
|
let pending2;
|
|
@@ -38118,6 +38226,16 @@ async function runLoop(deps, ctx) {
|
|
|
38118
38226
|
const postedBefore = resume ? reviewPostedBefore(resume.events) : void 0;
|
|
38119
38227
|
if (postedBefore) reviewHead = postedBefore.head;
|
|
38120
38228
|
const facts2 = resume ? harnessFactsOf(resume.row.state.harness) : void 0;
|
|
38229
|
+
const harnessOf = (roster) => facts2 ? roster.harnesses[facts2.harness] : harnessForPreset(roster.harnesses, deps.config.config.harness, agent.name);
|
|
38230
|
+
const unknownWord = facts2 === void 0 ? unknownHarnessWordOf(resume?.row.state.harness) : void 0;
|
|
38231
|
+
const noteUnknownWord = (ending2) => {
|
|
38232
|
+
if (unknownWord === void 0) return;
|
|
38233
|
+
onEvent({
|
|
38234
|
+
type: "run_note",
|
|
38235
|
+
kind: "resumed",
|
|
38236
|
+
summary: `the row names the ${unknownWord.word} harness, which this build does not know; its process (pid ${unknownWord.pid} in container ${unknownWord.container}) was neither judged nor ended here; ${ending2}`
|
|
38237
|
+
});
|
|
38238
|
+
};
|
|
38121
38239
|
let lastFacts = facts2;
|
|
38122
38240
|
const saveFacts = (h) => {
|
|
38123
38241
|
lastFacts = h;
|
|
@@ -38134,10 +38252,11 @@ async function runLoop(deps, ctx) {
|
|
|
38134
38252
|
if (facts2) {
|
|
38135
38253
|
if (!deps.harness)
|
|
38136
38254
|
throw new Error(
|
|
38137
|
-
`the ${agent.name} preset's run left ${facts2.harness} harness facts on its row, but this process has no harness
|
|
38255
|
+
`the ${agent.name} preset's run left ${facts2.harness} harness facts on its row, but this process has no harness roster to end that process with`
|
|
38138
38256
|
);
|
|
38257
|
+
const judge3 = harnessOf(deps.harness);
|
|
38139
38258
|
const container = deps.harness.containerFor?.(executor, profile.machine) ?? harnessContainerFor(executor, profile.machine);
|
|
38140
|
-
const found = await
|
|
38259
|
+
const found = await judge3.find(facts2, container).catch((err2) => {
|
|
38141
38260
|
if (!isContainerGone(err2)) throw err2;
|
|
38142
38261
|
return "gone";
|
|
38143
38262
|
});
|
|
@@ -38162,29 +38281,32 @@ async function runLoop(deps, ctx) {
|
|
|
38162
38281
|
onEvent({
|
|
38163
38282
|
type: "run_note",
|
|
38164
38283
|
kind: "resumed",
|
|
38165
|
-
summary: `the run's row carries ${facts2.harness} harness facts (pid ${facts2.pid})
|
|
38284
|
+
summary: `the run's row carries ${facts2.harness} harness facts (pid ${facts2.pid}) that the ${judge3.name} harness does not own: that process was neither judged nor ended here`
|
|
38166
38285
|
});
|
|
38167
38286
|
break;
|
|
38168
38287
|
default:
|
|
38169
|
-
await
|
|
38288
|
+
await judge3.end(facts2, container);
|
|
38170
38289
|
}
|
|
38171
|
-
}
|
|
38290
|
+
} else noteUnknownWord("the run finished on the answer it already had");
|
|
38172
38291
|
answer = answerUnderEnding(finish2.answer, loopEnding, agent.maxMinutes);
|
|
38173
38292
|
} else {
|
|
38174
38293
|
if (!deps.harness)
|
|
38175
|
-
throw new Error(`the ${agent.name} preset runs on
|
|
38294
|
+
throw new Error(`the ${agent.name} preset runs on a harness, but this process has no harness roster`);
|
|
38176
38295
|
const harnessDeps = deps.harness;
|
|
38296
|
+
const harness = harnessOf(harnessDeps);
|
|
38297
|
+
noteUnknownWord(`the run was rebuilt on ${harness.name}`);
|
|
38177
38298
|
const onBotHost = profile.machine === "none";
|
|
38178
38299
|
const harnessUrl = onBotHost ? deps.harness.loopbackUrl : deps.harness.harnessUrl;
|
|
38179
38300
|
if (!harnessUrl)
|
|
38180
38301
|
throw new Error(
|
|
38181
|
-
onBotHost ?
|
|
38302
|
+
onBotHost ? `the ${harness.name} harness needs PORT: a run without a workspace runs its process on the bot host, which reaches the model proxy over loopback` : `the ${harness.name} harness needs PUBLIC_BASE_URL: the run's container reaches the model proxy through it`
|
|
38182
38303
|
);
|
|
38183
38304
|
if (ctx.bearer === void 0)
|
|
38184
|
-
throw new Error(
|
|
38305
|
+
throw new Error(`the ${harness.name} harness needs the run's model-proxy bearer, and this process minted none`);
|
|
38185
38306
|
const { provider: providerName, model: modelId } = parseModelRef(resolved.modelRef);
|
|
38186
38307
|
const providerCfg = deps.config.config.providers[providerName];
|
|
38187
|
-
if (!providerCfg)
|
|
38308
|
+
if (!providerCfg)
|
|
38309
|
+
throw new Error(`the ${harness.name} harness found no provider named ${providerName} in the config`);
|
|
38188
38310
|
const prBase = repoCtx.baseRef ?? await coordinatorBase;
|
|
38189
38311
|
const ownBranch = prBase !== void 0 ? binding?.ref ?? repoCtx.ref : void 0;
|
|
38190
38312
|
const protectedBranches = [
|
|
@@ -38205,7 +38327,7 @@ async function runLoop(deps, ctx) {
|
|
|
38205
38327
|
...facts2 ? { facts: facts2 } : {}
|
|
38206
38328
|
} : void 0;
|
|
38207
38329
|
const openRun = () => openThroughSeam(
|
|
38208
|
-
|
|
38330
|
+
harness,
|
|
38209
38331
|
{
|
|
38210
38332
|
container: harnessDeps.containerFor?.(executor, profile.machine) ?? harnessContainerFor(executor, profile.machine),
|
|
38211
38333
|
bearer,
|
|
@@ -38260,7 +38382,7 @@ async function runLoop(deps, ctx) {
|
|
|
38260
38382
|
repoCtx,
|
|
38261
38383
|
root,
|
|
38262
38384
|
clock: clock4,
|
|
38263
|
-
harness
|
|
38385
|
+
harness,
|
|
38264
38386
|
replaced: err2,
|
|
38265
38387
|
facts: lastFacts,
|
|
38266
38388
|
binding: workspaceBinding,
|
|
@@ -38589,6 +38711,16 @@ function answerUnderEnding(text, ending, maxMinutes) {
|
|
|
38589
38711
|
${text}` : `\u26A0\uFE0F ${ending.summary}`;
|
|
38590
38712
|
}
|
|
38591
38713
|
}
|
|
38714
|
+
function unknownHarnessWordOf(state) {
|
|
38715
|
+
if (typeof state !== "object" || state === null) return void 0;
|
|
38716
|
+
const { harness, pid, container } = state;
|
|
38717
|
+
if (typeof harness !== "string") return void 0;
|
|
38718
|
+
return {
|
|
38719
|
+
word: harness,
|
|
38720
|
+
pid: typeof pid === "number" ? String(pid) : "unknown",
|
|
38721
|
+
container: typeof container === "string" ? container : "unknown"
|
|
38722
|
+
};
|
|
38723
|
+
}
|
|
38592
38724
|
var init_runLoop = __esm({
|
|
38593
38725
|
"../../src/core/dispatch/runLoop.ts"() {
|
|
38594
38726
|
"use strict";
|
|
@@ -38598,6 +38730,7 @@ var init_runLoop = __esm({
|
|
|
38598
38730
|
init_toolsets();
|
|
38599
38731
|
init_contract4();
|
|
38600
38732
|
init_relaunch();
|
|
38733
|
+
init_roster();
|
|
38601
38734
|
init_botHostContainer();
|
|
38602
38735
|
init_factory();
|
|
38603
38736
|
init_container();
|
|
@@ -38799,8 +38932,8 @@ async function plan(deps, input) {
|
|
|
38799
38932
|
label: input.label
|
|
38800
38933
|
};
|
|
38801
38934
|
const taskText = shipTaskText(input.requestText, entry.repo);
|
|
38802
|
-
const
|
|
38803
|
-
if (
|
|
38935
|
+
const request2 = parseShipPlanRequest(taskText);
|
|
38936
|
+
if (request2 === void 0) {
|
|
38804
38937
|
const text2 = taskText || "Implement the task this thread's ship request describes.";
|
|
38805
38938
|
const planId2 = generatedPlanId(text2, msg.threadKey);
|
|
38806
38939
|
const graph2 = {
|
|
@@ -38826,38 +38959,38 @@ async function plan(deps, input) {
|
|
|
38826
38959
|
if (input.agentSource === "route")
|
|
38827
38960
|
return {
|
|
38828
38961
|
ok: false,
|
|
38829
|
-
reply: `\u{1F6AB} A routed request never runs a seeded plan \u2014 its units would merge under the runner's grant. Type \`agent:ship plan ${
|
|
38962
|
+
reply: `\u{1F6AB} A routed request never runs a seeded plan \u2014 its units would merge under the runner's grant. Type \`agent:ship plan ${request2.planPath}\` to run it.`
|
|
38830
38963
|
};
|
|
38831
38964
|
let planId;
|
|
38832
38965
|
try {
|
|
38833
|
-
planId = planIdOf(
|
|
38966
|
+
planId = planIdOf(request2.planPath);
|
|
38834
38967
|
} catch (err2) {
|
|
38835
38968
|
return { ok: false, reply: `\u{1F6AB} ${describe6(err2)}` };
|
|
38836
38969
|
}
|
|
38837
38970
|
let text;
|
|
38838
38971
|
try {
|
|
38839
|
-
text = (await deps.readFile(entry.repo,
|
|
38972
|
+
text = (await deps.readFile(entry.repo, request2.planPath, base)).content;
|
|
38840
38973
|
} catch (err2) {
|
|
38841
38974
|
return {
|
|
38842
38975
|
ok: false,
|
|
38843
|
-
reply: `\u{1F6AB} The plan \`${
|
|
38976
|
+
reply: `\u{1F6AB} The plan \`${request2.planPath}\` could not be read at \`${base}\` in ${entry.repo}: ${describe6(err2)}`
|
|
38844
38977
|
};
|
|
38845
38978
|
}
|
|
38846
38979
|
const graph = parsePlanGraph(text, planId);
|
|
38847
38980
|
if (graph.units.length === 0)
|
|
38848
38981
|
return {
|
|
38849
38982
|
ok: false,
|
|
38850
|
-
reply: `\u{1F6AB} The plan \`${
|
|
38983
|
+
reply: `\u{1F6AB} The plan \`${request2.planPath}\` has no unit headings (\`### U<n>. <title>\`) \u2014 nothing to run.`
|
|
38851
38984
|
};
|
|
38852
38985
|
let selected;
|
|
38853
38986
|
try {
|
|
38854
|
-
selected = openPlanCursor(graph,
|
|
38987
|
+
selected = openPlanCursor(graph, request2.units).order;
|
|
38855
38988
|
} catch (err2) {
|
|
38856
38989
|
return { ok: false, reply: `\u{1F6AB} ${describe6(err2)}` };
|
|
38857
38990
|
}
|
|
38858
38991
|
return {
|
|
38859
38992
|
ok: true,
|
|
38860
|
-
planned: { planId, path:
|
|
38993
|
+
planned: { planId, path: request2.planPath, base, graph, selected, identity: identity2, merge: "runner" }
|
|
38861
38994
|
};
|
|
38862
38995
|
}
|
|
38863
38996
|
function rowsFor(p, selected, instanceId, lastPushOf) {
|
|
@@ -39046,6 +39179,19 @@ function readCreateInstanceAnswer(status3, text) {
|
|
|
39046
39179
|
const detail = typeof body?.error === "string" ? body.error : text.slice(0, 200);
|
|
39047
39180
|
return { kind: "unanswered", reason: `HTTP ${status3} \u2014 ${detail}` };
|
|
39048
39181
|
}
|
|
39182
|
+
function readSendEventAnswer(status3, text) {
|
|
39183
|
+
let parsed;
|
|
39184
|
+
try {
|
|
39185
|
+
parsed = JSON.parse(text);
|
|
39186
|
+
} catch {
|
|
39187
|
+
parsed = void 0;
|
|
39188
|
+
}
|
|
39189
|
+
const body = typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
39190
|
+
if (status3 === 200 && body?.ok === true && body.sent === true) return { kind: "sent" };
|
|
39191
|
+
if (status3 === 404 && body?.error === "no_instance") return { kind: "absent" };
|
|
39192
|
+
const detail = typeof body?.error === "string" ? body.error : text.slice(0, 200);
|
|
39193
|
+
return { kind: "unanswered", reason: `HTTP ${status3} \u2014 ${detail}` };
|
|
39194
|
+
}
|
|
39049
39195
|
function readInstanceStatusAnswer(status3, text) {
|
|
39050
39196
|
let parsed;
|
|
39051
39197
|
try {
|
|
@@ -39060,13 +39206,14 @@ function readInstanceStatusAnswer(status3, text) {
|
|
|
39060
39206
|
const detail = typeof body?.error === "string" ? body.error : text.slice(0, 200);
|
|
39061
39207
|
return { kind: "unanswered", reason: `HTTP ${status3} \u2014 ${detail}` };
|
|
39062
39208
|
}
|
|
39063
|
-
var COORDINATOR_INSTANCES_PATH, COORDINATOR_INSTANCE_STATUS_PREFIX;
|
|
39209
|
+
var COORDINATOR_INSTANCES_PATH, COORDINATOR_INSTANCE_STATUS_PREFIX, COORDINATOR_INSTANCE_EVENTS_SUFFIX;
|
|
39064
39210
|
var init_instancesRoute = __esm({
|
|
39065
39211
|
"../../src/core/coordinator/instancesRoute.ts"() {
|
|
39066
39212
|
"use strict";
|
|
39067
39213
|
init_contract2();
|
|
39068
39214
|
COORDINATOR_INSTANCES_PATH = "/admin/coordinator/instances";
|
|
39069
39215
|
COORDINATOR_INSTANCE_STATUS_PREFIX = `${COORDINATOR_INSTANCES_PATH}/`;
|
|
39216
|
+
COORDINATOR_INSTANCE_EVENTS_SUFFIX = "/events";
|
|
39070
39217
|
}
|
|
39071
39218
|
});
|
|
39072
39219
|
|
|
@@ -39112,6 +39259,29 @@ async function fetchInstanceStatusViaShim(opts, id) {
|
|
|
39112
39259
|
return { kind: "unanswered", reason: unreachable(err2) };
|
|
39113
39260
|
}
|
|
39114
39261
|
}
|
|
39262
|
+
function shimWorkflowSender(opts) {
|
|
39263
|
+
return {
|
|
39264
|
+
get: (id) => Promise.resolve({
|
|
39265
|
+
async sendEvent(event) {
|
|
39266
|
+
const at = shimAddress(opts);
|
|
39267
|
+
if ("reason" in at) throw new Error(at.reason);
|
|
39268
|
+
const fetchImpl = opts.fetch ?? fetch;
|
|
39269
|
+
const res = await fetchImpl(
|
|
39270
|
+
`${at.base}${COORDINATOR_INSTANCE_STATUS_PREFIX}${encodeURIComponent(id)}${COORDINATOR_INSTANCE_EVENTS_SUFFIX}`,
|
|
39271
|
+
{
|
|
39272
|
+
method: "POST",
|
|
39273
|
+
headers: { authorization: `Bearer ${at.bearer}`, "content-type": "application/json" },
|
|
39274
|
+
body: JSON.stringify(event),
|
|
39275
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
39276
|
+
}
|
|
39277
|
+
);
|
|
39278
|
+
const answer = readSendEventAnswer(res.status, await res.text().catch(() => ""));
|
|
39279
|
+
if (answer.kind === "sent") return;
|
|
39280
|
+
throw new Error(answer.kind === "absent" ? `no such instance: ${id}` : answer.reason);
|
|
39281
|
+
}
|
|
39282
|
+
})
|
|
39283
|
+
};
|
|
39284
|
+
}
|
|
39115
39285
|
var DEFAULT_TIMEOUT_MS, unreachable;
|
|
39116
39286
|
var init_instancesClient = __esm({
|
|
39117
39287
|
"../../src/core/coordinator/instancesClient.ts"() {
|
|
@@ -39201,10 +39371,10 @@ async function runShipBranch(deps, msg, io, ctx) {
|
|
|
39201
39371
|
...msg.channelName ? { channel: msg.channelName } : {},
|
|
39202
39372
|
...msg.userName ? { user: msg.userName } : {}
|
|
39203
39373
|
};
|
|
39204
|
-
const
|
|
39374
|
+
const request2 = humanize ? humanizeMessageText(directives.text) : directives.text;
|
|
39205
39375
|
publishText(
|
|
39206
39376
|
"input",
|
|
39207
|
-
attachments ? `${
|
|
39377
|
+
attachments ? `${request2} ${attachments}` : request2,
|
|
39208
39378
|
Object.keys(source2).length > 0 ? source2 : void 0
|
|
39209
39379
|
);
|
|
39210
39380
|
registry.publish(run2.id, {
|
|
@@ -39527,18 +39697,18 @@ async function abandonLostWorkspace(ctx) {
|
|
|
39527
39697
|
return restored?.msg;
|
|
39528
39698
|
}
|
|
39529
39699
|
function prepareRestartTurn(deps, ctx) {
|
|
39530
|
-
const { request, pending: pending2, clock: clock4, restartOf } = ctx;
|
|
39700
|
+
const { request: request2, pending: pending2, clock: clock4, restartOf } = ctx;
|
|
39531
39701
|
const merged = mergeFollowUps(pending2);
|
|
39532
39702
|
const receivedAt = clock4();
|
|
39533
39703
|
const msg = {
|
|
39534
|
-
...
|
|
39704
|
+
...request2,
|
|
39535
39705
|
...merged ? {
|
|
39536
|
-
text: `${
|
|
39706
|
+
text: `${request2.text}
|
|
39537
39707
|
|
|
39538
39708
|
${merged.text}`,
|
|
39539
|
-
...merged.images ||
|
|
39540
|
-
...merged.documents ||
|
|
39541
|
-
...merged.staged ||
|
|
39709
|
+
...merged.images || request2.images ? { images: [...request2.images ?? [], ...merged.images ?? []] } : {},
|
|
39710
|
+
...merged.documents || request2.documents ? { documents: [...request2.documents ?? [], ...merged.documents ?? []] } : {},
|
|
39711
|
+
...merged.staged || request2.staged ? { staged: [...request2.staged ?? [], ...merged.staged ?? []] } : {}
|
|
39542
39712
|
} : {},
|
|
39543
39713
|
receivedAt,
|
|
39544
39714
|
originAt: void 0
|
|
@@ -39675,7 +39845,7 @@ var init_thread = __esm({
|
|
|
39675
39845
|
|
|
39676
39846
|
// ../../src/core/dispatch/seed.ts
|
|
39677
39847
|
function sessionSeed(input) {
|
|
39678
|
-
const { tail, previous, history, request, refusedRequests = [] } = input;
|
|
39848
|
+
const { tail, previous, history, request: request2, refusedRequests = [] } = input;
|
|
39679
39849
|
const { from, transcript } = tail;
|
|
39680
39850
|
if (from === 0 && transcript.turns === 0) return void 0;
|
|
39681
39851
|
const notes = [];
|
|
@@ -39750,7 +39920,7 @@ function sessionSeed(input) {
|
|
|
39750
39920
|
}
|
|
39751
39921
|
messages.push({
|
|
39752
39922
|
role: "user",
|
|
39753
|
-
content: requestContent(
|
|
39923
|
+
content: requestContent(request2.text, request2.images, request2.documents, request2.references)
|
|
39754
39924
|
});
|
|
39755
39925
|
return {
|
|
39756
39926
|
messages,
|
|
@@ -40247,7 +40417,7 @@ async function dispatch(deps, msg, io, opts = {}) {
|
|
|
40247
40417
|
if (attach.kind === "refused") return ended;
|
|
40248
40418
|
if (attach.kind === "reattach_refused") {
|
|
40249
40419
|
if (resume) {
|
|
40250
|
-
const
|
|
40420
|
+
const request2 = await abandonLostWorkspace({
|
|
40251
40421
|
msg,
|
|
40252
40422
|
io,
|
|
40253
40423
|
refuse: refuse3,
|
|
@@ -40261,7 +40431,7 @@ async function dispatch(deps, msg, io, opts = {}) {
|
|
|
40261
40431
|
ledgerRun,
|
|
40262
40432
|
why: attach.why
|
|
40263
40433
|
});
|
|
40264
|
-
if (
|
|
40434
|
+
if (request2) restartRequest = { request: request2, restartOf: resume.row.runId };
|
|
40265
40435
|
resumeRowClosed = true;
|
|
40266
40436
|
}
|
|
40267
40437
|
return ended;
|
|
@@ -40292,10 +40462,10 @@ async function dispatch(deps, msg, io, opts = {}) {
|
|
|
40292
40462
|
workspaceFiles.record(pulled);
|
|
40293
40463
|
line3 = attachmentsLine(pulled);
|
|
40294
40464
|
} else line3 = noWorkspaceLine(staged);
|
|
40295
|
-
const
|
|
40296
|
-
if (
|
|
40297
|
-
|
|
40298
|
-
{ type: "text", text:
|
|
40465
|
+
const request2 = messages[messages.length - 1];
|
|
40466
|
+
if (request2 && request2.role === "user" && line3) {
|
|
40467
|
+
request2.content = Array.isArray(request2.content) ? [...request2.content, { type: "text", text: line3 }] : [
|
|
40468
|
+
{ type: "text", text: request2.content },
|
|
40299
40469
|
{ type: "text", text: line3 }
|
|
40300
40470
|
];
|
|
40301
40471
|
}
|
|
@@ -41757,6 +41927,2214 @@ var init_piHarness = __esm({
|
|
|
41757
41927
|
}
|
|
41758
41928
|
});
|
|
41759
41929
|
|
|
41930
|
+
// ../../src/core/harness/opencode/dispositions.ts
|
|
41931
|
+
function openCodeDispositionOf(type) {
|
|
41932
|
+
if (type.startsWith(OPENCODE_RPC_EVENT_PREFIX)) return "folded";
|
|
41933
|
+
return OPENCODE_EVENT_DISPOSITION[type];
|
|
41934
|
+
}
|
|
41935
|
+
var OPENCODE_RPC_EVENT_PREFIX, OPENCODE_EVENT_DISPOSITION;
|
|
41936
|
+
var init_dispositions = __esm({
|
|
41937
|
+
"../../src/core/harness/opencode/dispositions.ts"() {
|
|
41938
|
+
"use strict";
|
|
41939
|
+
init_toolRules();
|
|
41940
|
+
OPENCODE_RPC_EVENT_PREFIX = "rpc.";
|
|
41941
|
+
OPENCODE_EVENT_DISPOSITION = {
|
|
41942
|
+
// The stream's own marker.
|
|
41943
|
+
"server.connected": "structure",
|
|
41944
|
+
// ── Session: the assistant's turn as the record speaks it ────────────────
|
|
41945
|
+
// The tool call and its result, in pi's tool words (the per-call mirror).
|
|
41946
|
+
"session.tool.called": "mapped",
|
|
41947
|
+
"session.tool.success": "mapped",
|
|
41948
|
+
"session.tool.failed": "mapped",
|
|
41949
|
+
// The model's prose as it lands: the narration beside a call (the `assistant`
|
|
41950
|
+
// event) or, for a text-only turn, the answer.
|
|
41951
|
+
"session.text.ended": "mapped",
|
|
41952
|
+
// The context OpenCode compacted, with the summary it wrote (the compaction row).
|
|
41953
|
+
"session.compaction.ended": "mapped",
|
|
41954
|
+
// ── Session: partial output the final record carries (folded) ────────────
|
|
41955
|
+
"session.text.delta": "folded",
|
|
41956
|
+
"session.reasoning.delta": "folded",
|
|
41957
|
+
"session.compaction.delta": "folded",
|
|
41958
|
+
"session.step.streamed": "folded",
|
|
41959
|
+
"session.tool.progress": "folded",
|
|
41960
|
+
"session.tool.input.started": "folded",
|
|
41961
|
+
"session.tool.input.delta": "folded",
|
|
41962
|
+
"session.tool.input.ended": "folded",
|
|
41963
|
+
// ── Session: the notes (a `run_note`) ────────────────────────────────────
|
|
41964
|
+
// The gate's ask and its answer; the bridge acts on these, and on the record
|
|
41965
|
+
// they are the `tool_refused` note a refusal writes (never a second line).
|
|
41966
|
+
"permission.asked": "note",
|
|
41967
|
+
"permission.replied": "note",
|
|
41968
|
+
"session.retry.scheduled": "note",
|
|
41969
|
+
"session.compaction.failed": "note",
|
|
41970
|
+
"session.execution.failed": "note",
|
|
41971
|
+
"session.step.failed": "note",
|
|
41972
|
+
// ── Session: the run's own shape, already recorded (structure) ───────────
|
|
41973
|
+
// A narration's start writes nothing (its end does); the reasoning is
|
|
41974
|
+
// dropped whole, as pi's mirror drops thinking blocks — the record carries
|
|
41975
|
+
// what the model said and was told, never its private replay material.
|
|
41976
|
+
"session.text.started": "structure",
|
|
41977
|
+
"session.reasoning.started": "structure",
|
|
41978
|
+
"session.reasoning.ended": "structure",
|
|
41979
|
+
"session.step.started": "structure",
|
|
41980
|
+
"session.step.ended": "structure",
|
|
41981
|
+
"session.usage.updated": "structure",
|
|
41982
|
+
"session.compaction.started": "structure",
|
|
41983
|
+
"session.created": "structure",
|
|
41984
|
+
"session.execution.started": "structure",
|
|
41985
|
+
"session.execution.succeeded": "structure",
|
|
41986
|
+
"session.execution.interrupted": "structure",
|
|
41987
|
+
"session.idle": "structure",
|
|
41988
|
+
"session.status": "structure",
|
|
41989
|
+
"session.inbox.delivered": "structure",
|
|
41990
|
+
"session.inbox.enqueued": "structure",
|
|
41991
|
+
"session.inbox.cancelled": "structure",
|
|
41992
|
+
"session.inbox.delivery.changed": "structure",
|
|
41993
|
+
"session.instructions.updated": "structure",
|
|
41994
|
+
"session.permissions.updated": "structure",
|
|
41995
|
+
"session.synthetic": "structure",
|
|
41996
|
+
"session.viewed": "structure",
|
|
41997
|
+
// ── Session: the kinds this run never causes (impossible) ────────────────
|
|
41998
|
+
// The custom agent is the one agent and the proxy the one model, chosen at
|
|
41999
|
+
// create; a switch mid-run would be someone else driving the session.
|
|
42000
|
+
"session.agent.selected": "impossible",
|
|
42001
|
+
"session.model.selected": "impossible",
|
|
42002
|
+
// The session is never moved, forked, renamed or deleted under the run.
|
|
42003
|
+
"session.moved": "impossible",
|
|
42004
|
+
"session.forked": "impossible",
|
|
42005
|
+
"session.renamed": "impossible",
|
|
42006
|
+
"session.deleted": "impossible",
|
|
42007
|
+
// The checkout is Switchboard's; OpenCode never reverts it (`snapshots: false`).
|
|
42008
|
+
"session.revert.staged": "impossible",
|
|
42009
|
+
"session.revert.cleared": "impossible",
|
|
42010
|
+
"session.revert.committed": "impossible",
|
|
42011
|
+
// The persistent shell and the skill activation are features this config leaves off.
|
|
42012
|
+
"session.shell.started": "impossible",
|
|
42013
|
+
"session.shell.ended": "impossible",
|
|
42014
|
+
"session.skill.activated": "impossible",
|
|
42015
|
+
// ── The server's global catalogues and status (structure) ────────────────
|
|
42016
|
+
"agent.updated": "structure",
|
|
42017
|
+
"catalog.updated": "structure",
|
|
42018
|
+
"command.updated": "structure",
|
|
42019
|
+
"config.updated": "structure",
|
|
42020
|
+
"filesystem.changed": "structure",
|
|
42021
|
+
"integration.updated": "structure",
|
|
42022
|
+
"mcp.resources.changed": "structure",
|
|
42023
|
+
"mcp.status.changed": "structure",
|
|
42024
|
+
"plugin.updated": "structure",
|
|
42025
|
+
"project.updated": "structure",
|
|
42026
|
+
"reference.updated": "structure",
|
|
42027
|
+
"skill.updated": "structure",
|
|
42028
|
+
"vcs.branch.updated": "structure",
|
|
42029
|
+
"worktree.resolved": "structure",
|
|
42030
|
+
"worktree.updated": "structure",
|
|
42031
|
+
// ── The server's features this run turns off (impossible) ────────────────
|
|
42032
|
+
"credential.switched": "impossible",
|
|
42033
|
+
"credential.updated": "impossible",
|
|
42034
|
+
"form.created": "impossible",
|
|
42035
|
+
"form.cancelled": "impossible",
|
|
42036
|
+
"form.replied": "impossible",
|
|
42037
|
+
"installation.update-available": "impossible",
|
|
42038
|
+
"installation.updated": "impossible",
|
|
42039
|
+
"models-dev.refreshed": "impossible",
|
|
42040
|
+
"persistent-pty.added": "impossible",
|
|
42041
|
+
"persistent-pty.removed": "impossible",
|
|
42042
|
+
"pty.created": "impossible",
|
|
42043
|
+
"pty.deleted": "impossible",
|
|
42044
|
+
"pty.exited": "impossible",
|
|
42045
|
+
"pty.updated": "impossible",
|
|
42046
|
+
"shell.created": "impossible",
|
|
42047
|
+
"shell.deleted": "impossible",
|
|
42048
|
+
"shell.exited": "impossible",
|
|
42049
|
+
"tui.command.execute": "impossible",
|
|
42050
|
+
"tui.prompt.append": "impossible",
|
|
42051
|
+
"tui.session.select": "impossible",
|
|
42052
|
+
"tui.toast.show": "impossible",
|
|
42053
|
+
"websearch.updated": "impossible"
|
|
42054
|
+
};
|
|
42055
|
+
}
|
|
42056
|
+
});
|
|
42057
|
+
|
|
42058
|
+
// ../../src/core/harness/opencode/client.ts
|
|
42059
|
+
function openCodeAuthHeader(password) {
|
|
42060
|
+
return `Basic ${Buffer.from(`${OPENCODE_AUTH_USER}:${password}`).toString("base64")}`;
|
|
42061
|
+
}
|
|
42062
|
+
function parseFeedRecord(line3) {
|
|
42063
|
+
let value;
|
|
42064
|
+
try {
|
|
42065
|
+
value = JSON.parse(line3);
|
|
42066
|
+
} catch {
|
|
42067
|
+
return void 0;
|
|
42068
|
+
}
|
|
42069
|
+
if (typeof value !== "object" || value === null || !("feed" in value)) return void 0;
|
|
42070
|
+
const feed = value.feed;
|
|
42071
|
+
return feed === "event" || feed === "permissions" || feed === "messages" || feed === "tailer" ? value : void 0;
|
|
42072
|
+
}
|
|
42073
|
+
function parseHealth(body) {
|
|
42074
|
+
let value;
|
|
42075
|
+
try {
|
|
42076
|
+
value = JSON.parse(body);
|
|
42077
|
+
} catch {
|
|
42078
|
+
return void 0;
|
|
42079
|
+
}
|
|
42080
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
42081
|
+
const v = value;
|
|
42082
|
+
if (v.healthy !== true || typeof v.version !== "string") return void 0;
|
|
42083
|
+
return { healthy: true, version: v.version, pid: typeof v.pid === "number" ? v.pid : 0 };
|
|
42084
|
+
}
|
|
42085
|
+
function parseConfigEntries(body) {
|
|
42086
|
+
let value;
|
|
42087
|
+
try {
|
|
42088
|
+
value = JSON.parse(body);
|
|
42089
|
+
} catch {
|
|
42090
|
+
return void 0;
|
|
42091
|
+
}
|
|
42092
|
+
if (!Array.isArray(value)) return void 0;
|
|
42093
|
+
const entries = [];
|
|
42094
|
+
for (const item of value) {
|
|
42095
|
+
if (typeof item !== "object" || item === null) return void 0;
|
|
42096
|
+
const e = item;
|
|
42097
|
+
if (typeof e.path !== "string") return void 0;
|
|
42098
|
+
if (e.type === "directory") entries.push({ type: "directory", path: e.path });
|
|
42099
|
+
else if (e.type === "document" && typeof e.info === "object" && e.info !== null)
|
|
42100
|
+
entries.push({ type: "document", path: e.path, info: e.info });
|
|
42101
|
+
else return void 0;
|
|
42102
|
+
}
|
|
42103
|
+
return entries;
|
|
42104
|
+
}
|
|
42105
|
+
var OPENCODE_VERSION, OPENCODE_VERSION_TEXT, OPENCODE_AUTH_USER, OPENCODE_ROUTES, openCodeSessionRoutes, openCodePermissionReplyRoute;
|
|
42106
|
+
var init_client2 = __esm({
|
|
42107
|
+
"../../src/core/harness/opencode/client.ts"() {
|
|
42108
|
+
"use strict";
|
|
42109
|
+
OPENCODE_VERSION = "2.0.3";
|
|
42110
|
+
OPENCODE_VERSION_TEXT = `opencode v${OPENCODE_VERSION}`;
|
|
42111
|
+
OPENCODE_AUTH_USER = "opencode";
|
|
42112
|
+
OPENCODE_ROUTES = {
|
|
42113
|
+
/** `packages/protocol/src/groups/health.ts:16` — answers `Health`; behind the password like every route (`packages/server/src/process.ts:189-192`). */
|
|
42114
|
+
"health.get": { method: "GET", path: "/api/health" },
|
|
42115
|
+
/** `packages/protocol/src/groups/config.ts:9` — the loaded configuration entries, the readiness proof that the run's file took. */
|
|
42116
|
+
"config.get": { method: "GET", path: "/api/config" },
|
|
42117
|
+
/** `packages/protocol/src/groups/plugin.ts:24` — settles plugin activation (the relay plugin) before the first session; 204. */
|
|
42118
|
+
"plugin.awaitActivation": { method: "POST", path: "/api/plugin/await-activation" },
|
|
42119
|
+
/** `packages/protocol/src/groups/event.ts:43` — the SSE stream the tailer subscribes to; volatile by contract. */
|
|
42120
|
+
"event.subscribe": { method: "GET", path: "/api/event" },
|
|
42121
|
+
/** `packages/protocol/src/groups/session.ts:172`. */
|
|
42122
|
+
"session.create": { method: "POST", path: "/api/session" },
|
|
42123
|
+
/** `packages/protocol/src/groups/session.ts:192` — the authored session (the survival word). */
|
|
42124
|
+
"session.import": { method: "POST", path: "/api/session/import" }
|
|
42125
|
+
};
|
|
42126
|
+
openCodeSessionRoutes = (sessionID) => ({
|
|
42127
|
+
/** `packages/protocol/src/groups/session.ts:339` — admits one input; async: the answer is the inbox item, not the turn. */
|
|
42128
|
+
"session.prompt": { method: "POST", path: `/api/session/${sessionID}/prompt` },
|
|
42129
|
+
/** `packages/protocol/src/groups/session.ts:464` — BLOCKS until the agent loop is idle, then 204 (measured: a 239 s wait held open). */
|
|
42130
|
+
"session.wait": { method: "POST", path: `/api/session/${sessionID}/wait` },
|
|
42131
|
+
/** `packages/protocol/src/groups/session.ts:692` — `?continue=true|false`; answers `{ interrupted }`. */
|
|
42132
|
+
"session.interrupt": { method: "POST", path: `/api/session/${sessionID}/interrupt` },
|
|
42133
|
+
/** `packages/protocol/src/groups/message.ts:43` — `?order=asc|desc&limit=1..200&cursor=&type=`; the store, the record's truth. */
|
|
42134
|
+
"session.messages": { method: "GET", path: `/api/session/${sessionID}/message` },
|
|
42135
|
+
/** `packages/protocol/src/groups/permission.ts:85` — the pending asks the session owns. */
|
|
42136
|
+
"session.permission.list": { method: "GET", path: `/api/session/${sessionID}/permission` }
|
|
42137
|
+
});
|
|
42138
|
+
openCodePermissionReplyRoute = (sessionID, requestID) => ({
|
|
42139
|
+
method: "POST",
|
|
42140
|
+
path: `/api/session/${sessionID}/permission/${requestID}/reply`
|
|
42141
|
+
});
|
|
42142
|
+
}
|
|
42143
|
+
});
|
|
42144
|
+
|
|
42145
|
+
// ../../src/core/harness/opencode/session.ts
|
|
42146
|
+
function openCodeReplacedCallNote(tool) {
|
|
42147
|
+
return `The container running OpenCode was replaced while this ${tool} call was in flight; its result was lost \u2014 re-check its effects before re-running it.`;
|
|
42148
|
+
}
|
|
42149
|
+
function openCodeSettlementNote(s) {
|
|
42150
|
+
return s.action === "synthetic" ? s.text : openCodeReplacedCallNote(s.toolUse.name);
|
|
42151
|
+
}
|
|
42152
|
+
function openCodeSessionId(runId3) {
|
|
42153
|
+
return `ses_${runId3.replace(/[^A-Za-z0-9]/g, "-")}`;
|
|
42154
|
+
}
|
|
42155
|
+
function tickingClock(at) {
|
|
42156
|
+
let n2 = 0;
|
|
42157
|
+
return () => at + n2++;
|
|
42158
|
+
}
|
|
42159
|
+
function textOf2(content) {
|
|
42160
|
+
return content.filter((p) => p.type === "text").map((p) => p.text).join("\n\n");
|
|
42161
|
+
}
|
|
42162
|
+
function openCodeToolContent(toolUse, resultText, at, isError = false) {
|
|
42163
|
+
const input = isRecord7(toolUse.input) ? toolUse.input : {};
|
|
42164
|
+
const content = [{ type: "text", text: resultText || "(no output)" }];
|
|
42165
|
+
return {
|
|
42166
|
+
type: "tool",
|
|
42167
|
+
id: toolUse.id,
|
|
42168
|
+
name: toolUse.name,
|
|
42169
|
+
state: isError ? { status: "error", input, error: { message: resultText || "the call was interrupted" }, content } : { status: "completed", input, content },
|
|
42170
|
+
time: { created: at, completed: at }
|
|
42171
|
+
};
|
|
42172
|
+
}
|
|
42173
|
+
function openCodeCompactionMessage(id, entry, at) {
|
|
42174
|
+
return {
|
|
42175
|
+
id,
|
|
42176
|
+
type: "compaction",
|
|
42177
|
+
status: "completed",
|
|
42178
|
+
reason: "auto",
|
|
42179
|
+
summary: entry.summary,
|
|
42180
|
+
recent: "",
|
|
42181
|
+
time: { created: at }
|
|
42182
|
+
};
|
|
42183
|
+
}
|
|
42184
|
+
function openCodeStoreMessages(messages, opts) {
|
|
42185
|
+
const out = [];
|
|
42186
|
+
const model = opts.model ?? { providerID: "switchboard", id: "model" };
|
|
42187
|
+
const agent = opts.agent ?? "switchboard";
|
|
42188
|
+
const at = opts.clock ?? tickingClock(opts.at);
|
|
42189
|
+
const resultOf = (toolUseId, from) => {
|
|
42190
|
+
for (let i = from; i < messages.length; i++) {
|
|
42191
|
+
const turn = messages[i];
|
|
42192
|
+
if (turn.role !== "user") break;
|
|
42193
|
+
for (const part of turn.content) {
|
|
42194
|
+
if (part.type === "tool_result" && part.toolUseId === toolUseId) {
|
|
42195
|
+
const text = typeof part.content === "string" ? part.content : part.content.map((c) => c.type === "text" ? c.text : `[${c.type}]`).join("\n");
|
|
42196
|
+
return { text, isError: part.isError === true };
|
|
42197
|
+
}
|
|
42198
|
+
}
|
|
42199
|
+
}
|
|
42200
|
+
return { text: opts.settlements?.get(toolUseId) ?? "", isError: true };
|
|
42201
|
+
};
|
|
42202
|
+
messages.forEach((message4, i) => {
|
|
42203
|
+
if (message4.role === "user") {
|
|
42204
|
+
const text = textOf2(message4.content);
|
|
42205
|
+
const onlyResults = message4.content.length > 0 && message4.content.every((p) => p.type === "tool_result");
|
|
42206
|
+
if (onlyResults) return;
|
|
42207
|
+
out.push({ id: `msg_${opts.sessionID}_u${i}`, type: "user", text, time: { created: at() } });
|
|
42208
|
+
return;
|
|
42209
|
+
}
|
|
42210
|
+
const content = [];
|
|
42211
|
+
for (const part of message4.content) {
|
|
42212
|
+
if (part.type === "text") content.push({ type: "text", text: part.text });
|
|
42213
|
+
else if (part.type === "tool_use") {
|
|
42214
|
+
const result = resultOf(part.id, i + 1);
|
|
42215
|
+
content.push(openCodeToolContent(part, result.text, at(), result.isError));
|
|
42216
|
+
}
|
|
42217
|
+
}
|
|
42218
|
+
if (content.length === 0) return;
|
|
42219
|
+
const now = at();
|
|
42220
|
+
out.push({
|
|
42221
|
+
id: `msg_${opts.sessionID}_a${i}`,
|
|
42222
|
+
type: "assistant",
|
|
42223
|
+
agent,
|
|
42224
|
+
model,
|
|
42225
|
+
content,
|
|
42226
|
+
time: { created: now, completed: now }
|
|
42227
|
+
});
|
|
42228
|
+
});
|
|
42229
|
+
return out;
|
|
42230
|
+
}
|
|
42231
|
+
function openCodeImportBody(messages, opts) {
|
|
42232
|
+
const clock4 = tickingClock(opts.at);
|
|
42233
|
+
const storeMessages = openCodeStoreMessages(messages, { ...opts, clock: clock4 });
|
|
42234
|
+
(opts.compactions ?? []).forEach(
|
|
42235
|
+
(entry, i) => storeMessages.push(openCodeCompactionMessage(`msg_${opts.sessionID}_c${i}`, entry, clock4()))
|
|
42236
|
+
);
|
|
42237
|
+
const info = {
|
|
42238
|
+
id: opts.sessionID,
|
|
42239
|
+
projectID: "global",
|
|
42240
|
+
cost: 0,
|
|
42241
|
+
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
42242
|
+
time: { created: opts.at, updated: opts.at },
|
|
42243
|
+
location: opts.location,
|
|
42244
|
+
...opts.agent ? { agent: opts.agent } : {},
|
|
42245
|
+
...opts.model ? { model: opts.model } : {}
|
|
42246
|
+
};
|
|
42247
|
+
return { info, messages: storeMessages, location: opts.location };
|
|
42248
|
+
}
|
|
42249
|
+
function openCodeSeedAndRequest(messages) {
|
|
42250
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
42251
|
+
if (messages[i].role === "user") {
|
|
42252
|
+
return { seed: messages.slice(0, i), request: textOf2(messages[i].content) };
|
|
42253
|
+
}
|
|
42254
|
+
}
|
|
42255
|
+
return { seed: messages.slice(), request: "" };
|
|
42256
|
+
}
|
|
42257
|
+
var isRecord7;
|
|
42258
|
+
var init_session2 = __esm({
|
|
42259
|
+
"../../src/core/harness/opencode/session.ts"() {
|
|
42260
|
+
"use strict";
|
|
42261
|
+
isRecord7 = (v) => typeof v === "object" && v !== null;
|
|
42262
|
+
}
|
|
42263
|
+
});
|
|
42264
|
+
|
|
42265
|
+
// ../../src/core/harness/opencode/pluginSource.ts
|
|
42266
|
+
var OC_BLOCKED_AT_DOOR_PREFIX, OC_BLOCKED_UNAVAILABLE_PREFIX, OPENCODE_PLUGIN_SOURCE;
|
|
42267
|
+
var init_pluginSource = __esm({
|
|
42268
|
+
"../../src/core/harness/opencode/pluginSource.ts"() {
|
|
42269
|
+
"use strict";
|
|
42270
|
+
OC_BLOCKED_AT_DOOR_PREFIX = "authorization refused at the door: ";
|
|
42271
|
+
OC_BLOCKED_UNAVAILABLE_PREFIX = "authorization unavailable: ";
|
|
42272
|
+
OPENCODE_PLUGIN_SOURCE = `// Switchboard's OpenCode plugin. Written into the run's plugin directory by the
|
|
42273
|
+
// bot before \`opencode serve\` starts; loaded as \`./plugins/switchboard\`'s
|
|
42274
|
+
// \`index.js\`. Imports nothing.
|
|
42275
|
+
|
|
42276
|
+
const BEARER_ENV = "SWITCHBOARD_RUN_BEARER";
|
|
42277
|
+
const URL_ENV = "SWITCHBOARD_HARNESS_URL";
|
|
42278
|
+
/** How long the door ask keeps retrying an unreachable bot before it blocks. */
|
|
42279
|
+
const AUTHORIZE_WAIT_MS = 90000;
|
|
42280
|
+
const AUTHORIZE_RETRY_MS = 2000;
|
|
42281
|
+
/** How long one request for a relayed tool's result may take before it is asked
|
|
42282
|
+
* again: over the bot's window (30 s, after which it answers the call is still
|
|
42283
|
+
* running), under this runtime's own header timeout, so a hung connection
|
|
42284
|
+
* costs a minute of the wait, not five. */
|
|
42285
|
+
const TOOL_REQUEST_TIMEOUT_MS = 60000;
|
|
42286
|
+
/** How long a relayed call keeps asking a bot it cannot reach before it fails. */
|
|
42287
|
+
const TOOL_WAIT_MS = 90000;
|
|
42288
|
+
/** A bot that says a call is still running is asked again no sooner than this,
|
|
42289
|
+
* so a misbehaving bot is never asked in a hot loop. */
|
|
42290
|
+
const TOOL_REASK_FLOOR_MS = 1000;
|
|
42291
|
+
|
|
42292
|
+
function settings() {
|
|
42293
|
+
const url = process.env[URL_ENV];
|
|
42294
|
+
const bearer = process.env[BEARER_ENV];
|
|
42295
|
+
if (!url || !bearer) throw new Error("switchboard plugin: " + URL_ENV + " and " + BEARER_ENV + " must be set");
|
|
42296
|
+
return { base: url.replace(/\\/$/, ""), bearer };
|
|
42297
|
+
}
|
|
42298
|
+
|
|
42299
|
+
/** The bot answered, and said no: the status rides along so a caller can tell a
|
|
42300
|
+
* verdict from a bot it could not reach. */
|
|
42301
|
+
class HarnessAnswerError extends Error {
|
|
42302
|
+
constructor(message, status) {
|
|
42303
|
+
super(message);
|
|
42304
|
+
this.status = status;
|
|
42305
|
+
}
|
|
42306
|
+
}
|
|
42307
|
+
|
|
42308
|
+
async function call(method, path, body, signal) {
|
|
42309
|
+
const { base, bearer } = settings();
|
|
42310
|
+
const res = await fetch(base + path, {
|
|
42311
|
+
method,
|
|
42312
|
+
headers: { authorization: "Bearer " + bearer, "content-type": "application/json", accept: "application/json" },
|
|
42313
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
42314
|
+
signal,
|
|
42315
|
+
});
|
|
42316
|
+
const status = res.status;
|
|
42317
|
+
const text = await res.text();
|
|
42318
|
+
let json;
|
|
42319
|
+
try {
|
|
42320
|
+
json = text ? JSON.parse(text) : {};
|
|
42321
|
+
} catch {
|
|
42322
|
+
throw new HarnessAnswerError(
|
|
42323
|
+
"switchboard plugin: " + method + " " + path + " answered " + status + " with a body that is not JSON",
|
|
42324
|
+
status,
|
|
42325
|
+
);
|
|
42326
|
+
}
|
|
42327
|
+
if (!res.ok && status !== 202) {
|
|
42328
|
+
throw new HarnessAnswerError(
|
|
42329
|
+
"switchboard plugin: " + method + " " + path + " answered " + status + ": " + (json.error || text),
|
|
42330
|
+
status,
|
|
42331
|
+
);
|
|
42332
|
+
}
|
|
42333
|
+
return { status, json };
|
|
42334
|
+
}
|
|
42335
|
+
|
|
42336
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
42337
|
+
|
|
42338
|
+
/** The bot's verdict on one tool call at the door. A verdict answers at once,
|
|
42339
|
+
* and so does a refusal at the door (a 4xx: the bearer wrong, expired or
|
|
42340
|
+
* revoked, the run not on the harness). A bot that cannot be reached or is
|
|
42341
|
+
* failing (a network error, a 5xx, a body that is not JSON) is asked again
|
|
42342
|
+
* every two seconds until the wait is up, then the call is blocked naming why. */
|
|
42343
|
+
async function authorize(toolCallId, tool, input) {
|
|
42344
|
+
const started = Date.now();
|
|
42345
|
+
let lastError = "";
|
|
42346
|
+
for (;;) {
|
|
42347
|
+
try {
|
|
42348
|
+
const { json } = await call("POST", "/harness/authorize", { toolCallId, tool, input });
|
|
42349
|
+
return json;
|
|
42350
|
+
} catch (err) {
|
|
42351
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
42352
|
+
const status = err instanceof HarnessAnswerError ? err.status : undefined;
|
|
42353
|
+
if (status !== undefined && status >= 400 && status < 500) {
|
|
42354
|
+
return { allow: false, reason: "${OC_BLOCKED_AT_DOOR_PREFIX}" + lastError };
|
|
42355
|
+
}
|
|
42356
|
+
if (Date.now() - started >= AUTHORIZE_WAIT_MS) {
|
|
42357
|
+
return { allow: false, reason: "${OC_BLOCKED_UNAVAILABLE_PREFIX}the bot did not answer for 90 s (" + lastError + ")" };
|
|
42358
|
+
}
|
|
42359
|
+
await sleep(AUTHORIZE_RETRY_MS);
|
|
42360
|
+
}
|
|
42361
|
+
}
|
|
42362
|
+
}
|
|
42363
|
+
|
|
42364
|
+
/** pi's abort signal for the call, with this request's own timeout beside it
|
|
42365
|
+
* where the runtime can combine the two; the timeout alone where it cannot. */
|
|
42366
|
+
function requestSignal(signal) {
|
|
42367
|
+
const timeout = typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(TOOL_REQUEST_TIMEOUT_MS) : undefined;
|
|
42368
|
+
if (!timeout) return signal;
|
|
42369
|
+
if (!signal) return timeout;
|
|
42370
|
+
return typeof AbortSignal.any === "function" ? AbortSignal.any([signal, timeout]) : signal;
|
|
42371
|
+
}
|
|
42372
|
+
|
|
42373
|
+
/** A relayed tool's result. The call is posted with its id; a bot that answers
|
|
42374
|
+
* that the call is still running (a \`202 pending\`) is asked again with the
|
|
42375
|
+
* same id until it answers: the bot runs the call once and every later ask
|
|
42376
|
+
* joins it. A bot that cannot be reached or is failing (a network error, a
|
|
42377
|
+
* timed-out request, a 5xx) is asked again every two seconds until the wait is
|
|
42378
|
+
* up, then the call fails naming why; a refusal at the door (a 4xx) fails it
|
|
42379
|
+
* at once; an abort ends the asking. */
|
|
42380
|
+
async function relay(toolCallId, tool, input, signal) {
|
|
42381
|
+
let lastAnswered = Date.now();
|
|
42382
|
+
let lastError = "";
|
|
42383
|
+
for (;;) {
|
|
42384
|
+
if (signal && signal.aborted) throw new Error("switchboard plugin: " + tool + " was aborted before the bot answered");
|
|
42385
|
+
const asked = Date.now();
|
|
42386
|
+
let answer;
|
|
42387
|
+
try {
|
|
42388
|
+
answer = await call("POST", "/harness/tool", { toolCallId, tool, input }, requestSignal(signal));
|
|
42389
|
+
} catch (err) {
|
|
42390
|
+
if (signal && signal.aborted) throw err;
|
|
42391
|
+
const status = err instanceof HarnessAnswerError ? err.status : undefined;
|
|
42392
|
+
if (status !== undefined && status >= 400 && status < 500) throw err;
|
|
42393
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
42394
|
+
if (Date.now() - lastAnswered >= TOOL_WAIT_MS) {
|
|
42395
|
+
throw new Error("switchboard plugin: the bot did not answer " + tool + " for 90 s (" + lastError + ")");
|
|
42396
|
+
}
|
|
42397
|
+
await sleep(AUTHORIZE_RETRY_MS);
|
|
42398
|
+
continue;
|
|
42399
|
+
}
|
|
42400
|
+
lastAnswered = Date.now();
|
|
42401
|
+
if (answer.status !== 202 && !answer.json.pending) return answer.json;
|
|
42402
|
+
const held = Date.now() - asked;
|
|
42403
|
+
if (held < TOOL_REASK_FLOOR_MS) await sleep(TOOL_REASK_FLOOR_MS - held);
|
|
42404
|
+
}
|
|
42405
|
+
}
|
|
42406
|
+
|
|
42407
|
+
/** One relayed tool's content in OpenCode's shape: pi's text parts joined into
|
|
42408
|
+
* one string, an image part named (OpenCode's tool content carries text or a
|
|
42409
|
+
* file URI, never inline image bytes); never empty. */
|
|
42410
|
+
function contentText(content) {
|
|
42411
|
+
if (!Array.isArray(content)) return typeof content === "string" ? content : "";
|
|
42412
|
+
const text = content
|
|
42413
|
+
.map((c) => (c && c.type === "text" ? String(c.text) : c && c.type === "image" ? "[image " + (c.mimeType || "") + "]" : ""))
|
|
42414
|
+
.filter((t) => t.length > 0)
|
|
42415
|
+
.join("\\n");
|
|
42416
|
+
return text;
|
|
42417
|
+
}
|
|
42418
|
+
|
|
42419
|
+
/** One relayed tool as OpenCode's editor registers it: a DIRECT tool
|
|
42420
|
+
* (\`codemode: false\`) under its own name and JSON Schema, whose execute runs
|
|
42421
|
+
* the door and the relay with the model's call id. A refusal at the door or an
|
|
42422
|
+
* error from the tool becomes the call's error text \u2014 the model reads it. */
|
|
42423
|
+
function relayTool(def) {
|
|
42424
|
+
return {
|
|
42425
|
+
name: def.name,
|
|
42426
|
+
description: def.description,
|
|
42427
|
+
input: def.inputSchema,
|
|
42428
|
+
options: { codemode: false },
|
|
42429
|
+
async execute(input, context) {
|
|
42430
|
+
const callId = context && context.id;
|
|
42431
|
+
const verdict = await authorize(callId, def.name, input);
|
|
42432
|
+
if (verdict && verdict.allow === false) throw new Error(verdict.reason || "the bot refused this tool call");
|
|
42433
|
+
const answer = await relay(callId, def.name, input, context && context.signal);
|
|
42434
|
+
const text = contentText(answer.content);
|
|
42435
|
+
if (answer.isError) throw new Error(text || "the tool failed");
|
|
42436
|
+
return { content: text || "(no output)" };
|
|
42437
|
+
},
|
|
42438
|
+
};
|
|
42439
|
+
}
|
|
42440
|
+
|
|
42441
|
+
export default {
|
|
42442
|
+
id: "switchboard",
|
|
42443
|
+
async setup(ctx) {
|
|
42444
|
+
const { json } = await call("GET", "/harness/tools");
|
|
42445
|
+
const tools = (json && json.tools) || [];
|
|
42446
|
+
await ctx.tool.transform((editor) => {
|
|
42447
|
+
for (const def of tools) editor.add(relayTool(def));
|
|
42448
|
+
});
|
|
42449
|
+
// The seam the harness watches for a call OpenCode is about to run; the gate
|
|
42450
|
+
// itself rides \`permission.asked\` in the bot, never a decision here.
|
|
42451
|
+
ctx.tool.hook("execute.before", () => {});
|
|
42452
|
+
},
|
|
42453
|
+
};
|
|
42454
|
+
`;
|
|
42455
|
+
}
|
|
42456
|
+
});
|
|
42457
|
+
|
|
42458
|
+
// ../../src/core/harness/opencode/tailerSource.ts
|
|
42459
|
+
var OPENCODE_SERVE_PID_ENV, OPENCODE_TAILER_SOURCE;
|
|
42460
|
+
var init_tailerSource = __esm({
|
|
42461
|
+
"../../src/core/harness/opencode/tailerSource.ts"() {
|
|
42462
|
+
"use strict";
|
|
42463
|
+
OPENCODE_SERVE_PID_ENV = "SWITCHBOARD_SERVE_PID";
|
|
42464
|
+
OPENCODE_TAILER_SOURCE = `// Switchboard's OpenCode tailer. Written into the run's directory by the bot
|
|
42465
|
+
// and started beside \`opencode serve\`; its stdout is the run's feed. Requires
|
|
42466
|
+
// only Node's own modules.
|
|
42467
|
+
"use strict";
|
|
42468
|
+
const http = require("node:http");
|
|
42469
|
+
|
|
42470
|
+
const port = Number(process.env.SWITCHBOARD_HARNESS_PORT);
|
|
42471
|
+
const password = process.env.OPENCODE_PASSWORD || "";
|
|
42472
|
+
const servePid = Number(process.env.SWITCHBOARD_SERVE_PID) || 0;
|
|
42473
|
+
const auth = "Basic " + Buffer.from("opencode:" + password).toString("base64");
|
|
42474
|
+
const REFILL_ON = new Set([
|
|
42475
|
+
"session.step.ended",
|
|
42476
|
+
"session.step.failed",
|
|
42477
|
+
"session.execution.succeeded",
|
|
42478
|
+
"session.execution.failed",
|
|
42479
|
+
"session.execution.interrupted",
|
|
42480
|
+
]);
|
|
42481
|
+
const MIN_BACKOFF_MS = 250;
|
|
42482
|
+
const MAX_BACKOFF_MS = 5000;
|
|
42483
|
+
const REQUEST_TIMEOUT_MS = 30000;
|
|
42484
|
+
const PAGE_LIMIT = 200;
|
|
42485
|
+
|
|
42486
|
+
/** sessionID -> Map(messageID -> the message as JSON text): the last list each refill saw. */
|
|
42487
|
+
const known = new Map();
|
|
42488
|
+
/** Refills run one at a time, in the order their step ends arrived, so the feed's records keep that order. */
|
|
42489
|
+
let pending = Promise.resolve();
|
|
42490
|
+
let backoffMs = MIN_BACKOFF_MS;
|
|
42491
|
+
let connections = 0;
|
|
42492
|
+
|
|
42493
|
+
function emit(record) {
|
|
42494
|
+
process.stdout.write(JSON.stringify(record) + "\\n");
|
|
42495
|
+
}
|
|
42496
|
+
|
|
42497
|
+
function note(text, extra) {
|
|
42498
|
+
emit(Object.assign({ feed: "tailer", at: Date.now(), note: text }, extra || {}));
|
|
42499
|
+
}
|
|
42500
|
+
|
|
42501
|
+
function detailOf(err) {
|
|
42502
|
+
return String((err && err.message) || err).slice(0, 300);
|
|
42503
|
+
}
|
|
42504
|
+
|
|
42505
|
+
function request(method, path, accept) {
|
|
42506
|
+
return new Promise((resolve, reject) => {
|
|
42507
|
+
const req = http.request(
|
|
42508
|
+
{ host: "127.0.0.1", port, method, path, headers: { authorization: auth, accept } },
|
|
42509
|
+
(res) => {
|
|
42510
|
+
let body = "";
|
|
42511
|
+
res.setEncoding("utf8");
|
|
42512
|
+
res.on("data", (chunk) => (body += chunk));
|
|
42513
|
+
res.on("end", () => resolve({ status: res.statusCode || 0, body }));
|
|
42514
|
+
res.on("error", reject);
|
|
42515
|
+
},
|
|
42516
|
+
);
|
|
42517
|
+
req.on("error", reject);
|
|
42518
|
+
req.setTimeout(REQUEST_TIMEOUT_MS, () => req.destroy(new Error("the request timed out")));
|
|
42519
|
+
req.end();
|
|
42520
|
+
});
|
|
42521
|
+
}
|
|
42522
|
+
|
|
42523
|
+
async function getJson(path) {
|
|
42524
|
+
const res = await request("GET", path, "application/json");
|
|
42525
|
+
if (res.status !== 200) throw new Error("GET " + path + " answered " + res.status);
|
|
42526
|
+
return JSON.parse(res.body);
|
|
42527
|
+
}
|
|
42528
|
+
|
|
42529
|
+
/** The session's pending asks, whole; then its messages, the changed ones only. */
|
|
42530
|
+
async function refill(sessionID, reason) {
|
|
42531
|
+
const at = Date.now();
|
|
42532
|
+
try {
|
|
42533
|
+
const answer = await getJson("/api/session/" + sessionID + "/permission");
|
|
42534
|
+
emit({ feed: "permissions", at, sessionID, reason, data: Array.isArray(answer.data) ? answer.data : [] });
|
|
42535
|
+
} catch (err) {
|
|
42536
|
+
note("permission refill failed", { sessionID, reason, detail: detailOf(err) });
|
|
42537
|
+
}
|
|
42538
|
+
try {
|
|
42539
|
+
const all = [];
|
|
42540
|
+
let cursor;
|
|
42541
|
+
for (let page = 0; page < 10000; page++) {
|
|
42542
|
+
const query = (cursor ? "cursor=" + encodeURIComponent(cursor) : "order=asc") + "&limit=" + PAGE_LIMIT;
|
|
42543
|
+
const answer = await getJson("/api/session/" + sessionID + "/message?" + query);
|
|
42544
|
+
for (const message of Array.isArray(answer.data) ? answer.data : []) all.push(message);
|
|
42545
|
+
cursor = answer.cursor && answer.cursor.next;
|
|
42546
|
+
if (!cursor) break;
|
|
42547
|
+
}
|
|
42548
|
+
const seen = known.get(sessionID) || new Map();
|
|
42549
|
+
const changed = [];
|
|
42550
|
+
for (const message of all) {
|
|
42551
|
+
const text = JSON.stringify(message);
|
|
42552
|
+
if (seen.get(message.id) !== text) {
|
|
42553
|
+
seen.set(message.id, text);
|
|
42554
|
+
changed.push(message);
|
|
42555
|
+
}
|
|
42556
|
+
}
|
|
42557
|
+
known.set(sessionID, seen);
|
|
42558
|
+
emit({ feed: "messages", at, sessionID, reason, data: changed });
|
|
42559
|
+
} catch (err) {
|
|
42560
|
+
// The session is known from here on even though its list is empty, so the
|
|
42561
|
+
// reconnect sweep covers it: a session whose only refill failed would
|
|
42562
|
+
// otherwise wait for its next step end.
|
|
42563
|
+
if (!known.has(sessionID)) known.set(sessionID, new Map());
|
|
42564
|
+
note("message refill failed", { sessionID, reason, detail: detailOf(err) });
|
|
42565
|
+
}
|
|
42566
|
+
}
|
|
42567
|
+
|
|
42568
|
+
function queueRefill(sessionID, reason) {
|
|
42569
|
+
pending = pending.then(() => refill(sessionID, reason));
|
|
42570
|
+
}
|
|
42571
|
+
|
|
42572
|
+
function onEvent(event) {
|
|
42573
|
+
emit({ feed: "event", at: Date.now(), event });
|
|
42574
|
+
if (REFILL_ON.has(event.type) && event.data && typeof event.data.sessionID === "string")
|
|
42575
|
+
queueRefill(event.data.sessionID, event.type);
|
|
42576
|
+
}
|
|
42577
|
+
|
|
42578
|
+
function connected() {
|
|
42579
|
+
connections++;
|
|
42580
|
+
backoffMs = MIN_BACKOFF_MS;
|
|
42581
|
+
note(connections === 1 ? "connected" : "reconnected", { connections });
|
|
42582
|
+
// A gap is possible only after a drop: refill what a step end during it would have.
|
|
42583
|
+
if (connections > 1) for (const sessionID of known.keys()) queueRefill(sessionID, "reconnect");
|
|
42584
|
+
}
|
|
42585
|
+
|
|
42586
|
+
/** One subscription; resolves with why it ended. Blocks are \`data:\` lines
|
|
42587
|
+
* ended by a blank line; a comment line (the heartbeat) has no data. */
|
|
42588
|
+
function subscribe() {
|
|
42589
|
+
return new Promise((resolve) => {
|
|
42590
|
+
const req = http.request(
|
|
42591
|
+
{ host: "127.0.0.1", port, method: "GET", path: "/api/event", headers: { authorization: auth, accept: "text/event-stream" } },
|
|
42592
|
+
(res) => {
|
|
42593
|
+
if (res.statusCode !== 200) {
|
|
42594
|
+
res.resume();
|
|
42595
|
+
res.on("end", () => resolve("the event stream answered " + res.statusCode));
|
|
42596
|
+
return;
|
|
42597
|
+
}
|
|
42598
|
+
connected();
|
|
42599
|
+
let buffer = "";
|
|
42600
|
+
res.setEncoding("utf8");
|
|
42601
|
+
res.on("data", (chunk) => {
|
|
42602
|
+
buffer += chunk;
|
|
42603
|
+
let cut;
|
|
42604
|
+
while ((cut = buffer.indexOf("\\n\\n")) >= 0) {
|
|
42605
|
+
const block = buffer.slice(0, cut);
|
|
42606
|
+
buffer = buffer.slice(cut + 2);
|
|
42607
|
+
const data = block
|
|
42608
|
+
.split("\\n")
|
|
42609
|
+
.filter((line) => line.startsWith("data:"))
|
|
42610
|
+
.map((line) => line.slice(5).replace(/^ /, ""))
|
|
42611
|
+
.join("\\n");
|
|
42612
|
+
if (!data) continue;
|
|
42613
|
+
try {
|
|
42614
|
+
onEvent(JSON.parse(data));
|
|
42615
|
+
} catch (err) {
|
|
42616
|
+
note("an event was not JSON", { detail: data.slice(0, 200) });
|
|
42617
|
+
}
|
|
42618
|
+
}
|
|
42619
|
+
});
|
|
42620
|
+
res.on("end", () => resolve("the event stream ended"));
|
|
42621
|
+
res.on("error", (err) => resolve("the event stream failed: " + detailOf(err)));
|
|
42622
|
+
},
|
|
42623
|
+
);
|
|
42624
|
+
req.on("error", (err) => resolve("the event stream could not be opened: " + detailOf(err)));
|
|
42625
|
+
req.end();
|
|
42626
|
+
});
|
|
42627
|
+
}
|
|
42628
|
+
|
|
42629
|
+
function serveAlive() {
|
|
42630
|
+
if (!servePid) return true;
|
|
42631
|
+
try {
|
|
42632
|
+
process.kill(servePid, 0);
|
|
42633
|
+
return true;
|
|
42634
|
+
} catch (err) {
|
|
42635
|
+
return false;
|
|
42636
|
+
}
|
|
42637
|
+
}
|
|
42638
|
+
|
|
42639
|
+
async function main() {
|
|
42640
|
+
note("started", { port, servePid });
|
|
42641
|
+
for (;;) {
|
|
42642
|
+
const why = await subscribe();
|
|
42643
|
+
note("stream closed", { detail: why });
|
|
42644
|
+
await pending.catch(() => {});
|
|
42645
|
+
if (!serveAlive()) {
|
|
42646
|
+
note("the server process is gone; exiting");
|
|
42647
|
+
return;
|
|
42648
|
+
}
|
|
42649
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
42650
|
+
backoffMs = Math.min(MAX_BACKOFF_MS, backoffMs * 2);
|
|
42651
|
+
}
|
|
42652
|
+
}
|
|
42653
|
+
|
|
42654
|
+
process.on("SIGTERM", () => process.exit(0));
|
|
42655
|
+
process.on("SIGINT", () => process.exit(0));
|
|
42656
|
+
main().then(
|
|
42657
|
+
() => process.exit(0),
|
|
42658
|
+
(err) => {
|
|
42659
|
+
note("tailer failed", { detail: String((err && err.stack) || err).slice(0, 600) });
|
|
42660
|
+
process.exit(1);
|
|
42661
|
+
},
|
|
42662
|
+
);
|
|
42663
|
+
`;
|
|
42664
|
+
}
|
|
42665
|
+
});
|
|
42666
|
+
|
|
42667
|
+
// ../../src/core/harness/opencode/process.ts
|
|
42668
|
+
import { randomBytes as randomBytes5 } from "node:crypto";
|
|
42669
|
+
function openCodeRunPaths(runId3) {
|
|
42670
|
+
return openCodeRunPathsAt(`/tmp/switchboard-oc-${runId3}`);
|
|
42671
|
+
}
|
|
42672
|
+
function openCodeRunPathsAt(dir) {
|
|
42673
|
+
const xdg = {
|
|
42674
|
+
data: `${dir}/xdg/data`,
|
|
42675
|
+
config: `${dir}/xdg/config`,
|
|
42676
|
+
cache: `${dir}/xdg/cache`,
|
|
42677
|
+
state: `${dir}/xdg/state`
|
|
42678
|
+
};
|
|
42679
|
+
const pluginDir = `${dir}/plugins/switchboard`;
|
|
42680
|
+
const commandDir = `${dir}/cmd`;
|
|
42681
|
+
const feed = `${dir}/feed.jsonl`;
|
|
42682
|
+
return {
|
|
42683
|
+
dir,
|
|
42684
|
+
// The directories the start makes at 700, the root first.
|
|
42685
|
+
dirs: [dir, xdg.data, xdg.config, xdg.cache, xdg.state, pluginDir, commandDir],
|
|
42686
|
+
fifo: `${dir}/serve.in`,
|
|
42687
|
+
log: `${dir}/serve.log`,
|
|
42688
|
+
errLog: `${dir}/serve.err`,
|
|
42689
|
+
pidFile: `${dir}/pid`,
|
|
42690
|
+
commandDir,
|
|
42691
|
+
xdg,
|
|
42692
|
+
config: `${dir}/opencode.json`,
|
|
42693
|
+
pluginDir,
|
|
42694
|
+
plugin: `${pluginDir}/index.js`,
|
|
42695
|
+
feed,
|
|
42696
|
+
tailerScript: `${dir}/tailer.js`,
|
|
42697
|
+
tailer: {
|
|
42698
|
+
dir,
|
|
42699
|
+
dirs: [dir],
|
|
42700
|
+
fifo: `${dir}/tailer.in`,
|
|
42701
|
+
log: feed,
|
|
42702
|
+
errLog: `${dir}/tailer.err`,
|
|
42703
|
+
pidFile: `${dir}/tailer.pid`,
|
|
42704
|
+
commandDir
|
|
42705
|
+
}
|
|
42706
|
+
};
|
|
42707
|
+
}
|
|
42708
|
+
function openCodeDeniedActions(identity2) {
|
|
42709
|
+
if (identity2 === "write") return OPENCODE_ALWAYS_DENIED;
|
|
42710
|
+
if (identity2 === "read") return [...OPENCODE_ALWAYS_DENIED, ...OPENCODE_READ_DENIED];
|
|
42711
|
+
return [...OPENCODE_ALWAYS_DENIED, ...OPENCODE_NONE_DENIED];
|
|
42712
|
+
}
|
|
42713
|
+
function openCodePermissionRules(identity2) {
|
|
42714
|
+
return [
|
|
42715
|
+
ASK_ALL_RULE,
|
|
42716
|
+
...openCodeDeniedActions(identity2).map((action) => ({ action, resource: "*", effect: "deny" }))
|
|
42717
|
+
];
|
|
42718
|
+
}
|
|
42719
|
+
function openCodeBuiltinToolsFor(identity2) {
|
|
42720
|
+
if (identity2 === "write") return OPENCODE_BUILTIN_TOOLS;
|
|
42721
|
+
if (identity2 === "read") return OPENCODE_READ_TOOLS;
|
|
42722
|
+
return OPENCODE_NO_TOOLS;
|
|
42723
|
+
}
|
|
42724
|
+
function openCodeProviderPackage(providerType) {
|
|
42725
|
+
return providerType === "anthropic" ? "aisdk:@ai-sdk/anthropic" : "aisdk:@ai-sdk/openai-compatible";
|
|
42726
|
+
}
|
|
42727
|
+
function openCodeProviderBaseUrl(harnessUrl) {
|
|
42728
|
+
return `${harnessUrl.replace(/\/$/, "")}/v1`;
|
|
42729
|
+
}
|
|
42730
|
+
function openCodePromptNote(relayTools, identity2) {
|
|
42731
|
+
const named = relayTools.map((t) => `\`${t}\``).join(", ");
|
|
42732
|
+
if (identity2 === "none") {
|
|
42733
|
+
return [
|
|
42734
|
+
"HARNESS NOTE: this run has no workspace, so none of OpenCode's own tools (`read`, `shell`, `edit`, `write`, `glob`, `grep` and `skill`) is available, and a call to any of them is refused.",
|
|
42735
|
+
relayTools.length > 0 ? `This run's tools are exactly these, each under its own name: ${named}.` : "No tools are available in this run."
|
|
42736
|
+
].join(" ");
|
|
42737
|
+
}
|
|
42738
|
+
const write = identity2 === "write";
|
|
42739
|
+
return [
|
|
42740
|
+
write ? "HARNESS NOTE: this run's workspace tools are OpenCode's own \u2014 `read`, `shell`, `edit`, `write`, `glob`, `grep` and `skill`, all running in the worktree." : "HARNESS NOTE: this run's workspace tools are OpenCode's own \u2014 `read`, `shell`, `glob`, `grep` and `skill`, all running in the worktree; it has no `edit` and no `write`: the run is read-only, and a call to either is refused.",
|
|
42741
|
+
write ? "Where these instructions say `read_file` use `read`; where they say `write_file` use `write` (a whole file) or `edit` (a targeted change); where they say `bash` use `shell`." : "Where these instructions say `read_file` use `read`; where they say `bash` use `shell`.",
|
|
42742
|
+
relayTools.length > 0 ? `Every other tool named above is available under its own name: ${named}.` : "No other tools are available in this run."
|
|
42743
|
+
].join(" ");
|
|
42744
|
+
}
|
|
42745
|
+
function openCodeSystemPrompt(spec) {
|
|
42746
|
+
return `${spec.system.trimEnd()}
|
|
42747
|
+
|
|
42748
|
+
${openCodePromptNote(spec.relayTools, spec.identity)}
|
|
42749
|
+
`;
|
|
42750
|
+
}
|
|
42751
|
+
function openCodeConfig(spec) {
|
|
42752
|
+
const { id, maxTokens, providerType, contextTokens } = spec.model;
|
|
42753
|
+
const compaction = {
|
|
42754
|
+
...spec.compaction?.buffer !== void 0 ? { buffer: spec.compaction.buffer } : {},
|
|
42755
|
+
...spec.compaction?.keepTokens !== void 0 ? { keep: { tokens: spec.compaction.keepTokens } } : {}
|
|
42756
|
+
};
|
|
42757
|
+
return {
|
|
42758
|
+
providers: {
|
|
42759
|
+
[PROXY_PROVIDER]: {
|
|
42760
|
+
name: "Switchboard model proxy",
|
|
42761
|
+
package: openCodeProviderPackage(providerType),
|
|
42762
|
+
settings: { baseURL: openCodeProviderBaseUrl(spec.harnessUrl), apiKey: `{env:${RUN_BEARER_ENV}}` },
|
|
42763
|
+
models: {
|
|
42764
|
+
[id]: {
|
|
42765
|
+
name: id,
|
|
42766
|
+
limit: { context: contextTokens ?? OPENCODE_DEFAULT_CONTEXT_TOKENS, output: maxTokens },
|
|
42767
|
+
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } }
|
|
42768
|
+
}
|
|
42769
|
+
}
|
|
42770
|
+
}
|
|
42771
|
+
},
|
|
42772
|
+
model: `${PROXY_PROVIDER}/${id}`,
|
|
42773
|
+
default_agent: OPENCODE_AGENT,
|
|
42774
|
+
agents: {
|
|
42775
|
+
[OPENCODE_AGENT]: {
|
|
42776
|
+
mode: "primary",
|
|
42777
|
+
system: openCodeSystemPrompt(spec),
|
|
42778
|
+
permissions: openCodePermissionRules(spec.identity)
|
|
42779
|
+
},
|
|
42780
|
+
title: { disabled: true }
|
|
42781
|
+
},
|
|
42782
|
+
plugins: [OPENCODE_PLUGIN_REF],
|
|
42783
|
+
update: "disable",
|
|
42784
|
+
share: "disabled",
|
|
42785
|
+
snapshots: false,
|
|
42786
|
+
lsp: false,
|
|
42787
|
+
formatter: false,
|
|
42788
|
+
...Object.keys(compaction).length > 0 ? { compaction } : {}
|
|
42789
|
+
};
|
|
42790
|
+
}
|
|
42791
|
+
function openCodeConfigJson(spec) {
|
|
42792
|
+
return JSON.stringify(openCodeConfig(spec), null, 2) + "\n";
|
|
42793
|
+
}
|
|
42794
|
+
function openCodeLaunchFiles(spec) {
|
|
42795
|
+
return [
|
|
42796
|
+
{ path: spec.paths.config, content: openCodeConfigJson(spec) },
|
|
42797
|
+
{ path: spec.paths.plugin, content: OPENCODE_PLUGIN_SOURCE },
|
|
42798
|
+
{ path: spec.paths.tailerScript, content: OPENCODE_TAILER_SOURCE }
|
|
42799
|
+
];
|
|
42800
|
+
}
|
|
42801
|
+
function openCodeLaunchArgs() {
|
|
42802
|
+
return ["serve", "--hostname", "127.0.0.1", "--port", PORT_ARG];
|
|
42803
|
+
}
|
|
42804
|
+
function openCodePassword(bearer) {
|
|
42805
|
+
return bearerHashOf(bearer) ?? randomBytes5(24).toString("base64url");
|
|
42806
|
+
}
|
|
42807
|
+
function openCodeLaunchEnv(spec, bearer, password) {
|
|
42808
|
+
const { paths } = spec;
|
|
42809
|
+
return {
|
|
42810
|
+
[RUN_BEARER_ENV]: bearer,
|
|
42811
|
+
[HARNESS_URL_ENV]: spec.harnessUrl,
|
|
42812
|
+
SWITCHBOARD_RUN_ID: spec.runId,
|
|
42813
|
+
XDG_DATA_HOME: paths.xdg.data,
|
|
42814
|
+
XDG_CONFIG_HOME: paths.xdg.config,
|
|
42815
|
+
XDG_CACHE_HOME: paths.xdg.cache,
|
|
42816
|
+
XDG_STATE_HOME: paths.xdg.state,
|
|
42817
|
+
OPENCODE_CONFIG: paths.config,
|
|
42818
|
+
OPENCODE_CONFIG_PROJECT_DISABLE: "1",
|
|
42819
|
+
OPENCODE_DISABLE_PROJECT_CONFIG: "1",
|
|
42820
|
+
OPENCODE_DISABLE_MODELS_FETCH: "1",
|
|
42821
|
+
OPENCODE_DISABLE_AUTOUPDATE: "1",
|
|
42822
|
+
OPENCODE_DB: ":memory:",
|
|
42823
|
+
[OPENCODE_PASSWORD_ENV]: password
|
|
42824
|
+
};
|
|
42825
|
+
}
|
|
42826
|
+
function openCodeTailerEnv(password, servePid) {
|
|
42827
|
+
return { [OPENCODE_PASSWORD_ENV]: password, [OPENCODE_SERVE_PID_ENV]: String(servePid) };
|
|
42828
|
+
}
|
|
42829
|
+
function configProblem(info, spec) {
|
|
42830
|
+
const providers = info.providers;
|
|
42831
|
+
const agents = info.agents;
|
|
42832
|
+
if (typeof providers?.[PROXY_PROVIDER] !== "object") return `providers.${PROXY_PROVIDER} did not take`;
|
|
42833
|
+
if (info.default_agent !== OPENCODE_AGENT) return `default_agent is not ${OPENCODE_AGENT}`;
|
|
42834
|
+
if (typeof agents?.[OPENCODE_AGENT] !== "object") return `agents.${OPENCODE_AGENT} did not take`;
|
|
42835
|
+
if (agents?.title?.disabled !== true) return "agents.title.disabled did not take";
|
|
42836
|
+
if (!Array.isArray(info.plugins) || !info.plugins.includes(OPENCODE_PLUGIN_REF)) return "plugins did not take";
|
|
42837
|
+
if (info.update !== "disable") return "update is not disable";
|
|
42838
|
+
if (info.share !== "disabled") return "share is not disabled";
|
|
42839
|
+
if (info.snapshots !== false) return "snapshots is not false";
|
|
42840
|
+
if (info.lsp !== false) return "lsp is not false";
|
|
42841
|
+
if (info.formatter !== false) return "formatter is not false";
|
|
42842
|
+
if (spec.compaction && Object.keys(spec.compaction).length > 0 && typeof info.compaction !== "object")
|
|
42843
|
+
return "compaction did not take";
|
|
42844
|
+
return void 0;
|
|
42845
|
+
}
|
|
42846
|
+
function loadedDocument(entries, configPath) {
|
|
42847
|
+
const document = entries.find((e) => e.type === "document" && e.path === configPath);
|
|
42848
|
+
return document && document.type === "document" ? document.info : void 0;
|
|
42849
|
+
}
|
|
42850
|
+
async function launchOpenCode(deps, spec, bearer) {
|
|
42851
|
+
const { container, clock: clock4, sleep: sleep2 } = deps;
|
|
42852
|
+
const pollMs = deps.pollMs ?? OPENCODE_READY_POLL_MS;
|
|
42853
|
+
const readyMs = deps.readyMs ?? OPENCODE_READY_MS;
|
|
42854
|
+
const root = await container.makeRoot(spec.paths.dir);
|
|
42855
|
+
const paths = root === spec.paths.dir ? spec.paths : openCodeRunPathsAt(root);
|
|
42856
|
+
const placed = { ...spec, paths };
|
|
42857
|
+
for (const file of openCodeLaunchFiles(placed)) await container.writeFile(file.path, file.content);
|
|
42858
|
+
const password = openCodePassword(bearer);
|
|
42859
|
+
const started = await container.start({
|
|
42860
|
+
paths,
|
|
42861
|
+
command: OPENCODE_BIN,
|
|
42862
|
+
args: openCodeLaunchArgs(),
|
|
42863
|
+
env: openCodeLaunchEnv(placed, bearer, password),
|
|
42864
|
+
port: "free"
|
|
42865
|
+
});
|
|
42866
|
+
if (started.port === void 0)
|
|
42867
|
+
throw new HarnessContainerError("start", "the start answered no port for opencode serve");
|
|
42868
|
+
const { pid, port } = started;
|
|
42869
|
+
const auth = { Authorization: openCodeAuthHeader(password) };
|
|
42870
|
+
const request2 = (route3) => container.request(paths, { method: route3.method, port, path: route3.path, secretHeaders: auth });
|
|
42871
|
+
const notReady = async (reason2, errLog = paths.errLog) => new OpenCodeNotReadyError(reason2, await container.tail(errLog, READY_TAIL_BYTES));
|
|
42872
|
+
const startedAt = clock4();
|
|
42873
|
+
const deadline = startedAt + readyMs;
|
|
42874
|
+
let last = "no answer yet";
|
|
42875
|
+
let version2;
|
|
42876
|
+
let probePid = true;
|
|
42877
|
+
let poll = pollMs;
|
|
42878
|
+
while (version2 === void 0) {
|
|
42879
|
+
if (probePid && !await container.alive(pid))
|
|
42880
|
+
throw await notReady("the server exited before it answered its health");
|
|
42881
|
+
let res;
|
|
42882
|
+
try {
|
|
42883
|
+
res = await request2(OPENCODE_ROUTES["health.get"]);
|
|
42884
|
+
probePid = false;
|
|
42885
|
+
} catch (err2) {
|
|
42886
|
+
if (isContainerGone(err2)) throw err2;
|
|
42887
|
+
last = err2 instanceof Error ? err2.message : String(err2);
|
|
42888
|
+
probePid = true;
|
|
42889
|
+
}
|
|
42890
|
+
if (res) {
|
|
42891
|
+
if (res.status === 401) throw await notReady("the server refused the run's password");
|
|
42892
|
+
if (res.status === 500) throw await notReady("the server reported that its start failed (health answered 500)");
|
|
42893
|
+
if (res.status === 200) {
|
|
42894
|
+
const health = parseHealth(res.body);
|
|
42895
|
+
if (!health) throw await notReady("the health answered something that is not the health shape");
|
|
42896
|
+
if (health.version !== OPENCODE_VERSION)
|
|
42897
|
+
throw await notReady(`the opencode on PATH is ${health.version}; this build drives ${OPENCODE_VERSION}`);
|
|
42898
|
+
version2 = health.version;
|
|
42899
|
+
break;
|
|
42900
|
+
}
|
|
42901
|
+
last = `the health answered ${res.status}`;
|
|
42902
|
+
}
|
|
42903
|
+
if (clock4() >= deadline)
|
|
42904
|
+
throw await notReady(`the server did not answer its health within ${readyMs} ms (${last})`);
|
|
42905
|
+
await sleep2(poll);
|
|
42906
|
+
if (clock4() - startedAt >= 1e3) poll = Math.min(OPENCODE_READY_POLL_MAX_MS, poll * 2);
|
|
42907
|
+
}
|
|
42908
|
+
const config = await request2(OPENCODE_ROUTES["config.get"]);
|
|
42909
|
+
if (config.status !== 200) throw await notReady(`the configuration route answered ${config.status}`);
|
|
42910
|
+
const entries = parseConfigEntries(config.body);
|
|
42911
|
+
if (!entries) throw await notReady("the configuration route answered something that is not the entry list");
|
|
42912
|
+
const info = loadedDocument(entries, paths.config);
|
|
42913
|
+
if (!info) throw await notReady("the server did not load the run's configuration file");
|
|
42914
|
+
const problem = configProblem(info, placed);
|
|
42915
|
+
if (problem) throw await notReady(`the run's configuration ${problem}`);
|
|
42916
|
+
const activation = await request2(OPENCODE_ROUTES["plugin.awaitActivation"]);
|
|
42917
|
+
if (activation.status < 200 || activation.status >= 300)
|
|
42918
|
+
throw await notReady(`plugin activation answered ${activation.status}`);
|
|
42919
|
+
const tailer = await container.start({
|
|
42920
|
+
paths: paths.tailer,
|
|
42921
|
+
command: TAILER_BIN,
|
|
42922
|
+
args: [paths.tailerScript],
|
|
42923
|
+
env: openCodeTailerEnv(password, pid),
|
|
42924
|
+
port,
|
|
42925
|
+
// The feed is read by offset and a later generation may restart the
|
|
42926
|
+
// tailer over it: its log is never truncated by a start.
|
|
42927
|
+
keepLog: true
|
|
42928
|
+
});
|
|
42929
|
+
let feedOffset;
|
|
42930
|
+
poll = pollMs;
|
|
42931
|
+
while (feedOffset === void 0) {
|
|
42932
|
+
const text = Buffer.from(await container.readLog(paths.feed, 0, LOG_READ_BYTES)).toString("utf8");
|
|
42933
|
+
let consumed = 0;
|
|
42934
|
+
for (const line3 of text.split("\n").slice(0, -1)) {
|
|
42935
|
+
consumed += Buffer.byteLength(line3, "utf8") + 1;
|
|
42936
|
+
const record2 = parseFeedRecord(line3);
|
|
42937
|
+
if (record2?.feed === "tailer" && record2.note === "connected") {
|
|
42938
|
+
feedOffset = consumed;
|
|
42939
|
+
break;
|
|
42940
|
+
}
|
|
42941
|
+
}
|
|
42942
|
+
if (feedOffset !== void 0) break;
|
|
42943
|
+
if (!await container.alive(tailer.pid))
|
|
42944
|
+
throw await notReady("the tailer exited before it connected to the event stream", paths.tailer.errLog);
|
|
42945
|
+
if (clock4() >= deadline)
|
|
42946
|
+
throw await notReady(`the tailer did not connect to the event stream within ${readyMs} ms`, paths.tailer.errLog);
|
|
42947
|
+
await sleep2(poll);
|
|
42948
|
+
if (clock4() - startedAt >= 1e3) poll = Math.min(OPENCODE_READY_POLL_MAX_MS, poll * 2);
|
|
42949
|
+
}
|
|
42950
|
+
return { pid, port, tailerPid: tailer.pid, password, paths, version: version2, feedOffset };
|
|
42951
|
+
}
|
|
42952
|
+
function openCodeFacts(started, run2) {
|
|
42953
|
+
const bearerHash = run2.bearer === void 0 ? void 0 : bearerHashOf(run2.bearer);
|
|
42954
|
+
return {
|
|
42955
|
+
harness: "opencode",
|
|
42956
|
+
pid: started.pid,
|
|
42957
|
+
port: started.port,
|
|
42958
|
+
tailerPid: started.tailerPid,
|
|
42959
|
+
logOffset: run2.logOffset,
|
|
42960
|
+
sessionID: run2.sessionID,
|
|
42961
|
+
root: started.paths.dir,
|
|
42962
|
+
relaunches: run2.relaunches,
|
|
42963
|
+
...bearerHash === void 0 ? {} : { bearerHash },
|
|
42964
|
+
...run2.container === void 0 ? {} : { container: run2.container }
|
|
42965
|
+
};
|
|
42966
|
+
}
|
|
42967
|
+
var OPENCODE_BIN, TAILER_BIN, OPENCODE_AGENT, OPENCODE_PLUGIN_REF, OPENCODE_PASSWORD_ENV, OPENCODE_READY_MS, OPENCODE_READY_POLL_MS, OPENCODE_READY_POLL_MAX_MS, OPENCODE_DEFAULT_CONTEXT_TOKENS, READY_TAIL_BYTES, OPENCODE_BUILTIN_TOOLS, OPENCODE_READ_TOOLS, OPENCODE_NO_TOOLS, ASK_ALL_RULE, OPENCODE_ALWAYS_DENIED, OPENCODE_NONE_DENIED, OPENCODE_READ_DENIED, OpenCodeNotReadyError;
|
|
42968
|
+
var init_process2 = __esm({
|
|
42969
|
+
"../../src/core/harness/opencode/process.ts"() {
|
|
42970
|
+
"use strict";
|
|
42971
|
+
init_runBearers();
|
|
42972
|
+
init_container();
|
|
42973
|
+
init_process();
|
|
42974
|
+
init_client2();
|
|
42975
|
+
init_pluginSource();
|
|
42976
|
+
init_tailerSource();
|
|
42977
|
+
OPENCODE_BIN = "opencode";
|
|
42978
|
+
TAILER_BIN = "node";
|
|
42979
|
+
OPENCODE_AGENT = "switchboard";
|
|
42980
|
+
OPENCODE_PLUGIN_REF = "./plugins/switchboard";
|
|
42981
|
+
OPENCODE_PASSWORD_ENV = "OPENCODE_PASSWORD";
|
|
42982
|
+
OPENCODE_READY_MS = 3e4;
|
|
42983
|
+
OPENCODE_READY_POLL_MS = 250;
|
|
42984
|
+
OPENCODE_READY_POLL_MAX_MS = 2e3;
|
|
42985
|
+
OPENCODE_DEFAULT_CONTEXT_TOKENS = 2e5;
|
|
42986
|
+
READY_TAIL_BYTES = 2e3;
|
|
42987
|
+
OPENCODE_BUILTIN_TOOLS = ["read", "shell", "edit", "write", "glob", "grep", "skill"];
|
|
42988
|
+
OPENCODE_READ_TOOLS = ["read", "shell", "glob", "grep", "skill"];
|
|
42989
|
+
OPENCODE_NO_TOOLS = [];
|
|
42990
|
+
ASK_ALL_RULE = { action: "*", resource: "*", effect: "ask" };
|
|
42991
|
+
OPENCODE_ALWAYS_DENIED = ["question", "subagent", "execute", "webfetch", "websearch"];
|
|
42992
|
+
OPENCODE_NONE_DENIED = [
|
|
42993
|
+
"read",
|
|
42994
|
+
"edit",
|
|
42995
|
+
"shell",
|
|
42996
|
+
"glob",
|
|
42997
|
+
"grep",
|
|
42998
|
+
"skill",
|
|
42999
|
+
"external_directory"
|
|
43000
|
+
];
|
|
43001
|
+
OPENCODE_READ_DENIED = ["edit"];
|
|
43002
|
+
OpenCodeNotReadyError = class extends Error {
|
|
43003
|
+
constructor(reason2, stderr) {
|
|
43004
|
+
super(`opencode serve is not ready: ${reason2}${stderr.trim() ? ` (stderr: ${stderr.trim().slice(-600)})` : ""}`);
|
|
43005
|
+
this.reason = reason2;
|
|
43006
|
+
this.name = "OpenCodeNotReadyError";
|
|
43007
|
+
}
|
|
43008
|
+
reason;
|
|
43009
|
+
};
|
|
43010
|
+
}
|
|
43011
|
+
});
|
|
43012
|
+
|
|
43013
|
+
// ../../src/core/harness/opencode/bridge.ts
|
|
43014
|
+
function openCodeToolNameWord(name) {
|
|
43015
|
+
return TOOL_NAME_WORD[name] ?? name;
|
|
43016
|
+
}
|
|
43017
|
+
function contentText(content) {
|
|
43018
|
+
if (typeof content === "string") return content;
|
|
43019
|
+
if (!Array.isArray(content)) return "";
|
|
43020
|
+
return content.filter((p) => isRecord8(p) && p.type === "text" && typeof p.text === "string").map((p) => String(p.text)).join("\n");
|
|
43021
|
+
}
|
|
43022
|
+
function isBudgetRefusal(error) {
|
|
43023
|
+
if (error.status !== void 0 && error.status !== 403) return false;
|
|
43024
|
+
return TURN_BUDGET_REFUSAL.test(error.message);
|
|
43025
|
+
}
|
|
43026
|
+
function judgeOpenCodeAsk(action, resources, rules, relayedToolNames) {
|
|
43027
|
+
const word = openCodeToolWord(action);
|
|
43028
|
+
const tool = TOOL_NAME_WORD[action] ?? word;
|
|
43029
|
+
if (relayedToolNames.has(action) || relayedToolNames.has(word)) return { reply: "once", tool: action };
|
|
43030
|
+
const refuse3 = (reason2) => ({ reply: "reject", message: reason2, tool });
|
|
43031
|
+
if (action === "external_directory") {
|
|
43032
|
+
for (const resource of resources) {
|
|
43033
|
+
const verdict2 = judgeToolCall("read", { path: resource }, rules);
|
|
43034
|
+
if (verdict2.verdict !== "allowed") return refuse3(verdict2.reason);
|
|
43035
|
+
}
|
|
43036
|
+
return { reply: "once", tool: "external_directory" };
|
|
43037
|
+
}
|
|
43038
|
+
if (word === "bash") {
|
|
43039
|
+
for (const command2 of resources.length > 0 ? resources : [""]) {
|
|
43040
|
+
const verdict2 = judgeToolCall("bash", { command: command2 }, rules);
|
|
43041
|
+
if (verdict2.verdict !== "allowed") return refuse3(verdict2.reason);
|
|
43042
|
+
}
|
|
43043
|
+
return { reply: "once", tool };
|
|
43044
|
+
}
|
|
43045
|
+
const input = word === "read" || word === "edit" || word === "write" ? { path: resources[0] } : {};
|
|
43046
|
+
const verdict = judgeToolCall(word, input, rules);
|
|
43047
|
+
if (verdict.verdict !== "allowed") return refuse3(verdict.reason);
|
|
43048
|
+
return { reply: "once", tool };
|
|
43049
|
+
}
|
|
43050
|
+
function assistantMessage2(turn) {
|
|
43051
|
+
return {
|
|
43052
|
+
role: "assistant",
|
|
43053
|
+
content: turn.content.map(
|
|
43054
|
+
(p) => p.type === "text" ? { type: "text", text: p.text } : p.type === "tool_use" ? { type: "toolCall", id: p.id, name: p.name, arguments: isRecord8(p.input) ? p.input : {} } : {}
|
|
43055
|
+
)
|
|
43056
|
+
};
|
|
43057
|
+
}
|
|
43058
|
+
function userMessage(turn) {
|
|
43059
|
+
const results = turn.content.filter(
|
|
43060
|
+
(p) => p.type === "tool_result"
|
|
43061
|
+
);
|
|
43062
|
+
if (results.length > 0) {
|
|
43063
|
+
return {
|
|
43064
|
+
role: "toolResult",
|
|
43065
|
+
toolCallId: results[0].toolUseId,
|
|
43066
|
+
content: [{ type: "text", text: typeof results[0].content === "string" ? results[0].content : "" }],
|
|
43067
|
+
isError: results[0].isError === true
|
|
43068
|
+
};
|
|
43069
|
+
}
|
|
43070
|
+
return {
|
|
43071
|
+
role: "user",
|
|
43072
|
+
content: turn.content.map((p) => p.type === "text" ? { type: "text", text: p.text } : {})
|
|
43073
|
+
};
|
|
43074
|
+
}
|
|
43075
|
+
function projectStore(messages, seedLength) {
|
|
43076
|
+
const turns = [];
|
|
43077
|
+
messages.slice(seedLength).forEach((message4) => {
|
|
43078
|
+
if (message4.type === "assistant") {
|
|
43079
|
+
const assistant = message4;
|
|
43080
|
+
const content = [];
|
|
43081
|
+
const results = [];
|
|
43082
|
+
for (const part of Array.isArray(assistant.content) ? assistant.content : []) {
|
|
43083
|
+
if (!isRecord8(part)) continue;
|
|
43084
|
+
if (part.type === "text" && typeof part.text === "string") content.push({ type: "text", text: part.text });
|
|
43085
|
+
else if (part.type === "tool") {
|
|
43086
|
+
const id = str6(part.id);
|
|
43087
|
+
const name = openCodeToolNameWord(str6(part.name));
|
|
43088
|
+
const state = isRecord8(part.state) ? part.state : {};
|
|
43089
|
+
content.push({ type: "tool_use", id, name, input: isRecord8(state.input) ? state.input : {} });
|
|
43090
|
+
if (state.status === "completed" || state.status === "error")
|
|
43091
|
+
results.push({
|
|
43092
|
+
type: "tool_result",
|
|
43093
|
+
toolUseId: id,
|
|
43094
|
+
content: contentText(state.content) || (state.status === "error" ? errorMessage(state.error) : ""),
|
|
43095
|
+
...state.status === "error" ? { isError: true } : {}
|
|
43096
|
+
});
|
|
43097
|
+
}
|
|
43098
|
+
}
|
|
43099
|
+
if (content.length > 0) turns.push({ role: "assistant", content });
|
|
43100
|
+
for (const result of results) turns.push({ role: "user", content: [result] });
|
|
43101
|
+
} else if (message4.type === "user") {
|
|
43102
|
+
const text = str6(message4.text);
|
|
43103
|
+
if (text) turns.push({ role: "user", content: [{ type: "text", text }] });
|
|
43104
|
+
}
|
|
43105
|
+
});
|
|
43106
|
+
return turns;
|
|
43107
|
+
}
|
|
43108
|
+
function errorMessage(error) {
|
|
43109
|
+
return isRecord8(error) && typeof error.message === "string" ? error.message : "";
|
|
43110
|
+
}
|
|
43111
|
+
function statusOf(error) {
|
|
43112
|
+
return isRecord8(error) && typeof error.status === "number" ? error.status : void 0;
|
|
43113
|
+
}
|
|
43114
|
+
function openCodePromptText(messages) {
|
|
43115
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
43116
|
+
const message4 = messages[i];
|
|
43117
|
+
if (message4.role === "user")
|
|
43118
|
+
return message4.content.filter((p) => p.type === "text").map((p) => p.text).join("\n\n");
|
|
43119
|
+
}
|
|
43120
|
+
return "";
|
|
43121
|
+
}
|
|
43122
|
+
async function driveOpenCode(deps, run2, conn) {
|
|
43123
|
+
const now = () => deps.clock();
|
|
43124
|
+
const agentSpan = run2.span?.start("run.agent");
|
|
43125
|
+
if (agentSpan) deps.bearers?.reparent(run2.runId, agentSpan);
|
|
43126
|
+
const remainingMs = run2.resume?.remainingMs ?? run2.agent.maxMinutes * 6e4;
|
|
43127
|
+
const deadline = now() + remainingMs;
|
|
43128
|
+
const warnAt = deadline - Math.min(3 * 6e4, run2.agent.maxMinutes * 15e3);
|
|
43129
|
+
const emit = (event) => run2.onEvent?.(event.at === void 0 ? { ...event, at: now() } : event);
|
|
43130
|
+
const note = (kind, summary) => {
|
|
43131
|
+
run2.onProgress?.(summary);
|
|
43132
|
+
emit({ type: "run_note", kind, summary });
|
|
43133
|
+
};
|
|
43134
|
+
const bridge = new OpenCodeBridge({
|
|
43135
|
+
emit,
|
|
43136
|
+
...run2.onProgress ? { onProgress: run2.onProgress } : {},
|
|
43137
|
+
clock: deps.clock,
|
|
43138
|
+
...agentSpan ? { agentSpan } : {},
|
|
43139
|
+
rules: { ...run2.rules, identity: run2.agent.identity },
|
|
43140
|
+
relayedToolNames: new Set(run2.tools.map((t) => t.name)),
|
|
43141
|
+
...run2.onStep ? { onStep: run2.onStep } : {},
|
|
43142
|
+
// The mirror skips the seed's rows: a fresh run's is the thread's turns plus
|
|
43143
|
+
// the request (`run.messages`); a rebuild's is the record it imported (its
|
|
43144
|
+
// transcript and every compaction row), which the store holds and the
|
|
43145
|
+
// request-prompt appends past.
|
|
43146
|
+
seedLength: run2.resume ? run2.resume.messages.length + (run2.resume.compactions?.length ?? 0) : run2.messages.length,
|
|
43147
|
+
remainingMs: () => deadline - now(),
|
|
43148
|
+
textFailing: new Set(run2.tools.filter((t) => t.failsInText).map((t) => t.name))
|
|
43149
|
+
});
|
|
43150
|
+
if (run2.resume && run2.resume.settlements.length > 0) {
|
|
43151
|
+
bridge.prime(
|
|
43152
|
+
run2.resume.settlements.map((s) => ({
|
|
43153
|
+
type: "tool_result",
|
|
43154
|
+
toolUseId: s.toolUse.id,
|
|
43155
|
+
content: openCodeSettlementNote(s),
|
|
43156
|
+
isError: true
|
|
43157
|
+
}))
|
|
43158
|
+
);
|
|
43159
|
+
}
|
|
43160
|
+
const auth = { Authorization: openCodeAuthHeader(conn.password) };
|
|
43161
|
+
const sessionRoutes = openCodeSessionRoutes(conn.sessionID);
|
|
43162
|
+
const request2 = (route3, body) => conn.container.request(conn.paths, {
|
|
43163
|
+
method: route3.method,
|
|
43164
|
+
port: conn.port,
|
|
43165
|
+
path: route3.path,
|
|
43166
|
+
secretHeaders: auth,
|
|
43167
|
+
...body !== void 0 ? { headers: { "content-type": "application/json" }, body: JSON.stringify(body) } : {}
|
|
43168
|
+
});
|
|
43169
|
+
const transport = new PiRpcTransport({
|
|
43170
|
+
container: conn.container,
|
|
43171
|
+
paths: { ...conn.paths.tailer, log: conn.paths.feed },
|
|
43172
|
+
pid: conn.tailerPid,
|
|
43173
|
+
pollMs: deps.pollMs ?? 250,
|
|
43174
|
+
sleep: deps.sleep,
|
|
43175
|
+
offset: conn.feedOffset
|
|
43176
|
+
});
|
|
43177
|
+
let writeUp;
|
|
43178
|
+
let hardStopped = false;
|
|
43179
|
+
let warned = false;
|
|
43180
|
+
let bypass;
|
|
43181
|
+
let replyFailed;
|
|
43182
|
+
let providerError;
|
|
43183
|
+
let settled = false;
|
|
43184
|
+
let containerSaid;
|
|
43185
|
+
const recordBase = {
|
|
43186
|
+
messages: run2.resume?.messages ?? run2.messages,
|
|
43187
|
+
compactions: run2.resume?.compactions ?? []
|
|
43188
|
+
};
|
|
43189
|
+
const startWriteUp = (w, instruction) => {
|
|
43190
|
+
writeUp = w;
|
|
43191
|
+
if (conn.writeUp)
|
|
43192
|
+
conn.writeUp.blocked = w.kind === "time" ? "the run has reached its time budget: no more tool calls \u2014 the run is writing its final answer" : "the run has hit its turn guard: no more tool calls \u2014 the run is writing its final answer";
|
|
43193
|
+
void request2(sessionRoutes["session.prompt"], { text: instruction, delivery: "steer" }).catch(() => {
|
|
43194
|
+
});
|
|
43195
|
+
};
|
|
43196
|
+
const turnCount = () => deps.bearers?.grantOf(run2.runId)?.turns ?? bridge.turns;
|
|
43197
|
+
const check = () => {
|
|
43198
|
+
if (run2.control?.requested === "hard") {
|
|
43199
|
+
if (!hardStopped) {
|
|
43200
|
+
hardStopped = true;
|
|
43201
|
+
run2.onProgress?.(hardStopNote());
|
|
43202
|
+
emit({ type: "run_note", kind: "stopped", summary: hardStopNote(), mode: "hard" });
|
|
43203
|
+
void request2(sessionRoutes["session.interrupt"]).catch(() => {
|
|
43204
|
+
});
|
|
43205
|
+
}
|
|
43206
|
+
return;
|
|
43207
|
+
}
|
|
43208
|
+
if (writeUp) return;
|
|
43209
|
+
if (now() >= deadline) {
|
|
43210
|
+
note("time_budget_exhausted", timeBudgetNote());
|
|
43211
|
+
startWriteUp({ kind: "time" }, timeBudgetInstruction());
|
|
43212
|
+
return;
|
|
43213
|
+
}
|
|
43214
|
+
if (turnCount() >= run2.agent.maxTurns) {
|
|
43215
|
+
const pace = turnGuardPace(turnCount(), run2.agent.maxMinutes * 6e4 - (deadline - now()));
|
|
43216
|
+
note("turn_budget_exhausted", turnGuardNote(pace));
|
|
43217
|
+
startWriteUp({ kind: "turns", pace }, turnGuardInstruction(pace));
|
|
43218
|
+
return;
|
|
43219
|
+
}
|
|
43220
|
+
if (!warned && now() >= warnAt) {
|
|
43221
|
+
warned = true;
|
|
43222
|
+
const minutesLeft = Math.max(1, Math.round((deadline - now()) / 6e4));
|
|
43223
|
+
note("wrap_up", wrapUpNote(minutesLeft));
|
|
43224
|
+
void request2(sessionRoutes["session.prompt"], { text: wrapUpInstruction(minutesLeft), delivery: "steer" }).catch(
|
|
43225
|
+
() => {
|
|
43226
|
+
}
|
|
43227
|
+
);
|
|
43228
|
+
}
|
|
43229
|
+
};
|
|
43230
|
+
try {
|
|
43231
|
+
await request2(sessionRoutes["session.prompt"], { text: openCodePromptText(run2.messages), delivery: "queue" });
|
|
43232
|
+
check();
|
|
43233
|
+
const iterator = transport.lines[Symbol.asyncIterator]();
|
|
43234
|
+
let pending2;
|
|
43235
|
+
for (; ; ) {
|
|
43236
|
+
pending2 ??= iterator.next();
|
|
43237
|
+
const tick = deps.sleep(deps.tickMs ?? 1e3).then(() => "tick");
|
|
43238
|
+
let next;
|
|
43239
|
+
try {
|
|
43240
|
+
next = await Promise.race([pending2, tick]);
|
|
43241
|
+
} catch (err2) {
|
|
43242
|
+
if (!(err2 instanceof Error && saysContainerReplaced(err2))) throw err2;
|
|
43243
|
+
containerSaid = err2;
|
|
43244
|
+
break;
|
|
43245
|
+
}
|
|
43246
|
+
if (next === "tick") {
|
|
43247
|
+
check();
|
|
43248
|
+
if (hardStopped) break;
|
|
43249
|
+
continue;
|
|
43250
|
+
}
|
|
43251
|
+
pending2 = void 0;
|
|
43252
|
+
if (next.done) break;
|
|
43253
|
+
const record2 = parseFeedRecord(next.value);
|
|
43254
|
+
if (!record2) continue;
|
|
43255
|
+
const obs = bridge.observe(record2);
|
|
43256
|
+
for (const reply of obs.replies) {
|
|
43257
|
+
let failure2;
|
|
43258
|
+
try {
|
|
43259
|
+
const res = await request2(openCodePermissionReplyRoute(conn.sessionID, reply.requestID), {
|
|
43260
|
+
reply: reply.reply,
|
|
43261
|
+
...reply.message ? { message: reply.message } : {}
|
|
43262
|
+
});
|
|
43263
|
+
if (res.status < 200 || res.status >= 300) failure2 = { status: res.status };
|
|
43264
|
+
} catch (err2) {
|
|
43265
|
+
failure2 = err2 instanceof Error ? err2 : new Error(String(err2));
|
|
43266
|
+
}
|
|
43267
|
+
if (failure2 !== void 0) {
|
|
43268
|
+
replyFailed = new OpenCodeReplyFailedError(reply.requestID, reply.callId, reply.reply, failure2);
|
|
43269
|
+
note("harness_error", `${replyFailed.message} \u2014 the run is stopped`);
|
|
43270
|
+
void request2(sessionRoutes["session.interrupt"]).catch(() => {
|
|
43271
|
+
});
|
|
43272
|
+
break;
|
|
43273
|
+
}
|
|
43274
|
+
}
|
|
43275
|
+
if (replyFailed) break;
|
|
43276
|
+
if (obs.bypass) {
|
|
43277
|
+
bypass = obs.bypass;
|
|
43278
|
+
void request2(sessionRoutes["session.interrupt"]).catch(() => {
|
|
43279
|
+
});
|
|
43280
|
+
break;
|
|
43281
|
+
}
|
|
43282
|
+
if (obs.budgetStop && !writeUp) {
|
|
43283
|
+
const pace = turnGuardPace(turnCount(), run2.agent.maxMinutes * 6e4 - (deadline - now()));
|
|
43284
|
+
note("turn_budget_exhausted", turnGuardNote(pace));
|
|
43285
|
+
startWriteUp({ kind: "turns", pace }, turnGuardInstruction(pace));
|
|
43286
|
+
}
|
|
43287
|
+
if (obs.providerError !== void 0) providerError = obs.providerError;
|
|
43288
|
+
if (obs.settled) {
|
|
43289
|
+
settled = true;
|
|
43290
|
+
break;
|
|
43291
|
+
}
|
|
43292
|
+
check();
|
|
43293
|
+
if (hardStopped) break;
|
|
43294
|
+
}
|
|
43295
|
+
} finally {
|
|
43296
|
+
transport.close();
|
|
43297
|
+
agentSpan?.end(hardStopped || bypass || replyFailed || containerSaid ? "error" : "ok");
|
|
43298
|
+
}
|
|
43299
|
+
if (containerSaid !== void 0) {
|
|
43300
|
+
const record2 = bridge.record(recordBase, deadline);
|
|
43301
|
+
bridge.closeOpenSpans((open2) => openCodeReplacedCallNote(open2.tool));
|
|
43302
|
+
const now2 = await conn.container.identity().catch(() => void 0);
|
|
43303
|
+
const replaced = new OpenCodeContainerReplacedError(containerSaid.message, conn.containerWord, now2, record2);
|
|
43304
|
+
note("sandbox_restarted", replaced.message);
|
|
43305
|
+
throw replaced;
|
|
43306
|
+
}
|
|
43307
|
+
if (bypass) throw bypass;
|
|
43308
|
+
if (replyFailed) throw replyFailed;
|
|
43309
|
+
if (hardStopped) return { answer: HARD_STOP_MESSAGE };
|
|
43310
|
+
if (providerError !== void 0) throw new Error(`the model call failed: ${providerError}`);
|
|
43311
|
+
if (!settled) throw new Error("the OpenCode run ended before its execution settled");
|
|
43312
|
+
await bridge.flush();
|
|
43313
|
+
const text = bridge.answer() ?? "";
|
|
43314
|
+
const answer = writeUpAnswer(writeUp, text, run2.agent.maxMinutes);
|
|
43315
|
+
return { answer };
|
|
43316
|
+
}
|
|
43317
|
+
function writeUpAnswer(writeUp, text, maxMinutes) {
|
|
43318
|
+
if (writeUp?.kind === "time") return timeBudgetAnswer(text, maxMinutes);
|
|
43319
|
+
if (writeUp?.kind === "turns") return turnGuardAnswer(text, writeUp.pace);
|
|
43320
|
+
return text || "_(no response)_";
|
|
43321
|
+
}
|
|
43322
|
+
var OpenCodeGateBypassedError, OpenCodeReplyFailedError, OpenCodeContainerReplacedError, TOOL_NAME_WORD, isRecord8, str6, TURN_BUDGET_CODE, TURN_BUDGET_REFUSAL, OpenCodeBridge;
|
|
43323
|
+
var init_bridge3 = __esm({
|
|
43324
|
+
"../../src/core/harness/opencode/bridge.ts"() {
|
|
43325
|
+
"use strict";
|
|
43326
|
+
init_runEvents();
|
|
43327
|
+
init_contract4();
|
|
43328
|
+
init_harness();
|
|
43329
|
+
init_bridge2();
|
|
43330
|
+
init_mirror();
|
|
43331
|
+
init_transport();
|
|
43332
|
+
init_toolRules();
|
|
43333
|
+
init_windDown();
|
|
43334
|
+
init_client2();
|
|
43335
|
+
init_dispositions();
|
|
43336
|
+
init_session2();
|
|
43337
|
+
init_process2();
|
|
43338
|
+
OpenCodeGateBypassedError = class extends Error {
|
|
43339
|
+
constructor(detail) {
|
|
43340
|
+
super(`the gate was bypassed: ${detail}`);
|
|
43341
|
+
this.detail = detail;
|
|
43342
|
+
this.name = "OpenCodeGateBypassedError";
|
|
43343
|
+
}
|
|
43344
|
+
detail;
|
|
43345
|
+
};
|
|
43346
|
+
OpenCodeReplyFailedError = class extends Error {
|
|
43347
|
+
constructor(requestID, callId, reply, cause) {
|
|
43348
|
+
super(
|
|
43349
|
+
`the gate's reply (${reply}) for request ${requestID}${callId ? ` (call ${callId})` : ""} could not be posted (${cause instanceof Error ? cause.message : `the server answered ${cause.status}`}): the ask stays unanswered, so the run stops rather than hang`
|
|
43350
|
+
);
|
|
43351
|
+
this.requestID = requestID;
|
|
43352
|
+
this.callId = callId;
|
|
43353
|
+
this.reply = reply;
|
|
43354
|
+
this.cause = cause;
|
|
43355
|
+
this.name = "OpenCodeReplyFailedError";
|
|
43356
|
+
}
|
|
43357
|
+
requestID;
|
|
43358
|
+
callId;
|
|
43359
|
+
reply;
|
|
43360
|
+
cause;
|
|
43361
|
+
};
|
|
43362
|
+
OpenCodeContainerReplacedError = class extends HarnessContainerReplacedError {
|
|
43363
|
+
constructor(said2, was, now, record2) {
|
|
43364
|
+
super(
|
|
43365
|
+
`the container running OpenCode was replaced (${was ?? "unknown"} \u2192 ${now ?? "unknown"}; the executor said: ${redactAndCap(said2.replace(/\s+/g, " ").trim(), 240)})`,
|
|
43366
|
+
said2,
|
|
43367
|
+
was,
|
|
43368
|
+
now,
|
|
43369
|
+
record2
|
|
43370
|
+
);
|
|
43371
|
+
this.name = "OpenCodeContainerReplacedError";
|
|
43372
|
+
}
|
|
43373
|
+
};
|
|
43374
|
+
TOOL_NAME_WORD = { shell: "bash", glob: "find" };
|
|
43375
|
+
isRecord8 = (v) => typeof v === "object" && v !== null;
|
|
43376
|
+
str6 = (v) => typeof v === "string" ? v : String(v ?? "");
|
|
43377
|
+
TURN_BUDGET_CODE = "turn_budget_exhausted";
|
|
43378
|
+
TURN_BUDGET_REFUSAL = new RegExp(`\\b${TURN_BUDGET_CODE}\\b`);
|
|
43379
|
+
OpenCodeBridge = class {
|
|
43380
|
+
constructor(deps) {
|
|
43381
|
+
this.deps = deps;
|
|
43382
|
+
this.mirror = new PiMirror({
|
|
43383
|
+
...deps.onStep ? { onStep: deps.onStep } : {},
|
|
43384
|
+
seedLength: deps.seedLength,
|
|
43385
|
+
remainingMs: deps.remainingMs
|
|
43386
|
+
});
|
|
43387
|
+
}
|
|
43388
|
+
deps;
|
|
43389
|
+
/** Model turns that did work — the guard's count (an assistant turn per step). */
|
|
43390
|
+
turns = 0;
|
|
43391
|
+
toolCalls = 0;
|
|
43392
|
+
answerText;
|
|
43393
|
+
/** Narration text seen since the last turn's start, emitted beside its call. */
|
|
43394
|
+
pendingNarration;
|
|
43395
|
+
/** callId → the tool name the model gave it (from `session.tool.input.started`). */
|
|
43396
|
+
toolNames = /* @__PURE__ */ new Map();
|
|
43397
|
+
/** The open calls' spans, by callId. */
|
|
43398
|
+
openTools = /* @__PURE__ */ new Map();
|
|
43399
|
+
/** callId → the bot's decision for its ask, `once` or `reject`. A settled
|
|
43400
|
+
* call not among these ran with no decision; a success for a call the bot
|
|
43401
|
+
* rejected ran against the reject. Both are bypasses. */
|
|
43402
|
+
answered = /* @__PURE__ */ new Map();
|
|
43403
|
+
/** requestID → the reply the bot decided, and whether the server has echoed
|
|
43404
|
+
* it once. A `permission.replied` is the bot's own echo only when its
|
|
43405
|
+
* requestID is here, its effect equals the decided one, and it is the first;
|
|
43406
|
+
* anything else — an id never decided, another effect, a second reply — is a
|
|
43407
|
+
* reply the bot did not send (a forgery). */
|
|
43408
|
+
decidedReplies = /* @__PURE__ */ new Map();
|
|
43409
|
+
/** The store messages already mirrored, by count of the pi-shaped turns fed. */
|
|
43410
|
+
fedTurns = 0;
|
|
43411
|
+
mirror;
|
|
43412
|
+
turnCounted = 0;
|
|
43413
|
+
/** The mirror writes, serialized: a refill (or a compaction) chains behind
|
|
43414
|
+
* the last, so two refills that arrive in one poll never race the
|
|
43415
|
+
* high-water mark. `flush` awaits the chain, so a caller reads the answer
|
|
43416
|
+
* and the steps only once every refill it saw has landed. */
|
|
43417
|
+
mirrorChain = Promise.resolve();
|
|
43418
|
+
/** The run's answer as it stands: the last text-only assistant turn. */
|
|
43419
|
+
answer() {
|
|
43420
|
+
return this.answerText;
|
|
43421
|
+
}
|
|
43422
|
+
/** Awaits every mirror write chained so far. */
|
|
43423
|
+
flush() {
|
|
43424
|
+
return this.mirrorChain;
|
|
43425
|
+
}
|
|
43426
|
+
/** The settlement turn a rebuilt process's session starts on (the rebuild is
|
|
43427
|
+
* an import): the calls in flight at the death, each a tool result — the
|
|
43428
|
+
* settlement note — primed as the next step's pending user content, so the
|
|
43429
|
+
* ledger's first step after the rebuild is `[user(settlements), assistant]`
|
|
43430
|
+
* from the seed index, and OpenCode's store rows (the settlement is a
|
|
43431
|
+
* completed tool content in the imported record) project to the same turn.
|
|
43432
|
+
* The record then rebuilds a transcript with a result for every call. */
|
|
43433
|
+
prime(parts) {
|
|
43434
|
+
this.mirror.prime(parts);
|
|
43435
|
+
}
|
|
43436
|
+
/** Settle every call still open when the container went under a running call
|
|
43437
|
+
* (the replaced verdict): each open span ends `error` and its call is put on
|
|
43438
|
+
* the record as a failed `tool_result` carrying `reason` — the restart note,
|
|
43439
|
+
* said of the container — so no span outlives the run and the record holds a
|
|
43440
|
+
* result for every call at the death, exactly as pi's `closeOpenSpans` does. */
|
|
43441
|
+
closeOpenSpans(reason2) {
|
|
43442
|
+
for (const [callId, open2] of this.openTools) {
|
|
43443
|
+
open2.span?.end("error", { callId, ok: false });
|
|
43444
|
+
this.emit({
|
|
43445
|
+
type: "tool_result",
|
|
43446
|
+
tool: open2.tool,
|
|
43447
|
+
ok: false,
|
|
43448
|
+
callId,
|
|
43449
|
+
summary: redactAndCap(reason2({ callId, tool: open2.tool }), COMMAND_CAP)
|
|
43450
|
+
});
|
|
43451
|
+
}
|
|
43452
|
+
this.openTools.clear();
|
|
43453
|
+
}
|
|
43454
|
+
/** The record as this generation holds it (`HarnessRecord`; harness.md item
|
|
43455
|
+
* 6): the base — the seed with its request, or the resumed transcript — and
|
|
43456
|
+
* the mirror's rows, every call of the last turn settled with the replaced
|
|
43457
|
+
* note, so the run loop can relaunch OpenCode from it. Mirrors pi's
|
|
43458
|
+
* `recordNow`. */
|
|
43459
|
+
record(base, deadline) {
|
|
43460
|
+
const written = this.mirror.written;
|
|
43461
|
+
const messages = [...base.messages, ...written.messages];
|
|
43462
|
+
const last = messages.at(-1);
|
|
43463
|
+
const calls = last?.role === "assistant" ? last.content.filter((p) => p.type === "tool_use") : [];
|
|
43464
|
+
return {
|
|
43465
|
+
messages,
|
|
43466
|
+
compactions: [
|
|
43467
|
+
...base.compactions,
|
|
43468
|
+
...written.compactions.map((c) => ({ ...c, before: base.messages.length + c.before }))
|
|
43469
|
+
],
|
|
43470
|
+
settlements: calls.map((toolUse) => ({
|
|
43471
|
+
toolUse,
|
|
43472
|
+
action: "synthetic",
|
|
43473
|
+
text: openCodeReplacedCallNote(toolUse.name)
|
|
43474
|
+
})),
|
|
43475
|
+
turn: this.turns,
|
|
43476
|
+
inboxConsumedSeq: this.mirror.inboxConsumedSeq,
|
|
43477
|
+
deadline
|
|
43478
|
+
};
|
|
43479
|
+
}
|
|
43480
|
+
note(kind, summary) {
|
|
43481
|
+
this.deps.onProgress?.(summary);
|
|
43482
|
+
this.emit({ type: "run_note", kind, summary });
|
|
43483
|
+
}
|
|
43484
|
+
emit(event) {
|
|
43485
|
+
this.deps.emit(event.at === void 0 ? { ...event, at: this.deps.clock() } : event);
|
|
43486
|
+
}
|
|
43487
|
+
observe(record2) {
|
|
43488
|
+
switch (record2.feed) {
|
|
43489
|
+
case "event":
|
|
43490
|
+
return this.onEvent(record2.event);
|
|
43491
|
+
case "permissions":
|
|
43492
|
+
return this.onPermissionsRefill(record2.data);
|
|
43493
|
+
case "messages":
|
|
43494
|
+
return this.onMessagesRefill(record2.data);
|
|
43495
|
+
case "tailer":
|
|
43496
|
+
if (record2.note === "stream closed" || record2.note.includes("failed"))
|
|
43497
|
+
this.deps.onProgress?.(`opencode feed: ${redactAndCap(record2.note, 120)}`);
|
|
43498
|
+
return { replies: [], settled: false };
|
|
43499
|
+
}
|
|
43500
|
+
}
|
|
43501
|
+
onEvent(event) {
|
|
43502
|
+
const out = { replies: [], settled: false };
|
|
43503
|
+
const disposition = openCodeDispositionOf(event.type);
|
|
43504
|
+
if (disposition === void 0) {
|
|
43505
|
+
this.note(
|
|
43506
|
+
"harness_error",
|
|
43507
|
+
`OpenCode emitted an event kind this build does not know: ${redactAndCap(event.type, 80)}`
|
|
43508
|
+
);
|
|
43509
|
+
return out;
|
|
43510
|
+
}
|
|
43511
|
+
if (disposition === "impossible") {
|
|
43512
|
+
this.note("harness_error", `OpenCode emitted ${event.type}, which this run's configuration turns off`);
|
|
43513
|
+
return out;
|
|
43514
|
+
}
|
|
43515
|
+
const data = isRecord8(event.data) ? event.data : {};
|
|
43516
|
+
switch (event.type) {
|
|
43517
|
+
case "session.tool.input.started":
|
|
43518
|
+
if (typeof data.id === "string" && typeof data.name === "string") this.toolNames.set(data.id, data.name);
|
|
43519
|
+
break;
|
|
43520
|
+
case "session.tool.called":
|
|
43521
|
+
this.onToolCalled(data);
|
|
43522
|
+
break;
|
|
43523
|
+
case "session.text.ended":
|
|
43524
|
+
if (typeof data.text === "string" && data.text.trim())
|
|
43525
|
+
this.pendingNarration = this.pendingNarration ? `${this.pendingNarration}
|
|
43526
|
+
${data.text}` : data.text;
|
|
43527
|
+
break;
|
|
43528
|
+
case "session.step.ended":
|
|
43529
|
+
this.pendingNarration = void 0;
|
|
43530
|
+
break;
|
|
43531
|
+
case "session.tool.success":
|
|
43532
|
+
this.onToolSettled(data, true, out);
|
|
43533
|
+
break;
|
|
43534
|
+
case "session.tool.failed":
|
|
43535
|
+
this.onToolSettled(data, false, out);
|
|
43536
|
+
break;
|
|
43537
|
+
case "permission.asked":
|
|
43538
|
+
this.onPermissionAsked(data, out);
|
|
43539
|
+
break;
|
|
43540
|
+
case "permission.replied":
|
|
43541
|
+
this.onPermissionReplied(data, out);
|
|
43542
|
+
break;
|
|
43543
|
+
case "session.compaction.ended":
|
|
43544
|
+
this.onCompactionEnded(data);
|
|
43545
|
+
break;
|
|
43546
|
+
case "session.compaction.failed":
|
|
43547
|
+
this.note("harness_error", `OpenCode's compaction failed: ${redactAndCap(errorMessage(data.error), 200)}`);
|
|
43548
|
+
break;
|
|
43549
|
+
case "session.retry.scheduled":
|
|
43550
|
+
this.note("harness_error", `OpenCode scheduled a model retry (${redactAndCap(errorMessage(data.error), 200)})`);
|
|
43551
|
+
break;
|
|
43552
|
+
case "session.step.failed":
|
|
43553
|
+
this.note("harness_error", `an OpenCode step failed: ${redactAndCap(errorMessage(data.error), 200)}`);
|
|
43554
|
+
break;
|
|
43555
|
+
case "session.execution.failed": {
|
|
43556
|
+
const error = { status: statusOf(data.error), message: errorMessage(data.error) };
|
|
43557
|
+
if (isBudgetRefusal(error)) out.budgetStop = true;
|
|
43558
|
+
else out.providerError = redactAndCap(error.message, 400);
|
|
43559
|
+
break;
|
|
43560
|
+
}
|
|
43561
|
+
case "session.execution.succeeded":
|
|
43562
|
+
case "session.execution.interrupted":
|
|
43563
|
+
case "session.idle":
|
|
43564
|
+
out.settled = true;
|
|
43565
|
+
break;
|
|
43566
|
+
default:
|
|
43567
|
+
break;
|
|
43568
|
+
}
|
|
43569
|
+
return out;
|
|
43570
|
+
}
|
|
43571
|
+
onToolCalled(data) {
|
|
43572
|
+
const callId = str6(data.id);
|
|
43573
|
+
const name = this.toolNames.get(callId) ?? "tool";
|
|
43574
|
+
const tool = openCodeToolNameWord(name);
|
|
43575
|
+
const span = this.deps.agentSpan?.start(`tool.${tool}`);
|
|
43576
|
+
this.openTools.set(callId, { span, tool });
|
|
43577
|
+
this.toolCalls++;
|
|
43578
|
+
const input = isRecord8(data.input) ? data.input : void 0;
|
|
43579
|
+
if (this.pendingNarration) {
|
|
43580
|
+
this.emit({ type: "assistant", text: redactSecrets(this.pendingNarration) });
|
|
43581
|
+
this.pendingNarration = void 0;
|
|
43582
|
+
}
|
|
43583
|
+
const command2 = tool === "bash" && typeof input?.command === "string" ? { command: redactAndCap(input.command, COMMAND_CAP) } : {};
|
|
43584
|
+
this.emit({
|
|
43585
|
+
type: "tool_call",
|
|
43586
|
+
tool,
|
|
43587
|
+
summary: redactAndCap(describePiToolCall(tool, input)),
|
|
43588
|
+
callId,
|
|
43589
|
+
...command2,
|
|
43590
|
+
...span ? { spanId: span.id } : {}
|
|
43591
|
+
});
|
|
43592
|
+
}
|
|
43593
|
+
onToolSettled(data, ok2, out) {
|
|
43594
|
+
const callId = str6(data.id);
|
|
43595
|
+
const open2 = this.openTools.get(callId);
|
|
43596
|
+
this.openTools.delete(callId);
|
|
43597
|
+
const tool = open2?.tool ?? openCodeToolNameWord(this.toolNames.get(callId) ?? "tool");
|
|
43598
|
+
const text = ok2 ? contentText(data.content) : errorMessage(data.error) || contentText(data.content);
|
|
43599
|
+
const failedInText = this.deps.textFailing?.has(tool) === true && toolTextFailed(text);
|
|
43600
|
+
const exit = tool === "bash" ? piBashExit(text, !ok2) : { failed: !ok2 || failedInText };
|
|
43601
|
+
const settledOk = !exit.failed;
|
|
43602
|
+
this.emit({
|
|
43603
|
+
type: "tool_result",
|
|
43604
|
+
tool,
|
|
43605
|
+
ok: settledOk,
|
|
43606
|
+
callId,
|
|
43607
|
+
...exit.exitCode !== void 0 ? { exitCode: exit.exitCode } : {},
|
|
43608
|
+
...prepareToolResult(text),
|
|
43609
|
+
...open2?.span ? { spanId: open2.span.id } : {}
|
|
43610
|
+
});
|
|
43611
|
+
open2?.span?.end(settledOk ? "ok" : "error", { callId, ok: settledOk });
|
|
43612
|
+
if (this.deps.relayedToolNames.has(this.toolNames.get(callId) ?? "")) return;
|
|
43613
|
+
const executed = data.executed === true;
|
|
43614
|
+
const decision = this.answered.get(callId);
|
|
43615
|
+
if (decision === void 0 && !executed) {
|
|
43616
|
+
const name = this.toolNames.get(callId);
|
|
43617
|
+
const absent = name !== void 0 && !openCodeBuiltinToolsFor(this.deps.rules.identity).includes(name);
|
|
43618
|
+
if (absent)
|
|
43619
|
+
this.note(
|
|
43620
|
+
"tool_refused",
|
|
43621
|
+
`${tool} refused: the run's identity has no ${tool} tool (the deny rules removed it), so the call ran nothing`
|
|
43622
|
+
);
|
|
43623
|
+
else
|
|
43624
|
+
this.note(
|
|
43625
|
+
"harness_error",
|
|
43626
|
+
`OpenCode settled ${tool} (call ${callId}) before it ran, with no ask the bot answered${text ? `: ${redactAndCap(text, 200)}` : ""}`
|
|
43627
|
+
);
|
|
43628
|
+
} else if (decision === void 0) {
|
|
43629
|
+
this.bypass(out, `OpenCode ran ${tool} (call ${callId}) with no ask the bot answered \u2014 a call with no ask`);
|
|
43630
|
+
} else if (decision === "reject" && ok2) {
|
|
43631
|
+
this.bypass(
|
|
43632
|
+
out,
|
|
43633
|
+
`OpenCode ran ${tool} (call ${callId}) after the bot's reject \u2014 a success after the bot's refusal`
|
|
43634
|
+
);
|
|
43635
|
+
}
|
|
43636
|
+
}
|
|
43637
|
+
bypass(out, detail) {
|
|
43638
|
+
out.bypass = new OpenCodeGateBypassedError(detail);
|
|
43639
|
+
this.note("harness_error", `${out.bypass.message} \u2014 the run is stopped`);
|
|
43640
|
+
}
|
|
43641
|
+
onPermissionAsked(request2, out) {
|
|
43642
|
+
const callId = request2.source?.id ?? request2.id;
|
|
43643
|
+
if (this.answered.has(callId)) return;
|
|
43644
|
+
const verdict = judgeOpenCodeAsk(
|
|
43645
|
+
request2.action,
|
|
43646
|
+
Array.isArray(request2.resources) ? request2.resources : [],
|
|
43647
|
+
this.deps.rules,
|
|
43648
|
+
this.deps.relayedToolNames
|
|
43649
|
+
);
|
|
43650
|
+
this.answered.set(callId, verdict.reply);
|
|
43651
|
+
this.decidedReplies.set(request2.id, { reply: verdict.reply, echoed: false });
|
|
43652
|
+
if (verdict.reply === "reject")
|
|
43653
|
+
this.note("tool_refused", `${verdict.tool} refused: ${redactAndCap(verdict.message ?? "", 300)}`);
|
|
43654
|
+
out.replies.push({
|
|
43655
|
+
requestID: request2.id,
|
|
43656
|
+
callId,
|
|
43657
|
+
reply: verdict.reply,
|
|
43658
|
+
...verdict.message ? { message: verdict.message } : {}
|
|
43659
|
+
});
|
|
43660
|
+
}
|
|
43661
|
+
onPermissionReplied(data, out) {
|
|
43662
|
+
const requestID = str6(data.requestID);
|
|
43663
|
+
const reply = str6(data.reply);
|
|
43664
|
+
const decided = this.decidedReplies.get(requestID);
|
|
43665
|
+
if (decided === void 0) {
|
|
43666
|
+
this.bypass(
|
|
43667
|
+
out,
|
|
43668
|
+
`a permission.replied for request ${requestID}, which the bot never decided \u2014 a reply the bot did not send (it raced the bot, or answered an ask the bot never saw)`
|
|
43669
|
+
);
|
|
43670
|
+
return;
|
|
43671
|
+
}
|
|
43672
|
+
if (reply !== decided.reply) {
|
|
43673
|
+
this.bypass(
|
|
43674
|
+
out,
|
|
43675
|
+
`a permission.replied answering \`${reply}\` where the bot decided \`${decided.reply}\` (request ${requestID}) \u2014 a reply that differs from the bot's decision, overriding it`
|
|
43676
|
+
);
|
|
43677
|
+
return;
|
|
43678
|
+
}
|
|
43679
|
+
if (decided.echoed) {
|
|
43680
|
+
this.bypass(out, `a second permission.replied for request ${requestID} \u2014 a duplicate reply the bot did not send`);
|
|
43681
|
+
return;
|
|
43682
|
+
}
|
|
43683
|
+
decided.echoed = true;
|
|
43684
|
+
}
|
|
43685
|
+
onPermissionsRefill(pending2) {
|
|
43686
|
+
const out = { replies: [], settled: false };
|
|
43687
|
+
for (const request2 of pending2) this.onPermissionAsked(request2, out);
|
|
43688
|
+
return out;
|
|
43689
|
+
}
|
|
43690
|
+
onMessagesRefill(messages) {
|
|
43691
|
+
this.mirrorChain = this.mirrorChain.then(() => this.syncMirror(messages));
|
|
43692
|
+
return { replies: [], settled: false };
|
|
43693
|
+
}
|
|
43694
|
+
/** The store's messages as the ledger's steps: the pi-shaped turns projected
|
|
43695
|
+
* from the store (each assistant message split into its assistant turn and a
|
|
43696
|
+
* user turn for its tool results), fed to the mirror past the high-water mark
|
|
43697
|
+
* so a re-refill writes no step twice (an upsert by message id). */
|
|
43698
|
+
async syncMirror(messages) {
|
|
43699
|
+
if (!this.deps.onStep) return;
|
|
43700
|
+
const turns = projectStore(messages, this.deps.seedLength);
|
|
43701
|
+
for (let i = this.fedTurns; i < turns.length; i++) {
|
|
43702
|
+
const turn = turns[i];
|
|
43703
|
+
if (turn.role === "assistant") {
|
|
43704
|
+
const hasTool = turn.content.some((p) => p.type === "tool_use");
|
|
43705
|
+
const text = turn.content.filter((p) => p.type === "text").map((p) => p.text).join("\n").trim();
|
|
43706
|
+
if (hasTool) {
|
|
43707
|
+
this.turns++;
|
|
43708
|
+
} else if (text) {
|
|
43709
|
+
this.answerText = text;
|
|
43710
|
+
}
|
|
43711
|
+
await this.mirror.onMessage(assistantMessage2(turn), ++this.turnCounted);
|
|
43712
|
+
} else {
|
|
43713
|
+
await this.mirror.onMessage(userMessage(turn), this.turnCounted);
|
|
43714
|
+
}
|
|
43715
|
+
}
|
|
43716
|
+
this.fedTurns = turns.length;
|
|
43717
|
+
}
|
|
43718
|
+
onCompactionEnded(data) {
|
|
43719
|
+
const text = typeof data.text === "string" ? data.text : void 0;
|
|
43720
|
+
if (!text) {
|
|
43721
|
+
this.note("compacted", "OpenCode compacted the context; the transcript keeps the originals");
|
|
43722
|
+
return;
|
|
43723
|
+
}
|
|
43724
|
+
const entry = { summary: text };
|
|
43725
|
+
this.note("compacted", `OpenCode compacted the context (${str6(data.reason)}); the transcript keeps the originals`);
|
|
43726
|
+
if (this.deps.onStep)
|
|
43727
|
+
this.mirrorChain = this.mirrorChain.then(() => void this.mirror.onCompaction(entry, this.turnCounted));
|
|
43728
|
+
}
|
|
43729
|
+
};
|
|
43730
|
+
}
|
|
43731
|
+
});
|
|
43732
|
+
|
|
43733
|
+
// ../../src/core/harness/opencode/harness.ts
|
|
43734
|
+
function modelRef2(run2, variant) {
|
|
43735
|
+
return { providerID: run2.model.provider, id: run2.model.id, ...variant ? { variant } : {} };
|
|
43736
|
+
}
|
|
43737
|
+
async function feedEnd(container, conn) {
|
|
43738
|
+
let off = conn.feedOffset;
|
|
43739
|
+
for (; ; ) {
|
|
43740
|
+
const chunk = await container.readLog(conn.paths.feed, off, LOG_READ_BYTES);
|
|
43741
|
+
off += chunk.length;
|
|
43742
|
+
if (chunk.length < LOG_READ_BYTES) return off;
|
|
43743
|
+
}
|
|
43744
|
+
}
|
|
43745
|
+
function resumeOpenCodeFacts(resume) {
|
|
43746
|
+
const facts2 = resume?.facts;
|
|
43747
|
+
return facts2 !== void 0 && facts2.harness === "opencode" ? facts2 : void 0;
|
|
43748
|
+
}
|
|
43749
|
+
async function openOpenCodeRun(deps, run2, settings) {
|
|
43750
|
+
const now = () => deps.clock();
|
|
43751
|
+
const emit = (event) => run2.onEvent?.(event.at === void 0 ? { ...event, at: now() } : event);
|
|
43752
|
+
const note = (kind, summary) => {
|
|
43753
|
+
run2.onProgress?.(summary);
|
|
43754
|
+
emit({ type: "run_note", kind, summary });
|
|
43755
|
+
};
|
|
43756
|
+
const identity2 = run2.agent.identity;
|
|
43757
|
+
const writeUp = {};
|
|
43758
|
+
const live = {
|
|
43759
|
+
runId: run2.runId,
|
|
43760
|
+
tools: run2.tools,
|
|
43761
|
+
toolContext: run2.toolContext,
|
|
43762
|
+
...run2.backend ? { backend: run2.backend } : {},
|
|
43763
|
+
rules: { ...run2.rules, identity: identity2 },
|
|
43764
|
+
emit,
|
|
43765
|
+
toolSpan: () => void 0,
|
|
43766
|
+
gateSaw: () => {
|
|
43767
|
+
},
|
|
43768
|
+
toolsBlocked: () => writeUp.blocked,
|
|
43769
|
+
callSeen: async () => {
|
|
43770
|
+
},
|
|
43771
|
+
callEnded: async () => {
|
|
43772
|
+
}
|
|
43773
|
+
};
|
|
43774
|
+
const resumeFacts = resumeOpenCodeFacts(run2.resume);
|
|
43775
|
+
const relaunch = run2.resume?.relaunch !== void 0;
|
|
43776
|
+
const forget = relaunch && deps.registry.get(run2.runId) !== void 0 ? deps.registry.replace(live) : deps.registry.register(live);
|
|
43777
|
+
const relayCalls = deps.registry.calls(run2.runId);
|
|
43778
|
+
for (const s of run2.resume?.settlements ?? [])
|
|
43779
|
+
relayCalls?.settle(s.toolUse.id, { content: [{ type: "text", text: openCodeSettlementNote(s) }], isError: true });
|
|
43780
|
+
const paths = openCodeRunPaths(run2.runId);
|
|
43781
|
+
const spec = {
|
|
43782
|
+
runId: run2.runId,
|
|
43783
|
+
paths,
|
|
43784
|
+
model: { id: run2.model.id, providerType: run2.model.providerType, maxTokens: run2.agent.maxTokens },
|
|
43785
|
+
harnessUrl: deps.harnessUrl,
|
|
43786
|
+
identity: identity2,
|
|
43787
|
+
system: run2.system,
|
|
43788
|
+
relayTools: run2.tools.map((t) => t.name),
|
|
43789
|
+
...settings.compaction ? { compaction: settings.compaction } : {}
|
|
43790
|
+
};
|
|
43791
|
+
let started;
|
|
43792
|
+
let replaced = false;
|
|
43793
|
+
const here = await deps.container.identity().catch(() => void 0);
|
|
43794
|
+
const end = async () => {
|
|
43795
|
+
forget();
|
|
43796
|
+
if (replaced || started === void 0) return;
|
|
43797
|
+
await deps.container.kill(started.pid).catch(() => {
|
|
43798
|
+
});
|
|
43799
|
+
await deps.container.kill(started.tailerPid).catch(() => {
|
|
43800
|
+
});
|
|
43801
|
+
await deps.container.remove(started.paths).catch(() => {
|
|
43802
|
+
});
|
|
43803
|
+
};
|
|
43804
|
+
try {
|
|
43805
|
+
if (relaunch) {
|
|
43806
|
+
const rl = run2.resume?.relaunch;
|
|
43807
|
+
note(
|
|
43808
|
+
"resumed",
|
|
43809
|
+
`relaunched after the container was replaced (${rl?.from ?? "unknown"} \u2192 ${rl?.to ?? here ?? "unknown"}); a fresh server was started on the record with ${Math.round((run2.resume?.remainingMs ?? 0) / 6e4)} min of budget left`
|
|
43810
|
+
);
|
|
43811
|
+
} else if (resumeFacts !== void 0) {
|
|
43812
|
+
const elsewhere = resumeFacts.container !== void 0 && here !== void 0 && resumeFacts.container !== here;
|
|
43813
|
+
if (elsewhere) {
|
|
43814
|
+
note(
|
|
43815
|
+
"resumed",
|
|
43816
|
+
`resumed after a restart: the row's OpenCode (pid ${resumeFacts.pid}) ran in container ${resumeFacts.container}, not the one this run was handed (${here}); it was neither probed nor ended here, and a fresh server was started on the record`
|
|
43817
|
+
);
|
|
43818
|
+
} else if (await serverAnswers(deps.container, resumeFacts)) {
|
|
43819
|
+
note(
|
|
43820
|
+
"resumed",
|
|
43821
|
+
`resumed after a restart: the row's OpenCode (pid ${resumeFacts.pid}) still answers in this container; ended it and its tailer before a fresh start on the record`
|
|
43822
|
+
);
|
|
43823
|
+
await deps.container.kill(resumeFacts.pid).catch(() => {
|
|
43824
|
+
});
|
|
43825
|
+
if (resumeFacts.tailerPid !== void 0) await deps.container.kill(resumeFacts.tailerPid).catch(() => {
|
|
43826
|
+
});
|
|
43827
|
+
await deps.container.remove(openCodeRunPathsAt(resumeFacts.root)).catch(() => {
|
|
43828
|
+
});
|
|
43829
|
+
} else {
|
|
43830
|
+
if (resumeFacts.root !== paths.dir)
|
|
43831
|
+
await deps.container.remove(openCodeRunPathsAt(resumeFacts.root)).catch(() => {
|
|
43832
|
+
});
|
|
43833
|
+
note(
|
|
43834
|
+
"resumed",
|
|
43835
|
+
`resumed after a restart: the row's OpenCode did not answer in this container; a fresh server was started on the record with ${Math.round((run2.resume?.remainingMs ?? 0) / 6e4)} min of budget left`
|
|
43836
|
+
);
|
|
43837
|
+
}
|
|
43838
|
+
}
|
|
43839
|
+
started = await launchOpenCode(
|
|
43840
|
+
{
|
|
43841
|
+
container: deps.container,
|
|
43842
|
+
clock: deps.clock,
|
|
43843
|
+
sleep: deps.sleep,
|
|
43844
|
+
...deps.pollMs !== void 0 ? { pollMs: deps.pollMs } : {}
|
|
43845
|
+
},
|
|
43846
|
+
spec,
|
|
43847
|
+
deps.bearer
|
|
43848
|
+
);
|
|
43849
|
+
const auth = { Authorization: openCodeAuthHeader(started.password) };
|
|
43850
|
+
const at = now();
|
|
43851
|
+
const cwd = deps.container.cwd(started.paths, run2.rules.checkout);
|
|
43852
|
+
let sessionID;
|
|
43853
|
+
const importInto = async (messages, opts) => {
|
|
43854
|
+
const body = openCodeImportBody(messages, opts);
|
|
43855
|
+
const res = await request(deps.container, started, auth, OPENCODE_ROUTES["session.import"], body);
|
|
43856
|
+
if (res.status < 200 || res.status >= 300)
|
|
43857
|
+
throw new Error(`OpenCode refused the session import (${res.status}): ${redactAndCap(res.body, 200)}`);
|
|
43858
|
+
};
|
|
43859
|
+
if (run2.resume !== void 0 && run2.resume.messages.length > 0) {
|
|
43860
|
+
sessionID = openCodeSessionId(run2.runId);
|
|
43861
|
+
const settlements = new Map(run2.resume.settlements.map((s) => [s.toolUse.id, openCodeSettlementNote(s)]));
|
|
43862
|
+
await importInto(run2.resume.messages, {
|
|
43863
|
+
sessionID,
|
|
43864
|
+
location: { directory: cwd },
|
|
43865
|
+
model: modelRef2(run2, void 0),
|
|
43866
|
+
agent: OPENCODE_AGENT,
|
|
43867
|
+
at,
|
|
43868
|
+
...run2.resume.compactions ? { compactions: run2.resume.compactions.map((c) => c.entry) } : {},
|
|
43869
|
+
settlements
|
|
43870
|
+
});
|
|
43871
|
+
} else {
|
|
43872
|
+
const { seed } = openCodeSeedAndRequest(run2.messages);
|
|
43873
|
+
if (seed.length > 0) {
|
|
43874
|
+
sessionID = openCodeSessionId(run2.runId);
|
|
43875
|
+
await importInto(seed, {
|
|
43876
|
+
sessionID,
|
|
43877
|
+
location: { directory: cwd },
|
|
43878
|
+
model: modelRef2(run2, void 0),
|
|
43879
|
+
agent: OPENCODE_AGENT,
|
|
43880
|
+
at
|
|
43881
|
+
});
|
|
43882
|
+
} else {
|
|
43883
|
+
sessionID = openCodeSessionId(run2.runId);
|
|
43884
|
+
const res = await request(deps.container, started, auth, OPENCODE_ROUTES["session.create"], {
|
|
43885
|
+
id: sessionID,
|
|
43886
|
+
agent: OPENCODE_AGENT,
|
|
43887
|
+
location: { directory: cwd },
|
|
43888
|
+
model: modelRef2(run2, void 0)
|
|
43889
|
+
});
|
|
43890
|
+
if (res.status < 200 || res.status >= 300)
|
|
43891
|
+
throw new Error(`OpenCode refused the session create (${res.status}): ${redactAndCap(res.body, 200)}`);
|
|
43892
|
+
const created = parseSessionId(res.body);
|
|
43893
|
+
if (created !== void 0) sessionID = created;
|
|
43894
|
+
}
|
|
43895
|
+
}
|
|
43896
|
+
const conn = {
|
|
43897
|
+
container: deps.container,
|
|
43898
|
+
paths: started.paths,
|
|
43899
|
+
port: started.port,
|
|
43900
|
+
password: started.password,
|
|
43901
|
+
sessionID,
|
|
43902
|
+
feedOffset: started.feedOffset,
|
|
43903
|
+
tailerPid: started.tailerPid,
|
|
43904
|
+
...here !== void 0 ? { containerWord: here } : {},
|
|
43905
|
+
writeUp
|
|
43906
|
+
};
|
|
43907
|
+
run2.saveFacts?.(
|
|
43908
|
+
openCodeFacts(
|
|
43909
|
+
{ pid: started.pid, port: started.port, paths: started.paths, tailerPid: started.tailerPid },
|
|
43910
|
+
{
|
|
43911
|
+
sessionID,
|
|
43912
|
+
logOffset: started.feedOffset,
|
|
43913
|
+
bearer: deps.bearer,
|
|
43914
|
+
...here !== void 0 ? { container: here } : {},
|
|
43915
|
+
relaunches: resumeFacts?.relaunches ?? 0
|
|
43916
|
+
}
|
|
43917
|
+
)
|
|
43918
|
+
);
|
|
43919
|
+
let draining = true;
|
|
43920
|
+
const drainer = drainFollowUps(deps, run2, conn, () => draining, emit, note);
|
|
43921
|
+
let answer;
|
|
43922
|
+
try {
|
|
43923
|
+
({ answer } = await driveOpenCode(deps, run2, conn));
|
|
43924
|
+
} finally {
|
|
43925
|
+
draining = false;
|
|
43926
|
+
await drainer.catch(() => {
|
|
43927
|
+
});
|
|
43928
|
+
}
|
|
43929
|
+
return {
|
|
43930
|
+
answer,
|
|
43931
|
+
followUp: async (input) => {
|
|
43932
|
+
const turnEnd = await feedEnd(deps.container, conn);
|
|
43933
|
+
const previous = live.toolContext;
|
|
43934
|
+
live.toolContext = input.toolContext;
|
|
43935
|
+
try {
|
|
43936
|
+
const { answer: turnAnswer } = await driveOpenCode(
|
|
43937
|
+
deps,
|
|
43938
|
+
{
|
|
43939
|
+
...run2,
|
|
43940
|
+
messages: [{ role: "user", content: [{ type: "text", text: input.text }] }],
|
|
43941
|
+
agent: { ...run2.agent, maxTurns: input.maxTurns, maxMinutes: input.maxMinutes },
|
|
43942
|
+
toolContext: input.toolContext,
|
|
43943
|
+
...input.span ? { span: input.span } : {},
|
|
43944
|
+
resume: void 0,
|
|
43945
|
+
inbox: void 0,
|
|
43946
|
+
// The post-turn is not mirrored to the ledger — the loop's
|
|
43947
|
+
// transcript is the record — but the bridge needs a step sink to
|
|
43948
|
+
// read the answer, so a discard stands in.
|
|
43949
|
+
onStep: async () => {
|
|
43950
|
+
}
|
|
43951
|
+
},
|
|
43952
|
+
{ ...conn, feedOffset: turnEnd }
|
|
43953
|
+
);
|
|
43954
|
+
return turnAnswer;
|
|
43955
|
+
} finally {
|
|
43956
|
+
live.toolContext = previous;
|
|
43957
|
+
}
|
|
43958
|
+
},
|
|
43959
|
+
end
|
|
43960
|
+
};
|
|
43961
|
+
} catch (err2) {
|
|
43962
|
+
if (err2 instanceof HarnessContainerReplacedError) replaced = true;
|
|
43963
|
+
await end();
|
|
43964
|
+
throw err2;
|
|
43965
|
+
}
|
|
43966
|
+
}
|
|
43967
|
+
async function serverAnswers(container, facts2) {
|
|
43968
|
+
try {
|
|
43969
|
+
await container.request(openCodeRunPathsAt(facts2.root), {
|
|
43970
|
+
method: "GET",
|
|
43971
|
+
port: facts2.port,
|
|
43972
|
+
path: OPENCODE_ROUTES["health.get"].path
|
|
43973
|
+
});
|
|
43974
|
+
return true;
|
|
43975
|
+
} catch (err2) {
|
|
43976
|
+
if (isContainerGone(err2)) throw err2;
|
|
43977
|
+
return false;
|
|
43978
|
+
}
|
|
43979
|
+
}
|
|
43980
|
+
function drainFollowUps(deps, run2, conn, running, emit, note) {
|
|
43981
|
+
const auth = { Authorization: openCodeAuthHeader(conn.password) };
|
|
43982
|
+
const routes = openCodeSessionRoutes(conn.sessionID);
|
|
43983
|
+
return (async () => {
|
|
43984
|
+
while (running()) {
|
|
43985
|
+
const inputs = run2.inbox?.drain() ?? [];
|
|
43986
|
+
for (const input of inputs) {
|
|
43987
|
+
let stagedLine = "";
|
|
43988
|
+
if (run2.stageFollowUps) {
|
|
43989
|
+
try {
|
|
43990
|
+
stagedLine = await run2.stageFollowUps([input]);
|
|
43991
|
+
} catch (err2) {
|
|
43992
|
+
stagedLine = `Attached files could not be staged: ${err2 instanceof Error ? err2.message : String(err2)}`;
|
|
43993
|
+
note("follow_up", `staging the follow-up's files failed: ${redactSecrets(stagedLine)}`);
|
|
43994
|
+
}
|
|
43995
|
+
}
|
|
43996
|
+
const prompt = followUpPrompt([input]);
|
|
43997
|
+
const text = stagedLine ? `${prompt}
|
|
43998
|
+
|
|
43999
|
+
${stagedLine}` : prompt;
|
|
44000
|
+
let failure2;
|
|
44001
|
+
try {
|
|
44002
|
+
const res = await conn.container.request(conn.paths, {
|
|
44003
|
+
method: routes["session.prompt"].method,
|
|
44004
|
+
port: conn.port,
|
|
44005
|
+
path: routes["session.prompt"].path,
|
|
44006
|
+
secretHeaders: auth,
|
|
44007
|
+
headers: { "content-type": "application/json" },
|
|
44008
|
+
body: JSON.stringify({ text, delivery: "steer" })
|
|
44009
|
+
});
|
|
44010
|
+
if (res.status < 200 || res.status >= 300) failure2 = `the server answered ${res.status}`;
|
|
44011
|
+
} catch (err2) {
|
|
44012
|
+
failure2 = err2 instanceof Error ? err2.message : String(err2);
|
|
44013
|
+
}
|
|
44014
|
+
if (failure2 !== void 0) {
|
|
44015
|
+
note(
|
|
44016
|
+
"follow_up",
|
|
44017
|
+
`follow-up not delivered \u2014 the steer did not reach the session (${redactAndCap(failure2, 200)}); handed back to the inbox for a fresh turn: ${redactSecrets(followUpSnippet(input))}`
|
|
44018
|
+
);
|
|
44019
|
+
run2.inbox?.requeue(inputs.slice(inputs.indexOf(input)));
|
|
44020
|
+
return;
|
|
44021
|
+
}
|
|
44022
|
+
const source2 = {
|
|
44023
|
+
...input.sourceUrl ? { url: input.sourceUrl } : {},
|
|
44024
|
+
...input.userName ? { user: input.userName } : {},
|
|
44025
|
+
...input.from ? { run: input.from.runId } : {}
|
|
44026
|
+
};
|
|
44027
|
+
emit({
|
|
44028
|
+
type: "input",
|
|
44029
|
+
text: redactSecrets(input.text),
|
|
44030
|
+
messageId: followUpMessageId(input),
|
|
44031
|
+
...Object.keys(source2).length > 0 ? { source: source2 } : {}
|
|
44032
|
+
});
|
|
44033
|
+
note("follow_up", `follow-up folded in: ${redactSecrets(followUpSnippet(input))}`);
|
|
44034
|
+
}
|
|
44035
|
+
if (!running()) break;
|
|
44036
|
+
await deps.sleep(deps.tickMs ?? 1e3);
|
|
44037
|
+
}
|
|
44038
|
+
})();
|
|
44039
|
+
}
|
|
44040
|
+
function request(container, started, auth, route3, body) {
|
|
44041
|
+
return container.request(started.paths, {
|
|
44042
|
+
method: route3.method,
|
|
44043
|
+
port: started.port,
|
|
44044
|
+
path: route3.path,
|
|
44045
|
+
secretHeaders: auth,
|
|
44046
|
+
...body !== void 0 ? { headers: { "content-type": "application/json" }, body: JSON.stringify(body) } : {}
|
|
44047
|
+
});
|
|
44048
|
+
}
|
|
44049
|
+
function parseSessionId(body) {
|
|
44050
|
+
try {
|
|
44051
|
+
const value = JSON.parse(body);
|
|
44052
|
+
return typeof value.data?.id === "string" ? value.data.id : void 0;
|
|
44053
|
+
} catch {
|
|
44054
|
+
return void 0;
|
|
44055
|
+
}
|
|
44056
|
+
}
|
|
44057
|
+
var OpenCodeHarness;
|
|
44058
|
+
var init_harness2 = __esm({
|
|
44059
|
+
"../../src/core/harness/opencode/harness.ts"() {
|
|
44060
|
+
"use strict";
|
|
44061
|
+
init_threadAdmission();
|
|
44062
|
+
init_runEvents();
|
|
44063
|
+
init_container();
|
|
44064
|
+
init_contract4();
|
|
44065
|
+
init_dispositions();
|
|
44066
|
+
init_bridge3();
|
|
44067
|
+
init_client2();
|
|
44068
|
+
init_process2();
|
|
44069
|
+
init_session2();
|
|
44070
|
+
OpenCodeHarness = class {
|
|
44071
|
+
constructor(settings = {}) {
|
|
44072
|
+
this.settings = settings;
|
|
44073
|
+
}
|
|
44074
|
+
settings;
|
|
44075
|
+
name = "opencode";
|
|
44076
|
+
/** OpenCode accepts an imported session as its own history: a seed is
|
|
44077
|
+
* that history written for it, a rebuild a rewrite of it. */
|
|
44078
|
+
history = "authored-session";
|
|
44079
|
+
dispositions = OPENCODE_EVENT_DISPOSITION;
|
|
44080
|
+
/** Switchboard's effort tier in OpenCode's word: the tier selects a model
|
|
44081
|
+
* `variant` on the session's model ref (item 11: variants are declared per
|
|
44082
|
+
* model and a session selects one by id). Stage A's configuration writer
|
|
44083
|
+
* declares no reasoning variants, so there is none to select and the tier is
|
|
44084
|
+
* left to OpenCode's default — the honest answer until a deployment declares
|
|
44085
|
+
* them (a follow-up to the configuration word). */
|
|
44086
|
+
effort(_tier) {
|
|
44087
|
+
return void 0;
|
|
44088
|
+
}
|
|
44089
|
+
/** OpenCode's own tools a run of this identity holds — what the deny rules
|
|
44090
|
+
* leave visible — in the record's shared vocabulary (`shell → bash`,
|
|
44091
|
+
* `glob → find`), so the conformance table, which reads the record, sees the
|
|
44092
|
+
* same words a tool call lands under and across harnesses. */
|
|
44093
|
+
builtinTools(identity2) {
|
|
44094
|
+
return openCodeBuiltinToolsFor(identity2).map(openCodeToolNameWord);
|
|
44095
|
+
}
|
|
44096
|
+
open(deps, run2) {
|
|
44097
|
+
return openOpenCodeRun(deps, run2, this.settings);
|
|
44098
|
+
}
|
|
44099
|
+
/** Where the row's OpenCode is (the survival clause), before any pid is
|
|
44100
|
+
* probed: another harness's facts answer `another-harness` with no command;
|
|
44101
|
+
* a row naming another container than this run was handed is
|
|
44102
|
+
* `another-container` (a pid here is a stranger's); else the recorded port is
|
|
44103
|
+
* probed — any HTTP answer (even the 401 the password-guarded health returns
|
|
44104
|
+
* without the bearer, which `find` does not hold) means the server is up
|
|
44105
|
+
* (`alive-here`), a connection that reaches no server means `dead`. A
|
|
44106
|
+
* container gone under the probe is the typed error, as pi's `find` rethrows. */
|
|
44107
|
+
async find(facts2, container) {
|
|
44108
|
+
if (facts2.harness !== "opencode") return "another-harness";
|
|
44109
|
+
const oc = facts2;
|
|
44110
|
+
const here = await container.identity();
|
|
44111
|
+
if (oc.container !== void 0 && here !== void 0 && oc.container !== here) return "another-container";
|
|
44112
|
+
try {
|
|
44113
|
+
const paths = openCodeRunPathsAt(oc.root);
|
|
44114
|
+
await container.request(paths, { method: "GET", port: oc.port, path: OPENCODE_ROUTES["health.get"].path });
|
|
44115
|
+
return "alive-here";
|
|
44116
|
+
} catch (err2) {
|
|
44117
|
+
if (isContainerGone(err2)) throw err2;
|
|
44118
|
+
return "dead";
|
|
44119
|
+
}
|
|
44120
|
+
}
|
|
44121
|
+
/** End the OpenCode a previous generation left behind — the server and its
|
|
44122
|
+
* tailer at the pids the facts name, the root removed — best-effort, like the
|
|
44123
|
+
* session's end; another harness's facts are left alone. */
|
|
44124
|
+
async end(facts2, container) {
|
|
44125
|
+
if (facts2.harness !== "opencode") return;
|
|
44126
|
+
const oc = facts2;
|
|
44127
|
+
await container.kill(oc.pid).catch(() => {
|
|
44128
|
+
});
|
|
44129
|
+
if (oc.tailerPid !== void 0) await container.kill(oc.tailerPid).catch(() => {
|
|
44130
|
+
});
|
|
44131
|
+
await container.remove(openCodeRunPathsAt(oc.root)).catch(() => {
|
|
44132
|
+
});
|
|
44133
|
+
}
|
|
44134
|
+
};
|
|
44135
|
+
}
|
|
44136
|
+
});
|
|
44137
|
+
|
|
41760
44138
|
// ../../src/core/runLedger/takeover.ts
|
|
41761
44139
|
var LedgerTakeover;
|
|
41762
44140
|
var init_takeover = __esm({
|
|
@@ -44894,8 +47272,8 @@ async function handleMcpMessage(identity2, rawBody, deps, options) {
|
|
|
44894
47272
|
return { status: 202 };
|
|
44895
47273
|
}
|
|
44896
47274
|
const params = typeof msg.params === "object" && msg.params !== null && !Array.isArray(msg.params) ? msg.params : {};
|
|
44897
|
-
const
|
|
44898
|
-
const body = await route2(
|
|
47275
|
+
const request2 = { id: readId(msg), method: msg.method, params };
|
|
47276
|
+
const body = await route2(request2, identity2, deps, options, lookup2);
|
|
44899
47277
|
return { status: 200, body };
|
|
44900
47278
|
}
|
|
44901
47279
|
function createMcpHandler(deps, options) {
|
|
@@ -45132,20 +47510,20 @@ function residentsFleetTone(records) {
|
|
|
45132
47510
|
return tones.size === 1 && tones.has("green") ? "green" : "grey";
|
|
45133
47511
|
}
|
|
45134
47512
|
function residentSlug(record2) {
|
|
45135
|
-
return
|
|
47513
|
+
return str7(record2.resource).replace(/^repo:/, "");
|
|
45136
47514
|
}
|
|
45137
47515
|
function residentLive(record2) {
|
|
45138
47516
|
const live = rec(record2.live);
|
|
45139
47517
|
if (typeof live.error === "string") return { ...live, state: "unreachable", reason: live.error };
|
|
45140
|
-
return { ...live, state:
|
|
47518
|
+
return { ...live, state: str7(live.state) || "unknown", reason: str7(live.reason) };
|
|
45141
47519
|
}
|
|
45142
|
-
var RESIDENT_SLUG_RE,
|
|
47520
|
+
var RESIDENT_SLUG_RE, str7, rec;
|
|
45143
47521
|
var init_residentsModel = __esm({
|
|
45144
47522
|
"../../src/channels/residentsModel.ts"() {
|
|
45145
47523
|
"use strict";
|
|
45146
47524
|
init_residentDiskBudget();
|
|
45147
47525
|
RESIDENT_SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?\/[a-z0-9](?:[a-z0-9._-]{0,98}[a-z0-9])?$/;
|
|
45148
|
-
|
|
47526
|
+
str7 = (v) => typeof v === "string" ? v : typeof v === "number" ? String(v) : "";
|
|
45149
47527
|
rec = (v) => v && typeof v === "object" ? v : {};
|
|
45150
47528
|
}
|
|
45151
47529
|
});
|
|
@@ -46200,6 +48578,707 @@ var init_residentsView = __esm({
|
|
|
46200
48578
|
}
|
|
46201
48579
|
});
|
|
46202
48580
|
|
|
48581
|
+
// ../../src/channels/accessAuth.ts
|
|
48582
|
+
import { createPublicKey, verify as cryptoVerify } from "node:crypto";
|
|
48583
|
+
function isServiceToken(identity2) {
|
|
48584
|
+
return identity2.sub === "" && typeof identity2.commonName === "string" && identity2.commonName !== "";
|
|
48585
|
+
}
|
|
48586
|
+
function decodeBase64Url(part) {
|
|
48587
|
+
if (part === "" || /[^A-Za-z0-9_-]/.test(part)) return null;
|
|
48588
|
+
return Buffer.from(part, "base64url");
|
|
48589
|
+
}
|
|
48590
|
+
function decodeJsonSegment(part) {
|
|
48591
|
+
const buf2 = decodeBase64Url(part);
|
|
48592
|
+
if (!buf2) return null;
|
|
48593
|
+
try {
|
|
48594
|
+
const parsed = JSON.parse(buf2.toString("utf8"));
|
|
48595
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
48596
|
+
return parsed;
|
|
48597
|
+
} catch {
|
|
48598
|
+
return null;
|
|
48599
|
+
}
|
|
48600
|
+
}
|
|
48601
|
+
async function resolveKey(kid, config, deps) {
|
|
48602
|
+
const cache2 = deps.cache ?? new JwksCache();
|
|
48603
|
+
const cached = cache2.get(kid, deps.now());
|
|
48604
|
+
if (cached) return cached;
|
|
48605
|
+
const ttlMs = (deps.ttlSeconds ?? DEFAULT_JWKS_TTL_SECONDS) * 1e3;
|
|
48606
|
+
const minRefetchIntervalMs = deps.minRefetchIntervalMs ?? DEFAULT_MIN_REFETCH_INTERVAL_MS;
|
|
48607
|
+
const certsUrl = `https://${config.teamDomain}${CERTS_PATH}`;
|
|
48608
|
+
await cache2.refresh(() => deps.fetchJwks(certsUrl), deps.now, ttlMs, minRefetchIntervalMs);
|
|
48609
|
+
return cache2.get(kid, deps.now());
|
|
48610
|
+
}
|
|
48611
|
+
function claimsValid(claims, config, now) {
|
|
48612
|
+
const nowSec = Math.floor(now / 1e3);
|
|
48613
|
+
if (claims.iss !== `https://${config.teamDomain}`) return false;
|
|
48614
|
+
const aud = claims.aud;
|
|
48615
|
+
const audOk = typeof aud === "string" ? aud === config.aud : Array.isArray(aud) && aud.includes(config.aud);
|
|
48616
|
+
if (!audOk) return false;
|
|
48617
|
+
if (typeof claims.exp !== "number" || claims.exp <= nowSec) return false;
|
|
48618
|
+
if (typeof claims.nbf === "number" && claims.nbf > nowSec + SKEW_SECONDS) return false;
|
|
48619
|
+
if (typeof claims.iat === "number" && claims.iat > nowSec + SKEW_SECONDS) return false;
|
|
48620
|
+
return hasSubject(claims);
|
|
48621
|
+
}
|
|
48622
|
+
function hasSubject(claims) {
|
|
48623
|
+
if (typeof claims.sub === "string" && claims.sub !== "") return true;
|
|
48624
|
+
const subEmpty = claims.sub === void 0 || claims.sub === "";
|
|
48625
|
+
return subEmpty && typeof claims.common_name === "string" && claims.common_name !== "";
|
|
48626
|
+
}
|
|
48627
|
+
async function verifyAccessJwt(token2, config, deps) {
|
|
48628
|
+
if (typeof token2 !== "string") return null;
|
|
48629
|
+
const parts = token2.split(".");
|
|
48630
|
+
if (parts.length !== 3) return null;
|
|
48631
|
+
const [rawHeader, rawPayload, rawSignature] = parts;
|
|
48632
|
+
const header4 = decodeJsonSegment(rawHeader);
|
|
48633
|
+
const payload = decodeJsonSegment(rawPayload);
|
|
48634
|
+
if (!header4 || !payload) return null;
|
|
48635
|
+
if (header4.alg !== "RS256") return null;
|
|
48636
|
+
if (typeof header4.kid !== "string" || header4.kid === "") return null;
|
|
48637
|
+
const signature = decodeBase64Url(rawSignature);
|
|
48638
|
+
if (!signature) return null;
|
|
48639
|
+
const jwk = await resolveKey(header4.kid, config, deps);
|
|
48640
|
+
if (!jwk) return null;
|
|
48641
|
+
let signatureOk;
|
|
48642
|
+
try {
|
|
48643
|
+
const key = createPublicKey({ format: "jwk", key: jwk });
|
|
48644
|
+
signatureOk = cryptoVerify("RSA-SHA256", Buffer.from(`${rawHeader}.${rawPayload}`), key, signature);
|
|
48645
|
+
} catch {
|
|
48646
|
+
return null;
|
|
48647
|
+
}
|
|
48648
|
+
if (!signatureOk) return null;
|
|
48649
|
+
if (!claimsValid(payload, config, deps.now())) return null;
|
|
48650
|
+
if (typeof payload.sub !== "string" || payload.sub === "") {
|
|
48651
|
+
return { sub: "", commonName: payload.common_name };
|
|
48652
|
+
}
|
|
48653
|
+
return {
|
|
48654
|
+
sub: payload.sub,
|
|
48655
|
+
email: typeof payload.email === "string" ? payload.email : void 0
|
|
48656
|
+
};
|
|
48657
|
+
}
|
|
48658
|
+
function normalizeTeamDomain(raw) {
|
|
48659
|
+
return raw.trim().replace(/^https?:\/\//i, "").replace(/\/+$/, "");
|
|
48660
|
+
}
|
|
48661
|
+
function parseAccessConfig(env) {
|
|
48662
|
+
const teamDomain = normalizeTeamDomain(env.ACCESS_TEAM_DOMAIN ?? "");
|
|
48663
|
+
const aud = (env.ACCESS_AUD ?? "").trim();
|
|
48664
|
+
if (teamDomain === "" || aud === "") return null;
|
|
48665
|
+
return { teamDomain, aud };
|
|
48666
|
+
}
|
|
48667
|
+
var CERTS_PATH, DEFAULT_JWKS_TTL_SECONDS, DEFAULT_MIN_REFETCH_INTERVAL_MS, SKEW_SECONDS, httpJwksFetcher, JwksCache;
|
|
48668
|
+
var init_accessAuth = __esm({
|
|
48669
|
+
"../../src/channels/accessAuth.ts"() {
|
|
48670
|
+
"use strict";
|
|
48671
|
+
CERTS_PATH = "/cdn-cgi/access/certs";
|
|
48672
|
+
DEFAULT_JWKS_TTL_SECONDS = 3600;
|
|
48673
|
+
DEFAULT_MIN_REFETCH_INTERVAL_MS = 3e4;
|
|
48674
|
+
SKEW_SECONDS = 60;
|
|
48675
|
+
httpJwksFetcher = async (certsUrl) => {
|
|
48676
|
+
const res = await fetch(certsUrl);
|
|
48677
|
+
if (!res.ok) throw new Error(`JWKS fetch failed: HTTP ${res.status}`);
|
|
48678
|
+
const body = await res.json();
|
|
48679
|
+
const keys = body?.keys;
|
|
48680
|
+
if (!Array.isArray(keys)) throw new Error("JWKS response missing keys[]");
|
|
48681
|
+
return keys;
|
|
48682
|
+
};
|
|
48683
|
+
JwksCache = class {
|
|
48684
|
+
entries = /* @__PURE__ */ new Map();
|
|
48685
|
+
/** Ms of the last SUCCESSFUL JWKS fetch, or null before the first one. Drives
|
|
48686
|
+
* the negative-cache interval in `refresh`; a failed fetch does not set it. */
|
|
48687
|
+
lastFetchAt = null;
|
|
48688
|
+
/** The in-flight refresh shared by concurrent callers (single-flight); null
|
|
48689
|
+
* when no fetch is running. It never rejects — `runFetch` swallows failures to
|
|
48690
|
+
* fail closed — so both the owner and any joiners resolve cleanly. */
|
|
48691
|
+
inFlight = null;
|
|
48692
|
+
/** The fresh JWK for `kid`, or null if absent/expired (expired entries are
|
|
48693
|
+
* evicted lazily). `now` is milliseconds. */
|
|
48694
|
+
get(kid, now) {
|
|
48695
|
+
const hit = this.entries.get(kid);
|
|
48696
|
+
if (!hit) return null;
|
|
48697
|
+
if (hit.expiresAt <= now) {
|
|
48698
|
+
this.entries.delete(kid);
|
|
48699
|
+
return null;
|
|
48700
|
+
}
|
|
48701
|
+
return hit.jwk;
|
|
48702
|
+
}
|
|
48703
|
+
/** Store every kid'd key from a freshly fetched JWKS with the given absolute
|
|
48704
|
+
* expiry (ms). Keyless entries are skipped. */
|
|
48705
|
+
put(keys, expiresAt) {
|
|
48706
|
+
for (const jwk of keys) {
|
|
48707
|
+
if (typeof jwk.kid === "string" && jwk.kid !== "") {
|
|
48708
|
+
this.entries.set(jwk.kid, { jwk, expiresAt });
|
|
48709
|
+
}
|
|
48710
|
+
}
|
|
48711
|
+
}
|
|
48712
|
+
/**
|
|
48713
|
+
* Refresh the JWKS to (try to) learn an unknown kid, with two guards against
|
|
48714
|
+
* the unknown-kid amplification vector:
|
|
48715
|
+
* - single-flight: if a refresh is already running, join it instead of
|
|
48716
|
+
* firing another — N concurrent unknown kids ⇒ at most one `fetch` call.
|
|
48717
|
+
* - negative cache: once a fetch has succeeded, another is suppressed until
|
|
48718
|
+
* `minRefetchIntervalMs` has elapsed, so an unknown kid resolves to null
|
|
48719
|
+
* WITHOUT a network round-trip inside that window. A genuinely rotated kid
|
|
48720
|
+
* is still picked up on the first refresh after the interval.
|
|
48721
|
+
* Fail-closed: a fetch failure leaves the store unchanged and does not advance
|
|
48722
|
+
* the last-fetch time. `now` is read lazily (ms) so the injected clock can move
|
|
48723
|
+
* across the await.
|
|
48724
|
+
*/
|
|
48725
|
+
async refresh(fetch2, now, ttlMs, minRefetchIntervalMs) {
|
|
48726
|
+
if (this.inFlight) {
|
|
48727
|
+
await this.inFlight;
|
|
48728
|
+
return;
|
|
48729
|
+
}
|
|
48730
|
+
if (this.lastFetchAt !== null && now() - this.lastFetchAt < minRefetchIntervalMs) {
|
|
48731
|
+
return;
|
|
48732
|
+
}
|
|
48733
|
+
this.inFlight = this.runFetch(fetch2, now, ttlMs);
|
|
48734
|
+
try {
|
|
48735
|
+
await this.inFlight;
|
|
48736
|
+
} finally {
|
|
48737
|
+
this.inFlight = null;
|
|
48738
|
+
}
|
|
48739
|
+
}
|
|
48740
|
+
/** Perform one JWKS fetch and fold the result into the store. Never rejects:
|
|
48741
|
+
* on any failure the store is left unchanged (fail closed) and `lastFetchAt`
|
|
48742
|
+
* is not advanced, so `resolveKey` returns null and a later verify may retry. */
|
|
48743
|
+
async runFetch(fetch2, now, ttlMs) {
|
|
48744
|
+
try {
|
|
48745
|
+
const keys = await fetch2();
|
|
48746
|
+
const fetchedAt = now();
|
|
48747
|
+
this.put(keys, fetchedAt + ttlMs);
|
|
48748
|
+
this.lastFetchAt = fetchedAt;
|
|
48749
|
+
} catch {
|
|
48750
|
+
}
|
|
48751
|
+
}
|
|
48752
|
+
};
|
|
48753
|
+
}
|
|
48754
|
+
});
|
|
48755
|
+
|
|
48756
|
+
// ../../src/channels/commandHttp.ts
|
|
48757
|
+
function normalizePath2(pathname) {
|
|
48758
|
+
let decoded = pathname;
|
|
48759
|
+
try {
|
|
48760
|
+
decoded = decodeURIComponent(pathname);
|
|
48761
|
+
} catch {
|
|
48762
|
+
}
|
|
48763
|
+
return decoded.replace(/\/{2,}/g, "/");
|
|
48764
|
+
}
|
|
48765
|
+
function isCommandPath(pathname) {
|
|
48766
|
+
const p = normalizePath2(pathname).toLowerCase();
|
|
48767
|
+
return p === API_ROOT || p === `${API_ROOT}/` || p.startsWith(`${API_ROOT}/`);
|
|
48768
|
+
}
|
|
48769
|
+
function commandIdFromPath(pathname) {
|
|
48770
|
+
const p = normalizePath2(pathname);
|
|
48771
|
+
if (!p.startsWith(`${API_ROOT}/`)) return void 0;
|
|
48772
|
+
const rest = p.slice(API_ROOT.length + 1);
|
|
48773
|
+
return COMMAND_ID.test(rest) ? rest : void 0;
|
|
48774
|
+
}
|
|
48775
|
+
function callerIdFor(identity2) {
|
|
48776
|
+
return isServiceToken(identity2) ? `access:svc:${identity2.commonName}` : `access:${identity2.sub}`;
|
|
48777
|
+
}
|
|
48778
|
+
function accessActor(identity2, grantsFor) {
|
|
48779
|
+
return isServiceToken(identity2) ? resolveActor({ surface: "access-service", subjectId: identity2.commonName }, grantsFor) : resolveActor({ surface: "access-browser", subjectId: identity2.sub }, grantsFor);
|
|
48780
|
+
}
|
|
48781
|
+
function serviceTokenAllowed(pathname, identity2) {
|
|
48782
|
+
return !isServiceToken(identity2) || isCommandPath(pathname);
|
|
48783
|
+
}
|
|
48784
|
+
async function resolveAccessActor(identity2, opts) {
|
|
48785
|
+
const actor = accessActor(identity2, opts.grantsFor);
|
|
48786
|
+
if (isServiceToken(identity2) || !identity2.email || !opts.personByEmail) return actor;
|
|
48787
|
+
const person = await opts.personByEmail(identity2.email).catch(() => void 0);
|
|
48788
|
+
if (!person || !person.id.startsWith("slack:")) return actor;
|
|
48789
|
+
return {
|
|
48790
|
+
...actor,
|
|
48791
|
+
self: [actor.id, person.id],
|
|
48792
|
+
asUser: { id: person.id, ...person.name ? { name: person.name } : {} }
|
|
48793
|
+
};
|
|
48794
|
+
}
|
|
48795
|
+
async function callerFor(identity2, opts) {
|
|
48796
|
+
return {
|
|
48797
|
+
kind: "access",
|
|
48798
|
+
id: callerIdFor(identity2),
|
|
48799
|
+
actor: await resolveAccessActor(identity2, opts),
|
|
48800
|
+
...identity2.email && !isServiceToken(identity2) ? { email: identity2.email } : {}
|
|
48801
|
+
};
|
|
48802
|
+
}
|
|
48803
|
+
function hostOf(url) {
|
|
48804
|
+
if (!url) return void 0;
|
|
48805
|
+
try {
|
|
48806
|
+
return new URL(url).host.toLowerCase();
|
|
48807
|
+
} catch {
|
|
48808
|
+
return void 0;
|
|
48809
|
+
}
|
|
48810
|
+
}
|
|
48811
|
+
function header2(req, name) {
|
|
48812
|
+
const v = req.headers[name];
|
|
48813
|
+
return Array.isArray(v) ? v[0] : v;
|
|
48814
|
+
}
|
|
48815
|
+
function originAllowed(req, opts) {
|
|
48816
|
+
const site = header2(req, "sec-fetch-site");
|
|
48817
|
+
if (site !== void 0 && site !== "same-origin" && site !== "none") return false;
|
|
48818
|
+
const origin = header2(req, "origin");
|
|
48819
|
+
if (origin === void 0) return true;
|
|
48820
|
+
const expected = originOf(opts.publicBaseUrl);
|
|
48821
|
+
if (expected !== void 0) return originOf(origin) === expected;
|
|
48822
|
+
const host = header2(req, "host")?.toLowerCase();
|
|
48823
|
+
const actual = hostOf(origin);
|
|
48824
|
+
return host !== void 0 && actual !== void 0 && actual === host;
|
|
48825
|
+
}
|
|
48826
|
+
function originOf(url) {
|
|
48827
|
+
if (!url) return void 0;
|
|
48828
|
+
try {
|
|
48829
|
+
const o = new URL(url).origin;
|
|
48830
|
+
return o === "null" ? void 0 : o.toLowerCase();
|
|
48831
|
+
} catch {
|
|
48832
|
+
return void 0;
|
|
48833
|
+
}
|
|
48834
|
+
}
|
|
48835
|
+
function isJsonContentType(req) {
|
|
48836
|
+
const ct = header2(req, "content-type");
|
|
48837
|
+
return ct !== void 0 && ct.split(";")[0].trim().toLowerCase() === "application/json";
|
|
48838
|
+
}
|
|
48839
|
+
function createCommandHttpHandler(commands, opts) {
|
|
48840
|
+
const maxBytes = opts.maxBodyBytes ?? MAX_BODY_BYTES2;
|
|
48841
|
+
const send = (res, status3, body, extra = {}) => {
|
|
48842
|
+
res.writeHead(status3, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", ...extra });
|
|
48843
|
+
res.end(JSON.stringify(body));
|
|
48844
|
+
};
|
|
48845
|
+
const refuse3 = (res, status3, code, error, extra = {}) => send(res, status3, { error, code }, extra);
|
|
48846
|
+
return async (req, res, identity2) => {
|
|
48847
|
+
const url = new URL(req.url ?? "/", "http://placeholder.invalid");
|
|
48848
|
+
const id = commandIdFromPath(url.pathname);
|
|
48849
|
+
const cmd = id === void 0 ? void 0 : commands.get(id);
|
|
48850
|
+
if (!cmd || !CommandRegistry.exposedTo(cmd, "access")) {
|
|
48851
|
+
refuse3(res, 404, "not_found", "unknown command");
|
|
48852
|
+
return;
|
|
48853
|
+
}
|
|
48854
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
48855
|
+
const allow = cmd.effect === "write" ? "POST" : "GET, POST";
|
|
48856
|
+
if (method !== "POST" && (cmd.effect === "write" || method !== "GET")) {
|
|
48857
|
+
refuse3(res, 405, "method_not_allowed", `${cmd.id} accepts ${allow}`, { allow });
|
|
48858
|
+
return;
|
|
48859
|
+
}
|
|
48860
|
+
if (method === "POST" && !isJsonContentType(req)) {
|
|
48861
|
+
refuse3(res, 415, "unsupported_media_type", "content-type must be application/json");
|
|
48862
|
+
return;
|
|
48863
|
+
}
|
|
48864
|
+
if (cmd.effect === "write" && !originAllowed(req, opts)) {
|
|
48865
|
+
refuse3(res, 403, "forbidden_origin", "cross-origin writes are refused");
|
|
48866
|
+
return;
|
|
48867
|
+
}
|
|
48868
|
+
const caller = await callerFor(identity2, opts);
|
|
48869
|
+
if (CommandRegistry.refuses(cmd, caller)) {
|
|
48870
|
+
refuse3(res, ERROR_STATUS.unauthorized, "unauthorized", `${caller.id} is not allowed to run ${cmd.id}`);
|
|
48871
|
+
return;
|
|
48872
|
+
}
|
|
48873
|
+
let named;
|
|
48874
|
+
let keys;
|
|
48875
|
+
if (method === "GET") {
|
|
48876
|
+
named = Object.fromEntries(url.searchParams);
|
|
48877
|
+
keys = "kebab";
|
|
48878
|
+
} else {
|
|
48879
|
+
const read = await readBody(req, maxBytes);
|
|
48880
|
+
if (!read.ok) {
|
|
48881
|
+
refuse3(res, 413, "payload_too_large", "request body too large");
|
|
48882
|
+
req.destroy();
|
|
48883
|
+
return;
|
|
48884
|
+
}
|
|
48885
|
+
let body;
|
|
48886
|
+
try {
|
|
48887
|
+
body = read.body.trim() === "" ? {} : JSON.parse(read.body);
|
|
48888
|
+
} catch {
|
|
48889
|
+
refuse3(res, ERROR_STATUS.invalid_input, "invalid_input", "body must be a JSON object");
|
|
48890
|
+
return;
|
|
48891
|
+
}
|
|
48892
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
48893
|
+
refuse3(res, ERROR_STATUS.invalid_input, "invalid_input", "body must be a JSON object");
|
|
48894
|
+
return;
|
|
48895
|
+
}
|
|
48896
|
+
named = body;
|
|
48897
|
+
keys = "camel";
|
|
48898
|
+
}
|
|
48899
|
+
const input = namedToInput(cmd, named, keys);
|
|
48900
|
+
if ("error" in input) {
|
|
48901
|
+
refuse3(res, ERROR_STATUS.invalid_input, "invalid_input", input.error);
|
|
48902
|
+
return;
|
|
48903
|
+
}
|
|
48904
|
+
const result = await commands.invoke(cmd.id, input, caller);
|
|
48905
|
+
if (!result.ok) {
|
|
48906
|
+
refuse3(res, result.status, result.error, result.message);
|
|
48907
|
+
return;
|
|
48908
|
+
}
|
|
48909
|
+
send(res, 200, result.value);
|
|
48910
|
+
};
|
|
48911
|
+
}
|
|
48912
|
+
var API_ROOT;
|
|
48913
|
+
var init_commandHttp = __esm({
|
|
48914
|
+
"../../src/channels/commandHttp.ts"() {
|
|
48915
|
+
"use strict";
|
|
48916
|
+
init_actor();
|
|
48917
|
+
init_commandRegistry();
|
|
48918
|
+
init_commandSurface();
|
|
48919
|
+
init_accessAuth();
|
|
48920
|
+
init_http();
|
|
48921
|
+
API_ROOT = "/api";
|
|
48922
|
+
}
|
|
48923
|
+
});
|
|
48924
|
+
|
|
48925
|
+
// ../../src/channels/web.ts
|
|
48926
|
+
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
48927
|
+
function parseThreadsRoute(pathname) {
|
|
48928
|
+
if (pathname === "/threads" || pathname === "/threads/") return { kind: "new" };
|
|
48929
|
+
const m = /^\/threads\/([^/]+)(\/send)?\/?$/.exec(pathname);
|
|
48930
|
+
if (!m) return null;
|
|
48931
|
+
let id;
|
|
48932
|
+
try {
|
|
48933
|
+
id = decodeURIComponent(m[1]);
|
|
48934
|
+
} catch {
|
|
48935
|
+
return null;
|
|
48936
|
+
}
|
|
48937
|
+
if (!CONVERSATION_ID_RE.test(id) && !THREAD_KEY_RE.test(id)) return null;
|
|
48938
|
+
return m[2] ? { kind: "send", id } : { kind: "thread", id };
|
|
48939
|
+
}
|
|
48940
|
+
function mintConversationId() {
|
|
48941
|
+
return randomBytes6(CONVERSATION_ID_BYTES).toString("hex");
|
|
48942
|
+
}
|
|
48943
|
+
function threadKeyFor(sub, id) {
|
|
48944
|
+
return id.includes(":") ? id : `${PLATFORM5}:${sub}:${id}`;
|
|
48945
|
+
}
|
|
48946
|
+
function ownLane(sub, threadKey) {
|
|
48947
|
+
return threadKey.startsWith(`${PLATFORM5}:${sub}:`);
|
|
48948
|
+
}
|
|
48949
|
+
function conversationIdOf(sub, threadKey) {
|
|
48950
|
+
return ownLane(sub, threadKey) ? threadKey.slice(`${PLATFORM5}:${sub}:`.length) : threadKey;
|
|
48951
|
+
}
|
|
48952
|
+
function requesterOf(actor, identity2) {
|
|
48953
|
+
if (actor.asUser)
|
|
48954
|
+
return {
|
|
48955
|
+
userId: actor.asUser.id,
|
|
48956
|
+
...actor.asUser.name ? { userName: actor.asUser.name } : {},
|
|
48957
|
+
authenticatedAs: actor.id
|
|
48958
|
+
};
|
|
48959
|
+
return { userId: actor.id, ...identity2.email ? { userName: identity2.email } : {} };
|
|
48960
|
+
}
|
|
48961
|
+
function threadTitle(firstRequest, max = TITLE_MAX2) {
|
|
48962
|
+
const line3 = firstRequest.split(/\r?\n/).find((l) => l.trim() !== "")?.trim() ?? "";
|
|
48963
|
+
if (line3.length <= max) return line3 || "New conversation";
|
|
48964
|
+
return `${line3.slice(0, max - 1).trimEnd()}\u2026`;
|
|
48965
|
+
}
|
|
48966
|
+
function turnOf(view, token2) {
|
|
48967
|
+
const { events, route: _route, ...rest } = view;
|
|
48968
|
+
let request2 = "";
|
|
48969
|
+
let answer;
|
|
48970
|
+
let route3;
|
|
48971
|
+
for (const e of events ?? []) {
|
|
48972
|
+
if (e.type === "input") {
|
|
48973
|
+
if (request2 === "") request2 = e.text;
|
|
48974
|
+
} else if (e.type === "answer") answer = e.text;
|
|
48975
|
+
else if (e.type === "route" && route3 === void 0) route3 = { preset: e.preset, reason: e.reason };
|
|
48976
|
+
}
|
|
48977
|
+
return {
|
|
48978
|
+
...rest,
|
|
48979
|
+
...token2 !== void 0 ? { token: token2 } : {},
|
|
48980
|
+
request: request2,
|
|
48981
|
+
...answer !== void 0 ? { answer } : {},
|
|
48982
|
+
...route3 !== void 0 ? { route: route3 } : {}
|
|
48983
|
+
};
|
|
48984
|
+
}
|
|
48985
|
+
function historyOf(turns) {
|
|
48986
|
+
const items = [];
|
|
48987
|
+
for (const t of turns) {
|
|
48988
|
+
if (t.request) items.push({ role: "user", text: t.request, at: t.receivedAt ?? t.startedAt });
|
|
48989
|
+
if (t.answer !== void 0 && t.finishedAt !== void 0)
|
|
48990
|
+
items.push({ role: "assistant", text: t.answer, at: t.finishedAt });
|
|
48991
|
+
}
|
|
48992
|
+
return items;
|
|
48993
|
+
}
|
|
48994
|
+
function threadsOf(runs, limit = RAIL_THREADS) {
|
|
48995
|
+
const groups = /* @__PURE__ */ new Map();
|
|
48996
|
+
for (const run2 of runs) {
|
|
48997
|
+
if (!run2.threadKey) continue;
|
|
48998
|
+
const at = run2.finishedAt ?? run2.startedAt;
|
|
48999
|
+
const g = groups.get(run2.threadKey);
|
|
49000
|
+
if (g) {
|
|
49001
|
+
g.runs.push(run2);
|
|
49002
|
+
g.lastAt = Math.max(g.lastAt, at);
|
|
49003
|
+
g.live ||= !run2.finished;
|
|
49004
|
+
} else {
|
|
49005
|
+
groups.set(run2.threadKey, {
|
|
49006
|
+
threadKey: run2.threadKey,
|
|
49007
|
+
surface: platformOf2(run2.channelId ?? run2.threadKey),
|
|
49008
|
+
runs: [run2],
|
|
49009
|
+
lastAt: at,
|
|
49010
|
+
live: !run2.finished
|
|
49011
|
+
});
|
|
49012
|
+
}
|
|
49013
|
+
}
|
|
49014
|
+
const out = [...groups.values()].sort((a, b) => b.lastAt - a.lastAt).slice(0, limit);
|
|
49015
|
+
for (const g of out) g.runs.sort((a, b) => a.startedAt - b.startedAt);
|
|
49016
|
+
return out;
|
|
49017
|
+
}
|
|
49018
|
+
function platformOf2(id) {
|
|
49019
|
+
const colon = id.indexOf(":");
|
|
49020
|
+
return colon > 0 ? id.slice(0, colon) : "unknown";
|
|
49021
|
+
}
|
|
49022
|
+
function paletteCommands(list, actor) {
|
|
49023
|
+
return list.filter((cmd) => cmd.surfaces?.chat !== false).filter((cmd) => authorize(actor, cmd.action, { type: "command", id: cmd.id }).allow).map((cmd) => ({ chat: chatForm(cmd.id), describe: cmd.describe })).sort((a, b) => a.chat.localeCompare(b.chat));
|
|
49024
|
+
}
|
|
49025
|
+
function suggestionsFor(input) {
|
|
49026
|
+
const repo = input.repos[0];
|
|
49027
|
+
const chips = [];
|
|
49028
|
+
if (repo) chips.push(`review the open PR on ${repo}`);
|
|
49029
|
+
else if (input.capabilities.github) chips.push("review a pull request \u2014 paste its link");
|
|
49030
|
+
if (input.failed) chips.push("why did my last run fail?");
|
|
49031
|
+
else if (input.any) chips.push("what did my last run do?");
|
|
49032
|
+
chips.push("what agent and model do I get here?");
|
|
49033
|
+
if (input.capabilities.mcp) chips.push("connect an MCP server");
|
|
49034
|
+
chips.push("What can Switchboard do?");
|
|
49035
|
+
return chips;
|
|
49036
|
+
}
|
|
49037
|
+
function createWebChatHandler(deps) {
|
|
49038
|
+
const now = deps.now ?? systemClock;
|
|
49039
|
+
const mintId = deps.mintId ?? mintConversationId;
|
|
49040
|
+
const warn = deps.warn ?? ((m) => console.warn(`[web] ${m}`));
|
|
49041
|
+
const dispatchFn = deps.dispatch ?? dispatch;
|
|
49042
|
+
const maxBytes = deps.maxBodyBytes ?? MAX_BODY_BYTES2;
|
|
49043
|
+
const retentionDays = deps.retention ? deps.retention.retentionDays : null;
|
|
49044
|
+
const subOf = (actor) => actor.id.replace(/^access:/, "");
|
|
49045
|
+
const liveToken = (run2) => run2.finished ? void 0 : deps.registry.getById(run2.id)?.token;
|
|
49046
|
+
async function turnsOf(threadKey, actor) {
|
|
49047
|
+
const listed = await deps.service.listRuns({
|
|
49048
|
+
status: "all",
|
|
49049
|
+
visibleTo: readableRuns(actor),
|
|
49050
|
+
threadKey,
|
|
49051
|
+
limit: CONVERSATION_RUNS
|
|
49052
|
+
});
|
|
49053
|
+
if (listed.storeUnavailable) warn(`the run store is unavailable: ${threadKey} seeds its live runs only`);
|
|
49054
|
+
const runs = [...listed.runs].sort((a, b) => a.startedAt - b.startedAt);
|
|
49055
|
+
const turns = await Promise.all(
|
|
49056
|
+
runs.map(async (run2) => {
|
|
49057
|
+
const read = await deps.service.getRun(run2.id, { include: "messages" });
|
|
49058
|
+
return turnOf(read.ok ? read.value : { ...run2 }, liveToken(run2));
|
|
49059
|
+
})
|
|
49060
|
+
);
|
|
49061
|
+
return { turns, runs };
|
|
49062
|
+
}
|
|
49063
|
+
async function railOf(actor, sub) {
|
|
49064
|
+
const listed = await deps.service.listRuns({
|
|
49065
|
+
status: "all",
|
|
49066
|
+
visibleTo: allOf([readableRuns(actor), ownedBy(actor)]),
|
|
49067
|
+
limit: RAIL_RUNS
|
|
49068
|
+
});
|
|
49069
|
+
const groups = threadsOf(listed.runs);
|
|
49070
|
+
const rows = await Promise.all(
|
|
49071
|
+
groups.map(async (g) => {
|
|
49072
|
+
const first = g.runs[0];
|
|
49073
|
+
const read = await deps.service.getRun(first.id, { include: "messages" });
|
|
49074
|
+
const request2 = read.ok ? turnOf(read.value).request : "";
|
|
49075
|
+
return {
|
|
49076
|
+
id: conversationIdOf(sub, g.threadKey),
|
|
49077
|
+
title: threadTitle(request2 || first.label || first.id),
|
|
49078
|
+
lastAt: g.lastAt,
|
|
49079
|
+
runs: g.runs.length,
|
|
49080
|
+
live: g.live,
|
|
49081
|
+
surface: g.surface
|
|
49082
|
+
};
|
|
49083
|
+
})
|
|
49084
|
+
);
|
|
49085
|
+
return { rows, runs: listed.runs };
|
|
49086
|
+
}
|
|
49087
|
+
async function seedFor(ctx, conversation, threadKey, open2) {
|
|
49088
|
+
const sub = subOf(ctx.actor);
|
|
49089
|
+
const rail = await railOf(ctx.actor, sub);
|
|
49090
|
+
const mine = rail.runs;
|
|
49091
|
+
const repos = [...new Set(mine.map((r) => r.repo).filter((r) => r !== void 0))];
|
|
49092
|
+
const foreign = !ownLane(sub, threadKey);
|
|
49093
|
+
return {
|
|
49094
|
+
page: "home",
|
|
49095
|
+
conversation,
|
|
49096
|
+
turns: open2.turns,
|
|
49097
|
+
conversations: rail.rows,
|
|
49098
|
+
viewer: { name: ctx.actor.asUser?.name ?? ctx.identity.email ?? sub },
|
|
49099
|
+
sendUrl: `/threads/${encodeURIComponent(conversation)}/send`,
|
|
49100
|
+
...foreign ? {
|
|
49101
|
+
elsewhere: {
|
|
49102
|
+
surface: platformOf2(threadKey),
|
|
49103
|
+
...(() => {
|
|
49104
|
+
const url = open2.runs.find((r) => r.sourceUrl)?.sourceUrl;
|
|
49105
|
+
return url ? { url } : {};
|
|
49106
|
+
})()
|
|
49107
|
+
}
|
|
49108
|
+
} : {},
|
|
49109
|
+
now: now(),
|
|
49110
|
+
retentionDays,
|
|
49111
|
+
suggestions: suggestionsFor({
|
|
49112
|
+
repos,
|
|
49113
|
+
failed: mine.some((r) => r.status === "failed"),
|
|
49114
|
+
any: mine.length > 0,
|
|
49115
|
+
capabilities: deps.capabilities
|
|
49116
|
+
}),
|
|
49117
|
+
commands: paletteCommands(deps.commands.list(), ctx.actor)
|
|
49118
|
+
};
|
|
49119
|
+
}
|
|
49120
|
+
async function send(req, res, ctx, id) {
|
|
49121
|
+
const answer = (status3, body) => {
|
|
49122
|
+
res.writeHead(status3, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
49123
|
+
res.end(JSON.stringify(body));
|
|
49124
|
+
};
|
|
49125
|
+
if (!originAllowed(req, { publicBaseUrl: deps.publicBaseUrl })) {
|
|
49126
|
+
answer(403, { error: "forbidden_origin" });
|
|
49127
|
+
req.destroy();
|
|
49128
|
+
return;
|
|
49129
|
+
}
|
|
49130
|
+
const ct = (Array.isArray(req.headers["content-type"]) ? req.headers["content-type"][0] : req.headers["content-type"])?.split(";")[0].trim().toLowerCase();
|
|
49131
|
+
if (ct !== "application/json") {
|
|
49132
|
+
answer(415, { error: "unsupported_media_type" });
|
|
49133
|
+
req.destroy();
|
|
49134
|
+
return;
|
|
49135
|
+
}
|
|
49136
|
+
const sub = subOf(ctx.actor);
|
|
49137
|
+
const threadKey = threadKeyFor(sub, id);
|
|
49138
|
+
if (!ownLane(sub, threadKey)) {
|
|
49139
|
+
answer(403, { error: "forbidden", detail: `this thread lives on ${platformOf2(threadKey)}; reply there` });
|
|
49140
|
+
req.destroy();
|
|
49141
|
+
return;
|
|
49142
|
+
}
|
|
49143
|
+
const read = await readBody(req, maxBytes);
|
|
49144
|
+
if (!read.ok) {
|
|
49145
|
+
answer(413, { error: "request body too large" });
|
|
49146
|
+
req.destroy();
|
|
49147
|
+
return;
|
|
49148
|
+
}
|
|
49149
|
+
let text;
|
|
49150
|
+
try {
|
|
49151
|
+
const parsed = JSON.parse(read.body);
|
|
49152
|
+
if (typeof parsed !== "object" || parsed === null || typeof parsed.text !== "string" || parsed.text.trim() === "")
|
|
49153
|
+
throw new Error("shape");
|
|
49154
|
+
text = parsed.text;
|
|
49155
|
+
} catch {
|
|
49156
|
+
answer(400, { error: "`text` is required and must be a non-empty string" });
|
|
49157
|
+
return;
|
|
49158
|
+
}
|
|
49159
|
+
const receivedAt = now();
|
|
49160
|
+
const trace = startRequestRoot(deps.core, { channel: "web", receivedAt });
|
|
49161
|
+
const msg = {
|
|
49162
|
+
...requesterOf(ctx.actor, ctx.identity),
|
|
49163
|
+
channelId: `${PLATFORM5}:${sub}`,
|
|
49164
|
+
threadKey,
|
|
49165
|
+
text,
|
|
49166
|
+
receivedAt
|
|
49167
|
+
};
|
|
49168
|
+
const io = new WebIO(
|
|
49169
|
+
async (exceptRunId) => historyOf((await turnsOf(threadKey, ctx.actor)).turns.filter((t) => t.id !== exceptRunId))
|
|
49170
|
+
);
|
|
49171
|
+
const done = dispatchFn(deps.core, msg, io, { trace }).catch((err2) => {
|
|
49172
|
+
warn(`dispatch: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
49173
|
+
});
|
|
49174
|
+
const started = await Promise.race([io.started, done.then(() => void 0)]);
|
|
49175
|
+
const token2 = started ? deps.registry.getById(started.id)?.token : void 0;
|
|
49176
|
+
if (started && token2 !== void 0) {
|
|
49177
|
+
answer(202, {
|
|
49178
|
+
runId: started.id,
|
|
49179
|
+
viewPath: `/runs/${encodeURIComponent(started.id)}?t=${token2}`,
|
|
49180
|
+
threadKey
|
|
49181
|
+
});
|
|
49182
|
+
return;
|
|
49183
|
+
}
|
|
49184
|
+
await done;
|
|
49185
|
+
const run2 = io.run();
|
|
49186
|
+
answer(200, { reply: io.collected(), ...run2 ? { run: run2 } : {} });
|
|
49187
|
+
}
|
|
49188
|
+
return (req, res, ctx) => {
|
|
49189
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
49190
|
+
const route3 = parseThreadsRoute(url.pathname);
|
|
49191
|
+
if (!route3) return false;
|
|
49192
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
49193
|
+
const plain5 = (status3, body, extra = {}) => {
|
|
49194
|
+
res.writeHead(status3, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store", ...extra });
|
|
49195
|
+
res.end(body);
|
|
49196
|
+
};
|
|
49197
|
+
const failed = (err2) => {
|
|
49198
|
+
const reason2 = (err2 instanceof Error ? err2.message : String(err2)).slice(0, UPSTREAM_REASON_MAX2);
|
|
49199
|
+
if (res.headersSent) {
|
|
49200
|
+
warn(`render failed after the head was sent: ${reason2}`);
|
|
49201
|
+
res.end();
|
|
49202
|
+
return;
|
|
49203
|
+
}
|
|
49204
|
+
plain5(502, `threads unavailable: ${reason2}`);
|
|
49205
|
+
};
|
|
49206
|
+
if (route3.kind === "send") {
|
|
49207
|
+
if (method !== "POST") {
|
|
49208
|
+
plain5(405, "method not allowed", { allow: "POST" });
|
|
49209
|
+
return true;
|
|
49210
|
+
}
|
|
49211
|
+
send(req, res, ctx, route3.id).catch(failed);
|
|
49212
|
+
return true;
|
|
49213
|
+
}
|
|
49214
|
+
if (method !== "GET") {
|
|
49215
|
+
plain5(405, "method not allowed", { allow: "GET" });
|
|
49216
|
+
return true;
|
|
49217
|
+
}
|
|
49218
|
+
const render3 = (title, seed) => {
|
|
49219
|
+
res.writeHead(200, WEB_HTML_HEADERS);
|
|
49220
|
+
res.end(deps.shell(title, seed));
|
|
49221
|
+
};
|
|
49222
|
+
const sub = subOf(ctx.actor);
|
|
49223
|
+
if (route3.kind === "new") {
|
|
49224
|
+
const conversation = mintId();
|
|
49225
|
+
seedFor(ctx, conversation, threadKeyFor(sub, conversation), { turns: [], runs: [] }).then((seed) => render3("Threads", seed)).catch(failed);
|
|
49226
|
+
return true;
|
|
49227
|
+
}
|
|
49228
|
+
const threadKey = threadKeyFor(sub, route3.id);
|
|
49229
|
+
turnsOf(threadKey, ctx.actor).then(async (open2) => {
|
|
49230
|
+
if (open2.runs.length === 0 && !ownLane(sub, threadKey)) {
|
|
49231
|
+
res.writeHead(404, WEB_HTML_HEADERS);
|
|
49232
|
+
res.end(deps.shell("Run not found", { page: "runNotFound", retentionDays }));
|
|
49233
|
+
return;
|
|
49234
|
+
}
|
|
49235
|
+
const seed = await seedFor(ctx, route3.id, threadKey, open2);
|
|
49236
|
+
const liveCount = open2.runs.filter((r) => !r.finished).length;
|
|
49237
|
+
render3(`${liveCount > 0 ? `(${liveCount}) ` : ""}Threads`, seed);
|
|
49238
|
+
}).catch(failed);
|
|
49239
|
+
return true;
|
|
49240
|
+
};
|
|
49241
|
+
}
|
|
49242
|
+
var PLATFORM5, CONVERSATION_ID_BYTES, CONVERSATION_ID_RE, THREAD_KEY_RE, RAIL_THREADS, CONVERSATION_RUNS, RAIL_RUNS, TITLE_MAX2, UPSTREAM_REASON_MAX2, WebIO;
|
|
49243
|
+
var init_web2 = __esm({
|
|
49244
|
+
"../../src/channels/web.ts"() {
|
|
49245
|
+
"use strict";
|
|
49246
|
+
init_authz();
|
|
49247
|
+
init_commandSurface();
|
|
49248
|
+
init_dispatcher();
|
|
49249
|
+
init_requestTrace();
|
|
49250
|
+
init_clock();
|
|
49251
|
+
init_commandHttp();
|
|
49252
|
+
init_http();
|
|
49253
|
+
init_viewer();
|
|
49254
|
+
init_webShell();
|
|
49255
|
+
PLATFORM5 = "web";
|
|
49256
|
+
CONVERSATION_ID_BYTES = 10;
|
|
49257
|
+
CONVERSATION_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
49258
|
+
THREAD_KEY_RE = /^[a-z]+:[A-Za-z0-9_.:@+-]{1,200}$/;
|
|
49259
|
+
RAIL_THREADS = 20;
|
|
49260
|
+
CONVERSATION_RUNS = 20;
|
|
49261
|
+
RAIL_RUNS = 100;
|
|
49262
|
+
TITLE_MAX2 = 60;
|
|
49263
|
+
UPSTREAM_REASON_MAX2 = 400;
|
|
49264
|
+
WebIO = class extends HttpIO {
|
|
49265
|
+
constructor(turns) {
|
|
49266
|
+
super();
|
|
49267
|
+
this.turns = turns;
|
|
49268
|
+
}
|
|
49269
|
+
turns;
|
|
49270
|
+
runId;
|
|
49271
|
+
runStarted(started) {
|
|
49272
|
+
this.runId = started.id;
|
|
49273
|
+
super.runStarted(started);
|
|
49274
|
+
}
|
|
49275
|
+
history() {
|
|
49276
|
+
return this.turns(this.runId);
|
|
49277
|
+
}
|
|
49278
|
+
};
|
|
49279
|
+
}
|
|
49280
|
+
});
|
|
49281
|
+
|
|
46203
49282
|
// ../../src/channels/costsView.ts
|
|
46204
49283
|
function parseCostsRoute(pathname, search = "") {
|
|
46205
49284
|
const params = new URLSearchParams(search);
|
|
@@ -46269,7 +49348,7 @@ function createCostsViewHandler(service, shell) {
|
|
|
46269
49348
|
plain2(res, 503, err2.message, { "retry-after": String(NO_SNAPSHOT_RETRY_AFTER_SECONDS) });
|
|
46270
49349
|
return;
|
|
46271
49350
|
}
|
|
46272
|
-
const reason2 = (err2 instanceof Error ? err2.message : String(err2)).slice(0,
|
|
49351
|
+
const reason2 = (err2 instanceof Error ? err2.message : String(err2)).slice(0, UPSTREAM_REASON_MAX3);
|
|
46273
49352
|
plain2(res, 502, `cost report unavailable: ${reason2}`);
|
|
46274
49353
|
};
|
|
46275
49354
|
if (route3.kind === "users-json") {
|
|
@@ -46299,7 +49378,7 @@ function createCostsViewHandler(service, shell) {
|
|
|
46299
49378
|
return true;
|
|
46300
49379
|
};
|
|
46301
49380
|
}
|
|
46302
|
-
var GROUP_RE, SNAPSHOT_COMMAND,
|
|
49381
|
+
var GROUP_RE, SNAPSHOT_COMMAND, UPSTREAM_REASON_MAX3, NO_SNAPSHOT_RETRY_AFTER_SECONDS, orNone;
|
|
46303
49382
|
var init_costsView = __esm({
|
|
46304
49383
|
"../../src/channels/costsView.ts"() {
|
|
46305
49384
|
"use strict";
|
|
@@ -46309,7 +49388,7 @@ var init_costsView = __esm({
|
|
|
46309
49388
|
init_webShell();
|
|
46310
49389
|
GROUP_RE = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
46311
49390
|
SNAPSHOT_COMMAND = { type: "command", id: "costs.snapshot" };
|
|
46312
|
-
|
|
49391
|
+
UPSTREAM_REASON_MAX3 = 400;
|
|
46313
49392
|
NO_SNAPSHOT_RETRY_AFTER_SECONDS = 60;
|
|
46314
49393
|
orNone = (read) => read.catch((err2) => {
|
|
46315
49394
|
if (err2 instanceof NoCostsSnapshotError) return null;
|
|
@@ -46383,13 +49462,13 @@ function createDeliveryViewHandler(deps, shell) {
|
|
|
46383
49462
|
res.writeHead(200, WEB_HTML_HEADERS);
|
|
46384
49463
|
res.end(html);
|
|
46385
49464
|
}).catch((err2) => {
|
|
46386
|
-
const reason2 = (err2 instanceof Error ? err2.message : String(err2)).slice(0,
|
|
49465
|
+
const reason2 = (err2 instanceof Error ? err2.message : String(err2)).slice(0, UPSTREAM_REASON_MAX4);
|
|
46387
49466
|
plain3(res, 502, `delivery sources unavailable: ${reason2}`);
|
|
46388
49467
|
});
|
|
46389
49468
|
return true;
|
|
46390
49469
|
};
|
|
46391
49470
|
}
|
|
46392
|
-
var SEGMENT, DELIVERY_NO_REPOS_MESSAGE,
|
|
49471
|
+
var SEGMENT, DELIVERY_NO_REPOS_MESSAGE, UPSTREAM_REASON_MAX4;
|
|
46393
49472
|
var init_deliveryView = __esm({
|
|
46394
49473
|
"../../src/channels/deliveryView.ts"() {
|
|
46395
49474
|
"use strict";
|
|
@@ -46399,7 +49478,7 @@ var init_deliveryView = __esm({
|
|
|
46399
49478
|
init_webShell();
|
|
46400
49479
|
SEGMENT = /^(?!\.+$)[\w.-]{1,100}$/;
|
|
46401
49480
|
DELIVERY_NO_REPOS_MESSAGE = "No repository to report on \u2014 name one or more `owner/name` under delivery.repos in config to enable this view (the `delivery report --repo` command needs no config).";
|
|
46402
|
-
|
|
49481
|
+
UPSTREAM_REASON_MAX4 = 400;
|
|
46403
49482
|
}
|
|
46404
49483
|
});
|
|
46405
49484
|
|
|
@@ -46435,6 +49514,20 @@ function plain4(res, status3, body, extra = {}) {
|
|
|
46435
49514
|
res.writeHead(status3, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store", ...extra });
|
|
46436
49515
|
res.end(body);
|
|
46437
49516
|
}
|
|
49517
|
+
function viewerSettingsView(description) {
|
|
49518
|
+
const strip = (s) => {
|
|
49519
|
+
if (!s) return void 0;
|
|
49520
|
+
const { mcpServers: _servers, ...kept } = s;
|
|
49521
|
+
return kept;
|
|
49522
|
+
};
|
|
49523
|
+
return {
|
|
49524
|
+
effective: description.effective,
|
|
49525
|
+
defaults: description.defaults,
|
|
49526
|
+
restrictedAgents: description.restrictedAgents,
|
|
49527
|
+
user: strip(description.user) ?? {},
|
|
49528
|
+
...description.org ? { org: strip(description.org) } : {}
|
|
49529
|
+
};
|
|
49530
|
+
}
|
|
46438
49531
|
function channelScopeView(description) {
|
|
46439
49532
|
const { user: _user, channel, org, ...rest } = description;
|
|
46440
49533
|
const strip = (s) => {
|
|
@@ -46478,11 +49571,15 @@ function createSettingsViewHandler(deps, shell) {
|
|
|
46478
49571
|
return { ...channel ? { channel } : {}, servers, canWrite: write };
|
|
46479
49572
|
}
|
|
46480
49573
|
async function channelsSeed(caller, channel) {
|
|
46481
|
-
const [indexed, shown] = await Promise.all([
|
|
49574
|
+
const [indexed, mine, shown] = await Promise.all([
|
|
46482
49575
|
deps.commands.invoke("config.overrides", {}, caller),
|
|
49576
|
+
// The viewer's own settings, always: a settings page never has "no data" (record 0041).
|
|
49577
|
+
deps.commands.invoke("config.show", {}, caller),
|
|
46483
49578
|
channel ? deps.commands.invoke("config.show", { options: { channel } }, caller) : Promise.resolve(void 0)
|
|
46484
49579
|
]);
|
|
46485
49580
|
const out = indexed.ok ? { index: indexed.value.channels ?? [] } : { index: [], unavailable: failureText(indexed) };
|
|
49581
|
+
if (mine.ok) out.viewer = viewerSettingsView(mine.value);
|
|
49582
|
+
else out.viewerUnavailable = failureText(mine);
|
|
46486
49583
|
if (channel && shown) {
|
|
46487
49584
|
const write = canWrite(caller, "config:write", "channel", channel);
|
|
46488
49585
|
out.selected = shown.ok ? { channelId: channel, scope: channelScopeView(shown.value), canWrite: write } : { channelId: channel, refused: failureText(shown), canWrite: write };
|
|
@@ -46499,7 +49596,7 @@ function createSettingsViewHandler(deps, shell) {
|
|
|
46499
49596
|
}
|
|
46500
49597
|
const tab = route3.tab === "home" ? homeTab(deps.capabilities) : route3.tab;
|
|
46501
49598
|
const failed = (err2) => {
|
|
46502
|
-
const reason2 = (err2 instanceof Error ? err2.message : String(err2)).slice(0,
|
|
49599
|
+
const reason2 = (err2 instanceof Error ? err2.message : String(err2)).slice(0, UPSTREAM_REASON_MAX5);
|
|
46503
49600
|
if (res.headersSent) {
|
|
46504
49601
|
console.error(`[settings] render failed after the head was sent: ${reason2}`);
|
|
46505
49602
|
res.end();
|
|
@@ -46536,14 +49633,14 @@ function createSettingsViewHandler(deps, shell) {
|
|
|
46536
49633
|
return true;
|
|
46537
49634
|
};
|
|
46538
49635
|
}
|
|
46539
|
-
var CHANNEL_ID_RE,
|
|
49636
|
+
var CHANNEL_ID_RE, UPSTREAM_REASON_MAX5, failureText;
|
|
46540
49637
|
var init_settingsView = __esm({
|
|
46541
49638
|
"../../src/channels/settingsView.ts"() {
|
|
46542
49639
|
"use strict";
|
|
46543
49640
|
init_authorize();
|
|
46544
49641
|
init_webShell();
|
|
46545
49642
|
CHANNEL_ID_RE = /^[a-z]+:[A-Za-z0-9_.:-]{1,80}$/;
|
|
46546
|
-
|
|
49643
|
+
UPSTREAM_REASON_MAX5 = 400;
|
|
46547
49644
|
failureText = (r) => r.message;
|
|
46548
49645
|
}
|
|
46549
49646
|
});
|
|
@@ -46821,184 +49918,9 @@ var init_installationSettings = __esm({
|
|
|
46821
49918
|
}
|
|
46822
49919
|
});
|
|
46823
49920
|
|
|
46824
|
-
// ../../src/channels/accessAuth.ts
|
|
46825
|
-
import { createPublicKey, verify as cryptoVerify } from "node:crypto";
|
|
46826
|
-
function isServiceToken(identity2) {
|
|
46827
|
-
return identity2.sub === "" && typeof identity2.commonName === "string" && identity2.commonName !== "";
|
|
46828
|
-
}
|
|
46829
|
-
function decodeBase64Url(part) {
|
|
46830
|
-
if (part === "" || /[^A-Za-z0-9_-]/.test(part)) return null;
|
|
46831
|
-
return Buffer.from(part, "base64url");
|
|
46832
|
-
}
|
|
46833
|
-
function decodeJsonSegment(part) {
|
|
46834
|
-
const buf2 = decodeBase64Url(part);
|
|
46835
|
-
if (!buf2) return null;
|
|
46836
|
-
try {
|
|
46837
|
-
const parsed = JSON.parse(buf2.toString("utf8"));
|
|
46838
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
46839
|
-
return parsed;
|
|
46840
|
-
} catch {
|
|
46841
|
-
return null;
|
|
46842
|
-
}
|
|
46843
|
-
}
|
|
46844
|
-
async function resolveKey(kid, config, deps) {
|
|
46845
|
-
const cache2 = deps.cache ?? new JwksCache();
|
|
46846
|
-
const cached = cache2.get(kid, deps.now());
|
|
46847
|
-
if (cached) return cached;
|
|
46848
|
-
const ttlMs = (deps.ttlSeconds ?? DEFAULT_JWKS_TTL_SECONDS) * 1e3;
|
|
46849
|
-
const minRefetchIntervalMs = deps.minRefetchIntervalMs ?? DEFAULT_MIN_REFETCH_INTERVAL_MS;
|
|
46850
|
-
const certsUrl = `https://${config.teamDomain}${CERTS_PATH}`;
|
|
46851
|
-
await cache2.refresh(() => deps.fetchJwks(certsUrl), deps.now, ttlMs, minRefetchIntervalMs);
|
|
46852
|
-
return cache2.get(kid, deps.now());
|
|
46853
|
-
}
|
|
46854
|
-
function claimsValid(claims, config, now) {
|
|
46855
|
-
const nowSec = Math.floor(now / 1e3);
|
|
46856
|
-
if (claims.iss !== `https://${config.teamDomain}`) return false;
|
|
46857
|
-
const aud = claims.aud;
|
|
46858
|
-
const audOk = typeof aud === "string" ? aud === config.aud : Array.isArray(aud) && aud.includes(config.aud);
|
|
46859
|
-
if (!audOk) return false;
|
|
46860
|
-
if (typeof claims.exp !== "number" || claims.exp <= nowSec) return false;
|
|
46861
|
-
if (typeof claims.nbf === "number" && claims.nbf > nowSec + SKEW_SECONDS) return false;
|
|
46862
|
-
if (typeof claims.iat === "number" && claims.iat > nowSec + SKEW_SECONDS) return false;
|
|
46863
|
-
return hasSubject(claims);
|
|
46864
|
-
}
|
|
46865
|
-
function hasSubject(claims) {
|
|
46866
|
-
if (typeof claims.sub === "string" && claims.sub !== "") return true;
|
|
46867
|
-
const subEmpty = claims.sub === void 0 || claims.sub === "";
|
|
46868
|
-
return subEmpty && typeof claims.common_name === "string" && claims.common_name !== "";
|
|
46869
|
-
}
|
|
46870
|
-
async function verifyAccessJwt(token2, config, deps) {
|
|
46871
|
-
if (typeof token2 !== "string") return null;
|
|
46872
|
-
const parts = token2.split(".");
|
|
46873
|
-
if (parts.length !== 3) return null;
|
|
46874
|
-
const [rawHeader, rawPayload, rawSignature] = parts;
|
|
46875
|
-
const header4 = decodeJsonSegment(rawHeader);
|
|
46876
|
-
const payload = decodeJsonSegment(rawPayload);
|
|
46877
|
-
if (!header4 || !payload) return null;
|
|
46878
|
-
if (header4.alg !== "RS256") return null;
|
|
46879
|
-
if (typeof header4.kid !== "string" || header4.kid === "") return null;
|
|
46880
|
-
const signature = decodeBase64Url(rawSignature);
|
|
46881
|
-
if (!signature) return null;
|
|
46882
|
-
const jwk = await resolveKey(header4.kid, config, deps);
|
|
46883
|
-
if (!jwk) return null;
|
|
46884
|
-
let signatureOk;
|
|
46885
|
-
try {
|
|
46886
|
-
const key = createPublicKey({ format: "jwk", key: jwk });
|
|
46887
|
-
signatureOk = cryptoVerify("RSA-SHA256", Buffer.from(`${rawHeader}.${rawPayload}`), key, signature);
|
|
46888
|
-
} catch {
|
|
46889
|
-
return null;
|
|
46890
|
-
}
|
|
46891
|
-
if (!signatureOk) return null;
|
|
46892
|
-
if (!claimsValid(payload, config, deps.now())) return null;
|
|
46893
|
-
if (typeof payload.sub !== "string" || payload.sub === "") {
|
|
46894
|
-
return { sub: "", commonName: payload.common_name };
|
|
46895
|
-
}
|
|
46896
|
-
return {
|
|
46897
|
-
sub: payload.sub,
|
|
46898
|
-
email: typeof payload.email === "string" ? payload.email : void 0
|
|
46899
|
-
};
|
|
46900
|
-
}
|
|
46901
|
-
function normalizeTeamDomain(raw) {
|
|
46902
|
-
return raw.trim().replace(/^https?:\/\//i, "").replace(/\/+$/, "");
|
|
46903
|
-
}
|
|
46904
|
-
function parseAccessConfig(env) {
|
|
46905
|
-
const teamDomain = normalizeTeamDomain(env.ACCESS_TEAM_DOMAIN ?? "");
|
|
46906
|
-
const aud = (env.ACCESS_AUD ?? "").trim();
|
|
46907
|
-
if (teamDomain === "" || aud === "") return null;
|
|
46908
|
-
return { teamDomain, aud };
|
|
46909
|
-
}
|
|
46910
|
-
var CERTS_PATH, DEFAULT_JWKS_TTL_SECONDS, DEFAULT_MIN_REFETCH_INTERVAL_MS, SKEW_SECONDS, httpJwksFetcher, JwksCache;
|
|
46911
|
-
var init_accessAuth = __esm({
|
|
46912
|
-
"../../src/channels/accessAuth.ts"() {
|
|
46913
|
-
"use strict";
|
|
46914
|
-
CERTS_PATH = "/cdn-cgi/access/certs";
|
|
46915
|
-
DEFAULT_JWKS_TTL_SECONDS = 3600;
|
|
46916
|
-
DEFAULT_MIN_REFETCH_INTERVAL_MS = 3e4;
|
|
46917
|
-
SKEW_SECONDS = 60;
|
|
46918
|
-
httpJwksFetcher = async (certsUrl) => {
|
|
46919
|
-
const res = await fetch(certsUrl);
|
|
46920
|
-
if (!res.ok) throw new Error(`JWKS fetch failed: HTTP ${res.status}`);
|
|
46921
|
-
const body = await res.json();
|
|
46922
|
-
const keys = body?.keys;
|
|
46923
|
-
if (!Array.isArray(keys)) throw new Error("JWKS response missing keys[]");
|
|
46924
|
-
return keys;
|
|
46925
|
-
};
|
|
46926
|
-
JwksCache = class {
|
|
46927
|
-
entries = /* @__PURE__ */ new Map();
|
|
46928
|
-
/** Ms of the last SUCCESSFUL JWKS fetch, or null before the first one. Drives
|
|
46929
|
-
* the negative-cache interval in `refresh`; a failed fetch does not set it. */
|
|
46930
|
-
lastFetchAt = null;
|
|
46931
|
-
/** The in-flight refresh shared by concurrent callers (single-flight); null
|
|
46932
|
-
* when no fetch is running. It never rejects — `runFetch` swallows failures to
|
|
46933
|
-
* fail closed — so both the owner and any joiners resolve cleanly. */
|
|
46934
|
-
inFlight = null;
|
|
46935
|
-
/** The fresh JWK for `kid`, or null if absent/expired (expired entries are
|
|
46936
|
-
* evicted lazily). `now` is milliseconds. */
|
|
46937
|
-
get(kid, now) {
|
|
46938
|
-
const hit = this.entries.get(kid);
|
|
46939
|
-
if (!hit) return null;
|
|
46940
|
-
if (hit.expiresAt <= now) {
|
|
46941
|
-
this.entries.delete(kid);
|
|
46942
|
-
return null;
|
|
46943
|
-
}
|
|
46944
|
-
return hit.jwk;
|
|
46945
|
-
}
|
|
46946
|
-
/** Store every kid'd key from a freshly fetched JWKS with the given absolute
|
|
46947
|
-
* expiry (ms). Keyless entries are skipped. */
|
|
46948
|
-
put(keys, expiresAt) {
|
|
46949
|
-
for (const jwk of keys) {
|
|
46950
|
-
if (typeof jwk.kid === "string" && jwk.kid !== "") {
|
|
46951
|
-
this.entries.set(jwk.kid, { jwk, expiresAt });
|
|
46952
|
-
}
|
|
46953
|
-
}
|
|
46954
|
-
}
|
|
46955
|
-
/**
|
|
46956
|
-
* Refresh the JWKS to (try to) learn an unknown kid, with two guards against
|
|
46957
|
-
* the unknown-kid amplification vector:
|
|
46958
|
-
* - single-flight: if a refresh is already running, join it instead of
|
|
46959
|
-
* firing another — N concurrent unknown kids ⇒ at most one `fetch` call.
|
|
46960
|
-
* - negative cache: once a fetch has succeeded, another is suppressed until
|
|
46961
|
-
* `minRefetchIntervalMs` has elapsed, so an unknown kid resolves to null
|
|
46962
|
-
* WITHOUT a network round-trip inside that window. A genuinely rotated kid
|
|
46963
|
-
* is still picked up on the first refresh after the interval.
|
|
46964
|
-
* Fail-closed: a fetch failure leaves the store unchanged and does not advance
|
|
46965
|
-
* the last-fetch time. `now` is read lazily (ms) so the injected clock can move
|
|
46966
|
-
* across the await.
|
|
46967
|
-
*/
|
|
46968
|
-
async refresh(fetch2, now, ttlMs, minRefetchIntervalMs) {
|
|
46969
|
-
if (this.inFlight) {
|
|
46970
|
-
await this.inFlight;
|
|
46971
|
-
return;
|
|
46972
|
-
}
|
|
46973
|
-
if (this.lastFetchAt !== null && now() - this.lastFetchAt < minRefetchIntervalMs) {
|
|
46974
|
-
return;
|
|
46975
|
-
}
|
|
46976
|
-
this.inFlight = this.runFetch(fetch2, now, ttlMs);
|
|
46977
|
-
try {
|
|
46978
|
-
await this.inFlight;
|
|
46979
|
-
} finally {
|
|
46980
|
-
this.inFlight = null;
|
|
46981
|
-
}
|
|
46982
|
-
}
|
|
46983
|
-
/** Perform one JWKS fetch and fold the result into the store. Never rejects:
|
|
46984
|
-
* on any failure the store is left unchanged (fail closed) and `lastFetchAt`
|
|
46985
|
-
* is not advanced, so `resolveKey` returns null and a later verify may retry. */
|
|
46986
|
-
async runFetch(fetch2, now, ttlMs) {
|
|
46987
|
-
try {
|
|
46988
|
-
const keys = await fetch2();
|
|
46989
|
-
const fetchedAt = now();
|
|
46990
|
-
this.put(keys, fetchedAt + ttlMs);
|
|
46991
|
-
this.lastFetchAt = fetchedAt;
|
|
46992
|
-
} catch {
|
|
46993
|
-
}
|
|
46994
|
-
}
|
|
46995
|
-
};
|
|
46996
|
-
}
|
|
46997
|
-
});
|
|
46998
|
-
|
|
46999
49921
|
// ../../src/channels/dashboardAuth.ts
|
|
47000
49922
|
import { createHash as createHash5, timingSafeEqual as timingSafeEqual4 } from "node:crypto";
|
|
47001
|
-
function
|
|
49923
|
+
function header3(headers, name) {
|
|
47002
49924
|
const raw = headers[name];
|
|
47003
49925
|
return Array.isArray(raw) ? raw[0] : raw;
|
|
47004
49926
|
}
|
|
@@ -47007,7 +49929,7 @@ function accessVerifier(config, deps) {
|
|
|
47007
49929
|
mode: "access",
|
|
47008
49930
|
describe: () => `access (Cloudflare Access SSO, ${config.teamDomain})`,
|
|
47009
49931
|
async verify(req) {
|
|
47010
|
-
const token2 =
|
|
49932
|
+
const token2 = header3(req.headers, "cf-access-jwt-assertion");
|
|
47011
49933
|
if (!token2) return FORBIDDEN;
|
|
47012
49934
|
const identity2 = await verifyAccessJwt(token2, config, deps);
|
|
47013
49935
|
return identity2 ? { ok: true, identity: identity2 } : FORBIDDEN;
|
|
@@ -47024,7 +49946,7 @@ function tokenVerifier(opts) {
|
|
|
47024
49946
|
mode: "token",
|
|
47025
49947
|
describe: () => `token (bearer from $${opts.env} \u2192 access:${opts.subject})`,
|
|
47026
49948
|
async verify(req) {
|
|
47027
|
-
const auth =
|
|
49949
|
+
const auth = header3(req.headers, "authorization");
|
|
47028
49950
|
if (!auth) return FORBIDDEN;
|
|
47029
49951
|
const m = /^Bearer\s+(\S+)\s*$/i.exec(auth);
|
|
47030
49952
|
if (!m) return FORBIDDEN;
|
|
@@ -47205,7 +50127,7 @@ async function start2(svc, nonce, identity2, res) {
|
|
|
47205
50127
|
);
|
|
47206
50128
|
return page(res, statusFor(result.refusal.kind), "Connect link unavailable", `<p class="err">${esc(message4)}</p>`);
|
|
47207
50129
|
}
|
|
47208
|
-
const host =
|
|
50130
|
+
const host = hostOf2(result.redirectUrl);
|
|
47209
50131
|
page(
|
|
47210
50132
|
res,
|
|
47211
50133
|
200,
|
|
@@ -47316,12 +50238,12 @@ function readForm(req) {
|
|
|
47316
50238
|
}
|
|
47317
50239
|
function entryForm(server, nonce) {
|
|
47318
50240
|
if (server.auth === "oauth") {
|
|
47319
|
-
const host =
|
|
50241
|
+
const host = hostOf2(server.url);
|
|
47320
50242
|
return `<p>Connect <strong>${esc(server.name)}</strong> (<code>${esc(server.url)}</code>) by signing in with ${esc(host)}. You will be sent to its sign-in page and back here; the resulting credential is encrypted and stored for your runs only \u2014 it never appears in Slack, logs, or run records.</p><form method="post" action="${MCP_CONNECT_PREFIX}${esc(nonce)}"><input type="hidden" name="action" value="start"><button type="submit">Continue to ${esc(host)}</button></form>`;
|
|
47321
50243
|
}
|
|
47322
50244
|
return `<p>Paste the API token for <strong>${esc(server.name)}</strong> (<code>${esc(server.url)}</code>). It is encrypted and stored for your runs only; it never appears in Slack, logs, or run records.</p><form method="post" action="${MCP_CONNECT_PREFIX}${esc(nonce)}"><label for="token">Token</label><input id="token" name="token" type="password" autocomplete="off" required maxlength="8192"><button type="submit">Connect</button></form>`;
|
|
47323
50245
|
}
|
|
47324
|
-
function
|
|
50246
|
+
function hostOf2(url) {
|
|
47325
50247
|
try {
|
|
47326
50248
|
return new URL(url).host;
|
|
47327
50249
|
} catch {
|
|
@@ -48078,8 +51000,8 @@ async function generatedUnitOf(instance, unit, readers) {
|
|
|
48078
51000
|
const url = resume.url ?? `https://github.com/${instance.repo}/pull/${resume.pr}`;
|
|
48079
51001
|
return generatedUnit(unit.unit, `Resume the review loop of ${url}`);
|
|
48080
51002
|
}
|
|
48081
|
-
const
|
|
48082
|
-
const task =
|
|
51003
|
+
const request2 = await readers.readShipRequest();
|
|
51004
|
+
const task = request2 !== void 0 ? shipTaskText(parseDirectives(request2).text, instance.repo) : "";
|
|
48083
51005
|
return generatedUnit(unit.unit, task || "Implement the task this thread's ship request describes.");
|
|
48084
51006
|
}
|
|
48085
51007
|
async function contractFor(instance, unit, readers) {
|
|
@@ -49033,20 +51955,24 @@ async function merge(body, deps, subject2) {
|
|
|
49033
51955
|
at
|
|
49034
51956
|
});
|
|
49035
51957
|
if (checks.failed.length > 0) return refused6(`CI is red at \`${headSha.slice(0, 7)}\`: ${checks.failed.join(", ")}`);
|
|
49036
|
-
if (checks.total === 0)
|
|
51958
|
+
if (checks.total === 0) {
|
|
51959
|
+
deps.noteMergeWait?.(headSha, id.value, at);
|
|
49037
51960
|
return json2(200, {
|
|
49038
51961
|
ok: true,
|
|
49039
51962
|
outcome: "pending",
|
|
49040
51963
|
reason: `no check has reported at \`${headSha.slice(0, 7)}\` yet`,
|
|
49041
51964
|
at
|
|
49042
51965
|
});
|
|
49043
|
-
|
|
51966
|
+
}
|
|
51967
|
+
if (checks.pending.length > 0) {
|
|
51968
|
+
deps.noteMergeWait?.(headSha, id.value, at);
|
|
49044
51969
|
return json2(200, {
|
|
49045
51970
|
ok: true,
|
|
49046
51971
|
outcome: "pending",
|
|
49047
51972
|
reason: `${checks.pending.length} check(s) still running at \`${headSha.slice(0, 7)}\`: ${checks.pending.join(", ")}`,
|
|
49048
51973
|
at
|
|
49049
51974
|
});
|
|
51975
|
+
}
|
|
49050
51976
|
let merged;
|
|
49051
51977
|
try {
|
|
49052
51978
|
merged = await deps.mergePullRequest(pr, {
|
|
@@ -49309,6 +52235,109 @@ var init_adminCoordinator = __esm({
|
|
|
49309
52235
|
}
|
|
49310
52236
|
});
|
|
49311
52237
|
|
|
52238
|
+
// ../../src/core/coordinator/checksIntake.ts
|
|
52239
|
+
async function verifyWebhookSignature(secret2, rawBody, signature) {
|
|
52240
|
+
const key = await crypto.subtle.importKey("raw", encoder2.encode(secret2), { name: "HMAC", hash: "SHA-256" }, false, [
|
|
52241
|
+
"sign"
|
|
52242
|
+
]);
|
|
52243
|
+
const digest = new Uint8Array(await crypto.subtle.sign("HMAC", key, encoder2.encode(rawBody)));
|
|
52244
|
+
const expected = `sha256=${toHex(digest)}`;
|
|
52245
|
+
if (signature.length !== expected.length) return false;
|
|
52246
|
+
let diff = 0;
|
|
52247
|
+
for (let i = 0; i < expected.length; i++) diff |= expected.charCodeAt(i) ^ signature.charCodeAt(i);
|
|
52248
|
+
return diff === 0;
|
|
52249
|
+
}
|
|
52250
|
+
function createMergeWaitRegistry(ttlMs = MERGE_WAIT_TTL_MS) {
|
|
52251
|
+
const byHead = /* @__PURE__ */ new Map();
|
|
52252
|
+
return {
|
|
52253
|
+
note(headSha, instanceId, now) {
|
|
52254
|
+
const entry = byHead.get(headSha) ?? /* @__PURE__ */ new Map();
|
|
52255
|
+
entry.set(instanceId, now);
|
|
52256
|
+
byHead.set(headSha, entry);
|
|
52257
|
+
},
|
|
52258
|
+
waitingAt(headSha, now) {
|
|
52259
|
+
const entry = byHead.get(headSha);
|
|
52260
|
+
if (!entry) return [];
|
|
52261
|
+
const alive = [];
|
|
52262
|
+
for (const [id, at] of entry) {
|
|
52263
|
+
if (now - at > ttlMs) entry.delete(id);
|
|
52264
|
+
else alive.push(id);
|
|
52265
|
+
}
|
|
52266
|
+
if (entry.size === 0) byHead.delete(headSha);
|
|
52267
|
+
return alive;
|
|
52268
|
+
}
|
|
52269
|
+
};
|
|
52270
|
+
}
|
|
52271
|
+
async function handleCheckRunIntake(headers, rawBody, deps) {
|
|
52272
|
+
if (deps.secret === void 0 || deps.secret === "") return { status: 503, body: { error: "disabled" } };
|
|
52273
|
+
if (headers.signature === void 0 || !await verifyWebhookSignature(deps.secret, rawBody, headers.signature))
|
|
52274
|
+
return { status: 401, body: { error: "unauthorized" } };
|
|
52275
|
+
if (headers.event !== "check_run") return { status: 200, body: { ok: true, ignored: "event" } };
|
|
52276
|
+
let payload;
|
|
52277
|
+
try {
|
|
52278
|
+
payload = JSON.parse(rawBody);
|
|
52279
|
+
} catch {
|
|
52280
|
+
return { status: 400, body: { error: "invalid_json" } };
|
|
52281
|
+
}
|
|
52282
|
+
if (typeof payload !== "object" || payload === null) return { status: 400, body: { error: "invalid_body" } };
|
|
52283
|
+
const p = payload;
|
|
52284
|
+
if (p.action !== "completed") return { status: 200, body: { ok: true, ignored: "action" } };
|
|
52285
|
+
const headSha = p.check_run?.head_sha;
|
|
52286
|
+
const repo = p.repository?.full_name;
|
|
52287
|
+
if (typeof headSha !== "string" || headSha === "" || typeof repo !== "string" || repo === "")
|
|
52288
|
+
return { status: 400, body: { error: "invalid_body" } };
|
|
52289
|
+
if (!await deps.checksSettled(repo, headSha)) return { status: 200, body: { ok: true, settled: false } };
|
|
52290
|
+
const instances = await deps.instancesWaitingAt(headSha);
|
|
52291
|
+
const sends = [];
|
|
52292
|
+
for (const instance of instances) sends.push(await sendChecksSettled(deps.workflow, instance, headSha, deps.now()));
|
|
52293
|
+
return {
|
|
52294
|
+
status: 200,
|
|
52295
|
+
body: { ok: true, settled: true, sent: sends.filter((s) => s.kind === "sent").length, of: instances.length }
|
|
52296
|
+
};
|
|
52297
|
+
}
|
|
52298
|
+
var GITHUB_SIGNATURE_HEADER, GITHUB_EVENT_HEADER, encoder2, toHex, MERGE_WAIT_TTL_MS;
|
|
52299
|
+
var init_checksIntake = __esm({
|
|
52300
|
+
"../../src/core/coordinator/checksIntake.ts"() {
|
|
52301
|
+
"use strict";
|
|
52302
|
+
init_contract2();
|
|
52303
|
+
GITHUB_SIGNATURE_HEADER = "x-hub-signature-256";
|
|
52304
|
+
GITHUB_EVENT_HEADER = "x-github-event";
|
|
52305
|
+
encoder2 = new TextEncoder();
|
|
52306
|
+
toHex = (bytes) => [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
52307
|
+
MERGE_WAIT_TTL_MS = 60 * 6e4;
|
|
52308
|
+
}
|
|
52309
|
+
});
|
|
52310
|
+
|
|
52311
|
+
// ../../src/channels/githubWebhook.ts
|
|
52312
|
+
function createGithubWebhookHandler(deps) {
|
|
52313
|
+
return async (req, res) => {
|
|
52314
|
+
const answer = (status3, body2) => {
|
|
52315
|
+
res.writeHead(status3, { "content-type": "application/json" });
|
|
52316
|
+
res.end(JSON.stringify(body2));
|
|
52317
|
+
};
|
|
52318
|
+
if (req.method !== "POST") return answer(405, { error: "method_not_allowed" });
|
|
52319
|
+
const body = await readBody(req, MAX_BODY_BYTES4);
|
|
52320
|
+
if (!body.ok) return answer(413, { error: "too_large" });
|
|
52321
|
+
const result = await handleCheckRunIntake(
|
|
52322
|
+
{ event: one(req.headers[GITHUB_EVENT_HEADER]), signature: one(req.headers[GITHUB_SIGNATURE_HEADER]) },
|
|
52323
|
+
body.body,
|
|
52324
|
+
deps
|
|
52325
|
+
);
|
|
52326
|
+
answer(result.status, result.body);
|
|
52327
|
+
};
|
|
52328
|
+
}
|
|
52329
|
+
var GITHUB_WEBHOOK_PATH, MAX_BODY_BYTES4, one;
|
|
52330
|
+
var init_githubWebhook = __esm({
|
|
52331
|
+
"../../src/channels/githubWebhook.ts"() {
|
|
52332
|
+
"use strict";
|
|
52333
|
+
init_checksIntake();
|
|
52334
|
+
init_http();
|
|
52335
|
+
GITHUB_WEBHOOK_PATH = "/webhooks/github";
|
|
52336
|
+
MAX_BODY_BYTES4 = 1024 * 1024;
|
|
52337
|
+
one = (v) => Array.isArray(v) ? v[0] : v;
|
|
52338
|
+
}
|
|
52339
|
+
});
|
|
52340
|
+
|
|
49312
52341
|
// ../../src/core/scheduleStore.ts
|
|
49313
52342
|
function buildScheduleStore(cfg, secrets, warn) {
|
|
49314
52343
|
const worker = cfg?.worker;
|
|
@@ -49375,175 +52404,6 @@ var init_scheduleStore = __esm({
|
|
|
49375
52404
|
}
|
|
49376
52405
|
});
|
|
49377
52406
|
|
|
49378
|
-
// ../../src/channels/commandHttp.ts
|
|
49379
|
-
function normalizePath2(pathname) {
|
|
49380
|
-
let decoded = pathname;
|
|
49381
|
-
try {
|
|
49382
|
-
decoded = decodeURIComponent(pathname);
|
|
49383
|
-
} catch {
|
|
49384
|
-
}
|
|
49385
|
-
return decoded.replace(/\/{2,}/g, "/");
|
|
49386
|
-
}
|
|
49387
|
-
function isCommandPath(pathname) {
|
|
49388
|
-
const p = normalizePath2(pathname).toLowerCase();
|
|
49389
|
-
return p === API_ROOT || p === `${API_ROOT}/` || p.startsWith(`${API_ROOT}/`);
|
|
49390
|
-
}
|
|
49391
|
-
function commandIdFromPath(pathname) {
|
|
49392
|
-
const p = normalizePath2(pathname);
|
|
49393
|
-
if (!p.startsWith(`${API_ROOT}/`)) return void 0;
|
|
49394
|
-
const rest = p.slice(API_ROOT.length + 1);
|
|
49395
|
-
return COMMAND_ID.test(rest) ? rest : void 0;
|
|
49396
|
-
}
|
|
49397
|
-
function callerIdFor(identity2) {
|
|
49398
|
-
return isServiceToken(identity2) ? `access:svc:${identity2.commonName}` : `access:${identity2.sub}`;
|
|
49399
|
-
}
|
|
49400
|
-
function accessActor(identity2, grantsFor) {
|
|
49401
|
-
return isServiceToken(identity2) ? resolveActor({ surface: "access-service", subjectId: identity2.commonName }, grantsFor) : resolveActor({ surface: "access-browser", subjectId: identity2.sub }, grantsFor);
|
|
49402
|
-
}
|
|
49403
|
-
function serviceTokenAllowed(pathname, identity2) {
|
|
49404
|
-
return !isServiceToken(identity2) || isCommandPath(pathname);
|
|
49405
|
-
}
|
|
49406
|
-
async function resolveAccessActor(identity2, opts) {
|
|
49407
|
-
const actor = accessActor(identity2, opts.grantsFor);
|
|
49408
|
-
if (isServiceToken(identity2) || !identity2.email || !opts.personByEmail) return actor;
|
|
49409
|
-
const person = await opts.personByEmail(identity2.email).catch(() => void 0);
|
|
49410
|
-
if (!person || !person.id.startsWith("slack:")) return actor;
|
|
49411
|
-
return {
|
|
49412
|
-
...actor,
|
|
49413
|
-
self: [actor.id, person.id],
|
|
49414
|
-
asUser: { id: person.id, ...person.name ? { name: person.name } : {} }
|
|
49415
|
-
};
|
|
49416
|
-
}
|
|
49417
|
-
async function callerFor(identity2, opts) {
|
|
49418
|
-
return {
|
|
49419
|
-
kind: "access",
|
|
49420
|
-
id: callerIdFor(identity2),
|
|
49421
|
-
actor: await resolveAccessActor(identity2, opts),
|
|
49422
|
-
...identity2.email && !isServiceToken(identity2) ? { email: identity2.email } : {}
|
|
49423
|
-
};
|
|
49424
|
-
}
|
|
49425
|
-
function hostOf2(url) {
|
|
49426
|
-
if (!url) return void 0;
|
|
49427
|
-
try {
|
|
49428
|
-
return new URL(url).host.toLowerCase();
|
|
49429
|
-
} catch {
|
|
49430
|
-
return void 0;
|
|
49431
|
-
}
|
|
49432
|
-
}
|
|
49433
|
-
function header3(req, name) {
|
|
49434
|
-
const v = req.headers[name];
|
|
49435
|
-
return Array.isArray(v) ? v[0] : v;
|
|
49436
|
-
}
|
|
49437
|
-
function originAllowed(req, opts) {
|
|
49438
|
-
const site = header3(req, "sec-fetch-site");
|
|
49439
|
-
if (site !== void 0 && site !== "same-origin" && site !== "none") return false;
|
|
49440
|
-
const origin = header3(req, "origin");
|
|
49441
|
-
if (origin === void 0) return true;
|
|
49442
|
-
const expected = originOf(opts.publicBaseUrl);
|
|
49443
|
-
if (expected !== void 0) return originOf(origin) === expected;
|
|
49444
|
-
const host = header3(req, "host")?.toLowerCase();
|
|
49445
|
-
const actual = hostOf2(origin);
|
|
49446
|
-
return host !== void 0 && actual !== void 0 && actual === host;
|
|
49447
|
-
}
|
|
49448
|
-
function originOf(url) {
|
|
49449
|
-
if (!url) return void 0;
|
|
49450
|
-
try {
|
|
49451
|
-
const o = new URL(url).origin;
|
|
49452
|
-
return o === "null" ? void 0 : o.toLowerCase();
|
|
49453
|
-
} catch {
|
|
49454
|
-
return void 0;
|
|
49455
|
-
}
|
|
49456
|
-
}
|
|
49457
|
-
function isJsonContentType(req) {
|
|
49458
|
-
const ct = header3(req, "content-type");
|
|
49459
|
-
return ct !== void 0 && ct.split(";")[0].trim().toLowerCase() === "application/json";
|
|
49460
|
-
}
|
|
49461
|
-
function createCommandHttpHandler(commands, opts) {
|
|
49462
|
-
const maxBytes = opts.maxBodyBytes ?? MAX_BODY_BYTES2;
|
|
49463
|
-
const send = (res, status3, body, extra = {}) => {
|
|
49464
|
-
res.writeHead(status3, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", ...extra });
|
|
49465
|
-
res.end(JSON.stringify(body));
|
|
49466
|
-
};
|
|
49467
|
-
const refuse3 = (res, status3, code, error, extra = {}) => send(res, status3, { error, code }, extra);
|
|
49468
|
-
return async (req, res, identity2) => {
|
|
49469
|
-
const url = new URL(req.url ?? "/", "http://placeholder.invalid");
|
|
49470
|
-
const id = commandIdFromPath(url.pathname);
|
|
49471
|
-
const cmd = id === void 0 ? void 0 : commands.get(id);
|
|
49472
|
-
if (!cmd || !CommandRegistry.exposedTo(cmd, "access")) {
|
|
49473
|
-
refuse3(res, 404, "not_found", "unknown command");
|
|
49474
|
-
return;
|
|
49475
|
-
}
|
|
49476
|
-
const method = (req.method ?? "GET").toUpperCase();
|
|
49477
|
-
const allow = cmd.effect === "write" ? "POST" : "GET, POST";
|
|
49478
|
-
if (method !== "POST" && (cmd.effect === "write" || method !== "GET")) {
|
|
49479
|
-
refuse3(res, 405, "method_not_allowed", `${cmd.id} accepts ${allow}`, { allow });
|
|
49480
|
-
return;
|
|
49481
|
-
}
|
|
49482
|
-
if (method === "POST" && !isJsonContentType(req)) {
|
|
49483
|
-
refuse3(res, 415, "unsupported_media_type", "content-type must be application/json");
|
|
49484
|
-
return;
|
|
49485
|
-
}
|
|
49486
|
-
if (cmd.effect === "write" && !originAllowed(req, opts)) {
|
|
49487
|
-
refuse3(res, 403, "forbidden_origin", "cross-origin writes are refused");
|
|
49488
|
-
return;
|
|
49489
|
-
}
|
|
49490
|
-
const caller = await callerFor(identity2, opts);
|
|
49491
|
-
if (CommandRegistry.refuses(cmd, caller)) {
|
|
49492
|
-
refuse3(res, ERROR_STATUS.unauthorized, "unauthorized", `${caller.id} is not allowed to run ${cmd.id}`);
|
|
49493
|
-
return;
|
|
49494
|
-
}
|
|
49495
|
-
let named;
|
|
49496
|
-
let keys;
|
|
49497
|
-
if (method === "GET") {
|
|
49498
|
-
named = Object.fromEntries(url.searchParams);
|
|
49499
|
-
keys = "kebab";
|
|
49500
|
-
} else {
|
|
49501
|
-
const read = await readBody(req, maxBytes);
|
|
49502
|
-
if (!read.ok) {
|
|
49503
|
-
refuse3(res, 413, "payload_too_large", "request body too large");
|
|
49504
|
-
req.destroy();
|
|
49505
|
-
return;
|
|
49506
|
-
}
|
|
49507
|
-
let body;
|
|
49508
|
-
try {
|
|
49509
|
-
body = read.body.trim() === "" ? {} : JSON.parse(read.body);
|
|
49510
|
-
} catch {
|
|
49511
|
-
refuse3(res, ERROR_STATUS.invalid_input, "invalid_input", "body must be a JSON object");
|
|
49512
|
-
return;
|
|
49513
|
-
}
|
|
49514
|
-
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
49515
|
-
refuse3(res, ERROR_STATUS.invalid_input, "invalid_input", "body must be a JSON object");
|
|
49516
|
-
return;
|
|
49517
|
-
}
|
|
49518
|
-
named = body;
|
|
49519
|
-
keys = "camel";
|
|
49520
|
-
}
|
|
49521
|
-
const input = namedToInput(cmd, named, keys);
|
|
49522
|
-
if ("error" in input) {
|
|
49523
|
-
refuse3(res, ERROR_STATUS.invalid_input, "invalid_input", input.error);
|
|
49524
|
-
return;
|
|
49525
|
-
}
|
|
49526
|
-
const result = await commands.invoke(cmd.id, input, caller);
|
|
49527
|
-
if (!result.ok) {
|
|
49528
|
-
refuse3(res, result.status, result.error, result.message);
|
|
49529
|
-
return;
|
|
49530
|
-
}
|
|
49531
|
-
send(res, 200, result.value);
|
|
49532
|
-
};
|
|
49533
|
-
}
|
|
49534
|
-
var API_ROOT;
|
|
49535
|
-
var init_commandHttp = __esm({
|
|
49536
|
-
"../../src/channels/commandHttp.ts"() {
|
|
49537
|
-
"use strict";
|
|
49538
|
-
init_actor();
|
|
49539
|
-
init_commandRegistry();
|
|
49540
|
-
init_commandSurface();
|
|
49541
|
-
init_accessAuth();
|
|
49542
|
-
init_http();
|
|
49543
|
-
API_ROOT = "/api";
|
|
49544
|
-
}
|
|
49545
|
-
});
|
|
49546
|
-
|
|
49547
52407
|
// ../../src/index.ts
|
|
49548
52408
|
var index_exports = {};
|
|
49549
52409
|
__export(index_exports, {
|
|
@@ -49683,13 +52543,19 @@ async function runBot() {
|
|
|
49683
52543
|
const residentFleet = fleetWatcher ?? NO_FLEET;
|
|
49684
52544
|
const runBearers = new RunBearerStore({ clock: systemClock });
|
|
49685
52545
|
const harnesses = new HarnessRegistry();
|
|
52546
|
+
const roster = {
|
|
52547
|
+
pi: new PiHarness(config.config.pi?.compaction ? { compaction: config.config.pi.compaction } : {}),
|
|
52548
|
+
opencode: new OpenCodeHarness(
|
|
52549
|
+
config.config.opencode?.compaction ? { compaction: config.config.opencode.compaction } : {}
|
|
52550
|
+
)
|
|
52551
|
+
};
|
|
49686
52552
|
const deps = {
|
|
49687
52553
|
config,
|
|
49688
52554
|
completions,
|
|
49689
52555
|
spanLog,
|
|
49690
52556
|
runBearers,
|
|
49691
52557
|
harness: {
|
|
49692
|
-
|
|
52558
|
+
harnesses: roster,
|
|
49693
52559
|
registry: harnesses,
|
|
49694
52560
|
...process.env.PUBLIC_BASE_URL ? { harnessUrl: process.env.PUBLIC_BASE_URL } : {},
|
|
49695
52561
|
...process.env.PORT ? { loopbackUrl: `http://127.0.0.1:${process.env.PORT}` } : {}
|
|
@@ -49806,6 +52672,21 @@ async function runBot() {
|
|
|
49806
52672
|
clock: systemClock
|
|
49807
52673
|
});
|
|
49808
52674
|
const harnessRoutes = createHarnessRoutesHandler({ bearers: runBearers, harnesses, takeover });
|
|
52675
|
+
const mergeWaits = createMergeWaitRegistry();
|
|
52676
|
+
const checksWorkflow = shimWorkflowSender({
|
|
52677
|
+
baseUrl: process.env.PUBLIC_BASE_URL,
|
|
52678
|
+
tokens: processSecrets.get("SWITCHBOARD_INGRESS_TOKENS")
|
|
52679
|
+
});
|
|
52680
|
+
const githubWebhook = createGithubWebhookHandler({
|
|
52681
|
+
secret: processSecrets.get("GITHUB_WEBHOOK_SECRET")?.reveal(),
|
|
52682
|
+
checksSettled: async (repo, headSha) => {
|
|
52683
|
+
const checks = await fetchCommitChecks(repo, headSha);
|
|
52684
|
+
return checks !== void 0 && checks.total > 0 && checks.pending.length === 0;
|
|
52685
|
+
},
|
|
52686
|
+
instancesWaitingAt: (headSha) => Promise.resolve(mergeWaits.waitingAt(headSha, systemClock())),
|
|
52687
|
+
workflow: checksWorkflow,
|
|
52688
|
+
now: systemClock
|
|
52689
|
+
});
|
|
49809
52690
|
const coordinatorAdmin = createAdminCoordinatorHandler({
|
|
49810
52691
|
tokens: processSecrets.get("SWITCHBOARD_INGRESS_TOKENS"),
|
|
49811
52692
|
grantsFor: (id) => config.grantsFor(id),
|
|
@@ -49830,7 +52711,8 @@ async function runBot() {
|
|
|
49830
52711
|
mergePullRequest,
|
|
49831
52712
|
selfIdentity: resolveGithubIdentity,
|
|
49832
52713
|
runHistoryWriter,
|
|
49833
|
-
channelVisibilityOf: (channelId) => channelVisibilityOf(deps, channelId)
|
|
52714
|
+
channelVisibilityOf: (channelId) => channelVisibilityOf(deps, channelId),
|
|
52715
|
+
noteMergeWait: (headSha, instanceId, at) => mergeWaits.note(headSha, instanceId, at)
|
|
49834
52716
|
});
|
|
49835
52717
|
const cronArmed = Object.values(auth.tokens).some((id) => id.subject === "cron");
|
|
49836
52718
|
const schedulesState = `${SCHEDULES.length} schedule(s) on /runs (${capabilities.schedules ? `firings from ${config.config.schedules?.worker?.baseUrl}` : "no firing store"}; cron identity ${cronArmed ? "armed" : "NOT in SWITCHBOARD_INGRESS_TOKENS \u2014 scheduled runs fail closed"})`;
|
|
@@ -49861,6 +52743,16 @@ async function runBot() {
|
|
|
49861
52743
|
},
|
|
49862
52744
|
shell
|
|
49863
52745
|
);
|
|
52746
|
+
const webChat = createWebChatHandler({
|
|
52747
|
+
core: deps,
|
|
52748
|
+
service: runsService,
|
|
52749
|
+
registry: defaultRunRegistry,
|
|
52750
|
+
commands,
|
|
52751
|
+
shell,
|
|
52752
|
+
capabilities,
|
|
52753
|
+
retention: capabilities.runHistory && runHistoryCfg ? { retentionDays: retentionPolicyOf(runHistoryCfg).retentionDays } : null,
|
|
52754
|
+
publicBaseUrl: process.env.PUBLIC_BASE_URL
|
|
52755
|
+
});
|
|
49864
52756
|
const deliveryState = !capabilities.github ? "GET /delivery (503 \u2014 no GitHub credential)" : deliveryService.repos().length === 0 ? "GET /delivery (503 \u2014 no delivery.repos)" : `GET /delivery (${deliveryService.repos().join(",")}; snapshot every ${deliveryEveryMinutes} min)`;
|
|
49865
52757
|
const tokenCount = Object.keys(auth.tokens).length;
|
|
49866
52758
|
const liveViewState = process.env.PUBLIC_BASE_URL ? "GET /runs (index) + /runs/:id (live view)" : "GET /runs (index) + live view (no PUBLIC_BASE_URL \u2014 per-run links omitted)";
|
|
@@ -49915,6 +52807,10 @@ async function runBot() {
|
|
|
49915
52807
|
ingress(req, res);
|
|
49916
52808
|
return;
|
|
49917
52809
|
}
|
|
52810
|
+
if (path === GITHUB_WEBHOOK_PATH) {
|
|
52811
|
+
void githubWebhook(req, res);
|
|
52812
|
+
return;
|
|
52813
|
+
}
|
|
49918
52814
|
if (path === "/mcp") {
|
|
49919
52815
|
mcp2(req, res);
|
|
49920
52816
|
return;
|
|
@@ -49963,7 +52859,7 @@ async function runBot() {
|
|
|
49963
52859
|
return;
|
|
49964
52860
|
}
|
|
49965
52861
|
if (webAssets.serve(req, res)) return;
|
|
49966
|
-
if (isCommandPath(path) || isConnectPath(path) || path === "/runs" || path.startsWith("/runs/") || path === "/residents" || path.startsWith("/residents/") || path === "/costs" || path === "/costs.json" || path.startsWith("/costs/") || path === "/delivery" || path === "/delivery.json" || path.startsWith("/delivery/") || path === "/settings" || path.startsWith("/settings/")) {
|
|
52862
|
+
if (isCommandPath(path) || isConnectPath(path) || path === "/runs" || path.startsWith("/runs/") || path === "/residents" || path.startsWith("/residents/") || path === "/costs" || path === "/costs.json" || path.startsWith("/costs/") || path === "/delivery" || path === "/delivery.json" || path.startsWith("/delivery/") || path === "/settings" || path.startsWith("/settings/") || path === "/threads" || path.startsWith("/threads/")) {
|
|
49967
52863
|
dashboardAuth.verify(req).then(async (gate) => {
|
|
49968
52864
|
if (!gate.ok) {
|
|
49969
52865
|
res.writeHead(gate.status, { "content-type": "text/plain; charset=utf-8" });
|
|
@@ -49985,6 +52881,7 @@ async function runBot() {
|
|
|
49985
52881
|
personByEmail
|
|
49986
52882
|
});
|
|
49987
52883
|
if (liveView2(req, res, { actor })) return;
|
|
52884
|
+
if (webChat(req, res, { actor, identity: gate.identity })) return;
|
|
49988
52885
|
if (mcpConnectView(req, res, gate.identity)) return;
|
|
49989
52886
|
if (residentsView(req, res, { actor })) return;
|
|
49990
52887
|
if (costsView(req, res, { identity: gate.identity, actor })) return;
|
|
@@ -50000,7 +52897,7 @@ async function runBot() {
|
|
|
50000
52897
|
return;
|
|
50001
52898
|
}
|
|
50002
52899
|
if (path === "/") {
|
|
50003
|
-
res.writeHead(302, { location: "/
|
|
52900
|
+
res.writeHead(302, { location: "/threads" });
|
|
50004
52901
|
res.end();
|
|
50005
52902
|
return;
|
|
50006
52903
|
}
|
|
@@ -50041,7 +52938,7 @@ async function runBot() {
|
|
|
50041
52938
|
}).listen(Number(process.env.PORT), () => {
|
|
50042
52939
|
httpListeningAt = systemClock();
|
|
50043
52940
|
console.log(
|
|
50044
|
-
`http server on :${process.env.PORT} (health + POST /ingress + POST /mcp + model proxy (POST ${ANTHROPIC_MESSAGES_PATH}, POST ${OPENAI_CHAT_COMPLETIONS_PATH}) + ${liveViewState} + ${schedulesState} + ${residentsState} + ${costsState} + ${deliveryState} + GET /settings + ${commandHttpState} + /docs \u2192 ${PROJECT_DOCS_URL}; ${tokenCount > 0 ? `${tokenCount} ingress token(s)` : "ingress + MCP DISABLED \u2014 no tokens configured"}; ${accessState})`
|
|
52941
|
+
`http server on :${process.env.PORT} (health + POST /ingress + POST /mcp + model proxy (POST ${ANTHROPIC_MESSAGES_PATH}, POST ${OPENAI_CHAT_COMPLETIONS_PATH}) + ${liveViewState} + ${schedulesState} + ${residentsState} + ${costsState} + ${deliveryState} + GET /settings + GET /threads (+ POST /threads/<id>/send) + ${commandHttpState} + /docs \u2192 ${PROJECT_DOCS_URL}; ${tokenCount > 0 ? `${tokenCount} ingress token(s)` : "ingress + MCP DISABLED \u2014 no tokens configured"}; ${accessState})`
|
|
50045
52942
|
);
|
|
50046
52943
|
});
|
|
50047
52944
|
}
|
|
@@ -50212,6 +53109,7 @@ var init_index = __esm({
|
|
|
50212
53109
|
init_packageRoot();
|
|
50213
53110
|
init_webShell();
|
|
50214
53111
|
init_residentsView();
|
|
53112
|
+
init_web2();
|
|
50215
53113
|
init_costsView();
|
|
50216
53114
|
init_deliveryView();
|
|
50217
53115
|
init_settingsView();
|
|
@@ -50259,6 +53157,7 @@ var init_index = __esm({
|
|
|
50259
53157
|
init_modelProxy();
|
|
50260
53158
|
init_runBearers();
|
|
50261
53159
|
init_piHarness();
|
|
53160
|
+
init_harness2();
|
|
50262
53161
|
init_relay();
|
|
50263
53162
|
init_harnessRoutes();
|
|
50264
53163
|
init_adminTraceLog();
|
|
@@ -50273,6 +53172,9 @@ var init_index = __esm({
|
|
|
50273
53172
|
init_docsLink();
|
|
50274
53173
|
init_dispatcher();
|
|
50275
53174
|
init_adminCoordinator();
|
|
53175
|
+
init_githubWebhook();
|
|
53176
|
+
init_checksIntake();
|
|
53177
|
+
init_instancesClient();
|
|
50276
53178
|
init_instanceStore();
|
|
50277
53179
|
init_githubPulls();
|
|
50278
53180
|
init_githubApi();
|
|
@@ -50329,6 +53231,7 @@ init_residentFleet();
|
|
|
50329
53231
|
init_statusCardFrame();
|
|
50330
53232
|
init_piAi();
|
|
50331
53233
|
init_piHarness();
|
|
53234
|
+
init_harness2();
|
|
50332
53235
|
init_relay();
|
|
50333
53236
|
init_takeover();
|
|
50334
53237
|
init_runBearers();
|
|
@@ -50688,12 +53591,18 @@ async function main() {
|
|
|
50688
53591
|
const runBearers = new RunBearerStore({ clock: systemClock });
|
|
50689
53592
|
const harnesses = new HarnessRegistry();
|
|
50690
53593
|
const loopback = await askLoopbackServer({ bearers: runBearers, harnesses, config });
|
|
53594
|
+
const roster = {
|
|
53595
|
+
pi: new PiHarness(config.config.pi?.compaction ? { compaction: config.config.pi.compaction } : {}),
|
|
53596
|
+
opencode: new OpenCodeHarness(
|
|
53597
|
+
config.config.opencode?.compaction ? { compaction: config.config.opencode.compaction } : {}
|
|
53598
|
+
)
|
|
53599
|
+
};
|
|
50691
53600
|
const deps = {
|
|
50692
53601
|
config,
|
|
50693
53602
|
completions,
|
|
50694
53603
|
runBearers,
|
|
50695
53604
|
harness: {
|
|
50696
|
-
|
|
53605
|
+
harnesses: roster,
|
|
50697
53606
|
registry: harnesses,
|
|
50698
53607
|
harnessUrl: loopback.url,
|
|
50699
53608
|
loopbackUrl: loopback.url
|