run402 4.37.0 → 4.38.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.
Files changed (56) hide show
  1. package/README.md +3 -1
  2. package/git-remote-run402.mjs +49 -16
  3. package/lib/command-manifest.mjs +7 -0
  4. package/lib/gitvault-scaffold.mjs +0 -14
  5. package/lib/gitvault.mjs +50 -8
  6. package/lib/org.mjs +70 -0
  7. package/lib/repos.mjs +108 -6
  8. package/package.json +1 -1
  9. package/sdk/dist/index.d.ts +1 -1
  10. package/sdk/dist/index.d.ts.map +1 -1
  11. package/sdk/dist/index.js +1 -1
  12. package/sdk/dist/index.js.map +1 -1
  13. package/sdk/dist/namespaces/gitvault.crypto.d.ts +11 -2
  14. package/sdk/dist/namespaces/gitvault.crypto.d.ts.map +1 -1
  15. package/sdk/dist/namespaces/gitvault.crypto.js +11 -2
  16. package/sdk/dist/namespaces/gitvault.crypto.js.map +1 -1
  17. package/sdk/dist/namespaces/gitvault.d.ts +109 -3
  18. package/sdk/dist/namespaces/gitvault.d.ts.map +1 -1
  19. package/sdk/dist/namespaces/gitvault.js +114 -1
  20. package/sdk/dist/namespaces/gitvault.js.map +1 -1
  21. package/sdk/dist/namespaces/org.d.ts +18 -1
  22. package/sdk/dist/namespaces/org.d.ts.map +1 -1
  23. package/sdk/dist/namespaces/org.js +38 -0
  24. package/sdk/dist/namespaces/org.js.map +1 -1
  25. package/sdk/dist/namespaces/org.types.d.ts +16 -0
  26. package/sdk/dist/namespaces/org.types.d.ts.map +1 -1
  27. package/sdk/dist/namespaces/projects.d.ts +11 -1
  28. package/sdk/dist/namespaces/projects.d.ts.map +1 -1
  29. package/sdk/dist/namespaces/projects.js +16 -0
  30. package/sdk/dist/namespaces/projects.js.map +1 -1
  31. package/sdk/dist/namespaces/projects.types.d.ts +7 -0
  32. package/sdk/dist/namespaces/projects.types.d.ts.map +1 -1
  33. package/sdk/dist/node/gitvault-address.d.ts +89 -0
  34. package/sdk/dist/node/gitvault-address.d.ts.map +1 -0
  35. package/sdk/dist/node/gitvault-address.js +113 -0
  36. package/sdk/dist/node/gitvault-address.js.map +1 -0
  37. package/sdk/dist/node/gitvault-creation-journal.d.ts +81 -9
  38. package/sdk/dist/node/gitvault-creation-journal.d.ts.map +1 -1
  39. package/sdk/dist/node/gitvault-creation-journal.js +93 -17
  40. package/sdk/dist/node/gitvault-creation-journal.js.map +1 -1
  41. package/sdk/dist/node/gitvault-publication.d.ts +13 -0
  42. package/sdk/dist/node/gitvault-publication.d.ts.map +1 -1
  43. package/sdk/dist/node/gitvault-publication.js +1 -0
  44. package/sdk/dist/node/gitvault-publication.js.map +1 -1
  45. package/sdk/dist/node/gitvault-push-to-create.d.ts +75 -0
  46. package/sdk/dist/node/gitvault-push-to-create.d.ts.map +1 -0
  47. package/sdk/dist/node/gitvault-push-to-create.js +132 -0
  48. package/sdk/dist/node/gitvault-push-to-create.js.map +1 -0
  49. package/sdk/dist/node/index.d.ts +6 -2
  50. package/sdk/dist/node/index.d.ts.map +1 -1
  51. package/sdk/dist/node/index.js +8 -1
  52. package/sdk/dist/node/index.js.map +1 -1
  53. package/sdk/dist/scoped.d.ts +2 -1
  54. package/sdk/dist/scoped.d.ts.map +1 -1
  55. package/sdk/dist/scoped.js +3 -0
  56. package/sdk/dist/scoped.js.map +1 -1
package/README.md CHANGED
@@ -65,7 +65,9 @@ run402 repos list --org org_1a2b3c
65
65
  run402 repos delete prj_xyz --force # refuses without --force while the vault holds generations
