@intx/db 0.2.2 → 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.
Files changed (170) hide show
  1. package/README.md +2 -2
  2. package/dist/approval-store.d.ts +52 -0
  3. package/dist/approval-store.js +79 -0
  4. package/dist/asset-resolution.d.ts +2 -2
  5. package/dist/client.d.ts +10 -0
  6. package/dist/connection.js +11 -8
  7. package/dist/credential-resolution.d.ts +130 -18
  8. package/dist/credential-resolution.js +142 -3
  9. package/dist/grant-store.js +35 -13
  10. package/dist/index.d.ts +13 -4
  11. package/dist/index.js +11 -3
  12. package/dist/model-source-resolution.d.ts +52 -6
  13. package/dist/model-source-resolution.js +145 -19
  14. package/dist/parse-row.d.ts +212 -32
  15. package/dist/parse-row.js +109 -18
  16. package/dist/rekey-credential-secrets.d.ts +13 -0
  17. package/dist/rekey-credential-secrets.js +61 -0
  18. package/dist/schema/approvals.d.ts +245 -0
  19. package/dist/schema/approvals.js +52 -0
  20. package/dist/schema/catalog.d.ts +17 -0
  21. package/dist/schema/catalog.js +7 -1
  22. package/dist/schema/credentials.d.ts +1 -1
  23. package/dist/schema/git-tokens.js +1 -1
  24. package/dist/schema/grants.js +8 -2
  25. package/dist/schema/index.d.ts +8 -4
  26. package/dist/schema/index.js +8 -4
  27. package/dist/schema/messages.d.ts +5 -5
  28. package/dist/schema/messages.js +20 -9
  29. package/dist/schema/offerings.js +8 -2
  30. package/dist/schema/principals.d.ts +2 -2
  31. package/dist/schema/principals.js +7 -5
  32. package/dist/schema/providers.d.ts +17 -0
  33. package/dist/schema/providers.js +5 -0
  34. package/dist/schema/roles.js +6 -2
  35. package/dist/schema/session-assets.d.ts +1 -38
  36. package/dist/schema/session-assets.js +36 -19
  37. package/dist/schema/sessions.js +7 -2
  38. package/dist/schema/sidecar-allocation.d.ts +406 -0
  39. package/dist/schema/sidecar-allocation.js +57 -0
  40. package/dist/schema/sidecar.d.ts +19 -2
  41. package/dist/schema/sidecar.js +13 -3
  42. package/dist/schema/signal-correlations.d.ts +177 -0
  43. package/dist/schema/signal-correlations.js +31 -0
  44. package/dist/schema/wallets.d.ts +3 -3
  45. package/dist/schema/wallets.js +4 -2
  46. package/dist/schema/{agents.d.ts → workflow-definitions.d.ts} +79 -113
  47. package/dist/schema/workflow-definitions.js +97 -0
  48. package/dist/schema/workflow-run-dispatch.d.ts +328 -0
  49. package/dist/schema/workflow-run-dispatch.js +56 -0
  50. package/dist/schema/{workflow-deployments.d.ts → workflow-run-execution.d.ts} +43 -46
  51. package/dist/schema/workflow-run-execution.js +48 -0
  52. package/dist/schema/workflow-run-launch-spec.d.ts +200 -0
  53. package/dist/schema/workflow-run-launch-spec.js +26 -0
  54. package/dist/schema/{instances.d.ts → workflow-run.d.ts} +39 -71
  55. package/dist/schema/workflow-run.js +102 -0
  56. package/dist/sidecar-allocation-store.d.ts +171 -0
  57. package/dist/sidecar-allocation-store.js +512 -0
  58. package/dist/signal-correlation-store.d.ts +36 -0
  59. package/dist/signal-correlation-store.js +63 -0
  60. package/dist/tenant-hierarchy.d.ts +2 -1
  61. package/dist/tenant-hierarchy.js +10 -5
  62. package/dist/workflow-definition-store.d.ts +55 -0
  63. package/dist/workflow-definition-store.js +93 -0
  64. package/dist/workflow-run-dispatch-store.d.ts +80 -0
  65. package/dist/workflow-run-dispatch-store.js +298 -0
  66. package/dist/workflow-run-launch-spec-store.d.ts +13 -0
  67. package/dist/workflow-run-launch-spec-store.js +30 -0
  68. package/dist/workflow-run-store.d.ts +54 -0
  69. package/dist/workflow-run-store.js +85 -0
  70. package/migrations/0037_credential_use_backfill.sql +39 -0
  71. package/migrations/0038_chilly_blacklash.sql +37 -0
  72. package/migrations/0039_quick_carnage.sql +2 -0
  73. package/migrations/0040_reshape_approval_origin.sql +24 -0
  74. package/migrations/0041_make_approval_timeout_at_nullable.sql +1 -0
  75. package/migrations/0042_youthful_mantis.sql +2 -0
  76. package/migrations/0043_signal_correlation_deployment_fk.sql +1 -0
  77. package/migrations/0044_model_offering_quirks.sql +1 -0
  78. package/migrations/0045_create_workflow_run.sql +13 -0
  79. package/migrations/0046_approval_signal_correlation_run_fk.sql +2 -0
  80. package/migrations/0047_create_workflow_definition.sql +30 -0
  81. package/migrations/0048_workflow_definition_origin_agent_id.sql +2 -0
  82. package/migrations/0049_curly_omega_flight.sql +2 -0
  83. package/migrations/0050_late_crystal.sql +10 -0
  84. package/migrations/0051_funny_madelyne_pryor.sql +1 -0
  85. package/migrations/0052_drop_inference_turn_instance_fk.sql +1 -0
  86. package/migrations/0053_session_mail_session_id_index.sql +1 -0
  87. package/migrations/0054_rekey_instance_grants_to_workflow_run.sql +10 -0
  88. package/migrations/0055_backfill_anchor_workflow_runs.sql +69 -0
  89. package/migrations/0056_repoint_deployment_fks_to_anchor_run.sql +32 -0
  90. package/migrations/0057_drop_workflow_deployment_projection.sql +22 -0
  91. package/migrations/0058_repoint_offering_fk_to_definition.sql +41 -0
  92. package/migrations/0059_repoint_transaction_fk_to_run.sql +10 -0
  93. package/migrations/0060_drop_session_mail_instance_fk.sql +6 -0
  94. package/migrations/0061_drop_session_asset_instance_fk.sql +7 -0
  95. package/migrations/0062_drop_agent_asset_table.sql +11 -0
  96. package/migrations/0063_repoint_agent_role_fk_to_definition.sql +39 -0
  97. package/migrations/0064_repoint_agent_session_fk_to_definition.sql +47 -0
  98. package/migrations/0065_add_workflow_definition_model_requirements.sql +37 -0
  99. package/migrations/0066_rekey_agent_definition_principals_to_workflow.sql +27 -0
  100. package/migrations/0067_add_workflow_definition_kind.sql +10 -0
  101. package/migrations/0068_drop_agent_tables_and_origin_agent_id.sql +31 -0
  102. package/migrations/0069_drop_workflow_definition_kind.sql +10 -0
  103. package/migrations/0070_flashy_proteus.sql +13 -0
  104. package/migrations/0072_add_workflow_definition_credential_bindings.sql +1 -0
  105. package/migrations/0073_grant_target_exactly_one_check.sql +13 -0
  106. package/migrations/0074_add_provider_api_base_url.sql +1 -0
  107. package/migrations/0075_workflow_run_launch_spec.sql +17 -0
  108. package/migrations/0076_sidecar_allocation.sql +40 -0
  109. package/migrations/0077_workflow_run_dispatch.sql +29 -0
  110. package/migrations/0078_workflow_run_dispatch_kind.sql +2 -0
  111. package/migrations/0079_rename_workflow_run_deployment_id_to_anchor_run_id.sql +4 -0
  112. package/migrations/0080_rename_correlation_approval_deployment_id_to_anchor_run_id.sql +9 -0
  113. package/migrations/0081_workflow_definition_content_hash_and_approved_wire_hash.sql +4 -0
  114. package/migrations/0082_blue_black_queen.sql +1 -0
  115. package/migrations/0083_replace_launch_spec_snapshot_with_frozen_bundle.sql +3 -0
  116. package/migrations/0084_delete_orphaned_credential_grants.sql +22 -0
  117. package/migrations/meta/0037_snapshot.json +3237 -0
  118. package/migrations/meta/0038_snapshot.json +3470 -0
  119. package/migrations/meta/0039_snapshot.json +3470 -0
  120. package/migrations/meta/0040_snapshot.json +3489 -0
  121. package/migrations/meta/0041_snapshot.json +3489 -0
  122. package/migrations/meta/0042_snapshot.json +3489 -0
  123. package/migrations/meta/0043_snapshot.json +3498 -0
  124. package/migrations/meta/0044_snapshot.json +3504 -0
  125. package/migrations/meta/0045_snapshot.json +3589 -0
  126. package/migrations/meta/0046_snapshot.json +3607 -0
  127. package/migrations/meta/0047_snapshot.json +3832 -0
  128. package/migrations/meta/0048_snapshot.json +3853 -0
  129. package/migrations/meta/0049_snapshot.json +3854 -0
  130. package/migrations/meta/0050_snapshot.json +3924 -0
  131. package/migrations/meta/0051_snapshot.json +3940 -0
  132. package/migrations/meta/0052_snapshot.json +3931 -0
  133. package/migrations/meta/0053_snapshot.json +3952 -0
  134. package/migrations/meta/0054_snapshot.json +3952 -0
  135. package/migrations/meta/0055_snapshot.json +3952 -0
  136. package/migrations/meta/0056_snapshot.json +3952 -0
  137. package/migrations/meta/0057_snapshot.json +3839 -0
  138. package/migrations/meta/0058_snapshot.json +3839 -0
  139. package/migrations/meta/0059_snapshot.json +3839 -0
  140. package/migrations/meta/0060_snapshot.json +3830 -0
  141. package/migrations/meta/0061_snapshot.json +3821 -0
  142. package/migrations/meta/0062_snapshot.json +3723 -0
  143. package/migrations/meta/0063_snapshot.json +3723 -0
  144. package/migrations/meta/0064_snapshot.json +3723 -0
  145. package/migrations/meta/0065_snapshot.json +3729 -0
  146. package/migrations/meta/0066_snapshot.json +3729 -0
  147. package/migrations/meta/0067_snapshot.json +3736 -0
  148. package/migrations/meta/0068_snapshot.json +3347 -0
  149. package/migrations/meta/0069_snapshot.json +3340 -0
  150. package/migrations/meta/0070_snapshot.json +3444 -0
  151. package/migrations/meta/0072_snapshot.json +3450 -0
  152. package/migrations/meta/0073_snapshot.json +3455 -0
  153. package/migrations/meta/0074_snapshot.json +3461 -0
  154. package/migrations/meta/0075_snapshot.json +3567 -0
  155. package/migrations/meta/0076_snapshot.json +3851 -0
  156. package/migrations/meta/0077_snapshot.json +4067 -0
  157. package/migrations/meta/0078_snapshot.json +4078 -0
  158. package/migrations/meta/0079_snapshot.json +4078 -0
  159. package/migrations/meta/0080_snapshot.json +4078 -0
  160. package/migrations/meta/0081_snapshot.json +4096 -0
  161. package/migrations/meta/0082_snapshot.json +4102 -0
  162. package/migrations/meta/0083_snapshot.json +4096 -0
  163. package/migrations/meta/0084_snapshot.json +4096 -0
  164. package/migrations/meta/_journal.json +329 -0
  165. package/package.json +6 -3
  166. package/dist/schema/agent-assets.d.ts +0 -109
  167. package/dist/schema/agent-assets.js +0 -15
  168. package/dist/schema/agents.js +0 -57
  169. package/dist/schema/instances.js +0 -39
  170. package/dist/schema/workflow-deployments.js +0 -37
