@retinue/agentkit 0.1.0 → 0.3.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 (246) hide show
  1. package/README.md +59 -277
  2. package/dist/adapters/audio/openai.d.ts +36 -0
  3. package/dist/adapters/audio/openai.js +136 -0
  4. package/dist/adapters/bullmq/lock.d.ts +26 -2
  5. package/dist/adapters/embeddings/openai.d.ts +45 -0
  6. package/dist/adapters/embeddings/openai.js +109 -0
  7. package/dist/adapters/memory/connections.d.ts +10 -0
  8. package/dist/adapters/memory/connections.js +87 -0
  9. package/dist/adapters/memory/graph.d.ts +17 -0
  10. package/dist/adapters/memory/graph.js +267 -0
  11. package/dist/adapters/memory/index.d.ts +3 -0
  12. package/dist/adapters/memory/index.js +3 -0
  13. package/dist/adapters/memory/rate-limit.d.ts +14 -0
  14. package/dist/adapters/memory/rate-limit.js +28 -0
  15. package/dist/adapters/memory/runtime.js +0 -0
  16. package/dist/adapters/postgres/connections.d.ts +15 -0
  17. package/dist/adapters/postgres/connections.js +134 -0
  18. package/dist/adapters/postgres/graph.d.ts +24 -0
  19. package/dist/adapters/postgres/graph.js +325 -0
  20. package/dist/adapters/postgres/index.d.ts +2 -0
  21. package/dist/adapters/postgres/index.js +2 -0
  22. package/dist/adapters/postgres/migrations.js +198 -0
  23. package/dist/adapters/postgres/run-store.js +26 -2
  24. package/dist/adapters/postgres/schema.d.ts +61 -2
  25. package/dist/adapters/postgres/schema.js +95 -10
  26. package/dist/adapters/redis/index.d.ts +1 -0
  27. package/dist/adapters/redis/index.js +1 -0
  28. package/dist/adapters/redis/rate-limit.d.ts +46 -0
  29. package/dist/adapters/redis/rate-limit.js +55 -0
  30. package/dist/adapters/supabase/index.d.ts +15 -0
  31. package/dist/adapters/supabase/index.js +15 -0
  32. package/dist/adapters/supabase/rls.js +33 -0
  33. package/dist/agents/agent.d.ts +99 -1
  34. package/dist/agents/agent.js +215 -16
  35. package/dist/agents/define.js +24 -4
  36. package/dist/agents/engine.d.ts +48 -0
  37. package/dist/agents/engine.js +370 -13
  38. package/dist/audio/index.d.ts +151 -0
  39. package/dist/audio/index.js +183 -0
  40. package/dist/capabilities/index.d.ts +5 -1
  41. package/dist/capabilities/index.js +23 -0
  42. package/dist/capabilities/runtime.d.ts +8 -0
  43. package/dist/connections/cipher.d.ts +103 -0
  44. package/dist/connections/cipher.js +141 -0
  45. package/dist/connections/index.d.ts +156 -0
  46. package/dist/connections/index.js +72 -0
  47. package/dist/connections/oauth/client.d.ts +84 -0
  48. package/dist/connections/oauth/client.js +141 -0
  49. package/dist/connections/oauth/index.d.ts +166 -0
  50. package/dist/connections/oauth/index.js +226 -0
  51. package/dist/connections/oauth/service.d.ts +90 -0
  52. package/dist/connections/oauth/service.js +132 -0
  53. package/dist/connections/pause.d.ts +68 -0
  54. package/dist/connections/pause.js +97 -0
  55. package/dist/connections/resolver.d.ts +59 -0
  56. package/dist/connections/resolver.js +138 -0
  57. package/dist/connections/resume.d.ts +64 -0
  58. package/dist/connections/resume.js +99 -0
  59. package/dist/connections/vault-cipher.d.ts +95 -0
  60. package/dist/connections/vault-cipher.js +206 -0
  61. package/dist/core/budget.d.ts +55 -0
  62. package/dist/core/budget.js +56 -0
  63. package/dist/core/content-parts.d.ts +26 -2
  64. package/dist/core/content-parts.js +1 -0
  65. package/dist/core/context.d.ts +25 -0
  66. package/dist/core/errors.d.ts +1 -1
  67. package/dist/core/errors.js +15 -0
  68. package/dist/core/events.d.ts +110 -2
  69. package/dist/core/events.js +33 -2
  70. package/dist/core/index.d.ts +1 -0
  71. package/dist/core/index.js +1 -0
  72. package/dist/core/validation.js +19 -0
  73. package/dist/documents/index.d.ts +14 -0
  74. package/dist/documents/parsers/text.d.ts +16 -0
  75. package/dist/documents/parsers/text.js +54 -2
  76. package/dist/entries/connections.d.ts +10 -0
  77. package/dist/entries/connections.js +10 -0
  78. package/dist/entries/guardrails.d.ts +14 -0
  79. package/dist/entries/guardrails.js +14 -0
  80. package/dist/entries/knowledge.d.ts +17 -0
  81. package/dist/entries/knowledge.js +16 -0
  82. package/dist/entries/mcp-server.d.ts +9 -0
  83. package/dist/entries/mcp-server.js +9 -0
  84. package/dist/entries/runtime.d.ts +10 -0
  85. package/dist/entries/runtime.js +8 -0
  86. package/dist/entries/testing.d.ts +28 -0
  87. package/dist/entries/testing.js +28 -0
  88. package/dist/graphql/resolvers.d.ts +7 -1
  89. package/dist/graphql/resolvers.js +20 -0
  90. package/dist/graphql/schema.d.ts +1 -1
  91. package/dist/graphql/schema.js +44 -0
  92. package/dist/guardrails/index.d.ts +115 -0
  93. package/dist/guardrails/index.js +108 -0
  94. package/dist/guardrails/moderation.d.ts +53 -0
  95. package/dist/guardrails/moderation.js +75 -0
  96. package/dist/guardrails/pii.d.ts +75 -0
  97. package/dist/guardrails/pii.js +193 -0
  98. package/dist/knowledge/communities.d.ts +166 -0
  99. package/dist/knowledge/communities.js +377 -0
  100. package/dist/knowledge/graph-global.d.ts +153 -0
  101. package/dist/knowledge/graph-global.js +246 -0
  102. package/dist/knowledge/graph-retrieval.d.ts +150 -0
  103. package/dist/knowledge/graph-retrieval.js +303 -0
  104. package/dist/knowledge/graph.d.ts +223 -0
  105. package/dist/knowledge/graph.js +369 -0
  106. package/dist/knowledge/index.d.ts +25 -0
  107. package/dist/knowledge/index.js +37 -1
  108. package/dist/knowledge/navigate.d.ts +89 -0
  109. package/dist/knowledge/navigate.js +107 -0
  110. package/dist/knowledge/retrieval.d.ts +98 -5
  111. package/dist/knowledge/retrieval.js +158 -28
  112. package/dist/mcp-server/index.d.ts +168 -0
  113. package/dist/mcp-server/index.js +175 -0
  114. package/dist/models/extraction.d.ts +73 -0
  115. package/dist/models/extraction.js +118 -0
  116. package/dist/models/index.d.ts +40 -1
  117. package/dist/models/index.js +22 -1
  118. package/dist/models/pricing.d.ts +2 -0
  119. package/dist/models/pricing.js +17 -1
  120. package/dist/models/provider-factory.js +9 -7
  121. package/dist/models/streaming.d.ts +111 -1
  122. package/dist/models/streaming.js +184 -3
  123. package/dist/persistence/index.d.ts +230 -0
  124. package/dist/persistence/index.js +2 -0
  125. package/dist/runtime/index.d.ts +1 -1
  126. package/dist/runtime/index.js +12 -0
  127. package/dist/runtime/worker.d.ts +36 -1
  128. package/dist/runtime/worker.js +37 -4
  129. package/dist/security/checklist.js +55 -0
  130. package/dist/security/findings.js +18 -9
  131. package/dist/server/bin.d.ts +19 -0
  132. package/dist/server/bin.js +226 -0
  133. package/dist/server/boot.d.ts +20 -2
  134. package/dist/server/boot.js +29 -3
  135. package/dist/server/cli.d.ts +13 -0
  136. package/dist/server/cli.js +7 -4
  137. package/dist/server/doctor.d.ts +74 -0
  138. package/dist/server/doctor.js +280 -0
  139. package/dist/skills/catalogue.d.ts +49 -0
  140. package/dist/skills/catalogue.js +61 -0
  141. package/dist/skills/context.d.ts +61 -0
  142. package/dist/skills/context.js +107 -0
  143. package/dist/skills/index.d.ts +2 -0
  144. package/dist/skills/index.js +2 -0
  145. package/dist/telemetry/spans.js +17 -0
  146. package/dist/testing/conformance/artifact-exports.d.ts +20 -0
  147. package/dist/testing/conformance/artifact-exports.js +172 -0
  148. package/dist/testing/conformance/artifacts.d.ts +11 -0
  149. package/dist/testing/conformance/artifacts.js +316 -0
  150. package/dist/testing/conformance/audio.d.ts +38 -0
  151. package/dist/testing/conformance/audio.js +145 -0
  152. package/dist/testing/conformance/capability.d.ts +23 -0
  153. package/dist/testing/conformance/capability.js +27 -0
  154. package/dist/testing/conformance/checkpoint-store.d.ts +15 -0
  155. package/dist/testing/conformance/checkpoint-store.js +72 -0
  156. package/dist/testing/conformance/connections.d.ts +14 -0
  157. package/dist/testing/conformance/connections.js +171 -0
  158. package/dist/testing/conformance/conversation-store.d.ts +10 -0
  159. package/dist/testing/conformance/conversation-store.js +60 -0
  160. package/dist/testing/conformance/evaluation.d.ts +10 -0
  161. package/dist/testing/conformance/evaluation.js +295 -0
  162. package/dist/testing/conformance/files.d.ts +19 -0
  163. package/dist/testing/conformance/files.js +454 -0
  164. package/dist/testing/conformance/flows.d.ts +16 -0
  165. package/dist/testing/conformance/flows.js +193 -0
  166. package/dist/testing/conformance/graph.d.ts +22 -0
  167. package/dist/testing/conformance/graph.js +500 -0
  168. package/dist/testing/conformance/hitl.d.ts +25 -0
  169. package/dist/testing/conformance/hitl.js +523 -0
  170. package/dist/testing/conformance/index.d.ts +163 -0
  171. package/dist/testing/conformance/index.js +321 -0
  172. package/dist/testing/conformance/invariants.d.ts +23 -0
  173. package/dist/testing/conformance/invariants.js +80 -0
  174. package/dist/testing/conformance/knowledge.d.ts +41 -0
  175. package/dist/testing/conformance/knowledge.js +592 -0
  176. package/dist/testing/conformance/parents.d.ts +50 -0
  177. package/dist/testing/conformance/parents.js +39 -0
  178. package/dist/testing/conformance/rate-limit.d.ts +25 -0
  179. package/dist/testing/conformance/rate-limit.js +71 -0
  180. package/dist/testing/conformance/records.d.ts +36 -0
  181. package/dist/testing/conformance/records.js +400 -0
  182. package/dist/testing/conformance/rollups.d.ts +30 -0
  183. package/dist/testing/conformance/rollups.js +425 -0
  184. package/dist/testing/conformance/run-coordinator.d.ts +27 -0
  185. package/dist/testing/conformance/run-coordinator.js +150 -0
  186. package/dist/testing/conformance/run-event-log.d.ts +9 -0
  187. package/dist/testing/conformance/run-event-log.js +138 -0
  188. package/dist/testing/conformance/run-store.d.ts +12 -0
  189. package/dist/testing/conformance/run-store.js +294 -0
  190. package/dist/testing/conformance/session-state.d.ts +38 -0
  191. package/dist/testing/conformance/session-state.js +198 -0
  192. package/dist/testing/conformance/usage-limits.d.ts +10 -0
  193. package/dist/testing/conformance/usage-limits.js +309 -0
  194. package/dist/testing/conformance.d.ts +8 -0
  195. package/dist/testing/conformance.js +8 -0
  196. package/dist/testing/memory-backend.d.ts +39 -0
  197. package/dist/testing/memory-backend.js +41 -0
  198. package/dist/testing/pglite.d.ts +55 -0
  199. package/dist/testing/pglite.js +78 -0
  200. package/dist/testing/stub-model.d.ts +58 -0
  201. package/dist/testing/stub-model.js +71 -0
  202. package/dist/testing/supabase-storage-double.d.ts +35 -0
  203. package/dist/testing/supabase-storage-double.js +101 -0
  204. package/dist/toolkit/files.d.ts +125 -0
  205. package/dist/toolkit/files.js +320 -0
  206. package/dist/toolkit/http.d.ts +27 -0
  207. package/dist/toolkit/http.js +48 -0
  208. package/dist/toolkit/index.d.ts +31 -1
  209. package/dist/toolkit/index.js +26 -1
  210. package/dist/toolkit/mime.d.ts +97 -0
  211. package/dist/toolkit/mime.js +233 -0
  212. package/dist/toolkit/sandbox.d.ts +119 -0
  213. package/dist/toolkit/sandbox.js +239 -0
  214. package/dist/toolkit/ssrf.d.ts +139 -0
  215. package/dist/toolkit/ssrf.js +349 -0
  216. package/dist/toolkit/vendor.d.ts +91 -0
  217. package/dist/toolkit/vendor.js +153 -0
  218. package/dist/toolkit/web.d.ts +13 -0
  219. package/dist/toolkit/web.js +7 -1
  220. package/dist/tools/budget.d.ts +28 -0
  221. package/dist/tools/budget.js +35 -0
  222. package/dist/tools/credentials.d.ts +316 -0
  223. package/dist/tools/credentials.js +369 -0
  224. package/dist/tools/define.d.ts +45 -0
  225. package/dist/tools/define.js +26 -0
  226. package/dist/tools/find.d.ts +109 -0
  227. package/dist/tools/find.js +210 -0
  228. package/dist/tools/index.d.ts +35 -2
  229. package/dist/tools/index.js +11 -0
  230. package/dist/tools/library/audio.d.ts +63 -0
  231. package/dist/tools/library/audio.js +148 -0
  232. package/dist/tools/library/fs.d.ts +24 -0
  233. package/dist/tools/library/fs.js +102 -0
  234. package/dist/tools/library/index.d.ts +37 -2
  235. package/dist/tools/library/index.js +47 -0
  236. package/dist/tools/library/shell.d.ts +45 -0
  237. package/dist/tools/library/shell.js +70 -0
  238. package/dist/tools/meta-tools.js +8 -0
  239. package/dist/tools/registry.d.ts +146 -0
  240. package/dist/tools/registry.js +250 -4
  241. package/dist/usage/index.d.ts +21 -0
  242. package/dist/usage/index.js +1 -0
  243. package/dist/usage/rate-limit.d.ts +122 -0
  244. package/dist/usage/rate-limit.js +131 -0
  245. package/dist/usage/recorder.js +3 -0
  246. package/package.json +33 -2
