@astrale-os/cli 1.0.0-beta.5 → 1.0.0-beta.7

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 (162) hide show
  1. package/dist/astrale.js +66254 -64461
  2. package/dist/public/connect-core.js +14895 -33867
  3. package/dist/public/keys/index.js +1246 -34250
  4. package/dist/public/paths/index.js +866 -33833
  5. package/dist/types/admin/instance/model.d.ts +3 -4
  6. package/dist/types/errors.d.ts +1 -1
  7. package/dist/types/lib/instance-target.d.ts +1 -1
  8. package/dist/types/lib/log.d.ts +2 -2
  9. package/package.json +4 -9
  10. package/src/__tests__/fixtures/publication.ts +20 -0
  11. package/src/admin/__tests__/binding.test.ts +12 -19
  12. package/src/admin/__tests__/fixture.ts +101 -0
  13. package/src/admin/binding.ts +83 -3
  14. package/src/admin/catalog/.spec/api.d.ts +1 -1
  15. package/src/admin/catalog/__tests__/client.test.ts +20 -49
  16. package/src/admin/catalog/client.ts +35 -26
  17. package/src/admin/catalog/model.ts +3 -4
  18. package/src/admin/graph/.spec/api.d.ts +4 -10
  19. package/src/admin/graph/nodes.ts +1 -1
  20. package/src/admin/instance/.spec/api.d.ts +1 -1
  21. package/src/admin/instance/__tests__/client.test.ts +58 -83
  22. package/src/admin/instance/client.ts +49 -39
  23. package/src/admin/instance/model.ts +9 -8
  24. package/src/commands/__tests__/call-describe.test.ts +46 -33
  25. package/src/commands/__tests__/domain-install-operation.test.ts +3 -14
  26. package/src/commands/__tests__/domain-install-owned.test.ts +1 -1
  27. package/src/commands/__tests__/domain-list.test.ts +5 -25
  28. package/src/commands/__tests__/install-identity-override.test.ts +3 -14
  29. package/src/commands/__tests__/read-commands.test.ts +35 -10
  30. package/src/commands/call-describe.ts +59 -67
  31. package/src/commands/call.ts +41 -54
  32. package/src/commands/domain/install.ts +2 -2
  33. package/src/commands/domain/uninstall.ts +2 -2
  34. package/src/commands/get.ts +13 -14
  35. package/src/commands/identity/__tests__/register.test.ts +3 -3
  36. package/src/commands/identity/register.ts +5 -4
  37. package/src/commands/instance/delete.ts +5 -3
  38. package/src/commands/instance/status.ts +1 -9
  39. package/src/commands/introspect.ts +4 -11
  40. package/src/commands/logs.ts +24 -25
  41. package/src/commands/mutate.ts +6 -5
  42. package/src/commands/query.ts +27 -30
  43. package/src/commands/token.ts +8 -7
  44. package/src/commands/view.ts +4 -3
  45. package/src/connection/.spec/api.d.ts +7 -8
  46. package/src/connection/.spec/architecture.md +5 -4
  47. package/src/connection/.spec/flows/session.ts +2 -2
  48. package/src/connection/.spec/laws/connection.ts +6 -2
  49. package/src/connection/.spec/layout.ts +1 -0
  50. package/src/connection/__tests__/credential.test.ts +1 -1
  51. package/src/connection/__tests__/errors.test.ts +27 -20
  52. package/src/connection/__tests__/exchange.test.ts +30 -1
  53. package/src/connection/__tests__/failure-fixtures.ts +39 -0
  54. package/src/connection/__tests__/failure-safety.test.ts +75 -0
  55. package/src/connection/__tests__/self.test.ts +31 -11
  56. package/src/connection/__tests__/session.test.ts +3 -3
  57. package/src/connection/call.ts +1 -1
  58. package/src/connection/credential.ts +2 -2
  59. package/src/connection/errors.ts +1 -355
  60. package/src/connection/exchange.ts +47 -23
  61. package/src/connection/failure/classify.ts +110 -0
  62. package/src/connection/failure/debug.ts +26 -0
  63. package/src/connection/failure/index.ts +18 -0
  64. package/src/connection/failure/model.ts +28 -0
  65. package/src/connection/failure/render.ts +116 -0
  66. package/src/connection/reasons.ts +16 -0
  67. package/src/connection/self.ts +39 -21
  68. package/src/connection/session.ts +8 -8
  69. package/src/errors.ts +3 -2
  70. package/src/graph/.spec/api.d.ts +12 -5
  71. package/src/graph/.spec/layout.ts +9 -1
  72. package/src/graph/__tests__/mutation.test.ts +2 -2
  73. package/src/graph/__tests__/query.test.ts +25 -6
  74. package/src/graph/class.ts +30 -0
  75. package/src/graph/index.ts +1 -0
  76. package/src/graph/query.ts +21 -32
  77. package/src/identity/.spec/api.d.ts +2 -2
  78. package/src/identity/__tests__/registration.test.ts +1 -1
  79. package/src/identity/registration.ts +3 -3
  80. package/src/lib/__tests__/domain-publication.test.ts +4 -15
  81. package/src/lib/__tests__/instance-target.test.ts +6 -39
  82. package/src/lib/__tests__/log-errors.test.ts +53 -0
  83. package/src/lib/__tests__/studio-server-deps.test.ts +1 -1
  84. package/src/lib/command-dx.ts +1 -4
  85. package/src/lib/domain-publication.ts +1 -1
  86. package/src/lib/instance-target.ts +3 -14
  87. package/src/lib/log.ts +11 -6
  88. package/src/lib/validation.ts +2 -2
  89. package/src/lib/view/resolve.ts +1 -1
  90. package/src/program/__tests__/program.test.ts +1 -1
  91. package/studio/client/dist/assets/{elk-api-YEDvaNU-.js → elk-api-Di4OXGNH.js} +1 -1
  92. package/studio/client/dist/assets/index-BLm6J11H.js +81 -0
  93. package/studio/client/dist/assets/index-BiGR8Oo9.css +1 -0
  94. package/studio/client/dist/assets/index-DTjvkOUX.js +8 -0
  95. package/studio/client/dist/index.html +2 -2
  96. package/studio/package.json +2 -7
  97. package/studio/server/agent/ask.test.ts +1 -1
  98. package/studio/server/agent/bridge/grant.test.ts +1 -1
  99. package/studio/server/agent/bridge/routes.test.ts +1 -1
  100. package/studio/server/agent/harness/claude/adapter.test.ts +15 -5
  101. package/studio/server/agent/prompts/anchors.test.ts +32 -36
  102. package/studio/server/agent/prompts/anchors.ts +20 -49
  103. package/studio/server/agent/prompts/system.test.ts +3 -2
  104. package/studio/server/agent/prompts/system.ts +3 -3
  105. package/studio/server/agent/routes.test.ts +1 -1
  106. package/studio/server/agent/run/coordinator.test.ts +1 -1
  107. package/studio/server/api/canvas.ts +0 -1
  108. package/studio/server/api-agent-loadout.test.ts +1 -1
  109. package/studio/server/api.test.ts +1 -1
  110. package/studio/server/cache.test.ts +23 -12
  111. package/studio/server/cache.ts +12 -15
  112. package/studio/server/domain.test.ts +50 -68
  113. package/studio/server/domain.ts +82 -63
  114. package/studio/server/handoff/copy.ts +1 -1
  115. package/studio/server/index.ts +1 -1
  116. package/studio/server/instances/probe.test.ts +1 -1
  117. package/studio/server/introspect/anatomy/views/routes.ts +46 -37
  118. package/studio/server/introspect/anatomy/views.ts +9 -14
  119. package/studio/server/introspect/anatomy-extras.test.ts +26 -42
  120. package/studio/server/introspect/anatomy.test.ts +22 -14
  121. package/studio/server/introspect/anatomy.ts +6 -9
  122. package/studio/server/introspect/bundle.ts +0 -3
  123. package/studio/server/introspect/canonical-schema.test.ts +94 -389
  124. package/studio/server/introspect/canonical-schema.ts +227 -457
  125. package/studio/server/introspect/core-extractor.ts +18 -127
  126. package/studio/server/introspect/core.ts +1 -2
  127. package/studio/server/introspect/diff.test.ts +11 -7
  128. package/studio/server/introspect/diff.ts +18 -55
  129. package/studio/server/introspect/extractor.ts +22 -117
  130. package/studio/server/introspect/overlay-tsmorph.test.ts +119 -240
  131. package/studio/server/introspect/overlay.test.ts +26 -54
  132. package/studio/server/introspect/overlay.ts +10 -37
  133. package/studio/server/introspect/revision.test.ts +1 -1
  134. package/studio/server/introspect/revision.ts +1 -1
  135. package/studio/server/introspect/runtime.test.ts +65 -183
  136. package/studio/server/introspect/runtime.ts +8 -25
  137. package/studio/server/introspect/schema-ir-json.test.ts +70 -0
  138. package/studio/server/introspect/schema-ir-json.ts +38 -68
  139. package/studio/server/introspect/schema-refs.test.ts +43 -88
  140. package/studio/server/introspect/schema-refs.ts +14 -49
  141. package/studio/server/introspect/source-overlay/handlers.ts +116 -636
  142. package/studio/server/introspect/source-overlay/kernel-calls.ts +0 -3
  143. package/studio/server/introspect/source-overlay/spans.ts +15 -45
  144. package/studio/server/state/baseline.test.ts +14 -9
  145. package/studio/server/state/baseline.ts +3 -5
  146. package/studio/server/state/visibility.ts +1 -5
  147. package/studio/server/views/model.ts +3 -3
  148. package/studio/server/views/target.test.ts +17 -74
  149. package/studio/server/views/target.ts +24 -50
  150. package/studio/server/watch.test.ts +11 -11
  151. package/studio/server/watch.ts +8 -4
  152. package/studio/server/workspace-watch.ts +2 -2
  153. package/studio/shared/contracts/schema.ts +38 -107
  154. package/studio/shared/contracts/surface.test.ts +12 -15
  155. package/studio/shared/contracts/workspace.ts +2 -11
  156. package/studio/shared/schema/identity.test.ts +20 -44
  157. package/studio/shared/schema/identity.ts +9 -23
  158. package/viewer/dist/main.js +41 -38
  159. package/studio/client/dist/assets/index-C-PvAXV4.js +0 -81
  160. package/studio/client/dist/assets/index-C4aNQ6dz.css +0 -1
  161. package/studio/client/dist/assets/index-CzpmJD0j.js +0 -8
  162. package/studio/server/introspect/anatomy/views/legacy.ts +0 -232
