@oxy-hq/cli 0.8.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.
Files changed (41) hide show
  1. package/README.md +312 -31
  2. package/dist/main.mjs +11236 -2822
  3. package/dist/main.mjs.map +1 -1
  4. package/dist/{mcp-xxWL3_dz.mjs → mcp-B2KBWNOU.mjs} +86 -3
  5. package/dist/mcp-B2KBWNOU.mjs.map +1 -0
  6. package/package.json +6 -5
  7. package/skills/oxy-agents/SKILL.md +318 -0
  8. package/skills/oxy-agents/references/quick-reference.md +222 -0
  9. package/skills/oxy-agents/templates/agentic-template.yml +141 -0
  10. package/skills/oxy-airway-pipelines/SKILL.md +223 -316
  11. package/skills/oxy-automations/SKILL.md +61 -15
  12. package/skills/oxy-cli/SKILL.md +40 -6
  13. package/skills/oxy-custom-apps/SKILL.md +380 -294
  14. package/skills/oxy-custom-apps/references/minimal-starter.md +71 -0
  15. package/skills/oxy-custom-apps/references/performance-and-usage.md +151 -0
  16. package/skills/oxy-custom-apps/references/reading-data.md +106 -0
  17. package/skills/oxy-custom-apps/references/sandbox-agent-loop.md +307 -0
  18. package/skills/oxy-custom-apps/references/verifying-in-the-browser.md +65 -0
  19. package/skills/oxy-data-apps/SKILL.md +362 -0
  20. package/skills/oxy-data-apps/references/examples.md +263 -0
  21. package/skills/oxy-data-apps/references/quick-reference.md +171 -0
  22. package/skills/oxy-data-apps/templates/dashboard-with-controls.app.yml +128 -0
  23. package/skills/oxy-data-apps/templates/simple-dashboard.app.yml +102 -0
  24. package/skills/oxy-data-architecture/SKILL.md +137 -152
  25. package/skills/oxy-evals/SKILL.md +362 -0
  26. package/skills/oxy-evals/references/diagnosis.md +243 -0
  27. package/skills/oxy-evals/references/results-json.md +145 -0
  28. package/skills/oxy-evals/references/writing-expected.md +112 -0
  29. package/skills/oxy-semantic-modeling/SKILL.md +239 -59
  30. package/skills/oxy-semantic-modeling/references/warehouse-casts.md +57 -0
  31. package/skills/oxy-semantic-modeling/templates/topic-template.yml +38 -0
  32. package/skills/oxy-semantic-modeling/templates/view-template.yml +119 -0
  33. package/template/.claude/hooks/oxyc-cloud-session.sh +66 -0
  34. package/template/.claude/settings.json +16 -0
  35. package/template/.github/workflows/publish.yaml +15 -13
  36. package/template/.nvmrc +1 -1
  37. package/template/.oxyc-managed +18 -5
  38. package/template/README.md +21 -10
  39. package/template/package.json +3 -2
  40. package/template/pnpm-workspace.yaml +28 -17
  41. package/dist/mcp-xxWL3_dz.mjs.map +0 -1
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,9 +158,24 @@ 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 …]
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>]]
162
163
  ```
163
164
 
165
+ **An agent runs on its own token, never on the person's `oxyc login`.** Which
166
+ one depends on the task:
167
+
168
+ | The agent is… | It mints | Which reaches |
169
+ | --- | --- | --- |
170
+ | building a custom app in a sandbox | `tokens create --sandbox-agent --app …` (`oxy_sbx_…`) | the sandboxes of the apps it names, and nothing else |
171
+ | doing anything else: reading data with `oxyc api`, reproducing a bug against live data | `tokens create --agent` (`oxy_pat_…`) | everything its approver can, for hours |
172
+
173
+ Both are approved once in the browser by the person the agent works for, both
174
+ are printed once as `export OXY_TOKEN=…` and never stored, and both end with
175
+ `tokens revoke --current`. After a mint, pass `--token-env OXY_TOKEN` to every
176
+ command: a named variable is the only source, so a command that lost the
177
+ variable exits `4` instead of quietly running on the person's cached login.
178
+
164
179
  **Every command resolves its credential the same way, first match wins:**
165
180
 
166
181
  1. **`OXY_TOKEN`** (or the variable `--token-env` names). It holds any
@@ -168,7 +183,8 @@ oxyc tokens create --sandbox-agent --app <org>/<app> [--app …] [--hours 8] [--
168
183
  token (`oxy_pat_…`), a service account token (`oxy_sat_…`), a CI token
169
184
  (`oxy_ci_…`) or a sandbox agent token (`oxy_sbx_…`) — a legacy API key
170
185
  (`oxy_…`), a publish token, or a session token. A blank value counts as
171
- unset.
186
+ unset. An agent token is a personal access token by its prefix: only its
187
+ own description tells it from a login (`oxyc whoami`).
172
188
  2. **The login cache** — what `oxyc login` stored for this deployment.
173
189
  3. **GitHub OIDC**, in a GitHub Actions job granted `id-token: write`: the job's
174
190
  OIDC token is exchanged, once per process, for a fifteen-minute token acting
@@ -196,7 +212,7 @@ Settings → Workspace → Legacy API keys. Use an API token for anything new.
196
212
 
197
213
  **`login`** runs a browser loopback flow. Against a current deployment it
198
214
  receives a one-time code and exchanges it (PKCE) for a **personal access
199
- token** named `oxyc on <hostname>`: revocable, ninety days, and replacing that
215
+ token** named `oxyc on <hostname>`: revocable, a year, and replacing that
200
216
  machine's previous one. No credential travels in a URL. Against a deployment
201
217
  that predates the exchange it stores the session token the page hands back,
202
218
  exactly as before, and says so — nothing needs upgrading first, in either
@@ -212,7 +228,10 @@ rather than reading as fine from the cache. For a token it also prints what the
212
228
  token is and what it can reach — `all access`, or one line per grant with the
213
229
  role it is capped at, and any organization whose policy blocks it. For a legacy
214
230
  API key it prints `Legacy API key`, and that it reaches everything its owner
215
- can.
231
+ can. For an agent token it says `agent token`, who approved it, whether it
232
+ carries their staff or partner access, and when it expires. When a token in
233
+ `OXY_TOKEN` is refused (exit `4`), the advice is never to log in — a login
234
+ cannot replace what is in the variable. An agent stops and reports.
216
235
 
217
236
  **`token`** prints the bearer on stdout and nothing else. Inside a GitHub
218
237
  Actions job with nothing stored it prints the token the job's OIDC identity
@@ -245,6 +264,37 @@ writes the credentials file, and revokes no other token. `--app` is
245
264
  `<org>/<app>` and repeats; arguments are checked before the browser opens
246
265
  (exit `2`). Only staff who can open the app's sandboxes can approve.
247
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
+
248
298
  What comes back is checked before anything is printed: it must be an
249
299
  `oxy_sbx_`, described as `sandbox_agent` and not all-access, for exactly the
250
300
  apps named, expiring no later than the hours asked for. Anything the check
@@ -257,14 +307,17 @@ what was minted, and exits `8`.
257
307
  With that token in `OXY_TOKEN`, `oxyc` behaves differently in five ways:
258
308
 
259
309
  - **An app resolves against the token's own list** (`GET /api/auth/token`),
260
- 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`.
261
312
  - **Read-back uses `/api/customer-apps/{id}/…`** — `invocations list`,
