Repositories openvibers openvibe.actor official
openvibers/openvibe.actor
Open source agent console: a task in plain words goes to the best AI agent, checked by a second model, with the cost and the why.
Get it
Get this repository
A Git token is made for this copy and shown once. It can read and write the repositories you own, and expires in an hour.
git clone https://git.openvibe.codes/openvibers/openvibe.actor.git
OpenVibe Link asks you on your computer. The folder is inside the root you chose in Link, and the request ends after two minutes.
Clone to my computerA Codes session clones from our disk: no network, no GitHub quota. When this repository has a GitHub twin, Codes pushes there too.
Open in CodesREADME.md
View fileOpenVibe.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
- 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.
- Route it ([server/agents/router.js](server/agents/router.js)):
openvibe-sdk/placementover 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 aplatform.placement-result@1, stored on the task as its explanation. - 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.
- 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.
- 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.actorthat 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 isactor.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/eventsverifyDeliveryV2(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 toactorforsign(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. Nowebhook_secretis 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 (422actor.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 toACTOR_FREE_PER_TASK_USD($0.05) a task andACTOR_FREE_PER_DAY_USD($0.05) a day. Past it, tasks are refused withactor.budget.day_spentuntil midnight UTC; the person can add their own model key at/chat/modeland keep going. - Operator ceiling:
ACTOR_MAX_COST_USD_PER_DAY($2) over everyone; past it, new tasks answer 503actor.capacity.spentuntil 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, andcost.free_allowance_usedsays 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 onehttp.requestsample per key, plus the active-requests, p95 latency and event-loop-lag gauges, is written to this service's owntelemetry_samplestable (openvibe-sdk 0.46 telemetry, theplatform.telemetry-sample@1shape); rows older than 14 days are deleted daily./sharedfiles, static assets, health, readiness and/metricsare not counted.server/app.jsbuilds the collector (createHttpTelemetry) and mounts its middleware;server/index.jsflushes 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 onekit.statsstrip, 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#toolsor#apiopens 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, clientactor), 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'ssearch_openvibetool (anonymous), and the web forsearch_web(cited results; asked as Actor's service principal with the grantsearch.query.delegate, naming the task's person inX-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, forfind_tools,run_toolandread_page. Called as any anonymous caller would. - OpenVibe.Bot (
ACTOR_BOT_URL, defaulthttp://127.0.0.1:4630; empty turns it off): a signed-in person's own robots, forlist_my_robotsandrobot_steps. Actor calls Bot with its own client-credentials token (audienceopenvibe.bot,bot.robot.read+bot.robot.control) andX-OV-Subjectnaming 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,handBackandreplayinopenvibe-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.
/consoleanswers 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/spendstays (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) andcomputer_write, on machines installed with OpenVibe Link's--computerplugin. Actor calls Bot's machine API with its own token (bot.computer.control) andX-OV-Subjectnaming 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; atask, 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 withcomputer_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 levelaskeach 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. Belowfullthe 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_inputandcomputer_browsertakedesk: "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_computersshowsdeskamong 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 atask; 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, pluschat.message.createdwhile asking from chat is on, and delivery to the loopback/internal/events. - OpenVibe.Chat (
ACTOR_CHAT_URL, defaulthttp://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, audienceopenvibe.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=0turns it off.- Models: OpenVibe.AI's router (
ACTOR_AI_ROUTER=1, set in production). Every model call is a run ofai.completeon OpenVibe.AI with Actor's service token (audienceopenvibe.ai,ai.run.create); each tier names a policy on it —fastopenvibe/economy,smartopenvibe/balanced,bestopenvibe/auto,checkopenvibe/economy,visionopenvibe/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 ason_behalf_of: their allowance and their Balance pay, and the run is theirs.ACTOR_AI_MAX_COST_USDcaps 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_*andACTOR_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 audienceopenvibe.actorneeds 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 inX-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 asvia; 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.readis the code's name for the catalog and the dry run, which are public. - Personal agents:
actor.agent.manageguards management and memory, andactor.agent.runguards 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, scopeevents.subscription.manage, creates the two ADR-033 subscriptions at OpenVibe.Events; the sameactorprincipal'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
- 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.
- Coding through OpenVibe.Codes on hosted Run sandboxes.
- More agent platforms behind the same router, bring-your-own keys through OpenVibe.AI, and paid tiers through Billing.
- 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_URLis 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(theactorprincipal;server/setup/service-principal.js create actoron the Network writes it);BASE_URL,DATABASE_URLandDATABASE_DIRECT_URL;- at least one agent:
ACTOR_AI_ROUTER=1(OpenVibe.AI's router), orACTOR_DEEPSEEK_API_KEY,ACTOR_OPENAI_API_KEY, orACTOR_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 actoron the host (git checkout at/opt/openvibe.actor, unitopenvibe-actor.serviceon 127.0.0.1:4950, env/etc/openvibe/actor.env, databaseov_actoron 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/readydoes 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 -- --strictmakes 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@1with 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
actorservice 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/eventsroute 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_totalmetric 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/partswith 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 withPOST /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
Authorizationheaders 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 -->