@greppleai/grip 0.0.1 → 0.1.4

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 (221) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/README.md +13 -7
  3. package/dist/bun/cli.d.ts.map +1 -1
  4. package/dist/bun/cli.js +4 -2
  5. package/dist/bun/cli.js.map +1 -1
  6. package/dist/bun/embedded-assets.d.ts +3 -0
  7. package/dist/bun/embedded-assets.d.ts.map +1 -0
  8. package/dist/bun/embedded-assets.js +52 -0
  9. package/dist/bun/embedded-assets.js.map +1 -0
  10. package/dist/cli/args.d.ts.map +1 -1
  11. package/dist/cli/args.js +1 -0
  12. package/dist/cli/args.js.map +1 -1
  13. package/dist/config.d.ts.map +1 -1
  14. package/dist/config.js +3 -3
  15. package/dist/config.js.map +1 -1
  16. package/dist/core/agent-session.d.ts.map +1 -1
  17. package/dist/core/agent-session.js +17 -2
  18. package/dist/core/agent-session.js.map +1 -1
  19. package/dist/core/extensions/types.d.ts +5 -1
  20. package/dist/core/extensions/types.d.ts.map +1 -1
  21. package/dist/core/extensions/types.js.map +1 -1
  22. package/dist/core/remote-workspace-protocol/contract.d.ts +13 -4
  23. package/dist/core/remote-workspace-protocol/contract.d.ts.map +1 -1
  24. package/dist/core/remote-workspace-protocol/contract.js +17 -1
  25. package/dist/core/remote-workspace-protocol/contract.js.map +1 -1
  26. package/dist/core/remote-workspace-protocol/guest-shared-client.d.ts +16 -0
  27. package/dist/core/remote-workspace-protocol/guest-shared-client.d.ts.map +1 -0
  28. package/dist/core/remote-workspace-protocol/guest-shared-client.js +54 -0
  29. package/dist/core/remote-workspace-protocol/guest-shared-client.js.map +1 -0
  30. package/dist/core/remote-workspace-protocol/guest-shared-outbound-scheduler.d.ts +23 -0
  31. package/dist/core/remote-workspace-protocol/guest-shared-outbound-scheduler.d.ts.map +1 -0
  32. package/dist/core/remote-workspace-protocol/guest-shared-outbound-scheduler.js +112 -0
  33. package/dist/core/remote-workspace-protocol/guest-shared-outbound-scheduler.js.map +1 -0
  34. package/dist/core/remote-workspace-protocol/guest-shared-uplink.d.ts +30 -0
  35. package/dist/core/remote-workspace-protocol/guest-shared-uplink.d.ts.map +1 -0
  36. package/dist/core/remote-workspace-protocol/guest-shared-uplink.js +243 -0
  37. package/dist/core/remote-workspace-protocol/guest-shared-uplink.js.map +1 -0
  38. package/dist/core/remote-workspace-protocol/lima-owner-directory.d.ts +3 -0
  39. package/dist/core/remote-workspace-protocol/lima-owner-directory.d.ts.map +1 -0
  40. package/dist/core/remote-workspace-protocol/lima-owner-directory.js +31 -0
  41. package/dist/core/remote-workspace-protocol/lima-owner-directory.js.map +1 -0
  42. package/dist/core/remote-workspace-protocol/lima-owner-lock.d.ts +9 -0
  43. package/dist/core/remote-workspace-protocol/lima-owner-lock.d.ts.map +1 -0
  44. package/dist/core/remote-workspace-protocol/lima-owner-lock.js +89 -0
  45. package/dist/core/remote-workspace-protocol/lima-owner-lock.js.map +1 -0
  46. package/dist/core/remote-workspace-protocol/session.d.ts +2 -0
  47. package/dist/core/remote-workspace-protocol/session.d.ts.map +1 -1
  48. package/dist/core/remote-workspace-protocol/session.js +3 -0
  49. package/dist/core/remote-workspace-protocol/session.js.map +1 -1
  50. package/dist/core/remote-workspace-protocol/shared-transport.d.ts +71 -0
  51. package/dist/core/remote-workspace-protocol/shared-transport.d.ts.map +1 -0
  52. package/dist/core/remote-workspace-protocol/shared-transport.js +68 -0
  53. package/dist/core/remote-workspace-protocol/shared-transport.js.map +1 -0
  54. package/dist/core/sandbox/command.d.ts +2 -4
  55. package/dist/core/sandbox/command.d.ts.map +1 -1
  56. package/dist/core/sandbox/command.js +24 -46
  57. package/dist/core/sandbox/command.js.map +1 -1
  58. package/dist/core/sandbox/config.d.ts +91 -0
  59. package/dist/core/sandbox/config.d.ts.map +1 -0
  60. package/dist/core/sandbox/config.js +88 -0
  61. package/dist/core/sandbox/config.js.map +1 -0
  62. package/dist/core/sandbox/lima-guest-binary.d.ts +11 -0
  63. package/dist/core/sandbox/lima-guest-binary.d.ts.map +1 -0
  64. package/dist/core/sandbox/lima-guest-binary.js +140 -0
  65. package/dist/core/sandbox/lima-guest-binary.js.map +1 -0
  66. package/dist/core/sandbox/lima.d.ts +22 -3
  67. package/dist/core/sandbox/lima.d.ts.map +1 -1
  68. package/dist/core/sandbox/lima.js +434 -42
  69. package/dist/core/sandbox/lima.js.map +1 -1
  70. package/dist/core/sandbox/shared-lima-roots.d.ts +11 -0
  71. package/dist/core/sandbox/shared-lima-roots.d.ts.map +1 -0
  72. package/dist/core/sandbox/shared-lima-roots.js +25 -0
  73. package/dist/core/sandbox/shared-lima-roots.js.map +1 -0
  74. package/dist/core/sandbox/update-shared-guest-binary.d.ts +8 -0
  75. package/dist/core/sandbox/update-shared-guest-binary.d.ts.map +1 -0
  76. package/dist/core/sandbox/update-shared-guest-binary.js +100 -0
  77. package/dist/core/sandbox/update-shared-guest-binary.js.map +1 -0
  78. package/dist/core/settings-manager.d.ts +1 -5
  79. package/dist/core/settings-manager.d.ts.map +1 -1
  80. package/dist/core/settings-manager.js.map +1 -1
  81. package/dist/core/tools/bash.d.ts.map +1 -1
  82. package/dist/core/tools/bash.js +25 -14
  83. package/dist/core/tools/bash.js.map +1 -1
  84. package/dist/core/tools/file-mutation-queue.d.ts +2 -0
  85. package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
  86. package/dist/core/tools/file-mutation-queue.js +9 -0
  87. package/dist/core/tools/file-mutation-queue.js.map +1 -1
  88. package/dist/core/tools/find.d.ts.map +1 -1
  89. package/dist/core/tools/find.js +2 -3
  90. package/dist/core/tools/find.js.map +1 -1
  91. package/dist/core/tools/grep.d.ts.map +1 -1
  92. package/dist/core/tools/grep.js +2 -3
  93. package/dist/core/tools/grep.js.map +1 -1
  94. package/dist/core/tools/ls.d.ts.map +1 -1
  95. package/dist/core/tools/ls.js +2 -3
  96. package/dist/core/tools/ls.js.map +1 -1
  97. package/dist/core/tools/operations.d.ts +13 -2
  98. package/dist/core/tools/operations.d.ts.map +1 -1
  99. package/dist/core/tools/operations.js +132 -36
  100. package/dist/core/tools/operations.js.map +1 -1
  101. package/dist/core/tools/read.d.ts.map +1 -1
  102. package/dist/core/tools/read.js +3 -3
  103. package/dist/core/tools/read.js.map +1 -1
  104. package/dist/core/tools/render-utils.d.ts +1 -0
  105. package/dist/core/tools/render-utils.d.ts.map +1 -1
  106. package/dist/core/tools/render-utils.js +5 -0
  107. package/dist/core/tools/render-utils.js.map +1 -1
  108. package/dist/core/tools/write.d.ts.map +1 -1
  109. package/dist/core/tools/write.js +2 -3
  110. package/dist/core/tools/write.js.map +1 -1
  111. package/dist/daemon/activity-budget.d.ts +10 -0
  112. package/dist/daemon/activity-budget.d.ts.map +1 -0
  113. package/dist/daemon/activity-budget.js +34 -0
  114. package/dist/daemon/activity-budget.js.map +1 -0
  115. package/dist/daemon/config.d.ts +2 -0
  116. package/dist/daemon/config.d.ts.map +1 -1
  117. package/dist/daemon/config.js +15 -0
  118. package/dist/daemon/config.js.map +1 -1
  119. package/dist/daemon/outbound-budget.d.ts +10 -0
  120. package/dist/daemon/outbound-budget.d.ts.map +1 -0
  121. package/dist/daemon/outbound-budget.js +30 -0
  122. package/dist/daemon/outbound-budget.js.map +1 -0
  123. package/dist/daemon/security.d.ts.map +1 -1
  124. package/dist/daemon/security.js +3 -1
  125. package/dist/daemon/security.js.map +1 -1
  126. package/dist/daemon/server.d.ts.map +1 -1
  127. package/dist/daemon/server.js +104 -24
  128. package/dist/daemon/server.js.map +1 -1
  129. package/dist/daemon/shared-connection.d.ts +33 -0
  130. package/dist/daemon/shared-connection.d.ts.map +1 -0
  131. package/dist/daemon/shared-connection.js +271 -0
  132. package/dist/daemon/shared-connection.js.map +1 -0
  133. package/dist/daemon/shared-revision.d.ts +5 -0
  134. package/dist/daemon/shared-revision.d.ts.map +1 -0
  135. package/dist/daemon/shared-revision.js +61 -0
  136. package/dist/daemon/shared-revision.js.map +1 -0
  137. package/dist/daemon/shared-roots.d.ts +9 -0
  138. package/dist/daemon/shared-roots.d.ts.map +1 -0
  139. package/dist/daemon/shared-roots.js +33 -0
  140. package/dist/daemon/shared-roots.js.map +1 -0
  141. package/dist/daemon/workspace-runtime.d.ts +1 -1
  142. package/dist/daemon/workspace-runtime.d.ts.map +1 -1
  143. package/dist/daemon/workspace-runtime.js +42 -10
  144. package/dist/daemon/workspace-runtime.js.map +1 -1
  145. package/dist/main.d.ts.map +1 -1
  146. package/dist/main.js +5 -3
  147. package/dist/main.js.map +1 -1
  148. package/dist/modes/interactive/components/assistant-message.d.ts +1 -0
  149. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  150. package/dist/modes/interactive/components/assistant-message.js +13 -3
  151. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  152. package/dist/modes/interactive/components/branch-summary-message.d.ts +4 -3
  153. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  154. package/dist/modes/interactive/components/branch-summary-message.js +5 -4
  155. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  156. package/dist/modes/interactive/components/compaction-summary-message.d.ts +4 -3
  157. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  158. package/dist/modes/interactive/components/compaction-summary-message.js +5 -4
  159. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  160. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
  161. package/dist/modes/interactive/components/custom-message.js +4 -3
  162. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  163. package/dist/modes/interactive/components/hook-execution.d.ts +3 -0
  164. package/dist/modes/interactive/components/hook-execution.d.ts.map +1 -1
  165. package/dist/modes/interactive/components/hook-execution.js +20 -2
  166. package/dist/modes/interactive/components/hook-execution.js.map +1 -1
  167. package/dist/modes/interactive/components/message-card.d.ts +9 -0
  168. package/dist/modes/interactive/components/message-card.d.ts.map +1 -0
  169. package/dist/modes/interactive/components/message-card.js +24 -0
  170. package/dist/modes/interactive/components/message-card.js.map +1 -0
  171. package/dist/modes/interactive/components/skill-invocation-message.d.ts +4 -3
  172. package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  173. package/dist/modes/interactive/components/skill-invocation-message.js +5 -4
  174. package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  175. package/dist/modes/interactive/components/tool-execution.d.ts +1 -1
  176. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  177. package/dist/modes/interactive/components/tool-execution.js +27 -21
  178. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  179. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  180. package/dist/modes/interactive/components/user-message.js +3 -2
  181. package/dist/modes/interactive/components/user-message.js.map +1 -1
  182. package/dist/modes/interactive/interactive-mode.d.ts +1 -3
  183. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  184. package/dist/modes/interactive/interactive-mode.js +106 -47
  185. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  186. package/dist/modes/interactive/theme/theme.d.ts +3 -0
  187. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  188. package/dist/modes/interactive/theme/theme.js +10 -0
  189. package/dist/modes/interactive/theme/theme.js.map +1 -1
  190. package/dist/utils/clipboard-native.d.ts.map +1 -1
  191. package/dist/utils/clipboard-native.js +1 -1
  192. package/dist/utils/clipboard-native.js.map +1 -1
  193. package/dist/utils/image-resize.d.ts.map +1 -1
  194. package/dist/utils/image-resize.js +5 -0
  195. package/dist/utils/image-resize.js.map +1 -1
  196. package/dist/utils/photon.d.ts.map +1 -1
  197. package/dist/utils/photon.js +1 -0
  198. package/dist/utils/photon.js.map +1 -1
  199. package/docs/daemon.md +6 -4
  200. package/docs/docs.json +1 -1
  201. package/docs/extensions.md +3 -0
  202. package/docs/index.md +8 -12
  203. package/docs/packages.md +3 -3
  204. package/docs/quickstart.md +11 -19
  205. package/docs/sandbox.md +40 -117
  206. package/docs/settings.md +12 -11
  207. package/docs/termux.md +1 -18
  208. package/docs/usage.md +2 -2
  209. package/examples/README.md +0 -2
  210. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  211. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  212. package/examples/extensions/sandbox/package.json +1 -1
  213. package/examples/extensions/with-deps/package.json +1 -1
  214. package/npm-shrinkwrap.json +12 -12
  215. package/package.json +5 -6
  216. package/dist/core/sandbox/docker.d.ts +0 -169
  217. package/dist/core/sandbox/docker.d.ts.map +0 -1
  218. package/dist/core/sandbox/docker.js +0 -633
  219. package/dist/core/sandbox/docker.js.map +0 -1
  220. package/examples/sandbox-docker/Dockerfile +0 -274
  221. package/examples/sandbox-docker/README.md +0 -88
