14 KiB
Automation System — Context Document
This document is written for AI assistants working on this codebase. It captures architectural decisions, the full vision, and the current state of the automation/tools system built during a long design and implementation session. Read this before touching anything in src/servers/api/pi/, src/servers/api/skills/, src/servers/api/tasks/, src/servers/api/processes/, seed/tools/, seed/extensions/, or src/servers/sync-*.ts.
What Officer Is
Officer is a self-hosted AI-native intranet for small and medium businesses. Every user gets a personal AI assistant, terminal, file browser, code editor, workspaces, and automation tools. The platform is multi-user with role-based access: Member → Admin → Owner → Super Admin.
The AI backend was originally Claude SDK / OpenCode. It has been migrated to Pi (@mariozechner/pi-coding-agent) as the primary agent harness, running in RPC mode as a subprocess managed by src/servers/api/pi/pi-bridge.ts.
The Automation Ladder
The owner has a clear conceptual model for automation capabilities, ordered from atomic to orchestrated:
| Rung | Description | Status |
|---|---|---|
| Skills | Passive markdown documentation the agent reads as context. Reference for CLIs, APIs, services. | ✅ Implemented |
| Tools | Executable TypeScript functions Pi can call directly during agent execution. Registered via Pi extensions. | ✅ Implemented |
| Tasks | Structured instructions (TASK.md) to accomplish an atomic goal. Uses skills + tools. Has inputs, triggers, steps. | ✅ Skeleton exists |
| Pipelines | A linear sequence of tasks, run one after the other (think pipe). | 🔲 Phase 2 |
| Processes | A sequence of tasks that forks based on the result of the previous one (conditional branching). | 🔲 Phase 2 |
| Workflows | Arbitrary task graphs, like n8n. | 🔲 Phase 3 |
| Services | Event-driven execution — watches directories or events, triggers tasks based on configuration. | 🔲 Phase 3 |
| Crons | Scheduled execution of a single task, pipeline, process, or workflow. | 🔲 Phase 3 |
When the owner says "tool", they mean any rung of this ladder, not just the Pi tool concept.
Key distinction: agent-interpreted vs headless execution
Currently all tasks are agent-interpreted — the agent reads the TASK.md and reasons about how to execute it. In phase 2, well-defined tasks (deterministic inputs → tool call → result) should be executable headlessly without an LLM, directly by a task runner. The tool implementations are already standalone TypeScript functions that support this future — they have no dependency on Pi or the agent.
How Pi Is Integrated
Pi runs as a subprocess in RPC mode (pi --mode rpc). The server communicates via stdin/stdout JSON. See src/servers/api/pi/pi-bridge.ts and src/servers/api/pi/websocket.ts.
Spawning Pi (host, non-sandboxed)
pi --mode rpc
--no-skills --no-prompt-templates --no-themes
--skill /data/skills/{name} (one per global skill)
--skill /data/{email}/skills/{name} (one per user skill)
--extension /data/extensions/{name}/index.ts
--model {model}
env:
PI_CODING_AGENT_DIR = DATA_PATH/pi-config
PI_TOOLS_DIRS = DATA_PATH/tools:DATA_PATH/{email}/tools
PI_SEARXNG_URL = https://searxng.home.pastilhas.eu
+ all stored API keys
Note: --no-extensions was intentionally removed to allow our tool-loader extension to work. --no-skills is kept because we pass skills explicitly via --skill flags to control scope correctly.
Spawning Pi (sandboxed, Docker container)
Same pattern but via docker exec, using container-side paths:
docker exec -i -w {workdir} \
-e PI_CODING_AGENT_DIR=/home/{username}/.pi/agent \
-e PI_TOOLS_DIRS=/officer/tools:/officer/user/tools \
-e PI_SEARXNG_URL=... \
-e {API_KEYS} \
{containerId} \
pi --mode rpc --no-skills --no-prompt-templates --no-themes \
--skill /officer/skills/{name} \
--skill /officer/user/skills/{name} \
--extension /officer/extensions/tool-loader/index.ts
Directory Structure
DATA_PATH/ # Default: ./data, override with DATA_PATH env
├── pi-config/ # Pi's global config (models.json, settings.json)
├── skills/ # Global skills (org-wide)
├── tools/ # Global tools (org-wide)
├── extensions/ # Global extensions (managed by server, not users)
├── {email}/
│ ├── home/ # Mounted as /home/{username} in Docker container
│ ├── skills/ # User-specific skills
│ ├── tools/ # User-specific tools
│ └── extensions/ # User-specific extensions (future)
└── searxng.json # SearXNG instance URL config
seed/ # Bundled with the app (read-only source of truth)
├── skills/ # Native/officerdev skills
├── tools/ # Native/officerdev tools
├── extensions/ # Native/officerdev extensions
│ └── tool-loader/index.ts # The Pi extension that registers tools
└── tasks/ # Native/officerdev tasks
Sync on server start (src/servers/bootstrap.ts)
syncSeedSkills()→ copiesseed/skills/*→DATA_PATH/skills/(skip if exists, preserves user edits)syncSeedTools()→ copiesseed/tools/*→DATA_PATH/tools/(skip if exists)syncSeedExtensions()→ copiesseed/extensions/*→DATA_PATH/extensions/(always overwrites — extensions are server-managed code, not user-editable)
The Tool System
How tools are defined
Each tool lives in a directory with two files:
seed/tools/web-fetch/
├── TOOL.md # Metadata + input schema (read by tool-loader, registered with Pi)
└── index.ts # Implementation (lazy-loaded when the tool is actually called)
TOOL.md frontmatter format:
---
name: tool_name # Pi tool name (snake_case)
label: Tool Label # Human-readable label
description: ... # What Pi sees in its context (keep concise — this is in every session prompt)
language: typescript # typescript | bash | python
inputs:
param_name:
type: string # string | number | boolean | enum
description: ...
optional: true # omit if required
mode:
type: enum
values: single,batch # comma-separated for enum (parser limitation)
description: ...
---
index.ts export signature:
export async function execute(
toolCallId: string,
params: { [key: string]: any },
signal: AbortSignal | undefined,
onUpdate?: (partial: { content: Array<{ type: string; text: string }> }) => void,
): Promise<{ content: Array<{ type: string; text: string }>; details?: object; isError?: boolean }>
The onUpdate callback streams progress to the agent. Use it liberally — the frontend shows it in real time.
How the tool-loader extension works (seed/extensions/tool-loader/index.ts)
- Loaded by Pi as an extension via
--extensionflag - Reads
PI_TOOLS_DIRSenv var (colon-separated list of directories) - Discovers
TOOL.mdin each directory, parses frontmatter, builds TypeBox schema - Registers each tool synchronously in the extension factory function (so tools appear in the system prompt)
- Lazy-loads
index.tsimplementation only when the tool is actually called - User dirs come after global — later registration wins (user tools override global by name)
Existing tools
| Tool | Location | Description |
|---|---|---|
web_fetch |
seed/tools/web-fetch/ |
Fetch URL content. Tries .md suffix → /llms.txt → plain text → HTML strip |
web_search |
seed/tools/web-search/ |
Search via SearXNG. Returns titles, URLs, snippets. Pair with web_fetch |
convert_audio_to_mp3 |
seed/tools/convert-audio-to-mp3/ |
Convert audio to MP3 via ffmpeg. Single file (% progress) or batch directory (per-file progress) |
Adding a new tool
- Create
seed/tools/{name}/TOOL.mdandseed/tools/{name}/index.ts - Restart server —
syncSeedTools()copies it toDATA_PATH/tools/ - New Pi sessions automatically get the tool registered
Docker Container Setup
Each user has a persistent Docker container (officer-terminal-{userId}). Containers are managed by src/servers/api/terminal/websocket.ts.
Read-only resource mounts
Added to every container at creation time:
/officer/skills → DATA_PATH/skills (global skills, ro)
/officer/tools → DATA_PATH/tools (global tools, ro)
/officer/extensions → DATA_PATH/extensions (global extensions, ro)
/officer/user/skills → DATA_PATH/{email}/skills (user skills, ro)
/officer/user/tools → DATA_PATH/{email}/tools (user tools, ro)
The user's home directory (DATA_PATH/{email}/home) is mounted read-write as /home/{username}.
Mount migration
ensureDockerContainer checks containerHasResourceMounts() before reusing an existing container. If the mounts are missing (old container created before this feature), the container is removed and recreated automatically. This is a one-time migration.
Scope & Permission Model
All automation entities (skills, tasks, processes, and future rungs) follow the same three-tier scope model:
| Scope | Location | Who creates | Who can see |
|---|---|---|---|
native |
seed/ (read-only) |
officerdev (us) | Everyone |
global |
DATA_PATH/{type}/ |
Admins+ | Everyone |
user |
DATA_PATH/{email}/{type}/ |
Anyone | Owner + SAs (future) |
Current API behavior (skills, tasks, processes)
function isPrivileged(role: string) {
return role === 'Admin' || role === 'Owner' || role === 'Super Admin';
}
POST /(create): Members → user scope. Admins+ → global scope.DELETE /:name: Members can only delete their own (user scope). Admins+ can delete global.PUT /:name/chat/DELETE /:name/chat: Members blocked from writing to native/global entries.
Full vision (phase 2/3 — not yet implemented)
- Super Admins can see all users' personal items (currently everyone only sees their own)
- Members can submit their personal item for SA review (
status: draft | submitted | approved | rejected— a flag in frontmatter, not a new scope) - SAs can promote any user's item to global — this is a move (not a copy), item leaves user scope
- SAs can see and use everyone's tools
- The
CapabilitySummarytype already hasscopesurfaced in the frontend list with a badge
SearXNG Integration
- Config stored in
DATA_PATH/searxng.json - Default URL:
https://searxng.home.pastilhas.eu(hardcoded default insrc/servers/api/server-settings/searxng.ts) - API:
GET /api/server-settings/searxng,PUT /api/server-settings/searxng - Injected into Pi processes as
PI_SEARXNG_URLenv var (both host and container) - UI configuration is a future task — the API is already there
Attribution & Marketplace Vision (future)
The owner has a clear attribution model:
- Native/officerdev:
officerdev/<name>— built-in, shipped with the product - User-created org tools: attribution via
author: <email>in frontmatter - Marketplace — officerdev official:
officerdev/<name> - Marketplace — third party:
<dev>/<name>or<organization>/<name>
Future marketplace will cover: Skills, Tools, Tasks, Pipelines, Processes, Workflows, Services, Crons, Widgets, Apps, and Themes. Widgets and Apps are already bootstrapped in the platform. Each self-hosted instance will connect to a central webstore.
The native scope covers both built-in and future marketplace-installed items. When marketplace is implemented, native may split into native (local seed) and marketplace (installed from store, with version + source metadata).
Files Changed / Created
New files
src/servers/sync-tools.ts— mirrors sync-skills pattern for toolssrc/servers/sync-extensions.ts— same for extensions (always overwrites)src/servers/api/server-settings/searxng.ts— SearXNG config routerseed/tools/web-fetch/TOOL.md+index.tsseed/tools/web-search/TOOL.md+index.tsseed/tools/convert-audio-to-mp3/TOOL.md+index.tsseed/extensions/tool-loader/index.ts
Modified files
src/servers/data-path.ts— added tool/extension dir helperssrc/servers/bootstrap.ts— calls syncSeedTools, syncSeedExtensionssrc/servers/api/pi/pi-bridge.ts— removed--no-extensions, added extension/tool flags, PI_TOOLS_DIRS, PI_SEARXNG_URL, container-aware path supportsrc/servers/api/terminal/websocket.ts— added 5 read-only resource mounts to containers, mount migration checksrc/servers/api/skills/skills.ts— fixed scope-aware POST/DELETE/chat endpointssrc/servers/api/tasks/tasks.ts— same fixessrc/servers/api/processes/processes.ts— same fixessrc/servers/api/server-settings/server-settings.ts— registered searxng routerseed/skills/convert-audio-to-mp3/SKILL.md— removed hardcoded paths, references tool insteadseed/tasks/convert-to-mp3/TASK.md— updated to use tool, addedtools:frontmatter fieldsrc/apps/officer-web/Screens/Dashboard/CapabilityPage.tsx— fixedscopetype to include'native'
What To Do Next (Rough Priority)
- Wire
tool_execution_updateevents throughpi-bridge.ts→ websocket → frontend soonUpdateprogress actually shows in the chat UI - Container support testing — verify tools and extensions work correctly inside Docker after the read-only mount changes
- SA cross-user visibility — API + UI for Super Admins to see all users' personal items
- Submit/approve workflow —
statusflag in frontmatter, submission UI for members, review UI for SAs - SearXNG UI — settings panel to configure the URL (API is already done)
- Headless task runner — execute deterministic tasks without an agent (phase 2)