@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.
Files changed (63) hide show
  1. package/lib/access-control-values.d.ts +12 -0
  2. package/lib/access-control-values.d.ts.map +1 -1
  3. package/lib/access-control-values.js +23 -0
  4. package/lib/access-control-values.js.map +1 -1
  5. package/lib/api-contract/components.generated.json +361 -5
  6. package/lib/api-schemas/account.d.ts +3 -10
  7. package/lib/api-schemas/account.d.ts.map +1 -1
  8. package/lib/api-schemas/account.js +5 -9
  9. package/lib/api-schemas/account.js.map +1 -1
  10. package/lib/api-schemas/agent-runs.d.ts +15 -0
  11. package/lib/api-schemas/agent-runs.d.ts.map +1 -1
  12. package/lib/api-schemas/agent-runs.js +24 -1
  13. package/lib/api-schemas/agent-runs.js.map +1 -1
  14. package/lib/api-schemas/apikey.d.ts +62 -1
  15. package/lib/api-schemas/apikey.d.ts.map +1 -1
  16. package/lib/api-schemas/apikey.js +40 -1
  17. package/lib/api-schemas/apikey.js.map +1 -1
  18. package/lib/api-schemas/project-configuration.d.ts +5 -0
  19. package/lib/api-schemas/project-configuration.d.ts.map +1 -1
  20. package/lib/api-schemas/project-configuration.js +3 -2
  21. package/lib/api-schemas/project-configuration.js.map +1 -1
  22. package/lib/api-schemas/project.d.ts +3 -0
  23. package/lib/api-schemas/project.d.ts.map +1 -1
  24. package/lib/api-schemas/registry.d.ts +81 -1
  25. package/lib/api-schemas/registry.d.ts.map +1 -1
  26. package/lib/api-schemas/registry.js +10 -1
  27. package/lib/api-schemas/registry.js.map +1 -1
  28. package/lib/apikey.d.ts +7 -0
  29. package/lib/apikey.d.ts.map +1 -1
  30. package/lib/apikey.js.map +1 -1
  31. package/lib/oauth-scopes.d.ts.map +1 -1
  32. package/lib/oauth-scopes.js +2 -1
  33. package/lib/oauth-scopes.js.map +1 -1
  34. package/lib/project-values.d.ts +1 -0
  35. package/lib/project-values.d.ts.map +1 -1
  36. package/lib/project-values.js +1 -0
  37. package/lib/project-values.js.map +1 -1
  38. package/lib/vertesia-common.js +2 -2
  39. package/lib/vertesia-common.js.map +1 -1
  40. package/lib/workflow-analytics.d.ts +35 -3
  41. package/lib/workflow-analytics.d.ts.map +1 -1
  42. package/lib/workflow-analytics.js +2 -0
  43. package/lib/workflow-analytics.js.map +1 -1
  44. package/package.json +5 -5
  45. package/src/access-control-values.ts +25 -0
  46. package/src/api-contract/components.contract.test.ts +2 -2
  47. package/src/api-contract/components.generated.json +361 -5
  48. package/src/api-schemas/account-apikey.test.ts +60 -0
  49. package/src/api-schemas/account.contract.test.ts +2 -1
  50. package/src/api-schemas/account.ts +5 -9
  51. package/src/api-schemas/agent-runs.contract.test.ts +37 -0
  52. package/src/api-schemas/agent-runs.ts +26 -1
  53. package/src/api-schemas/apikey.ts +42 -1
  54. package/src/api-schemas/bundle-isolation.test.ts +5 -41
  55. package/src/api-schemas/project-configuration.ts +3 -2
  56. package/src/api-schemas/project.contract.test.ts +15 -0
  57. package/src/api-schemas/registry.ts +16 -0
  58. package/src/api-schemas/store.contract.test.ts +4 -126
  59. package/src/apikey.ts +12 -0
  60. package/src/oauth-scopes.ts +2 -1
  61. package/src/project-values.ts +1 -0
  62. package/src/roles.test.ts +1 -0
  63. 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({ id: 'AgentRunFeedbackCounts', description: 'Ratings over the retained feedback entries.' });
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 six fields the handler applies, each optional.
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 { existsSync, readFileSync } from 'node:fs';
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
- * Guards the packaging boundary that keeps schema-authoring runtime code out of ordinary
7
- * `@vertesia/common` and `@vertesia/client` consumers.
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.skipIf(!existsSync(COMMON_BUNDLE))('@vertesia/common runtime schema isolation', () => {
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. Paths are ' +
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
- * published OpenAPI component, as the checked-in JSON artifact that zeno-server / the workflow tool /
18
- * the web editor compiles, and as the TypeScript type. This file is the gate that keeps all three
19
- * the same statement.
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
+ >;
@@ -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,
@@ -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
@@ -7,6 +7,7 @@ describe('shared role vocabulary', () => {
7
7
  expect(Object.values(SystemRoles).sort()).toEqual(
8
8
  [
9
9
  'admin',
10
+ 'app_admin',
10
11
  'app_member',
11
12
  'application',
12
13
  'auditor',
@@ -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