@jini-ai/core 0.4.1 → 0.4.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.
Files changed (118) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +18 -1
  3. package/dist/agent-tools.d.ts +1 -0
  4. package/dist/approval.d.ts +134 -0
  5. package/dist/approval.js +141 -0
  6. package/dist/index.d.ts +5 -0
  7. package/dist/index.js +5 -0
  8. package/dist/model-facing-tool-errors.d.ts +152 -0
  9. package/dist/model-facing-tool-errors.js +124 -0
  10. package/dist/native-regexp.d.ts +10 -0
  11. package/dist/native-regexp.js +2 -0
  12. package/dist/primitives/index.d.ts +2 -0
  13. package/dist/primitives/index.js +1 -0
  14. package/dist/primitives/sse.d.ts +60 -0
  15. package/dist/primitives/sse.js +122 -0
  16. package/dist/redact.js +4 -3
  17. package/dist/registration-kit.d.ts +13 -0
  18. package/dist/registration-kit.js +12 -0
  19. package/dist/settings/agent-tools.d.ts +10 -0
  20. package/dist/settings/agent-tools.js +204 -0
  21. package/dist/settings/agent-value-write-tools.d.ts +22 -0
  22. package/dist/settings/agent-value-write-tools.js +157 -0
  23. package/dist/settings/agent-writable-preferences.d.ts +67 -0
  24. package/dist/settings/agent-writable-preferences.js +132 -0
  25. package/dist/settings/agent-write-denylist.d.ts +19 -0
  26. package/dist/settings/agent-write-denylist.js +15 -0
  27. package/dist/settings/change-feed.d.ts +83 -0
  28. package/dist/settings/change-feed.js +61 -0
  29. package/dist/settings/definitions-dispatch.d.ts +56 -0
  30. package/dist/settings/definitions-dispatch.js +86 -0
  31. package/dist/settings/dictionaries/index.d.ts +48 -0
  32. package/dist/settings/dictionaries/index.js +69 -0
  33. package/dist/settings/dictionaries/settings-dialog.ar.d.ts +3 -0
  34. package/dist/settings/dictionaries/settings-dialog.ar.js +27 -0
  35. package/dist/settings/dictionaries/settings-dialog.bn.d.ts +3 -0
  36. package/dist/settings/dictionaries/settings-dialog.bn.js +27 -0
  37. package/dist/settings/dictionaries/settings-dialog.de.d.ts +3 -0
  38. package/dist/settings/dictionaries/settings-dialog.de.js +27 -0
  39. package/dist/settings/dictionaries/settings-dialog.es.d.ts +3 -0
  40. package/dist/settings/dictionaries/settings-dialog.es.js +27 -0
  41. package/dist/settings/dictionaries/settings-dialog.fa.d.ts +3 -0
  42. package/dist/settings/dictionaries/settings-dialog.fa.js +27 -0
  43. package/dist/settings/dictionaries/settings-dialog.fr.d.ts +3 -0
  44. package/dist/settings/dictionaries/settings-dialog.fr.js +27 -0
  45. package/dist/settings/dictionaries/settings-dialog.hi.d.ts +3 -0
  46. package/dist/settings/dictionaries/settings-dialog.hi.js +27 -0
  47. package/dist/settings/dictionaries/settings-dialog.hu.d.ts +3 -0
  48. package/dist/settings/dictionaries/settings-dialog.hu.js +27 -0
  49. package/dist/settings/dictionaries/settings-dialog.id.d.ts +3 -0
  50. package/dist/settings/dictionaries/settings-dialog.id.js +27 -0
  51. package/dist/settings/dictionaries/settings-dialog.it.d.ts +3 -0
  52. package/dist/settings/dictionaries/settings-dialog.it.js +27 -0
  53. package/dist/settings/dictionaries/settings-dialog.ja.d.ts +3 -0
  54. package/dist/settings/dictionaries/settings-dialog.ja.js +27 -0
  55. package/dist/settings/dictionaries/settings-dialog.ko.d.ts +3 -0
  56. package/dist/settings/dictionaries/settings-dialog.ko.js +27 -0
  57. package/dist/settings/dictionaries/settings-dialog.pl.d.ts +3 -0
  58. package/dist/settings/dictionaries/settings-dialog.pl.js +27 -0
  59. package/dist/settings/dictionaries/settings-dialog.pt-BR.d.ts +3 -0
  60. package/dist/settings/dictionaries/settings-dialog.pt-BR.js +27 -0
  61. package/dist/settings/dictionaries/settings-dialog.ru.d.ts +3 -0
  62. package/dist/settings/dictionaries/settings-dialog.ru.js +27 -0
  63. package/dist/settings/dictionaries/settings-dialog.th.d.ts +3 -0
  64. package/dist/settings/dictionaries/settings-dialog.th.js +27 -0
  65. package/dist/settings/dictionaries/settings-dialog.tr.d.ts +3 -0
  66. package/dist/settings/dictionaries/settings-dialog.tr.js +27 -0
  67. package/dist/settings/dictionaries/settings-dialog.uk.d.ts +3 -0
  68. package/dist/settings/dictionaries/settings-dialog.uk.js +27 -0
  69. package/dist/settings/dictionaries/settings-dialog.ur.d.ts +3 -0
  70. package/dist/settings/dictionaries/settings-dialog.ur.js +27 -0
  71. package/dist/settings/dictionaries/settings-dialog.zh-CN.d.ts +3 -0
  72. package/dist/settings/dictionaries/settings-dialog.zh-CN.js +27 -0
  73. package/dist/settings/dictionaries/settings-dialog.zh-TW.d.ts +3 -0
  74. package/dist/settings/dictionaries/settings-dialog.zh-TW.js +27 -0
  75. package/dist/settings/ensure-definitions.d.ts +101 -0
  76. package/dist/settings/ensure-definitions.js +101 -0
  77. package/dist/settings/errors.d.ts +42 -0
  78. package/dist/settings/errors.js +46 -0
  79. package/dist/settings/express/cms-adapter.d.ts +20 -0
  80. package/dist/settings/express/cms-adapter.js +124 -0
  81. package/dist/settings/express/contracts.d.ts +135 -0
  82. package/dist/settings/express/contracts.js +2 -0
  83. package/dist/settings/express/http.d.ts +10 -0
  84. package/dist/settings/express/http.js +211 -0
  85. package/dist/settings/express/index.d.ts +5 -0
  86. package/dist/settings/express/index.js +4 -0
  87. package/dist/settings/express/shared.d.ts +68 -0
  88. package/dist/settings/express/shared.js +54 -0
  89. package/dist/settings/express/stream.d.ts +24 -0
  90. package/dist/settings/express/stream.js +147 -0
  91. package/dist/settings/index.d.ts +40 -0
  92. package/dist/settings/index.js +37 -0
  93. package/dist/settings/ports.d.ts +118 -0
  94. package/dist/settings/ports.js +2 -0
  95. package/dist/settings/principal-lookup.d.ts +9 -0
  96. package/dist/settings/principal-lookup.js +2 -0
  97. package/dist/settings/purge-service.d.ts +54 -0
  98. package/dist/settings/purge-service.js +106 -0
  99. package/dist/settings/repo.memory.d.ts +119 -0
  100. package/dist/settings/repo.memory.js +199 -0
  101. package/dist/settings/settings.d.ts +139 -0
  102. package/dist/settings/settings.js +259 -0
  103. package/dist/settings/tool-registrations.d.ts +54 -0
  104. package/dist/settings/tool-registrations.js +250 -0
  105. package/dist/settings/types.d.ts +125 -0
  106. package/dist/settings/types.js +3 -0
  107. package/dist/settings/ui-tab-definitions.d.ts +41 -0
  108. package/dist/settings/ui-tab-definitions.js +148 -0
  109. package/dist/settings/write-service.d.ts +320 -0
  110. package/dist/settings/write-service.js +683 -0
  111. package/dist/surface-exchanges.d.ts +209 -0
  112. package/dist/surface-exchanges.js +55 -0
  113. package/dist/tool-metadata.d.ts +71 -0
  114. package/dist/tool-metadata.js +56 -0
  115. package/dist/tool-permissions.d.ts +76 -0
  116. package/dist/tool-permissions.js +57 -0
  117. package/dist/tool-registry.d.ts +3 -0
  118. package/package.json +60 -3
