@jini-ai/cms 0.3.6 → 0.3.8
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/dist/content-types/index-provisioning.d.ts +1 -1
- package/dist/content-types/index-provisioning.js +1 -1
- package/dist/content-types/write-service.js +31 -11
- package/dist/core/entity-liveness.d.ts +55 -0
- package/dist/core/entity-liveness.js +38 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/ports.d.ts +1 -1
- package/dist/core/tools/registration-kit.d.ts +55 -17
- package/dist/core/tools/registration-kit.js +49 -20
- package/dist/identity/auth-service.d.ts +1 -1
- package/dist/identity/auth-service.js +1 -1
- package/dist/identity/seed.d.ts +1 -1
- package/dist/identity/seed.js +3 -3
- package/dist/media/agent-tools.d.ts +13 -3
- package/dist/media/agent-tools.js +45 -10
- package/dist/media/html-attributes.d.ts +6 -6
- package/dist/media/html-attributes.js +6 -6
- package/dist/media/index.d.ts +1 -1
- package/dist/media/index.js +1 -1
- package/dist/media/media-service.d.ts +2 -1
- package/dist/media/media-service.js +23 -7
- package/dist/media/tool-registrations.d.ts +12 -0
- package/dist/media/tool-registrations.js +20 -12
- package/dist/media/types.d.ts +1 -1
- package/dist/navigation/index.d.ts +2 -2
- package/dist/navigation/index.js +2 -2
- package/dist/navigation/menu-service.d.ts +4 -4
- package/dist/navigation/menu-service.js +4 -4
- package/dist/settings/dictionaries/index.d.ts +3 -3
- package/dist/settings/dictionaries/index.js +3 -3
- package/dist/taxonomy/agent-tools.d.ts +12 -17
- package/dist/taxonomy/agent-tools.js +53 -29
- package/dist/taxonomy/content-lookup.d.ts +35 -0
- package/dist/taxonomy/content-lookup.js +28 -0
- package/dist/taxonomy/index.d.ts +2 -2
- package/dist/taxonomy/index.js +2 -2
- package/dist/taxonomy/repo.memory.d.ts +11 -2
- package/dist/taxonomy/repo.memory.js +11 -0
- package/dist/taxonomy/write-service.d.ts +44 -15
- package/dist/taxonomy/write-service.js +60 -8
- package/package.json +3 -2
- package/dist/content-types/agent-tools.d.ts.map +0 -1
- package/dist/content-types/agent-tools.js.map +0 -1
- package/dist/content-types/cleanup.d.ts.map +0 -1
- package/dist/content-types/cleanup.js.map +0 -1
- package/dist/content-types/errors.d.ts.map +0 -1
- package/dist/content-types/errors.js.map +0 -1
- package/dist/content-types/field-defs.d.ts.map +0 -1
- package/dist/content-types/field-defs.js.map +0 -1
- package/dist/content-types/index-provisioning.d.ts.map +0 -1
- package/dist/content-types/index-provisioning.js.map +0 -1
- package/dist/content-types/index.d.ts.map +0 -1
- package/dist/content-types/index.js.map +0 -1
- package/dist/content-types/lifecycle-dispatch.d.ts.map +0 -1
- package/dist/content-types/lifecycle-dispatch.js.map +0 -1
- package/dist/content-types/lifecycle.d.ts.map +0 -1
- package/dist/content-types/lifecycle.js.map +0 -1
- package/dist/content-types/list.d.ts.map +0 -1
- package/dist/content-types/list.js.map +0 -1
- package/dist/content-types/repo.memory.d.ts.map +0 -1
- package/dist/content-types/repo.memory.js.map +0 -1
- package/dist/content-types/tool-registrations.d.ts.map +0 -1
- package/dist/content-types/tool-registrations.js.map +0 -1
- package/dist/content-types/types.d.ts.map +0 -1
- package/dist/content-types/types.js.map +0 -1
- package/dist/content-types/write-service.d.ts.map +0 -1
- package/dist/content-types/write-service.js.map +0 -1
- package/dist/core/commands/change-set.d.ts.map +0 -1
- package/dist/core/commands/change-set.js.map +0 -1
- package/dist/core/commands/command.d.ts.map +0 -1
- package/dist/core/commands/command.js.map +0 -1
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/index.js.map +0 -1
- package/dist/core/ports.d.ts.map +0 -1
- package/dist/core/ports.js.map +0 -1
- package/dist/core/tools/registration-kit.d.ts.map +0 -1
- package/dist/core/tools/registration-kit.js.map +0 -1
- package/dist/entries/agent-tools.d.ts.map +0 -1
- package/dist/entries/agent-tools.js.map +0 -1
- package/dist/entries/errors.d.ts.map +0 -1
- package/dist/entries/errors.js.map +0 -1
- package/dist/entries/field-validation.d.ts.map +0 -1
- package/dist/entries/field-validation.js.map +0 -1
- package/dist/entries/index.d.ts.map +0 -1
- package/dist/entries/index.js.map +0 -1
- package/dist/entries/lifecycle-dispatch.d.ts.map +0 -1
- package/dist/entries/lifecycle-dispatch.js.map +0 -1
- package/dist/entries/list.d.ts.map +0 -1
- package/dist/entries/list.js.map +0 -1
- package/dist/entries/repo.memory.d.ts.map +0 -1
- package/dist/entries/repo.memory.js.map +0 -1
- package/dist/entries/tool-registrations.d.ts.map +0 -1
- package/dist/entries/tool-registrations.js.map +0 -1
- package/dist/entries/types.d.ts.map +0 -1
- package/dist/entries/types.js.map +0 -1
- package/dist/entries/write-service.d.ts.map +0 -1
- package/dist/entries/write-service.js.map +0 -1
- package/dist/identity/admin-crud-service.d.ts.map +0 -1
- package/dist/identity/admin-crud-service.js.map +0 -1
- package/dist/identity/agent-tool-input.d.ts.map +0 -1
- package/dist/identity/agent-tool-input.js.map +0 -1
- package/dist/identity/agent-tools.d.ts.map +0 -1
- package/dist/identity/agent-tools.js.map +0 -1
- package/dist/identity/auth-service.d.ts.map +0 -1
- package/dist/identity/auth-service.js.map +0 -1
- package/dist/identity/authorize.d.ts.map +0 -1
- package/dist/identity/authorize.js.map +0 -1
- package/dist/identity/grant-service.d.ts.map +0 -1
- package/dist/identity/grant-service.js.map +0 -1
- package/dist/identity/hasher.d.ts.map +0 -1
- package/dist/identity/hasher.js.map +0 -1
- package/dist/identity/index.d.ts.map +0 -1
- package/dist/identity/index.js.map +0 -1
- package/dist/identity/password-policy.d.ts.map +0 -1
- package/dist/identity/password-policy.js.map +0 -1
- package/dist/identity/permission-migrations.d.ts.map +0 -1
- package/dist/identity/permission-migrations.js.map +0 -1
- package/dist/identity/permissions.d.ts.map +0 -1
- package/dist/identity/permissions.js.map +0 -1
- package/dist/identity/ports.d.ts.map +0 -1
- package/dist/identity/ports.js.map +0 -1
- package/dist/identity/repo.memory.d.ts.map +0 -1
- package/dist/identity/repo.memory.js.map +0 -1
- package/dist/identity/seed.d.ts.map +0 -1
- package/dist/identity/seed.js.map +0 -1
- package/dist/identity/tool-registrations.d.ts.map +0 -1
- package/dist/identity/tool-registrations.js.map +0 -1
- package/dist/identity/types.d.ts.map +0 -1
- package/dist/identity/types.js.map +0 -1
- package/dist/identity/username.d.ts.map +0 -1
- package/dist/identity/username.js.map +0 -1
- package/dist/media/agent-tools.d.ts.map +0 -1
- package/dist/media/agent-tools.js.map +0 -1
- package/dist/media/blob-gc-lock.d.ts.map +0 -1
- package/dist/media/blob-gc-lock.js.map +0 -1
- package/dist/media/blob-gc.d.ts.map +0 -1
- package/dist/media/blob-gc.js.map +0 -1
- package/dist/media/blob-key.d.ts.map +0 -1
- package/dist/media/blob-key.js.map +0 -1
- package/dist/media/blob-store.fs.d.ts.map +0 -1
- package/dist/media/blob-store.fs.js.map +0 -1
- package/dist/media/blob-store.memory.d.ts.map +0 -1
- package/dist/media/blob-store.memory.js.map +0 -1
- package/dist/media/content-type-sniffer.d.ts.map +0 -1
- package/dist/media/content-type-sniffer.js.map +0 -1
- package/dist/media/html-attributes.d.ts.map +0 -1
- package/dist/media/html-attributes.js.map +0 -1
- package/dist/media/image-transformer.d.ts.map +0 -1
- package/dist/media/image-transformer.js.map +0 -1
- package/dist/media/image-transformer.sharp.d.ts.map +0 -1
- package/dist/media/image-transformer.sharp.js.map +0 -1
- package/dist/media/index.d.ts.map +0 -1
- package/dist/media/index.js.map +0 -1
- package/dist/media/media-service.d.ts.map +0 -1
- package/dist/media/media-service.js.map +0 -1
- package/dist/media/ports.d.ts.map +0 -1
- package/dist/media/ports.js.map +0 -1
- package/dist/media/rendition-service.d.ts.map +0 -1
- package/dist/media/rendition-service.js.map +0 -1
- package/dist/media/repo.memory.d.ts.map +0 -1
- package/dist/media/repo.memory.js.map +0 -1
- package/dist/media/tool-registrations.d.ts.map +0 -1
- package/dist/media/tool-registrations.js.map +0 -1
- package/dist/media/transform-lock.d.ts.map +0 -1
- package/dist/media/transform-lock.js.map +0 -1
- package/dist/media/transform-registry.d.ts.map +0 -1
- package/dist/media/transform-registry.js.map +0 -1
- package/dist/media/transform-types.d.ts.map +0 -1
- package/dist/media/transform-types.js.map +0 -1
- package/dist/media/types.d.ts.map +0 -1
- package/dist/media/types.js.map +0 -1
- package/dist/navigation/agent-tools.d.ts.map +0 -1
- package/dist/navigation/agent-tools.js.map +0 -1
- package/dist/navigation/contracts.d.ts.map +0 -1
- package/dist/navigation/contracts.js.map +0 -1
- package/dist/navigation/index.d.ts.map +0 -1
- package/dist/navigation/index.js.map +0 -1
- package/dist/navigation/menu-service.d.ts.map +0 -1
- package/dist/navigation/menu-service.js.map +0 -1
- package/dist/navigation/ports.d.ts.map +0 -1
- package/dist/navigation/ports.js.map +0 -1
- package/dist/navigation/read-model.d.ts.map +0 -1
- package/dist/navigation/read-model.js.map +0 -1
- package/dist/navigation/reconcile.d.ts.map +0 -1
- package/dist/navigation/reconcile.js.map +0 -1
- package/dist/navigation/repo.memory.d.ts.map +0 -1
- package/dist/navigation/repo.memory.js.map +0 -1
- package/dist/navigation/resolver.d.ts.map +0 -1
- package/dist/navigation/resolver.js.map +0 -1
- package/dist/navigation/tool-registrations.d.ts.map +0 -1
- package/dist/navigation/tool-registrations.js.map +0 -1
- package/dist/navigation/types.d.ts.map +0 -1
- package/dist/navigation/types.js.map +0 -1
- package/dist/presentation/index.d.ts.map +0 -1
- package/dist/presentation/index.js.map +0 -1
- package/dist/presentation/presentation.d.ts.map +0 -1
- package/dist/presentation/presentation.js.map +0 -1
- package/dist/presentation/repo.memory.d.ts.map +0 -1
- package/dist/presentation/repo.memory.js.map +0 -1
- package/dist/server/index.d.ts.map +0 -1
- package/dist/server/index.js.map +0 -1
- package/dist/settings/agent-tools.d.ts.map +0 -1
- package/dist/settings/agent-tools.js.map +0 -1
- package/dist/settings/agent-writable-preferences.d.ts.map +0 -1
- package/dist/settings/agent-writable-preferences.js.map +0 -1
- package/dist/settings/change-feed.d.ts.map +0 -1
- package/dist/settings/change-feed.js.map +0 -1
- package/dist/settings/definitions-dispatch.d.ts.map +0 -1
- package/dist/settings/definitions-dispatch.js.map +0 -1
- package/dist/settings/dictionaries/index.d.ts.map +0 -1
- package/dist/settings/dictionaries/index.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ar.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ar.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.bn.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.bn.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.de.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.de.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.es.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.es.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.fa.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.fa.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.fr.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.fr.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.hi.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.hi.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.hu.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.hu.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.id.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.id.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.it.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.it.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ja.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ja.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ko.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ko.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.pl.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.pl.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.pt-BR.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.pt-BR.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ru.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ru.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.th.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.th.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.tr.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.tr.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.uk.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.uk.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ur.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.ur.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.zh-CN.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.zh-CN.js.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.zh-TW.d.ts.map +0 -1
- package/dist/settings/dictionaries/settings-dialog.zh-TW.js.map +0 -1
- package/dist/settings/ensure-definitions.d.ts.map +0 -1
- package/dist/settings/ensure-definitions.js.map +0 -1
- package/dist/settings/errors.d.ts.map +0 -1
- package/dist/settings/errors.js.map +0 -1
- package/dist/settings/index.d.ts.map +0 -1
- package/dist/settings/index.js.map +0 -1
- package/dist/settings/ports.d.ts.map +0 -1
- package/dist/settings/ports.js.map +0 -1
- package/dist/settings/purge-service.d.ts.map +0 -1
- package/dist/settings/purge-service.js.map +0 -1
- package/dist/settings/repo.memory.d.ts.map +0 -1
- package/dist/settings/repo.memory.js.map +0 -1
- package/dist/settings/settings.d.ts.map +0 -1
- package/dist/settings/settings.js.map +0 -1
- package/dist/settings/tool-registrations.d.ts.map +0 -1
- package/dist/settings/tool-registrations.js.map +0 -1
- package/dist/settings/types.d.ts.map +0 -1
- package/dist/settings/types.js.map +0 -1
- package/dist/settings/ui-tab-definitions.d.ts.map +0 -1
- package/dist/settings/ui-tab-definitions.js.map +0 -1
- package/dist/settings/write-service.d.ts.map +0 -1
- package/dist/settings/write-service.js.map +0 -1
- package/dist/taxonomy/agent-tools.d.ts.map +0 -1
- package/dist/taxonomy/agent-tools.js.map +0 -1
- package/dist/taxonomy/content-lookup.d.ts.map +0 -1
- package/dist/taxonomy/content-lookup.js.map +0 -1
- package/dist/taxonomy/index.d.ts.map +0 -1
- package/dist/taxonomy/index.js.map +0 -1
- package/dist/taxonomy/list.d.ts.map +0 -1
- package/dist/taxonomy/list.js.map +0 -1
- package/dist/taxonomy/merge-term.d.ts.map +0 -1
- package/dist/taxonomy/merge-term.js.map +0 -1
- package/dist/taxonomy/repo.memory.d.ts.map +0 -1
- package/dist/taxonomy/repo.memory.js.map +0 -1
- package/dist/taxonomy/validation-chain.d.ts.map +0 -1
- package/dist/taxonomy/validation-chain.js.map +0 -1
- package/dist/taxonomy/write-service.d.ts.map +0 -1
- package/dist/taxonomy/write-service.js.map +0 -1
- package/dist/workspace/agent-tools.d.ts.map +0 -1
- package/dist/workspace/agent-tools.js.map +0 -1
- package/dist/workspace/create.d.ts.map +0 -1
- package/dist/workspace/create.js.map +0 -1
- package/dist/workspace/delete.d.ts.map +0 -1
- package/dist/workspace/delete.js.map +0 -1
- package/dist/workspace/index.d.ts.map +0 -1
- package/dist/workspace/index.js.map +0 -1
- package/dist/workspace/repo.memory.d.ts.map +0 -1
- package/dist/workspace/repo.memory.js.map +0 -1
- package/dist/workspace/tool-registrations.d.ts.map +0 -1
- package/dist/workspace/tool-registrations.js.map +0 -1
- package/dist/workspace/update.d.ts.map +0 -1
- package/dist/workspace/update.js.map +0 -1
|
@@ -83,7 +83,7 @@ export declare function validateIdentifierGrammar(value: string): boolean;
|
|
|
83
83
|
*
|
|
84
84
|
* The PARAMETER type is deliberately the wider `ContentTypeFieldKind`. Narrowing it would be a
|
|
85
85
|
* breaking change for `@jini-ai/cms`'s public consumers (`content-types/index.ts` documents this
|
|
86
|
-
* function as what a host's own DDL provisioner calls, and
|
|
86
|
+
* function as what a host's own DDL provisioner calls, and the host re-exports it), and it would also
|
|
87
87
|
* make the runtime guard below unreachable-looking to a reader while remaining fully reachable to
|
|
88
88
|
* an untyped JavaScript caller. The runtime gate — not the signature — is what excludes
|
|
89
89
|
* storage-only kinds.
|
|
@@ -113,7 +113,7 @@ const KIND_TO_CAST_LITERAL = {
|
|
|
113
113
|
*
|
|
114
114
|
* The PARAMETER type is deliberately the wider `ContentTypeFieldKind`. Narrowing it would be a
|
|
115
115
|
* breaking change for `@jini-ai/cms`'s public consumers (`content-types/index.ts` documents this
|
|
116
|
-
* function as what a host's own DDL provisioner calls, and
|
|
116
|
+
* function as what a host's own DDL provisioner calls, and the host re-exports it), and it would also
|
|
117
117
|
* make the runtime guard below unreachable-looking to a reader while remaining fully reachable to
|
|
118
118
|
* an untyped JavaScript caller. The runtime gate — not the signature — is what excludes
|
|
119
119
|
* storage-only kinds.
|
|
@@ -2,6 +2,30 @@ import { ContentTypeNotFoundError, ForbiddenError, InvalidFieldKindError, Invali
|
|
|
2
2
|
import { resolveFieldIndexTransition, validateIdentifierGrammar } from "./index-provisioning.js";
|
|
3
3
|
import { isContentTypeFieldKind, isIndexableFieldKind, } from "./types.js";
|
|
4
4
|
/** Reserved forever for the legacy `posts` table — an operator Collection can never take these keys. */
|
|
5
|
+
/**
|
|
6
|
+
* Field-name guards shared by register and update, in order: every name passes the identifier
|
|
7
|
+
* grammar (`InvalidFieldNameGrammarError`), THEN no name appears twice
|
|
8
|
+
* (`VALIDATION_ERROR(fields_duplicate_name)`). Duplicates are refused because entry values are
|
|
9
|
+
* stored keyed by field name and the index-transition diff builds a name-keyed map, so two defs
|
|
10
|
+
* sharing a name leave one of them unaddressable (last one wins). Returns `null` when both pass.
|
|
11
|
+
*
|
|
12
|
+
* @complexity O(f) in the number of submitted fields.
|
|
13
|
+
*/
|
|
14
|
+
function fieldNameError(fields) {
|
|
15
|
+
for (const field of fields) {
|
|
16
|
+
if (!validateIdentifierGrammar(field.name)) {
|
|
17
|
+
return new InvalidFieldNameGrammarError(`field name '${field.name}' fails the identifier grammar gate`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
for (const field of fields) {
|
|
22
|
+
if (seen.has(field.name)) {
|
|
23
|
+
return new ValidationError(`field name '${field.name}' appears more than once`, "fields_duplicate_name");
|
|
24
|
+
}
|
|
25
|
+
seen.add(field.name);
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
5
29
|
const RESERVED_CONTENT_TYPE_KEYS = new Set(["post", "page"]);
|
|
6
30
|
/** Per-type cap on `queryable` fields (queryable-index sprawl mitigation). */
|
|
7
31
|
const QUERYABLE_FIELD_CAP = 20;
|
|
@@ -34,12 +58,10 @@ export async function registerContentType(required) {
|
|
|
34
58
|
if (RESERVED_CONTENT_TYPE_KEYS.has(input.key)) {
|
|
35
59
|
return { ok: false, error: new ReservedContentTypeKeyError(`key '${input.key}' is permanently reserved for the legacy 'posts' table`) };
|
|
36
60
|
}
|
|
37
|
-
// Guard 3: field-name grammar (every field, before any kind/cap check).
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
}
|
|
61
|
+
// Guard 3: field-name grammar, then uniqueness (every field, before any kind/cap check).
|
|
62
|
+
const nameErrorOnRegister = fieldNameError(input.fields);
|
|
63
|
+
if (nameErrorOnRegister)
|
|
64
|
+
return { ok: false, error: nameErrorOnRegister };
|
|
43
65
|
// Guard 4: field-kind, closed enum.
|
|
44
66
|
for (const field of input.fields) {
|
|
45
67
|
if (!isContentTypeFieldKind(field.kind)) {
|
|
@@ -125,11 +147,9 @@ export async function updateContentTypeFields(required) {
|
|
|
125
147
|
if (input.fields.length === 0) {
|
|
126
148
|
return { ok: false, error: new ValidationError(`content type '${input.key}' update submitted an empty fields array`, "fields_empty") };
|
|
127
149
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
}
|
|
150
|
+
const nameErrorOnUpdate = fieldNameError(input.fields);
|
|
151
|
+
if (nameErrorOnUpdate)
|
|
152
|
+
return { ok: false, error: nameErrorOnUpdate };
|
|
133
153
|
for (const field of input.fields) {
|
|
134
154
|
if (!isContentTypeFieldKind(field.kind)) {
|
|
135
155
|
return { ok: false, error: new InvalidFieldKindError(`field '${field.name}' has kind '${field.kind}', not one of the closed field-kind enum`) };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ToolInputError } from "@jini-ai/core";
|
|
2
|
+
/**
|
|
3
|
+
* @file The write-side counterpart to `not-trashed.ts`'s read-side `notTrashed()` filter (Tovu
|
|
4
|
+
* `features/trash/not-trashed.ts`).
|
|
5
|
+
*
|
|
6
|
+
* Purpose:
|
|
7
|
+
* A recurring defect across this codebase's writers: a mutation loads its row with a `findById`
|
|
8
|
+
* (or equivalent) that returns trashed and tombstoned rows just as readily as live ones, and never
|
|
9
|
+
* asks whether the row it found is actually live before saving over it. Fixed per writer, that is
|
|
10
|
+
* an unbounded number of near-identical patches, each one easy to forget on the next new writer.
|
|
11
|
+
* This module is the one shared check every writer calls right after its own load, so the check
|
|
12
|
+
* exists exactly once.
|
|
13
|
+
*
|
|
14
|
+
* Why it lives in `@jini-ai/cms/core` rather than in a Tovu module or a Jini domain package:
|
|
15
|
+
* Tovu's writers (pages, SEO, redirects) and Jini's own domain writers (media, content-types,
|
|
16
|
+
* taxonomy) both need it, Jini cannot import anything Tovu owns, and duplicating the check per side
|
|
17
|
+
* reintroduces the drift this module exists to remove. `core` is the one place both reach.
|
|
18
|
+
*
|
|
19
|
+
* Why `EntityNotLiveError` extends `ToolInputError` rather than a plain `Error`: the daemon's tool
|
|
20
|
+
* executor already treats `ToolInputError` as caller-facing (see `registration-kit.ts`'s doc
|
|
21
|
+
* comment), so every domain's agent tool surfaces this error's message to the caller with no
|
|
22
|
+
* per-domain allowlist entry, and every HTTP route that already maps `ToolInputError`-family errors
|
|
23
|
+
* picks it up for free. A per-domain rule added to `contracts/core/model-facing-tool-errors.ts`
|
|
24
|
+
* instead would be exactly the "correct primitive, unwired call site" defect this codebase keeps
|
|
25
|
+
* hitting — the check would exist but a caller could still fail to wire it in.
|
|
26
|
+
*
|
|
27
|
+
* The message is built only from a fixed string, the entity kind and the caller's own id (never
|
|
28
|
+
* from row content), so it satisfies `contracts/core/model-facing-tool-errors.ts`'s safety rule for
|
|
29
|
+
* a caller-facing message with no redaction step.
|
|
30
|
+
*/
|
|
31
|
+
/** A record's liveness as far as this guard is concerned. `"live"` never throws. */
|
|
32
|
+
export type EntityLiveness = "live" | "trashed" | "tombstoned";
|
|
33
|
+
/**
|
|
34
|
+
* Thrown by {@link assertEntityLive} for a non-live entity. `code` is the stable machine-readable
|
|
35
|
+
* discriminant; `entityType`/`entityId`/`state` are the values the message was built from, kept on
|
|
36
|
+
* the instance so a caller that wants to branch on them (rather than parse the message) can.
|
|
37
|
+
*/
|
|
38
|
+
export declare class EntityNotLiveError extends ToolInputError {
|
|
39
|
+
readonly entityType: string;
|
|
40
|
+
readonly entityId: string;
|
|
41
|
+
readonly state: "trashed" | "tombstoned";
|
|
42
|
+
readonly code: "ENTITY_IN_TRASH" | "ENTITY_TOMBSTONED";
|
|
43
|
+
constructor(entityType: string, entityId: string, state: "trashed" | "tombstoned");
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Throws {@link EntityNotLiveError} unless `required.state` is `"live"`. Call this immediately
|
|
47
|
+
* after a writer's own load, before any mutation of the loaded row.
|
|
48
|
+
* @complexity O(1).
|
|
49
|
+
*/
|
|
50
|
+
export declare function assertEntityLive(required: {
|
|
51
|
+
entityType: string;
|
|
52
|
+
entityId: string;
|
|
53
|
+
state: EntityLiveness;
|
|
54
|
+
}): void;
|
|
55
|
+
//# sourceMappingURL=entity-liveness.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ToolInputError } from "@jini-ai/core";
|
|
2
|
+
/**
|
|
3
|
+
* Thrown by {@link assertEntityLive} for a non-live entity. `code` is the stable machine-readable
|
|
4
|
+
* discriminant; `entityType`/`entityId`/`state` are the values the message was built from, kept on
|
|
5
|
+
* the instance so a caller that wants to branch on them (rather than parse the message) can.
|
|
6
|
+
*/
|
|
7
|
+
export class EntityNotLiveError extends ToolInputError {
|
|
8
|
+
entityType;
|
|
9
|
+
entityId;
|
|
10
|
+
state;
|
|
11
|
+
code;
|
|
12
|
+
constructor(entityType, entityId, state) {
|
|
13
|
+
super(buildEntityNotLiveMessage(entityType, entityId, state));
|
|
14
|
+
this.entityType = entityType;
|
|
15
|
+
this.entityId = entityId;
|
|
16
|
+
this.state = state;
|
|
17
|
+
this.name = "EntityNotLiveError";
|
|
18
|
+
this.code = state === "trashed" ? "ENTITY_IN_TRASH" : "ENTITY_TOMBSTONED";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** @complexity O(1). Pure string formatting; the two messages are tested byte-for-byte. */
|
|
22
|
+
function buildEntityNotLiveMessage(entityType, entityId, state) {
|
|
23
|
+
if (state === "trashed") {
|
|
24
|
+
return `ENTITY_IN_TRASH: ${entityType} '${entityId}' is in the Trash. Restore it from the Trash before changing it.`;
|
|
25
|
+
}
|
|
26
|
+
return `ENTITY_TOMBSTONED: ${entityType} '${entityId}' was permanently deleted and can't be changed.`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Throws {@link EntityNotLiveError} unless `required.state` is `"live"`. Call this immediately
|
|
30
|
+
* after a writer's own load, before any mutation of the loaded row.
|
|
31
|
+
* @complexity O(1).
|
|
32
|
+
*/
|
|
33
|
+
export function assertEntityLive(required) {
|
|
34
|
+
if (required.state === "live")
|
|
35
|
+
return;
|
|
36
|
+
throw new EntityNotLiveError(required.entityType, required.entityId, required.state);
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=entity-liveness.js.map
|
package/dist/core/index.d.ts
CHANGED
|
@@ -41,5 +41,6 @@
|
|
|
41
41
|
export type { UUID, ISODateTime, JsonPrimitive, JsonValue, JsonObject, JsonArray, DomainEvent, EventBusPort, OutboxRecord, OutboxPort, ClockPort, IdGeneratorPort, } from './ports.js';
|
|
42
42
|
export type { ChangeSetStatus, ChangeSetOperation, ChangeSetRecord, ChangeSetItemRecord, ChangeSetWithItems, ChangeSetRepoPort, } from './commands/change-set.js';
|
|
43
43
|
export * from './commands/command.js';
|
|
44
|
+
export * from './entity-liveness.js';
|
|
44
45
|
export * from './tools/registration-kit.js';
|
|
45
46
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/core/index.js
CHANGED
package/dist/core/ports.d.ts
CHANGED
|
@@ -130,7 +130,7 @@ export interface OutboxPort {
|
|
|
130
130
|
* attempt cap) — an implementation must persist exactly what it is told, not re-derive the
|
|
131
131
|
* decision from the row's own stored `attempts` (2026-09-06: this parameter replaced that
|
|
132
132
|
* adapter-side re-derivation, mirroring `WebhookDeliveryRepoPort.markFailed`'s `nextStatus`
|
|
133
|
-
* shape in the
|
|
133
|
+
* shape in the host). `"pending"` re-enters the retry queue at `nextAttemptAt`; `"failed"`
|
|
134
134
|
* is terminal — `claimPending` only ever selects `"pending"` rows, so a `"failed"` row is
|
|
135
135
|
* permanently excluded from retry regardless of `nextAttemptAt`.
|
|
136
136
|
*/
|
|
@@ -91,22 +91,56 @@ export type DerivedRiskByToolId = ReadonlyMap<string, AgentToolSideEffect>;
|
|
|
91
91
|
*/
|
|
92
92
|
export declare const AGENT_TOOL_PRINCIPAL_KIND: "agent";
|
|
93
93
|
/**
|
|
94
|
-
* Actor-class rules that
|
|
94
|
+
* Actor-class rules that need a human confirmer.
|
|
95
95
|
*
|
|
96
96
|
* `confirmer-must-equal-own-delegatedBy` means "a human must confirm this, and the confirmer must
|
|
97
|
-
* be the principal who delegated".
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* confirmation await.
|
|
97
|
+
* be the principal who delegated". The kit never delivers that through `descriptor.requiresConfirmation`:
|
|
98
|
+
* without a host `ExecutionDelegate`, that flag parks the execution on a promise only
|
|
99
|
+
* `resumeConfirmation` can settle, and nothing would call it (unbounded, too, because
|
|
100
|
+
* `descriptor.timeoutMs`'s timer is armed only AFTER the confirmation await).
|
|
101
101
|
*
|
|
102
|
-
* So a tool carrying this rule
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* future edit wiring any of them fails the build instead of silently shipping a tool whose stated
|
|
106
|
-
* human-confirmation requirement is unenforceable. When a confirmation transport does land, this
|
|
107
|
-
* constant is the deliberate place to relax.
|
|
102
|
+
* So a tool carrying this rule is wirable only when its handler was built with
|
|
103
|
+
* {@link humanConfirmedHandler}, which asks the human through the host's own confirmation transport
|
|
104
|
+
* and runs the action only on an explicit yes. Any other handler fails the build.
|
|
108
105
|
*/
|
|
109
106
|
export declare const ACTOR_CLASS_RULES_REQUIRING_CONFIRMATION_TRANSPORT: Set<string>;
|
|
107
|
+
/**
|
|
108
|
+
* The human who confirmed, as {@link humanConfirmedHandler} hands it to `run`: always the run's own
|
|
109
|
+
* delegating principal (`ctx.principal.id`, see {@link AGENT_TOOL_PRINCIPAL_KIND}), always
|
|
110
|
+
* `kind: "user"`. The only thing a `run` step may pass to a confirm step as its confirmer.
|
|
111
|
+
*/
|
|
112
|
+
export interface HumanConfirmer {
|
|
113
|
+
readonly id: string;
|
|
114
|
+
readonly kind: "user";
|
|
115
|
+
}
|
|
116
|
+
/** What the host's transport reports back. Only exactly `confirmed: true` counts as a yes. */
|
|
117
|
+
export type HumanConfirmationAnswer = {
|
|
118
|
+
confirmed: true;
|
|
119
|
+
} | {
|
|
120
|
+
confirmed: false;
|
|
121
|
+
result: unknown;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Builds the one kind of handler a `confirmer-must-equal-own-delegatedBy` tool may be wired with
|
|
125
|
+
* (see {@link ACTOR_CLASS_RULES_REQUIRING_CONFIRMATION_TRANSPORT}).
|
|
126
|
+
*
|
|
127
|
+
* The order is fixed here, not left to each domain: `prepare` (validate, pre-authorize, derive what
|
|
128
|
+
* the human will see) → `askHuman` (the host's transport, e.g. an MCP-UI dialog) → `run`, only when
|
|
129
|
+
* the answer is exactly `{ confirmed: true }`. The decision never comes from `ctx.input`, so nothing
|
|
130
|
+
* the model sends can stand in for the human. `run` gets the confirmer as the delegating human.
|
|
131
|
+
*
|
|
132
|
+
* @param steps.prepare - Reads input and derives what the human is asked about. May throw.
|
|
133
|
+
* @param steps.askHuman - Asks the human through the host's transport. Throws when no human can be
|
|
134
|
+
* asked; a no-answer returns `{ confirmed: false, result }` and `result` is the tool's result.
|
|
135
|
+
* @param steps.run - Performs the action on a yes.
|
|
136
|
+
* @returns A handler {@link assertToolIsWirable} accepts for a tool carrying the rule.
|
|
137
|
+
* @complexity O(1) plus the three steps.
|
|
138
|
+
*/
|
|
139
|
+
export declare function humanConfirmedHandler<TPrepared>(steps: {
|
|
140
|
+
prepare: (ctx: Parameters<ToolHandler>[0]) => Promise<TPrepared>;
|
|
141
|
+
askHuman: (ctx: Parameters<ToolHandler>[0], prepared: TPrepared) => Promise<HumanConfirmationAnswer>;
|
|
142
|
+
run: (ctx: Parameters<ToolHandler>[0], prepared: TPrepared, confirmer: HumanConfirmer) => Promise<unknown>;
|
|
143
|
+
}): ToolHandler;
|
|
110
144
|
/**
|
|
111
145
|
* Indexes a domain catalog by tool id — the single lookup used for descriptors, risk metadata, and
|
|
112
146
|
* schema-bearing error messages.
|
|
@@ -220,8 +254,8 @@ export declare function decorateWithSchema(params: {
|
|
|
220
254
|
/**
|
|
221
255
|
* Build-time gate on a single tool's risk metadata: refuses to wire a tool whose declared
|
|
222
256
|
* `sideEffects` disagrees with the wiring layer's own {@link DerivedRiskByToolId} classification,
|
|
223
|
-
* whose id that layer has not classified at all, or whose `actorClassRule` needs a
|
|
224
|
-
*
|
|
257
|
+
* whose id that layer has not classified at all, or whose `actorClassRule` needs a human confirmer
|
|
258
|
+
* its handler does not ask for.
|
|
225
259
|
*
|
|
226
260
|
* Throws rather than warning, and at registration time rather than call time: a metadata
|
|
227
261
|
* inconsistency is a developer error that should stop the daemon booting, not a runtime condition
|
|
@@ -230,7 +264,10 @@ export declare function decorateWithSchema(params: {
|
|
|
230
264
|
* @param params.toolId - The wired tool id.
|
|
231
265
|
* @param params.catalogEntry - Its `agent-tools.ts` entry, the declared side of the comparison.
|
|
232
266
|
* @param params.derivedRisk - The wiring layer's independent classification, the derived side.
|
|
233
|
-
* @
|
|
267
|
+
* @param params.handler - The handler being wired; checked only for a tool carrying a rule in
|
|
268
|
+
* {@link ACTOR_CLASS_RULES_REQUIRING_CONFIRMATION_TRANSPORT}.
|
|
269
|
+
* @throws {Error} If the tool is unclassified, misclassified, or needs a human confirmer its handler
|
|
270
|
+
* does not ask for.
|
|
234
271
|
* @complexity O(1) — two map/set lookups.
|
|
235
272
|
* @overallScore 100
|
|
236
273
|
*/
|
|
@@ -238,6 +275,7 @@ export declare function assertToolIsWirable(params: {
|
|
|
238
275
|
toolId: string;
|
|
239
276
|
catalogEntry: WirableToolDefinition;
|
|
240
277
|
derivedRisk: DerivedRiskByToolId;
|
|
278
|
+
handler?: ToolHandler;
|
|
241
279
|
}): void;
|
|
242
280
|
/**
|
|
243
281
|
* Turns one domain's handler map into `ToolRegistration`s, applying every build-time gate the
|
|
@@ -258,9 +296,9 @@ export declare function assertToolIsWirable(params: {
|
|
|
258
296
|
* the same rule, and a `ToolPolicy`-only check would be bypassable by any future non-tool caller of
|
|
259
297
|
* the same domain function, which is precisely why the chokepoint owns the gate.
|
|
260
298
|
*
|
|
261
|
-
* `descriptor.requiresConfirmation` is deliberately never set —
|
|
262
|
-
* {@link
|
|
263
|
-
*
|
|
299
|
+
* `descriptor.requiresConfirmation` is deliberately never set — a tool that needs a human confirm
|
|
300
|
+
* asks inside its own {@link humanConfirmedHandler}; see
|
|
301
|
+
* {@link ACTOR_CLASS_RULES_REQUIRING_CONFIRMATION_TRANSPORT}.
|
|
264
302
|
*
|
|
265
303
|
* @param spec.domain - Human-readable domain name, used only in failure messages.
|
|
266
304
|
* @param spec.catalogModule - Path of the catalog file, named in the drift message so the failure
|
|
@@ -48,22 +48,47 @@ import { ForbiddenError } from "../commands/command.js";
|
|
|
48
48
|
// and Forms' `CommandActor.kind` param without a cast at either call site.
|
|
49
49
|
export const AGENT_TOOL_PRINCIPAL_KIND = "agent";
|
|
50
50
|
/**
|
|
51
|
-
* Actor-class rules that
|
|
51
|
+
* Actor-class rules that need a human confirmer.
|
|
52
52
|
*
|
|
53
53
|
* `confirmer-must-equal-own-delegatedBy` means "a human must confirm this, and the confirmer must
|
|
54
|
-
* be the principal who delegated".
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* confirmation await.
|
|
54
|
+
* be the principal who delegated". The kit never delivers that through `descriptor.requiresConfirmation`:
|
|
55
|
+
* without a host `ExecutionDelegate`, that flag parks the execution on a promise only
|
|
56
|
+
* `resumeConfirmation` can settle, and nothing would call it (unbounded, too, because
|
|
57
|
+
* `descriptor.timeoutMs`'s timer is armed only AFTER the confirmation await).
|
|
58
58
|
*
|
|
59
|
-
* So a tool carrying this rule
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* future edit wiring any of them fails the build instead of silently shipping a tool whose stated
|
|
63
|
-
* human-confirmation requirement is unenforceable. When a confirmation transport does land, this
|
|
64
|
-
* constant is the deliberate place to relax.
|
|
59
|
+
* So a tool carrying this rule is wirable only when its handler was built with
|
|
60
|
+
* {@link humanConfirmedHandler}, which asks the human through the host's own confirmation transport
|
|
61
|
+
* and runs the action only on an explicit yes. Any other handler fails the build.
|
|
65
62
|
*/
|
|
66
63
|
export const ACTOR_CLASS_RULES_REQUIRING_CONFIRMATION_TRANSPORT = new Set(["confirmer-must-equal-own-delegatedBy"]);
|
|
64
|
+
const HUMAN_CONFIRMED_HANDLERS = new WeakSet();
|
|
65
|
+
/**
|
|
66
|
+
* Builds the one kind of handler a `confirmer-must-equal-own-delegatedBy` tool may be wired with
|
|
67
|
+
* (see {@link ACTOR_CLASS_RULES_REQUIRING_CONFIRMATION_TRANSPORT}).
|
|
68
|
+
*
|
|
69
|
+
* The order is fixed here, not left to each domain: `prepare` (validate, pre-authorize, derive what
|
|
70
|
+
* the human will see) → `askHuman` (the host's transport, e.g. an MCP-UI dialog) → `run`, only when
|
|
71
|
+
* the answer is exactly `{ confirmed: true }`. The decision never comes from `ctx.input`, so nothing
|
|
72
|
+
* the model sends can stand in for the human. `run` gets the confirmer as the delegating human.
|
|
73
|
+
*
|
|
74
|
+
* @param steps.prepare - Reads input and derives what the human is asked about. May throw.
|
|
75
|
+
* @param steps.askHuman - Asks the human through the host's transport. Throws when no human can be
|
|
76
|
+
* asked; a no-answer returns `{ confirmed: false, result }` and `result` is the tool's result.
|
|
77
|
+
* @param steps.run - Performs the action on a yes.
|
|
78
|
+
* @returns A handler {@link assertToolIsWirable} accepts for a tool carrying the rule.
|
|
79
|
+
* @complexity O(1) plus the three steps.
|
|
80
|
+
*/
|
|
81
|
+
export function humanConfirmedHandler(steps) {
|
|
82
|
+
const handler = async (ctx) => {
|
|
83
|
+
const prepared = await steps.prepare(ctx);
|
|
84
|
+
const answer = await steps.askHuman(ctx, prepared);
|
|
85
|
+
if (answer.confirmed !== true)
|
|
86
|
+
return answer.result;
|
|
87
|
+
return steps.run(ctx, prepared, { id: ctx.principal.id, kind: "user" });
|
|
88
|
+
};
|
|
89
|
+
HUMAN_CONFIRMED_HANDLERS.add(handler);
|
|
90
|
+
return handler;
|
|
91
|
+
}
|
|
67
92
|
/**
|
|
68
93
|
* Indexes a domain catalog by tool id — the single lookup used for descriptors, risk metadata, and
|
|
69
94
|
* schema-bearing error messages.
|
|
@@ -256,8 +281,8 @@ export function decorateWithSchema(params) {
|
|
|
256
281
|
/**
|
|
257
282
|
* Build-time gate on a single tool's risk metadata: refuses to wire a tool whose declared
|
|
258
283
|
* `sideEffects` disagrees with the wiring layer's own {@link DerivedRiskByToolId} classification,
|
|
259
|
-
* whose id that layer has not classified at all, or whose `actorClassRule` needs a
|
|
260
|
-
*
|
|
284
|
+
* whose id that layer has not classified at all, or whose `actorClassRule` needs a human confirmer
|
|
285
|
+
* its handler does not ask for.
|
|
261
286
|
*
|
|
262
287
|
* Throws rather than warning, and at registration time rather than call time: a metadata
|
|
263
288
|
* inconsistency is a developer error that should stop the daemon booting, not a runtime condition
|
|
@@ -266,7 +291,10 @@ export function decorateWithSchema(params) {
|
|
|
266
291
|
* @param params.toolId - The wired tool id.
|
|
267
292
|
* @param params.catalogEntry - Its `agent-tools.ts` entry, the declared side of the comparison.
|
|
268
293
|
* @param params.derivedRisk - The wiring layer's independent classification, the derived side.
|
|
269
|
-
* @
|
|
294
|
+
* @param params.handler - The handler being wired; checked only for a tool carrying a rule in
|
|
295
|
+
* {@link ACTOR_CLASS_RULES_REQUIRING_CONFIRMATION_TRANSPORT}.
|
|
296
|
+
* @throws {Error} If the tool is unclassified, misclassified, or needs a human confirmer its handler
|
|
297
|
+
* does not ask for.
|
|
270
298
|
* @complexity O(1) — two map/set lookups.
|
|
271
299
|
* @overallScore 100
|
|
272
300
|
*/
|
|
@@ -279,8 +307,9 @@ export function assertToolIsWirable(params) {
|
|
|
279
307
|
if (derived !== catalogEntry.sideEffects) {
|
|
280
308
|
throw new Error(`tool-registrations: '${toolId}' declares sideEffects '${catalogEntry.sideEffects}' but this layer derives '${derived}' from what its handler calls — reconcile the two rather than trusting the declaration`);
|
|
281
309
|
}
|
|
282
|
-
|
|
283
|
-
|
|
310
|
+
const needsHuman = catalogEntry.actorClassRule && ACTOR_CLASS_RULES_REQUIRING_CONFIRMATION_TRANSPORT.has(catalogEntry.actorClassRule);
|
|
311
|
+
if (needsHuman && !(params.handler && HUMAN_CONFIRMED_HANDLERS.has(params.handler))) {
|
|
312
|
+
throw new Error(`tool-registrations: '${toolId}' declares actorClassRule '${catalogEntry.actorClassRule}', which needs a human confirmer — build its handler with humanConfirmedHandler so a human answers through the host's confirmation transport (see ACTOR_CLASS_RULES_REQUIRING_CONFIRMATION_TRANSPORT)`);
|
|
284
313
|
}
|
|
285
314
|
}
|
|
286
315
|
/**
|
|
@@ -302,9 +331,9 @@ export function assertToolIsWirable(params) {
|
|
|
302
331
|
* the same rule, and a `ToolPolicy`-only check would be bypassable by any future non-tool caller of
|
|
303
332
|
* the same domain function, which is precisely why the chokepoint owns the gate.
|
|
304
333
|
*
|
|
305
|
-
* `descriptor.requiresConfirmation` is deliberately never set —
|
|
306
|
-
* {@link
|
|
307
|
-
*
|
|
334
|
+
* `descriptor.requiresConfirmation` is deliberately never set — a tool that needs a human confirm
|
|
335
|
+
* asks inside its own {@link humanConfirmedHandler}; see
|
|
336
|
+
* {@link ACTOR_CLASS_RULES_REQUIRING_CONFIRMATION_TRANSPORT}.
|
|
308
337
|
*
|
|
309
338
|
* @param spec.domain - Human-readable domain name, used only in failure messages.
|
|
310
339
|
* @param spec.catalogModule - Path of the catalog file, named in the drift message so the failure
|
|
@@ -326,7 +355,7 @@ export function buildDomainRegistrations(spec) {
|
|
|
326
355
|
if (!catalogEntry) {
|
|
327
356
|
throw new Error(`tool-registrations: ${spec.domain} catalog has no entry named '${id}' — ${spec.catalogModule} drifted`);
|
|
328
357
|
}
|
|
329
|
-
assertToolIsWirable({ toolId: id, catalogEntry, derivedRisk: spec.derivedRisk });
|
|
358
|
+
assertToolIsWirable({ toolId: id, catalogEntry, derivedRisk: spec.derivedRisk, handler });
|
|
330
359
|
if (!catalogEntry.inputSchema) {
|
|
331
360
|
throw new Error(`tool-registrations: wired tool '${id}' publishes no inputSchema — add one to its entry in ${spec.catalogModule} so the model gets a contract, or leave the tool unwired`);
|
|
332
361
|
}
|
|
@@ -15,7 +15,7 @@ import { type PrincipalRecord, type SessionRecord } from "./types.js";
|
|
|
15
15
|
* verifying credentials, and a host that identifies a principal by some other means (e.g. a
|
|
16
16
|
* loopback-only single-use boot token, which has nothing to verify a password against) calls it
|
|
17
17
|
* directly instead of re-implementing session construction. Before this export existed, a
|
|
18
|
-
* downstream host
|
|
18
|
+
* downstream host had duplicated this file's private `hashToken` byte-for-byte because
|
|
19
19
|
* `login()` was the only minter and hard-requires a password — this closes that fork.
|
|
20
20
|
*
|
|
21
21
|
* Architectural role:
|
|
@@ -16,7 +16,7 @@ import { normalizeUsername } from "./username.js";
|
|
|
16
16
|
* verifying credentials, and a host that identifies a principal by some other means (e.g. a
|
|
17
17
|
* loopback-only single-use boot token, which has nothing to verify a password against) calls it
|
|
18
18
|
* directly instead of re-implementing session construction. Before this export existed, a
|
|
19
|
-
* downstream host
|
|
19
|
+
* downstream host had duplicated this file's private `hashToken` byte-for-byte because
|
|
20
20
|
* `login()` was the only minter and hard-requires a password — this closes that fork.
|
|
21
21
|
*
|
|
22
22
|
* Architectural role:
|
package/dist/identity/seed.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export interface SeedIdentityResult {
|
|
|
34
34
|
* - A seed that RAN TO COMPLETION is a no-op — the owner-username lookup below early-returns after
|
|
35
35
|
* a single confirming read (`ensureOwnerRoleBinding`) and writes nothing. (This path does not
|
|
36
36
|
* reconcile a built-in policy whose permission list has since grown; that is the host's job — see
|
|
37
|
-
*
|
|
37
|
+
* the host's `applyBuiltinRoleGrants` / `migrateDeprecatedPermissionGrants`, chained onto
|
|
38
38
|
* `identityReady`.)
|
|
39
39
|
* - "Ran to completion" is judged on the owner user AND its role link, not the user alone. The link
|
|
40
40
|
* is the seed's last write, so the user is not on its own evidence the seed finished — see
|
package/dist/identity/seed.js
CHANGED
|
@@ -149,7 +149,7 @@ const BUILTIN_VIEWER_PERMISSIONS = ["content.read", "changeset.read", "plugin.re
|
|
|
149
149
|
/**
|
|
150
150
|
* Why every step below is "find, then save only what is missing" rather than a straight insert.
|
|
151
151
|
*
|
|
152
|
-
* This function's writes are NOT transactional and its caller is not awaited by its hosts (
|
|
152
|
+
* This function's writes are NOT transactional and its caller is not awaited by its hosts (one host
|
|
153
153
|
* hands `seedIdentity`'s promise back as `RouteDeps.identityReady` and returns synchronously), so
|
|
154
154
|
* a process that exits before the seed finishes — a fast-failing CLI command, a Ctrl-C, a crash —
|
|
155
155
|
* leaves the workspace half-seeded. `seedIdentity`'s own early-return guard keys on the OWNER USER,
|
|
@@ -249,7 +249,7 @@ async function seedBuiltinRoleWithPolicy(required) {
|
|
|
249
249
|
* still log in, and holds no role — and `resolveEffectivePermissions` (`authorize.ts`) starts from
|
|
250
250
|
* `principal_roles` + `principal_policies`, so with neither present EVERY permission evaluates to
|
|
251
251
|
* `no_grant`. Without this step the guard early-returns on the user it finds forever and the site
|
|
252
|
-
* stays bricked for its owner, silently. Neither host-side reconciler closes it either:
|
|
252
|
+
* stays bricked for its owner, silently. Neither host-side reconciler closes it either: the host's
|
|
253
253
|
* `migrateDeprecatedPermissionGrants` and `applyBuiltinRoleGrants` are handed policy and role repos
|
|
254
254
|
* only, never `principalRoles`, so neither can write a principal->role link at all.
|
|
255
255
|
*
|
|
@@ -296,7 +296,7 @@ async function ensureOwnerRoleBinding(required) {
|
|
|
296
296
|
* - A seed that RAN TO COMPLETION is a no-op — the owner-username lookup below early-returns after
|
|
297
297
|
* a single confirming read (`ensureOwnerRoleBinding`) and writes nothing. (This path does not
|
|
298
298
|
* reconcile a built-in policy whose permission list has since grown; that is the host's job — see
|
|
299
|
-
*
|
|
299
|
+
* the host's `applyBuiltinRoleGrants` / `migrateDeprecatedPermissionGrants`, chained onto
|
|
300
300
|
* `identityReady`.)
|
|
301
301
|
* - "Ran to completion" is judged on the owner user AND its role link, not the user alone. The link
|
|
302
302
|
* is the seed's last write, so the user is not on its own evidence the seed finished — see
|
|
@@ -34,9 +34,19 @@
|
|
|
34
34
|
* content-types pass-through-`ToolPolicy` reasoning, not an oversight.
|
|
35
35
|
*
|
|
36
36
|
* Architectural role:
|
|
37
|
-
* `media` domain declaration. Imports only the
|
|
38
|
-
* (`
|
|
39
|
-
*
|
|
37
|
+
* `media` domain declaration. Imports only the constant its own domain already enforces
|
|
38
|
+
* (`DEFAULT_ALLOWED_MIME_TYPES` from `media-service.ts`), so the published JSON Schema's allowed
|
|
39
|
+
* content types cannot drift from the validator.
|
|
40
|
+
*
|
|
41
|
+
* `media_upload_asset`'s size-cap wording (2026-09-21): this catalog is a module-load-time constant
|
|
42
|
+
* shared by every host (see this file's own module doc above), but a host can now override
|
|
43
|
+
* `uploadMedia`'s cap per-call via `MediaToolDeps.maxUploadBytes` (`tool-registrations.ts`). Stating
|
|
44
|
+
* `DEFAULT_MAX_UPLOAD_BYTES` (10 MiB) here would be an outright lie for a host that raised its own
|
|
45
|
+
* cap (e.g. Tovu's 50 MiB) — the model would be told a wrong, lower number than what the tool
|
|
46
|
+
* actually accepts. The description and schema below therefore describe the cap generically instead
|
|
47
|
+
* of stating a figure; the size-rejection error itself (`uploadMedia`'s `MediaValidationError`,
|
|
48
|
+
* `media-service.ts`) states the ACTUAL configured limit in MB, which is the one place that can be
|
|
49
|
+
* host-accurate.
|
|
40
50
|
*/
|
|
41
51
|
export type AgentToolSideEffect = "none" | "mutates-durable-state" | "mints-token";
|
|
42
52
|
export type AgentToolActorClassRule = "confirmer-must-equal-own-delegatedBy" | "user-only" | "none";
|
|
@@ -34,11 +34,22 @@
|
|
|
34
34
|
* content-types pass-through-`ToolPolicy` reasoning, not an oversight.
|
|
35
35
|
*
|
|
36
36
|
* Architectural role:
|
|
37
|
-
* `media` domain declaration. Imports only the
|
|
38
|
-
* (`
|
|
39
|
-
*
|
|
37
|
+
* `media` domain declaration. Imports only the constant its own domain already enforces
|
|
38
|
+
* (`DEFAULT_ALLOWED_MIME_TYPES` from `media-service.ts`), so the published JSON Schema's allowed
|
|
39
|
+
* content types cannot drift from the validator.
|
|
40
|
+
*
|
|
41
|
+
* `media_upload_asset`'s size-cap wording (2026-09-21): this catalog is a module-load-time constant
|
|
42
|
+
* shared by every host (see this file's own module doc above), but a host can now override
|
|
43
|
+
* `uploadMedia`'s cap per-call via `MediaToolDeps.maxUploadBytes` (`tool-registrations.ts`). Stating
|
|
44
|
+
* `DEFAULT_MAX_UPLOAD_BYTES` (10 MiB) here would be an outright lie for a host that raised its own
|
|
45
|
+
* cap (e.g. Tovu's 50 MiB) — the model would be told a wrong, lower number than what the tool
|
|
46
|
+
* actually accepts. The description and schema below therefore describe the cap generically instead
|
|
47
|
+
* of stating a figure; the size-rejection error itself (`uploadMedia`'s `MediaValidationError`,
|
|
48
|
+
* `media-service.ts`) states the ACTUAL configured limit in MB, which is the one place that can be
|
|
49
|
+
* host-accurate.
|
|
40
50
|
*/
|
|
41
|
-
import {
|
|
51
|
+
import { MEDIA_HTML_ATTRIBUTE_ALLOWED_NAMES } from "./html-attributes.js";
|
|
52
|
+
import { DEFAULT_ALLOWED_MIME_TYPES } from "./media-service.js";
|
|
42
53
|
const MEDIA_ID_SCHEMA = {
|
|
43
54
|
type: "string",
|
|
44
55
|
description: "The media asset's id, as returned by media_upload_asset or media_list_assets.",
|
|
@@ -54,7 +65,7 @@ const MEDIA_ID_SCHEMA = {
|
|
|
54
65
|
export const mediaAgentToolCatalog = [
|
|
55
66
|
{
|
|
56
67
|
name: "media_list_assets",
|
|
57
|
-
description: "Lists every media asset in the workspace (all statuses — active and trashed), with id, title, alt, caption, credit, sha256, and status. Read-only. Call this to find a mediaId before updating or trashing an asset.",
|
|
68
|
+
description: "Lists every media asset in the workspace (all statuses — active and trashed), with id, slug, title, alt, caption, credit, sha256, and status. Read-only. Call this to find a mediaId before updating or trashing an asset.",
|
|
58
69
|
sideEffects: "none",
|
|
59
70
|
authorization: { permission: "media.read" },
|
|
60
71
|
inputSchema: {
|
|
@@ -68,8 +79,9 @@ export const mediaAgentToolCatalog = [
|
|
|
68
79
|
name: "media_upload_asset",
|
|
69
80
|
description: `Uploads a new media asset from base64-encoded bytes. Rejects a content type outside the allowed set ` +
|
|
70
81
|
`(${[...DEFAULT_ALLOWED_MIME_TYPES].join(", ")} — SVG is never accepted, even here: it requires a sanitizer ` +
|
|
71
|
-
`this build does not have) or a file over
|
|
72
|
-
`
|
|
82
|
+
`this build does not have) or a file over this host's configured size cap (a rejected upload's error ` +
|
|
83
|
+
`states the exact limit, in MB). Uploading the same bytes twice always creates two separate library ` +
|
|
84
|
+
`entries (the underlying blob is deduplicated, but each upload is its own asset).`,
|
|
73
85
|
sideEffects: "mutates-durable-state",
|
|
74
86
|
authorization: { permission: "media.upload" },
|
|
75
87
|
inputSchema: {
|
|
@@ -86,7 +98,7 @@ export const mediaAgentToolCatalog = [
|
|
|
86
98
|
dataBase64: {
|
|
87
99
|
type: "string",
|
|
88
100
|
minLength: 1,
|
|
89
|
-
description:
|
|
101
|
+
description: "The file's bytes, base64-encoded. Decoded size must be over 0 bytes and within this host's configured upload size cap (a too-large upload's error states the exact limit, in MB).",
|
|
90
102
|
},
|
|
91
103
|
alt: { type: "string", description: "Optional accessibility alt text." },
|
|
92
104
|
caption: { type: "string", description: "Optional display caption." },
|
|
@@ -96,8 +108,11 @@ export const mediaAgentToolCatalog = [
|
|
|
96
108
|
},
|
|
97
109
|
{
|
|
98
110
|
name: "media_update_metadata",
|
|
99
|
-
description: "Updates a media asset's
|
|
100
|
-
"
|
|
111
|
+
description: "Updates a media asset's metadata: title, alt, caption, credit, and its asset-wide presentation (cssClass, " +
|
|
112
|
+
"htmlAttributes). Omit a field to leave it unchanged. htmlAttributes/cssClass apply EVERYWHERE this asset " +
|
|
113
|
+
"renders; to change one placement on one page only, put the attribute on that page's data-embed-config " +
|
|
114
|
+
"marker element instead (pages_write_region). The bytes (sha256) are write-once and cannot be changed by " +
|
|
115
|
+
"this or any tool — to replace the file itself, upload a new asset.",
|
|
101
116
|
sideEffects: "mutates-durable-state",
|
|
102
117
|
authorization: { permission: "media.update" },
|
|
103
118
|
inputSchema: {
|
|
@@ -110,6 +125,26 @@ export const mediaAgentToolCatalog = [
|
|
|
110
125
|
alt: { type: "string", description: "New alt text. Omit to leave unchanged." },
|
|
111
126
|
caption: { type: "string", description: "New caption. Omit to leave unchanged." },
|
|
112
127
|
credit: { type: "string", description: "New credit line. Omit to leave unchanged." },
|
|
128
|
+
slug: {
|
|
129
|
+
type: "string",
|
|
130
|
+
description: "New slug: lowercase letters, numbers and dashes, not UUID-shaped, unique in the workspace (a slug " +
|
|
131
|
+
"another asset uses is refused and nothing is written). Changing it breaks any page marker that " +
|
|
132
|
+
"references the OLD slug.",
|
|
133
|
+
},
|
|
134
|
+
cssClass: {
|
|
135
|
+
type: "string",
|
|
136
|
+
description: "Space-separated class names added to the rendered <img>/<video> tag everywhere this asset renders. " +
|
|
137
|
+
"Replaces the stored value; empty string clears it.",
|
|
138
|
+
},
|
|
139
|
+
htmlAttributes: {
|
|
140
|
+
type: "string",
|
|
141
|
+
description: "Extra attributes for the rendered <img>/<video> tag, written as HTML attribute text, e.g. " +
|
|
142
|
+
'`autoplay muted loop playsinline` or `loading="eager" data-motion="fade"`. REPLACES the whole ' +
|
|
143
|
+
"stored value: read the current value first and keep what should stay. Empty string clears it. " +
|
|
144
|
+
`Allowed names only: ${MEDIA_HTML_ATTRIBUTE_ALLOWED_NAMES.join(", ")}, plus any data-* or aria-*. ` +
|
|
145
|
+
"Anything else (on* handlers, javascript: values, style, class) is rejected and NOTHING is written. " +
|
|
146
|
+
"Browsers autoplay only video that is also muted (and playsinline on iPhone).",
|
|
147
|
+
},
|
|
113
148
|
},
|
|
114
149
|
},
|
|
115
150
|
},
|