package/dist/index.js CHANGED
@@ -1,12 +1,20 @@
1
1
  export { createDB } from "./client.js";
2
2
  export { pgErrorCode, PG_UNIQUE_VIOLATION, PG_FOREIGN_KEY_VIOLATION, } from "./pg-error.js";
3
3
  export { runMigrations, dropSchema } from "./migrate.js";
4
+ export { rekeyCredentialSecrets, } from "./rekey-credential-secrets.js";
4
5
  export { createGrantStore } from "./grant-store.js";
6
+ export { createApprovalStore, } from "./approval-store.js";
7
+ export { createSignalCorrelationStore, } from "./signal-correlation-store.js";
8
+ export { createWorkflowRunStore, } from "./workflow-run-store.js";
9
+ export { createWorkflowRunLaunchSpecStore, } from "./workflow-run-launch-spec-store.js";
10
+ export { createWorkflowRunDispatchStore, WorkflowRunDispatchPayloadConflictError, } from "./workflow-run-dispatch-store.js";
11
+ export { createSidecarAllocationStore, } from "./sidecar-allocation-store.js";
12
+ export { createWorkflowDefinitionStore, loadFrozenGrantSnapshot, resolveDefinitionIdForAsset, } from "./workflow-definition-store.js";
5
13
  export { getAncestorChain, getDescendantTenants } from "./tenant-hierarchy.js";
