Company OS External Adapter Lane
This is the first Company OS-native external-adapter contract. It is inspired by Paperclip's external adapter, but it is Company OS-native first and makes No Paperclip drop-in claim.
Product Contract
adapter_type = "external" means:
- Company OS does not run a server-side model for this wake.
- Company OS does not emit usage or
costUsdfor this wake. - The external runner does the compute and writes results back through Company OS APIs, run events, task notes, connector sessions, and skills surfaces.
- The lane reuses existing
company_agents.adapter_configrows and the verified-run timeline rather than creating a second ledger.
Capability declaration:
| Capability | Value |
|---|---|
supportsInstructionsBundle | true |
supportsLocalAgentJwt | true |
supportsSkills | true |
requiresMaterializedRuntimeSkills | false |
supportsModelProfiles | false |
The runner, not Company OS, materializes desired skills. This keeps compute and runtime-specific files outside the server while preserving a durable desired state in the Company OS ledger.
Wake Modes
noop is the default. It records a cost-safe wake plan and returns immediately. Use this when an embodied local runner is already polling Company OS.
webhook sends a wake notification to url and then returns. The body shape is intended to be compatible with existing runner workers: payload template plus agent/run/context fields.
bridged is the live-mirroring mode. It is bounded by:
claimGraceMsdefault12000: if no runner claims the wake, degrade to webhook/noop-style completion.bridgedTimeoutMsdefault600000: once claimed, finalize after runner silence.bridgedMaxMsdefault3600000: absolute lifetime cap.pollIntervalMsdefault1500: relay polling cadence.
These defaults are part of the safety contract: bridged wake behavior must not hang runs indefinitely.
Config Schema
The native schema is exposed at:
GET /api/company/external-adapter/contractThe schema has two UI groups:
- Wake:
mode,url,method,headers,payloadTemplate,timeoutMs,claimGraceMs,bridgedTimeoutMs,bridgedMaxMs,pollIntervalMs. - Runner:
model,effort,maxTurnsPerRun,workspaceStrategy,instructionsFilePath.
Runner fields are hints for the external runner. Company OS stores them and reports them, but does not run a model because of them.
Config Diagnostics
The config diagnostics surface is intentionally adapter-specific so operators can test external wake configuration before assigning an agent to the lane.
Diagnostics are exposed at:
POST /api/company/external-adapter/diagnosewith either the config directly or { "adapterConfig": { ... } }.
Diagnostics:
nooppasses with an informational cost-safe check.webhookandbridgedrequire an absolutehttp(s)URL.- invalid URLs and unsupported protocols are errors.
- webhook/bridged configs without auth-like headers warn with
external_webhook_unauthenticated.
Integration Points
The runtime lane now reuses:
company_agents.adapter_typeandadapter_config- markdown agent definitions and inline runtime declarations
- repo and Company OS skills
- connector accounts, sessions, and trigger subscriptions
- run execution events and verified-run output/cost badges
model_gateway.rsfor paid provider budget enforcement
When execute-worker resolves an agent whose adapter_type = "external", it does not start the native LLM worker. It creates a cost-safe agent_runs row with external_system = "external_adapter", server_runs_model = false, emits_usage_or_cost = false, and cost_usd = null, then wakes the external runner.
Runtime Wake API
Company OS can create an external wake directly:
POST /api/company/companies/:company_id/external-adapter/wakeBody fields:
taskId: optional Company OS task id.companyAgentId: optional Company OS agent id.externalRunId: optional idempotency key from the runner side.adapterConfig: external adapter config, using the same shape as diagnostics.actor: operator or agent actor name.prompt: task prompt sent to the runner.context: structured task/company context.instructionsBundle: desired instructions for the external runner.skills: desired skills for the external runner to materialize.
The wake creates these receipts in run_execution_events:
external_adapter_wake_createdexternal_adapter_wake_sentwhen a webhook/bridged delivery succeedsexternal_adapter_wake_failedwhen delivery fails
noop mode records the wake and returns without sending HTTP. webhook and bridged send a bounded request to the configured URL with redirects disabled. Invalid field types or enum values, HTTP methods, header names, header values, and non-string header maps are rejected before a run is created; the diagnostics endpoint reports the same config/request-shape errors. Run creation and external_adapter_wake_created commit in one transaction. Delivery metadata and each sent/failed receipt also commit together. Transport timeouts and response resets are recorded as wake_delivery_state = "unknown" without terminalizing the run, because the remote may have accepted the wake; a later runner callback atomically promotes that state to confirmed_by_runner. Idempotent replay is rejected while delivery proof is pending or unknown, and each idempotency key is bound to the original task, agent, configuration, and payload identity.
Runner Relay API
External runners write their lifecycle back into Company OS with:
POST /api/company/companies/:company_id/agent-runs/:run_id/external-adapter/claim
POST /api/company/companies/:company_id/agent-runs/:run_id/external-adapter/progress
POST /api/company/companies/:company_id/agent-runs/:run_id/external-adapter/completeUse one stable, non-empty runnerId for the whole lifecycle:
{ "runnerId": "velora-runner-01" }After claiming, send progress as the heartbeat (optionally with progress, message, and a non-secret payload) and finish through complete with the same runnerId and a terminal status. The claim is exclusive under the run-row lock: simultaneous claimers produce one winner and one 409; once an owner exists, another runner cannot report progress or complete the run.
The supported local client reads transport and principal credentials from HSM_COMPANY_API_BEARER_TOKEN (or its private bearer file) and HSM_COMPANY_PRINCIPAL_TOKEN; credentials are never command arguments:
bash scripts/company-os external-runner claim \
--company-id "$COMPANY_ID" --run-id "$RUN_ID" --runner-id velora-runner-01
bash scripts/company-os external-runner progress \
--company-id "$COMPANY_ID" --run-id "$RUN_ID" --runner-id velora-runner-01 --progress 0.5
bash scripts/company-os external-runner complete \
--company-id "$COMPANY_ID" --run-id "$RUN_ID" --runner-id velora-runner-01 --status success
bash scripts/company-os external-runner reconcile --company-id "$COMPANY_ID"These endpoints only accept runs whose metadata marks them as execution_mode = "external_adapter".
Runner receipts:
external_adapter_runner_claimedexternal_adapter_runner_progressexternal_adapter_runner_completed
claim and progress keep the run in running. A legacy integration that never calls claim remains accepted, but new runners should always claim so ownership is enforced. complete maps runner status to success, cancelled, or error, stamps finished_at, and records execution_verified = true only for successful completion. The run mutation and its runner receipt are one transaction and require the exact company-bound external run to still be running; terminal replay and late progress cannot rewrite a completed result.
An operator stop (run interrupt, agent pause, or the company kill switch) stamps cancel_requested_at but leaves the run running so dispatched work can settle. After a stop, claim is refused with 409external_adapter_claim_cancel_requested and a refusal receipt. progress and complete are still accepted, and every accepted runner event response carries cancel_requested. A runner that reads cancel_requested: true should start no new work and finish with complete --status cancelled (or the real outcome of work already in flight). Company OS cannot recall an effect the runner has already performed.
Bridged Recovery API
Bridged runs can be swept explicitly:
POST /api/company/companies/:company_id/external-adapter/reconcileThe reconciler scans running external_adapter runs whose wake plan mode is bridged and finalizes stranded work with operator-readable receipts:
external_adapter_claim_timeout: no runner claimed the wake beforeclaimGraceMs.external_adapter_idle_timeout: a claimed runner stopped reporting progress beforebridgedTimeoutMs.external_adapter_max_lifetime_timeout: the run exceededbridgedMaxMs.
Timed-out runs are marked error, stamped with finished_at, and patched with reconcile_reason, terminal_status = "error", and execution_verified = false in external-adapter metadata.
Paperclip-Parity Runtime Surfaces
Company OS now exposes productized runtime surfaces for the remaining Paperclip-style gaps while keeping company_agents, agent_runs, tasks, skills, spend, and receipts as the canonical ledgers:
- external adapter install/register/migrate/verify:
POST /api/company/companies/:company_id/external-adapter/install,POST /api/company/companies/:company_id/external-adapter/:install_id/migrate-agent, andPOST /api/company/companies/:company_id/external-adapter/:install_id/verify - runner authoring guarantee: migrated external agents carry desired instructions, skills, capabilities, and local writeback-auth requirements in
adapter_config - canonical role contract: migrated agents receive a role-owned
role_instruction_contractwith owns/does-not-own scope - first-class routines:
/api/company/companies/:company_id/routinesand/api/company/companies/:company_id/routines/:routine_id/runs - per-agent monthly budgets:
/api/company/companies/:company_id/agent-budgets/status, plus 80 percent warning and 100 percent hard pause in runtime budget enforcement - unified receipts:
/api/company/companies/:company_id/receiptscombines run events, governance events, and spend events - skills store:
/api/company/companies/:company_id/skills-storeand/api/company/companies/:company_id/skills-store/install - sandbox provider plugin registry:
/api/company/companies/:company_id/sandbox-providers - tenant isolation audit:
/api/company/companies/:company_id/tenant-isolation/audit
SearXNG Search Adapter
Company OS also has a concrete search-only external adapter path:
- tool:
company_web_search - primary backend: SearXNG JSON API when
SEARXNG_URLorsearxng_urlis set - fallback backend: existing
web_searchwhen SearXNG is missing, unreachable, returns invalid JSON, exceeds response limits, or fails HTTP - bounded behavior: max 5 SearXNG pages, max 50 returned results, 10 second request timeout, and a 2 MiB JSON body cap
- supported search filters:
category/categories,language,time_range, andsafesearch
Direct curl shape:
curl -sS \
"http://127.0.0.1:8888/search?q=agent+memory&format=json&categories=science&pageno=1"Minimal Python shape:
import requests
base = "http://127.0.0.1:8888"
results = []
for page in range(1, 4):
response = requests.get(
f"{base}/search",
params={
"q": "agent memory",
"format": "json",
"categories": "science",
"pageno": page,
},
timeout=10,
)
response.raise_for_status()
payload = response.json()
page_results = payload.get("results", [])
if not page_results:
break
results.extend(page_results)Inside Company OS, agents should call:
{
"query": "latest pgvector indexing guidance",
"category": "science",
"pages": 2,
"limit": 12
}If SEARXNG_URL is not configured, or the self-hosted instance is down, company_web_search returns a successful fallback result when the normal web_search provider works and includes fallback metadata so the run receipt still shows what happened.
Web Extract Companion
SearXNG is search-only, so Company OS pairs it with a separate extraction tool:
- tool:
company_web_extract - preferred backend: Firecrawl via
FIRECRAWL_API_KEYor self-hostedFIRECRAWL_API_URL - fallback backend: bounded direct HTTP extraction when Firecrawl is not configured or
backend = "direct"is requested - bounded behavior: max 5 URLs per call, 2 MiB direct-fetch body cap, no automatic redirects, and 5k returned characters per URL by default
- backend override:
backend = "auto" | "firecrawl" | "direct"orHSM_WEB_EXTRACT_BACKEND
Typical Company OS agent flow:
{
"tool": "company_web_search",
"params": {
"query": "latest pgvector indexing guidance",
"category": "science",
"pages": 2,
"limit": 12
}
}Then extract the chosen result URLs:
{
"tool": "company_web_extract",
"params": {
"urls": ["https://example.com/relevant-result"],
"backend": "auto",
"max_chars": 8000
}
}Paperclip-Parity Backlog
The SearXNG and runtime wake slices close the self-hosted search and no-model external-runner dispatch gaps, but they do not claim full Paperclip parity. The remaining important slices are:
- bridged lifecycle productization: scheduling the reconcile sweep from a daemon, UI surfacing for stranded wakes, and DB-backed smoke tests
- routines: first-class routine definitions, routine runs, env/secrets, coalescing, wakeups, UI history, and broad action types
- per-agent budgets: monthly spend by agent ref, 80 percent warning, 100 percent hard pause, override flow, and external-runner cost events
- role instructions: every hired role should own a canonical instruction/skill bundle with explicit owns/does-not-own scope and assignment validation
- receipts: task, routine, handoff, tool calls, comments, spend, output, policy, and adapter events should collapse into one operator-readable audit timeline
- install UX: install/register adapter, inspect capabilities, attach skills, migrate agent, diagnose config, and verify runner health from the product UI