Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ ExitBox automatically:
- **Full Git Support** — optional mode that mounts host `.gitconfig` and SSH agent for seamless git operations inside the container
- **GitHub CLI Authentication** — pre-flight vault import for `GITHUB_TOKEN` with automatic in-container export so `gh` and HTTPS git work transparently
- **RTK Token Optimizer (Experimental)** — optional [rtk](https://github.com/rtk-ai/rtk) integration reduces CLI output token consumption by 60-90%
- **Graphify Knowledge Graph (Experimental)** — optional [graphify](https://github.com/safishamsi/graphify) integration; when enabled, the `/graphify` skill is auto-installed into compatible agents and removed when disabled
- **External Tools** — configure third-party tools (GitHub CLI, etc.) via the setup wizard; packages are auto-installed at image build time
- **Supply-Chain Hardened Installs** — Claude Code and OpenCode installed via direct download with SHA-256 checksum verification
- **Alpine Base Image** — minimal ~5 MB base with 3-layer image hierarchy and incremental rebuilds
Expand Down Expand Up @@ -241,6 +242,20 @@ exitbox agents config claude # Open agent config in $EDITOR

> **Note:** RTK is experimental. If you encounter issues, disable it via `exitbox setup` and rebuild with `exitbox run <agent> --update`.

### Graphify Knowledge Graph (Experimental)

[graphify](https://github.com/safishamsi/graphify) maps a project (code, docs, PDFs, images) into a queryable knowledge graph you invoke with `/graphify` inside an agent. When enabled, the `graphify` CLI is installed at image build time and the `/graphify` skill is registered into each compatible agent at container start; when disabled, the skill is removed.

```bash
# Enable via the setup wizard:
exitbox setup
# → Settings → Graphify → Enable
```

Compatible agents: **Claude, Codex, OpenCode, Copilot, Pi**. (Cursor's graphify install is project-scoped and Kimi Code's skill path differs from graphify's `kimi` target, so they are not auto-wired; Qwen has no graphify platform.)

> **Note:** Graphify is experimental and gated at image build time like RTK — toggling it requires a rebuild (`exitbox run <agent> --update`).

### Agent Skills

Skills are reusable SKILL.md files that give agents specialized capabilities (coding conventions, deploy workflows, review checklists, etc.). ExitBox provides a universal skill installer that works across all agents. Skills are installed once per workspace and automatically linked into each agent's skill directory at container start.
Expand Down
1 change: 1 addition & 0 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ func runAgent(agentName string, passthrough []string) {
Keybindings: cfg.Settings.Keybindings.EnvValue(),
FullGitSupport: cfg.Settings.DefaultFlags.FullGitSupport,
RTK: cfg.Settings.RTK,
Graphify: cfg.Settings.Graphify,
}

exitCode, err := run.AgentContainer(rt, opts)
Expand Down
1 change: 1 addition & 0 deletions internal/config/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func DefaultConfig() *Config {
AutoUpdate: false,
StatusBar: true,
RTK: false,
Graphify: false,
DefaultWorkspace: "default",
DefaultFlags: DefaultFlags{
AutoResume: false,
Expand Down
1 change: 1 addition & 0 deletions internal/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ type SettingsConfig struct {
AutoUpdate bool `yaml:"auto_update"`
StatusBar bool `yaml:"status_bar"`
RTK bool `yaml:"rtk"`
Graphify bool `yaml:"graphify"`
DefaultWorkspace string `yaml:"default_workspace,omitempty"`
DefaultFlags DefaultFlags `yaml:"default_flags"`
Keybindings KeybindingsConfig `yaml:"keybindings,omitempty"`
Expand Down
1 change: 1 addition & 0 deletions internal/image/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ func buildBaseFull(ctx context.Context, rt container.Runtime, cmd, imageName str
args = append(args,
"--build-arg", fmt.Sprintf("EXITBOX_VERSION=%s", Version),
"--build-arg", fmt.Sprintf("INSTALL_RTK=%v", cfg.Settings.RTK),
"--build-arg", fmt.Sprintf("INSTALL_GRAPHIFY=%v", cfg.Settings.Graphify),
"-t", publishedName,
"-f", dockerfilePath,
buildCtx,
Expand Down
5 changes: 5 additions & 0 deletions internal/run/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ type Options struct {
Keybindings string
FullGitSupport bool
RTK bool
Graphify bool
}

// AgentContainer runs an agent container interactively.
Expand Down Expand Up @@ -413,6 +414,9 @@ func AgentContainer(rt container.Runtime, opts Options) (int, error) {
if opts.RTK {
args = append(args, "-e", "EXITBOX_RTK=true")
}
if opts.Graphify {
args = append(args, "-e", "EXITBOX_GRAPHIFY=true")
}
if opts.Ollama {
args = append(args, ollamaEnvVars(opts.Agent)...)
}
Expand Down Expand Up @@ -537,6 +541,7 @@ func isReservedEnvVar(key string) bool {
"EXITBOX_VAULT_ENABLED": true,
"EXITBOX_VAULT_READONLY": true,
"EXITBOX_RTK": true,
"EXITBOX_GRAPHIFY": true,
"EXITBOX_IDE_PORT": true,
"CLAUDE_CODE_SSE_PORT": true,
"ENABLE_IDE_INTEGRATION": true,
Expand Down
5 changes: 5 additions & 0 deletions internal/wizard/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ type State struct {
ExternalTools []string // selected external tools (e.g. "GitHub CLI")
FullGitSupport bool // full git support (SSH agent + .gitconfig)
RTK bool // experimental: token-optimized CLI wrappers
Graphify bool // graphify knowledge-graph /graphify skill
}

// Model is the root bubbletea model for the wizard.
Expand Down Expand Up @@ -214,6 +215,7 @@ func NewModel() Model {
checked["setting:pass_env"] = true
checked["setting:read_only"] = false
checked["setting:rtk"] = false
checked["setting:graphify"] = false
kb := config.DefaultKeybindings()
return Model{
step: stepWelcome,
Expand Down Expand Up @@ -315,6 +317,7 @@ func NewModelFromConfig(cfg *config.Config) Model {
checked["setting:read_only"] = cfg.Settings.DefaultFlags.ReadOnly
checked["setting:full_git"] = cfg.Settings.DefaultFlags.FullGitSupport
checked["setting:rtk"] = cfg.Settings.RTK
checked["setting:graphify"] = cfg.Settings.Graphify

// On re-run, always start at the top menu so the user can choose
// between workspace management and general settings.
Expand Down Expand Up @@ -2052,6 +2055,7 @@ var settingsOptions = []struct {
{"setting:read_only", "Read-only workspace", "Mount workspace as read-only (agents cannot modify files)"},
{"setting:full_git", "Full Git support", "Mount SSH agent + .gitconfig into container (exposes git identity)"},
{"setting:rtk", "RTK token optimizer", "Experimental: use rtk to reduce CLI output tokens by 60-90%"},
{"setting:graphify", "Graphify knowledge graph", "Install the /graphify skill (maps your project into a queryable graph) in compatible agents"},
}

func (m Model) updateSettings(msg tea.Msg) (tea.Model, tea.Cmd) {
Expand All @@ -2078,6 +2082,7 @@ func (m Model) updateSettings(msg tea.Msg) (tea.Model, tea.Cmd) {
m.state.MakeDefault = m.checked["setting:make_default"]
m.state.FullGitSupport = m.checked["setting:full_git"]
m.state.RTK = m.checked["setting:rtk"]
m.state.Graphify = m.checked["setting:graphify"]
m.visitedSteps[stepSettings] = true
if !m.isFirstRun && m.topMenuChoice == 0 {
// Workspace management: Settings → Domains or Vault
Expand Down
1 change: 1 addition & 0 deletions internal/wizard/wizard.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ func applyResult(state State, existingCfg *config.Config) error {
cfg.Settings.AutoUpdate = state.AutoUpdate
cfg.Settings.StatusBar = state.StatusBar
cfg.Settings.RTK = state.RTK
cfg.Settings.Graphify = state.Graphify
cfg.Settings.DefaultFlags = config.DefaultFlags{
NoFirewall: !state.EnableFirewall,
AutoResume: state.AutoResume,
Expand Down
11 changes: 11 additions & 0 deletions static/build/Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,17 @@ RUN if [ "$INSTALL_RTK" = "true" ]; then \
apk del musl-dev gcc; \
fi

# Install graphify — knowledge-graph "/graphify" skill for coding agents
# (conditional). The CLI (pip package "graphifyy", command "graphify") is installed
# globally so the non-root runtime user can run it; per-agent skill registration
# happens at container start (see setup_graphify in docker-entrypoint).
ARG INSTALL_GRAPHIFY=false
RUN if [ "$INSTALL_GRAPHIFY" = "true" ]; then \
apk add --no-cache py3-pip && \
pip install --no-cache-dir --break-system-packages graphifyy && \
graphify --version; \
fi

# Stub xdg-open: no browser in container, just print the URL so OAuth
# flows (OpenCode, Codex) show the link instead of failing with ENOENT.
RUN printf '#!/bin/sh\necho "Open this URL in your browser:"\necho "$1"\n' \
Expand Down
52 changes: 52 additions & 0 deletions static/build/docker-entrypoint
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,7 @@ run_agent_loop() {
inject_sandbox_instructions
inject_codex_approval_rules
link_skills
setup_graphify
fi

build_resume_args "$@"
Expand Down Expand Up @@ -1217,6 +1218,56 @@ setup_rtk() {
fi
}

# graphify_platform_for_agent maps an ExitBox agent to its graphify "--platform"
# token, or "" when graphify can't target that agent. Only agents whose graphify
# skill destination matches ExitBox's config layout are included: kimi (graphify
# writes ~/.kimi, ExitBox uses ~/.kimi-code), cursor (project-scoped .cursor rules)
# and qwen (no graphify platform) are intentionally excluded.
graphify_platform_for_agent() {
case "$1" in
claude) echo "claude" ;;
codex) echo "codex" ;;
opencode) echo "opencode" ;;
copilot) echo "copilot" ;;
pi) echo "pi" ;;
*) echo "" ;;
esac
}

# graphify_skill_dir_for_agent returns the directory graphify installs its skill
# into for the agent, so it can be removed when graphify is disabled (the CLI is
# gone after a rebuild, so removal must be file-based). Paths mirror graphify's
# own _PLATFORM_CONFIG destinations.
graphify_skill_dir_for_agent() {
case "$1" in
claude) echo "$HOME/.claude/skills/graphify" ;;
codex) echo "$HOME/.codex/skills/graphify" ;;
opencode) echo "$HOME/.config/opencode/skills/graphify" ;;
copilot) echo "$HOME/.copilot/skills/graphify" ;;
pi) echo "$HOME/.pi/agent/skills/graphify" ;;
*) echo "" ;;
esac
}

setup_graphify() {
# Register (enabled) or remove (disabled) the graphify "/graphify" skill for
# the current agent. Runs after apply_active_workspace_links so the agent's
# config dir is symlinked to the persistent workspace location.
local platform skill_dir
platform="$(graphify_platform_for_agent "$AGENT")"
[[ -n "$platform" ]] || return 0
skill_dir="$(graphify_skill_dir_for_agent "$AGENT")"

if [[ "${EXITBOX_GRAPHIFY:-}" == "true" ]]; then
command -v graphify >/dev/null 2>&1 || return 0
graphify install --platform "$platform" >/dev/null 2>&1 || true
else
# Disabled: remove a previously installed graphify skill (file-based so it
# works even when the CLI is no longer present after a rebuild).
[[ -n "$skill_dir" ]] && rm -rf "$skill_dir" 2>/dev/null
fi
}

# ============================================================================
# AGENT DETECTION
# ============================================================================
Expand Down Expand Up @@ -1797,6 +1848,7 @@ if [[ -n "$AGENT" ]]; then
inject_sandbox_instructions
inject_codex_approval_rules
link_skills
setup_graphify
fi

# Debug: show what's available
Expand Down
53 changes: 53 additions & 0 deletions static/build/docker-entrypoint_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1696,6 +1696,59 @@ test_link_skills_pi() {

test_link_skills_pi

# ============================================================================
# Test: setup_graphify installs/removes the /graphify skill per agent
# ============================================================================
test_setup_graphify() {
local gpf sdf sgf
gpf="$(extract_func graphify_platform_for_agent)"
sdf="$(extract_func graphify_skill_dir_for_agent)"
sgf="$(extract_func setup_graphify)"

# Enabled + compatible agent → runs `graphify install --platform <X>`.
# setup_graphify redirects the command's stdout to /dev/null, so the stub
# records its args to a file instead.
local marker="$TEST_TMPDIR/gfy_args"
rm -f "$marker"
(
export HOME; HOME="$(mktemp -d "$TEST_TMPDIR/gfy1.XXXXXX")"
export AGENT="pi" EXITBOX_GRAPHIFY="true" GFY_MARKER="$marker"
eval "$gpf"; eval "$sdf"; eval "$sgf"
graphify() { echo "$*" > "$GFY_MARKER"; }
setup_graphify
) 2>/dev/null
local enabled=""
[[ -f "$marker" ]] && enabled="$(cat "$marker")"
assert_contains "setup_graphify installs for compatible agent" "$enabled" "install --platform pi"

# Disabled → removes a previously installed skill dir.
local home2 removed
home2="$(mktemp -d "$TEST_TMPDIR/gfy2.XXXXXX")"
mkdir -p "$home2/.pi/agent/skills/graphify"
removed="$(
export HOME="$home2"
export AGENT="pi" EXITBOX_GRAPHIFY="false"
eval "$gpf"; eval "$sdf"; eval "$sgf"
setup_graphify
[[ -d "$home2/.pi/agent/skills/graphify" ]] && echo PRESENT || echo GONE
)" 2>/dev/null
assert_eq "setup_graphify removes skill when disabled" "GONE" "$removed"

# Incompatible agent (qwen) → no graphify invocation even when enabled.
local skipped
skipped="$(
export HOME; HOME="$(mktemp -d "$TEST_TMPDIR/gfy3.XXXXXX")"
export AGENT="qwen" EXITBOX_GRAPHIFY="true"
eval "$gpf"; eval "$sdf"; eval "$sgf"
graphify() { echo "CALLED"; }
setup_graphify
echo DONE
)" 2>/dev/null
assert_eq "setup_graphify skips incompatible agent" "DONE" "$skipped"
}

test_setup_graphify

# ============================================================================
# Results
# ============================================================================
Expand Down
Loading