@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
@@ -1,36 +1,47 @@
1
1
  ---
2
2
  name: oxy-custom-apps
3
- description: Use when building, debugging, reviewing, or shipping an Oxy custom-app bundle (a code-first React/Vite app served by oxy), including writing Oxy Functions and driving them from the UI. Encodes the contract rules, pitfalls, performance guardrails, and the self-serve `oxyc publish` ship flow.
3
+ description: >-
4
+ Use when building, debugging, reviewing, shipping, or verifying an Oxy
5
+ custom-app bundle (a code-first React/Vite app served by Oxy at
6
+ /customer-apps/<org>/<slug>/), including writing Oxy Functions, driving them
7
+ from the UI, and checking a published app in the browser with the Playwright
8
+ MCP server (no CI checks a published bundle). Covers the oxy-app.json
9
+ contract, @oxy-hq/sdk and @oxy-hq/vite-plugin, where app data goes
10
+ (ctx.airhouse, ctx.oltp, ctx.storage), pitfalls, performance guardrails, load
11
+ speed and usage tracking (service worker, Activity tab, useTrackEvent), the
12
+ self-serve oxyc publish ship flow, dev-<handle> sandboxes and workspace
13
+ previews, how an AI agent gets its own short-lived credential (oxyc tokens
14
+ create --sandbox-agent or --agent, never the engineer's oxyc login), and
15
+ reading a deployment's live data with oxyc api and oxyc proxy (which replaced
16
+ oxy api and the Rust oxy login, publish and proxy).
4
17
  ---
5
18
 
6
- # Customer-app development
19
+ # Custom-app development
7
20
 
8
- Customer-app bundles are arbitrary JS apps served by oxy at
21
+ Custom-app bundles are arbitrary JS apps served by Oxy at
9
22
  `https://<env>/customer-apps/<org>/<slug>/`. Oxy is a data gateway:
10
23
  serves static assets, gates cookie auth, and proxies queries. The
11
24
  bundle is opaque above the data plane — no per-app server config.
12
25
 
26
+ This file is the contract. The detail lives in `references/` — see
27
+ [Read next](#read-next) for which file answers what.
28
+
13
29
  ## The Oxy App Kit (use it)
14
30
 
15
31
  Two small packages remove the footguns — default to them rather than wiring a
16
32
  bundle by hand:
17
33
 
18
34
  - **`@oxy-hq/sdk`** — the data layer (`useQuery`, `useSemanticQuery`,
19
- `useAgentRun`, `<OxyChat>`); see rule 2 and `oxy-hq/customer-apps:
20
- docs/sdk-flow.md`.
35
+ `useAgentRun`, `<OxyChat>`); see rule 2 and the `@oxy-hq/sdk` README.
21
36
  - **`@oxy-hq/vite-plugin`** (`oxyApp()` in `vite.config.ts`) — bakes the asset
22
37
  **base path** from the manifest, copies `oxy-app.json` into `out/`, proxies
23
38
  `/api` in dev, and injects the `window.__OXY_APP__` shim. Dropping it
24
39
  re-introduces the #1 bug — asset 404s → blank dashboard (rule 3).
25
40
 
26
- Scaffold both with `@oxy-hq/create-oxy-app` or by copying `oxy-hq/customer-apps:
27
- examples/hello-oxy/`. The
28
- platform is kit-agnostic (any bundle with a correct base path works), but
29
- hand-rolling means owning the base-path + dev-proxy wiring yourself.
30
-
31
- (**`oxy-hq/customer-apps:` prefixes a path in that repo**, not in whatever repo
32
- you are sitting in. This skill loads globally, so every such pointer is
33
- qualified.)
41
+ Scaffold both with `pnpm create @oxy-hq/oxy-app my-app` (templates `vite` — the
42
+ default — `functions`, `dashboard`, `single-store`; `-t functions` adds Oxy
43
+ Functions). The platform is kit-agnostic (any bundle with a correct base path
44
+ works), but hand-rolling means owning the base-path + dev-proxy wiring yourself.
34
45
 
35
46
  ## Three rules
36
47
 
@@ -39,6 +50,7 @@ qualified.)
39
50
 
40
51
  ```json
41
52
  {
53
+ "$schema": "https://unpkg.com/@oxy-hq/sdk/schema/oxy-app.schema.json",
42
54
  "schemaVersion": 2,
43
55
  "slug": "url-slug",
44
56
  "orgSlug": "your-org",
@@ -46,23 +58,44 @@ qualified.)
46
58
  }
47
59
  ```
48
60
 
