@oxygen-agent/cli 1.922.12 → 1.936.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 (46) hide show
  1. package/README.md +1 -1
  2. package/dist/admin-primary-providers-render.d.ts +18 -0
  3. package/dist/admin-primary-providers-render.js +371 -0
  4. package/dist/command-manifest.js +6 -0
  5. package/dist/functions-commands.d.ts +6 -0
  6. package/dist/functions-commands.js +56 -0
  7. package/dist/http-client.d.ts +4 -0
  8. package/dist/http-client.js +49 -2
  9. package/dist/index.js +175 -40
  10. package/dist/ugc-commands.d.ts +6 -0
  11. package/dist/ugc-commands.js +748 -0
  12. package/dist/visual-commands.d.ts +6 -0
  13. package/dist/visual-commands.js +57 -0
  14. package/dist/visual-render-wait.d.ts +3 -0
  15. package/dist/visual-render-wait.js +56 -0
  16. package/node_modules/@oxygen/shared/dist/byok-connect.d.ts +43 -0
  17. package/node_modules/@oxygen/shared/dist/byok-connect.js +84 -0
  18. package/node_modules/@oxygen/shared/dist/capability-discovery.js +26 -10
  19. package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +2 -0
  20. package/node_modules/@oxygen/shared/dist/feature-gates.js +3 -0
  21. package/node_modules/@oxygen/shared/dist/index.d.ts +4 -0
  22. package/node_modules/@oxygen/shared/dist/index.js +4 -0
  23. package/node_modules/@oxygen/shared/dist/knowledge-constants.d.ts +1 -1
  24. package/node_modules/@oxygen/shared/dist/knowledge-constants.js +3 -2
  25. package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +1 -1
  26. package/node_modules/@oxygen/shared/dist/langfuse.js +17 -0
  27. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +6 -0
  28. package/node_modules/@oxygen/shared/dist/object-storage.js +5 -0
  29. package/node_modules/@oxygen/shared/dist/provider-balance-signal.d.ts +113 -0
  30. package/node_modules/@oxygen/shared/dist/provider-balance-signal.js +158 -0
  31. package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +12 -0
  32. package/node_modules/@oxygen/shared/dist/sequence-failures.js +24 -0
  33. package/node_modules/@oxygen/shared/dist/sequences.d.ts +16 -0
  34. package/node_modules/@oxygen/shared/dist/sequences.js +54 -0
  35. package/node_modules/@oxygen/shared/dist/ugc.d.ts +113 -0
  36. package/node_modules/@oxygen/shared/dist/ugc.js +2 -0
  37. package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.d.ts +9 -0
  38. package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.js +33 -0
  39. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  40. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  41. package/node_modules/@oxygen/shared/dist/visual-render.d.ts +30 -0
  42. package/node_modules/@oxygen/shared/dist/visual-render.js +55 -0
  43. package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +43 -0
  44. package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +126 -0
  45. package/node_modules/@oxygen/shared/package.json +10 -0
  46. package/package.json +1 -1
