pi-radar 0.3.0 → 0.4.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 +4 -2
- package/dist/extension/baseline.js +227 -402
- package/dist/extension/radar-client.js +53 -13
- package/dist/extension/radar-communication.js +5 -5
- package/dist/extension/radar-execution.js +168 -383
- package/dist/extension/radar-launch.js +13 -13
- package/dist/extension/radar-publication.js +160 -299
- package/dist/index.js +239 -414
- package/extension/generated-contracts.ts +1249 -13
- package/extension/radar-client.ts +92 -29
- package/extension/radar-communication.ts +5 -5
- package/extension/radar-execution.ts +11 -58
- package/extension/radar-launch.ts +21 -21
- package/extension/radar-publication.ts +209 -412
- package/package.json +3 -4
- package/schema/public/v1/handoff-publish-params.deserialize.schema.json +2 -2
- package/schema/public/v1/handoff-publish-params.serialize.schema.json +2 -2
- package/schema/public/v1/handoff-receipt.deserialize.schema.json +4 -4
- package/schema/public/v1/handoff-receipt.serialize.schema.json +4 -4
- package/schema/public/v1/handoff.deserialize.schema.json +4 -4
- package/schema/public/v1/handoff.serialize.schema.json +4 -4
- package/schema/public/v1/observation-get-params.deserialize.schema.json +46 -0
- package/schema/public/v1/observation-get-params.serialize.schema.json +52 -0
- package/schema/public/v1/observation-get-response.deserialize.schema.json +2731 -0
- package/schema/public/v1/observation-get-response.serialize.schema.json +2862 -0
- package/schema/public/v1/observation-snapshot.deserialize.schema.json +2616 -0
- package/schema/public/v1/observation-snapshot.serialize.schema.json +2741 -0
- package/schema/public/v1/presence-answer.deserialize.schema.json +40 -0
- package/schema/public/v1/presence-answer.serialize.schema.json +40 -0
- package/schema/public/v1/presence-release.deserialize.schema.json +51 -0
- package/schema/public/v1/presence-release.serialize.schema.json +52 -0
- package/schema/public/v1/presence-report.deserialize.schema.json +149 -0
- package/schema/public/v1/presence-report.serialize.schema.json +157 -0
- package/extension/radar-session-marker.ts +0 -146
- package/schema/public/v1/launch-identity-marker.deserialize.schema.json +0 -32
- package/schema/public/v1/launch-identity-marker.serialize.schema.json +0 -32
|
@@ -4,7 +4,6 @@ import { createConnection } from "node:net";
|
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import type {
|
|
6
6
|
DaemonPingSerialize,
|
|
7
|
-
LaunchIdentityMarkerSerialize,
|
|
8
7
|
FactsSnapshotDeserialize,
|
|
9
8
|
HandoffInboxSerialize,
|
|
10
9
|
HandoffReceiptSerialize,
|
|
@@ -114,6 +113,10 @@ export type RadarLocation = {
|
|
|
114
113
|
pane?: string;
|
|
115
114
|
};
|
|
116
115
|
|
|
116
|
+
export type RadarPresenceReport = import("./generated-contracts.ts").PresenceReportDeserialize;
|
|
117
|
+
export type RadarPresenceAnswer = import("./generated-contracts.ts").PresenceAnswerSerialize;
|
|
118
|
+
export type RadarPresenceRelease = import("./generated-contracts.ts").PresenceReleaseDeserialize;
|
|
119
|
+
|
|
117
120
|
export type RadarProcessClaim = {
|
|
118
121
|
boot_id: string;
|
|
119
122
|
pid: number;
|
|
@@ -204,7 +207,6 @@ export type RadarWriterBinding = {
|
|
|
204
207
|
*/
|
|
205
208
|
export type RadarContextValue = {
|
|
206
209
|
session: string | null;
|
|
207
|
-
launch_identity?: LaunchIdentityMarkerSerialize;
|
|
208
210
|
};
|
|
209
211
|
|
|
210
212
|
/**
|
|
@@ -286,7 +288,7 @@ export type RadarEffectOutcome = "completed" | "refused" | "unknown";
|
|
|
286
288
|
/**
|
|
287
289
|
* One durable spawn edge as the daemon publishes it (`spawn.get`).
|
|
288
290
|
*
|
|
289
|
-
* `bound.
|
|
291
|
+
* `bound.registration_id` is the exact registry identity the child obtained with the
|
|
290
292
|
* launch's private token, which is the only thing that says *this* child belongs
|
|
291
293
|
* to *this* spawn: a label, a pane or a session UUID would each be a guess.
|
|
292
294
|
*/
|
|
@@ -296,7 +298,7 @@ export type RadarSpawnEdge = {
|
|
|
296
298
|
created: RadarEffectOutcome;
|
|
297
299
|
launched: RadarEffectOutcome;
|
|
298
300
|
location?: RadarLocation;
|
|
299
|
-
bound?: { source: string; incarnation: string;
|
|
301
|
+
bound?: { source: string; incarnation: string; registration_id?: string };
|
|
300
302
|
exit_observation?: RadarSpawnExitObservation;
|
|
301
303
|
};
|
|
302
304
|
|
|
@@ -307,7 +309,6 @@ export type RadarSpawnEdge = {
|
|
|
307
309
|
* no exit was observed.
|
|
308
310
|
*/
|
|
309
311
|
export type RadarSpawnExitObservation = SpawnExitObservationSerialize;
|
|
310
|
-
export type RadarLaunchIdentityMarker = LaunchIdentityMarkerSerialize;
|
|
311
312
|
|
|
312
313
|
/** Durable Radar-owned metadata accepted with a managed launch intent. */
|
|
313
314
|
export type RadarManagedLaunchMetadata = ManagedLaunchMetadataDeserialize;
|
|
@@ -502,6 +503,43 @@ export function isUuid(value: unknown): value is string {
|
|
|
502
503
|
return typeof value === "string" && UUID.test(value);
|
|
503
504
|
}
|
|
504
505
|
|
|
506
|
+
function decodePresenceAnswer(value: unknown): RadarResult<RadarPresenceAnswer> {
|
|
507
|
+
const registrationId = recordAt(value, "registration_id");
|
|
508
|
+
const claimId = recordAt(value, "claim_id");
|
|
509
|
+
const standing = recordAt(value, "standing");
|
|
510
|
+
const subjectId = recordAt(value, "subject_id");
|
|
511
|
+
if (
|
|
512
|
+
!isUuid(registrationId) ||
|
|
513
|
+
!isUuid(claimId) ||
|
|
514
|
+
!["pending", "granted", "duplicate", "released"].includes(String(standing)) ||
|
|
515
|
+
(subjectId !== undefined && !isUuid(subjectId)) ||
|
|
516
|
+
(standing === "granted" && !isUuid(subjectId))
|
|
517
|
+
)
|
|
518
|
+
return protocol("presence reply carries no valid claim standing");
|
|
519
|
+
return { ok: true, value: value as RadarPresenceAnswer };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* How Radar stands on a publisher's current session: adopted as a managed Pi
|
|
524
|
+
* session, rejected as a duplicate, still pending evidence/recovery, or not managed (yet).
|
|
525
|
+
*/
|
|
526
|
+
export type RadarManagement =
|
|
527
|
+
| { state: "managed"; subject_id: string }
|
|
528
|
+
| { state: "duplicate" }
|
|
529
|
+
| { state: "pending" }
|
|
530
|
+
| { state: "unmanaged" };
|
|
531
|
+
|
|
532
|
+
function managementAt(value: unknown): RadarManagement | undefined {
|
|
533
|
+
const field = recordAt(value, "management");
|
|
534
|
+
const state = stringAt(field, "state");
|
|
535
|
+
if (state === "managed") {
|
|
536
|
+
const subjectId = stringAt(field, "subject_id");
|
|
537
|
+
return subjectId === undefined ? undefined : { state, subject_id: subjectId };
|
|
538
|
+
}
|
|
539
|
+
if (state === "duplicate" || state === "pending" || state === "unmanaged") return { state };
|
|
540
|
+
return undefined;
|
|
541
|
+
}
|
|
542
|
+
|
|
505
543
|
function stringAt(value: unknown, field: string): string | undefined {
|
|
506
544
|
if (typeof value !== "object" || value === null) return undefined;
|
|
507
545
|
const found = (value as Record<string, unknown>)[field];
|
|
@@ -529,7 +567,7 @@ function handoffAt(value: unknown): HandoffSerialize | undefined {
|
|
|
529
567
|
? (value as Record<string, unknown>).correlation_id
|
|
530
568
|
: undefined;
|
|
531
569
|
if (
|
|
532
|
-
version !== 1 || !isUuid(id) || !isUuid(stringAt(sender, "
|
|
570
|
+
version !== 1 || !isUuid(id) || !isUuid(stringAt(sender, "registration_id")) ||
|
|
533
571
|
!isUuid(stringAt(sender, "session_id")) || !isUuid(stringAt(sender, "subject_id")) ||
|
|
534
572
|
!isUuid(stringAt(sender, "execution_id")) || !Number.isSafeInteger(senderGeneration) ||
|
|
535
573
|
(senderGeneration ?? 0) < 1 || !isUuid(target) || summary === undefined || content === undefined ||
|
|
@@ -548,7 +586,7 @@ function handoffReceiptAt(value: unknown): HandoffReceiptSerialize | undefined {
|
|
|
548
586
|
const generation = numberAt(recipient, "execution_generation");
|
|
549
587
|
const received = numberAt(value, "received_at_unix_ms");
|
|
550
588
|
if (
|
|
551
|
-
version !== 1 || !isUuid(handoffId) || !isUuid(stringAt(recipient, "
|
|
589
|
+
version !== 1 || !isUuid(handoffId) || !isUuid(stringAt(recipient, "registration_id")) ||
|
|
552
590
|
!isUuid(stringAt(recipient, "session_id")) || !isUuid(stringAt(recipient, "subject_id")) ||
|
|
553
591
|
!isUuid(stringAt(recipient, "execution_id")) || !Number.isSafeInteger(generation) ||
|
|
554
592
|
(generation ?? 0) < 1 || received === undefined || !Number.isSafeInteger(received) || received < 0
|
|
@@ -764,7 +802,7 @@ function locationAt(value: unknown): RadarLocation | undefined {
|
|
|
764
802
|
}
|
|
765
803
|
|
|
766
804
|
/**
|
|
767
|
-
* One durable edge, decoded strictly. The bound child's `
|
|
805
|
+
* One durable edge, decoded strictly. The bound child's `registration_id` is kept only
|
|
768
806
|
* when it is a canonical UUID: a legacy binding has none, and an unreadable one
|
|
769
807
|
* is not an identity to correlate with.
|
|
770
808
|
*/
|
|
@@ -784,7 +822,7 @@ function spawnEdge(value: unknown): RadarResult<RadarSpawnEdge> {
|
|
|
784
822
|
const bound = recordAt(value, "bound");
|
|
785
823
|
const source = stringAt(bound, "source");
|
|
786
824
|
const incarnation = stringAt(bound, "incarnation");
|
|
787
|
-
const agentId = stringAt(bound, "
|
|
825
|
+
const agentId = stringAt(bound, "registration_id");
|
|
788
826
|
const exit = exitAt(recordAt(value, "exit_observation"));
|
|
789
827
|
return {
|
|
790
828
|
ok: true,
|
|
@@ -799,7 +837,7 @@ function spawnEdge(value: unknown): RadarResult<RadarSpawnEdge> {
|
|
|
799
837
|
bound: {
|
|
800
838
|
source,
|
|
801
839
|
incarnation,
|
|
802
|
-
...(isUuid(agentId) ? {
|
|
840
|
+
...(isUuid(agentId) ? { registration_id: agentId } : {}),
|
|
803
841
|
},
|
|
804
842
|
}
|
|
805
843
|
: {}),
|
|
@@ -1001,15 +1039,15 @@ export type RadarClient = {
|
|
|
1001
1039
|
ping(): Promise<RadarResult<RadarHandshake>>;
|
|
1002
1040
|
register(
|
|
1003
1041
|
registration: RadarRegistration,
|
|
1004
|
-
): Promise<RadarResult<{
|
|
1042
|
+
): Promise<RadarResult<{ registration_id: string }>>;
|
|
1005
1043
|
acquire(request: {
|
|
1006
|
-
|
|
1044
|
+
registration_id: string;
|
|
1007
1045
|
channel: RadarChannel;
|
|
1008
1046
|
publisher: RadarPublisherIdentity;
|
|
1009
1047
|
replace?: { generation: number; handle: string };
|
|
1010
1048
|
}): Promise<RadarResult<RadarWriterBinding>>;
|
|
1011
1049
|
publish(request: {
|
|
1012
|
-
|
|
1050
|
+
registration_id: string;
|
|
1013
1051
|
channel: RadarChannel;
|
|
1014
1052
|
writer_handle: string;
|
|
1015
1053
|
sequence: number;
|
|
@@ -1017,8 +1055,10 @@ export type RadarClient = {
|
|
|
1017
1055
|
observed_at?: string;
|
|
1018
1056
|
snapshot: RadarChannelSnapshot;
|
|
1019
1057
|
}): Promise<RadarResult<{ sequence: number }>>;
|
|
1058
|
+
presence(request: RadarPresenceReport): Promise<RadarResult<RadarPresenceAnswer>>;
|
|
1059
|
+
releasePresence(request: RadarPresenceRelease): Promise<RadarResult<RadarPresenceAnswer>>;
|
|
1020
1060
|
context(request: {
|
|
1021
|
-
|
|
1061
|
+
registration_id: string;
|
|
1022
1062
|
publisher: RadarPublisherIdentity;
|
|
1023
1063
|
writer_handle?: string;
|
|
1024
1064
|
replace?: { generation: number; handle: string };
|
|
@@ -1026,9 +1066,9 @@ export type RadarClient = {
|
|
|
1026
1066
|
lease_ms?: number;
|
|
1027
1067
|
observed_at?: string;
|
|
1028
1068
|
context: RadarContextValue;
|
|
1029
|
-
}): Promise<RadarResult<{ writer: RadarWriterBinding; warning?: string }>>;
|
|
1069
|
+
}): Promise<RadarResult<{ writer: RadarWriterBinding; warning?: string; management?: RadarManagement }>>;
|
|
1030
1070
|
retire(request: {
|
|
1031
|
-
|
|
1071
|
+
registration_id: string;
|
|
1032
1072
|
channel: RadarChannel;
|
|
1033
1073
|
writer_handle: string;
|
|
1034
1074
|
}): Promise<RadarResult<void>>;
|
|
@@ -1061,15 +1101,15 @@ export type RadarClient = {
|
|
|
1061
1101
|
}): Promise<RadarResult<RadarSpawnEdge>>;
|
|
1062
1102
|
spawnGet(request_id: string): Promise<RadarResult<RadarSpawnTopology>>;
|
|
1063
1103
|
/** The registered child's process verdict, read from its immutable claim. */
|
|
1064
|
-
agentGet(
|
|
1104
|
+
agentGet(registration_id: string): Promise<RadarResult<RadarAgentProcess>>;
|
|
1065
1105
|
observe(): Promise<RadarResult<RadarInventory>>;
|
|
1066
1106
|
/** Read the durable event journal after `after`, waiting at most `wait_ms`. */
|
|
1067
1107
|
communicationPublish(request: HandoffPublishParamsDeserialize & {
|
|
1068
1108
|
request_id: string;
|
|
1069
1109
|
}): Promise<RadarResult<{ handoff: HandoffSerialize; created: boolean }>>;
|
|
1070
|
-
communicationInbox(request: {
|
|
1071
|
-
communicationGet(request: {
|
|
1072
|
-
communicationAck(request: {
|
|
1110
|
+
communicationInbox(request: { registration_id: string }): Promise<RadarResult<HandoffInboxSerialize>>;
|
|
1111
|
+
communicationGet(request: { registration_id: string; handoff_id: string }): Promise<RadarResult<{ handoff: HandoffSerialize; receipt: HandoffReceiptSerialize | null }>>;
|
|
1112
|
+
communicationAck(request: { registration_id: string; handoff_id: string }): Promise<RadarResult<{ receipt: HandoffReceiptSerialize; created: boolean }>>;
|
|
1073
1113
|
events(request: {
|
|
1074
1114
|
after: number;
|
|
1075
1115
|
epoch?: string;
|
|
@@ -1468,6 +1508,24 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
|
|
|
1468
1508
|
return {
|
|
1469
1509
|
socketPath,
|
|
1470
1510
|
ping,
|
|
1511
|
+
async presence(request) {
|
|
1512
|
+
return withRegistry(async () => {
|
|
1513
|
+
const answer = await call("agent.presence", { ...request });
|
|
1514
|
+
if (!answer.ok) return answer;
|
|
1515
|
+
return decodePresenceAnswer(answer.value);
|
|
1516
|
+
});
|
|
1517
|
+
},
|
|
1518
|
+
async releasePresence(request) {
|
|
1519
|
+
return withRegistry(async () => {
|
|
1520
|
+
const answer = await call("agent.release", { ...request });
|
|
1521
|
+
if (!answer.ok) return answer;
|
|
1522
|
+
const decoded = decodePresenceAnswer(answer.value);
|
|
1523
|
+
if (!decoded.ok) return decoded;
|
|
1524
|
+
return decoded.value.standing === "released"
|
|
1525
|
+
? decoded
|
|
1526
|
+
: protocol("release reply did not confirm the claim release");
|
|
1527
|
+
});
|
|
1528
|
+
},
|
|
1471
1529
|
async register(registration) {
|
|
1472
1530
|
return withRegistry(async () => {
|
|
1473
1531
|
const answer = await call("agent.register", {
|
|
@@ -1487,15 +1545,15 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
|
|
|
1487
1545
|
});
|
|
1488
1546
|
if (answer.ok === false) return answer;
|
|
1489
1547
|
const record = (answer.value as { registration?: unknown }).registration;
|
|
1490
|
-
const agentId = stringAt(record, "
|
|
1491
|
-
if (!isUuid(agentId)) return protocol("registration reply carries no
|
|
1492
|
-
return { ok: true, value: {
|
|
1548
|
+
const agentId = stringAt(record, "registration_id");
|
|
1549
|
+
if (!isUuid(agentId)) return protocol("registration reply carries no registration_id");
|
|
1550
|
+
return { ok: true, value: { registration_id: agentId } };
|
|
1493
1551
|
});
|
|
1494
1552
|
},
|
|
1495
1553
|
async acquire(request) {
|
|
1496
1554
|
return withRegistryChannel(request.channel, async () => {
|
|
1497
1555
|
const answer = await call("agent.acquire", {
|
|
1498
|
-
|
|
1556
|
+
registration_id: request.registration_id,
|
|
1499
1557
|
channel: request.channel,
|
|
1500
1558
|
publisher: request.publisher,
|
|
1501
1559
|
...(request.replace !== undefined ? { replace: request.replace } : {}),
|
|
@@ -1524,7 +1582,7 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
|
|
|
1524
1582
|
}
|
|
1525
1583
|
return withRegistryChannel(request.channel, async () => {
|
|
1526
1584
|
const answer = await call("agent.publish", {
|
|
1527
|
-
|
|
1585
|
+
registration_id: request.registration_id,
|
|
1528
1586
|
channel: request.channel,
|
|
1529
1587
|
writer_handle: request.writer_handle,
|
|
1530
1588
|
sequence: request.sequence,
|
|
@@ -1543,7 +1601,7 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
|
|
|
1543
1601
|
async context(request) {
|
|
1544
1602
|
return withRegistry(async () => {
|
|
1545
1603
|
const answer = await call("agent.context", {
|
|
1546
|
-
|
|
1604
|
+
registration_id: request.registration_id,
|
|
1547
1605
|
publisher: request.publisher,
|
|
1548
1606
|
...(request.writer_handle !== undefined
|
|
1549
1607
|
? { writer_handle: request.writer_handle }
|
|
@@ -1563,9 +1621,14 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
|
|
|
1563
1621
|
if (writer === undefined)
|
|
1564
1622
|
return protocol("context reply carries no writer binding");
|
|
1565
1623
|
const warning = stringAt(answer.value, "warning");
|
|
1624
|
+
const management = managementAt(answer.value);
|
|
1566
1625
|
return {
|
|
1567
1626
|
ok: true,
|
|
1568
|
-
value: {
|
|
1627
|
+
value: {
|
|
1628
|
+
writer,
|
|
1629
|
+
...(warning !== undefined ? { warning } : {}),
|
|
1630
|
+
...(management !== undefined ? { management } : {}),
|
|
1631
|
+
},
|
|
1569
1632
|
};
|
|
1570
1633
|
});
|
|
1571
1634
|
},
|
|
@@ -1658,7 +1721,7 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
|
|
|
1658
1721
|
message: "agent.get requires a canonical agent UUID",
|
|
1659
1722
|
};
|
|
1660
1723
|
return withRegistry(async () => {
|
|
1661
|
-
const answer = await call("agent.get", {
|
|
1724
|
+
const answer = await call("agent.get", { registration_id: agentId });
|
|
1662
1725
|
if (answer.ok === false) return answer;
|
|
1663
1726
|
return agentProcessAt(recordAt(answer.value, "agent"));
|
|
1664
1727
|
});
|
|
@@ -1724,7 +1787,7 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
|
|
|
1724
1787
|
async communicationPublish(request) {
|
|
1725
1788
|
return withContract(RADAR_COMMUNICATION_CONTRACT, async () => {
|
|
1726
1789
|
const answer = await call("communication.publish", {
|
|
1727
|
-
|
|
1790
|
+
sender_registration_id: request.sender_registration_id,
|
|
1728
1791
|
recipient_subject_id: request.recipient_subject_id,
|
|
1729
1792
|
...(request.correlation_id !== undefined ? { correlation_id: request.correlation_id } : {}),
|
|
1730
1793
|
summary: request.summary,
|
|
@@ -31,7 +31,7 @@ export function createRadarHandoffPort(
|
|
|
31
31
|
const subject = await publication.subject();
|
|
32
32
|
return subject === undefined
|
|
33
33
|
? { ok: false, code: "absent", message: "this process has no current Radar registration" }
|
|
34
|
-
: { ok: true, value: subject.
|
|
34
|
+
: { ok: true, value: subject.registration_id };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
return {
|
|
@@ -43,24 +43,24 @@ export function createRadarHandoffPort(
|
|
|
43
43
|
return { ok: false, code: "absent", message: "this managed execution has no Radar parent subject" };
|
|
44
44
|
return client.communicationPublish({
|
|
45
45
|
...request,
|
|
46
|
-
|
|
46
|
+
sender_registration_id: identity.value,
|
|
47
47
|
recipient_subject_id: parent,
|
|
48
48
|
});
|
|
49
49
|
},
|
|
50
50
|
async inbox() {
|
|
51
51
|
const identity = await agentId();
|
|
52
52
|
if (identity.ok === false) return identity;
|
|
53
|
-
return client.communicationInbox({
|
|
53
|
+
return client.communicationInbox({ registration_id: identity.value });
|
|
54
54
|
},
|
|
55
55
|
async get(handoff_id) {
|
|
56
56
|
const identity = await agentId();
|
|
57
57
|
if (identity.ok === false) return identity;
|
|
58
|
-
return client.communicationGet({
|
|
58
|
+
return client.communicationGet({ registration_id: identity.value, handoff_id });
|
|
59
59
|
},
|
|
60
60
|
async ack(handoff_id) {
|
|
61
61
|
const identity = await agentId();
|
|
62
62
|
if (identity.ok === false) return identity;
|
|
63
|
-
return client.communicationAck({
|
|
63
|
+
return client.communicationAck({ registration_id: identity.value, handoff_id });
|
|
64
64
|
},
|
|
65
65
|
};
|
|
66
66
|
}
|
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
isUuid,
|
|
4
4
|
type RadarScheduler,
|
|
5
5
|
type RadarSnapshot,
|
|
6
|
-
type RadarLaunchIdentityMarker,
|
|
7
6
|
} from "./radar-client.ts";
|
|
8
7
|
import type { RadarLocationResolution } from "./radar-location.ts";
|
|
9
8
|
import type { RadarFactsSnapshot } from "./radar-client.ts";
|
|
@@ -20,11 +19,6 @@ import {
|
|
|
20
19
|
type RadarPublication,
|
|
21
20
|
type RadarChannelWriter,
|
|
22
21
|
} from "./radar-publication.ts";
|
|
23
|
-
import {
|
|
24
|
-
activeRadarLaunchMarker,
|
|
25
|
-
persistRadarLaunchMarker,
|
|
26
|
-
radarLaunchMarkerFromEnvironment,
|
|
27
|
-
} from "./radar-session-marker.ts";
|
|
28
22
|
|
|
29
23
|
/**
|
|
30
24
|
* The minimal Pi surface this adapter uses. It is declared structurally so the
|
|
@@ -32,7 +26,6 @@ import {
|
|
|
32
26
|
* be lifted into its own extension without touching this module.
|
|
33
27
|
*/
|
|
34
28
|
export type RadarExecutionEventApi = {
|
|
35
|
-
appendEntry?: (customType: string, data?: unknown) => void;
|
|
36
29
|
on(name: string, handler: (event: any, context: any) => unknown): unknown;
|
|
37
30
|
events?: {
|
|
38
31
|
on(name: string, handler: (event: any) => unknown): unknown;
|
|
@@ -70,10 +63,8 @@ export type RadarExecutionOptions = {
|
|
|
70
63
|
* where no such event exists.
|
|
71
64
|
*/
|
|
72
65
|
questionnaireEvent?: string;
|
|
73
|
-
/** Best-effort diagnostic for
|
|
66
|
+
/** Best-effort diagnostic for context publication failures. */
|
|
74
67
|
diagnostic?: (message: string) => void;
|
|
75
|
-
/** Injectable only for tests; production reads the daemon-issued process env. */
|
|
76
|
-
markerEnvironment?: Record<string, string | undefined>;
|
|
77
68
|
};
|
|
78
69
|
|
|
79
70
|
export type RadarExecutionAdapter = {
|
|
@@ -138,10 +129,13 @@ function unavailablePublication(): RadarPublication {
|
|
|
138
129
|
return {
|
|
139
130
|
execution: writer,
|
|
140
131
|
facts: { ...writer, update: () => undefined },
|
|
141
|
-
|
|
142
|
-
|
|
132
|
+
presence: {
|
|
133
|
+
report: () => undefined,
|
|
143
134
|
stop: () => undefined,
|
|
144
135
|
reopen: () => undefined,
|
|
136
|
+
management: () => undefined,
|
|
137
|
+
ensure: async () => undefined,
|
|
138
|
+
release: async () => undefined,
|
|
145
139
|
diagnostic: () => "Radar private storage is unavailable",
|
|
146
140
|
},
|
|
147
141
|
assignment: () => writer,
|
|
@@ -269,14 +263,11 @@ export function createRadarExecutionAdapter(
|
|
|
269
263
|
* canonical UUID is left unreported: absent context is unknown, and publishing
|
|
270
264
|
* the explicit null would claim there is no session at all.
|
|
271
265
|
*/
|
|
272
|
-
function reportSession(
|
|
273
|
-
context: any,
|
|
274
|
-
launchIdentity?: RadarLaunchIdentityMarker,
|
|
275
|
-
): void {
|
|
266
|
+
function reportSession(context: any): void {
|
|
276
267
|
const session = context?.sessionManager?.getSessionId?.();
|
|
277
268
|
if (!isUuid(session)) return;
|
|
278
269
|
try {
|
|
279
|
-
publication.
|
|
270
|
+
publication.presence.report(session);
|
|
280
271
|
} catch {
|
|
281
272
|
// Optional publication must not interfere with Pi's lifecycle handler.
|
|
282
273
|
}
|
|
@@ -360,49 +351,10 @@ export function createRadarExecutionAdapter(
|
|
|
360
351
|
);
|
|
361
352
|
|
|
362
353
|
on("session_start", (_event, context) => {
|
|
363
|
-
const sessionHost = {
|
|
364
|
-
appendEntry: pi.appendEntry
|
|
365
|
-
? (customType: string, data?: unknown) => pi.appendEntry?.(customType, data)
|
|
366
|
-
: undefined,
|
|
367
|
-
sessionManager: context?.sessionManager,
|
|
368
|
-
};
|
|
369
|
-
let launchIdentity: RadarLaunchIdentityMarker | undefined;
|
|
370
|
-
try {
|
|
371
|
-
const result = persistRadarLaunchMarker(
|
|
372
|
-
sessionHost,
|
|
373
|
-
options.markerEnvironment,
|
|
374
|
-
);
|
|
375
|
-
if (result === "written" || result === "unchanged") {
|
|
376
|
-
const expected = radarLaunchMarkerFromEnvironment(options.markerEnvironment);
|
|
377
|
-
const activeMarker = activeRadarLaunchMarker(sessionHost);
|
|
378
|
-
if (
|
|
379
|
-
expected &&
|
|
380
|
-
activeMarker.status === "valid" &&
|
|
381
|
-
JSON.stringify(activeMarker.marker) === JSON.stringify(expected)
|
|
382
|
-
) {
|
|
383
|
-
launchIdentity = activeMarker.marker;
|
|
384
|
-
} else {
|
|
385
|
-
options.diagnostic?.("Radar launch marker was not visible on the active Pi session branch");
|
|
386
|
-
}
|
|
387
|
-
} else if (result === "unsupported") {
|
|
388
|
-
options.diagnostic?.("Pi host does not support Radar launch identity session entries");
|
|
389
|
-
} else if (
|
|
390
|
-
options.markerEnvironment &&
|
|
391
|
-
[
|
|
392
|
-
"PI_RADAR_MANAGED_SUBJECT_ID",
|
|
393
|
-
"PI_RADAR_MANAGED_LAUNCH_ID",
|
|
394
|
-
"PI_RADAR_MANAGED_EXECUTION_GENERATION",
|
|
395
|
-
].some((key) => options.markerEnvironment?.[key] !== undefined)
|
|
396
|
-
) {
|
|
397
|
-
options.diagnostic?.("incomplete or invalid Radar launch identity environment");
|
|
398
|
-
}
|
|
399
|
-
} catch (error) {
|
|
400
|
-
options.diagnostic?.(`could not persist Radar launch marker: ${String(error)}`);
|
|
401
|
-
}
|
|
402
354
|
rootSession = true;
|
|
403
355
|
// The session comes first: re-arming the publication must renew the session
|
|
404
356
|
// this process is in now, never the one a previous session left behind.
|
|
405
|
-
reportSession(context
|
|
357
|
+
reportSession(context);
|
|
406
358
|
// Facts follow the root flag, so they are published only after it is set.
|
|
407
359
|
publishFacts(context);
|
|
408
360
|
// A process can replace or resume its session without a new process, so the
|
|
@@ -450,12 +402,13 @@ export function createRadarExecutionAdapter(
|
|
|
450
402
|
emit();
|
|
451
403
|
});
|
|
452
404
|
|
|
453
|
-
on("session_shutdown", () => {
|
|
405
|
+
on("session_shutdown", async () => {
|
|
454
406
|
// A session can end or be replaced without ending this process, and Pi keeps
|
|
455
407
|
// the same extension instance across that: the listeners stay registered so
|
|
456
408
|
// the next session_start re-arms the same process publication. Only the
|
|
457
409
|
// timers and this session's facts are released.
|
|
458
410
|
suspend();
|
|
411
|
+
await publication.presence.release();
|
|
459
412
|
});
|
|
460
413
|
|
|
461
414
|
function suspend(): void {
|
|
@@ -46,7 +46,7 @@ export type LaunchMailbox<State extends LaunchState> = {
|
|
|
46
46
|
* the child's exact command into it, and still reports that pane in its own
|
|
47
47
|
* inventory;
|
|
48
48
|
* 2. a child registered with the launch's private token, so the edge names the
|
|
49
|
-
* exact `
|
|
49
|
+
* exact `registration_id` that child obtained — never a label, pane or session;
|
|
50
50
|
* 3. that child wrote a fresh startup acknowledgment for this launch generation
|
|
51
51
|
* after Herdsman startup finished, naming the pane the daemon created as its
|
|
52
52
|
* own location.
|
|
@@ -66,7 +66,7 @@ export type LaunchMailbox<State extends LaunchState> = {
|
|
|
66
66
|
|
|
67
67
|
/** The exact managed parent this launch may use, and nothing inferred about it. */
|
|
68
68
|
export type RadarLaunchParent = {
|
|
69
|
-
|
|
69
|
+
registration_id: string;
|
|
70
70
|
/** Stable subject from this process's current Radar registration. */
|
|
71
71
|
subject_id: string;
|
|
72
72
|
location?: RadarLocation;
|
|
@@ -109,12 +109,12 @@ export type RadarWorkerLaunch = {
|
|
|
109
109
|
* because a binding that was not persisted is one no later reader can hold the
|
|
110
110
|
* child by.
|
|
111
111
|
*/
|
|
112
|
-
onBound?: (bound: {
|
|
112
|
+
onBound?: (bound: { registration_id: string; pane_id: string }) => void;
|
|
113
113
|
};
|
|
114
114
|
|
|
115
115
|
export type RadarWorkerLaunched<State = unknown> = {
|
|
116
116
|
request_id: string;
|
|
117
|
-
|
|
117
|
+
registration_id: string;
|
|
118
118
|
pane_id: string;
|
|
119
119
|
/**
|
|
120
120
|
* The process the daemon bound this launch's edge to, taken from that same
|
|
@@ -143,7 +143,7 @@ export type RadarLaunchOutcome<State = unknown> =
|
|
|
143
143
|
*/
|
|
144
144
|
export type RadarChildEvidence = {
|
|
145
145
|
request_id: string;
|
|
146
|
-
|
|
146
|
+
registration_id: string;
|
|
147
147
|
source: string;
|
|
148
148
|
incarnation: string;
|
|
149
149
|
pane_id: string;
|
|
@@ -192,12 +192,12 @@ export async function observeRadarChild(
|
|
|
192
192
|
provenance: {
|
|
193
193
|
request_id: string;
|
|
194
194
|
pane_id: string | undefined;
|
|
195
|
-
|
|
195
|
+
registration_id: string | undefined;
|
|
196
196
|
/** The session this launch's mailbox records for the child. */
|
|
197
197
|
session_id: string | undefined;
|
|
198
198
|
},
|
|
199
199
|
): Promise<RadarChildObservation> {
|
|
200
|
-
if (provenance.
|
|
200
|
+
if (provenance.registration_id === undefined)
|
|
201
201
|
return unknownChild("not bound to a child by this launch");
|
|
202
202
|
// A launch that recorded no pane has nothing to compare the daemon's read
|
|
203
203
|
// against: the child's own claim about where it runs is not where the launch
|
|
@@ -211,11 +211,11 @@ export async function observeRadarChild(
|
|
|
211
211
|
);
|
|
212
212
|
const topology = read.value;
|
|
213
213
|
const edge = topology.edge;
|
|
214
|
-
if (edge.bound === undefined || edge.bound.
|
|
214
|
+
if (edge.bound === undefined || edge.bound.registration_id === undefined)
|
|
215
215
|
return unknownChild("not bound to a child by the daemon");
|
|
216
|
-
if (edge.bound.
|
|
216
|
+
if (edge.bound.registration_id !== provenance.registration_id)
|
|
217
217
|
return unknownChild(
|
|
218
|
-
`bound to another subject (${edge.bound.
|
|
218
|
+
`bound to another subject (${edge.bound.registration_id}), not to ${provenance.registration_id}`,
|
|
219
219
|
);
|
|
220
220
|
if (edge.location?.pane === undefined)
|
|
221
221
|
return unknownChild("recorded without a pane");
|
|
@@ -241,7 +241,7 @@ export async function observeRadarChild(
|
|
|
241
241
|
// The edge says where the child was bound, not that its process still runs.
|
|
242
242
|
// The registry's verdict on the bound subject, and the location it registered
|
|
243
243
|
// with, are what prove the child live.
|
|
244
|
-
const reply = await client.agentGet(edge.bound.
|
|
244
|
+
const reply = await client.agentGet(edge.bound.registration_id);
|
|
245
245
|
if (reply.ok === false)
|
|
246
246
|
return unknownChild(
|
|
247
247
|
`process verdict not readable (${reply.code}: ${reply.message})`,
|
|
@@ -271,7 +271,7 @@ export async function observeRadarChild(
|
|
|
271
271
|
kind: "live",
|
|
272
272
|
evidence: {
|
|
273
273
|
request_id: provenance.request_id,
|
|
274
|
-
|
|
274
|
+
registration_id: edge.bound.registration_id,
|
|
275
275
|
source: edge.bound.source,
|
|
276
276
|
incarnation: edge.bound.incarnation,
|
|
277
277
|
pane_id: edge.location.pane,
|
|
@@ -393,7 +393,7 @@ export async function launchRadarWorker<State extends LaunchState>(
|
|
|
393
393
|
const deadline = Date.now() + options.timeoutMs;
|
|
394
394
|
const remaining = (): number => Math.max(0, deadline - Date.now());
|
|
395
395
|
const pane = launch.parent.location?.pane;
|
|
396
|
-
if (!isUuid(launch.parent.
|
|
396
|
+
if (!isUuid(launch.parent.registration_id))
|
|
397
397
|
return refusedLaunch(
|
|
398
398
|
"the launch has no exact parent registration to spawn under",
|
|
399
399
|
);
|
|
@@ -411,7 +411,7 @@ export async function launchRadarWorker<State extends LaunchState>(
|
|
|
411
411
|
|
|
412
412
|
const spawned = await client.spawn({
|
|
413
413
|
request_id: launch.request_id,
|
|
414
|
-
parent: launch.parent.
|
|
414
|
+
parent: launch.parent.registration_id,
|
|
415
415
|
parent_session_id: launch.parent_session_id,
|
|
416
416
|
executable: launch.executable,
|
|
417
417
|
managed: { parent_subject_id: launch.parent.subject_id },
|
|
@@ -437,13 +437,13 @@ export async function launchRadarWorker<State extends LaunchState>(
|
|
|
437
437
|
...(signal !== undefined ? { signal } : {}),
|
|
438
438
|
});
|
|
439
439
|
if (bound.kind !== "bound") return bound;
|
|
440
|
-
const agentId = bound.
|
|
440
|
+
const agentId = bound.registration_id;
|
|
441
441
|
// The binding is recorded before the readiness wait, not after it: the child
|
|
442
442
|
// may never acknowledge, and the subject and pane the daemon named are still
|
|
443
443
|
// what a reload has to prove this child by.
|
|
444
444
|
if (launch.onBound !== undefined) {
|
|
445
445
|
try {
|
|
446
|
-
launch.onBound({
|
|
446
|
+
launch.onBound({ registration_id: agentId, pane_id: bound.pane_id });
|
|
447
447
|
} catch (error) {
|
|
448
448
|
return unresolvedLaunch(
|
|
449
449
|
launch.request_id,
|
|
@@ -475,7 +475,7 @@ export async function launchRadarWorker<State extends LaunchState>(
|
|
|
475
475
|
kind: "ready",
|
|
476
476
|
value: {
|
|
477
477
|
request_id: launch.request_id,
|
|
478
|
-
|
|
478
|
+
registration_id: agentId,
|
|
479
479
|
pane_id: bound.pane_id,
|
|
480
480
|
bound_process: {
|
|
481
481
|
source: bound.source,
|
|
@@ -498,7 +498,7 @@ export async function launchRadarWorker<State extends LaunchState>(
|
|
|
498
498
|
type BoundChild =
|
|
499
499
|
| {
|
|
500
500
|
kind: "bound";
|
|
501
|
-
|
|
501
|
+
registration_id: string;
|
|
502
502
|
pane_id: string;
|
|
503
503
|
source: string;
|
|
504
504
|
incarnation: string;
|
|
@@ -509,7 +509,7 @@ type BoundChild =
|
|
|
509
509
|
* Poll the edge this request id authored until a child presents its token.
|
|
510
510
|
*
|
|
511
511
|
* The daemon binds the edge once, to the exact registration the token produced,
|
|
512
|
-
* so `bound.
|
|
512
|
+
* so `bound.registration_id` is the child identity. A refusal or an unknown launch
|
|
513
513
|
* effect ends the wait at once: neither becomes a child by waiting longer. An
|
|
514
514
|
* edge whose recorded pane the daemon no longer reports ends it too: waiting
|
|
515
515
|
* cannot create the pane a launch claims, and a location read afterwards would
|
|
@@ -559,7 +559,7 @@ async function waitForChild(
|
|
|
559
559
|
requestId,
|
|
560
560
|
"the daemon observed the launched child's process exit",
|
|
561
561
|
);
|
|
562
|
-
const agentId = edge.bound?.
|
|
562
|
+
const agentId = edge.bound?.registration_id;
|
|
563
563
|
if (agentId !== undefined && isUuid(agentId)) {
|
|
564
564
|
// A child the daemon bound without the process it registered as is a
|
|
565
565
|
// child no assignment can be addressed to, and an unaddressed Radar
|
|
@@ -579,7 +579,7 @@ async function waitForChild(
|
|
|
579
579
|
);
|
|
580
580
|
return {
|
|
581
581
|
kind: "bound",
|
|
582
|
-
|
|
582
|
+
registration_id: agentId,
|
|
583
583
|
pane_id: pane,
|
|
584
584
|
source,
|
|
585
585
|
incarnation,
|