loopat 0.1.49 → 0.1.50
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 +1 -1
- package/server/src/loops.ts +7 -1
- package/server/src/podman.ts +46 -2
- package/web/dist/assets/{CodeEditor-JV36Z3V5.js → CodeEditor-DtHZtsPs.js} +8 -8
- package/web/dist/assets/Editor-C7JCzVsf.js +1 -0
- package/web/dist/assets/Markdown-DPZuNxt-.js +5 -0
- package/web/dist/assets/{MilkdownEditor-t_CG4MJj.js → MilkdownEditor-D2-3eNpY.js} +9 -9
- package/web/dist/assets/{Terminal-Vi3Ufhgi.js → Terminal-trnCVajY.js} +2 -2
- package/web/dist/assets/index-CRS7bmLR.js +162 -0
- package/web/dist/assets/{jsx-runtime-DAYmCNe8.js → jsx-runtime-Bt-cYkS5.js} +1 -1
- package/web/dist/index.html +2 -3
- package/web/dist/assets/Editor-BnfYQQzs.js +0 -1
- package/web/dist/assets/Markdown-D0eu-WNR.js +0 -5
- package/web/dist/assets/index-CoiEN2FX.js +0 -145
- package/web/dist/assets/lib-B8L80SIn.js +0 -18
- /package/web/dist/assets/{w3c-keyname-DXh_HxYD.js → w3c-keyname-BOAvb0qz.js} +0 -0
package/package.json
CHANGED
package/server/src/loops.ts
CHANGED
|
@@ -312,7 +312,13 @@ async function ensureContextRepo(dir: string, name: string, url?: string): Promi
|
|
|
312
312
|
console.log(`[loopat] cloned ${url} → ${dir}`)
|
|
313
313
|
return
|
|
314
314
|
} catch (e: any) {
|
|
315
|
-
|
|
315
|
+
// The WORKSPACE-DEFAULT context clone uses the host's bare ssh — it has
|
|
316
|
+
// no per-user vault key, so a private ssh:// URL here is EXPECTED to fail
|
|
317
|
+
// `Permission denied (publickey)`. This is a bootstrap display mirror only
|
|
318
|
+
// (loops use the per-user path with the vault key, see ensureUserContext),
|
|
319
|
+
// so we fall back to a local origin. Log at info, and DON'T echo the raw
|
|
320
|
+
// "Permission denied (publickey)" — it gets mistaken for a loop-auth bug.
|
|
321
|
+
console.log(`[loopat] workspace-default ${name} not cloned over ssh (no host credential for ${url}) — using local origin (loops use the per-user vault key, unaffected)`)
|
|
316
322
|
}
|
|
317
323
|
}
|
|
318
324
|
// Local backend: loopat-hosted bare origin.
|
package/server/src/podman.ts
CHANGED
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
*/
|
|
36
36
|
import { execFile, spawn } from "node:child_process"
|
|
37
37
|
import { createHash } from "node:crypto"
|
|
38
|
-
import { existsSync } from "node:fs"
|
|
39
|
-
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
|
|
38
|
+
import { existsSync, readdirSync, statSync } from "node:fs"
|
|
39
|
+
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
|
|
40
40
|
import { homedir, tmpdir } from "node:os"
|
|
41
41
|
import { join, dirname } from "node:path"
|
|
42
42
|
import { promisify } from "node:util"
|
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
loopHomeUpper,
|
|
58
58
|
workspaceHomeSkelDir,
|
|
59
59
|
loopDir,
|
|
60
|
+
personalVaultMountsHomeDir,
|
|
60
61
|
} from "./paths"
|
|
61
62
|
import { loadConfig } from "./config"
|
|
62
63
|
import { DEFAULT_VAULT, listVaultHomeMounts } from "./vaults"
|
|
@@ -1147,6 +1148,45 @@ export async function ensurePortProxyContainer(): Promise<void> {
|
|
|
1147
1148
|
const SERVE_HOST = process.env.LOOPAT_SERVE_HOST ?? "127.0.0.1"
|
|
1148
1149
|
const SERVE_PORT = Number(process.env.LOOPAT_SERVE_PORT ?? 7788)
|
|
1149
1150
|
|
|
1151
|
+
/**
|
|
1152
|
+
* Lock down the vault `.ssh` perms RIGHT BEFORE the container that bind-mounts
|
|
1153
|
+
* it starts. The vault's `.ssh` is git-crypt-decrypted / git-checked-out, so its
|
|
1154
|
+
* private key lands at the umask default (0664) and the dir at 0775 — and git
|
|
1155
|
+
* can't carry 0600 (it only tracks the exec bit). loops.ts chmods the key 0600
|
|
1156
|
+
* "at point of use" host-side, but that only runs when a HOST git op goes through
|
|
1157
|
+
* sshCommandForUser; a container that's created/recreated on a path that didn't
|
|
1158
|
+
* (server restart → first attach, config/image-drift recreate) would bind-mount
|
|
1159
|
+
* a stale-perms key. ssh then ignores the key (or rejects it under StrictModes)
|
|
1160
|
+
* and the FIRST sandbox ssh fails `Permission denied (publickey)`; a later op
|
|
1161
|
+
* that re-chmods host-side makes the retry succeed — exactly the intermittent
|
|
1162
|
+
* we saw. Do it here, in the one chokepoint every container start passes
|
|
1163
|
+
* through, so the perms are correct the instant the bind goes live. Cheap +
|
|
1164
|
+
* idempotent; best-effort (a missing vault is fine — the mount just won't exist).
|
|
1165
|
+
*/
|
|
1166
|
+
export async function ensureSandboxSshPerms(user: string, vault: string): Promise<void> {
|
|
1167
|
+
const sshDir = join(personalVaultMountsHomeDir(user, vault), ".ssh")
|
|
1168
|
+
if (!existsSync(sshDir)) return
|
|
1169
|
+
try {
|
|
1170
|
+
await chmod(sshDir, 0o700).catch(() => {})
|
|
1171
|
+
for (const name of readdirSync(sshDir)) {
|
|
1172
|
+
const p = join(sshDir, name)
|
|
1173
|
+
try {
|
|
1174
|
+
if (!statSync(p).isFile()) continue
|
|
1175
|
+
} catch { continue }
|
|
1176
|
+
// Private keys MUST be 0600 (id_*, *_rsa/_ed25519/_ecdsa with no .pub) and
|
|
1177
|
+
// `config` MUST NOT be group/world-writable or ssh silently ignores it.
|
|
1178
|
+
// .pub / known_hosts are fine readable; clamp them to 0644 to be safe.
|
|
1179
|
+
const isPub = name.endsWith(".pub")
|
|
1180
|
+
const isPrivKey = !isPub && /^id_|_rsa$|_ed25519$|_ecdsa$|_dsa$|^identity$/.test(name)
|
|
1181
|
+
if (name === "config" || isPrivKey) {
|
|
1182
|
+
await chmod(p, 0o600).catch(() => {})
|
|
1183
|
+
} else {
|
|
1184
|
+
await chmod(p, 0o644).catch(() => {})
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
} catch {}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1150
1190
|
/**
|
|
1151
1191
|
* Idempotent: bring the container to "running with current config".
|
|
1152
1192
|
* - missing → podman create + start
|
|
@@ -1157,6 +1197,10 @@ const SERVE_PORT = Number(process.env.LOOPAT_SERVE_PORT ?? 7788)
|
|
|
1157
1197
|
*/
|
|
1158
1198
|
export async function ensureContainer(opts: ContainerOptions, progress?: { onProgress?: (msg: string) => void }): Promise<void> {
|
|
1159
1199
|
await ensureLoopatNetwork()
|
|
1200
|
+
// Deterministically fix the vault ssh-key perms BEFORE the container that
|
|
1201
|
+
// bind-mounts them starts, so the first in-sandbox ssh authenticates reliably
|
|
1202
|
+
// regardless of how/when the key was git-checked-out (see ensureSandboxSshPerms).
|
|
1203
|
+
await ensureSandboxSshPerms(opts.createdBy, opts.vaultName?.trim() || DEFAULT_VAULT)
|
|
1160
1204
|
// Resolve the image first — for loops with a composed mise.toml this
|
|
1161
1205
|
// builds (or reuses) a per-loop child image with toolchains baked in.
|
|
1162
1206
|
// For loops without mise.toml, this returns the base SANDBOX_IMAGE.
|