262
- `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`.
263
315
  - **Refused before any request, exit `2`:** an `--app-env` that is not
264
316
  `dev-<handle>`; `publish` without `--app-env`, or with `--promote`;
265
- `fn call`, `logs`, `invocations list` and `checks run` without `--app-env`;
266
- `env show` of production or staging; `oxyc apps`, `tokens list` and
267
- `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.
268
321
  - **`whoami`** prints the token's description — `kind`, `expires_at`, the
269
322
  `apps` it reaches and its `minter` — and `--json` is that document as the
270
323
  server sent it.
@@ -276,6 +329,35 @@ A refusal from the server carries a hint an agent can act on — never "try
276
329
  `oxyc login` again". Exit `4` under this token means it expired, was revoked,
277
330
  or its minter lost access: stop and report.
278
331
 
332
+ **`tokens create --agent`** mints an **agent token** for everything that is not
333
+ a sandbox: a personal access token (`oxy_pat_…`) that reaches what its approver
334
+ reaches through their organization memberships, for 8 hours by default
335
+ (`--hours`, 1 to 168). It cannot be extended, edited or regenerated, and it
336
+ cannot create, extend or revoke tokens; it can open a browser session as itself
337
+ (`login-link`).
338
+
339
+ ```bash
340
+ eval "$(oxyc tokens create --agent --env dev)"
341
+ ```
342
+
343
+ The same browser loopback again, with the same guarantees: stdout is the single
344
+ line `export OXY_TOKEN=oxy_pat_…`, printed once, everything else is on stderr,
345
+ it holds no credential while it runs, never writes the credentials file and
346
+ revokes no other token. Arguments are checked before the browser opens (exit
347
+ `2`); `--agent` and `--sandbox-agent` together are a usage error.
348
+
349
+ `--standing` asks for the approver's staff or partner access as well. The
350
+ approver decides on the page, where it starts **off**: a token that comes back
351
+ without it is accepted, and stderr says so in one line. Without `--standing`, a
352
+ token that carries either is refused.
353
+
354
+ What comes back is checked before anything is printed: it must be a whole
355
+ `oxy_pat_`; it must describe itself (`GET /api/auth/token`) as `personal` with
356
+ `source` `oxyc_agent`; it must expire no later than the hours asked for; and it
357
+ must carry no standing that was not asked for. A deployment that predates agent
358
+ tokens would otherwise hand back an ordinary ninety-day login. On any mismatch
359
+ the command prints nothing, revokes what was minted, and exits `8`.
360
+
279
361
  Credentials live in the OS config directory under **`oxy`** — the same file the
280
362
  Rust `oxy login` wrote before it was removed, so an existing login still works:
281
363
 
@@ -493,6 +575,8 @@ the current directory while granting access to the customer's repo.
493
575
 
494
576
  ```bash
