@h-rig/runtime 0.0.6-alpha.21 → 0.0.6-alpha.23

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 (25) hide show
  1. package/dist/bin/rig-agent-dispatch.js +588 -28
  2. package/dist/src/control-plane/agent-wrapper.js +592 -28
  3. package/dist/src/control-plane/harness-main.js +142 -17
  4. package/dist/src/control-plane/hooks/completion-verification.js +142 -17
  5. package/dist/src/control-plane/native/harness-cli.js +142 -17
  6. package/dist/src/control-plane/native/pr-automation.js +142 -17
  7. package/dist/src/control-plane/native/pr-review-gate.js +142 -17
  8. package/dist/src/control-plane/native/run-ops.js +1 -1
  9. package/dist/src/control-plane/native/task-ops.js +142 -17
  10. package/dist/src/control-plane/native/verifier.js +142 -17
  11. package/dist/src/control-plane/pi-sessiond/bin.js +793 -0
  12. package/dist/src/control-plane/pi-sessiond/client.js +41 -0
  13. package/dist/src/control-plane/pi-sessiond/event-hub.js +59 -0
  14. package/dist/src/control-plane/pi-sessiond/extension-ui-context.js +198 -0
  15. package/dist/src/control-plane/pi-sessiond/launcher.js +163 -0
  16. package/dist/src/control-plane/pi-sessiond/server.js +802 -0
  17. package/dist/src/control-plane/pi-sessiond/session-service.js +540 -0
  18. package/dist/src/control-plane/pi-sessiond/types.js +1 -0
  19. package/dist/src/control-plane/runtime/index.js +17 -0
  20. package/dist/src/control-plane/runtime/isolation/home.js +17 -0
  21. package/dist/src/control-plane/runtime/isolation/index.js +17 -0
  22. package/dist/src/control-plane/runtime/isolation/runner.js +17 -0
  23. package/dist/src/control-plane/runtime/isolation.js +17 -0
  24. package/dist/src/control-plane/runtime/queue.js +17 -0
  25. package/package.json +7 -6
