@astrale-os/cli 1.0.0-beta.4 → 1.0.0-beta.6

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 (173) hide show
  1. package/dist/astrale.js +55649 -59388
  2. package/dist/public/connect-core.js +1972 -26831
  3. package/dist/public/keys/index.js +1754 -40686
  4. package/dist/public/paths/index.js +859 -39511
  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/idp.d.ts +1 -0
  8. package/dist/types/lib/instance-target.d.ts +1 -1
  9. package/dist/types/lib/log.d.ts +2 -2
  10. package/dist/types/lib/update.d.ts +30 -0
  11. package/package.json +4 -6
  12. package/src/__tests__/fixtures/publication.ts +20 -0
  13. package/src/admin/__tests__/binding.test.ts +14 -21
  14. package/src/admin/__tests__/fixture.ts +101 -0
  15. package/src/admin/binding.ts +86 -6
  16. package/src/admin/catalog/.spec/api.d.ts +2 -2
  17. package/src/admin/catalog/__tests__/client.test.ts +20 -49
  18. package/src/admin/catalog/client.ts +35 -26
  19. package/src/admin/catalog/model.ts +3 -4
  20. package/src/admin/graph/.spec/api.d.ts +4 -10
  21. package/src/admin/graph/nodes.ts +1 -1
  22. package/src/admin/instance/.spec/api.d.ts +2 -2
  23. package/src/admin/instance/__tests__/client.test.ts +58 -83
  24. package/src/admin/instance/client.ts +49 -39
  25. package/src/admin/instance/model.ts +9 -8
  26. package/src/commands/__tests__/call-describe.test.ts +46 -33
  27. package/src/commands/__tests__/domain-install-operation.test.ts +3 -14
  28. package/src/commands/__tests__/domain-install-owned.test.ts +1 -1
  29. package/src/commands/__tests__/domain-list.test.ts +5 -25
  30. package/src/commands/__tests__/install-identity-override.test.ts +3 -14
  31. package/src/commands/__tests__/read-commands.test.ts +35 -10
  32. package/src/commands/__tests__/update.test.ts +25 -3
  33. package/src/commands/call-describe.ts +59 -67
  34. package/src/commands/call.ts +41 -54
  35. package/src/commands/domain/install.ts +2 -2
  36. package/src/commands/domain/uninstall.ts +2 -2
  37. package/src/commands/get.ts +13 -14
  38. package/src/commands/identity/__tests__/register.test.ts +3 -3
  39. package/src/commands/identity/register.ts +5 -4
  40. package/src/commands/instance/delete.ts +5 -3
  41. package/src/commands/instance/status.ts +1 -9
  42. package/src/commands/introspect.ts +4 -11
  43. package/src/commands/logs.ts +24 -25
  44. package/src/commands/mutate.ts +6 -5
  45. package/src/commands/query.ts +27 -30
  46. package/src/commands/token.ts +8 -7
  47. package/src/commands/update.ts +31 -11
  48. package/src/commands/view.ts +4 -3
  49. package/src/connection/.spec/api.d.ts +7 -8
  50. package/src/connection/.spec/architecture.md +5 -4
  51. package/src/connection/.spec/flows/session.ts +2 -2
  52. package/src/connection/.spec/laws/connection.ts +6 -2
  53. package/src/connection/.spec/layout.ts +1 -0
  54. package/src/connection/__tests__/credential.test.ts +1 -1
  55. package/src/connection/__tests__/errors.test.ts +27 -20
  56. package/src/connection/__tests__/exchange.test.ts +30 -1
  57. package/src/connection/__tests__/failure-fixtures.ts +39 -0
  58. package/src/connection/__tests__/failure-safety.test.ts +75 -0
  59. package/src/connection/__tests__/self.test.ts +31 -11
  60. package/src/connection/__tests__/session.test.ts +3 -3
  61. package/src/connection/call.ts +1 -1
  62. package/src/connection/credential.ts +2 -2
  63. package/src/connection/errors.ts +1 -355
  64. package/src/connection/exchange.ts +47 -23
  65. package/src/connection/failure/classify.ts +110 -0
  66. package/src/connection/failure/debug.ts +26 -0
  67. package/src/connection/failure/index.ts +18 -0
  68. package/src/connection/failure/model.ts +28 -0
  69. package/src/connection/failure/render.ts +116 -0
  70. package/src/connection/reasons.ts +16 -0
  71. package/src/connection/self.ts +39 -21
  72. package/src/connection/session.ts +8 -8
  73. package/src/errors.ts +3 -2
  74. package/src/graph/.spec/api.d.ts +12 -5
  75. package/src/graph/.spec/layout.ts +9 -1
  76. package/src/graph/__tests__/mutation.test.ts +2 -2
  77. package/src/graph/__tests__/query.test.ts +25 -6
  78. package/src/graph/class.ts +30 -0
  79. package/src/graph/index.ts +1 -0
  80. package/src/graph/query.ts +21 -32
  81. package/src/identity/.spec/api.d.ts +2 -2
  82. package/src/identity/__tests__/registration.test.ts +1 -1
  83. package/src/identity/registration.ts +3 -3
  84. package/src/lib/__tests__/domain-publication.test.ts +4 -15
  85. package/src/lib/__tests__/idp.test.ts +25 -0
  86. package/src/lib/__tests__/instance-target.test.ts +6 -39
  87. package/src/lib/__tests__/log-errors.test.ts +53 -0
  88. package/src/lib/__tests__/studio-server-deps.test.ts +1 -1
  89. package/src/lib/__tests__/update.test.ts +101 -8
  90. package/src/lib/__tests__/view-open-intent.test.ts +0 -2
  91. package/src/lib/__tests__/view-server.test.ts +1 -1
  92. package/src/lib/__tests__/view-session.test.ts +1 -1
  93. package/src/lib/command-dx.ts +1 -4
  94. package/src/lib/domain-publication.ts +1 -1
  95. package/src/lib/idp.ts +29 -1
  96. package/src/lib/instance-target.ts +3 -14
  97. package/src/lib/log.ts +11 -6
  98. package/src/lib/update.ts +120 -26
  99. package/src/lib/validation.ts +2 -2
  100. package/src/lib/view/resolve.ts +1 -1
  101. package/src/program/__tests__/program.test.ts +1 -1
  102. package/studio/client/dist/assets/{elk-api-lwhFo7vB.js → elk-api-Di4OXGNH.js} +1 -1
  103. package/studio/client/dist/assets/index-BLm6J11H.js +81 -0
  104. package/studio/client/dist/assets/index-BiGR8Oo9.css +1 -0
  105. package/studio/client/dist/assets/index-DTjvkOUX.js +8 -0
  106. package/studio/client/dist/index.html +2 -2
  107. package/studio/package.json +2 -9
  108. package/studio/server/agent/ask.test.ts +1 -1
  109. package/studio/server/agent/bridge/grant.test.ts +1 -1
  110. package/studio/server/agent/bridge/routes.test.ts +1 -1
  111. package/studio/server/agent/harness/claude/adapter.test.ts +15 -5
  112. package/studio/server/agent/prompts/anchors.test.ts +32 -36
  113. package/studio/server/agent/prompts/anchors.ts +20 -49
  114. package/studio/server/agent/prompts/system.test.ts +3 -2
  115. package/studio/server/agent/prompts/system.ts +3 -3
  116. package/studio/server/agent/routes.test.ts +1 -1
  117. package/studio/server/agent/run/coordinator.test.ts +1 -1
  118. package/studio/server/api/canvas.ts +0 -1
  119. package/studio/server/api-agent-loadout.test.ts +1 -1
  120. package/studio/server/api.test.ts +1 -1
  121. package/studio/server/cache.test.ts +23 -12
  122. package/studio/server/cache.ts +12 -15
  123. package/studio/server/domain.test.ts +50 -68
  124. package/studio/server/domain.ts +82 -63
  125. package/studio/server/handoff/copy.ts +1 -1
  126. package/studio/server/index.ts +1 -1
  127. package/studio/server/instances/probe.test.ts +1 -1
  128. package/studio/server/introspect/anatomy/views/routes.ts +46 -37
  129. package/studio/server/introspect/anatomy/views.ts +9 -14
  130. package/studio/server/introspect/anatomy-extras.test.ts +26 -42
  131. package/studio/server/introspect/anatomy.test.ts +22 -14
  132. package/studio/server/introspect/anatomy.ts +6 -9
  133. package/studio/server/introspect/bundle.ts +0 -3
  134. package/studio/server/introspect/canonical-schema.test.ts +94 -389
  135. package/studio/server/introspect/canonical-schema.ts +227 -457
  136. package/studio/server/introspect/core-extractor.ts +18 -127
  137. package/studio/server/introspect/core.ts +1 -2
  138. package/studio/server/introspect/diff.test.ts +11 -7
  139. package/studio/server/introspect/diff.ts +18 -55
  140. package/studio/server/introspect/extractor.ts +22 -117
  141. package/studio/server/introspect/overlay-tsmorph.test.ts +119 -240
  142. package/studio/server/introspect/overlay.test.ts +26 -54
  143. package/studio/server/introspect/overlay.ts +10 -37
  144. package/studio/server/introspect/revision.test.ts +1 -1
  145. package/studio/server/introspect/revision.ts +1 -1
  146. package/studio/server/introspect/runtime.test.ts +65 -183
  147. package/studio/server/introspect/runtime.ts +8 -25
  148. package/studio/server/introspect/schema-ir-json.test.ts +70 -0
  149. package/studio/server/introspect/schema-ir-json.ts +38 -68
  150. package/studio/server/introspect/schema-refs.test.ts +43 -88
  151. package/studio/server/introspect/schema-refs.ts +14 -49
  152. package/studio/server/introspect/source-overlay/handlers.ts +116 -636
  153. package/studio/server/introspect/source-overlay/kernel-calls.ts +0 -3
  154. package/studio/server/introspect/source-overlay/spans.ts +15 -45
  155. package/studio/server/state/baseline.test.ts +14 -9
  156. package/studio/server/state/baseline.ts +3 -5
  157. package/studio/server/state/visibility.ts +1 -5
  158. package/studio/server/views/model.ts +3 -3
  159. package/studio/server/views/target.test.ts +17 -74
  160. package/studio/server/views/target.ts +24 -50
  161. package/studio/server/watch.test.ts +11 -11
  162. package/studio/server/watch.ts +8 -4
  163. package/studio/server/workspace-watch.ts +2 -2
  164. package/studio/shared/contracts/schema.ts +38 -107
  165. package/studio/shared/contracts/surface.test.ts +13 -15
  166. package/studio/shared/contracts/workspace.ts +2 -11
  167. package/studio/shared/schema/identity.test.ts +20 -44
  168. package/studio/shared/schema/identity.ts +9 -23
  169. package/viewer/dist/main.js +40 -61
  170. package/studio/client/dist/assets/index-B-c-smo9.js +0 -8
  171. package/studio/client/dist/assets/index-BckHuAWk.js +0 -81
  172. package/studio/client/dist/assets/index-C4aNQ6dz.css +0 -1
  173. package/studio/server/introspect/anatomy/views/legacy.ts +0 -232
