@nacre.work/api 0.33.0 → 0.34.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.
@@ -0,0 +1,724 @@
1
+ import { looksLikeEmail, McpToolRefusal, readSkillZip, withOrg, } from '@nacre.work/core';
2
+ import { AdminNames, isUuid } from './admin-names.js';
3
+ const auth = (call) => call.auth;
4
+ const text = (args, key, what, max = 200) => {
5
+ const value = args[key];
6
+ if (typeof value !== 'string' || value.trim() === '')
7
+ throw new McpToolRefusal(`'${key}' is required: ${what}.`);
8
+ if (value.length > max)
9
+ throw new McpToolRefusal(`'${key}' is at most ${String(max)} characters.`);
10
+ return value.trim();
11
+ };
12
+ const optionalText = (args, key, max) => {
13
+ const value = args[key];
14
+ if (value === undefined || value === null)
15
+ return undefined;
16
+ if (typeof value !== 'string')
17
+ throw new McpToolRefusal(`'${key}' must be a string.`);
18
+ if (value.length > max)
19
+ throw new McpToolRefusal(`'${key}' is at most ${String(max)} characters.`);
20
+ return value;
21
+ };
22
+ const oneOf = (args, key, allowed) => {
23
+ const value = args[key];
24
+ if (typeof value !== 'string' || !allowed.includes(value)) {
25
+ throw new McpToolRefusal(`'${key}' is one of ${allowed.join(', ')}.`);
26
+ }
27
+ return value;
28
+ };
29
+ const ids = (input, key) => {
30
+ const value = input[key];
31
+ if (typeof value !== 'string' || !isUuid(value))
32
+ throw new Error(`a stored proposal is missing ${key}`);
33
+ return value;
34
+ };
35
+ const PRINCIPAL_PROPS = {
36
+ person: { type: 'string', description: "A person's email address." },
37
+ group: { type: 'string', description: "A group's name." },
38
+ service_account: { type: 'string', description: "A service account's name." },
39
+ };
40
+ const LAYER_PROP = { type: 'string', description: "The layer's slug." };
41
+ const typeWord = (type) => type === 'user' ? 'person' : type === 'service_account' ? 'service account' : type;
42
+ export function coreAdminWrites(ports) {
43
+ const names = new AdminNames(ports.pool, ports.role);
44
+ const record = async (call, action, result, target, detail = {}) => {
45
+ const a = auth(call);
46
+ // Every call site names what it acted on, and this refuses one that does
47
+ // not rather than writing an event the `gin (target)` index cannot find —
48
+ // `lint:audit-target` reads `audit.write` literals and cannot see through a
49
+ // helper, so the property it holds is held here instead.
50
+ if (Object.keys(target).length === 0)
51
+ throw new Error(`${action.action} was recorded with no target`);
52
+ await ports.audit.write({
53
+ orgId: a.orgId,
54
+ actor: `${a.principal.type}:${a.principal.id}`,
55
+ ...action,
56
+ result,
57
+ surface: 'mcp-admin',
58
+ target: { ...target },
59
+ detail: { ...detail, ...(call.proposal === undefined ? {} : { proposal: call.proposal.id, through: call.proposal.through }) },
60
+ requestId: call.requestId,
61
+ });
62
+ };
63
+ /** The level a skill tool names: a layer by slug, or the organization's when none. */
64
+ const skillLevel = async (call, args) => {
65
+ if (typeof args.layer === 'string' && args.layer !== '') {
66
+ const layer = await names.layer(auth(call), args.layer);
67
+ return { level: { kind: 'layer', layerId: layer.id }, where: `the layer ${layer.slug}'s skill`, layer: layer.id };
68
+ }
69
+ return { level: { kind: 'organization' }, where: "the organization's skill" };
70
+ };
71
+ const storedLevel = (input) => typeof input.layer_id === 'string' ? { kind: 'layer', layerId: ids(input, 'layer_id') } : { kind: 'organization' };
72
+ const currentVersion = async (call, level) => (await ports.skills.current(auth(call), level))?.version ?? 0;
73
+ const skillOutcome = async (call, kind, level, outcome) => {
74
+ const target = level.kind === 'layer' ? { level: 'layer', layer_id: level.layerId } : { level: level.kind };
75
+ if (outcome.kind === 'written') {
76
+ await record(call, kind, 'allow', target, { version: outcome.version.version, has_scripts: outcome.version.hasScripts, surface: 'mcp-admin' });
77
+ return { version: outcome.version.version, cleared: outcome.cleared };
78
+ }
79
+ await record(call, kind, 'deny', target, { outcome: outcome.kind });
80
+ switch (outcome.kind) {
81
+ case 'conflict':
82
+ throw new McpToolRefusal(`The skill changed since this was proposed — it is at version ${String(outcome.current)} now. Read it again and propose afresh.`);
83
+ case 'refused':
84
+ throw new McpToolRefusal(outcome.reason);
85
+ default:
86
+ throw new McpToolRefusal('That skill is not one you may write.');
87
+ }
88
+ };
89
+ const SKILL = {
90
+ updated: { action: 'skill.updated' },
91
+ restored: { action: 'skill.restored' },
92
+ cleared: { action: 'skill.cleared' },
93
+ };
94
+ const proposal = (summary, details, input) => ({
95
+ summary,
96
+ details,
97
+ input,
98
+ });
99
+ return [
100
+ // ── grants ─────────────────────────────────────────────────────────────
101
+ {
102
+ kind: 'write',
103
+ name: 'issue_grant',
104
+ title: 'Grant access',
105
+ description: 'Propose giving a person, group or service account read, write or admin on one layer or workspace. ' +
106
+ 'Write does not include read; admin includes both. Nothing changes until the person approves it.',
107
+ inputSchema: {
108
+ type: 'object',
109
+ properties: {
110
+ ...PRINCIPAL_PROPS,
111
+ layer: LAYER_PROP,
112
+ workspace: { type: 'string', description: "The workspace's slug, to grant on every layer in it." },
113
+ permission: { type: 'string', enum: ['read', 'write', 'admin'] },
114
+ },
115
+ required: ['permission'],
116
+ additionalProperties: false,
117
+ },
118
+ async propose(call, args) {
119
+ const a = auth(call);
120
+ const subject = await names.principal(a, args);
121
+ if ((typeof args.layer === 'string') === (typeof args.workspace === 'string')) {
122
+ throw new McpToolRefusal('Name a layer or a workspace — exactly one.');
123
+ }
124
+ const scope = typeof args.layer === 'string'
125
+ ? { type: 'layer', ...(await names.layer(a, args.layer)) }
126
+ : { type: 'workspace', ...(await names.workspace(a, args.workspace)) };
127
+ const permission = oneOf(args, 'permission', ['read', 'write', 'admin']);
128
+ const named = (await names.names(a, [subject.id])).get(subject.id) ?? subject.id;
129
+ return proposal(`Give the ${typeWord(subject.type)} ${named} ${permission} on the ${scope.type} ${scope.slug}.`, [
130
+ { label: typeWord(subject.type), value: named },
131
+ { label: scope.type, value: scope.slug },
132
+ { label: 'permission', value: permission },
133
+ ...(permission === 'write'
134
+ ? [{ label: 'note', value: 'Write does not include read: they can add documents here and not search them.' }]
135
+ : permission === 'admin'
136
+ ? [{ label: 'note', value: 'Admin includes read and write, and lets them grant access here to others.' }]
137
+ : []),
138
+ ], { principal_type: subject.type, principal_id: subject.id, scope_type: scope.type, scope_id: scope.id, permission });
139
+ },
140
+ async apply(call, input) {
141
+ const grant = {
142
+ principalType: input.principal_type,
143
+ principalId: ids(input, 'principal_id'),
144
+ scopeType: input.scope_type,
145
+ scopeId: ids(input, 'scope_id'),
146
+ permission: input.permission,
147
+ };
148
+ const issued = await ports.grants.issue(auth(call), grant);
149
+ await record(call, { action: 'issue_grant' }, issued === undefined ? 'deny' : 'allow', {
150
+ principal: `${grant.principalType}:${grant.principalId}`,
151
+ scope: `${grant.scopeType}:${grant.scopeId}`,
152
+ permission: grant.permission,
153
+ });
154
+ if (issued === undefined)
155
+ throw new McpToolRefusal('That scope is not one you may administer, or it no longer exists.');
156
+ return { grant_id: issued.id };
157
+ },
158
+ },
159
+ {
160
+ kind: 'write',
161
+ name: 'revoke_grant',
162
+ title: 'Revoke a grant',
163
+ description: 'Propose withdrawing one grant, by the id list_grants gives. Takes effect on the next request once applied.',
164
+ inputSchema: {
165
+ type: 'object',
166
+ properties: { grant: { type: 'string', description: 'The grant id, from list_grants.' } },
167
+ required: ['grant'],
168
+ additionalProperties: false,
169
+ },
170
+ async propose(call, args) {
171
+ const a = auth(call);
172
+ const id = text(args, 'grant', 'the grant id from list_grants', 64);
173
+ const found = await names.grant(a, id);
174
+ if (found === undefined)
175
+ throw new McpToolRefusal(`No grant "${id}" in this organization.`);
176
+ const named = await names.names(a, [found.principalId, found.scopeId]);
177
+ const who = named.get(found.principalId) ?? found.principalId;
178
+ const where = named.get(found.scopeId) ?? found.scopeId;
179
+ return proposal(`Withdraw the ${typeWord(found.principalType)} ${who}'s ${found.effect === 'deny' ? `${found.permission} deny` : found.permission} on the ${found.scopeType} ${where}.`, [
180
+ { label: typeWord(found.principalType), value: who },
181
+ { label: found.scopeType, value: where },
182
+ { label: 'permission', value: found.permission },
183
+ { label: 'effect', value: found.effect },
184
+ ], { grant_id: id });
185
+ },
186
+ async apply(call, input) {
187
+ const id = ids(input, 'grant_id');
188
+ const revoked = await ports.grants.revoke(auth(call), id);
189
+ await record(call, { action: 'revoke_grant' }, revoked ? 'allow' : 'deny', { grant_id: id });
190
+ if (!revoked)
191
+ throw new McpToolRefusal('That grant is gone, or its scope is not one you may administer.');
192
+ return { revoked: true };
193
+ },
194
+ },
195
+ // ── groups ─────────────────────────────────────────────────────────────
196
+ {
197
+ kind: 'write',
198
+ name: 'create_group',
199
+ title: 'Create a group',
200
+ description: 'Propose a new group, empty, with no grants.',
201
+ inputSchema: {
202
+ type: 'object',
203
+ properties: { name: { type: 'string', description: 'The group name, unique in the organization.' } },
204
+ required: ['name'],
205
+ additionalProperties: false,
206
+ },
207
+ async propose(_call, args) {
208
+ const name = text(args, 'name', 'the group name', 100);
209
+ return proposal(`Create the group ${name}.`, [{ label: 'group', value: name }], { name });
210
+ },
211
+ async apply(call, input) {
212
+ const name = String(input.name);
213
+ const created = await ports.groups.create(auth(call), name);
214
+ await record(call, { action: 'create_group' }, created === undefined ? 'deny' : 'allow', { name });
215
+ if (created === undefined)
216
+ throw new McpToolRefusal(`A group named ${name} already exists.`);
217
+ return { group_id: created.id };
218
+ },
219
+ },
220
+ {
221
+ kind: 'write',
222
+ name: 'delete_group',
223
+ title: 'Delete a group',
224
+ description: "Propose deleting a group. Its members stay; the grants it holds go with it.",
225
+ inputSchema: {
226
+ type: 'object',
227
+ properties: { group: PRINCIPAL_PROPS.group },
228
+ required: ['group'],
229
+ additionalProperties: false,
230
+ },
231
+ async propose(call, args) {
232
+ const a = auth(call);
233
+ const group = await names.principal(a, { group: args.group }, ['group']);
234
+ const counts = await names.counts(a, { group: group.id });
235
+ const name = (await names.names(a, [group.id])).get(group.id) ?? group.id;
236
+ return proposal(`Delete the group ${name}. Its ${String(counts.members)} member${counts.members === 1 ? '' : 's'} stay; its ${String(counts.grants)} grant${counts.grants === 1 ? '' : 's'} go with it, and whoever reached something only through it stops reaching it.`, [
237
+ { label: 'group', value: name },
238
+ { label: 'members', value: String(counts.members) },
239
+ { label: 'grants removed', value: String(counts.grants) },
240
+ ], { group_id: group.id });
241
+ },
242
+ async apply(call, input) {
243
+ const id = ids(input, 'group_id');
244
+ const removed = await ports.groups.remove(auth(call), id);
245
+ await record(call, { action: 'delete_group' }, removed ? 'allow' : 'deny', { group_id: id });
246
+ if (!removed)
247
+ throw new McpToolRefusal('That group is gone.');
248
+ return { deleted: true };
249
+ },
250
+ },
251
+ {
252
+ kind: 'write',
253
+ name: 'add_group_member',
254
+ title: 'Add to a group',
255
+ description: 'Propose adding a person, or another group, to a group.',
256
+ inputSchema: {
257
+ type: 'object',
258
+ properties: {
259
+ group: PRINCIPAL_PROPS.group,
260
+ person: PRINCIPAL_PROPS.person,
261
+ member_group: { type: 'string', description: 'A group to nest inside this one, by name.' },
262
+ },
263
+ required: ['group'],
264
+ additionalProperties: false,
265
+ },
266
+ async propose(call, args) {
267
+ const a = auth(call);
268
+ const group = await names.principal(a, { group: args.group }, ['group']);
269
+ const member = await names.principal(a, { person: args.person, group: args.member_group }, ['person', 'group']);
270
+ const named = await names.names(a, [group.id, member.id]);
271
+ return proposal(`Add the ${typeWord(member.type)} ${named.get(member.id) ?? member.id} to the group ${named.get(group.id) ?? group.id}. They reach whatever the group reaches.`, [
272
+ { label: 'group', value: named.get(group.id) ?? group.id },
273
+ { label: typeWord(member.type), value: named.get(member.id) ?? member.id },
274
+ ], { group_id: group.id, member_type: member.type, member_id: member.id });
275
+ },
276
+ async apply(call, input) {
277
+ const groupId = ids(input, 'group_id');
278
+ const member = { type: input.member_type, id: ids(input, 'member_id') };
279
+ const outcome = await ports.groups.addMember(auth(call), groupId, member);
280
+ const ok = outcome === 'added' || outcome === 'already';
281
+ await record(call, { action: 'add_group_member' }, ok ? 'allow' : 'deny', { group_id: groupId, member: `${member.type}:${member.id}` }, { outcome });
282
+ if (outcome === 'no-group')
283
+ throw new McpToolRefusal('That group is gone.');
284
+ if (outcome === 'no-member')
285
+ throw new McpToolRefusal('That member is gone.');
286
+ return { added: outcome === 'added', already_a_member: outcome === 'already' };
287
+ },
288
+ },
289
+ {
290
+ kind: 'write',
291
+ name: 'remove_group_member',
292
+ title: 'Remove from a group',
293
+ description: 'Propose removing a person, or a nested group, from a group.',
294
+ inputSchema: {
295
+ type: 'object',
296
+ properties: {
297
+ group: PRINCIPAL_PROPS.group,
298
+ person: PRINCIPAL_PROPS.person,
299
+ member_group: { type: 'string', description: 'A nested group, by name.' },
300
+ },
301
+ required: ['group'],
302
+ additionalProperties: false,
303
+ },
304
+ async propose(call, args) {
305
+ const a = auth(call);
306
+ const group = await names.principal(a, { group: args.group }, ['group']);
307
+ const member = await names.principal(a, { person: args.person, group: args.member_group }, ['person', 'group']);
308
+ const named = await names.names(a, [group.id, member.id]);
309
+ return proposal(`Remove the ${typeWord(member.type)} ${named.get(member.id) ?? member.id} from the group ${named.get(group.id) ?? group.id}. Whatever they reached only through it stops at once.`, [
310
+ { label: 'group', value: named.get(group.id) ?? group.id },
311
+ { label: typeWord(member.type), value: named.get(member.id) ?? member.id },
312
+ ], { group_id: group.id, member_type: member.type, member_id: member.id });
313
+ },
314
+ async apply(call, input) {
315
+ const groupId = ids(input, 'group_id');
316
+ const member = { type: input.member_type, id: ids(input, 'member_id') };
317
+ const removed = await ports.groups.removeMember(auth(call), groupId, member);
318
+ await record(call, { action: 'remove_group_member' }, removed ? 'allow' : 'deny', { group_id: groupId, member: `${member.type}:${member.id}` });
319
+ if (!removed)
320
+ throw new McpToolRefusal('They are not in that group.');
321
+ return { removed: true };
322
+ },
323
+ },
324
+ // ── people ─────────────────────────────────────────────────────────────
325
+ {
326
+ kind: 'write',
327
+ name: 'create_person',
328
+ title: 'Add a person',
329
+ description: 'Propose an account for a person, as a member or an organization administrator. No password is handed out ' +
330
+ 'here: they set one with "Forgotten your password?" where mail is configured, or an administrator resets it ' +
331
+ 'in the console.',
332
+ inputSchema: {
333
+ type: 'object',
334
+ properties: {
335
+ email: { type: 'string', description: 'Their email address.' },
336
+ role: { type: 'string', enum: ['member', 'org_admin'], description: 'member by default.' },
337
+ },
338
+ required: ['email'],
339
+ additionalProperties: false,
340
+ },
341
+ async propose(_call, args) {
342
+ // Trimmed and otherwise kept as given, which is what `POST /v1/users`
343
+ // stores — two routes that normalise one address two ways are two
344
+ // answers about who `Dana@` is.
345
+ const email = text(args, 'email', 'their email address', 254).trim();
346
+ if (!looksLikeEmail(email))
347
+ throw new McpToolRefusal(`"${email}" is not an email address.`);
348
+ const role = args.role === undefined ? 'member' : oneOf(args, 'role', ['member', 'org_admin']);
349
+ return proposal(`Add ${email} as ${role === 'org_admin' ? 'an organization administrator' : 'a member'}.`, [
350
+ { label: 'email', value: email },
351
+ { label: 'role', value: role },
352
+ { label: 'password', value: 'none handed out — they set their own, or an administrator resets it in the console' },
353
+ ], { email, role });
354
+ },
355
+ async apply(call, input) {
356
+ const email = String(input.email);
357
+ const role = input.role === 'org_admin' ? 'org_admin' : 'member';
358
+ // The generated password is discarded, deliberately: anything returned
359
+ // here goes into a conversation, and conversations are stored.
360
+ const created = await ports.users.create(auth(call), email, role);
361
+ await record(call, { action: 'create_user' }, created === undefined ? 'deny' : 'allow', { email }, { role });
362
+ if (created === undefined)
363
+ throw new McpToolRefusal(`${email} already has an account here.`);
364
+ return { person_id: created.user.id, password: 'not handed out' };
365
+ },
366
+ },
367
+ ...['set_person_role', 'disable_person', 'enable_person'].map((name) => ({
368
+ kind: 'write',
369
+ name,
370
+ title: name === 'set_person_role' ? "Change a person's role" : name === 'disable_person' ? 'Disable a person' : 'Enable a person',
371
+ description: name === 'set_person_role'
372
+ ? 'Propose making a person a member or an organization administrator. The last administrator cannot be demoted.'
373
+ : name === 'disable_person'
374
+ ? 'Propose disabling a person: they cannot sign in, and every application acting for them stops on its next request. Their grants stay.'
375
+ : 'Propose enabling a disabled person again. Their grants and connections are as they were.',
376
+ inputSchema: {
377
+ type: 'object',
378
+ properties: {
379
+ person: PRINCIPAL_PROPS.person,
380
+ ...(name === 'set_person_role' ? { role: { type: 'string', enum: ['member', 'org_admin'] } } : {}),
381
+ },
382
+ required: name === 'set_person_role' ? ['person', 'role'] : ['person'],
383
+ additionalProperties: false,
384
+ },
385
+ async propose(call, args) {
386
+ const a = auth(call);
387
+ const person = await names.principal(a, { person: args.person }, ['person']);
388
+ const email = (await names.names(a, [person.id])).get(person.id) ?? person.id;
389
+ if (name === 'set_person_role') {
390
+ const role = oneOf(args, 'role', ['member', 'org_admin']);
391
+ return proposal(`Make ${email} ${role === 'org_admin' ? 'an organization administrator' : 'a member'}.`, [
392
+ { label: 'person', value: email },
393
+ { label: 'role', value: role },
394
+ ...(role === 'org_admin'
395
+ ? [{ label: 'note', value: 'An organization administrator reaches every layer and can change everything here.' }]
396
+ : []),
397
+ ], { person_id: person.id, role });
398
+ }
399
+ const disabling = name === 'disable_person';
400
+ return proposal(disabling
401
+ ? `Disable ${email}. They cannot sign in, and every application acting for them stops on its next request.`
402
+ : `Enable ${email} again.`, [{ label: 'person', value: email }, { label: 'state', value: disabling ? 'disabled' : 'enabled' }], { person_id: person.id, disabled: disabling });
403
+ },
404
+ async apply(call, input) {
405
+ const id = ids(input, 'person_id');
406
+ const change = name === 'set_person_role'
407
+ ? { role: input.role === 'org_admin' ? 'org_admin' : 'member' }
408
+ : { disabled: input.disabled === true };
409
+ const outcome = await ports.users.update(auth(call), id, change);
410
+ await record(call, name === 'set_person_role' ? { action: 'update_user' } : { action: 'disable_user' }, outcome === 'updated' ? 'allow' : 'deny', { user_id: id }, { ...change, outcome });
411
+ switch (outcome) {
412
+ case 'updated':
413
+ return { updated: true };
414
+ case 'last-admin':
415
+ throw new McpToolRefusal('That would leave the organization with no active administrator.');
416
+ case 'platform-admin':
417
+ throw new McpToolRefusal('That account administers the installation and is not changed from an organization.');
418
+ default:
419
+ throw new McpToolRefusal('That person is gone.');
420
+ }
421
+ },
422
+ })),
423
+ // ── workspaces and layers ──────────────────────────────────────────────
424
+ {
425
+ kind: 'write',
426
+ name: 'create_workspace',
427
+ title: 'Create a workspace',
428
+ description: 'Propose a workspace — a group of layers that can be granted together.',
429
+ inputSchema: {
430
+ type: 'object',
431
+ properties: {
432
+ slug: { type: 'string', description: 'Lower case, digits and hyphens.' },
433
+ name: { type: 'string' },
434
+ },
435
+ required: ['slug', 'name'],
436
+ additionalProperties: false,
437
+ },
438
+ async propose(_call, args) {
439
+ const slug = text(args, 'slug', 'the workspace slug', 63);
440
+ const name = text(args, 'name', 'the workspace name', 200);
441
+ return proposal(`Create the workspace ${slug} ("${name}").`, [{ label: 'slug', value: slug }, { label: 'name', value: name }], { slug, name });
442
+ },
443
+ async apply(call, input) {
444
+ const slug = String(input.slug);
445
+ const outcome = await ports.workspaces.create(auth(call), { slug, name: String(input.name) });
446
+ await record(call, { action: 'create_workspace' }, outcome.kind === 'created' ? 'allow' : 'deny', { slug }, { outcome: outcome.kind });
447
+ if (outcome.kind === 'conflict')
448
+ throw new McpToolRefusal(`A workspace with the slug ${slug} already exists.`);
449
+ if (outcome.kind !== 'created')
450
+ throw new McpToolRefusal('You may not create a workspace here.');
451
+ return { workspace_id: outcome.workspace.id };
452
+ },
453
+ },
454
+ {
455
+ kind: 'write',
456
+ name: 'create_layer',
457
+ title: 'Create a layer',
458
+ description: "Propose a layer in a workspace. Its embedding model is the organization's, or the provider named when " +
459
+ 'the organization runs more than one.',
460
+ inputSchema: {
461
+ type: 'object',
462
+ properties: {
463
+ workspace: { type: 'string', description: "The workspace's slug." },
464
+ slug: { type: 'string', description: 'Lower case, digits and hyphens. Clients address the layer by it, so it does not change later.' },
465
+ name: { type: 'string' },
466
+ provider: { type: 'string', description: 'An embedding provider, by name — only where the organization runs more than one.' },
467
+ },
468
+ required: ['workspace', 'slug', 'name'],
469
+ additionalProperties: false,
470
+ },
471
+ async propose(call, args) {
472
+ const a = auth(call);
473
+ const workspace = await names.workspace(a, text(args, 'workspace', "the workspace's slug", 63));
474
+ const slug = text(args, 'slug', 'the layer slug', 63);
475
+ const name = text(args, 'name', 'the layer name', 200);
476
+ let providerId;
477
+ let providerName;
478
+ if (typeof args.provider === 'string' && args.provider !== '') {
479
+ const found = await withOrg(ports.pool, a.orgId, async (client) => (await client.query(`SELECT id, name FROM embedding_providers
480
+ WHERE (org_id = $1 OR org_id IS NULL) AND ${isUuid(args.provider) ? 'id = $2::uuid' : 'name = $2'}
481
+ ORDER BY org_id NULLS LAST LIMIT 1`, [a.orgId, args.provider])).rows[0], { role: ports.role });
482
+ if (found === undefined)
483
+ throw new McpToolRefusal(`No embedding provider "${args.provider}" here.`);
484
+ providerId = found.id;
485
+ providerName = found.name;
486
+ }
487
+ return proposal(`Create the layer ${slug} ("${name}") in the workspace ${workspace.slug}.`, [
488
+ { label: 'workspace', value: workspace.slug },
489
+ { label: 'slug', value: slug },
490
+ { label: 'name', value: name },
491
+ ...(providerName === undefined ? [] : [{ label: 'provider', value: providerName }]),
492
+ ], { workspace_id: workspace.id, slug, name, ...(providerId === undefined ? {} : { provider_id: providerId }) });
493
+ },
494
+ async apply(call, input) {
495
+ const slug = String(input.slug);
496
+ const outcome = await ports.layers.create(auth(call), {
497
+ workspaceId: ids(input, 'workspace_id'),
498
+ slug,
499
+ name: String(input.name),
500
+ ...(typeof input.provider_id === 'string' ? { providerId: input.provider_id } : {}),
501
+ });
502
+ await record(call, { action: 'create_layer' }, outcome.kind === 'created' ? 'allow' : 'deny', { workspace_id: input.workspace_id, slug }, { outcome: outcome.kind });
503
+ switch (outcome.kind) {
504
+ case 'created':
505
+ return { layer_id: outcome.layer.id };
506
+ case 'conflict':
507
+ throw new McpToolRefusal(`A layer with the slug ${slug} already exists.`);
508
+ case 'provider':
509
+ throw new McpToolRefusal(outcome.detail);
510
+ default:
511
+ throw new McpToolRefusal('That workspace is not one you may administer, or it is gone.');
512
+ }
513
+ },
514
+ },
515
+ {
516
+ kind: 'write',
517
+ name: 'update_layer',
518
+ title: 'Rename a layer',
519
+ description: "Propose a layer's new name or description. The slug does not change — clients address the layer by it.",
520
+ inputSchema: {
521
+ type: 'object',
522
+ properties: { layer: LAYER_PROP, name: { type: 'string' }, description: { type: 'string' } },
523
+ required: ['layer'],
524
+ additionalProperties: false,
525
+ },
526
+ async propose(call, args) {
527
+ const layer = await names.layer(auth(call), text(args, 'layer', "the layer's slug", 63));
528
+ const name = optionalText(args, 'name', 200);
529
+ const description = optionalText(args, 'description', 2000);
530
+ if (name === undefined && description === undefined)
531
+ throw new McpToolRefusal("Give a new 'name', a new 'description', or both.");
532
+ return proposal(`Change the layer ${layer.slug}'s ${[name === undefined ? '' : 'name', description === undefined ? '' : 'description'].filter(Boolean).join(' and ')}.`, [
533
+ { label: 'layer', value: layer.slug },
534
+ ...(name === undefined ? [] : [{ label: 'name', value: name }]),
535
+ ...(description === undefined ? [] : [{ label: 'description', value: description }]),
536
+ ], { layer_id: layer.id, ...(name === undefined ? {} : { name }), ...(description === undefined ? {} : { description }) });
537
+ },
538
+ async apply(call, input) {
539
+ const id = ids(input, 'layer_id');
540
+ const updated = ports.layers.update === undefined
541
+ ? false
542
+ : await ports.layers.update(auth(call), id, {
543
+ ...(typeof input.name === 'string' ? { name: input.name } : {}),
544
+ ...(typeof input.description === 'string' ? { description: input.description } : {}),
545
+ });
546
+ await record(call, { action: 'update_layer' }, updated ? 'allow' : 'deny', { layer_id: id }, { layer_id: id });
547
+ if (!updated)
548
+ throw new McpToolRefusal('That layer is not one you may administer, or it is gone.');
549
+ return { updated: true };
550
+ },
551
+ },
552
+ {
553
+ kind: 'write',
554
+ name: 'delete_layer',
555
+ title: 'Delete a layer',
556
+ description: 'Propose deleting a layer and every document in it. Searches stop returning them the moment it is applied.',
557
+ inputSchema: {
558
+ type: 'object',
559
+ properties: { layer: LAYER_PROP },
560
+ required: ['layer'],
561
+ additionalProperties: false,
562
+ },
563
+ async propose(call, args) {
564
+ const a = auth(call);
565
+ const layer = await names.layer(a, text(args, 'layer', "the layer's slug", 63));
566
+ const counts = await names.counts(a, { layer: layer.id });
567
+ return proposal(`Delete the layer ${layer.slug} and its ${String(counts.documents)} document${counts.documents === 1 ? '' : 's'}. Searches stop returning them at once; the ${String(counts.grants)} grant${counts.grants === 1 ? '' : 's'} on it go too. This cannot be undone.`, [
568
+ { label: 'layer', value: layer.slug },
569
+ { label: 'documents', value: String(counts.documents) },
570
+ { label: 'grants removed', value: String(counts.grants) },
571
+ ], { layer_id: layer.id });
572
+ },
573
+ async apply(call, input) {
574
+ const id = ids(input, 'layer_id');
575
+ const removed = ports.layers.remove === undefined ? false : await ports.layers.remove(auth(call), id);
576
+ await record(call, { action: 'delete_layer' }, removed ? 'allow' : 'deny', { layer_id: id }, { layer_id: id });
577
+ if (!removed)
578
+ throw new McpToolRefusal('That layer is not one you may administer, or it is gone.');
579
+ return { deleted: true };
580
+ },
581
+ },
582
+ // ── skills ─────────────────────────────────────────────────────────────
583
+ {
584
+ kind: 'write',
585
+ name: 'write_skill',
586
+ title: 'Write a skill',
587
+ description: "Propose a new version of the organization's skill, or of a layer's. The files are a folder in Claude's skill " +
588
+ 'format, as files or as a base64 zip. Agents on the ordinary surface follow it; this surface never does.',
589
+ inputSchema: {
590
+ type: 'object',
591
+ properties: {
592
+ layer: { type: 'string', description: "A layer's slug; the organization's skill when absent." },
593
+ files: { type: 'object', additionalProperties: { type: 'string' }, description: 'Path to text, SKILL.md first.' },
594
+ zip_base64: { type: 'string', description: 'The folder as the .zip Claude exports, base64.' },
595
+ },
596
+ additionalProperties: false,
597
+ },
598
+ async propose(call, args) {
599
+ const { level, where, layer } = await skillLevel(call, args);
600
+ if ((args.files === undefined) === (args.zip_base64 === undefined)) {
601
+ throw new McpToolRefusal('Send the skill as files or as zip_base64 — exactly one.');
602
+ }
603
+ let files = args.files;
604
+ if (typeof args.zip_base64 === 'string') {
605
+ const read = readSkillZip(Buffer.from(args.zip_base64, 'base64'));
606
+ if ('error' in read)
607
+ throw new McpToolRefusal(`Not a skill zip: ${read.error}.`);
608
+ files = read.files;
609
+ }
610
+ if (files === null || typeof files !== 'object' || Object.values(files).some((v) => typeof v !== 'string')) {
611
+ throw new McpToolRefusal("'files' maps each path to its text.");
612
+ }
613
+ const paths = Object.keys(files).sort();
614
+ const based = await currentVersion(call, level);
615
+ return proposal(`Write ${where} as version ${String(based + 1)}: ${String(paths.length)} file${paths.length === 1 ? '' : 's'}${paths.some((p) => p.startsWith('scripts/')) ? ', including scripts' : ''}.`, [
616
+ { label: 'skill', value: where },
617
+ { label: 'version', value: `${String(based)} → ${String(based + 1)}` },
618
+ { label: 'files', value: paths.join(', ').slice(0, 900) },
619
+ { label: 'note', value: 'Written by an agent, and marked as such in the history.' },
620
+ ], { ...(layer === undefined ? {} : { layer_id: layer }), files, based_on: based });
621
+ },
622
+ async apply(call, input) {
623
+ const level = storedLevel(input);
624
+ const outcome = await ports.skills.write(auth(call), level, input.files, Number(input.based_on), 'mcp-admin');
625
+ return skillOutcome(call, outcome.kind === 'written' && outcome.cleared ? SKILL.cleared : SKILL.updated, level, outcome);
626
+ },
627
+ },
628
+ {
629
+ kind: 'write',
630
+ name: 'restore_skill',
631
+ title: 'Restore a skill version',
632
+ description: "Propose bringing back an earlier version of the organization's or a layer's skill, as a new version.",
633
+ inputSchema: {
634
+ type: 'object',
635
+ properties: {
636
+ layer: { type: 'string', description: "A layer's slug; the organization's skill when absent." },
637
+ version: { type: 'integer', minimum: 1 },
638
+ },
639
+ required: ['version'],
640
+ additionalProperties: false,
641
+ },
642
+ async propose(call, args) {
643
+ const { level, where, layer } = await skillLevel(call, args);
644
+ const n = typeof args.version === 'number' ? Math.trunc(args.version) : Number.NaN;
645
+ if (!Number.isInteger(n) || n < 1)
646
+ throw new McpToolRefusal("'version' is a version number from the skill's history.");
647
+ const old = await ports.skills.version(auth(call), level, n);
648
+ if (old === undefined)
649
+ throw new McpToolRefusal(`${where} has no version ${String(n)}.`);
650
+ const based = await currentVersion(call, level);
651
+ return proposal(`Bring back version ${String(n)} of ${where} as version ${String(based + 1)}.`, [
652
+ { label: 'skill', value: where },
653
+ { label: 'restoring', value: `version ${String(n)}${old.name === null ? ' (empty)' : ` — ${old.name}`}` },
654
+ { label: 'version', value: `${String(based)} → ${String(based + 1)}` },
655
+ ], { ...(layer === undefined ? {} : { layer_id: layer }), version: n, based_on: based });
656
+ },
657
+ async apply(call, input) {
658
+ const level = storedLevel(input);
659
+ const outcome = await ports.skills.restore(auth(call), level, Number(input.version), Number(input.based_on), 'mcp-admin');
660
+ return skillOutcome(call, SKILL.restored, level, outcome);
661
+ },
662
+ },
663
+ {
664
+ kind: 'write',
665
+ name: 'clear_skill',
666
+ title: 'Clear a skill',
667
+ description: "Propose clearing the organization's skill — agents fall back to the installation's — or a layer's, which " +
668
+ 'leaves the layer with none. The history stays.',
669
+ inputSchema: {
670
+ type: 'object',
671
+ properties: { layer: { type: 'string', description: "A layer's slug; the organization's skill when absent." } },
672
+ additionalProperties: false,
673
+ },
674
+ async propose(call, args) {
675
+ const { level, where, layer } = await skillLevel(call, args);
676
+ const based = await currentVersion(call, level);
677
+ if (based === 0)
678
+ throw new McpToolRefusal(`${where} is not set.`);
679
+ return proposal(`Clear ${where}. The history stays, and any version can be restored.`, [{ label: 'skill', value: where }, { label: 'version', value: `${String(based)} → ${String(based + 1)} (empty)` }], { ...(layer === undefined ? {} : { layer_id: layer }), based_on: based });
680
+ },
681
+ async apply(call, input) {
682
+ const level = storedLevel(input);
683
+ const outcome = await ports.skills.write(auth(call), level, {}, Number(input.based_on), 'mcp-admin');
684
+ return skillOutcome(call, SKILL.cleared, level, outcome);
685
+ },
686
+ },
687
+ // ── connections ────────────────────────────────────────────────────────
688
+ {
689
+ kind: 'write',
690
+ name: 'revoke_connection',
691
+ title: 'Revoke a connection',
692
+ description: 'Propose ending a connected application, by the id list_connections gives. It stops on its next request.',
693
+ inputSchema: {
694
+ type: 'object',
695
+ properties: { connection: { type: 'string', description: 'The connection id, from list_connections.' } },
696
+ required: ['connection'],
697
+ additionalProperties: false,
698
+ },
699
+ async propose(call, args) {
700
+ const a = auth(call);
701
+ const id = text(args, 'connection', 'the connection id from list_connections', 64).replace(/^connection:/, '');
702
+ const found = (await ports.consents.list(a)).find((c) => c.id === id && c.revokedAt === null);
703
+ if (found === undefined)
704
+ throw new McpToolRefusal(`No live connection "${id}" in this organization.`);
705
+ const who = found.subject.actsAs === 'user' ? (found.approvedByEmail ?? found.approvedBy) : (found.serviceAccountName ?? 'a service account');
706
+ return proposal(`End ${found.clientName ?? 'the application'}'s ${found.surface === 'admin' ? 'administrative ' : ''}connection, acting as ${who}. It stops on its next request.`, [
707
+ { label: 'application', value: found.clientName ?? id },
708
+ { label: 'acts as', value: who },
709
+ { label: 'surface', value: found.surface === 'admin' ? 'administrative MCP' : 'MCP and the API' },
710
+ ...(id === a.delegation?.id ? [{ label: 'note', value: 'This is the connection proposing it.' }] : []),
711
+ ], { connection_id: id });
712
+ },
713
+ async apply(call, input) {
714
+ const id = ids(input, 'connection_id');
715
+ const revoked = await ports.consents.revoke(auth(call), id, 'mcp-admin');
716
+ await record(call, { action: 'oauth.revoke' }, revoked ? 'allow' : 'deny', { connection_id: id });
717
+ if (!revoked)
718
+ throw new McpToolRefusal('That connection is already gone.');
719
+ return { revoked: true };
720
+ },
721
+ },
722
+ ];
723
+ }
724
+ //# sourceMappingURL=admin-writes.js.map