@jini-ai/cms 0.3.11 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +47 -19
  3. package/dist/content-types/agent-tools.d.ts +3 -56
  4. package/dist/content-types/agent-tools.js +22 -6
  5. package/dist/content-types/cleanup.d.ts +18 -13
  6. package/dist/content-types/cleanup.js +16 -11
  7. package/dist/content-types/errors.d.ts +57 -24
  8. package/dist/content-types/errors.js +56 -39
  9. package/dist/content-types/field-defs.d.ts +8 -4
  10. package/dist/content-types/field-defs.js +19 -14
  11. package/dist/content-types/index-provisioning.d.ts +61 -47
  12. package/dist/content-types/index-provisioning.js +90 -66
  13. package/dist/content-types/index.d.ts +4 -3
  14. package/dist/content-types/lifecycle-dispatch.d.ts +11 -5
  15. package/dist/content-types/lifecycle-dispatch.js +5 -4
  16. package/dist/content-types/lifecycle.d.ts +23 -18
  17. package/dist/content-types/lifecycle.js +71 -64
  18. package/dist/content-types/repo.memory.d.ts +5 -4
  19. package/dist/content-types/repo.memory.js +3 -2
  20. package/dist/content-types/tool-registrations.d.ts +6 -8
  21. package/dist/content-types/tool-registrations.js +24 -28
  22. package/dist/content-types/types.d.ts +17 -13
  23. package/dist/content-types/types.js +11 -5
  24. package/dist/content-types/write-service.d.ts +20 -22
  25. package/dist/content-types/write-service.js +78 -75
  26. package/dist/core/authorization.d.ts +15 -0
  27. package/dist/core/authorization.js +2 -0
  28. package/dist/core/commands/change-set.d.ts +15 -7
  29. package/dist/core/commands/command.d.ts +45 -26
  30. package/dist/core/commands/command.js +73 -63
  31. package/dist/core/entity-liveness.d.ts +11 -7
  32. package/dist/core/entity-liveness.js +5 -4
  33. package/dist/core/index.d.ts +12 -5
  34. package/dist/core/index.js +10 -4
  35. package/dist/core/ports.d.ts +25 -24
  36. package/dist/core/ports.js +1 -0
  37. package/dist/core/tools/args.d.ts +49 -0
  38. package/dist/core/tools/args.js +46 -0
  39. package/dist/core/tools/index.d.ts +3 -0
  40. package/dist/core/tools/index.js +3 -0
  41. package/dist/entries/agent-tools.d.ts +1 -73
  42. package/dist/entries/agent-tools.js +19 -13
  43. package/dist/entries/errors.d.ts +28 -14
  44. package/dist/entries/errors.js +17 -11
  45. package/dist/entries/field-validation.d.ts +5 -3
  46. package/dist/entries/field-validation.js +7 -5
  47. package/dist/entries/index.d.ts +2 -2
  48. package/dist/entries/lifecycle-dispatch.d.ts +5 -2
  49. package/dist/entries/lifecycle-dispatch.js +2 -1
  50. package/dist/entries/list.d.ts +4 -2
  51. package/dist/entries/list.js +4 -3
  52. package/dist/entries/repo.memory.d.ts +12 -10
  53. package/dist/entries/repo.memory.js +11 -10
  54. package/dist/entries/tool-registrations.d.ts +6 -8
  55. package/dist/entries/tool-registrations.js +42 -41
  56. package/dist/entries/types.d.ts +0 -7
  57. package/dist/entries/write-service.d.ts +28 -29
  58. package/dist/entries/write-service.js +114 -105
  59. package/dist/http/settings/cms-adapter.d.ts +20 -0
  60. package/dist/http/settings/cms-adapter.js +124 -0
  61. package/dist/http/settings/contracts.d.ts +135 -0
  62. package/dist/http/settings/contracts.js +2 -0
  63. package/dist/http/settings/http.d.ts +10 -0
  64. package/dist/http/settings/http.js +211 -0
  65. package/dist/http/settings/index.d.ts +5 -0
  66. package/dist/http/settings/index.js +4 -0
  67. package/dist/http/settings/shared.d.ts +68 -0
  68. package/dist/http/settings/shared.js +54 -0
  69. package/dist/http/settings/stream.d.ts +24 -0
  70. package/dist/http/settings/stream.js +147 -0
  71. package/dist/media/agent-tools.d.ts +1 -66
  72. package/dist/media/agent-tools.js +5 -1
  73. package/dist/media/blob-gc-lock.d.ts +6 -2
  74. package/dist/media/blob-gc-lock.js +4 -2
  75. package/dist/media/blob-gc.d.ts +64 -51
  76. package/dist/media/blob-gc.js +62 -52
  77. package/dist/media/blob-key.d.ts +1 -1
  78. package/dist/media/content-type-sniffer.d.ts +3 -1
  79. package/dist/media/content-type-sniffer.js +1 -1
  80. package/dist/media/html-attributes.d.ts +9 -5
  81. package/dist/media/html-attributes.js +8 -6
  82. package/dist/media/image-transformer.js +1 -1
  83. package/dist/media/image-transformer.sharp.d.ts +6 -0
  84. package/dist/media/image-transformer.sharp.js +12 -6
  85. package/dist/media/import/fetch-image.d.ts +24 -0
  86. package/dist/media/import/fetch-image.js +137 -0
  87. package/dist/media/import/index.d.ts +3 -0
  88. package/dist/media/import/index.js +3 -0
  89. package/dist/media/import/ports.d.ts +38 -0
  90. package/dist/media/index.d.ts +7 -1
  91. package/dist/media/index.js +1 -0
  92. package/dist/media/media-service.d.ts +14 -10
  93. package/dist/media/media-service.js +57 -52
  94. package/dist/media/ports.d.ts +3 -2
  95. package/dist/media/rendition-service.d.ts +3 -3
  96. package/dist/media/rendition-service.js +38 -37
  97. package/dist/media/repo.memory.d.ts +18 -7
  98. package/dist/media/repo.memory.js +12 -6
  99. package/dist/media/tool-registrations.d.ts +16 -15
  100. package/dist/media/tool-registrations.js +84 -72
  101. package/dist/media/transform-lock.d.ts +22 -12
  102. package/dist/media/transform-lock.js +14 -13
  103. package/dist/media/transform-registry.d.ts +3 -3
  104. package/dist/media/transform-registry.js +19 -18
  105. package/dist/media/transform-types.d.ts +8 -3
  106. package/dist/media/transform-types.js +10 -5
  107. package/dist/media/types.d.ts +21 -4
  108. package/dist/media/types.js +15 -2
  109. package/dist/navigation/agent-tools.d.ts +1 -80
  110. package/dist/navigation/agent-tools.js +15 -3
  111. package/dist/navigation/contracts.d.ts +1 -1
  112. package/dist/navigation/index.d.ts +2 -2
  113. package/dist/navigation/menu-service.d.ts +57 -40
  114. package/dist/navigation/menu-service.js +80 -69
  115. package/dist/navigation/ports.d.ts +1 -1
  116. package/dist/navigation/read-model.d.ts +3 -1
  117. package/dist/navigation/read-model.js +2 -2
  118. package/dist/navigation/reconcile.d.ts +2 -2
  119. package/dist/navigation/reconcile.js +2 -1
  120. package/dist/navigation/repo.memory.d.ts +7 -3
  121. package/dist/navigation/repo.memory.js +4 -2
  122. package/dist/navigation/resolver.d.ts +4 -1
  123. package/dist/navigation/resolver.js +1 -1
  124. package/dist/navigation/tool-registrations.d.ts +6 -8
  125. package/dist/navigation/tool-registrations.js +44 -42
  126. package/dist/navigation/types.d.ts +1 -1
  127. package/dist/presentation/presentation.d.ts +11 -3
  128. package/dist/presentation/presentation.js +13 -6
  129. package/dist/presentation/repo.memory.d.ts +6 -2
  130. package/dist/presentation/repo.memory.js +3 -2
  131. package/dist/settings/agent-tools.d.ts +2 -75
  132. package/dist/settings/agent-tools.js +49 -45
  133. package/dist/settings/agent-value-write-tools.d.ts +22 -0
  134. package/dist/settings/agent-value-write-tools.js +154 -0
  135. package/dist/settings/agent-writable-preferences.js +8 -20
  136. package/dist/settings/agent-write-denylist.d.ts +19 -0
  137. package/dist/settings/agent-write-denylist.js +15 -0
  138. package/dist/settings/change-feed.d.ts +11 -2
  139. package/dist/settings/change-feed.js +4 -4
  140. package/dist/settings/definitions-dispatch.d.ts +8 -3
  141. package/dist/settings/definitions-dispatch.js +5 -5
  142. package/dist/settings/dictionaries/index.d.ts +5 -2
  143. package/dist/settings/dictionaries/index.js +2 -2
  144. package/dist/settings/dictionaries/settings-dialog.ar.js +0 -1
  145. package/dist/settings/dictionaries/settings-dialog.bn.js +0 -1
  146. package/dist/settings/dictionaries/settings-dialog.de.js +0 -1
  147. package/dist/settings/dictionaries/settings-dialog.es.js +0 -1
  148. package/dist/settings/dictionaries/settings-dialog.fa.js +0 -1
  149. package/dist/settings/dictionaries/settings-dialog.fr.js +0 -1
  150. package/dist/settings/dictionaries/settings-dialog.hi.js +0 -1
  151. package/dist/settings/dictionaries/settings-dialog.hu.js +0 -1
  152. package/dist/settings/dictionaries/settings-dialog.id.js +0 -1
  153. package/dist/settings/dictionaries/settings-dialog.it.js +0 -1
  154. package/dist/settings/dictionaries/settings-dialog.ja.js +0 -1
  155. package/dist/settings/dictionaries/settings-dialog.ko.js +0 -1
  156. package/dist/settings/dictionaries/settings-dialog.pl.js +0 -1
  157. package/dist/settings/dictionaries/settings-dialog.pt-BR.js +0 -1
  158. package/dist/settings/dictionaries/settings-dialog.ru.js +0 -1
  159. package/dist/settings/dictionaries/settings-dialog.th.js +0 -1
  160. package/dist/settings/dictionaries/settings-dialog.tr.js +0 -1
  161. package/dist/settings/dictionaries/settings-dialog.uk.js +0 -1
  162. package/dist/settings/dictionaries/settings-dialog.ur.js +0 -1
  163. package/dist/settings/dictionaries/settings-dialog.zh-CN.js +0 -1
  164. package/dist/settings/dictionaries/settings-dialog.zh-TW.js +0 -1
  165. package/dist/settings/ensure-definitions.d.ts +5 -5
  166. package/dist/settings/errors.d.ts +7 -1
  167. package/dist/settings/errors.js +7 -1
  168. package/dist/settings/index.d.ts +4 -1
  169. package/dist/settings/index.js +1 -0
  170. package/dist/settings/ports.d.ts +6 -5
  171. package/dist/settings/principal-lookup.d.ts +9 -0
  172. package/dist/settings/principal-lookup.js +2 -0
  173. package/dist/settings/purge-service.d.ts +5 -4
  174. package/dist/settings/purge-service.js +3 -2
  175. package/dist/settings/settings.d.ts +22 -7
  176. package/dist/settings/settings.js +47 -26
  177. package/dist/settings/tool-registrations.d.ts +25 -32
  178. package/dist/settings/tool-registrations.js +66 -56
  179. package/dist/settings/types.d.ts +1 -1
  180. package/dist/settings/ui-tab-definitions.d.ts +1 -1
  181. package/dist/settings/write-service.d.ts +32 -26
  182. package/dist/settings/write-service.js +26 -20
  183. package/dist/taxonomy/agent-tools.d.ts +1 -66
  184. package/dist/taxonomy/agent-tools.js +25 -15
  185. package/dist/taxonomy/content-lookup.js +1 -1
  186. package/dist/taxonomy/index.d.ts +2 -2
  187. package/dist/taxonomy/merge-term.d.ts +7 -4
  188. package/dist/taxonomy/merge-term.js +7 -5
  189. package/dist/taxonomy/repo.memory.d.ts +42 -19
  190. package/dist/taxonomy/repo.memory.js +14 -12
  191. package/dist/taxonomy/validation-chain.d.ts +39 -18
  192. package/dist/taxonomy/validation-chain.js +35 -25
  193. package/dist/taxonomy/write-service.d.ts +103 -44
  194. package/dist/taxonomy/write-service.js +221 -179
  195. package/dist/trash/adapters/record-store.d.ts +52 -0
  196. package/dist/trash/adapters/record-store.js +36 -0
  197. package/dist/trash/cursor.d.ts +20 -0
  198. package/dist/trash/cursor.js +36 -0
  199. package/dist/trash/follow-ups.d.ts +76 -0
  200. package/dist/trash/follow-ups.js +46 -0
  201. package/dist/trash/index.d.ts +8 -0
  202. package/dist/trash/index.js +8 -0
  203. package/dist/trash/ports.d.ts +308 -0
  204. package/dist/trash/ports.js +2 -0
  205. package/dist/trash/repo.memory.d.ts +50 -0
  206. package/dist/trash/repo.memory.js +114 -0
  207. package/dist/trash/sweeper.d.ts +77 -0
  208. package/dist/trash/sweeper.js +127 -0
  209. package/dist/trash/write-service.d.ts +100 -0
  210. package/dist/trash/write-service.js +242 -0
  211. package/dist/workspace/agent-tools.d.ts +1 -59
  212. package/dist/workspace/agent-tools.js +27 -12
  213. package/dist/workspace/create.d.ts +31 -6
  214. package/dist/workspace/create.js +18 -5
  215. package/dist/workspace/delete.d.ts +18 -13
  216. package/dist/workspace/delete.js +20 -12
  217. package/dist/workspace/index.d.ts +1 -1
  218. package/dist/workspace/repo.memory.d.ts +19 -4
  219. package/dist/workspace/repo.memory.js +26 -4
  220. package/dist/workspace/tool-registrations.d.ts +2 -2
  221. package/dist/workspace/tool-registrations.js +13 -13
  222. package/dist/workspace/update.d.ts +6 -5
  223. package/dist/workspace/update.js +9 -8
  224. package/package.json +44 -38
  225. package/dist/identity/admin-crud-service.d.ts +0 -280
  226. package/dist/identity/admin-crud-service.js +0 -468
  227. package/dist/identity/agent-tool-input.d.ts +0 -58
  228. package/dist/identity/agent-tool-input.js +0 -126
  229. package/dist/identity/agent-tools.d.ts +0 -101
  230. package/dist/identity/agent-tools.js +0 -290
  231. package/dist/identity/auth-service.d.ts +0 -118
  232. package/dist/identity/auth-service.js +0 -170
  233. package/dist/identity/authorize.d.ts +0 -90
  234. package/dist/identity/authorize.js +0 -94
  235. package/dist/identity/grant-service.d.ts +0 -198
  236. package/dist/identity/grant-service.js +0 -361
  237. package/dist/identity/hasher.d.ts +0 -39
  238. package/dist/identity/hasher.js +0 -39
  239. package/dist/identity/index.d.ts +0 -62
  240. package/dist/identity/index.js +0 -54
  241. package/dist/identity/password-policy.d.ts +0 -57
  242. package/dist/identity/password-policy.js +0 -66
  243. package/dist/identity/permission-migrations.d.ts +0 -86
  244. package/dist/identity/permission-migrations.js +0 -67
  245. package/dist/identity/permissions.d.ts +0 -35
  246. package/dist/identity/permissions.js +0 -603
  247. package/dist/identity/ports.d.ts +0 -201
  248. package/dist/identity/repo.memory.d.ts +0 -164
  249. package/dist/identity/repo.memory.js +0 -224
  250. package/dist/identity/seed.d.ts +0 -54
  251. package/dist/identity/seed.js +0 -429
  252. package/dist/identity/tool-registrations.d.ts +0 -50
  253. package/dist/identity/tool-registrations.js +0 -419
  254. package/dist/identity/types.d.ts +0 -185
  255. package/dist/identity/types.js +0 -64
  256. package/dist/identity/username.d.ts +0 -12
  257. package/dist/identity/username.js +0 -14
  258. /package/dist/{identity → media/import}/ports.js +0 -0
