Skip to content

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 costUsd for 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_config rows and the verified-run timeline rather than creating a second ledger.

Capability declaration:

CapabilityValue
supportsInstructionsBundletrue
supportsLocalAgentJwttrue
supportsSkillstrue
requiresMaterializedRuntimeSkillsfalse
supportsModelProfilesfalse

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:

  • claimGraceMs default 12000: if no runner claims the wake, degrade to webhook/noop-style completion.
  • bridgedTimeoutMs default 600000: once claimed, finalize after runner silence.
  • bridgedMaxMs default 3600000: absolute lifetime cap.
  • pollIntervalMs default 1500: 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:

text
GET /api/company/external-adapter/contract

The 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:

text
POST /api/company/external-adapter/diagnose

with either the config directly or { "adapterConfig": { ... } }.

Diagnostics:

  • noop passes with an informational cost-safe check.
  • webhook and bridged require an absolute http(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_type and adapter_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.rs for 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:

text
POST /api/company/companies/:company_id/external-adapter/wake

Body 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_created
  • external_adapter_wake_sent when a webhook/bridged delivery succeeds
  • external_adapter_wake_failed when 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:

text
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/complete

Use one stable, non-empty runnerId for the whole lifecycle:

json
{ "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
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_claimed
  • external_adapter_runner_progress
  • external_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:

text
POST /api/company/companies/:company_id/external-adapter/reconcile

The 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 before claimGraceMs.
  • external_adapter_idle_timeout: a claimed runner stopped reporting progress before bridgedTimeoutMs.
  • external_adapter_max_lifetime_timeout: the run exceeded bridgedMaxMs.

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, and POST /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_contract with owns/does-not-own scope
  • first-class routines: /api/company/companies/:company_id/routines and /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/receipts combines run events, governance events, and spend events
  • skills store: /api/company/companies/:company_id/skills-store and /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_URL or searxng_url is set
  • fallback backend: existing web_search when 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, and safesearch

Direct curl shape:

bash
curl -sS \
  "http://127.0.0.1:8888/search?q=agent+memory&format=json&categories=science&pageno=1"

Minimal Python shape:

python
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:

json
{
  "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_KEY or self-hosted FIRECRAWL_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" or HSM_WEB_EXTRACT_BACKEND

Typical Company OS agent flow:

json
{
  "tool": "company_web_search",
  "params": {
    "query": "latest pgvector indexing guidance",
    "category": "science",
    "pages": 2,
    "limit": 12
  }
}

Then extract the chosen result URLs:

json
{
  "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

Company OS documentation — built with VitePress