@sixb/pg 0.1.6 → 0.1.7

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 (47) hide show
  1. package/dist/agents/rows.d.ts +8 -5
  2. package/dist/agents/rows.d.ts.map +1 -1
  3. package/dist/agents/runs.d.ts +3 -2
  4. package/dist/agents/runs.d.ts.map +1 -1
  5. package/dist/agents/threads.d.ts.map +1 -1
  6. package/dist/ai-cost-groups.d.ts +5 -0
  7. package/dist/ai-cost-groups.d.ts.map +1 -0
  8. package/dist/auth-storage/auth-storage.d.ts +4 -1
  9. package/dist/auth-storage/auth-storage.d.ts.map +1 -1
  10. package/dist/auth-storage/device-authorizations.d.ts +29 -0
  11. package/dist/auth-storage/device-authorizations.d.ts.map +1 -0
  12. package/dist/auth-storage/rows.d.ts +19 -1
  13. package/dist/auth-storage/rows.d.ts.map +1 -1
  14. package/dist/auth-storage/shared.d.ts +1 -1
  15. package/dist/auth-storage/shared.d.ts.map +1 -1
  16. package/dist/index.d.ts +2 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1721 -393
  19. package/dist/index.js.map +17 -14
  20. package/dist/migrations.d.ts.map +1 -1
  21. package/dist/pg-ai-cost-storage.d.ts +2 -1
  22. package/dist/pg-ai-cost-storage.d.ts.map +1 -1
  23. package/dist/pg-ai-limit-storage.d.ts +34 -0
  24. package/dist/pg-ai-limit-storage.d.ts.map +1 -0
  25. package/dist/pg-ai-usage-storage.d.ts +28 -0
  26. package/dist/pg-ai-usage-storage.d.ts.map +1 -1
  27. package/package.json +4 -4
  28. package/src/agents/rows.ts +38 -9
  29. package/src/agents/runs.ts +148 -11
  30. package/src/agents/threads.ts +1 -17
  31. package/src/ai-cost-groups.ts +102 -0
  32. package/src/auth-storage/auth-storage.ts +84 -0
  33. package/src/auth-storage/device-authorizations.ts +131 -0
  34. package/src/auth-storage/rows.ts +41 -0
  35. package/src/auth-storage/shared.ts +1 -0
  36. package/src/index.ts +8 -1
  37. package/src/migrations/029-model-accounting.sql +24 -0
  38. package/src/migrations/030-ai-usage-limits.sql +94 -0
  39. package/src/migrations/031-subagent-runs.sql +97 -0
  40. package/src/migrations/032-conversation-run-spec.sql +23 -0
  41. package/src/migrations/033-retire-agent-definitions.sql +28 -0
  42. package/src/migrations/034-device-authorizations.sql +24 -0
  43. package/src/migrations.ts +18 -0
  44. package/src/pg-ai-cost-storage.ts +71 -24
  45. package/src/pg-ai-limit-storage.ts +901 -0
  46. package/src/pg-ai-usage-storage.ts +34 -0
  47. package/src/pg-workflow-run-storage.ts +8 -8
