pi-radar 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -38,7 +38,7 @@ import { lstatSync } from "node:fs";
38
38
  import { createConnection } from "node:net";
39
39
  import { dirname, join } from "node:path";
40
40
  var RADAR_PROTOCOL_VERSION = 2;
41
- var RADAR_MINIMUM_REVISION = 1;
41
+ var RADAR_MINIMUM_REVISION = 3;
42
42
  var RADAR_REGISTRY_CONTRACT = "registry.v1";
43
43
  var RADAR_REGISTRY_FACTS_CONTRACT = "registry.facts.v1";
44
44
  var RADAR_SPAWN_CONTRACT = "spawn.v1";
@@ -258,6 +258,11 @@ function unresolvedOperation(requestId, detail) {
258
258
  };
259
259
  }
260
260
  var RADAR_SPAWN_TOKEN_ENV = "PI_RADAR_SPAWN_TOKEN";
261
+ var RADAR_MANAGED_IDENTITY_ENV = /* @__PURE__ */ new Set([
262
+ "PI_RADAR_MANAGED_SUBJECT_ID",
263
+ "PI_RADAR_MANAGED_LAUNCH_ID",
264
+ "PI_RADAR_MANAGED_EXECUTION_GENERATION"
265
+ ]);
261
266
  function unresolvedSpawn(requestId, detail) {
262
267
  return {
263
268
  ok: false,
@@ -402,9 +407,23 @@ function spawnRequestShape(request) {
402
407
  for (const [name, value] of env) {
403
408
  if (name === RADAR_SPAWN_TOKEN_ENV)
404
409
  return `spawn env cannot set the reserved ${RADAR_SPAWN_TOKEN_ENV}`;
410
+ if (RADAR_MANAGED_IDENTITY_ENV.has(name))
411
+ return `spawn env cannot set Radar-issued identity variable ${name}`;
405
412
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) || new RegExp("\\p{Cc}", "u").test(value))
406
413
  return `spawn env contains an invalid name or value: ${name}`;
407
414
  }
415
+ if (request.managed !== void 0) {
416
+ if (typeof request.managed !== "object" || request.managed === null)
417
+ return "managed launch metadata must be an object";
418
+ for (const [name, value] of Object.entries(request.managed)) {
419
+ if (!["owner", "run", "label", "role", "parent_subject_id"].includes(name))
420
+ return `unknown managed launch metadata field: ${name}`;
421
+ if (value != null && (typeof value !== "string" || value.trim() === "" || new TextEncoder().encode(value).length > 256 || new RegExp("\\p{Cc}", "u").test(value)))
422
+ return `managed launch ${name} must be non-empty, control-free, and at most 256 UTF-8 bytes`;
423
+ }
424
+ if (request.managed.parent_subject_id != null && !isUuid(request.managed.parent_subject_id))
425
+ return "managed parent_subject_id must be a canonical UUID";
426
+ }
408
427
  return void 0;
409
428
  }
410
429
  function focusOutcome(record, requestId) {
@@ -815,7 +834,8 @@ function createRadarClient(options = {}) {
815
834
  executable: request.executable,
816
835
  argv: request.argv,
817
836
  ...request.cwd !== void 0 ? { cwd: request.cwd } : {},
818
- ...carriesContext && request.env !== void 0 ? { env: request.env } : {}
837
+ ...carriesContext && request.env !== void 0 ? { env: request.env } : {},
838
+ ...request.managed !== void 0 ? { managed: request.managed } : {}
819
839
  },
820
840
  requester
821
841
  },
@@ -1173,6 +1193,77 @@ import {
1173
1193
  writeSync
1174
1194
  } from "node:fs";
1175
1195
  import { dirname as dirname2, join as join2, parse, resolve, sep } from "node:path";