66
66
  ```
67
67
 
68
- 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. `repos create|list|delete` and `gitvault`'s mutating verbs (`init`, `snapshot`, `policy`, `compact`, `prune`) are CLI/OpenClaw-only by design — no MCP tool exists or will exist for them (one-shot recovery receipts, immutable generations, irreversible delete). See `run402 gitvault --help` and `run402 repos --help` for the full surface, and the CLI reference's `gitvault` / `repos` sections for the terminal-loss statement and the progressive backup warning.
68
+ 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. `repos create|list|delete|name` and `gitvault`'s mutating verbs (`init`, `snapshot`, `policy`, `compact`, `prune`) are CLI/OpenClaw-only by design — no MCP tool exists or will exist for them (one-shot recovery receipts, immutable generations, irreversible delete). See `run402 gitvault --help` and `run402 repos --help` for the full surface, and the CLI reference's `gitvault` / `repos` sections for the terminal-loss statement and the progressive backup warning.
69
+
70
+ **Named addressing (design D6).** `run402 org slug <slug>` (owner-only, small one-time fee) claims an org's globally-unique, address-form slug, after which `run402::<slug>/<name>` addresses any repo under it — `git push` to a name that doesn't exist yet push-to-creates it. `run402 repos name <name> [--project <id>]` claims the per-org-unique `<name>` half explicitly (no fee); `repos create` claims one automatically, best-effort, when the org already has a slug. Also CLI/SDK-only — no MCP tool.
69
71
 
70
72
  ### Allowance
71
73
 
@@ -28,6 +28,21 @@
28
28
  * completes the push. One command, no prior `gitvault init`. `git ls-remote`
29
29
  * / `fetch` stay pure reads and allocate nothing.
30
30
  *
31
+ * NAMED ADDRESSING + PUSH-TO-CREATE (repo-first-onramp task 4, design D6).
32
+ * `run402::<org>/<name>` admits TWO forms in the same slot — id-form
33
+ * (`org_id`/`prj_...`, unchanged: resolved via `r.gitvault.openOrCreate`
34
+ * above) and slug-form (`run402::<org-slug>/<name>`, e.g.
35
+ * `run402::acme/my-notes`) — discriminated by
36
+ * `gitvaultRemoteAddressForm`. A slug-form remote resolves through
37
+ * `r.gitvault.resolveOrCreateAddress`, which ALSO drives push-to-create on a
38
+ * miss (`push` only; `list`/`fetch` pass `allow_create: false`, same "reads
39
+ * never allocate" discipline as the id-form path) and PINS the resolved
40
+ * `repo_id` in this checkout's local git config the first time it resolves
41
+ * (task 4.5) — every later invocation on THIS checkout goes straight to the
42
+ * pinned id, skipping the address resolution round-trip entirely and
43
+ * surviving a later rename of either half. `SLUG_RELEASED` is never
44
+ * auto-followed: it refuses, naming the successor slug.
45
+ *
31
46
  * WHICH REPOSITORY (the fail-closed rule). `process.cwd()` is NOT the
32
47
  * repository. git identifies the repository with `GIT_DIR`, and during
33
48
  * `git clone` cwd is the directory clone was RUN FROM — routinely some other,
@@ -68,7 +83,7 @@
68
83
 
69
84
  import { createInterface } from "node:readline";
70
85
  import { getSdk } from "./lib/sdk.mjs";
71
- import { parseGitvaultRemoteUrl } from "#sdk";
86
+ import { gitvaultRemoteAddressForm, gitvaultSlugReleasedInfo, parseGitvaultRemoteUrl } from "#sdk";
72
87
  import { hardenedGit, resolveGitInvocationRepo } from "#sdk/node";
73
88
 
74
89
  const out = (line) => process.stdout.write(`${line}\n`);
@@ -84,7 +99,11 @@ function oneLine(value) {
84
99
  function describeError(err) {
85
100
  const code = err?.code ?? err?.body?.code ?? null;
86
101
  const message = err?.message ?? err?.body?.message ?? String(err);
87
- return oneLine(code ? `${code}: ${message}` : message);
102
+ // SLUG_RELEASED is never auto-followed but the successor slug (design D6)
103
+ // is exactly the fact a human/agent reading stderr needs to act on it.
104
+ const released = gitvaultSlugReleasedInfo(err);
105
+ const suffix = released?.successor_slug ? ` (renamed to "${released.successor_slug}" — update the remote and re-run)` : "";
106
+ return oneLine(code ? `${code}: ${message}${suffix}` : message);
88
107
  }
89
108
 
90
109
  /**
@@ -132,7 +151,10 @@ async function main(argv) {
132
151
  // `org_id` rides in the parsed address (`run402::<org_id>/<project_id>`),
133
152
  // so it costs nothing extra to carry — it is exactly what D2's lazy
134
153
  // creation needs to allocate an unresolved vault from `runPush` below, with
135
- // no separate lookup.
154
+ // no separate lookup. Only meaningful for an ID-FORM address; a slug-form
155
+ // one resolves through `resolveOrCreateAddress` instead (below), which
156
+ // needs no separate org_id at all — the gateway resolves the slug itself.
157
+ const addressForm = gitvaultRemoteAddressForm(address);
136
158
  const target = { project_id: address.project_id, org_id: address.org_id };
137
159
  let verbosity = 1;
138
160
 
@@ -160,25 +182,42 @@ async function main(argv) {
160
182
  note(`if you meant to restore this vault: git init --bare <dir> && git -C <dir> remote add run402 run402::${address.org_id}/${address.project_id} && git -C <dir> fetch run402 '+refs/heads/*:refs/heads/*'`);
161
183
  }
162
184
 
163
- /** Open the vault lazily — `capabilities` and `option` must never touch the network. */
164
- const openVault = async (repoDir) => (await getSdk().gitvault.open(repoDir ? { ...target, repo_dir: repoDir } : target)).vault;
165
-
166
185
  /** A 404/absent-vault refusal — the "nothing here yet" shape, never a genuine failure to mask. */
167
186
  function isVaultNotFound(err) {
168
187
  return err?.status === 404 || err?.code === "RESOURCE_NOT_FOUND" || err?.code === "ROUTE_NOT_FOUND";
169
188
  }
170
189
 
171
190
  /**
172
- * Open the vault, allocating it first when it does not exist yet (D2). Used
173
- * ONLY by `runPush` `list`/`fetch` stay pure reads and never create
174
- * anything (see `runList`'s own not-found handling below).
191
+ * Open the vault lazily `capabilities` and `option` must never touch the
192
+ * network. Dispatches on the address form (design D6): id-form is
193
+ * BYTE-IDENTICAL to before (`gitvault.open` with `{org_id, project_id}`);
194
+ * slug-form resolves (and, on the first successful resolution, PINS
195
+ * `repo_id` in local git state — task 4.5) through
196
+ * `gitvault.resolveOrCreateAddress` with `allow_create: false` — a read
197
+ * never allocates, same discipline the id-form path already had.
198
+ */
199
+ const openVault = async (repoDir) => {
200
+ if (addressForm === "id") return (await getSdk().gitvault.open(repoDir ? { ...target, repo_dir: repoDir } : target)).vault;
201
+ const result = await getSdk().gitvault.resolveOrCreateAddress({ address, allow_create: false, ...(repoDir ? { repo_dir: repoDir } : {}) });
202
+ return result.handle.vault;
203
+ };
204
+
205
+ /**
206
+ * Open the vault, allocating it first when it does not exist yet (D2), and
207
+ * — for a SLUG-form address whose name does not resolve yet —
208
+ * PUSH-TO-CREATE it (design D6, task 4.4/4.5). Used ONLY by `runPush` —
209
+ * `list`/`fetch` stay pure reads and never create anything (see
210
+ * `runList`'s own not-found handling below).
175
211
  *
176
212
  * Prints the one-shot recovery receipt and the keystore path to stderr the
177
213
  * moment allocation happens, per the client-surface spec: an agent reads
178
214
  * stderr, and the receipt is worth exactly as many copies as get kept.
179
215
  */
180
216
  async function openOrCreateVault(repoDir) {
181
- const result = await getSdk().gitvault.openOrCreate({ ...target, repo_dir: repoDir });
217
+ const result =
218
+ addressForm === "id"
219
+ ? await getSdk().gitvault.openOrCreate({ ...target, repo_dir: repoDir })
220
+ : await getSdk().gitvault.resolveOrCreateAddress({ address, repo_dir: repoDir, allow_create: true });
182
221
  if (!result.found && result.created) {
183
222
  note("");
184
223
  note(`vault ${result.handle.repo_id} allocated (genesis ${result.created.genesis_sha256}) — one-shot recovery receipt, keep many copies:`);
@@ -189,12 +228,6 @@ async function main(argv) {
189
228
  } catch {
190
229
  // Never let a diagnostic line fail a push that already allocated successfully.
191
230
  }
192
- try {
193
- const { GITVAULT_ALLOCATION_POLICY_ADVISORY } = await import("./lib/gitvault-scaffold.mjs");
194
- note(GITVAULT_ALLOCATION_POLICY_ADVISORY);
195
- } catch {
196
- // Same rule: advisory only, never fatal.
197
- }
198
231
  note("");
199
232
  }
200
233
  return result.handle.vault;
@@ -197,6 +197,11 @@ export const COMMAND_MANIFEST = [
197
197
  { path: ["org", "get"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["org_gate1"] },
198
198
  { path: ["org", "rename"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["org_gate1", "--name", "Gate"] },
199
199
  { path: ["org", "payout-wallet"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["org_gate1", "--wallet", "0x1111111111111111111111111111111111111111"] },
200
+ // repo-first-onramp task 4.1 (design D6). `--org` (not a positional org_id
201
+ // like rename/payout-wallet above) goes through resolveOrg's SHAPE
202
+ // validation (a real UUID) — "org_gate1" fails that locally, so this needs
203
+ // the same UUID-shaped fixture `org use` below already established.
204
+ { path: ["org", "slug"], positionals: [p("slug")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["acme", "--org", "11111111-2222-3333-4444-555555555555"] },
200
205
  { path: ["org", "whoami"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: [] },
201
206
  { path: ["org", "audit"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["org_gate1"] },
202
207
  { path: ["org", "use"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["11111111-2222-3333-4444-555555555555"] },
@@ -274,6 +279,8 @@ export const COMMAND_MANIFEST = [
274
279
  { path: ["repos", "create"], positionals: [p("name")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["my-notes"], runStyle: "sub", skipBehavioral: "provisions a project, allocates a vault, and scaffolds a real git remote into cwd" },
275
280
  { path: ["repos", "list"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "cross-references live projects against their live gitvault status, one call per project" },
276
281
  { path: ["repos", "delete"], positionals: [], projectScoped: true, legacyPositionalProject: true, minimalArgs: [], runStyle: "sub", skipBehavioral: "irreversibly deletes a project after reading its live vault generation count" },
282
+ // repo-first-onramp task 4.2 (design D6): explicit address-form name claim.
283
+ { path: ["repos", "name"], positionals: [p("name")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["my-notes"], runStyle: "sub", skipBehavioral: "claims a per-org-unique repo name against a live project" },
277
284
  { path: ["errors"], positionals: [p("fingerprint_id", { required: false })], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "merged" },
278
285
 
279
286
  // ── jobs ─────────────────────────────────────────────────────────────────
@@ -19,20 +19,6 @@
19
19
  import { getSdk } from "./sdk.mjs";
20
20
  import { resolveOwningOrgId } from "./org-context.mjs";
21
21
 
22
- /**
23
- * The interim allocation→policy advisory (repo-first-onramp D3, stated as an
24
- * interim). TODAY the platform sets `gitvault_policy: required` at allocation
25
- * — the decoupling (allocation leaving the policy unset) is design D3's
26
- * gateway half and ships in a later release (task 3.1). Until it does, every
27
- * client-side path that allocates a vault prints this line so the policy flip
28
- * is never a silent side effect of a push or a create. Faithful over flattering:
29
- * documenting the designed future as the present is the breach; this line is
30
- * the truth. DELETE this export (and its four call sites) when the gateway
31
- * decoupling ships.
32
- */
33
- export const GITVAULT_ALLOCATION_POLICY_ADVISORY =
34
- "note: allocation currently sets gitvault_policy to 'required' — from now on a deploy must present a vaulted capture (produced automatically by `run402 deploy apply` on any machine holding this keystore). Opt out with `run402 gitvault policy grandfathered --reason \"<why>\"`. A later release decouples allocation from the deploy gate (design D3).";
35
-
36
22
  /**
37
23
  * @param {object} options
38
24
  * @param {string} [options.repoDir] Working tree to scaffold. Defaults to `process.cwd()`.
package/lib/gitvault.mjs CHANGED
@@ -19,7 +19,6 @@
19
19
  import { readFileSync } from "node:fs";
20
20
  import { resolveProjectId } from "./config.mjs";
21
21
  import { resolveOwningOrgId } from "./org-context.mjs";
22
- import { GITVAULT_ALLOCATION_POLICY_ADVISORY } from "./gitvault-scaffold.mjs";
23
22
  import { getSdk } from "./sdk.mjs";
24
23
  import { reportSdkError, fail } from "./sdk-errors.mjs";
25
24
  import {
@@ -70,8 +69,12 @@ Subcommands:
70
69
  deploy — a vault-only project snapshots for months without one.
71
70
  Against a project with no vault yet, this ALLOCATES one inline
72
71
  (the six-stage creation, same as \`init\`) before publishing — one
73
- command, no prior \`gitvault init\`. The one-shot recovery receipt
74
- and keystore path print to stderr the moment that happens.
72
+ command, no prior \`gitvault init\`. When --repo/--project are
73
+ both omitted and the local run402/origin remote is a slug-form
74
+ address (run402::<org-slug>/<name>), PUSH-TO-CREATES through it
75
+ instead (design D6) — same as pushing that name with \`git\`. The
76
+ one-shot recovery receipt and keystore path print to stderr the
77
+ moment that happens.
75
78
  Before reporting a snapshot as landed the SDK compares finalization
76
79
  receipts against the expected manifest and reads the admitted head
77
80
  back from storage; a 200 alone is never enough. \`push\` is a
@@ -284,7 +287,6 @@ async function init(args) {
284
287
  );
285
288
  if (result.remote) console.error(`remote '${result.remote.name}' -> ${result.remote.url} (${result.remote.reason})`);
286
289
  if (remoteSkipped) console.error(`remote not added: ${remoteSkipped}`);
287
- if (!result.deduplicated) console.error(GITVAULT_ALLOCATION_POLICY_ADVISORY);
288
290
  // The recovery receipt is integrity data, not a secret, and it is worth
289
291
  // exactly as much as the number of copies you keep. It is persisted into
290
292
  // the keystore automatically; say where, because "keep many copies" is
@@ -372,6 +374,15 @@ async function status(args) {
372
374
  if (s.remote) {
373
375
  console.error(`remote '${s.remote.name}': ${s.remote.url}${s.remote.matches ? "" : " ← points at a DIFFERENT project than this status"}`);
374
376
  }
377
+ // The id-pinning state (design D6, task 4.5): a slug-form remote pins
378
+ // repo_id in local git state the first time it resolves; id-form pins
379
+ // nothing (it needs no pin — see resolveGitvaultAddress's doc comment).
380
+ if (s.pinned) {
381
+ console.error(
382
+ `pinned: repo_id ${s.pinned.repo_id}` +
383
+ (s.pinned.resolved_from ? ` (resolved from run402::${s.pinned.resolved_from.org_slug}/${s.pinned.resolved_from.repo_name})` : ""),
384
+ );
385
+ }
375
386
  if (s.refs) {
376
387
  const names = Object.keys(s.refs).sort();
377
388
  console.error(names.length === 0 ? "refs: (none yet)" : `refs (${names.length}):`);
@@ -398,6 +409,33 @@ async function status(args) {
398
409
  * only the dispatched SUBCOMMAND name changed (see `run()` below, where
399
410
  * `gitvault push` survives one release as a deprecation-warning alias).
400
411
  */