49
- Required: `schemaVersion` (must be `2`), `slug` (non-empty).
61
+ Required: `schemaVersion` (must be `2`), `slug` (1–63 lowercase
62
+ letters, digits and single hyphens — it becomes the served path and
63
+ the OLTP schema name).
50
64
  Strongly recommended: `orgSlug` (lets `@oxy-hq/vite-plugin` derive
51
65
  the asset base path automatically, so plain `pnpm build` produces
52
- a bundle ready to drop into the linked org with no env var).
53
- Optional: `name` (display), `projectId` (dev-time hint).
66
+ a bundle ready to drop into the linked org with no env var, and lets
67
+ `pnpm dev` and `oxyc publish` find the app without `--org` / `OXY_ORG`;
68
+ `create-oxy-app --org <slug>` writes it).
69
+ Optional: `name` (display), `description`, `icon`, `art`, `status`
70
+ (launcher card), `projectId` (dev-time hint), `build` (what `oxyc
71
+ publish` runs — `install`, `command`, `outDir`; the plugin reads
72
+ `outDir` too), `functions`, `env`, `storage`, `notifications`, …
73
+
74
+ **One contract.** The file is validated against the JSON Schema
75
+ `@oxy-hq/sdk` ships (`schema/oxy-app.schema.json`, generated from the
76
+ SDK's `OxyAppManifest` type; `$schema` gives your editor completion)
77
+ by `oxyc validate`, `oxyc publish`, the Vite plugin and the SDK's own
78
+ loader — the same check, the same words. A wrong type, a bad name or
79
+ a missing required field is refused; a field the schema does not know
80
+ is a **warning** (the platform ignores what it does not read — so a
81
+ typo'd `timeoutSecond` is said, not silently dropped). Every field and
82
+ what the server does with it: `OxyAppManifest` in
83
+ `@oxy-hq/sdk/manifest`, which also exports the schema, `isValidSlug`
84
+ / `isValidFunctionName` and `validateOxyAppManifest` for a tool of
85
+ your own.
54
86
 
55
87
  At serve time, oxy injects the authoritative org + project from
56
88
  the admin row via `window.__OXY_APP__` — the manifest's `orgSlug`
57
89
  and `projectId` are advisory hints only, never enforcement.
58
90
 
59
91
  Do not invent `products`, `writers`, or `from:`-blocks. Those
60
- don't exist in v2 and the probe rejects them.
92
+ don't exist in v2 and the schema rejects them.
61
93
 
62
94
  File location: **`<root>/oxy-app.json`**, sibling to
63
- `vite.config.ts`. The vite-plugin reads it from there; legacy
64
- bundles that kept it under `public/` won't see the plugin's
65
- base-path derivation kick in.
95
+ `vite.config.ts`. The vite-plugin reads it from there, and so does
96
+ `oxyc publish` (only from the directory it runs in); legacy bundles
97
+ that kept it under `public/` get neither the plugin's base-path
98
+ derivation nor the publish's `slug` / `orgSlug`.
66
99
 
67
100
  2. **All data flows through SDK hooks → oxy HTTP endpoints.** The
68
101
  surface is intentionally small:
@@ -86,8 +119,8 @@ qualified.)
86
119
  <OxyChat agentId="analytics" />
87
120
  ```
88
121
 
89
- See `oxy-hq/customer-apps: docs/sdk-flow.md` for the full reference,
90
- including `OxyApiError` parsing and beta-surface caveats.
122
+ See the `@oxy-hq/sdk` README for the full reference, including
123
+ `OxyApiError` parsing and beta-surface caveats.
91
124
 
92
125
  **Server-side work → Oxy Functions, not the browser.** `useQuery` &
93
126
  friends run in the client; for work the browser shouldn't do
@@ -118,9 +151,10 @@ qualified.)
118
151
  `ctx.storage` — and customer warehouses are read-only. See
119
152
  **Where data goes** below before writing anything.
120
153
  `oxyc publish` bundles `functions/*.ts` with
121
- esbuild — no separate backend to stand up. Worked example +
122
- author-facing `ctx` types: `oxy-hq/customer-apps:
123
- examples/hello-oxy/functions/`.
154
+ esbuild — no separate backend to stand up. Author-facing types:
155
+ `OxyFunctionContext` / `OxyFunctionRequest` from `@oxy-hq/sdk`; a worked
156
+ example ships in the `functions` template (`pnpm create @oxy-hq/oxy-app
157
+ my-app -t functions`).
124
158
 
125
159
  3. **`OXY_APP_BASE_PATH` must match the path the bundle is linked
126
160
  under.** `@oxy-hq/vite-plugin` resolves it from (in order):
@@ -139,20 +173,12 @@ qualified.)
139
173
  | **Files** — photos, PDFs, exports | Your app's storage silo | `ctx.storage` | function: `"storage": { "read": true, "write": true }` |
140
174
  | **The customer's warehouse** — ClickHouse, Snowflake, BigQuery, their Postgres | Theirs | `ctx.query` · `ctx.warehouse.query(db, sql)` | read-only: a write is refused unless the function declares `"customerWarehouseWrites": { "<db>": "<why>" }` |
141
175
 
142
- **Availability.** `ctx.airhouse`, `ctx.oltp.tx`, `airhouseMigrations`, the
143
- read-only rule for customer warehouses and `oxyc validate`'s data-placement
144
- and manifest checks arrive with the Oxy release carrying oxygen-internal#3187,
145
- together with the `@oxy-hq/sdk` and `@oxy-hq/cli` published from it. Against
146
- an older Oxy, `ctx.airhouse` is `undefined` and the new manifest fields are
147
- ignored; vendored `oxy.d.ts` copies (`examples/hello-oxy/functions/oxy.d.ts`)
148
- gain the types when they are re-synced from that SDK.
149
-
150
176
  Three questions, in order: **Is it bytes?** → storage (keep the key in a record
151
177
  or fact). **Does it change after it's written?** No → a fact; yes → a record;
152
178
  both when you need history (the record says what is true now, the fact says
153
179
  what changed). **Is it already in the customer's warehouse?** → read it there.
154
180
 
155
- `<writer>` is your slug with `-` → `_` (`store-ops` → `app_store_ops`); a slug
181
+ `<writer>` is your slug with `-` → `_` (`my-app` → `app_my_app`); a slug
156
182
  with `_` can't back a schema. Both stores are written **as the app**, whoever
157
183
  invoked the function, so a scheduled run writes like a click.
158
184
 
@@ -189,6 +215,90 @@ async function completeChecklist(ctx, runId: string, storeGuid: string) {
189
215
  }
190
216
  ```
191
217
 
218
+ ## Telling people something
219
+
220
+ Name **people**; the platform reaches them. Don't build a recipient table, a
221
+ "sent" log or a Slack webhook helper:
222
+
223
+ ```ts
224
+ await ctx.notify({
225
+ to: { role: "Store Manager", location: storeId }, // or { userId } · { team }
226
+ kind: "list_overdue", // declared in oxy-app.json
227
+ title: "Opening list is 30 minutes late",
228
+ link: `/lists/${listId}`, // a path inside the app
229
+ email: true, // optional; slack: true too
230
+ idempotencyKey: `overdue:${occurrenceId}`, // retry-safe
231
+ });
232
+ ```
233
+
234
+ `oxy-app.json`: `"notify": { "kinds": { "list_overdue": {} } }` on the function
235
+ (`{ "critical": true }` for a kind people must not mute). Everyone lands in
236
+ their Oxygen inbox — frontline crew too, who have no email — and is pushed,
237
+ emailed or Slacked where they can be. `slack: true` DMs each person's own Slack
238
+ account (found by their email once the org has connected Slack), so an app
239
+ never holds a Slack token or maps users to Slack ids. Needs the org's OLTP
240
+ store. Use
241
+ `ctx.email.send` only for addresses **outside** the org (a vendor, an
242
+ accountant), and give it an `idempotencyKey` in a scheduled function.
243
+
244
+ ### Categories, levels, threads and actions
245
+
246
+ Describe your kinds once, at the top level of `oxy-app.json`. It is the tree
247
+ people mute in (a whole app, a category, or one kind) and the one a phone groups
248
+ by, so declare it with the first `ctx.notify`, not later:
249
+
250
+ ```json
251
+ "notifications": {
252
+ "categories": {
253
+ "operations": { "label": "Store operations" },
254
+ "operations/food-safety": { "label": "Food safety" }
255
+ },
256
+ "kinds": {
257
+ "list_overdue": { "category": "operations", "label": "Overdue lists", "level": "time_sensitive" },
258
+ "temp_alarm": { "category": "operations/food-safety", "label": "Temperature alarm", "level": "critical",
259
+ "actions": [{ "id": "ack", "label": "Acknowledge", "function": "ack-alarm" }] }
260
+ }
261
+ }
262
+ ```
263
+
264
+ - **Category**: a path of up to 3 segments (`a-z0-9`, `_`, `-`); a declared
265
+ path's parents need no entry. A kind with none is in `general`.
266
+ - **Level**: `passive`, `active` (default), `time_sensitive`, `critical`. Only
267
+ `critical` ignores mutes; a function's `"critical": true` still makes it so.
268
+ - **Actions**: at most 3, label ≤ 24 characters. With `function` (one this app
269
+ declares), a tap calls it as the person with `{ notificationId, action, link }`
270
+ and marks it read; without, it opens the link.
271
+ - **Thread**: ``ctx.notify({ …, thread: `store:${storeId}` })`` (≤ 128
272
+ characters) folds a run of notifications about one thing into the newest.
273
+ - The function's `notify.kinds` still decides what it may send; a kind it sends
274
+ that the catalogue leaves out gets the defaults. `oxyc validate`, the Vite
275
+ plugin (`pnpm dev` / `pnpm build`) and `oxyc publish` refuse a malformed or
276
+ unknown category, a bad level, more than 3 actions, an action naming an
277
+ undeclared function, a misspelt field and any `oxy.` kind — every problem at
278
+ once; a catalogue kind no function sends is a warning.
279
+
280
+ The app opens in its own tab, so show the inbox **in the app** — people who
281
+ live in it (crew on a kiosk) never see Oxygen's bell:
282
+
283
+ ```tsx
284
+ import { NotificationsBell } from "@oxy-hq/sdk";
285
+
286
+ <NotificationsBell onNavigate={(link) => navigate(link)} /> // in the header
287
+ ```
288
+
289
+ It lists this app's notifications (`scope="all"` for the whole inbox), each
290
+ with its category under the title, `critical` and `time_sensitive` ones marked,
291
+ and threads folded; a tap
292
+ hands the `link` you passed to `ctx.notify` to `onNavigate` (without it, the
293
+ page loads that path under the app's base). It renders nothing where there is
294
+ no inbox (signed out, an older server, `pnpm dev`). `useNotifications()` gives
295
+ `{ items, unread, markRead, markAllRead, refresh, … }` to draw your own, and
296
+ takes `{ category, kind, thread }` to narrow it. Theme
297
+ it with `--oxy-notifications-*` CSS variables. Push sign-up and mutes stay in
298
+ Oxygen's Settings → Notifications — don't build them. In tests,
299
+ `createTestInbox({ manifest })` from `@oxy-hq/sdk/testing` is a fake inbox to pass as
300
+ `<OxyAppProvider fetcher={inbox.fetcher}>`.
301
+
192
302
  ## Four pitfalls
193
303
 
194
304
  - **CORS errors are usually wrong-URL errors in disguise.** The SDK's
@@ -211,154 +321,140 @@ async function completeChecklist(ctx, runId: string, storeGuid: string) {
211
321
  rely on the nil UUID `00000000-...` as a default project; don't
212
322
  assume `OXY_STATE_DIR` is writable.
213
323
 
214
- ## Performance guardrails
215
-
216
- Any app that ships an Oxy Function or drives a batch of them should keep these —
217
- cheap up front, hard to retrofit. (Established by the pokehouse `post-je` function.)
218
-
219
- **In an Oxy Function — count the round-trips.** Each isolate run is stateless and
220
- pays a fresh network hop per `ctx.query` / `ctx.warehouse.*`, so serial *dependent*
221
- calls dominate latency.
222
- - Run independent data-plane calls with `Promise.all`, not sequential `await`
223
- (e.g. resolve reference data **and** provision tables concurrently — only the
224
- final step needs both).
225
- - Fold a check into a query that already runs: a duplicate/existence guard is a
226
- scalar subquery on the main `SELECT`, not a second round-trip.
227
- - `Promise.all` idempotent DDL (`CREATE TABLE IF NOT EXISTS`); better, keep
228
- provisioning off the hot path if a migration can own it.
229
- - A "cheap" extra `ctx.query`/`ctx.log` is N× the cost across N invocations.
230
-
231
- **Driving many function calls from the UI (bulk actions) — don't go serial.**
232
- - Independent calls run with **bounded concurrency** (~3–4) via a worker-pool over a
233
- queue, not one-at-a-time — turns an O(N) wall-clock batch into a few waves.
234
- - If the calls share mutable server state that races under concurrency (a rotating
235
- token, a lazily-created table), the best fix is to **remove the shared mutation**
236
- (e.g. a single-writer scheduled refresher owns the token; requests only read).
237
- When you can't, warm the state with one call first, then fan out.
238
- - Don't refetch derived state (a status ledger) after *every* item — the progress UI
239
- is the live feedback; **throttle** the refetch (~1/s) and flush once at the end.
240
- - Guard `setState` after the component may have unmounted mid-batch (`mounted` ref).
241
-
242
- **Trust the server, not the client body.** Environment / tenant / which-secret
243
- selection is derived server-side (a function secret, a server-side mapping table,
244
- the authed identity), never from the request body — validate or override anything
245
- the client sends that picks a target.
246
-
247
- **Reuse the SDK's data-plane caching.** `useQuery` already dedups concurrent identical
248
- queries and short-caches results — don't hand-roll a fetch that bypasses it. Mark
249
- read-only functions `cache: { ttlSeconds }` in the manifest; **never** cache a
250
- side-effecting one (it silently drops writes). `idempotencyKey` = a fresh UUID
251
- per ATTEMPT (click), never a stable per-subject key: the server stores each
252
- key's response indefinitely and replays it WITHOUT running the function, so a
253
- `store:period` key silently breaks any post → compensate (delete) → post-again
254
- cycle — the second post "succeeds" with the stale response and writes nothing.
255
- The key shields one request from transport duplication; cross-attempt dedup
256
- belongs in the function itself (a server-side guard over real state, e.g. a
257
- posted-ledger row the compensating action clears).
258
-
259
- **Watch-outs when optimizing (each surfaced in review):**
260
- - **Clear UI running-state in `finally`.** An async batch driver must reset its
261
- `running`/loading flag (and do its final refetch) in a `finally`, not after the
262
- `await` — a stray throw otherwise leaves the bar stuck at "…" with the button
263
- disabled.
264
- - **Parallelizing can move a side effect ahead of a gate.** Running an independent
265
- side-effecting call concurrently with a validation query means it can now fire
266
- even when validation would reject. Fine if the side effect is benign — just know
267
- you traded the ordering for the latency.
268
- - **Additive migration for a scoped guard → backfill legacy rows to their TRUE value,
269
- not a constant.** When a dedup/uniqueness guard starts keying on a new column,
270
- `ADD COLUMN … DEFAULT <constant>` silently defeats the guard for pre-existing rows.
271
- Derive the real value from an existing column (e.g. a DocNumber prefix) so old rows
272
- stay correctly scoped.
273
-
274
- ## Minimal starter
275
-
276
- Easiest path: `pnpm dlx @oxy-hq/create-oxy-app@latest my-app --template vite`.
277
- The scaffolded layout:
278
-
279
- ```
280
- my-app/
281
- ├── oxy-app.json ← project root, not public/
282
- ├── src/
283
- │ ├── App.tsx
284
- │ └── main.tsx
285
- ├── package.json
286
- ├── vite.config.ts
287
- └── tsconfig.json
288
- ```
289
-
290
- `oxy-app.json`:
291
- ```json
292
- { "schemaVersion": 2, "slug": "my-app", "orgSlug": "test", "name": "My App" }
293
- ```
294
-
295
- `src/main.tsx`:
296
- ```tsx
297
- import { OxyAppProvider } from "@oxy-hq/sdk";
298
- import { createRoot } from "react-dom/client";
299
- import { App } from "./App";
300
-
301
- createRoot(document.getElementById("root")!).render(
302
- <OxyAppProvider><App /></OxyAppProvider>
303
- );
304
- ```
305
-
306
- `src/App.tsx`:
307
- ```tsx
308
- import { useQuery } from "@oxy-hq/sdk";
324
+ Two more that read as bugs — `useTrackEvent("oxy-…")` is refused (`400`,
325
+ the prefix is reserved), and a hovered-but-never-opened app records no view —
326
+ are in [references/performance-and-usage.md](references/performance-and-usage.md).
309
327
 
310
- export function App() {
311
- const { rows, loading, error } = useQuery({
312
- sql: "SELECT 1 AS hello",
313
- });
314
- if (loading) return <p>loading…</p>;
315
- if (error) return <p style={{ color: "red" }}>{error.message}</p>;
316
- return <pre>{JSON.stringify(rows, null, 2)}</pre>;
317
- }
318
- ```
328
+ ## Run a function locally
319
329
 
320
- `vite.config.ts`:
321
- ```ts
322
- import oxyApp from "@oxy-hq/vite-plugin";
323
- import react from "@vitejs/plugin-react";
324
- import { defineConfig } from "vite";
330
+ `oxyc fn dev <name>` runs one handler from your source in-process, in seconds,
331
+ with no publish. It bundles the handler with the same esbuild call `oxyc publish`
332
+ uses, then invokes it on `createTestContext` seeded from `oxy-app.json`: the
333
+ function's capabilities, `ctx.env` from the env file, and its destinations. It
334
+ prints the response, then the host ops the handler made.
325
335
 
326
- export default defineConfig({
327
- plugins: [react(), oxyApp()],
328
- });
336
+ ```bash
337
+ oxyc fn dev notify --input '{"name":"Ada"}' # a literal, @file, or - for stdin
338
+ oxyc fn dev notify --input @sample.json --env-file .env.dev --watch
339
+ oxyc fn dev report --read-through --env dev # reads hit that deployment; writes stay stubbed
329
340
  ```
330
341
 
331
- The plugin handles `outDir = "out"`, base-path resolution from the
332
- manifest, dev-server proxy to `/api`, and the `window.__OXY_APP__`
333
- dev shim. Plain `pnpm build` produces a bundle with the correct
334
- asset base baked in.
342
+ - **Run it from the app directory** (the one with `oxy-app.json`). `--env-file`
343
+ defaults to `.env`; `ctx.env` holds that file's keys and nothing from your
344
+ shell. A `required` secret the file lacks is a warning.
345
+ - **`--watch` re-runs** when a file in the app changes, reading the manifest,
346
+ env file and `@file` input afresh. A broken edit prints its error and waits
347
+ for the next save. Exit with Ctrl-C.
348
+ - **Exit codes:** `0` for a 2xx `Response`; `1` when the handler throws or
349
+ answers non-2xx (as `fn call`); `2` for a function the manifest does not
350
+ declare, or a missing env file.
351
+ - **A local run is not the isolate.** There is no timeout, memory or log cap;
352
+ `Buffer`, `TextEncoder` and `process` exist where the isolate lacks them; and
353
+ every warehouse, semantic, storage, fetch and email call is a stub in memory,
354
+ so (without `--read-through`) nothing reaches a deployment. The context does refuse what the manifest
355
+ does not grant, in the host's words. A `ctx.warehouse` destination's engine is
356
+ assumed (a `customerWarehouseWrites` entry is a customer Postgres, any other
357
+ is Airhouse), because the workspace config is not read.
358
+ - **`--read-through` reads real data, and only reads.** It sends `ctx.query`,
359
+ `ctx.warehouse.query` and `ctx.semantic.query` to the deployment `--env <name|url>`
360
+ (or `--target`) names, through the app's own `POST /api/projects/{project}/query`
361
+ and `/semantic-query`, with your `oxyc login` bearer or `OXY_TOKEN` (the way
362
+ `oxyc api` authenticates). What you may read is the server's decision. The
363
+ project is `--project` / `OXY_PROJECT`, else looked up from `oxy-app.json`
364
+ `slug` + `orgSlug`. Every other call (warehouse writes, `ctx.tx`, `ctx.oltp`,
365
+ `ctx.airhouse`, storage, fetch, email, notify, and `ctx.queryStream`) stays a
366
+ stub in memory and never leaves your machine. Each call in the report is marked
367
+ `real` or `stubbed`. `--env` defaults to production, so name a dev environment
368
+ when you can. A sandbox agent token is refused: use a person's login.
369
+ `ctx.semantic.query` row keys are `view__member` in the isolate but the route
370
+ strips the prefix, so read-through puts it back from your spec; a key it could
371
+ not map unambiguously stays as the route named it, so confirm that one with
372
+ `fn call` on a sandbox. Still not the isolate.
373
+ - Before you ship, run it where it really runs: publish to your sandbox and
374
+ `oxyc fn call`, or `oxyc checks run`. For a committed regression test use
375
+ `createTestContext` from `@oxy-hq/sdk/testing` in vitest, as the functions
376
+ template does.
335
377
 
336
378
  ## Publishing
337
379
 
338
- Ship with `oxyc publish` — **no CI, no S3 sync, no folder picker**. From
339
- the app directory (`oxy-hq/customer-apps: examples/hello-oxy/` is the worked
340
- shell; note its manifest deliberately carries no `orgSlug`, leaving the org to
341
- be resolved at publish time):
380
+ **These commands are a person's, or an agent's on a token that person
381
+ approved.** `oxyc login` is the engineer's own browser login, and a publish to
382
+ a channel or a promote is refused under a sandbox agent token. As an agent,
383
+ iterate in your own sandbox
384
+ ([references/sandbox-agent-loop.md](references/sandbox-agent-loop.md)). Ship
385
+ only when your engineer asked you to, on an agent token they approved
386
+ (`oxyc tokens create --agent`, plus `--standing` for staff access): it
387
+ publishes and promotes as them. With no browser on your machine, see
388
+ [Headless and cloud sessions](#headless-and-cloud-sessions): a token approved
389
+ there never carries staff access.
390
+
391
+ Ship with `oxyc publish` (`npm i -g @oxy-hq/cli`) — **no CI step, no S3 sync,
392
+ no folder picker**. From the app directory:
342
393
 
343
394
  ```bash
344
395
  oxyc login --env production # once; browser flow, caches a token,
345
396
  # and prints whether you're an app-admin
346
- oxyc validate # data-placement and manifest checks first
347
- oxyc publish --env production # build → tar → upload (draft channel)
348
- oxyc publish --env production --promote # …straight to live
397
+ oxyc validate # schema, data-placement and function checks first
398
+ oxyc publish --env production # build → bundle functions → tar → upload (draft = staging)
399
+ oxyc promote <org>/<app> --env production --dry-run # the plan: changes, migrations, blockers
400
+ oxyc promote <org>/<app> --env production # staging's build → production (asks first)
401
+ oxyc rollback <org>/<app> --env production --app-env production # back to the build before
349
402
  ```
350
403
 
351
- - **Identity** is the manifest's `slug` + `orgSlug` (override with
352
- `--app` / `--org`). The server upserts the app row — there is no
353
- separate `oxy apps create` step.
354
- - **First publish of a new app** needs `--project <workspace-uuid>`: the
355
- build-config lookup can't resolve a project until the app exists. Drop
404
+ Production only ever serves a build staging has served, and every way of
405
+ moving it — `oxyc promote`, `oxyc rollback`, the console's **Promote draft**
406
+ and **Roll back to…**, a promoting publish — applies the build's pending migrations **before** it moves
407
+ (a failure leaves the old build live). `promote` and `rollback` print the plan
408
+ first and refuse on a blocker: `staging_never_served`, `required_secrets_unset`
409
+ (set the key in production first), `schedule_removal_unconfirmed` (pass
410
+ `--remove-schedule <function>`). Off a terminal pass `--yes`.
411
+ `oxyc publish --promote` still works and warns that it is deprecated: it shows
412
+ nobody the plan.
413
+
414
+ - **Identity**, for each half, is `--app` / `--org`, then `OXY_APP` /
415
+ `OXY_ORG`, then the manifest's `slug` + `orgSlug`, then an
416
+ `apps/<org>/<app>/` working directory. `--org` takes a slug or a UUID; pass
417
+ the slug whenever `oxyc` runs the build, because the asset base path is
418
+ baked from what you pass. With no org from any of those, the build stops
419
+ with `missing org: the app's base path needs it` (exit `2`): add
420
+ `"orgSlug"` to `oxy-app.json` (`create-oxy-app --org <slug>` writes it)
421
+ or `--org <org-slug>`, and `--app <slug>` when the directory has no
422
+ manifest. The server upserts the app row — there is no separate create
423
+ step.
424
+ - **First publish of a new app is one step.** The app is not registered
425
+ yet, so the build-config lookup 404s; with your login `oxyc publish` then
426
+ lists the org's workspaces, and when there is exactly one (a freshly
427
+ provisioned org) publishes into it and says which. With several it prints
428
+ them with their uuids beside the flag to pass — `--project
429
+ <workspace-uuid>` (or `OXY_PROJECT`), which also supplies the org. Drop
356
430
  the flag after the first publish — it's auto-resolved from then on.
357
- - **`--env` targets:** `local` → the oxy web-app Vite dev server `:5173`
358
- (run `pnpm dev` there; it serves `/cli-auth` and proxies `/api` to
359
- oxy on `:3000`), `dev`/`staging`/`production` →
360
- `app{-dev,-staging,}.oxygen-hq.com`.
361
- - `--dir out` skips the build and uploads a prebuilt directory.
431
+ Trusted publishing (CI) only ever publishes a registered app.
432
+ - **Old kit.** Before the build, an app whose `@oxy-hq/sdk` or
433
+ `@oxy-hq/vite-plugin` is more than one minor behind the versions this
434
+ `oxyc` was built with gets one warning naming the `pnpm add` — the
435
+ manifest contract and the function runtime move with them.
436
+ - **`--env` targets:** `local` → a local Oxy web-app Vite dev server `:5173`
437
+ (it serves `/cli-auth` and proxies `/api` to oxy on `:3000`), `dev` →
438
+ `aip.dev.oxy.tech`, `staging` → `aip.staging.oxy.tech`, `production` (the
439
+ default) → `app.oxygen-hq.com`. A value with a `.`, `/` or `:` is read as a
440
+ URL.
441
+ - **Function lint.** Before the build, each Oxy Function's source is checked
442
+ for what the host would refuse at the first call: a `ctx.*` call whose
443
+ capability the manifest lacks, a global the isolate does not have
444
+ (`Buffer`, `TextEncoder`, `process`, …), a write outside `destinations`. A finding fails
445
+ the publish (exit `1`); `--allow-function-lint` publishes past a false
446
+ positive.
447
+ - `--dir out` skips the build and uploads a prebuilt directory (declared
448
+ functions are still bundled into it); `--build-only` builds and bundles
449
+ without a credential or an upload; `--prebuilt` (with `--dir`) uploads a
450
+ directory whose functions are already bundled. Server warnings print on
451
+ stderr; `--json` prints the result.
452
+ - `.env.local` / `.env` are loaded from the app directory and its parents
453
+ (never overriding what is already set), so a stray `OXY_PROJECT` there
454
+ retargets a publish.
455
+ - **Credential:** `OXY_TOKEN`, then `oxyc login`'s cache — or, in a GitHub
456
+ Actions job with `id-token: write`, the job's OIDC identity.
457
+ `oxyc init-ci` writes a workflow that publishes with no stored secret.
362
458
  - Promote / roll back / unpublish from the oxy admin app-detail console;
363
459
  every publish is audited (who shipped each build, when).
364
460
  - **What is live now:** `oxyc apps show <org>/<app>` prints the live build, the
@@ -366,8 +462,86 @@ oxyc publish --env production --promote # …straight to live
366
462
  `oxyc apps builds <org>/<app>` is the history. `oxyc apps drift <org>/<app>`
367
463
  lists the commits in your checkout that touch the app after the live build's
368
464
  commit — `unknown` there means it could not compare, not that nothing changed.
369
-
370
- Full hands-on guide: `oxy-hq/customer-apps: docs/local-development.md`.
465
+ - **A customer's own domain** (`app.acme.com`) serves production only:
466
+ `oxyc apps domains add <org>/<app> app.acme.com` prints a CNAME and a TXT
467
+ record for the customer to create, then `oxyc apps domains verify …`. Build
468
+ nothing differently — the bundle keeps its `/customer-apps/<org>/<app>/` base
469
+ and its same-origin `/api` calls work on the domain. A visitor signs in once
470
+ through the Oxygen login; on that domain the session reaches only the app's
471
+ workspace, so a call to another workspace there is refused by design.
472
+
473
+ The build is not the check: after a publish, drive the page
474
+ ([references/verifying-in-the-browser.md](references/verifying-in-the-browser.md)).
475
+
476
+ ## Headless and cloud sessions
477
+
478
+ A Claude Code cloud session (claude.ai/code), a CI runner or any machine with
479
+ no browser can mint a token, iterate in a sandbox and publish. A person
480
+ approves once, from any device.
481
+
482
+ 1. **The cloud environment.** Its owner sets this once in the environment's
483
+ settings. Under **Network access → Custom**, allow `app.oxygen-hq.com` and
484
+ `*.oxy.tech`, and keep the default package-manager list ticked so npm can
485
+ install `oxyc`. Optionally add `OXY_APPROVER=<their email>` under
486
+ environment variables, so each approval request is pushed to them.
487
+ 2. **The repo's SessionStart hook,** `.claude/hooks/oxyc-cloud-session.sh`,
488
+ with its entry in `.claude/settings.json`. In a cloud session it installs
489
+ `oxyc` and routes it through the session's proxy. It also makes a minted
490
+ token reach every later command, which an `export` in one shell call cannot
491
+ do. What it prints at session start says what is missing.
492
+ - A customer repo that `oxyc new` or `oxyc adopt` made has both files.
493
+ - In an existing customer repo, `oxyc update --apply` adds the script but
494
+ only reports the settings entry, because the team may have their own
495
+ settings. Add the entry by hand:
496
+ ```json
497
+ { "hooks": { "SessionStart": [ { "matcher": "", "hooks": [ { "type": "command",
498
+ "command": "\"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/oxyc-cloud-session.sh\"",
499
+ "timeout": 120 } ] } ] } }
500
+ ```
501
+ - Any other repo can copy both from the `template/.claude/` directory of the
502
+ installed `@oxy-hq/cli` package.
503
+ 3. **The credential.** Run this in the background, because the approval can
504
+ outlast a two-minute shell limit:
505
+ ```bash
506
+ mkdir -p ~/.config/oxy && (umask 077; oxyc tokens create --agent --no-browser \
507
+ --env production >~/.config/oxy/agent-token.env)
508
+ ```
509
+ Give your engineer the URL and the code it prints on stderr. The token goes
510
+ to that file and never into your context. The hook loads the file before
511
+ every later command. To iterate in a sandbox, mint
512
+ `--sandbox-agent --app <org>/<app>` into the same file instead. A token
513
+ lasts 8 hours by default (`--hours`, up to 168). On exit `4`, mint again.
514
+ 4. **Ship** with the commands above, each with `--token-env OXY_TOKEN`.
515
+ `promote` and `rollback` need `--yes`, because there is no terminal to ask.
516
+
517
+ **A token approved by device code never carries staff or partner access.**
518
+ `--standing --no-browser` is a usage error (exit `2`). The token from step 3
519
+ reaches only what the approver's org memberships do. That is enough when they
520
+ belong to the app's org. Staff shipping a customer's app have three ways:
521
+
522
+ - **Let the repo's CI publish a draft, and a person promote it.** This works
523
+ only where the repo turned CI publishing on, with the repository variable
524
+ `OXY_CI_PUBLISH=on`. Without it, the publish job is skipped and the run
525
+ still shows green.
526
+ - `gh workflow run publish.yaml -f ref=<branch>` publishes that ref as
527
+ production's draft, which is staging. A push to `main` publishes to `dev`
528
+ instead.
529
+ - Check that the publish job ran, not that the run is green.
530
+ - Then your engineer runs `oxyc promote <org>/<app> --dry-run`, then
531
+ `oxyc promote <org>/<app>`, and sees the plan before it ships.
532
+ - Never pass `-f promote=true`. It rebuilds the ref and puts it live with no
533
+ plan, the deprecated promoting publish.
534
+ - **Mint the standing token where a browser opens.** On the engineer's own
535
+ machine, run `oxyc tokens create --agent --standing --hours <n>`. They put
536
+ the value in the cloud environment's variables as `OXY_TOKEN`. Anyone who
537
+ uses that environment can read it, so keep `--hours` short and revoke the
538
+ token when the task is done.
539
+ - **Leave the ship step to the engineer.** Hand back the sandbox's build and
540
+ the `oxyc promote --from dev-<handle>` command.
541
+
542
+ Without the hook, start each command with `. ~/.config/oxy/agent-token.env &&`.
543
+ If a command fails with `fetch failed` (exit `7`) while `HTTPS_PROXY` is set,
544
+ `oxyc` is too old to use the proxy. Update it, or set `NODE_USE_ENV_PROXY=1`.
371
545
 
372
546
  ## Sandboxes — try a change on real data before shipping it
373
547
 
@@ -375,13 +549,18 @@ A sandbox (`dev-<handle>`, 1-12 lowercase letters/digits/hyphens, no leading,
375
549
  trailing or double hyphen) is a named, short-lived environment of one app: its
376
550
  own build pointer, storage silo, secrets and Airhouse sibling schema. Reads
377
551
  hit production; most writes are isolated or *held* (recorded, not performed —
378
- `ctx.oltp`/`ctx.warehouse` stay shared — see
379
- `internal-docs/custom-app-sandboxes.md` if you have this repo checked out; if
380
- not, the per-call table is in this skill's "Where data goes" section above,
381
- and a held write's `note` field in `invocations held` names the manifest key
382
- that would make it land instead). It starts with no build, costs nothing
383
- idle, and is gone after 7 idle days or an explicit delete — safe to use for
384
- every iteration that would otherwise touch staging. Limit: 20 per app.
552
+ `ctx.oltp`/`ctx.warehouse` stay shared; a held write's `note` field in
553
+ `invocations held` names the manifest key that would make it land instead).
554
+ A sandbox's schedules are **off by default** (staging's too): staff turn them
555
+ on per environment with `oxyc env schedules <app> dev-x --on`, and a scheduled
556
+ fire is not activity. To test a scheduled function without that, run it once
557
+ as its cron would — `oxyc fn run <app> <fn> --app-env dev-x --as-schedule` (no
558
+ input, system identity). Webhooks reach a sandbox on their own route,
559
+ `/api/webhooks/apps-env/dev-x/<org>/<app>/<fn>`, verified with the sandbox's
560
+ own `secretVar` value — set it with `env secret set`; staging's does not count.
561
+ It starts with no build, costs nothing idle, and is gone after 7 idle days or
562
+ an explicit delete — safe to use for every iteration that would otherwise
563
+ touch staging. Limit: 20 per app, counting ones still being deleted.
385
564
 
386
565
  ```bash
387
566
  oxyc env create acme/store dev-x --env dev --json # starts with no build
@@ -389,136 +568,43 @@ oxyc publish --env dev --app-env dev-x --json # build + publish to
389
568
  oxyc env secret set acme/store KEY --app-env dev-x --env dev --value-env VAR # optional: its own secret
390
569
  oxyc fn call acme/store submit-order --app-env dev-x --env dev --data '{}' --json # call it
391
570
  oxyc checks run acme/store --app-env dev-x --env dev --json # run its checks
571
+ oxyc fn run acme/store nightly-sync --app-env dev-x --as-schedule --env dev --json # a scheduled function, once
392
572
  oxyc invocations list acme/store --app-env dev-x --env dev --json # what ran; a row's id is `id`
393
573
  oxyc invocations held acme/store <invocation-id> --env dev --json # what it held, not wrote
574
+ oxyc logs acme/store --app-env dev-x --env dev --json # its function logs
575
+ oxyc logs acme/store --app-env dev-x --env dev --follow --timeout 300 --json # live, as printed
394
576
  oxyc env delete acme/store dev-x --env dev --yes --wait --json # done; tears its homes down
395
577
  ```
396
578
 
397
579
  (`--data` has no short form — don't write `-d`.)
398
580
 
399
- **An agent's credential is a sandbox agent token** (`oxy_sbx_…`), not a staff
400
- login. Get it yourself, once per task — your operator approves in the browser
401
- that opens — and end it when the task is done:
402
-
403
- ```bash
404
- eval "$(oxyc tokens create --sandbox-agent --app acme/store --env dev)" # prints: export OXY_TOKEN=oxy_sbx_…
405
- oxyc whoami --env dev --json # kind "sandbox_agent", your app in `apps`, `expires_at` after the task ends
406
- # … the loop above …
407
- oxyc tokens revoke --current --env dev # last, after the delete
408
- ```
409
-
410
- It does this loop on the apps it was minted for (`--app`, up to five), in
411
- sandboxes it created, for 8 hours (`--hours`, up to 168) — and nothing else:
412
- production, staging, other apps, `oxyc api`, `oxyc apps` and a secret's value
413
- are all refused. With it, every verb that takes `--app-env` requires a
414
- `dev-<handle>` (exit `2` otherwise), and at most three of its sandboxes may be
415
- live at once.
416
-
417
- **Over MCP.** If your runtime speaks MCP, `oxyc mcp --env dev` serves the same
418
- loop as tools — `oxy_env_create` / `oxy_env_list` / `oxy_env_show` /
419
- `oxy_env_delete`, `oxy_publish_sandbox`, `oxy_fn_call`, `oxy_checks_run`,
420
- `oxy_invocations_list` / `oxy_invocations_held`, `oxy_logs`. It reads its
421
- credential from `OXY_TOKEN` and nowhere else (a person on their own login
422
- adds `--login`). With a sandbox agent token the list is those ten,
423
- `oxy_whoami`, `oxy_env_secret_list` / `_set` / `_delete` and
424
- `oxy_token_revoke` (`confirm: true`) — and no `oxy_request`. Prefer them to
425
- shelling out: each calls its verb's own request-building code, so the result
426
- is the document that verb's `--json` prints, and the choice changes nothing
427
- on the server. There is no exit code over MCP: a failure is `isError: true`
428
- with the class at the end of its text (`[exit 5 NOT_FOUND]`, plus `server
429
- code: <code>` when the server sent one) — branch on that the way you would
430
- on the exit code. `oxy_fn_call` and `oxy_checks_run` set `isError` when the
431
- function or a check failed, with the full document still in the result.
432
- `oxy_env_delete` needs `confirm: true`. `oxy_publish_sandbox` takes only a
433
- `dev-<handle>` and cannot reach staging or production.
434
-
435
581
  Iterate from `publish` as the code changes — each publish is a new build, and
436
- the sandbox always serves the newest. A sandbox build is **never promoted**:
437
- ship by publishing the same tree to staging, then promoting from the console.
582
+ the sandbox always serves the newest. A sandbox build reaches production
583
+ only through staging: a person runs `oxyc promote <app> --from dev-<handle>`
584
+ (the build you verified becomes staging's), checks it there, then `oxyc
585
+ promote <app>`.
438
586
  Staff only (needs `develop_apps` + `manage_apps`) — or a sandbox agent token
439
587
  such staff approved; a publish token is refused on every sandbox operation,
440
588
  so CI cannot use one.
441
589
 
442
- **Driving this loop unattended (an agent, not a human at a keyboard):**
443
-
444
- - Name your sandbox `dev-<short-task-slug>` — one per agent/task, never reused
445
- across unrelated tasks, so parallel agents don't collide. `env create`
446
- answers exit `6` if the name is already taken.
447
- - Branch on exit codes and JSON fields only, never on stderr text: `0` ok,
448
- `1` it ran and the thing itself failed, `2` you called it wrong, `4` not
449
- authenticated — your token expired, was revoked, or your operator lost
450
- access (stop — don't try another credential, a cached `oxyc login`
451
- included), `5` not found
452
- (environment/function/invocation — check the name, or publish first if
453
- the environment has no build), `6` a conflict or bad name (pick another
454
- name, wait out a teardown, or free up the 20-sandbox limit — or, on
455
- `token_sandbox_limit`, delete one of your own three with `--wait` first: a
456
- sandbox still being deleted counts toward the three), `7` retryable
457
- (network/5xx, or a `--wait` deadline), `8` refused (stop and report), `9`
458
- a check failed — read `invocations held` for that check's invocation
459
- before assuming the function is broken.
460
- - Hard rules: never pass `--promote` in this loop; never `publish` without
461
- `--app-env` while iterating in a sandbox; never use `production` as
462
- `--app-env` to "check" something — reads already hit production. Always
463
- delete the sandbox you created, **even when an earlier step failed**. On
464
- exit `4` or `8`, stop and report to a human rather than retrying with a
465
- different credential or flag. A held write is information to report, not
466
- an error to engineer around. Set a sandbox's secret with `oxyc env secret
467
- set` (`oxy_env_secret_set`), never a hand-built request, and never try to
468
- read a value back. Revoke your token last: `oxyc tokens revoke --current`.
469
- - Report back: the sandbox name, the build id(s) published, each check's
470
- `passed`, every held write (`op`/`plane`/`table`/`note`), and confirmation
471
- the sandbox was deleted.
590
+ **An agent runs this loop on its own sandbox agent token**, never a person's
591
+ login, with `--token-env OXY_TOKEN` on every command — the mint, exit codes,
592
+ rules and what to hand back are in
593
+ [references/sandbox-agent-loop.md](references/sandbox-agent-loop.md). Read it
594
+ before an agent runs any `oxyc` command against a deployment.
472
595
 
473
596
  **A sandbox isolates the app, not the workspace.** The semantic model,
474
597
  automations and Airway pipelines it reads are `main`'s. To try a workspace
475
- branch, open a **workspace preview** (staff only) — a separate loop:
476
-
477
- ```bash
478
- oxyc preview create <branch> --wait --workspace <id> --json # compile the branch; idempotent
479
- oxyc preview checks <branch> --workspace <id> --json # Airway change check of that revision
480
- oxyc preview run <branch> procedure <file.automation.yml> --wait --workspace <id> --json # held dry run
481
- oxyc preview runs list <branch> --workspace <id> --json # every run, the server's own included
482
- oxyc preview runs show <run-id> --wait --workspace <id> --json # one run: held steps, outcome
483
- oxyc preview delete <branch> --yes --workspace <id> --json # done
484
- ```
485
-
486
- `preview run`'s kind is `procedure` or `airway_sample`; never post a
487
- `transform_build` or `compare` run — the server queues those itself, and you
488
- read them with `preview runs list` / `runs show`. A `--wait` verb exits `1`
489
- when what it waited for ended `failed` or `cancelled`, `7` past its deadline.
490
- Over MCP the same verbs are `oxy_preview_create` / `_list` / `_show` /
491
- `_delete` (`confirm: true`) / `_checks` / `_run` / `_runs_list` / `_run_show`.
492
- To make a sandbox read the branch's semantic model, publish to it with
493
- `--semantic-branch <branch>`.
494
-
495
- ## Reading data while vibe-coding
496
-
497
- You don't need an `X-API-Key` to poke at the data API by hand — your
498
- `oxyc login` token authenticates as a bearer. From an app directory:
499
-
500
- ```bash
501
- oxyc login --env local # once; caches a token per host
502
-
503
- # gh-api-style client (resolves the target + bearer for you, no headers):
504
- oxyc api user --env local # who am I?
505
- oxyc api projects/<projectId>/query --env local -f sql='select 1'
506
-
507
- # raw curl with your token (e.g. to script or debug):
508
- curl -H "Authorization: Bearer $(oxyc token --env local)" \
509
- -H 'content-type: application/json' \
510
- localhost:5173/api/projects/<projectId>/query -d '{"sql":"select 1"}'
511
- ```
512
-
513
- `oxyc api <path>` takes the path relative to `/api/`; `-X` sets the method
514
- (POST when a body is given), `-f`/`-F` set the body (string / JSON-typed),
515
- `--input` sends a raw one, `-i` includes response headers. `--env` resolves the
516
- same targets as `oxyc publish` (`local` → the Vite dev server `:5173`, which
517
- proxies `/api` → oxy `:3000`).
518
-
519
- Do not guess a path: `oxyc routes <filter>` lists what exists and
520
- `oxyc schema <path>` gives the body it expects.
521
-
522
- Access is your **normal org membership** — fine for your own test
523
- workspace, not a cross-customer backdoor. The `projectId` is the one the
524
- vite-plugin already resolved for the app (it's in `window.__OXY_APP__`).
598
+ branch, open a workspace preview (staff only, `oxyc preview …`), or publish to
599
+ the sandbox with `--semantic-branch <branch>` — see
600
+ [references/sandbox-agent-loop.md](references/sandbox-agent-loop.md#a-sandbox-isolates-the-app-not-the-workspace).
601
+
602
+ ## Read next
603
+
604
+ | File | Read it when |
605
+ | --- | --- |
606
+ | [references/sandbox-agent-loop.md](references/sandbox-agent-loop.md) | You are an AI agent about to touch a deployment: minting the sandbox agent token, the loop, exit codes, rules, what to hand back, when you cannot get a token, the MCP tools, workspace previews. |
607
+ | [references/verifying-in-the-browser.md](references/verifying-in-the-browser.md) | You changed something a person sees (`src/**`, or a function a card calls) and need to prove it on the published page. |
608
+ | [references/reading-data.md](references/reading-data.md) | You need a deployment's live data while building: an agent's own `--agent` token, `oxyc api` / `routes` / `schema`, and `oxyc proxy` for `pnpm dev` against cloud data. |
609
+ | [references/performance-and-usage.md](references/performance-and-usage.md) | You write an Oxy Function, a bulk action, caching or idempotency keys — or need to know what Oxy does for load speed and usage tracking, and the opt-outs. |
610
+ | [references/minimal-starter.md](references/minimal-starter.md) | You are writing an app by hand instead of scaffolding it, or need the smallest correct layout. |