@intx/db 0.1.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/README.md +37 -0
- package/drizzle.config.ts +23 -0
- package/migrations/.gitkeep +0 -0
- package/migrations/0000_brown_wither.sql +50 -0
- package/migrations/0001_white_aqueduct.sql +105 -0
- package/migrations/0002_clever_falcon.sql +56 -0
- package/migrations/0003_stiff_tyrannus.sql +44 -0
- package/migrations/0004_rename_capability_to_offering.sql +1 -0
- package/migrations/0005_gigantic_cardiac.sql +1 -0
- package/migrations/0006_sidecar.sql +8 -0
- package/migrations/0007_agent_running_session.sql +1 -0
- package/migrations/0008_session.sql +10 -0
- package/migrations/0009_session_messages.sql +18 -0
- package/migrations/0010_agent_sidecar_pubkey.sql +2 -0
- package/migrations/0011_session_message_from.sql +2 -0
- package/migrations/0012_agent_instance.sql +22 -0
- package/migrations/0013_instance_session_id.sql +2 -0
- package/migrations/0014_drop_agent_runtime_columns.sql +12 -0
- package/migrations/0015_add_instance_id_to_session_message.sql +2 -0
- package/migrations/0016_jazzy_gamma_corps.sql +3 -0
- package/migrations/0017_hesitant_marvex.sql +1 -0
- package/migrations/0018_natural_sinister_six.sql +5 -0
- package/migrations/0019_rename_grant_source_to_origin.sql +1 -0
- package/migrations/0020_add_agent_role.sql +9 -0
- package/migrations/0021_acoustic_ozymandias.sql +15 -0
- package/migrations/0022_material_sleepwalker.sql +27 -0
- package/migrations/0023_flawless_scarlet_witch.sql +2 -0
- package/migrations/0024_bumpy_sharon_ventura.sql +1 -0
- package/migrations/0025_curvy_firestar.sql +26 -0
- package/migrations/0026_keen_ultimo.sql +13 -0
- package/migrations/0027_git_tokens.sql +21 -0
- package/migrations/0028_wet_sugar_man.sql +1 -0
- package/migrations/meta/0000_snapshot.json +316 -0
- package/migrations/meta/0001_snapshot.json +968 -0
- package/migrations/meta/0002_snapshot.json +1315 -0
- package/migrations/meta/0003_snapshot.json +1594 -0
- package/migrations/meta/0004_snapshot.json +1594 -0
- package/migrations/meta/0005_snapshot.json +1600 -0
- package/migrations/meta/0011_snapshot.json +1921 -0
- package/migrations/meta/0012_snapshot.json +2067 -0
- package/migrations/meta/0013_snapshot.json +2082 -0
- package/migrations/meta/0014_snapshot.json +2049 -0
- package/migrations/meta/0015_snapshot.json +2064 -0
- package/migrations/meta/0016_snapshot.json +2085 -0
- package/migrations/meta/0017_snapshot.json +2085 -0
- package/migrations/meta/0018_snapshot.json +2070 -0
- package/migrations/meta/0019_snapshot.json +2070 -0
- package/migrations/meta/0020_snapshot.json +2126 -0
- package/migrations/meta/0021_snapshot.json +2239 -0
- package/migrations/meta/0022_snapshot.json +2425 -0
- package/migrations/meta/0023_snapshot.json +2260 -0
- package/migrations/meta/0024_snapshot.json +2254 -0
- package/migrations/meta/0025_snapshot.json +2418 -0
- package/migrations/meta/0026_snapshot.json +2508 -0
- package/migrations/meta/0027_snapshot.json +2657 -0
- package/migrations/meta/0028_snapshot.json +2657 -0
- package/migrations/meta/_journal.json +209 -0
- package/package.json +27 -0
- package/src/client.ts +24 -0
- package/src/config.ts +19 -0
- package/src/connection.ts +27 -0
- package/src/credential-resolution.ts +378 -0
- package/src/grant-store.ts +51 -0
- package/src/index.ts +32 -0
- package/src/migrate.test.ts +35 -0
- package/src/migrate.ts +168 -0
- package/src/parse-row.test.ts +113 -0
- package/src/parse-row.ts +185 -0
- package/src/schema/agent-assets.ts +21 -0
- package/src/schema/agents.ts +49 -0
- package/src/schema/assets.ts +24 -0
- package/src/schema/auth.ts +51 -0
- package/src/schema/credentials.ts +43 -0
- package/src/schema/git-tokens.ts +83 -0
- package/src/schema/grants.ts +26 -0
- package/src/schema/index.ts +19 -0
- package/src/schema/instances.ts +36 -0
- package/src/schema/messages.ts +108 -0
- package/src/schema/oauth-clients.ts +26 -0
- package/src/schema/offerings.ts +20 -0
- package/src/schema/principals.ts +28 -0
- package/src/schema/providers.ts +23 -0
- package/src/schema/roles.ts +51 -0
- package/src/schema/session-assets.ts +49 -0
- package/src/schema/sessions.ts +26 -0
- package/src/schema/sidecar.ts +14 -0
- package/src/schema/tenants.ts +41 -0
- package/src/schema/wallets.ts +44 -0
- package/src/tenant-hierarchy.ts +34 -0
- package/tsconfig.json +4 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "7",
|
|
3
|
+
"dialect": "postgresql",
|
|
4
|
+
"entries": [
|
|
5
|
+
{
|
|
6
|
+
"idx": 0,
|
|
7
|
+
"version": "7",
|
|
8
|
+
"when": 1771377177003,
|
|
9
|
+
"tag": "0000_brown_wither",
|
|
10
|
+
"breakpoints": true
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"idx": 1,
|
|
14
|
+
"version": "7",
|
|
15
|
+
"when": 1771454692193,
|
|
16
|
+
"tag": "0001_white_aqueduct",
|
|
17
|
+
"breakpoints": true
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"idx": 2,
|
|
21
|
+
"version": "7",
|
|
22
|
+
"when": 1771459003764,
|
|
23
|
+
"tag": "0002_clever_falcon",
|
|
24
|
+
"breakpoints": true
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"idx": 3,
|
|
28
|
+
"version": "7",
|
|
29
|
+
"when": 1771541707112,
|
|
30
|
+
"tag": "0003_stiff_tyrannus",
|
|
31
|
+
"breakpoints": true
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"idx": 4,
|
|
35
|
+
"version": "7",
|
|
36
|
+
"when": 1771630000000,
|
|
37
|
+
"tag": "0004_rename_capability_to_offering",
|
|
38
|
+
"breakpoints": true
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"idx": 5,
|
|
42
|
+
"version": "7",
|
|
43
|
+
"when": 1771630000000,
|
|
44
|
+
"tag": "0005_gigantic_cardiac",
|
|
45
|
+
"breakpoints": true
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"idx": 6,
|
|
49
|
+
"version": "7",
|
|
50
|
+
"when": 1771630000001,
|
|
51
|
+
"tag": "0006_sidecar",
|
|
52
|
+
"breakpoints": true
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"idx": 7,
|
|
56
|
+
"version": "7",
|
|
57
|
+
"when": 1771630000002,
|
|
58
|
+
"tag": "0007_agent_running_session",
|
|
59
|
+
"breakpoints": true
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"idx": 8,
|
|
63
|
+
"version": "7",
|
|
64
|
+
"when": 1771630000003,
|
|
65
|
+
"tag": "0008_session",
|
|
66
|
+
"breakpoints": true
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"idx": 9,
|
|
70
|
+
"version": "7",
|
|
71
|
+
"when": 1771630000004,
|
|
72
|
+
"tag": "0009_session_messages",
|
|
73
|
+
"breakpoints": true
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"idx": 10,
|
|
77
|
+
"version": "7",
|
|
78
|
+
"when": 1771630000005,
|
|
79
|
+
"tag": "0010_agent_sidecar_pubkey",
|
|
80
|
+
"breakpoints": true
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"idx": 11,
|
|
84
|
+
"version": "7",
|
|
85
|
+
"when": 1776890951476,
|
|
86
|
+
"tag": "0011_session_message_from",
|
|
87
|
+
"breakpoints": true
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"idx": 12,
|
|
91
|
+
"version": "7",
|
|
92
|
+
"when": 1776909616212,
|
|
93
|
+
"tag": "0012_agent_instance",
|
|
94
|
+
"breakpoints": true
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"idx": 13,
|
|
98
|
+
"version": "7",
|
|
99
|
+
"when": 1776917900630,
|
|
100
|
+
"tag": "0013_instance_session_id",
|
|
101
|
+
"breakpoints": true
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"idx": 14,
|
|
105
|
+
"version": "7",
|
|
106
|
+
"when": 1776921400366,
|
|
107
|
+
"tag": "0014_drop_agent_runtime_columns",
|
|
108
|
+
"breakpoints": true
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"idx": 15,
|
|
112
|
+
"version": "7",
|
|
113
|
+
"when": 1776930080691,
|
|
114
|
+
"tag": "0015_add_instance_id_to_session_message",
|
|
115
|
+
"breakpoints": true
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"idx": 16,
|
|
119
|
+
"version": "7",
|
|
120
|
+
"when": 1777049455797,
|
|
121
|
+
"tag": "0016_jazzy_gamma_corps",
|
|
122
|
+
"breakpoints": true
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"idx": 17,
|
|
126
|
+
"version": "7",
|
|
127
|
+
"when": 1777050475351,
|
|
128
|
+
"tag": "0017_hesitant_marvex",
|
|
129
|
+
"breakpoints": true
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"idx": 18,
|
|
133
|
+
"version": "7",
|
|
134
|
+
"when": 1777053065460,
|
|
135
|
+
"tag": "0018_natural_sinister_six",
|
|
136
|
+
"breakpoints": true
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"idx": 19,
|
|
140
|
+
"version": "7",
|
|
141
|
+
"when": 1777180800000,
|
|
142
|
+
"tag": "0019_rename_grant_source_to_origin",
|
|
143
|
+
"breakpoints": true
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"idx": 20,
|
|
147
|
+
"version": "7",
|
|
148
|
+
"when": 1777085874297,
|
|
149
|
+
"tag": "0020_add_agent_role",
|
|
150
|
+
"breakpoints": true
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"idx": 21,
|
|
154
|
+
"version": "7",
|
|
155
|
+
"when": 1777228941707,
|
|
156
|
+
"tag": "0021_acoustic_ozymandias",
|
|
157
|
+
"breakpoints": true
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"idx": 22,
|
|
161
|
+
"version": "7",
|
|
162
|
+
"when": 1777229396694,
|
|
163
|
+
"tag": "0022_material_sleepwalker",
|
|
164
|
+
"breakpoints": true
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"idx": 23,
|
|
168
|
+
"version": "7",
|
|
169
|
+
"when": 1777235039207,
|
|
170
|
+
"tag": "0023_flawless_scarlet_witch",
|
|
171
|
+
"breakpoints": true
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"idx": 24,
|
|
175
|
+
"version": "7",
|
|
176
|
+
"when": 1780344351218,
|
|
177
|
+
"tag": "0024_bumpy_sharon_ventura",
|
|
178
|
+
"breakpoints": true
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"idx": 25,
|
|
182
|
+
"version": "7",
|
|
183
|
+
"when": 1780357614241,
|
|
184
|
+
"tag": "0025_curvy_firestar",
|
|
185
|
+
"breakpoints": true
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"idx": 26,
|
|
189
|
+
"version": "7",
|
|
190
|
+
"when": 1780358149943,
|
|
191
|
+
"tag": "0026_keen_ultimo",
|
|
192
|
+
"breakpoints": true
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"idx": 27,
|
|
196
|
+
"version": "7",
|
|
197
|
+
"when": 1780377246076,
|
|
198
|
+
"tag": "0027_git_tokens",
|
|
199
|
+
"breakpoints": true
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"idx": 28,
|
|
203
|
+
"version": "7",
|
|
204
|
+
"when": 1780502952568,
|
|
205
|
+
"tag": "0028_wet_sugar_man",
|
|
206
|
+
"breakpoints": true
|
|
207
|
+
}
|
|
208
|
+
]
|
|
209
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@intx/db",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"license": "LGPL-2.1-only",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./src/index.ts",
|
|
9
|
+
"default": "./src/index.ts"
|
|
10
|
+
},
|
|
11
|
+
"./schema": {
|
|
12
|
+
"types": "./src/schema/index.ts",
|
|
13
|
+
"default": "./src/schema/index.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"db:generate": "drizzle-kit generate --config=drizzle.config.ts",
|
|
18
|
+
"db:migrate": "drizzle-kit migrate --config=drizzle.config.ts"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@intx/log": "0.0.0",
|
|
22
|
+
"@intx/types": "0.0.0",
|
|
23
|
+
"arktype": "^2.1.29",
|
|
24
|
+
"drizzle-orm": "^0.45.1",
|
|
25
|
+
"postgres": "^3.4.8"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
import { drizzle } from "drizzle-orm/postgres-js";
|
|
3
|
+
|
|
4
|
+
import { DBConfig } from "./config";
|
|
5
|
+
import { createConnection } from "./connection";
|
|
6
|
+
import * as schema from "./schema";
|
|
7
|
+
|
|
8
|
+
export function createDB(raw: unknown) {
|
|
9
|
+
const config = DBConfig(raw);
|
|
10
|
+
if (config instanceof type.errors) {
|
|
11
|
+
throw new Error(`Invalid database config: ${config.summary}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const sql = createConnection(config);
|
|
15
|
+
const db = drizzle(sql, { schema });
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
db,
|
|
19
|
+
transaction: db.transaction.bind(db),
|
|
20
|
+
close: () => sql.end(),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type DB = ReturnType<typeof createDB>;
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
|
|
3
|
+
export const DBConfig = type({
|
|
4
|
+
host: "string",
|
|
5
|
+
port: "number.integer > 0",
|
|
6
|
+
user: "string",
|
|
7
|
+
password: "string",
|
|
8
|
+
database: "string",
|
|
9
|
+
"ssl?": "boolean",
|
|
10
|
+
"max?": "number.integer > 0",
|
|
11
|
+
// Postgres schema name. When set, the connection's `search_path` is
|
|
12
|
+
// pinned to this schema and migrations apply into it. This is the
|
|
13
|
+
// mechanism the integration-test harness uses to give each spawned
|
|
14
|
+
// hub a dedicated, droppable schema. When unset, the connection uses
|
|
15
|
+
// postgres' default `search_path` (which begins with `public`).
|
|
16
|
+
"schema?": "string",
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export type DBConfig = typeof DBConfig.infer;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import postgres from "postgres";
|
|
2
|
+
|
|
3
|
+
import type { DBConfig } from "./config";
|
|
4
|
+
|
|
5
|
+
function quoteIdentifier(name: string): string {
|
|
6
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createConnection(config: DBConfig) {
|
|
10
|
+
return postgres({
|
|
11
|
+
host: config.host,
|
|
12
|
+
port: config.port,
|
|
13
|
+
user: config.user,
|
|
14
|
+
password: config.password,
|
|
15
|
+
database: config.database,
|
|
16
|
+
max: config.max ?? 10,
|
|
17
|
+
...(config.ssl !== undefined && { ssl: config.ssl }),
|
|
18
|
+
...(config.schema !== undefined && {
|
|
19
|
+
// Pin the connection's search_path so unqualified table
|
|
20
|
+
// references resolve to the caller's schema. The migration
|
|
21
|
+
// runner emits SQL with the schema baked into FK references,
|
|
22
|
+
// but ORM-issued queries bind table names without a schema
|
|
23
|
+
// qualifier and rely on this setting.
|
|
24
|
+
connection: { search_path: quoteIdentifier(config.schema) },
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { eq, and, isNull } from "drizzle-orm";
|
|
2
|
+
import { type } from "arktype";
|
|
3
|
+
|
|
4
|
+
import { getLogger } from "@intx/log";
|
|
5
|
+
import { CredentialRequirement as CredentialRequirementType } from "@intx/types";
|
|
6
|
+
import type { InferenceSource } from "@intx/types/runtime";
|
|
7
|
+
|
|
8
|
+
import type { DB } from "./client";
|
|
9
|
+
import { agent } from "./schema/agents";
|
|
10
|
+
import { agentSession } from "./schema/sessions";
|
|
11
|
+
import { credential } from "./schema/credentials";
|
|
12
|
+
import { oauthClient } from "./schema/oauth-clients";
|
|
13
|
+
import { provider } from "./schema/providers";
|
|
14
|
+
import { getAncestorChain } from "./tenant-hierarchy";
|
|
15
|
+
|
|
16
|
+
const log = getLogger(["db", "credentials"]);
|
|
17
|
+
|
|
18
|
+
const CredentialRequirements = CredentialRequirementType.array();
|
|
19
|
+
|
|
20
|
+
export const ProviderMetadata = type({ baseURL: "string" });
|
|
21
|
+
|
|
22
|
+
const AgentModelConfig = type({ defaultModel: "string" });
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolves a provider by name, walking up the tenant hierarchy.
|
|
26
|
+
* Returns the first match (child shadows parent).
|
|
27
|
+
*/
|
|
28
|
+
export async function resolveProviderByName(
|
|
29
|
+
db: DB["db"],
|
|
30
|
+
tenantId: string,
|
|
31
|
+
name: string,
|
|
32
|
+
) {
|
|
33
|
+
const chain = await getAncestorChain(db, tenantId);
|
|
34
|
+
|
|
35
|
+
for (const tid of chain) {
|
|
36
|
+
const row = await db.query.provider.findFirst({
|
|
37
|
+
where: and(eq(provider.tenantId, tid), eq(provider.name, name)),
|
|
38
|
+
});
|
|
39
|
+
if (row) return row;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolves an OAuth client for a provider, walking up the tenant hierarchy.
|
|
47
|
+
* Returns the first match (child shadows parent).
|
|
48
|
+
*/
|
|
49
|
+
export async function resolveOAuthClient(
|
|
50
|
+
db: DB["db"],
|
|
51
|
+
tenantId: string,
|
|
52
|
+
providerId: string,
|
|
53
|
+
) {
|
|
54
|
+
const chain = await getAncestorChain(db, tenantId);
|
|
55
|
+
|
|
56
|
+
for (const tid of chain) {
|
|
57
|
+
const row = await db.query.oauthClient.findFirst({
|
|
58
|
+
where: and(
|
|
59
|
+
eq(oauthClient.tenantId, tid),
|
|
60
|
+
eq(oauthClient.providerId, providerId),
|
|
61
|
+
),
|
|
62
|
+
});
|
|
63
|
+
if (row) return row;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Resolves a credential by name, walking up the tenant hierarchy.
|
|
71
|
+
* Returns the first match (child shadows parent).
|
|
72
|
+
*/
|
|
73
|
+
export async function resolveCredentialByName(
|
|
74
|
+
db: DB["db"],
|
|
75
|
+
tenantId: string,
|
|
76
|
+
name: string,
|
|
77
|
+
) {
|
|
78
|
+
const chain = await getAncestorChain(db, tenantId);
|
|
79
|
+
|
|
80
|
+
for (const tid of chain) {
|
|
81
|
+
const row = await db.query.credential.findFirst({
|
|
82
|
+
where: and(eq(credential.tenantId, tid), eq(credential.name, name)),
|
|
83
|
+
});
|
|
84
|
+
if (row) return row;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolves a credential by ID, validating that it belongs to the
|
|
92
|
+
* given tenant or one of its ancestors.
|
|
93
|
+
*/
|
|
94
|
+
export async function resolveCredentialById(
|
|
95
|
+
db: DB["db"],
|
|
96
|
+
tenantId: string,
|
|
97
|
+
credentialId: string,
|
|
98
|
+
) {
|
|
99
|
+
const row = await db.query.credential.findFirst({
|
|
100
|
+
where: eq(credential.id, credentialId),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (!row) return null;
|
|
104
|
+
|
|
105
|
+
const chain = await getAncestorChain(db, tenantId);
|
|
106
|
+
if (!chain.includes(row.tenantId)) return null;
|
|
107
|
+
|
|
108
|
+
return row;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
type CredentialRequirement = {
|
|
112
|
+
providerName: string;
|
|
113
|
+
scopes?: string[];
|
|
114
|
+
source: "tenant" | "creator" | "invoker";
|
|
115
|
+
name?: string;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Resolves a credential matching an agent definition requirement.
|
|
120
|
+
* Used at agent launch time by the control plane.
|
|
121
|
+
*/
|
|
122
|
+
export async function resolveCredentialRequirement(
|
|
123
|
+
db: DB["db"],
|
|
124
|
+
tenantId: string,
|
|
125
|
+
requirement: CredentialRequirement,
|
|
126
|
+
creatorPrincipalId: string | null,
|
|
127
|
+
invokerPrincipalId: string | null,
|
|
128
|
+
) {
|
|
129
|
+
const resolvedProvider = await resolveProviderByName(
|
|
130
|
+
db,
|
|
131
|
+
tenantId,
|
|
132
|
+
requirement.providerName,
|
|
133
|
+
);
|
|
134
|
+
if (!resolvedProvider) return null;
|
|
135
|
+
|
|
136
|
+
const chain = await getAncestorChain(db, tenantId);
|
|
137
|
+
|
|
138
|
+
const principalFilter =
|
|
139
|
+
requirement.source === "tenant"
|
|
140
|
+
? null
|
|
141
|
+
: requirement.source === "creator"
|
|
142
|
+
? creatorPrincipalId
|
|
143
|
+
: invokerPrincipalId;
|
|
144
|
+
|
|
145
|
+
for (const tid of chain) {
|
|
146
|
+
const conditions = [
|
|
147
|
+
eq(credential.tenantId, tid),
|
|
148
|
+
eq(credential.providerId, resolvedProvider.id),
|
|
149
|
+
eq(credential.status, "active"),
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
if (principalFilter === null) {
|
|
153
|
+
conditions.push(isNull(credential.principalId));
|
|
154
|
+
} else if (principalFilter) {
|
|
155
|
+
conditions.push(eq(credential.principalId, principalFilter));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (requirement.name) {
|
|
159
|
+
conditions.push(eq(credential.name, requirement.name));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const rows = await db.query.credential.findMany({
|
|
163
|
+
where: and(...conditions),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const matching = rows.filter((row) => {
|
|
167
|
+
if (!requirement.scopes || requirement.scopes.length === 0) return true;
|
|
168
|
+
const rowScopes = row.scopes ?? [];
|
|
169
|
+
return requirement.scopes.every((s) => rowScopes.includes(s));
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const [sole] = matching;
|
|
173
|
+
if (matching.length === 1 && sole) return sole;
|
|
174
|
+
if (matching.length > 1) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Ambiguous credential match: ${matching.length} credentials match ` +
|
|
177
|
+
`provider=${requirement.providerName} source=${requirement.source} ` +
|
|
178
|
+
`in tenant ${tid}. Specify a name to disambiguate.`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Outcome of resolving one credential requirement to an
|
|
188
|
+
* `InferenceSource`. Callers map `failed` variants to their own
|
|
189
|
+
* error-handling convention (the API route surfaces 409s; the
|
|
190
|
+
* background credential pushers log and continue).
|
|
191
|
+
*/
|
|
192
|
+
export type CredentialOutcome =
|
|
193
|
+
| { ok: true; source: InferenceSource }
|
|
194
|
+
| {
|
|
195
|
+
ok: false;
|
|
196
|
+
reason: "credential_error";
|
|
197
|
+
requirement: CredentialRequirement;
|
|
198
|
+
message: string;
|
|
199
|
+
}
|
|
200
|
+
| {
|
|
201
|
+
ok: false;
|
|
202
|
+
reason: "credential_missing";
|
|
203
|
+
requirement: CredentialRequirement;
|
|
204
|
+
}
|
|
205
|
+
| {
|
|
206
|
+
ok: false;
|
|
207
|
+
reason: "skipped";
|
|
208
|
+
requirement: CredentialRequirement;
|
|
209
|
+
}
|
|
210
|
+
| {
|
|
211
|
+
ok: false;
|
|
212
|
+
reason: "provider_missing";
|
|
213
|
+
credentialId: string;
|
|
214
|
+
}
|
|
215
|
+
| {
|
|
216
|
+
ok: false;
|
|
217
|
+
reason: "provider_misconfigured";
|
|
218
|
+
providerName: string;
|
|
219
|
+
summary: string;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Resolve one credential requirement to an `InferenceSource`, stamping
|
|
224
|
+
* `defaultModel` as the model identity. Skips silently (`reason:
|
|
225
|
+
* "skipped"`) when the requirement targets a principal that does not
|
|
226
|
+
* exist (creator without a creator id, invoker without a session
|
|
227
|
+
* principal); all other failure modes are surfaced with structured
|
|
228
|
+
* reasons.
|
|
229
|
+
*/
|
|
230
|
+
export async function resolveOneCredential(
|
|
231
|
+
db: DB["db"],
|
|
232
|
+
tenantId: string,
|
|
233
|
+
req: CredentialRequirement,
|
|
234
|
+
creatorPrincipalId: string | null,
|
|
235
|
+
invokerPrincipalId: string | null,
|
|
236
|
+
defaultModel: string,
|
|
237
|
+
): Promise<CredentialOutcome> {
|
|
238
|
+
// Reject requirements whose targeted principal does not exist. Without
|
|
239
|
+
// these guards a null creator or invoker would fall through to
|
|
240
|
+
// `resolveCredentialRequirement`, where the principal filter becomes
|
|
241
|
+
// `isNull(credential.principalId)` — the tenant-credential lookup — and
|
|
242
|
+
// a tenant credential would be returned as if it satisfied the
|
|
243
|
+
// creator- or invoker-source requirement.
|
|
244
|
+
if (req.source === "creator" && !creatorPrincipalId) {
|
|
245
|
+
return { ok: false, reason: "skipped", requirement: req };
|
|
246
|
+
}
|
|
247
|
+
if (req.source === "invoker" && !invokerPrincipalId) {
|
|
248
|
+
return { ok: false, reason: "skipped", requirement: req };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let resolved;
|
|
252
|
+
try {
|
|
253
|
+
resolved = await resolveCredentialRequirement(
|
|
254
|
+
db,
|
|
255
|
+
tenantId,
|
|
256
|
+
req,
|
|
257
|
+
creatorPrincipalId,
|
|
258
|
+
invokerPrincipalId,
|
|
259
|
+
);
|
|
260
|
+
} catch (err: unknown) {
|
|
261
|
+
return {
|
|
262
|
+
ok: false,
|
|
263
|
+
reason: "credential_error",
|
|
264
|
+
requirement: req,
|
|
265
|
+
message: err instanceof Error ? err.message : String(err),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (!resolved) {
|
|
269
|
+
return { ok: false, reason: "credential_missing", requirement: req };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const providerRow = await db.query.provider.findFirst({
|
|
273
|
+
where: eq(provider.id, resolved.providerId),
|
|
274
|
+
});
|
|
275
|
+
if (!providerRow) {
|
|
276
|
+
return { ok: false, reason: "provider_missing", credentialId: resolved.id };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const metadata = ProviderMetadata(providerRow.metadata ?? {});
|
|
280
|
+
if (metadata instanceof type.errors) {
|
|
281
|
+
return {
|
|
282
|
+
ok: false,
|
|
283
|
+
reason: "provider_misconfigured",
|
|
284
|
+
providerName: providerRow.name,
|
|
285
|
+
summary: metadata.summary,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
ok: true,
|
|
291
|
+
source: {
|
|
292
|
+
id: `${providerRow.plugin}:${defaultModel}`,
|
|
293
|
+
provider: providerRow.plugin,
|
|
294
|
+
baseURL: metadata.baseURL,
|
|
295
|
+
apiKey: resolved.secret,
|
|
296
|
+
model: defaultModel,
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Resolve the full `InferenceSource[]` for a single running instance by
|
|
303
|
+
* re-resolving each credential requirement from the agent definition.
|
|
304
|
+
*
|
|
305
|
+
* Failure modes (invalid agent definition, malformed `modelConfig`, any
|
|
306
|
+
* per-credential resolution failure) collapse to an empty array with
|
|
307
|
+
* structured `db.credentials` log lines. Callers that need to surface
|
|
308
|
+
* per-credential outcomes to operators should call `resolveOneCredential`
|
|
309
|
+
* directly — see `packages/hub-api/src/routes/instances.ts` for the
|
|
310
|
+
* 409-mapping pattern.
|
|
311
|
+
*/
|
|
312
|
+
export async function resolveInstanceSources(
|
|
313
|
+
db: DB["db"],
|
|
314
|
+
tenantId: string,
|
|
315
|
+
instance: { agentId: string; sessionId: string | null },
|
|
316
|
+
): Promise<InferenceSource[]> {
|
|
317
|
+
const agentRow = await db.query.agent.findFirst({
|
|
318
|
+
where: eq(agent.id, instance.agentId),
|
|
319
|
+
});
|
|
320
|
+
if (!agentRow) return [];
|
|
321
|
+
|
|
322
|
+
const requirements = CredentialRequirements(
|
|
323
|
+
agentRow.credentialRequirements ?? [],
|
|
324
|
+
);
|
|
325
|
+
if (requirements instanceof type.errors) {
|
|
326
|
+
log.warn`Invalid credential requirements for agent ${agentRow.id}: ${requirements.summary}`;
|
|
327
|
+
return [];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const modelConfig = AgentModelConfig(agentRow.modelConfig ?? {});
|
|
331
|
+
if (modelConfig instanceof type.errors) {
|
|
332
|
+
log.warn`Invalid modelConfig for agent ${agentRow.id}: ${modelConfig.summary}`;
|
|
333
|
+
return [];
|
|
334
|
+
}
|
|
335
|
+
const defaultModel = modelConfig.defaultModel;
|
|
336
|
+
|
|
337
|
+
let invokerPrincipalId: string | null = null;
|
|
338
|
+
if (instance.sessionId) {
|
|
339
|
+
const session = await db.query.agentSession.findFirst({
|
|
340
|
+
where: eq(agentSession.id, instance.sessionId),
|
|
341
|
+
});
|
|
342
|
+
if (session) {
|
|
343
|
+
invokerPrincipalId = session.principalId;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const sources: InferenceSource[] = [];
|
|
348
|
+
for (const req of requirements) {
|
|
349
|
+
const outcome = await resolveOneCredential(
|
|
350
|
+
db,
|
|
351
|
+
tenantId,
|
|
352
|
+
req,
|
|
353
|
+
agentRow.creatorPrincipalId,
|
|
354
|
+
invokerPrincipalId,
|
|
355
|
+
defaultModel,
|
|
356
|
+
);
|
|
357
|
+
if (outcome.ok) {
|
|
358
|
+
sources.push(outcome.source);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
switch (outcome.reason) {
|
|
362
|
+
case "skipped":
|
|
363
|
+
break;
|
|
364
|
+
case "credential_error":
|
|
365
|
+
log.warn`Failed to resolve credential for provider ${outcome.requirement.providerName}: ${outcome.message}`;
|
|
366
|
+
break;
|
|
367
|
+
case "credential_missing":
|
|
368
|
+
break;
|
|
369
|
+
case "provider_missing":
|
|
370
|
+
break;
|
|
371
|
+
case "provider_misconfigured":
|
|
372
|
+
log.warn`Invalid provider metadata for provider ${outcome.providerName}: ${outcome.summary}`;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return sources;
|
|
378
|
+
}
|