412
+ /**
413
+ * D6 (repo-first-onramp task 4): when neither `--repo` nor `--project` was
414
+ * given explicitly, look at the local `run402`/`origin` remote (in that
415
+ * order, mirroring `scaffoldRemote`'s own naming) and, if it is a SLUG-form
416
+ * address (`run402::<org-slug>/<name>`), return the parsed address so
417
+ * `snapshot` can push-to-create through it — the same address-form
418
+ * resolution `git push` drives via the remote helper. `null` for an
419
+ * id-form remote, no remote at all, or an explicit `--repo`/`--project`.
420
+ */
421
+ async function detectSlugFormRemote(a, repoDir) {
422
+ if (flagValue(a, "--repo") != null || flagValue(a, "--project") != null) return null;
423
+ const { hardenedGit } = await import("#sdk/node");
424
+ const { parseGitvaultRemoteUrl, gitvaultRemoteAddressForm } = await import("#sdk");
425
+ for (const name of ["run402", "origin"]) {
426
+ let url;
427
+ try {
428
+ url = (await hardenedGit(repoDir, ["remote", "get-url", name])).text().trim();
429
+ } catch {
430
+ continue;
431
+ }
432
+ if (!url) continue;
433
+ const address = parseGitvaultRemoteUrl(url);
434
+ if (address && gitvaultRemoteAddressForm(address) === "slug") return address;
435
+ }
436
+ return null;
437
+ }
438
+
401
439
  async function snapshot(args) {
402
440
  const a = normalizeArgv(args);
403
441
  const valueFlags = [...COMMON_VALUE_FLAGS, "--message"];
@@ -406,14 +444,19 @@ async function snapshot(args) {
406
444
  min: 0, max: 0, command: "run402 gitvault snapshot", missing: "",
407
445
  });