6
14
  export { resolveActivePrice } from "./pricing.js";
7
- export { resolveProviderByName, resolveOAuthClient, resolveCredentialByName, resolveCredentialById, resolveCredentialRequirement, } from "./credential-resolution.js";
15
+ export { resolveProviderByName, resolveOAuthClient, resolveCredentialByName, resolveCredentialById, resolveCredentialRequirement, resolveTenantOwnedCredentialById, AmbiguousCredentialError, buildCredentialDelivery, } from "./credential-resolution.js";
8
16
  export { resolveAssetByName, resolveAssetById, listAssetsForTenant, } from "./asset-resolution.js";
9
17
  export { listVisibleModels, listVisibleProviders, listVisibleOfferings, } from "./catalog-resolution.js";
10
- export { resolveModelSources, resolveInstanceModelSources, } from "./model-source-resolution.js";
11
- export { parseAgentRow, parseAgentVersionRow, parseGrantRow, parseOfferingRow, parseCredentialRow, parseProviderRow, parseTenantRow, parseWalletRow, parseTransactionRow, parseOAuthClientRow, parseGitTokenRow, parseTurnPartType, } from "./parse-row.js";
18
+ export { resolveModelSources, resolveInferencePreferences, resolveInstanceModelSources, resolveSourcesByOfferingIds, } from "./model-source-resolution.js";
19
+ export { parseGrantRow, parseApprovalRow, parsePrincipalRow, parseSignalCorrelationRow, parseWorkflowRunRow, parseWorkflowRunDispatchRow, parseWorkflowRunLaunchSpecRow, parseWorkflowDefinitionRow, parseWorkflowDefinitionVersionRow, parseOfferingRow, parseModelOfferingRow, parseCredentialRow, parseProviderRow, parseTenantRow, parseWalletRow, parseTransactionRow, parseOAuthClientRow, parseGitTokenRow, parseTurnPartType, } from "./parse-row.js";
12
20
  export * as schema from "./schema/index.js";
