loopat 0.1.19 → 0.1.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopat",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "Self-hosted AI workspace built around context management — works solo, scales to teams",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/simpx/loopat",
@@ -14,6 +14,7 @@
14
14
  },
15
15
  "files": [
16
16
  "bin/",
17
+ "scripts/install-sandbox-claude.mjs",
17
18
  "server/src/",
18
19
  "!server/src/serve-rs",
19
20
  "!server/src/port-proxy-rs",
@@ -34,6 +35,7 @@
34
35
  "build": "bun install && (cd web && bun run build)",
35
36
  "build:web": "cd web && bunx tsc -b && bunx vite build",
36
37
  "prepublishOnly": "bun install && bun run build:web",
38
+ "postinstall": "node scripts/install-sandbox-claude.mjs",
37
39
  "test:e2e": "playwright test",
38
40
  "test:e2e:ui": "playwright test --ui"
39
41
  },
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * postinstall: make sure a LINUX claude binary is available for the sandbox.
4
+ *
5
+ * loopat runs the AI inside a linux podman sandbox, so it needs the
6
+ * linux-<arch> claude binary. npm only installs the claude-agent-sdk platform
7
+ * binary matching the HOST (os/cpu filtered optionalDependencies) — so on a
8
+ * linux host we already have it (no-op here), but on macOS/Windows npm installs
9
+ * the darwin/win binary and the sandbox would hit "Exec format error".
10
+ *
11
+ * On a non-linux host we fetch the linux-<arch> binary into
12
+ * <loopat>/sandbox-claude (pinned to the SDK version we depend on) using npm's
13
+ * --os/--cpu override. Best-effort: a failure only means sandbox AI won't run
14
+ * on this host until fixed; the install itself still succeeds.
15
+ */
16
+ import { execFileSync } from "node:child_process"
17
+ import { existsSync, mkdirSync } from "node:fs"
18
+ import { dirname, join, resolve } from "node:path"
19
+ import { fileURLToPath } from "node:url"
20
+ import { createRequire } from "node:module"
21
+
22
+ if (process.platform === "linux") process.exit(0) // host claude IS the sandbox claude
23
+
24
+ const arch = process.arch // "arm64" | "x64"
25
+ const pkg = `@anthropic-ai/claude-agent-sdk-linux-${arch}`
26
+ const installDir = resolve(dirname(fileURLToPath(import.meta.url)), "..")
27
+ const dest = join(installDir, "sandbox-claude")
28
+ const binary = join(dest, "node_modules", pkg, "claude")
29
+
30
+ if (existsSync(binary)) process.exit(0) // already fetched
31
+
32
+ try {
33
+ const require = createRequire(import.meta.url)
34
+ let version = ""
35
+ try {
36
+ version = require("@anthropic-ai/claude-agent-sdk/package.json").version
37
+ } catch {}
38
+ const spec = version ? `${pkg}@${version}` : pkg
39
+ mkdirSync(dest, { recursive: true })
40
+ console.log(`[loopat] host is ${process.platform}/${arch}; fetching linux claude for the sandbox (${spec})…`)
41
+ execFileSync(
42
+ "npm",
43
+ ["install", "--prefix", dest, "--no-save", "--os=linux", `--cpu=${arch}`, spec],
44
+ { stdio: "inherit" },
45
+ )
46
+ if (existsSync(binary)) console.log(`[loopat] sandbox claude ready at ${binary}`)
47
+ else console.warn(`[loopat] sandbox claude install finished but ${binary} is missing`)
48
+ } catch (e) {
49
+ console.warn(`[loopat] could not fetch linux claude for the sandbox: ${e?.message ?? e}`)
50
+ console.warn(`[loopat] sandbox AI won't run on this host until fixed; everything else works.`)
51
+ }
@@ -6,7 +6,7 @@
6
6
  import { existsSync } from "node:fs"
7
7
  import { execFileSync } from "node:child_process"
8
8
  import { join } from "node:path"
9
- import { resolveClaudeBinary } from "./claude-binary"
9
+ import { resolveSandboxClaudeBinary } from "./claude-binary"
10
10
  import { configPath, loadKnowledgeConfig, type WorkspaceConfig, type KnowledgeConfig } from "./config"
11
11
  import {
12
12
  WORKSPACE,
@@ -68,14 +68,19 @@ function checkPodman(): Check {
68
68
  }
69
69
 
70
70
  function checkClaudeBinary(): Check {
71
+ // The AI runs in the linux sandbox, so what matters is the SANDBOX claude.
71
72
  try {
72
- const p = resolveClaudeBinary()
73
- return { ok: true, label: `claude binary (${p.split("/").slice(-3).join("/")})` }
74
- } catch (e: any) {
73
+ const p = resolveSandboxClaudeBinary()
74
+ const tag = process.platform === "linux" ? "" : " [linux, for sandbox]"
75
+ return { ok: true, label: `claude binary${tag} (${p.split("/").slice(-3).join("/")})` }
76
+ } catch {
75
77
  return {
76
78
  ok: false,
77
- label: "claude binary",
78
- hint: "run `bun install` in the loopat repo root — SDK ships the binary as a platform-specific package",
79
+ label: "claude binary (sandbox/linux)",
80
+ hint:
81
+ process.platform === "linux"
82
+ ? "run `bun install` in the loopat repo root — SDK ships the binary as a platform-specific package"
83
+ : "the linux claude for the sandbox wasn't fetched (postinstall). Reinstall loopat, or run the `npm install --os=linux ...` command from the resolve error",
79
84
  }
80
85
  }
81
86
  }
@@ -66,3 +66,22 @@ export function resolveClaudeBinary(): string {
66
66
  }
67
67
  throw new Error(`claude binary not found; tried:\n${candidates.join("\n")}`)
68
68
  }
