@ashlr/hub 3.5.1 → 3.7.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 (110) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/README.md +3 -0
  3. package/dist/build-identity.json +2 -2
  4. package/dist/cli/activation.d.ts +41 -0
  5. package/dist/cli/activation.js +485 -0
  6. package/dist/cli/activation.js.map +1 -0
  7. package/dist/core/daemon/cutoff-checkpoint-scheduler.js +7 -0
  8. package/dist/core/daemon/cutoff-checkpoint-scheduler.js.map +1 -1
  9. package/dist/core/daemon/loop.d.ts +19 -0
  10. package/dist/core/daemon/loop.js +235 -5
  11. package/dist/core/daemon/loop.js.map +1 -1
  12. package/dist/core/daemon/overnight-status.d.ts +152 -0
  13. package/dist/core/daemon/overnight-status.js +275 -0
  14. package/dist/core/daemon/overnight-status.js.map +1 -0
  15. package/dist/core/daemon/post-merge-halt.d.ts +212 -0
  16. package/dist/core/daemon/post-merge-halt.js +503 -0
  17. package/dist/core/daemon/post-merge-halt.js.map +1 -0
  18. package/dist/core/daemon/run-window.d.ts +218 -0
  19. package/dist/core/daemon/run-window.js +420 -0
  20. package/dist/core/daemon/run-window.js.map +1 -0
  21. package/dist/core/fleet/generated-repair-resolution.d.ts +36 -0
  22. package/dist/core/fleet/generated-repair-resolution.js +245 -0
  23. package/dist/core/fleet/generated-repair-resolution.js.map +1 -0
  24. package/dist/core/local-runtime/llama/port.d.ts +17 -0
  25. package/dist/core/local-runtime/llama/port.js +47 -0
  26. package/dist/core/local-runtime/llama/port.js.map +1 -0
  27. package/dist/core/local-runtime/llama/proxy-host.d.ts +54 -0
  28. package/dist/core/local-runtime/llama/proxy-host.js +159 -0
  29. package/dist/core/local-runtime/llama/proxy-host.js.map +1 -0
  30. package/dist/core/local-runtime/llama/proxy-process.d.ts +148 -0
  31. package/dist/core/local-runtime/llama/proxy-process.js +250 -0
  32. package/dist/core/local-runtime/llama/proxy-process.js.map +1 -0
  33. package/dist/core/local-runtime/llama/proxy-record.d.ts +40 -0
  34. package/dist/core/local-runtime/llama/proxy-record.js +133 -0
  35. package/dist/core/local-runtime/llama/proxy-record.js.map +1 -0
  36. package/dist/core/local-runtime/llama/proxy-supervisor.d.ts +118 -0
  37. package/dist/core/local-runtime/llama/proxy-supervisor.js +511 -0
  38. package/dist/core/local-runtime/llama/proxy-supervisor.js.map +1 -0
  39. package/dist/core/web/public/next/assets/{App-BbVoUCIi.js → App-vtaHB9mp.js} +2 -2
  40. package/dist/core/web/public/next/assets/{App-BbVoUCIi.js.map → App-vtaHB9mp.js.map} +1 -1
  41. package/dist/core/web/public/next/assets/{ApprovalsSection-BW9iVjUa.js → ApprovalsSection-B-bFJ0I9.js} +2 -2
  42. package/dist/core/web/public/next/assets/{ApprovalsSection-BW9iVjUa.js.map → ApprovalsSection-B-bFJ0I9.js.map} +1 -1
  43. package/dist/core/web/public/next/assets/{AutonomySection-Cpk_oEhc.css → AutonomySection-CHjxJt2z.css} +1 -1
  44. package/dist/core/web/public/next/assets/AutonomySection-DoqmsCjw.js +2 -0
  45. package/dist/core/web/public/next/assets/AutonomySection-DoqmsCjw.js.map +1 -0
  46. package/dist/core/web/public/next/assets/ChatSection-BfnLe5dT.css +1 -0
  47. package/dist/core/web/public/next/assets/ChatSection-vNPIJffd.js +80 -0
  48. package/dist/core/web/public/next/assets/ChatSection-vNPIJffd.js.map +1 -0
  49. package/dist/core/web/public/next/assets/{ConfirmDialog-CXevY3Au.js → ConfirmDialog-Dd2APQhB.js} +2 -2
  50. package/dist/core/web/public/next/assets/{ConfirmDialog-CXevY3Au.js.map → ConfirmDialog-Dd2APQhB.js.map} +1 -1
  51. package/dist/core/web/public/next/assets/{Dialog-Bn0CgEGz.js → Dialog-CPM9JxBc.js} +2 -2
  52. package/dist/core/web/public/next/assets/{Dialog-Bn0CgEGz.js.map → Dialog-CPM9JxBc.js.map} +1 -1
  53. package/dist/core/web/public/next/assets/{DiffViewer-QKyisN2L.js → DiffViewer-B84qVef9.js} +2 -2
  54. package/dist/core/web/public/next/assets/{DiffViewer-QKyisN2L.js.map → DiffViewer-B84qVef9.js.map} +1 -1
  55. package/dist/core/web/public/next/assets/McpSection-CEUpV_5X.js +2 -0
  56. package/dist/core/web/public/next/assets/McpSection-CEUpV_5X.js.map +1 -0
  57. package/dist/core/web/public/next/assets/McpSection-D88QlDpG.css +1 -0
  58. package/dist/core/web/public/next/assets/Meter--h3MmZS-.js +2 -0
  59. package/dist/core/web/public/next/assets/Meter--h3MmZS-.js.map +1 -0
  60. package/dist/core/web/public/next/assets/Meter-CLcr-4wS.css +1 -0
  61. package/dist/core/web/public/next/assets/{MutationTokenDialog-DbSHPy3w.js → MutationTokenDialog-DA8t97XT.js} +2 -2
  62. package/dist/core/web/public/next/assets/{MutationTokenDialog-DbSHPy3w.js.map → MutationTokenDialog-DA8t97XT.js.map} +1 -1
  63. package/dist/core/web/public/next/assets/{ResourcePoolConsoleApp-C-PCgCzo.js → ResourcePoolConsoleApp-Bq7Vyud0.js} +2 -2
  64. package/dist/core/web/public/next/assets/{ResourcePoolConsoleApp-C-PCgCzo.js.map → ResourcePoolConsoleApp-Bq7Vyud0.js.map} +1 -1
  65. package/dist/core/web/public/next/assets/{RouteErrorBoundary-Dw8ZjmkS.js → RouteErrorBoundary-Badio_uO.js} +2 -2
  66. package/dist/core/web/public/next/assets/{RouteErrorBoundary-Dw8ZjmkS.js.map → RouteErrorBoundary-Badio_uO.js.map} +1 -1
  67. package/dist/core/web/public/next/assets/SettingsSection-BwrpQlpP.js +2 -0
  68. package/dist/core/web/public/next/assets/SettingsSection-BwrpQlpP.js.map +1 -0
  69. package/dist/core/web/public/next/assets/{UniverseConsoleApp-Bk5HipM0.js → UniverseConsoleApp-_FSohAdN.js} +2 -2
  70. package/dist/core/web/public/next/assets/{UniverseConsoleApp-Bk5HipM0.js.map → UniverseConsoleApp-_FSohAdN.js.map} +1 -1
  71. package/dist/core/web/public/next/assets/{UniverseView-D8k9q0ce.js → UniverseView-DqcXbed4.js} +2 -2
  72. package/dist/core/web/public/next/assets/{UniverseView-D8k9q0ce.js.map → UniverseView-DqcXbed4.js.map} +1 -1
  73. package/dist/core/web/public/next/assets/{UsageSection-B2E06ZH1.js → UsageSection-Bq6Daaby.js} +2 -2
  74. package/dist/core/web/public/next/assets/{UsageSection-B2E06ZH1.js.map → UsageSection-Bq6Daaby.js.map} +1 -1
  75. package/dist/core/web/public/next/assets/VerseConsoleApp-CMkCizvL.css +1 -0
  76. package/dist/core/web/public/next/assets/VerseConsoleApp-CsIQv9id.js +3 -0
  77. package/dist/core/web/public/next/assets/VerseConsoleApp-CsIQv9id.js.map +1 -0
  78. package/dist/core/web/public/next/assets/{charts-DSUpczxG.js → charts-CTZJ-JrH.js} +2 -2
  79. package/dist/core/web/public/next/assets/{charts-DSUpczxG.js.map → charts-CTZJ-JrH.js.map} +1 -1
  80. package/dist/core/web/public/next/assets/global-D6bTxIRn.js +2 -0
  81. package/dist/core/web/public/next/assets/global-D6bTxIRn.js.map +1 -0
  82. package/dist/core/web/public/next/assets/global-z-JfD3ow.css +1 -0
  83. package/dist/core/web/public/next/assets/{icons-BxOX8-XB.js → icons-D9vlf85u.js} +2 -2
  84. package/dist/core/web/public/next/assets/{icons-BxOX8-XB.js.map → icons-D9vlf85u.js.map} +1 -1
  85. package/dist/core/web/public/next/assets/{index-BMRIET7F.js → index-CZamKRUg.js} +3 -3
  86. package/dist/core/web/public/next/assets/{index-BMRIET7F.js.map → index-CZamKRUg.js.map} +1 -1
  87. package/dist/core/web/public/next/assets/{queries-AxnVa02h.js → queries-B8eBnFox.js} +2 -2
  88. package/dist/core/web/public/next/assets/{queries-AxnVa02h.js.map → queries-B8eBnFox.js.map} +1 -1
  89. package/dist/core/web/public/next/assets/{use-guarded-action-BO6c94Wp.js → use-guarded-action-bzrVVAYq.js} +2 -2
  90. package/dist/core/web/public/next/assets/{use-guarded-action-BO6c94Wp.js.map → use-guarded-action-bzrVVAYq.js.map} +1 -1
  91. package/dist/core/web/public/next/assets/workspace-model-v0a1gl-C.js +2 -0
  92. package/dist/core/web/public/next/assets/workspace-model-v0a1gl-C.js.map +1 -0
  93. package/dist/core/web/public/next/index.html +1 -1
  94. package/dist/release-dependency-inventory.json +1 -1
  95. package/package.json +1 -1
  96. package/dist/core/web/public/next/assets/AutonomySection-CRqHUhED.js +0 -2
  97. package/dist/core/web/public/next/assets/AutonomySection-CRqHUhED.js.map +0 -1
  98. package/dist/core/web/public/next/assets/ChatSection-5irrh6Jw.js +0 -80
  99. package/dist/core/web/public/next/assets/ChatSection-5irrh6Jw.js.map +0 -1
  100. package/dist/core/web/public/next/assets/ChatSection-LQiAn6Eu.css +0 -1
  101. package/dist/core/web/public/next/assets/SettingsSection-CWxtTNlD.js +0 -2
  102. package/dist/core/web/public/next/assets/SettingsSection-CWxtTNlD.js.map +0 -1
  103. package/dist/core/web/public/next/assets/VerseConsoleApp-CRfZUV-T.js +0 -3
  104. package/dist/core/web/public/next/assets/VerseConsoleApp-CRfZUV-T.js.map +0 -1
  105. package/dist/core/web/public/next/assets/VerseConsoleApp-D2E3mU9a.css +0 -1
  106. package/dist/core/web/public/next/assets/global-C4jfYHrj.js +0 -2
  107. package/dist/core/web/public/next/assets/global-C4jfYHrj.js.map +0 -1
  108. package/dist/core/web/public/next/assets/global-D463gG1X.css +0 -1
  109. package/dist/core/web/public/next/assets/workspace-model-cE4LkKpE.js +0 -2
  110. package/dist/core/web/public/next/assets/workspace-model-cE4LkKpE.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -339,6 +339,115 @@ superseded, not a setup procedure. Use [runtime activation authority](docs/RUNTI
339
339
  and the [current architecture boundary](docs/ARCHITECTURE.md#legacy-fleet-activation-boundary).
340
340
  Neither the historical record nor a successful test activates a resident fleet.
341
341
 
342
+ ## [3.7.0] — 2026-09-23 UTC — The console you can actually navigate, and a fleet that can be bounded
343
+
344
+ ### Verse
345
+
346
+ - **The rail expands** (⌘\), with labels and ⌘-digits. Hover used to show the
347
+ *native browser tooltip* — unstyled, mouse-only, clipping against the sidebar.
348
+ There is now a real portalled Tooltip primitive, and adopting it fixed a live
349
+ accessibility defect: `aria-describedby` sat on a wrapper rather than the
350
+ focusable child, so nothing was ever announced. A **disabled** trigger emits no
351
+ pointer events, so its tooltip could never open — precisely the tooltip that
352
+ matters, since it explains why the control is disabled.
353
+ - **The header earns its height.** It held one toggle floating at the right edge
354
+ with nothing anchoring the left, because the sidebar toggle only rendered while
355
+ the sidebar was collapsed.
356
+ - **Display size** — Default / Large / Extra large, scaling type *and* spacing as
357
+ a multiplier rather than a second table of literals, because density and scale
358
+ have identical CSS specificity and two literal tables would let file order
359
+ decide what "Compact + Large" means. Rail width and strip height are pinned so
360
+ the traffic-light clearance cannot regress.
361
+ - **Pane resizing** that survives a real drag: widths are a desired/effective
362
+ pair, so a wide sidebar narrows on a small window and springs back. It also
363
+ turned out the resize handle was eating 4px of the macOS window drag strip.
364
+ - **Saved projects and a native folder picker**, and the MCP section is mounted.
365
+
366
+ ### Autonomy
367
+
368
+ - **A bounded run window** — until paused, at a wall-clock time, or after N
369
+ iterations. The window is an absolute instant, never a countdown, so a host
370
+ suspended at 01:00 and opened at 09:00 finds its 07:00 window expired.
371
+ - **The post-merge halt.** The pre-merge gate was already strict; *nothing re-ran
372
+ the suite after a merge*, so a regression merged at 01:00 compounded all night.
373
+ A dirty working tree halts too, as **unprovable** rather than *regressed* — a
374
+ run that measured uncommitted edits has proven nothing. Ignored files are not
375
+ dirt, or it would halt every night.
376
+
377
+ ### Corrections
378
+
379
+ - A test guard was red, and a red guard protects nothing. The Anthropic shim is a
380
+ request normaliser, not a transport — its one `/chat/completions` is in a doc
381
+ comment recording a measurement.
382
+ - `VerseApp.test.tsx` had been passing by luck of file ordering; the first test to
383
+ mount the shell paid ChatSection's cold transform cost inside its own 1000ms
384
+ assertion window.
385
+
386
+ ## [3.6.0] — 2026-09-23 UTC — Open any project, from a real picker
387
+
388
+ Verse could already drive any folder on the machine — the API accepted an
389
+ unenrolled path and both Grok and local seats ran in it. What it could not do was
390
+ let you *choose* one. The only way in was typing an absolute path into a text
391
+ box, and a folder you typed vanished from the list until a chat existed on it.
392
+ This release is the door.
393
+
394
+ ### Any folder, from a native chooser
395
+
396
+ - **A real macOS directory picker.** "Choose folder…" opens `NSOpenPanel`
397
+ through the Tauri dialog plugin. Detection is a call-time feature probe of the
398
+ injected shell global, not a build flag, because the same bundle is served
399
+ inside the desktop app and as a plain web UI — in a browser the button is
400
+ absent and the absolute-path input remains, which is never removed.
401
+ - **Saved projects the console can actually create.** `createVerseWorkspace`,
402
+ `updateVerseWorkspace`, `deleteVerseWorkspace`, `setVerseRootPriority` and
403
+ `setVerseFocusSection` all existed with **zero callers**, so
404
+ `~/.ashlr/verse/workspaces.json` could only be written by hand. There is now a
405
+ Projects section in the sidebar: save, rename, add and remove folders, reorder
406
+ (the first folder is the primary — its directory is the chat's working
407
+ directory), and forget behind a two-step confirm that states existing chats
408
+ are untouched.
409
+ - **Interactive and autonomous stay separate registries.** Saving a folder to
410
+ work in never enrols it for unattended work. A saved project can be *offered*
411
+ to the autonomous lane, but only by ticking that box.
412
+ - **The Grok caveat travels with a saved project.** Grok takes no extra roots,
413
+ so a multi-root project picked with a Grok seat now says its extra folders are
414
+ unreachable. That case previously had no warning at all.
415
+
416
+ ### The MCP section exists again
417
+
418
+ `McpSection.tsx` was complete — query layer, tested contract, complete server
419
+ side — and unreachable: absent from `VERSE_SECTIONS`, no `'mcp'` member on
420
+ `VerseSectionId`, and outside the glob that mounts sections. It is now ⌘6.
421
+ ⌘1–⌘5 keep their meanings.
422
+
423
+ It also discloses a footgun nothing named before: Codex and Grok inherit MCP
424
+ from their native profile config, so a turn on an unenrolled folder can reach an
425
+ `ashlr__*` write tool and be refused by the enrollment gate — the turn fails on
426
+ policy, not on the model. Claude and local seats are unaffected, because the
427
+ adapter passes an empty `--mcp-config` under `--strict-mcp-config`.
428
+
429
+ ### One capability was widened, deliberately
430
+
431
+ `dialog:allow-open` is granted to the remote Verse origin in
432
+ `capabilities/verse-remote.json`, whose note said not to widen the list. It is
433
+ the narrowest dialog permission: it shows a chooser the user must confirm and
434
+ returns a path, reads no file contents, and no `fs:*` permission is granted
435
+ anywhere in the app. `allow-save`, `allow-message`, `allow-ask` and
436
+ `allow-confirm` stay ungranted so page code cannot forge a native prompt.
437
+
438
+ It had to go in that file rather than `main.json`: the Verse window loads a
439
+ remote origin, and a Tauri 2 capability without a `remote` block never reaches
440
+ one — the grant would have looked correct in review and failed silently at
441
+ runtime.
442
+
443
+ ### Verified in the built app, not only in tests
444
+
445
+ The picker was driven end to end in a real build: the button renders inside the
446
+ shell, `NSOpenPanel` opens, the chosen path lands in the field, "Start chat"
447
+ enables, "Save as a project" persists and appears in the sidebar, and forget
448
+ removes it. Separately, `POST /api/verse/workspaces` was confirmed against a
449
+ live server to create a record on disk and list it back.
450
+
342
451
  ## [3.5.1] — 2026-09-23 UTC — The dependency backlog, landed rather than deferred
343
452
 
344
453
  Four dependency PRs had been sitting open. None was a version bump; each needed
package/README.md CHANGED
@@ -7,6 +7,9 @@
7
7
  **[verse.ashlr.ai](https://verse.ashlr.ai)** — Ashlr Verse, the operator console: every account you own
8
8
  behind one surface, plus a local model that keeps working after the quota runs out.
9
9
 
10
+
11
+ ![Ashlr Verse — the operator console: an expandable section rail, a project sidebar, and every connected account with its live usage windows](docs/images/verse-console.png)
12
+
10
13
  [![npm](https://img.shields.io/npm/v/@ashlr/hub.svg?logo=npm&label=%40ashlr%2Fhub&color=cb3837)](https://www.npmjs.com/package/@ashlr/hub)
11
14
  [![npm downloads](https://img.shields.io/npm/dm/@ashlr/hub.svg?color=cb3837)](https://www.npmjs.com/package/@ashlr/hub)
12
15
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "packageVersion": "3.5.1",
4
- "revision": "5bbb2454b7238f8db83acc7b7b8b9635bd89d2da",
3
+ "packageVersion": "3.7.0",
4
+ "revision": "5f128f3a22e379862642a9f8837673baafd95a53",
5
5
  "dirty": false,
6
6
  "provenance": "git"
7
7
  }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * `ashlr activation` — operator authority for the autonomous fleet (M470).
3
+ *
4
+ * The daemon, goal/simple conductors, and resident-service install are all
5
+ * held shut by hard-coded denials until an operator EXPLICITLY grants
6
+ * authority here. Doing nothing keeps today's exact behavior: everything
7
+ * denied. This command group is how an operator opts in, on their own
8
+ * terms, with a real signed/expiring/revocable authorization trail.
9
+ *
10
+ * Subcommands:
11
+ * activation init [--label <text>] [--force] [--yes] [--json]
12
+ * Generate an Ed25519 keypair on this machine (private key never
13
+ * leaves it) and register the public half as a trust root. Grants
14
+ * NOTHING by itself — run `activation grant` next.
15
+ * activation status [--json]
16
+ * Trust roots, pending one-shot permit, standing grants, and what is
17
+ * currently authorized, right now, and why.
18
+ * activation grant <scope...> [--ttl <duration>] [--yes] [--json]
19
+ * Mint a scoped, signed, expiring authorization. `once`/`resident`
20
+ * mint a single-use permit for `ashlr daemon start`, good for a fixed
21
+ * 2-minute window; any of `residentStanding`/`conductor`/`automerge`/
22
+ * `repair`/`deploy`/`install`/`proposalOnly` mint a standing grant
23
+ * checked on every use until it expires (30d cap) or is revoked.
24
+ * `residentStanding` is what lets a resident daemon started by
25
+ * launchd/systemd/Task Scheduler after a reboot authorize itself with
26
+ * no operator present — grant it once, ahead of time, alongside
27
+ * whichever of `automerge`/`repair`/`deploy` the resident loop should
28
+ * also carry for its whole run.
29
+ * activation revoke --root <keyId> | --grant <grantId> [--yes] [--json]
30
+ * Immediately invalidate a trust root or a standing grant.
31
+ *
32
+ * Every subcommand supports --help. Mutating subcommands (init/grant/revoke)
33
+ * refuse to run unattended in a non-TTY session without --yes — the same
34
+ * confirm-gate convention as `ashlr inbox approve`.
35
+ */
36
+ /**
37
+ * `ashlr activation [init|status|grant|revoke] [flags]`
38
+ *
39
+ * Returns a process exit code (0 = success, non-zero = error/usage).
40
+ */
41
+ export declare function cmdActivation(args: string[]): Promise<number>;
@@ -0,0 +1,485 @@
1
+ /**
2
+ * `ashlr activation` — operator authority for the autonomous fleet (M470).
3
+ *
4
+ * The daemon, goal/simple conductors, and resident-service install are all
5
+ * held shut by hard-coded denials until an operator EXPLICITLY grants
6
+ * authority here. Doing nothing keeps today's exact behavior: everything
7
+ * denied. This command group is how an operator opts in, on their own
8
+ * terms, with a real signed/expiring/revocable authorization trail.
9
+ *
10
+ * Subcommands:
11
+ * activation init [--label <text>] [--force] [--yes] [--json]
12
+ * Generate an Ed25519 keypair on this machine (private key never
13
+ * leaves it) and register the public half as a trust root. Grants
14
+ * NOTHING by itself — run `activation grant` next.
15
+ * activation status [--json]
16
+ * Trust roots, pending one-shot permit, standing grants, and what is
17
+ * currently authorized, right now, and why.
18
+ * activation grant <scope...> [--ttl <duration>] [--yes] [--json]
19
+ * Mint a scoped, signed, expiring authorization. `once`/`resident`
20
+ * mint a single-use permit for `ashlr daemon start`, good for a fixed
21
+ * 2-minute window; any of `residentStanding`/`conductor`/`automerge`/
22
+ * `repair`/`deploy`/`install`/`proposalOnly` mint a standing grant
23
+ * checked on every use until it expires (30d cap) or is revoked.
24
+ * `residentStanding` is what lets a resident daemon started by
25
+ * launchd/systemd/Task Scheduler after a reboot authorize itself with
26
+ * no operator present — grant it once, ahead of time, alongside
27
+ * whichever of `automerge`/`repair`/`deploy` the resident loop should
28
+ * also carry for its whole run.
29
+ * activation revoke --root <keyId> | --grant <grantId> [--yes] [--json]
30
+ * Immediately invalidate a trust root or a standing grant.
31
+ *
32
+ * Every subcommand supports --help. Mutating subcommands (init/grant/revoke)
33
+ * refuse to run unattended in a non-TTY session without --yes — the same
34
+ * confirm-gate convention as `ashlr inbox approve`.
35
+ */
36
+ import { makeColors } from './ui.js';
37
+ import { DAEMON_ACTIVATION_SCOPE_KEYS, DEFAULT_STANDING_GRANT_VALIDITY_MS, EMPTY_DAEMON_ACTIVATION_SCOPE, MAX_STANDING_GRANT_VALIDITY_MS, daemonActivationDiagnosePendingPermitBindings, daemonActivationDirPath, daemonActivationGrantStatus, daemonActivationInit, daemonActivationMintOneShotPermit, daemonActivationMintStandingGrant, daemonActivationPermitPath, daemonActivationRevokeGrant, daemonActivationRevokeTrustRoot, daemonActivationScopeGranted, daemonActivationTrustRootStatus, inspectDaemonActivationPermit, } from '../core/daemon/activation-permit.js';
38
+ const ACTIVATION_SUBCOMMANDS = new Set(['init', 'status', 'grant', 'revoke']);
39
+ const ONE_SHOT_SCOPE_KEYS = new Set(['once', 'resident']);
40
+ const ACTIVATION_USAGE = {
41
+ init: 'Usage: ashlr activation init [--label <text>] [--force] [--yes] [--json]',
42
+ status: 'Usage: ashlr activation status [--json]',
43
+ grant: `Usage: ashlr activation grant <scope...> [--ttl <duration>] [--yes] [--json]\n` +
44
+ ` scopes: ${DAEMON_ACTIVATION_SCOPE_KEYS.join(', ')}\n` +
45
+ ` duration: <n>s | <n>m | <n>h | <n>d (default: 24h; hard cap: 30d; standing grants only)`,
46
+ revoke: 'Usage: ashlr activation revoke (--root <keyId> | --grant <grantId>) [--yes] [--json]',
47
+ };
48
+ const ACTIVATION_TOP_LEVEL_USAGE = `Usage: ashlr activation [subcommand] [flags]
49
+
50
+ Everything here is denied by default. Run \`activation init\` then
51
+ \`activation grant <scope...>\` to explicitly opt this machine's fleet into
52
+ real authority — nothing else changes anything on its own.
53
+
54
+ Subcommands:
55
+ init Generate an operator keypair and register the trust root
56
+ status Show what is granted, by whom, when, and current eligibility
57
+ grant Mint a scoped, signed, expiring permit or standing grant
58
+ revoke Immediately invalidate a trust root or a standing grant
59
+
60
+ Run \`ashlr activation <subcommand> --help\` for subcommand usage.`;
61
+ function isHelpFlag(arg) {
62
+ return arg === '--help' || arg === '-h';
63
+ }
64
+ function isActivationSubcommand(value) {
65
+ return ACTIVATION_SUBCOMMANDS.has(value);
66
+ }
67
+ function printActivationUsage(subcommand) {
68
+ console.log(subcommand ? ACTIVATION_USAGE[subcommand] : ACTIVATION_TOP_LEVEL_USAGE);
69
+ }
70
+ function printActivationUsageError(message, subcommand) {
71
+ const col = makeColors(process.stdout.isTTY === true);
72
+ console.error(col.red('error: ') + message);
73
+ console.error(col.dim(subcommand ? ACTIVATION_USAGE[subcommand] : ACTIVATION_TOP_LEVEL_USAGE));
74
+ return 2;
75
+ }
76
+ function parseDurationMs(text) {
77
+ const match = /^(\d+)(s|m|h|d)?$/u.exec(text.trim());
78
+ if (!match)
79
+ return null;
80
+ const value = Number(match[1]);
81
+ if (!Number.isFinite(value) || value <= 0)
82
+ return null;
83
+ const unit = match[2] ?? 'h';
84
+ const multiplier = unit === 's' ? 1_000 : unit === 'm' ? 60_000 : unit === 'h' ? 3_600_000 : 86_400_000;
85
+ return value * multiplier;
86
+ }
87
+ async function importConfig() {
88
+ try {
89
+ const mod = (await import('../core/config.js'));
90
+ return mod.loadConfigReadOnlyStrict;
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ }
96
+ /** Non-TTY without --yes refuses — no silently-automated grants, matching `inbox approve`. */
97
+ function refuseUnattended(yes, jsonMode, action) {
98
+ const tty = process.stdout.isTTY === true;
99
+ if (tty || yes)
100
+ return false;
101
+ const col = makeColors(false);
102
+ const msg = `non-TTY: activation ${action} requires --yes to prevent silent, unattended authorization`;
103
+ if (jsonMode) {
104
+ console.log(JSON.stringify({ ok: false, error: msg }));
105
+ }
106
+ else {
107
+ console.error(col.red('error: ') + msg);
108
+ console.error(col.dim(` Use \`ashlr activation ${action} ... --yes\` in non-interactive environments.`));
109
+ }
110
+ return true;
111
+ }
112
+ // ---------------------------------------------------------------------------
113
+ // Subcommand: init
114
+ // ---------------------------------------------------------------------------
115
+ async function cmdActivationInit(args) {
116
+ const jsonMode = args.includes('--json');
117
+ const force = args.includes('--force');
118
+ const yes = args.includes('--yes');
119
+ const labelIndex = args.indexOf('--label');
120
+ const label = labelIndex !== -1 ? args[labelIndex + 1] : undefined;
121
+ const known = new Set(['--json', '--force', '--yes', '--label', label].filter((v) => v !== undefined));
122
+ const unknown = args.find((a, i) => !known.has(a) && !(args[i - 1] === '--label'));
123
+ if (unknown)
124
+ return printActivationUsageError(`Unknown flag: ${unknown}`, 'init');
125
+ if (labelIndex !== -1 && label === undefined) {
126
+ return printActivationUsageError('--label requires a value', 'init');
127
+ }
128
+ const col = makeColors(process.stdout.isTTY === true);
129
+ if (!jsonMode) {
130
+ console.log('');
131
+ console.log(col.bold(' ashlr activation init'));
132
+ console.log(col.dim(' Generates an Ed25519 keypair on THIS machine and registers the public\n'
133
+ + ' half as a trust root. The private key never leaves this machine, is\n'
134
+ + ' written 0600 under an 0700 directory, and is the ONLY thing that can\n'
135
+ + ' sign a future `activation grant`. This step grants no capability by\n'
136
+ + ' itself — the fleet stays exactly as denied as it is today until you\n'
137
+ + ' run `ashlr activation grant <scope...>`.'));
138
+ console.log('');
139
+ }
140
+ if (refuseUnattended(yes, jsonMode, 'init'))
141
+ return 2;
142
+ const result = daemonActivationInit({ label, force });
143
+ if (!result.ok) {
144
+ const msg = `activation init refused: ${result.reason}`;
145
+ if (jsonMode)
146
+ console.log(JSON.stringify({ ok: false, error: result.reason }));
147
+ else
148
+ console.error(col.red('error: ') + msg);
149
+ return 1;
150
+ }
151
+ if (jsonMode) {
152
+ console.log(JSON.stringify({
153
+ ok: true,
154
+ keyId: result.keyId,
155
+ publicKeyPem: result.publicKeyPem,
156
+ rotated: result.rotated,
157
+ trustRootsPath: daemonActivationDirPath(),
158
+ }));
159
+ }
160
+ else {
161
+ console.log(col.green('ok: ') + `trust root registered (keyId=${result.keyId}${result.rotated ? ', rotated' : ''}).`);
162
+ console.log(col.dim(` store: ${daemonActivationDirPath()}`));
163
+ console.log(col.dim(' Next: `ashlr activation grant <scope...>` to actually authorize something.'));
164
+ }
165
+ return 0;
166
+ }
167
+ // ---------------------------------------------------------------------------
168
+ // Subcommand: status
169
+ // ---------------------------------------------------------------------------
170
+ async function cmdActivationStatus(args) {
171
+ const jsonMode = args.includes('--json');
172
+ const unknown = args.find((a) => a !== '--json');
173
+ if (unknown)
174
+ return printActivationUsageError(`Unknown flag: ${unknown}`, 'status');
175
+ const col = makeColors(process.stdout.isTTY === true);
176
+ const trustRoots = daemonActivationTrustRootStatus();
177
+ const grants = daemonActivationGrantStatus();
178
+ const loadConfig = await importConfig();
179
+ let cfg = null;
180
+ try {
181
+ cfg = loadConfig ? loadConfig() : null;
182
+ }
183
+ catch {
184
+ cfg = null;
185
+ }
186
+ const onceReadiness = cfg ? inspectDaemonActivationPermit(cfg, { once: true, dryRun: false }) : null;
187
+ const residentReadiness = cfg ? inspectDaemonActivationPermit(cfg, { once: false, dryRun: false }) : null;
188
+ const bindingMismatch = onceReadiness?.reason === 'permit-runtime-binding-mismatch'
189
+ || residentReadiness?.reason === 'permit-runtime-binding-mismatch';
190
+ const bindingDiagnosis = cfg && bindingMismatch
191
+ ? daemonActivationDiagnosePendingPermitBindings(cfg)
192
+ : null;
193
+ const nowMs = Date.now();
194
+ const grantSummaries = grants.ok
195
+ ? grants.records.map((record) => ({
196
+ grantId: record.grantId,
197
+ keyId: record.keyId,
198
+ scope: DAEMON_ACTIVATION_SCOPE_KEYS.filter((key) => record.scope[key]),
199
+ issuedAt: record.issuedAt,
200
+ expiresAt: record.expiresAt,
201
+ revoked: record.revokedAt !== null,
202
+ expired: Date.parse(record.expiresAt) <= nowMs,
203
+ }))
204
+ : [];
205
+ if (jsonMode) {
206
+ console.log(JSON.stringify({
207
+ ok: true,
208
+ store: daemonActivationDirPath(),
209
+ trustRoots: trustRoots.ok
210
+ ? trustRoots.records.map((r) => ({
211
+ keyId: r.keyId, label: r.label, createdAt: r.createdAt, revoked: r.revokedAt !== null,
212
+ }))
213
+ : null,
214
+ trustRootStoreError: trustRoots.ok ? null : trustRoots.reason,
215
+ grants: grants.ok ? grantSummaries : null,
216
+ grantStoreError: grants.ok ? null : grants.reason,
217
+ pendingOneShotPermitPath: daemonActivationPermitPath(),
218
+ onceEligibility: onceReadiness,
219
+ residentEligibility: residentReadiness,
220
+ bindingMismatchDiagnosis: bindingDiagnosis,
221
+ }, null, 2));
222
+ return 0;
223
+ }
224
+ console.log('');
225
+ console.log(col.bold(' ashlr activation status'));
226
+ console.log(col.dim(` store: ${daemonActivationDirPath()}`));
227
+ console.log('');
228
+ console.log(col.bold(' Trust roots'));
229
+ if (!trustRoots.ok) {
230
+ console.log(col.yellow(` unreadable/unsafe (${trustRoots.reason}) — treated as NO authority.`));
231
+ }
232
+ else if (trustRoots.records.length === 0) {
233
+ console.log(col.dim(' none — run `ashlr activation init`.'));
234
+ }
235
+ else {
236
+ for (const root of trustRoots.records) {
237
+ const state = root.revokedAt !== null ? col.red(`revoked ${root.revokedAt}`) : col.green('active');
238
+ console.log(` ${root.keyId} ${state} ${root.label ? `"${root.label}"` : ''} (created ${root.createdAt})`);
239
+ }
240
+ }
241
+ console.log('');
242
+ console.log(col.bold(' Standing grants'));
243
+ if (!grants.ok) {
244
+ console.log(col.yellow(` unreadable/unsafe (${grants.reason}) — treated as NO authority.`));
245
+ }
246
+ else if (grantSummaries.length === 0) {
247
+ console.log(col.dim(' none.'));
248
+ }
249
+ else {
250
+ for (const g of grantSummaries) {
251
+ const state = g.revoked ? col.red('revoked') : g.expired ? col.yellow('expired') : col.green('active');
252
+ console.log(` ${g.grantId} ${state} scope=[${g.scope.join(', ')}] keyId=${g.keyId} expires ${g.expiresAt}`);
253
+ }
254
+ }
255
+ console.log('');
256
+ console.log(col.bold(' Currently granted, right now'));
257
+ for (const scopeKey of DAEMON_ACTIVATION_SCOPE_KEYS) {
258
+ if (ONE_SHOT_SCOPE_KEYS.has(scopeKey))
259
+ continue;
260
+ const check = daemonActivationScopeGranted(scopeKey);
261
+ console.log(` ${scopeKey.padEnd(12)} ${check.granted ? col.green('granted') : col.dim('denied')} (${check.reason})`);
262
+ }
263
+ console.log('');
264
+ console.log(col.bold(' One-shot permit (ashlr daemon start)'));
265
+ console.log(` pending file: ${daemonActivationPermitPath()}`);
266
+ if (onceReadiness) {
267
+ console.log(` --once: ${onceReadiness.state === 'ready' ? col.green('ready') : col.dim(onceReadiness.state)} (${onceReadiness.reason})`);
268
+ }
269
+ if (residentReadiness) {
270
+ console.log(` resident: ${residentReadiness.state === 'ready' ? col.green('ready') : col.dim(residentReadiness.state)} (${residentReadiness.reason})`);
271
+ }
272
+ if (bindingDiagnosis) {
273
+ console.log('');
274
+ if (bindingDiagnosis.ok || !bindingDiagnosis.mismatches) {
275
+ console.log(col.dim(` binding diagnosis: ${bindingDiagnosis.reason}`));
276
+ }
277
+ else {
278
+ console.log(col.yellow(' binding mismatch — field(s) that differ between the signed permit and right now:'));
279
+ for (const [field, { signed, current }] of Object.entries(bindingDiagnosis.mismatches)) {
280
+ console.log(` ${col.bold(field)}:`);
281
+ console.log(` signed: ${formatBindingValue(signed)}`);
282
+ console.log(` current: ${formatBindingValue(current)}`);
283
+ }
284
+ }
285
+ }
286
+ console.log('');
287
+ return 0;
288
+ }
289
+ function formatBindingValue(value) {
290
+ if (value === null || typeof value !== 'object')
291
+ return JSON.stringify(value);
292
+ if ('sha256' in value) {
293
+ const binding = value;
294
+ return `${binding.path} sha256:${binding.sha256}`;
295
+ }
296
+ return JSON.stringify(value);
297
+ }
298
+ // ---------------------------------------------------------------------------
299
+ // Subcommand: grant
300
+ // ---------------------------------------------------------------------------
301
+ async function cmdActivationGrant(args) {
302
+ const jsonMode = args.includes('--json');
303
+ const yes = args.includes('--yes');
304
+ const ttlIndex = args.indexOf('--ttl');
305
+ const ttlText = ttlIndex !== -1 ? args[ttlIndex + 1] : undefined;
306
+ const flagsConsumed = new Set(['--json', '--yes', '--ttl', ttlText].filter((v) => v !== undefined));
307
+ const scopeArgs = args.filter((a, i) => !flagsConsumed.has(a) && args[i - 1] !== '--ttl');
308
+ const unknownFlag = args.find((a) => a.startsWith('--') && !['--json', '--yes', '--ttl'].includes(a));
309
+ if (unknownFlag)
310
+ return printActivationUsageError(`Unknown flag: ${unknownFlag}`, 'grant');
311
+ if (ttlIndex !== -1 && ttlText === undefined)
312
+ return printActivationUsageError('--ttl requires a value', 'grant');
313
+ if (scopeArgs.length === 0) {
314
+ return printActivationUsageError('At least one scope is required.', 'grant');
315
+ }
316
+ const invalidScope = scopeArgs.find((s) => !DAEMON_ACTIVATION_SCOPE_KEYS.includes(s));
317
+ if (invalidScope) {
318
+ return printActivationUsageError(`Unknown scope: ${invalidScope} (expected one of: ${DAEMON_ACTIVATION_SCOPE_KEYS.join(', ')})`, 'grant');
319
+ }
320
+ const scopeKeys = [...new Set(scopeArgs)];
321
+ const scope = { ...EMPTY_DAEMON_ACTIVATION_SCOPE };
322
+ for (const key of scopeKeys)
323
+ scope[key] = true;
324
+ const isOneShot = scopeKeys.some((key) => ONE_SHOT_SCOPE_KEYS.has(key));
325
+ let ttlMs = DEFAULT_STANDING_GRANT_VALIDITY_MS;
326
+ if (ttlText !== undefined) {
327
+ if (isOneShot) {
328
+ return printActivationUsageError('--ttl only applies to standing grants (not once/resident, which use a fixed 2-minute window).', 'grant');
329
+ }
330
+ const parsed = parseDurationMs(ttlText);
331
+ if (parsed === null || parsed > MAX_STANDING_GRANT_VALIDITY_MS) {
332
+ return printActivationUsageError(`Invalid --ttl (expected <n>s|m|h|d, max ${MAX_STANDING_GRANT_VALIDITY_MS / 86_400_000}d): ${ttlText}`, 'grant');
333
+ }
334
+ ttlMs = parsed;
335
+ }
336
+ const col = makeColors(process.stdout.isTTY === true);
337
+ if (!jsonMode) {
338
+ console.log('');
339
+ console.log(col.bold(' ashlr activation grant') + col.dim(` — scope=[${scopeKeys.join(', ')}]`));
340
+ console.log(col.dim(isOneShot
341
+ ? ' Mints a SINGLE-USE, signed permit for `ashlr daemon start`. Consumed and\n'
342
+ + ' deleted the moment the daemon starts; expires in 2 minutes if unused.'
343
+ : ` Mints a standing, signed grant valid for ${Math.round(ttlMs / 3_600_000)}h, checked on every\n`
344
+ + ' use until it expires or you run `ashlr activation revoke`.'));
345
+ console.log('');
346
+ }
347
+ if (refuseUnattended(yes, jsonMode, 'grant'))
348
+ return 2;
349
+ if (isOneShot) {
350
+ const loadConfig = await importConfig();
351
+ if (!loadConfig) {
352
+ console.error(col.red('error: ') + 'activation grant requires src/core/config.ts.');
353
+ return 1;
354
+ }
355
+ let cfg;
356
+ try {
357
+ cfg = loadConfig();
358
+ }
359
+ catch (e) {
360
+ console.error(col.red('error: ') + 'Failed to load config: ' + (e instanceof Error ? e.message : String(e)));
361
+ return 1;
362
+ }
363
+ const result = daemonActivationMintOneShotPermit({ cfg, scope });
364
+ if (!result.ok) {
365
+ if (jsonMode)
366
+ console.log(JSON.stringify({ ok: false, error: result.reason }));
367
+ else
368
+ console.error(col.red('error: ') + `activation grant refused: ${result.reason}`);
369
+ return 1;
370
+ }
371
+ if (jsonMode) {
372
+ console.log(JSON.stringify({ kind: 'one-shot-permit', ...result }));
373
+ }
374
+ else {
375
+ console.log(col.green('ok: ') + `one-shot permit minted (permitId=${result.permitId}, expires ${result.expiresAt}).`);
376
+ console.log(col.dim(` ${result.permitPath}`));
377
+ console.log(col.dim(' Run `ashlr daemon start' + (scope.once ? ' --once' : '') + '` now — the permit is single-use.'));
378
+ }
379
+ return 0;
380
+ }
381
+ const result = daemonActivationMintStandingGrant({ scope, ttlMs });
382
+ if (!result.ok) {
383
+ if (jsonMode)
384
+ console.log(JSON.stringify({ ok: false, error: result.reason }));
385
+ else
386
+ console.error(col.red('error: ') + `activation grant refused: ${result.reason}`);
387
+ return 1;
388
+ }
389
+ if (jsonMode) {
390
+ console.log(JSON.stringify({ ok: true, kind: 'standing-grant', record: result.record }));
391
+ }
392
+ else {
393
+ console.log(col.green('ok: ') + `standing grant minted (grantId=${result.record.grantId}, expires ${result.record.expiresAt}).`);
394
+ console.log(col.dim(` scope=[${scopeKeys.join(', ')}]`));
395
+ }
396
+ return 0;
397
+ }
398
+ // ---------------------------------------------------------------------------
399
+ // Subcommand: revoke
400
+ // ---------------------------------------------------------------------------
401
+ async function cmdActivationRevoke(args) {
402
+ const jsonMode = args.includes('--json');
403
+ const yes = args.includes('--yes');
404
+ const rootIndex = args.indexOf('--root');
405
+ const grantIndex = args.indexOf('--grant');
406
+ const rootId = rootIndex !== -1 ? args[rootIndex + 1] : undefined;
407
+ const grantId = grantIndex !== -1 ? args[grantIndex + 1] : undefined;
408
+ const known = new Set(['--json', '--yes', '--root', '--grant', rootId, grantId].filter((v) => v !== undefined));
409
+ const unknown = args.find((a, i) => !known.has(a) && args[i - 1] !== '--root' && args[i - 1] !== '--grant');
410
+ if (unknown)
411
+ return printActivationUsageError(`Unknown flag: ${unknown}`, 'revoke');
412
+ if ((rootIndex === -1) === (grantIndex === -1)) {
413
+ return printActivationUsageError('Specify exactly one of --root <keyId> or --grant <grantId>.', 'revoke');
414
+ }
415
+ if (rootIndex !== -1 && rootId === undefined)
416
+ return printActivationUsageError('--root requires a value', 'revoke');
417
+ if (grantIndex !== -1 && grantId === undefined)
418
+ return printActivationUsageError('--grant requires a value', 'revoke');
419
+ const col = makeColors(process.stdout.isTTY === true);
420
+ if (!jsonMode) {
421
+ console.log('');
422
+ console.log(col.bold(' ashlr activation revoke'));
423
+ console.log(col.dim(rootId
424
+ ? ` Immediately invalidates trust root ${rootId}. Every permit and standing\n`
425
+ + ' grant signed by it stops verifying from this instant on.'
426
+ : ` Immediately invalidates standing grant ${grantId}. Every guard that\n`
427
+ + ' consults it denies from this instant on.'));
428
+ console.log('');
429
+ }
430
+ if (refuseUnattended(yes, jsonMode, 'revoke'))
431
+ return 2;
432
+ const result = rootId !== undefined
433
+ ? daemonActivationRevokeTrustRoot(rootId)
434
+ : daemonActivationRevokeGrant(grantId);
435
+ if (!result.ok) {
436
+ if (jsonMode)
437
+ console.log(JSON.stringify({ ok: false, error: result.reason }));
438
+ else
439
+ console.error(col.red('error: ') + `activation revoke refused: ${result.reason}`);
440
+ return 1;
441
+ }
442
+ if (jsonMode)
443
+ console.log(JSON.stringify({ ok: true }));
444
+ else
445
+ console.log(col.green('ok: ') + 'revoked.');
446
+ return 0;
447
+ }
448
+ // ---------------------------------------------------------------------------
449
+ // Entry point
450
+ // ---------------------------------------------------------------------------
451
+ /**
452
+ * `ashlr activation [init|status|grant|revoke] [flags]`
453
+ *
454
+ * Returns a process exit code (0 = success, non-zero = error/usage).
455
+ */
456
+ export async function cmdActivation(args) {
457
+ const requestedSub = args[0];
458
+ if (requestedSub !== undefined && isHelpFlag(requestedSub)) {
459
+ printActivationUsage();
460
+ return 0;
461
+ }
462
+ if (requestedSub !== undefined && !isActivationSubcommand(requestedSub)) {
463
+ const message = requestedSub.startsWith('-')
464
+ ? `Unknown flag: ${requestedSub}`
465
+ : `Unknown activation subcommand: ${requestedSub}`;
466
+ return printActivationUsageError(message);
467
+ }
468
+ const sub = requestedSub ?? 'status';
469
+ const rest = args.slice(1);
470
+ if (rest.some(isHelpFlag)) {
471
+ printActivationUsage(sub);
472
+ return 0;
473
+ }
474
+ switch (sub) {
475
+ case 'init':
476
+ return cmdActivationInit(rest);
477
+ case 'status':
478
+ return cmdActivationStatus(rest);
479
+ case 'grant':
480
+ return cmdActivationGrant(rest);
481
+ case 'revoke':
482
+ return cmdActivationRevoke(rest);
483
+ }
484
+ }
485
+ //# sourceMappingURL=activation.js.map