@desplega.ai/agent-swarm 1.112.0 → 1.113.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/dist/{actions-eckb4g8r.js → actions-q4n7cz6e.js} +6 -6
- package/dist/{app-b05sgm02.js → app-es4nzc71.js} +3 -3
- package/dist/{assistant-7255sbyw.js → assistant-0x8ey0vs.js} +9 -9
- package/dist/{boot-reembed-y1bt3ttn.js → boot-reembed-69agkhv6.js} +4 -4
- package/dist/{boot-reembed-yv7awg4y.js → boot-reembed-p0ny05t8.js} +3 -3
- package/dist/{boot-scrub-logs-ddyaeqar.js → boot-scrub-logs-yybrkmvr.js} +2 -2
- package/dist/{cli-s9pj3c7z.js → cli-30bbaveh.js} +2 -2
- package/dist/{cli-r1btc895.js → cli-36ymmpmp.js} +2 -2
- package/dist/{cli-15598a4k.js → cli-4xyj9jtq.js} +1215 -235
- package/dist/{cli-52tbq17n.js → cli-bgef578c.js} +1 -1
- package/dist/{cli-76d1vtvq.js → cli-bgvskydy.js} +1 -1
- package/dist/{cli-pt7c7qxz.js → cli-bnzbn8j8.js} +3 -3
- package/dist/{cli-8f0dwca2.js → cli-c3frqf65.js} +4 -3
- package/dist/{cli-9p0qhead.js → cli-c9mtm09b.js} +1 -1
- package/dist/{cli-709t753c.js → cli-f3qa069v.js} +2 -2
- package/dist/{cli-94vzr3nq.js → cli-gtkqc144.js} +4 -4
- package/dist/{cli-7xkwgkrs.js → cli-h3k622d0.js} +1 -1
- package/dist/{cli-h9jx5mag.js → cli-hjhvekf4.js} +4 -4
- package/dist/{cli-tp7e60s3.js → cli-hsetkkd3.js} +2 -2
- package/dist/{cli-tch0ypp6.js → cli-mq580e06.js} +4 -4
- package/dist/{cli-951w7c79.js → cli-ncfgsqbd.js} +63 -7
- package/dist/{cli-30d2ct3k.js → cli-pwc5e83c.js} +3 -3
- package/dist/{cli-3r5y6ayj.js → cli-qwgtacd6.js} +2 -2
- package/dist/{cli-56s4ehzw.js → cli-r5g6ngtn.js} +1 -1
- package/dist/{cli-rp8pca98.js → cli-tbw13sx8.js} +1 -1
- package/dist/{cli-hm27prrc.js → cli-trmapn9k.js} +5 -5
- package/dist/{cli-0c3n0eba.js → cli-y4ycrnjc.js} +1 -1
- package/dist/{cli-a5e8n5hg.js → cli-y6mrptcn.js} +1 -1
- package/dist/{cli-tg0f2wmz.js → cli-z46pnksz.js} +5 -5
- package/dist/cli.js +11 -10
- package/dist/{commands-14g7taf4.js → commands-z5dbwta3.js} +2 -2
- package/dist/{db-3tge4jrn.js → db-bbgahh4z.js} +2 -2
- package/dist/{handlers-sy64egg6.js → handlers-6b44317z.js} +9 -9
- package/dist/{hook-rnkzw8vp.js → hook-sbp5fmps.js} +1 -1
- package/dist/{http-xx9xptcs.js → http-2xz43gz3.js} +354 -94
- package/dist/{index-zfx5p959.js → index-0gzmqj4k.js} +8 -8
- package/dist/{index-3wr6v2mm.js → index-5ghxn8s6.js} +7 -7
- package/dist/{index-brma88d4.js → index-nhat35em.js} +9 -9
- package/dist/{index-ha8e9xn2.js → index-vfedgdw6.js} +26 -15
- package/dist/{keepalive-rbz05zad.js → keepalive-62gfj91d.js} +4 -4
- package/dist/{lead-s790v7r0.js → lead-ag0akn0v.js} +17 -17
- package/dist/{maintenance-420rmadb.js → maintenance-xxrrzk5x.js} +4 -4
- package/dist/{onboard-jgqamqnw.js → onboard-64zyfcbs.js} +2 -2
- package/dist/{otel-impl-ch3n3783.js → otel-impl-b86qseaa.js} +1 -1
- package/dist/{pricing-refresh-rmtrby6h.js → pricing-refresh-cgtkffpa.js} +4 -4
- package/dist/{seed-pricing-1szjzfj1.js → seed-pricing-1r1y1xkq.js} +3 -3
- package/dist/{setup-8jfzkyzv.js → setup-mkcgkjf7.js} +2 -2
- package/dist/{worker-40vq0pm8.js → worker-351sze63.js} +17 -17
- package/openapi.json +314 -3
- package/package.json +4 -3
- package/src/be/db.ts +43 -3
- package/src/be/memory/providers/sqlite-store.ts +5 -1
- package/src/be/migrations/108_rbac_permission_audit.sql +26 -0
- package/src/be/rbac-audit.ts +218 -0
- package/src/http/all-routes.ts +58 -0
- package/src/http/config.ts +55 -0
- package/src/http/favorites.ts +5 -0
- package/src/http/fs.ts +27 -7
- package/src/http/index.ts +26 -0
- package/src/http/kv.ts +21 -4
- package/src/http/route-def.ts +9 -0
- package/src/http/schedules.ts +56 -22
- package/src/http/scripts.ts +33 -0
- package/src/http/workflows.ts +13 -2
- package/src/prompts/base-prompt.ts +7 -6
- package/src/prompts/session-templates.ts +38 -11
- package/src/rbac/can.ts +44 -0
- package/src/rbac/index.ts +13 -0
- package/src/rbac/legacy-policy.ts +185 -0
- package/src/rbac/permissions.ts +182 -0
- package/src/rbac/types.ts +45 -0
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/swarm-sdk.ts +81 -0
- package/src/scripts-runtime/types/stdlib.d.ts +18 -2
- package/src/scripts-runtime/types/swarm-sdk.d.ts +18 -2
- package/src/server.ts +2 -0
- package/src/stdio.ts +43 -0
- package/src/tests/base-prompt.test.ts +7 -4
- package/src/tests/harness-provider-resolution.test.ts +5 -0
- package/src/tests/http-api-integration.test.ts +8 -1
- package/src/tests/prompt-template-session.test.ts +21 -8
- package/src/tests/rbac-audit.test.ts +345 -0
- package/src/tests/rbac-charact-http.test.ts +272 -0
- package/src/tests/rbac-charact-misc-tools.test.ts +428 -0
- package/src/tests/rbac-charact-skills.test.ts +492 -0
- package/src/tests/rbac-charact-slack.test.ts +283 -0
- package/src/tests/rbac-e2e-helpers.ts +305 -0
- package/src/tests/rbac-engine.test.ts +433 -0
- package/src/tests/rbac-lifecycle-e2e.test.ts +262 -0
- package/src/tests/rbac-wire-e2e.test.ts +574 -0
- package/src/tests/schedule-http-triage-tools.test.ts +121 -0
- package/src/tests/scheduled-tasks.test.ts +34 -0
- package/src/tests/scripts-http.test.ts +56 -7
- package/src/tests/swarm-config-reserved-keys.test.ts +17 -0
- package/src/tests/tool-annotations.test.ts +1 -0
- package/src/tests/update-schedule-mcp-tool.test.ts +57 -0
- package/src/tests/workflow-http-v2.test.ts +80 -0
- package/src/tools/cancel-task.ts +13 -3
- package/src/tools/context-diff.ts +12 -1
- package/src/tools/context-history.ts +12 -1
- package/src/tools/credential-bindings/tool.ts +12 -1
- package/src/tools/delete-channel.ts +12 -1
- package/src/tools/get-task-details.ts +1 -1
- package/src/tools/inject-learning.ts +12 -1
- package/src/tools/kv/kv-delete.ts +3 -18
- package/src/tools/kv/kv-incr.ts +3 -18
- package/src/tools/kv/kv-set.ts +3 -20
- package/src/tools/kv/kv-write-auth.ts +40 -0
- package/src/tools/manage-user.ts +12 -1
- package/src/tools/mcp-servers/mcp-server-create.ts +12 -1
- package/src/tools/mcp-servers/mcp-server-delete.ts +12 -1
- package/src/tools/mcp-servers/mcp-server-install.ts +12 -1
- package/src/tools/mcp-servers/mcp-server-uninstall.ts +12 -1
- package/src/tools/mcp-servers/mcp-server-update.ts +12 -1
- package/src/tools/memory-delete.ts +12 -4
- package/src/tools/register-kapso-number.ts +23 -2
- package/src/tools/schedules/create-schedule.ts +4 -3
- package/src/tools/schedules/index.ts +1 -0
- package/src/tools/schedules/list-schedules.ts +36 -2
- package/src/tools/schedules/patch-schedule.ts +405 -0
- package/src/tools/schedules/update-schedule.ts +2 -2
- package/src/tools/script-connections/tool.ts +12 -1
- package/src/tools/skills/skill-create.ts +12 -1
- package/src/tools/skills/skill-delete.ts +12 -1
- package/src/tools/skills/skill-install-remote.ts +12 -1
- package/src/tools/skills/skill-install.ts +12 -1
- package/src/tools/skills/skill-uninstall.ts +12 -1
- package/src/tools/skills/skill-update.ts +25 -2
- package/src/tools/slack-delete.ts +8 -1
- package/src/tools/slack-post.ts +8 -1
- package/src/tools/slack-read.ts +8 -1
- package/src/tools/slack-start-thread.ts +8 -1
- package/src/tools/slack-update.ts +8 -1
- package/src/tools/slack-upload-file.ts +8 -1
- package/src/tools/swarm-config/delete-config.ts +28 -1
- package/src/tools/swarm-config/get-config.ts +32 -5
- package/src/tools/swarm-config/list-config.ts +31 -5
- package/src/tools/swarm-config/set-config.ts +38 -1
- package/src/tools/task-action.ts +1 -1
- package/src/tools/task-tool-ctx.ts +19 -3
- package/src/tools/tool-config.ts +2 -1
- package/src/tools/update-profile.ts +8 -1
- package/src/tools/workflows/list-workflows.ts +14 -2
- package/templates/skills/swarm-scripts/SKILL.md +4 -9
- package/templates/skills/swarm-scripts/content.md +20 -9
package/src/http/schedules.ts
CHANGED
|
@@ -22,6 +22,27 @@ import { json, jsonError } from "./utils";
|
|
|
22
22
|
|
|
23
23
|
// ─── Route Definitions ───────────────────────────────────────────────────────
|
|
24
24
|
|
|
25
|
+
const scheduleUpdateBodySchema = z.object({
|
|
26
|
+
name: z.string().optional(),
|
|
27
|
+
description: z.string().optional(),
|
|
28
|
+
cronExpression: z.string().nullable().optional(),
|
|
29
|
+
intervalMs: z.number().int().positive().nullable().optional(),
|
|
30
|
+
taskTemplate: z.string().optional(),
|
|
31
|
+
taskType: z.string().optional(),
|
|
32
|
+
tags: z.array(z.string()).optional(),
|
|
33
|
+
priority: z.number().int().optional(),
|
|
34
|
+
targetAgentId: z.string().uuid().nullable().optional(),
|
|
35
|
+
enabled: z.boolean().optional(),
|
|
36
|
+
timezone: z.string().optional(),
|
|
37
|
+
model: z.string().nullable().optional(),
|
|
38
|
+
modelTier: ModelTierSchema.nullable().optional(),
|
|
39
|
+
nextRunAt: z.string().nullable().optional(),
|
|
40
|
+
targetType: ScheduledTaskTargetTypeSchema.optional(),
|
|
41
|
+
workflowId: z.string().uuid().nullable().optional(),
|
|
42
|
+
scriptName: z.string().nullable().optional(),
|
|
43
|
+
scriptArgs: z.record(z.string(), z.unknown()).nullable().optional(),
|
|
44
|
+
});
|
|
45
|
+
|
|
25
46
|
const createSchedule = route({
|
|
26
47
|
method: "post",
|
|
27
48
|
path: "/api/schedules",
|
|
@@ -93,6 +114,8 @@ const listSchedules = route({
|
|
|
93
114
|
.enum(["true", "false"])
|
|
94
115
|
.optional()
|
|
95
116
|
.transform((v) => (v === undefined ? undefined : v === "true")),
|
|
117
|
+
consecutiveErrorsMin: z.coerce.number().int().min(0).optional(),
|
|
118
|
+
lastRunStatus: z.enum(["failed", "succeeded"]).optional(),
|
|
96
119
|
/** `full` restores the legacy shape (includes `taskTemplate`); default is slim. */
|
|
97
120
|
fields: z.enum(["full", "slim"]).optional(),
|
|
98
121
|
}),
|
|
@@ -121,26 +144,7 @@ const updateSchedule = route({
|
|
|
121
144
|
summary: "Update a schedule",
|
|
122
145
|
tags: ["Schedules"],
|
|
123
146
|
params: z.object({ id: z.string() }),
|
|
124
|
-
body:
|
|
125
|
-
name: z.string().optional(),
|
|
126
|
-
description: z.string().optional(),
|
|
127
|
-
cronExpression: z.string().nullable().optional(),
|
|
128
|
-
intervalMs: z.number().int().positive().nullable().optional(),
|
|
129
|
-
taskTemplate: z.string().optional(),
|
|
130
|
-
taskType: z.string().optional(),
|
|
131
|
-
tags: z.array(z.string()).optional(),
|
|
132
|
-
priority: z.number().int().optional(),
|
|
133
|
-
targetAgentId: z.string().uuid().optional(),
|
|
134
|
-
enabled: z.boolean().optional(),
|
|
135
|
-
timezone: z.string().optional(),
|
|
136
|
-
model: z.string().optional(),
|
|
137
|
-
modelTier: ModelTierSchema.nullable().optional(),
|
|
138
|
-
nextRunAt: z.string().nullable().optional(),
|
|
139
|
-
targetType: ScheduledTaskTargetTypeSchema.optional(),
|
|
140
|
-
workflowId: z.string().uuid().nullable().optional(),
|
|
141
|
-
scriptName: z.string().nullable().optional(),
|
|
142
|
-
scriptArgs: z.record(z.string(), z.unknown()).nullable().optional(),
|
|
143
|
-
}),
|
|
147
|
+
body: scheduleUpdateBodySchema,
|
|
144
148
|
responses: {
|
|
145
149
|
200: { description: "Schedule updated" },
|
|
146
150
|
400: { description: "Validation error" },
|
|
@@ -149,6 +153,28 @@ const updateSchedule = route({
|
|
|
149
153
|
},
|
|
150
154
|
});
|
|
151
155
|
|
|
156
|
+
const patchSchedule = route({
|
|
157
|
+
method: "patch",
|
|
158
|
+
path: "/api/schedules/{id}",
|
|
159
|
+
pattern: ["api", "schedules", null],
|
|
160
|
+
summary: "Patch a schedule",
|
|
161
|
+
description:
|
|
162
|
+
"Partially updates a schedule by shallow-merging provided fields over the existing row.",
|
|
163
|
+
tags: ["Schedules"],
|
|
164
|
+
params: z.object({ id: z.string() }),
|
|
165
|
+
body: scheduleUpdateBodySchema,
|
|
166
|
+
responses: {
|
|
167
|
+
200: { description: "Schedule patched" },
|
|
168
|
+
400: { description: "Validation error" },
|
|
169
|
+
404: { description: "Schedule not found" },
|
|
170
|
+
409: { description: "Duplicate name" },
|
|
171
|
+
},
|
|
172
|
+
rbac: {
|
|
173
|
+
ungated:
|
|
174
|
+
"matches existing schedule update/delete posture: bearer-authenticated agents may manage schedules",
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
152
178
|
const deleteSchedule = route({
|
|
153
179
|
method: "delete",
|
|
154
180
|
path: "/api/schedules/{id}",
|
|
@@ -182,6 +208,8 @@ export async function handleSchedules(
|
|
|
182
208
|
workflowId: parsed.query.workflowId,
|
|
183
209
|
scriptName: parsed.query.scriptName,
|
|
184
210
|
hideCompleted: parsed.query.hideCompleted,
|
|
211
|
+
consecutiveErrorsMin: parsed.query.consecutiveErrorsMin,
|
|
212
|
+
lastRunStatus: parsed.query.lastRunStatus,
|
|
185
213
|
};
|
|
186
214
|
// List responses default to slim (no full `taskTemplate`); `?fields=full` restores it.
|
|
187
215
|
const schedules =
|
|
@@ -387,8 +415,14 @@ export async function handleSchedules(
|
|
|
387
415
|
return true;
|
|
388
416
|
}
|
|
389
417
|
|
|
390
|
-
if (
|
|
391
|
-
|
|
418
|
+
if (
|
|
419
|
+
updateSchedule.match(req.method, pathSegments) ||
|
|
420
|
+
patchSchedule.match(req.method, pathSegments)
|
|
421
|
+
) {
|
|
422
|
+
const routeHandle = updateSchedule.match(req.method, pathSegments)
|
|
423
|
+
? updateSchedule
|
|
424
|
+
: patchSchedule;
|
|
425
|
+
const parsed = await routeHandle.parse(req, res, pathSegments, queryParams);
|
|
392
426
|
if (!parsed) return true;
|
|
393
427
|
const body = parsed.body as Record<string, unknown>;
|
|
394
428
|
if (parsed.body.model !== undefined || parsed.body.modelTier !== undefined) {
|
package/src/http/scripts.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
scriptSdkTypesWithGeneratedApis,
|
|
28
28
|
typecheckScript,
|
|
29
29
|
} from "../be/scripts/typecheck";
|
|
30
|
+
import { can } from "../rbac";
|
|
30
31
|
import { extractScriptSignature } from "../scripts-runtime/extract-signature";
|
|
31
32
|
import { runScript } from "../scripts-runtime/loader";
|
|
32
33
|
import {
|
|
@@ -96,6 +97,7 @@ const upsertRoute = route({
|
|
|
96
97
|
400: { description: "Validation or typecheck failure" },
|
|
97
98
|
403: { description: "Global write requires lead agent" },
|
|
98
99
|
},
|
|
100
|
+
rbac: { permission: "script.global.write" },
|
|
99
101
|
});
|
|
100
102
|
|
|
101
103
|
const runRoute = route({
|
|
@@ -145,6 +147,7 @@ const deleteRoute = route({
|
|
|
145
147
|
400: { description: "Validation error" },
|
|
146
148
|
403: { description: "Global delete requires lead agent" },
|
|
147
149
|
},
|
|
150
|
+
rbac: { permission: "script.global.delete" },
|
|
148
151
|
});
|
|
149
152
|
|
|
150
153
|
const typesRoute = route({
|
|
@@ -410,6 +413,21 @@ export async function handleScripts(
|
|
|
410
413
|
const agent = requireAgent(res, agentId);
|
|
411
414
|
if (!agent) return true;
|
|
412
415
|
|
|
416
|
+
// Global-scope writes require lead — the 403 this route has always
|
|
417
|
+
// documented, enforced since DES-445 slice 1. Agent-scope ops unchanged.
|
|
418
|
+
if (parsed.body.scope === "global") {
|
|
419
|
+
const decision = can({
|
|
420
|
+
principal: { kind: "agent", agentId: agent.id, isLead: agent.isLead },
|
|
421
|
+
verb: "script.global.write",
|
|
422
|
+
resource: { kind: "owned", scope: "global" },
|
|
423
|
+
source: "http",
|
|
424
|
+
});
|
|
425
|
+
if (!decision.allow) {
|
|
426
|
+
jsonError(res, "Global write requires lead agent", 403);
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
413
431
|
const typecheck = typecheckScript(parsed.body.source, { agentId: agent.id });
|
|
414
432
|
if (!typecheck.ok) {
|
|
415
433
|
json(
|
|
@@ -712,6 +730,21 @@ export async function handleScripts(
|
|
|
712
730
|
const agent = requireAgent(res, agentId);
|
|
713
731
|
if (!agent) return true;
|
|
714
732
|
|
|
733
|
+
// Global-scope deletes require lead — the 403 this route has always
|
|
734
|
+
// documented, enforced since DES-445 slice 1. Agent-scope ops unchanged.
|
|
735
|
+
if (parsed.query.scope === "global") {
|
|
736
|
+
const decision = can({
|
|
737
|
+
principal: { kind: "agent", agentId: agent.id, isLead: agent.isLead },
|
|
738
|
+
verb: "script.global.delete",
|
|
739
|
+
resource: { kind: "owned", scope: "global" },
|
|
740
|
+
source: "http",
|
|
741
|
+
});
|
|
742
|
+
if (!decision.allow) {
|
|
743
|
+
jsonError(res, "Global delete requires lead agent", 403);
|
|
744
|
+
return true;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
715
748
|
const deleted = deleteScript({
|
|
716
749
|
name: parsed.params.name,
|
|
717
750
|
scope: parsed.query.scope,
|
package/src/http/workflows.ts
CHANGED
|
@@ -45,6 +45,12 @@ const listWorkflowsRoute = route({
|
|
|
45
45
|
"Returns workflows WITHOUT the heavy `definition` (the full DAG) by default — the list view only needs a `nodeCount`, which is included. Pass `fields=full` to restore `definition` + trigger config. Fetch the full workflow via `GET /api/workflows/{id}`.",
|
|
46
46
|
tags: ["Workflows"],
|
|
47
47
|
query: z.object({
|
|
48
|
+
enabled: z
|
|
49
|
+
.enum(["true", "false"])
|
|
50
|
+
.optional()
|
|
51
|
+
.transform((v) => (v === undefined ? undefined : v === "true")),
|
|
52
|
+
consecutiveErrorsMin: z.coerce.number().int().min(0).optional(),
|
|
53
|
+
lastRunStatus: WorkflowRunStatusSchema.optional(),
|
|
48
54
|
/** `full` restores the legacy shape (includes `definition`); default is slim. */
|
|
49
55
|
fields: z.enum(["full", "slim"]).optional(),
|
|
50
56
|
}),
|
|
@@ -403,13 +409,18 @@ export async function handleWorkflows(
|
|
|
403
409
|
const parsed = await listWorkflowsRoute.parse(req, res, pathSegments, queryParams);
|
|
404
410
|
if (!parsed) return true;
|
|
405
411
|
const userId = resolveHttpAuditUserId(req, myAgentId);
|
|
412
|
+
const filters = {
|
|
413
|
+
enabled: parsed.query.enabled,
|
|
414
|
+
consecutiveErrorsMin: parsed.query.consecutiveErrorsMin,
|
|
415
|
+
lastRunStatus: parsed.query.lastRunStatus,
|
|
416
|
+
};
|
|
406
417
|
// List responses default to slim (no `definition`); `?fields=full` restores it.
|
|
407
418
|
if (parsed.query.fields === "full") {
|
|
408
|
-
json(res, withFavoriteFlags(listWorkflows(), { userId, itemType: "workflow" }));
|
|
419
|
+
json(res, withFavoriteFlags(listWorkflows(filters), { userId, itemType: "workflow" }));
|
|
409
420
|
} else {
|
|
410
421
|
json(
|
|
411
422
|
res,
|
|
412
|
-
withFavoriteFlags(listWorkflows(
|
|
423
|
+
withFavoriteFlags(listWorkflows(filters, { slim: true }), {
|
|
413
424
|
userId,
|
|
414
425
|
itemType: "workflow",
|
|
415
426
|
}),
|
|
@@ -58,8 +58,8 @@ export type BasePromptArgs = {
|
|
|
58
58
|
traits?: ProviderTraits;
|
|
59
59
|
/**
|
|
60
60
|
* Harness provider for this session. Gates provider-specific prompt blocks
|
|
61
|
-
* (e.g. the context-mode
|
|
62
|
-
* context-mode MCP wiring yet
|
|
61
|
+
* (e.g. the context-mode MCP tool list is excluded for `pi`, which has no
|
|
62
|
+
* context-mode MCP wiring yet).
|
|
63
63
|
*/
|
|
64
64
|
provider?: ProviderName;
|
|
65
65
|
name?: string;
|
|
@@ -102,10 +102,11 @@ export const getBasePrompt = async (args: BasePromptArgs): Promise<string> => {
|
|
|
102
102
|
} else if (role === "lead") {
|
|
103
103
|
compositeEventType = "system.session.lead";
|
|
104
104
|
} else if (args.provider === "pi") {
|
|
105
|
-
// Pi has no context-mode MCP wiring yet
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
// keep the block via the standard worker
|
|
105
|
+
// Pi has no context-mode MCP wiring yet, so it uses a worker composite that
|
|
106
|
+
// omits the context-mode tool list while still including the shared script
|
|
107
|
+
// rubric and seed-script guidance. All other local providers (claude,
|
|
108
|
+
// codex, opencode) keep the full context block via the standard worker
|
|
109
|
+
// composite.
|
|
109
110
|
compositeEventType = "system.session.worker.pi";
|
|
110
111
|
} else {
|
|
111
112
|
compositeEventType = "system.session.worker";
|
|
@@ -392,13 +392,9 @@ Use this to debug issues and propose improvements to your own infrastructure.
|
|
|
392
392
|
});
|
|
393
393
|
|
|
394
394
|
registerTemplate({
|
|
395
|
-
eventType: "system.agent.
|
|
395
|
+
eventType: "system.agent.script_rubric",
|
|
396
396
|
header: "",
|
|
397
397
|
defaultBody: `
|
|
398
|
-
### Context Window Management
|
|
399
|
-
|
|
400
|
-
You have access to the \`context-mode\` MCP tools (\`batch_execute\`, \`execute\`, \`execute_file\`, \`search\`, \`fetch_and_index\`, \`index\`) which compress tool output to save context window space. For data-heavy operations (web fetches, large file reads, CLI output processing), prefer these over raw Bash/WebFetch to avoid flooding your context window with raw output.
|
|
401
|
-
|
|
402
398
|
### Agent Scripts — for bulk, repetitive, or data-heavy work
|
|
403
399
|
|
|
404
400
|
Use **scripts** (\`script-upsert\` + \`script-run\`) when a task involves repetitive SDK calls, large data processing, or deterministic multi-step pipelines. Scripts run out-of-process and return only their final result.
|
|
@@ -407,19 +403,49 @@ Use **scripts** (\`script-upsert\` + \`script-run\`) when a task involves repeti
|
|
|
407
403
|
|
|
408
404
|
| Situation | Preferred approach |
|
|
409
405
|
|---|---|
|
|
410
|
-
| 1–10 SDK calls, result fits in context | Direct tool call |
|
|
406
|
+
| 1–10 SDK calls, result fits in context | Direct tool call. Do not script below the ~10-call threshold. |
|
|
411
407
|
| 10+ items, bulk/fan-out SDK ops | **Script** (\`script-run\` with inline source or named) |
|
|
412
408
|
| Heavy data (fetch + parse + transform) | **Script** or \`ctx_*\` (context-mode) |
|
|
413
409
|
| Single expensive web fetch | \`ctx_fetch_and_index\` (context-mode) |
|
|
414
410
|
| Multi-agent fan-out, parallel work, deterministic pipeline | **Workflow** |
|
|
415
411
|
| One-off bash/TS with no reuse needed | \`code-mode run\` (Bash) |
|
|
416
412
|
|
|
413
|
+
**Script persistence guardrail:** use a named script only when the logic will be invoked ≥2 times by you, another agent, or a workflow. For genuine one-offs, use inline \`script-run\` so the catalog does not accumulate scratch-* auto-saves.
|
|
414
|
+
|
|
415
|
+
**Worked example:** workflow triage that previously cost ~26 underlying calls can return one ~4.3k-token result in ~13s, a ~90-95% context reduction.
|
|
416
|
+
|
|
417
417
|
The 5 script tools (\`script-search\`, \`script-run\`, \`script-upsert\`, \`script-delete\`, \`script-query-types\`) are deferred tools. Call ToolSearch to load \`script-upsert\`, \`script-run\`, and \`script-query-types\` before using them.
|
|
418
418
|
|
|
419
419
|
**Key gotchas:**
|
|
420
420
|
- \`agentId\` IS propagated to scripts via the \`X-Agent-ID\` header.
|
|
421
421
|
- \`taskId\` is NOT propagated to scripts — there is no ambient task context. Pass \`taskId\` explicitly via \`args\` if the script needs to call \`ctx.swarm.task_storeProgress\`.
|
|
422
422
|
- Use \`script-query-types\` to inspect the live \`swarm-sdk.d.ts\` before authoring a complex script.
|
|
423
|
+
- Typed API connections: a lead-registered \`script_connections\` entry exposes a typed \`ctx.api.<slug>.<method>(...)\` client in scripts.
|
|
424
|
+
- \`ctx.api\` clients auto-inject the configured credential at egress, so scripts calling an allow-listed external API should prefer \`ctx.api\` over hand-written \`[REDACTED:<KEY>]\` Authorization headers.
|
|
425
|
+
- Registration is lead-only; workers should document the connection spec and hand it to the lead to register.
|
|
426
|
+
`,
|
|
427
|
+
variables: [],
|
|
428
|
+
category: "system",
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
registerTemplate({
|
|
432
|
+
eventType: "system.agent.context_mode",
|
|
433
|
+
header: "",
|
|
434
|
+
defaultBody: `
|
|
435
|
+
### Context Window Management
|
|
436
|
+
|
|
437
|
+
You have access to the \`context-mode\` MCP tools (\`batch_execute\`, \`execute\`, \`execute_file\`, \`search\`, \`fetch_and_index\`, \`index\`) which compress tool output to save context window space. For data-heavy operations (web fetches, large file reads, CLI output processing), prefer these over raw Bash/WebFetch to avoid flooding your context window with raw output.
|
|
438
|
+
|
|
439
|
+
{{@template[system.agent.script_rubric]}}
|
|
440
|
+
|
|
441
|
+
### Scheduling — Pick the Right targetType
|
|
442
|
+
|
|
443
|
+
When creating a schedule, match \`targetType\` to the work being fired:
|
|
444
|
+
- Use \`targetType: "workflow"\` with \`workflowId\` when the schedule's only job is to start a workflow.
|
|
445
|
+
- Use \`targetType: "script"\` with \`scriptName\` and optional \`scriptArgs\` when it only needs to run a catalog script.
|
|
446
|
+
- Use \`targetType: "agent-task"\` only when a reasoning agent genuinely needs to be in the loop for judgment, open-ended work, or tool orchestration that is not already captured by a workflow or script.
|
|
447
|
+
|
|
448
|
+
Do not create an \`agent-task\` schedule whose \`taskTemplate\` just says to trigger a workflow or script. Workflow/script targets dispatch directly; agent-task fields such as \`targetAgentId\`, \`model\`, \`taskTemplate\`, \`priority\`, and \`tags\` do not drive those direct runs, and workflow cooldowns still gate workflow targets.
|
|
423
449
|
`,
|
|
424
450
|
variables: [],
|
|
425
451
|
category: "system",
|
|
@@ -680,11 +706,10 @@ registerTemplate({
|
|
|
680
706
|
});
|
|
681
707
|
|
|
682
708
|
// Pi-specific worker composite. Identical to `system.session.worker` except it
|
|
683
|
-
//
|
|
684
|
-
// wiring yet
|
|
685
|
-
//
|
|
686
|
-
//
|
|
687
|
-
// keep the context_mode block via `system.session.worker`.
|
|
709
|
+
// omits only the `system.agent.context_mode` MCP-tool block — pi has no
|
|
710
|
+
// context-mode MCP wiring yet, so advertising the `ctx_*` tools would point at
|
|
711
|
+
// phantom tools. It still includes the shared script rubric and seed-script
|
|
712
|
+
// guidance so pi sessions get the same bulk-work decision policy (DES-514).
|
|
688
713
|
registerTemplate({
|
|
689
714
|
eventType: "system.session.worker.pi",
|
|
690
715
|
header: "",
|
|
@@ -694,6 +719,8 @@ registerTemplate({
|
|
|
694
719
|
{{@template[system.agent.worker]}}
|
|
695
720
|
{{@template[system.agent.filesystem]}}
|
|
696
721
|
{{@template[system.agent.self_awareness]}}
|
|
722
|
+
{{@template[system.agent.script_rubric]}}
|
|
723
|
+
{{@template[system.agent.seed_scripts]}}
|
|
697
724
|
|
|
698
725
|
{{@template[system.agent.system]}}
|
|
699
726
|
{{@template[system.agent.share_urls]}}
|
package/src/rbac/can.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central authorization engine (DES-445, slice 1).
|
|
3
|
+
*
|
|
4
|
+
* `can()` is pure and synchronous: no DB imports, no caching (correctness >
|
|
5
|
+
* performance — every call evaluates directly). It centralizes the DECISION;
|
|
6
|
+
* call sites keep their existing denial presentation (MCP soft failures,
|
|
7
|
+
* HTTP 403 bodies).
|
|
8
|
+
*
|
|
9
|
+
* Audit seam: `setAuditSink()` installs a fire-and-forget observer invoked
|
|
10
|
+
* (sync, exceptions swallowed) on every `can()` call — allow AND deny. The
|
|
11
|
+
* real batched writer is wired at server boot in increment 2 (Phase 6);
|
|
12
|
+
* until then the sink stays unset and `can()` still decides correctly.
|
|
13
|
+
*/
|
|
14
|
+
import { LEGACY_POLICY } from "./legacy-policy";
|
|
15
|
+
import type { RbacCheck, RbacDecision } from "./types";
|
|
16
|
+
|
|
17
|
+
export type AuditSink = (check: RbacCheck, decision: RbacDecision) => void;
|
|
18
|
+
|
|
19
|
+
let auditSink: AuditSink | null = null;
|
|
20
|
+
|
|
21
|
+
export function setAuditSink(fn: AuditSink): void {
|
|
22
|
+
auditSink = fn;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function clearAuditSink(): void {
|
|
26
|
+
auditSink = null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function can(check: RbacCheck): RbacDecision {
|
|
30
|
+
const rule = LEGACY_POLICY[check.verb];
|
|
31
|
+
const decision: RbacDecision = rule.evaluate(check.principal, check.resource)
|
|
32
|
+
? { allow: true }
|
|
33
|
+
: { allow: false, reason: rule.denyReason, missing: check.verb };
|
|
34
|
+
|
|
35
|
+
if (auditSink) {
|
|
36
|
+
try {
|
|
37
|
+
auditSink(check, decision);
|
|
38
|
+
} catch {
|
|
39
|
+
// The audit sink must never break or slow the request path.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return decision;
|
|
44
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public surface of the RBAC module (DES-445).
|
|
3
|
+
*
|
|
4
|
+
* Import from here at call sites: `import { can } from "@/rbac"`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type { AuditSink } from "./can";
|
|
8
|
+
export { can, clearAuditSink, setAuditSink } from "./can";
|
|
9
|
+
export type { LegacyRule } from "./legacy-policy";
|
|
10
|
+
export { LEGACY_POLICY, LEGACY_RULES } from "./legacy-policy";
|
|
11
|
+
export type { PermissionVerb } from "./permissions";
|
|
12
|
+
export { PERMISSION_VERBS, PERMISSIONS, PermissionVerbSchema } from "./permissions";
|
|
13
|
+
export type { RbacCheck, RbacDecision, RbacPrincipal, RbacResource } from "./types";
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in legacy policy (DES-445, slice 1).
|
|
3
|
+
*
|
|
4
|
+
* Data-driven rule table reproducing TODAY'S exact inline authorization rules
|
|
5
|
+
* (research doc §3 / plan Appendix A, verified against HEAD 2026-07-07).
|
|
6
|
+
* This IS `can()`'s disabled-mode policy — not allow-all. The role engine
|
|
7
|
+
* (increment 3) replaces the lookup behind the same `can()` signature.
|
|
8
|
+
*
|
|
9
|
+
* Every rule is a small pure predicate `(principal, resource) => boolean`.
|
|
10
|
+
* Compile-time exhaustiveness: `LEGACY_POLICY` must cover every
|
|
11
|
+
* `PermissionVerb` (enforced via `satisfies`).
|
|
12
|
+
*/
|
|
13
|
+
import type { PermissionVerb } from "./permissions";
|
|
14
|
+
import type { RbacPrincipal, RbacResource } from "./types";
|
|
15
|
+
|
|
16
|
+
export type LegacyRule = {
|
|
17
|
+
/** Stable rule identifier — surfaces in audit rows and debug output. */
|
|
18
|
+
name: string;
|
|
19
|
+
/** Human-readable reason attached to deny decisions. */
|
|
20
|
+
denyReason: string;
|
|
21
|
+
/** Pure predicate — true = allow. Never touches the DB. */
|
|
22
|
+
evaluate: (principal: RbacPrincipal, resource: RbacResource | undefined) => boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// ── Named rules (research §3 Rule column) ────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const leadOnly: LegacyRule = {
|
|
28
|
+
name: "lead-only",
|
|
29
|
+
denyReason: "requires lead agent",
|
|
30
|
+
evaluate: (principal) => principal.kind === "agent" && principal.isLead,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const leadOrTaskCreator: LegacyRule = {
|
|
34
|
+
name: "lead-or-task-creator",
|
|
35
|
+
denyReason: "requires lead agent or task creator",
|
|
36
|
+
evaluate: (principal, resource) => {
|
|
37
|
+
if (principal.kind !== "agent") return false;
|
|
38
|
+
if (principal.isLead) return true;
|
|
39
|
+
return (
|
|
40
|
+
resource?.kind === "task" &&
|
|
41
|
+
resource.creatorAgentId != null &&
|
|
42
|
+
resource.creatorAgentId === principal.agentId
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const leadOrResourceOwner: LegacyRule = {
|
|
48
|
+
name: "lead-or-resource-owner",
|
|
49
|
+
denyReason: "requires lead agent or resource owner",
|
|
50
|
+
evaluate: (principal, resource) => {
|
|
51
|
+
if (principal.kind !== "agent") return false;
|
|
52
|
+
if (principal.isLead) return true;
|
|
53
|
+
return (
|
|
54
|
+
resource?.kind === "owned" &&
|
|
55
|
+
resource.ownerAgentId != null &&
|
|
56
|
+
resource.ownerAgentId === principal.agentId
|
|
57
|
+
);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const leadOrOwnNamespace: LegacyRule = {
|
|
62
|
+
name: "lead-or-own-namespace",
|
|
63
|
+
denyReason: "requires lead agent or your own task:agent: namespace",
|
|
64
|
+
evaluate: (principal, resource) => {
|
|
65
|
+
if (principal.kind !== "agent") return false;
|
|
66
|
+
if (principal.isLead) return true;
|
|
67
|
+
// A blank agent id can never own a namespace — the pre-migration guards
|
|
68
|
+
// used truthiness (`if (info.agentId && ...)`), so `X-Agent-ID: ""` plus
|
|
69
|
+
// the literal namespace `task:agent:` must stay denied.
|
|
70
|
+
return (
|
|
71
|
+
principal.agentId !== "" &&
|
|
72
|
+
resource?.kind === "kv-namespace" &&
|
|
73
|
+
resource.namespace === `task:agent:${principal.agentId}`
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const anyAuthenticated: LegacyRule = {
|
|
79
|
+
name: "any-authenticated",
|
|
80
|
+
denyReason: "requires an authenticated principal",
|
|
81
|
+
// Reaching can() at all implies the request passed handleCore auth — every
|
|
82
|
+
// constructed principal counts as authenticated. Unused by any slice-1 verb;
|
|
83
|
+
// kept because Phase-1 additions and increment 3 need the rule kind.
|
|
84
|
+
evaluate: () => true,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const requesterOwnsTask: LegacyRule = {
|
|
88
|
+
name: "requester-owns-task",
|
|
89
|
+
denyReason: "not the task requester",
|
|
90
|
+
// Mirrors assertOwnsTask (src/tools/task-tool-ctx.ts): owner contexts
|
|
91
|
+
// (agent-side and operator calls) always pass; user principals must match
|
|
92
|
+
// the task's requestedByUserId.
|
|
93
|
+
evaluate: (principal, resource) => {
|
|
94
|
+
if (principal.kind !== "user") return true;
|
|
95
|
+
return resource?.kind === "task" && resource.requestedByUserId === principal.userId;
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// ── Composites (verified against HEAD) ───────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
/** memory.delete.any — owner OR (lead AND scope=swarm) (src/tools/memory-delete.ts:54-56). */
|
|
102
|
+
const memoryOwnerOrLeadSwarm: LegacyRule = {
|
|
103
|
+
name: "memory-owner-or-lead-swarm",
|
|
104
|
+
denyReason: "requires memory owner, or lead agent for swarm-scoped memories",
|
|
105
|
+
evaluate: (principal, resource) => {
|
|
106
|
+
if (principal.kind !== "agent" || resource?.kind !== "owned") return false;
|
|
107
|
+
if (resource.ownerAgentId != null && resource.ownerAgentId === principal.agentId) return true;
|
|
108
|
+
return principal.isLead && resource.scope === "swarm";
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* task.fs.mutate — operator OR user OR lead OR task-assignee OR task-creator
|
|
114
|
+
* (src/http/fs.ts canMutateTask). Order-independent OR: today's early
|
|
115
|
+
* operator/user returns short-circuit before agent identity, but no input
|
|
116
|
+
* satisfies one branch while failing another, so a flat OR is equivalent
|
|
117
|
+
* (plan Appendix A row 36 note).
|
|
118
|
+
*/
|
|
119
|
+
const taskFsMutate: LegacyRule = {
|
|
120
|
+
name: "operator-or-user-or-lead-or-task-owner",
|
|
121
|
+
denyReason: "requires operator, user, lead agent, task assignee, or task creator",
|
|
122
|
+
evaluate: (principal, resource) => {
|
|
123
|
+
if (principal.kind === "operator" || principal.kind === "user") return true;
|
|
124
|
+
if (principal.isLead) return true;
|
|
125
|
+
if (resource?.kind !== "task") return false;
|
|
126
|
+
return (
|
|
127
|
+
(resource.agentId != null && resource.agentId === principal.agentId) ||
|
|
128
|
+
(resource.creatorAgentId != null && resource.creatorAgentId === principal.agentId)
|
|
129
|
+
);
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/** All named (non-composite) rule kinds, keyed by identifier. */
|
|
134
|
+
export const LEGACY_RULES = {
|
|
135
|
+
"lead-only": leadOnly,
|
|
136
|
+
"lead-or-task-creator": leadOrTaskCreator,
|
|
137
|
+
"lead-or-resource-owner": leadOrResourceOwner,
|
|
138
|
+
"lead-or-own-namespace": leadOrOwnNamespace,
|
|
139
|
+
"any-authenticated": anyAuthenticated,
|
|
140
|
+
"requester-owns-task": requesterOwnsTask,
|
|
141
|
+
} as const;
|
|
142
|
+
|
|
143
|
+
// ── Verb → rule table ────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
export const LEGACY_POLICY = {
|
|
146
|
+
"user.manage": leadOnly,
|
|
147
|
+
"agent.profile.update.any": leadOnly,
|
|
148
|
+
"agent.context.read.any": leadOnly,
|
|
149
|
+
"task.cancel.any": leadOrTaskCreator,
|
|
150
|
+
"task.read.own": requesterOwnsTask,
|
|
151
|
+
"task.cancel.own": requesterOwnsTask,
|
|
152
|
+
"task.action.own": requesterOwnsTask,
|
|
153
|
+
"task.fs.mutate": taskFsMutate,
|
|
154
|
+
"memory.learning.inject": leadOnly,
|
|
155
|
+
"memory.delete.any": memoryOwnerOrLeadSwarm,
|
|
156
|
+
"channel.delete": leadOnly,
|
|
157
|
+
"integration.kapso.manage": leadOnly,
|
|
158
|
+
"integration.slack.post": leadOnly,
|
|
159
|
+
"integration.slack.read": leadOnly,
|
|
160
|
+
"integration.slack.thread.start": leadOnly,
|
|
161
|
+
"integration.slack.upload": leadOnly,
|
|
162
|
+
"integration.slack.delete": leadOnly,
|
|
163
|
+
"integration.slack.update": leadOnly,
|
|
164
|
+
"credential-binding.manage": leadOnly,
|
|
165
|
+
"script-connection.manage": leadOnly,
|
|
166
|
+
"config.credential-bindings.write": leadOnly,
|
|
167
|
+
"config.write.any": leadOnly,
|
|
168
|
+
"config.delete.any": leadOnly,
|
|
169
|
+
"config.read.secrets": leadOnly,
|
|
170
|
+
"skill.create.swarm": leadOnly,
|
|
171
|
+
"skill.install.any": leadOnly,
|
|
172
|
+
"skill.install.global": leadOnly,
|
|
173
|
+
"skill.uninstall.any": leadOnly,
|
|
174
|
+
"skill.update.any": leadOrResourceOwner,
|
|
175
|
+
"skill.promote.swarm": leadOnly,
|
|
176
|
+
"skill.delete.any": leadOrResourceOwner,
|
|
177
|
+
"mcp-server.create.swarm": leadOnly,
|
|
178
|
+
"mcp-server.install.any": leadOnly,
|
|
179
|
+
"mcp-server.uninstall.any": leadOnly,
|
|
180
|
+
"mcp-server.delete.any": leadOrResourceOwner,
|
|
181
|
+
"mcp-server.update.any": leadOrResourceOwner,
|
|
182
|
+
"kv.write.any": leadOrOwnNamespace,
|
|
183
|
+
"script.global.write": leadOnly,
|
|
184
|
+
"script.global.delete": leadOnly,
|
|
185
|
+
} as const satisfies Record<PermissionVerb, LegacyRule>;
|