@oxygen-agent/cli 1.720.7 → 1.739.1

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 (42) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.js +27 -12
  3. package/dist/index.js +227 -63
  4. package/dist/skills.js +2 -2
  5. package/node_modules/@oxygen/shared/dist/cli-result.js +2 -0
  6. package/node_modules/@oxygen/shared/dist/crm-activity-events.d.ts +20 -0
  7. package/node_modules/@oxygen/shared/dist/crm-activity-events.js +5 -0
  8. package/node_modules/@oxygen/shared/dist/future-signup-events.d.ts +27 -0
  9. package/node_modules/@oxygen/shared/dist/future-signup-events.js +90 -0
  10. package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.d.ts +320 -0
  11. package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.js +1233 -0
  12. package/node_modules/@oxygen/shared/dist/index.d.ts +6 -0
  13. package/node_modules/@oxygen/shared/dist/index.js +6 -0
  14. package/node_modules/@oxygen/shared/dist/notetaker-events.d.ts +4 -0
  15. package/node_modules/@oxygen/shared/dist/notetaker-events.js +23 -0
  16. package/node_modules/@oxygen/shared/dist/plain-support-events.d.ts +40 -0
  17. package/node_modules/@oxygen/shared/dist/plain-support-events.js +43 -0
  18. package/node_modules/@oxygen/shared/dist/redaction.js +6 -1
  19. package/node_modules/@oxygen/shared/dist/user-capability-routing.d.ts +2 -0
  20. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +29 -0
  21. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  22. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  23. package/node_modules/@oxygen/shared/dist/webhook-headers.d.ts +10 -0
  24. package/node_modules/@oxygen/shared/dist/webhook-headers.js +48 -0
  25. package/node_modules/@oxygen/shared/dist/workspace-event-catalog.d.ts +31 -1
  26. package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +112 -1
  27. package/node_modules/@oxygen/workflows/dist/graph/expression.js +7 -5
  28. package/node_modules/@oxygen/workflows/dist/graph/lint.d.ts +9 -0
  29. package/node_modules/@oxygen/workflows/dist/graph/lint.js +409 -7
  30. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +513 -0
  31. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.js +104 -1
  32. package/node_modules/@oxygen/workflows/dist/graph/params.d.ts +2 -0
  33. package/node_modules/@oxygen/workflows/dist/graph/params.js +15 -0
  34. package/node_modules/@oxygen/workflows/dist/graph/remap.js +12 -3
  35. package/node_modules/@oxygen/workflows/dist/graph/topology.d.ts +9 -0
  36. package/node_modules/@oxygen/workflows/dist/graph/topology.js +33 -1
  37. package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +123 -16
  38. package/node_modules/@oxygen/workflows/dist/graph/types.js +45 -0
  39. package/node_modules/@oxygen/workflows/dist/index.d.ts +94 -2
  40. package/node_modules/@oxygen/workflows/dist/index.js +169 -26
  41. package/node_modules/@oxygen/workflows/dist/portable.js +3 -1
  42. package/package.json +1 -1
@@ -6,12 +6,13 @@
6
6
  // validateTrigger from ../index.js, which pulls node:vm and node:crypto. This is
7
7
  // why ./index.ts (the "@oxygen/workflows/graph" entry) does not re-export this
8
8
  // module — the editor lints through the API, not in the bundle.
9
+ import * as vm from "node:vm";
9
10
  import { validateFormulaExpression } from "@oxygen/formula";
10
- import { isWorkflowToolRejected, lintRecipeManifest, validateOptionalMaxCredits, validatePureFunctionSource, validateTrigger, } from "../index.js";
11
+ import { isWorkflowToolRejected, lintRecipeManifest, lintWorkflowCodeSourceSafety, validateOptionalMaxCredits, validatePureFunctionSource, validateTrigger, } from "../index.js";
11
12
  import { UNARY_COMPARE_OPS, WORKFLOW_COMPARE_OPS, compileConditionPattern, parseScopePath, SCOPE_PATH_ROOTS, } from "./expression.js";
12
13
  import { missingToolParams, unknownToolParams } from "./params.js";
