@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.
- package/dist/astrale.js +55649 -59388
- package/dist/public/connect-core.js +1972 -26831
- package/dist/public/keys/index.js +1754 -40686
- package/dist/public/paths/index.js +859 -39511
- package/dist/types/admin/instance/model.d.ts +3 -4
- package/dist/types/errors.d.ts +1 -1
- package/dist/types/lib/idp.d.ts +1 -0
- package/dist/types/lib/instance-target.d.ts +1 -1
- package/dist/types/lib/log.d.ts +2 -2
- package/dist/types/lib/update.d.ts +30 -0
- package/package.json +4 -6
- package/src/__tests__/fixtures/publication.ts +20 -0
- package/src/admin/__tests__/binding.test.ts +14 -21
- package/src/admin/__tests__/fixture.ts +101 -0
- package/src/admin/binding.ts +86 -6
- package/src/admin/catalog/.spec/api.d.ts +2 -2
- package/src/admin/catalog/__tests__/client.test.ts +20 -49
- package/src/admin/catalog/client.ts +35 -26
- package/src/admin/catalog/model.ts +3 -4
- package/src/admin/graph/.spec/api.d.ts +4 -10
- package/src/admin/graph/nodes.ts +1 -1
- package/src/admin/instance/.spec/api.d.ts +2 -2
- package/src/admin/instance/__tests__/client.test.ts +58 -83
- package/src/admin/instance/client.ts +49 -39
- package/src/admin/instance/model.ts +9 -8
- package/src/commands/__tests__/call-describe.test.ts +46 -33
- package/src/commands/__tests__/domain-install-operation.test.ts +3 -14
- package/src/commands/__tests__/domain-install-owned.test.ts +1 -1
- package/src/commands/__tests__/domain-list.test.ts +5 -25
- package/src/commands/__tests__/install-identity-override.test.ts +3 -14
- package/src/commands/__tests__/read-commands.test.ts +35 -10
- package/src/commands/__tests__/update.test.ts +25 -3
- package/src/commands/call-describe.ts +59 -67
- package/src/commands/call.ts +41 -54
- package/src/commands/domain/install.ts +2 -2
- package/src/commands/domain/uninstall.ts +2 -2
- package/src/commands/get.ts +13 -14
- package/src/commands/identity/__tests__/register.test.ts +3 -3
- package/src/commands/identity/register.ts +5 -4
- package/src/commands/instance/delete.ts +5 -3
- package/src/commands/instance/status.ts +1 -9
- package/src/commands/introspect.ts +4 -11
- package/src/commands/logs.ts +24 -25
- package/src/commands/mutate.ts +6 -5
- package/src/commands/query.ts +27 -30
- package/src/commands/token.ts +8 -7
- package/src/commands/update.ts +31 -11
- package/src/commands/view.ts +4 -3
- package/src/connection/.spec/api.d.ts +7 -8
- package/src/connection/.spec/architecture.md +5 -4
- package/src/connection/.spec/flows/session.ts +2 -2
- package/src/connection/.spec/laws/connection.ts +6 -2
- package/src/connection/.spec/layout.ts +1 -0
- package/src/connection/__tests__/credential.test.ts +1 -1
- package/src/connection/__tests__/errors.test.ts +27 -20
- package/src/connection/__tests__/exchange.test.ts +30 -1
- package/src/connection/__tests__/failure-fixtures.ts +39 -0
- package/src/connection/__tests__/failure-safety.test.ts +75 -0
- package/src/connection/__tests__/self.test.ts +31 -11
- package/src/connection/__tests__/session.test.ts +3 -3
- package/src/connection/call.ts +1 -1
- package/src/connection/credential.ts +2 -2
- package/src/connection/errors.ts +1 -355
- package/src/connection/exchange.ts +47 -23
- package/src/connection/failure/classify.ts +110 -0
- package/src/connection/failure/debug.ts +26 -0
- package/src/connection/failure/index.ts +18 -0
- package/src/connection/failure/model.ts +28 -0
- package/src/connection/failure/render.ts +116 -0
- package/src/connection/reasons.ts +16 -0
- package/src/connection/self.ts +39 -21
- package/src/connection/session.ts +8 -8
- package/src/errors.ts +3 -2
- package/src/graph/.spec/api.d.ts +12 -5
- package/src/graph/.spec/layout.ts +9 -1
- package/src/graph/__tests__/mutation.test.ts +2 -2
- package/src/graph/__tests__/query.test.ts +25 -6
- package/src/graph/class.ts +30 -0
- package/src/graph/index.ts +1 -0
- package/src/graph/query.ts +21 -32
- package/src/identity/.spec/api.d.ts +2 -2
- package/src/identity/__tests__/registration.test.ts +1 -1
- package/src/identity/registration.ts +3 -3
- package/src/lib/__tests__/domain-publication.test.ts +4 -15
- package/src/lib/__tests__/idp.test.ts +25 -0
- package/src/lib/__tests__/instance-target.test.ts +6 -39
- package/src/lib/__tests__/log-errors.test.ts +53 -0
- package/src/lib/__tests__/studio-server-deps.test.ts +1 -1
- package/src/lib/__tests__/update.test.ts +101 -8
- package/src/lib/__tests__/view-open-intent.test.ts +0 -2
- package/src/lib/__tests__/view-server.test.ts +1 -1
- package/src/lib/__tests__/view-session.test.ts +1 -1
- package/src/lib/command-dx.ts +1 -4
- package/src/lib/domain-publication.ts +1 -1
- package/src/lib/idp.ts +29 -1
- package/src/lib/instance-target.ts +3 -14
- package/src/lib/log.ts +11 -6
- package/src/lib/update.ts +120 -26
- package/src/lib/validation.ts +2 -2
- package/src/lib/view/resolve.ts +1 -1
- package/src/program/__tests__/program.test.ts +1 -1
- package/studio/client/dist/assets/{elk-api-lwhFo7vB.js → elk-api-Di4OXGNH.js} +1 -1
- package/studio/client/dist/assets/index-BLm6J11H.js +81 -0
- package/studio/client/dist/assets/index-BiGR8Oo9.css +1 -0
- package/studio/client/dist/assets/index-DTjvkOUX.js +8 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/package.json +2 -9
- package/studio/server/agent/ask.test.ts +1 -1
- package/studio/server/agent/bridge/grant.test.ts +1 -1
- package/studio/server/agent/bridge/routes.test.ts +1 -1
- package/studio/server/agent/harness/claude/adapter.test.ts +15 -5
- package/studio/server/agent/prompts/anchors.test.ts +32 -36
- package/studio/server/agent/prompts/anchors.ts +20 -49
- package/studio/server/agent/prompts/system.test.ts +3 -2
- package/studio/server/agent/prompts/system.ts +3 -3
- package/studio/server/agent/routes.test.ts +1 -1
- package/studio/server/agent/run/coordinator.test.ts +1 -1
- package/studio/server/api/canvas.ts +0 -1
- package/studio/server/api-agent-loadout.test.ts +1 -1
- package/studio/server/api.test.ts +1 -1
- package/studio/server/cache.test.ts +23 -12
- package/studio/server/cache.ts +12 -15
- package/studio/server/domain.test.ts +50 -68
- package/studio/server/domain.ts +82 -63
- package/studio/server/handoff/copy.ts +1 -1
- package/studio/server/index.ts +1 -1
- package/studio/server/instances/probe.test.ts +1 -1
- package/studio/server/introspect/anatomy/views/routes.ts +46 -37
- package/studio/server/introspect/anatomy/views.ts +9 -14
- package/studio/server/introspect/anatomy-extras.test.ts +26 -42
- package/studio/server/introspect/anatomy.test.ts +22 -14
- package/studio/server/introspect/anatomy.ts +6 -9
- package/studio/server/introspect/bundle.ts +0 -3
- package/studio/server/introspect/canonical-schema.test.ts +94 -389
- package/studio/server/introspect/canonical-schema.ts +227 -457
- package/studio/server/introspect/core-extractor.ts +18 -127
- package/studio/server/introspect/core.ts +1 -2
- package/studio/server/introspect/diff.test.ts +11 -7
- package/studio/server/introspect/diff.ts +18 -55
- package/studio/server/introspect/extractor.ts +22 -117
- package/studio/server/introspect/overlay-tsmorph.test.ts +119 -240
- package/studio/server/introspect/overlay.test.ts +26 -54
- package/studio/server/introspect/overlay.ts +10 -37
- package/studio/server/introspect/revision.test.ts +1 -1
- package/studio/server/introspect/revision.ts +1 -1
- package/studio/server/introspect/runtime.test.ts +65 -183
- package/studio/server/introspect/runtime.ts +8 -25
- package/studio/server/introspect/schema-ir-json.test.ts +70 -0
- package/studio/server/introspect/schema-ir-json.ts +38 -68
- package/studio/server/introspect/schema-refs.test.ts +43 -88
- package/studio/server/introspect/schema-refs.ts +14 -49
- package/studio/server/introspect/source-overlay/handlers.ts +116 -636
- package/studio/server/introspect/source-overlay/kernel-calls.ts +0 -3
- package/studio/server/introspect/source-overlay/spans.ts +15 -45
- package/studio/server/state/baseline.test.ts +14 -9
- package/studio/server/state/baseline.ts +3 -5
- package/studio/server/state/visibility.ts +1 -5
- package/studio/server/views/model.ts +3 -3
- package/studio/server/views/target.test.ts +17 -74
- package/studio/server/views/target.ts +24 -50
- package/studio/server/watch.test.ts +11 -11
- package/studio/server/watch.ts +8 -4
- package/studio/server/workspace-watch.ts +2 -2
- package/studio/shared/contracts/schema.ts +38 -107
- package/studio/shared/contracts/surface.test.ts +13 -15
- package/studio/shared/contracts/workspace.ts +2 -11
- package/studio/shared/schema/identity.test.ts +20 -44
- package/studio/shared/schema/identity.ts +9 -23
- package/viewer/dist/main.js +40 -61
- package/studio/client/dist/assets/index-B-c-smo9.js +0 -8
- package/studio/client/dist/assets/index-BckHuAWk.js +0 -81
- package/studio/client/dist/assets/index-C4aNQ6dz.css +0 -1
- package/studio/server/introspect/anatomy/views/legacy.ts +0 -232
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import type { ClientSession } from '@astrale-os/
|
|
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 {
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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({
|
|
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
|
|
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
|
-
}
|
|
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
|
|
128
|
-
|
|
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
|
-
}
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
|
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
|
package/src/admin/graph/nodes.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
function definition(name: string) {
|
|
13
|
+
function hostNode(id: string, props: { readonly id: string; readonly state: string }): Node {
|
|
17
14
|
return {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
21
|
+
function instanceNode(id: string): Node {
|
|
33
22
|
return {
|
|
34
23
|
id: NodeId(id),
|
|
35
|
-
class:
|
|
36
|
-
props:
|
|
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 === '
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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) === '
|
|
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:
|
|
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.
|
|
150
|
-
|
|
151
|
-
|
|
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: [
|
|
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.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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 =
|
|
188
|
-
const consumer =
|
|
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({
|
|
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.
|
|
204
|
-
|
|
205
|
-
|
|
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 =
|
|
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.
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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 =
|
|
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.
|
|
286
|
-
|
|
287
|
-
|
|
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/
|
|
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 {
|
|
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
|
|
63
|
-
const Host = binding
|
|
64
|
-
const Fleet = binding
|
|
65
|
-
const
|
|
66
|
-
|
|
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
|
|
72
|
-
|
|
69
|
+
const result: unknown = await invokeAdminMethod(
|
|
70
|
+
context.session,
|
|
71
|
+
binding,
|
|
72
|
+
Fleet,
|
|
73
|
+
'listInstances',
|
|
73
74
|
fleet,
|
|
74
|
-
{}
|
|
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
|
|
92
|
-
|
|
92
|
+
const output = await invokeAdminMethod(
|
|
93
|
+
context.session,
|
|
94
|
+
binding,
|
|
95
|
+
Instance,
|
|
96
|
+
method,
|
|
93
97
|
Path.parse(instance.id),
|
|
94
|
-
{ operationId: operationId(method) }
|
|
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,
|
|
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
|
|
114
|
-
|
|
117
|
+
await invokeAdminMethod(
|
|
118
|
+
context.session,
|
|
119
|
+
binding,
|
|
120
|
+
Host,
|
|
121
|
+
'createInstance',
|
|
115
122
|
Path.parse(host.path),
|
|
116
|
-
input
|
|
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
|
|
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
|
|
133
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
155
|
-
|
|
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({
|
|
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 =
|
|
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
|
|
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
|
|
32
|
-
readonly name = 'NotFoundError'
|
|
33
|
-
|
|
31
|
+
export class AdminInstanceNotFoundError extends AstraleError {
|
|
34
32
|
constructor(readonly identifier: string) {
|
|
35
|
-
super(
|
|
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
|
|
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'
|