Add pi extension for container environment context
Injects workspace, filesystem, and firewall info into the system
prompt via before_agent_start, keeping AGENTS.md free for user
customization.
Assisted-by: Claude Opus 4.6 via pi
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..94c9b8c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,49 @@
+# Ramekin
+
+Containerized harness for running the [pi coding agent](https://github.com/badlogic/pi-mono) with network-restricted access.
+
+## Quick start
+
+```sh
+cargo run # run pi in a container against the current directory
+cargo run -- /some/path # mount a specific workspace
+```
+
+Ramekin builds a Docker image with pi and its dependencies, starts it via Docker Compose, and attaches your terminal. Auth state, settings, and keybindings persist across runs.
+
+## How it works
+
+A Rust CLI orchestrates a Docker Compose stack. On each run it:
+
+1. Creates XDG directories for persistent state
+2. Writes the embedded Dockerfile and compose file to `$XDG_CACHE_HOME/ramekin/`
+3. Starts the agent container with the workspace mounted at `/workspace`
+4. Attaches interactively, then tears down on exit
+
+### Persistence
+
+| What | Where (host) | Where (container) |
+|---|---|---|
+| Auth, sessions | `$XDG_DATA_HOME/ramekin/` | `/root/.pi` |
+| settings.json | `$XDG_CONFIG_HOME/ramekin/settings.json` | `/root/.pi/agent/settings.json` |
+| keybindings.json | `$XDG_CONFIG_HOME/ramekin/keybindings.json` | `/root/.pi/agent/keybindings.json` |
+| AGENTS.md | `$XDG_CONFIG_HOME/ramekin/AGENTS.md` | `/root/.pi/agent/AGENTS.md` |
+
+Settings and keybindings are seeded as empty JSON (`{}`) on first run. AGENTS.md is seeded empty.
+
+### Container environment extension
+
+A built-in pi extension (`ramekin.ts`) is mounted into the agent container. It appends container environment context to the system prompt via `before_agent_start`, telling the agent about the workspace mount, ephemeral filesystem, and networking constraints (when the firewall is enabled). AGENTS.md remains fully available for user customization.
+
+### Custom Dockerfile
+
+Place a `Dockerfile` at `.ramekin/Dockerfile` in your workspace to override the default agent image. The workspace is used as the build context, so `COPY` instructions work relative to the project root.
+
+## Development
+
+```sh
+cargo check # type-check
+cargo fmt # format
+cargo clippy # lint
+just # all three
+```
diff --git a/assets/compose.yml b/assets/compose.yml
index d64ab0f..b9bd9a2 100644
--- a/assets/compose.yml
+++ b/assets/compose.yml
@@ -6,9 +6,15 @@ services:
image: ramekin-agent
stdin_open: true
tty: true
+ environment:
+ - RAMEKIN_FIREWALL=${RAMEKIN_FIREWALL:-true}
volumes:
- "${RAMEKIN_WORKSPACE:-.}:/workspace"
- "${RAMEKIN_DATA_DIR}:/root/.pi"
- "${RAMEKIN_CONFIG_DIR}/settings.json:/root/.pi/agent/settings.json"
- "${RAMEKIN_CONFIG_DIR}/keybindings.json:/root/.pi/agent/keybindings.json"
- "${RAMEKIN_CONFIG_DIR}/AGENTS.md:/root/.pi/agent/AGENTS.md"
+ - type: bind
+ source: "${RAMEKIN_EXTENSION}"
+ target: /root/.pi/agent/extensions/ramekin.ts
+ read_only: true
diff --git a/assets/ramekin.ts b/assets/ramekin.ts
new file mode 100644
index 0000000..f4b4284
--- /dev/null
+++ b/assets/ramekin.ts
@@ -0,0 +1,31 @@
+import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
+
+export default function (pi: ExtensionAPI) {
+ const firewall = process.env.RAMEKIN_FIREWALL !== "false";
+
+ let context = `
+# Ramekin Container Environment
+
+You are running inside a Docker container managed by **ramekin**.
+
+## Workspace
+
+The project workspace is bind-mounted at \`/workspace\`. This is the only directory where your changes are visible to the host.
+
+## Filesystem
+
+The container filesystem is ephemeral. Any files written outside \`/workspace\` will be lost when the session ends. System packages installed with \`apt-get\` do not persist across sessions — use a custom \`.ramekin/Dockerfile\` to add permanent dependencies.
+`;
+
+ if (firewall) {
+ context += `
+## Networking
+
+Networking is restricted by an nftables firewall. Only outbound connections to \`api.anthropic.com:443\` are allowed. You cannot fetch URLs, install packages from remote registries, or reach any other external host. All other outbound traffic is blocked.
+`;
+ }
+
+ pi.on("before_agent_start", async (event) => {
+ return { systemPrompt: event.systemPrompt + "\n" + context };
+ });
+}
diff --git a/src/main.rs b/src/main.rs
index a55ed14..3fba218 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -8,6 +8,7 @@ use tracing_subscriber::{EnvFilter, fmt, prelude::*};
const COMPOSE_YML: &str = include_str!("../assets/compose.yml");
const DOCKERFILE: &str = include_str!("../assets/Dockerfile");
+const RAMEKIN_EXTENSION: &str = include_str!("../assets/ramekin.ts");
#[derive(Parser)]
#[command(about = "Run a pi coding agent in a containerized environment")]
@@ -70,6 +71,7 @@ fn run() -> Result<()> {
.wrap_err("failed to create cache directory")?;
fs_err::write(cache_dir.join("compose.yml"), COMPOSE_YML)?;
fs_err::write(cache_dir.join("Dockerfile"), DOCKERFILE)?;
+ fs_err::write(cache_dir.join("ramekin.ts"), RAMEKIN_EXTENSION)?;
let compose_file = cache_dir.join("compose.yml");
@@ -92,7 +94,8 @@ fn run() -> Result<()> {
.env("RAMEKIN_DATA_DIR", &pi_data_dir)
.env("RAMEKIN_DOCKERFILE", &dockerfile)
.env("RAMEKIN_BUILD_CONTEXT", &build_context)
- .env("RAMEKIN_CONFIG_DIR", &pi_config_dir);
+ .env("RAMEKIN_CONFIG_DIR", &pi_config_dir)
+ .env("RAMEKIN_EXTENSION", cache_dir.join("ramekin.ts"));
Ok(cmd)
};