@@ -0,0 +1,158 @@
1
+ /**
2
+ * The managed-provider BALANCE signal — one contract shared by the two producers
3
+ * that can observe it and the one Axiom monitor that alerts on it.
4
+ *
5
+ * WHY THIS EXISTS. Until now the only way OXYGEN learned that one of its POOLED
6
+ * provider accounts had run out of money was a customer's call being refused.
7
+ * `provider.managed_credits_exhausted` fires at that moment — after the failure,
8
+ * never before it. Plain T-104 (Mentcape, 2026-08-24 and again 2026-08-31) is what
9
+ * that costs: `serper.places`, `serper.maps` and `parallel.search` refused live
10
+ * calls for a workspace holding 75,347 Oxygen credits, because OUR balance with
11
+ * those vendors was empty. Prod Axiom over the 16 days to 2026-09-05 shows the same
12
+ * refusal reaching >=6 orgs on serper, >=3 on parallel and >=5 on exa.
13
+ *
14
+ * The balance snapshot cron already read most of those balances every two hours and
15
+ * wrote them to `provider_balance_snapshots`. It emitted no per-provider log line at
16
+ * all, so a balance sliding toward zero was visible only to a human who opened
17
+ * /admin/costs. This module is the missing signal: one structured line per managed
18
+ * provider per snapshot, carrying the number AND the verdict.
19
+ *
20
+ * TWO PRODUCERS, ONE VOCABULARY.
21
+ *
22
+ * - `provider_balance.snapshot` — the cron, for every provider whose balance we can
23
+ * actually read. Proactive: it fires while there is still money left.
24
+ * - `provider_balance.exhausted` — the tool runner, when a managed account actually
25
+ * refuses a paid call. That is the ONLY balance evidence available for a provider
26
+ * with no usable balance API, which is exactly the T-104 three (see
27
+ * PROVIDERS_WITHOUT_BALANCE_API in @oxygen/providers). Without it the new monitor
28
+ * would be structurally blind to the providers that caused the incident.
29
+ *
30
+ * Both carry the same `status` vocabulary so ONE monitor query covers both, and both
31
+ * put `status` and `provider` in flat fields because those are the two dimensions the
32
+ * monitor filters and groups on. Every other field rides in the `worker_fields` map
33
+ * at zero column cost — see AXIOM_STABLE_FIELDS in ./axiom-field-budget.ts, which is
34
+ * load-bearing in both directions.
35
+ */
36
+ /** The proactive line, emitted per managed provider by the balance-snapshot cron. */
37
+ export const PROVIDER_BALANCE_SNAPSHOT_MSG = "provider_balance.snapshot";
38
+ /**
39
+ * The reactive line, emitted by the tool runner when a managed account refuses a paid
40
+ * call. Deliberately a SEPARATE msg from `provider.managed_credits_exhausted`: that
41
+ * one is the refusal event (a customer call failed), this one is the balance fact (our
42
+ * account is empty). The refusal monitor counts the former; conflating them would make
43
+ * one query mean two different things.
44
+ */
45
+ export const PROVIDER_BALANCE_EXHAUSTED_MSG = "provider_balance.exhausted";
46
+ /** The statuses the P1 balance monitor alerts on. Anything else is informational. */
47
+ export const ALERTING_PROVIDER_BALANCE_STATUSES = [
48
+ "low",
49
+ "exhausted",
50
+ ];
51
+ /**
52
+ * Default low-balance floor, in the provider's OWN unit (almost always provider
53
+ * credits). 1,000 is not a round number chosen for looking tidy — it is roughly
54
+ * three to eight days of measured burn for the credit providers OXYGEN funds,
55
+ * taken from `provider_balance_snapshots` in the prod control DB on 2026-09-07
56
+ * over the preceding 30 days:
57
+ *
58
+ * bettercontact 10,130 -> 364 (~325/day) ~3 days of head-room at 1,000
59
+ * leadmagic 7,702 -> 903 (~227/day) ~4 days
60
+ * millionverifier 41,357 -> 37,695 (~122/day) ~8 days
61
+ * ai_ark 10,000 -> 9,813 (~6/day) months
62
+ *
63
+ * A floor is a claim about how the account behaved when it was measured, nothing
64
+ * more. Re-measure it rather than nudging it when it turns out to be noisy — the
65
+ * monitor changelog exists for exactly that conversation.
66
+ */
67
+ export const DEFAULT_MANAGED_BALANCE_FLOOR = 1_000;
68
+ /**
69
+ * Per-provider floors, and the providers that are deliberately NOT balance-monitored
70
+ * (`null`). Only entries that the default gets wrong are listed; everything else
71
+ * inherits DEFAULT_MANAGED_BALANCE_FLOOR.
72
+ */
73
+ export const MANAGED_BALANCE_FLOORS = {
74
+ // A 1,000-credit/month plan. The default floor would mark it low on the first day
75
+ // of every billing period, forever — the classic alert nobody reads. Measured
76
+ // 2026-09-07: it has ranged 1,002 -> 670 over 30 days, so 150 is ~15% of the plan
77
+ // and still several days of head-room.
78
+ firecrawl: 150,
79
+ // Unit is REQUESTS, not credits, and the managed key has reported a null balance on
80
+ // every row for 30 days (see `unknown` above). The floor is a guess until the
81
+ // fetcher returns a number; it costs nothing while the balance stays unreadable.
82
+ contactout: 250,
83
+ // USD, not credits. Measured 2026-09-07: $23.29 -> $7.98 over 30 days, and an empty
84
+ // OpenRouter balance stops every AI column in the product.
85
+ openrouter: 5,
86
+ // NOT a spendable balance. Stripe's /v1/balance is the PAYOUT balance — money owed
87
+ // to us, which sits at 0.00 in the normal case (it did on 2026-09-07) and goes
88
+ // negative after a refund. A floor here would page daily about nothing.
89
+ stripe: null,
90
+ // Reachability probes with no balance concept at all: both fetchers return
91
+ // `balanceRemaining: null` by construction.
92
+ vercel: null,
93
+ neon: null,
94
+ };
95
+ /** `OXYGEN_MANAGED_BALANCE_FLOOR_FIRECRAWL`, `..._AI_ARK`, ... */
96
+ export function managedBalanceFloorEnvVar(provider) {
97
+ return `OXYGEN_MANAGED_BALANCE_FLOOR_${provider.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}`;
98
+ }
99
+ /**
100
+ * The floor in force for one provider. `null` means "do not judge this balance".
101
+ *
102
+ * The env override is the operator's escape hatch between deploys: a top-up that
103
+ * changes the plan size, or a provider that turns out to be noisy at the default,
104
+ * is one Doppler value away from being right. `off`/`none`/`disabled` switches the
105
+ * provider off the monitor entirely; an unparseable or negative value is IGNORED
106
+ * rather than obeyed, because a typo must not silently disarm an alert.
107
+ */
108
+ export function managedBalanceFloor(provider, env = process.env) {
109
+ const raw = env[managedBalanceFloorEnvVar(provider)]?.trim();
110
+ if (raw) {
111
+ const lowered = raw.toLowerCase();
112
+ if (lowered === "off" || lowered === "none" || lowered === "disabled")
113
+ return null;
114
+ const parsed = Number(raw);
115
+ if (Number.isFinite(parsed) && parsed >= 0)
116
+ return parsed;
117
+ }
118
+ const configured = MANAGED_BALANCE_FLOORS[provider];
119
+ if (configured === null)
120
+ return null;
121
+ return configured ?? DEFAULT_MANAGED_BALANCE_FLOOR;
122
+ }
123
+ /**
124
+ * Turn one balance reading into the verdict both producers log.
125
+ *
126
+ * Pure on purpose: the thresholds are the alerting policy, so they are unit-testable
127
+ * without a provider, a cron, or a network.
128
+ */
129
+ export function classifyManagedBalance(input) {
130
+ const floor = managedBalanceFloor(input.provider, input.env ?? process.env);
131
+ if (input.fetchStatus !== "ok") {
132
+ // We could not read the balance. That is a monitoring outage, not a funding
133
+ // outage, so it must NOT enter the balance monitor: a rotated key would
134
+ // otherwise page as "the account is empty" and send ops to top up an account
135
+ // that is full. It is still a warn, because a fetcher that has been blind for a
136
+ // week is how a dry account stays invisible.
137
+ return { status: input.fetchStatus, balanceLow: false, floor, level: "warn" };
138
+ }
139
+ // "Not monitored" is decided BEFORE "no number", because for these providers the
140
+ // absent number is the design, not a symptom. Vercel and Neon have no balance concept
141
+ // at all and their fetchers return null by construction as reachability probes; the
142
+ // other order filed them under `unknown`, the status that means "we asked and got no
143
+ // number" and exists so a fetcher that has gone blind is visible. Two permanent rows
144
+ // in that bucket is how a real blind fetcher stops standing out.
145
+ if (floor === null) {
146
+ return { status: "not_monitored", balanceLow: false, floor: null, level: "info" };
147
+ }
148
+ if (input.balanceRemaining === null) {
149
+ return { status: "unknown", balanceLow: false, floor, level: "info" };
150
+ }
151
+ if (input.balanceRemaining <= 0) {
152
+ return { status: "exhausted", balanceLow: true, floor, level: "error" };
153
+ }
154
+ if (input.balanceRemaining <= floor) {
155
+ return { status: "low", balanceLow: true, floor, level: "warn" };
156
+ }
157
+ return { status: "ok", balanceLow: false, floor, level: "info" };
158
+ }
@@ -39,6 +39,18 @@ export type SequenceFailureOverrides = {
39
39
  * retries, defers, fails, consumes credits, or advances an enrollment.
40
40
  */
41
41
  export declare function serializeSequenceFailure(error: unknown, overrides?: SequenceFailureOverrides): Record<string, unknown> & SequenceFailureEnvelope;
42
+ /**
43
+ * Does this stored `error` value describe a FAILURE at all? A Sequence action's
44
+ * `error` column is also where deferrals stamp their diagnostic context
45
+ * (`native_email_defer_log`, `defer_count`, sender status …), and readSequenceFailure
46
+ * will happily classify that context as an `unknown` failure because it is
47
+ * built to never lose a historical error. Grasp (2026-09-09): every action parked
48
+ * on mailbox_spacing therefore surfaced as `failure:unknown` in `sequences
49
+ * status`, mirroring the deferral count 1:1 while nothing had failed. A failure
50
+ * carries at least one of the failure fields below; a deferral record carries
51
+ * none of them.
52
+ */
53
+ export declare function hasSequenceFailureSignal(error: unknown): boolean;
42
54
  /**
43
55
  * Read both the versioned envelope and historical Sequence error shapes. A
44
56
  * missing fact stays missing: in particular, this reader never manufactures a
@@ -55,6 +55,30 @@ export function serializeSequenceFailure(error, overrides = {}) {
55
55
  Object.assign(output, failure, { error_message: failure.message });
56
56
  return output;
57
57
  }
58
+ /**
59
+ * Does this stored `error` value describe a FAILURE at all? A Sequence action's
60
+ * `error` column is also where deferrals stamp their diagnostic context
61
+ * (`native_email_defer_log`, `defer_count`, sender status …), and readSequenceFailure
62
+ * will happily classify that context as an `unknown` failure because it is
63
+ * built to never lose a historical error. Grasp (2026-09-09): every action parked
64
+ * on mailbox_spacing therefore surfaced as `failure:unknown` in `sequences
65
+ * status`, mirroring the deferral count 1:1 while nothing had failed. A failure
66
+ * carries at least one of the failure fields below; a deferral record carries
67
+ * none of them.
68
+ */
69
+ export function hasSequenceFailureSignal(error) {
70
+ if (error === null || error === undefined)
71
+ return false;
72
+ if (typeof error === "string")
73
+ return error.trim().length > 0;
74
+ if (error instanceof Error)
75
+ return true;
76
+ const records = failureRecords(error);
77
+ if (records.length === 0)
78
+ return false;
79
+ return (firstString(records, ["failure_class", "code", "error_code", "errorCode"]) !== null
80
+ || firstString(records, ["message", "error_message", "errorMessage"]) !== null);
81
+ }
58
82
  /**
59
83
  * Read both the versioned envelope and historical Sequence error shapes. A
60
84
  * missing fact stays missing: in particular, this reader never manufactures a
@@ -1010,4 +1010,20 @@ export declare function resolveSendWindowTimezone(window: SequenceSendWindow, ro
1010
1010
  * start>end wraps past midnight.
1011
1011
  */
1012
1012
  export declare function isWithinSendWindow(window: SequenceSendWindow, now: Date, timezone?: string): boolean;
1013
+ /**
1014
+ * Seconds until today's send window closes, evaluated like isWithinSendWindow
1015
+ * (same timezone resolution, same wrap-past-midnight rule). Returns null when
1016
+ * `now` is outside the window, the window is malformed, or the timezone cannot be
1017
+ * resolved — every case where "remaining window" has no meaning and the caller
1018
+ * must fall back to the plain cap-over-window spacing. Minute granularity, like
1019
+ * the window itself.
1020
+ */
1021
+ export declare function secondsUntilSendWindowEnd(window: SequenceSendWindow, now: Date, timezone?: string): number | null;
1022
+ /**
1023
+ * Seconds since today's send window opened, with the same rules and null cases
1024
+ * as secondsUntilSendWindowEnd. Together the two locate the window's opening
1025
+ * instant, which is what lets a caller tell whether a day-keyed counter still
1026
+ * describes this window's sends.
1027
+ */
1028
+ export declare function secondsSinceSendWindowStart(window: SequenceSendWindow, now: Date, timezone?: string): number | null;
1013
1029
  export {};
@@ -2300,6 +2300,60 @@ export function isWithinSendWindow(window, now, timezone) {
2300
2300
  const minutes = parts.hour * 60 + parts.minute;
2301
2301
  return startM < endM ? minutes >= startM && minutes < endM : minutes >= startM || minutes < endM;
2302
2302
  }
2303
+ /**
2304
+ * Seconds until today's send window closes, evaluated like isWithinSendWindow
2305
+ * (same timezone resolution, same wrap-past-midnight rule). Returns null when
2306
+ * `now` is outside the window, the window is malformed, or the timezone cannot be
2307
+ * resolved — every case where "remaining window" has no meaning and the caller
2308
+ * must fall back to the plain cap-over-window spacing. Minute granularity, like
2309
+ * the window itself.
2310
+ */
2311
+ export function secondsUntilSendWindowEnd(window, now, timezone) {
2312
+ if (!isWithinSendWindow(window, now, timezone))
2313
+ return null;
2314
+ const tz = timezone?.trim() ? timezone.trim() : window.timezone;
2315
+ const parts = tzParts(now, tz) ?? tzParts(now, window.timezone) ?? tzParts(now, "UTC");
2316
+ if (!parts)
2317
+ return null;
2318
+ const startM = hhmmToMinutes(window.start);
2319
+ const endM = hhmmToMinutes(window.end);
2320
+ if (startM === null || endM === null || startM === endM)
2321
+ return null;
2322
+ const minutes = parts.hour * 60 + parts.minute;
2323
+ let remainingMinutes;
2324
+ if (startM < endM) {
2325
+ remainingMinutes = endM - minutes;
2326
+ }
2327
+ else {
2328
+ // Wraps past midnight: before the wrap the window runs to midnight and on
2329
+ // through to `end`; after the wrap only `end - now` is left.
2330
+ remainingMinutes = minutes >= startM ? (24 * 60 - minutes) + endM : endM - minutes;
2331
+ }
2332
+ return remainingMinutes > 0 ? remainingMinutes * 60 : null;
2333
+ }
2334
+ /**
2335
+ * Seconds since today's send window opened, with the same rules and null cases
2336
+ * as secondsUntilSendWindowEnd. Together the two locate the window's opening
2337
+ * instant, which is what lets a caller tell whether a day-keyed counter still
2338
+ * describes this window's sends.
2339
+ */
2340
+ export function secondsSinceSendWindowStart(window, now, timezone) {
2341
+ if (!isWithinSendWindow(window, now, timezone))
2342
+ return null;
2343
+ const tz = timezone?.trim() ? timezone.trim() : window.timezone;
2344
+ const parts = tzParts(now, tz) ?? tzParts(now, window.timezone) ?? tzParts(now, "UTC");
2345
+ if (!parts)
2346
+ return null;
2347
+ const startM = hhmmToMinutes(window.start);
2348
+ const endM = hhmmToMinutes(window.end);
2349
+ if (startM === null || endM === null || startM === endM)
2350
+ return null;
2351
+ const minutes = parts.hour * 60 + parts.minute;
2352
+ const elapsedMinutes = startM < endM || minutes >= startM
2353
+ ? minutes - startM
2354
+ : minutes + 24 * 60 - startM; // wrapped past midnight: opened yesterday evening
2355
+ return elapsedMinutes >= 0 ? elapsedMinutes * 60 : null;
2356
+ }
2303
2357
  function tzParts(now, tz) {
2304
2358
  try {
2305
2359
  const formatted = new Intl.DateTimeFormat("en-US", {
@@ -0,0 +1,113 @@
1
+ /** UGC is a Publishing capability. These are projections, never a second Posts store. */
2
+ export type UgcApprovalMode = "creator" | "delegated";
3
+ export type UgcProgram = {
4
+ id: string;
5
+ name: string;
6
+ productUrl: string;
7
+ description: string;
8
+ knowledgePageSlugs: string[];
9
+ brandApprovalRequired: boolean;
10
+ status: "active" | "archived";
11
+ createdAt: Date;
12
+ updatedAt: Date;
13
+ };
14
+ export type UgcBrief = {
15
+ id: string;
16
+ programId: string;
17
+ title: string;
18
+ prompt: string;
19
+ questions: string[];
20
+ cycle: string | null;
21
+ createdAt: Date;
22
+ updatedAt: Date;
23
+ };
24
+ export type UgcPostProjection = {
25
+ title?: string | null;
26
+ contentText: string;
27
+ status: string;
28
+ scheduledAt?: string | null;
29
+ publishedAt?: string | null;
30
+ publishedUrl?: string | null;
31
+ providerPostId?: string | null;
32
+ metrics?: Record<string, number | null>;
33
+ metricsSyncedAt?: string | null;
34
+ projectedAt?: string;
35
+ commentsSyncedAt?: string | null;
36
+ publicComments?: {
37
+ id: string;
38
+ parentId: string | null;
39
+ body: string;
40
+ authorName: string | null;
41
+ authorProfileUrl: string | null;
42
+ createdAt: string | null;
43
+ }[];
44
+ commentCoverage?: {
45
+ totalKnown: number;
46
+ projected: number;
47
+ truncated: boolean;
48
+ status: string;
49
+ };
50
+ metricsHistory?: {
51
+ date: string;
52
+ reactions: number | null;
53
+ comments: number | null;
54
+ shares: number | null;
55
+ impressions: number | null;
56
+ }[];
57
+ };
58
+ export type UgcPostLink = {
59
+ id: string;
60
+ programId: string;
61
+ participationId: string;
62
+ creatorOrgId: string;
63
+ masterOrgId: string;
64
+ brandApprovalRequired: boolean;
65
+ sourcePostId: string;
66
+ briefId: string | null;
67
+ attribution: Record<string, unknown>;
68
+ projection: UgcPostProjection;
69
+ sourceRevision: string;
70
+ creatorApprovedRevision: string | null;
71
+ brandApprovedRevision: string | null;
72
+ grantVersion: number;
73
+ createdAt: Date;
74
+ updatedAt: Date;
75
+ };
76
+ export type UgcVoiceImport = {
77
+ id: string;
78
+ senderId: string;
79
+ linkedinUrl: string;
80
+ programId: string;
81
+ participationId: string;
82
+ grantVersion: number;
83
+ status: "queued" | "running" | "completed" | "failed" | "cancelled";
84
+ cursor: string | null;
85
+ importedCount: number;
86
+ scannedCount: number;
87
+ maxPosts: number;
88
+ creditCap: number;
89
+ creditsUsed: number;
90
+ receiptIds: string[];
91
+ error: string | null;
92
+ approvedByUserId: string | null;
93
+ approvedAt: Date;
94
+ leaseToken: string | null;
95
+ leaseExpiresAt: Date | null;
96
+ phase: "pending" | "scraping" | "synthesizing" | "ready" | "effect_unknown";
97
+ importedPostIds: string[];
98
+ pageNumber: number;
99
+ backfillComplete: boolean;
100
+ synthesizedCount: number;
101
+ voicePageId: string | null;
102
+ samples: {
103
+ providerPostId: string;
104
+ contentText: string;
105
+ providerPublishedAt: string | null;
106
+ providerUrl: string;
107
+ }[];
108
+ pendingPage: Record<string, unknown> | null;
109
+ createdAt: Date;
110
+ updatedAt: Date;
111
+ };
112
+ export declare const UGC_MONTHLY_CREATOR_CREDITS = 10000;
113
+ export declare const UGC_SPONSORED_LINKEDIN_SEATS = 1;
@@ -0,0 +1,2 @@
1
+ export const UGC_MONTHLY_CREATOR_CREDITS = 10_000;
2
+ export const UGC_SPONSORED_LINKEDIN_SEATS = 1;
@@ -0,0 +1,9 @@
1
+ /** The Sandbox SDK retries transport failures and 429/5xx responses even for
2
+ * POSTs. Visual execution cannot replay an ambiguously accepted mutation.
3
+ * The SDK recognizes AbortError as non-retryable; outcomeUnknown deliberately
4
+ * distinguishes this transport fence from ordinary caller cancellation. */
5
+ export declare class SandboxMutationOutcomeUnknownError extends Error {
6
+ readonly outcomeUnknown = true;
7
+ constructor(options?: ErrorOptions);
8
+ }
9
+ export declare function createSandboxNonReplayFetch(rawFetch?: typeof globalThis.fetch): typeof globalThis.fetch;
@@ -0,0 +1,33 @@
1
+ /** The Sandbox SDK retries transport failures and 429/5xx responses even for
2
+ * POSTs. Visual execution cannot replay an ambiguously accepted mutation.
3
+ * The SDK recognizes AbortError as non-retryable; outcomeUnknown deliberately
4
+ * distinguishes this transport fence from ordinary caller cancellation. */
5
+ export class SandboxMutationOutcomeUnknownError extends Error {
6
+ outcomeUnknown = true;
7
+ constructor(options) {
8
+ super("Sandbox mutation acceptance requires reconciliation.", options);
9
+ this.name = "AbortError";
10
+ }
11
+ }
12
+ export function createSandboxNonReplayFetch(rawFetch = globalThis.fetch) {
13
+ return async (input, init) => {
14
+ const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
15
+ if (["GET", "HEAD", "OPTIONS"].includes(method))
16
+ return rawFetch(input, init);
17
+ try {
18
+ // Redirects must not resubmit a mutation either. No request URL, body or
19
+ // authentication header is copied into the error or telemetry.
20
+ const response = await rawFetch(input, { ...init, redirect: "error" });
21
+ if (response.status === 429 || response.status >= 500) {
22
+ void response.body?.cancel().catch(() => { });
23
+ throw new SandboxMutationOutcomeUnknownError();
24
+ }
25
+ return response;
26
+ }
27
+ catch (error) {
28
+ if (error instanceof SandboxMutationOutcomeUnknownError)
29
+ throw error;
30
+ throw new SandboxMutationOutcomeUnknownError({ cause: error });
31
+ }
32
+ };
33
+ }
@@ -1,4 +1,4 @@
1
- export declare const OXYGEN_VERSION = "1.922.12";
1
+ export declare const OXYGEN_VERSION = "1.936.1";
2
2
  export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.181.0";
3
3
  export declare const MANAGED_INBOX_MINIMUM_CLI_VERSION = "1.326.2";
4
4
  export declare const SUPPORT_AGENT_REPLY_MINIMUM_CLI_VERSION = "1.747.0";
@@ -1,4 +1,4 @@
1
- export const OXYGEN_VERSION = "1.922.12";
1
+ export const OXYGEN_VERSION = "1.936.1";
2
2
  // The GLOBAL CLI compatibility floor: the oldest CLI allowed to call any
3
3
  // operational route. Raising it hard-rejects every older CLI from the entire
4
4
  // product, so it obeys one law, enforced by scripts/ci/cli-min-version-gate.mjs:
@@ -0,0 +1,30 @@
1
+ export declare const VISUAL_RENDER_PROFILE: "oxygen-static-v1";
2
+ export declare const VISUAL_RENDER_LIMITS: Readonly<{
3
+ maxPages: 12;
4
+ maxSide: 4096;
5
+ maxPixels: 32000000;
6
+ maxSourceBytes: number;
7
+ maxOutputBytes: number;
8
+ timeoutMs: 120000;
9
+ }>;
10
+ export type VisualRenderRequest = {
11
+ source_file_ids: string[];
12
+ width: number;
13
+ height: number;
14
+ scale: 1 | 2;
15
+ formats: ("png" | "pdf")[];
16
+ };
17
+ export type VisualRenderProfile = {
18
+ version: typeof VISUAL_RENDER_PROFILE;
19
+ renderSnapshotId: string;
20
+ validatorSnapshotId: string;
21
+ rendererDigest: string;
22
+ expiresAt: string;
23
+ credits: number;
24
+ maxCostMicros: number;
25
+ };
26
+ export declare function visualRenderProfilesMatch(a: VisualRenderProfile, b: VisualRenderProfile): boolean;
27
+ export declare function validateVisualFileId(value: unknown): string;
28
+ export declare function parseVisualRenderRequest(value: Record<string, unknown>): VisualRenderRequest;
29
+ /** No default snapshot or tariff: enabling this profile is an explicit deployment operation. */
30
+ export declare function resolveVisualRenderProfile(env: Record<string, string | undefined>, now?: number): VisualRenderProfile | null;
@@ -0,0 +1,55 @@
1
+ import { OxygenError } from "./cli-result.js";
2
+ export const VISUAL_RENDER_PROFILE = "oxygen-static-v1";
3
+ export const VISUAL_RENDER_LIMITS = Object.freeze({
4
+ maxPages: 12, maxSide: 4096, maxPixels: 32_000_000,
5
+ maxSourceBytes: 2 * 1024 * 1024, maxOutputBytes: 50 * 1024 * 1024,
6
+ timeoutMs: 120_000,
7
+ });
8
+ export function visualRenderProfilesMatch(a, b) {
9
+ return a.version === b.version && a.renderSnapshotId === b.renderSnapshotId
10
+ && a.validatorSnapshotId === b.validatorSnapshotId && a.rendererDigest === b.rendererDigest
11
+ && a.expiresAt === b.expiresAt && a.credits === b.credits && a.maxCostMicros === b.maxCostMicros;
12
+ }
13
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
14
+ export function validateVisualFileId(value) {
15
+ if (typeof value !== "string" || !uuid.test(value))
16
+ throw new OxygenError("invalid_input", "A workspace file UUID is required.");
17
+ return value;
18
+ }
19
+ export function parseVisualRenderRequest(value) {
20
+ const ids = value.source_file_ids;
21
+ if (!Array.isArray(ids) || ids.length < 1 || ids.length > VISUAL_RENDER_LIMITS.maxPages) {
22
+ throw new OxygenError("invalid_input", "Choose between 1 and 12 HTML source files in page order.");
23
+ }
24
+ const source_file_ids = ids.map(validateVisualFileId);
25
+ const width = value.width ?? 1080, height = value.height ?? 1350, scale = value.scale ?? 1;
26
+ if (typeof width !== "number" || !Number.isInteger(width) || width < 1
27
+ || typeof height !== "number" || !Number.isInteger(height) || height < 1
28
+ || (scale !== 1 && scale !== 2)
29
+ || width * scale > VISUAL_RENDER_LIMITS.maxSide || height * scale > VISUAL_RENDER_LIMITS.maxSide
30
+ || width * height * scale * scale * ids.length > VISUAL_RENDER_LIMITS.maxPixels) {
31
+ throw new OxygenError("visual_render_limit", "Use scale 1 or 2, at most 4096 pixels per output side, and at most 32 million pixels across all pages.");
32
+ }
33
+ const formats = value.formats ?? ["png"];
34
+ if (!Array.isArray(formats) || formats.length < 1 || formats.length > 2
35
+ || formats.some((f) => f !== "png" && f !== "pdf") || new Set(formats).size !== formats.length) {
36
+ throw new OxygenError("invalid_input", "Formats must be png, pdf, or both.");
37
+ }
38
+ return { source_file_ids, width, height, scale, formats: formats };
39
+ }
40
+ /** No default snapshot or tariff: enabling this profile is an explicit deployment operation. */
41
+ export function resolveVisualRenderProfile(env, now = Date.now()) {
42
+ const renderSnapshotId = env.OXYGEN_VISUAL_RENDER_SNAPSHOT_ID?.trim();
43
+ const validatorSnapshotId = env.OXYGEN_VISUAL_VALIDATOR_SNAPSHOT_ID?.trim();
44
+ const rendererDigest = env.OXYGEN_VISUAL_RENDERER_DIGEST?.trim();
45
+ const expiresAt = env.OXYGEN_VISUAL_PROFILE_EXPIRES_AT?.trim();
46
+ const credits = Number(env.OXYGEN_VISUAL_RENDER_CREDITS);
47
+ const maxCostMicros = Number(env.OXYGEN_VISUAL_RENDER_MAX_COST_MICROS);
48
+ if (env.OXYGEN_VISUAL_RENDER_ENABLED !== "1" || !renderSnapshotId || !validatorSnapshotId
49
+ || renderSnapshotId === validatorSnapshotId || !rendererDigest || !/^[a-f0-9]{64}$/.test(rendererDigest)
50
+ || !expiresAt || !Number.isFinite(Date.parse(expiresAt)) || Date.parse(expiresAt) <= now + 180_000
51
+ || !Number.isFinite(credits) || credits <= 0 || credits > 1000
52
+ || !Number.isSafeInteger(maxCostMicros) || maxCostMicros < 50_000 || maxCostMicros > 1_000_000)
53
+ return null;
54
+ return { version: VISUAL_RENDER_PROFILE, renderSnapshotId, validatorSnapshotId, rendererDigest, expiresAt, credits, maxCostMicros };
55
+ }
@@ -0,0 +1,43 @@
1
+ export declare const WORKSPACE_VISUAL_SOURCE_MAX_BYTES = 1500000;
2
+ export declare const WORKSPACE_VISUAL_BINARY_MAX_BYTES: number;
3
+ /** Platform abuse guard shared by every plan, not a storage entitlement. */
4
+ export declare const WORKSPACE_RETAINED_FILES_MAX_BYTES: number;
5
+ export type WorkspaceVisualBinaryMimeType = "image/png" | "application/pdf";
6
+ export declare function assertWorkspaceFileId(id: string): void;
7
+ export declare function normalizeWorkspaceVisualFileName(name: string): string;
8
+ export declare function buildWorkspaceFileObjectKey(input: {
9
+ organizationId: string;
10
+ fileId: string;
11
+ fileName: string;
12
+ }): string;
13
+ export declare function isWorkspaceFileObjectKeyForOrganization(key: string, organizationId: string, fileId?: string): boolean;
14
+ export declare function assertWorkspaceFileObjectKey(input: {
15
+ storageKey: string;
16
+ organizationId: string;
17
+ fileId?: string;
18
+ }): void;
19
+ /** Framing check only. Full media validation belongs in the isolated renderer validator. */
20
+ export declare function assertWorkspaceVisualBinary(bytes: Uint8Array, mimeType: WorkspaceVisualBinaryMimeType): void;
21
+ export declare function workspaceFileSha256(bytes: Uint8Array): string;
22
+ export declare function readWorkspaceFileObject(input: {
23
+ organizationId: string;
24
+ fileId: string;
25
+ storageKey: string;
26
+ sizeBytes: number;
27
+ sha256: string;
28
+ mimeType: WorkspaceVisualBinaryMimeType;
29
+ signal?: AbortSignal;
30
+ }): Promise<Uint8Array>;
31
+ /** Immutable PUT followed by bounded readback. A matching existing object is a safe retry. */
32
+ export declare function putWorkspaceFileObject(input: {
33
+ organizationId: string;
34
+ fileId: string;
35
+ fileName: string;
36
+ bytes: Uint8Array;
37
+ mimeType: WorkspaceVisualBinaryMimeType;
38
+ signal?: AbortSignal;
39
+ }): Promise<{
40
+ storageKey: string;
41
+ sizeBytes: number;
42
+ sha256: string;
43
+ }>;