@@ -1,4 +1,4 @@
1
- import { type ModelRequirement, type ProviderPreference } from "@intx/types";
1
+ import { type CredentialCipher, type ModelRequirement, type ProviderPreference } from "@intx/types";
2
2
  import type { InferenceSource } from "@intx/types/runtime";
3
3
  import type { DB } from "./client.js";
4
4
  /**
@@ -12,6 +12,9 @@ export type SourceSkip = {
12
12
  } | {
13
13
  reason: "credential_unresolved";
14
14
  provider: string;
15
+ } | {
16
+ reason: "credential_unauthorized";
17
+ provider: string;
15
18
  } | {
16
19
  reason: "provider_misconfigured";
17
20
  provider: string;
@@ -40,6 +43,22 @@ export type CatalogSourceResolution = {
40
43
  model: string;
41
44
  skips: SourceSkip[];
42
45
  };
46
+ export type OfferingSourceResolution = {
47
+ ok: true;
48
+ sources: InferenceSource[];
49
+ } | {
50
+ ok: false;
51
+ reason: "offering_unavailable";
52
+ offeringId: string;
53
+ skip?: SourceSkip;
54
+ };
55
+ /**
56
+ * Rebuild an exact, ordered source chain from durable catalog offering ids.
57
+ * This is the recovery counterpart to launch-time source selection: it keeps
58
+ * secrets out of persisted launch specs and rechecks tenant visibility and
59
+ * credential ownership on every launch.
60
+ */
61
+ export declare function resolveSourcesByOfferingIds(db: DB["db"], tenantId: string, offeringIds: readonly string[], credentialCipher?: CredentialCipher): Promise<OfferingSourceResolution>;
43
62
  /**
44
63
  * Resolves an agent's model requirements against the tenant catalog into an
45
64
  * ordered `InferenceSource[]` for the harness.
@@ -51,19 +70,46 @@ export type CatalogSourceResolution = {
51
70
  * offering is resolved to a credential-backed source; offerings that cannot
52
71
  * produce one are skipped. A required model that yields no source makes the
53
72
  * agent unlaunchable.
73
+ *
74
+ * A credential-backed source is emitted only when the launching tenant owns
75
+ * the referenced credential within its hierarchy (ownership is the authority);
76
+ * otherwise the offering is skipped (`credential_unauthorized`) and its secret
77
+ * is withheld.
54
78
  */
