AL

ashlrai/locus

Developer tools
2 stars 0 forks Quality 77 Trend 28MIT

Identity plane for coding agents — wrong account, impossible. Multi-account MCP/CLI bindings so agents never act in the wrong tenant.

Overview

Pin a client, and every CLI command, MCP tool and agent session is sealed to that identity until you re-pin. No more ambient gh auth, a shared Supabase token or "whichever Vercel team was last". locus agent setup --apply writes the configs for every client it finds. By hand: locus mcp --supervised serves the pinned binding's provider tools; launch the client from the shell that ran locus hook and locus pin, and restart it after a re-pin. Registered as plain locus-mcp (what locus setup writes), the server serves control tools only (pin status, verification, locus_request_pin). Neither returns credential values. See docs/mcp.md. These tools compose: binding credentials can be Phantom (phm:) or environment (env:) references. Try it in a minute with the executable recipes (two clients, a directory allowlist, a CI session). Homebrew formula (for taps): integrations/homebrew.

README

Locus

Your coding agent can’t act as the wrong account. Pin a client, and every CLI command, MCP tool and agent session is sealed to that identity until you re-pin. No more ambient gh auth, a shared Supabase token or “whichever Vercel team was last”.

30-second start

brew install ashlrai/tap/locus          # or cargo; see Install options (npm 0.5.0 is broken)
locus quickstart                        # sample bindings, pin, whoami, doctor
eval "$(locus hook zsh)"                # or bash; exports the control capability
locus whoami                            # who your agents are acting as, right now

Connect your agent (MCP)

locus agent setup --apply writes the configs for every client it finds. By hand:

claude mcp add locus -- locus mcp --supervised    # Claude Code
codex mcp add locus -- locus mcp --supervised     # Codex

Cursor (~/.cursor/mcp.json):

{ "mcpServers": { "locus": { "command": "locus", "args": ["mcp", "--supervised"] } } }

locus mcp --supervised serves the pinned binding’s provider tools; launch the client from the shell that ran locus hook and locus pin, and restart it after a re-pin. Registered as plain locus-mcp (what locus setup writes), the server serves control tools only (pin status, verification, locus_request_pin). Neither returns credential values. See docs/mcp.md.

How it compares

Approach Where the identity lives Reach of a switch
gh auth switch gh’s shared config (hosts.yml) Every process that reads that config, until the next switch
direnv .envrc Variables exported into your shell on cd Only the variables in .envrc; the rest of the shell’s environment stays
1Password op run Secret references resolved for one command That command
Locus run / exec / pin A sealed session bound to one binding That command or session. Ambient GH_TOKEN, AWS_*, SUPABASE_*… are scrubbed, a directory’s .locus.toml can refuse other bindings, and MCP provider tools (locus mcp --supervised) are only the pinned binding’s

These tools compose: binding credentials can be Phantom (phm:) or environment (env:) references. Try it in a minute with the executable recipes (two clients, a directory allowlist, a CI session).


Install options

# Homebrew (tap live) — installs locus + locus-mcp
brew install ashlrai/tap/locus

# npm: wait for 0.5.1. The 0.5.0 on npm is the latest published version, and
# its `locus --version` loops until killed (fixed in source, ships in 0.5.1).
# Once `npm view locus-cli version` prints 0.5.1:
# (downloads a release binary, or falls back to cargo install)
npm install -g [email protected] @ashlrai/[email protected]
npx [email protected] --help
npx @ashlrai/[email protected]   # MCP server for Claude Code / Cursor

# cargo
export PATH="$HOME/.cargo/bin:$PATH"
cargo install --git https://github.com/ashlrai/locus locus-cli locus-mcp --locked

Local checkout:

export PATH="$HOME/.cargo/bin:$PATH"
cargo install --path crates/locus-cli
cargo install --path crates/locus-mcp

Homebrew formula (for taps): integrations/homebrew.


More of the CLI

# First 60 seconds
locus quickstart                 # samples · enter · whoami · doctor
# quickstart also mints the operator control capability if missing, persists it
# 0600 at ~/.locus/control_capability (respects LOCUS_HOME), and adopts it for
# the run. Export it in new shells (value never echoed):
eval "$(locus hook zsh)"
# Manual alternative: export LOCUS_CONTROL_CAPABILITY="$(openssl rand -hex 32)"

# Or explicit pin
locus init --with-samples
locus enter personal && locus whoami

# Switch client — previous identity is gone from the process env
locus enter acme && locus whoami

# Local identity dashboard (loopback UI + API)
locus dashboard                  # http://127.0.0.1:8750