495
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]
496
580
  oxyc init-ci [--app <org>/<app>] [--environment <name>] [--promote] [--force] [--no-register] [--setup-action]
497
581
  oxyc proxy [--port <n>] [--allow-writes] [--allow-events] [--yes]
498
582
  ```
@@ -501,11 +585,32 @@ oxyc proxy [--port <n>] [--allow-writes] [--allow-events] [--yes]
501
585
  `pnpm install`, `pnpm build`, output `out/`), bundles each declared Oxy Function
502
586
  with `pnpm exec esbuild` into `functions/<name>.js`, resolves the project from
503
587
  the target's public `build-config`, and uploads the bundle — to the **draft**
504
- channel unless `--promote`. `--env` defaults to **production**; name it.
505
-
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.
506
601
  - **Identity**: `--org` / `--app`, then `OXY_ORG` / `OXY_APP`, then the manifest's
507
602
  `orgSlug` / `slug`, then an `apps/<org>/<app>/` working directory. `--org`
508
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.
509
614
  - **`--dir`** publishes a pre-built directory instead of building; functions are
510
615
  still bundled into it. **`--build-only`** stops after building and bundling —
511
616
  no credential, no network unless the org must come from `--project`.
@@ -548,7 +653,15 @@ lets it publish.
548
653
  *The workflow* is `.github/workflows/oxy-publish.yml` at the repo root: a
549
654
  `build` job (no id-token) that runs `publish --build-only`, and a `publish` job
550
655
  bound to a GitHub `environment:` (`--environment`, default `oxy-publish`) whose
551
- only work is `publish --prebuilt`. That job uses no third-party action: each
656
+ only work is `publish --prebuilt`. `build` installs pnpm with
657
+ `pnpm/action-setup@v6` — the pnpm the repo root's package.json pins
658
+ (`packageManager` or `devEngines.packageManager`), with no `version:` of its
659
+ own, since the action refuses a second, different one; pnpm 11 when nothing
660
+ pins it — and both jobs run the Node in `.nvmrc`, else 26 (pnpm 11 needs
661
+ Node 22.13 or later). It warns when nothing pins pnpm, and when
662
+ `pnpm-workspace.yaml` approves build scripts only with `onlyBuiltDependencies`,
663
+ which pnpm 11 ignores (the build's install would fail with
664
+ `ERR_PNPM_IGNORED_BUILDS`; move the entries to `allowBuilds`). The publish job uses no third-party action: each
552
665
  step runs `npx @oxy-hq/cli@<this version>` and `oxyc` exchanges the job's OIDC
553
666
  token for itself, acting as the service account `--service-account <org>/<name>`
554
667
  names — by default `<app's org>/deployer`. **Here, and only here, the account is
