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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/dist/astrale.js +66254 -64461
  2. package/dist/public/connect-core.js +14895 -33867
  3. package/dist/public/keys/index.js +1246 -34250
  4. package/dist/public/paths/index.js +866 -33833
  5. package/dist/types/admin/instance/model.d.ts +3 -4
  6. package/dist/types/errors.d.ts +1 -1
  7. package/dist/types/lib/instance-target.d.ts +1 -1
  8. package/dist/types/lib/log.d.ts +2 -2
  9. package/package.json +4 -9
  10. package/src/__tests__/fixtures/publication.ts +20 -0
  11. package/src/admin/__tests__/binding.test.ts +12 -19
  12. package/src/admin/__tests__/fixture.ts +101 -0
  13. package/src/admin/binding.ts +83 -3
  14. package/src/admin/catalog/.spec/api.d.ts +1 -1
  15. package/src/admin/catalog/__tests__/client.test.ts +20 -49
  16. package/src/admin/catalog/client.ts +35 -26
  17. package/src/admin/catalog/model.ts +3 -4
  18. package/src/admin/graph/.spec/api.d.ts +4 -10
  19. package/src/admin/graph/nodes.ts +1 -1
  20. package/src/admin/instance/.spec/api.d.ts +1 -1
  21. package/src/admin/instance/__tests__/client.test.ts +58 -83
  22. package/src/admin/instance/client.ts +49 -39
  23. package/src/admin/instance/model.ts +9 -8
  24. package/src/commands/__tests__/call-describe.test.ts +46 -33
  25. package/src/commands/__tests__/domain-install-operation.test.ts +3 -14
  26. package/src/commands/__tests__/domain-install-owned.test.ts +1 -1
  27. package/src/commands/__tests__/domain-list.test.ts +5 -25
  28. package/src/commands/__tests__/install-identity-override.test.ts +3 -14
  29. package/src/commands/__tests__/read-commands.test.ts +35 -10
  30. package/src/commands/call-describe.ts +59 -67
  31. package/src/commands/call.ts +41 -54
  32. package/src/commands/domain/install.ts +2 -2
  33. package/src/commands/domain/uninstall.ts +2 -2
  34. package/src/commands/get.ts +13 -14
  35. package/src/commands/identity/__tests__/register.test.ts +3 -3
  36. package/src/commands/identity/register.ts +5 -4
  37. package/src/commands/instance/delete.ts +5 -3
  38. package/src/commands/instance/status.ts +1 -9
  39. package/src/commands/introspect.ts +4 -11
  40. package/src/commands/logs.ts +24 -25
  41. package/src/commands/mutate.ts +6 -5
  42. package/src/commands/query.ts +27 -30
  43. package/src/commands/token.ts +8 -7
  44. package/src/commands/view.ts +4 -3
  45. package/src/connection/.spec/api.d.ts +7 -8
  46. package/src/connection/.spec/architecture.md +5 -4
  47. package/src/connection/.spec/flows/session.ts +2 -2
  48. package/src/connection/.spec/laws/connection.ts +6 -2
  49. package/src/connection/.spec/layout.ts +1 -0
  50. package/src/connection/__tests__/credential.test.ts +1 -1
  51. package/src/connection/__tests__/errors.test.ts +27 -20
  52. package/src/connection/__tests__/exchange.test.ts +30 -1
  53. package/src/connection/__tests__/failure-fixtures.ts +39 -0
  54. package/src/connection/__tests__/failure-safety.test.ts +75 -0
  55. package/src/connection/__tests__/self.test.ts +31 -11
  56. package/src/connection/__tests__/session.test.ts +3 -3
  57. package/src/connection/call.ts +1 -1
  58. package/src/connection/credential.ts +2 -2
  59. package/src/connection/errors.ts +1 -355
  60. package/src/connection/exchange.ts +47 -23
  61. package/src/connection/failure/classify.ts +110 -0
  62. package/src/connection/failure/debug.ts +26 -0
  63. package/src/connection/failure/index.ts +18 -0
  64. package/src/connection/failure/model.ts +28 -0
  65. package/src/connection/failure/render.ts +116 -0
  66. package/src/connection/reasons.ts +16 -0
  67. package/src/connection/self.ts +39 -21
  68. package/src/connection/session.ts +8 -8
  69. package/src/errors.ts +3 -2
  70. package/src/graph/.spec/api.d.ts +12 -5
  71. package/src/graph/.spec/layout.ts +9 -1
  72. package/src/graph/__tests__/mutation.test.ts +2 -2
  73. package/src/graph/__tests__/query.test.ts +25 -6
  74. package/src/graph/class.ts +30 -0
  75. package/src/graph/index.ts +1 -0
  76. package/src/graph/query.ts +21 -32
  77. package/src/identity/.spec/api.d.ts +2 -2
  78. package/src/identity/__tests__/registration.test.ts +1 -1
  79. package/src/identity/registration.ts +3 -3
  80. package/src/lib/__tests__/domain-publication.test.ts +4 -15
  81. package/src/lib/__tests__/instance-target.test.ts +6 -39
  82. package/src/lib/__tests__/log-errors.test.ts +53 -0
  83. package/src/lib/__tests__/studio-server-deps.test.ts +1 -1
  84. package/src/lib/command-dx.ts +1 -4
  85. package/src/lib/domain-publication.ts +1 -1
  86. package/src/lib/instance-target.ts +3 -14
  87. package/src/lib/log.ts +11 -6
  88. package/src/lib/validation.ts +2 -2
  89. package/src/lib/view/resolve.ts +1 -1
  90. package/src/program/__tests__/program.test.ts +1 -1
  91. package/studio/client/dist/assets/{elk-api-YEDvaNU-.js → elk-api-Di4OXGNH.js} +1 -1
  92. package/studio/client/dist/assets/index-BLm6J11H.js +81 -0
  93. package/studio/client/dist/assets/index-BiGR8Oo9.css +1 -0
  94. package/studio/client/dist/assets/index-DTjvkOUX.js +8 -0
  95. package/studio/client/dist/index.html +2 -2
  96. package/studio/package.json +2 -7
  97. package/studio/server/agent/ask.test.ts +1 -1
  98. package/studio/server/agent/bridge/grant.test.ts +1 -1
  99. package/studio/server/agent/bridge/routes.test.ts +1 -1
  100. package/studio/server/agent/harness/claude/adapter.test.ts +15 -5
  101. package/studio/server/agent/prompts/anchors.test.ts +32 -36
  102. package/studio/server/agent/prompts/anchors.ts +20 -49
  103. package/studio/server/agent/prompts/system.test.ts +3 -2
  104. package/studio/server/agent/prompts/system.ts +3 -3
  105. package/studio/server/agent/routes.test.ts +1 -1
  106. package/studio/server/agent/run/coordinator.test.ts +1 -1
  107. package/studio/server/api/canvas.ts +0 -1
  108. package/studio/server/api-agent-loadout.test.ts +1 -1
  109. package/studio/server/api.test.ts +1 -1
  110. package/studio/server/cache.test.ts +23 -12
  111. package/studio/server/cache.ts +12 -15
  112. package/studio/server/domain.test.ts +50 -68
  113. package/studio/server/domain.ts +82 -63
  114. package/studio/server/handoff/copy.ts +1 -1
  115. package/studio/server/index.ts +1 -1
  116. package/studio/server/instances/probe.test.ts +1 -1
  117. package/studio/server/introspect/anatomy/views/routes.ts +46 -37
  118. package/studio/server/introspect/anatomy/views.ts +9 -14
  119. package/studio/server/introspect/anatomy-extras.test.ts +26 -42
  120. package/studio/server/introspect/anatomy.test.ts +22 -14
  121. package/studio/server/introspect/anatomy.ts +6 -9
  122. package/studio/server/introspect/bundle.ts +0 -3
  123. package/studio/server/introspect/canonical-schema.test.ts +94 -389
  124. package/studio/server/introspect/canonical-schema.ts +227 -457
  125. package/studio/server/introspect/core-extractor.ts +18 -127
  126. package/studio/server/introspect/core.ts +1 -2
  127. package/studio/server/introspect/diff.test.ts +11 -7
  128. package/studio/server/introspect/diff.ts +18 -55
  129. package/studio/server/introspect/extractor.ts +22 -117
  130. package/studio/server/introspect/overlay-tsmorph.test.ts +119 -240
  131. package/studio/server/introspect/overlay.test.ts +26 -54
  132. package/studio/server/introspect/overlay.ts +10 -37
  133. package/studio/server/introspect/revision.test.ts +1 -1
  134. package/studio/server/introspect/revision.ts +1 -1
  135. package/studio/server/introspect/runtime.test.ts +65 -183
  136. package/studio/server/introspect/runtime.ts +8 -25
  137. package/studio/server/introspect/schema-ir-json.test.ts +70 -0
  138. package/studio/server/introspect/schema-ir-json.ts +38 -68
  139. package/studio/server/introspect/schema-refs.test.ts +43 -88
  140. package/studio/server/introspect/schema-refs.ts +14 -49
  141. package/studio/server/introspect/source-overlay/handlers.ts +116 -636
  142. package/studio/server/introspect/source-overlay/kernel-calls.ts +0 -3
  143. package/studio/server/introspect/source-overlay/spans.ts +15 -45
  144. package/studio/server/state/baseline.test.ts +14 -9
  145. package/studio/server/state/baseline.ts +3 -5
  146. package/studio/server/state/visibility.ts +1 -5
  147. package/studio/server/views/model.ts +3 -3
  148. package/studio/server/views/target.test.ts +17 -74
  149. package/studio/server/views/target.ts +24 -50
  150. package/studio/server/watch.test.ts +11 -11
  151. package/studio/server/watch.ts +8 -4
  152. package/studio/server/workspace-watch.ts +2 -2
  153. package/studio/shared/contracts/schema.ts +38 -107
  154. package/studio/shared/contracts/surface.test.ts +12 -15
  155. package/studio/shared/contracts/workspace.ts +2 -11
  156. package/studio/shared/schema/identity.test.ts +20 -44
  157. package/studio/shared/schema/identity.ts +9 -23
  158. package/viewer/dist/main.js +41 -38
  159. package/studio/client/dist/assets/index-C-PvAXV4.js +0 -81
  160. package/studio/client/dist/assets/index-C4aNQ6dz.css +0 -1
  161. package/studio/client/dist/assets/index-CzpmJD0j.js +0 -8
  162. package/studio/server/introspect/anatomy/views/legacy.ts +0 -232
