@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.
- package/README.md +59 -277
- package/dist/adapters/audio/openai.d.ts +36 -0
- package/dist/adapters/audio/openai.js +136 -0
- package/dist/adapters/bullmq/lock.d.ts +26 -2
- package/dist/adapters/embeddings/openai.d.ts +45 -0
- package/dist/adapters/embeddings/openai.js +109 -0
- package/dist/adapters/memory/connections.d.ts +10 -0
- package/dist/adapters/memory/connections.js +87 -0
- package/dist/adapters/memory/graph.d.ts +17 -0
- package/dist/adapters/memory/graph.js +267 -0
- package/dist/adapters/memory/index.d.ts +3 -0
- package/dist/adapters/memory/index.js +3 -0
- package/dist/adapters/memory/rate-limit.d.ts +14 -0
- package/dist/adapters/memory/rate-limit.js +28 -0
- package/dist/adapters/memory/runtime.js +0 -0
- package/dist/adapters/postgres/connections.d.ts +15 -0
- package/dist/adapters/postgres/connections.js +134 -0
- package/dist/adapters/postgres/graph.d.ts +24 -0
- package/dist/adapters/postgres/graph.js +325 -0
- package/dist/adapters/postgres/index.d.ts +2 -0
- package/dist/adapters/postgres/index.js +2 -0
- package/dist/adapters/postgres/migrations.js +198 -0
- package/dist/adapters/postgres/run-store.js +26 -2
- package/dist/adapters/postgres/schema.d.ts +61 -2
- package/dist/adapters/postgres/schema.js +95 -10
- package/dist/adapters/redis/index.d.ts +1 -0
- package/dist/adapters/redis/index.js +1 -0
- package/dist/adapters/redis/rate-limit.d.ts +46 -0
- package/dist/adapters/redis/rate-limit.js +55 -0
- package/dist/adapters/supabase/index.d.ts +15 -0
- package/dist/adapters/supabase/index.js +15 -0
- package/dist/adapters/supabase/rls.js +33 -0
- package/dist/agents/agent.d.ts +99 -1
- package/dist/agents/agent.js +215 -16
- package/dist/agents/define.js +24 -4
- package/dist/agents/engine.d.ts +48 -0
- package/dist/agents/engine.js +370 -13
- package/dist/audio/index.d.ts +151 -0
- package/dist/audio/index.js +183 -0
- package/dist/capabilities/index.d.ts +5 -1
- package/dist/capabilities/index.js +23 -0
- package/dist/capabilities/runtime.d.ts +8 -0
- package/dist/connections/cipher.d.ts +103 -0
- package/dist/connections/cipher.js +141 -0
- package/dist/connections/index.d.ts +156 -0
- package/dist/connections/index.js +72 -0
- package/dist/connections/oauth/client.d.ts +84 -0
- package/dist/connections/oauth/client.js +141 -0
- package/dist/connections/oauth/index.d.ts +166 -0
- package/dist/connections/oauth/index.js +226 -0
- package/dist/connections/oauth/service.d.ts +90 -0
- package/dist/connections/oauth/service.js +132 -0
- package/dist/connections/pause.d.ts +68 -0
- package/dist/connections/pause.js +97 -0
- package/dist/connections/resolver.d.ts +59 -0
- package/dist/connections/resolver.js +138 -0
- package/dist/connections/resume.d.ts +64 -0
- package/dist/connections/resume.js +99 -0
- package/dist/connections/vault-cipher.d.ts +95 -0
- package/dist/connections/vault-cipher.js +206 -0
- package/dist/core/budget.d.ts +55 -0
- package/dist/core/budget.js +56 -0
- package/dist/core/content-parts.d.ts +26 -2
- package/dist/core/content-parts.js +1 -0
- package/dist/core/context.d.ts +25 -0
- package/dist/core/errors.d.ts +1 -1
- package/dist/core/errors.js +15 -0
- package/dist/core/events.d.ts +110 -2
- package/dist/core/events.js +33 -2
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/validation.js +19 -0
- package/dist/documents/index.d.ts +14 -0
- package/dist/documents/parsers/text.d.ts +16 -0
- package/dist/documents/parsers/text.js +54 -2
- package/dist/entries/connections.d.ts +10 -0
- package/dist/entries/connections.js +10 -0
- package/dist/entries/guardrails.d.ts +14 -0
- package/dist/entries/guardrails.js +14 -0
- package/dist/entries/knowledge.d.ts +17 -0
- package/dist/entries/knowledge.js +16 -0
- package/dist/entries/mcp-server.d.ts +9 -0
- package/dist/entries/mcp-server.js +9 -0
- package/dist/entries/runtime.d.ts +10 -0
- package/dist/entries/runtime.js +8 -0
- package/dist/entries/testing.d.ts +28 -0
- package/dist/entries/testing.js +28 -0
- package/dist/graphql/resolvers.d.ts +7 -1
- package/dist/graphql/resolvers.js +20 -0
- package/dist/graphql/schema.d.ts +1 -1
- package/dist/graphql/schema.js +44 -0
- package/dist/guardrails/index.d.ts +115 -0
- package/dist/guardrails/index.js +108 -0
- package/dist/guardrails/moderation.d.ts +53 -0
- package/dist/guardrails/moderation.js +75 -0
- package/dist/guardrails/pii.d.ts +75 -0
- package/dist/guardrails/pii.js +193 -0
- package/dist/knowledge/communities.d.ts +166 -0
- package/dist/knowledge/communities.js +377 -0
- package/dist/knowledge/graph-global.d.ts +153 -0
- package/dist/knowledge/graph-global.js +246 -0
- package/dist/knowledge/graph-retrieval.d.ts +150 -0
- package/dist/knowledge/graph-retrieval.js +303 -0
- package/dist/knowledge/graph.d.ts +223 -0
- package/dist/knowledge/graph.js +369 -0
- package/dist/knowledge/index.d.ts +25 -0
- package/dist/knowledge/index.js +37 -1
- package/dist/knowledge/navigate.d.ts +89 -0
- package/dist/knowledge/navigate.js +107 -0
- package/dist/knowledge/retrieval.d.ts +98 -5
- package/dist/knowledge/retrieval.js +158 -28
- package/dist/mcp-server/index.d.ts +168 -0
- package/dist/mcp-server/index.js +175 -0
- package/dist/models/extraction.d.ts +73 -0
- package/dist/models/extraction.js +118 -0
- package/dist/models/index.d.ts +40 -1
- package/dist/models/index.js +22 -1
- package/dist/models/pricing.d.ts +2 -0
- package/dist/models/pricing.js +17 -1
- package/dist/models/provider-factory.js +9 -7
- package/dist/models/streaming.d.ts +111 -1
- package/dist/models/streaming.js +184 -3
- package/dist/persistence/index.d.ts +230 -0
- package/dist/persistence/index.js +2 -0
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +12 -0
- package/dist/runtime/worker.d.ts +36 -1
- package/dist/runtime/worker.js +37 -4
- package/dist/security/checklist.js +55 -0
- package/dist/security/findings.js +18 -9
- package/dist/server/bin.d.ts +19 -0
- package/dist/server/bin.js +226 -0
- package/dist/server/boot.d.ts +20 -2
- package/dist/server/boot.js +29 -3
- package/dist/server/cli.d.ts +13 -0
- package/dist/server/cli.js +7 -4
- package/dist/server/doctor.d.ts +74 -0
- package/dist/server/doctor.js +280 -0
- package/dist/skills/catalogue.d.ts +49 -0
- package/dist/skills/catalogue.js +61 -0
- package/dist/skills/context.d.ts +61 -0
- package/dist/skills/context.js +107 -0
- package/dist/skills/index.d.ts +2 -0
- package/dist/skills/index.js +2 -0
- package/dist/telemetry/spans.js +17 -0
- package/dist/testing/conformance/artifact-exports.d.ts +20 -0
- package/dist/testing/conformance/artifact-exports.js +172 -0
- package/dist/testing/conformance/artifacts.d.ts +11 -0
- package/dist/testing/conformance/artifacts.js +316 -0
- package/dist/testing/conformance/audio.d.ts +38 -0
- package/dist/testing/conformance/audio.js +145 -0
- package/dist/testing/conformance/capability.d.ts +23 -0
- package/dist/testing/conformance/capability.js +27 -0
- package/dist/testing/conformance/checkpoint-store.d.ts +15 -0
- package/dist/testing/conformance/checkpoint-store.js +72 -0
- package/dist/testing/conformance/connections.d.ts +14 -0
- package/dist/testing/conformance/connections.js +171 -0
- package/dist/testing/conformance/conversation-store.d.ts +10 -0
- package/dist/testing/conformance/conversation-store.js +60 -0
- package/dist/testing/conformance/evaluation.d.ts +10 -0
- package/dist/testing/conformance/evaluation.js +295 -0
- package/dist/testing/conformance/files.d.ts +19 -0
- package/dist/testing/conformance/files.js +454 -0
- package/dist/testing/conformance/flows.d.ts +16 -0
- package/dist/testing/conformance/flows.js +193 -0
- package/dist/testing/conformance/graph.d.ts +22 -0
- package/dist/testing/conformance/graph.js +500 -0
- package/dist/testing/conformance/hitl.d.ts +25 -0
- package/dist/testing/conformance/hitl.js +523 -0
- package/dist/testing/conformance/index.d.ts +163 -0
- package/dist/testing/conformance/index.js +321 -0
- package/dist/testing/conformance/invariants.d.ts +23 -0
- package/dist/testing/conformance/invariants.js +80 -0
- package/dist/testing/conformance/knowledge.d.ts +41 -0
- package/dist/testing/conformance/knowledge.js +592 -0
- package/dist/testing/conformance/parents.d.ts +50 -0
- package/dist/testing/conformance/parents.js +39 -0
- package/dist/testing/conformance/rate-limit.d.ts +25 -0
- package/dist/testing/conformance/rate-limit.js +71 -0
- package/dist/testing/conformance/records.d.ts +36 -0
- package/dist/testing/conformance/records.js +400 -0
- package/dist/testing/conformance/rollups.d.ts +30 -0
- package/dist/testing/conformance/rollups.js +425 -0
- package/dist/testing/conformance/run-coordinator.d.ts +27 -0
- package/dist/testing/conformance/run-coordinator.js +150 -0
- package/dist/testing/conformance/run-event-log.d.ts +9 -0
- package/dist/testing/conformance/run-event-log.js +138 -0
- package/dist/testing/conformance/run-store.d.ts +12 -0
- package/dist/testing/conformance/run-store.js +294 -0
- package/dist/testing/conformance/session-state.d.ts +38 -0
- package/dist/testing/conformance/session-state.js +198 -0
- package/dist/testing/conformance/usage-limits.d.ts +10 -0
- package/dist/testing/conformance/usage-limits.js +309 -0
- package/dist/testing/conformance.d.ts +8 -0
- package/dist/testing/conformance.js +8 -0
- package/dist/testing/memory-backend.d.ts +39 -0
- package/dist/testing/memory-backend.js +41 -0
- package/dist/testing/pglite.d.ts +55 -0
- package/dist/testing/pglite.js +78 -0
- package/dist/testing/stub-model.d.ts +58 -0
- package/dist/testing/stub-model.js +71 -0
- package/dist/testing/supabase-storage-double.d.ts +35 -0
- package/dist/testing/supabase-storage-double.js +101 -0
- package/dist/toolkit/files.d.ts +125 -0
- package/dist/toolkit/files.js +320 -0
- package/dist/toolkit/http.d.ts +27 -0
- package/dist/toolkit/http.js +48 -0
- package/dist/toolkit/index.d.ts +31 -1
- package/dist/toolkit/index.js +26 -1
- package/dist/toolkit/mime.d.ts +97 -0
- package/dist/toolkit/mime.js +233 -0
- package/dist/toolkit/sandbox.d.ts +119 -0
- package/dist/toolkit/sandbox.js +239 -0
- package/dist/toolkit/ssrf.d.ts +139 -0
- package/dist/toolkit/ssrf.js +349 -0
- package/dist/toolkit/vendor.d.ts +91 -0
- package/dist/toolkit/vendor.js +153 -0
- package/dist/toolkit/web.d.ts +13 -0
- package/dist/toolkit/web.js +7 -1
- package/dist/tools/budget.d.ts +28 -0
- package/dist/tools/budget.js +35 -0
- package/dist/tools/credentials.d.ts +316 -0
- package/dist/tools/credentials.js +369 -0
- package/dist/tools/define.d.ts +45 -0
- package/dist/tools/define.js +26 -0
- package/dist/tools/find.d.ts +109 -0
- package/dist/tools/find.js +210 -0
- package/dist/tools/index.d.ts +35 -2
- package/dist/tools/index.js +11 -0
- package/dist/tools/library/audio.d.ts +63 -0
- package/dist/tools/library/audio.js +148 -0
- package/dist/tools/library/fs.d.ts +24 -0
- package/dist/tools/library/fs.js +102 -0
- package/dist/tools/library/index.d.ts +37 -2
- package/dist/tools/library/index.js +47 -0
- package/dist/tools/library/shell.d.ts +45 -0
- package/dist/tools/library/shell.js +70 -0
- package/dist/tools/meta-tools.js +8 -0
- package/dist/tools/registry.d.ts +146 -0
- package/dist/tools/registry.js +250 -4
- package/dist/usage/index.d.ts +21 -0
- package/dist/usage/index.js +1 -0
- package/dist/usage/rate-limit.d.ts +122 -0
- package/dist/usage/rate-limit.js +131 -0
- package/dist/usage/recorder.js +3 -0
- package/package.json +33 -2
package/dist/runtime/index.js
CHANGED
|
@@ -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: [],
|
package/dist/runtime/worker.d.ts
CHANGED
|
@@ -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
|
-
|
|
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. */
|
package/dist/runtime/worker.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
386
|
-
|
|
387
|
-
|
|
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: "
|
|
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
|
-
"
|
|
140
|
-
"
|
|
141
|
-
"
|
|
142
|
-
|
|
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: "
|
|
147
|
-
"
|
|
148
|
-
"
|
|
149
|
-
|
|
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
|
package/dist/server/boot.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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[];
|
package/dist/server/boot.js
CHANGED
|
@@ -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 {
|
|
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
|
package/dist/server/cli.d.ts
CHANGED
|
@@ -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) => {
|