@oxy-hq/cli 0.9.0 → 0.10.0

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 CHANGED
@@ -7,7 +7,7 @@ manages customer workspace repos.
7
7
  - **Customer workspaces** — `list`, `new`, `import`, `doctor`, `update`, `adopt`, `launch`
8
8
  - **Custom apps** — `publish`, `init-ci`, `proxy`, `apps`
9
9
  - **Checks** — `checks run`
10
- - **Sandboxes** — `env`, `fn call`, `invocations`, `logs`
10
+ - **Sandboxes** — `env`, `fn dev`, `fn call`, `invocations`, `logs`
11
11
  - **Workspace previews** — `preview`
12
12
  - **Development** — `validate`, `mcp`, `guide`, `skills`
13
13
 
@@ -149,7 +149,7 @@ route's surface and fleet role.
149
149
  ## Authentication
150
150
 
151
151
  ```bash
152
- oxyc login [--env <e>] [--login-env <e...>] [--assume <org> -r <why>]
152
+ oxyc login [--env <e>] [--login-env <e...>] [--assume <org> -r <why>] [--no-browser [--approver <email>]]
153
153
  oxyc whoami [--json] # who the credential is, and what it can reach
154
154
  oxyc token # print the bearer, for a raw curl
155
155
  oxyc login-link [--next <path>] [--json] [--open] # a one-time URL that signs a browser in
@@ -158,8 +158,8 @@ oxyc tokens list [--json] # your personal access tokens
158
158
  oxyc tokens revoke <id>
159
159
  oxyc tokens revoke --current # end the token in OXY_TOKEN
160
160
  oxyc tokens create # opens Account → Personal access tokens
161
- oxyc tokens create --sandbox-agent --app <org>/<app> [--app …] [--hours 8] [--name …]
162
- oxyc tokens create --agent [--standing] [--hours 8] [--name …]
161
+ oxyc tokens create --sandbox-agent --app <org>/<app> [--app …] [--hours 8] [--name …] [--no-browser [--approver <email>]]
162
+ oxyc tokens create --agent [--standing] [--hours 8] [--name …] [--no-browser [--approver <email>]]
163
163
  ```
164
164
 
165
165
  **An agent runs on its own token, never on the person's `oxyc login`.** Which
@@ -264,6 +264,37 @@ writes the credentials file, and revokes no other token. `--app` is
264
264
  `<org>/<app>` and repeats; arguments are checked before the browser opens
265
265
  (exit `2`). Only staff who can open the app's sandboxes can approve.
266
266
 
267
+ **`--no-browser`** is the same approval for a machine no browser can reach
268
+ (a cloud container, a CI runner, a remote dev box), where the loopback's
269
+ `127.0.0.1` port is not the approver's. Instead of opening a page, `oxyc`
270
+ asks for a device code (`POST /api/auth/cli/device`) and prints, on stderr,
271
+ a URL (`<deployment>/cli-auth/device?code=…`) and an eight-letter code. The
272
+ operator opens the URL on any device where they are signed in, types the
273
+ code from the terminal (the page never shows it), and approves; `oxyc` polls
274
+ (`POST /api/auth/cli/device/poll`, with the PKCE verifier) and prints the
275
+ token exactly as above. A request lives ten minutes. A decline, or a refusal
276
+ on the page, stops the command with exit `4` (`access_denied`) and the
277
+ reason; nobody deciding in time is exit `7`. It works the same for
278
+ `tokens create --agent` and for `oxyc login`, with one difference: a token
279
+ approved by device code **never carries staff or partner access**, since
280
+ whoever started the request collects it and the approver cannot see who that
281
+ is. `--standing --no-browser` is a usage error (exit `2`), and a login made
282
+ this way reaches only what its approver's organization memberships do.
283
+
284
+ **`--approver <email>`** (env `OXY_APPROVER`, valid only with `--no-browser`;
285
+ anything else is a usage error, exit `2`) names who must approve, so nobody
286
+ has to hand them a URL: the platform tells that person itself, by push,
287
+ email and Slack DM where their organization connected it, and in their
288
+ Oxygen inbox. They type the code from your output on the approval page. The
289
+ notice never holds the code and links to `/cli-auth/device` without it, and
290
+ the request is then theirs alone: anyone else who finds the page is told
291
+ there is no such request. `oxyc` prints `Approval request sent to <email>`
292
+ beside the code and the URL; it prints that whether or not the deployment
293
+ knew the address, since the deployment never says. An address nobody holds
294
+ leaves a request nobody can approve: it lapses after ten minutes, so a typo
295
+ means running the command again with the right one. An agent still never
296
+ opens or approves the page.
297
+
267
298
  What comes back is checked before anything is printed: it must be an
