@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.
- package/dist/agents/rows.d.ts +8 -5
- package/dist/agents/rows.d.ts.map +1 -1
- package/dist/agents/runs.d.ts +3 -2
- package/dist/agents/runs.d.ts.map +1 -1
- package/dist/agents/threads.d.ts.map +1 -1
- package/dist/ai-cost-groups.d.ts +5 -0
- package/dist/ai-cost-groups.d.ts.map +1 -0
- package/dist/auth-storage/auth-storage.d.ts +4 -1
- package/dist/auth-storage/auth-storage.d.ts.map +1 -1
- package/dist/auth-storage/device-authorizations.d.ts +29 -0
- package/dist/auth-storage/device-authorizations.d.ts.map +1 -0
- package/dist/auth-storage/rows.d.ts +19 -1
- package/dist/auth-storage/rows.d.ts.map +1 -1
- package/dist/auth-storage/shared.d.ts +1 -1
- package/dist/auth-storage/shared.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1721 -393
- package/dist/index.js.map +17 -14
- package/dist/migrations.d.ts.map +1 -1
- package/dist/pg-ai-cost-storage.d.ts +2 -1
- package/dist/pg-ai-cost-storage.d.ts.map +1 -1
- package/dist/pg-ai-limit-storage.d.ts +34 -0
- package/dist/pg-ai-limit-storage.d.ts.map +1 -0
- package/dist/pg-ai-usage-storage.d.ts +28 -0
- package/dist/pg-ai-usage-storage.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/agents/rows.ts +38 -9
- package/src/agents/runs.ts +148 -11
- package/src/agents/threads.ts +1 -17
- package/src/ai-cost-groups.ts +102 -0
- package/src/auth-storage/auth-storage.ts +84 -0
- package/src/auth-storage/device-authorizations.ts +131 -0
- package/src/auth-storage/rows.ts +41 -0
- package/src/auth-storage/shared.ts +1 -0
- package/src/index.ts +8 -1
- package/src/migrations/029-model-accounting.sql +24 -0
- package/src/migrations/030-ai-usage-limits.sql +94 -0
- package/src/migrations/031-subagent-runs.sql +97 -0
- package/src/migrations/032-conversation-run-spec.sql +23 -0
- package/src/migrations/033-retire-agent-definitions.sql +28 -0
- package/src/migrations/034-device-authorizations.sql +24 -0
- package/src/migrations.ts +18 -0
- package/src/pg-ai-cost-storage.ts +71 -24
- package/src/pg-ai-limit-storage.ts +901 -0
- package/src/pg-ai-usage-storage.ts +34 -0
- 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
|
+
}
|
package/src/auth-storage/rows.ts
CHANGED
|
@@ -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 {
|
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);
|