@astrale-os/cli 1.0.0-beta.1 → 1.0.0-beta.2

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 (180) hide show
  1. package/README.md +7 -11
  2. package/dist/astrale.js +3737 -3523
  3. package/dist/public/connect-core.js +396 -392
  4. package/dist/public/keys/index.js +371 -378
  5. package/dist/public/paths/index.js +371 -378
  6. package/dist/types/lib/idp.d.ts +6 -0
  7. package/dist/types/lib/meta.d.ts +2 -2
  8. package/package.json +6 -4
  9. package/src/admin/binding.ts +168 -0
  10. package/src/admin/catalog/__tests__/client.test.ts +2 -2
  11. package/src/admin/catalog/client.ts +4 -9
  12. package/src/admin/instance/__tests__/client.test.ts +2 -2
  13. package/src/admin/instance/client.ts +6 -11
  14. package/src/commands/__tests__/auth-login.test.ts +54 -1
  15. package/src/commands/__tests__/call.test.ts +5 -1
  16. package/src/commands/__tests__/domain-install-operation.test.ts +23 -7
  17. package/src/commands/__tests__/domain-install-owned.test.ts +5 -1
  18. package/src/commands/__tests__/domain-list.test.ts +83 -4
  19. package/src/commands/__tests__/install-identity-override.test.ts +30 -19
  20. package/src/commands/__tests__/studio-descriptor.test.ts +22 -0
  21. package/src/commands/__tests__/update.test.ts +23 -16
  22. package/src/commands/domain/install.ts +11 -15
  23. package/src/commands/domain/list.ts +17 -12
  24. package/src/commands/studio.ts +20 -0
  25. package/src/commands/update.ts +9 -5
  26. package/src/connection/__tests__/errors.test.ts +16 -12
  27. package/src/connection/__tests__/exchange.test.ts +6 -5
  28. package/src/connection/exchange.ts +5 -4
  29. package/src/lib/__tests__/domain-publication.test.ts +144 -0
  30. package/src/lib/__tests__/idp-session.test.ts +22 -0
  31. package/src/lib/__tests__/idp.test.ts +2 -0
  32. package/src/lib/__tests__/instance.test.ts +6 -0
  33. package/src/lib/__tests__/studio-server-deps.test.ts +53 -16
  34. package/src/lib/__tests__/update.test.ts +12 -0
  35. package/src/lib/domain-publication.ts +102 -0
  36. package/src/lib/idp.ts +5 -1
  37. package/src/lib/instance.ts +6 -1
  38. package/src/lib/login-flow.ts +13 -3
  39. package/src/lib/meta.ts +2 -2
  40. package/src/lib/update.ts +4 -4
  41. package/src/program/__tests__/program.test.ts +1 -1
  42. package/studio/client/dist/assets/{elk-api-D2xgMJvi.js → elk-api-lwhFo7vB.js} +1 -1
  43. package/studio/client/dist/assets/index-B-c-smo9.js +8 -0
  44. package/studio/client/dist/assets/index-BckHuAWk.js +81 -0
  45. package/studio/client/dist/assets/index-C4aNQ6dz.css +1 -0
  46. package/studio/client/dist/index.html +2 -2
  47. package/studio/package.json +3 -1
  48. package/studio/server/agent/bridge/stdio.ts +8 -1
  49. package/studio/server/agent/conversation.test.ts +10 -5
  50. package/studio/server/agent/conversation.ts +56 -8
  51. package/studio/server/agent/harness/codex/loadout.ts +18 -21
  52. package/studio/server/agent/harness/gateway/config.ts +20 -18
  53. package/studio/server/agent/harness/selection.ts +9 -1
  54. package/studio/server/agent/prompts/system.ts +3 -2
  55. package/studio/server/agent/prompts/turn.ts +13 -4
  56. package/studio/server/agent/run/completion.ts +2 -2
  57. package/studio/server/agent/run/preparation.ts +6 -5
  58. package/studio/server/agent/run/transcript.test.ts +35 -1
  59. package/studio/server/agent/run/transcript.ts +169 -3
  60. package/studio/server/agent/run/usage.test.ts +24 -0
  61. package/studio/server/agent/run/usage.ts +26 -1
  62. package/studio/server/api/canvas.ts +44 -0
  63. package/studio/server/api/comments.ts +85 -0
  64. package/studio/server/api/context.ts +61 -0
  65. package/studio/server/api/deployment.ts +39 -0
  66. package/studio/server/api/documents.ts +55 -0
  67. package/studio/server/api/domain.ts +59 -0
  68. package/studio/server/api/http.ts +45 -0
  69. package/studio/server/api/project.ts +57 -0
  70. package/studio/server/api/schema.ts +12 -0
  71. package/studio/server/api/views.ts +57 -0
  72. package/studio/server/api/workspace.ts +60 -0
  73. package/studio/server/api.test.ts +147 -0
  74. package/studio/server/api.ts +20 -422
  75. package/studio/server/cache.test.ts +111 -0
  76. package/studio/server/cache.ts +163 -9
  77. package/studio/server/cli-consumers.test.ts +494 -0
  78. package/studio/server/cli.test.ts +88 -0
  79. package/studio/server/cli.ts +240 -0
  80. package/studio/server/client-package.ts +25 -1
  81. package/studio/server/domain.test.ts +30 -2
  82. package/studio/server/domain.ts +14 -2
  83. package/studio/server/environment/dotenv-preview.test.ts +39 -0
  84. package/studio/server/environment/dotenv-preview.ts +27 -0
  85. package/studio/server/environment/files.test.ts +53 -0
  86. package/studio/server/{state/env.ts → environment/files.ts} +7 -48
  87. package/studio/server/{state → handoff}/copy.ts +21 -6
  88. package/studio/server/handoff/service.test.ts +22 -0
  89. package/studio/server/{state/handoff.ts → handoff/service.ts} +21 -14
  90. package/studio/server/instances/active.ts +72 -0
  91. package/studio/server/instances/deploy-record.test.ts +31 -0
  92. package/studio/server/instances/deploy-record.ts +37 -0
  93. package/studio/server/instances/deploy.ts +56 -0
  94. package/studio/server/{state/instance.test.ts → instances/probe.test.ts} +25 -21
  95. package/studio/server/instances/probe.ts +67 -0
  96. package/studio/server/instances/status.test.ts +12 -0
  97. package/studio/server/instances/status.ts +49 -0
  98. package/studio/server/introspect/anatomy/client-tree.ts +57 -0
  99. package/studio/server/introspect/anatomy/env-fields.ts +66 -0
  100. package/studio/server/introspect/anatomy/schema-definition.ts +53 -0
  101. package/studio/server/introspect/anatomy/source.ts +181 -0
  102. package/studio/server/introspect/anatomy/views/legacy.ts +232 -0
  103. package/studio/server/introspect/anatomy/views/routes.ts +105 -0
  104. package/studio/server/introspect/anatomy/views.ts +58 -0
  105. package/studio/server/introspect/anatomy-extras.test.ts +91 -4
  106. package/studio/server/introspect/anatomy-extras.ts +7 -726
  107. package/studio/server/introspect/anatomy.ts +46 -19
  108. package/studio/server/introspect/bundle.ts +15 -6
  109. package/studio/server/introspect/canonical-schema.test.ts +79 -0
  110. package/studio/server/introspect/canonical-schema.ts +80 -18
  111. package/studio/server/introspect/config-preview.test.ts +32 -0
  112. package/studio/server/introspect/config-preview.ts +119 -0
  113. package/studio/server/introspect/diff.test.ts +7 -11
  114. package/studio/server/introspect/diff.ts +28 -37
  115. package/studio/server/introspect/extractor.ts +8 -6
  116. package/studio/server/introspect/hash.ts +5 -2
  117. package/studio/server/introspect/overlay-tsmorph.ts +7 -1305
  118. package/studio/server/introspect/revision.test.ts +54 -0
  119. package/studio/server/introspect/revision.ts +49 -0
  120. package/studio/server/introspect/runtime.test.ts +63 -5
  121. package/studio/server/introspect/runtime.ts +49 -3
  122. package/studio/server/introspect/schema-ir-json.ts +181 -0
  123. package/studio/server/introspect/source-overlay/annotations.ts +14 -0
  124. package/studio/server/introspect/source-overlay/handlers.ts +681 -0
  125. package/studio/server/introspect/source-overlay/kernel-calls.ts +49 -0
  126. package/studio/server/introspect/source-overlay/project.ts +248 -0
  127. package/studio/server/introspect/source-overlay/spans.ts +360 -0
  128. package/studio/server/json.ts +46 -0
  129. package/studio/server/lifecycle.ts +5 -1
  130. package/studio/server/sse.test.ts +26 -0
  131. package/studio/server/sse.ts +4 -1
  132. package/studio/server/state/baseline.test.ts +212 -1
  133. package/studio/server/state/baseline.ts +145 -29
  134. package/studio/server/state/comments.test.ts +66 -1
  135. package/studio/server/state/comments.ts +173 -10
  136. package/studio/server/state/context.ts +34 -2
  137. package/studio/server/state/documents.ts +42 -1
  138. package/studio/server/state/integrations.ts +27 -1
  139. package/studio/server/state/layout.test.ts +34 -0
  140. package/studio/server/state/layout.ts +44 -17
  141. package/studio/server/state/settings.ts +29 -22
  142. package/studio/server/state/store.test.ts +122 -0
  143. package/studio/server/state/store.ts +66 -17
  144. package/studio/server/state/visibility.ts +22 -7
  145. package/studio/server/views/model.test.ts +57 -0
  146. package/studio/server/views/model.ts +66 -0
  147. package/studio/server/views/runtime.ts +40 -0
  148. package/studio/server/views/selection-repository.test.ts +31 -0
  149. package/studio/server/views/selection-repository.ts +60 -0
  150. package/studio/server/views/session.test.ts +73 -0
  151. package/studio/server/views/session.ts +166 -0
  152. package/studio/server/{state/views.test.ts → views/target.test.ts} +8 -159
  153. package/studio/server/views/target.ts +248 -0
  154. package/studio/server/watch.ts +1 -1
  155. package/studio/server/workspace/catalog.test.ts +17 -0
  156. package/studio/server/workspace/catalog.ts +43 -0
  157. package/studio/server/{state → workspace}/create.test.ts +1 -1
  158. package/studio/server/{state → workspace}/create.ts +1 -1
  159. package/studio/server/{state → workspace}/git.ts +1 -1
  160. package/studio/server/workspace/updates.ts +83 -0
  161. package/studio/server/workspace-state.ts +1 -1
  162. package/studio/shared/contracts/README.md +16 -0
  163. package/studio/shared/contracts/agent.ts +270 -0
  164. package/studio/shared/contracts/runtime.ts +50 -0
  165. package/studio/shared/contracts/schema.ts +462 -0
  166. package/studio/shared/contracts/surface.test.ts +319 -0
  167. package/studio/shared/contracts/workspace.ts +274 -0
  168. package/studio/shared/layout.test.ts +116 -0
  169. package/studio/shared/schema/identity.test.ts +58 -0
  170. package/studio/shared/schema/identity.ts +82 -0
  171. package/studio/shared/types.ts +11 -1099
  172. package/studio/tsconfig.json +1 -1
  173. package/viewer/dist/main.js +35 -35
  174. package/studio/client/dist/assets/index-BMdnsIJA.css +0 -1
  175. package/studio/client/dist/assets/index-D-vRV8w7.js +0 -8
  176. package/studio/client/dist/assets/index-LGSWRrk8.js +0 -81
  177. package/studio/server/state/catalog.ts +0 -117
  178. package/studio/server/state/instance.ts +0 -280
  179. package/studio/server/state/updates.ts +0 -63
  180. package/studio/server/state/views.ts +0 -535