# Run any command with only the pinned binding's surface
locus exec -- env | grep LOCUS_

# Wire AI clients + hub readiness
locus agent setup --apply
locus agent report --json        # ready | protected | unsafe

# Directory-local default
cd ~/clients/acme
locus workspace --default acme --allow acme,acme-ro --require-pin
locus pin          # uses .locus.toml

Shell prompt

eval "$(locus hook zsh)"
# shows [locus:acme:acme-corp] or [locus:unpinned]
# also exports the persisted LOCUS_CONTROL_CAPABILITY when the shell lacks one

Measured: wrong-client credential reachability

Scenario: your shell still holds another client’s ambient credentials (the gh auth switch or direnv leftover) and you launch a child process. Which of those can it still reach? Reproduce with python3 eval/env-reachability/redteam.py (needs locus 0.5.0 on PATH; fake canary credentials, throwaway HOME and LOCUS_HOME).

launch env credentials reachable (16 variables x 3 trials)
plain launch 48 of 48
locus run -b acme -- 0 of 48

Also measured: with locus, credential files read through $HOME (~/.config/gh/hosts.yml, ~/.aws/credentials, ~/.netrc, ~/.git-credentials, ~/.npmrc) were not reachable (0 of 15), and an agent attempt to pin a binding was refused (3 of 3).

This is not a sandbox. The same files read by their absolute real path stayed reachable under locus (15 of 15, same as a plain launch). Locus scrubs the environment and redirects HOME; it does not stop a process from opening files it already has OS permission to open. Scripted adversary, one machine, locus 0.5.0, 3 trials, no external audit.

How isolation works

locus pin acme
        │
        ▼
  Session sealed (HMAC) to binding "acme"
        │
        ├─► locus-mcp   → tools = only this binding (+ locus_whoami)
        │                 agents cannot pin (locus_request_pin only)
        │                 scope freeze: wrong project_ref → deny
        │
        └─► locus exec -- 
              ├─ scrubs ambient AWS_PROFILE / GH_TOKEN / SUPABASE_* / …
              ├─ resolves phm: / env: credential_refs into provider env vars
              ├─ private GH_CONFIG_DIR + AWS_* under ~/.locus/workers//
              └─ never injects other bindings' providers

Credential refs

Ref Resolution
phm:NAME phantom reveal --yes NAME (values only in child env)
env:VAR parent process env (CI / tests)

test: credentials are compiled-test-only and are always rejected by production binaries, regardless of environment.

Schemes are mandatory: bare names, raw tokens, empty refs, and unsupported schemes are rejected when bindings are saved or loaded. Never put raw secrets in binding files.

Legacy bare Phantom names are never silently skipped. Inspect the safe dry run with locus binding migrate-credential-refs , then persist conservative conversions with --write. Unsafe values require manual editing and are never printed.

MCP (Claude Code / Cursor)

# after Install (cargo / brew / npm)
locus enter acme
locus agent setup --apply      # MCP configs + AGENT.md (or: locus setup --client claude)
# restart Claude Code — tools: locus_whoami, supabase.scope, … + resources/prompts

# Optional: HTTP MCP for CI agents (loopback + token)
LOCUS_MCP_HTTP_TOKEN=secret locus-mcp --http 127.0.0.1:8742
# POST /mcp  ·  GET /health

MCP identity and provider-scope responses report only credential presence and source (phantom or environment); they never return credential_ref values or resolved secrets.


Core concepts

Term Meaning
Binding principal × tenant × providers × CredentialRefs × policy
Session Live pin sealed to exactly one Binding
Workspace .locus.toml — default pin + allowlist for a repo tree
CredentialRef phm:NAME / vault pointer — never the secret itself

Workspace policy discovery is fail closed: if the nearest .locus.toml exists but is unreadable or malformed, explicit pin, autopin, and --force pin stop with an error, while locus doctor reports UNSAFE.

Example binding (~/.locus/bindings/acme.toml):

[binding]
id = "bnd_acme"
alias = "acme"
tenant = "acme-corp"
description = "Acme client engagement"

[binding.policy]
default = "allow"
require_approval = ["*.delete*", "vercel.deploy.prod"]
dual_control = ["*.delete*", "vercel.deploy.prod"]  # two distinct principals
max_ttl = "8h"

[[binding.providers]]
provider = "supabase"
account = "acme-prod"
credential_ref = "phm:SUPABASE_ACME"
scope = { project_ref = "abcdefghij", read_only = true }