408
446
  const message = flagValue(a, "--message");
409
- const target = vaultTarget(a);
447
+ const repoDir = process.cwd();
448
+ const address = await detectSlugFormRemote(a, repoDir);
410
449
  // D2: lazily allocate the vault on first push when there is a project to
411
450
  // resolve the owning org from — the same resolution `gitvault init` uses.
412
451
  // `--repo`-only addressing has nothing to create FROM (no project_id), so
413
- // it is skipped there, matching `open()`'s own precedence.
414
- const orgId = target.project_id ? await resolveOwningOrgId(target.project_id) : null;
452
+ // it is skipped there, matching `open()`'s own precedence. Skipped
453
+ // entirely for a slug-form remote (`address` above) that resolves
454
+ // through the address, not a project_id, and needs no separate org_id.
455
+ const target = address ? { repo_dir: repoDir } : vaultTarget(a);
456
+ const orgId = !address && target.project_id ? await resolveOwningOrgId(target.project_id) : null;
415
457
  const opts = {
416
458
  ...target,
459
+ ...(address ? { address } : {}),
417
460
  ...(orgId ? { org_id: orgId } : {}),
418
461
  // The gitvault_commit line is progress, not payload: print it the moment
419
462
  // the snapshot exists, well before the publication round-trips finish, so
@@ -428,7 +471,6 @@ async function snapshot(args) {
428
471
  console.error(`vault allocated (genesis ${created.genesis_sha256}) — one-shot recovery receipt, keep many copies:`);
429
472
  console.error(JSON.stringify(created.recovery_receipt));
430
473
  await printKeystoreLocation();
431
- console.error(GITVAULT_ALLOCATION_POLICY_ADVISORY);
432
474
  console.error("");
433
475
  },
434
476
  };