69
+
70
+ /**
71
+ * The claude binary the SANDBOX runs (the AI executes inside a linux podman
72
+ * container). On a linux host that's just the host claude. On a non-linux host
73
+ * npm only installed the host (e.g. darwin) binary, so postinstall fetched the
74
+ * linux-<arch> one into <loopat>/sandbox-claude — bind THAT into the sandbox,
75
+ * not the host binary (otherwise: "Exec format error").
76
+ */
77
+ export function resolveSandboxClaudeBinary(): string {
78
+ if (process.platform === "linux") return resolveClaudeBinary()
79
+ const arch = process.arch
80
+ const installDir = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..")
81
+ const candidate = join(installDir, "sandbox-claude", "node_modules", "@anthropic-ai", `claude-agent-sdk-linux-${arch}`, "claude")
82
+ if (existsSync(candidate)) return candidate
83
+ throw new Error(
84
+ `sandbox (linux) claude not found at ${candidate}; postinstall may have failed. Fix: ` +
85
+ `npm install --prefix "${join(installDir, "sandbox-claude")}" --no-save --os=linux --cpu=${arch} @anthropic-ai/claude-agent-sdk-linux-${arch}`,
86
+ )
87
+ }
@@ -60,7 +60,7 @@ import {
60
60
  import { loadConfig } from "./config"
61
61
  import { DEFAULT_VAULT, listVaultHomeMounts } from "./vaults"
62
62
  import { hostExecDir, writeHostShims } from "./host-exec"
63
- import { resolveClaudeBinary } from "./claude-binary"
63
+ import { resolveSandboxClaudeBinary } from "./claude-binary"
64
64
  import { parse as tomlParse, stringify as tomlStringify } from "smol-toml"
65
65
 
66
66
  const execFileP = promisify(execFile)
@@ -221,7 +221,7 @@ export async function buildVolumeMounts(opts: ContainerOptions): Promise<VolumeM
221
221
  // sandbox exec's it by its host path, so bind that path in (ro) when it isn't
222
222
  // already covered by the install-dir mount — otherwise the AI is code 127.
223
223
  try {
224
- const claudeDir = dirname(resolveClaudeBinary())
224
+ const claudeDir = dirname(resolveSandboxClaudeBinary())
225
225
  if (existsSync(claudeDir) && !claudeDir.startsWith(LOOPAT_INSTALL_DIR)) {
226
226
  mounts.push({ src: claudeDir, dst: claudeDir, ro: true })
227
227
  }
@@ -5,7 +5,7 @@ import { createWriteStream, mkdirSync, existsSync } from "node:fs"
5
5
  import { randomUUID } from "node:crypto"
6
6
  import { join } from "node:path"
7
7
  import { loopClaudeDir, loopDir, loopHistoryPath, personalSkillsDir, workspaceTeamSkillsDir } from "./paths"
8
- import { resolveClaudeBinary } from "./claude-binary"
8
+ import { resolveSandboxClaudeBinary } from "./claude-binary"
9
9
  import { loadConfig, loadPersonalConfig, parseDefault, type ProviderConfig } from "./config"
10
10
  import { buildLoopatAppend } from "./system-prompt"
11
11
  import { composeLoopClaudeConfig, writeLoopSettings } from "./compose"
@@ -21,8 +21,11 @@ import { updateLoopStatus } from "./loop-status"
21
21
  // Resolved lazily — each spawn re-reads the env var so the full-suite test
22
22
  // run, where module load order isn't guaranteed, sees the test's override
23
23
  // even if session.ts was imported earlier with the env var unset.
24
+ // The AI runs inside the linux sandbox, so it needs the linux claude binary
25
+ // (resolveSandboxClaudeBinary) — on a linux host that's the host claude; on a
26
+ // darwin/win host it's the linux binary postinstall fetched into sandbox-claude.
24
27
  function getClaudeBinary(): string {
25
- return process.env.LOOPAT_CLAUDE_BIN || resolveClaudeBinary()
28
+ return process.env.LOOPAT_CLAUDE_BIN || resolveSandboxClaudeBinary()
26
29
  }
27
30
  const DEBUG = !!process.env.LOOPAT_DEBUG || !!process.env.LOOPAT_DEBUG_SPAWN
28
31