Bring Your Own Agent
Modulo can run an agent image you already own. You keep your agent; Modulo provides the governed workspace, the identity it runs under, the schedule it runs on, and the audit trail around it. Your agent runs the same interface a first-party runner image satisfies, so nothing in your pipelines changes when you point a node at your own image.
The agent interface
Your image must satisfy four things:
- It provides a POSIX
sh. Modulo’s file operations run through a shell, so a shell must exist in the image. - Modulo launches one process. Modulo writes the prompt and the context files into the workspace, then starts one process from the command the agent declares and runs it to completion.
- Modulo observes the exit code. The exit code is the only signal that decides the node’s status.
- It may write one JSON document. The process may write a single JSON object to
/home/user/output.json. Nothing else is read.
There is no protocol, no callback endpoint, no sidecar, and no configurable output path. The workspace holds no credential back to the Modulo control plane. The output file and the exit code together cover reporting, so no second channel is needed.
The output file
The output file is optional. If you write it, no key is required. Modulo surfaces these conventional keys, and any agent that writes them gets uniform treatment in the run detail:
| Key | Type | What Modulo does with it |
|---|---|---|
status |
string | Surfaces the agent’s own verdict verbatim as agent_status. It never changes the node’s status. |
outcome |
string | Surfaces the agent’s own verdict verbatim as agent_outcome. It never changes the node’s status. |
summary |
string | Surfaced as the node’s result summary. |
changed_files |
array of strings | Surfaced on the node output for review. |
pr_url |
string | Surfaced as a delivered pull request (PR) URL, marked agent-reported and unverified. |
Pipeline config
{
"status": "success",
"outcome": "opened-pr",
"summary": "Updated the retry policy and opened a pull request.",
"changed_files": ["backend/src/modulo/core/retry.py"],
"pr_url": "https://github.com/your-org/your-repo/pull/123"
}
Anything beyond these keys is yours to model. Declare an output schema to make it machine-readable.
Declaring the command
A sandbox agent node runs the command on its agent. In the default llm mode a node needs a non-empty agent_commands list and a prompt; in script mode it needs a non-empty script_command. The graph validator rejects a node that is missing its command (SANDBOX_MISSING_COMMAND). The items in agent_commands are joined into one shell command at run time.
Output schemas and the strict and lenient modes
An agent or a pipeline node can declare an output schema. The schema is pinned to a version and validated against the payload your agent writes. You can also declare no schema, in which case the envelope is accepted as a free-form JSON object.
Validation runs in one of two modes, chosen by an operator for the instance or for one organization:
| Mode | What it enforces |
|---|---|
lenient (default) |
Missing required fields still fail the node. Violations of the other constraints (types, enums, patterns, formats, bounds, and composition) are logged as warnings, and the payload passes. |
strict |
Any violation fails the node. Modulo attempts a repair pass before it gives up. |
The mode is resolved in this order: the organization’s setting, then the instance-wide setting, then the MODULO_SCHEMA_VALIDATOR_MODE environment variable as a first-boot seed, then lenient.
On a schema failure the node fails, the raw output is retained for diagnosis, and the run carries a contract schema error (contract.schema).
Where the image runs
Every workspace image resolves through one channel: the environment profile bound to the pipeline. The profile’s image_ref names the image, and the profile’s provider_type decides where it runs. The image is org-owned on every tier, so you control the supply chain wherever the work lands.
| Tier | provider_type |
Where your image comes from |
|---|---|---|
| Bundled Runner (Docker) | runner_docker |
The deployment’s own Docker engine, behind the shipped filtered socket proxy. |
| Remote Docker | runner_docker |
A Docker engine you own, reached through MODULO_DOCKER_HOST. |
| Kubernetes | kubernetes |
Your cluster and your registry. The image never leaves your boundary. |
| E2B | e2b |
A template in your own E2B account. The profile’s image_ref is the template ID. |
On Kubernetes, the profile must declare image_ref, and the image must provide a POSIX shell because the dispatcher’s file operations run through the exec subresource. Attach image pull secrets to the workspace ServiceAccount if your image is private. The Kubernetes guide covers the profile, the ServiceAccount, and the least-privilege Role.
Pin your images by digest for a reproducible supply chain. Modulo resolves whichever image_ref you set; digest pinning is advice, not something the platform enforces. Execution Environments describes each provider in full.
What Modulo observes and what your agent reports
Modulo separates what it observed from what your agent claimed. Every fact carries one of four provenance values:
| Value | Meaning |
|---|---|
observed |
Modulo saw it directly through a credential it holds. |
derived |
Modulo computed it from observed facts. |
reported |
It arrived from an external surface, including your agent’s output file. |
attested |
A governance event Modulo recorded itself. |
For a bring-your-own agent run:
- The process exit code and the wall-clock time are
observed. - The node’s terminal status is
derivedfrom the exit code. - Everything read from
/home/user/output.jsonisreported. Your agent’sstatus,outcome,summary,changed_files, andpr_urlare the agent’s claims; Modulo did not measure them. - Modulo’s own approvals, guardrail verdicts, and human-in-the-loop (HITL) decisions are
attested.
A reported datum also carries a witnessed_via stamp naming the channel it arrived through.
The default is optimistic: Modulo records the provenance and does not block a run because a fact was reported. There is no built-in refusal mechanism. If you want to enforce a claim, compose primitives you already have: fetch the truth with a connector query and let a gate compare it against what the agent said. An agent that reports truthfully meets no friction.
How failures and partial results surface
The exit code decides the node’s status, and there is no synthetic completion:
| Situation | Outcome |
|---|---|
| Non-zero exit | The node fails. Retries and terminal classification follow the node’s policy, and the raw output streams are retained. |
Exit 0, but output.json is missing, empty, or unparseable |
The node fails with sandbox.no_output_json. Modulo never invents a completion. |
| Exit 0, but the output is a lost-session marker | The node fails. |
Exit 0, a valid output.json, no declared schema |
The node completes and the envelope is surfaced as it is. |
Exit 0, a valid output.json, a schema violation |
The node fails per the strict and lenient modes above, and the content is retained. |
A partial result, meaning a failed run that still wrote a readable output.json, is kept for diagnosis and surfaced with its provenance intact. It never turns a failed node into a completed one.
Related
- Agents: the atomic unit of pipeline work and its configuration fields
- Execution Environments: every runtime provider and how to choose between them
- Kubernetes: run workspaces as pods in your own cluster
- Schema System: versioned input and output definitions that validate agent output
- Secrets: how connector and model credentials are encrypted at rest
- Troubleshooting: common setup issues and fixes