@@ -1,633 +0,0 @@
1
- import { createHash, randomBytes } from "node:crypto";
2
- import { open, readFile, stat, unlink } from "node:fs/promises";
3
- import { createConnection, createServer } from "node:net";
4
- import { tmpdir } from "node:os";
5
- import { basename, join, resolve } from "node:path";
6
- import { VERSION } from "../../config.js";
7
- import { DAEMON_WEBSOCKET_PATH } from "../../daemon/config.js";
8
- import { spawnProcess } from "../../utils/child-process.js";
9
- export const DEFAULT_SANDBOX_IMAGE = "ghcr.io/greppleai/grip-sandbox:latest";
10
- export const DEFAULT_SANDBOX_WORKSPACE_MOUNT = "/workspace";
11
- export const DEFAULT_SANDBOX_DAEMON_PORT = 8787;
12
- export const DEFAULT_SANDBOX_DOCKER_BINARY = "docker";
13
- export const DEFAULT_SANDBOX_LIMA_BINARY = "limactl";
14
- export const DEFAULT_SANDBOX_LIMA_TEMPLATE = "template:docker-rootful";
15
- export const SANDBOX_LABEL_PREFIX = "ai.greppleai.grip";
16
- export const SANDBOX_LABEL_ENABLED = `${SANDBOX_LABEL_PREFIX}.sandbox`;
17
- export const SANDBOX_LABEL_WORKSPACE_HASH = `${SANDBOX_LABEL_PREFIX}.workspace-hash`;
18
- export const SANDBOX_LABEL_WORKSPACE_MOUNT = `${SANDBOX_LABEL_PREFIX}.workspace-mount`;
19
- export const SANDBOX_LABEL_DAEMON_PORT = `${SANDBOX_LABEL_PREFIX}.daemon-port`;
20
- export const SANDBOX_LABEL_OWNER_UID = `${SANDBOX_LABEL_PREFIX}.owner-uid`;
21
- export const SANDBOX_LABEL_VERSION = `${SANDBOX_LABEL_PREFIX}.version`;
22
- export const SANDBOX_LABEL_SESSION = `${SANDBOX_LABEL_PREFIX}.session`;
23
- const reservedSandboxDaemonPorts = new Set();
24
- const sandboxPortLockPath = join(tmpdir(), `greppleai-grip-sandbox-port-${process.getuid?.() ?? "user"}.lock`);
25
- let sandboxStartQueue = Promise.resolve();
26
- function isProcessAlive(pid) {
27
- try {
28
- process.kill(pid, 0);
29
- return true;
30
- }
31
- catch (error) {
32
- return error instanceof Error && "code" in error && error.code === "EPERM";
33
- }
34
- }
35
- async function isSandboxPortLockStale() {
36
- try {
37
- const owner = JSON.parse(await readFile(sandboxPortLockPath, "utf8"));
38
- return typeof owner.pid === "number" && !isProcessAlive(owner.pid);
39
- }
40
- catch {
41
- try {
42
- return Date.now() - (await stat(sandboxPortLockPath)).mtimeMs > 10_000;
43
- }
44
- catch {
45
- return false;
46
- }
47
- }
48
- }
49
- async function acquireSandboxPortLock() {
50
- const token = `${process.pid}:${randomBytes(16).toString("hex")}`;
51
- const deadline = Date.now() + 30_000;
52
- while (Date.now() < deadline) {
53
- try {
54
- const handle = await open(sandboxPortLockPath, "wx", 0o600);
55
- await handle.writeFile(JSON.stringify({ pid: process.pid, token }));
56
- return async () => {
57
- await handle.close();
58
- try {
59
- const current = JSON.parse(await readFile(sandboxPortLockPath, "utf8"));
60
- if (current.token === token)
61
- await unlink(sandboxPortLockPath);
62
- }
63
- catch { }
64
- };
65
- }
66
- catch (error) {
67
- if (!(error instanceof Error && "code" in error && error.code === "EEXIST"))
68
- throw error;
69
- const stale = await isSandboxPortLockStale();
70
- if (stale) {
71
- await unlink(sandboxPortLockPath).catch(() => undefined);
72
- continue;
73
- }
74
- await new Promise((resolveDelay) => setTimeout(resolveDelay, 50));
75
- }
76
- }
77
- throw new Error("Timed out waiting for the sandbox daemon port allocation lock");
78
- }
79
- async function withSandboxStartLock(operation) {
80
- const previous = sandboxStartQueue;
81
- let release;
82
- sandboxStartQueue = new Promise((resolveQueue) => {
83
- release = resolveQueue;
84
- });
85
- await previous;
86
- let releaseFileLock;
87
- try {
88
- releaseFileLock = await acquireSandboxPortLock();
89
- return await operation();
90
- }
91
- finally {
92
- try {
93
- await releaseFileLock?.();
94
- }
95
- finally {
96
- release();
97
- }
98
- }
99
- }
100
- class ProcessDockerRunner {
101
- run(command, args, options = {}) {
102
- return new Promise((resolveCommand, reject) => {
103
- const child = spawnProcess(command, args, {
104
- stdio: ["ignore", "pipe", "pipe"],
105
- env: { ...process.env, ...options.env },
106
- });
107
- let stdout = "";
108
- let stderr = "";
109
- child.stdout?.on("data", (chunk) => {
110
- stdout += chunk.toString("utf8");
111
- });
112
- child.stderr?.on("data", (chunk) => {
113
- stderr += chunk.toString("utf8");
114
- });
115
- child.once("error", reject);
116
- child.once("close", (exitCode) => resolveCommand({ exitCode, stdout, stderr }));
117
- });
118
- }
119
- }
120
- function nonEmpty(value) {
121
- const trimmed = value?.trim();
122
- return trimmed ? trimmed : undefined;
123
- }
124
- function parseRuntime(value, settingName) {
125
- if (value === undefined)
126
- return undefined;
127
- if (value === "docker" || value === "lima")
128
- return value;
129
- throw new Error(`Invalid ${settingName}: expected docker or lima`);
130
- }
131
- function parsePositiveInteger(value, settingName) {
132
- if (value === undefined)
133
- return undefined;
134
- const text = String(value).trim();
135
- if (!/^\d+$/.test(text))
136
- throw new Error(`Invalid ${settingName}: expected a decimal integer`);
137
- const parsed = Number(text);
138
- if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 65535) {
139
- throw new Error(`Invalid ${settingName}: expected a port from 1 to 65535`);
140
- }
141
- return parsed;
142
- }
143
- function parseCleanup(value, settingName) {
144
- if (value === undefined)
145
- return undefined;
146
- if (value === "stop" || value === "remove")
147
- return value;
148
- throw new Error(`Invalid ${settingName}: expected stop or remove`);
149
- }
150
- function parseLabels(text) {
151
- const labels = {};
152
- if (!text)
153
- return labels;
154
- for (const entry of text.split(",")) {
155
- const separator = entry.indexOf("=");
156
- if (separator === -1)
157
- continue;
158
- labels[entry.slice(0, separator)] = entry.slice(separator + 1);
159
- }
160
- return labels;
161
- }
162
- function sanitizeNameSegment(value) {
163
- const sanitized = value
164
- .toLowerCase()
165
- .replace(/[^a-z0-9_.-]+/g, "-")
166
- .replace(/^-+|-+$/g, "");
167
- return sanitized || "workspace";
168
- }
169
- function getWorkspaceHash(workspaceRoot) {
170
- return createHash("sha256").update(resolve(workspaceRoot)).digest("hex").slice(0, 16);
171
- }
172
- function getOwnerUid() {
173
- const uid = process.getuid?.();
174
- return uid === undefined ? undefined : String(uid);
175
- }
176
- function generateToken() {
177
- return randomBytes(32).toString("base64url");
178
- }
179
- async function tryReservePort(host, port) {
180
- return new Promise((resolvePort) => {
181
- const server = createServer();
182
- server.unref();
183
- server.once("error", () => resolvePort(undefined));
184
- server.listen({ host, port, exclusive: true }, () => {
185
- const address = server.address();
186
- const selectedPort = typeof address === "object" && address ? address.port : undefined;
187
- server.close(() => resolvePort(selectedPort));
188
- });
189
- });
190
- }
191
- export async function allocateSandboxDaemonPort(host, preferredPort) {
192
- const preferred = await tryReservePort(host, preferredPort);
193
- if (preferred !== undefined)
194
- return preferred;
195
- const fallback = await tryReservePort(host, 0);
196
- if (fallback === undefined)
197
- throw new Error(`Unable to allocate a sandbox daemon port on ${host}`);
198
- return fallback;
199
- }
200
- async function canConnect(host, port) {
201
- return new Promise((resolveConnection) => {
202
- const socket = createConnection({ host, port });
203
- let settled = false;
204
- const finish = (connected) => {
205
- if (settled)
206
- return;
207
- settled = true;
208
- socket.destroy();
209
- resolveConnection(connected);
210
- };
211
- socket.setTimeout(250, () => finish(false));
212
- socket.once("connect", () => finish(true));
213
- socket.once("error", () => finish(false));
214
- });
215
- }
216
- async function waitForSandboxDaemonPort(host, port) {
217
- const connectHost = host === "0.0.0.0" ? "127.0.0.1" : host;
218
- const deadline = Date.now() + 15_000;
219
- while (Date.now() < deadline) {
220
- if (await canConnect(connectHost, port))
221
- return;
222
- await new Promise((resolveDelay) => setTimeout(resolveDelay, 100));
223
- }
224
- throw new Error(`Sandbox daemon did not bind ${connectHost}:${port} within 15 seconds`);
225
- }
226
- function ensureSuccessful(result, args) {
227
- if (result.exitCode === 0)
228
- return;
229
- const detail = result.stderr.trim() || result.stdout.trim() || `exit code ${result.exitCode ?? "unknown"}`;
230
- throw new Error(redactSecrets(`Docker command failed: docker ${args.join(" ")}: ${detail}`));
231
- }
232
- function parseJsonLines(text) {
233
- return text
234
- .split(/\r?\n/)
235
- .map((line) => line.trim())
236
- .filter((line) => line.length > 0)
237
- .map((line) => JSON.parse(line));
238
- }
239
- function parseJsonArray(text) {
240
- const trimmed = text.trim();
241
- if (!trimmed)
242
- return [];
243
- const parsed = JSON.parse(trimmed);
244
- return Array.isArray(parsed) ? parsed : [parsed];
245
- }
246
- function getEndpointFromPorts(ports, daemonPort) {
247
- const bindings = ports?.[`${daemonPort}/tcp`];
248
- const binding = bindings?.[0];
249
- if (!binding?.HostPort)
250
- return undefined;
251
- const host = daemonConnectionHost(binding.HostIp || "127.0.0.1");
252
- return `ws://${urlHost(host)}:${binding.HostPort}${DAEMON_WEBSOCKET_PATH}`;
253
- }
254
- export function redactSecrets(value) {
255
- return value
256
- .replace(/(GRIP_DAEMON_TOKEN=)[^\s,;]+/g, "$1[REDACTED]")
257
- .replace(/(PI_DAEMON_TOKEN=)[^\s,;]+/g, "$1[REDACTED]")
258
- .replace(/(Authorization:\s*Bearer\s+)[^\s,;]+/gi, "$1[REDACTED]")
259
- .replace(/([?&]token=)[^\s&]+/gi, "$1[REDACTED]");
260
- }
261
- export function resolveSandboxConfig(settings, env = {}, overrides = {}) {
262
- const runtime = parseRuntime(overrides.runtime, "sandbox.runtime") ??
263
- parseRuntime(env.GRIP_SANDBOX_RUNTIME, "GRIP_SANDBOX_RUNTIME") ??
264
- parseRuntime(settings?.runtime, "sandbox.runtime") ??
265
- "docker";
266
- const image = nonEmpty(overrides.image) ??
267
- nonEmpty(env.GRIP_SANDBOX_IMAGE) ??
268
- nonEmpty(settings?.image) ??
269
- DEFAULT_SANDBOX_IMAGE;
270
- const dockerBinary = nonEmpty(overrides.dockerBinary) ??
271
- nonEmpty(env.GRIP_SANDBOX_DOCKER) ??
272
- nonEmpty(settings?.dockerBinary) ??
273
- DEFAULT_SANDBOX_DOCKER_BINARY;
274
- const limaBinary = nonEmpty(overrides.limaBinary) ??
275
- nonEmpty(env.GRIP_SANDBOX_LIMA) ??
276
- nonEmpty(settings?.limaBinary) ??
277
- DEFAULT_SANDBOX_LIMA_BINARY;
278
- const limaTemplate = nonEmpty(overrides.limaTemplate) ??
279
- nonEmpty(env.GRIP_SANDBOX_LIMA_TEMPLATE) ??
280
- nonEmpty(settings?.limaTemplate) ??
281
- DEFAULT_SANDBOX_LIMA_TEMPLATE;
282
- const limaInstanceNamePrefix = nonEmpty(overrides.limaInstanceNamePrefix) ??
283
- nonEmpty(env.GRIP_SANDBOX_LIMA_INSTANCE_PREFIX) ??
284
- nonEmpty(settings?.limaInstanceNamePrefix) ??
285
- "grip-sandbox";
286
- const workspaceMountPath = nonEmpty(overrides.workspaceMountPath) ??
287
- nonEmpty(env.GRIP_SANDBOX_WORKSPACE_MOUNT) ??
288
- nonEmpty(settings?.workspaceMountPath) ??
289
- DEFAULT_SANDBOX_WORKSPACE_MOUNT;
290
- const containerNamePrefix = nonEmpty(overrides.containerNamePrefix) ??
291
- nonEmpty(env.GRIP_SANDBOX_CONTAINER_PREFIX) ??
292
- nonEmpty(settings?.containerNamePrefix) ??
293
- "grip-sandbox";
294
- const daemonPort = overrides.daemonPort ??
295
- parsePositiveInteger(env.GRIP_SANDBOX_DAEMON_PORT, "GRIP_SANDBOX_DAEMON_PORT") ??
296
- parsePositiveInteger(settings?.daemonPort, "sandbox.daemonPort") ??
297
- DEFAULT_SANDBOX_DAEMON_PORT;
298
- const daemonHostBind = nonEmpty(overrides.daemonHostBind) ??
299
- nonEmpty(env.GRIP_SANDBOX_DAEMON_HOST_BIND) ??
300
- nonEmpty(settings?.daemonHostBind) ??
301
- "127.0.0.1";
302
- const cleanup = overrides.cleanup ??
303
- parseCleanup(env.GRIP_SANDBOX_CLEANUP, "GRIP_SANDBOX_CLEANUP") ??
304
- parseCleanup(settings?.cleanup, "sandbox.cleanup") ??
305
- "stop";
306
- return {
307
- runtime,
308
- image,
309
- dockerBinary,
310
- limaBinary,
311
- limaTemplate,
312
- limaInstanceNamePrefix,
313
- workspaceMountPath,
314
- containerNamePrefix,
315
- daemonPort,
316
- daemonHostBind,
317
- cleanup,
318
- ownerUid: getOwnerUid(),
319
- };
320
- }
321
- function dockerPublishHost(host) {
322
- return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
323
- }
324
- function daemonConnectionHost(host) {
325
- if (host === "0.0.0.0")
326
- return "127.0.0.1";
327
- if (host === "::")
328
- return "::1";
329
- return host;
330
- }
331
- function urlHost(host) {
332
- return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
333
- }
334
- export function createSandboxLabels(config, workspaceRoot, sessionId) {
335
- const labels = {
336
- [SANDBOX_LABEL_ENABLED]: "true",
337
- [SANDBOX_LABEL_WORKSPACE_HASH]: getWorkspaceHash(workspaceRoot),
338
- [SANDBOX_LABEL_WORKSPACE_MOUNT]: config.workspaceMountPath,
339
- [SANDBOX_LABEL_DAEMON_PORT]: String(config.daemonPort),
340
- [SANDBOX_LABEL_VERSION]: VERSION,
341
- [SANDBOX_LABEL_SESSION]: sessionId,
342
- };
343
- if (config.ownerUid)
344
- labels[SANDBOX_LABEL_OWNER_UID] = config.ownerUid;
345
- return labels;
346
- }
347
- export function createSandboxContainerName(config, workspaceRoot, sessionId, instanceId) {
348
- const base = `${sanitizeNameSegment(config.containerNamePrefix)}-${sanitizeNameSegment(basename(resolve(workspaceRoot)))}-${getWorkspaceHash(workspaceRoot)}-${sanitizeNameSegment(sessionId).slice(0, 16)}`;
349
- return instanceId ? `${base}-${sanitizeNameSegment(instanceId).slice(0, 8)}` : base;
350
- }
351
- export function buildDockerRunInvocation(config, options, token = generateToken()) {
352
- const workspaceRoot = resolve(options.workspaceRoot);
353
- const sessionId = options.sessionId ?? randomBytes(8).toString("hex");
354
- const labels = createSandboxLabels(config, workspaceRoot, sessionId);
355
- const containerName = createSandboxContainerName(config, workspaceRoot, sessionId, createHash("sha256").update(token).digest("hex"));
356
- const args = [
357
- "run",
358
- "--detach",
359
- "--network",
360
- "bridge",
361
- "--publish",
362
- `${dockerPublishHost(config.daemonHostBind)}:${config.daemonPort}:${config.daemonPort}`,
363
- "--name",
364
- containerName,
365
- "--workdir",
366
- config.workspaceMountPath,
367
- "--mount",
368
- `type=bind,source=${workspaceRoot},target=${config.workspaceMountPath}`,
369
- "--env",
370
- "GRIP_DAEMON_TOKEN",
371
- ];
372
- for (const [key, value] of Object.entries(labels))
373
- args.push("--label", `${key}=${value}`);
374
- args.push(config.image, "grip", "--daemon", "--daemon-host", "0.0.0.0", "--daemon-allow-insecure-transport", "--daemon-port", String(config.daemonPort), "--daemon-cwd", config.workspaceMountPath, "--daemon-allow-root", "--daemon-allow-process-exec");
375
- return {
376
- command: config.dockerBinary,
377
- args,
378
- env: { GRIP_DAEMON_TOKEN: token },
379
- token,
380
- containerName,
381
- labels,
382
- workspaceRoot,
383
- workspaceHash: labels[SANDBOX_LABEL_WORKSPACE_HASH],
384
- };
385
- }
386
- export class DockerSandboxService {
387
- settingsManager;
388
- env;
389
- runner;
390
- tokenGenerator;
391
- portAllocator;
392
- readinessWaiter;
393
- managedContainers;
394
- constructor(options = {}) {
395
- this.settingsManager = options.settingsManager;
396
- this.env = options.env ?? process.env;
397
- this.runner = options.runner ?? new ProcessDockerRunner();
398
- this.tokenGenerator = options.tokenGenerator ?? generateToken;
399
- this.portAllocator = options.portAllocator ?? allocateSandboxDaemonPort;
400
- this.readinessWaiter = options.readinessWaiter ?? waitForSandboxDaemonPort;
401
- this.managedContainers = options.managedContainers ?? new Map();
402
- }
403
- resolveConfig(overrides = {}) {
404
- return resolveSandboxConfig(this.settingsManager?.getSandboxSettings(), this.env, overrides);
405
- }
406
- buildStartInvocation(options) {
407
- return buildDockerRunInvocation(this.resolveConfig(options), options, this.tokenGenerator());
408
- }
409
- async checkDockerAvailable(config = this.resolveConfig()) {
410
- const args = ["version", "--format", "{{.Server.Version}}"];
411
- try {
412
- ensureSuccessful(await this.runner.run(config.dockerBinary, args), args);
413
- }
414
- catch (error) {
415
- const message = error instanceof Error ? error.message : String(error);
416
- throw new Error(redactSecrets(`Docker is unavailable or not reachable via ${config.dockerBinary}: ${message}`));
417
- }
418
- }
419
- async reserveDaemonPort(config) {
420
- for (let attempt = 0; attempt < 16; attempt++) {
421
- const preferredPort = reservedSandboxDaemonPorts.has(config.daemonPort) ? 0 : config.daemonPort;
422
- const candidate = await this.portAllocator(config.daemonHostBind, preferredPort);
423
- if (!reservedSandboxDaemonPorts.has(candidate))
424
- return candidate;
425
- }
426
- throw new Error("Unable to reserve a unique sandbox daemon port");
427
- }
428
- async waitForStartedSandbox(config, containerId) {
429
- try {
430
- await this.readinessWaiter(config.daemonHostBind, config.daemonPort);
431
- }
432
- catch (error) {
433
- const cleanupArgs = ["rm", "--force", containerId];
434
- const cleanupResult = await this.runner.run(config.dockerBinary, cleanupArgs);
435
- if (cleanupResult.exitCode !== 0) {
436
- const cleanupError = new Error(cleanupResult.stderr.trim() || cleanupResult.stdout.trim() || "Failed to remove unready sandbox");
437
- throw new AggregateError([error, cleanupError], "Sandbox readiness and cleanup both failed");
438
- }
439
- this.managedContainers.delete(containerId);
440
- throw error;
441
- }
442
- }
443
- async startWithReservedPort(options, daemonPort) {
444
- const effectiveOptions = { ...options, daemonPort };
445
- const config = this.resolveConfig(effectiveOptions);
446
- const invocation = buildDockerRunInvocation(config, effectiveOptions, this.tokenGenerator());
447
- await this.checkDockerAvailable(config);
448
- const runResult = await this.runner.run(invocation.command, invocation.args, { env: invocation.env });
449
- ensureSuccessful(runResult, invocation.args);
450
- const containerId = runResult.stdout.trim();
451
- this.managedContainers.set(containerId, {
452
- workspaceRoot: invocation.workspaceRoot,
453
- daemonPort: config.daemonPort,
454
- ownerId: options.sessionId,
455
- });
456
- await this.waitForStartedSandbox(config, containerId);
457
- const daemonHost = daemonConnectionHost(config.daemonHostBind);
458
- const daemonUrl = `ws://${urlHost(daemonHost)}:${config.daemonPort}${DAEMON_WEBSOCKET_PATH}`;
459
- return {
460
- containerId,
461
- containerName: invocation.containerName,
462
- workspaceRoot: invocation.workspaceRoot,
463
- workspaceMountPath: config.workspaceMountPath,
464
- daemonUrl,
465
- daemonUrlRedacted: redactSecrets(daemonUrl),
466
- token: invocation.token,
467
- labels: invocation.labels,
468
- };
469
- }
470
- async startLocked(options) {
471
- const configured = this.resolveConfig(options);
472
- const daemonPort = await this.reserveDaemonPort(configured);
473
- reservedSandboxDaemonPorts.add(daemonPort);
474
- try {
475
- return await this.startWithReservedPort(options, daemonPort);
476
- }
477
- catch (error) {
478
- if (![...this.managedContainers.values()].some((container) => container.daemonPort === daemonPort)) {
479
- reservedSandboxDaemonPorts.delete(daemonPort);
480
- }
481
- throw error;
482
- }
483
- }
484
- async start(options) {
485
- return withSandboxStartLock(() => this.startLocked(options));
486
- }
487
- async list(options) {
488
- const config = this.resolveConfig();
489
- const args = [
490
- "container",
491
- "ls",
492
- "--all",
493
- "--filter",
494
- `label=${SANDBOX_LABEL_ENABLED}=true`,
495
- ...(config.ownerUid ? ["--filter", `label=${SANDBOX_LABEL_OWNER_UID}=${config.ownerUid}`] : []),
496
- ];
497
- if (!options.allWorkspaces) {
498
- args.push("--filter", `label=${SANDBOX_LABEL_WORKSPACE_HASH}=${getWorkspaceHash(options.workspaceRoot)}`);
499
- }
500
- args.push("--format", "{{json .}}");
501
- const result = await this.runner.run(config.dockerBinary, args);
502
- ensureSuccessful(result, args);
503
- const rows = parseJsonLines(result.stdout);
504
- return rows.filter((row) => row.ID || row.Names).map((row) => this.containerFromPsRecord(row));
505
- }
506
- forgetManagedContainer(reference, workspaceRoot) {
507
- const canonicalWorkspaceRoot = resolve(workspaceRoot);
508
- for (const [containerId, container] of this.managedContainers) {
509
- if (container.workspaceRoot === canonicalWorkspaceRoot && containerIdsMatch(containerId, reference)) {
510
- this.managedContainers.delete(containerId);
511
- reservedSandboxDaemonPorts.delete(container.daemonPort);
512
- }
513
- }
514
- }
515
- forgetStoppedContainer(container, workspaceRoot) {
516
- this.forgetManagedContainer(container.id, workspaceRoot);
517
- if (container.daemonPort !== undefined)
518
- reservedSandboxDaemonPorts.delete(container.daemonPort);
519
- }
520
- async stop(options) {
521
- const containers = await this.list({ workspaceRoot: options.workspaceRoot });
522
- const selected = selectStopTarget(containers, options.target, options.currentContainerId);
523
- if (!selected) {
524
- const requestedId = options.currentContainerId ?? options.target;
525
- if (requestedId)
526
- this.forgetManagedContainer(requestedId, options.workspaceRoot);
527
- return { status: "not-found", message: "No matching Grip sandbox container found" };
528
- }
529
- const config = this.resolveConfig();
530
- if (selected.state && selected.state !== "running" && config.cleanup === "stop") {
531
- this.forgetStoppedContainer(selected, options.workspaceRoot);
532
- return { status: "already-stopped", container: selected };
533
- }
534
- const args = [
535
- config.cleanup === "remove" ? "rm" : "stop",
536
- ...(config.cleanup === "remove" ? ["--force"] : []),
537
- selected.id,
538
- ];
539
- const result = await this.runner.run(config.dockerBinary, args);
540
- if (result.exitCode !== 0) {
541
- const output = `${result.stderr}\n${result.stdout}`;
542
- if (/No such container|not found/i.test(output)) {
543
- this.forgetStoppedContainer(selected, options.workspaceRoot);
544
- return { status: "not-found", message: "Sandbox container no longer exists" };
545
- }
546
- ensureSuccessful(result, args);
547
- }
548
- this.forgetStoppedContainer(selected, options.workspaceRoot);
549
- return { status: config.cleanup === "remove" ? "removed" : "stopped", container: selected };
550
- }
551
- async stopManagedContainers() {
552
- await withSandboxStartLock(async () => {
553
- const failures = [];
554
- for (const [containerId, container] of [...this.managedContainers]) {
555
- try {
556
- await this.stop({
557
- workspaceRoot: container.workspaceRoot,
558
- target: containerId,
559
- currentContainerId: containerId,
560
- });
561
- this.managedContainers.delete(containerId);
562
- reservedSandboxDaemonPorts.delete(container.daemonPort);
563
- }
564
- catch (error) {
565
- failures.push(error);
566
- }
567
- }
568
- if (failures.length > 0)
569
- throw new AggregateError(failures, "Failed to stop managed sandbox containers");
570
- });
571
- }
572
- containerFromPsRecord(row) {
573
- const labels = parseLabels(row.Labels);
574
- const daemonPort = parsePositiveInteger(labels[`${SANDBOX_LABEL_PREFIX}.daemon-port`] ?? undefined, "daemon port");
575
- return {
576
- id: row.ID ?? "",
577
- name: row.Names ?? "",
578
- image: row.Image ?? "",
579
- status: row.Status ?? "",
580
- state: row.State,
581
- createdAt: row.CreatedAt,
582
- workspaceRoot: undefined,
583
- workspaceMountPath: labels[SANDBOX_LABEL_WORKSPACE_MOUNT],
584
- daemonPort,
585
- daemonEndpoint: row.Ports ? redactSecrets(row.Ports) : undefined,
586
- labels,
587
- };
588
- }
589
- }
590
- export function parseDockerPortOutput(output) {
591
- const firstLine = output.trim().split(/\r?\n/)[0];
592
- const match = firstLine?.match(/^(?<host>.+):(?<port>\d+)$/);
593
- if (!match?.groups)
594
- throw new Error(`Unable to parse Docker daemon port output: ${output.trim()}`);
595
- const host = match.groups.host === "0.0.0.0" ? "127.0.0.1" : match.groups.host;
596
- return `ws://${host}:${match.groups.port}${DAEMON_WEBSOCKET_PATH}`;
597
- }
598
- export function parseDockerInspectContainers(output, daemonPort = DEFAULT_SANDBOX_DAEMON_PORT) {
599
- return parseJsonArray(output).map((record) => {
600
- const labels = record.Config?.Labels ?? {};
601
- const mountPath = labels[SANDBOX_LABEL_WORKSPACE_MOUNT] ?? DEFAULT_SANDBOX_WORKSPACE_MOUNT;
602
- const mount = record.Mounts?.find((candidate) => candidate.Destination === mountPath);
603
- const name = record.Name?.replace(/^\//, "") ?? "";
604
- return {
605
- id: record.Id ?? "",
606
- name,
607
- image: record.Config?.Image ?? "",
608
- status: record.State?.Status ?? "",
609
- state: record.State?.Running ? "running" : record.State?.Status,
610
- createdAt: record.State?.StartedAt ?? record.Created,
611
- workspaceRoot: mount?.Source,
612
- workspaceMountPath: mount?.Destination ?? mountPath,
613
- daemonPort,
614
- daemonEndpoint: getEndpointFromPorts(record.NetworkSettings?.Ports, daemonPort),
615
- labels,
616
- };
617
- });
618
- }
619
- function containerIdsMatch(left, right) {
620
- return left === right || left.startsWith(right) || right.startsWith(left);
621
- }
622
- function selectStopTarget(containers, target, currentContainerId) {
623
- if (target) {
624
- return containers.find((container) => containerIdsMatch(container.id, target) || container.name === target);
625
- }
626
- if (currentContainerId) {
627
- const current = containers.find((container) => containerIdsMatch(container.id, currentContainerId));
628
- if (current)
629
- return current;
630
- }
631
- return containers.length === 1 ? containers[0] : undefined;
632
- }
633
- //# sourceMappingURL=docker.js.map