@@ -1,9 +1,9 @@
1
1
  import type { IssuerId, JsonWebKey, ProvisionRequest } from '@astrale-os/sdk/auth'
2
+ import type { ClassKey as ClassKeyValue } from '@astrale-os/sdk/graph/class'
2
3
  import type { JWK } from 'jose'
3
4
 
4
5
  import { provision, jwk } from '@astrale-os/sdk/auth'
5
- import { ClassPath } from '@astrale-os/sdk/graph/class'
6
- import { LocalBinding } from '@astrale-os/sdk/graph/model'
6
+ import { LocalBinding } from '@astrale-os/sdk/graph'
7
7
  import { normalizeProperties } from '@astrale-os/sdk/graph/properties'
8
8
  import { MutationAST } from '@astrale-os/sdk/mutation'
9
9
  import { importJWK, SignJWT } from 'jose'
@@ -14,6 +14,7 @@ import type { IdentityRegistrationResult } from '../../identity/index'
14
14
  import type { CommandDefinition } from '../../program/index'
15
15
 
16
16
  import { registrationKeyForTarget, runKernelCommand } from '../../connection'
17
+ import { classKey } from '../../graph'
17
18
  import { getIdentity, setRegistration, submitIdentityProvision } from '../../identity/index'
18
19
  import { fileExists, keypairPaths } from '../../keys/index'
