@retinue/agentkit 0.2.0 → 0.3.1

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 (209) hide show
  1. package/README.md +1 -1
  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/memory/connections.d.ts +10 -0
  6. package/dist/adapters/memory/connections.js +87 -0
  7. package/dist/adapters/memory/graph.d.ts +17 -0
  8. package/dist/adapters/memory/graph.js +267 -0
  9. package/dist/adapters/memory/index.d.ts +3 -0
  10. package/dist/adapters/memory/index.js +3 -0
  11. package/dist/adapters/memory/rate-limit.d.ts +14 -0
  12. package/dist/adapters/memory/rate-limit.js +28 -0
  13. package/dist/adapters/memory/runtime.js +0 -0
  14. package/dist/adapters/postgres/connections.d.ts +15 -0
  15. package/dist/adapters/postgres/connections.js +134 -0
  16. package/dist/adapters/postgres/graph.d.ts +24 -0
  17. package/dist/adapters/postgres/graph.js +325 -0
  18. package/dist/adapters/postgres/index.d.ts +2 -0
  19. package/dist/adapters/postgres/index.js +2 -0
  20. package/dist/adapters/postgres/migrations.js +198 -0
  21. package/dist/adapters/postgres/run-store.js +26 -2
  22. package/dist/adapters/postgres/schema.d.ts +61 -2
  23. package/dist/adapters/postgres/schema.js +95 -10
  24. package/dist/adapters/redis/index.d.ts +1 -0
  25. package/dist/adapters/redis/index.js +1 -0
  26. package/dist/adapters/redis/rate-limit.d.ts +46 -0
  27. package/dist/adapters/redis/rate-limit.js +55 -0
  28. package/dist/adapters/supabase/index.d.ts +15 -0
  29. package/dist/adapters/supabase/index.js +15 -0
  30. package/dist/adapters/supabase/rls.js +33 -0
  31. package/dist/agents/agent.d.ts +78 -1
  32. package/dist/agents/agent.js +118 -5
  33. package/dist/agents/define.js +24 -4
  34. package/dist/agents/engine.d.ts +20 -0
  35. package/dist/agents/engine.js +177 -6
  36. package/dist/audio/index.d.ts +151 -0
  37. package/dist/audio/index.js +183 -0
  38. package/dist/connections/cipher.d.ts +103 -0
  39. package/dist/connections/cipher.js +141 -0
  40. package/dist/connections/index.d.ts +156 -0
  41. package/dist/connections/index.js +72 -0
  42. package/dist/connections/oauth/client.d.ts +84 -0
  43. package/dist/connections/oauth/client.js +141 -0
  44. package/dist/connections/oauth/index.d.ts +166 -0
  45. package/dist/connections/oauth/index.js +226 -0
  46. package/dist/connections/oauth/service.d.ts +90 -0
  47. package/dist/connections/oauth/service.js +132 -0
  48. package/dist/connections/pause.d.ts +68 -0
  49. package/dist/connections/pause.js +97 -0
  50. package/dist/connections/resolver.d.ts +59 -0
  51. package/dist/connections/resolver.js +138 -0
  52. package/dist/connections/resume.d.ts +64 -0
  53. package/dist/connections/resume.js +99 -0
  54. package/dist/connections/vault-cipher.d.ts +95 -0
  55. package/dist/connections/vault-cipher.js +206 -0
  56. package/dist/core/content-parts.d.ts +18 -2
  57. package/dist/core/content-parts.js +1 -0
  58. package/dist/core/context.d.ts +25 -0
  59. package/dist/core/errors.d.ts +1 -1
  60. package/dist/core/errors.js +15 -0
  61. package/dist/core/events.d.ts +44 -2
  62. package/dist/core/events.js +31 -2
  63. package/dist/core/validation.js +19 -0
  64. package/dist/entries/connections.d.ts +10 -0
  65. package/dist/entries/connections.js +10 -0
  66. package/dist/entries/knowledge.d.ts +8 -0
  67. package/dist/entries/knowledge.js +8 -0
  68. package/dist/entries/mcp-server.d.ts +9 -0
  69. package/dist/entries/mcp-server.js +9 -0
  70. package/dist/entries/runtime.d.ts +10 -0
  71. package/dist/entries/runtime.js +8 -0
  72. package/dist/entries/testing.d.ts +28 -0
  73. package/dist/entries/testing.js +28 -0
  74. package/dist/graphql/resolvers.d.ts +3 -1
  75. package/dist/graphql/resolvers.js +14 -0
  76. package/dist/knowledge/communities.d.ts +166 -0
  77. package/dist/knowledge/communities.js +377 -0
  78. package/dist/knowledge/graph-global.d.ts +153 -0
  79. package/dist/knowledge/graph-global.js +246 -0
  80. package/dist/knowledge/graph-retrieval.d.ts +150 -0
  81. package/dist/knowledge/graph-retrieval.js +303 -0
  82. package/dist/knowledge/graph.d.ts +223 -0
  83. package/dist/knowledge/graph.js +369 -0
  84. package/dist/knowledge/index.d.ts +24 -0
  85. package/dist/knowledge/index.js +36 -1
  86. package/dist/knowledge/retrieval.d.ts +26 -1
  87. package/dist/knowledge/retrieval.js +77 -1
  88. package/dist/mcp-server/index.d.ts +168 -0
  89. package/dist/mcp-server/index.js +175 -0
  90. package/dist/models/extraction.d.ts +73 -0
  91. package/dist/models/extraction.js +118 -0
  92. package/dist/models/index.d.ts +40 -1
  93. package/dist/models/index.js +22 -1
  94. package/dist/models/pricing.d.ts +2 -0
  95. package/dist/models/pricing.js +17 -1
  96. package/dist/models/provider-factory.js +9 -7
  97. package/dist/models/streaming.d.ts +89 -0
  98. package/dist/models/streaming.js +179 -2
  99. package/dist/persistence/index.d.ts +230 -0
  100. package/dist/persistence/index.js +2 -0
  101. package/dist/runtime/index.d.ts +1 -1
  102. package/dist/runtime/index.js +12 -0
  103. package/dist/runtime/worker.d.ts +36 -1
  104. package/dist/runtime/worker.js +37 -4
  105. package/dist/security/checklist.js +46 -0
  106. package/dist/server/bin.d.ts +72 -0
  107. package/dist/server/bin.js +304 -0
  108. package/dist/server/boot.d.ts +20 -2
  109. package/dist/server/boot.js +29 -3
  110. package/dist/server/cli-worker.js +6 -3
  111. package/dist/server/cli.d.ts +13 -0
  112. package/dist/server/cli.js +10 -5
  113. package/dist/server/config.d.ts +11 -1
  114. package/dist/server/config.js +18 -0
  115. package/dist/server/doctor.d.ts +85 -0
  116. package/dist/server/doctor.js +280 -0
  117. package/dist/server/pool.d.ts +47 -0
  118. package/dist/server/pool.js +73 -0
  119. package/dist/skills/context.d.ts +61 -0
  120. package/dist/skills/context.js +107 -0
  121. package/dist/skills/index.d.ts +1 -0
  122. package/dist/skills/index.js +1 -0
  123. package/dist/telemetry/spans.js +5 -0
  124. package/dist/testing/conformance/artifact-exports.d.ts +20 -0
  125. package/dist/testing/conformance/artifact-exports.js +172 -0
  126. package/dist/testing/conformance/artifacts.d.ts +11 -0
  127. package/dist/testing/conformance/artifacts.js +316 -0
  128. package/dist/testing/conformance/audio.d.ts +38 -0
  129. package/dist/testing/conformance/audio.js +145 -0
  130. package/dist/testing/conformance/capability.d.ts +23 -0
  131. package/dist/testing/conformance/capability.js +27 -0
  132. package/dist/testing/conformance/checkpoint-store.d.ts +15 -0
  133. package/dist/testing/conformance/checkpoint-store.js +72 -0
  134. package/dist/testing/conformance/connections.d.ts +14 -0
  135. package/dist/testing/conformance/connections.js +171 -0
  136. package/dist/testing/conformance/conversation-store.d.ts +10 -0
  137. package/dist/testing/conformance/conversation-store.js +60 -0
  138. package/dist/testing/conformance/evaluation.d.ts +10 -0
  139. package/dist/testing/conformance/evaluation.js +295 -0
  140. package/dist/testing/conformance/files.d.ts +19 -0
  141. package/dist/testing/conformance/files.js +454 -0
  142. package/dist/testing/conformance/flows.d.ts +16 -0
  143. package/dist/testing/conformance/flows.js +193 -0
  144. package/dist/testing/conformance/graph.d.ts +22 -0
  145. package/dist/testing/conformance/graph.js +500 -0
  146. package/dist/testing/conformance/hitl.d.ts +25 -0
  147. package/dist/testing/conformance/hitl.js +523 -0
  148. package/dist/testing/conformance/index.d.ts +163 -0
  149. package/dist/testing/conformance/index.js +321 -0
  150. package/dist/testing/conformance/invariants.d.ts +23 -0
  151. package/dist/testing/conformance/invariants.js +80 -0
  152. package/dist/testing/conformance/knowledge.d.ts +41 -0
  153. package/dist/testing/conformance/knowledge.js +592 -0
  154. package/dist/testing/conformance/parents.d.ts +50 -0
  155. package/dist/testing/conformance/parents.js +39 -0
  156. package/dist/testing/conformance/rate-limit.d.ts +25 -0
  157. package/dist/testing/conformance/rate-limit.js +71 -0
  158. package/dist/testing/conformance/records.d.ts +36 -0
  159. package/dist/testing/conformance/records.js +400 -0
  160. package/dist/testing/conformance/rollups.d.ts +30 -0
  161. package/dist/testing/conformance/rollups.js +425 -0
  162. package/dist/testing/conformance/run-coordinator.d.ts +27 -0
  163. package/dist/testing/conformance/run-coordinator.js +150 -0
  164. package/dist/testing/conformance/run-event-log.d.ts +9 -0
  165. package/dist/testing/conformance/run-event-log.js +138 -0
  166. package/dist/testing/conformance/run-store.d.ts +12 -0
  167. package/dist/testing/conformance/run-store.js +294 -0
  168. package/dist/testing/conformance/session-state.d.ts +38 -0
  169. package/dist/testing/conformance/session-state.js +198 -0
  170. package/dist/testing/conformance/usage-limits.d.ts +10 -0
  171. package/dist/testing/conformance/usage-limits.js +309 -0
  172. package/dist/testing/conformance.d.ts +8 -0
  173. package/dist/testing/conformance.js +8 -0
  174. package/dist/testing/memory-backend.d.ts +39 -0
  175. package/dist/testing/memory-backend.js +41 -0
  176. package/dist/testing/pglite.d.ts +55 -0
  177. package/dist/testing/pglite.js +78 -0
  178. package/dist/testing/stub-model.d.ts +58 -0
  179. package/dist/testing/stub-model.js +71 -0
  180. package/dist/testing/supabase-storage-double.d.ts +35 -0
  181. package/dist/testing/supabase-storage-double.js +101 -0
  182. package/dist/toolkit/http.d.ts +27 -0
  183. package/dist/toolkit/http.js +48 -0
  184. package/dist/toolkit/index.d.ts +27 -1
  185. package/dist/toolkit/index.js +24 -1
  186. package/dist/toolkit/mime.d.ts +97 -0
  187. package/dist/toolkit/mime.js +233 -0
  188. package/dist/toolkit/ssrf.d.ts +139 -0
  189. package/dist/toolkit/ssrf.js +349 -0
  190. package/dist/toolkit/vendor.d.ts +91 -0
  191. package/dist/toolkit/vendor.js +153 -0
  192. package/dist/tools/credentials.d.ts +265 -6
  193. package/dist/tools/credentials.js +318 -3
  194. package/dist/tools/define.d.ts +14 -0
  195. package/dist/tools/define.js +3 -0
  196. package/dist/tools/index.d.ts +21 -0
  197. package/dist/tools/index.js +7 -0
  198. package/dist/tools/library/audio.d.ts +63 -0
  199. package/dist/tools/library/audio.js +148 -0
  200. package/dist/tools/library/index.d.ts +8 -0
  201. package/dist/tools/library/index.js +7 -0
  202. package/dist/tools/registry.d.ts +33 -0
  203. package/dist/tools/registry.js +72 -2
  204. package/dist/usage/index.d.ts +21 -0
  205. package/dist/usage/index.js +1 -0
  206. package/dist/usage/rate-limit.d.ts +122 -0
  207. package/dist/usage/rate-limit.js +131 -0
  208. package/dist/usage/recorder.js +3 -0
  209. package/package.json +29 -2
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import type { PlatformError } from "../core/errors.js";
5
5
  import type { AgentId, ConversationId, PrincipalId, RunId, TenantId } from "../core/ids.js";
