@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,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RBAC increment-3 admission wire e2e (DES-445).
|
|
3
|
+
*
|
|
4
|
+
* Spawns the real HTTP server with RBAC_ENABLED=true, narrows a user to the
|
|
5
|
+
* requester role in the scratch DB, and proves central HTTP admission gates
|
|
6
|
+
* user-token REST calls before the handler runs. A second boot with the flag
|
|
7
|
+
* off proves the narrowed user still has legacy behavior when admission is
|
|
8
|
+
* disabled.
|
|
9
|
+
*/
|
|
10
|
+
import { Database } from "bun:sqlite";
|
|
11
|
+
import { afterAll, describe, expect, setDefaultTimeout, test } from "bun:test";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import {
|
|
14
|
+
api,
|
|
15
|
+
makeScratchDir,
|
|
16
|
+
readAuditRows,
|
|
17
|
+
registerAgent,
|
|
18
|
+
removeScratchDir,
|
|
19
|
+
type SwarmServer,
|
|
20
|
+
spawnSwarmServer,
|
|
21
|
+
WORKER_A,
|
|
22
|
+
waitForAuditCount,
|
|
23
|
+
} from "./rbac-e2e-helpers";
|
|
24
|
+
|
|
25
|
+
setDefaultTimeout(120_000);
|
|
26
|
+
|
|
27
|
+
const REQUESTER_ROLE_ID = "rbac-role-requester";
|
|
28
|
+
const CONFIG_SECRET_READER_ROLE_ID = "rbac-test-config-secret-reader";
|
|
29
|
+
const CONFIG_SECRET_KEY = "RBAC_ADMISSION_SECRET";
|
|
30
|
+
const CONFIG_SECRET_VALUE = "rbac-admission-secret-value";
|
|
31
|
+
|
|
32
|
+
let dir: string;
|
|
33
|
+
let server: SwarmServer | undefined;
|
|
34
|
+
|
|
35
|
+
function configValue(
|
|
36
|
+
body: { configs?: Array<{ key: string; value?: unknown }> },
|
|
37
|
+
key: string,
|
|
38
|
+
): string | undefined {
|
|
39
|
+
const row = body.configs?.find((config) => config.key === key);
|
|
40
|
+
return typeof row?.value === "string" ? row.value : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function rewriteUserToRequester(dbPath: string, userId: string): void {
|
|
44
|
+
const db = new Database(dbPath);
|
|
45
|
+
try {
|
|
46
|
+
db.run("PRAGMA busy_timeout = 5000");
|
|
47
|
+
db.transaction(() => {
|
|
48
|
+
db.prepare(
|
|
49
|
+
"DELETE FROM principal_roles WHERE principalType = 'user' AND principalId = ?",
|
|
50
|
+
).run(userId);
|
|
51
|
+
db.prepare(
|
|
52
|
+
`INSERT INTO principal_roles (principalType, principalId, roleId)
|
|
53
|
+
VALUES ('user', ?, ?)`,
|
|
54
|
+
).run(userId, REQUESTER_ROLE_ID);
|
|
55
|
+
})();
|
|
56
|
+
} finally {
|
|
57
|
+
db.close();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function grantUserConfigSecretRead(dbPath: string, userId: string): void {
|
|
62
|
+
const db = new Database(dbPath);
|
|
63
|
+
try {
|
|
64
|
+
db.run("PRAGMA busy_timeout = 5000");
|
|
65
|
+
db.transaction(() => {
|
|
66
|
+
db.prepare(
|
|
67
|
+
`INSERT OR IGNORE INTO roles (id, name, description, isBuiltin, grantsAll)
|
|
68
|
+
VALUES (?, ?, ?, 0, 0)`,
|
|
69
|
+
).run(CONFIG_SECRET_READER_ROLE_ID, "config-secret-reader", "Test config secret reader");
|
|
70
|
+
db.prepare("INSERT OR IGNORE INTO role_permissions (roleId, verb) VALUES (?, ?)").run(
|
|
71
|
+
CONFIG_SECRET_READER_ROLE_ID,
|
|
72
|
+
"config.read.secrets",
|
|
73
|
+
);
|
|
74
|
+
db.prepare(
|
|
75
|
+
`INSERT OR IGNORE INTO principal_roles (principalType, principalId, roleId)
|
|
76
|
+
VALUES ('user', ?, ?)`,
|
|
77
|
+
).run(userId, CONFIG_SECRET_READER_ROLE_ID);
|
|
78
|
+
})();
|
|
79
|
+
} finally {
|
|
80
|
+
db.close();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe("RBAC admission over real HTTP", () => {
|
|
85
|
+
afterAll(async () => {
|
|
86
|
+
if (server) {
|
|
87
|
+
await server.stop();
|
|
88
|
+
server = undefined;
|
|
89
|
+
}
|
|
90
|
+
if (dir) await removeScratchDir(dir);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("flag-on admission narrows user-token writes and flag-off preserves legacy behavior", async () => {
|
|
94
|
+
dir = await makeScratchDir();
|
|
95
|
+
const dbPath = join(dir, "admission.sqlite");
|
|
96
|
+
|
|
97
|
+
server = await spawnSwarmServer({
|
|
98
|
+
dbPath,
|
|
99
|
+
logPath: join(dir, "server-on.log"),
|
|
100
|
+
env: { RBAC_ENABLED: "true" },
|
|
101
|
+
});
|
|
102
|
+
await registerAgent(server.base, WORKER_A, "rbac-admission-worker", false);
|
|
103
|
+
|
|
104
|
+
const user = await api(server.base, "POST", "/api/users", {
|
|
105
|
+
body: { name: "admission-user", email: "rbac-admission-e2e@example.com" },
|
|
106
|
+
});
|
|
107
|
+
expect(user.status).toBe(200);
|
|
108
|
+
const userId = user.body.user.id as string;
|
|
109
|
+
|
|
110
|
+
const minted = await api(server.base, "POST", `/api/users/${userId}/mcp-tokens`, {
|
|
111
|
+
body: {},
|
|
112
|
+
});
|
|
113
|
+
expect(minted.status).toBe(200);
|
|
114
|
+
const userToken = minted.body.plaintext as string;
|
|
115
|
+
expect(userToken).toStartWith("aswt_");
|
|
116
|
+
|
|
117
|
+
const upsertSecret = await api(server.base, "PUT", "/api/config?includeSecrets=true", {
|
|
118
|
+
body: {
|
|
119
|
+
scope: "global",
|
|
120
|
+
key: CONFIG_SECRET_KEY,
|
|
121
|
+
value: CONFIG_SECRET_VALUE,
|
|
122
|
+
isSecret: true,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
expect(upsertSecret.status).toBe(200);
|
|
126
|
+
expect(upsertSecret.body.value).toBe(CONFIG_SECRET_VALUE);
|
|
127
|
+
|
|
128
|
+
const scriptName = `rbac-admission-script-${crypto.randomUUID()}`;
|
|
129
|
+
const upsertScript = await api(server.base, "POST", "/api/scripts/upsert", {
|
|
130
|
+
agentId: WORKER_A,
|
|
131
|
+
body: {
|
|
132
|
+
name: scriptName,
|
|
133
|
+
source: "export default async function run() { return { ok: true }; }",
|
|
134
|
+
scope: "agent",
|
|
135
|
+
description: "RBAC admission script API fixture",
|
|
136
|
+
intent: "rbac admission fixture",
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
expect(upsertScript.status).toBe(200);
|
|
140
|
+
const scripts = await api(server.base, "GET", "/api/scripts?scope=agent");
|
|
141
|
+
expect(scripts.status).toBe(200);
|
|
142
|
+
const script = scripts.body.scripts.find((row: { name: string }) => row.name === scriptName);
|
|
143
|
+
expect(script).toBeTruthy();
|
|
144
|
+
const scriptId = script.id as string;
|
|
145
|
+
|
|
146
|
+
const scriptApi = await api(server.base, "POST", `/api/scripts/${scriptId}/apis`, {
|
|
147
|
+
body: { authMode: "bearer", label: "rbac admission endpoint" },
|
|
148
|
+
});
|
|
149
|
+
expect(scriptApi.status).toBe(201);
|
|
150
|
+
const endpointId = scriptApi.body.id as string;
|
|
151
|
+
const scriptApiToken = scriptApi.body.token as string;
|
|
152
|
+
expect(scriptApiToken).toStartWith("xsk_");
|
|
153
|
+
|
|
154
|
+
const mcpServer = await api(server.base, "POST", "/api/mcp-servers", {
|
|
155
|
+
body: {
|
|
156
|
+
name: `rbac-admission-mcp-${crypto.randomUUID()}`,
|
|
157
|
+
transport: "http",
|
|
158
|
+
scope: "agent",
|
|
159
|
+
ownerAgentId: WORKER_A,
|
|
160
|
+
url: "https://example.invalid/mcp",
|
|
161
|
+
headerConfigKeys: JSON.stringify({ Authorization: CONFIG_SECRET_KEY }),
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
expect(mcpServer.status).toBe(201);
|
|
165
|
+
const mcpServerId = mcpServer.body.server.id as string;
|
|
166
|
+
const installMcp = await api(server.base, "POST", `/api/mcp-servers/${mcpServerId}/install`, {
|
|
167
|
+
body: { agentId: WORKER_A },
|
|
168
|
+
});
|
|
169
|
+
expect(installMcp.status).toBe(200);
|
|
170
|
+
|
|
171
|
+
const grantsAllSecretRead = await api(
|
|
172
|
+
server.base,
|
|
173
|
+
"GET",
|
|
174
|
+
"/api/config?scope=global&includeSecrets=true",
|
|
175
|
+
{ bearer: userToken },
|
|
176
|
+
);
|
|
177
|
+
expect(grantsAllSecretRead.status).toBe(200);
|
|
178
|
+
expect(configValue(grantsAllSecretRead.body, CONFIG_SECRET_KEY)).toBe(CONFIG_SECRET_VALUE);
|
|
179
|
+
expect(grantsAllSecretRead.body.message).toBeUndefined();
|
|
180
|
+
|
|
181
|
+
const grantsAllScriptApiSecret = await api(
|
|
182
|
+
server.base,
|
|
183
|
+
"GET",
|
|
184
|
+
`/api/scripts/${scriptId}/apis/${endpointId}/secret`,
|
|
185
|
+
{ bearer: userToken },
|
|
186
|
+
);
|
|
187
|
+
expect(grantsAllScriptApiSecret.status).toBe(200);
|
|
188
|
+
expect(grantsAllScriptApiSecret.body.token).toBe(scriptApiToken);
|
|
189
|
+
|
|
190
|
+
const grantsAllMcpSecrets = await api(
|
|
191
|
+
server.base,
|
|
192
|
+
"GET",
|
|
193
|
+
`/api/agents/${WORKER_A}/mcp-servers?resolveSecrets=true`,
|
|
194
|
+
{ bearer: userToken },
|
|
195
|
+
);
|
|
196
|
+
expect(grantsAllMcpSecrets.status).toBe(200);
|
|
197
|
+
expect(grantsAllMcpSecrets.body.servers[0]?.resolvedHeaders?.Authorization).toBe(
|
|
198
|
+
CONFIG_SECRET_VALUE,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const defaultCreate = await api(server.base, "POST", "/api/tasks", {
|
|
202
|
+
bearer: userToken,
|
|
203
|
+
body: { task: "rbac admission default role no-op" },
|
|
204
|
+
});
|
|
205
|
+
expect(defaultCreate.status).toBe(201);
|
|
206
|
+
const taskId = defaultCreate.body.id as string;
|
|
207
|
+
|
|
208
|
+
rewriteUserToRequester(dbPath, userId);
|
|
209
|
+
|
|
210
|
+
const deniedTaskCreate = await api(server.base, "POST", "/api/tasks", {
|
|
211
|
+
bearer: userToken,
|
|
212
|
+
body: { task: "rbac admission denied task" },
|
|
213
|
+
});
|
|
214
|
+
expect(deniedTaskCreate.status).toBe(403);
|
|
215
|
+
expect(deniedTaskCreate.body.error).toContain("admission: route has no permission verb");
|
|
216
|
+
|
|
217
|
+
const allowedFavorite = await api(server.base, "PUT", "/api/favorites", {
|
|
218
|
+
bearer: userToken,
|
|
219
|
+
body: { itemType: "workflow", itemId: "wf-rbac-admission", favorite: true },
|
|
220
|
+
});
|
|
221
|
+
expect(allowedFavorite.status).toBe(200);
|
|
222
|
+
expect(allowedFavorite.body.favorite).toBe(true);
|
|
223
|
+
|
|
224
|
+
const listTasks = await api(server.base, "GET", `/api/tasks?requestedByUserId=${userId}`, {
|
|
225
|
+
bearer: userToken,
|
|
226
|
+
});
|
|
227
|
+
expect(listTasks.status).toBe(200);
|
|
228
|
+
|
|
229
|
+
const maskedSecretRead = await api(
|
|
230
|
+
server.base,
|
|
231
|
+
"GET",
|
|
232
|
+
"/api/config?scope=global&includeSecrets=true",
|
|
233
|
+
{ bearer: userToken },
|
|
234
|
+
);
|
|
235
|
+
expect(maskedSecretRead.status).toBe(200);
|
|
236
|
+
expect(configValue(maskedSecretRead.body, CONFIG_SECRET_KEY)).toBe("********");
|
|
237
|
+
expect(maskedSecretRead.body.message).toContain("secret values masked");
|
|
238
|
+
expect(maskedSecretRead.body.message).toContain(
|
|
239
|
+
"reading unmasked secrets requires the lead agent",
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const secretlessMcpList = await api(server.base, "GET", `/api/agents/${WORKER_A}/mcp-servers`, {
|
|
243
|
+
bearer: userToken,
|
|
244
|
+
});
|
|
245
|
+
expect(secretlessMcpList.status).toBe(200);
|
|
246
|
+
expect(secretlessMcpList.body.servers[0]?.resolvedHeaders).toBeUndefined();
|
|
247
|
+
|
|
248
|
+
const deniedMcpSecretRead = await api(
|
|
249
|
+
server.base,
|
|
250
|
+
"GET",
|
|
251
|
+
`/api/agents/${WORKER_A}/mcp-servers?resolveSecrets=true`,
|
|
252
|
+
{ bearer: userToken },
|
|
253
|
+
);
|
|
254
|
+
expect(deniedMcpSecretRead.status).toBe(403);
|
|
255
|
+
expect(deniedMcpSecretRead.body.error).toContain(
|
|
256
|
+
"missing permission 'mcp-server.read.secrets'",
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const deniedScriptApiSecretRead = await api(
|
|
260
|
+
server.base,
|
|
261
|
+
"GET",
|
|
262
|
+
`/api/scripts/${scriptId}/apis/${endpointId}/secret`,
|
|
263
|
+
{ bearer: userToken },
|
|
264
|
+
);
|
|
265
|
+
expect(deniedScriptApiSecretRead.status).toBe(403);
|
|
266
|
+
expect(deniedScriptApiSecretRead.body.error).toContain(
|
|
267
|
+
"missing permission 'script.api.read.secrets'",
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const deniedScriptApiCreate = await api(server.base, "POST", `/api/scripts/${scriptId}/apis`, {
|
|
271
|
+
bearer: userToken,
|
|
272
|
+
body: { authMode: "none", label: "denied" },
|
|
273
|
+
});
|
|
274
|
+
expect(deniedScriptApiCreate.status).toBe(403);
|
|
275
|
+
expect(deniedScriptApiCreate.body.error).toContain("missing permission 'script.api.create'");
|
|
276
|
+
|
|
277
|
+
grantUserConfigSecretRead(dbPath, userId);
|
|
278
|
+
|
|
279
|
+
const grantedSecretRead = await api(
|
|
280
|
+
server.base,
|
|
281
|
+
"GET",
|
|
282
|
+
"/api/config?scope=global&includeSecrets=true",
|
|
283
|
+
{ bearer: userToken },
|
|
284
|
+
);
|
|
285
|
+
expect(grantedSecretRead.status).toBe(200);
|
|
286
|
+
expect(configValue(grantedSecretRead.body, CONFIG_SECRET_KEY)).toBe(CONFIG_SECRET_VALUE);
|
|
287
|
+
expect(grantedSecretRead.body.message).toBeUndefined();
|
|
288
|
+
|
|
289
|
+
const fsUpload = await api(
|
|
290
|
+
server.base,
|
|
291
|
+
"POST",
|
|
292
|
+
`/api/fs/tasks/${taskId}/files?name=admission.txt`,
|
|
293
|
+
{ bearer: userToken, rawBody: Buffer.from("rbac admission file") },
|
|
294
|
+
);
|
|
295
|
+
expect(fsUpload.status).toBe(201);
|
|
296
|
+
|
|
297
|
+
const operatorCreate = await api(server.base, "POST", "/api/tasks", {
|
|
298
|
+
body: { task: "rbac admission operator bypass" },
|
|
299
|
+
});
|
|
300
|
+
expect(operatorCreate.status).toBe(201);
|
|
301
|
+
|
|
302
|
+
expect(await waitForAuditCount(dbPath, 2)).toBeGreaterThanOrEqual(2);
|
|
303
|
+
const userHttpRows = readAuditRows(dbPath).filter(
|
|
304
|
+
(row) =>
|
|
305
|
+
row.principalType === "user" &&
|
|
306
|
+
row.principalId === userId &&
|
|
307
|
+
row.source === "http" &&
|
|
308
|
+
row.resourceType === "http-route",
|
|
309
|
+
);
|
|
310
|
+
const denyRows = userHttpRows.filter((row) => row.decision === "deny");
|
|
311
|
+
expect(denyRows.map((row) => row.resourceId).sort()).toEqual([
|
|
312
|
+
"GET /api/agents/{id}/mcp-servers",
|
|
313
|
+
"GET /api/scripts/{id}/apis/{endpointId}/secret",
|
|
314
|
+
"POST /api/scripts/{id}/apis",
|
|
315
|
+
"POST /api/tasks",
|
|
316
|
+
]);
|
|
317
|
+
expect(
|
|
318
|
+
denyRows.some(
|
|
319
|
+
(row) =>
|
|
320
|
+
row.resourceId === "GET /api/agents/{id}/mcp-servers" &&
|
|
321
|
+
row.verb === "mcp-server.read.secrets",
|
|
322
|
+
),
|
|
323
|
+
).toBe(true);
|
|
324
|
+
expect(
|
|
325
|
+
denyRows.some(
|
|
326
|
+
(row) =>
|
|
327
|
+
row.resourceId === "GET /api/scripts/{id}/apis/{endpointId}/secret" &&
|
|
328
|
+
row.verb === "script.api.read.secrets",
|
|
329
|
+
),
|
|
330
|
+
).toBe(true);
|
|
331
|
+
expect(
|
|
332
|
+
userHttpRows.some(
|
|
333
|
+
(row) =>
|
|
334
|
+
row.resourceId === "PUT /api/favorites" &&
|
|
335
|
+
row.verb === "favorite.write.own" &&
|
|
336
|
+
row.decision === "allow",
|
|
337
|
+
),
|
|
338
|
+
).toBe(true);
|
|
339
|
+
|
|
340
|
+
rewriteUserToRequester(dbPath, userId);
|
|
341
|
+
|
|
342
|
+
await server.stop();
|
|
343
|
+
server = undefined;
|
|
344
|
+
|
|
345
|
+
server = await spawnSwarmServer({
|
|
346
|
+
dbPath,
|
|
347
|
+
logPath: join(dir, "server-off.log"),
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const flagOffCreate = await api(server.base, "POST", "/api/tasks", {
|
|
351
|
+
bearer: userToken,
|
|
352
|
+
body: { task: "rbac admission flag off legacy create" },
|
|
353
|
+
});
|
|
354
|
+
expect(flagOffCreate.status).toBe(201);
|
|
355
|
+
|
|
356
|
+
const flagOffSecretRead = await api(
|
|
357
|
+
server.base,
|
|
358
|
+
"GET",
|
|
359
|
+
"/api/config?scope=global&includeSecrets=true",
|
|
360
|
+
{ bearer: userToken },
|
|
361
|
+
);
|
|
362
|
+
expect(flagOffSecretRead.status).toBe(200);
|
|
363
|
+
expect(configValue(flagOffSecretRead.body, CONFIG_SECRET_KEY)).toBe(CONFIG_SECRET_VALUE);
|
|
364
|
+
expect(flagOffSecretRead.body.message).toBeUndefined();
|
|
365
|
+
|
|
366
|
+
const flagOffScriptApiSecret = await api(
|
|
367
|
+
server.base,
|
|
368
|
+
"GET",
|
|
369
|
+
`/api/scripts/${scriptId}/apis/${endpointId}/secret`,
|
|
370
|
+
{ bearer: userToken },
|
|
371
|
+
);
|
|
372
|
+
expect(flagOffScriptApiSecret.status).toBe(200);
|
|
373
|
+
expect(flagOffScriptApiSecret.body.token).toBe(scriptApiToken);
|
|
374
|
+
|
|
375
|
+
const flagOffMcpSecrets = await api(
|
|
376
|
+
server.base,
|
|
377
|
+
"GET",
|
|
378
|
+
`/api/agents/${WORKER_A}/mcp-servers?resolveSecrets=true`,
|
|
379
|
+
{ bearer: userToken },
|
|
380
|
+
);
|
|
381
|
+
expect(flagOffMcpSecrets.status).toBe(200);
|
|
382
|
+
expect(flagOffMcpSecrets.body.servers[0]?.resolvedHeaders?.Authorization).toBe(
|
|
383
|
+
CONFIG_SECRET_VALUE,
|
|
384
|
+
);
|
|
385
|
+
});
|
|
386
|
+
});
|