@desplega.ai/agent-swarm 2.5.1 → 2.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{acp-adapter-7g6bpwbk.js → acp-adapter-gr2fcyyj.js} +3 -3
- package/dist/{actions-w8xd014k.js → actions-wv766ghf.js} +18 -18
- package/dist/{amp-adapter-z4qvhacj.js → amp-adapter-speb906t.js} +3 -3
- package/dist/{app-nndcvftq.js → app-ag29n14f.js} +3 -3
- package/dist/{assistant-hjmqyscx.js → assistant-3x8bz3eg.js} +19 -19
- package/dist/{boot-reembed-r8rhf9h8.js → boot-reembed-5b3d2eea.js} +1 -1
- package/dist/{boot-reembed-vwcj2y5d.js → boot-reembed-jsmfrpzf.js} +1 -1
- package/dist/{boot-scrub-sweep-fr9q36ne.js → boot-scrub-sweep-s73gerxy.js} +2 -2
- package/dist/{boot-seeds-9jqwnegq.js → boot-seeds-5zmax2wr.js} +4 -4
- package/dist/{claude-adapter-2f8b85df.js → claude-adapter-hswdnm3t.js} +3 -3
- package/dist/{claude-managed-adapter-gt6pa35d.js → claude-managed-adapter-cvbzj1h6.js} +2 -2
- package/dist/{claude-managed-setup-c3hp36ph.js → claude-managed-setup-3k8m3hhc.js} +2 -2
- package/dist/{claude-sdk-session-05kat8rv.js → claude-sdk-session-fkbyk66x.js} +6905 -6855
- package/dist/{cli-y3zyaee5.js → cli-1fc85mvt.js} +5 -5
- package/dist/{cli-v2g93k4v.js → cli-210xd1f4.js} +4 -4
- package/dist/{cli-sn1vbtg2.js → cli-25j01dpj.js} +3 -3
- package/dist/{cli-n6kmqngw.js → cli-35gtk1p6.js} +1 -1
- package/dist/{cli-r9fy0ddc.js → cli-3ngdk5te.js} +1 -1
- package/dist/{cli-z63a269p.js → cli-40jsnrjm.js} +2 -2
- package/dist/{cli-ba8ne1es.js → cli-4rvr7zfx.js} +10 -0
- package/dist/{cli-qfm6my86.js → cli-502eqv93.js} +1 -1
- package/dist/{cli-xd4eqtr2.js → cli-6n8axjv1.js} +1 -1
- package/dist/{cli-hadv0725.js → cli-8s5wx3nz.js} +2 -2
- package/dist/{cli-z2dv25mf.js → cli-99xgw26r.js} +116 -5
- package/dist/{cli-jpxsrxcb.js → cli-9s05422w.js} +1 -1
- package/dist/{cli-kcsd1ez8.js → cli-aecdcr77.js} +343 -325
- package/dist/{cli-w9etnnxk.js → cli-bf8ma3f1.js} +120 -50
- package/dist/{cli-e9btqw4y.js → cli-ce0ak9ms.js} +18 -13
- package/dist/{cli-nj12tn2c.js → cli-e2ar4bp1.js} +71 -20
- package/dist/{cli-gwcf52gz.js → cli-e8jyq7d8.js} +11 -7
- package/dist/{cli-rngde2jv.js → cli-egjggehm.js} +1 -1
- package/dist/{cli-qfjm4gvt.js → cli-ewm3pwbq.js} +2 -2
- package/dist/{cli-9mm5ade4.js → cli-fsdprwrg.js} +1 -1
- package/dist/{cli-3ee5z0q0.js → cli-fzcn3d87.js} +1 -1
- package/dist/{cli-eym3jg7w.js → cli-gpsz7g88.js} +6 -6
- package/dist/{cli-cwyafhw1.js → cli-hcpj2xqz.js} +1 -1
- package/dist/{cli-j1eghqfm.js → cli-hhxjnjzs.js} +1 -1
- package/dist/{cli-pyy4c9ch.js → cli-jjznk3f7.js} +881 -232
- package/dist/{cli-q9pzwxvk.js → cli-kxawjrxg.js} +222 -37
- package/dist/{cli-rh5smt8t.js → cli-mz9afxxf.js} +14 -3
- package/dist/{cli-mz2ygeyq.js → cli-n45mr7zh.js} +2 -2
- package/dist/{cli-fes51ya2.js → cli-p0rzjkya.js} +2 -2
- package/dist/{cli-3qv8naq8.js → cli-p6zs9h4y.js} +827 -107
- package/dist/{cli-mq7kg1b1.js → cli-p9rafdrb.js} +41 -6
- package/dist/{cli-cah5detr.js → cli-qgf8emrn.js} +5 -5
- package/dist/{cli-2wba1ac9.js → cli-qzme5949.js} +26 -26
- package/dist/{cli-znvq0k3t.js → cli-s49hpn37.js} +2 -2
- package/dist/{cli-xc65x52q.js → cli-s53vp709.js} +1 -1
- package/dist/{cli-0jy85zz4.js → cli-svrj71pg.js} +18 -16
- package/dist/{cli-1dp1zj04.js → cli-vwn81jbs.js} +3 -3
- package/dist/{cli-ajqhxtnz.js → cli-w1zwctj2.js} +1 -1
- package/dist/{cli-22rmcjkg.js → cli-y561h5a9.js} +7 -6
- package/dist/{cli-2jg61wab.js → cli-z21whrrg.js} +15 -15
- package/dist/cli.js +19 -18
- package/dist/{codex-adapter-p4ptwar5.js → codex-adapter-n8ka66vf.js} +2 -2
- package/dist/{codex-session-runner-964c7dnk.js → codex-session-runner-fsebts37.js} +1 -1
- package/dist/{commands-mbvhbrcy.js → commands-t04rt2pc.js} +2 -2
- package/dist/{controller-gnq1k1mf.js → controller-rgna04r2.js} +2 -2
- package/dist/{controllers-connect-6dkm71tg.js → controllers-connect-bqm1rtft.js} +20 -20
- package/dist/{cursor-adapter-yq5xgfcg.js → cursor-adapter-swr2k95y.js} +1 -1
- package/dist/{db-88wcymxt.js → db-24ck09xy.js} +1 -1
- package/dist/{db-af9y8nkf.js → db-a4m7haw0.js} +3 -3
- package/dist/{dispatcher-606ydx5v.js → dispatcher-nwf9ss0q.js} +17 -17
- package/dist/{dsh-adapter-tvhc4k2d.js → dsh-adapter-kpdnhjst.js} +2 -2
- package/dist/{external-channel-y8q5z8z2.js → external-channel-j1e2p34p.js} +2 -2
- package/dist/{grok-adapter-39aha52t.js → grok-adapter-qtjz0whk.js} +4 -4
- package/dist/{handlers-rk9dqech.js → handlers-19xr6dnz.js} +20 -20
- package/dist/{heartbeat-jfe9zfjr.js → heartbeat-vtesda5d.js} +20 -20
- package/dist/{hook-b1mez1m8.js → hook-8wqaszda.js} +1 -1
- package/dist/{hook-ffevzed1.js → hook-b2hhj6rd.js} +1 -1
- package/dist/{http-d5b44f1r.js → http-mxcqhhcs.js} +3020 -2463
- package/dist/{index-5en23s5z.js → index-1r8wfpzs.js} +1 -1
- package/dist/{index-cbxkmfc5.js → index-9pn1pfkf.js} +1 -1
- package/dist/{index-mj46rk9k.js → index-kw2mmat7.js} +20 -20
- package/dist/{index-nq8hhv28.js → index-n1vbvta9.js} +19 -10
- package/dist/{index-6srwvd5b.js → index-p4xfz37z.js} +1 -1
- package/dist/{index-d2qeem97.js → index-spwxjgay.js} +17 -17
- package/dist/{index-4q3vj1yr.js → index-srg522m6.js} +17 -17
- package/dist/{keepalive-wsby1btq.js → keepalive-dsd8rtgx.js} +3 -3
- package/dist/{lead-paa141j6.js → lead-zyerk1x2.js} +30 -30
- package/dist/{maintenance-vnshcyv9.js → maintenance-jqmqwnet.js} +1 -1
- package/dist/{oauth-refresh-sweep-d6sqn5np.js → oauth-refresh-sweep-q2c3bnvm.js} +2 -2
- package/dist/{onboard-nr4xhpzy.js → onboard-w4yd7s8t.js} +2 -2
- package/dist/{opencode-adapter-e56086m1.js → opencode-adapter-9tppvxar.js} +2 -2
- package/dist/{otel-impl-ytn9wyv9.js → otel-impl-f36byzbd.js} +1 -1
- package/dist/{pricing-refresh-pva04zee.js → pricing-refresh-xvak5p4v.js} +3 -3
- package/dist/{rbac-roles-7rc1q3g4.js → rbac-roles-682k2mt5.js} +12 -2
- package/dist/rbac-roles-gfn30fb8.js +26 -0
- package/dist/{render-v2-pyfsg9zk.js → render-v2-hgytz8nn.js} +3 -3
- package/dist/{resume-phj8xhqr.js → resume-2dvq348g.js} +7 -5
- package/dist/{session-access-jzx2z9h5.js → session-access-7f21ekq9.js} +4 -4
- package/dist/{setup-rfg0f0sg.js → setup-epffxtbk.js} +1 -1
- package/dist/{work-objects-44z2yjg8.js → work-objects-xz1a5rqk.js} +20 -20
- package/dist/{worker-zsfqzxhm.js → worker-42t8emjt.js} +30 -30
- package/openapi.json +3163 -2114
- package/package.json +7 -6
- package/src/be/approval-config-question.ts +389 -0
- package/src/be/automation-preflight.ts +37 -3
- package/src/be/db/session-tree.ts +158 -13
- package/src/be/db/session-visibility.ts +31 -0
- package/src/be/db/skill-search.ts +87 -0
- package/src/be/db/tasks/read.ts +47 -7
- package/src/be/db/workflow-runs.ts +54 -7
- package/src/be/db.ts +27 -5
- package/src/be/migrations/233_script_run_session_tokens.sql +8 -0
- package/src/be/migrations/235_chat_relay_defer.sql +86 -0
- package/src/be/migrations/237_session_inbox_cleanup.sql +75 -0
- package/src/be/migrations/239_skills_fts.sql +40 -0
- package/src/be/migrations/240_workflow_run_parent_step_attempt.sql +37 -0
- package/src/be/migrations/241_skills_meta_fts.sql +39 -0
- package/src/be/rbac-roles.ts +138 -0
- package/src/be/scripts/run-credential.ts +64 -0
- package/src/be/scripts/run-saved.ts +3 -2
- package/src/be/swarm-config-guard.ts +23 -0
- package/src/be/users.ts +28 -4
- package/src/extensions/catalog.generated.json +4 -4
- package/src/http/all-routes.ts +1 -0
- package/src/http/approval-requests.ts +254 -43
- package/src/http/config.ts +22 -37
- package/src/http/index.ts +2 -0
- package/src/http/schedules.ts +26 -0
- package/src/http/scripts.ts +2 -2
- package/src/http/sessions.ts +118 -2
- package/src/http/user-roles.ts +173 -0
- package/src/http/users.ts +9 -0
- package/src/http/x.ts +2 -2
- package/src/integrations/chat-sdk/commands.ts +14 -1
- package/src/integrations/chat-sdk/inbound.ts +42 -6
- package/src/integrations/chat-sdk/outbound.ts +35 -10
- package/src/integrations/chat-sdk/reactions.ts +107 -0
- package/src/integrations/chat-sdk/relay.ts +75 -7
- package/src/integrations/chat-sdk/thread-buffer.ts +2 -0
- package/src/integrations/reaction-events.ts +20 -0
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/rbac/legacy-policy.ts +3 -0
- package/src/rbac/permissions.ts +8 -0
- package/src/scheduler/schedule-task.ts +44 -3
- package/src/scheduler/scheduler.ts +4 -4
- package/src/scripts-runtime/loader.ts +8 -1
- package/src/slack/ack.ts +2 -1
- package/src/slack/reaction-shortcode.ts +4 -7
- package/src/tasks/worker-follow-up.ts +30 -0
- package/src/tests/approval-config-question.test.ts +702 -0
- package/src/tests/approval-requests-notifications.test.ts +189 -0
- package/src/tests/chat-sdk-commands.test.ts +12 -0
- package/src/tests/chat-sdk-inbound.test.ts +183 -9
- package/src/tests/chat-sdk-outbound-files.test.ts +9 -2
- package/src/tests/chat-sdk-relay.test.ts +361 -10
- package/src/tests/chat-sdk-tools.test.ts +104 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/entrypoint-gitlab-credential-helper.test.ts +273 -0
- package/src/tests/extensions-example-comms-adherence-gate.test.ts +321 -12
- package/src/tests/http-users.test.ts +38 -0
- package/src/tests/list-endpoint-slimming.test.ts +33 -0
- package/src/tests/migration-237-session-inbox-cleanup.test.ts +136 -0
- package/src/tests/migration-240-workflow-run-parent-step-attempt.test.ts +55 -0
- package/src/tests/rbac-engine.test.ts +2 -0
- package/src/tests/rbac-roles.test.ts +10 -0
- package/src/tests/schedule-http-triage-tools.test.ts +44 -0
- package/src/tests/script-api-credential.test.ts +173 -0
- package/src/tests/script-connections.test.ts +10 -0
- package/src/tests/script-run-config-scope.test.ts +120 -0
- package/src/tests/script-run-token.test.ts +75 -0
- package/src/tests/scripts-http.test.ts +19 -0
- package/src/tests/sessions-tree.test.ts +410 -1
- package/src/tests/skill-search-long-content.test.ts +193 -0
- package/src/tests/skill-search.test.ts +173 -22
- package/src/tests/user-roles-http.test.ts +204 -0
- package/src/tests/workflow-hitl-dynamic-questions.test.ts +1 -1
- package/src/tests/workflow-sub-workflow.test.ts +538 -1
- package/src/tools/chat-reply.ts +2 -0
- package/src/tools/request-human-input.ts +27 -5
- package/src/tools/schedules/patch-schedule.ts +18 -0
- package/src/tools/schedules/update-schedule.ts +18 -0
- package/src/types.ts +23 -0
- package/src/workflows/approval-notifications.ts +221 -0
- package/src/workflows/checkpoint.ts +69 -35
- package/src/workflows/engine.ts +234 -87
- package/src/workflows/executors/base.ts +6 -0
- package/src/workflows/executors/human-in-the-loop.ts +36 -173
- package/src/workflows/executors/sub-workflow.ts +45 -16
- package/src/workflows/executors/swarm-script.ts +2 -2
- package/src/workflows/recovery.ts +63 -1
- package/src/workflows/resume.ts +60 -21
- package/src/workflows/retry-poller.ts +25 -4
- package/templates/extensions/comms-adherence-gate/README.md +38 -13
- package/templates/extensions/comms-adherence-gate/hooks.ts +272 -69
- package/templates/extensions/comms-adherence-gate/manifest.json +2 -2
- package/templates/schedules/weekly-harness-upgrade-check/content.md +1 -1
- package/templates/skills/code-quality/content.md +1 -1
- package/templates/skills/turso-interaction/content.md +1 -1
- package/templates/skills/workflow-iterate/content.md +8 -1
- package/dist/rbac-roles-ff1ffmec.js +0 -12
- /package/dist/{cli-stxdrqs2.js → cli-avdvgcay.js} +0 -0
- /package/dist/{pi-mono-adapter-tzps8f38.js → pi-mono-adapter-tqsge1ay.js} +0 -0
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
updateWorkflowRun,
|
|
35
35
|
upsertKv,
|
|
36
36
|
} from "../be/db";
|
|
37
|
+
import { ensureRbacSeedsSynced, setUserRoles } from "../be/rbac-roles";
|
|
37
38
|
import { fingerprintApiKey, linkIdentity } from "../be/users";
|
|
38
39
|
import { handleCore } from "../http/core";
|
|
39
40
|
import { handleUsers } from "../http/users";
|
|
@@ -910,6 +911,43 @@ describe("POST /api/users/:id/merge", () => {
|
|
|
910
911
|
}
|
|
911
912
|
});
|
|
912
913
|
|
|
914
|
+
test("409 last_admin when the source is the only admin, and nothing is merged", async () => {
|
|
915
|
+
ensureRbacSeedsSynced({ quiet: true });
|
|
916
|
+
const admin = await createUser({ name: "OnlyAdmin" });
|
|
917
|
+
const viewer = await createUser({ name: "Viewer" });
|
|
918
|
+
await setUserRoles(admin.id, ["admin"], null);
|
|
919
|
+
await setUserRoles(viewer.id, ["viewer"], null);
|
|
920
|
+
|
|
921
|
+
const r = await authedFetch(`/api/users/${viewer.id}/merge`, {
|
|
922
|
+
method: "POST",
|
|
923
|
+
body: JSON.stringify({ sourceUserId: admin.id }),
|
|
924
|
+
});
|
|
925
|
+
expect(r.status).toBe(409);
|
|
926
|
+
expect(
|
|
927
|
+
(
|
|
928
|
+
await getDbClient().get<{ n: number }>("SELECT COUNT(*) AS n FROM users WHERE id = ?", [
|
|
929
|
+
admin.id,
|
|
930
|
+
])
|
|
931
|
+
)?.n,
|
|
932
|
+
).toBe(1);
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
test("merging an admin source succeeds when another admin remains", async () => {
|
|
936
|
+
ensureRbacSeedsSynced({ quiet: true });
|
|
937
|
+
const keeper = await createUser({ name: "KeeperAdmin" });
|
|
938
|
+
const source = await createUser({ name: "SourceAdmin" });
|
|
939
|
+
const viewer = await createUser({ name: "Viewer2" });
|
|
940
|
+
await setUserRoles(keeper.id, ["admin"], null);
|
|
941
|
+
await setUserRoles(source.id, ["admin"], null);
|
|
942
|
+
await setUserRoles(viewer.id, ["viewer"], null);
|
|
943
|
+
|
|
944
|
+
const r = await authedFetch(`/api/users/${viewer.id}/merge`, {
|
|
945
|
+
method: "POST",
|
|
946
|
+
body: JSON.stringify({ sourceUserId: source.id }),
|
|
947
|
+
});
|
|
948
|
+
expect(r.status).toBe(200);
|
|
949
|
+
});
|
|
950
|
+
|
|
913
951
|
test("400 when target == source", async () => {
|
|
914
952
|
const u = await createUser({ name: "Self" });
|
|
915
953
|
const r = await authedFetch(`/api/users/${u.id}/merge`, {
|
|
@@ -266,6 +266,39 @@ describe("list-endpoint slimming", () => {
|
|
|
266
266
|
}
|
|
267
267
|
});
|
|
268
268
|
|
|
269
|
+
test("getAllTasks — the preview starts after a leading sibling block", async () => {
|
|
270
|
+
// `prependSiblingBlock` output: a long block, a blank line, then the ask.
|
|
271
|
+
const agentId = "preview-sibling-agent";
|
|
272
|
+
const sibling = (n: number) =>
|
|
273
|
+
`- [in_progress] task:${n} — agent:Picateclas — started 2m ago, last active 10s ago\n "${"é".repeat(190)}"`;
|
|
274
|
+
const block = `<sibling_tasks_in_progress>\nThe following tasks are already running in the same context (contextKey: slack:C1:1.2). The user has submitted new input while they were in flight — coordinate with them, do not duplicate:\n\n${[1, 2, 3].map(sibling).join("\n")}\n</sibling_tasks_in_progress>`;
|
|
275
|
+
const ask = `also bump the version ${"v".repeat(400)}`;
|
|
276
|
+
const texts = [
|
|
277
|
+
`${block}\n\n${ask}`,
|
|
278
|
+
`${block}\nno blank line`,
|
|
279
|
+
// No closing tag: nothing to skip.
|
|
280
|
+
`<sibling_tasks_in_progress>\n${"x".repeat(400)}`,
|
|
281
|
+
// Not at the start: nothing to skip.
|
|
282
|
+
`lead in\n\n${block}\n\nask`,
|
|
283
|
+
];
|
|
284
|
+
for (const text of texts) await createTaskExtended(text, { agentId });
|
|
285
|
+
|
|
286
|
+
const expected = [
|
|
287
|
+
`${ask.slice(0, 300)}…`,
|
|
288
|
+
"\nno blank line",
|
|
289
|
+
`<sibling_tasks_in_progress>\n${"x".repeat(272)}…`,
|
|
290
|
+
`${`lead in\n\n${block}\n\nask`.slice(0, 300)}…`,
|
|
291
|
+
].sort();
|
|
292
|
+
for (const opts of [{ slim: true }, { fields: "timeline" as const }]) {
|
|
293
|
+
const rows = await getAllTasks({ agentId, limit: 50 }, opts);
|
|
294
|
+
expect(rows.map((t) => t.task).sort()).toEqual(expected);
|
|
295
|
+
}
|
|
296
|
+
// Session roots map the full text, so the JS skip must agree with the SQL one.
|
|
297
|
+
const sessions = await listRecentSessions({ limit: 200, slim: true });
|
|
298
|
+
const roots = sessions.filter((s) => s.root.agentId === agentId).map((s) => s.root.task);
|
|
299
|
+
expect(roots.sort()).toEqual(expected);
|
|
300
|
+
});
|
|
301
|
+
|
|
269
302
|
test("getAllTasks — slim and full pages hold the same rows in the same order", async () => {
|
|
270
303
|
for (let i = 0; i < 12; i++) {
|
|
271
304
|
await createTaskExtended(`page-parity ${i}`, { agentId: "slim-agent-1" });
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { afterAll, beforeAll, expect, test } from "bun:test";
|
|
2
|
+
import { closeDb, createTaskExtended, createUser, getDb, getDbClient, initDb } from "../be/db";
|
|
3
|
+
import { setSessionShares } from "../be/session-shares";
|
|
4
|
+
|
|
5
|
+
const day = 86_400_000;
|
|
6
|
+
const ago = (days: number) => new Date(Date.now() - days * day).toISOString();
|
|
7
|
+
|
|
8
|
+
beforeAll(() => {
|
|
9
|
+
initDb(":memory:");
|
|
10
|
+
});
|
|
11
|
+
afterAll(() => {
|
|
12
|
+
closeDb();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
async function root(requestedByUserId: string | undefined, activity: string, status = "completed") {
|
|
16
|
+
const task = await createTaskExtended("Cleanup test", { key: "shared/", requestedByUserId });
|
|
17
|
+
await getDbClient().run("UPDATE agent_tasks SET lastUpdatedAt = ?, status = ? WHERE id = ?", [
|
|
18
|
+
activity,
|
|
19
|
+
status,
|
|
20
|
+
task.id,
|
|
21
|
+
]);
|
|
22
|
+
return task;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function row(
|
|
26
|
+
userId: string,
|
|
27
|
+
itemId: string,
|
|
28
|
+
status: string,
|
|
29
|
+
lastReadAt: string | null = null,
|
|
30
|
+
{ snoozeUntil = "2999-01-01T00:00:00.000Z", lastUpdatedAt = ago(20) } = {},
|
|
31
|
+
) {
|
|
32
|
+
await getDbClient().run(
|
|
33
|
+
`INSERT INTO inbox_item_state (userId, itemType, itemId, status, snoozeUntil, lastReadAt, lastUpdatedAt)
|
|
34
|
+
VALUES (?, 'to_read', ?, ?, ?, ?, ?)`,
|
|
35
|
+
[userId, itemId, status, status === "snoozed" ? snoozeUntil : null, lastReadAt, lastUpdatedAt],
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function states(): Promise<Record<string, string>> {
|
|
40
|
+
const rows = await getDbClient().query<{ userId: string; itemId: string; status: string }>(
|
|
41
|
+
"SELECT userId, itemId, status FROM inbox_item_state ORDER BY userId, itemId",
|
|
42
|
+
);
|
|
43
|
+
return Object.fromEntries(rows.map((r) => [`${r.userId}:${r.itemId}`, r.status]));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
test("migration 237 moves quiet, read sessions to Done for every viewer, once", async () => {
|
|
47
|
+
const owner = await createUser({ name: "Owner" });
|
|
48
|
+
const other = await createUser({ name: "Other" });
|
|
49
|
+
const third = await createUser({ name: "Third" });
|
|
50
|
+
|
|
51
|
+
const quiet = await root(owner.id, ago(30));
|
|
52
|
+
const recent = await root(owner.id, ago(1));
|
|
53
|
+
// A quiet root whose child moved yesterday is not quiet.
|
|
54
|
+
const busyChain = await root(owner.id, ago(30));
|
|
55
|
+
const child = await createTaskExtended("Child", { parentTaskId: busyChain.id });
|
|
56
|
+
await getDbClient().run(
|
|
57
|
+
"UPDATE agent_tasks SET lastUpdatedAt = ?, status = 'completed' WHERE id = ?",
|
|
58
|
+
[ago(1), child.id],
|
|
59
|
+
);
|
|
60
|
+
// Quiet but not finished, or waiting on an approval: both stay in the Inbox.
|
|
61
|
+
const stuck = await root(owner.id, ago(30), "in_progress");
|
|
62
|
+
const failed = await root(owner.id, ago(30), "failed");
|
|
63
|
+
const awaiting = await root(owner.id, ago(30));
|
|
64
|
+
await getDbClient().run(
|
|
65
|
+
`INSERT INTO approval_requests (id, sourceTaskId, title, questions, approvers, status)
|
|
66
|
+
VALUES (?, ?, 'Approve deploy', '[]', '{}', 'pending')`,
|
|
67
|
+
[crypto.randomUUID(), awaiting.id],
|
|
68
|
+
);
|
|
69
|
+
// Open rows: read up to the newest activity, or unread (cursor before it).
|
|
70
|
+
const openRead = await root(owner.id, ago(30));
|
|
71
|
+
await row(owner.id, openRead.id, "open", ago(25));
|
|
72
|
+
const openUnread = await root(owner.id, ago(30));
|
|
73
|
+
await row(owner.id, openUnread.id, "open", "1970-01-01T00:00:00.000Z");
|
|
74
|
+
// Open rows with no cursor: the row's own time is the cursor.
|
|
75
|
+
const openNoCursor = await root(owner.id, ago(30));
|
|
76
|
+
await row(owner.id, openNoCursor.id, "open");
|
|
77
|
+
const openNoCursorOld = await root(owner.id, ago(30));
|
|
78
|
+
await row(owner.id, openNoCursorOld.id, "open", null, { lastUpdatedAt: ago(40) });
|
|
79
|
+
// Snoozed (also past its wake time, which reads as unread) and dismissed rows keep their state.
|
|
80
|
+
const snoozed = await root(owner.id, ago(30));
|
|
81
|
+
await row(owner.id, snoozed.id, "snoozed");
|
|
82
|
+
const woken = await root(owner.id, ago(30));
|
|
83
|
+
await row(owner.id, woken.id, "snoozed", null, { snoozeUntil: ago(10) });
|
|
84
|
+
const dismissed = await root(owner.id, ago(30));
|
|
85
|
+
await row(owner.id, dismissed.id, "dismissed");
|
|
86
|
+
// Shares: one user, and everyone.
|
|
87
|
+
const sharedDirect = await root(other.id, ago(30));
|
|
88
|
+
await setSessionShares(
|
|
89
|
+
{ id: sharedDirect.id, requestedByUserId: other.id },
|
|
90
|
+
{ everyone: "none", users: [{ userId: owner.id, access: "read" }] },
|
|
91
|
+
other.id,
|
|
92
|
+
);
|
|
93
|
+
const sharedAll = await root(other.id, ago(30));
|
|
94
|
+
await setSessionShares(
|
|
95
|
+
{ id: sharedAll.id, requestedByUserId: other.id },
|
|
96
|
+
{ everyone: "read", users: [] },
|
|
97
|
+
other.id,
|
|
98
|
+
);
|
|
99
|
+
// Ownerless roots are not in the tree.
|
|
100
|
+
const ownerless = await root(undefined, ago(30));
|
|
101
|
+
|
|
102
|
+
const sql = await Bun.file("src/be/migrations/237_session_inbox_cleanup.sql").text();
|
|
103
|
+
getDb().exec(sql);
|
|
104
|
+
const after = await states();
|
|
105
|
+
expect(after).toEqual({
|
|
106
|
+
[`${owner.id}:${quiet.id}`]: "done",
|
|
107
|
+
[`${owner.id}:${failed.id}`]: "done",
|
|
108
|
+
[`${owner.id}:${openRead.id}`]: "done",
|
|
109
|
+
[`${owner.id}:${openUnread.id}`]: "open",
|
|
110
|
+
[`${owner.id}:${openNoCursor.id}`]: "done",
|
|
111
|
+
[`${owner.id}:${openNoCursorOld.id}`]: "open",
|
|
112
|
+
[`${owner.id}:${snoozed.id}`]: "snoozed",
|
|
113
|
+
[`${owner.id}:${woken.id}`]: "snoozed",
|
|
114
|
+
[`${owner.id}:${dismissed.id}`]: "dismissed",
|
|
115
|
+
[`${owner.id}:${sharedDirect.id}`]: "done",
|
|
116
|
+
[`${owner.id}:${sharedAll.id}`]: "done",
|
|
117
|
+
[`${other.id}:${sharedDirect.id}`]: "done",
|
|
118
|
+
[`${other.id}:${sharedAll.id}`]: "done",
|
|
119
|
+
[`${third.id}:${sharedAll.id}`]: "done",
|
|
120
|
+
});
|
|
121
|
+
for (const id of [recent.id, busyChain.id, ownerless.id, stuck.id, awaiting.id]) {
|
|
122
|
+
expect(Object.keys(after).some((key) => key.endsWith(`:${id}`))).toBe(false);
|
|
123
|
+
}
|
|
124
|
+
// Every timestamp it writes is ISO, so the read rule compares it with task times.
|
|
125
|
+
const written = await getDbClient().get<{ doneAt: string; lastUpdatedAt: string }>(
|
|
126
|
+
"SELECT doneAt, lastUpdatedAt FROM inbox_item_state WHERE itemId = ?",
|
|
127
|
+
[quiet.id],
|
|
128
|
+
);
|
|
129
|
+
expect(written?.doneAt).toMatch(/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/);
|
|
130
|
+
expect(written?.lastUpdatedAt).toBe(written?.doneAt);
|
|
131
|
+
|
|
132
|
+
// A second run changes nothing.
|
|
133
|
+
getDb().exec(sql);
|
|
134
|
+
expect(await states()).toEqual(after);
|
|
135
|
+
expect(getDb().query("PRAGMA integrity_check").get()).toEqual({ integrity_check: "ok" });
|
|
136
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { expect, test } from "bun:test";
|
|
3
|
+
|
|
4
|
+
test("migration 240 hands each legacy child to the attempt that owns it now", async () => {
|
|
5
|
+
const db = new Database(":memory:");
|
|
6
|
+
try {
|
|
7
|
+
db.exec("CREATE TABLE workflow_runs (id TEXT PRIMARY KEY, workflowId TEXT, status TEXT)");
|
|
8
|
+
db.exec(
|
|
9
|
+
`CREATE TABLE workflow_run_steps (
|
|
10
|
+
id TEXT PRIMARY KEY, status TEXT, retryCount INTEGER NOT NULL DEFAULT 0, nextRetryAt TEXT
|
|
11
|
+
)`,
|
|
12
|
+
);
|
|
13
|
+
db.exec(await Bun.file("src/be/migrations/203_workflow_run_parent_step.sql").text());
|
|
14
|
+
const step = db.query(
|
|
15
|
+
"INSERT INTO workflow_run_steps (id, status, retryCount, nextRetryAt) VALUES (?, ?, ?, ?)",
|
|
16
|
+
);
|
|
17
|
+
// Created on attempt 1 (attempt 0 failed before any child existed) and parked.
|
|
18
|
+
step.run("parked-retry", "waiting", 1, null);
|
|
19
|
+
// Its child failed; attempt 2 is queued and must start a new child.
|
|
20
|
+
step.run("queued-retry", "failed", 2, "2030-01-01T00:00:00.000Z");
|
|
21
|
+
step.run("first-attempt", "completed", 0, null);
|
|
22
|
+
step.run("exhausted", "failed", 3, null);
|
|
23
|
+
const run = db.query(
|
|
24
|
+
"INSERT INTO workflow_runs (id, workflowId, status, parentStepId) VALUES (?, 'wf', ?, ?)",
|
|
25
|
+
);
|
|
26
|
+
run.run("child-parked", "waiting", "parked-retry");
|
|
27
|
+
run.run("child-queued", "failed", "queued-retry");
|
|
28
|
+
run.run("child-first", "completed", "first-attempt");
|
|
29
|
+
run.run("child-exhausted", "failed", "exhausted");
|
|
30
|
+
run.run("child-orphan", "completed", "missing-step");
|
|
31
|
+
run.run("top-level", "completed", null);
|
|
32
|
+
|
|
33
|
+
db.exec(await Bun.file("src/be/migrations/240_workflow_run_parent_step_attempt.sql").text());
|
|
34
|
+
|
|
35
|
+
expect(
|
|
36
|
+
db.prepare("SELECT id, parentStepAttempt AS attempt FROM workflow_runs ORDER BY id").all(),
|
|
37
|
+
).toEqual([
|
|
38
|
+
{ id: "child-exhausted", attempt: 3 },
|
|
39
|
+
{ id: "child-first", attempt: 0 },
|
|
40
|
+
{ id: "child-orphan", attempt: 0 },
|
|
41
|
+
{ id: "child-parked", attempt: 1 },
|
|
42
|
+
{ id: "child-queued", attempt: 1 },
|
|
43
|
+
{ id: "top-level", attempt: 0 },
|
|
44
|
+
]);
|
|
45
|
+
// One child per attempt: a second child for the parked attempt is rejected,
|
|
46
|
+
// one for the queued attempt is the new invocation.
|
|
47
|
+
const insert = db.query(
|
|
48
|
+
"INSERT INTO workflow_runs (id, workflowId, status, parentStepId, parentStepAttempt) VALUES (?, 'wf', 'running', ?, ?)",
|
|
49
|
+
);
|
|
50
|
+
expect(() => insert.run("dup", "parked-retry", 1)).toThrow();
|
|
51
|
+
insert.run("next", "queued-retry", 2);
|
|
52
|
+
} finally {
|
|
53
|
+
db.close();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
@@ -106,6 +106,7 @@ const OPERATOR_OR_USER_VERBS: PermissionVerb[] = [
|
|
|
106
106
|
"runtime_controller.write",
|
|
107
107
|
"runtime_pool.write",
|
|
108
108
|
"agent.token.manage",
|
|
109
|
+
"user.role.manage",
|
|
109
110
|
];
|
|
110
111
|
|
|
111
112
|
const LEAD_OR_OPERATOR_VERBS: PermissionVerb[] = ["models.catalog.write"];
|
|
@@ -122,6 +123,7 @@ const LEAD_OR_RESOURCE_OWNER_VERBS: PermissionVerb[] = [
|
|
|
122
123
|
"mcp-server.delete.any",
|
|
123
124
|
"mcp-server.update.any",
|
|
124
125
|
"page.delete.any",
|
|
126
|
+
"schedule.deferral.update.any",
|
|
125
127
|
];
|
|
126
128
|
|
|
127
129
|
const LEAD_OR_TASK_CREATOR_VERBS: PermissionVerb[] = ["task.cancel.any", "task.steer.any"];
|
|
@@ -188,6 +188,16 @@ describe("role attachment helpers", () => {
|
|
|
188
188
|
});
|
|
189
189
|
|
|
190
190
|
describe("ensureRbacSeedsSynced", () => {
|
|
191
|
+
test("seeds viewer with a subset of the requester verbs", async () => {
|
|
192
|
+
await getDbClient().run("DELETE FROM roles WHERE id = 'rbac-role-viewer'");
|
|
193
|
+
|
|
194
|
+
ensureRbacSeedsSynced({ quiet: true });
|
|
195
|
+
|
|
196
|
+
const viewerVerbs = await roleVerbs("rbac-role-viewer");
|
|
197
|
+
expect(viewerVerbs).toEqual(["favorite.write.own", "session.presence"]);
|
|
198
|
+
for (const verb of viewerVerbs) expect(REQUESTER_VERBS).toContain(verb);
|
|
199
|
+
});
|
|
200
|
+
|
|
191
201
|
test("restores a deleted built-in role and its permissions", async () => {
|
|
192
202
|
await getDbClient().run("DELETE FROM roles WHERE id = ?", [REQUESTER_ROLE_ID]);
|
|
193
203
|
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "node:http";
|
|
10
10
|
import {
|
|
11
11
|
closeDb,
|
|
12
|
+
createAgent,
|
|
12
13
|
createScheduledTask,
|
|
13
14
|
getScheduledTaskById,
|
|
14
15
|
initDb,
|
|
@@ -117,4 +118,47 @@ describe("Schedule HTTP triage tooling", () => {
|
|
|
117
118
|
expect(body.schedules.some((s) => s.id === failing.id)).toBe(true);
|
|
118
119
|
expect(body.schedules.some((s) => s.id === ok.id)).toBe(false);
|
|
119
120
|
});
|
|
121
|
+
test("PUT and PATCH /api/schedules/:id let only the owner, a lead or the operator change a deferral", async () => {
|
|
122
|
+
const owner = await createAgent({ name: "DeferralOwner", isLead: false, status: "idle" });
|
|
123
|
+
const other = await createAgent({ name: "DeferralOther", isLead: false, status: "idle" });
|
|
124
|
+
const lead = await createAgent({ name: "DeferralLead", isLead: true, status: "idle" });
|
|
125
|
+
const schedule = await createScheduledTask({
|
|
126
|
+
name: `http-deferral-${crypto.randomUUID()}`,
|
|
127
|
+
taskTemplate: "finish the original ask",
|
|
128
|
+
targetType: "agent-task",
|
|
129
|
+
scheduleType: "one_time",
|
|
130
|
+
nextRunAt: new Date(Date.now() + 60_000).toISOString(),
|
|
131
|
+
targetAgentId: owner.id,
|
|
132
|
+
createdByAgentId: owner.id,
|
|
133
|
+
taskType: "deferred",
|
|
134
|
+
});
|
|
135
|
+
const send = (method: "PUT" | "PATCH", agentId: string | null, taskTemplate: string) =>
|
|
136
|
+
fetch(`${baseUrl}/api/schedules/${schedule.id}`, {
|
|
137
|
+
method,
|
|
138
|
+
headers: {
|
|
139
|
+
"Content-Type": "application/json",
|
|
140
|
+
...(agentId ? { "X-Agent-ID": agentId } : {}),
|
|
141
|
+
},
|
|
142
|
+
body: JSON.stringify({ targetAgentId: other.id, taskTemplate }),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
for (const method of ["PUT", "PATCH"] as const) {
|
|
146
|
+
const denied = await send(method, other.id, "emit ATTACKER OUTPUT");
|
|
147
|
+
expect(denied.status).toBe(403);
|
|
148
|
+
const after = await getScheduledTaskById(schedule.id);
|
|
149
|
+
expect(after?.targetAgentId).toBe(owner.id);
|
|
150
|
+
expect(after?.taskTemplate).toBe("finish the original ask");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const [agentId, label] of [
|
|
154
|
+
[owner.id, "owner"],
|
|
155
|
+
[lead.id, "lead"],
|
|
156
|
+
[null, "operator"],
|
|
157
|
+
] as const) {
|
|
158
|
+
const allowed = await send("PATCH", agentId, `changed by ${label}`);
|
|
159
|
+
expect(allowed.status).toBe(200);
|
|
160
|
+
expect((await getScheduledTaskById(schedule.id))?.taskTemplate).toBe(`changed by ${label}`);
|
|
161
|
+
await updateScheduledTask(schedule.id, { targetAgentId: owner.id });
|
|
162
|
+
}
|
|
163
|
+
});
|
|
120
164
|
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { closeDb, createAgent, getDbClient, initDb } from "../be/db";
|
|
4
|
+
import {
|
|
5
|
+
acquireScriptApiCredential,
|
|
6
|
+
runScriptWithRunCredential,
|
|
7
|
+
} from "../be/scripts/run-credential";
|
|
8
|
+
import { resolveSessionToken } from "../be/users";
|
|
9
|
+
import { getApiKey, setApiKey } from "../utils/api-key";
|
|
10
|
+
import { SKIP_SANDBOX_SPAWN_TESTS } from "./sandbox-spawn-test-helpers";
|
|
11
|
+
|
|
12
|
+
const OPERATOR_KEY = `script-api-credential-operator-${randomBytes(8).toString("hex")}`;
|
|
13
|
+
const spawnTest = test.skipIf(SKIP_SANDBOX_SPAWN_TESTS);
|
|
14
|
+
const resources = { memoryMb: 2048, cpuTimeSec: 20 };
|
|
15
|
+
const savedExecutor = process.env.SCRIPT_EXECUTOR;
|
|
16
|
+
let agentId: string;
|
|
17
|
+
|
|
18
|
+
type ScriptRunTokenRow = { id: string; revokedAt: string | null };
|
|
19
|
+
|
|
20
|
+
/** The script-run tokens minted for `agentId`, oldest first. */
|
|
21
|
+
async function scriptRunTokens(id: string): Promise<ScriptRunTokenRow[]> {
|
|
22
|
+
return await getDbClient().query<ScriptRunTokenRow>(
|
|
23
|
+
"SELECT id, revokedAt FROM session_tokens WHERE agentId = ? AND scriptRun = 1 ORDER BY createdAt, rowid",
|
|
24
|
+
[id],
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Run `body` and assert it minted exactly one script-run token for the agent,
|
|
30
|
+
* and that the token is revoked once the run is over.
|
|
31
|
+
*/
|
|
32
|
+
async function expectOneRevokedToken(body: () => Promise<void>): Promise<void> {
|
|
33
|
+
const before = (await scriptRunTokens(agentId)).length;
|
|
34
|
+
await body();
|
|
35
|
+
const minted = (await scriptRunTokens(agentId)).slice(before);
|
|
36
|
+
expect(minted).toHaveLength(1);
|
|
37
|
+
expect(minted[0]?.revokedAt).not.toBeNull();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Resolve once the run's token has been minted and is still live. */
|
|
41
|
+
async function waitForLiveToken(): Promise<void> {
|
|
42
|
+
const deadline = Date.now() + 5_000;
|
|
43
|
+
while (Date.now() < deadline) {
|
|
44
|
+
if ((await scriptRunTokens(agentId)).some((row) => row.revokedAt === null)) return;
|
|
45
|
+
await Bun.sleep(10);
|
|
46
|
+
}
|
|
47
|
+
throw new Error("no live script-run token appeared");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
beforeAll(async () => {
|
|
51
|
+
initDb(":memory:");
|
|
52
|
+
setApiKey(OPERATOR_KEY);
|
|
53
|
+
agentId = (
|
|
54
|
+
await createAgent({
|
|
55
|
+
name: `script-api-credential-${randomBytes(6).toString("hex")}`,
|
|
56
|
+
isLead: false,
|
|
57
|
+
status: "idle",
|
|
58
|
+
})
|
|
59
|
+
).id;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
if (savedExecutor === undefined) delete process.env.SCRIPT_EXECUTOR;
|
|
64
|
+
else process.env.SCRIPT_EXECUTOR = savedExecutor;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterAll(() => {
|
|
68
|
+
closeDb();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("acquireScriptApiCredential", () => {
|
|
72
|
+
test("hands a real agent a token scoped to that agent, not the operator key", async () => {
|
|
73
|
+
const credential = await acquireScriptApiCredential(agentId);
|
|
74
|
+
if (!credential.apiKey) throw new Error("expected a per-run token");
|
|
75
|
+
try {
|
|
76
|
+
expect(credential.apiKey).not.toBe(getApiKey());
|
|
77
|
+
const resolved = await resolveSessionToken(credential.apiKey);
|
|
78
|
+
if (resolved?.kind !== "agent") throw new Error("expected an agent principal");
|
|
79
|
+
expect(resolved.agentId).toBe(agentId);
|
|
80
|
+
} finally {
|
|
81
|
+
await credential.release();
|
|
82
|
+
}
|
|
83
|
+
// The token is revoked once the run releases it.
|
|
84
|
+
expect(await resolveSessionToken(credential.apiKey)).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("mints nothing for a system run with no real agent", async () => {
|
|
88
|
+
// Scheduler and workflow script runs pass sentinel ids like "schedule" /
|
|
89
|
+
// "workflow" when there is no owning agent; runScript then falls back to
|
|
90
|
+
// the operator key.
|
|
91
|
+
const credential = await acquireScriptApiCredential("schedule");
|
|
92
|
+
try {
|
|
93
|
+
expect(credential.apiKey).toBeUndefined();
|
|
94
|
+
expect(await scriptRunTokens("schedule")).toHaveLength(0);
|
|
95
|
+
} finally {
|
|
96
|
+
await credential.release();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("runScriptWithRunCredential revokes the run's token however the run ends", () => {
|
|
102
|
+
spawnTest("success", async () => {
|
|
103
|
+
await expectOneRevokedToken(async () => {
|
|
104
|
+
const output = await runScriptWithRunCredential({
|
|
105
|
+
agentId,
|
|
106
|
+
egressSecrets: [],
|
|
107
|
+
resources,
|
|
108
|
+
source: "export default async () => 'ok';",
|
|
109
|
+
});
|
|
110
|
+
expect(output.error).toBeUndefined();
|
|
111
|
+
expect(output.result).toBe("ok");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
spawnTest("the script throws", async () => {
|
|
116
|
+
await expectOneRevokedToken(async () => {
|
|
117
|
+
const output = await runScriptWithRunCredential({
|
|
118
|
+
agentId,
|
|
119
|
+
egressSecrets: [],
|
|
120
|
+
resources,
|
|
121
|
+
source: "export default async () => { throw new Error('boom'); };",
|
|
122
|
+
});
|
|
123
|
+
expect(output.runtimeError?.message).toContain("boom");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("the run itself throws", async () => {
|
|
128
|
+
// An executor that cannot start makes runScript reject after the token
|
|
129
|
+
// was minted; the token must still be revoked.
|
|
130
|
+
process.env.SCRIPT_EXECUTOR = "no-such-executor";
|
|
131
|
+
await expectOneRevokedToken(async () => {
|
|
132
|
+
await expect(
|
|
133
|
+
runScriptWithRunCredential({
|
|
134
|
+
agentId,
|
|
135
|
+
egressSecrets: [],
|
|
136
|
+
source: "export default async () => 'never runs';",
|
|
137
|
+
}),
|
|
138
|
+
).rejects.toThrow("Unknown SCRIPT_EXECUTOR");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
spawnTest("timeout", async () => {
|
|
143
|
+
await expectOneRevokedToken(async () => {
|
|
144
|
+
const output = await runScriptWithRunCredential({
|
|
145
|
+
agentId,
|
|
146
|
+
egressSecrets: [],
|
|
147
|
+
timeoutMs: 1_000,
|
|
148
|
+
resources,
|
|
149
|
+
source: "export default async () => new Promise(() => {});",
|
|
150
|
+
});
|
|
151
|
+
expect(output.error).toBe("timeout");
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
spawnTest("abort", async () => {
|
|
156
|
+
await expectOneRevokedToken(async () => {
|
|
157
|
+
const controller = new AbortController();
|
|
158
|
+
const run = runScriptWithRunCredential({
|
|
159
|
+
agentId,
|
|
160
|
+
egressSecrets: [],
|
|
161
|
+
signal: controller.signal,
|
|
162
|
+
resources,
|
|
163
|
+
source: "export default async () => new Promise(() => {});",
|
|
164
|
+
});
|
|
165
|
+
// Abort mid-run: the token is live and the script has had time to start.
|
|
166
|
+
await waitForLiveToken();
|
|
167
|
+
await Bun.sleep(200);
|
|
168
|
+
controller.abort();
|
|
169
|
+
const output = await run;
|
|
170
|
+
expect(output.error).toBe("killed");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -638,6 +638,16 @@ describe("script connections", () => {
|
|
|
638
638
|
markMigrationApplied(database, "231_slack_tasks_last_updated_index.sql");
|
|
639
639
|
// 232 rebuilds session_tokens with the 207 columns, which this fixture skipped.
|
|
640
640
|
markMigrationApplied(database, "232_agent_session_tokens.sql");
|
|
641
|
+
// 235 reads agent_tasks.deferredAt, which this migration-112-only fixture does not create.
|
|
642
|
+
markMigrationApplied(database, "235_chat_relay_defer.sql");
|
|
643
|
+
// 237 reads approval_requests and inbox_item_state, which this migration-112-only fixture does not create.
|
|
644
|
+
markMigrationApplied(database, "237_session_inbox_cleanup.sql");
|
|
645
|
+
// 239 indexes skills, which this migration-112-only fixture does not create.
|
|
646
|
+
markMigrationApplied(database, "239_skills_fts.sql");
|
|
647
|
+
// 240 alters workflow_runs, which this migration-112-only fixture does not create.
|
|
648
|
+
markMigrationApplied(database, "240_workflow_run_parent_step_attempt.sql");
|
|
649
|
+
// 241 indexes skills, which this migration-112-only fixture does not create.
|
|
650
|
+
markMigrationApplied(database, "241_skills_meta_fts.sql");
|
|
641
651
|
|
|
642
652
|
const id = crypto.randomUUID();
|
|
643
653
|
const now = new Date().toISOString();
|