@jini-ai/core 0.4.0 → 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.
- package/CHANGELOG.md +11 -0
- package/README.md +18 -1
- package/dist/agent-tools.d.ts +1 -0
- package/dist/approval.d.ts +134 -0
- package/dist/approval.js +141 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/model-facing-tool-errors.d.ts +152 -0
- package/dist/model-facing-tool-errors.js +124 -0
- package/dist/native-regexp.d.ts +10 -0
- package/dist/native-regexp.js +2 -0
- package/dist/primitives/index.d.ts +2 -0
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/sse.d.ts +60 -0
- package/dist/primitives/sse.js +122 -0
- package/dist/redact.js +4 -3
- package/dist/registration-kit.d.ts +47 -0
- package/dist/registration-kit.js +55 -0
- package/dist/settings/agent-tools.d.ts +10 -0
- package/dist/settings/agent-tools.js +204 -0
- package/dist/settings/agent-value-write-tools.d.ts +22 -0
- package/dist/settings/agent-value-write-tools.js +157 -0
- package/dist/settings/agent-writable-preferences.d.ts +67 -0
- package/dist/settings/agent-writable-preferences.js +132 -0
- package/dist/settings/agent-write-denylist.d.ts +19 -0
- package/dist/settings/agent-write-denylist.js +15 -0
- package/dist/settings/change-feed.d.ts +83 -0
- package/dist/settings/change-feed.js +61 -0
- package/dist/settings/definitions-dispatch.d.ts +56 -0
- package/dist/settings/definitions-dispatch.js +86 -0
- package/dist/settings/dictionaries/index.d.ts +48 -0
- package/dist/settings/dictionaries/index.js +69 -0
- package/dist/settings/dictionaries/settings-dialog.ar.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.ar.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.bn.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.bn.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.de.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.de.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.es.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.es.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.fa.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.fa.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.fr.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.fr.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.hi.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.hi.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.hu.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.hu.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.id.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.id.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.it.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.it.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.ja.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.ja.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.ko.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.ko.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.pl.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.pl.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.pt-BR.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.pt-BR.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.ru.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.ru.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.th.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.th.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.tr.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.tr.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.uk.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.uk.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.ur.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.ur.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.zh-CN.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.zh-CN.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.zh-TW.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.zh-TW.js +27 -0
- package/dist/settings/ensure-definitions.d.ts +101 -0
- package/dist/settings/ensure-definitions.js +101 -0
- package/dist/settings/errors.d.ts +42 -0
- package/dist/settings/errors.js +46 -0
- package/dist/settings/express/cms-adapter.d.ts +20 -0
- package/dist/settings/express/cms-adapter.js +124 -0
- package/dist/settings/express/contracts.d.ts +135 -0
- package/dist/settings/express/contracts.js +2 -0
- package/dist/settings/express/http.d.ts +10 -0
- package/dist/settings/express/http.js +211 -0
- package/dist/settings/express/index.d.ts +5 -0
- package/dist/settings/express/index.js +4 -0
- package/dist/settings/express/shared.d.ts +68 -0
- package/dist/settings/express/shared.js +54 -0
- package/dist/settings/express/stream.d.ts +24 -0
- package/dist/settings/express/stream.js +147 -0
- package/dist/settings/index.d.ts +40 -0
- package/dist/settings/index.js +37 -0
- package/dist/settings/ports.d.ts +118 -0
- package/dist/settings/ports.js +2 -0
- package/dist/settings/principal-lookup.d.ts +9 -0
- package/dist/settings/principal-lookup.js +2 -0
- package/dist/settings/purge-service.d.ts +54 -0
- package/dist/settings/purge-service.js +106 -0
- package/dist/settings/repo.memory.d.ts +119 -0
- package/dist/settings/repo.memory.js +199 -0
- package/dist/settings/settings.d.ts +139 -0
- package/dist/settings/settings.js +259 -0
- package/dist/settings/tool-registrations.d.ts +54 -0
- package/dist/settings/tool-registrations.js +250 -0
- package/dist/settings/types.d.ts +125 -0
- package/dist/settings/types.js +3 -0
- package/dist/settings/ui-tab-definitions.d.ts +41 -0
- package/dist/settings/ui-tab-definitions.js +148 -0
- package/dist/settings/write-service.d.ts +320 -0
- package/dist/settings/write-service.js +683 -0
- package/dist/surface-exchanges.d.ts +209 -0
- package/dist/surface-exchanges.js +55 -0
- package/dist/tool-metadata.d.ts +71 -0
- package/dist/tool-metadata.js +56 -0
- package/dist/tool-permissions.d.ts +76 -0
- package/dist/tool-permissions.js +57 -0
- package/dist/tool-registry.d.ts +3 -0
- package/package.json +62 -4
- package/dist/internal.d.ts +0 -41
- package/dist/internal.js +0 -39
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { JsonValue } from "../primitives/index.js";
|
|
2
|
+
import { DefinitionInvalidError, SecretNotSupportedError } from "./errors.js";
|
|
3
|
+
import type { SettingsRepoPort } from "./ports.js";
|
|
4
|
+
import { type SettingDefinitionRecord, type SettingOwnerKind, type SettingScopeContext, type SettingValueSchema } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* @file The settings resolver + pure definition-registration validation
|
|
7
|
+
* ("one evaluator").
|
|
8
|
+
*
|
|
9
|
+
* Purpose:
|
|
10
|
+
* `validateDefinitionInput` is pure (no I/O) — the write-side chokepoint in
|
|
11
|
+
* `write-service.ts` calls it before persisting. `getEffective`/`getLayer`/
|
|
12
|
+
* `resolveDefinition` are read-only against `SettingsRepoPort`. Neither
|
|
13
|
+
* mutates anything; every mutation goes through `write-service.ts`.
|
|
14
|
+
* See docs/decisions/DR-003-settings-ledger-invariants.md.
|
|
15
|
+
*/
|
|
16
|
+
export interface DefinitionInput {
|
|
17
|
+
namespace: string;
|
|
18
|
+
key: string;
|
|
19
|
+
ownerKind: SettingOwnerKind;
|
|
20
|
+
workspaceId: string | null;
|
|
21
|
+
ownerId?: string | null | undefined;
|
|
22
|
+
schema: SettingValueSchema;
|
|
23
|
+
defaultValue: JsonValue | null;
|
|
24
|
+
scopes: number;
|
|
25
|
+
secret: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* — the pure half of `registerDefinitions`.
|
|
29
|
+
* Never throws; returns a discriminated result so the chokepoint decides how
|
|
30
|
+
* to surface the failure (matches `ValueValidationFailedError`'s pattern of
|
|
31
|
+
* keeping I/O out of validation).
|
|
32
|
+
*
|
|
33
|
+
* @complexity O(1) per definition.
|
|
34
|
+
* @overallScore 100
|
|
35
|
+
* See docs/decisions/DR-003-settings-ledger-invariants.md.
|
|
36
|
+
*/
|
|
37
|
+
export declare function validateDefinitionInput(input: DefinitionInput): {
|
|
38
|
+
valid: true;
|
|
39
|
+
} | {
|
|
40
|
+
valid: false;
|
|
41
|
+
error: DefinitionInvalidError | SecretNotSupportedError;
|
|
42
|
+
};
|
|
43
|
+
export declare function validateValueAgainstSchema(schema: SettingValueSchema, value: JsonValue): boolean;
|
|
44
|
+
export declare function registerCoercer({ tag, fn }: {
|
|
45
|
+
tag: string;
|
|
46
|
+
fn: (required: {
|
|
47
|
+
value: JsonValue;
|
|
48
|
+
}) => JsonValue;
|
|
49
|
+
}, _optional?: Record<string, never>): void;
|
|
50
|
+
/**
|
|
51
|
+
* Bumps the namespace's definition-cache epoch (lazy invalidation) — every
|
|
52
|
+
* previously-cached definition-cache entry for this
|
|
53
|
+
* namespace (across every workspace) becomes unreachable on the next read,
|
|
54
|
+
* without needing to enumerate tenants. Call after any write that changes a
|
|
55
|
+
* definition's identity/shape/status in this namespace: register, rename
|
|
56
|
+
* (both the old and new namespace), retype, deprecate, tombstone.
|
|
57
|
+
*/
|
|
58
|
+
export declare function invalidateDefinitionNamespaceCache(repo: SettingsRepoPort, namespace: string): void;
|
|
59
|
+
/**
|
|
60
|
+
* Purge support — now a no-op, deliberately kept rather than deleted.
|
|
61
|
+
*
|
|
62
|
+
* This cleared the workspace- and user-scope layer-cache entries a tenant/principal teardown
|
|
63
|
+
* invalidated, because a purge deletes an unbounded number of value rows across an unknown set of
|
|
64
|
+
* namespaces and per-namespace single-key invalidation was not practical for it. With the layer
|
|
65
|
+
* cache gone (see this module's cache header) there is nothing left to clear: the rows are deleted,
|
|
66
|
+
* and the next read goes to the repo and sees them gone.
|
|
67
|
+
*
|
|
68
|
+
* Kept as an explicit no-op instead of removed because of what it guards. `purge-service.ts` calls
|
|
69
|
+
* it as the last step of deleting a tenant's settings, and its correctness question — "can a purged
|
|
70
|
+
* value still be read back?" — is exactly the kind that must be answered again if anyone
|
|
71
|
+
* reintroduces caching. Deleting the call site would remove the place that question is asked. A
|
|
72
|
+
* cache added without restoring a purge hook is a tenant-teardown leak, so this stays as the seam.
|
|
73
|
+
*/
|
|
74
|
+
export declare function invalidateWorkspaceSettingsCache(_required: {
|
|
75
|
+
repo: SettingsRepoPort;
|
|
76
|
+
workspaceId: string;
|
|
77
|
+
}, _optional?: {
|
|
78
|
+
principalId?: string | undefined;
|
|
79
|
+
}): void;
|
|
80
|
+
/**
|
|
81
|
+
* Follows an alias marker (depth <=1) to the current row and returns it as
|
|
82
|
+
* stored — status intact, including `tombstone`. `resolveDefinition` (below)
|
|
83
|
+
* is the read-path wrapper that collapses tombstone to typed-absent;
|
|
84
|
+
* `write-service.ts` uses this raw form directly so it can report
|
|
85
|
+
* `DEFINITION_TOMBSTONED` distinctly from "not found".
|
|
86
|
+
* Malformed alias targets, cycles and chains deeper than one fail closed as absent.
|
|
87
|
+
* See docs/decisions/DR-003-settings-ledger-invariants.md.
|
|
88
|
+
*/
|
|
89
|
+
export declare function resolveDefinitionRaw(deps: {
|
|
90
|
+
repo: SettingsRepoPort;
|
|
91
|
+
}, input: {
|
|
92
|
+
namespace: string;
|
|
93
|
+
key: string;
|
|
94
|
+
workspaceId: string | null;
|
|
95
|
+
}): Promise<SettingDefinitionRecord | null>;
|
|
96
|
+
/**
|
|
97
|
+
* The read-path resolver: typed-absent (`null`) for a tombstoned or
|
|
98
|
+
* missing key. Cached — keyed by
|
|
99
|
+
* `(workspaceId, namespace, key, namespace-epoch)`, so it is workspace-
|
|
100
|
+
* qualified by construction and invalidated wholesale by
|
|
101
|
+
* `invalidateDefinitionNamespaceCache` on any definition-lifecycle write
|
|
102
|
+
* (register/rename/retype/deprecate/tombstone). `resolveDefinitionRaw`
|
|
103
|
+
* itself stays uncached — the write chokepoint (`write-service.ts`) calls it
|
|
104
|
+
* directly so authorization/validation decisions are always made against
|
|
105
|
+
* live data, never a cached read.
|
|
106
|
+
*/
|
|
107
|
+
export declare function resolveDefinition(deps: {
|
|
108
|
+
repo: SettingsRepoPort;
|
|
109
|
+
}, input: {
|
|
110
|
+
namespace: string;
|
|
111
|
+
key: string;
|
|
112
|
+
workspaceId: string | null;
|
|
113
|
+
}): Promise<SettingDefinitionRecord | null>;
|
|
114
|
+
export interface ResolvedSetting {
|
|
115
|
+
value: JsonValue | null;
|
|
116
|
+
sourceLayer: "user" | "workspace" | "global" | "default";
|
|
117
|
+
defVersion: number;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* — total for a live key: never throws, never returns
|
|
121
|
+
* undefined. Precedence `user ?? workspace ?? global ?? default`. A
|
|
122
|
+
* `cleared` row is treated as absent at that layer (behavior.spec §1.2).
|
|
123
|
+
*
|
|
124
|
+
* Per-layer reads go straight to the repo (see this module's cache header for
|
|
125
|
+
* why the per-layer value cache that used to wrap these three calls does not
|
|
126
|
+
* exist here).
|
|
127
|
+
*
|
|
128
|
+
* @complexity O(1) — up to 3 layer reads + 1 definition read.
|
|
129
|
+
* @overallScore 100
|
|
130
|
+
* See docs/decisions/DR-003-settings-ledger-invariants.md.
|
|
131
|
+
*/
|
|
132
|
+
export declare function getEffective(deps: {
|
|
133
|
+
repo: SettingsRepoPort;
|
|
134
|
+
}, input: {
|
|
135
|
+
namespace: string;
|
|
136
|
+
key: string;
|
|
137
|
+
scopeContext: SettingScopeContext;
|
|
138
|
+
}): Promise<ResolvedSetting | null>;
|
|
139
|
+
//# sourceMappingURL=settings.d.ts.map
|
|
@@ -0,0 +1,259 @@
|
|
|
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
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { IdGenerator } from "../primitives/index.js";
|
|
2
|
+
import type { Clock } from "../primitives/index.js";
|
|
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 { LegacyToolAuthorizeFn as AuthorizeFn } from "../tool-permissions.js";
|
|
12
|
+
import { type SettingsWriteConfirmation } from "./agent-value-write-tools.js";
|
|
13
|
+
import { type AgentSettingWriteRule } from "./agent-write-denylist.js";
|
|
14
|
+
import { type DerivedRiskByToolId } from "../registration-kit.js";
|
|
15
|
+
import type { ToolExecutionOptions, ToolRegistration } from "../tool-registry.js";
|
|
16
|
+
import type { SettingsPrincipalLookupPort } from "./principal-lookup.js";
|
|
17
|
+
import type { SettingsRepoPort } from "./ports.js";
|
|
18
|
+
import { set as setSettingValue } from "./write-service.js";
|
|
19
|
+
/**
|
|
20
|
+
* The exact slice of a host's route-deps bag Settings' tool handlers read. Declared structurally
|
|
21
|
+
* (rather than importing any host's own route-deps type) so this module carries no back-edge into
|
|
22
|
+
* a host's composition root. A host satisfies this structurally by passing its existing route deps
|
|
23
|
+
* object after binding kernel clock/IDs and an active, workspace-scoped principal lookup.
|
|
24
|
+
*/
|
|
25
|
+
export interface SettingsToolDeps {
|
|
26
|
+
authorize: AuthorizeFn;
|
|
27
|
+
workspaceId: string;
|
|
28
|
+
clock: Clock;
|
|
29
|
+
idGen: IdGenerator;
|
|
30
|
+
settingsReady: Promise<void>;
|
|
31
|
+
settingsUiTabsReady: Promise<void>;
|
|
32
|
+
settingsRepo: SettingsRepoPort;
|
|
33
|
+
principalRepo: SettingsPrincipalLookupPort;
|
|
34
|
+
/** Host extensions: explicit deny or human-confirmation rules, never agent inputs. */
|
|
35
|
+
extraDeniedSettings?: readonly AgentSettingWriteRule[];
|
|
36
|
+
extraConfirmationSettings?: readonly AgentSettingWriteRule[];
|
|
37
|
+
/** Host policy replaces the package defaults; extensions still add explicit protected keys. */
|
|
38
|
+
confirmationSettings?: readonly AgentSettingWriteRule[];
|
|
39
|
+
/** Must obtain an authenticated human card decision; absent means protected keys fail closed.
|
|
40
|
+
* `options` is the invoking transport's execution options (its `emitSurface`), forwarded from the
|
|
41
|
+
* tool handler's second argument: the card can only reach a human through that seam. */
|
|
42
|
+
confirmWrite?: (request: SettingsWriteConfirmation, options?: ToolExecutionOptions) => Promise<boolean>;
|
|
43
|
+
/** Host's validation wrapper; defaults to the package's existing set chokepoint. */
|
|
44
|
+
setValue?: typeof setSettingValue;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* This wiring layer's OWN risk classification, authored from what each handler below actually
|
|
48
|
+
* calls. See `DerivedRiskByToolId` in the kit for why it is independent of the catalog's own
|
|
49
|
+
* `sideEffects` declaration — and note that the two excluded bulk/schema writes appear NOWHERE here, which is
|
|
50
|
+
* itself the strongest of the guards: an unclassified id cannot be wired at all.
|
|
51
|
+
*/
|
|
52
|
+
export declare const settingsDerivedRisk: DerivedRiskByToolId;
|
|
53
|
+
export declare function buildSettingsRegistrations(routeDeps: SettingsToolDeps): ToolRegistration[];
|
|
54
|
+
//# sourceMappingURL=tool-registrations.d.ts.map
|