From 2a26eb23e52fc57a572ca54cfd45c49093fd2a72 Mon Sep 17 00:00:00 2001 From: Ali Mohammad Date: Tue, 20 Oct 2026 09:55:23 +0300 Subject: [PATCH 1/2] V23: Refuse to serve an unprotected agent on a public interface The built node-server (and the Docker image, which binds 0.0.0.0) now exits with LOUSHO_SERVER_UNPROTECTED when it would listen on a non-loopback host with no route auth (no LOUSHO_API_TOKEN, build token or auth.ts). LOUSHO_ALLOW_PUBLIC=1 keeps the old open-with-warning behaviour. Loopback hosts are unchanged. Co-Authored-By: Claude Opus 5.5 --- docs/deployment.md | 31 +++++++----- docs/errors.md | 16 ++++++- docs/hostinger-vps.md | 3 +- docs/upgrading.md | 1 + llms-full.txt | 51 +++++++++++++++----- src/deploy/adapters/docker.docker.test.ts | 5 +- src/deploy/adapters/docker.test.ts | 2 +- src/deploy/adapters/docker.ts | 10 ++-- src/deploy/adapters/node-server.test.ts | 20 ++++++-- src/deploy/adapters/node-server.ts | 17 +++---- src/deploy/nodeServer.ts | 29 ++++++++++++ src/deploy/nodeServerPublic.test.ts | 58 +++++++++++++++++++++++ src/deploy/runtime.ts | 2 +- src/utils/errorCodes.ts | 3 ++ 14 files changed, 204 insertions(+), 44 deletions(-) create mode 100644 src/deploy/nodeServerPublic.test.ts diff --git a/docs/deployment.md b/docs/deployment.md index 11d972d8..b22c1196 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -15,7 +15,7 @@ deploy the result). `tsup` must be installed (`npm install --save-dev tsup`). | Target | Output | Printed command | | ------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------- | | `node-server` | `server.ts`, `agent.config.js`, `package.json`, `dist/server.js` | `node dist/server.js` | -| `docker` | everything `node-server` writes, plus `Dockerfile` | `docker build -t lousho-agent . && docker run -p 3000:3000 lousho-agent` | +| `docker` | everything `node-server` writes, plus `Dockerfile` | `docker build -t lousho-agent . && docker run -p 3000:3000 -e LOUSHO_API_TOKEN= lousho-agent` | | `cloudflare-worker` | `worker.ts`, `agent.config.js`, `wrangler.toml`, `dist/worker.js` | `wrangler deploy` | Every target answers `GET /health` (`200 ok`) and serves the full @@ -182,9 +182,14 @@ own `verify` require `Authorization: Bearer `; anything else gets `401` with a JSON error (the token is compared in constant time). `/health` is open, and channels that authenticate themselves (a `verify` function, or a platform signature check such as Slack's) are not gated by the token. Without it the server is open: that -is fine on `127.0.0.1`, but **treat the token as required for anything that is -not on localhost** (the server logs a warning when it listens on another -interface without one, and the `docker` image listens on all interfaces). +is fine on `127.0.0.1`, but on any other interface the built `node-server` +(and so the `docker` image, which listens on `0.0.0.0`) **refuses to start** +with no auth: it exits with +[`LOUSHO_SERVER_UNPROTECTED`](./errors.md#lousho_server_unprotected). Set +`LOUSHO_API_TOKEN`, add an [`auth.ts`](./auth.md), or bind to `127.0.0.1`. To +serve an open agent on a public interface on purpose (a private network, auth +at a proxy in front), set `LOUSHO_ALLOW_PUBLIC=1`: the server then starts and +logs a warning instead. Terminate TLS in front of the server, since a bearer token travels in clear text over plain HTTP. Pass the token at run time (`docker run -e LOUSHO_API_TOKEN=...`). @@ -258,12 +263,14 @@ dependencies are included), so it runs without `npm install`: ```bash cd .lousho/build/node-server node dist/server.js # http://127.0.0.1:3000 -node dist/server.js --port=8080 --host=0.0.0.0 +LOUSHO_API_TOKEN=... node dist/server.js --port=8080 --host=0.0.0.0 ``` Like `lousho dev`, it binds to `127.0.0.1` unless you opt in to another interface with `--host=` (or `HOST=`); the port comes from `--port`, -`PORT`, or defaults to `3000`. +`PORT`, or defaults to `3000`. On any interface but loopback it needs auth +(`LOUSHO_API_TOKEN` or an `auth.ts`) or `LOUSHO_ALLOW_PUBLIC=1`, or it refuses +to start ([Auth](#auth)). On SIGINT or SIGTERM (a `docker stop`, a rolling deploy) the server drains before it exits: @@ -314,15 +321,17 @@ Reuses the `node-server` scaffold and bundle and adds a `Dockerfile` based on `node:22-slim` that copies `dist/` and runs `node dist/server.js` on port 3000 as the image's unprivileged `node` user. The image sets `HOST=0.0.0.0` - inside a container the server has to listen on all interfaces for -`docker run -p` to reach it. For a reproducible image, run +`docker run -p` to reach it, so the container refuses to start without auth: +pass `LOUSHO_API_TOKEN` (or ship an `auth.ts`), or `LOUSHO_ALLOW_PUBLIC=1` for an +open agent on purpose ([Auth](#auth)). For a reproducible image, run `npm install --package-lock-only` in the output directory once and keep the -`package-lock.json`: the Dockerfile then installs with `npm ci`. Pass provider -credentials at run time: +`package-lock.json`: the Dockerfile then installs with `npm ci`. Pass the token +and provider credentials at run time: ```bash cd .lousho/build/docker docker build -t lousho-agent . -docker run -p 3000:3000 -e OPENAI_API_KEY=... lousho-agent +docker run -p 3000:3000 -e LOUSHO_API_TOKEN=... -e OPENAI_API_KEY=... lousho-agent ``` The image creates `/app/data`, owned by `node`, and declares it a `VOLUME`. Docker @@ -331,7 +340,7 @@ there without a manual `chown`. Keep sessions across restarts by mounting a volu pointing the store at it: ```bash -docker run -p 3000:3000 -e OPENAI_API_KEY=... -v lousho-data:/app/data -e LOUSHO_STORE=sqlite:/app/data/lousho.db lousho-agent +docker run -p 3000:3000 -e LOUSHO_API_TOKEN=... -e OPENAI_API_KEY=... -v lousho-data:/app/data -e LOUSHO_STORE=sqlite:/app/data/lousho.db lousho-agent ``` A bind mount (`-v ./data:/app/data`) keeps the host directory's own ownership, so diff --git a/docs/errors.md b/docs/errors.md index e5d824a3..a88f904f 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -98,7 +98,7 @@ Find a code by area: | [Registry](#registry) | [`LOUSHO_REGISTRY_UNREACHABLE`](#lousho_registry_unreachable), [`LOUSHO_REGISTRY_ITEM_NOT_FOUND`](#lousho_registry_item_not_found), [`LOUSHO_REGISTRY_INVALID`](#lousho_registry_invalid), [`LOUSHO_REGISTRY_UNSAFE_PATH`](#lousho_registry_unsafe_path), [`LOUSHO_REGISTRY_FILE_EXISTS`](#lousho_registry_file_exists), [`LOUSHO_REGISTRY_MANIFEST_MISMATCH`](#lousho_registry_manifest_mismatch) | `lousho add` fetching or copying from a registry. | | [Sandbox](#sandbox) | [`LOUSHO_SANDBOX_EGRESS_UNSUPPORTED`](#lousho_sandbox_egress_unsupported), [`LOUSHO_SANDBOX_PATH_DENIED`](#lousho_sandbox_path_denied) | Asking a sandbox for something it cannot do on the current platform, or to write outside its root. | | [Agent directories, skills and flows](#agent-directories-skills-and-flows) | [`LOUSHO_AGENT_DIR_INVALID`](#lousho_agent_dir_invalid), [`LOUSHO_SKILL_INVALID`](#lousho_skill_invalid), [`LOUSHO_FLOW_INVALID`](#lousho_flow_invalid), [`LOUSHO_FLOW_TOOL_DENIED`](#lousho_flow_tool_denied), [`LOUSHO_FLOW_CHANGED`](#lousho_flow_changed) | Loading an agent directory, a skill, or a flow definition, and gating a flow's tool calls. | -| [Storage, deployment and integrations](#storage-deployment-and-integrations) | [`LOUSHO_STORAGE_FAILED`](#lousho_storage_failed), [`LOUSHO_STORAGE_BUSY`](#lousho_storage_busy), [`LOUSHO_TRIGGER_INVALID`](#lousho_trigger_invalid), [`LOUSHO_CHANNEL_REQUEST_FAILED`](#lousho_channel_request_failed), [`LOUSHO_DEPLOY_FAILED`](#lousho_deploy_failed) | A storage backend, a trigger, a channel request or `lousho build`. | +| [Storage, deployment and integrations](#storage-deployment-and-integrations) | [`LOUSHO_STORAGE_FAILED`](#lousho_storage_failed), [`LOUSHO_STORAGE_BUSY`](#lousho_storage_busy), [`LOUSHO_TRIGGER_INVALID`](#lousho_trigger_invalid), [`LOUSHO_CHANNEL_REQUEST_FAILED`](#lousho_channel_request_failed), [`LOUSHO_DEPLOY_FAILED`](#lousho_deploy_failed), [`LOUSHO_SERVER_UNPROTECTED`](#lousho_server_unprotected) | A storage backend, a trigger, a channel request, `lousho build` or the built server's start-up. | | [Tests and evals](#tests-and-evals) | [`LOUSHO_EVALS_INVALID`](#lousho_evals_invalid), [`LOUSHO_TEST_FAILED`](#lousho_test_failed), [`LOUSHO_CASSETTE_INVALID`](#lousho_cassette_invalid) | `defineEval()`, `mockModel` and cassettes. | | [General](#general) | [`LOUSHO_GENERIC_ERROR`](#lousho_generic_error), [`LOUSHO_AGENT_EXECUTION_FAILED`](#lousho_agent_execution_failed), [`LOUSHO_FLOW_EXECUTION_FAILED`](#lousho_flow_execution_failed), [`LOUSHO_VALIDATION_FAILED`](#lousho_validation_failed), [`LOUSHO_OPERATION_TIMEOUT`](#lousho_operation_timeout), [`LOUSHO_OUTPUT_INVALID`](#lousho_output_invalid), [`LOUSHO_BUDGET_EXCEEDED`](#lousho_budget_exceeded), [`LOUSHO_GUARDRAIL_TRIPPED`](#lousho_guardrail_tripped) | Run-level failures: a timeout, a budget or guardrail stop, invalid output, and the catch-all codes. | | [Auth](#auth) | [`LOUSHO_AUTH_CONFIG_INVALID`](#lousho_auth_config_invalid) | A route auth helper (`jwt()`, `oidc()`, `basic()`, `apiToken()`) got options it cannot verify with. | @@ -1079,6 +1079,20 @@ Cloudflare Worker target does not have. **Example:** `lousho build --target node-server` without `--agent`. +### LOUSHO_SERVER_UNPROTECTED + +**Means:** the built `node-server` (or the `docker` image) was asked to listen +on an interface other than loopback (`localhost`, `127.x.x.x`, `::1`) with no +route auth: no `LOUSHO_API_TOKEN`, no build token and no `auth.ts`. Anyone who +could reach that interface could use the agent, so it refuses to start. + +**Fix:** set `LOUSHO_API_TOKEN`, add an [`auth.ts`](./auth.md) to the agent +directory, or bind to `127.0.0.1` (`HOST` / `--host`). To serve an open agent +on purpose (a private network, auth at a proxy in front), set +`LOUSHO_ALLOW_PUBLIC=1`. See [Deployment: Auth](./deployment.md#auth). + +**Example:** `docker run -p 3000:3000 lousho-agent` with no `-e LOUSHO_API_TOKEN=...`. + ## Tests and evals ### LOUSHO_EVALS_INVALID diff --git a/docs/hostinger-vps.md b/docs/hostinger-vps.md index 6d2a8d6d..1ac41fbb 100644 --- a/docs/hostinger-vps.md +++ b/docs/hostinger-vps.md @@ -34,7 +34,8 @@ The example's environment flags are worth copying: - `-e LOUSHO_STORE=sqlite:/data/lousho.db -v lousho-data:/data` — sessions and paused approvals survive container restarts (`/data` is a named volume). - `-e LOUSHO_API_TOKEN=...` — required: every route except `/health` is then - bearer-authed. Port 3000 is plain HTTP; put Traefik (the VPS template + bearer-authed. The image listens on `0.0.0.0`, so without a token (or an + `auth.ts`) the container refuses to start (`LOUSHO_SERVER_UNPROTECTED`). Port 3000 is plain HTTP; put Traefik (the VPS template installs it) or another TLS terminator in front. Provisioning itself — purchasing, recreating, resizing a VM — is deliberately diff --git a/docs/upgrading.md b/docs/upgrading.md index b900c950..086cd6f1 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -225,6 +225,7 @@ there is one. A change marked "no opt-out" closed a security or data-loss bug. | A channel turn on a session another process is running answers `409` (`LOUSHO_SESSION_BUSY`, `Retry-After: 2`) instead of `500`. | no opt-out (retry after the delay) | | `POST /chat` on a session another process is running answers `409` (`LOUSHO_SESSION_BUSY`, `Retry-After: 2`) instead of a `200` stream holding only the error. | no opt-out (retry after the delay) | | The built server resumes runs a crash left in progress when it starts, and every 30 seconds. | `LOUSHO_RECOVER_ON_START=0` | +| The built node-server (and so the Docker image, which sets `HOST=0.0.0.0`) refuses to start on a non-loopback host with no auth (no `LOUSHO_API_TOKEN`, build token or `auth.ts`): it exits with `LOUSHO_SERVER_UNPROTECTED` instead of serving an open agent with a warning. Loopback hosts are unchanged. See [Auth](./deployment.md#auth). | set `LOUSHO_API_TOKEN` (or add `auth.ts`), or `LOUSHO_ALLOW_PUBLIC=1` to serve the open agent on purpose | ### Sessions, approvals and durability diff --git a/llms-full.txt b/llms-full.txt index d52b2a8c..b402109c 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -7075,7 +7075,7 @@ deploy the result). `tsup` must be installed (`npm install --save-dev tsup`). | Target | Output | Printed command | | ------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------- | | `node-server` | `server.ts`, `agent.config.js`, `package.json`, `dist/server.js` | `node dist/server.js` | -| `docker` | everything `node-server` writes, plus `Dockerfile` | `docker build -t lousho-agent . && docker run -p 3000:3000 lousho-agent` | +| `docker` | everything `node-server` writes, plus `Dockerfile` | `docker build -t lousho-agent . && docker run -p 3000:3000 -e LOUSHO_API_TOKEN= lousho-agent` | | `cloudflare-worker` | `worker.ts`, `agent.config.js`, `wrangler.toml`, `dist/worker.js` | `wrangler deploy` | Every target answers `GET /health` (`200 ok`) and serves the full @@ -7242,9 +7242,14 @@ own `verify` require `Authorization: Bearer `; anything else gets `401` with a JSON error (the token is compared in constant time). `/health` is open, and channels that authenticate themselves (a `verify` function, or a platform signature check such as Slack's) are not gated by the token. Without it the server is open: that -is fine on `127.0.0.1`, but **treat the token as required for anything that is -not on localhost** (the server logs a warning when it listens on another -interface without one, and the `docker` image listens on all interfaces). +is fine on `127.0.0.1`, but on any other interface the built `node-server` +(and so the `docker` image, which listens on `0.0.0.0`) **refuses to start** +with no auth: it exits with +[`LOUSHO_SERVER_UNPROTECTED`](https://github.com/LinuxDevil/agent-sdk/blob/main/docs/errors.md#lousho_server_unprotected). Set +`LOUSHO_API_TOKEN`, add an [`auth.ts`](https://github.com/LinuxDevil/agent-sdk/blob/main/docs/auth.md), or bind to `127.0.0.1`. To +serve an open agent on a public interface on purpose (a private network, auth +at a proxy in front), set `LOUSHO_ALLOW_PUBLIC=1`: the server then starts and +logs a warning instead. Terminate TLS in front of the server, since a bearer token travels in clear text over plain HTTP. Pass the token at run time (`docker run -e LOUSHO_API_TOKEN=...`). @@ -7318,12 +7323,14 @@ dependencies are included), so it runs without `npm install`: ```bash cd .lousho/build/node-server node dist/server.js # http://127.0.0.1:3000 -node dist/server.js --port=8080 --host=0.0.0.0 +LOUSHO_API_TOKEN=... node dist/server.js --port=8080 --host=0.0.0.0 ``` Like `lousho dev`, it binds to `127.0.0.1` unless you opt in to another interface with `--host=` (or `HOST=`); the port comes from `--port`, -`PORT`, or defaults to `3000`. +`PORT`, or defaults to `3000`. On any interface but loopback it needs auth +(`LOUSHO_API_TOKEN` or an `auth.ts`) or `LOUSHO_ALLOW_PUBLIC=1`, or it refuses +to start ([Auth](#auth)). On SIGINT or SIGTERM (a `docker stop`, a rolling deploy) the server drains before it exits: @@ -7374,15 +7381,17 @@ Reuses the `node-server` scaffold and bundle and adds a `Dockerfile` based on `node:22-slim` that copies `dist/` and runs `node dist/server.js` on port 3000 as the image's unprivileged `node` user. The image sets `HOST=0.0.0.0` - inside a container the server has to listen on all interfaces for -`docker run -p` to reach it. For a reproducible image, run +`docker run -p` to reach it, so the container refuses to start without auth: +pass `LOUSHO_API_TOKEN` (or ship an `auth.ts`), or `LOUSHO_ALLOW_PUBLIC=1` for an +open agent on purpose ([Auth](#auth)). For a reproducible image, run `npm install --package-lock-only` in the output directory once and keep the -`package-lock.json`: the Dockerfile then installs with `npm ci`. Pass provider -credentials at run time: +`package-lock.json`: the Dockerfile then installs with `npm ci`. Pass the token +and provider credentials at run time: ```bash cd .lousho/build/docker docker build -t lousho-agent . -docker run -p 3000:3000 -e OPENAI_API_KEY=... lousho-agent +docker run -p 3000:3000 -e LOUSHO_API_TOKEN=... -e OPENAI_API_KEY=... lousho-agent ``` The image creates `/app/data`, owned by `node`, and declares it a `VOLUME`. Docker @@ -7391,7 +7400,7 @@ there without a manual `chown`. Keep sessions across restarts by mounting a volu pointing the store at it: ```bash -docker run -p 3000:3000 -e OPENAI_API_KEY=... -v lousho-data:/app/data -e LOUSHO_STORE=sqlite:/app/data/lousho.db lousho-agent +docker run -p 3000:3000 -e LOUSHO_API_TOKEN=... -e OPENAI_API_KEY=... -v lousho-data:/app/data -e LOUSHO_STORE=sqlite:/app/data/lousho.db lousho-agent ``` A bind mount (`-v ./data:/app/data`) keeps the host directory's own ownership, so @@ -8361,7 +8370,7 @@ Find a code by area: | [Registry](#registry) | [`LOUSHO_REGISTRY_UNREACHABLE`](#lousho_registry_unreachable), [`LOUSHO_REGISTRY_ITEM_NOT_FOUND`](#lousho_registry_item_not_found), [`LOUSHO_REGISTRY_INVALID`](#lousho_registry_invalid), [`LOUSHO_REGISTRY_UNSAFE_PATH`](#lousho_registry_unsafe_path), [`LOUSHO_REGISTRY_FILE_EXISTS`](#lousho_registry_file_exists), [`LOUSHO_REGISTRY_MANIFEST_MISMATCH`](#lousho_registry_manifest_mismatch) | `lousho add` fetching or copying from a registry. | | [Sandbox](#sandbox) | [`LOUSHO_SANDBOX_EGRESS_UNSUPPORTED`](#lousho_sandbox_egress_unsupported), [`LOUSHO_SANDBOX_PATH_DENIED`](#lousho_sandbox_path_denied) | Asking a sandbox for something it cannot do on the current platform, or to write outside its root. | | [Agent directories, skills and flows](#agent-directories-skills-and-flows) | [`LOUSHO_AGENT_DIR_INVALID`](#lousho_agent_dir_invalid), [`LOUSHO_SKILL_INVALID`](#lousho_skill_invalid), [`LOUSHO_FLOW_INVALID`](#lousho_flow_invalid), [`LOUSHO_FLOW_TOOL_DENIED`](#lousho_flow_tool_denied), [`LOUSHO_FLOW_CHANGED`](#lousho_flow_changed) | Loading an agent directory, a skill, or a flow definition, and gating a flow's tool calls. | -| [Storage, deployment and integrations](#storage-deployment-and-integrations) | [`LOUSHO_STORAGE_FAILED`](#lousho_storage_failed), [`LOUSHO_STORAGE_BUSY`](#lousho_storage_busy), [`LOUSHO_TRIGGER_INVALID`](#lousho_trigger_invalid), [`LOUSHO_CHANNEL_REQUEST_FAILED`](#lousho_channel_request_failed), [`LOUSHO_DEPLOY_FAILED`](#lousho_deploy_failed) | A storage backend, a trigger, a channel request or `lousho build`. | +| [Storage, deployment and integrations](#storage-deployment-and-integrations) | [`LOUSHO_STORAGE_FAILED`](#lousho_storage_failed), [`LOUSHO_STORAGE_BUSY`](#lousho_storage_busy), [`LOUSHO_TRIGGER_INVALID`](#lousho_trigger_invalid), [`LOUSHO_CHANNEL_REQUEST_FAILED`](#lousho_channel_request_failed), [`LOUSHO_DEPLOY_FAILED`](#lousho_deploy_failed), [`LOUSHO_SERVER_UNPROTECTED`](#lousho_server_unprotected) | A storage backend, a trigger, a channel request, `lousho build` or the built server's start-up. | | [Tests and evals](#tests-and-evals) | [`LOUSHO_EVALS_INVALID`](#lousho_evals_invalid), [`LOUSHO_TEST_FAILED`](#lousho_test_failed), [`LOUSHO_CASSETTE_INVALID`](#lousho_cassette_invalid) | `defineEval()`, `mockModel` and cassettes. | | [General](#general) | [`LOUSHO_GENERIC_ERROR`](#lousho_generic_error), [`LOUSHO_AGENT_EXECUTION_FAILED`](#lousho_agent_execution_failed), [`LOUSHO_FLOW_EXECUTION_FAILED`](#lousho_flow_execution_failed), [`LOUSHO_VALIDATION_FAILED`](#lousho_validation_failed), [`LOUSHO_OPERATION_TIMEOUT`](#lousho_operation_timeout), [`LOUSHO_OUTPUT_INVALID`](#lousho_output_invalid), [`LOUSHO_BUDGET_EXCEEDED`](#lousho_budget_exceeded), [`LOUSHO_GUARDRAIL_TRIPPED`](#lousho_guardrail_tripped) | Run-level failures: a timeout, a budget or guardrail stop, invalid output, and the catch-all codes. | | [Auth](#auth) | [`LOUSHO_AUTH_CONFIG_INVALID`](#lousho_auth_config_invalid) | A route auth helper (`jwt()`, `oidc()`, `basic()`, `apiToken()`) got options it cannot verify with. | @@ -9342,6 +9351,20 @@ Cloudflare Worker target does not have. **Example:** `lousho build --target node-server` without `--agent`. +### LOUSHO_SERVER_UNPROTECTED + +**Means:** the built `node-server` (or the `docker` image) was asked to listen +on an interface other than loopback (`localhost`, `127.x.x.x`, `::1`) with no +route auth: no `LOUSHO_API_TOKEN`, no build token and no `auth.ts`. Anyone who +could reach that interface could use the agent, so it refuses to start. + +**Fix:** set `LOUSHO_API_TOKEN`, add an [`auth.ts`](https://github.com/LinuxDevil/agent-sdk/blob/main/docs/auth.md) to the agent +directory, or bind to `127.0.0.1` (`HOST` / `--host`). To serve an open agent +on purpose (a private network, auth at a proxy in front), set +`LOUSHO_ALLOW_PUBLIC=1`. See [Deployment: Auth](https://github.com/LinuxDevil/agent-sdk/blob/main/docs/deployment.md#auth). + +**Example:** `docker run -p 3000:3000 lousho-agent` with no `-e LOUSHO_API_TOKEN=...`. + ## Tests and evals ### LOUSHO_EVALS_INVALID @@ -11706,7 +11729,8 @@ The example's environment flags are worth copying: - `-e LOUSHO_STORE=sqlite:/data/lousho.db -v lousho-data:/data` — sessions and paused approvals survive container restarts (`/data` is a named volume). - `-e LOUSHO_API_TOKEN=...` — required: every route except `/health` is then - bearer-authed. Port 3000 is plain HTTP; put Traefik (the VPS template + bearer-authed. The image listens on `0.0.0.0`, so without a token (or an + `auth.ts`) the container refuses to start (`LOUSHO_SERVER_UNPROTECTED`). Port 3000 is plain HTTP; put Traefik (the VPS template installs it) or another TLS terminator in front. Provisioning itself — purchasing, recreating, resizing a VM — is deliberately @@ -20250,6 +20274,7 @@ there is one. A change marked "no opt-out" closed a security or data-loss bug. | A channel turn on a session another process is running answers `409` (`LOUSHO_SESSION_BUSY`, `Retry-After: 2`) instead of `500`. | no opt-out (retry after the delay) | | `POST /chat` on a session another process is running answers `409` (`LOUSHO_SESSION_BUSY`, `Retry-After: 2`) instead of a `200` stream holding only the error. | no opt-out (retry after the delay) | | The built server resumes runs a crash left in progress when it starts, and every 30 seconds. | `LOUSHO_RECOVER_ON_START=0` | +| The built node-server (and so the Docker image, which sets `HOST=0.0.0.0`) refuses to start on a non-loopback host with no auth (no `LOUSHO_API_TOKEN`, build token or `auth.ts`): it exits with `LOUSHO_SERVER_UNPROTECTED` instead of serving an open agent with a warning. Loopback hosts are unchanged. See [Auth](https://github.com/LinuxDevil/agent-sdk/blob/main/docs/deployment.md#auth). | set `LOUSHO_API_TOKEN` (or add `auth.ts`), or `LOUSHO_ALLOW_PUBLIC=1` to serve the open agent on purpose | ### Sessions, approvals and durability diff --git a/src/deploy/adapters/docker.docker.test.ts b/src/deploy/adapters/docker.docker.test.ts index eed81e8e..4001373a 100644 --- a/src/deploy/adapters/docker.docker.test.ts +++ b/src/deploy/adapters/docker.docker.test.ts @@ -58,7 +58,8 @@ describe.skipIf(!hasDocker)('DockerAdapter image (real Docker daemon)', () => { const tag = `lousho-agent-test-${Date.now()}`; execFileSync('docker', ['build', '-t', tag, '.'], { cwd: outDir, stdio: 'inherit' }); - const containerId = execFileSync('docker', ['run', '-d', '-p', '127.0.0.1::3000', tag], { + // The image binds 0.0.0.0 and this test calls /chat without a token: opt in to the open agent. + const containerId = execFileSync('docker', ['run', '-d', '-p', '127.0.0.1::3000', '-e', 'LOUSHO_ALLOW_PUBLIC=1', tag], { encoding: 'utf8', }).trim(); try { @@ -100,7 +101,7 @@ describe.skipIf(!hasDocker)('DockerAdapter image (real Docker daemon)', () => { execFileSync('docker', ['volume', 'create', volume]); const containerId = execFileSync( 'docker', - ['run', '-d', '-p', '127.0.0.1::3000', '-v', `${volume}:/app/data`, '-e', 'LOUSHO_STORE=sqlite:/app/data/lousho.db', tag], + ['run', '-d', '-p', '127.0.0.1::3000', '-v', `${volume}:/app/data`, '-e', 'LOUSHO_STORE=sqlite:/app/data/lousho.db', '-e', 'LOUSHO_ALLOW_PUBLIC=1', tag], { encoding: 'utf8' } ).trim(); try { diff --git a/src/deploy/adapters/docker.test.ts b/src/deploy/adapters/docker.test.ts index cb5dab5a..8cf8cb0b 100644 --- a/src/deploy/adapters/docker.test.ts +++ b/src/deploy/adapters/docker.test.ts @@ -35,7 +35,7 @@ describe('DockerAdapter', () => { registerBuiltInAdapters(); expect(getAdapter('docker')).toBe(DockerAdapter); expect(DockerAdapter.describe('/anywhere')).toBe( - 'docker build -t lousho-agent . && docker run -p 3000:3000 lousho-agent' + 'docker build -t lousho-agent . && docker run -p 3000:3000 -e LOUSHO_API_TOKEN= lousho-agent' ); }); diff --git a/src/deploy/adapters/docker.ts b/src/deploy/adapters/docker.ts index 15a29389..912f624c 100644 --- a/src/deploy/adapters/docker.ts +++ b/src/deploy/adapters/docker.ts @@ -5,13 +5,15 @@ * it (the same server.ts/agent.config.js/package.json and the same bundled * dist/server.js), and scaffold() additionally writes a Dockerfile (plus a * .dockerignore) that packages dist/ into a node:22-slim image (22 has the node:sqlite that LOUSHO_STORE=sqlite: needs). - * describe(): 'docker build -t lousho-agent . && docker run -p 3000:3000 lousho-agent'. + * describe(): 'docker build -t lousho-agent . && docker run -p 3000:3000 -e LOUSHO_API_TOKEN= lousho-agent'. * The image also owns /app/data (declared a VOLUME) for LOUSHO_STORE=sqlite:/app/data/. * * The image sets HOST=0.0.0.0: inside a container, the server's default * 127.0.0.1 binding would be unreachable through `docker run -p`, so the * image explicitly opts in to all-interfaces binding (the network boundary - * is then the container's published port, chosen by whoever runs it). + * is then the container's published port, chosen by whoever runs it). On + * that public interface the server refuses to start without auth + * (LOUSHO_API_TOKEN or an auth.ts) unless LOUSHO_ALLOW_PUBLIC=1. */ import * as path from 'node:path'; import { DeploymentAdapter, DeployOptions } from '../types'; @@ -36,6 +38,8 @@ VOLUME /app/data # Run as the image's unprivileged node user, not root. USER node # Listen on all interfaces inside the container so \`docker run -p\` can reach it. +# On 0.0.0.0 the server refuses to start without auth: pass -e LOUSHO_API_TOKEN= +# (or ship an auth.ts), or -e LOUSHO_ALLOW_PUBLIC=1 to serve an open agent on purpose. ENV HOST=0.0.0.0 ENV PORT=3000 EXPOSE 3000 @@ -68,6 +72,6 @@ export const DockerAdapter: DeploymentAdapter = { }, describe(): string { - return 'docker build -t lousho-agent . && docker run -p 3000:3000 lousho-agent'; + return 'docker build -t lousho-agent . && docker run -p 3000:3000 -e LOUSHO_API_TOKEN= lousho-agent'; }, }; diff --git a/src/deploy/adapters/node-server.test.ts b/src/deploy/adapters/node-server.test.ts index 14cb7101..69d89065 100644 --- a/src/deploy/adapters/node-server.test.ts +++ b/src/deploy/adapters/node-server.test.ts @@ -167,7 +167,7 @@ describe('NodeServerAdapter', () => { } }, 60_000); - it('requires LOUSHO_API_TOKEN as a bearer token on every route but /health, and warns when public without one', async () => { + it('requires LOUSHO_API_TOKEN as a bearer token on every route but /health, and refuses to serve publicly without one unless LOUSHO_ALLOW_PUBLIC=1', async () => { const { child, port } = await startServer(outDir, [], { LOUSHO_API_TOKEN: 'deploy-secret' }); try { const base = `http://127.0.0.1:${port}`; @@ -184,9 +184,23 @@ describe('NodeServerAdapter', () => { child.kill(); } + // No auth on a public interface: the server refuses to start ... + const refused = spawn(process.execPath, ['dist/server.js', '--port=0', '--host=0.0.0.0'], { + cwd: outDir, + env: { ...process.env, LOUSHO_API_TOKEN: '', LOUSHO_ALLOW_PUBLIC: '' }, + stdio: ['ignore', 'pipe', 'pipe'], + }); + let refusedErr = ''; + refused.stderr!.on('data', (chunk) => (refusedErr += chunk)); + const code = await new Promise((resolve) => refused.on('exit', resolve)); + expect(code).not.toBe(0); + expect(refusedErr).toContain('LOUSHO_SERVER_UNPROTECTED'); + expect(refusedErr).toContain('LOUSHO_ALLOW_PUBLIC=1'); + + // ... unless LOUSHO_ALLOW_PUBLIC=1 serves the open agent on purpose, with the warning. const open = spawn(process.execPath, ['dist/server.js', '--port=0', '--host=0.0.0.0'], { cwd: outDir, - env: { ...process.env, LOUSHO_API_TOKEN: '' }, + env: { ...process.env, LOUSHO_API_TOKEN: '', LOUSHO_ALLOW_PUBLIC: '1' }, stdio: ['ignore', 'pipe', 'pipe'], }); try { @@ -204,7 +218,7 @@ describe('NodeServerAdapter', () => { }, 60_000); it('binds to an explicitly opted-in host via --host', async () => { - const { child, host } = await startServer(outDir, ['--host=0.0.0.0']); + const { child, host } = await startServer(outDir, ['--host=0.0.0.0'], { LOUSHO_API_TOKEN: 'deploy-secret' }); try { expect(host).toBe('0.0.0.0'); } finally { diff --git a/src/deploy/adapters/node-server.ts b/src/deploy/adapters/node-server.ts index cc50eb6f..be7803b9 100644 --- a/src/deploy/adapters/node-server.ts +++ b/src/deploy/adapters/node-server.ts @@ -103,7 +103,7 @@ interface ServerVariant { function specVariant(options: DeployOptions, serviceName: string): ServerVariant { return { - imports: `import { agentSpecSchema, createDeployedAgent, createDeployedServer, otelTracingFromEnv, specSchedules, specToAgent, storeFromEnv } from '${RUNTIME_SPECIFIER}'; + imports: `import { agentSpecSchema, checkServerExposure, createDeployedAgent, createDeployedServer, otelTracingFromEnv, specSchedules, specToAgent, storeFromEnv } from '${RUNTIME_SPECIFIER}'; import agentConfig from './agent.config.js';`, boot: `const spec = agentSpecSchema.parse(agentConfig); const agent = tracing ? specToAgent(spec, { store: storeFromEnv(), exporter: tracing.exporter }) : await createDeployedAgent(spec); @@ -121,7 +121,7 @@ function agentDirVariant(options: DeployOptions, serviceName: string): ServerVar return { imports: `import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { createAgent, createDeployedServer, otelTracingFromEnv, resolveAgentDir, storeFromEnv, withOtelTracing } from '${RUNTIME_SPECIFIER}';`, +import { checkServerExposure, createAgent, createDeployedServer, otelTracingFromEnv, resolveAgentDir, storeFromEnv, withOtelTracing } from '${RUNTIME_SPECIFIER}';`, boot: `const resolved = await resolveAgentDir(path.join(path.dirname(fileURLToPath(import.meta.url)), 'agent')); // A 'store' the directory's config declares wins over the LOUSHO_STORE default; an explicit LOUSHO_STORE still wins. const store = process.env.LOUSHO_STORE === undefined ? (resolved.config.store ?? storeFromEnv()) : storeFromEnv(); @@ -144,7 +144,7 @@ import { createAgent, createDeployedServer, otelTracingFromEnv, resolveAgentDir, function agentModuleVariant(agentPath: string, options: DeployOptions, serviceName: string): ServerVariant { const file = JSON.stringify(agentPath.split(path.sep).join('/')); return { - imports: `import { agentWithOtelTracing, createAgent, createDeployedServer, otelTracingFromEnv, storeFromEnv, withOtelTracing } from '${RUNTIME_SPECIFIER}'; + imports: `import { agentWithOtelTracing, checkServerExposure, createAgent, createDeployedServer, otelTracingFromEnv, storeFromEnv, withOtelTracing } from '${RUNTIME_SPECIFIER}'; import * as agentModule from ${file};`, boot: `const exported: any = (agentModule as any).default ?? (agentModule as any).agent; if (typeof exported !== 'object' || exported === null) { @@ -228,7 +228,9 @@ function serverSource(variant: ServerVariant): string { * they are imported only when the variable is set (docs/observability.md). * * Binds to 127.0.0.1 by default; pass --host= (or HOST=) to opt in to - * listening on other interfaces. Port: --port= / PORT, default 3000. + * listening on other interfaces. On a non-loopback host it refuses to start + * without auth (LOUSHO_API_TOKEN or auth.ts) unless LOUSHO_ALLOW_PUBLIC=1. + * Port: --port= / PORT, default 3000. */ ${variant.imports} @@ -241,13 +243,14 @@ function readFlag(name: string): string | undefined { const port = Number(readFlag('port') || process.env.PORT || 3000); const host = readFlag('host') || process.env.HOST || '127.0.0.1'; -const LOOPBACK = ['127.0.0.1', 'localhost', '::1']; async function main(): Promise { // Set only when OTEL_EXPORTER_OTLP_ENDPOINT / OTEL_EXPORTER_OTLP_TRACES_ENDPOINT is. const tracing = await otelTracingFromEnv({ serviceName: ${JSON.stringify(variant.serviceName)} }); ${variant.boot} const { server, authenticated, channelRoutes, shutdown } = createDeployedServer(agent, ${variant.options}); + // No route auth on a public interface: refuse to start (LOUSHO_SERVER_UNPROTECTED) unless LOUSHO_ALLOW_PUBLIC=1. + const exposureWarning = checkServerExposure(host, authenticated); server.on('error', (err: NodeJS.ErrnoException) => { console.error( @@ -261,9 +264,7 @@ async function main(): Promise { const actualPort = addr && typeof addr === 'object' ? addr.port : port; console.log('lousho server: listening on http://' + host + ':' + actualPort); ${variant.report} - if (!authenticated && !LOOPBACK.includes(host)) { - console.warn('[lousho server] warning: no LOUSHO_API_TOKEN set, so anyone who can reach ' + host + ' can use this agent.'); - } + if (exposureWarning) console.warn(exposureWarning); }); // Stop accepting, stop the schedules, let in-flight turns finish (bounded), then close the agent. diff --git a/src/deploy/nodeServer.ts b/src/deploy/nodeServer.ts index eb400940..0729bf58 100644 --- a/src/deploy/nodeServer.ts +++ b/src/deploy/nodeServer.ts @@ -41,6 +41,35 @@ const API_TOKEN_ENV = 'LOUSHO_API_TOKEN'; const STORE_ENV = 'LOUSHO_STORE'; /** Environment variable that turns off crash recovery at start-up when `0` or `false`. */ const RECOVER_ENV = 'LOUSHO_RECOVER_ON_START'; +/** Environment variable that, when `1` or `true`, lets the server listen on a public interface with no auth. */ +const ALLOW_PUBLIC_ENV = 'LOUSHO_ALLOW_PUBLIC'; + +/** Whether `host` only takes connections from this machine: `localhost`, `::1` or an address in 127.0.0.0/8. */ +function isLoopbackHost(host: string): boolean { + const name = host.trim().toLowerCase().replace(/^\[(.*)\]$/, '$1'); + return name === 'localhost' || name === '::1' || /^127(\.\d{1,3}){3}$/.test(name); +} + +/** + * The built server's start-up check, before it listens on `host`. With no + * route auth (`authenticated` false: no `LOUSHO_API_TOKEN`, no build token, + * no `auth.ts`) on a non-loopback host, it throws `LOUSHO_SERVER_UNPROTECTED`, + * unless `LOUSHO_ALLOW_PUBLIC=1` in `env` serves the open agent on purpose: + * then it returns the warning to log. `undefined` when there is nothing to say. + */ +export function checkServerExposure(host: string, authenticated: boolean, env: NodeJS.ProcessEnv = process.env): string | undefined { + if (authenticated || isLoopbackHost(host)) return undefined; + const allowPublic = env[ALLOW_PUBLIC_ENV]?.trim().toLowerCase(); + if (allowPublic === '1' || allowPublic === 'true') { + return `[lousho server] warning: no ${API_TOKEN_ENV} set, so anyone who can reach ${host} can use this agent.`; + } + throw new SDKError( + `refusing to listen on ${host} with no auth: anyone who can reach it could use this agent. ` + + `Set ${API_TOKEN_ENV}, add an auth.ts to the agent directory, bind to 127.0.0.1 (HOST / --host), ` + + `or set ${ALLOW_PUBLIC_ENV}=1 to serve an open agent on purpose.`, + 'LOUSHO_SERVER_UNPROTECTED' + ); +} /** * `authorizeSession`, `authorizeApproval` and `exposeErrors` work as on diff --git a/src/deploy/nodeServerPublic.test.ts b/src/deploy/nodeServerPublic.test.ts new file mode 100644 index 00000000..2083ad89 --- /dev/null +++ b/src/deploy/nodeServerPublic.test.ts @@ -0,0 +1,58 @@ +/** The built server's start-up check: no open agent on a public interface unless LOUSHO_ALLOW_PUBLIC=1. */ +import { describe, expect, it } from 'vitest'; +import { createAgent } from '../createAgent'; +import { mockModel } from '../testing'; +import { SDKError } from '../utils/sdkError'; +import { checkServerExposure, createDeployedServer, type DeployedServerOptions } from './nodeServer'; + +/** `authenticated` of a deployed server built with `options`. */ +function authenticatedWith(options: DeployedServerOptions): boolean { + const agent = createAgent({ provider: mockModel(['ok']), instructions: 'x' }); + return createDeployedServer(agent, options).authenticated; +} + +function refusal(run: () => unknown): SDKError { + try { + run(); + } catch (error) { + if (error instanceof SDKError) return error; + throw error; + } + throw new Error('expected checkServerExposure() to throw'); +} + +describe('checkServerExposure()', () => { + it('refuses 0.0.0.0 with no auth (LOUSHO_SERVER_UNPROTECTED), naming every way out', () => { + const error = refusal(() => checkServerExposure('0.0.0.0', authenticatedWith({ env: {} }), {})); + expect(error.code).toBe('LOUSHO_SERVER_UNPROTECTED'); + for (const fix of ['LOUSHO_API_TOKEN', 'auth.ts', '127.0.0.1', 'LOUSHO_ALLOW_PUBLIC=1']) expect(error.message).toContain(fix); + }); + + it.each(['::', '192.168.1.20', 'example.com', '128.0.0.1'])('refuses %s with no auth too', (host) => { + expect(refusal(() => checkServerExposure(host, false, {})).code).toBe('LOUSHO_SERVER_UNPROTECTED'); + }); + + it('starts on 0.0.0.0 when LOUSHO_API_TOKEN is set', () => { + const env = { LOUSHO_API_TOKEN: 's3cret' }; + expect(checkServerExposure('0.0.0.0', authenticatedWith({ env }), env)).toBeUndefined(); + }); + + it('starts on 0.0.0.0 with an auth list (an agent directory auth.ts) and no token', () => { + const allowAll = async () => ({ id: 'anyone', type: 'user' as const, authenticator: 'custom' }); + expect(checkServerExposure('0.0.0.0', authenticatedWith({ env: {}, auth: [allowAll] }), {})).toBeUndefined(); + }); + + it.each(['1', 'true'])('starts on 0.0.0.0 with LOUSHO_ALLOW_PUBLIC=%s, returning the open-agent warning', (value) => { + expect(checkServerExposure('0.0.0.0', false, { LOUSHO_ALLOW_PUBLIC: value })).toBe( + '[lousho server] warning: no LOUSHO_API_TOKEN set, so anyone who can reach 0.0.0.0 can use this agent.' + ); + }); + + it('does not take LOUSHO_ALLOW_PUBLIC=0 as an opt-out', () => { + expect(refusal(() => checkServerExposure('0.0.0.0', false, { LOUSHO_ALLOW_PUBLIC: '0' })).code).toBe('LOUSHO_SERVER_UNPROTECTED'); + }); + + it.each(['127.0.0.1', '127.1.2.3', 'localhost', 'LOCALHOST', '::1', '[::1]'])('starts on loopback %s with no auth and no warning', (host) => { + expect(checkServerExposure(host, authenticatedWith({ env: {} }), {})).toBeUndefined(); + }); +}); diff --git a/src/deploy/runtime.ts b/src/deploy/runtime.ts index 09b93c73..4ddf5adb 100644 --- a/src/deploy/runtime.ts +++ b/src/deploy/runtime.ts @@ -12,7 +12,7 @@ */ export { agentSpecSchema } from '../spec/schema'; export type { AgentSpec } from '../spec/schema'; -export { createDeployedAgent, createDeployedServer, storeFromEnv } from './nodeServer'; +export { checkServerExposure, createDeployedAgent, createDeployedServer, storeFromEnv } from './nodeServer'; export { specSchedules } from '../schedules/specSchedules'; export { createAgent } from '../createAgent'; export { resolveAgentDir } from '../agentDir'; diff --git a/src/utils/errorCodes.ts b/src/utils/errorCodes.ts index c2acd508..3b8b44c8 100644 --- a/src/utils/errorCodes.ts +++ b/src/utils/errorCodes.ts @@ -61,6 +61,9 @@ export const ERROR_CODES = { LOUSHO_STORAGE_BUSY: 'Another process holds the SQLite write lock; retry the call, or find what keeps the lock (a long transaction, a backup, a second replica).', LOUSHO_TRIGGER_INVALID: 'Fix the trigger option the message names; the message shows a working example.', LOUSHO_DEPLOY_FAILED: 'Read the message: it names the missing option, file or unsupported feature; docs/deployment.md covers each target.', + /** The built server was asked to listen on a non-loopback host with no route auth. */ + LOUSHO_SERVER_UNPROTECTED: + 'Set LOUSHO_API_TOKEN, add an auth.ts to the agent directory, bind to 127.0.0.1, or set LOUSHO_ALLOW_PUBLIC=1 to serve an open agent on purpose.', LOUSHO_EVALS_INVALID: 'Call the eval helper the way the message says (inside a vitest file, after t.send(), with a judge configured).', LOUSHO_TEST_FAILED: 'Read the message: an eval or a mockModel() script did not hold; fix the agent, or the expectation.', LOUSHO_CASSETTE_INVALID: 'Record the cassette again (lousho eval --record, or recordReplay mode: record), or fix the file the message names.', From 605d72f886279dffaac68709b3939336f96167e2 Mon Sep 17 00:00:00 2001 From: Ali Mohammad Date: Tue, 20 Oct 2026 13:28:47 +0300 Subject: [PATCH 2/2] chore(api): refresh API reports for LOUSHO_SERVER_UNPROTECTED Co-Authored-By: Claude Opus 5.5 --- api/index.api.md | 1 + api/mcp.api.md | 1 + api/testing.api.md | 1 + api/tools.api.md | 1 + api/worker.api.md | 1 + 5 files changed, 5 insertions(+) diff --git a/api/index.api.md b/api/index.api.md index 729a18cc..906b1da2 100644 --- a/api/index.api.md +++ b/api/index.api.md @@ -1934,6 +1934,7 @@ export const ERROR_CODES: { readonly LOUSHO_STORAGE_BUSY: "Another process holds the SQLite write lock; retry the call, or find what keeps the lock (a long transaction, a backup, a second replica)."; readonly LOUSHO_TRIGGER_INVALID: "Fix the trigger option the message names; the message shows a working example."; readonly LOUSHO_DEPLOY_FAILED: "Read the message: it names the missing option, file or unsupported feature; docs/deployment.md covers each target."; + readonly LOUSHO_SERVER_UNPROTECTED: "Set LOUSHO_API_TOKEN, add an auth.ts to the agent directory, bind to 127.0.0.1, or set LOUSHO_ALLOW_PUBLIC=1 to serve an open agent on purpose."; readonly LOUSHO_EVALS_INVALID: "Call the eval helper the way the message says (inside a vitest file, after t.send(), with a judge configured)."; readonly LOUSHO_TEST_FAILED: "Read the message: an eval or a mockModel() script did not hold; fix the agent, or the expectation."; readonly LOUSHO_CASSETTE_INVALID: "Record the cassette again (lousho eval --record, or recordReplay mode: record), or fix the file the message names."; diff --git a/api/mcp.api.md b/api/mcp.api.md index 6bd1ef7b..7a29f6fb 100644 --- a/api/mcp.api.md +++ b/api/mcp.api.md @@ -573,6 +573,7 @@ const ERROR_CODES: { readonly LOUSHO_STORAGE_BUSY: "Another process holds the SQLite write lock; retry the call, or find what keeps the lock (a long transaction, a backup, a second replica)."; readonly LOUSHO_TRIGGER_INVALID: "Fix the trigger option the message names; the message shows a working example."; readonly LOUSHO_DEPLOY_FAILED: "Read the message: it names the missing option, file or unsupported feature; docs/deployment.md covers each target."; + readonly LOUSHO_SERVER_UNPROTECTED: "Set LOUSHO_API_TOKEN, add an auth.ts to the agent directory, bind to 127.0.0.1, or set LOUSHO_ALLOW_PUBLIC=1 to serve an open agent on purpose."; readonly LOUSHO_EVALS_INVALID: "Call the eval helper the way the message says (inside a vitest file, after t.send(), with a judge configured)."; readonly LOUSHO_TEST_FAILED: "Read the message: an eval or a mockModel() script did not hold; fix the agent, or the expectation."; readonly LOUSHO_CASSETTE_INVALID: "Record the cassette again (lousho eval --record, or recordReplay mode: record), or fix the file the message names."; diff --git a/api/testing.api.md b/api/testing.api.md index a21272ad..cbd95e2b 100644 --- a/api/testing.api.md +++ b/api/testing.api.md @@ -95,6 +95,7 @@ const ERROR_CODES: { readonly LOUSHO_STORAGE_BUSY: "Another process holds the SQLite write lock; retry the call, or find what keeps the lock (a long transaction, a backup, a second replica)."; readonly LOUSHO_TRIGGER_INVALID: "Fix the trigger option the message names; the message shows a working example."; readonly LOUSHO_DEPLOY_FAILED: "Read the message: it names the missing option, file or unsupported feature; docs/deployment.md covers each target."; + readonly LOUSHO_SERVER_UNPROTECTED: "Set LOUSHO_API_TOKEN, add an auth.ts to the agent directory, bind to 127.0.0.1, or set LOUSHO_ALLOW_PUBLIC=1 to serve an open agent on purpose."; readonly LOUSHO_EVALS_INVALID: "Call the eval helper the way the message says (inside a vitest file, after t.send(), with a judge configured)."; readonly LOUSHO_TEST_FAILED: "Read the message: an eval or a mockModel() script did not hold; fix the agent, or the expectation."; readonly LOUSHO_CASSETTE_INVALID: "Record the cassette again (lousho eval --record, or recordReplay mode: record), or fix the file the message names."; diff --git a/api/tools.api.md b/api/tools.api.md index 7b8045c8..f97a8345 100644 --- a/api/tools.api.md +++ b/api/tools.api.md @@ -668,6 +668,7 @@ const ERROR_CODES: { readonly LOUSHO_STORAGE_BUSY: "Another process holds the SQLite write lock; retry the call, or find what keeps the lock (a long transaction, a backup, a second replica)."; readonly LOUSHO_TRIGGER_INVALID: "Fix the trigger option the message names; the message shows a working example."; readonly LOUSHO_DEPLOY_FAILED: "Read the message: it names the missing option, file or unsupported feature; docs/deployment.md covers each target."; + readonly LOUSHO_SERVER_UNPROTECTED: "Set LOUSHO_API_TOKEN, add an auth.ts to the agent directory, bind to 127.0.0.1, or set LOUSHO_ALLOW_PUBLIC=1 to serve an open agent on purpose."; readonly LOUSHO_EVALS_INVALID: "Call the eval helper the way the message says (inside a vitest file, after t.send(), with a judge configured)."; readonly LOUSHO_TEST_FAILED: "Read the message: an eval or a mockModel() script did not hold; fix the agent, or the expectation."; readonly LOUSHO_CASSETTE_INVALID: "Record the cassette again (lousho eval --record, or recordReplay mode: record), or fix the file the message names."; diff --git a/api/worker.api.md b/api/worker.api.md index f696b3ff..b9db1bb8 100644 --- a/api/worker.api.md +++ b/api/worker.api.md @@ -1309,6 +1309,7 @@ const ERROR_CODES: { readonly LOUSHO_STORAGE_BUSY: "Another process holds the SQLite write lock; retry the call, or find what keeps the lock (a long transaction, a backup, a second replica)."; readonly LOUSHO_TRIGGER_INVALID: "Fix the trigger option the message names; the message shows a working example."; readonly LOUSHO_DEPLOY_FAILED: "Read the message: it names the missing option, file or unsupported feature; docs/deployment.md covers each target."; + readonly LOUSHO_SERVER_UNPROTECTED: "Set LOUSHO_API_TOKEN, add an auth.ts to the agent directory, bind to 127.0.0.1, or set LOUSHO_ALLOW_PUBLIC=1 to serve an open agent on purpose."; readonly LOUSHO_EVALS_INVALID: "Call the eval helper the way the message says (inside a vitest file, after t.send(), with a judge configured)."; readonly LOUSHO_TEST_FAILED: "Read the message: an eval or a mockModel() script did not hold; fix the agent, or the expectation."; readonly LOUSHO_CASSETTE_INVALID: "Record the cassette again (lousho eval --record, or recordReplay mode: record), or fix the file the message names.";