@sema-agent/client-core 0.111.0 → 0.112.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.
Files changed (106) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/README.md +15 -6
  3. package/dist/agentSession/engineSessionBackgroundStop.js +2 -2
  4. package/dist/checkpointRowVerdict.d.ts +3 -0
  5. package/dist/checkpointRowVerdict.js +14 -1
  6. package/dist/consoleCaPin.d.ts +136 -0
  7. package/dist/consoleCaPin.js +827 -0
  8. package/dist/decideReceipt.d.ts +2 -1
  9. package/dist/deviceRebind.js +3 -3
  10. package/dist/effectiveFacts.d.ts +9 -0
  11. package/dist/effectiveFacts.js +33 -0
  12. package/dist/engineAddress.d.ts +1 -0
  13. package/dist/engineAddress.js +4 -1
  14. package/dist/engineCredentialRelease.d.ts +54 -0
  15. package/dist/engineCredentialRelease.js +146 -0
  16. package/dist/engineErrorCodes.d.ts +1 -0
  17. package/dist/engineErrorCodes.js +1 -0
  18. package/dist/engineHealthFacts.d.ts +0 -11
  19. package/dist/engineHealthFacts.js +0 -21
  20. package/dist/engineHealthProbeVerdict.d.ts +1 -0
  21. package/dist/engineHealthProbeVerdict.js +21 -13
  22. package/dist/engineUnavailable.d.ts +3 -2
  23. package/dist/engineUnavailable.js +16 -12
  24. package/dist/engineUrl.js +4 -2
  25. package/dist/gateFailureCodes.js +2 -2
  26. package/dist/generated/sdkFactTables.d.ts +1 -0
  27. package/dist/generated/sdkFactTables.js +13 -0
  28. package/dist/generated/settingsSchemaFns.d.ts +1 -0
  29. package/dist/generated/settingsSchemaFns.js +3 -0
  30. package/dist/generated/settingsSchemaHooksClassifier.js +38 -9
  31. package/dist/generated/settingsSchemaTables.d.ts +52 -0
  32. package/dist/generated/settingsSchemaTables.js +18 -0
  33. package/dist/generated/settingsSchemaTypes.d.ts +23 -13
  34. package/dist/generated/settingsSchemaUrlShape.d.ts +1 -0
  35. package/dist/generated/settingsSchemaUrlShape.js +17 -0
  36. package/dist/hitl/decisionRefusal.d.ts +4 -0
  37. package/dist/hitl/decisionRefusal.js +12 -2
  38. package/dist/hitl/localAllowRedemption.d.ts +9 -6
  39. package/dist/hitl/localAllowRedemption.js +20 -0
  40. package/dist/hitl/parkOwnership.js +2 -1
  41. package/dist/hitl/parkResolver.js +7 -0
  42. package/dist/hitl/screenDecisionCards.js +3 -2
  43. package/dist/hitl/sessionPolicyWire.js +2 -2
  44. package/dist/hitl/standingRows.d.ts +4 -2
  45. package/dist/hitl/standingRows.js +10 -3
  46. package/dist/hooksReaderGenerations.js +1 -1
  47. package/dist/i18n/messages.en.d.ts +163 -0
  48. package/dist/i18n/messages.en.js +163 -0
  49. package/dist/i18n/messages.zh.js +174 -0
  50. package/dist/index.d.ts +30 -5
  51. package/dist/index.js +15 -2
  52. package/dist/liveModelCatalog.d.ts +4 -0
  53. package/dist/liveModelCatalog.js +10 -1
  54. package/dist/localConfig/documents.d.ts +61 -0
  55. package/dist/localConfig/documents.js +51 -0
  56. package/dist/localConfig/kernel.d.ts +149 -0
  57. package/dist/localConfig/kernel.js +597 -0
  58. package/dist/localConfig/mcp/edit.d.ts +161 -0
  59. package/dist/localConfig/mcp/edit.js +846 -0
  60. package/dist/localConfig/mcp/servers.d.ts +165 -0
  61. package/dist/localConfig/mcp/servers.js +776 -0
  62. package/dist/localConfig/mcp/views.d.ts +170 -0
  63. package/dist/localConfig/mcp/views.js +429 -0
  64. package/dist/localConfig/modelPool.d.ts +262 -0
  65. package/dist/localConfig/modelPool.js +1489 -0
  66. package/dist/localConfig/modelPoolWords.d.ts +13 -0
  67. package/dist/localConfig/modelPoolWords.js +20 -0
  68. package/dist/localConfig/receiptShape.d.ts +7 -0
  69. package/dist/localConfig/receiptShape.js +135 -0
  70. package/dist/localConfig/settingsWrite.d.ts +127 -0
  71. package/dist/localConfig/settingsWrite.js +237 -0
  72. package/dist/localConfig/skills.d.ts +124 -0
  73. package/dist/localConfig/skills.js +397 -0
  74. package/dist/localConfig/skillsWords.d.ts +10 -0
  75. package/dist/localConfig/skillsWords.js +5 -0
  76. package/dist/mcpProbeWire.js +2 -1
  77. package/dist/mcpWireCaps.js +1 -1
  78. package/dist/memoryVerbsWire.js +3 -2
  79. package/dist/model/onboardJudgement.d.ts +14 -4
  80. package/dist/model/onboardJudgement.js +91 -139
  81. package/dist/modelConfigGapNotice.d.ts +20 -0
  82. package/dist/modelConfigGapNotice.js +102 -0
  83. package/dist/modsKernelIdentity.js +2 -47
  84. package/dist/ownKey.d.ts +1 -0
  85. package/dist/ownKey.js +40 -8
  86. package/dist/permissionsDeclaration.d.ts +2 -1
  87. package/dist/permissionsDeclaration.js +31 -17
  88. package/dist/promptBlockedCopy.js +2 -2
  89. package/dist/resumeRefusalCopy.d.ts +3 -1
  90. package/dist/resumeRefusalCopy.js +12 -4
  91. package/dist/runLedgerSnapshot.js +3 -3
  92. package/dist/runPermissionModeSwitch.js +2 -3
  93. package/dist/seatContract.d.ts +154 -4
  94. package/dist/seatContract.js +228 -2
  95. package/dist/sessionIdSegment.d.ts +1 -0
  96. package/dist/sessionIdSegment.js +4 -1
  97. package/dist/sessionMemoryErase.js +2 -1
  98. package/dist/sha256Bytes.d.ts +1 -0
  99. package/dist/sha256Bytes.js +46 -0
  100. package/dist/skillOverrideChain.d.ts +83 -0
  101. package/dist/skillOverrideChain.js +378 -0
  102. package/dist/skillsWireCaps.d.ts +3 -0
  103. package/dist/skillsWireCaps.js +16 -1
  104. package/dist/turnFailurePresentation.js +4 -10
  105. package/docs/INTEGRATION-CLIENTS.md +614 -14
  106. package/package.json +3 -3
