run402 4.79.2 → 4.80.1

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.
Files changed (64) hide show
  1. package/README.md +22 -4
  2. package/gitvault-surface.json +1 -1
  3. package/lib/command-manifest.mjs +4 -1
  4. package/lib/deploy-v2.mjs +18 -2
  5. package/lib/doctor.mjs +164 -30
  6. package/lib/errors.mjs +5 -3
  7. package/lib/functions.mjs +153 -42
  8. package/lib/gitvault-scaffold.mjs +19 -7
  9. package/lib/harness-context.mjs +6 -2
  10. package/lib/image.mjs +3 -1
  11. package/lib/init.mjs +5 -3
  12. package/lib/logs.mjs +125 -96
  13. package/lib/repos.mjs +40 -8
  14. package/lib/up.mjs +67 -21
  15. package/package.json +1 -1
  16. package/sdk/dist/actions.d.ts +27 -9
  17. package/sdk/dist/actions.d.ts.map +1 -1
  18. package/sdk/dist/errors.d.ts +1 -1
  19. package/sdk/dist/errors.d.ts.map +1 -1
  20. package/sdk/dist/errors.js.map +1 -1
  21. package/sdk/dist/index.d.ts +1 -1
  22. package/sdk/dist/index.d.ts.map +1 -1
  23. package/sdk/dist/index.js +1 -1
  24. package/sdk/dist/index.js.map +1 -1
  25. package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
  26. package/sdk/dist/namespaces/deploy.js +35 -3
  27. package/sdk/dist/namespaces/deploy.js.map +1 -1
  28. package/sdk/dist/namespaces/deploy.types.d.ts +15 -8
  29. package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
  30. package/sdk/dist/namespaces/deploy.types.js +1 -1
  31. package/sdk/dist/namespaces/deploy.types.js.map +1 -1
  32. package/sdk/dist/namespaces/errors.d.ts +4 -2
  33. package/sdk/dist/namespaces/errors.d.ts.map +1 -1
  34. package/sdk/dist/namespaces/errors.js +19 -9
  35. package/sdk/dist/namespaces/errors.js.map +1 -1
  36. package/sdk/dist/namespaces/functions.d.ts +17 -1
  37. package/sdk/dist/namespaces/functions.d.ts.map +1 -1
  38. package/sdk/dist/namespaces/functions.js +131 -2
  39. package/sdk/dist/namespaces/functions.js.map +1 -1
  40. package/sdk/dist/namespaces/functions.types.d.ts +62 -0
  41. package/sdk/dist/namespaces/functions.types.d.ts.map +1 -1
  42. package/sdk/dist/namespaces/gitvault.d.ts +38 -0
  43. package/sdk/dist/namespaces/gitvault.d.ts.map +1 -1
  44. package/sdk/dist/namespaces/gitvault.js +122 -14
  45. package/sdk/dist/namespaces/gitvault.js.map +1 -1
  46. package/sdk/dist/node/actions-node.d.ts.map +1 -1
  47. package/sdk/dist/node/actions-node.js +176 -16
  48. package/sdk/dist/node/actions-node.js.map +1 -1
  49. package/sdk/dist/node/client-detect.d.ts +16 -1
  50. package/sdk/dist/node/client-detect.d.ts.map +1 -1
  51. package/sdk/dist/node/client-detect.js +26 -9
  52. package/sdk/dist/node/client-detect.js.map +1 -1
  53. package/sdk/dist/node/deploy-manifest.d.ts +42 -0
  54. package/sdk/dist/node/deploy-manifest.d.ts.map +1 -1
  55. package/sdk/dist/node/deploy-manifest.js +175 -1
  56. package/sdk/dist/node/deploy-manifest.js.map +1 -1
  57. package/sdk/dist/node/index.d.ts +2 -2
  58. package/sdk/dist/node/index.d.ts.map +1 -1
  59. package/sdk/dist/node/index.js +1 -1
  60. package/sdk/dist/node/index.js.map +1 -1
  61. package/sdk/dist/scoped.d.ts +2 -1
  62. package/sdk/dist/scoped.d.ts.map +1 -1
  63. package/sdk/dist/scoped.js +3 -0
  64. package/sdk/dist/scoped.js.map +1 -1