@@ -589,13 +702,42 @@ policies. Registration never fails the command.
589
702
  **`--app-env <dev-handle>`** moves one sandbox's build pointer instead of the
590
703
  draft/live channel — see [Sandboxes](#sandboxes). It needs a staff credential
591
704
  (never a publish token or OIDC) and is refused together with `--promote`: a
592
- sandbox build is never promoted, so the loop is publish to the sandbox,
593
- 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.
594
735
 
595
736
  ## Apps
596
737
 
597
- Read-only: every request these make is a GET, against `/api/customer-apps/**`
598
- (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.
599
741
 
600
742
  ```bash
601
743
  oxyc apps list [--org <slug>] [--published | --draft] [--builds] [--json]
@@ -604,6 +746,10 @@ oxyc apps builds <app> [--json]
604
746
  oxyc apps health [<app>] [--needs-attention] [--json]
605
747
  oxyc apps usage <app> [--json]
606
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]
607
753
  ```
608
754
 
609
755
  - **`list`** — every app you can see, across organizations, walking the paged
@@ -611,12 +757,17 @@ oxyc apps drift [<app>] [--org <slug>] [--dir <path>] [--refresh] [--json]
611
757
  `draft` when it has none. `SOURCE` is the repository the app is *registered*
612
758
  against. `--builds` adds the live build's id, the repository and commit it was
613
759
  built from, who published it and when — one extra request per app, six at a
614
- 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.
615
763
  - **`show`** — one app on one screen: the registry row, the live build, the
616
764
  draft build when it is newer than the live one, deployment integrity,
617
765
  availability, and the 7-day usage summary. `last promote` is when the promote
618
766
  or rollback action was last used; `oxyc publish --promote` does not update it,
619
- 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`.
620
771
  - **`builds`** — the build history, newest first. `CHANNEL` marks the build each
621
772
  channel points at: `live`, `draft`, or both for one build.
622
773
  - **`health`** — with no argument, the fleet table (published apps only):
@@ -626,6 +777,7 @@ oxyc apps drift [<app>] [--org <slug>] [--dir <path>] [--refresh] [--json]
626
777
  of the last 24 hours.
627
778
  - **`usage`** — the 7-day summary, the visitors, and the tracked events by name.
628
779
  - **`drift`** — see below.
780
+ - **`domains`** — see [Custom domains](#custom-domains).
629
781
 
630
782
  Times are UTC. `--json` prints one document on stdout; a table goes to stdout and
631
783
  the count line to stderr.
@@ -642,6 +794,39 @@ a drifted app is an answer and exits `0` — read it from the output.
642
794
  The health route answers **503 with its report** when a check fails; `oxyc`
643
795
  prints that as a failing report, not as an outage.
644
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
+
645
830
  ### `apps drift`
646
831
 
647
832
  Compares what is live against source on **this machine**. For each app it takes
@@ -705,8 +890,28 @@ object to stdout instead:
705
890
  { "app": "acme/dashboards", "appId": "…", "checks": [{ "name": "canary", "runId": "…", "status": "done", "passed": true, "durationMs": 4123 }] }
706
891
  ```
707
892
 
893
+ **`--browser`** also loads the app's page in headless Chromium and proves it
894
+ mounted: the platform runtime's `oxy-app-ready` beacon fires, nothing on the
895
+ page throws, and `--expect-selector <css>` (optional) becomes visible. With
896
+ `--app-env` the page must also report that environment and the build it serves
897
+ (`oxyc env show`'s `build_uuid`) in `window.__OXY_APP__`. The credential and
898
+ `X-Oxy-App-Env` ride only on requests under the app's own base path
899
+ (`/customer-apps/<org>/<app>/`: the document, assets, beacon and `/fn` calls).
900
+ The rest of the deployment's API is on the same origin, and the code under test
901
+ is often a build someone else published, so its `/api/…` calls go without the
902
+ credential, as an anonymous visitor's would. The result is one more entry in `checks[]`, with `"kind": "browser"` and
903
+ the `/fn` calls the page made. Playwright is not a dependency: it is resolved
904
+ from the working directory (`playwright`, `playwright-core` or
905
+ `@playwright/test`), so run it where one is installed. The page loads as a
906
+ user, so a publish, service-account, CI or sandbox agent token is refused
907
+ before any request.
908
+
909
+ ```bash
910
+ oxyc checks run acme/store --app-env dev-a1 --browser --expect-selector 'main'
911
+ ```
912
+
708
913
  **Auth** follows the same rule as `oxyc api`'s bearer surface, with one
709
- difference: `checks run` talks to `/api/admin/**`, which is not the
914
+ difference: `checks run` talks to `/api/customer-apps/**`, which is not the
710
915
  `/external/api/*` surface, so a configured API key is sent explicitly as
711
916
  `X-API-Key` only when **no bearer resolves** — never both. A bearer from
712
917
  `oxyc login` (or `--token-env`) always wins when one is present.
@@ -715,11 +920,12 @@ difference: `checks run` talks to `/api/admin/**`, which is not the
715
920
  holding `id-token: write` signs itself in from its GitHub OIDC token — the same
716
921
  two exchanges `oxyc publish` tries, in the same order: a service account's
717
922
  trust policy first, the app's registered publisher second — and drives
718
- `/api/customer-apps/**` instead: the same three handlers, mounted where a
719
- machine credential may reach them. A service account's token (`oxy_sat_…`,
720
- `oxy_ci_…`) or a publish token set in `OXY_TOKEN` takes that surface too,
721
- without minting; a personal token, a legacy API key or a session takes the admin
722
- 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
723
929
  which credential it means.
724
930
 
725
931
  The app id comes from the credential where it can. The publisher exchange
@@ -776,20 +982,33 @@ oxyc env create <app> <name> # name is dev-<handle>, 1-12 chars
776
982
  oxyc env list <app>
777
983
  oxyc env show <app> <name>
778
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
779
986
 
780
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
781
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>]
782
991
  oxyc checks run <app> --app-env <name> # see "Checks" above
783
992
 
784
993
  oxyc invocations list <app> [--app-env] [--build] [--function] [--limit]
785
994
  oxyc invocations held <app> <invocation-id>
786
995
  oxyc logs <app> [--app-env] [--invocation] [--request] [--hours] [--limit]
996
+ oxyc logs <app> [--app-env <name>] --follow [--timeout <seconds>] [--json]
787
997
 
788
998
  oxyc env secret list <app> --app-env <name> # keys and flags, never a value
789
999
  oxyc env secret set <app> <key> --app-env <name> (--value <text> | --value-env <VAR>)
790
1000
  oxyc env secret delete <app> <key> --app-env <name>
791
1001
  ```
792
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
+
793
1012
  A **sandbox** is a named, short-lived `dev-<handle>` environment of one custom
794
1013
  app: its own build pointer, storage silo, secrets, Airhouse sibling and — in
795
1014
  an org with an OLTP staging branch — its own copy of the app's database, with
@@ -817,8 +1036,8 @@ to sit in an agent's context.
817
1036
  validated client-side — exit `2` on a malformed name) is a *different axis*
818
1037
  from `--env`, which is the deployment: both can appear on the same command
819
1038
  (`oxyc fn call acme/store f --env dev --app-env dev-a1`). It is not a global
820
- flag — only `publish`, `fn call`, `checks run`, `invocations list` and `logs`
821
- 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
822
1041
  `--app-env` other than `production` (exit `2`); `env`, `invocations` and
823
1042
  `logs` refuse one outright, for any environment, because sandbox management
824
1043
  is a staff console surface.
@@ -852,6 +1071,56 @@ distinct from a non-2xx on the POST itself (exit per the usual status mapping
852
1071
  — e.g. `EnvironmentHasNoBuild` before the stream starts). The
853
1072
  `x-oxy-invocation-id` response header becomes `invocationId` in the result.
854
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
+
855
1124
  **`invocations held`** is what the non-production write policy did *not* do —
856
1125
  production's would-be effect, recorded rather than performed. It is `[]` for a
857
1126
  production invocation, since nothing is held there.
@@ -1021,9 +1290,12 @@ them, and a tool that can only fail still costs its schema every turn. A dead
1021
1290
  token ends the server at startup with exit `4`.
1022
1291
 
1023
1292
  **`guide`** prints a page to paste into `AGENTS.md` / `CLAUDE.md`.
1024
- **`skills install`** symlinks the six bundled Claude skills into
1293
+ **`skills install`** symlinks the bundled Oxy platform skills into
1025
1294
  `~/.claude/skills`; it refuses to run from an `npx` cache, whose symlinks dangle
1026
- once npm reclaims it.
1295
+ once npm reclaims it. The same skills are published to
1296
+ [`oxy-hq/skills`](https://github.com/oxy-hq/skills) for every other agent —
1297
+ `npx skills add oxy-hq/skills`, or the Claude Code plugin `oxy@oxy-hq`. They are
1298
+ written in `skills/` here; see `internal-docs/platform-skills.md`.
1027
1299
 
1028
1300
  ## Exit codes
1029
1301
 
@@ -1039,7 +1311,10 @@ once npm reclaims it.
1039
1311
  9 a check ran and failed or timed out (`oxyc checks run`)
1040
1312
  ```
1041
1313
 
1042
- `4` almost always means the wrong `--env`. `7` is worth retrying; `6` never is.
1314
+ `4` almost always means the wrong `--env`. A person logs in again. An agent on
1315
+ its own token (`tokens create --agent` or `--sandbox-agent`) does not: there,
1316
+ `4` means the token expired or was revoked, and the agent stops and reports.
1317
+ `7` is worth retrying; `6` never is.
1043
1318
 
1044
1319
  ## Environment variables
1045
1320
 
@@ -1067,6 +1342,12 @@ once npm reclaims it.
1067
1342
  | `OXYC_QUIET` | — | `1` is `--quiet` |
1068
1343
  | `OXYC_DRY_RUN` | — | `1` makes `launch` print the command instead of running it |
1069
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".
1070
1351
 
1071
1352
  **An agent should set `OXY_AGENT`.** Every request `oxyc` makes to a deployment
1072
1353
  carries the user agent `oxyc/<version>`, with `agent/<label>` after it when an