@@ -1,3 +1,7 @@
1
+ import { defineDomain } from '@astrale-os/sdk'
2
+ import { issuer } from '@astrale-os/sdk/auth'
3
+ import { createDeployment } from '@astrale-os/sdk/deployment'
4
+ import { defineSchema } from '@astrale-os/sdk/schema/v1'
1
5
  import { afterAll, describe, expect, test } from 'bun:test'
2
6
 
3
7
  import { domainRefFromTarget, isIdentityOverride, probeDeclaredOrigin } from '../domain/install'
@@ -29,46 +33,53 @@ describe('identity-override detection', () => {
29
33
  })
30
34
  })
31
35
 
32
- describe('declared-origin probe (/meta)', () => {
36
+ describe('declared-origin Publication probe', () => {
33
37
  const servers: { stop(): void }[] = []
34
38
  afterAll(() => {
35
39
  for (const s of servers) s.stop()
36
40
  })
37
41
 
38
- function serveMeta(handler: (req: Request) => Response): string {
42
+ function servePublication(handler: (req: Request) => Response): string {
39
43
  const server = Bun.serve({ port: 0, fetch: handler })
40
44
  servers.push(server)
41
45
  return `http://localhost:${server.port}`
42
46
  }
43
47
 
44
- test('reads origin from a well-formed /meta', async () => {
45
- // The exact shape SDK workers serve (adapter-cloudflare worker `/meta`).
46
- const url = serveMeta((req) =>
47
- new URL(req.url).pathname === '/meta'
48
- ? Response.json({
49
- origin: 'crm.acme.dev',
50
- issuer: 'https://crm.acme.dev',
51
- schemaRevision: 'sha256:abc',
52
- deploymentVersion: 'v1',
53
- })
48
+ test('reads origin from a well-formed canonical Publication', async () => {
49
+ const schema = defineSchema('crm.acme.dev', {})
50
+ const definition = defineDomain({
51
+ schema,
52
+ handlers: { functions: {}, classes: {}, interfaces: {} },
53
+ })
54
+ const deployed = createDeployment({
55
+ definition,
56
+ issuer: issuer.accept('https://crm.acme.dev'),
57
+ bundleHref: 'https://crm.acme.dev/domain.bundle.json',
58
+ bindings: { callables: [] },
59
+ }).publication
60
+ const url = servePublication((req) =>
61
+ new URL(req.url).pathname === '/.well-known/astrale/domain.json'
62
+ ? Response.json(deployed)
54
63
  : new Response('nope', { status: 404 }),
55
64
  )
56
65
  expect(await probeDeclaredOrigin(url)).toBe('crm.acme.dev')
57
66
  })
58
67
 
59
- test('falls back to the pre-Kernel-V2 domainName field', async () => {
60
- const legacy = serveMeta(() => Response.json({ iss: 'https://x', domainName: 'crm.acme.dev' }))
61
- expect(await probeDeclaredOrigin(legacy)).toBe('crm.acme.dev')
68
+ test('does not accept the removed pre-Kernel-V2 domainName shape', async () => {
69
+ const legacy = servePublication(() =>
70
+ Response.json({ iss: 'https://x', domainName: 'crm.acme.dev' }),
71
+ )
72
+ expect(await probeDeclaredOrigin(legacy)).toBeUndefined()
62
73
  })
63
74
 
64
- test('degrades to undefined on missing origin, non-200, bad JSON, or dead host', async () => {
65
- const noName = serveMeta(() => Response.json({ iss: 'https://x' }))
75
+ test('degrades to undefined on invalid Publication, non-200, bad JSON, or dead host', async () => {
76
+ const noName = servePublication(() => Response.json({ iss: 'https://x' }))
66
77
  expect(await probeDeclaredOrigin(noName)).toBeUndefined()
67
78
 
68
- const error = serveMeta(() => new Response('boom', { status: 500 }))
79
+ const error = servePublication(() => new Response('boom', { status: 500 }))
69
80
  expect(await probeDeclaredOrigin(error)).toBeUndefined()
70
81
 
71
- const badJson = serveMeta(() => new Response('<html>', { status: 200 }))
82
+ const badJson = servePublication(() => new Response('<html>', { status: 200 }))
72
83
  expect(await probeDeclaredOrigin(badJson)).toBeUndefined()
73
84
 
74
85
  expect(await probeDeclaredOrigin('http://127.0.0.1:1')).toBeUndefined()
@@ -0,0 +1,22 @@
1
+ import { expect, test } from 'bun:test'
2
+ import { realpathSync } from 'node:fs'
3
+
4
+ import { encodeStudioCliDescriptor } from '../studio'
5
+
6
+ test('Studio descriptor preserves the exact runtime and script entrypoint', () => {
7
+ expect(JSON.parse(encodeStudioCliDescriptor(process.execPath, import.meta.path))).toEqual({
8
+ version: 1,
9
+ executable: process.execPath,
10
+ args: [realpathSync(import.meta.path)],
11
+ })
12
+ })
13
+
14
+ test('Studio descriptor reinvokes a compiled executable without its virtual bunfs entry', () => {
15
+ expect(JSON.parse(encodeStudioCliDescriptor('/opt/bin/astrale', '/$bunfs/root/astrale'))).toEqual(
16
+ {
17
+ version: 1,
18
+ executable: '/opt/bin/astrale',
19
+ args: [],
20
+ },
21
+ )
22
+ })
@@ -6,14 +6,17 @@ describe('CLI update staleness', () => {
6
6
  test('trusts the release manifest for a script install without consulting npm latest', async () => {
7
7
  const fetchPackageVersion = mock(async () => '0.8.1-alpha.7')
8
8
  const result = await cliStale(
9
- { channel: 'beta' },
9
+ {},
10
10
  {
11
- update: async () => ({
12
- status: 'up-to-date',
13
- currentVersion: '1.0.0-beta.0',
14
- latestVersion: '1.0.0-beta.0',
15
- channel: 'beta',
16
- }),
11
+ update: async ({ channel }) => {
12
+ expect(channel).toBe('beta')
13
+ return {
14
+ status: 'up-to-date',
15
+ currentVersion: '1.0.0-beta.0',
16
+ latestVersion: '1.0.0-beta.0',
17
+ channel: 'beta',
18
+ }
19
+ },
17
20
  fetchPackageVersion,
18
21
  },
19
22
  )
@@ -28,37 +31,41 @@ describe('CLI update staleness', () => {
28
31
  expect(fetchPackageVersion).not.toHaveBeenCalled()
29
32
  })
30
33
 
31
- test('uses the selected npm dist-tag only for package-managed installs', async () => {
34
+ test('uses an explicit npm dist-tag only for package-managed installs', async () => {
32
35
  const result = await cliStale(
33
- { channel: 'beta' },
36
+ { channel: 'canary' },
34
37
  {
35
38
  update: async () => {
36
39
  throw new Error('package managed')
37
40
  },
38
41
  fetchPackageVersion: async ({ channel }) => {
39
- expect(channel).toBe('beta')
40
- return '1.0.0-beta.0'
42
+ expect(channel).toBe('canary')
43
+ return '1.0.0-canary.0'
41
44
  },
42
45
  },
43
46
  )
44
47
 
45
48
  expect(result).toMatchObject({
46
49
  managed: true,
47
- latest: '1.0.0-beta.0',
50
+ latest: '1.0.0-canary.0',
48
51
  channel: 'npm',
49
52
  })
50
53
  })
51
54
 
52
- test('maps the stable channel to the npm latest dist-tag', async () => {
55
+ test('maps the default to beta and the stable channel to npm latest', async () => {
53
56
  const originalFetch = globalThis.fetch
54
57
  const fetchMock = mock(async (input: RequestInfo | URL) => {
55
- expect(String(input)).toBe('https://registry.npmjs.org/@astrale-os/cli/latest')
56
- return Response.json({ version: '1.0.0' })
58
+ const target = String(input).split('/').at(-1)
59
+ return Response.json({ version: target === 'beta' ? '1.0.0-beta.0' : '1.0.0' })
57
60
  })
58
61
  globalThis.fetch = fetchMock as unknown as typeof fetch
59
62
  try {
63
+ expect(await fetchNpmTargetVersion({})).toBe('1.0.0-beta.0')
60
64
  expect(await fetchNpmTargetVersion({ channel: 'stable' })).toBe('1.0.0')
61
- expect(fetchMock).toHaveBeenCalledTimes(1)
65
+ expect(fetchMock.mock.calls.map(([input]) => String(input))).toEqual([
66
+ 'https://registry.npmjs.org/@astrale-os/cli/beta',
67
+ 'https://registry.npmjs.org/@astrale-os/cli/latest',
68
+ ])
62
69
  } finally {
63
70
  globalThis.fetch = originalFetch
64
71
  }
@@ -15,6 +15,7 @@ import {
15
15
  } from '../../lib/admin-domain'
16
16
  import { listOwnedInstancesInContext, type OwnedInstanceInfo } from '../../lib/admin-instance'
17
17
  import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
18
+ import { fetchDomainPublication } from '../../lib/domain-publication'
18
19
  import { getActive } from '../../lib/instance'
19
20
  import { fatal, log, withSpinner } from '../../lib/log'
20
21
  import { isMachine, output } from '../../lib/output'
@@ -429,11 +430,11 @@ export async function installDirect(
429
430
  log.dim(` operation: ${result.operation}`)
430
431
  // Belt-and-braces: the kernel-confirmed origin is authoritative. If it
431
432
  // aliases the host and the pre-install gate never consented to THAT
432
- // origin (lying or absent `/meta`), say so loudly after the fact.
433
+ // origin (lying or unavailable Publication), say so loudly after the fact.
433
434
  if (isIdentityOverride(installed.origin, host) && installed.origin !== consentedOrigin) {
434
435
  log.warn(
435
436
  `Installed origin "${installed.origin}" differs from the serving host "${host}" ` +
436
- `and was not confirmed before install (the worker's /meta did not declare it). ` +
437
+ `and was not confirmed before install (the worker Publication was unavailable). ` +
437
438
  `Every ${installed.origin}/* call on this instance now routes to ${host}.`,
438
439
  )
439
440
  }
@@ -478,10 +479,10 @@ export function isIdentityOverride(origin: string, host: string): boolean {
478
479
  * Claiming an origin that differs from the serving host is an explicit actAs
479
480
  * and needs typed consent (or `--allow-identity-override` in scripts).
480
481
  *
481
- * The pre-install check reads the worker's self-reported `/meta.origin`,
482
+ * The pre-install check reads the worker's canonical Publication origin,
482
483
  * so it is consent UX, not enforcement — a hostile worker can lie here, and
483
484
  * the kernel anchors the cryptographic identity (`iss`) on the real URL
484
- * regardless. When `/meta` is unreachable or silent on the origin, the gate
485
+ * regardless. When the Publication is unreachable or invalid, the gate
485
486
  * degrades to a warning and the kernel-confirmed origin is re-checked after
486
487
  * install (see `format` above).
487
488
  *
@@ -496,7 +497,8 @@ async function ensureIdentityOverrideConsent(
496
497
  const origin = await probeDeclaredOrigin(url)
497
498
  if (origin === undefined) {
498
499
  log.warn(
499
- `Could not read a declared origin from ${new URL('/meta', url).href} — ` +
500
+ `Could not read a declared origin from ` +
501
+ `${new URL('/.well-known/astrale/domain.json', url).href} — ` +
500
502
  `skipping the pre-install identity check (the installed origin is verified after install).`,
501
503
  )
502
504
  return undefined
@@ -534,19 +536,13 @@ async function ensureIdentityOverrideConsent(
534
536
  return origin
535
537
  }
536
538
 
537
- /** Best-effort read of the worker's declared origin from its `/meta` probe. */
539
+ /** Best-effort read of the worker's origin from its admitted canonical Publication. */
538
540
  export async function probeDeclaredOrigin(url: string): Promise<string | undefined> {
539
541
  try {
540
- const res = await fetch(new URL('/meta', url), { signal: AbortSignal.timeout(10_000) })
541
- if (!res.ok) return undefined
542
- // SDK workers serve `origin`; `domainName` is the pre-Kernel-V2 name, kept
543
- // as a fallback for workers deployed before the rename.
544
- const body = (await res.json()) as { origin?: unknown; domainName?: unknown }
545
- const declared = body.origin ?? body.domainName
546
- return typeof declared === 'string' && declared.length > 0 ? declared : undefined
542
+ return (await fetchDomainPublication(url, AbortSignal.timeout(10_000))).origin
547
543
  } catch {
548
- // Unreachable /meta is not fatal here: the caller warns and the install
549
- // itself will surface a dead worker with its own error.
544
+ // An unreachable or invalid Publication is not fatal here: the caller warns
545
+ // and the install itself will surface a dead worker with its own error.
550
546
  return undefined
551
547
  }
552
548
  }
@@ -6,6 +6,7 @@ import type { CommandDefinition } from '../../program/index'
6
6
 
7
7
  import { listAdminDomains, type DomainInfo } from '../../lib/admin-domain'
8
8
  import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
9
+ import { fetchDomainPublication } from '../../lib/domain-publication'
9
10
  import { fatal, withSpinner } from '../../lib/log'
10
11
  import { isMachine, presentList } from '../../lib/output'
11
12
 
@@ -23,7 +24,7 @@ type ListOpts = KernelCommandOpts &
23
24
  /** A catalog entry, optionally enriched with a live `--check` probe. */
24
25
  export type DomainRow = DomainInfo & {
25
26
  reachable?: boolean
26
- schemaHash?: string
27
+ schemaRevision?: string
27
28
  checkError?: string | null
28
29
  }
29
30
 
@@ -73,8 +74,8 @@ Behavior:
73
74
  one install URL per line (pipeable into \`domain install\`); --count prints
74
75
  only the number. --default-only keeps the install-by-default entries (what
75
76
  every new instance gets via alphaCreate). --check probes each published URL's
76
- /meta and adds a live/unreachable STATUS column (+ reachable/schemaHash in
77
- machine output).
77
+ canonical Publication and adds a live/unreachable STATUS column
78
+ (+ reachable/schemaRevision in machine output).
78
79
 
79
80
  The admin kernel is selected like every admin op — the configured default,
80
81
  or --admin <bookmark> / --admin-url <url>.
@@ -87,7 +88,10 @@ Examples:
87
88
  `,
88
89
  options: [
89
90
  ...ADMIN_TARGET_OPTIONS,
90
- { flags: '--check', description: "Probe each domain's URL for reachability + schema hash" },
91
+ {
92
+ flags: '--check',
93
+ description: "Probe each domain's canonical Publication + schema revision",
94
+ },
91
95
  { flags: '--default-only', description: 'Only show install-by-default domains' },
92
96
  { flags: '-q, --quiet', description: 'One install URL per line (unix-pipeable)' },
93
97
  { flags: '--count', description: 'Print only the number of published domains' },
@@ -103,7 +107,7 @@ Examples:
103
107
  const filtered = opts.defaultOnly ? list.filter((d) => d.installByDefault) : list
104
108
  filtered.sort(byDefaultThenName)
105
109
  if (!opts.check) return filtered as DomainRow[]
106
- // Reachability is a direct client-side /meta fetch per entry, in
110
+ // Reachability is a direct client-side Publication fetch per entry, in
107
111
  // parallel — no admin round-trip, and version-independent of the
108
112
  // admin worker (mirrors `domain install`'s probeDeclaredOrigin).
109
113
  return Promise.all(filtered.map(probe))
@@ -129,19 +133,20 @@ export function byDefaultThenName(a: DomainInfo, b: DomainInfo): number {
129
133
 
130
134
  /**
131
135
  * Enrich one entry with a reachability probe: fetch the published worker's
132
- * `/meta` (the same endpoint the admin `DomainEntry.check` hits) and read its
133
- * schema hash. A dead / missing url is itself a result, not a throw.
136
+ * canonical Publication and read its schema revision. A dead or missing URL is
137
+ * itself a result, not a throw.
134
138
  */
135
- async function probe(d: DomainInfo): Promise<DomainRow> {
139
+ export async function probe(d: DomainInfo): Promise<DomainRow> {
136
140
  if (!d.url) return { ...d, reachable: false, checkError: 'no url published' }
137
141
  try {
138
- const res = await fetch(new URL('/meta', d.url), { signal: AbortSignal.timeout(10_000) })
139
- if (!res.ok) return { ...d, reachable: false, checkError: `meta HTTP ${res.status}` }
140
- const meta = (await res.json()) as { schemaHash?: unknown }
142
+ const deployed = await fetchDomainPublication(d.url, AbortSignal.timeout(10_000))
143
+ if (deployed.origin !== d.origin) {
144
+ throw new Error(`Domain origin mismatch: deployed=${deployed.origin} expected=${d.origin}`)
145
+ }
141
146
  return {
142
147
  ...d,
143
148
  reachable: true,
144
- ...(typeof meta.schemaHash === 'string' ? { schemaHash: meta.schemaHash } : {}),
149
+ schemaRevision: deployed.schema.revision,
145
150
  checkError: null,
146
151
  }
147
152
  } catch (err) {
@@ -25,6 +25,23 @@ type StudioOpts = RawOutputOpts & {
25
25
  const STUDIO_PORT_BASE = 4319
26
26
  const VITE_PORT_BASE = 5273
27
27
  const PORT_SPAN = 20
28
+ const STUDIO_CLI_DESCRIPTOR_ENV = 'DOMAIN_STUDIO_CLI_DESCRIPTOR'
29
+
30
+ /**
31
+ * Describe this exact CLI invocation for the Studio server. A source/published
32
+ * script needs its entrypoint after node/bun; a Bun-compiled executable is
33
+ * self-contained and therefore has no prefix argument.
34
+ */
35
+ export function encodeStudioCliDescriptor(
36
+ executable = process.execPath,
37
+ entry = process.argv[1],
38
+ ): string {
39
+ const args =
40
+ entry && entry !== executable && !entry.startsWith('/$bunfs') && existsSync(entry)
41
+ ? [realpathSync(entry)]
42
+ : []
43
+ return JSON.stringify({ version: 1, executable, args })
44
+ }
28
45
 
29
46
  /**
30
47
  * Locate the studio package shipped with / alongside the CLI. Anchored to the
@@ -217,6 +234,7 @@ Examples:
217
234
  log.dim(` using ASTRALE_STUDIO_DIR=${studioDir}`)
218
235
  }
219
236
  await ensureBun()
237
+ const cliDescriptor = encodeStudioCliDescriptor()
220
238
 
221
239
  // Default to PROD — serve the prebuilt client. It always renders, is what a
222
240
  // published/global install runs, and avoids dev's heavy source-module graph.
@@ -342,6 +360,7 @@ Examples:
342
360
  VITE_URL: `http://127.0.0.1:${vitePort}`,
343
361
  PORT: String(studioPort),
344
362
  DOMAIN_STUDIO_HOST: '127.0.0.1',
363
+ [STUDIO_CLI_DESCRIPTOR_ENV]: cliDescriptor,
345
364
  ...(opts.harness ? { DOMAIN_STUDIO_HARNESS: opts.harness } : {}),
346
365
  },
347
366
  },
@@ -367,6 +386,7 @@ Examples:
367
386
  DOMAIN_STUDIO_DIST: dist,
368
387
  PORT: String(studioPort),
369
388
  DOMAIN_STUDIO_HOST: '127.0.0.1',
389
+ [STUDIO_CLI_DESCRIPTOR_ENV]: cliDescriptor,
370
390
  ...(opts.harness ? { DOMAIN_STUDIO_HARNESS: opts.harness } : {}),
371
391
  },
372
392
  },
@@ -12,7 +12,7 @@ import {
12
12
  type SdkOutdated,
13
13
  } from '../lib/sdk-deps'
14
14
  import { ASTRALE_CLI_SKILL, detectSkill, installSkills, SKILL_INSTALL_HINT } from '../lib/skills'
15
- import { updateAstrale } from '../lib/update'
15
+ import { DEFAULT_UPDATE_CHANNEL, updateAstrale } from '../lib/update'
16
16
 
17
17
  type UpdateOpts = RawOutputOpts & {
18
18
  check?: boolean
@@ -130,11 +130,12 @@ export async function cliStale(
130
130
  dependencies: CliStaleDependencies = CLI_STALE_DEPENDENCIES,
131
131
  ): Promise<StaleReport['cli']> {
132
132
  const running = pkg.version
133
+ const target = { ...opts, channel: opts.channel ?? DEFAULT_UPDATE_CHANNEL }
133
134
  try {
134
135
  const r = await dependencies.update({
135
136
  check: true,
136
- channel: opts.channel,
137
- version: opts.version,
137
+ channel: target.channel,
138
+ version: target.version,
138
139
  currentVersion: running,
139
140
  })
140
141
  if (r.status === 'updated') {
@@ -154,7 +155,7 @@ export async function cliStale(
154
155
  channel: r.channel,
155
156
  }
156
157
  } catch {
157
- const latest = await dependencies.fetchPackageVersion(opts).catch(() => undefined)
158
+ const latest = await dependencies.fetchPackageVersion(target).catch(() => undefined)
158
159
  return {
159
160
  stale: latest !== undefined && latest !== running,
160
161
  managed: true,
@@ -167,7 +168,8 @@ export async function cliStale(
167
168
  export async function fetchNpmTargetVersion(
168
169
  opts: Pick<UpdateOpts, 'channel' | 'version'>,
169
170
  ): Promise<string> {
170
- const target = opts.version ?? (opts.channel === 'stable' ? 'latest' : opts.channel) ?? 'latest'
171
+ const channel = opts.channel ?? DEFAULT_UPDATE_CHANNEL
172
+ const target = opts.version ?? (channel === 'stable' ? 'latest' : channel)
171
173
  const response = await fetch(`https://registry.npmjs.org/@astrale-os/cli/${target}`)
172
174
  if (!response.ok) throw new Error(`npm registry HTTP ${response.status}`)
173
175
  const body: unknown = await response.json()
@@ -197,6 +199,7 @@ export default {
197
199
  {
198
200
  flags: '--channel <name>',
199
201
  description: 'Update from a release channel (alpha, beta, rc, canary, stable)',
202
+ default: DEFAULT_UPDATE_CHANNEL,
200
203
  },
201
204
  { flags: '--version <version>', description: 'Update to an exact version tag' },
202
205
  { flags: '--no-skills', description: 'Skip refreshing the astrale agent skills' },
@@ -221,6 +224,7 @@ Behavior:
221
224
  the lockfile, honoring your registry + supply-chain age policy; run "pnpm
222
225
  install" to materialize).
223
226
 
227
+ The default release channel is beta; --channel overrides it for one run.
224
228
  --check is a dry run (binary + SDK deps; exit 10 if anything is available) and
225
229
  never writes. With --json it emits a unified staleness report
226
230
  ({ stale, cli, sdk }) for tooling — non-throwing, skills omitted (they ride
@@ -5,6 +5,10 @@ import { formatKernelError, functionInputIssues, schemaUpgradeHint } from '../er
5
5
 
6
6
  const CONFLICT = 4001
7
7
  const VALIDATION = 1003
8
+ const TEST_INVOCATION = {
9
+ source: 'https://kernel.test',
10
+ id: 'connection-errors',
11
+ } as ConstructorParameters<typeof ResponseError>[2]
8
12
 
9
13
  describe('formatKernelError', () => {
10
14
  /** @evidence TEST-CLI-CONNECTION-MAPS-TYPED-TRANSPORT */
@@ -87,7 +91,7 @@ describe('formatKernelError', () => {
87
91
  }) as typeof process.stderr.write
88
92
  try {
89
93
  await formatKernelError(
90
- new ResponseError(CONFLICT, 'Schema change requires migration.', {
94
+ new ResponseError(CONFLICT, 'Schema change requires migration.', TEST_INVOCATION, {
91
95
  code: 'DATA_MIGRATION_REQUIRED',
92
96
  details: {
93
97
  requirements: [
@@ -138,7 +142,7 @@ describe('formatKernelError', () => {
138
142
  }) as typeof process.stderr.write
139
143
  try {
140
144
  await formatKernelError(
141
- new ResponseError(CONFLICT, 'Schema change requires migration.', {
145
+ new ResponseError(CONFLICT, 'Schema change requires migration.', TEST_INVOCATION, {
142
146
  code: 'DATA_MIGRATION_REQUIRED',
143
147
  details: {
144
148
  requirements: [
@@ -193,7 +197,7 @@ describe('formatKernelError', () => {
193
197
  console.log = (...values: unknown[]) => hints.push(values.map(String).join(' '))
194
198
  try {
195
199
  await formatKernelError(
196
- new ResponseError(CONFLICT, 'Schema change requires migration.', {
200
+ new ResponseError(CONFLICT, 'Schema change requires migration.', TEST_INVOCATION, {
197
201
  code: 'DATA_MIGRATION_REQUIRED',
198
202
  details: {
199
203
  requirements: [
@@ -254,7 +258,7 @@ describe('formatKernelError', () => {
254
258
  console.log = (...values: unknown[]) => lines.push(values.join(' '))
255
259
  try {
256
260
  await formatKernelError(
257
- new ResponseError(VALIDATION, 'Function input is invalid.', {
261
+ new ResponseError(VALIDATION, 'Function input is invalid.', TEST_INVOCATION, {
258
262
  code: 'FUNCTION_INPUT_INVALID',
259
263
  details: {
260
264
  issues: [
@@ -279,7 +283,7 @@ describe('formatKernelError', () => {
279
283
  false,
280
284
  )
281
285
  await formatKernelError(
282
- new ResponseError(VALIDATION, 'Query input is invalid.', {
286
+ new ResponseError(VALIDATION, 'Query input is invalid.', TEST_INVOCATION, {
283
287
  code: 'QUERY_INPUT_INVALID',
284
288
  details: {
285
289
  phase: 'plan',
@@ -310,7 +314,7 @@ describe('formatKernelError', () => {
310
314
  }) as typeof process.stderr.write
311
315
  try {
312
316
  await formatKernelError(
313
- new ResponseError(5001, 'Schema operation is not supported.', {
317
+ new ResponseError(5001, 'Schema operation is not supported.', TEST_INVOCATION, {
314
318
  code: 'SCHEMA_UPGRADE_INCOMPATIBLE',
315
319
  details: {
316
320
  phase: 'upgrade',
@@ -358,7 +362,7 @@ describe('formatKernelError', () => {
358
362
  console.log = (...values: unknown[]) => details.push(values.map(String).join(' '))
359
363
  try {
360
364
  await formatKernelError(
361
- new ResponseError(5001, 'Schema operation is not supported.', {
365
+ new ResponseError(5001, 'Schema operation is not supported.', TEST_INVOCATION, {
362
366
  code: 'SCHEMA_UPGRADE_INCOMPATIBLE',
363
367
  details: {
364
368
  phase: 'upgrade',
@@ -391,7 +395,7 @@ describe('formatKernelError', () => {
391
395
  }) as typeof process.stderr.write
392
396
  try {
393
397
  await formatKernelError(
394
- new ResponseError(1003, 'Function input is invalid.', {
398
+ new ResponseError(1003, 'Function input is invalid.', TEST_INVOCATION, {
395
399
  code: 'SCHEMA_DOMAIN_ADDRESS_NOT_PUBLIC',
396
400
  details: { phase: 'publication', requiredScope: 'public' },
397
401
  }),
@@ -421,7 +425,7 @@ describe('formatKernelError', () => {
421
425
  console.log = (...values: unknown[]) => hints.push(values.map(String).join(' '))
422
426
  try {
423
427
  await formatKernelError(
424
- new ResponseError(1003, 'Function input is invalid.', {
428
+ new ResponseError(1003, 'Function input is invalid.', TEST_INVOCATION, {
425
429
  code: 'SCHEMA_DOMAIN_ADDRESS_NOT_PUBLIC',
426
430
  details: { phase: 'publication', requiredScope: 'public' },
427
431
  }),
@@ -448,7 +452,7 @@ describe('formatKernelError', () => {
448
452
  }) as typeof process.stderr.write
449
453
  try {
450
454
  await formatKernelError(
451
- new ResponseError(1003, 'Function input is invalid.', {
455
+ new ResponseError(1003, 'Function input is invalid.', TEST_INVOCATION, {
452
456
  code: 'FUNCTION_INPUT_INVALID',
453
457
  details: {
454
458
  issues: [
@@ -495,7 +499,7 @@ describe('formatKernelError', () => {
495
499
  console.log = (...values: unknown[]) => details.push(values.map(String).join(' '))
496
500
  try {
497
501
  await formatKernelError(
498
- new ResponseError(1003, 'Function input is invalid.', {
502
+ new ResponseError(1003, 'Function input is invalid.', TEST_INVOCATION, {
499
503
  code: 'FUNCTION_INPUT_INVALID',
500
504
  details: {
501
505
  issues: [
@@ -530,7 +534,7 @@ describe('formatKernelError', () => {
530
534
  }) as typeof process.stderr.write
531
535
  try {
532
536
  await formatKernelError(
533
- new ResponseError(1003, 'Function input is invalid.', {
537
+ new ResponseError(1003, 'Function input is invalid.', TEST_INVOCATION, {
534
538
  code: 'FUNCTION_INPUT_INVALID',
535
539
  details: { issues: [{ code: 'INVALID_FORMAT', path: '/customer/email' }] },
536
540
  }),
@@ -47,7 +47,7 @@ describe('Domain token exchange', () => {
47
47
  observed.push({ url, init, body })
48
48
  const index = observed.filter((entry) => entry.url === INVOCATION).length
49
49
  return invocationResponse(
50
- body.id,
50
+ body.requestId,
51
51
  index === 1
52
52
  ? { id: 'user-1' }
53
53
  : index === 2
@@ -121,7 +121,7 @@ describe('Domain token exchange', () => {
121
121
  if (url === INVOCATION) {
122
122
  const body = JSON.parse(await new Response(init?.body).text()) as Record<string, any>
123
123
  return invocationResponse(
124
- body.id,
124
+ body.requestId,
125
125
  body.call.input && Object.keys(body.call.input).length === 0
126
126
  ? { id: 'user-1' }
127
127
  : 'kernel-destination-envelope',
@@ -201,7 +201,7 @@ function exchangeFetch(
201
201
  if (url === INVOCATION) {
202
202
  const body = JSON.parse(await new Response(init?.body).text()) as Record<string, any>
203
203
  return invocationResponse(
204
- body.id,
204
+ body.requestId,
205
205
  body.call.input && Object.keys(body.call.input).length === 0
206
206
  ? { id: 'user-1' }
207
207
  : 'kernel-destination-envelope',
@@ -233,8 +233,9 @@ function configuration(enabled: boolean) {
233
233
  }
234
234
  }
235
235
 
236
- function invocationResponse(id: number, result: unknown, contentType: string): Response {
237
- return new Response(JSON.stringify({ id, result }), {
236
+ function invocationResponse(requestId: unknown, result: unknown, contentType: string): Response {
237
+ const invocation = { source: KERNEL, id: `exchange-${String(requestId)}` }
238
+ return new Response(JSON.stringify({ requestId, invocation, result }), {
238
239
  headers: { 'content-type': contentType, 'cache-control': 'no-store' },
239
240
  })
240
241
  }
@@ -33,12 +33,13 @@ export function createExchangeCredentialResolver(
33
33
  const client = new Client({ url: `${kernelIssuer}/invoke`, fetch, timeoutMs })
34
34
  try {
35
35
  const authenticated = client.as(sourceToken)
36
- const auth = createAuth((path, input, options) =>
37
- authenticated.call(call(path, input), {
36
+ const auth = createAuth(async (path, input, options) => {
37
+ const result = await authenticated.call(call(path, input), {
38
38
  ...options,
39
39
  delegate: { ttlSeconds: delegationTtlSeconds },
40
- }),
41
- )
40
+ })
41
+ return result.value
42
+ })
42
43
  const user = await auth.whoami({ signal })
43
44
  const key = Object.freeze({
44
45
  kernelIssuer,