Porter exposes individual tools directly to LLM agents, filtered by role. Each agent sees only the tools its role permits. AS2 (ActivityStreams 2.0) is used internally as the bus wire format but is fully transparent to agents -- they send and receive plain text messages.
Porter previously used a 4-tool gateway pattern (execute, file,
message, memory) that collapsed 12+ individual tools behind 4
aggregated gateway tools with sub-action discriminators. This was dissolved
in favor of exposing individual tools directly, which works better with
smaller models and avoids the cognitive overhead of sub-action routing.
Run a shell command. Full shell access with git, deno, node, and
standard unix tools. Environment variables like $GITLAB_TOKEN are
available.
bash({command: "ls -la src/"})
bash({command: "deno test --allow-all"})
| Param | Required | Description |
|---|---|---|
command |
yes | Shell command to run |
Read the contents of a file.
read_file({path: "src/app.ts"})
| Param | Required | Description |
|---|---|---|
path |
yes | File path (relative to working directory or absolute) |
Create or overwrite a file.
write_file({path: "src/hello.ts", content: "export const hi = 'world';"})
| Param | Required | Description |
|---|---|---|
path |
yes | File path |
content |
yes | File content |
Apply a find-and-replace edit to a file.
edit_file({path: "src/app.ts", old_string: "old text", new_string: "new text"})
| Param | Required | Description |
|---|---|---|
path |
yes | File path |
old_string |
yes | Text to find |
new_string |
yes | Replacement text |
Find files matching a glob pattern.
glob({pattern: "src/**/*.ts"})
| Param | Required | Description |
|---|---|---|
pattern |
yes | Glob pattern |
path |
no | Base directory (defaults to working directory) |
Search file contents for a pattern.
grep({pattern: "TODO", path: "src/"})
| Param | Required | Description |
|---|---|---|
pattern |
yes | Search pattern |
path |
no | Directory to search (defaults to working directory) |
List files and directories.
list_dir({path: "src/"})
| Param | Required | Description |
|---|---|---|
path |
no | Directory path (defaults to working directory) |
Run a git command.
git({command: "status"})
git({command: "diff HEAD~1"})
| Param | Required | Description |
|---|---|---|
command |
yes | Git subcommand and arguments |
Send a message to another agent or channel on the bus.
send_message({channel: "task:worker-1", message: "Implement the login form"})
send_message({channel: "log", message: "All tasks assigned"})
| Param | Required | Description |
|---|---|---|
channel |
yes | Channel: log, task, review, control, or task:<agent-name> |
message |
yes | Message text |
Check for incoming messages from subscribed channels.
read_messages({})
read_messages({channel: "task"})
| Param | Required | Description |
|---|---|---|
channel |
no | Filter to a specific channel |
Store an observation in the shared knowledge graph.
memory_write({about: "architecture", finding: "Using Deno + Oak for the API"})
| Param | Required | Description |
|---|---|---|
about |
yes | Topic or subject |
finding |
yes | Fact or observation to store |
Query the shared knowledge graph using SPARQL.
memory_query({sparql: "SELECT ?about ?finding WHERE { ?obs <https://porter.chapeaux.io/vocab#about> ?about ; <https://porter.chapeaux.io/vocab#finding> ?finding }"})
| Param | Required | Description |
|---|---|---|
sparql |
yes | SPARQL query string |
The applyRoleFilter() function in src/runtime/agent.ts restricts tool
access based on the agent's role:
| Role | Available Tools | Rationale |
|---|---|---|
admin |
send_message, read_messages, memory_write, memory_query |
Admins plan and delegate; they do not execute directly |
worker |
all 12 tools | Workers have full capabilities |
reviewer |
all 12 tools | Reviewers can read code and run tests |
When an agent attempts to use a tool outside its role, the error includes the names of other agents that have the required capability:
You (admin-1) cannot use 'bash'. Your available tools are: send_message, read_messages, memory_write, memory_query.
To use 'bash', delegate to: worker-1, worker-2.
Example: send_message({channel: "task:worker-1", message: "Please run: ..."})
This teaches the agent to delegate rather than retry the forbidden tool.
AS2 (ActivityStreams 2.0) is used as the internal wire format on the
message bus. It is managed entirely by the send_message and
read_messages tool handlers in executeTool():
- send_message wraps plain text messages in an AS2 envelope before
publishing to the bus. Messages to
task:*channels get typeOffer; other channels get typeAnnounce. - read_messages unwraps AS2 JSON back to plain
actor: summaryformat before returning to the agent. Non-JSON messages fall through as raw text.
Agents never see or construct AS2 JSON. They send and receive plain text.
When a tool call produces a "not found" or "No such file or directory"
error, the executeTool() function automatically stores the error in the
shared memory graph via memory_write. Other agents can query the graph
to discover known failures before attempting similar operations.
The executeTool() function resolves relative file paths against the
session working directory:
bashandgitcommands receive the working directory ascwdglob,grep, andlist_dirreceive it as the basepathwrite_file,edit_file, andread_filehave relative paths prefixed with the working directory; absolute paths are passed through unchanged
When sandbox mode is enabled (sandbox: true in config or --sandbox CLI
flag), tools are restricted to the workspace directory:
When Porter is already running inside a container (e.g., an OpenShift pod
provisioned by porter router), the container sandbox is automatically
skipped -- the pod itself provides isolation. Path validation for
Deno-native tools still applies in all environments.
read_file, write_file, edit_file, glob, grep, and list_dir all
validate paths via validatePath() from src/sandbox/paths.ts. The
validator resolves symlinks, normalizes traversals, and confirms the final
path is within the workspace. Attempts to access paths outside the workspace
return a PathEscapeError.
bash and git commands run inside a podman/docker container with only the
workspace directory bind-mounted at /workspace. The host filesystem,
~/.ssh, ~/.gnupg, and host environment variables are not accessible
inside the container. Only explicitly configured session env vars (e.g.,
GITLAB_TOKEN) are passed through.
The chat_endpoint field on a model configuration overrides the default API
path used for chat completions. This is useful for providers that use
non-standard paths:
- Vertex AI:
/v1/projects/{project}/locations/{region}/publishers/anthropic/models/{model}:streamRawPredict - Custom gateways: any path that the gateway expects
The chat_endpoint is passed to the provider constructor and used as the
request path instead of the default /v1/messages (Anthropic) or
/v1/chat/completions (OpenAI-compatible).
At session startup, the orchestrator seeds the shared knowledge graph with
observations about each team member (team:<name> with role and tools) and
a team:porter-ui entry identifying the human dashboard user. Agents can
query this via memory_query with a SPARQL filter on team: subjects.