268
299
  `oxy_sbx_`, described as `sandbox_agent` and not all-access, for exactly the
269
300
  apps named, expiring no later than the hours asked for. Anything the check
@@ -276,14 +307,17 @@ what was minted, and exits `8`.
276
307
  With that token in `OXY_TOKEN`, `oxyc` behaves differently in five ways:
277
308
 
278
309
  - **An app resolves against the token's own list** (`GET /api/auth/token`),
279
- never `/api/admin/apps`. An app it was not minted for is exit `5`.
310
+ never the app listing (`GET /api/customer-apps`), which it may not call. An
311
+ app it was not minted for is exit `5`.
280
312
  - **Read-back uses `/api/customer-apps/{id}/…`** — `invocations list`,
281
- `invocations held` and `checks run` — since the token never reaches `/admin`.
313
+ `invocations held` and `checks run` — the same routes every other credential
314
+ uses; the token never reaches `/admin`.
282
315
  - **Refused before any request, exit `2`:** an `--app-env` that is not
283
316
  `dev-<handle>`; `publish` without `--app-env`, or with `--promote`;
284
- `fn call`, `logs`, `invocations list` and `checks run` without `--app-env`;
285
- `env show` of production or staging; `oxyc apps`, `tokens list` and
286
- `tokens revoke <id>`; and `oxyc api` against any path.
317
+ `fn call`, `fn run`, `logs`, `invocations list` and `checks run` without
318
+ `--app-env`; `env show` of production or staging; `env schedules`;
319
+ `oxyc apps`, `tokens list` and `tokens revoke <id>`; and `oxyc api` against
320
+ any path.
287
321
  - **`whoami`** prints the token's description — `kind`, `expires_at`, the
288
322
  `apps` it reaches and its `minter` — and `--json` is that document as the
289
323
  server sent it.
@@ -541,6 +575,8 @@ the current directory while granting access to the customer's repo.
541
575
 
542
576
  ```bash
543
577
  oxyc publish [--env <e>] [--dir <path>] [--promote] [--build-only | --prebuilt] [--json] [--allow-function-lint] [--app-env <dev-handle>]
578
+ oxyc promote <app> [--from staging|dev-<handle>] [--build <id>] [--remove-schedule <fn>]… [--dry-run] [--yes] [--json]
579
+ oxyc rollback <app> --app-env staging|production [--to <build>] [--remove-schedule <fn>]… [--dry-run] [--yes] [--json]
544
580
  oxyc init-ci [--app <org>/<app>] [--environment <name>] [--promote] [--force] [--no-register] [--setup-action]
545
581
  oxyc proxy [--port <n>] [--allow-writes] [--allow-events] [--yes]
546
582
  ```
@@ -549,11 +585,32 @@ oxyc proxy [--port <n>] [--allow-writes] [--allow-events] [--yes]
549
585
  `pnpm install`, `pnpm build`, output `out/`), bundles each declared Oxy Function
550
586
  with `pnpm exec esbuild` into `functions/<name>.js`, resolves the project from
551
587
  the target's public `build-config`, and uploads the bundle — to the **draft**