@@ -0,0 +1,41 @@
1
+ // @bun
2
+ // packages/runtime/src/control-plane/pi-sessiond/client.ts
3
+ class RigPiSessionDaemonClient {
4
+ baseUrl;
5
+ token;
6
+ constructor(options) {
7
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
8
+ this.token = options.token;
9
+ }
10
+ static fromConnection(connection, token) {
11
+ if (connection.mode === "http")
12
+ return new RigPiSessionDaemonClient({ baseUrl: connection.baseUrl, token });
13
+ throw new Error("Unix-socket Rig Pi daemon connections are not implemented in this build; use loopback HTTP.");
14
+ }
15
+ async request(method, path, body) {
16
+ const response = await fetch(`${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`, {
17
+ method,
18
+ headers: {
19
+ authorization: `Bearer ${this.token}`,
20
+ ...body === undefined ? {} : { "content-type": "application/json" }
21
+ },
22
+ body: body === undefined ? undefined : JSON.stringify(body)
23
+ });
24
+ const text = await response.text();
25
+ const payload = text.trim() ? JSON.parse(text) : undefined;
26
+ if (!response.ok) {
27
+ const message = payload && typeof payload === "object" && !Array.isArray(payload) && typeof payload.error === "string" ? payload.error : text || response.statusText;
28
+ throw new Error(`Rig Pi session daemon request failed (${response.status}): ${message}`);
29
+ }
30
+ return payload;
31
+ }
32
+ webSocketUrl(path) {
33
+ const url = new URL(`${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`);
34
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
35
+ url.searchParams.set("token", this.token);
36
+ return url.toString();
37
+ }
38
+ }
39
+ export {
40
+ RigPiSessionDaemonClient
41
+ };
@@ -0,0 +1,59 @@
1
+ // @bun
2
+ // packages/runtime/src/control-plane/pi-sessiond/event-hub.ts
3
+ function isOpen(socket) {
4
+ return socket.readyState === (socket.OPEN ?? 1);
5
+ }
6
+
7
+ class RigPiSessionEventHub {
8
+ socketsBySession = new Map;
9
+ socketsByRun = new Map;
10
+ addSession(sessionId, socket) {
11
+ return addToMapSet(this.socketsBySession, sessionId, socket);
12
+ }
13
+ addRun(runId, socket) {
14
+ return addToMapSet(this.socketsByRun, runId, socket);
15
+ }
16
+ publishSession(sessionId, event) {
17
+ this.publishSet(this.socketsBySession.get(sessionId), event);
18
+ }
19
+ publishRun(runId, event) {
20
+ this.publishSet(this.socketsByRun.get(runId), event);
21
+ }
22
+ publish(sessionId, runId, event) {
23
+ this.publishSession(sessionId, event);
24
+ if (runId)
25
+ this.publishRun(runId, event);
26
+ }
27
+ publishSet(sockets, event) {
28
+ if (!sockets || sockets.size === 0)
29
+ return;
30
+ const payload = JSON.stringify(event);
31
+ for (const socket of [...sockets]) {
32
+ if (isOpen(socket)) {
33
+ socket.send(payload);
34
+ } else {
35
+ sockets.delete(socket);
36
+ }
37
+ }
38
+ }
39
+ }
40
+ function addToMapSet(map, key, socket) {
41
+ let sockets = map.get(key);
42
+ if (!sockets) {
43
+ sockets = new Set;
44
+ map.set(key, sockets);
45
+ }
46
+ sockets.add(socket);
47
+ let removed = false;
48
+ return () => {
49
+ if (removed)
50
+ return;
51
+ removed = true;
52
+ sockets?.delete(socket);
53
+ if (sockets?.size === 0)
54
+ map.delete(key);
55
+ };
56
+ }
57
+ export {
58
+ RigPiSessionEventHub
59
+ };
@@ -0,0 +1,198 @@
1
+ // @bun
2
+ // packages/runtime/src/control-plane/pi-sessiond/extension-ui-context.ts
3
+ import { randomUUID } from "crypto";
4
+ import {
5
+ initTheme
6
+ } from "@earendil-works/pi-coding-agent";
7
+ var headlessTheme = {
8
+ fg: (_color, text) => text,
9
+ bg: (_color, text) => text,
10
+ dim: (text) => text,
11
+ bold: (text) => text,
12
+ italic: (text) => text,
13
+ underline: (text) => text,
14
+ strikethrough: (text) => text,
15
+ reset: (text) => text,
16
+ getFgAnsi: () => "",
17
+ getBgAnsi: () => "",
18
+ name: "rig-headless"
19
+ };
20
+ function ensureThemeInitialized() {
21
+ try {
22
+ initTheme(undefined, false);
23
+ } catch {}
24
+ }
25
+
26
+ class RigPiExtensionUiBridge {
27
+ sessionId;
28
+ runId;
29
+ publish;
30
+ pending = new Map;
31
+ constructor(sessionId, runId, publish) {
32
+ this.sessionId = sessionId;
33
+ this.runId = runId;
34
+ this.publish = publish;
35
+ ensureThemeInitialized();
36
+ }
37
+ createContext() {
38
+ const dialog = (opts, defaultValue, request, parse) => {
39
+ if (opts?.signal?.aborted)
40
+ return Promise.resolve(defaultValue);
41
+ const id = randomUUID();
42
+ return new Promise((resolve) => {
43
+ let timeout;
44
+ const cleanup = () => {
45
+ if (timeout)
46
+ clearTimeout(timeout);
47
+ opts?.signal?.removeEventListener("abort", onAbort);
48
+ this.pending.delete(id);
49
+ };
50
+ const finish = (value) => {
51
+ cleanup();
52
+ resolve(value);
53
+ };
54
+ const onAbort = () => finish(defaultValue);
55
+ opts?.signal?.addEventListener("abort", onAbort, { once: true });
56
+ if (opts?.timeout)
57
+ timeout = setTimeout(() => finish(defaultValue), opts.timeout);
58
+ this.pending.set(id, {
59
+ timeout,
60
+ abort: onAbort,
61
+ resolve: (raw) => finish(parse(normalizeResponse(raw)))
62
+ });
63
+ this.publish({
64
+ type: "extension_ui_request",
65
+ sessionId: this.sessionId,
66
+ runId: this.runId,
67
+ request: { id, timeout: opts?.timeout, ...request }
68
+ });
69
+ });
70
+ };
71
+ return {
72
+ select: (title, options, opts) => dialog(opts, undefined, { method: "select", title, options }, (response) => {
73
+ if (response.cancelled)
74
+ return;
75
+ return typeof response.value === "string" ? response.value : undefined;
76
+ }),
77
+ confirm: (title, message, opts) => dialog(opts, false, { method: "confirm", title, message }, (response) => {
78
+ if (response.cancelled)
79
+ return false;
80
+ return response.confirmed === true || response.value === true;
81
+ }),
82
+ input: (title, placeholder, opts) => dialog(opts, undefined, { method: "input", title, placeholder }, (response) => {
83
+ if (response.cancelled)
84
+ return;
85
+ return typeof response.value === "string" ? response.value : undefined;
86
+ }),
87
+ notify: (message, type) => {
88
+ this.fire({ method: "notify", message, notifyType: type });
89
+ },
90
+ onTerminalInput: () => () => {},
91
+ setStatus: (key, text) => {
92
+ this.fire({ method: "setStatus", statusKey: key, statusText: text });
93
+ },
94
+ setWorkingMessage: (message) => {
95
+ this.fire({ method: "setWorkingMessage", message });
96
+ },
97
+ setWorkingVisible: (visible) => {
98
+ this.fire({ method: "setWorkingVisible", visible });
99
+ },
100
+ setWorkingIndicator: (options) => {
101
+ this.fire({ method: "setWorkingIndicator", options });
102
+ },
103
+ setHiddenThinkingLabel: (label) => {
104
+ this.fire({ method: "setHiddenThinkingLabel", label });
105
+ },
106
+ setWidget: (key, content, options) => {
107
+ if (content === undefined || Array.isArray(content)) {
108
+ this.fire({ method: "setWidget", widgetKey: key, widgetLines: content, widgetPlacement: options?.placement });
109
+ return;
110
+ }
111
+ this.fire({ method: "notify", message: `Extension widget '${key}' uses a component factory that Rig daemon mode cannot render.`, notifyType: "warning" });
112
+ },
113
+ setFooter: (factory) => {
114
+ if (factory !== undefined)
115
+ this.fire({ method: "notify", message: "Custom extension footers are not rendered in Rig daemon mode.", notifyType: "warning" });
116
+ },
117
+ setHeader: (factory) => {
118
+ if (factory !== undefined)
119
+ this.fire({ method: "notify", message: "Custom extension headers are not rendered in Rig daemon mode.", notifyType: "warning" });
120
+ },
121
+ setTitle: (title) => {
122
+ this.fire({ method: "setTitle", title });
123
+ },
124
+ custom: async () => {
125
+ this.fire({ method: "notify", message: "Custom extension UI components are not supported in Rig daemon mode.", notifyType: "warning" });
126
+ return;
127
+ },
128
+ pasteToEditor: (text) => {
129
+ this.fire({ method: "set_editor_text", text });
130
+ },
131
+ setEditorText: (text) => {
132
+ this.fire({ method: "set_editor_text", text });
133
+ },
134
+ getEditorText: () => "",
135
+ editor: (title, prefill) => dialog(undefined, undefined, { method: "editor", title, prefill }, (response) => {
136
+ if (response.cancelled)
137
+ return;
138
+ return typeof response.value === "string" ? response.value : undefined;
139
+ }),
140
+ addAutocompleteProvider: () => {},
141
+ setEditorComponent: (factory) => {
142
+ if (factory !== undefined)
143
+ this.fire({ method: "notify", message: "Custom editor components are not supported in Rig daemon mode.", notifyType: "warning" });
144
+ },
145
+ getEditorComponent: () => {
146
+ return;
147
+ },
148
+ get theme() {
149
+ return headlessTheme;
150
+ },
151
+ getAllThemes: () => [],
152
+ getTheme: () => {
153
+ return;
154
+ },
155
+ setTheme: () => ({ success: false, error: "Theme switching is not supported in Rig daemon mode" }),
156
+ getToolsExpanded: () => false,
157
+ setToolsExpanded: () => {}
158
+ };
159
+ }
160
+ respond(input) {
161
+ const pending = this.pending.get(input.requestId);
162
+ if (!pending)
163
+ return false;
164
+ pending.resolve(input);
165
+ return true;
166
+ }
167
+ cancelAll() {
168
+ for (const [id, pending] of this.pending.entries()) {
169
+ if (pending.timeout)
170
+ clearTimeout(pending.timeout);
171
+ pending.abort?.();
172
+ this.pending.delete(id);
173
+ pending.resolve({ requestId: id, cancelled: true });
174
+ }
175
+ }
176
+ fire(request) {
177
+ this.publish({
178
+ type: "extension_ui_request",
179
+ sessionId: this.sessionId,
180
+ runId: this.runId,
181
+ request: { id: randomUUID(), ...request }
182
+ });
183
+ }
184
+ }
185
+ function normalizeResponse(value) {
186
+ if (!value || typeof value !== "object" || Array.isArray(value))
187
+ return { requestId: "", cancelled: true };
188
+ const record = value;
189
+ return {
190
+ requestId: typeof record.requestId === "string" ? record.requestId : "",
191
+ value: record.value,
192
+ confirmed: typeof record.confirmed === "boolean" ? record.confirmed : undefined,
193
+ cancelled: record.cancelled === true
194
+ };
195
+ }
196
+ export {
197
+ RigPiExtensionUiBridge
198
+ };
@@ -0,0 +1,163 @@
1
+ // @bun
2
+ // packages/runtime/src/control-plane/pi-sessiond/launcher.ts
3
+ import { randomBytes } from "crypto";
4
+ import { existsSync, mkdirSync, readFileSync, rmSync } from "fs";
5
+ import { dirname, resolve } from "path";
6
+ import { fileURLToPath } from "url";
7
+
8
+ // packages/runtime/src/control-plane/pi-sessiond/client.ts
9
+ class RigPiSessionDaemonClient {
10
+ baseUrl;
11
+ token;
12
+ constructor(options) {
13
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
14
+ this.token = options.token;
15
+ }
16
+ static fromConnection(connection, token) {
17
+ if (connection.mode === "http")
18
+ return new RigPiSessionDaemonClient({ baseUrl: connection.baseUrl, token });
19
+ throw new Error("Unix-socket Rig Pi daemon connections are not implemented in this build; use loopback HTTP.");
20
+ }
21
+ async request(method, path, body) {
22
+ const response = await fetch(`${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`, {
23
+ method,
24
+ headers: {
25
+ authorization: `Bearer ${this.token}`,
26
+ ...body === undefined ? {} : { "content-type": "application/json" }
27
+ },
28
+ body: body === undefined ? undefined : JSON.stringify(body)
29
+ });
30
+ const text = await response.text();
31
+ const payload = text.trim() ? JSON.parse(text) : undefined;
32
+ if (!response.ok) {
33
+ const message = payload && typeof payload === "object" && !Array.isArray(payload) && typeof payload.error === "string" ? payload.error : text || response.statusText;
34
+ throw new Error(`Rig Pi session daemon request failed (${response.status}): ${message}`);
35
+ }
36
+ return payload;
37
+ }
38
+ webSocketUrl(path) {
39
+ const url = new URL(`${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`);
40
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
41
+ url.searchParams.set("token", this.token);
42
+ return url.toString();
43
+ }
44
+ }
45
+
46
+ // packages/runtime/src/control-plane/pi-sessiond/launcher.ts
47
+ var BUILD_CONFIG = {};
48
+ var BAKED_RIG_SOURCE_ROOT = BUILD_CONFIG.RIG_SOURCE_ROOT ?? "";
49
+ async function ensureRigPiSessionDaemon(input) {
50
+ const rootDir = resolve(input.rootDir);
51
+ mkdirSync(rootDir, { recursive: true });
52
+ const readyFile = resolve(rootDir, "ready.json");
53
+ const existing = readDaemonReadyFile(readyFile);
54
+ const existingHandle = existing ? await tryReady(existing) : null;
55
+ if (existingHandle)
56
+ return existingHandle;
57
+ try {
58
+ rmSync(readyFile, { force: true });
59
+ } catch {}
60
+ const token = randomBytes(32).toString("hex");
61
+ const binPath = resolveRigPiSessionDaemonBinPath(input.env);
62
+ const bunPath = input.env.RIG_BUN_PATH || process.execPath;
63
+ const proc = Bun.spawn([bunPath, binPath], {
64
+ cwd: rootDir,
65
+ env: {
66
+ ...input.env,
67
+ RIG_PI_SESSIOND_ROOT: rootDir,
68
+ RIG_PI_SESSIOND_TOKEN: token,
69
+ RIG_PI_SESSIOND_READY_FILE: readyFile,
70
+ RIG_PI_SESSIOND_HOST: "127.0.0.1",
71
+ RIG_PI_SESSIOND_PORT: "0",
72
+ ...input.version ? { RIG_VERSION: input.version } : {},
73
+ ...input.commit ? { RIG_GIT_COMMIT: input.commit } : {}
74
+ },
75
+ stdin: "ignore",
76
+ stdout: "ignore",
77
+ stderr: "inherit"
78
+ });
79
+ proc.unref();
80
+ const deadline = Date.now() + (input.timeoutMs ?? 15000);
81
+ while (Date.now() < deadline) {
82
+ const ready = readDaemonReadyFile(readyFile);
83
+ const handle = ready ? await tryReady(ready) : null;
84
+ if (handle)
85
+ return handle;
86
+ await sleep(100);
87
+ }
88
+ throw new Error(`Rig Pi session daemon did not become ready at ${readyFile}`);
89
+ }
90
+ function privateMetadataForDaemon(input) {
91
+ return { public: input.publicMetadata, daemonConnection: input.connection };
92
+ }
93
+ async function tryReady(ready) {
94
+ const host = typeof ready.host === "string" ? ready.host : "127.0.0.1";
95
+ const port = typeof ready.port === "number" ? ready.port : Number(ready.port);
96
+ const token = typeof ready.token === "string" ? ready.token : "";
97
+ if (!Number.isFinite(port) || port <= 0 || !token)
98
+ return null;
99
+ const baseUrl = `http://${host}:${port}`;
100
+ const client = new RigPiSessionDaemonClient({ baseUrl, token });
101
+ try {
102
+ await client.request("GET", "/health");
103
+ } catch {
104
+ return null;
105
+ }
106
+ return {
107
+ client,
108
+ connection: { mode: "http", baseUrl, tokenRef: tokenRefFromReady(ready) },
109
+ token,
110
+ ready
111
+ };
112
+ }
113
+ function tokenRefFromReady(ready) {
114
+ const token = typeof ready.token === "string" ? ready.token : "";
115
+ return token ? `inline:${token}` : "missing";
116
+ }
117
+ function resolveRigPiSessionDaemonBinPath(env) {
118
+ const explicit = env.RIG_PI_SESSIOND_BIN?.trim();
119
+ if (explicit)
120
+ return explicit;
121
+ const roots = [
122
+ env.RIG_CONTROL_PLANE_SOURCE_ROOT?.trim(),
123
+ BAKED_RIG_SOURCE_ROOT.trim(),
124
+ process.env.RIG_CONTROL_PLANE_SOURCE_ROOT?.trim(),
125
+ process.env.RIG_HOST_PROJECT_ROOT?.trim(),
126
+ process.env.PROJECT_RIG_ROOT?.trim()
127
+ ].filter((value) => Boolean(value));
128
+ for (const root of roots) {
129
+ const candidate = resolve(root, "packages/runtime/src/control-plane/pi-sessiond/bin.ts");
130
+ if (existsSync(candidate))
131
+ return candidate;
132
+ }
133
+ const moduleCandidate = fileURLToPath(new URL("./bin.ts", import.meta.url));
134
+ if (existsSync(moduleCandidate))
135
+ return moduleCandidate;
136
+ throw new Error("Unable to locate rig-pi-sessiond entrypoint. Set RIG_PI_SESSIOND_BIN or RIG_CONTROL_PLANE_SOURCE_ROOT to the Rig source checkout.");
137
+ }
138
+ function readDaemonReadyFile(path) {
139
+ if (!existsSync(path))
140
+ return null;
141
+ try {
142
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
143
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
144
+ } catch {
145
+ return null;
146
+ }
147
+ }
148
+ function sleep(ms) {
149
+ return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
150
+ }
151
+ function resolveRigPiSessionDaemonRoot(stateDir) {
152
+ const root = resolve(stateDir, "pi-sessiond");
153
+ mkdirSync(dirname(root), { recursive: true });
154
+ if (!existsSync(root))
155
+ mkdirSync(root, { recursive: true });
156
+ return root;
157
+ }
158
+ export {
159
+ resolveRigPiSessionDaemonRoot,
160
+ readDaemonReadyFile,
161
+ privateMetadataForDaemon,
162
+ ensureRigPiSessionDaemon
163
+ };