@desplega.ai/agent-swarm 2.4.3 → 2.4.4
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-1885dpr7.js → acp-adapter-thq04adh.js} +1 -2
- package/dist/{actions-pn76n32q.js → actions-j6ywh6x5.js} +17 -17
- package/dist/{amp-adapter-af84rx1d.js → amp-adapter-k8b5xmj8.js} +2 -2
- package/dist/{app-6sv8mhzm.js → app-rb7vc02s.js} +3 -3
- package/dist/{assistant-sc2tf825.js → assistant-ftta1zjf.js} +18 -18
- package/dist/{boot-reembed-y3m0ydzj.js → boot-reembed-srp6v6af.js} +1 -1
- package/dist/{boot-reembed-gvxjs2py.js → boot-reembed-xns7h5h9.js} +1 -1
- package/dist/{boot-scrub-sweep-1j9mk05f.js → boot-scrub-sweep-3qqrh2mz.js} +2 -2
- package/dist/{boot-seeds-37sp626b.js → boot-seeds-9ks8hce7.js} +4 -4
- package/dist/{claude-adapter-rp0jtnwr.js → claude-adapter-k5ksyres.js} +2 -2
- package/dist/{claude-managed-setup-rdt248k7.js → claude-managed-setup-n5a5eqks.js} +1 -1
- package/dist/{claude-sdk-session-53rcw5we.js → claude-sdk-session-2j3j4kwn.js} +2 -2
- package/dist/{cli-v87dr6e3.js → cli-1d70crsy.js} +1 -1
- package/dist/{cli-1ma335fz.js → cli-1ephgwnq.js} +2 -2
- package/dist/{cli-4yef8ghv.js → cli-2v045xyf.js} +3 -3
- package/dist/{cli-73ezfhha.js → cli-2y91wnbh.js} +10 -5
- package/dist/{cli-1eszr343.js → cli-2zqpkvtt.js} +1 -1
- package/dist/{cli-e4d78xsv.js → cli-37yb7307.js} +1 -1
- package/dist/{cli-8y99kfey.js → cli-3cgngyd8.js} +9 -9
- package/dist/{cli-wk28hzqn.js → cli-44159k28.js} +1 -1
- package/dist/{cli-qp2m4btv.js → cli-4dg55wsk.js} +2 -2
- package/dist/{cli-0dxxwfkt.js → cli-51y09c6p.js} +17 -14
- package/dist/{cli-aa3tb29r.js → cli-55qmbp6z.js} +1 -1
- package/dist/{cli-mzkwkbdy.js → cli-82d3nkc3.js} +1 -1
- package/dist/{cli-sds5zzs8.js → cli-9fhkkpj5.js} +78 -12
- package/dist/{cli-vq9gqarq.js → cli-amyhm3gj.js} +1 -1
- package/dist/{cli-bxrqvpg5.js → cli-b8k7epj1.js} +5 -5
- package/dist/{cli-8vverzf7.js → cli-ba8ne1es.js} +5 -0
- package/dist/{cli-58vwn100.js → cli-bnz0r3kc.js} +1 -1
- package/dist/{cli-6eyncg0y.js → cli-cynvc9fj.js} +34 -6
- package/dist/{cli-f0p0f6ja.js → cli-dqb4n629.js} +7 -7
- package/dist/{cli-pygdsgh8.js → cli-esxerzg7.js} +1 -1
- package/dist/{cli-dm8p2dpf.js → cli-gqvh8d8a.js} +6 -6
- package/dist/{cli-p0qerd2r.js → cli-hj3debgw.js} +1 -1
- package/dist/{cli-836ehxmt.js → cli-nc35pxb7.js} +2 -2
- package/dist/{cli-vcgscaan.js → cli-p20r8w42.js} +8 -6
- package/dist/{cli-1xsraagq.js → cli-pccfmtb5.js} +4 -4
- package/dist/{cli-s9rr4gr2.js → cli-q6r7t299.js} +1036 -298
- package/dist/{cli-tkf4x6ha.js → cli-qkhc5aqy.js} +11 -11
- package/dist/{cli-1n9gdg56.js → cli-t7cyef5g.js} +4 -4
- package/dist/{cli-038fga5k.js → cli-te6b5wvg.js} +148 -183
- package/dist/{cli-kgnkyabk.js → cli-v3yr2gbg.js} +109 -35
- package/dist/{cli-h0mgxann.js → cli-vdm20wej.js} +2 -2
- package/dist/{cli-zvbb4n0e.js → cli-vq97g1jq.js} +2 -2
- package/dist/{cli-gx0j2khs.js → cli-w6nadkh4.js} +3 -3
- package/dist/{cli-99jez72r.js → cli-w9etnnxk.js} +6 -1
- package/dist/{cli-npfe9t1s.js → cli-xc27fwzg.js} +2 -2
- package/dist/{cli-6aen3ezb.js → cli-xj0re5tm.js} +1 -1
- package/dist/{cli-aypbg9r2.js → cli-zrk6jxv8.js} +1 -1
- package/dist/{cli-1012xb7j.js → cli-zrwpctp4.js} +20 -20
- package/dist/cli.js +41 -16
- package/dist/{codex-adapter-2zrmab9b.js → codex-adapter-968xwfy7.js} +1 -1
- package/dist/{codex-session-runner-kbkz62vc.js → codex-session-runner-ht4z99fa.js} +1 -1
- package/dist/{commands-hd574th6.js → commands-pcymaeg9.js} +2 -2
- package/dist/{controller-pz5mcfg4.js → controller-fpjyk46r.js} +2 -2
- package/dist/{controllers-connect-5rgq2hfk.js → controllers-connect-mwzx1fh6.js} +18 -18
- package/dist/{cursor-adapter-mgaprq0j.js → cursor-adapter-cp4s2e2f.js} +1 -1
- package/dist/{db-h06dxp05.js → db-sgd12bt9.js} +3 -3
- package/dist/{db-6nf1207q.js → db-xsjg9jqq.js} +1 -1
- package/dist/{dispatcher-zthpy4m1.js → dispatcher-x0jfw2c9.js} +16 -16
- package/dist/{dsh-adapter-64dhw0j7.js → dsh-adapter-jrq7ma7h.js} +1 -1
- package/dist/{external-channel-x01t7ke6.js → external-channel-8njj4h3b.js} +2 -2
- package/dist/{grok-adapter-h21jv594.js → grok-adapter-kkr97v2f.js} +2 -3
- package/dist/handlers-0pvee9nw.js +39 -0
- package/dist/{heartbeat-zzfksckz.js → heartbeat-wd03zhn0.js} +19 -19
- package/dist/{hook-z4zhzj1f.js → hook-f0vghj42.js} +1 -1
- package/dist/{hook-3xfqhh2s.js → hook-xzj2ts2p.js} +1 -1
- package/dist/{http-q813tmya.js → http-4hwxv62p.js} +255 -117
- package/dist/{index-va3y63es.js → index-6srwvd5b.js} +1 -2
- package/dist/{index-220tk9bw.js → index-97q4xn03.js} +19 -10
- package/dist/{index-s98037v0.js → index-cbxkmfc5.js} +1 -2
- package/dist/{index-m0a4ydqj.js → index-gdh8jrdm.js} +16 -16
- package/dist/{index-wbbs14c5.js → index-hg0x2cdv.js} +16 -16
- package/dist/{index-d0g18bax.js → index-thm0y6a8.js} +19 -19
- package/dist/{index-x8xbyn1t.js → index-ww2vznkp.js} +1 -1
- package/dist/{keepalive-y5t1my7m.js → keepalive-hrh7xnr9.js} +3 -3
- package/dist/{lead-2xwtq66b.js → lead-ee8vazz2.js} +26 -27
- package/dist/{maintenance-ybfsaedv.js → maintenance-e8wnwnqx.js} +1 -1
- package/dist/{oauth-refresh-sweep-909jmn12.js → oauth-refresh-sweep-zm0yj4sj.js} +2 -2
- package/dist/{onboard-xn6fkkys.js → onboard-dtwcq603.js} +2 -2
- package/dist/{otel-impl-e6mn0z13.js → otel-impl-w3fbhpn0.js} +1 -1
- package/dist/{pricing-refresh-ygd362ep.js → pricing-refresh-j38dehfs.js} +3 -3
- package/dist/{rbac-roles-c9axvfnz.js → rbac-roles-ae8yttq4.js} +2 -2
- package/dist/{rbac-roles-mb470p4e.js → rbac-roles-ejxx9b35.js} +2 -2
- package/dist/{render-v2-g0306nh1.js → render-v2-n16n0phe.js} +3 -3
- package/dist/{resume-v315hnq6.js → resume-z0fgjc38.js} +5 -5
- package/dist/{session-access-2zg285a1.js → session-access-cv3mq6mc.js} +6 -4
- package/dist/{setup-a4mtf02k.js → setup-mafhj3yt.js} +1 -1
- package/dist/{work-objects-9fr0fgtn.js → work-objects-8b86mgjp.js} +19 -19
- package/dist/{worker-qae22jeb.js → worker-16s75hm5.js} +26 -27
- package/dist/worker-token-xf11yyds.js +136 -0
- package/openapi.json +295 -12
- package/package.json +4 -4
- package/src/apps/action-runner.ts +4 -0
- package/src/apps/catalog.generated.json +3 -0
- package/src/apps/page-validator.ts +8 -2
- package/src/be/migrations/232_agent_session_tokens.sql +41 -0
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +10 -1
- package/src/be/swarm-config-guard.ts +16 -0
- package/src/be/users.ts +169 -12
- package/src/cli.tsx +24 -0
- package/src/commands/worker-token.ts +164 -0
- package/src/http/apps.ts +43 -5
- package/src/http/codex-oauth-keep-warm.ts +27 -9
- package/src/http/config.ts +5 -3
- package/src/http/core.ts +2 -0
- package/src/http/session-access.ts +51 -2
- package/src/http/sessions.ts +249 -10
- package/src/http/tasks.ts +4 -32
- package/src/providers/acp-adapter.ts +33 -13
- package/src/providers/acp-swarm-events.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-oauth/storage.ts +1 -1
- package/src/rbac/legacy-policy.ts +3 -1
- package/src/rbac/permissions.ts +4 -0
- package/src/tests/acp-adapter.test.ts +10 -23
- package/src/tests/acp-fake-agent.ts +36 -0
- package/src/tests/acp-swarm-events.test.ts +1 -1
- package/src/tests/agent-session-tokens.test.ts +452 -0
- package/src/tests/apps-action-parent-task.test.ts +295 -0
- package/src/tests/codex-oauth-keep-warm.test.ts +85 -1
- package/src/tests/config-session-auth.test.ts +14 -0
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/grok-adapter.test.ts +3 -3
- package/src/tests/heartbeat-jev-gate.test.ts +23 -0
- package/src/tests/migration-232-agent-session-tokens.test.ts +67 -0
- package/src/tests/rbac-engine.test.ts +1 -0
- package/src/tests/runtime-child-token-cleanup.test.ts +3 -3
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/session-route-posture.test.ts +2 -0
- package/src/tests/worker-token-cli.test.ts +76 -0
- package/src/tests/workflow-executors.test.ts +4 -2
- package/src/tests/workflow-system-one-extraction.test.ts +805 -0
- package/src/types.ts +2 -1
- package/src/utils/request-auth-context.ts +2 -1
- package/src/workflows/definition.ts +7 -1
- package/src/workflows/engine.ts +18 -12
- package/src/workflows/executors/index.ts +1 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/executors/system-one-decision.ts +56 -255
- package/src/workflows/executors/system-one-extract-wire.ts +57 -0
- package/src/workflows/executors/system-one-extraction.ts +830 -0
- package/src/workflows/executors/system-one-providers.ts +277 -110
- package/src/workflows/executors/system-one-transport.ts +264 -0
- package/templates/official/lead/HEARTBEAT.md +1 -1
- package/templates/skills/apps/SKILL.md +3 -1
- package/templates/skills/apps/content.md +3 -1
- package/templates/skills/heartbeat-runbook/content.md +3 -3
- package/dist/cli-v1sjhn52.js +0 -7
- package/dist/handlers-2qjw434w.js +0 -39
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
createServer as createHttpServer,
|
|
5
|
+
type IncomingMessage,
|
|
6
|
+
type Server,
|
|
7
|
+
type ServerResponse,
|
|
8
|
+
} from "node:http";
|
|
9
|
+
import { parseAppDefinition } from "../apps/definition";
|
|
10
|
+
import { createApp } from "../apps/store";
|
|
11
|
+
import {
|
|
12
|
+
closeDb,
|
|
13
|
+
createAgent,
|
|
14
|
+
createTask,
|
|
15
|
+
createTaskExtended,
|
|
16
|
+
createUser,
|
|
17
|
+
getTaskById,
|
|
18
|
+
initDb,
|
|
19
|
+
} from "../be/db";
|
|
20
|
+
import { type IdentityActor, mintToken } from "../be/users";
|
|
21
|
+
import { handleApps } from "../http/apps";
|
|
22
|
+
import { resolveHttpRequestAuth } from "../http/auth";
|
|
23
|
+
import { SESSION_WRITE_DENIED } from "../http/session-access";
|
|
24
|
+
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
25
|
+
import { setRequestAuth } from "../utils/request-auth-context";
|
|
26
|
+
import { listenOnFreePort } from "./test-net";
|
|
27
|
+
|
|
28
|
+
const TEST_DB_PATH = `/private/tmp/test-apps-action-parent-task-${process.pid}.sqlite`;
|
|
29
|
+
const API_KEY = "example-apps-action-parent-task-key";
|
|
30
|
+
const OPERATOR_ACTOR: IdentityActor = { kind: "operator", id: "apps-action-parent-task-test" };
|
|
31
|
+
/** Non-lead worker: the default caller. */
|
|
32
|
+
const AGENT_ID = crypto.randomUUID();
|
|
33
|
+
const LEAD_ID = crypto.randomUUID();
|
|
34
|
+
/** Owns the parent task the worker has no context for. */
|
|
35
|
+
const OTHER_AGENT_ID = crypto.randomUUID();
|
|
36
|
+
const TASK_ACTION = "generateScreen";
|
|
37
|
+
const SYNC_ACTION = "syncNotes";
|
|
38
|
+
const CHAIN_ISSUE = "$row, $rowIndex, $form, and $chain bindings are only allowed in action params";
|
|
39
|
+
|
|
40
|
+
let server: Server;
|
|
41
|
+
let base = "";
|
|
42
|
+
/** A user with no share on anyone else's session. */
|
|
43
|
+
let userToken = "";
|
|
44
|
+
let previousApiKey: string | undefined;
|
|
45
|
+
|
|
46
|
+
function createTestServer(): Server {
|
|
47
|
+
return createHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
48
|
+
setRequestAuth(req, await resolveHttpRequestAuth(req, API_KEY));
|
|
49
|
+
res.setHeader("Content-Type", "application/json");
|
|
50
|
+
const pathSegments = getPathSegments(req.url || "");
|
|
51
|
+
const queryParams = parseQueryParams(req.url || "");
|
|
52
|
+
const myAgentId = req.headers["x-agent-id"] as string | undefined;
|
|
53
|
+
const handled = await handleApps(req, res, pathSegments, queryParams, myAgentId);
|
|
54
|
+
if (!handled) {
|
|
55
|
+
res.writeHead(404);
|
|
56
|
+
res.end(JSON.stringify({ error: "not found" }));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Runs an action as an agent (default: the worker) or, with `bearer`, as a token holder. */
|
|
62
|
+
async function runAction<T>(
|
|
63
|
+
appId: string,
|
|
64
|
+
body: Record<string, unknown>,
|
|
65
|
+
options: { agentId?: string; bearer?: string; action?: string } = {},
|
|
66
|
+
): Promise<{ status: number; body: T }> {
|
|
67
|
+
const action = options.action ?? TASK_ACTION;
|
|
68
|
+
const response = await fetch(`${base}/api/apps/${appId}/actions/${action}`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: {
|
|
71
|
+
"Content-Type": "application/json",
|
|
72
|
+
...(options.bearer
|
|
73
|
+
? { Authorization: `Bearer ${options.bearer}` }
|
|
74
|
+
: { "X-Agent-ID": options.agentId ?? AGENT_ID }),
|
|
75
|
+
},
|
|
76
|
+
body: JSON.stringify(body),
|
|
77
|
+
});
|
|
78
|
+
return { status: response.status, body: (await response.json()) as T };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A page whose Button chains a task-kind action under the chain scope's leaf task. */
|
|
82
|
+
function chainedButtonPage() {
|
|
83
|
+
return {
|
|
84
|
+
root: "root",
|
|
85
|
+
elements: {
|
|
86
|
+
root: { type: "Container", props: {}, children: ["send"] },
|
|
87
|
+
send: {
|
|
88
|
+
type: "Button",
|
|
89
|
+
props: { label: "Send" },
|
|
90
|
+
on: {
|
|
91
|
+
press: [
|
|
92
|
+
{
|
|
93
|
+
action: "app.action",
|
|
94
|
+
params: {
|
|
95
|
+
name: TASK_ACTION,
|
|
96
|
+
input: { prompt: "next step" },
|
|
97
|
+
parentTaskId: { $chain: "leafTaskId" },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function definitionWith(page: Record<string, unknown>) {
|
|
108
|
+
return {
|
|
109
|
+
models: {
|
|
110
|
+
note: {
|
|
111
|
+
columns: { title: { kind: "string" } },
|
|
112
|
+
// The sync action needs one (model x source) pair to be valid.
|
|
113
|
+
sources: { tracker: { connector: "swarm-tasks", joinKey: "title" } },
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
queries: { allNotes: { model: "note" } },
|
|
117
|
+
actions: {
|
|
118
|
+
[TASK_ACTION]: { kind: "task", prompt: "Generate the next screen" },
|
|
119
|
+
// A non-task action, to show parentTaskId is rejected rather than dropped.
|
|
120
|
+
[SYNC_ACTION]: { kind: "sync" },
|
|
121
|
+
},
|
|
122
|
+
pages: { main: page },
|
|
123
|
+
defaultPage: "main",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Stores the chained-Button app the way the API does: validated first. */
|
|
128
|
+
async function createChainedApp(): Promise<string> {
|
|
129
|
+
const parsed = await parseAppDefinition(definitionWith(chainedButtonPage()));
|
|
130
|
+
if (!parsed.success) throw new Error(JSON.stringify(parsed.issues));
|
|
131
|
+
const app = await createApp({
|
|
132
|
+
name: `Chained ${crypto.randomUUID()}`,
|
|
133
|
+
definition: parsed.definition,
|
|
134
|
+
});
|
|
135
|
+
return app.id;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
beforeAll(async () => {
|
|
139
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
140
|
+
try {
|
|
141
|
+
await unlink(`${TEST_DB_PATH}${suffix}`);
|
|
142
|
+
} catch {}
|
|
143
|
+
}
|
|
144
|
+
// Safe to mutate: CI runs one process per test file (`bun test --parallel=4`)
|
|
145
|
+
// and afterAll restores the previous value.
|
|
146
|
+
previousApiKey = process.env.AGENT_SWARM_API_KEY;
|
|
147
|
+
process.env.AGENT_SWARM_API_KEY = API_KEY;
|
|
148
|
+
initDb(TEST_DB_PATH);
|
|
149
|
+
const user = await createUser({ name: "Parent Task User" });
|
|
150
|
+
userToken = (await mintToken(user.id, "apps-action-parent-task", OPERATOR_ACTOR)).plaintext;
|
|
151
|
+
await createAgent({ id: AGENT_ID, name: "parent-task-worker", isLead: false, status: "idle" });
|
|
152
|
+
await createAgent({ id: LEAD_ID, name: "parent-task-lead", isLead: true, status: "idle" });
|
|
153
|
+
await createAgent({
|
|
154
|
+
id: OTHER_AGENT_ID,
|
|
155
|
+
name: "parent-task-other",
|
|
156
|
+
isLead: false,
|
|
157
|
+
status: "idle",
|
|
158
|
+
});
|
|
159
|
+
server = createTestServer();
|
|
160
|
+
const port = await listenOnFreePort(server, "127.0.0.1");
|
|
161
|
+
base = `http://127.0.0.1:${port}`;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
afterAll(async () => {
|
|
165
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
166
|
+
closeDb();
|
|
167
|
+
if (previousApiKey === undefined) delete process.env.AGENT_SWARM_API_KEY;
|
|
168
|
+
else process.env.AGENT_SWARM_API_KEY = previousApiKey;
|
|
169
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
170
|
+
try {
|
|
171
|
+
await unlink(`${TEST_DB_PATH}${suffix}`);
|
|
172
|
+
} catch {}
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("$chain sentinel in the page validator", () => {
|
|
177
|
+
test("accepts parentTaskId: { $chain } inside an app.action step's params", async () => {
|
|
178
|
+
const parsed = await parseAppDefinition(definitionWith(chainedButtonPage()));
|
|
179
|
+
expect(parsed.success).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("accepts a $chain sentinel nested deeper inside app.action params", async () => {
|
|
183
|
+
const page = chainedButtonPage();
|
|
184
|
+
page.elements.send.on.press[0]!.params = {
|
|
185
|
+
name: TASK_ACTION,
|
|
186
|
+
input: { context: { parentTaskId: { $chain: "leafTaskId" } } },
|
|
187
|
+
};
|
|
188
|
+
const parsed = await parseAppDefinition(definitionWith(page));
|
|
189
|
+
expect(parsed.success).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("rejects a $chain sentinel used as a plain prop value", async () => {
|
|
193
|
+
const page = chainedButtonPage();
|
|
194
|
+
page.elements = {
|
|
195
|
+
...page.elements,
|
|
196
|
+
echo: { type: "Text", props: { content: { $chain: "leafTaskId" } } },
|
|
197
|
+
} as typeof page.elements;
|
|
198
|
+
page.elements.root.children = ["send", "echo"];
|
|
199
|
+
const parsed = await parseAppDefinition(definitionWith(page));
|
|
200
|
+
expect(parsed.success).toBe(false);
|
|
201
|
+
if (parsed.success) return;
|
|
202
|
+
expect(parsed.issues.some((entry) => entry.message === CHAIN_ISSUE)).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("POST /api/apps/:id/actions/:name with parentTaskId", () => {
|
|
207
|
+
test("chains the created task under the given parent", async () => {
|
|
208
|
+
const appId = await createChainedApp();
|
|
209
|
+
const parent = await createTask(AGENT_ID, "Previous turn of the chain");
|
|
210
|
+
|
|
211
|
+
const started = await runAction<{ ok: boolean; taskId: string; status: string }>(appId, {
|
|
212
|
+
input: { prompt: "next step" },
|
|
213
|
+
parentTaskId: parent.id,
|
|
214
|
+
});
|
|
215
|
+
expect(started.status).toBe(200);
|
|
216
|
+
expect(started.body).toMatchObject({ ok: true, status: "pending" });
|
|
217
|
+
|
|
218
|
+
const created = await getTaskById(started.body.taskId);
|
|
219
|
+
expect(created?.parentTaskId).toBe(parent.id);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("answers 404 for an unknown parent instead of storing a dangling id", async () => {
|
|
223
|
+
const appId = await createChainedApp();
|
|
224
|
+
const missing = await runAction<{ error: string }>(appId, {
|
|
225
|
+
input: {},
|
|
226
|
+
parentTaskId: crypto.randomUUID(),
|
|
227
|
+
});
|
|
228
|
+
expect(missing.status).toBe(404);
|
|
229
|
+
expect(missing.body.error).toBe("parent task not found");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("denies an agent caller without context for the parent, as POST /api/tasks does", async () => {
|
|
233
|
+
const appId = await createChainedApp();
|
|
234
|
+
const foreign = await createTask(OTHER_AGENT_ID, "Someone else's thread");
|
|
235
|
+
|
|
236
|
+
const denied = await runAction<{ error: string }>(appId, {
|
|
237
|
+
input: {},
|
|
238
|
+
parentTaskId: foreign.id,
|
|
239
|
+
});
|
|
240
|
+
expect(denied.status).toBe(403);
|
|
241
|
+
expect(denied.body.error).toBe(`You don't have context for parent task ${foreign.id}.`);
|
|
242
|
+
|
|
243
|
+
// A lead keeps its `task.parent.attach` grant: the gate is about identity, not the route.
|
|
244
|
+
const asLead = await runAction<{ taskId: string }>(
|
|
245
|
+
appId,
|
|
246
|
+
{ input: {}, parentTaskId: foreign.id },
|
|
247
|
+
{ agentId: LEAD_ID },
|
|
248
|
+
);
|
|
249
|
+
expect(asLead.status).toBe(200);
|
|
250
|
+
expect((await getTaskById(asLead.body.taskId))?.parentTaskId).toBe(foreign.id);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("denies a user caller without a write share on the parent's session", async () => {
|
|
254
|
+
const appId = await createChainedApp();
|
|
255
|
+
const owner = await createUser({ name: "Session Owner" });
|
|
256
|
+
const theirs = await createTaskExtended("Someone else's session", {
|
|
257
|
+
agentId: OTHER_AGENT_ID,
|
|
258
|
+
requestedByUserId: owner.id,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const denied = await runAction<{ error: string }>(
|
|
262
|
+
appId,
|
|
263
|
+
{ input: {}, parentTaskId: theirs.id },
|
|
264
|
+
{ bearer: userToken },
|
|
265
|
+
);
|
|
266
|
+
expect(denied.status).toBe(403);
|
|
267
|
+
expect(denied.body.error).toBe(SESSION_WRITE_DENIED);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("rejects parentTaskId on a non-task action instead of dropping it", async () => {
|
|
271
|
+
const appId = await createChainedApp();
|
|
272
|
+
const parent = await createTask(AGENT_ID, "Previous turn of the chain");
|
|
273
|
+
|
|
274
|
+
const rejected = await runAction<{ error: string }>(
|
|
275
|
+
appId,
|
|
276
|
+
{ input: {}, parentTaskId: parent.id },
|
|
277
|
+
{ action: SYNC_ACTION },
|
|
278
|
+
);
|
|
279
|
+
expect(rejected.status).toBe(400);
|
|
280
|
+
expect(rejected.body.error).toBe("parentTaskId is only supported for task-kind actions");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("rejects an empty parentTaskId", async () => {
|
|
284
|
+
const appId = await createChainedApp();
|
|
285
|
+
const rejected = await runAction<{ error: string }>(appId, { input: {}, parentTaskId: "" });
|
|
286
|
+
expect(rejected.status).toBe(400);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("still creates a parentless task when parentTaskId is omitted", async () => {
|
|
290
|
+
const appId = await createChainedApp();
|
|
291
|
+
const started = await runAction<{ ok: boolean; taskId: string }>(appId, { input: {} });
|
|
292
|
+
expect(started.status).toBe(200);
|
|
293
|
+
expect((await getTaskById(started.body.taskId))?.parentTaskId ?? null).toBeNull();
|
|
294
|
+
});
|
|
295
|
+
});
|
|
@@ -10,7 +10,8 @@ import { afterAll, afterEach, beforeAll, describe, expect, it } from "bun:test";
|
|
|
10
10
|
import { unlink } from "node:fs/promises";
|
|
11
11
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
12
12
|
import { closeDb, initDb, upsertKv } from "../be/db";
|
|
13
|
-
import {
|
|
13
|
+
import { validateConfigValue } from "../be/swarm-config-guard";
|
|
14
|
+
import { handleCodexOAuthKeepWarm, keepWarmMaxAgeMs } from "../http/codex-oauth-keep-warm";
|
|
14
15
|
import { handleOAuthLocks } from "../http/oauth-locks";
|
|
15
16
|
import { resetFetchForTesting, setFetchForTesting } from "../providers/codex-oauth/flow.js";
|
|
16
17
|
import type { CodexOAuthCredentials } from "../providers/codex-oauth/types.js";
|
|
@@ -176,6 +177,70 @@ describe("POST /api/oauth/keep-warm/codex", () => {
|
|
|
176
177
|
expect(slots.get(1)?.access).toBe("example-at_keepwarm");
|
|
177
178
|
});
|
|
178
179
|
|
|
180
|
+
it("refreshes a 6-day-old slot by default, before the ~6d20h point where OpenAI ended a session", async () => {
|
|
181
|
+
const slots = new Map<number, CodexOAuthCredentials>([
|
|
182
|
+
[
|
|
183
|
+
0,
|
|
184
|
+
{
|
|
185
|
+
access: "at_six_days",
|
|
186
|
+
refresh: "rt_six_days",
|
|
187
|
+
expires: Date.now() + 4 * 24 * 60 * 60 * 1000, // issued ~6 days ago (10d TTL)
|
|
188
|
+
accountId: "acc-six",
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
]);
|
|
192
|
+
installMockTransport(slots);
|
|
193
|
+
setFetchForTesting(
|
|
194
|
+
async () =>
|
|
195
|
+
new Response(
|
|
196
|
+
JSON.stringify({
|
|
197
|
+
access_token: "example-at_six_days_new",
|
|
198
|
+
refresh_token: "example-rt_six_days_new",
|
|
199
|
+
expires_in: 864000,
|
|
200
|
+
}),
|
|
201
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const { req, res, captured } = fakeReqRes();
|
|
206
|
+
await handleCodexOAuthKeepWarm(req, res, ["api", "oauth", "keep-warm", "codex"]);
|
|
207
|
+
|
|
208
|
+
const body = JSON.parse(captured.body) as { results: Array<Record<string, unknown>> };
|
|
209
|
+
expect(body.results[0]).toMatchObject({ slot: 0, outcome: "refreshed" });
|
|
210
|
+
expect(slots.get(0)?.refresh).toBe("example-rt_six_days_new");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("honors CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS read at sweep time", async () => {
|
|
214
|
+
const saved = process.env.CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS;
|
|
215
|
+
process.env.CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS = "168";
|
|
216
|
+
try {
|
|
217
|
+
const slots = new Map<number, CodexOAuthCredentials>([
|
|
218
|
+
[
|
|
219
|
+
0,
|
|
220
|
+
{
|
|
221
|
+
access: "at_six_days",
|
|
222
|
+
refresh: "rt_six_days",
|
|
223
|
+
expires: Date.now() + 4 * 24 * 60 * 60 * 1000, // issued ~6 days ago, under 168h
|
|
224
|
+
accountId: "acc-six",
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
]);
|
|
228
|
+
installMockTransport(slots);
|
|
229
|
+
setFetchForTesting(async () => {
|
|
230
|
+
throw new Error("no /oauth/token exchange should happen under a 168h threshold");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const { req, res, captured } = fakeReqRes();
|
|
234
|
+
await handleCodexOAuthKeepWarm(req, res, ["api", "oauth", "keep-warm", "codex"]);
|
|
235
|
+
|
|
236
|
+
const body = JSON.parse(captured.body) as { results: Array<Record<string, unknown>> };
|
|
237
|
+
expect(body.results[0]).toMatchObject({ slot: 0, outcome: "warm" });
|
|
238
|
+
} finally {
|
|
239
|
+
if (saved === undefined) delete process.env.CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS;
|
|
240
|
+
else process.env.CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS = saved;
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
179
244
|
it("skips a slot with an active codex-auth-watch bench marker", async () => {
|
|
180
245
|
const keySuffix = "c-old"; // last 5 of accountId (no chatgpt_user_id in this fixture's access token)
|
|
181
246
|
await upsertKv({
|
|
@@ -284,6 +349,25 @@ describe("POST /api/oauth/keep-warm/codex", () => {
|
|
|
284
349
|
}
|
|
285
350
|
});
|
|
286
351
|
|
|
352
|
+
it("keepWarmMaxAgeMs defaults to 120h and ignores invalid or out-of-range values", () => {
|
|
353
|
+
const hour = 60 * 60 * 1000;
|
|
354
|
+
expect(keepWarmMaxAgeMs({})).toBe(120 * hour);
|
|
355
|
+
expect(keepWarmMaxAgeMs({ CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS: " 72 " })).toBe(72 * hour);
|
|
356
|
+
expect(keepWarmMaxAgeMs({ CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS: "216" })).toBe(216 * hour);
|
|
357
|
+
for (const bad of ["0", "217", "1.5", "-3", "abc", ""]) {
|
|
358
|
+
expect(keepWarmMaxAgeMs({ CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS: bad })).toBe(120 * hour);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("the config API rejects the values keepWarmMaxAgeMs would fall back from", () => {
|
|
363
|
+
const key = "CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS";
|
|
364
|
+
expect(validateConfigValue(key, "1")).toBeNull();
|
|
365
|
+
expect(validateConfigValue(key, "216")).toBeNull();
|
|
366
|
+
expect(validateConfigValue(key, "0")).toContain("between 1 and 216");
|
|
367
|
+
expect(validateConfigValue(key, "217")).toContain("between 1 and 216");
|
|
368
|
+
expect(validateConfigValue(key, "1.5")).toContain("integer");
|
|
369
|
+
});
|
|
370
|
+
|
|
287
371
|
it("does not match unrelated paths", async () => {
|
|
288
372
|
const { req, res } = fakeReqRes();
|
|
289
373
|
const handled = await handleCodexOAuthKeepWarm(req, res, [
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
2
|
import { createServer, type Server } from "node:http";
|
|
3
3
|
import { closeDb, createAgent, initDb, upsertSwarmConfig } from "../be/db";
|
|
4
|
+
import { mintAgentToken, revokeAgentTokens } from "../be/users";
|
|
4
5
|
import { handleConfig } from "../http/config";
|
|
5
6
|
import { handleCore } from "../http/core";
|
|
6
7
|
import { handleSessions } from "../http/sessions";
|
|
@@ -116,6 +117,19 @@ describe("config HTTP session-token authorization", () => {
|
|
|
116
117
|
expect((await response.json()).configs[0].value).toBe("global");
|
|
117
118
|
});
|
|
118
119
|
|
|
120
|
+
test("agent token resolved config without agentId reads its own agent scope", async () => {
|
|
121
|
+
const token = await mintAgentToken(workerId);
|
|
122
|
+
try {
|
|
123
|
+
const response = await fetch(`${baseUrl}/api/config/resolved?includeSecrets=true`, {
|
|
124
|
+
headers: headers(token.plaintext),
|
|
125
|
+
});
|
|
126
|
+
expect(response.status).toBe(200);
|
|
127
|
+
expect(await response.text()).toContain(SECRET);
|
|
128
|
+
} finally {
|
|
129
|
+
await revokeAgentTokens(workerId);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
119
133
|
for (const method of ["PUT", "DELETE"] as const) {
|
|
120
134
|
test(`${method} cannot escalate a non-lead session by changing only X-Agent-ID`, async () => {
|
|
121
135
|
const token = await mint(workerId);
|
|
@@ -103,8 +103,8 @@ describe("docs API index footprint", () => {
|
|
|
103
103
|
expect(spec.paths["/api/agents/{id}/runtime"]?.get).toBeDefined();
|
|
104
104
|
|
|
105
105
|
const generatedKeys = await generatedOperationKeys();
|
|
106
|
-
expect(operationKeys).toHaveLength(
|
|
107
|
-
expect(generatedKeys).toHaveLength(
|
|
106
|
+
expect(operationKeys).toHaveLength(440);
|
|
107
|
+
expect(generatedKeys).toHaveLength(440);
|
|
108
108
|
expect(new Set(generatedKeys)).toEqual(new Set(operationKeys));
|
|
109
109
|
});
|
|
110
110
|
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from "../providers/grok-adapter";
|
|
31
31
|
import type { ProviderEvent, ProviderSessionConfig } from "../providers/types";
|
|
32
32
|
import { getModelAwareCredentialVars } from "../utils/credentials";
|
|
33
|
+
import { ACP_SDK_PATH, serveFakeAcpAgent } from "./acp-fake-agent";
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Recorded from a live `grok agent stdio` session (Grok CLI 1.0.46,
|
|
@@ -39,7 +40,6 @@ import { getModelAwareCredentialVars } from "../utils/credentials";
|
|
|
39
40
|
*/
|
|
40
41
|
const FIXTURE = join(import.meta.dir, "fixtures", "grok", "acp-updates-recorded.jsonl");
|
|
41
42
|
const PROMPT_META = join(import.meta.dir, "fixtures", "grok", "prompt-response-meta.json");
|
|
42
|
-
const SDK_PATH = join(process.cwd(), "node_modules/@agentclientprotocol/sdk/dist/acp.js");
|
|
43
43
|
|
|
44
44
|
const tmpDirs: string[] = [];
|
|
45
45
|
|
|
@@ -129,7 +129,7 @@ function writeFakeGrok(
|
|
|
129
129
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
130
130
|
import { join } from "node:path";
|
|
131
131
|
import { Readable, Writable } from "node:stream";
|
|
132
|
-
import {
|
|
132
|
+
import { PROTOCOL_VERSION, RequestError, agent, methods, ndJsonStream } from "${ACP_SDK_PATH}";
|
|
133
133
|
const home = process.env.GROK_HOME ?? "";
|
|
134
134
|
const read = (name) => (existsSync(join(home, name)) ? readFileSync(join(home, name), "utf8") : null);
|
|
135
135
|
const capture = {
|
|
@@ -160,7 +160,7 @@ class FakeGrok {
|
|
|
160
160
|
}
|
|
161
161
|
async cancel() { this.cancelled?.(); }
|
|
162
162
|
}
|
|
163
|
-
|
|
163
|
+
${serveFakeAcpAgent("FakeGrok")}
|
|
164
164
|
`,
|
|
165
165
|
);
|
|
166
166
|
const binPath = join(dir, "grok");
|
|
@@ -261,6 +261,29 @@ describe("heartbeat-jev-gate with a provider", () => {
|
|
|
261
261
|
});
|
|
262
262
|
});
|
|
263
263
|
|
|
264
|
+
test("only a bare MM-DD counts as a due tracked item", async () => {
|
|
265
|
+
const mocked = Date.now;
|
|
266
|
+
Date.now = () => Date.parse("2026-10-20T09:30:00.000Z");
|
|
267
|
+
try {
|
|
268
|
+
const { ctx } = fakeCtx({
|
|
269
|
+
configs: { TYPESAFE_API_KEY: "x" },
|
|
270
|
+
heartbeatMd: [
|
|
271
|
+
"- **2026-10-20 ~10:45Z (heartbeat 2026-10-20):** re-verified, nothing pending",
|
|
272
|
+
"- Watch: 10-20s retry window closed, nothing pending",
|
|
273
|
+
"- Watch: session 4a12-10-20-9e2f finished cleanly, nothing pending",
|
|
274
|
+
"- Watch: release on 10-20 — lift after ship",
|
|
275
|
+
].join("\n"),
|
|
276
|
+
jev: jevSays(0.9),
|
|
277
|
+
});
|
|
278
|
+
const verdict = await run(ctx, { dryRun: true });
|
|
279
|
+
expect(verdict.state.tracked_items_mentioning_today_or_past_delete_date).toEqual([
|
|
280
|
+
"- Watch: release on 10-20 — lift after ship",
|
|
281
|
+
]);
|
|
282
|
+
} finally {
|
|
283
|
+
Date.now = mocked;
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
264
287
|
for (const ordersFirst of [true, false]) {
|
|
265
288
|
test(`previous failures are not new with standing orders ${ordersFirst ? "before" : "after"} status`, async () => {
|
|
266
289
|
const failures =
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { expect, test } from "bun:test";
|
|
3
|
+
|
|
4
|
+
test("migration 232 preserves every token kind and adds agent-only tokens", async () => {
|
|
5
|
+
const db = new Database(":memory:");
|
|
6
|
+
try {
|
|
7
|
+
db.exec(await Bun.file("src/be/migrations/149_session_tokens.sql").text());
|
|
8
|
+
db.exec(await Bun.file("src/be/migrations/207_runtime_session_tokens.sql").text());
|
|
9
|
+
const seed = db.query(
|
|
10
|
+
`INSERT INTO session_tokens
|
|
11
|
+
(id, tokenHash, tokenPreview, agentId, taskId, runtimeId, parentRuntimeId, controllerId,
|
|
12
|
+
expiresAt, revokedAt, createdAt, lastUsedAt)
|
|
13
|
+
VALUES (?, ?, 'view', 'agent', ?, ?, ?, ?, '2030-01-01', NULL, '2025-01-01', '2025-12-01')`,
|
|
14
|
+
);
|
|
15
|
+
seed.run("acp", "acp", "task", null, null, null);
|
|
16
|
+
seed.run("runtime", "runtime", null, "runtime", null, null);
|
|
17
|
+
seed.run("child", "child", "task", null, "runtime", null);
|
|
18
|
+
seed.run("controller", "controller", null, null, null, "ctrl");
|
|
19
|
+
const before = db.query("SELECT * FROM session_tokens ORDER BY id").all();
|
|
20
|
+
|
|
21
|
+
db.exec(await Bun.file("src/be/migrations/232_agent_session_tokens.sql").text());
|
|
22
|
+
|
|
23
|
+
// prepare, not query: query() caches the statement and its pre-migration column list.
|
|
24
|
+
expect(db.prepare("SELECT * FROM session_tokens ORDER BY id").all()).toEqual(
|
|
25
|
+
before.map((row) => ({ ...(row as object), parentTokenId: null })),
|
|
26
|
+
);
|
|
27
|
+
const insert = db.query(
|
|
28
|
+
`INSERT INTO session_tokens
|
|
29
|
+
(id, tokenHash, tokenPreview, agentId, taskId, runtimeId, parentRuntimeId, controllerId,
|
|
30
|
+
parentTokenId, expiresAt)
|
|
31
|
+
VALUES (?, ?, 'view', 'agent', ?, ?, ?, ?, ?, '2030-01-01')`,
|
|
32
|
+
);
|
|
33
|
+
insert.run("worker", "worker", null, null, null, null, null);
|
|
34
|
+
insert.run("worker-child", "worker-child", "task", null, null, null, "worker");
|
|
35
|
+
expect(() => insert.run("mixed", "mixed", "task", "runtime", null, null, null)).toThrow();
|
|
36
|
+
expect(() =>
|
|
37
|
+
insert.run("two-parents", "two-parents", "task", null, "rt", null, "worker"),
|
|
38
|
+
).toThrow();
|
|
39
|
+
expect(() =>
|
|
40
|
+
insert.run("runtime-parent-token", "runtime-parent-token", null, "rt", null, null, "worker"),
|
|
41
|
+
).toThrow();
|
|
42
|
+
expect(() =>
|
|
43
|
+
insert.run("controller-parent", "controller-parent", null, null, null, "ctrl", "worker"),
|
|
44
|
+
).toThrow();
|
|
45
|
+
expect(() =>
|
|
46
|
+
insert.run("agent-parent-runtime", "agent-parent-runtime", null, null, "rt", null, null),
|
|
47
|
+
).toThrow();
|
|
48
|
+
expect(() =>
|
|
49
|
+
insert.run("agent-with-parent", "agent-with-parent", null, null, null, null, "worker"),
|
|
50
|
+
).toThrow();
|
|
51
|
+
const indexes = db
|
|
52
|
+
.query<{ name: string }, []>("PRAGMA index_list(session_tokens)")
|
|
53
|
+
.all()
|
|
54
|
+
.map((row) => row.name);
|
|
55
|
+
for (const name of [
|
|
56
|
+
"idx_session_tokens_agentId",
|
|
57
|
+
"idx_session_tokens_runtimeId",
|
|
58
|
+
"idx_session_tokens_parentRuntimeId",
|
|
59
|
+
"idx_session_tokens_parentTokenId",
|
|
60
|
+
]) {
|
|
61
|
+
expect(indexes).toContain(name);
|
|
62
|
+
}
|
|
63
|
+
expect(db.query("PRAGMA integrity_check").get()).toEqual({ integrity_check: "ok" });
|
|
64
|
+
} finally {
|
|
65
|
+
db.close();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
@@ -9,6 +9,7 @@ import { handleSessions } from "../http/sessions";
|
|
|
9
9
|
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
10
10
|
import { ACPAdapter } from "../providers/acp-adapter";
|
|
11
11
|
import { setRequestAuth } from "../utils/request-auth-context";
|
|
12
|
+
import { ACP_SDK_PATH, serveFakeAcpAgent } from "./acp-fake-agent";
|
|
12
13
|
import { listenOnFreePort } from "./test-net";
|
|
13
14
|
import { CHILD_PROCESS_TEST_BUDGET_MS } from "./test-proc";
|
|
14
15
|
|
|
@@ -69,12 +70,11 @@ for (const setupFails of [false, true]) {
|
|
|
69
70
|
const api = await startApi(resolveRevoke);
|
|
70
71
|
const script = join(tmpdir(), `runtime-child-${crypto.randomUUID()}.ts`);
|
|
71
72
|
const capture = `${script}.auth`;
|
|
72
|
-
const sdk = join(process.cwd(), "node_modules/@agentclientprotocol/sdk/dist/acp.js");
|
|
73
73
|
await Bun.write(
|
|
74
74
|
script,
|
|
75
75
|
`
|
|
76
76
|
import { Readable, Writable } from "node:stream";
|
|
77
|
-
import {
|
|
77
|
+
import { PROTOCOL_VERSION, agent, methods, ndJsonStream } from ${JSON.stringify(ACP_SDK_PATH)};
|
|
78
78
|
class Agent {
|
|
79
79
|
async initialize() {
|
|
80
80
|
return { protocolVersion: PROTOCOL_VERSION, agentCapabilities: { loadSession: false } };
|
|
@@ -89,7 +89,7 @@ class Agent {
|
|
|
89
89
|
async prompt() { return { stopReason: "end_turn" }; }
|
|
90
90
|
async cancel() {}
|
|
91
91
|
}
|
|
92
|
-
|
|
92
|
+
${serveFakeAcpAgent("Agent")}
|
|
93
93
|
`,
|
|
94
94
|
);
|
|
95
95
|
try {
|
|
@@ -636,6 +636,8 @@ describe("script connections", () => {
|
|
|
636
636
|
markMigrationApplied(database, "227_inbox_item_state_last_read_at.sql");
|
|
637
637
|
// 231 indexes agent_tasks columns that this fixture's id-only agent_tasks lacks.
|
|
638
638
|
markMigrationApplied(database, "231_slack_tasks_last_updated_index.sql");
|
|
639
|
+
// 232 rebuilds session_tokens with the 207 columns, which this fixture skipped.
|
|
640
|
+
markMigrationApplied(database, "232_agent_session_tokens.sql");
|
|
639
641
|
|
|
640
642
|
const id = crypto.randomUUID();
|
|
641
643
|
const now = new Date().toISOString();
|
|
@@ -25,6 +25,7 @@ import "../http/all-routes";
|
|
|
25
25
|
import { handleActiveSessions } from "../http/active-sessions";
|
|
26
26
|
import { handleApiKeys } from "../http/api-keys";
|
|
27
27
|
import { handleApprovalRequests } from "../http/approval-requests";
|
|
28
|
+
import { handleApps } from "../http/apps";
|
|
28
29
|
import { handleContext } from "../http/context";
|
|
29
30
|
import { handleCore } from "../http/core";
|
|
30
31
|
import { handleEvents } from "../http/events";
|
|
@@ -76,6 +77,7 @@ const HANDLERS: Handler[] = [
|
|
|
76
77
|
handleFs,
|
|
77
78
|
handleActiveSessions,
|
|
78
79
|
handleSessions,
|
|
80
|
+
handleApps,
|
|
79
81
|
(req, res, segments, query) => handleApprovalRequests(req, res, segments, query),
|
|
80
82
|
(req, res, segments, query) => handleApiKeys(req, res, segments, query),
|
|
81
83
|
(req, res, segments, query) => handleScriptRuns(req, res, segments, query),
|