@oxygen-agent/cli 1.377.3 → 1.575.19

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 (95) hide show
  1. package/README.md +1 -1
  2. package/dist/column-run-notices.d.ts +11 -0
  3. package/dist/column-run-notices.js +37 -0
  4. package/dist/command-manifest.js +13 -8
  5. package/dist/help.js +78 -16
  6. package/dist/index.js +3450 -447
  7. package/dist/skills.js +106 -1
  8. package/node_modules/@oxygen/formula/dist/coerce.d.ts +8 -0
  9. package/node_modules/@oxygen/formula/dist/coerce.js +10 -0
  10. package/node_modules/@oxygen/formula/dist/evaluate.d.ts +31 -0
  11. package/node_modules/@oxygen/formula/dist/evaluate.js +248 -0
  12. package/node_modules/@oxygen/formula/dist/expression.d.ts +64 -0
  13. package/node_modules/@oxygen/formula/dist/expression.js +428 -0
  14. package/node_modules/@oxygen/formula/dist/formula-functions.d.ts +71 -0
  15. package/node_modules/@oxygen/formula/dist/formula-functions.js +1100 -0
  16. package/node_modules/@oxygen/formula/dist/index.d.ts +17 -0
  17. package/node_modules/@oxygen/formula/dist/index.js +17 -0
  18. package/node_modules/@oxygen/formula/dist/value-normalizers.d.ts +30 -0
  19. package/node_modules/@oxygen/formula/dist/value-normalizers.js +80 -0
  20. package/node_modules/@oxygen/formula/package.json +26 -0
  21. package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +30 -0
  22. package/node_modules/@oxygen/recipe-sdk/dist/index.js +2 -2
  23. package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +60 -0
  24. package/node_modules/@oxygen/shared/dist/billing-anchors.js +135 -0
  25. package/node_modules/@oxygen/shared/dist/billing.d.ts +99 -5
  26. package/node_modules/@oxygen/shared/dist/billing.js +185 -8
  27. package/node_modules/@oxygen/shared/dist/call-outcomes.d.ts +59 -0
  28. package/node_modules/@oxygen/shared/dist/call-outcomes.js +73 -0
  29. package/node_modules/@oxygen/shared/dist/cli-result.js +1 -0
  30. package/node_modules/@oxygen/shared/dist/credit-guidance.js +3 -1
  31. package/node_modules/@oxygen/shared/dist/crm-reply-events.d.ts +35 -0
  32. package/node_modules/@oxygen/shared/dist/crm-reply-events.js +31 -0
  33. package/node_modules/@oxygen/shared/dist/dial-guardrail-overrides.d.ts +50 -0
  34. package/node_modules/@oxygen/shared/dist/dial-guardrail-overrides.js +65 -0
  35. package/node_modules/@oxygen/shared/dist/directory.d.ts +1 -1
  36. package/node_modules/@oxygen/shared/dist/directory.js +1 -0
  37. package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +15 -0
  38. package/node_modules/@oxygen/shared/dist/hosted-ai.js +19 -0
  39. package/node_modules/@oxygen/shared/dist/index.d.ts +9 -0
  40. package/node_modules/@oxygen/shared/dist/index.js +9 -0
  41. package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.d.ts +31 -0
  42. package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.js +56 -0
  43. package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +5 -4
  44. package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +5 -4
  45. package/node_modules/@oxygen/shared/dist/linkedin-url.d.ts +22 -0
  46. package/node_modules/@oxygen/shared/dist/linkedin-url.js +7 -4
  47. package/node_modules/@oxygen/shared/dist/log.js +41 -2
  48. package/node_modules/@oxygen/shared/dist/microsoft-consent-url.d.ts +7 -0
  49. package/node_modules/@oxygen/shared/dist/microsoft-consent-url.js +29 -0
  50. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +31 -0
  51. package/node_modules/@oxygen/shared/dist/object-storage.js +61 -0
  52. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +636 -0
  53. package/node_modules/@oxygen/shared/dist/plan-limits.js +199 -0
  54. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +89 -23
  55. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +88 -24
  56. package/node_modules/@oxygen/shared/dist/sequence-crm-events.d.ts +291 -0
  57. package/node_modules/@oxygen/shared/dist/sequence-crm-events.js +224 -0
  58. package/node_modules/@oxygen/shared/dist/sequence-template.d.ts +42 -1
  59. package/node_modules/@oxygen/shared/dist/sequence-template.js +0 -0
  60. package/node_modules/@oxygen/shared/dist/sequences.d.ts +287 -24
  61. package/node_modules/@oxygen/shared/dist/sequences.js +940 -60
  62. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +70 -0
  63. package/node_modules/@oxygen/shared/dist/spend-safety.js +106 -0
  64. package/node_modules/@oxygen/shared/dist/tags.d.ts +90 -1
  65. package/node_modules/@oxygen/shared/dist/tags.js +122 -6
  66. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  67. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  68. package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.d.ts +1 -1
  69. package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.js +4 -0
  70. package/node_modules/@oxygen/shared/package.json +95 -0
  71. package/node_modules/@oxygen/workflows/dist/event-dispatch.d.ts +126 -0
  72. package/node_modules/@oxygen/workflows/dist/event-dispatch.js +173 -0
  73. package/node_modules/@oxygen/workflows/dist/graph/expression.d.ts +78 -0
  74. package/node_modules/@oxygen/workflows/dist/graph/expression.js +700 -0
  75. package/node_modules/@oxygen/workflows/dist/graph/index.d.ts +20 -0
  76. package/node_modules/@oxygen/workflows/dist/graph/index.js +20 -0
  77. package/node_modules/@oxygen/workflows/dist/graph/lint.d.ts +4 -0
  78. package/node_modules/@oxygen/workflows/dist/graph/lint.js +812 -0
  79. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +501 -0
  80. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.js +200 -0
  81. package/node_modules/@oxygen/workflows/dist/graph/params.d.ts +86 -0
  82. package/node_modules/@oxygen/workflows/dist/graph/params.js +173 -0
  83. package/node_modules/@oxygen/workflows/dist/graph/remap.d.ts +48 -0
  84. package/node_modules/@oxygen/workflows/dist/graph/remap.js +213 -0
  85. package/node_modules/@oxygen/workflows/dist/graph/topology.d.ts +46 -0
  86. package/node_modules/@oxygen/workflows/dist/graph/topology.js +280 -0
  87. package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +270 -0
  88. package/node_modules/@oxygen/workflows/dist/graph/types.js +93 -0
  89. package/node_modules/@oxygen/workflows/dist/index.d.ts +113 -1
  90. package/node_modules/@oxygen/workflows/dist/index.js +179 -13
  91. package/node_modules/@oxygen/workflows/dist/tool-effects.d.ts +1 -0
  92. package/node_modules/@oxygen/workflows/dist/tool-effects.js +19 -0
  93. package/node_modules/@oxygen/workflows/dist/usage-estimate.js +135 -4
  94. package/node_modules/@oxygen/workflows/package.json +4 -0
  95. package/package.json +7 -5
