@cosmicdrift/kumiko-samples 0.401.0 → 0.402.0
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/package.json +1 -1
- package/packages/bundled-features/package.json +1 -1
- package/packages/bundled-features/src/agent-tools/agent-manifest.ts +177 -42
- package/packages/bundled-features/src/agent-tools/changes.json +19 -0
- package/packages/bundled-features/src/agent-tools/tool-catalog.ts +8 -15
- package/packages/bundled-features/src/agent-tools/types.ts +11 -10
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +2 -2
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +7 -0
- package/packages/bundled-features/src/inbound-mail-foundation/entities.ts +1 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-lease.ts +3 -3
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor-types.ts +5 -5
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +5 -7
- package/packages/bundled-features/src/managed-pages/changes.json +6 -0
- package/packages/bundled-features/src/managed-pages/feature.ts +9 -1
- package/packages/bundled-features/src/managed-pages/render-page-body.ts +66 -12
- package/packages/bundled-features/src/managed-pages/web/i18n.ts +1 -2
- package/packages/bundled-features/src/managed-pages/web/lossy-markdown.ts +2 -19
- package/packages/bundled-features/src/managed-pages/web/markdown-to-document.ts +2 -2
- package/packages/bundled-features/src/managed-pages/web/page-body-section.tsx +0 -1
- package/packages/bundled-features/src/readiness/changes.json +6 -0
- package/packages/bundled-features/src/readiness/handlers/run-list.query.ts +2 -1
- package/packages/bundled-features/src/readiness/run-view.ts +4 -5
- package/packages/bundled-features/src/template-resolver/changes.json +12 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +2 -2
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +3 -2
- package/packages/bundled-features/src/template-resolver/index.ts +1 -0
- package/packages/bundled-features/src/template-resolver/web/document-schema.ts +18 -1
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +2 -2
- package/packages/bundled-features/src/template-resolver/web/use-collection-entry-create.ts +5 -3
- package/packages/bundled-features/src/user-data-rights/boot-checks.ts +41 -33
- package/packages/bundled-features/src/user-data-rights/boot-rules.ts +19 -0
- package/packages/bundled-features/src/user-data-rights/changes.json +12 -0
- package/packages/bundled-features/src/user-data-rights/web/request-deletion-screen.tsx +20 -18
- package/samples/apps/use-all-bundled/src/app/collection-labels-feature.ts +3 -1
- package/samples/apps/use-all-bundled/src/run-config.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-samples",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.402.0",
|
|
4
4
|
"description": "Source trees of the Kumiko sample recipes, sample apps and bundled features in repo layout, for tooling such as few-shot corpus builds.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.402.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -21,6 +21,7 @@ import { translationValueOtherText } from "@cosmicdrift/kumiko-framework/ui-type
|
|
|
21
21
|
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
22
22
|
import * as z from "zod";
|
|
23
23
|
import { recordManifestDenyQns } from "./manifest-deny-qns.ts";
|
|
24
|
+
import { isExplicitlyAgentHidden } from "./tool-catalog.ts";
|
|
24
25
|
import type {
|
|
25
26
|
AgentManifest,
|
|
26
27
|
AgentManifestEntity,
|
|
@@ -34,6 +35,31 @@ import type {
|
|
|
34
35
|
RegistryManifestView,
|
|
35
36
|
} from "./types.ts";
|
|
36
37
|
|
|
38
|
+
const FALLBACK_LOCALE = "en";
|
|
39
|
+
|
|
40
|
+
/** Requested locale, then English, then the first remaining locale in code-point
|
|
41
|
+
* order; `undefined` when the entry has no text at all (the key is then omitted). */
|
|
42
|
+
function pickLabel(labels: Readonly<Record<string, string>>, locale: string): string | undefined {
|
|
43
|
+
const nonEmpty = (value: string | undefined): string | undefined =>
|
|
44
|
+
value !== undefined && value !== "" ? value : undefined;
|
|
45
|
+
const firstLocale = Object.keys(labels)
|
|
46
|
+
.sort(compareIds)
|
|
47
|
+
.find((key) => nonEmpty(labels[key]));
|
|
48
|
+
return (
|
|
49
|
+
nonEmpty(labels[locale]) ??
|
|
50
|
+
nonEmpty(labels[FALLBACK_LOCALE]) ??
|
|
51
|
+
(firstLocale !== undefined ? labels[firstLocale] : undefined)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function labelProp(
|
|
56
|
+
labels: Readonly<Record<string, string>>,
|
|
57
|
+
locale: string,
|
|
58
|
+
): { readonly label?: string } {
|
|
59
|
+
const label = pickLabel(labels, locale);
|
|
60
|
+
return label === undefined ? {} : { label };
|
|
61
|
+
}
|
|
62
|
+
|
|
37
63
|
/** The manifest surfaces one string per locale — a plural entry reads as its
|
|
38
64
|
* CLDR `other` form since the manifest has no `count` to select a category. */
|
|
39
65
|
function entryAsLabels(entry: TranslationEntry): Readonly<Record<string, string>> {
|
|
@@ -135,6 +161,7 @@ function buildField(
|
|
|
135
161
|
field: FieldDefinition,
|
|
136
162
|
translations: TranslationKeys,
|
|
137
163
|
searchableFields: ReadonlySet<string>,
|
|
164
|
+
locale: string,
|
|
138
165
|
): AgentManifestField {
|
|
139
166
|
const options =
|
|
140
167
|
field.type === "select" || field.type === "multiSelect" ? field.options : undefined;
|
|
@@ -143,7 +170,7 @@ function buildField(
|
|
|
143
170
|
return {
|
|
144
171
|
name: fieldName,
|
|
145
172
|
type: field.type,
|
|
146
|
-
|
|
173
|
+
...labelProp(labelsForSuffix(translations, `:entity:${entityName}:field:${fieldName}`), locale),
|
|
147
174
|
...(field.description !== undefined && { description: field.description }),
|
|
148
175
|
...(options !== undefined && { options }),
|
|
149
176
|
...(references !== undefined && { references }),
|
|
@@ -153,23 +180,25 @@ function buildField(
|
|
|
153
180
|
};
|
|
154
181
|
}
|
|
155
182
|
|
|
156
|
-
/**
|
|
157
|
-
*
|
|
158
|
-
* The manifest describes shape, never data. */
|
|
183
|
+
/** Shape, never data. Only entities the role reaches are emitted: the manifest is
|
|
184
|
+
* prompt payload, so every extra schema costs tokens on each turn. */
|
|
159
185
|
function buildEntities(
|
|
160
186
|
entities: ReadonlyMap<string, EntityDefinition>,
|
|
161
187
|
translations: TranslationKeys,
|
|
162
188
|
getSearchableFields: (entityName: string) => readonly string[],
|
|
189
|
+
visibleEntityNames: ReadonlySet<string>,
|
|
190
|
+
locale: string,
|
|
163
191
|
): readonly AgentManifestEntity[] {
|
|
164
192
|
const result: AgentManifestEntity[] = [];
|
|
165
193
|
for (const [entityName, entity] of entities) {
|
|
194
|
+
if (!visibleEntityNames.has(entityName)) continue;
|
|
166
195
|
const searchableFields = new Set(getSearchableFields(entityName));
|
|
167
196
|
const fields = Object.entries(entity.fields).map(([fieldName, field]) =>
|
|
168
|
-
buildField(entityName, fieldName, field, translations, searchableFields),
|
|
197
|
+
buildField(entityName, fieldName, field, translations, searchableFields, locale),
|
|
169
198
|
);
|
|
170
199
|
result.push({
|
|
171
200
|
name: entityName,
|
|
172
|
-
|
|
201
|
+
...labelProp(labelsForSuffix(translations, `:entity:${entityName}`), locale),
|
|
173
202
|
...(entity.description !== undefined && { description: entity.description }),
|
|
174
203
|
fields,
|
|
175
204
|
});
|
|
@@ -177,14 +206,32 @@ function buildEntities(
|
|
|
177
206
|
return result;
|
|
178
207
|
}
|
|
179
208
|
|
|
209
|
+
const FEATURE_DESCRIPTION_MAX_CHARS = 300;
|
|
210
|
+
|
|
211
|
+
// Only ". " before a capital ends the sentence, so "e.g. the" and "Art. 15" stay whole.
|
|
212
|
+
const SENTENCE_END = /\.\s+(?=\p{Lu})/u;
|
|
213
|
+
|
|
214
|
+
/** The manifest is prompt payload; the full text stays in `FeatureDefinition.description`
|
|
215
|
+
* for the docs generators. */
|
|
216
|
+
function leadSentence(description: string): string {
|
|
217
|
+
const text = description.replace(/\s+/g, " ").trim();
|
|
218
|
+
const end = SENTENCE_END.exec(text);
|
|
219
|
+
const sentence = end === null ? text : text.slice(0, end.index + 1);
|
|
220
|
+
return sentence.slice(0, FEATURE_DESCRIPTION_MAX_CHARS);
|
|
221
|
+
}
|
|
222
|
+
|
|
180
223
|
function buildFeatures(
|
|
181
224
|
features: ReadonlyMap<string, FeatureDefinition>,
|
|
225
|
+
visibleFeatureNames: ReadonlySet<string>,
|
|
182
226
|
): readonly AgentManifestFeature[] {
|
|
183
227
|
const result: AgentManifestFeature[] = [];
|
|
184
228
|
for (const [, feature] of features) {
|
|
229
|
+
if (!visibleFeatureNames.has(feature.name)) continue;
|
|
185
230
|
result.push({
|
|
186
231
|
name: feature.name,
|
|
187
|
-
...(feature.description !== undefined && {
|
|
232
|
+
...(feature.description !== undefined && {
|
|
233
|
+
description: leadSentence(feature.description),
|
|
234
|
+
}),
|
|
188
235
|
...(feature.uiHints?.displayLabel !== undefined && {
|
|
189
236
|
displayLabel: feature.uiHints.displayLabel,
|
|
190
237
|
}),
|
|
@@ -237,7 +284,8 @@ function buildHandlerEntry(
|
|
|
237
284
|
|
|
238
285
|
// `agent.expose: true` without a `description` still opts in — the author
|
|
239
286
|
// asked for exposure explicitly, there's just nothing to show as prose.
|
|
240
|
-
const description =
|
|
287
|
+
const description =
|
|
288
|
+
def.description !== undefined && def.description !== "" ? def.description : undefined;
|
|
241
289
|
|
|
242
290
|
let inputSchema: Readonly<Record<string, unknown>>;
|
|
243
291
|
try {
|
|
@@ -254,7 +302,7 @@ function buildHandlerEntry(
|
|
|
254
302
|
return {
|
|
255
303
|
qn,
|
|
256
304
|
kind,
|
|
257
|
-
description,
|
|
305
|
+
...(description !== undefined && { description }),
|
|
258
306
|
risk: exposure.risk,
|
|
259
307
|
inputSchema,
|
|
260
308
|
...(entity !== undefined && { entity }),
|
|
@@ -298,6 +346,7 @@ function buildNavs(
|
|
|
298
346
|
translations: TranslationKeys,
|
|
299
347
|
roles: readonly string[],
|
|
300
348
|
screens: ReadonlyMap<string, ScreenDefinition>,
|
|
349
|
+
locale: string,
|
|
301
350
|
): readonly AgentManifestNav[] {
|
|
302
351
|
const result: AgentManifestNav[] = [];
|
|
303
352
|
for (const [, nav] of navs) {
|
|
@@ -312,10 +361,10 @@ function buildNavs(
|
|
|
312
361
|
|
|
313
362
|
result.push({
|
|
314
363
|
id: nav.id,
|
|
315
|
-
|
|
364
|
+
...labelProp(entryAsLabels(translations[nav.label] ?? {}), locale),
|
|
316
365
|
...(nav.screen !== undefined && { screen: nav.screen }),
|
|
317
366
|
...(nav.parent !== undefined && { parent: nav.parent }),
|
|
318
|
-
workspaces: visibleWorkspaces,
|
|
367
|
+
...(visibleWorkspaces.length > 0 && { workspaces: visibleWorkspaces }),
|
|
319
368
|
});
|
|
320
369
|
}
|
|
321
370
|
return withoutOrphanedNavs(result);
|
|
@@ -333,6 +382,70 @@ function withoutOrphanedNavs(navs: readonly AgentManifestNav[]): readonly AgentM
|
|
|
333
382
|
}
|
|
334
383
|
}
|
|
335
384
|
|
|
385
|
+
// Qualified handler, screen and nav ids all start with the owning feature name.
|
|
386
|
+
function featureNameOf(qualifiedId: string): string {
|
|
387
|
+
const idx = qualifiedId.indexOf(":");
|
|
388
|
+
return idx === -1 ? qualifiedId : qualifiedId.slice(0, idx);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function visibleFeatureNamesOf(
|
|
392
|
+
handlers: readonly AgentManifestHandler[],
|
|
393
|
+
screens: readonly AgentManifestScreen[],
|
|
394
|
+
navs: readonly AgentManifestNav[],
|
|
395
|
+
): ReadonlySet<string> {
|
|
396
|
+
return new Set(
|
|
397
|
+
[...handlers.map((h) => h.qn), ...screens.map((s) => s.id), ...navs.map((n) => n.id)].map(
|
|
398
|
+
featureNameOf,
|
|
399
|
+
),
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function entitiesOfQueryToolsFor(
|
|
404
|
+
registry: RegistryManifestView,
|
|
405
|
+
roles: readonly string[],
|
|
406
|
+
denyQns: ReadonlySet<string>,
|
|
407
|
+
): readonly string[] {
|
|
408
|
+
const names: string[] = [];
|
|
409
|
+
for (const [qn, def] of registry.getAllQueryHandlers()) {
|
|
410
|
+
const entityName = registry.getHandlerEntity(qn);
|
|
411
|
+
if (entityName === undefined || denyQns.has(qn) || isExplicitlyAgentHidden(def)) continue;
|
|
412
|
+
if (hasAccess({ roles }, def.access)) names.push(entityName);
|
|
413
|
+
}
|
|
414
|
+
return names;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// A reference target is "entity" or "<feature>:entity"; registry entity keys are the bare name.
|
|
418
|
+
function entityNameOfReference(target: string): string {
|
|
419
|
+
return target.slice(target.lastIndexOf(":") + 1);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function referenceTargetsOf(entity: EntityDefinition | undefined): readonly string[] {
|
|
423
|
+
return Object.values(entity?.fields ?? {}).flatMap((field) =>
|
|
424
|
+
field.type === "reference" ? [entityNameOfReference(field.entity)] : [],
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Reachable = tagged on a manifest handler, shown by a manifest screen, or read by a
|
|
429
|
+
* registry-derived `get_`/`list_` tool (which takes labels from the manifest entity). */
|
|
430
|
+
function visibleEntityNamesOf(
|
|
431
|
+
registry: RegistryManifestView,
|
|
432
|
+
roles: readonly string[],
|
|
433
|
+
denyQns: ReadonlySet<string>,
|
|
434
|
+
handlers: readonly AgentManifestHandler[],
|
|
435
|
+
screens: readonly AgentManifestScreen[],
|
|
436
|
+
): ReadonlySet<string> {
|
|
437
|
+
const reachable = new Set<string>([
|
|
438
|
+
...handlers.flatMap((handler) => (handler.entity !== undefined ? [handler.entity] : [])),
|
|
439
|
+
...screens.flatMap((screen) => [screen.entity ?? [], screen.detailFor ?? []].flat()),
|
|
440
|
+
...entitiesOfQueryToolsFor(registry, roles, denyQns),
|
|
441
|
+
]);
|
|
442
|
+
const entities = registry.getAllEntities();
|
|
443
|
+
return new Set([
|
|
444
|
+
...reachable,
|
|
445
|
+
...[...reachable].flatMap((n) => referenceTargetsOf(entities.get(n))),
|
|
446
|
+
]);
|
|
447
|
+
}
|
|
448
|
+
|
|
336
449
|
function lastSegment(qualifiedId: string): string {
|
|
337
450
|
const idx = qualifiedId.lastIndexOf(":");
|
|
338
451
|
return idx === -1 ? qualifiedId : qualifiedId.slice(idx + 1);
|
|
@@ -346,10 +459,42 @@ function screenTitles(
|
|
|
346
459
|
translations: TranslationKeys,
|
|
347
460
|
screenId: string,
|
|
348
461
|
shortId: string,
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
462
|
+
locale: string,
|
|
463
|
+
): { readonly title?: string } {
|
|
464
|
+
const title = pickLabel(
|
|
465
|
+
entryAsLabels(
|
|
466
|
+
translations[`${screenId}.title`] ?? translations[`screen:${shortId}.title`] ?? {},
|
|
467
|
+
),
|
|
468
|
+
locale,
|
|
352
469
|
);
|
|
470
|
+
return title === undefined ? {} : { title };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function buildScreenEntry(
|
|
474
|
+
screen: ScreenDefinition,
|
|
475
|
+
screenWorkspaces: readonly string[],
|
|
476
|
+
translations: TranslationKeys,
|
|
477
|
+
locale: string,
|
|
478
|
+
): AgentManifestScreen {
|
|
479
|
+
return {
|
|
480
|
+
id: screen.id,
|
|
481
|
+
type: screen.type,
|
|
482
|
+
...screenTitles(translations, screen.id, lastSegment(screen.id), locale),
|
|
483
|
+
// `screen.description` may be a raw i18n key (`registry.getAllTranslations()`
|
|
484
|
+
// double-prefixes it, see build-app-schema.ts:77-81) — match on suffix like
|
|
485
|
+
// `labelsForSuffix` does for entity/field labels, else fall back to the
|
|
486
|
+
// literal string as authored.
|
|
487
|
+
...(screen.description !== undefined && {
|
|
488
|
+
description:
|
|
489
|
+
pickLabel(labelsForSuffix(translations, screen.description), locale) ?? screen.description,
|
|
490
|
+
}),
|
|
491
|
+
...("entity" in screen && screen.entity !== undefined && { entity: screen.entity }),
|
|
492
|
+
// A detail screen takes the row id under its idParam; everything else is parameterless.
|
|
493
|
+
...(screen.detailFor !== undefined && { params: [detailIdParam(screen)] }),
|
|
494
|
+
...(screenWorkspaces.length > 0 && { workspaces: screenWorkspaces }),
|
|
495
|
+
...(screen.detailFor !== undefined && { detailFor: screen.detailFor }),
|
|
496
|
+
...("handler" in screen && typeof screen.handler === "string" && { handler: screen.handler }),
|
|
497
|
+
};
|
|
353
498
|
}
|
|
354
499
|
|
|
355
500
|
function buildScreens(
|
|
@@ -359,6 +504,7 @@ function buildScreens(
|
|
|
359
504
|
translations: TranslationKeys,
|
|
360
505
|
roles: readonly string[],
|
|
361
506
|
visibleNavIds: ReadonlySet<string>,
|
|
507
|
+
locale: string,
|
|
362
508
|
): readonly AgentManifestScreen[] {
|
|
363
509
|
const allNavs = [...navs.values()];
|
|
364
510
|
const result: AgentManifestScreen[] = [];
|
|
@@ -382,29 +528,11 @@ function buildScreens(
|
|
|
382
528
|
});
|
|
383
529
|
if (!uiVisible(screen.access, roles) || !reachable) continue;
|
|
384
530
|
|
|
385
|
-
const shortId = lastSegment(screen.id);
|
|
386
531
|
const screenWorkspaces = dedupeSorted(
|
|
387
532
|
accessibleNavs.flatMap((nav) => visibleWorkspaceIds(nav.workspaces, workspaces, roles)),
|
|
388
533
|
);
|
|
389
534
|
|
|
390
|
-
result.push(
|
|
391
|
-
id: screen.id,
|
|
392
|
-
type: screen.type,
|
|
393
|
-
titles: screenTitles(translations, screen.id, shortId),
|
|
394
|
-
// `screen.description` may be a raw i18n key (`registry.getAllTranslations()`
|
|
395
|
-
// double-prefixes it, see build-app-schema.ts:77-81) — match on suffix like
|
|
396
|
-
// `labelsForSuffix` does for entity/field labels, else fall back to the
|
|
397
|
-
// literal string as authored.
|
|
398
|
-
...(screen.description !== undefined && {
|
|
399
|
-
description: labelsForSuffix(translations, screen.description)["en"] ?? screen.description,
|
|
400
|
-
}),
|
|
401
|
-
...("entity" in screen && screen.entity !== undefined && { entity: screen.entity }),
|
|
402
|
-
// A detail screen takes the row id under its idParam; everything else is parameterless.
|
|
403
|
-
params: screen.detailFor !== undefined ? [detailIdParam(screen)] : [],
|
|
404
|
-
workspaces: screenWorkspaces,
|
|
405
|
-
...(screen.detailFor !== undefined && { detailFor: screen.detailFor }),
|
|
406
|
-
...("handler" in screen && typeof screen.handler === "string" && { handler: screen.handler }),
|
|
407
|
-
});
|
|
535
|
+
result.push(buildScreenEntry(screen, screenWorkspaces, translations, locale));
|
|
408
536
|
}
|
|
409
537
|
return result;
|
|
410
538
|
}
|
|
@@ -413,13 +541,14 @@ function buildWorkspaces(
|
|
|
413
541
|
workspaces: ReadonlyMap<string, WorkspaceDefinition>,
|
|
414
542
|
translations: TranslationKeys,
|
|
415
543
|
roles: readonly string[],
|
|
544
|
+
locale: string,
|
|
416
545
|
): readonly AgentManifestWorkspace[] {
|
|
417
546
|
const result: AgentManifestWorkspace[] = [];
|
|
418
547
|
for (const [, workspace] of workspaces) {
|
|
419
548
|
if (!uiVisible(workspace.access, roles)) continue;
|
|
420
549
|
result.push({
|
|
421
550
|
id: workspace.id,
|
|
422
|
-
|
|
551
|
+
...labelProp(entryAsLabels(translations[workspace.label] ?? {}), locale),
|
|
423
552
|
...(workspace.default === true && { default: true as const }),
|
|
424
553
|
});
|
|
425
554
|
}
|
|
@@ -439,19 +568,16 @@ export function buildAgentManifest(
|
|
|
439
568
|
const { roles } = options;
|
|
440
569
|
assertDenyQnsExist(options.denyQns ?? [], registry);
|
|
441
570
|
|
|
442
|
-
const
|
|
443
|
-
const entities = buildEntities(registry.getAllEntities(), translations, (name) =>
|
|
444
|
-
registry.getSearchableFields(name),
|
|
445
|
-
);
|
|
571
|
+
const denyQns = new Set(options.denyQns ?? []);
|
|
446
572
|
const handlers = buildHandlers(
|
|
447
573
|
registry.getAllQueryHandlers(),
|
|
448
574
|
registry.getAllWriteHandlers(),
|
|
449
575
|
roles,
|
|
450
576
|
(qn) => registry.getHandlerEntity(qn),
|
|
451
|
-
|
|
577
|
+
denyQns,
|
|
452
578
|
);
|
|
453
579
|
const screenMap = registry.getAllScreens();
|
|
454
|
-
const navs = buildNavs(navMap, workspaceMap, translations, roles, screenMap);
|
|
580
|
+
const navs = buildNavs(navMap, workspaceMap, translations, roles, screenMap, options.locale);
|
|
455
581
|
const screens = buildScreens(
|
|
456
582
|
screenMap,
|
|
457
583
|
navMap,
|
|
@@ -459,8 +585,17 @@ export function buildAgentManifest(
|
|
|
459
585
|
translations,
|
|
460
586
|
roles,
|
|
461
587
|
new Set(navs.map((nav) => nav.id)),
|
|
588
|
+
options.locale,
|
|
589
|
+
);
|
|
590
|
+
const workspaces = buildWorkspaces(workspaceMap, translations, roles, options.locale);
|
|
591
|
+
const features = buildFeatures(registry.features, visibleFeatureNamesOf(handlers, screens, navs));
|
|
592
|
+
const entities = buildEntities(
|
|
593
|
+
registry.getAllEntities(),
|
|
594
|
+
translations,
|
|
595
|
+
(name) => registry.getSearchableFields(name),
|
|
596
|
+
visibleEntityNamesOf(registry, roles, denyQns, handlers, screens),
|
|
597
|
+
options.locale,
|
|
462
598
|
);
|
|
463
|
-
const workspaces = buildWorkspaces(workspaceMap, translations, roles);
|
|
464
599
|
|
|
465
600
|
const manifest: AgentManifest = {
|
|
466
601
|
builtForRoles: [...roles],
|
|
@@ -475,6 +610,6 @@ export function buildAgentManifest(
|
|
|
475
610
|
...(options.currency && { currency: options.currency }),
|
|
476
611
|
},
|
|
477
612
|
};
|
|
478
|
-
recordManifestDenyQns(manifest,
|
|
613
|
+
recordManifestDenyQns(manifest, denyQns);
|
|
479
614
|
return manifest;
|
|
480
615
|
}
|
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.402.0",
|
|
4
|
+
"type": "breaking",
|
|
5
|
+
"title": "Agent manifest labels are one string in the requested locale",
|
|
6
|
+
"detail": "`buildAgentManifest` used to emit every label in every locale (`labels: { de, en, es }`, often `{}`), although the caller names one `locale`. Entities, fields, navs and workspaces now carry `label?: string`, screens `title?: string`, picked in the requested locale, then English, then the first other locale, and left out when untranslated. Empty `params` and `workspaces` on screens, empty `workspaces` on navs and an empty handler `description` are left out as well (`description`, `params` and `workspaces` are optional in the types). `buildToolCatalog` reads the new fields.",
|
|
7
|
+
"migration": "Read `entity.label`, `field.label`, `nav.label`, `workspace.label` and `screen.title` instead of the `labels`/`titles` maps. Code that only passes the manifest to `buildToolCatalog` or serialises it needs no change."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.402.0",
|
|
11
|
+
"type": "improvement",
|
|
12
|
+
"title": "Agent manifest shows only the first sentence of a feature description",
|
|
13
|
+
"detail": "`buildAgentManifest` emits the first sentence of `FeatureDefinition.description` (a `. ` before a capital letter ends it, so \"e.g.\" and \"Art. 15\" stay whole), at most 300 characters. The full text stays on the definition for the docs generators. In an app with 77 features the descriptions drop from about 46.6k to 12.2k characters."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"version": "0.402.0",
|
|
17
|
+
"type": "improvement",
|
|
18
|
+
"title": "Agent manifest features and entities follow the role",
|
|
19
|
+
"detail": "`buildAgentManifest` now leaves out a feature when none of its handlers, screens or navs remain in the manifest for the role, and an entity when no remaining handler or screen refers to it, no `get_`/`list_` tool of the role reads it and no reachable entity points at it through a reference field (one hop). Before, `features` and `entities` were identical for every role, so a restricted role paid for schemas it could not use in every system prompt. A translation-only feature such as `locale-de` no longer appears in `features`."
|
|
20
|
+
},
|
|
2
21
|
{
|
|
3
22
|
"version": "0.389.0",
|
|
4
23
|
"type": "improvement",
|
|
@@ -104,7 +104,7 @@ function isDetailHandlerQn(qn: string, entityName: string): boolean {
|
|
|
104
104
|
* `resolveAgentExposure` never runs for them. Only an EXPLICIT opt-out may hide one here —
|
|
105
105
|
* the resolver's own default is fail-closed on a missing `description`, and CRUD-generated
|
|
106
106
|
* handlers never carry one, so reusing it would delete every entity tool in every app. */
|
|
107
|
-
function isExplicitlyAgentHidden(def: QueryHandlerDef): boolean {
|
|
107
|
+
export function isExplicitlyAgentHidden(def: QueryHandlerDef): boolean {
|
|
108
108
|
return def.agent?.expose === false;
|
|
109
109
|
}
|
|
110
110
|
|
|
@@ -146,13 +146,9 @@ function addToolsForListHandler(
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
function entityDisplayLabel(
|
|
150
|
-
entity: AgentManifestEntity | undefined,
|
|
151
|
-
entityName: string,
|
|
152
|
-
locale: string,
|
|
153
|
-
): string {
|
|
149
|
+
function entityDisplayLabel(entity: AgentManifestEntity | undefined, entityName: string): string {
|
|
154
150
|
if (entity?.description) return entity.description;
|
|
155
|
-
const label = entity?.
|
|
151
|
+
const label = entity?.label;
|
|
156
152
|
if (label) return label;
|
|
157
153
|
return entityName;
|
|
158
154
|
}
|
|
@@ -285,7 +281,7 @@ function stripFieldFromSchema(
|
|
|
285
281
|
}
|
|
286
282
|
|
|
287
283
|
function handlerDescription(handler: AgentManifestHandler): string {
|
|
288
|
-
return handler.description
|
|
284
|
+
return handler.description ?? `Run ${handler.qn}.`;
|
|
289
285
|
}
|
|
290
286
|
|
|
291
287
|
/** Drops the `query`/`write` verb segment from a handler QN, joins the rest with `_`, and
|
|
@@ -483,12 +479,11 @@ function addGetTools(
|
|
|
483
479
|
registry: RegistrySearchView,
|
|
484
480
|
detailQnByEntity: ReadonlyMap<string, string>,
|
|
485
481
|
entityByName: ReadonlyMap<string, AgentManifestEntity>,
|
|
486
|
-
locale: string,
|
|
487
482
|
sink: CatalogSink,
|
|
488
483
|
): void {
|
|
489
484
|
const getEntries = [...detailQnByEntity.entries()].sort((a, b) => compareByCodePoint(a[0], b[0]));
|
|
490
485
|
for (const [entityName, qn] of getEntries) {
|
|
491
|
-
const label = entityDisplayLabel(entityByName.get(entityName), entityName
|
|
486
|
+
const label = entityDisplayLabel(entityByName.get(entityName), entityName);
|
|
492
487
|
const { tool, descriptor } = buildGetTool(
|
|
493
488
|
entityName,
|
|
494
489
|
qn,
|
|
@@ -504,12 +499,11 @@ function addListTools(
|
|
|
504
499
|
listQnByEntity: ReadonlyMap<string, string>,
|
|
505
500
|
entityByName: ReadonlyMap<string, AgentManifestEntity>,
|
|
506
501
|
roles: readonly string[],
|
|
507
|
-
locale: string,
|
|
508
502
|
sink: CatalogSink,
|
|
509
503
|
): void {
|
|
510
504
|
const listEntries = [...listQnByEntity.entries()].sort((a, b) => compareByCodePoint(a[0], b[0]));
|
|
511
505
|
for (const [entityName, qn] of listEntries) {
|
|
512
|
-
const label = entityDisplayLabel(entityByName.get(entityName), entityName
|
|
506
|
+
const label = entityDisplayLabel(entityByName.get(entityName), entityName);
|
|
513
507
|
const searchableFields = registry.getSearchableFields(entityName);
|
|
514
508
|
const filterableFields = filterableFieldsOf(
|
|
515
509
|
entityByName.get(entityName),
|
|
@@ -636,7 +630,6 @@ export function buildToolCatalog(
|
|
|
636
630
|
): ToolCatalog {
|
|
637
631
|
const sink: CatalogSink = { tools: [], dispatchTable: new Map(), usedNames: new Set() };
|
|
638
632
|
const roleFilter = { roles: manifest.builtForRoles };
|
|
639
|
-
const locale = manifest.tenantSettings.locale;
|
|
640
633
|
const entityByName = new Map(manifest.entities.map((entity) => [entity.name, entity]));
|
|
641
634
|
const denyQns = new Set(options.denyQns ?? []);
|
|
642
635
|
assertCatalogMirrorsManifestDenyQns(manifest, denyQns);
|
|
@@ -647,8 +640,8 @@ export function buildToolCatalog(
|
|
|
647
640
|
roleFilter,
|
|
648
641
|
denyQns,
|
|
649
642
|
);
|
|
650
|
-
addGetTools(registry, detailQnByEntity, entityByName,
|
|
651
|
-
addListTools(registry, listQnByEntity, entityByName, roleFilter.roles,
|
|
643
|
+
addGetTools(registry, detailQnByEntity, entityByName, sink);
|
|
644
|
+
addListTools(registry, listQnByEntity, entityByName, roleFilter.roles, sink);
|
|
652
645
|
addQueryHandlerTools(manifest, entityListDetailQns, denyQns, sink);
|
|
653
646
|
if (options.mode !== "read-only") {
|
|
654
647
|
addWriteHandlerTools(manifest, detailQnByEntity, denyQns, sink);
|
|
@@ -125,8 +125,8 @@ export type AgentManifestOptions = {
|
|
|
125
125
|
export type AgentManifestField = {
|
|
126
126
|
readonly name: string;
|
|
127
127
|
readonly type: string;
|
|
128
|
-
/** Label
|
|
129
|
-
readonly
|
|
128
|
+
/** Label in the requested locale (English, then any other, as fallback); absent when untranslated. */
|
|
129
|
+
readonly label?: string;
|
|
130
130
|
readonly description?: string;
|
|
131
131
|
readonly options?: readonly string[];
|
|
132
132
|
/** Entity a `reference` field points at. */
|
|
@@ -140,7 +140,7 @@ export type AgentManifestField = {
|
|
|
140
140
|
|
|
141
141
|
export type AgentManifestEntity = {
|
|
142
142
|
readonly name: string;
|
|
143
|
-
readonly
|
|
143
|
+
readonly label?: string;
|
|
144
144
|
readonly description?: string;
|
|
145
145
|
readonly fields: readonly AgentManifestField[];
|
|
146
146
|
};
|
|
@@ -148,7 +148,7 @@ export type AgentManifestEntity = {
|
|
|
148
148
|
export type AgentManifestHandler = {
|
|
149
149
|
readonly qn: string;
|
|
150
150
|
readonly kind: "query" | "write";
|
|
151
|
-
readonly description
|
|
151
|
+
readonly description?: string;
|
|
152
152
|
readonly risk: AgentRisk;
|
|
153
153
|
readonly inputSchema: Readonly<Record<string, unknown>>;
|
|
154
154
|
readonly entity?: string;
|
|
@@ -159,11 +159,12 @@ export type AgentManifestHandler = {
|
|
|
159
159
|
export type AgentManifestScreen = {
|
|
160
160
|
readonly id: string;
|
|
161
161
|
readonly type: string;
|
|
162
|
-
readonly
|
|
162
|
+
readonly title?: string;
|
|
163
163
|
readonly description?: string;
|
|
164
164
|
readonly entity?: string;
|
|
165
|
-
|
|
166
|
-
readonly
|
|
165
|
+
/** Only present on detail screens. */
|
|
166
|
+
readonly params?: readonly string[];
|
|
167
|
+
readonly workspaces?: readonly string[];
|
|
167
168
|
/** Entity this screen is the detail view for. Distinct from `entity`: a custom
|
|
168
169
|
* screen can render entity A while being the detail view for B. */
|
|
169
170
|
readonly detailFor?: string;
|
|
@@ -173,15 +174,15 @@ export type AgentManifestScreen = {
|
|
|
173
174
|
|
|
174
175
|
export type AgentManifestNav = {
|
|
175
176
|
readonly id: string;
|
|
176
|
-
readonly
|
|
177
|
+
readonly label?: string;
|
|
177
178
|
readonly screen?: string;
|
|
178
179
|
readonly parent?: string;
|
|
179
|
-
readonly workspaces
|
|
180
|
+
readonly workspaces?: readonly string[];
|
|
180
181
|
};
|
|
181
182
|
|
|
182
183
|
export type AgentManifestWorkspace = {
|
|
183
184
|
readonly id: string;
|
|
184
|
-
readonly
|
|
185
|
+
readonly label?: string;
|
|
185
186
|
readonly default?: true;
|
|
186
187
|
};
|
|
187
188
|
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-renderer/headles
|
|
|
2
2
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
3
3
|
import { Marked } from "marked";
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
isAllowedDocumentUrlHref,
|
|
6
6
|
isDocumentPlaceholderHref,
|
|
7
7
|
} from "../../template-resolver/index.ts";
|
|
8
8
|
import type { CheckoutConsentRecordedPayload } from "../events.ts";
|
|
@@ -149,7 +149,7 @@ function formatDate(iso: string, locale: ConsentLocale, timeZone: string): strin
|
|
|
149
149
|
const ROW_END = "\n\n";
|
|
150
150
|
|
|
151
151
|
function isMailLinkHref(href: string): boolean {
|
|
152
|
-
return
|
|
152
|
+
return isAllowedDocumentUrlHref(href) && !isDocumentPlaceholderHref(href);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
const inlineHtmlMarkdown = new Marked({ gfm: false, breaks: false });
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.402.0",
|
|
4
|
+
"type": "breaking",
|
|
5
|
+
"title": "The inbound mail supervisor requires a lock and the dedup anchor is unique",
|
|
6
|
+
"detail": "`createInboundMailSupervisor` takes a required `lock` (a DistributedLock) instead of an optional one, so only one worker holds the IMAP watch per account; the single-process path without a lock is gone. The unmanaged table store_mail_seen_messages gets a unique index on (account_id, provider_message_id), which the ingest comment always promised; ingest-message already serialized concurrent ingests of one mail with a per-message advisory lock, so the second one reports duplicate.",
|
|
7
|
+
"migration": "Pass `lock: createDistributedLock(redis, \"kumiko:lock:inbound-mail:watch:\")` to createInboundMailSupervisor. Run `kumiko-schema generate <name>` in each app that mounts inbound-mail-foundation to get the migration for the unique index on store_mail_seen_messages; existing rows must hold no duplicate (account_id, provider_message_id)."
|
|
8
|
+
},
|
|
2
9
|
{
|
|
3
10
|
"version": "0.394.0",
|
|
4
11
|
"type": "improvement",
|
|
@@ -231,6 +231,7 @@ export const syncCursorEntity = createEntity({
|
|
|
231
231
|
export const seenMessageEntity = createEntity({
|
|
232
232
|
table: "store_mail_seen_messages",
|
|
233
233
|
softDelete: false,
|
|
234
|
+
indexes: [{ unique: true, columns: ["accountId", "providerMessageId"] }],
|
|
234
235
|
fields: {
|
|
235
236
|
accountId: createTextField({
|
|
236
237
|
required: true,
|
|
@@ -10,7 +10,7 @@ export type WatchLease = {
|
|
|
10
10
|
// The cross-worker claim on one account's watch: acquired before plugin.watch(), renewed by a
|
|
11
11
|
// heartbeat for as long as the watcher state exists, and given up locally once it is lost.
|
|
12
12
|
export function createWatchLease(deps: {
|
|
13
|
-
readonly lock: DistributedLock
|
|
13
|
+
readonly lock: DistributedLock;
|
|
14
14
|
readonly ttlSeconds: number;
|
|
15
15
|
readonly renewIntervalMs: number;
|
|
16
16
|
readonly watchers: ReadonlyMap<string, WatcherState>;
|
|
@@ -53,7 +53,7 @@ export function createWatchLease(deps: {
|
|
|
53
53
|
): Promise<void> {
|
|
54
54
|
state.renewTimer = null;
|
|
55
55
|
// skip: supervisor stopped, generation superseded, or lease already lost — stale timer fire, nothing to renew.
|
|
56
|
-
if (!isRunning() || state.generation !== generation || !state.lockToken
|
|
56
|
+
if (!isRunning() || state.generation !== generation || !state.lockToken) return;
|
|
57
57
|
let renewed: boolean;
|
|
58
58
|
try {
|
|
59
59
|
renewed = await lock.renew(account.id, state.lockToken, ttlSeconds);
|
|
@@ -97,7 +97,7 @@ export function createWatchLease(deps: {
|
|
|
97
97
|
account: MailAccountRecord,
|
|
98
98
|
state: WatcherState,
|
|
99
99
|
): Promise<WatchLeaseResult> {
|
|
100
|
-
if (
|
|
100
|
+
if (state.lockToken) return "acquired";
|
|
101
101
|
// A throwing lock (Redis outage) reads as "not claimed": the next tick retries instead of leaving the start flag set.
|
|
102
102
|
const token = await lock.acquire(account.id, { ttlSeconds: ttlSeconds }).catch((err) => {
|
|
103
103
|
log(
|
|
@@ -41,15 +41,15 @@ export type InboundMailSupervisorDeps = {
|
|
|
41
41
|
/** Coordinates the IMAP watch (not the poll) across worker processes:
|
|
42
42
|
* before `plugin.watch()`, ensureWatcher claims a TTL lease for the
|
|
43
43
|
* account via `lock.acquire` and renews it via `lock.renew` while the
|
|
44
|
-
* watcher lives.
|
|
45
|
-
*
|
|
46
|
-
readonly lock
|
|
44
|
+
* watcher lives. Required: without it every worker would hold its own
|
|
45
|
+
* IDLE connection per account. */
|
|
46
|
+
readonly lock: DistributedLock;
|
|
47
47
|
/** Access-token lifecycle for OAuth accounts. Defaults to a manager built
|
|
48
48
|
* from `db` + `providerCtx.secrets`; pass the instance shared with
|
|
49
49
|
* `createInboundMailConnectRoutes` so a fresh connect primes this cache. */
|
|
50
50
|
readonly oauthTokens?: OAuthAccessTokenManager;
|
|
51
51
|
/** TTL (seconds) for the per-account watch lease. Default 90s, renewed
|
|
52
|
-
* every ttl/3.
|
|
52
|
+
* every ttl/3. */
|
|
53
53
|
readonly watchLeaseTtlSeconds?: number;
|
|
54
54
|
readonly log?: (line: string) => void;
|
|
55
55
|
};
|
|
@@ -62,7 +62,7 @@ export type WatcherState = {
|
|
|
62
62
|
* an already stopped watcher. */
|
|
63
63
|
generation: number;
|
|
64
64
|
/** Token from `deps.lock.acquire` while this worker owns the account's
|
|
65
|
-
* watch lease; null when unclaimed (
|
|
65
|
+
* watch lease; null when unclaimed (claim lost). */
|
|
66
66
|
lockToken: string | null;
|
|
67
67
|
renewTimer: ReturnType<typeof setTimeout> | null;
|
|
68
68
|
/** Consecutive renew() throws — tear down once failures span a full TTL. */
|