# Running the server

`TnsServer` serves agents over a WebSocket at `/chat`, plus a few HTTP endpoints for health, indexing and audit. Each new session starts with an `assistant` agent, and a client can add more. `AgentFactory` builds every one of them with shell, file and git tools. This page covers starting the server from code, the tools those agents get, and when the server shuts itself down. To run the server from the command line instead, see [Server](index.md).

## Example

<!-- java-contract: src/main/java/com/example/tnsai/docs/ServerRunExample.java -->
```java
package com.example.tnsai.docs;

import com.tnsai.agents.Agent;
import com.tnsai.llm.LLMClient;
import com.tnsai.llm.providers.OllamaClient;
import com.tnsai.server.TnsServer;
import com.tnsai.server.agent.AgentFactory;
import java.nio.file.Path;
import java.time.Duration;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;

/** Compilation contract for server/run.md. */
public final class ServerRunExample {

    private ServerRunExample() {
    }

    static TnsServer run() {
        TnsServer server = TnsServer.builder()
            .port(7777)
            .llmClientSupplier(() -> new OllamaClient("qwen3:8b"))   // called once per agent
            .idleTimeout(Duration.ofMinutes(30))
            .build();
        server.start();                                   // returns once the listener is up
        return server;
    }

    static void runUntilIdle(Supplier<LLMClient> llm) throws InterruptedException {
        CountDownLatch idle = new CountDownLatch(1);
        AtomicReference<TnsServer> ref = new AtomicReference<>();
        TnsServer server = TnsServer.builder()
            .llmClientSupplier(llm)
            .idleTimeout(Duration.ofMinutes(5))
            .onIdleShutdown(() -> {                       // replaces the built-in stop
                ref.get().stop();
                idle.countDown();
            })
            .build();
        ref.set(server);
        server.start();
        idle.await();
    }

    static Agent developer(Supplier<LLMClient> llm) {
        AgentFactory factory = new AgentFactory(llm, Path.of("/srv/project"));   // the tools' sandbox
        return factory.createAgent("dev-1", "developer");
    }
}
```

`build()` throws `NullPointerException` when no `llmClientSupplier` is set. Every other option has a default.

## Builder options

| Option | Default | Notes |
|---|---|---|
| `port` | `7777` | |
| `llmClientSupplier` | required | Called once for each agent the server creates. |
| `idleTimeout` | 30 minutes | See [Idle shutdown](#idle-shutdown). |
| `onIdleShutdown` | stop the server | Replaces the built-in stop. |
| `bindPolicy` | loopback, `127.0.0.1` | See [Server hardening](../security/server-hardening.md). |
| `authConfig` | disabled | Bearer token. Required for any non-loopback bind. |
| `originPolicy` | loopback origins | Checked on the WebSocket upgrade. |
| `workspaceConfig` | process working directory | Limits the paths `/api/index` may index. |
| `healthRegistry` | the shared `HealthRegistry.getInstance()` | Checks behind `/health` and `/health/ready`. |
| `liabilitySink`, `principalProvider` | none, local identity | Accountability records for each agent. |
| `idempotencyStore` | in memory | |

`start()` returns once the listener is up. It also registers a JVM shutdown hook that stops the server. `stop()` stops the idle timer, cancels pending approvals and running chat work, and then stops the HTTP listener.

## Agents

The server builds every session agent through `AgentFactory`. You can also use the factory on its own, as `developer()` does in the example.

The role name picks a built-in goal: `assistant`, `developer`, `reviewer`, `architect` or `tester`. Any other name gets a generic goal for that role. The domain defaults to `tnsai-server`. The longer `createAgent` overloads take an explicit goal, domain, provider, model and extra tools.

Each agent also gets:

- a principal from the factory's `AgentPrincipalProvider`;
- an unrestricted authority scope that lasts 24 hours;
- a search knowledge entry over the session's index (see [Session index](#session-index)).

Provider resolution:

- With no provider, the factory calls the supplier.
- With `ollama`, it creates an `OllamaClient` for the given model, or for `glm-5:cloud` when no model is given.
- Any other provider name throws `IllegalArgumentException`.

## Tools

The tools are sandboxed to the factory's working directory. That is the directory you pass to `AgentFactory`. The server always uses the process working directory (`user.dir`), not the workspace allowlist.

| Tool | What it does |
|---|---|
| `shell` | Runs the command with `sh -c` in the working directory. It times out after 30 seconds. Commands containing `../` are refused, and so are `sudo`, `su`, `doas`, `pkexec` and `runas`. |
| `file_read` | Reads a file, up to 512,000 bytes. A larger file throws `IOException`. |
| `file_write` | Writes a file and creates missing parent directories. |
| `git_read` | Runs `status`, `log`, `diff`, `blame`, `branch`, `show` or `stash list`. Flags that delete or copy branches are refused. |
| `git_write` | Runs `add`, `commit`, `push`, `pull`, `checkout`, `branch`, `merge`, `rebase`, `stash`, `tag` or `reset`. A force push to `main` or `master` is refused. |

`file_read` and `file_write` refuse a path that leaves the working directory after normalization. They do not resolve symbolic links. The git tools time out after 60 seconds for `push` and `pull` and after 30 seconds for everything else. `shell` and the git tools cut stdout at 10,000 characters and stderr at 5,000.

Over the WebSocket, every tool call first goes through the approval filter, which classifies its risk; see [Tool approval](tool-approval.md). An agent you build with `AgentFactory` directly has no approval filter. Its tools run as soon as the model calls them.

## Session index

Each session has its own index. It combines BM25 keyword scores (weight 0.6) with vector similarity (weight 0.4). You fill it with `POST /api/index` for a directory inside the workspace allowlist, or with documents sent over the WebSocket. The session's agent searches it on every chat turn and reads the top 5 results. The index is discarded with the session. For the message formats, see [WebSocket protocol](websocket.md).

## Idle shutdown

The server counts open WebSocket connections:

- `start()` arms the timer, so a server that nobody connects to stops after `idleTimeout`.
- Each new connection cancels the timer.
- When the last connection closes, the timer starts again.
- When the timer fires, the server checks the connection count again before acting.

The timeout is counted in whole seconds, so a timeout under one second fires at once.

The default action stops the server. An `onIdleShutdown` action replaces that stop, so if the action does not call `stop()` itself, the server keeps running.

## Related

- [Server](index.md): run the server from the command line.
- [Tool call hooks](hooks.md): log or run commands around each tool call.
- [Tool approval](tool-approval.md): risk levels and approval requests.
- [WebSocket protocol](websocket.md): the messages a client sends and receives.
- [Server hardening](../security/server-hardening.md): binding, tokens, origins and the workspace allowlist.
