@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.
Files changed (91) hide show
  1. package/README.md +37 -0
  2. package/drizzle.config.ts +23 -0
  3. package/migrations/.gitkeep +0 -0
  4. package/migrations/0000_brown_wither.sql +50 -0
  5. package/migrations/0001_white_aqueduct.sql +105 -0
  6. package/migrations/0002_clever_falcon.sql +56 -0
  7. package/migrations/0003_stiff_tyrannus.sql +44 -0
  8. package/migrations/0004_rename_capability_to_offering.sql +1 -0
  9. package/migrations/0005_gigantic_cardiac.sql +1 -0
  10. package/migrations/0006_sidecar.sql +8 -0
  11. package/migrations/0007_agent_running_session.sql +1 -0
  12. package/migrations/0008_session.sql +10 -0
  13. package/migrations/0009_session_messages.sql +18 -0
  14. package/migrations/0010_agent_sidecar_pubkey.sql +2 -0
  15. package/migrations/0011_session_message_from.sql +2 -0
  16. package/migrations/0012_agent_instance.sql +22 -0
  17. package/migrations/0013_instance_session_id.sql +2 -0
  18. package/migrations/0014_drop_agent_runtime_columns.sql +12 -0
  19. package/migrations/0015_add_instance_id_to_session_message.sql +2 -0
  20. package/migrations/0016_jazzy_gamma_corps.sql +3 -0
  21. package/migrations/0017_hesitant_marvex.sql +1 -0
  22. package/migrations/0018_natural_sinister_six.sql +5 -0
  23. package/migrations/0019_rename_grant_source_to_origin.sql +1 -0
  24. package/migrations/0020_add_agent_role.sql +9 -0
  25. package/migrations/0021_acoustic_ozymandias.sql +15 -0
  26. package/migrations/0022_material_sleepwalker.sql +27 -0
  27. package/migrations/0023_flawless_scarlet_witch.sql +2 -0
  28. package/migrations/0024_bumpy_sharon_ventura.sql +1 -0
  29. package/migrations/0025_curvy_firestar.sql +26 -0
  30. package/migrations/0026_keen_ultimo.sql +13 -0
  31. package/migrations/0027_git_tokens.sql +21 -0
  32. package/migrations/0028_wet_sugar_man.sql +1 -0
  33. package/migrations/meta/0000_snapshot.json +316 -0
  34. package/migrations/meta/0001_snapshot.json +968 -0
  35. package/migrations/meta/0002_snapshot.json +1315 -0
  36. package/migrations/meta/0003_snapshot.json +1594 -0
  37. package/migrations/meta/0004_snapshot.json +1594 -0
  38. package/migrations/meta/0005_snapshot.json +1600 -0
  39. package/migrations/meta/0011_snapshot.json +1921 -0
  40. package/migrations/meta/0012_snapshot.json +2067 -0
  41. package/migrations/meta/0013_snapshot.json +2082 -0
  42. package/migrations/meta/0014_snapshot.json +2049 -0
  43. package/migrations/meta/0015_snapshot.json +2064 -0
  44. package/migrations/meta/0016_snapshot.json +2085 -0
  45. package/migrations/meta/0017_snapshot.json +2085 -0
  46. package/migrations/meta/0018_snapshot.json +2070 -0
  47. package/migrations/meta/0019_snapshot.json +2070 -0
  48. package/migrations/meta/0020_snapshot.json +2126 -0
  49. package/migrations/meta/0021_snapshot.json +2239 -0
  50. package/migrations/meta/0022_snapshot.json +2425 -0
  51. package/migrations/meta/0023_snapshot.json +2260 -0
  52. package/migrations/meta/0024_snapshot.json +2254 -0
  53. package/migrations/meta/0025_snapshot.json +2418 -0
  54. package/migrations/meta/0026_snapshot.json +2508 -0
  55. package/migrations/meta/0027_snapshot.json +2657 -0
  56. package/migrations/meta/0028_snapshot.json +2657 -0
  57. package/migrations/meta/_journal.json +209 -0
  58. package/package.json +27 -0
  59. package/src/client.ts +24 -0
  60. package/src/config.ts +19 -0
  61. package/src/connection.ts +27 -0
  62. package/src/credential-resolution.ts +378 -0
  63. package/src/grant-store.ts +51 -0
  64. package/src/index.ts +32 -0
  65. package/src/migrate.test.ts +35 -0
  66. package/src/migrate.ts +168 -0
  67. package/src/parse-row.test.ts +113 -0
  68. package/src/parse-row.ts +185 -0
  69. package/src/schema/agent-assets.ts +21 -0
  70. package/src/schema/agents.ts +49 -0
  71. package/src/schema/assets.ts +24 -0
  72. package/src/schema/auth.ts +51 -0
  73. package/src/schema/credentials.ts +43 -0
  74. package/src/schema/git-tokens.ts +83 -0
  75. package/src/schema/grants.ts +26 -0
  76. package/src/schema/index.ts +19 -0
  77. package/src/schema/instances.ts +36 -0
  78. package/src/schema/messages.ts +108 -0
  79. package/src/schema/oauth-clients.ts +26 -0
  80. package/src/schema/offerings.ts +20 -0
  81. package/src/schema/principals.ts +28 -0
  82. package/src/schema/providers.ts +23 -0
  83. package/src/schema/roles.ts +51 -0
  84. package/src/schema/session-assets.ts +49 -0
  85. package/src/schema/sessions.ts +26 -0
  86. package/src/schema/sidecar.ts +14 -0
  87. package/src/schema/tenants.ts +41 -0
  88. package/src/schema/wallets.ts +44 -0
  89. package/src/tenant-hierarchy.ts +34 -0
  90. package/tsconfig.json +4 -0
  91. 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
+ }