@zenera/server 0.0.0-stage → 1.2.0
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 +245 -2
- package/dist/agui.d.ts +29 -0
- package/dist/agui.js +333 -0
- package/dist/auth.d.ts +16 -0
- package/dist/auth.js +79 -0
- package/dist/config.d.ts +71 -0
- package/dist/config.js +44 -0
- package/dist/history.d.ts +8 -0
- package/dist/history.js +73 -0
- package/dist/http.d.ts +9 -0
- package/dist/http.js +26 -0
- package/dist/identity.d.ts +18 -0
- package/dist/identity.js +55 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +20 -0
- package/dist/layout.d.ts +15 -0
- package/dist/layout.js +47 -0
- package/dist/localFileCors.d.ts +10 -0
- package/dist/localFileCors.js +34 -0
- package/dist/memory.d.ts +81 -0
- package/dist/memory.js +471 -0
- package/dist/plugins.d.ts +5 -0
- package/dist/plugins.js +29 -0
- package/dist/projectEnv.d.ts +3 -0
- package/dist/projectEnv.js +37 -0
- package/dist/runLog.d.ts +27 -0
- package/dist/runLog.js +153 -0
- package/dist/runRecords.d.ts +38 -0
- package/dist/runRecords.js +154 -0
- package/dist/runs.d.ts +12 -0
- package/dist/runs.js +376 -0
- package/dist/server.d.ts +28 -0
- package/dist/server.js +105 -0
- package/dist/sessions.d.ts +13 -0
- package/dist/sessions.js +61 -0
- package/dist/state.d.ts +24 -0
- package/dist/state.js +55 -0
- package/dist/status.d.ts +56 -0
- package/dist/status.js +127 -0
- package/dist/store.d.ts +52 -0
- package/dist/store.js +203 -0
- package/dist/token.d.ts +14 -0
- package/dist/token.js +30 -0
- package/dist/trace.d.ts +84 -0
- package/dist/trace.js +389 -0
- package/package.json +56 -5
package/dist/status.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type ProcMode } from '@zenera/sandbox';
|
|
2
|
+
import { type ServerSettings } from './config.ts';
|
|
3
|
+
export interface ServerStatus {
|
|
4
|
+
server: string;
|
|
5
|
+
zen: {
|
|
6
|
+
engine?: string;
|
|
7
|
+
neo?: string;
|
|
8
|
+
sandbox?: string;
|
|
9
|
+
};
|
|
10
|
+
node: string;
|
|
11
|
+
project: {
|
|
12
|
+
id: string;
|
|
13
|
+
dir: string;
|
|
14
|
+
};
|
|
15
|
+
/** jwt: the host's signed tokens; insecure: read, not checked */
|
|
16
|
+
auth: ServerSettings['auth']['mode'];
|
|
17
|
+
/** where it listens, host:port; unset before it does */
|
|
18
|
+
listen?: string;
|
|
19
|
+
debug: boolean;
|
|
20
|
+
sandbox: {
|
|
21
|
+
provider: string;
|
|
22
|
+
/** bwrap: how commands get /proc, or why they cannot be isolated */
|
|
23
|
+
proc?: ProcMode;
|
|
24
|
+
ok: boolean;
|
|
25
|
+
error?: string;
|
|
26
|
+
hint?: string;
|
|
27
|
+
};
|
|
28
|
+
image: {
|
|
29
|
+
/** what this container was built from (ZEN_IMAGE_BUILD / ZEN_IMAGE_BASE) */
|
|
30
|
+
build?: string;
|
|
31
|
+
base?: string;
|
|
32
|
+
release?: string;
|
|
33
|
+
/** what the project's sandbox config asks for */
|
|
34
|
+
wants: {
|
|
35
|
+
build?: string;
|
|
36
|
+
image?: string;
|
|
37
|
+
};
|
|
38
|
+
matches?: boolean;
|
|
39
|
+
error?: string;
|
|
40
|
+
};
|
|
41
|
+
/** where every user's things are: users/<user>/{sessions,memory} */
|
|
42
|
+
root: {
|
|
43
|
+
dir: string;
|
|
44
|
+
version?: number;
|
|
45
|
+
supported: number;
|
|
46
|
+
error?: string;
|
|
47
|
+
};
|
|
48
|
+
/** what each user's memory starts as, when there is anything */
|
|
49
|
+
memorySeed?: string;
|
|
50
|
+
}
|
|
51
|
+
/** Where the server listens, for the report. */
|
|
52
|
+
export declare function setListening(where: string | undefined): void;
|
|
53
|
+
export declare function serverStatus(env?: NodeJS.ProcessEnv): Promise<ServerStatus>;
|
|
54
|
+
/** Why the server must not start, if anything. An image that was never labelled is not one. */
|
|
55
|
+
export declare function startupErrors(status: ServerStatus): string[];
|
|
56
|
+
//# sourceMappingURL=status.d.ts.map
|
package/dist/status.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// What the server can say about itself without being asked anything: `/api/status`, and the
|
|
2
|
+
// checks `zen server start` makes before it listens (`zen server check` prints them).
|
|
3
|
+
// serverStatus - versions, auth, sandbox, image identity, where users' things and the seed are
|
|
4
|
+
// startupErrors - the reasons not to start: nowhere to run commands, the wrong image, a root
|
|
5
|
+
// that is not this server's
|
|
6
|
+
import { resolveBuild } from '@zenera/engine';
|
|
7
|
+
import { readProjectConfig } from '@zenera/neo';
|
|
8
|
+
import { IMAGE_BASE_ENV, IMAGE_BUILD_ENV, probeBwrap, runProcess, } from '@zenera/sandbox';
|
|
9
|
+
import { readFileSync } from 'node:fs';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
import { settings } from "./config.js";
|
|
12
|
+
import { LAYOUT_VERSION, openLayout } from "./layout.js";
|
|
13
|
+
import { memorySeed } from "./store.js";
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
function versionOf(pkg) {
|
|
16
|
+
try {
|
|
17
|
+
const at = require.resolve(`${pkg}/package.json`);
|
|
18
|
+
return JSON.parse(readFileSync(at, 'utf8')).version;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
let listening;
|
|
25
|
+
/** Where the server listens, for the report. */
|
|
26
|
+
export function setListening(where) {
|
|
27
|
+
listening = where;
|
|
28
|
+
}
|
|
29
|
+
/** Whether podman answers: the machine running, on the platforms that have one. */
|
|
30
|
+
async function probePodman() {
|
|
31
|
+
try {
|
|
32
|
+
const info = await runProcess('podman', ['info', '--format', '{{.Host.OS}}'], {
|
|
33
|
+
timeoutMs: 30_000,
|
|
34
|
+
});
|
|
35
|
+
if (info.code === 0)
|
|
36
|
+
return { ok: true };
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
error: `podman is not responding: ${(info.stderr.trim() || `exit ${info.code}`).split('\n')[0]}`,
|
|
40
|
+
hint: process.platform === 'linux' ? undefined : 'podman machine start',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
error: err instanceof Error ? err.message : String(err),
|
|
47
|
+
hint: 'install podman, or run the server in the agent image (bwrap)',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export async function serverStatus(env = process.env) {
|
|
52
|
+
const s = settings();
|
|
53
|
+
const sandbox = { provider: s.sandboxProvider, ok: true };
|
|
54
|
+
if (s.sandbox === 'podman') {
|
|
55
|
+
Object.assign(sandbox, await probePodman());
|
|
56
|
+
}
|
|
57
|
+
else if (s.sandbox === 'bwrap') {
|
|
58
|
+
const probe = await probeBwrap();
|
|
59
|
+
Object.assign(sandbox, {
|
|
60
|
+
ok: probe.ok,
|
|
61
|
+
proc: probe.proc,
|
|
62
|
+
error: probe.error,
|
|
63
|
+
hint: probe.hint,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const image = {
|
|
67
|
+
build: env[IMAGE_BUILD_ENV]?.trim() || undefined,
|
|
68
|
+
base: env[IMAGE_BASE_ENV]?.trim() || undefined,
|
|
69
|
+
release: env.ZEN_RELEASE?.trim() || undefined,
|
|
70
|
+
wants: {},
|
|
71
|
+
};
|
|
72
|
+
try {
|
|
73
|
+
const { root, config } = readProjectConfig(s.projectDir);
|
|
74
|
+
const build = resolveBuild(root, config.sandbox);
|
|
75
|
+
image.wants = build ? { build: build.hash } : { image: config.sandbox?.image };
|
|
76
|
+
if (image.build || image.base) {
|
|
77
|
+
image.matches = build
|
|
78
|
+
? image.build === build.hash
|
|
79
|
+
: image.base === (config.sandbox?.image ?? image.base);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
image.error = err instanceof Error ? err.message : String(err);
|
|
84
|
+
}
|
|
85
|
+
const root = { dir: s.root, supported: LAYOUT_VERSION };
|
|
86
|
+
try {
|
|
87
|
+
root.version = openLayout(s.root, s.projectId, { create: false })?.version;
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
root.error = err instanceof Error ? err.message : String(err);
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
server: versionOf('@zenera/server') ?? 'unknown',
|
|
94
|
+
zen: {
|
|
95
|
+
engine: versionOf('@zenera/engine'),
|
|
96
|
+
neo: versionOf('@zenera/neo'),
|
|
97
|
+
sandbox: versionOf('@zenera/sandbox'),
|
|
98
|
+
},
|
|
99
|
+
node: process.versions.node,
|
|
100
|
+
project: { id: s.projectId, dir: s.projectDir },
|
|
101
|
+
auth: s.auth.mode,
|
|
102
|
+
...(listening ? { listen: listening } : {}),
|
|
103
|
+
debug: s.debug,
|
|
104
|
+
sandbox,
|
|
105
|
+
image,
|
|
106
|
+
root,
|
|
107
|
+
...(memorySeed() ? { memorySeed: memorySeed() } : {}),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/** Why the server must not start, if anything. An image that was never labelled is not one. */
|
|
111
|
+
export function startupErrors(status) {
|
|
112
|
+
const errors = [];
|
|
113
|
+
if (!status.sandbox.ok) {
|
|
114
|
+
errors.push(`commands have nowhere to run: ${status.sandbox.error}` +
|
|
115
|
+
(status.sandbox.hint ? ` (${status.sandbox.hint})` : ''));
|
|
116
|
+
}
|
|
117
|
+
if (status.image.error)
|
|
118
|
+
errors.push(`the project cannot be read: ${status.image.error}`);
|
|
119
|
+
if (status.root.error)
|
|
120
|
+
errors.push(status.root.error);
|
|
121
|
+
if (status.image.matches === false) {
|
|
122
|
+
errors.push(`the image was built for another project environment: ${status.image.build ?? status.image.base}, ` +
|
|
123
|
+
`the project asks for ${status.image.wants.build ?? status.image.wants.image} (rebuild it: zen image build)`);
|
|
124
|
+
}
|
|
125
|
+
return errors;
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=status.js.map
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type Engine, type RunIdentity, type SandboxSetup, type SessionPaths } from '@zenera/engine';
|
|
2
|
+
import type { Request } from 'express';
|
|
3
|
+
export interface ChatThread {
|
|
4
|
+
id: string;
|
|
5
|
+
/** who the thread's runs count against: a user, or the project for shared chats */
|
|
6
|
+
owner: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ChatStore {
|
|
9
|
+
/** The thread a request names, checked to be the caller's; throws an HttpError otherwise. */
|
|
10
|
+
thread(req: Request): ChatThread;
|
|
11
|
+
existingSession(t: ChatThread): SessionPaths | null;
|
|
12
|
+
/** The session, created on the thread's first turn. */
|
|
13
|
+
sessionFor(t: ChatThread): SessionPaths;
|
|
14
|
+
/** The thread's run logs and run records. */
|
|
15
|
+
runsDir(t: ChatThread): string;
|
|
16
|
+
/** The graph the memory explorer reads; undefined when the project declares none. */
|
|
17
|
+
memoryDir(t: ChatThread): string | undefined;
|
|
18
|
+
/** The graph a run remembers into, ready; undefined lets the engine use the project's. */
|
|
19
|
+
engineMemoryDir(t: ChatThread): Promise<string | undefined>;
|
|
20
|
+
removeThread(t: ChatThread): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* How many runs one owner may have going at once. Memory does not lower it: the graph is a
|
|
23
|
+
* SQLite database that several runs remember into at the same time.
|
|
24
|
+
*/
|
|
25
|
+
maxOwnerRuns: number;
|
|
26
|
+
/** Why a run is refused when the owner has that many going. */
|
|
27
|
+
ownerBusy: string;
|
|
28
|
+
}
|
|
29
|
+
/** Where one user's things are. */
|
|
30
|
+
export declare function userDir(user: string): string;
|
|
31
|
+
/** The user's graph, whether or not it has been made yet. */
|
|
32
|
+
export declare function memoryDir(user: string): string;
|
|
33
|
+
/** What a user's memory starts as: the server's seed, else the project's own graph. */
|
|
34
|
+
export declare function memorySeed(): string | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* The user's graph, made first from the seed when they have none: a consistent copy of it
|
|
37
|
+
* (`copyMemory`, the seed opened read-only) into a directory of its own, renamed into place, so
|
|
38
|
+
* a crash never leaves half a graph and two first runs at once make one. No seed, or one not made
|
|
39
|
+
* yet: the user starts with nothing, and their first run makes their graph.
|
|
40
|
+
*/
|
|
41
|
+
export declare function userMemory(user: string): Promise<string>;
|
|
42
|
+
/** Beside a user's graph: which seed it was copied from, and when. */
|
|
43
|
+
export declare const SEED_FILE = "seed.json";
|
|
44
|
+
export declare const privateStore: ChatStore;
|
|
45
|
+
export declare function usesMemory(): boolean;
|
|
46
|
+
export declare function imageReady(setup: SandboxSetup): Promise<void>;
|
|
47
|
+
export declare function openEngineFor(session: SessionPaths, memoryDir: string | undefined, user: RunIdentity | null, mcpToken?: string | null): Promise<Engine>;
|
|
48
|
+
/** The HTTP status for a zen exit code or EngineError code. */
|
|
49
|
+
export declare function statusFor(code: number): number;
|
|
50
|
+
export declare function errorMessage(err: unknown): string;
|
|
51
|
+
export declare function asHttpError(err: unknown): unknown;
|
|
52
|
+
//# sourceMappingURL=store.d.ts.map
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// Where a chat's session, run logs and memory live, and the engine opened on one for a turn.
|
|
2
|
+
// The chat code (runs.ts, history.ts, trace.ts, memory.ts) is the same for every kind of chat; a
|
|
3
|
+
// ChatStore says where a thread's things are and who it belongs to.
|
|
4
|
+
// privateStore - each user's chats, under that user's own folder:
|
|
5
|
+
// <root>/users/<user>/sessions/<thread id>/ - one zen session per chat thread, as `zen run`
|
|
6
|
+
// keeps one: .data/ (state), runs/ (the engine's
|
|
7
|
+
// records), workspace/, and agui/ (its runs'
|
|
8
|
+
// AG-UI events, runLog.ts — never in the
|
|
9
|
+
// sandbox's view)
|
|
10
|
+
// <root>/users/<user>/memory/ - the user's graph: a copy of the seed, made the
|
|
11
|
+
// first time a run of theirs remembers, shared by
|
|
12
|
+
// all their sessions and nobody else's
|
|
13
|
+
// A thread id is only ever looked up under its caller's own folder, so another user's
|
|
14
|
+
// thread is simply not there.
|
|
15
|
+
// userMemory - the user's graph, copied from the seed first if it has to be
|
|
16
|
+
// ChatStore, ChatThread - the contract (a host may serve other chats with it: the Test tab)
|
|
17
|
+
// usesMemory - whether any agent remembers, so the memory explorer has a graph
|
|
18
|
+
// openEngineFor - the project wired to a session, memory and the user the run acts
|
|
19
|
+
// for, its env file applied
|
|
20
|
+
// errorMessage, asHttpError - what an EngineError says, and means to an HTTP caller
|
|
21
|
+
import { createSessionAt, home, EngineError, EXIT, openEngine, sessionPathsAt, } from '@zenera/engine';
|
|
22
|
+
import { copyMemory, isMemory, LEGACY_GRAPH_FILE, memoryDir as projectMemoryDir, readProjectConfig, } from '@zenera/neo';
|
|
23
|
+
import { existsSync } from 'node:fs';
|
|
24
|
+
import { rename, rm, writeFile } from 'node:fs/promises';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import { BwrapSandbox } from '@zenera/sandbox';
|
|
27
|
+
import { settings } from "./config.js";
|
|
28
|
+
import { HttpError } from "./http.js";
|
|
29
|
+
import { identify } from "./identity.js";
|
|
30
|
+
import { applyProjectEnv } from "./projectEnv.js";
|
|
31
|
+
/** Where one user's things are. */
|
|
32
|
+
export function userDir(user) {
|
|
33
|
+
return join(settings().root, 'users', user);
|
|
34
|
+
}
|
|
35
|
+
const sessionDir = (t) => join(userDir(t.owner), 'sessions', t.id);
|
|
36
|
+
/** The user's graph, whether or not it has been made yet. */
|
|
37
|
+
export function memoryDir(user) {
|
|
38
|
+
return join(userDir(user), 'memory');
|
|
39
|
+
}
|
|
40
|
+
/** What a user's memory starts as: the server's seed, else the project's own graph. */
|
|
41
|
+
export function memorySeed() {
|
|
42
|
+
const s = settings();
|
|
43
|
+
if (s.memorySeed === null)
|
|
44
|
+
return undefined;
|
|
45
|
+
if (s.memorySeed)
|
|
46
|
+
return s.memorySeed;
|
|
47
|
+
try {
|
|
48
|
+
const { root, config } = readProjectConfig(s.projectDir);
|
|
49
|
+
return projectMemoryDir(root, config);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const copying = new Map();
|
|
56
|
+
/**
|
|
57
|
+
* The user's graph, made first from the seed when they have none: a consistent copy of it
|
|
58
|
+
* (`copyMemory`, the seed opened read-only) into a directory of its own, renamed into place, so
|
|
59
|
+
* a crash never leaves half a graph and two first runs at once make one. No seed, or one not made
|
|
60
|
+
* yet: the user starts with nothing, and their first run makes their graph.
|
|
61
|
+
*/
|
|
62
|
+
export function userMemory(user) {
|
|
63
|
+
const dir = memoryDir(user);
|
|
64
|
+
const pending = copying.get(dir);
|
|
65
|
+
if (pending)
|
|
66
|
+
return pending;
|
|
67
|
+
const made = (async () => {
|
|
68
|
+
if (existsSync(dir))
|
|
69
|
+
return dir;
|
|
70
|
+
const seed = memorySeed();
|
|
71
|
+
if (seed && !isMemory(seed) && existsSync(join(seed, LEGACY_GRAPH_FILE))) {
|
|
72
|
+
throw new EngineError(`the memory seed ${seed} holds a version 1 memory`, EXIT.invalid, `convert it first: zen memory migrate --dir ${seed}`);
|
|
73
|
+
}
|
|
74
|
+
if (!seed || !isMemory(seed))
|
|
75
|
+
return dir;
|
|
76
|
+
const tmp = `${dir}.copy-${process.pid}-${Date.now()}`;
|
|
77
|
+
try {
|
|
78
|
+
await copyMemory(seed, tmp);
|
|
79
|
+
await writeFile(join(tmp, SEED_FILE), `${JSON.stringify({ seed, copiedAt: new Date().toISOString() }, null, 2)}\n`);
|
|
80
|
+
await rename(tmp, dir);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
await rm(tmp, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
return dir;
|
|
86
|
+
})().finally(() => copying.delete(dir));
|
|
87
|
+
copying.set(dir, made);
|
|
88
|
+
return made;
|
|
89
|
+
}
|
|
90
|
+
/** Beside a user's graph: which seed it was copied from, and when. */
|
|
91
|
+
export const SEED_FILE = 'seed.json';
|
|
92
|
+
export const privateStore = {
|
|
93
|
+
thread(req) {
|
|
94
|
+
const id = identify(req);
|
|
95
|
+
return { id: id.threadId, owner: id.userId };
|
|
96
|
+
},
|
|
97
|
+
existingSession(t) {
|
|
98
|
+
const p = sessionPathsAt(sessionDir(t));
|
|
99
|
+
return existsSync(p.meta) ? p : null;
|
|
100
|
+
},
|
|
101
|
+
sessionFor(t) {
|
|
102
|
+
const dir = sessionDir(t);
|
|
103
|
+
return this.existingSession(t) ?? createSessionAt(dir, join(dir, 'workspace'));
|
|
104
|
+
},
|
|
105
|
+
runsDir: (t) => join(sessionDir(t), 'agui'),
|
|
106
|
+
memoryDir: (t) => memoryDir(t.owner),
|
|
107
|
+
engineMemoryDir: (t) => userMemory(t.owner),
|
|
108
|
+
async removeThread(t) {
|
|
109
|
+
await rm(sessionDir(t), { recursive: true, force: true });
|
|
110
|
+
},
|
|
111
|
+
get maxOwnerRuns() {
|
|
112
|
+
return settings().maxUserRuns;
|
|
113
|
+
},
|
|
114
|
+
ownerBusy: 'your other chats are still answering',
|
|
115
|
+
};
|
|
116
|
+
export function usesMemory() {
|
|
117
|
+
try {
|
|
118
|
+
return readProjectConfig(settings().projectDir).config.agents.some((a) => Boolean(a.memory));
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
throw new EngineError(err instanceof Error ? err.message : String(err), EXIT.invalid);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// A turn must not start a build of its own: the image is built ahead (the editor, `zen image
|
|
125
|
+
// build`). Whichever sandbox the run gets, it says how its image is made ready.
|
|
126
|
+
export async function imageReady(setup) {
|
|
127
|
+
try {
|
|
128
|
+
await setup.sandbox.prepare({ allowBuild: false });
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
132
|
+
const notBuilt = /is not built/.test(message);
|
|
133
|
+
throw new EngineError(notBuilt
|
|
134
|
+
? `the sandbox image ${setup.image} is not built`
|
|
135
|
+
: `the sandbox image is not ready: ${message}`, EXIT.sandbox, notBuilt ? 'rebuild it from the project editor' : err.hint);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** Where the agent image installs zen and the server. */
|
|
139
|
+
const AGENT_APP_DIR = '/opt/zenera/app';
|
|
140
|
+
/** The factory a run's engine opens its sandbox with. */
|
|
141
|
+
function sandboxFactory(s) {
|
|
142
|
+
if (s.sandbox !== 'bwrap')
|
|
143
|
+
return s.sandbox;
|
|
144
|
+
// Every user's chats and graphs, the seed (the server's to read and write, through its
|
|
145
|
+
// tools), the zen home (its keyring), the project (its .env) and, in the agent image, the
|
|
146
|
+
// server's own install are not the command's; its own workspace and home are bound back in.
|
|
147
|
+
const seed = memorySeed();
|
|
148
|
+
const hidden = [
|
|
149
|
+
s.root,
|
|
150
|
+
...(seed ? [seed] : []),
|
|
151
|
+
home(),
|
|
152
|
+
s.projectDir,
|
|
153
|
+
...(existsSync(AGENT_APP_DIR) ? [AGENT_APP_DIR] : []),
|
|
154
|
+
...s.hidden,
|
|
155
|
+
];
|
|
156
|
+
return (opts) => new BwrapSandbox(opts, { hidden });
|
|
157
|
+
}
|
|
158
|
+
export async function openEngineFor(session, memoryDir,
|
|
159
|
+
// Who the run acts for; null is nobody, and stops the engine reading ZEN_USER_* from this process.
|
|
160
|
+
user,
|
|
161
|
+
// The run's MCP broker token from zenera-app, if it gave one.
|
|
162
|
+
mcpToken = null) {
|
|
163
|
+
const s = settings();
|
|
164
|
+
applyProjectEnv();
|
|
165
|
+
const plugins = await s.plugins({ mcpToken });
|
|
166
|
+
return openEngine({
|
|
167
|
+
project: { dir: s.projectDir, name: s.projectId },
|
|
168
|
+
session,
|
|
169
|
+
memoryDir,
|
|
170
|
+
user,
|
|
171
|
+
quiet: true,
|
|
172
|
+
yes: true,
|
|
173
|
+
// Each turn's sandbox goes away; the sandbox HOME is in the session folder and stays.
|
|
174
|
+
disposable: true,
|
|
175
|
+
logger: { warn: (message) => console.warn(`[engine] ${message}`) },
|
|
176
|
+
preflight: s.preflight ?? imageReady,
|
|
177
|
+
// The sandbox is the server's to choose: a plugin in ZEN_PLUGINS does not get to replace it.
|
|
178
|
+
sandboxPlugins: false,
|
|
179
|
+
...(s.sandbox === 'podman' ? {} : { newSandbox: sandboxFactory(s) }),
|
|
180
|
+
...(plugins.length ? { plugins } : {}),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
const STATUS = {
|
|
184
|
+
[EXIT.usage]: 400,
|
|
185
|
+
[EXIT.invalid]: 422,
|
|
186
|
+
[EXIT.credentials]: 503,
|
|
187
|
+
[EXIT.sandbox]: 503,
|
|
188
|
+
};
|
|
189
|
+
/** The HTTP status for a zen exit code or EngineError code. */
|
|
190
|
+
export function statusFor(code) {
|
|
191
|
+
return STATUS[code] ?? 500;
|
|
192
|
+
}
|
|
193
|
+
export function errorMessage(err) {
|
|
194
|
+
if (err instanceof EngineError)
|
|
195
|
+
return err.hint ? `${err.message} (${err.hint})` : err.message;
|
|
196
|
+
return err instanceof Error ? err.message : String(err);
|
|
197
|
+
}
|
|
198
|
+
export function asHttpError(err) {
|
|
199
|
+
if (!(err instanceof EngineError))
|
|
200
|
+
return err;
|
|
201
|
+
return new HttpError(statusFor(err.code), errorMessage(err));
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=store.js.map
|
package/dist/token.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const TOKEN_ENV = "ZEN_SERVER_TOKEN";
|
|
2
|
+
export declare const MIN_TOKEN_LENGTH = 32;
|
|
3
|
+
export interface ServerToken {
|
|
4
|
+
token: string;
|
|
5
|
+
/** where it came from: the environment, or the file */
|
|
6
|
+
from: 'env' | string;
|
|
7
|
+
}
|
|
8
|
+
/** Where a project's server for a user keeps its token. Both are single path segments here. */
|
|
9
|
+
export declare function tokenFile(project: string, user: string): string;
|
|
10
|
+
export declare function serverToken(file: string, opts?: {
|
|
11
|
+
create?: boolean;
|
|
12
|
+
env?: NodeJS.ProcessEnv;
|
|
13
|
+
}): ServerToken | undefined;
|
|
14
|
+
//# sourceMappingURL=token.d.ts.map
|
package/dist/token.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// The server's bearer token, when its host gave none: one per project and user, in the zen home
|
|
2
|
+
// (which no sandboxed command can see), readable by its owner only.
|
|
3
|
+
// tokenFile - <zen home>/servers/<project>/<user>.token
|
|
4
|
+
// serverToken - $ZEN_SERVER_TOKEN, else the file, else a new one written there (`create`)
|
|
5
|
+
import { randomBytes } from 'node:crypto';
|
|
6
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { home } from '@zenera/engine';
|
|
8
|
+
import { dirname, join } from 'node:path';
|
|
9
|
+
export const TOKEN_ENV = 'ZEN_SERVER_TOKEN';
|
|
10
|
+
export const MIN_TOKEN_LENGTH = 32;
|
|
11
|
+
/** Where a project's server for a user keeps its token. Both are single path segments here. */
|
|
12
|
+
export function tokenFile(project, user) {
|
|
13
|
+
const segment = (s) => s.replace(/[^\w.@-]/g, '_');
|
|
14
|
+
return join(home(), 'servers', segment(project), `${segment(user)}.token`);
|
|
15
|
+
}
|
|
16
|
+
export function serverToken(file, opts = {}) {
|
|
17
|
+
const given = (opts.env ?? process.env)[TOKEN_ENV]?.trim();
|
|
18
|
+
if (given)
|
|
19
|
+
return { token: given, from: 'env' };
|
|
20
|
+
if (existsSync(file))
|
|
21
|
+
return { token: readFileSync(file, 'utf8').trim(), from: file };
|
|
22
|
+
if (!opts.create)
|
|
23
|
+
return undefined;
|
|
24
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
25
|
+
const token = randomBytes(32).toString('base64url');
|
|
26
|
+
writeFileSync(file, `${token}\n`, { mode: 0o600, flag: 'wx' });
|
|
27
|
+
chmodSync(file, 0o600);
|
|
28
|
+
return { token, from: file };
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=token.js.map
|
package/dist/trace.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { PayloadResolver, type AgentState, type TokenUsage, type TrajectoryNode } from '@zenera/neo';
|
|
2
|
+
import { Router, type Request } from 'express';
|
|
3
|
+
import type { ChatStore } from './store.ts';
|
|
4
|
+
export declare const MAX_PART_CHARS: number;
|
|
5
|
+
export declare const MAX_REQUEST_CHARS: number;
|
|
6
|
+
export interface TraceStep {
|
|
7
|
+
/** position in the whole session, flattened; the address of the step */
|
|
8
|
+
index: number;
|
|
9
|
+
id: string;
|
|
10
|
+
type: TrajectoryNode['type'];
|
|
11
|
+
agent: string;
|
|
12
|
+
ts: string;
|
|
13
|
+
/** 0 on the trunk, one more per fork */
|
|
14
|
+
depth: number;
|
|
15
|
+
/** fork branch path, e.g. `research › web` */
|
|
16
|
+
branch?: string;
|
|
17
|
+
/** the step's id in `zen inspect graph|node|ask` */
|
|
18
|
+
node: string;
|
|
19
|
+
title: string;
|
|
20
|
+
model?: string;
|
|
21
|
+
tools?: string[];
|
|
22
|
+
usage?: TokenUsage;
|
|
23
|
+
stopReason?: string;
|
|
24
|
+
isError?: boolean;
|
|
25
|
+
durationMs?: number;
|
|
26
|
+
synthetic?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface TracePage {
|
|
29
|
+
runId: string;
|
|
30
|
+
engineRunId: string;
|
|
31
|
+
scope: 'turn' | 'session';
|
|
32
|
+
/** false for runs recorded before turn boundaries were kept: only the session can be shown */
|
|
33
|
+
turnAvailable: boolean;
|
|
34
|
+
total: number;
|
|
35
|
+
offset: number;
|
|
36
|
+
limit: number;
|
|
37
|
+
counts: Partial<Record<TrajectoryNode['type'], number>>;
|
|
38
|
+
steps: TraceStep[];
|
|
39
|
+
}
|
|
40
|
+
export interface TracePart {
|
|
41
|
+
key: string;
|
|
42
|
+
label: string;
|
|
43
|
+
kind: 'text' | 'json';
|
|
44
|
+
text?: string;
|
|
45
|
+
json?: unknown;
|
|
46
|
+
/** recorded size, for a payload */
|
|
47
|
+
bytes?: number;
|
|
48
|
+
truncated?: boolean;
|
|
49
|
+
/** too large to send with the step: fetch it with ?part=<key> */
|
|
50
|
+
deferred?: boolean;
|
|
51
|
+
error?: string;
|
|
52
|
+
}
|
|
53
|
+
export interface TraceStepDetail {
|
|
54
|
+
step: TraceStep;
|
|
55
|
+
parts: TracePart[];
|
|
56
|
+
}
|
|
57
|
+
interface Flat {
|
|
58
|
+
node: TrajectoryNode;
|
|
59
|
+
depth: number;
|
|
60
|
+
branch?: string;
|
|
61
|
+
/** index of the trunk node it belongs to */
|
|
62
|
+
top: number;
|
|
63
|
+
/** `zen inspect` id */
|
|
64
|
+
key: string;
|
|
65
|
+
}
|
|
66
|
+
export declare function flatten(trajectory: TrajectoryNode[]): Flat[];
|
|
67
|
+
export declare function toStep(f: Flat, index: number): TraceStep;
|
|
68
|
+
export declare function openTrace(store: ChatStore, req: Request): Promise<{
|
|
69
|
+
thread: import("./store.ts").ChatThread;
|
|
70
|
+
record: import("./runRecords.ts").RunRecord;
|
|
71
|
+
runDir: string;
|
|
72
|
+
state: AgentState;
|
|
73
|
+
payloads: PayloadResolver;
|
|
74
|
+
flat: Flat[];
|
|
75
|
+
}>;
|
|
76
|
+
/** The step a route names by its index, or 404. */
|
|
77
|
+
export declare function stepParam(flat: Flat[], raw: string): {
|
|
78
|
+
f: Flat;
|
|
79
|
+
index: number;
|
|
80
|
+
};
|
|
81
|
+
export declare function stepParts(n: TrajectoryNode, payloads: PayloadResolver): Promise<TracePart[]>;
|
|
82
|
+
export declare function traceRouter(store: ChatStore): Router;
|
|
83
|
+
export {};
|
|
84
|
+
//# sourceMappingURL=trace.d.ts.map
|