@oxygen-agent/cli 1.365.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.
- package/README.md +1 -1
- package/dist/column-run-notices.d.ts +11 -0
- package/dist/column-run-notices.js +37 -0
- package/dist/command-manifest.js +13 -8
- package/dist/help.js +78 -16
- package/dist/index.js +3873 -514
- package/dist/skills.js +106 -1
- package/node_modules/@oxygen/formula/dist/coerce.d.ts +8 -0
- package/node_modules/@oxygen/formula/dist/coerce.js +10 -0
- package/node_modules/@oxygen/formula/dist/evaluate.d.ts +31 -0
- package/node_modules/@oxygen/formula/dist/evaluate.js +248 -0
- package/node_modules/@oxygen/formula/dist/expression.d.ts +64 -0
- package/node_modules/@oxygen/formula/dist/expression.js +428 -0
- package/node_modules/@oxygen/formula/dist/formula-functions.d.ts +71 -0
- package/node_modules/@oxygen/formula/dist/formula-functions.js +1100 -0
- package/node_modules/@oxygen/formula/dist/index.d.ts +17 -0
- package/node_modules/@oxygen/formula/dist/index.js +17 -0
- package/node_modules/@oxygen/formula/dist/value-normalizers.d.ts +30 -0
- package/node_modules/@oxygen/formula/dist/value-normalizers.js +80 -0
- package/node_modules/@oxygen/formula/package.json +26 -0
- package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +30 -0
- package/node_modules/@oxygen/recipe-sdk/dist/index.js +2 -2
- package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +60 -0
- package/node_modules/@oxygen/shared/dist/billing-anchors.js +135 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +101 -5
- package/node_modules/@oxygen/shared/dist/billing.js +192 -8
- package/node_modules/@oxygen/shared/dist/call-outcomes.d.ts +59 -0
- package/node_modules/@oxygen/shared/dist/call-outcomes.js +73 -0
- package/node_modules/@oxygen/shared/dist/cli-result.js +1 -0
- package/node_modules/@oxygen/shared/dist/credit-guidance.js +3 -1
- package/node_modules/@oxygen/shared/dist/crm-reply-events.d.ts +35 -0
- package/node_modules/@oxygen/shared/dist/crm-reply-events.js +31 -0
- package/node_modules/@oxygen/shared/dist/dial-guardrail-overrides.d.ts +50 -0
- package/node_modules/@oxygen/shared/dist/dial-guardrail-overrides.js +65 -0
- package/node_modules/@oxygen/shared/dist/directory.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/directory.js +1 -0
- package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +17 -1
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +52 -3
- package/node_modules/@oxygen/shared/dist/index.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/index.js +15 -0
- package/node_modules/@oxygen/shared/dist/langfuse.d.ts +77 -0
- package/node_modules/@oxygen/shared/dist/langfuse.js +231 -0
- package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.d.ts +31 -0
- package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.js +56 -0
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +5 -4
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +5 -4
- package/node_modules/@oxygen/shared/dist/linkedin-url.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/linkedin-url.js +7 -4
- package/node_modules/@oxygen/shared/dist/log.js +56 -4
- package/node_modules/@oxygen/shared/dist/microsoft-consent-url.d.ts +7 -0
- package/node_modules/@oxygen/shared/dist/microsoft-consent-url.js +29 -0
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +31 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +61 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +636 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.js +199 -0
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +89 -23
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +88 -24
- package/node_modules/@oxygen/shared/dist/sequence-crm-events.d.ts +291 -0
- package/node_modules/@oxygen/shared/dist/sequence-crm-events.js +224 -0
- package/node_modules/@oxygen/shared/dist/sequence-template.d.ts +42 -1
- package/node_modules/@oxygen/shared/dist/sequence-template.js +0 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +287 -24
- package/node_modules/@oxygen/shared/dist/sequences.js +940 -60
- package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +70 -0
- package/node_modules/@oxygen/shared/dist/spend-safety.js +106 -0
- package/node_modules/@oxygen/shared/dist/tags.d.ts +90 -1
- package/node_modules/@oxygen/shared/dist/tags.js +126 -6
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.js +4 -0
- package/node_modules/@oxygen/shared/dist/workspace-agents.d.ts +8 -7
- package/node_modules/@oxygen/shared/dist/workspace-agents.js +34 -7
- package/node_modules/@oxygen/shared/package.json +95 -0
- package/node_modules/@oxygen/workflows/dist/event-dispatch.d.ts +126 -0
- package/node_modules/@oxygen/workflows/dist/event-dispatch.js +173 -0
- package/node_modules/@oxygen/workflows/dist/graph/expression.d.ts +78 -0
- package/node_modules/@oxygen/workflows/dist/graph/expression.js +700 -0
- package/node_modules/@oxygen/workflows/dist/graph/index.d.ts +20 -0
- package/node_modules/@oxygen/workflows/dist/graph/index.js +20 -0
- package/node_modules/@oxygen/workflows/dist/graph/lint.d.ts +4 -0
- package/node_modules/@oxygen/workflows/dist/graph/lint.js +812 -0
- package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +501 -0
- package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.js +200 -0
- package/node_modules/@oxygen/workflows/dist/graph/params.d.ts +86 -0
- package/node_modules/@oxygen/workflows/dist/graph/params.js +173 -0
- package/node_modules/@oxygen/workflows/dist/graph/remap.d.ts +48 -0
- package/node_modules/@oxygen/workflows/dist/graph/remap.js +213 -0
- package/node_modules/@oxygen/workflows/dist/graph/topology.d.ts +46 -0
- package/node_modules/@oxygen/workflows/dist/graph/topology.js +280 -0
- package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +270 -0
- package/node_modules/@oxygen/workflows/dist/graph/types.js +93 -0
- package/node_modules/@oxygen/workflows/dist/index.d.ts +113 -1
- package/node_modules/@oxygen/workflows/dist/index.js +179 -13
- package/node_modules/@oxygen/workflows/dist/tool-effects.d.ts +1 -0
- package/node_modules/@oxygen/workflows/dist/tool-effects.js +19 -0
- package/node_modules/@oxygen/workflows/dist/usage-estimate.js +135 -4
- package/node_modules/@oxygen/workflows/package.json +4 -0
- package/package.json +10 -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
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1662
|
-
//
|
|
1663
|
-
//
|
|
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;
|
|
15
|
-
//
|
|
16
|
-
//
|
|
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:
|
|
182
|
+
dynamicRuntimeActions: manifestHasDynamicRuntimeActions(manifest),
|
|
52
183
|
perRowFanout: manifestWritesRowsPerRun(manifest),
|
|
53
184
|
};
|
|
54
185
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxygen-agent/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.575.19",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -32,18 +32,23 @@
|
|
|
32
32
|
"README.md"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
+
"@aws-sdk/client-s3": "3.1050.0",
|
|
36
|
+
"@aws-sdk/s3-request-presigner": "3.1050.0",
|
|
35
37
|
"@opentelemetry/api": "1.9.1",
|
|
36
38
|
"commander": "14.0.3",
|
|
37
39
|
"esbuild": "0.27.7",
|
|
40
|
+
"langfuse": "3.38.20",
|
|
38
41
|
"read-excel-file": "6.0.3",
|
|
39
42
|
"typescript": "6.0.3",
|
|
43
|
+
"@oxygen/formula": "0.0.0",
|
|
44
|
+
"@oxygen/recipe-sdk": "0.0.0",
|
|
40
45
|
"@oxygen/shared": "0.0.0",
|
|
41
|
-
"@oxygen/workflows": "0.0.0"
|
|
42
|
-
"@oxygen/recipe-sdk": "0.0.0"
|
|
46
|
+
"@oxygen/workflows": "0.0.0"
|
|
43
47
|
},
|
|
44
48
|
"bundledDependencies": [
|
|
49
|
+
"@oxygen/formula",
|
|
50
|
+
"@oxygen/recipe-sdk",
|
|
45
51
|
"@oxygen/shared",
|
|
46
|
-
"@oxygen/workflows"
|
|
47
|
-
"@oxygen/recipe-sdk"
|
|
52
|
+
"@oxygen/workflows"
|
|
48
53
|
]
|
|
49
54
|
}
|