githolon 0.17.0 → 0.18.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 (2) hide show
  1. package/dist/cli.mjs +492 -210
  2. package/package.json +3 -2
package/dist/cli.mjs CHANGED
@@ -9,14 +9,172 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
+ // src/governance.ts
13
+ var governance_exports = {};
14
+ __export(governance_exports, {
15
+ grantOrRevoke: () => grantOrRevoke,
16
+ keyEnroll: () => keyEnroll,
17
+ keyShow: () => keyShow,
18
+ resolveGovernancePrincipal: () => resolveGovernancePrincipal,
19
+ signAndPostGovernance: () => signAndPostGovernance
20
+ });
21
+ import { connect } from "@githolon/client";
22
+ async function connectGovernance(cloud, parent) {
23
+ const authToken = await sessionToken().catch(() => void 0);
24
+ const clientId = `githolon-cli-${Date.now().toString(16)}`;
25
+ return await connect({
26
+ cloud,
27
+ workspace: parent,
28
+ clientId,
29
+ ...authToken !== void 0 ? { authToken } : {}
30
+ });
31
+ }
32
+ async function ensureDeviceKey(holon, principal) {
33
+ const existing = getDeviceKey(principal);
34
+ if (existing !== void 0) return existing;
35
+ const minted = await holon.authorKeygen();
36
+ if (typeof minted.secret !== "string" || typeof minted.public !== "string") {
37
+ throw new Error("authorKeygen did not return a {secret, public} device key");
38
+ }
39
+ const payload = await holon.enrollSignerPayload({ principal, publicKey: minted.public });
40
+ const key = { secret: minted.secret, public: minted.public, keyHash: payload.keyHash };
41
+ setDeviceKey(principal, key);
42
+ return key;
43
+ }
44
+ function resolveGovernancePrincipal(opts) {
45
+ const principal = opts.principal ?? currentPrincipal();
46
+ if (principal === void 0) {
47
+ throw new Error(
48
+ "signed governance needs a principal \u2014 githolon login --agent (self-onboarding), or pass --principal <your-auth-uid>"
49
+ );
50
+ }
51
+ return principal;
52
+ }
53
+ async function signAndPostGovernance(parent, directiveId, payload, opts) {
54
+ const cloud = cloudBase(opts.cloud);
55
+ const principal = resolveGovernancePrincipal(opts);
56
+ const holon = await connectGovernance(cloud, parent);
57
+ const device = await ensureDeviceKey(holon, principal);
58
+ const sealed = await holon.signGovernanceOffer({
59
+ directiveId,
60
+ payload,
61
+ authorSecret: device.secret,
62
+ actor: `user:${principal}`
63
+ });
64
+ const verdict = await holon.postGovernanceOffer({ directiveId, intentBytes: sealed.intentBytes, ws: parent });
65
+ return { ...verdict, principal };
66
+ }
67
+ async function grantOrRevoke(verb, kind, subject, opts) {
68
+ const parent = opts.parent ?? "root";
69
+ const now = Date.now();
70
+ let directiveId;
71
+ let payload;
72
+ const granter = (() => {
73
+ try {
74
+ return resolveGovernancePrincipal(opts);
75
+ } catch (e) {
76
+ err(e.message);
77
+ return void 0;
78
+ }
79
+ })();
80
+ if (granter === void 0) return 1;
81
+ const attribution = verb === "grant" ? { grantedBy: granter, grantedAt: now } : { revokedBy: granter, revokedAt: now };
82
+ if (kind === "admin" || kind === "creation") {
83
+ directiveId = (verb === "grant" ? "grant" : "revoke") + (kind === "admin" ? "Admin" : "Creation");
84
+ payload = { principal: subject, ...verb === "grant" ? { label: opts.label ?? `${kind} via githolon` } : {}, ...attribution };
85
+ } else if (kind === "quota" && verb === "grant") {
86
+ if (typeof opts.max !== "number" || !Number.isInteger(opts.max) || opts.max <= 0) {
87
+ err("grant quota needs --max <positive int> (the principal's workspace allowance)");
88
+ return 1;
89
+ }
90
+ directiveId = "grantQuota";
91
+ payload = { principal: subject, maxWorkspaces: opts.max, ...attribution };
92
+ } else {
93
+ err(`unsupported: githolon ${verb} ${kind} \u2014 expected ${verb} <admin|creation${verb === "grant" ? "|quota" : ""}> <principal>`);
94
+ return 1;
95
+ }
96
+ let v;
97
+ try {
98
+ v = await signAndPostGovernance(parent, directiveId, payload, opts);
99
+ } catch (e) {
100
+ err(e.message);
101
+ return 1;
102
+ }
103
+ if (!v.ok) {
104
+ err(`${directiveId} refused (${v.status}): ${typeof v.body === "string" ? v.body : JSON.stringify(v.body)}`);
105
+ return 1;
106
+ }
107
+ out(`\u2713 ${directiveId} ${subject} via signed governance offer to ${parent}`);
108
+ return 0;
109
+ }
110
+ async function keyEnroll(opts) {
111
+ const cloud = cloudBase(opts.cloud);
112
+ const principal = opts.principal ?? currentPrincipal();
113
+ if (principal === void 0) {
114
+ err("enroll needs a principal \u2014 githolon login --agent first (or --principal <uid>)");
115
+ return 1;
116
+ }
117
+ const parent = opts.parent ?? "root";
118
+ let holon;
119
+ try {
120
+ holon = await connectGovernance(cloud, parent);
121
+ } catch (e) {
122
+ err(`could not connect to ${parent} on ${cloud} to enroll: ${e.message}`);
123
+ return 1;
124
+ }
125
+ const device = await ensureDeviceKey(holon, principal);
126
+ const enrollPayload = await holon.enrollSignerPayload({ principal, publicKey: device.public });
127
+ const sealed = await holon.signGovernanceOffer({
128
+ directiveId: "enrollSigner",
129
+ payload: enrollPayload,
130
+ authorSecret: device.secret,
131
+ actor: `user:${principal}`
132
+ });
133
+ const v = await holon.postGovernanceOffer({ directiveId: "enrollSigner", intentBytes: sealed.intentBytes, ws: parent });
134
+ if (!v.ok) {
135
+ err(`enrollSigner refused (${v.status}): ${typeof v.body === "string" ? v.body : JSON.stringify(v.body)}`);
136
+ return 1;
137
+ }
138
+ out(`\u2713 device key enrolled for ${principal} at ${parent} on ${cloud}`);
139
+ out(` keyHash ${device.keyHash.slice(0, 16)}\u2026 (secret stays local; signed governance is now warranted)`);
140
+ return 0;
141
+ }
142
+ async function keyShow(opts) {
143
+ const principal = opts.principal ?? currentPrincipal();
144
+ if (principal === void 0) {
145
+ out("no principal on file \u2014 githolon login --agent, or pass --principal <uid>");
146
+ return 0;
147
+ }
148
+ const device = getDeviceKey(principal);
149
+ if (device === void 0) {
150
+ out(`no signing device key for ${principal} yet \u2014 githolon key enroll mints + enrolls one`);
151
+ return 0;
152
+ }
153
+ out(`device key for ${principal}:`);
154
+ out(` public ${device.public}`);
155
+ out(` keyHash ${device.keyHash} (the on-chain key:<hash> leaf; secret stays local)`);
156
+ return 0;
157
+ }
158
+ var out, err;
159
+ var init_governance = __esm({
160
+ "src/governance.ts"() {
161
+ "use strict";
162
+ init_cloud();
163
+ out = (s) => void process.stdout.write(s + "\n");
164
+ err = (s) => void process.stderr.write("error: " + s + "\n");
165
+ }
166
+ });
167
+
12
168
  // src/cloud.ts
13
169
  var cloud_exports = {};
