@vertesia/common 1.4.0 → 1.5.0-dev.20260713.113008Z
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.d.ts +31 -6
- package/lib/access-control.d.ts.map +1 -1
- package/lib/access-control.js +12 -2
- package/lib/access-control.js.map +1 -1
- package/lib/apikey.d.ts +29 -5
- package/lib/apikey.d.ts.map +1 -1
- package/lib/apikey.js.map +1 -1
- package/lib/apps.d.ts +78 -4
- package/lib/apps.d.ts.map +1 -1
- package/lib/apps.js +2 -0
- package/lib/apps.js.map +1 -1
- package/lib/audit-trail.d.ts +15 -2
- package/lib/audit-trail.d.ts.map +1 -1
- package/lib/audit-trail.js +20 -0
- package/lib/audit-trail.js.map +1 -1
- package/lib/environment.d.ts +7 -0
- package/lib/environment.d.ts.map +1 -1
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/integrations.d.ts +13 -1
- package/lib/integrations.d.ts.map +1 -1
- package/lib/integrations.js.map +1 -1
- package/lib/platform-event-validation.d.ts +45 -0
- package/lib/platform-event-validation.d.ts.map +1 -0
- package/lib/platform-event-validation.js +323 -0
- package/lib/platform-event-validation.js.map +1 -0
- package/lib/platform-event.d.ts +665 -0
- package/lib/platform-event.d.ts.map +1 -0
- package/lib/platform-event.js +2 -0
- package/lib/platform-event.js.map +1 -0
- package/lib/project.d.ts +32 -0
- package/lib/project.d.ts.map +1 -1
- package/lib/project.js.map +1 -1
- package/lib/prompt.d.ts +1 -0
- package/lib/prompt.d.ts.map +1 -1
- package/lib/prompt.js.map +1 -1
- package/lib/query.d.ts +1 -0
- package/lib/query.d.ts.map +1 -1
- package/lib/rate-limiter.d.ts +72 -0
- package/lib/rate-limiter.d.ts.map +1 -1
- package/lib/roles/types.d.ts +1 -1
- package/lib/roles/types.js +1 -1
- package/lib/secrets.d.ts +59 -0
- package/lib/secrets.d.ts.map +1 -1
- package/lib/store/agent-run.d.ts +17 -1
- package/lib/store/agent-run.d.ts.map +1 -1
- package/lib/store/process-validation.js +19 -0
- package/lib/store/process-validation.js.map +1 -1
- package/lib/store/store.d.ts +234 -10
- package/lib/store/store.d.ts.map +1 -1
- package/lib/store/store.js +3 -1
- package/lib/store/store.js.map +1 -1
- package/lib/store/workflow.d.ts +6 -5
- package/lib/store/workflow.d.ts.map +1 -1
- package/lib/store/workflow.js.map +1 -1
- package/lib/sts-token-types.d.ts +1 -0
- package/lib/sts-token-types.d.ts.map +1 -1
- package/lib/sts-token-types.js.map +1 -1
- package/lib/user.d.ts +36 -1
- package/lib/user.d.ts.map +1 -1
- package/lib/user.js +39 -0
- package/lib/user.js.map +1 -1
- package/lib/vertesia-common.js +3 -2
- package/lib/vertesia-common.js.map +1 -1
- package/package.json +4 -7
- package/src/access-control.ts +31 -6
- package/src/apikey.ts +29 -5
- package/src/apps.ts +87 -4
- package/src/audit-trail.ts +34 -18
- package/src/environment.ts +7 -0
- package/src/index.ts +25 -0
- package/src/integrations.ts +14 -1
- package/src/platform-event-validation.test.ts +225 -0
- package/src/platform-event-validation.ts +359 -0
- package/src/platform-event.ts +771 -0
- package/src/project.ts +38 -0
- package/src/prompt.ts +1 -0
- package/src/query.ts +1 -0
- package/src/rate-limiter.ts +78 -0
- package/src/roles/types.ts +1 -1
- package/src/secrets.ts +69 -0
- package/src/store/agent-run.ts +22 -1
- package/src/store/process-validation.test.ts +62 -0
- package/src/store/process-validation.ts +18 -0
- package/src/store/store.ts +263 -10
- package/src/store/workflow.ts +7 -5
- package/src/sts-token-types.ts +1 -0
- package/src/user.ts +49 -1
package/src/project.ts
CHANGED
|
@@ -269,6 +269,39 @@ export const BrowserUseProjectConfigurationSchema: JSONSchemaType<BrowserUseProj
|
|
|
269
269
|
export type ProjectSearchTier = 'standard' | 'performance';
|
|
270
270
|
export type ElasticsearchBackend = 'serverless' | 'hosted';
|
|
271
271
|
|
|
272
|
+
export interface ProjectIntakeConfiguration {
|
|
273
|
+
/**
|
|
274
|
+
* Generate table-of-content sections during standard document intake.
|
|
275
|
+
* Defaults to false.
|
|
276
|
+
*/
|
|
277
|
+
generate_toc?: boolean;
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Skip table-of-content generation when the document text exceeds this many characters.
|
|
281
|
+
* Avoids sending very large documents through the TOC interactions. Unset means no limit.
|
|
282
|
+
*/
|
|
283
|
+
generate_toc_max_size?: number;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Select or assign a content type during standard intake.
|
|
287
|
+
* Defaults to true.
|
|
288
|
+
*/
|
|
289
|
+
generate_content_type?: boolean;
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Extract document properties after content type assignment.
|
|
293
|
+
* Defaults to true.
|
|
294
|
+
*/
|
|
295
|
+
generate_properties?: boolean;
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Default content type assigned during intake when type selection finds no matching type.
|
|
299
|
+
* A type id resolvable in this project (a stored `oid:` type, an `app:` type, or a `sys:` type).
|
|
300
|
+
* Defaults to the platform `sys:GenericDocument` when unset.
|
|
301
|
+
*/
|
|
302
|
+
default_content_type?: string;
|
|
303
|
+
}
|
|
304
|
+
|
|
272
305
|
export interface ProjectConfiguration {
|
|
273
306
|
human_context?: string;
|
|
274
307
|
|
|
@@ -321,6 +354,11 @@ export interface ProjectConfiguration {
|
|
|
321
354
|
backend?: ElasticsearchBackend;
|
|
322
355
|
};
|
|
323
356
|
|
|
357
|
+
/**
|
|
358
|
+
* Standard content intake behavior.
|
|
359
|
+
*/
|
|
360
|
+
intake?: ProjectIntakeConfiguration;
|
|
361
|
+
|
|
324
362
|
/**
|
|
325
363
|
* Primary language for full-text search analysis.
|
|
326
364
|
* ISO 639-1 code (e.g., 'en', 'fr', 'ja', 'de').
|
package/src/prompt.ts
CHANGED
package/src/query.ts
CHANGED
package/src/rate-limiter.ts
CHANGED
|
@@ -32,3 +32,81 @@ export interface RateLimiterStatus {
|
|
|
32
32
|
|
|
33
33
|
// Always return an array for consistency
|
|
34
34
|
export type RateLimiterStatusResponse = RateLimiterStatus[];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A caller's own quota standing (GET /api/v1/quota/standing) — "where am I".
|
|
38
|
+
* API rate limits are genuinely per-tenant; workflow admission and the LLM limiter are
|
|
39
|
+
* global/discovered, so they are reported as posture, not per-tenant numbers.
|
|
40
|
+
*/
|
|
41
|
+
export interface QuotaStandingWindow {
|
|
42
|
+
/** Effective limit for this window (after effective account tier + per-tenant override). */
|
|
43
|
+
limit: number;
|
|
44
|
+
/** Requests used in the current window. */
|
|
45
|
+
used: number;
|
|
46
|
+
/** Requests remaining (max(0, limit - used)). */
|
|
47
|
+
remaining: number;
|
|
48
|
+
window_ms: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface QuotaStandingResource {
|
|
52
|
+
resource: string;
|
|
53
|
+
name: string;
|
|
54
|
+
burst: QuotaStandingWindow;
|
|
55
|
+
quota: QuotaStandingWindow;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface QuotaStandingAdmissionClass {
|
|
59
|
+
/** Workflow class (e.g. ExecuteConversationWorkflow). */
|
|
60
|
+
class: string;
|
|
61
|
+
/** This tenant's currently active (leased) slots for the class. */
|
|
62
|
+
tenant_active: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Effective quota tier name after account-level overrides and account-type derivation.
|
|
67
|
+
* Code-defined tier names are currently `QuotaTier`, but this remains a string because deployments
|
|
68
|
+
* can introduce quota tiers through configuration.
|
|
69
|
+
*/
|
|
70
|
+
export type QuotaEffectiveTier = string;
|
|
71
|
+
|
|
72
|
+
export interface QuotaStandingResponse {
|
|
73
|
+
tenant_id: string;
|
|
74
|
+
/**
|
|
75
|
+
* False when the limiter store (Redis) was unavailable, so `api`/`admission` are empty because
|
|
76
|
+
* standing could not be read — NOT because there are no limits. Limiters fail open in this case.
|
|
77
|
+
*/
|
|
78
|
+
available: boolean;
|
|
79
|
+
/** Deployment base tier (env QUOTA_BASE_TIER); `default` = the static limits stand. */
|
|
80
|
+
base_tier: string;
|
|
81
|
+
/**
|
|
82
|
+
* Tier used to compute the API limits below: explicit account `quota_tier`, else account_type
|
|
83
|
+
* derived tier, else `base_tier` when the account tier could not be resolved.
|
|
84
|
+
*/
|
|
85
|
+
effective_tier: QuotaEffectiveTier;
|
|
86
|
+
/** Per-resource API rate-limit standing (effective limits + current usage). */
|
|
87
|
+
api: QuotaStandingResource[];
|
|
88
|
+
/**
|
|
89
|
+
* Workflow admission: per-tenant active slots per probed class. The budget itself is global and
|
|
90
|
+
* discovered (AIMD), not a per-tenant number — this is occupancy, not a limit.
|
|
91
|
+
*/
|
|
92
|
+
admission: {
|
|
93
|
+
classes: QuotaStandingAdmissionClass[];
|
|
94
|
+
note: string;
|
|
95
|
+
};
|
|
96
|
+
/** The LLM interaction limiter is shared per environment/model, not per tenant. */
|
|
97
|
+
llm: {
|
|
98
|
+
note: string;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Lightweight per-account quota tier for the calling account — served by `GET /api/v1/quota/tier`.
|
|
104
|
+
* A cheap, cacheable read that lets another service (e.g. zeno-server's API rate limiter) resolve
|
|
105
|
+
* the caller's tier through studio-server instead of reaching into the account store directly.
|
|
106
|
+
* `tier` is the SAME value {@link QuotaStandingResponse.effective_tier} reports: the account's
|
|
107
|
+
* explicit `quota_tier`, else its account_type-derived tier, else the deployment base tier when the
|
|
108
|
+
* account tier cannot be resolved.
|
|
109
|
+
*/
|
|
110
|
+
export interface QuotaTierResponse {
|
|
111
|
+
tier: QuotaEffectiveTier;
|
|
112
|
+
}
|
package/src/roles/types.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { Permission } from '../access-control.js';
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Kind of object a
|
|
11
|
+
* Kind of object a ResourceSet's `resource_props` matches at query time. Used
|
|
12
12
|
* in `AceConditions.scope` (validated at runtime against this list) and as
|
|
13
13
|
* the prefix in JWT `content_security` keys (e.g. `collection:read`).
|
|
14
14
|
*
|
package/src/secrets.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
WebsiteCredentialRecord,
|
|
4
4
|
WebsiteCredentialSecretInput,
|
|
5
5
|
} from './browser-credentials.js';
|
|
6
|
+
import type { EventCategory } from './platform-event.js';
|
|
6
7
|
|
|
7
8
|
// First supported top-level secret kind. OAuth connector grants continue to use
|
|
8
9
|
// the OAuth/MCP token flows and can be materialized later by tools that need them.
|
|
@@ -58,3 +59,71 @@ export interface UpdateSecretRequest {
|
|
|
58
59
|
clear_totp?: boolean;
|
|
59
60
|
clear_oauth?: boolean;
|
|
60
61
|
}
|
|
62
|
+
|
|
63
|
+
export interface EventWebhookSigningSecretRequest {
|
|
64
|
+
account_id?: string;
|
|
65
|
+
project_id: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface EventWebhookSigningSecretResponse {
|
|
69
|
+
subscription_id: string;
|
|
70
|
+
secret: string;
|
|
71
|
+
secret_label: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface SignEventWebhookRequest extends EventWebhookSigningSecretRequest {
|
|
75
|
+
delivery_id: string;
|
|
76
|
+
body: string;
|
|
77
|
+
event_id: string;
|
|
78
|
+
event_category: EventCategory;
|
|
79
|
+
action: string;
|
|
80
|
+
timestamp?: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface SignEventWebhookResponse {
|
|
84
|
+
headers: Record<string, string>;
|
|
85
|
+
timestamp: number;
|
|
86
|
+
signature: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface EventIngestSigningSecretRequest {
|
|
90
|
+
account_id?: string;
|
|
91
|
+
project_id: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface EventIngestSigningSecretResponse {
|
|
95
|
+
channel_id: string;
|
|
96
|
+
secret: string;
|
|
97
|
+
secret_label: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface VerifyEventIngestSignatureRequest extends EventIngestSigningSecretRequest {
|
|
101
|
+
/** Raw request body bytes (exactly as received) the sender signed. */
|
|
102
|
+
body: string;
|
|
103
|
+
/** Signature value from the request header. */
|
|
104
|
+
signature_header: string;
|
|
105
|
+
algorithm?: 'sha256' | 'sha1';
|
|
106
|
+
encoding?: 'hex' | 'base64';
|
|
107
|
+
/** Literal prefix to strip from the header value, e.g. `sha256=`. */
|
|
108
|
+
prefix?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface VerifyEventIngestSignatureResponse {
|
|
112
|
+
valid: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface GithubInstallationTokenRequest {
|
|
116
|
+
account_id?: string;
|
|
117
|
+
project_id: string;
|
|
118
|
+
/** Numeric GitHub App installation id (from the trusted webhook payload, never agent-supplied). */
|
|
119
|
+
installation_id: string;
|
|
120
|
+
/** `owner/name` of the repo to scope the token to. Required — tokens are always repository-scoped. */
|
|
121
|
+
repo: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface GithubInstallationTokenResponse {
|
|
125
|
+
/** Short-lived, repository-scoped GitHub App installation token. */
|
|
126
|
+
token: string;
|
|
127
|
+
/** ISO-8601 expiry returned by GitHub, when available. */
|
|
128
|
+
expires_at?: string;
|
|
129
|
+
}
|
package/src/store/agent-run.ts
CHANGED
|
@@ -20,6 +20,8 @@ import type {
|
|
|
20
20
|
InteractionRef,
|
|
21
21
|
RunSource,
|
|
22
22
|
} from '../interaction.js';
|
|
23
|
+
import type { EventRef } from '../platform-event.js';
|
|
24
|
+
import type { ResourceRef } from '../refs.js';
|
|
23
25
|
import type { AgentEvent } from '../workflow-analytics.js';
|
|
24
26
|
import type { AgentToolApprovalMode } from './agent-approval.js';
|
|
25
27
|
import type { ProcessDefinitionBody, ProcessState } from './process.js';
|
|
@@ -53,7 +55,7 @@ export type AgentRunArchiveState = 'none' | 'pending' | 'archiving' | 'complete'
|
|
|
53
55
|
/**
|
|
54
56
|
* How the agent run was created.
|
|
55
57
|
*/
|
|
56
|
-
export type AgentRunType = 'api' | 'schedule';
|
|
58
|
+
export type AgentRunType = 'api' | 'schedule' | 'event_subscription';
|
|
57
59
|
|
|
58
60
|
/**
|
|
59
61
|
* Internal discriminator key for documents stored in the agent_runs collection.
|
|
@@ -130,6 +132,12 @@ export interface RunBase {
|
|
|
130
132
|
/** Schedule ID — set when this run was triggered by a Temporal schedule */
|
|
131
133
|
schedule_id?: string;
|
|
132
134
|
|
|
135
|
+
/** Event subscription ID — set when this run was triggered by the event bus. */
|
|
136
|
+
event_subscription_id?: string;
|
|
137
|
+
|
|
138
|
+
/** Event reference — set when this run was triggered by the event bus. */
|
|
139
|
+
event_ref?: EventRef;
|
|
140
|
+
|
|
133
141
|
/** Archive lifecycle state */
|
|
134
142
|
archive_state?: AgentRunArchiveState;
|
|
135
143
|
|
|
@@ -235,6 +243,13 @@ export interface AgentRun<TData = Record<string, unknown>, TProperties = Record<
|
|
|
235
243
|
|
|
236
244
|
interactionRef: InteractionRef;
|
|
237
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Resolved environment reference (name resolved from `config.environment` id).
|
|
248
|
+
* Populated by the list endpoint; may be absent on other endpoints or when the id
|
|
249
|
+
* cannot be resolved, in which case consumers should fall back to `config.environment`.
|
|
250
|
+
*/
|
|
251
|
+
environmentRef?: ResourceRef;
|
|
252
|
+
|
|
238
253
|
// --- Lifecycle ---
|
|
239
254
|
|
|
240
255
|
/** Current status of the agent run */
|
|
@@ -749,6 +764,12 @@ export interface AgentRunSearchHit {
|
|
|
749
764
|
/** Schedule ID (if schedule-triggered) */
|
|
750
765
|
schedule_id?: string;
|
|
751
766
|
|
|
767
|
+
/** Event subscription ID (if event-triggered) */
|
|
768
|
+
event_subscription_id?: string;
|
|
769
|
+
|
|
770
|
+
/** Event reference (if event-triggered) */
|
|
771
|
+
event_ref?: EventRef;
|
|
772
|
+
|
|
752
773
|
/** How the run was created */
|
|
753
774
|
source_type?: AgentRunType;
|
|
754
775
|
|
|
@@ -410,6 +410,68 @@ describe('process definition validation', () => {
|
|
|
410
410
|
expect(result.errors).toContain('human_task node "review" is missing task');
|
|
411
411
|
});
|
|
412
412
|
|
|
413
|
+
it('rejects human_task fields that use "id" instead of "name"', () => {
|
|
414
|
+
const definition = validDefinition();
|
|
415
|
+
definition.nodes.review.task = {
|
|
416
|
+
title: 'Review',
|
|
417
|
+
fields: [{ id: 'approved', type: 'boolean' } as unknown as { name: string; type: 'boolean' }],
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const result = getProcessDefinitionValidationResult(definition);
|
|
421
|
+
|
|
422
|
+
expect(result.valid).toBe(false);
|
|
423
|
+
expect(result.errors).toContain(
|
|
424
|
+
'human_task node "review" task.fields[0].name must be a non-empty string (not "id")',
|
|
425
|
+
);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('rejects human_task fields with an unsupported type', () => {
|
|
429
|
+
const definition = validDefinition();
|
|
430
|
+
definition.nodes.review.task = {
|
|
431
|
+
title: 'Review',
|
|
432
|
+
fields: [{ name: 'approved', type: 'date' } as unknown as { name: string; type: 'string' }],
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const result = getProcessDefinitionValidationResult(definition);
|
|
436
|
+
|
|
437
|
+
expect(result.valid).toBe(false);
|
|
438
|
+
expect(result.errors).toContain(
|
|
439
|
+
'human_task node "review" task.fields[0].type must be one of string, number, boolean, select, text',
|
|
440
|
+
);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('rejects select fields without options', () => {
|
|
444
|
+
const definition = validDefinition();
|
|
445
|
+
definition.nodes.review.task = {
|
|
446
|
+
title: 'Review',
|
|
447
|
+
fields: [{ name: 'decision', type: 'select' }],
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const result = getProcessDefinitionValidationResult(definition);
|
|
451
|
+
|
|
452
|
+
expect(result.valid).toBe(false);
|
|
453
|
+
expect(result.errors).toContain(
|
|
454
|
+
'human_task node "review" task.fields[0] of type "select" requires a non-empty options[] array',
|
|
455
|
+
);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('accepts human_task select fields with options', () => {
|
|
459
|
+
const definition = validDefinition();
|
|
460
|
+
definition.nodes.review.task = {
|
|
461
|
+
title: 'Review',
|
|
462
|
+
fields: [
|
|
463
|
+
{
|
|
464
|
+
name: 'decision',
|
|
465
|
+
type: 'select',
|
|
466
|
+
options: ['approved', 'rejected'],
|
|
467
|
+
required: true,
|
|
468
|
+
},
|
|
469
|
+
],
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
expect(() => validateProcessDefinitionBody(definition)).not.toThrow();
|
|
473
|
+
});
|
|
474
|
+
|
|
413
475
|
it('rejects overly deep guard rules', () => {
|
|
414
476
|
const definition = validDefinition();
|
|
415
477
|
let guard: Record<string, unknown> = { var: 'approved' };
|
|
@@ -113,6 +113,24 @@ function validateNodeDefinition(
|
|
|
113
113
|
errors.push(`human_task node "${nodeId}" task title is missing`);
|
|
114
114
|
} else if (!Array.isArray(node.task.fields)) {
|
|
115
115
|
errors.push(`human_task node "${nodeId}" task fields must be an array`);
|
|
116
|
+
} else {
|
|
117
|
+
const allowedTypes = ['string', 'number', 'boolean', 'select', 'text'];
|
|
118
|
+
(node.task.fields as unknown[]).forEach((field, i) => {
|
|
119
|
+
const path = `human_task node "${nodeId}" task.fields[${i}]`;
|
|
120
|
+
if (!isRecord(field)) {
|
|
121
|
+
errors.push(`${path} must be an object`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (typeof field.name !== 'string' || field.name.length === 0) {
|
|
125
|
+
errors.push(`${path}.name must be a non-empty string (not "id")`);
|
|
126
|
+
}
|
|
127
|
+
if (typeof field.type !== 'string' || !allowedTypes.includes(field.type)) {
|
|
128
|
+
errors.push(`${path}.type must be one of ${allowedTypes.join(', ')}`);
|
|
129
|
+
}
|
|
130
|
+
if (field.type === 'select' && !(Array.isArray(field.options) && field.options.length > 0)) {
|
|
131
|
+
errors.push(`${path} of type "select" requires a non-empty options[] array`);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
116
134
|
}
|
|
117
135
|
}
|
|
118
136
|
if (node.type === 'tool') {
|