openvibers/openvibe.actor README.md

README.md

main 35.5 KB · 272 lines · markdown

OpenVibe.Actor

Your agent for everything: give a task in plain words, and Actor sends it to the best agent for it — OpenVibe's own, acting through every OpenVibe service, or another platform's — has a second model check the answer, and shows you what it cost and why. Like OpenRouter, for agents.

Live site: https://openvibe.actor · API docs: https://openvibe.services · For AI: https://openvibe.actor/llms.txt
Run it: npm ci && npm run dev (Node 22 or newer) · Tests: npm test

Status: alpha. Public at https://openvibe.actor (openvibe-ovh, unit openvibe-actor on 127.0.0.1:4950 behind nginx). See [STATUS.json](STATUS.json) for exactly what works and what does not.
Domain: openvibe.actor · Port: 4950 · Service id: actor
Plan: T17 (owner direction 2026-10-08: OpenVibe's own general agent, competing with the big labs' agents while able to use theirs; an OpenRouter for agents; cheap self-hosted browser and desktop agents). Decisions: ADR-044 (proposed).
License: AGPL-3.0 (same as every OpenVibe service).

Purpose

Actor is OpenVibe's own general agent and the router for agent work: it takes a task in plain words, sends it to the best agent system for it, streams the steps, has a model of another family check the answer, and gives back the result with its cost and the reason it went where it did. People use it in the browser at openvibe.actor with their OpenVibe.Network account. Apps, agents and services use the same API with a Network token for audience openvibe.actor and an actor.task.* grant.

