@astrale-os/kernel-protocol 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.
@@ -9,6 +9,8 @@ export declare const RETRY_REASONS: Readonly<{
9
9
  readonly CAPACITY_EXHAUSTED: 'CAPACITY_EXHAUSTED';
10
10
  /** The server stopped admitting work before it shuts down. */
11
11
  readonly SERVER_DRAINING: 'SERVER_DRAINING';
12
+ /** The Instance is under a maintenance session that admits only its root operator. */
13
+ readonly INSTANCE_MAINTENANCE: 'INSTANCE_MAINTENANCE';
12
14
  }>;
13
15
  export type RetryReasonCode = (typeof RETRY_REASONS)[keyof typeof RETRY_REASONS];
14
16
  export type RetryReason<Code extends RetryReasonCode = RetryReasonCode> = ErrorReason<Code, {
@@ -17,7 +19,7 @@ export type RetryReason<Code extends RetryReasonCode = RetryReasonCode> = ErrorR
17
19
  /** One hour bounds a hint that only schedules the next attempt. */
18
20
  export declare const MAXIMUM_RETRY_AFTER_MS = 3600000;
19
21
  export declare function retryReason<Code extends RetryReasonCode>(code: Code, retryAfterMs: number): RetryReason<Code>;
20
- /** Return the retry delay of a back-pressure or draining refusal, and nothing for other errors. */
22
+ /** Return the retry delay of a back-pressure, draining or maintenance refusal, and nothing else. */
21
23
  export declare function retryAfterOf(error: ErrorPayload): number | undefined;
22
24
  /** Present a retry delay as an HTTP `Retry-After` delta in whole seconds, rounded up. */
23
25
  export declare function retryAfterField(retryAfterMs: number): string;
@@ -9,6 +9,8 @@ export const RETRY_REASONS = Object.freeze({
9
9
  CAPACITY_EXHAUSTED: 'CAPACITY_EXHAUSTED',
10
10
  /** The server stopped admitting work before it shuts down. */
11
11
  SERVER_DRAINING: 'SERVER_DRAINING',
12
+ /** The Instance is under a maintenance session that admits only its root operator. */
13
+ INSTANCE_MAINTENANCE: 'INSTANCE_MAINTENANCE',
12
14
  });
13
15
  /** One hour bounds a hint that only schedules the next attempt. */
14
16
  export const MAXIMUM_RETRY_AFTER_MS = 3_600_000;
@@ -20,7 +22,7 @@ export function retryReason(code, retryAfterMs) {
20
22
  throw new TypeError('Retry reason retryAfterMs is invalid.');
21
23
  return Object.freeze({ code, details: Object.freeze({ retryAfterMs }) });
22
24
  }
23
- /** Return the retry delay of a back-pressure or draining refusal, and nothing for other errors. */
25
+ /** Return the retry delay of a back-pressure, draining or maintenance refusal, and nothing else. */
24
26
  export function retryAfterOf(error) {
25
27
  if (error.code !== CODES.BACKEND_UNAVAILABLE)
26
28
  return undefined;
@@ -5,13 +5,14 @@ export { acceptIdempotencyKey, HTTP_IDEMPOTENCY_KEY, MAXIMUM_IDEMPOTENCY_KEY_LEN
5
5
  export { acceptInvocationId, InvocationError, MAXIMUM_INVOCATION_ID_LENGTH, sameInvocationId, type InvocationId, } from './identity.js';
6
6
  export * as http from './http/index.js';
7
7
  export { acceptDelegate, type Delegate } from './delegate.js';
8
+ export { acceptMaintenance, MAXIMUM_MAINTENANCE_SESSION_LENGTH, type Maintenance, type MaintenanceSession, } from './maintenance.js';
8
9
  export { decodeMiss, miss, type Miss } from './miss.js';
9
10
  export { acceptRoute, acceptRedirection, decodeRedirectResponse, remoteRoute, type RemoteBindingInput, type Redirection, type RedirectResponse, type Route, } from './redirect.js';
10
11
  export { decodeRequest, type Request } from './request.js';
11
12
  export { admitTrace, MAXIMUM_TRACEPARENT_BYTES, MAXIMUM_TRACESTATE_BYTES, MAXIMUM_TRACESTATE_MEMBERS, type Trace, } from './trace.js';
12
13
  export { acceptRouting, routedDigest, routedRevision, type Refresh, type Routing, type Via, } from './routing.js';
13
14
  export { acceptSchemaTarget, type SchemaTarget } from './schema.js';
14
- export { decodeResponse, type ErrorResponse, type Response, type ResponseContext, type SuccessResponse, } from './response.js';
15
+ export { decodeResponse, type ErrorResponse, type ExecutionResponse, type Response, type ResponseContext, type SuccessResponse, } from './response.js';
15
16
  export { CALLABLE_FINGERPRINT_MISMATCH, DEPLOYMENT_RETIRED, SERVICE_CANDIDATE_INVALID, SERVICE_TRUST_REQUIRED, SERVICE_UNAVAILABLE, serviceErrorOf, type CallableFingerprintMismatchDetails, type CallableFingerprintMismatchReason, type DeploymentRetiredDetails, type DeploymentRetiredReason, type ServiceCandidateInvalidDetails, type ServiceCandidateInvalidReason, type ServiceErrorPayload, type ServiceReason, type ServiceTrustRequiredDetails, type ServiceTrustRequiredReason, type ServiceUnavailableDetails, type ServiceUnavailableReason, } from './service-reason.js';
16
17
  export { decodeStale, stale, type Stale } from './stale-route.js';
17
18
  export { decodeStreamFrame, type StreamEndFrame, type StreamErrorFrame, type StreamFrame, type StreamItemFrame, type StreamStartFrame, } from './stream.js';
@@ -5,6 +5,7 @@ export { acceptIdempotencyKey, HTTP_IDEMPOTENCY_KEY, MAXIMUM_IDEMPOTENCY_KEY_LEN
5
5
  export { acceptInvocationId, InvocationError, MAXIMUM_INVOCATION_ID_LENGTH, sameInvocationId, } from './identity.js';
6
6
  export * as http from './http/index.js';
7
7
  export { acceptDelegate } from './delegate.js';
8
+ export { acceptMaintenance, MAXIMUM_MAINTENANCE_SESSION_LENGTH, } from './maintenance.js';
8
9
  export { decodeMiss, miss } from './miss.js';
9
10
  export { acceptRoute, acceptRedirection, decodeRedirectResponse, remoteRoute, } from './redirect.js';
10
11
  export { decodeRequest } from './request.js';
@@ -0,0 +1,14 @@
1
+ declare const MAINTENANCE_SESSION: unique symbol;
2
+ /** Identity of the install Execution that owns one maintenance session; it grants no authority. */
3
+ export type MaintenanceSession = string & {
4
+ readonly [MAINTENANCE_SESSION]: true;
5
+ };
6
+ /** Source-Kernel guard naming the maintenance session a root request expects to act within. */
7
+ export interface Maintenance {
8
+ readonly session: MaintenanceSession;
9
+ }
10
+ /** An Execution identifier: 1-255 ASCII characters drawn from `[A-Za-z0-9._-]`. */
11
+ export declare const MAXIMUM_MAINTENANCE_SESSION_LENGTH = 255;
12
+ /** Admit one exact maintenance guard without deciding whether its session is active. */
13
+ export declare function acceptMaintenance(input: unknown): Maintenance;
14
+ export {};
@@ -0,0 +1,18 @@
1
+ import { isNodeId } from '@astrale-os/kernel-core/graph/node';
2
+ import { exactFields, invalid, portableObject, required } from './admission.js';
3
+ /** An Execution identifier: 1-255 ASCII characters drawn from `[A-Za-z0-9._-]`. */
4
+ export const MAXIMUM_MAINTENANCE_SESSION_LENGTH = 255;
5
+ const FIELDS = new Set(['session']);
6
+ /** Admit one exact maintenance guard without deciding whether its session is active. */
7
+ export function acceptMaintenance(input) {
8
+ const object = portableObject(input, 'maintenance');
9
+ exactFields(object, FIELDS, 'maintenance');
10
+ required(object, ['session'], 'maintenance');
11
+ // An Execution identifier shares the Core NodeId grammar; it names no graph Node.
12
+ const session = object.session;
13
+ if (!isNodeId(session)) {
14
+ invalid('maintenance.session must contain 1-255 ASCII characters drawn from [A-Za-z0-9._-]');
15
+ }
16
+ const admitted = session;
17
+ return Object.freeze({ session: admitted });
18
+ }
@@ -1,6 +1,7 @@
1
1
  import type { RequestId } from './correlation.js';
2
2
  import type { Delegate } from './delegate.js';
3
3
  import type { IdempotencyKey } from './idempotency.js';
4
+ import type { Maintenance } from './maintenance.js';
4
5
  import type { Routing } from './routing.js';
5
6
  import type { SchemaTarget } from './schema.js';
6
7
  import type { Call } from './target.js';
@@ -15,6 +16,8 @@ export interface Request {
15
16
  readonly routing?: Routing;
16
17
  /** Source-Kernel precondition; forbidden on an already-routed destination request. */
17
18
  readonly schema?: SchemaTarget;
19
+ /** Source-Kernel maintenance session guard; forbidden on an already-routed destination request. */
20
+ readonly maintenance?: Maintenance;
18
21
  readonly idempotencyKey?: IdempotencyKey;
19
22
  readonly trace?: Trace;
20
23
  }
@@ -1,6 +1,7 @@
1
1
  import { exactFields, invalid, portableObject, requestId, required, string } from './admission.js';
2
2
  import { acceptDelegate } from './delegate.js';
3
3
  import { acceptIdempotencyKey } from './idempotency.js';
4
+ import { acceptMaintenance } from './maintenance.js';
4
5
  import { acceptRouting } from './routing.js';
5
6
  import { acceptSchemaTarget } from './schema.js';
6
7
  import { callFromSnapshot } from './target.js';
@@ -13,6 +14,7 @@ const REQUEST_FIELDS = new Set([
13
14
  'delegate',
14
15
  'routing',
15
16
  'schema',
17
+ 'maintenance',
16
18
  'idempotencyKey',
17
19
  'trace',
18
20
  ]);
@@ -26,12 +28,16 @@ export function decodeRequest(input) {
26
28
  const delegate = object.delegate === undefined ? undefined : acceptDelegate(object.delegate);
27
29
  const routing = object.routing === undefined ? undefined : acceptRouting(object.routing);
28
30
  const schema = object.schema === undefined ? undefined : acceptSchemaTarget(object.schema);
31
+ const maintenance = object.maintenance === undefined ? undefined : acceptMaintenance(object.maintenance);
29
32
  const trace = object.trace === undefined ? undefined : admitTrace(object.trace);
30
33
  if (routing?.kind === 'via') {
31
34
  if (delegate !== undefined)
32
35
  invalid('destination requests with via routing forbid delegate');
33
36
  if (schema !== undefined)
34
37
  invalid('destination requests with via routing forbid schema target');
38
+ if (maintenance !== undefined) {
39
+ invalid('destination requests with via routing forbid maintenance');
40
+ }
35
41
  }
36
42
  else if ((credential === undefined) !== (delegate === undefined)) {
37
43
  invalid('source requests require credential and delegate together');
@@ -44,6 +50,7 @@ export function decodeRequest(input) {
44
50
  ...(delegate === undefined ? {} : { delegate }),
45
51
  ...(routing === undefined ? {} : { routing }),
46
52
  ...(schema === undefined ? {} : { schema }),
53
+ ...(maintenance === undefined ? {} : { maintenance }),
47
54
  ...(object.idempotencyKey === undefined
48
55
  ? {}
49
56
  : {
@@ -1,4 +1,5 @@
1
1
  import type { Value } from '@astrale-os/kernel-dsl/value';
2
+ import { type NodeId } from '@astrale-os/kernel-core/graph/node';
2
3
  import type { ErrorPayload } from '../errors/index.js';
3
4
  import type { RequestId } from './correlation.js';
4
5
  import type { InvocationId } from './identity.js';
@@ -15,5 +16,9 @@ export interface SuccessResponse<Result extends Value = Value> extends ResponseC
15
16
  export interface ErrorResponse extends ResponseContext {
16
17
  readonly error: ErrorPayload;
17
18
  }
18
- export type Response<Result extends Value = Value> = SuccessResponse<Result> | ErrorResponse | RedirectResponse | Stale | Miss;
19
+ /** A durable call's answer: the Execution it started or recovered, never a handler result. */
20
+ export interface ExecutionResponse extends ResponseContext {
21
+ readonly execution: NodeId;
22
+ }
23
+ export type Response<Result extends Value = Value> = SuccessResponse<Result> | ErrorResponse | ExecutionResponse | RedirectResponse | Stale | Miss;
19
24
  export declare function decodeResponse(input: unknown): Response;
@@ -1,17 +1,18 @@
1
+ import { isNodeId } from '@astrale-os/kernel-core/graph/node';
1
2
  import { errorPayload, exactFields, invalid, invocation, portableObject, requestId, required, } from './admission.js';
2
3
  import { decodeMiss } from './miss.js';
3
4
  import { decodeRedirectResponse } from './redirect.js';
4
5
  import { decodeStale } from './stale-route.js';
5
6
  const SUCCESS_FIELDS = new Set(['result', 'requestId', 'invocation']);
6
7
  const ERROR_FIELDS = new Set(['error', 'requestId', 'invocation']);
8
+ const EXECUTION_FIELDS = new Set(['execution', 'requestId', 'invocation']);
7
9
  export function decodeResponse(input) {
8
10
  const object = portableObject(input, 'response');
9
- const hasResult = Object.hasOwn(object, 'result');
10
- const hasError = Object.hasOwn(object, 'error');
11
- if (hasResult || hasError) {
12
- if (hasResult === hasError)
13
- invalid('response must contain exactly one of result or error');
14
- if (hasResult) {
11
+ const outcomes = ['result', 'error', 'execution'].filter((field) => Object.hasOwn(object, field));
12
+ if (outcomes.length > 1)
13
+ invalid('response must contain exactly one of result, error or execution');
14
+ switch (outcomes[0]) {
15
+ case 'result':
15
16
  exactFields(object, SUCCESS_FIELDS, 'response');
16
17
  required(object, ['requestId', 'invocation'], 'response');
17
18
  return Object.freeze({
@@ -19,14 +20,24 @@ export function decodeResponse(input) {
19
20
  requestId: requestId(object.requestId, 'response.requestId'),
20
21
  invocation: invocation(object.invocation, 'response.invocation'),
21
22
  });
22
- }
23
- exactFields(object, ERROR_FIELDS, 'response');
24
- required(object, ['requestId', 'error', 'invocation'], 'response');
25
- return Object.freeze({
26
- error: errorPayload(object.error, 'response.error'),
27
- requestId: requestId(object.requestId, 'response.requestId'),
28
- invocation: invocation(object.invocation, 'response.invocation'),
29
- });
23
+ case 'error':
24
+ exactFields(object, ERROR_FIELDS, 'response');
25
+ required(object, ['requestId', 'error', 'invocation'], 'response');
26
+ return Object.freeze({
27
+ error: errorPayload(object.error, 'response.error'),
28
+ requestId: requestId(object.requestId, 'response.requestId'),
29
+ invocation: invocation(object.invocation, 'response.invocation'),
30
+ });
31
+ case 'execution':
32
+ exactFields(object, EXECUTION_FIELDS, 'response');
33
+ required(object, ['requestId', 'execution', 'invocation'], 'response');
34
+ if (!isNodeId(object.execution))
35
+ invalid('response.execution must be a NodeId');
36
+ return Object.freeze({
37
+ execution: object.execution,
38
+ requestId: requestId(object.requestId, 'response.requestId'),
39
+ invocation: invocation(object.invocation, 'response.invocation'),
40
+ });
30
41
  }
31
42
  if (object.kind === 'redirect')
32
43
  return decodeRedirectResponse(object);
@@ -34,5 +45,5 @@ export function decodeResponse(input) {
34
45
  return decodeStale(object);
35
46
  if (object.kind === 'miss')
36
47
  return decodeMiss(object);
37
- return invalid('response must contain a result, error, redirect, stale, or miss control');
48
+ return invalid('response must contain a result, error, execution, redirect, stale, or miss control');
38
49
  }
@@ -1,4 +1,9 @@
1
- import type { InstallResult } from './result.js';
2
- export declare const MEDIA_TYPE: 'application/vnd.astrale.schema-installation.v3+json';
3
- export declare function encode(result: InstallResult): Promise<Uint8Array>;
4
- export declare function decode(bytes: Uint8Array, maximumBytes: number): Promise<InstallResult>;
1
+ import type { InstalledGeneration, InstallResult } from './result.js';
2
+ /**
3
+ * Admit one install result, the value a completed install execution ends with: strict, detached
4
+ * and frozen. Throws a TypeError when it is not exactly a changed receipt or the unchanged
5
+ * generations.
6
+ */
7
+ export declare function acceptResult(input: unknown): InstallResult;
8
+ /** One exact installed generation: its origin, Schema revision and Installation Target digest. */
9
+ export declare function acceptGeneration(input: unknown): InstalledGeneration;
@@ -1,32 +1,18 @@
1
1
  import { installReceipt } from '@astrale-os/kernel-core/schema/kernel/syscalls/contracts';
2
- import { json } from '@astrale-os/kernel-dsl/value';
3
- import { acceptDomainInfo } from './introspection/index.js';
4
- export const MEDIA_TYPE = 'application/vnd.astrale.schema-installation.v3+json';
5
- const encoder = new TextEncoder();
6
- export async function encode(result) {
7
- return encoder.encode(json.serialize(accept(result)));
8
- }
9
- export async function decode(bytes, maximumBytes) {
10
- if (!(bytes instanceof Uint8Array) ||
11
- !Number.isSafeInteger(maximumBytes) ||
12
- maximumBytes < 1 ||
13
- bytes.byteLength > maximumBytes) {
14
- throw new TypeError('Schema installation evidence is invalid or exceeds its bound.');
15
- }
16
- let value;
17
- try {
18
- value = json.decode(bytes, { limits: { maxBytes: maximumBytes } });
19
- }
20
- catch (cause) {
21
- throw new TypeError('Schema installation evidence is malformed.', { cause });
22
- }
23
- return accept(value);
24
- }
25
- function accept(input) {
2
+ import { patterns } from '@astrale-os/kernel-dsl/v1/addressing';
3
+ /**
4
+ * Admit one install result, the value a completed install execution ends with: strict, detached
5
+ * and frozen. Throws a TypeError when it is not exactly a changed receipt or the unchanged
6
+ * generations.
7
+ */
8
+ export function acceptResult(input) {
26
9
  const value = record(input);
27
10
  if (value.changed === true) {
28
11
  exact(value, ['changed', 'receipt']);
29
- const receipt = installReceipt.parse(value.receipt);
12
+ const parsed = installReceipt.safeParse(value.receipt);
13
+ if (!parsed.success)
14
+ throw new TypeError('Schema installation receipt is invalid.');
15
+ const receipt = deepFreeze(parsed.data);
30
16
  if (receipt.transitions.some((transition) => transition.intent.operation !== receipt.operation)) {
31
17
  throw new TypeError('Schema installation Transition belongs to another operation.');
32
18
  }
@@ -41,7 +27,7 @@ function accept(input) {
41
27
  if (!Array.isArray(value.domains) || value.domains.length === 0) {
42
28
  throw new TypeError('Schema installation unchanged Domains are empty.');
43
29
  }
44
- const domains = value.domains.map(acceptDomainInfo);
30
+ const domains = value.domains.map(acceptGeneration);
45
31
  uniqueOrigins(domains.map((domain) => domain.origin), 'Schema installation unchanged Domains');
46
32
  return Object.freeze({
47
33
  changed: false,
@@ -50,6 +36,32 @@ function accept(input) {
50
36
  }
51
37
  throw new TypeError('Schema installation result kind is invalid.');
52
38
  }
39
+ /** One exact installed generation: its origin, Schema revision and Installation Target digest. */
40
+ export function acceptGeneration(input) {
41
+ const value = record(input);
42
+ exact(value, ['generation', 'origin', 'revision']);
43
+ if (typeof value.origin !== 'string' ||
44
+ !patterns.origin.test(value.origin) ||
45
+ typeof value.revision !== 'string' ||
46
+ !patterns.revision.test(value.revision) ||
47
+ typeof value.generation !== 'string' ||
48
+ !/^sha256:[0-9a-f]{64}$/u.test(value.generation)) {
49
+ throw new TypeError('Schema installation unchanged Domain generation is invalid.');
50
+ }
51
+ return Object.freeze({
52
+ origin: value.origin,
53
+ revision: value.revision,
54
+ generation: value.generation,
55
+ });
56
+ }
57
+ function deepFreeze(input) {
58
+ if (input !== null && typeof input === 'object' && !Object.isFrozen(input)) {
59
+ for (const value of Object.values(input))
60
+ deepFreeze(value);
61
+ Object.freeze(input);
62
+ }
63
+ return input;
64
+ }
53
65
  function uniqueOrigins(origins, label) {
54
66
  if (new Set(origins).size !== origins.length) {
55
67
  throw new TypeError(`${label} contain duplicate origins.`);
@@ -1,3 +1,3 @@
1
- export { MEDIA_TYPE, decode, encode } from './codec.js';
1
+ export { acceptGeneration, acceptResult } from './codec.js';
2
2
  export * as introspection from './introspection/index.js';
3
- export type { InstallResult } from './result.js';
3
+ export type { InstalledGeneration, InstallResult } from './result.js';
@@ -1,2 +1,2 @@
1
- export { MEDIA_TYPE, decode, encode } from './codec.js';
1
+ export { acceptGeneration, acceptResult } from './codec.js';
2
2
  export * as introspection from './introspection/index.js';
@@ -127,7 +127,10 @@ function acceptDependencyBinding(input) {
127
127
  binding: expected,
128
128
  });
129
129
  }
130
- /** Admit the installed listing: origin-ordered, one release per origin, each pin of one kind. */
130
+ /**
131
+ * Admit the installed listing: origin-ordered, one installation per origin, each pin of one kind,
132
+ * with a deployment URL exactly for a release pin.
133
+ */
131
134
  function acceptInstalledReleases(input) {
132
135
  if (!Array.isArray(input))
133
136
  invalid('Installed releases are invalid.');
@@ -142,7 +145,11 @@ function acceptInstalledReleases(input) {
142
145
  }
143
146
  function acceptInstalledRelease(input) {
144
147
  const value = record(input, 'Installed release');
145
- exact(value, ['inFlight', 'issuer', 'origin', 'pin', 'revision', 'url'], 'Installed release');
148
+ const pin = installedPin(value.pin);
149
+ // Only a release pin names a deployment: an artifact pin carries no URL.
150
+ exact(value, pin.kind === 'release'
151
+ ? ['inFlight', 'issuer', 'origin', 'pin', 'revision', 'url']
152
+ : ['inFlight', 'issuer', 'origin', 'pin', 'revision'], 'Installed release');
146
153
  if (typeof value.origin !== 'string' || !patterns.origin.test(value.origin)) {
147
154
  invalid('Installed release origin is invalid.');
148
155
  }
@@ -154,8 +161,8 @@ function acceptInstalledRelease(input) {
154
161
  origin: value.origin,
155
162
  revision: value.revision,
156
163
  issuer,
157
- url: deploymentUrl(value.url),
158
- pin: installedPin(value.pin),
164
+ ...(pin.kind === 'release' ? { url: deploymentUrl(value.url) } : {}),
165
+ pin,
159
166
  inFlight: inFlightIssuers(value.inFlight, issuer),
160
167
  });
161
168
  }
@@ -169,6 +176,13 @@ function installedPin(input) {
169
176
  build: digest(value.build, 'Installed build'),
170
177
  });
171
178
  }
179
+ if (value.kind === 'artifact') {
180
+ exact(value, ['artifact', 'kind'], 'Installed artifact pin');
181
+ return Object.freeze({
182
+ kind: 'artifact',
183
+ artifact: digest(value.artifact, 'Installed artifact'),
184
+ });
185
+ }
172
186
  return invalid('Installed pin kind is invalid.');
173
187
  }
174
188
  function inFlightIssuers(input, current) {
@@ -38,12 +38,18 @@ export interface DomainDependencies {
38
38
  readonly domain: DomainInfo;
39
39
  readonly dependencies: readonly DependencyBinding[];
40
40
  }
41
- /** A DomainRelease v4 pin: its release digest beside the build digest it runs. */
42
- export interface InstalledPin {
41
+ /**
42
+ * What one installation pins: a DomainRelease v4 pin, its release digest beside the build digest
43
+ * it runs, or, for a local install, the digest of the stored artifact its source named.
44
+ */
45
+ export type InstalledPin = {
43
46
  readonly kind: 'release';
44
47
  readonly release: ReleaseDigest;
45
48
  readonly build: BuildDigest;
46
- }
49
+ } | {
50
+ readonly kind: 'artifact';
51
+ readonly artifact: Digest;
52
+ };
47
53
  /**
48
54
  * The issuer of a release one installation replaced, as the Kernel records it: accepted while its
49
55
  * work drains, and listed until a later consented install re-pins or prunes it, drained or not.
@@ -55,14 +61,20 @@ export interface InFlightIssuer {
55
61
  /** UTC millisecond ISO 8601 instant of the activation commit that replaced it. */
56
62
  readonly replacedAt: string;
57
63
  }
58
- /** One ready installation the Kernel pins to a deployment, as its caller can read it. */
64
+ /**
65
+ * One ready installation the Kernel pins to a deployment or to a stored artifact, as its caller can
66
+ * read it.
67
+ */
59
68
  export interface InstalledRelease {
60
69
  readonly origin: Key.Origin;
61
70
  readonly revision: Revision;
62
71
  /** Issuer of the installation's Domain principal. */
63
72
  readonly issuer: IssuerId;
64
- /** The deployment URL, an HTTP(S) origin, the Kernel discovered the pinned document at. */
65
- readonly url: string;
73
+ /**
74
+ * The deployment URL, an HTTP(S) origin, the Kernel discovered the pinned document at: present
75
+ * exactly for a `release` pin, since an `artifact` pin names no deployment.
76
+ */
77
+ readonly url?: string;
66
78
  readonly pin: InstalledPin;
67
79
  /** Issuer-ordered, distinct, never the current issuer. */
68
80
  readonly inFlight: readonly InFlightIssuer[];
@@ -1,9 +1,15 @@
1
1
  import type { InstallReceipt } from '@astrale-os/kernel-core/schema/kernel/syscalls/contracts';
2
- import type { DomainInfo } from './introspection/index.js';
2
+ import type { Key, Revision } from '@astrale-os/kernel-dsl/v1';
3
+ /** One exact installed Domain generation: what an install that changes nothing reports. */
4
+ export interface InstalledGeneration {
5
+ readonly origin: Key.Origin;
6
+ readonly revision: Revision;
7
+ readonly generation: `sha256:${string}`;
8
+ }
3
9
  export type InstallResult = {
4
10
  readonly changed: true;
5
11
  readonly receipt: InstallReceipt;
6
12
  } | {
7
13
  readonly changed: false;
8
- readonly domains: readonly [DomainInfo, ...DomainInfo[]];
14
+ readonly domains: readonly [InstalledGeneration, ...InstalledGeneration[]];
9
15
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/kernel-protocol",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0-beta.6",
4
4
  "description": "Portable Astrale invocation, Release, installation, routing, and issuer protocols",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -89,7 +89,7 @@
89
89
  "access": "public"
90
90
  },
91
91
  "dependencies": {
92
- "@astrale-os/kernel-core": "1.0.0-beta.4",
92
+ "@astrale-os/kernel-core": "1.0.0-beta.6",
93
93
  "@astrale-os/kernel-dsl": "1.0.0-beta.3",
94
94
  "@noble/hashes": "2.3.0"
95
95
  },