@@ -1,24 +1,27 @@
1
+ import { nowIso as kernelNowIso } from "@jini-ai/core/primitives";
1
2
  /**
2
3
  * Thrown when a command's idempotency key was already used in the workspace.
3
4
  * Callers should re-read current state instead of retrying the mutation.
4
5
  */
5
6
  export class DuplicateCommandError extends Error {
6
7
  changeSetId;
7
- constructor(message, changeSetId) {
8
+ constructor(requiredArgs, optionalArgs = {}) {
9
+ const { message, changeSetId } = requiredArgs;
8
10
  super(message);
9
11
  this.changeSetId = changeSetId;
10
12
  }
11
13
  }
12
14
  /**
13
- * Thrown when `authorize()` denies the caller. Routes map
14
- * this to 403 `FORBIDDEN`. Raised before the idempotency check (INV-04) — a
15
+ * Thrown when `authorize` denies the caller. Routes map
16
+ * this to 403 `FORBIDDEN`. Raised before the idempotency check — a
15
17
  * replayed command id from a denied caller therefore never produces
16
- * `DuplicateCommandError` either (EC-08).
18
+ * `DuplicateCommandError` either.
19
+ * See docs/decisions/DR-006-mutation-audit-atomicity.md.
17
20
  */
18
21
  export class ForbiddenError extends Error {
19
22
  permission;
20
23
  reason;
21
- constructor(message, permission, reason) {
24
+ constructor({ message, permission, reason }, _optional = {}) {
22
25
  super(message);
23
26
  this.permission = permission;
24
27
  this.reason = reason;
@@ -47,26 +50,29 @@ function isIdempotencyKeyConflict(error) {
47
50
  * If execute throws, nothing is recorded; if inverse capture throws, the
48
51
  * mutation never runs.
49
52
  *
50
- * The feature mutation and its change-set record commit as one unit of work
51
- * (REQ-01): if `changeSets.insert` fails after `execute` has applied the
52
- * mutation, the gateway rolls the mutation back via `mutation.rollback` and
53
- * re-throws, so no change set and no outbox event survive (EC-08 / AC-17,
54
- * INV-01).
53
+ * With a working `mutation.rollback`, the feature mutation and its change-set
54
+ * record form one compensated unit of work : failures in version capture,
55
+ * record preparation or `changeSets.insert` after `execute` trigger rollback and
56
+ * rejection. When rollback is omitted, the gateway still
57
+ * rejects but cannot undo the applied feature mutation; a caller-owned outer
58
+ * transaction must supply atomicity. Record/event atomicity depends on the repo
59
+ * honoring its port contract.
55
60
  *
56
- * The `change-set.applied` event is passed as `changeSets.insert()`'s third argument, not a
57
- * separate `deps.outbox.enqueue()` call afterward — a durable adapter co-persists it inside the
61
+ * The `change-set.applied` event is passed in `changeSets.insert`'s optional object, not a
62
+ * separate `deps.outbox.enqueue` call afterward — a durable adapter co-persists it inside the
58
63
  * same transaction as the change-set record, so it can never land without a durable delivery
59
64
  * record (or vice versa). This still does NOT cover the domain mutation itself
60
- * (`mutation.execute()` stays outside any shared transaction, covered only by the compensating
65
+ * (`mutation.execute` stays outside any shared transaction, covered only by the compensating
61
66
  * rollback above), and it covers only this producer. See
62
67
  * `docs/decisions/change-set-outbox-transaction-boundary.md` for the full rationale, including the
63
- * other direct `OutboxPort.enqueue()` producers this decision deliberately leaves untouched.
68
+ * other direct `OutboxPort.enqueue` producers this decision deliberately leaves untouched.
69
+ * See docs/decisions/DR-006-mutation-audit-atomicity.md.
64
70
  */
65
71
  export async function executeCommand(required, _optional = {}) {
66
72
  const { deps, command, mutation } = required;
67
73
  // authorize() runs BEFORE the idempotency check, so
68
74
  // a denied caller never learns whether a replayed command id previously
69
- // succeeded (no DUPLICATE_COMMAND / changeSetId leak, EC-08). `permission`
75
+ // succeeded (no DUPLICATE_COMMAND / changeSetId leak). `permission`. See docs/decisions/DR-006-mutation-audit-atomicity.md.
70
76
  // and `authorize` are wired together or not at all (see ExecuteCommandDeps
71
77
  // doc) — partial wiring is a programming error, not a silent allow/skip.
72
78
  if (Boolean(deps.authorize) !== Boolean(command.permission)) {
@@ -82,7 +88,7 @@ export async function executeCommand(required, _optional = {}) {
82
88
  entityId: mutation.entityId,
83
89
  });
84
90
  if (!authResult.allowed) {
85
- throw new ForbiddenError(`principal '${command.actor.id}' is not authorized for '${command.permission}' (${authResult.reason})`, command.permission, authResult.reason);
91
+ throw new ForbiddenError({ message: `principal '${command.actor.id}' is not authorized for '${command.permission}' (${authResult.reason})`, permission: command.permission, reason: authResult.reason });
86
92
  }
87
93
  }
88
94
  if (command.idempotencyKey) {
@@ -91,63 +97,68 @@ export async function executeCommand(required, _optional = {}) {
91
97
  idempotencyKey: command.idempotencyKey,
92
98
  });
93
99
  if (existing) {
94
- throw new DuplicateCommandError(`command with idempotency key '${command.idempotencyKey}' was already executed`, existing.id);
100
+ throw new DuplicateCommandError({ message: `command with idempotency key '${command.idempotencyKey}' was already executed`, changeSetId: existing.id });
95
101
  }
96
102
  }
97
103
  const inversePayload = await mutation.captureInverse();
98
104
  const result = await mutation.execute();
99
- const entityVersionAtApply = mutation.captureEntityVersion?.(result) ?? undefined;
100
- const now = deps.clock.nowIso();
101
- const changeSetId = deps.idGen.newId();
102
- // BR-04: the event rides into insert()'s third argument (see this function's doc comment) so a
103
- // durable adapter co-persists it atomically with the record it belongs to. Built unconditionally
104
- // (cheap, pure data) but only passed through when an outbox is actually wired.
105
- const event = {
106
- id: deps.idGen.newId(),
107
- name: "change-set.applied",
108
- occurredAt: now,
109
- aggregateId: changeSetId,
110
- workspaceId: command.workspaceId,
111
- actorId: command.actor.id,
112
- changeSetId,
113
- payload: {
114
- changeSetId,
115
- entityType: mutation.entityType,
116
- entityId: mutation.entityId,
117
- },
118
- };
119
- // Unit of work: the change-set record (and, when an outbox is wired, its delivery event) must
120
- // land together, or the feature mutation is rolled back (REQ-01 / EC-08). On the in-memory
121
- // adapter this is a compensating restore via `mutation.rollback`; the SQLite adapter does this
122
- // as one real transaction and the rollback becomes a no-op for this step.
105
+ let insertingRecord = false;
123
106
  try {
124
- await deps.changeSets.insert({
125
- id: changeSetId,
107
+ const entityVersionAtApply = mutation.captureEntityVersion?.({ result }) ?? undefined;
108
+ const now = kernelNowIso({ clock: deps.clock });
109
+ const changeSetId = deps.idGen.newId();
110
+ // the event rides into insert's optional object (see this function's doc comment) so a. See docs/decisions/DR-006-mutation-audit-atomicity.md.
111
+ // durable adapter co-persists it atomically with the record it belongs to. Built unconditionally
112
+ // (cheap, pure data) but only passed through when an outbox is actually wired.
113
+ const event = {
114
+ id: deps.idGen.newId(),
115
+ name: "change-set.applied",
116
+ occurredAt: now,
117
+ aggregateId: changeSetId,
126
118
  workspaceId: command.workspaceId,
127
119
  actorId: command.actor.id,
128
- status: "applied",
129
- summary: command.summary,
130
- idempotencyKey: command.idempotencyKey,
131
- intentRef: command.intentRef,
132
- createdAt: now,
133
- appliedAt: now,
134
- }, [
135
- {
136
- id: deps.idGen.newId(),
120
+ changeSetId,
121
+ payload: {
137
122
  changeSetId,
138
123
  entityType: mutation.entityType,
139
124
  entityId: mutation.entityId,
140
- operation: mutation.operation,
141
- inversePayload: inversePayload ?? undefined,
142
- entityVersionAtApply,
143
- position: 0,
144
125
  },
145
- ], deps.outbox ? event : undefined);
126
+ };
127
+ // Unit of work: the change-set record (and, when an outbox is wired, its delivery event) must
128
+ // land together, or the feature mutation is rolled back. On the in-memory. See docs/decisions/DR-006-mutation-audit-atomicity.md.
129
+ // adapter this is a compensating restore via `mutation.rollback`; a caller-owned transaction
130
+ // can instead cover the feature mutation and record together (see the rollback contract).
131
+ const itemId = deps.idGen.newId();
132
+ insertingRecord = true;
133
+ await deps.changeSets.insert({ record: {
134
+ id: changeSetId,
135
+ workspaceId: command.workspaceId,
136
+ actorId: command.actor.id,
137
+ status: "applied",
138
+ summary: command.summary,
139
+ idempotencyKey: command.idempotencyKey,
140
+ intentRef: command.intentRef,
141
+ createdAt: now,
142
+ appliedAt: now,
143
+ },
144
+ items: [
145
+ {
146
+ id: itemId,
147
+ changeSetId,
148
+ entityType: mutation.entityType,
149
+ entityId: mutation.entityId,
150
+ operation: mutation.operation,
151
+ inversePayload: inversePayload ?? undefined,
152
+ entityVersionAtApply,
153
+ position: 0,
154
+ },
155
+ ] }, { event: deps.outbox ? event : undefined });
156
+ return { result, changeSetId };
146
157
  }
147
158
  catch (recordError) {
148
- // AC-17 / EC-08: the mutation applied but its record did not. Undo the
149
- // mutation so INV-01 holds (no mutation without a record). We surface the
150
- // original persist error; a rollback that itself throws is a harder failure
159
+ // the mutation applied but its record did not. Undo the. See docs/decisions/DR-006-mutation-audit-atomicity.md.
160
+ // mutation so holds when rollback is supplied (no mutation without a record). See docs/decisions/DR-006-mutation-audit-atomicity.md.
161
+ // We surface the original preparation/persist error; a rollback that itself throws is a harder failure
151
162
  // that the SQLite transaction path is designed to remove.
152
163
  await mutation.rollback?.();
153
164
  // TOCTOU: the idempotency check above and this insert are separated by
@@ -157,17 +168,16 @@ export async function executeCommand(required, _optional = {}) {
157
168
  // change set) rather than letting the raw driver error escape, so callers' existing
158
169
  // `instanceof DuplicateCommandError` handling (409) catches this race the same as the
159
170
  // already-covered non-racing replay.
160
- if (command.idempotencyKey && isIdempotencyKeyConflict(recordError)) {
171
+ if (insertingRecord && command.idempotencyKey && isIdempotencyKeyConflict(recordError)) {
161
172
  const winner = await deps.changeSets.findByIdempotencyKey({
162
173
  workspaceId: command.workspaceId,
163
174
  idempotencyKey: command.idempotencyKey,
164
175
  });
165
176
  if (winner) {
166
- throw new DuplicateCommandError(`command with idempotency key '${command.idempotencyKey}' was already executed`, winner.id);
177
+ throw new DuplicateCommandError({ message: `command with idempotency key '${command.idempotencyKey}' was already executed`, changeSetId: winner.id });
167
178
  }
168
179
  }
169
180
  throw recordError;
170
181
  }
171
- return { result, changeSetId };
172
182
  }
173
183
  //# sourceMappingURL=command.js.map
@@ -1,6 +1,6 @@
1
1
  import { ToolInputError } from "@jini-ai/core";
2
2
  /**
3
- * @file The write-side counterpart to `not-trashed.ts`'s read-side `notTrashed()` filter (Tovu
3
+ * @file The write-side counterpart to `not-trashed.ts`'s read-side `notTrashed()` filter (host
4
4
  * `features/trash/not-trashed.ts`).
5
5
  *
6
6
  * Purpose:
@@ -11,13 +11,13 @@ import { ToolInputError } from "@jini-ai/core";
11
11
  * This module is the one shared check every writer calls right after its own load, so the check
12
12
  * exists exactly once.
13
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
14
+ * Why it lives in `@jini-ai/cms/core` rather than in a host module or a Jini domain package:
15
+ * a host's writers (pages, SEO, redirects) and Jini's own domain writers (media, content-types,
16
+ * taxonomy) both need it, Jini cannot import anything a host owns, and duplicating the check per side
17
17
  * reintroduces the drift this module exists to remove. `core` is the one place both reach.
18
18
  *
19
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
20
+ * executor already treats `ToolInputError` as caller-facing (see the kernel registration kit's doc
21
21
  * comment), so every domain's agent tool surfaces this error's message to the caller with no
22
22
  * per-domain allowlist entry, and every HTTP route that already maps `ToolInputError`-family errors
23
23
  * picks it up for free. A per-domain rule added to `contracts/core/model-facing-tool-errors.ts`
@@ -36,11 +36,15 @@ export type EntityLiveness = "live" | "trashed" | "tombstoned";
36
36
  * the instance so a caller that wants to branch on them (rather than parse the message) can.
37
37
  */
38
38
  export declare class EntityNotLiveError extends ToolInputError {
39
+ readonly code: "ENTITY_IN_TRASH" | "ENTITY_TOMBSTONED";
39
40
  readonly entityType: string;
40
41
  readonly entityId: string;
41
42
  readonly state: "trashed" | "tombstoned";
42
- readonly code: "ENTITY_IN_TRASH" | "ENTITY_TOMBSTONED";
43
- constructor(entityType: string, entityId: string, state: "trashed" | "tombstoned");
43
+ constructor(requiredArgs: {
44
+ entityType: string;
45
+ entityId: string;
46
+ state: "trashed" | "tombstoned";
47
+ }, _optionalArgs?: Record<string, never>);
44
48
  }
45
49
  /**
46
50
  * Throws {@link EntityNotLiveError} unless `required.state` is `"live"`. Call this immediately
@@ -5,12 +5,13 @@ import { ToolInputError } from "@jini-ai/core";
5
5
  * the instance so a caller that wants to branch on them (rather than parse the message) can.
6
6
  */
7
7
  export class EntityNotLiveError extends ToolInputError {
8
+ code;
8
9
  entityType;
9
10
  entityId;
10
11
  state;
11
- code;
12
- constructor(entityType, entityId, state) {
13
- super(buildEntityNotLiveMessage(entityType, entityId, state));
12
+ constructor(requiredArgs, _optionalArgs = {}) {
13
+ const { entityType, entityId, state } = requiredArgs;
14
+ super({ message: buildEntityNotLiveMessage(entityType, entityId, state) });
14
15
  this.entityType = entityType;
15
16
  this.entityId = entityId;
16
17
  this.state = state;
@@ -33,6 +34,6 @@ function buildEntityNotLiveMessage(entityType, entityId, state) {
33
34
  export function assertEntityLive(required) {
34
35
  if (required.state === "live")
35
36
  return;
36
- throw new EntityNotLiveError(required.entityType, required.entityId, required.state);
37
+ throw new EntityNotLiveError({ entityType: required.entityType, entityId: required.entityId, state: required.state });
37
38
  }
38
39
  //# sourceMappingURL=entity-liveness.js.map
@@ -8,10 +8,16 @@
8
8
  * kit every domain's agent-tool layer builds on. No Express, no `node:*`, no DOM, no concrete
9
9
  * adapters.
10
10
  *
11
- * ## What belongs in `../server`
11
+ * ## What belongs in Node-bound domain entries
12
12
  *
13
13
  * Only the Node-bound half: concrete repository implementations (SQLite/Postgres), filesystem blob
14
- * stores, and anything importing `node:*`.
14
+ * stores, image transformers, and anything importing `node:*` or a native dependency. These
15
+ * adapters depend on core; the reverse is a boundary violation. User-management tool registration
16
+ * composition belongs to its own server entry point.
17
+ *
18
+ * HTTP routing and request/response handling belong in domain HTTP entries, composed through
19
+ * `@jini-ai/http-kit` and `@jini-ai/server`. The source runtime's Express composition root stays
20
+ * out of this kernel: putting its routes here would reproduce the coupling described below.
15
21
  *
16
22
  * ## Why the split is drawn here, and why it is enforced by `exports` rather than convention
17
23
  *
@@ -26,7 +32,7 @@
26
32
  * A package's `exports` map is exactly the enforcement a single `src/` tree cannot provide: a
27
33
  * consumer of `@jini-ai/cms/core` physically cannot reach a Node adapter, and `/core` physically
28
34
  * cannot import a transport type, because the module resolver refuses it. Keep it that way — if a
29
- * core module wants something from `/server`, the dependency is pointing the wrong way and the fix
35
+ * core module wants something from a Node-bound adapter, the dependency points the wrong way and the fix
30
36
  * is a port (interface) in core, not a widened export.
31
37
  *
32
38
  * ## Why the barrel does not re-export `./commands/appliers`
@@ -38,9 +44,10 @@
38
44
  * measured reason no domain could be extracted on its own. A per-entity applier registry belongs
39
45
  * behind a registration call the host makes, not behind a kernel barrel.
40
46
  */
41
- export type { UUID, ISODateTime, JsonPrimitive, JsonValue, JsonObject, JsonArray, DomainEvent, EventBusPort, OutboxRecord, OutboxPort, ClockPort, IdGeneratorPort, } from './ports.js';
47
+ export type { DomainEvent, EventBusPort, OutboxRecord, OutboxPort, } from './ports.js';
42
48
  export type { ChangeSetStatus, ChangeSetOperation, ChangeSetRecord, ChangeSetItemRecord, ChangeSetWithItems, ChangeSetRepoPort, } from './commands/change-set.js';
43
49
  export * from './commands/command.js';
44
50
  export * from './entity-liveness.js';
45
- export * from './tools/registration-kit.js';
51
+ export * from './tools/args.js';
52
+ export type { AuthorizationPort, AuthorizationRequired, AuthorizationOptional } from './authorization.js';
46
53
  //# sourceMappingURL=index.d.ts.map
@@ -8,10 +8,16 @@
8
8
  * kit every domain's agent-tool layer builds on. No Express, no `node:*`, no DOM, no concrete
9
9
  * adapters.
10
10
  *
11
- * ## What belongs in `../server`
11
+ * ## What belongs in Node-bound domain entries
12
12
  *
13
13
  * Only the Node-bound half: concrete repository implementations (SQLite/Postgres), filesystem blob
14
- * stores, and anything importing `node:*`.
14
+ * stores, image transformers, and anything importing `node:*` or a native dependency. These
15
+ * adapters depend on core; the reverse is a boundary violation. User-management tool registration
16
+ * composition belongs to its own server entry point.
17
+ *
18
+ * HTTP routing and request/response handling belong in domain HTTP entries, composed through
19
+ * `@jini-ai/http-kit` and `@jini-ai/server`. The source runtime's Express composition root stays
20
+ * out of this kernel: putting its routes here would reproduce the coupling described below.
15
21
  *
16
22
  * ## Why the split is drawn here, and why it is enforced by `exports` rather than convention
17
23
  *
@@ -26,7 +32,7 @@
26
32
  * A package's `exports` map is exactly the enforcement a single `src/` tree cannot provide: a
27
33
  * consumer of `@jini-ai/cms/core` physically cannot reach a Node adapter, and `/core` physically
28
34
  * cannot import a transport type, because the module resolver refuses it. Keep it that way — if a
29
- * core module wants something from `/server`, the dependency is pointing the wrong way and the fix
35
+ * core module wants something from a Node-bound adapter, the dependency points the wrong way and the fix
30
36
  * is a port (interface) in core, not a widened export.
31
37
  *
32
38
  * ## Why the barrel does not re-export `./commands/appliers`
@@ -40,5 +46,5 @@
40
46
  */
41
47
  export * from './commands/command.js';
42
48
  export * from './entity-liveness.js';
43
- export * from './tools/registration-kit.js';
49
+ export * from './tools/args.js';
44
50
  //# sourceMappingURL=index.js.map
@@ -21,14 +21,7 @@
21
21
  * Core/domain code depends on interfaces from this file only, never on concrete
22
22
  * infrastructure SDKs or framework-specific implementations.
23
23
  */
24
- export type UUID = string;
25
- export type ISODateTime = string;
26
- export type JsonPrimitive = string | number | boolean | null;
27
- export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
28
- export interface JsonObject {
29
- [key: string]: JsonValue;
30
- }
31
- export type JsonArray = JsonValue[];
24
+ import type { UUID, ISODateTime } from "@jini-ai/core/primitives";
32
25
  /**
33
26
  * Immutable domain event emitted by synchronous command handlers and delivered
34
27
  * asynchronously via the outbox pipeline.
@@ -73,19 +66,26 @@ export interface EventBusPort {
73
66
  /** Publish a single event to subscribers. */
74
67
  publish<TPayload>(event: DomainEvent<TPayload>): Promise<void>;
75
68
  /** Publish multiple events in order. */
76
- publishBatch<TPayload>(events: Array<DomainEvent<TPayload>>): Promise<void>;
69
+ publishBatch<TPayload>(required: {
70
+ events: Array<DomainEvent<TPayload>>;
71
+ }, optional?: Record<string, never>): Promise<void>;
77
72
  /**
78
73
  * Subscribe a handler to an event name.
79
74
  * Returns an async unsubscriber function.
80
75
  */
81
- subscribe<TPayload>(eventName: string, handler: (event: DomainEvent<TPayload>) => Promise<void>): Promise<() => Promise<void>>;
76
+ subscribe<TPayload>(required: {
77
+ eventName: string;
78
+ handler: (event: DomainEvent<TPayload>) => Promise<void>;
79
+ }, optional?: Record<string, never>): Promise<() => Promise<void>>;
82
80
  /**
83
81
  * Subscribe a handler to every event published on this bus, regardless of name. Additive to
84
82
  * `subscribe` — existing per-name callers are unaffected.
85
83
  * The Integrations fan-out (`registerWebhookFanout`) is the first real consumer: it narrows
86
84
  * by topic itself rather than requiring a fixed list of event names up front.
87
85
  */
88
- subscribeAll(handler: (event: DomainEvent) => Promise<void>): Promise<() => Promise<void>>;
86
+ subscribeAll(required: {
87
+ handler: (event: DomainEvent) => Promise<void>;
88
+ }, optional?: Record<string, never>): Promise<() => Promise<void>>;
89
89
  }
90
90
  /**
91
91
  * Persisted outbox row used for reliable async event delivery.
@@ -120,9 +120,14 @@ export interface OutboxPort {
120
120
  /** Append an event for later delivery. */
121
121
  enqueue(event: DomainEvent): Promise<void>;
122
122
  /** Claim pending rows for processing. */
123
- claimPending(batchSize: number, nowIso: ISODateTime): Promise<OutboxRecord[]>;
123
+ claimPending(required: {
124
+ batchSize: number;
125
+ nowIso: ISODateTime;
126
+ }, optional?: Record<string, never>): Promise<OutboxRecord[]>;
124
127
  /** Mark a row as delivered. */
125
- markDelivered(id: UUID): Promise<void>;
128
+ markDelivered(required: {
129
+ id: UUID;
130
+ }, optional?: Record<string, never>): Promise<void>;
126
131
  /**
127
132
  * Mark a row as failed and set its next retry time.
128
133
  *
@@ -134,16 +139,12 @@ export interface OutboxPort {
134
139
  * is terminal — `claimPending` only ever selects `"pending"` rows, so a `"failed"` row is
135
140
  * permanently excluded from retry regardless of `nextAttemptAt`.
136
141
  */
137
- markFailed(id: UUID, error: string, nextAttemptAt: ISODateTime, nextStatus: Extract<OutboxRecord["status"], "pending" | "failed">): Promise<void>;
138
- }
139
- /** Clock abstraction to make time deterministic in tests. */
140
- export interface ClockPort {
141
- /** Current wall-clock time as ISO string. */
142
- nowIso(): ISODateTime;
143
- }
144
- /** ID generator abstraction to keep ID strategy swappable and testable. */
145
- export interface IdGeneratorPort {
146
- /** Create a new globally unique ID. */
147
- newId(): UUID;
142
+ markFailed(required: {
143
+ id: UUID;
144
+ error: string;
145
+ nextAttemptAt: ISODateTime;
146
+ nextStatus: Extract<OutboxRecord["status"], "pending" | "failed">;
147
+ }, optional?: Record<string, never>): Promise<void>;
148
148
  }
149
+ /** Clock and ID generator contracts are owned by the shared kernel primitives. */
149
150
  //# sourceMappingURL=ports.d.ts.map
@@ -1,2 +1,3 @@
1
1
  export {};
2
+ /** Clock and ID generator contracts are owned by the shared kernel primitives. */
2
3
  //# sourceMappingURL=ports.js.map
@@ -0,0 +1,49 @@
1
+ /** CMS permission checking preserves the CMS command denial contract. Generic registration wiring lives in the kernel. */
2
+ import { type AuthorizeFn } from "../commands/command.js";
3
+ import type { AuthorizationPort } from "../authorization.js";
4
+ export type { AuthorizationPort, AuthorizationRequired, AuthorizationOptional } from "../authorization.js";
5
+ export interface RequireToolPermissionRequired {
6
+ authorize: AuthorizationPort;
7
+ workspaceId: string;
8
+ principalId: string;
9
+ permission: string;
10
+ }
11
+ export interface RequireToolPermissionOptional {
12
+ entityType?: string | undefined;
13
+ entityId?: string | undefined;
14
+ }
15
+ /**
16
+ * Runs the identical inline `authorize()` check the corresponding admin HTTP route performs, for a
17
+ * tool whose own domain function carries no `authorize()` call to inherit.
18
+ *
19
+ * This is NOT a second evaluator alongside another check — that is exactly the single-evaluator
20
+ * rule's concern. For each tool that
21
+ * calls it, this IS the only gate that tool's execution ever reaches, reached by an identical path
22
+ * to the one a human clicking the same admin route reaches. Domains split cleanly on which kind
23
+ * they are: content-types/Forms/Identity mutations and every Widgets write-service call self-enforce
24
+ * inside the domain function and must NOT call this; Comments/Members/Newsletter/Media/Menus/
25
+ * Database/Recovery gate in the route instead, so their handlers call this in the route's place.
26
+ * Which kind a given tool is stays documented next to that tool's handler, not here.
27
+ *
28
+ * Throws `core/commands`'s `ForbiddenError` — the same class `executeCommand` itself throws — so a
29
+ * tool caller and a route caller see the identical error shape for an identical denial. Throw
30
+ * rather than a 403 body because the caller here is `ToolExecutor`, which reads a thrown error as a
31
+ * failed execution and has no response to write to.
32
+ *
33
+ * Import the defining command module directly: its barrel also exports mutation appliers that
34
+ * name content features. Pulling that barrel into shared permission wiring would couple every
35
+ * tool domain to those features; the direct import preserves the identical denial class without
36
+ * that dependency closure.
37
+ *
38
+ * @param required - The `authorize()` evaluator and the workspace the run is scoped to.
39
+ * @param optional - Entity scope for who is asking, for what permission, optionally against which entity.
40
+ * @throws {ForbiddenError} If `authorize()` denies.
41
+ * @complexity O(1) beyond the injected `authorize()` call.
42
+ * @overallScore 100
43
+ */
44
+ export declare function requireToolPermission(required: RequireToolPermissionRequired, optional?: RequireToolPermissionOptional): Promise<void>;
45
+ /** Adapt an existing evaluator without losing entity scope. @complexity O(1) plus evaluation. */
46
+ export declare function adaptLegacyAuthorize(required: {
47
+ authorize: AuthorizeFn;
48
+ }, _optional?: Record<string, never>): AuthorizationPort;
49
+ //# sourceMappingURL=args.d.ts.map
@@ -0,0 +1,46 @@
1
+ /** CMS permission checking preserves the CMS command denial contract. Generic registration wiring lives in the kernel. */
2
+ import { ForbiddenError } from "../commands/command.js";
3
+ /**
4
+ * Runs the identical inline `authorize()` check the corresponding admin HTTP route performs, for a
5
+ * tool whose own domain function carries no `authorize()` call to inherit.
6
+ *
7
+ * This is NOT a second evaluator alongside another check — that is exactly the single-evaluator
8
+ * rule's concern. For each tool that
9
+ * calls it, this IS the only gate that tool's execution ever reaches, reached by an identical path
10
+ * to the one a human clicking the same admin route reaches. Domains split cleanly on which kind
11
+ * they are: content-types/Forms/Identity mutations and every Widgets write-service call self-enforce
12
+ * inside the domain function and must NOT call this; Comments/Members/Newsletter/Media/Menus/
13
+ * Database/Recovery gate in the route instead, so their handlers call this in the route's place.
14
+ * Which kind a given tool is stays documented next to that tool's handler, not here.
15
+ *
16
+ * Throws `core/commands`'s `ForbiddenError` — the same class `executeCommand` itself throws — so a
17
+ * tool caller and a route caller see the identical error shape for an identical denial. Throw
18
+ * rather than a 403 body because the caller here is `ToolExecutor`, which reads a thrown error as a
19
+ * failed execution and has no response to write to.
20
+ *
21
+ * Import the defining command module directly: its barrel also exports mutation appliers that
22
+ * name content features. Pulling that barrel into shared permission wiring would couple every
23
+ * tool domain to those features; the direct import preserves the identical denial class without
24
+ * that dependency closure.
25
+ *
26
+ * @param required - The `authorize()` evaluator and the workspace the run is scoped to.
27
+ * @param optional - Entity scope for who is asking, for what permission, optionally against which entity.
28
+ * @throws {ForbiddenError} If `authorize()` denies.
29
+ * @complexity O(1) beyond the injected `authorize()` call.
30
+ * @overallScore 100
31
+ */
32
+ export async function requireToolPermission(required, optional = {}) {
33
+ const result = await required.authorize({
34
+ principalId: required.principalId, permission: required.permission, workspaceId: required.workspaceId,
35
+ }, optional);
36
+ if (!result.allowed)
37
+ throw new ForbiddenError({
38
+ message: `principal '${required.principalId}' is not authorized for '${required.permission}' (${result.reason})`,
39
+ permission: required.permission, reason: result.reason,
40
+ });
41
+ }
42
+ /** Adapt an existing evaluator without losing entity scope. @complexity O(1) plus evaluation. */
43
+ export function adaptLegacyAuthorize(required, _optional = {}) {
44
+ return (request, optional = {}) => required.authorize({ ...request, ...optional });
45
+ }
46
+ //# sourceMappingURL=args.js.map
@@ -0,0 +1,3 @@
1
+ /** CMS authorization helpers; generic tool-registration wiring is exported by the kernel. */
2
+ export * from './args.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ /** CMS authorization helpers; generic tool-registration wiring is exported by the kernel. */
2
+ export * from './args.js';
3
+ //# sourceMappingURL=index.js.map
@@ -1,76 +1,4 @@
1
- /**
2
- * @file The Entries agent-tool catalog — this domain's instance of the per-domain `agent-tools.ts`
3
- * convention `features/content-types/agent-tools.ts`, `features/settings/agent-tools.ts`, and every
4
- * other domain catalog already use.
5
- *
6
- * Purpose:
7
- * A static, in-process catalog describing every agent-callable tool this domain exposes. Every
8
- * entry maps 1:1 onto a real exported function of `features/entries/write-service.ts` (`createEntry`,
9
- * `updateEntry`, `publishEntry`, `unpublishEntry`) or `features/entries/list.ts` (`listEntries`) —
10
- * this catalog never names an operation the domain cannot perform.
11
- *
12
- * Naming and scope — a real discrepancy from this dispatch's own brief, disclosed rather than
13
- * silently reconciled: the brief framed this domain as "backing the Pages, Posts, and Collections
14
- * admin sections", but reading the actual admin routes shows that is only true for Collections.
15
- * `server/routes/admin/entries/{create,update,lifecycle,list}.ts` (`/api/admin/v1/entries`, gated by
16
- * `admin.collections.manage`/`admin.collections.read`) are the ONLY routes backed by THIS package's
17
- * `write-service.ts`/`list.ts` — exactly what this catalog wraps. Pages and Posts
18
- * (`server/routes/admin/pages/*`, `server/routes/admin/posts/*`) are, in this codebase's CURRENT
19
- * state, backed by a wholly separate legacy `features/post` module (`createPost`/`updatePost`/
20
- * `getAdminPostById`/`listAdminPages`, its own `post` table, routed through `core/commands`'s
21
- * `executeCommand` gateway, gated by `content.write`/`content.read` — a different permission
22
- * namespace entirely). `features/entries` has no relationship to that module at all: no shared
23
- * table, no shared write chokepoint, no shared permission. So this catalog's tools name their
24
- * resource `collections_entry_*` (pairing with `features/content-types/agent-tools.ts`'s own
25
- * `collections_content_type_*` prefix — the two packages are "one cohesive domain" per
26
- * `server/routes/admin/content-types/deps.ts`'s own file header), not `entry_*`/`page_*`/`post_*` —
27
- * a name that could be misread as covering Pages/Posts. Wiring Pages/Posts agent tools would mean
28
- * wrapping `features/post`, a different backend this dispatch was not asked to touch and has not
29
- * inspected for the same safety discipline this catalog applies here.
30
- *
31
- * Deliberate absences (the point of a catalog, not an oversight):
32
- * - There is no delete/purge tool: `write-service.ts` exposes no delete function at all (confirmed
33
- * by reading the whole file) — there is nothing to wrap or exclude on that front.
34
- * - `collections_entry_update`'s schema omits `bodyJson` even though `updateEntry` itself accepts an
35
- * optional `bodyJson` (REQ-44/45, used internally by `widgets/embed-service.ts`'s own guardrailed
36
- * mutation path). The generic Collections admin route (`entries/update.ts`) never forwards
37
- * `body.bodyJson` at all — only `title`/`fieldsJson`/`expectedVersion` — so exposing it here would
38
- * invent capability beyond what the human admin UI's own Collections editor exposes. Omitting it
39
- * from the input is enough: `updateEntry` leaves `bodyJson` unchanged whenever the field is
40
- * `undefined`, so this exclusion changes nothing about what a call can still do.
41
- * - `collections_entry_list`'s schema omits `status`/`orderBy`/`limit` even though
42
- * `EntryListPort.listByWorkspace` supports all three (added for `widgets/resolvers/recent-entries.ts`'s
43
- * own bounded-query need) — `server/routes/admin/entries/list.ts` only ever forwards `type` from
44
- * its query string, so the other three are not part of what the Collections admin list screen
45
- * itself can do; adding them here would again exceed the human surface this catalog mirrors.
46
- *
47
- * What IS included, and why it is safe: all four write-service functions are `admin.collections.*`
48
- * self-enforcing (each opens with its own `deps.authorize()` call before any other side effect —
49
- * confirmed by reading `write-service.ts` directly), and `publishEntry`/`unpublishEntry` are ordinary,
50
- * fully reversible status flips with a full revision trail (`entryRepo.appendRevision`) — publishing
51
- * and unpublishing are each other's own undo.
52
- *
53
- * How it relates to the project:
54
- * `features/entries/tool-registrations.ts` maps these entries into `@jini-ai/core` `ToolRegistration`s.
55
- *
56
- * Architectural role:
57
- * `features/entries` domain declaration. No dependencies.
58
- */
59
- export type AgentToolSideEffect = "none" | "mutates-durable-state" | "mints-token";
60
- export interface AgentToolDefinition {
61
- name: string;
62
- description: string;
63
- sideEffects: AgentToolSideEffect;
64
- authorization: {
65
- permission: string;
66
- };
67
- /**
68
- * JSON Schema for this tool's `input`, published to the model via `ToolDescriptor.inputSchema`
69
- * (`assistant/tool-registration-kit.ts`'s `buildDomainRegistrations`, which refuses to wire any
70
- * tool lacking one).
71
- */
72
- inputSchema?: Readonly<Record<string, unknown>>;
73
- }
1
+ import type { AgentToolDefinition } from "@jini-ai/core";
74
2
  /** The Entries domain's fixed agent-tool catalog: the Collections authoring surface's 4 writes
75
3
  * (create/update/publish/unpublish) plus its 1 read (list) — see this file's header for the
76
4
  * Pages/Posts naming disclosure and the 3 deliberate scope exclusions. */