@desplega.ai/agent-swarm 1.113.0 → 1.115.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/README.md +2 -2
- package/dist/{actions-q4n7cz6e.js → actions-17ya5wde.js} +8 -9
- package/dist/{app-es4nzc71.js → app-4f718yk0.js} +6 -6
- package/dist/{assistant-0x8ey0vs.js → assistant-ah7csnjz.js} +19 -18
- package/dist/{boot-reembed-69agkhv6.js → boot-reembed-4jpxhb25.js} +6 -6
- package/dist/{boot-reembed-p0ny05t8.js → boot-reembed-qata34xa.js} +5 -5
- package/dist/{boot-scrub-logs-yybrkmvr.js → boot-scrub-logs-tv48nn3z.js} +4 -4
- package/dist/{claude-adapter-mma9a1yr.js → claude-adapter-3csn5bks.js} +3 -3
- package/dist/{claude-managed-adapter-pv2nbqy4.js → claude-managed-adapter-kysmkz89.js} +2 -2
- package/dist/cli-0pygq9r1.js +429 -0
- package/dist/{cli-hsetkkd3.js → cli-12vb0xg6.js} +33 -9
- package/dist/{cli-ncfgsqbd.js → cli-15y7qhzp.js} +262 -33
- package/dist/{cli-7ga43spr.js → cli-1yjrbg1f.js} +1 -1
- package/dist/{cli-36ymmpmp.js → cli-3samkyap.js} +2 -2
- package/dist/cli-52fg7s7x.js +269 -0
- package/dist/{cli-hjhvekf4.js → cli-65ekje01.js} +11 -8
- package/dist/{cli-h3k622d0.js → cli-6aq8xhvm.js} +90 -3
- package/dist/{cli-f3qa069v.js → cli-7xje5amz.js} +1 -1
- package/dist/cli-86fdnerz.js +405 -0
- package/dist/{cli-9cy6nc5m.js → cli-8fjwym53.js} +1 -1
- package/dist/{cli-c3frqf65.js → cli-b23axgz4.js} +3 -3
- package/dist/{cli-a0t7vapf.js → cli-bc89my0z.js} +1 -1
- package/dist/{cli-bnzbn8j8.js → cli-beea4a3y.js} +73 -24
- package/dist/{cli-q21d49ac.js → cli-bw80ck94.js} +1 -1
- package/dist/{cli-pwc5e83c.js → cli-cchksf5q.js} +12 -7
- package/dist/{cli-30bbaveh.js → cli-cmw9rzy5.js} +2 -2
- package/dist/{cli-y6mrptcn.js → cli-cnwssmsr.js} +1 -1
- package/dist/{cli-r5g6ngtn.js → cli-dcca6j8m.js} +46 -5
- package/dist/{cli-tkmm5nf9.js → cli-e61fs2hp.js} +59 -2
- package/dist/{cli-6wscj9z0.js → cli-e7306swv.js} +14 -14
- package/dist/{cli-dcfyyh3t.js → cli-ftyf58xj.js} +2 -2
- package/dist/{cli-4xyj9jtq.js → cli-hrhye2rm.js} +1062 -478
- package/dist/cli-jkhwmmgg.js +136 -0
- package/dist/{cli-y4ycrnjc.js → cli-k3y4z3h5.js} +1 -1
- package/dist/{cli-6xd0bvf8.js → cli-ma04faw5.js} +1 -1
- package/dist/{cli-2fc97xm5.js → cli-mnbgv3w7.js} +4 -4
- package/dist/{cli-trmapn9k.js → cli-pet3aev6.js} +95 -10
- package/dist/{cli-z46pnksz.js → cli-q1rx7g0d.js} +175 -28
- package/dist/{cli-qwgtacd6.js → cli-r9f6ngrf.js} +1244 -336
- package/dist/{cli-gtkqc144.js → cli-shvaxw27.js} +13 -7
- package/dist/{cli-ywez6yps.js → cli-sq7f72d6.js} +1 -1
- package/dist/cli-t0vqevnm.js +393 -0
- package/dist/{cli-tbw13sx8.js → cli-tece6zgm.js} +1 -1
- package/dist/{cli-bgvskydy.js → cli-tffvrh6j.js} +1 -1
- package/dist/cli-wh91zby6.js +187222 -0
- package/dist/{cli-anrj584m.js → cli-xz9aq0rf.js} +1 -1
- package/dist/{cli-mq580e06.js → cli-zsrdty74.js} +18 -18
- package/dist/cli.js +32 -15
- package/dist/{codex-adapter-5cmy858w.js → codex-adapter-de3w0nch.js} +10 -10
- package/dist/{codex-session-runner-46q8mrba.js → codex-session-runner-bnw8cysk.js} +10 -10
- package/dist/{commands-z5dbwta3.js → commands-hch4e2k1.js} +4 -4
- package/dist/{db-bbgahh4z.js → db-p3h2j06x.js} +14 -4
- package/dist/{handlers-6b44317z.js → handlers-9er4zx9q.js} +17 -31
- package/dist/{hook-sbp5fmps.js → hook-4axs756z.js} +7 -7
- package/dist/{http-2xz43gz3.js → http-809fvtz2.js} +1934 -886
- package/dist/{index-wz1ea15p.js → index-2dgfj9av.js} +3 -3
- package/dist/{index-nhat35em.js → index-47em3d2n.js} +14 -11
- package/dist/{index-5ghxn8s6.js → index-9fqaktjr.js} +12 -9
- package/dist/{index-0gzmqj4k.js → index-9ht8gr56.js} +13 -10
- package/dist/{index-vfedgdw6.js → index-rm85cfdx.js} +368 -40
- package/dist/{index-ykvs43z4.js → index-t2snazez.js} +4 -4
- package/dist/{keepalive-62gfj91d.js → keepalive-3cps6e3e.js} +7 -6
- package/dist/{lead-ag0akn0v.js → lead-fkk95wgq.js} +36 -32
- package/dist/{maintenance-xxrrzk5x.js → maintenance-fweqh0yj.js} +6 -6
- package/dist/{mistral-conversations-26yrnbf1.js → mistral-conversations-7gqnz2wt.js} +7 -7
- package/dist/oauth-refresh-sweep-sycqtbph.js +114 -0
- package/dist/{onboard-64zyfcbs.js → onboard-cy0rkgvx.js} +2 -2
- package/dist/{opencode-adapter-mqsxr170.js → opencode-adapter-p6bxnbm4.js} +2 -2
- package/dist/{otel-zjd9zaxs.js → otel-g299k41b.js} +2 -2
- package/dist/{otel-impl-b86qseaa.js → otel-impl-wbns8hf7.js} +1 -1
- package/dist/{pi-mono-adapter-kxjxpfgy.js → pi-mono-adapter-05sck1ds.js} +17 -109
- package/dist/{pricing-refresh-cgtkffpa.js → pricing-refresh-0hvazmkf.js} +6 -6
- package/dist/rbac-roles-kfesjy9y.js +28 -0
- package/dist/rbac-roles-mwcwhbxc.js +283 -0
- package/dist/{seed-pricing-1r1y1xkq.js → seed-pricing-h2kdq4cq.js} +5 -5
- package/dist/{setup-mkcgkjf7.js → setup-8506q97w.js} +2 -2
- package/dist/{worker-351sze63.js → worker-4r5c6zyb.js} +36 -32
- package/openapi.json +1327 -20
- package/package.json +3 -3
- package/src/agentmail/handlers.ts +16 -6
- package/src/agentmail/templates.ts +25 -5
- package/src/be/audit-user.ts +28 -4
- package/src/be/db-queries/oauth.ts +42 -0
- package/src/be/db.ts +314 -28
- package/src/be/identity.ts +79 -0
- package/src/be/mcp-proxy.ts +132 -0
- package/src/be/migrations/109_rbac_roles.sql +79 -0
- package/src/be/migrations/111_oauth_credential_bindings.sql +6 -0
- package/src/be/migrations/112_script_connections_graphql.sql +96 -0
- package/src/be/migrations/113_task_routing_affinity.sql +9 -0
- package/src/be/migrations/114_backfill_gpt_5_6_pricing.sql +15 -0
- package/src/be/modelsdev-cache.json +135 -0
- package/src/be/oauth-credential-bindings.ts +35 -0
- package/src/be/oauth-refresh-sweep.ts +159 -0
- package/src/be/rbac-audit.ts +52 -13
- package/src/be/rbac-roles.ts +424 -0
- package/src/be/script-connections.ts +459 -40
- package/src/be/script-credential-broker.ts +29 -3
- package/src/be/scripts/typecheck.ts +51 -7
- package/src/be/seed-scripts/catalog/catalog-report.inline.ts +325 -24
- package/src/be/seed-scripts/catalog/catalog-report.ts +325 -24
- package/src/be/users.ts +24 -0
- package/src/cli.tsx +17 -0
- package/src/github/handlers.ts +10 -9
- package/src/github/templates.ts +45 -9
- package/src/gitlab/handlers.ts +20 -8
- package/src/gitlab/templates.ts +18 -6
- package/src/heartbeat/heartbeat.ts +143 -21
- package/src/http/all-routes.ts +3 -0
- package/src/http/approval-requests.ts +12 -0
- package/src/http/config.ts +44 -10
- package/src/http/core.ts +30 -3
- package/src/http/favorites.ts +1 -5
- package/src/http/index.ts +25 -1
- package/src/http/mcp-oauth.ts +2 -0
- package/src/http/mcp-servers.ts +44 -3
- package/src/http/oauth-generic.ts +105 -0
- package/src/http/poll.ts +20 -3
- package/src/http/script-connection-proxy.ts +116 -0
- package/src/http/script-connections.ts +1657 -0
- package/src/http/scripts.ts +27 -7
- package/src/http/skills.ts +11 -0
- package/src/http/tasks.ts +27 -13
- package/src/http/x.ts +6 -2
- package/src/integrations/kapso/inbound.ts +23 -6
- package/src/jira/sync.ts +111 -17
- package/src/jira/templates.ts +10 -2
- package/src/mcp-client/http-client.ts +194 -0
- package/src/oauth/app-validation.ts +16 -0
- package/src/oauth/ensure-token.ts +69 -14
- package/src/oauth/mcp-wrapper.ts +15 -0
- package/src/oauth/wrapper.ts +48 -20
- package/src/prompts/session-templates.ts +3 -3
- package/src/providers/codex-models.ts +100 -30
- package/src/providers/modelsdev-reasoning.json +51 -0
- package/src/providers/pi-mono-mcp-client.ts +2 -133
- package/src/providers/reasoning-effort.ts +11 -5
- package/src/rbac/admission.ts +68 -0
- package/src/rbac/index.ts +2 -0
- package/src/rbac/legacy-policy.ts +12 -0
- package/src/rbac/permissions.ts +48 -0
- package/src/scheduler/scheduler.ts +9 -1
- package/src/scripts-runtime/api-client.ts +151 -19
- package/src/scripts-runtime/api-types.ts +38 -6
- package/src/scripts-runtime/credential-broker/default-bindings.ts +1 -0
- package/src/scripts-runtime/credential-broker/types.ts +8 -0
- package/src/scripts-runtime/ctx.ts +11 -1
- package/src/scripts-runtime/eval-harness.ts +5 -1
- package/src/scripts-runtime/executors/types.ts +2 -1
- package/src/scripts-runtime/loader.ts +3 -1
- package/src/scripts-runtime/mcp-client.ts +87 -0
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +6 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +19 -0
- package/src/server-user.ts +114 -59
- package/src/server.ts +12 -0
- package/src/slack/assistant.ts +14 -6
- package/src/slack/enrich.ts +17 -0
- package/src/slack/handlers.ts +8 -22
- package/src/slack/responses.ts +91 -1
- package/src/slack/thread-buffer.ts +6 -3
- package/src/slack/watcher.ts +9 -6
- package/src/tasks/worker-follow-up.ts +100 -3
- package/src/tests/agentmail-handlers.test.ts +32 -3
- package/src/tests/approval-requests.test.ts +61 -0
- package/src/tests/audit-user.test.ts +47 -1
- package/src/tests/codex-adapter.test.ts +46 -23
- package/src/tests/credential-broker.test.ts +5 -2
- package/src/tests/delete-page-tool.test.ts +197 -0
- package/src/tests/github-handlers.test.ts +28 -2
- package/src/tests/gitlab-handlers.test.ts +125 -0
- package/src/tests/heartbeat.test.ts +48 -3
- package/src/tests/identity.test.ts +117 -0
- package/src/tests/jira-sync.test.ts +99 -1
- package/src/tests/kapso-inbound.test.ts +7 -0
- package/src/tests/mcp-tools-user.test.ts +60 -6
- package/src/tests/model-control.test.ts +8 -2
- package/src/tests/oauth-credential-bindings.test.ts +490 -0
- package/src/tests/oauth-refresh-sweep.test.ts +160 -0
- package/src/tests/pool-affinity.test.ts +401 -0
- package/src/tests/prompt-template-remaining.test.ts +2 -2
- package/src/tests/rbac-admission-e2e.test.ts +386 -0
- package/src/tests/rbac-admission.test.ts +492 -0
- package/src/tests/rbac-e2e-helpers.ts +3 -0
- package/src/tests/rbac-engine.test.ts +50 -0
- package/src/tests/rbac-mcp-admission-e2e.test.ts +362 -0
- package/src/tests/rbac-roles.test.ts +328 -0
- package/src/tests/reasoning-effort.test.ts +15 -2
- package/src/tests/schedule-target-type.test.ts +50 -0
- package/src/tests/scheduled-tasks.test.ts +40 -0
- package/src/tests/script-apis-mcp.test.ts +37 -13
- package/src/tests/script-connections-http.test.ts +1158 -0
- package/src/tests/script-connections-mcp.test.ts +894 -0
- package/src/tests/script-connections.test.ts +754 -6
- package/src/tests/scripts-external-api.test.ts +49 -1
- package/src/tests/scripts-runtime-secret-egress.test.ts +1 -0
- package/src/tests/scripts-runtime.test.ts +291 -0
- package/src/tests/seed-scripts.test.ts +36 -0
- package/src/tests/slack-identity-resolution.test.ts +37 -3
- package/src/tests/slack-inline-output.test.ts +65 -0
- package/src/tests/slack-thread-buffer.test.ts +28 -0
- package/src/tests/tool-annotations.test.ts +1 -0
- package/src/tests/user-token-rest-auth.test.ts +140 -1
- package/src/tests/workflow-swarm-script.test.ts +44 -0
- package/src/tests/workflow-triggers-v2.test.ts +300 -1
- package/src/tools/credential-bindings/tool.ts +277 -18
- package/src/tools/delete-page.ts +145 -0
- package/src/tools/mcp-servers/mcp-server-delete.ts +7 -4
- package/src/tools/request-human-input.ts +2 -0
- package/src/tools/resolve-user.ts +77 -27
- package/src/tools/script-apis.ts +62 -3
- package/src/tools/script-connections/tool.ts +275 -31
- package/src/tools/send-task.ts +15 -1
- package/src/tools/task-action.ts +34 -2
- package/src/tools/templates.ts +53 -1
- package/src/tools/tool-config.ts +2 -1
- package/src/tools/utils.ts +2 -0
- package/src/tools/workflows/create-workflow.ts +8 -1
- package/src/tools/workflows/update-workflow.ts +11 -2
- package/src/types.ts +90 -11
- package/src/utils/scoped-resource.ts +25 -0
- package/src/workflows/executors/human-in-the-loop.ts +1 -0
- package/src/workflows/executors/swarm-script.ts +7 -1
- package/src/workflows/triggers.ts +146 -18
- package/templates/skills/swarm-scripts/content.md +33 -8
- package/dist/cli-bgef578c.js +0 -771
- package/dist/cli-c9mtm09b.js +0 -43
- package/dist/cli-ceewqajw.js +0 -1698
- package/dist/{anthropic-messages-8nc6r2dw.js → anthropic-messages-kq3pjhbp.js} +6 -6
- package/dist/{azure-openai-responses-6yfzw3qw.js → azure-openai-responses-gm1ejtmn.js} +7 -7
- package/dist/{cli-vbraamxr.js → cli-6b4wwmm8.js} +9 -9
- package/dist/{cli-2r7s8jhp.js → cli-sznberje.js} +3 -3
- package/dist/{google-generative-ai-vfq6ymz9.js → google-generative-ai-hyrra2jm.js} +3 -3
- package/dist/{google-vertex-mj5y5rfj.js → google-vertex-8bs2pzgn.js} +3 -3
- package/dist/{openai-codex-responses-8h6kvfv8.js → openai-codex-responses-3nkvje79.js} +4 -4
- package/dist/{openai-completions-x6kjnqjd.js → openai-completions-0asftkwg.js} +11 -11
- package/dist/{openai-responses-7jwyz3ds.js → openai-responses-pmatek45.js} +10 -10
package/src/http/config.ts
CHANGED
|
@@ -10,12 +10,13 @@ import {
|
|
|
10
10
|
maskSecrets,
|
|
11
11
|
upsertSwarmConfig,
|
|
12
12
|
} from "../be/db";
|
|
13
|
+
import { getUserGrant } from "../be/rbac-roles";
|
|
13
14
|
import {
|
|
14
15
|
isReservedConfigKey,
|
|
15
16
|
reservedKeyError,
|
|
16
17
|
validateConfigValue,
|
|
17
18
|
} from "../be/swarm-config-guard";
|
|
18
|
-
import { can, type PermissionVerb } from "../rbac";
|
|
19
|
+
import { can, isRbacEnabled, type PermissionVerb } from "../rbac";
|
|
19
20
|
import { getRequestAuth } from "../utils/request-auth-context";
|
|
20
21
|
import { registerVolatileSecret } from "../utils/secret-scrubber";
|
|
21
22
|
import { reloadGlobalConfigsAndIntegrations, scheduleIntegrationsReload } from "./core";
|
|
@@ -23,6 +24,8 @@ import { route } from "./route-def";
|
|
|
23
24
|
import { json, jsonError } from "./utils";
|
|
24
25
|
|
|
25
26
|
const MAX_ENV_PRESENCE_KEYS = 200;
|
|
27
|
+
const SECRETS_FORCE_MASK_NOTE =
|
|
28
|
+
" (secret values masked: reading unmasked secrets requires the lead agent)";
|
|
26
29
|
|
|
27
30
|
// Config keys the API server owns for its own use and must NEVER hand out over
|
|
28
31
|
// HTTP — workers/entrypoints read config to build their env, and the agent-fs
|
|
@@ -40,6 +43,25 @@ function singleHeader(req: IncomingMessage, name: string): string | undefined {
|
|
|
40
43
|
return Array.isArray(raw) ? raw[0] : raw;
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
function userMayReadSecrets(req: IncomingMessage): boolean {
|
|
47
|
+
const auth = getRequestAuth(req);
|
|
48
|
+
if (auth?.kind === "operator") return true;
|
|
49
|
+
// Agent/no-user HTTP reads keep their pre-RBAC behavior; MCP get-config has
|
|
50
|
+
// the per-agent config.read.secrets gate.
|
|
51
|
+
if (auth?.kind !== "user") return true;
|
|
52
|
+
if (!isRbacEnabled()) return true;
|
|
53
|
+
|
|
54
|
+
const grant = getUserGrant(auth.userId);
|
|
55
|
+
return grant.grantsAll || grant.verbs.has("config.read.secrets");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveSecretsRead(req: IncomingMessage, includeSecrets: boolean) {
|
|
59
|
+
if (!includeSecrets || userMayReadSecrets(req)) {
|
|
60
|
+
return { effectiveIncludeSecrets: includeSecrets, secretsNote: "" };
|
|
61
|
+
}
|
|
62
|
+
return { effectiveIncludeSecrets: false, secretsNote: SECRETS_FORCE_MASK_NOTE };
|
|
63
|
+
}
|
|
64
|
+
|
|
43
65
|
/**
|
|
44
66
|
* Gate a config write/delete over HTTP (DES-445 follow-up).
|
|
45
67
|
*
|
|
@@ -213,18 +235,22 @@ export async function handleConfig(
|
|
|
213
235
|
const parsed = await getResolvedConfigRoute.parse(req, res, pathSegments, queryParams);
|
|
214
236
|
if (!parsed) return true;
|
|
215
237
|
const includeSecrets = parsed.query.includeSecrets === "true";
|
|
238
|
+
const { effectiveIncludeSecrets, secretsNote } = resolveSecretsRead(req, includeSecrets);
|
|
216
239
|
const configs = stripApiOnlyKeys(
|
|
217
240
|
getResolvedConfig(parsed.query.agentId || undefined, parsed.query.repoId || undefined),
|
|
218
241
|
);
|
|
219
|
-
const result =
|
|
220
|
-
if (
|
|
242
|
+
const result = effectiveIncludeSecrets ? configs : maskSecrets(configs);
|
|
243
|
+
if (effectiveIncludeSecrets) {
|
|
221
244
|
for (const c of result) {
|
|
222
245
|
if (c.isSecret && c.value) {
|
|
223
246
|
registerVolatileSecret(c.value, `config:${c.key}`);
|
|
224
247
|
}
|
|
225
248
|
}
|
|
226
249
|
}
|
|
227
|
-
json(res, {
|
|
250
|
+
json(res, {
|
|
251
|
+
configs: result,
|
|
252
|
+
...(secretsNote ? { message: `Found ${result.length} config(s).${secretsNote}` } : {}),
|
|
253
|
+
});
|
|
228
254
|
return true;
|
|
229
255
|
}
|
|
230
256
|
|
|
@@ -271,11 +297,15 @@ export async function handleConfig(
|
|
|
271
297
|
jsonError(res, "Config not found", 404);
|
|
272
298
|
return true;
|
|
273
299
|
}
|
|
274
|
-
const
|
|
275
|
-
|
|
300
|
+
const { effectiveIncludeSecrets, secretsNote } = resolveSecretsRead(req, includeSecrets);
|
|
301
|
+
const singleResult = effectiveIncludeSecrets ? config : maskSecrets([config])[0]!;
|
|
302
|
+
if (effectiveIncludeSecrets && singleResult.isSecret && singleResult.value) {
|
|
276
303
|
registerVolatileSecret(singleResult.value, `config:${singleResult.key}`);
|
|
277
304
|
}
|
|
278
|
-
json(res,
|
|
305
|
+
json(res, {
|
|
306
|
+
...singleResult,
|
|
307
|
+
...(secretsNote ? { message: `Found 1 config(s).${secretsNote}` } : {}),
|
|
308
|
+
});
|
|
279
309
|
return true;
|
|
280
310
|
}
|
|
281
311
|
|
|
@@ -283,21 +313,25 @@ export async function handleConfig(
|
|
|
283
313
|
const parsed = await listConfig.parse(req, res, pathSegments, queryParams);
|
|
284
314
|
if (!parsed) return true;
|
|
285
315
|
const includeSecrets = parsed.query.includeSecrets === "true";
|
|
316
|
+
const { effectiveIncludeSecrets, secretsNote } = resolveSecretsRead(req, includeSecrets);
|
|
286
317
|
const configs = stripApiOnlyKeys(
|
|
287
318
|
getSwarmConfigs({
|
|
288
319
|
scope: parsed.query.scope || undefined,
|
|
289
320
|
scopeId: parsed.query.scopeId || undefined,
|
|
290
321
|
}),
|
|
291
322
|
);
|
|
292
|
-
const listResult =
|
|
293
|
-
if (
|
|
323
|
+
const listResult = effectiveIncludeSecrets ? configs : maskSecrets(configs);
|
|
324
|
+
if (effectiveIncludeSecrets) {
|
|
294
325
|
for (const c of listResult) {
|
|
295
326
|
if (c.isSecret && c.value) {
|
|
296
327
|
registerVolatileSecret(c.value, `config:${c.key}`);
|
|
297
328
|
}
|
|
298
329
|
}
|
|
299
330
|
}
|
|
300
|
-
json(res, {
|
|
331
|
+
json(res, {
|
|
332
|
+
configs: listResult,
|
|
333
|
+
...(secretsNote ? { message: `Found ${listResult.length} config(s).${secretsNote}` } : {}),
|
|
334
|
+
});
|
|
301
335
|
return true;
|
|
302
336
|
}
|
|
303
337
|
|
package/src/http/core.ts
CHANGED
|
@@ -10,17 +10,20 @@ import {
|
|
|
10
10
|
shouldBlockPolling,
|
|
11
11
|
updateAgentStatus,
|
|
12
12
|
} from "../be/db";
|
|
13
|
+
import { enqueueAdmissionRow } from "../be/rbac-audit";
|
|
14
|
+
import { getUserGrant } from "../be/rbac-roles";
|
|
13
15
|
import { initGitHub, resetGitHub } from "../github";
|
|
14
16
|
import { initJira, resetJira } from "../jira";
|
|
15
17
|
import { initLinear, resetLinear } from "../linear";
|
|
18
|
+
import { decideAdmission, isRbacEnabled } from "../rbac";
|
|
16
19
|
import { startSlackApp, stopSlackApp } from "../slack";
|
|
17
20
|
import type { AgentStatus } from "../types";
|
|
18
21
|
import { setRequestAuth } from "../utils/request-auth-context";
|
|
19
22
|
import { refreshSecretScrubberCache } from "../utils/secret-scrubber";
|
|
20
23
|
import { resolveHttpRequestAuth } from "./auth";
|
|
21
24
|
import { generateOpenApiSpec, SCALAR_HTML } from "./openapi";
|
|
22
|
-
import { isPublicRoute } from "./route-def";
|
|
23
|
-
import { agentWithCapacity, getPathSegments, parseQueryParams } from "./utils";
|
|
25
|
+
import { findRoute, isPublicRoute } from "./route-def";
|
|
26
|
+
import { agentWithCapacity, getPathSegments, jsonError, parseQueryParams } from "./utils";
|
|
24
27
|
|
|
25
28
|
/**
|
|
26
29
|
* Load global swarm_config entries into process.env.
|
|
@@ -243,12 +246,13 @@ export async function handleCore(
|
|
|
243
246
|
// either the global swarm key or an active user-bound `aswt_` token.
|
|
244
247
|
const pathSegments = getPathSegments(req.url || "");
|
|
245
248
|
const isUserMcpRoute = req.url === "/mcp-user";
|
|
249
|
+
let auth = null as ReturnType<typeof resolveHttpRequestAuth>;
|
|
246
250
|
// `/mcp-user` runs its own `aswt_`-token auth in `handleMcpUser`; the swarm
|
|
247
251
|
// API key must not gate it.
|
|
248
252
|
if (isUserMcpRoute || isPublicRoute(req.method, pathSegments)) {
|
|
249
253
|
setRequestAuth(req, null);
|
|
250
254
|
} else {
|
|
251
|
-
|
|
255
|
+
auth = resolveHttpRequestAuth(req, apiKey);
|
|
252
256
|
|
|
253
257
|
if (!auth) {
|
|
254
258
|
setRequestAuth(req, null);
|
|
@@ -259,6 +263,29 @@ export async function handleCore(
|
|
|
259
263
|
setRequestAuth(req, auth);
|
|
260
264
|
}
|
|
261
265
|
|
|
266
|
+
if (auth?.kind === "user" && isRbacEnabled()) {
|
|
267
|
+
const grant = getUserGrant(auth.userId);
|
|
268
|
+
if (!grant.grantsAll) {
|
|
269
|
+
const def = findRoute(req.method, pathSegments);
|
|
270
|
+
const decision = decideAdmission({
|
|
271
|
+
method: req.method ?? "",
|
|
272
|
+
rbac: def?.rbac,
|
|
273
|
+
routeKnown: def !== undefined,
|
|
274
|
+
grant,
|
|
275
|
+
});
|
|
276
|
+
enqueueAdmissionRow({
|
|
277
|
+
userId: auth.userId,
|
|
278
|
+
decision,
|
|
279
|
+
method: req.method,
|
|
280
|
+
route: def?.path ?? pathSegments.join("/"),
|
|
281
|
+
});
|
|
282
|
+
if (!decision.allow) {
|
|
283
|
+
jsonError(res, `Forbidden: ${decision.reason}`, 403);
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
262
289
|
// POST /internal/reload-config — re-read swarm_config into process.env and re-init integrations
|
|
263
290
|
if (req.method === "POST" && req.url === "/internal/reload-config") {
|
|
264
291
|
try {
|
package/src/http/favorites.ts
CHANGED
|
@@ -28,11 +28,7 @@ const putFavorite = route({
|
|
|
28
28
|
pattern: ["api", "favorites"],
|
|
29
29
|
summary: "Set favorite state for an item",
|
|
30
30
|
tags: ["Favorites"],
|
|
31
|
-
|
|
32
|
-
// (resolveHttpAuditUserId), never a body-supplied id, so there is no
|
|
33
|
-
// cross-principal authorization to gate — a user can only favorite for
|
|
34
|
-
// themselves.
|
|
35
|
-
rbac: { ungated: "self-scoped to the authenticated user; no cross-principal access" },
|
|
31
|
+
rbac: { permission: "favorite.write.own" },
|
|
36
32
|
body: z.object({
|
|
37
33
|
itemType: FavoriteItemTypeSchema,
|
|
38
34
|
itemId: z.string().min(1),
|
package/src/http/index.ts
CHANGED
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
withRemoteContext,
|
|
30
30
|
withSpanContext,
|
|
31
31
|
} from "../otel";
|
|
32
|
-
import { clearAuditSink, setAuditSink } from "../rbac";
|
|
32
|
+
import { clearAuditSink, isRbacEnabled, setAuditSink } from "../rbac";
|
|
33
33
|
import { startScriptRunSupervisor, stopScriptRunSupervisor } from "../script-workflows/supervisor";
|
|
34
34
|
import { getServerSessionsProcessed } from "../server-runtime-counters";
|
|
35
35
|
import { startSlackApp, stopSlackApp } from "../slack";
|
|
@@ -69,6 +69,7 @@ import { handleMcpServers } from "./mcp-servers";
|
|
|
69
69
|
import { closeIdleMcpUserTransports, handleMcpUser } from "./mcp-user";
|
|
70
70
|
import { handleMemory, startMemoryGc, stopMemoryGc } from "./memory";
|
|
71
71
|
import { handleMetrics } from "./metrics";
|
|
72
|
+
import { handleGenericOAuth } from "./oauth-generic";
|
|
72
73
|
import { handleOAuthLocks } from "./oauth-locks";
|
|
73
74
|
import { handlePageProxy } from "./page-proxy";
|
|
74
75
|
import { handlePages } from "./pages";
|
|
@@ -79,6 +80,8 @@ import { handlePromptTemplates } from "./prompt-templates";
|
|
|
79
80
|
import { handleRepos } from "./repos";
|
|
80
81
|
import { describeRequestRoute } from "./route-def";
|
|
81
82
|
import { handleSchedules } from "./schedules";
|
|
83
|
+
import { handleScriptConnectionProxy } from "./script-connection-proxy";
|
|
84
|
+
import { handleScriptConnections } from "./script-connections";
|
|
82
85
|
import { handleScriptRuns } from "./script-runs";
|
|
83
86
|
import { handleScripts } from "./scripts";
|
|
84
87
|
import { handleSessionData } from "./session-data";
|
|
@@ -323,6 +326,8 @@ const httpServer = createHttpServer(async (req, res) => {
|
|
|
323
326
|
() => handleMetrics(req, res, pathSegments, queryParams, myAgentId),
|
|
324
327
|
() => handleRepos(req, res, pathSegments, queryParams),
|
|
325
328
|
() => handleSkills(req, res, pathSegments, queryParams, myAgentId),
|
|
329
|
+
() => handleScriptConnections(req, res, pathSegments, queryParams, myAgentId),
|
|
330
|
+
() => handleScriptConnectionProxy(req, res, pathSegments, queryParams, myAgentId),
|
|
326
331
|
() => handleScriptRuns(req, res, pathSegments, queryParams, myAgentId),
|
|
327
332
|
() => handleScripts(req, res, pathSegments, queryParams, myAgentId),
|
|
328
333
|
() => handleX(req, res, pathSegments),
|
|
@@ -331,6 +336,7 @@ const httpServer = createHttpServer(async (req, res) => {
|
|
|
331
336
|
() => handleMcpOAuth(req, res, pathSegments, queryParams),
|
|
332
337
|
() => handleMemory(req, res, pathSegments, myAgentId),
|
|
333
338
|
() => handleOAuthLocks(req, res, pathSegments, queryParams),
|
|
339
|
+
() => handleGenericOAuth(req, res, pathSegments, queryParams),
|
|
334
340
|
() => handleCodexOAuthKeepWarm(req, res, pathSegments),
|
|
335
341
|
() => handlePagesPublic(req, res, pathSegments, queryParams),
|
|
336
342
|
() => handlePageProxy(req, res),
|
|
@@ -504,6 +510,16 @@ try {
|
|
|
504
510
|
console.error("[startup] Failed to seed pricing rows:", err);
|
|
505
511
|
}
|
|
506
512
|
|
|
513
|
+
try {
|
|
514
|
+
const { ensureRbacSeedsSynced } = await import("../be/rbac-roles");
|
|
515
|
+
ensureRbacSeedsSynced();
|
|
516
|
+
} catch (err) {
|
|
517
|
+
console.error("[startup] Failed to sync RBAC seed rows:", err);
|
|
518
|
+
// RBAC flag-on must fail closed; flag-off deployments should not be bricked
|
|
519
|
+
// by role-catalog drift for a disabled security feature.
|
|
520
|
+
if (isRbacEnabled()) throw err;
|
|
521
|
+
}
|
|
522
|
+
|
|
507
523
|
// Seed the built-in entity catalog (scripts today; more kinds later) so
|
|
508
524
|
// `script-search` & co. return useful hits from a fresh DB. Idempotent and
|
|
509
525
|
// version-aware: a pristine entity updates when its source changes, a
|
|
@@ -611,6 +627,14 @@ httpServer
|
|
|
611
627
|
startOAuthKeepalive();
|
|
612
628
|
}
|
|
613
629
|
|
|
630
|
+
// Start generic OAuth token refresh sweep (all oauth_apps providers,
|
|
631
|
+
// 15-min tick, first run ~1 min after boot). Complements the tracker-only
|
|
632
|
+
// keepalive above — see src/be/oauth-refresh-sweep.ts.
|
|
633
|
+
if (process.env.OAUTH_REFRESH_SWEEP_DISABLE !== "true") {
|
|
634
|
+
const { startOAuthRefreshSweep } = await import("../be/oauth-refresh-sweep");
|
|
635
|
+
startOAuthRefreshSweep();
|
|
636
|
+
}
|
|
637
|
+
|
|
614
638
|
// Start MCP OAuth pending-session garbage collector (5-min tick)
|
|
615
639
|
startMcpOAuthPendingGc();
|
|
616
640
|
|
package/src/http/mcp-oauth.ts
CHANGED
|
@@ -192,6 +192,7 @@ const authorizeRoute = route({
|
|
|
192
192
|
userId: z.string().optional(),
|
|
193
193
|
scopes: z.string().optional(),
|
|
194
194
|
}),
|
|
195
|
+
rbac: { permission: "mcp-oauth.authorize.any" },
|
|
195
196
|
responses: {
|
|
196
197
|
302: { description: "Redirect to authorization server" },
|
|
197
198
|
400: { description: "MCP has no URL / does not require OAuth" },
|
|
@@ -213,6 +214,7 @@ const authorizeUrlRoute = route({
|
|
|
213
214
|
userId: z.string().optional(),
|
|
214
215
|
scopes: z.string().optional(),
|
|
215
216
|
}),
|
|
217
|
+
rbac: { permission: "mcp-oauth.authorize.any" },
|
|
216
218
|
responses: {
|
|
217
219
|
200: { description: "{ providerUrl: string }" },
|
|
218
220
|
400: { description: "MCP has no URL / does not require OAuth" },
|
package/src/http/mcp-servers.ts
CHANGED
|
@@ -11,7 +11,11 @@ import {
|
|
|
11
11
|
uninstallMcpServer,
|
|
12
12
|
updateMcpServer,
|
|
13
13
|
} from "../be/db";
|
|
14
|
+
import { enqueueAdmissionRow } from "../be/rbac-audit";
|
|
15
|
+
import { getUserGrant } from "../be/rbac-roles";
|
|
14
16
|
import { ensureMcpToken } from "../oauth/ensure-mcp-token";
|
|
17
|
+
import { isRbacEnabled, type PermissionVerb } from "../rbac";
|
|
18
|
+
import { getRequestAuth } from "../utils/request-auth-context";
|
|
15
19
|
import { route } from "./route-def";
|
|
16
20
|
import { json, jsonError } from "./utils";
|
|
17
21
|
|
|
@@ -74,6 +78,7 @@ const createMcpServerRoute = route({
|
|
|
74
78
|
201: { description: "MCP server created" },
|
|
75
79
|
400: { description: "Validation error" },
|
|
76
80
|
},
|
|
81
|
+
rbac: { permission: "mcp-server.create.swarm" },
|
|
77
82
|
});
|
|
78
83
|
|
|
79
84
|
const updateMcpServerRoute = route({
|
|
@@ -89,6 +94,7 @@ const updateMcpServerRoute = route({
|
|
|
89
94
|
200: { description: "MCP server updated" },
|
|
90
95
|
404: { description: "MCP server not found" },
|
|
91
96
|
},
|
|
97
|
+
rbac: { permission: "mcp-server.update.any" },
|
|
92
98
|
});
|
|
93
99
|
|
|
94
100
|
const deleteMcpServerRoute = route({
|
|
@@ -103,6 +109,7 @@ const deleteMcpServerRoute = route({
|
|
|
103
109
|
200: { description: "MCP server deleted" },
|
|
104
110
|
404: { description: "MCP server not found" },
|
|
105
111
|
},
|
|
112
|
+
rbac: { permission: "mcp-server.delete.any" },
|
|
106
113
|
});
|
|
107
114
|
|
|
108
115
|
const installMcpServerRoute = route({
|
|
@@ -120,6 +127,7 @@ const installMcpServerRoute = route({
|
|
|
120
127
|
200: { description: "MCP server installed" },
|
|
121
128
|
404: { description: "MCP server not found" },
|
|
122
129
|
},
|
|
130
|
+
rbac: { permission: "mcp-server.install.any" },
|
|
123
131
|
});
|
|
124
132
|
|
|
125
133
|
const uninstallMcpServerRoute = route({
|
|
@@ -133,6 +141,7 @@ const uninstallMcpServerRoute = route({
|
|
|
133
141
|
responses: {
|
|
134
142
|
200: { description: "MCP server uninstalled" },
|
|
135
143
|
},
|
|
144
|
+
rbac: { permission: "mcp-server.uninstall.any" },
|
|
136
145
|
});
|
|
137
146
|
|
|
138
147
|
const getAgentMcpServersRoute = route({
|
|
@@ -153,6 +162,30 @@ const getAgentMcpServersRoute = route({
|
|
|
153
162
|
|
|
154
163
|
// ─── Handler ─────────────────────────────────────────────────────────────────
|
|
155
164
|
|
|
165
|
+
function canResolveMcpSecretsForHttpUser(req: IncomingMessage): boolean {
|
|
166
|
+
const auth = getRequestAuth(req);
|
|
167
|
+
if (auth?.kind !== "user") return true;
|
|
168
|
+
if (!isRbacEnabled()) return true;
|
|
169
|
+
|
|
170
|
+
const verb: PermissionVerb = "mcp-server.read.secrets";
|
|
171
|
+
const grant = getUserGrant(auth.userId);
|
|
172
|
+
const decision =
|
|
173
|
+
grant.grantsAll || grant.verbs.has(verb)
|
|
174
|
+
? ({ allow: true, verb } as const)
|
|
175
|
+
: ({
|
|
176
|
+
allow: false,
|
|
177
|
+
reason: `admission: missing permission '${verb}'`,
|
|
178
|
+
verb,
|
|
179
|
+
} as const);
|
|
180
|
+
enqueueAdmissionRow({
|
|
181
|
+
userId: auth.userId,
|
|
182
|
+
decision,
|
|
183
|
+
method: req.method,
|
|
184
|
+
route: getAgentMcpServersRoute.def.path,
|
|
185
|
+
});
|
|
186
|
+
return decision.allow;
|
|
187
|
+
}
|
|
188
|
+
|
|
156
189
|
export async function handleMcpServers(
|
|
157
190
|
req: IncomingMessage,
|
|
158
191
|
res: ServerResponse,
|
|
@@ -168,6 +201,11 @@ export async function handleMcpServers(
|
|
|
168
201
|
const resolveSecrets = parsed.query.resolveSecrets === "true";
|
|
169
202
|
|
|
170
203
|
if (resolveSecrets) {
|
|
204
|
+
if (!canResolveMcpSecretsForHttpUser(req)) {
|
|
205
|
+
jsonError(res, "Forbidden: admission: missing permission 'mcp-server.read.secrets'", 403);
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
171
209
|
const configs = getResolvedConfig(parsed.params.id);
|
|
172
210
|
const configMap = new Map(configs.map((c) => [c.key, c.value]));
|
|
173
211
|
|
|
@@ -408,12 +446,15 @@ export async function handleMcpServers(
|
|
|
408
446
|
const parsed = await deleteMcpServerRoute.parse(req, res, pathSegments, queryParams);
|
|
409
447
|
if (!parsed) return true;
|
|
410
448
|
|
|
411
|
-
const
|
|
412
|
-
if (!deleted) {
|
|
449
|
+
const result = deleteMcpServer(parsed.params.id);
|
|
450
|
+
if (!result.deleted) {
|
|
413
451
|
jsonError(res, "MCP server not found", 404);
|
|
414
452
|
return true;
|
|
415
453
|
}
|
|
416
|
-
json(res, {
|
|
454
|
+
json(res, {
|
|
455
|
+
success: true,
|
|
456
|
+
deletedScriptConnectionCount: result.deletedScriptConnectionCount,
|
|
457
|
+
});
|
|
417
458
|
return true;
|
|
418
459
|
}
|
|
419
460
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getOAuthProviderConfig } from "@/be/oauth-credential-bindings";
|
|
4
|
+
import { exchangeCode } from "@/oauth/wrapper";
|
|
5
|
+
import { route } from "./route-def";
|
|
6
|
+
import { jsonError } from "./utils";
|
|
7
|
+
|
|
8
|
+
const DEDICATED_CALLBACK_PROVIDERS = new Set(["linear"]);
|
|
9
|
+
|
|
10
|
+
const genericOAuthCallbackRoute = route({
|
|
11
|
+
method: "get",
|
|
12
|
+
path: "/api/oauth/{provider}/callback",
|
|
13
|
+
pattern: ["api", "oauth", null, "callback"],
|
|
14
|
+
operationId: "oauth_generic_callback",
|
|
15
|
+
summary: "Generic OAuth redirect target for script credential bindings",
|
|
16
|
+
tags: ["OAuth"],
|
|
17
|
+
auth: { apiKey: false },
|
|
18
|
+
params: z.object({
|
|
19
|
+
provider: z
|
|
20
|
+
.string()
|
|
21
|
+
.min(1)
|
|
22
|
+
.max(255)
|
|
23
|
+
.regex(/^[A-Za-z0-9_-]+$/),
|
|
24
|
+
}),
|
|
25
|
+
query: z.object({
|
|
26
|
+
code: z.string().optional(),
|
|
27
|
+
state: z.string().optional(),
|
|
28
|
+
error: z.string().optional(),
|
|
29
|
+
error_description: z.string().optional(),
|
|
30
|
+
}),
|
|
31
|
+
responses: {
|
|
32
|
+
200: { description: "OAuth authorization completed" },
|
|
33
|
+
400: { description: "Missing or invalid OAuth callback parameters" },
|
|
34
|
+
404: { description: "OAuth app not configured" },
|
|
35
|
+
409: { description: "Provider has a dedicated OAuth callback" },
|
|
36
|
+
502: { description: "Token exchange failed" },
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function sendAuthorizedHtml(res: ServerResponse, provider: string): void {
|
|
41
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
42
|
+
res.end(`<!DOCTYPE html>
|
|
43
|
+
<html>
|
|
44
|
+
<head><title>OAuth Authorized</title></head>
|
|
45
|
+
<body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0;">
|
|
46
|
+
<main style="text-align: center;">
|
|
47
|
+
<h1>${provider} authorized</h1>
|
|
48
|
+
<p>You can close this tab.</p>
|
|
49
|
+
</main>
|
|
50
|
+
</body>
|
|
51
|
+
</html>`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function handleGenericOAuth(
|
|
55
|
+
req: IncomingMessage,
|
|
56
|
+
res: ServerResponse,
|
|
57
|
+
pathSegments: string[],
|
|
58
|
+
queryParams: URLSearchParams,
|
|
59
|
+
): Promise<boolean> {
|
|
60
|
+
if (!genericOAuthCallbackRoute.match(req.method, pathSegments)) return false;
|
|
61
|
+
|
|
62
|
+
const parsed = await genericOAuthCallbackRoute.parse(req, res, pathSegments, queryParams);
|
|
63
|
+
if (!parsed) return true;
|
|
64
|
+
|
|
65
|
+
const { provider } = parsed.params;
|
|
66
|
+
if (DEDICATED_CALLBACK_PROVIDERS.has(provider)) {
|
|
67
|
+
jsonError(
|
|
68
|
+
res,
|
|
69
|
+
"Provider linear uses the dedicated OAuth flow at /api/trackers/linear/authorize and /api/trackers/linear/callback.",
|
|
70
|
+
409,
|
|
71
|
+
);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (parsed.query.error) {
|
|
76
|
+
jsonError(res, parsed.query.error_description ?? parsed.query.error, 400);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!parsed.query.code || !parsed.query.state) {
|
|
81
|
+
jsonError(res, "Missing code or state parameter", 400);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const config = getOAuthProviderConfig(provider);
|
|
86
|
+
if (!config) {
|
|
87
|
+
jsonError(res, `OAuth app ${provider} is not configured`, 404);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await exchangeCode(config, parsed.query.code, parsed.query.state);
|
|
93
|
+
sendAuthorizedHtml(res, provider);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
96
|
+
const status = message.includes("Invalid or expired OAuth state") ? 400 : 502;
|
|
97
|
+
jsonError(
|
|
98
|
+
res,
|
|
99
|
+
status === 400 ? "Invalid or expired OAuth state" : `Token exchange failed: ${message}`,
|
|
100
|
+
status,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return true;
|
|
105
|
+
}
|
package/src/http/poll.ts
CHANGED
|
@@ -18,13 +18,14 @@ import {
|
|
|
18
18
|
getPendingTaskForAgent,
|
|
19
19
|
getTaskAttachments,
|
|
20
20
|
getTaskById,
|
|
21
|
-
|
|
21
|
+
getUnassignedTaskIdsForAgent,
|
|
22
22
|
getUserById,
|
|
23
23
|
hasCapacity,
|
|
24
24
|
recordBudgetRefusalNotification,
|
|
25
25
|
startTask,
|
|
26
26
|
upsertChannelActivityCursor,
|
|
27
27
|
} from "../be/db";
|
|
28
|
+
import { renderIdentity, resolveIdentity } from "../be/identity";
|
|
28
29
|
import { fetchChannelActivity } from "../slack/channel-activity";
|
|
29
30
|
import { telemetry } from "../telemetry";
|
|
30
31
|
import { route } from "./route-def";
|
|
@@ -274,11 +275,19 @@ export async function handlePoll(
|
|
|
274
275
|
agentId: myAgentId,
|
|
275
276
|
});
|
|
276
277
|
|
|
277
|
-
// Resolve requesting user if available
|
|
278
|
+
// Resolve requesting user if available. If the task carries a
|
|
279
|
+
// machine-recorded external id (today: the Slack user field) but
|
|
280
|
+
// no requestedByUserId, render the explicit UNKNOWN sentinel
|
|
281
|
+
// instead of silently omitting the section — never a substituted
|
|
282
|
+
// human (Rule 33 / provenance-or-silence).
|
|
278
283
|
const requestedByUser = pendingTask.requestedByUserId
|
|
279
284
|
? getUserById(pendingTask.requestedByUserId)
|
|
280
285
|
: undefined;
|
|
281
286
|
const requestedByNotes = getRequesterNotes(requestedByUser?.notes);
|
|
287
|
+
const requestedByUnknownName =
|
|
288
|
+
!requestedByUser && pendingTask.slackUserId
|
|
289
|
+
? renderIdentity(resolveIdentity("slack", pendingTask.slackUserId))
|
|
290
|
+
: undefined;
|
|
282
291
|
|
|
283
292
|
return {
|
|
284
293
|
trigger: {
|
|
@@ -297,6 +306,9 @@ export async function handlePoll(
|
|
|
297
306
|
notes: requestedByNotes,
|
|
298
307
|
},
|
|
299
308
|
}),
|
|
309
|
+
...(requestedByUnknownName && {
|
|
310
|
+
requestedBy: { name: requestedByUnknownName },
|
|
311
|
+
}),
|
|
300
312
|
},
|
|
301
313
|
};
|
|
302
314
|
}
|
|
@@ -333,8 +345,13 @@ export async function handlePoll(
|
|
|
333
345
|
// one worker wins if multiple poll simultaneously.
|
|
334
346
|
// This ensures session logs are correctly associated with the real task ID
|
|
335
347
|
// from the start (no reassociation needed).
|
|
348
|
+
//
|
|
349
|
+
// Routing affinity: `getUnassignedTaskIdsForAgent` (not the plain
|
|
350
|
+
// `getUnassignedTaskIds`) pre-filters candidates through
|
|
351
|
+
// `isAgentEligibleForTask`, so an ineligible task is never even
|
|
352
|
+
// offered to the budget gate below or the claim loop.
|
|
336
353
|
if (hasCapacity(myAgentId)) {
|
|
337
|
-
const unassignedIds =
|
|
354
|
+
const unassignedIds = getUnassignedTaskIdsForAgent(myAgentId, 5);
|
|
338
355
|
// Budget admission gate (Phase 3). Pool path is workers-only —
|
|
339
356
|
// per-agent budgets matter most here, but we still check global.
|
|
340
357
|
// Only run the gate when there's at least one candidate task; an
|