@oxygen-agent/cli 1.922.14 → 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.
- package/README.md +1 -1
- package/dist/admin-primary-providers-render.d.ts +18 -0
- package/dist/admin-primary-providers-render.js +371 -0
- package/dist/command-manifest.js +6 -0
- package/dist/functions-commands.d.ts +6 -0
- package/dist/functions-commands.js +56 -0
- package/dist/http-client.d.ts +4 -0
- package/dist/http-client.js +49 -2
- package/dist/index.js +175 -40
- package/dist/ugc-commands.d.ts +6 -0
- package/dist/ugc-commands.js +748 -0
- package/dist/visual-commands.d.ts +6 -0
- package/dist/visual-commands.js +57 -0
- package/dist/visual-render-wait.d.ts +3 -0
- package/dist/visual-render-wait.js +56 -0
- package/node_modules/@oxygen/shared/dist/byok-connect.d.ts +43 -0
- package/node_modules/@oxygen/shared/dist/byok-connect.js +84 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +26 -10
- package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/feature-gates.js +3 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/index.js +4 -0
- package/node_modules/@oxygen/shared/dist/knowledge-constants.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/knowledge-constants.js +3 -2
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +1 -1
- package/node_modules/@oxygen/shared/dist/langfuse.js +17 -0
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +5 -0
- package/node_modules/@oxygen/shared/dist/provider-balance-signal.d.ts +113 -0
- package/node_modules/@oxygen/shared/dist/provider-balance-signal.js +158 -0
- package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +12 -0
- package/node_modules/@oxygen/shared/dist/sequence-failures.js +24 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +16 -0
- package/node_modules/@oxygen/shared/dist/sequences.js +54 -0
- package/node_modules/@oxygen/shared/dist/ugc.d.ts +113 -0
- package/node_modules/@oxygen/shared/dist/ugc.js +2 -0
- package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.js +33 -0
- 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/visual-render.d.ts +30 -0
- package/node_modules/@oxygen/shared/dist/visual-render.js +55 -0
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +43 -0
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +126 -0
- package/node_modules/@oxygen/shared/package.json +10 -0
- 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,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.
|
|
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.
|
|
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
|
+
}>;
|