@oxygen-agent/cli 1.686.3 → 1.692.5
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/command-manifest.js +24 -14
- package/dist/index.js +54 -3
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +48 -0
- package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +108 -0
- package/node_modules/@oxygen/shared/dist/feature-gates.js +153 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/index.js +1 -0
- package/node_modules/@oxygen/shared/dist/mailbox-import.d.ts +26 -0
- package/node_modules/@oxygen/shared/dist/mailbox-import.js +36 -0
- package/node_modules/@oxygen/shared/dist/sequence-template.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/sequence-template.js +0 -0
- package/node_modules/@oxygen/shared/dist/sql-error.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/sql-error.js +13 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/command-manifest.js
CHANGED
|
@@ -46,6 +46,13 @@ const MUTATING_VERBS = new Set([
|
|
|
46
46
|
"unbind", "unsubscribe", "update", "upload", "upsert", "use", "warm-send", "warmup",
|
|
47
47
|
"withdraw", "write",
|
|
48
48
|
]);
|
|
49
|
+
// `--approved` usually identifies a paid or externally mutating execution
|
|
50
|
+
// mode, but a small number of destructive local operations are explicitly
|
|
51
|
+
// zero-credit. Keep those exceptions exact so discovery never invents spend.
|
|
52
|
+
const ZERO_CREDIT_APPROVAL_COMMANDS = new Set(["mailboxes delete"]);
|
|
53
|
+
// These commands preview when the execution approval is omitted even though
|
|
54
|
+
// their gate is named `--approved` rather than `--live`.
|
|
55
|
+
const APPROVAL_PREVIEW_COMMANDS = new Set(["mailboxes delete"]);
|
|
49
56
|
export function buildCommandManifest(program, binaryName) {
|
|
50
57
|
const commands = [];
|
|
51
58
|
for (const child of program.commands) {
|
|
@@ -172,8 +179,9 @@ function toManifestEntry(command, path) {
|
|
|
172
179
|
const flagStrings = options.map((option) => option.flags);
|
|
173
180
|
const leafVerb = path[path.length - 1] ?? "";
|
|
174
181
|
const verbPrefix = leafVerb.split("-")[0] ?? leafVerb;
|
|
182
|
+
const commandName = path.join(" ");
|
|
175
183
|
return {
|
|
176
|
-
name:
|
|
184
|
+
name: commandName,
|
|
177
185
|
group: path[0] ?? "",
|
|
178
186
|
description: command.description(),
|
|
179
187
|
arguments: command.registeredArguments.map((argument) => ({
|
|
@@ -186,20 +194,22 @@ function toManifestEntry(command, path) {
|
|
|
186
194
|
description: option.description,
|
|
187
195
|
required: option.mandatory,
|
|
188
196
|
})),
|
|
189
|
-
spends_credits:
|
|
190
|
-
flags.includes("--
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
(
|
|
197
|
+
spends_credits: !ZERO_CREDIT_APPROVAL_COMMANDS.has(commandName) &&
|
|
198
|
+
(flagStrings.some((flags) => flags.includes("--approved") ||
|
|
199
|
+
flags.includes("--max-credits") ||
|
|
200
|
+
// Copilot's explicit session inference-budget approval flag.
|
|
201
|
+
flags.includes("--budget-credits")) ||
|
|
202
|
+
// `copilot send` bills managed inference (5x actual model cost) inside the
|
|
203
|
+
// session budget approved at `copilot start`, and `copilot approve`
|
|
204
|
+
// dispatches the gated paid/external capability and requeues the paused
|
|
205
|
+
// turn (waking the worker for more 5x-billed inference) — no per-command
|
|
206
|
+
// cap flag exists for either, so the flag heuristic alone would misreport
|
|
207
|
+
// them as free.
|
|
208
|
+
(path[0] === "copilot" &&
|
|
209
|
+
(leafVerb === "send" || leafVerb === "approve"))),
|
|
201
210
|
mutates: MUTATING_VERBS.has(leafVerb) || MUTATING_VERBS.has(verbPrefix),
|
|
202
|
-
preview_by_default: flagStrings.some((flags) => flags.includes("--live"))
|
|
211
|
+
preview_by_default: flagStrings.some((flags) => flags.includes("--live")) ||
|
|
212
|
+
APPROVAL_PREVIEW_COMMANDS.has(commandName),
|
|
203
213
|
json_supported: flagStrings.some((flags) => flags.includes("--json")),
|
|
204
214
|
hidden: isHiddenCommand(command),
|
|
205
215
|
};
|
package/dist/index.js
CHANGED
|
@@ -1169,6 +1169,15 @@ function buildCrmPhotosBody(options) {
|
|
|
1169
1169
|
...(limit ? { limit: Number.parseInt(limit, 10) } : {}),
|
|
1170
1170
|
};
|
|
1171
1171
|
}
|
|
1172
|
+
function buildCrmFieldsBody(options) {
|
|
1173
|
+
const object = readOption(options.object);
|
|
1174
|
+
const limit = readOption(options.limit);
|
|
1175
|
+
return {
|
|
1176
|
+
mode: resolveLiveDryRunMode(options),
|
|
1177
|
+
...(object ? { objects: object } : {}),
|
|
1178
|
+
...(limit ? { limit: Number.parseInt(limit, 10) } : {}),
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1172
1181
|
function buildCrmRollupBody(options) {
|
|
1173
1182
|
const object = readOption(options.object);
|
|
1174
1183
|
const row = readOption(options.row);
|
|
@@ -3758,6 +3767,19 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
3758
3767
|
method: "POST",
|
|
3759
3768
|
body: buildCrmPhotosBody(options),
|
|
3760
3769
|
}));
|
|
3770
|
+
}))
|
|
3771
|
+
.addCommand(new Command("fields")
|
|
3772
|
+
.description("Fill the stored CRM fields (Industry, Employees, HQ, and their siblings) from enrichment data this workspace already holds. These are ordinary text/number fields you can edit \u2014 the fill only ever writes an EMPTY cell, so a value you typed is never overwritten. Free: no provider calls, no credits. Run this once on a workspace whose records were enriched before those fields became editable. Defaults to dry-run.")
|
|
3773
|
+
.option("--object <object>", "Limit the fill to one CRM object (companies, people, deals). Defaults to all.")
|
|
3774
|
+
.option("--limit <n>", "Maximum rows to scan per object. Defaults to 2000.")
|
|
3775
|
+
.option("--dry-run", "Preview how many cells would be filled, without writing.")
|
|
3776
|
+
.option("--live", "Write the extracted values onto the records.")
|
|
3777
|
+
.option("--json", "Print a JSON envelope.")
|
|
3778
|
+
.action(async (options) => {
|
|
3779
|
+
await handleAsyncAction("crm fields", options, () => requestOxygen("/api/cli/crm/fields", {
|
|
3780
|
+
method: "POST",
|
|
3781
|
+
body: buildCrmFieldsBody(options),
|
|
3782
|
+
}));
|
|
3761
3783
|
}))
|
|
3762
3784
|
.addCommand(new Command("rollup")
|
|
3763
3785
|
.description("Recompute the Open Deal column on companies (and the copy stamped on their people) from this workspace's deals. It normally converges on every deal write; run this to repair after a bulk import, or on a workspace whose deals predate the column. Free: no provider calls, no credits. Defaults to dry-run.")
|
|
@@ -11789,9 +11811,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
11789
11811
|
});
|
|
11790
11812
|
}))
|
|
11791
11813
|
.addCommand(new Command("start")
|
|
11792
|
-
.description("Preview or start a sequence.
|
|
11814
|
+
.description("Preview or start a sequence. Without --approved, preview scans pending LinkedIn copy and returns exact rendered recipient samples, character counts/limits, copy blockers, per-kind scope, sender/mailbox capacity, CRM readiness, and safety caps. Live start requires --approved; --max-credits is optional. Email/WhatsApp/CRM-task journeys additionally require --max-live-sends because their external actions cost 0 Oxygen credits. Use --dry-run to simulate without any send, provider campaign, CRM task, or credits. The first live start also switches on the standing reply → CRM automation for the workspace.")
|
|
11793
11815
|
.argument("<sequence>", "Sequence id or slug.")
|
|
11794
|
-
.option("--approved", "Approve and activate live. Without this flag, returns a preview only.")
|
|
11816
|
+
.option("--approved", "Approve and activate live. Without this flag, returns a preview only, including copy_preview rendered samples and blockers.")
|
|
11795
11817
|
.option("--max-credits <n>", "Optional credit ceiling for the LinkedIn track (omit for an unbounded run).")
|
|
11796
11818
|
.option("--max-live-sends <n>", "Live external-action ceiling (positive integer). Required for email, WhatsApp, or crm_task steps.")
|
|
11797
11819
|
.option("--dry-run", "Activate in dry-run mode: advance every step with simulated actions, no sends, CRM writes, provider calls, or credits.")
|
|
@@ -12682,6 +12704,35 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
12682
12704
|
.option("--json", "Print a JSON envelope.")
|
|
12683
12705
|
.action(async (mailbox, options) => {
|
|
12684
12706
|
await handleAsyncAction("mailboxes get", options, () => requestOxygen(`/api/cli/mailboxes/${encodeURIComponent(mailbox)}`));
|
|
12707
|
+
}))
|
|
12708
|
+
.addCommand(new Command("delete")
|
|
12709
|
+
.description("Preview or approve removal of exact mailboxes from Oxygen at 0 credits. Deletion immediately removes them from sending but never deletes the underlying Google Workspace or Microsoft 365 accounts. It preserves conversation/message/delivery history and stops the 1,000-credit mailbox commitment for future renewals; the current period is not refunded. Listed active/paused sequences keep their status but lose these senders and are not automatically paused. Managed mailboxes and live warmup/monitoring add-ons fail closed with their exact address-scoped teardown steps.")
|
|
12710
|
+
.requiredOption("--mailboxes <list>", "Comma-separated mailbox ids or addresses (maximum 500).")
|
|
12711
|
+
.option("--approved", "Execute the fresh preview. Requires --plan-hash and --confirmation.")
|
|
12712
|
+
.option("--plan-hash <hash>", "Fresh preview plan_hash.")
|
|
12713
|
+
.option("--confirmation <phrase>", "Exact confirmation_phrase returned by the fresh preview.")
|
|
12714
|
+
.option("--json", "Print a JSON envelope.")
|
|
12715
|
+
.action(async (options) => {
|
|
12716
|
+
await handleAsyncAction("mailboxes delete", options, () => {
|
|
12717
|
+
const mailboxes = readCsvOption(options.mailboxes);
|
|
12718
|
+
if (mailboxes.length === 0) {
|
|
12719
|
+
throw new Error("--mailboxes must contain at least one mailbox id or address.");
|
|
12720
|
+
}
|
|
12721
|
+
const planHash = readOption(options.planHash);
|
|
12722
|
+
const confirmation = readOption(options.confirmation);
|
|
12723
|
+
if (options.approved === true && (!planHash || !confirmation)) {
|
|
12724
|
+
throw new Error("--approved requires --plan-hash and --confirmation from a fresh deletion preview.");
|
|
12725
|
+
}
|
|
12726
|
+
return requestOxygen("/api/cli/mailboxes/delete", {
|
|
12727
|
+
method: "POST",
|
|
12728
|
+
body: {
|
|
12729
|
+
mailboxes,
|
|
12730
|
+
...(options.approved === true ? { approved: true } : {}),
|
|
12731
|
+
...(planHash ? { plan_hash: planHash } : {}),
|
|
12732
|
+
...(confirmation ? { confirmation } : {}),
|
|
12733
|
+
},
|
|
12734
|
+
});
|
|
12735
|
+
});
|
|
12685
12736
|
}))
|
|
12686
12737
|
.addCommand(new Command("health")
|
|
12687
12738
|
.description("Fleet email-health: the sending pool rolled up by external deliverability reputation (healthy/degraded/critical/unknown), per-mailbox scores, connected health providers, and DIRECTIONAL recommendations. Pure read — 0 credits; never pauses a mailbox.")
|
|
@@ -12724,7 +12775,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
12724
12775
|
"Credential file contract:",
|
|
12725
12776
|
' JSON: {"mailboxes":[{"email_address":"ada@send-acme.com","provider":"google","app_password":"<Google mailbox app password>"}]}',
|
|
12726
12777
|
" Only Google app passwords enter the encrypted seven-day transfer vault. Microsoft rows remain identity-only. Generic SMTP passwords and OAuth/MFA/delegation secrets are rejected.",
|
|
12727
|
-
" Validation: add --validate-only to parse the complete real file and return safe aggregate counts without authentication, a network request, or a workspace write. Any parse, shape, provider, platform, tenant, secret-policy, or duplicate-conflict error rejects the entire file before the first mailbox write and names mailboxes[index]; validation-only never writes. A later import infrastructure failure may interrupt the upsert; re-run the same file because import is idempotent by address.",
|
|
12778
|
+
" Validation: add --validate-only to parse the complete real file and return safe aggregate counts plus the provider-specific exact-account OAuth review plan without authentication, a network request, or a workspace write. The plan groups only supplied addresses into reviews of at most 10; it never discovers a domain, and the later online preview may skip existing grants. Any parse, shape, provider, platform, tenant, secret-policy, or duplicate-conflict error rejects the entire file before the first mailbox write and names mailboxes[index]; validation-only never writes. A later import infrastructure failure may interrupt the upsert; re-run the same file because import is idempotent by address.",
|
|
12728
12779
|
" Docs: https://oxygen-agent.com/docs/providers/mailbox-compatibility",
|
|
12729
12780
|
" Skill: oxygen-email-infra (`oxygen skills install --skill oxygen-email-infra`).",
|
|
12730
12781
|
"",
|
|
@@ -512,6 +512,12 @@ function normalizeIntent(query) {
|
|
|
512
512
|
return query.toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
|
|
513
513
|
}
|
|
514
514
|
function explicitCapabilityIntent(query) {
|
|
515
|
+
if (isMailboxDeleteIntent(query)) {
|
|
516
|
+
return ROUTE_BY_ID.get("sending-infrastructure") ?? null;
|
|
517
|
+
}
|
|
518
|
+
if (isMailboxOnboardingIntent(query)) {
|
|
519
|
+
return ROUTE_BY_ID.get("sending-infrastructure") ?? null;
|
|
520
|
+
}
|
|
515
521
|
if (isHostedWorkflowIntent(query))
|
|
516
522
|
return ROUTE_BY_PRIMITIVE.get("workflows") ?? null;
|
|
517
523
|
if (isOwnedPostCommentIntent(query))
|
|
@@ -603,6 +609,36 @@ function highestScoringRoute(query) {
|
|
|
603
609
|
return best?.card ?? null;
|
|
604
610
|
}
|
|
605
611
|
function recommendationsFor(card, query) {
|
|
612
|
+
if (card.id === "sending-infrastructure" && isMailboxDeleteIntent(query)) {
|
|
613
|
+
return {
|
|
614
|
+
tools: [
|
|
615
|
+
"oxygen_mailboxes_delete",
|
|
616
|
+
"oxygen_mailboxes_compatibility",
|
|
617
|
+
"oxygen_mailboxes_list",
|
|
618
|
+
],
|
|
619
|
+
commands: [
|
|
620
|
+
"mailboxes delete",
|
|
621
|
+
"mailboxes compatibility",
|
|
622
|
+
"mailboxes list",
|
|
623
|
+
],
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
if (card.id === "sending-infrastructure" && isMailboxOnboardingIntent(query)) {
|
|
627
|
+
return {
|
|
628
|
+
tools: [
|
|
629
|
+
"oxygen_mailboxes_compatibility",
|
|
630
|
+
"oxygen_mailboxes_import",
|
|
631
|
+
"oxygen_mailboxes_connect_oauth",
|
|
632
|
+
"oxygen_mailboxes_oauth_health",
|
|
633
|
+
],
|
|
634
|
+
commands: [
|
|
635
|
+
"mailboxes compatibility",
|
|
636
|
+
"mailboxes import",
|
|
637
|
+
"mailboxes connect-oauth",
|
|
638
|
+
"mailboxes oauth-health",
|
|
639
|
+
],
|
|
640
|
+
};
|
|
641
|
+
}
|
|
606
642
|
if (card.primitive === "posts" && isOwnedPostCommentIntent(query)) {
|
|
607
643
|
return {
|
|
608
644
|
tools: ["oxygen_publishing_comments", "oxygen_publishing_analytics"],
|
|
@@ -782,6 +818,18 @@ function recommendationsFor(card, query) {
|
|
|
782
818
|
}
|
|
783
819
|
return { tools: [...card.gatewayTools], commands: [...card.gatewayCommands] };
|
|
784
820
|
}
|
|
821
|
+
function isMailboxOnboardingIntent(query) {
|
|
822
|
+
const operation = /\b(import|migrat\w*|onboard|register|upload|bring|connect existing|add existing)\b/.test(query);
|
|
823
|
+
const mailboxScope = /\b(mailbox(?:es)?|inbox(?:es)?|sender accounts?|email accounts?|sending pool)\b/.test(query);
|
|
824
|
+
return operation && mailboxScope;
|
|
825
|
+
}
|
|
826
|
+
function isMailboxDeleteIntent(query) {
|
|
827
|
+
const mailboxScope = /\b(mailbox(?:es)?|inbox(?:es)?|sender accounts?|email accounts?)\b/.test(query);
|
|
828
|
+
const removal = /\b(delete|remove)\b/.test(query)
|
|
829
|
+
|| /\bdisconnect\b.{0,24}\b(mailbox(?:es)?|inbox(?:es)?|sender accounts?|email accounts?)\b/.test(query);
|
|
830
|
+
const scopedDetach = /\b(remove|disconnect)\b.{0,48}\b(mailbox(?:es)?|inbox(?:es)?|sender accounts?|email accounts?)\b.{0,24}\b(?:from|in|on)\b.{0,24}\b(sequence|campaign|cadence|sender profile)\b/.test(query);
|
|
831
|
+
return mailboxScope && removal && !scopedDetach;
|
|
832
|
+
}
|
|
785
833
|
function isNetNewLinkedInInitiation(query) {
|
|
786
834
|
const mentionsLinkedIn = /\blinkedin\b|\bdm\b/.test(query);
|
|
787
835
|
const startsConversation = /\b(send|message|dm|contact|reach out|initiate|start)\b/.test(query)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
type FeatureGateEnv = Record<string, string | undefined>;
|
|
2
|
+
/** Where the effective value came from. Precedence is env → remote → default. */
|
|
3
|
+
export type FeatureGateSource = "env_override" | "remote" | "safe_default";
|
|
4
|
+
export type FeatureGateDefinition = {
|
|
5
|
+
/** PostHog feature-flag key. Lowercase kebab-case by convention. */
|
|
6
|
+
key: string;
|
|
7
|
+
/**
|
|
8
|
+
* The value used whenever the provider has no answer. Pick the value that is
|
|
9
|
+
* safe to serve indefinitely, not the value you expect: this is what ships
|
|
10
|
+
* during a vendor outage, and on Vercel it is also what every cold
|
|
11
|
+
* invocation serves until flag definitions finish loading.
|
|
12
|
+
*/
|
|
13
|
+
safeDefault: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Optional break-glass env var. Set it to force the gate on/off without
|
|
16
|
+
* touching the flag service — the only lever that still works when the flag
|
|
17
|
+
* service is the thing that is broken. Must not name a NEVER_FLAGGABLE key.
|
|
18
|
+
*/
|
|
19
|
+
envOverride?: string;
|
|
20
|
+
};
|
|
21
|
+
export type FeatureGateResolution = {
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
source: FeatureGateSource;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Env gates that must NEVER become remotely-flippable feature flags, and stay
|
|
27
|
+
* read directly from `process.env` (Doppler) at their call sites.
|
|
28
|
+
*
|
|
29
|
+
* Two disqualifying properties, each fatal on its own:
|
|
30
|
+
*
|
|
31
|
+
* (a) CIRCULARITY — a kill switch for a telemetry vendor cannot be resolved
|
|
32
|
+
* through a telemetry vendor. You reach for it precisely when that path is
|
|
33
|
+
* misbehaving, so a switch that needs the vendor reachable to be read is
|
|
34
|
+
* not a kill switch at all.
|
|
35
|
+
* (b) AUTHORIZATION — spend enforcement and external-write enablement. A
|
|
36
|
+
* remotely-flippable value means a vendor outage, a mis-click in someone
|
|
37
|
+
* else's UI, or a compromised vendor account can uncap credit spend or
|
|
38
|
+
* enable public side effects. Hard rules keep those in Doppler, where the
|
|
39
|
+
* change is an auditable secret change.
|
|
40
|
+
*
|
|
41
|
+
* Per-key rationale (call sites read 2026-08, verified against this base):
|
|
42
|
+
*
|
|
43
|
+
* - OXYGEN_TELEMETRY_ENABLED — (a). The master telemetry kill switch, read by
|
|
44
|
+
* `apps/web/src/lib/axiom-log-shipper.ts` and `apps/worker/src/telemetry.ts` /
|
|
45
|
+
* `log-shipper.ts`. Resolving it remotely makes turning telemetry OFF depend
|
|
46
|
+
* on a telemetry vendor being up.
|
|
47
|
+
* - OXYGEN_LOG_SHIPPING_ENABLED — (a). Same circularity, narrower blast radius
|
|
48
|
+
* (log shipping only; `axiom-log-shipper.ts`, `apps/worker/src/log-shipper.ts`).
|
|
49
|
+
* - OXYGEN_LLM_TRACING_ENABLED — (a) + (b). `packages/shared/src/langfuse.ts`
|
|
50
|
+
* fails CLOSED on it because it authorizes egress of full prompt/completion
|
|
51
|
+
* payloads to Langfuse (ADR 0014). Where prompt payloads may travel is a data
|
|
52
|
+
* boundary, not a rollout. It also lives in this transport-free package, so
|
|
53
|
+
* remote resolution would drag a flag client into the published CLI.
|
|
54
|
+
* - OXYGEN_TRIGGER_DEFAULT_CAPS — (b). Selects log-only vs enforce for the
|
|
55
|
+
* plan-tier default per-delivery ceiling on autonomous triggers
|
|
56
|
+
* (`apps/worker/src/workflow-spend-cap.ts`). Remote control over spend
|
|
57
|
+
* ENFORCEMENT means an outage can uncap autonomous credit spend everywhere.
|
|
58
|
+
* - OXYGEN_BYOK_DAILY_CAPS — (b). Same, for BYOK per-provider daily caps
|
|
59
|
+
* (`packages/providers/src/provider-fetch.ts`) — external money on the
|
|
60
|
+
* customer's own keys, and likewise in a package the CLI can reach.
|
|
61
|
+
* - OXYGEN_PUBLISHING_ENABLED — (b). Fail-closed production switch for posting
|
|
62
|
+
* to real provider accounts (`apps/web/src/app/(app)/(dashboard)/publishing/
|
|
63
|
+
* data.ts`). Wrong "on" = public posts from customer accounts.
|
|
64
|
+
* - OXYGEN_AGENTS_ENABLED — (b). Authorizes autonomous Agent run execution —
|
|
65
|
+
* tool authority AND credit spend (`apps/web/src/lib/agent-runtime.ts`,
|
|
66
|
+
* `apps/worker/src/agent-cycle.ts`). Same class as publishing.
|
|
67
|
+
*
|
|
68
|
+
* The list is a guard, not a to-do: none of these is migrated, and
|
|
69
|
+
* `findFeatureGateRegistryViolations` fails the build's tests if one ever is.
|
|
70
|
+
*/
|
|
71
|
+
export declare const NEVER_FLAGGABLE: readonly string[];
|
|
72
|
+
/**
|
|
73
|
+
* Every server-side gate resolved through PostHog.
|
|
74
|
+
*
|
|
75
|
+
* Intentionally empty: introducing the mechanism is not a reason to move
|
|
76
|
+
* existing switches onto it. The seven env gates above stay in Doppler, and a
|
|
77
|
+
* new entry belongs here only when its worst wrong value is a cosmetic or
|
|
78
|
+
* reversible product difference — never spend, external writes, or telemetry
|
|
79
|
+
* egress.
|
|
80
|
+
*/
|
|
81
|
+
export declare const FEATURE_GATES: readonly FeatureGateDefinition[];
|
|
82
|
+
/**
|
|
83
|
+
* Tri-state env parse: `true` / `false` / `null` for "unset or unparseable".
|
|
84
|
+
*
|
|
85
|
+
* The null case is load-bearing — an override that is merely absent has to fall
|
|
86
|
+
* through to the remote value, while an override explicitly set to `false` has
|
|
87
|
+
* to win over it. Collapsing the two into a boolean would make every gate
|
|
88
|
+
* without an override permanently forced off.
|
|
89
|
+
*/
|
|
90
|
+
export declare function parseFeatureGateBoolean(value: string | undefined): boolean | null;
|
|
91
|
+
/**
|
|
92
|
+
* Resolve one gate. Pure: the caller supplies the provider's answer (or
|
|
93
|
+
* `undefined` when it had none) and the env to read.
|
|
94
|
+
*
|
|
95
|
+
* Env override wins over the remote value on purpose. It is the break-glass for
|
|
96
|
+
* "the flag service is serving the wrong answer", which is worthless if the
|
|
97
|
+
* flag service outranks it.
|
|
98
|
+
*/
|
|
99
|
+
export declare function resolveFeatureGate(definition: FeatureGateDefinition, remote?: boolean | undefined, env?: FeatureGateEnv): FeatureGateResolution;
|
|
100
|
+
export declare function isNeverFlaggable(name: string): boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Registry lint, asserted by tests rather than thrown at import time (a gate
|
|
103
|
+
* registry must never be able to crash a boot). Returns one human-readable
|
|
104
|
+
* violation per problem; an empty array means the registry is clean.
|
|
105
|
+
*/
|
|
106
|
+
export declare function findFeatureGateRegistryViolations(gates?: readonly FeatureGateDefinition[]): string[];
|
|
107
|
+
export declare function findFeatureGate(key: string): FeatureGateDefinition | undefined;
|
|
108
|
+
export {};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Server-side feature gates (ADR 0021: PostHog owns server-side feature flags).
|
|
2
|
+
//
|
|
3
|
+
// This module is the CONTRACT ONLY — gate definitions, the unflaggable list, and
|
|
4
|
+
// the pure resolver. It is deliberately TRANSPORT-FREE: no `posthog-node`, no
|
|
5
|
+
// fetch, no client, no I/O of any kind. `packages/cli` is published to npm as
|
|
6
|
+
// `@oxygen-agent/cli` and depends on `@oxygen/shared` unbundled, so every import
|
|
7
|
+
// added here becomes a runtime dependency of every customer's CLI install. The
|
|
8
|
+
// evaluation transport lives in `apps/web/src/lib/feature-gate-provider.ts`,
|
|
9
|
+
// which nothing outside the web app imports.
|
|
10
|
+
//
|
|
11
|
+
// The resolver's one invariant: an absent provider answer is not an answer. A
|
|
12
|
+
// flag-service outage, a quota-limited project, a cold lambda whose definitions
|
|
13
|
+
// have not loaded yet, and a flag that simply does not exist all arrive here as
|
|
14
|
+
// `undefined`, and all of them resolve to the gate's `safeDefault`. A gate never
|
|
15
|
+
// flips because a vendor is unreachable.
|
|
16
|
+
/**
|
|
17
|
+
* Env gates that must NEVER become remotely-flippable feature flags, and stay
|
|
18
|
+
* read directly from `process.env` (Doppler) at their call sites.
|
|
19
|
+
*
|
|
20
|
+
* Two disqualifying properties, each fatal on its own:
|
|
21
|
+
*
|
|
22
|
+
* (a) CIRCULARITY — a kill switch for a telemetry vendor cannot be resolved
|
|
23
|
+
* through a telemetry vendor. You reach for it precisely when that path is
|
|
24
|
+
* misbehaving, so a switch that needs the vendor reachable to be read is
|
|
25
|
+
* not a kill switch at all.
|
|
26
|
+
* (b) AUTHORIZATION — spend enforcement and external-write enablement. A
|
|
27
|
+
* remotely-flippable value means a vendor outage, a mis-click in someone
|
|
28
|
+
* else's UI, or a compromised vendor account can uncap credit spend or
|
|
29
|
+
* enable public side effects. Hard rules keep those in Doppler, where the
|
|
30
|
+
* change is an auditable secret change.
|
|
31
|
+
*
|
|
32
|
+
* Per-key rationale (call sites read 2026-08, verified against this base):
|
|
33
|
+
*
|
|
34
|
+
* - OXYGEN_TELEMETRY_ENABLED — (a). The master telemetry kill switch, read by
|
|
35
|
+
* `apps/web/src/lib/axiom-log-shipper.ts` and `apps/worker/src/telemetry.ts` /
|
|
36
|
+
* `log-shipper.ts`. Resolving it remotely makes turning telemetry OFF depend
|
|
37
|
+
* on a telemetry vendor being up.
|
|
38
|
+
* - OXYGEN_LOG_SHIPPING_ENABLED — (a). Same circularity, narrower blast radius
|
|
39
|
+
* (log shipping only; `axiom-log-shipper.ts`, `apps/worker/src/log-shipper.ts`).
|
|
40
|
+
* - OXYGEN_LLM_TRACING_ENABLED — (a) + (b). `packages/shared/src/langfuse.ts`
|
|
41
|
+
* fails CLOSED on it because it authorizes egress of full prompt/completion
|
|
42
|
+
* payloads to Langfuse (ADR 0014). Where prompt payloads may travel is a data
|
|
43
|
+
* boundary, not a rollout. It also lives in this transport-free package, so
|
|
44
|
+
* remote resolution would drag a flag client into the published CLI.
|
|
45
|
+
* - OXYGEN_TRIGGER_DEFAULT_CAPS — (b). Selects log-only vs enforce for the
|
|
46
|
+
* plan-tier default per-delivery ceiling on autonomous triggers
|
|
47
|
+
* (`apps/worker/src/workflow-spend-cap.ts`). Remote control over spend
|
|
48
|
+
* ENFORCEMENT means an outage can uncap autonomous credit spend everywhere.
|
|
49
|
+
* - OXYGEN_BYOK_DAILY_CAPS — (b). Same, for BYOK per-provider daily caps
|
|
50
|
+
* (`packages/providers/src/provider-fetch.ts`) — external money on the
|
|
51
|
+
* customer's own keys, and likewise in a package the CLI can reach.
|
|
52
|
+
* - OXYGEN_PUBLISHING_ENABLED — (b). Fail-closed production switch for posting
|
|
53
|
+
* to real provider accounts (`apps/web/src/app/(app)/(dashboard)/publishing/
|
|
54
|
+
* data.ts`). Wrong "on" = public posts from customer accounts.
|
|
55
|
+
* - OXYGEN_AGENTS_ENABLED — (b). Authorizes autonomous Agent run execution —
|
|
56
|
+
* tool authority AND credit spend (`apps/web/src/lib/agent-runtime.ts`,
|
|
57
|
+
* `apps/worker/src/agent-cycle.ts`). Same class as publishing.
|
|
58
|
+
*
|
|
59
|
+
* The list is a guard, not a to-do: none of these is migrated, and
|
|
60
|
+
* `findFeatureGateRegistryViolations` fails the build's tests if one ever is.
|
|
61
|
+
*/
|
|
62
|
+
export const NEVER_FLAGGABLE = [
|
|
63
|
+
"OXYGEN_PUBLISHING_ENABLED",
|
|
64
|
+
"OXYGEN_AGENTS_ENABLED",
|
|
65
|
+
"OXYGEN_TELEMETRY_ENABLED",
|
|
66
|
+
"OXYGEN_LOG_SHIPPING_ENABLED",
|
|
67
|
+
"OXYGEN_LLM_TRACING_ENABLED",
|
|
68
|
+
"OXYGEN_TRIGGER_DEFAULT_CAPS",
|
|
69
|
+
"OXYGEN_BYOK_DAILY_CAPS",
|
|
70
|
+
];
|
|
71
|
+
/**
|
|
72
|
+
* Every server-side gate resolved through PostHog.
|
|
73
|
+
*
|
|
74
|
+
* Intentionally empty: introducing the mechanism is not a reason to move
|
|
75
|
+
* existing switches onto it. The seven env gates above stay in Doppler, and a
|
|
76
|
+
* new entry belongs here only when its worst wrong value is a cosmetic or
|
|
77
|
+
* reversible product difference — never spend, external writes, or telemetry
|
|
78
|
+
* egress.
|
|
79
|
+
*/
|
|
80
|
+
export const FEATURE_GATES = [];
|
|
81
|
+
/**
|
|
82
|
+
* Tri-state env parse: `true` / `false` / `null` for "unset or unparseable".
|
|
83
|
+
*
|
|
84
|
+
* The null case is load-bearing — an override that is merely absent has to fall
|
|
85
|
+
* through to the remote value, while an override explicitly set to `false` has
|
|
86
|
+
* to win over it. Collapsing the two into a boolean would make every gate
|
|
87
|
+
* without an override permanently forced off.
|
|
88
|
+
*/
|
|
89
|
+
export function parseFeatureGateBoolean(value) {
|
|
90
|
+
const normalized = value?.trim().toLowerCase();
|
|
91
|
+
if (!normalized)
|
|
92
|
+
return null;
|
|
93
|
+
if (normalized === "true" || normalized === "1" || normalized === "yes")
|
|
94
|
+
return true;
|
|
95
|
+
if (normalized === "false" || normalized === "0" || normalized === "no")
|
|
96
|
+
return false;
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Resolve one gate. Pure: the caller supplies the provider's answer (or
|
|
101
|
+
* `undefined` when it had none) and the env to read.
|
|
102
|
+
*
|
|
103
|
+
* Env override wins over the remote value on purpose. It is the break-glass for
|
|
104
|
+
* "the flag service is serving the wrong answer", which is worthless if the
|
|
105
|
+
* flag service outranks it.
|
|
106
|
+
*/
|
|
107
|
+
export function resolveFeatureGate(definition, remote, env = process.env) {
|
|
108
|
+
const override = definition.envOverride
|
|
109
|
+
? parseFeatureGateBoolean(env[definition.envOverride])
|
|
110
|
+
: null;
|
|
111
|
+
if (override !== null)
|
|
112
|
+
return { enabled: override, source: "env_override" };
|
|
113
|
+
if (typeof remote === "boolean")
|
|
114
|
+
return { enabled: remote, source: "remote" };
|
|
115
|
+
return { enabled: definition.safeDefault, source: "safe_default" };
|
|
116
|
+
}
|
|
117
|
+
// Compare env-var names and flag keys in one space, so `oxygen-agents-enabled`
|
|
118
|
+
// as a flag key is caught by a list written in SCREAMING_SNAKE env style.
|
|
119
|
+
function normalizeGateName(name) {
|
|
120
|
+
return name.trim().toUpperCase().replace(/-/g, "_");
|
|
121
|
+
}
|
|
122
|
+
export function isNeverFlaggable(name) {
|
|
123
|
+
const normalized = normalizeGateName(name);
|
|
124
|
+
return NEVER_FLAGGABLE.some((entry) => normalizeGateName(entry) === normalized);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Registry lint, asserted by tests rather than thrown at import time (a gate
|
|
128
|
+
* registry must never be able to crash a boot). Returns one human-readable
|
|
129
|
+
* violation per problem; an empty array means the registry is clean.
|
|
130
|
+
*/
|
|
131
|
+
export function findFeatureGateRegistryViolations(gates = FEATURE_GATES) {
|
|
132
|
+
const violations = [];
|
|
133
|
+
const seen = new Set();
|
|
134
|
+
for (const gate of gates) {
|
|
135
|
+
if (!gate.key.trim()) {
|
|
136
|
+
violations.push("A feature gate has an empty key.");
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (seen.has(gate.key))
|
|
140
|
+
violations.push(`Duplicate feature gate key: ${gate.key}`);
|
|
141
|
+
seen.add(gate.key);
|
|
142
|
+
if (isNeverFlaggable(gate.key)) {
|
|
143
|
+
violations.push(`Feature gate "${gate.key}" names a NEVER_FLAGGABLE switch — it must stay an env gate.`);
|
|
144
|
+
}
|
|
145
|
+
if (gate.envOverride && isNeverFlaggable(gate.envOverride)) {
|
|
146
|
+
violations.push(`Feature gate "${gate.key}" uses NEVER_FLAGGABLE env var "${gate.envOverride}" as its override.`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return violations;
|
|
150
|
+
}
|
|
151
|
+
export function findFeatureGate(key) {
|
|
152
|
+
return FEATURE_GATES.find((gate) => gate.key === key);
|
|
153
|
+
}
|
|
@@ -22,6 +22,7 @@ export * from "./directory.js";
|
|
|
22
22
|
export * from "./email-tracking-token.js";
|
|
23
23
|
export * from "./email-unsubscribe-token.js";
|
|
24
24
|
export * from "./error-redaction.js";
|
|
25
|
+
export * from "./feature-gates.js";
|
|
25
26
|
export * from "./hosted-ai.js";
|
|
26
27
|
export * from "./identifiers.js";
|
|
27
28
|
export * from "./knowledge-constants.js";
|
|
@@ -22,6 +22,7 @@ export * from "./directory.js";
|
|
|
22
22
|
export * from "./email-tracking-token.js";
|
|
23
23
|
export * from "./email-unsubscribe-token.js";
|
|
24
24
|
export * from "./error-redaction.js";
|
|
25
|
+
export * from "./feature-gates.js";
|
|
25
26
|
export * from "./hosted-ai.js";
|
|
26
27
|
export * from "./identifiers.js";
|
|
27
28
|
export * from "./knowledge-constants.js";
|
|
@@ -21,6 +21,7 @@ export type MailboxImportValidationSummary = {
|
|
|
21
21
|
infrastructure_platforms: Record<string, number>;
|
|
22
22
|
credential_rows: number;
|
|
23
23
|
identity_only_rows: number;
|
|
24
|
+
oauth_review_plan: MailboxOAuthReviewPlan;
|
|
24
25
|
limits: {
|
|
25
26
|
max_rows: number;
|
|
26
27
|
max_file_bytes: number;
|
|
@@ -31,8 +32,26 @@ export type MailboxImportValidationSummary = {
|
|
|
31
32
|
credits_used: 0;
|
|
32
33
|
next_action: string;
|
|
33
34
|
};
|
|
35
|
+
export type MailboxOAuthProviderReviewPlan = {
|
|
36
|
+
mailboxes: number;
|
|
37
|
+
batches: number;
|
|
38
|
+
batch_sizes: number[];
|
|
39
|
+
};
|
|
40
|
+
export type MailboxOAuthReviewPlan = {
|
|
41
|
+
authorization: "exact_account_oauth";
|
|
42
|
+
scope: "validated_addresses";
|
|
43
|
+
directory_discovery: false;
|
|
44
|
+
existing_grants_checked: false;
|
|
45
|
+
batch_size: number;
|
|
46
|
+
total_batches: number;
|
|
47
|
+
providers: {
|
|
48
|
+
google: MailboxOAuthProviderReviewPlan;
|
|
49
|
+
microsoft: MailboxOAuthProviderReviewPlan;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
34
52
|
export declare const MAILBOX_IMPORT_FILE_MAX_BYTES: number;
|
|
35
53
|
export declare const MAILBOX_IMPORT_ROW_LIMIT = 500;
|
|
54
|
+
export declare const MAILBOX_OAUTH_REVIEW_BATCH_SIZE = 10;
|
|
36
55
|
/**
|
|
37
56
|
* Infer the bounded mailbox-import format without node:path so browser and CLI
|
|
38
57
|
* preflight use the same extension contract.
|
|
@@ -55,4 +74,11 @@ export declare function summarizeMailboxImportValidation(mailboxes: readonly Nor
|
|
|
55
74
|
source: MailboxImportValidationSource;
|
|
56
75
|
sourceProvider: string | null;
|
|
57
76
|
}): MailboxImportValidationSummary;
|
|
77
|
+
/**
|
|
78
|
+
* Build the deterministic, provider-specific review queue for exact-account
|
|
79
|
+
* OAuth. This is safe during local file validation: it uses only normalized
|
|
80
|
+
* provider counts, performs no directory discovery, and does not claim to know
|
|
81
|
+
* whether a workspace already holds a usable grant for an address.
|
|
82
|
+
*/
|
|
83
|
+
export declare function planMailboxOAuthReviews(mailboxes: readonly Pick<NormalizedMailboxImportRow, "provider">[]): MailboxOAuthReviewPlan;
|
|
58
84
|
export declare function normalizeMailboxImportVendor(raw: string | null | undefined, from: string | null | undefined): string | null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { OxygenError } from "./cli-result.js";
|
|
2
2
|
export const MAILBOX_IMPORT_FILE_MAX_BYTES = 5 * 1024 * 1024;
|
|
3
3
|
export const MAILBOX_IMPORT_ROW_LIMIT = 500;
|
|
4
|
+
export const MAILBOX_OAUTH_REVIEW_BATCH_SIZE = 10;
|
|
4
5
|
const MAILBOX_EMAIL_HEADERS = new Set([
|
|
5
6
|
"address",
|
|
6
7
|
"email",
|
|
@@ -194,6 +195,7 @@ export function summarizeMailboxImportValidation(mailboxes, input) {
|
|
|
194
195
|
infrastructure_platforms: infrastructurePlatforms,
|
|
195
196
|
credential_rows: credentialRows,
|
|
196
197
|
identity_only_rows: mailboxes.length - credentialRows,
|
|
198
|
+
oauth_review_plan: planMailboxOAuthReviews(mailboxes),
|
|
197
199
|
limits: {
|
|
198
200
|
max_rows: MAILBOX_IMPORT_ROW_LIMIT,
|
|
199
201
|
max_file_bytes: MAILBOX_IMPORT_FILE_MAX_BYTES,
|
|
@@ -205,6 +207,40 @@ export function summarizeMailboxImportValidation(mailboxes, input) {
|
|
|
205
207
|
next_action: "Submit this reviewed file to register the mailboxes.",
|
|
206
208
|
};
|
|
207
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Build the deterministic, provider-specific review queue for exact-account
|
|
212
|
+
* OAuth. This is safe during local file validation: it uses only normalized
|
|
213
|
+
* provider counts, performs no directory discovery, and does not claim to know
|
|
214
|
+
* whether a workspace already holds a usable grant for an address.
|
|
215
|
+
*/
|
|
216
|
+
export function planMailboxOAuthReviews(mailboxes) {
|
|
217
|
+
const counts = { google: 0, microsoft: 0 };
|
|
218
|
+
for (const mailbox of mailboxes)
|
|
219
|
+
counts[mailbox.provider] += 1;
|
|
220
|
+
const google = planProviderOAuthReviews(counts.google);
|
|
221
|
+
const microsoft = planProviderOAuthReviews(counts.microsoft);
|
|
222
|
+
return {
|
|
223
|
+
authorization: "exact_account_oauth",
|
|
224
|
+
scope: "validated_addresses",
|
|
225
|
+
directory_discovery: false,
|
|
226
|
+
existing_grants_checked: false,
|
|
227
|
+
batch_size: MAILBOX_OAUTH_REVIEW_BATCH_SIZE,
|
|
228
|
+
total_batches: google.batches + microsoft.batches,
|
|
229
|
+
providers: { google, microsoft },
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function planProviderOAuthReviews(mailboxes) {
|
|
233
|
+
const fullBatches = Math.floor(mailboxes / MAILBOX_OAUTH_REVIEW_BATCH_SIZE);
|
|
234
|
+
const remainder = mailboxes % MAILBOX_OAUTH_REVIEW_BATCH_SIZE;
|
|
235
|
+
const batchSizes = Array.from({ length: fullBatches }, () => MAILBOX_OAUTH_REVIEW_BATCH_SIZE);
|
|
236
|
+
if (remainder > 0)
|
|
237
|
+
batchSizes.push(remainder);
|
|
238
|
+
return {
|
|
239
|
+
mailboxes,
|
|
240
|
+
batches: batchSizes.length,
|
|
241
|
+
batch_sizes: batchSizes,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
208
244
|
export function normalizeMailboxImportVendor(raw, from) {
|
|
209
245
|
if (from === "hypertide") {
|
|
210
246
|
if (raw && raw.trim().toLowerCase() !== "hypertide") {
|
|
@@ -66,6 +66,12 @@ export declare const MAX_SPINTAX_DEPTH = 10;
|
|
|
66
66
|
* `{{column}}` placeholders render empty; a malformed placeholder is left literal.
|
|
67
67
|
*/
|
|
68
68
|
export declare function renderTemplate(template: string, values: Record<string, unknown>, options?: RenderTemplateOptions): string;
|
|
69
|
+
/**
|
|
70
|
+
* Bare variables that are blank in the branch this exact render selected.
|
|
71
|
+
* Untaken conditional/spintax branches are deliberately ignored; fallback-bearing
|
|
72
|
+
* tokens are optional, while bare tokens selected inside a fallback are checked.
|
|
73
|
+
*/
|
|
74
|
+
export declare function renderedTemplateMissingVariables(template: string, values: Record<string, unknown>, options?: RenderTemplateOptions): string[];
|
|
69
75
|
/**
|
|
70
76
|
* Every column key a template references — across bare `{{column}}` substitutions,
|
|
71
77
|
* `{{column|fallback}}` fallbacks, `{{RANDOM|…}}` spintax options, and both the
|
|
Binary file
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Normalized failure category, derived from SQLSTATE first, then errno/message. */
|
|
2
|
-
export type SqlErrorCause = "connect_timeout" | "connection" | "statement_timeout" | "admin_shutdown" | "too_many_connections" | "insufficient_resources" | "auth" | "schema_drift" | "data_exception" | "integrity_constraint" | "serialization" | "deadlock" | "transaction_rollback" | "syntax_or_access" | "internal" | "unknown";
|
|
2
|
+
export type SqlErrorCause = "connect_timeout" | "connection" | "statement_timeout" | "admin_shutdown" | "too_many_connections" | "insufficient_resources" | "auth" | "schema_drift" | "data_exception" | "integrity_constraint" | "serialization" | "deadlock" | "transaction_rollback" | "read_only" | "syntax_or_access" | "internal" | "unknown";
|
|
3
3
|
export type SqlErrorAttribution = {
|
|
4
4
|
/** Postgres SQLSTATE (5 chars) when present; null for connection-level errnos. */
|
|
5
5
|
pgCode: string | null;
|
|
@@ -62,6 +62,12 @@ const TRANSIENT_CAUSES = new Set([
|
|
|
62
62
|
"serialization",
|
|
63
63
|
"deadlock",
|
|
64
64
|
"transaction_rollback",
|
|
65
|
+
// A Neon compute waking from autostop (or briefly in hot-standby/recovery)
|
|
66
|
+
// rejects writes with 25006 read_only_sql_transaction until the primary is
|
|
67
|
+
// writable again; the next poll tick succeeds. The dev worker's control-queue
|
|
68
|
+
// drain produced ~hourly 25006 bursts that self-healed within one wake cycle
|
|
69
|
+
// (OXY-5984).
|
|
70
|
+
"read_only",
|
|
65
71
|
]);
|
|
66
72
|
/**
|
|
67
73
|
* Classifies a pg/drizzle error into structured, non-secret attribution.
|
|
@@ -215,6 +221,13 @@ const EXACT_SQLSTATE_CAUSES = new Map([
|
|
|
215
221
|
["53300", "too_many_connections"],
|
|
216
222
|
["40001", "serialization"],
|
|
217
223
|
["40P01", "deadlock"],
|
|
224
|
+
// 25006 read_only_sql_transaction: a write hit a compute that is (still)
|
|
225
|
+
// read-only — a Neon dev branch mid-autostart or in hot-standby/recovery.
|
|
226
|
+
// Class 25 has no blanket mapping on purpose: its other members (25P02
|
|
227
|
+
// in_failed_sql_transaction, 25001 active_sql_transaction, ...) are
|
|
228
|
+
// transaction-protocol misuse, not a transient infra state. Only 25006 is
|
|
229
|
+
// the "retry on the next tick" case (OXY-5984).
|
|
230
|
+
["25006", "read_only"],
|
|
218
231
|
]);
|
|
219
232
|
// SQLSTATE class (first two chars) → cause, applied only after the exact and
|
|
220
233
|
// schema-drift lookups above so the precise codes keep their dedicated cause.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const OXYGEN_VERSION = "1.
|
|
1
|
+
export const OXYGEN_VERSION = "1.692.5";
|
|
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:
|