@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
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RBAC HTTP admission tests (DES-445 increment 3).
|
|
3
|
+
*
|
|
4
|
+
* In-process handleCore wiring depends on routeRegistry population. Import the
|
|
5
|
+
* route-owning handler modules used by this suite at file load so findRoute()
|
|
6
|
+
* sees the same route() definitions that the real server imports at boot.
|
|
7
|
+
*/
|
|
8
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
9
|
+
import { unlink } from "node:fs/promises";
|
|
10
|
+
import {
|
|
11
|
+
createServer as createHttpServer,
|
|
12
|
+
type IncomingMessage,
|
|
13
|
+
type Server,
|
|
14
|
+
type ServerResponse,
|
|
15
|
+
} from "node:http";
|
|
16
|
+
import { closeDb, createAgent, createUser, getDb, initDb } from "../be/db";
|
|
17
|
+
import { flushAuditBuffer } from "../be/rbac-audit";
|
|
18
|
+
import { attachRole, detachRole, ensureRbacSeedsSynced } from "../be/rbac-roles";
|
|
19
|
+
import { type IdentityActor, mintToken } from "../be/users";
|
|
20
|
+
import { handleCore } from "../http/core";
|
|
21
|
+
import { handleFs } from "../http/fs";
|
|
22
|
+
import { handleMcpOAuth } from "../http/mcp-oauth";
|
|
23
|
+
import { handleTasks } from "../http/tasks";
|
|
24
|
+
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
25
|
+
import { decideAdmission, decideToolAdmission, type PermissionVerb } from "../rbac";
|
|
26
|
+
|
|
27
|
+
const TEST_DB_PATH = "./test-rbac-admission.sqlite";
|
|
28
|
+
const API_KEY = "test-api-key";
|
|
29
|
+
const LEAD_ID = "aaaa7000-0000-4000-8000-000000000001";
|
|
30
|
+
const ACTOR: IdentityActor = { kind: "operator", id: "op:test" };
|
|
31
|
+
const MISSING_TASK_ID = "00000000-0000-4000-8000-000000000000";
|
|
32
|
+
|
|
33
|
+
type ApiResponse = {
|
|
34
|
+
status: number;
|
|
35
|
+
body: unknown;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type AdmissionAuditRow = {
|
|
39
|
+
principalType: string;
|
|
40
|
+
principalId: string | null;
|
|
41
|
+
verb: string;
|
|
42
|
+
resourceType: string | null;
|
|
43
|
+
resourceId: string | null;
|
|
44
|
+
decision: string;
|
|
45
|
+
reason: string | null;
|
|
46
|
+
source: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function grant(verbs: PermissionVerb[] = [], grantsAll = false) {
|
|
50
|
+
return { grantsAll, verbs: new Set(verbs) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function createTestServer(): Server {
|
|
54
|
+
return createHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
55
|
+
const pathSegments = getPathSegments(req.url || "");
|
|
56
|
+
const queryParams = parseQueryParams(req.url || "");
|
|
57
|
+
const myAgentId = req.headers["x-agent-id"] as string | undefined;
|
|
58
|
+
|
|
59
|
+
if (await handleCore(req, res, myAgentId, API_KEY)) return;
|
|
60
|
+
if (await handleTasks(req, res, pathSegments, queryParams, myAgentId)) return;
|
|
61
|
+
if (await handleFs(req, res, pathSegments, queryParams, myAgentId)) return;
|
|
62
|
+
if (await handleMcpOAuth(req, res, pathSegments, queryParams)) return;
|
|
63
|
+
|
|
64
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
65
|
+
res.end(JSON.stringify({ error: "Not Found" }));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function listen(server: Server): Promise<number> {
|
|
70
|
+
await new Promise<void>((resolve) => server.listen(0, resolve));
|
|
71
|
+
const addr = server.address();
|
|
72
|
+
if (!addr || typeof addr === "string") throw new Error("no port");
|
|
73
|
+
return addr.port;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function closeServer(server: Server | undefined): Promise<void> {
|
|
77
|
+
if (!server || !server.listening) return;
|
|
78
|
+
await new Promise<void>((resolve, reject) => {
|
|
79
|
+
server.close((err) => (err ? reject(err) : resolve()));
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function removeDbFiles() {
|
|
84
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
85
|
+
try {
|
|
86
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
87
|
+
} catch {
|
|
88
|
+
// File does not exist.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function api(
|
|
94
|
+
port: number,
|
|
95
|
+
method: string,
|
|
96
|
+
path: string,
|
|
97
|
+
opts: { bearer?: string; body?: unknown; rawBody?: BodyInit } = {},
|
|
98
|
+
): Promise<ApiResponse> {
|
|
99
|
+
const headers: Record<string, string> = {
|
|
100
|
+
Authorization: `Bearer ${opts.bearer ?? API_KEY}`,
|
|
101
|
+
};
|
|
102
|
+
let body: BodyInit | undefined;
|
|
103
|
+
if (opts.body !== undefined) {
|
|
104
|
+
headers["Content-Type"] = "application/json";
|
|
105
|
+
body = JSON.stringify(opts.body);
|
|
106
|
+
} else if (opts.rawBody !== undefined) {
|
|
107
|
+
headers["Content-Type"] = "application/octet-stream";
|
|
108
|
+
body = opts.rawBody;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const res = await fetch(`http://localhost:${port}${path}`, { method, headers, body });
|
|
112
|
+
const text = await res.text();
|
|
113
|
+
let parsed: unknown = text;
|
|
114
|
+
if (text) {
|
|
115
|
+
try {
|
|
116
|
+
parsed = JSON.parse(text);
|
|
117
|
+
} catch {
|
|
118
|
+
// Leave non-JSON responses as text.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { status: res.status, body: parsed };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function createTokenForUser(name: string): { userId: string; plaintext: string } {
|
|
125
|
+
const user = createUser({ name });
|
|
126
|
+
const { plaintext } = mintToken(user.id, "admission", ACTOR);
|
|
127
|
+
return { userId: user.id, plaintext };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function narrowUserToRequester(userId: string): void {
|
|
131
|
+
detachRole(userId, "admin");
|
|
132
|
+
attachRole(userId, "requester");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function admissionAuditRows(): AdmissionAuditRow[] {
|
|
136
|
+
return getDb()
|
|
137
|
+
.prepare(
|
|
138
|
+
`SELECT principalType, principalId, verb, resourceType, resourceId, decision, reason, source
|
|
139
|
+
FROM permission_audit
|
|
140
|
+
WHERE resourceType = 'http-route'
|
|
141
|
+
ORDER BY ts, id`,
|
|
142
|
+
)
|
|
143
|
+
.all() as AdmissionAuditRow[];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let savedEnv: NodeJS.ProcessEnv;
|
|
147
|
+
let server: Server | undefined;
|
|
148
|
+
let port = 0;
|
|
149
|
+
|
|
150
|
+
beforeAll(() => {
|
|
151
|
+
savedEnv = { ...process.env };
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
beforeEach(async () => {
|
|
155
|
+
await closeServer(server);
|
|
156
|
+
server = undefined;
|
|
157
|
+
closeDb();
|
|
158
|
+
await removeDbFiles();
|
|
159
|
+
|
|
160
|
+
delete process.env.RBAC_ENABLED;
|
|
161
|
+
delete process.env.RBAC_AUDIT_DISABLED;
|
|
162
|
+
delete process.env.RBAC_AUDIT_RETENTION_DAYS;
|
|
163
|
+
|
|
164
|
+
initDb(TEST_DB_PATH);
|
|
165
|
+
ensureRbacSeedsSynced({ quiet: true });
|
|
166
|
+
createAgent({ id: LEAD_ID, name: "Admission Lead", isLead: true, status: "idle" });
|
|
167
|
+
flushAuditBuffer();
|
|
168
|
+
getDb().run("DELETE FROM permission_audit");
|
|
169
|
+
|
|
170
|
+
server = createTestServer();
|
|
171
|
+
port = await listen(server);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
afterEach(async () => {
|
|
175
|
+
flushAuditBuffer();
|
|
176
|
+
await closeServer(server);
|
|
177
|
+
server = undefined;
|
|
178
|
+
closeDb();
|
|
179
|
+
await removeDbFiles();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
afterAll(() => {
|
|
183
|
+
for (const key of Object.keys(process.env)) {
|
|
184
|
+
if (!(key in savedEnv)) delete process.env[key];
|
|
185
|
+
}
|
|
186
|
+
for (const [key, value] of Object.entries(savedEnv)) {
|
|
187
|
+
if (value === undefined) delete process.env[key];
|
|
188
|
+
else process.env[key] = value;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("decideAdmission", () => {
|
|
193
|
+
test("allows a declared permission when the grant contains the verb", () => {
|
|
194
|
+
expect(
|
|
195
|
+
decideAdmission({
|
|
196
|
+
method: "POST",
|
|
197
|
+
rbac: { permission: "task.fs.mutate" },
|
|
198
|
+
routeKnown: true,
|
|
199
|
+
grant: grant(["task.fs.mutate"]),
|
|
200
|
+
}),
|
|
201
|
+
).toEqual({ allow: true, verb: "task.fs.mutate" });
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("denies a declared permission when the grant lacks the verb", () => {
|
|
205
|
+
expect(
|
|
206
|
+
decideAdmission({
|
|
207
|
+
method: "POST",
|
|
208
|
+
rbac: { permission: "task.fs.mutate" },
|
|
209
|
+
routeKnown: true,
|
|
210
|
+
grant: grant(["task.read.own"]),
|
|
211
|
+
}),
|
|
212
|
+
).toEqual({
|
|
213
|
+
allow: false,
|
|
214
|
+
reason: "admission: missing permission 'task.fs.mutate'",
|
|
215
|
+
verb: "task.fs.mutate",
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("declared GET permissions win over the GET fallback", () => {
|
|
220
|
+
expect(
|
|
221
|
+
decideAdmission({
|
|
222
|
+
method: "GET",
|
|
223
|
+
rbac: { permission: "mcp-oauth.authorize.any" },
|
|
224
|
+
routeKnown: true,
|
|
225
|
+
grant: grant(),
|
|
226
|
+
}),
|
|
227
|
+
).toEqual({
|
|
228
|
+
allow: false,
|
|
229
|
+
reason: "admission: missing permission 'mcp-oauth.authorize.any'",
|
|
230
|
+
verb: "mcp-oauth.authorize.any",
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
expect(
|
|
234
|
+
decideAdmission({
|
|
235
|
+
method: "GET",
|
|
236
|
+
rbac: { permission: "mcp-oauth.authorize.any" },
|
|
237
|
+
routeKnown: true,
|
|
238
|
+
grant: grant(["mcp-oauth.authorize.any"]),
|
|
239
|
+
}),
|
|
240
|
+
).toEqual({ allow: true, verb: "mcp-oauth.authorize.any" });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("allows GET and HEAD fallback when no permission verb is declared", () => {
|
|
244
|
+
expect(
|
|
245
|
+
decideAdmission({
|
|
246
|
+
method: "GET",
|
|
247
|
+
rbac: undefined,
|
|
248
|
+
routeKnown: true,
|
|
249
|
+
grant: grant(),
|
|
250
|
+
}),
|
|
251
|
+
).toEqual({ allow: true });
|
|
252
|
+
expect(
|
|
253
|
+
decideAdmission({
|
|
254
|
+
method: "HEAD",
|
|
255
|
+
rbac: { ungated: "self scoped" },
|
|
256
|
+
routeKnown: true,
|
|
257
|
+
grant: grant(),
|
|
258
|
+
}),
|
|
259
|
+
).toEqual({ allow: true });
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("denies verb-less non-GET routes, including ungated and unknown routes", () => {
|
|
263
|
+
const reason = "admission: route has no permission verb (operator-only)";
|
|
264
|
+
expect(
|
|
265
|
+
decideAdmission({
|
|
266
|
+
method: "POST",
|
|
267
|
+
rbac: { ungated: "self scoped" },
|
|
268
|
+
routeKnown: true,
|
|
269
|
+
grant: grant(),
|
|
270
|
+
}),
|
|
271
|
+
).toEqual({ allow: false, reason });
|
|
272
|
+
expect(
|
|
273
|
+
decideAdmission({
|
|
274
|
+
method: "PATCH",
|
|
275
|
+
rbac: undefined,
|
|
276
|
+
routeKnown: true,
|
|
277
|
+
grant: grant(),
|
|
278
|
+
}),
|
|
279
|
+
).toEqual({ allow: false, reason });
|
|
280
|
+
expect(
|
|
281
|
+
decideAdmission({
|
|
282
|
+
method: "DELETE",
|
|
283
|
+
rbac: undefined,
|
|
284
|
+
routeKnown: false,
|
|
285
|
+
grant: grant(),
|
|
286
|
+
}),
|
|
287
|
+
).toEqual({ allow: false, reason });
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("grantsAll allows before permission or method checks", () => {
|
|
291
|
+
expect(
|
|
292
|
+
decideAdmission({
|
|
293
|
+
method: "POST",
|
|
294
|
+
rbac: undefined,
|
|
295
|
+
routeKnown: false,
|
|
296
|
+
grant: grant([], true),
|
|
297
|
+
}),
|
|
298
|
+
).toEqual({ allow: true });
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe("decideToolAdmission", () => {
|
|
303
|
+
test("grantsAll allows before permission or read-only checks", () => {
|
|
304
|
+
expect(
|
|
305
|
+
decideToolAdmission({
|
|
306
|
+
rbac: undefined,
|
|
307
|
+
readOnly: false,
|
|
308
|
+
grant: grant([], true),
|
|
309
|
+
}),
|
|
310
|
+
).toEqual({ allow: true });
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("allows a declared permission when the grant contains the verb", () => {
|
|
314
|
+
expect(
|
|
315
|
+
decideToolAdmission({
|
|
316
|
+
rbac: { permission: "task.create.own" },
|
|
317
|
+
readOnly: false,
|
|
318
|
+
grant: grant(["task.create.own"]),
|
|
319
|
+
}),
|
|
320
|
+
).toEqual({ allow: true, verb: "task.create.own" });
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("denies a declared permission when the grant lacks the verb", () => {
|
|
324
|
+
expect(
|
|
325
|
+
decideToolAdmission({
|
|
326
|
+
rbac: { permission: "task.create.own" },
|
|
327
|
+
readOnly: false,
|
|
328
|
+
grant: grant(["task.read.own"]),
|
|
329
|
+
}),
|
|
330
|
+
).toEqual({
|
|
331
|
+
allow: false,
|
|
332
|
+
reason: "admission: missing permission 'task.create.own'",
|
|
333
|
+
verb: "task.create.own",
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("allows read-only tools without a permission verb", () => {
|
|
338
|
+
expect(
|
|
339
|
+
decideToolAdmission({
|
|
340
|
+
rbac: undefined,
|
|
341
|
+
readOnly: true,
|
|
342
|
+
grant: grant(),
|
|
343
|
+
}),
|
|
344
|
+
).toEqual({ allow: true });
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test("denies non-read-only tools without a permission verb", () => {
|
|
348
|
+
expect(
|
|
349
|
+
decideToolAdmission({
|
|
350
|
+
rbac: { ungated: "self scoped" },
|
|
351
|
+
readOnly: false,
|
|
352
|
+
grant: grant(),
|
|
353
|
+
}),
|
|
354
|
+
).toEqual({
|
|
355
|
+
allow: false,
|
|
356
|
+
reason: "admission: tool has no permission verb (operator-only)",
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
describe("handleCore admission wiring", () => {
|
|
362
|
+
test("flag off leaves narrowed user-token REST writes untouched and unaudited", async () => {
|
|
363
|
+
const { userId, plaintext } = createTokenForUser("Flag Off User");
|
|
364
|
+
detachRole(userId, "admin");
|
|
365
|
+
|
|
366
|
+
const res = await api(port, "POST", "/api/tasks", {
|
|
367
|
+
bearer: plaintext,
|
|
368
|
+
body: { task: "flag off admission bypass" },
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
expect(res.status).toBe(201);
|
|
372
|
+
flushAuditBuffer();
|
|
373
|
+
expect(admissionAuditRows()).toEqual([]);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("OAuth authorize-url GET uses its declared verb instead of the GET fallback", async () => {
|
|
377
|
+
const { userId: requesterId, plaintext: requesterToken } =
|
|
378
|
+
createTokenForUser("OAuth Requester User");
|
|
379
|
+
narrowUserToRequester(requesterId);
|
|
380
|
+
|
|
381
|
+
process.env.RBAC_ENABLED = "true";
|
|
382
|
+
|
|
383
|
+
const denied = await api(port, "GET", "/api/mcp-oauth/missing-server/authorize-url", {
|
|
384
|
+
bearer: requesterToken,
|
|
385
|
+
});
|
|
386
|
+
expect(denied.status).toBe(403);
|
|
387
|
+
expect(denied.body).toEqual({
|
|
388
|
+
error: "Forbidden: admission: missing permission 'mcp-oauth.authorize.any'",
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
const { plaintext: adminToken } = createTokenForUser("OAuth Admin User");
|
|
392
|
+
const admin = await api(port, "GET", "/api/mcp-oauth/missing-server/authorize-url", {
|
|
393
|
+
bearer: adminToken,
|
|
394
|
+
});
|
|
395
|
+
expect(admin.status).not.toBe(403);
|
|
396
|
+
expect(admin.status).toBe(404);
|
|
397
|
+
|
|
398
|
+
delete process.env.RBAC_ENABLED;
|
|
399
|
+
|
|
400
|
+
const flagOff = await api(port, "GET", "/api/mcp-oauth/missing-server/authorize-url", {
|
|
401
|
+
bearer: requesterToken,
|
|
402
|
+
});
|
|
403
|
+
expect(flagOff.status).not.toBe(403);
|
|
404
|
+
expect(flagOff.status).toBe(404);
|
|
405
|
+
|
|
406
|
+
flushAuditBuffer();
|
|
407
|
+
expect(
|
|
408
|
+
admissionAuditRows().find(
|
|
409
|
+
(row) =>
|
|
410
|
+
row.principalId === requesterId &&
|
|
411
|
+
row.resourceId === "GET /api/mcp-oauth/{mcpServerId}/authorize-url",
|
|
412
|
+
),
|
|
413
|
+
).toMatchObject({
|
|
414
|
+
verb: "mcp-oauth.authorize.any",
|
|
415
|
+
decision: "deny",
|
|
416
|
+
reason: "admission: missing permission 'mcp-oauth.authorize.any'",
|
|
417
|
+
source: "http",
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test("flag on default-admin users bypass admission and preserve no-op behavior", async () => {
|
|
422
|
+
process.env.RBAC_ENABLED = "true";
|
|
423
|
+
const { plaintext } = createTokenForUser("Default Admin User");
|
|
424
|
+
|
|
425
|
+
const res = await api(port, "POST", "/api/tasks", {
|
|
426
|
+
bearer: plaintext,
|
|
427
|
+
body: { task: "default admin admission bypass" },
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
expect(res.status).toBe(201);
|
|
431
|
+
flushAuditBuffer();
|
|
432
|
+
expect(admissionAuditRows()).toEqual([]);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test("flag on requester grant denies verb-less writes, allows reads and declared verbs, and audits each decision", async () => {
|
|
436
|
+
process.env.RBAC_ENABLED = "true";
|
|
437
|
+
const { userId, plaintext } = createTokenForUser("Requester User");
|
|
438
|
+
narrowUserToRequester(userId);
|
|
439
|
+
|
|
440
|
+
const denied = await api(port, "POST", "/api/tasks", {
|
|
441
|
+
bearer: plaintext,
|
|
442
|
+
body: { task: "requester should not create generic tasks" },
|
|
443
|
+
});
|
|
444
|
+
expect(denied.status).toBe(403);
|
|
445
|
+
expect(denied.body).toEqual({
|
|
446
|
+
error: "Forbidden: admission: route has no permission verb (operator-only)",
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
const read = await api(port, "GET", "/api/tasks", { bearer: plaintext });
|
|
450
|
+
expect(read.status).toBe(200);
|
|
451
|
+
|
|
452
|
+
const declaredVerb = await api(
|
|
453
|
+
port,
|
|
454
|
+
"POST",
|
|
455
|
+
`/api/fs/tasks/${MISSING_TASK_ID}/files?name=admission.txt`,
|
|
456
|
+
{ bearer: plaintext, rawBody: "hello" },
|
|
457
|
+
);
|
|
458
|
+
expect(declaredVerb.status).toBe(404);
|
|
459
|
+
expect(declaredVerb.body).toEqual({ error: "Task not found" });
|
|
460
|
+
|
|
461
|
+
flushAuditBuffer();
|
|
462
|
+
const rows = admissionAuditRows();
|
|
463
|
+
expect(rows).toHaveLength(3);
|
|
464
|
+
|
|
465
|
+
expect(rows.find((row) => row.resourceId === "POST /api/tasks")).toMatchObject({
|
|
466
|
+
principalType: "user",
|
|
467
|
+
principalId: userId,
|
|
468
|
+
verb: "(admission:no-verb)",
|
|
469
|
+
decision: "deny",
|
|
470
|
+
reason: "admission: route has no permission verb (operator-only)",
|
|
471
|
+
source: "http",
|
|
472
|
+
});
|
|
473
|
+
expect(rows.find((row) => row.resourceId === "GET /api/tasks")).toMatchObject({
|
|
474
|
+
principalType: "user",
|
|
475
|
+
principalId: userId,
|
|
476
|
+
verb: "(admission:no-verb)",
|
|
477
|
+
decision: "allow",
|
|
478
|
+
reason: null,
|
|
479
|
+
source: "http",
|
|
480
|
+
});
|
|
481
|
+
expect(
|
|
482
|
+
rows.find((row) => row.resourceId === "POST /api/fs/tasks/{taskId}/files"),
|
|
483
|
+
).toMatchObject({
|
|
484
|
+
principalType: "user",
|
|
485
|
+
principalId: userId,
|
|
486
|
+
verb: "task.fs.mutate",
|
|
487
|
+
decision: "allow",
|
|
488
|
+
reason: null,
|
|
489
|
+
source: "http",
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
});
|
|
@@ -92,6 +92,9 @@ export async function spawnSwarmServer(opts: {
|
|
|
92
92
|
OAUTH_KEEPALIVE_DISABLE: "true",
|
|
93
93
|
RBAC_AUDIT_DISABLED: "",
|
|
94
94
|
RBAC_AUDIT_FLUSH_MS: "",
|
|
95
|
+
// Keep subprocess suites deterministic: shell-level RBAC_ENABLED=true is
|
|
96
|
+
// ignored unless a suite explicitly opts in through opts.env.
|
|
97
|
+
RBAC_ENABLED: "false",
|
|
95
98
|
...opts.env,
|
|
96
99
|
},
|
|
97
100
|
});
|
|
@@ -67,8 +67,14 @@ const LEAD_ONLY_VERBS: PermissionVerb[] = [
|
|
|
67
67
|
"mcp-server.create.swarm",
|
|
68
68
|
"mcp-server.install.any",
|
|
69
69
|
"mcp-server.uninstall.any",
|
|
70
|
+
"mcp-server.read.secrets",
|
|
70
71
|
"script.global.write",
|
|
71
72
|
"script.global.delete",
|
|
73
|
+
"script.api.read.secrets",
|
|
74
|
+
"script.api.create",
|
|
75
|
+
"script.api.update",
|
|
76
|
+
"script.api.rotate",
|
|
77
|
+
"script.api.delete",
|
|
72
78
|
];
|
|
73
79
|
|
|
74
80
|
const LEAD_OR_RESOURCE_OWNER_VERBS: PermissionVerb[] = [
|
|
@@ -76,12 +82,21 @@ const LEAD_OR_RESOURCE_OWNER_VERBS: PermissionVerb[] = [
|
|
|
76
82
|
"skill.delete.any",
|
|
77
83
|
"mcp-server.delete.any",
|
|
78
84
|
"mcp-server.update.any",
|
|
85
|
+
"page.delete.any",
|
|
79
86
|
];
|
|
80
87
|
|
|
81
88
|
const LEAD_OR_TASK_CREATOR_VERBS: PermissionVerb[] = ["task.cancel.any"];
|
|
82
89
|
|
|
83
90
|
const LEAD_OR_OWN_NAMESPACE_VERBS: PermissionVerb[] = ["kv.write.any"];
|
|
84
91
|
|
|
92
|
+
const ANY_AUTHENTICATED_VERBS: PermissionVerb[] = [
|
|
93
|
+
"script-connection.invoke",
|
|
94
|
+
"mcp-oauth.authorize.any",
|
|
95
|
+
"task.create.own",
|
|
96
|
+
"favorite.write.own",
|
|
97
|
+
"script.search",
|
|
98
|
+
];
|
|
99
|
+
|
|
85
100
|
const REQUESTER_OWNS_TASK_VERBS: PermissionVerb[] = [
|
|
86
101
|
"task.read.own",
|
|
87
102
|
"task.cancel.own",
|
|
@@ -145,6 +160,7 @@ describe("verb-group partition", () => {
|
|
|
145
160
|
...LEAD_OR_RESOURCE_OWNER_VERBS,
|
|
146
161
|
...LEAD_OR_TASK_CREATOR_VERBS,
|
|
147
162
|
...LEAD_OR_OWN_NAMESPACE_VERBS,
|
|
163
|
+
...ANY_AUTHENTICATED_VERBS,
|
|
148
164
|
...REQUESTER_OWNS_TASK_VERBS,
|
|
149
165
|
...COMPOSITE_VERBS,
|
|
150
166
|
];
|
|
@@ -276,6 +292,23 @@ describe("lead-or-own-namespace verbs (kv.write.any)", () => {
|
|
|
276
292
|
});
|
|
277
293
|
});
|
|
278
294
|
|
|
295
|
+
describe("any-authenticated verbs", () => {
|
|
296
|
+
const expected: Expected = {
|
|
297
|
+
lead: true,
|
|
298
|
+
worker: true,
|
|
299
|
+
ownerWorker: true,
|
|
300
|
+
creatorWorker: true,
|
|
301
|
+
userRequester: true,
|
|
302
|
+
foreignUser: true,
|
|
303
|
+
operator: true,
|
|
304
|
+
};
|
|
305
|
+
for (const verb of ANY_AUTHENTICATED_VERBS) {
|
|
306
|
+
test(`${verb}: every authenticated principal is allowed`, () => {
|
|
307
|
+
expectDecisions(verb, { kind: "none" }, expected);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
279
312
|
describe("requester-owns-task verbs", () => {
|
|
280
313
|
// Mirrors assertOwnsTask: owner contexts (agents, operator) always pass;
|
|
281
314
|
// only a user principal that is not the requester is denied.
|
|
@@ -306,6 +339,23 @@ describe("requester-owns-task verbs", () => {
|
|
|
306
339
|
});
|
|
307
340
|
});
|
|
308
341
|
|
|
342
|
+
describe("any-authenticated verbs", () => {
|
|
343
|
+
const expected: Expected = {
|
|
344
|
+
lead: true,
|
|
345
|
+
worker: true,
|
|
346
|
+
ownerWorker: true,
|
|
347
|
+
creatorWorker: true,
|
|
348
|
+
userRequester: true,
|
|
349
|
+
foreignUser: true,
|
|
350
|
+
operator: true,
|
|
351
|
+
};
|
|
352
|
+
for (const verb of ANY_AUTHENTICATED_VERBS) {
|
|
353
|
+
test(`${verb}: any authenticated principal allowed`, () => {
|
|
354
|
+
expectDecisions(verb, { kind: "none" }, expected);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
309
359
|
describe("memory.delete.any composite (owner OR (lead AND scope=swarm))", () => {
|
|
310
360
|
test("swarm-scoped memory: owner or lead allowed", () => {
|
|
311
361
|
expectDecisions("memory.delete.any", SWARM_MEMORY_RESOURCE, {
|