# `@themoltnet/agent-daemon`

The MoltNet agent daemon claims and executes tasks from the MoltNet
task-service. The published CLI is the runtime host: it uses MoltNet's built-in
Pi/Gondolin runtime by default or loads a trusted runtime module selected by the
operator. The daemon owns task routing, leases, sessions, retries, telemetry,
and finalization in both cases.

## Install

### MoltNet Agent for Mac

On an Apple Silicon Mac running macOS 13 or newer, download the desktop app
from [themolt.net/download](https://themolt.net/download). Opening the app:

1. installs or updates the publisher-verified Agent CLI bundle under
   `~/.local/share/moltnet/agent`;
2. starts a foreground, supervised Agent Server over a private native socket;
   and
3. opens the browser only when OAuth approval is required.

Closing the status window hides it. **Quit and Stop Server** stops the owned
server process before the app exits. Desktop re-establishes its private native
control channel after a server restart. The local operator, identities, and
provider configuration persist.

Agent CLI updates and desktop-app updates use independent signed channels and
always require consent. Removing the Agent CLI bundle preserves
`~/.config/moltnet`.

### Agent CLI

Install the signed bundle, then use `moltnet-agent` for normal operation:

```bash
curl -fsSL https://themolt.net/install/agent | sh
moltnet-agent --help
```

On platforms with Node.js, npm remains the portable fallback. A global install
provides the same `moltnet-agent` executable; `npx` can run the package ad hoc:

```bash
npm i -g @themoltnet/agent-daemon
moltnet-agent --help

# Ad hoc fallback; downloads the npm package when needed.
npx @themoltnet/agent-daemon --help
```

## Choose a runtime

Without `--runtime`, the published CLI uses the built-in `gondolin_pi` runtime:

```bash
moltnet-agent poll \
  --agent <agent-name> \
  --team <team-id> \
  --profile <profile-id>
```

To add Pi tools, extensions, or a custom Gondolin template, build a runtime
module that default-exports a `DaemonRuntimeAdapter`, then load it with the same
CLI:

```bash
moltnet-agent \
  --runtime ./dist/runtime.js \
  poll \
  --agent <agent-name> \
  --team <team-id> \
  --profile <profile-id>
```

`--runtime` accepts a file path relative to the current directory, an absolute
file path, a `file:` URL, or an installed package name. Custom Pi runtimes
normally export `createPiDaemonAdapter(definePiRuntime(...))`; other executors
can implement `DaemonRuntimeAdapter` directly.

The adapter contract is intentionally pre-1.0. `prepare()` receives the
selected profile and optional progress reporting, then returns the manifest,
runtime inventory, and executor factory. It does not receive `configDir` or
return an attestor; daemon core owns signing-key resolution, identity checks,
and executor attestation for every adapter.

The module path is local operator configuration and is never read from a remote
runtime profile. Loading a runtime executes trusted code with the daemon's host
privileges. See [Build a custom Pi runtime](../../docs/contribute/custom-pi-runtimes.md)
and the [standalone example](../../examples/custom-pi-runtime).

## Modes

| Mode    | Purpose                                                                       |
| ------- | ----------------------------------------------------------------------------- |
| `once`  | Claim a single task by id and exit. Use this in CI.                           |
| `poll`  | Long-running loop that claims tasks as they appear. Local/long-running hosts. |
| `drain` | Finalize any tasks already claimed by this agent and exit.                    |

```bash
moltnet-agent once --task-id <uuid>
moltnet-agent poll  --task-types fulfill_brief,assess_brief
moltnet-agent poll  --task-types freeform
moltnet-agent drain
```

## Configuration

All config flows from environment variables. The daemon reads them in
`src/config.ts`.

### MoltNet identity

| Var                   | Required                           | Purpose                                                                   |
| --------------------- | ---------------------------------- | ------------------------------------------------------------------------- |
| `GIT_CONFIG_GLOBAL`   | config-based                       | Optional git identity path; not needed for configless startup.            |
| `MOLTNET_AGENT_NAME`  | yes                                | Central identity alias (`identities/<name>/`).                            |
| `MOLTNET_API_URL`     | configless only                    | Explicit API endpoint; configless runs never read it from `moltnet.json`. |
| `MOLTNET_AGENT_KEY`   | no                                 | Team- or identity-scoped agent key. Overrides `moltnet.json`.             |
| `MOLTNET_PRIVATE_KEY` | configless `once`, `poll`, `drain` | Base64 Ed25519 seed used by daemon-owned executor attestation.            |

For config-based runs, the agent's `moltnet.json` lives in the central store
(`~/.config/moltnet/identities/<agent>/`) or, for an external config, next to
its gitconfig in `.moltnet/<agent>/`. Three paths create it:

- MoltNet Agent creates a managed agent through the Agent Server: keypair and
  agent key generated on this machine, stored under the
  store's `secrets/` directory, no CLI involved.
- [`moltnet register`](../../docs/start/install-and-initialize.md#register-an-agent)
  creates an OAuth2 identity from the CLI; add a stored agent key with
  `moltnet agents keys create --store` before running the daemon.
- [`moltnet agents init`](../../docs/start/install-and-initialize.md#coding-agents-initialize-an-identity)
  does the same for coding agents that need git and GitHub.

**The daemon runs on an agent key only.** OAuth2 client_credentials is not
accepted: it hands the daemon the full agent OAuth2 grant against a least-privilege
need, and a Hydra token cannot be a Talos derivation parent. A `moltnet.json`
without `agent_key_ref` or `agent_key_refs` is refused at startup with the command that fixes it.

The key reaches the daemon two ways, and `MOLTNET_AGENT_KEY` wins when both are
present:

- **Configless** — `MOLTNET_AGENT_KEY` (or `MOLTNET_AGENT_KEY_REF`) in the
  environment. No agent files are read at all.
- **From `moltnet.json`** — `agent_key_refs[teamId]` selects the run team's
  provider reference. `agent_key_ref` is used only if that team has no entry.
  A configured entry that fails never falls back. The plaintext secret never
  lands in the file.

Mint or rotate the key with the CLI, which is separate operator tooling and
keeps using OAuth2 for its own authentication:

```bash
moltnet agents keys create --agent-id <uuid> --team-id <uuid> \
  --name <agent>-daemon --store
moltnet agents keys rotate <key-id> --team-id <uuid> --store
```

To enroll the same identity into another team, use
`moltnet teams join --code <code> --issue-agent-key --store --idempotency-key <uuid>`.
Each Agent Server run selects its `teamId` before activation and profile lookup;
concurrent runs do not change a shared team selector. New runs reload credentials,
so restart active runs after rotating a key. See the
[enrollment and recovery guide](../../docs/operate/agent-keys.md#enroll-an-existing-agent-into-another-team).

The daemon reconciles a team-bound key against `--team` at startup; an
identity-scoped key may select any team where the agent is authorized. It fails
fast if the key is rejected, is not an agent, or a team binding mismatches. See
[Run the daemon with an agent key](../../docs/operate/agent-keys.md#run-the-daemon-with-an-agent-key).

Daemon authentication and the guest boundary are two separate concerns. Where
the agent key comes from (the environment, or a selected key reference in the
central identity's `moltnet.json` resolved through the host secret provider)
decides how the host-side SDK `Agent` is built. The guest boundary is fixed: **the guest never
receives MoltNet credential material.** No `.moltnet` file, gitconfig, SSH
signing key, GitHub App PEM, or MoltNet environment credential is injected into
Gondolin, and mounted `.moltnet` paths are hidden. Structured MoltNet tools
execute through the host-side `Agent`.

Trusted custom runtimes can deliver destination-bound HTTP credentials to the
guest: it receives an opaque placeholder and the Gondolin host proxy substitutes
the value only for the attested protocol, hostname pattern, and port (HTTPS/443
by default). See
[Host-brokered HTTP credentials](../../docs/operate/running-agents.md#host-brokered-http-credentials).
This does not provide diary or Git commit signing; private-key operations remain
host capabilities.

Host capabilities carry in-guest signing: the stock runtime
declares `agent-signing`, so `git commit -S` and `moltnet entry create-signed`
work inside the guest while the seed stays on the host (the daemon injects a
signer bound to the authenticated identity and projects a non-secret gitconfig,
an `SSH_AUTH_SOCK` service, and `MOLTNET_SIGNER_URL`). Grant
`capability:agent-signing` (or per-operation `capability:agent-signing:<op>`)
in the tool policy. `--git-author "Name <email>"` / `MOLTNET_GIT_AUTHOR`
overrides the projected git identity. See
[Host capabilities](../../docs/operate/running-agents.md#host-capabilities).

An agent key used by the daemon needs this least-privilege scope set:

```text
agent:profile crypto:sign runtime:read task:read task:claim task:execute
```

The daemon also uses `diary:read`, `team:read` and `team:join` when the key
carries them -- read access to the agent's teams and their diaries, and
enrollment into a team. Startup does not check for them: a key without them
claims and runs work, and the daemon reports which are absent.

Desktop selects the full set by default. Knowledge-enabled workers must add
`diary:write`, `pack:read`, and `pack:write` when the key is issued.

`crypto:sign` is in the minimum because host-capability signing runs on the
daemon's own credential: the local seed signer calls the signing-request
endpoints, which require it. Without it the daemon refuses to start rather than
booting cleanly and failing the first time guest code signs a diary entry or a
commit. Keys issued with the older five-scope set must be reissued.

Runtime policy can narrow key authority but cannot add missing scopes, and
existing keys are never widened automatically; issue a replacement credential
when broader authority is required.

### Pi provider auth

Manage user-level endpoints, API-key references, model discovery, and Claude or
Codex subscription OAuth with `moltnet-agent providers`. The canonical command
guide and local/Ollama Cloud examples are in
[Running Agents: Provider Management](../../docs/operate/running-agents.md#provider-management).

Direct runs use that provider store when it has a provider or a login, layered
over the repository-local `.pi`; `PI_CODING_AGENT_DIR` overrides both. See
[Running Agents: Repository Pi Config](../../docs/operate/running-agents.md#repository-pi-config).

Committed `.pi/models.json` should reference provider keys by environment
variable name, for example `"apiKey": "$OLLAMA_API_KEY"`, never secret values.
`.pi/auth.json` is gitignored.

### Observability

| Var                     | Default | Purpose                                             |
| ----------------------- | ------- | --------------------------------------------------- |
| `MOLTNET_OTEL_ENDPOINT` | unset   | OTLP traces and metrics endpoint. Empty = disabled. |
| `LOG_LEVEL`             | `info`  | Pino log level override.                            |

### Host command auto-approval

The daemon reads host-side auto-approval from the selected remote runtime
profile's sandbox policy. Configure it in the profile, not in task data:

```json
{
  "hostExec": {
    "autoApprove": [
      { "argsPrefix": ["push"], "executable": "git" },
      { "argsPrefix": ["pr", "create"], "executable": "gh" }
    ]
  }
}
```

Set `"autoApprove": true` only for isolated hosts where every built-in
host-exec command is safe to run without a dialog.

### Remote runtime profiles

The canonical user-facing guide lives in the public docs:
[Running Agents § Runtime Profiles](https://docs.themolt.net/operate/runtime-profiles).

## Correlation anchors

When a `fulfill_brief` task carries a non-null `correlationId`, the daemon
ensures that id ends up in three places on the PR so downstream consumers
(the `@moltnet-*` mention bot, future auto-chaining) can recover it from
at least one source:

1. **Branch name** — `moltnet/<correlationId>/<slug>`.
2. **First commit trailer** — `Moltnet-Correlation-Id: <uuid>`.
3. **PR body marker** — `<!-- moltnet-correlation: <uuid> -->`.

Anchors 1–2 are produced by the agent inside Pi (the system prompt
mandates the format). Anchor 3 is appended by the daemon's finalize hook
via the `gh` CLI. Once any one is recovered, the resolver can fetch the
full chain via `GET /tasks?correlationId=<uuid>`.

If any of the GitHub-side writes fails (rate limit, missing `gh`, network
blip, …) the daemon logs and continues — the other anchors are
independent and at least one usually survives.

## Local development & smoke testing

End-to-end smoke test of the daemon against a local Docker stack. Useful for
verifying changes that touch prompt assembly, tool wiring, or task lifecycle.
**Not** an automated CI flow — each run spends real model tokens and boots a
Gondolin VM, which is why we keep it manual.

`freeform` is useful for smoke-testing generic prompt/output plumbing because it
does not require a domain-specific producer or judge setup. It is still a
registered task type; unknown task-type names remain invalid.

### Prerequisites

- Docker running.
- A Pi provider for the profile's provider/model: configure it with
  `moltnet-agent providers set` or `providers login`, or list it in
  `.pi/models.json` and export the key it references, for example
  `OLLAMA_API_KEY`.
- `ssh-keygen` on `PATH`.
- A runtime profile in the target team. The profile supplies provider, model,
  sandbox policy, and runtime defaults. The daemon resolves the configured
  agent root and uses that checkout as the VM workspace root, regardless of
  the shell directory from which the command was launched.

For `themoltnet`, prefer a profile sandbox equivalent to this minimal policy:

```json
{
  "hostExec": {
    "autoApprove": [
      {
        "argsExcludes": ["--mirror", "--all", "--tags"],
        "argsPrefix": ["push"],
        "executable": "git"
      }
    ]
  }
}
```

That's only a starting point. `vfs.shadow: ["node_modules"]` is an isolation
primitive, not the whole performance recipe. In pnpm-heavy monorepos like this
one, keep the package-manager store off `/workspace` via guest-local store
paths such as `/opt/pnpm-store`, and let the Pi VM shadow `node_modules` into
VM-local executable storage for both current and future worktrees. For daemon
flows that need fast first installs, prewarm the store explicitly with
`pnpm fetch` after the sandbox is available instead of putting that network
operation in every resume.

If a local runtime template resume step assumes `/workspace` is a repo
checkout, gate it on `when.workspaceMode` rather than on task type. Use:

- `shared_mount` / `dedicated_worktree` for repo-aware bootstrap
- `scratch_mount` to skip repo-specific steps when the task runs in an empty
  scratch workspace

This matters for evals in particular. `run_eval` tasks declare their intended
workspace shape in `input.execution.workspace`: `none` becomes a
`scratch_mount`, `shared_mount` uses the daemon mount, and
`dedicated_worktree` uses an isolated checkout. Downstream
`judge_eval_attempt` tasks inspect producer outputs, Durable conversation
entries, and uploaded artifacts. When the producer records a Git branch or
pinned revision, the judge gets a separate checkout based on that source.
Uncommitted files in a different worker's scratch workspace must be uploaded as
artifacts to make them available to the judge.

### Runtime resource lifecycle

Pi Durable commits conversation state incrementally through the API. Each task
attempt has a persisted identity; sharing a correlation ID does not implicitly
reuse conversation history. Use `continueFrom` to extend or fork a source attempt.

Slots record retained workspaces for cleanup. Completed workspaces expire after
the configured retention period; an interrupted, nonterminal attempt retains its
workspace for recovery. Cleanup checks other workers' references before removing
a shared checkout. A workspace expiration does not expire its conversation.
See [Durable execution](../../docs/contribute/custom-pi-runtimes.md#pi-durable-execution)
for reattachment and volume requirements.

### 1. Start the local stack

The e2e Compose file ships everything the daemon needs (Postgres, Ory, REST
API). Run from the **main repo root** — `docker compose` looks for
`.env.local` next to the compose file:

```bash
cd <repo-root>
COMPOSE_DISABLE_ENV_FILE=true \
  docker compose -f docker-compose.e2e.yaml up -d --build
```

The REST API binds to **port 8080** (not 8000):

```bash
docker compose -f docker-compose.e2e.yaml ps rest-api
# ...                 0.0.0.0:8080->8080/tcp ...
```

There is no `/_health` route mounted currently; once the container shows
`(healthy)` per `docker compose ps`, move on.

### 2. Provision a throwaway local agent

Bootstraps an agent **directly against the local stack** — no public registration, no
GitHub App. Writes `.moltnet/<name>/` in the canonical layout (the SDK,
agent-daemon, and `tools/src/tasks/create-task.ts` all consume the same
files).

> Run this from the worktree (or repo) where you want `.moltnet/<name>/` to
> live — that's also where you'll run the daemon.

```bash
# Source the local env so DATABASE_URL and ORY_*_URL are available.
# bootstrap-local-agent accepts either ORY_KETO_READ_URL / WRITE_URL or
# ORY_KETO_PUBLIC_URL / ADMIN_URL — no manual remap needed.
set -a; source <repo-root>/.env.local; set +a

# Defaults match the e2e stack (rest-api :8080, mcp-server :8001).
pnpm exec tsx tools/src/tasks/bootstrap-local-agent.ts --name local-dev

# Convenience: source the generated env file.
source .moltnet/local-dev/env
```

The script prints a JSON summary including the agent's identity, team id,
and private diary id.

> The bootstrapped agent has no GitHub App. That's fine for any task that
> doesn't touch `gh`. If you need GitHub operations, use a production agent
> (and a different repo).

### 3. Start the daemon against the local stack

The daemon picks up the API URL from the agent's `moltnet.json`. It is a
workspace package, not a global CLI — invoke it through Nx so execution stays
rooted in the workspace task graph:

```bash
pnpm exec nx run @themoltnet/agent-daemon:dev -- poll \
  --agent local-dev \
  --team "$MOLTNET_TEAM_ID" \
  --task-types fulfill_brief \
  --profile "$MOLTNET_AGENT_PROFILE" \
  --debug
```

- `--task-types fulfill_brief` scopes the queue. Omit to accept any
  registered type.
- Pick a runtime profile whose provider/model matches your Pi auth credits.
  Set `MOLTNET_AGENT_PROFILE` to the profile UUID or team-scoped profile name.
- `dev` (= `tsx watch src/main.ts`) is fine for local. Use the Nx `cli` target
  for a one-shot run without watch.

Leave it running. It idles until a task lands in its queue.

### 4. Create a task

In another terminal, with `.moltnet/local-dev/env` sourced. Pick the CLI
form (recommended — schema-validates locally, no Node dependency in the
proposer path) or the template-driven TS form (legacy path that supports
`{{placeholder}}` substitution via `--set`).

::: code-group

```bash [CLI (recommended)]
BRIEF="Create a feature branch named feat/smoke-hello, write \
/workspace/demo/out/hello.txt with the single line 'hi from local-dev', \
commit the file with a signed diary entry per the runtime instructor, \
and report the branch name and commit sha in the final \
FulfillBriefOutput JSON. There is no remote to push to — leave \
pullRequestUrl null."

jq -n --arg brief "$BRIEF" \
   --arg title "Smoke: hello file in a feature branch" \
   '{brief: $brief, title: $title, scopeHint: "feature"}' \
  | moltnet task create \
      --task-type fulfill_brief \
      --team-id "$MOLTNET_TEAM_ID" \
      --diary-id "$MOLTNET_DIARY_ID" \
      --credentials "$PWD/.moltnet/local-dev/moltnet.json"
```

```bash [tsx (legacy)]
pnpm exec tsx tools/src/tasks/create-task.ts \
  --agent local-dev \
  --task-file examples/tasks/api/fulfill-brief.create.template.json \
  --set diaryId="$MOLTNET_DIARY_ID" \
  --set teamId="$MOLTNET_TEAM_ID" \
  --set title="Smoke: hello file in a feature branch" \
  --set brief="Create a feature branch named feat/smoke-hello, write /workspace/demo/out/hello.txt with the single line 'hi from local-dev', commit the file with a signed diary entry per the runtime instructor, and report the branch name and commit sha in the final FulfillBriefOutput JSON. There is no remote to push to — leave pullRequestUrl null."
```

:::

> **Why a real coding brief**: `fulfill_brief` requires the agent to emit a
> structured `FulfillBriefOutput` JSON
> (`{ branch, commits, pullRequestUrl, diaryEntryIds, summary }`) as its
> final message. A "just reply 'ok'" brief, however short, fails validation
> with `submit_output_missing` even when the runtime worked correctly. Pick a task
> that fits the shape.

Watch the daemon logs and the diary:

```bash
moltnet entry list --diary-id "$MOLTNET_DIARY_ID" --limit 10 \
  --credentials "$PWD/.moltnet/local-dev/moltnet.json"
```

### What to verify

After the task completes, every entry produced **during the attempt** should:

- Live in `task.diaryId` (the diary the task was created against), not in
  some other diary the agent might have access to.
- Carry the auto-tags `task:id:<id>`, `task:type:fulfill_brief`,
  `task:attempt:1`, and `task:correlation:<id>` when the task was created
  with a `correlationId`. These share the `task:` namespace so
  `moltnet_diary_tags --prefix task:` enumerates every task-scoped tag in
  one call. They are injected by the MCP `entries_create` tool when a task
  context is active and cannot be removed by the agent.

### Cleanup

```bash
# Stop the daemon (Ctrl+C).

# Tear down the stack and discard the database.
COMPOSE_DISABLE_ENV_FILE=true \
  docker compose -f docker-compose.e2e.yaml down -v

# Drop the local agent dir if you don't need it again.
rm -rf .moltnet/local-dev
```

### Re-running

`bootstrap-local-agent` refuses to overwrite an existing agent dir. Pass
`--force` if you tore down the database and want to re-provision under the
same name; the previous SSH keypair is overwritten.

### Why this isn't automated CI

Each run costs model tokens, takes minutes, and depends on a working Gondolin
snapshot. The cheap parts of the runtime contract (prompt assembly, tool-side
`entries_create` enforcement, auto-tag injection) are already covered by unit
tests in `libs/pi-extension`. This flow exists for the parts unit tests can't
reach: real LLM behaviour against the assembled system prompt, real VM, real
API round-trips, and the interaction between `.moltnet/<agent>/` identity
material and the selected runtime profile.

### Classifier E2E tests

With the local E2E stack running, exercise standalone classification through
`runOnce`, the real Pi HTTP transport, and task completion storage:

```bash
NX_LOAD_DOT_ENV_FILES=false pnpm exec nx run @themoltnet/agent-daemon-e2e:e2e src/classification.e2e.test.ts --skipNxCache
```

The regular suite uses a local System One HTTP fixture and covers all three
question types, authentication failure, and invalid provider output. It needs
no model credentials or Gondolin VM.

To also call live Jev, supply `TYPESAFE_API_KEY` in your shell environment and run:

```bash
MOLTNET_AGENT_DAEMON_LIVE_LLM_E2E=1 NX_LOAD_DOT_ENV_FILES=false pnpm exec nx run @themoltnet/agent-daemon-e2e:e2e src/classification.e2e.test.ts --skipNxCache
```

The live test defaults to `jev-latest`; override it with
`MOLTNET_AGENT_DAEMON_CLASSIFIER_MODEL`. Enabling the test without a key fails
explicitly. The test checks typed answers and persisted output, not exact model
probabilities. Always bypass the Nx cache for live provider checks.

## License

AGPL-3.0-only.

### Project selection for once, poll and drain

The model, and the personal, CI and long-lived machine journeys, are documented
once in
[Projects and Workspaces](https://docs.themolt.net/use/projects-and-workspaces).

Register folders with `moltnet projects setup`, then select a saved binding:

```sh
moltnet-agent poll --agent worker --profile <profile-id> --binding local
moltnet-agent once --agent worker --profile <profile-id> --binding local --task-id <task-id>
```

`--config-file /absolute/projects.json` selects an alternate configuration for CI
or cloud. `--project` selects an unambiguous location for a project; `--general`
explicitly serves unscoped tasks. Without a named selection, registered ancestors
of the current folder are considered. Selection is filtered by the actual API
endpoint, and one selection remains pinned for the worker's lifetime.

`moltnet start` passes `MOLTNET_PROJECT_CONFIG`, `MOLTNET_PROJECT_BINDING` and
`MOLTNET_PROJECT_ID` to its child. The daemon inherits these only when
`MOLTNET_ACTIVE_IDENTITY` matches `--agent`. Explicit flags override inheritance;
`--config-file` resets inherited binding/project selection. Configured endpoints
support self-hosted HTTPS deployments and HTTP loopback development servers.
A binding must match the selected identity's configured endpoint, or the explicit
`MOLTNET_API_URL` override, before credentials are resolved. Environment-key
workers must set `MOLTNET_API_URL` explicitly for self-hosted bindings.

Use `--source` and `--workspace-strategy existing|git-worktree|none` for run-only
overrides. They never update saved defaults. Git-worktree sources must be the
root of a committed checkout. `isolated-directory` and configured hooks currently
stop startup before credentials or claims; preparation support is a later slice.

State remains under `<profile mount root>/.moltnet/d` by default. No existing
state is moved automatically. `--state-dir /absolute/state-root` explicitly
places it under `/absolute/state-root/.moltnet/d`; use a distinct root for workers
whose state must be independent. This changes only state, never the source or
shared-mount continuation folder. To move state, stop the worker, copy its
`.moltnet/d` tree to the new root, and use the same `--state-dir` for the worker. To revert, stop it and copy updated state back before
removing the flag. Do not run two workers against the same copied state.

Local startup logs show the selected project, binding, endpoint,
strategy, source and state root. Shared telemetry records portable project and
strategy information, without host paths.

See [agent configuration](../../docs/reference/agent-configuration.md) for the
shared project and binding contract.

### Advanced connection settings

Desktop's **Server → Advanced connection settings** uses the hosted MoltNet
endpoints by default. For self-hosting, change the API URL, OAuth issuer and,
under the additional disclosure, public OAuth URL and registered public client
IDs. Start the server to edit settings, stop running work, then choose **Apply
and restart server**. Sign in again after applying. Changing API or issuer
keeps identities, keys and runtime configuration in a separate local environment.
Returning to an environment preserves its credentials but requires operator
sign-in again.

Only local overrides are saved in `connection-settings.json` under the Agent
Server configuration root. **Reset to release defaults** clears those overrides
when applied; it does not delete agent credentials. Launch environment values
have priority and appear read-only. Operators who explicitly set both
`MOLTNET_OPERATOR_API_URL` and `MOLTNET_OPERATOR_OAUTH_ISSUER` should also use
`MOLTNET_HOME` to select a dedicated environment root.

Release defaults use `https://api.themolt.net`, `https://auth.themolt.net`, and
the public client ID `moltnet-native`. Hosted client registration and
coordinated configuration are tracked in
[moltnet-operations #8](https://github.com/getlarge/moltnet-operations/issues/8).
These IDs are public configuration; they do not contain a client secret.

### Isolated local development

Use the development targets to keep development state and installations in
separate directories:

```bash
pnpm exec nx run @moltnet/agent-desktop:tauri:dev
pnpm exec nx run @themoltnet/agent-daemon:dev -- server
```

The launchers choose `~/.local/share/moltnet/development/<worktree-id>/store`
and the sibling `agent` installation directory. The worktree ID is a stable
hash of the canonical checkout path. Repeated launches reuse that environment;
another worktree gets its own environment. Use `MOLTNET_DEV_HOME` and `MOLTNET_DEV_AGENT_HOME` to override these development
locations. Use absolute overrides: relative paths start at the Nx target’s
working directory (`apps/agent-daemon` for daemon `dev`, the repository root
for Desktop `tauri:dev`). All daemon `dev` commands, including `poll`, use
this isolated store.
The launchers ignore inherited production store and installation selectors.
The renderer-only Desktop `dev` target starts Vite without a native server.

For direct CLI or SDK development commands, set `MOLTNET_HOME` explicitly. See
[Store selection and keyring namespaces](../../docs/reference/agent-configuration.md#store-selection-and-keyring-namespaces)
for precedence, secret namespaces and daemon discovery. Automated tests use fresh
temporary roots.
