@jini-ai/cms 0.5.4 → 0.5.5

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 (113) hide show
  1. package/dist/navigation/agent-tools.js +20 -3
  2. package/dist/navigation/index.d.ts +3 -1
  3. package/dist/navigation/index.js +2 -0
  4. package/dist/navigation/menu-html.d.ts +31 -0
  5. package/dist/navigation/menu-html.js +53 -0
  6. package/dist/navigation/menu-service.d.ts +9 -3
  7. package/dist/navigation/menu-service.js +15 -3
  8. package/dist/navigation/tool-registrations.d.ts +5 -0
  9. package/dist/navigation/tool-registrations.js +32 -2
  10. package/dist/navigation/types.d.ts +6 -0
  11. package/dist/widgets/resolvers/menu.d.ts +8 -0
  12. package/dist/widgets/resolvers/menu.js +21 -6
  13. package/package.json +3 -3
  14. package/dist/http/settings/cms-adapter.d.ts +0 -20
  15. package/dist/http/settings/cms-adapter.js +0 -124
  16. package/dist/http/settings/contracts.d.ts +0 -135
  17. package/dist/http/settings/contracts.js +0 -2
  18. package/dist/http/settings/http.d.ts +0 -10
  19. package/dist/http/settings/http.js +0 -211
  20. package/dist/http/settings/index.d.ts +0 -5
  21. package/dist/http/settings/index.js +0 -4
  22. package/dist/http/settings/shared.d.ts +0 -68
  23. package/dist/http/settings/shared.js +0 -54
  24. package/dist/http/settings/stream.d.ts +0 -24
  25. package/dist/http/settings/stream.js +0 -147
  26. package/dist/settings/agent-tools.d.ts +0 -10
  27. package/dist/settings/agent-tools.js +0 -204
  28. package/dist/settings/agent-value-write-tools.d.ts +0 -22
  29. package/dist/settings/agent-value-write-tools.js +0 -154
  30. package/dist/settings/agent-writable-preferences.d.ts +0 -67
  31. package/dist/settings/agent-writable-preferences.js +0 -132
  32. package/dist/settings/agent-write-denylist.d.ts +0 -19
  33. package/dist/settings/agent-write-denylist.js +0 -15
  34. package/dist/settings/change-feed.d.ts +0 -83
  35. package/dist/settings/change-feed.js +0 -61
  36. package/dist/settings/definitions-dispatch.d.ts +0 -56
  37. package/dist/settings/definitions-dispatch.js +0 -86
  38. package/dist/settings/dictionaries/index.d.ts +0 -48
  39. package/dist/settings/dictionaries/index.js +0 -69
  40. package/dist/settings/dictionaries/settings-dialog.ar.d.ts +0 -3
  41. package/dist/settings/dictionaries/settings-dialog.ar.js +0 -27
  42. package/dist/settings/dictionaries/settings-dialog.bn.d.ts +0 -3
  43. package/dist/settings/dictionaries/settings-dialog.bn.js +0 -27
  44. package/dist/settings/dictionaries/settings-dialog.de.d.ts +0 -3
  45. package/dist/settings/dictionaries/settings-dialog.de.js +0 -27
  46. package/dist/settings/dictionaries/settings-dialog.es.d.ts +0 -3
  47. package/dist/settings/dictionaries/settings-dialog.es.js +0 -27
  48. package/dist/settings/dictionaries/settings-dialog.fa.d.ts +0 -3
  49. package/dist/settings/dictionaries/settings-dialog.fa.js +0 -27
  50. package/dist/settings/dictionaries/settings-dialog.fr.d.ts +0 -3
  51. package/dist/settings/dictionaries/settings-dialog.fr.js +0 -27
  52. package/dist/settings/dictionaries/settings-dialog.hi.d.ts +0 -3
  53. package/dist/settings/dictionaries/settings-dialog.hi.js +0 -27
  54. package/dist/settings/dictionaries/settings-dialog.hu.d.ts +0 -3
  55. package/dist/settings/dictionaries/settings-dialog.hu.js +0 -27
  56. package/dist/settings/dictionaries/settings-dialog.id.d.ts +0 -3
  57. package/dist/settings/dictionaries/settings-dialog.id.js +0 -27
  58. package/dist/settings/dictionaries/settings-dialog.it.d.ts +0 -3
  59. package/dist/settings/dictionaries/settings-dialog.it.js +0 -27
  60. package/dist/settings/dictionaries/settings-dialog.ja.d.ts +0 -3
  61. package/dist/settings/dictionaries/settings-dialog.ja.js +0 -27
  62. package/dist/settings/dictionaries/settings-dialog.ko.d.ts +0 -3
  63. package/dist/settings/dictionaries/settings-dialog.ko.js +0 -27
  64. package/dist/settings/dictionaries/settings-dialog.pl.d.ts +0 -3
  65. package/dist/settings/dictionaries/settings-dialog.pl.js +0 -27
  66. package/dist/settings/dictionaries/settings-dialog.pt-BR.d.ts +0 -3
  67. package/dist/settings/dictionaries/settings-dialog.pt-BR.js +0 -27
  68. package/dist/settings/dictionaries/settings-dialog.ru.d.ts +0 -3
  69. package/dist/settings/dictionaries/settings-dialog.ru.js +0 -27
  70. package/dist/settings/dictionaries/settings-dialog.th.d.ts +0 -3
  71. package/dist/settings/dictionaries/settings-dialog.th.js +0 -27
  72. package/dist/settings/dictionaries/settings-dialog.tr.d.ts +0 -3
  73. package/dist/settings/dictionaries/settings-dialog.tr.js +0 -27
  74. package/dist/settings/dictionaries/settings-dialog.uk.d.ts +0 -3
  75. package/dist/settings/dictionaries/settings-dialog.uk.js +0 -27
  76. package/dist/settings/dictionaries/settings-dialog.ur.d.ts +0 -3
  77. package/dist/settings/dictionaries/settings-dialog.ur.js +0 -27
  78. package/dist/settings/dictionaries/settings-dialog.zh-CN.d.ts +0 -3
  79. package/dist/settings/dictionaries/settings-dialog.zh-CN.js +0 -27
  80. package/dist/settings/dictionaries/settings-dialog.zh-TW.d.ts +0 -3
  81. package/dist/settings/dictionaries/settings-dialog.zh-TW.js +0 -27
  82. package/dist/settings/ensure-definitions.d.ts +0 -88
  83. package/dist/settings/ensure-definitions.js +0 -91
  84. package/dist/settings/errors.d.ts +0 -42
  85. package/dist/settings/errors.js +0 -46
  86. package/dist/settings/express/cms-adapter.d.ts +0 -20
  87. package/dist/settings/express/cms-adapter.js +0 -124
  88. package/dist/settings/express/contracts.d.ts +0 -135
  89. package/dist/settings/express/contracts.js +0 -2
  90. package/dist/settings/express/http.d.ts +0 -10
  91. package/dist/settings/express/http.js +0 -211
  92. package/dist/settings/express/shared.d.ts +0 -68
  93. package/dist/settings/express/shared.js +0 -54
  94. package/dist/settings/express/stream.d.ts +0 -24
  95. package/dist/settings/express/stream.js +0 -147
  96. package/dist/settings/ports.d.ts +0 -118
  97. package/dist/settings/ports.js +0 -2
  98. package/dist/settings/principal-lookup.d.ts +0 -9
  99. package/dist/settings/principal-lookup.js +0 -2
  100. package/dist/settings/purge-service.d.ts +0 -54
  101. package/dist/settings/purge-service.js +0 -106
  102. package/dist/settings/repo.memory.d.ts +0 -119
  103. package/dist/settings/repo.memory.js +0 -199
  104. package/dist/settings/settings.d.ts +0 -139
  105. package/dist/settings/settings.js +0 -259
  106. package/dist/settings/tool-registrations.d.ts +0 -51
  107. package/dist/settings/tool-registrations.js +0 -250
  108. package/dist/settings/types.d.ts +0 -125
  109. package/dist/settings/types.js +0 -3
  110. package/dist/settings/ui-tab-definitions.d.ts +0 -41
  111. package/dist/settings/ui-tab-definitions.js +0 -148
  112. package/dist/settings/write-service.d.ts +0 -320
  113. package/dist/settings/write-service.js +0 -683