1196
+
1197
+ // extension/radar-session-marker.ts
1198
+ var RADAR_LAUNCH_MARKER_TYPE = "pi-radar.launch-identity";
1199
+ var RADAR_LAUNCH_MARKER_VERSION = 1;
1200
+ var RADAR_MAX_EXECUTION_GENERATION = 4294967295;
1201
+ var RADAR_MANAGED_SUBJECT_ENV = "PI_RADAR_MANAGED_SUBJECT_ID";
1202
+ var RADAR_MANAGED_LAUNCH_ENV = "PI_RADAR_MANAGED_LAUNCH_ID";
1203
+ var RADAR_MANAGED_GENERATION_ENV = "PI_RADAR_MANAGED_EXECUTION_GENERATION";
1204
+ var UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
1205
+ function decodeRadarLaunchMarker(value) {
1206
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
1207
+ const record = value;
1208
+ if (Object.keys(record).sort().join(",") !== "execution_generation,launch_id,subject_id,version") {
1209
+ return void 0;
1210
+ }
1211
+ if (record.version !== RADAR_LAUNCH_MARKER_VERSION || typeof record.subject_id !== "string" || !UUID2.test(record.subject_id) || typeof record.launch_id !== "string" || !UUID2.test(record.launch_id) || !Number.isSafeInteger(record.execution_generation) || record.execution_generation < 1 || record.execution_generation > RADAR_MAX_EXECUTION_GENERATION) {
1212
+ return void 0;
1213
+ }
1214
+ return {
1215
+ version: RADAR_LAUNCH_MARKER_VERSION,
1216
+ subject_id: record.subject_id,
1217
+ launch_id: record.launch_id,
1218
+ execution_generation: record.execution_generation
1219
+ };
1220
+ }
1221
+ function appendRadarLaunchMarker(appendEntry, marker) {
1222
+ const validated = decodeRadarLaunchMarker(marker);
1223
+ if (!validated) throw new Error("invalid Radar launch identity marker");
1224
+ appendEntry(RADAR_LAUNCH_MARKER_TYPE, validated);
1225
+ }
1226
+ function latestRadarLaunchMarker(branch) {
1227
+ let latest;
1228
+ for (const entry of branch) {
1229
+ if (entry.type === "custom" && entry.customType === RADAR_LAUNCH_MARKER_TYPE) {
1230
+ latest = entry;
1231
+ }
1232
+ }
1233
+ if (!latest) return { status: "absent" };
1234
+ const marker = decodeRadarLaunchMarker(latest.data);
1235
+ return marker ? { status: "valid", marker } : { status: "invalid" };
1236
+ }
1237
+ function radarLaunchMarkerFromEnvironment(env = process.env) {
1238
+ const rawGeneration = env[RADAR_MANAGED_GENERATION_ENV];
1239
+ if (env[RADAR_MANAGED_SUBJECT_ENV] === void 0 || env[RADAR_MANAGED_LAUNCH_ENV] === void 0 || rawGeneration === void 0 || !/^[1-9][0-9]*$/.test(rawGeneration)) {
1240
+ return void 0;
1241
+ }
1242
+ return decodeRadarLaunchMarker({
1243
+ version: RADAR_LAUNCH_MARKER_VERSION,
1244
+ subject_id: env[RADAR_MANAGED_SUBJECT_ENV],
1245
+ launch_id: env[RADAR_MANAGED_LAUNCH_ENV],
1246
+ execution_generation: Number(rawGeneration)
1247
+ });
1248
+ }
1249
+ function activeRadarLaunchMarker(host) {
1250
+ if (!host.sessionManager) return { status: "absent" };
1251
+ return latestRadarLaunchMarker(host.sessionManager.getBranch());
1252
+ }
1253
+ function persistRadarLaunchMarker(host, env = process.env) {
1254
+ const marker = radarLaunchMarkerFromEnvironment(env);
1255
+ if (!marker) return "absent";
1256
+ if (!host.appendEntry || !host.sessionManager) return "unsupported";
1257
+ const current = latestRadarLaunchMarker(host.sessionManager.getBranch());
1258
+ if (current.status === "valid" && sameMarker(current.marker, marker)) return "unchanged";
1259
+ appendRadarLaunchMarker(host.appendEntry, marker);
1260
+ return "written";
1261
+ }
1262
+ function sameMarker(a, b) {
1263
+ return a.version === b.version && a.subject_id === b.subject_id && a.launch_id === b.launch_id && a.execution_generation === b.execution_generation;
1264
+ }
1265
+
1266
+ // extension/radar-publication.ts
1176
1267
  var RADAR_PUBLICATION_VERSION = 1;
1177
1268
  var RADAR_LEASE_MS = 3e4;
1178
1269
  var RADAR_HEARTBEAT_MS = 15e3;
@@ -1483,15 +1574,21 @@ function pendingAt(value, keys = PENDING_KEYS, codec = executionCodec) {
1483
1574
  return { sequence, snapshot, lease_ms: lease, observed_at: observed };
1484
1575
  }
