@oxy-hq/cli 0.6.0 → 0.8.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 +397 -42
- package/dist/main.mjs +5445 -2592
- package/dist/main.mjs.map +1 -1
- package/dist/{mcp-D5FFpqAw.mjs → mcp-xxWL3_dz.mjs} +246 -11
- package/dist/mcp-xxWL3_dz.mjs.map +1 -0
- package/package.json +1 -1
- package/skills/oxy-cli/SKILL.md +10 -1
- package/skills/oxy-custom-apps/SKILL.md +36 -6
- 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-D5FFpqAw.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
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`, `login-link`, `tokens`, `assume`, `oltp`
|
|
7
7
|
- **Customer workspaces** — `list`, `new`, `import`, `doctor`, `update`, `adopt`, `launch`
|
|
8
8
|
- **Custom apps** — `publish`, `init-ci`, `proxy`, `apps`
|
|
9
9
|
- **Checks** — `checks run`
|
|
@@ -50,8 +50,9 @@ and `cache`.
|
|
|
50
50
|
| --- | --- | --- |
|
|
51
51
|
| `--env <name\|url>` | `production` | deployment to target |
|
|
52
52
|
| `--target <url>` | — | explicit base URL; overrides `--env` |
|
|
53
|
-
| `--token-env <VAR>` | `OXY_TOKEN` | env var holding the bearer
|
|
54
|
-
| `--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 |
|
|
55
56
|
| `--org <slug>` | — | value for the `{org}` placeholder |
|
|
56
57
|
| `--workspace <id>` | — | value for the `{workspace}` placeholder |
|
|
57
58
|
| `--project <id>` | — | value for the `{project}` placeholder |
|
|
@@ -112,8 +113,12 @@ to `--env`, or from `--org` / `--workspace` / `--project`. An unresolvable
|
|
|
112
113
|
placeholder is an error naming the flag that fills it — never a literal sent to
|
|
113
114
|
the server.
|
|
114
115
|
|
|
115
|
-
**Surfaces.** The path selects the credential
|
|
116
|
-
|
|
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.
|
|
117
122
|
|
|
118
123
|
**`--paginate` is a heuristic.** Oxy sends `Link: rel="next"` on some endpoints
|
|
119
124
|
and nothing on others; where there is no header, `oxyc` reads `next_offset`
|
|
@@ -145,13 +150,131 @@ route's surface and fleet role.
|
|
|
145
150
|
|
|
146
151
|
```bash
|
|
147
152
|
oxyc login [--env <e>] [--login-env <e...>] [--assume <org> -r <why>]
|
|
148
|
-
oxyc whoami [--json]
|
|
153
|
+
oxyc whoami [--json] # who the credential is, and what it can reach
|
|
149
154
|
oxyc token # print the bearer, for a raw curl
|
|
150
|
-
oxyc
|
|
155
|
+
oxyc login-link [--next <path>] [--json] [--open] # a one-time URL that signs a browser in
|
|
156
|
+
oxyc logout # revoke the cached token, then forget it
|
|
157
|
+
oxyc tokens list [--json] # your personal access tokens
|
|
158
|
+
oxyc tokens revoke <id>
|
|
159
|
+
oxyc tokens revoke --current # end the token in OXY_TOKEN
|
|
160
|
+
oxyc tokens create # opens Account → Personal access tokens
|
|
161
|
+
oxyc tokens create --sandbox-agent --app <org>/<app> [--app …] [--hours 8] [--name …]
|
|
151
162
|
```
|
|
152
163
|
|
|
153
|
-
|
|
154
|
-
|
|
164
|
+
**Every command resolves its credential the same way, first match wins:**
|
|
165
|
+
|
|
166
|
+
1. **`OXY_TOKEN`** (or the variable `--token-env` names). It holds any
|
|
167
|
+
credential, and all are sent as a bearer: an API token — a personal access
|
|
168
|
+
token (`oxy_pat_…`), a service account token (`oxy_sat_…`), a CI token
|
|
169
|
+
(`oxy_ci_…`) or a sandbox agent token (`oxy_sbx_…`) — a legacy API key
|
|
170
|
+
(`oxy_…`), a publish token, or a session token. A blank value counts as
|
|
171
|
+
unset.
|
|
172
|
+
2. **The login cache** — what `oxyc login` stored for this deployment.
|
|
173
|
+
3. **GitHub OIDC**, in a GitHub Actions job granted `id-token: write`: the job's
|
|
174
|
+
OIDC token is exchanged, once per process, for a fifteen-minute token acting
|
|
175
|
+
as a service account, and that token is revoked when the command ends. See
|
|
176
|
+
[CI without a stored secret](#ci-without-a-stored-secret).
|
|
177
|
+
|
|
178
|
+
**Naming the variable stops the list at 1.** With `--token-env <VAR>`, and
|
|
179
|
+
always in `oxyc mcp`, an unset or empty variable is an auth error (exit `4`):
|
|
180
|
+
the login cache and the OIDC exchange are not tried. The flag says which
|
|
181
|
+
credential runs the command — an agent's scoped token, typically — so a typo in
|
|
182
|
+
the name must not quietly run it as whoever last ran `oxyc login` on the
|
|
183
|
+
machine. A person who wants `oxyc mcp` on their own login starts it with
|
|
184
|
+
`--login`. `OXY_API_KEY` is unaffected: a command that can use one on its own
|
|
185
|
+
(`checks run`, the sandbox verbs) still does.
|
|
186
|
+
|
|
187
|
+
`OXY_API_KEY` holds a legacy API key or an API token, and is honoured where it
|
|
188
|
+
always was — the `/external/api` surface, and `checks run`.
|
|
189
|
+
|
|
190
|
+
**Legacy API keys and API tokens are separate things.** A legacy API key is the
|
|
191
|
+
older `oxy_…` key: it reaches everything its owner can and can't be limited to
|
|
192
|
+
workspaces. It keeps working wherever it works today, but it is not a token —
|
|
193
|
+
`oxyc tokens` never lists or revokes one, and `oxyc whoami` calls it a
|
|
194
|
+
`Legacy API key`. They are extended and revoked in the web app, under
|
|
195
|
+
Settings → Workspace → Legacy API keys. Use an API token for anything new.
|
|
196
|
+
|
|
197
|
+
**`login`** runs a browser loopback flow. Against a current deployment it
|
|
198
|
+
receives a one-time code and exchanges it (PKCE) for a **personal access
|
|
199
|
+
token** named `oxyc on <hostname>`: revocable, ninety days, and replacing that
|
|
200
|
+
machine's previous one. No credential travels in a URL. Against a deployment
|
|
201
|
+
that predates the exchange it stores the session token the page hands back,
|
|
202
|
+
exactly as before, and says so — nothing needs upgrading first, in either
|
|
203
|
+
direction. `--login-env` is repeatable and comma-separated
|
|
204
|
+
(`--login-env dev,staging`); the browser opens once per environment, in
|
|
205
|
+
sequence.
|
|
206
|
+
|
|
207
|
+
**`logout`** revokes the cached token on the server, best-effort, then removes
|
|
208
|
+
it from this machine whatever the server said. It never touches `OXY_TOKEN`.
|
|
209
|
+
|
|
210
|
+
**`whoami`** makes a live call, so an expired or revoked credential fails here
|
|
211
|
+
rather than reading as fine from the cache. For a token it also prints what the
|
|
212
|
+
token is and what it can reach — `all access`, or one line per grant with the
|
|
213
|
+
role it is capped at, and any organization whose policy blocks it. For a legacy
|
|
214
|
+
API key it prints `Legacy API key`, and that it reaches everything its owner
|
|
215
|
+
can.
|
|
216
|
+
|
|
217
|
+
**`token`** prints the bearer on stdout and nothing else. Inside a GitHub
|
|
218
|
+
Actions job with nothing stored it prints the token the job's OIDC identity
|
|
219
|
+
exchanges for — like `gh auth token` — and that one is **not** revoked on exit,
|
|
220
|
+
since the output is the token. It still expires in fifteen minutes.
|
|
221
|
+
|
|
222
|
+
**`tokens`** manages personal access tokens, and the server allows that from a
|
|
223
|
+
**browser session only**: a token that could list, mint or revoke tokens could
|
|
224
|
+
outlive its own revocation. Since `login` now stores a token, `tokens list` and
|
|
225
|
+
`tokens revoke` usually answer with a pointer to the web app (Account →
|
|
226
|
+
Personal access tokens, `<deployment>/?settings=account.tokens`); they work
|
|
227
|
+
from a login that stored a session token. `tokens create` always opens that
|
|
228
|
+
page — a new token's secret is shown once, and a terminal is the wrong place.
|
|
229
|
+
|
|
230
|
+
**`tokens create --sandbox-agent`** is the exception, and the one command an
|
|
231
|
+
agent runs to get its own credential. It mints a **sandbox agent token**
|
|
232
|
+
(`oxy_sbx_…`): the [sandbox loop](#sandboxes) on one to five named apps, in
|
|
233
|
+
sandboxes the token created, for 8 hours by default (`--hours`, 1 to 168) —
|
|
234
|
+
and nothing else on the deployment.
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
eval "$(oxyc tokens create --sandbox-agent --app acme/store --env dev)"
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
It opens the same browser loopback as `login`, with the apps and the lifetime
|
|
241
|
+
on the page; the agent's operator approves once, under their own session.
|
|
242
|
+
Stdout is the single line `export OXY_TOKEN=oxy_sbx_…`, printed once, and
|
|
243
|
+
everything else is on stderr. It holds no credential while it runs, never
|
|
244
|
+
writes the credentials file, and revokes no other token. `--app` is
|
|
245
|
+
`<org>/<app>` and repeats; arguments are checked before the browser opens
|
|
246
|
+
(exit `2`). Only staff who can open the app's sandboxes can approve.
|
|
247
|
+
|
|
248
|
+
What comes back is checked before anything is printed: it must be an
|
|
249
|
+
`oxy_sbx_`, described as `sandbox_agent` and not all-access, for exactly the
|
|
250
|
+
apps named, expiring no later than the hours asked for. Anything the check
|
|
251
|
+
cannot read — an app entry with no slugs, an expiry that is missing or not a
|
|
252
|
+
time — is refused rather than skipped. A deployment that predates sandbox
|
|
253
|
+
agent tokens would otherwise hand back an ordinary login token with the
|
|
254
|
+
approver's whole reach. On any mismatch the command prints nothing, revokes
|
|
255
|
+
what was minted, and exits `8`.
|
|
256
|
+
|
|
257
|
+
With that token in `OXY_TOKEN`, `oxyc` behaves differently in five ways:
|
|
258
|
+
|
|
259
|
+
- **An app resolves against the token's own list** (`GET /api/auth/token`),
|
|
260
|
+
never `/api/admin/apps`. An app it was not minted for is exit `5`.
|
|
261
|
+
- **Read-back uses `/api/customer-apps/{id}/…`** — `invocations list`,
|
|
262
|
+
`invocations held` and `checks run` — since the token never reaches `/admin`.
|
|
263
|
+
- **Refused before any request, exit `2`:** an `--app-env` that is not
|
|
264
|
+
`dev-<handle>`; `publish` without `--app-env`, or with `--promote`;
|
|
265
|
+
`fn call`, `logs`, `invocations list` and `checks run` without `--app-env`;
|
|
266
|
+
`env show` of production or staging; `oxyc apps`, `tokens list` and
|
|
267
|
+
`tokens revoke <id>`; and `oxyc api` against any path.
|
|
268
|
+
- **`whoami`** prints the token's description — `kind`, `expires_at`, the
|
|
269
|
+
`apps` it reaches and its `minter` — and `--json` is that document as the
|
|
270
|
+
server sent it.
|
|
271
|
+
- **`tokens revoke --current`** ends it (`DELETE /api/auth/token`). Exit `0`
|
|
272
|
+
when it is revoked or was already dead. It works for any API token in the
|
|
273
|
+
variable; a cached login is ended with `logout` instead.
|
|
274
|
+
|
|
275
|
+
A refusal from the server carries a hint an agent can act on — never "try
|
|
276
|
+
`oxyc login` again". Exit `4` under this token means it expired, was revoked,
|
|
277
|
+
or its minter lost access: stop and report.
|
|
155
278
|
|
|
156
279
|
Credentials live in the OS config directory under **`oxy`** — the same file the
|
|
157
280
|
Rust `oxy login` wrote before it was removed, so an existing login still works:
|
|
@@ -159,8 +282,135 @@ Rust `oxy login` wrote before it was removed, so an existing login still works:
|
|
|
159
282
|
- macOS: `~/Library/Application Support/oxy/credentials.json`
|
|
160
283
|
- Linux: `$XDG_CONFIG_HOME/oxy/credentials.json`
|
|
161
284
|
|
|
162
|
-
`
|
|
163
|
-
|
|
285
|
+
An entry may now also carry `token_id` and `expires_at`; both are optional, and
|
|
286
|
+
a file written before they existed loads unchanged. `OXY_CREDENTIALS_PATH`
|
|
287
|
+
overrides the path. Caches are separate, under `oxyc` (`~/.cache/oxyc`).
|
|
288
|
+
|
|
289
|
+
### A signed-in browser for an agent
|
|
290
|
+
|
|
291
|
+
Sign-in is passwordless — Google or GitHub OAuth, or a magic link — and an
|
|
292
|
+
automation agent can finish neither. **`login-link`** prints a one-time URL
|
|
293
|
+
that signs a browser in with the credential `oxyc` is running on, so the whole
|
|
294
|
+
sign-in is one navigation:
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
browser_navigate("$(oxyc login-link --env dev --next /ide)") # Playwright MCP
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
// a Playwright script
|
|
302
|
+
const link = execFileSync("oxyc", ["login-link", "--env", "dev", "--next", "/ide"], { encoding: "utf8" });
|
|
303
|
+
await page.goto(link.trim());
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Stdout is the URL and nothing else; what the link is goes to stderr.
|
|
307
|
+
|
|
308
|
+
- **The link works once and expires in minutes** (five, at the time of
|
|
309
|
+
writing — the server's `expires_at`). The bearer is never in it: `oxyc`
|
|
310
|
+
trades the token for a single-use ticket (`POST /api/auth/browser-ticket`),
|
|
311
|
+
and the ticket and `--next` ride the URL's fragment, which is not sent to a
|
|
312
|
+
server and so reaches no access log.
|
|
313
|
+
- **The session is bounded by the token.** It reaches what the token reaches
|
|
314
|
+
and nothing more, lasts at most the `session_seconds` the server answers
|
|
315
|
+
(stderr states it in hours), ends sooner if the token is revoked or expires,
|
|
316
|
+
and cannot manage tokens.
|
|
317
|
+
- **It needs a personal access token** (`oxy_pat_…`) — what `oxyc login`
|
|
318
|
+
stores. Any other credential is exit `4` with the next step: a session token
|
|
319
|
+
from an older login, a service account, CI or publish token, a legacy API
|
|
320
|
+
key. A sandbox agent token is exit `2`, before any request. A deployment that
|
|
321
|
+
predates sign-in links is exit `5`.
|
|
322
|
+
- **`--next <path>`** is where the browser lands: a path on the deployment
|
|
323
|
+
starting with a single `/`. A URL, `//host` or a backslash is exit `2`, and
|
|
324
|
+
no ticket is spent on it.
|
|
325
|
+
- **`--json`** prints `{ "url", "expires_at", "session_seconds" }` instead.
|
|
326
|
+
**`--open`** also opens the link in your default browser — which uses it, so
|
|
327
|
+
the printed URL will not work a second time.
|
|
328
|
+
|
|
329
|
+
### CI without a stored secret
|
|
330
|
+
|
|
331
|
+
In a GitHub Actions job granted `id-token: write`, with no `OXY_TOKEN` set,
|
|
332
|
+
**any** `oxyc` command signs itself in — **as the service account the workflow
|
|
333
|
+
names, by its ID,** in `OXY_SERVICE_ACCOUNT` (or `--service-account`). It asks
|
|
334
|
+
GitHub for an OIDC token whose audience is `oxy:<the deployment's host>`, posts
|
|
335
|
+
it with the account's ID to `/api/auth/oidc/exchange`, and uses the
|
|
336
|
+
fifteen-minute `oxy_ci_…` token that comes back, revoking it on exit.
|
|
337
|
+
|
|
338
|
+
The audience is **worked out from the URL `oxyc` is pointed at** — the host of
|
|
339
|
+
the resolved `--env` / `--target`, lowercased, with a port only when it is not
|
|
340
|
+
the scheme's default: `oxy:app.oxygen-hq.com`, `oxy:aip.staging.oxy.tech`. It is
|
|
341
|
+
never asked of the deployment, and nothing a server says can change it. So the
|
|
342
|
+
token a job gives to a host is good at the deployment that calls itself by that
|
|
343
|
+
host and at no other: one handed to staging cannot be replayed at production,
|
|
344
|
+
and a server cannot talk the job into minting a token for somewhere else.
|
|
345
|
+
There is no shared fallback, and plain `oxy` is never requested.
|
|
346
|
+
|
|
347
|
+
A deployment that answers to a different address than the one `oxyc` was
|
|
348
|
+
pointed at — reached through an alias or a port-forward — refuses with
|
|
349
|
+
`wrong_audience` and names the address it does answer to: point `--target` at
|
|
350
|
+
that. One with no public URL configured (`OXY_API_URL`) takes no GitHub
|
|
351
|
+
sign-in at all; use `OXY_TOKEN` there.
|
|
352
|
+
|
|
353
|
+
What makes the deployment honour the job is a
|
|
354
|
+
**trust policy** on that service account, naming the repository, the workflow
|
|
355
|
+
file and the job's `environment:` — registered by `oxyc init-ci`, or in the web
|
|
356
|
+
app under Organization settings → API access → Service accounts → *the
|
|
357
|
+
account* → Trusted access.
|
|
358
|
+
|
|
359
|
+
**The account is always named, never guessed.** Anyone can register a trust
|
|
360
|
+
policy that names a repository they do not own, so the deployment matches a
|
|
361
|
+
run against the policies of the one account its workflow asked for and no
|
|
362
|
+
other. With no account named, `oxyc` attempts no exchange at all: `publish` and
|
|
363
|
+
`checks run` go straight to the app's registered publisher (below), and every
|
|
364
|
+
other command fails with "not authenticated", saying `OXY_SERVICE_ACCOUNT` is
|
|
365
|
+
what is missing.
|
|
366
|
+
|
|
367
|
+
**By ID, never by `<org>/<name>`.** An org's slug can be changed, and once the
|
|
368
|
+
org renames or is deleted the slug is free for anyone — who could create the
|
|
369
|
+
same account name under it and register a policy on your repository's public
|
|
370
|
+
ids. A workflow that said `acme/deployer` would then be naming *their*
|
|
371
|
+
account. An ID never changes hands, so it is the only thing the deployment
|
|
372
|
+
takes: anything else answers `400 service_account_required`, and `oxyc` does
|
|
373
|
+
not fall back from that. The ID is shown in the web app (Organization settings
|
|
374
|
+
→ API access → Service accounts → *the account*), and `oxyc init-ci` writes it
|
|
375
|
+
for you. `oxyc` passes the value through untouched; the answer still says
|
|
376
|
+
`<org>/<name>`, so the log reads as it did.
|
|
377
|
+
|
|
378
|
+
```yaml
|
|
379
|
+
permissions: { id-token: write, contents: read }
|
|
380
|
+
jobs:
|
|
381
|
+
deploy:
|
|
382
|
+
environment: production # the trust policy requires it
|
|
383
|
+
runs-on: ubuntu-latest
|
|
384
|
+
steps:
|
|
385
|
+
- uses: actions/checkout@v4
|
|
386
|
+
- uses: actions/setup-node@v4
|
|
387
|
+
with: { node-version: 20 }
|
|
388
|
+
# or any oxyc command the grants allow
|
|
389
|
+
- run: npx --yes @oxy-hq/cli publish --promote
|
|
390
|
+
env:
|
|
391
|
+
OXY_SERVICE_ACCOUNT: 3f2504e0-4f89-41d3-9a0c-0305e82c3301 # acme/deployer
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Each command exchanges for itself, which is all a job running one command
|
|
395
|
+
needs. Several policies of the named account matching one run is not an error:
|
|
396
|
+
the oldest is used. The exchange is rate-limited per client address (60 a
|
|
397
|
+
minute); a `429` is waited out once, for its `Retry-After` (at most 60 s), and
|
|
398
|
+
retried before it fails.
|
|
399
|
+
|
|
400
|
+
The `setup-oxyc` action (`sdk/setup-oxyc`) does the exchange once for a whole
|
|
401
|
+
job: it installs `oxyc`, exports `OXY_TOKEN` for the later steps and revokes
|
|
402
|
+
the token in a post step. **It is not published yet** — `uses:
|
|
403
|
+
oxy-hq/setup-oxyc@v1` does not resolve, and a job naming it fails at "Set up
|
|
404
|
+
job" — so nothing here depends on it.
|
|
405
|
+
|
|
406
|
+
A refused exchange says what to fix: `no_matching_policy` prints the
|
|
407
|
+
repository, workflow, ref and event the run presented and where to register a
|
|
408
|
+
policy on the named account; `missing_environment`, `self_hosted_runner` and
|
|
409
|
+
`pull_request_target` name the rule that was not met. A deployment with no
|
|
410
|
+
exchange route (404) reads as "not authenticated", with that reason — except
|
|
411
|
+
in `publish` and `checks run`, which fall back to the app's registered
|
|
412
|
+
publisher, as they also do on `no_matching_policy` and when no account is
|
|
413
|
+
named.
|
|
164
414
|
|
|
165
415
|
## Acting as an organization
|
|
166
416
|
|
|
@@ -243,7 +493,7 @@ the current directory while granting access to the customer's repo.
|
|
|
243
493
|
|
|
244
494
|
```bash
|
|
245
495
|
oxyc publish [--env <e>] [--dir <path>] [--promote] [--build-only | --prebuilt] [--json] [--allow-function-lint] [--app-env <dev-handle>]
|
|
246
|
-
oxyc init-ci [--app <org>/<app>] [--environment <name>] [--force]
|
|
496
|
+
oxyc init-ci [--app <org>/<app>] [--environment <name>] [--promote] [--force] [--no-register] [--setup-action]
|
|
247
497
|
oxyc proxy [--port <n>] [--allow-writes] [--allow-events] [--yes]
|
|
248
498
|
```
|
|
249
499
|
|
|
@@ -262,11 +512,16 @@ channel unless `--promote`. `--env` defaults to **production**; name it.
|
|
|
262
512
|
**`--prebuilt`** (with `--dir`) skips esbuild and refuses if a declared
|
|
263
513
|
function's `functions/<name>.js` is missing. The pair splits CI so the job that
|
|
264
514
|
runs package scripts never holds the credential.
|
|
265
|
-
- **Auth**:
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
515
|
+
- **Auth**: `OXY_TOKEN`, then the login cache — or, in a GitHub Actions job
|
|
516
|
+
with `id-token: write` and neither set, the job's OIDC token. (A variable
|
|
517
|
+
named with `--token-env` is the only source: unset, the publish stops.) Two exchanges are tried, in order, and the exchange happens last, after
|
|
518
|
+
the build. First the general one (`/api/auth/oidc/exchange`, audience `oxy:<the deployment's host>`):
|
|
519
|
+
a **trust policy** on a service account (see `init-ci`). Then, only when the
|
|
520
|
+
deployment has no such route (404) or answers `no_matching_policy`, the app's
|
|
521
|
+
own **registered publisher** (`/api/customer-apps/publish/oidc-exchange`,
|
|
522
|
+
audience `oxy-publish`) — the registration every workflow written before trust
|
|
523
|
+
policies relies on; it needs the org **slug**. Any other refusal is final: it
|
|
524
|
+
names something wrong with the run, which the older path would only hide.
|
|
270
525
|
- **Provenance**: the checkout's `origin`, `HEAD` and branch (else `GITHUB_SHA` /
|
|
271
526
|
`GITHUB_REF_NAME`) are recorded; a missing half or a dirty app directory is a
|
|
272
527
|
warning, never a failure. The build id defaults to
|
|
@@ -287,12 +542,49 @@ channel unless `--promote`. `--env` defaults to **production**; name it.
|
|
|
287
542
|
skipped. **`--allow-function-lint`** is the way past a false positive: every
|
|
288
543
|
finding becomes a warning that names its rule — please open an issue with it.
|
|
289
544
|
|
|
290
|
-
**`init-ci`** writes
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
545
|
+
**`init-ci`** does two things: writes the workflow, and registers the trust that
|
|
546
|
+
lets it publish.
|
|
547
|
+
|
|
548
|
+
*The workflow* is `.github/workflows/oxy-publish.yml` at the repo root: a
|
|
549
|
+
`build` job (no id-token) that runs `publish --build-only`, and a `publish` job
|
|
550
|
+
bound to a GitHub `environment:` (`--environment`, default `oxy-publish`) whose
|
|
551
|
+
only work is `publish --prebuilt`. That job uses no third-party action: each
|
|
552
|
+
step runs `npx @oxy-hq/cli@<this version>` and `oxyc` exchanges the job's OIDC
|
|
553
|
+
token for itself, acting as the service account `--service-account <org>/<name>`
|
|
554
|
+
names — by default `<app's org>/deployer`. **Here, and only here, the account is
|
|
555
|
+
given by name:** `init-ci` is run by a person, looks the account's ID up as that
|
|
556
|
+
person, and writes the ID into the workflow with the name beside it as a
|
|
557
|
+
comment (`OXY_SERVICE_ACCOUNT: 3f25…3301 # acme/deployer`). When it cannot
|
|
558
|
+
look the ID up — nobody logged in, no such account yet — it writes a
|
|
559
|
+
`<service-account-id>` placeholder and says so; it never writes the name as
|
|
560
|
+
the value. `--promote` makes the publish go live. **No `checks run` step is
|
|
561
|
+
written**, even when the manifest declares a check: the job's service-account
|
|
562
|
+
token is answered `403` on the check routes, which sit behind platform gates a
|
|
563
|
+
service account never passes, so the step could not succeed. The workflow
|
|
564
|
+
carries a one-line comment where it would be, and `init-ci` says so when it
|
|
565
|
+
writes one. Run the checks with a staff credential until `ci` tokens may.
|
|
566
|
+
`--setup-action` writes the same workflow around `uses: oxy-hq/setup-oxyc@v1`
|
|
567
|
+
instead, which exchanges once for the whole job. That action is not published
|
|
568
|
+
yet, so a workflow written with the flag cannot start until it is; `init-ci`
|
|
569
|
+
says so when it writes one.
|
|
570
|
+
|
|
571
|
+
*The registration* is a trust policy on that service account — this repository,
|
|
572
|
+
the workflow file, the environment — granted publishing this one app and
|
|
573
|
+
nothing else. `init-ci` creates the `deployer` account if none was named and it
|
|
574
|
+
does not exist, then the policy, and is idempotent: a second run finds both. A
|
|
575
|
+
named account is never created, and must be one of the app's own org — an
|
|
576
|
+
account's grants never reach another org, so `init-ci` refuses one. A service
|
|
577
|
+
account's name is lowercase words joined by single hyphens, starting with a
|
|
578
|
+
letter, 2–40 characters.
|
|
579
|
+
|
|
580
|
+
It can only register **when the caller can**: an org admin, logged in with a
|
|
581
|
+
browser-session credential — the server refuses those two creations to a token,
|
|
582
|
+
which is what a fresh `oxyc login` stores. When it cannot (not an admin, a token
|
|
583
|
+
login, offline, a deployment without trusted access, `--no-register`), the
|
|
584
|
+
workflow is still written and the command prints the exact steps instead: where
|
|
585
|
+
in the web app, the `oxyc api` calls with every id it managed to learn filled
|
|
586
|
+
in, and the older `…/publishers` registration for a deployment without trust
|
|
587
|
+
policies. Registration never fails the command.
|
|
296
588
|
|
|
297
589
|
**`--app-env <dev-handle>`** moves one sandbox's build pointer instead of the
|
|
298
590
|
draft/live channel — see [Sandboxes](#sandboxes). It needs a staff credential
|
|
@@ -420,13 +712,21 @@ difference: `checks run` talks to `/api/admin/**`, which is not the
|
|
|
420
712
|
`oxyc login` (or `--token-env`) always wins when one is present.
|
|
421
713
|
|
|
422
714
|
**In CI, nothing is stored.** With neither a token nor an API key set, a job
|
|
423
|
-
holding `id-token: write`
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
715
|
+
holding `id-token: write` signs itself in from its GitHub OIDC token — the same
|
|
716
|
+
two exchanges `oxyc publish` tries, in the same order: a service account's
|
|
717
|
+
trust policy first, the app's registered publisher second — and drives
|
|
718
|
+
`/api/customer-apps/**` instead: the same three handlers, mounted where a
|
|
719
|
+
machine credential may reach them. A service account's token (`oxy_sat_…`,
|
|
720
|
+
`oxy_ci_…`) or a publish token set in `OXY_TOKEN` takes that surface too,
|
|
721
|
+
without minting; a personal token, a legacy API key or a session takes the admin
|
|
722
|
+
one. `OXY_API_KEY` is read **before** OIDC here: a job that set one has said
|
|
723
|
+
which credential it means.
|
|
724
|
+
|
|
725
|
+
The app id comes from the credential where it can. The publisher exchange
|
|
726
|
+
returns it, so `<app>` must be `<org-slug>/<app-slug>` on that path (a UUID
|
|
727
|
+
names an app it cannot verify a publisher for). A service account's token names
|
|
728
|
+
the apps it may publish, so a slug resolves against its own grants — and when
|
|
729
|
+
it cannot, the publisher exchange is tried before asking for a UUID.
|
|
430
730
|
|
|
431
731
|
What that credential may do is deliberately small, and enforced server-side
|
|
432
732
|
rather than by the client. An app-scoped token is **confined to its app**: it
|
|
@@ -444,24 +744,27 @@ it admitted any function.
|
|
|
444
744
|
**A check runs against the app's live build** — the server resolves
|
|
445
745
|
`published_build_id`, else `draft_build_id`, for the pre-flight lookup and for
|
|
446
746
|
the execution alike. So `checks run` after a plain `oxyc publish` verifies the
|
|
447
|
-
*previously promoted* code, not the draft just uploaded. `oxyc init-ci` writes
|
|
448
|
-
|
|
449
|
-
|
|
747
|
+
*previously promoted* code, not the draft just uploaded. (`oxyc init-ci` writes
|
|
748
|
+
no checks step at all today: a service account's `oxy_ci_` token is answered
|
|
749
|
+
`403` on these routes — the token's scope admits them for its own app, and the
|
|
750
|
+
routes' platform gates then refuse a caller with no platform standing.) **With
|
|
751
|
+
`--app-env`**
|
|
450
752
|
it runs against that environment's own build instead — a sandbox's own
|
|
451
753
|
publish, or staging's — and the report gains `environment` and each check's
|
|
452
754
|
`invocationId`; the publish-token and OIDC paths are unchanged and work only
|
|
453
755
|
against production, since every non-production operation needs a staff
|
|
454
756
|
credential (see [Sandboxes](#sandboxes)).
|
|
455
757
|
|
|
456
|
-
With a publish token, `<app>` must be the **UUID** — resolving
|
|
457
|
-
listing every app, which such a token may not do.
|
|
458
|
-
|
|
459
|
-
exchange
|
|
758
|
+
With a publish token (`oxypublish_…`), `<app>` must be the **UUID** — resolving
|
|
759
|
+
a slug means listing every app, which such a token may not do. A service
|
|
760
|
+
account's token takes either: the UUID, or a slug it can match to one of its
|
|
761
|
+
own app-publish grants. The publisher exchange needs no id at all (it returns
|
|
762
|
+
it) but does need `<org-slug>/<app-slug>`, since it is registered by slug.
|
|
460
763
|
|
|
461
764
|
**Exit codes:** `0` every check passed; `9` (`CHECK_FAILED`) at least one check
|
|
462
765
|
failed or timed out; `1` the app declares no checks; `5` the app was not
|
|
463
766
|
found; `4` no credential resolved, or the API rejected it (401/403 — an
|
|
464
|
-
expired
|
|
767
|
+
expired legacy API key or API token reads this way, not as a missing one); `2` a bad
|
|
465
768
|
`--timeout`, or a slug where the credential needs a UUID; `7` (`UNAVAILABLE`)
|
|
466
769
|
the deployment answered the OIDC exchange without an `app_id`, meaning it
|
|
467
770
|
predates trusted checks.
|
|
@@ -481,6 +784,10 @@ oxyc checks run <app> --app-env <name> # see "Checks" above
|
|
|
481
784
|
oxyc invocations list <app> [--app-env] [--build] [--function] [--limit]
|
|
482
785
|
oxyc invocations held <app> <invocation-id>
|
|
483
786
|
oxyc logs <app> [--app-env] [--invocation] [--request] [--hours] [--limit]
|
|
787
|
+
|
|
788
|
+
oxyc env secret list <app> --app-env <name> # keys and flags, never a value
|
|
789
|
+
oxyc env secret set <app> <key> --app-env <name> (--value <text> | --value-env <VAR>)
|
|
790
|
+
oxyc env secret delete <app> <key> --app-env <name>
|
|
484
791
|
```
|
|
485
792
|
|
|
486
793
|
A **sandbox** is a named, short-lived `dev-<handle>` environment of one custom
|
|
@@ -503,7 +810,8 @@ staging's tables (rows up to a size cap), then the build's OLTP migrations.
|
|
|
503
810
|
The loop: `env create` → `publish --app-env` → `fn call` / `checks run
|
|
504
811
|
--app-env` → `invocations list` / `held` to read back what ran → iterate from
|
|
505
812
|
`publish` → `env delete --yes --wait` when done. `oxyc guide` has the same six
|
|
506
|
-
lines
|
|
813
|
+
lines — between the two an agent adds to mint and revoke its own token — meant
|
|
814
|
+
to sit in an agent's context.
|
|
507
815
|
|
|
508
816
|
**`--app-env <environment>`** (`production`, `staging` or `dev-<handle>`,
|
|
509
817
|
validated client-side — exit `2` on a malformed name) is a *different axis*
|
|
@@ -515,6 +823,21 @@ take it. A publish-token credential is refused before any request for
|
|
|
515
823
|
`logs` refuse one outright, for any environment, because sandbox management
|
|
516
824
|
is a staff console surface.
|
|
517
825
|
|
|
826
|
+
**An unattended agent drives this loop with a sandbox agent token**
|
|
827
|
+
(`oxy_sbx_…`), not a staff credential: `oxyc tokens create --sandbox-agent
|
|
828
|
+
--app <org>/<app>` mints one after a browser approval — see
|
|
829
|
+
[Authentication](#authentication) for what `oxyc` then refuses, and
|
|
830
|
+
`internal-docs/custom-app-sandboxes.md` §2 for the agent's procedure. With
|
|
831
|
+
it, `--app-env dev-<handle>` is required on every verb above that takes one.
|
|
832
|
+
|
|
833
|
+
**`env secret`** reads and writes one sandbox's own secrets, for any
|
|
834
|
+
credential that reaches the sandbox, and only a `dev-<handle>` environment —
|
|
835
|
+
never staging's or production's. `list` prints each key with `is_set`,
|
|
836
|
+
`required` and `inherits_staging`; no verb returns a value. `set` takes the
|
|
837
|
+
value from `--value`, or from an environment variable with `--value-env` so it
|
|
838
|
+
stays off the command line. A sandbox with no value of its own reads
|
|
839
|
+
staging's.
|
|
840
|
+
|
|
518
841
|
**`env delete`** without `--yes` asks on a terminal and exits `8` (refused)
|
|
519
842
|
off one, same as `oltp reset`. `--wait [seconds]` (default 120) polls until
|
|
520
843
|
the teardown finishes and prints `{"name","status":"deleted"}`; past the
|
|
@@ -674,10 +997,29 @@ no terminal to ask on inside an MCP server), and `oxy_publish_sandbox`
|
|
|
674
997
|
requires a `dev-<handle>` `appEnv` and refuses production, staging or a
|
|
675
998
|
promote client-side — it can never reach the live channel.
|
|
676
999
|
|
|
1000
|
+
**Its credential is the token variable and nothing else.** An agent runtime
|
|
1001
|
+
starts `oxyc mcp` with the agent's own token in `OXY_TOKEN` (or the
|
|
1002
|
+
`--token-env` variable); with the variable unset the server exits `4` before
|
|
1003
|
+
serving a tool, rather than running on the `oxyc login` of whoever owns the
|
|
1004
|
+
machine. To serve on your own login, start it with `--login`.
|
|
1005
|
+
|
|
677
1006
|
```bash
|
|
678
|
-
|
|
1007
|
+
# an agent, on its own token
|
|
1008
|
+
claude mcp add oxyc -e OXY_TOKEN=oxy_sbx_… -- npx -y @oxy-hq/cli mcp --env dev
|
|
1009
|
+
# you, on your login
|
|
1010
|
+
claude mcp add oxyc -- npx -y @oxy-hq/cli mcp --login --env production
|
|
679
1011
|
```
|
|
680
1012
|
|
|
1013
|
+
**A sandbox agent token (`oxy_sbx_…`) is served a different, smaller list**:
|
|
1014
|
+
the ten sandbox-loop tools, `oxy_whoami` (the token's `kind`, `expires_at`,
|
|
1015
|
+
`apps` and `minter`), and four that exist only for it — `oxy_env_secret_list`,
|
|
1016
|
+
`oxy_env_secret_set` and `oxy_env_secret_delete`, which accept only a
|
|
1017
|
+
`dev-<handle>` `appEnv`, and `oxy_token_revoke`, which needs `confirm: true`.
|
|
1018
|
+
`oxy_request`, `oxy_routes`, `oxy_schema` and the preview tools are dropped,
|
|
1019
|
+
and refused if called by name: the server answers that token `404` on all of
|
|
1020
|
+
them, and a tool that can only fail still costs its schema every turn. A dead
|
|
1021
|
+
token ends the server at startup with exit `4`.
|
|
1022
|
+
|
|
681
1023
|
**`guide`** prints a page to paste into `AGENTS.md` / `CLAUDE.md`.
|
|
682
1024
|
**`skills install`** symlinks the six bundled Claude skills into
|
|
683
1025
|
`~/.claude/skills`; it refuses to run from an `npx` cache, whose symlinks dangle
|
|
@@ -703,8 +1045,10 @@ once npm reclaims it.
|
|
|
703
1045
|
|
|
704
1046
|
| Variable | Default | Meaning |
|
|
705
1047
|
| --- | --- | --- |
|
|
706
|
-
| `OXY_TOKEN` | — |
|
|
707
|
-
| `OXY_API_KEY` | — | key for the `/external/api` surface |
|
|
1048
|
+
| `OXY_TOKEN` | — | any credential, sent as a bearer; overrides the login cache and GitHub OIDC |
|
|
1049
|
+
| `OXY_API_KEY` | — | a legacy API key or an API token, for the `/external/api` surface and for `checks run` |
|
|
1050
|
+
| `OXY_SERVICE_ACCOUNT` | — | the **ID** of the service account a GitHub OIDC exchange acts as (`--service-account`); never `<org>/<name>` |
|
|
1051
|
+
| `OXY_AGENT` | detected | the name of the agent driving `oxyc`, sent in the user agent — see below |
|
|
708
1052
|
| `OXY_CREDENTIALS_PATH` | OS config dir | the shared `credentials.json` |
|
|
709
1053
|
| `OXYC_ORG` | `oxy-hq` | GitHub org the customer repos live in |
|
|
710
1054
|
| `OXYC_CUSTOMER_TOPIC` | `oxy-customer` | topic that registers a customer repo |
|
|
@@ -724,6 +1068,17 @@ once npm reclaims it.
|
|
|
724
1068
|
| `OXYC_DRY_RUN` | — | `1` makes `launch` print the command instead of running it |
|
|
725
1069
|
| `NO_COLOR` / `FORCE_COLOR` | — | force plain / coloured output |
|
|
726
1070
|
|
|
1071
|
+
**An agent should set `OXY_AGENT`.** Every request `oxyc` makes to a deployment
|
|
1072
|
+
carries the user agent `oxyc/<version>`, with `agent/<label>` after it when an
|
|
1073
|
+
agent is driving, and `mcp` last when the call came through `oxyc mcp` —
|
|
1074
|
+
`oxyc/0.6.0 agent/release-bot mcp`. The server records it on audit rows and in
|
|
1075
|
+
each token's usage, and it is the one thing there that tells an agent from the
|
|
1076
|
+
engineer running the same commands. The label is `OXY_AGENT`, held to
|
|
1077
|
+
lowercase letters, digits, `.`, `_` and `-` and to 32 characters (anything else
|
|
1078
|
+
becomes `-`). With `OXY_AGENT` unset, Claude Code is recognised by the
|
|
1079
|
+
`CLAUDECODE` variable it exports and labelled `claude-code`. It identifies
|
|
1080
|
+
honest callers in a log; it is not a credential and grants nothing.
|
|
1081
|
+
|
|
727
1082
|
## Output contract
|
|
728
1083
|
|
|
729
1084
|
**stdout carries the response body and nothing else.** Progress, warnings,
|