13
- import { danglingEdges, findIllegalCycles, loopBodyNodeIds, reachableNodeIds } from "./topology.js";
14
- import { MAX_WORKFLOW_LOOP_ITERATIONS, MAX_WORKFLOW_NODE_RETRY_ATTEMPTS, MAX_WORKFLOW_NODE_RETRY_WAIT_SECONDS, RESERVED_EDGE_HANDLES, RESERVED_NODE_IDS, WORKFLOW_GRAPH_COMPILER_VERSION, WORKFLOW_GRAPH_MANIFEST_VERSION, LOOP_BODY_HANDLE, LOOP_DONE_HANDLE, } from "./types.js";
14
+ import { danglingEdges, findIllegalCycles, loopBodyNodeIds, reachableNodeIds, workflowCodeSandboxInvocationUpperBound, } from "./topology.js";
15
+ import { MAX_WORKFLOW_LOOP_ITERATIONS, MAX_WORKFLOW_NODE_RETRY_ATTEMPTS, MAX_WORKFLOW_NODE_RETRY_WAIT_SECONDS, RESERVED_EDGE_HANDLES, RESERVED_NODE_IDS, isWorkflowCodeFieldName, WORKFLOW_CODE_V1_LANGUAGE, WORKFLOW_CODE_V1_MAX_INPUT_BYTES, WORKFLOW_CODE_V1_MAX_INPUTS, WORKFLOW_CODE_V1_MAX_OUTPUT_BYTES, WORKFLOW_CODE_V1_MAX_SCHEMA_BYTES, WORKFLOW_CODE_V1_MAX_SCHEMA_DEPTH, WORKFLOW_CODE_V1_MAX_SCHEMA_NODES, WORKFLOW_CODE_V1_MAX_SCHEMA_PROPERTIES, WORKFLOW_CODE_V1_MAX_SANDBOX_INVOCATIONS_PER_RUN, WORKFLOW_CODE_V1_MAX_SOURCE_BYTES, WORKFLOW_CODE_V1_MAX_TIMEOUT_MS, WORKFLOW_CODE_V1_MIN_INPUT_BYTES, WORKFLOW_CODE_V1_MIN_OUTPUT_BYTES, WORKFLOW_CODE_V1_MIN_TIMEOUT_MS, WORKFLOW_CODE_V1_RUNTIME, WORKFLOW_CODE_V1_VERSION, WORKFLOW_GRAPH_COMPILER_VERSION, WORKFLOW_GRAPH_MANIFEST_VERSION, LOOP_BODY_HANDLE, LOOP_DONE_HANDLE, } from "./types.js";
15
16
  const NODE_KINDS = new Set([
16
17
  "trigger",
17
18
  "tool",
@@ -31,6 +32,53 @@ const STEP_MODES = new Set(["dry_run", "live", "smoke_test"]);
31
32
  // A condition tree is authored in a UI; anything deeper than this is either a
32
33
  // generator gone wrong or an attempt to make the linter recurse forever.
33
34
  const MAX_CONDITION_DEPTH = 12;
35
+ const WORKFLOW_CODE_JSON_TYPES = new Set([
36
+ "array",
37
+ "boolean",
38
+ "integer",
39
+ "null",
40
+ "number",
41
+ "object",
42
+ "string",
43
+ ]);
44
+ const WORKFLOW_CODE_SCHEMA_KEYWORDS = new Set([
45
+ "type",
46
+ "const",
47
+ "enum",
48
+ "anyOf",
49
+ "properties",
50
+ "required",
51
+ "additionalProperties",
52
+ "items",
53
+ "title",
54
+ "description",
55
+ "default",
56
+ "examples",
57
+ ]);
58
+ const WORKFLOW_CODE_SCHEMA_ANNOTATIONS = new Set([
59
+ "title",
60
+ "description",
61
+ "default",
62
+ "examples",
63
+ ]);
64
+ const WORKFLOW_CODE_FIELDS = new Set([
65
+ "version",
66
+ "language",
67
+ "runtime",
68
+ "source",
69
+ "inputs",
70
+ "output_schema",
71
+ "limits",
72
+ ]);
73
+ const WORKFLOW_CODE_LIMIT_FIELDS = new Set([
74
+ "timeout_ms",
75
+ "max_input_bytes",
76
+ "max_output_bytes",
77
+ ]);
78
+ // Code v1 deliberately takes one named input bag. Requiring the complete
79
+ // expression (rather than a body snippet) makes the stored source portable to a
80
+ // future executor without guessing how it should be wrapped.
81
+ const WORKFLOW_CODE_FUNCTION_EXPRESSION = /^(?:(?:async\s+)?(?:\(\s*inputs\s*\)|inputs)\s*=>|(?:async\s+)?function(?:\s+[A-Za-z_$][\w$]*)?\s*\(\s*inputs\s*\))/;
34
82
  function isRecord(value) {
35
83
  return typeof value === "object" && value !== null && !Array.isArray(value);
36
84
  }
@@ -88,6 +136,25 @@ export function assertWorkflowGraphManifest(value, options = {}) {
88
136
  const first = result.issues[0];
89
137
  throw new Error(first ? `${first.code}: ${first.message}` : "Invalid workflow graph manifest.");
90
138
  }
139
+ /**
140
+ * Validate one nested oxygen-js-v1 envelope without requiring its surrounding
141
+ * graph. Runtime entry points call this again immediately before sandbox
142
+ * creation; ref syntax is checked here, while graph node reachability remains a
143
+ * manifest-lint concern.
144
+ */
145
+ export function lintWorkflowCodeV1Contract(value) {
146
+ const issues = [];
147
+ const add = (path, code, message) => issues.push({ path, code, message });
148
+ lintWorkflowCodeV1(value, "$", add, UNSCOPED_NODE_SCOPE);
149
+ return { ok: issues.length === 0, issues };
150
+ }
151
+ export function assertWorkflowCodeV1Contract(value) {
152
+ const result = lintWorkflowCodeV1Contract(value);
153
+ if (result.ok)
154
+ return;
155
+ const first = result.issues[0];
156
+ throw new Error(first ? `${first.code}: ${first.message}` : "Invalid oxygen-js-v1 code contract.");
157
+ }
91
158
  // ---------------------------------------------------------------------------
92
159
  // Manifest metadata
93
160
  // ---------------------------------------------------------------------------
@@ -108,6 +175,11 @@ function lintWorkflowMetadata(workflow, add) {
108
175
  add("$.workflow.status", "invalid_workflow_status", "Workflow status must be active or disabled.");
109
176
  }
110
177
  }
178
+ const UNSCOPED_NODE_SCOPE = {
179
+ ids: new Set(),
180
+ loops: new Set(),
181
+ validateNodeIds: false,
182
+ };
111
183
  function nodeScopeIndex(value) {
112
184
  const ids = new Set();
113
185
  const loops = new Set();
@@ -274,13 +346,19 @@ node, path, add, scope, options) {
274
346
  }
275
347
  }
