@desplega.ai/agent-swarm 1.131.1 → 1.133.1
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 +2 -2
- package/dist/{actions-jgcy477k.js → actions-cskpayd7.js} +11 -9
- package/dist/{anthropic-messages-rjmd2e7n.js → anthropic-messages-tpxtbx7b.js} +28 -12
- package/dist/{app-5da78bd4.js → app-ang77nbb.js} +8 -6
- package/dist/{assistant-f2vkk1hj.js → assistant-b4exebdw.js} +16 -13
- package/dist/{azure-openai-responses-c0yjmbfd.js → azure-openai-responses-m3921t4y.js} +8 -8
- package/dist/{boot-reembed-q7mp8hcr.js → boot-reembed-ps2aftpb.js} +9 -7
- package/dist/{boot-reembed-xmw8wcx6.js → boot-reembed-znyk3nxb.js} +10 -8
- package/dist/{boot-scrub-logs-dsqrv79n.js → boot-scrub-logs-2ax3xgz1.js} +7 -5
- package/dist/{claude-adapter-q6x7yspx.js → claude-adapter-3ct8kgxw.js} +13 -9
- package/dist/{claude-managed-adapter-wsg37bt8.js → claude-managed-adapter-akx4ntf2.js} +2 -2
- package/dist/{cli-114fvtmj.js → cli-13w2hwkh.js} +1 -1
- package/dist/{cli-4rkfg1wx.js → cli-1532szb1.js} +8 -6
- package/dist/{cli-g74vpjp2.js → cli-23d3rseb.js} +33 -17
- package/dist/{cli-3ty0t082.js → cli-2a75z9c5.js} +196 -7
- package/dist/{cli-pxfxkx5m.js → cli-2t30rj0d.js} +1 -1
- package/dist/{cli-rgrfzg8z.js → cli-3c3tdgab.js} +10 -10
- package/dist/{cli-6dp3ydxa.js → cli-3zzxa9jz.js} +15 -11
- package/dist/{cli-2f07m0xr.js → cli-4k6233gc.js} +1 -1
- package/dist/{cli-t8d349cy.js → cli-5p2tz05r.js} +2 -2
- package/dist/{cli-w2wxd56v.js → cli-5z2yhzvd.js} +127 -49
- package/dist/{cli-kd1ck4rx.js → cli-6zfq31nj.js} +9 -3
- package/dist/{cli-er3a4bb0.js → cli-8m9de10a.js} +6 -1
- package/dist/{cli-gm9cbhbq.js → cli-8t14vg9w.js} +27 -8
- package/dist/{cli-ccf9vy7q.js → cli-9nfc2es8.js} +6 -6
- package/dist/{cli-ycrnynpw.js → cli-9ta3pwms.js} +11 -4
- package/dist/{cli-7dm2zqy2.js → cli-avr0ns7z.js} +2 -2
- package/dist/{cli-n6h8qt8n.js → cli-c897wyhw.js} +12 -7
- package/dist/{cli-gecy3z2s.js → cli-dgk20ft7.js} +4 -4
- package/dist/cli-et1bbwqf.js +13 -0
- package/dist/{cli-bqw0mt1n.js → cli-f5w27pj3.js} +3 -3
- package/dist/{cli-2wadx7p2.js → cli-f8dre4bz.js} +2 -2
- package/dist/{cli-6pcz9h15.js → cli-ftwpv451.js} +170 -387
- package/dist/{cli-nrkkknat.js → cli-g854s2dw.js} +1 -1
- package/dist/{cli-793jvbsh.js → cli-ggmdxeqa.js} +13 -8
- package/dist/cli-hkwfe3qj.js +530 -0
- package/dist/{cli-5dsbcjjw.js → cli-jqwnqx9m.js} +1 -1
- package/dist/{cli-9dr5qzc0.js → cli-ny122vh4.js} +21 -10
- package/dist/{cli-sy7jk3tx.js → cli-pvzqhbs9.js} +2 -2
- package/dist/cli-pzq5rjym.js +15 -0
- package/dist/{cli-xpxde7rm.js → cli-qes8ef67.js} +48 -30
- package/dist/{cli-sajgpkqg.js → cli-qs9hhsvb.js} +1723 -325
- package/dist/cli-qys3rhhz.js +27 -0
- package/dist/{cli-se2p6rkb.js → cli-r6tpm37h.js} +2 -2
- package/dist/{cli-g1re8r05.js → cli-rch80c76.js} +2 -2
- package/dist/{cli-0258203w.js → cli-rpp0x4zy.js} +137 -17
- package/dist/{cli-5n6e5yfn.js → cli-s1k0wxhe.js} +12 -7
- package/dist/{cli-aca5pz9f.js → cli-sfd9v7sj.js} +1 -1
- package/dist/{cli-2rk0cx7s.js → cli-sh3ytsyj.js} +49 -39
- package/dist/{cli-zsspvvm2.js → cli-w1mp9jy7.js} +1 -1
- package/dist/{cli-6dpytdr2.js → cli-xdb4mpnp.js} +2 -2
- package/dist/{cli-57khth04.js → cli-xmyqv00z.js} +120 -4
- package/dist/{cli-jbcva0xj.js → cli-xv8t8p3c.js} +56 -31
- package/dist/{cli-f1am9szv.js → cli-xxv5pn4f.js} +1 -1
- package/dist/{cli-jevgrb4w.js → cli-y08jvxzx.js} +601 -140
- package/dist/{cli-50vbt00w.js → cli-zfbf3srj.js} +650 -245
- package/dist/{cli-ygrbmjja.js → cli-zmq89k34.js} +6 -6
- package/dist/cli.js +22 -22
- package/dist/{codex-adapter-htjkn22b.js → codex-adapter-bef93pq4.js} +9 -7
- package/dist/{codex-hook-hq54t9ft.js → codex-hook-e8dba65p.js} +3 -2
- package/dist/{codex-session-runner-wjfc7smz.js → codex-session-runner-5pkxys7x.js} +9 -7
- package/dist/{commands-xhrfwqsk.js → commands-e23s1bff.js} +9 -7
- package/dist/{db-anvwb22f.js → db-m3nabatn.js} +17 -5
- package/dist/{google-generative-ai-hysrd8qx.js → google-generative-ai-8maw55xh.js} +11 -8
- package/dist/{google-vertex-p5arrga0.js → google-vertex-8vjqmx8g.js} +11 -8
- package/dist/{handlers-b9kbc621.js → handlers-akazedvr.js} +16 -13
- package/dist/{hook-xkvn6c5h.js → hook-ze4cpf0v.js} +55 -41
- package/dist/{hook-cgk4wxmx.js → hook-zfqyyenw.js} +10 -7
- package/dist/{http-9xh6ta4r.js → http-sja0n6kw.js} +608 -147
- package/dist/index-0gr8vd6k.js +39 -0
- package/dist/{index-eaxyqcmj.js → index-354vkhdv.js} +154 -74
- package/dist/{index-070v4eyb.js → index-5qthek5t.js} +2 -3
- package/dist/{index-25wjsdv2.js → index-95t3cqq5.js} +16 -14
- package/dist/{index-9znhar3x.js → index-sea66619.js} +3 -4
- package/dist/{index-ywds4812.js → index-vpn36mma.js} +15 -13
- package/dist/{keepalive-vc7s2aa6.js → keepalive-3sp3meqj.js} +16 -10
- package/dist/{lead-r2yqfmx7.js → lead-mefeewkf.js} +39 -36
- package/dist/{maintenance-mw70k468.js → maintenance-8tbrbjfg.js} +10 -8
- package/dist/mistral-conversations-00zrp7g4.js +717 -0
- package/dist/{oauth-refresh-sweep-yecgjpk4.js → oauth-refresh-sweep-srjwjz6m.js} +8 -6
- package/dist/{onboard-xj0f73f9.js → onboard-mt2tgyjg.js} +7 -7
- package/dist/{openai-codex-responses-5g27pd6f.js → openai-codex-responses-map32h2s.js} +18 -18
- package/dist/{openai-completions-3vkmfz2a.js → openai-completions-w871b1cq.js} +12 -11
- package/dist/{openai-responses-dg4t7j7a.js → openai-responses-b0j7xnw8.js} +15 -12
- package/dist/{opencode-adapter-1gm38aba.js → opencode-adapter-b39jefcj.js} +5 -3
- package/dist/{openrouter-images-g3gm8931.js → openrouter-images-srfkywy0.js} +4 -4
- package/dist/{otel-impl-nxg0n0b3.js → otel-impl-z6pxt3j9.js} +224 -9
- package/dist/{pi-mono-adapter-4g5kby31.js → pi-mono-adapter-btvqmj58.js} +1138 -408
- package/dist/{pricing-refresh-2cn6effr.js → pricing-refresh-kyq8cezh.js} +9 -7
- package/dist/{rbac-roles-1hcxdtg7.js → rbac-roles-3ryfrfna.js} +8 -6
- package/dist/{rbac-roles-vynday5g.js → rbac-roles-6qfvbee1.js} +9 -7
- package/dist/{render-v2-tc0bh8kk.js → render-v2-p61ppjey.js} +8 -6
- package/dist/{seed-pricing-8kceb43f.js → seed-pricing-j56bbj4y.js} +8 -6
- package/dist/{setup-46xtgq3c.js → setup-tpr6c1kn.js} +2 -2
- package/dist/{worker-qrbz6hmv.js → worker-9w1v0mbb.js} +39 -36
- package/openapi.json +542 -16
- package/package.json +6 -6
- package/src/be/budget-refusal-notify.ts +14 -7
- package/src/be/db-queries/task-artifacts.ts +182 -0
- package/src/be/db.ts +783 -169
- package/src/be/events.ts +5 -0
- package/src/be/migrations/131_session_logs_created_at_index.sql +2 -0
- package/src/be/migrations/132_multi_runtime_instances.sql +44 -0
- package/src/be/migrations/133_workflow_run_trigger_type.sql +12 -0
- package/src/be/migrations/135_backfill_task_pull_request_attachments.sql +228 -0
- package/src/be/migrations/136_task_requester_provenance.sql +37 -0
- package/src/be/migrations/137_memory_retrieval_composite_index.sql +1 -0
- package/src/be/multi-runtime.ts +430 -0
- package/src/be/scripts/extract-schema.ts +3 -1
- package/src/be/scripts/typecheck.ts +3 -2
- package/src/be/seed-skills/index.ts +7 -0
- package/src/be/steering.ts +1 -1
- package/src/be/swarm-config-guard.ts +18 -0
- package/src/commands/credential-wait.ts +54 -0
- package/src/commands/onboard/steps/core-credentials.tsx +1 -1
- package/src/commands/onboard/steps/custom-templates.tsx +1 -1
- package/src/commands/onboard/steps/harness-credentials.tsx +1 -1
- package/src/commands/onboard/steps/health-check.tsx +1 -1
- package/src/commands/onboard/steps/start.tsx +1 -1
- package/src/commands/profile-sync-audit.ts +56 -0
- package/src/commands/profile-sync.ts +287 -15
- package/src/commands/provider-credentials.ts +46 -19
- package/src/commands/runner.ts +196 -33
- package/src/commands/templates.ts +22 -0
- package/src/github/handlers.ts +55 -9
- package/src/heartbeat/heartbeat.ts +71 -12
- package/src/hooks/hook.ts +49 -33
- package/src/http/active-sessions.ts +11 -1
- package/src/http/agents.ts +261 -30
- package/src/http/all-routes.ts +1 -0
- package/src/http/config.ts +13 -2
- package/src/http/core.ts +102 -4
- package/src/http/db-query-bounded.ts +313 -0
- package/src/http/db-query-shared.ts +249 -0
- package/src/http/db-query.ts +54 -42
- package/src/http/events.ts +2 -0
- package/src/http/index.ts +22 -2
- package/src/http/mcp-bridge.ts +37 -3
- package/src/http/mcp.ts +12 -0
- package/src/http/memory.ts +13 -2
- package/src/http/openapi.ts +1 -0
- package/src/http/poll.ts +70 -27
- package/src/http/route-def.ts +43 -9
- package/src/http/schedules.ts +2 -2
- package/src/http/scripts.ts +10 -1
- package/src/http/session-data.ts +44 -0
- package/src/http/tasks.ts +2 -2
- package/src/http/users.ts +74 -52
- package/src/http/webhooks.ts +5 -2
- package/src/http/workflows.ts +1 -4
- package/src/integrations/kapso/inbound.ts +1 -1
- package/src/jira/outbound.ts +39 -8
- package/src/jira/webhook-lifecycle.ts +13 -2
- package/src/linear/outbound.ts +43 -10
- package/src/oauth/keepalive.ts +13 -2
- package/src/prompts/base-prompt.ts +1 -1
- package/src/prompts/session-templates.ts +45 -7
- package/src/providers/claude-adapter.ts +6 -0
- package/src/providers/codex-adapter.ts +6 -0
- package/src/providers/opencode-adapter.ts +6 -0
- package/src/providers/pi-mono-adapter.ts +8 -0
- package/src/providers/pricing-sources.md +24 -0
- package/src/queue-stall-alarm.ts +216 -0
- package/src/scheduler/scheduler.ts +7 -0
- package/src/script-workflows/executor.ts +2 -1
- package/src/scripts-runtime/executors/native.ts +2 -1
- package/src/scripts-runtime/executors/types.ts +5 -0
- package/src/scripts-runtime/loader.ts +5 -0
- package/src/scripts-runtime/swarm-config.ts +8 -0
- package/src/scripts-runtime/swarm-sdk.ts +7 -0
- package/src/scripts-runtime/types/stdlib.d.ts +1 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
- package/src/tests/budget-claim-gate.test.ts +20 -0
- package/src/tests/claude-adapter.test.ts +32 -0
- package/src/tests/codex-adapter.test.ts +30 -0
- package/src/tests/create-task-validation.test.ts +128 -0
- package/src/tests/credential-wait.test.ts +67 -1
- package/src/tests/db-query.test.ts +711 -2
- package/src/tests/http-api-integration.test.ts +81 -0
- package/src/tests/http-users.test.ts +166 -1
- package/src/tests/identity-field-budget.test.ts +176 -0
- package/src/tests/mcp-tools-user.test.ts +34 -0
- package/src/tests/mcp-user-route.test.ts +17 -0
- package/src/tests/metrics-http.test.ts +44 -0
- package/src/tests/multi-runtime-registration.test.ts +2703 -0
- package/src/tests/openapi-response-contract.test.ts +24 -0
- package/src/tests/openapi-route-headers.test.ts +83 -0
- package/src/tests/opencode-adapter.test.ts +40 -0
- package/src/tests/outbound-listener-rejection.test.ts +138 -0
- package/src/tests/pi-mono-adapter.test.ts +98 -0
- package/src/tests/profile-sync-warning.test.ts +277 -0
- package/src/tests/profile-sync.test.ts +232 -6
- package/src/tests/prompt-template-session.test.ts +32 -3
- package/src/tests/queue-stall-alarm.test.ts +226 -0
- package/src/tests/requester-comms.test.ts +32 -0
- package/src/tests/runner-requester-profile.test.ts +38 -0
- package/src/tests/schedule-target-type.test.ts +38 -0
- package/src/tests/script-connections.test.ts +13 -1
- package/src/tests/scripts-runtime-identity.test.ts +375 -0
- package/src/tests/scripts-runtime.test.ts +4 -3
- package/src/tests/sdk-allowlist.test.ts +47 -0
- package/src/tests/send-task-requested-by.test.ts +9 -0
- package/src/tests/session-costs.test.ts +506 -1
- package/src/tests/shutdown-error-scrubbing.test.ts +92 -0
- package/src/tests/steering-core.test.ts +28 -0
- package/src/tests/task-artifacts.test.ts +386 -0
- package/src/tests/task-attachments-schema.test.ts +17 -1
- package/src/tests/task-completion-idempotency.test.ts +57 -0
- package/src/tests/task-pull-request-backfill-migration.test.ts +321 -0
- package/src/tests/update-profile-api.test.ts +67 -39
- package/src/tests/update-profile-auth.test.ts +48 -4
- package/src/tests/user-attribution-migration.test.ts +54 -0
- package/src/tests/user-identity.test.ts +72 -0
- package/src/tests/workflow-async-v2.test.ts +39 -1
- package/src/tests/workflow-http-v2.test.ts +3 -2
- package/src/tests/workflow-mcp-trigger-schema.test.ts +52 -3
- package/src/tests/workflow-retry-v2.test.ts +45 -1
- package/src/tools/db-query.ts +18 -8
- package/src/tools/get-task-details.ts +24 -3
- package/src/tools/manage-user.ts +36 -3
- package/src/tools/poll-task.ts +93 -14
- package/src/tools/script-common.ts +5 -0
- package/src/tools/send-task.ts +1 -4
- package/src/tools/store-progress.ts +13 -2
- package/src/tools/swarm-config/delete-config.ts +9 -0
- package/src/tools/swarm-config/set-config.ts +3 -2
- package/src/tools/task-action.ts +30 -0
- package/src/tools/task-tool-ctx.ts +8 -1
- package/src/tools/update-profile.ts +16 -21
- package/src/tools/utils.ts +5 -0
- package/src/tools/workflows/trigger-workflow.ts +7 -1
- package/src/types.ts +165 -1
- package/src/utils/github-pull-request.ts +76 -0
- package/src/utils/identity-field-budget.ts +73 -0
- package/src/utils/multi-runtime.ts +20 -0
- package/src/utils/requester-comms.ts +24 -0
- package/src/workflows/engine.ts +22 -2
- package/src/workflows/resume.ts +19 -11
- package/src/workflows/retry-poller.ts +1 -0
- package/src/workflows/triggers.ts +4 -0
- package/templates/skills/db-query-guidance/config.json +14 -0
- package/templates/skills/db-query-guidance/content.md +57 -0
- package/dist/cli-3te4jdkm.js +0 -225
- package/dist/cli-bmze986d.js +0 -7
- package/dist/cli-kh3ba2zf.js +0 -607
- package/dist/index-vg4snay4.js +0 -36
- package/dist/mistral-conversations-81fwav0j.js +0 -30578
- package/dist/otel-nqa12wf3.js +0 -3149
- package/dist/{cli-wz9115x0.js → cli-zr9q011v.js} +6 -6
- package/dist/{pi-messages-tqj8q6b9.js → pi-messages-sssy32x4.js} +3 -3
package/src/http/db-query.ts
CHANGED
|
@@ -1,15 +1,30 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
3
|
+
import { executeReadOnlyQueryBounded } from "./db-query-bounded";
|
|
4
|
+
import {
|
|
5
|
+
assertSingleStatement,
|
|
6
|
+
DbQueryConcurrencyCapError,
|
|
7
|
+
type DbQueryResult,
|
|
8
|
+
DbQueryTimeoutError,
|
|
9
|
+
executeReadOnlyQuery,
|
|
10
|
+
getDbQueryHttpBudgetMs,
|
|
11
|
+
getDbQueryHttpMaxRows,
|
|
12
|
+
isDbQueryBoundedEnabled,
|
|
13
|
+
stripTrailingSemicolon,
|
|
14
|
+
warnDbQueryBoundedDisabledOnce,
|
|
15
|
+
} from "./db-query-shared";
|
|
4
16
|
import { route } from "./route-def";
|
|
5
17
|
import { json, jsonError } from "./utils";
|
|
6
18
|
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
19
|
+
export type { DbQueryResult } from "./db-query-shared";
|
|
20
|
+
export {
|
|
21
|
+
assertSingleStatement,
|
|
22
|
+
executeReadOnlyQuery,
|
|
23
|
+
getDbQueryHttpBudgetMs,
|
|
24
|
+
getDbQueryHttpMaxRows,
|
|
25
|
+
getDbQueryMcpBudgetMs,
|
|
26
|
+
isDbQueryBoundedEnabled,
|
|
27
|
+
} from "./db-query-shared";
|
|
13
28
|
|
|
14
29
|
export const DbQueryInputShape = {
|
|
15
30
|
sql: z.string().min(1).max(10_000).optional(),
|
|
@@ -29,17 +44,6 @@ export function resolveDbQuerySql(input: Pick<DbQueryInput, "sql" | "query">): s
|
|
|
29
44
|
return input.sql ?? input.query ?? "";
|
|
30
45
|
}
|
|
31
46
|
|
|
32
|
-
function stripTrailingSemicolon(sql: string): string {
|
|
33
|
-
return sql.trim().replace(/;\s*$/, "").trim();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function assertSingleStatement(sql: string): void {
|
|
37
|
-
const stripped = stripTrailingSemicolon(sql);
|
|
38
|
-
if (stripped.includes(";")) {
|
|
39
|
-
throw new Error("Only one SQL statement is allowed");
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
47
|
export function assertSelectOnlyQuery(sql: string): void {
|
|
44
48
|
assertSingleStatement(sql);
|
|
45
49
|
const normalized = stripTrailingSemicolon(sql).toLowerCase();
|
|
@@ -49,34 +53,23 @@ export function assertSelectOnlyQuery(sql: string): void {
|
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
56
|
+
* Gate in front of the bounded executor (Fix 1). `DB_QUERY_BOUNDED_ENABLED`
|
|
57
|
+
* (default on) picks the path: enabled runs the bounded child-process
|
|
58
|
+
* executor unchanged; disabled restores the pre-fix synchronous path with no
|
|
59
|
+
* wall-clock budget and logs a one-time warning, so turning off the
|
|
60
|
+
* protection can't happen silently.
|
|
54
61
|
*/
|
|
55
|
-
export function
|
|
62
|
+
export async function executeReadOnlyQueryGated(
|
|
56
63
|
sql: string,
|
|
57
64
|
params: unknown[] = [],
|
|
65
|
+
budgetMs: number,
|
|
58
66
|
maxRows?: number,
|
|
59
|
-
): DbQueryResult {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// bun:sqlite: columnNames is empty for write statements, populated for SELECT/PRAGMA/EXPLAIN
|
|
64
|
-
if (stmt.columnNames.length === 0) {
|
|
65
|
-
throw new Error("Only read-only queries are allowed");
|
|
67
|
+
): Promise<DbQueryResult> {
|
|
68
|
+
if (isDbQueryBoundedEnabled()) {
|
|
69
|
+
return executeReadOnlyQueryBounded(sql, params, budgetMs, maxRows);
|
|
66
70
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const start = performance.now();
|
|
70
|
-
const rows = (params.length > 0 ? stmt.all(...(params as [string])) : stmt.all()) as Record<
|
|
71
|
-
string,
|
|
72
|
-
unknown
|
|
73
|
-
>[];
|
|
74
|
-
const elapsed = Math.round(performance.now() - start);
|
|
75
|
-
|
|
76
|
-
const capped = maxRows ? rows.slice(0, maxRows) : rows;
|
|
77
|
-
const rowArrays = capped.map((row) => columns.map((col) => row[col]));
|
|
78
|
-
|
|
79
|
-
return { columns, rows: rowArrays, elapsed, total: rows.length };
|
|
71
|
+
warnDbQueryBoundedDisabledOnce();
|
|
72
|
+
return executeReadOnlyQuery(sql, params, maxRows);
|
|
80
73
|
}
|
|
81
74
|
|
|
82
75
|
const dbQueryRoute = route({
|
|
@@ -97,6 +90,8 @@ const dbQueryRoute = route({
|
|
|
97
90
|
}),
|
|
98
91
|
},
|
|
99
92
|
400: { description: "Invalid or disallowed SQL" },
|
|
93
|
+
408: { description: "Query exceeded its wall-clock budget and was terminated" },
|
|
94
|
+
429: { description: "Too many concurrent bounded db-query executions; retry shortly" },
|
|
100
95
|
},
|
|
101
96
|
auth: { apiKey: true },
|
|
102
97
|
});
|
|
@@ -115,9 +110,26 @@ export async function handleDbQuery(
|
|
|
115
110
|
if (!parsed) return true;
|
|
116
111
|
|
|
117
112
|
try {
|
|
118
|
-
const result =
|
|
113
|
+
const result = await executeReadOnlyQueryGated(
|
|
114
|
+
resolveDbQuerySql(parsed.body),
|
|
115
|
+
parsed.body.params,
|
|
116
|
+
getDbQueryHttpBudgetMs(),
|
|
117
|
+
getDbQueryHttpMaxRows(),
|
|
118
|
+
);
|
|
119
119
|
json(res, result);
|
|
120
120
|
} catch (err: unknown) {
|
|
121
|
+
if (err instanceof DbQueryConcurrencyCapError) {
|
|
122
|
+
// Machine-readable code + Retry-After so a caller can back off instead
|
|
123
|
+
// of treating this like a malformed request (the pre-existing generic
|
|
124
|
+
// 400 every other error path here returns).
|
|
125
|
+
res.setHeader("Retry-After", "1");
|
|
126
|
+
json(res, { error: "db_query_concurrency_cap", message: err.message }, 429);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
if (err instanceof DbQueryTimeoutError) {
|
|
130
|
+
json(res, { error: "db_query_timeout", message: err.message }, 408);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
121
133
|
const message = err instanceof Error ? err.message : String(err);
|
|
122
134
|
jsonError(res, message);
|
|
123
135
|
}
|
package/src/http/events.ts
CHANGED
|
@@ -82,6 +82,7 @@ const getEventsRoute = route({
|
|
|
82
82
|
agentId: z.string().optional(),
|
|
83
83
|
taskId: z.string().optional(),
|
|
84
84
|
sessionId: z.string().optional(),
|
|
85
|
+
dataField: z.string().optional(),
|
|
85
86
|
since: z.string().optional(),
|
|
86
87
|
until: z.string().optional(),
|
|
87
88
|
limit: z.coerce.number().int().min(1).max(1000).optional(),
|
|
@@ -191,6 +192,7 @@ export async function handleEvents(
|
|
|
191
192
|
agentId: parsed.query.agentId || undefined,
|
|
192
193
|
taskId: parsed.query.taskId || undefined,
|
|
193
194
|
sessionId: parsed.query.sessionId || undefined,
|
|
195
|
+
dataField: parsed.query.dataField || undefined,
|
|
194
196
|
since: parsed.query.since || undefined,
|
|
195
197
|
until: parsed.query.until || undefined,
|
|
196
198
|
limit: parsed.query.limit ?? 100,
|
package/src/http/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
withRemoteContext,
|
|
32
32
|
withSpanContext,
|
|
33
33
|
} from "../otel";
|
|
34
|
+
import { startQueueStallAlarm, stopQueueStallAlarm } from "../queue-stall-alarm";
|
|
34
35
|
import { clearAuditSink, isRbacEnabled, setAuditSink } from "../rbac";
|
|
35
36
|
import { startScriptRunSupervisor, stopScriptRunSupervisor } from "../script-workflows/supervisor";
|
|
36
37
|
import { getServerSessionsProcessed } from "../server-runtime-counters";
|
|
@@ -424,6 +425,9 @@ async function shutdown() {
|
|
|
424
425
|
// Stop heartbeat triage
|
|
425
426
|
stopHeartbeat();
|
|
426
427
|
|
|
428
|
+
// Stop the out-of-band queue alarm before disconnecting its Slack notifier.
|
|
429
|
+
stopQueueStallAlarm();
|
|
430
|
+
|
|
427
431
|
// Stop durable script workflow subprocesses
|
|
428
432
|
stopScriptRunSupervisor();
|
|
429
433
|
|
|
@@ -485,13 +489,26 @@ async function shutdown() {
|
|
|
485
489
|
// Only register signal handlers once (avoid duplicates on hot reload)
|
|
486
490
|
if (!globalState.__sigintRegistered) {
|
|
487
491
|
globalState.__sigintRegistered = true;
|
|
492
|
+
// A rejected shutdown must not become an unhandled rejection: the signal
|
|
493
|
+
// handler is the last code that runs, so an error here is the only chance to
|
|
494
|
+
// learn why the process failed to exit cleanly.
|
|
488
495
|
process.on("SIGINT", () => {
|
|
489
496
|
shutdownSignal = "SIGINT";
|
|
490
|
-
shutdown()
|
|
497
|
+
shutdown().catch((err) =>
|
|
498
|
+
console.error(
|
|
499
|
+
"[shutdown] SIGINT shutdown failed:",
|
|
500
|
+
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
501
|
+
),
|
|
502
|
+
);
|
|
491
503
|
});
|
|
492
504
|
process.on("SIGTERM", () => {
|
|
493
505
|
shutdownSignal = "SIGTERM";
|
|
494
|
-
shutdown()
|
|
506
|
+
shutdown().catch((err) =>
|
|
507
|
+
console.error(
|
|
508
|
+
"[shutdown] SIGTERM shutdown failed:",
|
|
509
|
+
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
510
|
+
),
|
|
511
|
+
);
|
|
495
512
|
});
|
|
496
513
|
}
|
|
497
514
|
|
|
@@ -608,6 +625,9 @@ httpServer
|
|
|
608
625
|
// Start Slack bot (if configured)
|
|
609
626
|
await startSlackApp();
|
|
610
627
|
|
|
628
|
+
// Independent of workers, scheduler targets, and heartbeat agent tasks.
|
|
629
|
+
startQueueStallAlarm();
|
|
630
|
+
|
|
611
631
|
// Initialize GitHub webhook handler (if configured)
|
|
612
632
|
initGitHub();
|
|
613
633
|
|
package/src/http/mcp-bridge.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import {
|
|
4
|
+
type AnySchema,
|
|
5
|
+
getParseErrorMessage,
|
|
6
|
+
normalizeObjectSchema,
|
|
7
|
+
safeParseAsync,
|
|
8
|
+
} from "@modelcontextprotocol/sdk/server/zod-compat.js";
|
|
3
9
|
import { z } from "zod";
|
|
4
10
|
import { createServer } from "@/server";
|
|
5
11
|
import { isMcpToolAllowedForScripts } from "../scripts-runtime/sdk-allowlist";
|
|
6
12
|
import { markScriptSdkRequestOrigin } from "../tools/utils";
|
|
7
|
-
import { route } from "./route-def";
|
|
13
|
+
import { route, runtimeInstanceHeader } from "./route-def";
|
|
8
14
|
import { json, jsonError } from "./utils";
|
|
9
15
|
|
|
10
16
|
// Lazy singleton — created once on first bridge call to avoid boot-time cost.
|
|
@@ -23,7 +29,7 @@ function getBridgeServer(): McpServer {
|
|
|
23
29
|
|
|
24
30
|
type RegisteredTool = {
|
|
25
31
|
handler: (argsOrExtra: unknown, extra?: unknown) => unknown | Promise<unknown>;
|
|
26
|
-
inputSchema?:
|
|
32
|
+
inputSchema?: AnySchema;
|
|
27
33
|
enabled?: boolean;
|
|
28
34
|
};
|
|
29
35
|
|
|
@@ -35,6 +41,7 @@ const mcpBridgeRoute = route({
|
|
|
35
41
|
pattern: ["api", "mcp-bridge"],
|
|
36
42
|
summary: "Generic MCP tool proxy for the scripts SDK bridge",
|
|
37
43
|
tags: ["Scripts"],
|
|
44
|
+
headers: runtimeInstanceHeader("acquire work through bridged tools"),
|
|
38
45
|
body: z.object({
|
|
39
46
|
tool: z.string().min(1).max(200),
|
|
40
47
|
args: z.record(z.string(), z.unknown()).default({}),
|
|
@@ -87,6 +94,11 @@ export async function handleMcpBridge(
|
|
|
87
94
|
const sourceTaskId = Array.isArray(req.headers["x-source-task-id"])
|
|
88
95
|
? req.headers["x-source-task-id"][0]
|
|
89
96
|
: (req.headers["x-source-task-id"] as string | undefined);
|
|
97
|
+
// Runtime identity rides the bridge like the agent identity so the
|
|
98
|
+
// work-acquisition gates in bridged tools see the invoking worker process.
|
|
99
|
+
const runtimeInstanceId = Array.isArray(req.headers["x-runtime-instance-id"])
|
|
100
|
+
? req.headers["x-runtime-instance-id"][0]
|
|
101
|
+
: (req.headers["x-runtime-instance-id"] as string | undefined);
|
|
90
102
|
|
|
91
103
|
const extra = markScriptSdkRequestOrigin({
|
|
92
104
|
sessionId: "mcp-bridge",
|
|
@@ -94,13 +106,35 @@ export async function handleMcpBridge(
|
|
|
94
106
|
headers: {
|
|
95
107
|
"x-agent-id": myAgentId ?? "",
|
|
96
108
|
...(sourceTaskId ? { "x-source-task-id": sourceTaskId } : {}),
|
|
109
|
+
...(runtimeInstanceId ? { "x-runtime-instance-id": runtimeInstanceId } : {}),
|
|
97
110
|
},
|
|
98
111
|
},
|
|
99
112
|
});
|
|
100
113
|
|
|
114
|
+
// Mirror the SDK's own tools/call validation (`validateToolInput` in
|
|
115
|
+
// @modelcontextprotocol/sdk server/mcp.js). The bridge bypasses the MCP
|
|
116
|
+
// transport, so without this parse raw script args reach handlers
|
|
117
|
+
// unchecked (2026-08-18 priority='high' incident) and the schema's
|
|
118
|
+
// `.default()`/`.transform()` never apply.
|
|
119
|
+
let handlerArgs: unknown = args;
|
|
120
|
+
if (tool.inputSchema) {
|
|
121
|
+
const inputObj = normalizeObjectSchema(tool.inputSchema);
|
|
122
|
+
const parseResult = await safeParseAsync(inputObj ?? tool.inputSchema, args);
|
|
123
|
+
if (!parseResult.success) {
|
|
124
|
+
const parseError = "error" in parseResult ? parseResult.error : "Unknown error";
|
|
125
|
+
jsonError(
|
|
126
|
+
res,
|
|
127
|
+
`Invalid arguments for tool '${toolName}': ${getParseErrorMessage(parseError)}`,
|
|
128
|
+
400,
|
|
129
|
+
);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
handlerArgs = parseResult.data;
|
|
133
|
+
}
|
|
134
|
+
|
|
101
135
|
try {
|
|
102
136
|
const result = tool.inputSchema
|
|
103
|
-
? await Promise.resolve(tool.handler(
|
|
137
|
+
? await Promise.resolve(tool.handler(handlerArgs, extra))
|
|
104
138
|
: await Promise.resolve(tool.handler(extra));
|
|
105
139
|
|
|
106
140
|
if (result && typeof result === "object" && "structuredContent" in result) {
|
package/src/http/mcp.ts
CHANGED
|
@@ -118,6 +118,18 @@ export async function handleMcp(
|
|
|
118
118
|
}
|
|
119
119
|
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
120
120
|
|
|
121
|
+
if (!sessionId && body.method === "server/discover") {
|
|
122
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
123
|
+
res.end(
|
|
124
|
+
JSON.stringify({
|
|
125
|
+
jsonrpc: "2.0",
|
|
126
|
+
error: { code: -32601, message: "Method not found" },
|
|
127
|
+
id: body.id ?? null,
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
121
133
|
let transport: StreamableHTTPServerTransport;
|
|
122
134
|
|
|
123
135
|
if (sessionId && transports[sessionId]) {
|
package/src/http/memory.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { getRetrievalsForAgent, hasRetrievalForTask } from "../be/memory/retriev
|
|
|
21
21
|
import { getUsefulnessStats } from "../be/memory/usefulness-stats";
|
|
22
22
|
import { shouldPersistAutomaticTaskMemory } from "../memory/automatic-task-gate";
|
|
23
23
|
import { AgentMemorySchema, AgentMemoryScopeSchema, AgentMemorySourceSchema } from "../types";
|
|
24
|
+
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
24
25
|
import { route } from "./route-def";
|
|
25
26
|
import { jsonError, parseQueryParams } from "./utils";
|
|
26
27
|
|
|
@@ -725,7 +726,12 @@ export async function handleMemory(
|
|
|
725
726
|
} catch (err) {
|
|
726
727
|
console.error("[memory] Batch embedding failed:", (err as Error).message);
|
|
727
728
|
}
|
|
728
|
-
})()
|
|
729
|
+
})().catch((err) =>
|
|
730
|
+
console.error(
|
|
731
|
+
"[memory] batch embed failed:",
|
|
732
|
+
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
733
|
+
),
|
|
734
|
+
);
|
|
729
735
|
|
|
730
736
|
indexMemory.respond(res, 202, { queued: true, memoryIds: memories.map((m) => m.id) });
|
|
731
737
|
return true;
|
|
@@ -1041,7 +1047,12 @@ export async function handleMemory(
|
|
|
1041
1047
|
}
|
|
1042
1048
|
}
|
|
1043
1049
|
console.log(`[memory] Re-embedding complete: ${memories.length} memories`);
|
|
1044
|
-
})()
|
|
1050
|
+
})().catch((err) =>
|
|
1051
|
+
console.error(
|
|
1052
|
+
"[memory] re-embed batch failed:",
|
|
1053
|
+
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
1054
|
+
),
|
|
1055
|
+
);
|
|
1045
1056
|
|
|
1046
1057
|
return true;
|
|
1047
1058
|
}
|
package/src/http/openapi.ts
CHANGED
|
@@ -65,6 +65,7 @@ export function generateOpenApiSpec(opts: OpenApiOptions): string {
|
|
|
65
65
|
const request: Record<string, unknown> = {};
|
|
66
66
|
if (routeDef.params) request.params = routeDef.params;
|
|
67
67
|
if (routeDef.query) request.query = routeDef.query;
|
|
68
|
+
if (routeDef.headers) request.headers = routeDef.headers;
|
|
68
69
|
if (routeDef.body) {
|
|
69
70
|
request.body = {
|
|
70
71
|
content: { "application/json": { schema: routeDef.body } },
|
package/src/http/poll.ts
CHANGED
|
@@ -23,9 +23,11 @@ import {
|
|
|
23
23
|
hasCapacity,
|
|
24
24
|
recordBudgetRefusalNotification,
|
|
25
25
|
startTask,
|
|
26
|
+
updateAgentStatusFromCapacity,
|
|
26
27
|
upsertChannelActivityCursor,
|
|
27
28
|
} from "../be/db";
|
|
28
29
|
import { renderIdentity, resolveIdentity } from "../be/identity";
|
|
30
|
+
import { touchRuntimeInstance } from "../be/multi-runtime";
|
|
29
31
|
import { hasCapability } from "../server";
|
|
30
32
|
import { fetchChannelActivity } from "../slack/channel-activity";
|
|
31
33
|
import { telemetry } from "../telemetry";
|
|
@@ -33,9 +35,12 @@ import {
|
|
|
33
35
|
AgentTaskSchema,
|
|
34
36
|
BudgetRefusedTriggerSchema,
|
|
35
37
|
TaskAttachmentSchema,
|
|
38
|
+
UserCommsPrefsSchema,
|
|
36
39
|
UserSchema,
|
|
37
40
|
} from "../types";
|
|
38
|
-
import {
|
|
41
|
+
import { isMultiRuntimeEnabled } from "../utils/multi-runtime";
|
|
42
|
+
import { getUserCommsPrefs } from "../utils/requester-comms";
|
|
43
|
+
import { route, runtimeInstanceHeader } from "./route-def";
|
|
39
44
|
import { jsonError } from "./utils";
|
|
40
45
|
|
|
41
46
|
// ─── Budget-refused trigger envelope ────────────────────────────────────────
|
|
@@ -93,12 +98,16 @@ const PollRequestedBySchema = UserSchema.pick({
|
|
|
93
98
|
email: true,
|
|
94
99
|
role: true,
|
|
95
100
|
notes: true,
|
|
101
|
+
}).extend({
|
|
102
|
+
// Structured communication preferences from `users.metadata.comms`.
|
|
103
|
+
comms: UserCommsPrefsSchema.optional(),
|
|
96
104
|
});
|
|
97
105
|
|
|
98
106
|
const PollTaskOfferedTriggerSchema = z.object({
|
|
99
107
|
type: z.literal("task_offered"),
|
|
100
108
|
taskId: z.string(),
|
|
101
109
|
task: AgentTaskSchema,
|
|
110
|
+
requestedBy: PollRequestedBySchema.optional(),
|
|
102
111
|
});
|
|
103
112
|
|
|
104
113
|
const PollTaskAssignedTriggerSchema = z.object({
|
|
@@ -155,6 +164,7 @@ const pollTriggers = route({
|
|
|
155
164
|
summary: "Poll for triggers (tasks, mentions)",
|
|
156
165
|
tags: ["Poll"],
|
|
157
166
|
auth: { apiKey: true, agentId: true },
|
|
167
|
+
headers: runtimeInstanceHeader("poll for work"),
|
|
158
168
|
responses: {
|
|
159
169
|
200: { description: "Trigger data or null", schema: pollResponseSchema },
|
|
160
170
|
400: { description: "Missing X-Agent-ID" },
|
|
@@ -171,6 +181,33 @@ function getRequesterNotes(notes: string | undefined): string | undefined {
|
|
|
171
181
|
return typeof notes === "string" && notes.trim().length > 0 ? notes : undefined;
|
|
172
182
|
}
|
|
173
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Resolve the requester projection for a task trigger. If the task carries a
|
|
186
|
+
* machine-recorded external id (today: the Slack user field) but no
|
|
187
|
+
* requestedByUserId, render the explicit UNKNOWN sentinel instead of silently
|
|
188
|
+
* omitting the section: never a substituted human (Rule 33 /
|
|
189
|
+
* provenance-or-silence).
|
|
190
|
+
*/
|
|
191
|
+
function buildTriggerRequestedBy(task: {
|
|
192
|
+
requestedByUserId?: string | null;
|
|
193
|
+
slackUserId?: string | null;
|
|
194
|
+
}): z.infer<typeof PollRequestedBySchema> | undefined {
|
|
195
|
+
const user = task.requestedByUserId ? getUserById(task.requestedByUserId) : undefined;
|
|
196
|
+
if (user) {
|
|
197
|
+
return {
|
|
198
|
+
name: user.name,
|
|
199
|
+
email: user.email,
|
|
200
|
+
role: user.role,
|
|
201
|
+
notes: getRequesterNotes(user.notes),
|
|
202
|
+
comms: getUserCommsPrefs(user),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (task.slackUserId) {
|
|
206
|
+
return { name: renderIdentity(resolveIdentity("slack", task.slackUserId)) };
|
|
207
|
+
}
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
|
|
174
211
|
/**
|
|
175
212
|
* Slim attachment projection for the `task_assigned` poll trigger — just
|
|
176
213
|
* enough (id, name, mimeType, sizeBytes) for the worker to build a one-shot
|
|
@@ -223,6 +260,9 @@ export async function handlePoll(
|
|
|
223
260
|
queryParams: URLSearchParams,
|
|
224
261
|
myAgentId: string | undefined,
|
|
225
262
|
): Promise<boolean> {
|
|
263
|
+
const runtimeInstanceId = ((h) => (Array.isArray(h) ? h[0] : h))(
|
|
264
|
+
req.headers["x-runtime-instance-id"],
|
|
265
|
+
);
|
|
226
266
|
// Handle cursor commit endpoint
|
|
227
267
|
if (commitCursorsRoute.match(req.method, pathSegments)) {
|
|
228
268
|
const parsed = await commitCursorsRoute.parse(req, res, pathSegments, queryParams);
|
|
@@ -266,18 +306,37 @@ export async function handlePoll(
|
|
|
266
306
|
return { error: "Agent not found", status: 404 };
|
|
267
307
|
}
|
|
268
308
|
|
|
309
|
+
// A process whose runtime has been retired must not be handed work: it
|
|
310
|
+
// would execute alongside whatever replaced it. Dispatch is gated on a
|
|
311
|
+
// live runtime identity rather than on X-Agent-ID alone, which only
|
|
312
|
+
// names the logical agent. Polling is worker activity, so this also
|
|
313
|
+
// refreshes liveness — it cannot revive a retired runtime, since the
|
|
314
|
+
// update only matches one that is still live.
|
|
315
|
+
if (
|
|
316
|
+
isMultiRuntimeEnabled() &&
|
|
317
|
+
!(runtimeInstanceId && touchRuntimeInstance(runtimeInstanceId, agent.id))
|
|
318
|
+
) {
|
|
319
|
+
return { trigger: null };
|
|
320
|
+
}
|
|
321
|
+
|
|
269
322
|
// Check for offered tasks first (highest priority for both workers and leads)
|
|
270
|
-
// Atomically claim the task for review to prevent duplicate processing
|
|
271
|
-
|
|
323
|
+
// Atomically claim the task for review to prevent duplicate processing.
|
|
324
|
+
// Capacity is checked in the same transaction as the claim: with
|
|
325
|
+
// several runtimes serving one agent these polls run concurrently, and
|
|
326
|
+
// an unguarded claim here would let each of them take a task past the
|
|
327
|
+
// agent's logical limit.
|
|
328
|
+
const offeredTasks = hasCapacity(myAgentId) ? getOfferedTasksForAgent(myAgentId) : [];
|
|
272
329
|
const firstOfferedTask = offeredTasks[0];
|
|
273
330
|
if (firstOfferedTask) {
|
|
274
331
|
const claimedTask = claimOfferedTask(firstOfferedTask.id, myAgentId);
|
|
275
332
|
if (claimedTask) {
|
|
333
|
+
const offeredRequestedBy = buildTriggerRequestedBy(claimedTask);
|
|
276
334
|
return {
|
|
277
335
|
trigger: {
|
|
278
336
|
type: "task_offered",
|
|
279
337
|
taskId: claimedTask.id,
|
|
280
338
|
task: claimedTask,
|
|
339
|
+
...(offeredRequestedBy && { requestedBy: offeredRequestedBy }),
|
|
281
340
|
},
|
|
282
341
|
};
|
|
283
342
|
}
|
|
@@ -337,6 +396,7 @@ export async function handlePoll(
|
|
|
337
396
|
|
|
338
397
|
// Mark task as in_progress immediately to prevent duplicate polling
|
|
339
398
|
startTask(pendingTask.id);
|
|
399
|
+
updateAgentStatusFromCapacity(myAgentId);
|
|
340
400
|
|
|
341
401
|
ensure({
|
|
342
402
|
id: "started",
|
|
@@ -360,19 +420,9 @@ export async function handlePoll(
|
|
|
360
420
|
agentId: myAgentId,
|
|
361
421
|
});
|
|
362
422
|
|
|
363
|
-
// Resolve requesting user if available
|
|
364
|
-
//
|
|
365
|
-
|
|
366
|
-
// instead of silently omitting the section — never a substituted
|
|
367
|
-
// human (Rule 33 / provenance-or-silence).
|
|
368
|
-
const requestedByUser = pendingTask.requestedByUserId
|
|
369
|
-
? getUserById(pendingTask.requestedByUserId)
|
|
370
|
-
: undefined;
|
|
371
|
-
const requestedByNotes = getRequesterNotes(requestedByUser?.notes);
|
|
372
|
-
const requestedByUnknownName =
|
|
373
|
-
!requestedByUser && pendingTask.slackUserId
|
|
374
|
-
? renderIdentity(resolveIdentity("slack", pendingTask.slackUserId))
|
|
375
|
-
: undefined;
|
|
423
|
+
// Resolve requesting user if available (UNKNOWN sentinel handling
|
|
424
|
+
// lives in buildTriggerRequestedBy).
|
|
425
|
+
const assignedRequestedBy = buildTriggerRequestedBy(pendingTask);
|
|
376
426
|
|
|
377
427
|
return {
|
|
378
428
|
trigger: {
|
|
@@ -383,17 +433,7 @@ export async function handlePoll(
|
|
|
383
433
|
status: "in_progress",
|
|
384
434
|
attachments: attachmentsForTrigger(pendingTask.id),
|
|
385
435
|
},
|
|
386
|
-
...(
|
|
387
|
-
requestedBy: {
|
|
388
|
-
name: requestedByUser.name,
|
|
389
|
-
email: requestedByUser.email,
|
|
390
|
-
role: requestedByUser.role,
|
|
391
|
-
notes: requestedByNotes,
|
|
392
|
-
},
|
|
393
|
-
}),
|
|
394
|
-
...(requestedByUnknownName && {
|
|
395
|
-
requestedBy: { name: requestedByUnknownName },
|
|
396
|
-
}),
|
|
436
|
+
...(assignedRequestedBy && { requestedBy: assignedRequestedBy }),
|
|
397
437
|
},
|
|
398
438
|
};
|
|
399
439
|
}
|
|
@@ -499,16 +539,19 @@ export async function handlePoll(
|
|
|
499
539
|
for (const candidateId of unassignedIds) {
|
|
500
540
|
const claimed = claimTask(candidateId, myAgentId);
|
|
501
541
|
if (claimed) {
|
|
542
|
+
updateAgentStatusFromCapacity(myAgentId);
|
|
502
543
|
telemetry.taskEvent("claimed", {
|
|
503
544
|
taskId: claimed.id,
|
|
504
545
|
source: claimed.source,
|
|
505
546
|
agentId: myAgentId,
|
|
506
547
|
});
|
|
548
|
+
const claimedRequestedBy = buildTriggerRequestedBy(claimed);
|
|
507
549
|
return {
|
|
508
550
|
trigger: {
|
|
509
551
|
type: "task_assigned",
|
|
510
552
|
taskId: claimed.id,
|
|
511
553
|
task: { ...claimed, attachments: attachmentsForTrigger(claimed.id) },
|
|
554
|
+
...(claimedRequestedBy && { requestedBy: claimedRequestedBy }),
|
|
512
555
|
},
|
|
513
556
|
};
|
|
514
557
|
}
|
package/src/http/route-def.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import type { PermissionVerb } from "../rbac";
|
|
4
|
+
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
4
5
|
import { jsonError, matchRoute, parseBody } from "./utils";
|
|
5
6
|
|
|
6
7
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
@@ -25,6 +26,24 @@ type ResponsesDef = Record<number, RouteResponseDef>;
|
|
|
25
26
|
/** Extracts the payload type respond() accepts for one response def. */
|
|
26
27
|
type ResponseData<R> = R extends { schema: infer S extends z.ZodType } ? z.input<S> : never;
|
|
27
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Optional `X-Runtime-Instance-ID` header shared by the lifecycle and poll
|
|
31
|
+
* routes. Optional in the schema because the requirement is conditional:
|
|
32
|
+
* only multi-runtime mode needs it, and legacy deployments must keep working
|
|
33
|
+
* without it.
|
|
34
|
+
*/
|
|
35
|
+
export const runtimeInstanceHeader = (operation: string) =>
|
|
36
|
+
z.object({
|
|
37
|
+
"X-Runtime-Instance-ID": z
|
|
38
|
+
.string()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe(
|
|
41
|
+
"Identifies the concrete runtime instance (worker process) making the call, " +
|
|
42
|
+
`as generated at its boot. Required to ${operation} when multi-runtime mode ` +
|
|
43
|
+
"(MULTI_RUNTIME_ENABLED) is on; ignored otherwise.",
|
|
44
|
+
),
|
|
45
|
+
});
|
|
46
|
+
|
|
28
47
|
export interface RouteDef<
|
|
29
48
|
TParams extends z.ZodType = z.ZodType,
|
|
30
49
|
TQuery extends z.ZodType = z.ZodType,
|
|
@@ -41,6 +60,14 @@ export interface RouteDef<
|
|
|
41
60
|
tags: string[];
|
|
42
61
|
params?: TParams;
|
|
43
62
|
query?: TQuery;
|
|
63
|
+
/**
|
|
64
|
+
* Request headers to publish as OpenAPI parameters, beyond the global
|
|
65
|
+
* `Authorization`/`X-Agent-ID` security schemes. Declaration-only: handlers
|
|
66
|
+
* still read headers off `req`, so adding this to a route changes the
|
|
67
|
+
* generated spec and nothing else. Mark each property `.optional()` unless
|
|
68
|
+
* the header is unconditionally required.
|
|
69
|
+
*/
|
|
70
|
+
headers?: z.ZodObject;
|
|
44
71
|
body?: TBody;
|
|
45
72
|
responses: TResponses;
|
|
46
73
|
auth?: {
|
|
@@ -187,10 +214,14 @@ export function describeRequestRoute(
|
|
|
187
214
|
// ─── Factory ─────────────────────────────────────────────────────────────────
|
|
188
215
|
|
|
189
216
|
/**
|
|
190
|
-
* Runtime response validation gate for `respond()`.
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
217
|
+
* Runtime response validation gate for `respond()`. Only `bun test`
|
|
218
|
+
* (NODE_ENV=test) fails hard on a violation: one bad row must never take an
|
|
219
|
+
* endpoint down outside tests (2026-08-18 incident — a single priority='high'
|
|
220
|
+
* row 500'd every task listing). When the gate is on outside tests
|
|
221
|
+
* (NODE_ENV=development, or VALIDATE_HTTP_RESPONSES=true), `respond()` logs
|
|
222
|
+
* the violation and sends the response anyway. Note: the compiled Bun binary
|
|
223
|
+
* defaults NODE_ENV to "development", so deploys must also set
|
|
224
|
+
* NODE_ENV=production explicitly (see the deploy-env change in the fix PR).
|
|
194
225
|
*/
|
|
195
226
|
const VALIDATE_RESPONSES =
|
|
196
227
|
process.env.NODE_ENV === "test" ||
|
|
@@ -216,12 +247,15 @@ export function route<
|
|
|
216
247
|
if (schema) {
|
|
217
248
|
const result = schema.safeParse(data);
|
|
218
249
|
if (!result.success) {
|
|
219
|
-
|
|
250
|
+
const detail =
|
|
220
251
|
`Response schema violation: ${def.method.toUpperCase()} ${def.path} ${code} — ` +
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
)
|
|
252
|
+
result.error.issues
|
|
253
|
+
.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`)
|
|
254
|
+
.join("; ");
|
|
255
|
+
if (process.env.NODE_ENV === "test") {
|
|
256
|
+
throw new Error(detail);
|
|
257
|
+
}
|
|
258
|
+
console.error(`[route-def] ${scrubSecrets(detail)}`);
|
|
225
259
|
}
|
|
226
260
|
}
|
|
227
261
|
}
|
package/src/http/schedules.ts
CHANGED
|
@@ -40,7 +40,7 @@ const scheduleUpdateBodySchema = z.object({
|
|
|
40
40
|
taskTemplate: z.string().optional(),
|
|
41
41
|
taskType: z.string().optional(),
|
|
42
42
|
tags: z.array(z.string()).optional(),
|
|
43
|
-
priority: z.number().int().optional(),
|
|
43
|
+
priority: z.number().int().min(0).max(100).optional(),
|
|
44
44
|
targetAgentId: z.string().nullable().optional(),
|
|
45
45
|
enabled: z.boolean().optional(),
|
|
46
46
|
timezone: z.string().optional(),
|
|
@@ -96,7 +96,7 @@ const createSchedule = route({
|
|
|
96
96
|
taskTemplate: z.string().min(1).optional(),
|
|
97
97
|
taskType: z.string().optional(),
|
|
98
98
|
tags: z.array(z.string()).optional(),
|
|
99
|
-
priority: z.number().int().optional(),
|
|
99
|
+
priority: z.number().int().min(0).max(100).optional(),
|
|
100
100
|
targetAgentId: z.string().optional(),
|
|
101
101
|
enabled: z.boolean().optional(),
|
|
102
102
|
timezone: z.string().optional(),
|