package/README.md CHANGED
@@ -4,7 +4,7 @@ Command-line interface for [Run402](https://run402.com) — provision Postgres d
4
4
 
5
5
  The CLI lets an agent act as its own first-class Run402 principal rather than borrowing a human account. Identity records who invoked an action; organization roles, grants, delegates, freshness, and spend policy determine what that principal may do. People remain first-class owners and collaborators without becoming routine CLI operators.
6
6
 
7
- For the full CLI reference (every flag, every subcommand) see **<https://docs.run402.com/llms-cli.txt>**.
7
+ For the full CLI reference (every flag, every subcommand) start at **<https://docs.run402.com/llms-cli.txt>** — the index holds the first-deploy contract and links one fetchable `/llms-cli-<slice>.txt` per topic (deploy, commands, repos, orgs, functions, assets, ops, errors, frontend, platform); <https://docs.run402.com/llms-cli-full.txt> is the whole reference as one document.
8
8
 
9
9
  ## Install
10
10
 
@@ -44,6 +44,7 @@ run402 up --manifest run402.deploy.ts --plan
44
44
  run402 up --manifest run402.deploy.ts --require-plan plan_...
45
45
  run402 up --manifest run402.deploy.json --project prj_...
46
46
  run402 up verify --project prj_...
47
+ run402 up --nested -y # app root inside another repo: its own nested repo + run402 remote
47
48
  ```
48
49
 
49
50
  `up` is a thin CLI shim over the Node SDK action runner. It discovers `run402.json`, `run402.deploy.json`, then `app.json`. A `run402.json` carrying the app schema/app markers uses the app-install graph; a release-shaped `run402.json` is normalized through the same ReleaseSpec path as `deploy apply` for compatibility. It validates the deploy input before any mutation, resolves the project as `--project` → `.run402/project.json` → manifest `project_id` → approved creation from `--name` → approved active-project fallback, then applies the manifest. `--name` is creation/link metadata only; it is not a manifest field and never renames an existing project. When everything is already configured, plain `run402 up` deploys. Non-interactive recursive prerequisites/local writes require `-y/--yes`.
@@ -52,6 +53,8 @@ For app manifests with `verify.http[]`, `up` runs HTTP checks after deploy. Fres
52
53
 
53
54
  For typed `run402.deploy.ts` configs, pass `--manifest` explicitly because TypeScript/JavaScript configs execute local code. Use `--check` for local-only import/normalize/file validation, `--print-spec` to inspect the normalized `ReleaseSpec`, `--plan` for a gateway-reviewed non-deploying plan, and `--require-plan <plan_id>` to apply only that reviewed intent. Warning flags are not used with `--require-plan`; the reviewed plan binds the exact warning/destructive set. Run402 Core skips Cloud allowance/tier prerequisites and fails closed when no Core project is selected.
54
55
 
56
+ Every mode (and `deploy apply`) verifies the files the manifest references before any gateway call: migration `sql_path`/`sql_file`, function `source`/`files`, site `{ path }` entries and `dir()` targets, `assets.put` sources. A missing one is `MANIFEST_FILE_MISSING` (`details.missing[]` = `{ field_path, path, kind }`, one `create_file` next action per file). With no manifest in the working directory, `UP_MANIFEST_REQUIRED` looks one directory down (`details.nearby_manifests[]`, a leading `run_in_directory` next action such as `run402 up --check --dir <dir>`); `--manifest <path>` to a missing file is a typed `MANIFEST_NOT_FOUND` with a `create_manifest` next action. An app root that lies inside another repository is never scaffolded into it: the skip carries a `create_nested_repo` next action, and `--nested` gives the app its own nested repository (`git init -b main`, the `run402` remote, the first push) plus exactly one line in the enclosing repository's local `.git/info/exclude`, nothing else touched. `up` also sets this principal's display name when it has none: `RUN402_AGENT_NAME` (overrides an existing name), else the detected client (`claude-code`, `codex`, `cursor`, `grok`, or `RUN402_CLIENT=<name>` for one with no marker), and `result.identity` reports `detected` plus `detection: { applied, reason }` either way.
57
+
55
58
  ### repos — host-blind encrypted git repos (zero deploy ceremony)
56
59
 
57
60
  One noun, fourteen verbs (`repo` singular resolves identically). `run402 gitvault <verb>` answers `COMMAND_MOVED`/`COMMAND_REMOVED`.
@@ -63,12 +66,13 @@ git push -u origin main # allocates the vault on first pus
63
66
 
64
67
  # or, one call: provision + allocate + scaffold, nothing deployed
65
68
  run402 repos create my-notes
69
+ run402 repos create --nested --project prj_xyz # app inside another repo: its own nested repo + remote
66
70
  run402 repos view --human
67
71
  run402 repos list --org org_1a2b3c
68
72
  run402 repos delete --project prj_xyz --force # refuses without --force while the vault holds generations
69
73
  ```
70
74
 
71
- A hosted git remote, encrypted before it leaves the machine — no deploy, no manifest, no app. `origin` is claimed additively — an existing `origin` is never touched, the run402 remote falls back to `run402` instead. Every mutating `repos` verb (`create`, `rename`, `delete`, `snapshot`, `policy`, `mirror`, `gc`, `handoff`, `resume`, `invite`, `join`) is CLI/OpenClaw-only by design — no MCP tool exists or will exist for them (one-shot recovery receipts, immutable generations, irreversible delete, single-use bearer keys). Three READ-ONLY tools do exist — `repos_view`, `repos_list_heads`, `repos_fsck` — teaching only `repos` spellings. See `run402 repos --help` for the full tiered surface (common: `create`/`view`/`list`; occasional: `snapshot`/`mirror`/`recover`/`handoff`/`resume`/`invite`/`join`; lifecycle: `rename`/`delete`; maintenance: `fsck`/`gc`/`access`/`policy`), and the CLI reference's `repos` section for the terminal-loss statement and the progressive backup warning.
75
+ A hosted git remote, encrypted before it leaves the machine — no deploy, no manifest, no app. `origin` is claimed additively — an existing `origin` is never touched, the run402 remote falls back to `run402` instead. A directory inside another repository is never scaffolded into that repository: the skip carries a `create_nested_repo` next action, `--nested` makes it its own nested repository (one line appended to the enclosing repository's local `.git/info/exclude`, nothing else touched), and `create` prints a `git push` next action only for a remote it actually added. Every mutating `repos` verb (`create`, `rename`, `delete`, `snapshot`, `policy`, `mirror`, `gc`, `handoff`, `resume`, `invite`, `join`) is CLI/OpenClaw-only by design — no MCP tool exists or will exist for them (one-shot recovery receipts, immutable generations, irreversible delete, single-use bearer keys). Three READ-ONLY tools do exist — `repos_view`, `repos_list_heads`, `repos_fsck` — teaching only `repos` spellings. See `run402 repos --help` for the full tiered surface (common: `create`/`view`/`list`; occasional: `snapshot`/`mirror`/`recover`/`handoff`/`resume`/`invite`/`join`; lifecycle: `rename`/`delete`; maintenance: `fsck`/`gc`/`access`/`policy`), and the CLI reference's `repos` section for the terminal-loss statement and the progressive backup warning.
72
76
 
73
77
  **Handoff / resume.** `run402 repos handoff [--note-file <path>]` captures the actual working tree (staged/unstaged/untracked, like `git stash push -u`) into the vault and mints a single-use bearer key (`kgh1_…`, printed to stdout exactly once); `run402 repos resume <kgh1_…>` claims it on another machine, clones fresh, and reapplies the exact dirty state with `git stash apply --index`. No shared keystore, no shared allowance — the key is the whole handoff. On a wallet with no active tier, `resume` first folds the same cold-start chain `create` does (allowance → faucet → one x402 prototype payment); `--no-init` opts out, and the claim itself never waits on it. Sensitive untracked files (`.env`, `*.pem`, SSH/AWS/GPG dirs, …) are excluded from capture by default (`--include-sensitive <glob>` re-admits one). See the reference `run402.com/llms-cli.txt` for the full note-schema and flag reference.
74
78
 
@@ -169,7 +173,7 @@ Promote success means the origin pointer is active; mutable public URLs may stil
169
173
  `deploy-dir` hashes each file client-side and only uploads bytes the gateway doesn't already have. Re-deploying an unchanged tree returns immediately with `bytes_uploaded: 0`. Progress events stream to stderr.
170
174
  Release inspection commands print `{ release: ... }` or `{ diff: ... }` (raw payload, no envelope — see the "Output Contract" section in [llms-cli.txt](llms-cli.txt)); use them after deploys to compare release inventory without starting another mutation. `deploy verify` prints the canonical edge-coherence report and exits 2 when the report is valid but not yet coherent. Inventories include `release_generation`, `static_manifest_sha256`, and nullable `static_manifest_metadata`; diffs include `static_assets` counters such as unchanged/changed/added/removed and CAS byte reuse. `deploy diagnose` / `deploy resolve --url` print URL-first diagnostics with `would_serve`, `diagnostic_status`, `match`, warnings, `edge_propagation`, and next steps; host misses are successful diagnostic calls with `would_serve: false`. Stable-host resolve fields can include `authorization_result`, `cas_object`, `response_variant`, `allow`, `route_pattern`, `target_type`, `target_name`, `target_file`, and `edge_propagation` (`settled`, `propagating`, or `sync_pending`).
171
175
 
172
- For database-bearing changes, use `run402 apply --manifest app.json --rehearse --json` before commit. It creates a contained branch, applies the candidate plan there, runs checks, and exits nonzero on a failed rehearsal. Manual restore points live under `run402 snapshots create|list|get|restore|delete`; temporary data branches live under `run402 branches create|list|renew|delete`.
176
+ For database-bearing changes, use `run402 apply --manifest app.json --rehearse --json` before commit. It creates a contained branch, applies the candidate plan there, runs checks, and exits nonzero on a failed rehearsal. The automatic rehearsal inside `up` / `apply` reports `rehearsal: { status: "skipped", reason }` when there is nothing to protect: `no_live_release` on a first deploy, `no_migrations`, or `migrations_unchanged` when every migration is already applied with an identical checksum (a page-only redeploy that still carries its migrations ships in seconds). `PUBLIC_ROUTED_FUNCTION` is informational (`requires_confirmation: false`) and needs no `--allow-warning`; only warnings with `requires_confirmation: true` do. Manual restore points live under `run402 snapshots create|list|get|restore|delete`; temporary data branches live under `run402 branches create|list|renew|delete`.
173
177
 
174
178
  ### GitHub Actions OIDC deploys
175
179
 
@@ -220,12 +224,17 @@ run402 functions deploy <id> my-fn --file fn.ts \
220
224
  --schedule "*/15 * * * *" \
221
225
  --deps "stripe,zod@^3"
222
226
  run402 functions logs <id> my-fn --tail 100 --request-id req_abc123 --follow
227
+ run402 functions logs --request-id req_abc123 --project <id> # <name> optional with --request-id: every function is searched
228
+ run402 functions logs my-fn --project <id> --all # include the Lambda INIT_START / REPORT lines
229
+ run402 logs --request-id req_abc123 --project <id> # the x-run402-request-id response header, no function name needed
223
230
  run402 functions invoke my-fn --project <id> --body-file request.json
224
231
  run402 functions invoke paid-fn --project <id> --body-file request.json --idempotency-key paid:call:123 --wait
225
232
  run402 functions rebuild <id> my-fn # refresh ONE function onto the current runtime
226
233
  run402 functions rebuild <id> --all # refresh every function in the project
227
234
  ```
228
235
 
236
+ Log reads are app-first: every entry carries `origin: "app" | "platform"`, the Lambda runtime lines (INIT_START, START/END/REPORT RequestId, billed duration) are hidden by default (`--app`), `--platform` shows only them and `--all` the raw stream; `hidden` counts what was dropped and a `hint` explains an empty result. `--request-id` accepts `req_`, `fnrun_`, and `fnatt_` ids, `--since` takes an ISO timestamp or epoch ms, and `run402 logs` rejects unknown flags.
237
+
229
238
  Inline `--body <json>` is validated before any request. Prefer `--body-file <path>` for agents and Windows `cmd.exe`; the file path avoids shell quoting entirely. Invalid or empty JSON fails locally instead of invoking the function with corrupted input.
230
239
 
231
240
  `functions rebuild` is opt-in and never changes your source: it re-bundles the stored source against the platform's current runtime/entry-wrapper (deps pinned to the exact versions recorded at deploy), so a gateway-side wrapper fix (e.g. an SSR `auth.*` fix) reaches an already-deployed function — a plain redeploy with unchanged source does not. The source `code_hash` is unchanged and no new release is created. Functions deployed before dependency locking return `CANNOT_REBUILD_UNLOCKED_DEPS`; redeploy those from source instead. `run402 doctor` flags functions on a stale runtime.
@@ -303,6 +312,15 @@ run402 tier set hobby # $5 / 30 days
303
312
  run402 billing checkout <org_id> --product tier --tier hobby # Stripe alternative
304
313
  ```
305
314
 
315
+ ### Doctor
316
+
317
+ ```bash
318
+ run402 doctor # { ok, blocking[], warnings[], checks[] }
319
+ run402 doctor --refresh # live npm check for a newer run402
320
+ ```
321
+
322
+ `ok` is structural: `true` exactly when `blocking[]` is empty. Every check carries `severity: "blocking" | "advisory" | "info"`; advisory findings (an unbound operator passkey, a degraded recovery posture on another org, a stale CLI, gitvault gaps) land in `warnings[]` without changing `ok` or the exit code. The `tier` check's `status` is a fixed vocabulary (`ok | inactive | frozen | past_due | dormant | purged | missing | unknown | error`, never a tier name; see `value.tier` / `value.lifecycle`), and a wallet whose own org holds no tier but can reach another org's projects reports `missing` as advisory (`TIER_MISSING_ON_OWN_ORG`).
323
+
306
324
  ## State
307
325
 
308
326
  Local state lives at:
@@ -357,7 +375,7 @@ The canonical, comprehensive CLI reference — every flag, every subcommand, edg
357
375
 
358
376
  **<https://docs.run402.com/llms-cli.txt>**
359
377
 
360
- Same content also at [`cli/llms-cli.txt`](./llms-cli.txt) in the repo. Treat that file as authoritative; this README is a quick-orientation landing page.
378
+ Same content also at [`cli/llms-cli.txt`](./llms-cli.txt) (index), [`cli/llms-cli-<slice>.txt`](./) (topic slices) and [`cli/llms-cli-full.txt`](./llms-cli-full.txt) (everything) in the repo. Treat those files as authoritative; this README is a quick-orientation landing page.
361
379
 
362
380
  ## Other interfaces
363
381
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "surface_version": "4.79.2",
2
+ "surface_version": "4.80.1",
3
3
  "verbs": [
4
4
  "repos create",
5
5
  "repos list",
@@ -485,7 +485,10 @@ export const COMMAND_MANIFEST = [
485
485
  // without rejecting it," which this satisfies.
486
486
  { path: ["doctor"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["--no-scan"], runStyle: "merged" },
487
487
  { path: ["webhook-secret", "rotate"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: [] },
488
- { path: ["logs"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["--request-id", "req_gate123"], runStyle: "merged" },
488
+ // `run402 logs [<function>] --request-id <id>`: the optional positional is
489
+ // the function name (`run402 errors` samples print it that way); it is
490
+ // never a project id.
491
+ { path: ["logs"], positionals: [p("function", { required: false })], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["--request-id", "req_gate123"], runStyle: "merged" },
489
492
  ];
490
493
 
491
494
  // Families deliberately absent from the manifest, consumed by the gate's
package/lib/deploy-v2.mjs CHANGED
@@ -26,6 +26,7 @@ import { existsSync, fstatSync, readFileSync } from "node:fs";
26
26
  import { resolve, dirname, extname, isAbsolute } from "node:path";
27
27
  import {
28
28
  applyWithGitvault,
29
+ assertLocalFileReferencesExist,
29
30
  buildDeployResolveSummary,
30
31
  githubActionsCredentials,
31
32
  loadDeployManifest,
@@ -1182,6 +1183,18 @@ async function applyCmd(args) {
1182
1183
  const releaseSpec = normalizedManifest.spec;
1183
1184
  const idempotencyKey = normalizedManifest.idempotencyKey;
1184
1185
 
1186
+ // Filesystem-reference check: every function source, site file / dir and
1187
+ // asset path the normalized spec still points at must exist. One `stat`
1188
+ // per reference, so it runs in every mode — `--check` / `--print-spec`
1189
+ // included — before any gateway call (MANIFEST_FILE_MISSING).
1190
+ try {
1191
+ await assertLocalFileReferencesExist(releaseSpec, {
1192
+ ...(manifestPath ? { manifestPath } : {}),
1193
+ });
1194
+ } catch (err) {
1195
+ reportSdkError(err);
1196
+ }
1197
+
1185
1198
  if (opts.mode === "check") {
1186
1199
  console.log(JSON.stringify({
1187
1200
  ok: true,
@@ -1531,12 +1544,15 @@ function enhanceDeployWarningError(err) {
1531
1544
 
1532
1545
  const ROUTE_WARNING_GUIDANCE = {
1533
1546
  PUBLIC_ROUTED_FUNCTION: {
1534
- hint: "A deploy route makes a function public same-origin browser ingress; direct /functions/v1/:name remains API-key protected.",
1547
+ // Informational on the gateway (requires_confirmation: false): it never
1548
+ // blocks a deploy by itself, so the guidance must not teach an
1549
+ // acknowledgement flag the warning does not need.
1550
+ hint: "A deploy route makes a function public same-origin browser ingress; direct /functions/v1/:name remains API-key protected. This warning is informational and needs no --allow-warning; another warning with requires_confirmation: true is what blocked this plan.",
1535
1551
  next_actions: [
1536
1552
  nextAction("edit_request", { why: "Review application auth and authorization in the routed function." }),
1537
1553
  nextAction("edit_request", { why: "Add CSRF protection for cookie-authenticated POST/PUT/PATCH/DELETE routes." }),
1538
1554
  nextAction("edit_request", { why: "Implement CORS and OPTIONS explicitly when cross-origin callers are intended." }),
1539
- retryAction("run402 deploy apply --allow-warnings", "Retry only after the public ingress review is intentional."),
1555
+ retryAction("run402 deploy apply --allow-warning <code>", "Acknowledge only the warning that has requires_confirmation: true, after review."),
1540
1556
  ],
1541
1557
  },
1542
1558
  ROUTE_TARGET_CARRIED_FORWARD: {
package/lib/doctor.mjs CHANGED
@@ -79,7 +79,25 @@ Usage:
79
79
  run402 --wallet <profile> doctor --buzz --buzz-agent <npub-or-hex>
80
80
 
81
81
  Output:
82
- Stdout is a JSON report { ok, checks: [{ name, status, value?, hint?, message? }] }.
82
+ Stdout is a JSON report:
83
+ { ok,
84
+ blocking: [{ check, status, message, hint? }],
85
+ warnings: [{ check, code?, message, hint? }],
86
+ checks: [{ name, status, severity, value?, hint?, message? }] }
87
+ \`ok\` answers ONE question — can this agent ship from here — and is true
88
+ exactly when \`blocking[]\` is empty. Every check carries a \`severity\`:
89
+ blocking would stop a deploy: config_dir / allowance missing or error,
90
+ api_reachable error, tier inactive / frozen / past_due /
91
+ dormant / missing / error, error-severity source_scan findings
92
+ advisory a warning that never stops a deploy: operator_health,
93
+ recovery_posture, gitvault, runtime_staleness, cli_update gaps
94
+ info ok / skipped / unknown
95
+ Agents: branch on \`ok\`; read \`warnings[]\` (one entry per gap) for the
96
+ non-blocking gaps and \`blocking[]\` for what to fix when \`ok\` is false.
97
+ \`--only\` and \`--refresh\` runs have the same shape. The tier check's
98
+ status is a fixed vocabulary — ok | inactive | frozen | past_due | dormant
99
+ | purged | missing | unknown | error — never a tier name; the tier name
100
+ and raw lifecycle ride in value.tier / value.lifecycle.
83
101
  Buzz mode adds { mode: "buzz", contract_id, generated_at, mutation_state,
84
102
  binding, telemetry } and uses check status ok|warning|blocked.
85
103
 
@@ -130,7 +148,10 @@ Checks performed:
130
148
  - Allowance is configured and on a valid rail (x402 / mpp)
131
149
  - Keystore has at least one wallet
132
150
  - API_BASE is reachable (network check via /health)
133
- - Active tier resolves and is not 'past_due' / 'frozen'
151
+ - Active tier resolves and is not 'past_due' / 'frozen' / 'dormant'. A wallet
152
+ whose OWN organization holds no tier but that can reach projects owned by
153
+ another organization (membership or grant) reports status 'missing' as an
154
+ advisory, not a blocker — deploys to those projects are unaffected.
134
155
  - Function runtime staleness: deployed functions running an older platform
135
156
  runtime than the current gateway build (refresh with 'run402 functions
136
157
  rebuild --all'; re-bundles from your stored source, no source change)
@@ -157,10 +178,89 @@ Buzz mode checks (in order):
157
178
  operator, runtime-staleness, and source-tree checks.
158
179
 
159
180
  Exit codes:
160
- 0 — all checks pass
161
- 1 — one or more checks failed (details in output)
181
+ 0 — ok: true (advisory warnings never change the exit code)
182
+ 1 — ok: false (one or more blocking checks; see blocking[])
162
183
  `;
163
184
 
185
+ /**
186
+ * Check statuses that carry no finding at all. Everything else is either the
187
+ * one advisory status (`warning`) or a blocking status — so a NEW status
188
+ * string that slips into a check (a lifecycle name, a tier name, a typo)
189
+ * fails closed as blocking instead of silently passing.
190
+ */
191
+ const INFO_STATUSES = new Set(["ok", "skipped", "unknown"]);
192
+
193
+ /** Non-active organization lifecycle states the gateway can report (`organization-lifecycle` spec); each is its own blocking tier status. */
194
+ const TIER_LIFECYCLE_STATUSES = new Set(["past_due", "frozen", "dormant", "purged"]);
195
+
196
+ /**
197
+ * The severity contract, in one place: `blocking` = would stop this agent
198
+ * from shipping; `advisory` = a gap worth surfacing that never stops a
199
+ * deploy; `info` = nothing to act on. A check may pin its own `severity`
200
+ * explicitly (the tier check does, for the missing-but-can-reach-projects
201
+ * shape); otherwise it is derived from `status`.
202
+ */
203
+ function severityOf(check) {
204
+ if (check.severity === "blocking" || check.severity === "advisory" || check.severity === "info") return check.severity;
205
+ if (INFO_STATUSES.has(check.status)) return "info";
206
+ if (check.status === "warning") return "advisory";
207
+ return "blocking";
208
+ }
209
+
210
+ /** Stable codes for advisory checks that report ONE finding (no `value.gaps`), keyed by check name. */
211
+ const ADVISORY_CODES = {
212
+ cli_update: "CLI_UPDATE_AVAILABLE",
213
+ runtime_staleness: "FUNCTION_RUNTIME_STALE",
214
+ source_scan: "SOURCE_SCAN_WARNINGS",
215
+ tier: "TIER_MISSING_ON_OWN_ORG",
216
+ };
217
+
218
+ /**
219
+ * Fold the per-check report into the agent-facing envelope:
220
+ * `{ ok, blocking[], warnings[], checks[] }`. `warnings[]` carries one entry
221
+ * per gap string of every advisory check (plus one for an advisory check
222
+ * with no gaps); `blocking[]` carries one entry per blocking check and is
223
+ * empty exactly when `ok` is true. Pure — the same fold serves a full run,
224
+ * `--only`, and `--refresh`.
225
+ */
226
+ export function buildDoctorReport(rawChecks) {
227
+ const checks = rawChecks.map((check) => {
228
+ const { name, status, severity: _pinned, ...rest } = check;
229
+ return { name, status, severity: severityOf(check), ...rest };
230
+ });
231
+ const blocking = [];
232
+ const warnings = [];
233
+ for (const check of checks) {
234
+ if (check.severity === "blocking") {
235
+ blocking.push({
236
+ check: check.name,
237
+ status: check.status,
238
+ message: check.message ?? `${check.name}: ${check.status}`,
239
+ ...(check.hint && { hint: check.hint }),
240
+ });
241
+ continue;
242
+ }
243
+ if (check.severity !== "advisory") continue;
244
+ const gaps = Array.isArray(check.value?.gaps) ? check.value.gaps.filter((g) => typeof g === "string" && g.length > 0) : [];
245
+ if (gaps.length > 0) {
246
+ for (const gap of gaps) {
247
+ warnings.push({ check: check.name, message: gap, ...(check.hint && { hint: check.hint }) });
248
+ }
249
+ } else {
250
+ const code = check.code ?? ADVISORY_CODES[check.name];
251
+ const message = check.message ?? check.hint ?? `${check.name}: ${check.status}`;
252
+ warnings.push({
253
+ check: check.name,
254
+ ...(code && { code }),
255
+ message,
256
+ // Only carry `hint` when it adds something the message does not.
257
+ ...(check.hint && check.hint !== message && { hint: check.hint }),
258
+ });
259
+ }
260
+ }
261
+ return { ok: blocking.length === 0, blocking, warnings, checks };
262
+ }
263
+
164
264
  function redactAllowanceForDiagnostics(allowance) {
165
265
  if (!allowance || typeof allowance !== "object") return allowance;
166
266
  const safe = { ...allowance };
@@ -368,36 +468,71 @@ export async function run(sub, args = []) {
368
468
  }
369
469
 
370
470
  // 5. Active tier.
471
+ //
472
+ // `status` is a FIXED vocabulary — ok | inactive | frozen | past_due |
473
+ // dormant | purged | missing | unknown | error — never the tier name and
474
+ // never a raw gateway string: the old derivation fell through to
475
+ // `tierName ?? "missing"`, which could put a tier NAME ("team") in the
476
+ // status slot, and any status outside the ok-allowlist silently flipped
477
+ // the whole report to ok:false while looking benign. The tier name and the
478
+ // raw lifecycle ride in value.tier / value.lifecycle instead.
371
479
  if (wanted("tier")) try {
372
480
  const sdk = getSdk();
373
481
  const tier = await sdk.tier.status();
374
- const tierName = tier?.tier ?? null;
375
- const lifecycle = tier?.organization_lifecycle_state ?? null;
482
+ const tierName = typeof tier?.tier === "string" && tier.tier.length > 0 ? tier.tier : null;
483
+ const lifecycle = typeof tier?.organization_lifecycle_state === "string" && tier.organization_lifecycle_state.length > 0
484
+ ? tier.organization_lifecycle_state
485
+ : null;
376
486
  const active = tier?.active === true;
377
- if (tierName && active && lifecycle === "active") {
487
+ // Projects this wallet can reach through org membership or a grant —
488
+ // the tier read already carries them. A wallet whose OWN org-of-one
489
+ // never bought a tier can still ship into another org's projects, so
490
+ // "missing" is only a blocker when there is nowhere to ship.
491
+ const reachableProjects = Array.isArray(tier?.projects) ? tier.projects.length : 0;
492
+ let status;
493
+ if (lifecycle !== null && lifecycle !== "active") {
494
+ status = TIER_LIFECYCLE_STATUSES.has(lifecycle) ? lifecycle : "inactive";
495
+ } else if (tierName === null) {
496
+ status = "missing";
497
+ } else if (!active) {
498
+ status = "inactive";
499
+ } else if (lifecycle === null) {
500
+ status = "unknown";
501
+ } else {
502
+ status = "ok";
503
+ }
504
+ const value = {
505
+ tier: tierName,
506
+ lifecycle,
507
+ active,
508
+ organization_lifecycle_state: lifecycle,
509
+ lease_expires_at: tier?.lease_expires_at ?? null,
510
+ reachable_projects: reachableProjects,
511
+ };
512
+ if (status === "ok") {
513
+ checks.push({ name: "tier", status, value });
514
+ } else if (status === "missing" && reachableProjects > 0) {
378
515
  checks.push({
379
516
  name: "tier",
380
- status: "ok",
381
- value: { tier: tierName, active, organization_lifecycle_state: lifecycle },
517
+ status,
518
+ severity: "advisory",
519
+ value,
520
+ message: `this wallet's own organization holds no tier, but it can reach ${reachableProjects} project(s) owned by another organization (membership or grant) — deploys to those are unaffected`,
521
+ hint: "Run 'run402 tier set prototype' only if you want to provision projects under this wallet's own organization.",
382
522
  });
383
523
  } else {
384
- const status = lifecycle && lifecycle !== "active"
385
- ? lifecycle
386
- : tierName && !active
387
- ? "inactive"
388
- : tierName && lifecycle === null
389
- ? "unknown"
390
- : tierName ?? "missing";
391
524
  checks.push({
392
525
  name: "tier",
393
526
  status,
394
- value: {
395
- tier: tierName,
396
- active,
397
- organization_lifecycle_state: lifecycle,
398
- lease_expires_at: tier?.lease_expires_at ?? null,
399
- },
400
- hint: lifecycle === null && tierName
527
+ value,
528
+ message: status === "unknown"
529
+ ? "tier resolved, but the organization lifecycle could not be determined"
530
+ : status === "missing"
531
+ ? "no tier on this wallet's organization and no reachable project — nothing can be provisioned or deployed from here"
532
+ : status === "inactive"
533
+ ? `tier '${tierName ?? "(none)"}' is not active${lifecycle && lifecycle !== "active" ? ` (organization lifecycle '${lifecycle}')` : ""}`
534
+ : `organization lifecycle is '${lifecycle}' — the control plane is gated until the tier is reactivated`,
535
+ hint: status === "unknown"
401
536
  ? "Tier resolved, but organization lifecycle could not be determined. Check `run402 tier status` before assuming the account is healthy."
402
537
  : "Run 'run402 tier set prototype' to subscribe, renew, or reactivate the tier.",
403
538
  });
@@ -838,11 +973,10 @@ export async function run(sub, args = []) {
838
973
  }
839
974
  }
840
975
 
841
- // 'warning' counts as ok for exit-code purposesgaps are surfaced in
842
- // output but don't fail the doctor. Only hard 'error' / 'missing' /
843
- // 'empty' fail.
844
- const allOk = checks.every((c) => c.status === "ok" || c.status === "warning" || c.status === "skipped" || c.status === "unknown");
845
-
846
- console.log(JSON.stringify({ ok: allOk, checks }, null, 2));
847
- process.exit(allOk ? 0 : 1);
976
+ // `ok` is structural "no check is blocking" never an allowlist of
977
+ // status strings. Advisory warnings ride in `warnings[]` and never change
978
+ // the exit code; `blocking[]` is what to fix when `ok` is false.
979
+ const report = buildDoctorReport(checks);
980
+ console.log(JSON.stringify(report, null, 2));
981
+ process.exit(report.ok ? 0 : 1);
848
982
  }
package/lib/errors.mjs CHANGED
@@ -108,9 +108,11 @@ Exit codes (the promote gate — only when --fail-on-new is set):
108
108
  from 1. Without --fail-on-new, failures are the usual exit 1.
109
109
 
110
110
  Auth:
111
- The addressed project's own anon_key or service_key. A key for project A
112
- requesting project B's errors gets 403 (never a 404 that leaks existence).
113
- Read-only; never lifecycle-gated.
111
+ The addressed project's own service_key when it is cached locally; otherwise
112
+ your wallet / session / delegate, which the gateway authorizes with
113
+ project.read an org member or teammate agent needs no project key. A key
114
+ for project A requesting project B's errors gets 403 (never a 404 that
115
+ leaks existence). Read-only; never lifecycle-gated.
114
116
 
115
117
  The golden path — gate a promote:
116
118
  run402 deploy promote --project <id> --release <rel>