@oxy-hq/cli 0.9.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 +224 -25
- package/dist/main.mjs +10616 -3356
- package/dist/main.mjs.map +1 -1
- package/dist/{mcp-Bdp0rX0l.mjs → mcp-B2KBWNOU.mjs} +86 -3
- package/dist/mcp-B2KBWNOU.mjs.map +1 -0
- package/package.json +3 -2
- package/skills/oxy-cli/SKILL.md +8 -1
- package/skills/oxy-custom-apps/SKILL.md +209 -25
- package/skills/oxy-custom-apps/references/minimal-starter.md +10 -2
- package/skills/oxy-custom-apps/references/performance-and-usage.md +46 -0
- package/skills/oxy-custom-apps/references/sandbox-agent-loop.md +57 -17
- package/template/.claude/hooks/oxyc-cloud-session.sh +66 -0
- package/template/.claude/settings.json +16 -0
- package/template/.github/workflows/publish.yaml +1 -1
- package/template/.oxyc-managed +13 -0
- package/dist/mcp-Bdp0rX0l.mjs.map +0 -1
|
@@ -50,6 +50,7 @@ works), but hand-rolling means owning the base-path + dev-proxy wiring yourself.
|
|
|
50
50
|
|
|
51
51
|
```json
|
|
52
52
|
{
|
|
53
|
+
"$schema": "https://unpkg.com/@oxy-hq/sdk/schema/oxy-app.schema.json",
|
|
53
54
|
"schemaVersion": 2,
|
|
54
55
|
"slug": "url-slug",
|
|
55
56
|
"orgSlug": "your-org",
|
|
@@ -57,18 +58,38 @@ works), but hand-rolling means owning the base-path + dev-proxy wiring yourself.
|
|
|
57
58
|
}
|
|
58
59
|
```
|
|
59
60
|
|
|
60
|
-
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).
|
|
61
64
|
Strongly recommended: `orgSlug` (lets `@oxy-hq/vite-plugin` derive
|
|
62
65
|
the asset base path automatically, so plain `pnpm build` produces
|
|
63
|
-
a bundle ready to drop into the linked org with no env var
|
|
64
|
-
|
|
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.
|
|
65
86
|
|
|
66
87
|
At serve time, oxy injects the authoritative org + project from
|
|
67
88
|
the admin row via `window.__OXY_APP__` — the manifest's `orgSlug`
|
|
68
89
|
and `projectId` are advisory hints only, never enforcement.
|
|
69
90
|
|
|
70
91
|
Do not invent `products`, `writers`, or `from:`-blocks. Those
|
|
71
|
-
don't exist in v2 and the
|
|
92
|
+
don't exist in v2 and the schema rejects them.
|
|
72
93
|
|
|
73
94
|
File location: **`<root>/oxy-app.json`**, sibling to
|
|
74
95
|
`vite.config.ts`. The vite-plugin reads it from there, and so does
|
|
@@ -304,13 +325,68 @@ Two more that read as bugs — `useTrackEvent("oxy-…")` is refused (`400`,
|
|
|
304
325
|
the prefix is reserved), and a hovered-but-never-opened app records no view —
|
|
305
326
|
are in [references/performance-and-usage.md](references/performance-and-usage.md).
|
|
306
327
|
|
|
328
|
+
## Run a function locally
|
|
329
|
+
|
|
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.
|
|
335
|
+
|
|
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
|
|
340
|
+
```
|
|
341
|
+
|
|
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.
|
|
377
|
+
|
|
307
378
|
## Publishing
|
|
308
379
|
|
|
309
|
-
**These commands are a person's
|
|
310
|
-
login
|
|
311
|
-
agent token. As an agent,
|
|
312
|
-
|
|
313
|
-
|
|
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.
|
|
314
390
|
|
|
315
391
|
Ship with `oxyc publish` (`npm i -g @oxy-hq/cli`) — **no CI step, no S3 sync,
|
|
316
392
|
no folder picker**. From the app directory:
|
|
@@ -318,24 +394,45 @@ no folder picker**. From the app directory:
|
|
|
318
394
|
```bash
|
|
319
395
|
oxyc login --env production # once; browser flow, caches a token,
|
|
320
396
|
# and prints whether you're an app-admin
|
|
321
|
-
oxyc validate # data-placement and
|
|
322
|
-
oxyc publish --env production # build → bundle functions → tar → upload (draft
|
|
323
|
-
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
|
|
324
402
|
```
|
|
325
403
|
|
|
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
|
+
|
|
326
414
|
- **Identity**, for each half, is `--app` / `--org`, then `OXY_APP` /
|
|
327
415
|
`OXY_ORG`, then the manifest's `slug` + `orgSlug`, then an
|
|
328
416
|
`apps/<org>/<app>/` working directory. `--org` takes a slug or a UUID; pass
|
|
329
417
|
the slug whenever `oxyc` runs the build, because the asset base path is
|
|
330
418
|
baked from what you pass. With no org from any of those, the build stops
|
|
331
419
|
with `missing org: the app's base path needs it` (exit `2`): add
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
|
430
|
+
the flag after the first publish — it's auto-resolved from then on.
|
|
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.
|
|
339
436
|
- **`--env` targets:** `local` → a local Oxy web-app Vite dev server `:5173`
|
|
340
437
|
(it serves `/cli-auth` and proxies `/api` to oxy on `:3000`), `dev` →
|
|
341
438
|
`aip.dev.oxy.tech`, `staging` → `aip.staging.oxy.tech`, `production` (the
|
|
@@ -365,10 +462,87 @@ oxyc publish --env production --promote # …straight to live
|
|
|
365
462
|
`oxyc apps builds <org>/<app>` is the history. `oxyc apps drift <org>/<app>`
|
|
366
463
|
lists the commits in your checkout that touch the app after the live build's
|
|
367
464
|
commit — `unknown` there means it could not compare, not that nothing changed.
|
|
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.
|
|
368
472
|
|
|
369
473
|
The build is not the check: after a publish, drive the page
|
|
370
474
|
([references/verifying-in-the-browser.md](references/verifying-in-the-browser.md)).
|
|
371
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`.
|
|
545
|
+
|
|
372
546
|
## Sandboxes — try a change on real data before shipping it
|
|
373
547
|
|
|
374
548
|
A sandbox (`dev-<handle>`, 1-12 lowercase letters/digits/hyphens, no leading,
|
|
@@ -377,10 +551,16 @@ 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
552
|
`ctx.oltp`/`ctx.warehouse` stay shared; a held write's `note` field in
|
|
379
553
|
`invocations held` names the manifest key that would make it land instead).
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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.
|
|
384
564
|
|
|
385
565
|
```bash
|
|
386
566
|
oxyc env create acme/store dev-x --env dev --json # starts with no build
|
|
@@ -388,17 +568,21 @@ oxyc publish --env dev --app-env dev-x --json # build + publish to
|
|
|
388
568
|
oxyc env secret set acme/store KEY --app-env dev-x --env dev --value-env VAR # optional: its own secret
|
|
389
569
|
oxyc fn call acme/store submit-order --app-env dev-x --env dev --data '{}' --json # call it
|
|
390
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
|
|
391
572
|
oxyc invocations list acme/store --app-env dev-x --env dev --json # what ran; a row's id is `id`
|
|
392
573
|
oxyc invocations held acme/store <invocation-id> --env dev --json # what it held, not wrote
|
|
393
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
581
|
Iterate from `publish` as the code changes — each publish is a new build, and
|
|
400
|
-
the sandbox always serves the newest. A sandbox build
|
|
401
|
-
|
|
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>`.
|
|
402
586
|
Staff only (needs `develop_apps` + `manage_apps`) — or a sandbox agent token
|
|
403
587
|
such staff approved; a publish token is refused on every sandbox operation,
|
|
404
588
|
so CI cannot use one.
|
|
@@ -15,9 +15,17 @@ my-app/
|
|
|
15
15
|
└── tsconfig.json
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
`oxy-app.json
|
|
18
|
+
`oxy-app.json` (`$schema` is optional — editor completion against the SDK's
|
|
19
|
+
generated schema; `oxyc validate` and the Vite plugin check the file against it
|
|
20
|
+
either way):
|
|
19
21
|
```json
|
|
20
|
-
{
|
|
22
|
+
{
|
|
23
|
+
"$schema": "https://unpkg.com/@oxy-hq/sdk/schema/oxy-app.schema.json",
|
|
24
|
+
"schemaVersion": 2,
|
|
25
|
+
"slug": "my-app",
|
|
26
|
+
"orgSlug": "your-org",
|
|
27
|
+
"name": "My App"
|
|
28
|
+
}
|
|
21
29
|
```
|
|
22
30
|
|
|
23
31
|
`src/main.tsx`:
|
|
@@ -94,6 +94,52 @@ false` stops only the *client* runtime — Oxy still records one view row per na
|
|
|
94
94
|
server-side, so the Activity tab never goes dark. Reach for these rarely: the defaults are
|
|
95
95
|
the fast, observable path.
|
|
96
96
|
|
|
97
|
+
### A tab left open across a publish (automatic — and what it can cost you)
|
|
98
|
+
|
|
99
|
+
Oxy's injected runtime also watches for a newer build. Every app gets this with no bundle
|
|
100
|
+
code, so an app that keeps work in memory has to know about it:
|
|
101
|
+
|
|
102
|
+
- **It notices a publish within ~5 minutes.** A visible tab polls `__oxy/build`, and checks
|
|
103
|
+
again when it comes back to the foreground or online. When the live build differs, it
|
|
104
|
+
fires a cancelable `oxy:update-available` event on `window`.
|
|
105
|
+
- **A broken lazy chunk after a publish reloads the page — in every mode except the
|
|
106
|
+
platform's `off`.** A route the tab never opened asks for a chunk the new build no
|
|
107
|
+
longer has; rather than white-screen, the runtime checks `__oxy/build` and, if the
|
|
108
|
+
build moved, reloads onto it. That reload **discards in-memory state** unless the app
|
|
109
|
+
holds `useBusyWhile` / `holdUpdates`, an input is focused, or it cancels the
|
|
110
|
+
`oxy:chunk-stale` event.
|
|
111
|
+
- **Whether a tab reloads by itself is `performance.autoUpdate`.** The platform default is
|
|
112
|
+
set by the operator (announce only, today). Set it per app:
|
|
113
|
+
|
|
114
|
+
```jsonc
|
|
115
|
+
{ "performance": { "autoUpdate": true } } // reload at a safe moment: a navigation,
|
|
116
|
+
// 2 idle minutes with nothing focused, or a
|
|
117
|
+
// return after 10+ minutes away
|
|
118
|
+
{ "performance": { "autoUpdate": false } } // never reload by itself; offer it with useAppUpdate
|
|
119
|
+
{ "performance": { "autoUpdate": "prompt" } } // shell shows "Update ready"; reloads at the
|
|
120
|
+
// next safe moment if nobody taps it in 24 h
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**The rule: wrap unsaved in-memory work.** A checklist half filled, a form not yet saved,
|
|
124
|
+
a write in flight — hold updates while it is dirty, and the runtime will not reload under
|
|
125
|
+
it (a held token older than 12 h is ignored, so a leak cannot pin a tab forever):
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
import { useAppUpdate, useBusyWhile } from "@oxy-hq/sdk";
|
|
129
|
+
|
|
130
|
+
useBusyWhile(isDirty, "checklist not saved"); // or: const release = holdUpdates("…")
|
|
131
|
+
|
|
132
|
+
const { available, apply } = useAppUpdate(); // offer the update in your own UI
|
|
133
|
+
{available && <button onClick={() => apply()}>Update now</button>}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`useFunction` already holds updates for the life of each call. `UpdatePill` (from
|
|
137
|
+
`@oxy-hq/sdk/shell`) is the ready-made "Update ready" chip; `useKioskIdle` signs a kiosk
|
|
138
|
+
out after the device's idle timeout, which is also a full reload onto the live build.
|
|
139
|
+
An app's own `beforeunload` "leave this page?" prompt still applies. When the viewer
|
|
140
|
+
chooses to stay, the runtime does not reload onto that build by itself again, but
|
|
141
|
+
`apply()` and the chip still work.
|
|
142
|
+
|
|
97
143
|
**Two gotchas that read as bugs:**
|
|
98
144
|
- **`oxy-` is a reserved event-name prefix.** `useTrackEvent("oxy-…")` is refused with a
|
|
99
145
|
400 — that namespace is the platform's own instrumentation. Name your events anything
|
|
@@ -23,11 +23,29 @@ oxyc tokens create --sandbox-agent --app <org>/<app> --env <deployment>
|
|
|
23
23
|
browser. The command waits five minutes for the click, so run it with a
|
|
24
24
|
timeout at least that long: a shell tool that stops a command after two
|
|
25
25
|
minutes kills it first. Never open or click that page with a browser tool.
|
|
26
|
+
- **Where no browser can reach your machine** (a cloud container, a CI job, a
|
|
27
|
+
remote box), add `--no-browser`. `oxyc` prints a URL and an eight-letter
|
|
28
|
+
code on stderr instead of opening a page, and waits up to ten minutes. Give
|
|
29
|
+
your engineer both: they open the URL on any device where they are signed
|
|
30
|
+
in, type the code the terminal shows, and approve. The token is printed
|
|
31
|
+
exactly as above. The rule does not change: never open or approve that page
|
|
32
|
+
yourself, with a browser tool or any other way.
|
|
33
|
+
- **Name who approves with `--approver <email>`** (or `OXY_APPROVER`), only
|
|
34
|
+
with `--no-browser`: the engineer is told by push, email and Slack; they type
|
|
35
|
+
the code from your output on the page. Use the email your engineer gave you,
|
|
36
|
+
and still show them the code `oxyc` printed, since the notice does not carry
|
|
37
|
+
it. Only that person can open the request: if you were given no address, omit
|
|
38
|
+
the flag and hand them the URL and the code as before. Never open or click
|
|
39
|
+
the page yourself.
|
|
26
40
|
- **Evaluate that line and work with the token.** In a shell that persists:
|
|
27
41
|
`eval "$(oxyc tokens create --sandbox-agent --app <org>/<app> --env <deployment>)"`.
|
|
28
|
-
In one that does not,
|
|
29
|
-
`
|
|
30
|
-
|
|
42
|
+
In one that does not, such as an agent's shell tool, redirect it to
|
|
43
|
+
`~/.config/oxy/agent-token.env` under `umask 077`, and start each command
|
|
44
|
+
with `. ~/.config/oxy/agent-token.env &&`. In a Claude Code cloud session,
|
|
45
|
+
the repo's SessionStart hook does that for you
|
|
46
|
+
([SKILL.md → Headless and cloud sessions](../SKILL.md#headless-and-cloud-sessions)).
|
|
47
|
+
`oxyc` writes nothing to the credentials file. That file is the only place
|
|
48
|
+
the token goes. Never put it in a commit or your report.
|
|
31
49
|
- **Make the variable the only credential.** Put `--token-env OXY_TOKEN` on
|
|
32
50
|
every command after the mint, as the loop below does. With it, a command
|
|
33
51
|
that lost the variable exits `4` (`OXY_TOKEN is not set`): set it and run the
|
|
@@ -127,9 +145,21 @@ Reads hit production data. A write lands in a home of the sandbox's own, or it
|
|
|
127
145
|
is *held*: recorded in `invocations held` and not performed. A held write is
|
|
128
146
|
the expected answer outside production. Report it. Unless the task asks for
|
|
129
147
|
it, do not edit `nonProduction` in the manifest to make the write land:
|
|
130
|
-
staging and every other sandbox share that map.
|
|
131
|
-
|
|
132
|
-
function
|
|
148
|
+
staging and every other sandbox share that map. A sandbox's schedules are off
|
|
149
|
+
unless staff turned them on, and your token cannot turn them on: to test a
|
|
150
|
+
scheduled function, run it once as its schedule would — `oxyc fn run
|
|
151
|
+
<org>/<app> <function> --app-env dev-x --as-schedule --env <deployment>
|
|
152
|
+
--token-env OXY_TOKEN --json` (no `--data`; `oxy_fn_run` with `asSchedule:
|
|
153
|
+
true` over MCP). It runs as the system, with the function's retry policy, and
|
|
154
|
+
answers `409 not_scheduled` (exit `6`) for a function with no `schedule`. The
|
|
155
|
+
`fn call` result carries the function's `logs`; `oxyc logs` shows the same
|
|
156
|
+
lines about 30 seconds later.
|
|
157
|
+
To watch a run you do not call yourself — a check on the worker fleet, a slow
|
|
158
|
+
function — start `oxyc logs <org>/<app> --app-env dev-x --follow --timeout 300
|
|
159
|
+
--env <deployment> --token-env OXY_TOKEN --json` in the background first. It
|
|
160
|
+
prints one JSON event per line (`open`, then `start` / `log` / `finish`),
|
|
161
|
+
needs no observability capture, and exits `0` on `done`. A `dropped` event
|
|
162
|
+
means lines were lost, not that the function failed.
|
|
133
163
|
|
|
134
164
|
## When a command fails
|
|
135
165
|
|
|
@@ -160,9 +190,11 @@ Branch on the exit code and the JSON fields, not on the text:
|
|
|
160
190
|
value shaped like one (`400 credential_shaped_value`). Set a secret with
|
|
161
191
|
`oxyc env secret set` (`oxy_env_secret_set`), never a hand-built request,
|
|
162
192
|
and never try to read a value back.
|
|
163
|
-
- **Never pass `--promote`,
|
|
164
|
-
|
|
165
|
-
your engineer
|
|
193
|
+
- **Never pass `--promote`, never run `oxyc promote` or `oxyc rollback`, and
|
|
194
|
+
never publish without `--app-env`.** When the sandbox build is right, say so
|
|
195
|
+
and leave the ship step to your engineer: they promote that build to staging
|
|
196
|
+
(`oxyc promote <app> --from dev-<handle>`), then to production. Your token is
|
|
197
|
+
refused both verbs anyway. Never use `production` as `--app-env` to "check" something:
|
|
166
198
|
reads already hit production.
|
|
167
199
|
- **A held write is information to report**, not an error to engineer around.
|
|
168
200
|
- **Clean up, even when a step failed.** Delete every sandbox you created,
|
|
@@ -211,16 +243,24 @@ cases:
|
|
|
211
243
|
- **Exit `8`.** The deployment did not return the token you asked for: it
|
|
212
244
|
predates sandbox agent tokens, or returned one for other apps. Nothing was
|
|
213
245
|
printed, and what it minted was revoked.
|
|
214
|
-
- **Exit `7`.** Nobody approved
|
|
215
|
-
the page said. Only Oxy staff
|
|
216
|
-
the app's org can approve: if
|
|
217
|
-
stop. If they missed it, run the
|
|
246
|
+
- **Exit `7`.** Nobody approved in time (five minutes, or ten with
|
|
247
|
+
`--no-browser`). Ask your engineer what the page said. Only Oxy staff
|
|
248
|
+
holding `develop_apps` and `manage_apps` over the app's org can approve: if
|
|
249
|
+
the page refused, report what it said and stop. If they missed it, run the
|
|
250
|
+
command once more. If you named an `--approver`, say so: the request went to
|
|
251
|
+
that person's phone and inbox, so they may have missed the notice rather
|
|
252
|
+
than the page.
|
|
218
253
|
- **Exit `4`.** The approval code was rejected and nothing was minted. Run the
|
|
219
254
|
command once more, with fewer `--hours` if an organization caps a token's
|
|
220
|
-
lifetime.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
255
|
+
lifetime. With `--no-browser`, exit `4` with `access_denied` means your
|
|
256
|
+
engineer declined, or the page refused the request: the reason is printed.
|
|
257
|
+
Report it and stop.
|
|
258
|
+
- **`--no-browser` is an unknown option (exit `2`), or the deployment has no
|
|
259
|
+
device-code approval (exit `8`).** The CLI or the deployment is too old for
|
|
260
|
+
a machine no browser can reach. Ask your engineer to update, or to run the
|
|
261
|
+
command on a machine with a browser and hand you the token.
|
|
262
|
+
- **`--approver` is an unknown option (exit `2`).** The CLI predates it. Run
|
|
263
|
+
without it and give your engineer the URL and the code yourself.
|
|
224
264
|
|
|
225
265
|
## Over MCP
|
|
226
266
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SessionStart hook for Claude Code cloud sessions (claude.ai/code): makes
|
|
3
|
+
# `oxyc` usable with no browser on this machine — installed, routed through the
|
|
4
|
+
# session's egress proxy, and given a credential that survives between Bash
|
|
5
|
+
# calls. A local session exits at once.
|
|
6
|
+
#
|
|
7
|
+
# stdout is read by Claude, so it says only what is missing and the one thing
|
|
8
|
+
# to do about it. The cloud *environment* still needs the Oxy hosts allowed
|
|
9
|
+
# (Network access → Custom): the oxy-custom-apps skill, "Headless and cloud
|
|
10
|
+
# sessions".
|
|
11
|
+
#
|
|
12
|
+
# Two copies, kept identical by a test: oxygen-internal's own, and the one in
|
|
13
|
+
# sdk/cli/template/ that `oxyc update` writes into every customer repo (it is
|
|
14
|
+
# managed there — edit it in oxygen-internal, not in the customer repo).
|
|
15
|
+
|
|
16
|
+
[ "${CLAUDE_CODE_REMOTE:-}" = "true" ] || exit 0
|
|
17
|
+
cat >/dev/null 2>&1 || true
|
|
18
|
+
|
|
19
|
+
# Where a minted agent token is kept: the one `export OXY_TOKEN=…` line that
|
|
20
|
+
# `oxyc tokens create --agent` prints on stdout.
|
|
21
|
+
TOKEN_FILE="$HOME/.config/oxy/agent-token.env"
|
|
22
|
+
ENV="${OXY_ENV:-production}"
|
|
23
|
+
case "$ENV" in
|
|
24
|
+
production) HOST=app.oxygen-hq.com ;;
|
|
25
|
+
staging) HOST=aip.staging.oxy.tech ;;
|
|
26
|
+
dev) HOST=aip.dev.oxy.tech ;;
|
|
27
|
+
*) HOST="" ;;
|
|
28
|
+
esac
|
|
29
|
+
|
|
30
|
+
if ! command -v oxyc >/dev/null 2>&1; then
|
|
31
|
+
npm install -g @oxy-hq/cli >/dev/null 2>&1 \
|
|
32
|
+
|| echo "oxyc: 'npm install -g @oxy-hq/cli' failed — run it before any oxyc command."
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
if [ -n "${CLAUDE_ENV_FILE:-}" ]; then
|
|
36
|
+
{
|
|
37
|
+
# Node's fetch ignores HTTPS_PROXY without this, and every request out of
|
|
38
|
+
# this VM goes through one. A current oxyc sets it itself; older ones don't.
|
|
39
|
+
echo 'export NODE_USE_ENV_PROXY=1'
|
|
40
|
+
# Runs before every Bash command, so a token minted mid-session reaches
|
|
41
|
+
# every command after it — an `export` in one call would not.
|
|
42
|
+
echo "[ -f \"$TOKEN_FILE\" ] && . \"$TOKEN_FILE\""
|
|
43
|
+
} >>"$CLAUDE_ENV_FILE"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
if [ -n "$HOST" ] && ! curl -fsS --max-time 10 -o /dev/null "https://$HOST/api/health" 2>/dev/null; then
|
|
47
|
+
echo "oxyc: https://$HOST is unreachable from this session. The cloud environment's network"
|
|
48
|
+
echo " access must allow it (Custom: app.oxygen-hq.com, *.oxy.tech) — ask the user to add it."
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
if [ -n "${OXY_TOKEN:-}" ]; then
|
|
52
|
+
echo "oxyc: credential is OXY_TOKEN from the environment."
|
|
53
|
+
elif [ -s "$TOKEN_FILE" ]; then
|
|
54
|
+
echo "oxyc: credential is the agent token in $TOKEN_FILE (it expires: on exit 4, mint again)."
|
|
55
|
+
else
|
|
56
|
+
cat <<EOF
|
|
57
|
+
oxyc: no Oxy credential yet. Before you publish or promote, mint an agent token — a person
|
|
58
|
+
approves it once from any device, nothing opens here. Run this in the background:
|
|
59
|
+
mkdir -p ~/.config/oxy && (umask 077; oxyc tokens create --agent --no-browser --env $ENV >$TOKEN_FILE)
|
|
60
|
+
and give the user the URL and code it prints. --approver <their email> (or OXY_APPROVER) also
|
|
61
|
+
pushes the request to them. Once approved, every later command has OXY_TOKEN.
|
|
62
|
+
It reaches what the approver's org memberships do, never staff access: staff shipping a
|
|
63
|
+
customer's app need another path. See the oxy-custom-apps skill, "Headless and cloud sessions".
|
|
64
|
+
EOF
|
|
65
|
+
fi
|
|
66
|
+
exit 0
|
|
@@ -160,7 +160,7 @@ env:
|
|
|
160
160
|
# service account are honoured from 0.7.0, which tries them first and falls
|
|
161
161
|
# back to the publisher. A pin older than that still publishes, through the
|
|
162
162
|
# publisher alone.
|
|
163
|
-
OXYC_VERSION: "0.
|
|
163
|
+
OXYC_VERSION: "0.10.0"
|
|
164
164
|
|
|
165
165
|
jobs:
|
|
166
166
|
build:
|
package/template/.oxyc-managed
CHANGED
|
@@ -180,6 +180,11 @@ managed .github/scripts/*.sh
|
|
|
180
180
|
# app locally", and it is meant to improve for everyone at once.
|
|
181
181
|
managed scripts/*.sh
|
|
182
182
|
|
|
183
|
+
# The Claude Code cloud-session hook: installs oxyc and wires its proxy and
|
|
184
|
+
# credential so an agent with no browser can publish. It tracks oxyc's own
|
|
185
|
+
# behaviour, and a test keeps it byte-identical to oxygen-internal's copy.
|
|
186
|
+
managed .claude/hooks/*.sh
|
|
187
|
+
|
|
183
188
|
# The merge rule for the generated memory index. It is a statement about how
|
|
184
189
|
# oxyc's own generated file behaves, so oxyc owns it.
|
|
185
190
|
managed .gitattributes
|
|
@@ -246,6 +251,10 @@ mixed .gitignore
|
|
|
246
251
|
# script somebody decided on is the customer's.
|
|
247
252
|
mixed pnpm-workspace.yaml
|
|
248
253
|
|
|
254
|
+
# The SessionStart entry that runs the hook above is the tool's; any other
|
|
255
|
+
# hook, permission or plugin a team adds beside it is theirs.
|
|
256
|
+
mixed .claude/settings.json
|
|
257
|
+
|
|
249
258
|
# --- scope: which tree each file describes ----------------------------------
|
|
250
259
|
#
|
|
251
260
|
# Grouped by answer rather than written beside the role above, so that the two
|
|
@@ -263,6 +272,10 @@ scope-repo .gitattributes
|
|
|
263
272
|
scope-repo .oxyc-managed
|
|
264
273
|
scope-repo .oxyc-template.json
|
|
265
274
|
|
|
275
|
+
# Claude Code reads `.claude/` from the root of the repo it opened.
|
|
276
|
+
scope-repo .claude/hooks/*.sh
|
|
277
|
+
scope-repo .claude/settings.json
|
|
278
|
+
|
|
266
279
|
# The PNPM workspace and the git working tree, which are root things in a repo
|
|
267
280
|
# whose Oxy workspace is somewhere else. `pnpm dev` is run from the repo root;
|
|
268
281
|
# `packages: apps/*` names directories at the repo root, which is also where
|