Framework of Operational Runtime & Gated Execution.
A coding and security execution runtime with sandboxed tools and a permission gate in front of anything irreversible.
Forge provides a terminal interface for local interactive work and an identity-free Python runtime for orchestrators. On a Mark VI server it is used by anonymous Legion workers; Mark VI retains every persona, conversation, upload, model connection, and result record.
./install.sh # Windows: .\install.ps1
cd your-project
forge chat- What it does
- Requirements
- Installation
- Terminal interface
- Tools
- Execution model
- Sandboxing
- Permissions
- Project conventions
- Sessions
- Commit attribution
- Context management
- Model providers
- Agent configuration
- Running as a peer
- Configuration reference
- Project layout
- Testing
Forge runs an agent against a codebase. The agent reads files, searches, edits, runs commands, and iterates on the results until the task is complete or a limit is reached.
Two agents ship with it. They use the same engine and differ only in configuration:
| Agent | Purpose | Tools |
|---|---|---|
optimus |
Software development | 14 |
centurion |
Security assessment | 6 |
Adding an agent means adding a configuration directory. No engine changes are required.
Capabilities:
- File reading, writing and editing, with staleness detection on edits
- Repository search (
grep,glob) - Shell execution inside a sandbox
- Web search and page retrieval
- Git worktree isolation, so edits land on a branch rather than in your working copy
- A task list that survives context compaction
- Project conventions from AGENTS.md loaded into every turn
- Conversations saved per workspace and resumable after the terminal closes
- Commits attributed to the agent that wrote the code
- Codebase structure queries via a Graphify sidecar (optional)
- Model routing across six providers
- Python 3.11 or later
- Docker — optional; without it Forge uses a subprocess sandbox
- Git — required for worktree isolation and the git commands
- An API key for at least one model provider
One command from a fresh clone to a working forge. The script finds a Python
3.11+, builds a .venv, installs the Forge with the extras that make it
usable, seeds a .env, and proves the install by listing the agents. Rerunning
it is safe — an existing environment is reused and an existing .env is never
touched.
Windows (PowerShell):
git clone https://github.com/spedatox/forge-mark1.git; cd forge-mark1; .\install.ps1 -AddToPathmacOS / Linux:
git clone https://github.com/spedatox/forge-mark1.git && cd forge-mark1 && ./install.sh --add-to-path-AddToPath / --add-to-path puts the environment's Scripts (bin on
Unix) directory on your PATH, so forge works in any project without
activating anything. Leave it off and the script prints the full path to run
instead.
Other flags, the same on both scripts:
| Flag | Effect |
|---|---|
-NoVenv / --no-venv |
Install into the interpreter itself instead of a .venv — what a server unit file wants |
-Extras providers / --extras providers |
Choose the optional-dependency groups (default tui,providers,dev) |
-Python <path> / --python <path> |
Build from a specific interpreter |
The scripts do exactly this:
python -m venv .venv
. .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[tui,providers,dev]"Optional extras:
| Extra | Provides |
|---|---|
tui |
Input history, completion and reverse search in the terminal interface |
providers |
OpenAI, Gemini, z.ai, DeepSeek and Ollama support |
graph |
Codebase structure queries |
dev |
Test dependencies |
all |
tui, providers and graph together |
Forge loads two .env files at startup, most specific first, and a real
environment variable beats both:
| File | Covers |
|---|---|
./.env |
the project you are sitting in |
~/.forge/.env (or $FORGE_HOME/.env) |
every project on the machine |
.env.example documents every variable; the installer copies it to .env when
neither file exists yet. Put your API key in it. For a machine-wide install,
move it where every project will find it:
mkdir -p ~/.forge && cp .env ~/.forge/.envOnly one key is genuinely required — whichever provider your agent's model ref names. Everything else is optional and fails loudly at the point it is needed.
forge agents # lists configured agents and their models
forge demo # a full offline turn — no API key, no network
forge chat # then type /doctorforge demo is the fastest proof that a machine is set up correctly: it runs a
real end-to-end turn against a scripted model, exercising the engine, the tools
and the sandbox without touching a provider. /doctor reports missing API
keys, an unreachable sandbox, and anything else that would otherwise fail
partway through a task.
cd your-project
forge chat # works in the current directory
forge chat --agent centurion
forge chat --cwd /path/to/repo
forge chat --model openai:gpt-5.1The first character of a line determines how it is handled:
| Input | Behaviour |
|---|---|
text |
Sent to the model |
!command |
Runs in the sandbox immediately — no model call, no token cost. Not added to the conversation. |
/command |
Handled by the interface |
Name a picture anywhere in a line and it is sent with the turn:
what is wrong with this layout? @screenshot.png
@ is optional — dragging a file onto the terminal pastes its path, quoted if
it contains a space, and that works too. A token is treated as an attachment
only if it has an image suffix, resolves against the workspace, and exists, so
email @bob about the .png pipeline attaches nothing.
PNG, JPEG, GIF and WebP, up to 3.75MB each and eight per turn. The path stays in the text where you typed it — it is how you refer to the picture in the next sentence. Anything that looked like an image and could not be read is reported and the turn goes ahead without it.
A turn carrying an image runs on the agent's vision_model if its default
cannot see; the swap is printed when it happens.
| Key | Action |
|---|---|
↑ ↓ |
Previous inputs, stored per workspace |
ctrl+r |
Search input history |
tab |
Complete a /command or an @path |
esc then enter |
Insert a newline instead of submitting |
shift+tab |
Switch between act and plan mode |
ctrl+o |
Reprint the last truncated tool output in full |
ctrl+c |
Interrupt the running task |
ctrl+d |
End the session |
| Command | Description |
|---|---|
/help |
List commands |
/status |
Session state and git status |
/diff |
Uncommitted changes in the working tree |
/branch [name] |
Show the current branch, or switch to another |
/doctor |
Check that this environment can run a task |
/cost |
Token usage for this session |
/context |
Context window utilisation |
/compact |
Summarise the conversation to free context |
/clear |
Discard the conversation, keep the session |
/tools |
Tools available to this agent |
/model [ref] |
Pick a model from the providers you have keys for, or switch straight to a ref |
/agent |
Agent profile in use |
/approved |
Operations approved for this session |
/transcript |
Print the raw conversation |
/export [file] |
Write the conversation to a file |
/keybindings |
Key reference |
/cwd |
Current working directory |
/commit <message> |
Stage everything and commit |
/review [focus] |
Have the agent read its own uncommitted changes |
/sessions |
Conversations that can be resumed here |
/resume [n|id] |
Continue an earlier conversation |
/vim [on|off] |
Vi keys in the input line |
/copy |
Last reply to the clipboard |
/mcp |
MCP servers and their tools |
/permissions |
What is gated and what you have allowed |
/init |
Write an AGENTS.md describing this project's conventions |
/exit |
End the session |
A summary is printed above each prompt:
optimus · deepseek-v4-pro · 18% ctx · 12.4k in / 3.1k out · main · my-project
Fields appear only when they apply; permission mode is shown when it is not
act. Token counts are always shown because they are measured. Currency
figures appear only if per-model prices are configured in
forge/tui/status.py, since a built-in price table would go out of date
without any indication that it had.
Tools are organised into groups. An agent's configuration lists the groups it may use, and that list is the security boundary — a tool absent from it cannot be called.
Navigation — read_file, grep, glob
Editing — write_file, edit_file
edit_file requires that the file has been read and has not changed since.
Content is compared by hash, so an edit against a file modified by another
process is rejected rather than applied to unexpected content. Edits are
reported to the operator as a diff.
Execution — run_command
Runs in the sandbox and returns stdout, stderr and the exit code. Read-only commands are identified per invocation and may run in parallel; anything that could write runs alone.
Version control — enter_worktree, exit_worktree
enter_worktree creates a git worktree on a new branch and moves the agent into
it. While it is active the sandbox refuses writes outside the worktree, so the
working copy open in your editor is unaffected. exit_worktree returns to the
workspace root and reports anything left uncommitted. Neither deletes the
branch — it remains for review.
Research — web_search, web_fetch
Requires TAVILY_API_KEY. These run in the Forge process rather than in the
sandbox, so an agent can consult documentation while the sandbox itself remains
without network access.
Planning — todo_write
Holds the task list outside the conversation so it survives context compaction. One item may be in progress at a time.
Codebase structure — graph_query, graph_path, graph_overview
Requires the graph extra. Returns an error result when unavailable rather
than failing the task.
Vault — hisar_list, hisar_read, hisar_deposit
Requires HISAR_MACHINE_TOKEN. The owner's file store, reached from the Forge
process rather than the sandbox. Reads are unrestricted; writes are confined to
/SPEDA and /Forge; deleting and renaming are owner-only and have no tool.
Its own group rather than part of coding, so an agent is given a coding
toolset without also being given the owner's documents.
Notification — telegram_send
Requires FORGE_TELEGRAM_TOKEN and FORGE_TELEGRAM_CHAT_ID. Messages the owner
mid-job — a peer run has nobody watching the terminal. Job completion is
reported automatically and needs no tool; this is for the case worth raising
before the job ends. Its own group, for the same reason as the vault.
Both are withheld from the toolset entirely when their credentials are unset, rather than being offered and failing: an agent has no way to tell a missing credential from a transient fault, and will retry a door that was never going to open.
Each task runs one loop:
- Call the model with the conversation and the available tools.
- Execute the tools it requests. Consecutive tools that declare themselves safe to parallelise run together; everything else runs one at a time.
- Append the results and repeat.
The loop ends with a typed outcome: completed, iteration limit reached,
interrupted, or failed. The default ceiling is 30 iterations for optimus.
Failure handling:
- Tool failures return a result marked as an error rather than raising. The model reads it and adapts.
- Transient provider errors are retried with exponential backoff.
- Interrupts are honoured at defined points in the loop. Pending tool results are filled in first so the conversation stays valid for the next task.
Commands and file operations run inside a Cell. Two implementations are
available, selected by FORGE_CELL_BACKEND.
Docker (docker)
- Non-root user, all Linux capabilities dropped
- CPU, memory and process limits
--network noneunless the agent configuration or the job enables it- Only the workspace is mounted
Subprocess (subprocess)
Used when Docker is unavailable. Commands run as child processes with the working directory pinned to the workspace and path traversal rejected.
This is a workspace boundary, not a security boundary. Network access cannot be reliably removed from a plain subprocess; when configuration disallows it, proxy variables are set as a deterrent only. Use the Docker backend when the code being executed is not trusted.
auto selects Docker when the daemon is reachable and subprocess otherwise.
Every tool call passes through a permission engine before it runs.
| Mode | Behaviour |
|---|---|
act |
Operations proceed; gated operations prompt |
plan |
Every mutating operation is denied |
High-impact operations — force pushes, recursive deletes, piping a download into a shell — are gated regardless of mode and cannot be pre-approved through configuration.
At a prompt the operator can allow once, allow for the session, or deny.
Session approvals are recorded and listed by /approved. If no operator is
reachable, the request is denied.
If the workspace contains AGENTS.md — or CLAUDE.md, since many
repositories already have one — it is loaded into the system prompt on
every turn, labelled with its filename so the agent can tell a project's
instructions from its own.
Put the things someone working in the repository has to know and could not infer from a single file: the real build and test commands, where a new module of each kind belongs, conventions the code follows consistently, and anything the project deliberately avoids.
/init has the agent survey the repository and write one. It refuses if a
file already exists; /init force rewrites it.
Keep it short. It is sent on every turn, so a sentence that would not change what an agent does is paying to say nothing. Files over 12,000 characters are truncated with a note.
Each conversation is written to .forge/sessions/ in the workspace after
every completed turn, and can be picked up later:
/sessions list what can be resumed here
/resume 1 continue the most recent
/resume 20260805-133421
A turn is the unit because it is the point at which the transcript is known to be replayable — mid-turn there can be a tool call with no result, which cannot be sent back to a provider.
Resuming restores the conversation only. The sandbox, tools and permissions are rebuilt from the current profile, and file read-tracking starts empty, so the first edit after a resume re-reads the file. A conversation from last week describes files as they were.
The 40 most recent sessions per workspace are kept.
Work an agent does is committed under that agent's name, declared in its profile:
[git]
name = "Optimus Mark II"
email = "optimus@spedatox.systems"This is applied as environment on the sandbox, so it covers every route to a
commit including run_command git commit — not only the /commit command.
Author and committer are both set; setting only the author would record the
operator as having applied a patch they never saw. The repository's own
user.name is left alone.
No account is required. Git stores a name and an address; a host such as GitHub links that to a profile only if the address belongs to a registered account.
A token ledger tracks utilisation. As the conversation approaches the model's limit, Forge reclaims space in two stages:
- Elision — the contents of older tool results are dropped, leaving the surrounding reasoning intact. Frequently sufficient on its own.
- Summarisation — a section of the conversation is replaced by a summary. The original task and the most recent exchanges are preserved verbatim.
After either, read-tracking is cleared: the model's knowledge of file contents is now a summary's, so an edit must read the file again first.
The todo_write task list is restated into the summary, because tasks long
enough to require compaction are the ones with a plan worth keeping.
| Provider | Reference format | Key |
|---|---|---|
| Anthropic | claude-sonnet-4-6 |
ANTHROPIC_API_KEY |
| OpenAI | openai:gpt-5.1 |
OPENAI_API_KEY |
| Gemini | gemini:gemini-2.5-pro |
GEMINI_API_KEY |
| z.ai | zai:glm-4.6 |
ZAI_API_KEY |
| DeepSeek | deepseek:deepseek-v4-pro |
DEEPSEEK_API_KEY |
| Ollama | ollama:llama3.1:8b |
OLLAMA_BASE_URL |
A bare reference is treated as Anthropic. Non-Anthropic providers require the
providers extra.
/model pick from a list, grouped by provider
/model openai:gpt-5.1 switch straight to a reference
/model refresh re-ask the providers (after adding a key)
/model show what is running now
/model asks each provider whose key is set what it currently serves, and
shows the answers grouped under provider headings — so the list is what your
keys can actually reach today rather than a table that goes stale when a model
ships. Arrows move, typing narrows the list, enter selects, escape cancels.
Only configured providers are asked; a provider that fails keeps its heading and shows why, which is the quickest way to find out a key is wrong. Embedding, speech and image models are filtered out, with the count of what was hidden in the heading. Answers are cached for ten minutes.
A typed reference is not checked against that list — a model released this
morning is usable this morning, and the provider is a better referee than a
cache. The switch lasts for the session and takes effect on the next turn, with
the conversation carried over; the profile's model_ref is untouched.
Selection order, highest priority first:
/modelin a session,--modelon the command line, or the model named in a job request- The agent's
vision_model, when the turn carries an image FORGE_LLM_FALLBACK_CHAIN, tried in order when a provider fails- The agent's
profile.toml
An explicit model outranks vision_model deliberately: an operator who named a
model meant it, and being silently overruled is the worse surprise. The picture
is sent to what they picked and refused out loud if that model cannot see it.
Providers differ in what they accept beyond text. An image is carried as an
Anthropic content block internally and translated at the request boundary — to
image_url for the chat-completions family, input_image for OpenAI's
Responses API. A model without vision returns an error rather than answering
about a picture it never received.
Providers differ in how strictly they enforce tool schemas. Anthropic enforces required parameters; others may omit them or substitute different names. Tools should reject incomplete input explicitly rather than applying a default.
An agent is a directory under forge/agents/ containing profile.toml and
system_prompt.md.
agent_id = "optimus"
name = "Optimus"
domain = "systems, code & infrastructure"
model = "deepseek:deepseek-v4-pro"
vision_model = "claude-sonnet-4-6"
tools = ["coding", "web"]
permission_mode = "act"
max_iterations = 30
[cell]
allow_network = false
cpus = 2.0
memory_mb = 2048
timeout_s = 120| Field | Purpose |
|---|---|
agent_id |
Identifier used on the wire and in job requests |
model |
Model reference. Model identifiers live only in profiles. |
vision_model |
Where a turn carrying an image goes instead. Omit if model can already see. |
tools |
Tool groups or individual tool names — see below |
permission_mode |
act or plan |
max_iterations |
Loop ceiling for a single task |
[cell] |
Sandbox resource and network policy |
vision_model is a second model reference rather than a supports_vision flag
because a flag would need a table, in code, of which model identifiers can see —
going stale silently, the same objection this project makes to a built-in price
table. An operator naming the model they want is a fact; a table is a guess with
a shelf life.
tools takes any of these group names, individual tool names, or a mix:
| Group | Contains |
|---|---|
coding |
Navigation, editing, execution, worktrees, planning, graph, delegation |
security |
Navigation, editing and execution only — no graph |
web |
web_search, web_fetch |
memory |
memory — the owner's memory, held by Mark VI |
hisar |
hisar_list, hisar_read, hisar_deposit |
notify |
telegram_send |
The last three are separate groups deliberately. They reach the owner rather than the repository, so an agent is given a coding toolset without also being given their documents, their memory, or a line to their phone.
To add an agent, create the directory, write the two files, and run
forge agents to confirm it loads.
forge connect --agent optimusForge opens an outbound WebSocket to SPEDA Mark VI and registers. Because the connection is outbound, no inbound firewall rule or public address is required.
Requires SPEDA_WS_URL and SPEDA_API_KEY.
The peer accepts chat requests and job dispatches, streams tool activity and output back, and relays permission prompts to the operator's client. A task that fails still sends a terminating frame, so a caller is never left waiting.
forge serve exposes the same job interface as a standalone WebSocket server
without connecting to Mark VI.
One agent may run in several places at once — on the server and on a
workstation. Mark VI keys connections by (agent_id, host), so those coexist
only if each peer says which machine it is:
| Variable | Purpose |
|---|---|
FORGE_HOST |
A stable name for this machine. Blank → the bare hostname. |
FORGE_ROOTS |
Directories this peer will work in, os.pathsep-separated. Blank → anything well-formed for its platform. |
FORGE_ROOTS is how the routing is steered: the owner picks a folder and the
folder identifies the machine. Leave it blank on the server, which should take
anything, and set it on a workstation so only that machine's own projects come
to it.
Advertising is not enforcement. These are routing hints; what a peer will actually do is bounded on the peer, by its own tools and permission gate.
Why this matters for working locally. A workstation peer runs the code in
your working tree while everything Mark VI provides — the owner's memory, the
model picker, images, permission prompts in the operator's client — arrives over
the socket exactly as it does in production. That is the loop that does not
require a deploy to test a change. Set FORGE_HOST before doing it: until each
peer named its machine, every one registered as default and the second to
connect silently displaced the first.
| Variable | Default | Purpose |
|---|---|---|
FORGE_CELL_BACKEND |
auto |
docker, subprocess or auto |
FORGE_CELL_IMAGE |
python:3.12-slim |
Docker image for the sandbox |
FORGE_WORKSPACE_ROOT |
./.forge/workspaces |
Root for per-agent workspaces |
FORGE_ALLOWLIST_PATH |
./.forge/allowlist.json |
Persisted operation approvals |
FORGE_LLM_FALLBACK_CHAIN |
unset | Comma-separated model references |
FORGE_GRAPHIFY_BIN |
PATH lookup | Path to the Graphify executable |
FORGE_AGENT |
optimus |
Default agent |
FORGE_LOG_LEVEL |
INFO |
Logging level |
FORGE_NO_STATUS |
unset | Suppress the status line |
SPEDA_WS_URL |
ws://127.0.0.1:8000/agents/ws/optimus |
Mark VI endpoint |
SPEDA_API_KEY |
unset | Mark VI authentication |
FORGE_HOST |
hostname | Which machine this peer speaks for |
FORGE_ROOTS |
unset | Directories this peer will work in, os.pathsep-separated |
TAVILY_API_KEY |
unset | Required by web_search |
HISAR_MACHINE_TOKEN |
unset | Required by the hisar tools |
HISAR_URL |
https://hisar.spedatox.systems |
Vault endpoint |
FORGE_TELEGRAM_TOKEN |
unset | Bot token; _<AGENT> suffix overrides per agent |
FORGE_TELEGRAM_CHAT_ID |
unset | Destination chat, required with the token |
FORGE_NO_TELEGRAM |
unset | Suppress Telegram entirely |
Provider keys are listed under Model providers.
See .env.example for the annotated list.
| Command | Purpose |
|---|---|
forge chat |
Interactive session in the current directory |
forge connect |
Run as a Mark VI peer |
forge serve |
Standalone job server |
forge agents |
List configured agents |
forge demo |
Offline demonstration; no API key required |
forge/
├── agents/ Agent profiles and system prompts
├── cell/ Sandbox backends (docker, subprocess)
├── gate/ Peer connection and job protocol
├── graph/ Graphify sidecar
├── mcp/ MCP client and tool provider
├── model/ Provider routing
├── skills/ Loadable skill definitions
├── tools/ Tool implementations
├── tui/ Terminal interface
└── warden/ Execution engine, permissions, context management
tests/
pytest -q966 tests. No network access or API key is required; provider calls and sandboxes are substituted.
Private project. Not licensed for redistribution.
