@astrale-os/cli 1.0.0-beta.3 → 1.0.0-beta.31

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 (345) hide show
  1. package/README.md +84 -6
  2. package/dist/astrale.js +57586 -60753
  3. package/dist/public/connect-core.js +2158 -30706
  4. package/dist/public/keys/index.js +1016 -29541
  5. package/dist/public/paths/index.js +370 -28654
  6. package/dist/types/admin/contract.d.ts +26 -0
  7. package/dist/types/admin/instance/client.d.ts +20 -0
  8. package/dist/types/admin/instance/index.d.ts +2 -0
  9. package/dist/types/admin/instance/model.d.ts +6 -10
  10. package/dist/types/connection/auth.d.ts +3 -0
  11. package/dist/types/connection/call.d.ts +3 -0
  12. package/dist/types/connection/command.d.ts +27 -0
  13. package/dist/types/connection/credential.d.ts +14 -0
  14. package/dist/types/connection/errors.d.ts +1 -0
  15. package/dist/types/connection/exchange.d.ts +9 -0
  16. package/dist/types/connection/failure/classify.d.ts +2 -0
  17. package/dist/types/connection/failure/index.d.ts +5 -0
  18. package/dist/types/connection/failure/model.d.ts +25 -0
  19. package/dist/types/connection/failure/render.d.ts +3 -0
  20. package/dist/types/connection/index.d.ts +8 -0
  21. package/dist/types/connection/reasons.d.ts +37 -0
  22. package/dist/types/connection/self.d.ts +37 -0
  23. package/dist/types/connection/session.d.ts +31 -0
  24. package/dist/types/connection/target.d.ts +38 -0
  25. package/dist/types/errors.d.ts +1 -1
  26. package/dist/types/lib/admin-instance.d.ts +17 -0
  27. package/dist/types/lib/admin-target.d.ts +11 -36
  28. package/dist/types/lib/config.d.ts +13 -59
  29. package/dist/types/lib/failure-debug.d.ts +1 -0
  30. package/dist/types/lib/idp.d.ts +25 -173
  31. package/dist/types/lib/instance-target.d.ts +1 -1
  32. package/dist/types/lib/instance.d.ts +30 -88
  33. package/dist/types/lib/log.d.ts +5 -3
  34. package/dist/types/lib/output.d.ts +1 -1
  35. package/dist/types/lib/proc.d.ts +43 -0
  36. package/dist/types/lib/self.d.ts +9 -0
  37. package/dist/types/lib/update.d.ts +109 -0
  38. package/dist/types/lib/validation.d.ts +4 -1
  39. package/dist/types/state/exchange-credentials.d.ts +6 -2
  40. package/dist/types/state/files.d.ts +2 -0
  41. package/dist/types/state/index.d.ts +3 -2
  42. package/dist/types/state/paths.d.ts +2 -0
  43. package/dist/types/state/session-routes.d.ts +10 -0
  44. package/package.json +16 -19
  45. package/src/__tests__/fixtures/publication.ts +20 -0
  46. package/src/admin/.spec/architecture.md +12 -5
  47. package/src/admin/__tests__/fixture.ts +25 -0
  48. package/src/admin/catalog/.spec/api.d.ts +1 -3
  49. package/src/admin/catalog/.spec/architecture.md +5 -4
  50. package/src/admin/catalog/__tests__/client.test.ts +143 -69
  51. package/src/admin/catalog/client.ts +40 -54
  52. package/src/admin/catalog/model.ts +3 -4
  53. package/src/admin/contract.ts +47 -0
  54. package/src/admin/graph/.spec/api.d.ts +4 -10
  55. package/src/admin/graph/nodes.ts +1 -1
  56. package/src/admin/instance/.spec/api.d.ts +5 -9
  57. package/src/admin/instance/.spec/architecture.md +7 -7
  58. package/src/admin/instance/__tests__/client.test.ts +157 -175
  59. package/src/admin/instance/client.ts +39 -128
  60. package/src/admin/instance/index.ts +1 -2
  61. package/src/admin/instance/model.ts +12 -18
  62. package/src/commands/__tests__/admin-instance.test.ts +11 -8
  63. package/src/commands/__tests__/call-describe.test.ts +46 -33
  64. package/src/commands/__tests__/call.test.ts +34 -0
  65. package/src/commands/__tests__/domain-install-operation.test.ts +30 -22
  66. package/src/commands/__tests__/domain-install-owned.test.ts +1 -1
  67. package/src/commands/__tests__/domain-list.test.ts +5 -25
  68. package/src/commands/__tests__/install-direct.test.ts +115 -8
  69. package/src/commands/__tests__/install-identity-override.test.ts +6 -17
  70. package/src/commands/__tests__/instance-list-rows.test.ts +1 -1
  71. package/src/commands/__tests__/instance-status.test.ts +139 -0
  72. package/src/commands/__tests__/introspect-parse.test.ts +80 -1
  73. package/src/commands/__tests__/logs.test.ts +250 -2
  74. package/src/commands/__tests__/read-commands.test.ts +119 -11
  75. package/src/commands/__tests__/token-ttl.test.ts +49 -4
  76. package/src/commands/__tests__/update.test.ts +25 -3
  77. package/src/commands/__tests__/view.test.ts +50 -2
  78. package/src/commands/auth/logout.ts +2 -1
  79. package/src/commands/browser.ts +29 -0
  80. package/src/commands/call-describe.ts +60 -68
  81. package/src/commands/call.ts +71 -59
  82. package/src/commands/domain/install.ts +73 -46
  83. package/src/commands/domain/list.ts +1 -1
  84. package/src/commands/domain/publish.ts +14 -11
  85. package/src/commands/domain/uninstall.ts +2 -2
  86. package/src/commands/get.ts +15 -16
  87. package/src/commands/identity/__tests__/register.test.ts +3 -3
  88. package/src/commands/identity/register.ts +8 -8
  89. package/src/commands/instance/create.ts +8 -16
  90. package/src/commands/instance/delete.ts +12 -9
  91. package/src/commands/instance/list.ts +2 -2
  92. package/src/commands/instance/status.ts +66 -20
  93. package/src/commands/instance/use.ts +16 -12
  94. package/src/commands/introspect.ts +53 -41
  95. package/src/commands/logs.ts +123 -36
  96. package/src/commands/mutate.ts +6 -5
  97. package/src/commands/query.ts +39 -36
  98. package/src/commands/session/analyze.ts +26 -4
  99. package/src/commands/token.ts +42 -27
  100. package/src/commands/ui/__tests__/commands.test.ts +152 -0
  101. package/src/commands/ui/add.ts +51 -0
  102. package/src/commands/ui/doctor.ts +21 -0
  103. package/src/commands/ui/init.ts +38 -0
  104. package/src/commands/ui/list.ts +26 -0
  105. package/src/commands/ui/preset-apply.ts +19 -0
  106. package/src/commands/ui/preset-list.ts +12 -0
  107. package/src/commands/ui/shared.ts +25 -0
  108. package/src/commands/update.ts +90 -48
  109. package/src/commands/view.ts +82 -37
  110. package/src/connection/.spec/api.d.ts +7 -8
  111. package/src/connection/.spec/architecture.md +14 -5
  112. package/src/connection/.spec/flows/session.ts +2 -2
  113. package/src/connection/.spec/laws/connection.ts +6 -2
  114. package/src/connection/.spec/layout.ts +1 -0
  115. package/src/connection/__tests__/auth.test.ts +1 -0
  116. package/src/connection/__tests__/credential.test.ts +21 -1
  117. package/src/connection/__tests__/errors.test.ts +27 -20
  118. package/src/connection/__tests__/exchange.test.ts +64 -11
  119. package/src/connection/__tests__/failure-fixtures.ts +39 -0
  120. package/src/connection/__tests__/failure-safety.test.ts +75 -0
  121. package/src/connection/__tests__/self.test.ts +31 -11
  122. package/src/connection/__tests__/session.test.ts +8 -4
  123. package/src/connection/__tests__/target.test.ts +2 -0
  124. package/src/connection/auth.ts +13 -0
  125. package/src/connection/call.ts +1 -1
  126. package/src/connection/credential.ts +2 -2
  127. package/src/connection/errors.ts +1 -355
  128. package/src/connection/exchange.ts +110 -61
  129. package/src/connection/failure/classify.ts +110 -0
  130. package/src/connection/failure/index.ts +18 -0
  131. package/src/connection/failure/model.ts +28 -0
  132. package/src/connection/failure/render.ts +116 -0
  133. package/src/connection/reasons.ts +16 -0
  134. package/src/connection/self.ts +39 -21
  135. package/src/connection/session.ts +27 -9
  136. package/src/errors.ts +3 -2
  137. package/src/graph/.spec/api.d.ts +13 -6
  138. package/src/graph/.spec/laws/documents.ts +7 -7
  139. package/src/graph/.spec/layout.ts +9 -1
  140. package/src/graph/__tests__/mutation.test.ts +2 -2
  141. package/src/graph/__tests__/query.test.ts +35 -9
  142. package/src/graph/class.ts +30 -0
  143. package/src/graph/index.ts +1 -0
  144. package/src/graph/query.ts +27 -40
  145. package/src/identity/.spec/api.d.ts +2 -2
  146. package/src/identity/__tests__/fixtures/registry-journey.ts +13 -1
  147. package/src/identity/__tests__/registration.test.ts +1 -1
  148. package/src/identity/__tests__/registry.test.ts +4 -0
  149. package/src/identity/registration.ts +3 -3
  150. package/src/identity/registry.ts +2 -0
  151. package/src/keys/__tests__/keys.test.ts +4 -0
  152. package/src/keys/credential.ts +1 -0
  153. package/src/lib/__tests__/admin-target.test.ts +12 -0
  154. package/src/lib/__tests__/binary.test.ts +16 -1
  155. package/src/lib/__tests__/browser-retention.test.ts +212 -0
  156. package/src/lib/__tests__/command-dx.test.ts +1 -1
  157. package/src/lib/__tests__/config.test.ts +27 -0
  158. package/src/lib/__tests__/domain-publication.test.ts +10 -16
  159. package/src/lib/__tests__/idp.test.ts +34 -4
  160. package/src/lib/__tests__/instance-candidates.test.ts +1 -3
  161. package/src/lib/__tests__/instance-target.test.ts +7 -39
  162. package/src/lib/__tests__/log-errors.test.ts +64 -0
  163. package/src/lib/__tests__/skills.test.ts +834 -0
  164. package/src/lib/__tests__/studio-server-deps.test.ts +1 -1
  165. package/src/lib/__tests__/update.test.ts +264 -11
  166. package/src/lib/__tests__/view-assets.test.ts +25 -0
  167. package/src/lib/__tests__/view-external-open-intent.test.ts +150 -0
  168. package/src/lib/__tests__/view-external-open-origins.test.ts +27 -0
  169. package/src/lib/__tests__/view-open-intent.test.ts +0 -2
  170. package/src/lib/__tests__/view-server.test.ts +26 -3
  171. package/src/lib/__tests__/view-session.test.ts +2 -1
  172. package/src/lib/admin-instance.ts +14 -8
  173. package/src/lib/admin-target.ts +21 -2
  174. package/src/lib/binary.ts +22 -5
  175. package/src/lib/browser-retention.ts +210 -0
  176. package/src/lib/command-dx.ts +9 -9
  177. package/src/lib/config.ts +12 -1
  178. package/src/lib/domain-publication.ts +9 -7
  179. package/src/lib/failure-debug.ts +26 -0
  180. package/src/lib/idp.ts +34 -3
  181. package/src/lib/instance-candidates.ts +2 -2
  182. package/src/lib/instance-target.ts +3 -14
  183. package/src/lib/instance.ts +16 -11
  184. package/src/lib/log.ts +16 -9
  185. package/src/lib/output.ts +1 -1
  186. package/src/lib/proc.ts +7 -2
  187. package/src/lib/provision-instance.ts +34 -57
  188. package/src/lib/skills/lock.ts +117 -0
  189. package/src/lib/skills/sync.ts +878 -0
  190. package/src/lib/skills.ts +1 -52
  191. package/src/lib/update.ts +298 -45
  192. package/src/lib/validation.ts +2 -2
  193. package/src/lib/view/assets.ts +11 -2
  194. package/src/lib/view/external-open-intent.ts +41 -0
  195. package/src/lib/view/external-open-origins.ts +37 -0
  196. package/src/lib/view/host-capabilities.ts +17 -0
  197. package/src/lib/view/resolve.ts +1 -1
  198. package/src/lib/view/server.ts +18 -3
  199. package/src/lib/view/session.ts +2 -0
  200. package/src/program/.spec/api.d.ts +1 -0
  201. package/src/program/__tests__/program.test.ts +201 -7
  202. package/src/program/argv.ts +14 -0
  203. package/src/program/build.ts +26 -4
  204. package/src/program/command.ts +1 -0
  205. package/src/program/index.ts +1 -0
  206. package/src/program/registry.ts +2 -1
  207. package/src/setup/__tests__/instance-step.test.ts +27 -10
  208. package/src/setup/steps/instance.ts +22 -15
  209. package/src/setup/steps/skills.ts +40 -38
  210. package/src/state/.spec/api.d.ts +21 -2
  211. package/src/state/.spec/architecture.md +18 -4
  212. package/src/state/.spec/layout.ts +1 -0
  213. package/src/state/__tests__/exchange-credentials.test.ts +88 -42
  214. package/src/state/__tests__/files.test.ts +24 -1
  215. package/src/state/__tests__/fixtures/session-route-process.ts +93 -0
  216. package/src/state/__tests__/paths.test.ts +1 -0
  217. package/src/state/__tests__/session-routes.test.ts +138 -0
  218. package/src/state/exchange-credentials.ts +22 -9
  219. package/src/state/files.ts +41 -0
  220. package/src/state/index.ts +3 -1
  221. package/src/state/paths.ts +3 -0
  222. package/src/state/session-routes.ts +34 -0
  223. package/src/telemetry/__tests__/analyze-log.test.ts +38 -0
  224. package/src/telemetry/__tests__/recorder.test.ts +1 -0
  225. package/src/telemetry/__tests__/retention.test.ts +267 -0
  226. package/src/telemetry/__tests__/settings.test.ts +102 -0
  227. package/src/telemetry/__tests__/store-scan.test.ts +128 -0
  228. package/src/telemetry/__tests__/trigger.test.ts +33 -4
  229. package/src/telemetry/analyze.ts +24 -2
  230. package/src/telemetry/recorder.ts +15 -4
  231. package/src/telemetry/retention.ts +176 -0
  232. package/src/telemetry/session.ts +18 -12
  233. package/src/telemetry/settings.ts +64 -11
  234. package/src/telemetry/store.ts +92 -9
  235. package/src/telemetry/trigger.ts +16 -28
  236. package/src/ui/.spec/api.d.ts +14 -0
  237. package/src/ui/.spec/architecture.md +14 -0
  238. package/src/ui/.spec/laws.ts +50 -0
  239. package/src/ui/.spec/layout.ts +16 -0
  240. package/src/ui/__tests__/ui.test.ts +1742 -0
  241. package/src/ui/index.ts +13 -0
  242. package/src/ui/lock.ts +89 -0
  243. package/src/ui/model.ts +83 -0
  244. package/src/ui/operations.ts +1044 -0
  245. package/src/ui/project.ts +333 -0
  246. package/src/ui/release.ts +398 -0
  247. package/src/ui/runner.ts +18 -0
  248. package/studio/client/dist/assets/elk-api-DjmKsHXc.js +1 -0
  249. package/studio/client/dist/assets/index-BFVFs_8x.js +81 -0
  250. package/studio/client/dist/assets/index-DuB9iUdu.css +2 -0
  251. package/studio/client/dist/assets/rolldown-runtime-Dd_uD5pT.js +1 -0
  252. package/studio/client/dist/assets/schema-studio--a0kX3QU.css +1 -0
  253. package/studio/client/dist/assets/schema-studio-DH6oEWME.js +8 -0
  254. package/studio/client/dist/index.html +4 -3
  255. package/studio/package.json +8 -14
  256. package/studio/server/agent/ask.test.ts +1 -1
  257. package/studio/server/agent/bridge/grant.test.ts +1 -1
  258. package/studio/server/agent/bridge/routes.test.ts +1 -1
  259. package/studio/server/agent/harness/claude/adapter.test.ts +15 -5
  260. package/studio/server/agent/harness/gateway/token.test.ts +1 -1
  261. package/studio/server/agent/harness/gateway/token.ts +3 -3
  262. package/studio/server/agent/prompts/anchors.test.ts +41 -32
  263. package/studio/server/agent/prompts/anchors.ts +23 -51
  264. package/studio/server/agent/prompts/system.test.ts +4 -4
  265. package/studio/server/agent/prompts/system.ts +5 -5
  266. package/studio/server/agent/routes.test.ts +1 -1
  267. package/studio/server/agent/run/coordinator.test.ts +1 -1
  268. package/studio/server/agent/run/preparation.ts +1 -1
  269. package/studio/server/api/canvas.ts +0 -1
  270. package/studio/server/api/context.ts +1 -1
  271. package/studio/server/api/deployment.ts +1 -1
  272. package/studio/server/api/views.ts +2 -2
  273. package/studio/server/api/workspace.ts +1 -1
  274. package/studio/server/api-agent-loadout.test.ts +1 -1
  275. package/studio/server/api.test.ts +1 -1
  276. package/studio/server/cache.test.ts +24 -18
  277. package/studio/server/cache.ts +10 -54
  278. package/studio/server/cli-consumers.test.ts +32 -4
  279. package/studio/server/cli.test.ts +2 -2
  280. package/studio/server/domain.test.ts +50 -68
  281. package/studio/server/domain.ts +82 -63
  282. package/studio/server/handoff/copy.ts +2 -2
  283. package/studio/server/index.ts +1 -1
  284. package/studio/server/instances/probe.test.ts +1 -1
  285. package/studio/server/introspect/anatomy/views/routes.ts +46 -37
  286. package/studio/server/introspect/anatomy/views.ts +10 -16
  287. package/studio/server/introspect/anatomy-extras.test.ts +29 -56
  288. package/studio/server/introspect/anatomy.test.ts +22 -14
  289. package/studio/server/introspect/anatomy.ts +6 -9
  290. package/studio/server/introspect/bundle.ts +0 -3
  291. package/studio/server/introspect/canonical-schema.test.ts +155 -424
  292. package/studio/server/introspect/canonical-schema.ts +248 -601
  293. package/studio/server/introspect/core.ts +14 -22
  294. package/studio/server/introspect/diff.test.ts +21 -9
  295. package/studio/server/introspect/diff.ts +19 -64
  296. package/studio/server/introspect/extractor.ts +23 -122
  297. package/studio/server/introspect/overlay-tsmorph.test.ts +116 -241
  298. package/studio/server/introspect/overlay-tsmorph.ts +0 -1
  299. package/studio/server/introspect/overlay.ts +3 -51
  300. package/studio/server/introspect/revision.test.ts +24 -28
  301. package/studio/server/introspect/revision.ts +11 -22
  302. package/studio/server/introspect/runtime.test.ts +77 -195
  303. package/studio/server/introspect/runtime.ts +5 -67
  304. package/studio/server/introspect/schema-ir-json.test.ts +81 -0
  305. package/studio/server/introspect/schema-ir-json.ts +39 -69
  306. package/studio/server/introspect/schema-refs.test.ts +43 -88
  307. package/studio/server/introspect/schema-refs.ts +14 -49
  308. package/studio/server/introspect/source-overlay/handlers.ts +116 -636
  309. package/studio/server/introspect/source-overlay/kernel-calls.ts +0 -3
  310. package/studio/server/introspect/source-overlay/spans.ts +15 -45
  311. package/studio/server/lifecycle.ts +4 -1
  312. package/studio/server/state/baseline.test.ts +14 -9
  313. package/studio/server/state/baseline.ts +3 -5
  314. package/studio/server/state/documents.test.ts +69 -0
  315. package/studio/server/state/documents.ts +57 -10
  316. package/studio/server/state/visibility.ts +1 -5
  317. package/studio/server/views/model.ts +3 -3
  318. package/studio/server/views/target.test.ts +18 -76
  319. package/studio/server/views/target.ts +25 -51
  320. package/studio/server/watch.test.ts +11 -11
  321. package/studio/server/watch.ts +8 -4
  322. package/studio/server/workspace/updates.ts +23 -1
  323. package/studio/server/workspace-watch.ts +2 -2
  324. package/studio/shared/contracts/runtime.ts +4 -0
  325. package/studio/shared/contracts/schema.ts +44 -131
  326. package/studio/shared/contracts/surface.test.ts +15 -17
  327. package/studio/shared/contracts/workspace.ts +5 -11
  328. package/studio/shared/schema/identity.test.ts +20 -44
  329. package/studio/shared/schema/identity.ts +9 -24
  330. package/studio/tsconfig.json +2 -1
  331. package/tsconfig.json +1 -1
  332. package/viewer/dist/index.html +21 -0
  333. package/viewer/dist/main.js +41 -62
  334. package/.check-workspace.cjs +0 -40
  335. package/src/admin/binding.ts +0 -168
  336. package/src/commands/__tests__/instance-create-hosts.test.ts +0 -29
  337. package/studio/client/dist/assets/elk-api-lwhFo7vB.js +0 -1
  338. package/studio/client/dist/assets/index-B-c-smo9.js +0 -8
  339. package/studio/client/dist/assets/index-BckHuAWk.js +0 -81
  340. package/studio/client/dist/assets/index-C4aNQ6dz.css +0 -1
  341. package/studio/client/dist/assets/index-ChOr3yP0.css +0 -1
  342. package/studio/server/introspect/anatomy/views/legacy.ts +0 -232
  343. package/studio/server/introspect/core-extractor.ts +0 -139
  344. package/studio/server/introspect/overlay.test.ts +0 -72
  345. package/studio/server/introspect/source-overlay/annotations.ts +0 -14
