pi-radar 0.9.0 → 0.11.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 +8 -10
- package/dist/extension/baseline.js +343 -361
- package/dist/extension/radar-children.js +108 -218
- package/dist/extension/radar-client.js +103 -15
- package/dist/extension/radar-execution.js +26 -17
- package/dist/extension/radar-inspection.js +0 -45
- package/dist/extension/radar-launch.js +24 -185
- package/dist/extension/radar-publication.js +26 -17
- package/dist/extension/radar-runtime.js +1 -1
- package/dist/index.js +317 -488
- package/extension/generated-contracts.ts +362 -13
- package/extension/radar-children.ts +60 -155
- package/extension/radar-client.ts +130 -19
- package/extension/radar-inspection.ts +2 -77
- package/extension/radar-launch.ts +52 -391
- package/extension/radar-publication.ts +36 -53
- package/extension/radar-runtime.ts +1 -1
- package/package.json +1 -1
- package/schema/public/v1/observation-get-response.deserialize.schema.json +5 -25
- package/schema/public/v1/observation-get-response.serialize.schema.json +7 -29
- package/schema/public/v1/observation-snapshot.deserialize.schema.json +5 -25
- package/schema/public/v1/observation-snapshot.serialize.schema.json +7 -29
- package/schema/public/v1/spawn-list-page.deserialize.schema.json +40 -17
- package/schema/public/v1/spawn-list-page.serialize.schema.json +40 -17
- package/schema/public/v1/subject-assignment-acquire.deserialize.schema.json +78 -0
- package/schema/public/v1/subject-assignment-acquire.serialize.schema.json +79 -0
- package/schema/public/v1/subject-assignment-publish.deserialize.schema.json +310 -0
- package/schema/public/v1/subject-assignment-publish.serialize.schema.json +310 -0
- package/schema/public/v1/subject-assignment-retire.deserialize.schema.json +22 -0
- package/schema/public/v1/subject-assignment-retire.serialize.schema.json +22 -0
- package/schema/public/v1/subject-child-get-params.deserialize.schema.json +18 -0
- package/schema/public/v1/subject-child-get-params.serialize.schema.json +18 -0
- package/schema/public/v1/subject-children-params.deserialize.schema.json +51 -0
- package/schema/public/v1/subject-children-params.serialize.schema.json +54 -0
- package/schema/public/v1/subject-get-params.deserialize.schema.json +14 -0
- package/schema/public/v1/subject-get-params.serialize.schema.json +14 -0
- package/schema/public/v1/subject-get-result.deserialize.schema.json +566 -0
- package/schema/public/v1/subject-get-result.serialize.schema.json +573 -0
- package/schema/public/v1/subject-list-params.deserialize.schema.json +38 -0
- package/schema/public/v1/subject-list-params.serialize.schema.json +43 -0
- package/schema/public/v1/subject-list-result.deserialize.schema.json +575 -0
- package/schema/public/v1/subject-list-result.serialize.schema.json +583 -0
|
@@ -1,176 +1,81 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import type { RadarClient, RadarChannelSnapshot, RadarResult } from "./radar-client.ts";
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import type { SubjectGetResult, SubjectChildrenParams } from "./generated-contracts.ts";
|
|
4
|
+
import type { RadarInspection } from "./radar-inspection.ts";
|
|
5
|
+
import { waitForChild, launchFailure, type RadarLaunchOutcome } from "./radar-launch.ts";
|
|
5
6
|
import type { RadarPublication } from "./radar-publication.ts";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
/** One child of the caller, named by the agent that survives its restarts. */
|
|
11
|
-
export type RadarChild = {
|
|
12
|
-
subject_id: string;
|
|
13
|
-
/** The launch that last bound this child. */
|
|
14
|
-
request_id: string;
|
|
15
|
-
pane_id?: string;
|
|
16
|
-
state: "unbound" | "bound" | "unresolved";
|
|
17
|
-
exited: boolean;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
/** The assignment a child is held to: published again when its process is replaced. */
|
|
8
|
+
/** An agent, including its retained identity when no process is attached. */
|
|
9
|
+
export type RadarChild = SubjectGetResult["subject"];
|
|
21
10
|
export type RadarChildAssignment = {
|
|
22
11
|
update(snapshot: RadarChannelSnapshot): void;
|
|
23
12
|
retire(): void;
|
|
24
13
|
};
|
|
25
|
-
|
|
14
|
+
export type RadarChildProgram = {
|
|
15
|
+
executable: string;
|
|
16
|
+
argv: string[];
|
|
17
|
+
cwd?: string;
|
|
18
|
+
env?: Record<string, string>;
|
|
19
|
+
};
|
|
20
|
+
export type RadarChildObservation =
|
|
21
|
+
| { kind: "live"; subject: RadarChild }
|
|
22
|
+
| { kind: "gone"; subject: RadarChild }
|
|
23
|
+
| { kind: "unknown"; diagnostic: string };
|
|
26
24
|
export type RadarChildren = {
|
|
27
|
-
list(): Promise<RadarResult<RadarChild[]>>;
|
|
25
|
+
list(selection?: SubjectChildrenParams["selection"]): Promise<RadarResult<RadarChild[]>>;
|
|
26
|
+
restart(subject: string, program: RadarChildProgram, options?: { pollMs?: number; signal?: AbortSignal }): Promise<RadarLaunchOutcome>;
|
|
28
27
|
assignment(subject: string, owner: string): RadarChildAssignment;
|
|
29
28
|
retire(subject: string): void;
|
|
30
|
-
observe(
|
|
31
|
-
|
|
32
|
-
session_id: string | undefined,
|
|
33
|
-
): Promise<RadarChildObservation>;
|
|
34
|
-
inspect(subject: string, session_id?: string): Promise<RadarInspection>;
|
|
29
|
+
observe(subject: string): Promise<RadarChildObservation>;
|
|
30
|
+
inspect(subject: string): Promise<RadarInspection>;
|
|
35
31
|
};
|
|
36
32
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* The caller's children, addressed by subject. A subject gains an edge per launch,
|
|
41
|
-
* so the newest bound edge is the current process; its registration is read per
|
|
42
|
-
* call and never remembered, because a remembered registration is the stale key
|
|
43
|
-
* this surface exists to hide. An edge bound without a subject is not addressable.
|
|
44
|
-
*/
|
|
33
|
+
/** All child lifecycle resolution and assignment ownership belong to Radar. */
|
|
45
34
|
export function createRadarChildren(
|
|
46
|
-
client: Pick<RadarClient, "
|
|
47
|
-
publication: Pick<RadarPublication, "assignment" | "retireAssignment">,
|
|
35
|
+
client: Pick<RadarClient, "subjectChildren" | "childGet" | "childInspect" | "spawnGet" | "restart">,
|
|
36
|
+
publication: Pick<RadarPublication, "assignment" | "retireAssignment" | "subject">,
|
|
48
37
|
): RadarChildren {
|
|
49
|
-
const held = new Map<
|
|
50
|
-
string,
|
|
51
|
-
{ writer: RadarChildAssignment; published?: string; publish?: (target: Resolved) => void }
|
|
52
|
-
>();
|
|
53
|
-
|
|
54
|
-
async function resolveAll(): Promise<RadarResult<Map<string, Resolved & { at: number }>>> {
|
|
55
|
-
const newest = new Map<string, Resolved & { at: number }>();
|
|
56
|
-
let after: string | undefined;
|
|
57
|
-
for (let page = 0; page < MAX_PAGES; page++) {
|
|
58
|
-
const read = await client.spawnList({ ...(after !== undefined ? { after } : {}), limit: PAGE });
|
|
59
|
-
if (read.ok === false) return read;
|
|
60
|
-
for (const edge of read.value.spawns as ListedEdge[]) {
|
|
61
|
-
const bound = edge.bound;
|
|
62
|
-
if (!bound?.subject_id || !bound.registration_id || edge.launched !== "completed") continue;
|
|
63
|
-
const at = Date.parse(bound.bound_at);
|
|
64
|
-
if (Number.isNaN(at)) continue;
|
|
65
|
-
const current = newest.get(bound.subject_id);
|
|
66
|
-
if (current !== undefined && current.at >= at) continue;
|
|
67
|
-
newest.set(bound.subject_id, {
|
|
68
|
-
at,
|
|
69
|
-
registration_id: bound.registration_id,
|
|
70
|
-
child: {
|
|
71
|
-
subject_id: bound.subject_id,
|
|
72
|
-
request_id: edge.request_id,
|
|
73
|
-
...(edge.location?.pane ? { pane_id: edge.location.pane } : {}),
|
|
74
|
-
state: edge.state,
|
|
75
|
-
exited: edge.exit_observation != null && edge.exit_observation.state !== "unknown",
|
|
76
|
-
},
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
if (!read.value.next) return { ok: true, value: newest };
|
|
80
|
-
after = read.value.next;
|
|
81
|
-
}
|
|
82
|
-
return { ok: false, code: "protocol", message: "the caller's launches exceed the readable pages" };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function resolve(subject: string): Promise<Resolved | undefined> {
|
|
86
|
-
const all = await resolveAll();
|
|
87
|
-
return all.ok ? all.value.get(subject) : undefined;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function resolveAndFollow(subject: string): Promise<Resolved | undefined> {
|
|
91
|
-
const all = await resolveAll();
|
|
92
|
-
if (!all.ok) return undefined;
|
|
93
|
-
follow(all.value);
|
|
94
|
-
return all.value.get(subject);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* A child replaced under its subject has a new process with no assignment. Each
|
|
99
|
-
* read the caller already makes finds the new registration and moves the held
|
|
100
|
-
* assignment onto it, so no caller has to notice a replacement.
|
|
101
|
-
*/
|
|
102
|
-
function follow(all: Map<string, Resolved>): void {
|
|
103
|
-
for (const [subject, entry] of held) {
|
|
104
|
-
const target = all.get(subject);
|
|
105
|
-
if (target !== undefined && entry.published !== target.registration_id)
|
|
106
|
-
entry.publish?.(target);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
38
|
return {
|
|
111
|
-
async list() {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
published?: string;
|
|
124
|
-
publish?: (target: Resolved) => void;
|
|
125
|
-
} = { writer: undefined as unknown as RadarChildAssignment };
|
|
126
|
-
const send = (target: Resolved): void => {
|
|
127
|
-
if (latest === undefined) return;
|
|
128
|
-
if (entry.published !== undefined && entry.published !== target.registration_id)
|
|
129
|
-
publication.retireAssignment(entry.published);
|
|
130
|
-
entry.published = target.registration_id;
|
|
131
|
-
publication.assignment(target.registration_id, owner).update(latest);
|
|
132
|
-
};
|
|
133
|
-
entry.publish = send;
|
|
134
|
-
entry.writer = {
|
|
135
|
-
update(snapshot) {
|
|
136
|
-
latest = snapshot;
|
|
137
|
-
void resolve(subject).then((target) => {
|
|
138
|
-
if (target !== undefined) send(target);
|
|
139
|
-
});
|
|
140
|
-
},
|
|
141
|
-
retire() {
|
|
142
|
-
latest = undefined;
|
|
143
|
-
if (entry.published !== undefined) publication.retireAssignment(entry.published);
|
|
144
|
-
entry.published = undefined;
|
|
145
|
-
},
|
|
146
|
-
};
|
|
147
|
-
held.set(subject, entry);
|
|
148
|
-
return entry.writer;
|
|
39
|
+
async list(selection) {
|
|
40
|
+
const children: RadarChild[] = [];
|
|
41
|
+
let after: string | undefined;
|
|
42
|
+
do {
|
|
43
|
+
const page = await client.subjectChildren({ selection, after, limit: 100 });
|
|
44
|
+
if (!page.ok) return page;
|
|
45
|
+
children.push(...page.value.subjects);
|
|
46
|
+
if (page.value.next === after)
|
|
47
|
+
return { ok: false, code: "protocol", message: "child page did not advance" };
|
|
48
|
+
after = page.value.next ?? undefined;
|
|
49
|
+
} while (after !== undefined);
|
|
50
|
+
return { ok: true, value: children };
|
|
149
51
|
},
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
pane_id: target.child.pane_id,
|
|
161
|
-
registration_id: target.registration_id,
|
|
162
|
-
session_id,
|
|
52
|
+
async restart(subject, program, options) {
|
|
53
|
+
const parent = await publication.subject();
|
|
54
|
+
if (parent?.subject_id === undefined)
|
|
55
|
+
return { kind: "failed", message: "this process has no granted subject" };
|
|
56
|
+
const request_id = randomUUID();
|
|
57
|
+
const requested = await client.restart({ request_id, parent: parent.registration_id, subject_id: subject, ...program });
|
|
58
|
+
if (!requested.ok) return launchFailure(requested, request_id);
|
|
59
|
+
const bound = await waitForChild(client, requested.value.launch_request_id, {
|
|
60
|
+
pollMs: options?.pollMs ?? 250,
|
|
61
|
+
...(options?.signal !== undefined ? { signal: options.signal } : {}),
|
|
163
62
|
});
|
|
63
|
+
if (bound.kind !== "bound") return bound;
|
|
64
|
+
return { kind: "ready", value: { request_id: requested.value.launch_request_id, subject_id: bound.subject_id, pane_id: bound.pane_id } };
|
|
164
65
|
},
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
66
|
+
assignment: (subject, owner) => publication.assignment(subject, owner),
|
|
67
|
+
retire: (subject) => publication.retireAssignment(subject),
|
|
68
|
+
async observe(subject) {
|
|
69
|
+
const read = await client.childGet(subject);
|
|
70
|
+
if (!read.ok) return { kind: "unknown", diagnostic: `${read.code}: ${read.message}` };
|
|
71
|
+
if (read.value.state === "live") return { kind: "live", subject: read.value };
|
|
72
|
+
if (read.value.state === "detached") return { kind: "gone", subject: read.value };
|
|
73
|
+
return { kind: "unknown", diagnostic: `child attachment is ${read.value.state}` };
|
|
74
|
+
},
|
|
75
|
+
async inspect(subject) {
|
|
76
|
+
const read = await client.childInspect(subject);
|
|
77
|
+
if (!read.ok) throw new Error(`Child inspection unavailable (${read.code}: ${read.message})`);
|
|
78
|
+
return read.value;
|
|
174
79
|
},
|
|
175
80
|
};
|
|
176
81
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { RadarInspection } from "./radar-inspection.ts";
|
|
2
3
|
import { lstatSync } from "node:fs";
|
|
3
4
|
import { createConnection } from "node:net";
|
|
4
5
|
import { dirname, join } from "node:path";
|
|
@@ -13,6 +14,10 @@ import type {
|
|
|
13
14
|
SpawnExitObservationSerialize,
|
|
14
15
|
SpawnListParamsDeserialize,
|
|
15
16
|
SpawnListPageSerialize,
|
|
17
|
+
SubjectChildGetParams,
|
|
18
|
+
SubjectChildrenParams,
|
|
19
|
+
SubjectGetResult,
|
|
20
|
+
SubjectListResult,
|
|
16
21
|
} from "./generated-contracts.ts";
|
|
17
22
|
|
|
18
23
|
/**
|
|
@@ -38,12 +43,11 @@ export const RADAR_PROTOCOL_VERSION = 2;
|
|
|
38
43
|
* parent continue or restart its own child by subject.
|
|
39
44
|
* Earlier daemons expose different identity fields; require a coordinated update.
|
|
40
45
|
*/
|
|
41
|
-
export const RADAR_MINIMUM_REVISION =
|
|
46
|
+
export const RADAR_MINIMUM_REVISION = 9;
|
|
42
47
|
/** Public daemon contracts this client may preflight. */
|
|
43
48
|
export const RADAR_REGISTRY_CONTRACT = "registry.v1";
|
|
44
49
|
export const RADAR_REGISTRY_FACTS_CONTRACT = "registry.facts.v1";
|
|
45
50
|
export const RADAR_SPAWN_CONTRACT = "spawn.v1";
|
|
46
|
-
export const RADAR_SPAWN_LIST_CONTRACT = "spawn.list.v1";
|
|
47
51
|
export const RADAR_SPAWN_CLOSE_CONTRACT = "spawn.close.v1";
|
|
48
52
|
export const RADAR_LIFECYCLE_RESTART_CONTRACT = "lifecycle.restart.v1";
|
|
49
53
|
export const RADAR_SPAWN_LAUNCH_CONTEXT_CONTRACT = "spawn.launch_context.v1";
|
|
@@ -309,6 +313,9 @@ export type RadarSpawnEdge = {
|
|
|
309
313
|
location?: RadarLocation;
|
|
310
314
|
bound?: { source: string; incarnation: string; registration_id?: string; subject_id?: string };
|
|
311
315
|
exit_observation?: RadarSpawnExitObservation;
|
|
316
|
+
/** Set when the daemon withdrew the launch before its process presented. */
|
|
317
|
+
withdrawn?: { at: string; reason: string };
|
|
318
|
+
granted_at?: string;
|
|
312
319
|
};
|
|
313
320
|
|
|
314
321
|
/**
|
|
@@ -808,8 +815,7 @@ function locationAt(value: unknown): RadarLocation | undefined {
|
|
|
808
815
|
|
|
809
816
|
/**
|
|
810
817
|
* One durable edge, decoded strictly. The bound child's `registration_id` is kept only
|
|
811
|
-
* when it is a canonical UUID:
|
|
812
|
-
* is not an identity to correlate with.
|
|
818
|
+
* when it is a canonical UUID: an unreadable one is not an identity to correlate with.
|
|
813
819
|
*/
|
|
814
820
|
function spawnEdge(value: unknown): RadarResult<RadarSpawnEdge> {
|
|
815
821
|
const requestId = stringAt(value, "request_id");
|
|
@@ -830,6 +836,9 @@ function spawnEdge(value: unknown): RadarResult<RadarSpawnEdge> {
|
|
|
830
836
|
const agentId = stringAt(bound, "registration_id");
|
|
831
837
|
const subjectId = stringAt(bound, "subject_id");
|
|
832
838
|
const exit = exitAt(recordAt(value, "exit_observation"));
|
|
839
|
+
const withdrawnRecord = recordAt(value, "withdrawn");
|
|
840
|
+
const withdrawnAt = stringAt(withdrawnRecord, "at");
|
|
841
|
+
const withdrawnReason = stringAt(withdrawnRecord, "reason");
|
|
833
842
|
return {
|
|
834
843
|
ok: true,
|
|
835
844
|
value: {
|
|
@@ -849,6 +858,10 @@ function spawnEdge(value: unknown): RadarResult<RadarSpawnEdge> {
|
|
|
849
858
|
}
|
|
850
859
|
: {}),
|
|
851
860
|
...(exit !== undefined ? { exit_observation: exit } : {}),
|
|
861
|
+
...(stringAt(value, "granted_at") !== undefined ? { granted_at: stringAt(value, "granted_at") } : {}),
|
|
862
|
+
...(withdrawnAt !== undefined && withdrawnReason !== undefined
|
|
863
|
+
? { withdrawn: { at: withdrawnAt, reason: withdrawnReason } }
|
|
864
|
+
: {}),
|
|
852
865
|
},
|
|
853
866
|
};
|
|
854
867
|
}
|
|
@@ -867,6 +880,16 @@ const PROCESS_VERDICTS: readonly RadarProcessVerdict[] = [
|
|
|
867
880
|
"unavailable",
|
|
868
881
|
];
|
|
869
882
|
|
|
883
|
+
function subjectAt(value: unknown): RadarResult<SubjectGetResult["subject"]> {
|
|
884
|
+
const subjectId = recordAt(value, "subject_id");
|
|
885
|
+
const session = recordAt(value, "session_id");
|
|
886
|
+
const state = recordAt(value, "state");
|
|
887
|
+
if (!isUuid(subjectId) || (session != null && !isUuid(session)) ||
|
|
888
|
+
!["starting", "live", "detached", "unknown"].includes(String(state)))
|
|
889
|
+
return protocol("subject read carries no valid identity or attachment state");
|
|
890
|
+
return { ok: true, value: value as SubjectGetResult["subject"] };
|
|
891
|
+
}
|
|
892
|
+
|
|
870
893
|
function agentProcessAt(agent: unknown): RadarResult<RadarAgentProcess> {
|
|
871
894
|
const process = recordAt(agent, "process");
|
|
872
895
|
const claimed = recordAt(process, "claimed");
|
|
@@ -1051,13 +1074,15 @@ export type RadarClient = {
|
|
|
1051
1074
|
registration: RadarRegistration,
|
|
1052
1075
|
): Promise<RadarResult<{ registration_id: string }>>;
|
|
1053
1076
|
acquire(request: {
|
|
1054
|
-
registration_id
|
|
1077
|
+
registration_id?: string;
|
|
1078
|
+
subject_id?: string;
|
|
1055
1079
|
channel: RadarChannel;
|
|
1056
1080
|
publisher: RadarPublisherIdentity;
|
|
1057
1081
|
replace?: { generation: number; handle: string };
|
|
1058
1082
|
}): Promise<RadarResult<RadarWriterBinding>>;
|
|
1059
1083
|
publish(request: {
|
|
1060
|
-
registration_id
|
|
1084
|
+
registration_id?: string;
|
|
1085
|
+
subject_id?: string;
|
|
1061
1086
|
channel: RadarChannel;
|
|
1062
1087
|
writer_handle: string;
|
|
1063
1088
|
sequence: number;
|
|
@@ -1078,7 +1103,8 @@ export type RadarClient = {
|
|
|
1078
1103
|
context: RadarContextValue;
|
|
1079
1104
|
}): Promise<RadarResult<{ writer: RadarWriterBinding; warning?: string; management?: RadarManagement }>>;
|
|
1080
1105
|
retire(request: {
|
|
1081
|
-
registration_id
|
|
1106
|
+
registration_id?: string;
|
|
1107
|
+
subject_id?: string;
|
|
1082
1108
|
channel: RadarChannel;
|
|
1083
1109
|
writer_handle: string;
|
|
1084
1110
|
}): Promise<RadarResult<void>>;
|
|
@@ -1124,10 +1150,13 @@ export type RadarClient = {
|
|
|
1124
1150
|
argv: string[];
|
|
1125
1151
|
cwd?: string;
|
|
1126
1152
|
env?: Record<string, string>;
|
|
1127
|
-
}): Promise<RadarResult<{ request_id: string }>>;
|
|
1153
|
+
}): Promise<RadarResult<{ request_id: string; launch_request_id: string }>>;
|
|
1128
1154
|
/** List launches owned by this client's registered process. */
|
|
1129
1155
|
spawnList(request: Pick<SpawnListParamsDeserialize, "after" | "limit">): Promise<RadarResult<SpawnListPageSerialize>>;
|
|
1130
1156
|
spawnGet(request_id: string): Promise<RadarResult<RadarSpawnTopology>>;
|
|
1157
|
+
subjectChildren(request?: Omit<SubjectChildrenParams, "registration_id">): Promise<RadarResult<SubjectListResult>>;
|
|
1158
|
+
childGet(subject_id: string): Promise<RadarResult<SubjectGetResult["subject"]>>;
|
|
1159
|
+
childInspect(subject_id: string): Promise<RadarResult<RadarInspection>>;
|
|
1131
1160
|
/** The registered child's process verdict, read from its immutable claim. */
|
|
1132
1161
|
agentGet(registration_id: string): Promise<RadarResult<RadarAgentProcess>>;
|
|
1133
1162
|
observe(): Promise<RadarResult<RadarInventory>>;
|
|
@@ -1541,7 +1570,11 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
|
|
|
1541
1570
|
return withRegistry(async () => {
|
|
1542
1571
|
const answer = await call("agent.presence", { ...request });
|
|
1543
1572
|
if (!answer.ok) return answer;
|
|
1544
|
-
|
|
1573
|
+
const decoded = decodePresenceAnswer(answer.value);
|
|
1574
|
+
// Presence names the caller's own registration, which is the
|
|
1575
|
+
// authority every subject-addressed child call is checked against.
|
|
1576
|
+
if (decoded.ok) processRegistrationId = decoded.value.registration_id;
|
|
1577
|
+
return decoded;
|
|
1545
1578
|
});
|
|
1546
1579
|
},
|
|
1547
1580
|
async releasePresence(request) {
|
|
@@ -1582,9 +1615,13 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
|
|
|
1582
1615
|
},
|
|
1583
1616
|
async acquire(request) {
|
|
1584
1617
|
return withRegistryChannel(request.channel, async () => {
|
|
1585
|
-
const
|
|
1586
|
-
|
|
1587
|
-
|
|
1618
|
+
const assignment = request.channel === "assignment";
|
|
1619
|
+
if (assignment && processRegistrationId === undefined)
|
|
1620
|
+
return { ok: false, code: "refused", message: "assignment requires a granted parent" };
|
|
1621
|
+
if (assignment && request.subject_id === undefined)
|
|
1622
|
+
return { ok: false, code: "bad_params", message: "an assignment names a child subject" };
|
|
1623
|
+
const answer = await call(assignment ? "subject.assignment.acquire" : "agent.acquire", {
|
|
1624
|
+
...(assignment ? { registration_id: processRegistrationId, subject_id: request.subject_id } : { registration_id: request.registration_id, channel: request.channel }),
|
|
1588
1625
|
publisher: request.publisher,
|
|
1589
1626
|
...(request.replace !== undefined ? { replace: request.replace } : {}),
|
|
1590
1627
|
});
|
|
@@ -1611,15 +1648,21 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
|
|
|
1611
1648
|
}
|
|
1612
1649
|
}
|
|
1613
1650
|
return withRegistryChannel(request.channel, async () => {
|
|
1614
|
-
const
|
|
1615
|
-
registration_id: request.registration_id,
|
|
1616
|
-
channel: request.channel,
|
|
1651
|
+
const update = {
|
|
1617
1652
|
writer_handle: request.writer_handle,
|
|
1618
1653
|
sequence: request.sequence,
|
|
1619
1654
|
...(request.lease_ms !== undefined ? { lease_ms: request.lease_ms } : {}),
|
|
1620
1655
|
...(request.observed_at !== undefined ? { observed_at: request.observed_at } : {}),
|
|
1621
1656
|
snapshot,
|
|
1622
|
-
}
|
|
1657
|
+
};
|
|
1658
|
+
const assignment = request.channel === "assignment";
|
|
1659
|
+
if (assignment && processRegistrationId === undefined)
|
|
1660
|
+
return { ok: false, code: "refused", message: "assignment requires a granted parent" };
|
|
1661
|
+
if (assignment && request.subject_id === undefined)
|
|
1662
|
+
return { ok: false, code: "bad_params", message: "an assignment names a child subject" };
|
|
1663
|
+
const answer = await call(assignment ? "subject.assignment.publish" : "agent.publish", assignment
|
|
1664
|
+
? { registration_id: processRegistrationId, subject_id: request.subject_id, update }
|
|
1665
|
+
: { registration_id: request.registration_id, channel: request.channel, ...update });
|
|
1623
1666
|
if (answer.ok === false) return answer;
|
|
1624
1667
|
const channel = (answer.value as { channel?: unknown }).channel;
|
|
1625
1668
|
const writer = (channel as { writer?: unknown } | undefined)?.writer;
|
|
@@ -1664,7 +1707,17 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
|
|
|
1664
1707
|
},
|
|
1665
1708
|
async retire(request) {
|
|
1666
1709
|
return withRegistryChannel(request.channel, async () => {
|
|
1667
|
-
const
|
|
1710
|
+
const assignment = request.channel === "assignment";
|
|
1711
|
+
if (assignment && processRegistrationId === undefined)
|
|
1712
|
+
return { ok: false, code: "refused", message: "assignment requires a granted parent" };
|
|
1713
|
+
if (assignment && request.subject_id === undefined)
|
|
1714
|
+
return { ok: false, code: "bad_params", message: "an assignment names a child subject" };
|
|
1715
|
+
const answer = await call(assignment ? "subject.assignment.retire" : "agent.retire", {
|
|
1716
|
+
...(assignment
|
|
1717
|
+
? { registration_id: processRegistrationId, subject_id: request.subject_id }
|
|
1718
|
+
: { registration_id: request.registration_id, channel: request.channel }),
|
|
1719
|
+
writer_handle: request.writer_handle,
|
|
1720
|
+
});
|
|
1668
1721
|
return answer.ok === true ? { ok: true, value: undefined } : answer;
|
|
1669
1722
|
});
|
|
1670
1723
|
},
|
|
@@ -1757,14 +1810,72 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
|
|
|
1757
1810
|
request.request_id,
|
|
1758
1811
|
);
|
|
1759
1812
|
if (answer.ok === false) return answer;
|
|
1760
|
-
|
|
1813
|
+
const settled = focusOutcome(recordAt(answer.value, "request"), request.request_id);
|
|
1814
|
+
if (settled.ok === false) return settled;
|
|
1815
|
+
const launch = recordAt(recordAt(answer.value, "request"), "launch_request_id");
|
|
1816
|
+
if (!isUuid(launch)) return protocol("lifecycle.restart reply names no launch");
|
|
1817
|
+
return { ok: true, value: { ...settled.value, launch_request_id: launch } };
|
|
1818
|
+
});
|
|
1819
|
+
},
|
|
1820
|
+
async subjectChildren(request = {}) {
|
|
1821
|
+
const registrationId = processRegistrationId;
|
|
1822
|
+
if (registrationId === undefined)
|
|
1823
|
+
return { ok: false, code: "refused", message: "this process has no Radar registration" };
|
|
1824
|
+
return withRegistry(async () => {
|
|
1825
|
+
const params: SubjectChildrenParams = { ...request, registration_id: registrationId };
|
|
1826
|
+
const answer = await call("subject.children", { ...params });
|
|
1827
|
+
if (!answer.ok) return answer;
|
|
1828
|
+
const subjects = recordAt(answer.value, "subjects");
|
|
1829
|
+
const next = recordAt(answer.value, "next");
|
|
1830
|
+
if (!Array.isArray(subjects) || (next != null && !isUuid(next)))
|
|
1831
|
+
return protocol("subject.children reply carries no valid page");
|
|
1832
|
+
for (const subject of subjects) {
|
|
1833
|
+
const decoded = subjectAt(subject);
|
|
1834
|
+
if (!decoded.ok) return decoded;
|
|
1835
|
+
}
|
|
1836
|
+
return { ok: true, value: answer.value as SubjectListResult };
|
|
1837
|
+
});
|
|
1838
|
+
},
|
|
1839
|
+
async childGet(subject_id) {
|
|
1840
|
+
const registrationId = processRegistrationId;
|
|
1841
|
+
if (registrationId === undefined)
|
|
1842
|
+
return { ok: false, code: "refused", message: "this process has no Radar registration" };
|
|
1843
|
+
if (!isUuid(subject_id))
|
|
1844
|
+
return { ok: false, code: "bad_params", message: "child requires a subject UUID" };
|
|
1845
|
+
return withRegistry(async () => {
|
|
1846
|
+
const params: SubjectChildGetParams = { registration_id: registrationId, subject_id };
|
|
1847
|
+
const answer = await call("subject.child.get", { ...params });
|
|
1848
|
+
return answer.ok ? subjectAt(recordAt(answer.value, "subject")) : answer;
|
|
1849
|
+
});
|
|
1850
|
+
},
|
|
1851
|
+
async childInspect(subject_id) {
|
|
1852
|
+
const registration_id = processRegistrationId;
|
|
1853
|
+
if (registration_id === undefined)
|
|
1854
|
+
return { ok: false, code: "refused", message: "this process has no Radar registration" };
|
|
1855
|
+
return withRegistry(async () => {
|
|
1856
|
+
const answer = await call("subject.child.inspect", { registration_id, subject_id });
|
|
1857
|
+
if (!answer.ok) return answer;
|
|
1858
|
+
const subject = subjectAt(recordAt(answer.value, "subject"));
|
|
1859
|
+
if (!subject.ok) return subject;
|
|
1860
|
+
const output = recordAt(answer.value, "output");
|
|
1861
|
+
const text = stringAt(output, "text");
|
|
1862
|
+
if (text === undefined || typeof recordAt(output, "truncated") !== "boolean")
|
|
1863
|
+
return protocol("child inspection has no output evidence");
|
|
1864
|
+
const foreground = recordAt(answer.value, "foreground");
|
|
1865
|
+
const activity = subject.value.execution?.snapshot?.snapshot;
|
|
1866
|
+
return { ok: true, value: {
|
|
1867
|
+
capturedAt: Date.now(), recentOutputTruncated: recordAt(output, "truncated") as boolean,
|
|
1868
|
+
...(text ? { recentOutput: text } : {}),
|
|
1869
|
+
...(foreground ? { foreground: foreground as RadarForeground } : {}),
|
|
1870
|
+
...(activity && "activity" in activity ? { activity: activity.activity } : {}),
|
|
1871
|
+
} };
|
|
1761
1872
|
});
|
|
1762
1873
|
},
|
|
1763
1874
|
async spawnList(request) {
|
|
1764
1875
|
const registrationId = processRegistrationId;
|
|
1765
1876
|
if (registrationId === undefined)
|
|
1766
1877
|
return { ok: false, code: "refused", message: "spawn.list requires this client's process registration" };
|
|
1767
|
-
return
|
|
1878
|
+
return withRegistry(async () => {
|
|
1768
1879
|
const params: SpawnListParamsDeserialize = {
|
|
1769
1880
|
registration_id: registrationId,
|
|
1770
1881
|
...(request.after !== undefined ? { after: request.after } : {}),
|
|
@@ -1,85 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type RadarAgentProcess,
|
|
3
|
-
type RadarClient,
|
|
4
|
-
type RadarForeground,
|
|
5
|
-
} from "./radar-client.ts";
|
|
6
|
-
|
|
7
|
-
// The caps the removed Herdr inspection applied; evidence is returned, never
|
|
8
|
-
// the pane's full scrollback.
|
|
9
|
-
const INSPECTION_LINES = 80;
|
|
10
|
-
const INSPECTION_OUTPUT_BYTES = 16 * 1024;
|
|
1
|
+
import type { RadarForeground } from "./radar-client.ts";
|
|
11
2
|
|
|
3
|
+
/** Bounded evidence captured and identity-checked by Radar for one child subject. */
|
|
12
4
|
export type RadarInspection = {
|
|
13
5
|
capturedAt: number;
|
|
14
6
|
recentOutputTruncated: boolean;
|
|
15
7
|
recentOutput?: string;
|
|
16
8
|
foreground?: RadarForeground;
|
|
17
|
-
/** The child's published activity from the proof read; unknown when absent. */
|
|
18
9
|
activity?: string;
|
|
19
10
|
};
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* The registration's own read when the registered child `agentId` still runs on
|
|
23
|
-
* `paneId` (and, when `session` is given, still publishes that session): a
|
|
24
|
-
* `verified` verdict. Absent, mismatched, unavailable and unreadable are not
|
|
25
|
-
* proof.
|
|
26
|
-
*/
|
|
27
|
-
export async function provedRead(
|
|
28
|
-
client: Pick<RadarClient, "agentGet">,
|
|
29
|
-
target: { agentId: string; paneId: string; session?: string },
|
|
30
|
-
): Promise<RadarAgentProcess | undefined> {
|
|
31
|
-
const read = await client.agentGet(target.agentId);
|
|
32
|
-
return read.ok === true &&
|
|
33
|
-
read.value.verification === "verified" &&
|
|
34
|
-
read.value.location?.pane === target.paneId &&
|
|
35
|
-
(target.session === undefined ||
|
|
36
|
-
read.value.contextSession === target.session)
|
|
37
|
-
? read.value
|
|
38
|
-
: undefined;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Reads a managed child's recent output and foreground process, once the
|
|
43
|
-
* registered child is proved on its pane before and after the read.
|
|
44
|
-
*/
|
|
45
|
-
export async function inspectRadarChild(
|
|
46
|
-
client: RadarClient,
|
|
47
|
-
target: { agentId: string; paneId: string; session?: string },
|
|
48
|
-
): Promise<RadarInspection> {
|
|
49
|
-
const before = await provedRead(client, target);
|
|
50
|
-
if (before === undefined)
|
|
51
|
-
throw new Error("Inspection target identity did not match");
|
|
52
|
-
const output = await client.output({
|
|
53
|
-
pane_id: target.paneId,
|
|
54
|
-
source: "recent_unwrapped",
|
|
55
|
-
lines: INSPECTION_LINES,
|
|
56
|
-
});
|
|
57
|
-
if (output.ok === false)
|
|
58
|
-
throw new Error(
|
|
59
|
-
`Inspection output unavailable (${output.code}: ${output.message})`,
|
|
60
|
-
);
|
|
61
|
-
const foreground = await client.processInfo({ pane_id: target.paneId });
|
|
62
|
-
const after = await provedRead(client, target);
|
|
63
|
-
if (after === undefined)
|
|
64
|
-
throw new Error("Inspection target changed during capture");
|
|
65
|
-
const lines = output.value.text.trim().split(/\r?\n/).slice(-INSPECTION_LINES);
|
|
66
|
-
const bounded = utf8Tail(lines.join("\n"), INSPECTION_OUTPUT_BYTES);
|
|
67
|
-
return Object.freeze({
|
|
68
|
-
capturedAt: Date.now(),
|
|
69
|
-
recentOutputTruncated: output.value.truncated || bounded.truncated,
|
|
70
|
-
...(bounded.text ? { recentOutput: bounded.text } : {}),
|
|
71
|
-
...(foreground.ok === true ? { foreground: foreground.value } : {}),
|
|
72
|
-
...(after.activity !== undefined ? { activity: after.activity } : {}),
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function utf8Tail(
|
|
77
|
-
value: string,
|
|
78
|
-
maxBytes: number,
|
|
79
|
-
): { text: string; truncated: boolean } {
|
|
80
|
-
const bytes = Buffer.from(value);
|
|
81
|
-
if (bytes.length <= maxBytes) return { text: value, truncated: false };
|
|
82
|
-
let start = bytes.length - maxBytes;
|
|
83
|
-
while (start < bytes.length && (bytes[start]! & 0xc0) === 0x80) start++;
|
|
84
|
-
return { text: bytes.subarray(start).toString(), truncated: true };
|
|
85
|
-
}
|