mercury-agent 0.5.0 → 0.5.2
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 +452 -451
- package/container/Dockerfile +127 -127
- package/container/Dockerfile.base +109 -109
- package/container/Dockerfile.power +17 -17
- package/container/agent-package.json +8 -8
- package/container/build.sh +54 -54
- package/docs/ROADMAP.md +3 -4
- package/docs/archive/summarization/2026-07-02-recent-dev-summary.md +71 -0
- package/docs/auth/dashboard.md +28 -28
- package/docs/auth/overview.md +109 -109
- package/docs/auth/whatsapp.md +173 -173
- package/docs/authoring-profiles.md +174 -174
- package/docs/configuration.md +54 -54
- package/docs/container-lifecycle.md +349 -349
- package/docs/{bugs/bwrap-privileged-linux-docker.md → debug/major/2026-07-02-bwrap-privileged-linux-docker.md} +21 -1
- package/docs/{bugs/e2big-prompt-delivery-fix.md → debug/major/2026-07-02-e2big-prompt-delivery-fix.md} +27 -1
- package/docs/{bugs/registry-pull-no-local-fallback.md → debug/major/2026-07-02-registry-pull-no-local-fallback.md} +27 -1
- package/docs/debug/summarization/2026-07-02-common-bug-patterns.md +72 -0
- package/docs/deployment.md +199 -199
- package/docs/extensions.md +375 -375
- package/docs/graceful-shutdown.md +62 -62
- package/docs/kb-distillation.md +77 -77
- package/docs/media/overview.md +140 -140
- package/docs/media/whatsapp.md +171 -171
- package/docs/memory.md +137 -137
- package/docs/permissions.md +217 -217
- package/docs/pipeline.md +228 -228
- package/docs/prd-chat-memory.md +76 -76
- package/docs/prd-config-load.md +82 -82
- package/docs/rate-limiting.md +229 -229
- package/docs/runbooks/publish-checklist.md +9 -1
- package/docs/scheduler.md +288 -288
- package/docs/setup-discord.md +100 -100
- package/docs/setup-slack.md +119 -119
- package/docs/setup-whatsapp.md +94 -94
- package/docs/subagents.md +166 -166
- package/docs/web-search.md +62 -62
- package/examples/extensions/README.md +12 -12
- package/examples/extensions/charts/index.ts +13 -13
- package/examples/extensions/charts/skill/SKILL.md +98 -98
- package/examples/extensions/gws/README.md +52 -52
- package/examples/extensions/gws/skill/SKILL.md +57 -57
- package/examples/extensions/gws/skill/references/calendar.md +101 -101
- package/examples/extensions/gws/skill/references/docs.md +65 -65
- package/examples/extensions/gws/skill/references/drive.md +79 -79
- package/examples/extensions/gws/skill/references/gmail.md +85 -85
- package/examples/extensions/gws/skill/references/sheets.md +60 -60
- package/examples/extensions/napkin/skill/SKILL.md +728 -728
- package/examples/extensions/pdf/skill/LICENSE.txt +30 -30
- package/examples/extensions/pdf/skill/SKILL.md +314 -314
- package/examples/extensions/pdf/skill/forms.md +294 -294
- package/examples/extensions/pdf/skill/reference.md +611 -611
- package/examples/extensions/pdf/skill/scripts/check_bounding_boxes.py +65 -65
- package/examples/extensions/pdf/skill/scripts/check_fillable_fields.py +11 -11
- package/examples/extensions/pdf/skill/scripts/convert_pdf_to_images.py +33 -33
- package/examples/extensions/pdf/skill/scripts/create_validation_image.py +37 -37
- package/examples/extensions/pdf/skill/scripts/extract_form_field_info.py +122 -122
- package/examples/extensions/pdf/skill/scripts/extract_form_structure.py +115 -115
- package/examples/extensions/pdf/skill/scripts/fill_fillable_fields.py +98 -98
- package/examples/extensions/pdf/skill/scripts/fill_pdf_form_with_annotations.py +107 -107
- package/examples/extensions/permission-guard/index.ts +65 -65
- package/examples/extensions/pinchtab/skill/SKILL.md +224 -224
- package/examples/extensions/pinchtab/skill/TRUST.md +69 -69
- package/examples/extensions/pinchtab/skill/references/api.md +297 -297
- package/examples/extensions/pinchtab/skill/references/env.md +45 -45
- package/examples/extensions/pinchtab/skill/references/profiles.md +107 -107
- package/examples/extensions/tradestation/host/refresh.ts +102 -102
- package/examples/extensions/tradestation/index.ts +153 -153
- package/examples/extensions/tradestation/skill/SKILL.md +67 -67
- package/examples/extensions/voice-synth/index.ts +94 -94
- package/examples/extensions/voice-synth/skill/SKILL.md +38 -38
- package/examples/extensions/voice-transcribe/requirements.txt +8 -8
- package/examples/extensions/voice-transcribe/scripts/transcribe.py +179 -179
- package/examples/extensions/voice-transcribe/skill/SKILL.md +53 -53
- package/examples/extensions/yahoo-mail/cli/package.json +13 -13
- package/examples/extensions/yahoo-mail/skill/SKILL.md +78 -78
- package/package.json +106 -106
- package/resources/agents/explore.md +50 -50
- package/resources/agents/worker.md +24 -24
- package/resources/connection-env-vars.json +25 -25
- package/resources/pi-extensions/subagent/agents.ts +126 -126
- package/resources/pi-extensions/subagent/index.ts +964 -964
- package/resources/profiles/coding/AGENTS.md +43 -43
- package/resources/profiles/coding/mercury-profile.yaml +15 -15
- package/resources/profiles/general/AGENTS.md +31 -31
- package/resources/profiles/general/mercury-profile.yaml +15 -15
- package/resources/profiles/research/AGENTS.md +40 -40
- package/resources/profiles/research/mercury-profile.yaml +15 -15
- package/resources/skills/config/SKILL.md +25 -25
- package/resources/skills/context/SKILL.md +33 -33
- package/resources/skills/conversation-recap/SKILL.md +19 -19
- package/resources/skills/mutes/SKILL.md +31 -31
- package/resources/skills/permissions/SKILL.md +19 -19
- package/resources/skills/preferences/SKILL.md +31 -31
- package/resources/skills/recall/SKILL.md +24 -24
- package/resources/skills/roles/SKILL.md +18 -18
- package/resources/skills/spaces/SKILL.md +18 -18
- package/resources/skills/tasks/SKILL.md +45 -45
- package/resources/templates/AGENTS.md +157 -157
- package/resources/templates/env.template +38 -38
- package/resources/templates/mercury.example.yaml +99 -99
- package/src/agent/container-entry.ts +1 -1
- package/src/agent/container-runner.ts +1345 -1346
- package/src/cli/mercury.ts +39 -7
- package/src/cli/mrctl.ts +636 -636
- package/src/config-file.ts +540 -540
- package/src/config.ts +339 -339
- package/src/core/api.ts +125 -125
- package/src/core/caller-token.ts +101 -101
- package/src/core/permissions.ts +228 -228
- package/src/core/profiles.ts +271 -271
- package/src/core/routes/capability.ts +70 -70
- package/src/core/routes/index.ts +15 -15
- package/src/core/runtime.ts +1530 -1530
- package/src/dashboard/index.html +729 -729
- package/src/extensions/api.ts +273 -273
- package/src/extensions/loader.ts +286 -286
- package/src/extensions/types.ts +517 -517
- package/src/main.ts +605 -605
- package/docs/pending-updates/applicative-profiles.md +0 -15
- package/docs/pending-updates/caller-bound-capability-token.md +0 -15
- package/docs/pending-updates/dm-auto-space.md +0 -15
- /package/docs/archive/{2026-07-01-applicative-profiles.md → applicative-profiles/2026-07-01-applicative-profiles.md} +0 -0
- /package/docs/archive/{2026-07-01-caller-bound-capability-token.md → applicative-profiles/2026-07-01-caller-bound-capability-token.md} +0 -0
- /package/docs/archive/{2026-07-01-dm-auto-space.md → applicative-profiles/2026-07-01-dm-auto-space.md} +0 -0
package/src/core/permissions.ts
CHANGED
|
@@ -1,228 +1,228 @@
|
|
|
1
|
-
import type { Db } from "../storage/db.js";
|
|
2
|
-
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// Built-in permissions (static, cannot be overridden)
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
|
|
7
|
-
const BUILT_IN_PERMISSIONS = new Set([
|
|
8
|
-
"prompt",
|
|
9
|
-
"stop",
|
|
10
|
-
"compact",
|
|
11
|
-
"clear",
|
|
12
|
-
"tasks.list",
|
|
13
|
-
"tasks.create",
|
|
14
|
-
"tasks.pause",
|
|
15
|
-
"tasks.resume",
|
|
16
|
-
"tasks.delete",
|
|
17
|
-
"config.get",
|
|
18
|
-
"config.set",
|
|
19
|
-
"prefs.get",
|
|
20
|
-
"prefs.set",
|
|
21
|
-
"roles.list",
|
|
22
|
-
"roles.grant",
|
|
23
|
-
"roles.revoke",
|
|
24
|
-
"permissions.get",
|
|
25
|
-
"permissions.set",
|
|
26
|
-
"spaces.list",
|
|
27
|
-
"spaces.rename",
|
|
28
|
-
"spaces.delete",
|
|
29
|
-
/** Purge inbox/outbox media files. */
|
|
30
|
-
"media.purge",
|
|
31
|
-
/** Host Text-to-Speech (/api/tts); admin-only by default. */
|
|
32
|
-
"tts.synthesize",
|
|
33
|
-
/** Mute/unmute users and list mutes; admin-only by default. */
|
|
34
|
-
"mutes.list",
|
|
35
|
-
"mutes.mute",
|
|
36
|
-
"mutes.unmute",
|
|
37
|
-
]);
|
|
38
|
-
|
|
39
|
-
// ---------------------------------------------------------------------------
|
|
40
|
-
// Extension-registered permissions (dynamic, added at runtime)
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
|
|
43
|
-
const registeredPermissions = new Map<string, { defaultRoles: string[] }>();
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Register a new permission from an extension.
|
|
47
|
-
* Throws if the name collides with a built-in permission.
|
|
48
|
-
*/
|
|
49
|
-
export function registerPermission(
|
|
50
|
-
name: string,
|
|
51
|
-
opts: { defaultRoles: string[] },
|
|
52
|
-
): void {
|
|
53
|
-
if (BUILT_IN_PERMISSIONS.has(name)) {
|
|
54
|
-
throw new Error(
|
|
55
|
-
`Permission "${name}" is a built-in and cannot be overridden`,
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
registeredPermissions.set(name, opts);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Get all valid permission names (built-in + extension-registered).
|
|
63
|
-
*/
|
|
64
|
-
export function getAllPermissions(): string[] {
|
|
65
|
-
return [...BUILT_IN_PERMISSIONS, ...registeredPermissions.keys()];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Check if a permission name is valid (built-in or registered).
|
|
70
|
-
*/
|
|
71
|
-
export function isValidPermission(name: string): boolean {
|
|
72
|
-
return BUILT_IN_PERMISSIONS.has(name) || registeredPermissions.has(name);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Clear all registered extension permissions. For test isolation only.
|
|
77
|
-
*/
|
|
78
|
-
export function resetPermissions(): void {
|
|
79
|
-
registeredPermissions.clear();
|
|
80
|
-
activeProfileMemberPermissions = null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
// Active applicative profile (project-wide)
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Member permission set imposed by the active applicative profile, if any.
|
|
89
|
-
* Project-wide (one profile per deployment): set once at startup from the
|
|
90
|
-
* persisted profile activation. When non-null it is the EXHAUSTIVE member
|
|
91
|
-
* permission set — no extension defaults are merged — so raw capabilities stay
|
|
92
|
-
* admin-only unless the profile lists them. A per-space
|
|
93
|
-
* `role.member.permissions` override still takes precedence over this.
|
|
94
|
-
*/
|
|
95
|
-
let activeProfileMemberPermissions: string[] | null = null;
|
|
96
|
-
|
|
97
|
-
/** Set (or clear, with null) the active profile's member permission set. */
|
|
98
|
-
export function setActiveProfileMemberPermissions(
|
|
99
|
-
permissions: string[] | null,
|
|
100
|
-
): void {
|
|
101
|
-
activeProfileMemberPermissions = permissions;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Parse a permission list into a validated set (drops unknown names). */
|
|
105
|
-
function toPermissionSet(list: string[]): Set<string> {
|
|
106
|
-
return new Set(list.map((s) => s.trim()).filter((s) => isValidPermission(s)));
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ---------------------------------------------------------------------------
|
|
110
|
-
// Seeded groups tracking
|
|
111
|
-
// ---------------------------------------------------------------------------
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Tracks which groups have had admins seeded to avoid redundant DB calls.
|
|
115
|
-
* Exported for test isolation (tests should clear this in beforeEach).
|
|
116
|
-
*/
|
|
117
|
-
export const seededSpaces = new Set<string>();
|
|
118
|
-
|
|
119
|
-
// ---------------------------------------------------------------------------
|
|
120
|
-
// System callers
|
|
121
|
-
// ---------------------------------------------------------------------------
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* System callers — these identities get full permissions without DB lookup.
|
|
125
|
-
* Used for scheduled tasks, internal system calls, etc.
|
|
126
|
-
*/
|
|
127
|
-
const SYSTEM_CALLERS = new Set(["system"]);
|
|
128
|
-
|
|
129
|
-
export function isSystemCaller(callerId: string): boolean {
|
|
130
|
-
return SYSTEM_CALLERS.has(callerId);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// ---------------------------------------------------------------------------
|
|
134
|
-
// Default role permissions
|
|
135
|
-
// ---------------------------------------------------------------------------
|
|
136
|
-
|
|
137
|
-
/** Built-in defaults for the member role */
|
|
138
|
-
const DEFAULT_MEMBER_PERMISSIONS = new Set(["prompt", "prefs.get"]);
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Compute the default permission set for a role, merging built-in defaults
|
|
142
|
-
* with extension-registered defaults.
|
|
143
|
-
*
|
|
144
|
-
* - `admin` and `system` get all permissions (built-in + extension)
|
|
145
|
-
* - `member` gets `prompt`, `prefs.get`, plus any extension permissions that list "member" in defaultRoles
|
|
146
|
-
* - Other roles get extension permissions that list them in defaultRoles
|
|
147
|
-
*/
|
|
148
|
-
function getDefaultPermissions(role: string): Set<string> {
|
|
149
|
-
if (role === "admin" || role === "system") {
|
|
150
|
-
return new Set(getAllPermissions());
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const perms = new Set<string>(
|
|
154
|
-
role === "member" ? DEFAULT_MEMBER_PERMISSIONS : [],
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
for (const [name, opts] of registeredPermissions) {
|
|
158
|
-
if (opts.defaultRoles.includes(role)) {
|
|
159
|
-
perms.add(name);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return perms;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// ---------------------------------------------------------------------------
|
|
167
|
-
// Permission resolution
|
|
168
|
-
// ---------------------------------------------------------------------------
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Load the permission set for a role in a group.
|
|
172
|
-
* Checks group_config for "role.<name>.permissions" override,
|
|
173
|
-
* falls back to defaults (built-in + extension).
|
|
174
|
-
*/
|
|
175
|
-
export function getRolePermissions(
|
|
176
|
-
db: Db,
|
|
177
|
-
spaceId: string,
|
|
178
|
-
role: string,
|
|
179
|
-
): Set<string> {
|
|
180
|
-
if (role === "system") return getDefaultPermissions("system");
|
|
181
|
-
|
|
182
|
-
const key = `role.${role}.permissions`;
|
|
183
|
-
const stored = db.getSpaceConfig(spaceId, key);
|
|
184
|
-
|
|
185
|
-
// Explicit per-space override wins over everything.
|
|
186
|
-
if (stored !== null) {
|
|
187
|
-
return toPermissionSet(stored.split(","));
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// An active profile sets the exhaustive member permission set (project-wide),
|
|
191
|
-
// ahead of built-in/extension defaults. Members only; profiles never widen
|
|
192
|
-
// admin/system. Guard with Array.isArray so a malformed persisted activation
|
|
193
|
-
// (missing/non-array value) falls back to defaults instead of throwing here.
|
|
194
|
-
if (role === "member" && Array.isArray(activeProfileMemberPermissions)) {
|
|
195
|
-
return toPermissionSet(activeProfileMemberPermissions);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return getDefaultPermissions(role);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export function hasPermission(
|
|
202
|
-
db: Db,
|
|
203
|
-
spaceId: string,
|
|
204
|
-
role: string,
|
|
205
|
-
permission: string,
|
|
206
|
-
): boolean {
|
|
207
|
-
return getRolePermissions(db, spaceId, role).has(permission);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export function resolveRole(
|
|
211
|
-
db: Db,
|
|
212
|
-
spaceId: string,
|
|
213
|
-
platformUserId: string,
|
|
214
|
-
seededAdmins: string[],
|
|
215
|
-
displayName?: string | null,
|
|
216
|
-
): string {
|
|
217
|
-
// System callers bypass DB entirely
|
|
218
|
-
if (isSystemCaller(platformUserId)) return "system";
|
|
219
|
-
|
|
220
|
-
if (seededAdmins.length > 0 && !seededSpaces.has(spaceId)) {
|
|
221
|
-
db.seedAdmins(spaceId, seededAdmins);
|
|
222
|
-
seededSpaces.add(spaceId);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
db.upsertMember(spaceId, platformUserId, displayName);
|
|
226
|
-
|
|
227
|
-
return db.getRole(spaceId, platformUserId) ?? "member";
|
|
228
|
-
}
|
|
1
|
+
import type { Db } from "../storage/db.js";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Built-in permissions (static, cannot be overridden)
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
const BUILT_IN_PERMISSIONS = new Set([
|
|
8
|
+
"prompt",
|
|
9
|
+
"stop",
|
|
10
|
+
"compact",
|
|
11
|
+
"clear",
|
|
12
|
+
"tasks.list",
|
|
13
|
+
"tasks.create",
|
|
14
|
+
"tasks.pause",
|
|
15
|
+
"tasks.resume",
|
|
16
|
+
"tasks.delete",
|
|
17
|
+
"config.get",
|
|
18
|
+
"config.set",
|
|
19
|
+
"prefs.get",
|
|
20
|
+
"prefs.set",
|
|
21
|
+
"roles.list",
|
|
22
|
+
"roles.grant",
|
|
23
|
+
"roles.revoke",
|
|
24
|
+
"permissions.get",
|
|
25
|
+
"permissions.set",
|
|
26
|
+
"spaces.list",
|
|
27
|
+
"spaces.rename",
|
|
28
|
+
"spaces.delete",
|
|
29
|
+
/** Purge inbox/outbox media files. */
|
|
30
|
+
"media.purge",
|
|
31
|
+
/** Host Text-to-Speech (/api/tts); admin-only by default. */
|
|
32
|
+
"tts.synthesize",
|
|
33
|
+
/** Mute/unmute users and list mutes; admin-only by default. */
|
|
34
|
+
"mutes.list",
|
|
35
|
+
"mutes.mute",
|
|
36
|
+
"mutes.unmute",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Extension-registered permissions (dynamic, added at runtime)
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
const registeredPermissions = new Map<string, { defaultRoles: string[] }>();
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Register a new permission from an extension.
|
|
47
|
+
* Throws if the name collides with a built-in permission.
|
|
48
|
+
*/
|
|
49
|
+
export function registerPermission(
|
|
50
|
+
name: string,
|
|
51
|
+
opts: { defaultRoles: string[] },
|
|
52
|
+
): void {
|
|
53
|
+
if (BUILT_IN_PERMISSIONS.has(name)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Permission "${name}" is a built-in and cannot be overridden`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
registeredPermissions.set(name, opts);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get all valid permission names (built-in + extension-registered).
|
|
63
|
+
*/
|
|
64
|
+
export function getAllPermissions(): string[] {
|
|
65
|
+
return [...BUILT_IN_PERMISSIONS, ...registeredPermissions.keys()];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if a permission name is valid (built-in or registered).
|
|
70
|
+
*/
|
|
71
|
+
export function isValidPermission(name: string): boolean {
|
|
72
|
+
return BUILT_IN_PERMISSIONS.has(name) || registeredPermissions.has(name);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Clear all registered extension permissions. For test isolation only.
|
|
77
|
+
*/
|
|
78
|
+
export function resetPermissions(): void {
|
|
79
|
+
registeredPermissions.clear();
|
|
80
|
+
activeProfileMemberPermissions = null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Active applicative profile (project-wide)
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Member permission set imposed by the active applicative profile, if any.
|
|
89
|
+
* Project-wide (one profile per deployment): set once at startup from the
|
|
90
|
+
* persisted profile activation. When non-null it is the EXHAUSTIVE member
|
|
91
|
+
* permission set — no extension defaults are merged — so raw capabilities stay
|
|
92
|
+
* admin-only unless the profile lists them. A per-space
|
|
93
|
+
* `role.member.permissions` override still takes precedence over this.
|
|
94
|
+
*/
|
|
95
|
+
let activeProfileMemberPermissions: string[] | null = null;
|
|
96
|
+
|
|
97
|
+
/** Set (or clear, with null) the active profile's member permission set. */
|
|
98
|
+
export function setActiveProfileMemberPermissions(
|
|
99
|
+
permissions: string[] | null,
|
|
100
|
+
): void {
|
|
101
|
+
activeProfileMemberPermissions = permissions;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Parse a permission list into a validated set (drops unknown names). */
|
|
105
|
+
function toPermissionSet(list: string[]): Set<string> {
|
|
106
|
+
return new Set(list.map((s) => s.trim()).filter((s) => isValidPermission(s)));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Seeded groups tracking
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Tracks which groups have had admins seeded to avoid redundant DB calls.
|
|
115
|
+
* Exported for test isolation (tests should clear this in beforeEach).
|
|
116
|
+
*/
|
|
117
|
+
export const seededSpaces = new Set<string>();
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// System callers
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* System callers — these identities get full permissions without DB lookup.
|
|
125
|
+
* Used for scheduled tasks, internal system calls, etc.
|
|
126
|
+
*/
|
|
127
|
+
const SYSTEM_CALLERS = new Set(["system"]);
|
|
128
|
+
|
|
129
|
+
export function isSystemCaller(callerId: string): boolean {
|
|
130
|
+
return SYSTEM_CALLERS.has(callerId);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Default role permissions
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
/** Built-in defaults for the member role */
|
|
138
|
+
const DEFAULT_MEMBER_PERMISSIONS = new Set(["prompt", "prefs.get"]);
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Compute the default permission set for a role, merging built-in defaults
|
|
142
|
+
* with extension-registered defaults.
|
|
143
|
+
*
|
|
144
|
+
* - `admin` and `system` get all permissions (built-in + extension)
|
|
145
|
+
* - `member` gets `prompt`, `prefs.get`, plus any extension permissions that list "member" in defaultRoles
|
|
146
|
+
* - Other roles get extension permissions that list them in defaultRoles
|
|
147
|
+
*/
|
|
148
|
+
function getDefaultPermissions(role: string): Set<string> {
|
|
149
|
+
if (role === "admin" || role === "system") {
|
|
150
|
+
return new Set(getAllPermissions());
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const perms = new Set<string>(
|
|
154
|
+
role === "member" ? DEFAULT_MEMBER_PERMISSIONS : [],
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
for (const [name, opts] of registeredPermissions) {
|
|
158
|
+
if (opts.defaultRoles.includes(role)) {
|
|
159
|
+
perms.add(name);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return perms;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Permission resolution
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Load the permission set for a role in a group.
|
|
172
|
+
* Checks group_config for "role.<name>.permissions" override,
|
|
173
|
+
* falls back to defaults (built-in + extension).
|
|
174
|
+
*/
|
|
175
|
+
export function getRolePermissions(
|
|
176
|
+
db: Db,
|
|
177
|
+
spaceId: string,
|
|
178
|
+
role: string,
|
|
179
|
+
): Set<string> {
|
|
180
|
+
if (role === "system") return getDefaultPermissions("system");
|
|
181
|
+
|
|
182
|
+
const key = `role.${role}.permissions`;
|
|
183
|
+
const stored = db.getSpaceConfig(spaceId, key);
|
|
184
|
+
|
|
185
|
+
// Explicit per-space override wins over everything.
|
|
186
|
+
if (stored !== null) {
|
|
187
|
+
return toPermissionSet(stored.split(","));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// An active profile sets the exhaustive member permission set (project-wide),
|
|
191
|
+
// ahead of built-in/extension defaults. Members only; profiles never widen
|
|
192
|
+
// admin/system. Guard with Array.isArray so a malformed persisted activation
|
|
193
|
+
// (missing/non-array value) falls back to defaults instead of throwing here.
|
|
194
|
+
if (role === "member" && Array.isArray(activeProfileMemberPermissions)) {
|
|
195
|
+
return toPermissionSet(activeProfileMemberPermissions);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return getDefaultPermissions(role);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function hasPermission(
|
|
202
|
+
db: Db,
|
|
203
|
+
spaceId: string,
|
|
204
|
+
role: string,
|
|
205
|
+
permission: string,
|
|
206
|
+
): boolean {
|
|
207
|
+
return getRolePermissions(db, spaceId, role).has(permission);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function resolveRole(
|
|
211
|
+
db: Db,
|
|
212
|
+
spaceId: string,
|
|
213
|
+
platformUserId: string,
|
|
214
|
+
seededAdmins: string[],
|
|
215
|
+
displayName?: string | null,
|
|
216
|
+
): string {
|
|
217
|
+
// System callers bypass DB entirely
|
|
218
|
+
if (isSystemCaller(platformUserId)) return "system";
|
|
219
|
+
|
|
220
|
+
if (seededAdmins.length > 0 && !seededSpaces.has(spaceId)) {
|
|
221
|
+
db.seedAdmins(spaceId, seededAdmins);
|
|
222
|
+
seededSpaces.add(spaceId);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
db.upsertMember(spaceId, platformUserId, displayName);
|
|
226
|
+
|
|
227
|
+
return db.getRole(spaceId, platformUserId) ?? "member";
|
|
228
|
+
}
|