1485
1576
  var CONTEXT_PENDING_KEYS = "lease_ms,observed_at,sequence,session";
1577
+ var CONTEXT_PENDING_MARKED_KEYS = "launch_identity,lease_ms,observed_at,sequence,session";
1486
1578
  var CONTEXT_ACCEPTED_KEYS = "lease_ms,observed_at,sent_at,sequence,session";
1579
+ var CONTEXT_ACCEPTED_MARKED_KEYS = "launch_identity,lease_ms,observed_at,sent_at,sequence,session";
1487
1580
  function sessionAt(value) {
1488
1581
  if (typeof value !== "object" || value === null) return void 0;
1489
1582
  const found = value.session;
1490
1583
  if (found === null) return null;
1491
1584
  return typeof found === "string" ? found : void 0;
1492
1585
  }
1493
- function contextReportAt(value, keys) {
1494
- if (!exactKeys(value, keys)) return void 0;
1586
+ function sameLaunchIdentity(a, b) {
1587
+ return a === b || a !== void 0 && b !== void 0 && a.version === b.version && a.subject_id === b.subject_id && a.launch_id === b.launch_id && a.execution_generation === b.execution_generation;
1588
+ }
1589
+ function contextReportAt(value, keys, markedKeys) {
1590
+ const marked = exactKeys(value, markedKeys);
1591
+ if (!exactKeys(value, keys) && !marked) return void 0;
1495
1592
  const sequence = numberAt2(value, "sequence");
1496
1593
  const lease = numberAt2(value, "lease_ms");
1497
1594
  const observed = textAt(value, "observed_at");
@@ -1499,7 +1596,15 @@ function contextReportAt(value, keys) {
1499
1596
  if (sequence === void 0 || sequence < 1) return void 0;
1500
1597
  if (lease === void 0 || observed === void 0 || session === void 0)
1501
1598
  return void 0;
1502
- return { sequence, session, lease_ms: lease, observed_at: observed };
1599
+ const marker = marked ? decodeRadarLaunchMarker(value.launch_identity) : void 0;
1600
+ if (marked && marker === void 0) return void 0;
1601
+ return {
1602
+ sequence,
1603
+ session,
1604
+ lease_ms: lease,
1605
+ observed_at: observed,
1606
+ ...marker !== void 0 ? { launch_identity: marker } : {}
1607
+ };
1503
1608
  }
1504
1609
  function createChannelWriter(input) {
1505
1610
  const { store, client, scheduler, recordName, channel, publisher } = input;
@@ -1738,6 +1843,7 @@ function createContextWriter(input) {
1738
1843
  const { store, client, scheduler, recordName, publisher } = input;
1739
1844
  let record = loadRecord();
1740
1845
  let desired;
1846
+ let desiredMarker;
1741
1847
  let running = false;
1742
1848
  let again = false;
1743
1849
  let recovering = false;
@@ -1763,9 +1869,17 @@ function createContextWriter(input) {
1763
1869
  const generation = numberAt2(writer, "generation");
1764
1870
  if (writer !== void 0 && (!exactKeys(writer, WRITER_KEYS) || !isUuid(handle) || generation === void 0))
1765
1871
  return blank(agentId);
1766
- const pending = contextReportAt(recordAt2(value, "pending"), CONTEXT_PENDING_KEYS);
1872
+ const pending = contextReportAt(
1873
+ recordAt2(value, "pending"),
1874
+ CONTEXT_PENDING_KEYS,
1875
+ CONTEXT_PENDING_MARKED_KEYS
1876
+ );
1767
1877
  const acceptedValue = recordAt2(value, "accepted");
1768
- const accepted = contextReportAt(acceptedValue, CONTEXT_ACCEPTED_KEYS);
1878
+ const accepted = contextReportAt(
1879
+ acceptedValue,
1880
+ CONTEXT_ACCEPTED_KEYS,
1881
+ CONTEXT_ACCEPTED_MARKED_KEYS
1882
+ );
1769
1883
  const sentAt = numberAt2(acceptedValue, "sent_at");
1770
1884
  return {
1771
1885
  version: RADAR_PUBLICATION_VERSION,
@@ -1825,7 +1939,10 @@ function createContextWriter(input) {
1825
1939
  sequence: pending.sequence,
1826
1940
  lease_ms: pending.lease_ms,
1827
1941
  observed_at: pending.observed_at,
1828
- context: { session: pending.session }
1942
+ context: {
1943
+ session: pending.session,
1944
+ ...pending.launch_identity !== void 0 ? { launch_identity: pending.launch_identity } : {}
1945
+ }
1829
1946
  });
1830
1947
  if (answer.ok === true) {
1831
1948
  recovering = false;
@@ -1856,12 +1973,13 @@ function createContextWriter(input) {
1856
1973
  return;
1857
1974
  }
1858
1975
  }
1859
- async function stage(session) {
1976
+ async function stage(session, launchIdentity) {
1860
1977
  const pending = {
1861
1978
  sequence: record.sequence + 1,
1862
1979
  session,
1863
1980
  lease_ms: input.leaseMs,
1864
- observed_at: new Date(scheduler.now()).toISOString()
1981
+ observed_at: new Date(scheduler.now()).toISOString(),
1982
+ ...launchIdentity !== void 0 ? { launch_identity: launchIdentity } : {}
1865
1983
  };
1866
1984
  record.pending = pending;
1867
1985
  save();
@@ -1876,11 +1994,11 @@ function createContextWriter(input) {
1876
1994
  if (desired === void 0) return;
1877
1995
  if (!await ensureSubject()) return;
1878
1996
  if (fenced) return;
1879
- if (record.writer === void 0 || record.accepted === void 0 || record.accepted.session !== desired) {
1880
- await stage(desired);
1997
+ if (record.writer === void 0 || record.accepted === void 0 || record.accepted.session !== desired || !sameLaunchIdentity(record.accepted.launch_identity, desiredMarker)) {
1998
+ await stage(desired, desiredMarker);
1881
1999
  return;
1882
2000
  }
1883
- if (heartbeatDue()) await stage(record.accepted.session);
2001
+ if (heartbeatDue()) await stage(record.accepted.session, desiredMarker);
1884
2002
  }
1885
2003
  async function drain() {
1886
2004
  if (running || stopped || fenced) return;
@@ -1912,9 +2030,11 @@ function createContextWriter(input) {
1912
2030
  void drain().catch(() => void 0);
1913
2031
  }
1914
2032
  return {
1915
- update(session) {
1916
- if (fenced || session === desired) return;
2033
+ update(session, launchIdentity) {
2034
+ if (fenced || session === desired && sameLaunchIdentity(desiredMarker, launchIdentity))
2035
+ return;
1917
2036
  desired = session;
2037
+ desiredMarker = launchIdentity;
1918
2038
  if (stopped) return;
1919
2039
  kick();
1920
2040
  },
@@ -2326,11 +2446,11 @@ function createRadarExecutionAdapter(pi, options) {
2326
2446
  ...lastOutcome !== void 0 ? { last_outcome: lastOutcome } : {}
2327
2447
  };
2328
2448
  }
2329
- function reportSession(context) {
2449
+ function reportSession(context, launchIdentity) {
2330
2450
  const session = context?.sessionManager?.getSessionId?.();
2331
2451
  if (!isUuid(session)) return;
2332
2452
  try {
2333
- publication.context.update(session);
2453
+ publication.context.update(session, launchIdentity);
2334
2454
  } catch {
2335
2455
  }
2336
2456
  }
@@ -2394,8 +2514,38 @@ function createRadarExecutionAdapter(pi, options) {
2394
2514
  })
2395
2515
  );
2396
2516
  on("session_start", (_event, context) => {
2517
+ const sessionHost = {
2518
+ appendEntry: pi.appendEntry ? (customType, data) => pi.appendEntry?.(customType, data) : void 0,
2519
+ sessionManager: context?.sessionManager
2520
+ };
2521
+ let launchIdentity;
2522
+ try {
2523
+ const result = persistRadarLaunchMarker(
2524
+ sessionHost,
2525
+ options.markerEnvironment
2526
+ );
2527
+ if (result === "written" || result === "unchanged") {
2528
+ const expected = radarLaunchMarkerFromEnvironment(options.markerEnvironment);
2529
+ const activeMarker = activeRadarLaunchMarker(sessionHost);
2530
+ if (expected && activeMarker.status === "valid" && JSON.stringify(activeMarker.marker) === JSON.stringify(expected)) {
2531
+ launchIdentity = activeMarker.marker;
2532
+ } else {
2533
+ options.diagnostic?.("Radar launch marker was not visible on the active Pi session branch");
2534
+ }
2535
+ } else if (result === "unsupported") {
2536
+ options.diagnostic?.("Pi host does not support Radar launch identity session entries");
2537
+ } else if (options.markerEnvironment && [
2538
+ "PI_RADAR_MANAGED_SUBJECT_ID",
2539
+ "PI_RADAR_MANAGED_LAUNCH_ID",
2540
+ "PI_RADAR_MANAGED_EXECUTION_GENERATION"
2541
+ ].some((key) => options.markerEnvironment?.[key] !== void 0)) {
2542
+ options.diagnostic?.("incomplete or invalid Radar launch identity environment");
2543
+ }
2544
+ } catch (error) {
2545
+ options.diagnostic?.(`could not persist Radar launch marker: ${String(error)}`);
2546
+ }
2397
2547
  rootSession = true;
2398
- reportSession(context);
2548
+ reportSession(context, launchIdentity);
2399
2549
  publishFacts(context);
2400
2550
  publication.reopen();
2401
2551
  active = context?.isIdle?.() === false;
@@ -2582,7 +2732,8 @@ async function resolveRadarProcessLocation(client, env = process.env) {
2582
2732
  const backend = ready.value.backend;
2583
2733
  if (backend === void 0 || backend === "none")
2584
2734
  return { state: "absent", reason: "the daemon serves no physical backend" };
2585
- const own = qualifiedOwnPane(env, backend, ready.value.instance);
2735
+ const instance = ready.value.instance ?? void 0;
2736
+ const own = qualifiedOwnPane(env, backend, instance);
2586
2737
  if ("absent" in own) return { state: "absent", reason: own.absent };
2587
2738
  if ("pending" in own) return { state: "pending", reason: own.pending };
2588
2739
  const paneId = own.pane;
@@ -2614,7 +2765,6 @@ async function resolveRadarProcessLocation(client, env = process.env) {
2614
2765
  if ("ambiguous" in containment)
2615
2766
  return { state: "pending", reason: containment.ambiguous };
2616
2767
  const row = containment.pane;
2617
- const instance = ready.value.instance;
2618
2768
  return {
2619
2769
  state: "resolved",
2620
2770
  location: {
@@ -2895,11 +3045,56 @@ async function waitForChild(client, requestId, options) {
2895
3045
  });
2896
3046
  }
2897
3047
  }
3048
+
3049
+ // extension/radar-runtime.ts
3050
+ var radar_runtime_exports = {};
3051
+ __export(radar_runtime_exports, {
3052
+ RADAR_RUNTIME_REQUEST: () => RADAR_RUNTIME_REQUEST,
3053
+ RADAR_RUNTIME_VERSION: () => RADAR_RUNTIME_VERSION,
3054
+ lazyRadarRuntime: () => lazyRadarRuntime,
3055
+ provideRadarRuntime: () => provideRadarRuntime,
3056
+ requestRadarRuntime: () => requestRadarRuntime
3057
+ });
3058
+ var RADAR_RUNTIME_REQUEST = "pi-radar/runtime-request";
3059
+ var RADAR_RUNTIME_VERSION = 1;
3060
+ function lazyRadarRuntime(start) {
3061
+ let pending;
3062
+ return {
3063
+ version: RADAR_RUNTIME_VERSION,
3064
+ start: () => pending ??= Promise.resolve().then(start)
3065
+ };
3066
+ }
3067
+ function provideRadarRuntime(events, handle) {
3068
+ return events.on(RADAR_RUNTIME_REQUEST, (value) => {
3069
+ if (!isRuntimeRequest(value)) return;
3070
+ value.provide(handle);
3071
+ });
3072
+ }
3073
+ function requestRadarRuntime(events) {
3074
+ const handles = [];
3075
+ events.emit(RADAR_RUNTIME_REQUEST, {
3076
+ version: RADAR_RUNTIME_VERSION,
3077
+ provide: (candidate) => {
3078
+ if (isRadarRuntimeHandle(candidate)) handles.push(candidate);
3079
+ }
3080
+ });
3081
+ if (handles.length === 1) return { ok: true, handle: handles[0] };
3082
+ return { ok: false, reason: handles.length === 0 ? "missing" : "duplicate" };
3083
+ }
3084
+ function isRuntimeRequest(value) {
3085
+ return typeof value === "object" && value !== null && value.version === RADAR_RUNTIME_VERSION && typeof value.provide === "function";
3086
+ }
3087
+ function isRadarRuntimeHandle(value) {
3088
+ if (typeof value !== "object" || value === null) return false;
3089
+ const handle = value;
3090
+ return handle.version === RADAR_RUNTIME_VERSION && typeof handle.start === "function";
3091
+ }
2898
3092
  export {
2899
3093
  radar_client_exports as client,
2900
3094
  radar_execution_exports as execution,
2901
3095
  radar_inspection_exports as inspection,
2902
3096
  radar_launch_exports as launch,
2903
3097
  radar_location_exports as location,
2904
- radar_publication_exports as publication
3098
+ radar_publication_exports as publication,
3099
+ radar_runtime_exports as runtime
2905
3100
  };
@@ -0,0 +1,37 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { createRadarClient } from "./radar-client.ts";
4
+ import {
5
+ createRadarExecutionAdapter,
6
+ type RadarExecutionEventApi,
7
+ } from "./radar-execution.ts";
8
+ import { resolveRadarProcessLocation } from "./radar-location.ts";
9
+ import {
10
+ lazyRadarRuntime,
11
+ provideRadarRuntime,
12
+ type RadarExtensionEventBus,
13
+ } from "./radar-runtime.ts";
14
+
15
+ /** Minimal structural Pi API used by the baseline extension. */
16
+ type PiRadarHost = RadarExecutionEventApi & {
17
+ events: RadarExtensionEventBus;
18
+ };
19
+
20
+ /**
21
+ * Register pi-radar as the one baseline registrar for this Pi process.
22
+ * Herdsman integrations request the same session-scoped runtime handle over
23
+ * `pi.events`; they add orchestration data without creating another publisher.
24
+ */
25
+ export default function registerPiRadar(pi: PiRadarHost): void {
26
+ const client = createRadarClient();
27
+ const execution = createRadarExecutionAdapter(pi, {
28
+ client,
29
+ dataRoot: join(homedir(), ".pi", "agent", "pi-radar"),
30
+ location: () => resolveRadarProcessLocation(client),
31
+ });
32
+ const runtime = lazyRadarRuntime(async () => ({
33
+ publication: execution.publication,
34
+ execution,
35
+ }));
36
+ provideRadarRuntime(pi.events, runtime);
37
+ }
@@ -19,6 +19,27 @@ export interface TaskLaneStatus {
19
19
  available: boolean;
20
20
  error: string | null;
21
21
  }
22
+ /**
23
+ * Managed owner/parent metadata accepted with a spawn intent. Domain validation
24
+ * and parent-binding checks remain in the daemon handler.
25
+ */
26
+ export interface ManagedLaunchMetadataDeserialize {
27
+ label?: string | null;
28
+ owner?: string | null;
29
+ parent_subject_id?: string | null;
30
+ role?: string | null;
31
+ run?: string | null;
32
+ }
33
+ /**
34
+ * A non-secret Pi-session breadcrumb issued by Radar. This is correlation
35
+ * evidence only and cannot establish lifecycle authority.
36
+ */
37
+ export interface LaunchIdentityMarkerSerialize {
38
+ execution_generation: number;
39
+ launch_id: string;
40
+ subject_id: string;
41
+ version: number;
42
+ }
22
43
  /**
23
44
  * The agent's own process facts. Self-authored by any Pi session, with no owner
24
45
  * or assignment, and kept separate from execution and assignment. Every field
@@ -4,7 +4,9 @@ import { createConnection } from "node:net";
4
4
  import { dirname, join } from "node:path";
5
5
  import type {
6
6
  DaemonPingSerialize,
7
+ LaunchIdentityMarkerSerialize,
7
8
  FactsSnapshotDeserialize,
9
+ ManagedLaunchMetadataDeserialize,
8
10
  SpawnExitObservationSerialize,
9
11
  } from "./generated-contracts.ts";
10
12
 
@@ -26,7 +28,7 @@ import type {
26
28
  */
27
29
  export const RADAR_PROTOCOL_VERSION = 2;
28
30
  /** The minimum daemon wire revision this client understands. */
29
- export const RADAR_MINIMUM_REVISION = 1;
31
+ export const RADAR_MINIMUM_REVISION = 3;
30
32
  /** Public daemon contracts this client may preflight. */
31
33
  export const RADAR_REGISTRY_CONTRACT = "registry.v1";
32
34
  export const RADAR_REGISTRY_FACTS_CONTRACT = "registry.facts.v1";
@@ -194,6 +196,7 @@ export type RadarWriterBinding = {
194
196
  */
195
197
  export type RadarContextValue = {
196
198
  session: string | null;
199
+ launch_identity?: LaunchIdentityMarkerSerialize;
197
200
  };
198
201
 
199
202
  /**
@@ -296,6 +299,10 @@ export type RadarSpawnEdge = {
296
299
  * no exit was observed.
297
300
  */
298
301
  export type RadarSpawnExitObservation = SpawnExitObservationSerialize;
302
+ export type RadarLaunchIdentityMarker = LaunchIdentityMarkerSerialize;
303
+
304
+ /** Durable Radar-owned metadata accepted with a managed launch intent. */
305
+ export type RadarManagedLaunchMetadata = ManagedLaunchMetadataDeserialize;
299
306
 
300
307
  /**
301
308
  * One edge with what *that read* learned about the location it recorded.
@@ -651,6 +658,11 @@ function unresolvedOperation(requestId: string, detail: string): RadarResult<nev
651
658
  }
652
659
 
653
660
  export const RADAR_SPAWN_TOKEN_ENV = "PI_RADAR_SPAWN_TOKEN";
661
+ const RADAR_MANAGED_IDENTITY_ENV = new Set([
662
+ "PI_RADAR_MANAGED_SUBJECT_ID",
663
+ "PI_RADAR_MANAGED_LAUNCH_ID",
664
+ "PI_RADAR_MANAGED_EXECUTION_GENERATION",
665
+ ]);
654
666
 
655
667
  /**
656
668
  * A spawn this process may still have caused.
@@ -857,6 +869,7 @@ function spawnRequestShape(request: {
857
869
  argv: string[];
858
870
  cwd?: string;
859
871
  env?: Record<string, string>;
872
+ managed?: RadarManagedLaunchMetadata;
860
873
  }): string | undefined {
861
874
  if (!isUuid(request.request_id))
862
875
  return "spawn requires a canonical request UUID";
@@ -875,9 +888,26 @@ function spawnRequestShape(request: {
875
888
  for (const [name, value] of env) {
876
889
  if (name === RADAR_SPAWN_TOKEN_ENV)
877
890
  return `spawn env cannot set the reserved ${RADAR_SPAWN_TOKEN_ENV}`;
891
+ if (RADAR_MANAGED_IDENTITY_ENV.has(name))
892
+ return `spawn env cannot set Radar-issued identity variable ${name}`;
878
893
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) || /\p{Cc}/u.test(value))
879
894
  return `spawn env contains an invalid name or value: ${name}`;
880
895
  }
896
+ if (request.managed !== undefined) {
897
+ if (typeof request.managed !== "object" || request.managed === null)
898
+ return "managed launch metadata must be an object";
899
+ for (const [name, value] of Object.entries(request.managed)) {
900
+ if (!["owner", "run", "label", "role", "parent_subject_id"].includes(name))
901
+ return `unknown managed launch metadata field: ${name}`;
902
+ if (value != null && (typeof value !== "string" || value.trim() === "" || new TextEncoder().encode(value).length > 256 || /\p{Cc}/u.test(value)))
903
+ return `managed launch ${name} must be non-empty, control-free, and at most 256 UTF-8 bytes`;
904
+ }
905
+ if (
906
+ request.managed.parent_subject_id != null &&
907
+ !isUuid(request.managed.parent_subject_id)
908
+ )
909
+ return "managed parent_subject_id must be a canonical UUID";
910
+ }
881
911
  return undefined;
882
912
  }
883
913
 
@@ -978,6 +1008,7 @@ export type RadarClient = {
978
1008
  argv: string[];
979
1009
  cwd?: string;
980
1010
  env?: Record<string, string>;
1011
+ managed?: RadarManagedLaunchMetadata;
981
1012
  }): Promise<RadarResult<RadarSpawnEdge>>;
982
1013
  spawnGet(request_id: string): Promise<RadarResult<RadarSpawnTopology>>;
983
1014
  /** The registered child's process verdict, read from its immutable claim. */
@@ -1513,6 +1544,9 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
1513
1544
  ...(carriesContext && request.env !== undefined
1514
1545
  ? { env: request.env }
1515
1546
  : {}),
1547
+ ...(request.managed !== undefined
1548
+ ? { managed: request.managed }
1549
+ : {}),
1516
1550
  },
1517
1551
  requester,
1518
1552
  },
@@ -3,6 +3,7 @@ import {
3
3
  isUuid,
4
4
  type RadarScheduler,
5
5
  type RadarSnapshot,
6
+ type RadarLaunchIdentityMarker,
6
7
  } from "./radar-client.ts";
7
8
  import type { RadarLocationResolution } from "./radar-location.ts";
8
9
  import type { RadarFactsSnapshot } from "./radar-client.ts";
@@ -19,6 +20,11 @@ import {
19
20
  type RadarPublication,
20
21
  type RadarChannelWriter,
21
22
  } from "./radar-publication.ts";
23
+ import {
24
+ activeRadarLaunchMarker,
25
+ persistRadarLaunchMarker,
26
+ radarLaunchMarkerFromEnvironment,
27
+ } from "./radar-session-marker.ts";
22
28
 
23
29
  /**
24
30
  * The minimal Pi surface this adapter uses. It is declared structurally so the
@@ -26,6 +32,7 @@ import {
26
32
  * be lifted into its own extension without touching this module.
27
33
  */
28
34
  export type RadarExecutionEventApi = {
35
+ appendEntry?: (customType: string, data?: unknown) => void;
29
36
  on(name: string, handler: (event: any, context: any) => unknown): unknown;
30
37
  events?: {
31
38
  on(name: string, handler: (event: any) => unknown): unknown;
@@ -63,6 +70,10 @@ export type RadarExecutionOptions = {
63
70
  * where no such event exists.
64
71
  */
65
72
  questionnaireEvent?: string;
73
+ /** Best-effort diagnostic for durable session marker failures. */
74
+ diagnostic?: (message: string) => void;
75
+ /** Injectable only for tests; production reads the daemon-issued process env. */
76
+ markerEnvironment?: Record<string, string | undefined>;
66
77
  };
67
78
 
68
79
  export type RadarExecutionAdapter = {
@@ -258,11 +269,14 @@ export function createRadarExecutionAdapter(
258
269
  * canonical UUID is left unreported: absent context is unknown, and publishing
259
270
  * the explicit null would claim there is no session at all.
260
271
  */
261
- function reportSession(context: any): void {
272
+ function reportSession(
273
+ context: any,
274
+ launchIdentity?: RadarLaunchIdentityMarker,
275
+ ): void {
262
276
  const session = context?.sessionManager?.getSessionId?.();
263
277
  if (!isUuid(session)) return;
264
278
  try {
265
- publication.context.update(session);
279
+ publication.context.update(session, launchIdentity);
266
280
  } catch {
267
281
  // Optional publication must not interfere with Pi's lifecycle handler.
268
282
  }
@@ -346,10 +360,49 @@ export function createRadarExecutionAdapter(
346
360
  );
347
361
 
348
362
  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
+ }
349
402
  rootSession = true;
350
403
  // The session comes first: re-arming the publication must renew the session
351
404
  // this process is in now, never the one a previous session left behind.
352
- reportSession(context);
405
+ reportSession(context, launchIdentity);
353
406
  // Facts follow the root flag, so they are published only after it is set.
354
407
  publishFacts(context);
355
408
  // A process can replace or resume its session without a new process, so the
@@ -153,7 +153,8 @@ export async function resolveRadarProcessLocation(
153
153
  const backend = ready.value.backend;
154
154
  if (backend === undefined || backend === "none")
155
155
  return { state: "absent", reason: "the daemon serves no physical backend" };
156
- const own = qualifiedOwnPane(env, backend, ready.value.instance);
156
+ const instance = ready.value.instance ?? undefined;
157
+ const own = qualifiedOwnPane(env, backend, instance);
157
158
  if ("absent" in own) return { state: "absent", reason: own.absent };
158
159
  if ("pending" in own) return { state: "pending", reason: own.pending };
159
160
  const paneId = own.pane;
@@ -186,7 +187,6 @@ export async function resolveRadarProcessLocation(
186
187
  if ("ambiguous" in containment)
187
188
  return { state: "pending", reason: containment.ambiguous };
188
189
  const row = containment.pane;
189
- const instance = ready.value.instance;
190
190
  return {
191
191
  state: "resolved",
192
192
  location: {