Connect agents
The acs connector
acs is the Apache-2.0 Rust connector in crates/acs, built on
nats-hub-client. It manages a device's credentials; agents connect directly
to the tenant's NATS account. The current control plane is DEV ONLY:
verification auto-approves the fixed dev user. Do not expose it with real data.
Install and sign in
Release tarballs ship acs beside hub-*. scripts/install_remote.sh <HUB>
installs them into ~/.local/share/nats-hub/bin/; add that directory to PATH.
From source run make setup && make build, or use the remote installer
with --from-source --with-acs. The connector itself links no
server or database code.
acs login --server https://control-plane.example
# Or print a URL to open on another device:
acs login --server https://control-plane.example --headless
acs agent add me # orchestrator by default
acs agent add laptop-worker --role worker
acs up # install/start a user service
acs status
acs worker run echo --identity laptop-worker
acs mcp install claude --identity meOnly HTTPS control-plane URLs are accepted, except HTTP on loopback for the
stub. ACS_SERVER supplies the default server. The stub's --nats-url is the
NATS URL returned at login; pass the operator stack's private NATS_URL when
running a local demo. HTTP redirects aren't followed with bearer credentials.
acs login --key KEY exchanges a long-lived, revocable per-device secret.
Prefer ACS_SERVICE_KEY to avoid shell-history exposure. The key is exchanged
for the usual short-lived access token and rotating refresh token; the CLI
does not retain the service key. No NATS user seed is sent to the issuer.
Local state and renewal
Default state is ~/.config/acs/; ACS_HOME overrides that directory for
isolated installs/tests. Refresh tokens use the OS keychain (keyring:
macOS Keychain, Linux Secret Service). When unavailable, an atomic 0600
refresh.token file is used with a clear message. ACS_NO_KEYRING=1 forces
that fallback for headless tests. State and credential files are also 0600,
inside the private 0700 state directory. Complete files are synced and
renamed over the old file; no partial seed/JWT pair is exposed. Separate
operation and daemon locks serialize refresh-token rotation and identity edits.
creds/<identity>.creds contains that identity's locally generated nkey seed
and short-lived NATS JWT. The public key is bound at registration. Renewal
keeps the same seed/public key; only the signed JWT changes. The daemon
refreshes with a randomized 15–25 second margin, retries failures with capped
exponential backoff and jitter, and logs identity/expiry without secrets.
Rust and Python clients reread the credentials on reconnect, including the
reconnect triggered by actual JWT expiry.
acs up writes ~/.config/systemd/user/acs.service on Linux and
~/Library/LaunchAgents/com.agentmemorylabs.acs.plist on macOS. The service
uses the running executable's absolute path and explicit ACS_HOME.
acs up --foreground runs without a service manager, useful on servers,
containers and in tests. acs down stops the daemon. acs agent rm <id>
revokes renewal, removes its local state/credential, and is serialized with
the daemon. acs logout stops the daemon, revokes the device (including its
service keys), and removes local credentials and tokens. Logout requires a
reachable control plane so it never claims remote revocation succeeded offline.
MCP configuration entries remain; remove them using your client's MCP UI/CLI.
The DEV ONLY stub does not push NATS account revocations. Removing an identity or device immediately stops new issuance/renewal; already issued NATS JWTs remain valid until expiry. Production must push resolver revocations as in OPERATOR_MODE.md.
MCP and workers
The installer merges only nats-hub; unrelated client settings/servers remain.
It writes command, args, and the three environment variables NATS_URL,
NATS_CREDENTIALS_FILE, NATS_HUB_IDENTITY. It never puts a token/seed into
client configuration. Choose --identity when more than one exists.
- Claude Code:
~/.claude.json,mcpServers.nats-hub(user scope). - Codex:
~/.codex/config.toml,[mcp_servers.nats-hub](MCP configuration). - Hermes:
~/.hermes/config.yaml,mcp_servers.nats-hub(configuration reference).
--home <dir> changes the client home, while ACS_HOME changes connector
state. The config uses absolute credential/runtime paths, so tests can launch
exactly what it generates. TOML comments are retained; YAML/JSON formatting
may be normalized. Invalid config types fail without overwriting the file.
The installer never adopts code from the current directory: the interpreter
and script it finds are written into the agent's config and run on every
session with the identity's credentials. It looks next to the acs binary
and in ~/.local/share/nats-hub/remote (the worker bundle). To use a checkout,
say so explicitly with ACS_MCP_SERVER/--server (and optionally
ACS_PYTHON/--python). A standalone binary embeds the canonical Python modules,
writes them under ACS_HOME/mcp/server, and creates a private venv with pinned
nats-py and the MCP dependencies. This first install needs Python 3.10+ with
venv/pip and network access. It does not embed a Python interpreter.
acs worker run <backend> --identity <id> -- <extra args> wraps an existing
Python worker from ACS_WORKER_DIR, or by default from the worker bundle in
~/.local/share/nats-hub/remote (never the current directory), and
optionally ACS_PYTHON. hub-worker uses the sibling binary or PATH and
accepts its usual extra flags, for example -- --execute cat. The wrapper
sets the same NATS environment as MCP and transfers signals/exit status to
the worker. Worker code and model CLIs still need to be installed separately.
DEV ONLY endpoints and test
The existing OAuth/MCP routes remain unchanged. Device endpoints are:
POST /device/code: form-encoded publicclient_id=acs; returns RFC 8628 code, user code, verification URL, expiry and interval.GET|POST /device/verify: auto-approve a user code; scripted POST acceptsuser_code.POST /device/token: form-encoded device-code grant (publicclient_id=acsrequired), rotatingrefresh_token, orservice_keyexchange.GET|DELETE /v1/device: inspect/revoke the authenticated device.POST /v1/device/identities: register{identity, public_key, role?}in the token's tenant.DELETE /v1/device/identities/<id>: revoke an owned identity.POST /v1/credentials/nats: device bearer token plus{identity, public_key}; no endpoint service key. Tenant, device, revocation and registered key are checked.POST /device/service-key: DEV admin bootstrap withX-ACS-Admin-Key(from--admin-key-file, default<service-key-file>.admin; deliberately not the MCP endpoint's service key); returns a new device and its one-time displayed secret.DELETEwith{device}and the same admin header revokes that bot device and key.
Device access-token audience is the control-plane issuer. These tokens cannot
be used as remote MCP tokens. Secrets are persisted hashed in the stub; refresh
replay revokes the device. Identity names obey check_identity, are unique
within a tenant (including revoked names), and roles are worker/orchestrator.
make setup && scripts/dev/install_nsc.sh && make build
scripts/dev/operator_stack.sh .venv/bin/python -m pytest -q -s tests/python/test_acs_e2e.pyThis CI test starts the stub, uses real RFC 8628 verification, registers me,
installs Claude config, launches exactly that stdio MCP configuration, and gets
an echo result. It runs acs up --foreground with 60-second NATS credentials,
proves the same Rust HubClient and Python MCP/worker survive expiry, rejects
an unrefreshed copy, and confirms identity removal prevents renewal. The
unit/CLI tests also cover service keys, device/tenant boundaries, refresh
rotation/replay and preservation of all three clients' unrelated settings.
Known limits (security review, 2026-10-21)
- One OS user = one trust domain. Every identity's seed (
creds/*.creds) and, without a keychain, the device refresh token live underACS_HOME, readable by any process of that user. An LLM worker with shell access can read the orchestrator's credentials. Run untrusted workers as a separate OS user or in a container that only gets its own.creds. - Revocation is not instant for issued credentials.
acs agent rmandacs logoutstop renewal; NATS JWTs already issued stay valid until they expire (stub default 1 h). The production control plane should push account revocations. - Refresh rotation is strict. A refresh token replayed after a lost response revokes the device (no grace window yet).
acs login --key KEYputs the key on the command line (visible in/proc); preferACS_SERVICE_KEY.- Config files. Merging rewrites
~/.claude.json/~/.codex/config.toml/~/.hermes/config.yamlthrough a temp file and rename: a symlinked config becomes a regular file, YAML comments are dropped, and a concurrent write by the agent itself can be lost. - The stub's device flow is DEV ONLY:
/device/verifyapproves on GET, user codes are short and unthrottled, and identity names are never reusable. Production needs an authenticated, CSRF-protected approval page, rate limits and quotas.