552
- channel unless `--promote`. `--env` defaults to **production**; name it.
553
-
588
+ channel (staging). `--env` defaults to **production**; name it. Ship it with
589
+ `oxyc promote`, below. `--promote` still publishes and promotes in one request,
590
+ through the same server-side path, and the server answers it with a
591
+ deprecation warning: it shows nobody the plan.
592
+
593
+ - **The manifest** is read strictly: `oxy-app.json` is validated against the
594
+ JSON Schema `@oxy-hq/sdk` generates from its `OxyAppManifest` type (the same
595
+ check `oxyc validate` and `@oxy-hq/vite-plugin` make, in the same words). An
596
+ error — a wrong type, a malformed `slug` or function name, a missing
597
+ required field, a notification catalogue the server refuses — stops the
598
+ publish before the build with exit `2`, listing every problem; a field the
599
+ schema does not know is a warning, since the platform ignores what it does
600
+ not read.
554
601
  - **Identity**: `--org` / `--app`, then `OXY_ORG` / `OXY_APP`, then the manifest's
555
602
  `orgSlug` / `slug`, then an `apps/<org>/<app>/` working directory. `--org`
556
603
  takes a slug or a UUID. `--project` pins the workspace (and implies its org).
604
+ - **First publish**: an app the target does not know yet makes the
605
+ build-config lookup 404. With a stored token, `publish` lists the org's
606
+ workspaces: exactly one, and it publishes there, saying so on stderr;
607
+ several, and it exits `5` listing them with their uuids beside the
608
+ `--project <uuid>` to pass. Under trusted publishing (no token) it exits `5`
609
+ with the two-step hint, as before.
610
+ - **Kit versions**: an app whose `@oxy-hq/sdk` or `@oxy-hq/vite-plugin` is more
611
+ than one minor behind the versions this `oxyc` was built beside gets one
612
+ warning per package before the build, naming the `pnpm add`. Read from the
613
+ app's `package.json` (the installed copy when there is one); no network.
557
614
  - **`--dir`** publishes a pre-built directory instead of building; functions are
558
615
  still bundled into it. **`--build-only`** stops after building and bundling —
559
616
  no credential, no network unless the org must come from `--project`.
@@ -645,13 +702,42 @@ policies. Registration never fails the command.
645
702
  **`--app-env <dev-handle>`** moves one sandbox's build pointer instead of the
646
703
  draft/live channel — see [Sandboxes](#sandboxes). It needs a staff credential
647
704
  (never a publish token or OIDC) and is refused together with `--promote`: a
648
- sandbox build is never promoted, so the loop is publish to the sandbox,
649
- iterate, then publish the same tree to staging and promote that.
705
+ sandbox build goes to production only through staging — `oxyc promote <app>
706
+ --from dev-<handle>`, then `oxyc promote <app>`.
707
+
708
+ **`promote`** moves a build up one environment: staging's to production (the
709
+ default), or a sandbox's to staging (`--from dev-<handle>`). Nothing else —
710
+ production is never promoted, and moving it backwards is `rollback`. It asks the
711
+ server for the **plan** first and prints it: the build (commit, branch,
712
+ publisher, how long the source has served it), what the target serves now,
713
+ functions and production schedules that change, the migrations production
714
+ applies **before** it moves, and `required` secrets production has no value for.
715
+ Then it asks on a terminal (off one it needs `--yes`, else exit `8`) and acts,
716
+ sending the plan's build and `expect_current` back — `409 target_moved` if the
717
+ target moved in between. `--dry-run` stops at the plan. A plan with blockers is
718
+ refused before any act (exit `6`): `staging_never_served` (production only
719
+ serves a build staging has served), `validation_not_passed`,
720
+ `required_secrets_unset`, and `schedule_removal_unconfirmed` — name each retired
721
+ schedule with `--remove-schedule <function>`. `--build` names an older build the
722
+ source served (publish id or UUID). Promoting the build already served is
723
+ `409 already_serving`.
724
+
725
+ **`rollback`** moves `--app-env` (staging or production; never a sandbox) back
726
+ to the build it served before its current one — twice swaps back — or `--to`
727
+ one it served earlier. Same plan, prompt and `--yes`. A rollback never reverses
728
+ a migration: the target's pending files are applied like any production move,
729
+ and the plan warns when the target predates files already applied. An unset
730
+ `required` secret only warns here (it is the emergency path).
731
+
732
+ Both need a staff credential: a publish token, a service account's and a sandbox
733
+ agent token are refused before any request (exit `2`). `--json` prints the
734
+ server's plan verbatim.
650
735
 
