Skip to content

Commit 67714e8

Browse files
committed
Add CI OIDC client surface
1 parent c8ae461 commit 67714e8

39 files changed

Lines changed: 4030 additions & 89 deletions

AGENTS.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ When adding a new tool/command, add it to the `SURFACE` array **and** `SDK_BY_CA
6767
## Architecture
6868

6969
```
70-
@run402/sdk (typed TypeScript kernel — 19 namespaces, ~100 methods)
70+
@run402/sdk (typed TypeScript kernel — 20 namespaces, ~100 methods)
7171
7272
│ /index.ts (isomorphic: Node + sandbox)
7373
│ /node (Node-only: keystore + allowance + x402-wrapped fetch + fileSetFromDir)
@@ -92,7 +92,7 @@ The SDK is the canonical kernel — a single typed client with a `CredentialsPro
9292
- **`kernel.ts`** — Request function, `Client` interface. Only place that calls `globalThis.fetch`.
9393
- **`errors.ts`**`Run402Error` hierarchy: `PaymentRequired`, `ProjectNotFound`, `Unauthorized`, `ApiError`, `NetworkError`, `Run402DeployError` (the v1.34+ structured envelope from the deploy state machine). Never calls `process.exit`.
9494
- **`credentials.ts`**`CredentialsProvider` interface. Required: `getAuth`, `getProject`. Optional: `saveProject`, `updateProject`, `removeProject`, `setActiveProject`, `getActiveProject`, `readAllowance`, `saveAllowance`, `createAllowance`, `getAllowancePath`.
95-
- **`namespaces/*.ts`** — One class per resource group (projects, blobs, functions, email, …). Namespaces hold a `Client` and expose typed methods. The canonical deploy primitive lives at **`namespaces/deploy.ts`** (with shared types in `deploy.types.ts`) — see "Unified Deploy" below.
95+
- **`namespaces/*.ts`** — One class per resource group (projects, blobs, functions, email, CI/OIDC, …). Namespaces hold a `Client` and expose typed methods. The canonical deploy primitive lives at **`namespaces/deploy.ts`** (with shared types in `deploy.types.ts`) — see "Unified Deploy" below.
9696
- **`node/*.ts`** — Node-only entry point (`@run402/sdk/node`). Wraps `core/` keystore + allowance into `NodeCredentialsProvider`. Sets up x402-wrapped fetch via `createLazyPaidFetch()`. Adds `fileSetFromDir(path)` for filesystem byte sources to the deploy primitive.
9797
- **`scoped.ts`**`ScopedRun402` sub-client. Returned by `r.project(id?)` and `r.useProject(id)`. Wraps every project-id-bearing namespace method with the id pre-bound, so `p.deploy.apply({ site })` (no `project`), `p.functions.list()`, `p.blobs.put(key, src)` all "just work" once the scope is set. Caller-supplied `project_id` / `project` still wins (override-friendly). The unwrapped namespaces (`r.deploy`, `r.functions`, …) keep their required-id signatures unchanged — scoped is sugar, not a replacement.
9898

@@ -116,6 +116,14 @@ The SDK is the canonical kernel — a single typed client with a `CredentialsPro
116116
- **Backward-compat shims.** `apps.bundleDeploy` translates legacy options into a `ReleaseSpec` and delegates to `deploy.apply` (the `inherit: true` flag is silently ignored — deprecation is preserved in the JSDoc only, the runtime warning was removed in #162 because it misled callers when an unrelated error followed). `sites.deployDir` is a thin wrapper that uses `fileSetFromDir(dir)` and synthesizes both unified `DeployEvent` shapes and the legacy `{ phase: ... }` shapes for v1.32-era event consumers.
117117
- **MCP/CLI surface.** `deploy` and `deploy_resume` MCP tools (in `src/tools/deploy.ts` and `src/tools/deploy-resume.ts`) expose the new primitive directly. CLI subcommands `run402 deploy apply` and `run402 deploy resume` (in `cli/lib/deploy-v2.mjs`) mirror them. The legacy `bundle_deploy`/`deploy_site`/`deploy_site_dir` MCP tools and `run402 deploy --manifest` CLI continue to work and route through the same SDK shim.
118118

119+
### CI/OIDC Federation (GitHub Actions)
120+
121+
- **`namespaces/ci.ts`**`/ci/v1/*` SDK surface: `createBinding`, `listBindings`, `getBinding`, `revokeBinding`, `exchangeToken`, plus canonical delegation builders (`buildCiDelegationStatement`, `buildCiDelegationResourceUri`) and validators.
122+
- **`ci-credentials.ts`** — isomorphic CI-session credential providers. `githubActionsCredentials({ projectId })` requests the GitHub OIDC subject token, exchanges it through `ci.exchangeToken`, caches the Run402 session until `expires_in - refreshBeforeSeconds`, and marks credentials with `CI_SESSION_CREDENTIALS`.
123+
- **`node/ci.ts`** — Node-only `signCiDelegation(values, opts?)`; reads the local allowance and signs the canonical SIWX delegation for `/ci/v1/bindings`. Default delegation chain id is `eip155:84532` unless overridden.
124+
- **Deploy integration is credential-driven.** `Deploy` detects the CI credential marker internally. Do not add public `ci` options, `r.ci.deployApply`, or broad MCP wrappers without a new design. CI deploys allow only `project`, `database`, `functions`, `site`, and absent/current `base`; secrets, subdomains, routes, checks, unknown top-level fields, non-current base, and `manifest_ref` are rejected before upload/plan.
125+
- **CLI DX.** `run402 ci link github` creates a deploy-scoped binding and generated workflow that calls `run402 deploy apply --manifest <manifest> --project <project>`. `run402 ci list` and `run402 ci revoke` manage bindings. V1 intentionally omits raw subject/wildcard/event/PR-deploy flags and requires GitHub repository-id binding.
126+
119127
### Shared Core (`core/src/`)
120128

121129
The `core/` module contains shared logic imported by all interfaces:
@@ -154,6 +162,7 @@ Core functions return `null` or throw — they never call `process.exit()`. Each
154162
- **`cli/lib/blob.mjs`** retains raw `fetch` for the `put` subcommand only — resumable uploads + per-part concurrency are CLI-specific UX not modeled in the SDK.
155163
- **`cli/lib/deploy.mjs`** delegates to `getSdk().apps.bundleDeploy(...)` (the v2 shim). The legacy custom undici dispatcher and retry-on-5xx logic was retired with the v1 route removal — v2 doesn't ship inline bytes, so the long-timeout rationale no longer applies.
156164
- **`cli/lib/deploy-v2.mjs`**`run402 deploy apply` and `run402 deploy resume` subcommands. Thin wrapper over `r.deploy.apply` / `r.deploy.resume`.
165+
- **`cli/lib/ci.mjs`**`run402 ci link github`, `run402 ci list`, and `run402 ci revoke`. Link signs the canonical delegation locally, verifies/inserts the GitHub repository id, and writes a workflow using GitHub OIDC (`permissions: id-token: write`) plus the existing `deploy apply` command.
157166

158167
### OpenClaw (`openclaw/`)
159168

README.md

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ This monorepo ships every surface an agent can pick up:
2020
|---------|-----------|
2121
| [`@run402/sdk`](./sdk/) | Calling Run402 from TypeScript — typed kernel, isomorphic (Node 22 / Deno / Bun / V8 isolates) with a Node entry that auto-loads the local keystore + allowance + x402 fetch |
2222
| [`run402` CLI](./cli/) | Terminal, scripts, CI, agent-controlled shells — JSON in, JSON out, exit code on failure |
23-
| [`run402-mcp`](./src/) | Claude Desktop, Cursor, Cline, Claude Code — every CLI capability as an MCP tool |
23+
| [`run402-mcp`](./src/) | Claude Desktop, Cursor, Cline, Claude Code — core Run402 operations as MCP tools |
2424
| [OpenClaw skill](./openclaw/) | OpenClaw agents (no MCP server required) |
2525
| [`@run402/functions`](./functions/) | Imported _inside_ deployed functions (`db(req)`, `adminDb()`, `getUser()`, `email`, `ai`) and for TypeScript autocomplete in your editor |
2626

27-
All four shipped surfaces release in lockstep at the same version and share a single typed kernel: `@run402/sdk`. MCP tools, CLI subcommands, and OpenClaw scripts are thin shims over SDK calls. Pick whichever interface fits your runtime.
27+
All five interfaces release in lockstep at the same version and share a single typed kernel where appropriate: `@run402/sdk`. MCP tools, CLI subcommands, and OpenClaw scripts are thin shims over SDK calls; `@run402/functions` is the in-function helper that runs inside deployed code. Pick whichever interface fits your runtime.
2828

2929
## 30-second start
3030

@@ -127,6 +127,32 @@ CLI:
127127
run402 sites deploy-dir ./dist --project prj_… > result.json 2> events.log
128128
```
129129

130+
### GitHub Actions OIDC deploys — link once, deploy with the same CLI
131+
132+
For repo-driven deploys, Run402 does not need service keys or allowance files in GitHub secrets. Run a local link command once:
133+
134+
```bash
135+
run402 ci link github --project prj_... --manifest run402.deploy.json
136+
```
137+
138+
That creates a deploy-scoped `/ci/v1/*` binding and writes a workflow that grants `id-token: write`, checks out the repo, and runs the existing deploy primitive:
139+
140+
```yaml
141+
permissions:
142+
contents: read
143+
id-token: write
144+
145+
jobs:
146+
deploy:
147+
runs-on: ubuntu-latest
148+
steps:
149+
- uses: actions/checkout@v4
150+
- name: Deploy to run402
151+
run: npx --yes run402@1.54.4 deploy apply --manifest 'run402.deploy.json' --project 'prj_...'
152+
```
153+
154+
CI deploys are intentionally narrow: `site`, `functions`, `database`, and absent/current `base` only. Keep secrets, domains, subdomains, routes, checks, and broader trust changes in a local allowance-backed deploy. Manage bindings with `run402 ci list` and `run402 ci revoke`.
155+
130156
### In-function helpers — caller-context vs BYPASSRLS
131157

132158
Inside a deployed function, import from `@run402/functions`. Two distinct DB clients keep RLS clean:
@@ -201,7 +227,7 @@ const project = await r.projects.provision({ tier: "prototype" });
201227
await r.blobs.put(project.project_id, "hello.txt", { content: "hi" });
202228
```
203229

204-
19 namespaces: `projects`, `deploy`, `sites`, `blobs`, `functions`, `secrets`, `subdomains`, `domains`, `email` (+ `webhooks`), `senderDomain`, `auth`, `apps`, `tier`, `billing`, `contracts`, `ai`, `allowance`, `service`, `admin`. Every operation throws a typed `Run402Error` subclass on failure: `PaymentRequired`, `ProjectNotFound`, `Unauthorized`, `ApiError`, `NetworkError`, `LocalError`, `Run402DeployError`. See [`sdk/README.md`](./sdk/README.md).
230+
20 namespaces: `projects`, `deploy`, `ci`, `sites`, `blobs`, `functions`, `secrets`, `subdomains`, `domains`, `email` (+ `webhooks`), `senderDomain`, `auth`, `apps`, `tier`, `billing`, `contracts`, `ai`, `allowance`, `service`, `admin`. Every operation throws a typed `Run402Error` subclass on failure: `PaymentRequired`, `ProjectNotFound`, `Unauthorized`, `ApiError`, `NetworkError`, `LocalError`, `Run402DeployError`. See [`sdk/README.md`](./sdk/README.md).
205231

206232
## CLI — `run402`
207233

@@ -219,6 +245,7 @@ run402 projects sql <id> "CREATE TABLE …"
219245
run402 projects apply-expose <id> --file manifest.json
220246
run402 sites deploy-dir ./dist
221247
run402 functions deploy <id> <name> --file fn.ts
248+
run402 ci link github --project <id> # GitHub Actions OIDC deploy binding
222249
run402 blob put ./asset.png --immutable
223250
run402 blob diagnose <url> # inspect live CDN state for a public URL
224251
run402 cdn wait-fresh <url> --sha <hex> # poll until a mutable URL serves the new SHA

SKILL.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,9 @@ describe("openclaw/SKILL.md (CLI-based)", () => {
171171
"run402 projects sql",
172172
"run402 projects apply-expose",
173173
"run402 sites deploy-dir",
174+
"run402 ci link github",
175+
"run402 ci list",
176+
"run402 ci revoke",
174177
"run402 blob put",
175178
"run402 tier set",
176179
];

0 commit comments

Comments
 (0)