@@ -7,6 +7,14 @@ export const RUN_STATUSES = [
7
7
  "running",
8
8
  "waiting-for-question",
9
9
  "waiting-for-approval",
10
+ /**
11
+ * The run needs a connection the tenant does not have, and a person must complete a consent — task #264.
12
+ *
13
+ * A third pause beside question and approval, and deliberately the *same shape*: the runtime already stops a
14
+ * run and resumes it twice over, and a bespoke polling loop or a failed run somebody restarts by hand would
15
+ * be a second mechanism for the thing the durable runtime exists to do.
16
+ */
17
+ "waiting-for-connection",
10
18
  "retry-pending",
11
19
  "completed",
12
20
  "failed",
@@ -21,6 +29,7 @@ export const RUN_TRANSITIONS = {
21
29
  running: [
22
30
  "waiting-for-question",
23
31
  "waiting-for-approval",
32
+ "waiting-for-connection",
24
33
  "retry-pending",
25
34
  "completed",
26
35
  "failed",
@@ -28,6 +37,9 @@ export const RUN_TRANSITIONS = {
28
37
  ],
29
38
  "waiting-for-question": ["queued", "cancelled"],
30
39
  "waiting-for-approval": ["queued", "cancelled"],
40
+ // Resumes to `queued` exactly as the other two do: consent completing is an answer, and the run rejoins the
41
+ // queue rather than continuing in whatever process happened to receive the callback.
42
+ "waiting-for-connection": ["queued", "cancelled"],
31
43
  "retry-pending": ["queued", "cancelled"],
32
44
  completed: [],
33
45
  failed: [],
@@ -101,16 +101,51 @@ export type DurableWorkerDeps = {
101
101
  * lost message costs latency rather than a stuck flow. A hook whose delivery was load-bearing would need a
102
102
  * queue, not a callback.
103
103
  */
104
+ /**
105
+ * The tenant's concurrent-run limit, or undefined for unlimited — REQ-058 (#246), task #265.
106
+ *
107
+ * A function rather than a value, for the reason `RateLimitGuardDeps.policyFor` gives: limits are per tenant
108
+ * and change without a redeploy, and a value captured at construction would be the limits of whoever booted
109
+ * the process.
110
+ *
111
+ * **Absent or non-positive means unlimited**, which is the branch that matters on the day this ships. A
112
+ * deployment upgrading into the feature with nothing configured must keep working; the alternative is an
113
+ * outage caused by adding a safety feature, which is how safety features get removed.
114
+ */
115
+ readonly concurrencyFor?: (tenantId: Run["tenantId"]) => Promise<number | undefined> | number | undefined;
116
+ /**
117
+ * How long a deferred run waits before its next attempt. Default one second.
118
+ *
119
+ * Not zero, and not the lease length. Zero spins a worker against the cap, burning capacity on the check
120
+ * itself; a full lease is longer than most runs, so a freed slot would sit idle. A second is short enough
121
+ * that a slot is picked up promptly and long enough that the retry is not the load.
122
+ */
123
+ readonly deferralBackoffMs?: number;
104
124
  readonly onRunSettled?: (input: {
105
125
  readonly context: ExecutionContext;
106
126
  readonly run: Run;
107
127
  readonly outcome: "completed" | "failed" | "cancelled";
108
128
  }) => Promise<void> | void;
109
129
  };
110
- export type ProcessOutcome = "completed" | "failed" | "cancelled" | "skipped" | "lost" | "paused";
130
+ /**
131
+ * `deferred` is distinct from `skipped`, and conflating them would strand work — #265.
132
+ *
133
+ * `skipped` means *somebody else has this, or it is already finished* — there is nothing to do and the job is
134
+ * done with. `deferred` means *this tenant is at its concurrency limit right now*: the run is still queued, it
135
+ * still needs to run, and a caller that acknowledged it the way it acknowledges a `skipped` job would drop it
136
+ * for ever. A `deferred` result must be re-enqueued, and `ProcessResult.retryAfterMs` says when.
137
+ */
138
+ export type ProcessOutcome = "completed" | "failed" | "cancelled" | "skipped" | "lost" | "paused" | "deferred";
111
139
  export type ProcessResult = {
112
140
  readonly run: Run | null;
113
141
  readonly outcome: ProcessOutcome;
142
+ /**
143
+ * How long to wait before trying this run again. Set only for `deferred`.
144
+ *
145
+ * Without it a deferred run is re-enqueued immediately and spins against the cap, burning a worker slot on
146
+ * the check itself — which would make a concurrency limit cost more capacity than it saves.
147
+ */
148
+ readonly retryAfterMs?: number;
114
149
  };
115
150
  export declare const createDurableWorker: (deps: DurableWorkerDeps) => {
116
151
  /** Claim (under an optional lock) and drive a run. Idempotent: a claimed/terminal run is skipped. */
@@ -37,6 +37,7 @@ export const createDurableWorker = (deps) => {
37
37
  const clock = deps.clock ?? (() => new Date(now()).toISOString());
38
38
  const leaseMs = deps.leaseMs ?? 30_000;
39
39
  const keepaliveEveryMs = deps.keepaliveEveryMs ?? Math.max(1, Math.floor(leaseMs / 3));
40
+ const deferralBackoffMs = Math.max(1, deps.deferralBackoffMs ?? 1_000);
40
41
  const channelFor = deps.channelFor ?? ((r) => `conversation:${r.conversationId}`);
41
42
  const { runs, checkpoints, publisher, engine, workerId } = deps;
42
43
  /** Drive one already-claimed run to a terminal state. */
@@ -114,6 +115,7 @@ export const createDurableWorker = (deps) => {
114
115
  inputTokens: event.inputTokens,
115
116
  outputTokens: event.outputTokens,
116
117
  cachedInputTokens: event.cachedInputTokens ?? 0,
118
+ ...(event.cacheWriteTokens === undefined ? {} : { cacheWriteTokens: event.cacheWriteTokens }),
117
119
  ...(event.reasoningTokens === undefined ? {} : { reasoningTokens: event.reasoningTokens }),
118
120
  ...(event.imageCount === undefined ? {} : { imageCount: event.imageCount }),
119
121
  ...(event.audioSeconds === undefined ? {} : { audioSeconds: event.audioSeconds }),
@@ -322,7 +324,13 @@ export const createDurableWorker = (deps) => {
322
324
  pause = "waiting-for-question";
323
325
  else if (body.type === "approval.requested")
324
326
  pause = "waiting-for-approval";
325
- else if (body.type === "question.answered" || body.type === "approval.decided")
327
+ // The third pause — #264. Same shape as the other two: an event the engine emits, a status the worker
328
+ // parks in, and a resume back to `queued`.
329
+ else if (body.type === "connection.requested")
330
+ pause = "waiting-for-connection";
331
+ else if (body.type === "question.answered" ||
332
+ body.type === "approval.decided" ||
333
+ body.type === "connection.completed")
326
334
  pause = null;
327
335
  await heartbeat(); // throttled; runs on every event so a tool-heavy run keeps its lease alive
328
336
  if (cancelRequested)
@@ -382,9 +390,34 @@ export const createDurableWorker = (deps) => {
382
390
  if (!lock)
383
391
  return { run: await runs.findById({ tenantId, id: runId }), outcome: "skipped" };
384
392
  try {
385
- const claimed = await runs.claim({ tenantId, id: runId, workerId, leaseMs, now: clock() });
386
- if (!claimed)
387
- return { run: await runs.findById({ tenantId, id: runId }), outcome: "skipped" };
393
+ const maxConcurrent = await deps.concurrencyFor?.(tenantId);
394
+ const now = clock();
395
+ const claimed = await runs.claim({
396
+ tenantId,
397
+ id: runId,
398
+ workerId,
399
+ leaseMs,
400
+ now,
401
+ ...(maxConcurrent === undefined ? {} : { maxConcurrent }),
402
+ });
403
+ if (!claimed) {
404
+ const run = await runs.findById({ tenantId, id: runId });
405
+ /**
406
+ * Why the claim failed, diagnosed *after* the fact — #265.
407
+ *
408
+ * The decision was already made atomically inside `claim`; this only chooses which outcome to report,
409
+ * so a slightly stale count costs a wrong label rather than a wrong admission. The run being still
410
+ * `queued` is the discriminator: a run somebody else is driving is `running`, and a finished one is
411
+ * terminal, so `queued` after a refused claim means the cap is what stopped it.
412
+ */
413
+ if (maxConcurrent !== undefined && maxConcurrent > 0 && run?.status === "queued") {
414
+ const live = await runs.countLive({ tenantId, now });
415
+ if (live >= maxConcurrent) {
416
+ return { run, outcome: "deferred", retryAfterMs: deferralBackoffMs };
417
+ }
418
+ }
419
+ return { run, outcome: "skipped" };
420
+ }
388
421
  return await drive(claimed);
389
422
  }
390
423
  finally {
@@ -190,6 +190,52 @@ export const SECURITY_CHECKS = [
190
190
  * forgotten case.
191
191
  */
192
192
  export const CREDENTIAL_FIELD_EXEMPTIONS = [
193
+ {
194
+ file: "connections/oauth/client.ts",
195
+ reason: "`ResolvedOAuthClient.clientSecret` and `registerTenantOAuthApp`'s parameter are a *tenant's own* OAuth " +
196
+ "client secret, which is exactly as much a secret as the deployment's above and is treated more " +
197
+ "carefully, not less: it is sealed by the same `SecretCipher` as every access token before it reaches " +
198
+ "the database, and the stored row holds ciphertext — asserted by the same raw-row test that covers " +
199
+ "connections. It exists in memory only between being opened for one flow and being handed to the token " +
200
+ "request, is never written to a table in plaintext, never placed in a message part, a result envelope or " +
201
+ "an audit row, and never put in an authorization URL. The readable half of a registration — client id, " +
202
+ "redirect URIs, scopes — is deliberately *not* sealed, so a settings screen renders without a key.",
203
+ },
204
+ {
205
+ file: "connections/oauth/index.ts",
206
+ reason: "`OAuthProviderConfig.clientSecret` is the *deployment's own* OAuth client secret, supplied by the host " +
207
+ "at wiring time — the same shape and the same limits as the model provider's key above. It goes into one " +
208
+ "token-request body over the back channel and is never put in an authorization URL, because a secret in " +
209
+ "a URL is in the browser history, the referrer header and every proxy log between. `TokenResponse` " +
210
+ "carries `accessToken`/`refreshToken` for the moments between the exchange and sealing them: the " +
211
+ "connection service passes them straight to `SecretCipher.seal`, and what reaches the database is " +
212
+ "ciphertext. Neither is written to a table, placed in a message part or a result envelope, and the " +
213
+ "provider's own error body is deliberately not echoed because it routinely quotes the request.",
214
+ },
215
+ {
216
+ file: "tools/credentials.ts",
217
+ reason: "The `Credential` union is the *resolved* value — the one place a third-party secret legitimately exists, " +
218
+ "for the duration of one call. `password` and the other secret-bearing fields are named honestly rather " +
219
+ "than hidden behind a generic `value`, because a `basic` credential genuinely has two parts and calling " +
220
+ "them `a` and `b` would make every call site guess. This module is what makes this the *only* such place: " +
221
+ "it is why a tool takes a `credentialRef` instead of a secret, and why nothing is stored on the tool. " +
222
+ "The exemption comes with compensating controls rather than on its own — #260 defines the secret fields " +
223
+ "**non-enumerably** and overrides `toJSON`, `toString` and `util.inspect`, so a spread, a " +
224
+ "`JSON.stringify` or a `console.log` of a credential yields `[credential redacted]` and never the secret. " +
225
+ "It is still never written to a table, never placed in a message part, a result envelope or an audit " +
226
+ "row, and `CredentialAudit` receives the scheme and the ref, never the value.",
227
+ },
228
+ {
229
+ file: "adapters/audio/openai.ts",
230
+ reason: "`OpenAiAudioConfig.apiKey` is the *deployment's own* provider key, supplied by the host at wiring time — " +
231
+ "the same shape, the same limits and the same reasoning as the embeddings adapter and the model provider " +
232
+ "factory. It reaches one `authorization` header on an operator-configured endpoint and nowhere else: it " +
233
+ "is never written to a table, never placed in a message part, a result envelope or an audit row, and the " +
234
+ "provider's error body is truncated rather than echoed whole. A `credentialRef` would be the wrong shape " +
235
+ "here for the reason it is wrong for the model provider — this is one deployment's key for its own " +
236
+ "provider account, not a per-tenant third-party grant, and routing it through the credential resolver " +
237
+ "would imply a per-tenant secret that does not exist.",
238
+ },
193
239
  {
194
240
  file: "models/provider-factory.ts",
195
241
  reason: "`ProviderCredentials.apiKey` is the model provider's own key, supplied by the host at wiring time and " +
@@ -198,6 +244,15 @@ export const CREDENTIAL_FIELD_EXEMPTIONS = [
198
244
  "credential must exist in memory to authenticate; what AC-1 forbids is storing, passing, returning or " +
199
245
  "logging one, and none of those happen here.",
200
246
  },
247
+ {
248
+ file: "adapters/embeddings/openai.ts",
249
+ reason: "`OpenAiEmbeddingsConfig.apiKey` is the same shape as the model provider's, for the same reason and with " +
250
+ "the same limits: a key the host supplies at wiring time and this adapter puts in one Authorization " +
251
+ "header. Process-local — never written to a table, never in a message part or a result envelope, and no " +
252
+ "allowlisted log field could carry it. It is also never reachable from a model: an embedding is computed " +
253
+ "for a chunk the platform is indexing, not for text a model asked about, so there is no input path that " +
254
+ "could name or read it.",
255
+ },
201
256
  ];
202
257
  /** Checks with no automated backing. The set a person must actually walk at each release. */
203
258
  export const manualChecks = () => SECURITY_CHECKS.filter((check) => check.verifiedBy === "manual");
@@ -134,19 +134,28 @@ export const FINDINGS = [
134
134
  id: "SEC-006",
135
135
  area: "egress",
136
136
  severity: "informational",
137
- title: "There is no research or web-fetch path to audit",
137
+ title: "The web-fetch path exists now, and the audit had been passing it for the wrong reason",
138
138
  impact: "AC-2 asks for the allow-list to be enforced at a single point covered by *both* the research and MCP paths. " +
139
- "The research path does not exist in this package: the only outbound HTTP is the MCP transport and the " +
140
- "Supabase storage adapter, whose destination is operator configuration rather than a model's choice. So the " +
141
- "single-point property holds trivially today and is not evidence that it will hold once research lands.",
142
- foundBy: "grepping for every `fetch(` call in the tree and finding two, neither model-directed",
139
+ "When this was written the research path did not exist and the property held trivially. It exists now " +
140
+ "REQ-039 (#188) shipped `fetch_url`, `fetch_json`, `http_request` and `http_write`, all through " +
141
+ "`toolkit/http.ts`, and #219 added an embedding adapter so the property is real rather than trivial: the " +
142
+ "model-directed path goes through `validateHttpEgress`, which refuses private ranges, cloud metadata, " +
143
+ "non-https schemes, credentials in the URL and followed redirects.\n\n" +
144
+ "The finding that replaces the original: the audit asserting 'no other outbound call' matched `fetch(` and " +
145
+ "not `?? fetch`, and every injectable client in this codebase captures the global that way. So " +
146
+ "`toolkit/http.ts` had never appeared in that check at all. Not a vulnerability — its destination was " +
147
+ "policed throughout — but the check was passing it by accident, and would have passed the next such file " +
148
+ "the same way.",
149
+ foundBy: "adding a third outbound path in #219 and asking why the audit's allow-list had only ever had three " +
150
+ "entries when the tree had four outbound sites",
143
151
  resolution: {
144
152
  kind: "accepted",
145
153
  owner: "azeem@snipe-solutions.de",
146
- reason: "Nothing to fix; recording it so the AC is not read as stronger than the evidence. A test asserts the " +
147
- "*absence* of any other outbound call, so adding one fails the audit and forces the author to route it " +
148
- "through `validateEndpoint` — which is the durable version of this guarantee.",
149
- revisitBy: "when a research or web-fetch tool is implemented",
154
+ reason: "The audit now matches a captured `fetch` as well as a called one, and its allow-list names five paths " +
155
+ "with the reason each destination is auditable. Adding a sixth fails the test and forces its author to " +
156
+ "say which policy governs the destination — which is the durable version of this guarantee, and is what " +
157
+ "the original entry was reaching for before there was anything to govern.",
158
+ revisitBy: "2027-06-30",
150
159
  },
151
160
  },
152
161
  ];
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `retinue` — the executable, task #252.
4
+ *
5
+ * `cli.ts` and `cli-worker.ts` have been here since #110 and the package had **no `bin`**, so a consumer had to
6
+ * write an entrypoint before anything ran. Applying the schema was the sharpest case:
7
+ * `adapters/postgres/migrations.ts` is a module, so provisioning a database meant writing a program first.
8
+ *
9
+ * ## What this does and does not invent
10
+ *
11
+ * It does not invent a deployment's wiring. `serve` and `worker` load `RETINUE_APP_MODULE` exactly as they did,
12
+ * and refuse to start without it — a permissive default would serve an open API to anyone who forgot to set it,
13
+ * which is the rule `cli.ts` established and this keeps.
14
+ *
15
+ * `migrate` and `doctor` deliberately need **no** app module: a database is provisioned before an application
16
+ * exists, and a diagnostic that cannot run until everything else is configured is a diagnostic nobody can use.
17
+ */
18
+ export declare const main: (argv: readonly string[], env?: NodeJS.ProcessEnv) => Promise<number>;
19
+ //# sourceMappingURL=bin.d.ts.map
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `retinue` — the executable, task #252.
4
+ *
5
+ * `cli.ts` and `cli-worker.ts` have been here since #110 and the package had **no `bin`**, so a consumer had to
6
+ * write an entrypoint before anything ran. Applying the schema was the sharpest case:
7
+ * `adapters/postgres/migrations.ts` is a module, so provisioning a database meant writing a program first.
8
+ *
9
+ * ## What this does and does not invent
10
+ *
11
+ * It does not invent a deployment's wiring. `serve` and `worker` load `RETINUE_APP_MODULE` exactly as they did,
12
+ * and refuse to start without it — a permissive default would serve an open API to anyone who forgot to set it,
13
+ * which is the rule `cli.ts` established and this keeps.
14
+ *
15
+ * `migrate` and `doctor` deliberately need **no** app module: a database is provisioned before an application
16
+ * exists, and a diagnostic that cannot run until everything else is configured is a diagnostic nobody can use.
17
+ */
18
+ import { runApiHost } from "./cli.js";
19
+ import { runWorker } from "./cli-worker.js";
20
+ import { loadConfig } from "./config.js";
21
+ import { report, runChecks } from "./doctor.js";
22
+ const USAGE = `retinue <command>
23
+
24
+ migrate Apply pending migrations to RETINUE_DATABASE_URL.
25
+ --status Report applied and pending migrations; change nothing.
26
+ --dry-run Print the statements that would run; change nothing.
27
+ serve Start the API host. Needs RETINUE_APP_MODULE.
28
+ worker Start a run worker. Needs RETINUE_APP_MODULE.
29
+ doctor Check configuration, database, schema and Redis. Reports every failure.
30
+
31
+ Configuration comes from the environment; see .env.example.`;
32
+ /** Opened lazily and per command, so `doctor` and `migrate` never load a driver they do not use. */
33
+ const postgres = async (url) => {
34
+ const { Pool } = await import("pg");
35
+ const { createPgExecutor, createPoolOpener } = await import("../entries/adapters-postgres.js");
36
+ // A connect timeout, because the default is *none*: `doctor` against a refused port sat silently instead of
37
+ // reporting the failure it exists to report. Short, since every command here either connects immediately or
38
+ // is misconfigured.
39
+ const pool = new Pool({ connectionString: url, connectionTimeoutMillis: 5_000 });
40
+ return { sql: createPgExecutor(pool), open: createPoolOpener(pool), end: () => pool.end() };
41
+ };
42
+ /**
43
+ * The advisory-lock key comes from `schema.ts` — #252's AC-2, and #266's AC-4.
44
+ *
45
+ * It used to be defined here. That made "the CLI and `auto` mode use the same key" a property of two constants
46
+ * happening to be equal, and a copy that drifted would produce two locks, no serialisation, and the original
47
+ * crash returning with the fix apparently in place. Two constants that must be equal are one constant.
48
+ */
49
+ const migrate = async (flags, env = process.env) => {
50
+ const config = loadConfig(env);
51
+ const { sql, open, end } = await postgres(config.databaseUrl);
52
+ try {
53
+ const { createSchemaManager, MIGRATION_LOCK } = await import("../entries/adapters-postgres.js");
54
+ if (flags.has("--status") || flags.has("--dry-run")) {
55
+ // Read-only paths take no lock. `plan()` and `currentVersion()` are documented as side-effect free — only
56
+ // `apply()` creates the ledger table — which is what makes a dry run honest rather than a dry run that
57
+ // provisions one table. Verified: after `--dry-run` against a fresh database, `public` has 0 tables.
58
+ const manager = createSchemaManager(sql);
59
+ const pending = await manager.plan();
60
+ const current = await manager.currentVersion();
61
+ const target = manager.targetVersion();
62
+ if (flags.has("--status")) {
63
+ console.log(`schema: ${current} of ${target} migrations applied`);
64
+ for (const change of pending)
65
+ console.log(` pending ${change.id} (${change.statements.length} statement(s))`);
66
+ if (pending.length === 0)
67
+ console.log(" nothing pending");
68
+ return 0;
69
+ }
70
+ console.log(`schema: ${current} of ${target} applied; ${pending.length} pending`);
71
+ for (const change of pending) {
72
+ console.log(`\n-- ${change.id}`);
73
+ for (const statement of change.statements)
74
+ console.log(`${statement};`);
75
+ }
76
+ return 0;
77
+ }
78
+ /**
79
+ * Applied under a session advisory lock, on **one** connection — AC-2.
80
+ *
81
+ * Measured before this existed: two concurrent `retinue migrate` runs against one database left the ledger
82
+ * correct (30 rows, 30 distinct) and **crashed one process** with
83
+ * `duplicate key value violates unique constraint "pg_type_typname_nsp_index"` — Postgres's own type
84
+ * catalogue, racing on DDL. The data was safe and the operator experience was not: running migrate from two
85
+ * pods, which is the ordinary Kubernetes init-container pattern, gives one crash loop and an error that
86
+ * names nothing an operator can act on.
87
+ *
88
+ * `createPoolOpener` rather than `createPgExecutor`, because `pool.query` picks a different connection per
89
+ * call — so a `pg_advisory_lock` taken through it would be held by a connection we might never get back, and
90
+ * the unlock would land elsewhere. One checked-out client makes the pair correct, and the lock is released
91
+ * by the session ending if the process dies mid-migration.
92
+ */
93
+ return await open(async (locked) => {
94
+ await locked.query("select pg_advisory_lock($1)", [MIGRATION_LOCK]);
95
+ try {
96
+ const manager = createSchemaManager(locked);
97
+ const pending = await manager.plan();
98
+ const target = manager.targetVersion();
99
+ if (pending.length === 0) {
100
+ console.log(`schema already at ${target}; nothing to apply`);
101
+ return 0;
102
+ }
103
+ await manager.apply();
104
+ console.log(`applied ${pending.length} migration(s); schema now at ${await manager.currentVersion()}`);
105
+ return 0;
106
+ }
107
+ finally {
108
+ await locked.query("select pg_advisory_unlock($1)", [MIGRATION_LOCK]);
109
+ }
110
+ });
111
+ }
112
+ finally {
113
+ await end();
114
+ }
115
+ };
116
+ const doctor = async (env = process.env) => {
117
+ const results = await runChecks({
118
+ env,
119
+ connectPostgres: async (url) => {
120
+ const { sql, end } = await postgres(url);
121
+ return { query: (text, params) => sql.query(text, params), end };
122
+ },
123
+ connectRedis: async (url) => {
124
+ const { Redis } = await import("ioredis");
125
+ /**
126
+ * Fail fast, on every axis ioredis has one.
127
+ *
128
+ * `maxRetriesPerRequest` alone was not enough: it bounds *command* retries, and a refused **connection**
129
+ * is retried for ever by the default `retryStrategy`. So `doctor` against a closed port hung rather than
130
+ * reporting it. `retryStrategy: () => null` stops reconnecting, `enableOfflineQueue: false` makes a
131
+ * command fail immediately instead of queueing for a connection that will never come, and `connectTimeout`
132
+ * bounds the first attempt.
133
+ */
134
+ const redis = new Redis(url, {
135
+ maxRetriesPerRequest: 1,
136
+ connectTimeout: 3_000,
137
+ // No reconnection: a refused connection must fail, not be retried behind a spinner.
138
+ retryStrategy: () => null,
139
+ /**
140
+ * The offline queue stays **on**, and that is a correction.
141
+ *
142
+ * Turning it off made the check fail against a *working* Redis: `lazyConnect: false` starts connecting
143
+ * but `ping()` is called before the socket is ready, and with no offline queue the command is rejected
144
+ * immediately with "Stream isn't writeable". So the queue is what lets the first command wait for the
145
+ * connection; `retryStrategy` and `connectTimeout` are what stop it waiting for ever, and `withTimeout`
146
+ * is the backstop.
147
+ */
148
+ lazyConnect: false,
149
+ });
150
+ // Otherwise an unreachable Redis emits an unhandled 'error' and takes the process down before the report.
151
+ redis.on("error", () => undefined);
152
+ return {
153
+ ping: () => redis.ping(),
154
+ /**
155
+ * `disconnect()`, not `quit()`, and wrapped so it is always a promise.
156
+ *
157
+ * `quit()` sends a QUIT command, which needs a working connection — so on the failure path it hangs or
158
+ * rejects, which is the path where closing matters most. `disconnect()` drops the socket unilaterally.
159
+ * It returns `void`, and returning that raw produced `Cannot read properties of undefined (reading
160
+ * 'catch')` in the caller's cleanup.
161
+ */
162
+ quit: async () => {
163
+ redis.disconnect();
164
+ },
165
+ };
166
+ },
167
+ schemaVersions: async (sql) => {
168
+ const { createSchemaManager, MIGRATION_LOCK } = await import("../entries/adapters-postgres.js");
169
+ const manager = createSchemaManager(sql);
170
+ return { current: await manager.currentVersion(), target: manager.targetVersion() };
171
+ },
172
+ });
173
+ return report(results);
174
+ };
175
+ export const main = async (argv, env = process.env) => {
176
+ const [command, ...rest] = argv;
177
+ const flags = new Set(rest.filter((a) => a.startsWith("--")));
178
+ switch (command) {
179
+ case "migrate":
180
+ return migrate(flags, env);
181
+ case "doctor":
182
+ return doctor(env);
183
+ case "serve":
184
+ await runApiHost(env);
185
+ // Resolves once listening; the process stays alive on the server's own handles.
186
+ return 0;
187
+ case "worker":
188
+ await runWorker(env);
189
+ return 0;
190
+ case undefined:
191
+ case "--help":
192
+ case "-h":
193
+ case "help":
194
+ console.log(USAGE);
195
+ return command === undefined ? 1 : 0;
196
+ default:
197
+ console.error(`unknown command "${command}"\n\n${USAGE}`);
198
+ return 1;
199
+ }
200
+ };
201
+ /** Commands that finish and must return the prompt. `serve` and `worker` deliberately do not. */
202
+ const ONE_SHOT = new Set(["migrate", "doctor", "help", "--help", "-h"]);
203
+ // Only when invoked as the binary, so importing this module for a test starts nothing.
204
+ if (process.argv[1] !== undefined && /(^|\/)(retinue|bin\.js)$/.test(process.argv[1])) {
205
+ const command = process.argv[2];
206
+ main(process.argv.slice(2))
207
+ .then((code) => {
208
+ /**
209
+ * A one-shot command **exits**, rather than setting `exitCode` and hoping the loop drains.
210
+ *
211
+ * `doctor` reported correctly and then hung: a driver that has been asked to stop reconnecting can still
212
+ * hold a socket handle, and one lingering handle keeps Node alive for ever. Closing every client is the
213
+ * fix and this is the guarantee — a diagnostic that never returns the prompt is a diagnostic nobody runs
214
+ * twice. `serve` and `worker` are excluded because their whole job is to stay up.
215
+ */
216
+ if (command === undefined || ONE_SHOT.has(command))
217
+ process.exit(code);
218
+ if (code !== 0)
219
+ process.exitCode = code;
220
+ })
221
+ .catch((error) => {
222
+ console.error(error instanceof Error ? error.message : String(error));
223
+ process.exit(1);
224
+ });
225
+ }
226
+ //# sourceMappingURL=bin.js.map
@@ -5,7 +5,8 @@
5
5
  * substance: configuration is validated *before* anything connects, so a misconfigured deployment
6
6
  * fails with a message about the variable rather than a connection error that names nothing useful.
7
7
  */
8
- import { type SchemaMode, type SqlExecutor } from "../entries/adapters-postgres.js";
8
+ import { type ConnectionOpener, type SchemaMode, type SqlExecutor } from "../entries/adapters-postgres.js";
9
+ import { type TransactionRunner } from "../adapters/postgres/transaction.js";
9
10
  import { type RetinueConfig, type Env } from "./config.js";
10
11
  export type StartupLog = {
11
12
  readonly event: string;
@@ -13,16 +14,33 @@ export type StartupLog = {
13
14
  };
14
15
  export type BootOptions = {
15
16
  readonly env: Env;
16
- /** Built after configuration validates, so a bad variable never opens a connection. */
17
+ /**
18
+ * Built after configuration validates, so a bad variable never opens a connection.
19
+ *
20
+ * `open` is optional and is what a `TransactionRunner` needs — the one primitive a pool-backed `SqlExecutor`
21
+ * cannot express, because `pool.query` picks a different connection per call. A caller that supplies it gets a
22
+ * runner in the result; one that does not gets `undefined`, and anything needing a transaction refuses by name.
23
+ */
17
24
  readonly connect: (config: RetinueConfig) => Promise<{
18
25
  readonly sql: SqlExecutor;
26
+ readonly open?: ConnectionOpener;
19
27
  }>;
20
28
  readonly log?: (entry: StartupLog) => void;
21
29
  readonly version?: string;
22
30
  };
23
31
  export type BootResult = {
24
32
  readonly config: RetinueConfig;
33
+ /**
34
+ * The executor an application should build its stores over.
35
+ *
36
+ * **Scoped when a runner exists**, so a store built on it joins the ambient transaction without knowing
37
+ * transactions exist. Handing back the unscoped executor alongside a runner is the trap
38
+ * `transaction.ts` warns about: "a store built over a *non*-scoped executor silently escapes the
39
+ * transaction rather than failing".
40
+ */
25
41
  readonly sql: SqlExecutor;
42
+ /** Present when `connect` supplied an `open`. `undefined` for a process that needs no transactions. */
43
+ readonly runner?: TransactionRunner;
26
44
  readonly schema: {
27
45
  readonly mode: SchemaMode;
28
46
  readonly applied: readonly string[];
@@ -5,16 +5,37 @@
5
5
  * substance: configuration is validated *before* anything connects, so a misconfigured deployment
6
6
  * fails with a message about the variable rather than a connection error that names nothing useful.
7
7
  */
8
- import { provisionSchema } from "../entries/adapters-postgres.js";
8
+ import { provisionSchema, } from "../entries/adapters-postgres.js";
9
+ import { createTransactionScope } from "../adapters/postgres/transaction.js";
9
10
  import { loadConfig } from "./config.js";
10
11
  export const boot = async (options) => {
11
12
  const log = options.log ?? ((entry) => console.log(JSON.stringify(entry)));
12
13
  // Configuration first, and it throws. A half-configured process that boots is worse than one that
13
14
  // refuses: it passes its own health check and fails on the first real request.
14
15
  const config = loadConfig(options.env);
15
- const { sql } = await options.connect(config);
16
+ const { sql: base, open } = await options.connect(config);
17
+ /**
18
+ * The transaction scope, when the caller can open a connection — found by #254.
19
+ *
20
+ * `runApiHost` never built one, so `app.deps({ config, sql })` was called without a runner and the reference
21
+ * app's coordinator refused: *"this process has no TransactionRunner, so the conversation run coordinator
22
+ * cannot be used. The API host supplies one"*. It did not. `sendMessage` — the mutation that starts every run
23
+ * — therefore failed with an internal error in the shipped API host and in `npm run api`, which is the
24
+ * documented way to run the reference app.
25
+ */
26
+ const scope = open === undefined ? undefined : createTransactionScope(open);
27
+ const sql = scope === undefined ? base : scope.scoped(base);
16
28
  const provisioned = await provisionSchema(sql, {
17
29
  mode: config.schemaMode,
30
+ /**
31
+ * Passed so `auto` mode serialises — #266.
32
+ *
33
+ * Several workers booting against a fresh database is the ordinary Kubernetes init pattern, and without
34
+ * this one of them crashes on Postgres's own type catalogue with an error naming nothing actionable.
35
+ * `undefined` for a process whose adapter cannot hold a connection, which is correct: those are
36
+ * single-process and have nothing to race with.
37
+ */
38
+ ...(open === undefined ? {} : { open }),
18
39
  // Routed through the structured log rather than printed, so `plan` mode's diff is machine-readable
19
40
  // in the same stream as everything else.
20
41
  log: (message) => log({ event: "schema", mode: config.schemaMode, message }),
@@ -31,6 +52,11 @@ export const boot = async (options) => {
31
52
  // The adapters actually selected, so a support question does not start with "which database?".
32
53
  adapters: { store: "postgres", queue: "bullmq", lock: "redis" },
33
54
  });
34
- return { config, sql, schema: { mode: config.schemaMode, applied: provisioned.applied } };
55
+ return {
56
+ config,
57
+ sql,
58
+ ...(scope === undefined ? {} : { runner: scope.runner }),
59
+ schema: { mode: config.schemaMode, applied: provisioned.applied },
60
+ };
35
61
  };
36
62
  //# sourceMappingURL=boot.js.map
@@ -2,6 +2,7 @@ import { type Authenticate } from "./host.js";
2
2
  import { type RetinueConfig } from "./config.js";
3
3
  import type { ResolverDeps } from "../index.js";
4
4
  import type { SqlExecutor } from "../entries/adapters-postgres.js";
5
+ import type { TransactionRunner } from "../adapters/postgres/transaction.js";
5
6
  /**
6
7
  * What a deployment's app module must default-export.
7
8
  *
@@ -10,9 +11,21 @@ import type { SqlExecutor } from "../entries/adapters-postgres.js";
10
11
  */
11
12
  export type RetinueApp = {
12
13
  readonly authenticate: Authenticate;
14
+ /**
15
+ * `runner` added by #254, and it was not optional in practice — it was missing.
16
+ *
17
+ * The reference app's `deps` has always accepted an optional `TransactionRunner`, and this host never passed
18
+ * one, so the conversation run coordinator refused with *"this process has no TransactionRunner… The API host
19
+ * supplies one"*. It did not. `sendMessage` — the mutation that starts every run — failed with an internal
20
+ * error in this command and in `npm run api`, which is the documented way to run the reference app.
21
+ *
22
+ * Still optional on the signature, because a worker genuinely does not need it and an app that ignores it
23
+ * keeps compiling.
24
+ */
13
25
  readonly deps: (input: {
14
26
  readonly config: RetinueConfig;
15
27
  readonly sql: SqlExecutor;
28
+ readonly runner?: TransactionRunner;
16
29
  }) => Promise<ResolverDeps> | ResolverDeps;
17
30
  /** Optional liveness/readiness extras beyond Postgres, Redis and the schema version. */
18
31
  readonly redis?: (config: RetinueConfig) => {