@oxy-hq/cli 0.4.0 → 0.5.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 +153 -3
- package/dist/main.mjs +2109 -183
- package/dist/main.mjs.map +1 -1
- package/json-schemas/config.json +2 -4
- package/package.json +7 -7
- package/skills/oxy-custom-apps/SKILL.md +69 -3
package/README.md
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
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
8
|
- **Custom apps** — `publish`, `init-ci`, `proxy`
|
|
9
|
+
- **Checks** — `checks run`
|
|
9
10
|
- **Development** — `validate`, `mcp`, `guide`, `skills`
|
|
10
11
|
|
|
11
12
|
## Install
|
|
@@ -172,6 +173,27 @@ it and `end` ends it in both. A staff 403 on a tenant surface is usually no
|
|
|
172
173
|
active session rather than a role problem; `assume status` answers "none"
|
|
173
174
|
without failing.
|
|
174
175
|
|
|
176
|
+
## OLTP databases
|
|
177
|
+
|
|
178
|
+
Staff can see and request an org's transactional Postgres — the store behind
|
|
179
|
+
`ctx.oltp` — without a shell on the server.
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
oxyc oltp status [--org <slug|uuid|url>] [--json]
|
|
183
|
+
oxyc oltp provision --org <slug|uuid|url> --writer app:<slug> [--writer pipeline:<source> …] [--yes] [--json]
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`status` lists every org, with a database or without; with `--org` it shows
|
|
187
|
+
that org's store and writers. No credential is ever printed.
|
|
188
|
+
|
|
189
|
+
`provision` prints what it will create and asks first, because it calls a paid
|
|
190
|
+
provider; without a terminal it refuses unless `--yes`. It is idempotent — an
|
|
191
|
+
existing database or writer is reconciled, not duplicated. `app:<slug>` is
|
|
192
|
+
derived the way the platform derives `ctx.oltp`'s schema (`app:store-ops` →
|
|
193
|
+
`app:store_ops`, schema `app_store_ops`); `pipeline:<source>` names
|
|
194
|
+
`raw_<source>`. A 404 can be an org outside your staff grant's scope, and an
|
|
195
|
+
active `assume` session closes `/admin`.
|
|
196
|
+
|
|
175
197
|
## Customer workspaces
|
|
176
198
|
|
|
177
199
|
A customer **is** a GitHub repo carrying the `oxy-customer` topic. Tagging the
|
|
@@ -203,7 +225,7 @@ the current directory while granting access to the customer's repo.
|
|
|
203
225
|
## Custom apps
|
|
204
226
|
|
|
205
227
|
```bash
|
|
206
|
-
oxyc publish [--env <e>] [--dir <path>] [--promote] [--build-only | --prebuilt] [--json]
|
|
228
|
+
oxyc publish [--env <e>] [--dir <path>] [--promote] [--build-only | --prebuilt] [--json] [--allow-function-lint]
|
|
207
229
|
oxyc init-ci [--app <org>/<app>] [--environment <name>] [--force]
|
|
208
230
|
oxyc proxy [--port <n>] [--allow-writes] [--allow-events] [--yes]
|
|
209
231
|
```
|
|
@@ -236,11 +258,93 @@ channel unless `--promote`. `--env` defaults to **production**; name it.
|
|
|
236
258
|
- `.env.local`, then `.env`, are read from the directory and its parents without
|
|
237
259
|
overriding the environment. The server's warnings print on stderr; `--json`
|
|
238
260
|
prints its result on stdout.
|
|
261
|
+
- **Function lint**: before the build, each declared function's source (and the
|
|
262
|
+
relative imports it reaches) is checked for what the host refuses at the first
|
|
263
|
+
call — a `ctx.*` call whose capability the manifest lacks, a global the isolate
|
|
264
|
+
does not have, a `ctx.warehouse` / `ctx.tx` write outside `destinations`; before
|
|
265
|
+
the upload, with the target's database list, an `upsert` or `ctx.tx` on an
|
|
266
|
+
engine that refuses it and a customer-warehouse write with no
|
|
267
|
+
`customerWarehouseWrites` reason. The rules are `validate`'s, below. A finding
|
|
268
|
+
fails the publish with exit `1` and names the file, line, call and fix; if the
|
|
269
|
+
target will not list the databases, the engine half prints one warning and is
|
|
270
|
+
skipped. **`--allow-function-lint`** is the way past a false positive: every
|
|
271
|
+
finding becomes a warning that names its rule — please open an issue with it.
|
|
239
272
|
|
|
240
273
|
**`init-ci`** writes `.github/workflows/oxy-publish.yml` at the repo root: a
|
|
241
274
|
`build` job (no id-token) that runs `publish --build-only`, and an
|
|
242
275
|
environment-gated `publish` job whose only work is `publish --prebuilt` with
|
|
243
|
-
OIDC.
|
|
276
|
+
OIDC. `--promote` makes that publish go live and adds a `checks run` step after
|
|
277
|
+
it, when the manifest declares a check. It prints the `oxyc api …/publishers`
|
|
278
|
+
call that registers the workflow.
|
|
279
|
+
|
|
280
|
+
## Checks
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
oxyc checks run <org-slug>/<app-slug> # or an app UUID
|
|
284
|
+
oxyc checks run acme/dashboards --json --timeout 60
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Runs every function the app declared `"check": true` (in `oxy-app.json`), one
|
|
288
|
+
POST per check to start its run, then polls each run to a terminal status —
|
|
289
|
+
`done`, `failed`, `cancelled`, or a client-side `timed_out` past `--timeout`
|
|
290
|
+
seconds (default 300, per check). A check **passes** when its run finishes
|
|
291
|
+
`done` and its answer does not parse to an object with `ok: false`.
|
|
292
|
+
|
|
293
|
+
Human mode prints one line per check to stderr as it lands (`✓ canary 4.1s` /
|
|
294
|
+
`✗ canary failed: <reason>`), suppressed by `--quiet`. `--json` prints one
|
|
295
|
+
object to stdout instead:
|
|
296
|
+
|
|
297
|
+
```json
|
|
298
|
+
{ "app": "acme/dashboards", "appId": "…", "checks": [{ "name": "canary", "runId": "…", "status": "done", "passed": true, "durationMs": 4123 }] }
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
**Auth** follows the same rule as `oxyc api`'s bearer surface, with one
|
|
302
|
+
difference: `checks run` talks to `/api/admin/**`, which is not the
|
|
303
|
+
`/external/api/*` surface, so a configured API key is sent explicitly as
|
|
304
|
+
`X-API-Key` only when **no bearer resolves** — never both. A bearer from
|
|
305
|
+
`oxyc login` (or `--token-env`) always wins when one is present.
|
|
306
|
+
|
|
307
|
+
**In CI, nothing is stored.** With neither a token nor an API key set, a job
|
|
308
|
+
holding `id-token: write` exchanges its GitHub OIDC token for the same
|
|
309
|
+
short-lived, app-scoped publish credential `oxyc publish` uses, and drives
|
|
310
|
+
`/api/customer-apps/**` instead — the same three handlers, mounted where a
|
|
311
|
+
publish token may reach them. The exchange returns the app id, so no lookup is
|
|
312
|
+
needed, and `<app>` must be `<org-slug>/<app-slug>` in this mode (a UUID names
|
|
313
|
+
an app the exchange cannot verify a publisher for). A publish token set in
|
|
314
|
+
`OXY_TOKEN` takes the same surface without minting.
|
|
315
|
+
|
|
316
|
+
What that credential may do is deliberately small, and enforced server-side
|
|
317
|
+
rather than by the client. An app-scoped token is **confined to its app**: it
|
|
318
|
+
reaches `/customer-apps/{its own id}/…` and the upload, and answers `404` for
|
|
319
|
+
everything else on that surface — another app's id, the registry listing, and
|
|
320
|
+
the fleet-wide rollups. That is the same answer an out-of-scope App Operator
|
|
321
|
+
gets, so it cannot learn which apps exist. Within its app it may publish, read,
|
|
322
|
+
and run a function **the manifest marks `"check": true`**; a function without
|
|
323
|
+
the flag is `403`. The
|
|
324
|
+
argument for allowing the run at all is narrow: a token that may publish
|
|
325
|
+
arbitrary code to an app can already cause anything that app can, so running
|
|
326
|
+
that app's own declared checks adds nothing. It would stop being true the moment
|
|
327
|
+
it admitted any function.
|
|
328
|
+
|
|
329
|
+
**A check runs against the app's live build** — the server resolves
|
|
330
|
+
`published_build_id`, else `draft_build_id`, for the pre-flight lookup and for
|
|
331
|
+
the execution alike. So `checks run` after a plain `oxyc publish` verifies the
|
|
332
|
+
*previously promoted* code, not the draft just uploaded. `oxyc init-ci` writes
|
|
333
|
+
the step only with `--promote`, where the build just published is the one the
|
|
334
|
+
check runs, and only when the manifest declares a check.
|
|
335
|
+
|
|
336
|
+
With a publish token, `<app>` must be the **UUID** — resolving a slug means
|
|
337
|
+
listing every app, which such a token may not do. The OIDC path needs no id at
|
|
338
|
+
all (the exchange returns it) but does need `<org-slug>/<app-slug>`, since the
|
|
339
|
+
exchange is registered by slug.
|
|
340
|
+
|
|
341
|
+
**Exit codes:** `0` every check passed; `9` (`CHECK_FAILED`) at least one check
|
|
342
|
+
failed or timed out; `1` the app declares no checks; `5` the app was not
|
|
343
|
+
found; `4` no credential resolved, or the API rejected it (401/403 — an
|
|
344
|
+
expired 90-day API key reads this way, not as a missing one); `2` a bad
|
|
345
|
+
`--timeout`, or a slug where the credential needs a UUID; `7` (`UNAVAILABLE`)
|
|
346
|
+
the deployment answered the OIDC exchange without an `app_id`, meaning it
|
|
347
|
+
predates trusted checks.
|
|
244
348
|
|
|
245
349
|
## Development commands
|
|
246
350
|
|
|
@@ -267,6 +371,51 @@ generated from the Rust config types. No network, no token.
|
|
|
267
371
|
Structural checks only. `oxy validate` additionally resolves `databases:` and
|
|
268
372
|
`llm.ref` against the loaded workspace, and wins where the two disagree.
|
|
269
373
|
|
|
374
|
+
Every **`oxy-app.json`** it finds is checked for data placement — the shape of
|
|
375
|
+
the data picks the store: facts and history in Airhouse (`ctx.airhouse`),
|
|
376
|
+
records in OLTP (`ctx.oltp`), files in `ctx.storage`, customer warehouses
|
|
377
|
+
read-only. These fail the run:
|
|
378
|
+
|
|
379
|
+
- `customerWarehouseWrites` that is not `{ "<database>": "<reason>" }`, or names
|
|
380
|
+
a database missing from the function's `destinations`;
|
|
381
|
+
- `"airhouse": { "enabled": true }` in an app whose slug cannot derive a writer
|
|
382
|
+
(`-` → `_`, 1–56 of `[a-z0-9_]`, starting with a letter; no `_` in the slug);
|
|
383
|
+
- `airhouseMigrations` without a `dir`, with one that does not exist, or with
|
|
384
|
+
`migrations.dir`'s; and in its `*.sql`, `PRIMARY KEY`, `UNIQUE`,
|
|
385
|
+
`CREATE INDEX`, `REFERENCES` or `FOREIGN KEY` (DuckLake has none), or a
|
|
386
|
+
`CREATE`/`ALTER`/`DROP`/`INSERT`/`UPDATE`/`DELETE`/`COMMENT ON` target not
|
|
387
|
+
written as `app_<writer>.<name>`.
|
|
388
|
+
|
|
389
|
+
Warnings print on stderr and do not fail: each customer-warehouse exception,
|
|
390
|
+
`CREATE SCHEMA` in a migration, and `ctx.secrets.set` holding state (a
|
|
391
|
+
`JSON.stringify` value, or a state-like key that is not a credential's). The SQL
|
|
392
|
+
checks are lexical; the server's parser is the authority.
|
|
393
|
+
|
|
394
|
+
Each declared **Oxy Function** is then linted — its entry and every relative
|
|
395
|
+
import it reaches, comments and strings masked — for the mistakes that work
|
|
396
|
+
locally and fail closed in production. These fail the run, each naming the
|
|
397
|
+
file, the line, the call and the fix:
|
|
398
|
+
|
|
399
|
+
- `capability` — a `ctx.<area>.<op>` call whose manifest capability the function
|
|
400
|
+
does not declare: `ctx.secrets.set` → `secrets.write`, `ctx.email.send` →
|
|
401
|
+
`email.send`, `ctx.org.*` → `org.read`, `ctx.storage.*` → `storage.read` /
|
|
402
|
+
`storage.write` (`copy` needs both), `ctx.oltp.*` → `oltp.enabled`,
|
|
403
|
+
`ctx.airhouse.*` → `airhouse.enabled`. The map is
|
|
404
|
+
`src/publish/capabilities.ts`, held to the host's `FunctionCapabilities` by a
|
|
405
|
+
Rust test.
|
|
406
|
+
- `isolate-global` — a value use of `Buffer`, `TextEncoder`, `TextDecoder`,
|
|
407
|
+
`Blob`, `File`, `FormData`, `crypto.subtle` or `process.*`: the isolate is bare
|
|
408
|
+
`deno_core` and each is a `ReferenceError` at runtime. A name the file declares
|
|
409
|
+
itself (`const Buffer = …`, `import { TextEncoder } from "./polyfill"`) is not
|
|
410
|
+
one; nor is a `typeof` guard or a type annotation.
|
|
411
|
+
- `destinations` — a `ctx.warehouse.insert` / `exec` / `upsert` or `ctx.tx` in a
|
|
412
|
+
function with no `destinations`, or naming a database not in them.
|
|
413
|
+
|
|
414
|
+
The engine half — `upsert` off Postgres / DuckDB, `ctx.tx` off Postgres, a
|
|
415
|
+
customer-warehouse write with no `customerWarehouseWrites` reason, a write to
|
|
416
|
+
the org's OLTP store — needs the database list, which only the server has;
|
|
417
|
+
`validate` says so and `oxyc publish` runs it before the upload.
|
|
418
|
+
|
|
270
419
|
**`proxy`** forwards a local dev server's Oxy calls to a cloud target with your
|
|
271
420
|
login token attached. Defaults: side-effecting calls are **held**, tracking
|
|
272
421
|
events are **dropped**, auth endpoints reach the backend unauthenticated so
|
|
@@ -299,6 +448,7 @@ once npm reclaims it.
|
|
|
299
448
|
6 the request was malformed (4xx other than 401/403/404)
|
|
300
449
|
7 unavailable — 5xx, a timeout, or the network failed. Retryable.
|
|
301
450
|
8 refused — the operation would have destroyed or overwritten something
|
|
451
|
+
9 a check ran and failed or timed out (`oxyc checks run`)
|
|
302
452
|
```
|
|
303
453
|
|
|
304
454
|
`4` almost always means the wrong `--env`. `7` is worth retrying; `6` never is.
|