@@ -0,0 +1,846 @@
1
+ import { applyLocalConfigIntent, previewLocalConfigIntent, localConfigAccess, } from '../kernel.js';
2
+ import { mcpServerNameVerdict } from '../../mcpWireCaps.js';
3
+ import { readHostSettingsOnce } from '../../hostSettingsSource.js';
4
+ import { settingsWriteStep } from '../settingsWrite.js';
5
+ import { settingsCredentialKeyPaths } from '../../settingsCredentialKeyPaths.js';
6
+ import { MCP_SERVER_SCOPES, USER_SCOPE_PATH, PROJECT_FILE_PATH, localScopePath, judgeTable, ownData, stringListOf, subTableOf, documentOf, fileReadingOf, nameListOf, } from './servers.js';
7
+ export const MCP_SERVER_EDIT_OPS = Object.freeze(['add', 'update', 'remove', 'setEnabled', 'setProjectApproval', 'importFromSibling']);
8
+ const REFUSAL_REASON = Object.freeze({
9
+ edit_malformed: 'invalid_request',
10
+ name_unusable: 'name_unusable',
11
+ name_reserved: 'name_reserved',
12
+ mcp_entry_invalid: 'mcp_entry_invalid',
13
+ name_taken: 'already_exists',
14
+ not_found: 'not_found',
15
+ ambiguous_scope: 'ambiguous_scope',
16
+ scope_not_offered: 'scope_unwritable',
17
+ scope_unreadable: 'scope_unreadable',
18
+ policy_locked: 'policy_denied',
19
+ base_conflict: 'base_conflict',
20
+ credentials_need_reentry: 'credentials_need_reentry',
21
+ untrusted_folder: 'untrusted_folder',
22
+ });
23
+ export const refusalReasonOf = (w) => REFUSAL_REASON[w];
24
+ const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
25
+ const own = (v, k) => {
26
+ const r = ownData(v, k);
27
+ return r.ok ? r.value : undefined;
28
+ };
29
+ const def = (o, k, v) => {
30
+ Object.defineProperty(o, k, { value: v, writable: true, enumerable: true, configurable: true });
31
+ };
32
+ function copyLayer(v) {
33
+ const out = {};
34
+ if (!isRecord(v))
35
+ return out;
36
+ for (const k of Object.keys(v)) {
37
+ const d = Object.getOwnPropertyDescriptor(v, k);
38
+ if (d !== undefined)
39
+ Object.defineProperty(out, k, { ...d, enumerable: true, configurable: true });
40
+ }
41
+ return out;
42
+ }
43
+ export function withPath(doc, path, value) {
44
+ const root = copyLayer(doc);
45
+ let box = root;
46
+ for (let i = 0; i < path.length - 1; i++) {
47
+ const seg = path[i];
48
+ const r = ownData(box, seg);
49
+ if (!r.ok)
50
+ return undefined;
51
+ if (r.value !== undefined && !isRecord(r.value))
52
+ return undefined;
53
+ const next = copyLayer(r.value);
54
+ def(box, seg, next);
55
+ box = next;
56
+ }
57
+ const last = path[path.length - 1];
58
+ if (value === undefined)
59
+ delete box[last];
60
+ else
61
+ def(box, last, value);
62
+ return root;
63
+ }
64
+ function tableWith(table, name, value) {
65
+ const out = {};
66
+ let placed = false;
67
+ if (table !== undefined) {
68
+ for (const k of Object.keys(table)) {
69
+ if (k === name) {
70
+ placed = true;
71
+ if (value !== undefined)
72
+ def(out, k, value);
73
+ continue;
74
+ }
75
+ const d = Object.getOwnPropertyDescriptor(table, k);
76
+ if (d !== undefined)
77
+ Object.defineProperty(out, k, { ...d, enumerable: true, configurable: true });
78
+ }
79
+ }
80
+ if (!placed && value !== undefined)
81
+ def(out, name, value);
82
+ return out;
83
+ }
84
+ const docOf = (fresh, d) => documentOf(fresh[d]);
85
+ function placeOf(scope, projectKey) {
86
+ if (scope === 'user')
87
+ return { doc: 'globalConfig', path: USER_SCOPE_PATH };
88
+ if (scope === 'project')
89
+ return { doc: 'projectMcp', path: PROJECT_FILE_PATH };
90
+ return projectKey === undefined ? undefined : { doc: 'globalConfig', path: localScopePath(projectKey) };
91
+ }
92
+ export const mcpRowBaseOf = (revision) => (revision === null ? null : `rev:${revision}`);
93
+ const ADD_KEYS = Object.freeze(['op', 'name', 'entry', 'scope', 'onNameTaken']);
94
+ const UPDATE_KEYS = Object.freeze(['op', 'name', 'scope', 'base', 'set', 'unset', 'env', 'headers', 'keep']);
95
+ const REMOVE_KEYS = Object.freeze(['op', 'name', 'scope']);
96
+ const ENABLE_KEYS = Object.freeze(['op', 'name', 'enabled']);
97
+ const APPROVAL_KEYS = Object.freeze(['op', 'name', 'approval']);
98
+ const IMPORT_KEYS = Object.freeze(['op', 'entries', 'scope']);
99
+ const isScope = (v) => MCP_SERVER_SCOPES.some((s) => s === v);
100
+ function plainData(v, depth = 0) {
101
+ if (depth > 16)
102
+ return false;
103
+ if (v === null || typeof v === 'string' || typeof v === 'boolean' || (typeof v === 'number' && Number.isFinite(v)))
104
+ return true;
105
+ if (typeof v !== 'object')
106
+ return false;
107
+ try {
108
+ if (!Array.isArray(v)) {
109
+ const p = Object.getPrototypeOf(v);
110
+ if (p !== null && Object.getPrototypeOf(p) !== null)
111
+ return false;
112
+ }
113
+ for (const k of Reflect.ownKeys(v)) {
114
+ if (typeof k !== 'string')
115
+ return false;
116
+ const d = Object.getOwnPropertyDescriptor(v, k);
117
+ if (d === undefined)
118
+ continue;
119
+ if (Array.isArray(v) && k === 'length')
120
+ continue;
121
+ if (!Object.hasOwn(d, 'value') || d.enumerable !== true || !plainData(d.value, depth + 1))
122
+ return false;
123
+ }
124
+ return true;
125
+ }
126
+ catch {
127
+ return false;
128
+ }
129
+ }
130
+ function secretMapOk(v) {
131
+ if (v === undefined)
132
+ return true;
133
+ if (!isRecord(v) || !plainData(v) || Object.keys(v).some((k) => k !== 'set' && k !== 'delete'))
134
+ return false;
135
+ const set = own(v, 'set');
136
+ if (set !== undefined && (!isRecord(set) || Object.keys(set).some((k) => typeof own(set, k) !== 'string')))
137
+ return false;
138
+ const del = own(v, 'delete');
139
+ return del === undefined || stringListOf(del) !== undefined;
140
+ }
141
+ export function shapeMcpServerEdit(v) {
142
+ try {
143
+ if (!isRecord(v) || !plainData(v))
144
+ return { ok: false, path: null };
145
+ const op = own(v, 'op');
146
+ const keysOk = (allowed) => Object.keys(v).find((k) => !allowed.includes(k));
147
+ const name = own(v, 'name');
148
+ const nameOk = typeof name === 'string';
149
+ switch (op) {
150
+ case 'add': {
151
+ const extra = keysOk(ADD_KEYS);
152
+ if (extra !== undefined)
153
+ return { ok: false, path: extra };
154
+ if (!nameOk)
155
+ return { ok: false, path: 'name' };
156
+ if (!isRecord(own(v, 'entry')))
157
+ return { ok: false, path: 'entry' };
158
+ if (!isScope(own(v, 'scope')))
159
+ return { ok: false, path: 'scope' };
160
+ const t = own(v, 'onNameTaken');
161
+ if (t !== undefined && t !== 'refuse' && t !== 'replace' && t !== 'rename')
162
+ return { ok: false, path: 'onNameTaken' };
163
+ return { ok: true, edit: v };
164
+ }
165
+ case 'update': {
166
+ const extra = keysOk(UPDATE_KEYS);
167
+ if (extra !== undefined)
168
+ return { ok: false, path: extra };
169
+ if (!nameOk)
170
+ return { ok: false, path: 'name' };
171
+ if (!isScope(own(v, 'scope')))
172
+ return { ok: false, path: 'scope' };
173
+ if (typeof own(v, 'base') !== 'string' || own(v, 'base') === '')
174
+ return { ok: false, path: 'base' };
175
+ const set = own(v, 'set');
176
+ if (set !== undefined && (!isRecord(set) || Object.keys(set).some((k) => k === 'env' || k === 'headers')))
177
+ return { ok: false, path: 'set' };
178
+ const unset = own(v, 'unset');
179
+ const unsetList = unset === undefined ? [] : stringListOf(unset);
180
+ if (unsetList === undefined)
181
+ return { ok: false, path: 'unset' };
182
+ if (!secretMapOk(own(v, 'env')))
183
+ return { ok: false, path: 'env' };
184
+ if (!secretMapOk(own(v, 'headers')))
185
+ return { ok: false, path: 'headers' };
186
+ if (unsetList.some((k) => (isRecord(set) && Object.hasOwn(set, k)) || ((k === 'env' || k === 'headers') && own(v, k) !== undefined)))
187
+ return { ok: false, path: 'unset' };
188
+ const keep = own(v, 'keep');
189
+ if (keep !== undefined && (!isRecord(keep) || typeof own(keep, 'base') !== 'string' || stringListOf(own(keep, 'members')) === undefined || Object.keys(keep).some((k) => k !== 'base' && k !== 'members')))
190
+ return { ok: false, path: 'keep' };
191
+ return { ok: true, edit: v };
192
+ }
193
+ case 'remove': {
194
+ const extra = keysOk(REMOVE_KEYS);
195
+ if (extra !== undefined)
196
+ return { ok: false, path: extra };
197
+ if (!nameOk)
198
+ return { ok: false, path: 'name' };
199
+ const scope = own(v, 'scope');
200
+ if (scope !== undefined && !isScope(scope))
201
+ return { ok: false, path: 'scope' };
202
+ return { ok: true, edit: v };
203
+ }
204
+ case 'setEnabled': {
205
+ const extra = keysOk(ENABLE_KEYS);
206
+ if (extra !== undefined)
207
+ return { ok: false, path: extra };
208
+ if (!nameOk)
209
+ return { ok: false, path: 'name' };
210
+ if (typeof own(v, 'enabled') !== 'boolean')
211
+ return { ok: false, path: 'enabled' };
212
+ return { ok: true, edit: v };
213
+ }
214
+ case 'setProjectApproval': {
215
+ const extra = keysOk(APPROVAL_KEYS);
216
+ if (extra !== undefined)
217
+ return { ok: false, path: extra };
218
+ if (!nameOk)
219
+ return { ok: false, path: 'name' };
220
+ const a = own(v, 'approval');
221
+ if (a !== 'approve' && a !== 'reject' && a !== 'clear')
222
+ return { ok: false, path: 'approval' };
223
+ return { ok: true, edit: v };
224
+ }
225
+ case 'importFromSibling': {
226
+ const extra = keysOk(IMPORT_KEYS);
227
+ if (extra !== undefined)
228
+ return { ok: false, path: extra };
229
+ const entries = own(v, 'entries');
230
+ if (!isRecord(entries) || Object.keys(entries).length === 0 || Object.keys(entries).length > 256)
231
+ return { ok: false, path: 'entries' };
232
+ if (!isScope(own(v, 'scope')))
233
+ return { ok: false, path: 'scope' };
234
+ return { ok: true, edit: v };
235
+ }
236
+ default:
237
+ return { ok: false, path: 'op' };
238
+ }
239
+ }
240
+ catch {
241
+ return { ok: false, path: null };
242
+ }
243
+ }
244
+ const refusedOutcome = (detail, notices = [], plan) => Object.freeze({
245
+ receipt: Object.freeze({ kind: 'refused', reason: refusalReasonOf(detail.refusal) }),
246
+ refusal: Object.freeze(detail),
247
+ ...(plan === undefined ? {} : { plan }),
248
+ notices: Object.freeze([...notices]),
249
+ });
250
+ function nameRefusal(name, siblings, reserved) {
251
+ if (reserved.has(name))
252
+ return { refusal: 'name_reserved', by: 'host' };
253
+ const v = mcpServerNameVerdict(name, siblings);
254
+ if (v.usable)
255
+ return undefined;
256
+ return v.reason === 'name_reserved' ? { refusal: 'name_reserved', by: 'package' } : { refusal: 'name_unusable', nameReason: v.reason };
257
+ }
258
+ function entryVerdict(name, entry, host, lang) {
259
+ const table = {};
260
+ def(table, name, entry);
261
+ const judged = judgeTable(table, host.env, { expand: true, reserved: new Set(), ...(lang === undefined ? {} : { lang }) });
262
+ const one = judged?.[0];
263
+ if (one === undefined)
264
+ return { ok: false, problems: Object.freeze([]) };
265
+ if (one.spec !== undefined)
266
+ return { ok: true, spec: one.spec, missing: one.missing };
267
+ const nonName = one.problems.filter((p) => !['name_blank', 'name_edge_whitespace', 'name_collides_after_trim', 'name_too_long', 'name_reserved'].includes(p.reason));
268
+ if (one.missing.length > 0 && nonName.length > 0 && nonName.every((p) => p.reason === 'transport_invalid'))
269
+ return { ok: true, spec: undefined, missing: one.missing };
270
+ return { ok: false, problems: nonName.length > 0 ? nonName : one.problems };
271
+ }
272
+ const NAME_N_MAX = 99;
273
+ function policyLockOf(ctx) {
274
+ const ent = ctx.enterprise;
275
+ if (isRecord(ent) && (own(ent, 'status') === 'loaded' || own(ent, 'status') === 'unreadable'))
276
+ return 'enterprise_config';
277
+ const s = ctx.settings;
278
+ if (s === undefined || s === 'none')
279
+ return undefined;
280
+ try {
281
+ const read = readHostSettingsOnce(s, 'applyMcpServerEdit');
282
+ const doc = read.layers.policySettings.doc;
283
+ const v = doc === null ? undefined : own(doc, 'strictPluginOnlyCustomization');
284
+ return v === true || (stringListOf(v) ?? []).includes('mcp') ? 'plugin_only' : undefined;
285
+ }
286
+ catch {
287
+ return undefined;
288
+ }
289
+ }
290
+ function approvalTrust(ctx) {
291
+ const s = ctx.settings;
292
+ if (s === undefined || s === 'none')
293
+ return 'unreadable';
294
+ try {
295
+ return readHostSettingsOnce(s, 'applyMcpServerEdit').trust;
296
+ }
297
+ catch {
298
+ return 'unreadable';
299
+ }
300
+ }
301
+ const targetOf = (e) => JSON.stringify([own(e, 'type') ?? null, own(e, 'command') ?? null, own(e, 'args') ?? null, own(e, 'url') ?? null, own(e, 'headersHelper') ?? null]);
302
+ function maskedArgIndices(args) {
303
+ const list = stringListOf(args);
304
+ if (list === undefined)
305
+ return [];
306
+ const out = [];
307
+ for (const f of settingsCredentialKeyPaths({ args: list })) {
308
+ const m = /^args\[(\d+)\]$/.exec(f.path);
309
+ if (m !== null)
310
+ out.push(Number(m[1]));
311
+ }
312
+ return out;
313
+ }
314
+ function updatedEntry(old, edit, base) {
315
+ const next = copyLayer(old);
316
+ const set = edit.set === undefined ? undefined : edit.set;
317
+ if (set !== undefined)
318
+ for (const k of Object.keys(set))
319
+ def(next, k, own(set, k));
320
+ const unset = stringListOf(edit.unset) ?? [];
321
+ for (const k of unset)
322
+ delete next[k];
323
+ const carried = { env: [], headers: [], args: [] };
324
+ for (const key of ['env', 'headers']) {
325
+ if (unset.includes(key))
326
+ continue;
327
+ const m = edit[key];
328
+ const oldMap = own(old, key);
329
+ if (m === undefined && !isRecord(oldMap))
330
+ continue;
331
+ const map = copyLayer(oldMap);
332
+ const setM = m?.set;
333
+ if (setM !== undefined)
334
+ for (const mk of Object.keys(setM))
335
+ def(map, mk, own(setM, mk));
336
+ for (const mk of stringListOf(m?.delete) ?? [])
337
+ delete map[mk];
338
+ if (isRecord(oldMap))
339
+ for (const mk of Object.keys(oldMap))
340
+ if (Object.hasOwn(map, mk) && (setM === undefined || !Object.hasOwn(setM, mk)))
341
+ carried[key].push(mk);
342
+ if (m !== undefined || isRecord(oldMap))
343
+ def(next, key, map);
344
+ }
345
+ if (!unset.includes('args') && (set === undefined || !Object.hasOwn(set, 'args')))
346
+ carried.args.push(...maskedArgIndices(own(old, 'args')));
347
+ if (targetOf(old) !== targetOf(next) && carried.env.length + carried.headers.length + carried.args.length > 0) {
348
+ const want = [...carried.env.map((n) => `env:${n}`), ...carried.headers.map((n) => `headers:${n}`), ...carried.args.map((i) => `args:${i}`)].sort();
349
+ const keep = edit.keep;
350
+ const got = keep === undefined ? undefined : [...(stringListOf(keep.members) ?? [])].sort();
351
+ if (keep !== undefined && keep.base !== base)
352
+ return { ok: false, detail: { refusal: 'base_conflict' } };
353
+ if (got === undefined || JSON.stringify(got) !== JSON.stringify(want))
354
+ return { ok: false, detail: { refusal: 'credentials_need_reentry', env: Object.freeze(carried.env), headers: Object.freeze(carried.headers), args: Object.freeze(carried.args) } };
355
+ }
356
+ return { ok: true, entry: next };
357
+ }
358
+ function editFactsOf(ctx) {
359
+ const c = isRecord(ctx) ? ctx : { lane: {} };
360
+ const host = isRecord(c.host) ? c.host : {};
361
+ return {
362
+ c,
363
+ host,
364
+ reserved: new Set(stringListOf(own(host, 'reservedNames')) ?? []),
365
+ projectKey: typeof c.projectKey === 'string' && c.projectKey !== '' ? c.projectKey : undefined,
366
+ lang: c.lang,
367
+ };
368
+ }
369
+ function scopeGateOf(scope, f) {
370
+ if (scope === 'local' && f.projectKey === undefined)
371
+ return { refusal: 'scope_not_offered', scope, why: 'no_project_key' };
372
+ if (scope === 'project' && own(f.host, 'projectConfigRootSet') === true)
373
+ return { refusal: 'scope_not_offered', scope, why: 'project_config_root' };
374
+ return undefined;
375
+ }
376
+ function preGatesOf(e, f) {
377
+ if (e.op === 'add' || e.op === 'update' || e.op === 'importFromSibling') {
378
+ const lock = policyLockOf(f.c);
379
+ if (lock !== undefined)
380
+ return { ok: false, detail: { refusal: 'policy_locked', by: lock } };
381
+ const gate = scopeGateOf(e.scope, f);
382
+ if (gate !== undefined)
383
+ return { ok: false, detail: gate };
384
+ }
385
+ if (e.op === 'setEnabled' && f.projectKey === undefined)
386
+ return { ok: false, detail: { refusal: 'scope_not_offered', scope: 'local', why: 'no_project_key' } };
387
+ if (e.op === 'add') {
388
+ const nr = nameRefusal(e.name, [], f.reserved);
389
+ if (nr !== undefined)
390
+ return { ok: false, detail: nr };
391
+ const ev = entryVerdict(e.name, e.entry, f.host, f.lang);
392
+ if (!ev.ok)
393
+ return { ok: false, detail: { refusal: 'mcp_entry_invalid', problems: ev.problems } };
394
+ return { ok: true, entry: { spec: ev.spec, missing: ev.missing } };
395
+ }
396
+ if (e.op === 'importFromSibling') {
397
+ const good = [];
398
+ const skipped = [];
399
+ const problems = [];
400
+ for (const name of Object.keys(e.entries)) {
401
+ const raw = own(e.entries, name);
402
+ const nr = nameRefusal(name, [], f.reserved);
403
+ if (nr !== undefined) {
404
+ skipped.push({ name, reason: nr.refusal });
405
+ continue;
406
+ }
407
+ const ev = entryVerdict(name, raw, f.host, f.lang);
408
+ if (!ev.ok) {
409
+ skipped.push({ name, reason: 'mcp_entry_invalid' });
410
+ problems.push(...ev.problems);
411
+ continue;
412
+ }
413
+ good.push({ name, entry: copyLayer(raw) });
414
+ }
415
+ if (good.length === 0)
416
+ return { ok: false, detail: { refusal: 'mcp_entry_invalid', problems: Object.freeze(problems) }, plan: { op: 'importFromSibling', name: '', scope: e.scope, imported: Object.freeze([]), skipped: Object.freeze(skipped) } };
417
+ return { ok: true, good, skipped };
418
+ }
419
+ return { ok: true };
420
+ }
421
+ const entryProbeOf = (spec) => (spec !== undefined ? { kind: 'spec', spec } : { kind: 'none', why: 'env_missing' });
422
+ function fixedProbeOf(e) {
423
+ if (e.op === 'remove')
424
+ return { kind: 'none', why: 'removed' };
425
+ if (e.op === 'setEnabled')
426
+ return { kind: 'none', why: e.enabled ? 'not_active' : 'removed' };
427
+ return { kind: 'none', why: 'not_active' };
428
+ }
429
+ function updateVerdictOn(r, path, e, f) {
430
+ const cur = documentOf(r);
431
+ const t = subTableOf(cur, path);
432
+ if (t.kind === 'bad')
433
+ return { ok: false, detail: { refusal: 'scope_unreadable' } };
434
+ const old = t.kind === 'table' ? own(t.table, e.name) : undefined;
435
+ if (!isRecord(old))
436
+ return { ok: false, detail: { refusal: 'not_found' } };
437
+ const base = mcpRowBaseOf(r !== undefined && r.status === 'loaded' ? r.revision : null);
438
+ if (base === null || base !== e.base)
439
+ return { ok: false, detail: { refusal: 'base_conflict' } };
440
+ const u = updatedEntry(old, e, base);
441
+ if (!u.ok)
442
+ return { ok: false, detail: u.detail };
443
+ const ev = entryVerdict(e.name, u.entry, f.host, f.lang);
444
+ if (!ev.ok)
445
+ return { ok: false, detail: { refusal: 'mcp_entry_invalid', problems: ev.problems } };
446
+ return { ok: true, cur, table: t.kind === 'table' ? t.table : undefined, entry: u.entry, probe: entryProbeOf(ev.spec) };
447
+ }
448
+ async function snapshotOrUndefined(store) {
449
+ try {
450
+ const raw = await store.read(Object.freeze(['globalConfig', 'projectMcp']));
451
+ const holder = isRecord(raw) ? raw : {};
452
+ return Object.freeze({ globalConfig: fileReadingOf(holder, 'globalConfig', 'unreadable'), projectMcp: fileReadingOf(holder, 'projectMcp', 'unreadable') });
453
+ }
454
+ catch {
455
+ return undefined;
456
+ }
457
+ }
458
+ async function noticesFor(name, scope, entry, store, ctx, missing) {
459
+ const out = [];
460
+ const fresh = await snapshotOrUndefined(store);
461
+ if (fresh !== undefined) {
462
+ const rank = { user: 0, project: 1, local: 2 };
463
+ for (const s of MCP_SERVER_SCOPES) {
464
+ if (s === scope)
465
+ continue;
466
+ const place = placeOf(s, ctx.projectKey);
467
+ if (place === undefined)
468
+ continue;
469
+ const t = subTableOf(docOf(fresh, place.doc), place.path);
470
+ if (t.kind !== 'table')
471
+ continue;
472
+ const other = own(t.table, name);
473
+ if (other === undefined)
474
+ continue;
475
+ if (rank[s] > rank[scope])
476
+ out.push({ kind: 'shadowed_by', scope: s });
477
+ if (JSON.stringify([own(other, 'command') ?? null, own(other, 'url') ?? null]) !== JSON.stringify([own(entry, 'command') ?? null, own(entry, 'url') ?? null]))
478
+ out.push({ kind: 'endpoint_differs', scope: s });
479
+ }
480
+ }
481
+ const dep = ctx.host === undefined ? undefined : stringListOf(own(ctx.host, 'deploymentNames'));
482
+ if (dep !== undefined && dep.includes(name))
483
+ out.push({ kind: 'deployment_wins' });
484
+ if (missing.length > 0)
485
+ out.push({ kind: 'env_missing', names: Object.freeze([...missing]) });
486
+ return out;
487
+ }
488
+ export async function applyMcpServerEdit(edit, store, ctx, opts) {
489
+ try {
490
+ const shaped = shapeMcpServerEdit(edit);
491
+ if (!shaped.ok)
492
+ return refusedOutcome({ refusal: 'edit_malformed', path: shaped.path });
493
+ const e = shaped.edit;
494
+ const f = editFactsOf(ctx);
495
+ const { c, host, reserved, projectKey } = f;
496
+ const dry = isRecord(opts) && opts.dryRun === true;
497
+ const docsFor = () => {
498
+ if (e.op === 'setEnabled')
499
+ return ['globalConfig'];
500
+ if (e.op === 'setProjectApproval')
501
+ return ['localSettings'];
502
+ if (e.op === 'remove' && e.scope === undefined)
503
+ return ['globalConfig', 'projectMcp'];
504
+ if (e.op === 'add' && e.onNameTaken === 'rename')
505
+ return ['globalConfig', 'projectMcp'];
506
+ if (e.op === 'importFromSibling')
507
+ return ['globalConfig', 'projectMcp'];
508
+ return [e.scope === 'project' ? 'projectMcp' : 'globalConfig'];
509
+ };
510
+ const access = localConfigAccess(c.lane, { access: 'write', docs: docsFor() });
511
+ if (!access.ok)
512
+ return Object.freeze({ receipt: Object.freeze({ kind: 'unavailable', cause: access.cause }), notices: Object.freeze([]) });
513
+ const pre = preGatesOf(e, f);
514
+ if (!pre.ok)
515
+ return refusedOutcome(pre.detail, [], pre.plan);
516
+ const box = {};
517
+ const refuse = (d) => {
518
+ box.captured = d;
519
+ return { kind: 'refuse', reason: refusalReasonOf(d.refusal) };
520
+ };
521
+ let step;
522
+ let notices = [];
523
+ if (e.op === 'add') {
524
+ const ev = pre.entry;
525
+ notices = await noticesFor(e.name, e.scope, e.entry, store, c, ev.missing);
526
+ box.probe = entryProbeOf(ev.spec);
527
+ const place = placeOf(e.scope, projectKey);
528
+ const otherDoc = place.doc === 'globalConfig' ? 'projectMcp' : 'globalConfig';
529
+ const mode = e.onNameTaken ?? 'refuse';
530
+ const entryCopy = copyLayer(e.entry);
531
+ step = {
532
+ doc: place.doc,
533
+ role: 'effect',
534
+ ...(mode === 'rename' ? { lock: [otherDoc] } : {}),
535
+ plan: ({ fresh }) => {
536
+ const cur = docOf(fresh, place.doc);
537
+ const t = subTableOf(cur, place.path);
538
+ if (t.kind === 'bad')
539
+ return refuse({ refusal: 'scope_unreadable' });
540
+ const table = t.kind === 'table' ? t.table : undefined;
541
+ const siblings = table === undefined ? [] : Object.keys(table).filter((k) => k !== e.name);
542
+ const sib = nameRefusal(e.name, siblings, reserved);
543
+ if (sib !== undefined)
544
+ return refuse(sib);
545
+ let name = e.name;
546
+ let replaced = false;
547
+ if (table !== undefined && Object.hasOwn(table, e.name)) {
548
+ if (mode === 'refuse')
549
+ return refuse({ refusal: 'name_taken', scope: e.scope, exhausted: false });
550
+ if (mode === 'replace')
551
+ replaced = true;
552
+ else {
553
+ const taken = new Set();
554
+ for (const s of MCP_SERVER_SCOPES) {
555
+ const p = placeOf(s, projectKey);
556
+ if (p === undefined)
557
+ continue;
558
+ const tt = subTableOf(docOf(fresh, p.doc), p.path);
559
+ if (tt.kind === 'table')
560
+ for (const k of Object.keys(tt.table))
561
+ taken.add(k);
562
+ }
563
+ let found;
564
+ for (let n = 1; n <= NAME_N_MAX && found === undefined; n++) {
565
+ const cand = `${e.name}_${n}`;
566
+ if (!taken.has(cand) && nameRefusal(cand, siblings, reserved) === undefined)
567
+ found = cand;
568
+ }
569
+ if (found === undefined)
570
+ return refuse({ refusal: 'name_taken', scope: e.scope, exhausted: true });
571
+ name = found;
572
+ }
573
+ }
574
+ const next = withPath(cur, place.path, tableWith(table, name, entryCopy));
575
+ if (next === undefined)
576
+ return refuse({ refusal: 'scope_unreadable' });
577
+ box.plan = { op: 'add', name, scope: e.scope, ...(name !== e.name ? { renamedFrom: e.name } : {}), ...(replaced ? { replaced: true } : {}) };
578
+ return { kind: 'write', next };
579
+ },
580
+ };
581
+ }
582
+ else if (e.op === 'update') {
583
+ const place = placeOf(e.scope, projectKey);
584
+ step = {
585
+ doc: place.doc,
586
+ role: 'effect',
587
+ plan: ({ fresh }) => {
588
+ const v = updateVerdictOn(fresh[place.doc], place.path, e, f);
589
+ if (!v.ok)
590
+ return refuse(v.detail);
591
+ box.probe = v.probe;
592
+ const next = withPath(v.cur, place.path, tableWith(v.table, e.name, v.entry));
593
+ if (next === undefined)
594
+ return refuse({ refusal: 'scope_unreadable' });
595
+ box.plan = { op: 'update', name: e.name, scope: e.scope };
596
+ return { kind: 'write', next };
597
+ },
598
+ };
599
+ }
600
+ else if (e.op === 'remove') {
601
+ box.probe = fixedProbeOf(e);
602
+ const candidatesIn = (fresh) => {
603
+ const scopes = [];
604
+ let unreadable = false;
605
+ for (const s of ['local', 'project', 'user']) {
606
+ const p = placeOf(s, projectKey);
607
+ if (p === undefined)
608
+ continue;
609
+ const t = subTableOf(docOf(fresh, p.doc), p.path);
610
+ if (t.kind === 'bad')
611
+ unreadable = true;
612
+ else if (t.kind === 'table' && Object.hasOwn(t.table, e.name))
613
+ scopes.push(s);
614
+ }
615
+ return { scopes, unreadable };
616
+ };
617
+ let scope = e.scope;
618
+ if (scope === undefined) {
619
+ const snap = await snapshotOrUndefined(store);
620
+ if (snap === undefined)
621
+ return refusedOutcome({ refusal: 'scope_unreadable' });
622
+ const cand = candidatesIn(snap);
623
+ if (cand.unreadable)
624
+ return refusedOutcome({ refusal: 'scope_unreadable' });
625
+ if (cand.scopes.length === 0)
626
+ return refusedOutcome({ refusal: 'not_found' });
627
+ if (cand.scopes.length > 1)
628
+ return refusedOutcome({ refusal: 'ambiguous_scope', scopes: Object.freeze(cand.scopes) });
629
+ scope = cand.scopes[0];
630
+ }
631
+ const target = scope;
632
+ const gate = scopeGateOf(target, f);
633
+ if (gate !== undefined)
634
+ return refusedOutcome(gate);
635
+ const place = placeOf(target, projectKey);
636
+ const unscoped = e.scope === undefined;
637
+ step = {
638
+ doc: place.doc,
639
+ role: 'effect',
640
+ ...(unscoped ? { lock: [place.doc === 'globalConfig' ? 'projectMcp' : 'globalConfig'] } : {}),
641
+ plan: ({ fresh }) => {
642
+ if (unscoped) {
643
+ const cand = candidatesIn(fresh);
644
+ if (cand.unreadable)
645
+ return refuse({ refusal: 'scope_unreadable' });
646
+ if (cand.scopes.length > 1)
647
+ return refuse({ refusal: 'ambiguous_scope', scopes: Object.freeze(cand.scopes) });
648
+ if (cand.scopes.length === 0)
649
+ return refuse({ refusal: 'not_found' });
650
+ const inLock = scopeGateOf(cand.scopes[0], f);
651
+ if (inLock !== undefined)
652
+ return refuse(inLock);
653
+ if (cand.scopes[0] !== target)
654
+ return refuse({ refusal: 'base_conflict' });
655
+ }
656
+ const cur = docOf(fresh, place.doc);
657
+ const t = subTableOf(cur, place.path);
658
+ if (t.kind === 'bad')
659
+ return refuse({ refusal: 'scope_unreadable' });
660
+ if (t.kind !== 'table' || !Object.hasOwn(t.table, e.name))
661
+ return refuse({ refusal: 'not_found' });
662
+ const next = withPath(cur, place.path, tableWith(t.table, e.name, undefined));
663
+ if (next === undefined)
664
+ return refuse({ refusal: 'scope_unreadable' });
665
+ box.plan = { op: 'remove', name: e.name, scope: target };
666
+ return { kind: 'write', next };
667
+ },
668
+ };
669
+ }
670
+ else if (e.op === 'setEnabled') {
671
+ const defaultOff = new Set(stringListOf(own(host, 'defaultOffNames')) ?? []);
672
+ box.probe = fixedProbeOf(e);
673
+ const projPath = ['projects', projectKey];
674
+ step = {
675
+ doc: 'globalConfig',
676
+ role: 'effect',
677
+ plan: ({ fresh }) => {
678
+ const cur = docOf(fresh, 'globalConfig');
679
+ const t = subTableOf(cur, projPath);
680
+ if (t.kind === 'bad')
681
+ return refuse({ refusal: 'scope_unreadable' });
682
+ const entry = t.kind === 'table' ? t.table : undefined;
683
+ const listAt = (k) => {
684
+ const r = nameListOf(entry, k);
685
+ return r.kind === 'absent' ? [] : r.kind === 'list' ? [...r.list] : undefined;
686
+ };
687
+ const disabled = listAt('disabledMcpServers');
688
+ const enabled = listAt('enabledMcpServers');
689
+ if (disabled === undefined || enabled === undefined)
690
+ return refuse({ refusal: 'scope_unreadable' });
691
+ let nextEntry = copyLayer(entry);
692
+ const put = (k, list, had) => {
693
+ if (list.length === 0 && !had)
694
+ return;
695
+ def(nextEntry, k, list);
696
+ };
697
+ const hadD = entry !== undefined && Object.hasOwn(entry, 'disabledMcpServers');
698
+ const hadE = entry !== undefined && Object.hasOwn(entry, 'enabledMcpServers');
699
+ if (e.enabled) {
700
+ put('disabledMcpServers', disabled.filter((n) => n !== e.name), hadD);
701
+ if (defaultOff.has(e.name))
702
+ put('enabledMcpServers', enabled.includes(e.name) ? enabled : [...enabled, e.name], hadE);
703
+ }
704
+ else {
705
+ put('disabledMcpServers', disabled.includes(e.name) ? disabled : [...disabled, e.name], hadD);
706
+ if (hadE)
707
+ put('enabledMcpServers', enabled.filter((n) => n !== e.name), hadE);
708
+ }
709
+ if (entry === undefined && Object.keys(nextEntry).length === 0)
710
+ nextEntry = {};
711
+ const next = withPath(cur, projPath, nextEntry);
712
+ if (next === undefined)
713
+ return refuse({ refusal: 'scope_unreadable' });
714
+ box.plan = { op: 'setEnabled', name: e.name };
715
+ return { kind: 'write', next };
716
+ },
717
+ };
718
+ }
719
+ else if (e.op === 'setProjectApproval') {
720
+ box.probe = fixedProbeOf(e);
721
+ const WANT = { approve: ['set', 'delete'], reject: ['delete', 'set'], clear: ['delete', 'delete'] };
722
+ const want = WANT[e.approval];
723
+ const edit1 = (intent, op) => (op === 'set' ? { intent, member: e.name, op, value: e.name } : { intent, member: e.name, op });
724
+ const inner = settingsWriteStep({ doc: 'localSettings', trust: approvalTrust(c), edits: [edit1('mcpJsonEnabled', want[0]), edit1('mcpJsonDisabled', want[1])] });
725
+ step = {
726
+ ...inner,
727
+ plan: (input) => {
728
+ const out = inner.plan(input);
729
+ if (out.kind === 'refuse' && out.reason === 'untrusted_folder')
730
+ box.captured = { refusal: 'untrusted_folder' };
731
+ else if (out.kind === 'refuse' && out.reason === 'scope_unwritable')
732
+ box.captured = { refusal: 'scope_not_offered', scope: 'local', why: 'layer_not_admitted' };
733
+ if (out.kind !== 'refuse')
734
+ box.plan = { op: 'setProjectApproval', name: e.name };
735
+ return out;
736
+ },
737
+ };
738
+ }
739
+ else {
740
+ const place = placeOf(e.scope, projectKey);
741
+ const good = pre.good ?? [];
742
+ const skipped = pre.skipped ?? [];
743
+ box.probe = fixedProbeOf(e);
744
+ step = {
745
+ doc: place.doc,
746
+ role: 'effect',
747
+ lock: [place.doc === 'globalConfig' ? 'projectMcp' : 'globalConfig'],
748
+ plan: ({ fresh }) => {
749
+ const cur = docOf(fresh, place.doc);
750
+ const t = subTableOf(cur, place.path);
751
+ if (t.kind === 'bad')
752
+ return refuse({ refusal: 'scope_unreadable' });
753
+ const taken = new Set();
754
+ for (const s of MCP_SERVER_SCOPES) {
755
+ const p = placeOf(s, projectKey);
756
+ if (p === undefined)
757
+ continue;
758
+ const tt = subTableOf(docOf(fresh, p.doc), p.path);
759
+ if (tt.kind === 'table')
760
+ for (const k of Object.keys(tt.table))
761
+ taken.add(k);
762
+ }
763
+ let table = t.kind === 'table' ? copyLayer(t.table) : undefined;
764
+ const imported = [];
765
+ const skippedHere = [...skipped];
766
+ for (const g of good) {
767
+ let name = g.name;
768
+ if (taken.has(name)) {
769
+ let found;
770
+ for (let n = 1; n <= NAME_N_MAX && found === undefined; n++) {
771
+ const cand = `${g.name}_${n}`;
772
+ if (!taken.has(cand) && nameRefusal(cand, [], reserved) === undefined)
773
+ found = cand;
774
+ }
775
+ if (found === undefined) {
776
+ skippedHere.push({ name: g.name, reason: 'name_taken' });
777
+ continue;
778
+ }
779
+ name = found;
780
+ }
781
+ taken.add(name);
782
+ table = tableWith(table, name, g.entry);
783
+ imported.push(name === g.name ? { name } : { name, renamedFrom: g.name });
784
+ }
785
+ if (imported.length === 0)
786
+ return refuse({ refusal: 'name_taken', scope: e.scope, exhausted: true });
787
+ const next = withPath(cur, place.path, table);
788
+ if (next === undefined)
789
+ return refuse({ refusal: 'scope_unreadable' });
790
+ box.plan = { op: 'importFromSibling', name: imported[0].name, scope: e.scope, imported: Object.freeze(imported), skipped: Object.freeze(skippedHere) };
791
+ return { kind: 'write', next };
792
+ },
793
+ };
794
+ }
795
+ const intent = Object.freeze({ appliesOn: 'next_turn', steps: Object.freeze([Object.freeze(step)]) });
796
+ const receipt = dry ? await previewLocalConfigIntent(intent, store, c.lane) : await applyLocalConfigIntent(intent, store, c.lane);
797
+ const captured = box.captured;
798
+ const planOut = box.plan;
799
+ let probe = box.probe;
800
+ const refusal = receipt.kind === 'refused' && captured !== undefined && refusalReasonOf(captured.refusal) === receipt.reason ? captured : undefined;
801
+ if (receipt.kind === 'refused' && probe !== undefined && probe.kind === 'spec')
802
+ probe = { kind: 'none', why: 'edit_refused' };
803
+ return Object.freeze({
804
+ receipt,
805
+ ...(refusal === undefined ? {} : { refusal: Object.freeze(refusal) }),
806
+ ...(planOut === undefined || receipt.kind === 'refused' || receipt.kind === 'unavailable' ? {} : { plan: Object.freeze(planOut) }),
807
+ notices: Object.freeze(notices),
808
+ ...(dry && probe !== undefined ? { probe: Object.freeze(probe) } : {}),
809
+ });
810
+ }
811
+ catch {
812
+ return Object.freeze({ receipt: Object.freeze({ kind: 'refused', reason: 'invalid_request' }), notices: Object.freeze([]) });
813
+ }
814
+ }
815
+ export async function mcpServerProbeTargetOf(edit, store, ctx) {
816
+ const refused = { kind: 'refused' };
817
+ try {
818
+ const f = editFactsOf(ctx);
819
+ const access = localConfigAccess(f.c.lane, { access: 'read' });
820
+ if (!access.ok)
821
+ return Object.freeze({ kind: 'unavailable', cause: access.cause });
822
+ const shaped = shapeMcpServerEdit(edit);
823
+ if (!shaped.ok)
824
+ return refused;
825
+ const e = shaped.edit;
826
+ const pre = preGatesOf(e, f);
827
+ if (!pre.ok)
828
+ return refused;
829
+ if (e.op === 'add')
830
+ return Object.freeze(entryProbeOf(pre.entry?.spec));
831
+ if (e.op === 'update') {
832
+ const place = placeOf(e.scope, f.projectKey);
833
+ const snap = await snapshotOrUndefined(store);
834
+ if (snap === undefined)
835
+ return refused;
836
+ const v = updateVerdictOn(snap[place.doc], place.path, e, f);
837
+ return v.ok ? Object.freeze(v.probe) : refused;
838
+ }
839
+ if (e.op === 'remove' && e.scope !== undefined && scopeGateOf(e.scope, f) !== undefined)
840
+ return refused;
841
+ return Object.freeze(fixedProbeOf(e));
842
+ }
843
+ catch {
844
+ return refused;
845
+ }
846
+ }