@desplega.ai/agent-swarm 1.113.0 → 1.115.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/{actions-q4n7cz6e.js → actions-17ya5wde.js} +8 -9
- package/dist/{app-es4nzc71.js → app-4f718yk0.js} +6 -6
- package/dist/{assistant-0x8ey0vs.js → assistant-ah7csnjz.js} +19 -18
- package/dist/{boot-reembed-69agkhv6.js → boot-reembed-4jpxhb25.js} +6 -6
- package/dist/{boot-reembed-p0ny05t8.js → boot-reembed-qata34xa.js} +5 -5
- package/dist/{boot-scrub-logs-yybrkmvr.js → boot-scrub-logs-tv48nn3z.js} +4 -4
- package/dist/{claude-adapter-mma9a1yr.js → claude-adapter-3csn5bks.js} +3 -3
- package/dist/{claude-managed-adapter-pv2nbqy4.js → claude-managed-adapter-kysmkz89.js} +2 -2
- package/dist/cli-0pygq9r1.js +429 -0
- package/dist/{cli-hsetkkd3.js → cli-12vb0xg6.js} +33 -9
- package/dist/{cli-ncfgsqbd.js → cli-15y7qhzp.js} +262 -33
- package/dist/{cli-7ga43spr.js → cli-1yjrbg1f.js} +1 -1
- package/dist/{cli-36ymmpmp.js → cli-3samkyap.js} +2 -2
- package/dist/cli-52fg7s7x.js +269 -0
- package/dist/{cli-hjhvekf4.js → cli-65ekje01.js} +11 -8
- package/dist/{cli-h3k622d0.js → cli-6aq8xhvm.js} +90 -3
- package/dist/{cli-f3qa069v.js → cli-7xje5amz.js} +1 -1
- package/dist/cli-86fdnerz.js +405 -0
- package/dist/{cli-9cy6nc5m.js → cli-8fjwym53.js} +1 -1
- package/dist/{cli-c3frqf65.js → cli-b23axgz4.js} +3 -3
- package/dist/{cli-a0t7vapf.js → cli-bc89my0z.js} +1 -1
- package/dist/{cli-bnzbn8j8.js → cli-beea4a3y.js} +73 -24
- package/dist/{cli-q21d49ac.js → cli-bw80ck94.js} +1 -1
- package/dist/{cli-pwc5e83c.js → cli-cchksf5q.js} +12 -7
- package/dist/{cli-30bbaveh.js → cli-cmw9rzy5.js} +2 -2
- package/dist/{cli-y6mrptcn.js → cli-cnwssmsr.js} +1 -1
- package/dist/{cli-r5g6ngtn.js → cli-dcca6j8m.js} +46 -5
- package/dist/{cli-tkmm5nf9.js → cli-e61fs2hp.js} +59 -2
- package/dist/{cli-6wscj9z0.js → cli-e7306swv.js} +14 -14
- package/dist/{cli-dcfyyh3t.js → cli-ftyf58xj.js} +2 -2
- package/dist/{cli-4xyj9jtq.js → cli-hrhye2rm.js} +1062 -478
- package/dist/cli-jkhwmmgg.js +136 -0
- package/dist/{cli-y4ycrnjc.js → cli-k3y4z3h5.js} +1 -1
- package/dist/{cli-6xd0bvf8.js → cli-ma04faw5.js} +1 -1
- package/dist/{cli-2fc97xm5.js → cli-mnbgv3w7.js} +4 -4
- package/dist/{cli-trmapn9k.js → cli-pet3aev6.js} +95 -10
- package/dist/{cli-z46pnksz.js → cli-q1rx7g0d.js} +175 -28
- package/dist/{cli-qwgtacd6.js → cli-r9f6ngrf.js} +1244 -336
- package/dist/{cli-gtkqc144.js → cli-shvaxw27.js} +13 -7
- package/dist/{cli-ywez6yps.js → cli-sq7f72d6.js} +1 -1
- package/dist/cli-t0vqevnm.js +393 -0
- package/dist/{cli-tbw13sx8.js → cli-tece6zgm.js} +1 -1
- package/dist/{cli-bgvskydy.js → cli-tffvrh6j.js} +1 -1
- package/dist/cli-wh91zby6.js +187222 -0
- package/dist/{cli-anrj584m.js → cli-xz9aq0rf.js} +1 -1
- package/dist/{cli-mq580e06.js → cli-zsrdty74.js} +18 -18
- package/dist/cli.js +32 -15
- package/dist/{codex-adapter-5cmy858w.js → codex-adapter-de3w0nch.js} +10 -10
- package/dist/{codex-session-runner-46q8mrba.js → codex-session-runner-bnw8cysk.js} +10 -10
- package/dist/{commands-z5dbwta3.js → commands-hch4e2k1.js} +4 -4
- package/dist/{db-bbgahh4z.js → db-p3h2j06x.js} +14 -4
- package/dist/{handlers-6b44317z.js → handlers-9er4zx9q.js} +17 -31
- package/dist/{hook-sbp5fmps.js → hook-4axs756z.js} +7 -7
- package/dist/{http-2xz43gz3.js → http-809fvtz2.js} +1934 -886
- package/dist/{index-wz1ea15p.js → index-2dgfj9av.js} +3 -3
- package/dist/{index-nhat35em.js → index-47em3d2n.js} +14 -11
- package/dist/{index-5ghxn8s6.js → index-9fqaktjr.js} +12 -9
- package/dist/{index-0gzmqj4k.js → index-9ht8gr56.js} +13 -10
- package/dist/{index-vfedgdw6.js → index-rm85cfdx.js} +368 -40
- package/dist/{index-ykvs43z4.js → index-t2snazez.js} +4 -4
- package/dist/{keepalive-62gfj91d.js → keepalive-3cps6e3e.js} +7 -6
- package/dist/{lead-ag0akn0v.js → lead-fkk95wgq.js} +36 -32
- package/dist/{maintenance-xxrrzk5x.js → maintenance-fweqh0yj.js} +6 -6
- package/dist/{mistral-conversations-26yrnbf1.js → mistral-conversations-7gqnz2wt.js} +7 -7
- package/dist/oauth-refresh-sweep-sycqtbph.js +114 -0
- package/dist/{onboard-64zyfcbs.js → onboard-cy0rkgvx.js} +2 -2
- package/dist/{opencode-adapter-mqsxr170.js → opencode-adapter-p6bxnbm4.js} +2 -2
- package/dist/{otel-zjd9zaxs.js → otel-g299k41b.js} +2 -2
- package/dist/{otel-impl-b86qseaa.js → otel-impl-wbns8hf7.js} +1 -1
- package/dist/{pi-mono-adapter-kxjxpfgy.js → pi-mono-adapter-05sck1ds.js} +17 -109
- package/dist/{pricing-refresh-cgtkffpa.js → pricing-refresh-0hvazmkf.js} +6 -6
- package/dist/rbac-roles-kfesjy9y.js +28 -0
- package/dist/rbac-roles-mwcwhbxc.js +283 -0
- package/dist/{seed-pricing-1r1y1xkq.js → seed-pricing-h2kdq4cq.js} +5 -5
- package/dist/{setup-mkcgkjf7.js → setup-8506q97w.js} +2 -2
- package/dist/{worker-351sze63.js → worker-4r5c6zyb.js} +36 -32
- package/openapi.json +1327 -20
- package/package.json +3 -3
- package/src/agentmail/handlers.ts +16 -6
- package/src/agentmail/templates.ts +25 -5
- package/src/be/audit-user.ts +28 -4
- package/src/be/db-queries/oauth.ts +42 -0
- package/src/be/db.ts +314 -28
- package/src/be/identity.ts +79 -0
- package/src/be/mcp-proxy.ts +132 -0
- package/src/be/migrations/109_rbac_roles.sql +79 -0
- package/src/be/migrations/111_oauth_credential_bindings.sql +6 -0
- package/src/be/migrations/112_script_connections_graphql.sql +96 -0
- package/src/be/migrations/113_task_routing_affinity.sql +9 -0
- package/src/be/migrations/114_backfill_gpt_5_6_pricing.sql +15 -0
- package/src/be/modelsdev-cache.json +135 -0
- package/src/be/oauth-credential-bindings.ts +35 -0
- package/src/be/oauth-refresh-sweep.ts +159 -0
- package/src/be/rbac-audit.ts +52 -13
- package/src/be/rbac-roles.ts +424 -0
- package/src/be/script-connections.ts +459 -40
- package/src/be/script-credential-broker.ts +29 -3
- package/src/be/scripts/typecheck.ts +51 -7
- package/src/be/seed-scripts/catalog/catalog-report.inline.ts +325 -24
- package/src/be/seed-scripts/catalog/catalog-report.ts +325 -24
- package/src/be/users.ts +24 -0
- package/src/cli.tsx +17 -0
- package/src/github/handlers.ts +10 -9
- package/src/github/templates.ts +45 -9
- package/src/gitlab/handlers.ts +20 -8
- package/src/gitlab/templates.ts +18 -6
- package/src/heartbeat/heartbeat.ts +143 -21
- package/src/http/all-routes.ts +3 -0
- package/src/http/approval-requests.ts +12 -0
- package/src/http/config.ts +44 -10
- package/src/http/core.ts +30 -3
- package/src/http/favorites.ts +1 -5
- package/src/http/index.ts +25 -1
- package/src/http/mcp-oauth.ts +2 -0
- package/src/http/mcp-servers.ts +44 -3
- package/src/http/oauth-generic.ts +105 -0
- package/src/http/poll.ts +20 -3
- package/src/http/script-connection-proxy.ts +116 -0
- package/src/http/script-connections.ts +1657 -0
- package/src/http/scripts.ts +27 -7
- package/src/http/skills.ts +11 -0
- package/src/http/tasks.ts +27 -13
- package/src/http/x.ts +6 -2
- package/src/integrations/kapso/inbound.ts +23 -6
- package/src/jira/sync.ts +111 -17
- package/src/jira/templates.ts +10 -2
- package/src/mcp-client/http-client.ts +194 -0
- package/src/oauth/app-validation.ts +16 -0
- package/src/oauth/ensure-token.ts +69 -14
- package/src/oauth/mcp-wrapper.ts +15 -0
- package/src/oauth/wrapper.ts +48 -20
- package/src/prompts/session-templates.ts +3 -3
- package/src/providers/codex-models.ts +100 -30
- package/src/providers/modelsdev-reasoning.json +51 -0
- package/src/providers/pi-mono-mcp-client.ts +2 -133
- package/src/providers/reasoning-effort.ts +11 -5
- package/src/rbac/admission.ts +68 -0
- package/src/rbac/index.ts +2 -0
- package/src/rbac/legacy-policy.ts +12 -0
- package/src/rbac/permissions.ts +48 -0
- package/src/scheduler/scheduler.ts +9 -1
- package/src/scripts-runtime/api-client.ts +151 -19
- package/src/scripts-runtime/api-types.ts +38 -6
- package/src/scripts-runtime/credential-broker/default-bindings.ts +1 -0
- package/src/scripts-runtime/credential-broker/types.ts +8 -0
- package/src/scripts-runtime/ctx.ts +11 -1
- package/src/scripts-runtime/eval-harness.ts +5 -1
- package/src/scripts-runtime/executors/types.ts +2 -1
- package/src/scripts-runtime/loader.ts +3 -1
- package/src/scripts-runtime/mcp-client.ts +87 -0
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +6 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +19 -0
- package/src/server-user.ts +114 -59
- package/src/server.ts +12 -0
- package/src/slack/assistant.ts +14 -6
- package/src/slack/enrich.ts +17 -0
- package/src/slack/handlers.ts +8 -22
- package/src/slack/responses.ts +91 -1
- package/src/slack/thread-buffer.ts +6 -3
- package/src/slack/watcher.ts +9 -6
- package/src/tasks/worker-follow-up.ts +100 -3
- package/src/tests/agentmail-handlers.test.ts +32 -3
- package/src/tests/approval-requests.test.ts +61 -0
- package/src/tests/audit-user.test.ts +47 -1
- package/src/tests/codex-adapter.test.ts +46 -23
- package/src/tests/credential-broker.test.ts +5 -2
- package/src/tests/delete-page-tool.test.ts +197 -0
- package/src/tests/github-handlers.test.ts +28 -2
- package/src/tests/gitlab-handlers.test.ts +125 -0
- package/src/tests/heartbeat.test.ts +48 -3
- package/src/tests/identity.test.ts +117 -0
- package/src/tests/jira-sync.test.ts +99 -1
- package/src/tests/kapso-inbound.test.ts +7 -0
- package/src/tests/mcp-tools-user.test.ts +60 -6
- package/src/tests/model-control.test.ts +8 -2
- package/src/tests/oauth-credential-bindings.test.ts +490 -0
- package/src/tests/oauth-refresh-sweep.test.ts +160 -0
- package/src/tests/pool-affinity.test.ts +401 -0
- package/src/tests/prompt-template-remaining.test.ts +2 -2
- package/src/tests/rbac-admission-e2e.test.ts +386 -0
- package/src/tests/rbac-admission.test.ts +492 -0
- package/src/tests/rbac-e2e-helpers.ts +3 -0
- package/src/tests/rbac-engine.test.ts +50 -0
- package/src/tests/rbac-mcp-admission-e2e.test.ts +362 -0
- package/src/tests/rbac-roles.test.ts +328 -0
- package/src/tests/reasoning-effort.test.ts +15 -2
- package/src/tests/schedule-target-type.test.ts +50 -0
- package/src/tests/scheduled-tasks.test.ts +40 -0
- package/src/tests/script-apis-mcp.test.ts +37 -13
- package/src/tests/script-connections-http.test.ts +1158 -0
- package/src/tests/script-connections-mcp.test.ts +894 -0
- package/src/tests/script-connections.test.ts +754 -6
- package/src/tests/scripts-external-api.test.ts +49 -1
- package/src/tests/scripts-runtime-secret-egress.test.ts +1 -0
- package/src/tests/scripts-runtime.test.ts +291 -0
- package/src/tests/seed-scripts.test.ts +36 -0
- package/src/tests/slack-identity-resolution.test.ts +37 -3
- package/src/tests/slack-inline-output.test.ts +65 -0
- package/src/tests/slack-thread-buffer.test.ts +28 -0
- package/src/tests/tool-annotations.test.ts +1 -0
- package/src/tests/user-token-rest-auth.test.ts +140 -1
- package/src/tests/workflow-swarm-script.test.ts +44 -0
- package/src/tests/workflow-triggers-v2.test.ts +300 -1
- package/src/tools/credential-bindings/tool.ts +277 -18
- package/src/tools/delete-page.ts +145 -0
- package/src/tools/mcp-servers/mcp-server-delete.ts +7 -4
- package/src/tools/request-human-input.ts +2 -0
- package/src/tools/resolve-user.ts +77 -27
- package/src/tools/script-apis.ts +62 -3
- package/src/tools/script-connections/tool.ts +275 -31
- package/src/tools/send-task.ts +15 -1
- package/src/tools/task-action.ts +34 -2
- package/src/tools/templates.ts +53 -1
- package/src/tools/tool-config.ts +2 -1
- package/src/tools/utils.ts +2 -0
- package/src/tools/workflows/create-workflow.ts +8 -1
- package/src/tools/workflows/update-workflow.ts +11 -2
- package/src/types.ts +90 -11
- package/src/utils/scoped-resource.ts +25 -0
- package/src/workflows/executors/human-in-the-loop.ts +1 -0
- package/src/workflows/executors/swarm-script.ts +7 -1
- package/src/workflows/triggers.ts +146 -18
- package/templates/skills/swarm-scripts/content.md +33 -8
- package/dist/cli-bgef578c.js +0 -771
- package/dist/cli-c9mtm09b.js +0 -43
- package/dist/cli-ceewqajw.js +0 -1698
- package/dist/{anthropic-messages-8nc6r2dw.js → anthropic-messages-kq3pjhbp.js} +6 -6
- package/dist/{azure-openai-responses-6yfzw3qw.js → azure-openai-responses-gm1ejtmn.js} +7 -7
- package/dist/{cli-vbraamxr.js → cli-6b4wwmm8.js} +9 -9
- package/dist/{cli-2r7s8jhp.js → cli-sznberje.js} +3 -3
- package/dist/{google-generative-ai-vfq6ymz9.js → google-generative-ai-hyrra2jm.js} +3 -3
- package/dist/{google-vertex-mj5y5rfj.js → google-vertex-8bs2pzgn.js} +3 -3
- package/dist/{openai-codex-responses-8h6kvfv8.js → openai-codex-responses-3nkvje79.js} +4 -4
- package/dist/{openai-completions-x6kjnqjd.js → openai-completions-0asftkwg.js} +11 -11
- package/dist/{openai-responses-7jwyz3ds.js → openai-responses-pmatek45.js} +10 -10
package/src/be/rbac-audit.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* ticks daily and deletes rows older than RBAC_AUDIT_RETENTION_DAYS
|
|
17
17
|
* (default 30).
|
|
18
18
|
*/
|
|
19
|
-
import type { RbacCheck, RbacDecision } from "../rbac";
|
|
19
|
+
import type { AdmissionDecision, RbacCheck, RbacDecision } from "../rbac";
|
|
20
20
|
import { getDb } from "./db";
|
|
21
21
|
|
|
22
22
|
type AuditRow = {
|
|
@@ -87,6 +87,20 @@ function toRow(check: RbacCheck, decision: RbacDecision): AuditRow {
|
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
function enqueueRow(row: AuditRow): void {
|
|
91
|
+
buffer.push(row);
|
|
92
|
+
// The threshold flush is deferred to a zero-delay timeout so the caller
|
|
93
|
+
// that happens to be the 200th never pays for the SQLite transaction itself.
|
|
94
|
+
if (buffer.length >= FLUSH_MAX_ROWS && !thresholdFlushScheduled) {
|
|
95
|
+
thresholdFlushScheduled = true;
|
|
96
|
+
const t = setTimeout(() => {
|
|
97
|
+
thresholdFlushScheduled = false;
|
|
98
|
+
flushAuditBuffer();
|
|
99
|
+
}, 0);
|
|
100
|
+
if (typeof t.unref === "function") t.unref();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
90
104
|
/**
|
|
91
105
|
* The concrete AuditSink for `can()`. Never throws — `can()` already swallows
|
|
92
106
|
* sink exceptions, but the audit path defends independently.
|
|
@@ -94,23 +108,48 @@ function toRow(check: RbacCheck, decision: RbacDecision): AuditRow {
|
|
|
94
108
|
export function enqueueAuditRow(check: RbacCheck, decision: RbacDecision): void {
|
|
95
109
|
if (isAuditDisabled()) return;
|
|
96
110
|
try {
|
|
97
|
-
|
|
98
|
-
// The threshold flush is deferred to a zero-delay timeout so the can()
|
|
99
|
-
// call that happens to be the 200th never pays for the SQLite transaction
|
|
100
|
-
// itself — audit stays fire-and-forget on the request path.
|
|
101
|
-
if (buffer.length >= FLUSH_MAX_ROWS && !thresholdFlushScheduled) {
|
|
102
|
-
thresholdFlushScheduled = true;
|
|
103
|
-
const t = setTimeout(() => {
|
|
104
|
-
thresholdFlushScheduled = false;
|
|
105
|
-
flushAuditBuffer();
|
|
106
|
-
}, 0);
|
|
107
|
-
if (typeof t.unref === "function") t.unref();
|
|
108
|
-
}
|
|
111
|
+
enqueueRow(toRow(check, decision));
|
|
109
112
|
} catch (err) {
|
|
110
113
|
console.warn("[rbac-audit] enqueue failed, dropping row:", (err as Error).message);
|
|
111
114
|
}
|
|
112
115
|
}
|
|
113
116
|
|
|
117
|
+
type AdmissionAuditInput = {
|
|
118
|
+
userId: string;
|
|
119
|
+
decision: AdmissionDecision;
|
|
120
|
+
} & (
|
|
121
|
+
| {
|
|
122
|
+
source?: "http";
|
|
123
|
+
method: string | undefined;
|
|
124
|
+
route: string;
|
|
125
|
+
}
|
|
126
|
+
| {
|
|
127
|
+
source: "mcp";
|
|
128
|
+
toolName: string;
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
export function enqueueAdmissionRow(input: AdmissionAuditInput): void {
|
|
133
|
+
if (isAuditDisabled()) return;
|
|
134
|
+
try {
|
|
135
|
+
const isMcp = input.source === "mcp";
|
|
136
|
+
const method = isMcp ? undefined : (input.method ?? "UNKNOWN").toUpperCase();
|
|
137
|
+
enqueueRow({
|
|
138
|
+
principalType: "user",
|
|
139
|
+
principalId: input.userId,
|
|
140
|
+
originatorUserId: null,
|
|
141
|
+
verb: input.decision.verb ?? "(admission:no-verb)",
|
|
142
|
+
resourceType: isMcp ? "mcp-tool" : "http-route",
|
|
143
|
+
resourceId: isMcp ? input.toolName : `${method} ${input.route}`,
|
|
144
|
+
decision: input.decision.allow ? "allow" : "deny",
|
|
145
|
+
reason: input.decision.allow ? null : input.decision.reason,
|
|
146
|
+
source: isMcp ? "mcp" : "http",
|
|
147
|
+
});
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.warn("[rbac-audit] enqueue failed, dropping admission row:", (err as Error).message);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
114
153
|
/**
|
|
115
154
|
* Drain the buffer into permission_audit in one transaction. Called by the
|
|
116
155
|
* flush interval, the 200-row threshold, and shutdown. A failed flush logs a
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import type { PermissionVerb } from "../rbac";
|
|
2
|
+
import { PermissionVerbSchema } from "../rbac";
|
|
3
|
+
import { getDb } from "./db";
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_ROLE_ID = "rbac-role-admin";
|
|
6
|
+
|
|
7
|
+
type BuiltinRoleDefinition = {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
grantsAll: boolean;
|
|
12
|
+
verbs: PermissionVerb[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const BUILTIN_ROLES = [
|
|
16
|
+
{
|
|
17
|
+
id: DEFAULT_ROLE_ID,
|
|
18
|
+
name: "admin",
|
|
19
|
+
description: "Full access including verb-less routes (legacy-equivalent default).",
|
|
20
|
+
isBuiltin: true,
|
|
21
|
+
grantsAll: true,
|
|
22
|
+
verbs: [] as PermissionVerb[],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "rbac-role-requester",
|
|
26
|
+
name: "requester",
|
|
27
|
+
description: "Own-task lifecycle: what legacy policy grants user principals.",
|
|
28
|
+
isBuiltin: true,
|
|
29
|
+
grantsAll: false,
|
|
30
|
+
verbs: [
|
|
31
|
+
"task.create.own",
|
|
32
|
+
"task.read.own",
|
|
33
|
+
"task.cancel.own",
|
|
34
|
+
"task.action.own",
|
|
35
|
+
"task.fs.mutate",
|
|
36
|
+
"favorite.write.own",
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
] satisfies (BuiltinRoleDefinition & { isBuiltin: true })[];
|
|
40
|
+
|
|
41
|
+
export type EffectiveGrant = {
|
|
42
|
+
grantsAll: boolean;
|
|
43
|
+
verbs: ReadonlySet<PermissionVerb>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type UserRole = {
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
description: string | null;
|
|
50
|
+
isBuiltin: boolean;
|
|
51
|
+
grantsAll: boolean;
|
|
52
|
+
createdAt: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const CREATE_USER_DEFAULT_ROLE_TRIGGER_SQL = `
|
|
56
|
+
CREATE TRIGGER IF NOT EXISTS trg_users_default_role
|
|
57
|
+
AFTER INSERT ON users
|
|
58
|
+
BEGIN
|
|
59
|
+
INSERT OR IGNORE INTO principal_roles (principalType, principalId, roleId)
|
|
60
|
+
SELECT 'user', NEW.id, 'rbac-role-admin'
|
|
61
|
+
WHERE EXISTS (SELECT 1 FROM roles WHERE id = 'rbac-role-admin');
|
|
62
|
+
END;
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
type GrantRow = {
|
|
66
|
+
roleId: string;
|
|
67
|
+
grantsAll: number;
|
|
68
|
+
verb: string | null;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type RoleIdRow = {
|
|
72
|
+
id: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
type RoleIdentityRow = {
|
|
76
|
+
id: string;
|
|
77
|
+
name: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type UserRoleRow = {
|
|
81
|
+
id: string;
|
|
82
|
+
name: string;
|
|
83
|
+
description: string | null;
|
|
84
|
+
isBuiltin: number;
|
|
85
|
+
grantsAll: number;
|
|
86
|
+
createdAt: string;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
type PermissionRow = {
|
|
90
|
+
verb: string;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
type RbacRoleSummaryRow = {
|
|
94
|
+
name: string;
|
|
95
|
+
isBuiltin: number;
|
|
96
|
+
grantsAll: number;
|
|
97
|
+
verbCount: number;
|
|
98
|
+
attachedUserCount: number;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
type RbacSeedSyncStats = {
|
|
102
|
+
rolesInserted: number;
|
|
103
|
+
rolesUpdated: number;
|
|
104
|
+
permissionsInserted: number;
|
|
105
|
+
permissionsDeleted: number;
|
|
106
|
+
usersBackfilled: number;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
function roleRowToUserRole(row: UserRoleRow): UserRole {
|
|
110
|
+
return {
|
|
111
|
+
id: row.id,
|
|
112
|
+
name: row.name,
|
|
113
|
+
description: row.description,
|
|
114
|
+
isBuiltin: row.isBuiltin === 1,
|
|
115
|
+
grantsAll: row.grantsAll === 1,
|
|
116
|
+
createdAt: row.createdAt,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function requireRoleId(roleName: string): string {
|
|
121
|
+
const row = getDb()
|
|
122
|
+
.prepare<RoleIdRow, string>("SELECT id FROM roles WHERE name = ?")
|
|
123
|
+
.get(roleName);
|
|
124
|
+
if (!row) {
|
|
125
|
+
throw new Error(`Unknown RBAC role: ${roleName}`);
|
|
126
|
+
}
|
|
127
|
+
return row.id;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function validateBuiltinVerb(roleName: string, verb: string): PermissionVerb {
|
|
131
|
+
const parsed = PermissionVerbSchema.safeParse(verb);
|
|
132
|
+
if (!parsed.success) {
|
|
133
|
+
throw new Error(`Invalid RBAC permission verb "${verb}" in built-in role "${roleName}"`);
|
|
134
|
+
}
|
|
135
|
+
return parsed.data;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function validatedBuiltinVerbSets(): Map<string, Set<PermissionVerb>> {
|
|
139
|
+
const byRoleId = new Map<string, Set<PermissionVerb>>();
|
|
140
|
+
for (const role of BUILTIN_ROLES) {
|
|
141
|
+
const verbs = new Set<PermissionVerb>();
|
|
142
|
+
for (const verb of role.verbs) {
|
|
143
|
+
verbs.add(validateBuiltinVerb(role.name, verb));
|
|
144
|
+
}
|
|
145
|
+
byRoleId.set(role.id, verbs);
|
|
146
|
+
}
|
|
147
|
+
return byRoleId;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const loggedInvalidGrantVerbs = new Set<string>();
|
|
151
|
+
|
|
152
|
+
function parseDatabaseGrantVerb(roleId: string, verb: string): PermissionVerb | null {
|
|
153
|
+
const parsed = PermissionVerbSchema.safeParse(verb);
|
|
154
|
+
if (parsed.success) {
|
|
155
|
+
return parsed.data;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const logKey = `${roleId}:${verb}`;
|
|
159
|
+
if (!loggedInvalidGrantVerbs.has(logKey)) {
|
|
160
|
+
loggedInvalidGrantVerbs.add(logKey);
|
|
161
|
+
console.error(
|
|
162
|
+
`[rbac] Ignoring invalid role_permissions verb "${verb}" for roleId="${roleId}". ` +
|
|
163
|
+
"Invalid RBAC verbs grant nothing; remove or repair the row.",
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function getUserGrant(userId: string): EffectiveGrant {
|
|
171
|
+
const rows = getDb()
|
|
172
|
+
.prepare<GrantRow, string>(
|
|
173
|
+
`SELECT r.id AS roleId, r.grantsAll, rp.verb
|
|
174
|
+
FROM principal_roles pr
|
|
175
|
+
JOIN roles r ON r.id = pr.roleId
|
|
176
|
+
LEFT JOIN role_permissions rp ON rp.roleId = r.id
|
|
177
|
+
WHERE pr.principalType = 'user' AND pr.principalId = ?
|
|
178
|
+
ORDER BY r.grantsAll DESC`,
|
|
179
|
+
)
|
|
180
|
+
.all(userId);
|
|
181
|
+
|
|
182
|
+
if (rows.length === 0) {
|
|
183
|
+
return { grantsAll: false, verbs: new Set() };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const verbs = new Set<PermissionVerb>();
|
|
187
|
+
for (const row of rows) {
|
|
188
|
+
if (row.grantsAll === 1) {
|
|
189
|
+
return { grantsAll: true, verbs: new Set() };
|
|
190
|
+
}
|
|
191
|
+
if (row.verb) {
|
|
192
|
+
const verb = parseDatabaseGrantVerb(row.roleId, row.verb);
|
|
193
|
+
if (verb) {
|
|
194
|
+
verbs.add(verb);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { grantsAll: false, verbs };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function attachRole(userId: string, roleName: string): void {
|
|
203
|
+
const db = getDb();
|
|
204
|
+
db.transaction(() => {
|
|
205
|
+
const roleId = requireRoleId(roleName);
|
|
206
|
+
db.prepare(
|
|
207
|
+
`INSERT OR IGNORE INTO principal_roles (principalType, principalId, roleId)
|
|
208
|
+
VALUES ('user', ?, ?)`,
|
|
209
|
+
).run(userId, roleId);
|
|
210
|
+
})();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function detachRole(userId: string, roleName: string): void {
|
|
214
|
+
const db = getDb();
|
|
215
|
+
db.transaction(() => {
|
|
216
|
+
const roleId = requireRoleId(roleName);
|
|
217
|
+
db.prepare(
|
|
218
|
+
`DELETE FROM principal_roles
|
|
219
|
+
WHERE principalType = 'user' AND principalId = ? AND roleId = ?`,
|
|
220
|
+
).run(userId, roleId);
|
|
221
|
+
})();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function listUserRoles(userId: string): UserRole[] {
|
|
225
|
+
return getDb()
|
|
226
|
+
.prepare<UserRoleRow, string>(
|
|
227
|
+
`SELECT r.id, r.name, r.description, r.isBuiltin, r.grantsAll, r.createdAt
|
|
228
|
+
FROM principal_roles pr
|
|
229
|
+
JOIN roles r ON r.id = pr.roleId
|
|
230
|
+
WHERE pr.principalType = 'user' AND pr.principalId = ?
|
|
231
|
+
ORDER BY r.name`,
|
|
232
|
+
)
|
|
233
|
+
.all(userId)
|
|
234
|
+
.map(roleRowToUserRole);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function ensureRbacSeedsSynced(opts?: { quiet?: boolean }): RbacSeedSyncStats {
|
|
238
|
+
const db = getDb();
|
|
239
|
+
const desiredVerbsByRoleId = validatedBuiltinVerbSets();
|
|
240
|
+
|
|
241
|
+
const insertRole = db.prepare<null, [string, string, string, number]>(
|
|
242
|
+
`INSERT OR IGNORE INTO roles (id, name, description, isBuiltin, grantsAll)
|
|
243
|
+
VALUES (?, ?, ?, 1, ?)`,
|
|
244
|
+
);
|
|
245
|
+
const updateRole = db.prepare<null, [string, string, number, string, string, string, number]>(
|
|
246
|
+
`UPDATE roles
|
|
247
|
+
SET name = ?, description = ?, isBuiltin = 1, grantsAll = ?, lastUpdatedAt = CURRENT_TIMESTAMP
|
|
248
|
+
WHERE id = ?
|
|
249
|
+
AND (
|
|
250
|
+
name <> ?
|
|
251
|
+
OR COALESCE(description, '') <> COALESCE(?, '')
|
|
252
|
+
OR isBuiltin <> 1
|
|
253
|
+
OR grantsAll <> ?
|
|
254
|
+
)`,
|
|
255
|
+
);
|
|
256
|
+
const selectRoleById = db.prepare<RoleIdentityRow, string>(
|
|
257
|
+
"SELECT id, name FROM roles WHERE id = ?",
|
|
258
|
+
);
|
|
259
|
+
const selectRoleByName = db.prepare<RoleIdentityRow, string>(
|
|
260
|
+
"SELECT id, name FROM roles WHERE name = ?",
|
|
261
|
+
);
|
|
262
|
+
const selectRolePermissions = db.prepare<PermissionRow, string>(
|
|
263
|
+
"SELECT verb FROM role_permissions WHERE roleId = ?",
|
|
264
|
+
);
|
|
265
|
+
const deleteRolePermission = db.prepare<null, [string, string]>(
|
|
266
|
+
"DELETE FROM role_permissions WHERE roleId = ? AND verb = ?",
|
|
267
|
+
);
|
|
268
|
+
const insertRolePermission = db.prepare<null, [string, string]>(
|
|
269
|
+
"INSERT OR IGNORE INTO role_permissions (roleId, verb) VALUES (?, ?)",
|
|
270
|
+
);
|
|
271
|
+
const backfillDefaultRoleForZeroRoleUsers = db.prepare<null, string>(
|
|
272
|
+
`INSERT OR IGNORE INTO principal_roles (principalType, principalId, roleId)
|
|
273
|
+
SELECT 'user', u.id, ?
|
|
274
|
+
FROM users u
|
|
275
|
+
WHERE NOT EXISTS (
|
|
276
|
+
SELECT 1
|
|
277
|
+
FROM principal_roles pr
|
|
278
|
+
WHERE pr.principalType = 'user'
|
|
279
|
+
AND pr.principalId = u.id
|
|
280
|
+
)`,
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const stats: RbacSeedSyncStats = {
|
|
284
|
+
rolesInserted: 0,
|
|
285
|
+
rolesUpdated: 0,
|
|
286
|
+
permissionsInserted: 0,
|
|
287
|
+
permissionsDeleted: 0,
|
|
288
|
+
usersBackfilled: 0,
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
db.transaction(() => {
|
|
292
|
+
for (const role of BUILTIN_ROLES) {
|
|
293
|
+
const grantsAll = role.grantsAll ? 1 : 0;
|
|
294
|
+
const insertResult = insertRole.run(role.id, role.name, role.description, grantsAll);
|
|
295
|
+
stats.rolesInserted += insertResult.changes;
|
|
296
|
+
if (insertResult.changes === 0) {
|
|
297
|
+
const updateResult = updateRole.run(
|
|
298
|
+
role.name,
|
|
299
|
+
role.description,
|
|
300
|
+
grantsAll,
|
|
301
|
+
role.id,
|
|
302
|
+
role.name,
|
|
303
|
+
role.description,
|
|
304
|
+
grantsAll,
|
|
305
|
+
);
|
|
306
|
+
stats.rolesUpdated += updateResult.changes;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (const role of BUILTIN_ROLES) {
|
|
311
|
+
const row = selectRoleById.get(role.id);
|
|
312
|
+
if (row) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const colliding = selectRoleByName.get(role.name);
|
|
317
|
+
if (colliding) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`RBAC built-in role "${role.name}" (${role.id}) is missing because role ` +
|
|
320
|
+
`"${colliding.name}" (${colliding.id}) already uses that name. ` +
|
|
321
|
+
"Rename or remove the conflicting role, then rerun `rbac bootstrap`.",
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
throw new Error(
|
|
326
|
+
`RBAC built-in role "${role.name}" (${role.id}) is missing after seed sync. ` +
|
|
327
|
+
"Rerun `rbac bootstrap`; if it still fails, inspect the roles table.",
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
for (const role of BUILTIN_ROLES) {
|
|
332
|
+
const desiredVerbs = desiredVerbsByRoleId.get(role.id) ?? new Set<PermissionVerb>();
|
|
333
|
+
const existingRows = selectRolePermissions.all(role.id);
|
|
334
|
+
for (const row of existingRows) {
|
|
335
|
+
if (!desiredVerbs.has(row.verb as PermissionVerb)) {
|
|
336
|
+
stats.permissionsDeleted += deleteRolePermission.run(role.id, row.verb).changes;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
for (const verb of desiredVerbs) {
|
|
340
|
+
stats.permissionsInserted += insertRolePermission.run(role.id, verb).changes;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
db.run(CREATE_USER_DEFAULT_ROLE_TRIGGER_SQL);
|
|
345
|
+
|
|
346
|
+
// DES-445 pre-GA heal: zero user roles is never intentional today; users
|
|
347
|
+
// are deactivated, not stripped. When RBAC reaches GA with real role
|
|
348
|
+
// management, zero roles becomes a deliberate deny-all posture and this
|
|
349
|
+
// auto-attach MUST be removed. Keep the CLI as the explicit operator tool.
|
|
350
|
+
stats.usersBackfilled += backfillDefaultRoleForZeroRoleUsers.run(DEFAULT_ROLE_ID).changes;
|
|
351
|
+
})();
|
|
352
|
+
|
|
353
|
+
if (!opts?.quiet) {
|
|
354
|
+
console.log(
|
|
355
|
+
`[rbac] seed sync: roles inserted=${stats.rolesInserted}, updated=${stats.rolesUpdated}; permissions inserted=${stats.permissionsInserted}, deleted=${stats.permissionsDeleted}; trigger=ensured; users backfilled=${stats.usersBackfilled}`,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return stats;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function describeRbacFlag(): string {
|
|
363
|
+
const raw = process.env.RBAC_ENABLED;
|
|
364
|
+
if (raw === undefined) return "unset (off)";
|
|
365
|
+
return `${raw} (${raw === "true" ? "on" : "off"})`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function printRolesTable(rows: RbacRoleSummaryRow[]): void {
|
|
369
|
+
const tableRows = rows.map((row) => [
|
|
370
|
+
row.name,
|
|
371
|
+
row.isBuiltin === 1 ? "yes" : "no",
|
|
372
|
+
row.grantsAll === 1 ? "yes" : "no",
|
|
373
|
+
String(row.verbCount),
|
|
374
|
+
String(row.attachedUserCount),
|
|
375
|
+
]);
|
|
376
|
+
const headers = ["name", "builtin", "grantsAll", "verbs", "attachedUsers"];
|
|
377
|
+
const widths = headers.map((header, column) =>
|
|
378
|
+
Math.max(header.length, ...tableRows.map((row) => row[column]?.length ?? 0)),
|
|
379
|
+
);
|
|
380
|
+
const format = (row: string[]) =>
|
|
381
|
+
row.map((cell, column) => cell.padEnd(widths[column] ?? cell.length)).join(" ");
|
|
382
|
+
|
|
383
|
+
console.log("Roles:");
|
|
384
|
+
console.log(format(headers));
|
|
385
|
+
console.log(format(widths.map((width) => "-".repeat(width))));
|
|
386
|
+
for (const row of tableRows) {
|
|
387
|
+
console.log(format(row));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function getRbacRoleSummary(): RbacRoleSummaryRow[] {
|
|
392
|
+
return getDb()
|
|
393
|
+
.prepare<RbacRoleSummaryRow, []>(
|
|
394
|
+
`SELECT
|
|
395
|
+
r.name,
|
|
396
|
+
r.isBuiltin,
|
|
397
|
+
r.grantsAll,
|
|
398
|
+
COUNT(DISTINCT rp.verb) AS verbCount,
|
|
399
|
+
COUNT(DISTINCT CASE WHEN pr.principalType = 'user' THEN pr.principalId END)
|
|
400
|
+
AS attachedUserCount
|
|
401
|
+
FROM roles r
|
|
402
|
+
LEFT JOIN role_permissions rp ON rp.roleId = r.id
|
|
403
|
+
LEFT JOIN principal_roles pr ON pr.roleId = r.id
|
|
404
|
+
GROUP BY r.id, r.name, r.isBuiltin, r.grantsAll
|
|
405
|
+
ORDER BY r.isBuiltin DESC, r.name`,
|
|
406
|
+
)
|
|
407
|
+
.all();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export async function runRbacCliCommand(args: string[]): Promise<void> {
|
|
411
|
+
const subcommand = args[0];
|
|
412
|
+
if (args.length !== 1 || subcommand !== "bootstrap") {
|
|
413
|
+
throw new Error(`Unknown RBAC command: ${args.join(" ") || "(none)"}`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const stats = ensureRbacSeedsSynced({ quiet: true });
|
|
417
|
+
const roles = getRbacRoleSummary();
|
|
418
|
+
|
|
419
|
+
console.log("RBAC bootstrap complete");
|
|
420
|
+
console.log(`RBAC_ENABLED: ${describeRbacFlag()}`);
|
|
421
|
+
console.log(`Users backfilled this run: ${stats.usersBackfilled}`);
|
|
422
|
+
console.log("");
|
|
423
|
+
printRolesTable(roles);
|
|
424
|
+
}
|