@astrale-os/cli 1.0.0-beta.5 → 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 (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
@@ -24,17 +24,16 @@ export interface DomainInstallReceipt {
24
24
  readonly installedRevision?: string;
25
25
  readonly error?: string;
26
26
  }
27
- export declare class AdminInstanceNotFoundError extends Error {
27
+ export declare class AdminInstanceNotFoundError extends AstraleError {
28
28
  readonly identifier: string;
29
- readonly name = "NotFoundError";
30
29
  constructor(identifier: string);
31
30
  }
32
- export declare class AdminHostNotFoundError extends Error {
31
+ export declare class AdminHostNotFoundError extends AstraleError {
33
32
  readonly identifier: string;
34
- readonly name = "NotFoundError";
35
33
  constructor(identifier: string);
36
34
  }
37
35
  /** Match an owner-scoped Instance by slug, canonical Node Path, or bare Node id. */
38
36
  export declare function findOwnedInstance(instances: readonly OwnedInstanceInfo[], identifier: string): OwnedInstanceInfo | undefined;
39
37
  /** Human-readable location of an Instance (state, region, and Host Path). */
40
38
  export declare function formatInstanceLocation(info: InstanceInfo): string;
39
+ import { AstraleError } from '../../errors';
@@ -1,7 +1,7 @@
1
1
  export declare class AstraleError extends Error {
2
2
  code: string;
3
3
  hint?: string;
4
- constructor(code: string, message: string, hint?: string);
4
+ constructor(code: string, message: string, hint?: string, options?: ErrorOptions);
5
5
  }
6
6
  export declare class AuthError extends AstraleError {
7
7
  constructor(message: string, hint?: string);
@@ -1,5 +1,5 @@
1
- import type { InstanceInfo } from '../admin/instance/model';
2
1
  import type { AstraleConfig } from './config';
2
+ import { type InstanceInfo } from '../admin/instance/model';
3
3
  import { type AdminTargetCommandOpts, type ResolvedAdminTarget } from './admin-target';
4
4
  import { type InstanceStore } from './instance';
5
5
  export type InstanceTargetRequest = {
@@ -11,8 +11,8 @@ export declare const log: {
11
11
  /** Report an error with hint (when present) and exit. `--json` / `--ci` / a
12
12
  * non-TTY stdout always get one structured JSON line on stderr. */
13
13
  export declare function fatal(e: unknown, opts?: MachineOpts): never;
14
- /** Admit expected invalid input and exit through {@link fatal}. */
15
- export declare function failClosed(error: unknown, opts?: MachineOpts): never;
14
+ /** Project only a caller-authored admission failure into the CLI input family. */
15
+ export declare function failInput(error: unknown, opts?: MachineOpts): never;
16
16
  /** Shortcut for stub commands that aren't wired in v1 (§15). */
17
17
  export declare function fatalNotImplemented(feature: string, hint?: string): never;
18
18
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/cli",
3
- "version": "1.0.0-beta.5",
3
+ "version": "1.0.0-beta.6",
4
4
  "description": "Astrale CLI — connect to existing Astrale kernels",
5
5
  "keywords": [
6
6
  "astrale",
@@ -62,14 +62,9 @@
62
62
  "zod": "^3.25.0"
63
63
  },
64
64
  "devDependencies": {
65
- "@astrale-os/kernel-client": ">=0.6.0-beta.8 <1.0.0",
66
- "@astrale-os/kernel-core": ">=0.9.0-beta.7 <1.0.0",
67
- "@astrale-os/kernel-dsl": ">=0.2.0-beta.5 <1.0.0",
68
- "@astrale-os/kernel-protocol": ">=0.5.0-beta.7 <1.0.0",
69
- "@astrale-os/kernel-server": ">=0.5.0-beta.8 <1.0.0",
70
65
  "@astrale-os/ox": ">=0.1.3 <1.0.0",
71
- "@astrale-os/sdk": ">=0.5.0-beta.14 <1.0.0",
72
- "@astrale-os/shell": ">=0.3.8-beta.3 <1.0.0",
66
+ "@astrale-os/sdk": ">=0.5.0-beta.17 <1.0.0",
67
+ "@astrale-os/shell": ">=0.4.0 <1.0.0",
73
68
  "@astrale/commitlint-config": "npm:@jsr/astrale__commitlint-config@~2.0.1",
74
69
  "@commitlint/cli": "~20.3.1",
75
70
  "@commitlint/config-conventional": "~20.3.1",
@@ -111,7 +106,7 @@
111
106
  "lint:fix": "oxlint --fix .",
112
107
  "format": "pnpm exec oxfmt --write .",
113
108
  "format:check": "pnpm exec oxfmt --check .",
114
- "test": "bun test src studio/client/src studio/server studio/shared && node --test scripts/*.test.mjs",
109
+ "test": "node scripts/qualification/source-boundary.mjs --target && bun test src studio/client/src studio/server studio/shared && node --test scripts/*.test.mjs",
115
110
  "test:studio": "pnpm --dir studio test",
116
111
  "test:watch": "bun test --watch src"
117
112
  }
@@ -0,0 +1,20 @@
1
+ import type { schema } from '@astrale-os/sdk/schema'
2
+
3
+ import { defineApplication } from '@astrale-os/sdk/application'
4
+ import { compile } from '@astrale-os/sdk/deployment/build'
5
+ import { addressing, assemble } from '@astrale-os/sdk/deployment/release'
6
+ import { defineRuntime } from '@astrale-os/sdk/runtime'
7
+
8
+ /** Build a canonical empty-runtime Release for cross-boundary CLI tests. */
9
+ export function releaseFor<const Schema extends schema.DomainSchema>(
10
+ source: Schema,
11
+ issuer: string,
12
+ ) {
13
+ const runtime = defineRuntime<Schema>()({
14
+ integrations: {},
15
+ initialize: () => ({ providers: {} }),
16
+ actions: [],
17
+ workflows: [],
18
+ })
19
+ return assemble(compile(defineApplication({ schema: source, runtime })), addressing(issuer))
20
+ }
@@ -1,38 +1,31 @@
1
- import type { ClientSession } from '@astrale-os/kernel-client/session'
1
+ import type { ClientSession } from '@astrale-os/sdk/client/session'
2
2
 
3
- import { defineDomain } from '@astrale-os/sdk'
4
- import { issuer } from '@astrale-os/sdk/auth'
5
- import { createDeployment } from '@astrale-os/sdk/deployment'
6
- import { defineSchema } from '@astrale-os/sdk/schema/v1'
3
+ import { bundle, defineSchema, schema as schemaApi } from '@astrale-os/sdk/schema'
7
4
  import { expect, mock, test } from 'bun:test'
8
5
 
6
+ import { releaseFor } from '../../__tests__/fixtures/publication'
9
7
  import { bindAdmin } from '../binding'
10
8
 
11
9
  const schema = defineSchema('admin.astrale.ai', {})
12
- const deployment = createDeployment({
13
- definition: defineDomain({
14
- schema,
15
- handlers: { functions: {}, classes: {}, interfaces: {} },
16
- }),
17
- issuer: issuer.accept('https://admin.beta.astrale.ai'),
18
- bundleHref: 'https://admin.beta.astrale.ai/domain.bundle.json',
19
- bindings: { callables: [] },
20
- })
10
+ const release = releaseFor(schema, 'https://admin.beta.astrale.ai')
21
11
 
22
12
  test('binds the installed Admin Domain instead of the source Kernel publication', async () => {
23
13
  const installation = mock(async () => ({
24
14
  state: 'ready' as const,
25
15
  target: 'sha256:admin-target' as const,
26
- source: { kind: 'remote' as const, publication: deployment.publication },
27
- bundle: deployment.bundle,
16
+ source: { kind: 'remote' as const, publication: release.publication },
17
+ bundle: bundle.create(schema),
28
18
  readiness: 'sha256:admin-readiness' as const,
29
19
  capabilities: { requested: [], materialized: [] },
30
20
  }))
31
- const session = { installation } as unknown as ClientSession
21
+ const session = {
22
+ installation,
23
+ bind: (domain: unknown) => ({ domain, graph: {} }),
24
+ } as unknown as ClientSession
32
25
 
33
26
  const binding = await bindAdmin(session)
34
27
 
35
28
  expect(installation).toHaveBeenCalledWith('admin.astrale.ai')
36
- expect(binding.$.origin).toBe('admin.astrale.ai')
37
- expect(String(binding.$.publication?.identity.issuer)).toBe('https://admin.beta.astrale.ai')
29
+ expect(binding.domain.origin).toBe('admin.astrale.ai')
30
+ expect(binding.domain.revision).toBe(schemaApi.revision(schema))
38
31
  })
@@ -0,0 +1,101 @@
1
+ import type { ClientSession } from '@astrale-os/sdk/client/session'
2
+ import type { CallableReference } from '@astrale-os/sdk/client/session'
3
+
4
+ import {
5
+ callableAddress,
6
+ core,
7
+ defineSchema,
8
+ edgeClass,
9
+ method,
10
+ nodeClass,
11
+ property,
12
+ schema,
13
+ } from '@astrale-os/sdk/schema'
14
+ import { mock } from 'bun:test'
15
+
16
+ import type { AdminBinding } from '../binding'
17
+
18
+ const operation = () =>
19
+ method({
20
+ auth: 'authenticated',
21
+ input: { type: 'object', additionalProperties: true },
22
+ output: { type: 'object', additionalProperties: true },
23
+ })
24
+
25
+ const Domain = nodeClass({
26
+ properties: {
27
+ origin: property({ type: 'string' }, { required: true }),
28
+ name: property({ type: 'string' }, { required: true }),
29
+ discoveryUrl: property({ type: 'string' }, { required: true }),
30
+ description: property({ type: 'string' }),
31
+ createdAt: property({ type: 'string' }, { required: true }),
32
+ updatedAt: property({ type: 'string' }, { required: true }),
33
+ },
34
+ methods: { configureDefault: operation() },
35
+ })
36
+ const Fleet = nodeClass({
37
+ methods: {
38
+ publishDomain: operation(),
39
+ listInstances: operation(),
40
+ createInstance: operation(),
41
+ },
42
+ })
43
+ const Host = nodeClass({
44
+ properties: {
45
+ id: property({ type: 'string' }, { required: true }),
46
+ state: property({ type: 'string' }, { required: true }),
47
+ },
48
+ methods: { createInstance: operation() },
49
+ })
50
+ const Instance = nodeClass({
51
+ methods: {
52
+ status: operation(),
53
+ delete: operation(),
54
+ installDomain: operation(),
55
+ },
56
+ })
57
+ const fleet_installs_domain_by_default = edgeClass.directed({
58
+ source: { as: 'fleet', accepts: [Fleet], outgoing: '0..*' },
59
+ target: { as: 'domain', accepts: [Domain], incoming: '0..*' },
60
+ })
61
+ const fleet_reserves_admin_host = edgeClass.directed({
62
+ source: { as: 'fleet', accepts: [Fleet], outgoing: '0..1' },
63
+ target: { as: 'host', accepts: [Host], incoming: '0..1' },
64
+ })
65
+
66
+ export const AdminTestSchema = defineSchema('admin.astrale.ai', {
67
+ classes: {
68
+ Domain,
69
+ Fleet,
70
+ Host,
71
+ Instance,
72
+ fleet_installs_domain_by_default,
73
+ fleet_reserves_admin_host,
74
+ },
75
+ core: { nodes: { fleet: core.node(() => Fleet, {}) }, edges: [] },
76
+ })
77
+
78
+ export const AdminTestDomain = schema.resolve(AdminTestSchema)
79
+
80
+ export function adminBinding(): AdminBinding {
81
+ return { domain: AdminTestDomain, graph: {} } as unknown as AdminBinding
82
+ }
83
+
84
+ export function adminSession(
85
+ implementation?: (
86
+ method: { readonly owner: string; readonly name: string },
87
+ receiver: unknown,
88
+ input: unknown,
89
+ ) => unknown,
90
+ ) {
91
+ const invoke = mock(async (reference: CallableReference, input: unknown) => {
92
+ const address = callableAddress(reference.callable)
93
+ if (address.kind !== 'method') throw new Error('Expected an Admin Method.')
94
+ return implementation?.(
95
+ { owner: address.owner.name, name: address.name },
96
+ reference.target,
97
+ input,
98
+ )
99
+ })
100
+ return { invoke, session: { invoke } as unknown as ClientSession }
101
+ }
@@ -1,6 +1,15 @@
1
- import type { ClientSession } from '@astrale-os/kernel-client/session'
1
+ import type { Input } from '@astrale-os/sdk/client'
2
+ import type { ClientSession } from '@astrale-os/sdk/client/session'
3
+ import type {
4
+ ResolvedClass,
5
+ ResolvedCoreDefinition,
6
+ ResolvedMethod,
7
+ ResolvedProperty,
8
+ } from '@astrale-os/sdk/schema'
2
9
  import type { DomainBinding } from '@astrale-os/shell'
3
10
 
11
+ import { reference } from '@astrale-os/sdk/client/session'
12
+ import { Path } from '@astrale-os/sdk/graph/path'
4
13
  import { bindDomain } from '@astrale-os/shell'
5
14
 
6
15
  const ADMIN_ORIGIN = 'admin.astrale.ai'
@@ -10,9 +19,80 @@ export type AdminBinding = DomainBinding
10
19
  /** Bind the exact Admin revision installed on this source Kernel. */
11
20
  export async function bindAdmin(session: ClientSession): Promise<AdminBinding> {
12
21
  const installed = await session.installation(ADMIN_ORIGIN)
13
- const binding = await bindDomain(session, installed.bundle.root)
14
- if (binding.$.publication?.origin !== ADMIN_ORIGIN || binding.$.origin !== ADMIN_ORIGIN) {
22
+ return requireAdminBinding(await bindDomain(session, installed.bundle.root))
23
+ }
24
+
25
+ /** Admit an injected dynamic binding as the Admin Domain. */
26
+ export function requireAdminBinding(binding: AdminBinding): AdminBinding {
27
+ if (binding.domain.origin !== ADMIN_ORIGIN) {
15
28
  throw new TypeError('Configured Admin target does not serve the Admin Domain.')
16
29
  }
17
30
  return binding
18
31
  }
32
+
33
+ /** Resolve one required Admin Class without rebuilding its schema contract. */
34
+ export function requireAdminClass(
35
+ binding: AdminBinding,
36
+ name: string,
37
+ kind: 'node',
38
+ ): ResolvedClass<'node'>
39
+ export function requireAdminClass(
40
+ binding: AdminBinding,
41
+ name: string,
42
+ kind: 'edge',
43
+ ): ResolvedClass<'edge'>
44
+ export function requireAdminClass(
45
+ binding: AdminBinding,
46
+ name: string,
47
+ kind: 'node' | 'edge',
48
+ ): ResolvedClass<'node'> | ResolvedClass<'edge'> {
49
+ const selected = binding.domain.classes[name]
50
+ if (selected?.kind !== kind) throw new TypeError(`Admin ${name} is not a ${kind} Class.`)
51
+ return selected
52
+ }
53
+
54
+ /** Resolve one required Admin Core Node as its canonical projection Path. */
55
+ export function requireAdminCore(binding: AdminBinding, name: string): Path {
56
+ const selected = binding.domain.core.nodes[name] as ResolvedCoreDefinition | undefined
57
+ if (selected === undefined) throw new TypeError(`Admin has no Core Node ${name}.`)
58
+ return Path.project(selected.ref)
59
+ }
60
+
61
+ /** Resolve one effective Property by unambiguous member name. */
62
+ export function requireAdminProperty(
63
+ owner: ResolvedClass,
64
+ name: string,
65
+ ): ResolvedProperty<unknown> {
66
+ const selected = [...owner.properties].find((property) => property.name === name)
67
+ if (selected === undefined) {
68
+ throw new TypeError(`Admin ${owner.ref.name}.${name} Property is absent.`)
69
+ }
70
+ return selected
71
+ }
72
+
73
+ /** Invoke one resolved executable instance Method through Kernel Client. */
74
+ export async function invokeAdminMethod(
75
+ session: ClientSession,
76
+ binding: AdminBinding,
77
+ owner: ResolvedClass<'node'>,
78
+ name: string,
79
+ receiver: Path,
80
+ input: Input,
81
+ ): Promise<unknown> {
82
+ const method = [...owner.methods].find((candidate) => candidate.name === name)
83
+ if (method === undefined || !isExecutableInstanceMethod(method)) {
84
+ throw new TypeError(`Admin ${owner.ref.name}.${name} is not an executable instance Method.`)
85
+ }
86
+ return session.invoke(reference(binding.domain, method)(receiver), input)
87
+ }
88
+
89
+ type ExecutableInstanceMethod = ResolvedMethod<
90
+ unknown,
91
+ unknown,
92
+ false,
93
+ Exclude<ResolvedMethod['inheritance'], 'abstract'>
94
+ >
95
+
96
+ function isExecutableInstanceMethod(method: ResolvedMethod): method is ExecutableInstanceMethod {
97
+ return method.static === false && method.inheritance !== 'abstract'
98
+ }
@@ -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,41 +1,25 @@
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
- import { Path } from '@astrale-os/sdk/graph/path'
7
5
  import { describe, expect, mock, test } from 'bun:test'
8
6
 
9
- import type { AdminBinding } from '../../binding'
10
7
  import type { AdminGraphApi } from '../../graph'
11
8
 
9
+ import { AdminTestDomain, adminBinding, adminSession } from '../../__tests__/fixture'
12
10
  import { connectAdminCatalog } from '../client'
13
11
 
14
- function definition(name: string) {
15
- return {
16
- name,
17
- $: {
18
- method(method: string) {
19
- return { owner: name, name: method }
20
- },
21
- property(property: string) {
22
- return { key: property }
23
- },
24
- },
25
- }
26
- }
27
-
28
12
  function domainNode(id: string, origin: string): Node {
29
13
  return {
30
14
  id: NodeId(id),
31
- class: ClassPath.from('admin.astrale.ai', 'Domain'),
32
- props: {
15
+ class: AdminTestDomain.classes.Domain.key,
16
+ props: AdminTestDomain.classes.Domain.properties.from({
33
17
  origin,
34
18
  name: origin.split('.')[0]!,
35
19
  discoveryUrl: `https://${origin}`,
36
20
  createdAt: '2026-08-12T00:00:00.000Z',
37
21
  updatedAt: '2026-08-12T00:00:00.000Z',
38
- } as never,
22
+ }),
39
23
  }
40
24
  }
41
25
 
@@ -44,24 +28,16 @@ function fixture(input: {
44
28
  defaults?: readonly Node[]
45
29
  invoke?: (method: { owner: string; name: string }, receiver: unknown, value: unknown) => unknown
46
30
  }) {
47
- const Domain = definition('Domain')
48
- const Fleet = definition('Fleet')
49
- const invoke = mock(async (method: unknown, receiver: unknown, value: unknown) =>
50
- input.invoke?.(method as { owner: string; name: string }, receiver, value),
51
- )
52
- const binding = {
53
- $: {
54
- publication: { origin: 'admin.astrale.ai' },
55
- origin: 'admin.astrale.ai',
56
- class(name: string) {
57
- if (name === 'Domain') return Domain
58
- if (name === 'Fleet') return Fleet
59
- throw new Error(`Unexpected class ${name}`)
60
- },
61
- core: { nodes: { fleet: { path: Path.id(NodeId('fleet')) } } },
62
- invoke,
63
- },
64
- } as unknown as AdminBinding
31
+ const calls: Array<{
32
+ method: { owner: string; name: string }
33
+ receiver: string
34
+ value: unknown
35
+ }> = []
36
+ const remote = adminSession((method, receiver, value) => {
37
+ calls.push({ method, receiver: String(receiver), value })
38
+ return input.invoke?.(method, receiver, value)
39
+ })
40
+ const binding = adminBinding()
65
41
  const query = mock(async (_ast: QueryAST) => ({
66
42
  result: {
67
43
  kind: 'nodes' as const,
@@ -79,10 +55,11 @@ function fixture(input: {
79
55
  }))
80
56
  const graph = { query, neighbors } as unknown as AdminGraphApi
81
57
  return {
82
- invoke,
58
+ invoke: remote.invoke,
59
+ calls,
83
60
  connect: () =>
84
61
  connectAdminCatalog(
85
- { session: {} as never, graph },
62
+ { session: remote.session, graph },
86
63
  {
87
64
  bind: async () => binding,
88
65
  operationId: (kind) => `cli.domain.${kind}:test`,
@@ -130,16 +107,10 @@ describe('V2 Admin Domain catalog adapter', () => {
130
107
  installByDefault: true,
131
108
  }),
132
109
  ).resolves.toMatchObject({ changed: true, isNew: true, entry: { installByDefault: true } })
133
- expect(
134
- contract.invoke.mock.calls.map(([method, receiver, value]) => ({
135
- method,
136
- receiver: String(receiver),
137
- value,
138
- })),
139
- ).toEqual([
110
+ expect(contract.calls).toEqual([
140
111
  {
141
112
  method: { owner: 'Fleet', name: 'publishDomain' },
142
- receiver: '@fleet',
113
+ receiver: '/:admin.astrale.ai:core.fleet::publishDomain',
143
114
  value: {
144
115
  operationId: 'cli.domain.publish:test',
145
116
  origin: 'crm.acme.dev',
@@ -149,7 +120,7 @@ describe('V2 Admin Domain catalog adapter', () => {
149
120
  },
150
121
  {
151
122
  method: { owner: 'Domain', name: 'configureDefault' },
152
- receiver: '@crm-domain',
123
+ receiver: '@crm-domain::configureDefault',
153
124
  value: {
154
125
  operationId: 'cli.domain.configure-default:test',
155
126
  enabled: true,
@@ -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