55
79
  export declare function resolveModelSources(db: DB["db"], tenantId: string, requirements: ModelRequirement[], opts?: {
56
80
  invokerPreferences?: Record<string, ProviderPreference>;
81
+ credentialCipher?: CredentialCipher;
57
82
  }): Promise<CatalogSourceResolution>;
83
+ /**
84
+ * Resolve an agent's model requirements to the credential-free
85
+ * `{ provider, model }` inference preferences a folded workflow definition
86
+ * carries on its step agent. Reuses `resolveModelSources` -- the same catalog
87
+ * + creator-grant resolution the instance launch path runs -- and projects its
88
+ * ordered sources to their `{ provider (plugin), model }` identity, dropping
89
+ * the credentials: a definition's inference preference is hash-only, and the
90
+ * credential-bearing sources are supplied per deploy on the workflow path.
91
+ *
92
+ * A requirement set that resolves to no source is a hard failure. A folded
93
+ * agent whose model is unresolvable is undeployable, so this raises rather
94
+ * than synthesizing an empty preference list that would silently strip the
95
+ * agent's inference. It also raises when the resolved sources are not
96
+ * injective on `(provider, model)` -- the projection would otherwise freeze
97
+ * an ambiguous preference list whose lost `offering.id` distinctions cannot be
98
+ * recovered after the agent's columns are dropped.
99
+ */
100
+ export declare function resolveInferencePreferences(db: DB["db"], tenantId: string, requirements: ModelRequirement[]): Promise<{
101
+ provider: string;
102
+ model: string;
103
+ }[]>;
58
104
  /**
59
105
  * Resolves the ordered sources for a running instance from persisted state:
60
- * the agent definition's model requirements and the invoker's launch-time
61
- * preferences stored on the instance row. Launch, credential-rotation push,
62
- * and sidecar reconnect all resolve through this, so a running instance's
106
+ * the definition's model requirements and the invoker's launch-time
107
+ * preferences stored on the instance row. The credential-rotation and
108
+ * catalog-edit source push resolves through this, so a running instance's
63
109
  * source list is a pure function of persisted state — re-resolution
64
110
  * reproduces the launch ordering, including the invoker's reorder/restrict.
65
111
  */
66
112
  export declare function resolveInstanceModelSources(db: DB["db"], tenantId: string, instance: {
67
- agentId: string;
113
+ definitionId: string;
68
114
  modelPreferences: unknown;
69
- }): Promise<CatalogSourceResolution>;
115
+ }, credentialCipher?: CredentialCipher): Promise<CatalogSourceResolution>;
@@ -1,8 +1,10 @@
1
1
  import { and, eq } from "drizzle-orm";
2
- import { InvokerModelPreferences, ModelRequirements, } from "@intx/types";
2
+ import { createNoopCredentialCipher } from "@intx/crypto";
3
+ import { InvokerModelPreferences, ModelRequirements, credentialAad, } from "@intx/types";
3
4
  import { listVisibleOfferings, } from "./catalog-resolution.js";
4
5
  import { resolveCredentialById } from "./credential-resolution.js";
