@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.
Files changed (306) hide show
  1. package/dist/content-types/index-provisioning.d.ts +1 -1
  2. package/dist/content-types/index-provisioning.js +1 -1
  3. package/dist/content-types/write-service.js +31 -11
  4. package/dist/core/entity-liveness.d.ts +55 -0
  5. package/dist/core/entity-liveness.js +38 -0
  6. package/dist/core/index.d.ts +1 -0
  7. package/dist/core/index.js +1 -0
  8. package/dist/core/ports.d.ts +1 -1
  9. package/dist/core/tools/registration-kit.d.ts +55 -17
  10. package/dist/core/tools/registration-kit.js +49 -20
  11. package/dist/identity/auth-service.d.ts +1 -1
  12. package/dist/identity/auth-service.js +1 -1
  13. package/dist/identity/seed.d.ts +1 -1
  14. package/dist/identity/seed.js +3 -3
  15. package/dist/media/agent-tools.d.ts +13 -3
  16. package/dist/media/agent-tools.js +45 -10
  17. package/dist/media/html-attributes.d.ts +6 -6
  18. package/dist/media/html-attributes.js +6 -6
  19. package/dist/media/index.d.ts +1 -1
  20. package/dist/media/index.js +1 -1
  21. package/dist/media/media-service.d.ts +2 -1
  22. package/dist/media/media-service.js +23 -7
  23. package/dist/media/tool-registrations.d.ts +12 -0
  24. package/dist/media/tool-registrations.js +20 -12
  25. package/dist/media/types.d.ts +1 -1
  26. package/dist/navigation/index.d.ts +2 -2
  27. package/dist/navigation/index.js +2 -2
  28. package/dist/navigation/menu-service.d.ts +4 -4
  29. package/dist/navigation/menu-service.js +4 -4
  30. package/dist/settings/dictionaries/index.d.ts +3 -3
  31. package/dist/settings/dictionaries/index.js +3 -3
  32. package/dist/taxonomy/agent-tools.d.ts +12 -17
  33. package/dist/taxonomy/agent-tools.js +53 -29
  34. package/dist/taxonomy/content-lookup.d.ts +35 -0
  35. package/dist/taxonomy/content-lookup.js +28 -0
  36. package/dist/taxonomy/index.d.ts +2 -2
  37. package/dist/taxonomy/index.js +2 -2
  38. package/dist/taxonomy/repo.memory.d.ts +11 -2
  39. package/dist/taxonomy/repo.memory.js +11 -0
  40. package/dist/taxonomy/write-service.d.ts +44 -15
  41. package/dist/taxonomy/write-service.js +60 -8
  42. package/package.json +3 -2
  43. package/dist/content-types/agent-tools.d.ts.map +0 -1
  44. package/dist/content-types/agent-tools.js.map +0 -1
  45. package/dist/content-types/cleanup.d.ts.map +0 -1
  46. package/dist/content-types/cleanup.js.map +0 -1
  47. package/dist/content-types/errors.d.ts.map +0 -1
  48. package/dist/content-types/errors.js.map +0 -1
  49. package/dist/content-types/field-defs.d.ts.map +0 -1
  50. package/dist/content-types/field-defs.js.map +0 -1
  51. package/dist/content-types/index-provisioning.d.ts.map +0 -1
  52. package/dist/content-types/index-provisioning.js.map +0 -1
  53. package/dist/content-types/index.d.ts.map +0 -1
  54. package/dist/content-types/index.js.map +0 -1
  55. package/dist/content-types/lifecycle-dispatch.d.ts.map +0 -1
  56. package/dist/content-types/lifecycle-dispatch.js.map +0 -1
  57. package/dist/content-types/lifecycle.d.ts.map +0 -1
  58. package/dist/content-types/lifecycle.js.map +0 -1
  59. package/dist/content-types/list.d.ts.map +0 -1
  60. package/dist/content-types/list.js.map +0 -1
  61. package/dist/content-types/repo.memory.d.ts.map +0 -1
  62. package/dist/content-types/repo.memory.js.map +0 -1
  63. package/dist/content-types/tool-registrations.d.ts.map +0 -1
  64. package/dist/content-types/tool-registrations.js.map +0 -1
  65. package/dist/content-types/types.d.ts.map +0 -1
  66. package/dist/content-types/types.js.map +0 -1
  67. package/dist/content-types/write-service.d.ts.map +0 -1
  68. package/dist/content-types/write-service.js.map +0 -1
  69. package/dist/core/commands/change-set.d.ts.map +0 -1
  70. package/dist/core/commands/change-set.js.map +0 -1
  71. package/dist/core/commands/command.d.ts.map +0 -1
  72. package/dist/core/commands/command.js.map +0 -1
  73. package/dist/core/index.d.ts.map +0 -1
  74. package/dist/core/index.js.map +0 -1
  75. package/dist/core/ports.d.ts.map +0 -1
  76. package/dist/core/ports.js.map +0 -1
  77. package/dist/core/tools/registration-kit.d.ts.map +0 -1
  78. package/dist/core/tools/registration-kit.js.map +0 -1
  79. package/dist/entries/agent-tools.d.ts.map +0 -1
  80. package/dist/entries/agent-tools.js.map +0 -1
  81. package/dist/entries/errors.d.ts.map +0 -1
  82. package/dist/entries/errors.js.map +0 -1
  83. package/dist/entries/field-validation.d.ts.map +0 -1
  84. package/dist/entries/field-validation.js.map +0 -1
  85. package/dist/entries/index.d.ts.map +0 -1
  86. package/dist/entries/index.js.map +0 -1
  87. package/dist/entries/lifecycle-dispatch.d.ts.map +0 -1
  88. package/dist/entries/lifecycle-dispatch.js.map +0 -1
  89. package/dist/entries/list.d.ts.map +0 -1
  90. package/dist/entries/list.js.map +0 -1
  91. package/dist/entries/repo.memory.d.ts.map +0 -1
  92. package/dist/entries/repo.memory.js.map +0 -1
  93. package/dist/entries/tool-registrations.d.ts.map +0 -1
  94. package/dist/entries/tool-registrations.js.map +0 -1
  95. package/dist/entries/types.d.ts.map +0 -1
  96. package/dist/entries/types.js.map +0 -1
  97. package/dist/entries/write-service.d.ts.map +0 -1
  98. package/dist/entries/write-service.js.map +0 -1
  99. package/dist/identity/admin-crud-service.d.ts.map +0 -1
  100. package/dist/identity/admin-crud-service.js.map +0 -1
  101. package/dist/identity/agent-tool-input.d.ts.map +0 -1
  102. package/dist/identity/agent-tool-input.js.map +0 -1
  103. package/dist/identity/agent-tools.d.ts.map +0 -1
  104. package/dist/identity/agent-tools.js.map +0 -1
  105. package/dist/identity/auth-service.d.ts.map +0 -1
  106. package/dist/identity/auth-service.js.map +0 -1
  107. package/dist/identity/authorize.d.ts.map +0 -1
  108. package/dist/identity/authorize.js.map +0 -1
  109. package/dist/identity/grant-service.d.ts.map +0 -1
  110. package/dist/identity/grant-service.js.map +0 -1
  111. package/dist/identity/hasher.d.ts.map +0 -1
  112. package/dist/identity/hasher.js.map +0 -1
  113. package/dist/identity/index.d.ts.map +0 -1
  114. package/dist/identity/index.js.map +0 -1
  115. package/dist/identity/password-policy.d.ts.map +0 -1
  116. package/dist/identity/password-policy.js.map +0 -1
  117. package/dist/identity/permission-migrations.d.ts.map +0 -1
  118. package/dist/identity/permission-migrations.js.map +0 -1
  119. package/dist/identity/permissions.d.ts.map +0 -1
  120. package/dist/identity/permissions.js.map +0 -1
  121. package/dist/identity/ports.d.ts.map +0 -1
  122. package/dist/identity/ports.js.map +0 -1
  123. package/dist/identity/repo.memory.d.ts.map +0 -1
  124. package/dist/identity/repo.memory.js.map +0 -1
  125. package/dist/identity/seed.d.ts.map +0 -1
  126. package/dist/identity/seed.js.map +0 -1
  127. package/dist/identity/tool-registrations.d.ts.map +0 -1
  128. package/dist/identity/tool-registrations.js.map +0 -1
  129. package/dist/identity/types.d.ts.map +0 -1
  130. package/dist/identity/types.js.map +0 -1
  131. package/dist/identity/username.d.ts.map +0 -1
  132. package/dist/identity/username.js.map +0 -1
  133. package/dist/media/agent-tools.d.ts.map +0 -1
  134. package/dist/media/agent-tools.js.map +0 -1
  135. package/dist/media/blob-gc-lock.d.ts.map +0 -1
  136. package/dist/media/blob-gc-lock.js.map +0 -1
  137. package/dist/media/blob-gc.d.ts.map +0 -1
  138. package/dist/media/blob-gc.js.map +0 -1
  139. package/dist/media/blob-key.d.ts.map +0 -1
  140. package/dist/media/blob-key.js.map +0 -1
  141. package/dist/media/blob-store.fs.d.ts.map +0 -1
  142. package/dist/media/blob-store.fs.js.map +0 -1
  143. package/dist/media/blob-store.memory.d.ts.map +0 -1
  144. package/dist/media/blob-store.memory.js.map +0 -1
  145. package/dist/media/content-type-sniffer.d.ts.map +0 -1
  146. package/dist/media/content-type-sniffer.js.map +0 -1
  147. package/dist/media/html-attributes.d.ts.map +0 -1
  148. package/dist/media/html-attributes.js.map +0 -1
  149. package/dist/media/image-transformer.d.ts.map +0 -1
  150. package/dist/media/image-transformer.js.map +0 -1
  151. package/dist/media/image-transformer.sharp.d.ts.map +0 -1
  152. package/dist/media/image-transformer.sharp.js.map +0 -1
  153. package/dist/media/index.d.ts.map +0 -1
  154. package/dist/media/index.js.map +0 -1
  155. package/dist/media/media-service.d.ts.map +0 -1
  156. package/dist/media/media-service.js.map +0 -1
  157. package/dist/media/ports.d.ts.map +0 -1
  158. package/dist/media/ports.js.map +0 -1
  159. package/dist/media/rendition-service.d.ts.map +0 -1
  160. package/dist/media/rendition-service.js.map +0 -1
  161. package/dist/media/repo.memory.d.ts.map +0 -1
  162. package/dist/media/repo.memory.js.map +0 -1
  163. package/dist/media/tool-registrations.d.ts.map +0 -1
  164. package/dist/media/tool-registrations.js.map +0 -1
  165. package/dist/media/transform-lock.d.ts.map +0 -1
  166. package/dist/media/transform-lock.js.map +0 -1
  167. package/dist/media/transform-registry.d.ts.map +0 -1
  168. package/dist/media/transform-registry.js.map +0 -1
  169. package/dist/media/transform-types.d.ts.map +0 -1
  170. package/dist/media/transform-types.js.map +0 -1
  171. package/dist/media/types.d.ts.map +0 -1
  172. package/dist/media/types.js.map +0 -1
  173. package/dist/navigation/agent-tools.d.ts.map +0 -1
  174. package/dist/navigation/agent-tools.js.map +0 -1
  175. package/dist/navigation/contracts.d.ts.map +0 -1
  176. package/dist/navigation/contracts.js.map +0 -1
  177. package/dist/navigation/index.d.ts.map +0 -1
  178. package/dist/navigation/index.js.map +0 -1
  179. package/dist/navigation/menu-service.d.ts.map +0 -1
  180. package/dist/navigation/menu-service.js.map +0 -1
  181. package/dist/navigation/ports.d.ts.map +0 -1
  182. package/dist/navigation/ports.js.map +0 -1
  183. package/dist/navigation/read-model.d.ts.map +0 -1
  184. package/dist/navigation/read-model.js.map +0 -1
  185. package/dist/navigation/reconcile.d.ts.map +0 -1
  186. package/dist/navigation/reconcile.js.map +0 -1
  187. package/dist/navigation/repo.memory.d.ts.map +0 -1
  188. package/dist/navigation/repo.memory.js.map +0 -1
  189. package/dist/navigation/resolver.d.ts.map +0 -1
  190. package/dist/navigation/resolver.js.map +0 -1
  191. package/dist/navigation/tool-registrations.d.ts.map +0 -1
  192. package/dist/navigation/tool-registrations.js.map +0 -1
  193. package/dist/navigation/types.d.ts.map +0 -1
  194. package/dist/navigation/types.js.map +0 -1
  195. package/dist/presentation/index.d.ts.map +0 -1
  196. package/dist/presentation/index.js.map +0 -1
  197. package/dist/presentation/presentation.d.ts.map +0 -1
  198. package/dist/presentation/presentation.js.map +0 -1
  199. package/dist/presentation/repo.memory.d.ts.map +0 -1
  200. package/dist/presentation/repo.memory.js.map +0 -1
  201. package/dist/server/index.d.ts.map +0 -1
  202. package/dist/server/index.js.map +0 -1
  203. package/dist/settings/agent-tools.d.ts.map +0 -1
  204. package/dist/settings/agent-tools.js.map +0 -1
  205. package/dist/settings/agent-writable-preferences.d.ts.map +0 -1
  206. package/dist/settings/agent-writable-preferences.js.map +0 -1
  207. package/dist/settings/change-feed.d.ts.map +0 -1
  208. package/dist/settings/change-feed.js.map +0 -1
  209. package/dist/settings/definitions-dispatch.d.ts.map +0 -1
  210. package/dist/settings/definitions-dispatch.js.map +0 -1
  211. package/dist/settings/dictionaries/index.d.ts.map +0 -1
  212. package/dist/settings/dictionaries/index.js.map +0 -1
  213. package/dist/settings/dictionaries/settings-dialog.ar.d.ts.map +0 -1
  214. package/dist/settings/dictionaries/settings-dialog.ar.js.map +0 -1
  215. package/dist/settings/dictionaries/settings-dialog.bn.d.ts.map +0 -1
  216. package/dist/settings/dictionaries/settings-dialog.bn.js.map +0 -1
  217. package/dist/settings/dictionaries/settings-dialog.de.d.ts.map +0 -1
  218. package/dist/settings/dictionaries/settings-dialog.de.js.map +0 -1
  219. package/dist/settings/dictionaries/settings-dialog.es.d.ts.map +0 -1
  220. package/dist/settings/dictionaries/settings-dialog.es.js.map +0 -1
  221. package/dist/settings/dictionaries/settings-dialog.fa.d.ts.map +0 -1
  222. package/dist/settings/dictionaries/settings-dialog.fa.js.map +0 -1
  223. package/dist/settings/dictionaries/settings-dialog.fr.d.ts.map +0 -1
  224. package/dist/settings/dictionaries/settings-dialog.fr.js.map +0 -1
  225. package/dist/settings/dictionaries/settings-dialog.hi.d.ts.map +0 -1
  226. package/dist/settings/dictionaries/settings-dialog.hi.js.map +0 -1
  227. package/dist/settings/dictionaries/settings-dialog.hu.d.ts.map +0 -1
  228. package/dist/settings/dictionaries/settings-dialog.hu.js.map +0 -1
  229. package/dist/settings/dictionaries/settings-dialog.id.d.ts.map +0 -1
  230. package/dist/settings/dictionaries/settings-dialog.id.js.map +0 -1
  231. package/dist/settings/dictionaries/settings-dialog.it.d.ts.map +0 -1
  232. package/dist/settings/dictionaries/settings-dialog.it.js.map +0 -1
  233. package/dist/settings/dictionaries/settings-dialog.ja.d.ts.map +0 -1
  234. package/dist/settings/dictionaries/settings-dialog.ja.js.map +0 -1
  235. package/dist/settings/dictionaries/settings-dialog.ko.d.ts.map +0 -1
  236. package/dist/settings/dictionaries/settings-dialog.ko.js.map +0 -1
  237. package/dist/settings/dictionaries/settings-dialog.pl.d.ts.map +0 -1
  238. package/dist/settings/dictionaries/settings-dialog.pl.js.map +0 -1
  239. package/dist/settings/dictionaries/settings-dialog.pt-BR.d.ts.map +0 -1
  240. package/dist/settings/dictionaries/settings-dialog.pt-BR.js.map +0 -1
  241. package/dist/settings/dictionaries/settings-dialog.ru.d.ts.map +0 -1
  242. package/dist/settings/dictionaries/settings-dialog.ru.js.map +0 -1
  243. package/dist/settings/dictionaries/settings-dialog.th.d.ts.map +0 -1
  244. package/dist/settings/dictionaries/settings-dialog.th.js.map +0 -1
  245. package/dist/settings/dictionaries/settings-dialog.tr.d.ts.map +0 -1
  246. package/dist/settings/dictionaries/settings-dialog.tr.js.map +0 -1
  247. package/dist/settings/dictionaries/settings-dialog.uk.d.ts.map +0 -1
  248. package/dist/settings/dictionaries/settings-dialog.uk.js.map +0 -1
  249. package/dist/settings/dictionaries/settings-dialog.ur.d.ts.map +0 -1
  250. package/dist/settings/dictionaries/settings-dialog.ur.js.map +0 -1
  251. package/dist/settings/dictionaries/settings-dialog.zh-CN.d.ts.map +0 -1
  252. package/dist/settings/dictionaries/settings-dialog.zh-CN.js.map +0 -1
  253. package/dist/settings/dictionaries/settings-dialog.zh-TW.d.ts.map +0 -1
  254. package/dist/settings/dictionaries/settings-dialog.zh-TW.js.map +0 -1
  255. package/dist/settings/ensure-definitions.d.ts.map +0 -1
  256. package/dist/settings/ensure-definitions.js.map +0 -1
  257. package/dist/settings/errors.d.ts.map +0 -1
  258. package/dist/settings/errors.js.map +0 -1
  259. package/dist/settings/index.d.ts.map +0 -1
  260. package/dist/settings/index.js.map +0 -1
  261. package/dist/settings/ports.d.ts.map +0 -1
  262. package/dist/settings/ports.js.map +0 -1
  263. package/dist/settings/purge-service.d.ts.map +0 -1
  264. package/dist/settings/purge-service.js.map +0 -1
  265. package/dist/settings/repo.memory.d.ts.map +0 -1
  266. package/dist/settings/repo.memory.js.map +0 -1
  267. package/dist/settings/settings.d.ts.map +0 -1
  268. package/dist/settings/settings.js.map +0 -1
  269. package/dist/settings/tool-registrations.d.ts.map +0 -1
  270. package/dist/settings/tool-registrations.js.map +0 -1
  271. package/dist/settings/types.d.ts.map +0 -1
  272. package/dist/settings/types.js.map +0 -1
  273. package/dist/settings/ui-tab-definitions.d.ts.map +0 -1
  274. package/dist/settings/ui-tab-definitions.js.map +0 -1
  275. package/dist/settings/write-service.d.ts.map +0 -1
  276. package/dist/settings/write-service.js.map +0 -1
  277. package/dist/taxonomy/agent-tools.d.ts.map +0 -1
  278. package/dist/taxonomy/agent-tools.js.map +0 -1
  279. package/dist/taxonomy/content-lookup.d.ts.map +0 -1
  280. package/dist/taxonomy/content-lookup.js.map +0 -1
  281. package/dist/taxonomy/index.d.ts.map +0 -1
  282. package/dist/taxonomy/index.js.map +0 -1
  283. package/dist/taxonomy/list.d.ts.map +0 -1
  284. package/dist/taxonomy/list.js.map +0 -1
  285. package/dist/taxonomy/merge-term.d.ts.map +0 -1
  286. package/dist/taxonomy/merge-term.js.map +0 -1
  287. package/dist/taxonomy/repo.memory.d.ts.map +0 -1
  288. package/dist/taxonomy/repo.memory.js.map +0 -1
  289. package/dist/taxonomy/validation-chain.d.ts.map +0 -1
  290. package/dist/taxonomy/validation-chain.js.map +0 -1
  291. package/dist/taxonomy/write-service.d.ts.map +0 -1
  292. package/dist/taxonomy/write-service.js.map +0 -1
  293. package/dist/workspace/agent-tools.d.ts.map +0 -1
  294. package/dist/workspace/agent-tools.js.map +0 -1
  295. package/dist/workspace/create.d.ts.map +0 -1
  296. package/dist/workspace/create.js.map +0 -1
  297. package/dist/workspace/delete.d.ts.map +0 -1
  298. package/dist/workspace/delete.js.map +0 -1
  299. package/dist/workspace/index.d.ts.map +0 -1
  300. package/dist/workspace/index.js.map +0 -1
  301. package/dist/workspace/repo.memory.d.ts.map +0 -1
  302. package/dist/workspace/repo.memory.js.map +0 -1
  303. package/dist/workspace/tool-registrations.d.ts.map +0 -1
  304. package/dist/workspace/tool-registrations.js.map +0 -1
  305. package/dist/workspace/update.d.ts.map +0 -1
  306. 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 Tovu re-exports it), and it would also
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 Tovu re-exports it), and it would also
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
- for (const field of input.fields) {
39
- if (!validateIdentifierGrammar(field.name)) {
40
- return { ok: false, error: new InvalidFieldNameGrammarError(`field name '${field.name}' fails the identifier grammar gate`) };
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
- for (const field of input.fields) {
129
- if (!validateIdentifierGrammar(field.name)) {
130
- return { ok: false, error: new InvalidFieldNameGrammarError(`field name '${field.name}' fails the identifier grammar gate`) };
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
@@ -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
@@ -39,5 +39,6 @@
39
39
  * behind a registration call the host makes, not behind a kernel barrel.
40
40
  */
41
41
  export * from './commands/command.js';
42
+ export * from './entity-liveness.js';
42
43
  export * from './tools/registration-kit.js';
43
44
  //# sourceMappingURL=index.js.map
@@ -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 Tovu host). `"pending"` re-enters the retry queue at `nextAttemptAt`; `"failed"`
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 cannot be honored while no confirmation transport is wired.
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". A host can only deliver that by building `ToolExecutor` with an
98
- * `ExecutionDelegate`. Without one, `descriptor.requiresConfirmation` would park the execution on a
99
- * promise only `resumeConfirmation` can settle — and nothing would call it. The park is also unbounded, because `descriptor.timeoutMs`'s timer is armed only AFTER the
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 must not be wired at all. Today none is — `collections_execute_cleanup`,
103
- * `database_execute_migrate_forward`, and `backup_execute_restore` all carry it and all three are
104
- * declared unwired — which makes this guard a statement of the invariant rather than a fix: a
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 confirmation
224
- * transport that does not exist.
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
- * @throws {Error} If the tool is unclassified, misclassified, or needs missing confirmation support.
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 — see
262
- * {@link ACTOR_CLASS_RULES_REQUIRING_CONFIRMATION_TRANSPORT} for why that omission is safe rather
263
- * than a hole.
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 cannot be honored while no confirmation transport is wired.
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". A host can only deliver that by building `ToolExecutor` with an
55
- * `ExecutionDelegate`. Without one, `descriptor.requiresConfirmation` would park the execution on a
56
- * promise only `resumeConfirmation` can settle — and nothing would call it. The park is also unbounded, because `descriptor.timeoutMs`'s timer is armed only AFTER the
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 must not be wired at all. Today none is — `collections_execute_cleanup`,
60
- * `database_execute_migrate_forward`, and `backup_execute_restore` all carry it and all three are
61
- * declared unwired — which makes this guard a statement of the invariant rather than a fix: a
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 confirmation
260
- * transport that does not exist.
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
- * @throws {Error} If the tool is unclassified, misclassified, or needs missing confirmation support.
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
- if (catalogEntry.actorClassRule && ACTOR_CLASS_RULES_REQUIRING_CONFIRMATION_TRANSPORT.has(catalogEntry.actorClassRule)) {
283
- throw new Error(`tool-registrations: '${toolId}' declares actorClassRule '${catalogEntry.actorClassRule}', which requires a human-confirmation transport this host has not wired — leave it unwired until one exists (see ACTOR_CLASS_RULES_REQUIRING_CONFIRMATION_TRANSPORT)`);
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 — see
306
- * {@link ACTOR_CLASS_RULES_REQUIRING_CONFIRMATION_TRANSPORT} for why that omission is safe rather
307
- * than a hole.
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 (Tovu) had duplicated this file's private `hashToken` byte-for-byte because
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 (Tovu) had duplicated this file's private `hashToken` byte-for-byte because
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:
@@ -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
- * Tovu's `applyBuiltinRoleGrants` / `migrateDeprecatedPermissionGrants`, chained onto
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
@@ -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 (Tovu
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: Tovu's
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
- * Tovu's `applyBuiltinRoleGrants` / `migrateDeprecatedPermissionGrants`, chained onto
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 constants its own domain already enforces
38
- * (`DEFAULT_MAX_UPLOAD_BYTES`/`DEFAULT_ALLOWED_MIME_TYPES` from `media-service.ts`), so the
39
- * published JSON Schemas cannot drift from the validators.
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 constants its own domain already enforces
38
- * (`DEFAULT_MAX_UPLOAD_BYTES`/`DEFAULT_ALLOWED_MIME_TYPES` from `media-service.ts`), so the
39
- * published JSON Schemas cannot drift from the validators.
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 { DEFAULT_ALLOWED_MIME_TYPES, DEFAULT_MAX_UPLOAD_BYTES } from "./media-service.js";
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 ${DEFAULT_MAX_UPLOAD_BYTES} bytes. Uploading the same bytes twice ` +
72
- `always creates two separate library entries (the underlying blob is deduplicated, but each upload is its own asset).`,
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: `The file's bytes, base64-encoded. Decoded size must be over 0 and at most ${DEFAULT_MAX_UPLOAD_BYTES} bytes.`,
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 editorial metadata (title/alt/caption/credit only). The asset's underlying bytes " +
100
- "(sha256) are write-once and cannot be changed by this or any tool — to replace the file itself, upload a new asset.",
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
  },