14
170
  __export(cloud_exports, {
15
171
  cloudBase: () => cloudBase,
16
172
  configDir: () => configDir,
173
+ currentPrincipal: () => currentPrincipal,
17
174
  deploy: () => deploy,
18
175
  describeLawMove: () => describeLawMove,
19
176
  discoverDeployJson: () => discoverDeployJson,
177
+ getDeviceKey: () => getDeviceKey,
20
178
  getSecret: () => getSecret,
21
179
  loadCreds: () => loadCreds,
22
180
  login: () => login,
@@ -25,10 +183,12 @@ __export(cloud_exports, {
25
183
  secretRotate: () => secretRotate,
26
184
  secretSet: () => secretSet,
27
185
  sessionToken: () => sessionToken,
186
+ setDeviceKey: () => setDeviceKey,
28
187
  setPrincipal: () => setPrincipal,
29
188
  setSecret: () => setSecret,
30
189
  whoami: () => whoami,
31
190
  wsCreate: () => wsCreate,
191
+ wsReclaim: () => wsReclaim,
32
192
  wsRetire: () => wsRetire,
33
193
  wsStatus: () => wsStatus
34
194
  });
@@ -72,6 +232,18 @@ function setPrincipal(principal) {
72
232
  c.principal = principal;
73
233
  saveCreds(c);
74
234
  }
235
+ function currentPrincipal() {
236
+ const c = loadCreds();
237
+ return c.session?.uid ?? c.principal;
238
+ }
239
+ function getDeviceKey(principal) {
240
+ return loadCreds().devices?.[principal];
241
+ }
242
+ function setDeviceKey(principal, key) {
243
+ const c = loadCreds();
244
+ (c.devices ??= {})[principal] = key;
245
+ saveCreds(c);
246
+ }
75
247
  function principalHeaders(principal) {
76
248
  return /^[\w-]+\.[\w-]+\.[\w-]+$/.test(principal) ? { "x-nomos-auth": principal } : { "x-nomos-principal": principal };
77
249
  }
@@ -133,7 +305,7 @@ function discoverDeployJson(cwd, explicit) {
133
305
  }
134
306
  async function login(opts) {
135
307
  if (opts.agent !== true && opts.token === void 0) {
136
- err(
308
+ err2(
137
309
  "browser login is not wired up yet \u2014 use one of:\n githolon login --agent anonymous sign-in (verified identity, linkable later)\n githolon login --token <refresh> adopt an existing captain app session"
138
310
  );
139
311
  return 1;
@@ -141,43 +313,67 @@ async function login(opts) {
141
313
  const d = opts.agent === true ? await gotrue("/auth/v1/signup", "{}") : await gotrue("/auth/v1/token?grant_type=refresh_token", JSON.stringify({ refresh_token: opts.token }));
142
314
  const s = sessionFromGotrue(d);
143
315
  if (s === void 0) {
144
- err(`login failed: ${d.error_description ?? d.error ?? d.msg ?? JSON.stringify(d)}`);
316
+ err2(`login failed: ${d.error_description ?? d.error ?? d.msg ?? JSON.stringify(d)}`);
145
317
  return 1;
146
318
  }
147
319
  saveSession(s);
148
- out(`\u2713 logged in${s.anonymous ? " (anonymous agent identity \u2014 linkable to a full account later)" : ""}`);
149
- out(` principal uid ${s.uid}`);
150
- out(` session saved \u2192 ${credsPath()} (auto-refreshed; births now ride x-nomos-auth)`);
320
+ out2(`\u2713 logged in${s.anonymous ? " (anonymous agent identity \u2014 linkable to a full account later)" : ""}`);
321
+ out2(` principal uid ${s.uid}`);
322
+ out2(` session saved \u2192 ${credsPath()} (auto-refreshed; births now ride x-nomos-auth)`);
151
323
  return 0;
152
324
  }
153
325
  async function whoami() {
154
326
  const c = loadCreds();
155
327
  if (c.session !== void 0) {
156
- out(`session: ${c.session.anonymous ? "anonymous agent" : "account"} uid ${c.session.uid} (auth ${c.session.url})`);
328
+ out2(`session: ${c.session.anonymous ? "anonymous agent" : "account"} uid ${c.session.uid} (auth ${c.session.url})`);
157
329
  return 0;
158
330
  }
159
331
  if (c.principal !== void 0) {
160
- out(`bare principal (transitional, unverified): ${c.principal}`);
332
+ out2(`bare principal (transitional, unverified): ${c.principal}`);
161
333
  return 0;
162
334
  }
163
- out("not logged in \u2014 githolon login --agent, or pass --principal <uid> per birth");
335
+ out2("not logged in \u2014 githolon login --agent, or pass --principal <uid> per birth");
164
336
  return 0;
165
337
  }
166
338
  async function logout() {
167
339
  const c = loadCreds();
168
340
  delete c.session;
169
341
  saveCreds(c);
170
- out("\u2713 session cleared (stored workspace secrets kept)");
342
+ out2("\u2713 session cleared (stored workspace secrets kept)");
171
343
  return 0;
172
344
  }
173
345
  async function wsCreate(name, opts) {
174
346
  const cloud = cloudBase(opts.cloud);
175
- let principal = opts.principal ?? await sessionToken().catch((e) => (err(e.message), void 0)) ?? loadCreds().principal;
347
+ if (opts.via !== void 0) {
348
+ const { signAndPostGovernance: signAndPostGovernance2, resolveGovernancePrincipal: resolveGovernancePrincipal2 } = await Promise.resolve().then(() => (init_governance(), governance_exports));
349
+ let v;
350
+ try {
351
+ const principal2 = resolveGovernancePrincipal2(opts);
352
+ v = await signAndPostGovernance2(
353
+ opts.via,
354
+ "createWorkspace",
355
+ { name, tier: "edge", parent: opts.via === "root" ? "" : opts.via, principal: principal2, requestedBy: principal2, requestedAt: Date.now() },
356
+ opts
357
+ );
358
+ } catch (e) {
359
+ err2(e.message);
360
+ return 1;
361
+ }
362
+ if (!v.ok) {
363
+ err2(`createWorkspace '${name}' refused (${v.status}) at ${opts.via}: ${typeof v.body === "string" ? v.body : JSON.stringify(v.body)}`);
364
+ return 1;
365
+ }
366
+ out2(`\u2713 createWorkspace '${name}' requested via signed governance offer to ${opts.via} on ${cloud}`);
367
+ out2(` the worker provisions it on first touch; deploy is open (kernel-gated)`);
368
+ out2(` (want the direct birth instead? drop --via: githolon ws create ${name})`);
369
+ return 0;
370
+ }
371
+ let principal = opts.principal ?? await sessionToken().catch((e) => (err2(e.message), void 0)) ?? loadCreds().principal;
176
372
  if (principal === void 0) {
177
373
  const d2 = await gotrue("/auth/v1/signup", "{}");
178
374
  const s = sessionFromGotrue(d2);
179
375
  if (s === void 0) {
180
- err(
376
+ err2(
181
377
  `a birth needs a principal, and automatic agent login failed: ${d2.error_description ?? d2.error ?? d2.msg ?? JSON.stringify(d2)}
182
378
  githolon login --agent (self-onboarding), githolon login --token <refresh>,
183
379
  or pass --principal <your-auth-uid or access token> (saved as the default)`
@@ -185,9 +381,9 @@ async function wsCreate(name, opts) {
185
381
  return 1;
186
382
  }
187
383
  saveSession(s);
188
- out(`no principal on file \u2014 self-onboarded an anonymous agent identity (githolon login --agent)`);
189
- out(` principal uid ${s.uid} (linkable to a full account later)`);
190
- out(` session saved \u2192 ${credsPath()} (auto-refreshed; births ride x-nomos-auth)`);
384
+ out2(`no principal on file \u2014 self-onboarded an anonymous agent identity (githolon login --agent)`);
385
+ out2(` principal uid ${s.uid} (linkable to a full account later)`);
386
+ out2(` session saved \u2192 ${credsPath()} (auto-refreshed; births ride x-nomos-auth)`);
191
387
  principal = s.accessToken;
192
388
  }
193
389
  const r = await fetch(`${cloud}/v2/workspaces/${name}`, {
@@ -196,20 +392,12 @@ async function wsCreate(name, opts) {
196
392
  });
197
393
  const d = await r.json().catch(() => void 0);
198
394
  if (!r.ok || d?.ok !== true) {
199
- err(`create '${name}' refused (${r.status}): ${d ? JSON.stringify(d) : "no body"}`);
395
+ err2(`create '${name}' refused (${r.status}): ${d ? JSON.stringify(d) : "no body"}`);
200
396
  return 1;
201
397
  }
202
398
  if (opts.principal !== void 0) setPrincipal(opts.principal);
203
- if (typeof d.workspaceSecret === "string") {
204
- setSecret(cloud, name, d.workspaceSecret);
205
- out(`\u2713 workspace ${name} created on ${cloud}`);
206
- out(`\u2713 workspaceSecret saved \u2192 ${credsPath()} (githolon deploy uses it automatically)`);
207
- } else {
208
- out(`\u2713 workspace ${name} already exists on ${cloud} (no new secret issued)`);
209
- if (getSecret(cloud, name) === void 0) {
210
- out(` no stored secret for it here \u2014 if you own it: githolon secret set ${name} <secret>`);
211
- }
212
- }
399
+ out2(`\u2713 workspace ${name} created on ${cloud}`);
400
+ out2(` deploy is open (kernel-gated): githolon deploy ${name}`);
213
401
  return 0;
214
402
  }
215
403
  async function wsStatus(name, opts) {
@@ -217,39 +405,61 @@ async function wsStatus(name, opts) {
217
405
  const r = await fetch(`${cloud}/v2/workspaces/${name}`);
218
406
  const text = await r.text();
219
407
  if (!r.ok) {
220
- err(`status '${name}' (${r.status}): ${text}`);
408
+ err2(`status '${name}' (${r.status}): ${text}`);
221
409
  return 1;
222
410
  }
223
- out(text);
411
+ out2(text);
224
412
  return 0;
225
413
  }
226
414
  async function wsRetire(ws, opts) {
227
415
  const cloud = cloudBase(opts.cloud);
228
- const secret = getSecret(cloud, ws);
229
- if (secret === void 0) {
230
- err(
231
- `no stored secret for ${ws} on ${cloud} \u2014 retirement is owner-gated
232
- githolon secret set ${ws} <secret> (import the workspaceSecret you hold)`
416
+ const { signAndPostGovernance: signAndPostGovernance2, resolveGovernancePrincipal: resolveGovernancePrincipal2 } = await Promise.resolve().then(() => (init_governance(), governance_exports));
417
+ const parent = opts.parent ?? "root";
418
+ let v;
419
+ try {
420
+ const principal = resolveGovernancePrincipal2(opts);
421
+ v = await signAndPostGovernance2(
422
+ parent,
423
+ "retireWorkspace",
424
+ { workspaceId: ws, retiredBy: principal, retiredAt: Date.now() },
425
+ opts
233
426
  );
427
+ } catch (e) {
428
+ err2(e.message);
234
429
  return 1;
235
430
  }
236
- const r = await fetch(`${cloud}/v2/workspaces/${ws}/retire`, {
237
- method: "POST",
238
- headers: { authorization: `Bearer ${secret}` }
239
- });
240
- const d = await r.json().catch(() => void 0);
241
- if (r.status === 401) {
242
- err(`retire refused (401) \u2014 the stored secret for ${ws} is not the workspace's. githolon secret set ${ws} <secret>`);
431
+ if (!v.ok) {
432
+ err2(`retire '${ws}' refused (${v.status}): ${typeof v.body === "string" ? v.body : JSON.stringify(v.body)}`);
243
433
  return 1;
244
434
  }
245
- if (!r.ok || d?.ok !== true) {
246
- err(`retire '${ws}' failed (${r.status}): ${d ? JSON.stringify(d) : "no body"}`);
435
+ out2(`\u2713 workspace ${ws} retired via signed governance offer to ${parent} on ${cloud}`);
436
+ out2(" nothing was deleted \u2014 the ledger and its data are retained per the Nomos Pre-Release License");
437
+ out2(" it no longer counts toward your workspace allowance; reads keep serving, deploys/pushes refuse");
438
+ return 0;
439
+ }
440
+ async function wsReclaim(ws, opts) {
441
+ const cloud = cloudBase(opts.cloud);
442
+ const { signAndPostGovernance: signAndPostGovernance2, resolveGovernancePrincipal: resolveGovernancePrincipal2 } = await Promise.resolve().then(() => (init_governance(), governance_exports));
443
+ const parent = opts.parent ?? "root";
444
+ let v;
445
+ try {
446
+ const principal = resolveGovernancePrincipal2(opts);
447
+ v = await signAndPostGovernance2(
448
+ parent,
449
+ "reclaimWorkspace",
450
+ { workspaceId: ws, reclaimedBy: principal, reclaimedAt: Date.now() },
451
+ opts
452
+ );
453
+ } catch (e) {
454
+ err2(e.message);
247
455
  return 1;
248
456
  }
249
- out(`\u2713 workspace ${ws} retired on ${cloud}${d.alreadyRetired === true ? " (it was already retired)" : ""}`);
250
- out(" nothing was deleted \u2014 the ledger and its data are retained per the Nomos Pre-Release License");
251
- out(" it no longer counts toward your workspace allowance; reads keep serving, deploys/pushes refuse");
252
- out(" to un-retire: contact the operator (an admin restores the record via root's governance offer, POST /v2/workspaces/root/createWorkspace)");
457
+ if (!v.ok) {
458
+ err2(`reclaim '${ws}' refused (${v.status}): ${typeof v.body === "string" ? v.body : JSON.stringify(v.body)}`);
459
+ return 1;
460
+ }
461
+ out2(`\u2713 workspace ${ws} reclaimed via signed governance offer to ${parent} on ${cloud}`);
462
+ out2(" the custodian drops the ledger bytes on seeing this fact folded; the reclaimed FACT is append-only");
253
463
  return 0;
254
464
  }
255
465
  async function deploy(ws, opts) {
@@ -258,37 +468,24 @@ async function deploy(ws, opts) {
258
468
  try {
259
469
  file = discoverDeployJson(process.cwd(), opts.file);
260
470
  } catch (e) {
261
- err(e.message);
262
- return 1;
263
- }
264
- const secret = getSecret(cloud, ws);
265
- if (secret === void 0) {
266
- err(
267
- `no stored secret for ${ws} on ${cloud}
268
- githolon ws create ${ws} (a fresh workspace saves its secret here)
269
- githolon secret set ${ws} <secret> (import one you already hold)`
270
- );
471
+ err2(e.message);
271
472
  return 1;
272
473
  }
273
474
  const before = await fetch(`${cloud}/v2/workspaces/${ws}/domains`).then((br) => br.json()).then((bd) => bd.ok === true ? bd.domains ?? [] : []).catch(() => []);
274
475
  const r = await fetch(`${cloud}/v2/workspaces/${ws}/domains`, {
275
476
  method: "POST",
276
- headers: { "content-type": "application/json", authorization: `Bearer ${secret}` },
477
+ headers: { "content-type": "application/json" },
277
478
  body: readFileSync(file, "utf8")
278
479
  });
279
480
  const d = await r.json().catch(() => void 0);
280
- if (r.status === 401) {
281
- err(`deploy refused (401) \u2014 the stored secret for ${ws} is not the workspace's. githolon secret set ${ws} <secret>`);
282
- return 1;
283
- }
284
481
  if (!r.ok || d?.ok !== true) {
285
- err(`deploy '${ws}' failed (${r.status}): ${d ? JSON.stringify(d) : "no body"}`);
482
+ err2(`deploy '${ws}' failed (${r.status}): ${d ? JSON.stringify(d) : "no body"}`);
286
483
  return 1;
287
484
  }
288
485
  const phase = d.installation?.[0]?.data?.["status.phase"];
289
- out(`\u2713 deployed ${file.split("/").pop()} \u2192 ${ws}${typeof phase === "string" ? ` (${phase})` : ""}`);
486
+ out2(`\u2713 deployed ${file.split("/").pop()} \u2192 ${ws}${typeof phase === "string" ? ` (${phase})` : ""}`);
290
487
  if (typeof d.domainHash === "string") {
291
- out(` law ${describeLawMove(before, d.domainHash)}`);
488
+ out2(` law ${describeLawMove(before, d.domainHash)}`);
292
489
  }
293
490
  return 0;
294
491
  }
@@ -300,30 +497,19 @@ function describeLawMove(before, newHash) {
300
497
  }
301
498
  async function secretSet(ws, secret, opts) {
302
499
  setSecret(cloudBase(opts.cloud), ws, secret);
303
- out(`\u2713 secret for ${ws} saved \u2192 ${credsPath()}`);
500
+ out2(`\u2713 git push password for ${ws} saved \u2192 ${credsPath()} (used by the push-to-create birth lane)`);
304
501
  return 0;
305
502
  }
306
503
  async function secretRotate(ws, opts) {
307
504
  const cloud = cloudBase(opts.cloud);
308
- const current = getSecret(cloud, ws);
309
- if (current === void 0) {
310
- err(`no stored secret for ${ws} on ${cloud} \u2014 githolon secret set ${ws} <secret> first`);
311
- return 1;
312
- }
313
- const r = await fetch(`${cloud}/v2/workspaces/${ws}/rotate-secret`, {
314
- method: "POST",
315
- headers: { authorization: `Bearer ${current}` }
316
- });
317
- const d = await r.json().catch(() => void 0);
318
- if (!r.ok || d?.ok !== true || typeof d.workspaceSecret !== "string") {
319
- err(`rotate '${ws}' failed (${r.status}): ${d ? JSON.stringify(d) : "no body"}`);
320
- return 1;
321
- }
322
- setSecret(cloud, ws, d.workspaceSecret);
323
- out(`\u2713 secret for ${ws} rotated and saved \u2192 ${credsPath()}`);
324
- return 0;
505
+ err2(
506
+ `secret rotate is retired (host-authority strip) \u2014 there is no host-held workspace secret on ${cloud}
507
+ ownership is sha256(push password) bound in the chain; re-own by re-birthing via the upload lane
508
+ with a new push password (see: githolon git remote ${ws} && git push nomos main)`
509
+ );
510
+ return 1;
325
511
  }
326
- var DEFAULT_CLOUD, DEFAULT_AUTH_URL, DEFAULT_AUTH_ANON_KEY, secretKey, out, err;
512
+ var DEFAULT_CLOUD, DEFAULT_AUTH_URL, DEFAULT_AUTH_ANON_KEY, secretKey, out2, err2;
327
513
  var init_cloud = __esm({
328
514
  "src/cloud.ts"() {
329
515
  "use strict";
@@ -331,8 +517,8 @@ var init_cloud = __esm({
331
517
  DEFAULT_AUTH_URL = "https://kjbcjkihxskuwwfdqklt.supabase.co";
332
518
  DEFAULT_AUTH_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtqYmNqa2loeHNrdXd3ZmRxa2x0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTA3MDU2OTAsImV4cCI6MjA2NjI4MTY5MH0.V9e7XsuTlTOLqefOIedTqlBiTxUSn4O5FZSPWwAxiSI";
333
519
  secretKey = (cloud, ws) => `${cloud} ${ws}`;
334
- out = (s) => void process.stdout.write(s + "\n");
335
- err = (s) => void process.stderr.write("error: " + s + "\n");
520
+ out2 = (s) => void process.stdout.write(s + "\n");
521
+ err2 = (s) => void process.stderr.write("error: " + s + "\n");
336
522
  }
337
523
  });
338
524
 
@@ -453,13 +639,13 @@ var init_tree = __esm({
453
639
  function concatBytes(chunks) {
454
640
  let n = 0;
455
641
  for (const c of chunks) n += c.length;
456
- const out9 = new Uint8Array(n);
642
+ const out10 = new Uint8Array(n);
457
643
  let o = 0;
458
644
  for (const c of chunks) {
459
- out9.set(c, o);
645
+ out10.set(c, o);
460
646
  o += c.length;
461
647
  }
462
- return out9;
648
+ return out10;
463
649
  }
464
650
  function pkt(payload) {
465
651
  const body = typeof payload === "string" ? enc2.encode(payload) : payload;
@@ -612,13 +798,13 @@ async function sha256hex(t) {
612
798
  function osEntropyBuffer(n) {
613
799
  const bytes = new Uint8Array(n * 8);
614
800
  for (let o = 0; o < bytes.length; o += 65536) crypto.getRandomValues(bytes.subarray(o, Math.min(o + 65536, bytes.length)));
615
- const out9 = new Array(n), T = 2 ** 53;
801
+ const out10 = new Array(n), T = 2 ** 53;
616
802
  for (let i = 0; i < n; i++) {
617
803
  let z = 0n;
618
804
  for (let b = 7; b >= 0; b--) z = z << 8n | BigInt(bytes[i * 8 + b]);
619
- out9[i] = Number(z >> 11n) / T;
805
+ out10[i] = Number(z >> 11n) / T;
620
806
  }
621
- return out9;
807
+ return out10;
622
808
  }
623
809
  function stringifyBig(o) {
624
810
  return JSON.stringify(o, (_k, v) => typeof v === "bigint" ? `@@B:${v}@@` : v).replace(/"@@B:(\d+)@@"/g, "$1");
@@ -716,13 +902,19 @@ function mintId(eng, typeTag) {
716
902
  }
717
903
  function author(eng, ws, domain, directiveId, payload, controllerHash, opts = {}) {
718
904
  const seq = eng.seq++;
719
- const envelope = { payload: { domain, directiveId, payload }, captured_ports: { clock: { physical: Date.now(), logical: 0, replica: eng.replica }, rng: osEntropyBuffer(opts.rngDraws ?? 64) }, policy_version: 1, policy_domain: "Nomos", policy_gas: 0, policy_memory: 0 };
905
+ const capPorts = { clock: { physical: Date.now(), logical: 0, replica: eng.replica }, rng: osEntropyBuffer(opts.rngDraws ?? 64) };
906
+ if (opts.validFromEpoch !== void 0) capPorts.valid_from_epoch = opts.validFromEpoch;
907
+ if (opts.principalVerified !== void 0) {
908
+ capPorts.principal = opts.actor ?? "";
909
+ capPorts.principal_verified = !!opts.principalVerified;
910
+ }
911
+ const envelope = { payload: { domain, directiveId, payload }, captured_ports: capPorts, policy_version: 1, policy_domain: "Nomos", policy_gas: 0, policy_memory: 0 };
720
912
  writeWork(eng, `payload-${seq}.json`, enc3.encode(JSON.stringify(payload)));
721
913
  writeWork(eng, `envelope-${seq}.json`, enc3.encode(stringifyBig(envelope)));
722
914
  const genesis = !controllerHash;
723
915
  const defer = opts.deferProjection !== false;
724
- const v = JSON.parse(call(eng.ex, "offer", { repoArg: repoArgOf(ws), workspace: ws, domain, directiveId, payloadFile: `/work/payload-${seq}.json`, envelopeFile: `/work/envelope-${seq}.json`, seq, actor: opts.actor ?? "", domainFile: genesis ? "/work/domain.package.usda" : "", domainHash: genesis ? "" : controllerHash, branch: BRANCH, ...opts.authorSecret ? { authorSecret: opts.authorSecret } : {}, ...opts.dryRun ? { dryRun: true } : defer ? { deferProjection: true } : {} }, eng.STDERR));
725
- const res = v.outcome === "admitted" ? { ok: true, head: v.head, intentOut: v.intentOut } : v.outcome === "refused" ? { ok: false, error: v.verdict?.reason ?? v.error } : v;
916
+ const v = JSON.parse(call(eng.ex, "offer", { repoArg: repoArgOf(ws), workspace: ws, domain, directiveId, payloadFile: `/work/payload-${seq}.json`, envelopeFile: `/work/envelope-${seq}.json`, seq, actor: opts.actor ?? "", ...opts.authToken ? { authToken: opts.authToken } : {}, domainFile: genesis ? "/work/domain.package.usda" : "", domainHash: genesis ? "" : controllerHash, branch: BRANCH, ...opts.authorSecret ? { authorSecret: opts.authorSecret } : {}, ...opts.dryRun ? { dryRun: true } : defer ? { deferProjection: true } : {} }, eng.STDERR));
917
+ const res = v.outcome === "admitted" ? { ok: true, head: v.head, intentOut: v.intentOut, ...v.born ? { born: v.born } : {} } : v.outcome === "refused" ? { ok: false, error: v.verdict?.reason ?? v.error } : v;
726
918
  if (defer && res.ok) (eng.pendingProjection ??= /* @__PURE__ */ new Set()).add(ws);
727
919
  return res;
728
920
  }
@@ -763,7 +955,7 @@ function offerIntent(eng, ws, bytes, opts) {
763
955
  }
764
956
  function offerAdmit(eng, ws, bytes, opts) {
765
957
  const v = offerIntent(eng, ws, bytes, opts);
766
- return v.outcome === "admitted" ? { ok: true, head: v.head } : { ok: false, error: v.verdict?.reason ?? v.error ?? "the offer was refused" };
958
+ return v.outcome === "admitted" ? { ok: true, head: v.head, ...v.born ? { born: v.born } : {} } : { ok: false, error: v.verdict?.reason ?? v.error ?? "the offer was refused" };
767
959
  }
768
960
  function applyIntentBytes(eng, ws, bytes, opts) {
769
961
  return offerAdmit(eng, ws, bytes, opts);
@@ -919,21 +1111,21 @@ function sealedIntentOf(eng, ws, res) {
919
1111
  }
920
1112
  function printVerdict(v) {
921
1113
  if (v["valid"] === true) {
922
- out2(`\u2713 chain VALID \u2014 head ${String(v["head"]).slice(0, 12)}\u2026, ${v["intents"]} intent(s), ${v["plansRerun"]} plan(s) re-run`);
923
- out2(` controller ${String(v["controllerHash"]).slice(0, 12)}\u2026 | installed: ${v["installedDomains"]?.map((h) => h.slice(0, 12) + "\u2026").join(", ") ?? "\u2014"}`);
1114
+ out3(`\u2713 chain VALID \u2014 head ${String(v["head"]).slice(0, 12)}\u2026, ${v["intents"]} intent(s), ${v["plansRerun"]} plan(s) re-run`);
1115
+ out3(` controller ${String(v["controllerHash"]).slice(0, 12)}\u2026 | installed: ${v["installedDomains"]?.map((h) => h.slice(0, 12) + "\u2026").join(", ") ?? "\u2014"}`);
924
1116
  return true;
925
1117
  }
926
- err2(`chain INVALID at ${v["check"]}${v["atIndex"] !== void 0 ? ` (intent ${v["atIndex"]})` : ""}: ${v["error"]}`);
1118
+ err3(`chain INVALID at ${v["check"]}${v["atIndex"] !== void 0 ? ` (intent ${v["atIndex"]})` : ""}: ${v["error"]}`);
927
1119
  return false;
928
1120
  }
929
- var out2, err2;
1121
+ var out3, err3;
930
1122
  var init_local_holon = __esm({
931
1123
  "src/local_holon.ts"() {
932
1124
  "use strict";
933
1125
  init_engine();
934
1126
  init_cloud();
935
- out2 = (s) => void process.stdout.write(s + "\n");
936
- err2 = (s) => void process.stderr.write("error: " + s + "\n");
1127
+ out3 = (s) => void process.stdout.write(s + "\n");
1128
+ err3 = (s) => void process.stderr.write("error: " + s + "\n");
937
1129
  }
938
1130
  });
939
1131
 
@@ -1267,6 +1459,7 @@ Re-run with --force to replace it.`
1267
1459
 
1268
1460
  // src/cli.ts
1269
1461
  init_cloud();
1462
+ init_governance();
1270
1463
 
1271
1464
  // src/compile.ts
1272
1465
  import { spawn, spawnSync } from "node:child_process";
@@ -1412,8 +1605,8 @@ async function resolveWorkspace(explicit, cwd, target, usageHint) {
1412
1605
 
1413
1606
  // src/dev.ts
1414
1607
  init_proof_offline();
1415
- var out3 = (s) => void process.stdout.write(s + "\n");
1416
- var err3 = (s) => void process.stderr.write("error: " + s + "\n");
1608
+ var out4 = (s) => void process.stdout.write(s + "\n");
1609
+ var err4 = (s) => void process.stderr.write("error: " + s + "\n");
1417
1610
  var WS = "dev";
1418
1611
  var DEFAULT_PORT = 7700;
1419
1612
  var DEBOUNCE_MS = 200;
@@ -1536,21 +1729,21 @@ async function dev(opts) {
1536
1729
  const cwd = process.cwd();
1537
1730
  const cloud = cloudBase(opts.cloud);
1538
1731
  const project = await loadProject(cwd).catch((e) => {
1539
- err3(e.message);
1732
+ err4(e.message);
1540
1733
  return void 0;
1541
1734
  });
1542
1735
  if (project === void 0) {
1543
1736
  if (!existsSync6(join7(cwd, CONFIG_FILE))) {
1544
- err3(`no ${CONFIG_FILE} here (${cwd}) \u2014 \`githolon dev\` runs inside a project; scaffold one from examples/guestbook or \`githolon generate domain <name>\``);
1737
+ err4(`no ${CONFIG_FILE} here (${cwd}) \u2014 \`githolon dev\` runs inside a project; scaffold one from examples/guestbook or \`githolon generate domain <name>\``);
1545
1738
  }
1546
1739
  return 1;
1547
1740
  }
1548
1741
  const creds = loadCreds();
1549
1742
  const installedBy = creds.session?.uid ?? creds.principal ?? "githolon-dev";
1550
- out3(`githolon dev \u2014 ${project.name}: compiling\u2026`);
1743
+ out4(`githolon dev \u2014 ${project.name}: compiling\u2026`);
1551
1744
  const firstCompile = await compileAsync(cwd);
1552
1745
  if (!firstCompile.ok) {
1553
- err3(`compile failed \u2014 the dev loop needs one green compile to mint the holon:
1746
+ err4(`compile failed \u2014 the dev loop needs one green compile to mint the holon:
1554
1747
  ${firstCompile.output}`);
1555
1748
  return 1;
1556
1749
  }
@@ -1558,14 +1751,14 @@ ${firstCompile.output}`);
1558
1751
  try {
1559
1752
  deployBody = readDeployBody(cwd, project.name);
1560
1753
  } catch (e) {
1561
- err3(e.message);
1754
+ err4(e.message);
1562
1755
  return 1;
1563
1756
  }
1564
1757
  let engBoot;
1565
1758
  try {
1566
1759
  engBoot = await holonEngine(cloud, deployBody, installedBy);
1567
1760
  } catch (e) {
1568
- err3(`cannot boot the local holon: ${String(e.message ?? e)}
1761
+ err4(`cannot boot the local holon: ${String(e.message ?? e)}
1569
1762
  the runtime rides ${cloud}/v1/runtime/* and is cached in ~/.holon/runtime after one fetch \u2014 connect once, then dev runs offline`);
1570
1763
  return 1;
1571
1764
  }
@@ -1588,19 +1781,19 @@ ${firstCompile.output}`);
1588
1781
  s.intents++;
1589
1782
  const installed = await installLaw(s, deployBody, installedBy);
1590
1783
  if (!installed.ok) {
1591
- err3(installed.error ?? "law install failed");
1784
+ err4(installed.error ?? "law install failed");
1592
1785
  return 1;
1593
1786
  }
1594
1787
  s.proof = await runProofCycle(s, cwd, deployBody, installedBy);
1595
1788
  if (opts.once === true) {
1596
- out3(statusScreen({ ...s, watchRoots: ["(--once: no watch)"] }));
1789
+ out4(statusScreen({ ...s, watchRoots: ["(--once: no watch)"] }));
1597
1790
  return s.proof === void 0 || s.proof.ok ? 0 : 1;
1598
1791
  }
1599
1792
  let server;
1600
1793
  try {
1601
1794
  server = await startServer(s);
1602
1795
  } catch (e) {
1603
- err3(e.message);
1796
+ err4(e.message);
1604
1797
  return 1;
1605
1798
  }
1606
1799
  const moduleDirs = /* @__PURE__ */ new Set();
@@ -1628,7 +1821,7 @@ ${firstCompile.output}`);
1628
1821
  s.compileMs = run.ms;
1629
1822
  if (!run.ok) {
1630
1823
  s.lastError = run.output.trim() || "compile failed";
1631
- out3(`\u2717 cycle ${s.cycle}: compile failed \u2014 the previous law keeps serving
1824
+ out4(`\u2717 cycle ${s.cycle}: compile failed \u2014 the previous law keeps serving
1632
1825
  ${run.output}`);
1633
1826
  } else {
1634
1827
  s.lastError = void 0;
@@ -1637,16 +1830,16 @@ ${run.output}`);
1637
1830
  const inst = await installLaw(s, body, installedBy);
1638
1831
  if (!inst.ok) {
1639
1832
  s.lastError = inst.error ?? "install failed";
1640
- out3(`\u2717 cycle ${s.cycle}: ${s.lastError}`);
1833
+ out4(`\u2717 cycle ${s.cycle}: ${s.lastError}`);
1641
1834
  } else {
1642
1835
  s.proof = await runProofCycle(s, cwd, body, installedBy);
1643
1836
  const total = performance.now() - t0;
1644
- out3(statusScreen(s));
1645
- out3(` cycle edit \u2192 law-live \u2192 proof in ${total.toFixed(0)} ms total`);
1837
+ out4(statusScreen(s));
1838
+ out4(` cycle edit \u2192 law-live \u2192 proof in ${total.toFixed(0)} ms total`);
1646
1839
  }
1647
1840
  } catch (e) {
1648
1841
  s.lastError = String(e.message ?? e);
1649
- out3(`\u2717 cycle ${s.cycle}: ${s.lastError}`);
1842
+ out4(`\u2717 cycle ${s.cycle}: ${s.lastError}`);
1650
1843
  }
1651
1844
  }
1652
1845
  cycling = false;
@@ -1672,8 +1865,8 @@ ${run.output}`);
1672
1865
  })
1673
1866
  );
1674
1867
  s.watchRoots.push(CONFIG_FILE);
1675
- out3(statusScreen(s));
1676
- out3(" (ctrl-c stops; edits in the watched paths recompile + re-prove automatically)");
1868
+ out4(statusScreen(s));
1869
+ out4(" (ctrl-c stops; edits in the watched paths recompile + re-prove automatically)");
1677
1870
  return new Promise((resolveExit) => {
1678
1871
  const stop = () => {
1679
1872
  for (const w of watchers) w.close();
@@ -1690,8 +1883,8 @@ init_cloud();
1690
1883
  import { spawnSync as spawnSync2 } from "node:child_process";
1691
1884
  import { randomBytes as randomBytes2 } from "node:crypto";
1692
1885
  import { readFileSync as readFileSync5 } from "node:fs";
1693
- var out4 = (s) => void process.stdout.write(s + "\n");
1694
- var err4 = (s) => void process.stderr.write("error: " + s + "\n");
1886
+ var out5 = (s) => void process.stdout.write(s + "\n");
1887
+ var err5 = (s) => void process.stderr.write("error: " + s + "\n");
1695
1888
  function git(args, opts = {}) {
1696
1889
  const r = spawnSync2("git", args, { stdio: opts.quiet === true ? "ignore" : "inherit" });
1697
1890
  return r.status ?? 1;
@@ -1723,31 +1916,31 @@ async function gitCredential(action) {
1723
1916
  if (kv["protocol"] !== "https" || ws === void 0) return 0;
1724
1917
  const cloud = `https://${kv["host"]}`;
1725
1918
  const token = await sessionToken().catch(() => void 0);
1726
- out4(`username=${token ?? "nomos"}`);
1727
- out4(`password=${ensureSecret(cloud, ws)}`);
1728
- out4("");
1919
+ out5(`username=${token ?? "nomos"}`);
1920
+ out5(`password=${ensureSecret(cloud, ws)}`);
1921
+ out5("");
1729
1922
  return 0;
1730
1923
  }
1731
1924
  async function gitSetup(opts) {
1732
1925
  const cloud = cloudBase(opts.cloud);
1733
1926
  const self = process.argv[1];
1734
1927
  if (self === void 0) {
1735
- err4("cannot locate the githolon CLI entry to install as a credential helper");
1928
+ err5("cannot locate the githolon CLI entry to install as a credential helper");
1736
1929
  return 1;
1737
1930
  }
1738
1931
  const helper = `!"${process.execPath}" "${self}" git-credential`;
1739
1932
  if (git(["config", "--global", `credential.${cloud}.helper`, helper]) !== 0 || git(["config", "--global", `credential.${cloud}.useHttpPath`, "true"]) !== 0) {
1740
- err4("git config failed \u2014 is git installed?");
1933
+ err5("git config failed \u2014 is git installed?");
1741
1934
  return 1;
1742
1935
  }
1743
- out4(`\u2713 git credential helper installed for ${cloud} (global gitconfig)`);
1744
- out4(` pushes/clones of ${cloud}/v2/workspaces/<ws>/git now authenticate from ~/.holon`);
1936
+ out5(`\u2713 git credential helper installed for ${cloud} (global gitconfig)`);
1937
+ out5(` pushes/clones of ${cloud}/v2/workspaces/<ws>/git now authenticate from ~/.holon`);
1745
1938
  return 0;
1746
1939
  }
1747
1940
  async function gitRemote(ws, name, opts) {
1748
1941
  const cloud = cloudBase(opts.cloud);
1749
1942
  if (git(["rev-parse", "--git-dir"], { quiet: true }) !== 0) {
1750
- err4("not a git repository \u2014 run inside your repo (git init first)");
1943
+ err5("not a git repository \u2014 run inside your repo (git init first)");
1751
1944
  return 1;
1752
1945
  }
1753
1946
  const setupRc = await gitSetup(opts);
@@ -1756,7 +1949,7 @@ async function gitRemote(ws, name, opts) {
1756
1949
  const url = `${cloud}/v2/workspaces/${ws}/git`;
1757
1950
  if (git(["remote", "add", name, url], { quiet: true }) !== 0) {
1758
1951
  if (git(["remote", "set-url", name, url], { quiet: true }) !== 0) {
1759
- err4(`could not add or update remote '${name}'`);
1952
+ err5(`could not add or update remote '${name}'`);
1760
1953
  return 1;
1761
1954
  }
1762
1955
  }
@@ -1764,7 +1957,7 @@ async function gitRemote(ws, name, opts) {
1764
1957
  if (token === void 0) {
1765
1958
  const principal = loadCreds().principal;
1766
1959
  if (principal === void 0) {
1767
- err4(
1960
+ err5(
1768
1961
  `remote '${name}' \u2192 ${url} is wired, but a BIRTH push needs a principal:
1769
1962
  githolon login --agent (then just push)
1770
1963
  \u2014 or re-run after githolon ws create/login so a principal is on file`
@@ -1772,13 +1965,13 @@ async function gitRemote(ws, name, opts) {
1772
1965
  return 1;
1773
1966
  }
1774
1967
  if (git(["config", `http.${url}.extraHeader`, `x-nomos-principal: ${principal}`]) !== 0) {
1775
- err4("git config failed setting the principal header");
1968
+ err5("git config failed setting the principal header");
1776
1969
  return 1;
1777
1970
  }
1778
1971
  }
1779
- out4(`\u2713 remote '${name}' \u2192 ${url}`);
1780
- out4(` workspace secret on file; ${token !== void 0 ? "births ride your session (verified)" : "births ride x-nomos-principal (bare uid)"}`);
1781
- out4(` birth/deploy by push: git push ${name} main`);
1972
+ out5(`\u2713 remote '${name}' \u2192 ${url}`);
1973
+ out5(` workspace secret on file; ${token !== void 0 ? "births ride your session (verified)" : "births ride x-nomos-principal (bare uid)"}`);
1974
+ out5(` birth/deploy by push: git push ${name} main`);
1782
1975
  return 0;
1783
1976
  }
1784
1977
 
@@ -1858,18 +2051,50 @@ var HELP = {
1858
2051
  what: "Clear the session. Stored workspace secrets are KEPT."
1859
2052
  },
1860
2053
  ws: {
1861
- usage: "githolon ws <create|status|retire> [<name>] [--principal <uid|token>] [--cloud <url>] [--target <name>]",
1862
- what: "Workspace lifecycle on Nomos Cloud. `create` births one (saves its ONE-TIME secret to\n~/.holon); `status` shows lineage/phase/verdicts; `retire` frees the quota slot (owner; nothing\nis deleted). `status` with no <name> resolves the project's `workspace` binding.",
2054
+ usage: "githolon ws <create|status|retire|reclaim> [<name>] [--principal <uid|token>] [--via <parent>] [--parent <ws>] [--cloud <url>] [--target <name>]",
2055
+ what: "Workspace lifecycle on Nomos Cloud. `create` births one (NO host secret is issued \u2014\nownership is a ledger fact; deploy is open + kernel-gated) or, with --via <parent>, requests it\nas a SIGNED createWorkspace governance offer. `status` shows lineage/phase/verdicts. `retire`\nand `reclaim` are SIGNED governance offers to the parent (default root) \u2014 never host-author\nendpoints; retire withdraws the law (data retained), reclaim authorizes the bailiff to drop\ncustody (only a retired workspace; never root). `status` with no <name> resolves the project's\n`workspace` binding.",
1863
2056
  flags: [
1864
2057
  ["--principal <p>", "the birth's principal (uid or access token; remembered as the default)"],
2058
+ ["--via <parent>", "ws create: sign a createWorkspace offer at <parent> instead of the raw birth"],
2059
+ ["--parent <ws>", "retire/reclaim: the governance workspace to sign against (default root)"],
1865
2060
  ["--cloud <url>", "target cloud (default: $NOMOS_CLOUD or the public cloud)"],
1866
2061
  ["--target <name>", "ws status: pick among the project's named workspace targets"]
1867
2062
  ],
1868
- examples: ["githolon ws create my-guestbook", "githolon ws status", "githolon ws retire my-guestbook"]
2063
+ examples: ["githolon ws create my-guestbook", "githolon ws create app1 --via co2", "githolon ws status", "githolon ws retire my-guestbook"]
2064
+ },
2065
+ key: {
2066
+ usage: "githolon key <enroll|show> [--principal <uid>] [--parent <ws>] [--cloud <url>]",
2067
+ what: "The signing DEVICE KEY (ed25519) the SIGNED-governance lane authors with. `enroll` mints one\n(if needed) and enrolls its PUBLIC half on-chain (a signed enrollSigner offer at the parent,\ndefault root) so the warrant gate can prove this device authors as the principal; `show` prints\nthe public identity. The SECRET stays in ~/.holon/credentials.json (0600) \u2014 never on chain.",
2068
+ flags: [
2069
+ ["--principal <p>", "the principal the key authors as (default: the logged-in session uid)"],
2070
+ ["--parent <ws>", "the governance workspace to enroll at (default root)"],
2071
+ ["--cloud <url>", "target cloud"]
2072
+ ],
2073
+ examples: ["githolon key enroll", "githolon key show"]
2074
+ },
2075
+ grant: {
2076
+ usage: "githolon grant <admin|creation|quota> <principal> [--max <N>] [--label <L>] [--parent <ws>] [--cloud <url>]",
2077
+ what: "Grant authority as a SIGNED governance offer to the parent (default root) \u2014 never a host\nendpoint. The grant is attributed to YOUR signing principal; only an admin's signature is\nadmitted by the far-side gate. `admin`/`creation` grant the principal-centric relation; `quota`\ngrants a per-principal workspace allowance (--max). Enroll a device key first (githolon key enroll).",
2078
+ flags: [
2079
+ ["--max <N>", "grant quota: the principal's workspace allowance (positive int)"],
2080
+ ["--label <L>", "grant admin/creation: a provenance label"],
2081
+ ["--parent <ws>", "the governance workspace to sign against (default root)"],
2082
+ ["--cloud <url>", "target cloud"]
2083
+ ],
2084
+ examples: ["githolon grant creation 32c7100e-\u2026", "githolon grant admin 32c7100e-\u2026 --label lieutenant", "githolon grant quota 32c7100e-\u2026 --max 200"]
2085
+ },
2086
+ revoke: {
2087
+ usage: "githolon revoke <admin|creation> <principal> [--parent <ws>] [--cloud <url>]",
2088
+ what: "Revoke authority as a SIGNED governance offer to the parent (default root). The revoked FACT\n(who, when, by whom) rides the chain forever \u2014 audit is structural. Admin-gated, like grant.",
2089
+ flags: [
2090
+ ["--parent <ws>", "the governance workspace to sign against (default root)"],
2091
+ ["--cloud <url>", "target cloud"]
2092
+ ],
2093
+ examples: ["githolon revoke creation 32c7100e-\u2026", "githolon revoke admin 32c7100e-\u2026"]
1869
2094
  },
1870
2095
  deploy: {
1871
2096
  usage: "githolon deploy [<ws>] [--file <deploy.json>] [--cloud <url>] [--target <name>]",
1872
- what: "POST build/*.deploy.json with the stored workspace secret. Prints what MOVED (old \u2192 new law\nhash). With no <ws>, the project's `workspace` binding in nomos.package.mjs resolves it.",
2097
+ what: "POST build/*.deploy.json on the OPEN deploy lane (kernel-gated \u2014 no host secret). Prints what\nMOVED (old \u2192 new law hash). With no <ws>, the project's `workspace` binding in nomos.package.mjs\nresolves it.",
1873
2098
  flags: [
1874
2099
  ["--file <path>", "an explicit deploy body (default: the one build/*.deploy.json)"],
1875
2100
  ["--target <name>", "pick among named targets (workspace: { dev: \u2026, prod: \u2026 })"],
@@ -1878,9 +2103,9 @@ var HELP = {
1878
2103
  examples: ["githolon deploy", "githolon deploy my-guestbook", "githolon deploy --target prod"]
1879
2104
  },
1880
2105
  secret: {
1881
- usage: "githolon secret <set <ws> <secret> | rotate <ws>> [--cloud <url>]",
1882
- what: "The workspace-secret store (~/.holon/credentials.json, 0600). `set` imports one you already\nhold; `rotate` mints a fresh one (and claims a legacy workspace).",
1883
- examples: ["githolon secret set my-ws nws_v1_\u2026", "githolon secret rotate my-ws"]
2106
+ usage: "githolon secret <set <ws> <push-password> | rotate <ws>> [--cloud <url>]",
2107
+ what: "The git push-password store (~/.holon/credentials.json, 0600) for the push-to-create birth lane\n(sha256(it) becomes the workspace owner bound IN THE CHAIN). The open deploy lane uses no secret.\n`set` imports a push password you already hold; `rotate` is RETIRED (the host holds no ownership\ncredential to rotate \u2014 410).",
2108
+ examples: ["githolon secret set my-ws holon_v1_\u2026"]
1884
2109
  },
1885
2110
  git: {
1886
2111
  usage: "githolon git <setup | remote [<ws>] [<remoteName>]> [--cloud <url>] [--target <name>]",
@@ -1916,20 +2141,20 @@ init_local_holon();
1916
2141
  import { spawnSync as spawnSync3 } from "node:child_process";
1917
2142
  import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
1918
2143
  import { join as join8 } from "node:path";
1919
- var out5 = (s) => void process.stdout.write(s + "\n");
1920
- var err5 = (s) => void process.stderr.write("error: " + s + "\n");
2144
+ var out6 = (s) => void process.stdout.write(s + "\n");
2145
+ var err6 = (s) => void process.stderr.write("error: " + s + "\n");
1921
2146
  var WS2 = "local";
1922
2147
  async function ledgerInit(dir, opts) {
1923
2148
  const cloud = cloudBase(opts.cloud);
1924
2149
  if (existsSync7(dir) && readdirSync3(dir).length > 0) {
1925
- err5(`refusing to mint into non-empty directory: ${dir}`);
2150
+ err6(`refusing to mint into non-empty directory: ${dir}`);
1926
2151
  return 1;
1927
2152
  }
1928
2153
  let deployFile;
1929
2154
  try {
1930
2155
  deployFile = discoverDeployJson(process.cwd(), opts.file);
1931
2156
  } catch (e) {
1932
- err5(e.message);
2157
+ err6(e.message);
1933
2158
  return 1;
1934
2159
  }
1935
2160
  const deployBody = JSON.parse(readFileSync6(deployFile, "utf8"));
@@ -1937,10 +2162,10 @@ async function ledgerInit(dir, opts) {
1937
2162
  const c = loadCreds();
1938
2163
  const principal = await sessionToken().catch(() => void 0) !== void 0 ? c.session.uid : c.principal;
1939
2164
  if (principal === void 0) {
1940
- err5("a holon is born OF someone \u2014 githolon login --agent first (or githolon ws create --principal <uid> once)");
2165
+ err6("a holon is born OF someone \u2014 githolon login --agent first (or githolon ws create --principal <uid> once)");
1941
2166
  return 1;
1942
2167
  }
1943
- out5(`minting a holon locally (law ${domainHash.slice(0, 12)}\u2026, installedBy ${principal})`);
2168
+ out6(`minting a holon locally (law ${domainHash.slice(0, 12)}\u2026, installedBy ${principal})`);
1944
2169
  const { eng } = await holonEngine(cloud, deployBody, principal);
1945
2170
  await mountFresh(eng, WS2);
1946
2171
  author(eng, WS2, "bootstrap", "installDomain", installPayload(eng.hashes.nomos, eng.nomosPkg, principal), "");
@@ -1953,16 +2178,16 @@ async function ledgerInit(dir, opts) {
1953
2178
  const cfgPath = join8(gitDir, "config");
1954
2179
  writeFileSync4(cfgPath, readFileSync6(cfgPath, "utf8").replace(/bare = true/, "bare = false"), "utf8");
1955
2180
  spawnSync3("git", ["-C", dir, "reset", "--hard", "main"], { stdio: "ignore" });
1956
- out5(`\u2713 holon written \u2192 ${dir} (a normal git repo; git log IS the audit trail)`);
1957
- out5(`
2181
+ out6(`\u2713 holon written \u2192 ${dir} (a normal git repo; git log IS the audit trail)`);
2182
+ out6(`
1958
2183
  Birth it on Nomos Cloud (the cloud re-derives this exact verdict):`);
1959
- out5(` cd ${dir} && npx githolon git remote <ws> && git push nomos main`);
2184
+ out6(` cd ${dir} && npx githolon git remote <ws> && git push nomos main`);
1960
2185
  return 0;
1961
2186
  }
1962
2187
  async function ledgerVerify(dir, opts) {
1963
2188
  const gitDir = existsSync7(join8(dir, ".git")) ? join8(dir, ".git") : dir;
1964
2189
  if (!existsSync7(join8(gitDir, "HEAD"))) {
1965
- err5(`${dir} is not a git repo (no HEAD)`);
2190
+ err6(`${dir} is not a git repo (no HEAD)`);
1966
2191
  return 1;
1967
2192
  }
1968
2193
  const { eng } = await holonEngine(cloudBase(opts.cloud), void 0, "verify");
@@ -1980,8 +2205,8 @@ init_local_holon();
1980
2205
  import { existsSync as existsSync8 } from "node:fs";
1981
2206
  import { join as join9 } from "node:path";
1982
2207
  import git2 from "isomorphic-git";
1983
- var out6 = (s) => void process.stdout.write(s + "\n");
1984
- var err6 = (s) => void process.stderr.write("error: " + s + "\n");
2208
+ var out7 = (s) => void process.stdout.write(s + "\n");
2209
+ var err7 = (s) => void process.stderr.write("error: " + s + "\n");
1985
2210
  var SOURCE = "source";
1986
2211
  var REPLAY = "replay";
1987
2212
  function summarizePayload(payload, max = 100) {
@@ -2017,9 +2242,9 @@ function capJsonStrings(v, max = 2048) {
2017
2242
  if (typeof v === "string") return v.length > max ? `${v.slice(0, max)}\u2026(${v.length} chars)` : v;
2018
2243
  if (Array.isArray(v)) return v.map((x) => capJsonStrings(x, max));
2019
2244
  if (typeof v === "object" && v !== null) {
2020
- const out9 = {};
2021
- for (const [k, x] of Object.entries(v)) out9[k] = capJsonStrings(x, max);
2022
- return out9;
2245
+ const out10 = {};
2246
+ for (const [k, x] of Object.entries(v)) out10[k] = capJsonStrings(x, max);
2247
+ return out10;
2023
2248
  }
2024
2249
  return v;
2025
2250
  }
@@ -2066,12 +2291,12 @@ function readKey() {
2066
2291
  async function replay(target, opts) {
2067
2292
  const cloud = cloudBase(opts.cloud);
2068
2293
  const json = opts.json === true;
2069
- const emit = (o) => out6(JSON.stringify(o));
2294
+ const emit = (o) => out7(JSON.stringify(o));
2070
2295
  let eng;
2071
2296
  try {
2072
2297
  ({ eng } = await holonEngine(cloud, void 0, "githolon-replay"));
2073
2298
  } catch (e) {
2074
- err6(`cannot boot the local holon: ${String(e.message ?? e)}
2299
+ err7(`cannot boot the local holon: ${String(e.message ?? e)}
2075
2300
  the runtime rides ${cloud}/v1/runtime/* (cached in ~/.holon/runtime after one fetch)`);
2076
2301
  return 1;
2077
2302
  }
@@ -2079,7 +2304,7 @@ async function replay(target, opts) {
2079
2304
  if (isDir) {
2080
2305
  const gitDir = existsSync8(join9(target, ".git")) ? join9(target, ".git") : target;
2081
2306
  if (!existsSync8(join9(gitDir, "HEAD"))) {
2082
- err6(`${target} is not a git repo (no HEAD) \u2014 pass a holon directory (githolon ledger init <dir>) or a workspace name`);
2307
+ err7(`${target} is not a git repo (no HEAD) \u2014 pass a holon directory (githolon ledger init <dir>) or a workspace name`);
2083
2308
  return 1;
2084
2309
  }
2085
2310
  await mountFresh(eng, SOURCE);
@@ -2087,7 +2312,7 @@ async function replay(target, opts) {
2087
2312
  } else {
2088
2313
  const m = await mountWorkspace(eng, SOURCE, { remote: `${cloud}/v2/workspaces/${target}/git`, headers: {} });
2089
2314
  if (m.restored !== true) {
2090
- err6(
2315
+ err7(
2091
2316
  `workspace '${target}' has no refs/heads/main on ${cloud} \u2014 nothing to replay` + (m.restoreError ? ` (${String(m.restoreError).split(" | ")[0]})` : "") + `
2092
2317
  check the name (githolon ws status ${target}), or pass a local holon directory instead`
2093
2318
  );
@@ -2096,7 +2321,7 @@ async function replay(target, opts) {
2096
2321
  }
2097
2322
  const chain = await walkChain(eng, SOURCE);
2098
2323
  if (chain.length === 0) {
2099
- err6(`the chain on '${target}' carries no intents \u2014 nothing to replay`);
2324
+ err7(`the chain on '${target}' carries no intents \u2014 nothing to replay`);
2100
2325
  return 1;
2101
2326
  }
2102
2327
  await mountFresh(eng, REPLAY);
@@ -2105,8 +2330,8 @@ async function replay(target, opts) {
2105
2330
  const tallies = /* @__PURE__ */ new Map();
2106
2331
  let autoRun = !interactive;
2107
2332
  if (!json) {
2108
- out6(`replaying ${chain.length} intent(s) from ${isDir ? target : `${cloud} :: ${target}`} \u2014 every step re-admitted through the wasm gate`);
2109
- if (interactive) out6(" keys: enter = next intent \xB7 a = run to the end \xB7 q = quit\n");
2333
+ out7(`replaying ${chain.length} intent(s) from ${isDir ? target : `${cloud} :: ${target}`} \u2014 every step re-admitted through the wasm gate`);
2334
+ if (interactive) out7(" keys: enter = next intent \xB7 a = run to the end \xB7 q = quit\n");
2110
2335
  }
2111
2336
  for (let i = 0; i < chain.length; i++) {
2112
2337
  const { oid, bytes, doc } = chain[i];
@@ -2120,8 +2345,8 @@ async function replay(target, opts) {
2120
2345
  if (res.ok === false) {
2121
2346
  const msg = `replay REFUSED at intent ${i} (${domain}/${directive}, ${oid.slice(0, 10)}): ${res.error ?? "unknown"}`;
2122
2347
  if (json) emit({ step: i, oid, domain, directive, refused: true, error: res.error ?? "unknown" });
2123
- else err6(msg);
2124
- err6("the chain's own gate rejects this entry on a fresh fold \u2014 the source it came from would refuse it too (verify below names the check)");
2348
+ else err7(msg);
2349
+ err7("the chain's own gate rejects this entry on a fresh fold \u2014 the source it came from would refuse it too (verify below names the check)");
2125
2350
  printVerdict(verifyChainLocal(eng, SOURCE));
2126
2351
  return 1;
2127
2352
  }
@@ -2142,31 +2367,31 @@ async function replay(target, opts) {
2142
2367
  tallies: Object.fromEntries(tallies)
2143
2368
  });
2144
2369
  } else {
2145
- out6(`[${String(i + 1).padStart(String(chain.length).length)}/${chain.length}] ${domain}/${directive} (${oid.slice(0, 10)}, ${ms.toFixed(0)} ms)`);
2146
- out6(` payload ${summarizePayload(doc.payload?.payload)}`);
2370
+ out7(`[${String(i + 1).padStart(String(chain.length).length)}/${chain.length}] ${domain}/${directive} (${oid.slice(0, 10)}, ${ms.toFixed(0)} ms)`);
2371
+ out7(` payload ${summarizePayload(doc.payload?.payload)}`);
2147
2372
  for (const ev of doc.events ?? []) {
2148
2373
  const id = ev.aggregate ?? "?";
2149
2374
  const was = before.get(id);
2150
2375
  const verb = ev.marker === "Create" ? "+ created" : ev.marker === "Delete" ? "- deleted" : was === void 0 ? "~ touched" : "~ updated";
2151
- out6(` ${verb} ${id}`);
2376
+ out7(` ${verb} ${id}`);
2152
2377
  const ops = summarizeOps(ev);
2153
- if (ops.length > 0) out6(` ${ops}`);
2378
+ if (ops.length > 0) out7(` ${ops}`);
2154
2379
  }
2155
2380
  const tallyLine = [...tallies.entries()].map(([t, n]) => `${t}:${n}`).join(" ");
2156
2381
  const showTally = stepEvery > 0 ? (i + 1) % stepEvery === 0 || i === chain.length - 1 : true;
2157
- if (showTally && tallyLine.length > 0) out6(` rows ${tallyLine}`);
2382
+ if (showTally && tallyLine.length > 0) out7(` rows ${tallyLine}`);
2158
2383
  }
2159
2384
  if (interactive && !autoRun && i < chain.length - 1) {
2160
2385
  const key = await readKey();
2161
2386
  if (key === "quit") {
2162
- out6(`
2387
+ out7(`
2163
2388
  stopped at intent ${i + 1}/${chain.length} \u2014 the verify verdict below covers the WHOLE chain as delivered`);
2164
2389
  break;
2165
2390
  }
2166
2391
  if (key === "all") autoRun = true;
2167
2392
  }
2168
2393
  }
2169
- if (!json) out6("");
2394
+ if (!json) out7("");
2170
2395
  const verdict = verifyChainLocal(eng, SOURCE);
2171
2396
  if (json) {
2172
2397
  emit({ finale: true, verdict });
@@ -2180,32 +2405,32 @@ init_local_holon();
2180
2405
  init_cloud();
2181
2406
  init_engine();
2182
2407
  var SOURCE2 = "source";
2183
- var out7 = (s) => {
2408
+ var out8 = (s) => {
2184
2409
  process.stdout.write(s + "\n");
2185
2410
  };
2186
- var err7 = (s) => {
2411
+ var err8 = (s) => {
2187
2412
  process.stderr.write(`error: ${s}
2188
2413
  `);
2189
2414
  };
2190
2415
  async function decrypt(ws, opts) {
2191
2416
  const cloud = cloudBase(opts.cloud);
2192
2417
  if (!opts.as) {
2193
- err7("--as <principal> is required (who to decrypt as)");
2418
+ err8("--as <principal> is required (who to decrypt as)");
2194
2419
  return 1;
2195
2420
  }
2196
2421
  if (!opts.secret) {
2197
- err7("--secret <base64 X25519 device secret> is required");
2422
+ err8("--secret <base64 X25519 device secret> is required");
2198
2423
  return 1;
2199
2424
  }
2200
2425
  if (!opts.query) {
2201
- err7("--query <queryId> is required (the read to decrypt)");
2426
+ err8("--query <queryId> is required (the read to decrypt)");
2202
2427
  return 1;
2203
2428
  }
2204
2429
  let eng;
2205
2430
  try {
2206
2431
  ({ eng } = await holonEngine(cloud, opts.overlay, "githolon-decrypt"));
2207
2432
  } catch (e) {
2208
- err7(`cannot boot the local holon: ${String(e.message ?? e)}`);
2433
+ err8(`cannot boot the local holon: ${String(e.message ?? e)}`);
2209
2434
  return 1;
2210
2435
  }
2211
2436
  const m = await mountWorkspace(eng, SOURCE2, {
@@ -2213,7 +2438,7 @@ async function decrypt(ws, opts) {
2213
2438
  headers: { "x-nomos-principal": opts.as }
2214
2439
  });
2215
2440
  if (m.restored !== true) {
2216
- err7(`workspace '${ws}' has no main on ${cloud}, or its clone was refused for '${opts.as}'` + (m.restoreError ? ` (${String(m.restoreError).split(" | ")[0]})` : ""));
2441
+ err8(`workspace '${ws}' has no main on ${cloud}, or its clone was refused for '${opts.as}'` + (m.restoreError ? ` (${String(m.restoreError).split(" | ")[0]})` : ""));
2217
2442
  return 1;
2218
2443
  }
2219
2444
  const wraps = query(eng, SOURCE2, "wrappedKeysByRecipient", JSON.stringify({ recipientPrincipal: opts.as }), opts.as);
@@ -2228,14 +2453,14 @@ async function decrypt(ws, opts) {
2228
2453
  }
2229
2454
  }
2230
2455
  }
2231
- if (opts.json !== true) out7(`# resolved ${keys.length} scope key(s) for '${opts.as}': ${keys.map((k) => `${k.scope}@${k.epoch}`).join(", ") || "(none)"}`);
2456
+ if (opts.json !== true) out8(`# resolved ${keys.length} scope key(s) for '${opts.as}': ${keys.map((k) => `${k.scope}@${k.epoch}`).join(", ") || "(none)"}`);
2232
2457
  const rows = query(eng, SOURCE2, opts.query, JSON.stringify(opts.params ?? {}), opts.as, keys);
2233
2458
  if (rows && !Array.isArray(rows) && rows.ok === false) {
2234
- err7(`the read was refused: ${rows.error}` + (String(rows.error).startsWith("read-forbidden") ? `
2459
+ err8(`the read was refused: ${rows.error}` + (String(rows.error).startsWith("read-forbidden") ? `
2235
2460
  '${opts.as}' is not granted this scope's read capability` : ""));
2236
2461
  return 1;
2237
2462
  }
2238
- out7(JSON.stringify(rows, null, opts.json === true ? 0 : 2));
2463
+ out8(JSON.stringify(rows, null, opts.json === true ? 0 : 2));
2239
2464
  return 0;
2240
2465
  }
2241
2466
 
@@ -2243,8 +2468,8 @@ async function decrypt(ws, opts) {
2243
2468
  init_engine();
2244
2469
  init_cloud();
2245
2470
  import { readFileSync as readFileSync8 } from "node:fs";
2246
- var out8 = (s) => void process.stdout.write(s + "\n");
2247
- var err8 = (s) => void process.stderr.write("error: " + s + "\n");
2471
+ var out9 = (s) => void process.stdout.write(s + "\n");
2472
+ var err9 = (s) => void process.stderr.write("error: " + s + "\n");
2248
2473
  function lawDiffVerdict(localHash, installed, ws) {
2249
2474
  const active = installed.filter((d) => d.phase === "Active" && typeof d.domainHash === "string");
2250
2475
  const currents = active.filter((d) => d.current === true);
@@ -2282,7 +2507,7 @@ async function status(wsArg, opts) {
2282
2507
  try {
2283
2508
  ws = await resolveWorkspace(wsArg, process.cwd(), opts.target, "githolon status <ws>");
2284
2509
  } catch (e) {
2285
- err8(e.message);
2510
+ err9(e.message);
2286
2511
  return 1;
2287
2512
  }
2288
2513
  let localHash;
@@ -2294,35 +2519,35 @@ async function status(wsArg, opts) {
2294
2519
  }
2295
2520
  const r = await fetch(`${cloud}/v2/workspaces/${ws}/domains`).catch(() => void 0);
2296
2521
  if (r === void 0) {
2297
- err8(`cannot reach ${cloud} \u2014 githolon status compares against the DEPLOYED law and needs the cloud (local-only: githolon dev)`);
2522
+ err9(`cannot reach ${cloud} \u2014 githolon status compares against the DEPLOYED law and needs the cloud (local-only: githolon dev)`);
2298
2523
  return 1;
2299
2524
  }
2300
2525
  const d = await r.json().catch(() => void 0);
2301
2526
  if (!r.ok || d?.ok !== true) {
2302
- err8(`status '${ws}' failed (${r.status}): ${d ? JSON.stringify(d) : "no body"}
2527
+ err9(`status '${ws}' failed (${r.status}): ${d ? JSON.stringify(d) : "no body"}
2303
2528
  is the workspace born? githolon ws create ${ws}`);
2304
2529
  return 1;
2305
2530
  }
2306
- out8(`workspace ${ws} on ${cloud}`);
2531
+ out9(`workspace ${ws} on ${cloud}`);
2307
2532
  const allDomains = d.domains ?? [];
2308
2533
  const controlPlaneHashes = new Set(CONTROL_PLANE_KEYS.map((k) => d.currentLaw?.[k]).filter(Boolean));
2309
2534
  const domains = allDomains.filter((dm) => !controlPlaneHashes.has(dm.domainHash));
2310
2535
  const controllerCount = allDomains.length - domains.length;
2311
- if (allDomains.length === 0) out8(`installed (none \u2014 not even the controller; the workspace may be unborn)`);
2536
+ if (allDomains.length === 0) out9(`installed (none \u2014 not even the controller; the workspace may be unborn)`);
2312
2537
  for (const dom of domains) {
2313
2538
  const mark = dom.current === true ? " \u2190 current (the holon resolves dispatch here)" : "";
2314
- out8(`installed ${(dom.domainHash ?? "?").slice(0, 16)}\u2026 ${dom.phase ?? "?"} by ${dom.installedBy ?? "?"}${mark}`);
2539
+ out9(`installed ${(dom.domainHash ?? "?").slice(0, 16)}\u2026 ${dom.phase ?? "?"} by ${dom.installedBy ?? "?"}${mark}`);
2315
2540
  }
2316
- if (controllerCount > 0) out8(` (+ ${controllerCount} control-plane install(s) \u2014 the nomos lifecycle controller; not tenant law)`);
2541
+ if (controllerCount > 0) out9(` (+ ${controllerCount} control-plane install(s) \u2014 the nomos lifecycle controller; not tenant law)`);
2317
2542
  const tenantActive = domains.filter((dm) => dm.phase === "Active").length;
2318
- if (tenantActive > 1) out8(` (${tenantActive} active tenant installs \u2014 install-beside keeps every prior deploy Active; the holon serves the current one per domain)`);
2543
+ if (tenantActive > 1) out9(` (${tenantActive} active tenant installs \u2014 install-beside keeps every prior deploy Active; the holon serves the current one per domain)`);
2319
2544
  if (localHash === void 0) {
2320
- out8(`local (no build/*.deploy.json here \u2014 run \`githolon compile\` to build the law this project authors)`);
2545
+ out9(`local (no build/*.deploy.json here \u2014 run \`githolon compile\` to build the law this project authors)`);
2321
2546
  return 0;
2322
2547
  }
2323
- out8(`local ${localHash.slice(0, 16)}\u2026 (build/*.deploy.json)`);
2548
+ out9(`local ${localHash.slice(0, 16)}\u2026 (build/*.deploy.json)`);
2324
2549
  const verdict = lawDiffVerdict(localHash, domains, ws);
2325
- for (const line of verdict.lines) out8(line);
2550
+ for (const line of verdict.lines) out9(line);
2326
2551
  return verdict.inSync ? 0 : 2;
2327
2552
  }
2328
2553
 
@@ -2353,18 +2578,32 @@ Identity (~/.holon/credentials.json \u2014 sessions auto-refresh):
2353
2578
  githolon login --token <refresh_token> adopt an existing captain app session
2354
2579
  githolon whoami show the principal births will ride
2355
2580
  githolon logout clear the session (workspace secrets kept)
2581
+ githolon key enroll [--principal <uid>] [--parent <ws>] mint a signing device key + enroll it on-chain
2582
+ (secret stays local; warrants SIGNED governance)
2583
+ githolon key show [--principal <uid>] the device key's public identity (never the secret)
2584
+
2585
+ Governance (SIGNED client-side, relayed as {intentBytes} \u2014 never a host-author endpoint):
2586
+ githolon grant <admin|creation> <principal> [--label L] grant authority at the parent (default root)
2587
+ githolon grant quota <principal> --max <N> grant a per-principal workspace allowance
2588
+ githolon revoke <admin|creation> <principal> revoke it (the revoked fact rides the chain)
2589
+ (all gated by the parent's law: only an admin's
2590
+ signature is admitted; --parent <p> targets a platform)
2356
2591
 
2357
- Nomos Cloud (secrets live in ~/.holon/credentials.json \u2014 never copy one by hand;
2358
- a bare <ws> resolves from nomos.package.mjs's optional \`workspace\` binding):
2359
- githolon ws create <name> [--principal <uid|token>] birth a workspace; SAVES its one-time secret
2592
+ Nomos Cloud (a bare <ws> resolves from nomos.package.mjs's optional \`workspace\` binding;
2593
+ deploy is OPEN + kernel-gated \u2014 births return no host secret, ownership is a ledger fact):
2594
+ githolon ws create <name> [--principal <uid|token>] birth a workspace (no host secret is issued)
2360
2595
  (no identity on file? it self-onboards an agent login)
2596
+ githolon ws create <name> --via <parent> request it as a SIGNED createWorkspace governance
2597
+ offer at the parent
2361
2598
  githolon ws status [<name>] workspace status (lineage, phase, verdicts)
2362
- githolon ws retire <name> retire a workspace (owner) \u2014 quota freed; data retained
2363
- per the license; reads keep serving
2364
- githolon deploy [<ws>] [--file <deploy.json>] POST build/*.deploy.json with the stored secret;
2599
+ githolon ws retire <name> [--parent <ws>] retire a workspace via a SIGNED governance offer;
2600
+ quota freed; data retained; reads keep serving
2601
+ githolon ws reclaim <name> [--parent <ws>] lawful custody discard via a SIGNED governance offer
2602
+ (only a retired workspace; never root)
2603
+ githolon deploy [<ws>] [--file <deploy.json>] POST build/*.deploy.json (open deploy lane);
2365
2604
  prints what moved (old \u2192 new law hash)
2366
- githolon secret set <ws> <secret> import a secret you already hold
2367
- githolon secret rotate <ws> rotate (and re-save) the workspace secret
2605
+ githolon secret set <ws> <push-password> store a git push password (push-to-create birth lane)
2606
+ githolon secret rotate <ws> RETIRED \u2014 host holds no ownership credential (410)
2368
2607
 
2369
2608
  Git lane (stock git push as deploy/birth \u2014 see also: push-to-create):
2370
2609
  githolon git setup install the credential helper for the cloud host
@@ -2387,15 +2626,23 @@ Options:
2387
2626
  function cloudArgs(rest) {
2388
2627
  const pos = [];
2389
2628
  const opts = {};
2629
+ const VALUE_FLAGS = ["--cloud", "--principal", "--file", "--target", "--parent", "--via", "--max", "--label"];
2390
2630
  for (let i = 0; i < rest.length; i++) {
2391
2631
  const a = rest[i];
2392
- if (a === "--cloud" || a === "--principal" || a === "--file" || a === "--target") {
2632
+ if (VALUE_FLAGS.includes(a)) {
2393
2633
  const v = rest[++i];
2394
2634
  if (v === void 0) return { pos, opts, bad: `${a} requires a value` };
2395
2635
  if (a === "--cloud") opts.cloud = v;
2396
2636
  else if (a === "--principal") opts.principal = v;
2397
2637
  else if (a === "--target") opts.target = v;
2398
- else opts.file = v;
2638
+ else if (a === "--parent") opts.parent = v;
2639
+ else if (a === "--via") opts.via = v;
2640
+ else if (a === "--label") opts.label = v;
2641
+ else if (a === "--max") {
2642
+ const n = Number(v);
2643
+ if (!Number.isInteger(n) || n <= 0) return { pos, opts, bad: `--max requires a positive integer` };
2644
+ opts.max = n;
2645
+ } else opts.file = v;
2399
2646
  } else if (a.startsWith("--")) {
2400
2647
  return { pos, opts, bad: `unknown flag '${a}'` };
2401
2648
  } else {
@@ -2428,7 +2675,8 @@ ${USAGE}`);
2428
2675
  }
2429
2676
  }
2430
2677
  if (sub2 === "retire" && name !== void 0) return wsRetire(name, opts);
2431
- return usageFail("expected: githolon ws <create|status|retire> <name>");
2678
+ if (sub2 === "reclaim" && name !== void 0) return wsReclaim(name, opts);
2679
+ return usageFail("expected: githolon ws <create|status|retire|reclaim> <name>");
2432
2680
  }
2433
2681
  if (command === "deploy") {
2434
2682
  try {
@@ -2700,6 +2948,40 @@ ${commandHelp("status")}`);
2700
2948
  if (argv[0] === "ws" || argv[0] === "deploy" || argv[0] === "secret") {
2701
2949
  return runCloud(argv);
2702
2950
  }
2951
+ if (argv[0] === "key") {
2952
+ const { pos, opts, bad } = cloudArgs(argv.slice(1));
2953
+ if (bad !== void 0) {
2954
+ process.stderr.write(`error: ${bad}
2955
+
2956
+ ${USAGE}`);
2957
+ return 1;
2958
+ }
2959
+ const [sub] = pos;
2960
+ if (sub === "enroll") return keyEnroll(opts);
2961
+ if (sub === "show") return keyShow(opts);
2962
+ process.stderr.write(`error: expected: githolon key <enroll|show> [--principal <uid>] [--parent <ws>]
2963
+
2964
+ ${USAGE}`);
2965
+ return 1;
2966
+ }
2967
+ if (argv[0] === "grant" || argv[0] === "revoke") {
2968
+ const verb = argv[0];
2969
+ const { pos, opts, bad } = cloudArgs(argv.slice(1));
2970
+ if (bad !== void 0) {
2971
+ process.stderr.write(`error: ${bad}
2972
+
2973
+ ${USAGE}`);
2974
+ return 1;
2975
+ }
2976
+ const [kind, subject] = pos;
2977
+ if (kind === void 0 || subject === void 0) {
2978
+ process.stderr.write(`error: expected: githolon ${verb} <admin|creation${verb === "grant" ? "|quota" : ""}> <principal>
2979
+
2980
+ ${USAGE}`);
2981
+ return 1;
2982
+ }
2983
+ return grantOrRevoke(verb, kind, subject, opts);
2984
+ }
2703
2985
  if (argv[0] === "login") {
2704
2986
  const agent = argv.includes("--agent");
2705
2987
  const tokenAt = argv.indexOf("--token");
@@ -2756,8 +3038,8 @@ ${USAGE}`);
2756
3038
  let parsed;
2757
3039
  try {
2758
3040
  parsed = parseArgs(argv);
2759
- } catch (err9) {
2760
- process.stderr.write(`error: ${err9.message}
3041
+ } catch (err10) {
3042
+ process.stderr.write(`error: ${err10.message}
2761
3043
 
2762
3044
  ${USAGE}`);
2763
3045
  return 1;
@@ -2776,8 +3058,8 @@ ${USAGE}`);
2776
3058
  process.stdout.write(`created ${path}
2777
3059
  `);
2778
3060
  return 0;
2779
- } catch (err9) {
2780
- process.stderr.write(`error: ${err9.message}
3061
+ } catch (err10) {
3062
+ process.stderr.write(`error: ${err10.message}
2781
3063
  `);
2782
3064
  return 1;
2783
3065
  }