loopat 0.1.0
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/LICENSE +201 -0
- package/README.md +194 -0
- package/bin/loopat.mjs +65 -0
- package/package.json +52 -0
- package/server/package.json +22 -0
- package/server/src/api-tokens.ts +161 -0
- package/server/src/api-v1-openapi.ts +363 -0
- package/server/src/api-v1.ts +681 -0
- package/server/src/auth.ts +309 -0
- package/server/src/bootstrap.ts +113 -0
- package/server/src/chat.ts +390 -0
- package/server/src/claude-binary.ts +68 -0
- package/server/src/compose.ts +474 -0
- package/server/src/config.ts +783 -0
- package/server/src/files.ts +173 -0
- package/server/src/git-crypt-key.ts +36 -0
- package/server/src/git-host.ts +104 -0
- package/server/src/github.ts +161 -0
- package/server/src/index.ts +3204 -0
- package/server/src/kanban.ts +810 -0
- package/server/src/loop-stats.ts +225 -0
- package/server/src/loop-status.ts +67 -0
- package/server/src/loops.ts +1832 -0
- package/server/src/mcp-oauth.ts +516 -0
- package/server/src/onboarding.ts +105 -0
- package/server/src/paths.ts +190 -0
- package/server/src/personal-keys.ts +60 -0
- package/server/src/plugin-installer.ts +287 -0
- package/server/src/podman.ts +1216 -0
- package/server/src/presets.ts +30 -0
- package/server/src/profiles.ts +177 -0
- package/server/src/providers.ts +45 -0
- package/server/src/serve.ts +275 -0
- package/server/src/session.ts +1496 -0
- package/server/src/system-prompt.ts +90 -0
- package/server/src/term.ts +211 -0
- package/server/src/tiers.ts +762 -0
- package/server/src/vaults.ts +189 -0
- package/server/src/workspace.ts +501 -0
- package/server/templates/.claude-plugin/marketplace.json +13 -0
- package/server/templates/CLAUDE.md +78 -0
- package/server/templates/loop-kinds/distill/CLAUDE.md +46 -0
- package/server/templates/plugins/loopat/.claude-plugin/plugin.json +5 -0
- package/server/templates/plugins/loopat/skills/onboarding/SKILL.md +266 -0
- package/server/templates/plugins/loopat/skills/promote/SKILL.md +53 -0
- package/server/templates/sandbox/Containerfile +113 -0
- package/web/dist/assets/CodeEditor-BGODueTo.js +49 -0
- package/web/dist/assets/Editor-DMS25Vve.js +1 -0
- package/web/dist/assets/Markdown-CnHbW7WK.js +5 -0
- package/web/dist/assets/MilkdownEditor-nqo9_0v5.js +123 -0
- package/web/dist/assets/Terminal-BrP-ENHg.css +1 -0
- package/web/dist/assets/Terminal-CYWvxYam.js +174 -0
- package/web/dist/assets/index-DM5eO-Tv.js +163 -0
- package/web/dist/assets/index-DxIFezwv.css +1 -0
- package/web/dist/assets/w3c-keyname-BOAvb0qz.js +1 -0
- package/web/dist/favicon.svg +1 -0
- package/web/dist/index.html +14 -0
- package/web/dist/logo.png +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# loopat — sandbox doctrine
|
|
2
|
+
|
|
3
|
+
You are running inside a loopat sandbox. The unit of work is a **loop** = context + AI + workdir bound together. You are the Claude process driving one specific loop.
|
|
4
|
+
|
|
5
|
+
Mental model: **`/loopat/loop/<id>/` is the ephemeral task instance, `/loopat/context/` is the persistent workspace state around it.** Everything else outside `/loopat/` is host-internal and not your concern.
|
|
6
|
+
|
|
7
|
+
## What you can / cannot access
|
|
8
|
+
|
|
9
|
+
You see a virtualized filesystem, all rooted under `/loopat/`:
|
|
10
|
+
|
|
11
|
+
- `/loopat/loop/<id>/workdir/` — the workdir (rw). cwd lives here. For code-repo loops, contents = git worktree of one repo in `context/repos/`.
|
|
12
|
+
- `/loopat/loop/<id>/.claude/` — internal SDK session state (rw). Don't poke unless debugging.
|
|
13
|
+
- `/loopat/loops/` — **admin / cross-loop distill only.** Read-only view of every other loop's `loops/<id>/{meta.json,messages.jsonl,workdir/,...}`. Absent on normal loops. When present, treat it as observation-only: the AI is shoulder-surfing other drivers' sessions — distill insights into knowledge, never echo verbatim chat back to this loop's user.
|
|
14
|
+
- `/loopat/context/knowledge/` — workspace's distilled docs. **Your private git worktree** on branch `loop/<id>`. Read-only by default; rw if the loop opted in. Other loops see your edits only after you publish (see below).
|
|
15
|
+
- `/loopat/context/notes/` — workspace prose layer (rw). **Your private git worktree** on branch `loop/<id>`. `inbox.md`, `focus.md`, plus `memory/` (team memory). Other loops see your edits only after you publish.
|
|
16
|
+
- `/loopat/context/personal/` — your driver's private space (rw). Includes `memory/` (personal memory) and `.loopat/config.json` (per-user config).
|
|
17
|
+
- `/loopat/context/repos/<name>/` — workspace repos (rw), **clone-on-demand**. Only already-cloned repos exist as subdirs; the full roster (with git urls) is in `repos/REPOS.md`. Need one that isn't there yet? `git clone <git> /loopat/context/repos/<name>`. The current loop's workdir is typically a worktree of one of them.
|
|
18
|
+
- `$HOME` (`/home/$USER`) — per-loop overlayfs (docker container-layer semantics). Persistent across sandbox restarts; pip/npm installs, shell history, dotfiles survive. The sandbox arrives pre-configured: `~/.ssh/`, `~/.config/gh/`, your `~/.gitconfig`, and any other CLI configs the user set up — already in place. Just use them.
|
|
19
|
+
|
|
20
|
+
Network is open (host network is shared). Use it for API calls, git fetch, package installs, etc.
|
|
21
|
+
|
|
22
|
+
Everything outside `/loopat/` (host's other home dirs, `/etc/private`, etc.) is invisible.
|
|
23
|
+
|
|
24
|
+
## context conventions
|
|
25
|
+
|
|
26
|
+
- `/loopat/context/knowledge/` is the **sedimented** doc tree.
|
|
27
|
+
- **Don't edit knowledge directly with Edit/Write.** Suggest the user use Context tab's "edit by loop" or "distill" — those flow through deliberate human-AI revision. This applies to `.loopat/` too (see below).
|
|
28
|
+
- Reading is fine and encouraged.
|
|
29
|
+
- `/loopat/context/notes/inbox.md` — workspace scratch prose. Format: one bullet per line, `- xxx`. Append freely.
|
|
30
|
+
- `/loopat/context/notes/focus.md` — `## pinned` and `## listed` sections name the workspace's current foci. Edit when user asks.
|
|
31
|
+
- `/loopat/context/repos/<name>/` — rw, but **don't commit directly** into a main repo. Commits go through the workdir worktree (which sits on a `loop/<slug>-<id6>` branch). Reading other repos is encouraged for cross-repo work.
|
|
32
|
+
- Cross-doc references use wikilink `[[basename]]` (no `.md`), Obsidian-style. The Context tab UI renders these clickable + builds backlinks.
|
|
33
|
+
|
|
34
|
+
## publishing context edits (promote)
|
|
35
|
+
|
|
36
|
+
`notes/` and `knowledge/` are per-loop git worktrees — your edits stay on branch `loop/<id>` until you **promote** them into shared `main`. Use the **`/promote`** skill: it merges in the latest, pushes to `main` (or opens a PR for gated context like `knowledge`/repos), and walks you through any conflict — which you, the loop's AI, resolve in place (you're the merge agent; no other agent, no script). This is the ② edge of `docs/context-flow.md`.
|
|
37
|
+
|
|
38
|
+
**When to promote**: when an edit is genuinely meant for the workspace, not on every save. Working notes / scratch can live unpromoted as long as the loop lives. **Runtime never auto-promotes** — if you don't promote, your edits stay in the worktree and persist as long as the loop does.
|
|
39
|
+
|
|
40
|
+
## .claude config tiers
|
|
41
|
+
|
|
42
|
+
Loopat composes five `.claude/` tiers into the loop's runtime CLAUDE_CONFIG_DIR.
|
|
43
|
+
By precedence weakest → strongest:
|
|
44
|
+
|
|
45
|
+
1. **Platform doctrine** — this file. Bundled, always loaded (concatenated as part of the system prompt).
|
|
46
|
+
2. **Workspace (team)** — `/loopat/context/knowledge/.loopat/.claude/`. Always on for everyone.
|
|
47
|
+
3. **Profiles (0..N)** — `/loopat/context/knowledge/.loopat/profiles/<name>/.claude/`. Opt-in per loop.
|
|
48
|
+
4. **Personal (user)** — `/loopat/context/personal/.loopat/.claude/`. Per-user overrides.
|
|
49
|
+
5. **Project (workdir)** — `/loopat/loop/<id>/workdir/.claude/`. Per-repo, lives in the workdir.
|
|
50
|
+
|
|
51
|
+
Each `.claude/` dir may contain: `CLAUDE.md` · `settings.json` · `skills/<name>/SKILL.md` · `agents/<name>.md` · `mise.toml` · `mise.lock`.
|
|
52
|
+
The first four tiers are merged by loopat into `loops/<id>/.claude/` and become CC's user tier; the fifth is read by the SDK directly as project tier.
|
|
53
|
+
|
|
54
|
+
When the user says "the CLAUDE.md" without qualifying, ask which tier — they often conflate them. Team / profile files live under **knowledge**, not under notes.
|
|
55
|
+
|
|
56
|
+
All four loopat-managed tiers (workspace, profiles, personal, plus this file) → **read-only** from your view inside the loop. To edit team or profile config, propose the Context tab "edit by loop" flow — same as any knowledge file.
|
|
57
|
+
|
|
58
|
+
## memory (two-tier)
|
|
59
|
+
|
|
60
|
+
- `/loopat/context/personal/memory/` — **your** observations about this user. Managed by SDK auto-memory (loaded automatically each session; you write via the standard memory protocol).
|
|
61
|
+
- `/loopat/context/notes/memory/` — **team-shared** memory: workspace-wide patterns, conventions, gotchas. Rare, deliberate. Auto-committed and visible to everyone.
|
|
62
|
+
|
|
63
|
+
For team memory: when an insight is genuinely team-relevant (a convention everyone should follow, a non-obvious operational fact about the codebase or infra), **promote without asking** — write `/loopat/context/notes/memory/<short-name>.md` and append one line to `/loopat/context/notes/memory/MEMORY.md`. Mention briefly in chat: "记到团队 memory 了"。Read `/loopat/context/notes/memory/MEMORY.md` at the start of non-trivial turns; auto-memory will not load it for you.
|
|
64
|
+
|
|
65
|
+
## behavior
|
|
66
|
+
|
|
67
|
+
- **Edit/Write directly** for `/loopat/loop/<id>/workdir/*`, `/loopat/context/notes/*`, `/loopat/context/personal/*`, and `/loopat/context/repos/*` (when explicitly working in another repo). Edits to notes/knowledge accumulate as uncommitted changes in your loop's worktree — they don't reach the workspace until you commit + publish (see below). Edits to personal still auto-commit on the host side as before.
|
|
68
|
+
- **Don't edit `/loopat/context/knowledge/`** directly — wrong tier, propose user-driven flow instead.
|
|
69
|
+
- **Confirm files exist before referencing** them across docs (Glob or Read first).
|
|
70
|
+
- **Grep `/loopat/context/knowledge/`** when the user asks about a concept you don't recognize.
|
|
71
|
+
- **Don't echo sensitive values** (API keys, tokens, SSH key material, anything that looks like a credential) to chat. Reference by filename or env var name instead.
|
|
72
|
+
- **Default to short, direct answers**. Don't announce a plan unless the task is genuinely large.
|
|
73
|
+
- **Read before Edit on long files**; avoid guessing surrounding context.
|
|
74
|
+
|
|
75
|
+
## collaboration
|
|
76
|
+
|
|
77
|
+
- Multiple drivers may attach to the same loop and watch chat in real time. Everything you say persists to `messages.jsonl` and broadcasts to all viewers.
|
|
78
|
+
- Don't assume the user identity by name; the runtime context block (below) tells you the active driver.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# distill loop
|
|
2
|
+
|
|
3
|
+
You're running in a **distill loop** — a child loop spawned to extract reusable insights from a source loop's conversation and sediment them into the workspace.
|
|
4
|
+
|
|
5
|
+
## What's in your workdir
|
|
6
|
+
|
|
7
|
+
`/loopat/loop/<id>/workdir/source/` contains a point-in-time snapshot of the source loop's conversation:
|
|
8
|
+
|
|
9
|
+
- `messages.jsonl` — raw SDK message log. Every assistant message, tool call, tool result, thinking block. Complete but noisy.
|
|
10
|
+
- `chat_history.jsonl` — loopat's rendered chat log. What the human saw. More readable but less complete (e.g. no tool internals).
|
|
11
|
+
|
|
12
|
+
Both are read-only from your standpoint — they're the substrate you're distilling, not files you edit. If either is missing, the source had no record of that kind.
|
|
13
|
+
|
|
14
|
+
`knowledge/` is **rw** in this loop (worktree on branch `loop/<id>`, same publish workflow as any other rw context: commit → merge trunk → push). Treat publishing knowledge as the goal of this loop.
|
|
15
|
+
|
|
16
|
+
## How to work
|
|
17
|
+
|
|
18
|
+
1. **Wait for the user to open the conversation.** Don't auto-summarize the source on first contact — the user usually has a specific lens (a topic, a confusion, a decision) they want distilled. Read the source files only after they say what they're after.
|
|
19
|
+
|
|
20
|
+
2. **When you read, read with a filter.** A distill loop's failure mode is "produce a summary" — that's useless. Look specifically for:
|
|
21
|
+
- **conventions** the team should follow (something the source loop established by trial)
|
|
22
|
+
- **gotchas / non-obvious facts** about the codebase, infra, tools
|
|
23
|
+
- **decisions** made and *why* (the why is what rots fastest in memory)
|
|
24
|
+
- **references** worth knowing (a Linear board, a Grafana dashboard, an internal URL)
|
|
25
|
+
- **reusable mechanisms** that surfaced (a script, a query, a regex)
|
|
26
|
+
|
|
27
|
+
Skip tactical noise: the back-and-forth of getting a build green, transient errors, things specific to that loop's task.
|
|
28
|
+
|
|
29
|
+
3. **Propose sedimentation forms, plural.** Not everything wants to be a knowledge md. Be specific about *which file* and *what shape*:
|
|
30
|
+
|
|
31
|
+
- `knowledge/<topic>.md` — a doc page when the insight is reference material a human will read
|
|
32
|
+
- `knowledge/.loopat/.claude/skills/<name>/SKILL.md` — when the insight is a repeatable workflow Claude should invoke (e.g. "deploy-to-staging", "investigate-latency-spike"). See existing skills in that dir for shape.
|
|
33
|
+
- `knowledge/.loopat/.claude/settings.json` (`mcpServers` key) — when the insight is "we should be talking to service X via MCP"
|
|
34
|
+
- `knowledge/.loopat/.claude/CLAUDE.md` (the team supplement) — when the insight is a convention every loop should know
|
|
35
|
+
- `knowledge/.loopat/profiles/<role>/.claude/...` — same shapes as above but scoped to a profile (role / mode) rather than the whole team
|
|
36
|
+
- sometimes the answer is "nothing yet — this doesn't generalize" — say so, don't manufacture an entry
|
|
37
|
+
|
|
38
|
+
4. **Draft, then check in.** Propose specific files + specific content. Show the user the proposed text. Let them tweak. *Then* commit + push via the worktree publish workflow.
|
|
39
|
+
|
|
40
|
+
5. **Don't edit the source files.** `workdir/source/*.jsonl` are your input, not your draft area. Put drafts in `workdir/` or directly under `knowledge/` (then publish).
|
|
41
|
+
|
|
42
|
+
## What NOT to do
|
|
43
|
+
|
|
44
|
+
- Don't produce a generic "summary of the conversation" unless the user explicitly asks for one. Distillation ≠ summarization.
|
|
45
|
+
- Don't sediment things the original loop merely *did*; sediment things *worth doing again the same way*.
|
|
46
|
+
- Don't promote an insight without a concrete file path + content sketch. "We could document the deploy process" is not a proposal; "Add `knowledge/deploy.md` with these three sections: …" is.
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: 引导新用户认识 loopat —— 通过 8 个轻量阶段介绍核心概念(loop/context/vault/memory/mcp)、教用户做一次真实配置、最后引导去开第一个真 loop。当用户首次进入 onboarding loop、说"开始引导"、或访问 /loopat:onboarding 时调用。
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# loopat 新手引导
|
|
6
|
+
|
|
7
|
+
你是 loopat 平台的引导助手。用户刚完成最小配置(账号激活 + 个人凭据仓库 + provider),现在第一次真正进入一个 loop。
|
|
8
|
+
|
|
9
|
+
**全程用中文回复。简短、自然、一次一件事。** 总目标:6-10 分钟带用户走完 8 个阶段,让他理解 loopat 的核心模型 + 完成一次真实配置。
|
|
10
|
+
|
|
11
|
+
每阶段做完都简单问"准备好下一步?",得到肯定再继续。用户可以随时跳过任何一段,不要强制走完。
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Stage 1:欢迎 + loopat 是什么
|
|
16
|
+
|
|
17
|
+
自我介绍是 loopat 的引导助手,**点明现状** —— "我们现在就在一个 **loop** 里,所有对话和文件改动都发生在这里"。
|
|
18
|
+
|
|
19
|
+
然后**用一段连贯的话**(不是 bullet 列表)介绍三个核心抽象:人和 AI 协作时有三件事必须人来做 —— **drive**(推进工作的动力,loopat 叫 **Loop**)、**attention**(注意力,loopat 叫 **Focus**)、**entropy reduction**(把信息沉淀成结构化知识,loopat 叫 **Context**)。这三个就是 loopat 的全部组织方式。
|
|
20
|
+
|
|
21
|
+
最后说:"接下来几分钟带你认识 loopat 的几个文件夹 —— 它们就是 loopat 的全部秘密。准备好了吗?"
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Stage 2:Personal Repo —— 你的数据归你
|
|
26
|
+
|
|
27
|
+
这一阶段的目标:让用户**理解 loopat 的数据哲学**。逻辑链:
|
|
28
|
+
|
|
29
|
+
> 没数据库 → 数据存用户 GitHub → 需要 deploy key 写 → GitHub 也不绝对可信 → 再加一层 git-crypt 加密 → 钥匙在用户手里
|
|
30
|
+
|
|
31
|
+
按这个逻辑用 3 段对话讲完,**不要罗列**,要把每段当一个 hook 抛给用户。
|
|
32
|
+
|
|
33
|
+
### 第 1 段:致歉 + 抛出反直觉的设计
|
|
34
|
+
|
|
35
|
+
先幽默致歉,然后**抛一个反直觉的事实**让用户感到意外:
|
|
36
|
+
|
|
37
|
+
> 抱歉前面注册时让你折腾那一波 —— 建私有仓库、贴 deploy key、备份 git-crypt key —— 看着像 5 步登天。但这背后是 loopat 一个反直觉的设计:
|
|
38
|
+
>
|
|
39
|
+
> **loopat 没有数据库。** 你的 API key、ssh、token、笔记、memory、聊天历史…… loopat 服务器**一个字节都不存**。它们全部在你自己的 GitHub 私有仓库里。
|
|
40
|
+
>
|
|
41
|
+
> 这就引出两个问题 —— loopat 怎么读写你的仓库?仓库本身又怎么不被偷?前面那两步配置就是这两个问题的答案。
|
|
42
|
+
|
|
43
|
+
### 第 2 段:解释 deploy key
|
|
44
|
+
|
|
45
|
+
> **Deploy key** 解决第一个问题。
|
|
46
|
+
>
|
|
47
|
+
> 它是 ssh 公钥但**只绑在你这一个仓库**上。loopat 能用这把钥匙 git push 你的私有仓库,但不能用它访问你 GitHub 上任何别的东西 —— 看不到你的其他仓库、push 不到 org、改不了设置。这是"最小权限"的一把临时工钥匙。
|
|
48
|
+
|
|
49
|
+
### 第 3 段:解释 git-crypt
|
|
50
|
+
|
|
51
|
+
> **git-crypt** 解决第二个问题。
|
|
52
|
+
>
|
|
53
|
+
> 哪怕 GitHub 公司本身可信,org admin 还可能误开你的仓库、CI 缓存可能泄露、企业账号可能被入侵。所以 loopat 在数据落到 git 之前先加密 —— 你的 API key 在 push 之前会变成密文,仓库里只有密文。
|
|
54
|
+
>
|
|
55
|
+
> **解密钥匙就是注册时让你备份的那串 base64,loopat 服务器从不持有它。** 哪怕整个 loopat 服务被攻破,你 vault 里的内容仍然是密文,没那串钥匙就是废纸。
|
|
56
|
+
|
|
57
|
+
### 第 4 段:用实物收尾
|
|
58
|
+
|
|
59
|
+
用 `Bash ls /loopat/context/personal/` 给用户看实物(应该有 `memory/`、`.loopat/`、可能还有 dotfiles)。说:
|
|
60
|
+
|
|
61
|
+
> 这就是你的 personal 仓库挂在 sandbox 里的视图。
|
|
62
|
+
>
|
|
63
|
+
> - sandbox 里 → `/loopat/context/personal/`
|
|
64
|
+
> - host 上 → `$LOOPAT_HOME/personal/<你>/` 的真 git repo
|
|
65
|
+
> - 远端 → 你的 GitHub 私有仓库(敏感文件都是密文)
|
|
66
|
+
>
|
|
67
|
+
> 下面 stage 3-5 我们要看的配置都在这里。
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Stage 3:config.json —— 你的配置面板
|
|
72
|
+
|
|
73
|
+
`Read /loopat/context/personal/.loopat/config.json`,给用户看实际内容。
|
|
74
|
+
|
|
75
|
+
逐段简短解释**用户实际看到的字段**(**只解释、不修改**)—— 不同用户的 config 字段不同,只挑下面这几个里**确实存在**的来讲:
|
|
76
|
+
|
|
77
|
+
- `providers` —— 你能用哪些 AI 模型(anthropic / bailian / idealab 等),可以配多个,选一个 default。每个 provider 的 `apiKey` 里写的是 `${VAR}` 引用,真值在下一步要看的 vault 里
|
|
78
|
+
- `shell`(如果有)—— sandbox 终端用的 shell(默认 bash)
|
|
79
|
+
- `onboarding`(如果有)—— 引导状态,stage 8 最后会改它。**如果用户的 config 里还没有这个字段,跳过不提**(首次 onboarding 时它就是不存在的)
|
|
80
|
+
|
|
81
|
+
**最关键的一句**:「这些字段你**不需要手动编辑 JSON**。loopat 的 Settings 页面(侧栏 ⚙ 图标)有可视化界面 —— 加 provider、改 default,都能点点鼠标完成。这次引导是为了让你理解底层是文件,未来怎么改你随意。」
|
|
82
|
+
|
|
83
|
+
**注意**:config.json **不包含** env vars 和 mounts 字段——这些是约定的文件系统布局(下一阶段会讲),不在 JSON 里声明。
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Stage 4:Vault —— 加密保险柜 + 两个约定目录
|
|
88
|
+
|
|
89
|
+
`Bash ls /loopat/context/personal/.loopat/vaults/default/` 给用户看。应该至少有 `envs/`,可能还有 `mounts/`。
|
|
90
|
+
|
|
91
|
+
然后简短解释 **vault 的两个约定目录**——loopat 不需要任何配置文件,**文件系统布局本身就是配置**:
|
|
92
|
+
|
|
93
|
+
- **`envs/<NAME>`** —— 每个文件 = 一个环境变量,loop 启动时自动注入到 sandbox。文件名就是变量名,文件内容就是值。Provider 配置里的 `${ANTHROPIC_API_KEY}` 就是引用这里。MCP server token 也存这里(`MCP_<服务名>_TOKEN`)。
|
|
94
|
+
- **`mounts/home/<rel>/...`** —— 每个顶层条目自动 bind 到 sandbox 的 `$HOME/<rel>`。比如 `mounts/home/.ssh/` 就出现在 sandbox 里的 `~/.ssh/`。Stage 5 会动手放一个 `.gitconfig` 进去。
|
|
95
|
+
|
|
96
|
+
补一句关于加密 + 多 vault:
|
|
97
|
+
|
|
98
|
+
- vault 里所有文件 **git push 时自动加密**(git-crypt 在 background 干)
|
|
99
|
+
- 多 vault:可以建 `vaults/dev/`、`vaults/prod/`,loop 可以选挂哪一个 —— 不同身份隔离不同凭据。这次不展开
|
|
100
|
+
|
|
101
|
+
最后用 `Bash ls /loopat/context/personal/.loopat/vaults/default/envs/` 看一眼,告诉用户:"你之前在 Settings 里贴的 API key 现在就在这里,每个一个文件。你不需要在文件层操作,Settings 页可视化操作就够了。"
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Stage 5:放 `.gitconfig` —— 让 sandbox 里的 git 认识你
|
|
106
|
+
|
|
107
|
+
**这是 onboarding 里需要用户**给你信息**才能配置的环节。** 目标:把 `.gitconfig` 放到 `vaults/default/mounts/home/.gitconfig`,它会按 stage 4 讲的约定自动 bind 到 sandbox 的 `~/.gitconfig`,sandbox 里的 git 就知道你姓名邮箱了。**完全不需要碰 config.json**。
|
|
108
|
+
|
|
109
|
+
### 步骤
|
|
110
|
+
|
|
111
|
+
**5a.** 先用 `Bash ls /loopat/context/personal/.loopat/vaults/default/mounts/home/.gitconfig 2>/dev/null` 检查文件**是否已存在**。
|
|
112
|
+
|
|
113
|
+
- 如果已经有 → 告诉用户"你之前已经配过 `.gitconfig` 了",`Bash cat /loopat/context/personal/.loopat/vaults/default/mounts/home/.gitconfig` 给用户看现状,跳过 5b/5c,直接到 stage 6
|
|
114
|
+
- 没有 → 走 5b
|
|
115
|
+
|
|
116
|
+
**5b.** 问用户:
|
|
117
|
+
|
|
118
|
+
> 我需要你的 git 用户名和邮箱来配 `.gitconfig`。两种方式任选:
|
|
119
|
+
>
|
|
120
|
+
> - **方式 A**:直接告诉我"我叫 X,邮箱 Y",我帮你生成一份最小 `.gitconfig`
|
|
121
|
+
> - **方式 B**:把你 host 上 `~/.gitconfig` 整段内容贴过来(如果你已经有特殊配置如 aliases / commit signing 也会一起带过来)
|
|
122
|
+
|
|
123
|
+
等用户给你内容。
|
|
124
|
+
|
|
125
|
+
**5c.** 拿到内容后:
|
|
126
|
+
|
|
127
|
+
1. 准备 `.gitconfig` 内容:
|
|
128
|
+
- 如果用户走方式 A → 生成 INI 格式(用真实换行,**不要**字面写 `\n`):
|
|
129
|
+
```
|
|
130
|
+
[user]
|
|
131
|
+
name = <用户给的姓名>
|
|
132
|
+
email = <用户给的邮箱>
|
|
133
|
+
```
|
|
134
|
+
- 如果用户走方式 B → 直接用用户贴的内容(保留原换行 / 缩进)
|
|
135
|
+
2. `Write` 到 `/loopat/context/personal/.loopat/vaults/default/mounts/home/.gitconfig`(如果父目录不存在 Write 会自动创建)
|
|
136
|
+
3. 告诉用户:
|
|
137
|
+
|
|
138
|
+
> 配好了。这个文件 **下次 spawn loop 时**会自动 bind 到 sandbox 的 `~/.gitconfig`——当前 loop 里 git 还看不到,下个 loop 里就有了。约定就是这样:`vaults/default/mounts/home/` 下放什么,sandbox 的 `~` 就能用什么。
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Stage 6:Memory —— AI 的长期记忆
|
|
143
|
+
|
|
144
|
+
简短解释 loopat 有**两层 memory**:
|
|
145
|
+
|
|
146
|
+
- `/loopat/context/personal/memory/` —— 你的私人记忆,**每次 loop 启动 AI 自动召回**
|
|
147
|
+
- `/loopat/context/notes/memory/` —— 团队共享记忆,复杂任务时 AI 主动去 Read
|
|
148
|
+
|
|
149
|
+
然后**写一条 personal memory** 标记今天的引导:
|
|
150
|
+
|
|
151
|
+
1. `Read /loopat/context/personal/memory/MEMORY.md` 看现有结构
|
|
152
|
+
2. `Write /loopat/context/personal/memory/onboarded.md`,内容用 frontmatter 格式:
|
|
153
|
+
|
|
154
|
+
```markdown
|
|
155
|
+
---
|
|
156
|
+
name: 完成 loopat onboarding
|
|
157
|
+
description: 用户首次完成 loopat 平台引导(loop/vault/memory 概念)
|
|
158
|
+
type: project
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
用户在 <ISO 日期> 完成了 loopat 平台 onboarding:理解了 loop / vault 概念,配置了 .gitconfig,了解 personal memory 机制。
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
3. `Edit /loopat/context/personal/memory/MEMORY.md`,在文件末尾追加:
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
- [完成 loopat onboarding](onboarded.md) — 首次平台引导记录
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
4. 告诉用户:
|
|
171
|
+
|
|
172
|
+
> 写好了。这条 memory 现在就在你的 personal 仓库里 —— 自动 commit + push 到你的 GitHub。**下次开新 loop 时,AI 会自动把它召回当上下文。** 等于你跨 loop 的长期记忆。
|
|
173
|
+
>
|
|
174
|
+
> 你也可以让我以后帮你记别的:「记一下我用 pnpm 不用 npm」"我在阿里云团队,部门叫 xxx" —— 这种偏好类的话直接告诉我,我会写进 memory。
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Stage 7:MCP servers —— 让 AI 用外部工具
|
|
179
|
+
|
|
180
|
+
**MCP**(Model Context Protocol)是 AI agent 调用外部服务的协议 —— 比如查 yuque 文档、提 issue 到 jira、查内部 API。每个 MCP server 暴露一组工具给你 loop 里的 AI 用,工具名按 `mcp__<server>__<tool>` 命名。
|
|
181
|
+
|
|
182
|
+
loopat 里 MCP 跟着 .claude 的 tier 走(在每层 `.claude/settings.json` 的 `mcpServers` 字段里),后写者赢:
|
|
183
|
+
|
|
184
|
+
- **Workspace MCPs** —— admin 在 `knowledge/.loopat/.claude/settings.json` 里配的,团队共享
|
|
185
|
+
- **Profile MCPs** —— 写在 `knowledge/.loopat/profiles/<role>/.claude/settings.json`,跟着 profile 一起被选中才启用
|
|
186
|
+
- **Personal MCPs** —— 你自己加到 `personal/<user>/.loopat/.claude/settings.json`,only you 能看到
|
|
187
|
+
|
|
188
|
+
**授权是 per-user 的**:哪怕 admin 配过 server,每个用户也要自己 OAuth 一次。授权后 token 加密存进你的 vault(git-crypt + push 到你个人 repo)。
|
|
189
|
+
|
|
190
|
+
### 步骤
|
|
191
|
+
|
|
192
|
+
**7a.** 让用户在 chat input 里输入 `/mcp` 看一下:
|
|
193
|
+
|
|
194
|
+
> 你在 chat 输入框里打 `/mcp` —— 这是 loopat 的本地命令,会弹出一个面板列出当前 loop 看到的所有 MCP servers + 它们的连接状态。
|
|
195
|
+
|
|
196
|
+
等用户回来汇报看到什么(一般会有 `Workspace MCPs` 一组 + `Personal MCPs` 一组)。
|
|
197
|
+
|
|
198
|
+
**7b.** 根据汇报分支:
|
|
199
|
+
|
|
200
|
+
- **看到 server 但都是 needs auth** → 引导用户授权:「面板里 server 行右侧那个 `⚠ needs auth` 标的按钮 —— 点它,浏览器会跳到 OAuth 授权页,授权完自动跳回 loopat,token 存进你的 vault。**注意**:授权完成不会自动对当前 loop 生效,需要点 popover 底部的 `↻ Reload session` 按钮(这会重置 SDK session 但**保留**对话历史,然后你下一条消息就能用上)。」
|
|
201
|
+
- **看到 connected server** → 「太好了,已经有 server 可用。你可以让我调用对应工具,比如 `mcp__<server>__<tool>`。」
|
|
202
|
+
- **完全没看到 server** → 「你的 workspace 还没配任何 MCP server。
|
|
203
|
+
- 如果你是 admin:直接在 host 上编辑 `<knowledge-repo>/.loopat/claude/claude.json` 加 `mcpServers`,commit + push。或者从 loopat 的 Context tab 编辑 + 让 distill loop 帮你提交(这条路更正式但更慢)。
|
|
204
|
+
- 如果你是普通 user:加 personal MCP 到 `/loopat/context/personal/.loopat/claude/claude.json`,立刻生效,只对你可见。」
|
|
205
|
+
|
|
206
|
+
**7c.** 简短解释 vault-aware:
|
|
207
|
+
|
|
208
|
+
> 顺带一提,MCP token 是 **per-vault** 的。同一个 user 在 dev / prod 不同 vault 里可以授权同一个 server 到不同账号,互不污染。
|
|
209
|
+
|
|
210
|
+
不要强制用户真的去授权某个 server —— 看到面板 + 理解状态就够了。
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Stage 8:周边能力 + 开始真正工作
|
|
215
|
+
|
|
216
|
+
简短介绍侧栏三个 tab(不要展开):
|
|
217
|
+
|
|
218
|
+
- **Loop**(⑂)—— 你现在在的地方,每个 loop 一个独立工作空间
|
|
219
|
+
- **Focus**(◎)—— Kanban,按"任务"组织你的多个 loop
|
|
220
|
+
- **Context**(⌘)—— 浏览 knowledge / notes / personal 三棵文件树
|
|
221
|
+
- **Chat**(💬)—— 跨 loop 的团队聊天(如果团队部署)
|
|
222
|
+
|
|
223
|
+
然后**主动列出 repo 选项**:
|
|
224
|
+
|
|
225
|
+
1. `Bash ls /loopat/context/repos/` 给用户看可用的 repo
|
|
226
|
+
2. 根据结果分支:
|
|
227
|
+
- **有 repo** → 选一个最像"用户会用得上"的,明确建议:「试试用 `<repo-name>` 开第一个真 loop —— 点侧栏 ⑂ → "+ New Loop" → 选这个 repo + 选一个 sandbox。」
|
|
228
|
+
- **没有 repo**(目录为空)→ 告诉用户:"你的 admin 还没在 workspace 里注册任何代码仓库。可以让 admin 在 host 上的 workspace config(`$LOOPAT_HOME/config.json`)的 `repos` 字段加,或者你不绑 repo 也能开 loop(适合纯对话 / 写文档)。"
|
|
229
|
+
|
|
230
|
+
最后**标记 onboarding 完成**(这是引导最后一步):
|
|
231
|
+
|
|
232
|
+
1. `Read /loopat/context/personal/.loopat/config.json` —— 先看一眼现状。
|
|
233
|
+
2. `Edit` 在顶层加入或更新 `onboarding` 字段:
|
|
234
|
+
|
|
235
|
+
```json
|
|
236
|
+
"onboarding": {
|
|
237
|
+
"status": "done",
|
|
238
|
+
"at": "<当前 ISO8601 时间戳>"
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**`status` 的值必须是字面字符串 `"done"`**(不要写 "completed" / "finished" 之类的同义词,server 按字面值识别)。
|
|
243
|
+
|
|
244
|
+
注意 Edit 时的两种情况:
|
|
245
|
+
- 如果 config.json 里**已经有** `onboarding` 字段(状态可能是 `started`)→ 用 Edit 替换它整个对象的值
|
|
246
|
+
- 如果**没有** `onboarding` 字段 → 用 Edit 在 config.json 最末尾的 `}` 之前插入这一段(注意前面那个字段后要补逗号)
|
|
247
|
+
|
|
248
|
+
只动这一处,**不碰任何其他字段**。
|
|
249
|
+
|
|
250
|
+
3. 简单确认一句:"已标记 onboarding 完成 ✓ 引导到此结束。当前这个 loop 你可以保留,也可以从 loop header 上 archive 掉。"
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## 通用规则
|
|
255
|
+
|
|
256
|
+
- **每轮只问一个问题**,等回答再继续
|
|
257
|
+
- 中文回复。术语保留英文:`loop` / `sandbox` / `vault` / `memory` / `provider` / `config.json`
|
|
258
|
+
- 改文件前先 Read,最小化 Edit 范围,**绝对不要碰用户没要求改的字段**
|
|
259
|
+
- 这份指令是给你看的,**不要把它复述给用户**(不要列长清单 / 不要念阶段标题)
|
|
260
|
+
- 用户跑题 / 想做别的 → 直接配合,不要硬拉回 onboarding。这不是必走流程
|
|
261
|
+
|
|
262
|
+
## 关于写 `onboarding=done`
|
|
263
|
+
|
|
264
|
+
只在**用户跟着你走完 8 个阶段**的最后一步写。其他情况都不写 —— 主动跳过有 UI 按钮处理,中途放弃则保留 started 状态。如果你不确定是不是收尾时刻,宁可不写。
|
|
265
|
+
|
|
266
|
+
写 `done` 是强信号:用户对 loopat 有了完整理解。如果只走一半就标记完成,他再回来不会看到 Welcome card,可能错过没看的内容。
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Promote this loop's context into shared consensus — the ② edge of docs/context-flow.md. Use when work in a context worktree (notes / knowledge / personal / a repo workdir) is worth sharing, or when the user says promote / share this / publish / sync up / 发布 / 同步 / 合并上去. You merge the latest and push to main (or open a PR for gated context), resolving any conflict three-way yourself — that is the point: the loop's own AI resolves conflicts, nothing else does.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# promote — share a loop's context
|
|
6
|
+
|
|
7
|
+
Promote moves what's worth keeping from this loop into shared `main`. It is
|
|
8
|
+
**deliberate** — do it when the work is genuinely worth sharing, not on every
|
|
9
|
+
turn. You run plain git; if the merge conflicts, you resolve it yourself (you
|
|
10
|
+
are the merge agent — no other agent, no script).
|
|
11
|
+
|
|
12
|
+
## Steps
|
|
13
|
+
|
|
14
|
+
`cd` into the worktree you want to promote — `/loopat/context/notes`,
|
|
15
|
+
`/loopat/context/knowledge`, `/loopat/context/personal`, or a repo workdir —
|
|
16
|
+
then capture your work and merge the latest consensus:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
git add -A && git commit -m "<what you're sharing>"
|
|
20
|
+
git fetch origin
|
|
21
|
+
git merge origin/main
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**If the merge conflicts**, resolve it now, here:
|
|
25
|
+
- Edit each conflicted file; reconcile the `<<<<<<< ======= >>>>>>>` markers by
|
|
26
|
+
**keeping both sides' meaning** — this is notes/knowledge, so merge the
|
|
27
|
+
information, don't drop a side.
|
|
28
|
+
- `git add` the resolved files, then `git commit` to finish the merge.
|
|
29
|
+
- (`git merge --abort` backs out cleanly.)
|
|
30
|
+
|
|
31
|
+
Then push:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
# ungated — notes · personal — straight into main:
|
|
35
|
+
git push origin HEAD:main
|
|
36
|
+
|
|
37
|
+
# gated — knowledge · repos — open a PR instead:
|
|
38
|
+
git push origin HEAD
|
|
39
|
+
gh pr create --base main --head "$(git symbolic-ref --short HEAD)" --fill
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
If `git push` is **rejected** (`non-fast-forward` — `main` moved while you
|
|
43
|
+
worked), re-run `git merge origin/main`, resolve again, push again. It converges.
|
|
44
|
+
|
|
45
|
+
## Rules
|
|
46
|
+
|
|
47
|
+
- Always **merge, never rebase** — both parents survive, so a bad merge is
|
|
48
|
+
revertible.
|
|
49
|
+
- Resolve conflicts **here, yourself** — never hand off to another agent.
|
|
50
|
+
- `notes` / `personal` push straight to `main`; `knowledge` / repos are gated —
|
|
51
|
+
open a PR. (Gating is the team's choice; default for knowledge/repos = PR.)
|
|
52
|
+
- Trunk is `main` (your runtime context block names it if it ever differs).
|
|
53
|
+
- Solo works the same: `origin` is just a loopat-hosted local repo — same commands.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# loopat sandbox base image.
|
|
2
|
+
#
|
|
3
|
+
# The base is a "full-stack" interactive workbench: the user can apt
|
|
4
|
+
# install, git, make, curl, etc. inside the loop without leaving. Per-loop
|
|
5
|
+
# child images bake mise-managed toolchains on top of this base (see
|
|
6
|
+
# `ensureLoopImage` in server/src/podman.ts).
|
|
7
|
+
#
|
|
8
|
+
# Built once at first launch (or via scripts/build-sandbox-image.sh) and
|
|
9
|
+
# tagged `loopat-sandbox:latest`. Each loop's container is `podman
|
|
10
|
+
# create`'d from this image — or, when the loop has a composed mise.toml,
|
|
11
|
+
# from a per-loop child `loopat-sandbox-<hash>:latest`.
|
|
12
|
+
#
|
|
13
|
+
# Base: ubuntu:24.04 so glibc matches the dev hosts (we use Ubuntu 24.04).
|
|
14
|
+
# The host's claude binary binds in via --volume and runs against this glibc.
|
|
15
|
+
FROM ubuntu:24.04
|
|
16
|
+
|
|
17
|
+
ARG DEBIAN_FRONTEND=noninteractive
|
|
18
|
+
|
|
19
|
+
USER root
|
|
20
|
+
|
|
21
|
+
# Strip the image's default `ubuntu` user (uid 1000) and create our own
|
|
22
|
+
# fixed `loopat` user at uid 2000. The container then ALWAYS appears as
|
|
23
|
+
# `loopat` to whoever's inside, regardless of which host user is running
|
|
24
|
+
# rootless podman — see `--userns=keep-id:uid=2000,gid=2000` in podman.ts.
|
|
25
|
+
# Why uid 2000: avoid colliding with 1000 (still common host uid; the
|
|
26
|
+
# original ubuntu user previously claimed it inside this image).
|
|
27
|
+
#
|
|
28
|
+
# Why not root: the claude binary refuses --dangerously-skip-permissions
|
|
29
|
+
# when uid == 0 ("for security reasons"). The SDK driver always uses
|
|
30
|
+
# bypassPermissions in loopat, so container-root is untenable.
|
|
31
|
+
#
|
|
32
|
+
# Full-stack feel: sudo NOPASSWD for loopat → `sudo apt install <pkg>`
|
|
33
|
+
# from any shell, no prompts.
|
|
34
|
+
RUN userdel -r ubuntu 2>/dev/null || true \
|
|
35
|
+
&& groupadd -g 2000 loopat \
|
|
36
|
+
&& useradd -m -u 2000 -g 2000 -s /bin/bash loopat
|
|
37
|
+
|
|
38
|
+
# Full-stack system layer. Everything a dev reflexively reaches for.
|
|
39
|
+
# podman/uidmap/fuse-overlayfs/slirp4netns: nested rootless podman support
|
|
40
|
+
# — every sandbox can run podman without per-loop opt-in. The sandbox's
|
|
41
|
+
# create-args carry `--privileged --device /dev/fuse`, and inner loopat
|
|
42
|
+
# (or any AI workflow) probes for podman at runtime via the existing
|
|
43
|
+
# self-detection path.
|
|
44
|
+
RUN apt-get update \
|
|
45
|
+
&& apt-get install -y --no-install-recommends \
|
|
46
|
+
bash coreutils findutils grep sed less \
|
|
47
|
+
util-linux procps bsdmainutils ca-certificates \
|
|
48
|
+
sudo curl git build-essential openssh-client \
|
|
49
|
+
jq vim fish python3-minimal \
|
|
50
|
+
podman uidmap fuse-overlayfs slirp4netns \
|
|
51
|
+
&& apt-get clean \
|
|
52
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
53
|
+
|
|
54
|
+
# Passwordless sudo for the loopat user.
|
|
55
|
+
RUN echo "loopat ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/loopat \
|
|
56
|
+
&& chmod 0440 /etc/sudoers.d/loopat
|
|
57
|
+
|
|
58
|
+
# Nested rootless podman setup for `loopat` (uid 2000).
|
|
59
|
+
#
|
|
60
|
+
# Why the unusual subuid range (10000:50000, not the typical 100000:65536):
|
|
61
|
+
# the OUTER container's userns size is bounded by the host user's subuid
|
|
62
|
+
# allocation (typically 65536). With `--userns=keep-id:uid=2000,gid=2000`,
|
|
63
|
+
# uids 0..65535 are mapped inside; uid 100000 simply doesn't exist in
|
|
64
|
+
# this namespace, so newuidmap rejects it. Inner subuid must be a SUBSET
|
|
65
|
+
# of the outer-mapped range. 10000:50000 stays within 0..65535 and avoids
|
|
66
|
+
# clashing with uid 2000 (loopat itself).
|
|
67
|
+
#
|
|
68
|
+
# Storage driver `vfs`: rootless-in-rootless overlayfs is fragile across
|
|
69
|
+
# kernel/podman/storage combinations. vfs always works (cost: disk space
|
|
70
|
+
# proportional to layers × containers). Switch to fuse-overlayfs later if
|
|
71
|
+
# disk pressure shows up.
|
|
72
|
+
#
|
|
73
|
+
# Cgroup manager `cgroupfs`: there's no systemd inside the sandbox, so
|
|
74
|
+
# the systemd cgroup driver can't be used.
|
|
75
|
+
# NOTE on `>`: Ubuntu's useradd auto-allocates `loopat:100000:65536` via
|
|
76
|
+
# /etc/login.defs defaults. Appending our entry leaves both, and rootless
|
|
77
|
+
# podman tries to apply BOTH ranges — the 100000 one falls outside outer's
|
|
78
|
+
# 0..65535 view and newuidmap fails. Overwrite, don't append.
|
|
79
|
+
RUN echo "loopat:10000:50000" > /etc/subuid \
|
|
80
|
+
&& echo "loopat:10000:50000" > /etc/subgid \
|
|
81
|
+
&& mkdir -p /home/loopat/.config/containers \
|
|
82
|
+
&& printf '[storage]\ndriver = "vfs"\n' > /home/loopat/.config/containers/storage.conf \
|
|
83
|
+
&& printf '[containers]\ncgroup_manager = "cgroupfs"\n' > /home/loopat/.config/containers/containers.conf \
|
|
84
|
+
&& printf 'unqualified-search-registries = ["docker.io"]\n' > /home/loopat/.config/containers/registries.conf \
|
|
85
|
+
&& chown -R loopat:loopat /home/loopat/.config
|
|
86
|
+
|
|
87
|
+
# mise — tool version manager. Per-loop child images bake their tools via
|
|
88
|
+
# `mise install` during their build, so the toolchain ends up baked into
|
|
89
|
+
# image layers and reused across loops with the same composed mise.toml.
|
|
90
|
+
# Note: mise.run's installer reads MISE_INSTALL_PATH (full path to the
|
|
91
|
+
# binary), NOT a "dir" env var.
|
|
92
|
+
RUN curl -fsSL https://mise.run | MISE_INSTALL_PATH=/usr/local/bin/mise sh
|
|
93
|
+
|
|
94
|
+
# mise state lives OUTSIDE $HOME so the per-loop $HOME overlay (used for
|
|
95
|
+
# persistent shell history) can't shadow the tools the image bakes in.
|
|
96
|
+
# Shims dir on PATH makes every tool globally callable without needing
|
|
97
|
+
# `mise activate` in each shell.
|
|
98
|
+
ENV MISE_DATA_DIR=/opt/loopat-mise \
|
|
99
|
+
MISE_CONFIG_DIR=/opt/loopat-mise/config \
|
|
100
|
+
MISE_CACHE_DIR=/opt/loopat-mise/cache \
|
|
101
|
+
PATH=/opt/loopat-mise/shims:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
102
|
+
|
|
103
|
+
RUN mkdir -p /opt/loopat-mise/shims /opt/loopat-mise/config /opt/loopat-mise/cache \
|
|
104
|
+
&& chown -R loopat:loopat /opt/loopat-mise \
|
|
105
|
+
&& chmod -R 755 /opt/loopat-mise
|
|
106
|
+
|
|
107
|
+
# Switch to loopat. mise install (per-loop child build) and runtime
|
|
108
|
+
# exec'd processes all run as this user.
|
|
109
|
+
USER loopat
|
|
110
|
+
|
|
111
|
+
# The container's main process is just a long-lived sleeper holding the
|
|
112
|
+
# namespaces open. SDK driver + PTY shell `podman exec` in as siblings.
|
|
113
|
+
CMD ["/bin/sleep", "infinity"]
|