You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The gateway deleted POST /projects/v1/admin/:id/functions (live on main), so
r.functions.deploy() 404s in prod. Re-point it onto the unified apply engine
(a one-function functions.patch.set ReleaseSpec) — the same path the rest of
the SDK uses. Auth shifts from the service-key bearer to the standard apply
credential (SIWX wallet, or the v3.1.0 project.deploy operator-approval gate).
Public signature + FunctionDeployResult shape unchanged; runtime_version and
deps_resolved are null on the deploy result (apply returns release-level data —
they live on the functions-list record).
Wire `deps` end-to-end through the apply engine (capability
apply-v1-function-deps, verified on origin/main): public FunctionSpec.deps, the
validateSpec field allowlist, normalizeFunction, and functionToWire. Without all
four, functions.deploy({ deps }) would silently drop deps at the wire.
transfer-accept (#428): AcceptTransferResult gains anon_key/service_key, and
accept() persists them via saveProject + setActiveProject (mirroring provision)
so the new owner can operate the inherited project with no extra provisioning.
Tests: rewrote the SDK + MCP deploy tests onto the apply flow (plan+commit;
empty missing_content skips upload, terminal ready skips poll); added
transfer-accept persistence tests. Corrected the now-false docs (unified deploy
does accept deps; the deploy result no longer surfaces runtime_version /
deps_resolved). npm test green (0 fail), build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: cli/llms-cli.txt
+5-5Lines changed: 5 additions & 5 deletions
Original file line number
Diff line number
Diff line change
@@ -228,7 +228,7 @@ Manifest format mirrors a v2 `ReleaseSpec`. For editor autocomplete, use top-lev
228
228
229
229
File entries accept a bare UTF-8 string, `{ "data": "...", "encoding": "utf-8" | "base64", "content_type": "..." }`, or `{ "path": "dist/index.html", "content_type": "text/html" }`. In `--manifest` files, relative paths resolve against the manifest file's directory; in `--spec` / stdin they resolve against the current working directory. Migration entries may use `"sql_path"` or `"sql_file"` instead of inline `"sql"`. The CLI and MCP adapters share the SDK's typed `normalizeDeployManifest` path, so the same JSON can become SDK-native `ReleaseSpec` code without reimplementing base64 or path handling. The adapter is strict: only top-level `$schema` metadata is ignored; unknown fields and no-op specs are rejected before deploy planning, so typos like `"subdomain"`, `"site.replcae"`, `"functions.replace.api.deps"`, or `"functions.replace.api.config.schedule"` do not become partial deploys.
230
230
231
-
Function specs in unified deploy manifests accept `runtime` (`"node22"`), exactly one code source (`source` for a single module, or `files` plus `entrypoint` for multi-file functions), `config.timeout_seconds`, `config.memory_mb`, and `schedule` as a sibling of `config`. In patch mode, omit `schedule` to leave it unchanged and use `"schedule": null` to remove an existing schedule. Unified deploy manifests do not accept `deps` yet; bundle dependencies into the function files or use `run402 functions deploy --deps` for the standalone function path.
231
+
Function specs in unified deploy manifests accept `runtime` (`"node22"`), exactly one code source (`source` for a single module, or `files` plus `entrypoint` for multi-file functions), `config.timeout_seconds`, `config.memory_mb`, and `schedule` as a sibling of `config`. In patch mode, omit `schedule` to leave it unchanged and use `"schedule": null` to remove an existing schedule. Unified deploy manifests accept `deps` (a `string[]` of npm specs) under capability `apply-v1-function-deps`; the gateway installs and bundles them. `run402 functions deploy --deps` builds a one-function `functions.patch.set` and rides this same unified-apply path (the legacy standalone deploy route was removed).
232
232
233
233
Deploy preflights obvious tier function caps after manifest normalization but before CAS upload or `/apply/v1/plans`: timeout, memory, cron interval, and scheduled-function count. Structured local failures use `code: "BAD_FIELD"` with `details.field`, `details.value`, `details.tier`, the relevant limit e.g. `tier_max` or `min_interval_minutes`, and `details.limit_source` (`tier_status` or `local_static_fallback`). Current authoring caps: prototype 10s / 128 MB / 1 scheduled fn / 15 min, hobby 30s / 256 MB / 3 / 5 min, team 60s / 512 MB / 10 / 1 min. `run402 tier status` shows live function caps and current scheduled usage when the gateway returns them.
234
234
@@ -589,7 +589,7 @@ Create a manifest file `app.json` (use the `project_id` from provision):
589
589
590
590
`project_id` is required (from `run402 projects provision`). All other fields are optional; omitted top-level sections are carried forward from the current release. The adapter is strict: only top-level `$schema` metadata is ignored; typo fields e.g. `subdomain`, `site.replcae`, `functions.replace.my-fn.deps`, or `functions.replace.my-fn.config.schedule` fail before planning.
591
591
592
-
Function specs accept `runtime: "node22"`, exactly one code source (`source`, or `files` plus `entrypoint`), `config.timeout_seconds`, `config.memory_mb`, a sibling `schedule`, and the v1.51+ auth-gate fields `require_auth` / `require_role` (see "Function-level auth gates" below). Omit `schedule` in patch mode to leave it unchanged; set it to `null` to remove the schedule. Unified deploy manifests do not accept `deps`; bundle dependencies into the function files or use `run402 functions deploy --deps`.
592
+
Function specs accept `runtime: "node22"`, exactly one code source (`source`, or `files` plus `entrypoint`), `config.timeout_seconds`, `config.memory_mb`, a sibling `schedule`, and the v1.51+ auth-gate fields `require_auth` / `require_role` (see "Function-level auth gates" below). Omit `schedule` in patch mode to leave it unchanged; set it to `null` to remove the schedule. Unified deploy manifests accept `deps` (npm specs) under capability `apply-v1-function-deps`; `run402 functions deploy --deps` rides this same unified-apply path.
593
593
594
594
Function-level auth gates (v1.51+). Two optional declarative fields on each function spec:
595
595
@@ -890,7 +890,7 @@ One noun, two rails: a **wallet** recipient is a two-party wallet transfer; an *
890
890
- `run402 transfer init --to <wallet|email> [--project <id>] [--billing-policy migrate] [--message <text>] [--kysigned <record_id>] [--retain-collaborator developer]` — initiate (owner-only). `--to` routes by recipient kind: a wallet starts a transfer, an email starts a handoff. Returns `{ transfer_id, expires_at, ... }`. `--retain-collaborator developer` (v1.91, **email handoffs only** — rejected with `BAD_FLAG` on the wallet rail; only `developer` is valid) asks to keep a `developer` membership in the recipient's org after handoff; the recipient must accept it at claim time, and omitting it is a full severance (the default). Gateway codes: `INVALID_RETAIN_ROLE`, `RETAIN_SUBJECT_REQUIRED`.
891
891
- `run402 transfer preview <transfer_id> [--handoff]` — fetch the preview document (either party); `--handoff` previews an email->org handoff. Lists custom domains, subdomains, function names, secret NAMES (never values), CI bindings that will be revoked on accept, mailbox summary, billing implications.
892
892
- `run402 transfer list [--incoming | --outgoing | --handoffs] [--limit N] [--offset N]` — `--incoming` (default) shows wallet transfers OFFERED TO you; `--outgoing` shows transfers you initiated; `--handoffs` shows incoming email->org handoffs. Each entry includes `preview_path`.
893
-
- `run402 transfer accept <transfer_id>` — accept a WALLET transfer (recipient). Atomically flips ownership, revokes the previous owner's CI bindings on the project, and stamps a `secrets_rotation_advised` advisory. Secret VALUES are inherited; the response returns `secret_names_inherited[]`.
893
+
- `run402 transfer accept <transfer_id>` — accept a WALLET transfer (recipient). Atomically flips ownership, revokes the previous owner's CI bindings on the project, and stamps a `secrets_rotation_advised` advisory. Secret VALUES are inherited; the response returns `secret_names_inherited[]`. The accept response also returns the new owner's project `anon_key` + `service_key` (#428), and the SDK/CLI persist them into the local keystore and set the project active — so you can deploy / set secrets / run SQL on the inherited project with no extra provisioning step.
894
894
- `run402 transfer claim <transfer_id> [--into <organization_id>] [--accept-retained-collaborator]` — claim an incoming EMAIL handoff into an org you own (omit `--into` to claim into a brand-new org). The handoff analog of `accept`. `--accept-retained-collaborator` accepts the sender's v1.91 retained-`developer`-membership offer (visible as `retain_collaborator` on `transfer preview`); omit it for a full severance. The claim result carries `retained_collaborator_principal_id` (or `null`).
895
895
- `run402 transfer cancel <transfer_id> [--reason <text>] [--handoff]` — cancel (either party); `--handoff` cancels an email->org handoff.
896
896
@@ -1040,11 +1040,11 @@ For routed browser 500s, copy `X-Run402-Request-Id` or the JSON `request_id` fro
The deploy / list / update responses include two fields populated under bundling-at-deploy:
1043
+
`run402 functions deploy` routes through the unified apply engine, so its **result** sets `runtime_version` and `deps_resolved` to `null` (apply returns release-level data, not per-function build metadata). Read the resolved values from `run402 functions list` (and `update`), whose records carry both fields populated under bundling-at-deploy:
1044
1044
- `runtime_version` — the bundled `@run402/functions` version (e.g. `"1.48.0"`). Surface this as "Functions runtime version" — never bare "runtime", which already names the Node runtime (e.g. `node22`). `null` for legacy functions.
1045
1045
- `deps_resolved` — map of each `--deps` name to the -installed concrete version (e.g. `{"date-fns": "3.7.0"}` for a `^3.6.0` spec). Direct deps only; this is not a lockfile. `{}` for an empty `--deps`; `null` for legacy functions.
1046
1046
1047
-
The deploy response also gains an optional top-level `warnings: string[]` (sibling to the function record, not inside it) for non-fatal deploy notes e.g. bundle-size advisories. Omitted or `[]` when there are no warnings.
1047
+
The deploy result still includes an optional top-level `warnings: string[]` (sibling to the function record, not inside it) for non-fatal deploy notes e.g. bundle-size advisories. Omitted or `[]` when there are no warnings.
Two gateway changes (run402-private, Jun 16-17) landed after the public client was last synced:
4
+
5
+
-**`d38a2e51`** deleted `POST /projects/v1/admin/:project_id/functions` (~160 lines) and removed it from `openapi.json` + `llms-full.txt`. The commit body self-corrects: *"the route WAS in openapi+llms (the proposal wrongly said absent)."* The gateway kept `deployFunction` in services (used by `/apply/v1` + bundling) and the `/functions/:name/*` management sub-routes + the GET-list. It migrated its own test callers onto a `deployFunctionViaApplyV1` helper: compute SHA-256 of the code, CAS-upload, build a `functions.patch.set``ReleaseSpec`, POST `/apply/v1/plans` → commit → poll, return the legacy `{name,url,status,schedule}` shape.
6
+
-**`ea6bc171` (#428)** added `anon_key`+`service_key` to the transfer-accept 200 body (derived post-commit via `deriveProjectKeys(project_id)` — stateless JWTs, never persisted, same as `POST /projects/v1`). `acceptTransfer` (the service) stays pure; keys are a route-layer concern.
7
+
8
+
Public state: `r.functions.deploy()` POSTs the deleted route with the service-key bearer; `AcceptTransferResult` is a closed interface with no key fields and `accept()` does a bare request with no keystore write.
9
+
10
+
## Goals / Non-Goals
11
+
12
+
**Goals:** restore function deploy (it 404s in prod) by routing through the same `/apply/v1` engine the rest of the SDK uses; give the transfer recipient working keys with no extra step; keep both public method signatures + return types stable.
13
+
**Non-Goals:** changing the deploy-spec / apply surface; the handoff (email→org) path (#428 touched only the wallet-transfer accept); re-adding any deleted gateway route.
14
+
15
+
## Decisions
16
+
17
+
### Decision 1 — `functions.deploy()` delegates to the apply engine, not a hand-rolled dance
18
+
19
+
`new Deploy(this.client)` is constructible from the kernel `Client` (exactly what `Run402` does for `_applyEngine = new Deploy(client)`). `deploy()` builds a one-function `ReleaseSpec` and calls `engine.apply(spec, {})`:
The engine handles the CAS upload of `source` (a polymorphic `ContentSource` — inline bytes are normalized + uploaded automatically), the plan/commit/poll state machine, and safe-retry. We do NOT reimplement the gateway test helper's manual CAS dance — the SDK already owns it. **Alternative — hand-roll plan/upload/commit in `functions.deploy`** (mirror the gateway test helper): rejected, it duplicates `_applyEngine` and drifts.
32
+
33
+
**`deps` is preserved — verified against `origin/main`.** The riskiest correctness property: would re-pointing through apply silently drop the documented `deps` option? The local private working tree (`0cda4ebf`, stale) shows apply activation passing `undefined` for deps ("not surfaced through the v2 spec yet") — but that predates `d38a2e51`. On `origin/main` (the prod deploy source) the apply path fully supports function deps via capability **`apply-v1-function-deps`**: `functions.patch.set.<name>.deps: string[]`, validated at `apply-v1.ts:1417` and persisted into the staged `config` JSONB. The gateway's own `deployFunctionViaApplyV1` migration helper sets `fnSpec.deps = args.deps`, and the prod-validated `deps-smoke` test (deps:[lodash], "6/0 against prod") rides this exact path. So `functions.deploy` carries `deps` straight into `FunctionSpec.deps` with no loss. This required adding the missing **`deps?: string[]`** field to the public `FunctionSpec` type (the apply surface was deps-capable on the wire but the SDK type never exposed it).
-`url` ← `result.urls[name]` if present, else derived `…/functions/v1/<name>` from the project's site host (best-effort).
40
+
-`warnings` ← `result.warnings` mapped to their messages.
41
+
-`runtime_version` / `deps_resolved` ← **`null`**. They were fields of the deleted route's response and are not on the apply result. The gateway's own migration shim returns only `{name,url,status,schedule}`, so this matches; both fields are already typed `?: … | null`. Documented as "not surfaced via deploy; the function record carries them."
42
+
43
+
### Decision 3 — Auth shifts from the service key to the `project.deploy` gate
44
+
45
+
The old route took `Authorization: Bearer <service_key>`. `/apply/v1` authorizes via the standard credential (SIWX wallet, or — for a wallet-less human — the operator-approval `project.deploy` gate shipped in v3.1.0). This is correct: deploying a function IS a `project.deploy`. An agent with a wallet is unaffected; a wallet-less human gets the operator-approval flow automatically. `deploy()` still calls `getProject(projectId)` first to fail fast with `ProjectNotFound` on an unknown id (and the spec needs the id), but no longer sends the service key.
`AcceptTransferResult` gains `anon_key: string` / `service_key: string`. After the accept request returns, `accept()` does the same persistence `provision` does:
50
+
51
+
```
52
+
if (creds.saveProject) await creds.saveProject(result.project_id, { anon_key, service_key });
53
+
if (creds.setActiveProject) await creds.setActiveProject(result.project_id);
54
+
```
55
+
56
+
so the new owner's keystore has the project + it's active. Gated on the optional provider methods (sandbox providers without persistence simply skip). The CLI prints the accept result as-is (the `service_key` JWT is identical to what `provision` already prints). The handoff/claim path is untouched (#428 changed only the wallet-transfer accept).
57
+
58
+
## Risks / Trade-offs
59
+
60
+
-**`runtime_version`/`deps_resolved` degradation** → documented; both already nullable; matches the gateway shim. A consumer needing them reads the function record (kept GET routes).
61
+
-**`url` is best-effort** → derived from `urls[name]` or the site host; a future enrichment could read the materialized route, but is out of scope for restoring deploy.
62
+
-**Auth shift could surprise a service-key-only caller** → in practice `functions.deploy` callers are the wallet/agent flow or the CLI (which has a wallet/operator session); the gate is the same one `deploy apply` uses.
63
+
-**Persisting `service_key` to the keystore** → identical to `provision`; the new owner legitimately holds it.
64
+
65
+
## Migration Plan
66
+
67
+
Additive/behavioral, client-only, non-breaking to the public signatures. Ships in the normal lockstep release (urgent — the function-deploy 404 is live). Rollback = revert; the gateway routes are independent.
68
+
69
+
## Open Questions
70
+
71
+
- Whether to enrich `runtime_version`/`deps_resolved` via a post-deploy function-record read — deferred (extra round-trip; both nullable; not needed to restore deploy).
0 commit comments