19
20
  import { fatal, log } from '../../lib/log'
@@ -83,7 +84,7 @@ Example:
83
84
 
84
85
  const privateKey = await readJwk(privatePath)
85
86
  const publicKey = jwk.acceptPublic(await readJwk(publicPath))
86
- const classPath = ClassPath.parse(opts.class)
87
+ const classPath = classKey(opts.class, '--class')
87
88
  const properties = normalizeProperties(opts.props ? JSON.parse(opts.props) : {})
88
89
 
89
90
  await runKernelCommand<IdentityRegistrationResult>({
@@ -142,7 +143,7 @@ export function formatIdentityRegistration(
142
143
  /** Build and self-prove one exact canonical provision request for the target Kernel. */
143
144
  export async function prepareIdentityProvision(input: {
144
145
  readonly name: string
145
- readonly classPath: ReturnType<typeof ClassPath.parse>
146
+ readonly classPath: ClassKeyValue
146
147
  readonly properties: ReturnType<typeof normalizeProperties>
147
148
  readonly privateKey: JWK
148
149
  readonly publicKey: JsonWebKey
@@ -1,6 +1,7 @@
1
1
  import type { KernelCommandOpts } from '../../connection'
2
2
  import type { CommandDefinition } from '../../program/index'
3
3
 
4
+ import { AdminInstanceNotFoundError } from '../../admin/instance'
4
5
  import { deleteOwnedInstance } from '../../lib/admin-instance'
5
6
  import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
6
7
  import { clearActive, readInstances, removeInstance, resolveInstanceKey } from '../../lib/instance'
@@ -53,9 +54,10 @@ Behavior:
53
54
  } catch (e) {
54
55
  // Bookmark-only id → no admin Instance → kernel NOT_FOUND. Point to
55
56
  // `forget` (local drop) instead of surfacing the raw path error.
56
- const notManaged =
57
- e instanceof Error && (e.name === 'NotFoundError' || e.message.startsWith('Path not found'))
58
- if (notManaged && resolveInstanceKey(await readInstances(), id)) {
57
+ if (
58
+ e instanceof AdminInstanceNotFoundError &&
59
+ resolveInstanceKey(await readInstances(), id)
60
+ ) {
59
61
  log.error(`No admin-managed instance "${id}".`)
60
62
  log.dim(` hint: it's a local bookmark — drop it with: astrale instance forget ${id}`)
61
63
  process.exit(1)
@@ -3,7 +3,6 @@ import chalk from 'chalk'
3
3
  import type { KernelCommandOpts } from '../../connection'
4
4
  import type { CommandDefinition } from '../../program/index'
5
5
 
6
- import { AstraleError } from '../../errors'
7
6
  import { statusOwnedInstance } from '../../lib/admin-instance'
8
7
  import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
9
8
  import { fatal, log, withSpinner } from '../../lib/log'
@@ -19,14 +18,7 @@ export default {
19
18
  action: async (id: string, opts: StatusOpts) => {
20
19
  try {
21
20
  const result = await withSpinner(`Fetching instance ${id}`, !isMachine(opts), () =>
22
- statusOwnedInstance(opts, id).catch((error) => {
23
- if (!(error instanceof Error) || error.name !== 'NotFoundError') throw error
24
- throw new AstraleError(
25
- 'INSTANCE_NOT_FOUND',
26
- `No owned instance matches "${id}".`,
27
- 'Run `astrale instance list` to see your instances.',
28
- )
29
- }),
21
+ statusOwnedInstance(opts, id),
30
22
  )
31
23
  if (isMachine(opts)) {
32
24
  output(result, opts)
@@ -5,9 +5,9 @@ import type { CommandDefinition } from '../program/index'
5
5
 
6
6
  import { runKernelCommand } from '../connection'
7
7
  import { AstraleError } from '../errors'
8
- import { failClosed } from '../lib/log'
8
+ import { failInput } from '../lib/log'
9
9
  import { output } from '../lib/output'
10
- import { describeCallableFromSchema, missingCallableDescription } from './call-describe'
10
+ import { describeCallableFromBundle, missingCallableDescription } from './call-describe'
11
11
 
12
12
  type IntrospectOpts = KernelCommandOpts & { bundle?: boolean }
13
13
 
@@ -17,7 +17,7 @@ export async function introspectCommand(target: string, opts: IntrospectOpts): P
17
17
  try {
18
18
  ;({ origin, path } = parseIntrospectTarget(target))
19
19
  } catch (error) {
20
- failClosed(error, opts)
20
+ failInput(error, opts)
21
21
  }
22
22
  const wantsCallable = isCallablePath(path)
23
23
 
@@ -44,7 +44,7 @@ export async function introspectCommand(target: string, opts: IntrospectOpts): P
44
44
  )
45
45
  }
46
46
  if (wantsCallable) {
47
- const described = describeCallableFromSchema(path, bundleRoot(result.bundle))
47
+ const described = describeCallableFromBundle(path, result.bundle)
48
48
  if (described === undefined) throw missingCallableDescription(path.raw)
49
49
  return described
50
50
  }
@@ -85,13 +85,6 @@ function isCallablePath(path: Path): boolean {
85
85
  return last.kind === 'projection' && last.projection.kind === 'function'
86
86
  }
87
87
 
88
- function bundleRoot(bundle: unknown): unknown {
89
- if (bundle !== null && typeof bundle === 'object' && 'root' in bundle) {
90
- return (bundle as { root: unknown }).root
91
- }
92
- return bundle
93
- }
94
-
95
88
  export default {
96
89
  name: 'introspect',
97
90
  description: 'Read installed Domain schema from the Kernel Schema syscall',
@@ -1,21 +1,16 @@
1
1
  import { Path } from '@astrale-os/sdk/graph/path'
2
- import { syscalls } from '@astrale-os/sdk/schema/kernel'
2
+ import { K } from '@astrale-os/sdk/schema'
3
3
  import chalk from 'chalk'
4
4
 
5
5
  import type { ConnectionContext, KernelCommandOpts } from '../connection'
6
6
  import type { Column, ListProjection } from '../lib/output'
7
7
  import type { CommandDefinition } from '../program/index'
8
8
 
9
- import {
10
- createPathCall,
11
- expandSelfInPath,
12
- runKernelCommand,
13
- withClientSession,
14
- } from '../connection'
15
- import { failClosed } from '../lib/log'
9
+ import { createPathCall, expandSelfInPath, runKernelCommand } from '../connection'
10
+ import { failInput } from '../lib/log'
16
11
  import { isMachine, output, presentList } from '../lib/output'
17
12
 
18
- const JOURNAL_PATH = Path.project(syscalls.journal.ref).raw
13
+ const JOURNAL_PATH = Path.project(K.functions.journal.ref).raw
19
14
  const DEFAULT_LIMIT = 200
20
15
  const FOLLOW_INTERVAL_MS = 2_000
21
16
 
@@ -127,7 +122,7 @@ async function runOnce(opts: LogsOpts): Promise<void> {
127
122
  await runKernelCommand({
128
123
  opts,
129
124
  label: 'Kernel journal',
130
- fn: (context) => fetchPage(context, opts),
125
+ fn: async (context) => fetchPage(context, await resolveLogsOpts(opts, context)),
131
126
  format: (page, format) => {
132
127
  if (isMachine(format) || format.format !== undefined) output(page, format)
133
128
  else {
@@ -139,14 +134,19 @@ async function runOnce(opts: LogsOpts): Promise<void> {
139
134
  }
140
135
 
141
136
  async function follow(opts: LogsOpts): Promise<void> {
142
- await withClientSession(opts, async (context) => {
143
- let cursor = opts.cursor
144
- for (;;) {
145
- const page = await fetchPage(context, { ...opts, cursor })
146
- for (const record of page.records) printRecord(record)
147
- cursor = page.cursor ?? cursor
148
- await new Promise((resolve) => setTimeout(resolve, FOLLOW_INTERVAL_MS))
149
- }
137
+ await runKernelCommand({
138
+ opts,
139
+ label: 'Kernel journal',
140
+ fn: async (context): Promise<never> => {
141
+ const resolved = await resolveLogsOpts(opts, context)
142
+ let cursor = resolved.cursor
143
+ for (;;) {
144
+ const page = await fetchPage(context, { ...resolved, cursor })
145
+ for (const record of page.records) printRecord(record)
146
+ cursor = page.cursor ?? cursor
147
+ await new Promise((resolve) => setTimeout(resolve, FOLLOW_INTERVAL_MS))
148
+ }
149
+ },
150
150
  })
151
151
  }
152
152
 
@@ -227,11 +227,10 @@ function positiveInteger(flag: string, raw: string): number {
227
227
  return value
228
228
  }
229
229
 
230
- async function prepareLogsOpts(opts: LogsOpts): Promise<LogsOpts> {
231
- buildJournalInput(opts)
230
+ async function resolveLogsOpts(opts: LogsOpts, context: ConnectionContext): Promise<LogsOpts> {
232
231
  const principal = nonEmpty(opts.principal)
233
232
  if (principal !== '@self') return opts
234
- const { path } = await expandSelfInPath('@self', opts)
233
+ const { path } = await expandSelfInPath('@self', context)
235
234
  return { ...opts, principal: path.startsWith('@') ? path.slice(1) : path }
236
235
  }
237
236
 
@@ -274,11 +273,11 @@ Examples:
274
273
  ],
275
274
  action: async (opts: LogsOpts) => {
276
275
  try {
277
- const prepared = await prepareLogsOpts(opts)
278
- if (prepared.follow) await follow(prepared)
279
- else await runOnce(prepared)
276
+ buildJournalInput(opts)
280
277
  } catch (error) {
281
- failClosed(error, opts)
278
+ failInput(error, opts)
282
279
  }
280
+ if (opts.follow) await follow(opts)
281
+ else await runOnce(opts)
283
282
  },
284
283
  } satisfies CommandDefinition
@@ -7,8 +7,9 @@ import type { KernelCommandOpts } from '../connection'
7
7
  import type { CommandDefinition } from '../program/index'
8
8
 
9
9
  import { runKernelCommand } from '../connection'
10
+ import { AstraleError } from '../errors'
10
11
  import { prepareMutation } from '../graph/index'
11
- import { failClosed } from '../lib/log'
12
+ import { failInput } from '../lib/log'
12
13
  import { output } from '../lib/output'
13
14
  import { renderTable } from '../lib/table'
14
15
 
@@ -23,7 +24,7 @@ export async function mutateCommand(opts: MutateOpts): Promise<void> {
23
24
  try {
24
25
  mutation = prepareMutation(await readDocument(opts))
25
26
  } catch (error) {
26
- failClosed(error, opts)
27
+ failInput(error, opts)
27
28
  }
28
29
 
29
30
  if (opts.dry) {
@@ -51,9 +52,9 @@ async function readDocument(opts: MutateOpts): Promise<unknown> {
51
52
  try {
52
53
  raw = await readFile(opts.file, 'utf8')
53
54
  } catch (error) {
54
- throw new Error(
55
- `Cannot read --file ${opts.file}: ${error instanceof Error ? error.message : error}`,
56
- )
55
+ throw new AstraleError('FILE_READ_FAILED', `Cannot read --file ${opts.file}.`, undefined, {
56
+ cause: error,
57
+ })
57
58
  }
58
59
  return parseJson(raw, opts.file)
59
60
  }
@@ -6,8 +6,9 @@ import type { KernelCommandOpts, SelfExpansionMeta } from '../connection'
6
6
  import type { CommandDefinition } from '../program/index'
7
7
 
8
8
  import { expandSelfInPath, runKernelCommand, withSelfHint } from '../connection'
9
+ import { AstraleError } from '../errors'
9
10
  import { prepareQuery, type QueryCommandInput } from '../graph/index'
10
- import { failClosed } from '../lib/log'
11
+ import { failInput } from '../lib/log'
11
12
  import { isMachine, output } from '../lib/output'
12
13
 
13
14
  type QueryOpts = KernelCommandOpts & {
@@ -22,46 +23,42 @@ type QueryOpts = KernelCommandOpts & {
22
23
 
23
24
  export async function queryCommand(sources: string[], opts: QueryOpts): Promise<void> {
24
25
  let input: QueryCommandInput
25
- let meta: SelfExpansionMeta | undefined
26
26
  try {
27
27
  const ast = await readAst(opts)
28
- if (ast === undefined) {
29
- const expanded = await Promise.all(sources.map((source) => expandSelfInPath(source, opts)))
30
- input = {
31
- sources: expanded.map(({ path }) => path),
32
- definition: opts.definition,
33
- edge: opts.edge,
34
- direction: opts.direction,
35
- limit: opts.limit,
36
- cursor: opts.cursor,
37
- }
38
- meta = expanded.find((entry) => entry.meta !== undefined)?.meta
39
- } else {
40
- input = {
41
- sources,
42
- ast,
43
- definition: opts.definition,
44
- edge: opts.edge,
45
- direction: opts.direction,
46
- limit: opts.limit,
47
- cursor: opts.cursor,
48
- }
28
+ input = {
29
+ sources,
30
+ ...(ast === undefined ? {} : { ast }),
31
+ definition: opts.definition,
32
+ edge: opts.edge,
33
+ direction: opts.direction,
34
+ limit: opts.limit,
35
+ cursor: opts.cursor,
49
36
  }
50
37
  } catch (error) {
51
- failClosed(error, opts)
38
+ failInput(error, opts)
52
39
  }
53
40
 
54
41
  let prepared
55
42
  try {
56
43
  prepared = prepareQuery(input)
57
44
  } catch (error) {
58
- failClosed(error, opts)
45
+ failInput(error, opts)
59
46
  }
60
47
 
61
48
  await runKernelCommand({
62
49
  opts,
63
50
  label: 'Query',
64
- fn: ({ graph }) => withSelfHint(() => graph.query(prepared.ast, { page: prepared.page }), meta),
51
+ fn: async (context) => {
52
+ const expanded = await Promise.all(sources.map((source) => expandSelfInPath(source, context)))
53
+ const meta: SelfExpansionMeta | undefined = expanded.find(
54
+ (entry) => entry.meta !== undefined,
55
+ )?.meta
56
+ const resolved =
57
+ meta === undefined
58
+ ? prepared
59
+ : prepareQuery({ ...input, sources: expanded.map(({ path }) => path) })
60
+ return withSelfHint(() => context.graph.query(resolved.ast, { page: resolved.page }), meta)
61
+ },
65
62
  format: (response, format) => {
66
63
  output(response.result, format)
67
64
  if (response.page.next && !isMachine(format)) {
@@ -81,9 +78,9 @@ async function readAst(opts: QueryOpts): Promise<unknown | undefined> {
81
78
  try {
82
79
  raw = await readFile(opts.file, 'utf8')
83
80
  } catch (error) {
84
- throw new Error(
85
- `Cannot read --file ${opts.file}: ${error instanceof Error ? error.message : error}`,
86
- )
81
+ throw new AstraleError('FILE_READ_FAILED', `Cannot read --file ${opts.file}.`, undefined, {
82
+ cause: error,
83
+ })
87
84
  }
88
85
  return parseJson(raw, opts.file)
89
86
  }
@@ -122,7 +119,7 @@ Examples:
122
119
  { flags: '-f, --file <path>', description: 'Read a canonical Query V6 document from a file' },
123
120
  {
124
121
  flags: '--definition <path>',
125
- description: 'Select Nodes implementing one exact Class or Interface',
122
+ description: 'Select Nodes satisfying one exact Class',
126
123
  },
127
124
  { flags: '--edge <class>', description: 'Expand one exact Edge Class' },
128
125
  {
@@ -6,7 +6,7 @@ import type { CommandDefinition } from '../program/index'
6
6
  import { runKernelCommand } from '../connection'
7
7
  import { AstraleError } from '../errors'
8
8
  import { decodeJwtExpiration } from '../lib/local-status'
9
- import { failClosed, log } from '../lib/log'
9
+ import { failInput, log } from '../lib/log'
10
10
  import { output } from '../lib/output'
11
11
 
12
12
  /**
@@ -23,7 +23,12 @@ export type TokenOpts = KernelCommandOpts & {
23
23
 
24
24
  export async function tokenCommand(opts: TokenOpts): Promise<void> {
25
25
  const commandOpts: TokenOpts = opts.for && !opts.as ? { ...opts, as: opts.for } : opts
26
- const ttl = parseTtl(commandOpts.ttl)
26
+ let ttl: number
27
+ try {
28
+ ttl = parseTtl(commandOpts.ttl)
29
+ } catch (error) {
30
+ failInput(error, opts)
31
+ }
27
32
  await runKernelCommand<string>({
28
33
  opts: commandOpts,
29
34
  label: 'Minting delegation token',
@@ -97,10 +102,6 @@ Examples:
97
102
  { flags: '--for <identity>', description: 'Mint the token for this identity (alias of --as)' },
98
103
  ],
99
104
  action: async (opts) => {
100
- try {
101
- await tokenCommand(opts as Parameters<typeof tokenCommand>[0])
102
- } catch (error) {
103
- failClosed(error, opts as TokenOpts)
104
- }
105
+ await tokenCommand(opts as Parameters<typeof tokenCommand>[0])
105
106
  },
106
107
  } satisfies CommandDefinition
@@ -89,9 +89,10 @@ export async function resolveSession(
89
89
  }
90
90
  const targetInput =
91
91
  parsed.kind === 'target' ? parsed.path : (opts.target ?? viewOwnerTarget(parsed.path))
92
- const { path: target } = await expandSelfInPath(targetInput, opts)
93
-
94
- const candidates = await withClientSession(opts, (ctx) => resolveViewCandidates(ctx, target))
92
+ const { target, candidates } = await withClientSession(opts, async (context) => {
93
+ const { path: target } = await expandSelfInPath(targetInput, context)
94
+ return { target, candidates: await resolveViewCandidates(context, target) }
95
+ })
95
96
 
96
97
  if (opts.list) {
97
98
  return { candidates }
@@ -1,8 +1,7 @@
1
- import type { Call } from '@astrale-os/kernel-client'
2
- import type { AuthApi } from '@astrale-os/kernel-client/auth'
3
- import type { GraphApi } from '@astrale-os/kernel-client/graph'
4
- import type { ClientSession } from '@astrale-os/kernel-client/session'
5
1
  import type { IssuerId } from '@astrale-os/sdk/auth'
2
+ import type { AuthApi } from '@astrale-os/sdk/auth'
3
+ import type { Call, GraphApi } from '@astrale-os/sdk/client'
4
+ import type { ClientSession } from '@astrale-os/sdk/client/session'
6
5
 
7
6
  /** Existing CLI connection flags accepted by Kernel-touching commands. */
8
7
  export interface ConnectionOptions {
@@ -78,17 +77,17 @@ export function withAdminClientSession<Value>(
78
77
  /** Expand @self through the effective principal returned by authenticated Identity.whoami. */
79
78
  export function expandSelfInPath(
80
79
  path: string,
81
- options: KernelCommandOpts,
80
+ context: ConnectionContext,
82
81
  ): Promise<{ readonly path: string; readonly meta?: SelfExpansionMeta }>
83
82
 
84
83
  /** Expand @self once across one Call path and its CLI-authored string parameters. */
85
84
  export function expandSelfInCall(
86
85
  path: string,
87
- parameters: readonly string[],
88
- options: KernelCommandOpts,
86
+ parameters: Readonly<Record<string, unknown>>,
87
+ context: ConnectionContext,
89
88
  ): Promise<{
90
89
  readonly path: string
91
- readonly parameters: readonly string[]
90
+ readonly parameters: Readonly<Record<string, unknown>>
92
91
  readonly meta?: SelfExpansionMeta
93
92
  }>
94
93
 
@@ -40,7 +40,8 @@ customizes only the Fetch capability passed to Client. `withClientSession` and
40
40
  `withAdminClientSession` are terminal lifecycle boundaries: success, failure, and cancellation all
41
41
  close both the Client Session and its direct source-Auth client.
42
42
 
43
- The command boundary maps typed Client transport phase and delivery evidence without inspecting a
44
- private cause message. It preserves every admitted Kernel reason in machine output; human repair
45
- details are rendered only after the connection owner admits bounded public Function issues or one
46
- exact Query reason variant.
43
+ The command boundary projects typed Client failure identity, transport context, phase, and
44
+ invocation-only delivery evidence without inspecting a private cause message. Unknown native
45
+ failures become one honest unexpected diagnostic; their bounded cause graph is visible only under
46
+ explicit debug output. Every admitted Kernel reason remains available in machine output, while
47
+ human repair details require a bounded public Function issue or exact Query reason variant.
@@ -1,5 +1,5 @@
1
- import type { SessionAuth } from '@astrale-os/kernel-client/session'
2
- import type { Call } from '@astrale-os/kernel-protocol/invocation'
1
+ import type { Call } from '@astrale-os/sdk/client'
2
+ import type { SessionAuth } from '@astrale-os/sdk/client/session'
3
3
 
4
4
  import type { ConnectionContext, ConnectionOptions, ConnectionTarget } from '../api.js'
5
5
 
@@ -111,7 +111,7 @@ export const CLI_AUTH_REGISTRATION_TARGET = defineLaw({
111
111
  export const CLI_SELF_AUTHENTICATED_PRINCIPAL = defineLaw({
112
112
  id: 'CLI-SELF-AUTHENTICATED-PRINCIPAL',
113
113
  statement:
114
- '@self resolves only from authenticated Identity.whoami on the selected target, so delegated carriers use their effective principal and never an unverified JWT subject or stale local registration.',
114
+ '@self resolves inside the command-owned Client Session from authenticated Identity.whoami on the selected target, so resolution and dispatch share one lifecycle and delegated carriers never use an unverified JWT subject or stale local registration.',
115
115
  tests: [
116
116
  {
117
117
  file: '__tests__/self.test.ts',
@@ -183,7 +183,7 @@ export const CLI_CONNECTION_PUBLIC_SEMANTIC_REASON = defineLaw({
183
183
  export const CLI_CONNECTION_TYPED_ERROR_PRESENTATION = defineLaw({
184
184
  id: 'CLI-CONNECTION-TYPED-ERROR-PRESENTATION',
185
185
  statement:
186
- 'The command boundary maps typed Client transport phase and delivery evidence without parsing a private cause, preserves the admitted Kernel reason in machine output, and renders only bounded public Function issues or one exact Query repair variant for humans.',
186
+ 'The command boundary maps typed Client failure identity, transport context, phase, and invocation-only delivery evidence without parsing a private cause; acquisition never receives retry advice, unknown native failures have one non-blank unexpected diagnostic, and raw causes appear only under explicit debug output.',
187
187
  tests: [
188
188
  {
189
189
  file: '__tests__/errors.test.ts',
@@ -197,5 +197,9 @@ export const CLI_CONNECTION_TYPED_ERROR_PRESENTATION = defineLaw({
197
197
  file: '__tests__/reasons.test.ts',
198
198
  id: 'TEST-CLI-CONNECTION-ADMITS-BOUNDED-REASONS',
199
199
  },
200
+ {
201
+ file: '__tests__/failure-safety.test.ts',
202
+ id: 'TEST-CLI-CONNECTION-FAILURE-SAFETY',
203
+ },
200
204
  ],
201
205
  })
@@ -8,6 +8,7 @@ export default defineLayout({
8
8
  'command.ts',
9
9
  'credential.ts',
10
10
  'errors.ts',
11
+ 'failure/',
11
12
  'exchange.ts',
12
13
  'index.ts',
13
14
  'reasons.ts',
@@ -1,4 +1,4 @@
1
- import type { SessionAuth } from '@astrale-os/kernel-client/session'
1
+ import type { SessionAuth } from '@astrale-os/sdk/client/session'
2
2
 
3
3
  import { issuer, type IssuerId } from '@astrale-os/sdk/auth'
4
4
  import { Path } from '@astrale-os/sdk/graph/path'
@@ -1,7 +1,9 @@
1
- import { ResponseError, TransportError } from '@astrale-os/kernel-client'
1
+ import { ResponseError } from '@astrale-os/sdk/client'
2
+ import { Path } from '@astrale-os/sdk/graph/path'
2
3
  import { describe, expect, test } from 'bun:test'
3
4
 
4
5
  import { formatKernelError, functionInputIssues, schemaUpgradeHint } from '../errors'
6
+ import { transportFailure } from './failure-fixtures'
5
7
 
6
8
  const CONFLICT = 4001
7
9
  const VALIDATION = 1003
@@ -19,24 +21,28 @@ describe('formatKernelError', () => {
19
21
  writes.push(typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk))
20
22
  return true
21
23
  }) as typeof process.stderr.write
22
- const error = new TransportError('Publication discovery request failed.', {
23
- cause: Object.assign(new Error('connect ECONNREFUSED'), { code: 'ECONNREFUSED' }),
24
- phase: 'connect',
25
- delivery: 'unknown',
26
- }) as TransportError & { url?: string }
27
- error.url = 'https://localhost:8443/kernel/host'
24
+ const error = Object.assign(
25
+ transportFailure('Publication discovery request failed.', 'unknown', {
26
+ kind: 'acquisition',
27
+ resource: 'publication',
28
+ }),
29
+ {
30
+ url: 'https://localhost:8443/kernel/host',
31
+ cause: Object.assign(new Error('connect ECONNREFUSED'), { code: 'ECONNREFUSED' }),
32
+ },
33
+ )
28
34
  try {
29
- await formatKernelError(error, true)
35
+ await formatKernelError(error, true, 'https://localhost:8443/kernel/host')
30
36
  } finally {
31
37
  process.stderr.write = original
32
38
  }
33
39
 
34
40
  expect(JSON.parse(writes[0]!)).toMatchObject({
35
- error: 'CONNECTION_ERROR',
41
+ error: 'TRANSPORT_ERROR',
36
42
  message: 'Publication discovery request failed.',
37
43
  url: 'https://localhost:8443/kernel/host',
38
- phase: 'connect',
39
- delivery: 'unknown',
44
+ phase: 'unknown',
45
+ transport: { kind: 'acquisition', resource: 'publication' },
40
46
  })
41
47
  expect(writes[0]).not.toContain('ECONNREFUSED')
42
48
  })
@@ -50,9 +56,8 @@ describe('formatKernelError', () => {
50
56
  }) as typeof process.stderr.write
51
57
  try {
52
58
  await formatKernelError(
53
- new TransportError('Request timed out.', {
54
- cause: new Error('timeout'),
55
- phase: 'timeout',
59
+ transportFailure('Request timed out.', 'timeout', {
60
+ kind: 'invocation',
56
61
  delivery: 'unknown',
57
62
  }),
58
63
  true,
@@ -74,7 +79,7 @@ describe('formatKernelError', () => {
74
79
  expect(JSON.parse(writes[0]!)).toMatchObject({
75
80
  error: 'TIMEOUT',
76
81
  phase: 'timeout',
77
- delivery: 'unknown',
82
+ transport: { kind: 'invocation', delivery: 'unknown' },
78
83
  operation: '4a4c9a18-50f6-4d84-a7b7-2d83e3e45dc8',
79
84
  retry:
80
85
  'astrale domain install https://crm.test --direct --operation 4a4c9a18-50f6-4d84-a7b7-2d83e3e45dc8',
@@ -229,7 +234,7 @@ describe('formatKernelError', () => {
229
234
  )
230
235
  })
231
236
 
232
- test('maps SDK class names to stable CLI error codes', async () => {
237
+ test('maps SDK error identities to stable CLI error codes', async () => {
233
238
  const writes: string[] = []
234
239
  const original = process.stderr.write
235
240
  process.stderr.write = ((chunk: string | Uint8Array) => {
@@ -237,15 +242,17 @@ describe('formatKernelError', () => {
237
242
  return true
238
243
  }) as typeof process.stderr.write
239
244
  try {
240
- const pathError = new Error('Path must have an Id or Domain anchor.')
241
- pathError.name = 'PathError'
242
- await formatKernelError(pathError, true)
245
+ try {
246
+ Path.parse('not-a-path')
247
+ } catch (error) {
248
+ await formatKernelError(error, true)
249
+ }
243
250
  } finally {
244
251
  process.stderr.write = original
245
252
  }
246
253
  expect(JSON.parse(writes[0]!)).toEqual({
247
254
  error: 'PATH_INVALID',
248
- message: 'Path must have an Id or Domain anchor.',
255
+ message: expect.any(String),
249
256
  })
250
257
  })
251
258
 
@@ -1,4 +1,4 @@
1
- import type { Fetch } from '@astrale-os/kernel-client'
1
+ import type { Fetch } from '@astrale-os/sdk/client'
2
2
 
3
3
  import { issuer } from '@astrale-os/sdk/auth'
4
4
  import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
@@ -190,6 +190,35 @@ describe('Domain token exchange', () => {
190
190
  message: 'Token exchange returned an invalid success response.',
191
191
  })
192
192
  })
193
+
194
+ test('normalizes native issuer Fetch failures without exposing their message', async () => {
195
+ const native = new AggregateError([new Error('private DNS state')], '')
196
+ const resolver = createExchangeCredentialResolver(
197
+ TARGET,
198
+ { resolve: async () => SOURCE_TOKEN },
199
+ async (input, init) => {
200
+ if (String(input) === INVOCATION) {
201
+ const body = JSON.parse(await new Response(init?.body).text()) as Record<string, any>
202
+ return invocationResponse(
203
+ body.requestId,
204
+ body.call.input && Object.keys(body.call.input).length === 0
205
+ ? { id: 'user-1' }
206
+ : 'kernel-destination-envelope',
207
+ new Headers(init?.headers).get('accept')!,
208
+ )
209
+ }
210
+ throw native
211
+ },
212
+ 5_000,
213
+ new ExchangeCredentialCache(join(directory, 'native-failure.json')),
214
+ )
215
+
216
+ await expect(resolver.resolve(KERNEL, new AbortController().signal)).rejects.toMatchObject({
217
+ code: 'TOKEN_EXCHANGE_UNAVAILABLE',
218
+ message: 'Domain issuer discovery could not be reached.',
219
+ cause: native,
220
+ })
221
+ })
193
222
  })
194
223
 
195
224
  function exchangeFetch(