@astrale-os/cli 0.8.1-alpha.2 → 0.8.1-alpha.3
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 +192 -47
- package/package.json +1 -1
- package/src/commands/__tests__/call-describe.test.ts +67 -0
- package/src/commands/__tests__/install-direct.test.ts +20 -0
- package/src/commands/__tests__/instance-list-admin.test.ts +26 -0
- package/src/commands/__tests__/logs.test.ts +23 -0
- package/src/commands/call-describe.ts +131 -0
- package/src/commands/call.ts +17 -21
- package/src/commands/domain/install.ts +39 -13
- package/src/commands/get.ts +3 -3
- package/src/commands/instance/list.ts +36 -3
- package/src/commands/logs.ts +29 -9
- package/src/graph/__tests__/mutation.test.ts +3 -1
- package/src/graph/mutation.ts +15 -0
- package/src/program/__tests__/program.test.ts +1 -1
package/dist/astrale.js
CHANGED
|
@@ -2576,7 +2576,7 @@ var package_default;
|
|
|
2576
2576
|
var init_package = __esm(() => {
|
|
2577
2577
|
package_default = {
|
|
2578
2578
|
name: "@astrale-os/cli",
|
|
2579
|
-
version: "0.8.1-alpha.
|
|
2579
|
+
version: "0.8.1-alpha.3",
|
|
2580
2580
|
description: "Astrale CLI — connect to existing Astrale kernels",
|
|
2581
2581
|
keywords: [
|
|
2582
2582
|
"astrale",
|
|
@@ -90137,6 +90137,9 @@ var init_mutation2 = __esm(() => {
|
|
|
90137
90137
|
|
|
90138
90138
|
// src/graph/mutation.ts
|
|
90139
90139
|
function prepareMutation(input) {
|
|
90140
|
+
if (isLegacyPatchData(input)) {
|
|
90141
|
+
throw new TypeError("Legacy PatchData { nodes, edges } is not Mutation V3. Author { preconditions, operations } or a canonical astrale.graph.mutation/v3 document.");
|
|
90142
|
+
}
|
|
90140
90143
|
if (isCanonicalCandidate(input))
|
|
90141
90144
|
return MutationAST.decode(input);
|
|
90142
90145
|
return MutationAST.create(input);
|
|
@@ -90144,6 +90147,9 @@ function prepareMutation(input) {
|
|
|
90144
90147
|
function isCanonicalCandidate(input) {
|
|
90145
90148
|
return input !== null && typeof input === "object" && !Array.isArray(input) && (Object.hasOwn(input, "format") || Object.hasOwn(input, "version"));
|
|
90146
90149
|
}
|
|
90150
|
+
function isLegacyPatchData(input) {
|
|
90151
|
+
return input !== null && typeof input === "object" && !Array.isArray(input) && !Object.hasOwn(input, "operations") && (Object.hasOwn(input, "nodes") || Object.hasOwn(input, "edges"));
|
|
90152
|
+
}
|
|
90147
90153
|
var init_mutation3 = __esm(() => {
|
|
90148
90154
|
init_mutation2();
|
|
90149
90155
|
});
|
|
@@ -90308,6 +90314,99 @@ var init_binary4 = __esm(() => {
|
|
|
90308
90314
|
init_output();
|
|
90309
90315
|
});
|
|
90310
90316
|
|
|
90317
|
+
// src/commands/call-describe.ts
|
|
90318
|
+
function describeCallableFromSchema(path5, schema2) {
|
|
90319
|
+
if (path5.ast.anchor.kind !== "domain")
|
|
90320
|
+
return;
|
|
90321
|
+
const origin = path5.ast.anchor.origin;
|
|
90322
|
+
const last = path5.ast.steps.at(-1);
|
|
90323
|
+
const document = asRecord2(schema2);
|
|
90324
|
+
if (document === undefined || last === undefined)
|
|
90325
|
+
return;
|
|
90326
|
+
if (last.kind === "method") {
|
|
90327
|
+
const owner = path5.ast.steps.at(-2);
|
|
90328
|
+
if (owner?.kind === "projection" && (owner.projection.kind === "class" || owner.projection.kind === "interface")) {
|
|
90329
|
+
const bag = asRecord2(owner.projection.kind === "class" ? document.classes : document.interfaces);
|
|
90330
|
+
const definition3 = asRecord2(bag?.[owner.projection.name]);
|
|
90331
|
+
const method = asRecord2(asRecord2(definition3?.methods)?.[last.name]);
|
|
90332
|
+
if (method !== undefined) {
|
|
90333
|
+
return Object.freeze({
|
|
90334
|
+
path: path5.raw,
|
|
90335
|
+
origin,
|
|
90336
|
+
class: owner.projection.name,
|
|
90337
|
+
method: last.name,
|
|
90338
|
+
dispatch: last.dispatch,
|
|
90339
|
+
...callableFields(method)
|
|
90340
|
+
});
|
|
90341
|
+
}
|
|
90342
|
+
}
|
|
90343
|
+
const matches3 = findNamedMethods(document, origin, path5.raw, last.name, last.dispatch);
|
|
90344
|
+
if (matches3.length === 1)
|
|
90345
|
+
return matches3[0];
|
|
90346
|
+
if (matches3.length > 1) {
|
|
90347
|
+
return Object.freeze({
|
|
90348
|
+
path: path5.raw,
|
|
90349
|
+
origin,
|
|
90350
|
+
method: last.name,
|
|
90351
|
+
dispatch: last.dispatch,
|
|
90352
|
+
candidates: Object.freeze(matches3)
|
|
90353
|
+
});
|
|
90354
|
+
}
|
|
90355
|
+
return;
|
|
90356
|
+
}
|
|
90357
|
+
if (last.kind === "projection" && last.projection.kind === "function") {
|
|
90358
|
+
const fn2 = asRecord2(asRecord2(document.functions)?.[last.projection.name]);
|
|
90359
|
+
if (fn2 === undefined)
|
|
90360
|
+
return;
|
|
90361
|
+
return Object.freeze({
|
|
90362
|
+
path: path5.raw,
|
|
90363
|
+
origin,
|
|
90364
|
+
function: last.projection.name,
|
|
90365
|
+
...callableFields(fn2)
|
|
90366
|
+
});
|
|
90367
|
+
}
|
|
90368
|
+
return;
|
|
90369
|
+
}
|
|
90370
|
+
function missingCallableDescription(path5) {
|
|
90371
|
+
return new AstraleError("CALL_DESCRIBE_UNAVAILABLE", `No callable schema is installed for ${path5}.`, "Use a Domain-rooted Path such as /:host.astrale.ai:class.Manager:createInstance. Method Paths are not Function nodes.");
|
|
90372
|
+
}
|
|
90373
|
+
function findNamedMethods(schema2, origin, path5, method, dispatch) {
|
|
90374
|
+
const matches3 = [];
|
|
90375
|
+
for (const bag of [schema2.classes, schema2.interfaces]) {
|
|
90376
|
+
const definitions2 = asRecord2(bag);
|
|
90377
|
+
if (definitions2 === undefined)
|
|
90378
|
+
continue;
|
|
90379
|
+
for (const [className, definition3] of Object.entries(definitions2)) {
|
|
90380
|
+
const methodDef = asRecord2(asRecord2(asRecord2(definition3)?.methods)?.[method]);
|
|
90381
|
+
if (methodDef === undefined)
|
|
90382
|
+
continue;
|
|
90383
|
+
matches3.push(Object.freeze({
|
|
90384
|
+
path: path5,
|
|
90385
|
+
origin,
|
|
90386
|
+
class: className,
|
|
90387
|
+
method,
|
|
90388
|
+
dispatch,
|
|
90389
|
+
...callableFields(methodDef)
|
|
90390
|
+
}));
|
|
90391
|
+
}
|
|
90392
|
+
}
|
|
90393
|
+
return matches3;
|
|
90394
|
+
}
|
|
90395
|
+
function callableFields(value2) {
|
|
90396
|
+
return {
|
|
90397
|
+
...typeof value2.description === "string" ? { description: value2.description } : {},
|
|
90398
|
+
...value2.auth === undefined ? {} : { auth: value2.auth },
|
|
90399
|
+
...value2.input === undefined ? {} : { input: value2.input },
|
|
90400
|
+
...value2.output === undefined ? {} : { output: value2.output }
|
|
90401
|
+
};
|
|
90402
|
+
}
|
|
90403
|
+
function asRecord2(input) {
|
|
90404
|
+
return input !== null && typeof input === "object" && !Array.isArray(input) ? input : undefined;
|
|
90405
|
+
}
|
|
90406
|
+
var init_call_describe = __esm(() => {
|
|
90407
|
+
init_errors2();
|
|
90408
|
+
});
|
|
90409
|
+
|
|
90311
90410
|
// src/commands/call.ts
|
|
90312
90411
|
var exports_call = {};
|
|
90313
90412
|
__export(exports_call, {
|
|
@@ -90374,28 +90473,20 @@ async function describeOperation(path5, opts) {
|
|
|
90374
90473
|
await runKernelCommand({
|
|
90375
90474
|
opts,
|
|
90376
90475
|
label: `Schema for ${path5}`,
|
|
90377
|
-
fn: async ({ graph }) =>
|
|
90378
|
-
|
|
90379
|
-
|
|
90380
|
-
|
|
90381
|
-
|
|
90382
|
-
|
|
90383
|
-
|
|
90384
|
-
if (
|
|
90385
|
-
|
|
90386
|
-
|
|
90387
|
-
}
|
|
90476
|
+
fn: async ({ graph }) => {
|
|
90477
|
+
const parsed = Path.parse(path5);
|
|
90478
|
+
if (parsed.ast.anchor.kind !== "domain") {
|
|
90479
|
+
throw missingCallableDescription(path5);
|
|
90480
|
+
}
|
|
90481
|
+
const domain3 = await graph.getOrThrow(Path.parse(`/:${parsed.ast.anchor.origin}`));
|
|
90482
|
+
const described = describeCallableFromSchema(parsed, nodeProperty(domain3, "schema"));
|
|
90483
|
+
if (described === undefined)
|
|
90484
|
+
throw missingCallableDescription(path5);
|
|
90485
|
+
return described;
|
|
90486
|
+
},
|
|
90487
|
+
format: (schema2, fmtOpts) => output(schema2, fmtOpts)
|
|
90388
90488
|
});
|
|
90389
90489
|
}
|
|
90390
|
-
function tryParseJson(value2) {
|
|
90391
|
-
if (typeof value2 !== "string")
|
|
90392
|
-
return value2;
|
|
90393
|
-
try {
|
|
90394
|
-
return JSON.parse(value2);
|
|
90395
|
-
} catch {
|
|
90396
|
-
return value2;
|
|
90397
|
-
}
|
|
90398
|
-
}
|
|
90399
90490
|
async function parseParams(rawParams, dataFlag) {
|
|
90400
90491
|
if (dataFlag) {
|
|
90401
90492
|
if (rawParams.length > 0) {
|
|
@@ -90471,6 +90562,7 @@ var init_call2 = __esm(() => {
|
|
|
90471
90562
|
init_binary4();
|
|
90472
90563
|
init_log();
|
|
90473
90564
|
init_output();
|
|
90565
|
+
init_call_describe();
|
|
90474
90566
|
PARAM_KEY_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
90475
90567
|
call_default = {
|
|
90476
90568
|
name: "call",
|
|
@@ -90490,16 +90582,19 @@ Self-reference:
|
|
|
90490
90582
|
(e.g. via 'astrale get @self --json'). Resolution authenticates to
|
|
90491
90583
|
the selected Kernel and never trusts a local registration or JWT sub.
|
|
90492
90584
|
|
|
90585
|
+
--describe reads the callable's input/output from the installed Domain
|
|
90586
|
+
schema. Method Paths are not Function nodes.
|
|
90587
|
+
|
|
90493
90588
|
Examples:
|
|
90494
|
-
$ astrale call /:host.astrale.ai:class.
|
|
90589
|
+
$ astrale call /:host.astrale.ai:class.Manager:createInstance --describe
|
|
90495
90590
|
$ astrale call /:blog.acme.com:class.Author:list limit=10
|
|
90496
90591
|
$ astrale call '@self::deactivate'
|
|
90497
|
-
$ astrale call /:
|
|
90592
|
+
$ astrale call /:kernel.astrale.ai:function.journal --data '{"limit":5}' --json
|
|
90498
90593
|
`,
|
|
90499
90594
|
arguments: [
|
|
90500
90595
|
{
|
|
90501
90596
|
name: "path",
|
|
90502
|
-
description: "Operation path (e.g., /:host.astrale.ai:class.
|
|
90597
|
+
description: "Operation path (e.g., /:host.astrale.ai:class.Manager:createInstance or /node::method)"
|
|
90503
90598
|
},
|
|
90504
90599
|
{ name: "params...", description: "Params as key=value pairs", required: false }
|
|
90505
90600
|
],
|
|
@@ -90597,7 +90692,7 @@ async function getCommand(target2, opts) {
|
|
|
90597
90692
|
try {
|
|
90598
90693
|
({ path: path5, meta: meta3 } = await expandSelfInPath(target2, opts));
|
|
90599
90694
|
} catch (error52) {
|
|
90600
|
-
|
|
90695
|
+
await formatKernelError(error52, isMachine(opts), undefined, opts.debug);
|
|
90601
90696
|
process.exit(1);
|
|
90602
90697
|
}
|
|
90603
90698
|
await runKernelCommand({
|
|
@@ -90611,7 +90706,7 @@ var get_default;
|
|
|
90611
90706
|
var init_get = __esm(() => {
|
|
90612
90707
|
init_path5();
|
|
90613
90708
|
init_connection2();
|
|
90614
|
-
|
|
90709
|
+
init_errors12();
|
|
90615
90710
|
init_output();
|
|
90616
90711
|
get_default = {
|
|
90617
90712
|
name: "get",
|
|
@@ -91173,24 +91268,37 @@ function printRecord(record10) {
|
|
|
91173
91268
|
`);
|
|
91174
91269
|
}
|
|
91175
91270
|
function acceptRecord2(input, index) {
|
|
91176
|
-
if (!isRecord8(input) || !Number.isSafeInteger(input.sequence) || typeof input.
|
|
91271
|
+
if (!isRecord8(input) || !Number.isSafeInteger(input.sequence) || typeof input.topic !== "string") {
|
|
91177
91272
|
throw new TypeError(`Kernel journal record ${index} is invalid`);
|
|
91178
91273
|
}
|
|
91179
|
-
|
|
91180
|
-
|
|
91181
|
-
|
|
91182
|
-
}
|
|
91274
|
+
const occurredAt = optionalText2(input.occurredAt, index, "occurredAt");
|
|
91275
|
+
const timestamp = optionalText2(input.timestamp, index, "timestamp") ?? occurredAt;
|
|
91276
|
+
if (timestamp === undefined) {
|
|
91277
|
+
throw new TypeError(`Kernel journal record ${index} is missing occurredAt/timestamp`);
|
|
91183
91278
|
}
|
|
91279
|
+
const correlation = isRecord8(input.correlation) ? input.correlation : undefined;
|
|
91280
|
+
const correlationId = optionalText2(input.correlationId, index, "correlationId") ?? optionalText2(correlation?.invocationId, index, "correlation.invocationId");
|
|
91281
|
+
const principal = optionalText2(input.principal, index, "principal");
|
|
91184
91282
|
return Object.freeze({
|
|
91185
91283
|
sequence: input.sequence,
|
|
91186
|
-
timestamp
|
|
91284
|
+
timestamp,
|
|
91187
91285
|
topic: input.topic,
|
|
91188
91286
|
payload: input.payload,
|
|
91189
|
-
...
|
|
91190
|
-
...input.
|
|
91191
|
-
...
|
|
91287
|
+
...occurredAt === undefined ? {} : { occurredAt },
|
|
91288
|
+
...optionalText2(input.committedAt, index, "committedAt") === undefined ? {} : { committedAt: input.committedAt },
|
|
91289
|
+
...principal === undefined ? {} : { principal },
|
|
91290
|
+
...correlationId === undefined ? {} : { correlationId },
|
|
91291
|
+
...optionalText2(input.causationId, index, "causationId") === undefined ? {} : { causationId: input.causationId }
|
|
91192
91292
|
});
|
|
91193
91293
|
}
|
|
91294
|
+
function optionalText2(input, index, field) {
|
|
91295
|
+
if (input === undefined)
|
|
91296
|
+
return;
|
|
91297
|
+
if (typeof input !== "string") {
|
|
91298
|
+
throw new TypeError(`Kernel journal record ${index}.${field} must be text`);
|
|
91299
|
+
}
|
|
91300
|
+
return input;
|
|
91301
|
+
}
|
|
91194
91302
|
function positiveInteger3(flag, raw2) {
|
|
91195
91303
|
if (!/^\d+$/.test(raw2))
|
|
91196
91304
|
throw new TypeError(`${flag} must be a positive integer`);
|
|
@@ -92742,7 +92850,8 @@ Examples:
|
|
|
92742
92850
|
var exports_list = {};
|
|
92743
92851
|
__export(exports_list, {
|
|
92744
92852
|
default: () => list_default,
|
|
92745
|
-
buildInstanceRows: () => buildInstanceRows
|
|
92853
|
+
buildInstanceRows: () => buildInstanceRows,
|
|
92854
|
+
adminInventoryUnavailable: () => adminInventoryUnavailable
|
|
92746
92855
|
});
|
|
92747
92856
|
function buildInstanceRows(managed, bookmarks, show) {
|
|
92748
92857
|
const rows = [];
|
|
@@ -92786,9 +92895,19 @@ function buildInstanceRows(managed, bookmarks, show) {
|
|
|
92786
92895
|
}
|
|
92787
92896
|
return rows;
|
|
92788
92897
|
}
|
|
92789
|
-
|
|
92898
|
+
function adminInventoryUnavailable(cause) {
|
|
92899
|
+
const code = cause instanceof AstraleError ? cause.code : undefined;
|
|
92900
|
+
if (code !== undefined && ADMIN_INVENTORY_CODES.has(code)) {
|
|
92901
|
+
return new AstraleError("ADMIN_INVENTORY_UNAVAILABLE", "Managed instance listing needs the Admin Domain and an IdP-backed identity. Admin is not deployed in this environment.", "Use `astrale instance list --bookmarked` for local kernel bookmarks. Key-backed identities cannot mint an Admin Domain token.");
|
|
92902
|
+
}
|
|
92903
|
+
if (cause instanceof AstraleError)
|
|
92904
|
+
return cause;
|
|
92905
|
+
return new AstraleError("ADMIN_INVENTORY_UNAVAILABLE", cause instanceof Error ? cause.message : String(cause), "Use `astrale instance list --bookmarked` for local kernel bookmarks.");
|
|
92906
|
+
}
|
|
92907
|
+
var COLUMNS, list_default, ADMIN_INVENTORY_CODES;
|
|
92790
92908
|
var init_list = __esm(() => {
|
|
92791
92909
|
init_source();
|
|
92910
|
+
init_errors2();
|
|
92792
92911
|
init_admin_instance();
|
|
92793
92912
|
init_admin_instance();
|
|
92794
92913
|
init_admin_target();
|
|
@@ -92823,7 +92942,11 @@ var init_list = __esm(() => {
|
|
|
92823
92942
|
}));
|
|
92824
92943
|
let managed = [];
|
|
92825
92944
|
if (!opts.bookmarked) {
|
|
92826
|
-
|
|
92945
|
+
try {
|
|
92946
|
+
managed = await withSpinner("Fetching instances", !isMachine(opts), () => listOwnedInstances(opts));
|
|
92947
|
+
} catch (error52) {
|
|
92948
|
+
throw adminInventoryUnavailable(error52);
|
|
92949
|
+
}
|
|
92827
92950
|
}
|
|
92828
92951
|
if (isMachine(opts)) {
|
|
92829
92952
|
output({
|
|
@@ -92848,6 +92971,15 @@ var init_list = __esm(() => {
|
|
|
92848
92971
|
}
|
|
92849
92972
|
}
|
|
92850
92973
|
};
|
|
92974
|
+
ADMIN_INVENTORY_CODES = new Set([
|
|
92975
|
+
"TOKEN_EXCHANGE_SOURCE_INVALID",
|
|
92976
|
+
"TOKEN_EXCHANGE_SOURCE_EXPIRED",
|
|
92977
|
+
"TOKEN_EXCHANGE_UNSUPPORTED",
|
|
92978
|
+
"TOKEN_EXCHANGE_DISCOVERY_FAILED",
|
|
92979
|
+
"TOKEN_EXCHANGE_PROTOCOL_ERROR",
|
|
92980
|
+
"TOKEN_EXCHANGE_INSECURE",
|
|
92981
|
+
"ADMIN_DOMAIN_ISSUER_MISSING"
|
|
92982
|
+
]);
|
|
92851
92983
|
});
|
|
92852
92984
|
|
|
92853
92985
|
// src/commands/instance/bookmark.ts
|
|
@@ -93591,8 +93723,23 @@ __export(exports_install, {
|
|
|
93591
93723
|
isIdentityOverride: () => isIdentityOverride,
|
|
93592
93724
|
installViaAdmin: () => installViaAdmin,
|
|
93593
93725
|
domainRefFromTarget: () => domainRefFromTarget,
|
|
93726
|
+
directInstallCallInput: () => directInstallCallInput,
|
|
93594
93727
|
default: () => install_default
|
|
93595
93728
|
});
|
|
93729
|
+
function directInstallCallInput(url3, token, operation2 = crypto.randomUUID()) {
|
|
93730
|
+
return Object.freeze({
|
|
93731
|
+
operation: operation2,
|
|
93732
|
+
domains: [
|
|
93733
|
+
Object.freeze({
|
|
93734
|
+
source: Object.freeze({
|
|
93735
|
+
kind: "remote",
|
|
93736
|
+
url: url3,
|
|
93737
|
+
...token === undefined ? {} : { token }
|
|
93738
|
+
})
|
|
93739
|
+
})
|
|
93740
|
+
]
|
|
93741
|
+
});
|
|
93742
|
+
}
|
|
93596
93743
|
async function installViaAdmin(target2, opts, dependencies = {}) {
|
|
93597
93744
|
const admin = { ...defaultAdminInstallDependencies, ...dependencies };
|
|
93598
93745
|
const interactive = !!process.stdin.isTTY && !(opts.ci || opts.noPrompt || process.env.CI);
|
|
@@ -93710,20 +93857,18 @@ async function installDirect(target2, opts) {
|
|
|
93710
93857
|
await runKernelCommand({
|
|
93711
93858
|
opts,
|
|
93712
93859
|
label: `Installing domain from ${url3}`,
|
|
93713
|
-
fn: async ({ session }) => await session.call(createPathCall(Path.project(exports_syscalls.install.ref).raw,
|
|
93714
|
-
domains: [{ url: url3, ...opts.token ? { token: opts.token } : {} }]
|
|
93715
|
-
})),
|
|
93860
|
+
fn: async ({ session }) => await session.call(createPathCall(Path.project(exports_syscalls.install.ref).raw, directInstallCallInput(url3, opts.token))),
|
|
93716
93861
|
format: (result, fmtOpts, isRaw) => {
|
|
93717
93862
|
if (isRaw) {
|
|
93718
93863
|
output(result, fmtOpts);
|
|
93719
93864
|
return;
|
|
93720
93865
|
}
|
|
93721
|
-
const installed = result[0];
|
|
93722
|
-
if (
|
|
93723
|
-
throw new Error("Kernel install returned no
|
|
93724
|
-
|
|
93725
|
-
|
|
93726
|
-
|
|
93866
|
+
const installed = result.transitions[0]?.intent;
|
|
93867
|
+
if (installed === undefined) {
|
|
93868
|
+
throw new Error("Kernel install returned no committed Domain transition.");
|
|
93869
|
+
}
|
|
93870
|
+
const revision = installed.target?.schemaRevision ?? result.operation;
|
|
93871
|
+
log.success(`Domain installed: ${installed.origin}@${revision}`);
|
|
93727
93872
|
if (isIdentityOverride(installed.origin, host) && installed.origin !== consentedOrigin) {
|
|
93728
93873
|
log.warn(`Installed origin "${installed.origin}" differs from the serving host "${host}" ` + `and was not confirmed before install (the worker's /meta did not declare it). ` + `Every ${installed.origin}/* call on this instance now routes to ${host}.`);
|
|
93729
93874
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Path } from '@astrale-os/sdk/graph/path'
|
|
2
|
+
import { describe, expect, test } from 'bun:test'
|
|
3
|
+
|
|
4
|
+
import { describeCallableFromSchema } from '../call-describe'
|
|
5
|
+
|
|
6
|
+
const schema = {
|
|
7
|
+
classes: {
|
|
8
|
+
Manager: {
|
|
9
|
+
methods: {
|
|
10
|
+
createInstance: {
|
|
11
|
+
auth: 'authorized',
|
|
12
|
+
description: 'Create one child Instance.',
|
|
13
|
+
input: { type: 'object', required: ['operationId', 'slug'] },
|
|
14
|
+
output: { mode: 'value' },
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
functions: {
|
|
20
|
+
journal: {
|
|
21
|
+
description: 'Read the authorized Kernel journal.',
|
|
22
|
+
input: { type: 'object' },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('describeCallableFromSchema', () => {
|
|
28
|
+
test('reads a static Class method from the Domain schema', () => {
|
|
29
|
+
const described = describeCallableFromSchema(
|
|
30
|
+
Path.parse('/:host.astrale.ai:class.Manager:createInstance'),
|
|
31
|
+
schema,
|
|
32
|
+
)
|
|
33
|
+
expect(described).toMatchObject({
|
|
34
|
+
origin: 'host.astrale.ai',
|
|
35
|
+
class: 'Manager',
|
|
36
|
+
method: 'createInstance',
|
|
37
|
+
dispatch: 'static',
|
|
38
|
+
description: 'Create one child Instance.',
|
|
39
|
+
auth: 'authorized',
|
|
40
|
+
})
|
|
41
|
+
expect(described?.input).toMatchObject({ required: ['operationId', 'slug'] })
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('finds an instance method when the receiver Path is not a Class projection', () => {
|
|
45
|
+
expect(
|
|
46
|
+
describeCallableFromSchema(
|
|
47
|
+
Path.parse('/:host.astrale.ai:core.manager::createInstance'),
|
|
48
|
+
schema,
|
|
49
|
+
),
|
|
50
|
+
).toMatchObject({
|
|
51
|
+
origin: 'host.astrale.ai',
|
|
52
|
+
class: 'Manager',
|
|
53
|
+
method: 'createInstance',
|
|
54
|
+
dispatch: 'instance',
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('reads a standalone Function from the Domain schema', () => {
|
|
59
|
+
expect(
|
|
60
|
+
describeCallableFromSchema(Path.parse('/:kernel.astrale.ai:function.journal'), schema),
|
|
61
|
+
).toMatchObject({
|
|
62
|
+
origin: 'kernel.astrale.ai',
|
|
63
|
+
function: 'journal',
|
|
64
|
+
description: 'Read the authorized Kernel journal.',
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { directInstallCallInput } from '../domain/install'
|
|
4
|
+
|
|
5
|
+
describe('directInstallCallInput', () => {
|
|
6
|
+
test('sends the current remote install syscall, not a legacy url list', () => {
|
|
7
|
+
expect(directInstallCallInput('https://tasks.example.test', 'secret', 'op-1')).toEqual({
|
|
8
|
+
operation: 'op-1',
|
|
9
|
+
domains: [
|
|
10
|
+
{
|
|
11
|
+
source: {
|
|
12
|
+
kind: 'remote',
|
|
13
|
+
url: 'https://tasks.example.test',
|
|
14
|
+
token: 'secret',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { AstraleError } from '../../errors'
|
|
4
|
+
import { adminInventoryUnavailable } from '../instance/list'
|
|
5
|
+
|
|
6
|
+
describe('adminInventoryUnavailable', () => {
|
|
7
|
+
test('turns key-backed Admin exchange failures into a bookmark-only instruction', () => {
|
|
8
|
+
const error = adminInventoryUnavailable(
|
|
9
|
+
new AstraleError(
|
|
10
|
+
'TOKEN_EXCHANGE_SOURCE_INVALID',
|
|
11
|
+
'The source identity credential has no valid expiration.',
|
|
12
|
+
),
|
|
13
|
+
)
|
|
14
|
+
expect(error.code).toBe('ADMIN_INVENTORY_UNAVAILABLE')
|
|
15
|
+
expect(error.message).toContain('Admin is not deployed')
|
|
16
|
+
expect(error.hint).toContain('instance list --bookmarked')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('turns a missing Admin Domain issuer into the same instruction', () => {
|
|
20
|
+
const error = adminInventoryUnavailable(
|
|
21
|
+
new AstraleError('ADMIN_DOMAIN_ISSUER_MISSING', 'no Domain issuer'),
|
|
22
|
+
)
|
|
23
|
+
expect(error.code).toBe('ADMIN_INVENTORY_UNAVAILABLE')
|
|
24
|
+
expect(error.hint).toContain('Key-backed identities cannot mint an Admin Domain token')
|
|
25
|
+
})
|
|
26
|
+
})
|
|
@@ -66,4 +66,27 @@ describe('acceptJournalPage', () => {
|
|
|
66
66
|
expect(() => acceptJournalPage({ records: [{}] })).toThrow('record 0')
|
|
67
67
|
expect(() => acceptJournalPage({ records: [], cursor: 7 })).toThrow('cursor')
|
|
68
68
|
})
|
|
69
|
+
|
|
70
|
+
test('admits journal v2 records that use occurredAt instead of timestamp', () => {
|
|
71
|
+
const page = acceptJournalPage({
|
|
72
|
+
records: [
|
|
73
|
+
{
|
|
74
|
+
sequence: 10241,
|
|
75
|
+
topic: 'function.invoke',
|
|
76
|
+
occurredAt: '2026-08-19T16:51:10.049Z',
|
|
77
|
+
committedAt: '2026-08-19T16:51:10.070Z',
|
|
78
|
+
payload: { outcome: 'rejected' },
|
|
79
|
+
correlation: { invocationId: 'cf862a64-3aa1-4343-ba86-f9b516c4ff95' },
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
})
|
|
83
|
+
expect(page.records[0]).toMatchObject({
|
|
84
|
+
sequence: 10241,
|
|
85
|
+
topic: 'function.invoke',
|
|
86
|
+
timestamp: '2026-08-19T16:51:10.049Z',
|
|
87
|
+
occurredAt: '2026-08-19T16:51:10.049Z',
|
|
88
|
+
committedAt: '2026-08-19T16:51:10.070Z',
|
|
89
|
+
correlationId: 'cf862a64-3aa1-4343-ba86-f9b516c4ff95',
|
|
90
|
+
})
|
|
91
|
+
})
|
|
69
92
|
})
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { Path } from '@astrale-os/sdk/graph/path'
|
|
2
|
+
|
|
3
|
+
import { AstraleError } from '../errors'
|
|
4
|
+
|
|
5
|
+
type JsonRecord = Record<string, unknown>
|
|
6
|
+
|
|
7
|
+
export interface CallableDescription {
|
|
8
|
+
readonly path: string
|
|
9
|
+
readonly origin: string
|
|
10
|
+
readonly method?: string
|
|
11
|
+
readonly function?: string
|
|
12
|
+
readonly class?: string
|
|
13
|
+
readonly dispatch?: 'static' | 'instance'
|
|
14
|
+
readonly description?: string
|
|
15
|
+
readonly auth?: unknown
|
|
16
|
+
readonly input?: unknown
|
|
17
|
+
readonly output?: unknown
|
|
18
|
+
readonly candidates?: readonly CallableDescription[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Resolve one callable's input/output from an installed Domain schema document. */
|
|
22
|
+
export function describeCallableFromSchema(
|
|
23
|
+
path: Path,
|
|
24
|
+
schema: unknown,
|
|
25
|
+
): CallableDescription | undefined {
|
|
26
|
+
if (path.ast.anchor.kind !== 'domain') return undefined
|
|
27
|
+
const origin = path.ast.anchor.origin
|
|
28
|
+
const last = path.ast.steps.at(-1)
|
|
29
|
+
const document = asRecord(schema)
|
|
30
|
+
if (document === undefined || last === undefined) return undefined
|
|
31
|
+
|
|
32
|
+
if (last.kind === 'method') {
|
|
33
|
+
const owner = path.ast.steps.at(-2)
|
|
34
|
+
if (
|
|
35
|
+
owner?.kind === 'projection' &&
|
|
36
|
+
(owner.projection.kind === 'class' || owner.projection.kind === 'interface')
|
|
37
|
+
) {
|
|
38
|
+
const bag = asRecord(
|
|
39
|
+
owner.projection.kind === 'class' ? document.classes : document.interfaces,
|
|
40
|
+
)
|
|
41
|
+
const definition = asRecord(bag?.[owner.projection.name])
|
|
42
|
+
const method = asRecord(asRecord(definition?.methods)?.[last.name])
|
|
43
|
+
if (method !== undefined) {
|
|
44
|
+
return Object.freeze({
|
|
45
|
+
path: path.raw,
|
|
46
|
+
origin,
|
|
47
|
+
class: owner.projection.name,
|
|
48
|
+
method: last.name,
|
|
49
|
+
dispatch: last.dispatch,
|
|
50
|
+
...callableFields(method),
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const matches = findNamedMethods(document, origin, path.raw, last.name, last.dispatch)
|
|
55
|
+
if (matches.length === 1) return matches[0]
|
|
56
|
+
if (matches.length > 1) {
|
|
57
|
+
return Object.freeze({
|
|
58
|
+
path: path.raw,
|
|
59
|
+
origin,
|
|
60
|
+
method: last.name,
|
|
61
|
+
dispatch: last.dispatch,
|
|
62
|
+
candidates: Object.freeze(matches),
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
return undefined
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (last.kind === 'projection' && last.projection.kind === 'function') {
|
|
69
|
+
const fn = asRecord(asRecord(document.functions)?.[last.projection.name])
|
|
70
|
+
if (fn === undefined) return undefined
|
|
71
|
+
return Object.freeze({
|
|
72
|
+
path: path.raw,
|
|
73
|
+
origin,
|
|
74
|
+
function: last.projection.name,
|
|
75
|
+
...callableFields(fn),
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
return undefined
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function missingCallableDescription(path: string): AstraleError {
|
|
82
|
+
return new AstraleError(
|
|
83
|
+
'CALL_DESCRIBE_UNAVAILABLE',
|
|
84
|
+
`No callable schema is installed for ${path}.`,
|
|
85
|
+
'Use a Domain-rooted Path such as /:host.astrale.ai:class.Manager:createInstance. Method Paths are not Function nodes.',
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function findNamedMethods(
|
|
90
|
+
schema: JsonRecord,
|
|
91
|
+
origin: string,
|
|
92
|
+
path: string,
|
|
93
|
+
method: string,
|
|
94
|
+
dispatch: 'static' | 'instance',
|
|
95
|
+
): CallableDescription[] {
|
|
96
|
+
const matches: CallableDescription[] = []
|
|
97
|
+
for (const bag of [schema.classes, schema.interfaces]) {
|
|
98
|
+
const definitions = asRecord(bag)
|
|
99
|
+
if (definitions === undefined) continue
|
|
100
|
+
for (const [className, definition] of Object.entries(definitions)) {
|
|
101
|
+
const methodDef = asRecord(asRecord(asRecord(definition)?.methods)?.[method])
|
|
102
|
+
if (methodDef === undefined) continue
|
|
103
|
+
matches.push(
|
|
104
|
+
Object.freeze({
|
|
105
|
+
path,
|
|
106
|
+
origin,
|
|
107
|
+
class: className,
|
|
108
|
+
method,
|
|
109
|
+
dispatch,
|
|
110
|
+
...callableFields(methodDef),
|
|
111
|
+
}),
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return matches
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function callableFields(value: JsonRecord): Partial<CallableDescription> {
|
|
119
|
+
return {
|
|
120
|
+
...(typeof value.description === 'string' ? { description: value.description } : {}),
|
|
121
|
+
...(value.auth === undefined ? {} : { auth: value.auth }),
|
|
122
|
+
...(value.input === undefined ? {} : { input: value.input }),
|
|
123
|
+
...(value.output === undefined ? {} : { output: value.output }),
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function asRecord(input: unknown): JsonRecord | undefined {
|
|
128
|
+
return input !== null && typeof input === 'object' && !Array.isArray(input)
|
|
129
|
+
? (input as JsonRecord)
|
|
130
|
+
: undefined
|
|
131
|
+
}
|
package/src/commands/call.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { nodeProperty } from '../graph/index'
|
|
|
8
8
|
import { presentBinary } from '../lib/binary'
|
|
9
9
|
import { log } from '../lib/log'
|
|
10
10
|
import { output, present } from '../lib/output'
|
|
11
|
+
import { describeCallableFromSchema, missingCallableDescription } from './call-describe'
|
|
11
12
|
|
|
12
13
|
type CallOpts = KernelCommandOpts & {
|
|
13
14
|
data?: string
|
|
@@ -97,28 +98,20 @@ async function describeOperation(path: string, opts: CallOpts): Promise<void> {
|
|
|
97
98
|
await runKernelCommand({
|
|
98
99
|
opts,
|
|
99
100
|
label: `Schema for ${path}`,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
|
|
101
|
+
fn: async ({ graph }) => {
|
|
102
|
+
const parsed = Path.parse(path)
|
|
103
|
+
if (parsed.ast.anchor.kind !== 'domain') {
|
|
104
|
+
throw missingCallableDescription(path)
|
|
105
|
+
}
|
|
106
|
+
const domain = await graph.getOrThrow(Path.parse(`/:${parsed.ast.anchor.origin}`))
|
|
107
|
+
const described = describeCallableFromSchema(parsed, nodeProperty(domain, 'schema'))
|
|
108
|
+
if (described === undefined) throw missingCallableDescription(path)
|
|
109
|
+
return described
|
|
109
110
|
},
|
|
111
|
+
format: (schema, fmtOpts) => output(schema, fmtOpts),
|
|
110
112
|
})
|
|
111
113
|
}
|
|
112
114
|
|
|
113
|
-
function tryParseJson(value: unknown): unknown {
|
|
114
|
-
if (typeof value !== 'string') return value
|
|
115
|
-
try {
|
|
116
|
-
return JSON.parse(value)
|
|
117
|
-
} catch {
|
|
118
|
-
return value
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
115
|
// ── Param parsing ───────────────────────────────────────────
|
|
123
116
|
|
|
124
117
|
export async function parseParams(
|
|
@@ -221,17 +214,20 @@ Self-reference:
|
|
|
221
214
|
(e.g. via 'astrale get @self --json'). Resolution authenticates to
|
|
222
215
|
the selected Kernel and never trusts a local registration or JWT sub.
|
|
223
216
|
|
|
217
|
+
--describe reads the callable's input/output from the installed Domain
|
|
218
|
+
schema. Method Paths are not Function nodes.
|
|
219
|
+
|
|
224
220
|
Examples:
|
|
225
|
-
$ astrale call /:host.astrale.ai:class.
|
|
221
|
+
$ astrale call /:host.astrale.ai:class.Manager:createInstance --describe
|
|
226
222
|
$ astrale call /:blog.acme.com:class.Author:list limit=10
|
|
227
223
|
$ astrale call '@self::deactivate'
|
|
228
|
-
$ astrale call /:
|
|
224
|
+
$ astrale call /:kernel.astrale.ai:function.journal --data '{"limit":5}' --json
|
|
229
225
|
`,
|
|
230
226
|
arguments: [
|
|
231
227
|
{
|
|
232
228
|
name: 'path',
|
|
233
229
|
description:
|
|
234
|
-
'Operation path (e.g., /:host.astrale.ai:class.
|
|
230
|
+
'Operation path (e.g., /:host.astrale.ai:class.Manager:createInstance or /node::method)',
|
|
235
231
|
},
|
|
236
232
|
{ name: 'params...', description: 'Params as key=value pairs', required: false },
|
|
237
233
|
],
|
|
@@ -20,12 +20,35 @@ import { isMachine, output } from '../../lib/output'
|
|
|
20
20
|
import { confirmWithInput, promptText, selectFrom } from '../../lib/prompt'
|
|
21
21
|
import { isHttpUrl } from '../../lib/validation'
|
|
22
22
|
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
/** Public Kernel install syscall input for one remote URL. */
|
|
24
|
+
export function directInstallCallInput(
|
|
25
|
+
url: string,
|
|
26
|
+
token?: string,
|
|
27
|
+
operation: string = crypto.randomUUID(),
|
|
28
|
+
) {
|
|
29
|
+
return Object.freeze({
|
|
30
|
+
operation,
|
|
31
|
+
domains: [
|
|
32
|
+
Object.freeze({
|
|
33
|
+
source: Object.freeze({
|
|
34
|
+
kind: 'remote' as const,
|
|
35
|
+
url,
|
|
36
|
+
...(token === undefined ? {} : { token }),
|
|
37
|
+
}),
|
|
38
|
+
}),
|
|
39
|
+
],
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type DirectInstallResult = {
|
|
44
|
+
readonly operation: string
|
|
45
|
+
readonly transitions: readonly {
|
|
46
|
+
readonly intent: {
|
|
47
|
+
readonly origin: string
|
|
48
|
+
readonly target?: { readonly schemaRevision?: string } | null
|
|
49
|
+
}
|
|
50
|
+
}[]
|
|
51
|
+
}
|
|
29
52
|
|
|
30
53
|
type InstallOpts = KernelCommandOpts &
|
|
31
54
|
AdminTargetCommandOpts & {
|
|
@@ -330,19 +353,22 @@ async function installDirect(target: string | undefined, opts: InstallOpts): Pro
|
|
|
330
353
|
label: `Installing domain from ${url}`,
|
|
331
354
|
fn: async ({ session }) =>
|
|
332
355
|
(await session.call(
|
|
333
|
-
createPathCall(
|
|
334
|
-
|
|
335
|
-
|
|
356
|
+
createPathCall(
|
|
357
|
+
Path.project(syscalls.install.ref).raw,
|
|
358
|
+
directInstallCallInput(url, opts.token),
|
|
359
|
+
),
|
|
336
360
|
)) as DirectInstallResult,
|
|
337
361
|
format: (result, fmtOpts, isRaw) => {
|
|
338
362
|
if (isRaw) {
|
|
339
363
|
output(result, fmtOpts)
|
|
340
364
|
return
|
|
341
365
|
}
|
|
342
|
-
const installed = result[0]
|
|
343
|
-
if (
|
|
344
|
-
|
|
345
|
-
|
|
366
|
+
const installed = result.transitions[0]?.intent
|
|
367
|
+
if (installed === undefined) {
|
|
368
|
+
throw new Error('Kernel install returned no committed Domain transition.')
|
|
369
|
+
}
|
|
370
|
+
const revision = installed.target?.schemaRevision ?? result.operation
|
|
371
|
+
log.success(`Domain installed: ${installed.origin}@${revision}`)
|
|
346
372
|
// Belt-and-braces: the kernel-confirmed origin is authoritative. If it
|
|
347
373
|
// aliases the host and the pre-install gate never consented to THAT
|
|
348
374
|
// origin (lying or absent `/meta`), say so loudly after the fact.
|
package/src/commands/get.ts
CHANGED
|
@@ -4,8 +4,8 @@ import type { KernelCommandOpts } from '../connection'
|
|
|
4
4
|
import type { CommandDefinition } from '../program/index'
|
|
5
5
|
|
|
6
6
|
import { expandSelfInPath, runKernelCommand, withSelfHint } from '../connection'
|
|
7
|
-
import {
|
|
8
|
-
import { output } from '../lib/output'
|
|
7
|
+
import { formatKernelError } from '../connection/errors'
|
|
8
|
+
import { isMachine, output } from '../lib/output'
|
|
9
9
|
|
|
10
10
|
type GetOpts = KernelCommandOpts
|
|
11
11
|
|
|
@@ -16,7 +16,7 @@ export async function getCommand(target: string, opts: GetOpts): Promise<void> {
|
|
|
16
16
|
try {
|
|
17
17
|
;({ path, meta } = await expandSelfInPath(target, opts))
|
|
18
18
|
} catch (error) {
|
|
19
|
-
|
|
19
|
+
await formatKernelError(error, isMachine(opts), undefined, opts.debug)
|
|
20
20
|
process.exit(1)
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -4,6 +4,7 @@ import type { KernelCommandOpts } from '../../connection'
|
|
|
4
4
|
import type { Column } from '../../lib/output'
|
|
5
5
|
import type { CommandDefinition } from '../../program/index'
|
|
6
6
|
|
|
7
|
+
import { AstraleError } from '../../errors'
|
|
7
8
|
import { listOwnedInstances } from '../../lib/admin-instance'
|
|
8
9
|
import {
|
|
9
10
|
formatInstanceLocation,
|
|
@@ -61,9 +62,13 @@ export default {
|
|
|
61
62
|
|
|
62
63
|
let managed: OwnedInstanceInfo[] = []
|
|
63
64
|
if (!opts.bookmarked) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
try {
|
|
66
|
+
managed = await withSpinner('Fetching instances', !isMachine(opts), () =>
|
|
67
|
+
listOwnedInstances(opts),
|
|
68
|
+
)
|
|
69
|
+
} catch (error) {
|
|
70
|
+
throw adminInventoryUnavailable(error)
|
|
71
|
+
}
|
|
67
72
|
}
|
|
68
73
|
if (isMachine(opts)) {
|
|
69
74
|
output(
|
|
@@ -148,3 +153,31 @@ export function buildInstanceRows(
|
|
|
148
153
|
|
|
149
154
|
return rows
|
|
150
155
|
}
|
|
156
|
+
|
|
157
|
+
const ADMIN_INVENTORY_CODES = new Set([
|
|
158
|
+
'TOKEN_EXCHANGE_SOURCE_INVALID',
|
|
159
|
+
'TOKEN_EXCHANGE_SOURCE_EXPIRED',
|
|
160
|
+
'TOKEN_EXCHANGE_UNSUPPORTED',
|
|
161
|
+
'TOKEN_EXCHANGE_DISCOVERY_FAILED',
|
|
162
|
+
'TOKEN_EXCHANGE_PROTOCOL_ERROR',
|
|
163
|
+
'TOKEN_EXCHANGE_INSECURE',
|
|
164
|
+
'ADMIN_DOMAIN_ISSUER_MISSING',
|
|
165
|
+
])
|
|
166
|
+
|
|
167
|
+
/** Admin-managed inventory is not available without a deployed Admin Domain + IdP identity. */
|
|
168
|
+
export function adminInventoryUnavailable(cause: unknown): AstraleError {
|
|
169
|
+
const code = cause instanceof AstraleError ? cause.code : undefined
|
|
170
|
+
if (code !== undefined && ADMIN_INVENTORY_CODES.has(code)) {
|
|
171
|
+
return new AstraleError(
|
|
172
|
+
'ADMIN_INVENTORY_UNAVAILABLE',
|
|
173
|
+
'Managed instance listing needs the Admin Domain and an IdP-backed identity. Admin is not deployed in this environment.',
|
|
174
|
+
'Use `astrale instance list --bookmarked` for local kernel bookmarks. Key-backed identities cannot mint an Admin Domain token.',
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
if (cause instanceof AstraleError) return cause
|
|
178
|
+
return new AstraleError(
|
|
179
|
+
'ADMIN_INVENTORY_UNAVAILABLE',
|
|
180
|
+
cause instanceof Error ? cause.message : String(cause),
|
|
181
|
+
'Use `astrale instance list --bookmarked` for local kernel bookmarks.',
|
|
182
|
+
)
|
|
183
|
+
}
|
package/src/commands/logs.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface JournalRecord {
|
|
|
29
29
|
readonly timestamp: string
|
|
30
30
|
readonly topic: string
|
|
31
31
|
readonly payload: unknown
|
|
32
|
+
readonly occurredAt?: string
|
|
33
|
+
readonly committedAt?: string
|
|
32
34
|
readonly principal?: string
|
|
33
35
|
readonly correlationId?: string
|
|
34
36
|
readonly causationId?: string
|
|
@@ -146,27 +148,45 @@ function acceptRecord(input: unknown, index: number): JournalRecord {
|
|
|
146
148
|
if (
|
|
147
149
|
!isRecord(input) ||
|
|
148
150
|
!Number.isSafeInteger(input.sequence) ||
|
|
149
|
-
typeof input.timestamp !== 'string' ||
|
|
150
151
|
typeof input.topic !== 'string'
|
|
151
152
|
) {
|
|
152
153
|
throw new TypeError(`Kernel journal record ${index} is invalid`)
|
|
153
154
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
155
|
+
const occurredAt = optionalText(input.occurredAt, index, 'occurredAt')
|
|
156
|
+
const timestamp = optionalText(input.timestamp, index, 'timestamp') ?? occurredAt
|
|
157
|
+
if (timestamp === undefined) {
|
|
158
|
+
throw new TypeError(`Kernel journal record ${index} is missing occurredAt/timestamp`)
|
|
158
159
|
}
|
|
160
|
+
const correlation = isRecord(input.correlation) ? input.correlation : undefined
|
|
161
|
+
const correlationId =
|
|
162
|
+
optionalText(input.correlationId, index, 'correlationId') ??
|
|
163
|
+
optionalText(correlation?.invocationId, index, 'correlation.invocationId')
|
|
164
|
+
const principal = optionalText(input.principal, index, 'principal')
|
|
159
165
|
return Object.freeze({
|
|
160
166
|
sequence: input.sequence as number,
|
|
161
|
-
timestamp
|
|
167
|
+
timestamp,
|
|
162
168
|
topic: input.topic,
|
|
163
169
|
payload: input.payload,
|
|
164
|
-
...(
|
|
165
|
-
...(input.
|
|
166
|
-
|
|
170
|
+
...(occurredAt === undefined ? {} : { occurredAt }),
|
|
171
|
+
...(optionalText(input.committedAt, index, 'committedAt') === undefined
|
|
172
|
+
? {}
|
|
173
|
+
: { committedAt: input.committedAt as string }),
|
|
174
|
+
...(principal === undefined ? {} : { principal }),
|
|
175
|
+
...(correlationId === undefined ? {} : { correlationId }),
|
|
176
|
+
...(optionalText(input.causationId, index, 'causationId') === undefined
|
|
177
|
+
? {}
|
|
178
|
+
: { causationId: input.causationId as string }),
|
|
167
179
|
})
|
|
168
180
|
}
|
|
169
181
|
|
|
182
|
+
function optionalText(input: unknown, index: number, field: string): string | undefined {
|
|
183
|
+
if (input === undefined) return undefined
|
|
184
|
+
if (typeof input !== 'string') {
|
|
185
|
+
throw new TypeError(`Kernel journal record ${index}.${field} must be text`)
|
|
186
|
+
}
|
|
187
|
+
return input
|
|
188
|
+
}
|
|
189
|
+
|
|
170
190
|
function positiveInteger(flag: string, raw: string): number {
|
|
171
191
|
if (!/^\d+$/.test(raw)) throw new TypeError(`${flag} must be a positive integer`)
|
|
172
192
|
const value = Number.parseInt(raw, 10)
|
|
@@ -38,6 +38,8 @@ describe('prepareMutation', () => {
|
|
|
38
38
|
|
|
39
39
|
/** @evidence TEST-CLI-GRAPH-REJECTS-PATCH-DATA */
|
|
40
40
|
test('rejects legacy PatchData instead of narrowing it', () => {
|
|
41
|
-
expect(() => prepareMutation({ nodes: { create: [] }, edges: { create: [] } })).toThrow(
|
|
41
|
+
expect(() => prepareMutation({ nodes: { create: [] }, edges: { create: [] } })).toThrow(
|
|
42
|
+
/Legacy PatchData/,
|
|
43
|
+
)
|
|
42
44
|
})
|
|
43
45
|
})
|
package/src/graph/mutation.ts
CHANGED
|
@@ -4,6 +4,11 @@ import { MutationAST } from '@astrale-os/sdk/mutation'
|
|
|
4
4
|
|
|
5
5
|
/** Admit the canonical document or Core's exact rich authoring input at the CLI JSON boundary. */
|
|
6
6
|
export function prepareMutation(input: unknown): MutationASTValue {
|
|
7
|
+
if (isLegacyPatchData(input)) {
|
|
8
|
+
throw new TypeError(
|
|
9
|
+
'Legacy PatchData { nodes, edges } is not Mutation V3. Author { preconditions, operations } or a canonical astrale.graph.mutation/v3 document.',
|
|
10
|
+
)
|
|
11
|
+
}
|
|
7
12
|
if (isCanonicalCandidate(input)) return MutationAST.decode(input)
|
|
8
13
|
return MutationAST.create(input as MutationInput)
|
|
9
14
|
}
|
|
@@ -16,3 +21,13 @@ function isCanonicalCandidate(input: unknown): boolean {
|
|
|
16
21
|
(Object.hasOwn(input, 'format') || Object.hasOwn(input, 'version'))
|
|
17
22
|
)
|
|
18
23
|
}
|
|
24
|
+
|
|
25
|
+
function isLegacyPatchData(input: unknown): boolean {
|
|
26
|
+
return (
|
|
27
|
+
input !== null &&
|
|
28
|
+
typeof input === 'object' &&
|
|
29
|
+
!Array.isArray(input) &&
|
|
30
|
+
!Object.hasOwn(input, 'operations') &&
|
|
31
|
+
(Object.hasOwn(input, 'nodes') || Object.hasOwn(input, 'edges'))
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -187,7 +187,7 @@ describe('program composition', () => {
|
|
|
187
187
|
'whoami',
|
|
188
188
|
])
|
|
189
189
|
expect(createHash('sha256').update(JSON.stringify(surface)).digest('hex')).toBe(
|
|
190
|
-
'
|
|
190
|
+
'4a57bfc347f9abebcea239427214eb25fbaf9ca704170c8b4a933d0c9ba4d14e',
|
|
191
191
|
)
|
|
192
192
|
})
|
|
193
193
|
|