@oxy-hq/cli 0.4.0 → 0.6.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 +407 -11
- package/dist/main.mjs +5207 -400
- package/dist/main.mjs.map +1 -1
- package/dist/mcp-D5FFpqAw.mjs +902 -0
- package/dist/mcp-D5FFpqAw.mjs.map +1 -0
- package/json-schemas/agentic.json +1 -1
- package/json-schemas/config.json +2 -4
- package/json-schemas/flow-test.json +3 -3
- package/package.json +8 -8
- package/skills/oxy-cli/SKILL.md +23 -0
- package/skills/oxy-custom-apps/SKILL.md +167 -3
- package/dist/mcp-CIsyezzU.mjs +0 -316
- package/dist/mcp-CIsyezzU.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
`oxyc` — a `gh api`-shaped client for the Oxy HTTP API, plus the tooling that
|
|
4
4
|
manages customer workspace repos.
|
|
5
5
|
|
|
6
|
-
- **API client** — `api`, `routes`, `schema`, `openapi`, `login`, `whoami`, `assume`
|
|
6
|
+
- **API client** — `api`, `routes`, `schema`, `openapi`, `login`, `whoami`, `assume`, `oltp`
|
|
7
7
|
- **Customer workspaces** — `list`, `new`, `import`, `doctor`, `update`, `adopt`, `launch`
|
|
8
|
-
- **Custom apps** — `publish`, `init-ci`, `proxy`
|
|
8
|
+
- **Custom apps** — `publish`, `init-ci`, `proxy`, `apps`
|
|
9
|
+
- **Checks** — `checks run`
|
|
10
|
+
- **Sandboxes** — `env`, `fn call`, `invocations`, `logs`
|
|
11
|
+
- **Workspace previews** — `preview`
|
|
9
12
|
- **Development** — `validate`, `mcp`, `guide`, `skills`
|
|
10
13
|
|
|
11
14
|
## Install
|
|
@@ -76,7 +79,10 @@ Credentials are stored **per deployment**, and the default is production. A
|
|
|
76
79
|
oxyc api <path> [flags]
|
|
77
80
|
```
|
|
78
81
|
|
|
79
|
-
`<path>` is relative to `/api`; a leading `/` or `api/` is accepted.
|
|
82
|
+
`<path>` is relative to `/api`; a leading `/` or `api/` is accepted. One
|
|
83
|
+
exception: `customer-apps/…` is sent as written, because `/customer-apps/<org>/<app>/…`
|
|
84
|
+
is where an app's bundle is served. The app registry API is `api/customer-apps/…`
|
|
85
|
+
— spell the `api/` out for it (a 404 on the bare form says so).
|
|
80
86
|
|
|
81
87
|
| Flag | Meaning |
|
|
82
88
|
| --- | --- |
|
|
@@ -110,9 +116,10 @@ the server.
|
|
|
110
116
|
`oxyc login`; `/external/api/**` sends `X-API-Key` from `$OXY_API_KEY`.
|
|
111
117
|
|
|
112
118
|
**`--paginate` is a heuristic.** Oxy sends `Link: rel="next"` on some endpoints
|
|
113
|
-
and nothing on others; where there is no header, `oxyc` reads
|
|
114
|
-
`pagination.has_next`, then `has_more`,
|
|
115
|
-
|
|
119
|
+
and nothing on others; where there is no header, `oxyc` reads `next_offset`
|
|
120
|
+
(and asks again with `?offset=`), then `pagination.has_next`, then `has_more`,
|
|
121
|
+
then `page < total_pages` (asking again with `?page=`). Nothing recognised
|
|
122
|
+
means one page, and it warns when the server said nothing at all.
|
|
116
123
|
|
|
117
124
|
```bash
|
|
118
125
|
oxyc api {org}/workspaces --md
|
|
@@ -172,6 +179,38 @@ it and `end` ends it in both. A staff 403 on a tenant surface is usually no
|
|
|
172
179
|
active session rather than a role problem; `assume status` answers "none"
|
|
173
180
|
without failing.
|
|
174
181
|
|
|
182
|
+
## OLTP databases
|
|
183
|
+
|
|
184
|
+
Staff can see and request an org's transactional Postgres — the store behind
|
|
185
|
+
`ctx.oltp` — without a shell on the server.
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
oxyc oltp status [--org <slug|uuid|url>] [--json]
|
|
189
|
+
oxyc oltp provision --org <slug|uuid|url> --writer app:<slug> [--writer pipeline:<source> …] [--yes] [--json]
|
|
190
|
+
oxyc oltp provision --org <slug|uuid|url> --branch staging [--yes] [--json]
|
|
191
|
+
oxyc oltp reset --org <slug|uuid|url> --branch staging [--yes] [--json]
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`status` lists every org, with a database or without; with `--org` it shows
|
|
195
|
+
that org's store and writers. No credential is ever printed.
|
|
196
|
+
|
|
197
|
+
`provision` prints what it will create and asks first, because it calls a paid
|
|
198
|
+
provider; without a terminal it refuses unless `--yes`. It is idempotent — an
|
|
199
|
+
existing database or writer is reconciled, not duplicated. `app:<slug>` is
|
|
200
|
+
derived the way the platform derives `ctx.oltp`'s schema (`app:store-ops` →
|
|
201
|
+
`app:store_ops`, schema `app_store_ops`); `pipeline:<source>` names
|
|
202
|
+
`raw_<source>`. A 404 can be an org outside your staff grant's scope, and an
|
|
203
|
+
active `assume` session closes `/admin`.
|
|
204
|
+
|
|
205
|
+
`--branch staging` also cuts the org's **staging branch**: one copy of the whole
|
|
206
|
+
database per org, which every app's staging environment writes to. It is made
|
|
207
|
+
only by this flag — never by a publish — and re-running it mints whatever a
|
|
208
|
+
writer added since the cut needs, without resetting anyone's data. `status`
|
|
209
|
+
shows its age and calls it stale past 30 days; nothing resets it on a timer.
|
|
210
|
+
`reset --branch staging` re-copies it from production, discarding every app's
|
|
211
|
+
staging data in the org, so it lists those apps and asks first (`--yes`
|
|
212
|
+
without a terminal).
|
|
213
|
+
|
|
175
214
|
## Customer workspaces
|
|
176
215
|
|
|
177
216
|
A customer **is** a GitHub repo carrying the `oxy-customer` topic. Tagging the
|
|
@@ -203,7 +242,7 @@ the current directory while granting access to the customer's repo.
|
|
|
203
242
|
## Custom apps
|
|
204
243
|
|
|
205
244
|
```bash
|
|
206
|
-
oxyc publish [--env <e>] [--dir <path>] [--promote] [--build-only | --prebuilt] [--json]
|
|
245
|
+
oxyc publish [--env <e>] [--dir <path>] [--promote] [--build-only | --prebuilt] [--json] [--allow-function-lint] [--app-env <dev-handle>]
|
|
207
246
|
oxyc init-ci [--app <org>/<app>] [--environment <name>] [--force]
|
|
208
247
|
oxyc proxy [--port <n>] [--allow-writes] [--allow-events] [--yes]
|
|
209
248
|
```
|
|
@@ -236,11 +275,309 @@ channel unless `--promote`. `--env` defaults to **production**; name it.
|
|
|
236
275
|
- `.env.local`, then `.env`, are read from the directory and its parents without
|
|
237
276
|
overriding the environment. The server's warnings print on stderr; `--json`
|
|
238
277
|
prints its result on stdout.
|
|
278
|
+
- **Function lint**: before the build, each declared function's source (and the
|
|
279
|
+
relative imports it reaches) is checked for what the host refuses at the first
|
|
280
|
+
call — a `ctx.*` call whose capability the manifest lacks, a global the isolate
|
|
281
|
+
does not have, a `ctx.warehouse` / `ctx.tx` write outside `destinations`; before
|
|
282
|
+
the upload, with the target's database list, an `upsert` or `ctx.tx` on an
|
|
283
|
+
engine that refuses it and a customer-warehouse write with no
|
|
284
|
+
`customerWarehouseWrites` reason. The rules are `validate`'s, below. A finding
|
|
285
|
+
fails the publish with exit `1` and names the file, line, call and fix; if the
|
|
286
|
+
target will not list the databases, the engine half prints one warning and is
|
|
287
|
+
skipped. **`--allow-function-lint`** is the way past a false positive: every
|
|
288
|
+
finding becomes a warning that names its rule — please open an issue with it.
|
|
239
289
|
|
|
240
290
|
**`init-ci`** writes `.github/workflows/oxy-publish.yml` at the repo root: a
|
|
241
291
|
`build` job (no id-token) that runs `publish --build-only`, and an
|
|
242
292
|
environment-gated `publish` job whose only work is `publish --prebuilt` with
|
|
243
|
-
OIDC.
|
|
293
|
+
OIDC. `--promote` makes that publish go live and adds a `checks run` step after
|
|
294
|
+
it, when the manifest declares a check. It prints the `oxyc api …/publishers`
|
|
295
|
+
call that registers the workflow.
|
|
296
|
+
|
|
297
|
+
**`--app-env <dev-handle>`** moves one sandbox's build pointer instead of the
|
|
298
|
+
draft/live channel — see [Sandboxes](#sandboxes). It needs a staff credential
|
|
299
|
+
(never a publish token or OIDC) and is refused together with `--promote`: a
|
|
300
|
+
sandbox build is never promoted, so the loop is publish to the sandbox,
|
|
301
|
+
iterate, then publish the same tree to staging and promote that.
|
|
302
|
+
|
|
303
|
+
## Apps
|
|
304
|
+
|
|
305
|
+
Read-only: every request these make is a GET, against `/api/customer-apps/**`
|
|
306
|
+
(the app-admin role). `<app>` is `<org-slug>/<app-slug>` or an app UUID.
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
oxyc apps list [--org <slug>] [--published | --draft] [--builds] [--json]
|
|
310
|
+
oxyc apps show <app> [--json]
|
|
311
|
+
oxyc apps builds <app> [--json]
|
|
312
|
+
oxyc apps health [<app>] [--needs-attention] [--json]
|
|
313
|
+
oxyc apps usage <app> [--json]
|
|
314
|
+
oxyc apps drift [<app>] [--org <slug>] [--dir <path>] [--refresh] [--json]
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
- **`list`** — every app you can see, across organizations, walking the paged
|
|
318
|
+
registry to its end. `STATE` is `live` when the app has a published build and
|
|
319
|
+
`draft` when it has none. `SOURCE` is the repository the app is *registered*
|
|
320
|
+
against. `--builds` adds the live build's id, the repository and commit it was
|
|
321
|
+
built from, who published it and when — one extra request per app, six at a
|
|
322
|
+
time.
|
|
323
|
+
- **`show`** — one app on one screen: the registry row, the live build, the
|
|
324
|
+
draft build when it is newer than the live one, deployment integrity,
|
|
325
|
+
availability, and the 7-day usage summary. `last promote` is when the promote
|
|
326
|
+
or rollback action was last used; `oxyc publish --promote` does not update it,
|
|
327
|
+
so it can be older than the live build.
|
|
328
|
+
- **`builds`** — the build history, newest first. `CHANNEL` marks the build each
|
|
329
|
+
channel points at: `live`, `draft`, or both for one build.
|
|
330
|
+
- **`health`** — with no argument, the fleet table (published apps only):
|
|
331
|
+
`down`, `degraded`, `not_measured`, `quiet` or `operational` per app, with the
|
|
332
|
+
totals on stderr. `--needs-attention` keeps the first three. With `<app>`: the
|
|
333
|
+
deployment-integrity checks, the availability windows, and the browser errors
|
|
334
|
+
of the last 24 hours.
|
|
335
|
+
- **`usage`** — the 7-day summary, the visitors, and the tracked events by name.
|
|
336
|
+
- **`drift`** — see below.
|
|
337
|
+
|
|
338
|
+
Times are UTC. `--json` prints one document on stdout; a table goes to stdout and
|
|
339
|
+
the count line to stderr.
|
|
340
|
+
|
|
341
|
+
**An empty result and a failed request are different answers.** No apps, no
|
|
342
|
+
builds, no errors recorded: the command says so and exits `0`. A request that
|
|
343
|
+
failed exits with the code its status maps to (`4`, `5`, `7`, …). Where a
|
|
344
|
+
command makes several independent requests (`show`, `health <app>`,
|
|
345
|
+
`list --builds`, `drift`), it prints what it did read, marks what it did not as
|
|
346
|
+
`NOT READ` (`"failed": […]` / `live_build_error` in `--json`), and then exits
|
|
347
|
+
non-zero. These commands report state: a failing health check, a `down` app or
|
|
348
|
+
a drifted app is an answer and exits `0` — read it from the output.
|
|
349
|
+
|
|
350
|
+
The health route answers **503 with its report** when a check fails; `oxyc`
|
|
351
|
+
prints that as a failing report, not as an outage.
|
|
352
|
+
|
|
353
|
+
### `apps drift`
|
|
354
|
+
|
|
355
|
+
Compares what is live against source on **this machine**. For each app it takes
|
|
356
|
+
the live build's recorded repository and commit, finds a local checkout of that
|
|
357
|
+
repository (the ones `oxyc repos` and `oxyc path` report, or `--dir`), finds the
|
|
358
|
+
app's directory by the `oxy-app.json` whose `slug` and `orgSlug` match — at
|
|
359
|
+
`<app>/oxy-app.json` or `<app>/public/oxy-app.json`, never by folder name — and
|
|
360
|
+
counts the commits on the checkout's **current branch** that touch that
|
|
361
|
+
directory after the published commit.
|
|
362
|
+
|
|
363
|
+
| Result | Meaning |
|
|
364
|
+
| --- | --- |
|
|
365
|
+
| `in sync` | no commit on the current branch touches the app directory after the published commit |
|
|
366
|
+
| `N commits ahead` | that many do; they are listed |
|
|
367
|
+
| `unknown — <reason>` | the comparison could not be made |
|
|
368
|
+
|
|
369
|
+
`unknown` is never reported as `in sync`. Its reasons (`reason` in `--json`):
|
|
370
|
+
|
|
371
|
+
| `reason` | |
|
|
372
|
+
| --- | --- |
|
|
373
|
+
| `not_published` | nothing is live |
|
|
374
|
+
| `source_unrecorded` | the live build records no repository or no commit |
|
|
375
|
+
| `repo_not_checked_out` | the repository is not on this machine — clone it, or pass `--dir` |
|
|
376
|
+
| `commit_not_in_checkout` | the checkout does not have the published commit — `git fetch` there |
|
|
377
|
+
| `app_dir_not_found` | no tracked `oxy-app.json` declares this `slug` and `orgSlug` |
|
|
378
|
+
| `app_dir_ambiguous` | more than one does |
|
|
379
|
+
| `commit_not_on_branch` | the published commit is not an ancestor of the checked-out branch |
|
|
380
|
+
| `working_tree_dirty` | the app directory has uncommitted or untracked files |
|
|
381
|
+
| `git_failed` | `git log` or `git status` failed in the checkout, so the comparison could not be read |
|
|
382
|
+
| `request_failed` | the builds request failed (the command then exits non-zero) |
|
|
383
|
+
|
|
384
|
+
It runs read-only git commands and nothing else: it does not fetch, check out,
|
|
385
|
+
or refresh the index. The answer is therefore about the checkout as it is on
|
|
386
|
+
disk — a branch that is behind its remote reports fewer commits than the remote
|
|
387
|
+
has. With no `<app>`, every published app is compared, and `--dir` applies to
|
|
388
|
+
the apps whose source repository is that checkout's `origin`; with `<app>`,
|
|
389
|
+
`--dir` is used whatever its remote is.
|
|
390
|
+
|
|
391
|
+
An app published to two organizations from one directory (a staging org and a
|
|
392
|
+
production org) matches the manifest for only one of them; the other reports
|
|
393
|
+
`app_dir_not_found` and names the manifest it found with the other `orgSlug`.
|
|
394
|
+
|
|
395
|
+
## Checks
|
|
396
|
+
|
|
397
|
+
```bash
|
|
398
|
+
oxyc checks run <org-slug>/<app-slug> # or an app UUID
|
|
399
|
+
oxyc checks run acme/dashboards --json --timeout 60
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Runs every function the app declared `"check": true` (in `oxy-app.json`), one
|
|
403
|
+
POST per check to start its run, then polls each run to a terminal status —
|
|
404
|
+
`done`, `failed`, `cancelled`, or a client-side `timed_out` past `--timeout`
|
|
405
|
+
seconds (default 300, per check). A check **passes** when its run finishes
|
|
406
|
+
`done` and its answer does not parse to an object with `ok: false`.
|
|
407
|
+
|
|
408
|
+
Human mode prints one line per check to stderr as it lands (`✓ canary 4.1s` /
|
|
409
|
+
`✗ canary failed: <reason>`), suppressed by `--quiet`. `--json` prints one
|
|
410
|
+
object to stdout instead:
|
|
411
|
+
|
|
412
|
+
```json
|
|
413
|
+
{ "app": "acme/dashboards", "appId": "…", "checks": [{ "name": "canary", "runId": "…", "status": "done", "passed": true, "durationMs": 4123 }] }
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**Auth** follows the same rule as `oxyc api`'s bearer surface, with one
|
|
417
|
+
difference: `checks run` talks to `/api/admin/**`, which is not the
|
|
418
|
+
`/external/api/*` surface, so a configured API key is sent explicitly as
|
|
419
|
+
`X-API-Key` only when **no bearer resolves** — never both. A bearer from
|
|
420
|
+
`oxyc login` (or `--token-env`) always wins when one is present.
|
|
421
|
+
|
|
422
|
+
**In CI, nothing is stored.** With neither a token nor an API key set, a job
|
|
423
|
+
holding `id-token: write` exchanges its GitHub OIDC token for the same
|
|
424
|
+
short-lived, app-scoped publish credential `oxyc publish` uses, and drives
|
|
425
|
+
`/api/customer-apps/**` instead — the same three handlers, mounted where a
|
|
426
|
+
publish token may reach them. The exchange returns the app id, so no lookup is
|
|
427
|
+
needed, and `<app>` must be `<org-slug>/<app-slug>` in this mode (a UUID names
|
|
428
|
+
an app the exchange cannot verify a publisher for). A publish token set in
|
|
429
|
+
`OXY_TOKEN` takes the same surface without minting.
|
|
430
|
+
|
|
431
|
+
What that credential may do is deliberately small, and enforced server-side
|
|
432
|
+
rather than by the client. An app-scoped token is **confined to its app**: it
|
|
433
|
+
reaches `/customer-apps/{its own id}/…` and the upload, and answers `404` for
|
|
434
|
+
everything else on that surface — another app's id, the registry listing, and
|
|
435
|
+
the fleet-wide rollups. That is the same answer an out-of-scope App Operator
|
|
436
|
+
gets, so it cannot learn which apps exist. Within its app it may publish, read,
|
|
437
|
+
and run a function **the manifest marks `"check": true`**; a function without
|
|
438
|
+
the flag is `403`. The
|
|
439
|
+
argument for allowing the run at all is narrow: a token that may publish
|
|
440
|
+
arbitrary code to an app can already cause anything that app can, so running
|
|
441
|
+
that app's own declared checks adds nothing. It would stop being true the moment
|
|
442
|
+
it admitted any function.
|
|
443
|
+
|
|
444
|
+
**A check runs against the app's live build** — the server resolves
|
|
445
|
+
`published_build_id`, else `draft_build_id`, for the pre-flight lookup and for
|
|
446
|
+
the execution alike. So `checks run` after a plain `oxyc publish` verifies the
|
|
447
|
+
*previously promoted* code, not the draft just uploaded. `oxyc init-ci` writes
|
|
448
|
+
the step only with `--promote`, where the build just published is the one the
|
|
449
|
+
check runs, and only when the manifest declares a check. **With `--app-env`**
|
|
450
|
+
it runs against that environment's own build instead — a sandbox's own
|
|
451
|
+
publish, or staging's — and the report gains `environment` and each check's
|
|
452
|
+
`invocationId`; the publish-token and OIDC paths are unchanged and work only
|
|
453
|
+
against production, since every non-production operation needs a staff
|
|
454
|
+
credential (see [Sandboxes](#sandboxes)).
|
|
455
|
+
|
|
456
|
+
With a publish token, `<app>` must be the **UUID** — resolving a slug means
|
|
457
|
+
listing every app, which such a token may not do. The OIDC path needs no id at
|
|
458
|
+
all (the exchange returns it) but does need `<org-slug>/<app-slug>`, since the
|
|
459
|
+
exchange is registered by slug.
|
|
460
|
+
|
|
461
|
+
**Exit codes:** `0` every check passed; `9` (`CHECK_FAILED`) at least one check
|
|
462
|
+
failed or timed out; `1` the app declares no checks; `5` the app was not
|
|
463
|
+
found; `4` no credential resolved, or the API rejected it (401/403 — an
|
|
464
|
+
expired 90-day API key reads this way, not as a missing one); `2` a bad
|
|
465
|
+
`--timeout`, or a slug where the credential needs a UUID; `7` (`UNAVAILABLE`)
|
|
466
|
+
the deployment answered the OIDC exchange without an `app_id`, meaning it
|
|
467
|
+
predates trusted checks.
|
|
468
|
+
|
|
469
|
+
## Sandboxes
|
|
470
|
+
|
|
471
|
+
```bash
|
|
472
|
+
oxyc env create <app> <name> # name is dev-<handle>, 1-12 chars a-z0-9-
|
|
473
|
+
oxyc env list <app>
|
|
474
|
+
oxyc env show <app> <name>
|
|
475
|
+
oxyc env delete <app> <name> [--yes] [--wait [seconds]]
|
|
476
|
+
|
|
477
|
+
oxyc publish --app-env <name> [--env <e>] # see "Custom apps" above
|
|
478
|
+
oxyc fn call <app> <function> [--app-env <name>] [--data <json|@file|->]
|
|
479
|
+
oxyc checks run <app> --app-env <name> # see "Checks" above
|
|
480
|
+
|
|
481
|
+
oxyc invocations list <app> [--app-env] [--build] [--function] [--limit]
|
|
482
|
+
oxyc invocations held <app> <invocation-id>
|
|
483
|
+
oxyc logs <app> [--app-env] [--invocation] [--request] [--hours] [--limit]
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
A **sandbox** is a named, short-lived `dev-<handle>` environment of one custom
|
|
487
|
+
app: its own build pointer, storage silo, secrets, Airhouse sibling and — in
|
|
488
|
+
an org with an OLTP staging branch — its own copy of the app's database, with
|
|
489
|
+
reads hitting production and most writes either isolated or *held* (recorded,
|
|
490
|
+
not performed — see `invocations held`). It starts with no build, is deleted
|
|
491
|
+
explicitly or after 7 idle days, and an app has at most 20. Full contract,
|
|
492
|
+
including what is **not** isolated (`ctx.warehouse`) and the other gaps:
|
|
493
|
+
`internal-docs/custom-app-sandboxes.md`.
|
|
494
|
+
|
|
495
|
+
`ctx.oltp` in a sandbox runs in a schema of its own (`app_<app>__dev_<handle>`)
|
|
496
|
+
inside the org's staging branch. `publish --app-env` queues it: a copy of
|
|
497
|
+
staging's tables (rows up to a size cap), then the build's OLTP migrations.
|
|
498
|
+
`env show` reports it as `oltp_schema` — `status` is `seeding`, `ready`,
|
|
499
|
+
`failed` (with `error`) or `stale` (the branch was reset; publish again), and
|
|
500
|
+
`structure_only` lists the tables copied empty. Until it is `ready` a
|
|
501
|
+
`ctx.oltp` call in the sandbox is refused with a message that says which.
|
|
502
|
+
|
|
503
|
+
The loop: `env create` → `publish --app-env` → `fn call` / `checks run
|
|
504
|
+
--app-env` → `invocations list` / `held` to read back what ran → iterate from
|
|
505
|
+
`publish` → `env delete --yes --wait` when done. `oxyc guide` has the same six
|
|
506
|
+
lines, meant to sit in an agent's context.
|
|
507
|
+
|
|
508
|
+
**`--app-env <environment>`** (`production`, `staging` or `dev-<handle>`,
|
|
509
|
+
validated client-side — exit `2` on a malformed name) is a *different axis*
|
|
510
|
+
from `--env`, which is the deployment: both can appear on the same command
|
|
511
|
+
(`oxyc fn call acme/store f --env dev --app-env dev-a1`). It is not a global
|
|
512
|
+
flag — only `publish`, `fn call`, `checks run`, `invocations list` and `logs`
|
|
513
|
+
take it. A publish-token credential is refused before any request for
|
|
514
|
+
`--app-env` other than `production` (exit `2`); `env`, `invocations` and
|
|
515
|
+
`logs` refuse one outright, for any environment, because sandbox management
|
|
516
|
+
is a staff console surface.
|
|
517
|
+
|
|
518
|
+
**`env delete`** without `--yes` asks on a terminal and exits `8` (refused)
|
|
519
|
+
off one, same as `oltp reset`. `--wait [seconds]` (default 120) polls until
|
|
520
|
+
the teardown finishes and prints `{"name","status":"deleted"}`; past the
|
|
521
|
+
deadline it exits `7` (retryable) rather than hanging.
|
|
522
|
+
|
|
523
|
+
**`fn call`** POSTs directly to `{target}/customer-apps/{org}/{app}/fn/{name}`
|
|
524
|
+
(no `/api`) — the response is Server-Sent Events, not JSON, so `oxyc` parses
|
|
525
|
+
the `log` / `data` / `done` / `error` frames itself. It exits `0` only when
|
|
526
|
+
the stream ends `done` with a 2xx status; a function that threw, or answered a
|
|
527
|
+
non-2xx, is a function-level failure (exit `1`, `ok: false` in the JSON), kept
|
|
528
|
+
distinct from a non-2xx on the POST itself (exit per the usual status mapping
|
|
529
|
+
— e.g. `EnvironmentHasNoBuild` before the stream starts). The
|
|
530
|
+
`x-oxy-invocation-id` response header becomes `invocationId` in the result.
|
|
531
|
+
|
|
532
|
+
**`invocations held`** is what the non-production write policy did *not* do —
|
|
533
|
+
production's would-be effect, recorded rather than performed. It is `[]` for a
|
|
534
|
+
production invocation, since nothing is held there.
|
|
535
|
+
|
|
536
|
+
## Workspace previews
|
|
537
|
+
|
|
538
|
+
```bash
|
|
539
|
+
oxyc preview create <branch> [--wait [seconds]] [--json]
|
|
540
|
+
oxyc preview list [--json]
|
|
541
|
+
oxyc preview show <branch> [--json]
|
|
542
|
+
oxyc preview delete <branch> [--yes] [--json]
|
|
543
|
+
oxyc preview checks <branch> [--json]
|
|
544
|
+
|
|
545
|
+
oxyc preview run <branch> <kind> <ref> [--variables <json|@file|->] [--read-live-only]
|
|
546
|
+
[--window-from <iso>] [--window-to <iso>] [--resource <name>...]
|
|
547
|
+
[--wait [seconds]] [--json]
|
|
548
|
+
oxyc preview runs list <branch> [--json]
|
|
549
|
+
oxyc preview runs show <run-id> [--wait [seconds]] [--json]
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
A **preview** opens a workspace branch on the real product, against real data,
|
|
553
|
+
without the branch being live — staff only. `create` compiles the branch's
|
|
554
|
+
head into a staging revision (or reuses a ready one for that commit) and is
|
|
555
|
+
idempotent; `--wait [seconds]` (default 120) blocks until the compile is
|
|
556
|
+
`ready` or `failed` instead of returning the `compiling` item the request
|
|
557
|
+
itself answers with, and exits `7` (retryable) past the deadline. There is no
|
|
558
|
+
single-preview route: `show` fetches `list` and filters it client-side, so its
|
|
559
|
+
`5` (not found) is synthesized here, not a literal 404.
|
|
560
|
+
|
|
561
|
+
The loop: `preview create --wait` → `preview checks` → `preview run` →
|
|
562
|
+
`preview runs show --wait` → iterate → `preview delete --yes` when done.
|
|
563
|
+
|
|
564
|
+
**`<kind>`** is `procedure` or `airway_sample` — the only two `POST
|
|
565
|
+
/previews/runs` accepts (exit `2` client-side on anything else, naming the
|
|
566
|
+
read-back verb). `transform_build` and `compare` runs are queued by the
|
|
567
|
+
server's own Airway change check, never started here; `preview runs list`
|
|
568
|
+
surfaces them alongside the kinds you *can* start, and `preview runs show
|
|
569
|
+
<run-id> --wait` reads any of the four back — a procedure's held steps, a
|
|
570
|
+
transform build's or compare's outcome, or a sample's ask and result.
|
|
571
|
+
`--variables` / `--read-live-only` are `procedure`-only; `--window-from` /
|
|
572
|
+
`--window-to` / `--resource` (repeatable) are `airway_sample`-only.
|
|
573
|
+
|
|
574
|
+
Every verb needs `{workspace}` resolved — `--workspace <id>`, exactly the way
|
|
575
|
+
`oxyc api {workspace}/...` resolves it; there is no second workspace
|
|
576
|
+
resolver. A deployment with `OXY_PREVIEW_RUNS` off answers every runs route
|
|
577
|
+
`404 preview_runs_disabled`, surfaced the same way any other server refusal
|
|
578
|
+
is: the server's own `code` rides along on the thrown error, and the exit
|
|
579
|
+
class follows the usual HTTP-status mapping (`400`/`409` → `6`, `404` → `5`).
|
|
580
|
+
Full contract: `internal-docs/workspace-previews.md`.
|
|
244
581
|
|
|
245
582
|
## Development commands
|
|
246
583
|
|
|
@@ -267,6 +604,51 @@ generated from the Rust config types. No network, no token.
|
|
|
267
604
|
Structural checks only. `oxy validate` additionally resolves `databases:` and
|
|
268
605
|
`llm.ref` against the loaded workspace, and wins where the two disagree.
|
|
269
606
|
|
|
607
|
+
Every **`oxy-app.json`** it finds is checked for data placement — the shape of
|
|
608
|
+
the data picks the store: facts and history in Airhouse (`ctx.airhouse`),
|
|
609
|
+
records in OLTP (`ctx.oltp`), files in `ctx.storage`, customer warehouses
|
|
610
|
+
read-only. These fail the run:
|
|
611
|
+
|
|
612
|
+
- `customerWarehouseWrites` that is not `{ "<database>": "<reason>" }`, or names
|
|
613
|
+
a database missing from the function's `destinations`;
|
|
614
|
+
- `"airhouse": { "enabled": true }` in an app whose slug cannot derive a writer
|
|
615
|
+
(`-` → `_`, 1–56 of `[a-z0-9_]`, starting with a letter; no `_` in the slug);
|
|
616
|
+
- `airhouseMigrations` without a `dir`, with one that does not exist, or with
|
|
617
|
+
`migrations.dir`'s; and in its `*.sql`, `PRIMARY KEY`, `UNIQUE`,
|
|
618
|
+
`CREATE INDEX`, `REFERENCES` or `FOREIGN KEY` (DuckLake has none), or a
|
|
619
|
+
`CREATE`/`ALTER`/`DROP`/`INSERT`/`UPDATE`/`DELETE`/`COMMENT ON` target not
|
|
620
|
+
written as `app_<writer>.<name>`.
|
|
621
|
+
|
|
622
|
+
Warnings print on stderr and do not fail: each customer-warehouse exception,
|
|
623
|
+
`CREATE SCHEMA` in a migration, and `ctx.secrets.set` holding state (a
|
|
624
|
+
`JSON.stringify` value, or a state-like key that is not a credential's). The SQL
|
|
625
|
+
checks are lexical; the server's parser is the authority.
|
|
626
|
+
|
|
627
|
+
Each declared **Oxy Function** is then linted — its entry and every relative
|
|
628
|
+
import it reaches, comments and strings masked — for the mistakes that work
|
|
629
|
+
locally and fail closed in production. These fail the run, each naming the
|
|
630
|
+
file, the line, the call and the fix:
|
|
631
|
+
|
|
632
|
+
- `capability` — a `ctx.<area>.<op>` call whose manifest capability the function
|
|
633
|
+
does not declare: `ctx.secrets.set` → `secrets.write`, `ctx.email.send` →
|
|
634
|
+
`email.send`, `ctx.org.*` → `org.read`, `ctx.storage.*` → `storage.read` /
|
|
635
|
+
`storage.write` (`copy` needs both), `ctx.oltp.*` → `oltp.enabled`,
|
|
636
|
+
`ctx.airhouse.*` → `airhouse.enabled`. The map is
|
|
637
|
+
`src/publish/capabilities.ts`, held to the host's `FunctionCapabilities` by a
|
|
638
|
+
Rust test.
|
|
639
|
+
- `isolate-global` — a value use of `Buffer`, `TextEncoder`, `TextDecoder`,
|
|
640
|
+
`Blob`, `File`, `FormData`, `crypto.subtle` or `process.*`: the isolate is bare
|
|
641
|
+
`deno_core` and each is a `ReferenceError` at runtime. A name the file declares
|
|
642
|
+
itself (`const Buffer = …`, `import { TextEncoder } from "./polyfill"`) is not
|
|
643
|
+
one; nor is a `typeof` guard or a type annotation.
|
|
644
|
+
- `destinations` — a `ctx.warehouse.insert` / `exec` / `upsert` or `ctx.tx` in a
|
|
645
|
+
function with no `destinations`, or naming a database not in them.
|
|
646
|
+
|
|
647
|
+
The engine half — `upsert` off Postgres / DuckDB, `ctx.tx` off Postgres, a
|
|
648
|
+
customer-warehouse write with no `customerWarehouseWrites` reason, a write to
|
|
649
|
+
the org's OLTP store — needs the database list, which only the server has;
|
|
650
|
+
`validate` says so and `oxyc publish` runs it before the upload.
|
|
651
|
+
|
|
270
652
|
**`proxy`** forwards a local dev server's Oxy calls to a cloud target with your
|
|
271
653
|
login token attached. Defaults: side-effecting calls are **held**, tracking
|
|
272
654
|
events are **dropped**, auth endpoints reach the backend unauthenticated so
|
|
@@ -275,9 +657,22 @@ production target (any host under `oxygen-hq.com`) is refused without `--yes`.
|
|
|
275
657
|
Each Oxy Function call carries a W3C trace — the SDK's, or one minted here — and
|
|
276
658
|
prints `↳ <status> fn <name> request_id=… trace_id=…`.
|
|
277
659
|
|
|
278
|
-
**`mcp`** serves the API over stdio as four tools — `oxy_routes`,
|
|
279
|
-
`oxy_request`, `oxy_whoami` — rather than one per endpoint, so
|
|
280
|
-
cost ~2 KB per turn and reach endpoints added after this
|
|
660
|
+
**`mcp`** serves the API over stdio as four generic tools — `oxy_routes`,
|
|
661
|
+
`oxy_schema`, `oxy_request`, `oxy_whoami` — rather than one per endpoint, so
|
|
662
|
+
those schemas cost ~2 KB per turn and reach endpoints added after this
|
|
663
|
+
package shipped. Two named exceptions trade that minimalism for validation an
|
|
664
|
+
agent needs to drive a loop unsupervised: the **sandbox loop**
|
|
665
|
+
(`oxy_env_create`/`list`/`show`/`delete`, `oxy_publish_sandbox`,
|
|
666
|
+
`oxy_fn_call`, `oxy_checks_run`, `oxy_invocations_list`/`held`, `oxy_logs`)
|
|
667
|
+
and **workspace previews** (`oxy_preview_create`/`list`/`show`/`delete`/
|
|
668
|
+
`checks`/`run`/`runs_list`/`run_show`). Each tool calls its CLI verb's own
|
|
669
|
+
request-building function, so a tool's result is the same JSON document that
|
|
670
|
+
verb's `--json` prints, and a failure carries the server's own error code
|
|
671
|
+
plus the exit-code class (`[exit 5 NOT_FOUND]`) the way `oxyc`'s exit code
|
|
672
|
+
would. `oxy_env_delete` / `oxy_preview_delete` need `confirm: true` (there is
|
|
673
|
+
no terminal to ask on inside an MCP server), and `oxy_publish_sandbox`
|
|
674
|
+
requires a `dev-<handle>` `appEnv` and refuses production, staging or a
|
|
675
|
+
promote client-side — it can never reach the live channel.
|
|
281
676
|
|
|
282
677
|
```bash
|
|
283
678
|
claude mcp add oxyc -- npx -y @oxy-hq/cli mcp --env production
|
|
@@ -299,6 +694,7 @@ once npm reclaims it.
|
|
|
299
694
|
6 the request was malformed (4xx other than 401/403/404)
|
|
300
695
|
7 unavailable — 5xx, a timeout, or the network failed. Retryable.
|
|
301
696
|
8 refused — the operation would have destroyed or overwritten something
|
|
697
|
+
9 a check ran and failed or timed out (`oxyc checks run`)
|
|
302
698
|
```
|
|
303
699
|
|
|
304
700
|
`4` almost always means the wrong `--env`. `7` is worth retrying; `6` never is.
|