5
- import { agent } from "./schema/agents.js";
6
+ import { parseModelOfferingRow } from "./parse-row.js";
7
+ import { workflowDefinition } from "./schema/workflow-definitions.js";
6
8
  function byPriority(a, b) {
7
9
  if (a.offering.priority !== b.offering.priority) {
8
10
  return a.offering.priority - b.offering.priority;
@@ -41,7 +43,7 @@ function applyPreference(candidates, preference) {
41
43
  // the rest as fallback after the named providers.
42
44
  return preference.mode === "pin" ? named : [...named, ...rest];
43
45
  }
44
- async function buildSource(db, tenantId, resolved) {
46
+ async function buildSource(db, tenantId, resolved, credentialCipher) {
45
47
  const { provider, model, offering } = resolved;
46
48
  if (provider.credentialId === null) {
47
49
  // No credential reference. A wallet-backed provider is a valid catalog
@@ -60,7 +62,7 @@ async function buildSource(db, tenantId, resolved) {
60
62
  }
61
63
  // Resolve the secret through the tenant-scoped credential resolver so a
62
64
  // provider row referencing a credential outside the tenant's ancestor
63
- // chain cannot leak that secret (INTR-203: the chain is the authority).
65
+ // chain cannot leak that secret (the chain is the authority).
64
66
  const credential = await resolveCredentialById(db, tenantId, provider.credentialId);
65
67
  if (credential === null) {
66
68
  return {
@@ -68,18 +70,74 @@ async function buildSource(db, tenantId, resolved) {
68
70
  skip: { reason: "credential_unresolved", provider: provider.name },
69
71
  };
70
72
  }
73
+ // Authority to use a credential through a catalog provider is ownership
74
+ // within the tenant hierarchy -- the same rule the tool-binding path resolves
75
+ // (`source: "tenant"`). `resolveCredentialById` above already proved the
76
+ // credential is reachable in this tenant's ancestor chain, so a tenant-owned
77
+ // credential (`principalId IS NULL`) is authorized here by ownership alone,
78
+ // with no personal grant and no role grant consulted. This runs live on every
79
+ // launch and rotation, so a catalog edit that adds an eligible credential is
80
+ // picked up without re-materializing any grant. A principal-owned credential
81
+ // is not usable through a shared catalog provider; fail closed.
82
+ if (credential.principalId !== null) {
83
+ return {
84
+ ok: false,
85
+ skip: { reason: "credential_unauthorized", provider: provider.name },
86
+ };
87
+ }
88
+ // Validate the row once at this DB-to-runtime boundary. This narrows the
89
+ // jsonb `quirks` from `unknown` to a `Record | null` and checks
90
+ // `capabilities` against the curated enum. `quirks` is spread in only when
91
+ // non-null so a source with no accommodations omits the key entirely
92
+ // (InferenceSource.quirks is present-or-absent, never null). The capability
93
+ // filter in resolveModelSources still reads the raw row's `capabilities`
94
+ // for its `.includes` check; that read-only comparison cannot be corrupted
95
+ // into a wrong routing decision, so it is left as-is.
96
+ const parsed = parseModelOfferingRow(offering);
71
97
  return {
72
98
  ok: true,
73
99
  source: {
74
100
  id: offering.id,
75
101
  provider: provider.plugin,
76
102
  baseURL: provider.baseURL,
77
- apiKey: credential.secret,
103
+ // Decrypt the stored secret at the one point it is used. Strict: a
104
+ // non-ciphertext value (a row not yet re-keyed, or a write that failed to
105
+ // encrypt) throws rather than delivering a bad key -- fail closed.
106
+ apiKey: await credentialCipher.decrypt(credential.secret, credentialAad(credential.id, "secret")),
78
107
  model: model.canonicalName,
79
- capabilities: offering.capabilities,
108
+ capabilities: parsed.capabilities,
109
+ ...(parsed.quirks !== null ? { quirks: parsed.quirks } : {}),
80
110
  },
81
111
  };
82
112
  }
113
+ /**
114
+ * Rebuild an exact, ordered source chain from durable catalog offering ids.
115
+ * This is the recovery counterpart to launch-time source selection: it keeps
116
+ * secrets out of persisted launch specs and rechecks tenant visibility and
117
+ * credential ownership on every launch.
118
+ */
119
+ export async function resolveSourcesByOfferingIds(db, tenantId, offeringIds, credentialCipher = createNoopCredentialCipher()) {
120
+ const visible = await listVisibleOfferings(db, tenantId);
121
+ const byId = new Map(visible.map((entry) => [entry.offering.id, entry]));
122
+ const sources = [];
123
+ for (const offeringId of offeringIds) {
124
+ const offering = byId.get(offeringId);
125
+ if (offering === undefined) {
126
+ return { ok: false, reason: "offering_unavailable", offeringId };
127
+ }
128
+ const built = await buildSource(db, tenantId, offering, credentialCipher);
129
+ if (!built.ok) {
130
+ return {
131
+ ok: false,
132
+ reason: "offering_unavailable",
133
+ offeringId,
134
+ skip: built.skip,
135
+ };
136
+ }
137
+ sources.push(built.source);
138
+ }
139
+ return { ok: true, sources };
140
+ }
83
141
  /**
84
142
  * Resolves an agent's model requirements against the tenant catalog into an
85
143
  * ordered `InferenceSource[]` for the harness.
@@ -91,11 +149,17 @@ async function buildSource(db, tenantId, resolved) {
91
149
  * offering is resolved to a credential-backed source; offerings that cannot
92
150
  * produce one are skipped. A required model that yields no source makes the
93
151
  * agent unlaunchable.
152
+ *
153
+ * A credential-backed source is emitted only when the launching tenant owns
154
+ * the referenced credential within its hierarchy (ownership is the authority);
155
+ * otherwise the offering is skipped (`credential_unauthorized`) and its secret
156
+ * is withheld.
94
157
  */
95
158
  export async function resolveModelSources(db, tenantId, requirements, opts) {
96
159
  if (requirements.length === 0) {
97
160
  return { ok: false, reason: "no_requirements" };
98
161
  }
162
+ const credentialCipher = opts?.credentialCipher ?? createNoopCredentialCipher();
99
163
  const visible = await listVisibleOfferings(db, tenantId);
100
164
  const sources = [];
101
165
  for (const requirement of requirements) {
@@ -111,7 +175,7 @@ export async function resolveModelSources(db, tenantId, requirements, opts) {
111
175
  const skips = [];
112
176
  const modelSources = [];
113
177
  for (const candidate of candidates) {
114
- const built = await buildSource(db, tenantId, candidate);
178
+ const built = await buildSource(db, tenantId, candidate, credentialCipher);
115
179
  if (built.ok) {
116
180
  modelSources.push(built.source);
117
181
  }
@@ -131,26 +195,87 @@ export async function resolveModelSources(db, tenantId, requirements, opts) {
131
195
  }
132
196
  return { ok: true, sources };
133
197
  }
198
+ /**
199
+ * Resolve an agent's model requirements to the credential-free
200
+ * `{ provider, model }` inference preferences a folded workflow definition
201
+ * carries on its step agent. Reuses `resolveModelSources` -- the same catalog
202
+ * + creator-grant resolution the instance launch path runs -- and projects its
203
+ * ordered sources to their `{ provider (plugin), model }` identity, dropping
204
+ * the credentials: a definition's inference preference is hash-only, and the
205
+ * credential-bearing sources are supplied per deploy on the workflow path.
206
+ *
207
+ * A requirement set that resolves to no source is a hard failure. A folded
208
+ * agent whose model is unresolvable is undeployable, so this raises rather
209
+ * than synthesizing an empty preference list that would silently strip the
210
+ * agent's inference. It also raises when the resolved sources are not
211
+ * injective on `(provider, model)` -- the projection would otherwise freeze
212
+ * an ambiguous preference list whose lost `offering.id` distinctions cannot be
213
+ * recovered after the agent's columns are dropped.
214
+ */
215
+ export async function resolveInferencePreferences(db, tenantId, requirements) {
216
+ const resolution = await resolveModelSources(db, tenantId, requirements);
217
+ if (!resolution.ok) {
218
+ throw new Error(`cannot resolve inference preferences for the folded definition: ${resolution.reason}`);
219
+ }
220
+ // The projection to `{ provider (plugin), model }` drops the `offering.id`
221
+ // that keys each resolved source. Two distinct offerings can share a
222
+ // provider plugin and canonical model -- the catalog permits two
223
+ // `model_provider` rows on the same plugin (uniqueness is on the provider
224
+ // name), each offering the same model -- so the projection is only
225
+ // well-defined when the resolved sources are injective on `(provider,
226
+ // model)`. If they collapse, the folded definition's inference preferences
227
+ // cannot distinguish which offering each entry meant, and once the agent's
228
+ // columns are dropped that distinction is unrecoverable. Refuse to
229
+ // synthesize an ambiguous preference list: fail loud so the collapse
230
+ // surfaces in the fold/materialize manifest rather than freezing silently.
231
+ const preferences = resolution.sources.map((source) => ({
232
+ provider: source.provider,
233
+ model: source.model,
234
+ }));
235
+ const seen = new Set();
236
+ for (const preference of preferences) {
237
+ // Join on a NUL: it cannot appear in a provider plugin or canonical model
238
+ // name, so the composite key is collision-free. Written as an explicit
239
+ // unicode escape rather than an invisible raw control byte in the source.
240
+ const key = `${preference.provider}\u0000${preference.model}`;
241
+ if (seen.has(key)) {
242
+ throw new Error(`cannot resolve inference preferences for the folded definition: ` +
243
+ `the model resolution is not injective on (provider, model) -- ` +
244
+ `two offerings collapse to (${preference.provider}, ${preference.model}); ` +
245
+ `a folded agent requires a distinguishable inference preference per source`);
246
+ }
247
+ seen.add(key);
248
+ }
249
+ return preferences;
250
+ }
134
251
  /**
135
252
  * Resolves the ordered sources for a running instance from persisted state:
136
- * the agent definition's model requirements and the invoker's launch-time
137
- * preferences stored on the instance row. Launch, credential-rotation push,
138
- * and sidecar reconnect all resolve through this, so a running instance's
253
+ * the definition's model requirements and the invoker's launch-time
254
+ * preferences stored on the instance row. The credential-rotation and
255
+ * catalog-edit source push resolves through this, so a running instance's
139
256
  * source list is a pure function of persisted state — re-resolution
140
257
  * reproduces the launch ordering, including the invoker's reorder/restrict.
141
258
  */
142
- export async function resolveInstanceModelSources(db, tenantId, instance) {
143
- // Scope the agent lookup to the resolving tenant, matching the launch
144
- // route. A cross-tenant agentId resolves to nothing rather than
145
- // contributing another tenant's model names or preferences.
146
- const agentRow = await db.query.agent.findFirst({
147
- where: and(eq(agent.id, instance.agentId), eq(agent.tenantId, tenantId)),
259
+ export async function resolveInstanceModelSources(db, tenantId, instance, credentialCipher) {
260
+ // Resolve from the run's own definition by primary key. This is the SAME row
261
+ // the launch resolves its requirements from, so a rotation or catalog edit
262
+ // reproduces the launch's model resolution rather than drifting. Scope to the
263
+ // resolving tenant, matching the launch route: a definition in another tenant
264
+ // resolves to nothing rather than contributing another tenant's models. A
265
+ // definition with no creator principal cannot authorize a credential-backed
266
+ // source, so it fails closed like a missing definition.
267
+ const definitionRow = await db.query.workflowDefinition.findFirst({
268
+ where: and(eq(workflowDefinition.id, instance.definitionId), eq(workflowDefinition.tenantId, tenantId)),
148
269
  });
149
- if (agentRow === undefined) {
270
+ // A credential-backed source is authorized by tenant ownership of the
271
+ // credential (walked in `buildSource`), not by the definition's creator, so a
272
+ // definition with no creator can still launch a tenant-owned inference
273
+ // source. Only a missing definition (or one in another tenant) fails closed.
274
+ if (definitionRow === undefined) {
150
275
  return { ok: false, reason: "no_requirements" };
151
276
  }
152
- const requirements = agentRow.modelRequirements !== null
153
- ? ModelRequirements.assert(agentRow.modelRequirements)
277
+ const requirements = definitionRow.modelRequirements !== null
278
+ ? ModelRequirements.assert(definitionRow.modelRequirements)
154
279
  : [];
155
280
  const preferences = instance.modelPreferences !== null
156
281
  ? InvokerModelPreferences.assert(instance.modelPreferences)
@@ -161,5 +286,6 @@ export async function resolveInstanceModelSources(db, tenantId, instance) {
161
286
  }
162
287
  return resolveModelSources(db, tenantId, requirements, {
163
288
  invokerPreferences,
289
+ ...(credentialCipher !== undefined ? { credentialCipher } : {}),
164
290
  });
165
291
  }