@oxygen-agent/cli 1.50.37 → 1.98.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,10 @@ import * as vm from "node:vm"; // skipcq: JS-C1003
3
3
  export const WORKFLOW_MANIFEST_VERSION = 1;
4
4
  export const WORKFLOW_COMPILER_VERSION = "oxygen-workflows-v1";
5
5
  export const DURABLE_RECIPE_COMPILER_VERSION = "oxygen-recipes-v2";
6
+ export const BLUEPRINT_VERSION = 1;
7
+ export const BLUEPRINT_COMPILER_VERSION = "oxygen-blueprints-v1";
6
8
  export const MAX_RECIPE_BUNDLE_BYTES = 2_000_000;
9
+ export const MAX_BLUEPRINT_BYTES = 4_000_000;
7
10
  export const DEFAULT_WORKFLOW_CRON_TIMEZONE = "UTC";
8
11
  // Compatibility and determinism lint only. The Vercel sandbox process,
9
12
  // denied network policy, and runtime global guards are the security boundary.
@@ -380,6 +383,234 @@ export function assertWorkflowManifest(value, options = {}) {
380
383
  const first = result.issues[0];
381
384
  throw new Error(first ? `${first.code}: ${first.message}` : "Invalid workflow manifest.");
382
385
  }
386
+ export function isBlueprint(value) {
387
+ return isRecord(value)
388
+ && value.blueprint_version === BLUEPRINT_VERSION
389
+ && value.compiler_version === BLUEPRINT_COMPILER_VERSION;
390
+ }
391
+ const BLUEPRINT_REF_PATTERN = /^[a-z][a-z0-9_-]{0,79}$/;
392
+ const BLUEPRINT_PROMPT_SLUG_PATTERN = /^[a-z][a-z0-9_-]{0,119}$/;
393
+ export function lintBlueprint(// skipcq: JS-R1005
394
+ value, options = {}) {
395
+ const issues = [];
396
+ const add = (path, code, message) => issues.push({ path, code, message });
397
+ if (!isRecord(value)) {
398
+ add("$", "invalid_blueprint", "Blueprint must be an object.");
399
+ return { ok: false, issues };
400
+ }
401
+ if (value.blueprint_version !== BLUEPRINT_VERSION) {
402
+ add("$.blueprint_version", "invalid_blueprint_version", "Blueprint version must be 1.");
403
+ }
404
+ if (value.compiler_version !== BLUEPRINT_COMPILER_VERSION) {
405
+ add("$.compiler_version", "invalid_compiler_version", `Blueprint compiler_version must be '${BLUEPRINT_COMPILER_VERSION}'.`);
406
+ }
407
+ if (!isNonEmptyString(value.id))
408
+ add("$.id", "invalid_blueprint_id", "Blueprint id is required.");
409
+ if (!isNonEmptyString(value.name))
410
+ add("$.name", "invalid_blueprint_name", "Blueprint name is required.");
411
+ if (typeof value.summary !== "string")
412
+ add("$.summary", "invalid_summary", "Blueprint summary must be a string.");
413
+ if (!Array.isArray(value.tags)) {
414
+ add("$.tags", "invalid_tags", "Blueprint tags must be an array of strings.");
415
+ }
416
+ if (!isNonEmptyString(value.exported_at)) {
417
+ add("$.exported_at", "invalid_exported_at", "Blueprint exported_at must be an ISO timestamp.");
418
+ }
419
+ if (!isNonEmptyString(value.source_hash)) {
420
+ add("$.source_hash", "missing_source_hash", "Blueprint source_hash is required.");
421
+ }
422
+ const tableRefs = new Set();
423
+ if (!Array.isArray(value.tables)) {
424
+ add("$.tables", "invalid_tables", "Blueprint tables must be an array.");
425
+ }
426
+ else {
427
+ value.tables.forEach((table, index) => {
428
+ const path = `$.tables.${index}`;
429
+ if (!isRecord(table)) {
430
+ add(path, "invalid_table", "Blueprint table must be an object.");
431
+ return;
432
+ }
433
+ if (!isNonEmptyString(table.ref) || !BLUEPRINT_REF_PATTERN.test(table.ref)) {
434
+ add(`${path}.ref`, "invalid_table_ref", "Blueprint table ref must be lowercase kebab/snake_case (a-z0-9_-).");
435
+ }
436
+ else {
437
+ if (tableRefs.has(table.ref)) {
438
+ add(`${path}.ref`, "duplicate_table_ref", `Blueprint table ref '${table.ref}' is duplicated.`);
439
+ }
440
+ tableRefs.add(table.ref);
441
+ }
442
+ if (!isNonEmptyString(table.name))
443
+ add(`${path}.name`, "invalid_table_name", "Blueprint table name is required.");
444
+ if (!Array.isArray(table.columns) || table.columns.length === 0) {
445
+ add(`${path}.columns`, "invalid_table_columns", "Blueprint table must declare at least one column.");
446
+ }
447
+ else {
448
+ table.columns.forEach((column, columnIndex) => {
449
+ const columnPath = `${path}.columns.${columnIndex}`;
450
+ if (!isRecord(column)) {
451
+ add(columnPath, "invalid_column", "Blueprint column must be an object.");
452
+ return;
453
+ }
454
+ if (!isNonEmptyString(column.label)) {
455
+ add(`${columnPath}.label`, "invalid_column_label", "Blueprint column label is required.");
456
+ }
457
+ });
458
+ }
459
+ });
460
+ }
461
+ if (value.column_grafts !== undefined) {
462
+ if (!Array.isArray(value.column_grafts)) {
463
+ add("$.column_grafts", "invalid_column_grafts", "Blueprint column_grafts must be an array.");
464
+ }
465
+ else {
466
+ value.column_grafts.forEach((graft, index) => {
467
+ const path = `$.column_grafts.${index}`;
468
+ if (!isRecord(graft)) {
469
+ add(path, "invalid_column_graft", "Column graft must be an object.");
470
+ return;
471
+ }
472
+ if (!isNonEmptyString(graft.table_ref) || !tableRefs.has(graft.table_ref)) {
473
+ add(`${path}.table_ref`, "invalid_graft_ref", "Column graft table_ref must match a declared blueprint table ref.");
474
+ }
475
+ if (!isRecord(graft.column) || !isNonEmptyString(graft.column.label)) {
476
+ add(`${path}.column`, "invalid_graft_column", "Column graft column must be an object with a label.");
477
+ }
478
+ });
479
+ }
480
+ }
481
+ const promptSlugs = new Set();
482
+ if (!Array.isArray(value.prompt_templates)) {
483
+ add("$.prompt_templates", "invalid_prompt_templates", "Blueprint prompt_templates must be an array.");
484
+ }
485
+ else {
486
+ value.prompt_templates.forEach((prompt, index) => {
487
+ const path = `$.prompt_templates.${index}`;
488
+ if (!isRecord(prompt)) {
489
+ add(path, "invalid_prompt", "Blueprint prompt must be an object.");
490
+ return;
491
+ }
492
+ if (!isNonEmptyString(prompt.slug) || !BLUEPRINT_PROMPT_SLUG_PATTERN.test(prompt.slug)) {
493
+ add(`${path}.slug`, "invalid_prompt_slug", "Prompt slug must be lowercase kebab/snake_case (a-z0-9_-).");
494
+ }
495
+ else {
496
+ if (promptSlugs.has(prompt.slug)) {
497
+ add(`${path}.slug`, "duplicate_prompt_slug", `Prompt slug '${prompt.slug}' is duplicated.`);
498
+ }
499
+ promptSlugs.add(prompt.slug);
500
+ }
501
+ if (!isNonEmptyString(prompt.name))
502
+ add(`${path}.name`, "invalid_prompt_name", "Prompt name is required.");
503
+ if (!isNonEmptyString(prompt.body))
504
+ add(`${path}.body`, "invalid_prompt_body", "Prompt body is required.");
505
+ if (prompt.kind !== "ai_column_system" && prompt.kind !== "scoring_rubric" && prompt.kind !== "other") {
506
+ add(`${path}.kind`, "invalid_prompt_kind", "Prompt kind must be ai_column_system, scoring_rubric, or other.");
507
+ }
508
+ });
509
+ }
510
+ if (!Array.isArray(value.workflows)) {
511
+ add("$.workflows", "invalid_workflows", "Blueprint workflows must be an array.");
512
+ }
513
+ else if (value.workflows.length === 0) {
514
+ add("$.workflows", "missing_workflows", "Blueprint must include at least one workflow.");
515
+ }
516
+ else {
517
+ value.workflows.forEach((entry, index) => {
518
+ const path = `$.workflows.${index}`;
519
+ if (!isRecord(entry)) {
520
+ add(path, "invalid_workflow_entry", "Workflow entry must be an object.");
521
+ return;
522
+ }
523
+ const manifest = entry.manifest;
524
+ const manifestResult = lintWorkflowManifest(manifest, options);
525
+ if (!manifestResult.ok) {
526
+ manifestResult.issues.forEach((issue) => {
527
+ add(`${path}.manifest${issue.path.replace(/^\$/, "")}`, issue.code, issue.message);
528
+ });
529
+ }
530
+ if (entry.table_refs !== undefined && !isRecord(entry.table_refs)) {
531
+ add(`${path}.table_refs`, "invalid_workflow_table_refs", "Workflow table_refs must be an object map.");
532
+ }
533
+ else if (isRecord(entry.table_refs)) {
534
+ for (const refName of Object.keys(entry.table_refs)) {
535
+ if (!tableRefs.has(refName)) {
536
+ add(`${path}.table_refs.${refName}`, "unknown_workflow_table_ref", `Workflow references table ref '${refName}' which is not declared on this blueprint.`);
537
+ }
538
+ }
539
+ }
540
+ if (entry.prompt_template_slugs !== undefined) {
541
+ if (!Array.isArray(entry.prompt_template_slugs)) {
542
+ add(`${path}.prompt_template_slugs`, "invalid_prompt_slug_list", "Workflow prompt_template_slugs must be an array of strings.");
543
+ }
544
+ else {
545
+ entry.prompt_template_slugs.forEach((slug, slugIndex) => {
546
+ if (!isNonEmptyString(slug)) {
547
+ add(`${path}.prompt_template_slugs.${slugIndex}`, "invalid_prompt_slug", "Workflow prompt slug references must be non-empty strings.");
548
+ }
549
+ else if (!promptSlugs.has(slug)) {
550
+ add(`${path}.prompt_template_slugs.${slugIndex}`, "unknown_prompt_slug", `Workflow references prompt slug '${slug}' which is not declared on this blueprint.`);
551
+ }
552
+ });
553
+ }
554
+ }
555
+ });
556
+ }
557
+ const requires = value.requires;
558
+ if (!isRecord(requires)) {
559
+ add("$.requires", "invalid_requires", "Blueprint requires must be an object.");
560
+ }
561
+ else {
562
+ if (requires.integrations !== undefined) {
563
+ if (!Array.isArray(requires.integrations)) {
564
+ add("$.requires.integrations", "invalid_requires_integrations", "Blueprint requires.integrations must be an array.");
565
+ }
566
+ else {
567
+ requires.integrations.forEach((entry, index) => {
568
+ const path = `$.requires.integrations.${index}`;
569
+ if (!isRecord(entry)) {
570
+ add(path, "invalid_integration", "Integration requirement must be an object.");
571
+ return;
572
+ }
573
+ if (!isNonEmptyString(entry.kind)) {
574
+ add(`${path}.kind`, "invalid_integration_kind", "Integration requirement kind is required (e.g. 'instantly', 'apollo').");
575
+ }
576
+ });
577
+ }
578
+ }
579
+ if (requires.context_keys !== undefined && !Array.isArray(requires.context_keys)) {
580
+ add("$.requires.context_keys", "invalid_context_keys", "Blueprint requires.context_keys must be an array of strings.");
581
+ }
582
+ }
583
+ return { ok: issues.length === 0, issues };
584
+ }
585
+ export function assertBlueprint(value, options = {}) {
586
+ const result = lintBlueprint(value, options);
587
+ if (result.ok)
588
+ return;
589
+ const first = result.issues[0];
590
+ throw new Error(first ? `${first.code}: ${first.message}` : "Invalid blueprint.");
591
+ }
592
+ export function buildBlueprint(input) {
593
+ const draft = {
594
+ blueprint_version: BLUEPRINT_VERSION,
595
+ id: input.id,
596
+ name: input.name,
597
+ summary: input.summary ?? "",
598
+ tags: Array.isArray(input.tags) ? Array.from(new Set(input.tags.filter(Boolean))) : [],
599
+ ...(input.audience ? { audience: input.audience } : {}),
600
+ requires: input.requires ?? {},
601
+ ...(input.inputSchema ? { input_schema: input.inputSchema } : {}),
602
+ tables: input.tables,
603
+ ...(input.columnGrafts && input.columnGrafts.length > 0 ? { column_grafts: input.columnGrafts } : {}),
604
+ prompt_templates: input.promptTemplates ?? [],
605
+ workflows: input.workflows,
606
+ exported_at: (input.exportedAt ?? new Date()).toISOString(),
607
+ ...(input.exportedFrom ? { exported_from: input.exportedFrom } : {}),
608
+ compiler_version: BLUEPRINT_COMPILER_VERSION,
609
+ };
610
+ const sourceHash = input.sourceHash
611
+ ?? hashWorkflowSource(JSON.stringify(draft, workflowJsonReplacer));
612
+ return { ...draft, source_hash: sourceHash };
613
+ }
383
614
  export function validateJsonSchemaValue(value, schema, path = "$") {
384
615
  if (!schema || typeof schema !== "object" || Array.isArray(schema))
385
616
  return [];
@@ -411,11 +642,44 @@ export async function runPureWorkflowFunction(input) {
411
642
  const first = issues[0];
412
643
  throw new Error(first ? `${first.code}: ${first.message}` : "Invalid workflow function source.");
413
644
  }
645
+ // Cross the host/sandbox boundary as a JSON string, then parse INSIDE the
646
+ // vm context. If we passed the host object directly its prototype chain
647
+ // would point at the host's Object/Function, and the regex token blacklist
648
+ // is trivially bypassed (e.g. "con"+"structor") to reach
649
+ // __oxygen_context.constructor.constructor === host Function — which is
650
+ // not affected by the new context's codeGeneration setting and yields
651
+ // worker-process RCE. Parsing inside the context rebinds the prototype to
652
+ // the sandboxed Object, so the same walk reaches the sandboxed Function,
653
+ // which then trips contextCodeGeneration.strings=false below.
414
654
  const context = toJsonValue(input.context, "context");
655
+ const contextJson = JSON.stringify(context);
415
656
  const sandbox = Object.create(null);
416
- sandbox.__oxygen_context = context;
417
- const script = new vm.Script(`"use strict";\nconst __oxygen_fn = (${input.source});\n__oxygen_fn(__oxygen_context);`);
418
- const result = script.runInNewContext(sandbox, { timeout: timeoutMs });
657
+ sandbox.__oxygen_context_json = contextJson;
658
+ // Shadow dangerous globals on the sandbox surface. Defense-in-depth: the
659
+ // load-bearing block is contextCodeGeneration; this just removes the
660
+ // obvious top-level handles.
661
+ sandbox.Function = undefined;
662
+ sandbox.eval = undefined;
663
+ sandbox.setTimeout = undefined;
664
+ sandbox.setInterval = undefined;
665
+ sandbox.setImmediate = undefined;
666
+ sandbox.queueMicrotask = undefined;
667
+ sandbox.WebAssembly = undefined;
668
+ const script = new vm.Script(`"use strict";\n`
669
+ + `const __oxygen_context = JSON.parse(__oxygen_context_json);\n`
670
+ + `const __oxygen_fn = (${input.source});\n`
671
+ + `__oxygen_fn(__oxygen_context);`);
672
+ // Disable dynamic code generation in the sandboxed context. Combined with
673
+ // the JSON re-parse above, this means every prototype-walk path to
674
+ // Function — direct, via context.constructor.constructor, or any other
675
+ // reachable Function instance — throws EvalError when called with source.
676
+ const result = script.runInNewContext(sandbox, {
677
+ timeout: timeoutMs,
678
+ contextCodeGeneration: {
679
+ strings: false,
680
+ wasm: false,
681
+ },
682
+ });
419
683
  const resolved = isPromiseLike(result)
420
684
  ? await withTimeout(result, timeoutMs)
421
685
  : result;
@@ -775,7 +1039,7 @@ function validateRecipeBundleSafety(source, path, add) {
775
1039
  }
776
1040
  }
777
1041
  function isWorkflowToolRejected(toolId) {
778
- return toolId === "run_javascript" || toolId === "custom_http" || toolId.includes("custom_http");
1042
+ return toolId === "run_javascript" || toolId === "custom_http";
779
1043
  }
780
1044
  const CRON_FIELD_LIMITS = [
781
1045
  { name: "minute", min: 0, max: 59 },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.50.37",
3
+ "version": "1.98.7",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",