@desplega.ai/agent-swarm 2.4.0 → 2.4.2
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/README.md +1 -1
- package/dist/{acp-adapter-kedstxx6.js → acp-adapter-dv7hmx4r.js} +9 -9
- package/dist/{actions-vget6r1k.js → actions-51nc1gny.js} +21 -21
- package/dist/{amp-adapter-xeb5mktv.js → amp-adapter-abp5tdx8.js} +4 -4
- package/dist/{app-mqap8t8m.js → app-9h14qvr6.js} +3 -3
- package/dist/{assistant-h0gv97mv.js → assistant-wv1gz3mt.js} +22 -22
- package/dist/{boot-reembed-qr8a894z.js → boot-reembed-6yn53ms9.js} +1 -1
- package/dist/{boot-reembed-rj85jjhw.js → boot-reembed-7fcka4z5.js} +1 -1
- package/dist/{boot-scrub-sweep-eyfvn6v9.js → boot-scrub-sweep-pm1580ax.js} +4 -4
- package/dist/{boot-seeds-xs9a8dx1.js → boot-seeds-g5hfc1x8.js} +4 -4
- package/dist/{claude-adapter-bm23cye7.js → claude-adapter-gf3kxhf0.js} +9 -9
- package/dist/{claude-managed-adapter-1mwrsm9d.js → claude-managed-adapter-h2y889ge.js} +2 -2
- package/dist/{claude-managed-setup-gkfgks4j.js → claude-managed-setup-h00hsgtz.js} +2 -2
- package/dist/{claude-sdk-session-apb4ypz3.js → claude-sdk-session-s2k1qd8y.js} +13 -13
- package/dist/{cli-tj4j2ts4.js → cli-02qm7src.js} +8 -8
- package/dist/{cli-1cevz46c.js → cli-0qvtffe9.js} +6 -6
- package/dist/{cli-avmqhatn.js → cli-10hhg0km.js} +8 -8
- package/dist/{cli-7gh7020n.js → cli-1dmcq2eh.js} +1 -1
- package/dist/{cli-kz116kvw.js → cli-239yjk27.js} +7 -7
- package/dist/{cli-7ayprxp7.js → cli-2dfbv8qr.js} +8 -0
- package/dist/{cli-a49vpbf3.js → cli-5kqnemen.js} +1 -1
- package/dist/{cli-x719j3vs.js → cli-5rk36sjf.js} +442 -205
- package/dist/{cli-fb6t92v8.js → cli-6x9ma5w5.js} +1 -1
- package/dist/{cli-c8fwpgqb.js → cli-7225gwj7.js} +2 -2
- package/dist/{cli-5s5hx57y.js → cli-7awf1fp8.js} +1 -1
- package/dist/{cli-bn26xbcs.js → cli-8nyhf7za.js} +1 -1
- package/dist/{cli-ywyrffff.js → cli-8sxqsbbq.js} +1 -1
- package/dist/{cli-rsd7ga7v.js → cli-8zwe9mpk.js} +11 -11
- package/dist/{cli-y83zk5sb.js → cli-a0kcty51.js} +4 -4
- package/dist/{cli-8b9wvjps.js → cli-a4gvwttf.js} +4 -4
- package/dist/{cli-jhp7w1fy.js → cli-a5ebc95n.js} +2 -2
- package/dist/{cli-mvg2hf8p.js → cli-ahtpy9kf.js} +14 -7
- package/dist/{cli-x1p5dhq2.js → cli-bcj753sq.js} +3 -3
- package/dist/{cli-p22zdvnt.js → cli-bn0rewsy.js} +3 -3
- package/dist/{cli-p1m2b6gc.js → cli-d2mb5s8a.js} +7 -7
- package/dist/{cli-rg7ntypm.js → cli-d3qqdy4c.js} +456 -94
- package/dist/{cli-c5sbmw1v.js → cli-d82btycs.js} +94 -15
- package/dist/{cli-02vhhssk.js → cli-efaqv1c7.js} +5 -5
- package/dist/{cli-vjfpshn3.js → cli-ejs3m1sx.js} +1 -1
- package/dist/{cli-kk2r80n5.js → cli-faypxz7q.js} +4 -4
- package/dist/{cli-a1esmmee.js → cli-fpmsqq59.js} +7 -2
- package/dist/{cli-wrv09v7q.js → cli-ftyypbg5.js} +1 -1
- package/dist/{cli-zvze9rbm.js → cli-fyzjmseb.js} +3 -3
- package/dist/{cli-8rwvksbc.js → cli-g0hgtz4p.js} +5 -5
- package/dist/{cli-nrrg0fvy.js → cli-h38rjv87.js} +133 -12
- package/dist/{cli-tg9ksbn8.js → cli-h93c9vb3.js} +2 -2
- package/dist/{cli-eqgfs202.js → cli-hqhzg1qq.js} +2 -2
- package/dist/{cli-bhwa8rzc.js → cli-hyfw2j1e.js} +13 -12
- package/dist/{cli-3wjs22x3.js → cli-j4rqdf2n.js} +2 -2
- package/dist/{cli-6ftrrwm1.js → cli-n85dqg6d.js} +5 -5
- package/dist/{cli-8cxkw1f5.js → cli-nmqdc2t3.js} +2 -2
- package/dist/{cli-4fhyq711.js → cli-nn47d7e8.js} +13 -13
- package/dist/{cli-x3en6ct3.js → cli-nwyw3zhg.js} +1 -1
- package/dist/{cli-3avfh5az.js → cli-pemywhfq.js} +2 -2
- package/dist/{cli-xfe63zx3.js → cli-pkht5hap.js} +2 -2
- package/dist/{cli-sppc0cmw.js → cli-pnp2j5zg.js} +2 -2
- package/dist/{cli-wk8ccrz6.js → cli-pw7v69vp.js} +2 -2
- package/dist/{cli-tgvpmnwz.js → cli-r99kc9yq.js} +5 -5
- package/dist/{cli-bfc3k1x5.js → cli-rdyw3mct.js} +9 -9
- package/dist/{cli-b80a1100.js → cli-s6vr7r2d.js} +2 -2
- package/dist/{cli-zv4vy2b0.js → cli-s95dknxh.js} +1 -1
- package/dist/{cli-ttaz6c7m.js → cli-saw9a6f6.js} +6 -5
- package/dist/{cli-mz1ezzzh.js → cli-shvfedvm.js} +3 -3
- package/dist/{cli-rdr166ht.js → cli-smvcw9z5.js} +1 -1
- package/dist/{cli-2rwet3r7.js → cli-sv5gb0yb.js} +1 -1
- package/dist/{cli-5ff2sjyt.js → cli-t30yj95q.js} +2 -2
- package/dist/{cli-ptkpaqfy.js → cli-wt645v7q.js} +1 -1
- package/dist/{cli-f3nx1h8z.js → cli-wxbn3yfx.js} +2 -2
- package/dist/{cli-wyrr38xm.js → cli-x5mq1fyq.js} +1 -1
- package/dist/{cli-ccn0eybf.js → cli-xbzvet96.js} +6 -6
- package/dist/{cli-4mkvwgzr.js → cli-xf234ws8.js} +1 -1
- package/dist/{cli-nme7wery.js → cli-y2x52gs2.js} +46 -13
- package/dist/{cli-ffbrkh1r.js → cli-y8n5k515.js} +46 -38
- package/dist/{cli-ecshf504.js → cli-z4wd2ftv.js} +7 -7
- package/dist/{cli-dsgj3e7a.js → cli-zfg9enh9.js} +2 -2
- package/dist/{cli-yxqag76b.js → cli-zw88zjba.js} +1 -1
- package/dist/cli.js +20 -20
- package/dist/{codex-adapter-pp2gq8gr.js → codex-adapter-ttygw382.js} +6 -6
- package/dist/{codex-hook-qwzccd64.js → codex-hook-x7nejyrr.js} +7 -4
- package/dist/{codex-login-35p4sk85.js → codex-login-m819c41r.js} +1 -1
- package/dist/{codex-session-runner-0vzn09t8.js → codex-session-runner-60b58vv3.js} +7 -7
- package/dist/{commands-psqhkttb.js → commands-edmdwa4x.js} +2 -2
- package/dist/{controller-t20vz2k8.js → controller-m263fg43.js} +4 -4
- package/dist/{controllers-connect-gkxmqw9d.js → controllers-connect-5fym8sw1.js} +23 -23
- package/dist/{cursor-adapter-3v542qe5.js → cursor-adapter-cz6nm6mj.js} +3 -3
- package/dist/{db-nc1rd869.js → db-2y0mgstc.js} +3 -3
- package/dist/{db-2xbyenjr.js → db-nn0hcmx5.js} +6 -2
- package/dist/{devin-adapter-7egzmt0k.js → devin-adapter-zgqadvnm.js} +1 -1
- package/dist/{dispatcher-ewceb8ts.js → dispatcher-0a9wwjcx.js} +20 -20
- package/dist/{docker-njavd3fb.js → docker-fkgtr75t.js} +2 -2
- package/dist/{dsh-adapter-75xwh78t.js → dsh-adapter-3n3net33.js} +3 -3
- package/dist/{e2b-5r6a5jeb.js → e2b-7am6tmq6.js} +2 -2
- package/dist/{e2b-pw1nvtmh.js → e2b-thdcxtjd.js} +2 -2
- package/dist/{easter-egg-3d-xsc47jwf.js → easter-egg-3d-5cgynaja.js} +1 -1
- package/dist/{event-bus-101fenkh.js → event-bus-9vpk8gmq.js} +1 -1
- package/dist/{execute-gzmw1mnw.js → execute-t4e6m3rw.js} +1 -1
- package/dist/{external-channel-a5dsmxn4.js → external-channel-zhvt95n3.js} +2 -2
- package/dist/{grok-adapter-vj88xr23.js → grok-adapter-qnr1hp2y.js} +10 -10
- package/dist/handlers-dkcarkd0.js +39 -0
- package/dist/{heartbeat-t2m9r3wa.js → heartbeat-gz97r7ac.js} +23 -23
- package/dist/{hook-xa677f8r.js → hook-1mmdgjqs.js} +16 -9
- package/dist/{hook-8femx1bg.js → hook-x8rthca4.js} +6 -6
- package/dist/{http-xpmmq028.js → http-r8f85xr9.js} +274 -160
- package/dist/index-5jecgp3n.js +41 -0
- package/dist/index-aex0eba9.js +40 -0
- package/dist/{index-syztqdt6.js → index-g6tbb6ve.js} +1 -1
- package/dist/{index-cdzyfrsp.js → index-smxyk04s.js} +1 -1
- package/dist/{index-dg2tmj99.js → index-vzvfqzfj.js} +20 -20
- package/dist/{index-s7ckdajq.js → index-y7rbcrc1.js} +10 -10
- package/dist/{keepalive-3nr4st3r.js → keepalive-64c0f34c.js} +4 -4
- package/dist/lead-b1da0f97.js +78 -0
- package/dist/{maintenance-verwpe2c.js → maintenance-mnxpar12.js} +1 -1
- package/dist/{oauth-refresh-sweep-9q2t0y3q.js → oauth-refresh-sweep-36hs2v4h.js} +3 -3
- package/dist/{onboard-aytqedmq.js → onboard-xes8de61.js} +3 -3
- package/dist/{opencode-adapter-f4hryjny.js → opencode-adapter-3s7ax02e.js} +7 -7
- package/dist/{otel-impl-hckw4wt1.js → otel-impl-fzc57kcq.js} +2 -2
- package/dist/{pi-mono-adapter-hxby0ce1.js → pi-mono-adapter-ytp7gz8b.js} +22 -11
- package/dist/{pricing-refresh-5nvme3sm.js → pricing-refresh-1nek2w7b.js} +3 -3
- package/dist/{rbac-roles-vbpy21mg.js → rbac-roles-sqefb5hw.js} +2 -2
- package/dist/{rbac-roles-pd8c26m1.js → rbac-roles-ttyfaep2.js} +1 -1
- package/dist/{render-v2-7nqgvk2q.js → render-v2-vb527jwn.js} +3 -3
- package/dist/{resume-b6djcj3c.js → resume-jcdzg1bn.js} +10 -8
- package/dist/{runtime-txrxc9za.js → runtime-xe3aqy4x.js} +1 -1
- package/dist/{session-access-fc3f9y12.js → session-access-zsp4wsp0.js} +4 -4
- package/dist/{setup-n39szm6b.js → setup-1nb2zqd3.js} +1 -1
- package/dist/{virtual-modules-wyp2k6z3.js → virtual-modules-4r3zkc20.js} +2 -2
- package/dist/{work-objects-jem2fre0.js → work-objects-61w08fwr.js} +23 -23
- package/dist/worker-c1e68hpa.js +78 -0
- package/dist/{x-8ewt8gxn.js → x-y45mthwn.js} +2 -2
- package/openapi.json +491 -52
- package/package.json +3 -3
- package/src/apps/action-runner.ts +146 -0
- package/src/apps/catalog.generated.json +3 -2
- package/src/apps/definition.ts +152 -4
- package/src/apps/row-store.ts +23 -9
- package/src/apps/schema-migrate.ts +14 -6
- package/src/apps/script-types.ts +2 -1
- package/src/apps/sync.ts +20 -19
- package/src/be/db/tasks/read.ts +112 -0
- package/src/be/db.ts +16 -4
- package/src/be/fleet-benchmark-stats.ts +10 -5
- package/src/be/models-catalog.ts +4 -0
- package/src/be/onboarding.ts +6 -0
- package/src/be/scripts/typecheck.ts +13 -0
- package/src/be/swarm-config-guard.ts +25 -1
- package/src/be/telemetry-enable.ts +1 -1
- package/src/commands/provider-credentials.ts +12 -2
- package/src/commands/runner.ts +3 -0
- package/src/heartbeat/heartbeat.ts +2 -2
- package/src/hooks/codex-hook.ts +6 -2
- package/src/hooks/hook.ts +7 -0
- package/src/hooks/pr-body-guard.ts +38 -11
- package/src/hooks/pr-room-guard.ts +88 -0
- package/src/http/agents.ts +1 -0
- package/src/http/apps.ts +99 -183
- package/src/http/fleet-benchmark.ts +79 -30
- package/src/http/global-config.ts +7 -0
- package/src/http/ranking.ts +24 -40
- package/src/http/status.ts +49 -43
- package/src/http/tasks.ts +49 -1
- package/src/http/telemetry-ui.ts +1 -1
- package/src/http/used-by.ts +55 -19
- package/src/http/users.ts +2 -2
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +15 -0
- package/src/providers/reasoning-effort.ts +2 -1
- package/src/providers/types.ts +2 -0
- package/src/ranking/telemetry-ranking.ts +5 -33
- package/src/scripts-runtime/sdk-allowlist.ts +7 -0
- package/src/scripts-runtime/types/stdlib.d.ts +31 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +31 -0
- package/src/server.ts +4 -0
- package/src/telemetry-context.ts +19 -1
- package/src/telemetry-contract/EVENTS.md +42 -8
- package/src/telemetry-contract/SOURCE.json +6 -5
- package/src/telemetry-contract/catalog.json +225 -5
- package/src/telemetry-contract/ranking/fixtures/valid/no-claim.json +10 -0
- package/src/telemetry-contract/ranking/listing.v1.schema.json +2 -2
- package/src/telemetry-contract/types.gen.ts +225 -27
- package/src/telemetry-identity.ts +47 -9
- package/src/telemetry-snapshot.ts +9 -1
- package/src/telemetry-ui-events.ts +9 -2
- package/src/telemetry.ts +29 -6
- package/src/tests/app-action-run-tool.test.ts +178 -0
- package/src/tests/app-row-tools.test.ts +385 -0
- package/src/tests/apps-json-column.test.ts +611 -0
- package/src/tests/apps-script-types.test.ts +2 -0
- package/src/tests/apps-sync-engine.test.ts +42 -0
- package/src/tests/check-pr-body.test.ts +0 -23
- package/src/tests/credential-check.test.ts +38 -0
- package/src/tests/db-task-read-contract.test.ts +4 -0
- package/src/tests/db-task-write-contract.test.ts +2 -0
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/fleet-benchmark.test.ts +122 -2
- package/src/tests/http-users.test.ts +62 -1
- package/src/tests/model-catalog-table.test.ts +63 -0
- package/src/tests/model-profiles-api.test.ts +19 -7
- package/src/tests/models-catalog.test.ts +50 -0
- package/src/tests/onboarding.test.ts +21 -0
- package/src/tests/org-website.test.ts +62 -0
- package/src/tests/pi-mono-adapter.test.ts +23 -0
- package/src/tests/pr-body-leaks.test.ts +4 -0
- package/src/tests/pr-room-guard.test.ts +242 -0
- package/src/tests/ranking-listing.test.ts +8 -23
- package/src/tests/ranking-route.test.ts +49 -47
- package/src/tests/reasoning-effort.test.ts +39 -3
- package/src/tests/runner-harness-telemetry-env.test.ts +62 -1
- package/src/tests/status.test.ts +158 -9
- package/src/tests/swarm-tool-result-gate.test.ts +2 -4
- package/src/tests/task-lineage.test.ts +180 -0
- package/src/tests/telemetry-context.test.ts +128 -2
- package/src/tests/telemetry-init.test.ts +62 -5
- package/src/tests/telemetry-ui-route.test.ts +123 -0
- package/src/tests/tool-annotations.test.ts +3 -2
- package/src/tests/used-by-route.test.ts +144 -10
- package/src/tests/user-token-rest-auth.test.ts +38 -0
- package/src/tests/workflow-agent-task-retry.test.ts +130 -4
- package/src/tests/workflow-wait-recovery.test.ts +116 -4
- package/src/tools/app-action-run.ts +63 -0
- package/src/tools/app-rows.ts +239 -0
- package/src/tools/model-catalog/index.ts +1 -1
- package/src/tools/tool-config.ts +8 -0
- package/src/tools/utils.ts +1 -1
- package/src/tools/workflows/retry-workflow-run.ts +1 -1
- package/src/types.ts +17 -0
- package/src/utils/company-profile.ts +24 -0
- package/src/utils/org-website.ts +58 -21
- package/src/workflows/recovery.ts +10 -22
- package/src/workflows/resume.ts +16 -0
- package/src/workflows/task-step-routing.ts +47 -7
- package/templates/skills/apps/SKILL.md +44 -4
- package/templates/skills/apps/content.md +44 -4
- package/dist/handlers-zbs90bny.js +0 -39
- package/dist/index-8pwc1jc8.js +0 -40
- package/dist/index-nxa6me0q.js +0 -41
- package/dist/lead-td8h2sfn.js +0 -78
- package/dist/worker-wr6xgfr2.js +0 -78
- package/src/tests/check-ranking-entry.test.ts +0 -146
package/src/be/db/tasks/read.ts
CHANGED
|
@@ -436,6 +436,118 @@ export async function getChildTasks(parentTaskId: string): Promise<AgentTask[]>
|
|
|
436
436
|
return rows.map(rowToAgentTask);
|
|
437
437
|
}
|
|
438
438
|
|
|
439
|
+
/** Caps for {@link getTaskLineage}: a fan-out of thousands still answers in one bounded read. */
|
|
440
|
+
export const TASK_LINEAGE_LIMITS = { maxNodes: 500, maxDepth: 30, maxAncestors: 50 } as const;
|
|
441
|
+
|
|
442
|
+
/** One task in a lineage tree: the slim list row plus the workflow run it belongs to. */
|
|
443
|
+
type TaskLineageNode = AgentTaskSummary & { workflowRunId?: string };
|
|
444
|
+
|
|
445
|
+
interface TaskLineage {
|
|
446
|
+
/** The topmost ancestor reached (the session root unless `truncated`). */
|
|
447
|
+
rootTaskId: string;
|
|
448
|
+
/** Root first, then descendants; always holds the path root → `taskId`. */
|
|
449
|
+
tasks: TaskLineageNode[];
|
|
450
|
+
/** True when some ancestor or descendant is missing from `tasks`. */
|
|
451
|
+
truncated: boolean;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const LINEAGE_COLUMNS = `${LIST_COLUMNS.slim}, agent_tasks.workflowRunId,
|
|
455
|
+
(SELECT SUM(totalCostUsd) FROM session_costs WHERE session_costs.taskId = agent_tasks.id) AS totalCostUsd`;
|
|
456
|
+
|
|
457
|
+
type TaskLineageRow = AgentTaskSummaryRow & { workflowRunId: string | null };
|
|
458
|
+
|
|
459
|
+
function rowToTaskLineageNode(row: TaskLineageRow): TaskLineageNode {
|
|
460
|
+
return { ...rowToAgentTaskSummary(row), workflowRunId: row.workflowRunId ?? undefined };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* The whole tree `taskId` belongs to, following `parentTaskId`: up to the
|
|
465
|
+
* topmost ancestor, then every descendant of that root breadth-first. The
|
|
466
|
+
* descendants of `taskId` itself are walked first, so a wide tree cut at
|
|
467
|
+
* `maxNodes` still shows the task's own subtree. Read-only and bounded: at
|
|
468
|
+
* most `maxAncestors` hops up, `maxDepth` levels down and about `2 * maxNodes`
|
|
469
|
+
* rows. Null when the task does not exist.
|
|
470
|
+
*/
|
|
471
|
+
export async function getTaskLineage(
|
|
472
|
+
taskId: string,
|
|
473
|
+
limits: { maxNodes?: number; maxDepth?: number; maxAncestors?: number } = {},
|
|
474
|
+
): Promise<TaskLineage | null> {
|
|
475
|
+
const maxNodes = limits.maxNodes ?? TASK_LINEAGE_LIMITS.maxNodes;
|
|
476
|
+
const maxDepth = limits.maxDepth ?? TASK_LINEAGE_LIMITS.maxDepth;
|
|
477
|
+
const maxAncestors = limits.maxAncestors ?? TASK_LINEAGE_LIMITS.maxAncestors;
|
|
478
|
+
const db = getDbClient();
|
|
479
|
+
// Hops are counted, so a parentTaskId cycle stops at the cap instead of looping.
|
|
480
|
+
const path = await db.query<{ id: string; parentTaskId: string | null; hops: number }>(
|
|
481
|
+
`WITH RECURSIVE up(id, parentTaskId, hops) AS (
|
|
482
|
+
SELECT id, parentTaskId, 0 FROM agent_tasks WHERE id = ?
|
|
483
|
+
UNION ALL
|
|
484
|
+
SELECT t.id, t.parentTaskId, up.hops + 1 FROM agent_tasks t
|
|
485
|
+
JOIN up ON t.id = up.parentTaskId
|
|
486
|
+
WHERE up.hops < ?
|
|
487
|
+
)
|
|
488
|
+
SELECT id, parentTaskId, hops FROM up ORDER BY hops`,
|
|
489
|
+
[taskId, maxAncestors],
|
|
490
|
+
);
|
|
491
|
+
if (path.length === 0) return null;
|
|
492
|
+
const top = path[path.length - 1]!;
|
|
493
|
+
// A parent that no longer exists ends the walk like a root; a cap does not.
|
|
494
|
+
let truncated = top.hops === maxAncestors && top.parentTaskId !== null;
|
|
495
|
+
|
|
496
|
+
// LIMIT inside the recursive CTE stops the walk itself, not just the output.
|
|
497
|
+
const descendants = (startId: string) =>
|
|
498
|
+
db.query<TaskLineageRow>(
|
|
499
|
+
`WITH RECURSIVE down(id, depth) AS (
|
|
500
|
+
SELECT ?, 0
|
|
501
|
+
UNION ALL
|
|
502
|
+
SELECT t.id, down.depth + 1 FROM down
|
|
503
|
+
JOIN agent_tasks t ON t.parentTaskId = down.id
|
|
504
|
+
WHERE down.depth < ?
|
|
505
|
+
ORDER BY 2
|
|
506
|
+
LIMIT ?
|
|
507
|
+
)
|
|
508
|
+
SELECT ${LINEAGE_COLUMNS} FROM down
|
|
509
|
+
CROSS JOIN agent_tasks ON agent_tasks.id = down.id
|
|
510
|
+
ORDER BY down.depth, agent_tasks.createdAt, agent_tasks.rowid`,
|
|
511
|
+
[startId, maxDepth, maxNodes + 1],
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
const byId = new Map<string, TaskLineageNode>();
|
|
515
|
+
const add = (rows: TaskLineageRow[]) => {
|
|
516
|
+
for (const row of rows) {
|
|
517
|
+
if (!byId.has(row.id)) byId.set(row.id, rowToTaskLineageNode(row));
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
const ownSubtree = await descendants(taskId);
|
|
521
|
+
const fromRoot = top.id === taskId ? ownSubtree : await descendants(top.id);
|
|
522
|
+
add(fromRoot.slice(0, maxNodes));
|
|
523
|
+
add(ownSubtree.slice(0, maxNodes));
|
|
524
|
+
|
|
525
|
+
// The path root → task is always present, even past the depth or node cap.
|
|
526
|
+
const missing = path.map((row) => row.id).filter((id) => !byId.has(id));
|
|
527
|
+
if (missing.length > 0) {
|
|
528
|
+
add(
|
|
529
|
+
await db.query<TaskLineageRow>(
|
|
530
|
+
`SELECT ${LINEAGE_COLUMNS} FROM agent_tasks WHERE id IN (${missing.map(() => "?").join(", ")})`,
|
|
531
|
+
missing,
|
|
532
|
+
),
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// The caps cut the walk at many points (depth, node count, either walk), so
|
|
537
|
+
// ask the table directly: is any child of a returned task not returned?
|
|
538
|
+
if (!truncated) {
|
|
539
|
+
const ids = [...byId.keys()];
|
|
540
|
+
const list = ids.map(() => "?").join(", ");
|
|
541
|
+
const omitted = await db.get(
|
|
542
|
+
`SELECT 1 FROM agent_tasks WHERE parentTaskId IN (${list}) AND id NOT IN (${list}) LIMIT 1`,
|
|
543
|
+
[...ids, ...ids],
|
|
544
|
+
);
|
|
545
|
+
truncated = omitted !== undefined && omitted !== null;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return { rootTaskId: top.id, tasks: [...byId.values()], truncated };
|
|
549
|
+
}
|
|
550
|
+
|
|
439
551
|
/**
|
|
440
552
|
* Returns true if `parentId` has at least one non-terminal child task with
|
|
441
553
|
* `taskType = 'resume'`. Used by the heartbeat sweep as an idempotency guard:
|
package/src/be/db.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { realtimeBus } from "../realtime/bus";
|
|
11
11
|
import { slackChannelFromContextKey } from "../tasks/slack-routing";
|
|
12
12
|
import { _resolveIntegrationType, emitIntegrationConnected, telemetry } from "../telemetry";
|
|
13
|
-
import { scheduleOrgDomainRecompute } from "../telemetry-identity";
|
|
13
|
+
import { invalidateTelemetryUserRole, scheduleOrgDomainRecompute } from "../telemetry-identity";
|
|
14
14
|
import {
|
|
15
15
|
emitTaskTelemetry,
|
|
16
16
|
type TaskTelemetryEvent,
|
|
@@ -327,6 +327,7 @@ export {
|
|
|
327
327
|
getSupersededTasksWithoutResume,
|
|
328
328
|
getSupersededTasksWithUnsettledDependents,
|
|
329
329
|
getTaskById,
|
|
330
|
+
getTaskLineage,
|
|
330
331
|
getTaskStats,
|
|
331
332
|
getTasksByAgentId,
|
|
332
333
|
getTasksByStatus,
|
|
@@ -346,6 +347,7 @@ export {
|
|
|
346
347
|
recordChatRelayError,
|
|
347
348
|
resetTasksNotified,
|
|
348
349
|
SUPERSEDED_RESUME_REPAIR_WINDOW_MS,
|
|
350
|
+
TASK_LINEAGE_LIMITS,
|
|
349
351
|
type TaskFilters,
|
|
350
352
|
} from "./db/tasks/read";
|
|
351
353
|
|
|
@@ -12354,7 +12356,7 @@ export async function updateUser(
|
|
|
12354
12356
|
id: string,
|
|
12355
12357
|
data: Partial<{
|
|
12356
12358
|
name: string;
|
|
12357
|
-
email: string;
|
|
12359
|
+
email: string | null;
|
|
12358
12360
|
role: string;
|
|
12359
12361
|
notes: string;
|
|
12360
12362
|
emailAliases: string[];
|
|
@@ -12419,7 +12421,12 @@ export async function updateUser(
|
|
|
12419
12421
|
`UPDATE users SET ${sets.join(", ")} WHERE id = ? RETURNING *`,
|
|
12420
12422
|
params,
|
|
12421
12423
|
);
|
|
12422
|
-
if (row)
|
|
12424
|
+
if (row) {
|
|
12425
|
+
getDbClient().afterCommit(() => {
|
|
12426
|
+
invalidateTelemetryUserRole(id);
|
|
12427
|
+
scheduleOrgDomainRecompute();
|
|
12428
|
+
});
|
|
12429
|
+
}
|
|
12423
12430
|
return row ? rowToUser(row) : null;
|
|
12424
12431
|
}
|
|
12425
12432
|
|
|
@@ -12556,7 +12563,12 @@ export async function deleteUser(id: string, replacementUserId?: string): Promis
|
|
|
12556
12563
|
await reclassifyTaskHumanFree(reclassifySeedIds);
|
|
12557
12564
|
|
|
12558
12565
|
const result = await tx.run("DELETE FROM users WHERE id = ?", [id]);
|
|
12559
|
-
if (result.changes > 0)
|
|
12566
|
+
if (result.changes > 0) {
|
|
12567
|
+
getDbClient().afterCommit(() => {
|
|
12568
|
+
invalidateTelemetryUserRole(id);
|
|
12569
|
+
scheduleOrgDomainRecompute();
|
|
12570
|
+
});
|
|
12571
|
+
}
|
|
12560
12572
|
return result.changes > 0;
|
|
12561
12573
|
});
|
|
12562
12574
|
}
|
|
@@ -5,15 +5,20 @@
|
|
|
5
5
|
* registered agent count (the same count `org.snapshot` reports as
|
|
6
6
|
* `agents_total`).
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Comparisons require a known ingest start. With telemetry off, callers can
|
|
9
|
+
* use the local 30-day window without an ingest anchor.
|
|
10
10
|
*/
|
|
11
|
-
import { fleetCountStart, type LocalFleetStats } from "../fleet-benchmark";
|
|
11
|
+
import { FLEET_WINDOW_DAYS, fleetCountStart, type LocalFleetStats } from "../fleet-benchmark";
|
|
12
12
|
import { getDbClient } from "./db/runtime";
|
|
13
13
|
import { getTelemetryIngestAnchor } from "./telemetry-ingest-anchor";
|
|
14
14
|
|
|
15
|
-
export async function getLocalFleetStats(
|
|
16
|
-
|
|
15
|
+
export async function getLocalFleetStats(
|
|
16
|
+
now = new Date(),
|
|
17
|
+
{ requireIngestAnchor = true } = {},
|
|
18
|
+
): Promise<LocalFleetStats | null> {
|
|
19
|
+
const start = requireIngestAnchor
|
|
20
|
+
? fleetCountStart(await getTelemetryIngestAnchor(), now)
|
|
21
|
+
: new Date(now.getTime() - FLEET_WINDOW_DAYS * 86_400_000);
|
|
17
22
|
if (!start) return null;
|
|
18
23
|
const since = start.toISOString();
|
|
19
24
|
const client = getDbClient();
|
package/src/be/models-catalog.ts
CHANGED
|
@@ -27,6 +27,10 @@ export const CATALOG_PROVIDER_IDS = [
|
|
|
27
27
|
"openai",
|
|
28
28
|
"amazon-bedrock",
|
|
29
29
|
"opencode",
|
|
30
|
+
// OpenCode Go is opencode's first-party subscription provider, a sibling of
|
|
31
|
+
// its Zen section (`opencode`). models.dev lists it, so projecting it gives
|
|
32
|
+
// `opencode-go/<model>` ids their context window and reasoning facts.
|
|
33
|
+
"opencode-go",
|
|
30
34
|
...openAICompatibleProviderIds(),
|
|
31
35
|
] as const;
|
|
32
36
|
|
package/src/be/onboarding.ts
CHANGED
|
@@ -498,6 +498,12 @@ function deriveState(
|
|
|
498
498
|
if (state.steps.connect.status !== "done") {
|
|
499
499
|
changed = completeStep(state, "connect", "api_key", true, events) || changed;
|
|
500
500
|
}
|
|
501
|
+
// A name saved anywhere (setup, the org-name popup, Settings, the API) names
|
|
502
|
+
// the swarm. The old setup default "Your Swarm" does not count.
|
|
503
|
+
const orgName = process.env.SWARM_ORG_NAME?.trim();
|
|
504
|
+
if (state.steps.name.status !== "done" && orgName && orgName !== "Your Swarm") {
|
|
505
|
+
changed = completeStep(state, "name", "custom_name", true, events) || changed;
|
|
506
|
+
}
|
|
501
507
|
if (
|
|
502
508
|
state.steps.ai.status !== "done" &&
|
|
503
509
|
signals.providers.some((entry) => entry.state === "verified")
|
|
@@ -419,6 +419,19 @@ export interface SwarmSdk {
|
|
|
419
419
|
query: string;
|
|
420
420
|
params?: Record<string, string | number | boolean>;
|
|
421
421
|
}): Promise<unknown>;
|
|
422
|
+
app_rows_list(args: {
|
|
423
|
+
appId: string;
|
|
424
|
+
model: string;
|
|
425
|
+
equals?: Record<string, string | number | boolean>;
|
|
426
|
+
sort?: \`\${string}:asc\` | \`\${string}:desc\`;
|
|
427
|
+
limit?: number;
|
|
428
|
+
}): Promise<unknown>;
|
|
429
|
+
app_row_get(args: { appId: string; model: string; rowId: string }): Promise<unknown>;
|
|
430
|
+
app_row_create(args: { appId: string; model: string; values: Record<string, unknown> }): Promise<unknown>;
|
|
431
|
+
app_rows_create(args: { appId: string; model: string; rows: Record<string, unknown>[] }): Promise<unknown>;
|
|
432
|
+
app_row_patch(args: { appId: string; model: string; rowId: string; values: Record<string, unknown> }): Promise<unknown>;
|
|
433
|
+
app_row_delete(args: { appId: string; model: string; rowId: string }): Promise<unknown>;
|
|
434
|
+
app_rows_delete(args: { appId: string; model: string; equals: Record<string, string | number | boolean>; limit?: number }): Promise<unknown>;
|
|
422
435
|
app_rollback(args: {
|
|
423
436
|
appId: string;
|
|
424
437
|
version: number;
|
|
@@ -4,6 +4,11 @@ import { validateQuickJSPoolSize } from "../scripts-runtime/executors/quickjs-po
|
|
|
4
4
|
import { normalizeSlackReactionShortcode } from "../slack/reaction-shortcode";
|
|
5
5
|
import { ProviderNameSchema } from "../types";
|
|
6
6
|
import { API_DRAIN_MAX_MS_LIMIT } from "../utils/api-drain";
|
|
7
|
+
import {
|
|
8
|
+
COMPANY_ACQUISITION_SOURCES,
|
|
9
|
+
COMPANY_TEAM_SIZES,
|
|
10
|
+
COMPANY_USE_CASES,
|
|
11
|
+
} from "../utils/company-profile";
|
|
7
12
|
import { envExportRestrictedError, isEnvExportRestrictedKey } from "../utils/config-env-export";
|
|
8
13
|
import { validateEgressAllowlist } from "../utils/egress-guard";
|
|
9
14
|
import { parseEnabledTools } from "../utils/enabled-tools";
|
|
@@ -134,6 +139,16 @@ export const DB_RETENTION_TUNING_BOUNDS = {
|
|
|
134
139
|
DB_RETENTION_MAX_STATEMENT_MS: { min: 25, max: 5_000 },
|
|
135
140
|
} as const;
|
|
136
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Bounds for the Swarm Apps json column size cap (`APPS_JSON_COLUMN_MAX_BYTES`).
|
|
144
|
+
*
|
|
145
|
+
* `src/apps/definition.ts` reads the knob against exactly these at call time
|
|
146
|
+
* and falls back to its 256 KB default outside them; VALIDATED_KEYS below
|
|
147
|
+
* rejects out-of-range writes with the same numbers, so the config API cannot
|
|
148
|
+
* accept a cap the row store would silently ignore.
|
|
149
|
+
*/
|
|
150
|
+
export const APPS_JSON_COLUMN_MAX_BYTES_BOUNDS = { min: 1_024, max: 16_777_216 } as const;
|
|
151
|
+
|
|
137
152
|
/** Build `{ KEY: validator }` entries accepting only boolean literals. */
|
|
138
153
|
function booleanValidators(keys: string[]): Record<string, ConfigValidator> {
|
|
139
154
|
const message = (key: string) =>
|
|
@@ -151,7 +166,8 @@ function booleanValidators(keys: string[]): Record<string, ConfigValidator> {
|
|
|
151
166
|
|
|
152
167
|
/** Build a single `{ KEY: validator }` entry restricted to `options`. */
|
|
153
168
|
function enumValidator(key: string, options: string[]): Record<string, ConfigValidator> {
|
|
154
|
-
const
|
|
169
|
+
const listed = options.filter(Boolean).join(", ");
|
|
170
|
+
const message = `Invalid ${key} value (must be one of: ${listed}${options.includes("") ? ", or empty" : ""})`;
|
|
155
171
|
return {
|
|
156
172
|
[key]: (value: unknown) => {
|
|
157
173
|
if (typeof value !== "string") return message;
|
|
@@ -423,6 +439,9 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
|
|
|
423
439
|
? null
|
|
424
440
|
: "Invalid SWARM_ORG_WEBSITE (must be a public hostname such as example.com, without scheme, www, path or port)";
|
|
425
441
|
},
|
|
442
|
+
...enumValidator("SWARM_ORG_TEAM_SIZE", ["", ...COMPANY_TEAM_SIZES]),
|
|
443
|
+
...enumValidator("SWARM_ORG_USE_CASE", ["", ...COMPANY_USE_CASES]),
|
|
444
|
+
...enumValidator("SWARM_ORG_ACQUISITION_SOURCE", ["", ...COMPANY_ACQUISITION_SOURCES]),
|
|
426
445
|
...booleanValidators([
|
|
427
446
|
"SWARM_USED_BY_CONSENT",
|
|
428
447
|
"PUBLIC_RANKING_ENABLED",
|
|
@@ -518,6 +537,11 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
|
|
|
518
537
|
// an operator could save a 500ms tick budget, see it accepted, and have the
|
|
519
538
|
// sweep keep running for the default 30000ms.
|
|
520
539
|
...boundedIntegerValidatorsFor(DB_RETENTION_TUNING_BOUNDS),
|
|
540
|
+
...boundedIntegerValidators(
|
|
541
|
+
["APPS_JSON_COLUMN_MAX_BYTES"],
|
|
542
|
+
APPS_JSON_COLUMN_MAX_BYTES_BOUNDS.min,
|
|
543
|
+
APPS_JSON_COLUMN_MAX_BYTES_BOUNDS.max,
|
|
544
|
+
),
|
|
521
545
|
...boundedIntegerValidators(
|
|
522
546
|
[
|
|
523
547
|
"SESSION_LOG_RETENTION_DAYS",
|
|
@@ -13,7 +13,7 @@ import { telemetryIngestRecorder } from "./telemetry-ingest-anchor";
|
|
|
13
13
|
|
|
14
14
|
type GetConfig = (key: string) => Promise<string | undefined>;
|
|
15
15
|
type SetConfig = (key: string, value: string) => Promise<void>;
|
|
16
|
-
export type TelemetryEnabledVia = "banner" | "settings" | "env";
|
|
16
|
+
export type TelemetryEnabledVia = "banner" | "settings" | "benchmark" | "env";
|
|
17
17
|
|
|
18
18
|
let accessors: { getConfig: GetConfig; setConfig: SetConfig } | null = null;
|
|
19
19
|
let lastEnabled = false;
|
|
@@ -155,8 +155,13 @@ export const CREDENTIAL_PROVIDER_CHECKERS: Record<SupportedProvider, CredentialC
|
|
|
155
155
|
devin: (env) => checkDevinCredentials(env),
|
|
156
156
|
opencode: (env, opts) => checkOpencodeCredentials(env, opts),
|
|
157
157
|
pi: async (env, opts) => {
|
|
158
|
-
const { checkPiMonoCredentials } = await import(
|
|
159
|
-
|
|
158
|
+
const { checkPiMonoCredentials, listPiOpencodeGoModelIds } = await import(
|
|
159
|
+
"../providers/pi-mono-adapter"
|
|
160
|
+
);
|
|
161
|
+
return {
|
|
162
|
+
...(await checkPiMonoCredentials(env, opts)),
|
|
163
|
+
opencodeGoModels: listPiOpencodeGoModelIds(),
|
|
164
|
+
};
|
|
160
165
|
},
|
|
161
166
|
acp: (env) => {
|
|
162
167
|
const target = env.ACP_TARGET ?? "custom";
|
|
@@ -662,6 +667,10 @@ export async function buildCredStatusReport(
|
|
|
662
667
|
error: presence.ready ? undefined : (presence.hint ?? undefined),
|
|
663
668
|
}
|
|
664
669
|
: null;
|
|
670
|
+
const opencodeGo: AgentCredStatus["opencodeGo"] =
|
|
671
|
+
presence.opencodeGoModels !== undefined
|
|
672
|
+
? { probedAt: Date.now(), models: presence.opencodeGoModels }
|
|
673
|
+
: null;
|
|
665
674
|
return {
|
|
666
675
|
ready: presence.ready,
|
|
667
676
|
missing: presence.missing ?? [],
|
|
@@ -672,6 +681,7 @@ export async function buildCredStatusReport(
|
|
|
672
681
|
reportedAt: Date.now(),
|
|
673
682
|
reportKind: kind,
|
|
674
683
|
bedrock,
|
|
684
|
+
opencodeGo,
|
|
675
685
|
acp: null,
|
|
676
686
|
};
|
|
677
687
|
}
|
package/src/commands/runner.ts
CHANGED
|
@@ -1233,6 +1233,9 @@ export const RELOADABLE_ENV_KEYS: ReadonlySet<string> = new Set([
|
|
|
1233
1233
|
// turning the setting off stops it on running workers.
|
|
1234
1234
|
"SWARM_ORG_WEBSITE",
|
|
1235
1235
|
"PUBLIC_RANKING_ENABLED",
|
|
1236
|
+
"SWARM_ORG_TEAM_SIZE",
|
|
1237
|
+
"SWARM_ORG_USE_CASE",
|
|
1238
|
+
"SWARM_ORG_ACQUISITION_SOURCE",
|
|
1236
1239
|
// pi reads these from process.env for its traits and its session.
|
|
1237
1240
|
"PI_TOOL_DEFERRAL",
|
|
1238
1241
|
"PI_CODEMODE",
|
|
@@ -76,7 +76,7 @@ import {
|
|
|
76
76
|
REBOOT_RETRY_PIN_TAG,
|
|
77
77
|
resolveLeadOnlyRecoveryAssignment,
|
|
78
78
|
} from "../tasks/worker-follow-up";
|
|
79
|
-
import { type AgentTask, TERMINAL_TASK_STATUSES } from "../types";
|
|
79
|
+
import { type AgentTask, SUPERSEDED_WORKFLOW_TASK_REASON, TERMINAL_TASK_STATUSES } from "../types";
|
|
80
80
|
import { isEnvFlagEnabled } from "../utils/env-flag";
|
|
81
81
|
import { isMultiRuntimeEnabled } from "../utils/multi-runtime";
|
|
82
82
|
import { DEFAULT_OPENROUTER_BASE_URL, getOpenRouterBaseUrl } from "../utils/openrouter-base-url";
|
|
@@ -679,7 +679,7 @@ function isRemediationAction(value: unknown): value is RemediationAction {
|
|
|
679
679
|
|
|
680
680
|
function decideRemediation(task: AgentTask, facts: RemediationFacts): RemediationDecision {
|
|
681
681
|
if (task.workflowRunStepId != null) {
|
|
682
|
-
return { action: "fail", reason:
|
|
682
|
+
return { action: "fail", reason: SUPERSEDED_WORKFLOW_TASK_REASON };
|
|
683
683
|
}
|
|
684
684
|
if (facts.skipAutoResume || facts.alreadyResumed) {
|
|
685
685
|
return { action: "fail", reason: facts.legacyFailReason };
|
package/src/hooks/codex-hook.ts
CHANGED
|
@@ -35,6 +35,7 @@ import { getApiKey } from "../utils/api-key";
|
|
|
35
35
|
import { getMcpBaseUrl } from "../utils/constants";
|
|
36
36
|
import { isSteeringEnabled } from "../utils/steering-enabled";
|
|
37
37
|
import { guardGhPrBody, type PrBodyGuardDeps } from "./pr-body-guard";
|
|
38
|
+
import { guardGhPrRoom, type PrRoomGuardDeps } from "./pr-room-guard";
|
|
38
39
|
|
|
39
40
|
const FETCH_TIMEOUT_MS = 5_000;
|
|
40
41
|
|
|
@@ -169,13 +170,16 @@ export async function handleCodexHookEvent(
|
|
|
169
170
|
};
|
|
170
171
|
}
|
|
171
172
|
|
|
172
|
-
/** PreToolUse: the PR
|
|
173
|
+
/** PreToolUse: return the first PR guard block reason, or null to allow the call. */
|
|
173
174
|
export async function codexPreToolUseBlock(
|
|
174
175
|
msg: CodexHookMessage,
|
|
175
176
|
deps?: PrBodyGuardDeps,
|
|
177
|
+
roomDeps?: PrRoomGuardDeps,
|
|
176
178
|
): Promise<string | null> {
|
|
177
179
|
if (msg.hook_event_name !== "PreToolUse") return null;
|
|
178
|
-
|
|
180
|
+
const cwd = msg.cwd ?? process.cwd();
|
|
181
|
+
const block = await guardGhPrBody(msg.tool_input, cwd, deps);
|
|
182
|
+
return block ?? (await guardGhPrRoom(msg.tool_input, cwd, roomDeps));
|
|
179
183
|
}
|
|
180
184
|
|
|
181
185
|
/** stdin/stdout entry point used by the `codex-hook` CLI command. */
|
package/src/hooks/hook.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { getMcpBaseUrl, MAX_PROFILE_FILE_LENGTH } from "../utils/constants";
|
|
|
24
24
|
import { summarizeSession as runSummarize } from "../utils/internal-ai";
|
|
25
25
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
26
26
|
import { guardGhPrBody } from "./pr-body-guard";
|
|
27
|
+
import { guardGhPrRoom } from "./pr-room-guard";
|
|
27
28
|
import { checkToolLoop, clearToolHistory } from "./tool-loop-detection";
|
|
28
29
|
|
|
29
30
|
const SERVER_NAME = pkg.config?.name ?? "agent-swarm";
|
|
@@ -1189,6 +1190,12 @@ export async function handleHook(): Promise<void> {
|
|
|
1189
1190
|
process.exitCode = 2;
|
|
1190
1191
|
return;
|
|
1191
1192
|
}
|
|
1193
|
+
const prRoomBlock = await guardGhPrRoom(msg.tool_input, msg.cwd ?? process.cwd());
|
|
1194
|
+
if (prRoomBlock) {
|
|
1195
|
+
console.error(prRoomBlock);
|
|
1196
|
+
process.exitCode = 2;
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1192
1199
|
}
|
|
1193
1200
|
|
|
1194
1201
|
// Emitted last, so a later block stays the only stdout of the event.
|
|
@@ -40,22 +40,29 @@ import { isAbsolute, resolve } from "node:path";
|
|
|
40
40
|
import { findPrBodyLeaks, type LeakCategory } from "../utils/pr-body-leaks";
|
|
41
41
|
|
|
42
42
|
export type GhPrInvocation = {
|
|
43
|
+
action: "create" | "edit";
|
|
43
44
|
/** Working directory of the `gh` call after any `cd`; null when a `cd` target could not be resolved. */
|
|
44
45
|
cwd: string | null;
|
|
45
46
|
/** Explicit target repos: `--repo` / `-R`, a PR URL, or `GH_REPO`. Empty means the checkout at `cwd`. */
|
|
46
47
|
repos: string[];
|
|
48
|
+
/** Expanded targets for workflow guards. Null means an unknown shell value. */
|
|
49
|
+
resolvedRepos: Array<string | null>;
|
|
47
50
|
/** Also check the checkout: `GH_REPO` was assigned without `export`, so gh may not see it. */
|
|
48
51
|
alsoCheckout: boolean;
|
|
49
52
|
/** The `gh pr edit` target could not be classified (unset variable, `$(...)`, non-PR URL), so the PR's repo is unknown. */
|
|
50
53
|
unknownTarget: boolean;
|
|
51
54
|
/** Literal public text: `--title` and `--body` values, heredoc or here-string stdin. */
|
|
52
55
|
texts: string[];
|
|
56
|
+
/** Expanded body values and stdin, excluding the title. Null means an unknown shell value. */
|
|
57
|
+
bodyTexts: Array<string | null>;
|
|
53
58
|
/** Files that become the body, expanded: `--body-file PATH`, or `--body-file -` with `< PATH`. */
|
|
54
59
|
bodyFiles: string[];
|
|
55
60
|
/** Body file paths whose variables could not be resolved, as written. They count as unreadable. */
|
|
56
61
|
unresolvedBodyFiles: string[];
|
|
57
62
|
/** The body text could not be isolated (piped stdin, unquoted `$(...)`); scan the whole command. */
|
|
58
63
|
opaqueBody: boolean;
|
|
64
|
+
/** gh may build or change the body from a source the parser cannot read: `--fill*`, `--template`, `--editor`, `--web`, `--recover`. */
|
|
65
|
+
externalBody: boolean;
|
|
59
66
|
};
|
|
60
67
|
|
|
61
68
|
export type PrBodyGuardDeps = {
|
|
@@ -282,6 +289,9 @@ const SHORT_FLAGS: Record<string, string> = {
|
|
|
282
289
|
p: "project",
|
|
283
290
|
r: "reviewer",
|
|
284
291
|
T: "template",
|
|
292
|
+
e: "editor",
|
|
293
|
+
f: "fill",
|
|
294
|
+
w: "web",
|
|
285
295
|
};
|
|
286
296
|
|
|
287
297
|
type ParsedArgs = {
|
|
@@ -826,11 +836,14 @@ function parseWithState(
|
|
|
826
836
|
else bodyFiles.push(expanded);
|
|
827
837
|
}
|
|
828
838
|
found.push({
|
|
839
|
+
action,
|
|
829
840
|
cwd: wrapperFlags || dirUnknown ? null : state.dir,
|
|
830
841
|
repos: [...new Set(repos)],
|
|
842
|
+
resolvedRepos: repos.map(expand),
|
|
831
843
|
alsoCheckout,
|
|
832
844
|
unknownTarget,
|
|
833
845
|
texts: [...inline, ...(fromStdin ? stdin : [])],
|
|
846
|
+
bodyTexts: [...values("body"), ...(fromStdin ? stdin : [])].map(expand),
|
|
834
847
|
bodyFiles,
|
|
835
848
|
unresolvedBodyFiles,
|
|
836
849
|
// An unquoted `$(` splits at the paren, leaving a value that ends in `$`.
|
|
@@ -839,6 +852,9 @@ function parseWithState(
|
|
|
839
852
|
inline.some((value) => value.endsWith("$")) ||
|
|
840
853
|
(action === "edit" && target?.endsWith("$") === true) ||
|
|
841
854
|
(fromStdin && stdin.length === 0 && args.stdinFile === undefined),
|
|
855
|
+
externalBody:
|
|
856
|
+
values("fill", "fill-first", "fill-verbose", "template", "editor", "web", "recover")
|
|
857
|
+
.length > 0,
|
|
842
858
|
});
|
|
843
859
|
};
|
|
844
860
|
|
|
@@ -861,6 +877,25 @@ function parseWithState(
|
|
|
861
877
|
const BLOCK_ADVICE =
|
|
862
878
|
"Paraphrase the motivation ('a maintainer asked for X') and link only public sources (Fixes #N, a public PR or issue). Task, session, agent-fs and Slack permalinks go only under '## Swarm provenance'. Bare Slack ids, ts values and private-chat quotes are blocked everywhere.";
|
|
863
879
|
|
|
880
|
+
/** Read body files relative to the parsed checkout and retain the first unreadable path. */
|
|
881
|
+
export async function readGhPrBodyFiles(
|
|
882
|
+
call: GhPrInvocation,
|
|
883
|
+
readFile: PrBodyGuardDeps["readFile"],
|
|
884
|
+
): Promise<{ texts: string[]; unreadable: string | undefined }> {
|
|
885
|
+
const texts: string[] = [];
|
|
886
|
+
let unreadable = call.unresolvedBodyFiles[0];
|
|
887
|
+
for (const bodyFile of call.bodyFiles) {
|
|
888
|
+
try {
|
|
889
|
+
const base = isAbsolute(bodyFile) ? "/" : call.cwd;
|
|
890
|
+
if (base === null) throw new Error("unknown working directory");
|
|
891
|
+
texts.push(await readFile(resolve(base, bodyFile)));
|
|
892
|
+
} catch {
|
|
893
|
+
unreadable ??= bodyFile;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return { texts, unreadable };
|
|
897
|
+
}
|
|
898
|
+
|
|
864
899
|
/** Check a shell command. Returns a block reason, or null to allow. */
|
|
865
900
|
export async function checkGhPrCommand(
|
|
866
901
|
command: string,
|
|
@@ -873,17 +908,9 @@ export async function checkGhPrCommand(
|
|
|
873
908
|
for (const text of call.opaqueBody ? [...call.texts, command] : call.texts) {
|
|
874
909
|
for (const leak of findPrBodyLeaks(text)) leaks.add(leak);
|
|
875
910
|
}
|
|
876
|
-
|
|
877
|
-
for (const
|
|
878
|
-
|
|
879
|
-
const base = isAbsolute(bodyFile) ? "/" : call.cwd;
|
|
880
|
-
if (base === null) throw new Error("unknown working directory");
|
|
881
|
-
for (const leak of findPrBodyLeaks(await deps.readFile(resolve(base, bodyFile)))) {
|
|
882
|
-
leaks.add(leak);
|
|
883
|
-
}
|
|
884
|
-
} catch {
|
|
885
|
-
unreadable ??= bodyFile;
|
|
886
|
-
}
|
|
911
|
+
const { texts, unreadable } = await readGhPrBodyFiles(call, deps.readFile);
|
|
912
|
+
for (const text of texts) {
|
|
913
|
+
for (const leak of findPrBodyLeaks(text)) leaks.add(leak);
|
|
887
914
|
}
|
|
888
915
|
if (leaks.size === 0 && unreadable === undefined) continue;
|
|
889
916
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { parseGhPrCommands, readGhPrBodyFiles, shellCommandOf } from "./pr-body-guard";
|
|
2
|
+
|
|
3
|
+
export type PrRoomGuardDeps = {
|
|
4
|
+
env: Record<string, string | undefined>;
|
|
5
|
+
readFile: (path: string) => Promise<string>;
|
|
6
|
+
remoteOrigin: (cwd: string) => Promise<string>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const BLOCK_REASON =
|
|
10
|
+
'This repo uses PR rooms. Create the room first with the pr-room skill (app action createRoom). Then pass the returned prBody with --body-file. The body must contain the line "PR room: prr-...".';
|
|
11
|
+
const ROOM_LINE = /^PR room: prr-[a-z2-7]{10}\b/m;
|
|
12
|
+
const REMOTE_LOOKUP_TIMEOUT_MS = 5_000;
|
|
13
|
+
|
|
14
|
+
/** Read the `remote.origin.url` of a checkout. A lookup that does not finish in `timeoutMs` is killed and rejects. */
|
|
15
|
+
export async function gitRemoteOrigin(
|
|
16
|
+
cwd: string,
|
|
17
|
+
timeoutMs = REMOTE_LOOKUP_TIMEOUT_MS,
|
|
18
|
+
): Promise<string> {
|
|
19
|
+
const proc = Bun.spawn(["git", "-C", cwd, "config", "--get", "remote.origin.url"], {
|
|
20
|
+
stdout: "pipe",
|
|
21
|
+
stderr: "ignore",
|
|
22
|
+
stdin: "ignore",
|
|
23
|
+
timeout: timeoutMs,
|
|
24
|
+
killSignal: "SIGKILL",
|
|
25
|
+
});
|
|
26
|
+
const [out, code] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
27
|
+
if (code !== 0) throw new Error("Cannot resolve checkout remote");
|
|
28
|
+
return out.trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const defaultDeps = (): PrRoomGuardDeps => ({
|
|
32
|
+
env: process.env,
|
|
33
|
+
readFile: (path) => Bun.file(path).text(),
|
|
34
|
+
remoteOrigin: (cwd) => gitRemoteOrigin(cwd),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/** Extract owner/repo from an HTTPS, SSH URL, or SCP-style SSH remote. */
|
|
38
|
+
function repoFromRemote(remote: string): string | undefined {
|
|
39
|
+
const match =
|
|
40
|
+
/^(?:https?:\/\/[^/]+\/|ssh:\/\/[^/]+\/|[^\s/@:]+@[^\s/:]+:)([^/\s]+)\/([^/\s]+?)\/?$/.exec(
|
|
41
|
+
remote.trim(),
|
|
42
|
+
);
|
|
43
|
+
return match ? `${match[1]}/${match[2]?.replace(/\.git$/, "")}`.toLowerCase() : undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Require a room marker for configured repositories. Unresolved or ambiguous inputs allow the command. */
|
|
47
|
+
export async function guardGhPrRoom(
|
|
48
|
+
toolInput: unknown,
|
|
49
|
+
cwd: string,
|
|
50
|
+
deps: PrRoomGuardDeps = defaultDeps(),
|
|
51
|
+
): Promise<string | null> {
|
|
52
|
+
try {
|
|
53
|
+
const repos = new Set(
|
|
54
|
+
(deps.env.PR_ROOM_REPOS ?? "")
|
|
55
|
+
.split(",")
|
|
56
|
+
.map((repo) => repo.trim().toLowerCase())
|
|
57
|
+
.filter(Boolean),
|
|
58
|
+
);
|
|
59
|
+
if (repos.size === 0) return null;
|
|
60
|
+
const command = shellCommandOf(toolInput);
|
|
61
|
+
if (command === null) return null;
|
|
62
|
+
for (const call of parseGhPrCommands(command, cwd, deps.env)) {
|
|
63
|
+
if (call.action !== "create" || call.unknownTarget || call.cwd === null) continue;
|
|
64
|
+
// The body comes from a pipe, a command substitution, gh itself, or an unknown variable.
|
|
65
|
+
if (call.opaqueBody || call.externalBody || call.bodyTexts.includes(null)) continue;
|
|
66
|
+
const targets = [...call.resolvedRepos];
|
|
67
|
+
if (targets.includes(null)) continue;
|
|
68
|
+
if (targets.length === 0 || call.alsoCheckout) {
|
|
69
|
+
try {
|
|
70
|
+
const repo = repoFromRemote(await deps.remoteOrigin(call.cwd));
|
|
71
|
+
if (!repo) continue;
|
|
72
|
+
targets.push(repo);
|
|
73
|
+
} catch {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// An unexported `GH_REPO=x` may not reach gh: block only when every candidate is listed.
|
|
78
|
+
if (!targets.every((repo) => repo !== null && repos.has(repo.trim().toLowerCase()))) continue;
|
|
79
|
+
const body = await readGhPrBodyFiles(call, deps.readFile);
|
|
80
|
+
if (body.unreadable !== undefined) continue;
|
|
81
|
+
const texts = [...call.bodyTexts, ...body.texts];
|
|
82
|
+
if (!texts.some((text) => text !== null && ROOM_LINE.test(text))) return BLOCK_REASON;
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// A workflow reminder must not prevent commands when resolution fails.
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
package/src/http/agents.ts
CHANGED
|
@@ -1502,6 +1502,7 @@ export async function handleAgentsRest(
|
|
|
1502
1502
|
reportedAt: parsed.body.latest_model?.reportedAt ?? parsed.body.acp!.reportedAt,
|
|
1503
1503
|
reportKind: "post_task" as const,
|
|
1504
1504
|
bedrock: null,
|
|
1505
|
+
opencodeGo: null,
|
|
1505
1506
|
acp: null,
|
|
1506
1507
|
};
|
|
1507
1508
|
finalAgent =
|