package/lib/org.mjs CHANGED
@@ -30,6 +30,7 @@ Usage:
30
30
  run402 org get <org_id>
31
31
  run402 org rename <org_id> --name <display_name> (or: --clear to remove the label)
32
32
  run402 org payout-wallet <org_id> --wallet <wallet_address> (or: --clear to remove the explicit default)
33
+ run402 org slug <slug> [--org <org_id>]
33
34
  run402 org whoami
34
35
  run402 org use <org_id>
35
36
  run402 org current
@@ -53,6 +54,9 @@ Subcommands:
53
54
  list Orgs you are a member of
54
55
  get Read one org (label + tier/lease + your role)
55
56
  rename Set or clear an org's display label (owner-only)
57
+ slug Claim or rename the org's globally-unique, address-form slug
58
+ (owner-only). A genesis claim spends a one-time claim fee; a
59
+ rename releases the old slug into a ~90-day cooldown.
56
60
  use Select the current org for this wallet profile
57
61
  current Report the resolved current org and where it came from
58
62
  clear Clear this wallet profile's org selection
@@ -120,6 +124,26 @@ Legacy (still supported):
120
124
 
121
125
  Owner-only + step-up gated. Pass --clear (or an empty display_name) to remove
122
126
  the label. Output includes the updated tier and lease timestamps.
127
+ `,
128
+ slug: `run402 org slug — claim or rename the org's address-form slug
129
+
130
+ Usage:
131
+ run402 org slug <slug> [--org <org_id>] [--idempotency-key <key>]
132
+
133
+ The slug is a globally-unique, claimable, address-form handle for the org
134
+ (repo-first-onramp design D6) — the <org-slug> half of a named repo address
135
+ run402::<org-slug>/<name>. Grammar: lowercase [a-z0-9-], no leading/trailing/
136
+ double hyphen, max 39 chars. Owner-only.
137
+
138
+ A genesis claim (the org had no prior slug) spends a one-time claim fee off
139
+ the org's balance. A rename is free but releases the OLD slug into a ~90-day
140
+ cooldown: it stops resolving, with a typed SLUG_RELEASED refusal naming the
141
+ new slug as successor — there is no redirect, so update every remote and
142
+ address that still names the old one.
143
+
144
+ This is a paid, side-effecting mutation and requires Idempotency-Key; the SDK
145
+ generates one automatically unless --idempotency-key is passed, so a retried
146
+ call after a dropped response can never double-bill.
123
147
  `,
124
148
  "payout-wallet": `run402 org payout-wallet — set or clear the tenant route payout wallet