651
736
  ## Apps
652
737
 
653
- Read-only: every request these make is a GET, against `/api/customer-apps/**`
654
- (the app-admin role). `<app>` is `<org-slug>/<app-slug>` or an app UUID.
738
+ Read-only except `domains add | verify | remove`: every other request these make
739
+ is a GET, against `/api/customer-apps/**` (the app-admin role). `<app>` is
740
+ `<org-slug>/<app-slug>` or an app UUID.
655
741
 
656
742
  ```bash
657
743
  oxyc apps list [--org <slug>] [--published | --draft] [--builds] [--json]
@@ -660,6 +746,10 @@ oxyc apps builds <app> [--json]
660
746
  oxyc apps health [<app>] [--needs-attention] [--json]
661
747
  oxyc apps usage <app> [--json]
662
748
  oxyc apps drift [<app>] [--org <slug>] [--dir <path>] [--refresh] [--json]
749
+ oxyc apps domains [list] <app> [--json]
750
+ oxyc apps domains add <app> <domain> [--json]
751
+ oxyc apps domains verify <app> <domain> [--json]
752
+ oxyc apps domains remove <app> <domain> [--json]
663
753
  ```
664
754
 
665
755
  - **`list`** — every app you can see, across organizations, walking the paged
@@ -667,12 +757,17 @@ oxyc apps drift [<app>] [--org <slug>] [--dir <path>] [--refresh] [--json]
667
757
  `draft` when it has none. `SOURCE` is the repository the app is *registered*
668
758
  against. `--builds` adds the live build's id, the repository and commit it was
669
759
  built from, who published it and when — one extra request per app, six at a
670
- time.
760
+ time. `--json` rows also carry the server's `verdict` and, for a published
761
+ app that was measured, `availability`: request success over six windows and
762
+ the burn verdict. An absent `availability` means "not read", never healthy.
671
763
  - **`show`** — one app on one screen: the registry row, the live build, the
672
764
  draft build when it is newer than the live one, deployment integrity,
673
765
  availability, and the 7-day usage summary. `last promote` is when the promote
674
766
  or rollback action was last used; `oxyc publish --promote` does not update it,
675
- so it can be older than the live build.
767
+ so it can be older than the live build. Availability is the registry row's
768
+ reading, not a request of its own: with none, `show` prints `no reading`, and
769
+ it is never in `failed`. `--json` has the registry row under `app` and its
770
+ `availability` (or `null`) under `availability`.
676
771
  - **`builds`** — the build history, newest first. `CHANNEL` marks the build each
677
772
  channel points at: `live`, `draft`, or both for one build.
678
773
  - **`health`** — with no argument, the fleet table (published apps only):
@@ -682,6 +777,7 @@ oxyc apps drift [<app>] [--org <slug>] [--dir <path>] [--refresh] [--json]
682
777
  of the last 24 hours.
683
778
  - **`usage`** — the 7-day summary, the visitors, and the tracked events by name.
684
779
  - **`drift`** — see below.
