A local-first, permission-controlled desktop assistant for Windows. The assistant is named JARVIS by default; the product is Local Agent.
Status: Phase 1 complete. Phase 2, Milestone 6 (controlled coding actions) in progress. Phase 1 delivered the hardened desktop shell, non-secret settings storage, an audit-log foundation, the permission-policy runtime, persisted emergency-stop state, first-run onboarding, an encrypted secret store, and CI. Phase 2 Milestone 1 added a mock-only chat foundation: a validated chat message and conversation model, a provider-independent
ChatProviderinterface, a deterministic mock provider, and a chat surface in the renderer with empty, loading, error and retry states. Milestone 2 added the provider-adapter foundation on top of that: bounded provider request/response schemas, a normalized five-category error vocabulary, an approved-provider registry, a composable timeout decorator, and safe provider-status display, with no real model provider called and no network request made anywhere in that milestone. Milestone 3 adds the first real provider: an OpenAI-compatible chat-completions adapter, living entirely in the privileged main process, reached from the renderer only through two new, narrow, schema-validated IPC channels (chat.send,chat.cancel). Sending a message is now a genuine permission-controlled action —chat.sendpasses through the same unmodified permission engine, emergency-stop gate and audit writer every other action type does — and the API key it uses is read from the existing encrypted secret store inside the main process and never crosses into the renderer. Milestone 4 completes the provider layer and adds streaming:glmandollamajoin it over one shared, audited HTTP transport, and assistant replies now arrive incrementally, bounded at every hop, through a single one-waychat.onChunkevent. Ollama is enforced local — a configured endpoint must be a loopback, private or link-local address, or the request is refused rather than sent — and reads no credential at all. A streamed fragment is a preview, never a message: the reply committed to the conversation is still the whole, separately validated one.anthropic,openai,claudeandgeminiremain absent. Milestone 5 adds the read-only coding workspace: the user approves one project directory in a native picker the main process owns — the renderer cannot name a directory, becauseworkspace.selecttakes no argument at all — and the application can then list, read, search and prepare a coding plan inside it, and nothing else. Every path is contained twice (lexically, then again afterrealpath, so a symbolic link cannot lead out); dependencies, build output and credential files are never opened; every listing, file and search is bounded; and nothing in this milestone can modify a file. That is structural rather than a policy: no action type, no IPC channel, no preload function and no schema can express a modification. A generated plan is inert — itsdiffis pinned tonull, it always awaits explicit approval, and approving it unlocks nothing, because there is nothing to unlock. See docs/phase-2-chat-architecture.md, docs/phase-2-provider-architecture.md, docs/phase-2-real-provider-architecture.md, docs/phase-2-provider-completion.md and docs/phase-2-coding-workspace.md for the full design.Milestone 6 adds controlled coding actions — the first three things this application can change outside its own data directory, each behind a native confirmation the renderer cannot forge or answer. It can overwrite files inside the approved project: a change is proposed once, shown as a diff, and then applied by identifier —
workspace:applycarries no path and no content, so the bytes written are necessarily the bytes that were reviewed. A backup is taken outside the project first, the latest change can be undone, and nothing can create or delete a file. It can run one of five commands (test,lint,typecheck,build,format:check) that the project itself declares — named by an enum member, never a command string, with no shell, no terminal, no inherited secrets, bounded time and output, and a kill that reaches the whole process tree and is triggered by the emergency stop. And it can read Git status and diff and create one checkpoint commit on the branch already checked out; there is no reset, checkout, branch, push or remote anywhere in the codebase, and a repository's own hooks are disabled for every invocation. See docs/phase-2-coding-actions.md for the design, the bounds, and what remains explicitly deferred (file creation and deletion, arbitrary commands, dependency installation, browser and Windows automation, and everything else Phase 2 has not reached yet).Phase 2, Milestone 8 adds local memory. Short notes you write about how you want to be worked with, in three scopes:
session, held in memory for one run of the application and never written to disk;project, kept in a file addressed by a hash of the approved project's own root path, so one project's notes are in a file another project's session never opens; andpersonal. Nothing here grants anything — a memory record declares no field for a permission, a tool or a path, and nothing reads one out of its text — and nothing here can be written by a model: a record'ssourceis an enum ofuserandimport, both stamped by the main process, so there is no value a chat reply or an agent step could be stored under. Retrieval is a bounded keyword scan returning at most eight records, never the store. See docs/phase-2-memory.md for the design, the credential screen and its stated limits, and what is deliberately absent (vector RAG, embeddings, cloud sync, telemetry, transcript storage and automatic inference).Phase 2, Milestone 9 adds the workflow engine. A saved, named, repeatable recipe for running an agent you already have: ordered steps, conditions, bounded retries, confirmation checkpoints, success criteria and limits. It sits at the narrow end of
workflow ⊆ agent profile ⊆ permission policy— every step names a tool from the fixed Milestone 7 registry, must also be allowed by the agent profile the workflow selects, and is re-checked against that profile before it runs — so a workflow introduces no new capability and cannot widen anything. It cannot start itself: its trigger is an enum with one member,manual, so a schedule, a file watch, a Git hook and an inbox are not disabled anywhere, they are not representable. Retries are capped per step and every attempt counts against the run's step ceiling, so an unbounded loop is not expressible either. A run can be paused at the next step boundary or cancelled outright, and its steps are executed by the same step runner an agent run uses — so every one of them reaches the unchanged permission engine and audit log. See docs/phase-2-workflows.md for the design, the check order, and why a configured rollback honestly reports "nothing to roll back" in this milestone.Phase 2, Milestone 10 adds Windows automation. A bounded layer over fifteen registered desktop actions — launching an approved application, opening an approved folder or website, focusing this application's own window, and running a registered script — every one fixed in reviewed source (
shared/automation/registry.ts), never configuration. A request names an id from that closed enum and nothing else: no path, no URL, no argument, no command line. Every tool routes through the same action type,automation.run, on the confirmation floor, through the unmodifiedhandleActionProposalpipeline. A run is reported successful only once Local Agent has observed a concrete verification signal — the process did not fail immediately, the OS shell reported no error, or the window was actually focused — never on the strength of having started. See docs/phase-2-automation.md for the design and the two limitations stated plainly there:focus-windowreaches only this application's own window, and cancelling a launch cannot kill a process that already started.
All rights reserved. No licence has been granted for this project.
Models propose actions. Only the permission-controlled executor performs them.
Every privileged operation is described as an inert proposal, evaluated against a default-deny permission policy, confirmed by the user when it is destructive or sensitive, performed by a single executor module, and recorded in an append-only audit log. A model's reasoning never carries authority of its own.
Phase 1 delivers a hardened desktop shell and a first-run onboarding flow. It performs no AI inference and takes no autonomous action.
- Project scaffold and Windows desktop application shell
- First-run onboarding: assistant name, user name, interface language
- Model-provider settings foundation (settings only, no model calls)
- Secure local settings storage, with secrets kept outside settings entirely
- Permission-policy foundation, audit-log foundation, emergency-stop foundation
- Documentation and automated tests
See docs/PROJECT_SPEC.md for the full specification and docs/phase-1-scope.md for what is deliberately postponed.
-
The renderer process is sandboxed (
sandbox: true,contextIsolation: true,nodeIntegration: false,webSecurity: true). It cannot reach the filesystem, spawn a process, or import Node built-ins — asserted by an end-to-end test, not just declared in the window's configuration. -
The Electron main process is the only privileged boundary.
-
The preload bridge exposes only narrow, explicitly named, typed functions —
localAgent.health,localAgent.settings.{get,update},localAgent.secrets.{status,write,clear},localAgent.chat.{send,cancel,onChunk},localAgent.workspace.{status,select,tree,file,search,plan,propose,apply,rollback,changes},localAgent.command.{list,run,cancel},localAgent.git.{status,diff,checkpoint}— neveripcRendereritself, never a generic invoke-any-channel function, and never a generic listen-to-any-channel one:chat.onChunksubscribes to a single fixed, one-way streaming channel whose payloads are schema-validated in the preload before any renderer code sees them. None of them can return a plaintext API key. Of the ones that change something:workspace.selectdoes not accept a path, since the directory is chosen by the user in a native dialog the main process owns;workspace.applyaccepts only a change identifier, so the bytes written are necessarily the ones already diffed and shown;command.runaccepts an identifier from a five-value enum, never a command string; andgit.checkpointaccepts no argument at all. There is no function anywhere on the bridge that can create a file, delete one, run an arbitrary command, or perform a destructive Git operation — asserted against the real built bridge by an end-to-end test. -
A strict Content-Security-Policy blocks remote script and network access outright; navigation,
window.openand<webview>are all denied. -
An action with no matching policy rule is denied — enforced both by the schema layer and, independently, by the runtime permission engine (
decidePermission). -
Destructive, irreversible, privacy-sensitive and security-sensitive actions require explicit confirmation, enforced in code (the confirmation floor, re-checked at decision time) rather than by policy defaults alone.
-
No side effect can run without a permission decision:
execute, the one module permitted to perform one, requires a verdict as an explicit argument and has no code path that skips it. -
The emergency stop, once engaged, denies every non-exempt action; a missing state file starts disengaged, but a corrupt or unreadable one fails safe to engaged, not the reverse. Releasing it always requires an explicit, approved confirmation — never a model's say-so, never a policy rule alone.
-
No credential is ever stored in this repository, in
.env, in.env.example, in a settings file, in a log, or in an error message. -
API keys are encrypted with Electron's
safeStorage(Windows DPAPI) before ever touching disk; writing or clearing one requires the same explicit, approved confirmation as any other sensitive action, and no code path falls back to plaintext if encryption is unavailable. -
hasApiKey, the only secret-related value the interface ever sees, is reconciled against the encrypted store on every read and write — the store is the source of truth, never the cached flag. -
Chat's provider-independent layer (Phase 2, Milestones 1–2) still makes no network request itself: every file under
src/shared/chatis bound by the same lint boundary that keepssrc/sharedfree of Electron and Node access and also blocksfetch,XMLHttpRequest,WebSocketandEventSourceas globals, and its own registry still fails closed for every one of the four approved identifiers, unconditionally. Milestones 3 and 4 add the real providers on top of that, entirely in the main process: one audited transport (src/main/chat-completions-transport.ts) makes every network request for all three adapters,src/main/chat-provider-registry.tsresolves which endpoint and which credential each may use from the existing encrypted secret store, andchat.sendreaches both only through the unmodified permission engine, emergency-stop gate and audit writer — the same pipelinesecrets.writealready uses. Ollama is enforced local and reads no credential at all.window.localAgentis called from exactly one renderer file,src/renderer/chat/ipc-chat-provider.ts, asserted by an automated source scan; the API key itself never crosses into the renderer. Assistant text — streamed in fragments or returned whole — is still rendered as plain JSX text, never as HTML, and is still never treated as authorization for anything: there is no path from a chat message, typed, streamed or received, tomain/executor.tsor to an action proposal. -
The coding workspace (Phase 2, Milestone 5) is read-only, and structurally so. The user approves one directory through a native picker the main process owns; the renderer cannot name a directory, and no action type, IPC channel, preload function or schema in this milestone can express a modification. Every renderer-supplied path is validated lexically and then re-checked after
realpath, so a symbolic link cannot lead outside the approved project. Dependencies, build output and credential files are never opened. Every listing, file read and search is bounded and reports when a bound stopped it. A generated coding plan is inert: it always awaits explicit approval, and approving it unlocks nothing. The approved path lives in memory for one application run and is never written to disk.
Full detail, including known limitations, is in docs/security-model.md; the chat-specific design is in docs/phase-2-chat-architecture.md, docs/phase-2-provider-architecture.md, docs/phase-2-real-provider-architecture.md and docs/phase-2-provider-completion.md; the workspace design is in docs/phase-2-coding-workspace.md; the agent profile and orchestration design is in docs/phase-2-agent-profiles.md; and the memory design is in docs/phase-2-memory.md; and the workflow design is in docs/phase-2-workflows.md; and the Windows automation design is in docs/phase-2-automation.md.
Application code lives in this repository. Everything else — settings,
secrets, permission policy, audit logs, emergency-stop state and memory —
lives outside it, under %APPDATA%\Local-Agent\, each in its own location.
Since Milestone 9, workflows\workflows.json holds workflow definitions —
never a credential, and never a trigger that could start one by itself.
settings.json and the encrypted secrets\secrets.enc are now reachable
from the running application through real, permission-gated IPC channels;
permission policy and emergency-stop state are still loaded read-only at
startup, with no channel of their own yet. Since Milestone 8, memory\ holds
one file per persisted scope — never a credential, and never a session note,
which is held in memory and written nowhere. Windows automation, added in
Milestone 10, adds no file at all: its tool registry is fixed in reviewed
source, not user-editable configuration, so there is nothing for it to
persist. See docs/data-locations.md.
- Windows 11 (Windows 10 not yet verified)
- Node.js 20 or newer (developed against Node 24)
- npm 10 or newer
No Rust toolchain, Visual Studio Build Tools or Python installation is required.
npm installCopy the example environment file if you want to change development defaults. It contains non-secret settings only.
cp .env.example .envnpm run typecheck # tsc --noEmit
npm run lint # eslint .
npm run format:check # prettier --check .
npm test # vitest run — unit tests only, no build required
npm run test:e2e # build, then drive the real app with Playwright + Electron
npm run build # tsc (main) + Vite (preload, bundled) + Vite (renderer)
npm run verify # typecheck, lint, format:check and npm test, in order.github/workflows/ci.yml runs the same checks — type-check, lint, format
check, unit tests, build, the Playwright/Electron end-to-end smoke suite, and
npm audit — on every push to main and every pull request, on
windows-latest against both the minimum supported Node version (20.x) and
the version this project is developed against (24.x). It installs from
package-lock.json (npm ci) and uses no secret of any kind: there is no
deployment or publish step. windows-latest was chosen deliberately, not for
convenience — Phase 1 targets Windows 11 only (see
docs/phase-1-scope.md), and it is also the only
runner that launches the real Electron application for the end-to-end suite
without extra scaffolding a Linux runner would need to work around a
platform this project does not ship on.
npm run package:win # builds the app, then produces a Windows NSIS installerThis runs npm run build and then electron-builder --win nsis, configured
in electron-builder.json. The installer is written
to release/Local Agent Setup <version>.exe (an unpacked, runnable copy also
lands in release/win-unpacked/); both are git-ignored, never committed.
files in that config keeps the package to exactly what the running app
needs: the compiled out/ (main process, preload) and built dist/renderer/
output, plus package.json, are included; react, react-dom and their
transitive scheduler dependency are excluded, since Vite already inlines
them into the renderer bundle and the main process never requires them at
runtime — the only node_modules package left in the packaged app is zod,
which the main process's schema validation genuinely needs unbundled. No
source file, test, secret, .env, or development-only file is included;
electron-builder excludes devDependencies automatically, and nothing under
src/, tests/ or docs/ is ever selected.
The first NSIS build downloads NSIS's own build tooling (from
electron-builder's maintained, checksum-verified binaries release, cached
under %LOCALAPPDATA%\electron-builder\Cache afterward) — this is
electron-builder's standard, expected mechanism for producing a Windows
installer and happens once per machine.
Out of scope, deliberately: code signing (the installer and its
executables are unsigned — Get-AuthenticodeSignature reports NotSigned),
auto-updates, and any publish/release step (--publish never is passed
explicitly, and nothing in this repository's configuration references an
update feed or a publish target).
src/shared/ Pure schemas, types and constants, plus chat/ (the
provider-independent ChatProvider interface, the
deterministic mock provider, the approved-provider registry
with its fail-closed adapters, the composable timeout
decorator, and the local-endpoint classifier that keeps
Ollama local), plus workspace/ (the lexical path-safety rules,
the exclusion lists, the normalized error vocabulary, and the
pure coding-plan builder), plus agent/ (the fixed tool
registry and its capability ceiling, the profile registry with
its built-ins and fail-closed resolution, the normalized error
vocabulary, and the pure bounded-orchestration decider). No
I/O, no Electron, no network — safe to import from any
process, including the renderer.
src/main/ Privileged Electron main process. Owns the BrowserWindow,
the Content-Security-Policy, navigation/window-open/webview
hardening, non-secret settings storage (paths.ts,
settings.ts), the audit-log writer (audit.ts), the
permission-policy runtime: a pure decision engine
(permissions.ts), the executor gate (executor.ts), fail-safe
policy loading (policy.ts) and the assembled request path
(action-pipeline.ts), persisted emergency-stop state with its
engage/reset operations (emergency.ts), the encrypted secret
store (secrets.ts), settings/secret reconciliation
(settings-service.ts), the per-request policy/emergency-state
loader (action-runtime.ts), the native confirmation dialog
(confirm.ts), the one audited HTTP transport every real
provider shares (chat-completions-transport.ts) with its three
adapters (openai-compatible-provider.ts, glm-provider.ts,
ollama-provider.ts — the last enforced local), the
main-process provider registry that resolves them from
settings and the encrypted secret store
(chat-provider-registry.ts), the read-only coding workspace
(directory-picker.ts — the native project picker;
workspace-paths.ts — canonical path containment;
workspace-session.ts — the approved project, in memory only;
workspace-inspector.ts — the only module that reads a user's
file; workspace-planner.ts), agent profiles and the bounded
orchestrator (agent-profiles.ts — fail-safe, atomic storage
that never persists a built-in; agent-orchestrator.ts — the
run loop, which decides nothing about permissions), local
memory (memory-store.ts — fail-safe, atomic storage, with the
session scope held in memory and never written;
memory-service.ts — the scoped operations, which never learn
a path; memory-transfer.ts and memory-picker.ts — files
outside the application, at a path only a native dialog can
choose), the workflow engine (workflow-store.ts — fail-safe,
atomic storage that refuses to edit or delete a running
workflow; workflow-runner.ts — the manual run loop, which
decides nothing about permissions and executes no step
itself), Windows automation (windows-automation.ts —
performs exactly one registered tool from a fixed registry,
never a renderer-supplied path, URL or command), and the
registered IPC channels (ipc.ts), of which chat:chunk and
workflow:progress are the only main-to-renderer events.
src/preload/ The single contextBridge. Exposes a narrow, explicitly
enumerated, typed API — never ipcRenderer, never a generic
invoke-any-channel function. Bundled into one file: a
sandboxed preload cannot require() local modules at runtime.
src/renderer/ React interface: App.tsx gates on onboardingCompleted,
Onboarding.tsx is the first-run form, chat/ is the Phase 2
chat surface (Chat.tsx, useConversation.ts, the
framework-independent conversation-controller.ts,
useActiveChatProvider.ts — the one place that resolves which
provider is active — and, since Milestone 3,
ipc-chat-provider.ts, the one file in this directory
permitted to call window.localAgent, reaching the real
provider through chat.send/chat.cancel), and workspace/ is the
Milestone 5 read-only coding surface (Workspace.tsx, the
framework-independent workspace-controller.ts, useWorkspace.ts,
and ipc-workspace-client.ts — the second file permitted to
call window.localAgent), and agent/ is the Milestone 7 profile
and run surface (Agents.tsx, the framework-independent
agent-controller.ts, useAgent.ts, and ipc-agent-client.ts —
the third file permitted to call window.localAgent), and
memory/ is the Milestone 8 Memory Centre (Memory.tsx, the
framework-independent memory-controller.ts, useMemory.ts, and
ipc-memory-client.ts — the fourth file permitted to call
window.localAgent), and workflow/ is the Milestone 9 Workflow
Dashboard (Workflows.tsx, the framework-independent
workflow-controller.ts, useWorkflow.ts, and
ipc-workflow-client.ts — the fifth file permitted to call
window.localAgent), and automation/ is the Milestone 10
Automation panel (Automation.tsx, the framework-independent
automation-controller.ts, useAutomation.ts, and
ipc-automation-client.ts — the sixth and last file permitted
to call window.localAgent).
No Node, no Electron, no direct filesystem or network access
anywhere else in this directory — only the bridge at
window.localAgent.
tests/unit/ Unit tests. `npm test`.
tests/e2e/ Playwright + Electron smoke test against the built app.
`npm run test:e2e`.
docs/ Specification, architecture, security model, decisions.
A lint rule prevents src/shared from importing Electron, Node built-ins, or
any process-specific module. src/main and src/preload are the only layers
with OS or Electron access; src/renderer has neither.
This project is built collaboratively by several AI agents with separate, non-overlapping roles. If you are an agent working in this repository, read AGENTS.md first. Claude Code additionally follows CLAUDE.md. The permanent workflow is recorded in docs/AI_COLLABORATION.md.