This document describes Vogt as it is built and shipped: what the pieces are, how they fit together, and the rules that hold the shape in place.
Companion documents: SCHEMA.md (tables and data topology),
DEPLOYMENT.md (running it), ENGINE.md (the
session engine's own reference), USER_GUIDE.md (driving
it) and CONFIG.md (the generated settings reference).
Vogt is a self-hosted product development environment for small teams, solo developers and the coding agents that work with them. It answers one question for a single repository or across every project you register:
What is the state of this work, how fresh is that answer, what should be done next, and is the project itself healthy?
The answer is the same whether a person asks in the PWA, a script asks over REST, or an agent asks over MCP — and every answer says where it came from and how old it is. Alongside the answer, Vogt gives those agents a place to do the work: terminal sessions in a project's own tree, a scheduled task queue, and an assistant that can be driven by voice.
Vogt is fully functional with no forge at all: projects, work items, backlog, ranking, contracts, compliance, dependency references, drift and audit all work over plain folders and local git. Forge integration (GitHub, and Forgejo/Gitea hosts) is optional; an absent forge yields "not collected", never a failure.
Vogt ships as one stack: the vogt-stack image (Python core, Rust engine and
Solid PWA behind one port) plus the vogt-voice sidecar, released as a pair
and run by one Compose file with voice on by default. The core image is a
build input to the stack image, not a product; the engine is not optional.
Development happens on main; releases are v* tags.
- Observed-first. Work that collectors find in your repositories and on your forge is visible immediately, before anyone types it in. Adoption upgrades its trust; suppression removes it from ranked views.
- Vogt reports; it never enforces. No operation takes contract compliance, trust state or drift status as a precondition.
- Vogt never goes looking. Collection scope is the list of registered projects. Nothing crawls the filesystem, nothing keeps a list of unregistered candidates, and nothing re-checks compliance on a timer.
- Declared and observed stay separated. What you assert lives in one store; what collectors found lives in another. A drift engine joins them and raises proposals. Collector failure can corrupt nothing.
- Every answer carries provenance and freshness. Entities carry a trust state; aggregate views carry the age of the sweeps behind them; a query whose scope exceeds swept scope says so.
- Transport parity. Every operation is defined once and reaches the CLI, REST, MCP and the PWA from that one definition. Parity is tested.
- Every write is audited and explained. Each declared write records its
actor, its operation and a required free-text
reason, and lands its event in the same transaction. Identity comes from authentication, never from a request field. - Nothing runs on its own. Every session traces to a person or to a schedule a person created. An execution surface is not an enforcement surface.
One published port, one container, one sidecar:
vogt-stack container
vogt-engine (Rust; the front door, :8910)
├── / the Solid PWA, embedded in the binary
├── /api/... sessions, terminals, files, git, agent tasks, push,
│ assistant, history
├── /api/vogt/... proxied to the core's /api, caller's bearer forwarded
├── /mcp proxied to the core, caller's token forwarded
├── /api/install/..., /api/auth/login
│ proxied untouched; the core self-gates
└── /healthz, /readyz
vogt serve (Python; the core, loopback only, :8000 inside the container)
├── /api/... REST (FastAPI; OpenAPI at /openapi.json)
├── /mcp MCP streamable HTTP
├── /health/live, /health/ready, /version
└── collector scheduler (background sweeps)
vogt-voice container (Rust; STT + TTS on the Compose network, reached by
the engine at http://voice:8000/v1, never published)
- The core owns the work: projects, work items, backlog, bugs, ranking,
drift, contract, audit, the two SQLite stores, and the operation registry
every surface is generated from. It listens on loopback inside the
container and is never published; the entrypoint refuses to start if
VOGT_CORE_URLnames anything else. - The engine owns the doing: PTY sessions, agent tasks, the assistant,
workspace-scoped file and git APIs, push, session history — and the front
door, the only listening process. The image also carries the
claudeandcodexagent CLIs the sessions run. - The PWA (
web/) is compiled into the engine binary. It calls the engine's APIs directly and reaches the core through/api/vogt, holding one credential: a core session minted by a username-and-password login (POST /api/auth/login), or a pasted API token. The engine accepts it on its own routes by asking the core who it is, and forwards it untouched on/api/vogt, so the core's audit names the person who acted. - The voice sidecar (
voice/) speaks the OpenAI-compatible audio contract the engine uses, with a baked-in Whisper model and Piper voice, so a fresh stack transcribes the microphone and speaks replies with no account. It is thevoiceservice indeploy/stack.compose.yml, on by default throughCOMPOSE_PROFILES=voice; clearing the profile leaves it out. - The mobile shell (
mobile/) is a Capacitor Android wrapper that loads a deployed front door by URL, so server and PWA releases reach installed phones without an APK rebuild; only native plumbing (push, microphone) needs the shell rebuilt.
The engine calls the core over loopback for /api/vogt and /mcp, and
fetches the assistant's Vogt tools from the core's own MCP tools/list. The
core calls the engine only for sessions — the session.* operations
(vogt.adapters.engine.client) and the session-outcomes collector. The
engine calls the voice sidecar; the PWA and the mobile shell call only the
engine. The two halves share exactly one credential, the stack secret: a
file both read (VOGT_CORE_TOKEN_FILE for the engine,
VOGT_BOOTSTRAP_CORE_TOKEN_FILE for the core). The core adopts it at init
as the actor and scopes named by VOGT_BOOTSTRAP_CORE_TOKEN_*, revoking any
earlier adoption of a different value in the same transaction, and calls the
engine with it to start sessions; the engine recognises it as the core's own
identity, follows the core's event feed with it, and lends it to the optional
break-glass ENGINE_TOKEN, which has no actor of its own. That is the whole
first-boot bootstrap. The engine keeps no token table: every other bearer it
sees is a core token — an API token from token.issue or a session from a
password login — and it resolves each by asking the core's auth.whoami,
caching the answer for seconds. People are created by the first-run wizard or
vogt user create; agents get API tokens from vogt token issue.
registry/ the operation registry: every capability defined once
core/ entities, workflow, drift, ranking, trust, contract, auth
application/ use-cases and services; the ONLY layer adapters may call
adapters/
cli/ argparse over the registry
http/ FastAPI: REST + OpenAPI, health, install, the sweep scheduler
mcp/ MCP over stdio and streamable HTTP, and the remote bridge
forge/ the ForgeProvider seam: GitHub and Forgejo/Gitea, sync, write-back
github/ GitHub client, consolidation, notifications, posture
engine/ the client the session operations use
git/ clone through an askpass helper (tokens never reach a URL)
collectors/ core (offline): git-local, source-markers, dep-refs,
mirrored-source, contract-checker; session-outcomes when an
engine is configured; forge-* when a forge is configured
storage/ SQLite x2 (declared.sqlite3, observed.sqlite3), migrations
Adapters call the application layer, the application layer calls core and
storage, and nothing below an adapter knows which transport asked. Collectors
write only the observed store; the application layer is the only writer of
the declared store, and application/writes.py is the one path an audited
write takes.
engine/ is its own Cargo workspace: server/ builds the vogt-engine
binary and contract/ holds the shared wire DTOs. One module per concern:
- PTY sessions (
pty.rs,sessions.rs,scrollback.rs) — a server-owned PTY per session that survives every client disconnecting, with a ring-buffer scrollback. - WebSocket attach (
ws.rs) — the first frame authenticates, a snapshot is replayed, then live output streams. - SSE (
events.rs,activity.rs) — a server-wide event stream carrying each session'sidle/running/waiting-for-input/erroredstate. - Files and git (
files.rs,git.rs) — APIs bounded to the workspace root. - Agent tasks (
agent_tasks.rs,workflow_engine.rs) — a durable scheduled-agent registry (manual,interval, UTCdaily, event triggers from the core's own state, an explicitapifire) whose runs are real PTY sessions, optionally bound to a project or work item. - The assistant (
assistant*.rs,vogt_tools.rs) — a server-side tool-use loop over the sessions and a curated slice of Vogt, every effector behind an on-screen approval, with a durable interaction log. - The front door (
vogt_core.rs,app.rs,auth.rs,assets.rs) — the embedded PWA, the bearer gate, and the two proxied route families. - Push and history (
push*.rs,history.rs) — web push (VAPID), native push (FCM), and archived session history. - Session briefs (
prompt_files.rs) — the brief vogt-core composed and a companion guide beside it. A project's agent policy is text in the brief; the engine enforces none of it yet.
The engine is bootable alone: with no core configured it serves sessions,
stays ready, and refuses the Vogt routes with a named reason. The wire
contract, the assistant's threat model and the task scheduler are in
ENGINE.md.
Every capability of the core is one entry in
src/vogt/registry/operations.py: a name, a summary, a scope, a mutating
flag, a pydantic parameter model, a result model, a handler in the
application layer, an HTTP route and a CLI binding. The registry holds 95
operations. From that one definition the CLI (vogt <verb>) is
generated; the REST routes under /api are generated with their schemas
in the OpenAPI document at /openapi.json; and the MCP tool list is
generated, one tool per operation, named for it (work.get is work_get).
The PWA is a client: web/src/vogtApi.ts names each path it uses after
the operation that serves it, and tests/test_pwa.py resolves every entry
against the registry, so a view cannot grow an endpoint of its own.
There are no path parameters: HttpRoute is a method and a literal path, and
an operation's arguments are one model shared by all three transports, so an
identifier travels as a query or body field (GET /api/projects/brief?project=…).
Nothing uses DELETE: a revocation is POST /api/suppressions/revoke,
because it is an audited write that needs a reason.
Parity is tested. tests/test_parity.py drives each operation through
CLI, REST and MCP and asserts identical results and identical audit rows. Two
named exclusion lists live in src/vogt/registry/registry.py, each entry
carrying its reason, and the test fails if either names an operation that
does not exist: LOCAL_ONLY (init, migrate, serve, backup,
restore, clone, import, mcp.stdio — each acts on the local process or data
directory, and none is mounted under /api or offered over MCP) and
HTTP_ONLY (session.token, the engine's own call to mint a session's
credential). The engine's routes are held to the same rule by a third table,
src/vogt/registry/engine_routes.py: each route names the core operation
that is its MCP counterpart or says why it has none, and
tests/test_engine_parity.py fails on a route in neither (API.md).
Three routes sit beside the registry rather than in it: the unauthenticated
health probes; the first-run install surface (GET /api/install/status,
POST /api/install/bootstrap), which names the first operator only while no
person holds a credential — no token bound to a non-agent actor, revoked
included, and no password login; agent-bound tokens such as the adopted
stack secret do not count (#903) — and refuses with install_closed forever
after (the store latches it: an upgraded store that already held a token,
or one whose user was later removed, stays closed) — given a password it creates that person's admin login and returns
an expiring session, which is what the browser wizard rides, and without one
it returns an admin API token shown once, the headless shape; and
POST /api/auth/login, the password login, which is unauthenticated by
construction and mints a session bound to the person's own actor. Both are
mounted beside the registry because neither has a principal to authorise.
Identity is never an argument. The principal is derived from
authentication — a token bound to an actor, or local:<os-user> on the
unauthenticated loopback path, which is granted admin because the caller
already has the data directory. A token is one of three kinds
(tokens.kind): api, minted by token.issue or adopted at init;
session, minted by a password login and expiring after session_ttl_days;
or agent, a coding session's own token. Authentication treats every kind
identically. reason is the only caller-supplied audit field, and it is
required.
People sign in with a password; agents hold tokens. A human's login lives
in password_credentials — a username, a scrypt hash and the scopes every
session they open will carry — created by user.create (vogt user create,
admin), rotated by user.set_password (which revokes their live sessions by
default) and taken away by user.remove, which leaves the actor and its audit
history in place; user.list never returns a hash. A login is throttled
after five failures on one username within a minute, and a wrong password
and an unknown username get the same answer. auth.whoami (read) tells any
caller who authentication decided they are and their effective scopes;
auth.logout revokes the token the call arrived with. Passwords never cross
the CLI's argument vector: --password-file and --password-stdin are the
only forms, with a hidden prompt on a terminal.
Scopes are read, work.write, project.write, writeback and
admin. admin implies everything; work.write and project.write each
imply only read; writeback gates exactly one operation, forge.writeback,
which arms a project's upstream pushing. Scopes are instance-wide. Allow and
deny decisions are both recorded in auth_decisions, separately from the
write audit.
Two SQLite databases with different write disciplines:
declared.sqlite3— what you assert: projects, work items, relations, labels, initiatives, actors, tokens, suppressions, contract adoptions and exemptions, coding sessions, drift proposals, workflow definitions, theeventsfeed and theauditlog. Every write goes through the application layer with an actor and a reason. A project also carries anagent_policy: text a commit, branch or PR must not match, whether a merge to a named branch deploys production, guidance (a commit identity, a test recipe, free-text rules), and three optional structured blocks —landing(base branch, branch pattern, who lands, who merges and how, who deploys),reviewandverification.project.updatereplaces it wholesale. Only the first two are enforced, and only by the session hooks the engine writes (§4.2); the rest is told to the agent in its brief and nothing else acts on it.observed.sqlite3— what collectors found:sweeps(coverage records) andobservations(immutable evidence rows), the derivedlatest_observationsandlatest_dep_refstables rebuilt at sweep completion, and the forge sync's bookkeeping (sync_state,subject_seen). Nothing writes it except collectors.
Cross-store joins happen in the application layer, never by SQL ATTACH, so
each store stays independently portable and restorable.
A collector runs over registered projects and appends observations under a sweep record naming its collector, scope, start, finish and outcome. The sweep is what makes "absent" different from "not collected": "vanished upstream" is asserted only when a completed sweep provably covered the subject.
| Collector | Needs | Produces |
|---|---|---|
git-local |
nothing | git.checkout, git.tag, git.branch |
source-markers |
nothing | marker (TODO/FIXME occurrences, with a promoted flag) |
dep-refs |
nothing | dep_ref (internal-looking path/git references read from Cargo.toml, package.json, pyproject.toml) and one dep_scan receipt per project |
mirrored-source |
nothing | mirrored_source where a path member of one project is also a registered project, matched on declared package name |
contract-checker |
nothing | contract.check — on demand only; a plain sweep never runs it |
session-outcomes |
a configured engine | session.outcome, agent_task.run |
forge-issues, forge-prs |
a forge token | forge.issue, forge.pull_request, synced all-state and incrementally by watermark, plus a forge.sync receipt |
forge-checks, forge-releases, forge-labels, forge-posture, forge-notifications |
a forge token | ci.check, release, forge.label, forge.posture, forge.notification |
deploy-lanes |
deploy_lanes configured |
deploy.lane — per lane, the deployed SHA (version endpoint or pipeline receipt) compared with the lane's branch |
CI is read two ways (core/ci_alerts.py). core/checks.py rolls a project
up by its newest revision — right for a pull request. Watched refs (default
branches, prod, release tags) are read by lane, one workflow per ref (tags:
per tag pattern), so a release-tag failure stays an Inbox alert until a later
run in that lane succeeds rather than vanishing when another commit becomes
newest. Branches bound to work items are read per branch; after a sweep the
application layer announces a fresh settled verdict once (a
ci.branch_concluded event plus a nudge into the item's live sessions) —
the collector itself still writes nothing.
Every observation carries a deterministic subject_key
(gh:{owner}/{repo}#{n}, contract:{slug}, session:{id}, …) and a content
digest; an unchanged subject in a new sweep writes no new row, so growth is
proportional to change, not to polling. While serve runs,
CollectorSchedule (adapters/http/scheduler.py) runs the collectors in the
background every sweep_interval_seconds (default 900), one sweep at a time,
as a service principal; sweep is also callable on demand from every
transport. Each registered project carries exclusion patterns applied before
collection, so vendored and generated trees never become observations.
- Promotion by convention. Only markers matching
marker_promotion_patterns(defaultTODO(vogt),FIXME(vogt)) enter backlog and bug views; every other marker is still observed and queryable. - Suppression.
suppressis an audited write keyed on asubject_keyor a pattern, with a required reason, that removes a subject from ranked and aggregated views permanently, surviving re-observation;suppression.revokeundoes it. - Adoption.
work.adoptpromotes an observed subject into a declared work item withorigin=adoptedand awork_linksrow; drift then keeps the pair honest.
A project carries a persisted link_state. On a linked project — one
attached to a forge repository by forge.link, forge.publish or
project.import — the work items are the mirrored forge issues. The
observed mirror is the truth for title, body, labels and open/closed; the
work_overlay table carries the local half (workflow state, priority,
effort, assignee, initiative, bound branches), keyed by the forge subject.
services/upstream.py is the one place the join happens, so every surface
returns each upstream issue exactly once. Writes go through synchronously:
the provider call runs before the declared transaction, so a failed upstream
write commits nothing locally. At link time a project's open native work
items migrate upstream and are retired by marker (superseded_by), never
deleted.
The forge write surface is append-only by construction: comment,
create_issue, add_labels, set_state, create_repo, and one bounded
update_issue_body that initiative.publish uses to re-render a managed
region of a tracking issue. The ForgeProvider interface has no name for
delete, history rewrite or force-push. Comments flow outbound only: a comment
authored on the forge stays an observation.
The drift engine (core/drift.py, services/drift_service.py) reads both
stores and writes only drift_proposals. A proposal never silently mutates
declared data: a person or an authorised agent accepts, rejects or leaves it
contested through drift.resolve. The kinds are version_mismatch,
unresolved_dependency, broken_path_dependency, forge_state_mismatch,
vanished_upstream, ci_red_vs_healthy, update_automation_gap,
referenced_issue_state_mismatch, initiative_checkbox_drift and
initiative_tracking_close.
The default policy is low-risk auto-accept: version_mismatch and
forge_state_mismatch in the closing direction may be accepted by an agent;
reopening finished work, and everything destructive or structural, is
human-gated with the reason named. Every proposal carries a self-contained
evidence snapshot taken at raise time and pins the observations it references
against retention pruning, so a proposal never outlives its evidence. A
proposal whose raising condition a later completed sweep no longer reproduces
is marked superseded_at rather than closed: it stays open and still needs a
person. Contract violations are not drift; they are a computed status on the
project (§8) with no declared counterpart to disagree with.
Trust is computed, never hand-set (services/views.py: trust_for):
| State | Meaning |
|---|---|
verified |
a sweep within the last verify_horizon_hours (default 24) confirmed the linked subject |
stale |
the last confirmation is older than the horizon |
unverified |
nothing has ever confirmed it — the honest answer for declared work linked to nothing |
disputed |
an open drift proposal names it |
Confirmation is "last seen", not "last changed": subject_seen.last_confirmed_at
is touched on every forge sync batch, including those where nothing moved, so
a stable open issue does not age to stale merely because its payload has
not changed. A trust state is disputed; a drift resolution a person chose
is contested.
Every aggregate answer — a project brief, the backlog, the bugs view, the inbox — carries the freshness of the sweeps behind it and its coverage: which collectors have run over this scope and when, and which have never run. An uncollected source is a different answer from an empty one.
Provenance is a row, not a column. Every declared write lands in audit
(actor, operation, entity, reason, payload digest, transaction id, monotonic
revision) and in events (a monotonic seq that is the /events cursor) in
the same transaction. Sweep completions and CI transitions are published into
the same feed by the application layer on the collectors' behalf, so a client
never merges orderings across the two stores.
Ranking is deterministic and explainable: priority, staleness, blocking
fan-out, initiative weight, an open pull request or a recently committed
branch, and a trust penalty, each with a constant weight in
core/ranking.py. why <item> returns the per-input contributions.
A contract states what a compliant project looks like. It is sourced from
four settings read through one helper (core/contract.py) —
contract_required_files, contract_required_dirs, contract_required_meta
and contract_version — with these defaults, which are the contract this
repository holds itself to:
required_files: [AGENTS.md, README.md, LICENSE]
required_dirs: [docs/, design/, src/]
required_meta: [name, lifecycle_state, owner]
A contract whose rules differ from the built-in default while still carrying
the default version gets a digest of its rules appended (v1+3f9a2c), so a
recorded status always names the contract it was evaluated against.
Adopted, not imposed. contract.adopt is an audited declaration per
project and contract.decline withdraws it; a project that has adopted
nothing reports not_applicable. project.create scaffolds a compliant
skeleton and adopts in the same act. contract.inapplicable records, with a
reason and an author, that a criterion cannot apply to this project; it is
still reported, not counted as failing.
Evaluated on demand, and only on demand. contract.check runs against
any path, registered or not, and returns every criterion evaluated with the
failures named. Against a registered project it records compliance_status
(compliant, non_compliant, not_checked) with compliance_checked_at,
and lands a contract:{slug} observation so the evidence outlives the
column. compliance reads the last result together with its age;
not_checked is a first-class answer. A criterion is asked of the
repository, not of the disk: in a git checkout a required file must be
tracked, and a required directory needs a tracked file in it. Each failing
criterion carries its remedy — the mechanical fix (project.scaffold writes
the missing skeleton files and never overwrites) or, where the content is a
decision such as a licence, an instruction addressed to a person.
It reports and never gates. No code path treats compliance_status,
trust or drift as a precondition. Registering a non-compliant folder succeeds
and reports its status; a declined contract refuses nothing.
Schema detail is in SCHEMA.md; operator procedure is in
DEPLOYMENT.md.
$VOGT_DATA_DIR/ (in the stack: /var/lib/vogt)
declared.sqlite3 authoritative, mutable, audited, revisioned
observed.sqlite3 append-only evidence + derived tables
backups/<timestamp>/ both stores, engine state, manifest.json
Every connection opens with journal_mode = WAL, foreign_keys = ON, a busy
timeout and the configured sqlite_synchronous. A write opens its own
connection per transaction. Reads borrow from a small per-file pool in the
process (storage/sqlite/connection.py). A pooled connection is lent to one
thread at a time, always outside a transaction. It is closed rather than lent
again once a restore or clone has replaced its file. There is no external
database, cache or queue; the core needs SQLite and git.
The shell's badge counts (place.metrics) and the stored half of the Inbox
are shared within the process between callers while neither store has moved
(application/memo.py). The key is the declared revision, the newest event,
and the observed projection's generation, which every rebuild_latest bumps.
Concurrent callers share one computation (WI-1224).
Migrations are forward-only SQL files under
src/vogt/storage/sqlite/migrations/{declared,observed}/, applied by
storage/sqlite/migrator.py under a migration_lock (a lock older than
fifteen minutes is stolen so a crashed process cannot wedge the instance),
with every applied migration's checksum verified on every run. vogt init
creates an instance and is idempotent; vogt migrate brings an existing one
forward and refuses a data directory that holds none. serve migrates both
stores before it accepts traffic, and /health/ready compares each store's
applied version with the highest migration the build ships, answering 503
with the store, both numbers and the verb to run until they match.
Retention (observations.prune, retention_days, default 180) prunes
observation history under three rules in precedence order: the latest
observation per subject_key is kept indefinitely; any observation a drift
proposal references is kept; everything else older than the window goes.
Backup and restore (services/lifecycle.py) copy both stores and, when
engine_state_dir is configured, the engine's state directory as one act,
with a manifest.json recording what was taken from where. That directory
holds what the core's stores do not: archived session history (history.db),
push subscriptions and the VAPID keypair (push.json), the agent-task
registry and the assistant's interaction log (assistant-log.db). In the
shipped stack the core's data and the engine's home are two named volumes
(vogt-data, engine-home), so the data outlives a pod you reset.
Clone (clone, same module) is a restore of another instance's backup
that leaves this instance itself. It verifies the manifest as restore does,
copies the stores into a staging directory beside the live ones, migrates them
there, and sanitises the copy in one audited write before swapping it in: the
source's live tokens are revoked and its password logins and linked forge
accounts dropped, this instance's own credentials are carried in (actors
matched by identity_ref), every project's write_back becomes none,
sessions the source recorded as running are marked stopped, and the instance id
stays this instance's, with a clone stamp in meta that status reports. Of
the engine's state it copies session history only on request, and never
push.json or the agent-task registry. The operator procedure is
DEPLOYMENT.md §5.
Export and import (same module, policy in
services/instance_merge.py, decision in core/merge.py) move declared
entities between live instances rather than replacing one. Export format 2
carries projects, work items (labels, relations and the initiative link
inside each), initiatives, labels, actors, every comment and the clone
stamp, never a credential; --project narrows it to one project. import
merges such a file: a dry-run report by default, one audited write under
--apply --confirm. Entities match on stable identity (project slug, work
item id, initiative slug, label name, actor identity_ref, comment id), and
"which side changed" is measured against the clone stamp's backup time, so
only an instance pair related by clone can take one side's change without
a conflict. A change on both sides keeps the target and records the incoming
version as a comment, or fails the import under --strict. The merge is
additive: it never deletes. The full policy is DEPLOYMENT.md §5.
MCP is the primary agent surface, generated from the same registry as everything else. Two transports and one bridge:
- stdio (
vogt-mcp, orvogt mcp stdio) — an agent spawns the process and speaks newline-delimited JSON-RPC over stdin/stdout against the local data directory. No server is required; the principal islocal:<os-user>. stdout is the framing channel and every diagnostic goes to stderr. - streamable HTTP at
/mcp, on the same port as REST and the PWA. In the stack the engine proxies it to the core with the caller'sAuthorizationforwarded untouched, because the credential on an MCP request is already a core token bound to an actor. Responses stream over SSE. vogt-mcp-remote— a stdio bridge for agent products that can only spawn a local process. Configured byVOGT_URLandVOGT_TOKEN_FILE(orVOGT_HTTP_TOKEN), it forwards everything, hardcodes no tools, learns the remote's tool list as it passes, and reports version skew as one stderr warning that never blocks.
Both server transports share one dispatcher (adapters/mcp/surface.py), and
unsupported protocol versions are refused with the supported list named.
Tokens. A core token is minted by token.issue (vogt token issue),
bound to an actor and a scope set, and revocable by token.revoke. A
browser's session, minted by a password login, is a core token too and works
on /mcp as well, until it expires or auth.logout revokes it. Writes
are double-gated: the server must run with writes enabled and the principal
must hold the scope, checked at both tools/list and tools/call. Ungranted
tools are absent from the list rather than present and refusing, so an
agent's tool list is exactly what it can do.
A session started from Vogt for a project or work item registers Vogt's own
MCP server into the agent automatically, carrying a per-session token bound
to that session's actor, so the agent's writes are attributed to it. The
assistant fetches its Vogt tools from the same tools/list at the start of
every turn; a curated capability matrix decides which are readable without
confirmation, which need an on-screen approval, and which are never offered.
No default endpoint ships anywhere: a client is told where its instance is.
Two public sites run the current build against seeded, read-only data, with no sign-in and nothing persisted:
- vogt-demo.thedancingdeveloper.com — the desktop app.
- vogt-mobile-demo.thedancingdeveloper.com — the same app in a phone frame, as the Android shell wraps it.
Both are the ghcr.io/thedancingdeveloper-org/vogt-demo image, the
demo-runtime target of engine/Dockerfile. The demo is the shipped Solid
PWA selected at runtime, not a fork or a screenshot site. Normal and demo
packaging share one PWA compilation: demo-build.json records the source ref
and SHA and the SHA-256 of every built asset; demo augmentation verifies those
hashes and only then adds demo-manifest.json, a static GUI-stream
illustration and mobile-demo.html. A stale hash fails the build.
Before App evaluates, the boot module (web/src/runtimeTransport.ts)
validates the manifest against demo-build.json and installs a browser-only
transport (web/src/demo/). Its deterministic state lives in
sessionStorage; HTTP, event-stream and terminal-socket requests are answered
in the browser. Terminal sessions replay the real attach ordering but accept
only a canned input list and execute nothing. Writes last only in that tab;
Reset demo restores the canonical tour.
The only server in the image is a small read-only static-file origin
(engine/deploy/demo-server.mjs) with no Python core, session engine, PTY,
proxy, workspace mount or credential, and it refuses /api and /mcp paths.
The mobile site is a second Compose project from the same signed digest with
deploy/mobile-demo.overlay.yml, which changes only the root document
(DEMO_ROOT_DOCUMENT); /index.html remains the real PWA loaded inside the
phone frame, so the two entry points cannot drift apart. Native push and
microphone behaviour are the only parts of the mobile shell the browser frame
cannot demonstrate.