@vertesia/common 1.6.0-dev.20260912.110338Z → 1.6.0-dev.20260918.061604Z
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/lib/access-control-values.d.ts +12 -0
- package/lib/access-control-values.d.ts.map +1 -1
- package/lib/access-control-values.js +23 -0
- package/lib/access-control-values.js.map +1 -1
- package/lib/api-contract/components.generated.json +361 -5
- package/lib/api-schemas/account.d.ts +3 -10
- package/lib/api-schemas/account.d.ts.map +1 -1
- package/lib/api-schemas/account.js +5 -9
- package/lib/api-schemas/account.js.map +1 -1
- package/lib/api-schemas/agent-runs.d.ts +15 -0
- package/lib/api-schemas/agent-runs.d.ts.map +1 -1
- package/lib/api-schemas/agent-runs.js +24 -1
- package/lib/api-schemas/agent-runs.js.map +1 -1
- package/lib/api-schemas/apikey.d.ts +62 -1
- package/lib/api-schemas/apikey.d.ts.map +1 -1
- package/lib/api-schemas/apikey.js +40 -1
- package/lib/api-schemas/apikey.js.map +1 -1
- package/lib/api-schemas/project-configuration.d.ts +5 -0
- package/lib/api-schemas/project-configuration.d.ts.map +1 -1
- package/lib/api-schemas/project-configuration.js +3 -2
- package/lib/api-schemas/project-configuration.js.map +1 -1
- package/lib/api-schemas/project.d.ts +3 -0
- package/lib/api-schemas/project.d.ts.map +1 -1
- package/lib/api-schemas/registry.d.ts +81 -1
- package/lib/api-schemas/registry.d.ts.map +1 -1
- package/lib/api-schemas/registry.js +10 -1
- package/lib/api-schemas/registry.js.map +1 -1
- package/lib/apikey.d.ts +7 -0
- package/lib/apikey.d.ts.map +1 -1
- package/lib/apikey.js.map +1 -1
- package/lib/oauth-scopes.d.ts.map +1 -1
- package/lib/oauth-scopes.js +2 -1
- package/lib/oauth-scopes.js.map +1 -1
- package/lib/project-values.d.ts +1 -0
- package/lib/project-values.d.ts.map +1 -1
- package/lib/project-values.js +1 -0
- package/lib/project-values.js.map +1 -1
- package/lib/vertesia-common.js +2 -2
- package/lib/vertesia-common.js.map +1 -1
- package/lib/workflow-analytics.d.ts +35 -3
- package/lib/workflow-analytics.d.ts.map +1 -1
- package/lib/workflow-analytics.js +2 -0
- package/lib/workflow-analytics.js.map +1 -1
- package/package.json +5 -5
- package/src/access-control-values.ts +25 -0
- package/src/api-contract/components.contract.test.ts +2 -2
- package/src/api-contract/components.generated.json +361 -5
- package/src/api-schemas/account-apikey.test.ts +60 -0
- package/src/api-schemas/account.contract.test.ts +2 -1
- package/src/api-schemas/account.ts +5 -9
- package/src/api-schemas/agent-runs.contract.test.ts +37 -0
- package/src/api-schemas/agent-runs.ts +26 -1
- package/src/api-schemas/apikey.ts +42 -1
- package/src/api-schemas/bundle-isolation.test.ts +5 -41
- package/src/api-schemas/project-configuration.ts +3 -2
- package/src/api-schemas/project.contract.test.ts +15 -0
- package/src/api-schemas/registry.ts +16 -0
- package/src/api-schemas/store.contract.test.ts +4 -126
- package/src/apikey.ts +12 -0
- package/src/oauth-scopes.ts +2 -1
- package/src/project-values.ts +1 -0
- package/src/roles.test.ts +1 -0
- package/src/workflow-analytics.ts +38 -2
|
@@ -64,6 +64,7 @@ export const TurnEvaluationFlagSchema = z
|
|
|
64
64
|
'overhead',
|
|
65
65
|
'followup_after_answer',
|
|
66
66
|
'approval_denied',
|
|
67
|
+
'circuit_breaker',
|
|
67
68
|
])
|
|
68
69
|
.meta({ id: 'TurnEvaluationFlag', description: 'Reason behind an evaluation severity.' });
|
|
69
70
|
|
|
@@ -169,7 +170,12 @@ export const AgentRunFeedbackCountsSchema = z
|
|
|
169
170
|
last_rating: AgentRunFeedbackRatingSchema.optional(),
|
|
170
171
|
last_reason_code: AgentRunFeedbackReasonCodeSchema.optional(),
|
|
171
172
|
})
|
|
172
|
-
.meta({
|
|
173
|
+
.meta({
|
|
174
|
+
id: 'AgentRunFeedbackCounts',
|
|
175
|
+
description:
|
|
176
|
+
'Ratings over the retained feedback entries, one vote per rater: a user who rated the run and ' +
|
|
177
|
+
'one of its answers counts once, by their latest rating.',
|
|
178
|
+
});
|
|
173
179
|
|
|
174
180
|
export const AgentRunFeedbackResponseSchema = z
|
|
175
181
|
.strictObject({
|
|
@@ -213,6 +219,8 @@ const AgentRunEvaluationTotalsSchema = z.strictObject({
|
|
|
213
219
|
approvals_requested: z.number().int(),
|
|
214
220
|
approvals_denied: z.number().int(),
|
|
215
221
|
stop_requests: z.number().int(),
|
|
222
|
+
/** Absent on rollups written before the stall counters existed; read as 0. */
|
|
223
|
+
stall_trips: z.number().int().optional(),
|
|
216
224
|
});
|
|
217
225
|
|
|
218
226
|
/**
|
|
@@ -421,6 +429,9 @@ const TurnEvaluationEventSchema = z.strictObject({
|
|
|
421
429
|
approvalsRequested: z.number().int(),
|
|
422
430
|
approvalsDenied: z.number().int(),
|
|
423
431
|
stopRequests: z.number().int(),
|
|
432
|
+
/** Absent on events from producers that predate the stall counters; read as 0. */
|
|
433
|
+
stallCorrectives: z.number().int().optional(),
|
|
434
|
+
stallTrips: z.number().int().optional(),
|
|
424
435
|
followupAfterAnswer: z.boolean(),
|
|
425
436
|
severity: EvaluationSeveritySchema,
|
|
426
437
|
flags: z.array(TurnEvaluationFlagSchema),
|
|
@@ -458,6 +469,19 @@ const TurnJudgementEventSchema = z.strictObject({
|
|
|
458
469
|
detectorVersion: z.number().int().optional(),
|
|
459
470
|
});
|
|
460
471
|
|
|
472
|
+
const StallBreakerEventSchema = z.strictObject({
|
|
473
|
+
...agentEventBase,
|
|
474
|
+
eventType: z.literal(AgentEventType.StallBreaker),
|
|
475
|
+
action: z.enum(['corrective', 'trip']),
|
|
476
|
+
toolNames: z.array(z.string()),
|
|
477
|
+
repeatCount: z.number().int(),
|
|
478
|
+
stallMeasure: z.number().int(),
|
|
479
|
+
allErrored: z.boolean(),
|
|
480
|
+
iteration: z.number().int(),
|
|
481
|
+
interactive: z.boolean(),
|
|
482
|
+
workstreamId: z.string(),
|
|
483
|
+
});
|
|
484
|
+
|
|
461
485
|
export const AgentEventSchema: z.ZodType<AgentEvent> = z
|
|
462
486
|
.discriminatedUnion('eventType', [
|
|
463
487
|
AgentRunStartedEventSchema,
|
|
@@ -467,6 +491,7 @@ export const AgentEventSchema: z.ZodType<AgentEvent> = z
|
|
|
467
491
|
TurnEvaluationEventSchema,
|
|
468
492
|
FeedbackEventSchema,
|
|
469
493
|
TurnJudgementEventSchema,
|
|
494
|
+
StallBreakerEventSchema,
|
|
470
495
|
])
|
|
471
496
|
.meta({ id: 'AgentEvent' });
|
|
472
497
|
|
|
@@ -84,6 +84,14 @@ export const ApiKeySchema = z
|
|
|
84
84
|
account: z.string(),
|
|
85
85
|
project: ProjectRefSchema,
|
|
86
86
|
enabled: z.boolean(),
|
|
87
|
+
scim_provisioning: z
|
|
88
|
+
.boolean()
|
|
89
|
+
.optional()
|
|
90
|
+
.meta({
|
|
91
|
+
description:
|
|
92
|
+
'Organization-wide SCIM provisioning credential. Only account administrators may create ' +
|
|
93
|
+
'or manage these keys. May be enabled by account administrators on existing keys; disable or delete to revoke access.',
|
|
94
|
+
}),
|
|
87
95
|
created_by: z.string(),
|
|
88
96
|
updated_by: z.string(),
|
|
89
97
|
created_at: z.string().meta({ format: 'date-time' }),
|
|
@@ -147,6 +155,7 @@ export const CreateApiKeyPayloadSchema = ApiKeySchema.pick({
|
|
|
147
155
|
role: true,
|
|
148
156
|
type: true,
|
|
149
157
|
expires_at: true,
|
|
158
|
+
scim_provisioning: true,
|
|
150
159
|
properties: true,
|
|
151
160
|
clearance: true,
|
|
152
161
|
compartments: true,
|
|
@@ -155,7 +164,7 @@ export const CreateApiKeyPayloadSchema = ApiKeySchema.pick({
|
|
|
155
164
|
.meta({ id: 'CreateApiKeyPayload' });
|
|
156
165
|
|
|
157
166
|
/**
|
|
158
|
-
* What `PUT /apikeys/:keyId` accepts: the
|
|
167
|
+
* What `PUT /apikeys/:keyId` accepts: the fields the handler applies, each optional.
|
|
159
168
|
*
|
|
160
169
|
* Narrower than the create payload in both directions — `type` and `expires_at` are immutable after
|
|
161
170
|
* creation, `enabled` is only settable here — which is precisely why one shared component could not
|
|
@@ -167,6 +176,7 @@ export const UpdateApiKeyPayloadSchema = ApiKeySchema.pick({
|
|
|
167
176
|
name: true,
|
|
168
177
|
role: true,
|
|
169
178
|
enabled: true,
|
|
179
|
+
scim_provisioning: true,
|
|
170
180
|
properties: true,
|
|
171
181
|
clearance: true,
|
|
172
182
|
compartments: true,
|
|
@@ -197,3 +207,34 @@ export type ApiKeyReadResponseFromSchema = z.infer<typeof ApiKeyReadResponseSche
|
|
|
197
207
|
export type CreateApiKeyPayloadFromSchema = z.infer<typeof CreateApiKeyPayloadSchema>;
|
|
198
208
|
export type UpdateApiKeyPayloadFromSchema = z.infer<typeof UpdateApiKeyPayloadSchema>;
|
|
199
209
|
export type AuthTokenResponseFromSchema = z.infer<typeof AuthTokenResponseSchema>;
|
|
210
|
+
|
|
211
|
+
/** Project-independent organization credentials, managed only by interactive administrators. */
|
|
212
|
+
export const AccountApiKeySchema = ApiKeySchema.pick({
|
|
213
|
+
id: true,
|
|
214
|
+
name: true,
|
|
215
|
+
account: true,
|
|
216
|
+
enabled: true,
|
|
217
|
+
maskedValue: true,
|
|
218
|
+
created_by: true,
|
|
219
|
+
updated_by: true,
|
|
220
|
+
created_at: true,
|
|
221
|
+
updated_at: true,
|
|
222
|
+
expires_at: true,
|
|
223
|
+
})
|
|
224
|
+
.extend({
|
|
225
|
+
scope: z.literal('account'),
|
|
226
|
+
profile: z.literal('account_admin_v1'),
|
|
227
|
+
last_used_at: z.string().meta({ format: 'date-time' }).optional(),
|
|
228
|
+
})
|
|
229
|
+
.meta({ id: 'AccountApiKey' });
|
|
230
|
+
export const AccountApiKeyWithValueSchema = AccountApiKeySchema.extend({ value: z.string() }).meta({
|
|
231
|
+
id: 'AccountApiKeyWithValue',
|
|
232
|
+
});
|
|
233
|
+
export const AccountApiKeyArraySchema = z.array(AccountApiKeySchema).meta({ id: 'AccountApiKeyArray' });
|
|
234
|
+
export const CreateAccountApiKeyPayloadSchema = AccountApiKeySchema.pick({ name: true, expires_at: true })
|
|
235
|
+
.extend({ name: z.string().trim().min(1).regex(/\S/) })
|
|
236
|
+
.meta({ id: 'CreateAccountApiKeyPayload' });
|
|
237
|
+
export const UpdateAccountApiKeyPayloadSchema = AccountApiKeySchema.pick({ name: true, enabled: true })
|
|
238
|
+
.extend({ name: z.string().trim().min(1).regex(/\S/) })
|
|
239
|
+
.partial()
|
|
240
|
+
.meta({ id: 'UpdateAccountApiKeyPayload' });
|
|
@@ -1,37 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { gzipSync } from 'node:zlib';
|
|
3
3
|
import { describe, expect, it } from 'vitest';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `lib/index.js` is bundled wholesale into `lib/vertesia-common.js` by rolldown, and
|
|
10
|
-
* `apps/composable-ui/build/shared-libs.ts` serves that single file to the browser mapped to the
|
|
11
|
-
* bare specifier `@vertesia/common`. There is no per-consumer tree-shaking anywhere on that path,
|
|
12
|
-
* so `import type` erasure in app code cannot save us: anything runtime-reachable from the root
|
|
13
|
-
* barrel ships to every UI user.
|
|
14
|
-
*
|
|
15
|
-
* The rule this enforces: the root may re-export schema-derived TYPES (`export type`, erased by
|
|
16
|
-
* tsc) but must never runtime-export `./api-schemas/*`. `@vertesia/client` is covered too — it is
|
|
17
|
-
* shipped to the browser by the same mechanism, so a schema import there leaks just as badly. Its
|
|
18
|
-
* default and Node.js entries are checked independently because this is also an SDK dependency
|
|
19
|
-
* boundary for Node.js, serverless, CLI, and other JavaScript applications—not only a browser-size
|
|
20
|
-
* concern.
|
|
21
|
-
*
|
|
22
|
-
* Runs against build output, so each artifact is skipped when it has not been built.
|
|
6
|
+
* Keep schema-authoring code out of the common browser bundle and public root entry.
|
|
7
|
+
* Build outputs are required; Turbo builds this package before running its tests.
|
|
8
|
+
* Client packaging is tested by the client package.
|
|
23
9
|
*/
|
|
24
10
|
const path = (relative: string) => new URL(relative, import.meta.url).pathname;
|
|
25
11
|
|
|
26
12
|
const COMMON_BUNDLE = path('../../lib/vertesia-common.js');
|
|
27
13
|
const COMMON_ENTRY = path('../../lib/index.js');
|
|
28
|
-
const CLIENT_BUNDLE = path('../../../client/lib/vertesia-client.js');
|
|
29
|
-
/** `.` and `./node` in the client's exports map — the latter is `lib/nodejs/index.js`, not `lib/index.js`. */
|
|
30
|
-
const CLIENT_ENTRIES = [
|
|
31
|
-
{ label: 'client default entry (.)', file: path('../../../client/lib/index.js') },
|
|
32
|
-
{ label: 'client node entry (./node)', file: path('../../../client/lib/nodejs/index.js') },
|
|
33
|
-
];
|
|
34
|
-
|
|
35
14
|
/** Markers that prove a runtime schema import leaked into a browser artifact. */
|
|
36
15
|
const FORBIDDEN = ['zod', '_zod', 'ZodObject', 'api-schemas', 'toOpenApiComponents', 'ApiSchemaComponents'];
|
|
37
16
|
|
|
@@ -45,7 +24,7 @@ function expectNoSchemaRuntime(file: string, label: string): void {
|
|
|
45
24
|
}
|
|
46
25
|
}
|
|
47
26
|
|
|
48
|
-
describe
|
|
27
|
+
describe('@vertesia/common runtime schema isolation', () => {
|
|
49
28
|
it('does not runtime-reference the api-schemas subpath from the root entry', () => {
|
|
50
29
|
expect(readFileSync(COMMON_ENTRY, 'utf8')).not.toContain('api-schemas');
|
|
51
30
|
});
|
|
@@ -63,18 +42,3 @@ describe.skipIf(!existsSync(COMMON_BUNDLE))('@vertesia/common runtime schema iso
|
|
|
63
42
|
).toBeLessThan(MAX_COMMON_GZIP_BYTES);
|
|
64
43
|
});
|
|
65
44
|
});
|
|
66
|
-
|
|
67
|
-
describe.skipIf(!existsSync(CLIENT_BUNDLE))('@vertesia/client consumer runtime isolation', () => {
|
|
68
|
-
it('contains no zod import or schema registry code', () => {
|
|
69
|
-
// The client is served to the browser the same way common is, so importing
|
|
70
|
-
// `@vertesia/common/api-schemas` from an SDK method would ship zod to every UI user.
|
|
71
|
-
expectNoSchemaRuntime(CLIENT_BUNDLE, 'client browser bundle');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it.each(CLIENT_ENTRIES)('does not reference the api-schemas subpath from the $label', ({ label, file }) => {
|
|
75
|
-
// Asserted rather than skipped: a missing artifact means the entry moved or stopped being
|
|
76
|
-
// built, and silently passing would let a packaging regression through unnoticed.
|
|
77
|
-
expect(existsSync(file), `${label} not found at ${file} — has the exports map changed?`).toBe(true);
|
|
78
|
-
expect(readFileSync(file, 'utf8'), `${label} must not import api-schemas`).not.toContain('api-schemas');
|
|
79
|
-
});
|
|
80
|
-
});
|
|
@@ -67,11 +67,12 @@ export const ProjectSearchTierSchema = z.enum(['standard', 'performance']).meta(
|
|
|
67
67
|
export const ElasticsearchBackendSchema = z.enum(['serverless', 'hosted']).meta({ id: 'ElasticsearchBackend' });
|
|
68
68
|
|
|
69
69
|
export const ProjectSearchPropertyTypeSchema = z
|
|
70
|
-
.enum(['keyword', 'text', 'boolean', 'long', 'double', 'date', 'geo_point'])
|
|
70
|
+
.enum(['keyword', 'text', 'boolean', 'long', 'double', 'date', 'geo_point', 'nested'])
|
|
71
71
|
.meta({
|
|
72
72
|
id: 'ProjectSearchPropertyType',
|
|
73
73
|
description:
|
|
74
|
-
'Elasticsearch field types that may be explicitly assigned to content-object properties.
|
|
74
|
+
'Elasticsearch field types that may be explicitly assigned to content-object properties. ' +
|
|
75
|
+
'Declare nested object-array paths with type `nested` and their children as separate dotted paths. Paths are ' +
|
|
75
76
|
"relative to the object's `properties` field.",
|
|
76
77
|
});
|
|
77
78
|
|
|
@@ -253,6 +253,21 @@ describe('gate 2 — the closure is closed, bottom-up', () => {
|
|
|
253
253
|
});
|
|
254
254
|
});
|
|
255
255
|
|
|
256
|
+
it('accepts explicit nested paths through the project configuration contract', () => {
|
|
257
|
+
expect(ApiSchemaComponents.ProjectSearchPropertyType.enum).toContain('nested');
|
|
258
|
+
expect(
|
|
259
|
+
validateApiRequest('UpdateProjectConfigurationPayload', {
|
|
260
|
+
indexing: {
|
|
261
|
+
property_mappings: {
|
|
262
|
+
line_items: { type: 'nested' },
|
|
263
|
+
'line_items.sku': { type: 'keyword' },
|
|
264
|
+
'line_items.quantity': { type: 'long' },
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
}).valid,
|
|
268
|
+
).toBe(true);
|
|
269
|
+
});
|
|
270
|
+
|
|
256
271
|
it('publishes geo_point as an explicit project property mapping type', () => {
|
|
257
272
|
expect(ApiSchemaComponents.ProjectSearchPropertyType.enum).toContain('geo_point');
|
|
258
273
|
});
|
|
@@ -90,6 +90,9 @@ import {
|
|
|
90
90
|
TimeResolutionSchema,
|
|
91
91
|
} from './analytics.js';
|
|
92
92
|
import {
|
|
93
|
+
AccountApiKeyArraySchema,
|
|
94
|
+
AccountApiKeySchema,
|
|
95
|
+
AccountApiKeyWithValueSchema,
|
|
93
96
|
ApiKeyArraySchema,
|
|
94
97
|
ApiKeyListQuerySchema,
|
|
95
98
|
ApiKeyReadQuerySchema,
|
|
@@ -97,9 +100,11 @@ import {
|
|
|
97
100
|
ApiKeySchema,
|
|
98
101
|
ApiKeyWithValueSchema,
|
|
99
102
|
AuthTokenResponseSchema,
|
|
103
|
+
CreateAccountApiKeyPayloadSchema,
|
|
100
104
|
CreateApiKeyPayloadSchema,
|
|
101
105
|
DeleteOperationResultSchema,
|
|
102
106
|
ProjectRefArraySchema,
|
|
107
|
+
UpdateAccountApiKeyPayloadSchema,
|
|
103
108
|
UpdateApiKeyPayloadSchema,
|
|
104
109
|
} from './apikey.js';
|
|
105
110
|
import {
|
|
@@ -841,6 +846,12 @@ const IAM_AND_ACCOUNT_SCHEMAS = {
|
|
|
841
846
|
ApiKey: ApiKeySchema,
|
|
842
847
|
ApiKeyArray: ApiKeyArraySchema,
|
|
843
848
|
ApiKeyWithValue: ApiKeyWithValueSchema,
|
|
849
|
+
AccountApiKey: AccountApiKeySchema,
|
|
850
|
+
AccountApiKeyWithValue: AccountApiKeyWithValueSchema,
|
|
851
|
+
AccountApiKeyArray: AccountApiKeyArraySchema,
|
|
852
|
+
CreateAccountApiKeyPayload: CreateAccountApiKeyPayloadSchema,
|
|
853
|
+
UpdateAccountApiKeyPayload: UpdateAccountApiKeyPayloadSchema,
|
|
854
|
+
|
|
844
855
|
ApiKeyReadResponse: ApiKeyReadResponseSchema,
|
|
845
856
|
ApiKeyReadQuery: ApiKeyReadQuerySchema,
|
|
846
857
|
CreateApiKeyPayload: CreateApiKeyPayloadSchema,
|
|
@@ -2408,6 +2419,11 @@ const API_SCHEMAS: Readonly<Record<ApiComponentName, z.ZodType>> = mergeComponen
|
|
|
2408
2419
|
* objects, so a body carrying an undeclared property is rejected rather than quietly accepted.
|
|
2409
2420
|
*/
|
|
2410
2421
|
const STRICT_COMPONENTS: ReadonlySet<string> = new Set<string>([
|
|
2422
|
+
'AccountApiKey',
|
|
2423
|
+
'AccountApiKeyWithValue',
|
|
2424
|
+
'CreateAccountApiKeyPayload',
|
|
2425
|
+
'UpdateAccountApiKeyPayload',
|
|
2426
|
+
|
|
2411
2427
|
'CreateDelegationGrantPayload',
|
|
2412
2428
|
'DelegationGrant',
|
|
2413
2429
|
// Process Test Lab request, fixture, and result contracts.
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { fileURLToPath } from 'node:url';
|
|
3
1
|
import { Ajv } from 'ajv';
|
|
4
2
|
import { describe, expect, it } from 'vitest';
|
|
5
3
|
import { ContentTypeIntakePolicySchema as GeneratedIntakePolicySchema } from '../store/intake-policy-schema.generated.js';
|
|
@@ -14,9 +12,9 @@ function assertType<T extends true>(_ok: T): void {}
|
|
|
14
12
|
|
|
15
13
|
/**
|
|
16
14
|
* The intake policy is now stated once, in Zod, and reaches its consumers three ways: as the
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* canonical component, as the checked-in JSON artifact consumers compile, and as the TypeScript
|
|
16
|
+
* type. This file keeps those package-owned contracts aligned. Downstream consumers test their
|
|
17
|
+
* published API documents and generated-client fixtures separately.
|
|
20
18
|
*
|
|
21
19
|
* It also pins the behaviour changes the conversion makes, because the schema it replaced was
|
|
22
20
|
* hand-written and disagreed with the contract in both directions. Each is asserted rather than
|
|
@@ -30,101 +28,13 @@ function errors(): string {
|
|
|
30
28
|
return ajv.errorsText(validate.errors, { separator: '\n' });
|
|
31
29
|
}
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
* Reads a file from the studio repository this package is a submodule of.
|
|
35
|
-
*
|
|
36
|
-
* `undefined` when it is not there, which is the normal state in this repository's own CI: it clones
|
|
37
|
-
* composableai alone, so nothing above the submodule root exists. Two of the gates below compare
|
|
38
|
-
* against the published document and the generated-client fixture, both of which live up there — so
|
|
39
|
-
* they are skipped when they cannot be read rather than failing the whole file at import time, which
|
|
40
|
-
* is what used to happen and what left this suite red on every commit.
|
|
41
|
-
*
|
|
42
|
-
* They still run where they are the actual gate: a checkout of the parent repository, which is what
|
|
43
|
-
* the migration's own verification and the studio CI use.
|
|
44
|
-
*/
|
|
45
|
-
function readFromStudioRepo(relativePath: string): unknown {
|
|
46
|
-
try {
|
|
47
|
-
return JSON.parse(readFileSync(fileURLToPath(new URL(relativePath, import.meta.url)), 'utf8'));
|
|
48
|
-
} catch (error) {
|
|
49
|
-
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
|
|
50
|
-
throw error;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const publishedDocument = readFromStudioRepo('../../../../../packages/api-specs/vertesia-openapi.json') as
|
|
55
|
-
| { components: { schemas: Record<string, Record<string, unknown>> } }
|
|
56
|
-
| undefined;
|
|
57
|
-
const published = publishedDocument?.components.schemas ?? {};
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* The published component minus the OpenAPI-only discriminator hint the bundler drops.
|
|
61
|
-
*
|
|
62
|
-
* Written out here rather than imported from the registry on purpose: reusing the production
|
|
63
|
-
* function to check the production function would make the comparison below pass by construction.
|
|
64
|
-
* The rule is small enough to state twice, and stating it twice is what makes it a gate.
|
|
65
|
-
*/
|
|
66
|
-
function withoutDiscriminatorHints(value: unknown): unknown {
|
|
67
|
-
if (Array.isArray(value)) return value.map(withoutDiscriminatorHints);
|
|
68
|
-
if (!value || typeof value !== 'object') return value;
|
|
69
|
-
const node = value as Record<string, unknown>;
|
|
70
|
-
const restated = node.discriminator !== undefined && node.properties === undefined;
|
|
71
|
-
return Object.fromEntries(
|
|
72
|
-
Object.entries(node)
|
|
73
|
-
.filter(([key]) => key !== 'discriminator' && !(restated && (key === 'type' || key === 'required')))
|
|
74
|
-
.map(([key, item]) => [key, withoutDiscriminatorHints(item)]),
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function $defsOf(schema: unknown): unknown {
|
|
79
|
-
return (schema as { $defs: unknown }).$defs;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
describe.skipIf(!publishedDocument)('gate — generated artifact = canonical component = published component', () => {
|
|
31
|
+
describe('generated artifact and canonical component', () => {
|
|
83
32
|
it('is the exact bundle the registry emits, so the artifact is compiled output', () => {
|
|
84
33
|
// Deep equality, not a spot check: the file is generated by `pnpm run gen:schemas` from this
|
|
85
34
|
// same function, so any difference means someone hand-edited it or forgot to regenerate.
|
|
86
35
|
expect(GeneratedIntakePolicySchema).toEqual(bundleCanonicalComponent('ContentTypeIntakePolicy'));
|
|
87
36
|
});
|
|
88
37
|
|
|
89
|
-
it('describes the same shape as the component the OpenAPI document publishes', () => {
|
|
90
|
-
// The artifact re-roots `#/components/schemas/X` to `#/$defs/X` so AJV and Monaco can compile
|
|
91
|
-
// it standalone. Undoing that is what makes the two directly comparable.
|
|
92
|
-
const { $defs, ...body } = GeneratedIntakePolicySchema as { $defs: Record<string, unknown> };
|
|
93
|
-
const rerooted = JSON.parse(JSON.stringify(body).replaceAll('#/$defs/', '#/components/schemas/'));
|
|
94
|
-
expect(rerooted).toEqual(withoutDiscriminatorHints(published.ContentTypeIntakePolicy));
|
|
95
|
-
|
|
96
|
-
// ...and every definition it carries is the published component of that name, so the bundle
|
|
97
|
-
// cannot describe a private variant of a shared type.
|
|
98
|
-
for (const [name, definition] of Object.entries($defs)) {
|
|
99
|
-
const asComponent = JSON.parse(JSON.stringify(definition).replaceAll('#/$defs/', '#/components/schemas/'));
|
|
100
|
-
expect(asComponent, `$defs.${name} disagrees with the published component`).toEqual(
|
|
101
|
-
withoutDiscriminatorHints(published[name]),
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('drops the discriminator hint the published component keeps, and only that', () => {
|
|
107
|
-
// The one place the artifact and the component are allowed to differ, pinned from both sides
|
|
108
|
-
// so neither can move alone. `synthesizeDiscriminator` restates `type` and `required` on the
|
|
109
|
-
// union node for the generated Java/Go clients; the node has no `properties`, and Gemini
|
|
110
|
-
// rejects the whole tool schema over it ("requires unspecified property '_option_id'") when
|
|
111
|
-
// the artifact reaches Vertex inside `create_or_update_type`'s input schema.
|
|
112
|
-
expect(published.ModelOptions).toMatchObject({
|
|
113
|
-
discriminator: { propertyName: '_option_id' },
|
|
114
|
-
type: 'object',
|
|
115
|
-
required: ['_option_id'],
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
const bundled = ($defsOf(GeneratedIntakePolicySchema) as Record<string, Record<string, unknown>>).ModelOptions;
|
|
119
|
-
expect(bundled.discriminator).toBeUndefined();
|
|
120
|
-
expect(bundled.required).toBeUndefined();
|
|
121
|
-
expect(bundled.type).toBeUndefined();
|
|
122
|
-
// The branches are what carried the contract all along, so nothing was validated away.
|
|
123
|
-
expect((bundled.oneOf as unknown[]).length).toBe(
|
|
124
|
-
((published.ModelOptions as { oneOf: unknown[] }).oneOf ?? []).length,
|
|
125
|
-
);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
38
|
it('registers the component the alias claims, under the id the alias name requires', () => {
|
|
129
39
|
expect(Object.keys(ApiSchemaComponents)).toContain('ContentTypeIntakePolicy');
|
|
130
40
|
assertType<Equals<ContentTypeIntakePolicy, (typeof ContentTypeIntakePolicySchema)['_zod']['output']>>(true);
|
|
@@ -230,38 +140,6 @@ describe('gate — the artifact accepts exactly what the Zod schema accepts', ()
|
|
|
230
140
|
});
|
|
231
141
|
});
|
|
232
142
|
|
|
233
|
-
describe.skipIf(!publishedDocument)('the fixture the generated Java and Go clients deserialize', () => {
|
|
234
|
-
// `openapi/fixtures/content-type-intake-policy.json` is read by
|
|
235
|
-
// `openapi/java/src/test/.../ContentTypeIntakePolicyDeserializationTest.java` and by
|
|
236
|
-
// `openapi/go/tests/intake_policy_deserialization_test.go`. Asserting it HERE is what stops it
|
|
237
|
-
// going stale: a schema change that the fixture no longer satisfies fails in this package,
|
|
238
|
-
// beside the schema, rather than in a client harness that only CI runs.
|
|
239
|
-
const fixture = readFromStudioRepo('../../../../../openapi/fixtures/content-type-intake-policy.json') ?? {};
|
|
240
|
-
|
|
241
|
-
it('is a policy the canonical schema and the generated artifact both accept', () => {
|
|
242
|
-
expect(ContentTypeIntakePolicySchema.safeParse(fixture).success).toBe(true);
|
|
243
|
-
expect(validate(fixture), errors()).toBe(true);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it('exercises the shapes the number -> integer correction changed', () => {
|
|
247
|
-
// The fields whose generated Java type moved from Double to Integer and whose Go type moved
|
|
248
|
-
// from float32 to int32. A fixture that omitted them would let the clients compile and
|
|
249
|
-
// deserialize while proving nothing about the change this batch makes.
|
|
250
|
-
const policy = fixture as ContentTypeIntakePolicy;
|
|
251
|
-
expect(policy.extraction?.max_pages).toBe(6);
|
|
252
|
-
expect(policy.extraction?.page_ranges).toEqual([
|
|
253
|
-
[1, 2],
|
|
254
|
-
[-1, -1],
|
|
255
|
-
]);
|
|
256
|
-
expect(policy.text_conversion?.render_dpi).toBe(150);
|
|
257
|
-
expect(policy.extraction?.vision?.max_image_tokens).toBe(9000);
|
|
258
|
-
expect(policy.extraction?.grounding?.window_pages).toBe(4);
|
|
259
|
-
// ...and the discriminated union, which is what a Go oneOf decoder gets wrong when the
|
|
260
|
-
// discriminator is missing from the document.
|
|
261
|
-
expect(policy.extraction?.config?.model_options?._option_id).toBe('vertexai-claude');
|
|
262
|
-
});
|
|
263
|
-
});
|
|
264
|
-
|
|
265
143
|
describe('the TypeScript type says what the schema says', () => {
|
|
266
144
|
it('widens page ranges to number[][], which is what the component has always described', () => {
|
|
267
145
|
// `[number, number][]` is what the interface used to claim. The published component is
|
package/src/apikey.ts
CHANGED
|
@@ -80,6 +80,8 @@ export type ApiKeyListQuery = z.infer<typeof ApiKeyListQuerySchema>;
|
|
|
80
80
|
export type ApiKeyReadQuery = z.infer<typeof ApiKeyReadQuerySchema>;
|
|
81
81
|
|
|
82
82
|
export interface AuthTokenPayload {
|
|
83
|
+
credential_scope?: 'project' | 'account';
|
|
84
|
+
credential_profile?: 'account_admin_v1';
|
|
83
85
|
delegation?: import('./delegation.js').DelegationTokenClaim;
|
|
84
86
|
sub: string;
|
|
85
87
|
name: string;
|
|
@@ -194,3 +196,13 @@ export enum PrincipalType {
|
|
|
194
196
|
Agent = 'agent',
|
|
195
197
|
Schedule = 'schedule',
|
|
196
198
|
}
|
|
199
|
+
|
|
200
|
+
export type AccountApiKey = z.infer<typeof import('./api-schemas/apikey.js').AccountApiKeySchema>;
|
|
201
|
+
export type AccountApiKeyWithValue = z.infer<typeof import('./api-schemas/apikey.js').AccountApiKeyWithValueSchema>;
|
|
202
|
+
export type AccountApiKeyArray = z.infer<typeof import('./api-schemas/apikey.js').AccountApiKeyArraySchema>;
|
|
203
|
+
export type CreateAccountApiKeyPayload = z.infer<
|
|
204
|
+
typeof import('./api-schemas/apikey.js').CreateAccountApiKeyPayloadSchema
|
|
205
|
+
>;
|
|
206
|
+
export type UpdateAccountApiKeyPayload = z.infer<
|
|
207
|
+
typeof import('./api-schemas/apikey.js').UpdateAccountApiKeyPayloadSchema
|
|
208
|
+
>;
|
package/src/oauth-scopes.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Permission } from './access-control-values.js';
|
|
1
|
+
import { ACCOUNT_SCOPED_PERMISSIONS, Permission } from './access-control-values.js';
|
|
2
2
|
|
|
3
3
|
export const OAUTH_SCOPE_OPENID = 'openid';
|
|
4
4
|
export const OAUTH_SCOPE_PROFILE = 'profile';
|
|
@@ -12,6 +12,7 @@ export const OAUTH_STANDARD_SCOPES = [OAUTH_SCOPE_OPENID, OAUTH_SCOPE_PROFILE, O
|
|
|
12
12
|
* hand out secret material, or are internal to the platform's own consoles.
|
|
13
13
|
*/
|
|
14
14
|
const NON_OAUTH_PERMISSION_SCOPES = new Set<Permission>([
|
|
15
|
+
...ACCOUNT_SCOPED_PERMISSIONS,
|
|
15
16
|
Permission.schedule_delegate,
|
|
16
17
|
Permission.api_key_create,
|
|
17
18
|
Permission.api_key_update,
|
package/src/project-values.ts
CHANGED
|
@@ -24,6 +24,7 @@ export enum ResourceVisibility {
|
|
|
24
24
|
export enum SystemRoles {
|
|
25
25
|
owner = 'owner', // all permissions
|
|
26
26
|
admin = 'admin', // all permissions
|
|
27
|
+
app_admin = 'app_admin', // all permissions but studio:access — admin driving a custom admin UI
|
|
27
28
|
manager = 'manager', // all permissions but manage_account, manage_billing
|
|
28
29
|
developer = 'developer', // all permissions but manage_account, manage_billing, manage_roles, delete
|
|
29
30
|
application = 'application', // executor + request_pk
|
package/src/roles.test.ts
CHANGED
|
@@ -25,6 +25,8 @@ export enum AgentEventType {
|
|
|
25
25
|
Feedback = 'feedback',
|
|
26
26
|
/** Verdict of the LLM judge on one turn, emitted by the judge workflow */
|
|
27
27
|
TurnJudgement = 'turn_judgement',
|
|
28
|
+
/** The stall circuit breaker acted: a corrective was injected, or the loop was stopped */
|
|
29
|
+
StallBreaker = 'stall_breaker',
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
/**
|
|
@@ -81,7 +83,8 @@ export type TurnEvaluationFlag =
|
|
|
81
83
|
| 'high_gather'
|
|
82
84
|
| 'overhead'
|
|
83
85
|
| 'followup_after_answer'
|
|
84
|
-
| 'approval_denied'
|
|
86
|
+
| 'approval_denied'
|
|
87
|
+
| 'circuit_breaker';
|
|
85
88
|
|
|
86
89
|
/** Coarse class of a tool error, derived from the error text. */
|
|
87
90
|
export type ToolErrorClass = 'schema' | 'platform' | 'config' | 'environment' | 'other';
|
|
@@ -313,6 +316,31 @@ export interface TurnToolObservation {
|
|
|
313
316
|
durationMs: number;
|
|
314
317
|
}
|
|
315
318
|
|
|
319
|
+
/** What the stall circuit breaker did. */
|
|
320
|
+
export type StallBreakerAction = 'corrective' | 'trip';
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Emitted the moment the stall circuit breaker acts on a repeated tool call: `corrective` when it
|
|
324
|
+
* appends its warning to the tool results, `trip` when the model repeated anyway and the loop was
|
|
325
|
+
* stopped (the run fails, or an interactive run hands control back to the user). The closing turn
|
|
326
|
+
* evaluation counts these too; this row carries the tools and the measure at the time.
|
|
327
|
+
*/
|
|
328
|
+
export interface StallBreakerEvent extends BaseAgentEvent {
|
|
329
|
+
eventType: AgentEventType.StallBreaker;
|
|
330
|
+
action: StallBreakerAction;
|
|
331
|
+
/** Tools of the repeated call set */
|
|
332
|
+
toolNames: string[];
|
|
333
|
+
/** Consecutive repeats of the identical call set */
|
|
334
|
+
repeatCount: number;
|
|
335
|
+
/** The measure the breaker acted on: the repeat count, or the windowed unproductive run */
|
|
336
|
+
stallMeasure: number;
|
|
337
|
+
/** Whether every result of the repeated call set was an error */
|
|
338
|
+
allErrored: boolean;
|
|
339
|
+
iteration: number;
|
|
340
|
+
interactive: boolean;
|
|
341
|
+
workstreamId: string;
|
|
342
|
+
}
|
|
343
|
+
|
|
316
344
|
/**
|
|
317
345
|
* Emitted when an agent turn ends. One turn is the agent work between two user hand-offs.
|
|
318
346
|
* Deterministic: computed inside the workflow from what the workflow observed.
|
|
@@ -378,6 +406,13 @@ export interface TurnEvaluationEvent extends BaseAgentEvent {
|
|
|
378
406
|
approvalsRequested: number;
|
|
379
407
|
approvalsDenied: number;
|
|
380
408
|
stopRequests: number;
|
|
409
|
+
/**
|
|
410
|
+
* Stall correctives injected: the model was warned it kept repeating the same call.
|
|
411
|
+
* Absent on events from producers that predate the counter; read as 0.
|
|
412
|
+
*/
|
|
413
|
+
stallCorrectives?: number;
|
|
414
|
+
/** Stall circuit-breaker trips: the model ignored the corrective and the loop was stopped. Same caveat. */
|
|
415
|
+
stallTrips?: number;
|
|
381
416
|
/** An unprompted user message after an answer was followed by substantive tool work */
|
|
382
417
|
followupAfterAnswer: boolean;
|
|
383
418
|
severity: EvaluationSeverity;
|
|
@@ -474,7 +509,8 @@ export type AgentEvent =
|
|
|
474
509
|
| ToolCallEvent
|
|
475
510
|
| TurnEvaluationEvent
|
|
476
511
|
| FeedbackEvent
|
|
477
|
-
| TurnJudgementEvent
|
|
512
|
+
| TurnJudgementEvent
|
|
513
|
+
| StallBreakerEvent;
|
|
478
514
|
|
|
479
515
|
/**
|
|
480
516
|
* Workflow Analytics Types
|