780
+ - **`domains`** — see [Custom domains](#custom-domains).
685
781
 
686
782
  Times are UTC. `--json` prints one document on stdout; a table goes to stdout and
687
783
  the count line to stderr.
@@ -698,6 +794,39 @@ a drifted app is an answer and exits `0` — read it from the output.
698
794
  The health route answers **503 with its report** when a check fails; `oxyc`
699
795
  prints that as a failing report, not as an outage.
700
796
 
797
+ ### Custom domains
798
+
799
+ Serve an app on a domain the customer owns (`app.acme.com`) instead of its Oxy
800
+ subdomain. `<domain>` is a bare host name; `verify` and `remove` find the row by
801
+ that name (case-insensitive), so you never handle the domain's id.
802
+
803
+ ```bash
804
+ oxyc apps domains acme/store # list (the default verb)
805
+ oxyc apps domains add acme/store app.acme.com # then create the two DNS records it prints
806
+ oxyc apps domains verify acme/store app.acme.com
807
+ oxyc apps domains remove acme/store app.acme.com
808
+ ```
809
+
810
+ - **`list`** — each domain's `STATUS` (`pending` or `verified`), its URL and
811
+ when it was last checked. For every `pending` domain it prints the DNS records
812
+ to create — a `CNAME` from the domain to the deployment's target (a note to
813
+ ask Oxy ops when the deployment has none configured) and a `TXT` ownership
814
+ record — plus the checker's last error, if any.
815
+ - **`add`** — registers the domain as `pending` and prints the same two records
816
+ and the `verify` command to run next. A malformed name is `400
817
+ invalid_domain` and a duplicate `409 domain_exists`; both exit `6` with the
818
+ server's message.
819
+ - **`verify`** — checks DNS now. `verified` prints the URL and exits `0`. Still
820
+ `pending` is **not** success: it prints the records again and the checker's
821
+ reason, and exits `1` — DNS may not have propagated, so re-run it. `409
822
+ domain_taken` (another app already serves the domain) exits `6`.
823
+ - **`remove`** — stops serving the app on that domain. A name the app does not
824
+ have exits `5` and deletes nothing.
825
+
826
+ `--json` prints one document on stdout: `{app, app_id, domains, cname_target}`
827
+ for `list`, and `{app, app_id, domain, cname_target}` for `add` and `verify`
828
+ (`verify` still exits `1` while pending).
829
+
701
830
  ### `apps drift`
702
831
 
703
832
  Compares what is live against source on **this machine**. For each app it takes