[[binding.providers]]
provider = "github"
account = "acme-corp"
credential_ref = "phm:GH_TOKEN_ACME"
scope = { orgs = ["acme-corp"], repos = ["acme-corp/*"] }

CLI

locus init [--with-samples] · quickstart
locus onboard [--detect-only] [--yes] [--reset]  # guided refs-only tenant setup
locus pin [alias] [--force] [--client claude] [--ns a,b]
locus enter                    # firm workflow pin
locus leave
locus whoami [--json]
locus status [--oneline] [--json]
locus exec --  [args...]
locus run -b  --      # one-shot; global pin unchanged
locus binding list|show|add|rm
locus workspace --default  [--allow a,b] [--require-pin]
locus doctor [--json]                 # SAFE|WARN|UNSAFE (exit 0/1/2)
locus watch [--once] [--require-ok]   # session heartbeat each tick (NDJSON with --json)
locus dashboard · serve               # local identity UI + API (127.0.0.1)
locus forensics export                # shareable pack (no secrets)
locus verify claim|session            # verification plane: claim score · session pack
locus approve list|grant|status|wait|deny  # require_approval / dual-control
locus notify status|on|off            # desktop banners OFF by default
locus events --last N · events export # audit tail / fleet pulse / OTLP
locus graph list|export|import        # encrypted binding graph share (no secrets)
locus ci mint|env|run                 # short-lived sealed sessions for pipelines
locus engagement init|close           # client engagement lifecycle (binding + workspace)
locus upstream list|suggest           # built-in upstream MCP recipes
locus adapter list|verify|trust       # provider adapter catalog + signature trust
locus adapter search|install|uninstall  # community adapters (executable code; see docs/adapters.md)
locus hook zsh|bash|fish
locus setup --client claude|cursor|codex|grok|gemini
locus mcp [--supervised]              # run the stdio MCP server; --supervised serves pinned provider tools
locus agent report|setup|doctor       # AI-native readiness (hub JSON)
locus goal status                     # northstar progress from GOALS.md
locus completion zsh|bash|fish        # shell completions
locus topic                     # dashboard · forensics · serve · goal · verify · …

Agency kit: examples/agency-starter/ — personal ↔ client A ↔ client B, dual-control, workspaces, offboarding. Guide: docs/agency-starter.md.

Northstar / hub: GOALS.md · docs/hub-integration.md · integrations/phantom/

Approvals:

# After an agent hits require_approval (MCP returns appr_…)
locus approve list
locus approve grant appr_… --as alice  # advisory review label only
# A second local label still cannot satisfy dual_control:
locus approve grant appr_… --as bob    # authority remains 0/2
locus events --last 20 --op approval.advisory --json
# Provider execution stays blocked pending a closed external authorization envelope.

Graph share (bindings + workspace templates only — CredentialRefs, never secret values):

export LOCUS_GRAPH_PASSPHRASE='…'   # required for encrypt/decrypt
locus graph list
locus graph export --out team.locusgraph
locus graph import team.locusgraph

CI ephemeral pins (do not touch active.json):

locus ci mint -b acme --json          # short-lived sealed session
eval "$(locus ci env -b acme)"        # export LOCUS_SESSION_ID + env
locus ci run -b acme -- npm test      # mint → run → cleanup

Override home for tests/CI: LOCUS_HOME=/tmp/locus-test locus …


Roadmap

Phase Status
0 Daemon-less control plane, pin/whoami/exec, isolation tests done
1 locus-mcp, credential resolve, adapters, scope freeze, setup done
2 Firm UX, doctor pane, run/ns, notify, local locus graph, locus ci done (0.1.1)
2.x / 0.2–0.5 Dashboard, forensics, HTTP MCP, agent report, goal loop; 0.3 dogfood polish, TTL, client add, MCP session pin-anchoring; 0.4 multi-tenant multiplexor, Anthropic/OpenAI adapters, locus switch; 0.5 signed registry manifests, hub MT drop-in you are here (0.5.1)
3 Remote binding graph sync, dual-control packs, offboard, SIEM export next
4 Adapter SDK, broader prebuilt platforms later

See GOALS.md (living milestones), PLAN.md, and DESIGN.md.


Development

export PATH="$HOME/.cargo/bin:$PATH"
cargo test --workspace
cargo build --release
./target/release/locus --help

# Full shell e2e (pin, isolation, MCP freeze/approval, dual-control,
# doctor, events, enter/run, notify, graph, ci, heartbeat, dashboard health,
# forensics, goal status — feature-detected)
./scripts/e2e.sh