@@ -1,4 +1,4 @@
1
- import type { GraphApi } from '@astrale-os/kernel-client/graph'
1
+ import type { GraphApi } from '@astrale-os/sdk/client'
2
2
  import type { Node } from '@astrale-os/sdk/graph/node'
3
3
  import type { QueryAST } from '@astrale-os/sdk/query'
4
4
 
@@ -1,4 +1,4 @@
1
- import type { ClientSession } from '@astrale-os/kernel-client/session'
1
+ import type { ClientSession } from '@astrale-os/sdk/client/session'
2
2
  import type { DomainBinding } from '@astrale-os/shell'
3
3
 
4
4
  import type { AdminGraphApi } from '../../graph/.spec/api.js'
@@ -1,39 +1,28 @@
1
1
  import type { Node } from '@astrale-os/sdk/graph/node'
2
2
  import type { QueryAST } from '@astrale-os/sdk/query'
3
3
 
4
- import { ClassPath } from '@astrale-os/sdk/graph/class'
5
4
  import { NodeId } from '@astrale-os/sdk/graph/node'
6
5
  import { Path } from '@astrale-os/sdk/graph/path'
7
6
  import { describe, expect, mock, test } from 'bun:test'
8
7
 
9
- import type { AdminBinding } from '../../binding'
10
8
  import type { AdminGraphApi } from '../../graph'