@@ -1,6 +1,16 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import * as vm from "node:vm"; // skipcq: JS-C1003
3
+ import { isWorkflowGraphManifest } from "./graph/types.js";
3
4
  export * from "./usage-estimate.js";
5
+ // Pure event-dispatch semantics, shared by the apps/web and apps/worker event
6
+ // dispatchers so the filter gate, idempotency rule, and delivery verdict cannot
7
+ // drift between the two processes that fan the same event out.
8
+ export * from "./event-dispatch.js";
9
+ // oxygen-workflows-v2. The graph barrel is browser-safe on its own (importable as
10
+ // "@oxygen/workflows/graph"); the linter is not — it reuses the v1 pure-function
11
+ // scanner below — so it is re-exported here, from the node entry point only.
12
+ export * from "./graph/index.js";
13
+ export * from "./graph/lint.js";
4
14
  export const WORKFLOW_MANIFEST_VERSION = 1;
5
15
  export const WORKFLOW_COMPILER_VERSION = "oxygen-workflows-v1";
6
16
  export const DURABLE_RECIPE_COMPILER_VERSION = "oxygen-recipes-v2";
@@ -198,8 +208,15 @@ export function isRecipeManifest(value) {
198
208
  && value.manifest_version === WORKFLOW_MANIFEST_VERSION
199
209
  && value.compiler_version === DURABLE_RECIPE_COMPILER_VERSION;
200
210
  }
211
+ /**
212
+ * Every manifest shape the platform can store and run: v1 hosted steps, durable
213
+ * recipes, and oxygen-workflows-v2 graphs. This is the gate `workflows apply`
214
+ * and the CLI loader route through, so a shape listed here must already have a
215
+ * linter and a worker execution path — accepting a manifest we cannot run would
216
+ * cut a revision that fails at run time instead of at authoring time.
217
+ */
201
218
  export function isAnyWorkflowManifest(value) {
202
- return isWorkflowManifest(value) || isRecipeManifest(value);
219
+ return isWorkflowManifest(value) || isRecipeManifest(value) || isWorkflowGraphManifest(value);
203
220
  }
