@oxy-hq/cli 0.8.0 → 0.9.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 +89 -7
- package/dist/main.mjs +4914 -3760
- package/dist/main.mjs.map +1 -1
- package/dist/{mcp-xxWL3_dz.mjs → mcp-Bdp0rX0l.mjs} +2 -2
- package/dist/{mcp-xxWL3_dz.mjs.map → mcp-Bdp0rX0l.mjs.map} +1 -1
- package/package.json +5 -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 +32 -5
- package/skills/oxy-custom-apps/SKILL.md +193 -291
- package/skills/oxy-custom-apps/references/minimal-starter.md +63 -0
- package/skills/oxy-custom-apps/references/performance-and-usage.md +105 -0
- package/skills/oxy-custom-apps/references/reading-data.md +106 -0
- package/skills/oxy-custom-apps/references/sandbox-agent-loop.md +267 -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/.github/workflows/publish.yaml +15 -13
- package/template/.nvmrc +1 -1
- package/template/.oxyc-managed +5 -5
- package/template/README.md +21 -10
- package/template/package.json +3 -2
- package/template/pnpm-workspace.yaml +28 -17
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Minimal starter
|
|
2
|
+
|
|
3
|
+
Easiest path: `pnpm dlx @oxy-hq/create-oxy-app@latest my-app --template vite`
|
|
4
|
+
(`--template functions` adds Oxy Functions).
|
|
5
|
+
The scaffolded layout:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
my-app/
|
|
9
|
+
├── oxy-app.json ← project root, not public/
|
|
10
|
+
├── src/
|
|
11
|
+
│ ├── App.tsx
|
|
12
|
+
│ └── main.tsx
|
|
13
|
+
├── package.json
|
|
14
|
+
├── vite.config.ts
|
|
15
|
+
└── tsconfig.json
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`oxy-app.json`:
|
|
19
|
+
```json
|
|
20
|
+
{ "schemaVersion": 2, "slug": "my-app", "orgSlug": "your-org", "name": "My App" }
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`src/main.tsx`:
|
|
24
|
+
```tsx
|
|
25
|
+
import { OxyAppProvider } from "@oxy-hq/sdk";
|
|
26
|
+
import { createRoot } from "react-dom/client";
|
|
27
|
+
import { App } from "./App";
|
|
28
|
+
|
|
29
|
+
createRoot(document.getElementById("root")!).render(
|
|
30
|
+
<OxyAppProvider><App /></OxyAppProvider>
|
|
31
|
+
);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`src/App.tsx`:
|
|
35
|
+
```tsx
|
|
36
|
+
import { useQuery } from "@oxy-hq/sdk";
|
|
37
|
+
|
|
38
|
+
export function App() {
|
|
39
|
+
const { rows, loading, error } = useQuery({
|
|
40
|
+
sql: "SELECT 1 AS hello",
|
|
41
|
+
});
|
|
42
|
+
if (loading) return <p>loading…</p>;
|
|
43
|
+
if (error) return <p style={{ color: "red" }}>{error.message}</p>;
|
|
44
|
+
return <pre>{JSON.stringify(rows, null, 2)}</pre>;
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`vite.config.ts`:
|
|
49
|
+
```ts
|
|
50
|
+
import oxyApp from "@oxy-hq/vite-plugin";
|
|
51
|
+
import react from "@vitejs/plugin-react";
|
|
52
|
+
import { defineConfig } from "vite";
|
|
53
|
+
|
|
54
|
+
export default defineConfig({
|
|
55
|
+
plugins: [react(), oxyApp()],
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The plugin handles `outDir = "out"`, base-path resolution from the
|
|
60
|
+
manifest, dev-server proxy to `/api`, and the `window.__OXY_APP__`
|
|
61
|
+
dev shim. Plain `pnpm build` produces a bundle with the correct
|
|
62
|
+
asset base baked in. Then publish it:
|
|
63
|
+
[SKILL.md → Publishing](../SKILL.md#publishing).
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Performance and usage
|
|
2
|
+
|
|
3
|
+
Two halves: what you keep in your own code (the guardrails), and what Oxy does
|
|
4
|
+
for every published app without any (load speed and usage tracking).
|
|
5
|
+
|
|
6
|
+
## Performance guardrails
|
|
7
|
+
|
|
8
|
+
Any app that ships an Oxy Function or drives a batch of them should keep these —
|
|
9
|
+
cheap up front, hard to retrofit.
|
|
10
|
+
|
|
11
|
+
**In an Oxy Function — count the round-trips.** Each isolate run is stateless and
|
|
12
|
+
pays a fresh network hop per `ctx.query` / `ctx.warehouse.*`, so serial *dependent*
|
|
13
|
+
calls dominate latency.
|
|
14
|
+
- Run independent data-plane calls with `Promise.all`, not sequential `await`
|
|
15
|
+
(e.g. resolve reference data **and** provision tables concurrently — only the
|
|
16
|
+
final step needs both).
|
|
17
|
+
- Fold a check into a query that already runs: a duplicate/existence guard is a
|
|
18
|
+
scalar subquery on the main `SELECT`, not a second round-trip.
|
|
19
|
+
- `Promise.all` idempotent DDL (`CREATE TABLE IF NOT EXISTS`); better, keep
|
|
20
|
+
provisioning off the hot path if a migration can own it.
|
|
21
|
+
- A "cheap" extra `ctx.query`/`ctx.log` is N× the cost across N invocations.
|
|
22
|
+
|
|
23
|
+
**Driving many function calls from the UI (bulk actions) — don't go serial.**
|
|
24
|
+
- Independent calls run with **bounded concurrency** (~3–4) via a worker-pool over a
|
|
25
|
+
queue, not one-at-a-time — turns an O(N) wall-clock batch into a few waves.
|
|
26
|
+
- If the calls share mutable server state that races under concurrency (a rotating
|
|
27
|
+
token, a lazily-created table), the best fix is to **remove the shared mutation**
|
|
28
|
+
(e.g. a single-writer scheduled refresher owns the token; requests only read).
|
|
29
|
+
When you can't, warm the state with one call first, then fan out.
|
|
30
|
+
- Don't refetch derived state (a status ledger) after *every* item — the progress UI
|
|
31
|
+
is the live feedback; **throttle** the refetch (~1/s) and flush once at the end.
|
|
32
|
+
- Guard `setState` after the component may have unmounted mid-batch (`mounted` ref).
|
|
33
|
+
|
|
34
|
+
**Trust the server, not the client body.** Environment / tenant / which-secret
|
|
35
|
+
selection is derived server-side (a function secret, a server-side mapping table,
|
|
36
|
+
the authed identity), never from the request body — validate or override anything
|
|
37
|
+
the client sends that picks a target.
|
|
38
|
+
|
|
39
|
+
**Reuse the SDK's data-plane caching.** `useQuery` already dedups concurrent identical
|
|
40
|
+
queries and short-caches results — don't hand-roll a fetch that bypasses it. Mark
|
|
41
|
+
read-only functions `cache: { ttlSeconds }` in the manifest; **never** cache a
|
|
42
|
+
side-effecting one (it silently drops writes). `idempotencyKey` = a fresh UUID
|
|
43
|
+
per ATTEMPT (click), never a stable per-subject key: the server stores each
|
|
44
|
+
key's response indefinitely and replays it WITHOUT running the function, so a
|
|
45
|
+
`store:period` key silently breaks any post → compensate (delete) → post-again
|
|
46
|
+
cycle — the second post "succeeds" with the stale response and writes nothing.
|
|
47
|
+
The key shields one request from transport duplication; cross-attempt dedup
|
|
48
|
+
belongs in the function itself (a server-side guard over real state, e.g. a
|
|
49
|
+
posted-ledger row the compensating action clears).
|
|
50
|
+
|
|
51
|
+
**Watch-outs when optimizing (each surfaced in review):**
|
|
52
|
+
- **Clear UI running-state in `finally`.** An async batch driver must reset its
|
|
53
|
+
`running`/loading flag (and do its final refetch) in a `finally`, not after the
|
|
54
|
+
`await` — a stray throw otherwise leaves the bar stuck at "…" with the button
|
|
55
|
+
disabled.
|
|
56
|
+
- **Parallelizing can move a side effect ahead of a gate.** Running an independent
|
|
57
|
+
side-effecting call concurrently with a validation query means it can now fire
|
|
58
|
+
even when validation would reject. Fine if the side effect is benign — just know
|
|
59
|
+
you traded the ordering for the latency.
|
|
60
|
+
- **Additive migration for a scoped guard → backfill legacy rows to their TRUE value,
|
|
61
|
+
not a constant.** When a dedup/uniqueness guard starts keying on a new column,
|
|
62
|
+
`ADD COLUMN … DEFAULT <constant>` silently defeats the guard for pre-existing rows.
|
|
63
|
+
Derive the real value from an existing column (e.g. a DocNumber prefix) so old rows
|
|
64
|
+
stay correctly scoped.
|
|
65
|
+
|
|
66
|
+
## Load speed & usage tracking (automatic — know what you get, and the opt-outs)
|
|
67
|
+
|
|
68
|
+
**You do not wire load performance; Oxy does it at publish and serve time.**
|
|
69
|
+
Every published app gets, with no bundle code:
|
|
70
|
+
|
|
71
|
+
- **Preload hints.** At publish, Oxy parses your built `index.html` and emits
|
|
72
|
+
`Link: rel=preload`/`modulepreload` on the shell, so the browser fetches your entry
|
|
73
|
+
chunks while the HTML is still in flight. This is why the **base-path wiring in
|
|
74
|
+
[rule 3](../SKILL.md#three-rules) is load-bearing twice over**: a bundle whose
|
|
75
|
+
entry `<script>`/`<link>` tags are real, hashed, same-origin assets (what
|
|
76
|
+
`@oxy-hq/vite-plugin` produces) gets hinted; one that inlines everything into a
|
|
77
|
+
single HTML file has nothing to hint, which is fine but flat.
|
|
78
|
+
- **A service worker** (`__oxy/sw.js`) that precaches those entry assets and serves
|
|
79
|
+
content-hashed files cache-first — a repeat load is near-instant and works offline.
|
|
80
|
+
- **Usage instrumentation** — SPA pageviews, Core Web Vitals, engagement time, and
|
|
81
|
+
uncaught-error counts, posted to `__oxy/beacon` and shown on the app's Activity tab.
|
|
82
|
+
This is *in addition to* `useTrackEvent` (your own named events), which is unaffected.
|
|
83
|
+
|
|
84
|
+
**The two opt-outs, in `oxy-app.json` (both default on — omit to keep them):**
|
|
85
|
+
|
|
86
|
+
```jsonc
|
|
87
|
+
{ "schemaVersion": 2, "slug": "…",
|
|
88
|
+
"performance": { "serviceWorker": false }, // ships its own SW, or must never cache
|
|
89
|
+
"analytics": false } // silences the platform's client events only
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Typed as `OxyAppManifest["performance"]` / `["analytics"]` in `@oxy-hq/sdk`. `analytics:
|
|
93
|
+
false` stops only the *client* runtime — Oxy still records one view row per navigation
|
|
94
|
+
server-side, so the Activity tab never goes dark. Reach for these rarely: the defaults are
|
|
95
|
+
the fast, observable path.
|
|
96
|
+
|
|
97
|
+
**Two gotchas that read as bugs:**
|
|
98
|
+
- **`oxy-` is a reserved event-name prefix.** `useTrackEvent("oxy-…")` is refused with a
|
|
99
|
+
400 — that namespace is the platform's own instrumentation. Name your events anything
|
|
100
|
+
else. (It is a collision guard, not a security boundary: the beacon rides your app's own
|
|
101
|
+
auth gate, so it is *your* rows that are protected from accidental merging, not made
|
|
102
|
+
unforgeable.)
|
|
103
|
+
- **A prefetch is not a view.** A launcher may warm an app on hover; Oxy checks
|
|
104
|
+
`Sec-Purpose`/`Purpose`/`X-moz` and does not record a view or start a session for a
|
|
105
|
+
speculative load. So a hovered-but-never-opened app shows no phantom traffic.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Reading data while vibe-coding: `oxyc`
|
|
2
|
+
|
|
3
|
+
You don't need an `X-API-Key` to poke at the data API by hand — a bearer
|
|
4
|
+
token works, and **`oxyc`** (`npm i -g @oxy-hq/cli`) is the client that sends
|
|
5
|
+
it for you. **It replaced `oxy api`, and the Rust `oxy login`, `oxy publish`
|
|
6
|
+
and `oxy proxy` with it**: every command that talks to a deployment's HTTP API
|
|
7
|
+
is `oxyc`. It reads the `credentials.json` the Rust `oxy login` wrote, so an
|
|
8
|
+
existing login still works.
|
|
9
|
+
|
|
10
|
+
The bearer is `OXY_TOKEN` (or the variable `--token-env` names), else the
|
|
11
|
+
`oxyc login` cache for that deployment.
|
|
12
|
+
|
|
13
|
+
## As an agent: your own agent token
|
|
14
|
+
|
|
15
|
+
**An agent does not read data on its engineer's login.** It mints its own
|
|
16
|
+
token for the task:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
eval "$(oxyc tokens create --agent --env <deployment>)" # prints: export OXY_TOKEN=oxy_pat_…
|
|
20
|
+
oxyc api user --env <deployment> --token-env OXY_TOKEN # …work…
|
|
21
|
+
oxyc tokens revoke --current --env <deployment> --token-env OXY_TOKEN # last
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- A page opens in your engineer's browser; **the click is theirs** — tell
|
|
25
|
+
them it is waiting, and never click it with a browser tool. The token is
|
|
26
|
+
printed once as `export OXY_TOKEN=…` and never written to disk; never put it
|
|
27
|
+
in a file, a commit or your report.
|
|
28
|
+
- It reaches what its approver can reach, for 8 hours
|
|
29
|
+
by default (`--hours`, 1 to 168). Staff or partner access comes only with
|
|
30
|
+
`--standing`, and the approver still chooses it on the page.
|
|
31
|
+
- Pass `--token-env OXY_TOKEN` to every command: one that lost the variable
|
|
32
|
+
then exits `4` instead of running on a cached login.
|
|
33
|
+
- It is not the sandbox agent token. To build and test a custom-app change,
|
|
34
|
+
mint `--sandbox-agent` instead ([sandbox-agent-loop.md](sandbox-agent-loop.md)).
|
|
35
|
+
A sandbox agent token is refused by `oxyc api` before any request (exit `2`),
|
|
36
|
+
and the data API answers it `404`: read what your change did through your
|
|
37
|
+
sandbox (`oxyc fn call`, `oxyc invocations`, `oxyc logs`). Do not unset
|
|
38
|
+
`OXY_TOKEN` to reach the login cache.
|
|
39
|
+
|
|
40
|
+
## Ask the deployment, never guess a path
|
|
41
|
+
|
|
42
|
+
The point of the tool is that **you never guess a path** — ask the deployment
|
|
43
|
+
you are actually talking to:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
oxyc login --env local # a person, once; caches a token per host
|
|
47
|
+
|
|
48
|
+
oxyc routes projects --env local # what exists, and what each does
|
|
49
|
+
oxyc schema projects/{project_id}/query -X POST # the body it expects
|
|
50
|
+
oxyc api user --env local # who am I?
|
|
51
|
+
oxyc api projects/<projectId>/query --env local -f sql='select 1' --md
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- **Path** is relative to `/api/` (`oxyc api user` → `<target>/api/user`); a
|
|
55
|
+
leading `/` or `api/` is fine, and `/external/api/**` passes through to the
|
|
56
|
+
API-key surface untouched.
|
|
57
|
+
- **Fields follow `gh api`**: `-f k=v` is a **string** field, `-F k=v` a
|
|
58
|
+
**typed** one (`true`, `3`, `["a"]`, `@file`), `--input @body.json` (or `-`)
|
|
59
|
+
a raw body. On GET/HEAD/DELETE, fields become query parameters instead.
|
|
60
|
+
`-X` sets the method (POST when a body is given); `-i` includes the status
|
|
61
|
+
line and response headers.
|
|
62
|
+
- **Read cheaply.** `--md` renders a markdown table and `--jq '<expr>'` (`-q`)
|
|
63
|
+
filters before you pay for the rows — both far fewer tokens than raw JSON,
|
|
64
|
+
which repeats every field name per row. `/sql/query` answers arrays of
|
|
65
|
+
strings **header row first**, not objects; `--md` is what makes that
|
|
66
|
+
readable.
|
|
67
|
+
- **Exit codes are a contract** (`oxyc exit-codes`) — branch on them rather
|
|
68
|
+
than parsing stderr: `4` = no usable credential, `5` = not found, `6` = the
|
|
69
|
+
request was malformed and will fail identically forever, `7` = retryable.
|
|
70
|
+
- **Credentials are per deployment and `--env` defaults to production.** A
|
|
71
|
+
401 that reads like an expired session is usually a dev call made with a
|
|
72
|
+
prod token — check the `--env`. A `200` with a `null` body is the *other*
|
|
73
|
+
expired-token shape; `oxyc whoami` tells them apart.
|
|
74
|
+
- For a raw `curl`, `oxyc token` prints the resolved bearer (`oxy api
|
|
75
|
+
--print-token` went with the rest of that command):
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
curl -H "Authorization: Bearer $(oxyc token --env local)" \
|
|
79
|
+
-H 'content-type: application/json' \
|
|
80
|
+
localhost:5173/api/projects/<projectId>/query -d '{"sql":"select 1"}'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`--env` resolves the same targets as `oxyc publish` (`local` → the Vite dev
|
|
84
|
+
server `:5173`, which proxies `/api` → oxy `:3000`; `dev`, `staging`,
|
|
85
|
+
`production`), and a value with a `.`, `/` or `:` is read as a URL.
|
|
86
|
+
|
|
87
|
+
Access is your **normal org membership** — fine for your own test workspace,
|
|
88
|
+
not a cross-customer backdoor. The `projectId` is the one the vite-plugin
|
|
89
|
+
already resolved for the app (it's in `window.__OXY_APP__`).
|
|
90
|
+
|
|
91
|
+
## Working locally against cloud data: `oxyc proxy`
|
|
92
|
+
|
|
93
|
+
`pnpm dev` is cross-origin, so the browser sends no oxy cookie and the data
|
|
94
|
+
API answers `origin not allowed`. The sidecar listens where a local
|
|
95
|
+
`oxy serve` would (`:3000`) and forwards your dev server's Oxy calls to the
|
|
96
|
+
cloud target with your token attached:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
oxyc proxy --env dev # then `pnpm dev` as usual
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Its guardrails are the reason to use it rather than a bare token:
|
|
103
|
+
side-effecting calls are **held** (`--allow-writes` forwards them), tracking
|
|
104
|
+
events **dropped** (`--allow-events`), and a production target is refused
|
|
105
|
+
without `--yes`. The token stays in the proxy process and never reaches the
|
|
106
|
+
browser. It cannot target a sandbox.
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# The sandbox agent loop
|
|
2
|
+
|
|
3
|
+
How an AI agent builds and tests a custom-app change on a deployment without a
|
|
4
|
+
person's login: its own short-lived token, its own `dev-<handle>` sandbox, and
|
|
5
|
+
a report its engineer can act on. What a sandbox is, and the same loop as a
|
|
6
|
+
person runs it, are in
|
|
7
|
+
[SKILL.md → Sandboxes](../SKILL.md#sandboxes--try-a-change-on-real-data-before-shipping-it).
|
|
8
|
+
|
|
9
|
+
## An agent's own credential: the sandbox agent token
|
|
10
|
+
|
|
11
|
+
**If you are an AI agent, you do not work on your engineer's login.** A login
|
|
12
|
+
carries everything the engineer can do, for 90 days. You mint a token for the
|
|
13
|
+
task instead (`oxy_sbx_…`): it reaches only the apps you name, your engineer
|
|
14
|
+
approves it once, and you end it when the task is done.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
oxyc tokens create --sandbox-agent --app <org>/<app> --env <deployment>
|
|
18
|
+
# → a page opens in your engineer's browser: the apps, the lifetime, Approve
|
|
19
|
+
# → stdout, once: export OXY_TOKEN=oxy_sbx_…
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- **The click is your engineer's.** Tell them a page is waiting in their
|
|
23
|
+
browser. The command waits five minutes for the click, so run it with a
|
|
24
|
+
timeout at least that long: a shell tool that stops a command after two
|
|
25
|
+
minutes kills it first. Never open or click that page with a browser tool.
|
|
26
|
+
- **Evaluate that line and work with the token.** In a shell that persists:
|
|
27
|
+
`eval "$(oxyc tokens create --sandbox-agent --app <org>/<app> --env <deployment>)"`.
|
|
28
|
+
In one that does not, keep the value and set `OXY_TOKEN` on each command.
|
|
29
|
+
`oxyc` writes nothing to the credentials file. Never write the token to a
|
|
30
|
+
file, a commit or your report.
|
|
31
|
+
- **Make the variable the only credential.** Put `--token-env OXY_TOKEN` on
|
|
32
|
+
every command after the mint, as the loop below does. With it, a command
|
|
33
|
+
that lost the variable exits `4` (`OXY_TOKEN is not set`): set it and run the
|
|
34
|
+
command again. Without it, `oxyc` falls back to your engineer's login cache
|
|
35
|
+
on this machine, and a publish with no `--app-env` then goes to the app's
|
|
36
|
+
draft channel as them.
|
|
37
|
+
- **Name every app the task touches** with its own `--app`, five at most.
|
|
38
|
+
`<org>/<app>` is `orgSlug` and `slug` from the app's `oxy-app.json`, not the
|
|
39
|
+
folder names, which can differ. When the manifest has no `orgSlug`, take the
|
|
40
|
+
org slug from the app's README or from your engineer. Pass `--hours` when
|
|
41
|
+
the task will outlast the default 8; the most is 168 (seven days). Apps and
|
|
42
|
+
lifetime are fixed at mint, and the token cannot be extended.
|
|
43
|
+
- **Check it** with `oxyc whoami --env <deployment> --token-env OXY_TOKEN --json`:
|
|
44
|
+
`"kind": "sandbox_agent"`, your app in `apps`, and an `expires_at` after the
|
|
45
|
+
task ends. Run it first when `OXY_TOKEN` was already set before you started:
|
|
46
|
+
without that `kind` it is someone else's credential, and you mint your own.
|
|
47
|
+
Then `oxyc env list <org>/<app> --env <deployment> --token-env OXY_TOKEN --json`:
|
|
48
|
+
exit `0` means the app and the deployment are right.
|
|
49
|
+
- **Set `OXY_AGENT=<your name>`** beside it, so the audit log tells your
|
|
50
|
+
requests from your engineer's.
|
|
51
|
+
- `--env` is the deployment (`dev`, `staging`, `production`). Pass the same
|
|
52
|
+
one on every command; omitted, it is `production`. Use the one your engineer
|
|
53
|
+
named. When they named none, use `production` and say so when you tell them
|
|
54
|
+
the approval is waiting.
|
|
55
|
+
|
|
56
|
+
For anything that is not the sandbox loop — reading data with `oxyc api`,
|
|
57
|
+
reproducing a bug against live data — the agent's credential is a different
|
|
58
|
+
token, `oxyc tokens create --agent`: see [reading-data.md](reading-data.md).
|
|
59
|
+
|
|
60
|
+
## What it reaches
|
|
61
|
+
|
|
62
|
+
One to five named apps, and only `dev-<handle>` sandboxes it created itself —
|
|
63
|
+
three at a time at most, counting ones still being deleted. Everything else
|
|
64
|
+
answers `404`: production and staging, another app, someone else's sandbox,
|
|
65
|
+
`/admin`, a secret's value. A publish to a channel (staging, production) or a
|
|
66
|
+
promote answers `403`. `oxyc` refuses those two, and `oxyc api`, `oxyc apps`
|
|
67
|
+
and `oxyc login-link`, before any request (exit `2`). **Shipping stays with a
|
|
68
|
+
person.**
|
|
69
|
+
|
|
70
|
+
## The loop
|
|
71
|
+
|
|
72
|
+
Under the token the environment must be named: `publish`, `fn call`,
|
|
73
|
+
`checks run`, `invocations list` and `logs` are refused without
|
|
74
|
+
`--app-env dev-<handle>` (exit `2`). Run `publish` from the app directory: it
|
|
75
|
+
reads the app from the `oxy-app.json` there. Every other command takes
|
|
76
|
+
`<org>/<app>`.
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# 1. Create it. It starts with no build. `url` in the answer is the sandbox's link.
|
|
80
|
+
oxyc env create <org>/<app> dev-x --env <deployment> --token-env OXY_TOKEN --json
|
|
81
|
+
# 2. Build + publish into it. Success: "channel":"sandbox" and "environment":"dev-x".
|
|
82
|
+
oxyc publish --app-env dev-x --env <deployment> --token-env OXY_TOKEN --json
|
|
83
|
+
# Optional: a secret of the sandbox's own, read from a variable.
|
|
84
|
+
oxyc env secret set <org>/<app> KEY --app-env dev-x --value-env VAR --env <deployment> --token-env OXY_TOKEN
|
|
85
|
+
# 3. Call what you changed. Success: exit 0 and "ok":true. Then run the app's checks.
|
|
86
|
+
oxyc fn call <org>/<app> <function> --app-env dev-x --data '{}' --env <deployment> --token-env OXY_TOKEN --json
|
|
87
|
+
oxyc checks run <org>/<app> --app-env dev-x --env <deployment> --token-env OXY_TOKEN --json
|
|
88
|
+
# 4. Read back: what ran (a row's id is `id`), what it held instead of writing, its logs.
|
|
89
|
+
oxyc invocations list <org>/<app> --app-env dev-x --env <deployment> --token-env OXY_TOKEN --json
|
|
90
|
+
oxyc invocations held <org>/<app> <invocation-id> --env <deployment> --token-env OXY_TOKEN --json
|
|
91
|
+
oxyc logs <org>/<app> --app-env dev-x --env <deployment> --token-env OXY_TOKEN --json
|
|
92
|
+
# 5. When the task is done: delete, then revoke, in that order.
|
|
93
|
+
oxyc env delete <org>/<app> dev-x --yes --wait --env <deployment> --token-env OXY_TOKEN --json
|
|
94
|
+
oxyc tokens revoke --current --env <deployment> --token-env OXY_TOKEN
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`dev-x` is `dev-<handle>`: 1–12 characters of `a-z`, `0-9` and single hyphens,
|
|
98
|
+
not starting or ending with one. Name it after the task
|
|
99
|
+
(`dev-<short-task-slug>`), one per agent and task, never reused across
|
|
100
|
+
unrelated tasks, so parallel agents do not collide. If your runtime speaks
|
|
101
|
+
MCP, the same loop is served as tools — see [Over MCP](#over-mcp).
|
|
102
|
+
|
|
103
|
+
## Publishing into it
|
|
104
|
+
|
|
105
|
+
Three things to know before the first publish:
|
|
106
|
+
|
|
107
|
+
- **`missing org: the app's base path needs it` (exit `2`).** `publish` takes
|
|
108
|
+
the org from `--org`, `OXY_ORG`, the `orgSlug` in the directory's
|
|
109
|
+
`oxy-app.json`, else the folder under an `apps/<org>/<app>/` path. An app
|
|
110
|
+
whose manifest names no org, outside such a path, has none. An app that
|
|
111
|
+
keeps its manifest in `public/` gets the folder name, which is wrong
|
|
112
|
+
wherever the folder is not the slug. In both cases add `--org <org-slug>`,
|
|
113
|
+
and `--app <slug>` when the directory has no manifest. Pass the slug, never
|
|
114
|
+
a UUID: the asset base path is baked from it.
|
|
115
|
+
- **`pnpm install` stops with `ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY`
|
|
116
|
+
(exit `1`).** The default install step is `pnpm install`; pnpm wants to
|
|
117
|
+
rebuild `node_modules` and your shell has no terminal for it to ask on. Run
|
|
118
|
+
the same command with `CI=true` in front.
|
|
119
|
+
- **A broken build costs an approval.** `oxyc publish --build-only` builds and
|
|
120
|
+
bundles with no credential, so run it before you ask for a token (it takes
|
|
121
|
+
`--org` too). `oxyc publish --dir out --prebuilt --app-env dev-x …` then
|
|
122
|
+
publishes that build without building again.
|
|
123
|
+
|
|
124
|
+
## What a sandbox does with a call
|
|
125
|
+
|
|
126
|
+
Reads hit production data. A write lands in a home of the sandbox's own, or it
|
|
127
|
+
is *held*: recorded in `invocations held` and not performed. A held write is
|
|
128
|
+
the expected answer outside production. Report it. Unless the task asks for
|
|
129
|
+
it, do not edit `nonProduction` in the manifest to make the write land:
|
|
130
|
+
staging and every other sandbox share that map. Schedules and webhooks do not
|
|
131
|
+
fire in a sandbox: call the function. The `fn call` result carries the
|
|
132
|
+
function's `logs`; `oxyc logs` shows the same lines about 30 seconds later.
|
|
133
|
+
|
|
134
|
+
## When a command fails
|
|
135
|
+
|
|
136
|
+
Branch on the exit code and the JSON fields, not on the text:
|
|
137
|
+
|
|
138
|
+
| Exit | From | It means | Do |
|
|
139
|
+
| --- | --- | --- | --- |
|
|
140
|
+
| `1` | `publish` | The build or the Oxy Function lint failed. | Fix what the error names, or see [Publishing into it](#publishing-into-it). |
|
|
141
|
+
| `1` | `fn call` | The function threw or answered a non-2xx: `"ok": false`. | Read `body` and `logs` in the result. It is your code. |
|
|
142
|
+
| `1` | `checks run` | The app declares no checks (no function has `"check": true`). | Not a failure. Verify with `fn call`, and report that the app has no checks. |
|
|
143
|
+
| `2` | any | The command was wrong, or the token may not do it. Nothing was sent. | Read the message. Do not retry it unchanged. |
|
|
144
|
+
| `4` | any | `OXY_TOKEN is not set`: the command lost the variable. Any other message: the token expired, was revoked, or your engineer lost access to the app. | Set the variable and run it again. Otherwise stop and report. Never log in. |
|
|
145
|
+
| `5` | any | Not found: the app, the sandbox, the function or the invocation, or one your token does not reach. From `fn call` before the first publish: the sandbox has no build. | Check the slugs and `--env` once. Do not try other spellings. From `env delete` it means already gone. |
|
|
146
|
+
| `6` | `env create` | The name is taken or reserved, a sandbox of that name is still tearing down, the app is at its 20-sandbox limit, or your token already holds three (`token_sandbox_limit`; one still being deleted counts). | Pick another handle, or delete one of yours with `--wait` first. Do not retry the same name. |
|
|
147
|
+
| `7` | any | Unavailable (network, 5xx), or a `--wait` deadline ran out. | Run it once more. If `env delete --wait` exits `7` twice, report the sandbox as not deleted. |
|
|
148
|
+
| `8` | `env delete` | Refused: it had no `--yes` and no terminal to ask on. | Add `--yes`. |
|
|
149
|
+
| `8` | anything else | A refusal. | Stop and report. Do not retry with another credential or flag. |
|
|
150
|
+
| `9` | `checks run` | A check failed or timed out. | Read `checks[].error`, then `invocations held` for that check's `invocationId` before assuming the function is broken: a held write can be the expected answer. |
|
|
151
|
+
|
|
152
|
+
## Rules
|
|
153
|
+
|
|
154
|
+
- **Never borrow another credential.** Do not run `oxyc login`, read the login
|
|
155
|
+
cache (`credentials.json`, `oxyc token`), or use an engineer's personal
|
|
156
|
+
token or API key. What the token does not reach is a person's step: ask your
|
|
157
|
+
engineer. Exit `4` under the token means it expired, was revoked, or your
|
|
158
|
+
engineer lost access to the app: stop and report.
|
|
159
|
+
- **Never put an Oxy credential into a sandbox secret.** The server refuses a
|
|
160
|
+
value shaped like one (`400 credential_shaped_value`). Set a secret with
|
|
161
|
+
`oxyc env secret set` (`oxy_env_secret_set`), never a hand-built request,
|
|
162
|
+
and never try to read a value back.
|
|
163
|
+
- **Never pass `--promote`, and never publish without `--app-env`.** A sandbox
|
|
164
|
+
build is never promoted. When it is right, say so and leave the ship step to
|
|
165
|
+
your engineer. Never use `production` as `--app-env` to "check" something:
|
|
166
|
+
reads already hit production.
|
|
167
|
+
- **A held write is information to report**, not an error to engineer around.
|
|
168
|
+
- **Clean up, even when a step failed.** Delete every sandbox you created,
|
|
169
|
+
then revoke your token — in that order, because the delete needs the token.
|
|
170
|
+
A sandbox you could not delete is removed a day after its token ends.
|
|
171
|
+
- **Leave the sandbox up when your engineer has to look at it**: they asked
|
|
172
|
+
for its link, or the change shows on a page. Say that it is still up, and
|
|
173
|
+
revoke the token once you will publish nothing more. The sandbox stays for a
|
|
174
|
+
day after its token ends, and your engineer can delete it sooner with the
|
|
175
|
+
`env delete` command on their own login.
|
|
176
|
+
|
|
177
|
+
## You cannot open a page
|
|
178
|
+
|
|
179
|
+
The token signs no browser in, and the sandbox's host answers only a signed-in
|
|
180
|
+
staff session. So you verify the functions (`fn call`, `checks run`,
|
|
181
|
+
`invocations held`, `logs`) and your engineer verifies what a person sees. Do
|
|
182
|
+
not log in to get a session, and do not drive the app's production URL
|
|
183
|
+
instead: it does not serve your build. What your engineer does with the link
|
|
184
|
+
is in [verifying-in-the-browser.md](verifying-in-the-browser.md).
|
|
185
|
+
|
|
186
|
+
## What to hand back
|
|
187
|
+
|
|
188
|
+
Whether the task worked or not:
|
|
189
|
+
|
|
190
|
+
- the deployment and the sandbox's name, and its link: `url` from
|
|
191
|
+
`env create`, `environment_url` from `publish` (`null` means the sandbox has
|
|
192
|
+
no host: say so);
|
|
193
|
+
- the build id(s) the publish answered;
|
|
194
|
+
- each function you called, with its input and whether it was `ok`; each
|
|
195
|
+
check's name and `passed`, or that the app declares no checks;
|
|
196
|
+
- every held write (`op`, `plane`, `table`, `note`);
|
|
197
|
+
- what you did not verify: under this token, everything on the page;
|
|
198
|
+
- the cleanup: the sandbox deleted or still up, and the token revoked.
|
|
199
|
+
|
|
200
|
+
Copy what you need out of `invocations` and `logs` into the report before you
|
|
201
|
+
delete the sandbox: read-back is not permanent. Never put the token in the
|
|
202
|
+
report.
|
|
203
|
+
|
|
204
|
+
## When you cannot get a token
|
|
205
|
+
|
|
206
|
+
Stop and ask your engineer. Do not fall back to their login in any of these
|
|
207
|
+
cases:
|
|
208
|
+
|
|
209
|
+
- **`oxyc` rejects `--sandbox-agent` as an unknown option (exit `2`).** The
|
|
210
|
+
CLI is too old. Ask your engineer to update `@oxy-hq/cli`.
|
|
211
|
+
- **Exit `8`.** The deployment did not return the token you asked for: it
|
|
212
|
+
predates sandbox agent tokens, or returned one for other apps. Nothing was
|
|
213
|
+
printed, and what it minted was revoked.
|
|
214
|
+
- **Exit `7`.** Nobody approved within five minutes. Ask your engineer what
|
|
215
|
+
the page said. Only Oxy staff holding `develop_apps` and `manage_apps` over
|
|
216
|
+
the app's org can approve: if the page refused, report what it said and
|
|
217
|
+
stop. If they missed it, run the command once more.
|
|
218
|
+
- **Exit `4`.** The approval code was rejected and nothing was minted. Run the
|
|
219
|
+
command once more, with fewer `--hours` if an organization caps a token's
|
|
220
|
+
lifetime.
|
|
221
|
+
- **No browser on this machine can open the page** (a remote container, a CI
|
|
222
|
+
job). The page answers to a local port on the machine `oxyc` runs on, so it
|
|
223
|
+
cannot be approved from another one.
|
|
224
|
+
|
|
225
|
+
## Over MCP
|
|
226
|
+
|
|
227
|
+
If your runtime speaks MCP, `oxyc mcp --env <deployment>` serves the same loop
|
|
228
|
+
as tools — `oxy_env_create` / `oxy_env_list` / `oxy_env_show` /
|
|
229
|
+
`oxy_env_delete`, `oxy_publish_sandbox`, `oxy_fn_call`, `oxy_checks_run`,
|
|
230
|
+
`oxy_invocations_list` / `oxy_invocations_held`, `oxy_logs`. It reads its
|
|
231
|
+
credential from `OXY_TOKEN` and nowhere else (a person on their own login
|
|
232
|
+
adds `--login`). With a sandbox agent token the list is those ten,
|
|
233
|
+
`oxy_whoami`, `oxy_env_secret_list` / `_set` / `_delete` and
|
|
234
|
+
`oxy_token_revoke` (`confirm: true`) — and no `oxy_request`. Prefer them to
|
|
235
|
+
shelling out: each calls its verb's own request-building code, so the result
|
|
236
|
+
is the document that verb's `--json` prints, and the choice changes nothing
|
|
237
|
+
on the server. There is no exit code over MCP: a failure is `isError: true`
|
|
238
|
+
with the class at the end of its text (`[exit 5 NOT_FOUND]`, plus `server
|
|
239
|
+
code: <code>` when the server sent one) — branch on that the way you would
|
|
240
|
+
on the exit code. `oxy_fn_call` and `oxy_checks_run` set `isError` when the
|
|
241
|
+
function or a check failed, with the full document still in the result.
|
|
242
|
+
`oxy_env_delete` needs `confirm: true`. `oxy_publish_sandbox` takes only a
|
|
243
|
+
`dev-<handle>` and cannot reach staging or production.
|
|
244
|
+
|
|
245
|
+
## A sandbox isolates the app, not the workspace
|
|
246
|
+
|
|
247
|
+
The semantic model, automations and Airway pipelines a sandbox reads are
|
|
248
|
+
`main`'s. To try a workspace branch, open a **workspace preview** (staff
|
|
249
|
+
only) — a separate loop:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
oxyc preview create <branch> --wait --workspace <id> --json # compile the branch; idempotent
|
|
253
|
+
oxyc preview checks <branch> --workspace <id> --json # Airway change check of that revision
|
|
254
|
+
oxyc preview run <branch> procedure <file.automation.yml> --wait --workspace <id> --json # held dry run
|
|
255
|
+
oxyc preview runs list <branch> --workspace <id> --json # every run, the server's own included
|
|
256
|
+
oxyc preview runs show <run-id> --wait --workspace <id> --json # one run: held steps, outcome
|
|
257
|
+
oxyc preview delete <branch> --yes --workspace <id> --json # done
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
`preview run`'s kind is `procedure` or `airway_sample`; never post a
|
|
261
|
+
`transform_build` or `compare` run — the server queues those itself, and you
|
|
262
|
+
read them with `preview runs list` / `runs show`. A `--wait` verb exits `1`
|
|
263
|
+
when what it waited for ended `failed` or `cancelled`, `7` past its deadline.
|
|
264
|
+
Over MCP the same verbs are `oxy_preview_create` / `_list` / `_show` /
|
|
265
|
+
`_delete` (`confirm: true`) / `_checks` / `_run` / `_runs_list` / `_run_show`.
|
|
266
|
+
To make a sandbox read the branch's semantic model, publish to it with
|
|
267
|
+
`--semantic-branch <branch>`.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Verify it in the browser (CI does not check a published bundle)
|
|
2
|
+
|
|
3
|
+
Nothing checks a bundle before it reaches users. `tsc` and `vite build` pass on
|
|
4
|
+
a card rendering a red error banner, a grid with ragged rows, a button wired to
|
|
5
|
+
nothing, or a component misreading its function's response shape. **The build is
|
|
6
|
+
not the check — driving the published app is.**
|
|
7
|
+
|
|
8
|
+
## An agent on a sandbox agent token cannot do this on a cloud target
|
|
9
|
+
|
|
10
|
+
And says so. The token signs no browser in. Your sandbox's page is on its own
|
|
11
|
+
host (the `url` from `env create`), behind a staff session, and `oxyc proxy`
|
|
12
|
+
cannot target a sandbox. Verify the functions in the sandbox, hand your
|
|
13
|
+
engineer the link, and list what on the page you could not check
|
|
14
|
+
([What to hand back](sandbox-agent-loop.md#what-to-hand-back)). The rest of
|
|
15
|
+
this file is what they do with that link.
|
|
16
|
+
|
|
17
|
+
## Drive the page
|
|
18
|
+
|
|
19
|
+
Use a browser-automation MCP server — the Playwright one (`@playwright/mcp`)
|
|
20
|
+
fits. After `oxyc publish`, for anything touching `src/**` or a function a
|
|
21
|
+
card calls:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
browser_navigate → the published /customer-apps/<org>/<slug>/ URL
|
|
25
|
+
browser_find → locate a control by its text, get its ref
|
|
26
|
+
browser_click → drive it
|
|
27
|
+
browser_evaluate → MEASURE (grid widths, computed styles, element counts)
|
|
28
|
+
browser_take_screenshot → look, and compare before/after for visual work
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Calling the function endpoint is not verifying the UI.** A bearer-token
|
|
32
|
+
`fetch` proves the handler returns JSON. It does not prove the card renders it,
|
|
33
|
+
that the button is wired, or that a timer stops when told — and it can mislead
|
|
34
|
+
outright: a bearer-token call and a browser session don't always resolve the
|
|
35
|
+
same app state, so such a check can "pass" against data the page never shows.
|
|
36
|
+
If the claim is about what a person sees, drive the page.
|
|
37
|
+
|
|
38
|
+
`browser_evaluate` is the underused one. Design questions are usually
|
|
39
|
+
measurable, and measuring beats squinting:
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
// Ragged rows? Sum each grid row's children against the container.
|
|
43
|
+
const grid = document.querySelector("main > div");
|
|
44
|
+
const rows = {};
|
|
45
|
+
for (const el of grid.children) {
|
|
46
|
+
const b = el.getBoundingClientRect();
|
|
47
|
+
(rows[Math.round(b.top)] ||= []).push(Math.round(b.width));
|
|
48
|
+
}
|
|
49
|
+
// any row summing well under the container width is a gap
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Two prerequisites that bite
|
|
53
|
+
|
|
54
|
+
- **The app is behind auth.** The browser must hold a session for the target
|
|
55
|
+
Oxy deployment — sign in there first.
|
|
56
|
+
- **`pnpm dev` against cloud data returns `origin not allowed`** — the data API
|
|
57
|
+
rejects a localhost origin for a published app. Drive the published URL, or
|
|
58
|
+
run the `oxyc proxy` sidecar
|
|
59
|
+
([reading-data.md](reading-data.md#working-locally-against-cloud-data-oxyc-proxy)).
|
|
60
|
+
|
|
61
|
+
## Check the whole page
|
|
62
|
+
|
|
63
|
+
Not just the card you changed. A shared component (a layout wrapper, a design
|
|
64
|
+
primitive) touches every card, and a section you didn't open is where the
|
|
65
|
+
regression hides.
|