11
9
 
10
+ import { AdminTestDomain, adminBinding, adminSession } from '../../__tests__/fixture'
12
11
  import { connectAdminInstances } from '../client'
13
12
 
14
- const methods: Array<{ owner: string; name: string }> = []
15
-
16
- function definition(name: string) {
13
+ function hostNode(id: string, props: { readonly id: string; readonly state: string }): Node {
17
14
  return {
18
- name,
19
- $: {
20
- method(method: string) {
21
- const value = { owner: name, name: method }
22
- methods.push(value)
23
- return value
24
- },
25
- property(property: string) {
26
- return { key: property }
27
- },
28
- },
15
+ id: NodeId(id),
16
+ class: AdminTestDomain.classes.Host.key,
17
+ props: AdminTestDomain.classes.Host.properties.from(props),
29
18
  }
30
19
  }
31
20
 
32
- function node(id: string, props: Record<string, unknown>): Node {
21
+ function instanceNode(id: string): Node {
33
22
  return {
34
23
  id: NodeId(id),
35
- class: ClassPath.from('admin.astrale.ai', 'Instance'),
36
- props: props as never,
24
+ class: AdminTestDomain.classes.Instance.key,
25
+ props: AdminTestDomain.classes.Instance.properties.from({}),
37
26
  }
38
27
  }
39
28
 
@@ -49,7 +38,7 @@ function graphResult(nodes: readonly Node[], cursor?: string) {
49
38
 
50
39
  function sourceName(ast: QueryAST): string | undefined {
51
40
  const term = ast.source.terms[0]
52
- return term?.kind === 'definition' ? term.definition.name : undefined
41
+ return term?.kind === 'class' ? term.class.name : undefined
53
42
  }
54
43
 
55
44
  function fixture(input: {
@@ -58,27 +47,16 @@ function fixture(input: {
58
47
  reserved?: Node | null
59
48
  invoke?: (method: { owner: string; name: string }, receiver: unknown, input: unknown) => unknown
60
49
  }) {
61
- methods.length = 0
62
- const Instance = definition('Instance')
63
- const Host = definition('Host')
64
- const Fleet = definition('Fleet')
65
- const invoke = mock(async (method: unknown, receiver: unknown, value: unknown) =>
66
- input.invoke?.(method as { owner: string; name: string }, receiver, value),
67
- )
68
- const binding = {
69
- $: {
70
- publication: { origin: 'admin.astrale.ai' },
71
- origin: 'admin.astrale.ai',
72
- class(name: string) {
73
- if (name === 'Instance') return Instance
74
- if (name === 'Host') return Host
75
- if (name === 'Fleet') return Fleet
76
- throw new Error(`Unexpected class ${name}`)
77
- },
78
- core: { nodes: { fleet: { path: Path.id(NodeId('fleet')) } } },
79
- invoke,
80
- },
81
- } as unknown as AdminBinding
50
+ const calls: Array<{
51
+ method: { owner: string; name: string }
52
+ receiver: string
53
+ value: unknown
54
+ }> = []
55
+ const remote = adminSession((method, receiver, value) => {
56
+ calls.push({ method, receiver: String(receiver), value })
57
+ return input.invoke?.(method, receiver, value)
58
+ })
59
+ const binding = adminBinding()
82
60
  const query = mock(async (ast: QueryAST) => {
83
61
  const name = sourceName(ast)
84
62
  if (name === 'Instance') return graphResult(input.instances ?? [])
@@ -86,7 +64,7 @@ function fixture(input: {
86
64
  throw new Error(`Unexpected graph query ${String(name)}`)
87
65
  })
88
66
  const neighbors = mock(async (source: unknown, edge: { name?: string }) => {
89
- if (String(source) === '@fleet') return page(input.reserved ?? null)
67
+ if (String(source) === '/:admin.astrale.ai:core.fleet') return page(input.reserved ?? null)
90
68
  if (edge.name === 'instance_runs_on_host') return page(input.hosts?.[0] ?? null)
91
69
  throw new Error(`Unexpected neighbor query ${String(source)} ${String(edge)}`)
92
70
  })
@@ -94,10 +72,11 @@ function fixture(input: {
94
72
  return {
95
73
  binding,
96
74
  graph,
97
- invoke,
75
+ invoke: remote.invoke,
76
+ calls,
98
77
  connect: () =>
99
78
  connectAdminInstances(
100
- { session: {} as never, graph },
79
+ { session: remote.session, graph },
101
80
  {
102
81
  bind: async () => binding,
103
82
  operationId: (kind) => `cli.instance.${kind}:test`,
@@ -146,11 +125,13 @@ describe('V2 Admin Instance adapter', () => {
146
125
  region: 'fr-par',
147
126
  },
148
127
  ])
149
- expect(contract.invoke).toHaveBeenCalledWith(
150
- expect.objectContaining({ owner: 'Fleet', name: 'listInstances' }),
151
- Path.id(NodeId('fleet')),
152
- {},
153
- )
128
+ expect(contract.calls).toEqual([
129
+ {
130
+ method: { owner: 'Fleet', name: 'listInstances' },
131
+ receiver: '/:admin.astrale.ai:core.fleet::listInstances',
132
+ value: {},
133
+ },
134
+ ])
154
135
  })
155
136
 
156
137
  test('delegates default placement to Fleet without reading Host inventory', async () => {
@@ -163,7 +144,7 @@ describe('V2 Admin Instance adapter', () => {
163
144
  updatedAt: '2026-08-12T00:00:00.000Z',
164
145
  }
165
146
  const contract = fixture({
166
- hosts: [node('host-node', { id: 'host-paris', state: 'ready' })],
147
+ hosts: [hostNode('host-node', { id: 'host-paris', state: 'ready' })],
167
148
  invoke: () => created,
168
149
  })
169
150
 
@@ -174,18 +155,20 @@ describe('V2 Admin Instance adapter', () => {
174
155
  state: created.state,
175
156
  createdAt: created.createdAt,
176
157
  })
177
- expect(contract.invoke).toHaveBeenCalledWith(
178
- expect.objectContaining({ owner: 'Fleet', name: 'createInstance' }),
179
- Path.id(NodeId('fleet')),
180
- { operationId: 'cli.instance.create:test', slug: 'demo' },
181
- )
158
+ expect(contract.calls).toEqual([
159
+ {
160
+ method: { owner: 'Fleet', name: 'createInstance' },
161
+ receiver: '/:admin.astrale.ai:core.fleet::createInstance',
162
+ value: { operationId: 'cli.instance.create:test', slug: 'demo' },
163
+ },
164
+ ])
182
165
  expect(contract.graph.query).not.toHaveBeenCalled()
183
166
  expect(contract.graph.neighbors).not.toHaveBeenCalled()
184
167
  })
185
168
 
186
169
  test('resolves --host-id to an exact Host receiver and rejects the reserved Admin Host', async () => {
187
- const reserved = node('admin-host', { id: 'admin', state: 'ready' })
188
- const consumer = node('consumer-host', { id: 'consumer-paris', state: 'ready' })
170
+ const reserved = hostNode('admin-host', { id: 'admin', state: 'ready' })
171
+ const consumer = hostNode('consumer-host', { id: 'consumer-paris', state: 'ready' })
189
172
  const contract = fixture({
190
173
  hosts: [reserved, consumer],
191
174
  reserved,
@@ -198,21 +181,17 @@ describe('V2 Admin Instance adapter', () => {
198
181
  })
199
182
  const api = await contract.connect()
200
183
 
201
- await expect(api.create('demo', 'admin')).rejects.toMatchObject({ name: 'NotFoundError' })
184
+ await expect(api.create('demo', 'admin')).rejects.toMatchObject({ code: 'HOST_NOT_FOUND' })
202
185
  await expect(api.create('demo', 'consumer-paris')).resolves.toMatchObject({ slug: 'demo' })
203
- expect(contract.invoke).toHaveBeenCalledWith(
204
- expect.objectContaining({ owner: 'Host', name: 'createInstance' }),
205
- Path.id(NodeId('consumer-host')),
206
- { operationId: 'cli.instance.create:test', slug: 'demo' },
207
- )
186
+ expect(contract.calls.at(-1)).toEqual({
187
+ method: { owner: 'Host', name: 'createInstance' },
188
+ receiver: '@consumer-host::createInstance',
189
+ value: { operationId: 'cli.instance.create:test', slug: 'demo' },
190
+ })
208
191
  })
209
192
 
210
193
  test('refreshes and deletes through the resolved Instance receiver', async () => {
211
- const instance = node('instance-node', {
212
- slug: 'demo',
213
- url: 'https://demo.eu.astrale.ai',
214
- state: 'ready',
215
- })
194
+ const instance = instanceNode('instance-node')
216
195
  const contract = fixture({
217
196
  instances: [instance],
218
197
  invoke: (method) =>
@@ -238,20 +217,16 @@ describe('V2 Admin Instance adapter', () => {
238
217
 
239
218
  await expect(api.status('demo')).resolves.toMatchObject({ state: 'ready' })
240
219
  await expect(api.delete('@instance-node')).resolves.toMatchObject({ state: 'deleted' })
241
- expect(contract.invoke.mock.calls.map(([method]) => method)).toEqual([
242
- expect.objectContaining({ owner: 'Fleet', name: 'listInstances' }),
243
- expect.objectContaining({ owner: 'Instance', name: 'status' }),
244
- expect.objectContaining({ owner: 'Fleet', name: 'listInstances' }),
245
- expect.objectContaining({ owner: 'Instance', name: 'delete' }),
220
+ expect(contract.calls.map(({ method }) => method)).toEqual([
221
+ { owner: 'Fleet', name: 'listInstances' },
222
+ { owner: 'Instance', name: 'status' },
223
+ { owner: 'Fleet', name: 'listInstances' },
224
+ { owner: 'Instance', name: 'delete' },
246
225
  ])
247
226
  })
248
227
 
249
228
  test('installs a resolved catalog Domain through Instance.installDomain', async () => {
250
- const instance = node('instance-node', {
251
- slug: 'demo',
252
- url: 'https://demo.eu.astrale.ai',
253
- state: 'ready',
254
- })
229
+ const instance = instanceNode('instance-node')
255
230
  const contract = fixture({
256
231
  instances: [instance],
257
232
  invoke: (method) =>
@@ -282,10 +257,10 @@ describe('V2 Admin Instance adapter', () => {
282
257
  origin: 'crm.acme.dev',
283
258
  ok: true,
284
259
  })
285
- expect(contract.invoke).toHaveBeenCalledWith(
286
- expect.objectContaining({ owner: 'Instance', name: 'installDomain' }),
287
- Path.id(NodeId('instance-node')),
288
- { operationId: 'cli.instance.install-domain:test', domain: '@crm-domain' },
289
- )
260
+ expect(contract.calls.at(-1)).toEqual({
261
+ method: { owner: 'Instance', name: 'installDomain' },
262
+ receiver: '@instance-node::installDomain',
263
+ value: { operationId: 'cli.instance.install-domain:test', domain: '@crm-domain' },
264
+ })
290
265
  })
291
266
  })
@@ -1,10 +1,19 @@
1
- import type { ClientSession } from '@astrale-os/kernel-client/session'
1
+ import type { ClientSession } from '@astrale-os/sdk/client/session'
2
2
  import type { Node } from '@astrale-os/sdk/graph/node'
3
+ import type { ResolvedClass } from '@astrale-os/sdk/schema'
3
4
 
4
5
  import { Path } from '@astrale-os/sdk/graph/path'
5
6
  import { Query } from '@astrale-os/sdk/query'
6
7
 
7
- import { bindAdmin, type AdminBinding } from '../binding'
8
+ import {
9
+ bindAdmin,
10
+ invokeAdminMethod,
11
+ requireAdminBinding,
12
+ requireAdminClass,
13
+ requireAdminCore,
14
+ requireAdminProperty,
15
+ type AdminBinding,
16
+ } from '../binding'
8
17
  import { readAllNodes, type AdminGraphApi } from '../graph'
9
18
  import {
10
19
  AdminHostNotFoundError,
@@ -16,18 +25,10 @@ import {
16
25
  type OwnedInstanceInfo,
17
26
  } from './model'
18
27
 
19
- const ADMIN_ORIGIN = 'admin.astrale.ai'
20
28
  const PAGE_SIZE = 256
21
29
  const MAXIMUM_INSTANCES = 10_000
22
30
  const MAXIMUM_PAGES = Math.ceil(MAXIMUM_INSTANCES / PAGE_SIZE) + 1
23
31
 
24
- const HostRef = Object.freeze({ origin: ADMIN_ORIGIN, kind: 'class' as const, name: 'Host' })
25
- const fleetReservesAdminHost = Object.freeze({
26
- origin: ADMIN_ORIGIN,
27
- kind: 'class' as const,
28
- name: 'fleet_reserves_admin_host',
29
- })
30
-
31
32
  export interface AdminInstanceContext {
32
33
  readonly session: ClientSession
33
34
  readonly graph: AdminGraphApi
@@ -54,24 +55,24 @@ export async function connectAdminInstances(
54
55
  context: AdminInstanceContext,
55
56
  dependencies: AdminInstanceDependencies = {},
56
57
  ): Promise<AdminInstanceApi> {
57
- const binding = await (dependencies.bind ?? bindAdmin)(context.session)
58
- if (binding.$.publication?.origin !== ADMIN_ORIGIN || binding.$.origin !== ADMIN_ORIGIN) {
59
- throw new TypeError('Configured Admin target does not serve the Admin Domain.')
60
- }
58
+ const binding = requireAdminBinding(await (dependencies.bind ?? bindAdmin)(context.session))
61
59
 
62
- const Instance = binding.$.class('Instance')
63
- const Host = binding.$.class('Host')
64
- const Fleet = binding.$.class('Fleet')
65
- const fleet = binding.$.core.nodes.fleet?.path
66
- if (fleet === undefined) throw new TypeError('Admin Domain has no singleton Fleet receiver.')
60
+ const Instance = requireAdminClass(binding, 'Instance', 'node')
61
+ const Host = requireAdminClass(binding, 'Host', 'node')
62
+ const Fleet = requireAdminClass(binding, 'Fleet', 'node')
63
+ const fleetReservesAdminHost = requireAdminClass(binding, 'fleet_reserves_admin_host', 'edge')
64
+ const fleet = requireAdminCore(binding, 'fleet')
67
65
 
68
66
  const operationId = dependencies.operationId ?? defaultOperationId
69
67
 
70
68
  const list = async (): Promise<OwnedInstanceInfo[]> => {
71
- const result: unknown = await binding.$.invoke(
72
- Fleet.$.method('listInstances') as never,
69
+ const result: unknown = await invokeAdminMethod(
70
+ context.session,
71
+ binding,
72
+ Fleet,
73
+ 'listInstances',
73
74
  fleet,
74
- {} as never,
75
+ {},
75
76
  )
76
77
  if (!Array.isArray(result)) throw new TypeError('Admin Instance inventory is invalid.')
77
78
  return result.map((value) => instanceFromSummary(value) as OwnedInstanceInfo)
@@ -88,10 +89,13 @@ export async function connectAdminInstances(
88
89
  identifier: string,
89
90
  ): Promise<InstanceInfo> => {
90
91
  const instance = await requireInstance(identifier)
91
- const output = await binding.$.invoke(
92
- Instance.$.method(method) as never,
92
+ const output = await invokeAdminMethod(
93
+ context.session,
94
+ binding,
95
+ Instance,
96
+ method,
93
97
  Path.parse(instance.id),
94
- { operationId: operationId(method) } as never,
98
+ { operationId: operationId(method) },
95
99
  )
96
100
  return instanceFromSummary(output)
97
101
  }
@@ -104,16 +108,19 @@ export async function connectAdminInstances(
104
108
  slug,
105
109
  })
106
110
  if (hostId !== undefined) {
107
- const selected = await hostInventory(context.graph, binding, Host, fleet)
111
+ const selected = await hostInventory(context.graph, Host, fleetReservesAdminHost, fleet)
108
112
  const host = selected.hosts.find(
109
113
  (item) => item.id === hostId && item.state === 'ready' && item.path !== selected.reserved,
110
114
  )
111
115
  if (host === undefined) throw new AdminHostNotFoundError(hostId)
112
116
  return instanceFromSummary(
113
- await binding.$.invoke(
114
- Host.$.method('createInstance') as never,
117
+ await invokeAdminMethod(
118
+ context.session,
119
+ binding,
120
+ Host,
121
+ 'createInstance',
115
122
  Path.parse(host.path),
116
- input as never,
123
+ input,
117
124
  ),
118
125
  )
119
126
  }
@@ -122,20 +129,23 @@ export async function connectAdminInstances(
122
129
  // it requires broader graph authority than this Fleet operation and can
123
130
  // deny an otherwise authorized caller before the server selects a Host.
124
131
  return instanceFromSummary(
125
- await binding.$.invoke(Fleet.$.method('createInstance') as never, fleet, input as never),
132
+ await invokeAdminMethod(context.session, binding, Fleet, 'createInstance', fleet, input),
126
133
  )
127
134
  },
128
135
  status: (identifier: string) => invokeInstance('status', identifier),
129
136
  delete: (identifier: string) => invokeInstance('delete', identifier),
130
137
  async installDomain(identifier: string, domain: string): Promise<DomainInstallReceipt> {
131
138
  const instance = await requireInstance(identifier)
132
- const output = await binding.$.invoke(
133
- Instance.$.method('installDomain') as never,
139
+ const output = await invokeAdminMethod(
140
+ context.session,
141
+ binding,
142
+ Instance,
143
+ 'installDomain',
134
144
  Path.parse(instance.id),
135
145
  {
136
146
  operationId: operationId('install-domain'),
137
147
  domain: Path.parse(domain).raw,
138
- } as never,
148
+ },
139
149
  )
140
150
  return domainInstallReceipt(output)
141
151
  },
@@ -151,14 +161,14 @@ interface HostInventoryItem {
151
161
 
152
162
  async function hostInventory(
153
163
  graph: AdminGraphApi,
154
- binding: AdminBinding,
155
- Host: ReturnType<AdminBinding['$']['class']>,
164
+ Host: ResolvedClass<'node'>,
165
+ fleetReservesAdminHost: ResolvedClass<'edge'>,
156
166
  fleet: Path,
157
167
  ): Promise<{ readonly hosts: readonly HostInventoryItem[]; readonly reserved?: string }> {
158
168
  const [nodes, reservedPage] = await Promise.all([
159
169
  readAllNodes(
160
170
  graph,
161
- Query.from({ kind: 'node', definitions: [HostRef] }).select({
171
+ Query.from({ nodes: [Host] }).select({
162
172
  kind: 'nodes',
163
173
  projection: { kind: 'value' },
164
174
  }),
@@ -189,7 +199,7 @@ async function hostInventory(
189
199
  })
190
200
  }
191
201
 
192
- type DynamicDefinition = ReturnType<AdminBinding['$']['class']>
202
+ type DynamicDefinition = ResolvedClass<'node'>
193
203
 
194
204
  function instanceFromSummary(input: unknown): InstanceInfo {
195
205
  const value = record(input, 'Admin Instance summary')
@@ -240,8 +250,8 @@ function domainInstallReceipt(input: unknown): DomainInstallReceipt {
240
250
 
241
251
  function requiredStringProperty(definition: DynamicDefinition, node: Node, name: string): string {
242
252
  return requiredString(
243
- node.props[definition.$.property(name).key],
244
- `Admin ${definition.name}.${name}`,
253
+ node.props[requireAdminProperty(definition, name).key],
254
+ `Admin ${definition.ref.name}.${name}`,
245
255
  )
246
256
  }
247
257
 
@@ -28,19 +28,19 @@ export interface DomainInstallReceipt {
28
28
  readonly error?: string
29
29
  }
30
30
 
31
- export class AdminInstanceNotFoundError extends Error {
32
- readonly name = 'NotFoundError'
33
-
31
+ export class AdminInstanceNotFoundError extends AstraleError {
34
32
  constructor(readonly identifier: string) {
35
- super(`No owned Admin Instance matches ${JSON.stringify(identifier)}.`)
33
+ super(
34
+ 'INSTANCE_NOT_FOUND',
35
+ `No owned Admin Instance matches ${JSON.stringify(identifier)}.`,
36
+ 'Run `astrale instance list` to see your instances.',
37
+ )
36
38
  }
37
39
  }
38
40
 
39
- export class AdminHostNotFoundError extends Error {
40
- readonly name = 'NotFoundError'
41
-
41
+ export class AdminHostNotFoundError extends AstraleError {
42
42
  constructor(readonly identifier: string) {
43
- super(`No caller-usable Admin Host matches ${JSON.stringify(identifier)}.`)
43
+ super('HOST_NOT_FOUND', `No caller-usable Admin Host matches ${JSON.stringify(identifier)}.`)
44
44
  }
45
45
  }
46
46
 
@@ -63,3 +63,4 @@ export function formatInstanceLocation(info: InstanceInfo): string {
63
63
  .filter(Boolean)
64
64
  .join(' · ')
65
65
  }
66
+ import { AstraleError } from '../../errors'
@@ -1,34 +1,47 @@
1
1
  import { Path } from '@astrale-os/sdk/graph/path'
2
+ import { bundle, defineSchema, func, method, nodeClass } from '@astrale-os/sdk/schema'
2
3
  import { describe, expect, test } from 'bun:test'
3
4
 
4
- import { describeCallableFromSchema } from '../call-describe'
5
+ import { describeCallableFromBundle } from '../call-describe'
5
6
 
6
- const schema = {
7
- classes: {
8
- Manager: {
9
- methods: {
10
- createInstance: {
11
- auth: 'authorized',
12
- description: 'Create one child Instance.',
13
- input: { type: 'object', required: ['operationId', 'slug'] },
14
- output: { mode: 'value' },
15
- },
16
- },
17
- },
7
+ const createInstance = method({
8
+ auth: 'authenticated',
9
+ description: 'Create one child Instance.',
10
+ input: {
11
+ type: 'object',
12
+ properties: { operationId: { type: 'string' }, slug: { type: 'string' } },
13
+ required: ['operationId', 'slug'],
14
+ additionalProperties: false,
18
15
  },
19
- functions: {
20
- journal: {
21
- description: 'Read the authorized Kernel journal.',
22
- input: { type: 'object' },
23
- },
16
+ output: { type: 'object', additionalProperties: true },
17
+ static: true,
18
+ })
19
+ const inspectInstance = method({
20
+ auth: 'authenticated',
21
+ description: 'Inspect one child Instance.',
22
+ input: { type: 'object', properties: {}, additionalProperties: false },
23
+ output: { type: 'object', additionalProperties: true },
24
+ })
25
+ const journal = func({
26
+ auth: 'authenticated',
27
+ description: 'Read the authorized Kernel journal.',
28
+ input: { type: 'object', properties: {}, additionalProperties: false },
29
+ output: { type: 'object', additionalProperties: true },
30
+ })
31
+ const source = defineSchema('host.astrale.ai', {
32
+ classes: {
33
+ Manager: nodeClass({ methods: { createInstance } }),
34
+ Instance: nodeClass({ methods: { inspectInstance } }),
24
35
  },
25
- }
36
+ functions: { journal },
37
+ })
38
+ const installed = bundle.create(source)
26
39
 
27
- describe('describeCallableFromSchema', () => {
28
- test('reads a static Class method from the Domain schema', () => {
29
- const described = describeCallableFromSchema(
40
+ describe('describeCallableFromBundle', () => {
41
+ test('reads a static Class method from the resolved Domain', () => {
42
+ const described = describeCallableFromBundle(
30
43
  Path.parse('/:host.astrale.ai:class.Manager:createInstance'),
31
- schema,
44
+ installed,
32
45
  )
33
46
  expect(described).toMatchObject({
34
47
  origin: 'host.astrale.ai',
@@ -36,30 +49,30 @@ describe('describeCallableFromSchema', () => {
36
49
  method: 'createInstance',
37
50
  dispatch: 'static',
38
51
  description: 'Create one child Instance.',
39
- auth: 'authorized',
52
+ auth: 'authenticated',
40
53
  })
41
54
  expect(described?.input).toMatchObject({ required: ['operationId', 'slug'] })
42
55
  })
43
56
 
44
- test('finds an instance method when the receiver Path is not a Class projection', () => {
57
+ test('finds one instance method when the receiver Path is not a Class projection', () => {
45
58
  expect(
46
- describeCallableFromSchema(
47
- Path.parse('/:host.astrale.ai:core.manager::createInstance'),
48
- schema,
59
+ describeCallableFromBundle(
60
+ Path.parse('/:host.astrale.ai:core.manager::inspectInstance'),
61
+ installed,
49
62
  ),
50
63
  ).toMatchObject({
51
64
  origin: 'host.astrale.ai',
52
- class: 'Manager',
53
- method: 'createInstance',
65
+ class: 'Instance',
66
+ method: 'inspectInstance',
54
67
  dispatch: 'instance',
55
68
  })
56
69
  })
57
70
 
58
- test('reads a standalone Function from the Domain schema', () => {
71
+ test('reads a standalone Function from the resolved Domain', () => {
59
72
  expect(
60
- describeCallableFromSchema(Path.parse('/:kernel.astrale.ai:function.journal'), schema),
73
+ describeCallableFromBundle(Path.parse('/:host.astrale.ai:function.journal'), installed),
61
74
  ).toMatchObject({
62
- origin: 'kernel.astrale.ai',
75
+ origin: 'host.astrale.ai',
63
76
  function: 'journal',
64
77
  description: 'Read the authorized Kernel journal.',
65
78
  })
@@ -1,9 +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
+ import { defineSchema } from '@astrale-os/sdk/schema'
5
2
  import { afterEach, describe, expect, mock, test } from 'bun:test'
6
3
 
4
+ import { releaseFor } from '../../__tests__/fixtures/publication'
7
5
  import { installDirect } from '../domain/install'
8
6
 
9
7
  const GENERATED = '4a4c9a18-50f6-4d84-a7b7-2d83e3e45dc8'
@@ -12,16 +10,7 @@ const RETRY = '139137b5-af47-47ce-92b2-b64a2b0c63d7'
12
10
  const originalFetch = globalThis.fetch
13
11
 
14
12
  const schema = defineSchema('crm.test', {})
15
- const definition = defineDomain({
16
- schema,
17
- handlers: { functions: {}, classes: {}, interfaces: {} },
18
- })
19
- const deployed = createDeployment({
20
- definition,
21
- issuer: issuer.accept('https://crm.test'),
22
- bundleHref: 'https://crm.test/domain.bundle.json',
23
- bindings: { callables: [] },
24
- }).publication
13
+ const deployed = releaseFor(schema, 'https://crm.test').publication
25
14
 
26
15
  function publicationResponse(): Response {
27
16
  return Response.json(deployed)
@@ -1,4 +1,4 @@
1
- import { ResponseError } from '@astrale-os/kernel-client'
1
+ import { ResponseError } from '@astrale-os/sdk/client'
2
2
  import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
3
3
 
4
4
  import type { OwnedInstanceInfo } from '../../lib/admin-instance'