@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.
- package/README.md +312 -31
- package/dist/main.mjs +11236 -2822
- package/dist/main.mjs.map +1 -1
- package/dist/{mcp-xxWL3_dz.mjs → mcp-B2KBWNOU.mjs} +86 -3
- package/dist/mcp-B2KBWNOU.mjs.map +1 -0
- package/package.json +6 -5
- package/skills/oxy-agents/SKILL.md +318 -0
- package/skills/oxy-agents/references/quick-reference.md +222 -0
- package/skills/oxy-agents/templates/agentic-template.yml +141 -0
- package/skills/oxy-airway-pipelines/SKILL.md +223 -316
- package/skills/oxy-automations/SKILL.md +61 -15
- package/skills/oxy-cli/SKILL.md +40 -6
- package/skills/oxy-custom-apps/SKILL.md +380 -294
- package/skills/oxy-custom-apps/references/minimal-starter.md +71 -0
- package/skills/oxy-custom-apps/references/performance-and-usage.md +151 -0
- package/skills/oxy-custom-apps/references/reading-data.md +106 -0
- package/skills/oxy-custom-apps/references/sandbox-agent-loop.md +307 -0
- package/skills/oxy-custom-apps/references/verifying-in-the-browser.md +65 -0
- package/skills/oxy-data-apps/SKILL.md +362 -0
- package/skills/oxy-data-apps/references/examples.md +263 -0
- package/skills/oxy-data-apps/references/quick-reference.md +171 -0
- package/skills/oxy-data-apps/templates/dashboard-with-controls.app.yml +128 -0
- package/skills/oxy-data-apps/templates/simple-dashboard.app.yml +102 -0
- package/skills/oxy-data-architecture/SKILL.md +137 -152
- package/skills/oxy-evals/SKILL.md +362 -0
- package/skills/oxy-evals/references/diagnosis.md +243 -0
- package/skills/oxy-evals/references/results-json.md +145 -0
- package/skills/oxy-evals/references/writing-expected.md +112 -0
- package/skills/oxy-semantic-modeling/SKILL.md +239 -59
- package/skills/oxy-semantic-modeling/references/warehouse-casts.md +57 -0
- package/skills/oxy-semantic-modeling/templates/topic-template.yml +38 -0
- package/skills/oxy-semantic-modeling/templates/view-template.yml +119 -0
- package/template/.claude/hooks/oxyc-cloud-session.sh +66 -0
- package/template/.claude/settings.json +16 -0
- package/template/.github/workflows/publish.yaml +15 -13
- package/template/.nvmrc +1 -1
- package/template/.oxyc-managed +18 -5
- package/template/README.md +21 -10
- package/template/package.json +3 -2
- package/template/pnpm-workspace.yaml +28 -17
- package/dist/mcp-xxWL3_dz.mjs.map +0 -1
|
@@ -1,36 +1,47 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: oxy-custom-apps
|
|
3
|
-
description:
|
|
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
|
-
#
|
|
19
|
+
# Custom-app development
|
|
7
20
|
|
|
8
|
-
|
|
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
|
|
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
|
|
27
|
-
|
|
28
|
-
platform is kit-agnostic (any bundle with a correct base path
|
|
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` (
|
|
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
|
-
|
|
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
|
|
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
|
|
64
|
-
|
|
65
|
-
base-path
|
|
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
|
|
90
|
-
|
|
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.
|
|
122
|
-
|
|
123
|
-
|
|
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 `-` → `_` (`
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
327
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
|
347
|
-
oxyc publish --env production # build → tar → upload (draft
|
|
348
|
-
oxyc
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
oxy
|
|
360
|
-
`
|
|
361
|
-
|
|
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
|
-
|
|
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
|
|
379
|
-
`
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
|
437
|
-
|
|
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
|
-
**
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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. |
|