githolon 0.21.0 → 0.22.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 +282 -37
  2. package/package.json +3 -3
package/dist/cli.mjs CHANGED
@@ -12,12 +12,18 @@ var __export = (target, all) => {
12
12
  // src/governance.ts
13
13
  var governance_exports = {};
14
14
  __export(governance_exports, {
15
+ governanceOffer: () => governanceOffer,
16
+ governanceRelay: () => governanceRelay,
15
17
  grantOrRevoke: () => grantOrRevoke,
16
18
  keyEnroll: () => keyEnroll,
19
+ keyExport: () => keyExport,
20
+ keyImport: () => keyImport,
17
21
  keyShow: () => keyShow,
18
22
  resolveGovernancePrincipal: () => resolveGovernancePrincipal,
19
23
  signAndPostGovernance: () => signAndPostGovernance
20
24
  });
25
+ import { createHash, createPrivateKey, createPublicKey } from "node:crypto";
26
+ import { readFileSync, writeFileSync as writeFileSync2 } from "node:fs";
21
27
  import { connect } from "@githolon/client";
22
28
  async function connectGovernance(cloud, parent) {
23
29
  const authToken = await sessionToken().catch(() => void 0);
@@ -155,6 +161,174 @@ async function keyShow(opts) {
155
161
  out(` keyHash ${device.keyHash} (the on-chain key:<hash> leaf; secret stays local)`);
156
162
  return 0;
157
163
  }
164
+ function deviceKeyFromSeed(seed) {
165
+ if (seed.length !== 32) throw new Error(`an ed25519 seed is 32 bytes; got ${seed.length}`);
166
+ const pkcs8 = Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), seed]);
167
+ const pub = Buffer.from(
168
+ createPublicKey(createPrivateKey({ key: pkcs8, format: "der", type: "pkcs8" })).export({ format: "der", type: "spki" }).subarray(-32)
169
+ );
170
+ return { secret: seed.toString("base64"), public: pub.toString("base64"), keyHash: createHash("sha256").update(pub).digest("hex") };
171
+ }
172
+ function seedFromInput(input, opts) {
173
+ const v = input.trim();
174
+ if (opts.sha256 === true) return createHash("sha256").update(v).digest();
175
+ if (opts.format === "hex" || opts.format === void 0 && /^[0-9a-fA-F]{64}$/.test(v)) return Buffer.from(v, "hex");
176
+ if (opts.format === "base64" || opts.format === void 0) {
177
+ const b = Buffer.from(v, "base64");
178
+ if (b.length === 32) return b;
179
+ if (opts.format === "base64") throw new Error(`--secret base64 decoded to ${b.length} bytes, not a 32-byte seed`);
180
+ }
181
+ throw new Error(
182
+ "--secret is not a 32-byte ed25519 seed \u2014 pass a base64 seed, a 64-hex seed (--format hex is auto-detected), or --sha256 to derive the seed from a passphrase/preimage"
183
+ );
184
+ }
185
+ async function keyImport(opts) {
186
+ const principal = opts.principal ?? currentPrincipal();
187
+ if (principal === void 0) {
188
+ err("key import needs --principal <uid> (the auth-uid this key authors as)");
189
+ return 1;
190
+ }
191
+ if (opts.secret === void 0 || opts.secret === "") {
192
+ err("key import needs --secret <hex|base64 ed25519 seed> (or --sha256 to derive the seed from a passphrase)");
193
+ return 1;
194
+ }
195
+ let key;
196
+ try {
197
+ key = deviceKeyFromSeed(seedFromInput(opts.secret, opts));
198
+ } catch (e) {
199
+ err(e.message);
200
+ return 1;
201
+ }
202
+ const existing = getDeviceKey(principal);
203
+ if (existing !== void 0 && existing.keyHash !== key.keyHash && opts.yes !== true) {
204
+ err(`a DIFFERENT key is already stored for ${principal} (keyHash ${existing.keyHash.slice(0, 12)}\u2026) \u2014 re-run with --yes to replace it`);
205
+ return 1;
206
+ }
207
+ setDeviceKey(principal, key);
208
+ out(`\u2713 imported signing key for ${principal}`);
209
+ out(` keyHash ${key.keyHash}`);
210
+ out(` public ${key.public} (secret stored 0600 in ~/.holon)`);
211
+ out(` now: githolon gov <ws> <directive> --as ${principal} (if not yet enrolled there: githolon key enroll --principal ${principal} --parent <ws>)`);
212
+ return 0;
213
+ }
214
+ async function keyExport(opts) {
215
+ const principal = opts.principal ?? currentPrincipal();
216
+ if (principal === void 0) {
217
+ err("key export needs --principal <uid>");
218
+ return 1;
219
+ }
220
+ const device = getDeviceKey(principal);
221
+ if (device === void 0) {
222
+ out(`no signing key on file for ${principal}`);
223
+ return 0;
224
+ }
225
+ if (opts.yes !== true) {
226
+ err(`key export prints the SECRET seed for ${principal} to stdout \u2014 re-run with --yes to confirm`);
227
+ return 1;
228
+ }
229
+ out(`signing key for ${principal} (KEEP SECRET):`);
230
+ out(` secret ${device.secret} (base64 ed25519 seed \u2014 = githolon key import --secret <this>)`);
231
+ out(` public ${device.public}`);
232
+ out(` keyHash ${device.keyHash}`);
233
+ return 0;
234
+ }
235
+ function buildOfferPayload(opts) {
236
+ const raw = opts.payloadFile !== void 0 ? readFileSync(opts.payloadFile, "utf8") : opts.payload;
237
+ let payload = {};
238
+ if (raw !== void 0 && raw.trim() !== "") payload = JSON.parse(raw);
239
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
240
+ throw new Error("payload must be a JSON object");
241
+ }
242
+ const p = payload;
243
+ if (opts.stamp === true) {
244
+ const now = Date.now();
245
+ for (const f of ["requestedAt", "grantedAt", "revokedAt", "setAt"]) if (!(f in p)) p[f] = now;
246
+ if (!("bornAt" in p)) p["bornAt"] = new Date(now).toISOString();
247
+ }
248
+ return p;
249
+ }
250
+ async function governanceOffer(ws, directiveId, opts) {
251
+ let payload;
252
+ try {
253
+ payload = buildOfferPayload(opts);
254
+ } catch (e) {
255
+ err(`--payload: ${e.message}`);
256
+ return 1;
257
+ }
258
+ let principal;
259
+ try {
260
+ principal = resolveGovernancePrincipal(opts);
261
+ } catch (e) {
262
+ err(e.message);
263
+ return 1;
264
+ }
265
+ if (opts.save !== void 0) {
266
+ try {
267
+ const holon = await connectGovernance(cloudBase(opts.cloud), ws);
268
+ const device = await ensureDeviceKey(holon, principal);
269
+ const sealed = await holon.signGovernanceOffer({ directiveId, payload, authorSecret: device.secret, actor: `user:${principal}` });
270
+ writeFileSync2(opts.save, JSON.stringify({ ws, directiveId, principal, intentBytes: sealed.intentBytes }, null, 2) + "\n");
271
+ } catch (e) {
272
+ err(e.message);
273
+ return 1;
274
+ }
275
+ out(`\u2713 authored + signed ${directiveId} for ${ws} \u2192 ${opts.save} (not relayed)`);
276
+ out(` relay it (the only step that needs the cloud): githolon gov relay ${ws} ${opts.save}`);
277
+ return 0;
278
+ }
279
+ let v;
280
+ try {
281
+ v = await signAndPostGovernance(ws, directiveId, payload, opts);
282
+ } catch (e) {
283
+ err(e.message);
284
+ return 1;
285
+ }
286
+ if (!v.ok) {
287
+ err(`${directiveId} refused (${v.status}): ${typeof v.body === "string" ? v.body : JSON.stringify(v.body)}`);
288
+ return 1;
289
+ }
290
+ const head = v.body?.head;
291
+ out(`\u2713 ${directiveId} admitted at ${ws} via signed offer (as ${principal})${head !== void 0 ? ` \u2014 head ${head.slice(0, 12)}\u2026` : ""}`);
292
+ return 0;
293
+ }
294
+ async function governanceRelay(ws, file, opts) {
295
+ let saved;
296
+ try {
297
+ saved = JSON.parse(readFileSync(file, "utf8"));
298
+ } catch (e) {
299
+ err(`cannot read offer file ${file}: ${e.message}`);
300
+ return 1;
301
+ }
302
+ const { directiveId, intentBytes } = saved;
303
+ if (typeof directiveId !== "string" || typeof intentBytes !== "string") {
304
+ err(`${file} is not a saved offer (expected { directiveId, intentBytes })`);
305
+ return 1;
306
+ }
307
+ const cloud = cloudBase(opts.cloud);
308
+ let r;
309
+ try {
310
+ r = await fetch(`${cloud}/v2/workspaces/${ws}/${directiveId}`, {
311
+ method: "POST",
312
+ headers: { "content-type": "application/json" },
313
+ body: JSON.stringify({ intentBytes })
314
+ });
315
+ } catch (e) {
316
+ err(`relay to ${ws} failed: ${e.message}`);
317
+ return 1;
318
+ }
319
+ const text = await r.text();
320
+ if (!r.ok) {
321
+ err(`${directiveId} refused (${r.status}): ${text}`);
322
+ return 1;
323
+ }
324
+ let head;
325
+ try {
326
+ head = JSON.parse(text).head;
327
+ } catch {
328
+ }
329
+ out(`\u2713 relayed ${directiveId} to ${ws}${head !== void 0 ? ` \u2014 head ${head.slice(0, 12)}\u2026` : ""}`);
330
+ return 0;
331
+ }
158
332
  var out, err;