@@ -782,7 +911,7 @@ oxyc checks run acme/store --app-env dev-a1 --browser --expect-selector 'main'
782
911
  ```
783
912
 
784
913
  **Auth** follows the same rule as `oxyc api`'s bearer surface, with one
785
- difference: `checks run` talks to `/api/admin/**`, which is not the
914
+ difference: `checks run` talks to `/api/customer-apps/**`, which is not the
786
915
  `/external/api/*` surface, so a configured API key is sent explicitly as
787
916
  `X-API-Key` only when **no bearer resolves** — never both. A bearer from
788
917
  `oxyc login` (or `--token-env`) always wins when one is present.
@@ -791,11 +920,12 @@ difference: `checks run` talks to `/api/admin/**`, which is not the
791
920
  holding `id-token: write` signs itself in from its GitHub OIDC token — the same
792
921
  two exchanges `oxyc publish` tries, in the same order: a service account's
793
922
  trust policy first, the app's registered publisher second — and drives
794
- `/api/customer-apps/**` instead: the same three handlers, mounted where a
795
- machine credential may reach them. A service account's token (`oxy_sat_…`,
796
- `oxy_ci_…`) or a publish token set in `OXY_TOKEN` takes that surface too,
797
- without minting; a personal token, a legacy API key or a session takes the admin
798
- one. `OXY_API_KEY` is read **before** OIDC here: a job that set one has said
923
+ `/api/customer-apps/**`, the same three routes a person's credential uses. A
924
+ service account's token (`oxy_sat_…`, `oxy_ci_…`) or a publish token set in
925
+ `OXY_TOKEN` is used without minting. What differs is the app lookup: a
926
+ personal token, a legacy API key or a session can list apps to resolve
927
+ `<org>/<app>`; a machine credential may not, so it names the app by UUID or
928
+ through its own grants. `OXY_API_KEY` is read **before** OIDC here: a job that set one has said
799
929
  which credential it means.
800
930
 
801
931
  The app id comes from the credential where it can. The publisher exchange
@@ -852,20 +982,33 @@ oxyc env create <app> <name> # name is dev-<handle>, 1-12 chars
852
982
  oxyc env list <app>
853
983
  oxyc env show <app> <name>
854
984
  oxyc env delete <app> <name> [--yes] [--wait [seconds]]
985
+ oxyc env schedules <app> <name> [--on|--off] # staging's or a sandbox's switch; off by default
855
986
 
856
987
  oxyc publish --app-env <name> [--env <e>] # see "Custom apps" above
988
+ oxyc fn dev <function> [--input <json|@file|->] [--env-file <path>] [--watch] [--read-through [--env <e>]] # local, in-process
857
989
  oxyc fn call <app> <function> [--app-env <name>] [--data <json|@file|->]
990
+ oxyc fn run <app> <function> [--app-env <name>] [--as-schedule] [--data …] [--timeout <s>]
858
991
  oxyc checks run <app> --app-env <name> # see "Checks" above
859
992
 
860
993
  oxyc invocations list <app> [--app-env] [--build] [--function] [--limit]
861
994
  oxyc invocations held <app> <invocation-id>
862
995
  oxyc logs <app> [--app-env] [--invocation] [--request] [--hours] [--limit]
996
+ oxyc logs <app> [--app-env <name>] --follow [--timeout <seconds>] [--json]
863
997
 
864
998
  oxyc env secret list <app> --app-env <name> # keys and flags, never a value
865
999
  oxyc env secret set <app> <key> --app-env <name> (--value <text> | --value-env <VAR>)
866
1000
  oxyc env secret delete <app> <key> --app-env <name>
867
1001
  ```
868
1002
 
1003
+ `logs --follow` is the live tail: each line as a function prints it,
1004
+ wherever the function runs, with no observability capture needed (the polled
1005
+ `logs` reads ClickHouse and answers `501` without it). `--json` prints one
1006
+ event per line — `open`, then `start` / `log` / `finish` per invocation,
1007
+ `dropped` when lines were lost, and `done` last, on which it exits `0`. A
1008
+ dropped connection is reopened with `Last-Event-ID`, and the server replays
1009
+ the outcomes of what finished meanwhile, never the lines. Production without
1010
+ `--app-env`; staging and sandboxes need staff with non-production reach.
1011
+
869
1012
  A **sandbox** is a named, short-lived `dev-<handle>` environment of one custom
870
1013
  app: its own build pointer, storage silo, secrets, Airhouse sibling and — in
871
1014
  an org with an OLTP staging branch — its own copy of the app's database, with
@@ -893,8 +1036,8 @@ to sit in an agent's context.
893
1036
  validated client-side — exit `2` on a malformed name) is a *different axis*
894
1037
  from `--env`, which is the deployment: both can appear on the same command
895
1038
  (`oxyc fn call acme/store f --env dev --app-env dev-a1`). It is not a global
896
- flag — only `publish`, `fn call`, `checks run`, `invocations list` and `logs`
897
- take it. A publish-token credential is refused before any request for
1039
+ flag — only `publish`, `fn call`, `fn run`, `checks run`, `invocations list`
1040
+ and `logs` take it. A publish-token credential is refused before any request for
898
1041
  `--app-env` other than `production` (exit `2`); `env`, `invocations` and
899
1042
  `logs` refuse one outright, for any environment, because sandbox management
900
1043
  is a staff console surface.
@@ -928,6 +1071,56 @@ distinct from a non-2xx on the POST itself (exit per the usual status mapping
928
1071
  — e.g. `EnvironmentHasNoBuild` before the stream starts). The
929
1072
  `x-oxy-invocation-id` response header becomes `invocationId` in the result.
930
1073
 
1074
+ **`fn dev`** runs a handler locally, from the app directory, with no server and
1075
+ no credentials: it bundles `functions/<name>.ts` with the esbuild call `oxyc
1076
+ publish` uses, runs it in-process on `createTestContext` from
1077
+ `@oxy-hq/sdk/testing`, and prints the response and every host op the handler
1078
+ made (`host ops: email.send`, one line per call). The context is seeded from
1079
+ `oxy-app.json` — the function's capabilities and destinations — and `ctx.env`
1080
+ from `--env-file` (default `.env`; never your shell). `--input` is the request
1081
+ body, as `fn call --data`. `--watch` re-runs on a change under the app
1082
+ directory. It exits `0` for a 2xx `Response`, `1` when the handler throws or
1083
+ answers non-2xx, `2` for a function the manifest does not declare. **A local run
1084
+ is not the isolate:** no timeout, memory or log limits, Node globals the isolate
1085
+ lacks, and every warehouse, semantic, storage, fetch and email call is stubbed
1086
+ in memory — nothing reaches a deployment. Check it where it really runs with
1087
+ `fn call` against a sandbox.
1088
+
1089
+ `fn dev --read-through [--env <name|url>]` sends exactly `ctx.query`,
1090
+ `ctx.warehouse.query` and `ctx.semantic.query` to that deployment, through
1091
+ `POST /api/projects/{project}/query` and `/semantic-query` (the routes a
1092
+ published app calls), authenticated as `oxyc api` is (`OXY_TOKEN` or your
1093
+ `oxyc login`). The server decides what you may read. The project is `--project`
1094
+ / `OXY_PROJECT`, else looked up from `oxy-app.json` `slug` + `orgSlug`. Every
1095
+ other call — writes, transactions, storage, fetch, email — stays stubbed, the
1096
+ report marks each call `real` or `stubbed`, and a local run is still not the
1097
+ isolate.
1098
+
1099
+ **`env schedules`** reads, or with `--on` / `--off` flips, the schedules
1100
+ switch of staging or a sandbox (`GET`/`PUT
1101
+ /api/customer-apps/{id}/environments/{name}/schedules`). Off by default: a
1102
+ schedule outside production reads production data on a timer and calls third
1103
+ parties with staging's credentials, so an environment fires its functions'
1104
+ `schedule`s only once staff turn it on. On, it has one schedule row per
1105
+ function its build schedules, kept in step by every publish to it; `--off`
1106
+ removes them. Production has no switch (exit `2`) — its schedules always run.
1107
+ Staff only: a sandbox agent token is refused before any request (exit `2`).
1108
+ A scheduled fire, and a delivery to the environment's own webhook route
1109
+ (`/api/webhooks/apps-env/{environment}/{org}/{app}/{function}`, verified with
1110
+ that environment's own secret), are not a sandbox's activity.
1111
+
1112
+ **`fn run`** queues a function as a background run — the console's **Run
1113
+ now** — and polls it to a verdict (`--timeout`, default 300 s), unlike `fn
1114
+ call`, which is the function's own route. It runs as the system (the org
1115
+ owner, no caller), with the function's retry policy; outside production only a
1116
+ `"check": true` function runs. **`--as-schedule`** runs a function that
1117
+ declares a `schedule` once, as its cron would: no input (`--data` is exit
1118
+ `2`), the environment's build and policy, whether or not its switch is on —
1119
+ the way to test a scheduled function in a sandbox without turning schedules
1120
+ on. A function with no schedule is `409 not_scheduled` (exit `6`), production
1121
+ included. `--json` prints `{function, environment, runId, status, ok, trigger,
1122
+ answer, error?, invocationId}`; exit `1` when the run did not end `done`.
1123
+
931
1124
  **`invocations held`** is what the non-production write policy did *not* do —
932
1125
  production's would-be effect, recorded rather than performed. It is `[]` for a
933
1126
  production invocation, since nothing is held there.
@@ -1149,6 +1342,12 @@ its own token (`tokens create --agent` or `--sandbox-agent`) does not: there,
1149
1342
  | `OXYC_QUIET` | — | `1` is `--quiet` |
1150
1343
  | `OXYC_DRY_RUN` | — | `1` makes `launch` print the command instead of running it |
1151
1344
  | `NO_COLOR` / `FORCE_COLOR` | — | force plain / coloured output |
1345
+ | `HTTPS_PROXY` / `HTTP_PROXY` | — | requests go through it. On Node 22.21+ and 24.5+, `oxyc` turns on Node's proxy support itself (`NODE_USE_ENV_PROXY`), re-running once if it has to. Older Node cannot proxy `fetch`, and the error says so |
1346
+
1347
+ **In a Claude Code cloud session,** the repo's SessionStart hook installs
1348
+ `oxyc` and keeps an agent token across shell calls. The environment must allow
1349
+ the Oxy hosts. Setup: the `oxy-custom-apps` skill, "Headless and cloud
1350
+ sessions".
1152
1351
 
1153
1352
  **An agent should set `OXY_AGENT`.** Every request `oxyc` makes to a deployment
1154
1353
  carries the user agent `oxyc/<version>`, with `agent/<label>` after it when an