@@ -1,8 +1,11 @@
1
1
  import type {
2
2
  AuthStorage,
3
+ CompleteDeviceAuthorizationInput,
4
+ CompleteDeviceAuthorizationResult,
3
5
  CompleteMagicLinkSignInInput,
4
6
  CompleteOidcSignInInput,
5
7
  CompleteSignInResult,
8
+ DeviceAuthorizationRecord,
6
9
  GroupMembershipRecord,
7
10
  InvitationRecord,
8
11
  SessionRecord,
@@ -19,6 +22,10 @@ import {
19
22
  runPgTransaction,
20
23
  } from "../transactions"
21
24
  import { PgAuthAccessTokenStore } from "./access-tokens"
25
+ import {
26
+ getPgDeviceAuthorizationById,
27
+ PgAuthDeviceAuthorizationStore,
28
+ } from "./device-authorizations"
22
29
  import { PgAuthGroupMembershipStore } from "./group-memberships"
23
30
  import { PgAuthUserIdentityStore } from "./identities"
24
31
  import { PgAuthInvitationStore } from "./invitations"
@@ -77,6 +84,7 @@ export class PgAuthStorage implements AuthStorage {
77
84
  readonly groupMemberships: PgAuthGroupMembershipStore
78
85
  readonly magicLinks: PgAuthMagicLinkStore
79
86
  readonly oidcAuthorizationAttempts: PgAuthOidcAuthorizationAttemptStore
87
+ readonly deviceAuthorizations: PgAuthDeviceAuthorizationStore
80
88
 
81
89
  constructor(options: PgAuthStorageOptions) {
82
90
  this.sql = options.sql
@@ -90,6 +98,43 @@ export class PgAuthStorage implements AuthStorage {
90
98
  this.groupMemberships = new PgAuthGroupMembershipStore(this.sql)
91
99
  this.magicLinks = new PgAuthMagicLinkStore(this.sql)
92
100
  this.oidcAuthorizationAttempts = new PgAuthOidcAuthorizationAttemptStore(this.sql)
101
+ this.deviceAuthorizations = new PgAuthDeviceAuthorizationStore(this.sql)
102
+ }
103
+
104
+ async completeDeviceAuthorization(
105
+ input: CompleteDeviceAuthorizationInput
106
+ ): Promise<CompleteDeviceAuthorizationResult> {
107
+ return runPgTransaction(this.sql, async (tx) => {
108
+ const authorization = await getPgDeviceAuthorizationById(tx, input.projectId, input.id)
109
+ if (!authorization) {
110
+ throw new AuthStorageError(
111
+ "missing_device_authorization",
112
+ `[Sixb] Device authorization '${input.id}' not found for project '${input.projectId}'.`
113
+ )
114
+ }
115
+ assertCompletableDeviceAuthorization(authorization, input)
116
+ const accessToken = await new PgAuthAccessTokenStore(tx).create(input.accessToken)
117
+ const [consumed] = await tx`
118
+ UPDATE auth_device_authorizations
119
+ SET status = 'consumed', consumed_at = ${input.completedAt}
120
+ WHERE project_id = ${input.projectId} AND id = ${input.id} AND status = 'approved'
121
+ RETURNING *
122
+ `
123
+ if (!consumed) {
124
+ throw new AuthStorageError(
125
+ "invalid_device_authorization",
126
+ "[Sixb] Device authorization was already consumed."
127
+ )
128
+ }
129
+ return {
130
+ authorization: {
131
+ ...authorization,
132
+ status: "consumed" as const,
133
+ consumedAt: new Date(input.completedAt),
134
+ },
135
+ accessToken,
136
+ }
137
+ })
93
138
  }
94
139
 
95
140
  async completeMagicLinkSignIn(
@@ -796,3 +841,42 @@ function assertSignInSessionAudience(
796
841
  `[Sixb] Sign-in session audience '${sessionAudience}' does not match stored auth audience '${storedAudience}' for project '${projectId}'.`
797
842
  )
798
843
  }
844
+
845
+ function assertCompletableDeviceAuthorization(
846
+ authorization: DeviceAuthorizationRecord,
847
+ input: CompleteDeviceAuthorizationInput
848
+ ): void {
849
+ if (authorization.deviceCodeHash !== input.deviceCodeHash) {
850
+ throw new AuthStorageError(
851
+ "invalid_device_authorization",
852
+ "[Sixb] Invalid device authorization."
853
+ )
854
+ }
855
+ if (authorization.status === "pending") {
856
+ throw new AuthStorageError(
857
+ "pending_device_authorization",
858
+ "[Sixb] Device authorization is pending."
859
+ )
860
+ }
861
+ if (authorization.status === "denied") {
862
+ throw new AuthStorageError(
863
+ "device_authorization_denied",
864
+ "[Sixb] Device authorization was denied."
865
+ )
866
+ }
867
+ if (
868
+ authorization.status !== "approved" ||
869
+ authorization.expiresAt <= input.completedAt ||
870
+ !authorization.approvedUserId ||
871
+ !authorization.approvedSessionId ||
872
+ input.accessToken.projectId !== input.projectId ||
873
+ input.accessToken.subjectType !== "user" ||
874
+ input.accessToken.subjectId !== authorization.approvedUserId ||
875
+ input.accessToken.createdBySessionId !== authorization.approvedSessionId
876
+ ) {
877
+ throw new AuthStorageError(
878
+ "invalid_device_authorization",
879
+ "[Sixb] Device authorization is invalid."
880
+ )
881
+ }
882
+ }
@@ -0,0 +1,131 @@
1
+ import type {
2
+ AuthDeviceAuthorizationStore,
3
+ CreateDeviceAuthorizationInput,
4
+ DeviceAuthorizationRecord,
5
+ } from "@sixb/core/storage"
6
+ import { AuthStorageError, MAX_PENDING_DEVICE_AUTHORIZATIONS } from "@sixb/core/storage"
7
+ import {
8
+ authLockKey,
9
+ lockAdvisoryKeys,
10
+ type PgStoreClient,
11
+ runPgTransaction,
12
+ } from "../transactions"
13
+ import type { PgAuthDeviceAuthorizationRow } from "./rows"
14
+ import { rowToDeviceAuthorizationRecord } from "./rows"
15
+ import { assertNonEmpty, mapUniqueConstraintError } from "./shared"
16
+
17
+ export class PgAuthDeviceAuthorizationStore implements AuthDeviceAuthorizationStore {
18
+ constructor(private readonly sql: PgStoreClient) {}
19
+
20
+ async create(input: CreateDeviceAuthorizationInput): Promise<DeviceAuthorizationRecord> {
21
+ return runPgTransaction(this.sql, async (tx) => {
22
+ await lockAdvisoryKeys(tx, [authLockKey("device-authorizations", input.projectId)])
23
+ await tx`
24
+ DELETE FROM auth_device_authorizations
25
+ WHERE project_id = ${input.projectId} AND expires_at <= ${input.createdAt}
26
+ `
27
+ const [pending] = await tx<{ readonly count: string | number }[]>`
28
+ SELECT COUNT(*)::bigint AS count FROM auth_device_authorizations
29
+ WHERE project_id = ${input.projectId} AND status = 'pending'
30
+ `
31
+ if (Number(pending?.count ?? 0) >= MAX_PENDING_DEVICE_AUTHORIZATIONS) {
32
+ throw new AuthStorageError(
33
+ "device_authorization_limit_exceeded",
34
+ `[Sixb] Project '${input.projectId}' has too many pending device authorizations.`
35
+ )
36
+ }
37
+ try {
38
+ const [row] = await tx<PgAuthDeviceAuthorizationRow[]>`
39
+ INSERT INTO auth_device_authorizations (
40
+ project_id, id, device_code_hash, user_code, client_name, token_name,
41
+ token_expires_at, status, created_at, expires_at
42
+ ) VALUES (
43
+ ${assertNonEmpty(input.projectId, "Project id")},
44
+ ${assertNonEmpty(input.id, "Device authorization id")},
45
+ ${assertNonEmpty(input.deviceCodeHash, "Device code hash")},
46
+ ${assertNonEmpty(input.userCode, "User code")},
47
+ ${assertNonEmpty(input.clientName, "Client name")},
48
+ ${assertNonEmpty(input.tokenName, "Token name")},
49
+ ${input.tokenExpiresAt}, 'pending', ${input.createdAt}, ${input.expiresAt}
50
+ ) RETURNING *
51
+ `
52
+ return rowToDeviceAuthorizationRecord(row)
53
+ } catch (error) {
54
+ mapUniqueConstraintError(
55
+ error,
56
+ "duplicate_device_authorization",
57
+ `[Sixb] Device authorization '${input.id}' already exists for project '${input.projectId}'.`
58
+ )
59
+ }
60
+ })
61
+ }
62
+
63
+ async getById(params: { readonly projectId: string; readonly id: string }) {
64
+ return getPgDeviceAuthorizationById(this.sql, params.projectId, params.id)
65
+ }
66
+
67
+ async getByUserCode(params: { readonly projectId: string; readonly userCode: string }) {
68
+ const [row] = await this.sql<PgAuthDeviceAuthorizationRow[]>`
69
+ SELECT * FROM auth_device_authorizations
70
+ WHERE project_id = ${params.projectId} AND user_code = ${params.userCode}
71
+ `
72
+ return row ? rowToDeviceAuthorizationRecord(row) : null
73
+ }
74
+
75
+ async approve(params: {
76
+ readonly projectId: string
77
+ readonly id: string
78
+ readonly userId: string
79
+ readonly sessionId: string
80
+ readonly approvedAt: Date
81
+ }) {
82
+ const [row] = await this.sql<PgAuthDeviceAuthorizationRow[]>`
83
+ UPDATE auth_device_authorizations
84
+ SET status = 'approved', approved_user_id = ${params.userId},
85
+ approved_session_id = ${params.sessionId}, approved_at = ${params.approvedAt}
86
+ WHERE project_id = ${params.projectId} AND id = ${params.id}
87
+ AND status = 'pending' AND expires_at > ${params.approvedAt}
88
+ AND EXISTS (
89
+ SELECT 1 FROM auth_sessions
90
+ WHERE project_id = ${params.projectId} AND id = ${params.sessionId}
91
+ AND user_id = ${params.userId} AND revoked_at IS NULL
92
+ AND expires_at > ${params.approvedAt}
93
+ AND (absolute_expires_at IS NULL OR absolute_expires_at > ${params.approvedAt})
94
+ )
95
+ RETURNING *
96
+ `
97
+ if (!row) throw invalidTransition(params.id, params.projectId)
98
+ return rowToDeviceAuthorizationRecord(row)
99
+ }
100
+
101
+ async deny(params: { readonly projectId: string; readonly id: string; readonly deniedAt: Date }) {
102
+ const [row] = await this.sql<PgAuthDeviceAuthorizationRow[]>`
103
+ UPDATE auth_device_authorizations
104
+ SET status = 'denied', denied_at = ${params.deniedAt}
105
+ WHERE project_id = ${params.projectId} AND id = ${params.id}
106
+ AND status = 'pending' AND expires_at > ${params.deniedAt}
107
+ RETURNING *
108
+ `
109
+ if (!row) throw invalidTransition(params.id, params.projectId)
110
+ return rowToDeviceAuthorizationRecord(row)
111
+ }
112
+ }
113
+
114
+ export async function getPgDeviceAuthorizationById(
115
+ sql: PgStoreClient,
116
+ projectId: string,
117
+ id: string
118
+ ) {
119
+ const [row] = await sql<PgAuthDeviceAuthorizationRow[]>`
120
+ SELECT * FROM auth_device_authorizations
121
+ WHERE project_id = ${projectId} AND id = ${id}
122
+ `
123
+ return row ? rowToDeviceAuthorizationRecord(row) : null
124
+ }
125
+
126
+ function invalidTransition(id: string, projectId: string): AuthStorageError {
127
+ return new AuthStorageError(
128
+ "invalid_device_authorization",
129
+ `[Sixb] Device authorization '${id}' is no longer pending for project '${projectId}'.`
130
+ )
131
+ }
@@ -1,6 +1,7 @@
1
1
  import type { Principal } from "@sixb/core"
2
2
  import type {
3
3
  AccessTokenRecord,
4
+ DeviceAuthorizationRecord,
4
5
  GroupMembershipRecord,
5
6
  GroupMembershipSource,
6
7
  InvitationRecord,
@@ -150,6 +151,24 @@ export interface PgAuthOidcAttemptRow {
150
151
  readonly consumed_at: PgDate | null
151
152
  }
152
153
 
154
+ export interface PgAuthDeviceAuthorizationRow {
155
+ readonly project_id: string
156
+ readonly id: string
157
+ readonly device_code_hash: string
158
+ readonly user_code: string
159
+ readonly client_name: string
160
+ readonly token_name: string
161
+ readonly token_expires_at: PgDate
162
+ readonly status: DeviceAuthorizationRecord["status"]
163
+ readonly approved_user_id: string | null
164
+ readonly approved_session_id: string | null
165
+ readonly created_at: PgDate
166
+ readonly expires_at: PgDate
167
+ readonly approved_at: PgDate | null
168
+ readonly denied_at: PgDate | null
169
+ readonly consumed_at: PgDate | null
170
+ }
171
+
153
172
  export function rowToUserRecord(row: PgAuthUserRow): UserRecord {
154
173
  return {
155
174
  id: row.id,
@@ -304,6 +323,28 @@ export function rowToOidcAuthorizationAttemptRecord(
304
323
  }
305
324
  }
306
325
 
326
+ export function rowToDeviceAuthorizationRecord(
327
+ row: PgAuthDeviceAuthorizationRow
328
+ ): DeviceAuthorizationRecord {
329
+ return {
330
+ id: row.id,
331
+ projectId: row.project_id,
332
+ deviceCodeHash: row.device_code_hash,
333
+ userCode: row.user_code,
334
+ clientName: row.client_name,
335
+ tokenName: row.token_name,
336
+ tokenExpiresAt: toDate(row.token_expires_at),
337
+ status: row.status,
338
+ approvedUserId: row.approved_user_id ?? undefined,
339
+ approvedSessionId: row.approved_session_id ?? undefined,
340
+ createdAt: toDate(row.created_at),
341
+ expiresAt: toDate(row.expires_at),
342
+ approvedAt: row.approved_at ? toDate(row.approved_at) : undefined,
343
+ deniedAt: row.denied_at ? toDate(row.denied_at) : undefined,
344
+ consumedAt: row.consumed_at ? toDate(row.consumed_at) : undefined,
345
+ }
346
+ }
347
+
307
348
  export function serializeOptionalRecord(
308
349
  value: Readonly<Record<string, unknown>> | undefined
309
350
  ): string | null {
@@ -761,6 +761,7 @@ export function mapUniqueConstraintError(
761
761
  error: unknown,
762
762
  code:
763
763
  | "duplicate_access_token"
764
+ | "duplicate_device_authorization"
764
765
  | "duplicate_identity"
765
766
  | "duplicate_invitation"
766
767
  | "duplicate_magic_link"
package/src/index.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  } from "@sixb/core/internal/storage-operation-scope"
21
21
  import {
22
22
  type AiCostStorage,
23
+ type AiLimitStorage,
23
24
  type AiUsageStorage,
24
25
  createTransactionStorageProxy,
25
26
  type ObjectStorage,
@@ -33,6 +34,7 @@ import { createPostgresStorageMigrators, dropSchema } from "./migrations"
33
34
  import { PgOntologyStorage, type PgOntologyTransactionContext } from "./ontology-storage"
34
35
  import { PgActionRunStorage } from "./pg-action-run-storage"
35
36
  import { PgAiCostStorage } from "./pg-ai-cost-storage"
37
+ import { PgAiLimitStorage } from "./pg-ai-limit-storage"
36
38
  import { PgAiUsageStorage } from "./pg-ai-usage-storage"
37
39
  import { createPgClient, type SQL, type SQLClient } from "./pg-client"
38
40
  import { PgExecutionStorage } from "./pg-execution-storage"
@@ -138,6 +140,7 @@ export class PostgresStorage implements MigrationCapableStorage {
138
140
  readonly agents: PgAgentStorage
139
141
  readonly aiUsage: AiUsageStorage
140
142
  readonly aiCosts: AiCostStorage
143
+ readonly aiLimits: AiLimitStorage
141
144
  readonly actionRuns: PgActionRunStorage
142
145
  readonly pipelineRuns: PgPipelineRunStorage
143
146
  readonly workflowRuns: PgWorkflowRunStorage
@@ -212,6 +215,7 @@ export class PostgresStorage implements MigrationCapableStorage {
212
215
  this.agents = createAgentOperationScope(stores.agents, scope)
213
216
  this.aiUsage = createOperationScopedFacade(stores.aiUsage, scope)
214
217
  this.aiCosts = createOperationScopedFacade(stores.aiCosts, scope)
218
+ this.aiLimits = createOperationScopedFacade(stores.aiLimits, scope)
215
219
  this.actionRuns = createOperationScopedFacade(stores.actionRuns, scope)
216
220
  this.pipelineRuns = createOperationScopedFacade(stores.pipelineRuns, scope)
217
221
  this.workflowRuns = createWorkflowRunOperationScope(stores.workflowRuns, scope)
@@ -258,6 +262,7 @@ export class PostgresStorage implements MigrationCapableStorage {
258
262
  transactionContext.materializations.assertCommittable()
259
263
  return result
260
264
  } finally {
265
+ await txStorage.aiLimits.settleOperations()
261
266
  transactionContext.active = false
262
267
  txStorage.ontology.deactivateSessions()
263
268
  transactionContext.materializations.deactivate()
@@ -305,7 +310,7 @@ export class PostgresStorage implements MigrationCapableStorage {
305
310
  private createTransactionStorage(
306
311
  client: PgStoreClient,
307
312
  transactionContext: PgOntologyTransactionContext
308
- ): Storage & { readonly ontology: PgOntologyStorage } {
313
+ ): Storage & { readonly ontology: PgOntologyStorage; readonly aiLimits: PgAiLimitStorage } {
309
314
  return {
310
315
  ...createPostgresStores(client, {
311
316
  runOntologyOperation: async (run) => run(client),
@@ -342,6 +347,7 @@ function createPostgresStores(
342
347
  agents: new PgAgentStorage({ sql, executions }),
343
348
  aiUsage: new PgAiUsageStorage(sql),
344
349
  aiCosts: new PgAiCostStorage(sql),
350
+ aiLimits: new PgAiLimitStorage(sql),
345
351
  actionRuns: new PgActionRunStorage(sql, executions),
346
352
  pipelineRuns: new PgPipelineRunStorage(sql, executions),
347
353
  workflowRuns: new PgWorkflowRunStorage(sql, executions),
@@ -363,6 +369,7 @@ interface PostgresStoreSet {
363
369
  readonly agents: PgAgentStorage
364
370
  readonly aiUsage: PgAiUsageStorage
365
371
  readonly aiCosts: PgAiCostStorage
372
+ readonly aiLimits: PgAiLimitStorage
366
373
  readonly actionRuns: PgActionRunStorage
367
374
  readonly pipelineRuns: PgPipelineRunStorage
368
375
  readonly workflowRuns: PgWorkflowRunStorage
@@ -0,0 +1,24 @@
1
+ -- Call-time model configuration and provider identity. Historical identity remains unknown.
2
+ ALTER TABLE ai_model_call_usage
3
+ ADD COLUMN requested_reasoning JSONB
4
+ CHECK (
5
+ requested_reasoning IS NULL OR
6
+ jsonb_typeof(requested_reasoning) IN ('string', 'object')
7
+ );
8
+ ALTER TABLE ai_model_call_usage ADD COLUMN provider_ids JSONB;
9
+
10
+ -- Upgrade the shipped valuation reason while retaining the selected-cost projection.
11
+ ALTER TABLE ai_model_call_valuations
12
+ DROP CONSTRAINT ai_model_call_valuations_reason_check;
13
+
14
+ UPDATE ai_model_call_valuations SET reason = 'missingRateCard'
15
+ WHERE reason = 'missingCatalogEntry';
16
+
17
+ ALTER TABLE ai_model_call_valuations ADD CONSTRAINT ai_model_call_valuations_reason_check
18
+ CHECK (reason IS NULL OR reason IN (
19
+ 'missingBillingIdentity',
20
+ 'missingRateCard',
21
+ 'missingUsageMeter',
22
+ 'unsupportedPricingDimension',
23
+ 'invalidUsageForFormula'
24
+ ));
@@ -0,0 +1,94 @@
1
+ CREATE TABLE ai_usage_limits (
2
+ project_id TEXT NOT NULL CHECK (length(btrim(project_id)) > 0),
3
+ id TEXT NOT NULL CHECK (length(btrim(id)) > 0),
4
+ subject_type TEXT NOT NULL CHECK (
5
+ subject_type IN ('project', 'group', 'user', 'serviceAccount')
6
+ ),
7
+ subject_id TEXT NOT NULL CHECK (
8
+ (subject_type = 'project' AND subject_id = '')
9
+ OR (subject_type <> 'project' AND length(btrim(subject_id)) > 0)
10
+ ),
11
+ meter TEXT NOT NULL CHECK (meter IN ('tokens.total', 'cost.catalogEstimated')),
12
+ currency TEXT NOT NULL CHECK (
13
+ (meter = 'tokens.total' AND currency = '')
14
+ OR (meter = 'cost.catalogEstimated' AND currency = 'USD')
15
+ ),
16
+ limit_amount NUMERIC(78, 0) NOT NULL CHECK (limit_amount >= 0),
17
+ period_kind TEXT NOT NULL CHECK (period_kind = 'calendarMonth'),
18
+ enabled BOOLEAN NOT NULL,
19
+ created_at TIMESTAMPTZ NOT NULL,
20
+ updated_at TIMESTAMPTZ NOT NULL,
21
+ PRIMARY KEY (project_id, id),
22
+ UNIQUE (project_id, subject_type, subject_id, meter, currency)
23
+ );
24
+
25
+ CREATE INDEX idx_ai_usage_limits_project_enabled
26
+ ON ai_usage_limits (project_id, enabled, subject_type, subject_id, meter, currency);
27
+
28
+ CREATE TABLE ai_usage_limit_periods (
29
+ project_id TEXT NOT NULL CHECK (length(btrim(project_id)) > 0),
30
+ subject_type TEXT NOT NULL CHECK (
31
+ subject_type IN ('project', 'group', 'user', 'serviceAccount')
32
+ ),
33
+ subject_id TEXT NOT NULL CHECK (
34
+ (subject_type = 'project' AND subject_id = '')
35
+ OR (subject_type <> 'project' AND length(btrim(subject_id)) > 0)
36
+ ),
37
+ meter TEXT NOT NULL CHECK (meter IN ('tokens.total', 'cost.catalogEstimated')),
38
+ currency TEXT NOT NULL CHECK (
39
+ (meter = 'tokens.total' AND currency = '')
40
+ OR (meter = 'cost.catalogEstimated' AND currency = 'USD')
41
+ ),
42
+ period_start TIMESTAMPTZ NOT NULL,
43
+ period_end TIMESTAMPTZ NOT NULL,
44
+ actual_amount NUMERIC(78, 0) NOT NULL DEFAULT 0 CHECK (actual_amount >= 0),
45
+ reserved_amount NUMERIC(78, 0) NOT NULL DEFAULT 0 CHECK (reserved_amount >= 0),
46
+ unknown_amount NUMERIC(78, 0) NOT NULL DEFAULT 0 CHECK (unknown_amount >= 0),
47
+ accounting_status TEXT NOT NULL DEFAULT 'complete' CHECK (
48
+ accounting_status IN ('complete', 'unavailable')
49
+ ),
50
+ updated_at TIMESTAMPTZ NOT NULL,
51
+ PRIMARY KEY (
52
+ project_id, subject_type, subject_id, meter, currency, period_start
53
+ )
54
+ );
55
+
56
+ CREATE INDEX idx_ai_usage_limit_periods_subject_period
57
+ ON ai_usage_limit_periods (
58
+ project_id, subject_type, subject_id, period_start, period_end, meter, currency
59
+ );
60
+
61
+ CREATE TABLE ai_model_call_reservations (
62
+ project_id TEXT NOT NULL CHECK (length(btrim(project_id)) > 0),
63
+ execution_id TEXT NOT NULL CHECK (length(btrim(execution_id)) > 0),
64
+ attempt BIGINT NOT NULL CHECK (attempt >= 1),
65
+ call_id TEXT NOT NULL CHECK (length(btrim(call_id)) > 0),
66
+ buckets JSONB NOT NULL CHECK (jsonb_typeof(buckets) = 'array'),
67
+ request_key TEXT NOT NULL,
68
+ period_start TIMESTAMPTZ NOT NULL,
69
+ period_end TIMESTAMPTZ NOT NULL,
70
+ state TEXT NOT NULL CHECK (state IN ('active', 'reconciled', 'unknown')),
71
+ usage_record_id TEXT,
72
+ reserved_at TIMESTAMPTZ NOT NULL,
73
+ updated_at TIMESTAMPTZ NOT NULL,
74
+ PRIMARY KEY (project_id, execution_id, attempt, call_id),
75
+ FOREIGN KEY (project_id, execution_id)
76
+ REFERENCES executions(project_id, id)
77
+ ON DELETE RESTRICT,
78
+ FOREIGN KEY (project_id, usage_record_id)
79
+ REFERENCES ai_model_call_usage(project_id, id)
80
+ ON DELETE RESTRICT,
81
+ CHECK (
82
+ (
83
+ state = 'reconciled'
84
+ AND usage_record_id IS NOT NULL
85
+ )
86
+ OR (
87
+ state <> 'reconciled'
88
+ AND usage_record_id IS NULL
89
+ )
90
+ )
91
+ );
92
+
93
+ CREATE INDEX idx_ai_model_call_reservations_active
94
+ ON ai_model_call_reservations (project_id, state, period_end, execution_id, attempt, call_id);
@@ -0,0 +1,97 @@
1
+ -- Agent executions may either use a managed service account or inherit their parent's exact
2
+ -- credentialed-user / auth-disabled authority.
3
+ ALTER TABLE executions
4
+ DROP CONSTRAINT executions_check3,
5
+ ADD CONSTRAINT executions_agent_authority_check CHECK (
6
+ (
7
+ executor_kind = 'request'
8
+ AND source_kind = 'http'
9
+ AND executor_id = source_id
10
+ AND (
11
+ (
12
+ authority_kind = 'principal'
13
+ AND requested_by_user_id IS NOT DISTINCT FROM authority_user_id
14
+ AND requested_by_service_account_id IS NOT DISTINCT FROM authority_service_account_id
15
+ )
16
+ OR (
17
+ authority_kind = 'disabled'
18
+ AND requested_by_user_id IS NULL
19
+ AND requested_by_service_account_id IS NULL
20
+ )
21
+ )
22
+ )
23
+ OR (
24
+ executor_kind IN ('action', 'pipeline', 'projection', 'rule', 'sync', 'webhook', 'workflow')
25
+ AND authority_kind = 'trustedPrimitive'
26
+ AND executor_kind = authority_primitive_kind
27
+ )
28
+ OR (
29
+ executor_kind = 'agent'
30
+ AND source_kind = 'execution'
31
+ AND (
32
+ (
33
+ authority_kind = 'principal'
34
+ AND authority_service_account_id IS NOT NULL
35
+ AND authority_session_id IS NULL
36
+ AND authority_access_token_id IS NULL
37
+ )
38
+ OR (
39
+ authority_kind = 'principal'
40
+ AND authority_user_id IS NOT NULL
41
+ AND num_nonnulls(authority_session_id, authority_access_token_id) = 1
42
+ )
43
+ OR authority_kind = 'disabled'
44
+ )
45
+ )
46
+ OR (executor_kind = 'kernel' AND authority_kind = 'kernel')
47
+ );
48
+
49
+ ALTER TABLE agent_runs
50
+ ADD COLUMN kind TEXT NOT NULL DEFAULT 'conversation'
51
+ CHECK (kind IN ('conversation', 'subagent')),
52
+ ADD COLUMN parent_run_id TEXT,
53
+ ADD COLUMN spawn_key TEXT,
54
+ ADD COLUMN spec JSONB,
55
+ ADD COLUMN result JSONB,
56
+ ALTER COLUMN thread_id DROP NOT NULL,
57
+ ALTER COLUMN agent_id DROP NOT NULL,
58
+ ALTER COLUMN trigger_message_id DROP NOT NULL,
59
+ ADD CONSTRAINT fk_agent_runs_parent
60
+ FOREIGN KEY (project_id, parent_run_id)
61
+ REFERENCES agent_runs (project_id, id)
62
+ ON DELETE RESTRICT,
63
+ ADD CONSTRAINT agent_runs_kind_fields CHECK (
64
+ (
65
+ kind = 'conversation'
66
+ AND thread_id IS NOT NULL
67
+ AND agent_id IS NOT NULL
68
+ AND trigger_message_id IS NOT NULL
69
+ AND parent_run_id IS NULL
70
+ AND spawn_key IS NULL
71
+ AND spec IS NULL
72
+ AND result IS NULL
73
+ )
74
+ OR
75
+ (
76
+ kind = 'subagent'
77
+ AND thread_id IS NULL
78
+ AND agent_id IS NULL
79
+ AND trigger_message_id IS NULL
80
+ AND parent_run_id IS NOT NULL
81
+ AND spawn_key IS NOT NULL
82
+ AND spec IS NOT NULL
83
+ )
84
+ ),
85
+ ADD CONSTRAINT agent_runs_subagent_result CHECK (
86
+ kind <> 'subagent' OR status <> 'succeeded' OR result IS NOT NULL
87
+ ),
88
+ ADD CONSTRAINT uq_agent_runs_parent_spawn_key UNIQUE (project_id, parent_run_id, spawn_key);
89
+
90
+ CREATE INDEX idx_agent_runs_project_parent_started
91
+ ON agent_runs (
92
+ project_id,
93
+ parent_run_id,
94
+ status,
95
+ COALESCE(started_at, created_at) DESC,
96
+ id DESC
97
+ );
@@ -0,0 +1,23 @@
1
+ ALTER TABLE agent_runs
2
+ DROP CONSTRAINT agent_runs_kind_fields,
3
+ ADD CONSTRAINT agent_runs_kind_fields CHECK (
4
+ (
5
+ kind = 'conversation'
6
+ AND thread_id IS NOT NULL
7
+ AND agent_id IS NOT NULL
8
+ AND trigger_message_id IS NOT NULL
9
+ AND parent_run_id IS NULL
10
+ AND spawn_key IS NULL
11
+ AND result IS NULL
12
+ )
13
+ OR
14
+ (
15
+ kind = 'subagent'
16
+ AND thread_id IS NULL
17
+ AND agent_id IS NULL
18
+ AND trigger_message_id IS NULL
19
+ AND parent_run_id IS NOT NULL
20
+ AND spawn_key IS NOT NULL
21
+ AND spec IS NOT NULL
22
+ )
23
+ );
@@ -0,0 +1,28 @@
1
+ DROP INDEX IF EXISTS idx_agent_threads_project_agent_activity;
2
+ ALTER TABLE workflow_agent_node_runs RENAME COLUMN agent_id TO actor_id;
3
+ DROP INDEX IF EXISTS idx_agent_runs_project_agent_started;
4
+
5
+ ALTER TABLE agent_threads DROP COLUMN agent_id;
6
+
7
+ ALTER TABLE agent_runs
8
+ DROP CONSTRAINT agent_runs_kind_fields,
9
+ DROP COLUMN agent_id,
10
+ ADD CONSTRAINT agent_runs_kind_fields CHECK (
11
+ (
12
+ kind = 'conversation'
13
+ AND thread_id IS NOT NULL
14
+ AND trigger_message_id IS NOT NULL
15
+ AND parent_run_id IS NULL
16
+ AND spawn_key IS NULL
17
+ AND result IS NULL
18
+ )
19
+ OR
20
+ (
21
+ kind = 'subagent'
22
+ AND thread_id IS NULL
23
+ AND trigger_message_id IS NULL
24
+ AND parent_run_id IS NOT NULL
25
+ AND spawn_key IS NOT NULL
26
+ AND spec IS NOT NULL
27
+ )
28
+ );
@@ -0,0 +1,24 @@
1
+ CREATE TABLE auth_device_authorizations (
2
+ project_id TEXT NOT NULL,
3
+ id TEXT NOT NULL,
4
+ device_code_hash TEXT NOT NULL,
5
+ user_code TEXT NOT NULL,
6
+ client_name TEXT NOT NULL,
7
+ token_name TEXT NOT NULL,
8
+ token_expires_at TIMESTAMPTZ NOT NULL,
9
+ status TEXT NOT NULL CHECK (status IN ('pending', 'approved', 'denied', 'consumed')),
10
+ approved_user_id TEXT,
11
+ approved_session_id TEXT,
12
+ created_at TIMESTAMPTZ NOT NULL,
13
+ expires_at TIMESTAMPTZ NOT NULL,
14
+ approved_at TIMESTAMPTZ,
15
+ denied_at TIMESTAMPTZ,
16
+ consumed_at TIMESTAMPTZ,
17
+ PRIMARY KEY (project_id, id),
18
+ UNIQUE (project_id, user_code),
19
+ FOREIGN KEY (project_id, approved_user_id) REFERENCES auth_users (project_id, id),
20
+ FOREIGN KEY (project_id, approved_session_id) REFERENCES auth_sessions (project_id, id)
21
+ );
22
+
23
+ CREATE INDEX idx_auth_device_authorizations_pending
24
+ ON auth_device_authorizations (project_id, status, expires_at DESC);