276
348
  function lintCodeNode(node, path, add, scope, options) {
277
- const hasSource = typeof node.run_source === "string";
278
- const hasRecipe = isRecord(node.recipe);
279
- if (hasSource === hasRecipe) {
280
- add(path, "ambiguous_code_runtime", "A code node must declare exactly one executable runtime: run_source or recipe.");
349
+ const hasSource = node.run_source !== undefined;
350
+ const hasRecipe = node.recipe !== undefined;
351
+ const hasTypedCode = node.code !== undefined;
352
+ const runtimeCount = Number(hasSource) + Number(hasRecipe) + Number(hasTypedCode);
353
+ if (runtimeCount !== 1) {
354
+ add(path, "ambiguous_code_runtime", "A code node must declare exactly one executable runtime: run_source, recipe, or code.");
281
355
  return;
282
356
  }
283
357
  if (hasSource) {
358
+ if (typeof node.run_source !== "string") {
359
+ add(`${path}.run_source`, "unsafe_code_source", "Legacy code run_source must be a string.");
360
+ return;
361
+ }
284
362
  // v2 source nodes run through the same sandbox as v1 transform steps, so
285
363
  // they clear the same purity scan; every issue it raises is re-labelled to
286
364
  // the v2 code so callers match on one code for "this source is not safe".
@@ -291,6 +369,20 @@ function lintCodeNode(node, path, add, scope, options) {
291
369
  }
292
370
  return;
293
371
  }
372
+ if (hasTypedCode) {
373
+ lintWorkflowCodeV1(node.code, `${path}.code`, add, scope);
374
+ if (node.configuration !== undefined) {
375
+ add(`${path}.configuration`, "unexpected_recipe_configuration", "Typed code nodes do not use recipe configuration.");
376
+ }
377
+ if (node.legacy_context !== undefined) {
378
+ add(`${path}.legacy_context`, "unexpected_legacy_context", "Typed code nodes do not use legacy_context.");
379
+ }
380
+ return;
381
+ }
382
+ if (!isRecord(node.recipe)) {
383
+ add(`${path}.recipe`, "invalid_recipe_manifest", "Durable recipe runtime must be an object.");
384
+ return;
385
+ }
294
386
  // The graph owns configuration as value refs, which are resolved only at run
295
387
  // time. The standalone recipe linter validates concrete JSON values, so lint
296
388
  // the executable envelope without its literal configuration and validate the
@@ -332,6 +424,309 @@ function lintCodeNode(node, path, add, scope, options) {
332
424
  }
333
425
  }
334
426
  }
