@geohar/un-bien 0.8.0 → 0.9.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.
Files changed (83) hide show
  1. package/README.md +223 -254
  2. package/dist/bin/launcher.js +38 -0
  3. package/dist/bin/launcher.js.map +1 -0
  4. package/dist/config.d.ts +28 -1
  5. package/dist/config.js.map +1 -1
  6. package/dist/daemon/install.d.ts +33 -35
  7. package/dist/daemon/install.js +43 -73
  8. package/dist/daemon/install.js.map +1 -1
  9. package/dist/enrich_tool_args.d.ts +9 -0
  10. package/dist/enrich_tool_args.js +171 -0
  11. package/dist/enrich_tool_args.js.map +1 -0
  12. package/dist/image_codec.d.ts +22 -0
  13. package/dist/image_codec.js +149 -0
  14. package/dist/image_codec.js.map +1 -0
  15. package/dist/index.d.ts +0 -26
  16. package/dist/index.js +200 -1236
  17. package/dist/index.js.map +1 -1
  18. package/dist/launch.d.ts +34 -0
  19. package/dist/launch.js +171 -0
  20. package/dist/launch.js.map +1 -0
  21. package/dist/launcher/launcher.d.ts +6 -0
  22. package/dist/launcher/launcher.js +188 -0
  23. package/dist/launcher/launcher.js.map +1 -0
  24. package/dist/pairing/peer_trust.d.ts +20 -0
  25. package/dist/pairing/peer_trust.js +82 -0
  26. package/dist/pairing/peer_trust.js.map +1 -0
  27. package/dist/panel_bridge.d.ts +3 -1
  28. package/dist/panel_bridge.js +10 -10
  29. package/dist/panel_bridge.js.map +1 -1
  30. package/dist/rooms.d.ts +16 -0
  31. package/dist/rooms.js +22 -0
  32. package/dist/rooms.js.map +1 -1
  33. package/dist/session/debug_log.js +2 -2
  34. package/dist/session/debug_log.js.map +1 -1
  35. package/dist/session/local_config.js +2 -0
  36. package/dist/session/local_config.js.map +1 -1
  37. package/dist/session/rpc_envelope.d.ts +18 -0
  38. package/dist/session/rpc_envelope.js +1 -1
  39. package/dist/session/rpc_envelope.js.map +1 -1
  40. package/dist/session/rpc_inbound.d.ts +2 -2
  41. package/dist/session/rpc_inbound.js +1 -1
  42. package/dist/session/rpc_inbound.js.map +1 -1
  43. package/dist/subagent_rooms.d.ts +49 -0
  44. package/dist/subagent_rooms.js +787 -0
  45. package/dist/subagent_rooms.js.map +1 -0
  46. package/dist/transport/peer_channel.d.ts +7 -1
  47. package/dist/transport/peer_channel.js +8 -1
  48. package/dist/transport/peer_channel.js.map +1 -1
  49. package/dist/transport/relay_client.d.ts +19 -0
  50. package/dist/transport/relay_client.js.map +1 -1
  51. package/docs/daemon.md +65 -64
  52. package/package.json +13 -11
  53. package/service-templates/launchd.plist.template +5 -5
  54. package/service-templates/systemd.service.template +2 -2
  55. package/service-templates/task-launcher.vbs.template +5 -5
  56. package/service-templates/task-scheduler.xml.template +1 -1
  57. package/dist/bin/supervisord.js +0 -86
  58. package/dist/bin/supervisord.js.map +0 -1
  59. package/dist/daemon/client.d.ts +0 -20
  60. package/dist/daemon/client.js +0 -131
  61. package/dist/daemon/client.js.map +0 -1
  62. package/dist/daemon/control_protocol.d.ts +0 -176
  63. package/dist/daemon/control_protocol.js +0 -63
  64. package/dist/daemon/control_protocol.js.map +0 -1
  65. package/dist/daemon/cron_log.d.ts +0 -45
  66. package/dist/daemon/cron_log.js +0 -71
  67. package/dist/daemon/cron_log.js.map +0 -1
  68. package/dist/daemon/cron_registry.d.ts +0 -80
  69. package/dist/daemon/cron_registry.js +0 -198
  70. package/dist/daemon/cron_registry.js.map +0 -1
  71. package/dist/daemon/id.d.ts +0 -24
  72. package/dist/daemon/id.js +0 -36
  73. package/dist/daemon/id.js.map +0 -1
  74. package/dist/daemon/registry.d.ts +0 -63
  75. package/dist/daemon/registry.js +0 -153
  76. package/dist/daemon/registry.js.map +0 -1
  77. package/dist/daemon/rpc_child.d.ts +0 -194
  78. package/dist/daemon/rpc_child.js +0 -419
  79. package/dist/daemon/rpc_child.js.map +0 -1
  80. package/dist/daemon/supervisor.d.ts +0 -93
  81. package/dist/daemon/supervisor.js +0 -666
  82. package/dist/daemon/supervisor.js.map +0 -1
  83. /package/dist/bin/{supervisord.d.ts → launcher.d.ts} +0 -0
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Launch backends for remote `session_launch` — shared by the extension (index.ts)
3
+ * and the regime-2 launcher daemon. PTY-hosted backends exec `pi` cleanly (no
4
+ * keystrokes): `tmux` = a WINDOW in one shared named session; `herdr` =
5
+ * `workspace create` then `agent start --kind pi`. `rpc` is a fast-follow.
6
+ */
7
+ /** Expand a leading `~`/`~/` to the extension machine's home dir. Node's `fs` does
8
+ * NOT expand `~`, so a launch cwd like `~/proj` would fail the existsSync
9
+ * check and silently abort the launch. Machine-side (the phone's `~` is
10
+ * meaningless here). Exported for tests. */
11
+ export declare function _expandTilde(p: string): string;
12
+ /** Build the argv for launching `pi` in `cwd` as a WINDOW of the shared tmux
13
+ * session `session` (one named session, a window per pi — clean `new-window`,
14
+ * NO keystrokes/prefix). First launch creates the detached session; later ones
15
+ * add a window. Array (never a shell string) so cwd/names can't inject.
16
+ * Exported for tests. */
17
+ export declare function _buildTmuxLaunchArgs(session: string, windowName: string, cwd: string, sessionExists: boolean): string[];
18
+ /** argv for creating a detached herdr workspace in `cwd` (JSON response).
19
+ * ARRAY (never a shell string) so cwd/label can't inject. Exported for tests. */
20
+ export declare function _buildHerdrWorkspaceArgs(label: string, cwd: string): string[];
21
+ /** argv for exec-launching `pi` as a named herdr agent in an existing pane —
22
+ * herdr's canonical-executable launch (NOT keystroke injection). */
23
+ export declare function _buildHerdrAgentStartArgs(agentName: string, paneId: string): string[];
24
+ /** Extract `.result.root_pane.pane_id` from `herdr workspace create --json`. */
25
+ export declare function _herdrPaneIdFromCreate(stdout: string): string | null;
26
+ /**
27
+ * Honor a `session_launch` request. The caller checked the per-cwd opt-in and
28
+ * picked `mode` from `launch.backend` (machine config). PTY-hosted backends
29
+ * exec `pi` cleanly (no keystrokes): `tmux` = detached `new-session … pi`;
30
+ * `herdr` = `workspace create` then `agent start --kind pi`. `rpc` is a
31
+ * fast-follow (stubbed). Returns null when the launch is initiated, else an
32
+ * error string.
33
+ */
34
+ export declare function _launchSession(mode: "tmux" | "herdr" | "rpc", cwd: string, name: string | undefined): string | null;
package/dist/launch.js ADDED
@@ -0,0 +1,171 @@
1
+ import { spawn, spawnSync, execFile } from "node:child_process";
2
+ import { basename, join } from "node:path";
3
+ import { existsSync, statSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { loadConfig } from "./config.js";
6
+ import { envLog } from "./session/debug_log.js";
7
+ /**
8
+ * Launch backends for remote `session_launch` — shared by the extension (index.ts)
9
+ * and the regime-2 launcher daemon. PTY-hosted backends exec `pi` cleanly (no
10
+ * keystrokes): `tmux` = a WINDOW in one shared named session; `herdr` =
11
+ * `workspace create` then `agent start --kind pi`. `rpc` is a fast-follow.
12
+ */
13
+ /** Expand a leading `~`/`~/` to the extension machine's home dir. Node's `fs` does
14
+ * NOT expand `~`, so a launch cwd like `~/proj` would fail the existsSync
15
+ * check and silently abort the launch. Machine-side (the phone's `~` is
16
+ * meaningless here). Exported for tests. */
17
+ export function _expandTilde(p) {
18
+ if (p === "~")
19
+ return homedir();
20
+ if (p.startsWith("~/"))
21
+ return join(homedir(), p.slice(2));
22
+ return p;
23
+ }
24
+ /** Sanitize a tmux session name: keep it shell-safe and tmux-legal. */
25
+ function _safeTmuxName(name, fallback) {
26
+ const base = (name ?? "").trim() || fallback;
27
+ // tmux disallows `.` and `:` in session names; strip anything risky.
28
+ const clean = base
29
+ .replace(/[^A-Za-z0-9_-]+/g, "-")
30
+ .replace(/-{2,}/g, "-")
31
+ .replace(/^-+|-+$/g, "");
32
+ return clean.slice(0, 40) || "pi";
33
+ }
34
+ /** Build the argv for launching `pi` in `cwd` as a WINDOW of the shared tmux
35
+ * session `session` (one named session, a window per pi — clean `new-window`,
36
+ * NO keystrokes/prefix). First launch creates the detached session; later ones
37
+ * add a window. Array (never a shell string) so cwd/names can't inject.
38
+ * Exported for tests. */
39
+ export function _buildTmuxLaunchArgs(session, windowName, cwd, sessionExists) {
40
+ return sessionExists
41
+ ? ["new-window", "-t", session, "-n", windowName, "-c", cwd, "pi"]
42
+ : ["new-session", "-d", "-s", session, "-n", windowName, "-c", cwd, "pi"];
43
+ }
44
+ /** Sanitize a herdr agent name: must match [a-z][a-z0-9_-]{0,31}. */
45
+ function _safeHerdrName(name, fallback) {
46
+ const base = (name ?? "").trim().toLowerCase() || fallback;
47
+ let clean = base
48
+ .replace(/[^a-z0-9_-]+/g, "-")
49
+ .replace(/-{2,}/g, "-")
50
+ .replace(/^-+|-+$/g, "");
51
+ if (!/^[a-z]/.test(clean))
52
+ clean = `pi-${clean}`;
53
+ return clean.slice(0, 32).replace(/-+$/g, "") || "pi";
54
+ }
55
+ /** argv for creating a detached herdr workspace in `cwd` (JSON response).
56
+ * ARRAY (never a shell string) so cwd/label can't inject. Exported for tests. */
57
+ export function _buildHerdrWorkspaceArgs(label, cwd) {
58
+ return [
59
+ "workspace",
60
+ "create",
61
+ "--cwd",
62
+ cwd,
63
+ "--label",
64
+ label,
65
+ "--no-focus",
66
+ "--json",
67
+ ];
68
+ }
69
+ /** argv for exec-launching `pi` as a named herdr agent in an existing pane —
70
+ * herdr's canonical-executable launch (NOT keystroke injection). */
71
+ export function _buildHerdrAgentStartArgs(agentName, paneId) {
72
+ return ["agent", "start", agentName, "--kind", "pi", "--pane", paneId];
73
+ }
74
+ /** Extract `.result.root_pane.pane_id` from `herdr workspace create --json`. */
75
+ export function _herdrPaneIdFromCreate(stdout) {
76
+ try {
77
+ const j = JSON.parse(stdout);
78
+ const id = j.result?.root_pane?.pane_id;
79
+ return typeof id === "string" && id.length > 0 ? id : null;
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ }
85
+ /** Is a launch backend's binary present on PATH? A `--version` probe: a missing
86
+ * binary (ENOENT) sets `error`; anything that runs counts as present. */
87
+ function _backendAvailable(backend) {
88
+ try {
89
+ return (spawnSync(backend, ["--version"], {
90
+ stdio: "ignore",
91
+ timeout: 5_000,
92
+ }).error === undefined);
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ }
98
+ function _execFileCapture(cmd, args) {
99
+ return new Promise((resolve, reject) => {
100
+ execFile(cmd, args, { timeout: 15_000 }, (err, stdout) => {
101
+ if (err)
102
+ reject(err);
103
+ else
104
+ resolve(stdout);
105
+ });
106
+ });
107
+ }
108
+ /**
109
+ * herdr launch (clean exec, no keystrokes): create a detached workspace in
110
+ * `cwd`, then start `pi` as its agent via herdr's canonical-executable path
111
+ * (`agent start --kind pi`). Fire-and-forget — the launched pi joins the relay
112
+ * and the app attaches there; create/start errors are logged, not returned.
113
+ */
114
+ async function _launchHerdr(cwd, agentName) {
115
+ try {
116
+ const created = await _execFileCapture("herdr", _buildHerdrWorkspaceArgs(agentName, cwd));
117
+ const paneId = _herdrPaneIdFromCreate(created);
118
+ if (!paneId) {
119
+ envLog("herdr launch: no root_pane_id in `workspace create` output");
120
+ return;
121
+ }
122
+ await _execFileCapture("herdr", _buildHerdrAgentStartArgs(agentName, paneId));
123
+ envLog(`herdr launch: agent '${agentName}' started in pane ${paneId}`);
124
+ }
125
+ catch (error) {
126
+ envLog(`herdr launch failed: ${error instanceof Error ? error.message : String(error)}`);
127
+ }
128
+ }
129
+ /**
130
+ * Honor a `session_launch` request. The caller checked the per-cwd opt-in and
131
+ * picked `mode` from `launch.backend` (machine config). PTY-hosted backends
132
+ * exec `pi` cleanly (no keystrokes): `tmux` = detached `new-session … pi`;
133
+ * `herdr` = `workspace create` then `agent start --kind pi`. `rpc` is a
134
+ * fast-follow (stubbed). Returns null when the launch is initiated, else an
135
+ * error string.
136
+ */
137
+ export function _launchSession(mode, cwd, name) {
138
+ if (mode === "rpc")
139
+ return "launch mode 'rpc' is not supported yet";
140
+ if (mode !== "tmux" && mode !== "herdr") {
141
+ return `unknown launch mode '${mode}'`;
142
+ }
143
+ if (!existsSync(cwd) || !statSync(cwd).isDirectory()) {
144
+ return `cwd does not exist or is not a directory: ${cwd}`;
145
+ }
146
+ if (!_backendAvailable(mode)) {
147
+ return `launch backend '${mode}' is not installed`;
148
+ }
149
+ if (mode === "herdr") {
150
+ const agentName = _safeHerdrName(name, `pi-${basename(cwd) || "session"}`);
151
+ void _launchHerdr(cwd, agentName);
152
+ return null;
153
+ }
154
+ // One shared, named tmux session; each launch is a WINDOW in it (single
155
+ // attach point). Clean `new-window` via the CLI — never a prefix keystroke.
156
+ const session = _safeTmuxName(loadConfig().launch?.tmux_session, "un-bien");
157
+ const windowName = _safeTmuxName(name, `pi-${basename(cwd) || "session"}`);
158
+ const sessionExists = spawnSync("tmux", ["has-session", "-t", session], {
159
+ stdio: "ignore",
160
+ timeout: 5_000,
161
+ }).status === 0;
162
+ try {
163
+ const child = spawn("tmux", _buildTmuxLaunchArgs(session, windowName, cwd, sessionExists), { detached: true, stdio: "ignore" });
164
+ child.unref();
165
+ return null;
166
+ }
167
+ catch (error) {
168
+ return `tmux launch failed: ${error instanceof Error ? error.message : String(error)}`;
169
+ }
170
+ }
171
+ //# sourceMappingURL=launch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"launch.js","sourceRoot":"","sources":["../src/launch.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export interface LauncherHandle {
2
+ readonly roomId: string;
3
+ readonly epk: string;
4
+ stop(): void;
5
+ }
6
+ export declare function startLauncher(): Promise<LauncherHandle>;
@@ -0,0 +1,188 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { hostname, homedir } from "node:os";
3
+ import { RelayClient } from "../transport/relay_client.js";
4
+ import { PlainPeerChannel } from "../transport/peer_channel.js";
5
+ import { getOrCreateEd25519Keypair } from "../pairing/storage.js";
6
+ import { roomIdForControl } from "../rooms.js";
7
+ import { _findKnownPeer } from "../pairing/peer_trust.js";
8
+ import { _launchSession, _expandTilde } from "../launch.js";
9
+ import { helloEnvelope, isEnvelopeFrame, } from "../session/rpc_envelope.js";
10
+ import { loadConfig, resolveRelayUrl } from "../config.js";
11
+ import { loadLocalConfig, effectiveAllowRemoteLaunch, } from "../session/local_config.js";
12
+ import { envLog } from "../session/debug_log.js";
13
+ /**
14
+ * Regime-2 machine-launcher core (pi-INDEPENDENT — no pi SDK). A lightweight
15
+ * mesh peer that lets a paired app reach a machine with NO live pi session:
16
+ * it joins the machine-level control room (roomIdForControl), advertises the
17
+ * `remote_launch` capability, and on a `session_launch` frame spawns a
18
+ * tmux/herdr window via the shared launch module. Owner-auth reuses the extension's
19
+ * exact gate (relay verifies the peer's key; _findKnownPeer checks peers.json).
20
+ *
21
+ * NOT a pi process: no transcript/rpc/panels, no pairing (pairing happens once
22
+ * via any session extension's QR — the launcher only trusts already-paired owners).
23
+ */
24
+ const RECONNECT_DELAY_MS = 3_000;
25
+ /** Caps the launcher daemon advertises: `remote_launch` gates the app's launch
26
+ * control; `is_daemon` marks the control room so the app filters it. */
27
+ const DAEMON_CAPS = ["remote_launch", "is_daemon"];
28
+ export async function startLauncher() {
29
+ const kp = await getOrCreateEd25519Keypair();
30
+ const epk = Buffer.from(kp.publicKey).toString("base64url");
31
+ const roomId = roomIdForControl(epk);
32
+ const resolution = resolveRelayUrl();
33
+ if (!resolution.url) {
34
+ throw new Error("un-bien launcher: no relay configured (set UNBIEN_RELAY or the `relay` config key)");
35
+ }
36
+ const relayUrl = resolution.url;
37
+ let stopped = false;
38
+ let relay = null;
39
+ const channels = new Map();
40
+ /** The machine's configured launch backend (rpc is a fast-follow). */
41
+ function configuredBackend() {
42
+ return loadConfig().launch?.backend === "herdr" ? "herdr" : "tmux";
43
+ }
44
+ /** Liveness: answer a stock `ping` with `pong`, mirroring the extension's
45
+ * transport-control handler (index.ts) so a health check works against an
46
+ * idle machine too. Distinct from the `presence_status` caps PULL — this is
47
+ * the plain are-you-there reply, no caps. */
48
+ function handleStockMessage(msg, sender) {
49
+ if (msg.type === "ping") {
50
+ sender.send({
51
+ type: "pong",
52
+ in_reply_to: msg.id,
53
+ });
54
+ }
55
+ }
56
+ /** Handle a ub-frame from an attached owner. `presence_status` is the
57
+ * DAEMON-SPECIFIC caps PULL (design 01M1813Q) — reply with caps + hostname +
58
+ * backend. `session_launch` mirrors the extension's _routeUnBienPlaneFrom handler:
59
+ * per-cwd opt-in, machine-config backend, clean exec (no keystrokes). */
60
+ function handleUbFrame(env, sender) {
61
+ if (env.ub === undefined)
62
+ return;
63
+ const frame = env.ub;
64
+ if (frame.type === "presence_status") {
65
+ sender.sendEnvelope({
66
+ ub: {
67
+ type: "presence_status",
68
+ caps: [...DAEMON_CAPS],
69
+ hostname: hostname(),
70
+ backend: configuredBackend(),
71
+ ...(typeof frame.id === "string" ? { in_reply_to: frame.id } : {}),
72
+ },
73
+ });
74
+ return;
75
+ }
76
+ if (frame.type !== "session_launch")
77
+ return;
78
+ const cwd = _expandTilde(typeof frame.cwd === "string" && frame.cwd.length > 0
79
+ ? frame.cwd
80
+ : process.cwd());
81
+ if (!effectiveAllowRemoteLaunch(loadLocalConfig(cwd))) {
82
+ envLog("launcher session_launch: remote launch disabled on this machine");
83
+ return;
84
+ }
85
+ const launchError = _launchSession(configuredBackend(), cwd, typeof frame.name === "string" ? frame.name : undefined);
86
+ if (launchError)
87
+ envLog(`launcher session_launch error: ${launchError}`);
88
+ }
89
+ async function gateAndAttach(r, peer, firstInner) {
90
+ const known = await _findKnownPeer(peer);
91
+ if (!known) {
92
+ // Relay-verified but not a paired owner (never paired / revoked). Signal
93
+ // so the app can react, mirroring the extension's unknown-peer reply.
94
+ try {
95
+ const errCt = Buffer.from(JSON.stringify({
96
+ type: "error",
97
+ code: "unknown_peer",
98
+ message: "Peer not paired — re-scan QR",
99
+ })).toString("base64");
100
+ r.send(JSON.stringify({ peer, ct: errCt }));
101
+ }
102
+ catch {
103
+ /* relay down — nothing to signal */
104
+ }
105
+ return;
106
+ }
107
+ if (channels.has(peer))
108
+ return;
109
+ const channel = new PlainPeerChannel(r, peer, roomId, (msg) => handleStockMessage(msg, channel), // liveness ping->pong
110
+ () => channels.delete(peer), (env) => handleUbFrame(env, channel));
111
+ channels.set(peer, channel);
112
+ // Advertise machine caps up front so the app enables its launch control for
113
+ // this control room. No sessionId — the launcher has no pi session.
114
+ channel.sendEnvelope(helloEnvelope([...DAEMON_CAPS]));
115
+ envLog(`launcher: owner ${peer.slice(0, 8)} (${known.name}) attached; caps sent`);
116
+ // The channel didn't see the line that triggered the attach — route it.
117
+ if (isEnvelopeFrame(firstInner)) {
118
+ handleUbFrame(firstInner, channel);
119
+ }
120
+ }
121
+ function onMsg(r, line) {
122
+ let outer;
123
+ try {
124
+ outer = JSON.parse(line);
125
+ }
126
+ catch {
127
+ return;
128
+ }
129
+ if (!outer.peer || !outer.ct)
130
+ return;
131
+ if (channels.has(outer.peer))
132
+ return; // its PlainPeerChannel handles routing
133
+ let inner;
134
+ try {
135
+ inner = JSON.parse(Buffer.from(outer.ct, "base64").toString("utf8"));
136
+ }
137
+ catch {
138
+ return;
139
+ }
140
+ if (!inner || typeof inner !== "object")
141
+ return;
142
+ void gateAndAttach(r, outer.peer, inner);
143
+ }
144
+ function scheduleReconnect() {
145
+ if (stopped)
146
+ return;
147
+ setTimeout(() => {
148
+ if (stopped)
149
+ return;
150
+ connectOnce().catch(() => scheduleReconnect());
151
+ }, RECONNECT_DELAY_MS);
152
+ }
153
+ async function connectOnce() {
154
+ const r = new RelayClient(relayUrl, kp);
155
+ relay = r;
156
+ r.on("message", (line) => onMsg(r, line));
157
+ r.on("close", () => {
158
+ channels.clear();
159
+ if (!stopped)
160
+ scheduleReconnect();
161
+ });
162
+ await r.connect({
163
+ roomId,
164
+ // caps ride room_meta so the app filters the control room from the announce.
165
+ roomMeta: { name: hostname(), cwd: homedir(), caps: [...DAEMON_CAPS] },
166
+ });
167
+ envLog(`launcher: connected to control room ${roomId} (epk ${epk.slice(0, 12)}…)`);
168
+ }
169
+ await connectOnce();
170
+ return {
171
+ roomId,
172
+ epk,
173
+ stop() {
174
+ stopped = true;
175
+ for (const ch of channels.values()) {
176
+ try {
177
+ ch.detach();
178
+ }
179
+ catch {
180
+ /* best-effort */
181
+ }
182
+ }
183
+ channels.clear();
184
+ relay?.close();
185
+ },
186
+ };
187
+ }
188
+ //# sourceMappingURL=launcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"launcher.js","sourceRoot":"","sources":["../../src/launcher/launcher.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import { type PeerRecord } from "./storage.js";
2
+ /**
3
+ * Owner-trust lookups shared by the extension (index.ts) and the regime-2 launcher
4
+ * daemon. The trust model: the RELAY authenticates a peer's Ed25519 key
5
+ * ownership (challenge-response), so an inbound `peer` id IS the owner's
6
+ * cryptographic identity; authorization is then "is this key in peers.json?".
7
+ * Revocation is expressed by REMOVING the record, so a peers.json match is
8
+ * itself the not-revoked check — there is no separate revoked flag to honor.
9
+ */
10
+ export interface InspectedPeerRecord {
11
+ readonly record: PeerRecord;
12
+ readonly rawHandle: string;
13
+ readonly runtimeKey: string | null;
14
+ }
15
+ export declare function _runtimeOwnerFingerprint(runtimeKey: string): string;
16
+ export declare function _inspectPeerRecord(record: unknown): InspectedPeerRecord | null;
17
+ /** Resolve a relay-verified inbound `peer` id to its paired PeerRecord, or null
18
+ * if it isn't a known owner (never paired, or revoked → removed from
19
+ * peers.json). This IS the owner-authorization gate. */
20
+ export declare function _findKnownPeer(appPeerIdStd: string): Promise<PeerRecord | null>;
@@ -0,0 +1,82 @@
1
+ import { createHash } from "node:crypto";
2
+ import { canonicalizeEd25519PublicKey, decodeEd25519PublicKey, publicKeyFingerprint, } from "../mesh/encoding.js";
3
+ import { listPeers } from "./storage.js";
4
+ /** Metadata-only fingerprint of an UNTRUSTED/unparseable owner handle — used in
5
+ * diagnostics so a malformed record never puts key bytes in a log line. */
6
+ function _rawOwnerFingerprint(rawValue) {
7
+ let fingerprintInput;
8
+ if (typeof rawValue === "string") {
9
+ fingerprintInput = rawValue;
10
+ }
11
+ else {
12
+ try {
13
+ const serialized = JSON.stringify(rawValue);
14
+ const type = rawValue === null ? "null" : typeof rawValue;
15
+ fingerprintInput = `${type}:${serialized ?? ""}`;
16
+ }
17
+ catch {
18
+ fingerprintInput = `${typeof rawValue}:unserializable`;
19
+ }
20
+ }
21
+ return createHash("sha256")
22
+ .update(fingerprintInput, "utf8")
23
+ .digest("hex")
24
+ .slice(0, 8);
25
+ }
26
+ export function _runtimeOwnerFingerprint(runtimeKey) {
27
+ try {
28
+ return publicKeyFingerprint(decodeEd25519PublicKey(runtimeKey, "Owner runtime key"));
29
+ }
30
+ catch {
31
+ // Relay authentication guarantees canonical keys in production. This
32
+ // fallback keeps diagnostics metadata-only at defensive/test boundaries.
33
+ return _rawOwnerFingerprint(runtimeKey);
34
+ }
35
+ }
36
+ export function _inspectPeerRecord(record) {
37
+ if (!record || typeof record !== "object") {
38
+ const fingerprint = _rawOwnerFingerprint(record);
39
+ console.warn(`[un-bien] event=invalid_owner_record owner_fp=${fingerprint}`);
40
+ return null;
41
+ }
42
+ const candidate = record;
43
+ const rawHandle = candidate.remote_epk;
44
+ if (typeof rawHandle !== "string") {
45
+ const fingerprint = _rawOwnerFingerprint(rawHandle);
46
+ console.warn(`[un-bien] event=invalid_owner_record owner_fp=${fingerprint}`);
47
+ return null;
48
+ }
49
+ const safeRecord = {
50
+ name: typeof candidate.name === "string" ? candidate.name : "Unknown Owner",
51
+ remote_epk: rawHandle,
52
+ paired_at: typeof candidate.paired_at === "string" ? candidate.paired_at : "",
53
+ };
54
+ try {
55
+ const runtimeKey = canonicalizeEd25519PublicKey(rawHandle, "stored Owner public key");
56
+ return { record: safeRecord, rawHandle, runtimeKey };
57
+ }
58
+ catch {
59
+ const fingerprint = _rawOwnerFingerprint(rawHandle);
60
+ console.warn(`[un-bien] event=invalid_owner_record owner_fp=${fingerprint}`);
61
+ return { record: safeRecord, rawHandle, runtimeKey: null };
62
+ }
63
+ }
64
+ /** Resolve a relay-verified inbound `peer` id to its paired PeerRecord, or null
65
+ * if it isn't a known owner (never paired, or revoked → removed from
66
+ * peers.json). This IS the owner-authorization gate. */
67
+ export async function _findKnownPeer(appPeerIdStd) {
68
+ let runtimeKey;
69
+ try {
70
+ runtimeKey = canonicalizeEd25519PublicKey(appPeerIdStd, "Relay Owner key");
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ for (const record of await listPeers()) {
76
+ const inspected = _inspectPeerRecord(record);
77
+ if (inspected?.runtimeKey === runtimeKey)
78
+ return inspected.record;
79
+ }
80
+ return null;
81
+ }
82
+ //# sourceMappingURL=peer_trust.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"peer_trust.js","sourceRoot":"","sources":["../../src/pairing/peer_trust.ts"],"names":[],"mappings":"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"}
@@ -12,4 +12,6 @@ export interface PanelBridge {
12
12
  * Returns `null` when the SDK exposes no usable events bus (defensive — modern
13
13
  * Pi always has one); callers stay null-safe.
14
14
  */
15
- export declare function createPanelBridge(pi: ExtensionAPI, broadcast: (msg: ServerMessage) => void): PanelBridge | null;
15
+ export declare function createPanelBridge(pi: ExtensionAPI, broadcast: (msg: ServerMessage) => void, opts?: {
16
+ suppressAgents?: boolean;
17
+ }): PanelBridge | null;
@@ -30,7 +30,7 @@ const AGENTS_KEY = "subagents";
30
30
  * Returns `null` when the SDK exposes no usable events bus (defensive — modern
31
31
  * Pi always has one); callers stay null-safe.
32
32
  */
33
- export function createPanelBridge(pi, broadcast) {
33
+ export function createPanelBridge(pi, broadcast, opts) {
34
34
  const eventsRaw = pi.events;
35
35
  if (!eventsRaw || typeof eventsRaw.on !== "function") {
36
36
  return null;
@@ -38,8 +38,9 @@ export function createPanelBridge(pi, broadcast) {
38
38
  const events = eventsRaw;
39
39
  // Opt-in tracing: shows whether bus events land and whether a panel frame is
40
40
  // broadcast, to isolate capture vs render. Enabled by the `debug.panels` pref
41
- // in the global config (`extensions/un-bien.json`) — logs to stderr AND appends
42
- // to `<state>/panel-bridge.log`. stderr scrolls too fast — tail the file.
41
+ // in the global config (`extensions/un-bien.json`) — appends to
42
+ // `<state>/panel-bridge.log` only. Direct stderr writes corrupt the TUI, so
43
+ // `tail -f` the file instead.
43
44
  const debug = (() => {
44
45
  if (loadConfig().debug?.panels !== true)
45
46
  return () => { };
@@ -54,12 +55,6 @@ export function createPanelBridge(pi, broadcast) {
54
55
  const logFile = join(dir, "panel-bridge.log");
55
56
  return (msg) => {
56
57
  const line = `${new Date().toISOString()} [panel-bridge] ${msg}`;
57
- try {
58
- console.error(line);
59
- }
60
- catch {
61
- /* stderr closed */
62
- }
63
58
  try {
64
59
  appendFileSync(logFile, `${line}\n`);
65
60
  }
@@ -237,7 +232,12 @@ export function createPanelBridge(pi, broadcast) {
237
232
  debug(`fleet=${fleet.size} after ${status} ${id}`);
238
233
  broadcastNow(AGENTS_KEY, agentsFrame);
239
234
  };
240
- const unsubAgents = Object.entries(LIFECYCLE).map(([channel, status]) => events.on(channel, recordAgent(status)));
235
+ // When the subagent-rooms feature owns the subagents panel (producing it from
236
+ // pi child detection), don't ALSO drive it from the bus here — two producers
237
+ // would fight over the same `subagents` panel key.
238
+ const unsubAgents = opts?.suppressAgents
239
+ ? []
240
+ : Object.entries(LIFECYCLE).map(([channel, status]) => events.on(channel, recordAgent(status)));
241
241
  // NB: unlike pi-plan's TUI (which clears its fleet on session_shutdown to reset
242
242
  // the widget), we deliberately KEEP finished agents. Broadcasting an empty
243
243
  // frame on shutdown would clobber the app's panel the instant a fast agent