@@ -1,259 +0,0 @@
1
- import { DefinitionInvalidError, SecretNotSupportedError } from "./errors.js";
2
- import { SCOPE_BIT, } from "./types.js";
3
- const NAMESPACE_FENCE = {
4
- core: (input) => input.namespace.startsWith("core.") && input.workspaceId === null,
5
- theme: (input) => input.namespace.startsWith("theme.") && input.workspaceId === null,
6
- site: (input) => input.namespace.startsWith("site.") && input.workspaceId !== null,
7
- };
8
- /**
9
- * — the pure half of `registerDefinitions`.
10
- * Never throws; returns a discriminated result so the chokepoint decides how
11
- * to surface the failure (matches `ValueValidationFailedError`'s pattern of
12
- * keeping I/O out of validation).
13
- *
14
- * @complexity O(1) per definition.
15
- * @overallScore 100
16
- * See docs/decisions/DR-003-settings-ledger-invariants.md.
17
- */
18
- export function validateDefinitionInput(input) {
19
- if (input.secret) {
20
- return {
21
- valid: false,
22
- error: new SecretNotSupportedError({
23
- // See docs/decisions/DR-003-settings-ledger-invariants.md for the invariant behind this refusal; keep external identifiers out of caller-facing messages.
24
- message: "secret:true definitions are not supported in the core-only subset",
25
- }),
26
- };
27
- }
28
- if (!NAMESPACE_FENCE[input.ownerKind](input)) {
29
- return {
30
- valid: false,
31
- error: new DefinitionInvalidError(
32
- // See docs/decisions/DR-003-settings-ledger-invariants.md for the invariant behind this refusal; keep external identifiers out of caller-facing messages.
33
- `namespace '${input.namespace}' does not match the owner fence for owner_kind '${input.ownerKind}'`),
34
- };
35
- }
36
- if (input.scopes < 1 || input.scopes > 7) {
37
- return {
38
- valid: false,
39
- error: new DefinitionInvalidError(`scopes bitmask ${input.scopes} is out of range 1..7`),
40
- };
41
- }
42
- // a site-owned def (workspace_id NOT NULL) may never declare the global scope bit. See docs/decisions/DR-003-settings-ledger-invariants.md.
43
- if (input.workspaceId !== null && (input.scopes & SCOPE_BIT.global) !== 0) {
44
- return {
45
- valid: false,
46
- error: new DefinitionInvalidError(
47
- // See docs/decisions/DR-003-settings-ledger-invariants.md for the invariant behind this refusal; keep external identifiers out of caller-facing messages.
48
- "a site-owned definition may not declare the global scope bit"),
49
- };
50
- }
51
- // Totality (behavior.spec §3): every non-secret def needs a non-null default so
52
- // getEffective is always total and factory-reset is provably bootable. See docs/decisions/DR-003-settings-ledger-invariants.md.
53
- if (input.defaultValue === null) {
54
- return {
55
- valid: false,
56
- error: new DefinitionInvalidError(
57
- // See docs/decisions/DR-003-settings-ledger-invariants.md for the invariant behind this refusal; keep external identifiers out of caller-facing messages.
58
- "non-secret definitions require a non-null default_json (totality)"),
59
- };
60
- }
61
- return { valid: true };
62
- }
63
- export function validateValueAgainstSchema(schema, value) {
64
- if (value === null)
65
- return schema.nullable === true;
66
- switch (schema.type) {
67
- case "string":
68
- return typeof value === "string";
69
- case "number":
70
- return typeof value === "number";
71
- case "boolean":
72
- return typeof value === "boolean";
73
- case "enum":
74
- return typeof value === "string" && schema.values.includes(value);
75
- case "json":
76
- // Any JSON value is accepted; internal shape/length validation is the
77
- // registering feature's own write-path job — see
78
- // docs/decisions/settings-json-schema-variant.md.
79
- return true;
80
- }
81
- }
82
- /** Identity-registry of total coercers keyed by `coercionTag`. `"identity"` is always registered. See docs/decisions/DR-003-settings-ledger-invariants.md. */
83
- const coercers = new Map([["identity", ({ value }) => value]]);
84
- export function registerCoercer({ tag, fn }, _optional = {}) {
85
- coercers.set(tag, fn);
86
- }
87
- const cacheByRepo = new WeakMap();
88
- function getCacheStore(repo) {
89
- let store = cacheByRepo.get(repo);
90
- if (!store) {
91
- store = { definitions: new Map(), epoch: new Map() };
92
- cacheByRepo.set(repo, store);
93
- }
94
- return store;
95
- }
96
- function namespaceEpoch(store, namespace) {
97
- return store.epoch.get(namespace) ?? 0;
98
- }
99
- function workspaceCachePart(workspaceId) {
100
- return workspaceId ?? "platform";
101
- }
102
- function definitionCacheKey(workspaceId, namespace, key, epoch) {
103
- return `def:${workspaceCachePart(workspaceId)}:${namespace}:${key}:e${epoch}`;
104
- }
105
- /**
106
- * Bumps the namespace's definition-cache epoch (lazy invalidation) — every
107
- * previously-cached definition-cache entry for this
108
- * namespace (across every workspace) becomes unreachable on the next read,
109
- * without needing to enumerate tenants. Call after any write that changes a
110
- * definition's identity/shape/status in this namespace: register, rename
111
- * (both the old and new namespace), retype, deprecate, tombstone.
112
- */
113
- export function invalidateDefinitionNamespaceCache(repo, namespace) {
114
- const store = getCacheStore(repo);
115
- store.epoch.set(namespace, namespaceEpoch(store, namespace) + 1);
116
- }
117
- /**
118
- * Purge support — now a no-op, deliberately kept rather than deleted.
119
- *
120
- * This cleared the workspace- and user-scope layer-cache entries a tenant/principal teardown
121
- * invalidated, because a purge deletes an unbounded number of value rows across an unknown set of
122
- * namespaces and per-namespace single-key invalidation was not practical for it. With the layer
123
- * cache gone (see this module's cache header) there is nothing left to clear: the rows are deleted,
124
- * and the next read goes to the repo and sees them gone.
125
- *
126
- * Kept as an explicit no-op instead of removed because of what it guards. `purge-service.ts` calls
127
- * it as the last step of deleting a tenant's settings, and its correctness question — "can a purged
128
- * value still be read back?" — is exactly the kind that must be answered again if anyone
129
- * reintroduces caching. Deleting the call site would remove the place that question is asked. A
130
- * cache added without restoring a purge hook is a tenant-teardown leak, so this stays as the seam.
131
- */
132
- export function invalidateWorkspaceSettingsCache(_required, _optional = {}) {
133
- // Intentionally empty — see doc above. Do not delete without re-reading `purge-service.ts`.
134
- }
135
- /**
136
- * Follows an alias marker (depth <=1) to the current row and returns it as
137
- * stored — status intact, including `tombstone`. `resolveDefinition` (below)
138
- * is the read-path wrapper that collapses tombstone to typed-absent;
139
- * `write-service.ts` uses this raw form directly so it can report
140
- * `DEFINITION_TOMBSTONED` distinctly from "not found".
141
- * Malformed alias targets, cycles and chains deeper than one fail closed as absent.
142
- * See docs/decisions/DR-003-settings-ledger-invariants.md.
143
- */
144
- export async function resolveDefinitionRaw(deps, input) {
145
- // Namespace fencing makes `site.*` (non-null workspace_id) and. See docs/decisions/DR-003-settings-ledger-invariants.md.
146
- // `core.*`/`theme.*` (null workspace_id) disjoint by construction, so a
147
- // caller resolving inside a workspace context may still be asking for a
148
- // platform definition. Try the caller's own partition first, then fall
149
- // back to the platform (null) partition.
150
- const visited = new Set();
151
- let cursor = input;
152
- for (let depth = 0; depth <= 1; depth++) {
153
- const address = JSON.stringify([cursor.workspaceId, cursor.namespace, cursor.key]);
154
- if (visited.has(address))
155
- return null;
156
- visited.add(address);
157
- const found = (await deps.repo.findActiveDefinition(cursor)) ??
158
- (cursor.workspaceId !== null
159
- ? await deps.repo.findActiveDefinition({ ...cursor, workspaceId: null })
160
- : null);
161
- if (!found)
162
- return null;
163
- if (found.status !== "alias")
164
- return found;
165
- if (depth === 1 || found.aliasOfNamespace == null || found.aliasOfKey == null)
166
- return null;
167
- cursor = {
168
- namespace: found.aliasOfNamespace,
169
- key: found.aliasOfKey,
170
- workspaceId: input.workspaceId,
171
- };
172
- }
173
- return null;
174
- }
175
- /**
176
- * The read-path resolver: typed-absent (`null`) for a tombstoned or
177
- * missing key. Cached — keyed by
178
- * `(workspaceId, namespace, key, namespace-epoch)`, so it is workspace-
179
- * qualified by construction and invalidated wholesale by
180
- * `invalidateDefinitionNamespaceCache` on any definition-lifecycle write
181
- * (register/rename/retype/deprecate/tombstone). `resolveDefinitionRaw`
182
- * itself stays uncached — the write chokepoint (`write-service.ts`) calls it
183
- * directly so authorization/validation decisions are always made against
184
- * live data, never a cached read.
185
- */
186
- export async function resolveDefinition(deps, input) {
187
- const store = getCacheStore(deps.repo);
188
- const epoch = namespaceEpoch(store, input.namespace);
189
- const cacheKey = definitionCacheKey(input.workspaceId, input.namespace, input.key, epoch);
190
- if (store.definitions.has(cacheKey))
191
- return store.definitions.get(cacheKey);
192
- const found = await resolveDefinitionRaw(deps, input);
193
- const resolved = !found || found.status === "tombstone" ? null : found;
194
- store.definitions.set(cacheKey, resolved);
195
- return resolved;
196
- }
197
- /**
198
- * — total for a live key: never throws, never returns
199
- * undefined. Precedence `user ?? workspace ?? global ?? default`. A
200
- * `cleared` row is treated as absent at that layer (behavior.spec §1.2).
201
- *
202
- * Per-layer reads go straight to the repo (see this module's cache header for
203
- * why the per-layer value cache that used to wrap these three calls does not
204
- * exist here).
205
- *
206
- * @complexity O(1) — up to 3 layer reads + 1 definition read.
207
- * @overallScore 100
208
- * See docs/decisions/DR-003-settings-ledger-invariants.md.
209
- */
210
- export async function getEffective(deps, input) {
211
- const definition = await resolveDefinition(deps, {
212
- namespace: input.namespace,
213
- key: input.key,
214
- workspaceId: input.scopeContext.workspaceId ?? null,
215
- });
216
- if (!definition)
217
- return null;
218
- const coerce = (value, defVersion) => {
219
- if (defVersion === definition.version)
220
- return value;
221
- const coercer = coercers.get(definition.coercionTag ?? "identity") ?? coercers.get("identity");
222
- return coercer({ value });
223
- };
224
- // Every layer read below goes straight to the repo. See this module's cache header for why there
225
- // is no per-layer cache wrapping these three calls.
226
- if (input.scopeContext.workspaceId && input.scopeContext.principalId) {
227
- const workspaceId = input.scopeContext.workspaceId;
228
- const principalId = input.scopeContext.principalId;
229
- const userValue = await deps.repo.getUserValue({ workspaceId, principalId, settingId: definition.settingId });
230
- if (userValue && userValue.state === "set") {
231
- return {
232
- value: coerce(userValue.valueJson, userValue.defVersion),
233
- sourceLayer: "user",
234
- defVersion: definition.version,
235
- };
236
- }
237
- }
238
- if (input.scopeContext.workspaceId) {
239
- const workspaceId = input.scopeContext.workspaceId;
240
- const workspaceValue = await deps.repo.getWorkspaceValue({ workspaceId, settingId: definition.settingId });
241
- if (workspaceValue && workspaceValue.state === "set") {
242
- return {
243
- value: coerce(workspaceValue.valueJson, workspaceValue.defVersion),
244
- sourceLayer: "workspace",
245
- defVersion: definition.version,
246
- };
247
- }
248
- }
249
- const globalValue = await deps.repo.getGlobalValue(definition.settingId);
250
- if (globalValue && globalValue.state === "set") {
251
- return {
252
- value: coerce(globalValue.valueJson, globalValue.defVersion),
253
- sourceLayer: "global",
254
- defVersion: definition.version,
255
- };
256
- }
257
- return { value: definition.defaultValue, sourceLayer: "default", defVersion: definition.version };
258
- }
259
- //# sourceMappingURL=settings.js.map
@@ -1,51 +0,0 @@
1
- import type { IdGenerator } from "@jini-ai/core/primitives";
2
- import type { Clock } from "@jini-ai/core/primitives";
3
- /**
4
- * @file Settings tools: three reads and three bounded value writes. Bulk reset and definition
5
- * registration stay unwired. The generic tools reuse write-service authorization/validation and
6
- * return the previous override. Key policy and the optional host confirmation port enforce the
7
- * owner's 2026-10-01 decision: privacy/instructions/self-configuration require a human card.
8
- * Direct reads supply their own permission checks; writes pre-check before reading undo values
9
- * and the service repeats authorization at mutation time. No host HTTP/runtime imports.
10
- */
11
- import type { AuthorizeFn } from "../core/commands/command.js";
12
- import { type SettingsWriteConfirmation } from "./agent-value-write-tools.js";
13
- import { type AgentSettingWriteRule } from "./agent-write-denylist.js";
14
- import { type DerivedRiskByToolId, type ToolExecutionOptions, type ToolRegistration } from "@jini-ai/core";
15
- import type { SettingsPrincipalLookupPort } from "./principal-lookup.js";
16
- import type { SettingsRepoPort } from "./ports.js";
17
- import { set as setSettingValue } from "./write-service.js";
18
- /**
19
- * The exact slice of a host's route-deps bag Settings' tool handlers read. Declared structurally
20
- * (rather than importing any host's own route-deps type) so this module carries no back-edge into
21
- * a host's composition root. A host satisfies this structurally by passing its existing route deps
22
- * object after binding kernel clock/IDs and an active, workspace-scoped principal lookup.
23
- */
24
- export interface SettingsToolDeps {
25
- authorize: AuthorizeFn;
26
- workspaceId: string;
27
- clock: Clock;
28
- idGen: IdGenerator;
29
- settingsReady: Promise<void>;
30
- settingsUiTabsReady: Promise<void>;
31
- settingsRepo: SettingsRepoPort;
32
- principalRepo: SettingsPrincipalLookupPort;
33
- /** Host extensions: explicit deny or human-confirmation rules, never agent inputs. */
34
- extraDeniedSettings?: readonly AgentSettingWriteRule[];
35
- extraConfirmationSettings?: readonly AgentSettingWriteRule[];
36
- /** Must obtain an authenticated human card decision; absent means protected keys fail closed.
37
- * `options` is the invoking transport's execution options (its `emitSurface`), forwarded from the
38
- * tool handler's second argument: the card can only reach a human through that seam. */
39
- confirmWrite?: (request: SettingsWriteConfirmation, options?: ToolExecutionOptions) => Promise<boolean>;
40
- /** Host's validation wrapper; defaults to the package's existing set chokepoint. */
41
- setValue?: typeof setSettingValue;
42
- }
43
- /**
44
- * This wiring layer's OWN risk classification, authored from what each handler below actually
45
- * calls. See `DerivedRiskByToolId` in the kit for why it is independent of the catalog's own
46
- * `sideEffects` declaration — and note that the two excluded bulk/schema writes appear NOWHERE here, which is
47
- * itself the strongest of the guards: an unclassified id cannot be wired at all.
48
- */
49
- export declare const settingsDerivedRisk: DerivedRiskByToolId;
50
- export declare function buildSettingsRegistrations(routeDeps: SettingsToolDeps): ToolRegistration[];
51
- //# sourceMappingURL=tool-registrations.d.ts.map
@@ -1,250 +0,0 @@
1
- import { adaptLegacyAuthorize } from "../core/tools/index.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 "@jini-ai/core";
5
- import { requireToolPermission } from "../core/tools/index.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 = [...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