See CONTRIBUTING.md for build/test/lint and how to add adapters. Agent-oriented rules: AGENTS.md. Short IDE guide: CLAUDE.md.

Landing page & dashboard

App Role
apps/web/ Marketing landing — AI-native identity plane hero
apps/dashboard/ Operator UI embedded by locus serve / dashboard
cd apps/web && npm start   # http://localhost:3000
locus dashboard            # http://127.0.0.1:8750 (needs built CLI)

Deploy notes (Vercel / Cloudflare Pages): apps/web/README.md.

Docs

Doc Topic
AGENTS.md AI coding agents: build, test, invariants, secrets
CLAUDE.md Short development guide
apps/web/ Landing page (static HTML)
apps/dashboard/ Local identity dashboard UI
GOALS.md Northstar goal loop
scripts/e2e.sh End-to-end shell suite
docs/architecture.md System diagram (DESIGN distilled)
docs/agency-certainty.md Identity vs epistemic certainty (Ashlr stack)
docs/firm-mode.md Agencies: bindings, dual-control, workspaces
docs/agency-starter.md Agency starter kit + doctor single pane
examples/agency-starter/ Bindings, workspaces, dual-control, offboarding
docs/phantom-enrolled-repos.md Phantom workbench: personal / company / client scope bindings per enrolled repo
docs/mcp.md locus-mcp with Claude Code / Cursor
docs/onboarding.md Agency onboarding: 3 agent clients × 3 tenants end-to-end
docs/adapters.md Writing a provider adapter
docs/workers.md Synthetic vs MCP stdio workers
SECURITY.md Threat model summary & reporting
CHANGELOG.md Release notes
DESIGN.md / PLAN.md Full architecture & roadmap

Daily firm workflow

locus engagement init acme --tenant acme-corp --workspace
locus enter acme
locus whoami
locus exec -- gh pr list
locus run -b personal -- npm test    # one-shot; global pin unchanged
locus leave

locus approve list
locus approve grant appr_… --as mason  # advisory only; never execution authority
locus notify status                  # OFF by default (no spam)
locus doctor                         # SAFE | WARN | UNSAFE

# Encrypted binding-graph share (CredentialRefs only — never secret values)
export LOCUS_GRAPH_PASSPHRASE='…'    # or interactive TTY prompt
locus graph export --out team.locusgraph
locus graph import team.locusgraph

# CI / ephemeral sealed pin (does not touch active.json)
locus ci mint -b acme --json         # session_id + env map; no secrets by default

Desktop notifications are off by default. Agents create many pending approvals; banners only after locus notify on or LOCUS_NOTIFY=1 (silent, rate-limited). Kill switch: locus notify off / LOCUS_QUIET=1.

Security model

  • Session pins use V3 HMAC seals plus a supervised live authority broker. The broker binds the exact record digest, backing file, authority, expiry, and monotonic generation, so reading daemon.key alone cannot forge or replay current authority.
  • Workspace allowed_bindings blocks wrong-tenant pins (unless --force, audited).
  • locus exec, locus run, and locus ci run scrub ambient identity; resolved credentials are injected into the child by default. Their shared --no-resolve preflight expands recipe defaults and fails before child, worker, session, or credential effects when a declared upstream can resolve credentials. Credential-free upstreams remain usable.
  • MCP never returns secret values; agents cannot pin (request only).
  • Scope freeze: model cannot override frozen project_ref / team_id.
  • Policy: globs + structured [[rules]], require_approval, dual-control (2 principals).
  • Upstream MCP workers start only after an authorized provider call. tools/list is discovery-only, multi-provider startup rolls back on partial failure, and each worker receives only its named provider’s resolved credential keys.
  • Drift freeze + hub heartbeat: locus watch [--json] [--require-ok] re-runs session verify each tick; doctor re-pin if binding changes under a session.

Details: SECURITY.md. Threat model: DESIGN.md §9.


Part of the Ashlr ecosystem

Locus is one of the open-source tools from Ashlr, a forward-deployed engineering company. Its siblings: Lexicon, Phantom Secrets, Ashlr Stack, Ashlr Markdown, Ashlr plugin.

Contributing

Issues and PRs are welcome. Look for good first issue to get started, and read CONTRIBUTING.md. Report security problems privately per SECURITY.md.

License

MIT — see LICENSE.

Code of Conduct: CODE_OF_CONDUCT.md.

View this README on GitHub

Install

npx [email protected] --help

Configuration

{ "mcpServers": { "locus": { "command": "locus", "args": ["mcp", "--supervised"] } } }