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
change ntmklzoqolzmzkllymnptvzqtkswtrzl
commit e6c28cd7b1b5fc27e1d42fd102cb816171cfc2aa
author Alpha Chen <alpha@kejadlen.dev>
date
parent uslqqlps
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)
     };