@zenera/server 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +204 -231
- package/dist/auth.d.ts +2 -0
- package/dist/config.d.ts +15 -1
- package/dist/config.js +4 -0
- package/dist/http.js +5 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/plugins.d.ts +19 -0
- package/dist/plugins.js +47 -0
- package/dist/runs.d.ts +8 -0
- package/dist/runs.js +17 -0
- package/dist/server.d.ts +5 -2
- package/dist/server.js +21 -9
- package/dist/status.d.ts +7 -0
- package/dist/status.js +7 -0
- package/dist/store.d.ts +4 -0
- package/dist/store.js +4 -1
- package/dist/userState.d.ts +99 -0
- package/dist/userState.js +461 -0
- package/package.json +4 -4
- package/dist/state.d.ts +0 -24
- package/dist/state.js +0 -55
- package/dist/token.d.ts +0 -14
- package/dist/token.js +0 -30
package/README.md
CHANGED
|
@@ -1,246 +1,219 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img
|
|
3
|
+
src="https://raw.githubusercontent.com/zenera-ai/ZeneraNeo/main/docs/imgs/zenera-logo-with-media-queries.svg"
|
|
4
|
+
alt="Zenera"
|
|
5
|
+
width="24"
|
|
6
|
+
/>
|
|
7
|
+
<div>Powered By Zenera</div>
|
|
8
|
+
<br>
|
|
9
|
+
<img
|
|
10
|
+
src="https://raw.githubusercontent.com/zenera-ai/ZeneraNeo/main/docs/imgs/banner-server-with-media-queries.svg"
|
|
11
|
+
alt="ZENERA SERVER"
|
|
12
|
+
width="918"
|
|
13
|
+
/>
|
|
2
14
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
[
|
|
6
|
-
|
|
7
|
-
|
|
15
|
+
[](https://www.npmjs.com/package/@zenera/server)
|
|
16
|
+
[](https://github.com/zenera-ai/ZeneraNeo/blob/main/LICENSE)
|
|
17
|
+
[](https://nodejs.org)
|
|
18
|
+
|
|
19
|
+
[Quickstart](#quickstart) · [Secure setup](#secure-setup) · [Commands](#commands) · [Options](#common-options) · [HTTP API](#http-api)
|
|
20
|
+
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
# `@zenera/server`
|
|
24
|
+
|
|
25
|
+
Serve a ZeneraNeo project's chats over HTTP. Each user has private sessions and
|
|
26
|
+
memory; agent commands run in Podman sandboxes, or under bwrap in an agent container.
|
|
27
|
+
Requires Node.js 24+ and the [Zenera CLI](../cli/README.md).
|
|
28
|
+
|
|
29
|
+
## Quickstart
|
|
30
|
+
|
|
31
|
+
For local testing, run the server in the configured project's agent container:
|
|
8
32
|
|
|
9
33
|
```sh
|
|
10
|
-
zen server start --insecure
|
|
11
|
-
zen server token --user ann # a token such a server takes
|
|
12
|
-
zen server start --jwt-public-key app.pub.pem # behind zenera-app
|
|
13
|
-
zen server start --container --insecure # in the project's agent image, under bwrap
|
|
14
|
-
zen server check # could it start here?
|
|
15
|
-
zen server status # a running one's report
|
|
34
|
+
zen server start --insecure --container --user andrey
|
|
16
35
|
```
|
|
17
36
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
Startup uses an aligned, colored summary for the project, HTTP address, auth,
|
|
32
|
-
sandbox, state directory, image and browser user. Container builds show one
|
|
33
|
-
updating, width-limited line with a spinner and elapsed build time by default,
|
|
34
|
-
even during quiet steps; piped logs retain only the final
|
|
35
|
-
progress line. Warnings and errors remain visible in both cases.
|
|
36
|
-
Use `zen server start --container --verbose` for every build line, grouped by
|
|
37
|
-
stage and colored by severity. This applies to both the sandbox base image and
|
|
38
|
-
the agent image. Colors honor `NO_COLOR` and normal terminal detection.
|
|
39
|
-
The image reports listener readiness without repeating the host's summary or
|
|
40
|
-
insecure warning; container startup and HTTP readiness are distinct messages.
|
|
41
|
-
|
|
42
|
-
The HTML contains its JavaScript, React components and CSS; it needs no frontend
|
|
43
|
-
server or CDN. It reuses ZeneraLite's chat messages, composer, tool cards and
|
|
44
|
-
AG-UI reducer. It lists sessions, loads history, streams turns, follows active
|
|
45
|
-
runs, cancels runs and deletes sessions. Connected-account actions still need
|
|
46
|
-
zenera-app. Reopen the printed link after a page reload, or paste a dev JWT into
|
|
47
|
-
the connection form: tokens are deliberately not saved in browser storage.
|
|
48
|
-
|
|
49
|
-
Local files send `Origin: null`. Only insecure servers allow that origin and
|
|
50
|
-
its GET/POST/DELETE preflights with Authorization and JSON headers; actual API
|
|
51
|
-
requests still require a token. Secure servers do not enable this CORS access.
|
|
52
|
-
A browser may ask you to allow local-network access.
|
|
53
|
-
|
|
54
|
-
```ts
|
|
55
|
-
import { createServer } from '@zenera/server';
|
|
56
|
-
|
|
57
|
-
const server = createServer({
|
|
58
|
-
projectId: 'p1',
|
|
59
|
-
projectDir: '/project',
|
|
60
|
-
root: '/state',
|
|
61
|
-
auth: {
|
|
62
|
-
mode: 'jwt',
|
|
63
|
-
publicKey: process.env.ZEN_SERVER_JWT_PUBLIC_KEY!,
|
|
64
|
-
issuer: 'zenera-app',
|
|
65
|
-
audience: 'zenera-runtime',
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
await server.listen({ port: 3000, host: '0.0.0.0' });
|
|
37
|
+
Open the printed HTTP `/chat` link to list sessions, send messages, follow active
|
|
38
|
+
runs, cancel them, or delete a session. `--user` selects the development JWT's
|
|
39
|
+
subject, not a server-wide user. The default is `local`.
|
|
40
|
+
|
|
41
|
+
**Insecure mode is for local testing only:** anyone who can reach the server can
|
|
42
|
+
impersonate any user. API requests still need a JWT, but signatures are not checked.
|
|
43
|
+
The test page is available only in this mode. Its token stays in memory; after a
|
|
44
|
+
reload, reopen the printed link or enter a development token again.
|
|
45
|
+
|
|
46
|
+
To show full image build logs:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
zen server start --insecure --container --user andrey --verbose
|
|
69
50
|
```
|
|
70
51
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
so neither it nor a sandboxed command that reaches its port can make one.
|
|
77
|
-
|
|
78
|
-
| Claim | Checked |
|
|
79
|
-
| ----------------------- | --------------------------------------------------------------------------- |
|
|
80
|
-
| alg | `EdDSA` only |
|
|
81
|
-
| `iss` | `zenera-app` (`--jwt-issuer`) |
|
|
82
|
-
| `aud` | `zenera-runtime` (`--jwt-audience`) |
|
|
83
|
-
| `iat` | at most 2 minutes old, 30 s of clock skew |
|
|
84
|
-
| `prj` | this server's project id |
|
|
85
|
-
| `req` | `"<METHOD> <url>"` of this very request: a token seen in transit is no good |
|
|
86
|
-
| `sub` | the user, required |
|
|
87
|
-
| `thr` | when there, must be the thread in the URL |
|
|
88
|
-
| `role`, `email`, `name` | when there, handed to the run as the user's (`zenera` library) |
|
|
89
|
-
|
|
90
|
-
Logging people in and deciding who may use which project stay in front of the
|
|
91
|
-
server (zenera-app); the server only trusts what it signed.
|
|
92
|
-
|
|
93
|
-
**`--insecure`** is for trying a server on your machine: tokens are read, not
|
|
94
|
-
checked (any signature or none, any age, any project or request), but `sub` is
|
|
95
|
-
still required — `zen server token --user ann` prints one. It listens on
|
|
96
|
-
loopback only, and is refused inside the agent image unless whoever runs the
|
|
97
|
-
container says so (`ZEN_SERVER_INSECURE_LOCAL=1`, which `--container` sets while
|
|
98
|
-
publishing the port on loopback only).
|
|
99
|
-
|
|
100
|
-
## Root: every user in a folder of their own
|
|
52
|
+
Podman builds and caches the image. Default build output shows a spinner, elapsed
|
|
53
|
+
time, and the latest line; warnings remain visible. Use `--rebuild` to rebuild.
|
|
54
|
+
Press Ctrl+C to stop gracefully and save active runs.
|
|
55
|
+
|
|
56
|
+
To run the server directly on the host instead, omit `--container`:
|
|
101
57
|
|
|
58
|
+
```sh
|
|
59
|
+
zen server start --insecure --user andrey
|
|
102
60
|
```
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
61
|
+
|
|
62
|
+
## Secure setup
|
|
63
|
+
|
|
64
|
+
A trusted caller, usually Zenera-app, authenticates users, checks project access,
|
|
65
|
+
and signs a short-lived JWT for each runtime request. The runtime holds **only
|
|
66
|
+
the public key**.
|
|
67
|
+
|
|
68
|
+
For a standalone caller, generate an Ed25519 pair outside the project directory:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
zen server keygen --out "$HOME/runtime-auth"
|
|
72
|
+
zen server start --jwt-public-key "$HOME/runtime-auth.public.pem"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`keygen` creates `<prefix>.private.pem` (PKCS#8, owner-only `0600` permissions)
|
|
76
|
+
and `<prefix>.public.pem` (SPKI). The parent directory must already exist.
|
|
77
|
+
Existing files are never overwritten; a failed write removes newly created files.
|
|
78
|
+
Keep the private key with the trusted caller, out of source control and runtime
|
|
79
|
+
containers. Zenera-app already manages its own signing identity; it does not need
|
|
80
|
+
this command.
|
|
81
|
+
|
|
82
|
+
Generating keys does **not** sign requests. Secure callers send an
|
|
83
|
+
`Authorization: Bearer <jwt>` header containing an EdDSA-signed token with:
|
|
84
|
+
|
|
85
|
+
- `iss` / `aud`: defaults `zenera-app` / `zenera-runtime`.
|
|
86
|
+
- `sub`: the authenticated user; `prj`: this server's project ID.
|
|
87
|
+
- `req`: the exact `"<METHOD> <path-and-query>"` of the request.
|
|
88
|
+
- `iat` / `exp`: issue time and expiry; maximum age 2 minutes, 30 seconds clock tolerance.
|
|
89
|
+
|
|
90
|
+
Use HTTPS at the ingress or a protected internal connection. A token cannot
|
|
91
|
+
authorize a different endpoint, but can be replayed for the same request until
|
|
92
|
+
it expires. The server does not perform user login or project-membership checks.
|
|
93
|
+
|
|
94
|
+
## Commands
|
|
95
|
+
|
|
96
|
+
| Command | Use |
|
|
97
|
+
| ---------------------------------- | --------------------------------------------------------------- |
|
|
98
|
+
| `zen server start` | Start a project's runtime. |
|
|
99
|
+
| `zen server check` | Check whether it can start in the current environment. |
|
|
100
|
+
| `zen server status` | Inspect a running runtime; use `--token <jwt>` for secure mode. |
|
|
101
|
+
| `zen server keygen --out <prefix>` | Generate private/public signing keys. |
|
|
102
|
+
| `zen server token --user <id>` | Print an **unsigned** development JWT for insecure mode. |
|
|
103
|
+
| `zen server build` | Build (and push) a release image; see below. |
|
|
104
|
+
|
|
105
|
+
Test an API without the browser:
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
TOKEN=$(zen server token --user ann)
|
|
109
|
+
curl http://127.0.0.1:3000/api/sessions \
|
|
110
|
+
-H "Authorization: Bearer $TOKEN"
|
|
109
111
|
```
|
|
110
112
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
| `/api/sessions/:tid/{runs,…/trace,memory}` debug | With `debug: true` only: recorded runs, traces, memory graph |
|
|
146
|
-
|
|
147
|
-
Every session route is the caller's own: `:tid` is looked up under their folder.
|
|
148
|
-
The list returns `{ sessions: [{ id, title, createdAt, updatedAt, running }] }`;
|
|
149
|
-
`running` is `{ runId }` or `null`, and titles come from the engine's session
|
|
150
|
-
metadata (falling back to the ID). A thread-scoped token lists only its thread.
|
|
151
|
-
Listing does not create sessions or memory. A new session is still created on
|
|
152
|
-
its first turn.
|
|
153
|
-
|
|
154
|
-
A run belongs to the server, not the request: it goes on when the caller leaves
|
|
155
|
-
and can be followed again. At most `maxRuns` (`ZEN_RUNTIME_MAX_RUNS`, 4) run at
|
|
156
|
-
once, `maxUserRuns` (`ZEN_RUNTIME_MAX_USER_RUNS`, 2) per user. An
|
|
157
|
-
`x-zenera-mcp-token` header on a run is handed to the run's MCP plugin
|
|
158
|
-
(`ZEN_PLUGINS`, `ZEN_MCP_BROKER_URL`).
|
|
159
|
-
|
|
160
|
-
## Sandbox
|
|
161
|
-
|
|
162
|
-
Chosen once, at start:
|
|
163
|
-
|
|
164
|
-
- **podman** — anywhere but the agent image, as `zen run`: a container per
|
|
165
|
-
session on this machine. `zen server start` readies podman as `zen run` does,
|
|
166
|
-
and a turn pulls or builds the image it needs.
|
|
167
|
-
- **bwrap** — inside the agent image, which ships `/opt/zenera/bin/bwrap` and is
|
|
168
|
-
itself the container: each command under bubblewrap. Commands see the image
|
|
169
|
-
(what the project's `sandbox/Dockerfile` installed, with its `ENV`), their own
|
|
170
|
-
workspace and home, and the `zenera` library; never the root (other chats,
|
|
171
|
-
other users, memories), the seed, the zen home, the project (its `.env`), the
|
|
172
|
-
server's install, `/state` or `/run/secrets`.
|
|
173
|
-
|
|
174
|
-
`--sandbox` or `$ZEN_SERVER_SANDBOX` overrides the choice; `createServer` takes
|
|
175
|
-
`sandbox: 'podman' | 'bwrap' | factory` and an optional `preflight`. A sandbox
|
|
176
|
-
plugin in `ZEN_PLUGINS` never replaces it.
|
|
177
|
-
|
|
178
|
-
The `zenera` library (Python and Node: `identity()`, `require_identity()`) is one
|
|
179
|
-
shared read-only directory mounted at `/opt/zenera/runtime` for every session —
|
|
180
|
-
built into the agent image, or written once per content under the zen home.
|
|
181
|
-
|
|
182
|
-
`zen server start` refuses to listen when commands have nowhere to run (podman
|
|
183
|
-
not answering, bwrap unable to isolate), when the image was built for another
|
|
184
|
-
project environment (`ZEN_IMAGE_BUILD` / `ZEN_IMAGE_BASE`), or when the root is
|
|
185
|
-
another project's.
|
|
186
|
-
|
|
187
|
-
## `--container`
|
|
188
|
-
|
|
189
|
-
`zen server start --container` runs the server in the project's agent image on
|
|
190
|
-
this machine, with podman:
|
|
191
|
-
|
|
192
|
-
1. The project's sandbox image is readied as for `zen run` (`sandbox/Dockerfile`).
|
|
193
|
-
2. The agent image is built on it: this zen's packages (`npm pack` of the ones
|
|
194
|
-
installed), Node in `/opt/zenera`, a static bwrap, the `zenera` library, the
|
|
195
|
-
sandbox image's `ENV` (so commands get it under bwrap). Tagged
|
|
196
|
-
`localhost/zenera-agent:<hash>`; built again only when something changed, or
|
|
197
|
-
with `--rebuild`.
|
|
198
|
-
3. `podman run`: the project read-only at `/project`, the root at `/state`, the
|
|
199
|
-
port published on `--host` (loopback by default), the auth flags and model
|
|
200
|
-
keys and provider settings passed through by environment name, credential files
|
|
201
|
-
mounted as private Podman secrets, logs followed;
|
|
202
|
-
Ctrl-C stops it.
|
|
203
|
-
|
|
204
|
-
The server runs as numeric UID/GID **10001**, with `HOME=/home/zenera`, not
|
|
205
|
-
as root or as the sandbox base image's `USER`. Commands inherit that UID unless
|
|
206
|
-
the project's sandbox config explicitly selects another user. Rootless podman
|
|
207
|
-
uses `--userns keep-id:uid=10001,gid=10001`: existing host-owned state stays
|
|
208
|
-
writable, without changing its ownership. With rootful podman, the host must
|
|
209
|
-
provide a state directory writable by UID/GID 10001; ownership is not changed
|
|
210
|
-
automatically. Project and seed mounts must also be readable by that user.
|
|
211
|
-
The container drops all capabilities and disables privilege escalation;
|
|
212
|
-
bwrap uses unprivileged user namespaces.
|
|
213
|
-
|
|
214
|
-
For Kubernetes, set `runAsNonRoot: true`, `runAsUser: 10001`,
|
|
215
|
-
`runAsGroup: 10001`, `fsGroup: 10001` for the state PVC, and
|
|
216
|
-
`allowPrivilegeEscalation: false` with all capabilities dropped. The node's
|
|
217
|
-
security policy must allow bwrap's unprivileged user namespaces; startup
|
|
218
|
-
reports a failed probe rather than running commands without isolation.
|
|
219
|
-
|
|
220
|
-
Model keys come from the keyring (`zen key …`) and the environment, as for any
|
|
221
|
-
run, including the project's `.env`; in the agent image, the environment.
|
|
222
|
-
Vertex project, location and quota project, and the Bifrost endpoint, are forwarded
|
|
223
|
-
alongside keys. Explicit `GOOGLE_APPLICATION_CREDENTIALS` takes precedence over
|
|
224
|
-
ambient gcloud application-default credentials.
|
|
225
|
-
|
|
226
|
-
File credentials use ephemeral Podman secrets readable only by UID 10001. The
|
|
227
|
-
server stages them in its private container home before opening engines, allowing
|
|
228
|
-
bwrap to bind container-owned files when the project's keys policy permits it.
|
|
229
|
-
They are not embedded in the image, argv, workspaces or persistent user state.
|
|
230
|
-
Secrets are removed after confirmed shutdown or failed startup; a failed stop
|
|
231
|
-
retains them and the memory seed until the container can be stopped. Failed secret
|
|
232
|
-
cleanup is reported with a manual removal command.
|
|
233
|
-
|
|
234
|
-
## Rebuilding the standalone page
|
|
235
|
-
|
|
236
|
-
The source and build live in ZeneraLite's `services/zenera-app`. Run there:
|
|
113
|
+
`token` also accepts `--email`, `--role`, `--name`, and `--thread`. It neither
|
|
114
|
+
creates an account nor signs a secure token.
|
|
115
|
+
|
|
116
|
+
## Common options
|
|
117
|
+
|
|
118
|
+
| Option | Use / default |
|
|
119
|
+
| --------------------------------- | ---------------------------------------------------------------------- |
|
|
120
|
+
| `--project <name-or-dir>` | Select a project; otherwise use the current project. |
|
|
121
|
+
| `--port <n>` / `--host <addr>` | `3000` / `127.0.0.1`. Insecure mode is loopback-only. |
|
|
122
|
+
| `--root <dir>` | Persistent user state; `~/.zenera/neo/servers/<project>` on the host. |
|
|
123
|
+
| `--memory-seed <dir>` | Initial memory for new users; defaults to project memory. |
|
|
124
|
+
| `--no-memory-seed` | Start new users with empty memory. |
|
|
125
|
+
| `--jwt-public-key <file>` | Public PEM for secure mode; alternatively `ZEN_SERVER_JWT_PUBLIC_KEY`. |
|
|
126
|
+
| `--jwt-issuer` / `--jwt-audience` | Override the expected signer / audience. |
|
|
127
|
+
| `--container` | Run the server in the project image; commands use bwrap. |
|
|
128
|
+
| `--verbose` / `--rebuild` | Full container build logs / rebuild images. |
|
|
129
|
+
| `--debug` | Enable run, trace, and memory inspection routes. |
|
|
130
|
+
|
|
131
|
+
Run `zen server --help` for all options. Sessions and memory live under
|
|
132
|
+
`<root>/users/<user>/`; memory is private per user and shared across their sessions.
|
|
133
|
+
New users receive the memory seed captured at startup. Existing user memory is
|
|
134
|
+
never replaced by a restart.
|
|
135
|
+
|
|
136
|
+
Model credentials come from `zen key`, the environment, and the project's `.env`.
|
|
137
|
+
Container mode forwards provider settings, including Vertex and Bifrost, and
|
|
138
|
+
credential files. The gateway must be reachable from the container; host
|
|
139
|
+
`localhost` is not container `localhost`.
|
|
140
|
+
Set `sandbox.keys: false` to withhold credentials from agent-written commands,
|
|
141
|
+
while allowing the engine to use them for model calls.
|
|
142
|
+
|
|
143
|
+
## Release images
|
|
144
|
+
|
|
145
|
+
`zen server build` bakes a project into an immutable image for Kubernetes and
|
|
146
|
+
prints its digest (or the local tag when not pushed):
|
|
237
147
|
|
|
238
148
|
```sh
|
|
239
|
-
|
|
149
|
+
zen server build --project p1 --release r42 \
|
|
150
|
+
--push registry.local:5000/agent-p1:r42 [--authfile auth.json] [--insecure-registry]
|
|
240
151
|
```
|
|
241
152
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
`
|
|
245
|
-
|
|
246
|
-
|
|
153
|
+
The image is the agent image plus the project at `/project`, its memory seed at
|
|
154
|
+
`/seed` (`--memory-seed <dir>` / `--no-memory-seed`), `ZEN_RELEASE` and
|
|
155
|
+
`ZEN_PROJECT_ID`. The release id defaults to `r-<content hash>`; `--json` prints
|
|
156
|
+
`{release, image, agentImage, base, built, ref, digest}`.
|
|
157
|
+
|
|
158
|
+
Never baked in: `.env*` (except `.env.example`), `sessions/`, `.git`,
|
|
159
|
+
`node_modules`, `.tmp/`, or the live memory directory. Deployment settings (JWT public key, MCP
|
|
160
|
+
plugin, Bifrost URL/key, storage) are passed at deploy time via environment
|
|
161
|
+
variables or a ConfigMap. Use `--add <src>:<dest>` (repeatable) to add large
|
|
162
|
+
content such as plugins, e.g. `--add ./plugins:/opt/zenera/plugins`.
|
|
163
|
+
|
|
164
|
+
## Many servers per project
|
|
165
|
+
|
|
166
|
+
Several servers can serve one project when users' data lives in a store rather
|
|
167
|
+
than on one disk. Name a store module with `ZEN_STATE_STORE` (ZeneraLite ships an
|
|
168
|
+
S3 store as `state.mjs`). The server then:
|
|
169
|
+
|
|
170
|
+
- loads a user's data before their first request, and a chat's files when it is opened;
|
|
171
|
+
- saves them after every run, after deletions, every `ZEN_STATE_CHECKPOINT_SECONDS`
|
|
172
|
+
(300) while runs go on, when released, and on shutdown;
|
|
173
|
+
- refuses a token whose `gen` (lease generation) is older than the one it holds,
|
|
174
|
+
and drops its copy when the store reports that a newer lease saved the user.
|
|
175
|
+
|
|
176
|
+
The host manages users through `/api/admin` with tokens for its own subject
|
|
177
|
+
(`ZEN_SERVER_SYSTEM_SUBJECT`, default `zenera-app`):
|
|
178
|
+
|
|
179
|
+
| Route | Use |
|
|
180
|
+
| ------------------------------------- | --------------------------------------------------------- |
|
|
181
|
+
| `GET /api/admin/users[?usage=1]` | Users here: lease, runs, unsaved changes, disk use. |
|
|
182
|
+
| `POST /api/admin/users/:sub/release` | Save and let go of one user; `409 busy` while runs go on. |
|
|
183
|
+
| `POST /api/admin/drain[?timeout=600]` | No new users; release each one as their runs end. |
|
|
184
|
+
| `POST /api/admin/undrain` | Take new users again. |
|
|
185
|
+
|
|
186
|
+
Soft limits for many users per pod:
|
|
187
|
+
|
|
188
|
+
| Variable | Limit |
|
|
189
|
+
| -------------------------- | ---------------------------------------------------------- |
|
|
190
|
+
| `ZEN_RUNTIME_USER_DISK_MB` | Per-user disk; over it, no new runs and running ones stop. |
|
|
191
|
+
| `ZEN_RUNTIME_NICE` | Lower CPU priority for commands (default 10). |
|
|
192
|
+
| `ZEN_RUNTIME_MAX_FILE_MB` | Largest file a command may write. |
|
|
193
|
+
| `ZEN_RUNTIME_TMP_MB` | Size of each command's `/tmp` (it uses memory). |
|
|
194
|
+
|
|
195
|
+
These are soft. Hard limits come from the pod: CPU, memory, and a fixed-size
|
|
196
|
+
volume for `/state`.
|
|
197
|
+
|
|
198
|
+
## HTTP API
|
|
199
|
+
|
|
200
|
+
Session routes operate only on the JWT subject's own sessions.
|
|
201
|
+
|
|
202
|
+
| Route | Use |
|
|
203
|
+
| ---------------------------------------------- | ---------------------------------------------- |
|
|
204
|
+
| `GET /healthz` | Public health probe. |
|
|
205
|
+
| `GET /chat` | Public test page, insecure mode only. |
|
|
206
|
+
| `GET /api/status` / `GET /api/activity` | Runtime status / active runs. |
|
|
207
|
+
| `GET /api/sessions` / `GET /api/sessions/:tid` | List sessions / load history. |
|
|
208
|
+
| `POST /api/sessions/:tid/runs` | Start a turn, streaming AG-UI events over SSE. |
|
|
209
|
+
| `GET /api/sessions/:tid/runs/:runId/events` | Follow or replay a run. |
|
|
210
|
+
| `POST /api/sessions/:tid/runs/:runId/cancel` | Cancel a run. |
|
|
211
|
+
| `DELETE /api/sessions/:tid` | Delete a session and its files. |
|
|
212
|
+
|
|
213
|
+
Runs continue after a client disconnects and can be followed again. Defaults:
|
|
214
|
+
4 concurrent runs total, 2 per user (`ZEN_RUNTIME_MAX_RUNS`,
|
|
215
|
+
`ZEN_RUNTIME_MAX_USER_RUNS`).
|
|
216
|
+
|
|
217
|
+
For deployment and sandbox details, see [agent-image architecture](../../docs/agent-image.md).
|
|
218
|
+
To rebuild the bundled test page, run `npm run build:chat` from ZeneraLite's root;
|
|
219
|
+
it automatically copies the page into a sibling ZeneraNeo checkout when present.
|
package/dist/auth.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface CallerClaims extends JWTPayload {
|
|
|
8
8
|
role?: string;
|
|
9
9
|
email?: string;
|
|
10
10
|
name?: string;
|
|
11
|
+
/** the host's lease generation for this user on this server (userState.ts) */
|
|
12
|
+
gen?: number;
|
|
11
13
|
}
|
|
12
14
|
export declare function verifyCaller(req: Request): Promise<CallerClaims>;
|
|
13
15
|
export declare function requireCaller(): (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
package/dist/config.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SandboxSetup, ZenPlugin } from '@zenera/engine';
|
|
2
|
-
import { type SandboxFactory } from '@zenera/sandbox';
|
|
2
|
+
import { type CommandLimits, type SandboxFactory } from '@zenera/sandbox';
|
|
3
|
+
import type { UserStateStore } from './userState.ts';
|
|
3
4
|
export type ServerAuth =
|
|
4
5
|
/** EdDSA tokens signed per request by the host (zenera-app), checked with its public key */
|
|
5
6
|
{
|
|
@@ -50,6 +51,19 @@ export interface ServerSettings {
|
|
|
50
51
|
maxUserRuns: number;
|
|
51
52
|
maxRunMinutes: number;
|
|
52
53
|
runLogMaxBytes: number;
|
|
54
|
+
/**
|
|
55
|
+
* Where users' data lives beyond this disk (userState.ts): loaded before their first request,
|
|
56
|
+
* saved at safe points, released on the host's word. Unset: only here (`localStore`).
|
|
57
|
+
*/
|
|
58
|
+
stateStore?: UserStateStore;
|
|
59
|
+
/** how often users with runs going (or a failed save) are saved again; 0: never */
|
|
60
|
+
checkpointSeconds: number;
|
|
61
|
+
/** the `sub` of the host's own calls, the only one /api/admin answers */
|
|
62
|
+
systemSubject: string;
|
|
63
|
+
/** soft limit on one user's folder; over it, no new runs and running ones stop; 0: none */
|
|
64
|
+
userDiskQuotaBytes: number;
|
|
65
|
+
/** what each bwrap command is held to: priority, largest file, size of its /tmp; 0: none */
|
|
66
|
+
commandLimits: CommandLimits;
|
|
53
67
|
}
|
|
54
68
|
/**
|
|
55
69
|
* bwrap inside the agent image, which ships it at /opt/zenera/bin/bwrap; podman anywhere else,
|
package/dist/config.js
CHANGED
|
@@ -30,6 +30,10 @@ export const DEFAULTS = {
|
|
|
30
30
|
maxUserRuns: 2,
|
|
31
31
|
maxRunMinutes: 120,
|
|
32
32
|
runLogMaxBytes: 50 * 1024 * 1024,
|
|
33
|
+
checkpointSeconds: 300,
|
|
34
|
+
systemSubject: DEFAULT_ISSUER,
|
|
35
|
+
userDiskQuotaBytes: 0,
|
|
36
|
+
commandLimits: { nice: 10, fileSizeBytes: 0, tmpBytes: 0 },
|
|
33
37
|
};
|
|
34
38
|
let current;
|
|
35
39
|
export function configure(settings) {
|
package/dist/http.js
CHANGED
|
@@ -21,6 +21,10 @@ export function errorHandler(err, req, res, _next) {
|
|
|
21
21
|
const status = err instanceof HttpError ? err.status : (err?.status ?? 500);
|
|
22
22
|
if (status >= 500)
|
|
23
23
|
console.error(`${req.method} ${req.originalUrl}:`, err);
|
|
24
|
-
|
|
24
|
+
const code = err?.code;
|
|
25
|
+
res.status(status).json({
|
|
26
|
+
error: errorMessage(err),
|
|
27
|
+
...(err instanceof HttpError && typeof code === 'string' ? { code } : {}),
|
|
28
|
+
});
|
|
25
29
|
}
|
|
26
30
|
//# sourceMappingURL=http.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { requireCaller, unsignedToken, verifyCaller, type CallerClaims } from '.
|
|
|
4
4
|
export { identify, runUser, userFolder, type Identity } from './identity.ts';
|
|
5
5
|
export { LAYOUT_VERSION, LayoutError, openLayout, type Layout } from './layout.ts';
|
|
6
6
|
export { privateStore, memoryDir, memorySeed, userDir, userMemory, SEED_FILE, usesMemory, imageReady, openEngineFor, statusFor, errorMessage, asHttpError, type ChatStore, type ChatThread, } from './store.ts';
|
|
7
|
-
export { runsRouter, runDebugRouter, activeRuns, currentRun, stopAllRuns } from './runs.ts';
|
|
7
|
+
export { runsRouter, runDebugRouter, activeRuns, currentRun, ownerRuns, stopAllRuns, stopOwnerRuns, } from './runs.ts';
|
|
8
8
|
export { historyRouter, toMessages } from './history.ts';
|
|
9
9
|
export { traceRouter, openTrace, stepParam } from './trace.ts';
|
|
10
10
|
export { memoryRouter } from './memory.ts';
|
|
@@ -13,6 +13,7 @@ export { RunLog, compact, readRunLog, threadRunLogs } from './runLog.ts';
|
|
|
13
13
|
export * from './runRecords.ts';
|
|
14
14
|
export { applyProjectEnv, withoutProjectEnv } from './projectEnv.ts';
|
|
15
15
|
export { HttpError, errorHandler, notFound } from './http.ts';
|
|
16
|
-
export { envRunPlugins } from './plugins.ts';
|
|
16
|
+
export { envRunPlugins, envServerLimits, envStateStore, type StateStoreOptions, } from './plugins.ts';
|
|
17
|
+
export { localStore, StaleGenerationError, USER_LAYOUT, userStates, userUsageBytes, type CheckpointReason, type UserInfo, type UserRef, type UserSnapshot, type UserStateStore, } from './userState.ts';
|
|
17
18
|
export { serverStatus, startupErrors, type ServerStatus } from './status.ts';
|
|
18
19
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ export { requireCaller, unsignedToken, verifyCaller } from "./auth.js";
|
|
|
6
6
|
export { identify, runUser, userFolder } from "./identity.js";
|
|
7
7
|
export { LAYOUT_VERSION, LayoutError, openLayout } from "./layout.js";
|
|
8
8
|
export { privateStore, memoryDir, memorySeed, userDir, userMemory, SEED_FILE, usesMemory, imageReady, openEngineFor, statusFor, errorMessage, asHttpError, } from "./store.js";
|
|
9
|
-
export { runsRouter, runDebugRouter, activeRuns, currentRun, stopAllRuns } from "./runs.js";
|
|
9
|
+
export { runsRouter, runDebugRouter, activeRuns, currentRun, ownerRuns, stopAllRuns, stopOwnerRuns, } from "./runs.js";
|
|
10
10
|
export { historyRouter, toMessages } from "./history.js";
|
|
11
11
|
export { traceRouter, openTrace, stepParam } from "./trace.js";
|
|
12
12
|
export { memoryRouter } from "./memory.js";
|
|
@@ -15,6 +15,7 @@ export { RunLog, compact, readRunLog, threadRunLogs } from "./runLog.js";
|
|
|
15
15
|
export * from "./runRecords.js";
|
|
16
16
|
export { applyProjectEnv, withoutProjectEnv } from "./projectEnv.js";
|
|
17
17
|
export { HttpError, errorHandler, notFound } from "./http.js";
|
|
18
|
-
export { envRunPlugins } from "./plugins.js";
|
|
18
|
+
export { envRunPlugins, envServerLimits, envStateStore, } from "./plugins.js";
|
|
19
|
+
export { localStore, StaleGenerationError, USER_LAYOUT, userStates, userUsageBytes, } from "./userState.js";
|
|
19
20
|
export { serverStatus, startupErrors } from "./status.js";
|
|
20
21
|
//# sourceMappingURL=index.js.map
|
package/dist/plugins.d.ts
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import type { ZenPlugin } from '@zenera/engine';
|
|
2
|
+
import { type ServerSettings } from './config.ts';
|
|
3
|
+
import { USER_LAYOUT, type UserStateStore } from './userState.ts';
|
|
2
4
|
export declare function envRunPlugins(env?: NodeJS.ProcessEnv): (run: {
|
|
3
5
|
mcpToken: string | null;
|
|
4
6
|
}) => Promise<ZenPlugin[]>;
|
|
7
|
+
/** What a state store module is given: who it serves, and the user folder's layout. */
|
|
8
|
+
export interface StateStoreOptions {
|
|
9
|
+
projectId: string;
|
|
10
|
+
root: string;
|
|
11
|
+
layout: typeof USER_LAYOUT;
|
|
12
|
+
env: NodeJS.ProcessEnv;
|
|
13
|
+
log: {
|
|
14
|
+
info(message: string): void;
|
|
15
|
+
warn(message: string): void;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** The store `ZEN_STATE_STORE` names, made and checked; a broken one stops the server starting. */
|
|
19
|
+
export declare function envStateStore(env: NodeJS.ProcessEnv, at: {
|
|
20
|
+
projectId: string;
|
|
21
|
+
root: string;
|
|
22
|
+
}): Promise<UserStateStore | undefined>;
|
|
23
|
+
export declare function envServerLimits(env: NodeJS.ProcessEnv): Pick<ServerSettings, 'checkpointSeconds' | 'systemSubject' | 'userDiskQuotaBytes' | 'commandLimits'>;
|
|
5
24
|
//# sourceMappingURL=plugins.d.ts.map
|