159
333
  var init_governance = __esm({
160
334
  "src/governance.ts"() {
@@ -192,7 +366,7 @@ __export(cloud_exports, {
192
366
  wsRetire: () => wsRetire,
193
367
  wsStatus: () => wsStatus
194
368
  });
195
- import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync, writeFileSync as writeFileSync2 } from "node:fs";
369
+ import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
196
370
  import { join as join2 } from "node:path";
197
371
  import { homedir } from "node:os";
198
372
  function cloudBase(flag) {
@@ -212,11 +386,11 @@ function credsPath() {
212
386
  }
213
387
  function loadCreds() {
214
388
  if (!existsSync2(credsPath())) return { version: 1, secrets: {} };
215
- return JSON.parse(readFileSync(credsPath(), "utf8"));
389
+ return JSON.parse(readFileSync2(credsPath(), "utf8"));
216
390
  }
217
391
  function saveCreds(c) {
218
392
  mkdirSync2(configDir(), { recursive: true });
219
- writeFileSync2(credsPath(), JSON.stringify(c, null, 2) + "\n", "utf8");
393
+ writeFileSync3(credsPath(), JSON.stringify(c, null, 2) + "\n", "utf8");
220
394
  chmodSync(credsPath(), 384);
221
395
  }
222
396
  function getSecret(cloud, ws) {
@@ -475,7 +649,7 @@ async function deploy(ws, opts) {
475
649
  const r = await fetch(`${cloud}/v2/workspaces/${ws}/domains`, {
476
650
  method: "POST",
477
651
  headers: { "content-type": "application/json" },
478
- body: readFileSync(file, "utf8")
652
+ body: readFileSync2(file, "utf8")
479
653
  });
480
654
  const d = await r.json().catch(() => void 0);
481
655
  if (!r.ok || d?.ok !== true) {
@@ -1028,7 +1202,7 @@ var init_engine = __esm({
1028
1202
 
1029
1203
  // src/local_holon.ts
1030
1204
  import { randomBytes } from "node:crypto";
1031
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync, writeFileSync as writeFileSync3 } from "node:fs";
1205
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync4 } from "node:fs";
1032
1206
  import { dirname as dirname2, join as join4 } from "node:path";
1033
1207
  import { File as File2, Directory as Directory2 } from "@bjorn3/browser_wasi_shim";
1034
1208
  async function fetchJsonCached(url, cacheFile) {
@@ -1037,10 +1211,10 @@ async function fetchJsonCached(url, cacheFile) {
1037
1211
  if (!r.ok) throw new Error(`${url} \u2192 ${r.status}`);
1038
1212
  const text = await r.text();
1039
1213
  mkdirSync3(dirname2(cacheFile), { recursive: true });
1040
- writeFileSync3(cacheFile, text, "utf8");
1214
+ writeFileSync4(cacheFile, text, "utf8");
1041
1215
  return JSON.parse(text);
1042
1216
  } catch (e) {
1043
- if (existsSync3(cacheFile)) return JSON.parse(readFileSync2(cacheFile, "utf8"));
1217
+ if (existsSync3(cacheFile)) return JSON.parse(readFileSync3(cacheFile, "utf8"));
1044
1218
  throw e;
1045
1219
  }
1046
1220
  }
@@ -1050,7 +1224,7 @@ async function fetchRuntime(cloud) {
1050
1224
  const wasmCache = join4(cache, "holon.wasm");
1051
1225
  const localWasm = process.env["NOMOS_LOCAL_WASM"];
1052
1226
  if (localWasm) {
1053
- const bytes = readFileSync2(localWasm);
1227
+ const bytes = readFileSync3(localWasm);
1054
1228
  const packages2 = await fetchJsonCached(`${cloud}/v1/runtime/packages`, join4(cache, "packages.json"));
1055
1229
  return { wasmBytes: bytes, packages: packages2 };
1056
1230
  }
@@ -1059,10 +1233,10 @@ async function fetchRuntime(cloud) {
1059
1233
  if (!r.ok) throw new Error(`runtime wasm \u2192 ${r.status}`);
1060
1234
  wasmBytes = new Uint8Array(await r.arrayBuffer());
1061
1235
  mkdirSync3(cache, { recursive: true });
1062
- writeFileSync3(wasmCache, wasmBytes);
1236
+ writeFileSync4(wasmCache, wasmBytes);
1063
1237
  } catch (e) {
1064
1238
  if (!existsSync3(wasmCache)) throw e;
1065
- wasmBytes = readFileSync2(wasmCache);
1239
+ wasmBytes = readFileSync3(wasmCache);
1066
1240
  }
1067
1241
  const packages = await fetchJsonCached(`${cloud}/v1/runtime/packages`, join4(cache, "packages.json"));
1068
1242
  return { wasmBytes, packages };
@@ -1072,7 +1246,7 @@ function writeTreeToDisk(dir, diskPath) {
1072
1246
  for (const [name, inode] of dir.contents) {
1073
1247
  const p = join4(diskPath, name);
1074
1248
  if (inode.contents instanceof Map) writeTreeToDisk(inode, p);
1075
- else writeFileSync3(p, inode.data ?? new Uint8Array(0));
1249
+ else writeFileSync4(p, inode.data ?? new Uint8Array(0));
1076
1250
  }
1077
1251
  }
1078
1252
  function readTreeFromDisk(diskPath) {
@@ -1080,7 +1254,7 @@ function readTreeFromDisk(diskPath) {
1080
1254
  for (const name of readdirSync2(diskPath)) {
1081
1255
  const p = join4(diskPath, name);
1082
1256
  if (statSync(p).isDirectory()) contents.set(name, readTreeFromDisk(p));
1083
- else contents.set(name, new File2(readFileSync2(p)));
1257
+ else contents.set(name, new File2(readFileSync3(p)));
1084
1258
  }
1085
1259
  return new Directory2(contents);
1086
1260
  }
@@ -1137,7 +1311,7 @@ __export(proof_offline_exports, {
1137
1311
  runOfflineLegs: () => runOfflineLegs,
1138
1312
  runOfflineProofStandalone: () => runOfflineProofStandalone
1139
1313
  });
1140
- import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
1314
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
1141
1315
  import { basename, dirname as dirname3, join as join6 } from "node:path";
1142
1316
  function parseOfflineLegs(proofSource) {
1143
1317
  if (!proofSource.includes("AUTO-GENERATED by nomos-compile")) {
@@ -1300,7 +1474,7 @@ function runChildBirthLeg(eng, ws, lawHash, cb, frameworkHash) {
1300
1474
  return done(true);
1301
1475
  }
1302
1476
  async function runOfflineProofStandalone(proofPath, cloud, domain) {
1303
- const src = readFileSync3(proofPath, "utf8");
1477
+ const src = readFileSync4(proofPath, "utf8");
1304
1478
  const packageName = basename(proofPath).replace(/\.proof\.mts$/, "");
1305
1479
  const deployFile = join6(dirname3(proofPath), `${packageName}.deploy.json`);
1306
1480
  if (!existsSync5(deployFile)) {
@@ -1309,7 +1483,7 @@ async function runOfflineProofStandalone(proofPath, cloud, domain) {
1309
1483
  let legs;
1310
1484
  const legsFile = join6(dirname3(proofPath), `${packageName}.proof-legs.json`);
1311
1485
  if (existsSync5(legsFile)) {
1312
- const index = JSON.parse(readFileSync3(legsFile, "utf8"));
1486
+ const index = JSON.parse(readFileSync4(legsFile, "utf8"));
1313
1487
  const keys = Object.keys(index.domains);
1314
1488
  const chosen = domain ?? index.default;
1315
1489
  if (index.domains[chosen] === void 0) {
@@ -1327,7 +1501,7 @@ async function runOfflineProofStandalone(proofPath, cloud, domain) {
1327
1501
  if (domain !== void 0) throw new Error(`--domain needs a per-domain legs index (build/${packageName}.proof-legs.json) \u2014 recompile with \`githolon compile\` to emit it`);
1328
1502
  legs = parseOfflineLegs(src);
1329
1503
  }
1330
- const deployBody = JSON.parse(readFileSync3(deployFile, "utf8"));
1504
+ const deployBody = JSON.parse(readFileSync4(deployFile, "utf8"));
1331
1505
  const lawHash = await sha256hex(deployBody.packageUsda);
1332
1506
  console.log(`githolon proof \u2014 OFFLINE legs on a local holon (the cloud loop: githolon proof --live)`);
1333
1507
  const { eng } = await holonEngine(cloud, deployBody, "githolon-proof");
@@ -1355,7 +1529,7 @@ var init_proof_offline = __esm({
1355
1529
  });
1356
1530
 
1357
1531
  // src/cli.ts
1358
- import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync9 } from "node:fs";
1532
+ import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync10 } from "node:fs";
1359
1533
  import { spawnSync as spawnSync4 } from "node:child_process";
1360
1534
  import { createRequire as createRequire2 } from "node:module";
1361
1535
  import { dirname as dirname5, join as join10, resolve as resolve2 } from "node:path";
@@ -1594,7 +1768,7 @@ function compileAsync(cwd, args = []) {
1594
1768
  // src/dev.ts
1595
1769
  init_engine();
1596
1770
  init_cloud();
1597
- import { existsSync as existsSync6, readFileSync as readFileSync4, watch } from "node:fs";
1771
+ import { existsSync as existsSync6, readFileSync as readFileSync5, watch } from "node:fs";
1598
1772
  import { createServer } from "node:http";
1599
1773
  import { dirname as dirname4, join as join7, resolve } from "node:path";
1600
1774
  import { pathToFileURL as pathToFileURL3 } from "node:url";
@@ -1776,7 +1950,7 @@ function readDeployBody(cwd, packageName) {
1776
1950
  if (!existsSync6(file)) {
1777
1951
  throw new Error(`compile produced no ${file} \u2014 check the compile output above`);
1778
1952
  }
1779
- return JSON.parse(readFileSync4(file, "utf8"));
1953
+ return JSON.parse(readFileSync5(file, "utf8"));
1780
1954
  }
1781
1955
  async function installLaw(s, deployBody, installedBy) {
1782
1956
  const newHash = await sha256hex(deployBody.packageUsda);
@@ -1796,7 +1970,7 @@ async function runProofCycle(s, cwd, deployBody, installedBy) {
1796
1970
  if (!existsSync6(proofPath)) return void 0;
1797
1971
  const scratch = `proof-${s.cycle}`;
1798
1972
  try {
1799
- const legs = parseOfflineLegs(readFileSync4(proofPath, "utf8"));
1973
+ const legs = parseOfflineLegs(readFileSync5(proofPath, "utf8"));
1800
1974
  await mountFresh(s.eng, scratch);
1801
1975
  author(s.eng, scratch, "bootstrap", "installDomain", installPayload(s.eng.hashes.nomos, s.eng.nomosPkg, installedBy), "");
1802
1976
  author(s.eng, scratch, "nomos", "installDomain", installPayload(s.lawHash, deployBody.packageUsda, installedBy), s.eng.hashes.nomos);
@@ -1964,7 +2138,7 @@ ${run.output}`);
1964
2138
  init_cloud();
1965
2139
  import { spawnSync as spawnSync2 } from "node:child_process";
1966
2140
  import { randomBytes as randomBytes2 } from "node:crypto";
1967
- import { readFileSync as readFileSync5 } from "node:fs";
2141
+ import { readFileSync as readFileSync6 } from "node:fs";
1968
2142
  var out5 = (s) => void process.stdout.write(s + "\n");
1969
2143
  var err5 = (s) => void process.stderr.write("error: " + s + "\n");
1970
2144
  function git(args, opts = {}) {
@@ -1993,7 +2167,7 @@ function workspaceFromPath(path) {
1993
2167
  }
1994
2168
  async function gitCredential(action) {
1995
2169
  if (action !== "get") return 0;
1996
- const kv = parseCredentialInput(readFileSync5(0, "utf8"));
2170
+ const kv = parseCredentialInput(readFileSync6(0, "utf8"));
1997
2171
  const ws = workspaceFromPath(kv["path"]);
1998
2172
  if (kv["protocol"] !== "https" || ws === void 0) return 0;
1999
2173
  const cloud = `https://${kv["host"]}`;
@@ -2145,14 +2319,41 @@ var HELP = {
2145
2319
  examples: ["githolon ws create my-guestbook", "githolon ws create app1 --via co2", "githolon ws status", "githolon ws retire my-guestbook"]
2146
2320
  },
2147
2321
  key: {
2148
- usage: "githolon key <enroll|show> [--principal <uid>] [--parent <ws>] [--cloud <url>]",
2149
- 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.",
2322
+ usage: "githolon key <enroll|show|import|export> [--principal <uid>] [--secret <key>] [--sha256] [--format hex|base64] [--parent <ws>] [--yes]",
2323
+ 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. `import` brings\nan EXISTING admin key you already hold into ~/.holon \u2014 pass a base64 seed, a 64-hex seed\n(auto-detected; or --format hex), or --sha256 to derive the seed from a passphrase/preimage.\n`show` prints the public identity; `export --yes` prints the SECRET seed (backup/move). The\nsecret stays in ~/.holon/credentials.json (0600) \u2014 never on chain.",
2150
2324
  flags: [
2151
2325
  ["--principal <p>", "the principal the key authors as (default: the logged-in session uid)"],
2152
- ["--parent <ws>", "the governance workspace to enroll at (default root)"],
2326
+ ["--secret <key>", "import: the ed25519 seed (base64 or 64-hex), or a passphrase with --sha256"],
2327
+ ["--sha256", "import: derive the seed as sha256(--secret) (a passphrase/preimage)"],
2328
+ ["--format <f>", "import: force hex|base64 interpretation (default: auto-detect)"],
2329
+ ["--parent <ws>", "enroll: the governance workspace to enroll at (default root)"],
2330
+ ["--yes", "export: confirm printing the secret to stdout"]
2331
+ ],
2332
+ examples: [
2333
+ "githolon key enroll",
2334
+ "githolon key import --principal 32c7100e-\u2026 --secret 21c23da3\u2026 # a 64-hex admin seed",
2335
+ "githolon key import --principal d205ca94-\u2026 --secret 'copak_\u2026' --sha256",
2336
+ "githolon key show",
2337
+ "githolon key export --principal 32c7100e-\u2026 --yes"
2338
+ ]
2339
+ },
2340
+ gov: {
2341
+ usage: "githolon gov <ws> <directiveId> [--payload <json> | --payload-file <f>] [--as <uid>] [--stamp] [--save <f>] [--cloud <url>]\n githolon gov relay <ws> <offer-file> [--cloud <url>]",
2342
+ what: "Author ANY `workspaces` governance directive as a SIGNED offer to <ws> and relay it \u2014 the generic\nlane the grant/ws/key verbs are sugar over. The directive is planned against <ws>'s OWN folded law\n(we connect + sign locally with your enrolled key; the host relays opaque {intentBytes} and the\nkernel re-verifies the signature). SIGNING IS LOCAL: with --save it writes the sealed offer WITHOUT\nrelaying \u2014 sign now, relay later/elsewhere with `gov relay` (the only step that touches the cloud).\nThe acting principal needs an enrolled signing key (githolon key enroll | key import).",
2343
+ flags: [
2344
+ ["--payload <json>", `the directive payload as a JSON object string ('{"name":"\u2026"}')`],
2345
+ ["--payload-file <f>", "read the payload JSON from a file instead"],
2346
+ ["--as <uid>", "the principal to author as (alias of --principal; default: session uid)"],
2347
+ ["--stamp", "fill any missing requestedAt/grantedAt/revokedAt/setAt (epoch ms) + bornAt (ISO) with now"],
2348
+ ["--save <f>", "author + sign, write the sealed offer to <f>, do NOT relay"],
2153
2349
  ["--cloud <url>", "target cloud"]
2154
2350
  ],
2155
- examples: ["githolon key enroll", "githolon key show"]
2351
+ examples: [
2352
+ `githolon gov root grantCreation --payload '{"principal":"d205ca94-\u2026"}' --as 32c7100e-\u2026 --stamp`,
2353
+ "githolon gov co2-platform birthHome --payload-file home.json --as d205ca94-\u2026",
2354
+ "githolon gov root birthChild --payload-file co2.json --as 32c7100e-\u2026 --save co2.offer.json",
2355
+ "githolon gov relay root co2.offer.json"
2356
+ ]
2156
2357
  },
2157
2358
  grant: {
2158
2359
  usage: "githolon grant <admin|creation|quota> <principal> [--max <N>] [--label <L>] [--parent <ws>] [--cloud <url>]",
@@ -2221,7 +2422,7 @@ init_engine();
2221
2422
  init_cloud();
2222
2423
  init_local_holon();
2223
2424
  import { spawnSync as spawnSync3 } from "node:child_process";
2224
- import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
2425
+ import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
2225
2426
  import { join as join8 } from "node:path";
2226
2427
  var out6 = (s) => void process.stdout.write(s + "\n");
2227
2428
  var err6 = (s) => void process.stderr.write("error: " + s + "\n");
@@ -2239,7 +2440,7 @@ async function ledgerInit(dir, opts) {
2239
2440
  err6(e.message);
2240
2441
  return 1;
2241
2442
  }
2242
- const deployBody = JSON.parse(readFileSync6(deployFile, "utf8"));
2443
+ const deployBody = JSON.parse(readFileSync7(deployFile, "utf8"));
2243
2444
  const domainHash = await sha256hex(deployBody.packageUsda);
2244
2445
  const c = loadCreds();
2245
2446
  const principal = await sessionToken().catch(() => void 0) !== void 0 ? c.session.uid : c.principal;
@@ -2258,7 +2459,7 @@ async function ledgerInit(dir, opts) {
2258
2459
  const gitDir = join8(dir, ".git");
2259
2460
  writeTreeToDisk(gitTree, gitDir);
2260
2461
  const cfgPath = join8(gitDir, "config");
2261
- writeFileSync4(cfgPath, readFileSync6(cfgPath, "utf8").replace(/bare = true/, "bare = false"), "utf8");
2462
+ writeFileSync5(cfgPath, readFileSync7(cfgPath, "utf8").replace(/bare = true/, "bare = false"), "utf8");
2262
2463
  spawnSync3("git", ["-C", dir, "reset", "--hard", "main"], { stdio: "ignore" });
2263
2464
  out6(`\u2713 holon written \u2192 ${dir} (a normal git repo; git log IS the audit trail)`);
2264
2465
  out6(`
@@ -2549,7 +2750,7 @@ async function decrypt(ws, opts) {
2549
2750
  // src/status.ts
2550
2751
  init_engine();
2551
2752
  init_cloud();
2552
- import { readFileSync as readFileSync8 } from "node:fs";
2753
+ import { readFileSync as readFileSync9 } from "node:fs";
2553
2754
  var out9 = (s) => void process.stdout.write(s + "\n");
2554
2755
  var err9 = (s) => void process.stderr.write("error: " + s + "\n");
2555
2756
  function lawDiffVerdict(localHash, installed, ws) {
@@ -2594,7 +2795,7 @@ async function status(wsArg, opts) {
2594
2795
  }
2595
2796
  let localHash;
2596
2797
  try {
2597
- const body = JSON.parse(readFileSync8(discoverDeployJson(process.cwd(), opts.file), "utf8"));
2798
+ const body = JSON.parse(readFileSync9(discoverDeployJson(process.cwd(), opts.file), "utf8"));
2598
2799
  if (typeof body.packageUsda === "string") localHash = await sha256hex(body.packageUsda);
2599
2800
  } catch {
2600
2801
  localHash = void 0;
@@ -2662,9 +2863,14 @@ Identity (~/.holon/credentials.json \u2014 sessions auto-refresh):
2662
2863
  githolon logout clear the session (workspace secrets kept)
2663
2864
  githolon key enroll [--principal <uid>] [--parent <ws>] mint a signing device key + enroll it on-chain
2664
2865
  (secret stays local; warrants SIGNED governance)
2665
- githolon key show [--principal <uid>] the device key's public identity (never the secret)
2866
+ githolon key import --principal <uid> --secret <key> bring an EXISTING admin key into ~/.holon (accepts
2867
+ hex / base64 seed, or --sha256 <passphrase>)
2868
+ githolon key show | export [--principal <uid>] the device key's public identity (export --yes: secret)
2666
2869
 
2667
2870
  Governance (SIGNED client-side, relayed as {intentBytes} \u2014 never a host-author endpoint):
2871
+ githolon gov <ws> <directive> [--payload <json>] author ANY workspaces directive as a signed offer
2872
+ [--as <uid>] [--stamp] [--save <file>] to <ws> (--save signs without relaying; relay later)
2873
+ githolon gov relay <ws> <offer-file> relay a saved offer (the only step that hits the cloud)
2668
2874
  githolon grant <admin|creation> <principal> [--label L] grant authority at the parent (default root)
2669
2875
  githolon grant quota <principal> --max <N> grant a per-principal workspace allowance
2670
2876
  githolon revoke <admin|creation> <principal> revoke it (the revoked fact rides the chain)
@@ -2708,18 +2914,28 @@ Options:
2708
2914
  function cloudArgs(rest) {
2709
2915
  const pos = [];
2710
2916
  const opts = {};
2711
- const VALUE_FLAGS = ["--cloud", "--principal", "--file", "--target", "--parent", "--via", "--max", "--label"];
2917
+ const VALUE_FLAGS = ["--cloud", "--principal", "--as", "--file", "--target", "--parent", "--via", "--max", "--label", "--secret", "--payload", "--payload-file", "--save", "--format"];
2918
+ const BOOL_FLAGS = ["--sha256", "--stamp", "--yes"];
2712
2919
  for (let i = 0; i < rest.length; i++) {
2713
2920
  const a = rest[i];
2714
- if (VALUE_FLAGS.includes(a)) {
2921
+ if (BOOL_FLAGS.includes(a)) {
2922
+ if (a === "--sha256") opts.sha256 = true;
2923
+ else if (a === "--stamp") opts.stamp = true;
2924
+ else opts.yes = true;
2925
+ } else if (VALUE_FLAGS.includes(a)) {
2715
2926
  const v = rest[++i];
2716
2927
  if (v === void 0) return { pos, opts, bad: `${a} requires a value` };
2717
2928
  if (a === "--cloud") opts.cloud = v;
2718
- else if (a === "--principal") opts.principal = v;
2929
+ else if (a === "--principal" || a === "--as") opts.principal = v;
2719
2930
  else if (a === "--target") opts.target = v;
2720
2931
  else if (a === "--parent") opts.parent = v;
2721
2932
  else if (a === "--via") opts.via = v;
2722
2933
  else if (a === "--label") opts.label = v;
2934
+ else if (a === "--secret") opts.secret = v;
2935
+ else if (a === "--payload") opts.payload = v;
2936
+ else if (a === "--payload-file") opts.payloadFile = v;
2937
+ else if (a === "--save") opts.save = v;
2938
+ else if (a === "--format") opts.format = v;
2723
2939
  else if (a === "--max") {
2724
2940
  const n = Number(v);
2725
2941
  if (!Number.isInteger(n) || n <= 0) return { pos, opts, bad: `--max requires a positive integer` };
@@ -2849,7 +3065,7 @@ async function runProof(args) {
2849
3065
  if (tsxDir === void 0) {
2850
3066
  return refuse("tsx not found \u2014 add @githolon/dsl to your project's dependencies (tsx rides along) and npm install");
2851
3067
  }
2852
- const tsxPkg = JSON.parse(readFileSync9(join10(tsxDir, "package.json"), "utf8"));
3068
+ const tsxPkg = JSON.parse(readFileSync10(join10(tsxDir, "package.json"), "utf8"));
2853
3069
  const tsxBinRel = typeof tsxPkg.bin === "string" ? tsxPkg.bin : tsxPkg.bin?.["tsx"];
2854
3070
  const tsxCli = join10(tsxDir, tsxBinRel ?? "dist/cli.mjs");
2855
3071
  console.log("githolon proof --live \u2014 the full cloud loop (throwaway workspace, retired on exit" + (keep ? "; --keep: kept, secret printed once" : "") + ")");
@@ -3041,11 +3257,40 @@ ${USAGE}`);
3041
3257
  const [sub] = pos;
3042
3258
  if (sub === "enroll") return keyEnroll(opts);
3043
3259
  if (sub === "show") return keyShow(opts);
3044
- process.stderr.write(`error: expected: githolon key <enroll|show> [--principal <uid>] [--parent <ws>]
3260
+ if (sub === "import") return keyImport(opts);
3261
+ if (sub === "export") return keyExport(opts);
3262
+ process.stderr.write(`error: expected: githolon key <enroll|show|import|export> [--principal <uid>]
3045
3263
 
3046
3264
  ${USAGE}`);
3047
3265
  return 1;
3048
3266
  }
3267
+ if (argv[0] === "gov") {
3268
+ const { pos, opts, bad } = cloudArgs(argv.slice(1));
3269
+ if (bad !== void 0) {
3270
+ process.stderr.write(`error: ${bad}
3271
+
3272
+ ${commandHelp("gov") ?? USAGE}`);
3273
+ return 1;
3274
+ }
3275
+ if (pos[0] === "relay") {
3276
+ const [, ws2, file] = pos;
3277
+ if (ws2 === void 0 || file === void 0) {
3278
+ process.stderr.write(`error: expected: githolon gov relay <ws> <offer-file>
3279
+
3280
+ ${commandHelp("gov") ?? USAGE}`);
3281
+ return 1;
3282
+ }
3283
+ return governanceRelay(ws2, file, opts);
3284
+ }
3285
+ const [ws, directiveId] = pos;
3286
+ if (ws === void 0 || directiveId === void 0) {
3287
+ process.stderr.write(`error: expected: githolon gov <ws> <directiveId> [--payload <json>] [--as <uid>]
3288
+
3289
+ ${commandHelp("gov") ?? USAGE}`);
3290
+ return 1;
3291
+ }
3292
+ return governanceOffer(ws, directiveId, opts);
3293
+ }
3049
3294
  if (argv[0] === "grant" || argv[0] === "revoke") {
3050
3295
  const verb = argv[0];
3051
3296
  const { pos, opts, bad } = cloudArgs(argv.slice(1));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "githolon",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "type": "module",
5
5
  "description": "githolon — the Nomos developer CLI: Rails-style generators for @githolon/dsl domains + the package compiler. Kernel-independent.",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -27,8 +27,8 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@bjorn3/browser_wasi_shim": "0.4.2",
30
- "@githolon/client": "^0.21.0",
31
- "@githolon/dsl": "^0.21.0",
30
+ "@githolon/client": "^0.22.0",
31
+ "@githolon/dsl": "^0.22.0",
32
32
  "isomorphic-git": "^1.38.4"
33
33
  },
34
34
  "devDependencies": {