125
149
 
@@ -413,6 +437,51 @@ async function payoutWallet(args) {
413
437
  }
414
438
  }
415
439
 
440
+ /**
441
+ * `run402 org slug <slug>` — claim or rename the org's address-form slug
442
+ * (repo-first-onramp design D6). Owner-only, and a genesis claim spends a
443
+ * one-time claim fee — this is a PAID, side-effecting mutation, so it
444
+ * requires `Idempotency-Key`; the SDK generates one client-side when
445
+ * `--idempotency-key` is omitted, so a retried call after a dropped response
446
+ * can never double-bill.
447
+ */
448
+ async function slug(args) {
449
+ const a = normalizeArgv(args);
450
+ const valueFlags = ["--org", "--idempotency-key"];
451
+ assertKnownFlags(a, [...valueFlags, "--help", "-h"], valueFlags);
452
+ const [newSlug] = requirePositionalCount(a, valueFlags, {
453
+ min: 1,
454
+ max: 1,
455
+ command: "run402 org slug <slug> [--org <org_id>]",
456
+ missing: "Missing <slug>.",
457
+ });
458
+ const org = await resolveOrg(a, { cmd: "org" });
459
+ if (!org) {
460
+ fail({
461
+ code: "ORG_UNRESOLVED",
462
+ message: "Could not resolve which organization to claim this slug for.",
463
+ hint: "Pass --org <org_id>, or select one first with `run402 org use <id>`.",
464
+ });
465
+ }
466
+ const idempotencyKey = flagValue(a, "--idempotency-key");
467
+ try {
468
+ const result = await getSdk().org(org.orgId).claimSlug(newSlug, idempotencyKey != null ? { idempotencyKey } : {});
469
+ console.log(JSON.stringify(result, null, 2));
470
+ if (result.created) {
471
+ console.error(`slug "${result.slug}" claimed for ${org.orgId} — a one-time claim fee was debited from the org's balance.`);
472
+ } else if (result.previous_slug && result.previous_slug !== result.slug) {
473
+ console.error(
474
+ `org ${org.orgId} renamed from "${result.previous_slug}" to "${result.slug}" — no fee. ` +
475
+ `"${result.previous_slug}" now enters its ~90-day release cooldown: it stops resolving with a typed SLUG_RELEASED refusal (naming "${result.slug}" as the successor), never a redirect. Update every remote and address that still names it.`,
476
+ );
477
+ } else {
478
+ console.error(`"${result.slug}" was already ${org.orgId}'s current slug — nothing changed, no fee.`);
479
+ }
480
+ } catch (err) {
481
+ reportSdkError(err);
482
+ }
483
+ }
484
+
416
485
  async function audit(args) {
417
486
  const a = normalizeArgv(args);
418
487
  const valueFlags = ["--limit", "--after", "--before"];
@@ -630,6 +699,7 @@ export async function run(sub, args) {
630
699
  case "get": await get(args); break;
631
700
  case "rename": await rename(args); break;
632
701
  case "payout-wallet": await payoutWallet(args); break;
702
+ case "slug": await slug(args); break;
633
703
  case "whoami": await whoami(args); break;
634
704
  case "use": await use(args); break;
635
705
  case "current": await current(args); break;
package/lib/repos.mjs CHANGED
@@ -35,12 +35,12 @@
35
35
  import { getSdk } from "./sdk.mjs";
36
36
  import { reportSdkError, fail } from "./sdk-errors.mjs";
37
37
  import { withAutoApprove } from "./operator.mjs";
38
- import { allowanceAuthHeaders, isCoreApiTarget } from "./config.mjs";
38
+ import { allowanceAuthHeaders, isCoreApiTarget, resolveProjectId } from "./config.mjs";
39
39
  import { loadLiveControlPlaneSession } from "../core-dist/control-plane-session.js";
40
40
  import { resolveOrgId, resolveOwningOrgId } from "./org-context.mjs";
41
41
  import { nextAction } from "./next-actions.mjs";
42
42
  import { printKeystoreLocation } from "./gitvault.mjs";
43
- import { GITVAULT_ALLOCATION_POLICY_ADVISORY } from "./gitvault-scaffold.mjs";
43
+ import { gitvaultRemoteUrlForRepo } from "#sdk";
44
44
  import {
45
45
  normalizeArgv,
46
46
  hasHelp,
@@ -57,21 +57,30 @@ Usage:
57
57
  run402 repos create <name> [--org <org_id>] [--dir <path>] [--tier <tier>]
58
58
  run402 repos list [--org <org_id>]
59
59
  run402 repos delete <project_id> [--force]
60
+ run402 repos name <name> [--project <id>]
60
61
 
61
62
  Subcommands:
62
63
  create Provision a project, ALLOCATE its vault (mints key material and a
63
64
  one-shot recovery receipt), and scaffold the run402 remote —
64
65
  origin when free, run402 when taken (D1). No deploy plan, no
65
66
  release, nothing deployed: the vault-only track (design D8), for a
66
- project that only ever hosts encrypted source.
67
+ project that only ever hosts encrypted source. When the owning org
68
+ has a slug (run402 org slug), also claims the project's address-
69
+ form repo name (best-effort — a name collision or missing slug
70
+ never fails the command) and prints the run402::<slug>/<name>
71
+ address (design D6).
67
72
  list The organization's vault-bearing projects — those with an
68
73
  allocated vault, whether or not they have ever deployed. Not
69
- every project in the org; ones with no vault are omitted.
74
+ every project in the org; ones with no vault are omitted. Shows
75
+ the run402::<slug>/<name> address for a repo that has claimed one.
70
76
  delete Delete the project and everything in it (database, functions,
71
77
  subdomains, mailbox, secrets). REFUSES while the vault holds any
72
78
  admitted generation unless --force is passed — this is
73
79
  irreversible and destroys the vault's entire encrypted history
74
80
  along with everything else.
81
+ name Claim or rename the project's per-org-unique, address-form name
82
+ (design D6) — the <name> half of run402::<org-slug>/<name>. No
83
+ fee, unlike the org slug. Same authority as renaming the project.
75
84
 
76
85
  Options:
77
86
  --org <org_id> create/list: the owning organization. create resolves it
@@ -89,6 +98,8 @@ Options:
89
98
  derived from the name)
90
99
  --force delete: proceed even though the vault holds generations
91
100
  that would be permanently and irrecoverably lost
101
+ --project <id> name: project to claim the repo name for (default: the
102
+ active project)
92
103
  --json No-op: stdout is already JSON.
93
104
 
94
105
  There is no separate gitvault price: bytes count against the same
@@ -98,6 +109,7 @@ organization-pooled storage budget every project already has.
98
109
  const CREATE_VALUE_FLAGS = ["--org", "--dir", "--tier", "--idempotency-key"];
99
110
  const LIST_VALUE_FLAGS = ["--org"];
100
111
  const DELETE_VALUE_FLAGS = ["--project"];
112
+ const NAME_VALUE_FLAGS = ["--project"];
101
113
 
102
114
  function validateProjectName(name) {
103
115
  if (name === "") {
@@ -124,6 +136,24 @@ function validateProjectName(name) {
124
136
  }
125
137
  }
126
138
 
139
+ /**
140
+ * Best-effort slugify for the address-form repo name (design D6's grammar:
141
+ * lowercase [a-z0-9-], no leading/trailing/double hyphen, <=63 chars). The
142
+ * free-text project display name (`repos create <name>`'s positional) is
143
+ * NOT already in this charset, so `create` derives a candidate rather than
144
+ * sending the raw name straight to the claim route and failing on the first
145
+ * space or capital letter.
146
+ */
147
+ function slugifyRepoName(name) {
148
+ return name
149
+ .toLowerCase()
150
+ .replace(/[^a-z0-9-]+/g, "-")
151
+ .replace(/-+/g, "-")
152
+ .replace(/^-+|-+$/g, "")
153
+ .slice(0, 63)
154
+ .replace(/-+$/g, "");
155
+ }
156
+
127
157
  async function create(args) {
128
158
  const a = normalizeArgv(args);
129
159
  assertKnownFlags(a, [...CREATE_VALUE_FLAGS, "--help", "-h"], CREATE_VALUE_FLAGS);
@@ -178,9 +208,29 @@ async function create(args) {
178
208
  project_id: provisioned.project_id,
179
209
  repo_dir: dir,
180
210
  });
211
+ // Best-effort address-form name claim (design D6): when the owning org
212
+ // has a slug, name this repo so it is reachable as
213
+ // run402::<slug>/<name> too — never fails `create` itself. A collision,
214
+ // a missing slug, or any other refusal just means no address this time;
215
+ // `run402 repos name <name>` claims it explicitly later.
216
+ let address = null;
217
+ try {
218
+ const orgRecord = await getSdk().org(effectiveOrgId).get();
219
+ if (orgRecord.slug) {
220
+ const candidate = slugifyRepoName(name);
221
+ if (candidate) {
222
+ const named = await getSdk().projects.setRepoName(provisioned.project_id, candidate);
223
+ address = gitvaultRemoteUrlForRepo(orgRecord.slug, named.repo_name);
224
+ }
225
+ }
226
+ } catch (err) {
227
+ console.error(`repo name not claimed (non-fatal): ${err?.message ?? String(err)}`);
228
+ }
229
+
181
230
  const out = {
182
231
  project_id: provisioned.project_id,
183
232
  repo_id: vault.repo_id,
233
+ address,
184
234
  remote: vault.remote,
185
235
  deduplicated: vault.deduplicated,
186
236
  genesis_sha256: vault.genesis_sha256,
@@ -193,11 +243,11 @@ async function create(args) {
193
243
  `project ${provisioned.project_id} provisioned; vault ${vault.repo_id} ` +
194
244
  (vault.deduplicated ? "already existed — nothing was re-allocated" : `allocated (genesis ${vault.genesis_sha256})`),
195
245
  );
246
+ if (address) console.error(`address: ${address}`);
196
247
  if (vault.remote) console.error(`remote '${vault.remote.name}' -> ${vault.remote.url} (${vault.remote.reason})`);
197
248
  console.error("");
198
249
  console.error(vault.terminal_loss_statement);
199
250
  await printKeystoreLocation();
200
- if (!vault.deduplicated) console.error(GITVAULT_ALLOCATION_POLICY_ADVISORY);
201
251
  console.error("");
202
252
  console.error("nothing was deployed — this is a vault-only repo. Deploy later with `run402 deploy apply`, or never.");
203
253
  } catch (err) {
@@ -246,8 +296,21 @@ async function list(args) {
246
296
  genesis_admitted_at: status.vault.genesis_admitted_at,
247
297
  });
248
298
  }
249
- console.log(JSON.stringify({ org_id: orgId, repos }, null, 2));
299
+ // The org's slug, when claimed (design D6) — printed so a human/agent can
300
+ // construct run402::<slug>/<name> addresses by hand. There is deliberately
301
+ // no per-project `address` field here yet: the gateway has no bulk (or
302
+ // even single) READ for a project's claimed repo_name today, only the
303
+ // WRITE route (`POST /projects/v1/:id/repo-name`) — adding one is gateway
304
+ // work, out of scope for this client-only change (see the final report).
305
+ let orgSlug = null;
306
+ try {
307
+ orgSlug = (await getSdk().org(orgId).get()).slug;
308
+ } catch {
309
+ // Best-effort — `list` must not fail over an org-slug lookup.
310
+ }
311
+ console.log(JSON.stringify({ org_id: orgId, org_slug: orgSlug, repos }, null, 2));
250
312
  console.error(`${repos.length} vault-bearing project(s) of ${projects.length} total in this organization`);
313
+ if (orgSlug) console.error(`org slug: ${orgSlug} — a repo with a claimed address-form name is reachable at run402::${orgSlug}/<name>`);
251
314
  }
252
315
 
253
316
  async function del(args) {
@@ -299,6 +362,41 @@ async function del(args) {
299
362
  }
300
363
  }
301
364
 
365
+ /**
366
+ * `run402 repos name <name> [--project <id>]` — the explicit address-form
367
+ * claim (design D6, task 4.2): a project gets its per-org-unique `<name>`
368
+ * half of `run402::<org-slug>/<name>` either at push-to-create time or here.
369
+ */
370
+ async function name(args) {
371
+ const a = normalizeArgv(args);
372
+ assertKnownFlags(a, [...NAME_VALUE_FLAGS, "--help", "-h"], NAME_VALUE_FLAGS);
373
+ const [repoName] = requirePositionalCount(a, NAME_VALUE_FLAGS, {
374
+ min: 1, max: 1, command: "run402 repos name <name> [--project <id>]", missing: "run402 repos name <name>: a name is required",
375
+ });
376
+ const projectId = resolveProjectId(flagValue(a, "--project"));
377
+ try {
378
+ const result = await getSdk().projects.setRepoName(projectId, repoName);
379
+ let address = null;
380
+ try {
381
+ const owningOrg = await resolveOwningOrgId(projectId);
382
+ const orgSlug = owningOrg ? (await getSdk().org(owningOrg).get()).slug : null;
383
+ if (orgSlug) address = gitvaultRemoteUrlForRepo(orgSlug, result.repo_name);
384
+ } catch {
385
+ // The claim itself already succeeded — a failed address-preview lookup is never fatal.
386
+ }
387
+ console.log(JSON.stringify({ ...result, address }, null, 2));
388
+ console.error(
389
+ result.previous_repo_name && result.previous_repo_name !== result.repo_name
390
+ ? `renamed from "${result.previous_repo_name}" to "${result.repo_name}"`
391
+ : `name "${result.repo_name}" claimed for ${projectId}`,
392
+ );
393
+ if (address) console.error(`address: ${address}`);
394
+ else console.error("this org has no slug yet — claim one with `run402 org slug <slug>` to get a full run402::<slug>/<name> address");
395
+ } catch (err) {
396
+ reportSdkError(err);
397
+ }
398
+ }
399
+
302
400
  export async function run(sub, args) {
303
401
  const argv = Array.isArray(args) ? args : [];
304
402
  if (!sub || hasHelp([sub, ...argv])) {
@@ -318,6 +416,10 @@ export async function run(sub, args) {
318
416
  await del(argv);
319
417
  break;
320
418
  }
419
+ case "name": {
420
+ await name(argv);
421
+ break;
422
+ }
321
423
  default:
322
424
  failUnknownSubcommand("repos", sub, {
323
425
  hint: "Run `run402 repos --help` for usage.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "4.37.0",
3
+ "version": "4.38.0",
4
4
  "description": "CLI for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -363,6 +363,6 @@ export type * from "./namespaces/wallets.js";
363
363
  export * from "./namespaces/gitvault.crypto.js";
364
364
  export type * from "./namespaces/gitvault.crypto.js";
365
365
  export type * from "./namespaces/gitvault.types.js";
366
- export { Gitvault, gitvaultRemoteUrl, parseGitvaultRemoteUrl, GITVAULT_LOSS_WARNING_THRESHOLDS, gitvaultLossWarningTrip, gitvaultLossWarningTripped, gitvaultLossWarningMessage, } from "./namespaces/gitvault.js";
366
+ export { Gitvault, gitvaultRemoteUrl, gitvaultRemoteUrlForRepo, parseGitvaultRemoteUrl, gitvaultRemoteAddressForm, gitvaultSlugReleasedInfo, GITVAULT_LOSS_WARNING_THRESHOLDS, gitvaultLossWarningTrip, gitvaultLossWarningTripped, gitvaultLossWarningMessage, } from "./namespaces/gitvault.js";
367
367
  export type * from "./namespaces/gitvault.js";
368
368
  //# sourceMappingURL=index.d.ts.map