package/src/lib/skills.ts CHANGED
@@ -3,62 +3,11 @@ import { homedir } from 'node:os'
3
3
  import { dirname, join } from 'node:path'
4
4
 
5
5
  import { findAgentBrowser } from './browser'
6
- import { run } from './proc'
7
6
 
8
- /**
9
- * Agent skills and the agent-browser tool are owned by the coding-agent harness
10
- * (Claude Code et al.) and by npm — not by this CLI. `astrale setup` and
11
- * `astrale update` only *detect* them and delegate installation to their real
12
- * installers (`npx skills add`, `npm i -g agent-browser`). This module is that
13
- * detection + delegation layer.
14
- */
7
+ export * from './skills/sync'
15
8
 
16
- /** The skill name the agent harness looks up under `<dir>/<name>/SKILL.md`. */
17
- export const ASTRALE_CLI_SKILL = 'astrale-cli'
18
- export const ASTRALE_DOMAIN_SKILL = 'astrale-domain'
19
9
  export const AGENT_BROWSER_SKILL = 'agent-browser'
20
10
 
21
- /**
22
- * Published skill source consumed by `npx skills add`. The public `astrale-os/cli`
23
- * repo hosts BOTH the astrale-cli and astrale-domain skills (under `skills/`), so
24
- * one `npx skills add astrale-os/cli` installs both; address one with
25
- * `astrale-os/cli@astrale-cli` or `astrale-os/cli@astrale-domain`.
26
- */
27
- export const ASTRALE_CLI_SKILL_SOURCE = 'astrale-os/cli'
28
-
29
- /** Human-facing command that installs (or refreshes) both astrale skills, globally. */
30
- export const SKILL_INSTALL_HINT = `npx skills add ${ASTRALE_CLI_SKILL_SOURCE} -g`
31
-
32
- /**
33
- * Install or refresh the astrale agent skills by delegating to the skill package
34
- * manager (`npx skills add`). We install GLOBALLY (`-g`, user-level): one run
35
- * equips every project on the machine — the harness resolves the skills from the
36
- * user Claude dir (see {@link detectSkill}) regardless of cwd, and installs land
37
- * in `~/.agents/skills` rather than clobbering any project's own `.agents/skills`
38
- * (notably this repo's symlinked source). We don't reimplement skill installation
39
- * — re-running the real installer is also how an existing install updates to the
40
- * latest published SKILL.md, so this doubles as the update path.
41
- *
42
- * We CAPTURE the installer's output rather than stream it, and surface it only on
43
- * failure. A global install reports a benign per-agent note for any agent format
44
- * that can't go user-level (e.g. PromptScript: "does not support global skill
45
- * installation") as a "Failed to install N" banner — even though `npx skills`
46
- * still exits 0 and the skills land for every other agent. Streaming that made a
47
- * successful refresh look broken; suppressing it on success lets the caller print
48
- * a clean line. Resolves true on exit 0. Requires Node/`npx` on PATH + network.
49
- */
50
- export async function installSkills(): Promise<boolean> {
51
- const { code, stdout, stderr } = await run('npx', [
52
- 'skills',
53
- 'add',
54
- ASTRALE_CLI_SKILL_SOURCE,
55
- '-g',
56
- '-y',
57
- ])
58
- if (code !== 0) process.stderr.write(stdout + stderr)
59
- return code === 0
60
- }
61
-
62
11
  /**
63
12
  * The directories the agent harness ACTUALLY loads skills from, in resolution
64
13
  * order: a project `.claude/skills` found by walking UP from cwd (the harness
package/src/lib/update.ts CHANGED
@@ -1,5 +1,15 @@
1
1
  import { createHash } from 'node:crypto'
2
- import { chmod, copyFile, mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises'
2
+ import {
3
+ chmod,
4
+ copyFile,
5
+ mkdir,
6
+ mkdtemp,
7
+ readFile,
8
+ realpath,
9
+ rename,
10
+ rm,
11
+ writeFile,
12
+ } from 'node:fs/promises'
3
13
  import { tmpdir } from 'node:os'
4
14
  import { dirname, join } from 'node:path'
5
15
  import { z } from 'zod'
@@ -61,9 +71,26 @@ export type UpdateRequest = {
61
71
  currentVersion: string
62
72
  platform?: Platform
63
73
  installPath?: string
74
+ execution?: UpdateExecution
64
75
  }
65
76
 
77
+ export type UpdateExecution =
78
+ | { kind: 'standalone'; executable: string }
79
+ | { kind: 'package-managed'; executable: string }
80
+
81
+ const admittedScriptInstall = Symbol('admittedScriptInstall')
82
+ export type AdmittedScriptInstall = Readonly<{
83
+ metadata: InstallMetadata
84
+ executable: string
85
+ [admittedScriptInstall]: true
86
+ }>
87
+
66
88
  export type UpdateResult =
89
+ | {
90
+ status: 'managed'
91
+ currentVersion: string
92
+ executable: string
93
+ }
67
94
  | {
68
95
  status: 'up-to-date'
69
96
  currentVersion: string
@@ -106,50 +133,138 @@ export function platformKey(platform: Platform): string {
106
133
  return `${platform.os}-${platform.arch}`
107
134
  }
108
135
 
109
- /**
110
- * True when running as the Bun-compiled standalone binary (Linux/macOS), which
111
- * self-updates by swapping its own file. The Node/npm build does not expose
112
- * `process.versions.bun`; it is managed by the user's package manager instead.
113
- */
114
- function isStandaloneBinary(): boolean {
115
- return Boolean((process.versions as { bun?: string }).bun)
136
+ export function classifyUpdateExecution(input: {
137
+ bunVersion?: string
138
+ executable: string
139
+ entry?: string
140
+ }): UpdateExecution {
141
+ return input.bunVersion && input.entry?.startsWith('/$bunfs/')
142
+ ? { kind: 'standalone', executable: input.executable }
143
+ : { kind: 'package-managed', executable: input.executable }
144
+ }
145
+
146
+ export function detectUpdateExecution(): UpdateExecution {
147
+ return classifyUpdateExecution({
148
+ bunVersion: (process.versions as { bun?: string }).bun,
149
+ executable: process.execPath,
150
+ entry: process.argv[1],
151
+ })
152
+ }
153
+
154
+ export function packageManagedUpdateError(executable: string): AstraleError {
155
+ return new AstraleError(
156
+ 'UPDATE_PACKAGE_MANAGED',
157
+ 'This Astrale build is managed by your package manager.',
158
+ `Active binary: ${executable}. Update it with npm, pnpm, or bun; or put the official script installation first on PATH.`,
159
+ )
116
160
  }
117
161
 
118
162
  export async function readInstallMetadata(path = INSTALL_PATH): Promise<InstallMetadata> {
119
163
  let raw: string
120
164
  try {
121
165
  raw = await readFile(path, 'utf8')
122
- } catch {
123
- throw isStandaloneBinary()
124
- ? new AstraleError(
125
- 'UPDATE_NOT_SCRIPT_INSTALLED',
126
- 'Astrale was not installed by the official install script.',
127
- 'Reinstall with: curl -fsSL https://raw.githubusercontent.com/astrale-os/cli/main/install.sh | sh',
128
- )
129
- : new AstraleError(
130
- 'UPDATE_PACKAGE_MANAGED',
131
- 'This Astrale build is managed by your package manager.',
132
- 'Update with: npm install -g @astrale-os/cli@latest (or pnpm/bun)',
133
- )
166
+ } catch (error) {
167
+ if (!isMissingFile(error)) throw error
168
+ throw new AstraleError(
169
+ 'UPDATE_NOT_SCRIPT_INSTALLED',
170
+ 'Astrale was not installed by the official install script.',
171
+ 'Reinstall with: curl -fsSL https://raw.githubusercontent.com/astrale-os/cli/main/install.sh | sh',
172
+ )
134
173
  }
135
174
 
136
- const parsed = InstallMetadataSchema.safeParse(JSON.parse(raw))
175
+ let decoded: unknown
176
+ try {
177
+ decoded = JSON.parse(raw)
178
+ } catch {
179
+ throw badInstallMetadata(path)
180
+ }
181
+ const parsed = InstallMetadataSchema.safeParse(decoded)
137
182
  if (!parsed.success) {
183
+ throw badInstallMetadata(path)
184
+ }
185
+ return parsed.data
186
+ }
187
+
188
+ function isMissingFile(error: unknown): error is NodeJS.ErrnoException {
189
+ return error instanceof Error && 'code' in error && error.code === 'ENOENT'
190
+ }
191
+
192
+ function badInstallMetadata(path: string): AstraleError {
193
+ return new AstraleError(
194
+ 'UPDATE_BAD_INSTALL_METADATA',
195
+ `Invalid install metadata at ${path}.`,
196
+ 'Reinstall with: curl -fsSL https://raw.githubusercontent.com/astrale-os/cli/main/install.sh | sh',
197
+ )
198
+ }
199
+
200
+ export async function admitScriptInstall(
201
+ meta: InstallMetadata,
202
+ execution: Extract<UpdateExecution, { kind: 'standalone' }>,
203
+ ): Promise<AdmittedScriptInstall> {
204
+ const [running, recorded] = await Promise.all([
205
+ realpathIfExists(execution.executable),
206
+ realpathIfExists(meta.bin),
207
+ ])
208
+ if (running === undefined || recorded === undefined || running !== recorded) {
138
209
  throw new AstraleError(
139
- 'UPDATE_BAD_INSTALL_METADATA',
140
- `Invalid install metadata at ${path}.`,
141
- 'Reinstall with: curl -fsSL https://raw.githubusercontent.com/astrale-os/cli/main/install.sh | sh',
210
+ 'UPDATE_INSTALL_MISMATCH',
211
+ 'The running Astrale binary does not own the recorded script installation.',
212
+ `Running binary: ${execution.executable}; recorded binary: ${meta.bin}. Refusing to replace a different installation.`,
142
213
  )
143
214
  }
144
- return parsed.data
215
+ return Object.freeze({
216
+ metadata: meta,
217
+ executable: running,
218
+ [admittedScriptInstall]: true as const,
219
+ })
220
+ }
221
+
222
+ async function realpathIfExists(path: string): Promise<string | undefined> {
223
+ try {
224
+ return await realpath(path)
225
+ } catch (error) {
226
+ if (isMissingFile(error)) return undefined
227
+ throw error
228
+ }
145
229
  }
146
230
 
147
231
  export async function writeInstallMetadata(
148
232
  meta: InstallMetadata,
149
233
  path = INSTALL_PATH,
234
+ filesystem: Pick<CohortFilesystem, 'mkdir' | 'rename' | 'rm'> &
235
+ Pick<typeof import('node:fs/promises'), 'writeFile'> = { mkdir, rename, rm, writeFile },
150
236
  ): Promise<void> {
151
- await mkdir(dirname(path), { recursive: true })
152
- await writeFile(path, JSON.stringify(meta, null, 2) + '\n')
237
+ const staged = `${path}.next`
238
+ const previous = `${path}.previous`
239
+ await filesystem.mkdir(dirname(path), { recursive: true })
240
+ await filesystem.rm(staged, { force: true })
241
+ await filesystem.rm(previous, { force: true })
242
+ await filesystem.writeFile(staged, JSON.stringify(meta, null, 2) + '\n')
243
+
244
+ let backedUp = false
245
+ try {
246
+ try {
247
+ await filesystem.rename(path, previous)
248
+ backedUp = true
249
+ } catch (error) {
250
+ if (!isMissingFile(error)) throw error
251
+ }
252
+ await filesystem.rename(staged, path)
253
+ await filesystem.rm(previous, { force: true })
254
+ } catch (error) {
255
+ const rollback: unknown[] = []
256
+ if (backedUp) {
257
+ await filesystem.rename(previous, path).catch((failure) => rollback.push(failure))
258
+ }
259
+ await filesystem.rm(staged, { force: true }).catch((failure) => rollback.push(failure))
260
+ if (rollback.length > 0) {
261
+ throw new AggregateError(
262
+ [error, ...rollback],
263
+ 'Install metadata update and rollback both failed.',
264
+ )
265
+ }
266
+ throw error
267
+ }
153
268
  }
154
269
 
155
270
  export function releaseBase(
@@ -174,8 +289,138 @@ export function shouldUpdate(currentVersion: string, manifestVersion: string): b
174
289
  return currentVersion !== manifestVersion
175
290
  }
176
291
 
177
- export async function updateAstrale(req: UpdateRequest): Promise<UpdateResult> {
178
- const meta = await readInstallMetadata(req.installPath)
292
+ interface CohortFilesystem {
293
+ readonly chmod: typeof chmod
294
+ readonly copyFile: typeof copyFile
295
+ readonly mkdir: typeof mkdir
296
+ readonly rename: typeof rename
297
+ readonly rm: typeof rm
298
+ }
299
+
300
+ const defaultCohortFilesystem: CohortFilesystem = { chmod, copyFile, mkdir, rename, rm }
301
+
302
+ export interface StandaloneCohortReplacement {
303
+ readonly finalize: () => Promise<void>
304
+ readonly rollback: () => Promise<void>
305
+ }
306
+
307
+ /** Replace the standalone executable and its Viewer as one rollback-safe cohort. */
308
+ export async function replaceStandaloneCohort(
309
+ installedBinary: string,
310
+ nextBinary: string,
311
+ nextViewerDist: string,
312
+ filesystem: Partial<CohortFilesystem> = {},
313
+ ): Promise<StandaloneCohortReplacement> {
314
+ const fs = { ...defaultCohortFilesystem, ...filesystem }
315
+ const binDirectory = dirname(installedBinary)
316
+ const previousBinary = `${installedBinary}.previous`
317
+ const stagedBinary = `${installedBinary}.next`
318
+ const viewer = join(binDirectory, 'viewer')
319
+ const previousViewer = join(binDirectory, 'viewer.previous')
320
+ const stagedViewer = join(binDirectory, 'viewer.next')
321
+ const stagedViewerDist = join(stagedViewer, 'dist')
322
+
323
+ try {
324
+ await fs.rm(stagedViewer, { recursive: true, force: true })
325
+ await fs.mkdir(stagedViewerDist, { recursive: true })
326
+ await fs.copyFile(join(nextViewerDist, 'main.js'), join(stagedViewerDist, 'main.js'))
327
+ await fs.copyFile(join(nextViewerDist, 'index.html'), join(stagedViewerDist, 'index.html'))
328
+ await fs.copyFile(installedBinary, previousBinary)
329
+ await fs.copyFile(nextBinary, stagedBinary)
330
+ await fs.chmod(stagedBinary, 0o755)
331
+ } catch (error) {
332
+ await fs.rm(stagedViewer, { recursive: true, force: true }).catch(() => undefined)
333
+ await fs.rm(stagedBinary, { force: true }).catch(() => undefined)
334
+ throw error
335
+ }
336
+
337
+ await fs.rm(previousViewer, { recursive: true, force: true })
338
+ let viewerBackedUp = false
339
+ let viewerCommitted = false
340
+ let binaryCommitted = false
341
+ try {
342
+ try {
343
+ await fs.rename(viewer, previousViewer)
344
+ viewerBackedUp = true
345
+ } catch (error) {
346
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
347
+ }
348
+ await fs.rename(stagedViewer, viewer)
349
+ viewerCommitted = true
350
+ await fs.rename(stagedBinary, installedBinary)
351
+ binaryCommitted = true
352
+ } catch (error) {
353
+ const rollback: unknown[] = []
354
+ if (binaryCommitted) {
355
+ await fs.copyFile(previousBinary, installedBinary).catch((failure) => rollback.push(failure))
356
+ await fs.chmod(installedBinary, 0o755).catch((failure) => rollback.push(failure))
357
+ }
358
+ if (viewerCommitted) {
359
+ await fs
360
+ .rm(viewer, { recursive: true, force: true })
361
+ .catch((failure) => rollback.push(failure))
362
+ }
363
+ if (viewerBackedUp) {
364
+ await fs.rename(previousViewer, viewer).catch((failure) => rollback.push(failure))
365
+ }
366
+ if (rollback.length > 0) {
367
+ throw new AggregateError([error, ...rollback], 'Standalone update and rollback both failed.')
368
+ }
369
+ throw error
370
+ } finally {
371
+ await fs.rm(stagedViewer, { recursive: true, force: true }).catch(() => undefined)
372
+ await fs.rm(stagedBinary, { force: true }).catch(() => undefined)
373
+ }
374
+
375
+ let settled = false
376
+ return Object.freeze({
377
+ finalize: async () => {
378
+ if (settled) return
379
+ settled = true
380
+ await fs.rm(previousViewer, { recursive: true, force: true }).catch(() => undefined)
381
+ },
382
+ rollback: async () => {
383
+ if (settled) return
384
+ settled = true
385
+ const rollback: unknown[] = []
386
+ await fs.copyFile(previousBinary, installedBinary).catch((failure) => rollback.push(failure))
387
+ await fs.chmod(installedBinary, 0o755).catch((failure) => rollback.push(failure))
388
+ await fs
389
+ .rm(viewer, { recursive: true, force: true })
390
+ .catch((failure) => rollback.push(failure))
391
+ if (viewerBackedUp) {
392
+ await fs.rename(previousViewer, viewer).catch((failure) => rollback.push(failure))
393
+ }
394
+ if (rollback.length > 0) {
395
+ throw new AggregateError(rollback, 'Standalone update rollback failed.')
396
+ }
397
+ },
398
+ })
399
+ }
400
+
401
+ interface UpdateDependencies {
402
+ readonly replaceStandaloneCohort: typeof replaceStandaloneCohort
403
+ readonly writeInstallMetadata: typeof writeInstallMetadata
404
+ }
405
+
406
+ const defaultUpdateDependencies = Object.freeze({ replaceStandaloneCohort, writeInstallMetadata })
407
+
408
+ export async function updateAstrale(
409
+ req: UpdateRequest,
410
+ dependencies: Partial<UpdateDependencies> = {},
411
+ ): Promise<UpdateResult> {
412
+ const update = { ...defaultUpdateDependencies, ...dependencies }
413
+ const execution = req.execution ?? detectUpdateExecution()
414
+ if (execution.kind === 'package-managed') {
415
+ return {
416
+ status: 'managed',
417
+ currentVersion: req.currentVersion,
418
+ executable: execution.executable,
419
+ }
420
+ }
421
+
422
+ const install = await admitScriptInstall(await readInstallMetadata(req.installPath), execution)
423
+ const meta = install.metadata
179
424
  const currentVersion = meta.version ?? req.currentVersion
180
425
  const channel = req.channel ?? meta.channel ?? DEFAULT_UPDATE_CHANNEL
181
426
  const platform = req.platform ?? detectPlatform()
@@ -226,25 +471,33 @@ export async function updateAstrale(req: UpdateRequest): Promise<UpdateResult> {
226
471
 
227
472
  await extractTarGz(archive, tmp)
228
473
  const nextBin = join(tmp, 'astrale')
474
+ const nextViewer = join(tmp, 'viewer', 'dist')
229
475
  await chmod(nextBin, 0o755)
230
476
  await smokeVersion(nextBin, manifest.binaryVersion ?? manifest.version)
231
477
 
232
- const previous = `${meta.bin}.previous`
233
- const staged = `${meta.bin}.next`
234
- await copyFile(meta.bin, previous).catch(() => undefined)
235
- await copyFile(nextBin, staged)
236
- await chmod(staged, 0o755)
237
- await rename(staged, meta.bin)
238
-
239
- await writeInstallMetadata(
240
- {
241
- ...meta,
242
- channel: manifest.channel,
243
- version: manifest.version,
244
- installedAt: new Date().toISOString(),
245
- },
246
- req.installPath,
247
- )
478
+ const replacement = await update.replaceStandaloneCohort(meta.bin, nextBin, nextViewer)
479
+ try {
480
+ await update.writeInstallMetadata(
481
+ {
482
+ ...meta,
483
+ channel: manifest.channel,
484
+ version: manifest.version,
485
+ installedAt: new Date().toISOString(),
486
+ },
487
+ req.installPath,
488
+ )
489
+ } catch (error) {
490
+ try {
491
+ await replacement.rollback()
492
+ } catch (rollbackError) {
493
+ throw new AggregateError(
494
+ [error, rollbackError],
495
+ 'Standalone update metadata commit and cohort rollback both failed.',
496
+ )
497
+ }
498
+ throw error
499
+ }
500
+ await replacement.finalize()
248
501
 
249
502
  return {
250
503
  status: 'updated',
@@ -1,4 +1,4 @@
1
- import { isDnsLabel } from '@astrale-os/sdk/domain'
1
+ import { isDnsLabel } from '@astrale-os/sdk/value'
2
2
  import { z } from 'zod'
3
3
 
4
4
  import { ReservedSlugError } from '../errors'
@@ -21,7 +21,7 @@ export function validateName(name: string, entity: string): void {
21
21
  }
22
22
 
23
23
  export function validateSlug(slug: string): void {
24
- // Canonical DNS-label rule (`@astrale-os/sdk/domain`): a slug becomes a
24
+ // Canonical Core DNS-label rule: a slug becomes a
25
25
  // hostname label, so reject anything not DNS-safe (§4.7).
26
26
  if (!slug || !isDnsLabel(slug)) {
27
27
  throw new Error(
@@ -8,7 +8,11 @@ import { fileURLToPath } from 'node:url'
8
8
  * (`<pkg>/viewer/dist`). The module URL is authoritative because npm global
9
9
  * installs expose the CLI through a bin symlink outside the package root.
10
10
  */
11
- export function viewerDistDir(moduleUrl = import.meta.url, entry = process.argv[1] ?? '.'): string {
11
+ export function viewerDistDir(
12
+ moduleUrl = import.meta.url,
13
+ entry = process.argv[1] ?? '.',
14
+ executable = process.execPath,
15
+ ): string {
12
16
  const override = process.env.ASTRALE_VIEWER_DIR
13
17
  if (override) return override
14
18
 
@@ -16,7 +20,12 @@ export function viewerDistDir(moduleUrl = import.meta.url, entry = process.argv[
16
20
  const published = join(moduleDirectory, '..', 'viewer', 'dist')
17
21
  const source = join(moduleDirectory, '..', '..', '..', 'viewer', 'dist')
18
22
  const legacy = join(dirname(entry), '..', 'viewer', 'dist')
19
- const complete = [published, source, legacy].find(hasViewerBundle)
23
+ const standalone = entry.startsWith('/$bunfs/')
24
+ ? join(dirname(executable), 'viewer', 'dist')
25
+ : undefined
26
+ const complete = [standalone, published, source, legacy].find(
27
+ (candidate): candidate is string => candidate !== undefined && hasViewerBundle(candidate),
28
+ )
20
29
  if (complete) return complete
21
30
 
22
31
  // A source checkout may intentionally omit generated dist assets. Keep the
@@ -0,0 +1,41 @@
1
+ import type { ExternalOpenRequest, IntentMessage, Shell } from '@astrale-os/shell'
2
+
3
+ import { replyToIntent } from '@astrale-os/shell'
4
+
5
+ export interface ExternalOpenIntentHost {
6
+ open(request: ExternalOpenRequest): boolean
7
+ }
8
+
9
+ /** Register the root host's browser-owned external navigation effect. */
10
+ export function installExternalOpenIntentHandler(
11
+ shell: Shell,
12
+ host: ExternalOpenIntentHost,
13
+ ): () => void {
14
+ return shell.onIntent('browser.openExternal', (message) => {
15
+ const opened = host.open(message.envelope.payload)
16
+ replyToIntent(shell.children, message.envelope.sender.windowId, message, {
17
+ outcome: opened ? 'opened' : 'blocked',
18
+ })
19
+ })
20
+ }
21
+
22
+ /** Open an external document without retaining a cross-origin opener capability. */
23
+ export function openExternalBrowserWindow(
24
+ browser: Pick<Window, 'open'>,
25
+ request: ExternalOpenRequest,
26
+ ): boolean {
27
+ const popup = request.mode === 'popup'
28
+ const opened = browser.open('', '_blank', popup ? 'popup,width=720,height=760' : undefined)
29
+ if (opened === null) return false
30
+ try {
31
+ opened.opener = null
32
+ if (opened.opener !== null) throw new Error('Browser retained the opener capability.')
33
+ opened.location.replace(request.url)
34
+ } catch {
35
+ opened.close()
36
+ return false
37
+ }
38
+ return true
39
+ }
40
+
41
+ export type ExternalOpenIntentMessage = IntentMessage<'browser.openExternal'>
@@ -0,0 +1,37 @@
1
+ import { AstraleError } from '../../errors.js'
2
+
3
+ /** Admit exact HTTPS origins explicitly granted by the CLI operator. */
4
+ export function admitExternalOpenOrigins(input: readonly string[] | undefined): readonly string[] {
5
+ if (input === undefined) return Object.freeze([])
6
+ const admitted = new Set<string>()
7
+ for (const candidate of input) {
8
+ let url: URL
9
+ try {
10
+ url = new URL(candidate)
11
+ } catch (cause) {
12
+ throw invalidExternalOrigin(candidate, cause)
13
+ }
14
+ if (
15
+ url.protocol !== 'https:' ||
16
+ url.hostname.includes('*') ||
17
+ url.username !== '' ||
18
+ url.password !== '' ||
19
+ url.pathname !== '/' ||
20
+ url.search !== '' ||
21
+ url.hash !== ''
22
+ ) {
23
+ throw invalidExternalOrigin(candidate)
24
+ }
25
+ admitted.add(url.origin)
26
+ }
27
+ return Object.freeze([...admitted])
28
+ }
29
+
30
+ function invalidExternalOrigin(candidate: string, cause?: unknown): AstraleError {
31
+ return new AstraleError(
32
+ 'INVALID_EXTERNAL_ORIGIN',
33
+ `External navigation grant "${candidate}" is not an exact HTTPS origin.`,
34
+ 'Pass an origin such as https://connect.example.com with no credentials, path, query, or fragment.',
35
+ cause === undefined ? undefined : { cause },
36
+ )
37
+ }
@@ -0,0 +1,17 @@
1
+ import type { HostCapabilities } from '@astrale-os/shell'
2
+
3
+ import { admitHostCapabilities } from '@astrale-os/shell'
4
+
5
+ /** Build the exact host grant handed to a CLI-hosted View session. */
6
+ export function viewHostCapabilities(externalOrigins: readonly string[]): HostCapabilities {
7
+ return admitHostCapabilities({
8
+ version: 1,
9
+ navigation: {
10
+ openView: {},
11
+ ...(externalOrigins.length === 0 ? {} : { external: { origins: externalOrigins } }),
12
+ },
13
+ actions: {},
14
+ browser: {},
15
+ access: {},
16
+ })
17
+ }
@@ -1,4 +1,4 @@
1
- import type { ResolvedView as SessionResolvedView } from '@astrale-os/kernel-client/session'
1
+ import type { ResolvedView as SessionResolvedView } from '@astrale-os/sdk/client/session'
2
2
  import type { ResolvedView } from '@astrale-os/shell'
3
3
 
4
4
  import { Path } from '@astrale-os/sdk/graph/path'
@@ -1,3 +1,5 @@
1
+ import type { AuthApi } from '@astrale-os/sdk/auth'
2
+ import type { IssuerId } from '@astrale-os/sdk/auth'
1
3
  import type { ReadableStream as WebReadableStream } from 'node:stream/web'
2
4
 
3
5
  import { readFile } from 'node:fs/promises'
@@ -23,8 +25,9 @@ export { ensureViewerAssets, viewerDistDir } from './assets'
23
25
  * heartbeat keeps a live session alive.
24
26
  */
25
27
 
26
- const TOKEN_REFRESH_MARGIN_MS = 5 * 60_000
27
- const FALLBACK_TOKEN_TTL_MS = 3600_000
28
+ const VIEW_TOKEN_TTL_SECONDS = 4 * 60
29
+ const TOKEN_REFRESH_MARGIN_MS = 60_000
30
+ const FALLBACK_TOKEN_TTL_MS = VIEW_TOKEN_TTL_SECONDS * 1_000
28
31
  const IDLE_SWEEP_MS = 60_000
29
32
 
30
33
  export type PageStatus = { state: string; error?: string; at: string }
@@ -44,7 +47,7 @@ export function startViewServer(config: ViewServeConfig): Server {
44
47
  async function freshGrant(): Promise<TokenGrant> {
45
48
  if (grant && grant.expiresAt - Date.now() > TOKEN_REFRESH_MARGIN_MS) return grant
46
49
  grant = await withClientSession(config.kernel, async ({ auth, target }) => {
47
- const token = await auth.mint({ audience: target.kernelIssuer, ttlSeconds: 3_600 })
50
+ const token = await mintViewCredential(auth, target.kernelIssuer)
48
51
  return {
49
52
  token,
50
53
  expiresAt: jwtExpiry(token) ?? Date.now() + FALLBACK_TOKEN_TTL_MS,
@@ -98,6 +101,7 @@ export function startViewServer(config: ViewServeConfig): Server {
98
101
  identity: session.identity ?? null,
99
102
  instance: session.instance ?? null,
100
103
  sessionId: session.id,
104
+ externalOrigins: config.externalOrigins,
101
105
  })
102
106
  return
103
107
  }
@@ -182,6 +186,17 @@ export function startViewServer(config: ViewServeConfig): Server {
182
186
  return server
183
187
  }
184
188
 
189
+ /** Mint a short-lived credential from the selected CLI identity proof. */
190
+ export async function mintViewCredential(
191
+ auth: Pick<AuthApi, 'mint'>,
192
+ audience: IssuerId,
193
+ ): Promise<string> {
194
+ return auth.mint({
195
+ audience,
196
+ ttlSeconds: VIEW_TOKEN_TTL_SECONDS,
197
+ })
198
+ }
199
+
185
200
  function jwtExpiry(token: string): number | null {
186
201
  try {
187
202
  const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString('utf8')) as {
@@ -41,6 +41,8 @@ export type ViewServeConfig = {
41
41
  * handles CORS and self-signed local CAs.
42
42
  */
43
43
  proxy: { kernelUrl: string; issuer: string; caFile?: string; direct: boolean }
44
+ /** Exact HTTPS origins the operator consented to open from this View session. */
45
+ externalOrigins: readonly string[]
44
46
  idleMs: number
45
47
  }
46
48
 
@@ -5,6 +5,7 @@ export interface CommandArgument {
5
5
  readonly name: string
6
6
  readonly description: string
7
7
  readonly required?: boolean
8
+ readonly variadic?: boolean
8
9
  }
9
10
 
10
11
  /** One Commander-compatible option in a CLI command definition. */