@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.
- package/CHANGELOG.md +114 -0
- package/README.md +13 -7
- package/dist/bun/cli.d.ts.map +1 -1
- package/dist/bun/cli.js +4 -2
- package/dist/bun/cli.js.map +1 -1
- package/dist/bun/embedded-assets.d.ts +3 -0
- package/dist/bun/embedded-assets.d.ts.map +1 -0
- package/dist/bun/embedded-assets.js +52 -0
- package/dist/bun/embedded-assets.js.map +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -3
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +17 -2
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/types.d.ts +5 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/remote-workspace-protocol/contract.d.ts +13 -4
- package/dist/core/remote-workspace-protocol/contract.d.ts.map +1 -1
- package/dist/core/remote-workspace-protocol/contract.js +17 -1
- package/dist/core/remote-workspace-protocol/contract.js.map +1 -1
- package/dist/core/remote-workspace-protocol/guest-shared-client.d.ts +16 -0
- package/dist/core/remote-workspace-protocol/guest-shared-client.d.ts.map +1 -0
- package/dist/core/remote-workspace-protocol/guest-shared-client.js +54 -0
- package/dist/core/remote-workspace-protocol/guest-shared-client.js.map +1 -0
- package/dist/core/remote-workspace-protocol/guest-shared-outbound-scheduler.d.ts +23 -0
- package/dist/core/remote-workspace-protocol/guest-shared-outbound-scheduler.d.ts.map +1 -0
- package/dist/core/remote-workspace-protocol/guest-shared-outbound-scheduler.js +112 -0
- package/dist/core/remote-workspace-protocol/guest-shared-outbound-scheduler.js.map +1 -0
- package/dist/core/remote-workspace-protocol/guest-shared-uplink.d.ts +30 -0
- package/dist/core/remote-workspace-protocol/guest-shared-uplink.d.ts.map +1 -0
- package/dist/core/remote-workspace-protocol/guest-shared-uplink.js +243 -0
- package/dist/core/remote-workspace-protocol/guest-shared-uplink.js.map +1 -0
- package/dist/core/remote-workspace-protocol/lima-owner-directory.d.ts +3 -0
- package/dist/core/remote-workspace-protocol/lima-owner-directory.d.ts.map +1 -0
- package/dist/core/remote-workspace-protocol/lima-owner-directory.js +31 -0
- package/dist/core/remote-workspace-protocol/lima-owner-directory.js.map +1 -0
- package/dist/core/remote-workspace-protocol/lima-owner-lock.d.ts +9 -0
- package/dist/core/remote-workspace-protocol/lima-owner-lock.d.ts.map +1 -0
- package/dist/core/remote-workspace-protocol/lima-owner-lock.js +89 -0
- package/dist/core/remote-workspace-protocol/lima-owner-lock.js.map +1 -0
- package/dist/core/remote-workspace-protocol/session.d.ts +2 -0
- package/dist/core/remote-workspace-protocol/session.d.ts.map +1 -1
- package/dist/core/remote-workspace-protocol/session.js +3 -0
- package/dist/core/remote-workspace-protocol/session.js.map +1 -1
- package/dist/core/remote-workspace-protocol/shared-transport.d.ts +71 -0
- package/dist/core/remote-workspace-protocol/shared-transport.d.ts.map +1 -0
- package/dist/core/remote-workspace-protocol/shared-transport.js +68 -0
- package/dist/core/remote-workspace-protocol/shared-transport.js.map +1 -0
- package/dist/core/sandbox/command.d.ts +2 -4
- package/dist/core/sandbox/command.d.ts.map +1 -1
- package/dist/core/sandbox/command.js +24 -46
- package/dist/core/sandbox/command.js.map +1 -1
- package/dist/core/sandbox/config.d.ts +91 -0
- package/dist/core/sandbox/config.d.ts.map +1 -0
- package/dist/core/sandbox/config.js +88 -0
- package/dist/core/sandbox/config.js.map +1 -0
- package/dist/core/sandbox/lima-guest-binary.d.ts +11 -0
- package/dist/core/sandbox/lima-guest-binary.d.ts.map +1 -0
- package/dist/core/sandbox/lima-guest-binary.js +140 -0
- package/dist/core/sandbox/lima-guest-binary.js.map +1 -0
- package/dist/core/sandbox/lima.d.ts +22 -3
- package/dist/core/sandbox/lima.d.ts.map +1 -1
- package/dist/core/sandbox/lima.js +434 -42
- package/dist/core/sandbox/lima.js.map +1 -1
- package/dist/core/sandbox/shared-lima-roots.d.ts +11 -0
- package/dist/core/sandbox/shared-lima-roots.d.ts.map +1 -0
- package/dist/core/sandbox/shared-lima-roots.js +25 -0
- package/dist/core/sandbox/shared-lima-roots.js.map +1 -0
- package/dist/core/sandbox/update-shared-guest-binary.d.ts +8 -0
- package/dist/core/sandbox/update-shared-guest-binary.d.ts.map +1 -0
- package/dist/core/sandbox/update-shared-guest-binary.js +100 -0
- package/dist/core/sandbox/update-shared-guest-binary.js.map +1 -0
- package/dist/core/settings-manager.d.ts +1 -5
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +25 -14
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/file-mutation-queue.d.ts +2 -0
- package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
- package/dist/core/tools/file-mutation-queue.js +9 -0
- package/dist/core/tools/file-mutation-queue.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +2 -3
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +2 -3
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +2 -3
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/operations.d.ts +13 -2
- package/dist/core/tools/operations.d.ts.map +1 -1
- package/dist/core/tools/operations.js +132 -36
- package/dist/core/tools/operations.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +3 -3
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/render-utils.d.ts +1 -0
- package/dist/core/tools/render-utils.d.ts.map +1 -1
- package/dist/core/tools/render-utils.js +5 -0
- package/dist/core/tools/render-utils.js.map +1 -1
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +2 -3
- package/dist/core/tools/write.js.map +1 -1
- package/dist/daemon/activity-budget.d.ts +10 -0
- package/dist/daemon/activity-budget.d.ts.map +1 -0
- package/dist/daemon/activity-budget.js +34 -0
- package/dist/daemon/activity-budget.js.map +1 -0
- package/dist/daemon/config.d.ts +2 -0
- package/dist/daemon/config.d.ts.map +1 -1
- package/dist/daemon/config.js +15 -0
- package/dist/daemon/config.js.map +1 -1
- package/dist/daemon/outbound-budget.d.ts +10 -0
- package/dist/daemon/outbound-budget.d.ts.map +1 -0
- package/dist/daemon/outbound-budget.js +30 -0
- package/dist/daemon/outbound-budget.js.map +1 -0
- package/dist/daemon/security.d.ts.map +1 -1
- package/dist/daemon/security.js +3 -1
- package/dist/daemon/security.js.map +1 -1
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +104 -24
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/shared-connection.d.ts +33 -0
- package/dist/daemon/shared-connection.d.ts.map +1 -0
- package/dist/daemon/shared-connection.js +271 -0
- package/dist/daemon/shared-connection.js.map +1 -0
- package/dist/daemon/shared-revision.d.ts +5 -0
- package/dist/daemon/shared-revision.d.ts.map +1 -0
- package/dist/daemon/shared-revision.js +61 -0
- package/dist/daemon/shared-revision.js.map +1 -0
- package/dist/daemon/shared-roots.d.ts +9 -0
- package/dist/daemon/shared-roots.d.ts.map +1 -0
- package/dist/daemon/shared-roots.js +33 -0
- package/dist/daemon/shared-roots.js.map +1 -0
- package/dist/daemon/workspace-runtime.d.ts +1 -1
- package/dist/daemon/workspace-runtime.d.ts.map +1 -1
- package/dist/daemon/workspace-runtime.js +42 -10
- package/dist/daemon/workspace-runtime.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +5 -3
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +1 -0
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +13 -3
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.d.ts +4 -3
- package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.js +5 -4
- package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.d.ts +4 -3
- package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.js +5 -4
- package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-message.js +4 -3
- package/dist/modes/interactive/components/custom-message.js.map +1 -1
- package/dist/modes/interactive/components/hook-execution.d.ts +3 -0
- package/dist/modes/interactive/components/hook-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/hook-execution.js +20 -2
- package/dist/modes/interactive/components/hook-execution.js.map +1 -1
- package/dist/modes/interactive/components/message-card.d.ts +9 -0
- package/dist/modes/interactive/components/message-card.d.ts.map +1 -0
- package/dist/modes/interactive/components/message-card.js +24 -0
- package/dist/modes/interactive/components/message-card.js.map +1 -0
- package/dist/modes/interactive/components/skill-invocation-message.d.ts +4 -3
- package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/skill-invocation-message.js +5 -4
- package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +27 -21
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +3 -2
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +1 -3
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +106 -47
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +3 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +10 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/utils/clipboard-native.d.ts.map +1 -1
- package/dist/utils/clipboard-native.js +1 -1
- package/dist/utils/clipboard-native.js.map +1 -1
- package/dist/utils/image-resize.d.ts.map +1 -1
- package/dist/utils/image-resize.js +5 -0
- package/dist/utils/image-resize.js.map +1 -1
- package/dist/utils/photon.d.ts.map +1 -1
- package/dist/utils/photon.js +1 -0
- package/dist/utils/photon.js.map +1 -1
- package/docs/daemon.md +6 -4
- package/docs/docs.json +1 -1
- package/docs/extensions.md +3 -0
- package/docs/index.md +8 -12
- package/docs/packages.md +3 -3
- package/docs/quickstart.md +11 -19
- package/docs/sandbox.md +40 -117
- package/docs/settings.md +12 -11
- package/docs/termux.md +1 -18
- package/docs/usage.md +2 -2
- package/examples/README.md +0 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +5 -6
- package/dist/core/sandbox/docker.d.ts +0 -169
- package/dist/core/sandbox/docker.d.ts.map +0 -1
- package/dist/core/sandbox/docker.js +0 -633
- package/dist/core/sandbox/docker.js.map +0 -1
- package/examples/sandbox-docker/Dockerfile +0 -274
- package/examples/sandbox-docker/README.md +0 -88
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { realpath } from "node:fs/promises";
|
|
2
3
|
import { createConnection } from "node:net";
|
|
3
4
|
import { basename, resolve } from "node:path";
|
|
4
5
|
import { VERSION } from "../../config.js";
|
|
5
6
|
import { DAEMON_WEBSOCKET_PATH } from "../../daemon/config.js";
|
|
6
7
|
import { spawnProcess } from "../../utils/child-process.js";
|
|
7
|
-
import {
|
|
8
|
+
import { acquireSharedLimaOwnerLock } from "../remote-workspace-protocol/lima-owner-lock.js";
|
|
9
|
+
import { allocateSandboxDaemonPort, redactSecrets, resolveSandboxConfig, SANDBOX_LABEL_PREFIX } from "./config.js";
|
|
10
|
+
import { inspectLocalGuestBinary, installLocalGuestBinary } from "./lima-guest-binary.js";
|
|
11
|
+
import { resolveSharedLimaWorkspace } from "./shared-lima-roots.js";
|
|
12
|
+
import { stageSharedGuestBinary } from "./update-shared-guest-binary.js";
|
|
8
13
|
class ProcessLimaRunner {
|
|
9
14
|
run(command, args, options = {}) {
|
|
10
15
|
return new Promise((resolveCommand, reject) => {
|
|
@@ -162,6 +167,258 @@ export class LimaSandboxService {
|
|
|
162
167
|
throw new Error(redactSecrets(`Lima is unavailable or not reachable via ${config.limaBinary}: ${message}`));
|
|
163
168
|
}
|
|
164
169
|
}
|
|
170
|
+
/** Read-only validation: no VM creation, daemon restart, token rotation or ownership claim. */
|
|
171
|
+
async inspectSharedInstance(options) {
|
|
172
|
+
const config = this.resolveConfig();
|
|
173
|
+
const { instanceName } = options;
|
|
174
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(instanceName))
|
|
175
|
+
throw new Error("Invalid shared Lima instance name");
|
|
176
|
+
if (config.daemonHostBind !== "127.0.0.1" && config.daemonHostBind !== "::1") {
|
|
177
|
+
throw new Error("Shared Lima daemon forward must bind to loopback");
|
|
178
|
+
}
|
|
179
|
+
const binding = await resolveSharedLimaWorkspace(options.hostParent, config.workspaceMountPath, options.hostRoot);
|
|
180
|
+
const records = await this.listRecords(config, instanceName);
|
|
181
|
+
if (records.length !== 1 || records[0]?.name !== instanceName) {
|
|
182
|
+
throw new Error(`Shared Lima instance ${instanceName} does not exist by exact name`);
|
|
183
|
+
}
|
|
184
|
+
const record = records[0];
|
|
185
|
+
if (record.status?.toLowerCase() !== "running") {
|
|
186
|
+
throw new Error(`Shared Lima instance ${instanceName} is not running`);
|
|
187
|
+
}
|
|
188
|
+
const mounts = record.config?.mounts?.filter((mount) => mount.mountPoint === binding.guestParent) ?? [];
|
|
189
|
+
if (mounts.length !== 1 || !mounts[0]?.writable || !mounts[0].location) {
|
|
190
|
+
throw new Error(`Shared Lima instance ${instanceName} requires one writable fixed parent mount`);
|
|
191
|
+
}
|
|
192
|
+
if ((await realpath(mounts[0].location)) !== binding.hostParent) {
|
|
193
|
+
throw new Error(`Shared Lima instance ${instanceName} mounts a different host parent`);
|
|
194
|
+
}
|
|
195
|
+
const forwards = record.config?.portForwards?.filter((forward) => forward.guestPort === config.daemonPort || forward.hostPort === config.daemonPort) ?? [];
|
|
196
|
+
if (forwards.length !== 1 ||
|
|
197
|
+
forwards[0]?.guestPort !== config.daemonPort ||
|
|
198
|
+
forwards[0].hostPort !== config.daemonPort ||
|
|
199
|
+
forwards[0].hostIP !== config.daemonHostBind) {
|
|
200
|
+
throw new Error(`Shared Lima instance ${instanceName} requires one matching loopback daemon forward`);
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
...binding,
|
|
204
|
+
instanceName,
|
|
205
|
+
daemonUrl: `ws://${urlHost(config.daemonHostBind)}:${config.daemonPort}${DAEMON_WEBSOCKET_PATH}`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/** Reinspect and reuse the supervised guest daemon under the exclusive named-VM owner lock. */
|
|
209
|
+
async ensureSharedDaemon(options, owner, updated = false) {
|
|
210
|
+
if (owner.instanceName !== options.instanceName)
|
|
211
|
+
throw new Error("Shared Lima owner lock names a different VM");
|
|
212
|
+
await owner.assertHeld();
|
|
213
|
+
await this.inspectSharedInstance(options);
|
|
214
|
+
const config = this.resolveConfig();
|
|
215
|
+
const local = this.env.GRIP_SANDBOX_GUEST_BINARY
|
|
216
|
+
? await inspectLocalGuestBinary(this.env.GRIP_SANDBOX_GUEST_BINARY)
|
|
217
|
+
: undefined;
|
|
218
|
+
const versionArgs = [
|
|
219
|
+
"shell",
|
|
220
|
+
"--tty=false",
|
|
221
|
+
options.instanceName,
|
|
222
|
+
"sh",
|
|
223
|
+
"-lc",
|
|
224
|
+
local || updated ? '"$HOME/.local/bin/grip" --version' : "grip --version",
|
|
225
|
+
];
|
|
226
|
+
const version = await this.runner.run(config.limaBinary, versionArgs);
|
|
227
|
+
if (version.exitCode !== 0 || version.stdout.trim() !== VERSION) {
|
|
228
|
+
throw new Error("Shared Lima grip version mismatch or binary missing; install the matching version manually");
|
|
229
|
+
}
|
|
230
|
+
await owner.assertHeld();
|
|
231
|
+
const directory = "$HOME/.cache/grip/shared-lima";
|
|
232
|
+
// /proc start time plus boot ID distinguishes a recycled PID or a rebooted guest.
|
|
233
|
+
// kill -0 also succeeds for zombies, which must never be reused as a live daemon.
|
|
234
|
+
const processStartTime = `awk '{ sub(/^.*\\) /, ""); if ($1 == "Z" || $1 == "X") exit 1; print $20 }'`;
|
|
235
|
+
const script = [
|
|
236
|
+
"set -eu",
|
|
237
|
+
"umask 077",
|
|
238
|
+
`directory="${directory}"`,
|
|
239
|
+
// Both current modes reserve the guest transition; a crashed reservation is operator-owned.
|
|
240
|
+
'mkdir -p "$HOME/.cache/grip"',
|
|
241
|
+
'mode_lock="$HOME/.cache/grip/daemon-mode.lock"',
|
|
242
|
+
'mkdir "$mode_lock" || exit 28',
|
|
243
|
+
"trap 'rmdir \"$mode_lock\" 2>/dev/null || true' EXIT",
|
|
244
|
+
// A legacy per-session daemon may own this VM or its port. Do not start a second daemon.
|
|
245
|
+
'for legacy_file in "$HOME/.cache/grip"/sandbox-daemon-*.pid "$HOME/.cache/grip"/sandbox-daemon-*.log; do',
|
|
246
|
+
' if test -e "$legacy_file" || test -L "$legacy_file"; then exit 27; fi',
|
|
247
|
+
"done",
|
|
248
|
+
// A transient user service must outlive the Lima SSH session. Do not mutate
|
|
249
|
+
// private state unless the guest manager is running and linger was enabled by an operator.
|
|
250
|
+
"command -v systemd-run >/dev/null && systemctl --user show-environment >/dev/null || exit 29",
|
|
251
|
+
'test "$(loginctl show-user "$(id -un)" --property=Linger --value)" = yes || exit 29',
|
|
252
|
+
`mkdir -p "$directory"; test ! -L "$directory"; chmod 700 "$directory"`,
|
|
253
|
+
'rmdir "$mode_lock" || exit 28',
|
|
254
|
+
"trap - EXIT",
|
|
255
|
+
`token_file="$directory/daemon-${config.daemonPort}.token"`,
|
|
256
|
+
`pid_file="$directory/daemon-${config.daemonPort}.pid"`,
|
|
257
|
+
`identity_file="$directory/daemon-${config.daemonPort}.identity"`,
|
|
258
|
+
`log_file="$directory/daemon-${config.daemonPort}.log"`,
|
|
259
|
+
`start_lock="$directory/daemon-${config.daemonPort}.starting"`,
|
|
260
|
+
`unit="grip-shared-lima-${config.daemonPort}.service"`,
|
|
261
|
+
'test ! -L "$token_file"; test ! -L "$pid_file"; test ! -L "$identity_file"; test ! -L "$log_file"',
|
|
262
|
+
'if test -e "$log_file"; then test -f "$log_file" && test "$(stat -c %a "$log_file")" = 600 || exit 22; fi',
|
|
263
|
+
'test ! -e "$start_lock" && test ! -L "$start_lock" || exit 26',
|
|
264
|
+
'if test -e "$token_file" || test -e "$pid_file" || test -e "$identity_file"; then',
|
|
265
|
+
' test -f "$token_file" && test -f "$pid_file" && test -f "$identity_file" || exit 21',
|
|
266
|
+
' test "$(stat -c %a "$token_file")" = 600 || exit 22',
|
|
267
|
+
' test "$(stat -c %a "$pid_file")" = 600 || exit 22',
|
|
268
|
+
' test "$(stat -c %a "$identity_file")" = 600 || exit 22',
|
|
269
|
+
' pid="$(cat "$pid_file")"',
|
|
270
|
+
" case \"$pid\" in ''|*[!0-9]*) exit 23;; esac",
|
|
271
|
+
' kill -0 "$pid" || exit 24',
|
|
272
|
+
` start_time="$(${processStartTime} "/proc/$pid/stat")" || exit 25`,
|
|
273
|
+
' boot_id="$(cat /proc/sys/kernel/random/boot_id)" || exit 25',
|
|
274
|
+
' test -n "$start_time" && test -n "$boot_id" && test "$(cat "$identity_file")" = "$boot_id:$start_time" || exit 25',
|
|
275
|
+
' test "$(systemctl --user show -P MainPID "$unit")" = "$pid" && systemctl --user is-active --quiet "$unit" || exit 25',
|
|
276
|
+
' token="$(cat "$token_file")"',
|
|
277
|
+
"else",
|
|
278
|
+
' mkdir "$start_lock" || exit 26',
|
|
279
|
+
" trap 'rmdir \"$start_lock\" 2>/dev/null || true' EXIT",
|
|
280
|
+
" token=\"$(head -c 32 /dev/urandom | base64 | tr -d '\\n')\"",
|
|
281
|
+
' printf \'%s\' "$token" >"$token_file"',
|
|
282
|
+
' chmod 600 "$token_file"',
|
|
283
|
+
// The systemd user manager owns the process. It reads the private token file
|
|
284
|
+
// inside the unit, never from a systemd-run argument or environment property.
|
|
285
|
+
local
|
|
286
|
+
? ' grip_bin="$HOME/.local/bin/grip"; test -x "$grip_bin"'
|
|
287
|
+
: ' grip_bin="$(command -v grip)"; case "$grip_bin" in /*) ;; *) exit 25;; esac',
|
|
288
|
+
` systemd-run --user --unit="$unit" --property=Restart=no --property=KillMode=control-group -- sh -c 'set -eu; umask 077; GRIP_DAEMON_TOKEN="$(cat "$HOME/.cache/grip/shared-lima/daemon-${config.daemonPort}.token")"; test -n "$GRIP_DAEMON_TOKEN"; export GRIP_DAEMON_TOKEN; exec "$1" --daemon --daemon-host 127.0.0.1 --daemon-allow-insecure-transport --daemon-port ${config.daemonPort} --daemon-cwd "$2" --daemon-shared-parent "$2" --daemon-allow-root --daemon-allow-process-exec >"$HOME/.cache/grip/shared-lima/daemon-${config.daemonPort}.log" 2>&1 </dev/null' sh "$grip_bin" ${shellQuote(config.workspaceMountPath)} >/dev/null`,
|
|
289
|
+
' pid=""; attempts=0',
|
|
290
|
+
' while test "$attempts" -lt 50; do',
|
|
291
|
+
' pid="$(systemctl --user show -P MainPID "$unit")" || exit 25',
|
|
292
|
+
' case "$pid" in ""|0) sleep 0.1;; *[!0-9]*) exit 25;; *) break;; esac',
|
|
293
|
+
" attempts=$((attempts + 1))",
|
|
294
|
+
" done",
|
|
295
|
+
' case "$pid" in ""|0|*[!0-9]*) exit 25;; esac',
|
|
296
|
+
' systemctl --user is-active --quiet "$unit" || exit 25',
|
|
297
|
+
' printf \'%s\' "$pid" >"$pid_file"',
|
|
298
|
+
' chmod 600 "$pid_file"',
|
|
299
|
+
` start_time="$(${processStartTime} "/proc/$pid/stat")" || exit 25`,
|
|
300
|
+
' boot_id="$(cat /proc/sys/kernel/random/boot_id)" || exit 25',
|
|
301
|
+
' test -n "$start_time" && test -n "$boot_id" || exit 25',
|
|
302
|
+
' printf \'%s:%s\' "$boot_id" "$start_time" >"$identity_file"',
|
|
303
|
+
' chmod 600 "$identity_file"',
|
|
304
|
+
' rmdir "$start_lock" || exit 26',
|
|
305
|
+
" trap - EXIT",
|
|
306
|
+
"fi",
|
|
307
|
+
"printf 'GRIP_SHARED_TOKEN=%s\\n' \"$token\"",
|
|
308
|
+
].join("\n");
|
|
309
|
+
const args = ["shell", "--tty=false", options.instanceName, "sh", "-lc", script];
|
|
310
|
+
const result = await this.runner.run(config.limaBinary, args);
|
|
311
|
+
// Never include stdout/stderr from a credential-bearing shell in an error.
|
|
312
|
+
if (result.exitCode !== 0)
|
|
313
|
+
throw new Error("Shared Lima daemon state is uncertain; reconcile guest PID/token files manually");
|
|
314
|
+
const token = result.stdout.match(/(?:^|\n)GRIP_SHARED_TOKEN=([A-Za-z0-9+/]{43}=)(?:\r?\n|$)/)?.[1];
|
|
315
|
+
if (!token)
|
|
316
|
+
throw new Error("Shared Lima daemon did not return a valid credential; reconcile manually");
|
|
317
|
+
await this.readinessWaiter(config.daemonHostBind, config.daemonPort);
|
|
318
|
+
// Readiness can outlive the owner. Never hand a guest credential to a lost owner.
|
|
319
|
+
await owner.assertHeld();
|
|
320
|
+
return token;
|
|
321
|
+
}
|
|
322
|
+
/** Update only a verified supervised daemon. Staging happens before disconnecting peers. */
|
|
323
|
+
async updateSharedDaemon(options, owner) {
|
|
324
|
+
await owner.assertHeld();
|
|
325
|
+
await this.inspectSharedInstance(options);
|
|
326
|
+
const config = this.resolveConfig();
|
|
327
|
+
const name = options.instanceName;
|
|
328
|
+
const port = config.daemonPort;
|
|
329
|
+
if (!this.env.GRIP_SANDBOX_GUEST_BINARY && process.argv[1]?.endsWith("/packages/coding-agent/src/cli.ts")) {
|
|
330
|
+
throw new Error("Development sandbox update requires GRIP_SANDBOX_GUEST_BINARY pointing to a built Linux Grip executable");
|
|
331
|
+
}
|
|
332
|
+
const local = this.env.GRIP_SANDBOX_GUEST_BINARY
|
|
333
|
+
? await inspectLocalGuestBinary(this.env.GRIP_SANDBOX_GUEST_BINARY)
|
|
334
|
+
: undefined;
|
|
335
|
+
const shell = (script) => this.runner.run(config.limaBinary, ["shell", "--tty=false", name, "sh", "-lc", script]);
|
|
336
|
+
const directory = "$HOME/.cache/grip/shared-lima";
|
|
337
|
+
const unit = `grip-shared-lima-${port}.service`;
|
|
338
|
+
const verify = [
|
|
339
|
+
"set -eu",
|
|
340
|
+
`directory="${directory}"`,
|
|
341
|
+
`unit=${shellQuote(unit)}`,
|
|
342
|
+
'test -d "$directory" && test ! -L "$directory"',
|
|
343
|
+
'test ! -e "$HOME/.cache/grip/daemon-mode.lock" && test ! -L "$HOME/.cache/grip/daemon-mode.lock"',
|
|
344
|
+
`token_file="$directory/daemon-${port}.token"`,
|
|
345
|
+
`pid_file="$directory/daemon-${port}.pid"`,
|
|
346
|
+
`identity_file="$directory/daemon-${port}.identity"`,
|
|
347
|
+
'for file in "$token_file" "$pid_file" "$identity_file"; do test -f "$file" && test ! -L "$file" && test "$(stat -c %a "$file")" = 600; done',
|
|
348
|
+
'pid="$(cat "$pid_file")"; case "$pid" in ""|0|*[!0-9]*) exit 45;; esac',
|
|
349
|
+
'kill -0 "$pid"',
|
|
350
|
+
`start_time="$(awk '{ sub(/^.*\\) /, ""); if ($1 == "Z" || $1 == "X") exit 1; print $20 }' "/proc/$pid/stat")"`,
|
|
351
|
+
'boot_id="$(cat /proc/sys/kernel/random/boot_id)"',
|
|
352
|
+
'test -n "$start_time" && test "$(cat "$identity_file")" = "$boot_id:$start_time"',
|
|
353
|
+
'test "$(systemctl --user show -P MainPID "$unit")" = "$pid" && systemctl --user is-active --quiet "$unit"',
|
|
354
|
+
'case "$(systemctl --user show -P ExecStart "$unit")" in *"$HOME/.local/bin/grip"*) ;; *) exit 46;; esac',
|
|
355
|
+
];
|
|
356
|
+
const preflight = await shell(verify.join("\n"));
|
|
357
|
+
if (preflight.exitCode !== 0) {
|
|
358
|
+
throw new Error("Shared Lima daemon identity or binary path is uncertain; update refused without disconnecting clients");
|
|
359
|
+
}
|
|
360
|
+
const staged = await stageSharedGuestBinary(this.runner, config.limaBinary, name, unit, VERSION, local);
|
|
361
|
+
try {
|
|
362
|
+
await owner.assertHeld();
|
|
363
|
+
const stop = await shell([
|
|
364
|
+
...verify,
|
|
365
|
+
'mkdir "$HOME/.cache/grip/daemon-mode.lock" || exit 45',
|
|
366
|
+
'trap \u0027rmdir "$HOME/.cache/grip/daemon-mode.lock" 2>/dev/null || true\u0027 EXIT',
|
|
367
|
+
'systemctl --user stop "$unit"',
|
|
368
|
+
'test "$(systemctl --user show -P MainPID "$unit")" = 0',
|
|
369
|
+
'! kill -0 "$pid" 2>/dev/null',
|
|
370
|
+
].join("\n"));
|
|
371
|
+
if (stop.exitCode !== 0)
|
|
372
|
+
throw new Error("Shared daemon stop is uncertain; inspect the guest before retrying update");
|
|
373
|
+
await owner.assertHeld();
|
|
374
|
+
await staged.install();
|
|
375
|
+
await owner.assertHeld();
|
|
376
|
+
const restart = await shell([
|
|
377
|
+
"set -eu",
|
|
378
|
+
`directory="${directory}"`,
|
|
379
|
+
`unit=${shellQuote(unit)}`,
|
|
380
|
+
'test -d "$directory" && test ! -L "$directory"',
|
|
381
|
+
`token_file="$directory/daemon-${port}.token"`,
|
|
382
|
+
`pid_file="$directory/daemon-${port}.pid"`,
|
|
383
|
+
`identity_file="$directory/daemon-${port}.identity"`,
|
|
384
|
+
'for file in "$token_file" "$pid_file" "$identity_file"; do test -f "$file" && test ! -L "$file" && test "$(stat -c %a "$file")" = 600; done',
|
|
385
|
+
'test ! -e "$HOME/.cache/grip/daemon-mode.lock" && test ! -L "$HOME/.cache/grip/daemon-mode.lock"',
|
|
386
|
+
'mkdir "$HOME/.cache/grip/daemon-mode.lock" || exit 45',
|
|
387
|
+
'trap \u0027rmdir "$HOME/.cache/grip/daemon-mode.lock" 2>/dev/null || true\u0027 EXIT',
|
|
388
|
+
'old_pid="$(cat "$pid_file")"; case "$old_pid" in ""|0|*[!0-9]*) exit 45;; esac',
|
|
389
|
+
'! kill -0 "$old_pid" 2>/dev/null',
|
|
390
|
+
`test "$("$HOME/.local/bin/grip" --version)" = ${shellQuote(VERSION)}`,
|
|
391
|
+
'load="$(systemctl --user show -P LoadState "$unit" 2>/dev/null || true)"',
|
|
392
|
+
'case "$load" in',
|
|
393
|
+
' loaded) test "$(systemctl --user show -P MainPID "$unit")" = 0; systemctl --user start "$unit" ;;',
|
|
394
|
+
' not-found|"")',
|
|
395
|
+
` systemd-run --user --unit="$unit" --property=Restart=no --property=KillMode=control-group -- sh -c 'set -eu; umask 077; GRIP_DAEMON_TOKEN="$(cat "$HOME/.cache/grip/shared-lima/daemon-${port}.token")"; test -n "$GRIP_DAEMON_TOKEN"; export GRIP_DAEMON_TOKEN; exec "$1" --daemon --daemon-host 127.0.0.1 --daemon-allow-insecure-transport --daemon-port ${port} --daemon-cwd "$2" --daemon-shared-parent "$2" --daemon-allow-root --daemon-allow-process-exec >"$HOME/.cache/grip/shared-lima/daemon-${port}.log" 2>&1 </dev/null' sh "$HOME/.local/bin/grip" ${shellQuote(config.workspaceMountPath)} >/dev/null ;;`,
|
|
396
|
+
" *) exit 45 ;;",
|
|
397
|
+
"esac",
|
|
398
|
+
'pid=""; attempts=0',
|
|
399
|
+
'while test "$attempts" -lt 50; do',
|
|
400
|
+
' pid="$(systemctl --user show -P MainPID "$unit")" || exit 45',
|
|
401
|
+
' case "$pid" in ""|0) sleep 0.1;; *[!0-9]*) exit 45;; *) break;; esac',
|
|
402
|
+
" attempts=$((attempts + 1))",
|
|
403
|
+
"done",
|
|
404
|
+
'case "$pid" in ""|0|*[!0-9]*) exit 45;; esac',
|
|
405
|
+
'kill -0 "$pid"; systemctl --user is-active --quiet "$unit"',
|
|
406
|
+
`start_time="$(awk '{ sub(/^.*\\) /, ""); if ($1 == "Z" || $1 == "X") exit 1; print $20 }' "/proc/$pid/stat")"`,
|
|
407
|
+
'boot_id="$(cat /proc/sys/kernel/random/boot_id)"',
|
|
408
|
+
'test -n "$start_time" && test -n "$boot_id"',
|
|
409
|
+
'printf "%s" "$pid" >"$pid_file"',
|
|
410
|
+
'printf "%s:%s" "$boot_id" "$start_time" >"$identity_file"',
|
|
411
|
+
'chmod 600 "$pid_file" "$identity_file"',
|
|
412
|
+
].join("\n"));
|
|
413
|
+
if (restart.exitCode !== 0)
|
|
414
|
+
throw new Error("Shared daemon restart is uncertain; inspect the guest before retrying update");
|
|
415
|
+
await this.readinessWaiter(config.daemonHostBind, port);
|
|
416
|
+
await owner.assertHeld();
|
|
417
|
+
}
|
|
418
|
+
finally {
|
|
419
|
+
await staged.cleanup();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
165
422
|
async startInstance(config, workspaceRoot, name, daemonPort) {
|
|
166
423
|
const mountExpression = `.mounts = [{"location":${JSON.stringify(workspaceRoot)},"mountPoint":${JSON.stringify(config.workspaceMountPath)},"writable":true}]`;
|
|
167
424
|
const forwardExpression = `.portForwards += [{"guestPort":${daemonPort},"hostPort":${daemonPort},"hostIP":${JSON.stringify(config.daemonHostBind)}}]`;
|
|
@@ -179,29 +436,56 @@ export class LimaSandboxService {
|
|
|
179
436
|
];
|
|
180
437
|
ensureLimaSuccessful(config.limaBinary, await this.runner.run(config.limaBinary, args), args);
|
|
181
438
|
}
|
|
182
|
-
async ensureGrip(config, name, managed) {
|
|
183
|
-
|
|
439
|
+
async ensureGrip(config, name, managed, local) {
|
|
440
|
+
if (local) {
|
|
441
|
+
await installLocalGuestBinary(this.runner, config.limaBinary, name, local, VERSION);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const checkArgs = [
|
|
445
|
+
"shell",
|
|
446
|
+
"--tty=false",
|
|
447
|
+
name,
|
|
448
|
+
"sh",
|
|
449
|
+
"-lc",
|
|
450
|
+
'command -v grip >/dev/null || test -x "$HOME/.local/bin/grip"',
|
|
451
|
+
];
|
|
184
452
|
const check = await this.runner.run(config.limaBinary, checkArgs);
|
|
185
453
|
if (check.exitCode === 0)
|
|
186
454
|
return;
|
|
187
455
|
if (!managed) {
|
|
188
|
-
throw new Error(`Lima instance ${name} does not provide grip; install
|
|
456
|
+
throw new Error(`Lima instance ${name} does not provide grip; install the matching Linux release in the VM first`);
|
|
189
457
|
}
|
|
190
|
-
const
|
|
191
|
-
const script = `set -eu; sudo apt-get update; sudo DEBIAN_FRONTEND=noninteractive apt-get install -y nodejs npm; sudo npm install -g ${shellQuote(packageSpec)}`;
|
|
458
|
+
const script = `set -eu; tmp="$(mktemp)"; trap 'rm -f "$tmp"' EXIT; curl -fsSL -o "$tmp" https://raw.githubusercontent.com/greppleai/grip/main/install.sh; GRIP_VERSION=${shellQuote(`v${VERSION}`)} sh "$tmp"; test -x "$HOME/.local/bin/grip"`;
|
|
192
459
|
const args = ["shell", "--tty=false", name, "sh", "-lc", script];
|
|
193
460
|
ensureLimaSuccessful(config.limaBinary, await this.runner.run(config.limaBinary, args), args);
|
|
194
461
|
}
|
|
462
|
+
/** A legacy named attach or stop must not rotate or kill a shared guest daemon. */
|
|
463
|
+
async assertNoSharedGuestState(config, name, operation = "attach") {
|
|
464
|
+
const script = 'test ! -e "$HOME/.cache/grip/shared-lima" && test ! -L "$HOME/.cache/grip/shared-lima"';
|
|
465
|
+
const args = ["shell", "--tty=false", name, "sh", "-lc", script];
|
|
466
|
+
const result = await this.runner.run(config.limaBinary, args);
|
|
467
|
+
// A guest shell failure is ambiguous too; never guess that shared state is absent.
|
|
468
|
+
if (result.exitCode !== 0) {
|
|
469
|
+
throw new Error(`Lima named VM may host a shared daemon; legacy ${operation} refused; reconcile manually`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
195
472
|
async startDaemon(config, name) {
|
|
196
473
|
const pidFile = `.cache/grip/sandbox-daemon-${config.daemonPort}.pid`;
|
|
197
474
|
const logFile = `.cache/grip/sandbox-daemon-${config.daemonPort}.log`;
|
|
198
475
|
const script = [
|
|
199
476
|
"set -eu",
|
|
477
|
+
"umask 077",
|
|
200
478
|
`mkdir -p "$HOME/.cache/grip"`,
|
|
479
|
+
'mode_lock="$HOME/.cache/grip/daemon-mode.lock"',
|
|
480
|
+
'mkdir "$mode_lock" || exit 28',
|
|
481
|
+
"trap 'rmdir \"$mode_lock\" 2>/dev/null || true' EXIT",
|
|
482
|
+
'test ! -e "$HOME/.cache/grip/shared-lima" && test ! -L "$HOME/.cache/grip/shared-lima" || exit 29',
|
|
201
483
|
`if test -f "$HOME/${pidFile}"; then old_pid="$(cat "$HOME/${pidFile}")"; kill "$old_pid" 2>/dev/null || true; fi`,
|
|
202
484
|
`token="$(head -c 32 /dev/urandom | base64 | tr -d '\\n')"`,
|
|
203
|
-
`nohup env GRIP_DAEMON_TOKEN="$token" grip --daemon --daemon-host 127.0.0.1 --daemon-allow-insecure-transport --daemon-port ${config.daemonPort} --daemon-cwd ${shellQuote(config.workspaceMountPath)} --daemon-allow-root --daemon-allow-process-exec >"$HOME/${logFile}" 2>&1 </dev/null &`,
|
|
485
|
+
`nohup env GRIP_DAEMON_TOKEN="$token" "${this.env.GRIP_SANDBOX_GUEST_BINARY ? "$HOME/.local/bin/grip" : '$(command -v grip || printf %s "$HOME/.local/bin/grip")'}" --daemon --daemon-host 127.0.0.1 --daemon-allow-insecure-transport --daemon-port ${config.daemonPort} --daemon-cwd ${shellQuote(config.workspaceMountPath)} --daemon-allow-root --daemon-allow-process-exec >"$HOME/${logFile}" 2>&1 </dev/null &`,
|
|
204
486
|
`echo $! >"$HOME/${pidFile}"`,
|
|
487
|
+
'rmdir "$mode_lock" || exit 28',
|
|
488
|
+
"trap - EXIT",
|
|
205
489
|
`printf 'GRIP_SANDBOX_TOKEN=%s\\n' "$token"`,
|
|
206
490
|
].join("\n");
|
|
207
491
|
const args = ["shell", "--tty=false", "--workdir", config.workspaceMountPath, name, "sh", "-lc", script];
|
|
@@ -217,20 +501,103 @@ export class LimaSandboxService {
|
|
|
217
501
|
if (!mount || resolve(mount.location ?? "") !== workspaceRoot || !mount.writable) {
|
|
218
502
|
throw new Error(`Lima instance ${name} must mount ${workspaceRoot} read-write at ${config.workspaceMountPath}`);
|
|
219
503
|
}
|
|
220
|
-
|
|
504
|
+
const status = record.status?.toLowerCase();
|
|
505
|
+
if (status === "running")
|
|
221
506
|
return;
|
|
507
|
+
if (status !== "stopped") {
|
|
508
|
+
throw new Error(`Lima instance ${name} is in an uncertain state; refusing to resume`);
|
|
509
|
+
}
|
|
222
510
|
const args = ["start", "--tty=false", name];
|
|
223
511
|
ensureLimaSuccessful(config.limaBinary, await this.runner.run(config.limaBinary, args), args);
|
|
224
512
|
}
|
|
513
|
+
async findExactLegacyInstance(config, name) {
|
|
514
|
+
if (!name)
|
|
515
|
+
return undefined;
|
|
516
|
+
const records = await this.listRecords(config, name);
|
|
517
|
+
if (records.length === 0)
|
|
518
|
+
return undefined;
|
|
519
|
+
if (records.length !== 1 || records[0]?.name !== name) {
|
|
520
|
+
throw new Error("Lima instance lookup did not return one exact name; refusing to attach or create");
|
|
521
|
+
}
|
|
522
|
+
return records[0];
|
|
523
|
+
}
|
|
225
524
|
async start(options) {
|
|
226
|
-
|
|
227
|
-
|
|
525
|
+
const requestedName = options.name?.trim();
|
|
526
|
+
if (!requestedName) {
|
|
527
|
+
if (options.update)
|
|
528
|
+
throw new Error("Sandbox update requires a named running shared VM");
|
|
529
|
+
return this.startWithLegacyInstance(options);
|
|
530
|
+
}
|
|
531
|
+
// Coordinate VM resume/create as well as guest daemon launch with a named shared owner.
|
|
532
|
+
// A stale lock is ambiguous, not permission to start an existing VM.
|
|
533
|
+
const owner = await acquireSharedLimaOwnerLock(requestedName);
|
|
534
|
+
try {
|
|
535
|
+
const config = this.resolveConfig(options);
|
|
536
|
+
const existing = await this.findExactLegacyInstance(config, requestedName);
|
|
537
|
+
if (options.update && existing?.status?.toLowerCase() !== "running") {
|
|
538
|
+
throw new Error("Sandbox update requires an existing running shared VM");
|
|
539
|
+
}
|
|
540
|
+
if (existing?.status?.toLowerCase() !== "running") {
|
|
541
|
+
return await this.startWithLegacyInstance(options, { record: existing });
|
|
542
|
+
}
|
|
543
|
+
// Select the guest mode before any legacy setup. An unreadable or ambiguous
|
|
544
|
+
// marker is not permission to start a per-session daemon in this VM.
|
|
545
|
+
const probe = await this.runner.run(config.limaBinary, [
|
|
546
|
+
"shell",
|
|
547
|
+
"--tty=false",
|
|
548
|
+
requestedName,
|
|
549
|
+
"sh",
|
|
550
|
+
"-lc",
|
|
551
|
+
'if test -e "$HOME/.cache/grip/shared-lima" || test -L "$HOME/.cache/grip/shared-lima"; then if test -d "$HOME/.cache/grip/shared-lima" && test ! -L "$HOME/.cache/grip/shared-lima"; then printf "GRIP_SHARED_GUEST\\n"; else exit 42; fi; else printf "GRIP_LEGACY_GUEST\\n"; fi',
|
|
552
|
+
]);
|
|
553
|
+
if (probe.exitCode !== 0)
|
|
554
|
+
throw new Error("Lima guest daemon mode is uncertain; refusing named attach");
|
|
555
|
+
if (probe.stdout.trim() === "GRIP_LEGACY_GUEST") {
|
|
556
|
+
if (options.update)
|
|
557
|
+
throw new Error("Sandbox update requires a supervised shared guest; legacy guest is unchanged");
|
|
558
|
+
return await this.startWithLegacyInstance(options, { record: existing });
|
|
559
|
+
}
|
|
560
|
+
if (probe.stdout.trim() !== "GRIP_SHARED_GUEST") {
|
|
561
|
+
throw new Error("Lima guest daemon mode is uncertain; refusing named attach");
|
|
562
|
+
}
|
|
563
|
+
await owner.assertHeld();
|
|
564
|
+
const mount = findWorkspaceMount(existing, config.workspaceMountPath);
|
|
565
|
+
if (!mount?.location)
|
|
566
|
+
throw new Error("Shared Lima instance mount is unavailable; refusing attach");
|
|
567
|
+
const sharedOptions = {
|
|
568
|
+
instanceName: requestedName,
|
|
569
|
+
hostParent: resolve(mount.location),
|
|
570
|
+
hostRoot: resolve(options.workspaceRoot),
|
|
571
|
+
};
|
|
572
|
+
const inspection = await this.inspectSharedInstance(sharedOptions);
|
|
573
|
+
if (options.update)
|
|
574
|
+
await this.updateSharedDaemon(sharedOptions, owner);
|
|
575
|
+
const token = await this.ensureSharedDaemon(sharedOptions, owner, options.update);
|
|
576
|
+
const daemonUrl = `grip+${inspection.daemonUrl}?root=${encodeURIComponent(inspection.guestRoot)}`;
|
|
577
|
+
return {
|
|
578
|
+
containerId: requestedName,
|
|
579
|
+
containerName: requestedName,
|
|
580
|
+
workspaceRoot: inspection.hostRoot,
|
|
581
|
+
workspaceMountPath: inspection.guestRoot,
|
|
582
|
+
daemonUrl,
|
|
583
|
+
daemonUrlRedacted: inspection.daemonUrl,
|
|
584
|
+
token,
|
|
585
|
+
labels: { [`${SANDBOX_LABEL_PREFIX}.runtime`]: "lima", [`${SANDBOX_LABEL_PREFIX}.version`]: VERSION },
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
finally {
|
|
589
|
+
await owner.release();
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
async startWithLegacyInstance(options, known) {
|
|
228
593
|
const workspaceRoot = resolve(options.workspaceRoot);
|
|
594
|
+
const local = this.env.GRIP_SANDBOX_GUEST_BINARY
|
|
595
|
+
? await inspectLocalGuestBinary(this.env.GRIP_SANDBOX_GUEST_BINARY)
|
|
596
|
+
: undefined;
|
|
229
597
|
const config = this.resolveConfig(options);
|
|
230
598
|
const requestedName = options.name?.trim();
|
|
231
|
-
const records = requestedName ? await this.listRecords(config, requestedName) : [];
|
|
232
599
|
const managed = !requestedName;
|
|
233
|
-
const existing =
|
|
600
|
+
const existing = known ? known.record : await this.findExactLegacyInstance(config, requestedName);
|
|
234
601
|
if (existing)
|
|
235
602
|
await this.prepareExistingInstance(config, existing, requestedName, workspaceRoot);
|
|
236
603
|
const configuredForward = existing ? findDaemonForward(existing, config.daemonPort) : undefined;
|
|
@@ -242,28 +609,26 @@ export class LimaSandboxService {
|
|
|
242
609
|
const effectiveConfig = { ...config, daemonPort };
|
|
243
610
|
if (!existing)
|
|
244
611
|
await this.startInstance(effectiveConfig, workspaceRoot, name, daemonPort);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
throw error;
|
|
266
|
-
}
|
|
612
|
+
// A failed guest install/start can have changed state; never delete an uncertain VM.
|
|
613
|
+
if (existing)
|
|
614
|
+
await this.assertNoSharedGuestState(effectiveConfig, name);
|
|
615
|
+
await this.ensureGrip(effectiveConfig, name, !existing, local);
|
|
616
|
+
const token = await this.startDaemon(effectiveConfig, name);
|
|
617
|
+
await this.readinessWaiter(config.daemonHostBind, daemonPort);
|
|
618
|
+
// A failed readiness check is not proof that this daemon is ours to stop.
|
|
619
|
+
// Leave uncertain guest state for explicit operator reconciliation.
|
|
620
|
+
this.managedInstances.set(name, { workspaceRoot, daemonPort, ownerId: options.sessionId, managed });
|
|
621
|
+
const daemonUrl = `ws://${urlHost(daemonConnectionHost(config.daemonHostBind))}:${daemonPort}${DAEMON_WEBSOCKET_PATH}`;
|
|
622
|
+
return {
|
|
623
|
+
containerId: name,
|
|
624
|
+
containerName: name,
|
|
625
|
+
workspaceRoot,
|
|
626
|
+
workspaceMountPath: config.workspaceMountPath,
|
|
627
|
+
daemonUrl,
|
|
628
|
+
daemonUrlRedacted: daemonUrl,
|
|
629
|
+
token,
|
|
630
|
+
labels: { [`${SANDBOX_LABEL_PREFIX}.runtime`]: "lima", [`${SANDBOX_LABEL_PREFIX}.version`]: VERSION },
|
|
631
|
+
};
|
|
267
632
|
}
|
|
268
633
|
async list(options) {
|
|
269
634
|
const config = this.resolveConfig();
|
|
@@ -278,27 +643,54 @@ export class LimaSandboxService {
|
|
|
278
643
|
const args = ["shell", "--tty=false", name, "sh", "-lc", script];
|
|
279
644
|
ensureLimaSuccessful(config.limaBinary, await this.runner.run(config.limaBinary, args), args);
|
|
280
645
|
}
|
|
281
|
-
async removeInstance(config, name) {
|
|
282
|
-
const args = ["delete", "--force", name];
|
|
283
|
-
ensureLimaSuccessful(config.limaBinary, await this.runner.run(config.limaBinary, args), args);
|
|
284
|
-
}
|
|
285
646
|
async stop(options) {
|
|
286
647
|
const config = this.resolveConfig();
|
|
287
648
|
const instances = await this.list({ workspaceRoot: options.workspaceRoot });
|
|
288
649
|
const selected = selectLimaSandbox(instances, options.target ?? options.currentContainerId);
|
|
289
650
|
if (!selected)
|
|
290
651
|
return { status: "not-found", message: "No matching Grip Lima sandbox found" };
|
|
652
|
+
if (instances.filter((instance) => instance.name === selected.name).length !== 1) {
|
|
653
|
+
throw new Error("Lima sandbox identity is ambiguous; refusing to stop a VM or daemon");
|
|
654
|
+
}
|
|
291
655
|
const tracked = this.managedInstances.get(selected.name);
|
|
292
|
-
|
|
656
|
+
// Names and Lima's list output do not establish ownership. In particular, a
|
|
657
|
+
// shared named VM can have the ordinary sandbox prefix without being ours.
|
|
658
|
+
// Only a VM started by this session may be stopped or have its daemon touched.
|
|
659
|
+
if (!tracked || tracked.workspaceRoot !== selected.workspaceRoot || tracked.daemonPort !== selected.daemonPort) {
|
|
660
|
+
throw new Error("Lima sandbox ownership is unknown or daemon port changed; refusing to stop an untracked VM or daemon");
|
|
661
|
+
}
|
|
662
|
+
// A shared guest must never lose its daemon or VM to a legacy
|
|
663
|
+
// in-process record. Resolve the selected VM again under the host owner lock.
|
|
664
|
+
const owner = await acquireSharedLimaOwnerLock(selected.name);
|
|
665
|
+
try {
|
|
666
|
+
const matches = (await this.list({ workspaceRoot: options.workspaceRoot })).filter((instance) => instance.name === selected.name);
|
|
667
|
+
const current = matches[0];
|
|
668
|
+
if (matches.length !== 1 ||
|
|
669
|
+
!current ||
|
|
670
|
+
current.id !== selected.id ||
|
|
671
|
+
current.state !== selected.state ||
|
|
672
|
+
current.workspaceRoot !== selected.workspaceRoot ||
|
|
673
|
+
current.daemonPort !== selected.daemonPort) {
|
|
674
|
+
throw new Error("Lima sandbox identity changed during stop; refusing to touch the VM or daemon");
|
|
675
|
+
}
|
|
676
|
+
return await this.stopTrackedInstance(config, current, tracked);
|
|
677
|
+
}
|
|
678
|
+
finally {
|
|
679
|
+
await owner.release();
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
async stopTrackedInstance(config, selected, tracked) {
|
|
683
|
+
const ownsInstance = tracked.managed;
|
|
293
684
|
if (selected.state !== "running") {
|
|
294
685
|
if (ownsInstance && config.cleanup === "remove") {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
686
|
+
// An already stopped VM cannot be probed without resuming it. Its
|
|
687
|
+
// in-memory record is not proof it was never repurposed externally.
|
|
688
|
+
throw new Error("Lima sandbox is stopped; refusing to delete an unverifiable VM; reconcile manually");
|
|
298
689
|
}
|
|
299
690
|
this.managedInstances.delete(selected.name);
|
|
300
691
|
return { status: "already-stopped", container: selected };
|
|
301
692
|
}
|
|
693
|
+
await this.assertNoSharedGuestState(config, selected.name, "stop");
|
|
302
694
|
await this.stopDaemon(config, selected.name, tracked?.daemonPort ?? selected.daemonPort ?? config.daemonPort);
|
|
303
695
|
if (ownsInstance) {
|
|
304
696
|
const args = config.cleanup === "remove" ? ["delete", "--force", selected.name] : ["stop", selected.name];
|