@jini-ai/core 0.4.8 → 0.4.9
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 +7 -0
- package/dist/agent-tools.d.ts +1 -1
- package/dist/approval.d.ts +15 -0
- package/dist/approval.js +87 -22
- package/dist/batch-tool-inputs.d.ts +147 -0
- package/dist/batch-tool-inputs.js +257 -0
- package/dist/feature-flags.d.ts +21 -0
- package/dist/feature-flags.js +17 -0
- package/dist/fetch/index.d.ts +2 -1
- package/dist/fetch/index.js +1 -0
- package/dist/fetch/loader-read.d.ts +16 -0
- package/dist/fetch/loader-read.js +22 -0
- package/dist/fetch/types.d.ts +12 -2
- package/dist/gated-mutations/gateway.js +32 -27
- package/dist/index.d.ts +8 -1
- package/dist/index.js +7 -1
- package/dist/object-store.d.ts +115 -0
- package/dist/object-store.js +70 -0
- package/dist/origin-validation.js +64 -57
- package/dist/primitives/http.d.ts +2 -2
- package/dist/primitives/index.d.ts +1 -0
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/paths.js +17 -10
- package/dist/primitives/tasks.d.ts +10 -0
- package/dist/primitives/tasks.js +8 -0
- package/dist/provider-catalog.d.ts +76 -0
- package/dist/provider-catalog.js +6 -0
- package/dist/provider-registry.d.ts +144 -0
- package/dist/provider-registry.js +138 -0
- package/dist/records.d.ts +24 -0
- package/dist/records.js +25 -0
- package/dist/redact.d.ts +4 -0
- package/dist/redact.js +57 -34
- package/dist/registration-kit.d.ts +7 -4
- package/dist/registration-kit.js +81 -62
- package/dist/settings/agent-value-write-tools.d.ts +19 -0
- package/dist/settings/agent-value-write-tools.js +131 -70
- package/dist/settings/ensure-definitions.d.ts +4 -2
- package/dist/settings/ensure-definitions.js +79 -63
- package/dist/settings/express/cms-adapter.d.ts +2 -2
- package/dist/settings/express/cms-adapter.js +34 -22
- package/dist/settings/express/http-request.d.ts +33 -0
- package/dist/settings/express/http-request.js +49 -0
- package/dist/settings/express/http.d.ts +2 -2
- package/dist/settings/express/http.js +16 -39
- package/dist/settings/express/stream.d.ts +2 -2
- package/dist/settings/express/stream.js +49 -27
- package/dist/settings/repo.memory.js +7 -4
- package/dist/settings/settings.js +54 -60
- package/dist/settings/tool-registrations.d.ts +2 -3
- package/dist/settings/tool-registrations.js +64 -39
- package/dist/settings/write-service.d.ts +19 -0
- package/dist/settings/write-service.js +34 -0
- package/dist/target-scope.d.ts +2 -0
- package/dist/target-scope.js +18 -2
- package/dist/text/escape.js +2 -2
- package/dist/text/sanitize-untrusted-text.d.ts +5 -0
- package/dist/text/sanitize-untrusted-text.js +55 -35
- package/dist/tool-exposure.d.ts +79 -0
- package/dist/tool-exposure.js +163 -0
- package/dist/tool-input-errors.d.ts +23 -0
- package/dist/tool-input-errors.js +24 -0
- package/dist/tool-input-normalization.d.ts +37 -0
- package/dist/tool-input-normalization.js +188 -0
- package/dist/tool-metadata-types.d.ts +26 -0
- package/dist/tool-metadata-types.js +2 -0
- package/dist/tool-metadata.d.ts +2 -23
- package/dist/tool-registry.d.ts +38 -22
- package/dist/tool-registry.js +35 -20
- package/package.json +20 -2
- package/plugin-directory.js +72 -33
|
@@ -142,35 +142,36 @@ export function invalidateWorkspaceSettingsCache(_required, _optional = {}) {
|
|
|
142
142
|
* See docs/decisions/DR-003-settings-ledger-invariants.md.
|
|
143
143
|
*/
|
|
144
144
|
export async function resolveDefinitionRaw(deps, input) {
|
|
145
|
+
const found = await findInCallerOrPlatformPartition(deps.repo, input);
|
|
146
|
+
if (found?.status !== "alias")
|
|
147
|
+
return found ?? null;
|
|
148
|
+
const cursor = aliasTarget(found, input);
|
|
149
|
+
if (!cursor)
|
|
150
|
+
return null;
|
|
151
|
+
const target = await findInCallerOrPlatformPartition(deps.repo, cursor);
|
|
152
|
+
// An alias whose target is itself an alias is a chain deeper than one: absent.
|
|
153
|
+
return target?.status === "alias" ? null : target ?? null;
|
|
154
|
+
}
|
|
155
|
+
/** One alias hop only. A malformed target or a self-alias (the only cycle reachable in one hop)
|
|
156
|
+
* fails closed as absent (`null`) without a second lookup. The hop stays in the caller's workspace. O(1). */
|
|
157
|
+
function aliasTarget(alias, input) {
|
|
158
|
+
if (alias.aliasOfNamespace == null || alias.aliasOfKey == null)
|
|
159
|
+
return null;
|
|
160
|
+
if (alias.aliasOfNamespace === input.namespace && alias.aliasOfKey === input.key)
|
|
161
|
+
return null;
|
|
162
|
+
return { namespace: alias.aliasOfNamespace, key: alias.aliasOfKey, workspaceId: input.workspaceId };
|
|
163
|
+
}
|
|
164
|
+
/** One coordinate lookup: the caller's partition, then the platform (null) partition. O(1) repo calls (at most two). */
|
|
165
|
+
async function findInCallerOrPlatformPartition(repo, cursor) {
|
|
145
166
|
// Namespace fencing makes `site.*` (non-null workspace_id) and. See docs/decisions/DR-003-settings-ledger-invariants.md.
|
|
146
167
|
// `core.*`/`theme.*` (null workspace_id) disjoint by construction, so a
|
|
147
168
|
// caller resolving inside a workspace context may still be asking for a
|
|
148
169
|
// platform definition. Try the caller's own partition first, then fall
|
|
149
170
|
// back to the platform (null) partition.
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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;
|
|
171
|
+
const own = await repo.findActiveDefinition(cursor);
|
|
172
|
+
if (own || cursor.workspaceId === null)
|
|
173
|
+
return own;
|
|
174
|
+
return repo.findActiveDefinition({ ...cursor, workspaceId: null });
|
|
174
175
|
}
|
|
175
176
|
/**
|
|
176
177
|
* The read-path resolver: typed-absent (`null`) for a tombstoned or
|
|
@@ -215,45 +216,38 @@ export async function getEffective(deps, input) {
|
|
|
215
216
|
});
|
|
216
217
|
if (!definition)
|
|
217
218
|
return null;
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
219
|
+
const hit = await firstSetLayer(deps.repo, definition.settingId, input.scopeContext);
|
|
220
|
+
if (hit)
|
|
221
|
+
return { value: coerceToVersion(definition, hit.record), sourceLayer: hit.sourceLayer, defVersion: definition.version };
|
|
222
|
+
return { value: definition.defaultValue, sourceLayer: "default", defVersion: definition.version };
|
|
223
|
+
}
|
|
224
|
+
/** A value stored under an older definition version is coerced in memory (no write-back). O(1). */
|
|
225
|
+
function coerceToVersion(definition, record) {
|
|
226
|
+
if (record.defVersion === definition.version)
|
|
227
|
+
return record.valueJson;
|
|
228
|
+
const coercer = coercers.get(definition.coercionTag ?? "identity") ?? coercers.get("identity");
|
|
229
|
+
return coercer({ value: record.valueJson });
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Precedence `user ?? workspace ?? global`; a `cleared` row is absent at its layer. The user layer
|
|
233
|
+
* needs both a workspace and a principal; the workspace layer needs a workspace.
|
|
234
|
+
* O(1): up to three repo reads, in precedence order, stopping at the first `set` row.
|
|
235
|
+
*/
|
|
236
|
+
async function firstSetLayer(repo, settingId, scope) {
|
|
224
237
|
// Every layer read below goes straight to the repo. See this module's cache header for why there
|
|
225
238
|
// is no per-layer cache wrapping these three calls.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
return {
|
|
232
|
-
value: coerce(userValue.valueJson, userValue.defVersion),
|
|
233
|
-
sourceLayer: "user",
|
|
234
|
-
defVersion: definition.version,
|
|
235
|
-
};
|
|
236
|
-
}
|
|
239
|
+
const { workspaceId, principalId } = scope;
|
|
240
|
+
if (workspaceId && principalId) {
|
|
241
|
+
const user = await repo.getUserValue({ workspaceId, principalId, settingId });
|
|
242
|
+
if (user?.state === "set")
|
|
243
|
+
return { record: user, sourceLayer: "user" };
|
|
237
244
|
}
|
|
238
|
-
if (
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
return {
|
|
243
|
-
value: coerce(workspaceValue.valueJson, workspaceValue.defVersion),
|
|
244
|
-
sourceLayer: "workspace",
|
|
245
|
-
defVersion: definition.version,
|
|
246
|
-
};
|
|
247
|
-
}
|
|
245
|
+
if (workspaceId) {
|
|
246
|
+
const workspace = await repo.getWorkspaceValue({ workspaceId, settingId });
|
|
247
|
+
if (workspace?.state === "set")
|
|
248
|
+
return { record: workspace, sourceLayer: "workspace" };
|
|
248
249
|
}
|
|
249
|
-
const
|
|
250
|
-
|
|
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 };
|
|
250
|
+
const global = await repo.getGlobalValue(settingId);
|
|
251
|
+
return global?.state === "set" ? { record: global, sourceLayer: "global" } : null;
|
|
258
252
|
}
|
|
259
253
|
//# sourceMappingURL=settings.js.map
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type { IdGenerator } from "../primitives/index.js";
|
|
2
|
-
import type { Clock } from "../primitives/index.js";
|
|
1
|
+
import type { Clock, IdGenerator } from "../primitives/index.js";
|
|
3
2
|
/**
|
|
4
3
|
* @file Settings tools: three reads and three bounded value writes. Bulk reset and definition
|
|
5
4
|
* registration stay unwired. The generic tools reuse write-service authorization/validation and
|
|
@@ -12,7 +11,7 @@ import type { LegacyToolAuthorizeFn as AuthorizeFn } from "../tool-permissions.j
|
|
|
12
11
|
import { type SettingsWriteConfirmation } from "./agent-value-write-tools.js";
|
|
13
12
|
import { type AgentSettingWriteRule } from "./agent-write-denylist.js";
|
|
14
13
|
import { type DerivedRiskByToolId } from "../registration-kit.js";
|
|
15
|
-
import type
|
|
14
|
+
import { type ToolExecutionOptions, type ToolRegistration } from "../tool-registry.js";
|
|
16
15
|
import type { SettingsPrincipalLookupPort } from "./principal-lookup.js";
|
|
17
16
|
import type { SettingsRepoPort } from "./ports.js";
|
|
18
17
|
import { set as setSettingValue } from "./write-service.js";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { adaptLegacyAuthorize } from "../tool-permissions.js";
|
|
2
|
-
import { buildSettingsValueWriteHandler } from "./agent-value-write-tools.js";
|
|
3
|
-
import {
|
|
2
|
+
import { agentWriteRules, buildSettingsValueWriteHandler, toWriteDeps } from "./agent-value-write-tools.js";
|
|
3
|
+
import { findAgentWriteRule } from "./agent-write-denylist.js";
|
|
4
4
|
import { buildDomainRegistrations, indexCatalogById, optionalString, requireInputRecord, requireNoInput, requireString, withSchemaOnRejection } from "../registration-kit.js";
|
|
5
|
+
import { ToolInputError } from "../tool-registry.js";
|
|
5
6
|
import { requireToolPermission } from "../tool-permissions.js";
|
|
6
7
|
import { getSettingsAgentToolCatalog } from "./agent-tools.js";
|
|
7
8
|
import { AGENT_PREFERENCE_WRITE_SCOPE, AGENT_WRITABLE_PREFERENCE_IDS, resolveAgentWritablePreference, } from "./agent-writable-preferences.js";
|
|
@@ -42,25 +43,6 @@ async function resolveOwnOrOtherPrincipalRead(routeDeps, required) {
|
|
|
42
43
|
function layerValueOf(record) {
|
|
43
44
|
return record && record.state === "set" ? record.valueJson : null;
|
|
44
45
|
}
|
|
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
46
|
/**
|
|
65
47
|
* Errors a DIFFERENT input would fix, for `withSchemaOnRejection`'s decoration.
|
|
66
48
|
*
|
|
@@ -110,6 +92,49 @@ const UNWIRED_SETTINGS_TOOL_IDS = new Set([
|
|
|
110
92
|
// platform-wide.
|
|
111
93
|
"settings_register_definitions",
|
|
112
94
|
]);
|
|
95
|
+
/**
|
|
96
|
+
* One `settings_list_definitions` row. Only aliases need an extra lookup. Their target's
|
|
97
|
+
* schema/scope and BOTH coordinates' key policies must agree with the write handler;
|
|
98
|
+
* dangling/tombstoned targets fail closed. O(r) in policy rules plus at most one alias resolution.
|
|
99
|
+
*/
|
|
100
|
+
async function describeDefinitionForAgent(routeDeps, rules, def) {
|
|
101
|
+
const resolved = def.status === "alias"
|
|
102
|
+
? await resolveDefinitionRaw({ repo: routeDeps.settingsRepo }, { namespace: def.namespace, key: def.key, workspaceId: routeDeps.workspaceId })
|
|
103
|
+
: def;
|
|
104
|
+
const coordinates = resolved ? [def, resolved] : [def];
|
|
105
|
+
const matchesAny = (list) => coordinates.some((coordinate) => !!findAgentWriteRule(list, coordinate));
|
|
106
|
+
const denied = matchesAny(rules.denied);
|
|
107
|
+
const confirmationRequired = matchesAny(rules.confirmation);
|
|
108
|
+
const scopes = resolved?.scopes ?? def.scopes;
|
|
109
|
+
return { namespace: def.namespace, key: def.key, ownerKind: def.ownerKind, scopes, status: def.status, version: def.version,
|
|
110
|
+
schema: resolved?.schema ?? def.schema,
|
|
111
|
+
agentWritable: isAgentWritable({ resolved, denied, confirmationRequired, canConfirm: !!routeDeps.confirmWrite, scopes }),
|
|
112
|
+
confirmationRequired };
|
|
113
|
+
}
|
|
114
|
+
/** Mirrors the write handler's refusals: live target, not denied, confirmable when protected,
|
|
115
|
+
* and writable at workspace or user scope. O(1). */
|
|
116
|
+
function isAgentWritable(facts) {
|
|
117
|
+
if (!facts.resolved || facts.resolved.status === "tombstone" || facts.denied)
|
|
118
|
+
return false;
|
|
119
|
+
if (facts.confirmationRequired && !facts.canConfirm)
|
|
120
|
+
return false;
|
|
121
|
+
return (facts.scopes & (SCOPE_BIT.workspace | SCOPE_BIT.user)) !== 0;
|
|
122
|
+
}
|
|
123
|
+
/** Every split point of a dotted key that names a live (non-tombstoned) definition.
|
|
124
|
+
* Leading/trailing dots are never split points. O(k) resolver calls for k dots. */
|
|
125
|
+
async function findDottedKeySplits(routeDeps, dottedKey) {
|
|
126
|
+
const matches = [];
|
|
127
|
+
// Reuse the definition resolver, including workspace fencing and aliases; never split on a guess.
|
|
128
|
+
for (let index = 1; index < dottedKey.length - 1; index++) {
|
|
129
|
+
if (dottedKey[index] !== ".")
|
|
130
|
+
continue;
|
|
131
|
+
const candidate = { namespace: dottedKey.slice(0, index), key: dottedKey.slice(index + 1) };
|
|
132
|
+
const definition = await resolveDefinitionRaw({ repo: routeDeps.settingsRepo }, { ...candidate, workspaceId: routeDeps.workspaceId });
|
|
133
|
+
if (definition && definition.status !== "tombstone")
|
|
134
|
+
matches.push(candidate);
|
|
135
|
+
}
|
|
136
|
+
return matches;
|
|
137
|
+
}
|
|
113
138
|
export function buildSettingsRegistrations(routeDeps) {
|
|
114
139
|
const handlers = {
|
|
115
140
|
settings_set_value: buildSettingsValueWriteHandler(routeDeps, "settings_set_value"),
|
|
@@ -122,23 +147,8 @@ export function buildSettingsRegistrations(routeDeps) {
|
|
|
122
147
|
routeDeps.settingsRepo.listActiveDefinitions({ workspaceId: null }),
|
|
123
148
|
routeDeps.settingsRepo.listActiveDefinitions({ workspaceId: routeDeps.workspaceId }),
|
|
124
149
|
]);
|
|
125
|
-
const
|
|
126
|
-
const
|
|
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
|
-
})))
|
|
150
|
+
const rules = agentWriteRules(routeDeps);
|
|
151
|
+
const data = (await Promise.all([...platformDefs, ...siteDefs].map((def) => describeDefinitionForAgent(routeDeps, rules, def))))
|
|
142
152
|
.sort((a, b) => a.namespace.localeCompare(b.namespace) || a.key.localeCompare(b.key));
|
|
143
153
|
return { data };
|
|
144
154
|
},
|
|
@@ -245,6 +255,21 @@ export function buildSettingsRegistrations(routeDeps) {
|
|
|
245
255
|
return { setting: settingId, value: stored, scope: AGENT_PREFERENCE_WRITE_SCOPE, revisionSeq };
|
|
246
256
|
},
|
|
247
257
|
};
|
|
248
|
-
return buildDomainRegistrations({ domain: "settings", catalogModule: "settings/agent-tools.ts", catalog: CATALOG_BY_ID, handlers, derivedRisk: settingsDerivedRisk
|
|
258
|
+
return buildDomainRegistrations({ domain: "settings", catalogModule: "settings/agent-tools.ts", catalog: CATALOG_BY_ID, handlers, derivedRisk: settingsDerivedRisk,
|
|
259
|
+
inputNormalizers: { settings_set_value: async ({ input }) => {
|
|
260
|
+
if (input.namespace !== undefined || typeof input.key !== "string" || !input.key.includes("."))
|
|
261
|
+
return { input, notes: [] };
|
|
262
|
+
if (input.key.length > 512)
|
|
263
|
+
throw new ToolInputError({ message: "settings_set_value: dotted key is too long. Send namespace and key separately." });
|
|
264
|
+
await routeDeps.settingsReady;
|
|
265
|
+
await routeDeps.settingsUiTabsReady;
|
|
266
|
+
const matches = await findDottedKeySplits(routeDeps, input.key);
|
|
267
|
+
if (matches.length !== 1)
|
|
268
|
+
throw new ToolInputError({ message: matches.length
|
|
269
|
+
? `settings_set_value: dotted key is ambiguous (${matches.map(match => `namespace=${match.namespace}, key=${match.key}`).join("; ")}). Send namespace and key explicitly.`
|
|
270
|
+
: "settings_set_value: no registered namespace/key pair matches this dotted key. Call settings_list_definitions to see valid keys; send namespace and key explicitly." });
|
|
271
|
+
return { input: { ...input, ...matches[0] }, notes: ["dotted key→namespace + key"] };
|
|
272
|
+
} },
|
|
273
|
+
}, { unwiredToolIds: UNWIRED_SETTINGS_TOOL_IDS });
|
|
249
274
|
}
|
|
250
275
|
//# sourceMappingURL=tool-registrations.js.map
|
|
@@ -239,6 +239,25 @@ export declare function retypeDefinition(required: RetypeDefinitionRequired): Pr
|
|
|
239
239
|
settingId: string;
|
|
240
240
|
version: number;
|
|
241
241
|
}>;
|
|
242
|
+
/** Add source-declared scopes to a core definition without moving or rewriting existing values.
|
|
243
|
+
* Boot uses this when a workspace preference gains a personal override. Scope removal is a
|
|
244
|
+
* separate migration decision: this operation only adds bits and preserves definition identity.
|
|
245
|
+
* @throws DefinitionInvalidError for operator-owned definitions or invalid scope bits.
|
|
246
|
+
* @complexity O(1), one definition transaction and cache invalidation.
|
|
247
|
+
* @example await extendDefinitionScopes({ deps, input: { namespace, key, workspaceId: null,
|
|
248
|
+
* scopes: SCOPE_BIT.user, callerPrincipalId, authWorkspaceId } });
|
|
249
|
+
*/
|
|
250
|
+
export declare function extendDefinitionScopes({ deps, input }: {
|
|
251
|
+
deps: SettingsWriteServiceDeps;
|
|
252
|
+
input: {
|
|
253
|
+
namespace: string;
|
|
254
|
+
key: string;
|
|
255
|
+
workspaceId: UUID | null;
|
|
256
|
+
scopes: number;
|
|
257
|
+
callerPrincipalId: UUID;
|
|
258
|
+
authWorkspaceId: UUID;
|
|
259
|
+
};
|
|
260
|
+
}, _options?: {}): Promise<void>;
|
|
242
261
|
export interface ReconcileDefinitionDefaultRequired {
|
|
243
262
|
deps: SettingsWriteServiceDeps;
|
|
244
263
|
input: {
|
|
@@ -416,6 +416,11 @@ export async function renameDefinition(required) {
|
|
|
416
416
|
const { deps, input } = required;
|
|
417
417
|
await authorizeDefinitionsManage(deps, input.callerPrincipalId, input.authWorkspaceId);
|
|
418
418
|
const current = await resolveActiveDefinitionOrThrow(deps, input, "rename");
|
|
419
|
+
// A dispatched `rename` item that omits its target arrives here as `""` (see definitions-dispatch);
|
|
420
|
+
// without this guard the definition was silently moved to the empty name `.`.
|
|
421
|
+
if (!input.newNamespace || !input.newKey) {
|
|
422
|
+
throw new DefinitionInvalidError("rename requires a non-empty newNamespace and newKey");
|
|
423
|
+
}
|
|
419
424
|
if (input.namespace === input.newNamespace && input.key === input.newKey) {
|
|
420
425
|
throw new DefinitionInvalidError("rename requires newNamespace/newKey to differ from the current name");
|
|
421
426
|
}
|
|
@@ -564,6 +569,35 @@ export async function retypeDefinition(required) {
|
|
|
564
569
|
invalidateDefinitionNamespaceCache(deps.repo, input.namespace);
|
|
565
570
|
return result;
|
|
566
571
|
}
|
|
572
|
+
/** Add source-declared scopes to a core definition without moving or rewriting existing values.
|
|
573
|
+
* Boot uses this when a workspace preference gains a personal override. Scope removal is a
|
|
574
|
+
* separate migration decision: this operation only adds bits and preserves definition identity.
|
|
575
|
+
* @throws DefinitionInvalidError for operator-owned definitions or invalid scope bits.
|
|
576
|
+
* @complexity O(1), one definition transaction and cache invalidation.
|
|
577
|
+
* @example await extendDefinitionScopes({ deps, input: { namespace, key, workspaceId: null,
|
|
578
|
+
* scopes: SCOPE_BIT.user, callerPrincipalId, authWorkspaceId } });
|
|
579
|
+
*/
|
|
580
|
+
export async function extendDefinitionScopes({ deps, input }, _options = {}) {
|
|
581
|
+
await authorizeDefinitionsManage(deps, input.callerPrincipalId, input.authWorkspaceId);
|
|
582
|
+
const current = await resolveActiveDefinitionOrThrow(deps, input, "extend the scopes of");
|
|
583
|
+
const validBits = SCOPE_BIT.global | SCOPE_BIT.workspace | SCOPE_BIT.user;
|
|
584
|
+
if (current.ownerKind !== "core" || !Number.isInteger(input.scopes) || input.scopes <= 0 || input.scopes > validBits) {
|
|
585
|
+
throw new DefinitionInvalidError("Only core definitions can gain valid source-declared scope bits");
|
|
586
|
+
}
|
|
587
|
+
const scopes = current.scopes | input.scopes;
|
|
588
|
+
if (scopes === current.scopes)
|
|
589
|
+
return;
|
|
590
|
+
await deps.repo.transaction(async () => {
|
|
591
|
+
const now = kernelNowIso({ clock: deps.clock });
|
|
592
|
+
await deps.repo.saveDefinition({ ...current, scopes, updatedAt: now });
|
|
593
|
+
// Re-registration of metadata, not a value/schema change; retain the existing revision vocabulary.
|
|
594
|
+
await deps.repo.appendRevision({ entityKind: "definition", settingId: current.settingId,
|
|
595
|
+
scope: null, workspaceId: current.workspaceId, principalId: null, op: "register",
|
|
596
|
+
beforeJson: { scopes: current.scopes }, afterJson: { scopes }, defVersion: current.version,
|
|
597
|
+
actor: input.callerPrincipalId, originPluginId: null, changeSetId: null, createdAt: now });
|
|
598
|
+
});
|
|
599
|
+
invalidateDefinitionNamespaceCache(deps.repo, input.namespace);
|
|
600
|
+
}
|
|
567
601
|
/**
|
|
568
602
|
* Reconciles a stored `default_json` that has drifted from the default its
|
|
569
603
|
* source declares. Idempotent and a no-op when they already agree.
|
package/dist/target-scope.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export type TargetResolution = 'current' | {
|
|
|
21
21
|
* @param required.isCurrentTarget - Check whether an unscoped tool names the current target.
|
|
22
22
|
* @param optional - Target input name, schema description, refusal text and a narrowing scope predicate.
|
|
23
23
|
* @returns A new array in the same order, preserving tools that already own the target input.
|
|
24
|
+
* Unscoped targets are also checked by normalizeInput, before policy or approval planning;
|
|
25
|
+
* the handler retains its check for direct handler callers.
|
|
24
26
|
* @throws ToolInputError from a wrapped handler for a non-string target or a refused foreign target.
|
|
25
27
|
* Host port errors propagate unchanged from the wrapped handler.
|
|
26
28
|
* @complexity O(n) over registrations, plus copying supported schemas' top-level properties.
|
package/dist/target-scope.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isNonArrayRecord } from './records.js';
|
|
2
|
-
import { isReadOnlyTool
|
|
2
|
+
import { isReadOnlyTool } from './tool-registry.js';
|
|
3
|
+
import { ToolInputError } from './tool-input-errors.js';
|
|
3
4
|
/** Narrow the shared record predicate to ordinary or null-prototype objects, excluding instances. */
|
|
4
5
|
function isPlainObject(value) {
|
|
5
6
|
if (!isNonArrayRecord({ value }))
|
|
@@ -25,6 +26,8 @@ function withTargetParam(schema, param, description) {
|
|
|
25
26
|
* @param required.isCurrentTarget - Check whether an unscoped tool names the current target.
|
|
26
27
|
* @param optional - Target input name, schema description, refusal text and a narrowing scope predicate.
|
|
27
28
|
* @returns A new array in the same order, preserving tools that already own the target input.
|
|
29
|
+
* Unscoped targets are also checked by normalizeInput, before policy or approval planning;
|
|
30
|
+
* the handler retains its check for direct handler callers.
|
|
28
31
|
* @throws ToolInputError from a wrapped handler for a non-string target or a refused foreign target.
|
|
29
32
|
* Host port errors propagate unchanged from the wrapped handler.
|
|
30
33
|
* @complexity O(n) over registrations, plus copying supported schemas' top-level properties.
|
|
@@ -39,10 +42,23 @@ export function applyTargetScope(required, optional = {}) {
|
|
|
39
42
|
if (isNonArrayRecord(properties) && Object.hasOwn(properties.value, param))
|
|
40
43
|
return registration;
|
|
41
44
|
const scoped = isReadOnlyTool({ descriptor }) && (optional.scoped?.({ registration }) ?? true);
|
|
42
|
-
const inputSchema =
|
|
45
|
+
const inputSchema = withTargetParam(descriptor.inputSchema, param, describeParam);
|
|
43
46
|
return {
|
|
44
47
|
...registration,
|
|
45
48
|
descriptor: inputSchema === descriptor.inputSchema ? descriptor : { ...descriptor, inputSchema },
|
|
49
|
+
normalizeInput: async ({ input }, options) => {
|
|
50
|
+
const normalized = registration.normalizeInput ? await registration.normalizeInput({ input }, options) : { input, notes: [] };
|
|
51
|
+
// Target refusal precedes policy and confirmation, including each merged plan item.
|
|
52
|
+
// Keep the key intact: downstream gates must authorize exactly this target.
|
|
53
|
+
if (!scoped && isPlainObject(normalized.input) && Object.hasOwn(normalized.input, param)) {
|
|
54
|
+
const target = normalized.input[param];
|
|
55
|
+
if (typeof target !== 'string')
|
|
56
|
+
throw new ToolInputError({ message: `${param} must be a string` });
|
|
57
|
+
if (!await required.isCurrentTarget({ target }))
|
|
58
|
+
throw new ToolInputError({ message: refuseMessage });
|
|
59
|
+
}
|
|
60
|
+
return normalized;
|
|
61
|
+
},
|
|
46
62
|
handler: async (ctx, options) => {
|
|
47
63
|
if (!isPlainObject(ctx.input) || !Object.hasOwn(ctx.input, param))
|
|
48
64
|
return registration.handler(ctx, options);
|
package/dist/text/escape.js
CHANGED
|
@@ -13,11 +13,11 @@ const XML_ENTITIES = { ...HTML_ENTITIES, "'": "'" };
|
|
|
13
13
|
const SPECIAL = /[&<>"']/g;
|
|
14
14
|
/** Escapes `& < > " '` for HTML text or a quoted attribute value. @complexity O(text length). */
|
|
15
15
|
export function escapeHtml({ text }, _optional = {}) {
|
|
16
|
-
return text.replace(SPECIAL, (char) => HTML_ENTITIES[char]
|
|
16
|
+
return text.replace(SPECIAL, (char) => HTML_ENTITIES[char]);
|
|
17
17
|
}
|
|
18
18
|
/** {@link escapeHtml} with the XML named entity `'` for the apostrophe (sitemap and feed XML).
|
|
19
19
|
* @complexity O(text length). */
|
|
20
20
|
export function escapeXml({ text }, _optional = {}) {
|
|
21
|
-
return text.replace(SPECIAL, (char) => XML_ENTITIES[char]
|
|
21
|
+
return text.replace(SPECIAL, (char) => XML_ENTITIES[char]);
|
|
22
22
|
}
|
|
23
23
|
//# sourceMappingURL=escape.js.map
|
|
@@ -8,6 +8,9 @@ export declare function stripControlSequences({ text }: {
|
|
|
8
8
|
text: string;
|
|
9
9
|
}): string;
|
|
10
10
|
export interface SanitizeTextOptions {
|
|
11
|
+
/** Exact IDs from trusted registration metadata, never inferred from error text or arguments.
|
|
12
|
+
* Credential rules still run first; prefixes and glued suffixes are never exempted. */
|
|
13
|
+
diagnosticToolIds?: readonly string[];
|
|
11
14
|
/** Maximum output length; longer input is truncated with a trailing marker. Defaults to 500. */
|
|
12
15
|
maxLength?: number;
|
|
13
16
|
}
|
|
@@ -15,6 +18,8 @@ export interface SanitizeTextOptions {
|
|
|
15
18
|
* Strip terminal control sequences, redact anything that looks like a secret, and cap the
|
|
16
19
|
* length of `text` — the one function every boundary that surfaces daemon/network-derived text
|
|
17
20
|
* to stderr/stdout should route through first.
|
|
21
|
+
* Trusted diagnosticToolIds exempt complete IDs (including dots) only, after credential masking.
|
|
22
|
+
* @complexity O(n + t) local time/space for n text bytes and t trusted ID bytes.
|
|
18
23
|
*/
|
|
19
24
|
export declare function sanitizeUntrustedText({ text }: {
|
|
20
25
|
text: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LABELED_SECRET_RE, OPAQUE_TOKEN_RE, NON_SECRET_TOKEN_RE } from "../redact.js";
|
|
1
|
+
import { redactSecrets, LABELED_SECRET_RE, OPAQUE_TOKEN_RE, NON_SECRET_TOKEN_RE } from "../redact.js";
|
|
2
2
|
/**
|
|
3
3
|
* @module redact
|
|
4
4
|
*
|
|
@@ -39,6 +39,38 @@ function isOtherControlCode(code) {
|
|
|
39
39
|
return true; // C1 controls
|
|
40
40
|
return false;
|
|
41
41
|
}
|
|
42
|
+
/** True when position `j` starts an OSC terminator: BEL, or ST (`ESC \\`). */
|
|
43
|
+
function isOscTerminatorAt(text, j) {
|
|
44
|
+
const code = text.charCodeAt(j);
|
|
45
|
+
return code === BEL || (code === ESC && text.charCodeAt(j + 1) === BACKSLASH);
|
|
46
|
+
}
|
|
47
|
+
/** Index of a CSI's final byte at or after `from`, or `text.length` when the CSI is truncated. */
|
|
48
|
+
function csiEnd(text, from) {
|
|
49
|
+
let j = from;
|
|
50
|
+
while (j < text.length && !isCsiFinalByte(text.charCodeAt(j)))
|
|
51
|
+
j++;
|
|
52
|
+
return j;
|
|
53
|
+
}
|
|
54
|
+
/** Index of the last code unit of an OSC body starting at `from`: the BEL, the `\\` of ST, or the
|
|
55
|
+
* final code unit when the OSC is never terminated. */
|
|
56
|
+
function oscEnd(text, from) {
|
|
57
|
+
let j = from;
|
|
58
|
+
while (j < text.length && !isOscTerminatorAt(text, j))
|
|
59
|
+
j++;
|
|
60
|
+
// charCodeAt past the end is NaN, so a truncated OSC never matches BEL here.
|
|
61
|
+
if (text.charCodeAt(j) === BEL)
|
|
62
|
+
return j;
|
|
63
|
+
return Math.min(j + 1, text.length - 1);
|
|
64
|
+
}
|
|
65
|
+
/** Index of the last code unit consumed by the escape sequence whose ESC sits at `start`. */
|
|
66
|
+
function escapeSequenceEnd(text, start) {
|
|
67
|
+
const next = text.charCodeAt(start + 1);
|
|
68
|
+
if (next === CSI_OPEN)
|
|
69
|
+
return csiEnd(text, start + 2);
|
|
70
|
+
if (next === OSC_OPEN)
|
|
71
|
+
return oscEnd(text, start + 2);
|
|
72
|
+
return start; // bare ESC (or an unrecognized ESC-prefixed byte): drop just the ESC byte
|
|
73
|
+
}
|
|
42
74
|
/**
|
|
43
75
|
* Strip ANSI/terminal escape sequences (CSI `ESC [ ... final-byte`, OSC `ESC ] ... BEL-or-ST`,
|
|
44
76
|
* and bare ESC-prefixed bytes) plus other C0/C1 control characters from `text`. Written as an
|
|
@@ -50,53 +82,41 @@ export function stripControlSequences({ text }) {
|
|
|
50
82
|
for (let i = 0; i < text.length; i++) {
|
|
51
83
|
const code = text.charCodeAt(i);
|
|
52
84
|
if (code === ESC) {
|
|
53
|
-
|
|
54
|
-
if (next === CSI_OPEN) {
|
|
55
|
-
let j = i + 2;
|
|
56
|
-
while (j < text.length && !isCsiFinalByte(text.charCodeAt(j)))
|
|
57
|
-
j++;
|
|
58
|
-
i = j; // land on the final byte; the outer loop's i++ steps past it
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
if (next === OSC_OPEN) {
|
|
62
|
-
let j = i + 2;
|
|
63
|
-
while (j < text.length &&
|
|
64
|
-
text.charCodeAt(j) !== BEL &&
|
|
65
|
-
!(text.charCodeAt(j) === ESC && text.charCodeAt(j + 1) === BACKSLASH)) {
|
|
66
|
-
j++;
|
|
67
|
-
}
|
|
68
|
-
if (j < text.length && text.charCodeAt(j) === BEL)
|
|
69
|
-
i = j;
|
|
70
|
-
else
|
|
71
|
-
i = Math.min(j + 1, text.length - 1);
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
continue; // bare ESC (or an unrecognized ESC-prefixed byte): drop just the ESC byte
|
|
75
|
-
}
|
|
76
|
-
if (isOtherControlCode(code))
|
|
85
|
+
i = escapeSequenceEnd(text, i); // land on the last consumed byte; the loop's i++ steps past it
|
|
77
86
|
continue;
|
|
78
|
-
|
|
87
|
+
}
|
|
88
|
+
if (!isOtherControlCode(code))
|
|
89
|
+
out += text[i];
|
|
79
90
|
}
|
|
80
91
|
return out;
|
|
81
92
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
/** Exact diagnostic vocabulary only: unknown spellings and glued suffixes remain opaque. */
|
|
94
|
+
const DIAGNOSTIC_TOKENS = new Set([
|
|
95
|
+
'additionalProperties', 'additionalItems', 'unevaluatedProperties', 'unevaluatedItems',
|
|
96
|
+
'dependentRequired', 'dependentSchemas', 'exclusiveMinimum', 'exclusiveMaximum',
|
|
97
|
+
'minContains', 'maxContains', 'patternProperties', 'propertyNames', 'prefixItems',
|
|
98
|
+
'contentEncoding', 'contentMediaType', 'contentSchema',
|
|
99
|
+
]);
|
|
100
|
+
/** Mask credentials before preserving complete trusted IDs and UPPER_SNAKE diagnostics; redact opaque spans elsewhere. */
|
|
101
|
+
function redactSecretLike({ text, diagnosticToolIds }) {
|
|
102
|
+
return redactSecrets({ input: text }, { redactPii: false })
|
|
103
|
+
.replace(LABELED_SECRET_RE, match => match.replace(/([:=]\s*)\S+$/, '$1[redacted]'))
|
|
104
|
+
.replace(/\[REDACTED:[^\]\r\n]+\]/g, '[redacted]')
|
|
105
|
+
// Trust whole dotted IDs so their components cannot become a general redaction bypass.
|
|
106
|
+
.replace(/[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*/g, token => diagnosticToolIds.has(token) ? token
|
|
107
|
+
: token.replace(OPAQUE_TOKEN_RE, match => NON_SECRET_TOKEN_RE.test(match) || DIAGNOSTIC_TOKENS.has(match) ? match : '[redacted]'));
|
|
90
108
|
}
|
|
91
109
|
/**
|
|
92
110
|
* Strip terminal control sequences, redact anything that looks like a secret, and cap the
|
|
93
111
|
* length of `text` — the one function every boundary that surfaces daemon/network-derived text
|
|
94
112
|
* to stderr/stdout should route through first.
|
|
113
|
+
* Trusted diagnosticToolIds exempt complete IDs (including dots) only, after credential masking.
|
|
114
|
+
* @complexity O(n + t) local time/space for n text bytes and t trusted ID bytes.
|
|
95
115
|
*/
|
|
96
116
|
export function sanitizeUntrustedText({ text }, options = {}) {
|
|
97
117
|
const requestedLength = options.maxLength ?? DEFAULT_MAX_EXCERPT_LENGTH;
|
|
98
118
|
const maxLength = Number.isFinite(requestedLength) ? Math.max(0, Math.floor(requestedLength)) : DEFAULT_MAX_EXCERPT_LENGTH;
|
|
99
|
-
const sanitized = redactSecretLike({ text: stripControlSequences({ text }) });
|
|
119
|
+
const sanitized = redactSecretLike({ text: stripControlSequences({ text }), diagnosticToolIds: new Set(options.diagnosticToolIds) });
|
|
100
120
|
if (sanitized.length <= maxLength)
|
|
101
121
|
return sanitized;
|
|
102
122
|
// The marker is part of the output budget, including when the budget cannot fit the full marker.
|