How a task runs

  1. Sort it ([server/agents/classify.js](server/agents/classify.js)): an answer, a lookup (a domain, address or site), research (pages it names, or OpenVibe's content), a live web search, or code. A cheap model decides, or a free heuristic in private mode, so the text never leaves OpenVibe there.
  2. Route it ([server/agents/router.js](server/agents/router.js)): openvibe-sdk/placement over the agent catalog. Hard requirements come first: the task class's capability, the trust private mode demands, each agent's health, and the budget as a cost ceiling. Then the mode's objective: cheapest, balanced (the default), best (high-quality agents first), fastest, or private (first-party only). The answer is a platform.placement-result@1, stored on the task as its explanation.
  3. Run it ([server/agents/adapters.js](server/agents/adapters.js)): the agent works while its steps stream as events. Every model call and web search is metered at once at the published rate card, so the cost is true even if the task is cancelled half-way.
  4. Check it ([server/agents/check.js](server/agents/check.js)): a model of another family reads the answer. DeepSeek's answers are checked by OpenAI's smallest model, and OpenAI's by DeepSeek. In private mode the open model checks itself, and the task says so. An answer that cannot be checked is never delivered.
  5. Deliver or hand on ([server/tasks/engine.js](server/tasks/engine.js)): a passed check gives the result (the answer, its sources, the agent and model, the check), the cost and the explanation. An agent error or a failed check sends the task to the next eligible agent (at most two), with each placement appended to the explanation.

The agents ([server/agents/catalog.js](server/agents/catalog.js))

| Agent | Kind | Can do | Trust | Price |
|---|---|---|---|---|
| OpenVibe runtime | openvibe-runtime | answer, lookup, research, robot | external (the model runs at DeepSeek) | DeepSeek Flash, $0.30 in / $1.20 out per 1M tokens (peak; off-peak is half, Actor charges peak) |
| OpenAI agent | agent-platform | answer, research, web | external | gpt-5-mini, $0.25 / $2.00 per 1M, plus $0.01 a web search |
| Open model on OpenVibe | open-model | answer | first-party | free (OpenVibe's own server; openvibe-llm on the host) |
| OpenVibe.Codes | codes | code | first-party | listed, not hosted yet: coding runs need OpenVibe.Run sandboxes; use openvibe-codes on your machine |

The runtime acts only through OpenVibe's public APIs ([server/agents/tools.js](server/agents/tools.js)):

  • OpenVibe.Search: content across the network;
  • OpenVibe.Tools' catalog and run API: DNS, mail records, certificates, headers, WHOIS, conversions and more, limited to tools any anonymous caller may run;
  • the Tools page reader (read): a web page as text.

Actor itself never fetches a URL from a task or a model; Tools' own SSRF guard decides what a tool may reach.

Rate cards are typed in by a person from each provider's price page, with the page and the day it was checked, and are never written by a model.

API

| Route | Capability | |
|---|---|---|
| POST /api/v1/tasks | actor.task.create | actor.task-create-request@1 → platform.task@1 (201, queued; with webhooks, the answer carries webhook_secret once) |
| GET /api/v1/tasks/:id | actor.task.read | the task |
| GET /api/v1/tasks/:id/events | actor.task.read | server-sent events, actor.task-event@1, resumable with Last-Event-ID; ?deltas=1 adds delta events ({ text, n }: the next piece of the text the model is writing in its n-th call; no id, not stored, not replayed) |
| GET /api/v1/tasks/:id/trace | actor.task.read | the task's spans: every model, tool, check, judge and sub-agent call with its time and cost, and the totals |
| POST /api/v1/tasks/:id/cancel | actor.task.create | stop it |
| GET /api/v1/tasks | actor.task.list | actor.task-list-result@1 |
| GET /api/v1/agents | public / person | no person: the agent-system catalog (actor.agent-list-result@2); a signed-in person: their own contacts, Vibe first (actor.agent-list@1) |
| GET /api/v1/agent-systems | public | the agent-system catalog for any caller, including a signed-in person (actor.agent-list-result@2) |
| GET /api/v1/agents/presets | public | the four presets a create can copy: Researcher, Inbox, Studio editor, Party host |
| POST /api/v1/agents, GET/PATCH/DELETE /api/v1/agents/:id, PUT /api/v1/agents/:id/level | actor.agent.manage (signed in) | a person's agent contacts (actor.agent@1): create from a preset, read, change, delete (Vibe refuses actor.agent.vibe) and set the approval level (actor.level-change@1); the default is ask |
| GET /api/v1/stats/tools, GET /api/v1/stats/models | public | live numbers per tool and model over the last 7 days (n, success rate, p50/p90 ms, mean cost, last failure); only rows with n >= 20 |
| GET/POST /api/v1/me/agents (signed in) | actor.agent.manage for a token | list or create your personal agents; signed-out GET keeps the public catalog |
| GET/PATCH/DELETE /api/v1/me/agents/:id | actor.agent.manage for a token | read, edit or delete one personal agent |
| GET /me/approvals, GET/POST /me/approvals/:id (signed in) | the person | the approval inbox ([server/agents/approvals.js](server/agents/approvals.js)): when one of the person's own agents runs while they are away and would start a coding task on their machine (the codes agent), the run asks instead and ends with a link; nothing reaches Codes until the person approves, which starts it as their own task (declining runs nothing; a request expires after 7 days). In the account export (approvals.json) and deletion. |
| GET/POST /api/v1/me/agents/:id/memory, PATCH/DELETE /api/v1/me/agents/:id/memory/:mem, DELETE /api/v1/me/agents/:id/memory | actor.agent.manage for a token | control its memory |
| POST /api/v1/me/agents/:id/run, GET /api/v1/me/agents/:id/runs | actor.agent.run for a token | run now or list its tasks |
| GET /api/v1/me/agents/:id/suggestions, POST /api/v1/me/agents/:id/suggestions/:pid/{apply,dismiss,undo}; POST /me/agents/:id/suggestions/:pid (signed in) | actor.agent.manage for a token | what the agent suggested adding to its instructions (a run that passed its check may end with IMPROVE: <line>; at most three wait, the same line once), also under "Suggestions from your agent" on its page: apply adds the line, dismiss drops it, undo restores the instructions while they are as it left them. In the account export (agent-suggestions.json) and deletion. |
| POST /api/v1/route | public | dry run: which agent and why (heuristic class; nothing runs or is charged) |
| GET /api/v1/resources | actor.resource.read (service token) | common.resource-list-result@1 for actor.task; filters: project, kind, cursor, limit |
| GET /api/v1/resources/:ovrn | actor.resource.read (service token) | one common.resource-summary@1 for a project-owned task |
| POST /api/v1/resources/control | actor.resource.control (service token) | common.resource-control-request@1 → -result@1: stop cancels a task (queued: done at once; running: pending until its step returns) |

The resource index is for OpenVibe.Services on loopback and is blocked at the public nginx vhost. It lists task summaries under the actor.task kind; only a service token with actor.resource.read can read it. A task without a project has no OVRN and cannot be fetched by name. The control route acts for the person in on_behalf_of: the person who asked for the task, or an admin or owner of its project (asked of Network with network.project.member.read), so a project's admins can stop what its apps started. Repeats with the same idempotency_key return the first answer (actor_control_log).

Who can call it:

  • A person: their Network token as a Bearer, or this site's session. A write made with the session must come from openvibe.actor itself (fetch metadata or Origin).
  • An app, agent or service: a token for audience openvibe.actor that holds the route's capability, granted on OpenVibe.Services. Its tasks belong to the token's project.

What a caller can see: a task is visible only to its requester, or to tokens of its project. Anyone else gets 404, the same answer as a task that doesn't exist.

Webhooks: a task may register up to 8 https endpoints, each with the state changes to deliver (webhooks: [{ url, events }], the names of state).

  • Signing: the answer that creates the task carries webhook_secret, minted for that task. It is in no read or list, never exported, and erased once the task has ended and its deliveries are done. Each delivery is actor.task-webhook@1 ({ type: "actor.task.state", delivery_id, state, task }, the task as it was then), signed like an Events delivery (X-OpenVibe-Timestamp + X-OpenVibe-Signature-V2), so a receiver checks it with openvibe-sdk/events verifyDeliveryV2(rawBody, headers, secret). The delivery id stays the same across retries; dedupe on it.
  • Outcomes: 2xx is delivered; 410, or a URL refused at send time, ends the delivery; anything else is retried after 10 s, 1 min, 5 min, 30 min, 2 h, 6 h and 12 h, then fails.
  • Where Actor may connect: a webhook URL is the one address a caller chooses (server/net/webhook-post.js): https on port 443 or 8443, no credentials, public addresses only. It is checked at creation (actor.webhook.refused) and again at send time through openvibe-shared/egress, so a name that later resolves to a private address is refused. Redirects are never followed.
  • A project secret instead (ADR-056): a webhook may name one (secret_ref, a lower-case name) when the task is made
    by a project's app or agent token. The secret is looked up in the task's project and environment, and the project must
    have granted it to actor for sign (on openvibe.services, the project's Secrets page). OpenVibe.Network then signs
    each delivery over "<t>.<raw body>" in the same two headers, so the receiver's check is the same and Actor never
    holds the value. No webhook_secret is minted when every webhook names one. A secret Network refuses (missing, or not
    granted) fails that delivery at once; Network unreachable is retried on the usual backoff. A person's own task cannot
    name one (422 actor.webhook.secret_unavailable).

Errors: RFC 9457 problems. The codes are on openvibe.actor/docs.

Money

  • Free tier, per person: ACTOR_FREE_TASKS_PER_DAY (20) tasks a day, up to ACTOR_FREE_PER_TASK_USD ($0.05) a task and ACTOR_FREE_PER_DAY_USD ($0.05) a day. Past it, tasks are refused with actor.budget.day_spent until midnight UTC; the person can add their own model key at /chat/model and keep going.
  • Operator ceiling: ACTOR_MAX_COST_USD_PER_DAY ($2) over everyone; past it, new tasks answer 503 actor.capacity.spent until midnight UTC.
  • Budgets are hard:
  • a budget above the tier is refused, never lowered;
  • a task whose cheapest capable agent would cost more than its budget fails before running;
  • an adapter stops before a call when nothing is left.
  • Billing: OpenVibe.Billing (T5) is not in the loop yet. Every cent is the free allowance's, the operator pays the providers with keys in /etc/openvibe/actor.env, and cost.free_allowance_used says so.

Owns

  • Tasks, their event logs and daily spend (migrations/0001_tasks.sql). Tasks are kept 30 days, then pruned with their events. Nothing stored is a credential, a key or a token.
  • Request telemetry (migrations/0010_telemetry.sql): each HTTP request is aggregated by route template, method and status class, never by raw URL. Once a minute one http.request sample per key, plus the active-requests, p95 latency and event-loop-lag gauges, is written to this service's own telemetry_samples table (openvibe-sdk 0.46 telemetry, the platform.telemetry-sample@1 shape); rows older than 14 days are deleted daily. /shared files, static assets, health, readiness and /metrics are not counted. server/app.js builds the collector (createHttpTelemetry) and mounts its middleware; server/index.js flushes it every minute and on stop, and runs the 14-day retention.
  • The agent catalog and its rate cards; the routing policy over them (placement itself is openvibe-sdk/placement).
  • An agent's voice as a file (actor.voice@1, [server/agents/voice.js](server/agents/voice.js)): its name, a short bio, the house voice it speaks in, its rules, its level, and the model and apps it prefers (ids, never a key). The document's integrity hash shows the file was not edited after it was written and says nothing about who wrote it, so the author in a file is only a claim. A shared voice ([server/agents/voice-shares.js](server/agents/voice-shares.js), migrations/0040_agent_shared_voices.sql) is the public link /voices/<id>: the owner may stop sharing it, the document is served as JSON at /voices/<id>.json, and Actor's own record of who shared it is the only place a voice's author is vouched for.
  • The home page (server/http/pages.js, public/css/home.css, test/home.test.js) is a taste and a door, about five phone screens, built from the shared kit (kit.css, room.css, showcase.css): the task box, numbers read from the catalog, the running config and OpenVibe.Tools' catalog (the runtime's own cached copy, waited for at most a second) as one kit.stats strip, task templates as a rail of cards that fill the task box (/?task=…&mode=…, kept by no shared cache; signed out, the task waits for sign-in; what a robot, computer, desk or private task needs is said in the box), the agents as rows that open /agents, the parts read once as folds (how a task finds its agent, the comparison, what works today, the Tools tools the runtime can run, the API samples; a link to #tools or #api opens its fold), the questions people ask first and the rest of OpenVibe as a launcher, with JSON-LD WebSite, WebApplication, WebPage, ItemList (the agents) and FAQPage (exactly the questions shown).

Does not own

  • Coding: OpenVibe.Codes (the coding-agent harness).
  • Execution sandboxes, browsers and desktops: OpenVibe.Run / OpenVibe Link (next).
  • Accounts, apps and grants: OpenVibe.Network and OpenVibe.Services.
  • The tools the runtime uses: OpenVibe.Tools. Search: OpenVibe.Search.

Depends on

Services it calls at runtime (base URLs in [server/config.js](server/config.js)):

  • OpenVibe.Network (OV_NETWORK_URL, OV_NETWORK_INTERNAL_URL): sign-in (OAuth2 + PKCE, client actor), the JWKS that verifies session and app tokens, and the internal routes (with this service's own client-credentials token) where account export parts and deletion confirmations are pushed.
  • OpenVibe.Search (ACTOR_SEARCH_URL), through the Search Fabric (/api/v1/query): content across the network for the runtime's search_openvibe tool (anonymous), and the web for search_web (cited results; asked as Actor's service principal with the grant search.query.delegate, naming the task's person in X-OV-Subject, so the paid steps past the open index count against them).
  • OpenVibe.Tools (ACTOR_TOOLS_URL): the catalog, the run API and the page reader, for find_tools, run_tool and read_page. Called as any anonymous caller would.
  • OpenVibe.Bot (ACTOR_BOT_URL, default http://127.0.0.1:4630; empty turns it off): a signed-in person's own robots, for list_my_robots and robot_steps. Actor calls Bot with its own client-credentials token (audience openvibe.bot, bot.robot.read + bot.robot.control) and X-OV-Subject naming the person. Bot answers only for the robot's owner, and only on robots where that owner turned on "Let my OpenVibe.Actor agent drive this robot". Bot's gate and limits judge every step. App, agent and service tasks never see these tools.
  • See it work (one-agent system, W1): a running task's card shows its desk — the private Live stream Link publishes for the task's desk (or the still at 1/s), the latest step in plain words, a ring where it last clicked, the page address and where it is. A tap opens the viewer: Take over (Run's lease, or Bot input as the person on a Link desk), Pause, Stop and Hand back with a note. A sign-in step is an Ask that opens it in control. A finished run has a replay of each step's still and action; typed text is never kept. The SDK calls are desks, takeOver, handBack and replay in openvibe-sdk/agents (PR open, released by the main session).
  • The chat is the one page (one-agent system, stage 1): the Console is folded into it. /console answers 301 to /chat; the running tasks the Console listed are the "Now" strip at the top of the chat (one line each, Stop on each, hidden when nothing runs), and a running task is a card in its conversation that updates in place. The run settings ("New runs: Only me … Apply to all my existing runs") live in /chat/settings; the composer is openvibe-shared's composer v2, agent preset (Attach, Speak, the Fast/Balanced/Best mode chip, the round send, progress on each file). The empty chat shows four compact suggestion chips. /me/spend stays (server/http/console-pages.js, server/http/chat-pages.js, test/one-page.test.js).
  • A person's own computers, also through OpenVibe.Bot (plan T14/T17, contracts 0.166.0): list_my_computers, computer_list, computer_read, computer_run (argv, never a shell line) and computer_write, on machines installed with OpenVibe Link's --computer plugin. Actor calls Bot's machine API with its own token (bot.computer.control) and X-OV-Subject naming the person. Bot answers only for the machine's owner, and only where that owner turned on "Let my agent use this computer". The level the owner set on the machine decides what may happen there; at ask, each run or write waits up to two minutes for the owner's approval on the machine. Only a signed-in person's task sees these tools (server/agents/computers.js, test/computers.test.js). computer_screen (when an OpenAI key is set) takes a picture of the machine's screen through Bot (1280 px wide) and has OpenAI's vision model answer the agent's question about it. The runtime model gets that answer, never the picture; the picture is not stored; secrets visible on screen are named, not repeated; and each look is metered on the task like any model call (refused once the task's budget is spent). computer_input (offered with computer_screen; contracts 0.171.0) uses the mouse and keyboard: a click, double or right click or move at a position in the last screen picture (its width and height are passed, and the Link scales them to the real screen), a scroll, typed text, or one key or chord. It is an acting command, so at level ask each one waits for the person's approval on the machine; the runtime is told to look first, act in small steps, look again, and never type a secret the task did not give it.
  • computer_browser (contracts 0.174.0, Link v0.10.0) drives a browser of the agent's own on the machine. It is a
    profile of its own, so it never sees the person's sessions.
  • It can open a URL, click a selector or a visible link or button, fill a field, press a key, go back, read the
    page, take a screenshot and close. The page comes back as its text (up to 3,500 characters) and links (up to 25),
    which fits the result clip, so the tool is offered without a vision key.
  • Reading, screenshots and closing run at every level; the rest wait for approval at ask. Below full the machine
    refuses addresses on its own network.
  • Approvals can come from the Link's tray or notification buttons, as well as openvibe-link approve (Link v0.11.0).
  • computer_desk (contracts 0.214.0, Link v0.15.0 on Linux) starts, stops or reports the machine's private desk: a
    screen of its own (Xvfb with its own bus and window manager) where the agent works without touching the person's.
    computer_screen, computer_input and computer_browser take desk: "private" to act there. Bot refuses that on
    a machine without a desk, so it never falls back to the person's screen.
  • list_my_computers shows desk among what a machine can do. The runtime is told to do screen, mouse and browser
    work on the private desk when there is one (start, work, stop), unless the task is about what is on the person's
    own screen. Starting waits for approval at ask; sizes are the contract's (640-3840 × 480-2160, 1280 × 800 by default).
  • OpenVibe.Events (ACTOR_EVENTS_URL): the two ADR-033 subscriptions created at boot, plus chat.message.created while asking from chat is on, and delivery to the loopback /internal/events.
  • OpenVibe.Chat (ACTOR_CHAT_URL, default http://127.0.0.1:4400): ask Actor from chat ([server/chat/asks.js](server/chat/asks.js)).
  • Who and where: a signed-in person writes @actor <task> in the global chat or their own channel's chat. Guests, relayed external chatters (source_platform) and other people's channels are ignored.
  • How it runs: the message becomes their task, on their free tier, with via: chat. The answer is posted where everyone reads it, so the task uses public tools only. The engine refuses a robot, computer or coding task (actor.task.not_public), and the runtime is never given the person's robots or computers.
  • The reply: the first lines of the answer and the task's page, posted in the same room as "OpenVibe Actor", answering the message. It goes through Chat's service API (chat.message.send, audience openvibe.chat) and is retried with backoff for up to an hour.
  • Limits: 5 asks an hour per person and 60 across everyone (ACTOR_CHAT_ASKS_PER_HOUR, ACTOR_CHAT_ASKS_ALL_PER_HOUR). Past a limit, one reply says so and further asks are dropped quietly.
  • Data: actor_chat_asks (migration 0009) is part of the person's export and erasure.
  • ACTOR_CHAT_ASKS=0 turns it off.
  • Models: OpenVibe.AI's router (ACTOR_AI_ROUTER=1, set in production). Every model call is a run of ai.complete on OpenVibe.AI with Actor's service token (audience openvibe.ai, ai.run.create); each tier names a policy on it — fast openvibe/economy, smart openvibe/balanced, best openvibe/auto, check openvibe/economy, vision openvibe/auto (ACTOR_AI_MODEL_*) — and AI picks the offer by price, latency, uptime and policy, fails over, and says which model ran. The person a task belongs to rides as on_behalf_of: their allowance and their Balance pay, and the run is theirs. ACTOR_AI_MAX_COST_USD caps each call. Actor then holds no provider key: ACTOR_DEEPSEEK_API_KEY, ACTOR_OPENAI_API_KEY, ACTOR_LOCAL_LLM_URL/ACTOR_LOCAL_LLM_MODEL, ACTOR_RUNTIME_* and ACTOR_COMPAT_* are unused (the cross-family check is one family now: the verdict says so). A person can still bring their own key, sealed in OpenVibe.AI.
  • The model providers (a self-hosted Actor, router off): DeepSeek (ACTOR_DEEPSEEK_API_KEY, api.deepseek.com), OpenAI (ACTOR_OPENAI_API_KEY, api.openai.com/v1) for the web-search agent and the cross-family check, and an OpenAI-compatible local server (ACTOR_LOCAL_LLM_URL) for private mode.

Programs: poppler-utils' pdftotext reads the text of a PDF attached in the chat (ACTOR_PDFTOTEXT names another path); without it a PDF is kept but its text is not read, and the run is told so.

Data: PostgreSQL (openvibe-sdk/db, DATABASE_URL / DATABASE_DIRECT_URL) holds tasks, their events and daily spend; Valkey (VALKEY_URL) holds the shared per-caller limit counters. A provider or service without its key or URL is listed unavailable, never invented.

Libraries: openvibe-contracts (contract validation, problem responses, service-token verification, capability checks); openvibe-sdk (placement for routing, auth, db, limits, account-data, valkey, service); openvibe-shared (serve, cache-policy, frame, shell, showcase, seo, legal, release, metrics, ready); express, helmet, express-rate-limit, cookie-parser, pg, iovalkey.

Capabilities

  • Required on this API: actor.task.create (create a task, cancel one), actor.task.read (one task, its live stream), actor.task.list (list your tasks). A person acting for themself needs no capability; an app, agent or service token for audience openvibe.actor needs the one the route names, granted on OpenVibe.Services. A first-party service (svc:) may act for a person who asked it to by naming them in X-OV-Subject (OpenVibe.Watch's actor-task action): the task is the person's (their allowance, their list, their export and deletion) and records the service as via; the service still needs the route's capability, and it reads, lists and cancels only the tasks it started. A developer app or an agent never names a person this way (403 subject.not_delegated). actor.agent.read is the code's name for the catalog and the dry run, which are public.
  • Personal agents: actor.agent.manage guards management and memory, and actor.agent.run guards runs and their list for a token. Browser sessions and a person's own token work as task calls do. The new capability manifests are pending in OpenVibe.Contracts.
  • Held on other services: a client-credentials token for audience openvibe.events, scope events.subscription.manage, creates the two ADR-033 subscriptions at OpenVibe.Events; the same actor principal's token pushes account export parts and deletion confirmations to OpenVibe.Network's internal routes.
  • Not held: the runtime presents no grant to OpenVibe.Search or OpenVibe.Tools — it uses only what any anonymous caller may call.

What comes next

  1. Browser and desktop agents on OpenVibe's own cheap servers (Run workers with a headless browser and a virtual desktop), watchable live and taken over by the owner.
  2. Coding through OpenVibe.Codes on hosted Run sandboxes.
  3. More agent platforms behind the same router, bring-your-own keys through OpenVibe.AI, and paid tiers through Billing.
  4. Personal agents v1 are in this worktree: named instructions, memory people control, hourly/daily/weekly schedules and ordinary task runs. An approval inbox (a coding task an agent wants to start waits for its owner) and Watch triggers (a watch hands its

task to an agent when its condition fires) are built, and so is asking Actor from chat (@actor …). Frame access comes later.

Run your own

A self-hosted Actor ([docs/self-host.md](docs/self-host.md)): npm run selfhost:init, then
docker compose -f docker-compose.selfhost.yml --profile ollama up -d.

  • One owner signs in with a password, and the API takes their own token.
  • One process, with its database on disk (PGlite) unless DATABASE_URL is set.
  • The OpenVibe runtime runs on any OpenAI-compatible model (ACTOR_RUNTIME_URL), at the operator's price, listed as
    user-owned. With no other model family, it checks its own answers and says so.
  • OpenVibe's public Search and Tools are used anonymously, or not at all.
  • The parts that need an OpenVibe service identity are off: Bot, Codes, chat asks and Network-signed webhooks.

Configuration

See [.env.example](.env.example). Required in production:

  • OV_OAUTH_CLIENT_SECRET (the actor principal; server/setup/service-principal.js create actor on the Network writes it);
  • BASE_URL, DATABASE_URL and DATABASE_DIRECT_URL;
  • at least one agent: ACTOR_AI_ROUTER=1 (OpenVibe.AI's router), or ACTOR_DEEPSEEK_API_KEY, ACTOR_OPENAI_API_KEY, or ACTOR_LOCAL_LLM_URL + ACTOR_LOCAL_LLM_MODEL.

Readiness requires the database and at least one available agent.

Deploy (for the lead)

  • Deploy: sudo ovhost deploy actor on the host (git checkout at /opt/openvibe.actor, unit openvibe-actor.service on 127.0.0.1:4950, env /etc/openvibe/actor.env, database ov_actor on the data role).
  • nginx: [deploy/nginx/openvibe.actor.conf](deploy/nginx/openvibe.actor.conf), installed with ov-vhost-install. A task's event stream is unbuffered.
  • Rollback: ovhost puts the previous sha back by itself when /api/ready does not answer after the restart.
  • A stop: running tasks are stopped and recorded as actor.task.interrupted. They are never re-run behind the person's back.

Acceptance

Run the suite with Link 22 (the Development block below):

  • npm test — every [test/*.test.js](test/) in its own process on temp PGlite databases, with in-process mocks of OpenVibe.Network, OpenVibe.Events and OpenVibe.Media and stand-in model providers; nothing needs the network or a running site.
  • npm run test:pg — the same suite against PostgreSQL instead of PGlite.
  • npm test -- <word> runs only files whose name contains a word; npm test -- --strict makes a skipped test fail the run.

What the main tests prove:

  • [test/tasks.test.js](test/tasks.test.js): a task end to end through the API and the real adapters — classified, routed, run, checked, delivered as platform.task@1 with its cost and explanation; hand-off to the next agent on an error or a failed check; private mode never sending the text outside OpenVibe; hard budgets and the free tier; idempotency; cancel; the live stream replaying and ending; nobody seeing another person's task.
  • [test/contracts.test.js](test/contracts.test.js): the released actor service manifest and its four capability manifests match the routes, guards and response shapes in the code.
  • [test/account-data.test.js](test/account-data.test.js): the ADR-033 export and deletion through the signed /internal/events route carry only the person's rows; a redelivery erases nothing twice, and a bad signature or a forwarded request is refused.
  • [test/caller-limits.test.js](test/caller-limits.test.js): one caller past its limit gets 429 before any work while another passes; the budgets and the actor_rate_limited_total metric are pinned.
  • [test/security-ssrf.test.js](test/security-ssrf.test.js), [test/security-secrets.test.js](test/security-secrets.test.js) and [test/security-session.test.js](test/security-session.test.js): Actor never fetches a URL from a request or a task, outbound requests reach only configured bases, no secret leaves in a body or a log, and a FedCM assertion is not a session.

The remaining files cover sign-in (PKCE, state, redirect safety), the Network JWKS, the pages and layout, crawl artifacts, asset caching, per-file size budgets, graceful stop and the nginx sign-in limits.

Development

npm install
fnm exec --using=22 npm test        # every test/*.test.js on temp PGlite databases, a mock Network and stand-in providers
fnm exec --using=22 npm run dev     # http://localhost:4950 (agents without keys are listed as unavailable)

Tests:

  • [test/tasks.test.js](test/tasks.test.js) runs a task end to end through the real adapters against stand-in providers, Tools and Search. It covers routing per class and mode, the cross-family check, hand-off on an error or a failed check, private mode never sending the text outside, hard budgets and the free tier, idempotency, cancel, the live stream, visibility, cross-site writes, and app tokens with capabilities.
  • The other files cover sign-in (PKCE, state, next=), discovery, the released contracts manifest matching the routes, per-caller limits, readiness, secrets never stored or logged, Actor never fetching a URL from a task, and the home page's size budgets.

Account export and deletion

A person's account at OpenVibe.Network can be exported and deleted, and every service holding their rows answers its
part (ADR-033). Actor receives network.account.export_requested and network.account.deleted at POST /internal/events
(loopback only) — the two tables are mapped in [server/identity/account-data.js](server/identity/account-data.js), and
the boot-time subscriptions are created by [server/events-consumer.js](server/events-consumer.js):

  • Exported: the tasks a person asked for (tasks.json), what they spent per UTC day (spend.json), their agents (agents.json), every memory note (agent-memory.json), and — when they turned on the nightly tidy (HO5) — their agents' diaries (agent-diary.json), the changes the nights made (agent-memory-changes.json), the history of the nights (agent-memory-nights.json) and their consent (agent-memory-tidy.json), pushed to
    POST /internal/account-exports/:id/parts with this service's own token. Nothing here is a secret — Actor stores no
    token, key or credential.
  • Erased: all four tables hold the person's own rows, so they are deleted whole and nothing is kept; a task's event log
    (task_events) goes with the task by cascade. Actor then confirms with POST /internal/account-deletions/:id/confirmations
    and the counts.
  • Anonymized: nothing. There is no row Actor keeps that was written by this person for another person to read.

A task an app, agent or service ran for this person is not matched: its requester is app:…/agent:…/service:…, and
Network's deletion event carries only the person's usr_…, so only rows requested as user:usr_… are erased. The spend
row that does not name the person (*, the operator's daily ceiling) is a separate row and is kept.

Environment: ACTOR_EVENTS_SECRET (comma-separated for rotation, 32+ characters each; unset makes the route answer
503), ACTOR_EVENTS_URL (or EVENTS_URL) is where the two subscriptions are created at boot (off when unset), and
ACTOR_EVENTS_ENDPOINT overrides the loopback endpoint; ACTOR_EVENTS_SUBSCRIBE=0 turns the boot-time subscription off.

Security (threat notes)

Reporting a vulnerability: [SECURITY.md](SECURITY.md).

  • Session tokens are httpOnly cookies; a FedCM assertion or an app or service token is never a session.
  • Provider keys live only in the env file and in outbound Authorization headers to the providers; OpenVibe services get no provider key.
  • A model's links in an answer are rendered rel="nofollow ugc noopener"; all answer text is escaped first (a small Markdown subset).
  • Tasks are people's words: request bodies are never logged.

Part of the OpenVibe network. Built in the open by OpenVibers.

<!-- versions:start -->

  • openvibe-contracts: v0.421.0
  • openvibe-sdk: v0.164.0
  • openvibe-shared: v4.235.0

<!-- versions:end -->