@@ -0,0 +1,250 @@
1
+ import { adaptLegacyAuthorize } from "../tool-permissions.js";
2
+ import { buildSettingsValueWriteHandler } from "./agent-value-write-tools.js";
3
+ import { AGENT_WRITE_CONFIRMATION_SETTINGS, AGENT_WRITE_DENIED_SETTINGS, findAgentWriteRule } from "./agent-write-denylist.js";
4
+ import { buildDomainRegistrations, indexCatalogById, optionalString, requireInputRecord, requireNoInput, requireString, withSchemaOnRejection } from "../registration-kit.js";
5
+ import { requireToolPermission } from "../tool-permissions.js";
6
+ import { getSettingsAgentToolCatalog } from "./agent-tools.js";
7
+ import { AGENT_PREFERENCE_WRITE_SCOPE, AGENT_WRITABLE_PREFERENCE_IDS, resolveAgentWritablePreference, } from "./agent-writable-preferences.js";
8
+ import { DefinitionNotFoundError, DefinitionTombstonedError, ScopeNotAllowedError, ValueValidationFailedError, } from "./errors.js";
9
+ import { getEffective, resolveDefinition, resolveDefinitionRaw } from "./settings.js";
10
+ import { SCOPE_BIT } from "./types.js";
11
+ import { set as setSettingValue } from "./write-service.js";
12
+ const CATALOG_BY_ID = indexCatalogById({ catalog: getSettingsAgentToolCatalog() });
13
+ /** Permission gating a read that names a DIFFERENT principal than the caller — mirrors a host's
14
+ * own HTTP admin layer's equivalent constant (kept as a literal here per this file's header; the
15
+ * two are data, not logic, so duplication carries no drift risk beyond a string comparison). */
16
+ const CROSS_PRINCIPAL_SETTINGS_READ_PERMISSION = "settings.user.read";
17
+ /**
18
+ * Decides which principal's user layer a settings read tool may target — the tool-wiring twin of
19
+ * a host's own HTTP admin layer's equivalent read-target resolver (see this file's header for why
20
+ * it is duplicated rather than imported).
21
+ *
22
+ * @throws {ForbiddenError} (`core/commands`, via `requireToolPermission`) If `requestedPrincipalId`
23
+ * names a different principal than the caller and the caller lacks `settings.user.read`.
24
+ * @returns The principal id to read (`undefined` = skip the user layer entirely).
25
+ * @complexity O(1); at most one `authorize()` call (none for a self-read or no-op read).
26
+ * @overallScore 100
27
+ */
28
+ async function resolveOwnOrOtherPrincipalRead(routeDeps, required) {
29
+ const { requestedPrincipalId, callerPrincipalId } = required;
30
+ // Defaults to the CALLER, never to `undefined`. `undefined` does not mean "the caller" to
31
+ // `getEffective` — it means "skip the user layer entirely" (`settings.ts`'s
32
+ // `if (workspaceId && principalId)` guard), so an omitted `principalId` would otherwise make
33
+ // every read tool report the workspace/global/default value while silently ignoring the
34
+ // operator's own — a read tool that cannot see the layer the operator is actually looking at
35
+ // does not fail loudly; it produces confident wrong answers.
36
+ const target = requestedPrincipalId ?? callerPrincipalId;
37
+ if (target === callerPrincipalId)
38
+ return target;
39
+ await requireToolPermission({ authorize: adaptLegacyAuthorize({ authorize: routeDeps.authorize }), workspaceId: routeDeps.workspaceId, principalId: callerPrincipalId, permission: CROSS_PRINCIPAL_SETTINGS_READ_PERMISSION }, { entityType: "setting-value" });
40
+ return requestedPrincipalId;
41
+ }
42
+ function layerValueOf(record) {
43
+ return record && record.state === "set" ? record.valueJson : null;
44
+ }
45
+ /**
46
+ * Builds `write-service.ts`'s own five-field dep shape from `RouteDeps`.
47
+ *
48
+ * Deliberately duplicates a host's own HTTP admin layer's equivalent field-mapping helper rather
49
+ * than importing it, for the reason this file's header already gives about
50
+ * `resolveOwnOrOtherPrincipalRead`: this is domain-owned wiring, and importing from a host's HTTP
51
+ * admin layer would invert the ports/adapters direction. It is a field mapping, not logic, so the
52
+ * two copies carry no behavioral drift risk — and `write-service.ts` is the single chokepoint
53
+ * either way, so a divergence here could only produce a missing dependency, which fails loudly.
54
+ */
55
+ function toWriteDeps(routeDeps) {
56
+ return {
57
+ repo: routeDeps.settingsRepo,
58
+ clock: routeDeps.clock,
59
+ ids: routeDeps.idGen,
60
+ authorize: routeDeps.authorize,
61
+ principals: routeDeps.principalRepo,
62
+ };
63
+ }
64
+ /**
65
+ * Errors a DIFFERENT input would fix, for `withSchemaOnRejection`'s decoration.
66
+ *
67
+ * `ValueValidationFailedError` is the common one — the model sent a value of the wrong shape for
68
+ * the chosen setting, and republishing the schema (with its per-key value hints) lets it correct
69
+ * in one turn instead of guessing. The two definition errors are included because they are
70
+ * genuinely reachable from a bad `setting`: an id that passed the enum but names a definition this
71
+ * workspace has tombstoned. A `ForbiddenError` is deliberately absent — no input change fixes a
72
+ * missing grant, and decorating it would imply the call is worth retrying.
73
+ */
74
+ function isPreferenceShapeRejection(error) {
75
+ return (error instanceof ValueValidationFailedError ||
76
+ error instanceof DefinitionNotFoundError ||
77
+ error instanceof DefinitionTombstonedError ||
78
+ error instanceof ScopeNotAllowedError);
79
+ }
80
+ /**
81
+ * This wiring layer's OWN risk classification, authored from what each handler below actually
82
+ * calls. See `DerivedRiskByToolId` in the kit for why it is independent of the catalog's own
83
+ * `sideEffects` declaration — and note that the two excluded bulk/schema writes appear NOWHERE here, which is
84
+ * itself the strongest of the guards: an unclassified id cannot be wired at all.
85
+ */
86
+ export const settingsDerivedRisk = new Map([
87
+ // -> write-service.set(): one value override and one revision, with optional human confirmation.
88
+ ["settings_set_value", "mutates-durable-state"],
89
+ // -> write-service.clear(): one override marked cleared and one revision; no definition/delete.
90
+ ["settings_clear_value", "mutates-durable-state"],
91
+ // -> settingsRepo.listActiveDefinitions() x2 + resolveDefinitionRaw() for aliases: reads only.
92
+ ["settings_list_definitions", "none"],
93
+ // -> settingsRepo.listActiveDefinitions() x2 + getEffective() per key: reads only, no write path.
94
+ ["settings_get_effective", "none"],
95
+ // -> resolveDefinition() + settingsRepo.getGlobalValue/getWorkspaceValue/getUserValue: reads only.
96
+ ["settings_get_raw", "none"],
97
+ // -> write-service.set() at scope=user against the caller's own principal: writes a value row
98
+ // and a revision-ledger entry. Durable, and correctly classified as such even though every key
99
+ // it can reach is a reversible display preference — the classification describes what the
100
+ // handler DOES, not how much the operator would mind.
101
+ ["settings_set_ui_preference", "mutates-durable-state"],
102
+ ]);
103
+ /** Settings catalog entries this pass does not wire, and why — see `agent-tools.ts`'s own per-entry comments for the full reasoning. */
104
+ const UNWIRED_SETTINGS_TOOL_IDS = new Set([
105
+ // EXCLUDED BY DESIGN: bulk variant — clears every value in an operator-named namespace at a
106
+ // scope in one call, irreversible.
107
+ "settings_reset",
108
+ // EXCLUDED BY DESIGN: schema-level, not value-level — can rename/retype/deprecate/tombstone the
109
+ // definition a key resolves through, reinterpreting every existing stored value for that key
110
+ // platform-wide.
111
+ "settings_register_definitions",
112
+ ]);
113
+ export function buildSettingsRegistrations(routeDeps) {
114
+ const handlers = {
115
+ settings_set_value: buildSettingsValueWriteHandler(routeDeps, "settings_set_value"),
116
+ settings_clear_value: buildSettingsValueWriteHandler(routeDeps, "settings_clear_value"),
117
+ settings_list_definitions: async (ctx) => {
118
+ requireNoInput({ input: ctx.input });
119
+ await routeDeps.settingsReady;
120
+ await requireToolPermission({ authorize: adaptLegacyAuthorize({ authorize: routeDeps.authorize }), workspaceId: routeDeps.workspaceId, principalId: ctx.principal.id, permission: "settings.read.definitions" }, { entityType: "setting-definition" });
121
+ const [platformDefs, siteDefs] = await Promise.all([
122
+ routeDeps.settingsRepo.listActiveDefinitions({ workspaceId: null }),
123
+ routeDeps.settingsRepo.listActiveDefinitions({ workspaceId: routeDeps.workspaceId }),
124
+ ]);
125
+ const deniedRules = [...AGENT_WRITE_DENIED_SETTINGS, ...(routeDeps.extraDeniedSettings ?? [])];
126
+ const confirmationRules = [...(routeDeps.confirmationSettings ?? AGENT_WRITE_CONFIRMATION_SETTINGS), ...(routeDeps.extraConfirmationSettings ?? [])];
127
+ // Only aliases need an extra lookup. Their target's schema/scope and BOTH coordinates'
128
+ // key policies must agree with the write handler; dangling/tombstoned targets fail closed.
129
+ const data = (await Promise.all([...platformDefs, ...siteDefs].map(async (def) => {
130
+ const resolved = def.status === "alias"
131
+ ? await resolveDefinitionRaw({ repo: routeDeps.settingsRepo }, { namespace: def.namespace, key: def.key, workspaceId: routeDeps.workspaceId })
132
+ : def;
133
+ const coordinates = resolved ? [def, resolved] : [def];
134
+ const denied = coordinates.some((coordinate) => !!findAgentWriteRule(deniedRules, coordinate));
135
+ const confirmationRequired = coordinates.some((coordinate) => !!findAgentWriteRule(confirmationRules, coordinate));
136
+ const scopes = resolved?.scopes ?? def.scopes;
137
+ return { namespace: def.namespace, key: def.key, ownerKind: def.ownerKind, scopes, status: def.status, version: def.version,
138
+ schema: resolved?.schema ?? def.schema,
139
+ agentWritable: !!resolved && resolved.status !== "tombstone" && !denied && (!confirmationRequired || !!routeDeps.confirmWrite) && (scopes & (SCOPE_BIT.workspace | SCOPE_BIT.user)) !== 0,
140
+ confirmationRequired };
141
+ })))
142
+ .sort((a, b) => a.namespace.localeCompare(b.namespace) || a.key.localeCompare(b.key));
143
+ return { data };
144
+ },
145
+ settings_get_effective: async (ctx) => {
146
+ const input = requireInputRecord({ input: ctx.input });
147
+ const namespace = requireString({ input: input, key: "namespace" });
148
+ await routeDeps.settingsReady;
149
+ await requireToolPermission({ authorize: adaptLegacyAuthorize({ authorize: routeDeps.authorize }), workspaceId: routeDeps.workspaceId, principalId: ctx.principal.id, permission: "settings.read" }, { entityType: "setting-value" });
150
+ const workspaceId = routeDeps.workspaceId;
151
+ const principalId = await resolveOwnOrOtherPrincipalRead(routeDeps, {
152
+ requestedPrincipalId: optionalString({ input: input, key: "principalId" }),
153
+ callerPrincipalId: ctx.principal.id,
154
+ });
155
+ const [platformDefs, siteDefs] = await Promise.all([
156
+ routeDeps.settingsRepo.listActiveDefinitions({ workspaceId: null }),
157
+ routeDeps.settingsRepo.listActiveDefinitions({ workspaceId }),
158
+ ]);
159
+ const keys = new Set([...platformDefs, ...siteDefs].filter((d) => d.namespace === namespace).map((d) => d.key));
160
+ const data = [];
161
+ for (const key of keys) {
162
+ const resolved = await getEffective({ repo: routeDeps.settingsRepo }, { namespace, key, scopeContext: { workspaceId, principalId } });
163
+ if (resolved)
164
+ data.push({ key, value: resolved.value, sourceLayer: resolved.sourceLayer, defVersion: resolved.defVersion });
165
+ }
166
+ return { data };
167
+ },
168
+ settings_get_raw: async (ctx) => {
169
+ const input = requireInputRecord({ input: ctx.input });
170
+ const namespace = requireString({ input: input, key: "namespace" });
171
+ const key = requireString({ input: input, key: "key" });
172
+ await routeDeps.settingsReady;
173
+ await requireToolPermission({ authorize: adaptLegacyAuthorize({ authorize: routeDeps.authorize }), workspaceId: routeDeps.workspaceId, principalId: ctx.principal.id, permission: "settings.read.raw" }, { entityType: "setting-value" });
174
+ const workspaceId = routeDeps.workspaceId;
175
+ const principalId = await resolveOwnOrOtherPrincipalRead(routeDeps, {
176
+ requestedPrincipalId: optionalString({ input: input, key: "principalId" }),
177
+ callerPrincipalId: ctx.principal.id,
178
+ });
179
+ const definition = await resolveDefinition({ repo: routeDeps.settingsRepo }, { namespace, key, workspaceId });
180
+ if (!definition)
181
+ throw new Error(`definition '${namespace}.${key}' was not found`);
182
+ const [globalValue, workspaceValue, userValue] = await Promise.all([
183
+ routeDeps.settingsRepo.getGlobalValue(definition.settingId),
184
+ routeDeps.settingsRepo.getWorkspaceValue({ workspaceId, settingId: definition.settingId }),
185
+ principalId ? routeDeps.settingsRepo.getUserValue({ workspaceId, principalId, settingId: definition.settingId }) : Promise.resolve(null),
186
+ ]);
187
+ return {
188
+ key: `${namespace}.${key}`,
189
+ global: layerValueOf(globalValue),
190
+ workspace: layerValueOf(workspaceValue),
191
+ user: layerValueOf(userValue),
192
+ default: definition.defaultValue,
193
+ };
194
+ },
195
+ settings_set_ui_preference: async (ctx) => {
196
+ const input = requireInputRecord({ input: ctx.input });
197
+ const settingId = requireString({ input: input, key: "setting" });
198
+ // Re-checked here even though the published schema carries the same enum. See
199
+ // `agent-writable-preferences.ts` on why: the enum is enforced by a caller's descriptor
200
+ // validation, and this is the check that still holds if a stale or bypassed descriptor lets
201
+ // an unlisted id through. Rejected BEFORE `settingsUiTabsReady` is awaited so an unlisted id
202
+ // cannot even be used to probe which definitions exist.
203
+ const preference = resolveAgentWritablePreference(settingId);
204
+ if (!preference) {
205
+ throw new Error(`'${settingId}' is not an agent-writable preference. Allowed: ${AGENT_WRITABLE_PREFERENCE_IDS.join(", ")}.`);
206
+ }
207
+ // `in` rather than a truthiness check: `false` and `0` are legitimate values here
208
+ // (`soundEnabled`, `desktopEnabled`), and rejecting them would make the two boolean
209
+ // preferences settable in one direction only.
210
+ if (!("value" in input)) {
211
+ throw new Error(`a 'value' is required to set '${settingId}'`);
212
+ }
213
+ const value = input.value;
214
+ // This tool's definitions are registered by `ensureSettingsUiTabDefinitions()` at boot, not
215
+ // by any legacy-migration promise a host might also cover — awaiting the wrong promise would
216
+ // make an early call fail with a spurious "definition was not found".
217
+ await routeDeps.settingsUiTabsReady;
218
+ // No `requireToolPermission` call here, unlike every read above. That is deliberate and is
219
+ // the opposite of the reads' situation: the read paths carry no `authorize()` of their own,
220
+ // so this layer must supply it, whereas `write-service.set()` IS the authorization
221
+ // chokepoint and derives the permission itself from the scope and target. Adding a. See docs/decisions/DR-003-settings-ledger-invariants.md.
222
+ // pre-check would mean this file naming a permission string that the chokepoint might later
223
+ // derive differently — the exact fail-open drift `deriveRequiredPermission`'s own header
224
+ // (Red-Team ) says must not be duplicated. Omitting `principalId` is what makes the. See docs/decisions/DR-003-settings-ledger-invariants.md.
225
+ // derived permission `settings.user.self.write`.
226
+ const { value: stored, revisionSeq } = await withSchemaOnRejection({
227
+ toolId: "settings_set_ui_preference",
228
+ catalog: CATALOG_BY_ID,
229
+ isShapeRejection: ({ error }) => isPreferenceShapeRejection(error), fn: () => setSettingValue({
230
+ deps: toWriteDeps(routeDeps),
231
+ input: {
232
+ namespace: preference.namespace,
233
+ key: preference.key,
234
+ scope: AGENT_PREFERENCE_WRITE_SCOPE,
235
+ value,
236
+ workspaceId: routeDeps.workspaceId,
237
+ // `principalId` is deliberately NOT passed. `write-service.set()` falls back to
238
+ // `callerPrincipalId` for a user-scoped write, so the caller's own layer is the only
239
+ // reachable target — and the self-vs-other permission derivation sees a self-write.
240
+ callerPrincipalId: ctx.principal.id,
241
+ authWorkspaceId: routeDeps.workspaceId,
242
+ },
243
+ })
244
+ });
245
+ return { setting: settingId, value: stored, scope: AGENT_PREFERENCE_WRITE_SCOPE, revisionSeq };
246
+ },
247
+ };
248
+ return buildDomainRegistrations({ domain: "settings", catalogModule: "settings/agent-tools.ts", catalog: CATALOG_BY_ID, handlers, derivedRisk: settingsDerivedRisk }, { unwiredToolIds: UNWIRED_SETTINGS_TOOL_IDS });
249
+ }
250
+ //# sourceMappingURL=tool-registrations.js.map
@@ -0,0 +1,125 @@
1
+ import type { ISODateTime, JsonValue, UUID } from "../primitives/index.js";
2
+ /**
3
+ * @file Settings domain types.
4
+ *
5
+ * Purpose:
6
+ * Record shapes + the schema-validation vocabulary shared by `settings.ts`,
7
+ * `write-service.ts`, `purge-service.ts`, and both repo adapters. Interfaces
8
+ * and types only — no feature logic (mirrors `identity/types.ts`).
9
+ */
10
+ export type SettingScope = "global" | "workspace" | "user";
11
+ export type SettingOwnerKind = "core" | "site" | "theme";
12
+ export type DefinitionStatus = "active" | "alias" | "deprecated" | "tombstone";
13
+ export type ValueState = "set" | "cleared";
14
+ export type RevisionEntityKind = "definition" | "value";
15
+ export type RevisionOp = "register" | "alias" | "retype"
16
+ /**
17
+ * A `core` definition's stored `default_json` was reconciled to the default
18
+ * its source declares (`write-service.ts`'s `reconcileDefinitionDefault`).
19
+ * Distinct from `retype`: the schema is unchanged, so no stored value needs
20
+ * coercion and the definition version does NOT advance. Only unset-value
21
+ * resolution changes.
22
+ */
23
+ | "redefault" | "deprecate" | "tombstone" | "set" | "clear" | "purge" | "coerce";
24
+ /** Scope bitmask values: global=1, workspace=2, user=4. Legal range 1..7. */
25
+ export declare const SCOPE_BIT: {
26
+ readonly global: 1;
27
+ readonly workspace: 2;
28
+ readonly user: 4;
29
+ };
30
+ /**
31
+ * A deliberately small, total value-schema vocabulary (Article III — this
32
+ * feature registers string/number/boolean/enum settings; a generic JSON
33
+ * Schema engine is not needed and would be speculative generality). Every
34
+ * variant is exhaustively validatable by `validateValue`.
35
+ */
36
+ /**
37
+ * `{type:"json"}` is a deliberately narrow, scalar-ADJACENT exception to the
38
+ * "deliberately small" vocabulary above: it accepts any JSON value
39
+ * (object/array/scalar) and validates nothing about its internal shape —
40
+ * that is the registering feature's own write-path responsibility, never
41
+ * this ledger's job.
42
+ *
43
+ * Enforcement rule that must survive any refactor: Code Review must confirm
44
+ * that EVERY use is genuinely unbounded, list-shaped data with no scalar
45
+ * decomposition available — it is not a general escape hatch for fields that
46
+ * could be scalar-decomposed instead. The check is per-use, deliberately not
47
+ * a headcount. See `docs/decisions/settings-json-schema-variant.md` for the
48
+ * originating rationale and a related non-null-default invariant.
49
+ */
50
+ export type SettingValueSchema = {
51
+ type: "string";
52
+ nullable?: boolean;
53
+ } | {
54
+ type: "number";
55
+ nullable?: boolean;
56
+ } | {
57
+ type: "boolean";
58
+ nullable?: boolean;
59
+ } | {
60
+ type: "enum";
61
+ values: readonly string[];
62
+ nullable?: boolean;
63
+ } | {
64
+ type: "json";
65
+ nullable?: boolean;
66
+ };
67
+ export interface SettingDefinitionRecord {
68
+ settingId: UUID;
69
+ version: number;
70
+ /** NULL = platform def (core/theme); non-null = site-owned. */
71
+ workspaceId: UUID | null;
72
+ namespace: string;
73
+ key: string;
74
+ ownerKind: SettingOwnerKind;
75
+ ownerId: string | null;
76
+ schema: SettingValueSchema;
77
+ defaultValue: JsonValue | null;
78
+ /** Bitmask over `SCOPE_BIT`, legal range 1..7. */
79
+ scopes: number;
80
+ secret: boolean;
81
+ status: DefinitionStatus;
82
+ aliasOfNamespace: string | null;
83
+ aliasOfKey: string | null;
84
+ /** Present only when `version > 1`; a total coercer id/tag for the prior version. */
85
+ coercionTag: string | null;
86
+ createdAt: ISODateTime;
87
+ updatedAt: ISODateTime;
88
+ }
89
+ export interface SettingValueRecord {
90
+ settingId: UUID;
91
+ scope: SettingScope;
92
+ /** Present for workspace/user scope; null for global. */
93
+ workspaceId: UUID | null;
94
+ /** Present for user scope only. */
95
+ principalId: UUID | null;
96
+ valueJson: JsonValue | null;
97
+ state: ValueState;
98
+ defVersion: number;
99
+ seq: number;
100
+ updatedBy: UUID;
101
+ updatedAt: ISODateTime;
102
+ originPluginId: string | null;
103
+ }
104
+ export interface SettingRevisionRecord {
105
+ seq: number;
106
+ entityKind: RevisionEntityKind;
107
+ settingId: UUID;
108
+ scope: SettingScope | null;
109
+ workspaceId: UUID | null;
110
+ principalId: UUID | null;
111
+ op: RevisionOp;
112
+ beforeJson: JsonValue | null;
113
+ afterJson: JsonValue | null;
114
+ defVersion: number;
115
+ actor: UUID;
116
+ originPluginId: string | null;
117
+ changeSetId: string | null;
118
+ createdAt: ISODateTime;
119
+ }
120
+ /** The context a resolver/write call is evaluated against. */
121
+ export interface SettingScopeContext {
122
+ workspaceId?: UUID | undefined;
123
+ principalId?: UUID | undefined;
124
+ }
125
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,3 @@
1
+ /** Scope bitmask values: global=1, workspace=2, user=4. Legal range 1..7. */
2
+ export const SCOPE_BIT = { global: 1, workspace: 2, user: 4 };
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,41 @@
1
+ import type { UUID } from "../primitives/index.js";
2
+ import { type EnsureSettingDefinitionsDeps } from "./ensure-definitions.js";
3
+ /**
4
+ * @file Boot-time definition registration for the settings-dialog tabs that
5
+ * need nothing but ledger storage — Instructions, Notifications, and Privacy.
6
+ *
7
+ * These three sit together because they are the same shape of problem, not
8
+ * because they are the same domain: each is a `value` + `onChange` component
9
+ * from a shared UI library with no async port, so the entire host-side cost
10
+ * is "register some keys and map them". A tab that needs real backend work
11
+ * (routes, a port) should keep its own module rather than accreting into
12
+ * this one.
13
+ *
14
+ * All three read and write through the generic settings ledger routes a host
15
+ * exposes, gated by the ledger's own scope-derived `settings.workspace.write`
16
+ * check. There is no bespoke get/set service and no per-tab permission — the
17
+ * reason the shared `ensureSettingDefinitions` helper exists.
18
+ */
19
+ /** Custom instructions applied to every admin-assistant conversation. */
20
+ export declare const INSTRUCTIONS_NAMESPACE = "core.instructions";
21
+ /** Completion-sound and desktop-notification preferences. */
22
+ export declare const NOTIFICATIONS_NAMESPACE = "core.notifications";
23
+ /** Telemetry consent state. */
24
+ export declare const PRIVACY_NAMESPACE = "core.privacy";
25
+ /** Theme choice and accent color for the settings-dialog surface. */
26
+ export declare const APPEARANCE_NAMESPACE = "core.appearance";
27
+ /** Admin UI locale. */
28
+ export declare const LANGUAGE_NAMESPACE = "core.language";
29
+ export interface EnsureSettingsUiTabDefinitionsInput {
30
+ /** The trusted boot-time actor these writes are attributed to. */
31
+ systemPrincipalId: UUID;
32
+ }
33
+ /**
34
+ * Idempotently registers the Instructions, Notifications, and Privacy
35
+ * definitions. Safe to call on every boot.
36
+ *
37
+ * @complexity O(n) in the total number of definitions (9), each a
38
+ * skip-if-registered check plus at most one `registerDefinitions` call.
39
+ */
40
+ export declare function ensureSettingsUiTabDefinitions(deps: EnsureSettingDefinitionsDeps, input: EnsureSettingsUiTabDefinitionsInput): Promise<void>;
41
+ //# sourceMappingURL=ui-tab-definitions.d.ts.map
@@ -0,0 +1,148 @@
1
+ import { ensureSettingDefinitions, } from "./ensure-definitions.js";
2
+ import { SCOPE_BIT } from "./types.js";
3
+ /**
4
+ * @file Boot-time definition registration for the settings-dialog tabs that
5
+ * need nothing but ledger storage — Instructions, Notifications, and Privacy.
6
+ *
7
+ * These three sit together because they are the same shape of problem, not
8
+ * because they are the same domain: each is a `value` + `onChange` component
9
+ * from a shared UI library with no async port, so the entire host-side cost
10
+ * is "register some keys and map them". A tab that needs real backend work
11
+ * (routes, a port) should keep its own module rather than accreting into
12
+ * this one.
13
+ *
14
+ * All three read and write through the generic settings ledger routes a host
15
+ * exposes, gated by the ledger's own scope-derived `settings.workspace.write`
16
+ * check. There is no bespoke get/set service and no per-tab permission — the
17
+ * reason the shared `ensureSettingDefinitions` helper exists.
18
+ */
19
+ /** Custom instructions applied to every admin-assistant conversation. */
20
+ export const INSTRUCTIONS_NAMESPACE = "core.instructions";
21
+ /** Completion-sound and desktop-notification preferences. */
22
+ export const NOTIFICATIONS_NAMESPACE = "core.notifications";
23
+ /** Telemetry consent state. */
24
+ export const PRIVACY_NAMESPACE = "core.privacy";
25
+ /** Theme choice and accent color for the settings-dialog surface. */
26
+ export const APPEARANCE_NAMESPACE = "core.appearance";
27
+ /** Admin UI locale. */
28
+ export const LANGUAGE_NAMESPACE = "core.language";
29
+ /**
30
+ * "No consent decision recorded yet", which a host's UI layer may spell as
31
+ * `privacyDecisionAt: null`.
32
+ *
33
+ * A literal `null` default is not registrable (see `SettingDefinitionSpec.
34
+ * defaultValue` — non-secret definitions reject a null default outright), so
35
+ * this needs a sentinel. `0` is safe rather than merely convenient: the field
36
+ * is epoch milliseconds, and 0 is 1970-01-01, which cannot be a real decision
37
+ * timestamp for software that did not exist then. The tab uses this exact
38
+ * distinction — an unset value renders the first-run consent prompt, a set one
39
+ * renders the "decided" state — so collapsing the two would silently re-prompt
40
+ * an operator who has already chosen.
41
+ */
42
+ const NO_DECISION_SENTINEL = 0;
43
+ /**
44
+ * "No installation id", which a host's UI layer may spell as
45
+ * `installationId: null` (its meaning: telemetry sharing is fully declined).
46
+ * `""` is not a legal id — the tab generates opaque non-empty strings — so it
47
+ * cannot collide with a real one.
48
+ */
49
+ const NO_INSTALLATION_ID_SENTINEL = "";
50
+ const INSTRUCTIONS_DEFINITIONS = [
51
+ // A host's `InstructionsTab` may collapse an all-empty textarea to `undefined` rather than `''`
52
+ // (its `onChange` contract), so the adapter maps that back to this empty-string default instead
53
+ // of storing a distinguishable "explicitly emptied" state the tab cannot itself produce.
54
+ { key: "custom", schema: { type: "string" }, defaultValue: "" },
55
+ ];
56
+ /**
57
+ * Notification preferences are the one genuinely PER-OPERATOR surface in this
58
+ * set, so they register `SCOPE_BIT.user` alongside `workspace` and the adapter
59
+ * writes at user scope. Two admins sharing a workspace should not fight over
60
+ * whether a completion sound plays on the other's machine — unlike a tab
61
+ * whose backing resource is genuinely shared workspace state.
62
+ *
63
+ * The `workspace` bit stays in the mask so an operator with no user-scoped row
64
+ * still inherits a workspace-level default rather than falling all the way
65
+ * back to the registered one.
66
+ */
67
+ const NOTIFICATIONS_SCOPES = SCOPE_BIT.user | SCOPE_BIT.workspace;
68
+ const NOTIFICATIONS_DEFINITIONS = [
69
+ { key: "soundEnabled", schema: { type: "boolean" }, defaultValue: false, scopes: NOTIFICATIONS_SCOPES },
70
+ // Free-form strings rather than an enum: sound ids are plain strings upstream and the catalog is
71
+ // owned by a host's UI library. Pinning an enum here would make a package-side catalog addition
72
+ // fail validation on an already-registered definition, which is a worse failure than accepting an
73
+ // id the catalog no longer lists (the tab falls back to its own default for an unknown id).
74
+ { key: "successSoundId", schema: { type: "string" }, defaultValue: "ding", scopes: NOTIFICATIONS_SCOPES },
75
+ { key: "failureSoundId", schema: { type: "string" }, defaultValue: "buzz", scopes: NOTIFICATIONS_SCOPES },
76
+ // Desktop notifications also need the BROWSER's Notification permission,
77
+ // which is per-browser and not ours to store. This flag is only the
78
+ // operator's intent; the tab reconciles it against the live permission.
79
+ { key: "desktopEnabled", schema: { type: "boolean" }, defaultValue: false, scopes: NOTIFICATIONS_SCOPES },
80
+ ];
81
+ const PRIVACY_DEFINITIONS = [
82
+ // Both telemetry categories default OFF. A "consent state" type may type them optional, but
83
+ // "unset" is carried by `decisionAt` instead — an operator who has decided has a timestamp, and
84
+ // these two then mean exactly what they say.
85
+ { key: "telemetry.metrics", schema: { type: "boolean" }, defaultValue: false },
86
+ { key: "telemetry.content", schema: { type: "boolean" }, defaultValue: false },
87
+ { key: "installationId", schema: { type: "string" }, defaultValue: NO_INSTALLATION_ID_SENTINEL },
88
+ { key: "decisionAt", schema: { type: "number" }, defaultValue: NO_DECISION_SENTINEL },
89
+ ];
90
+ /**
91
+ * Appearance and Language are per-OPERATOR, same reasoning as Notifications:
92
+ * which theme one admin looks at, and which language they read, are not
93
+ * properties of the workspace. The `workspace` bit stays in the mask so an
94
+ * operator with no user-scoped row still inherits a workspace default.
95
+ */
96
+ const PER_OPERATOR_SCOPES = SCOPE_BIT.user | SCOPE_BIT.workspace;
97
+ const APPEARANCE_DEFINITIONS = [
98
+ // Defaults to LIGHT rather than "system" here — a host whose surrounding admin UI is
99
+ // light-only would put a dark panel inside a light app for every operator on a dark-mode
100
+ // machine if this defaulted to following the OS. "system" stays selectable; a host can
101
+ // override this default at its own registration call site if its UI has a real dark variant.
102
+ {
103
+ key: "theme",
104
+ schema: { type: "enum", values: ["system", "light", "dark"] },
105
+ defaultValue: "light",
106
+ scopes: PER_OPERATOR_SCOPES,
107
+ },
108
+ // Free-form string rather than an enum over a swatch palette: a host's UI may ship a color
109
+ // picker, so any `#rrggbb` is reachable, and its own accent-color normalizer already rejects
110
+ // malformed values at render time. Pinning an enum here would reject a legitimate custom color
111
+ // at the ledger.
112
+ {
113
+ key: "accentColor",
114
+ schema: { type: "string" },
115
+ defaultValue: "#2563eb",
116
+ scopes: PER_OPERATOR_SCOPES,
117
+ },
118
+ ];
119
+ const LANGUAGE_DEFINITIONS = [
120
+ // A BCP-47-ish code, not an enum. The catalog of real locales is a host's own i18n concern, not
121
+ // this library's; the tab takes a host-supplied `LocaleOption[]` and an unknown stored code
122
+ // simply selects nothing rather than failing a ledger write.
123
+ { key: "locale", schema: { type: "string" }, defaultValue: "en", scopes: PER_OPERATOR_SCOPES },
124
+ ];
125
+ /**
126
+ * Idempotently registers the Instructions, Notifications, and Privacy
127
+ * definitions. Safe to call on every boot.
128
+ *
129
+ * @complexity O(n) in the total number of definitions (9), each a
130
+ * skip-if-registered check plus at most one `registerDefinitions` call.
131
+ */
132
+ export async function ensureSettingsUiTabDefinitions(deps, input) {
133
+ const namespaces = [
134
+ { namespace: INSTRUCTIONS_NAMESPACE, definitions: INSTRUCTIONS_DEFINITIONS },
135
+ { namespace: NOTIFICATIONS_NAMESPACE, definitions: NOTIFICATIONS_DEFINITIONS },
136
+ { namespace: PRIVACY_NAMESPACE, definitions: PRIVACY_DEFINITIONS },
137
+ { namespace: APPEARANCE_NAMESPACE, definitions: APPEARANCE_DEFINITIONS },
138
+ { namespace: LANGUAGE_NAMESPACE, definitions: LANGUAGE_DEFINITIONS },
139
+ ];
140
+ for (const { namespace, definitions } of namespaces) {
141
+ await ensureSettingDefinitions(deps, {
142
+ namespace,
143
+ definitions,
144
+ systemPrincipalId: input.systemPrincipalId,
145
+ });
146
+ }
147
+ }
148
+ //# sourceMappingURL=ui-tab-definitions.js.map