6
- export declare const RUN_STATUSES: readonly ["queued", "running", "waiting-for-question", "waiting-for-approval", "retry-pending", "completed", "failed", "cancelled"];
6
+ export declare const RUN_STATUSES: readonly ["queued", "running", "waiting-for-question", "waiting-for-approval", "waiting-for-connection", "retry-pending", "completed", "failed", "cancelled"];
7
7
  export type RunStatus = (typeof RUN_STATUSES)[number];
8
8
  /**
9
9
  * The state machine from the specification. A transition absent from this map is a
@@ -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 " +
@@ -0,0 +1,72 @@
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
+ /**
19
+ * The advisory-lock key comes from `schema.ts` — #252's AC-2, and #266's AC-4.
20
+ *
21
+ * It used to be defined here. That made "the CLI and `auto` mode use the same key" a property of two constants
22
+ * happening to be equal, and a copy that drifted would produce two locks, no serialisation, and the original
23
+ * crash returning with the fix apparently in place. Two constants that must be equal are one constant.
24
+ */
25
+ /**
26
+ * Create the configured schema if it is not there, before anything tries to use it.
27
+ *
28
+ * `migrate` is the command that owns provisioning, so the namespace it was told to provision into is
29
+ * its job too — and the reason is worse than a missing-schema error. **Postgres does not error.**
30
+ * `SET search_path TO retinue, public` succeeds when `retinue` does not exist: a missing entry is
31
+ * skipped, not rejected. `CREATE TABLE conversations` then lands in the first schema that *does*
32
+ * exist, which is `public` — so all 34 platform migrations would silently be created alongside the
33
+ * product's tables, reporting success the whole way. Verified against a real Postgres 17: the SET
34
+ * returns, the CREATE returns, and `information_schema` says `public`.
35
+ *
36
+ * That is the failure this function exists to prevent, and it is why it runs before the pool rather
37
+ * than relying on a connection error that never comes.
38
+ *
39
+ * On its own connection with the **default** search path, which is the part that is easy to get wrong.
40
+ * A connection configured for `retinue` cannot be the one that creates `retinue` — `openPostgres`
41
+ * destroys it during setup, before a statement of ours runs.
42
+ *
43
+ * `IF NOT EXISTS` and nothing else: no owner, no grants, no drop. Provisioning a namespace is additive;
44
+ * deciding who may use it is a deployment's decision and not a migration's.
45
+ */
46
+ /**
47
+ * Which `migrate` invocations must change nothing.
48
+ *
49
+ * Its own function because it is the link between a flag and a side effect, and that link is invisible
50
+ * to a test of either end: sabotaging it to `false` — so `--dry-run` provisions a schema — broke
51
+ * nothing, while every assertion about `ensureSchema` itself stayed green. The list is also the same
52
+ * one the read-only branch below uses, so a third flag added to one and not the other cannot silently
53
+ * become a writing dry run.
54
+ */
55
+ export declare const READ_ONLY_FLAGS: readonly ["--status", "--dry-run"];
56
+ export declare const isReadOnly: (flags: ReadonlySet<string>) => boolean;
57
+ export declare const ensureSchema: (config: {
58
+ readonly databaseUrl: string;
59
+ readonly databaseSchema?: string;
60
+ }, { create, connect, }: {
61
+ readonly create: boolean;
62
+ readonly connect?: (settings: {
63
+ readonly databaseUrl: string;
64
+ }) => Promise<{
65
+ readonly sql: {
66
+ query<Row>(text: string, params?: readonly unknown[]): Promise<Row[]>;
67
+ };
68
+ readonly end: () => Promise<void>;
69
+ }>;
70
+ }) => Promise<boolean>;
71
+ export declare const main: (argv: readonly string[], env?: NodeJS.ProcessEnv) => Promise<number>;
72
+ //# sourceMappingURL=bin.d.ts.map
@@ -0,0 +1,304 @@
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
+ /**
33
+ * Opened lazily and per command, so `doctor` and `migrate` never load a driver they do not use.
34
+ *
35
+ * The pool itself comes from `pool.ts`, which is also what the API host and the worker use — the schema
36
+ * a deployment configures has to be the same one migrations run against, and that is only true while
37
+ * there is one place that decides it.
38
+ *
39
+ * A connect timeout, because the default is *none*: `doctor` against a refused port sat silently instead
40
+ * of reporting the failure it exists to report. Short, since every command here either connects
41
+ * immediately or is misconfigured.
42
+ */
43
+ const postgres = async (config) => {
44
+ const { openPostgres } = await import("./pool.js");
45
+ return openPostgres({ ...config, connectionTimeoutMillis: 5_000 });
46
+ };
47
+ /**
48
+ * The advisory-lock key comes from `schema.ts` — #252's AC-2, and #266's AC-4.
49
+ *
50
+ * It used to be defined here. That made "the CLI and `auto` mode use the same key" a property of two constants
51
+ * happening to be equal, and a copy that drifted would produce two locks, no serialisation, and the original
52
+ * crash returning with the fix apparently in place. Two constants that must be equal are one constant.
53
+ */
54
+ /**
55
+ * Create the configured schema if it is not there, before anything tries to use it.
56
+ *
57
+ * `migrate` is the command that owns provisioning, so the namespace it was told to provision into is
58
+ * its job too — and the reason is worse than a missing-schema error. **Postgres does not error.**
59
+ * `SET search_path TO retinue, public` succeeds when `retinue` does not exist: a missing entry is
60
+ * skipped, not rejected. `CREATE TABLE conversations` then lands in the first schema that *does*
61
+ * exist, which is `public` — so all 34 platform migrations would silently be created alongside the
62
+ * product's tables, reporting success the whole way. Verified against a real Postgres 17: the SET
63
+ * returns, the CREATE returns, and `information_schema` says `public`.
64
+ *
65
+ * That is the failure this function exists to prevent, and it is why it runs before the pool rather
66
+ * than relying on a connection error that never comes.
67
+ *
68
+ * On its own connection with the **default** search path, which is the part that is easy to get wrong.
69
+ * A connection configured for `retinue` cannot be the one that creates `retinue` — `openPostgres`
70
+ * destroys it during setup, before a statement of ours runs.
71
+ *
72
+ * `IF NOT EXISTS` and nothing else: no owner, no grants, no drop. Provisioning a namespace is additive;
73
+ * deciding who may use it is a deployment's decision and not a migration's.
74
+ */
75
+ /**
76
+ * Which `migrate` invocations must change nothing.
77
+ *
78
+ * Its own function because it is the link between a flag and a side effect, and that link is invisible
79
+ * to a test of either end: sabotaging it to `false` — so `--dry-run` provisions a schema — broke
80
+ * nothing, while every assertion about `ensureSchema` itself stayed green. The list is also the same
81
+ * one the read-only branch below uses, so a third flag added to one and not the other cannot silently
82
+ * become a writing dry run.
83
+ */
84
+ export const READ_ONLY_FLAGS = ["--status", "--dry-run"];
85
+ export const isReadOnly = (flags) => READ_ONLY_FLAGS.some((flag) => flags.has(flag));
86
+ export const ensureSchema = async (config, { create,
87
+ // Injectable so a test can run this against a real Postgres — PGlite — without a live server. The
88
+ // default is the same `postgres` every command here uses.
89
+ connect = (settings) => postgres(settings), }) => {
90
+ if (config.databaseSchema === undefined)
91
+ return true;
92
+ const { sql, end } = await connect({ databaseUrl: config.databaseUrl });
93
+ try {
94
+ if (create) {
95
+ // Validated as an unquoted identifier by `loadConfig` — the only reason this concatenation is safe,
96
+ // and the reason that check refuses anything Postgres would need quoted.
97
+ await sql.query(`CREATE SCHEMA IF NOT EXISTS ${config.databaseSchema}`);
98
+ console.log(`schema: ${config.databaseSchema} ready`);
99
+ return true;
100
+ }
101
+ /**
102
+ * The read-only paths report instead of creating, and that is not fussiness.
103
+ *
104
+ * `--dry-run` and `--status` are documented a few lines below as side-effect free — the note says
105
+ * `public` has 0 tables after a dry run against a fresh database — and a reader deciding whether to
106
+ * trust a dry run is exactly the reader who must not find it provisioned a schema. Creating one is
107
+ * a small side effect and a large broken promise.
108
+ */
109
+ const rows = await sql.query(`select exists (select 1 from pg_namespace where nspname = $1) as exists`, [config.databaseSchema]);
110
+ if (rows[0]?.exists === true)
111
+ return true;
112
+ console.error(`schema: ${config.databaseSchema} does not exist. Postgres will not complain — it skips a missing ` +
113
+ `entry in search_path — so tables would be created in the next schema on the path instead, ` +
114
+ `silently. Run \`retinue migrate\`, which creates it, or unset RETINUE_DATABASE_SCHEMA to use the ` +
115
+ `connection's own schema deliberately.`);
116
+ return false;
117
+ }
118
+ finally {
119
+ await end();
120
+ }
121
+ };
122
+ const migrate = async (flags, env = process.env) => {
123
+ const config = loadConfig(env);
124
+ // Before the pool, because a missing schema does not fail: `SET search_path` skips an entry that does
125
+ // not exist and every CREATE lands in `public` instead, silently. The read-only flags report rather
126
+ // than create — see `ensureSchema`.
127
+ if (!(await ensureSchema(config, { create: !isReadOnly(flags) })))
128
+ return 1;
129
+ const { sql, open, end } = await postgres(config);
130
+ try {
131
+ const { createSchemaManager, MIGRATION_LOCK } = await import("../entries/adapters-postgres.js");
132
+ if (isReadOnly(flags)) {
133
+ // Read-only paths take no lock. `plan()` and `currentVersion()` are documented as side-effect free — only
134
+ // `apply()` creates the ledger table — which is what makes a dry run honest rather than a dry run that
135
+ // provisions one table. Verified: after `--dry-run` against a fresh database, `public` has 0 tables.
136
+ const manager = createSchemaManager(sql);
137
+ const pending = await manager.plan();
138
+ const current = await manager.currentVersion();
139
+ const target = manager.targetVersion();
140
+ if (flags.has("--status")) {
141
+ console.log(`schema: ${current} of ${target} migrations applied`);
142
+ for (const change of pending)
143
+ console.log(` pending ${change.id} (${change.statements.length} statement(s))`);
144
+ if (pending.length === 0)
145
+ console.log(" nothing pending");
146
+ return 0;
147
+ }
148
+ console.log(`schema: ${current} of ${target} applied; ${pending.length} pending`);
149
+ for (const change of pending) {
150
+ console.log(`\n-- ${change.id}`);
151
+ for (const statement of change.statements)
152
+ console.log(`${statement};`);
153
+ }
154
+ return 0;
155
+ }
156
+ /**
157
+ * Applied under a session advisory lock, on **one** connection — AC-2.
158
+ *
159
+ * Measured before this existed: two concurrent `retinue migrate` runs against one database left the ledger
160
+ * correct (30 rows, 30 distinct) and **crashed one process** with
161
+ * `duplicate key value violates unique constraint "pg_type_typname_nsp_index"` — Postgres's own type
162
+ * catalogue, racing on DDL. The data was safe and the operator experience was not: running migrate from two
163
+ * pods, which is the ordinary Kubernetes init-container pattern, gives one crash loop and an error that
164
+ * names nothing an operator can act on.
165
+ *
166
+ * `createPoolOpener` rather than `createPgExecutor`, because `pool.query` picks a different connection per
167
+ * call — so a `pg_advisory_lock` taken through it would be held by a connection we might never get back, and
168
+ * the unlock would land elsewhere. One checked-out client makes the pair correct, and the lock is released
169
+ * by the session ending if the process dies mid-migration.
170
+ */
171
+ return await open(async (locked) => {
172
+ await locked.query("select pg_advisory_lock($1)", [MIGRATION_LOCK]);
173
+ try {
174
+ const manager = createSchemaManager(locked);
175
+ const pending = await manager.plan();
176
+ const target = manager.targetVersion();
177
+ if (pending.length === 0) {
178
+ console.log(`schema already at ${target}; nothing to apply`);
179
+ return 0;
180
+ }
181
+ await manager.apply();
182
+ console.log(`applied ${pending.length} migration(s); schema now at ${await manager.currentVersion()}`);
183
+ return 0;
184
+ }
185
+ finally {
186
+ await locked.query("select pg_advisory_unlock($1)", [MIGRATION_LOCK]);
187
+ }
188
+ });
189
+ }
190
+ finally {
191
+ await end();
192
+ }
193
+ };
194
+ const doctor = async (env = process.env) => {
195
+ const results = await runChecks({
196
+ env,
197
+ connectPostgres: async (settings) => {
198
+ const { sql, end } = await postgres(settings);
199
+ return { query: (text, params) => sql.query(text, params), end };
200
+ },
201
+ connectRedis: async (url) => {
202
+ const { Redis } = await import("ioredis");
203
+ /**
204
+ * Fail fast, on every axis ioredis has one.
205
+ *
206
+ * `maxRetriesPerRequest` alone was not enough: it bounds *command* retries, and a refused **connection**
207
+ * is retried for ever by the default `retryStrategy`. So `doctor` against a closed port hung rather than
208
+ * reporting it. `retryStrategy: () => null` stops reconnecting, `enableOfflineQueue: false` makes a
209
+ * command fail immediately instead of queueing for a connection that will never come, and `connectTimeout`
210
+ * bounds the first attempt.
211
+ */
212
+ const redis = new Redis(url, {
213
+ maxRetriesPerRequest: 1,
214
+ connectTimeout: 3_000,
215
+ // No reconnection: a refused connection must fail, not be retried behind a spinner.
216
+ retryStrategy: () => null,
217
+ /**
218
+ * The offline queue stays **on**, and that is a correction.
219
+ *
220
+ * Turning it off made the check fail against a *working* Redis: `lazyConnect: false` starts connecting
221
+ * but `ping()` is called before the socket is ready, and with no offline queue the command is rejected
222
+ * immediately with "Stream isn't writeable". So the queue is what lets the first command wait for the
223
+ * connection; `retryStrategy` and `connectTimeout` are what stop it waiting for ever, and `withTimeout`
224
+ * is the backstop.
225
+ */
226
+ lazyConnect: false,
227
+ });
228
+ // Otherwise an unreachable Redis emits an unhandled 'error' and takes the process down before the report.
229
+ redis.on("error", () => undefined);
230
+ return {
231
+ ping: () => redis.ping(),
232
+ /**
233
+ * `disconnect()`, not `quit()`, and wrapped so it is always a promise.
234
+ *
235
+ * `quit()` sends a QUIT command, which needs a working connection — so on the failure path it hangs or
236
+ * rejects, which is the path where closing matters most. `disconnect()` drops the socket unilaterally.
237
+ * It returns `void`, and returning that raw produced `Cannot read properties of undefined (reading
238
+ * 'catch')` in the caller's cleanup.
239
+ */
240
+ quit: async () => {
241
+ redis.disconnect();
242
+ },
243
+ };
244
+ },
245
+ schemaVersions: async (sql) => {
246
+ const { createSchemaManager, MIGRATION_LOCK } = await import("../entries/adapters-postgres.js");
247
+ const manager = createSchemaManager(sql);
248
+ return { current: await manager.currentVersion(), target: manager.targetVersion() };
249
+ },
250
+ });
251
+ return report(results);
252
+ };
253
+ export const main = async (argv, env = process.env) => {
254
+ const [command, ...rest] = argv;
255
+ const flags = new Set(rest.filter((a) => a.startsWith("--")));
256
+ switch (command) {
257
+ case "migrate":
258
+ return migrate(flags, env);
259
+ case "doctor":
260
+ return doctor(env);
261
+ case "serve":
262
+ await runApiHost(env);
263
+ // Resolves once listening; the process stays alive on the server's own handles.
264
+ return 0;
265
+ case "worker":
266
+ await runWorker(env);
267
+ return 0;
268
+ case undefined:
269
+ case "--help":
270
+ case "-h":
271
+ case "help":
272
+ console.log(USAGE);
273
+ return command === undefined ? 1 : 0;
274
+ default:
275
+ console.error(`unknown command "${command}"\n\n${USAGE}`);
276
+ return 1;
277
+ }
278
+ };
279
+ /** Commands that finish and must return the prompt. `serve` and `worker` deliberately do not. */
280
+ const ONE_SHOT = new Set(["migrate", "doctor", "help", "--help", "-h"]);
281
+ // Only when invoked as the binary, so importing this module for a test starts nothing.
282
+ if (process.argv[1] !== undefined && /(^|\/)(retinue|bin\.js)$/.test(process.argv[1])) {
283
+ const command = process.argv[2];
284
+ main(process.argv.slice(2))
285
+ .then((code) => {
286
+ /**
287
+ * A one-shot command **exits**, rather than setting `exitCode` and hoping the loop drains.
288
+ *
289
+ * `doctor` reported correctly and then hung: a driver that has been asked to stop reconnecting can still
290
+ * hold a socket handle, and one lingering handle keeps Node alive for ever. Closing every client is the
291
+ * fix and this is the guarantee — a diagnostic that never returns the prompt is a diagnostic nobody runs
292
+ * twice. `serve` and `worker` are excluded because their whole job is to stay up.
293
+ */
294
+ if (command === undefined || ONE_SHOT.has(command))
295
+ process.exit(code);
296
+ if (code !== 0)
297
+ process.exitCode = code;
298
+ })
299
+ .catch((error) => {
300
+ console.error(error instanceof Error ? error.message : String(error));
301
+ process.exit(1);
302
+ });
303
+ }
304
+ //# 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[];