@greppleai/grip 0.1.4 → 0.1.5
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 +31 -0
- package/README.md +1 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +3 -1
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/grip-agent.d.ts.map +1 -1
- package/dist/core/grip-agent.js +4 -2
- package/dist/core/grip-agent.js.map +1 -1
- package/dist/core/sandbox/lima.d.ts.map +1 -1
- package/dist/core/sandbox/lima.js +6 -6
- package/dist/core/sandbox/lima.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +18 -7
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +26 -18
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +220 -192
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +68 -6
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/grip-user-agent.d.ts.map +1 -1
- package/dist/utils/grip-user-agent.js +1 -1
- package/dist/utils/grip-user-agent.js.map +1 -1
- package/docs/keybindings.md +3 -1
- package/docs/providers.md +3 -1
- package/docs/sandbox.md +1 -1
- package/docs/usage.md +37 -1
- 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 +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lima.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/lima.ts"],"names":[],"mappings":"AAOA,OAAO,EAA8B,KAAK,mBAAmB,EAAE,MAAM,iDAAiD,CAAC;AACvH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,KAAK,EAGX,aAAa,EACb,sBAAsB,EACtB,gBAAgB,EAChB,kBAAkB,EAClB,aAAa,EACb,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAA8B,KAAK,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AAErG,MAAM,WAAW,4BAA6B,SAAQ,0BAA0B;IAC/E,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,yBAAyB;IACzC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IACnC,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,yBAAyB;IACzC,eAAe,CAAC,EAAE,IAAI,CAAC,eAAe,EAAE,oBAAoB,CAAC,CAAC;IAC9D,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACzE,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,gBAAgB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;CACpD;AA0KD,qBAAa,kBAAkB;IAC9B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA0D;IAC1F,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAoB;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA2D;IACzF,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAgD;IAChF,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAmC;IAEpE,YAAY,OAAO,GAAE,yBAA8B,EAOlD;IAED,aAAa,CAAC,SAAS,GAAE,sBAA2B,GAAG,aAAa,CAEnE;YAEa,WAAW;IAmBzB,+FAA+F;IACzF,qBAAqB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,4BAA4B,CAAC,CAwCrG;IACD,+FAA+F;IACzF,kBAAkB,CACvB,OAAO,EAAE,yBAAyB,EAClC,KAAK,EAAE,mBAAmB,EAC1B,OAAO,UAAQ,GACb,OAAO,CAAC,MAAM,CAAC,CA8GjB;YAGa,kBAAkB;YA4GlB,aAAa;YAuBb,UAAU;YA8BV,wBAAwB;YAcxB,WAAW;YA2BX,uBAAuB;YAqBvB,uBAAuB;IAU/B,KAAK,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA8DrE;YAEa,uBAAuB;IA8C/B,IAAI,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAQnE;YAEa,UAAU;IAOlB,IAAI,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAuClE;YAEa,mBAAmB;IAyB3B,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAU1C;CACD","sourcesContent":["import { createHash, randomBytes } from \"node:crypto\";\nimport { realpath } from \"node:fs/promises\";\nimport { createConnection } from \"node:net\";\nimport { basename, resolve } from \"node:path\";\nimport { VERSION } from \"../../config.ts\";\nimport { DAEMON_WEBSOCKET_PATH } from \"../../daemon/config.ts\";\nimport { spawnProcess } from \"../../utils/child-process.ts\";\nimport { acquireSharedLimaOwnerLock, type SharedLimaOwnerLock } from \"../remote-workspace-protocol/lima-owner-lock.ts\";\nimport type { SettingsManager } from \"../settings-manager.ts\";\nimport type {\n\tSandboxCommandOptions,\n\tSandboxCommandResult,\n\tSandboxConfig,\n\tSandboxConfigOverrides,\n\tSandboxContainer,\n\tSandboxListOptions,\n\tSandboxRunner,\n\tSandboxStartOptions,\n\tSandboxStartResult,\n\tSandboxStopOptions,\n\tSandboxStopResult,\n} from \"./config.ts\";\nimport { allocateSandboxDaemonPort, redactSecrets, resolveSandboxConfig, SANDBOX_LABEL_PREFIX } from \"./config.ts\";\nimport { inspectLocalGuestBinary, installLocalGuestBinary, type LocalGuestBinary } from \"./lima-guest-binary.ts\";\nimport { resolveSharedLimaWorkspace, type SharedLimaWorkspaceBinding } from \"./shared-lima-roots.ts\";\nimport { stageSharedGuestBinary } from \"./update-shared-guest-binary.ts\";\nexport interface SharedLimaInstanceInspection extends SharedLimaWorkspaceBinding {\n\tinstanceName: string;\n\tdaemonUrl: string;\n}\n\nexport interface SharedLimaInstanceOptions {\n\tinstanceName: string;\n\thostParent: string;\n\thostRoot: string;\n}\n\nexport interface ManagedLimaInstance {\n\tworkspaceRoot: string;\n\tdaemonPort: number;\n\townerId?: string;\n\tmanaged: boolean;\n}\n\nexport interface LimaSandboxServiceOptions {\n\tsettingsManager?: Pick<SettingsManager, \"getSandboxSettings\">;\n\tenv?: NodeJS.ProcessEnv;\n\trunner?: SandboxRunner;\n\tportAllocator?: (host: string, preferredPort: number) => Promise<number>;\n\treadinessWaiter?: (host: string, port: number) => Promise<void>;\n\tmanagedInstances?: Map<string, ManagedLimaInstance>;\n}\n\ninterface LimaMount {\n\tlocation?: string;\n\tmountPoint?: string;\n\twritable?: boolean;\n}\n\ninterface LimaPortForward {\n\tguestPort?: number;\n\thostPort?: number;\n\thostIP?: string;\n}\n\ninterface LimaConfig {\n\tmounts?: LimaMount[];\n\tportForwards?: LimaPortForward[];\n}\n\ninterface LimaListRecord {\n\tname?: string;\n\tstatus?: string;\n\tarch?: string;\n\tvmType?: string;\n\tdir?: string;\n\tconfig?: LimaConfig;\n}\n\ninterface KnownLimaRecord {\n\trecord: LimaListRecord | undefined;\n}\n\nclass ProcessLimaRunner implements SandboxRunner {\n\trun(command: string, args: string[], options: SandboxCommandOptions = {}): Promise<SandboxCommandResult> {\n\t\treturn new Promise((resolveCommand, reject) => {\n\t\t\tconst child = spawnProcess(command, args, {\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\tenv: { ...process.env, ...options.env },\n\t\t\t});\n\t\t\tlet stdout = \"\";\n\t\t\tlet stderr = \"\";\n\t\t\tchild.stdout?.on(\"data\", (chunk: Buffer) => {\n\t\t\t\tstdout += chunk.toString(\"utf8\");\n\t\t\t});\n\t\t\tchild.stderr?.on(\"data\", (chunk: Buffer) => {\n\t\t\t\tstderr += chunk.toString(\"utf8\");\n\t\t\t});\n\t\t\tchild.once(\"error\", reject);\n\t\t\tchild.once(\"close\", (exitCode) => resolveCommand({ exitCode, stdout, stderr }));\n\t\t});\n\t}\n}\n\nfunction shellQuote(value: string): string {\n\treturn `'${value.replaceAll(\"'\", `'\"'\"'`)}'`;\n}\n\nfunction parseLimaRecords(output: string): LimaListRecord[] {\n\tconst text = output.trim();\n\tif (!text) return [];\n\ttry {\n\t\tconst parsed = JSON.parse(text) as unknown;\n\t\treturn Array.isArray(parsed) ? (parsed as LimaListRecord[]) : [parsed as LimaListRecord];\n\t} catch {\n\t\treturn text\n\t\t\t.split(/\\r?\\n/)\n\t\t\t.filter(Boolean)\n\t\t\t.map((line) => JSON.parse(line) as LimaListRecord);\n\t}\n}\n\nfunction ensureLimaSuccessful(binary: string, result: SandboxCommandResult, args: string[]): void {\n\tif (result.exitCode === 0) return;\n\tconst detail = result.stderr.trim() || result.stdout.trim() || `exit code ${result.exitCode ?? \"unknown\"}`;\n\tthrow new Error(redactSecrets(`Lima command failed: ${binary} ${args.join(\" \")}: ${detail}`));\n}\n\nfunction daemonConnectionHost(host: string): string {\n\tif (host === \"0.0.0.0\") return \"127.0.0.1\";\n\tif (host === \"::\") return \"::1\";\n\treturn host;\n}\n\nfunction urlHost(host: string): string {\n\treturn host.includes(\":\") && !host.startsWith(\"[\") ? `[${host}]` : host;\n}\n\nasync function canConnect(host: string, port: number): Promise<boolean> {\n\treturn new Promise((resolveConnection) => {\n\t\tconst socket = createConnection({ host, port });\n\t\tlet settled = false;\n\t\tconst finish = (connected: boolean): void => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tsocket.destroy();\n\t\t\tresolveConnection(connected);\n\t\t};\n\t\tsocket.setTimeout(250, () => finish(false));\n\t\tsocket.once(\"connect\", () => finish(true));\n\t\tsocket.once(\"error\", () => finish(false));\n\t});\n}\n\nasync function waitForLimaDaemon(host: string, port: number): Promise<void> {\n\tconst deadline = Date.now() + 30_000;\n\twhile (Date.now() < deadline) {\n\t\tif (await canConnect(daemonConnectionHost(host), port)) return;\n\t\tawait new Promise((resolveDelay) => setTimeout(resolveDelay, 100));\n\t}\n\tthrow new Error(`Lima sandbox daemon did not become reachable on ${daemonConnectionHost(host)}:${port}`);\n}\n\nfunction createInstanceName(\n\tconfig: SandboxConfig,\n\tworkspaceRoot: string,\n\tsessionId: string,\n\tdaemonPort: number,\n): string {\n\tconst clean = (value: string): string =>\n\t\tvalue\n\t\t\t.toLowerCase()\n\t\t\t.replace(/[^a-z0-9-]+/g, \"-\")\n\t\t\t.replace(/^-+|-+$/g, \"\") || \"workspace\";\n\tconst hash = createHash(\"sha256\").update(resolve(workspaceRoot)).digest(\"hex\").slice(0, 12);\n\treturn `${clean(config.limaInstanceNamePrefix)}-${clean(basename(resolve(workspaceRoot)))}-${hash}-${clean(sessionId).slice(0, 8).replace(/-+$/, \"\")}-p${daemonPort}-${randomBytes(4).toString(\"hex\")}`;\n}\n\nfunction findWorkspaceMount(record: LimaListRecord, workspaceMountPath: string): LimaMount | undefined {\n\treturn record.config?.mounts?.find((mount) => mount.mountPoint === workspaceMountPath);\n}\n\nfunction findDaemonForward(record: LimaListRecord, preferredPort?: number): LimaPortForward | undefined {\n\tconst namePort = Number(record.name?.match(/-p(\\d+)-[a-f0-9]+$/)?.[1]);\n\tconst daemonPort = Number.isInteger(namePort) && namePort > 0 ? namePort : preferredPort;\n\tconst forwards = record.config?.portForwards?.filter(\n\t\t(forward) => forward.guestPort === forward.hostPort && forward.hostPort !== undefined,\n\t);\n\treturn (\n\t\tforwards?.find((forward) => forward.guestPort === daemonPort) ??\n\t\t(forwards?.length === 1 ? forwards[0] : undefined)\n\t);\n}\n\nfunction toSandboxContainer(record: LimaListRecord, workspaceMountPath: string): SandboxContainer {\n\tconst mount = findWorkspaceMount(record, workspaceMountPath);\n\tconst forward = findDaemonForward(record);\n\tconst daemonEndpoint = forward?.hostPort\n\t\t? `ws://${urlHost(daemonConnectionHost(forward.hostIP || \"127.0.0.1\"))}:${forward.hostPort}${DAEMON_WEBSOCKET_PATH}`\n\t\t: undefined;\n\treturn {\n\t\tid: record.name ?? \"\",\n\t\tname: record.name ?? \"\",\n\t\timage: `lima:${record.vmType ?? \"vm\"}/${record.arch ?? \"unknown\"}`,\n\t\tstatus: record.status ?? \"\",\n\t\tstate: record.status?.toLowerCase(),\n\t\tcreatedAt: undefined,\n\t\tworkspaceRoot: mount?.location ? resolve(mount.location) : undefined,\n\t\tworkspaceMountPath: mount?.mountPoint,\n\t\tdaemonPort: forward?.guestPort,\n\t\tdaemonEndpoint,\n\t\tlabels: { [`${SANDBOX_LABEL_PREFIX}.runtime`]: \"lima\", [`${SANDBOX_LABEL_PREFIX}.version`]: VERSION },\n\t};\n}\n\nfunction selectLimaSandbox(instances: SandboxContainer[], target: string | undefined): SandboxContainer | undefined {\n\tif (!target) return instances.length === 1 ? instances[0] : undefined;\n\treturn instances.find((instance) => instance.name === target || instance.id.startsWith(target));\n}\ntype LegacyLimaGuestStateOperation = \"attach\" | \"stop\";\n\nexport class LimaSandboxService {\n\tprivate readonly settingsManager: Pick<SettingsManager, \"getSandboxSettings\"> | undefined;\n\tprivate readonly env: NodeJS.ProcessEnv;\n\tprivate readonly runner: SandboxRunner;\n\tprivate readonly portAllocator: (host: string, preferredPort: number) => Promise<number>;\n\tprivate readonly readinessWaiter: (host: string, port: number) => Promise<void>;\n\tprivate readonly managedInstances: Map<string, ManagedLimaInstance>;\n\n\tconstructor(options: LimaSandboxServiceOptions = {}) {\n\t\tthis.settingsManager = options.settingsManager;\n\t\tthis.env = options.env ?? process.env;\n\t\tthis.runner = options.runner ?? new ProcessLimaRunner();\n\t\tthis.portAllocator = options.portAllocator ?? allocateSandboxDaemonPort;\n\t\tthis.readinessWaiter = options.readinessWaiter ?? waitForLimaDaemon;\n\t\tthis.managedInstances = options.managedInstances ?? new Map();\n\t}\n\n\tresolveConfig(overrides: SandboxConfigOverrides = {}): SandboxConfig {\n\t\treturn resolveSandboxConfig(this.settingsManager?.getSandboxSettings(), this.env, overrides);\n\t}\n\n\tprivate async listRecords(config: SandboxConfig, name?: string): Promise<LimaListRecord[]> {\n\t\tconst args = [\"list\", \"--json\", ...(name ? [name] : [])];\n\t\ttry {\n\t\t\tconst result = await this.runner.run(config.limaBinary, args);\n\t\t\tif (\n\t\t\t\tname &&\n\t\t\t\tresult.exitCode !== 0 &&\n\t\t\t\t/unmatched|not found|does not exist/i.test(`${result.stderr}\\n${result.stdout}`)\n\t\t\t) {\n\t\t\t\treturn [];\n\t\t\t}\n\t\t\tensureLimaSuccessful(config.limaBinary, result, args);\n\t\t\treturn parseLimaRecords(result.stdout);\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tthrow new Error(redactSecrets(`Lima is unavailable or not reachable via ${config.limaBinary}: ${message}`));\n\t\t}\n\t}\n\n\t/** Read-only validation: no VM creation, daemon restart, token rotation or ownership claim. */\n\tasync inspectSharedInstance(options: SharedLimaInstanceOptions): Promise<SharedLimaInstanceInspection> {\n\t\tconst config = this.resolveConfig();\n\t\tconst { instanceName } = options;\n\t\tif (!/^[a-z0-9][a-z0-9-]*$/.test(instanceName)) throw new Error(\"Invalid shared Lima instance name\");\n\t\tif (config.daemonHostBind !== \"127.0.0.1\" && config.daemonHostBind !== \"::1\") {\n\t\t\tthrow new Error(\"Shared Lima daemon forward must bind to loopback\");\n\t\t}\n\t\tconst binding = await resolveSharedLimaWorkspace(options.hostParent, config.workspaceMountPath, options.hostRoot);\n\t\tconst records = await this.listRecords(config, instanceName);\n\t\tif (records.length !== 1 || records[0]?.name !== instanceName) {\n\t\t\tthrow new Error(`Shared Lima instance ${instanceName} does not exist by exact name`);\n\t\t}\n\t\tconst record = records[0];\n\t\tif (record.status?.toLowerCase() !== \"running\") {\n\t\t\tthrow new Error(`Shared Lima instance ${instanceName} is not running`);\n\t\t}\n\t\tconst mounts = record.config?.mounts?.filter((mount) => mount.mountPoint === binding.guestParent) ?? [];\n\t\tif (mounts.length !== 1 || !mounts[0]?.writable || !mounts[0].location) {\n\t\t\tthrow new Error(`Shared Lima instance ${instanceName} requires one writable fixed parent mount`);\n\t\t}\n\t\tif ((await realpath(mounts[0].location)) !== binding.hostParent) {\n\t\t\tthrow new Error(`Shared Lima instance ${instanceName} mounts a different host parent`);\n\t\t}\n\t\tconst forwards =\n\t\t\trecord.config?.portForwards?.filter(\n\t\t\t\t(forward) => forward.guestPort === config.daemonPort || forward.hostPort === config.daemonPort,\n\t\t\t) ?? [];\n\t\tif (\n\t\t\tforwards.length !== 1 ||\n\t\t\tforwards[0]?.guestPort !== config.daemonPort ||\n\t\t\tforwards[0].hostPort !== config.daemonPort ||\n\t\t\tforwards[0].hostIP !== config.daemonHostBind\n\t\t) {\n\t\t\tthrow new Error(`Shared Lima instance ${instanceName} requires one matching loopback daemon forward`);\n\t\t}\n\t\treturn {\n\t\t\t...binding,\n\t\t\tinstanceName,\n\t\t\tdaemonUrl: `ws://${urlHost(config.daemonHostBind)}:${config.daemonPort}${DAEMON_WEBSOCKET_PATH}`,\n\t\t};\n\t}\n\t/** Reinspect and reuse the supervised guest daemon under the exclusive named-VM owner lock. */\n\tasync ensureSharedDaemon(\n\t\toptions: SharedLimaInstanceOptions,\n\t\towner: SharedLimaOwnerLock,\n\t\tupdated = false,\n\t): Promise<string> {\n\t\tif (owner.instanceName !== options.instanceName) throw new Error(\"Shared Lima owner lock names a different VM\");\n\t\tawait owner.assertHeld();\n\t\tawait this.inspectSharedInstance(options);\n\t\tconst config = this.resolveConfig();\n\t\tconst local = this.env.GRIP_SANDBOX_GUEST_BINARY\n\t\t\t? await inspectLocalGuestBinary(this.env.GRIP_SANDBOX_GUEST_BINARY)\n\t\t\t: undefined;\n\t\tconst versionArgs = [\n\t\t\t\"shell\",\n\t\t\t\"--tty=false\",\n\t\t\toptions.instanceName,\n\t\t\t\"sh\",\n\t\t\t\"-lc\",\n\t\t\tlocal || updated ? '\"$HOME/.local/bin/grip\" --version' : \"grip --version\",\n\t\t];\n\t\tconst version = await this.runner.run(config.limaBinary, versionArgs);\n\t\tif (version.exitCode !== 0 || version.stdout.trim() !== VERSION) {\n\t\t\tthrow new Error(\"Shared Lima grip version mismatch or binary missing; install the matching version manually\");\n\t\t}\n\t\tawait owner.assertHeld();\n\t\tconst directory = \"$HOME/.cache/grip/shared-lima\";\n\t\t// /proc start time plus boot ID distinguishes a recycled PID or a rebooted guest.\n\t\t// kill -0 also succeeds for zombies, which must never be reused as a live daemon.\n\t\tconst processStartTime = `awk '{ sub(/^.*\\\\) /, \"\"); if ($1 == \"Z\" || $1 == \"X\") exit 1; print $20 }'`;\n\t\tconst script = [\n\t\t\t\"set -eu\",\n\t\t\t\"umask 077\",\n\t\t\t`directory=\"${directory}\"`,\n\t\t\t// Both current modes reserve the guest transition; a crashed reservation is operator-owned.\n\t\t\t'mkdir -p \"$HOME/.cache/grip\"',\n\t\t\t'mode_lock=\"$HOME/.cache/grip/daemon-mode.lock\"',\n\t\t\t'mkdir \"$mode_lock\" || exit 28',\n\t\t\t\"trap 'rmdir \\\"$mode_lock\\\" 2>/dev/null || true' EXIT\",\n\t\t\t// A legacy per-session daemon may own this VM or its port. Do not start a second daemon.\n\t\t\t'for legacy_file in \"$HOME/.cache/grip\"/sandbox-daemon-*.pid \"$HOME/.cache/grip\"/sandbox-daemon-*.log; do',\n\t\t\t' if test -e \"$legacy_file\" || test -L \"$legacy_file\"; then exit 27; fi',\n\t\t\t\"done\",\n\t\t\t// A transient user service must outlive the Lima SSH session. Do not mutate\n\t\t\t// private state unless the guest manager is running and linger was enabled by an operator.\n\t\t\t\"command -v systemd-run >/dev/null && systemctl --user show-environment >/dev/null || exit 29\",\n\t\t\t'test \"$(loginctl show-user \"$(id -un)\" --property=Linger --value)\" = yes || exit 29',\n\t\t\t`mkdir -p \"$directory\"; test ! -L \"$directory\"; chmod 700 \"$directory\"`,\n\t\t\t'rmdir \"$mode_lock\" || exit 28',\n\t\t\t\"trap - EXIT\",\n\t\t\t`token_file=\"$directory/daemon-${config.daemonPort}.token\"`,\n\t\t\t`pid_file=\"$directory/daemon-${config.daemonPort}.pid\"`,\n\t\t\t`identity_file=\"$directory/daemon-${config.daemonPort}.identity\"`,\n\t\t\t`log_file=\"$directory/daemon-${config.daemonPort}.log\"`,\n\t\t\t`start_lock=\"$directory/daemon-${config.daemonPort}.starting\"`,\n\t\t\t`unit=\"grip-shared-lima-${config.daemonPort}.service\"`,\n\t\t\t'test ! -L \"$token_file\"; test ! -L \"$pid_file\"; test ! -L \"$identity_file\"; test ! -L \"$log_file\"',\n\t\t\t'if test -e \"$log_file\"; then test -f \"$log_file\" && test \"$(stat -c %a \"$log_file\")\" = 600 || exit 22; fi',\n\t\t\t'test ! -e \"$start_lock\" && test ! -L \"$start_lock\" || exit 26',\n\t\t\t'if test -e \"$token_file\" || test -e \"$pid_file\" || test -e \"$identity_file\"; then',\n\t\t\t' test -f \"$token_file\" && test -f \"$pid_file\" && test -f \"$identity_file\" || exit 21',\n\t\t\t' test \"$(stat -c %a \"$token_file\")\" = 600 || exit 22',\n\t\t\t' test \"$(stat -c %a \"$pid_file\")\" = 600 || exit 22',\n\t\t\t' test \"$(stat -c %a \"$identity_file\")\" = 600 || exit 22',\n\t\t\t' pid=\"$(cat \"$pid_file\")\"',\n\t\t\t\" case \\\"$pid\\\" in ''|*[!0-9]*) exit 23;; esac\",\n\t\t\t' kill -0 \"$pid\" || exit 24',\n\t\t\t` start_time=\"$(${processStartTime} \"/proc/$pid/stat\")\" || exit 25`,\n\t\t\t' boot_id=\"$(cat /proc/sys/kernel/random/boot_id)\" || exit 25',\n\t\t\t' test -n \"$start_time\" && test -n \"$boot_id\" && test \"$(cat \"$identity_file\")\" = \"$boot_id:$start_time\" || exit 25',\n\t\t\t' test \"$(systemctl --user show -P MainPID \"$unit\")\" = \"$pid\" && systemctl --user is-active --quiet \"$unit\" || exit 25',\n\t\t\t' token=\"$(cat \"$token_file\")\"',\n\t\t\t\"else\",\n\t\t\t' mkdir \"$start_lock\" || exit 26',\n\t\t\t\" trap 'rmdir \\\"$start_lock\\\" 2>/dev/null || true' EXIT\",\n\t\t\t\" token=\\\"$(head -c 32 /dev/urandom | base64 | tr -d '\\\\n')\\\"\",\n\t\t\t' printf \\'%s\\' \"$token\" >\"$token_file\"',\n\t\t\t' chmod 600 \"$token_file\"',\n\t\t\t// The systemd user manager owns the process. It reads the private token file\n\t\t\t// inside the unit, never from a systemd-run argument or environment property.\n\t\t\tlocal\n\t\t\t\t? ' grip_bin=\"$HOME/.local/bin/grip\"; test -x \"$grip_bin\"'\n\t\t\t\t: ' grip_bin=\"$(command -v grip)\"; case \"$grip_bin\" in /*) ;; *) exit 25;; esac',\n\t\t\t` 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`,\n\t\t\t' pid=\"\"; attempts=0',\n\t\t\t' while test \"$attempts\" -lt 50; do',\n\t\t\t' pid=\"$(systemctl --user show -P MainPID \"$unit\")\" || exit 25',\n\t\t\t' case \"$pid\" in \"\"|0) sleep 0.1;; *[!0-9]*) exit 25;; *) break;; esac',\n\t\t\t\" attempts=$((attempts + 1))\",\n\t\t\t\" done\",\n\t\t\t' case \"$pid\" in \"\"|0|*[!0-9]*) exit 25;; esac',\n\t\t\t' systemctl --user is-active --quiet \"$unit\" || exit 25',\n\t\t\t' printf \\'%s\\' \"$pid\" >\"$pid_file\"',\n\t\t\t' chmod 600 \"$pid_file\"',\n\t\t\t` start_time=\"$(${processStartTime} \"/proc/$pid/stat\")\" || exit 25`,\n\t\t\t' boot_id=\"$(cat /proc/sys/kernel/random/boot_id)\" || exit 25',\n\t\t\t' test -n \"$start_time\" && test -n \"$boot_id\" || exit 25',\n\t\t\t' printf \\'%s:%s\\' \"$boot_id\" \"$start_time\" >\"$identity_file\"',\n\t\t\t' chmod 600 \"$identity_file\"',\n\t\t\t' rmdir \"$start_lock\" || exit 26',\n\t\t\t\" trap - EXIT\",\n\t\t\t\"fi\",\n\t\t\t\"printf 'GRIP_SHARED_TOKEN=%s\\\\n' \\\"$token\\\"\",\n\t\t].join(\"\\n\");\n\t\tconst args = [\"shell\", \"--tty=false\", options.instanceName, \"sh\", \"-lc\", script];\n\t\tconst result = await this.runner.run(config.limaBinary, args);\n\t\t// Never include stdout/stderr from a credential-bearing shell in an error.\n\t\tif (result.exitCode !== 0)\n\t\t\tthrow new Error(\"Shared Lima daemon state is uncertain; reconcile guest PID/token files manually\");\n\t\tconst token = result.stdout.match(/(?:^|\\n)GRIP_SHARED_TOKEN=([A-Za-z0-9+/]{43}=)(?:\\r?\\n|$)/)?.[1];\n\t\tif (!token) throw new Error(\"Shared Lima daemon did not return a valid credential; reconcile manually\");\n\t\tawait this.readinessWaiter(config.daemonHostBind, config.daemonPort);\n\t\t// Readiness can outlive the owner. Never hand a guest credential to a lost owner.\n\t\tawait owner.assertHeld();\n\t\treturn token;\n\t}\n\n\t/** Update only a verified supervised daemon. Staging happens before disconnecting peers. */\n\tprivate async updateSharedDaemon(options: SharedLimaInstanceOptions, owner: SharedLimaOwnerLock): Promise<void> {\n\t\tawait owner.assertHeld();\n\t\tawait this.inspectSharedInstance(options);\n\t\tconst config = this.resolveConfig();\n\t\tconst name = options.instanceName;\n\t\tconst port = config.daemonPort;\n\t\tif (!this.env.GRIP_SANDBOX_GUEST_BINARY && process.argv[1]?.endsWith(\"/packages/coding-agent/src/cli.ts\")) {\n\t\t\tthrow new Error(\n\t\t\t\t\"Development sandbox update requires GRIP_SANDBOX_GUEST_BINARY pointing to a built Linux Grip executable\",\n\t\t\t);\n\t\t}\n\t\tconst local = this.env.GRIP_SANDBOX_GUEST_BINARY\n\t\t\t? await inspectLocalGuestBinary(this.env.GRIP_SANDBOX_GUEST_BINARY)\n\t\t\t: undefined;\n\t\tconst shell = (script: string) =>\n\t\t\tthis.runner.run(config.limaBinary, [\"shell\", \"--tty=false\", name, \"sh\", \"-lc\", script]);\n\t\tconst directory = \"$HOME/.cache/grip/shared-lima\";\n\t\tconst unit = `grip-shared-lima-${port}.service`;\n\t\tconst verify = [\n\t\t\t\"set -eu\",\n\t\t\t`directory=\"${directory}\"`,\n\t\t\t`unit=${shellQuote(unit)}`,\n\t\t\t'test -d \"$directory\" && test ! -L \"$directory\"',\n\t\t\t'test ! -e \"$HOME/.cache/grip/daemon-mode.lock\" && test ! -L \"$HOME/.cache/grip/daemon-mode.lock\"',\n\t\t\t`token_file=\"$directory/daemon-${port}.token\"`,\n\t\t\t`pid_file=\"$directory/daemon-${port}.pid\"`,\n\t\t\t`identity_file=\"$directory/daemon-${port}.identity\"`,\n\t\t\t'for file in \"$token_file\" \"$pid_file\" \"$identity_file\"; do test -f \"$file\" && test ! -L \"$file\" && test \"$(stat -c %a \"$file\")\" = 600; done',\n\t\t\t'pid=\"$(cat \"$pid_file\")\"; case \"$pid\" in \"\"|0|*[!0-9]*) exit 45;; esac',\n\t\t\t'kill -0 \"$pid\"',\n\t\t\t`start_time=\"$(awk '{ sub(/^.*\\\\) /, \"\"); if ($1 == \"Z\" || $1 == \"X\") exit 1; print $20 }' \"/proc/$pid/stat\")\"`,\n\t\t\t'boot_id=\"$(cat /proc/sys/kernel/random/boot_id)\"',\n\t\t\t'test -n \"$start_time\" && test \"$(cat \"$identity_file\")\" = \"$boot_id:$start_time\"',\n\t\t\t'test \"$(systemctl --user show -P MainPID \"$unit\")\" = \"$pid\" && systemctl --user is-active --quiet \"$unit\"',\n\t\t\t'case \"$(systemctl --user show -P ExecStart \"$unit\")\" in *\"$HOME/.local/bin/grip\"*) ;; *) exit 46;; esac',\n\t\t];\n\t\tconst preflight = await shell(verify.join(\"\\n\"));\n\t\tif (preflight.exitCode !== 0) {\n\t\t\tthrow new Error(\n\t\t\t\t\"Shared Lima daemon identity or binary path is uncertain; update refused without disconnecting clients\",\n\t\t\t);\n\t\t}\n\t\tconst staged = await stageSharedGuestBinary(this.runner, config.limaBinary, name, unit, VERSION, local);\n\t\ttry {\n\t\t\tawait owner.assertHeld();\n\t\t\tconst stop = await shell(\n\t\t\t\t[\n\t\t\t\t\t...verify,\n\t\t\t\t\t'mkdir \"$HOME/.cache/grip/daemon-mode.lock\" || exit 45',\n\t\t\t\t\t'trap \\u0027rmdir \"$HOME/.cache/grip/daemon-mode.lock\" 2>/dev/null || true\\u0027 EXIT',\n\t\t\t\t\t'systemctl --user stop \"$unit\"',\n\t\t\t\t\t'test \"$(systemctl --user show -P MainPID \"$unit\")\" = 0',\n\t\t\t\t\t'! kill -0 \"$pid\" 2>/dev/null',\n\t\t\t\t].join(\"\\n\"),\n\t\t\t);\n\t\t\tif (stop.exitCode !== 0)\n\t\t\t\tthrow new Error(\"Shared daemon stop is uncertain; inspect the guest before retrying update\");\n\t\t\tawait owner.assertHeld();\n\t\t\tawait staged.install();\n\t\t\tawait owner.assertHeld();\n\t\t\tconst restart = await shell(\n\t\t\t\t[\n\t\t\t\t\t\"set -eu\",\n\t\t\t\t\t`directory=\"${directory}\"`,\n\t\t\t\t\t`unit=${shellQuote(unit)}`,\n\t\t\t\t\t'test -d \"$directory\" && test ! -L \"$directory\"',\n\t\t\t\t\t`token_file=\"$directory/daemon-${port}.token\"`,\n\t\t\t\t\t`pid_file=\"$directory/daemon-${port}.pid\"`,\n\t\t\t\t\t`identity_file=\"$directory/daemon-${port}.identity\"`,\n\t\t\t\t\t'for file in \"$token_file\" \"$pid_file\" \"$identity_file\"; do test -f \"$file\" && test ! -L \"$file\" && test \"$(stat -c %a \"$file\")\" = 600; done',\n\t\t\t\t\t'test ! -e \"$HOME/.cache/grip/daemon-mode.lock\" && test ! -L \"$HOME/.cache/grip/daemon-mode.lock\"',\n\t\t\t\t\t'mkdir \"$HOME/.cache/grip/daemon-mode.lock\" || exit 45',\n\t\t\t\t\t'trap \\u0027rmdir \"$HOME/.cache/grip/daemon-mode.lock\" 2>/dev/null || true\\u0027 EXIT',\n\t\t\t\t\t'old_pid=\"$(cat \"$pid_file\")\"; case \"$old_pid\" in \"\"|0|*[!0-9]*) exit 45;; esac',\n\t\t\t\t\t'! kill -0 \"$old_pid\" 2>/dev/null',\n\t\t\t\t\t`test \"$(\"$HOME/.local/bin/grip\" --version)\" = ${shellQuote(VERSION)}`,\n\t\t\t\t\t'load=\"$(systemctl --user show -P LoadState \"$unit\" 2>/dev/null || true)\"',\n\t\t\t\t\t'case \"$load\" in',\n\t\t\t\t\t' loaded) test \"$(systemctl --user show -P MainPID \"$unit\")\" = 0; systemctl --user start \"$unit\" ;;',\n\t\t\t\t\t' not-found|\"\")',\n\t\t\t\t\t` 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 ;;`,\n\t\t\t\t\t\" *) exit 45 ;;\",\n\t\t\t\t\t\"esac\",\n\t\t\t\t\t'pid=\"\"; attempts=0',\n\t\t\t\t\t'while test \"$attempts\" -lt 50; do',\n\t\t\t\t\t' pid=\"$(systemctl --user show -P MainPID \"$unit\")\" || exit 45',\n\t\t\t\t\t' case \"$pid\" in \"\"|0) sleep 0.1;; *[!0-9]*) exit 45;; *) break;; esac',\n\t\t\t\t\t\" attempts=$((attempts + 1))\",\n\t\t\t\t\t\"done\",\n\t\t\t\t\t'case \"$pid\" in \"\"|0|*[!0-9]*) exit 45;; esac',\n\t\t\t\t\t'kill -0 \"$pid\"; systemctl --user is-active --quiet \"$unit\"',\n\t\t\t\t\t`start_time=\"$(awk '{ sub(/^.*\\\\) /, \"\"); if ($1 == \"Z\" || $1 == \"X\") exit 1; print $20 }' \"/proc/$pid/stat\")\"`,\n\t\t\t\t\t'boot_id=\"$(cat /proc/sys/kernel/random/boot_id)\"',\n\t\t\t\t\t'test -n \"$start_time\" && test -n \"$boot_id\"',\n\t\t\t\t\t'printf \"%s\" \"$pid\" >\"$pid_file\"',\n\t\t\t\t\t'printf \"%s:%s\" \"$boot_id\" \"$start_time\" >\"$identity_file\"',\n\t\t\t\t\t'chmod 600 \"$pid_file\" \"$identity_file\"',\n\t\t\t\t].join(\"\\n\"),\n\t\t\t);\n\t\t\tif (restart.exitCode !== 0)\n\t\t\t\tthrow new Error(\"Shared daemon restart is uncertain; inspect the guest before retrying update\");\n\t\t\tawait this.readinessWaiter(config.daemonHostBind, port);\n\t\t\tawait owner.assertHeld();\n\t\t} finally {\n\t\t\tawait staged.cleanup();\n\t\t}\n\t}\n\n\tprivate async startInstance(\n\t\tconfig: SandboxConfig,\n\t\tworkspaceRoot: string,\n\t\tname: string,\n\t\tdaemonPort: number,\n\t): Promise<void> {\n\t\tconst mountExpression = `.mounts = [{\"location\":${JSON.stringify(workspaceRoot)},\"mountPoint\":${JSON.stringify(config.workspaceMountPath)},\"writable\":true}]`;\n\t\tconst forwardExpression = `.portForwards += [{\"guestPort\":${daemonPort},\"hostPort\":${daemonPort},\"hostIP\":${JSON.stringify(config.daemonHostBind)}}]`;\n\t\tconst args = [\n\t\t\t\"start\",\n\t\t\t\"--tty=false\",\n\t\t\t\"--name\",\n\t\t\tname,\n\t\t\t\"--mount-none\",\n\t\t\t\"--set\",\n\t\t\tmountExpression,\n\t\t\t\"--set\",\n\t\t\tforwardExpression,\n\t\t\tconfig.limaTemplate,\n\t\t];\n\t\tensureLimaSuccessful(config.limaBinary, await this.runner.run(config.limaBinary, args), args);\n\t}\n\n\tprivate async ensureGrip(\n\t\tconfig: SandboxConfig,\n\t\tname: string,\n\t\tmanaged: boolean,\n\t\tlocal?: LocalGuestBinary,\n\t): Promise<void> {\n\t\tif (local) {\n\t\t\tawait installLocalGuestBinary(this.runner, config.limaBinary, name, local, VERSION);\n\t\t\treturn;\n\t\t}\n\t\tconst checkArgs = [\n\t\t\t\"shell\",\n\t\t\t\"--tty=false\",\n\t\t\tname,\n\t\t\t\"sh\",\n\t\t\t\"-lc\",\n\t\t\t'command -v grip >/dev/null || test -x \"$HOME/.local/bin/grip\"',\n\t\t];\n\t\tconst check = await this.runner.run(config.limaBinary, checkArgs);\n\t\tif (check.exitCode === 0) return;\n\t\tif (!managed) {\n\t\t\tthrow new Error(\n\t\t\t\t`Lima instance ${name} does not provide grip; install the matching Linux release in the VM first`,\n\t\t\t);\n\t\t}\n\t\tconst 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\"`;\n\t\tconst args = [\"shell\", \"--tty=false\", name, \"sh\", \"-lc\", script];\n\t\tensureLimaSuccessful(config.limaBinary, await this.runner.run(config.limaBinary, args), args);\n\t}\n\t/** A legacy named attach or stop must not rotate or kill a shared guest daemon. */\n\tprivate async assertNoSharedGuestState(\n\t\tconfig: SandboxConfig,\n\t\tname: string,\n\t\toperation: LegacyLimaGuestStateOperation = \"attach\",\n\t): Promise<void> {\n\t\tconst script = 'test ! -e \"$HOME/.cache/grip/shared-lima\" && test ! -L \"$HOME/.cache/grip/shared-lima\"';\n\t\tconst args = [\"shell\", \"--tty=false\", name, \"sh\", \"-lc\", script];\n\t\tconst result = await this.runner.run(config.limaBinary, args);\n\t\t// A guest shell failure is ambiguous too; never guess that shared state is absent.\n\t\tif (result.exitCode !== 0) {\n\t\t\tthrow new Error(`Lima named VM may host a shared daemon; legacy ${operation} refused; reconcile manually`);\n\t\t}\n\t}\n\n\tprivate async startDaemon(config: SandboxConfig, name: string): Promise<string> {\n\t\tconst pidFile = `.cache/grip/sandbox-daemon-${config.daemonPort}.pid`;\n\t\tconst logFile = `.cache/grip/sandbox-daemon-${config.daemonPort}.log`;\n\t\tconst script = [\n\t\t\t\"set -eu\",\n\t\t\t\"umask 077\",\n\t\t\t`mkdir -p \"$HOME/.cache/grip\"`,\n\t\t\t'mode_lock=\"$HOME/.cache/grip/daemon-mode.lock\"',\n\t\t\t'mkdir \"$mode_lock\" || exit 28',\n\t\t\t\"trap 'rmdir \\\"$mode_lock\\\" 2>/dev/null || true' EXIT\",\n\t\t\t'test ! -e \"$HOME/.cache/grip/shared-lima\" && test ! -L \"$HOME/.cache/grip/shared-lima\" || exit 29',\n\t\t\t`if test -f \"$HOME/${pidFile}\"; then old_pid=\"$(cat \"$HOME/${pidFile}\")\"; kill \"$old_pid\" 2>/dev/null || true; fi`,\n\t\t\t`token=\"$(head -c 32 /dev/urandom | base64 | tr -d '\\\\n')\"`,\n\t\t\t`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 &`,\n\t\t\t`echo $! >\"$HOME/${pidFile}\"`,\n\t\t\t'rmdir \"$mode_lock\" || exit 28',\n\t\t\t\"trap - EXIT\",\n\t\t\t`printf 'GRIP_SANDBOX_TOKEN=%s\\\\n' \"$token\"`,\n\t\t].join(\"\\n\");\n\t\tconst args = [\"shell\", \"--tty=false\", \"--workdir\", config.workspaceMountPath, name, \"sh\", \"-lc\", script];\n\t\tconst result = await this.runner.run(config.limaBinary, args);\n\t\tensureLimaSuccessful(config.limaBinary, result, args);\n\t\tconst token = result.stdout.match(/(?:^|\\n)GRIP_SANDBOX_TOKEN=([^\\r\\n]+)/)?.[1];\n\t\tif (!token) throw new Error(`Lima instance ${name} started the daemon without returning an authentication token`);\n\t\treturn token;\n\t}\n\n\tprivate async prepareExistingInstance(\n\t\tconfig: SandboxConfig,\n\t\trecord: LimaListRecord,\n\t\tname: string,\n\t\tworkspaceRoot: string,\n\t): Promise<void> {\n\t\tconst mount = findWorkspaceMount(record, config.workspaceMountPath);\n\t\tif (!mount || resolve(mount.location ?? \"\") !== workspaceRoot || !mount.writable) {\n\t\t\tthrow new Error(\n\t\t\t\t`Lima instance ${name} must mount ${workspaceRoot} read-write at ${config.workspaceMountPath}`,\n\t\t\t);\n\t\t}\n\t\tconst status = record.status?.toLowerCase();\n\t\tif (status === \"running\") return;\n\t\tif (status !== \"stopped\") {\n\t\t\tthrow new Error(`Lima instance ${name} is in an uncertain state; refusing to resume`);\n\t\t}\n\t\tconst args = [\"start\", \"--tty=false\", name];\n\t\tensureLimaSuccessful(config.limaBinary, await this.runner.run(config.limaBinary, args), args);\n\t}\n\n\tprivate async findExactLegacyInstance(config: SandboxConfig, name?: string): Promise<LimaListRecord | undefined> {\n\t\tif (!name) return undefined;\n\t\tconst records = await this.listRecords(config, name);\n\t\tif (records.length === 0) return undefined;\n\t\tif (records.length !== 1 || records[0]?.name !== name) {\n\t\t\tthrow new Error(\"Lima instance lookup did not return one exact name; refusing to attach or create\");\n\t\t}\n\t\treturn records[0];\n\t}\n\n\tasync start(options: SandboxStartOptions): Promise<SandboxStartResult> {\n\t\tconst requestedName = options.name?.trim();\n\t\tif (!requestedName) {\n\t\t\tif (options.update) throw new Error(\"Sandbox update requires a named running shared VM\");\n\t\t\treturn this.startWithLegacyInstance(options);\n\t\t}\n\t\t// Coordinate VM resume/create as well as guest daemon launch with a named shared owner.\n\t\t// A stale lock is ambiguous, not permission to start an existing VM.\n\t\tconst owner = await acquireSharedLimaOwnerLock(requestedName);\n\t\ttry {\n\t\t\tconst config = this.resolveConfig(options);\n\t\t\tconst existing = await this.findExactLegacyInstance(config, requestedName);\n\t\t\tif (options.update && existing?.status?.toLowerCase() !== \"running\") {\n\t\t\t\tthrow new Error(\"Sandbox update requires an existing running shared VM\");\n\t\t\t}\n\t\t\tif (existing?.status?.toLowerCase() !== \"running\") {\n\t\t\t\treturn await this.startWithLegacyInstance(options, { record: existing });\n\t\t\t}\n\t\t\t// Select the guest mode before any legacy setup. An unreadable or ambiguous\n\t\t\t// marker is not permission to start a per-session daemon in this VM.\n\t\t\tconst probe = await this.runner.run(config.limaBinary, [\n\t\t\t\t\"shell\",\n\t\t\t\t\"--tty=false\",\n\t\t\t\trequestedName,\n\t\t\t\t\"sh\",\n\t\t\t\t\"-lc\",\n\t\t\t\t'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',\n\t\t\t]);\n\t\t\tif (probe.exitCode !== 0) throw new Error(\"Lima guest daemon mode is uncertain; refusing named attach\");\n\t\t\tif (probe.stdout.trim() === \"GRIP_LEGACY_GUEST\") {\n\t\t\t\tif (options.update)\n\t\t\t\t\tthrow new Error(\"Sandbox update requires a supervised shared guest; legacy guest is unchanged\");\n\t\t\t\treturn await this.startWithLegacyInstance(options, { record: existing });\n\t\t\t}\n\t\t\tif (probe.stdout.trim() !== \"GRIP_SHARED_GUEST\") {\n\t\t\t\tthrow new Error(\"Lima guest daemon mode is uncertain; refusing named attach\");\n\t\t\t}\n\t\t\tawait owner.assertHeld();\n\t\t\tconst mount = findWorkspaceMount(existing, config.workspaceMountPath);\n\t\t\tif (!mount?.location) throw new Error(\"Shared Lima instance mount is unavailable; refusing attach\");\n\t\t\tconst sharedOptions = {\n\t\t\t\tinstanceName: requestedName,\n\t\t\t\thostParent: resolve(mount.location),\n\t\t\t\thostRoot: resolve(options.workspaceRoot),\n\t\t\t};\n\t\t\tconst inspection = await this.inspectSharedInstance(sharedOptions);\n\t\t\tif (options.update) await this.updateSharedDaemon(sharedOptions, owner);\n\t\t\tconst token = await this.ensureSharedDaemon(sharedOptions, owner, options.update);\n\t\t\tconst daemonUrl = `grip+${inspection.daemonUrl}?root=${encodeURIComponent(inspection.guestRoot)}`;\n\t\t\treturn {\n\t\t\t\tcontainerId: requestedName,\n\t\t\t\tcontainerName: requestedName,\n\t\t\t\tworkspaceRoot: inspection.hostRoot,\n\t\t\t\tworkspaceMountPath: inspection.guestRoot,\n\t\t\t\tdaemonUrl,\n\t\t\t\tdaemonUrlRedacted: inspection.daemonUrl,\n\t\t\t\ttoken,\n\t\t\t\tlabels: { [`${SANDBOX_LABEL_PREFIX}.runtime`]: \"lima\", [`${SANDBOX_LABEL_PREFIX}.version`]: VERSION },\n\t\t\t};\n\t\t} finally {\n\t\t\tawait owner.release();\n\t\t}\n\t}\n\n\tprivate async startWithLegacyInstance(\n\t\toptions: SandboxStartOptions,\n\t\tknown?: KnownLimaRecord,\n\t): Promise<SandboxStartResult> {\n\t\tconst workspaceRoot = resolve(options.workspaceRoot);\n\t\tconst local = this.env.GRIP_SANDBOX_GUEST_BINARY\n\t\t\t? await inspectLocalGuestBinary(this.env.GRIP_SANDBOX_GUEST_BINARY)\n\t\t\t: undefined;\n\t\tconst config = this.resolveConfig(options);\n\t\tconst requestedName = options.name?.trim();\n\t\tconst managed = !requestedName;\n\t\tconst existing = known ? known.record : await this.findExactLegacyInstance(config, requestedName);\n\t\tif (existing) await this.prepareExistingInstance(config, existing, requestedName!, workspaceRoot);\n\t\tconst configuredForward = existing ? findDaemonForward(existing, config.daemonPort) : undefined;\n\t\tif (configuredForward && configuredForward.guestPort !== configuredForward.hostPort) {\n\t\t\tthrow new Error(\n\t\t\t\t`Lima instance ${requestedName} must forward the sandbox daemon on the same guest and host port`,\n\t\t\t);\n\t\t}\n\t\tconst daemonPort =\n\t\t\tconfiguredForward?.hostPort ?? (await this.portAllocator(config.daemonHostBind, config.daemonPort));\n\t\tconst name =\n\t\t\trequestedName ?? createInstanceName(config, workspaceRoot, options.sessionId ?? \"session\", daemonPort);\n\t\tconst effectiveConfig = { ...config, daemonPort };\n\t\tif (!existing) await this.startInstance(effectiveConfig, workspaceRoot, name, daemonPort);\n\t\t// A failed guest install/start can have changed state; never delete an uncertain VM.\n\t\tif (existing) await this.assertNoSharedGuestState(effectiveConfig, name);\n\t\tawait this.ensureGrip(effectiveConfig, name, !existing, local);\n\t\tconst token = await this.startDaemon(effectiveConfig, name);\n\t\tawait this.readinessWaiter(config.daemonHostBind, daemonPort);\n\t\t// A failed readiness check is not proof that this daemon is ours to stop.\n\t\t// Leave uncertain guest state for explicit operator reconciliation.\n\t\tthis.managedInstances.set(name, { workspaceRoot, daemonPort, ownerId: options.sessionId, managed });\n\t\tconst daemonUrl = `ws://${urlHost(daemonConnectionHost(config.daemonHostBind))}:${daemonPort}${DAEMON_WEBSOCKET_PATH}`;\n\t\treturn {\n\t\t\tcontainerId: name,\n\t\t\tcontainerName: name,\n\t\t\tworkspaceRoot,\n\t\t\tworkspaceMountPath: config.workspaceMountPath,\n\t\t\tdaemonUrl,\n\t\t\tdaemonUrlRedacted: daemonUrl,\n\t\t\ttoken,\n\t\t\tlabels: { [`${SANDBOX_LABEL_PREFIX}.runtime`]: \"lima\", [`${SANDBOX_LABEL_PREFIX}.version`]: VERSION },\n\t\t};\n\t}\n\n\tasync list(options: SandboxListOptions): Promise<SandboxContainer[]> {\n\t\tconst config = this.resolveConfig();\n\t\tconst workspaceRoot = resolve(options.workspaceRoot);\n\t\treturn (await this.listRecords(config))\n\t\t\t.map((record) => toSandboxContainer(record, config.workspaceMountPath))\n\t\t\t.filter(\n\t\t\t\t(instance) => instance.workspaceRoot && (options.allWorkspaces || instance.workspaceRoot === workspaceRoot),\n\t\t\t);\n\t}\n\n\tprivate async stopDaemon(config: SandboxConfig, name: string, daemonPort: number): Promise<void> {\n\t\tconst pidFile = `.cache/grip/sandbox-daemon-${daemonPort}.pid`;\n\t\tconst script = `if test -f \"$HOME/${pidFile}\"; then pid=\"$(cat \"$HOME/${pidFile}\")\"; kill \"$pid\" 2>/dev/null || true; rm -f \"$HOME/${pidFile}\"; fi`;\n\t\tconst args = [\"shell\", \"--tty=false\", name, \"sh\", \"-lc\", script];\n\t\tensureLimaSuccessful(config.limaBinary, await this.runner.run(config.limaBinary, args), args);\n\t}\n\n\tasync stop(options: SandboxStopOptions): Promise<SandboxStopResult> {\n\t\tconst config = this.resolveConfig();\n\t\tconst instances = await this.list({ workspaceRoot: options.workspaceRoot });\n\t\tconst selected = selectLimaSandbox(instances, options.target ?? options.currentContainerId);\n\t\tif (!selected) return { status: \"not-found\", message: \"No matching Grip Lima sandbox found\" };\n\t\tif (instances.filter((instance) => instance.name === selected.name).length !== 1) {\n\t\t\tthrow new Error(\"Lima sandbox identity is ambiguous; refusing to stop a VM or daemon\");\n\t\t}\n\t\tconst tracked = this.managedInstances.get(selected.name);\n\t\t// Names and Lima's list output do not establish ownership. In particular, a\n\t\t// shared named VM can have the ordinary sandbox prefix without being ours.\n\t\t// Only a VM started by this session may be stopped or have its daemon touched.\n\t\tif (!tracked || tracked.workspaceRoot !== selected.workspaceRoot || tracked.daemonPort !== selected.daemonPort) {\n\t\t\tthrow new Error(\n\t\t\t\t\"Lima sandbox ownership is unknown or daemon port changed; refusing to stop an untracked VM or daemon\",\n\t\t\t);\n\t\t}\n\t\t// A shared guest must never lose its daemon or VM to a legacy\n\t\t// in-process record. Resolve the selected VM again under the host owner lock.\n\t\tconst owner = await acquireSharedLimaOwnerLock(selected.name);\n\t\ttry {\n\t\t\tconst matches = (await this.list({ workspaceRoot: options.workspaceRoot })).filter(\n\t\t\t\t(instance) => instance.name === selected.name,\n\t\t\t);\n\t\t\tconst current = matches[0];\n\t\t\tif (\n\t\t\t\tmatches.length !== 1 ||\n\t\t\t\t!current ||\n\t\t\t\tcurrent.id !== selected.id ||\n\t\t\t\tcurrent.state !== selected.state ||\n\t\t\t\tcurrent.workspaceRoot !== selected.workspaceRoot ||\n\t\t\t\tcurrent.daemonPort !== selected.daemonPort\n\t\t\t) {\n\t\t\t\tthrow new Error(\"Lima sandbox identity changed during stop; refusing to touch the VM or daemon\");\n\t\t\t}\n\t\t\treturn await this.stopTrackedInstance(config, current, tracked);\n\t\t} finally {\n\t\t\tawait owner.release();\n\t\t}\n\t}\n\n\tprivate async stopTrackedInstance(\n\t\tconfig: SandboxConfig,\n\t\tselected: SandboxContainer,\n\t\ttracked: ManagedLimaInstance,\n\t): Promise<SandboxStopResult> {\n\t\tconst ownsInstance = tracked.managed;\n\t\tif (selected.state !== \"running\") {\n\t\t\tif (ownsInstance && config.cleanup === \"remove\") {\n\t\t\t\t// An already stopped VM cannot be probed without resuming it. Its\n\t\t\t\t// in-memory record is not proof it was never repurposed externally.\n\t\t\t\tthrow new Error(\"Lima sandbox is stopped; refusing to delete an unverifiable VM; reconcile manually\");\n\t\t\t}\n\t\t\tthis.managedInstances.delete(selected.name);\n\t\t\treturn { status: \"already-stopped\", container: selected };\n\t\t}\n\t\tawait this.assertNoSharedGuestState(config, selected.name, \"stop\");\n\t\tawait this.stopDaemon(config, selected.name, tracked?.daemonPort ?? selected.daemonPort ?? config.daemonPort);\n\t\tif (ownsInstance) {\n\t\t\tconst args = config.cleanup === \"remove\" ? [\"delete\", \"--force\", selected.name] : [\"stop\", selected.name];\n\t\t\tensureLimaSuccessful(config.limaBinary, await this.runner.run(config.limaBinary, args), args);\n\t\t}\n\t\tthis.managedInstances.delete(selected.name);\n\t\treturn { status: ownsInstance && config.cleanup === \"remove\" ? \"removed\" : \"stopped\", container: selected };\n\t}\n\n\tasync stopManagedInstances(): Promise<void> {\n\t\tconst failures: unknown[] = [];\n\t\tfor (const [name, instance] of [...this.managedInstances]) {\n\t\t\ttry {\n\t\t\t\tawait this.stop({ workspaceRoot: instance.workspaceRoot, target: name, currentContainerId: name });\n\t\t\t} catch (error) {\n\t\t\t\tfailures.push(error);\n\t\t\t}\n\t\t}\n\t\tif (failures.length > 0) throw new AggregateError(failures, \"Failed to stop managed Lima sandbox instances\");\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"lima.d.ts","sourceRoot":"","sources":["../../../src/core/sandbox/lima.ts"],"names":[],"mappings":"AAOA,OAAO,EAA8B,KAAK,mBAAmB,EAAE,MAAM,iDAAiD,CAAC;AACvH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,KAAK,EAGX,aAAa,EACb,sBAAsB,EACtB,gBAAgB,EAChB,kBAAkB,EAClB,aAAa,EACb,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAA8B,KAAK,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AAErG,MAAM,WAAW,4BAA6B,SAAQ,0BAA0B;IAC/E,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,yBAAyB;IACzC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IACnC,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,yBAAyB;IACzC,eAAe,CAAC,EAAE,IAAI,CAAC,eAAe,EAAE,oBAAoB,CAAC,CAAC;IAC9D,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACzE,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,gBAAgB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;CACpD;AA0KD,qBAAa,kBAAkB;IAC9B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA0D;IAC1F,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAoB;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA2D;IACzF,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAgD;IAChF,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAmC;IAEpE,YAAY,OAAO,GAAE,yBAA8B,EAOlD;IAED,aAAa,CAAC,SAAS,GAAE,sBAA2B,GAAG,aAAa,CAEnE;YAEa,WAAW;IAmBzB,+FAA+F;IACzF,qBAAqB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,4BAA4B,CAAC,CAwCrG;IACD,+FAA+F;IACzF,kBAAkB,CACvB,OAAO,EAAE,yBAAyB,EAClC,KAAK,EAAE,mBAAmB,EAC1B,OAAO,UAAQ,GACb,OAAO,CAAC,MAAM,CAAC,CA8GjB;YAGa,kBAAkB;YA4GlB,aAAa;YAuBb,UAAU;YA4BV,wBAAwB;YAcxB,WAAW;YA2BX,uBAAuB;YAqBvB,uBAAuB;IAU/B,KAAK,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA8DrE;YAEa,uBAAuB;IA8C/B,IAAI,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAQnE;YAEa,UAAU;IAOlB,IAAI,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAuClE;YAEa,mBAAmB;IAyB3B,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAU1C;CACD","sourcesContent":["import { createHash, randomBytes } from \"node:crypto\";\nimport { realpath } from \"node:fs/promises\";\nimport { createConnection } from \"node:net\";\nimport { basename, resolve } from \"node:path\";\nimport { VERSION } from \"../../config.ts\";\nimport { DAEMON_WEBSOCKET_PATH } from \"../../daemon/config.ts\";\nimport { spawnProcess } from \"../../utils/child-process.ts\";\nimport { acquireSharedLimaOwnerLock, type SharedLimaOwnerLock } from \"../remote-workspace-protocol/lima-owner-lock.ts\";\nimport type { SettingsManager } from \"../settings-manager.ts\";\nimport type {\n\tSandboxCommandOptions,\n\tSandboxCommandResult,\n\tSandboxConfig,\n\tSandboxConfigOverrides,\n\tSandboxContainer,\n\tSandboxListOptions,\n\tSandboxRunner,\n\tSandboxStartOptions,\n\tSandboxStartResult,\n\tSandboxStopOptions,\n\tSandboxStopResult,\n} from \"./config.ts\";\nimport { allocateSandboxDaemonPort, redactSecrets, resolveSandboxConfig, SANDBOX_LABEL_PREFIX } from \"./config.ts\";\nimport { inspectLocalGuestBinary, installLocalGuestBinary, type LocalGuestBinary } from \"./lima-guest-binary.ts\";\nimport { resolveSharedLimaWorkspace, type SharedLimaWorkspaceBinding } from \"./shared-lima-roots.ts\";\nimport { stageSharedGuestBinary } from \"./update-shared-guest-binary.ts\";\nexport interface SharedLimaInstanceInspection extends SharedLimaWorkspaceBinding {\n\tinstanceName: string;\n\tdaemonUrl: string;\n}\n\nexport interface SharedLimaInstanceOptions {\n\tinstanceName: string;\n\thostParent: string;\n\thostRoot: string;\n}\n\nexport interface ManagedLimaInstance {\n\tworkspaceRoot: string;\n\tdaemonPort: number;\n\townerId?: string;\n\tmanaged: boolean;\n}\n\nexport interface LimaSandboxServiceOptions {\n\tsettingsManager?: Pick<SettingsManager, \"getSandboxSettings\">;\n\tenv?: NodeJS.ProcessEnv;\n\trunner?: SandboxRunner;\n\tportAllocator?: (host: string, preferredPort: number) => Promise<number>;\n\treadinessWaiter?: (host: string, port: number) => Promise<void>;\n\tmanagedInstances?: Map<string, ManagedLimaInstance>;\n}\n\ninterface LimaMount {\n\tlocation?: string;\n\tmountPoint?: string;\n\twritable?: boolean;\n}\n\ninterface LimaPortForward {\n\tguestPort?: number;\n\thostPort?: number;\n\thostIP?: string;\n}\n\ninterface LimaConfig {\n\tmounts?: LimaMount[];\n\tportForwards?: LimaPortForward[];\n}\n\ninterface LimaListRecord {\n\tname?: string;\n\tstatus?: string;\n\tarch?: string;\n\tvmType?: string;\n\tdir?: string;\n\tconfig?: LimaConfig;\n}\n\ninterface KnownLimaRecord {\n\trecord: LimaListRecord | undefined;\n}\n\nclass ProcessLimaRunner implements SandboxRunner {\n\trun(command: string, args: string[], options: SandboxCommandOptions = {}): Promise<SandboxCommandResult> {\n\t\treturn new Promise((resolveCommand, reject) => {\n\t\t\tconst child = spawnProcess(command, args, {\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\tenv: { ...process.env, ...options.env },\n\t\t\t});\n\t\t\tlet stdout = \"\";\n\t\t\tlet stderr = \"\";\n\t\t\tchild.stdout?.on(\"data\", (chunk: Buffer) => {\n\t\t\t\tstdout += chunk.toString(\"utf8\");\n\t\t\t});\n\t\t\tchild.stderr?.on(\"data\", (chunk: Buffer) => {\n\t\t\t\tstderr += chunk.toString(\"utf8\");\n\t\t\t});\n\t\t\tchild.once(\"error\", reject);\n\t\t\tchild.once(\"close\", (exitCode) => resolveCommand({ exitCode, stdout, stderr }));\n\t\t});\n\t}\n}\n\nfunction shellQuote(value: string): string {\n\treturn `'${value.replaceAll(\"'\", `'\"'\"'`)}'`;\n}\n\nfunction parseLimaRecords(output: string): LimaListRecord[] {\n\tconst text = output.trim();\n\tif (!text) return [];\n\ttry {\n\t\tconst parsed = JSON.parse(text) as unknown;\n\t\treturn Array.isArray(parsed) ? (parsed as LimaListRecord[]) : [parsed as LimaListRecord];\n\t} catch {\n\t\treturn text\n\t\t\t.split(/\\r?\\n/)\n\t\t\t.filter(Boolean)\n\t\t\t.map((line) => JSON.parse(line) as LimaListRecord);\n\t}\n}\n\nfunction ensureLimaSuccessful(binary: string, result: SandboxCommandResult, args: string[]): void {\n\tif (result.exitCode === 0) return;\n\tconst detail = result.stderr.trim() || result.stdout.trim() || `exit code ${result.exitCode ?? \"unknown\"}`;\n\tthrow new Error(redactSecrets(`Lima command failed: ${binary} ${args.join(\" \")}: ${detail}`));\n}\n\nfunction daemonConnectionHost(host: string): string {\n\tif (host === \"0.0.0.0\") return \"127.0.0.1\";\n\tif (host === \"::\") return \"::1\";\n\treturn host;\n}\n\nfunction urlHost(host: string): string {\n\treturn host.includes(\":\") && !host.startsWith(\"[\") ? `[${host}]` : host;\n}\n\nasync function canConnect(host: string, port: number): Promise<boolean> {\n\treturn new Promise((resolveConnection) => {\n\t\tconst socket = createConnection({ host, port });\n\t\tlet settled = false;\n\t\tconst finish = (connected: boolean): void => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tsocket.destroy();\n\t\t\tresolveConnection(connected);\n\t\t};\n\t\tsocket.setTimeout(250, () => finish(false));\n\t\tsocket.once(\"connect\", () => finish(true));\n\t\tsocket.once(\"error\", () => finish(false));\n\t});\n}\n\nasync function waitForLimaDaemon(host: string, port: number): Promise<void> {\n\tconst deadline = Date.now() + 30_000;\n\twhile (Date.now() < deadline) {\n\t\tif (await canConnect(daemonConnectionHost(host), port)) return;\n\t\tawait new Promise((resolveDelay) => setTimeout(resolveDelay, 100));\n\t}\n\tthrow new Error(`Lima sandbox daemon did not become reachable on ${daemonConnectionHost(host)}:${port}`);\n}\n\nfunction createInstanceName(\n\tconfig: SandboxConfig,\n\tworkspaceRoot: string,\n\tsessionId: string,\n\tdaemonPort: number,\n): string {\n\tconst clean = (value: string): string =>\n\t\tvalue\n\t\t\t.toLowerCase()\n\t\t\t.replace(/[^a-z0-9-]+/g, \"-\")\n\t\t\t.replace(/^-+|-+$/g, \"\") || \"workspace\";\n\tconst hash = createHash(\"sha256\").update(resolve(workspaceRoot)).digest(\"hex\").slice(0, 12);\n\treturn `${clean(config.limaInstanceNamePrefix)}-${clean(basename(resolve(workspaceRoot)))}-${hash}-${clean(sessionId).slice(0, 8).replace(/-+$/, \"\")}-p${daemonPort}-${randomBytes(4).toString(\"hex\")}`;\n}\n\nfunction findWorkspaceMount(record: LimaListRecord, workspaceMountPath: string): LimaMount | undefined {\n\treturn record.config?.mounts?.find((mount) => mount.mountPoint === workspaceMountPath);\n}\n\nfunction findDaemonForward(record: LimaListRecord, preferredPort?: number): LimaPortForward | undefined {\n\tconst namePort = Number(record.name?.match(/-p(\\d+)-[a-f0-9]+$/)?.[1]);\n\tconst daemonPort = Number.isInteger(namePort) && namePort > 0 ? namePort : preferredPort;\n\tconst forwards = record.config?.portForwards?.filter(\n\t\t(forward) => forward.guestPort === forward.hostPort && forward.hostPort !== undefined,\n\t);\n\treturn (\n\t\tforwards?.find((forward) => forward.guestPort === daemonPort) ??\n\t\t(forwards?.length === 1 ? forwards[0] : undefined)\n\t);\n}\n\nfunction toSandboxContainer(record: LimaListRecord, workspaceMountPath: string): SandboxContainer {\n\tconst mount = findWorkspaceMount(record, workspaceMountPath);\n\tconst forward = findDaemonForward(record);\n\tconst daemonEndpoint = forward?.hostPort\n\t\t? `ws://${urlHost(daemonConnectionHost(forward.hostIP || \"127.0.0.1\"))}:${forward.hostPort}${DAEMON_WEBSOCKET_PATH}`\n\t\t: undefined;\n\treturn {\n\t\tid: record.name ?? \"\",\n\t\tname: record.name ?? \"\",\n\t\timage: `lima:${record.vmType ?? \"vm\"}/${record.arch ?? \"unknown\"}`,\n\t\tstatus: record.status ?? \"\",\n\t\tstate: record.status?.toLowerCase(),\n\t\tcreatedAt: undefined,\n\t\tworkspaceRoot: mount?.location ? resolve(mount.location) : undefined,\n\t\tworkspaceMountPath: mount?.mountPoint,\n\t\tdaemonPort: forward?.guestPort,\n\t\tdaemonEndpoint,\n\t\tlabels: { [`${SANDBOX_LABEL_PREFIX}.runtime`]: \"lima\", [`${SANDBOX_LABEL_PREFIX}.version`]: VERSION },\n\t};\n}\n\nfunction selectLimaSandbox(instances: SandboxContainer[], target: string | undefined): SandboxContainer | undefined {\n\tif (!target) return instances.length === 1 ? instances[0] : undefined;\n\treturn instances.find((instance) => instance.name === target || instance.id.startsWith(target));\n}\ntype LegacyLimaGuestStateOperation = \"attach\" | \"stop\";\n\nexport class LimaSandboxService {\n\tprivate readonly settingsManager: Pick<SettingsManager, \"getSandboxSettings\"> | undefined;\n\tprivate readonly env: NodeJS.ProcessEnv;\n\tprivate readonly runner: SandboxRunner;\n\tprivate readonly portAllocator: (host: string, preferredPort: number) => Promise<number>;\n\tprivate readonly readinessWaiter: (host: string, port: number) => Promise<void>;\n\tprivate readonly managedInstances: Map<string, ManagedLimaInstance>;\n\n\tconstructor(options: LimaSandboxServiceOptions = {}) {\n\t\tthis.settingsManager = options.settingsManager;\n\t\tthis.env = options.env ?? process.env;\n\t\tthis.runner = options.runner ?? new ProcessLimaRunner();\n\t\tthis.portAllocator = options.portAllocator ?? allocateSandboxDaemonPort;\n\t\tthis.readinessWaiter = options.readinessWaiter ?? waitForLimaDaemon;\n\t\tthis.managedInstances = options.managedInstances ?? new Map();\n\t}\n\n\tresolveConfig(overrides: SandboxConfigOverrides = {}): SandboxConfig {\n\t\treturn resolveSandboxConfig(this.settingsManager?.getSandboxSettings(), this.env, overrides);\n\t}\n\n\tprivate async listRecords(config: SandboxConfig, name?: string): Promise<LimaListRecord[]> {\n\t\tconst args = [\"list\", \"--json\", ...(name ? [name] : [])];\n\t\ttry {\n\t\t\tconst result = await this.runner.run(config.limaBinary, args);\n\t\t\tif (\n\t\t\t\tname &&\n\t\t\t\tresult.exitCode !== 0 &&\n\t\t\t\t/unmatched|not found|does not exist/i.test(`${result.stderr}\\n${result.stdout}`)\n\t\t\t) {\n\t\t\t\treturn [];\n\t\t\t}\n\t\t\tensureLimaSuccessful(config.limaBinary, result, args);\n\t\t\treturn parseLimaRecords(result.stdout);\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tthrow new Error(redactSecrets(`Lima is unavailable or not reachable via ${config.limaBinary}: ${message}`));\n\t\t}\n\t}\n\n\t/** Read-only validation: no VM creation, daemon restart, token rotation or ownership claim. */\n\tasync inspectSharedInstance(options: SharedLimaInstanceOptions): Promise<SharedLimaInstanceInspection> {\n\t\tconst config = this.resolveConfig();\n\t\tconst { instanceName } = options;\n\t\tif (!/^[a-z0-9][a-z0-9-]*$/.test(instanceName)) throw new Error(\"Invalid shared Lima instance name\");\n\t\tif (config.daemonHostBind !== \"127.0.0.1\" && config.daemonHostBind !== \"::1\") {\n\t\t\tthrow new Error(\"Shared Lima daemon forward must bind to loopback\");\n\t\t}\n\t\tconst binding = await resolveSharedLimaWorkspace(options.hostParent, config.workspaceMountPath, options.hostRoot);\n\t\tconst records = await this.listRecords(config, instanceName);\n\t\tif (records.length !== 1 || records[0]?.name !== instanceName) {\n\t\t\tthrow new Error(`Shared Lima instance ${instanceName} does not exist by exact name`);\n\t\t}\n\t\tconst record = records[0];\n\t\tif (record.status?.toLowerCase() !== \"running\") {\n\t\t\tthrow new Error(`Shared Lima instance ${instanceName} is not running`);\n\t\t}\n\t\tconst mounts = record.config?.mounts?.filter((mount) => mount.mountPoint === binding.guestParent) ?? [];\n\t\tif (mounts.length !== 1 || !mounts[0]?.writable || !mounts[0].location) {\n\t\t\tthrow new Error(`Shared Lima instance ${instanceName} requires one writable fixed parent mount`);\n\t\t}\n\t\tif ((await realpath(mounts[0].location)) !== binding.hostParent) {\n\t\t\tthrow new Error(`Shared Lima instance ${instanceName} mounts a different host parent`);\n\t\t}\n\t\tconst forwards =\n\t\t\trecord.config?.portForwards?.filter(\n\t\t\t\t(forward) => forward.guestPort === config.daemonPort || forward.hostPort === config.daemonPort,\n\t\t\t) ?? [];\n\t\tif (\n\t\t\tforwards.length !== 1 ||\n\t\t\tforwards[0]?.guestPort !== config.daemonPort ||\n\t\t\tforwards[0].hostPort !== config.daemonPort ||\n\t\t\tforwards[0].hostIP !== config.daemonHostBind\n\t\t) {\n\t\t\tthrow new Error(`Shared Lima instance ${instanceName} requires one matching loopback daemon forward`);\n\t\t}\n\t\treturn {\n\t\t\t...binding,\n\t\t\tinstanceName,\n\t\t\tdaemonUrl: `ws://${urlHost(config.daemonHostBind)}:${config.daemonPort}${DAEMON_WEBSOCKET_PATH}`,\n\t\t};\n\t}\n\t/** Reinspect and reuse the supervised guest daemon under the exclusive named-VM owner lock. */\n\tasync ensureSharedDaemon(\n\t\toptions: SharedLimaInstanceOptions,\n\t\towner: SharedLimaOwnerLock,\n\t\tupdated = false,\n\t): Promise<string> {\n\t\tif (owner.instanceName !== options.instanceName) throw new Error(\"Shared Lima owner lock names a different VM\");\n\t\tawait owner.assertHeld();\n\t\tawait this.inspectSharedInstance(options);\n\t\tconst config = this.resolveConfig();\n\t\tconst local = this.env.GRIP_SANDBOX_GUEST_BINARY\n\t\t\t? await inspectLocalGuestBinary(this.env.GRIP_SANDBOX_GUEST_BINARY)\n\t\t\t: undefined;\n\t\tconst versionArgs = [\n\t\t\t\"shell\",\n\t\t\t\"--tty=false\",\n\t\t\toptions.instanceName,\n\t\t\t\"sh\",\n\t\t\t\"-lc\",\n\t\t\tlocal || updated ? '\"$HOME/.local/bin/grip\" --version' : \"grip --version\",\n\t\t];\n\t\tconst version = await this.runner.run(config.limaBinary, versionArgs);\n\t\tif (version.exitCode !== 0 || version.stdout.trim() !== VERSION) {\n\t\t\tthrow new Error(\"Shared Lima grip version mismatch or binary missing; install the matching version manually\");\n\t\t}\n\t\tawait owner.assertHeld();\n\t\tconst directory = \"$HOME/.cache/grip/shared-lima\";\n\t\t// /proc start time plus boot ID distinguishes a recycled PID or a rebooted guest.\n\t\t// kill -0 also succeeds for zombies, which must never be reused as a live daemon.\n\t\tconst processStartTime = `awk '{ sub(/^.*\\\\) /, \"\"); if ($1 == \"Z\" || $1 == \"X\") exit 1; print $20 }'`;\n\t\tconst script = [\n\t\t\t\"set -eu\",\n\t\t\t\"umask 077\",\n\t\t\t`directory=\"${directory}\"`,\n\t\t\t// Both current modes reserve the guest transition; a crashed reservation is operator-owned.\n\t\t\t'mkdir -p \"$HOME/.cache/grip\"',\n\t\t\t'mode_lock=\"$HOME/.cache/grip/daemon-mode.lock\"',\n\t\t\t'mkdir \"$mode_lock\" || exit 28',\n\t\t\t\"trap 'rmdir \\\"$mode_lock\\\" 2>/dev/null || true' EXIT\",\n\t\t\t// A legacy per-session daemon may own this VM or its port. Do not start a second daemon.\n\t\t\t'for legacy_file in \"$HOME/.cache/grip\"/sandbox-daemon-*.pid \"$HOME/.cache/grip\"/sandbox-daemon-*.log; do',\n\t\t\t' if test -e \"$legacy_file\" || test -L \"$legacy_file\"; then exit 27; fi',\n\t\t\t\"done\",\n\t\t\t// A transient user service must outlive the Lima SSH session. Do not mutate\n\t\t\t// private state unless the guest manager is running and linger was enabled by an operator.\n\t\t\t\"command -v systemd-run >/dev/null && systemctl --user show-environment >/dev/null || exit 29\",\n\t\t\t'test \"$(loginctl show-user \"$(id -un)\" --property=Linger --value)\" = yes || exit 29',\n\t\t\t`mkdir -p \"$directory\"; test ! -L \"$directory\"; chmod 700 \"$directory\"`,\n\t\t\t'rmdir \"$mode_lock\" || exit 28',\n\t\t\t\"trap - EXIT\",\n\t\t\t`token_file=\"$directory/daemon-${config.daemonPort}.token\"`,\n\t\t\t`pid_file=\"$directory/daemon-${config.daemonPort}.pid\"`,\n\t\t\t`identity_file=\"$directory/daemon-${config.daemonPort}.identity\"`,\n\t\t\t`log_file=\"$directory/daemon-${config.daemonPort}.log\"`,\n\t\t\t`start_lock=\"$directory/daemon-${config.daemonPort}.starting\"`,\n\t\t\t`unit=\"grip-shared-lima-${config.daemonPort}.service\"`,\n\t\t\t'test ! -L \"$token_file\"; test ! -L \"$pid_file\"; test ! -L \"$identity_file\"; test ! -L \"$log_file\"',\n\t\t\t'if test -e \"$log_file\"; then test -f \"$log_file\" && test \"$(stat -c %a \"$log_file\")\" = 600 || exit 22; fi',\n\t\t\t'test ! -e \"$start_lock\" && test ! -L \"$start_lock\" || exit 26',\n\t\t\t'if test -e \"$token_file\" || test -e \"$pid_file\" || test -e \"$identity_file\"; then',\n\t\t\t' test -f \"$token_file\" && test -f \"$pid_file\" && test -f \"$identity_file\" || exit 21',\n\t\t\t' test \"$(stat -c %a \"$token_file\")\" = 600 || exit 22',\n\t\t\t' test \"$(stat -c %a \"$pid_file\")\" = 600 || exit 22',\n\t\t\t' test \"$(stat -c %a \"$identity_file\")\" = 600 || exit 22',\n\t\t\t' pid=\"$(cat \"$pid_file\")\"',\n\t\t\t\" case \\\"$pid\\\" in ''|*[!0-9]*) exit 23;; esac\",\n\t\t\t' kill -0 \"$pid\" || exit 24',\n\t\t\t` start_time=\"$(${processStartTime} \"/proc/$pid/stat\")\" || exit 25`,\n\t\t\t' boot_id=\"$(cat /proc/sys/kernel/random/boot_id)\" || exit 25',\n\t\t\t' test -n \"$start_time\" && test -n \"$boot_id\" && test \"$(cat \"$identity_file\")\" = \"$boot_id:$start_time\" || exit 25',\n\t\t\t' test \"$(systemctl --user show -P MainPID \"$unit\")\" = \"$pid\" && systemctl --user is-active --quiet \"$unit\" || exit 25',\n\t\t\t' token=\"$(cat \"$token_file\")\"',\n\t\t\t\"else\",\n\t\t\t' mkdir \"$start_lock\" || exit 26',\n\t\t\t\" trap 'rmdir \\\"$start_lock\\\" 2>/dev/null || true' EXIT\",\n\t\t\t\" token=\\\"$(head -c 32 /dev/urandom | base64 | tr -d '\\\\n')\\\"\",\n\t\t\t' printf \\'%s\\' \"$token\" >\"$token_file\"',\n\t\t\t' chmod 600 \"$token_file\"',\n\t\t\t// The systemd user manager owns the process. It reads the private token file\n\t\t\t// inside the unit, never from a systemd-run argument or environment property.\n\t\t\tlocal\n\t\t\t\t? ' grip_bin=\"$HOME/.local/bin/grip\"; test -x \"$grip_bin\"'\n\t\t\t\t: ' grip_bin=\"$(command -v grip)\"; case \"$grip_bin\" in /*) ;; *) exit 25;; esac',\n\t\t\t` 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`,\n\t\t\t' pid=\"\"; attempts=0',\n\t\t\t' while test \"$attempts\" -lt 50; do',\n\t\t\t' pid=\"$(systemctl --user show -P MainPID \"$unit\")\" || exit 25',\n\t\t\t' case \"$pid\" in \"\"|0) sleep 0.1;; *[!0-9]*) exit 25;; *) break;; esac',\n\t\t\t\" attempts=$((attempts + 1))\",\n\t\t\t\" done\",\n\t\t\t' case \"$pid\" in \"\"|0|*[!0-9]*) exit 25;; esac',\n\t\t\t' systemctl --user is-active --quiet \"$unit\" || exit 25',\n\t\t\t' printf \\'%s\\' \"$pid\" >\"$pid_file\"',\n\t\t\t' chmod 600 \"$pid_file\"',\n\t\t\t` start_time=\"$(${processStartTime} \"/proc/$pid/stat\")\" || exit 25`,\n\t\t\t' boot_id=\"$(cat /proc/sys/kernel/random/boot_id)\" || exit 25',\n\t\t\t' test -n \"$start_time\" && test -n \"$boot_id\" || exit 25',\n\t\t\t' printf \\'%s:%s\\' \"$boot_id\" \"$start_time\" >\"$identity_file\"',\n\t\t\t' chmod 600 \"$identity_file\"',\n\t\t\t' rmdir \"$start_lock\" || exit 26',\n\t\t\t\" trap - EXIT\",\n\t\t\t\"fi\",\n\t\t\t\"printf 'GRIP_SHARED_TOKEN=%s\\\\n' \\\"$token\\\"\",\n\t\t].join(\"\\n\");\n\t\tconst args = [\"shell\", \"--tty=false\", options.instanceName, \"sh\", \"-lc\", script];\n\t\tconst result = await this.runner.run(config.limaBinary, args);\n\t\t// Never include stdout/stderr from a credential-bearing shell in an error.\n\t\tif (result.exitCode !== 0)\n\t\t\tthrow new Error(\"Shared Lima daemon state is uncertain; reconcile guest PID/token files manually\");\n\t\tconst token = result.stdout.match(/(?:^|\\n)GRIP_SHARED_TOKEN=([A-Za-z0-9+/]{43}=)(?:\\r?\\n|$)/)?.[1];\n\t\tif (!token) throw new Error(\"Shared Lima daemon did not return a valid credential; reconcile manually\");\n\t\tawait this.readinessWaiter(config.daemonHostBind, config.daemonPort);\n\t\t// Readiness can outlive the owner. Never hand a guest credential to a lost owner.\n\t\tawait owner.assertHeld();\n\t\treturn token;\n\t}\n\n\t/** Update only a verified supervised daemon. Staging happens before disconnecting peers. */\n\tprivate async updateSharedDaemon(options: SharedLimaInstanceOptions, owner: SharedLimaOwnerLock): Promise<void> {\n\t\tawait owner.assertHeld();\n\t\tawait this.inspectSharedInstance(options);\n\t\tconst config = this.resolveConfig();\n\t\tconst name = options.instanceName;\n\t\tconst port = config.daemonPort;\n\t\tif (!this.env.GRIP_SANDBOX_GUEST_BINARY && process.argv[1]?.endsWith(\"/packages/coding-agent/src/cli.ts\")) {\n\t\t\tthrow new Error(\n\t\t\t\t\"Development sandbox update requires GRIP_SANDBOX_GUEST_BINARY pointing to a built Linux Grip executable\",\n\t\t\t);\n\t\t}\n\t\tconst local = this.env.GRIP_SANDBOX_GUEST_BINARY\n\t\t\t? await inspectLocalGuestBinary(this.env.GRIP_SANDBOX_GUEST_BINARY)\n\t\t\t: undefined;\n\t\tconst shell = (script: string) =>\n\t\t\tthis.runner.run(config.limaBinary, [\"shell\", \"--tty=false\", name, \"sh\", \"-lc\", script]);\n\t\tconst directory = \"$HOME/.cache/grip/shared-lima\";\n\t\tconst unit = `grip-shared-lima-${port}.service`;\n\t\tconst verify = [\n\t\t\t\"set -eu\",\n\t\t\t`directory=\"${directory}\"`,\n\t\t\t`unit=${shellQuote(unit)}`,\n\t\t\t'test -d \"$directory\" && test ! -L \"$directory\"',\n\t\t\t'test ! -e \"$HOME/.cache/grip/daemon-mode.lock\" && test ! -L \"$HOME/.cache/grip/daemon-mode.lock\"',\n\t\t\t`token_file=\"$directory/daemon-${port}.token\"`,\n\t\t\t`pid_file=\"$directory/daemon-${port}.pid\"`,\n\t\t\t`identity_file=\"$directory/daemon-${port}.identity\"`,\n\t\t\t'for file in \"$token_file\" \"$pid_file\" \"$identity_file\"; do test -f \"$file\" && test ! -L \"$file\" && test \"$(stat -c %a \"$file\")\" = 600; done',\n\t\t\t'pid=\"$(cat \"$pid_file\")\"; case \"$pid\" in \"\"|0|*[!0-9]*) exit 45;; esac',\n\t\t\t'kill -0 \"$pid\"',\n\t\t\t`start_time=\"$(awk '{ sub(/^.*\\\\) /, \"\"); if ($1 == \"Z\" || $1 == \"X\") exit 1; print $20 }' \"/proc/$pid/stat\")\"`,\n\t\t\t'boot_id=\"$(cat /proc/sys/kernel/random/boot_id)\"',\n\t\t\t'test -n \"$start_time\" && test \"$(cat \"$identity_file\")\" = \"$boot_id:$start_time\"',\n\t\t\t'test \"$(systemctl --user show -P MainPID \"$unit\")\" = \"$pid\" && systemctl --user is-active --quiet \"$unit\"',\n\t\t\t'case \"$(systemctl --user show -P ExecStart \"$unit\")\" in *\"$HOME/.local/bin/grip\"*) ;; *) exit 46;; esac',\n\t\t];\n\t\tconst preflight = await shell(verify.join(\"\\n\"));\n\t\tif (preflight.exitCode !== 0) {\n\t\t\tthrow new Error(\n\t\t\t\t\"Shared Lima daemon identity or binary path is uncertain; update refused without disconnecting clients\",\n\t\t\t);\n\t\t}\n\t\tconst staged = await stageSharedGuestBinary(this.runner, config.limaBinary, name, unit, VERSION, local);\n\t\ttry {\n\t\t\tawait owner.assertHeld();\n\t\t\tconst stop = await shell(\n\t\t\t\t[\n\t\t\t\t\t...verify,\n\t\t\t\t\t'mkdir \"$HOME/.cache/grip/daemon-mode.lock\" || exit 45',\n\t\t\t\t\t'trap \\u0027rmdir \"$HOME/.cache/grip/daemon-mode.lock\" 2>/dev/null || true\\u0027 EXIT',\n\t\t\t\t\t'systemctl --user stop \"$unit\"',\n\t\t\t\t\t'test \"$(systemctl --user show -P MainPID \"$unit\")\" = 0',\n\t\t\t\t\t'! kill -0 \"$pid\" 2>/dev/null',\n\t\t\t\t].join(\"\\n\"),\n\t\t\t);\n\t\t\tif (stop.exitCode !== 0)\n\t\t\t\tthrow new Error(\"Shared daemon stop is uncertain; inspect the guest before retrying update\");\n\t\t\tawait owner.assertHeld();\n\t\t\tawait staged.install();\n\t\t\tawait owner.assertHeld();\n\t\t\tconst restart = await shell(\n\t\t\t\t[\n\t\t\t\t\t\"set -eu\",\n\t\t\t\t\t`directory=\"${directory}\"`,\n\t\t\t\t\t`unit=${shellQuote(unit)}`,\n\t\t\t\t\t'test -d \"$directory\" && test ! -L \"$directory\"',\n\t\t\t\t\t`token_file=\"$directory/daemon-${port}.token\"`,\n\t\t\t\t\t`pid_file=\"$directory/daemon-${port}.pid\"`,\n\t\t\t\t\t`identity_file=\"$directory/daemon-${port}.identity\"`,\n\t\t\t\t\t'for file in \"$token_file\" \"$pid_file\" \"$identity_file\"; do test -f \"$file\" && test ! -L \"$file\" && test \"$(stat -c %a \"$file\")\" = 600; done',\n\t\t\t\t\t'test ! -e \"$HOME/.cache/grip/daemon-mode.lock\" && test ! -L \"$HOME/.cache/grip/daemon-mode.lock\"',\n\t\t\t\t\t'mkdir \"$HOME/.cache/grip/daemon-mode.lock\" || exit 45',\n\t\t\t\t\t'trap \\u0027rmdir \"$HOME/.cache/grip/daemon-mode.lock\" 2>/dev/null || true\\u0027 EXIT',\n\t\t\t\t\t'old_pid=\"$(cat \"$pid_file\")\"; case \"$old_pid\" in \"\"|0|*[!0-9]*) exit 45;; esac',\n\t\t\t\t\t'! kill -0 \"$old_pid\" 2>/dev/null',\n\t\t\t\t\t`test \"$(\"$HOME/.local/bin/grip\" --version)\" = ${shellQuote(VERSION)}`,\n\t\t\t\t\t'load=\"$(systemctl --user show -P LoadState \"$unit\" 2>/dev/null || true)\"',\n\t\t\t\t\t'case \"$load\" in',\n\t\t\t\t\t' loaded) test \"$(systemctl --user show -P MainPID \"$unit\")\" = 0; systemctl --user start \"$unit\" ;;',\n\t\t\t\t\t' not-found|\"\")',\n\t\t\t\t\t` 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 ;;`,\n\t\t\t\t\t\" *) exit 45 ;;\",\n\t\t\t\t\t\"esac\",\n\t\t\t\t\t'pid=\"\"; attempts=0',\n\t\t\t\t\t'while test \"$attempts\" -lt 50; do',\n\t\t\t\t\t' pid=\"$(systemctl --user show -P MainPID \"$unit\")\" || exit 45',\n\t\t\t\t\t' case \"$pid\" in \"\"|0) sleep 0.1;; *[!0-9]*) exit 45;; *) break;; esac',\n\t\t\t\t\t\" attempts=$((attempts + 1))\",\n\t\t\t\t\t\"done\",\n\t\t\t\t\t'case \"$pid\" in \"\"|0|*[!0-9]*) exit 45;; esac',\n\t\t\t\t\t'kill -0 \"$pid\"; systemctl --user is-active --quiet \"$unit\"',\n\t\t\t\t\t`start_time=\"$(awk '{ sub(/^.*\\\\) /, \"\"); if ($1 == \"Z\" || $1 == \"X\") exit 1; print $20 }' \"/proc/$pid/stat\")\"`,\n\t\t\t\t\t'boot_id=\"$(cat /proc/sys/kernel/random/boot_id)\"',\n\t\t\t\t\t'test -n \"$start_time\" && test -n \"$boot_id\"',\n\t\t\t\t\t'printf \"%s\" \"$pid\" >\"$pid_file\"',\n\t\t\t\t\t'printf \"%s:%s\" \"$boot_id\" \"$start_time\" >\"$identity_file\"',\n\t\t\t\t\t'chmod 600 \"$pid_file\" \"$identity_file\"',\n\t\t\t\t].join(\"\\n\"),\n\t\t\t);\n\t\t\tif (restart.exitCode !== 0)\n\t\t\t\tthrow new Error(\"Shared daemon restart is uncertain; inspect the guest before retrying update\");\n\t\t\tawait this.readinessWaiter(config.daemonHostBind, port);\n\t\t\tawait owner.assertHeld();\n\t\t} finally {\n\t\t\tawait staged.cleanup();\n\t\t}\n\t}\n\n\tprivate async startInstance(\n\t\tconfig: SandboxConfig,\n\t\tworkspaceRoot: string,\n\t\tname: string,\n\t\tdaemonPort: number,\n\t): Promise<void> {\n\t\tconst mountExpression = `.mounts = [{\"location\":${JSON.stringify(workspaceRoot)},\"mountPoint\":${JSON.stringify(config.workspaceMountPath)},\"writable\":true}]`;\n\t\tconst forwardExpression = `.portForwards += [{\"guestPort\":${daemonPort},\"hostPort\":${daemonPort},\"hostIP\":${JSON.stringify(config.daemonHostBind)}}]`;\n\t\tconst args = [\n\t\t\t\"start\",\n\t\t\t\"--tty=false\",\n\t\t\t\"--name\",\n\t\t\tname,\n\t\t\t\"--mount-none\",\n\t\t\t\"--set\",\n\t\t\tmountExpression,\n\t\t\t\"--set\",\n\t\t\tforwardExpression,\n\t\t\tconfig.limaTemplate,\n\t\t];\n\t\tensureLimaSuccessful(config.limaBinary, await this.runner.run(config.limaBinary, args), args);\n\t}\n\n\tprivate async ensureGrip(\n\t\tconfig: SandboxConfig,\n\t\tname: string,\n\t\texisting: boolean,\n\t\tlocal?: LocalGuestBinary,\n\t): Promise<void> {\n\t\tif (local) {\n\t\t\tawait installLocalGuestBinary(this.runner, config.limaBinary, name, local, VERSION);\n\t\t\treturn;\n\t\t}\n\t\tconst checkArgs = [\n\t\t\t\"shell\",\n\t\t\t\"--tty=false\",\n\t\t\tname,\n\t\t\t\"sh\",\n\t\t\t\"-lc\",\n\t\t\t'command -v grip >/dev/null || test -x \"$HOME/.local/bin/grip\"',\n\t\t];\n\t\tconst check = await this.runner.run(config.limaBinary, checkArgs);\n\t\tif (check.exitCode === 0) return;\n\t\t// Existing named VMs can be bootstrapped too. Keep managed VMs pinned to\n\t\t// the host release; use the latest published release for user-owned VMs.\n\t\tconst version = existing ? \"\" : `GRIP_VERSION=${shellQuote(`v${VERSION}`)} `;\n\t\tconst script = `set -eu; tmp=\"$(mktemp)\"; trap 'rm -f \"$tmp\"' EXIT; curl -fsSL -o \"$tmp\" https://raw.githubusercontent.com/greppleai/grip/main/install.sh; ${version}sh \"$tmp\"; test -x \"$HOME/.local/bin/grip\"`;\n\t\tconst args = [\"shell\", \"--tty=false\", name, \"sh\", \"-lc\", script];\n\t\tensureLimaSuccessful(config.limaBinary, await this.runner.run(config.limaBinary, args), args);\n\t}\n\t/** A legacy named attach or stop must not rotate or kill a shared guest daemon. */\n\tprivate async assertNoSharedGuestState(\n\t\tconfig: SandboxConfig,\n\t\tname: string,\n\t\toperation: LegacyLimaGuestStateOperation = \"attach\",\n\t): Promise<void> {\n\t\tconst script = 'test ! -e \"$HOME/.cache/grip/shared-lima\" && test ! -L \"$HOME/.cache/grip/shared-lima\"';\n\t\tconst args = [\"shell\", \"--tty=false\", name, \"sh\", \"-lc\", script];\n\t\tconst result = await this.runner.run(config.limaBinary, args);\n\t\t// A guest shell failure is ambiguous too; never guess that shared state is absent.\n\t\tif (result.exitCode !== 0) {\n\t\t\tthrow new Error(`Lima named VM may host a shared daemon; legacy ${operation} refused; reconcile manually`);\n\t\t}\n\t}\n\n\tprivate async startDaemon(config: SandboxConfig, name: string): Promise<string> {\n\t\tconst pidFile = `.cache/grip/sandbox-daemon-${config.daemonPort}.pid`;\n\t\tconst logFile = `.cache/grip/sandbox-daemon-${config.daemonPort}.log`;\n\t\tconst script = [\n\t\t\t\"set -eu\",\n\t\t\t\"umask 077\",\n\t\t\t`mkdir -p \"$HOME/.cache/grip\"`,\n\t\t\t'mode_lock=\"$HOME/.cache/grip/daemon-mode.lock\"',\n\t\t\t'mkdir \"$mode_lock\" || exit 28',\n\t\t\t\"trap 'rmdir \\\"$mode_lock\\\" 2>/dev/null || true' EXIT\",\n\t\t\t'test ! -e \"$HOME/.cache/grip/shared-lima\" && test ! -L \"$HOME/.cache/grip/shared-lima\" || exit 29',\n\t\t\t`if test -f \"$HOME/${pidFile}\"; then old_pid=\"$(cat \"$HOME/${pidFile}\")\"; kill \"$old_pid\" 2>/dev/null || true; fi`,\n\t\t\t`token=\"$(head -c 32 /dev/urandom | base64 | tr -d '\\\\n')\"`,\n\t\t\t`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 &`,\n\t\t\t`echo $! >\"$HOME/${pidFile}\"`,\n\t\t\t'rmdir \"$mode_lock\" || exit 28',\n\t\t\t\"trap - EXIT\",\n\t\t\t`printf 'GRIP_SANDBOX_TOKEN=%s\\\\n' \"$token\"`,\n\t\t].join(\"\\n\");\n\t\tconst args = [\"shell\", \"--tty=false\", \"--workdir\", config.workspaceMountPath, name, \"sh\", \"-lc\", script];\n\t\tconst result = await this.runner.run(config.limaBinary, args);\n\t\tensureLimaSuccessful(config.limaBinary, result, args);\n\t\tconst token = result.stdout.match(/(?:^|\\n)GRIP_SANDBOX_TOKEN=([^\\r\\n]+)/)?.[1];\n\t\tif (!token) throw new Error(`Lima instance ${name} started the daemon without returning an authentication token`);\n\t\treturn token;\n\t}\n\n\tprivate async prepareExistingInstance(\n\t\tconfig: SandboxConfig,\n\t\trecord: LimaListRecord,\n\t\tname: string,\n\t\tworkspaceRoot: string,\n\t): Promise<void> {\n\t\tconst mount = findWorkspaceMount(record, config.workspaceMountPath);\n\t\tif (!mount || resolve(mount.location ?? \"\") !== workspaceRoot || !mount.writable) {\n\t\t\tthrow new Error(\n\t\t\t\t`Lima instance ${name} must mount ${workspaceRoot} read-write at ${config.workspaceMountPath}`,\n\t\t\t);\n\t\t}\n\t\tconst status = record.status?.toLowerCase();\n\t\tif (status === \"running\") return;\n\t\tif (status !== \"stopped\") {\n\t\t\tthrow new Error(`Lima instance ${name} is in an uncertain state; refusing to resume`);\n\t\t}\n\t\tconst args = [\"start\", \"--tty=false\", name];\n\t\tensureLimaSuccessful(config.limaBinary, await this.runner.run(config.limaBinary, args), args);\n\t}\n\n\tprivate async findExactLegacyInstance(config: SandboxConfig, name?: string): Promise<LimaListRecord | undefined> {\n\t\tif (!name) return undefined;\n\t\tconst records = await this.listRecords(config, name);\n\t\tif (records.length === 0) return undefined;\n\t\tif (records.length !== 1 || records[0]?.name !== name) {\n\t\t\tthrow new Error(\"Lima instance lookup did not return one exact name; refusing to attach or create\");\n\t\t}\n\t\treturn records[0];\n\t}\n\n\tasync start(options: SandboxStartOptions): Promise<SandboxStartResult> {\n\t\tconst requestedName = options.name?.trim();\n\t\tif (!requestedName) {\n\t\t\tif (options.update) throw new Error(\"Sandbox update requires a named running shared VM\");\n\t\t\treturn this.startWithLegacyInstance(options);\n\t\t}\n\t\t// Coordinate VM resume/create as well as guest daemon launch with a named shared owner.\n\t\t// A stale lock is ambiguous, not permission to start an existing VM.\n\t\tconst owner = await acquireSharedLimaOwnerLock(requestedName);\n\t\ttry {\n\t\t\tconst config = this.resolveConfig(options);\n\t\t\tconst existing = await this.findExactLegacyInstance(config, requestedName);\n\t\t\tif (options.update && existing?.status?.toLowerCase() !== \"running\") {\n\t\t\t\tthrow new Error(\"Sandbox update requires an existing running shared VM\");\n\t\t\t}\n\t\t\tif (existing?.status?.toLowerCase() !== \"running\") {\n\t\t\t\treturn await this.startWithLegacyInstance(options, { record: existing });\n\t\t\t}\n\t\t\t// Select the guest mode before any legacy setup. An unreadable or ambiguous\n\t\t\t// marker is not permission to start a per-session daemon in this VM.\n\t\t\tconst probe = await this.runner.run(config.limaBinary, [\n\t\t\t\t\"shell\",\n\t\t\t\t\"--tty=false\",\n\t\t\t\trequestedName,\n\t\t\t\t\"sh\",\n\t\t\t\t\"-lc\",\n\t\t\t\t'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',\n\t\t\t]);\n\t\t\tif (probe.exitCode !== 0) throw new Error(\"Lima guest daemon mode is uncertain; refusing named attach\");\n\t\t\tif (probe.stdout.trim() === \"GRIP_LEGACY_GUEST\") {\n\t\t\t\tif (options.update)\n\t\t\t\t\tthrow new Error(\"Sandbox update requires a supervised shared guest; legacy guest is unchanged\");\n\t\t\t\treturn await this.startWithLegacyInstance(options, { record: existing });\n\t\t\t}\n\t\t\tif (probe.stdout.trim() !== \"GRIP_SHARED_GUEST\") {\n\t\t\t\tthrow new Error(\"Lima guest daemon mode is uncertain; refusing named attach\");\n\t\t\t}\n\t\t\tawait owner.assertHeld();\n\t\t\tconst mount = findWorkspaceMount(existing, config.workspaceMountPath);\n\t\t\tif (!mount?.location) throw new Error(\"Shared Lima instance mount is unavailable; refusing attach\");\n\t\t\tconst sharedOptions = {\n\t\t\t\tinstanceName: requestedName,\n\t\t\t\thostParent: resolve(mount.location),\n\t\t\t\thostRoot: resolve(options.workspaceRoot),\n\t\t\t};\n\t\t\tconst inspection = await this.inspectSharedInstance(sharedOptions);\n\t\t\tif (options.update) await this.updateSharedDaemon(sharedOptions, owner);\n\t\t\tconst token = await this.ensureSharedDaemon(sharedOptions, owner, options.update);\n\t\t\tconst daemonUrl = `grip+${inspection.daemonUrl}?root=${encodeURIComponent(inspection.guestRoot)}`;\n\t\t\treturn {\n\t\t\t\tcontainerId: requestedName,\n\t\t\t\tcontainerName: requestedName,\n\t\t\t\tworkspaceRoot: inspection.hostRoot,\n\t\t\t\tworkspaceMountPath: inspection.guestRoot,\n\t\t\t\tdaemonUrl,\n\t\t\t\tdaemonUrlRedacted: inspection.daemonUrl,\n\t\t\t\ttoken,\n\t\t\t\tlabels: { [`${SANDBOX_LABEL_PREFIX}.runtime`]: \"lima\", [`${SANDBOX_LABEL_PREFIX}.version`]: VERSION },\n\t\t\t};\n\t\t} finally {\n\t\t\tawait owner.release();\n\t\t}\n\t}\n\n\tprivate async startWithLegacyInstance(\n\t\toptions: SandboxStartOptions,\n\t\tknown?: KnownLimaRecord,\n\t): Promise<SandboxStartResult> {\n\t\tconst workspaceRoot = resolve(options.workspaceRoot);\n\t\tconst local = this.env.GRIP_SANDBOX_GUEST_BINARY\n\t\t\t? await inspectLocalGuestBinary(this.env.GRIP_SANDBOX_GUEST_BINARY)\n\t\t\t: undefined;\n\t\tconst config = this.resolveConfig(options);\n\t\tconst requestedName = options.name?.trim();\n\t\tconst managed = !requestedName;\n\t\tconst existing = known ? known.record : await this.findExactLegacyInstance(config, requestedName);\n\t\tif (existing) await this.prepareExistingInstance(config, existing, requestedName!, workspaceRoot);\n\t\tconst configuredForward = existing ? findDaemonForward(existing, config.daemonPort) : undefined;\n\t\tif (configuredForward && configuredForward.guestPort !== configuredForward.hostPort) {\n\t\t\tthrow new Error(\n\t\t\t\t`Lima instance ${requestedName} must forward the sandbox daemon on the same guest and host port`,\n\t\t\t);\n\t\t}\n\t\tconst daemonPort =\n\t\t\tconfiguredForward?.hostPort ?? (await this.portAllocator(config.daemonHostBind, config.daemonPort));\n\t\tconst name =\n\t\t\trequestedName ?? createInstanceName(config, workspaceRoot, options.sessionId ?? \"session\", daemonPort);\n\t\tconst effectiveConfig = { ...config, daemonPort };\n\t\tif (!existing) await this.startInstance(effectiveConfig, workspaceRoot, name, daemonPort);\n\t\t// A failed guest install/start can have changed state; never delete an uncertain VM.\n\t\tif (existing) await this.assertNoSharedGuestState(effectiveConfig, name);\n\t\tawait this.ensureGrip(effectiveConfig, name, !!existing, local);\n\t\tconst token = await this.startDaemon(effectiveConfig, name);\n\t\tawait this.readinessWaiter(config.daemonHostBind, daemonPort);\n\t\t// A failed readiness check is not proof that this daemon is ours to stop.\n\t\t// Leave uncertain guest state for explicit operator reconciliation.\n\t\tthis.managedInstances.set(name, { workspaceRoot, daemonPort, ownerId: options.sessionId, managed });\n\t\tconst daemonUrl = `ws://${urlHost(daemonConnectionHost(config.daemonHostBind))}:${daemonPort}${DAEMON_WEBSOCKET_PATH}`;\n\t\treturn {\n\t\t\tcontainerId: name,\n\t\t\tcontainerName: name,\n\t\t\tworkspaceRoot,\n\t\t\tworkspaceMountPath: config.workspaceMountPath,\n\t\t\tdaemonUrl,\n\t\t\tdaemonUrlRedacted: daemonUrl,\n\t\t\ttoken,\n\t\t\tlabels: { [`${SANDBOX_LABEL_PREFIX}.runtime`]: \"lima\", [`${SANDBOX_LABEL_PREFIX}.version`]: VERSION },\n\t\t};\n\t}\n\n\tasync list(options: SandboxListOptions): Promise<SandboxContainer[]> {\n\t\tconst config = this.resolveConfig();\n\t\tconst workspaceRoot = resolve(options.workspaceRoot);\n\t\treturn (await this.listRecords(config))\n\t\t\t.map((record) => toSandboxContainer(record, config.workspaceMountPath))\n\t\t\t.filter(\n\t\t\t\t(instance) => instance.workspaceRoot && (options.allWorkspaces || instance.workspaceRoot === workspaceRoot),\n\t\t\t);\n\t}\n\n\tprivate async stopDaemon(config: SandboxConfig, name: string, daemonPort: number): Promise<void> {\n\t\tconst pidFile = `.cache/grip/sandbox-daemon-${daemonPort}.pid`;\n\t\tconst script = `if test -f \"$HOME/${pidFile}\"; then pid=\"$(cat \"$HOME/${pidFile}\")\"; kill \"$pid\" 2>/dev/null || true; rm -f \"$HOME/${pidFile}\"; fi`;\n\t\tconst args = [\"shell\", \"--tty=false\", name, \"sh\", \"-lc\", script];\n\t\tensureLimaSuccessful(config.limaBinary, await this.runner.run(config.limaBinary, args), args);\n\t}\n\n\tasync stop(options: SandboxStopOptions): Promise<SandboxStopResult> {\n\t\tconst config = this.resolveConfig();\n\t\tconst instances = await this.list({ workspaceRoot: options.workspaceRoot });\n\t\tconst selected = selectLimaSandbox(instances, options.target ?? options.currentContainerId);\n\t\tif (!selected) return { status: \"not-found\", message: \"No matching Grip Lima sandbox found\" };\n\t\tif (instances.filter((instance) => instance.name === selected.name).length !== 1) {\n\t\t\tthrow new Error(\"Lima sandbox identity is ambiguous; refusing to stop a VM or daemon\");\n\t\t}\n\t\tconst tracked = this.managedInstances.get(selected.name);\n\t\t// Names and Lima's list output do not establish ownership. In particular, a\n\t\t// shared named VM can have the ordinary sandbox prefix without being ours.\n\t\t// Only a VM started by this session may be stopped or have its daemon touched.\n\t\tif (!tracked || tracked.workspaceRoot !== selected.workspaceRoot || tracked.daemonPort !== selected.daemonPort) {\n\t\t\tthrow new Error(\n\t\t\t\t\"Lima sandbox ownership is unknown or daemon port changed; refusing to stop an untracked VM or daemon\",\n\t\t\t);\n\t\t}\n\t\t// A shared guest must never lose its daemon or VM to a legacy\n\t\t// in-process record. Resolve the selected VM again under the host owner lock.\n\t\tconst owner = await acquireSharedLimaOwnerLock(selected.name);\n\t\ttry {\n\t\t\tconst matches = (await this.list({ workspaceRoot: options.workspaceRoot })).filter(\n\t\t\t\t(instance) => instance.name === selected.name,\n\t\t\t);\n\t\t\tconst current = matches[0];\n\t\t\tif (\n\t\t\t\tmatches.length !== 1 ||\n\t\t\t\t!current ||\n\t\t\t\tcurrent.id !== selected.id ||\n\t\t\t\tcurrent.state !== selected.state ||\n\t\t\t\tcurrent.workspaceRoot !== selected.workspaceRoot ||\n\t\t\t\tcurrent.daemonPort !== selected.daemonPort\n\t\t\t) {\n\t\t\t\tthrow new Error(\"Lima sandbox identity changed during stop; refusing to touch the VM or daemon\");\n\t\t\t}\n\t\t\treturn await this.stopTrackedInstance(config, current, tracked);\n\t\t} finally {\n\t\t\tawait owner.release();\n\t\t}\n\t}\n\n\tprivate async stopTrackedInstance(\n\t\tconfig: SandboxConfig,\n\t\tselected: SandboxContainer,\n\t\ttracked: ManagedLimaInstance,\n\t): Promise<SandboxStopResult> {\n\t\tconst ownsInstance = tracked.managed;\n\t\tif (selected.state !== \"running\") {\n\t\t\tif (ownsInstance && config.cleanup === \"remove\") {\n\t\t\t\t// An already stopped VM cannot be probed without resuming it. Its\n\t\t\t\t// in-memory record is not proof it was never repurposed externally.\n\t\t\t\tthrow new Error(\"Lima sandbox is stopped; refusing to delete an unverifiable VM; reconcile manually\");\n\t\t\t}\n\t\t\tthis.managedInstances.delete(selected.name);\n\t\t\treturn { status: \"already-stopped\", container: selected };\n\t\t}\n\t\tawait this.assertNoSharedGuestState(config, selected.name, \"stop\");\n\t\tawait this.stopDaemon(config, selected.name, tracked?.daemonPort ?? selected.daemonPort ?? config.daemonPort);\n\t\tif (ownsInstance) {\n\t\t\tconst args = config.cleanup === \"remove\" ? [\"delete\", \"--force\", selected.name] : [\"stop\", selected.name];\n\t\t\tensureLimaSuccessful(config.limaBinary, await this.runner.run(config.limaBinary, args), args);\n\t\t}\n\t\tthis.managedInstances.delete(selected.name);\n\t\treturn { status: ownsInstance && config.cleanup === \"remove\" ? \"removed\" : \"stopped\", container: selected };\n\t}\n\n\tasync stopManagedInstances(): Promise<void> {\n\t\tconst failures: unknown[] = [];\n\t\tfor (const [name, instance] of [...this.managedInstances]) {\n\t\t\ttry {\n\t\t\t\tawait this.stop({ workspaceRoot: instance.workspaceRoot, target: name, currentContainerId: name });\n\t\t\t} catch (error) {\n\t\t\t\tfailures.push(error);\n\t\t\t}\n\t\t}\n\t\tif (failures.length > 0) throw new AggregateError(failures, \"Failed to stop managed Lima sandbox instances\");\n\t}\n}\n"]}
|
|
@@ -436,7 +436,7 @@ export class LimaSandboxService {
|
|
|
436
436
|
];
|
|
437
437
|
ensureLimaSuccessful(config.limaBinary, await this.runner.run(config.limaBinary, args), args);
|
|
438
438
|
}
|
|
439
|
-
async ensureGrip(config, name,
|
|
439
|
+
async ensureGrip(config, name, existing, local) {
|
|
440
440
|
if (local) {
|
|
441
441
|
await installLocalGuestBinary(this.runner, config.limaBinary, name, local, VERSION);
|
|
442
442
|
return;
|
|
@@ -452,10 +452,10 @@ export class LimaSandboxService {
|
|
|
452
452
|
const check = await this.runner.run(config.limaBinary, checkArgs);
|
|
453
453
|
if (check.exitCode === 0)
|
|
454
454
|
return;
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
}
|
|
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;
|
|
455
|
+
// Existing named VMs can be bootstrapped too. Keep managed VMs pinned to
|
|
456
|
+
// the host release; use the latest published release for user-owned VMs.
|
|
457
|
+
const version = existing ? "" : `GRIP_VERSION=${shellQuote(`v${VERSION}`)} `;
|
|
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; ${version}sh "$tmp"; test -x "$HOME/.local/bin/grip"`;
|
|
459
459
|
const args = ["shell", "--tty=false", name, "sh", "-lc", script];
|
|
460
460
|
ensureLimaSuccessful(config.limaBinary, await this.runner.run(config.limaBinary, args), args);
|
|
461
461
|
}
|
|
@@ -612,7 +612,7 @@ export class LimaSandboxService {
|
|
|
612
612
|
// A failed guest install/start can have changed state; never delete an uncertain VM.
|
|
613
613
|
if (existing)
|
|
614
614
|
await this.assertNoSharedGuestState(effectiveConfig, name);
|
|
615
|
-
await this.ensureGrip(effectiveConfig, name,
|
|
615
|
+
await this.ensureGrip(effectiveConfig, name, !!existing, local);
|
|
616
616
|
const token = await this.startDaemon(effectiveConfig, name);
|
|
617
617
|
await this.readinessWaiter(config.daemonHostBind, daemonPort);
|
|
618
618
|
// A failed readiness check is not proof that this daemon is ours to stop.
|