@jini-ai/core 0.6.15-rc.1 → 0.6.15
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 +16 -0
- package/README.md +1 -0
- package/dist/approval.d.ts +4 -2
- package/dist/approval.js +7 -3
- package/dist/batch-tool-inputs.js +10 -3
- package/dist/contribution-registry.d.ts +23 -0
- package/dist/fetch/cache.d.ts +1 -0
- package/dist/fetch/cache.js +15 -7
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/origin-validation.js +9 -8
- package/dist/primitives/index.d.ts +1 -0
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/paths.js +2 -1
- package/dist/primitives/sse.js +4 -3
- package/dist/primitives/trim-chars.d.ts +25 -0
- package/dist/primitives/trim-chars.js +28 -0
- package/dist/records.d.ts +29 -6
- package/dist/records.js +105 -15
- package/dist/redact.d.ts +0 -1
- package/dist/redact.js +29 -7
- package/dist/registration-kit.js +1 -3
- package/dist/settings/agent-tools.js +7 -5
- package/dist/settings/agent-value-write-tools.js +7 -3
- package/dist/settings/dictionaries/index.d.ts +0 -22
- package/dist/settings/dictionaries/index.js +0 -1
- package/dist/settings/ensure-definitions.js +4 -2
- package/dist/settings/express/http-request.js +2 -1
- package/dist/settings/express/http.js +12 -4
- package/dist/settings/repo.memory.js +5 -2
- package/dist/settings/settings.js +5 -1
- package/dist/settings/tool-registrations.js +9 -3
- package/dist/settings/write-service.js +11 -9
- package/dist/target-scope.js +2 -1
- package/dist/text/index.d.ts +2 -0
- package/dist/text/index.js +2 -0
- package/dist/text/line-tail.d.ts +17 -0
- package/dist/text/line-tail.js +25 -0
- package/dist/text/sanitize-untrusted-text.js +33 -4
- package/dist/tool-exposure.js +20 -16
- package/dist/tool-input-normalization.d.ts +11 -3
- package/dist/tool-input-normalization.js +14 -8
- package/dist/tool-metadata-types.d.ts +5 -0
- package/dist/tool-registry.d.ts +5 -0
- package/package.json +1 -1
package/dist/redact.js
CHANGED
|
@@ -53,7 +53,16 @@ export const SECRET_SHAPE_PATTERNS = [
|
|
|
53
53
|
{ name: PEM_PATTERN_NAME, pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
54
54
|
];
|
|
55
55
|
/** Broad token shapes for agent-failure text; consumers retain their plain [REDACTED] pipeline. */
|
|
56
|
-
|
|
56
|
+
const AGENT_FAILURE_TOKEN_SHAPES = [
|
|
57
|
+
/AIza[\w-]+/, // Google API key
|
|
58
|
+
/AQ\.[\w.\/-]+/, // AI Studio key
|
|
59
|
+
/sk-[\w.-]+/, // OpenAI-style secret key
|
|
60
|
+
/gh[pousr]_\w+/, // GitHub token
|
|
61
|
+
/github_pat_\w+/, // GitHub fine-grained token
|
|
62
|
+
/ya29\.[\w.-]+/, // Google OAuth access token
|
|
63
|
+
/eyJ[\w-]+\.[\w-]+(?:\.[\w-]+)?/, // JWT
|
|
64
|
+
];
|
|
65
|
+
export const AGENT_FAILURE_SECRET_SHAPE_RE = new RegExp(`\\b(?:${AGENT_FAILURE_TOKEN_SHAPES.map((shape) => shape.source).join('|')})`, 'g');
|
|
57
66
|
/**
|
|
58
67
|
* Exact path SEGMENT names refused at any depth, matched case-insensitively against each segment
|
|
59
68
|
* of the caller-supplied path (never a substring — a directory named `not-secrets-actually` is not
|
|
@@ -89,6 +98,12 @@ export const LOCAL_OWNER_PROXY_MARKER_HEADERS = [
|
|
|
89
98
|
// Order matters: list specific rules before more general ones. Langfuse
|
|
90
99
|
// keys (`sk-lf-...`) would otherwise be eaten by the generic `sk-...`
|
|
91
100
|
// rule and labeled as a generic OpenAI-style key.
|
|
101
|
+
// One IPv4 octet, 0-255, so junk shapes like 999.1.1.1 do not count.
|
|
102
|
+
const IPV4_OCTET = '(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)';
|
|
103
|
+
// Optional +country code, then a bare or parenthesized area code, with optional separators.
|
|
104
|
+
const PHONE_SEPARATOR = '[\\s.-]?';
|
|
105
|
+
const PHONE_COUNTRY_CODE = /(?:\+?\d{1,3}[\s.-]?)?/;
|
|
106
|
+
const PHONE_AREA_CODE = /(?:\(\d{3}\)|\d{3})/;
|
|
92
107
|
const PATTERNS = [
|
|
93
108
|
// Langfuse public/secret keys (pk-lf- / sk-lf-). Must run before the
|
|
94
109
|
// generic sk- rule so the more specific label wins.
|
|
@@ -124,14 +139,16 @@ const PATTERNS = [
|
|
|
124
139
|
// Bearer tokens in HTTP Authorization header copy/paste. We only match
|
|
125
140
|
// the value, not the literal 'Bearer ', so the marker stays readable in
|
|
126
141
|
// the redacted output ("Authorization: Bearer [REDACTED:bearer_token]").
|
|
127
|
-
|
|
142
|
+
// The leading lookahead only lets the lookbehind run where a value can start, so a long
|
|
143
|
+
// whitespace run is not rescanned from every position inside it.
|
|
144
|
+
{ name: 'bearer_token', regex: /(?=[A-Za-z0-9._~+/-])(?<=\bBearer\s+)[A-Za-z0-9._~+/-]{16,}={0,2}/g },
|
|
128
145
|
// Alphabetic TLDs preserve versioned package paths in error stacks (e.g. pkg@1.2.3).
|
|
129
146
|
{ name: 'email', regex: /\b[\w.+-]+@(?:[\w-]+\.)+[A-Za-z]{2,}\b/g },
|
|
130
147
|
// IPv4. Reject all-zero / 255.255.255.255-ish junk shapes by gating on
|
|
131
148
|
// each octet being 0-255.
|
|
132
149
|
{
|
|
133
150
|
name: 'ipv4',
|
|
134
|
-
regex:
|
|
151
|
+
regex: new RegExp(`\\b(?:${IPV4_OCTET}\\.){3}${IPV4_OCTET}\\b`, 'g'),
|
|
135
152
|
},
|
|
136
153
|
// Phone numbers. Tight US-leaning shape; a global PII detector would
|
|
137
154
|
// need a real lib. We keep this so US-based test prompts ('call me at
|
|
@@ -141,7 +158,7 @@ const PATTERNS = [
|
|
|
141
158
|
// hashes and identifiers stay readable; separators within phones still match.
|
|
142
159
|
{
|
|
143
160
|
name: 'phone',
|
|
144
|
-
regex:
|
|
161
|
+
regex: new RegExp(`(?<![\\w-])${PHONE_COUNTRY_CODE.source}${PHONE_AREA_CODE.source}${PHONE_SEPARATOR}\\d{3}${PHONE_SEPARATOR}\\d{4}(?![\\w-])`, 'g'),
|
|
145
162
|
},
|
|
146
163
|
];
|
|
147
164
|
// Credit card sweep is special: a naive 13-19 digit run matches a lot of
|
|
@@ -184,14 +201,18 @@ function isLuhnValid(digits) {
|
|
|
184
201
|
const SECRET_LABELS = 'authorization|bearer|api[-_]?key|access[-_]?token|refresh[-_]?token|client[-_]?secret|password|secret|cookie';
|
|
185
202
|
export const LABELED_SECRET_RE = new RegExp(`\\b(${SECRET_LABELS})\\b\\s*[:=]\\s*\\S+`, 'gi');
|
|
186
203
|
export const OPAQUE_TOKEN_RE = /[A-Za-z0-9_-]{20,}/g;
|
|
187
|
-
|
|
204
|
+
// Pattern parts, only ever used inside the anchored NON_SECRET_TOKEN_RE.
|
|
205
|
+
const CORRELATION_ID = 'ERR-[0-9A-F]{4}(?:-[0-9A-F]{4}){3}';
|
|
206
|
+
const UPPER_SNAKE_DIAGNOSTIC = '[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+';
|
|
207
|
+
export const NON_SECRET_TOKEN_RE = new RegExp(`^(?:${CORRELATION_ID}|delegated-tool-calls|${UPPER_SNAKE_DIAGNOSTIC})$`);
|
|
188
208
|
// Runtime markers must survive a second pass; the bounded sanitizer deliberately retains
|
|
189
209
|
// its historical lowercase marker contract and uses the unmodified lexical vocabulary.
|
|
190
210
|
// Reject a masked header/query value before matching, so a following field's credential
|
|
191
211
|
// remains available to the global scan rather than being consumed beside the marker.
|
|
192
212
|
const MASK_AWARE_LABELED_SECRET_RE = new RegExp(LABELED_SECRET_RE.source.replace(/\\S\+$/, "(?!Bearer\\s+\\[REDACTED:[^\\]]+\\](?:\\s|$))(?!\\[REDACTED:[^\\]]+\\][;&])(\\S+)"), LABELED_SECRET_RE.flags);
|
|
193
213
|
// Provider failures also echo short bearer values. Exclude our own marker on subsequent passes.
|
|
194
|
-
|
|
214
|
+
// The leading lookahead only lets the lookbehind run where a value can start (see bearer_token).
|
|
215
|
+
const SHORT_BEARER_RE = /(?=[\w.+/=-])(?<=Bearer\s+)[\w.+/=-]+/gi;
|
|
195
216
|
const PROVIDER_API_KEY_HEADER = /(x-api-key|api-key|x-goog-api-key)\s*[:=]\s*([^\s,;"']+)/gi;
|
|
196
217
|
const PROVIDER_API_KEY_QUERY = /([?&]key=)([^&\s]+)/gi;
|
|
197
218
|
// URL userinfo and signed query values have explicit credential context; unlike opaque runs,
|
|
@@ -205,7 +226,8 @@ const SIGNED_QUERY_RE = /([?&](?:signature|sig|x-amz-signature|x-goog-signature|
|
|
|
205
226
|
// monkey) are masked too; over-masking a URL value is the cheaper failure. Empty values stay.
|
|
206
227
|
// Match through a trailing fragment; only '&', quotes, parentheses or whitespace end the value.
|
|
207
228
|
// Both name runs are bounded so a long separator-free run costs linear time, not quadratic.
|
|
208
|
-
const
|
|
229
|
+
const URL_SECRET_WORDS = 'token|key|secret|signature|sig|code|password|passwd|pwd|auth|authorization|credential';
|
|
230
|
+
const URL_SECRET_PARAM_RE = new RegExp(`[?&#;]([\\w.-]{0,128}?(?:${URL_SECRET_WORDS})s?(?=[._=-])[\\w.-]{0,128})=[^&\\s"'<>()]+`, 'gi');
|
|
209
231
|
// A parameter the labeled rule already names (password, client_secret, ...) keeps that category.
|
|
210
232
|
const LABELED_URL_PARAM_NAME_RE = new RegExp(`(?:^|[.-])(?:${SECRET_LABELS})$`, 'i');
|
|
211
233
|
// Only an entire marker is already masked. A secret glued onto marker-like text still needs
|
package/dist/registration-kit.js
CHANGED
|
@@ -443,9 +443,7 @@ function registrationFor({ spec, id, handler }) {
|
|
|
443
443
|
/** The tripwire: a catalog entry is either wired or recorded as deliberately unwired, never silently neither. */
|
|
444
444
|
function assertEveryEntryIsWiredOrDeclared({ spec }, { unwiredToolIds } = {}) {
|
|
445
445
|
for (const id of spec.catalog.keys()) {
|
|
446
|
-
if (id in spec.handlers)
|
|
447
|
-
continue;
|
|
448
|
-
if (unwiredToolIds?.has(id))
|
|
446
|
+
if (id in spec.handlers || unwiredToolIds?.has(id))
|
|
449
447
|
continue;
|
|
450
448
|
throw new Error(`tool-registrations: ${spec.domain} catalog entry '${id}' is neither wired nor declared unwired — wire a handler for it, or add it to that domain's unwired set with the reason recorded next to it`);
|
|
451
449
|
}
|
|
@@ -43,6 +43,8 @@
|
|
|
43
43
|
* See docs/decisions/DR-003-settings-ledger-invariants.md.
|
|
44
44
|
*/
|
|
45
45
|
import { AGENT_WRITABLE_PREFERENCE_IDS, AGENT_WRITABLE_PREFERENCES } from "./agent-writable-preferences.js";
|
|
46
|
+
/** The side-effect class every settings write tool declares. */
|
|
47
|
+
const MUTATES_DURABLE_STATE = "mutates-durable-state";
|
|
46
48
|
/**
|
|
47
49
|
* JSON Schema for this tool's `input`, published to the model via `ToolDescriptor.inputSchema`
|
|
48
50
|
* (a host's tool-registration layer refuses to wire any tool lacking one). Optional — the two bulk/schema
|
|
@@ -163,7 +165,7 @@ export function getSettingsAgentToolCatalog() {
|
|
|
163
165
|
{
|
|
164
166
|
name: "settings_set_ui_preference",
|
|
165
167
|
description: "Sets one of the operator's own admin-UI preferences (interface language, theme, accent color, or notification sounds) and returns the stored value. Writes only to the calling operator's own user layer — it cannot target another operator, another scope, or any setting outside its fixed list. Use settings_get_effective to read current values first.",
|
|
166
|
-
sideEffects:
|
|
168
|
+
sideEffects: MUTATES_DURABLE_STATE,
|
|
167
169
|
// The narrowest write grant in the system, and it is what this tool derives BECAUSE it never
|
|
168
170
|
// names a target principal (`write-service.ts`'s `deriveRequiredPermission`). Declared here
|
|
169
171
|
// to match; the handler does not pass this string, it lets the chokepoint derive it, so the
|
|
@@ -174,14 +176,14 @@ export function getSettingsAgentToolCatalog() {
|
|
|
174
176
|
{
|
|
175
177
|
name: "settings_set_value",
|
|
176
178
|
description: "Sets one registered setting at workspace scope (default) or the caller's own user scope. Call settings_list_definitions for valid keys, schemas, scopes and confirmation policy. Returns {key, scope, previous, value, revisionSeq}; previous is the old override, or null if absent. Privacy, instructions and host-protected assistant settings need a human confirmation card; the agent cannot confirm itself. Rejects unknown/tombstoned keys, disallowed scopes, invalid values and missing permissions. Cannot write globally, target another operator, reset a namespace or change definitions.",
|
|
177
|
-
sideEffects:
|
|
179
|
+
sideEffects: MUTATES_DURABLE_STATE,
|
|
178
180
|
authorization: { permission: "settings.workspace.write" },
|
|
179
181
|
inputSchema: SET_VALUE_SCHEMA,
|
|
180
182
|
},
|
|
181
183
|
{
|
|
182
184
|
name: "settings_clear_value",
|
|
183
185
|
description: "Clears one setting override at workspace scope (default) or the caller's own user scope so the next layer/default applies. Use to put one setting back to its inherited value; not to reset a namespace or delete a definition. Returns {key, scope, previous, effective}; restore a non-null previous override with settings_set_value. Protected assistant settings require a human confirmation card. Rejects unknown/tombstoned keys, disallowed scopes and missing permissions; cannot target global or another operator's layer.",
|
|
184
|
-
sideEffects:
|
|
186
|
+
sideEffects: MUTATES_DURABLE_STATE,
|
|
185
187
|
authorization: { permission: "settings.workspace.write" },
|
|
186
188
|
inputSchema: CLEAR_VALUE_SCHEMA,
|
|
187
189
|
},
|
|
@@ -190,7 +192,7 @@ export function getSettingsAgentToolCatalog() {
|
|
|
190
192
|
// clears every value in an operator-named namespace at a scope in one call, irreversible.
|
|
191
193
|
name: "settings_reset",
|
|
192
194
|
description: "Resets every setting in a namespace to defaults at a scope. NEVER agent-callable — see file header.",
|
|
193
|
-
sideEffects:
|
|
195
|
+
sideEffects: MUTATES_DURABLE_STATE,
|
|
194
196
|
authorization: { permission: "settings.reset.workspace" },
|
|
195
197
|
},
|
|
196
198
|
{
|
|
@@ -199,7 +201,7 @@ export function getSettingsAgentToolCatalog() {
|
|
|
199
201
|
// every existing stored value for that key is interpreted platform-wide.
|
|
200
202
|
name: "settings_register_definitions",
|
|
201
203
|
description: "Registers, renames, retypes, deprecates, or tombstones setting definitions. NEVER agent-callable — see file header.",
|
|
202
|
-
sideEffects:
|
|
204
|
+
sideEffects: MUTATES_DURABLE_STATE,
|
|
203
205
|
authorization: { permission: "settings.definitions.manage" },
|
|
204
206
|
},
|
|
205
207
|
];
|
|
@@ -56,8 +56,11 @@ function snapshotJsonValue(toolId, input) {
|
|
|
56
56
|
throw new ToolInputError({ message: `${toolId}: value must be valid JSON.` }, { cause });
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
/** `typeof` results JSON cannot represent. */
|
|
60
|
+
const NON_JSON_TYPES = new Set(["undefined", "function", "symbol"]);
|
|
59
61
|
function rejectNonJson(_key, value) {
|
|
60
|
-
|
|
62
|
+
const nonFiniteNumber = typeof value === "number" && !Number.isFinite(value);
|
|
63
|
+
if (NON_JSON_TYPES.has(typeof value) || nonFiniteNumber)
|
|
61
64
|
throw new Error("not JSON");
|
|
62
65
|
return value;
|
|
63
66
|
}
|
|
@@ -70,7 +73,8 @@ async function resolveTarget(deps, target, isSet) {
|
|
|
70
73
|
throw new DefinitionNotFoundError(`setting '${name}' was not found`);
|
|
71
74
|
if (definition.status === "tombstone")
|
|
72
75
|
throw new DefinitionTombstonedError(`setting '${name}' has been tombstoned`);
|
|
73
|
-
|
|
76
|
+
const scopeAllowed = (definition.scopes & SCOPE_BIT[target.scope]) !== 0;
|
|
77
|
+
if (!scopeAllowed)
|
|
74
78
|
throw new ScopeNotAllowedError(name);
|
|
75
79
|
if (isSet && !validateValueAgainstSchema(definition.schema, target.value))
|
|
76
80
|
throw new ValueValidationFailedError(`value for '${name}' does not match the definition schema`);
|
|
@@ -211,7 +215,7 @@ export function buildSettingsValueWriteHandler(deps, toolId) {
|
|
|
211
215
|
return await applyWrite(deps, request, prepared);
|
|
212
216
|
}
|
|
213
217
|
catch (error) {
|
|
214
|
-
rethrowWriteError(error, toolId, target, permission);
|
|
218
|
+
return rethrowWriteError(error, toolId, target, permission);
|
|
215
219
|
}
|
|
216
220
|
};
|
|
217
221
|
}
|
|
@@ -14,28 +14,6 @@
|
|
|
14
14
|
* IS the key" — `translateSettingsDialog` falls back to the raw key for English and any
|
|
15
15
|
* unrecognized locale.
|
|
16
16
|
*/
|
|
17
|
-
import { SETTINGS_DIALOG_ES } from './settings-dialog.es.js';
|
|
18
|
-
import { SETTINGS_DIALOG_ID } from './settings-dialog.id.js';
|
|
19
|
-
import { SETTINGS_DIALOG_DE } from './settings-dialog.de.js';
|
|
20
|
-
import { SETTINGS_DIALOG_ZH_CN } from './settings-dialog.zh-CN.js';
|
|
21
|
-
import { SETTINGS_DIALOG_ZH_TW } from './settings-dialog.zh-TW.js';
|
|
22
|
-
import { SETTINGS_DIALOG_PT_BR } from './settings-dialog.pt-BR.js';
|
|
23
|
-
import { SETTINGS_DIALOG_RU } from './settings-dialog.ru.js';
|
|
24
|
-
import { SETTINGS_DIALOG_FA } from './settings-dialog.fa.js';
|
|
25
|
-
import { SETTINGS_DIALOG_AR } from './settings-dialog.ar.js';
|
|
26
|
-
import { SETTINGS_DIALOG_JA } from './settings-dialog.ja.js';
|
|
27
|
-
import { SETTINGS_DIALOG_KO } from './settings-dialog.ko.js';
|
|
28
|
-
import { SETTINGS_DIALOG_PL } from './settings-dialog.pl.js';
|
|
29
|
-
import { SETTINGS_DIALOG_HU } from './settings-dialog.hu.js';
|
|
30
|
-
import { SETTINGS_DIALOG_FR } from './settings-dialog.fr.js';
|
|
31
|
-
import { SETTINGS_DIALOG_UK } from './settings-dialog.uk.js';
|
|
32
|
-
import { SETTINGS_DIALOG_TR } from './settings-dialog.tr.js';
|
|
33
|
-
import { SETTINGS_DIALOG_TH } from './settings-dialog.th.js';
|
|
34
|
-
import { SETTINGS_DIALOG_IT } from './settings-dialog.it.js';
|
|
35
|
-
import { SETTINGS_DIALOG_HI } from './settings-dialog.hi.js';
|
|
36
|
-
import { SETTINGS_DIALOG_UR } from './settings-dialog.ur.js';
|
|
37
|
-
import { SETTINGS_DIALOG_BN } from './settings-dialog.bn.js';
|
|
38
|
-
export { SETTINGS_DIALOG_ES, SETTINGS_DIALOG_ID, SETTINGS_DIALOG_DE, SETTINGS_DIALOG_ZH_CN, SETTINGS_DIALOG_ZH_TW, SETTINGS_DIALOG_PT_BR, SETTINGS_DIALOG_RU, SETTINGS_DIALOG_FA, SETTINGS_DIALOG_AR, SETTINGS_DIALOG_JA, SETTINGS_DIALOG_KO, SETTINGS_DIALOG_PL, SETTINGS_DIALOG_HU, SETTINGS_DIALOG_FR, SETTINGS_DIALOG_UK, SETTINGS_DIALOG_TR, SETTINGS_DIALOG_TH, SETTINGS_DIALOG_IT, SETTINGS_DIALOG_HI, SETTINGS_DIALOG_UR, SETTINGS_DIALOG_BN, };
|
|
39
17
|
/** Every non-English locale this dictionary covers, keyed the same way `@jini-ai/ui`'s
|
|
40
18
|
* `SETTINGS_DIALOG_DICTIONARIES` is. */
|
|
41
19
|
export declare const SETTINGS_DIALOG_DICTIONARIES: Readonly<Record<string, Record<string, string>>>;
|
|
@@ -35,7 +35,6 @@ import { SETTINGS_DIALOG_IT } from './settings-dialog.it.js';
|
|
|
35
35
|
import { SETTINGS_DIALOG_HI } from './settings-dialog.hi.js';
|
|
36
36
|
import { SETTINGS_DIALOG_UR } from './settings-dialog.ur.js';
|
|
37
37
|
import { SETTINGS_DIALOG_BN } from './settings-dialog.bn.js';
|
|
38
|
-
export { SETTINGS_DIALOG_ES, SETTINGS_DIALOG_ID, SETTINGS_DIALOG_DE, SETTINGS_DIALOG_ZH_CN, SETTINGS_DIALOG_ZH_TW, SETTINGS_DIALOG_PT_BR, SETTINGS_DIALOG_RU, SETTINGS_DIALOG_FA, SETTINGS_DIALOG_AR, SETTINGS_DIALOG_JA, SETTINGS_DIALOG_KO, SETTINGS_DIALOG_PL, SETTINGS_DIALOG_HU, SETTINGS_DIALOG_FR, SETTINGS_DIALOG_UK, SETTINGS_DIALOG_TR, SETTINGS_DIALOG_TH, SETTINGS_DIALOG_IT, SETTINGS_DIALOG_HI, SETTINGS_DIALOG_UR, SETTINGS_DIALOG_BN, };
|
|
39
38
|
/** Every non-English locale this dictionary covers, keyed the same way `@jini-ai/ui`'s
|
|
40
39
|
* `SETTINGS_DIALOG_DICTIONARIES` is. */
|
|
41
40
|
export const SETTINGS_DIALOG_DICTIONARIES = {
|
|
@@ -49,14 +49,16 @@ export async function ensureSettingDefinitions(deps, input) {
|
|
|
49
49
|
};
|
|
50
50
|
for (const def of input.definitions) {
|
|
51
51
|
const existing = await resolveDefinitionRaw({ repo: deps.settingsRepo }, { namespace: target.namespace, key: def.key, workspaceId });
|
|
52
|
-
if (!existing)
|
|
52
|
+
if (!existing) {
|
|
53
53
|
await registerMissingDefinition(target, def);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
54
56
|
// Core registration is once-only, but the DEFAULT is not. Without this, a
|
|
55
57
|
// `defaultValue` changed in source is unreachable in any install that
|
|
56
58
|
// has already booted — the stored `default_json` from first boot keeps
|
|
57
59
|
// winning, for every settings-dialog tab. See `reconcileDefinitionDefault`
|
|
58
60
|
// for the bug this closes.
|
|
59
|
-
|
|
61
|
+
if (isReconcilableCoreSlot(existing, { ownerKind: target.ownerKind, namespace: target.namespace, key: def.key })) {
|
|
60
62
|
await reconcileExistingCoreDefinition(target, def);
|
|
61
63
|
}
|
|
62
64
|
}
|
|
@@ -43,7 +43,8 @@ export function readValueMutation(body, operation) {
|
|
|
43
43
|
const namespace = String(body.namespace ?? "");
|
|
44
44
|
const key = String(body.key ?? "");
|
|
45
45
|
const scope = body.scope;
|
|
46
|
-
const
|
|
46
|
+
const hasValue = operation === "clear" || "valueJson" in body;
|
|
47
|
+
const complete = namespace && key && SETTING_SCOPES.includes(scope) && hasValue;
|
|
47
48
|
return complete ? { namespace, key, scope } : null;
|
|
48
49
|
}
|
|
49
50
|
//# sourceMappingURL=http-request.js.map
|
|
@@ -177,12 +177,20 @@ export function registerSettingsRoutes(required, optional = {}) {
|
|
|
177
177
|
const close = await openSettingsChangeStream({ request: req, response: res, workspaceId, principalId,
|
|
178
178
|
changeFeed: required.changeFeed, authorize: required.authorize, permission: permissions.read, scheduler: required.scheduler,
|
|
179
179
|
onClose: ({ close }) => { streams.delete(close); }, isDisposed: () => disposed }, optional);
|
|
180
|
-
if (disposed)
|
|
180
|
+
if (disposed) {
|
|
181
181
|
close();
|
|
182
|
-
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (!res.destroyed && !res.writableEnded)
|
|
183
185
|
streams.add(close);
|
|
184
186
|
}));
|
|
185
|
-
return {
|
|
186
|
-
|
|
187
|
+
return {
|
|
188
|
+
dispose: () => {
|
|
189
|
+
disposed = true;
|
|
190
|
+
for (const close of streams)
|
|
191
|
+
close();
|
|
192
|
+
streams.clear();
|
|
193
|
+
},
|
|
194
|
+
};
|
|
187
195
|
}
|
|
188
196
|
//# sourceMappingURL=http.js.map
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** Statuses a definition lookup can resolve to (everything but superseded rows). */
|
|
2
|
+
const RESOLVABLE_STATUSES = new Set(["active", "alias", "tombstone"]);
|
|
1
3
|
/**
|
|
2
4
|
* @file In-memory adapter for `SettingsRepoPort` (the rule-of-two: in-memory + production adapters).
|
|
3
5
|
*
|
|
@@ -45,7 +47,7 @@ export class InMemorySettingsRepo {
|
|
|
45
47
|
return (this.definitions.find((d) => d.namespace === required.namespace &&
|
|
46
48
|
d.key === required.key &&
|
|
47
49
|
d.workspaceId === required.workspaceId &&
|
|
48
|
-
(d.status
|
|
50
|
+
RESOLVABLE_STATUSES.has(d.status)) ?? null);
|
|
49
51
|
}
|
|
50
52
|
async findDefinitionBySettingId(required) {
|
|
51
53
|
const candidates = this.definitions.filter((d) => d.settingId === required.settingId);
|
|
@@ -122,7 +124,8 @@ export class InMemorySettingsRepo {
|
|
|
122
124
|
v.settingId === required.settingId));
|
|
123
125
|
}
|
|
124
126
|
async appendRevision(record) {
|
|
125
|
-
const seq = this.nextSeq
|
|
127
|
+
const seq = this.nextSeq;
|
|
128
|
+
this.nextSeq += 1;
|
|
126
129
|
this.revisions.push({ ...record, seq });
|
|
127
130
|
return seq;
|
|
128
131
|
}
|
|
@@ -40,7 +40,8 @@ export function validateDefinitionInput(input) {
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
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
|
-
|
|
43
|
+
const declaresGlobalScope = (input.scopes & SCOPE_BIT.global) !== 0;
|
|
44
|
+
if (input.workspaceId !== null && declaresGlobalScope) {
|
|
44
45
|
return {
|
|
45
46
|
valid: false,
|
|
46
47
|
error: new DefinitionInvalidError(
|
|
@@ -77,6 +78,9 @@ export function validateValueAgainstSchema(schema, value) {
|
|
|
77
78
|
// registering feature's own write-path job — see
|
|
78
79
|
// docs/decisions/settings-json-schema-variant.md.
|
|
79
80
|
return true;
|
|
81
|
+
default:
|
|
82
|
+
// Unreachable for a typed schema; a stored row with an unknown type validates nothing.
|
|
83
|
+
return false;
|
|
80
84
|
}
|
|
81
85
|
}
|
|
82
86
|
/** Identity-registry of total coercers keyed by `coercionTag`. `"identity"` is always registered. See docs/decisions/DR-003-settings-ledger-invariants.md. */
|
|
@@ -26,6 +26,14 @@ const CROSS_PRINCIPAL_SETTINGS_READ_PERMISSION = "settings.user.read";
|
|
|
26
26
|
* @complexity O(1); at most one `authorize()` call (none for a self-read or no-op read).
|
|
27
27
|
* @overallScore 100
|
|
28
28
|
*/
|
|
29
|
+
/** Why a dotted key did not resolve to exactly one namespace/key pair. */
|
|
30
|
+
function dottedKeyMismatchMessage(matches) {
|
|
31
|
+
if (matches.length === 0) {
|
|
32
|
+
return "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.";
|
|
33
|
+
}
|
|
34
|
+
const candidates = matches.map(match => `namespace=${match.namespace}, key=${match.key}`).join("; ");
|
|
35
|
+
return `settings_set_value: dotted key is ambiguous (${candidates}). Send namespace and key explicitly.`;
|
|
36
|
+
}
|
|
29
37
|
async function resolveOwnOrOtherPrincipalRead(routeDeps, required) {
|
|
30
38
|
const { requestedPrincipalId, callerPrincipalId } = required;
|
|
31
39
|
// Defaults to the CALLER, never to `undefined`. `undefined` does not mean "the caller" to
|
|
@@ -271,9 +279,7 @@ export function buildSettingsRegistrations(routeDeps) {
|
|
|
271
279
|
await routeDeps.settingsUiTabsReady;
|
|
272
280
|
const matches = await findDottedKeySplits(routeDeps, input.key);
|
|
273
281
|
if (matches.length !== 1)
|
|
274
|
-
throw new ToolInputError({ message: matches
|
|
275
|
-
? `settings_set_value: dotted key is ambiguous (${matches.map(match => `namespace=${match.namespace}, key=${match.key}`).join("; ")}). Send namespace and key explicitly.`
|
|
276
|
-
: "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." });
|
|
282
|
+
throw new ToolInputError({ message: dottedKeyMismatchMessage(matches) });
|
|
277
283
|
return { input: { ...input, ...matches[0] }, notes: ["dotted key→namespace + key"] };
|
|
278
284
|
} },
|
|
279
285
|
}, { unwiredToolIds: UNWIRED_SETTINGS_TOOL_IDS });
|
|
@@ -154,15 +154,19 @@ async function assertTargetPrincipalInWorkspace(deps, input) {
|
|
|
154
154
|
* The caller's enclosing transaction rolls the appended revision back on conflict.
|
|
155
155
|
* @complexity At most one scoped value read; no reads when the option is omitted.
|
|
156
156
|
*/
|
|
157
|
+
/** The one stored value the write addresses, at its own scope. */
|
|
158
|
+
function readAddressedValue({ repo, settingId, input }) {
|
|
159
|
+
if (input.scope === "global")
|
|
160
|
+
return repo.getGlobalValue(settingId);
|
|
161
|
+
if (input.scope === "workspace")
|
|
162
|
+
return repo.getWorkspaceValue({ workspaceId: input.workspaceId, settingId });
|
|
163
|
+
return repo.getUserValue({ workspaceId: input.workspaceId, principalId: input.principalId ?? input.callerPrincipalId, settingId });
|
|
164
|
+
}
|
|
157
165
|
async function assertExpectedRevision(required, optional = {}) {
|
|
158
166
|
if (optional.expectedRevisionSeq === undefined)
|
|
159
167
|
return;
|
|
160
168
|
const { repo, settingId, input } = required;
|
|
161
|
-
const current = input
|
|
162
|
-
? await repo.getGlobalValue(settingId)
|
|
163
|
-
: input.scope === "workspace"
|
|
164
|
-
? await repo.getWorkspaceValue({ workspaceId: input.workspaceId, settingId })
|
|
165
|
-
: await repo.getUserValue({ workspaceId: input.workspaceId, principalId: input.principalId ?? input.callerPrincipalId, settingId });
|
|
169
|
+
const current = await readAddressedValue({ repo, settingId, input });
|
|
166
170
|
const actualRevisionSeq = current?.seq ?? 0;
|
|
167
171
|
if (actualRevisionSeq !== optional.expectedRevisionSeq) {
|
|
168
172
|
throw new SettingsRevisionConflictError({ expectedRevisionSeq: optional.expectedRevisionSeq, actualRevisionSeq });
|
|
@@ -198,7 +202,7 @@ export async function set(required, optional = {}) {
|
|
|
198
202
|
throw new ValueValidationFailedError(`value for '${input.namespace}.${input.key}' does not match the definition schema`);
|
|
199
203
|
}
|
|
200
204
|
await assertTargetPrincipalInWorkspace(deps, input);
|
|
201
|
-
|
|
205
|
+
return deps.repo.transaction(async () => {
|
|
202
206
|
const now = kernelNowIso({ clock: deps.clock });
|
|
203
207
|
const revisionSeq = await deps.repo.appendRevision({
|
|
204
208
|
entityKind: "value",
|
|
@@ -246,7 +250,6 @@ export async function set(required, optional = {}) {
|
|
|
246
250
|
}
|
|
247
251
|
return { value: input.value, revisionSeq };
|
|
248
252
|
});
|
|
249
|
-
return result;
|
|
250
253
|
}
|
|
251
254
|
/**
|
|
252
255
|
* Authorize -> cleared value + revision in one transaction; the optional concurrency guard has
|
|
@@ -327,8 +330,7 @@ export async function clear(required, optional = {}) {
|
|
|
327
330
|
};
|
|
328
331
|
// `skipTransaction` means an enclosing caller already owns the boundary, so
|
|
329
332
|
// this write joins it and a later failure rolls this one back too.
|
|
330
|
-
|
|
331
|
-
return result;
|
|
333
|
+
return input.skipTransaction ? runClear() : deps.repo.transaction(runClear);
|
|
332
334
|
}
|
|
333
335
|
/**
|
|
334
336
|
* — an explicit, human-invoked orchestrator: authorize
|
package/dist/target-scope.js
CHANGED
|
@@ -65,7 +65,8 @@ export function applyTargetScope(required, optional = {}) {
|
|
|
65
65
|
const target = ctx.input[param];
|
|
66
66
|
if (typeof target !== 'string')
|
|
67
67
|
throw new ToolInputError({ message: `${param} must be a string` });
|
|
68
|
-
const
|
|
68
|
+
const input = { ...ctx.input };
|
|
69
|
+
delete input[param];
|
|
69
70
|
if (!scoped) {
|
|
70
71
|
if (!await required.isCurrentTarget({ target }))
|
|
71
72
|
throw new ToolInputError({ message: refuseMessage });
|
package/dist/text/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { sanitizeUntrustedText, stripControlSequences } from './sanitize-untrusted-text.js';
|
|
2
2
|
export type { SanitizeTextOptions } from './sanitize-untrusted-text.js';
|
|
3
3
|
export { escapeHtml, escapeXml } from "./escape.js";
|
|
4
|
+
export { trimChars, trimCharsEnd, trimCharsStart } from "../primitives/trim-chars.js";
|
|
5
|
+
export { captureLineTail } from "./line-tail.js";
|
|
4
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/text/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { sanitizeUntrustedText, stripControlSequences } from './sanitize-untrusted-text.js';
|
|
2
2
|
export { escapeHtml, escapeXml } from "./escape.js";
|
|
3
|
+
export { trimChars, trimCharsEnd, trimCharsStart } from "../primitives/trim-chars.js";
|
|
4
|
+
export { captureLineTail } from "./line-tail.js";
|
|
3
5
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file A linear-time stand-in for the regex tail `\s*(.+)$` / `\s+(.+)$`. In those spellings the
|
|
3
|
+
* whitespace run and `.+` can both match the same characters, so the engine backtracks
|
|
4
|
+
* quadratically on long whitespace runs. A leaf module with no imports.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Returns what `(.+)` captures when `rest` is matched against `\s{minWhitespace,}(.+)$` from its
|
|
8
|
+
* start, or null when that pattern does not match. A whitespace-only `rest` captures its final
|
|
9
|
+
* character, exactly as the greedy regex gives back one character to `.+`.
|
|
10
|
+
* @complexity O(rest length).
|
|
11
|
+
*/
|
|
12
|
+
export declare function captureLineTail({ rest }: {
|
|
13
|
+
rest: string;
|
|
14
|
+
}, { minWhitespace }?: {
|
|
15
|
+
minWhitespace?: 0 | 1;
|
|
16
|
+
}): string | null;
|
|
17
|
+
//# sourceMappingURL=line-tail.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file A linear-time stand-in for the regex tail `\s*(.+)$` / `\s+(.+)$`. In those spellings the
|
|
3
|
+
* whitespace run and `.+` can both match the same characters, so the engine backtracks
|
|
4
|
+
* quadratically on long whitespace runs. A leaf module with no imports.
|
|
5
|
+
*/
|
|
6
|
+
/** Characters `.` does not match without the `s` flag. */
|
|
7
|
+
const LINE_TERMINATOR = /[\n\r\u2028\u2029]/;
|
|
8
|
+
/**
|
|
9
|
+
* Returns what `(.+)` captures when `rest` is matched against `\s{minWhitespace,}(.+)$` from its
|
|
10
|
+
* start, or null when that pattern does not match. A whitespace-only `rest` captures its final
|
|
11
|
+
* character, exactly as the greedy regex gives back one character to `.+`.
|
|
12
|
+
* @complexity O(rest length).
|
|
13
|
+
*/
|
|
14
|
+
export function captureLineTail({ rest }, { minWhitespace = 0 } = {}) {
|
|
15
|
+
const stripped = rest.trimStart();
|
|
16
|
+
if (stripped !== "") {
|
|
17
|
+
const leading = rest.length - stripped.length;
|
|
18
|
+
return leading >= minWhitespace && !LINE_TERMINATOR.test(stripped) ? stripped : null;
|
|
19
|
+
}
|
|
20
|
+
if (rest.length < minWhitespace + 1)
|
|
21
|
+
return null;
|
|
22
|
+
const last = rest.slice(-1);
|
|
23
|
+
return LINE_TERMINATOR.test(last) ? null : last;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=line-tail.js.map
|
|
@@ -79,14 +79,16 @@ function escapeSequenceEnd(text, start) {
|
|
|
79
79
|
*/
|
|
80
80
|
export function stripControlSequences({ text }) {
|
|
81
81
|
let out = '';
|
|
82
|
-
|
|
82
|
+
let i = 0;
|
|
83
|
+
while (i < text.length) {
|
|
83
84
|
const code = text.charCodeAt(i);
|
|
84
85
|
if (code === ESC) {
|
|
85
|
-
i = escapeSequenceEnd(text, i); //
|
|
86
|
+
i = escapeSequenceEnd(text, i) + 1; // escapeSequenceEnd is the last consumed byte; step past it
|
|
86
87
|
continue;
|
|
87
88
|
}
|
|
88
89
|
if (!isOtherControlCode(code))
|
|
89
90
|
out += text[i];
|
|
91
|
+
i += 1;
|
|
90
92
|
}
|
|
91
93
|
return out;
|
|
92
94
|
}
|
|
@@ -103,7 +105,34 @@ const DIAGNOSTIC_TOKENS = new Set([
|
|
|
103
105
|
* @complexity O(n) in token length; pure. */
|
|
104
106
|
function isIdentifierShaped({ token }) {
|
|
105
107
|
return token.length <= 31 && /[a-z]/.test(token)
|
|
106
|
-
&& /^(?:[a-z]{1,15}
|
|
108
|
+
&& /^(?:[a-z]{1,15}|\d{1,15})(?:[-_](?:[a-z]{1,15}|\d{1,15})){2,}$/.test(token);
|
|
109
|
+
}
|
|
110
|
+
const isSpace = (char) => char !== undefined && /\s/.test(char);
|
|
111
|
+
const LABEL_SEPARATORS = ':=';
|
|
112
|
+
/** Replaces the value after a label's `:`/`=` with `[redacted]`, exactly as
|
|
113
|
+
* `match.replace(/([:=]\s*)\S+$/, '$1[redacted]')` does, without that pattern's quadratic
|
|
114
|
+
* backtracking. The value is the final non-space run; its separator is the `:`/`=` right before
|
|
115
|
+
* the spaces preceding it, else the first one inside the run. @complexity O(match length). */
|
|
116
|
+
function redactLabeledValue(match) {
|
|
117
|
+
let valueStart = match.length;
|
|
118
|
+
while (valueStart > 0 && !isSpace(match[valueStart - 1]))
|
|
119
|
+
valueStart -= 1;
|
|
120
|
+
if (valueStart === match.length)
|
|
121
|
+
return match;
|
|
122
|
+
let gapStart = valueStart;
|
|
123
|
+
while (isSpace(match[gapStart - 1]))
|
|
124
|
+
gapStart -= 1;
|
|
125
|
+
const spacedSeparator = gapStart > 0 && gapStart < valueStart && LABEL_SEPARATORS.includes(match.charAt(gapStart - 1));
|
|
126
|
+
if (spacedSeparator)
|
|
127
|
+
return match.slice(0, valueStart) + '[redacted]';
|
|
128
|
+
return redactAfterInnerSeparator(match, valueStart);
|
|
129
|
+
}
|
|
130
|
+
function redactAfterInnerSeparator(match, valueStart) {
|
|
131
|
+
for (let i = valueStart; i < match.length - 1; i += 1) {
|
|
132
|
+
if (LABEL_SEPARATORS.includes(match.charAt(i)))
|
|
133
|
+
return match.slice(0, i + 1) + '[redacted]';
|
|
134
|
+
}
|
|
135
|
+
return match;
|
|
107
136
|
}
|
|
108
137
|
/** Mask credentials before preserving trusted IDs and UPPER_SNAKE diagnostics; redact opaque spans elsewhere.
|
|
109
138
|
* `keepIdentifiers` additionally exempts identifier-shaped runs (see {@link isIdentifierShaped}).
|
|
@@ -113,7 +142,7 @@ function redactSecretLike({ text }, options = {}) {
|
|
|
113
142
|
const keepOpaque = (match) => NON_SECRET_TOKEN_RE.test(match) || DIAGNOSTIC_TOKENS.has(match)
|
|
114
143
|
|| (options.keepIdentifiers === true && isIdentifierShaped({ token: match }));
|
|
115
144
|
return redactSecrets({ input: text }, { redactPii: false, exactSecrets: options.exactSecrets ?? [] })
|
|
116
|
-
.replace(LABELED_SECRET_RE, match => match
|
|
145
|
+
.replace(LABELED_SECRET_RE, match => redactLabeledValue(match))
|
|
117
146
|
.replace(/\[REDACTED:[^\]\r\n]+\]/g, '[redacted]')
|
|
118
147
|
// Trust whole dotted IDs so their components cannot become a general redaction bypass.
|
|
119
148
|
.replace(/[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*/g, token => diagnosticToolIds.has(token) ? token
|
package/dist/tool-exposure.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const SAFE_NAME = /^[A-Za-z_]
|
|
1
|
+
const SAFE_NAME = /^[A-Za-z_]\w{0,63}$/;
|
|
2
2
|
const MAX_NAME_LENGTH = 64;
|
|
3
3
|
/** A stable, non-security hash keeps transformed names independent of discovery order.
|
|
4
4
|
* Collisions are still checked against the entire namespace, never trusted as unique. */
|
|
@@ -11,7 +11,7 @@ function nameHash(id) {
|
|
|
11
11
|
function nameBase(id) {
|
|
12
12
|
if (SAFE_NAME.test(id))
|
|
13
13
|
return id;
|
|
14
|
-
const readable = id.replace(/
|
|
14
|
+
const readable = id.replace(/\W/g, '_');
|
|
15
15
|
return `tool_${readable.slice(0, 49)}_${nameHash(id)}`;
|
|
16
16
|
}
|
|
17
17
|
/** O(c) collision probes for c occupied alternatives; suffix space stays inside the wire limit. */
|
|
@@ -22,7 +22,8 @@ function assignName(index, id) {
|
|
|
22
22
|
let candidate = base;
|
|
23
23
|
let counter = 1;
|
|
24
24
|
while (index.occupied.has(candidate)) {
|
|
25
|
-
const suffix = `_${counter
|
|
25
|
+
const suffix = `_${counter}`;
|
|
26
|
+
counter += 1;
|
|
26
27
|
candidate = `${base.slice(0, MAX_NAME_LENGTH - suffix.length)}${suffix}`;
|
|
27
28
|
}
|
|
28
29
|
index.forward.set(id, candidate);
|
|
@@ -89,6 +90,14 @@ function pinDescriptor(descriptor) {
|
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
92
|
/** Count and byte caps reject new entries without evicting any previously callable definition. */
|
|
93
|
+
/** A tool that can join the snapshot (pinned), or why it is skipped. */
|
|
94
|
+
function promotionCandidate(descriptor, snapshot, limits) {
|
|
95
|
+
if (!descriptor)
|
|
96
|
+
return { reason: 'not-found' };
|
|
97
|
+
const pinned = pinDescriptor(descriptor);
|
|
98
|
+
const reason = limitReason(snapshot, pinned.bytes, limits);
|
|
99
|
+
return reason ? { reason } : { pinned };
|
|
100
|
+
}
|
|
92
101
|
function limitReason(snapshot, bytes, limits) {
|
|
93
102
|
if (snapshot.tools.length >= limits.maxTools)
|
|
94
103
|
return 'tool-limit';
|
|
@@ -107,21 +116,16 @@ function promoteDescriptors(snapshot, toolIds, descriptors, names, limits) {
|
|
|
107
116
|
for (const toolId of new Set(toolIds)) {
|
|
108
117
|
if (published.has(toolId))
|
|
109
118
|
continue;
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
112
|
-
skipped.push({ toolId, reason:
|
|
113
|
-
continue;
|
|
119
|
+
const candidate = promotionCandidate(available.get(toolId), { tools, schemaBytes, revision: snapshot.revision }, limits);
|
|
120
|
+
if ('reason' in candidate) {
|
|
121
|
+
skipped.push({ toolId, reason: candidate.reason });
|
|
114
122
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
else {
|
|
124
|
+
assignName(names, toolId);
|
|
125
|
+
tools.push({ wireName: names.forward.get(toolId), descriptor: candidate.pinned.descriptor });
|
|
126
|
+
schemaBytes += candidate.pinned.bytes;
|
|
127
|
+
added.push(toolId);
|
|
120
128
|
}
|
|
121
|
-
assignName(names, toolId);
|
|
122
|
-
tools.push({ wireName: names.forward.get(toolId), descriptor: pinned.descriptor });
|
|
123
|
-
schemaBytes += pinned.bytes;
|
|
124
|
-
added.push(toolId);
|
|
125
129
|
}
|
|
126
130
|
let next = snapshot;
|
|
127
131
|
if (added.length > 0)
|