@@ -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
  AdminDomainNotFoundError,
@@ -13,16 +22,9 @@ import {
13
22
  type PublishDomainResult,
14
23
  } from './model'
15
24
 
16
- const ADMIN_ORIGIN = 'admin.astrale.ai'
17
25
  const PAGE_SIZE = 256
18
26
  const MAXIMUM_DOMAINS = 10_000
19
27
  const MAXIMUM_PAGES = Math.ceil(MAXIMUM_DOMAINS / PAGE_SIZE) + 1
20
- const DomainRef = Object.freeze({ origin: ADMIN_ORIGIN, kind: 'class' as const, name: 'Domain' })
21
- const fleetInstallsByDefault = Object.freeze({
22
- origin: ADMIN_ORIGIN,
23
- kind: 'class' as const,
24
- name: 'fleet_installs_domain_by_default',
25
- })
26
28
 
27
29
  export interface AdminCatalogContext {
28
30
  readonly session: ClientSession
@@ -45,21 +47,22 @@ export async function connectAdminCatalog(
45
47
  context: AdminCatalogContext,
46
48
  dependencies: AdminCatalogDependencies = {},
47
49
  ): Promise<AdminCatalogApi> {
48
- const binding = await (dependencies.bind ?? bindAdmin)(context.session)
49
- if (binding.$.publication?.origin !== ADMIN_ORIGIN || binding.$.origin !== ADMIN_ORIGIN) {
50
- throw new TypeError('Configured Admin target does not serve the Admin Domain.')
51
- }
52
- const Domain = binding.$.class('Domain')
53
- const Fleet = binding.$.class('Fleet')
54
- const fleet = binding.$.core.nodes.fleet?.path
55
- if (fleet === undefined) throw new TypeError('Admin Domain has no singleton Fleet receiver.')
50
+ const binding = requireAdminBinding(await (dependencies.bind ?? bindAdmin)(context.session))
51
+ const Domain = requireAdminClass(binding, 'Domain', 'node')
52
+ const Fleet = requireAdminClass(binding, 'Fleet', 'node')
53
+ const fleetInstallsByDefault = requireAdminClass(
54
+ binding,
55
+ 'fleet_installs_domain_by_default',
56
+ 'edge',
57
+ )
58
+ const fleet = requireAdminCore(binding, 'fleet')
56
59
  const operationId = dependencies.operationId ?? defaultOperationId
57
60
 
58
61
  const list = async (): Promise<DomainInfo[]> => {
59
62
  const [nodes, defaultsPage] = await Promise.all([
60
63
  readAllNodes(
61
64
  context.graph,
62
- Query.from({ kind: 'node', definitions: [DomainRef] }).select({
65
+ Query.from({ nodes: [Domain] }).select({
63
66
  kind: 'nodes',
64
67
  projection: { kind: 'value' },
65
68
  }),
@@ -107,13 +110,13 @@ export async function connectAdminCatalog(
107
110
  let entry = existing
108
111
  if (registryChanged) {
109
112
  entry = domainFromSummary(
110
- await binding.$.invoke(Fleet.$.method('publishDomain') as never, fleet, {
113
+ await invokeAdminMethod(context.session, binding, Fleet, 'publishDomain', fleet, {
111
114
  operationId: operationId('publish'),
112
115
  origin: input.origin,
113
116
  name: input.name,
114
117
  discoveryUrl: input.url,
115
118
  ...(description === undefined ? {} : { description }),
116
- } as never),
119
+ }),
117
120
  existing?.installByDefault === true,
118
121
  )
119
122
  }
@@ -124,13 +127,16 @@ export async function connectAdminCatalog(
124
127
  (entry.installByDefault ?? false) !== input.installByDefault
125
128
  if (defaultChanged) {
126
129
  entry = domainFromSummary(
127
- await binding.$.invoke(
128
- Domain.$.method('configureDefault') as never,
130
+ await invokeAdminMethod(
131
+ context.session,
132
+ binding,
133
+ Domain,
134
+ 'configureDefault',
129
135
  Path.parse(entry.id),
130
136
  {
131
137
  operationId: operationId('configure-default'),
132
138
  enabled: input.installByDefault,
133
- } as never,
139
+ },
134
140
  ),
135
141
  input.installByDefault === true,
136
142
  )
@@ -144,7 +150,7 @@ export async function connectAdminCatalog(
144
150
  })
145
151
  }
146
152
 
147
- type DynamicDefinition = ReturnType<AdminBinding['$']['class']>
153
+ type DynamicDefinition = ResolvedClass<'node'>
148
154
 
149
155
  function domainFromNode(
150
156
  definition: DynamicDefinition,
@@ -180,7 +186,10 @@ function domainFromSummary(input: unknown, installByDefault: boolean): DomainInf
180
186
  }
181
187
 
182
188
  function requiredProperty(definition: DynamicDefinition, node: Node, name: string): string {
183
- return requiredString(node.props[definition.$.property(name).key], `Admin Domain.${name}`)
189
+ return requiredString(
190
+ node.props[requireAdminProperty(definition, name).key],
191
+ `Admin Domain.${name}`,
192
+ )
184
193
  }
185
194
 
186
195
  function optionalProperty(
@@ -188,7 +197,7 @@ function optionalProperty(
188
197
  node: Node,
189
198
  name: string,
190
199
  ): Readonly<Record<string, string>> {
191
- const value = node.props[definition.$.property(name).key]
200
+ const value = node.props[requireAdminProperty(definition, name).key]
192
201
  return value === undefined ? {} : { [name]: requiredString(value, `Admin Domain.${name}`) }
193
202
  }
194
203
 
@@ -33,10 +33,9 @@ export interface InstallDomainResult {
33
33
  readonly error?: string | null
34
34
  }
35
35
 
36
- export class AdminDomainNotFoundError extends Error {
37
- readonly name = 'NotFoundError'
38
-
36
+ export class AdminDomainNotFoundError extends AstraleError {
39
37
  constructor(readonly identifier: string) {
40
- super(`No visible Admin Domain matches ${JSON.stringify(identifier)}.`)
38
+ super('DOMAIN_NOT_FOUND', `No visible Admin Domain matches ${JSON.stringify(identifier)}.`)
41
39
  }
42
40
  }
41
+ import { AstraleError } from '../../errors'
@@ -1,18 +1,12 @@
1
- import type { GraphQueryOptions, NeighborsOptions, NodePage } from '@astrale-os/kernel-client/graph'
2
- import type { ClassPath } from '@astrale-os/sdk/graph/class'
1
+ import type { GraphApi } from '@astrale-os/sdk/client'
3
2
  import type { Node } from '@astrale-os/sdk/graph/node'
4
- import type { PathLike } from '@astrale-os/sdk/graph/path'
5
- import type { QueryAST, QueryResult } from '@astrale-os/sdk/query'
3
+ import type { QueryAST } from '@astrale-os/sdk/query'
6
4
 
7
5
  /** Smallest Kernel graph capability needed by bounded Admin graph reads. */
8
- export interface AdminGraphQueryApi {
9
- query(ast: QueryAST, options?: GraphQueryOptions): Promise<QueryResult>
10
- }
6
+ export type AdminGraphQueryApi = Pick<GraphApi, 'query'>
11
7
 
12
8
  /** Caller-scoped graph reads shared by the Admin catalog and Instance journeys. */
13
- export interface AdminGraphApi extends AdminGraphQueryApi {
14
- neighbors(source: PathLike, edge: ClassPath, options: NeighborsOptions): Promise<NodePage>
15
- }
9
+ export type AdminGraphApi = Pick<GraphApi, 'query' | 'neighbors'>
16
10
 
17
11
  export interface ReadAllNodesOptions {
18
12
  readonly label: string
@@ -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,5 +1,5 @@
1
- import type { DomainBinding } from '@astrale-os/kernel-client/domain'
2
- import type { ClientSession } from '@astrale-os/kernel-client/session'
1
+ import type { ClientSession } from '@astrale-os/sdk/client/session'
2
+ import type { DomainBinding } from '@astrale-os/shell'
3
3
 
4
4
  import type { AdminGraphApi } from '../../graph/.spec/api.js'
5
5
 
@@ -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'