@retinue/agentkit 0.3.1 → 0.3.2
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/dist/server/pool.d.ts +35 -9
- package/dist/server/pool.js +63 -34
- package/package.json +1 -1
package/dist/server/pool.d.ts
CHANGED
|
@@ -7,22 +7,35 @@
|
|
|
7
7
|
* the ones that must land in the same schema as everything else.
|
|
8
8
|
*
|
|
9
9
|
* **What it adds over `new Pool`.** `RETINUE_DATABASE_SCHEMA` names the schema the platform's tables
|
|
10
|
-
* live in, and honouring it takes
|
|
10
|
+
* live in, and honouring it takes three things:
|
|
11
11
|
*
|
|
12
|
-
* - `
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
12
|
+
* - `options: "-c search_path=…"`, a **startup parameter**, so every connection has it before it runs
|
|
13
|
+
* a single statement. This matters because `createPgExecutor` runs each query through `pool.query`,
|
|
14
|
+
* which takes a different connection per call and hands back one carrying whatever `search_path` its
|
|
15
|
+
* last borrower left.
|
|
16
|
+
*
|
|
17
|
+
* The first version of this did it with `pool.on("connect", (c) => c.query("SET search_path …"))`,
|
|
18
|
+
* node-postgres's documented idiom for session state. It worked, and pg deprecated it while this was
|
|
19
|
+
* being written: *"Calling client.query() when the client is already executing a query is deprecated
|
|
20
|
+
* and will be removed in pg@9.0"* — printed on every boot. A startup parameter needs no query at
|
|
21
|
+
* all, so the ordering question disappears rather than being answered.
|
|
22
|
+
*
|
|
23
|
+
* Verified against a real pooler before relying on it: Supabase's Supavisor forwards `options`
|
|
24
|
+
* on the session port, both in the config and in the URL's query string. That was the open
|
|
25
|
+
* question, since PgBouncer historically rejects unknown startup parameters.
|
|
26
|
+
* - `assertSearchPath`, one query at startup, because the mechanism above is the kind that fails
|
|
27
|
+
* silently. A pooler that swallowed `options` would leave every connection on the default path and
|
|
28
|
+
* every write in the wrong schema, with nothing to see. One round-trip turns that into a refusal.
|
|
29
|
+
* - `createPoolOpener(pool, …)` sets it per checkout as well, which is what the transaction scope
|
|
30
|
+
* uses. Belt and braces, deliberately: the opener is the path that holds `SELECT … FOR UPDATE`
|
|
31
|
+
* across statements, and it is worth its own guarantee.
|
|
20
32
|
*
|
|
21
33
|
* **`public` stays on the path** after the named schema. Two things need it: the `vector` type is
|
|
22
34
|
* pinned to `public` so it resolves from any schema (see the note in `migrations.ts`), and ShareFlow's
|
|
23
35
|
* adapters qualify all 70 of their queries as `public.`, which is what lets one pool serve a platform
|
|
24
36
|
* schema and a product schema at once.
|
|
25
37
|
*/
|
|
38
|
+
import type { Pool } from "pg";
|
|
26
39
|
import type { SqlExecutor } from "../adapters/postgres/sql.js";
|
|
27
40
|
import type { ConnectionOpener } from "../adapters/postgres/transaction.js";
|
|
28
41
|
export type PostgresConnection = {
|
|
@@ -43,5 +56,18 @@ export type PoolSettings = {
|
|
|
43
56
|
* cheaper than reading two call sites to find out what a deployment actually gets.
|
|
44
57
|
*/
|
|
45
58
|
export declare const searchPathFor: (schema: string | undefined) => string | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* One query, to prove the startup parameter actually took effect.
|
|
61
|
+
*
|
|
62
|
+
* Without it the mechanism is silent when it fails. A pooler that dropped `options` — PgBouncer
|
|
63
|
+
* rejects unknown startup parameters by default, and a managed pooler can change behaviour under you —
|
|
64
|
+
* would leave every connection on the default `search_path`, and every table the platform creates
|
|
65
|
+
* would land in whatever schema comes first there. No error, no log line, and the symptom is two
|
|
66
|
+
* projects quietly sharing a namespace.
|
|
67
|
+
*
|
|
68
|
+
* Compared as a set rather than as a string: Postgres echoes what it was given, and `retinue, public`
|
|
69
|
+
* is the same path as `retinue,public` while being a different string.
|
|
70
|
+
*/
|
|
71
|
+
export declare const assertSearchPath: (pool: Pool, expected: string) => Promise<void>;
|
|
46
72
|
export declare const openPostgres: (settings: PoolSettings) => Promise<PostgresConnection>;
|
|
47
73
|
//# sourceMappingURL=pool.d.ts.map
|
package/dist/server/pool.js
CHANGED
|
@@ -7,16 +7,28 @@
|
|
|
7
7
|
* the ones that must land in the same schema as everything else.
|
|
8
8
|
*
|
|
9
9
|
* **What it adds over `new Pool`.** `RETINUE_DATABASE_SCHEMA` names the schema the platform's tables
|
|
10
|
-
* live in, and honouring it takes
|
|
10
|
+
* live in, and honouring it takes three things:
|
|
11
11
|
*
|
|
12
|
-
* - `
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
12
|
+
* - `options: "-c search_path=…"`, a **startup parameter**, so every connection has it before it runs
|
|
13
|
+
* a single statement. This matters because `createPgExecutor` runs each query through `pool.query`,
|
|
14
|
+
* which takes a different connection per call and hands back one carrying whatever `search_path` its
|
|
15
|
+
* last borrower left.
|
|
16
|
+
*
|
|
17
|
+
* The first version of this did it with `pool.on("connect", (c) => c.query("SET search_path …"))`,
|
|
18
|
+
* node-postgres's documented idiom for session state. It worked, and pg deprecated it while this was
|
|
19
|
+
* being written: *"Calling client.query() when the client is already executing a query is deprecated
|
|
20
|
+
* and will be removed in pg@9.0"* — printed on every boot. A startup parameter needs no query at
|
|
21
|
+
* all, so the ordering question disappears rather than being answered.
|
|
22
|
+
*
|
|
23
|
+
* Verified against a real pooler before relying on it: Supabase's Supavisor forwards `options`
|
|
24
|
+
* on the session port, both in the config and in the URL's query string. That was the open
|
|
25
|
+
* question, since PgBouncer historically rejects unknown startup parameters.
|
|
26
|
+
* - `assertSearchPath`, one query at startup, because the mechanism above is the kind that fails
|
|
27
|
+
* silently. A pooler that swallowed `options` would leave every connection on the default path and
|
|
28
|
+
* every write in the wrong schema, with nothing to see. One round-trip turns that into a refusal.
|
|
29
|
+
* - `createPoolOpener(pool, …)` sets it per checkout as well, which is what the transaction scope
|
|
30
|
+
* uses. Belt and braces, deliberately: the opener is the path that holds `SELECT … FOR UPDATE`
|
|
31
|
+
* across statements, and it is worth its own guarantee.
|
|
20
32
|
*
|
|
21
33
|
* **`public` stays on the path** after the named schema. Two things need it: the `vector` type is
|
|
22
34
|
* pinned to `public` so it resolves from any schema (see the note in `migrations.ts`), and ShareFlow's
|
|
@@ -29,41 +41,58 @@
|
|
|
29
41
|
* Exported because it is the only string in this file that ends up in SQL, and a test that pins it is
|
|
30
42
|
* cheaper than reading two call sites to find out what a deployment actually gets.
|
|
31
43
|
*/
|
|
32
|
-
export const searchPathFor = (schema) =>
|
|
44
|
+
export const searchPathFor = (schema) =>
|
|
45
|
+
// **No space after the comma**, and that is not a style choice. This string is passed as libpq's
|
|
46
|
+
// `-c search_path=…`, where a space separates one option from the next: `-c search_path=retinue,
|
|
47
|
+
// public` reaches Postgres as `search_path` = `retinue,` and a stray `public`, and the server
|
|
48
|
+
// refuses it outright — `invalid value for parameter "search_path": "retinue,"`. Found by running
|
|
49
|
+
// `migrate` against a real database, not by reading the code. `SET search_path TO retinue,public` is
|
|
50
|
+
// equally valid, so one representation serves both uses.
|
|
51
|
+
schema === undefined || schema === "" ? undefined : `${schema},public`;
|
|
52
|
+
/**
|
|
53
|
+
* One query, to prove the startup parameter actually took effect.
|
|
54
|
+
*
|
|
55
|
+
* Without it the mechanism is silent when it fails. A pooler that dropped `options` — PgBouncer
|
|
56
|
+
* rejects unknown startup parameters by default, and a managed pooler can change behaviour under you —
|
|
57
|
+
* would leave every connection on the default `search_path`, and every table the platform creates
|
|
58
|
+
* would land in whatever schema comes first there. No error, no log line, and the symptom is two
|
|
59
|
+
* projects quietly sharing a namespace.
|
|
60
|
+
*
|
|
61
|
+
* Compared as a set rather than as a string: Postgres echoes what it was given, and `retinue, public`
|
|
62
|
+
* is the same path as `retinue,public` while being a different string.
|
|
63
|
+
*/
|
|
64
|
+
export const assertSearchPath = async (pool, expected) => {
|
|
65
|
+
const normalise = (value) => value
|
|
66
|
+
.split(",")
|
|
67
|
+
.map((part) => part.trim().replace(/^"|"$/g, ""))
|
|
68
|
+
.filter((part) => part !== "");
|
|
69
|
+
const rows = await pool.query("show search_path");
|
|
70
|
+
const actual = normalise(rows.rows[0]?.search_path ?? "");
|
|
71
|
+
const wanted = normalise(expected);
|
|
72
|
+
const matches = wanted.length === actual.length && wanted.every((part, index) => part === actual[index]);
|
|
73
|
+
if (!matches) {
|
|
74
|
+
await pool.end().catch(() => undefined);
|
|
75
|
+
throw new Error(`RETINUE_DATABASE_SCHEMA asked for search_path "${expected}" but this connection reports ` +
|
|
76
|
+
`"${rows.rows[0]?.search_path ?? "(nothing)"}". The connection options were not applied — a ` +
|
|
77
|
+
`pooler in front of Postgres may be dropping them. Refusing to start, because every table this ` +
|
|
78
|
+
`process creates would otherwise land in the wrong schema with no error.`);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
33
81
|
export const openPostgres = async (settings) => {
|
|
34
82
|
const { Pool } = await import("pg");
|
|
35
83
|
const { createPgExecutor, createPoolOpener } = await import("../entries/adapters-postgres.js");
|
|
36
84
|
const searchPath = searchPathFor(settings.databaseSchema);
|
|
37
85
|
const pool = new Pool({
|
|
38
86
|
connectionString: settings.databaseUrl,
|
|
87
|
+
// The startup parameter, applied by the server before the connection is usable. `-c key=value` is
|
|
88
|
+
// libpq's form and node-postgres passes it straight through.
|
|
89
|
+
...(searchPath === undefined ? {} : { options: `-c search_path=${searchPath}` }),
|
|
39
90
|
...(settings.connectionTimeoutMillis === undefined
|
|
40
91
|
? {}
|
|
41
92
|
: { connectionTimeoutMillis: settings.connectionTimeoutMillis }),
|
|
42
93
|
});
|
|
43
|
-
if (searchPath !== undefined)
|
|
44
|
-
|
|
45
|
-
* Queued on the client, not awaited — which is what makes it correct rather than racy.
|
|
46
|
-
*
|
|
47
|
-
* node-postgres queues queries per client in order, so this `SET` is ahead of whatever the borrower
|
|
48
|
-
* runs next on that same connection. An `await` here would have nothing to attach to: `connect` is
|
|
49
|
-
* an event, and the pool hands the client out regardless of what a listener is still doing.
|
|
50
|
-
*
|
|
51
|
-
* A failure is surfaced rather than swallowed, and it is worth being precise about what it can and
|
|
52
|
-
* cannot catch. It means the role may not *use* the schema. It does **not** mean the schema is
|
|
53
|
-
* missing: `SET search_path TO retinue, public` succeeds when `retinue` does not exist, because a
|
|
54
|
-
* missing entry is skipped rather than rejected, and every write then lands in `public` with no
|
|
55
|
-
* error anywhere. Nothing at this layer can see that — which is why `retinue migrate` creates the
|
|
56
|
-
* schema before anything connects, and why this listener is not the guard against it.
|
|
57
|
-
*/
|
|
58
|
-
pool.on("connect", (client) => {
|
|
59
|
-
void client.query(`SET search_path TO ${searchPath}`).catch((error) => {
|
|
60
|
-
client.end();
|
|
61
|
-
pool.emit("error", error instanceof Error
|
|
62
|
-
? new Error(`could not SET search_path TO ${searchPath}: ${error.message}`, { cause: error })
|
|
63
|
-
: new Error(`could not SET search_path TO ${searchPath}`), client);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
}
|
|
94
|
+
if (searchPath !== undefined)
|
|
95
|
+
await assertSearchPath(pool, searchPath);
|
|
67
96
|
return {
|
|
68
97
|
sql: createPgExecutor(pool),
|
|
69
98
|
open: createPoolOpener(pool, searchPath),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retinue/agentkit",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "A provider-neutral, durable AI agent runtime for TypeScript: agents, tools, approvals, context, knowledge and persistence behind ports.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|