pi-radar 0.2.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 +10 -4
- package/dist/extension/baseline.js +333 -394
- package/dist/extension/radar-client.js +126 -11
- package/dist/extension/radar-communication.js +39 -0
- package/dist/extension/radar-execution.js +169 -382
- package/dist/extension/radar-launch.js +18 -13
- package/dist/extension/radar-publication.js +161 -298
- package/dist/extension/radar-runtime.js +1 -1
- package/dist/index.js +356 -407
- package/extension/baseline.ts +2 -0
- package/extension/generated-contracts.ts +1310 -10
- package/extension/radar-client.ts +191 -23
- package/extension/radar-communication.ts +66 -0
- package/extension/radar-execution.ts +11 -58
- package/extension/radar-launch.ts +29 -22
- package/extension/radar-publication.ts +211 -410
- package/extension/radar-runtime.ts +4 -1
- package/index.ts +1 -0
- package/package.json +9 -5
- package/schema/public/v1/handoff-inbox.deserialize.schema.json +18 -0
- package/schema/public/v1/handoff-inbox.serialize.schema.json +18 -0
- package/schema/public/v1/handoff-publish-params.deserialize.schema.json +33 -0
- package/schema/public/v1/handoff-publish-params.serialize.schema.json +33 -0
- package/schema/public/v1/handoff-receipt.deserialize.schema.json +66 -0
- package/schema/public/v1/handoff-receipt.serialize.schema.json +66 -0
- package/schema/public/v1/handoff.deserialize.schema.json +84 -0
- package/schema/public/v1/handoff.serialize.schema.json +84 -0
- 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,8 +4,11 @@ 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,
|
|
8
|
+
HandoffInboxSerialize,
|
|
9
|
+
HandoffReceiptSerialize,
|
|
10
|
+
HandoffPublishParamsDeserialize,
|
|
11
|
+
HandoffSerialize,
|
|
9
12
|
ManagedLaunchMetadataDeserialize,
|
|
10
13
|
SpawnExitObservationSerialize,
|
|
11
14
|
} from "./generated-contracts.ts";
|
|
@@ -36,6 +39,7 @@ export const RADAR_SPAWN_CONTRACT = "spawn.v1";
|
|
|
36
39
|
export const RADAR_SPAWN_CLOSE_CONTRACT = "spawn.close.v1";
|
|
37
40
|
export const RADAR_SPAWN_LAUNCH_CONTEXT_CONTRACT = "spawn.launch_context.v1";
|
|
38
41
|
export const RADAR_EVENTS_CONTRACT = "events.v1";
|
|
42
|
+
export const RADAR_COMMUNICATION_CONTRACT = "communication.handoff.v1";
|
|
39
43
|
/** The default audit identity; callers can supply their own requester. */
|
|
40
44
|
export const RADAR_REQUESTER = "pi-radar";
|
|
41
45
|
/** The daemon's own line bound; nothing longer is sent or accepted. */
|
|
@@ -109,6 +113,10 @@ export type RadarLocation = {
|
|
|
109
113
|
pane?: string;
|
|
110
114
|
};
|
|
111
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
|
+
|
|
112
120
|
export type RadarProcessClaim = {
|
|
113
121
|
boot_id: string;
|
|
114
122
|
pid: number;
|
|
@@ -162,6 +170,9 @@ export type RadarBackgroundTask = NonNullable<
|
|
|
162
170
|
FactsSnapshotDeserialize["background_tasks"]
|
|
163
171
|
>[number];
|
|
164
172
|
export type RadarFactsSnapshot = FactsSnapshotDeserialize;
|
|
173
|
+
export type RadarHandoff = HandoffSerialize;
|
|
174
|
+
export type RadarHandoffInbox = HandoffInboxSerialize;
|
|
175
|
+
export type RadarHandoffReceipt = HandoffReceiptSerialize;
|
|
165
176
|
|
|
166
177
|
/**
|
|
167
178
|
* The assignment channel's interim fields. `request` is the owner's active
|
|
@@ -196,7 +207,6 @@ export type RadarWriterBinding = {
|
|
|
196
207
|
*/
|
|
197
208
|
export type RadarContextValue = {
|
|
198
209
|
session: string | null;
|
|
199
|
-
launch_identity?: LaunchIdentityMarkerSerialize;
|
|
200
210
|
};
|
|
201
211
|
|
|
202
212
|
/**
|
|
@@ -278,7 +288,7 @@ export type RadarEffectOutcome = "completed" | "refused" | "unknown";
|
|
|
278
288
|
/**
|
|
279
289
|
* One durable spawn edge as the daemon publishes it (`spawn.get`).
|
|
280
290
|
*
|
|
281
|
-
* `bound.
|
|
291
|
+
* `bound.registration_id` is the exact registry identity the child obtained with the
|
|
282
292
|
* launch's private token, which is the only thing that says *this* child belongs
|
|
283
293
|
* to *this* spawn: a label, a pane or a session UUID would each be a guess.
|
|
284
294
|
*/
|
|
@@ -288,7 +298,7 @@ export type RadarSpawnEdge = {
|
|
|
288
298
|
created: RadarEffectOutcome;
|
|
289
299
|
launched: RadarEffectOutcome;
|
|
290
300
|
location?: RadarLocation;
|
|
291
|
-
bound?: { source: string; incarnation: string;
|
|
301
|
+
bound?: { source: string; incarnation: string; registration_id?: string };
|
|
292
302
|
exit_observation?: RadarSpawnExitObservation;
|
|
293
303
|
};
|
|
294
304
|
|
|
@@ -299,7 +309,6 @@ export type RadarSpawnEdge = {
|
|
|
299
309
|
* no exit was observed.
|
|
300
310
|
*/
|
|
301
311
|
export type RadarSpawnExitObservation = SpawnExitObservationSerialize;
|
|
302
|
-
export type RadarLaunchIdentityMarker = LaunchIdentityMarkerSerialize;
|
|
303
312
|
|
|
304
313
|
/** Durable Radar-owned metadata accepted with a managed launch intent. */
|
|
305
314
|
export type RadarManagedLaunchMetadata = ManagedLaunchMetadataDeserialize;
|
|
@@ -494,6 +503,43 @@ export function isUuid(value: unknown): value is string {
|
|
|
494
503
|
return typeof value === "string" && UUID.test(value);
|
|
495
504
|
}
|
|
496
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
|
+
|
|
497
543
|
function stringAt(value: unknown, field: string): string | undefined {
|
|
498
544
|
if (typeof value !== "object" || value === null) return undefined;
|
|
499
545
|
const found = (value as Record<string, unknown>)[field];
|
|
@@ -508,6 +554,46 @@ function numberAt(value: unknown, field: string): number | undefined {
|
|
|
508
554
|
: undefined;
|
|
509
555
|
}
|
|
510
556
|
|
|
557
|
+
function handoffAt(value: unknown): HandoffSerialize | undefined {
|
|
558
|
+
const version = numberAt(value, "version");
|
|
559
|
+
const id = stringAt(value, "id");
|
|
560
|
+
const sender = recordAt(value, "sender");
|
|
561
|
+
const senderGeneration = numberAt(sender, "execution_generation");
|
|
562
|
+
const target = stringAt(value, "recipient_subject_id");
|
|
563
|
+
const summary = stringAt(value, "summary");
|
|
564
|
+
const content = stringAt(value, "content");
|
|
565
|
+
const published = numberAt(value, "published_at_unix_ms");
|
|
566
|
+
const correlation = typeof value === "object" && value !== null
|
|
567
|
+
? (value as Record<string, unknown>).correlation_id
|
|
568
|
+
: undefined;
|
|
569
|
+
if (
|
|
570
|
+
version !== 1 || !isUuid(id) || !isUuid(stringAt(sender, "registration_id")) ||
|
|
571
|
+
!isUuid(stringAt(sender, "session_id")) || !isUuid(stringAt(sender, "subject_id")) ||
|
|
572
|
+
!isUuid(stringAt(sender, "execution_id")) || !Number.isSafeInteger(senderGeneration) ||
|
|
573
|
+
(senderGeneration ?? 0) < 1 || !isUuid(target) || summary === undefined || content === undefined ||
|
|
574
|
+
published === undefined || !Number.isSafeInteger(published) || published < 0 ||
|
|
575
|
+
(correlation !== undefined && correlation !== null &&
|
|
576
|
+
(typeof correlation !== "string" || correlation.length === 0 ||
|
|
577
|
+
Buffer.byteLength(correlation, "utf8") > 256 || /\p{Cc}/u.test(correlation)))
|
|
578
|
+
) return undefined;
|
|
579
|
+
return value as HandoffSerialize;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function handoffReceiptAt(value: unknown): HandoffReceiptSerialize | undefined {
|
|
583
|
+
const version = numberAt(value, "version");
|
|
584
|
+
const handoffId = stringAt(value, "handoff_id");
|
|
585
|
+
const recipient = recordAt(value, "recipient");
|
|
586
|
+
const generation = numberAt(recipient, "execution_generation");
|
|
587
|
+
const received = numberAt(value, "received_at_unix_ms");
|
|
588
|
+
if (
|
|
589
|
+
version !== 1 || !isUuid(handoffId) || !isUuid(stringAt(recipient, "registration_id")) ||
|
|
590
|
+
!isUuid(stringAt(recipient, "session_id")) || !isUuid(stringAt(recipient, "subject_id")) ||
|
|
591
|
+
!isUuid(stringAt(recipient, "execution_id")) || !Number.isSafeInteger(generation) ||
|
|
592
|
+
(generation ?? 0) < 1 || received === undefined || !Number.isSafeInteger(received) || received < 0
|
|
593
|
+
) return undefined;
|
|
594
|
+
return value as HandoffReceiptSerialize;
|
|
595
|
+
}
|
|
596
|
+
|
|
511
597
|
/** A writer binding a channel or context reply hands back to its publisher. */
|
|
512
598
|
function writerBinding(value: unknown): RadarWriterBinding | undefined {
|
|
513
599
|
const handle = stringAt(value, "handle");
|
|
@@ -662,6 +748,7 @@ const RADAR_MANAGED_IDENTITY_ENV = new Set([
|
|
|
662
748
|
"PI_RADAR_MANAGED_SUBJECT_ID",
|
|
663
749
|
"PI_RADAR_MANAGED_LAUNCH_ID",
|
|
664
750
|
"PI_RADAR_MANAGED_EXECUTION_GENERATION",
|
|
751
|
+
"PI_RADAR_MANAGED_PARENT_SUBJECT_ID",
|
|
665
752
|
]);
|
|
666
753
|
|
|
667
754
|
/**
|
|
@@ -715,7 +802,7 @@ function locationAt(value: unknown): RadarLocation | undefined {
|
|
|
715
802
|
}
|
|
716
803
|
|
|
717
804
|
/**
|
|
718
|
-
* One durable edge, decoded strictly. The bound child's `
|
|
805
|
+
* One durable edge, decoded strictly. The bound child's `registration_id` is kept only
|
|
719
806
|
* when it is a canonical UUID: a legacy binding has none, and an unreadable one
|
|
720
807
|
* is not an identity to correlate with.
|
|
721
808
|
*/
|
|
@@ -735,7 +822,7 @@ function spawnEdge(value: unknown): RadarResult<RadarSpawnEdge> {
|
|
|
735
822
|
const bound = recordAt(value, "bound");
|
|
736
823
|
const source = stringAt(bound, "source");
|
|
737
824
|
const incarnation = stringAt(bound, "incarnation");
|
|
738
|
-
const agentId = stringAt(bound, "
|
|
825
|
+
const agentId = stringAt(bound, "registration_id");
|
|
739
826
|
const exit = exitAt(recordAt(value, "exit_observation"));
|
|
740
827
|
return {
|
|
741
828
|
ok: true,
|
|
@@ -750,7 +837,7 @@ function spawnEdge(value: unknown): RadarResult<RadarSpawnEdge> {
|
|
|
750
837
|
bound: {
|
|
751
838
|
source,
|
|
752
839
|
incarnation,
|
|
753
|
-
...(isUuid(agentId) ? {
|
|
840
|
+
...(isUuid(agentId) ? { registration_id: agentId } : {}),
|
|
754
841
|
},
|
|
755
842
|
}
|
|
756
843
|
: {}),
|
|
@@ -952,15 +1039,15 @@ export type RadarClient = {
|
|
|
952
1039
|
ping(): Promise<RadarResult<RadarHandshake>>;
|
|
953
1040
|
register(
|
|
954
1041
|
registration: RadarRegistration,
|
|
955
|
-
): Promise<RadarResult<{
|
|
1042
|
+
): Promise<RadarResult<{ registration_id: string }>>;
|
|
956
1043
|
acquire(request: {
|
|
957
|
-
|
|
1044
|
+
registration_id: string;
|
|
958
1045
|
channel: RadarChannel;
|
|
959
1046
|
publisher: RadarPublisherIdentity;
|
|
960
1047
|
replace?: { generation: number; handle: string };
|
|
961
1048
|
}): Promise<RadarResult<RadarWriterBinding>>;
|
|
962
1049
|
publish(request: {
|
|
963
|
-
|
|
1050
|
+
registration_id: string;
|
|
964
1051
|
channel: RadarChannel;
|
|
965
1052
|
writer_handle: string;
|
|
966
1053
|
sequence: number;
|
|
@@ -968,8 +1055,10 @@ export type RadarClient = {
|
|
|
968
1055
|
observed_at?: string;
|
|
969
1056
|
snapshot: RadarChannelSnapshot;
|
|
970
1057
|
}): Promise<RadarResult<{ sequence: number }>>;
|
|
1058
|
+
presence(request: RadarPresenceReport): Promise<RadarResult<RadarPresenceAnswer>>;
|
|
1059
|
+
releasePresence(request: RadarPresenceRelease): Promise<RadarResult<RadarPresenceAnswer>>;
|
|
971
1060
|
context(request: {
|
|
972
|
-
|
|
1061
|
+
registration_id: string;
|
|
973
1062
|
publisher: RadarPublisherIdentity;
|
|
974
1063
|
writer_handle?: string;
|
|
975
1064
|
replace?: { generation: number; handle: string };
|
|
@@ -977,9 +1066,9 @@ export type RadarClient = {
|
|
|
977
1066
|
lease_ms?: number;
|
|
978
1067
|
observed_at?: string;
|
|
979
1068
|
context: RadarContextValue;
|
|
980
|
-
}): Promise<RadarResult<{ writer: RadarWriterBinding; warning?: string }>>;
|
|
1069
|
+
}): Promise<RadarResult<{ writer: RadarWriterBinding; warning?: string; management?: RadarManagement }>>;
|
|
981
1070
|
retire(request: {
|
|
982
|
-
|
|
1071
|
+
registration_id: string;
|
|
983
1072
|
channel: RadarChannel;
|
|
984
1073
|
writer_handle: string;
|
|
985
1074
|
}): Promise<RadarResult<void>>;
|
|
@@ -1012,9 +1101,15 @@ export type RadarClient = {
|
|
|
1012
1101
|
}): Promise<RadarResult<RadarSpawnEdge>>;
|
|
1013
1102
|
spawnGet(request_id: string): Promise<RadarResult<RadarSpawnTopology>>;
|
|
1014
1103
|
/** The registered child's process verdict, read from its immutable claim. */
|
|
1015
|
-
agentGet(
|
|
1104
|
+
agentGet(registration_id: string): Promise<RadarResult<RadarAgentProcess>>;
|
|
1016
1105
|
observe(): Promise<RadarResult<RadarInventory>>;
|
|
1017
1106
|
/** Read the durable event journal after `after`, waiting at most `wait_ms`. */
|
|
1107
|
+
communicationPublish(request: HandoffPublishParamsDeserialize & {
|
|
1108
|
+
request_id: string;
|
|
1109
|
+
}): Promise<RadarResult<{ handoff: HandoffSerialize; created: boolean }>>;
|
|
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 }>>;
|
|
1018
1113
|
events(request: {
|
|
1019
1114
|
after: number;
|
|
1020
1115
|
epoch?: string;
|
|
@@ -1413,6 +1508,24 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
|
|
|
1413
1508
|
return {
|
|
1414
1509
|
socketPath,
|
|
1415
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
|
+
},
|
|
1416
1529
|
async register(registration) {
|
|
1417
1530
|
return withRegistry(async () => {
|
|
1418
1531
|
const answer = await call("agent.register", {
|
|
@@ -1432,15 +1545,15 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
|
|
|
1432
1545
|
});
|
|
1433
1546
|
if (answer.ok === false) return answer;
|
|
1434
1547
|
const record = (answer.value as { registration?: unknown }).registration;
|
|
1435
|
-
const agentId = stringAt(record, "
|
|
1436
|
-
if (!isUuid(agentId)) return protocol("registration reply carries no
|
|
1437
|
-
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 } };
|
|
1438
1551
|
});
|
|
1439
1552
|
},
|
|
1440
1553
|
async acquire(request) {
|
|
1441
1554
|
return withRegistryChannel(request.channel, async () => {
|
|
1442
1555
|
const answer = await call("agent.acquire", {
|
|
1443
|
-
|
|
1556
|
+
registration_id: request.registration_id,
|
|
1444
1557
|
channel: request.channel,
|
|
1445
1558
|
publisher: request.publisher,
|
|
1446
1559
|
...(request.replace !== undefined ? { replace: request.replace } : {}),
|
|
@@ -1469,7 +1582,7 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
|
|
|
1469
1582
|
}
|
|
1470
1583
|
return withRegistryChannel(request.channel, async () => {
|
|
1471
1584
|
const answer = await call("agent.publish", {
|
|
1472
|
-
|
|
1585
|
+
registration_id: request.registration_id,
|
|
1473
1586
|
channel: request.channel,
|
|
1474
1587
|
writer_handle: request.writer_handle,
|
|
1475
1588
|
sequence: request.sequence,
|
|
@@ -1488,7 +1601,7 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
|
|
|
1488
1601
|
async context(request) {
|
|
1489
1602
|
return withRegistry(async () => {
|
|
1490
1603
|
const answer = await call("agent.context", {
|
|
1491
|
-
|
|
1604
|
+
registration_id: request.registration_id,
|
|
1492
1605
|
publisher: request.publisher,
|
|
1493
1606
|
...(request.writer_handle !== undefined
|
|
1494
1607
|
? { writer_handle: request.writer_handle }
|
|
@@ -1508,9 +1621,14 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
|
|
|
1508
1621
|
if (writer === undefined)
|
|
1509
1622
|
return protocol("context reply carries no writer binding");
|
|
1510
1623
|
const warning = stringAt(answer.value, "warning");
|
|
1624
|
+
const management = managementAt(answer.value);
|
|
1511
1625
|
return {
|
|
1512
1626
|
ok: true,
|
|
1513
|
-
value: {
|
|
1627
|
+
value: {
|
|
1628
|
+
writer,
|
|
1629
|
+
...(warning !== undefined ? { warning } : {}),
|
|
1630
|
+
...(management !== undefined ? { management } : {}),
|
|
1631
|
+
},
|
|
1514
1632
|
};
|
|
1515
1633
|
});
|
|
1516
1634
|
},
|
|
@@ -1603,7 +1721,7 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
|
|
|
1603
1721
|
message: "agent.get requires a canonical agent UUID",
|
|
1604
1722
|
};
|
|
1605
1723
|
return withRegistry(async () => {
|
|
1606
|
-
const answer = await call("agent.get", {
|
|
1724
|
+
const answer = await call("agent.get", { registration_id: agentId });
|
|
1607
1725
|
if (answer.ok === false) return answer;
|
|
1608
1726
|
return agentProcessAt(recordAt(answer.value, "agent"));
|
|
1609
1727
|
});
|
|
@@ -1666,6 +1784,56 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
|
|
|
1666
1784
|
return protocol("spawn.close reply carries no close outcome");
|
|
1667
1785
|
});
|
|
1668
1786
|
},
|
|
1787
|
+
async communicationPublish(request) {
|
|
1788
|
+
return withContract(RADAR_COMMUNICATION_CONTRACT, async () => {
|
|
1789
|
+
const answer = await call("communication.publish", {
|
|
1790
|
+
sender_registration_id: request.sender_registration_id,
|
|
1791
|
+
recipient_subject_id: request.recipient_subject_id,
|
|
1792
|
+
...(request.correlation_id !== undefined ? { correlation_id: request.correlation_id } : {}),
|
|
1793
|
+
summary: request.summary,
|
|
1794
|
+
content: request.content,
|
|
1795
|
+
}, request.request_id);
|
|
1796
|
+
if (answer.ok === false) return answer;
|
|
1797
|
+
const handoff = handoffAt(recordAt(answer.value, "handoff"));
|
|
1798
|
+
const created = recordAt(answer.value, "created");
|
|
1799
|
+
if (handoff === undefined || typeof created !== "boolean")
|
|
1800
|
+
return protocol("communication.publish reply carries no handoff");
|
|
1801
|
+
return { ok: true, value: { handoff, created } };
|
|
1802
|
+
});
|
|
1803
|
+
},
|
|
1804
|
+
async communicationInbox(request) {
|
|
1805
|
+
return withContract(RADAR_COMMUNICATION_CONTRACT, async () => {
|
|
1806
|
+
const answer = await call("communication.inbox", request);
|
|
1807
|
+
if (answer.ok === false) return answer;
|
|
1808
|
+
const handoffIds = recordAt(answer.value, "handoff_ids");
|
|
1809
|
+
if (!Array.isArray(handoffIds) || handoffIds.some((id) => !isUuid(id)))
|
|
1810
|
+
return protocol("communication.inbox reply carries malformed handoff IDs");
|
|
1811
|
+
return { ok: true, value: answer.value as HandoffInboxSerialize };
|
|
1812
|
+
});
|
|
1813
|
+
},
|
|
1814
|
+
async communicationGet(request) {
|
|
1815
|
+
return withContract(RADAR_COMMUNICATION_CONTRACT, async () => {
|
|
1816
|
+
const answer = await call("communication.get", request);
|
|
1817
|
+
if (answer.ok === false) return answer;
|
|
1818
|
+
const handoff = handoffAt(recordAt(answer.value, "handoff"));
|
|
1819
|
+
const receiptValue = recordAt(answer.value, "receipt");
|
|
1820
|
+
const receipt = receiptValue === null ? null : handoffReceiptAt(receiptValue);
|
|
1821
|
+
if (handoff === undefined || (receiptValue !== null && receipt === undefined))
|
|
1822
|
+
return protocol("communication.get reply carries malformed content or receipt");
|
|
1823
|
+
return { ok: true, value: { handoff, receipt: receipt ?? null } };
|
|
1824
|
+
});
|
|
1825
|
+
},
|
|
1826
|
+
async communicationAck(request) {
|
|
1827
|
+
return withContract(RADAR_COMMUNICATION_CONTRACT, async () => {
|
|
1828
|
+
const answer = await call("communication.ack", request);
|
|
1829
|
+
if (answer.ok === false) return answer;
|
|
1830
|
+
const receipt = handoffReceiptAt(recordAt(answer.value, "receipt"));
|
|
1831
|
+
const created = recordAt(answer.value, "created");
|
|
1832
|
+
if (receipt === undefined || typeof created !== "boolean")
|
|
1833
|
+
return protocol("communication.ack reply carries no receipt");
|
|
1834
|
+
return { ok: true, value: { receipt, created } };
|
|
1835
|
+
});
|
|
1836
|
+
},
|
|
1669
1837
|
async events(request) {
|
|
1670
1838
|
if (
|
|
1671
1839
|
!Number.isSafeInteger(request.after) ||
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { HandoffInboxSerialize, HandoffSerialize, HandoffReceiptSerialize } from "./generated-contracts.ts";
|
|
2
|
+
import type { RadarClient, RadarResult } from "./radar-client.ts";
|
|
3
|
+
import type { RadarPublication } from "./radar-publication.ts";
|
|
4
|
+
|
|
5
|
+
export type RadarHandoffPort = {
|
|
6
|
+
/** Publish to this managed execution's Radar-recorded parent. Retry an uncertain result with the same ID and bytes. */
|
|
7
|
+
publish(request: {
|
|
8
|
+
request_id: string;
|
|
9
|
+
correlation_id?: string;
|
|
10
|
+
summary: string;
|
|
11
|
+
content: string;
|
|
12
|
+
}): Promise<RadarResult<{ handoff: HandoffSerialize; created: boolean }>>;
|
|
13
|
+
/** Durable inbox scan for this process's managed subject. Notifications are hints; callers can always rescan. */
|
|
14
|
+
inbox(): Promise<RadarResult<HandoffInboxSerialize>>;
|
|
15
|
+
/** Retrieve the full immutable content even after it has been acknowledged. */
|
|
16
|
+
get(handoff_id: string): Promise<RadarResult<{ handoff: HandoffSerialize; receipt: HandoffReceiptSerialize | null }>>;
|
|
17
|
+
/** Call only after the consumer durably recorded the retrieved payload. */
|
|
18
|
+
ack(handoff_id: string): Promise<RadarResult<{ receipt: HandoffReceiptSerialize; created: boolean }>>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Bind handoff operations to this Pi process's Radar registration and the
|
|
23
|
+
* parent subject injected from its accepted managed-launch metadata.
|
|
24
|
+
*/
|
|
25
|
+
export function createRadarHandoffPort(
|
|
26
|
+
client: RadarClient,
|
|
27
|
+
publication: Pick<RadarPublication, "subject">,
|
|
28
|
+
parentSubjectId: () => string | undefined = () => process.env.PI_RADAR_MANAGED_PARENT_SUBJECT_ID,
|
|
29
|
+
): RadarHandoffPort {
|
|
30
|
+
async function agentId(): Promise<RadarResult<string>> {
|
|
31
|
+
const subject = await publication.subject();
|
|
32
|
+
return subject === undefined
|
|
33
|
+
? { ok: false, code: "absent", message: "this process has no current Radar registration" }
|
|
34
|
+
: { ok: true, value: subject.registration_id };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
async publish(request) {
|
|
39
|
+
const identity = await agentId();
|
|
40
|
+
if (identity.ok === false) return identity;
|
|
41
|
+
const parent = parentSubjectId();
|
|
42
|
+
if (parent === undefined)
|
|
43
|
+
return { ok: false, code: "absent", message: "this managed execution has no Radar parent subject" };
|
|
44
|
+
return client.communicationPublish({
|
|
45
|
+
...request,
|
|
46
|
+
sender_registration_id: identity.value,
|
|
47
|
+
recipient_subject_id: parent,
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
async inbox() {
|
|
51
|
+
const identity = await agentId();
|
|
52
|
+
if (identity.ok === false) return identity;
|
|
53
|
+
return client.communicationInbox({ registration_id: identity.value });
|
|
54
|
+
},
|
|
55
|
+
async get(handoff_id) {
|
|
56
|
+
const identity = await agentId();
|
|
57
|
+
if (identity.ok === false) return identity;
|
|
58
|
+
return client.communicationGet({ registration_id: identity.value, handoff_id });
|
|
59
|
+
},
|
|
60
|
+
async ack(handoff_id) {
|
|
61
|
+
const identity = await agentId();
|
|
62
|
+
if (identity.ok === false) return identity;
|
|
63
|
+
return client.communicationAck({ registration_id: identity.value, handoff_id });
|
|
64
|
+
},
|
|
65
|
+
};
|
|
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 {
|