204
221
  export function compileWorkflowDefinition(// skipcq: JS-R1005 -- compiler validates workflow metadata, trigger, steps, branch targets, and defaults together.
205
222
  definition, options = {}) {
@@ -284,7 +301,9 @@ export function buildRecipeManifest(input) {
284
301
  // Shared positive-finite validator for the optional per-run / per-step
285
302
  // max_credits caps (Gap 2). `undefined` is valid (no cap); anything else must be
286
303
  // a positive finite number, or lint rejects the manifest.
287
- function validateOptionalMaxCredits(value, path, add) {
304
+ // Exported for ./graph/lint.ts so the v2 graph linter enforces the same rule as
305
+ // v1 rather than a second copy of it. Not part of the product surface.
306
+ export function validateOptionalMaxCredits(value, path, add) {
288
307
  if (value === undefined)
289
308
  return;
290
309
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
@@ -391,6 +410,30 @@ value, options = {}) {
391
410
  && step.effect !== "external_write") {
392
411
  add(`${path}.effect`, "invalid_step_effect", "Step effect is invalid.");
393
412
  }
413
+ else if (isNonEmptyString(step.tool)) {
414
+ // No resolver: skip the canonical cross-check rather than refuse.
415
+ //
416
+ // This function is reached by compileWorkflowDefinition, which the CLI
417
+ // calls locally — and the CLI deliberately cannot classify effects,
418
+ // because that is exactly the dependency removed from this package.
419
+ // Refusing here would make `oxygen workflows` unable to compile any
420
+ // manifest containing a tool step.
421
+ //
422
+ // Skipping is safe because this check is a CONVENIENCE, not the gate:
423
+ // apply and the worker runtime both resolve the effect canonically and
424
+ // both REFUSE what they cannot classify. The divergence v1.513.2 fixed
425
+ // was lint disagreeing with apply, and that is addressed by the server
426
+ // lint path always injecting resolveWorkflowToolEffect (see
427
+ // apps/web/src/lib/workflows.ts lintHostedWorkflowManifest) — not by a
428
+ // default inside this package.
429
+ const resolveEffect = options.resolveToolEffect;
430
+ if (resolveEffect) {
431
+ const canonicalEffect = resolveEffect(step.tool);
432
+ if (canonicalEffect !== null && step.effect !== canonicalEffect) {
433
+ add(`${path}.effect`, "tool_effect_mismatch", `Tool '${step.tool}' has canonical effect '${canonicalEffect}', not '${String(step.effect)}'.`);
434
+ }
435
+ }
436
+ }
394
437
  if (step.mode !== undefined
395
438
  && step.mode !== "dry_run"
396
439
  && step.mode !== "live"
@@ -473,9 +516,6 @@ value, options = {}) {
473
516
  add("$.runtime", "invalid_recipe_runtime", "Durable recipe manifests must set runtime to durable.");
474
517
  }
475
518
  if (Array.isArray(value.tools_used)) {
476
- if (value.tools_used.length === 0) {
477
- add("$.tools_used", "missing_recipe_tools", "Durable recipes must declare at least one allowed tool.");
478
- }
479
519
  value.tools_used.forEach((toolId, index) => {
480
520
  if (typeof toolId !== "string" || !toolId.trim()) {
481
521
  add(`$.tools_used.${index}`, "invalid_tool_id", "Recipe tools_used entries must be non-empty strings.");
@@ -492,7 +532,7 @@ value, options = {}) {
492
532
  add("$.tools_used", "invalid_tools_used", "Recipe tools_used must be an array of tool ids.");
493
533
  }
494
534
  else {
495
- add("$.tools_used", "missing_recipe_tools", "Durable recipes must declare at least one allowed tool.");
535
+ add("$.tools_used", "missing_recipe_tools", "Durable recipe manifests must declare tools_used (use [] for pure recipes).");
496
536
  }
497
537
  const declaredTools = Array.isArray(value.tools_used)
498
538
  ? new Set(value.tools_used.filter((toolId) => typeof toolId === "string" && toolId.trim().length > 0))
@@ -556,7 +596,7 @@ function validateRecipeVisualPlan(value, path, declaredTools, add) {
556
596
  if (!isNonEmptyString(step.tool)) {
557
597
  add(`${stepPath}.tool`, "missing_visual_plan_tool", "Recipe visual_plan tool step requires a tool id.");
558
598
  }
559
- else if (declaredTools.size > 0 && !declaredTools.has(step.tool)) {
599
+ else if (!declaredTools.has(step.tool)) {
560
600
  add(`${stepPath}.tool`, "visual_plan_tool_not_declared", `Recipe visual_plan tool '${step.tool}' must be present in tools_used.`);
561
601
  }
562
602
  if (step.effect !== undefined
@@ -935,6 +975,15 @@ export const workflowApplySchema = {
935
975
  additionalProperties: false,
936
976
  properties: {
937
977
  manifest: { $ref: "#/$defs/manifest" },
978
+ approved: {
979
+ type: "boolean",
980
+ description: "Standing tool authorization for this exact autonomous revision.",
981
+ },
982
+ max_credits: {
983
+ type: "number",
984
+ exclusiveMinimum: 0,
985
+ description: "Hard credit ceiling for each autonomous delivery.",
986
+ },
938
987
  },
939
988
  required: ["manifest"],
940
989
  $defs: {
@@ -1153,7 +1202,8 @@ function validateOptionalIdempotencyKeyPath(value, path, label, add) {
1153
1202
  add(`${path}.idempotency_key_path`, "invalid_idempotency_key_path", `${label} idempotency path is invalid.`);
1154
1203
  }
1155
1204
  }
1156
- function validateTrigger(// skipcq: JS-R1005
1205
+ // Exported for ./graph/lint.ts — see validateOptionalMaxCredits above.
1206
+ export function validateTrigger(// skipcq: JS-R1005
1157
1207
  value, path, add) {
1158
1208
  if (!isRecord(value)) {
1159
1209
  add(path, "invalid_trigger", "Workflow trigger must be an object.");
@@ -1266,7 +1316,11 @@ function validateBranchTarget(target, path, fieldLabel, branchIndex, idToIndex,
1266
1316
  add(path, "branch_back_jump", `Branch ${fieldLabel} must reference a step that comes after the branch in declared order.`);
1267
1317
  }
1268
1318
  }
1269
- function validatePureFunctionSource(source, path, add) {
1319
+ // Exported for ./graph/lint.ts — see validateOptionalMaxCredits above. v2 `code`
1320
+ // nodes are the v2 spelling of a v1 transform step and must clear the identical
1321
+ // purity scan; the graph linter re-labels the issues it raises as
1322
+ // `unsafe_code_source`.
1323
+ export function validatePureFunctionSource(source, path, add) {
1270
1324
  if (!source.trim()) {
1271
1325
  add(path, "empty_function_source", "Workflow function source cannot be empty.");
1272
1326
  return;
@@ -1386,7 +1440,9 @@ function validateRecipeBundleSafety(source, path, add) {
1386
1440
  }
1387
1441
  }
1388
1442
  }
1389
- function isWorkflowToolRejected(toolId) {
1443
+ // Exported for ./graph/lint.ts — see validateOptionalMaxCredits above. One deny
1444
+ // list, enforced identically for v1 tool steps and v2 tool nodes.
1445
+ export function isWorkflowToolRejected(toolId) {
1390
1446
  return toolId === "run_javascript" || toolId === "custom_http";
1391
1447
  }
1392
1448
  const CRON_FIELD_LIMITS = [
@@ -1654,13 +1710,123 @@ export function readWorkflowRunMaxCredits(metadata) {
1654
1710
  return null;
1655
1711
  return parsed;
1656
1712
  }
1713
+ export const WORKFLOW_STANDING_AUTHORIZATION_METADATA_KEY = "standing_authorization";
1714
+ export const WORKFLOW_STANDING_AUTHORIZATION_POLICY_METADATA_KEY = "standing_authorization_policy";
1715
+ export const WORKFLOW_STANDING_AUTHORIZATION_POLICY_REVISION_BOUND_V1 = "revision_bound_v1";
1716
+ export const WORKFLOW_STANDING_AUTHORIZATION_REQUIRED_ERROR_CODE = "workflow_standing_authorization_required";
1717
+ export const WORKFLOW_STANDING_AUTHORIZATION_REQUIRED_MESSAGE = "This autonomous workflow can call tools and needs standing authorization for its current revision. Re-enable it with approved=true; pass a positive max_credits per delivery or omit it to use the plan default.";
1718
+ export function isAutonomousWorkflowTriggerType(value) {
1719
+ return value === "cron" || value === "webhook" || value === "event";
1720
+ }
1721
+ /**
1722
+ * Whether this manifest can cross into a tool at runtime. Hosted workflows are
1723
+ * explicit in `steps`; oxygen-workflows-v2 graphs are explicit in `nodes`;
1724
+ * durable recipes declare the complete allowlist in `tools_used`. Keeping this
1725
+ * predicate structural means legacy revisions can be checked before execution
1726
+ * without trusting trigger metadata.
1727
+ *
1728
+ * The v2 branch is load-bearing, not cosmetic: this predicate is the ONLY input
1729
+ * to workflowRequiresStandingAuthorization, so a graph it does not recognise arms
1730
+ * a cron / webhook / event trigger with no standing authorization and therefore
1731
+ * no per-delivery credit ceiling. A `workflow` (sub-graph) node counts as well —
1732
+ * a child run can invoke tools of its own, and the gate must fail closed.
1733
+ */
1734
+ export function workflowManifestCanInvokeTool(manifest) {
1735
+ if (!isRecord(manifest))
1736
+ return false;
1737
+ // EVERY shape is checked and the results OR'd — deliberately not a chain of
1738
+ // early returns dispatching on which field is present.
1739
+ //
1740
+ // The chain was a bypass: `tools_used` was tested first and returned from that
1741
+ // branch, so a v2 graph manifest carrying an extra `tools_used: []` never
1742
+ // reached the `nodes` check and reported "cannot invoke a tool". That is this
1743
+ // predicate's ONLY input, so apply then skipped the standing-authorization gate
1744
+ // for a cron-triggered graph full of tool nodes, left the trigger unmarked so
1745
+ // the fire-time check was fooled too, and resolved a delivery cap of null — no
1746
+ // ceiling at all. The graph linter accepts the extra key, so nothing else
1747
+ // objected either.
1748
+ //
1749
+ // A manifest that carries a tool node CAN invoke a tool no matter what other
1750
+ // fields it also carries, so the honest form is a union that fails closed.
1751
+ const viaRecipe = Array.isArray(manifest.tools_used)
1752
+ && manifest.tools_used.some((toolId) => isNonEmptyString(toolId));
1753
+ const viaGraph = Array.isArray(manifest.nodes)
1754
+ && manifest.nodes.some((node) => isRecord(node) && (node.kind === "tool" || node.kind === "workflow"));
1755
+ const viaSteps = Array.isArray(manifest.steps)
1756
+ && manifest.steps.some((step) => isRecord(step) && step.kind === "tool");
1757
+ return viaRecipe || viaGraph || viaSteps;
1758
+ }
1759
+ /** The single apply/enable/runtime gate for standing autonomous authority. */
1760
+ export function workflowRequiresStandingAuthorization(manifest, triggerType) {
1761
+ const manifestTriggerType = isRecord(manifest)
1762
+ && isRecord(manifest.trigger)
1763
+ ? manifest.trigger.type
1764
+ : "api";
1765
+ return isAutonomousWorkflowTriggerType(triggerType ?? manifestTriggerType)
1766
+ && workflowManifestCanInvokeTool(manifest);
1767
+ }
1768
+ /**
1769
+ * Runtime enforcement marker for triggers created or explicitly re-armed after
1770
+ * revision-bound standing authority shipped. Legacy active triggers have no
1771
+ * marker and stay runnable during the default-ceiling compatibility rollout;
1772
+ * every new/re-armed tool trigger carries this marker and fails closed if its
1773
+ * matching authorization is missing or stale.
1774
+ */
1775
+ export function workflowStandingAuthorizationPolicyIsEnforced(metadata, triggerDefaultCapsMode) {
1776
+ return triggerDefaultCapsMode === "enforce"
1777
+ || (isRecord(metadata)
1778
+ && metadata[WORKFLOW_STANDING_AUTHORIZATION_POLICY_METADATA_KEY]
1779
+ === WORKFLOW_STANDING_AUTHORIZATION_POLICY_REVISION_BOUND_V1);
1780
+ }
1781
+ /**
1782
+ * Read the standing authority for one autonomous trigger revision. A null
1783
+ * result is fail-closed for policy-marked triggers. Legacy active rows
1784
+ * are distinguished by the separate policy marker so they can continue under
1785
+ * the fire-time default-ceiling compatibility rollout.
1786
+ */
1787
+ export function readWorkflowStandingAuthorization(metadata, revisionId) {
1788
+ if (!isRecord(metadata))
1789
+ return null;
1790
+ const raw = metadata[WORKFLOW_STANDING_AUTHORIZATION_METADATA_KEY];
1791
+ if (!isRecord(raw) || raw.approved !== true)
1792
+ return null;
1793
+ const maxCredits = typeof raw.max_credits === "number"
1794
+ ? raw.max_credits
1795
+ : Number(raw.max_credits);
1796
+ const capPolicy = raw.cap_policy === "plan_default"
1797
+ ? "plan_default"
1798
+ : raw.cap_policy === "explicit" || raw.cap_policy === undefined
1799
+ ? "explicit"
1800
+ : null;
1801
+ const authorizedAt = typeof raw.authorized_at === "string" ? raw.authorized_at.trim() : "";
1802
+ const authorizedByActorId = typeof raw.authorized_by_actor_id === "string"
1803
+ ? raw.authorized_by_actor_id.trim()
1804
+ : "";
1805
+ if (!capPolicy
1806
+ || (capPolicy === "explicit" && (!Number.isFinite(maxCredits) || maxCredits <= 0))
1807
+ || raw.revision_id !== revisionId
1808
+ || !authorizedByActorId
1809
+ || !authorizedAt
1810
+ || Number.isNaN(Date.parse(authorizedAt))) {
1811
+ return null;
1812
+ }
1813
+ const base = {
1814
+ approved: true,
1815
+ revisionId,
1816
+ authorizedByActorId,
1817
+ authorizedAt,
1818
+ };
1819
+ return capPolicy === "explicit"
1820
+ ? { ...base, capPolicy, maxCredits }
1821
+ : { ...base, capPolicy, maxCredits: null };
1822
+ }
1657
1823
  // Approval labels the run's enqueue route already collected SYNCHRONOUS human
1658
1824
  // approval for (a CLI/MCP `approved=true` gate before the run was created), and
1659
1825
  // recorded in run metadata as `preapproved_labels`. A live ctx.approvals.require()
1660
1826
  // call whose `label` is in this set is honored immediately by both recipe
1661
- // executors instead of parking on a durable decision row (local) or failing
1662
- // closed with approval_requires_local_executor (sandbox). Every other label
1663
- // still goes through the full durable gate, and the max_credits spend cap is
1827
+ // executors instead of creating and parking on a durable decision row. Every
1828
+ // other label goes through the same host-mediated durable gate under both local
1829
+ // and sandbox executors, and the max_credits spend cap is
1664
1830
  // enforced separately per paid tool step, so a pre-approved label never widens
1665
1831
  // spend. Only trusted server code (the enqueue route) sets this — it is read
1666
1832
  // from run metadata, never from user-suppliable workflow input.
@@ -0,0 +1 @@
1
+ export type CanonicalWorkflowToolEffect = "external_read" | "external_write";
@@ -0,0 +1,19 @@
1
+ // The TYPE of the one answer to "what does this workflow tool do — read or write?"
2
+ //
3
+ // The RESOLVER that produces it lives in @oxygen/tools
4
+ // (resolveWorkflowToolEffect / isWorkflowToolStaticallyKnown), deliberately NOT here.
5
+ //
6
+ // v1.513.2 put the resolver in this package, which forced @oxygen/workflows to depend
7
+ // on @oxygen/tools and @oxygen/mcp-server. This package is imported by the CLI for
8
+ // manifest compile/assert helpers and is bundled into the browser, so one line of
9
+ // resolver dragged tools -> providers -> control-db into both. That single edge broke
10
+ // four things: node:async_hooks reaching a client chunk (fixed v1.517.0), the worker
11
+ // Docker build order (v1.525.1), the CLI tarball's workspace vendoring, and — through
12
+ // the transitive closure — an attempt to ship `pg` inside the customer CLI.
13
+ //
14
+ // So @oxygen/workflows now depends on @oxygen/formula alone, and every caller that
15
+ // needs classification passes a resolver in. The safety property v1.513.2 was written
16
+ // to guarantee ("a caller who forgets gets the correct resolver, not silence") is now
17
+ // enforced by the COMPILER instead of a default: resolveToolEffect is required, so
18
+ // forgetting it does not typecheck.
19
+ export {};
@@ -6,20 +6,30 @@
6
6
  // @oxygen/integrations automation-usage.ts), and retried attempts bill again.
7
7
  // Surfaces must present these numbers as minimums; observed run history is the
8
8
  // honest projection (projectObservedAutomationUsage below).
9
+ import { loopBodyNodeIds } from "./graph/topology.js";
10
+ import { isWorkflowGraphManifest, workflowGraphNodeKindBills, MAX_WORKFLOW_LOOP_ITERATIONS, } from "./graph/types.js";
9
11
  const ROW_FANOUT_TOOL_IDS = new Set(["oxygen.rows_upsert"]);
10
12
  function isRecord(value) {
11
13
  return typeof value === "object" && value !== null && !Array.isArray(value);
12
14
  }
13
15
  // Tolerant of raw DB json (the scheduler feeds unvalidated manifest rows):
14
- // steps-array manifests count 1 per step; everything else is treated as a
15
- // recipe shape (1 + max(visual checkpoints, declared tools)). Byte-compatible
16
- // with the two prior copies in workflow-scheduler.ts and the web page.
16
+ // steps-array manifests count 1 per step; oxygen-workflows-v2 graphs count
17
+ // their billable nodes (see estimateWorkflowGraphActionsFloor); everything else
18
+ // is treated as a recipe shape (1 + max(visual checkpoints, declared tools)).
19
+ // The v1 branches stay byte-compatible with the two prior copies in
20
+ // workflow-scheduler.ts and the web page.
17
21
  export function estimateWorkflowRunAutomationActionsFloor(manifest) {
18
22
  if (!isRecord(manifest))
19
23
  return 1;
20
24
  const steps = Array.isArray(manifest.steps) ? manifest.steps : null;
21
25
  if (steps)
22
26
  return Math.max(1, steps.length);
27
+ // Must precede the recipe fallback: a v2 manifest carries neither `steps` nor
28
+ // `visual_plan`/`tools_used`, so without this branch every graph — five tool
29
+ // nodes or five hundred — fell through and estimated as exactly 1 action.
30
+ const graph = readWorkflowGraph(manifest);
31
+ if (graph)
32
+ return estimateWorkflowGraphActionsFloor(graph);
23
33
  const visualPlan = isRecord(manifest.visual_plan) ? manifest.visual_plan : null;
24
34
  const visualSteps = Array.isArray(visualPlan?.steps) ? visualPlan.steps : [];
25
35
  const checkpointCount = visualSteps.filter((step) => isRecord(step) && (step.kind !== "branch" || typeof step.checkpoint_id === "string")).length;
@@ -27,8 +37,27 @@ export function estimateWorkflowRunAutomationActionsFloor(manifest) {
27
37
  return 1 + Math.max(checkpointCount, toolsUsed.length, 0);
28
38
  }
29
39
  function manifestIsRecipeShaped(manifest) {
40
+ // A v2 graph has no `steps` array either, but it is the opposite of a recipe:
41
+ // every node that can bill is declared in the manifest.
42
+ if (isWorkflowGraphManifest(manifest))
43
+ return false;
30
44
  return isRecord(manifest) && !Array.isArray(manifest.steps);
31
45
  }
46
+ /**
47
+ * Whether the floor can be exceeded by orders of magnitude at run time. True for
48
+ * recipe manifests (arbitrary code, unknowable checkpoints) and for a v2 graph
49
+ * containing an enabled loop with no declared `max_iterations` — that loop
50
+ * iterates once per element of a collection that only exists at run time, so the
51
+ * single pass the floor assumes is a lower bound and nothing more.
52
+ */
53
+ function manifestHasDynamicRuntimeActions(manifest) {
54
+ if (manifestIsRecipeShaped(manifest))
55
+ return true;
56
+ const graph = readWorkflowGraph(manifest);
57
+ if (!graph)
58
+ return false;
59
+ return graph.nodes.some((node) => node.kind === "loop" && !node.disabled && declaredLoopIterations(node.maxIterations) === null);
60
+ }
32
61
  function manifestWritesRowsPerRun(manifest) {
33
62
  if (!isRecord(manifest))
34
63
  return false;
@@ -36,9 +65,111 @@ function manifestWritesRowsPerRun(manifest) {
36
65
  if (steps) {
37
66
  return steps.some((step) => isRecord(step) && step.kind === "tool" && typeof step.tool === "string" && ROW_FANOUT_TOOL_IDS.has(step.tool));
38
67
  }
68
+ const graph = readWorkflowGraph(manifest);
69
+ if (graph) {
70
+ return graph.nodes.some((node) => node.kind === "tool" && typeof node.tool === "string" && ROW_FANOUT_TOOL_IDS.has(node.tool));
71
+ }
39
72
  const toolsUsed = Array.isArray(manifest.tools_used) ? manifest.tools_used : [];
40
73
  return toolsUsed.some((tool) => typeof tool === "string" && ROW_FANOUT_TOOL_IDS.has(tool));
41
74
  }
75
+ function readWorkflowGraph(manifest) {
76
+ if (!isWorkflowGraphManifest(manifest))
77
+ return null;
78
+ const nodes = [];
79
+ for (const raw of manifest.nodes) {
80
+ if (!isRecord(raw))
81
+ continue;
82
+ if (typeof raw.id !== "string" || !raw.id || typeof raw.kind !== "string")
83
+ continue;
84
+ nodes.push({
85
+ id: raw.id,
86
+ kind: raw.kind,
87
+ disabled: raw.disabled === true,
88
+ tool: raw.tool,
89
+ maxIterations: raw.max_iterations,
90
+ });
91
+ }
92
+ const edges = manifest.edges.flatMap((raw) => {
93
+ if (!isRecord(raw) || typeof raw.source !== "string" || typeof raw.target !== "string")
94
+ return [];
95
+ return [{
96
+ id: typeof raw.id === "string" ? raw.id : `${raw.source}->${raw.target}`,
97
+ source: raw.source,
98
+ target: raw.target,
99
+ ...(typeof raw.source_handle === "string" ? { source_handle: raw.source_handle } : {}),
100
+ }];
101
+ });
102
+ return {
103
+ nodes,
104
+ // Cast: the traversal only ever reads the two node fields kept here, and
105
+ // giving it the raw (possibly malformed) nodes is what this sanitizing pass
106
+ // exists to prevent.
107
+ topology: {
108
+ ...manifest,
109
+ nodes: nodes.map((node) => ({ id: node.id, kind: node.kind })),
110
+ edges,
111
+ },
112
+ };
113
+ }
114
+ /**
115
+ * A declared, usable iteration cap, or null when the loop is unbounded.
116
+ *
117
+ * Clamped to MAX_WORKFLOW_LOOP_ITERATIONS because that is what the interpreter
118
+ * actually runs: a manifest declaring 50,000 iterations executes 1,000, and
119
+ * pricing the un-clamped number would make a cron cadence "impossible" over work
120
+ * that can never happen. The linter refuses an over-ceiling cap at authoring
121
+ * time; this clamp is what keeps an older or hand-written manifest honest.
122
+ */
123
+ function declaredLoopIterations(value) {
124
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0)
125
+ return null;
126
+ return Math.min(value, MAX_WORKFLOW_LOOP_ITERATIONS);
127
+ }
128
+ /**
129
+ * One action per billable node, multiplied by the iteration cap of every loop
130
+ * whose body contains it (nested loops compound, which is why the multipliers
131
+ * are collected per node rather than per loop).
132
+ *
133
+ * A loop with no declared `max_iterations` counts as a single pass. The count is
134
+ * a FLOOR, and one pass is the smallest a real run can have — over-counting an
135
+ * unknown collection would turn the billing gate into a guess. The unknowable
136
+ * remainder is reported through `dynamicRuntimeActions` instead.
137
+ *
138
+ * Disabled nodes are skipped, and so is the body of a disabled loop: a loop that
139
+ * never runs never enters its body. Both exclusions only ever move the estimate
140
+ * down, which is the safe direction for a lower bound.
141
+ */
142
+ function estimateWorkflowGraphActionsFloor(graph) {
143
+ const multipliers = new Map();
144
+ const skipped = new Set();
145
+ for (const node of graph.nodes) {
146
+ if (node.kind !== "loop")
147
+ continue;
148
+ const body = loopBodyNodeIds(graph.topology, node.id);
149
+ if (node.disabled) {
150
+ for (const bodyNodeId of body)
151
+ skipped.add(bodyNodeId);
152
+ continue;
153
+ }
154
+ const iterations = declaredLoopIterations(node.maxIterations);
155
+ if (iterations === null || iterations === 1)
156
+ continue;
157
+ for (const bodyNodeId of body) {
158
+ multipliers.set(bodyNodeId, (multipliers.get(bodyNodeId) ?? 1) * iterations);
159
+ }
160
+ }
161
+ let floor = 0;
162
+ for (const node of graph.nodes) {
163
+ if (node.disabled || skipped.has(node.id))
164
+ continue;
165
+ if (!workflowGraphNodeKindBills(node.kind))
166
+ continue;
167
+ floor += multipliers.get(node.id) ?? 1;
168
+ }
169
+ // Nested caps multiply, and two large ones overflow Number's exact range;
170
+ // clamping keeps every downstream product (runs x floor) a finite number.
171
+ return Math.max(1, Math.min(floor, Number.MAX_SAFE_INTEGER));
172
+ }
42
173
  export function estimateWorkflowAutomationUsage(manifest, cron) {
43
174
  if (!isRecord(manifest))
44
175
  return null;
@@ -48,7 +179,7 @@ export function estimateWorkflowAutomationUsage(manifest, cron) {
48
179
  actionsPerRunFloor,
49
180
  scheduledRunsPer30Days,
50
181
  scheduledActionsPer30DaysFloor: scheduledRunsPer30Days === null ? null : scheduledRunsPer30Days * actionsPerRunFloor,
51
- dynamicRuntimeActions: manifestIsRecipeShaped(manifest),
182
+ dynamicRuntimeActions: manifestHasDynamicRuntimeActions(manifest),
52
183
  perRowFanout: manifestWritesRowsPerRun(manifest),
53
184
  };
54
185
  }
@@ -9,6 +9,10 @@
9
9
  ".": {
10
10
  "types": "./dist/index.d.ts",
11
11
  "import": "./dist/index.js"
12
+ },
13
+ "./graph": {
14
+ "types": "./dist/graph/index.d.ts",
15
+ "import": "./dist/graph/index.js"
12
16
  }
13
17
  },
14
18
  "dependencies": {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.377.3",
3
+ "version": "1.575.19",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -40,13 +40,15 @@
40
40
  "langfuse": "3.38.20",
41
41
  "read-excel-file": "6.0.3",
42
42
  "typescript": "6.0.3",
43
+ "@oxygen/formula": "0.0.0",
44
+ "@oxygen/recipe-sdk": "0.0.0",
43
45
  "@oxygen/shared": "0.0.0",
44
- "@oxygen/workflows": "0.0.0",
45
- "@oxygen/recipe-sdk": "0.0.0"
46
+ "@oxygen/workflows": "0.0.0"
46
47
  },
47
48
  "bundledDependencies": [
49
+ "@oxygen/formula",
50
+ "@oxygen/recipe-sdk",
48
51
  "@oxygen/shared",
49
- "@oxygen/workflows",
50
- "@oxygen/recipe-sdk"
52
+ "@oxygen/workflows"
51
53
  ]
52
54
  }