@oxy-hq/cli 0.5.0 → 0.7.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 +611 -51
- package/dist/main.mjs +8937 -3407
- package/dist/main.mjs.map +1 -1
- package/dist/mcp-xxWL3_dz.mjs +1137 -0
- package/dist/mcp-xxWL3_dz.mjs.map +1 -0
- package/json-schemas/agentic.json +1 -1
- package/json-schemas/flow-test.json +3 -3
- package/package.json +4 -4
- package/skills/oxy-cli/SKILL.md +27 -1
- package/skills/oxy-custom-apps/SKILL.md +128 -0
- package/template/.github/scripts/ci-publish.sh +11 -10
- package/template/.github/workflows/publish.yaml +102 -30
- package/template/README.md +95 -31
- package/dist/mcp-CIsyezzU.mjs +0 -316
- package/dist/mcp-CIsyezzU.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -3,10 +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`, `oltp`
|
|
6
|
+
- **API client** — `api`, `routes`, `schema`, `openapi`, `login`, `whoami`, `token`, `tokens`, `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
9
|
- **Checks** — `checks run`
|
|
10
|
+
- **Sandboxes** — `env`, `fn call`, `invocations`, `logs`
|
|
11
|
+
- **Workspace previews** — `preview`
|
|
10
12
|
- **Development** — `validate`, `mcp`, `guide`, `skills`
|
|
11
13
|
|
|
12
14
|
## Install
|
|
@@ -48,8 +50,9 @@ and `cache`.
|
|
|
48
50
|
| --- | --- | --- |
|
|
49
51
|
| `--env <name\|url>` | `production` | deployment to target |
|
|
50
52
|
| `--target <url>` | — | explicit base URL; overrides `--env` |
|
|
51
|
-
| `--token-env <VAR>` | `OXY_TOKEN` | env var holding the bearer
|
|
52
|
-
| `--api-key-env <VAR>` | `OXY_API_KEY` | env var holding the key for `/external/api` |
|
|
53
|
+
| `--token-env <VAR>` | `OXY_TOKEN` | env var holding the bearer: any credential. **Named, it is the only source** — an unset variable is exit `4`, never a fallback to your login |
|
|
54
|
+
| `--api-key-env <VAR>` | `OXY_API_KEY` | env var holding the legacy API key or API token for `/external/api` |
|
|
55
|
+
| `--service-account <id>` | `OXY_SERVICE_ACCOUNT` | in GitHub Actions: the **ID** of the service account the OIDC exchange acts as |
|
|
53
56
|
| `--org <slug>` | — | value for the `{org}` placeholder |
|
|
54
57
|
| `--workspace <id>` | — | value for the `{workspace}` placeholder |
|
|
55
58
|
| `--project <id>` | — | value for the `{project}` placeholder |
|
|
@@ -77,7 +80,10 @@ Credentials are stored **per deployment**, and the default is production. A
|
|
|
77
80
|
oxyc api <path> [flags]
|
|
78
81
|
```
|
|
79
82
|
|
|
80
|
-
`<path>` is relative to `/api`; a leading `/` or `api/` is accepted.
|
|
83
|
+
`<path>` is relative to `/api`; a leading `/` or `api/` is accepted. One
|
|
84
|
+
exception: `customer-apps/…` is sent as written, because `/customer-apps/<org>/<app>/…`
|
|
85
|
+
is where an app's bundle is served. The app registry API is `api/customer-apps/…`
|
|
86
|
+
— spell the `api/` out for it (a 404 on the bare form says so).
|
|
81
87
|
|
|
82
88
|
| Flag | Meaning |
|
|
83
89
|
| --- | --- |
|
|
@@ -107,13 +113,18 @@ to `--env`, or from `--org` / `--workspace` / `--project`. An unresolvable
|
|
|
107
113
|
placeholder is an error naming the flag that fills it — never a literal sent to
|
|
108
114
|
the server.
|
|
109
115
|
|
|
110
|
-
**Surfaces.** The path selects the credential
|
|
111
|
-
|
|
116
|
+
**Surfaces.** The path selects how the credential is sent. `/api/**` sends it
|
|
117
|
+
as a bearer — whichever one [resolves](#authentication). `/external/api/**`
|
|
118
|
+
sends `X-API-Key` from `$OXY_API_KEY`, which holds a legacy API key (`oxy_…`) or
|
|
119
|
+
an API token (`oxy_pat_…`, `oxy_sat_…`, `oxy_ci_…`). With none set, an
|
|
120
|
+
`OXY_TOKEN` holding either of those stands in for it, so one `OXY_TOKEN` covers
|
|
121
|
+
both surfaces. A session token never does.
|
|
112
122
|
|
|
113
123
|
**`--paginate` is a heuristic.** Oxy sends `Link: rel="next"` on some endpoints
|
|
114
|
-
and nothing on others; where there is no header, `oxyc` reads
|
|
115
|
-
`pagination.has_next`, then `has_more`,
|
|
116
|
-
|
|
124
|
+
and nothing on others; where there is no header, `oxyc` reads `next_offset`
|
|
125
|
+
(and asks again with `?offset=`), then `pagination.has_next`, then `has_more`,
|
|
126
|
+
then `page < total_pages` (asking again with `?page=`). Nothing recognised
|
|
127
|
+
means one page, and it warns when the server said nothing at all.
|
|
117
128
|
|
|
118
129
|
```bash
|
|
119
130
|
oxyc api {org}/workspaces --md
|
|
@@ -139,13 +150,130 @@ route's surface and fleet role.
|
|
|
139
150
|
|
|
140
151
|
```bash
|
|
141
152
|
oxyc login [--env <e>] [--login-env <e...>] [--assume <org> -r <why>]
|
|
142
|
-
oxyc whoami [--json]
|
|
153
|
+
oxyc whoami [--json] # who the credential is, and what it can reach
|
|
143
154
|
oxyc token # print the bearer, for a raw curl
|
|
144
|
-
oxyc logout
|
|
155
|
+
oxyc logout # revoke the cached token, then forget it
|
|
156
|
+
oxyc tokens list [--json] # your personal access tokens
|
|
157
|
+
oxyc tokens revoke <id>
|
|
158
|
+
oxyc tokens revoke --current # end the token in OXY_TOKEN
|
|
159
|
+
oxyc tokens create # opens Account → Personal access tokens
|
|
160
|
+
oxyc tokens create --sandbox-agent --app <org>/<app> [--app …] [--hours 8] [--name …]
|
|
145
161
|
```
|
|
146
162
|
|
|
147
|
-
|
|
148
|
-
|
|
163
|
+
**Every command resolves its credential the same way, first match wins:**
|
|
164
|
+
|
|
165
|
+
1. **`OXY_TOKEN`** (or the variable `--token-env` names). It holds any
|
|
166
|
+
credential, and all are sent as a bearer: an API token — a personal access
|
|
167
|
+
token (`oxy_pat_…`), a service account token (`oxy_sat_…`), a CI token
|
|
168
|
+
(`oxy_ci_…`) or a sandbox agent token (`oxy_sbx_…`) — a legacy API key
|
|
169
|
+
(`oxy_…`), a publish token, or a session token. A blank value counts as
|
|
170
|
+
unset.
|
|
171
|
+
2. **The login cache** — what `oxyc login` stored for this deployment.
|
|
172
|
+
3. **GitHub OIDC**, in a GitHub Actions job granted `id-token: write`: the job's
|
|
173
|
+
OIDC token is exchanged, once per process, for a fifteen-minute token acting
|
|
174
|
+
as a service account, and that token is revoked when the command ends. See
|
|
175
|
+
[CI without a stored secret](#ci-without-a-stored-secret).
|
|
176
|
+
|
|
177
|
+
**Naming the variable stops the list at 1.** With `--token-env <VAR>`, and
|
|
178
|
+
always in `oxyc mcp`, an unset or empty variable is an auth error (exit `4`):
|
|
179
|
+
the login cache and the OIDC exchange are not tried. The flag says which
|
|
180
|
+
credential runs the command — an agent's scoped token, typically — so a typo in
|
|
181
|
+
the name must not quietly run it as whoever last ran `oxyc login` on the
|
|
182
|
+
machine. A person who wants `oxyc mcp` on their own login starts it with
|
|
183
|
+
`--login`. `OXY_API_KEY` is unaffected: a command that can use one on its own
|
|
184
|
+
(`checks run`, the sandbox verbs) still does.
|
|
185
|
+
|
|
186
|
+
`OXY_API_KEY` holds a legacy API key or an API token, and is honoured where it
|
|
187
|
+
always was — the `/external/api` surface, and `checks run`.
|
|
188
|
+
|
|
189
|
+
**Legacy API keys and API tokens are separate things.** A legacy API key is the
|
|
190
|
+
older `oxy_…` key: it reaches everything its owner can and can't be limited to
|
|
191
|
+
workspaces. It keeps working wherever it works today, but it is not a token —
|
|
192
|
+
`oxyc tokens` never lists or revokes one, and `oxyc whoami` calls it a
|
|
193
|
+
`Legacy API key`. They are extended and revoked in the web app, under
|
|
194
|
+
Settings → Workspace → Legacy API keys. Use an API token for anything new.
|
|
195
|
+
|
|
196
|
+
**`login`** runs a browser loopback flow. Against a current deployment it
|
|
197
|
+
receives a one-time code and exchanges it (PKCE) for a **personal access
|
|
198
|
+
token** named `oxyc on <hostname>`: revocable, ninety days, and replacing that
|
|
199
|
+
machine's previous one. No credential travels in a URL. Against a deployment
|
|
200
|
+
that predates the exchange it stores the session token the page hands back,
|
|
201
|
+
exactly as before, and says so — nothing needs upgrading first, in either
|
|
202
|
+
direction. `--login-env` is repeatable and comma-separated
|
|
203
|
+
(`--login-env dev,staging`); the browser opens once per environment, in
|
|
204
|
+
sequence.
|
|
205
|
+
|
|
206
|
+
**`logout`** revokes the cached token on the server, best-effort, then removes
|
|
207
|
+
it from this machine whatever the server said. It never touches `OXY_TOKEN`.
|
|
208
|
+
|
|
209
|
+
**`whoami`** makes a live call, so an expired or revoked credential fails here
|
|
210
|
+
rather than reading as fine from the cache. For a token it also prints what the
|
|
211
|
+
token is and what it can reach — `all access`, or one line per grant with the
|
|
212
|
+
role it is capped at, and any organization whose policy blocks it. For a legacy
|
|
213
|
+
API key it prints `Legacy API key`, and that it reaches everything its owner
|
|
214
|
+
can.
|
|
215
|
+
|
|
216
|
+
**`token`** prints the bearer on stdout and nothing else. Inside a GitHub
|
|
217
|
+
Actions job with nothing stored it prints the token the job's OIDC identity
|
|
218
|
+
exchanges for — like `gh auth token` — and that one is **not** revoked on exit,
|
|
219
|
+
since the output is the token. It still expires in fifteen minutes.
|
|
220
|
+
|
|
221
|
+
**`tokens`** manages personal access tokens, and the server allows that from a
|
|
222
|
+
**browser session only**: a token that could list, mint or revoke tokens could
|
|
223
|
+
outlive its own revocation. Since `login` now stores a token, `tokens list` and
|
|
224
|
+
`tokens revoke` usually answer with a pointer to the web app (Account →
|
|
225
|
+
Personal access tokens, `<deployment>/?settings=account.tokens`); they work
|
|
226
|
+
from a login that stored a session token. `tokens create` always opens that
|
|
227
|
+
page — a new token's secret is shown once, and a terminal is the wrong place.
|
|
228
|
+
|
|
229
|
+
**`tokens create --sandbox-agent`** is the exception, and the one command an
|
|
230
|
+
agent runs to get its own credential. It mints a **sandbox agent token**
|
|
231
|
+
(`oxy_sbx_…`): the [sandbox loop](#sandboxes) on one to five named apps, in
|
|
232
|
+
sandboxes the token created, for 8 hours by default (`--hours`, 1 to 168) —
|
|
233
|
+
and nothing else on the deployment.
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
eval "$(oxyc tokens create --sandbox-agent --app acme/store --env dev)"
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
It opens the same browser loopback as `login`, with the apps and the lifetime
|
|
240
|
+
on the page; the agent's operator approves once, under their own session.
|
|
241
|
+
Stdout is the single line `export OXY_TOKEN=oxy_sbx_…`, printed once, and
|
|
242
|
+
everything else is on stderr. It holds no credential while it runs, never
|
|
243
|
+
writes the credentials file, and revokes no other token. `--app` is
|
|
244
|
+
`<org>/<app>` and repeats; arguments are checked before the browser opens
|
|
245
|
+
(exit `2`). Only staff who can open the app's sandboxes can approve.
|
|
246
|
+
|
|
247
|
+
What comes back is checked before anything is printed: it must be an
|
|
248
|
+
`oxy_sbx_`, described as `sandbox_agent` and not all-access, for exactly the
|
|
249
|
+
apps named, expiring no later than the hours asked for. Anything the check
|
|
250
|
+
cannot read — an app entry with no slugs, an expiry that is missing or not a
|
|
251
|
+
time — is refused rather than skipped. A deployment that predates sandbox
|
|
252
|
+
agent tokens would otherwise hand back an ordinary login token with the
|
|
253
|
+
approver's whole reach. On any mismatch the command prints nothing, revokes
|
|
254
|
+
what was minted, and exits `8`.
|
|
255
|
+
|
|
256
|
+
With that token in `OXY_TOKEN`, `oxyc` behaves differently in five ways:
|
|
257
|
+
|
|
258
|
+
- **An app resolves against the token's own list** (`GET /api/auth/token`),
|
|
259
|
+
never `/api/admin/apps`. An app it was not minted for is exit `5`.
|
|
260
|
+
- **Read-back uses `/api/customer-apps/{id}/…`** — `invocations list`,
|
|
261
|
+
`invocations held` and `checks run` — since the token never reaches `/admin`.
|
|
262
|
+
- **Refused before any request, exit `2`:** an `--app-env` that is not
|
|
263
|
+
`dev-<handle>`; `publish` without `--app-env`, or with `--promote`;
|
|
264
|
+
`fn call`, `logs`, `invocations list` and `checks run` without `--app-env`;
|
|
265
|
+
`env show` of production or staging; `oxyc apps`, `tokens list` and
|
|
266
|
+
`tokens revoke <id>`; and `oxyc api` against any path.
|
|
267
|
+
- **`whoami`** prints the token's description — `kind`, `expires_at`, the
|
|
268
|
+
`apps` it reaches and its `minter` — and `--json` is that document as the
|
|
269
|
+
server sent it.
|
|
270
|
+
- **`tokens revoke --current`** ends it (`DELETE /api/auth/token`). Exit `0`
|
|
271
|
+
when it is revoked or was already dead. It works for any API token in the
|
|
272
|
+
variable; a cached login is ended with `logout` instead.
|
|
273
|
+
|
|
274
|
+
A refusal from the server carries a hint an agent can act on — never "try
|
|
275
|
+
`oxyc login` again". Exit `4` under this token means it expired, was revoked,
|
|
276
|
+
or its minter lost access: stop and report.
|
|
149
277
|
|
|
150
278
|
Credentials live in the OS config directory under **`oxy`** — the same file the
|
|
151
279
|
Rust `oxy login` wrote before it was removed, so an existing login still works:
|
|
@@ -153,8 +281,95 @@ Rust `oxy login` wrote before it was removed, so an existing login still works:
|
|
|
153
281
|
- macOS: `~/Library/Application Support/oxy/credentials.json`
|
|
154
282
|
- Linux: `$XDG_CONFIG_HOME/oxy/credentials.json`
|
|
155
283
|
|
|
156
|
-
`
|
|
157
|
-
|
|
284
|
+
An entry may now also carry `token_id` and `expires_at`; both are optional, and
|
|
285
|
+
a file written before they existed loads unchanged. `OXY_CREDENTIALS_PATH`
|
|
286
|
+
overrides the path. Caches are separate, under `oxyc` (`~/.cache/oxyc`).
|
|
287
|
+
|
|
288
|
+
### CI without a stored secret
|
|
289
|
+
|
|
290
|
+
In a GitHub Actions job granted `id-token: write`, with no `OXY_TOKEN` set,
|
|
291
|
+
**any** `oxyc` command signs itself in — **as the service account the workflow
|
|
292
|
+
names, by its ID,** in `OXY_SERVICE_ACCOUNT` (or `--service-account`). It asks
|
|
293
|
+
GitHub for an OIDC token whose audience is `oxy:<the deployment's host>`, posts
|
|
294
|
+
it with the account's ID to `/api/auth/oidc/exchange`, and uses the
|
|
295
|
+
fifteen-minute `oxy_ci_…` token that comes back, revoking it on exit.
|
|
296
|
+
|
|
297
|
+
The audience is **worked out from the URL `oxyc` is pointed at** — the host of
|
|
298
|
+
the resolved `--env` / `--target`, lowercased, with a port only when it is not
|
|
299
|
+
the scheme's default: `oxy:app.oxygen-hq.com`, `oxy:aip.staging.oxy.tech`. It is
|
|
300
|
+
never asked of the deployment, and nothing a server says can change it. So the
|
|
301
|
+
token a job gives to a host is good at the deployment that calls itself by that
|
|
302
|
+
host and at no other: one handed to staging cannot be replayed at production,
|
|
303
|
+
and a server cannot talk the job into minting a token for somewhere else.
|
|
304
|
+
There is no shared fallback, and plain `oxy` is never requested.
|
|
305
|
+
|
|
306
|
+
A deployment that answers to a different address than the one `oxyc` was
|
|
307
|
+
pointed at — reached through an alias or a port-forward — refuses with
|
|
308
|
+
`wrong_audience` and names the address it does answer to: point `--target` at
|
|
309
|
+
that. One with no public URL configured (`OXY_API_URL`) takes no GitHub
|
|
310
|
+
sign-in at all; use `OXY_TOKEN` there.
|
|
311
|
+
|
|
312
|
+
What makes the deployment honour the job is a
|
|
313
|
+
**trust policy** on that service account, naming the repository, the workflow
|
|
314
|
+
file and the job's `environment:` — registered by `oxyc init-ci`, or in the web
|
|
315
|
+
app under Organization settings → API access → Service accounts → *the
|
|
316
|
+
account* → Trusted access.
|
|
317
|
+
|
|
318
|
+
**The account is always named, never guessed.** Anyone can register a trust
|
|
319
|
+
policy that names a repository they do not own, so the deployment matches a
|
|
320
|
+
run against the policies of the one account its workflow asked for and no
|
|
321
|
+
other. With no account named, `oxyc` attempts no exchange at all: `publish` and
|
|
322
|
+
`checks run` go straight to the app's registered publisher (below), and every
|
|
323
|
+
other command fails with "not authenticated", saying `OXY_SERVICE_ACCOUNT` is
|
|
324
|
+
what is missing.
|
|
325
|
+
|
|
326
|
+
**By ID, never by `<org>/<name>`.** An org's slug can be changed, and once the
|
|
327
|
+
org renames or is deleted the slug is free for anyone — who could create the
|
|
328
|
+
same account name under it and register a policy on your repository's public
|
|
329
|
+
ids. A workflow that said `acme/deployer` would then be naming *their*
|
|
330
|
+
account. An ID never changes hands, so it is the only thing the deployment
|
|
331
|
+
takes: anything else answers `400 service_account_required`, and `oxyc` does
|
|
332
|
+
not fall back from that. The ID is shown in the web app (Organization settings
|
|
333
|
+
→ API access → Service accounts → *the account*), and `oxyc init-ci` writes it
|
|
334
|
+
for you. `oxyc` passes the value through untouched; the answer still says
|
|
335
|
+
`<org>/<name>`, so the log reads as it did.
|
|
336
|
+
|
|
337
|
+
```yaml
|
|
338
|
+
permissions: { id-token: write, contents: read }
|
|
339
|
+
jobs:
|
|
340
|
+
deploy:
|
|
341
|
+
environment: production # the trust policy requires it
|
|
342
|
+
runs-on: ubuntu-latest
|
|
343
|
+
steps:
|
|
344
|
+
- uses: actions/checkout@v4
|
|
345
|
+
- uses: actions/setup-node@v4
|
|
346
|
+
with: { node-version: 20 }
|
|
347
|
+
# or any oxyc command the grants allow
|
|
348
|
+
- run: npx --yes @oxy-hq/cli publish --promote
|
|
349
|
+
env:
|
|
350
|
+
OXY_SERVICE_ACCOUNT: 3f2504e0-4f89-41d3-9a0c-0305e82c3301 # acme/deployer
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Each command exchanges for itself, which is all a job running one command
|
|
354
|
+
needs. Several policies of the named account matching one run is not an error:
|
|
355
|
+
the oldest is used. The exchange is rate-limited per client address (60 a
|
|
356
|
+
minute); a `429` is waited out once, for its `Retry-After` (at most 60 s), and
|
|
357
|
+
retried before it fails.
|
|
358
|
+
|
|
359
|
+
The `setup-oxyc` action (`sdk/setup-oxyc`) does the exchange once for a whole
|
|
360
|
+
job: it installs `oxyc`, exports `OXY_TOKEN` for the later steps and revokes
|
|
361
|
+
the token in a post step. **It is not published yet** — `uses:
|
|
362
|
+
oxy-hq/setup-oxyc@v1` does not resolve, and a job naming it fails at "Set up
|
|
363
|
+
job" — so nothing here depends on it.
|
|
364
|
+
|
|
365
|
+
A refused exchange says what to fix: `no_matching_policy` prints the
|
|
366
|
+
repository, workflow, ref and event the run presented and where to register a
|
|
367
|
+
policy on the named account; `missing_environment`, `self_hosted_runner` and
|
|
368
|
+
`pull_request_target` name the rule that was not met. A deployment with no
|
|
369
|
+
exchange route (404) reads as "not authenticated", with that reason — except
|
|
370
|
+
in `publish` and `checks run`, which fall back to the app's registered
|
|
371
|
+
publisher, as they also do on `no_matching_policy` and when no account is
|
|
372
|
+
named.
|
|
158
373
|
|
|
159
374
|
## Acting as an organization
|
|
160
375
|
|
|
@@ -181,6 +396,8 @@ Staff can see and request an org's transactional Postgres — the store behind
|
|
|
181
396
|
```bash
|
|
182
397
|
oxyc oltp status [--org <slug|uuid|url>] [--json]
|
|
183
398
|
oxyc oltp provision --org <slug|uuid|url> --writer app:<slug> [--writer pipeline:<source> …] [--yes] [--json]
|
|
399
|
+
oxyc oltp provision --org <slug|uuid|url> --branch staging [--yes] [--json]
|
|
400
|
+
oxyc oltp reset --org <slug|uuid|url> --branch staging [--yes] [--json]
|
|
184
401
|
```
|
|
185
402
|
|
|
186
403
|
`status` lists every org, with a database or without; with `--org` it shows
|
|
@@ -194,6 +411,15 @@ derived the way the platform derives `ctx.oltp`'s schema (`app:store-ops` →
|
|
|
194
411
|
`raw_<source>`. A 404 can be an org outside your staff grant's scope, and an
|
|
195
412
|
active `assume` session closes `/admin`.
|
|
196
413
|
|
|
414
|
+
`--branch staging` also cuts the org's **staging branch**: one copy of the whole
|
|
415
|
+
database per org, which every app's staging environment writes to. It is made
|
|
416
|
+
only by this flag — never by a publish — and re-running it mints whatever a
|
|
417
|
+
writer added since the cut needs, without resetting anyone's data. `status`
|
|
418
|
+
shows its age and calls it stale past 30 days; nothing resets it on a timer.
|
|
419
|
+
`reset --branch staging` re-copies it from production, discarding every app's
|
|
420
|
+
staging data in the org, so it lists those apps and asks first (`--yes`
|
|
421
|
+
without a terminal).
|
|
422
|
+
|
|
197
423
|
## Customer workspaces
|
|
198
424
|
|
|
199
425
|
A customer **is** a GitHub repo carrying the `oxy-customer` topic. Tagging the
|
|
@@ -225,8 +451,8 @@ the current directory while granting access to the customer's repo.
|
|
|
225
451
|
## Custom apps
|
|
226
452
|
|
|
227
453
|
```bash
|
|
228
|
-
oxyc publish [--env <e>] [--dir <path>] [--promote] [--build-only | --prebuilt] [--json] [--allow-function-lint]
|
|
229
|
-
oxyc init-ci [--app <org>/<app>] [--environment <name>] [--force]
|
|
454
|
+
oxyc publish [--env <e>] [--dir <path>] [--promote] [--build-only | --prebuilt] [--json] [--allow-function-lint] [--app-env <dev-handle>]
|
|
455
|
+
oxyc init-ci [--app <org>/<app>] [--environment <name>] [--promote] [--force] [--no-register] [--setup-action]
|
|
230
456
|
oxyc proxy [--port <n>] [--allow-writes] [--allow-events] [--yes]
|
|
231
457
|
```
|
|
232
458
|
|
|
@@ -245,11 +471,16 @@ channel unless `--promote`. `--env` defaults to **production**; name it.
|
|
|
245
471
|
**`--prebuilt`** (with `--dir`) skips esbuild and refuses if a declared
|
|
246
472
|
function's `functions/<name>.js` is missing. The pair splits CI so the job that
|
|
247
473
|
runs package scripts never holds the credential.
|
|
248
|
-
- **Auth**:
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
474
|
+
- **Auth**: `OXY_TOKEN`, then the login cache — or, in a GitHub Actions job
|
|
475
|
+
with `id-token: write` and neither set, the job's OIDC token. (A variable
|
|
476
|
+
named with `--token-env` is the only source: unset, the publish stops.) Two exchanges are tried, in order, and the exchange happens last, after
|
|
477
|
+
the build. First the general one (`/api/auth/oidc/exchange`, audience `oxy:<the deployment's host>`):
|
|
478
|
+
a **trust policy** on a service account (see `init-ci`). Then, only when the
|
|
479
|
+
deployment has no such route (404) or answers `no_matching_policy`, the app's
|
|
480
|
+
own **registered publisher** (`/api/customer-apps/publish/oidc-exchange`,
|
|
481
|
+
audience `oxy-publish`) — the registration every workflow written before trust
|
|
482
|
+
policies relies on; it needs the org **slug**. Any other refusal is final: it
|
|
483
|
+
names something wrong with the run, which the older path would only hide.
|
|
253
484
|
- **Provenance**: the checkout's `origin`, `HEAD` and branch (else `GITHUB_SHA` /
|
|
254
485
|
`GITHUB_REF_NAME`) are recorded; a missing half or a dirty app directory is a
|
|
255
486
|
warning, never a failure. The build id defaults to
|
|
@@ -270,12 +501,147 @@ channel unless `--promote`. `--env` defaults to **production**; name it.
|
|
|
270
501
|
skipped. **`--allow-function-lint`** is the way past a false positive: every
|
|
271
502
|
finding becomes a warning that names its rule — please open an issue with it.
|
|
272
503
|
|
|
273
|
-
**`init-ci`** writes
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
504
|
+
**`init-ci`** does two things: writes the workflow, and registers the trust that
|
|
505
|
+
lets it publish.
|
|
506
|
+
|
|
507
|
+
*The workflow* is `.github/workflows/oxy-publish.yml` at the repo root: a
|
|
508
|
+
`build` job (no id-token) that runs `publish --build-only`, and a `publish` job
|
|
509
|
+
bound to a GitHub `environment:` (`--environment`, default `oxy-publish`) whose
|
|
510
|
+
only work is `publish --prebuilt`. That job uses no third-party action: each
|
|
511
|
+
step runs `npx @oxy-hq/cli@<this version>` and `oxyc` exchanges the job's OIDC
|
|
512
|
+
token for itself, acting as the service account `--service-account <org>/<name>`
|
|
513
|
+
names — by default `<app's org>/deployer`. **Here, and only here, the account is
|
|
514
|
+
given by name:** `init-ci` is run by a person, looks the account's ID up as that
|
|
515
|
+
person, and writes the ID into the workflow with the name beside it as a
|
|
516
|
+
comment (`OXY_SERVICE_ACCOUNT: 3f25…3301 # acme/deployer`). When it cannot
|
|
517
|
+
look the ID up — nobody logged in, no such account yet — it writes a
|
|
518
|
+
`<service-account-id>` placeholder and says so; it never writes the name as
|
|
519
|
+
the value. `--promote` makes the publish go live. **No `checks run` step is
|
|
520
|
+
written**, even when the manifest declares a check: the job's service-account
|
|
521
|
+
token is answered `403` on the check routes, which sit behind platform gates a
|
|
522
|
+
service account never passes, so the step could not succeed. The workflow
|
|
523
|
+
carries a one-line comment where it would be, and `init-ci` says so when it
|
|
524
|
+
writes one. Run the checks with a staff credential until `ci` tokens may.
|
|
525
|
+
`--setup-action` writes the same workflow around `uses: oxy-hq/setup-oxyc@v1`
|
|
526
|
+
instead, which exchanges once for the whole job. That action is not published
|
|
527
|
+
yet, so a workflow written with the flag cannot start until it is; `init-ci`
|
|
528
|
+
says so when it writes one.
|
|
529
|
+
|
|
530
|
+
*The registration* is a trust policy on that service account — this repository,
|
|
531
|
+
the workflow file, the environment — granted publishing this one app and
|
|
532
|
+
nothing else. `init-ci` creates the `deployer` account if none was named and it
|
|
533
|
+
does not exist, then the policy, and is idempotent: a second run finds both. A
|
|
534
|
+
named account is never created, and must be one of the app's own org — an
|
|
535
|
+
account's grants never reach another org, so `init-ci` refuses one. A service
|
|
536
|
+
account's name is lowercase words joined by single hyphens, starting with a
|
|
537
|
+
letter, 2–40 characters.
|
|
538
|
+
|
|
539
|
+
It can only register **when the caller can**: an org admin, logged in with a
|
|
540
|
+
browser-session credential — the server refuses those two creations to a token,
|
|
541
|
+
which is what a fresh `oxyc login` stores. When it cannot (not an admin, a token
|
|
542
|
+
login, offline, a deployment without trusted access, `--no-register`), the
|
|
543
|
+
workflow is still written and the command prints the exact steps instead: where
|
|
544
|
+
in the web app, the `oxyc api` calls with every id it managed to learn filled
|
|
545
|
+
in, and the older `…/publishers` registration for a deployment without trust
|
|
546
|
+
policies. Registration never fails the command.
|
|
547
|
+
|
|
548
|
+
**`--app-env <dev-handle>`** moves one sandbox's build pointer instead of the
|
|
549
|
+
draft/live channel — see [Sandboxes](#sandboxes). It needs a staff credential
|
|
550
|
+
(never a publish token or OIDC) and is refused together with `--promote`: a
|
|
551
|
+
sandbox build is never promoted, so the loop is publish to the sandbox,
|
|
552
|
+
iterate, then publish the same tree to staging and promote that.
|
|
553
|
+
|
|
554
|
+
## Apps
|
|
555
|
+
|
|
556
|
+
Read-only: every request these make is a GET, against `/api/customer-apps/**`
|
|
557
|
+
(the app-admin role). `<app>` is `<org-slug>/<app-slug>` or an app UUID.
|
|
558
|
+
|
|
559
|
+
```bash
|
|
560
|
+
oxyc apps list [--org <slug>] [--published | --draft] [--builds] [--json]
|
|
561
|
+
oxyc apps show <app> [--json]
|
|
562
|
+
oxyc apps builds <app> [--json]
|
|
563
|
+
oxyc apps health [<app>] [--needs-attention] [--json]
|
|
564
|
+
oxyc apps usage <app> [--json]
|
|
565
|
+
oxyc apps drift [<app>] [--org <slug>] [--dir <path>] [--refresh] [--json]
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
- **`list`** — every app you can see, across organizations, walking the paged
|
|
569
|
+
registry to its end. `STATE` is `live` when the app has a published build and
|
|
570
|
+
`draft` when it has none. `SOURCE` is the repository the app is *registered*
|
|
571
|
+
against. `--builds` adds the live build's id, the repository and commit it was
|
|
572
|
+
built from, who published it and when — one extra request per app, six at a
|
|
573
|
+
time.
|
|
574
|
+
- **`show`** — one app on one screen: the registry row, the live build, the
|
|
575
|
+
draft build when it is newer than the live one, deployment integrity,
|
|
576
|
+
availability, and the 7-day usage summary. `last promote` is when the promote
|
|
577
|
+
or rollback action was last used; `oxyc publish --promote` does not update it,
|
|
578
|
+
so it can be older than the live build.
|
|
579
|
+
- **`builds`** — the build history, newest first. `CHANNEL` marks the build each
|
|
580
|
+
channel points at: `live`, `draft`, or both for one build.
|
|
581
|
+
- **`health`** — with no argument, the fleet table (published apps only):
|
|
582
|
+
`down`, `degraded`, `not_measured`, `quiet` or `operational` per app, with the
|
|
583
|
+
totals on stderr. `--needs-attention` keeps the first three. With `<app>`: the
|
|
584
|
+
deployment-integrity checks, the availability windows, and the browser errors
|
|
585
|
+
of the last 24 hours.
|
|
586
|
+
- **`usage`** — the 7-day summary, the visitors, and the tracked events by name.
|
|
587
|
+
- **`drift`** — see below.
|
|
588
|
+
|
|
589
|
+
Times are UTC. `--json` prints one document on stdout; a table goes to stdout and
|
|
590
|
+
the count line to stderr.
|
|
591
|
+
|
|
592
|
+
**An empty result and a failed request are different answers.** No apps, no
|
|
593
|
+
builds, no errors recorded: the command says so and exits `0`. A request that
|
|
594
|
+
failed exits with the code its status maps to (`4`, `5`, `7`, …). Where a
|
|
595
|
+
command makes several independent requests (`show`, `health <app>`,
|
|
596
|
+
`list --builds`, `drift`), it prints what it did read, marks what it did not as
|
|
597
|
+
`NOT READ` (`"failed": […]` / `live_build_error` in `--json`), and then exits
|
|
598
|
+
non-zero. These commands report state: a failing health check, a `down` app or
|
|
599
|
+
a drifted app is an answer and exits `0` — read it from the output.
|
|
600
|
+
|
|
601
|
+
The health route answers **503 with its report** when a check fails; `oxyc`
|
|
602
|
+
prints that as a failing report, not as an outage.
|
|
603
|
+
|
|
604
|
+
### `apps drift`
|
|
605
|
+
|
|
606
|
+
Compares what is live against source on **this machine**. For each app it takes
|
|
607
|
+
the live build's recorded repository and commit, finds a local checkout of that
|
|
608
|
+
repository (the ones `oxyc repos` and `oxyc path` report, or `--dir`), finds the
|
|
609
|
+
app's directory by the `oxy-app.json` whose `slug` and `orgSlug` match — at
|
|
610
|
+
`<app>/oxy-app.json` or `<app>/public/oxy-app.json`, never by folder name — and
|
|
611
|
+
counts the commits on the checkout's **current branch** that touch that
|
|
612
|
+
directory after the published commit.
|
|
613
|
+
|
|
614
|
+
| Result | Meaning |
|
|
615
|
+
| --- | --- |
|
|
616
|
+
| `in sync` | no commit on the current branch touches the app directory after the published commit |
|
|
617
|
+
| `N commits ahead` | that many do; they are listed |
|
|
618
|
+
| `unknown — <reason>` | the comparison could not be made |
|
|
619
|
+
|
|
620
|
+
`unknown` is never reported as `in sync`. Its reasons (`reason` in `--json`):
|
|
621
|
+
|
|
622
|
+
| `reason` | |
|
|
623
|
+
| --- | --- |
|
|
624
|
+
| `not_published` | nothing is live |
|
|
625
|
+
| `source_unrecorded` | the live build records no repository or no commit |
|
|
626
|
+
| `repo_not_checked_out` | the repository is not on this machine — clone it, or pass `--dir` |
|
|
627
|
+
| `commit_not_in_checkout` | the checkout does not have the published commit — `git fetch` there |
|
|
628
|
+
| `app_dir_not_found` | no tracked `oxy-app.json` declares this `slug` and `orgSlug` |
|
|
629
|
+
| `app_dir_ambiguous` | more than one does |
|
|
630
|
+
| `commit_not_on_branch` | the published commit is not an ancestor of the checked-out branch |
|
|
631
|
+
| `working_tree_dirty` | the app directory has uncommitted or untracked files |
|
|
632
|
+
| `git_failed` | `git log` or `git status` failed in the checkout, so the comparison could not be read |
|
|
633
|
+
| `request_failed` | the builds request failed (the command then exits non-zero) |
|
|
634
|
+
|
|
635
|
+
It runs read-only git commands and nothing else: it does not fetch, check out,
|
|
636
|
+
or refresh the index. The answer is therefore about the checkout as it is on
|
|
637
|
+
disk — a branch that is behind its remote reports fewer commits than the remote
|
|
638
|
+
has. With no `<app>`, every published app is compared, and `--dir` applies to
|
|
639
|
+
the apps whose source repository is that checkout's `origin`; with `<app>`,
|
|
640
|
+
`--dir` is used whatever its remote is.
|
|
641
|
+
|
|
642
|
+
An app published to two organizations from one directory (a staging org and a
|
|
643
|
+
production org) matches the manifest for only one of them; the other reports
|
|
644
|
+
`app_dir_not_found` and names the manifest it found with the other `orgSlug`.
|
|
279
645
|
|
|
280
646
|
## Checks
|
|
281
647
|
|
|
@@ -305,13 +671,21 @@ difference: `checks run` talks to `/api/admin/**`, which is not the
|
|
|
305
671
|
`oxyc login` (or `--token-env`) always wins when one is present.
|
|
306
672
|
|
|
307
673
|
**In CI, nothing is stored.** With neither a token nor an API key set, a job
|
|
308
|
-
holding `id-token: write`
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
674
|
+
holding `id-token: write` signs itself in from its GitHub OIDC token — the same
|
|
675
|
+
two exchanges `oxyc publish` tries, in the same order: a service account's
|
|
676
|
+
trust policy first, the app's registered publisher second — and drives
|
|
677
|
+
`/api/customer-apps/**` instead: the same three handlers, mounted where a
|
|
678
|
+
machine credential may reach them. A service account's token (`oxy_sat_…`,
|
|
679
|
+
`oxy_ci_…`) or a publish token set in `OXY_TOKEN` takes that surface too,
|
|
680
|
+
without minting; a personal token, a legacy API key or a session takes the admin
|
|
681
|
+
one. `OXY_API_KEY` is read **before** OIDC here: a job that set one has said
|
|
682
|
+
which credential it means.
|
|
683
|
+
|
|
684
|
+
The app id comes from the credential where it can. The publisher exchange
|
|
685
|
+
returns it, so `<app>` must be `<org-slug>/<app-slug>` on that path (a UUID
|
|
686
|
+
names an app it cannot verify a publisher for). A service account's token names
|
|
687
|
+
the apps it may publish, so a slug resolves against its own grants — and when
|
|
688
|
+
it cannot, the publisher exchange is tried before asking for a UUID.
|
|
315
689
|
|
|
316
690
|
What that credential may do is deliberately small, and enforced server-side
|
|
317
691
|
rather than by the client. An app-scoped token is **confined to its app**: it
|
|
@@ -329,23 +703,164 @@ it admitted any function.
|
|
|
329
703
|
**A check runs against the app's live build** — the server resolves
|
|
330
704
|
`published_build_id`, else `draft_build_id`, for the pre-flight lookup and for
|
|
331
705
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
706
|
+
*previously promoted* code, not the draft just uploaded. (`oxyc init-ci` writes
|
|
707
|
+
no checks step at all today: a service account's `oxy_ci_` token is answered
|
|
708
|
+
`403` on these routes — the token's scope admits them for its own app, and the
|
|
709
|
+
routes' platform gates then refuse a caller with no platform standing.) **With
|
|
710
|
+
`--app-env`**
|
|
711
|
+
it runs against that environment's own build instead — a sandbox's own
|
|
712
|
+
publish, or staging's — and the report gains `environment` and each check's
|
|
713
|
+
`invocationId`; the publish-token and OIDC paths are unchanged and work only
|
|
714
|
+
against production, since every non-production operation needs a staff
|
|
715
|
+
credential (see [Sandboxes](#sandboxes)).
|
|
716
|
+
|
|
717
|
+
With a publish token (`oxypublish_…`), `<app>` must be the **UUID** — resolving
|
|
718
|
+
a slug means listing every app, which such a token may not do. A service
|
|
719
|
+
account's token takes either: the UUID, or a slug it can match to one of its
|
|
720
|
+
own app-publish grants. The publisher exchange needs no id at all (it returns
|
|
721
|
+
it) but does need `<org-slug>/<app-slug>`, since it is registered by slug.
|
|
340
722
|
|
|
341
723
|
**Exit codes:** `0` every check passed; `9` (`CHECK_FAILED`) at least one check
|
|
342
724
|
failed or timed out; `1` the app declares no checks; `5` the app was not
|
|
343
725
|
found; `4` no credential resolved, or the API rejected it (401/403 — an
|
|
344
|
-
expired
|
|
726
|
+
expired legacy API key or API token reads this way, not as a missing one); `2` a bad
|
|
345
727
|
`--timeout`, or a slug where the credential needs a UUID; `7` (`UNAVAILABLE`)
|
|
346
728
|
the deployment answered the OIDC exchange without an `app_id`, meaning it
|
|
347
729
|
predates trusted checks.
|
|
348
730
|
|
|
731
|
+
## Sandboxes
|
|
732
|
+
|
|
733
|
+
```bash
|
|
734
|
+
oxyc env create <app> <name> # name is dev-<handle>, 1-12 chars a-z0-9-
|
|
735
|
+
oxyc env list <app>
|
|
736
|
+
oxyc env show <app> <name>
|
|
737
|
+
oxyc env delete <app> <name> [--yes] [--wait [seconds]]
|
|
738
|
+
|
|
739
|
+
oxyc publish --app-env <name> [--env <e>] # see "Custom apps" above
|
|
740
|
+
oxyc fn call <app> <function> [--app-env <name>] [--data <json|@file|->]
|
|
741
|
+
oxyc checks run <app> --app-env <name> # see "Checks" above
|
|
742
|
+
|
|
743
|
+
oxyc invocations list <app> [--app-env] [--build] [--function] [--limit]
|
|
744
|
+
oxyc invocations held <app> <invocation-id>
|
|
745
|
+
oxyc logs <app> [--app-env] [--invocation] [--request] [--hours] [--limit]
|
|
746
|
+
|
|
747
|
+
oxyc env secret list <app> --app-env <name> # keys and flags, never a value
|
|
748
|
+
oxyc env secret set <app> <key> --app-env <name> (--value <text> | --value-env <VAR>)
|
|
749
|
+
oxyc env secret delete <app> <key> --app-env <name>
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
A **sandbox** is a named, short-lived `dev-<handle>` environment of one custom
|
|
753
|
+
app: its own build pointer, storage silo, secrets, Airhouse sibling and — in
|
|
754
|
+
an org with an OLTP staging branch — its own copy of the app's database, with
|
|
755
|
+
reads hitting production and most writes either isolated or *held* (recorded,
|
|
756
|
+
not performed — see `invocations held`). It starts with no build, is deleted
|
|
757
|
+
explicitly or after 7 idle days, and an app has at most 20. Full contract,
|
|
758
|
+
including what is **not** isolated (`ctx.warehouse`) and the other gaps:
|
|
759
|
+
`internal-docs/custom-app-sandboxes.md`.
|
|
760
|
+
|
|
761
|
+
`ctx.oltp` in a sandbox runs in a schema of its own (`app_<app>__dev_<handle>`)
|
|
762
|
+
inside the org's staging branch. `publish --app-env` queues it: a copy of
|
|
763
|
+
staging's tables (rows up to a size cap), then the build's OLTP migrations.
|
|
764
|
+
`env show` reports it as `oltp_schema` — `status` is `seeding`, `ready`,
|
|
765
|
+
`failed` (with `error`) or `stale` (the branch was reset; publish again), and
|
|
766
|
+
`structure_only` lists the tables copied empty. Until it is `ready` a
|
|
767
|
+
`ctx.oltp` call in the sandbox is refused with a message that says which.
|
|
768
|
+
|
|
769
|
+
The loop: `env create` → `publish --app-env` → `fn call` / `checks run
|
|
770
|
+
--app-env` → `invocations list` / `held` to read back what ran → iterate from
|
|
771
|
+
`publish` → `env delete --yes --wait` when done. `oxyc guide` has the same six
|
|
772
|
+
lines — between the two an agent adds to mint and revoke its own token — meant
|
|
773
|
+
to sit in an agent's context.
|
|
774
|
+
|
|
775
|
+
**`--app-env <environment>`** (`production`, `staging` or `dev-<handle>`,
|
|
776
|
+
validated client-side — exit `2` on a malformed name) is a *different axis*
|
|
777
|
+
from `--env`, which is the deployment: both can appear on the same command
|
|
778
|
+
(`oxyc fn call acme/store f --env dev --app-env dev-a1`). It is not a global
|
|
779
|
+
flag — only `publish`, `fn call`, `checks run`, `invocations list` and `logs`
|
|
780
|
+
take it. A publish-token credential is refused before any request for
|
|
781
|
+
`--app-env` other than `production` (exit `2`); `env`, `invocations` and
|
|
782
|
+
`logs` refuse one outright, for any environment, because sandbox management
|
|
783
|
+
is a staff console surface.
|
|
784
|
+
|
|
785
|
+
**An unattended agent drives this loop with a sandbox agent token**
|
|
786
|
+
(`oxy_sbx_…`), not a staff credential: `oxyc tokens create --sandbox-agent
|
|
787
|
+
--app <org>/<app>` mints one after a browser approval — see
|
|
788
|
+
[Authentication](#authentication) for what `oxyc` then refuses, and
|
|
789
|
+
`internal-docs/custom-app-sandboxes.md` §2 for the agent's procedure. With
|
|
790
|
+
it, `--app-env dev-<handle>` is required on every verb above that takes one.
|
|
791
|
+
|
|
792
|
+
**`env secret`** reads and writes one sandbox's own secrets, for any
|
|
793
|
+
credential that reaches the sandbox, and only a `dev-<handle>` environment —
|
|
794
|
+
never staging's or production's. `list` prints each key with `is_set`,
|
|
795
|
+
`required` and `inherits_staging`; no verb returns a value. `set` takes the
|
|
796
|
+
value from `--value`, or from an environment variable with `--value-env` so it
|
|
797
|
+
stays off the command line. A sandbox with no value of its own reads
|
|
798
|
+
staging's.
|
|
799
|
+
|
|
800
|
+
**`env delete`** without `--yes` asks on a terminal and exits `8` (refused)
|
|
801
|
+
off one, same as `oltp reset`. `--wait [seconds]` (default 120) polls until
|
|
802
|
+
the teardown finishes and prints `{"name","status":"deleted"}`; past the
|
|
803
|
+
deadline it exits `7` (retryable) rather than hanging.
|
|
804
|
+
|
|
805
|
+
**`fn call`** POSTs directly to `{target}/customer-apps/{org}/{app}/fn/{name}`
|
|
806
|
+
(no `/api`) — the response is Server-Sent Events, not JSON, so `oxyc` parses
|
|
807
|
+
the `log` / `data` / `done` / `error` frames itself. It exits `0` only when
|
|
808
|
+
the stream ends `done` with a 2xx status; a function that threw, or answered a
|
|
809
|
+
non-2xx, is a function-level failure (exit `1`, `ok: false` in the JSON), kept
|
|
810
|
+
distinct from a non-2xx on the POST itself (exit per the usual status mapping
|
|
811
|
+
— e.g. `EnvironmentHasNoBuild` before the stream starts). The
|
|
812
|
+
`x-oxy-invocation-id` response header becomes `invocationId` in the result.
|
|
813
|
+
|
|
814
|
+
**`invocations held`** is what the non-production write policy did *not* do —
|
|
815
|
+
production's would-be effect, recorded rather than performed. It is `[]` for a
|
|
816
|
+
production invocation, since nothing is held there.
|
|
817
|
+
|
|
818
|
+
## Workspace previews
|
|
819
|
+
|
|
820
|
+
```bash
|
|
821
|
+
oxyc preview create <branch> [--wait [seconds]] [--json]
|
|
822
|
+
oxyc preview list [--json]
|
|
823
|
+
oxyc preview show <branch> [--json]
|
|
824
|
+
oxyc preview delete <branch> [--yes] [--json]
|
|
825
|
+
oxyc preview checks <branch> [--json]
|
|
826
|
+
|
|
827
|
+
oxyc preview run <branch> <kind> <ref> [--variables <json|@file|->] [--read-live-only]
|
|
828
|
+
[--window-from <iso>] [--window-to <iso>] [--resource <name>...]
|
|
829
|
+
[--wait [seconds]] [--json]
|
|
830
|
+
oxyc preview runs list <branch> [--json]
|
|
831
|
+
oxyc preview runs show <run-id> [--wait [seconds]] [--json]
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
A **preview** opens a workspace branch on the real product, against real data,
|
|
835
|
+
without the branch being live — staff only. `create` compiles the branch's
|
|
836
|
+
head into a staging revision (or reuses a ready one for that commit) and is
|
|
837
|
+
idempotent; `--wait [seconds]` (default 120) blocks until the compile is
|
|
838
|
+
`ready` or `failed` instead of returning the `compiling` item the request
|
|
839
|
+
itself answers with, and exits `7` (retryable) past the deadline. There is no
|
|
840
|
+
single-preview route: `show` fetches `list` and filters it client-side, so its
|
|
841
|
+
`5` (not found) is synthesized here, not a literal 404.
|
|
842
|
+
|
|
843
|
+
The loop: `preview create --wait` → `preview checks` → `preview run` →
|
|
844
|
+
`preview runs show --wait` → iterate → `preview delete --yes` when done.
|
|
845
|
+
|
|
846
|
+
**`<kind>`** is `procedure` or `airway_sample` — the only two `POST
|
|
847
|
+
/previews/runs` accepts (exit `2` client-side on anything else, naming the
|
|
848
|
+
read-back verb). `transform_build` and `compare` runs are queued by the
|
|
849
|
+
server's own Airway change check, never started here; `preview runs list`
|
|
850
|
+
surfaces them alongside the kinds you *can* start, and `preview runs show
|
|
851
|
+
<run-id> --wait` reads any of the four back — a procedure's held steps, a
|
|
852
|
+
transform build's or compare's outcome, or a sample's ask and result.
|
|
853
|
+
`--variables` / `--read-live-only` are `procedure`-only; `--window-from` /
|
|
854
|
+
`--window-to` / `--resource` (repeatable) are `airway_sample`-only.
|
|
855
|
+
|
|
856
|
+
Every verb needs `{workspace}` resolved — `--workspace <id>`, exactly the way
|
|
857
|
+
`oxyc api {workspace}/...` resolves it; there is no second workspace
|
|
858
|
+
resolver. A deployment with `OXY_PREVIEW_RUNS` off answers every runs route
|
|
859
|
+
`404 preview_runs_disabled`, surfaced the same way any other server refusal
|
|
860
|
+
is: the server's own `code` rides along on the thrown error, and the exit
|
|
861
|
+
class follows the usual HTTP-status mapping (`400`/`409` → `6`, `404` → `5`).
|
|
862
|
+
Full contract: `internal-docs/workspace-previews.md`.
|
|
863
|
+
|
|
349
864
|
## Development commands
|
|
350
865
|
|
|
351
866
|
```bash
|
|
@@ -424,14 +939,46 @@ production target (any host under `oxygen-hq.com`) is refused without `--yes`.
|
|
|
424
939
|
Each Oxy Function call carries a W3C trace — the SDK's, or one minted here — and
|
|
425
940
|
prints `↳ <status> fn <name> request_id=… trace_id=…`.
|
|
426
941
|
|
|
427
|
-
**`mcp`** serves the API over stdio as four tools — `oxy_routes`,
|
|
428
|
-
`oxy_request`, `oxy_whoami` — rather than one per endpoint, so
|
|
429
|
-
cost ~2 KB per turn and reach endpoints added after this
|
|
942
|
+
**`mcp`** serves the API over stdio as four generic tools — `oxy_routes`,
|
|
943
|
+
`oxy_schema`, `oxy_request`, `oxy_whoami` — rather than one per endpoint, so
|
|
944
|
+
those schemas cost ~2 KB per turn and reach endpoints added after this
|
|
945
|
+
package shipped. Two named exceptions trade that minimalism for validation an
|
|
946
|
+
agent needs to drive a loop unsupervised: the **sandbox loop**
|
|
947
|
+
(`oxy_env_create`/`list`/`show`/`delete`, `oxy_publish_sandbox`,
|
|
948
|
+
`oxy_fn_call`, `oxy_checks_run`, `oxy_invocations_list`/`held`, `oxy_logs`)
|
|
949
|
+
and **workspace previews** (`oxy_preview_create`/`list`/`show`/`delete`/
|
|
950
|
+
`checks`/`run`/`runs_list`/`run_show`). Each tool calls its CLI verb's own
|
|
951
|
+
request-building function, so a tool's result is the same JSON document that
|
|
952
|
+
verb's `--json` prints, and a failure carries the server's own error code
|
|
953
|
+
plus the exit-code class (`[exit 5 NOT_FOUND]`) the way `oxyc`'s exit code
|
|
954
|
+
would. `oxy_env_delete` / `oxy_preview_delete` need `confirm: true` (there is
|
|
955
|
+
no terminal to ask on inside an MCP server), and `oxy_publish_sandbox`
|
|
956
|
+
requires a `dev-<handle>` `appEnv` and refuses production, staging or a
|
|
957
|
+
promote client-side — it can never reach the live channel.
|
|
958
|
+
|
|
959
|
+
**Its credential is the token variable and nothing else.** An agent runtime
|
|
960
|
+
starts `oxyc mcp` with the agent's own token in `OXY_TOKEN` (or the
|
|
961
|
+
`--token-env` variable); with the variable unset the server exits `4` before
|
|
962
|
+
serving a tool, rather than running on the `oxyc login` of whoever owns the
|
|
963
|
+
machine. To serve on your own login, start it with `--login`.
|
|
430
964
|
|
|
431
965
|
```bash
|
|
432
|
-
|
|
966
|
+
# an agent, on its own token
|
|
967
|
+
claude mcp add oxyc -e OXY_TOKEN=oxy_sbx_… -- npx -y @oxy-hq/cli mcp --env dev
|
|
968
|
+
# you, on your login
|
|
969
|
+
claude mcp add oxyc -- npx -y @oxy-hq/cli mcp --login --env production
|
|
433
970
|
```
|
|
434
971
|
|
|
972
|
+
**A sandbox agent token (`oxy_sbx_…`) is served a different, smaller list**:
|
|
973
|
+
the ten sandbox-loop tools, `oxy_whoami` (the token's `kind`, `expires_at`,
|
|
974
|
+
`apps` and `minter`), and four that exist only for it — `oxy_env_secret_list`,
|
|
975
|
+
`oxy_env_secret_set` and `oxy_env_secret_delete`, which accept only a
|
|
976
|
+
`dev-<handle>` `appEnv`, and `oxy_token_revoke`, which needs `confirm: true`.
|
|
977
|
+
`oxy_request`, `oxy_routes`, `oxy_schema` and the preview tools are dropped,
|
|
978
|
+
and refused if called by name: the server answers that token `404` on all of
|
|
979
|
+
them, and a tool that can only fail still costs its schema every turn. A dead
|
|
980
|
+
token ends the server at startup with exit `4`.
|
|
981
|
+
|
|
435
982
|
**`guide`** prints a page to paste into `AGENTS.md` / `CLAUDE.md`.
|
|
436
983
|
**`skills install`** symlinks the six bundled Claude skills into
|
|
437
984
|
`~/.claude/skills`; it refuses to run from an `npx` cache, whose symlinks dangle
|
|
@@ -457,8 +1004,10 @@ once npm reclaims it.
|
|
|
457
1004
|
|
|
458
1005
|
| Variable | Default | Meaning |
|
|
459
1006
|
| --- | --- | --- |
|
|
460
|
-
| `OXY_TOKEN` | — |
|
|
461
|
-
| `OXY_API_KEY` | — | key for the `/external/api` surface |
|
|
1007
|
+
| `OXY_TOKEN` | — | any credential, sent as a bearer; overrides the login cache and GitHub OIDC |
|
|
1008
|
+
| `OXY_API_KEY` | — | a legacy API key or an API token, for the `/external/api` surface and for `checks run` |
|
|
1009
|
+
| `OXY_SERVICE_ACCOUNT` | — | the **ID** of the service account a GitHub OIDC exchange acts as (`--service-account`); never `<org>/<name>` |
|
|
1010
|
+
| `OXY_AGENT` | detected | the name of the agent driving `oxyc`, sent in the user agent — see below |
|
|
462
1011
|
| `OXY_CREDENTIALS_PATH` | OS config dir | the shared `credentials.json` |
|
|
463
1012
|
| `OXYC_ORG` | `oxy-hq` | GitHub org the customer repos live in |
|
|
464
1013
|
| `OXYC_CUSTOMER_TOPIC` | `oxy-customer` | topic that registers a customer repo |
|
|
@@ -478,6 +1027,17 @@ once npm reclaims it.
|
|
|
478
1027
|
| `OXYC_DRY_RUN` | — | `1` makes `launch` print the command instead of running it |
|
|
479
1028
|
| `NO_COLOR` / `FORCE_COLOR` | — | force plain / coloured output |
|
|
480
1029
|
|
|
1030
|
+
**An agent should set `OXY_AGENT`.** Every request `oxyc` makes to a deployment
|
|
1031
|
+
carries the user agent `oxyc/<version>`, with `agent/<label>` after it when an
|
|
1032
|
+
agent is driving, and `mcp` last when the call came through `oxyc mcp` —
|
|
1033
|
+
`oxyc/0.6.0 agent/release-bot mcp`. The server records it on audit rows and in
|
|
1034
|
+
each token's usage, and it is the one thing there that tells an agent from the
|
|
1035
|
+
engineer running the same commands. The label is `OXY_AGENT`, held to
|
|
1036
|
+
lowercase letters, digits, `.`, `_` and `-` and to 32 characters (anything else
|
|
1037
|
+
becomes `-`). With `OXY_AGENT` unset, Claude Code is recognised by the
|
|
1038
|
+
`CLAUDECODE` variable it exports and labelled `claude-code`. It identifies
|
|
1039
|
+
honest callers in a log; it is not a credential and grants nothing.
|
|
1040
|
+
|
|
481
1041
|
## Output contract
|
|
482
1042
|
|
|
483
1043
|
**stdout carries the response body and nothing else.** Progress, warnings,
|