427
+ function lintWorkflowCodeV1(value, path, add, scope) {
428
+ if (!isRecord(value)) {
429
+ add(path, "invalid_code_contract", "Typed code must be an oxygen-js-v1 object.");
430
+ return;
431
+ }
432
+ for (const key of Object.keys(value)) {
433
+ if (!WORKFLOW_CODE_FIELDS.has(key)) {
434
+ add(`${path}.${key}`, "unknown_code_field", `Typed code has no field called '${key}'.`);
435
+ }
436
+ }
437
+ if (value.version !== WORKFLOW_CODE_V1_VERSION) {
438
+ add(`${path}.version`, "invalid_code_version", `Typed code version must be ${WORKFLOW_CODE_V1_VERSION}.`);
439
+ }
440
+ if (value.language !== WORKFLOW_CODE_V1_LANGUAGE) {
441
+ add(`${path}.language`, "invalid_code_language", `Typed code language must be '${WORKFLOW_CODE_V1_LANGUAGE}'.`);
442
+ }
443
+ if (value.runtime !== WORKFLOW_CODE_V1_RUNTIME) {
444
+ add(`${path}.runtime`, "invalid_code_runtime", `Typed code runtime must be '${WORKFLOW_CODE_V1_RUNTIME}'.`);
445
+ }
446
+ lintWorkflowCodeSource(value.source, `${path}.source`, add);
447
+ lintWorkflowCodeInputs(value.inputs, `${path}.inputs`, add, scope);
448
+ lintWorkflowCodeOutputSchema(value.output_schema, `${path}.output_schema`, add);
449
+ lintWorkflowCodeLimits(value.limits, `${path}.limits`, add);
450
+ }
451
+ function lintWorkflowCodeSource(value, path, add) {
452
+ if (typeof value !== "string") {
453
+ add(path, "invalid_code_source", "Typed code source must be a complete JavaScript function expression.");
454
+ return;
455
+ }
456
+ if (Buffer.byteLength(value, "utf8") > WORKFLOW_CODE_V1_MAX_SOURCE_BYTES) {
457
+ add(path, "code_source_too_large", `Typed code source cannot exceed ${WORKFLOW_CODE_V1_MAX_SOURCE_BYTES} UTF-8 bytes.`);
458
+ return;
459
+ }
460
+ for (const issue of lintWorkflowCodeSourceSafety(value, path)) {
461
+ add(issue.path, "unsafe_code_source", issue.message);
462
+ }
463
+ const source = value.trim();
464
+ if (!WORKFLOW_CODE_FUNCTION_EXPRESSION.test(source)) {
465
+ add(path, "invalid_code_source", "Typed code source must be a complete function expression accepting one 'inputs' argument.");
466
+ return;
467
+ }
468
+ try {
469
+ // Compile only; never execute authored code during lint. Wrapping as an
470
+ // expression rejects legacy body snippets such as `return inputs.x;`.
471
+ new vm.Script(`"use strict";\nconst __oxygen_code = (\n${source}\n);`);
472
+ }
473
+ catch {
474
+ add(path, "invalid_code_source", "Typed code source is not a valid JavaScript function expression.");
475
+ }
476
+ }
477
+ function lintWorkflowCodeInputs(value, path, add, scope) {
478
+ if (!isRecord(value)) {
479
+ add(path, "invalid_code_inputs", "Typed code inputs must be an object of value refs.");
480
+ return;
481
+ }
482
+ const entries = Object.entries(value);
483
+ if (entries.length > WORKFLOW_CODE_V1_MAX_INPUTS) {
484
+ add(path, "too_many_code_inputs", `Typed code cannot declare more than ${WORKFLOW_CODE_V1_MAX_INPUTS} named inputs.`);
485
+ }
486
+ for (const [name, ref] of entries) {
487
+ if (!isNonEmptyString(name)) {
488
+ add(`${path}.${name}`, "invalid_code_input_name", "Typed code input names cannot be empty.");
489
+ }
490
+ else if (!isWorkflowCodeFieldName(name)) {
491
+ add(`${path}.${name}`, "invalid_code_input_name", `Typed code input '${name}' must be a JavaScript identifier with no dots or brackets and cannot be a reserved object key.`);
492
+ }
493
+ validateValueRef(ref, `${path}.${name}`, add, scope);
494
+ if (isRecord(ref) && ref.type === "literal" && !isJsonValue(ref.value)) {
495
+ add(`${path}.${name}.value`, "invalid_code_input_literal", "Typed code literal inputs must be strict JSON values (finite numbers; no undefined, functions, symbols, BigInt, cycles, or class instances).");
496
+ }
497
+ if (isRecord(ref) && (ref.type === "context_profile" || ref.type === "context_asset")) {
498
+ add(`${path}.${name}.type`, "unsupported_code_input_ref", "oxygen-js-v1 inputs cannot read context_profile or context_asset; bind explicit upstream data instead.");
499
+ }
500
+ }
501
+ }
502
+ function lintWorkflowCodeLimits(value, path, add) {
503
+ if (!isRecord(value)) {
504
+ add(path, "invalid_code_limits", "Typed code limits must declare timeout_ms, max_input_bytes, and max_output_bytes.");
505
+ return;
506
+ }
507
+ for (const key of Object.keys(value)) {
508
+ if (!WORKFLOW_CODE_LIMIT_FIELDS.has(key)) {
509
+ add(`${path}.${key}`, "unknown_code_limit", `Typed code limits have no field called '${key}'.`);
510
+ }
511
+ }
512
+ lintBoundedCodeInteger(value.timeout_ms, `${path}.timeout_ms`, WORKFLOW_CODE_V1_MIN_TIMEOUT_MS, WORKFLOW_CODE_V1_MAX_TIMEOUT_MS, add);
513
+ lintBoundedCodeInteger(value.max_input_bytes, `${path}.max_input_bytes`, WORKFLOW_CODE_V1_MIN_INPUT_BYTES, WORKFLOW_CODE_V1_MAX_INPUT_BYTES, add);
514
+ lintBoundedCodeInteger(value.max_output_bytes, `${path}.max_output_bytes`, WORKFLOW_CODE_V1_MIN_OUTPUT_BYTES, WORKFLOW_CODE_V1_MAX_OUTPUT_BYTES, add);
515
+ }
516
+ function lintBoundedCodeInteger(value, path, minimum, maximum, add) {
517
+ if (typeof value !== "number" || !Number.isInteger(value) || value < minimum || value > maximum) {
518
+ add(path, "invalid_code_limit", `Typed code limit must be an integer from ${minimum} through ${maximum}.`);
519
+ }
520
+ }
521
+ function lintWorkflowCodeOutputSchema(value, path, add) {
522
+ if (!isRecord(value)) {
523
+ add(path, "invalid_code_output_schema", "Typed code output_schema must be a closed object JSON Schema.");
524
+ return;
525
+ }
526
+ const serializedBytes = jsonByteLength(value);
527
+ if (serializedBytes === null) {
528
+ add(path, "invalid_code_output_schema", "Typed code output_schema must be JSON serializable.");
529
+ }
530
+ else if (serializedBytes > WORKFLOW_CODE_V1_MAX_SCHEMA_BYTES) {
531
+ add(path, "code_output_schema_too_large", `Typed code output_schema cannot exceed ${WORKFLOW_CODE_V1_MAX_SCHEMA_BYTES} UTF-8 bytes.`);
532
+ }
533
+ if (value.type !== "object") {
534
+ add(`${path}.type`, "invalid_code_output_schema", "Typed code output_schema must have top-level type 'object'.");
535
+ }
536
+ const state = {
537
+ propertyCount: 0,
538
+ propertyLimitReported: false,
539
+ nodeCount: 0,
540
+ nodeLimitReported: false,
541
+ depthLimitReported: false,
542
+ ancestors: new Set(),
543
+ };
544
+ lintWorkflowCodeSchemaNode(value, path, add, state, 1);
545
+ }
546
+ function lintWorkflowCodeSchemaNode(// skipcq: JS-R1005 -- one recursive pass keeps the supported schema subset coherent.
547
+ schema, path, add, state, depth) {
548
+ state.nodeCount += 1;
549
+ if (state.nodeCount > WORKFLOW_CODE_V1_MAX_SCHEMA_NODES) {
550
+ if (!state.nodeLimitReported) {
551
+ add(path, "code_output_schema_too_complex", `Typed code output_schema cannot contain more than ${WORKFLOW_CODE_V1_MAX_SCHEMA_NODES} schema nodes.`);
552
+ state.nodeLimitReported = true;
553
+ }
554
+ return;
555
+ }
556
+ if (depth > WORKFLOW_CODE_V1_MAX_SCHEMA_DEPTH) {
557
+ if (!state.depthLimitReported) {
558
+ add(path, "code_output_schema_too_deep", `Typed code output_schema cannot exceed ${WORKFLOW_CODE_V1_MAX_SCHEMA_DEPTH} schema levels.`);
559
+ state.depthLimitReported = true;
560
+ }
561
+ return;
562
+ }
563
+ if (state.ancestors.has(schema)) {
564
+ add(path, "invalid_code_output_schema", "Typed code output_schema cannot contain a recursive object reference.");
565
+ return;
566
+ }
567
+ state.ancestors.add(schema);
568
+ const keys = Object.keys(schema);
569
+ for (const key of keys) {
570
+ if (!WORKFLOW_CODE_SCHEMA_KEYWORDS.has(key)) {
571
+ add(`${path}.${key}`, "unsupported_code_output_schema_keyword", `JSON Schema keyword '${key}' is not enforced by oxygen-js-v1.`);
572
+ }
573
+ }
574
+ if (schema.title !== undefined && typeof schema.title !== "string") {
575
+ add(`${path}.title`, "invalid_code_output_schema", "JSON Schema title must be a string.");
576
+ }
577
+ if (schema.description !== undefined && typeof schema.description !== "string") {
578
+ add(`${path}.description`, "invalid_code_output_schema", "JSON Schema description must be a string.");
579
+ }
580
+ if (schema.examples !== undefined
581
+ && (!Array.isArray(schema.examples) || schema.examples.some((entry) => !isJsonValue(entry)))) {
582
+ add(`${path}.examples`, "invalid_code_output_schema", "JSON Schema examples must be an array of JSON values.");
583
+ }
584
+ if (Object.hasOwn(schema, "default") && !isJsonValue(schema.default)) {
585
+ add(`${path}.default`, "invalid_code_output_schema", "JSON Schema default must be a JSON value.");
586
+ }
587
+ if (Object.hasOwn(schema, "const") && !isJsonValue(schema.const)) {
588
+ add(`${path}.const`, "invalid_code_output_schema", "JSON Schema const must be a JSON value.");
589
+ }
590
+ if (schema.enum !== undefined
591
+ && (!Array.isArray(schema.enum) || schema.enum.length === 0 || schema.enum.some((entry) => !isJsonValue(entry)))) {
592
+ add(`${path}.enum`, "invalid_code_output_schema", "JSON Schema enum must be a non-empty array of JSON values.");
593
+ }
594
+ const schemaTypes = workflowCodeSchemaTypes(schema.type, `${path}.type`, add);
595
+ const objectish = schemaTypes?.includes("object") === true
596
+ || schema.properties !== undefined
597
+ || schema.required !== undefined
598
+ || schema.additionalProperties !== undefined;
599
+ if (objectish && schema.additionalProperties !== false) {
600
+ add(`${path}.additionalProperties`, "open_code_output_schema", "Every object-shaped oxygen-js-v1 schema must set additionalProperties to false.");
601
+ }
602
+ if (objectish && schemaTypes && schemaTypes.length > 0 && !schemaTypes.includes("object")) {
603
+ add(`${path}.type`, "invalid_code_output_schema", "Object keywords require type 'object'.");
604
+ }
605
+ if (schema.anyOf !== undefined) {
606
+ const ignoredSiblings = keys.filter((key) => key !== "anyOf"
607
+ && !WORKFLOW_CODE_SCHEMA_ANNOTATIONS.has(key)
608
+ && key !== "const"
609
+ && key !== "enum");
610
+ if (ignoredSiblings.length > 0) {
611
+ add(path, "invalid_code_output_schema", `anyOf cannot be combined with ${ignoredSiblings.join(", ")} because oxygen-js-v1 would ignore those sibling constraints.`);
612
+ }
613
+ if (!Array.isArray(schema.anyOf) || schema.anyOf.length === 0) {
614
+ add(`${path}.anyOf`, "invalid_code_output_schema", "JSON Schema anyOf must be a non-empty array of schemas.");
615
+ }
616
+ else {
617
+ for (const [index, candidate] of schema.anyOf.entries()) {
618
+ if (!isRecord(candidate)) {
619
+ add(`${path}.anyOf.${index}`, "invalid_code_output_schema", "Each anyOf entry must be a schema object.");
620
+ }
621
+ else {
622
+ lintWorkflowCodeSchemaNode(candidate, `${path}.anyOf.${index}`, add, state, depth + 1);
623
+ }
624
+ }
625
+ }
626
+ }
627
+ const properties = schema.properties;
628
+ if (properties !== undefined) {
629
+ if (!isRecord(properties)) {
630
+ add(`${path}.properties`, "invalid_code_output_schema", "JSON Schema properties must be an object.");
631
+ }
632
+ else {
633
+ const propertyEntries = Object.entries(properties);
634
+ state.propertyCount += propertyEntries.length;
635
+ if (state.propertyCount > WORKFLOW_CODE_V1_MAX_SCHEMA_PROPERTIES && !state.propertyLimitReported) {
636
+ add(`${path}.properties`, "code_output_schema_too_large", `Typed code output_schema cannot declare more than ${WORKFLOW_CODE_V1_MAX_SCHEMA_PROPERTIES} named properties.`);
637
+ state.propertyLimitReported = true;
638
+ }
639
+ for (const [name, propertySchema] of propertyEntries) {
640
+ if (!isWorkflowCodeFieldName(name)) {
641
+ add(`${path}.properties.${name}`, "invalid_code_output_field_name", `Typed code output field '${name}' must be a JavaScript identifier with no dots or brackets and cannot be a reserved object key.`);
642
+ }
643
+ if (!isRecord(propertySchema)) {
644
+ add(`${path}.properties.${name}`, "invalid_code_output_schema", "Each property must be a schema object.");
645
+ }
646
+ else {
647
+ lintWorkflowCodeSchemaNode(propertySchema, `${path}.properties.${name}`, add, state, depth + 1);
648
+ }
649
+ }
650
+ }
651
+ }
652
+ if (schema.required !== undefined) {
653
+ if (!Array.isArray(schema.required) || schema.required.some((name) => typeof name !== "string")) {
654
+ add(`${path}.required`, "invalid_code_output_schema", "JSON Schema required must be an array of property names.");
655
+ }
656
+ else {
657
+ const required = schema.required;
658
+ if (new Set(required).size !== required.length) {
659
+ add(`${path}.required`, "invalid_code_output_schema", "JSON Schema required property names must be unique.");
660
+ }
661
+ const declared = isRecord(properties) ? properties : {};
662
+ for (const name of required) {
663
+ if (!Object.hasOwn(declared, name)) {
664
+ add(`${path}.required`, "invalid_code_output_schema", `Required property '${name}' needs a matching properties entry.`);
665
+ }
666
+ }
667
+ }
668
+ }
669
+ if (schema.items !== undefined) {
670
+ if (schemaTypes && schemaTypes.length > 0 && !schemaTypes.includes("array")) {
671
+ add(`${path}.type`, "invalid_code_output_schema", "JSON Schema items requires type 'array'.");
672
+ }
673
+ if (!isRecord(schema.items)) {
674
+ add(`${path}.items`, "invalid_code_output_schema", "JSON Schema items must be a schema object.");
675
+ }
676
+ else {
677
+ lintWorkflowCodeSchemaNode(schema.items, `${path}.items`, add, state, depth + 1);
678
+ }
679
+ }
680
+ state.ancestors.delete(schema);
681
+ }
682
+ function workflowCodeSchemaTypes(value, path, add) {
683
+ if (value === undefined)
684
+ return [];
685
+ const types = typeof value === "string"
686
+ ? [value]
687
+ : Array.isArray(value) && value.length > 0 && value.every((entry) => typeof entry === "string")
688
+ ? value
689
+ : null;
690
+ if (!types || types.some((type) => !WORKFLOW_CODE_JSON_TYPES.has(type))) {
691
+ add(path, "invalid_code_output_schema", "JSON Schema type must name only supported JSON value types.");
692
+ return null;
693
+ }
694
+ if (new Set(types).size !== types.length) {
695
+ add(path, "invalid_code_output_schema", "JSON Schema type arrays cannot contain duplicates.");
696
+ }
697
+ return types;
698
+ }
699
+ function isJsonValue(value, ancestors = new Set()) {
700
+ if (value === null || typeof value === "string" || typeof value === "boolean")
701
+ return true;
702
+ if (typeof value === "number")
703
+ return Number.isFinite(value);
704
+ if (typeof value !== "object")
705
+ return false;
706
+ if (ancestors.has(value))
707
+ return false;
708
+ ancestors.add(value);
709
+ let valid;
710
+ if (Array.isArray(value)) {
711
+ valid = value.every((entry) => isJsonValue(entry, ancestors));
712
+ }
713
+ else {
714
+ const prototype = Object.getPrototypeOf(value);
715
+ valid = (prototype === Object.prototype || prototype === null)
716
+ && Object.values(value).every((entry) => isJsonValue(entry, ancestors));
717
+ }
718
+ ancestors.delete(value);
719
+ return valid;
720
+ }
721
+ function jsonByteLength(value) {
722
+ try {
723
+ const serialized = JSON.stringify(value);
724
+ return serialized === undefined ? null : Buffer.byteLength(serialized, "utf8");
725
+ }
726
+ catch {
727
+ return null;
728
+ }
729
+ }
335
730
  function lintToolNode(node, path, add, scope, options) {
336
731
  const hasPayloadSource = typeof node.payload_source === "string";
337
732
  if (!isNonEmptyString(node.tool)) {
@@ -670,6 +1065,11 @@ function lintGraphTopology(graph, add) {
670
1065
  + "Move the approval before the loop so one decision covers the whole batch.");
671
1066
  });
672
1067
  }
1068
+ const codeSandboxUpperBound = workflowCodeSandboxInvocationUpperBound(graph);
1069
+ if (codeSandboxUpperBound > WORKFLOW_CODE_V1_MAX_SANDBOX_INVOCATIONS_PER_RUN) {
1070
+ add("$.nodes", "workflow_code_invocation_limit", `This graph can create more than ${WORKFLOW_CODE_V1_MAX_SANDBOX_INVOCATIONS_PER_RUN} Code sandboxes in one run after loops and retries. `
1071
+ + "Reduce loop max_iterations, Code retries, or the number of Code nodes.");
1072
+ }
673
1073
  const reachable = reachableNodeIds(graph);
674
1074
  graph.nodes.forEach((node, index) => {
675
1075
  if (reachable.has(node.id))
@@ -800,6 +1200,8 @@ function validateScopePath(path, issuePath, add, scope) {
800
1200
  * graph that a reachability rule would refuse.
801
1201
  */
802
1202
  function validateScopeNodeId(path, segments, issuePath, add, scope) {
1203
+ if (scope.validateNodeIds === false)
1204
+ return;
803
1205
  const root = segments[0];
804
1206
  if (root !== "steps" && root !== "loop")
805
1207
  return; // `trigger` names no node.