pi-radar 0.1.0 → 0.2.0
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/README.md +9 -5
- package/dist/extension/baseline.js +2626 -0
- package/dist/extension/radar-client.js +22 -2
- package/dist/extension/radar-execution.js +146 -16
- package/dist/extension/radar-location.js +2 -2
- package/dist/extension/radar-publication.js +65 -15
- package/dist/extension/radar-runtime.js +42 -0
- package/dist/extension/radar-session-marker.js +82 -0
- package/dist/index.js +216 -21
- package/extension/baseline.ts +37 -0
- package/extension/generated-contracts.ts +21 -0
- package/extension/radar-client.ts +35 -1
- package/extension/radar-execution.ts +56 -3
- package/extension/radar-location.ts +2 -2
- package/extension/radar-publication.ts +69 -12
- package/extension/radar-runtime.ts +94 -0
- package/extension/radar-session-marker.ts +146 -0
- package/index.ts +1 -0
- package/package.json +18 -4
- package/schema/public/v1/launch-identity-marker.deserialize.schema.json +32 -0
- package/schema/public/v1/launch-identity-marker.serialize.schema.json +32 -0
- package/schema/public/v1/managed-launch-metadata.deserialize.schema.json +39 -0
- package/schema/public/v1/managed-launch-metadata.serialize.schema.json +39 -0
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "node:fs";
|
|
15
15
|
import { dirname, isAbsolute, join, parse, resolve, sep } from "node:path";
|
|
16
16
|
import type { RadarLocationResolution } from "./radar-location.ts";
|
|
17
|
+
import { decodeRadarLaunchMarker } from "./radar-session-marker.ts";
|
|
17
18
|
import {
|
|
18
19
|
boundedRadarAssignment,
|
|
19
20
|
boundedRadarFacts,
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
type RadarFactsSnapshot,
|
|
28
29
|
type RadarClient,
|
|
29
30
|
type RadarLocation,
|
|
31
|
+
type RadarLaunchIdentityMarker,
|
|
30
32
|
type RadarPublisherIdentity,
|
|
31
33
|
type RadarRegistration,
|
|
32
34
|
type RadarScheduler,
|
|
@@ -323,6 +325,7 @@ type ContextReport = {
|
|
|
323
325
|
session: string | null;
|
|
324
326
|
lease_ms: number;
|
|
325
327
|
observed_at: string;
|
|
328
|
+
launch_identity?: RadarLaunchIdentityMarker;
|
|
326
329
|
};
|
|
327
330
|
|
|
328
331
|
/**
|
|
@@ -560,7 +563,9 @@ function pendingAt(
|
|
|
560
563
|
}
|
|
561
564
|
|
|
562
565
|
const CONTEXT_PENDING_KEYS = "lease_ms,observed_at,sequence,session";
|
|
566
|
+
const CONTEXT_PENDING_MARKED_KEYS = "launch_identity,lease_ms,observed_at,sequence,session";
|
|
563
567
|
const CONTEXT_ACCEPTED_KEYS = "lease_ms,observed_at,sent_at,sequence,session";
|
|
568
|
+
const CONTEXT_ACCEPTED_MARKED_KEYS = "launch_identity,lease_ms,observed_at,sent_at,sequence,session";
|
|
564
569
|
|
|
565
570
|
/** The reported session, or `undefined` when the field is not one of the two. */
|
|
566
571
|
function sessionAt(value: unknown): string | null | undefined {
|
|
@@ -570,11 +575,28 @@ function sessionAt(value: unknown): string | null | undefined {
|
|
|
570
575
|
return typeof found === "string" ? found : undefined;
|
|
571
576
|
}
|
|
572
577
|
|
|
578
|
+
function sameLaunchIdentity(
|
|
579
|
+
a: RadarLaunchIdentityMarker | undefined,
|
|
580
|
+
b: RadarLaunchIdentityMarker | undefined,
|
|
581
|
+
): boolean {
|
|
582
|
+
return (
|
|
583
|
+
a === b ||
|
|
584
|
+
(a !== undefined &&
|
|
585
|
+
b !== undefined &&
|
|
586
|
+
a.version === b.version &&
|
|
587
|
+
a.subject_id === b.subject_id &&
|
|
588
|
+
a.launch_id === b.launch_id &&
|
|
589
|
+
a.execution_generation === b.execution_generation)
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
|
|
573
593
|
function contextReportAt(
|
|
574
594
|
value: unknown,
|
|
575
595
|
keys: string,
|
|
596
|
+
markedKeys: string,
|
|
576
597
|
): ContextReport | undefined {
|
|
577
|
-
|
|
598
|
+
const marked = exactKeys(value, markedKeys);
|
|
599
|
+
if (!exactKeys(value, keys) && !marked) return undefined;
|
|
578
600
|
const sequence = numberAt(value, "sequence");
|
|
579
601
|
const lease = numberAt(value, "lease_ms");
|
|
580
602
|
const observed = textAt(value, "observed_at");
|
|
@@ -582,7 +604,17 @@ function contextReportAt(
|
|
|
582
604
|
if (sequence === undefined || sequence < 1) return undefined;
|
|
583
605
|
if (lease === undefined || observed === undefined || session === undefined)
|
|
584
606
|
return undefined;
|
|
585
|
-
|
|
607
|
+
const marker = marked
|
|
608
|
+
? decodeRadarLaunchMarker((value as Record<string, unknown>).launch_identity)
|
|
609
|
+
: undefined;
|
|
610
|
+
if (marked && marker === undefined) return undefined;
|
|
611
|
+
return {
|
|
612
|
+
sequence,
|
|
613
|
+
session,
|
|
614
|
+
lease_ms: lease,
|
|
615
|
+
observed_at: observed,
|
|
616
|
+
...(marker !== undefined ? { launch_identity: marker } : {}),
|
|
617
|
+
};
|
|
586
618
|
}
|
|
587
619
|
|
|
588
620
|
export type RadarFactsWriter = Omit<RadarChannelWriter, "update"> & {
|
|
@@ -939,7 +971,7 @@ export type RadarContextWriter = {
|
|
|
939
971
|
* session" the daemon serves; `undefined` never is, because a process that
|
|
940
972
|
* does not know its session has nothing to assert.
|
|
941
973
|
*/
|
|
942
|
-
update(session: string | null): void;
|
|
974
|
+
update(session: string | null, launchIdentity?: RadarLaunchIdentityMarker): void;
|
|
943
975
|
/** Release this writer's timers. The accepted report stays until it goes stale. */
|
|
944
976
|
stop(): void;
|
|
945
977
|
/** Re-arm a stopped writer after an extension reload. */
|
|
@@ -977,6 +1009,7 @@ function createContextWriter(input: ContextWriterInput): RadarContextWriter {
|
|
|
977
1009
|
const { store, client, scheduler, recordName, publisher } = input;
|
|
978
1010
|
let record = loadRecord();
|
|
979
1011
|
let desired: string | null | undefined;
|
|
1012
|
+
let desiredMarker: RadarLaunchIdentityMarker | undefined;
|
|
980
1013
|
let running = false;
|
|
981
1014
|
let again = false;
|
|
982
1015
|
let recovering = false;
|
|
@@ -1016,9 +1049,17 @@ function createContextWriter(input: ContextWriterInput): RadarContextWriter {
|
|
|
1016
1049
|
(!exactKeys(writer, WRITER_KEYS) || !isUuid(handle) || generation === undefined)
|
|
1017
1050
|
)
|
|
1018
1051
|
return blank(agentId);
|
|
1019
|
-
const pending = contextReportAt(
|
|
1052
|
+
const pending = contextReportAt(
|
|
1053
|
+
recordAt(value, "pending"),
|
|
1054
|
+
CONTEXT_PENDING_KEYS,
|
|
1055
|
+
CONTEXT_PENDING_MARKED_KEYS,
|
|
1056
|
+
);
|
|
1020
1057
|
const acceptedValue = recordAt(value, "accepted");
|
|
1021
|
-
const accepted = contextReportAt(
|
|
1058
|
+
const accepted = contextReportAt(
|
|
1059
|
+
acceptedValue,
|
|
1060
|
+
CONTEXT_ACCEPTED_KEYS,
|
|
1061
|
+
CONTEXT_ACCEPTED_MARKED_KEYS,
|
|
1062
|
+
);
|
|
1022
1063
|
const sentAt = numberAt(acceptedValue, "sent_at");
|
|
1023
1064
|
return {
|
|
1024
1065
|
version: RADAR_PUBLICATION_VERSION,
|
|
@@ -1100,7 +1141,12 @@ function createContextWriter(input: ContextWriterInput): RadarContextWriter {
|
|
|
1100
1141
|
sequence: pending.sequence,
|
|
1101
1142
|
lease_ms: pending.lease_ms,
|
|
1102
1143
|
observed_at: pending.observed_at,
|
|
1103
|
-
context: {
|
|
1144
|
+
context: {
|
|
1145
|
+
session: pending.session,
|
|
1146
|
+
...(pending.launch_identity !== undefined
|
|
1147
|
+
? { launch_identity: pending.launch_identity }
|
|
1148
|
+
: {}),
|
|
1149
|
+
},
|
|
1104
1150
|
});
|
|
1105
1151
|
if (answer.ok === true) {
|
|
1106
1152
|
recovering = false;
|
|
@@ -1144,12 +1190,16 @@ function createContextWriter(input: ContextWriterInput): RadarContextWriter {
|
|
|
1144
1190
|
// silently advance.
|
|
1145
1191
|
}
|
|
1146
1192
|
|
|
1147
|
-
async function stage(
|
|
1193
|
+
async function stage(
|
|
1194
|
+
session: string | null,
|
|
1195
|
+
launchIdentity?: RadarLaunchIdentityMarker,
|
|
1196
|
+
): Promise<void> {
|
|
1148
1197
|
const pending: ContextReport = {
|
|
1149
1198
|
sequence: record.sequence + 1,
|
|
1150
1199
|
session,
|
|
1151
1200
|
lease_ms: input.leaseMs,
|
|
1152
1201
|
observed_at: new Date(scheduler.now()).toISOString(),
|
|
1202
|
+
...(launchIdentity !== undefined ? { launch_identity: launchIdentity } : {}),
|
|
1153
1203
|
};
|
|
1154
1204
|
// Persisted before it is sent, so a lost reply replays this exact request.
|
|
1155
1205
|
record.pending = pending;
|
|
@@ -1170,12 +1220,13 @@ function createContextWriter(input: ContextWriterInput): RadarContextWriter {
|
|
|
1170
1220
|
if (
|
|
1171
1221
|
record.writer === undefined ||
|
|
1172
1222
|
record.accepted === undefined ||
|
|
1173
|
-
record.accepted.session !== desired
|
|
1223
|
+
record.accepted.session !== desired ||
|
|
1224
|
+
!sameLaunchIdentity(record.accepted.launch_identity, desiredMarker)
|
|
1174
1225
|
) {
|
|
1175
|
-
await stage(desired);
|
|
1226
|
+
await stage(desired, desiredMarker);
|
|
1176
1227
|
return;
|
|
1177
1228
|
}
|
|
1178
|
-
if (heartbeatDue()) await stage(record.accepted.session);
|
|
1229
|
+
if (heartbeatDue()) await stage(record.accepted.session, desiredMarker);
|
|
1179
1230
|
}
|
|
1180
1231
|
|
|
1181
1232
|
async function drain(): Promise<void> {
|
|
@@ -1211,12 +1262,18 @@ function createContextWriter(input: ContextWriterInput): RadarContextWriter {
|
|
|
1211
1262
|
}
|
|
1212
1263
|
|
|
1213
1264
|
return {
|
|
1214
|
-
update(session) {
|
|
1215
|
-
if (
|
|
1265
|
+
update(session, launchIdentity) {
|
|
1266
|
+
if (
|
|
1267
|
+
fenced ||
|
|
1268
|
+
(session === desired &&
|
|
1269
|
+
sameLaunchIdentity(desiredMarker, launchIdentity))
|
|
1270
|
+
)
|
|
1271
|
+
return;
|
|
1216
1272
|
// A session reported while this writer is stopped is remembered, not
|
|
1217
1273
|
// published: only `reopen` restarts the timers, and it must see the
|
|
1218
1274
|
// session the process is in now rather than the one it left.
|
|
1219
1275
|
desired = session;
|
|
1276
|
+
desiredMarker = launchIdentity;
|
|
1220
1277
|
if (stopped) return;
|
|
1221
1278
|
kick();
|
|
1222
1279
|
},
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { RadarExecutionAdapter } from "./radar-execution.ts";
|
|
2
|
+
import type { RadarPublication } from "./radar-publication.ts";
|
|
3
|
+
|
|
4
|
+
/** The event channel through which Pi extensions obtain the baseline runtime. */
|
|
5
|
+
export const RADAR_RUNTIME_REQUEST = "pi-radar/runtime-request";
|
|
6
|
+
export const RADAR_RUNTIME_VERSION = 1 as const;
|
|
7
|
+
|
|
8
|
+
/** The sole baseline publisher's session-scoped runtime, shared with consumers. */
|
|
9
|
+
export type RadarRuntimeServices = {
|
|
10
|
+
/** The one baseline publication; Herdsman contributes through its channels. */
|
|
11
|
+
publication: RadarPublication;
|
|
12
|
+
execution: RadarExecutionAdapter;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Lazy so discovering the provider in an extension factory starts no resources. */
|
|
16
|
+
export type RadarRuntimeHandle = {
|
|
17
|
+
version: typeof RADAR_RUNTIME_VERSION;
|
|
18
|
+
/** Provider memoizes this promise, so every extension shares one publisher. */
|
|
19
|
+
start(): Promise<RadarRuntimeServices>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Builds one lazy, memoized session runtime without starting factory resources. */
|
|
23
|
+
export function lazyRadarRuntime(
|
|
24
|
+
start: () => RadarRuntimeServices | Promise<RadarRuntimeServices>,
|
|
25
|
+
): RadarRuntimeHandle {
|
|
26
|
+
let pending: Promise<RadarRuntimeServices> | undefined;
|
|
27
|
+
return {
|
|
28
|
+
version: RADAR_RUNTIME_VERSION,
|
|
29
|
+
start: () => (pending ??= Promise.resolve().then(start)),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Minimal structural view of Pi's shared extension EventBus. */
|
|
34
|
+
export type RadarExtensionEventBus = {
|
|
35
|
+
emit(channel: string, data: unknown): void;
|
|
36
|
+
on(channel: string, handler: (data: unknown) => void): () => void;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type RuntimeRequest = {
|
|
40
|
+
version: typeof RADAR_RUNTIME_VERSION;
|
|
41
|
+
provide(handle: unknown): void;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Makes this extension's handle discoverable to other extensions in this Pi
|
|
46
|
+
* runtime. Registering the listener is safe in a factory: it starts no resources.
|
|
47
|
+
*/
|
|
48
|
+
export function provideRadarRuntime(
|
|
49
|
+
events: RadarExtensionEventBus,
|
|
50
|
+
handle: RadarRuntimeHandle,
|
|
51
|
+
): () => void {
|
|
52
|
+
return events.on(RADAR_RUNTIME_REQUEST, (value) => {
|
|
53
|
+
if (!isRuntimeRequest(value)) return;
|
|
54
|
+
value.provide(handle);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Finds the one baseline runtime in the current Pi process. Pi's event bus emits
|
|
60
|
+
* synchronously; the reply callback therefore completes before this returns.
|
|
61
|
+
* Zero or multiple providers are explicit errors, not invitations to register a
|
|
62
|
+
* second baseline publisher.
|
|
63
|
+
*/
|
|
64
|
+
export function requestRadarRuntime(
|
|
65
|
+
events: RadarExtensionEventBus,
|
|
66
|
+
): { ok: true; handle: RadarRuntimeHandle } | { ok: false; reason: "missing" | "duplicate" } {
|
|
67
|
+
const handles: RadarRuntimeHandle[] = [];
|
|
68
|
+
events.emit(RADAR_RUNTIME_REQUEST, {
|
|
69
|
+
version: RADAR_RUNTIME_VERSION,
|
|
70
|
+
provide: (candidate: unknown) => {
|
|
71
|
+
if (isRadarRuntimeHandle(candidate)) handles.push(candidate);
|
|
72
|
+
},
|
|
73
|
+
} satisfies RuntimeRequest);
|
|
74
|
+
if (handles.length === 1) return { ok: true, handle: handles[0] };
|
|
75
|
+
return { ok: false, reason: handles.length === 0 ? "missing" : "duplicate" };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isRuntimeRequest(value: unknown): value is RuntimeRequest {
|
|
79
|
+
return (
|
|
80
|
+
typeof value === "object" &&
|
|
81
|
+
value !== null &&
|
|
82
|
+
(value as RuntimeRequest).version === RADAR_RUNTIME_VERSION &&
|
|
83
|
+
typeof (value as RuntimeRequest).provide === "function"
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isRadarRuntimeHandle(value: unknown): value is RadarRuntimeHandle {
|
|
88
|
+
if (typeof value !== "object" || value === null) return false;
|
|
89
|
+
const handle = value as Partial<RadarRuntimeHandle>;
|
|
90
|
+
return (
|
|
91
|
+
handle.version === RADAR_RUNTIME_VERSION &&
|
|
92
|
+
typeof handle.start === "function"
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Radar-issued identity carried as a non-context custom entry in the active Pi
|
|
3
|
+
* session branch. It is a recovery breadcrumb, not an authority to create or
|
|
4
|
+
* mutate a Radar subject.
|
|
5
|
+
*/
|
|
6
|
+
import type { LaunchIdentityMarkerSerialize } from "./generated-contracts.ts";
|
|
7
|
+
|
|
8
|
+
export const RADAR_LAUNCH_MARKER_TYPE = "pi-radar.launch-identity";
|
|
9
|
+
export const RADAR_LAUNCH_MARKER_VERSION = 1 as const;
|
|
10
|
+
export const RADAR_MAX_EXECUTION_GENERATION = 0xffff_ffff;
|
|
11
|
+
|
|
12
|
+
export type RadarLaunchMarker = Omit<LaunchIdentityMarkerSerialize, "version"> & {
|
|
13
|
+
version: typeof RADAR_LAUNCH_MARKER_VERSION;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Structural subset of Pi's custom SessionEntry used by this codec. */
|
|
17
|
+
export type CustomSessionEntry = {
|
|
18
|
+
type: string;
|
|
19
|
+
customType?: string;
|
|
20
|
+
data?: unknown;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** pi.appendEntry-compatible callback; no Pi runtime import is needed here. */
|
|
24
|
+
export type AppendSessionEntry = (customType: string, data?: unknown) => void;
|
|
25
|
+
export type PiSessionMarkerHost = {
|
|
26
|
+
appendEntry?: AppendSessionEntry;
|
|
27
|
+
sessionManager?: { getBranch(): readonly CustomSessionEntry[] };
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const RADAR_MANAGED_SUBJECT_ENV = "PI_RADAR_MANAGED_SUBJECT_ID";
|
|
31
|
+
export const RADAR_MANAGED_LAUNCH_ENV = "PI_RADAR_MANAGED_LAUNCH_ID";
|
|
32
|
+
export const RADAR_MANAGED_GENERATION_ENV = "PI_RADAR_MANAGED_EXECUTION_GENERATION";
|
|
33
|
+
|
|
34
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
35
|
+
|
|
36
|
+
/** Validates the exact versioned marker shape before writing or accepting it. */
|
|
37
|
+
export function decodeRadarLaunchMarker(value: unknown): RadarLaunchMarker | undefined {
|
|
38
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
39
|
+
const record = value as Record<string, unknown>;
|
|
40
|
+
if (
|
|
41
|
+
Object.keys(record).sort().join(",") !==
|
|
42
|
+
"execution_generation,launch_id,subject_id,version"
|
|
43
|
+
) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
if (
|
|
47
|
+
record.version !== RADAR_LAUNCH_MARKER_VERSION ||
|
|
48
|
+
typeof record.subject_id !== "string" ||
|
|
49
|
+
!UUID.test(record.subject_id) ||
|
|
50
|
+
typeof record.launch_id !== "string" ||
|
|
51
|
+
!UUID.test(record.launch_id) ||
|
|
52
|
+
!Number.isSafeInteger(record.execution_generation) ||
|
|
53
|
+
(record.execution_generation as number) < 1 ||
|
|
54
|
+
(record.execution_generation as number) > RADAR_MAX_EXECUTION_GENERATION
|
|
55
|
+
) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
version: RADAR_LAUNCH_MARKER_VERSION,
|
|
60
|
+
subject_id: record.subject_id,
|
|
61
|
+
launch_id: record.launch_id,
|
|
62
|
+
execution_generation: record.execution_generation as number,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Appends a valid Radar-issued marker as a Pi custom session entry. */
|
|
67
|
+
export function appendRadarLaunchMarker(
|
|
68
|
+
appendEntry: AppendSessionEntry,
|
|
69
|
+
marker: RadarLaunchMarker,
|
|
70
|
+
): void {
|
|
71
|
+
const validated = decodeRadarLaunchMarker(marker);
|
|
72
|
+
if (!validated) throw new Error("invalid Radar launch identity marker");
|
|
73
|
+
appendEntry(RADAR_LAUNCH_MARKER_TYPE, validated);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Reads the newest marker on Pi's current branch. A malformed newest marker is
|
|
78
|
+
* returned as unresolved rather than silently falling back to an older launch.
|
|
79
|
+
*/
|
|
80
|
+
export function latestRadarLaunchMarker(
|
|
81
|
+
branch: readonly CustomSessionEntry[],
|
|
82
|
+
): { status: "absent" } | { status: "valid"; marker: RadarLaunchMarker } | { status: "invalid" } {
|
|
83
|
+
let latest: CustomSessionEntry | undefined;
|
|
84
|
+
for (const entry of branch) {
|
|
85
|
+
if (entry.type === "custom" && entry.customType === RADAR_LAUNCH_MARKER_TYPE) {
|
|
86
|
+
latest = entry;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!latest) return { status: "absent" };
|
|
90
|
+
const marker = decodeRadarLaunchMarker(latest.data);
|
|
91
|
+
return marker ? { status: "valid", marker } : { status: "invalid" };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Persist this daemon-injected process identity into the active Pi session branch.
|
|
96
|
+
* No marker is written when any variable is absent or malformed. Repeated session
|
|
97
|
+
* starts in the same generation do not append duplicate entries.
|
|
98
|
+
*/
|
|
99
|
+
export function radarLaunchMarkerFromEnvironment(
|
|
100
|
+
env: Record<string, string | undefined> = process.env,
|
|
101
|
+
): RadarLaunchMarker | undefined {
|
|
102
|
+
const rawGeneration = env[RADAR_MANAGED_GENERATION_ENV];
|
|
103
|
+
if (
|
|
104
|
+
env[RADAR_MANAGED_SUBJECT_ENV] === undefined ||
|
|
105
|
+
env[RADAR_MANAGED_LAUNCH_ENV] === undefined ||
|
|
106
|
+
rawGeneration === undefined ||
|
|
107
|
+
!/^[1-9][0-9]*$/.test(rawGeneration)
|
|
108
|
+
) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
return decodeRadarLaunchMarker({
|
|
112
|
+
version: RADAR_LAUNCH_MARKER_VERSION,
|
|
113
|
+
subject_id: env[RADAR_MANAGED_SUBJECT_ENV],
|
|
114
|
+
launch_id: env[RADAR_MANAGED_LAUNCH_ENV],
|
|
115
|
+
execution_generation: Number(rawGeneration),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function activeRadarLaunchMarker(
|
|
120
|
+
host: PiSessionMarkerHost,
|
|
121
|
+
): ReturnType<typeof latestRadarLaunchMarker> {
|
|
122
|
+
if (!host.sessionManager) return { status: "absent" };
|
|
123
|
+
return latestRadarLaunchMarker(host.sessionManager.getBranch());
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function persistRadarLaunchMarker(
|
|
127
|
+
host: PiSessionMarkerHost,
|
|
128
|
+
env: Record<string, string | undefined> = process.env,
|
|
129
|
+
): "absent" | "unsupported" | "unchanged" | "written" {
|
|
130
|
+
const marker = radarLaunchMarkerFromEnvironment(env);
|
|
131
|
+
if (!marker) return "absent";
|
|
132
|
+
if (!host.appendEntry || !host.sessionManager) return "unsupported";
|
|
133
|
+
const current = latestRadarLaunchMarker(host.sessionManager.getBranch());
|
|
134
|
+
if (current.status === "valid" && sameMarker(current.marker, marker)) return "unchanged";
|
|
135
|
+
appendRadarLaunchMarker(host.appendEntry, marker);
|
|
136
|
+
return "written";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function sameMarker(a: RadarLaunchMarker, b: RadarLaunchMarker): boolean {
|
|
140
|
+
return (
|
|
141
|
+
a.version === b.version &&
|
|
142
|
+
a.subject_id === b.subject_id &&
|
|
143
|
+
a.launch_id === b.launch_id &&
|
|
144
|
+
a.execution_generation === b.execution_generation
|
|
145
|
+
);
|
|
146
|
+
}
|
package/index.ts
CHANGED
|
@@ -4,3 +4,4 @@ export * as inspection from "./extension/radar-inspection.ts";
|
|
|
4
4
|
export * as launch from "./extension/radar-launch.ts";
|
|
5
5
|
export * as location from "./extension/radar-location.ts";
|
|
6
6
|
export * as publication from "./extension/radar-publication.ts";
|
|
7
|
+
export * as runtime from "./extension/radar-runtime.ts";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-radar",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Radar daemon client, publication, execution, location, launch and inspection surface for Pi extensions.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
"directory": "pi-radar"
|
|
14
14
|
},
|
|
15
15
|
"types": "./index.ts",
|
|
16
|
+
"pi": {
|
|
17
|
+
"extensions": ["./extension/baseline.ts"]
|
|
18
|
+
},
|
|
16
19
|
"exports": {
|
|
17
20
|
".": {
|
|
18
21
|
"types": "./index.ts",
|
|
@@ -30,6 +33,14 @@
|
|
|
30
33
|
"types": "./extension/radar-publication.ts",
|
|
31
34
|
"import": "./dist/extension/radar-publication.js"
|
|
32
35
|
},
|
|
36
|
+
"./runtime": {
|
|
37
|
+
"types": "./extension/radar-runtime.ts",
|
|
38
|
+
"import": "./dist/extension/radar-runtime.js"
|
|
39
|
+
},
|
|
40
|
+
"./baseline": {
|
|
41
|
+
"types": "./extension/baseline.ts",
|
|
42
|
+
"import": "./dist/extension/baseline.js"
|
|
43
|
+
},
|
|
33
44
|
"./launch": {
|
|
34
45
|
"types": "./extension/radar-launch.ts",
|
|
35
46
|
"import": "./dist/extension/radar-launch.js"
|
|
@@ -60,13 +71,16 @@
|
|
|
60
71
|
"extension/radar-inspection.ts",
|
|
61
72
|
"extension/radar-launch.ts",
|
|
62
73
|
"extension/radar-location.ts",
|
|
63
|
-
"extension/radar-publication.ts"
|
|
74
|
+
"extension/radar-publication.ts",
|
|
75
|
+
"extension/radar-session-marker.ts",
|
|
76
|
+
"extension/radar-runtime.ts",
|
|
77
|
+
"extension/baseline.ts"
|
|
64
78
|
],
|
|
65
79
|
"scripts": {
|
|
66
|
-
"build": "esbuild index.ts extension/generated-contracts.ts extension/radar-client.ts extension/radar-execution.ts extension/radar-inspection.ts extension/radar-launch.ts extension/radar-location.ts extension/radar-publication.ts --outbase=. --outdir=dist --bundle --platform=node --format=esm --target=node22",
|
|
80
|
+
"build": "esbuild index.ts extension/generated-contracts.ts extension/radar-client.ts extension/radar-execution.ts extension/radar-inspection.ts extension/radar-launch.ts extension/radar-location.ts extension/radar-publication.ts extension/radar-session-marker.ts extension/radar-runtime.ts extension/baseline.ts --outbase=. --outdir=dist --bundle --platform=node --format=esm --target=node22",
|
|
67
81
|
"generate:contracts": "node tools/generate-contracts.mjs",
|
|
68
82
|
"check:contracts": "node tools/generate-contracts.mjs --check",
|
|
69
|
-
"typecheck:contracts": "tsc --noEmit --strict --skipLibCheck --target ES2022 --module NodeNext --moduleResolution NodeNext --allowImportingTsExtensions --types node extension/generated-contracts.ts extension/generated-contracts.typecheck.ts",
|
|
83
|
+
"typecheck:contracts": "tsc --noEmit --strict --skipLibCheck --target ES2022 --module NodeNext --moduleResolution NodeNext --allowImportingTsExtensions --types node index.ts extension/baseline.ts extension/generated-contracts.ts extension/generated-contracts.typecheck.ts extension/radar-client.ts extension/radar-execution.ts extension/radar-publication.ts extension/radar-session-marker.ts extension/radar-session-marker.test.ts extension/radar-runtime.ts extension/radar-runtime.test.ts",
|
|
70
84
|
"prepack": "npm run build",
|
|
71
85
|
"test": "npm run check:contracts && npm run typecheck:contracts && node --experimental-strip-types --test extension/*.test.ts"
|
|
72
86
|
},
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"additionalProperties": false,
|
|
4
|
+
"description": "A non-secret Pi-session breadcrumb issued by Radar. This is correlation\nevidence only and cannot establish lifecycle authority.",
|
|
5
|
+
"properties": {
|
|
6
|
+
"execution_generation": {
|
|
7
|
+
"format": "uint32",
|
|
8
|
+
"maximum": 4294967295,
|
|
9
|
+
"minimum": 1,
|
|
10
|
+
"type": "integer"
|
|
11
|
+
},
|
|
12
|
+
"launch_id": {
|
|
13
|
+
"type": "string"
|
|
14
|
+
},
|
|
15
|
+
"subject_id": {
|
|
16
|
+
"type": "string"
|
|
17
|
+
},
|
|
18
|
+
"version": {
|
|
19
|
+
"format": "uint32",
|
|
20
|
+
"minimum": 0,
|
|
21
|
+
"type": "integer"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"required": [
|
|
25
|
+
"version",
|
|
26
|
+
"subject_id",
|
|
27
|
+
"launch_id",
|
|
28
|
+
"execution_generation"
|
|
29
|
+
],
|
|
30
|
+
"title": "launch-identity-marker-deserialize",
|
|
31
|
+
"type": "object"
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"additionalProperties": false,
|
|
4
|
+
"description": "A non-secret Pi-session breadcrumb issued by Radar. This is correlation\nevidence only and cannot establish lifecycle authority.",
|
|
5
|
+
"properties": {
|
|
6
|
+
"execution_generation": {
|
|
7
|
+
"format": "uint32",
|
|
8
|
+
"maximum": 4294967295,
|
|
9
|
+
"minimum": 1,
|
|
10
|
+
"type": "integer"
|
|
11
|
+
},
|
|
12
|
+
"launch_id": {
|
|
13
|
+
"type": "string"
|
|
14
|
+
},
|
|
15
|
+
"subject_id": {
|
|
16
|
+
"type": "string"
|
|
17
|
+
},
|
|
18
|
+
"version": {
|
|
19
|
+
"format": "uint32",
|
|
20
|
+
"minimum": 0,
|
|
21
|
+
"type": "integer"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"required": [
|
|
25
|
+
"version",
|
|
26
|
+
"subject_id",
|
|
27
|
+
"launch_id",
|
|
28
|
+
"execution_generation"
|
|
29
|
+
],
|
|
30
|
+
"title": "launch-identity-marker-serialize",
|
|
31
|
+
"type": "object"
|
|
32
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"additionalProperties": false,
|
|
4
|
+
"description": "Managed owner/parent metadata accepted with a spawn intent. Domain validation\nand parent-binding checks remain in the daemon handler.",
|
|
5
|
+
"properties": {
|
|
6
|
+
"label": {
|
|
7
|
+
"type": [
|
|
8
|
+
"string",
|
|
9
|
+
"null"
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
"owner": {
|
|
13
|
+
"type": [
|
|
14
|
+
"string",
|
|
15
|
+
"null"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"parent_subject_id": {
|
|
19
|
+
"type": [
|
|
20
|
+
"string",
|
|
21
|
+
"null"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"role": {
|
|
25
|
+
"type": [
|
|
26
|
+
"string",
|
|
27
|
+
"null"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"run": {
|
|
31
|
+
"type": [
|
|
32
|
+
"string",
|
|
33
|
+
"null"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"title": "managed-launch-metadata-deserialize",
|
|
38
|
+
"type": "object"
|
|
39
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"additionalProperties": false,
|
|
4
|
+
"description": "Managed owner/parent metadata accepted with a spawn intent. Domain validation\nand parent-binding checks remain in the daemon handler.",
|
|
5
|
+
"properties": {
|
|
6
|
+
"label": {
|
|
7
|
+
"type": [
|
|
8
|
+
"string",
|
|
9
|
+
"null"
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
"owner": {
|
|
13
|
+
"type": [
|
|
14
|
+
"string",
|
|
15
|
+
"null"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"parent_subject_id": {
|
|
19
|
+
"type": [
|
|
20
|
+
"string",
|
|
21
|
+
"null"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"role": {
|
|
25
|
+
"type": [
|
|
26
|
+
"string",
|
|
27
|
+
"null"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"run": {
|
|
31
|
+
"type": [
|
|
32
|
+
"string",
|
|
33
|
+
"null"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"title": "managed-launch-metadata-serialize",
|
|
38
|
+
"type": "object"
|
|
39
|
+
}
|