@zenera/server 0.0.0-stage → 1.2.1
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 +157 -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/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/trace.d.ts +84 -0
- package/dist/trace.js +389 -0
- package/package.json +56 -5
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { SandboxSetup, ZenPlugin } from '@zenera/engine';
|
|
2
|
+
import { type SandboxFactory } from '@zenera/sandbox';
|
|
3
|
+
export type ServerAuth =
|
|
4
|
+
/** EdDSA tokens signed per request by the host (zenera-app), checked with its public key */
|
|
5
|
+
{
|
|
6
|
+
mode: 'jwt';
|
|
7
|
+
publicKey: string;
|
|
8
|
+
issuer: string;
|
|
9
|
+
audience: string;
|
|
10
|
+
}
|
|
11
|
+
/** the same tokens, their signature, issuer, audience, age and binding not checked */
|
|
12
|
+
| {
|
|
13
|
+
mode: 'insecure';
|
|
14
|
+
};
|
|
15
|
+
export interface ServerSettings {
|
|
16
|
+
/** the project the agents come from; read-only to the server */
|
|
17
|
+
projectDir: string;
|
|
18
|
+
projectId: string;
|
|
19
|
+
/** everything the server writes: `<root>/users/<user>/{sessions,memory}`, `<root>/server.json` */
|
|
20
|
+
root: string;
|
|
21
|
+
/**
|
|
22
|
+
* The graph each user's memory starts as, copied the first time they need one. Unset: the
|
|
23
|
+
* project's own (`memory:` in agents.yaml); `null`: none, every user starts empty.
|
|
24
|
+
*/
|
|
25
|
+
memorySeed?: string | null;
|
|
26
|
+
auth: ServerAuth;
|
|
27
|
+
/** the Test tab's routes: recorded runs, traces, the memory graph */
|
|
28
|
+
debug: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Where commands run: `'podman'` as `zen run` has it, a container per session on this
|
|
31
|
+
* machine; `'bwrap'` in the agent image, with `hidden`, the root and the seed kept from
|
|
32
|
+
* them; or a factory. `defaultSandbox()` picks between the first two.
|
|
33
|
+
*/
|
|
34
|
+
sandbox: 'bwrap' | 'podman' | SandboxFactory;
|
|
35
|
+
/** what to call it, for `/api/status` */
|
|
36
|
+
sandboxProvider: string;
|
|
37
|
+
/**
|
|
38
|
+
* Readies a turn's sandbox before it runs. Unset: its image must already be there
|
|
39
|
+
* (`imageReady`) — the agent image's, or one built ahead of time.
|
|
40
|
+
*/
|
|
41
|
+
preflight?: (setup: SandboxSetup) => Promise<void>;
|
|
42
|
+
/** more paths bwrap commands never see (the root, the seed, the zen home and the project always) */
|
|
43
|
+
hidden: string[];
|
|
44
|
+
/** this run's own plugins (the MCP tools for its user), given the token its caller sent */
|
|
45
|
+
plugins: (run: {
|
|
46
|
+
mcpToken: string | null;
|
|
47
|
+
}) => Promise<ZenPlugin[]>;
|
|
48
|
+
maxRuns: number;
|
|
49
|
+
/** how many runs one user may have going at once */
|
|
50
|
+
maxUserRuns: number;
|
|
51
|
+
maxRunMinutes: number;
|
|
52
|
+
runLogMaxBytes: number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* bwrap inside the agent image, which ships it at /opt/zenera/bin/bwrap; podman anywhere else,
|
|
56
|
+
* as `zen run`. The image is the container, so podman has nothing to run there.
|
|
57
|
+
*/
|
|
58
|
+
export declare function defaultSandbox(): 'bwrap' | 'podman';
|
|
59
|
+
/** Inside the agent image: where `--insecure` is refused unless the host that runs it says so. */
|
|
60
|
+
export declare function inAgentImage(): boolean;
|
|
61
|
+
export declare const SANDBOX_PROVIDERS: {
|
|
62
|
+
readonly bwrap: "bwrap (this container)";
|
|
63
|
+
readonly podman: "podman (this machine)";
|
|
64
|
+
};
|
|
65
|
+
/** What zenera-app's tokens say today. */
|
|
66
|
+
export declare const DEFAULT_ISSUER = "zenera-app";
|
|
67
|
+
export declare const DEFAULT_AUDIENCE = "zenera-runtime";
|
|
68
|
+
export declare const DEFAULTS: Omit<ServerSettings, 'projectId' | 'auth' | 'root'>;
|
|
69
|
+
export declare function configure(settings: ServerSettings): void;
|
|
70
|
+
export declare function settings(): ServerSettings;
|
|
71
|
+
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ZENERA_BWRAP } from '@zenera/sandbox';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
/**
|
|
4
|
+
* bwrap inside the agent image, which ships it at /opt/zenera/bin/bwrap; podman anywhere else,
|
|
5
|
+
* as `zen run`. The image is the container, so podman has nothing to run there.
|
|
6
|
+
*/
|
|
7
|
+
export function defaultSandbox() {
|
|
8
|
+
return existsSync(ZENERA_BWRAP) ? 'bwrap' : 'podman';
|
|
9
|
+
}
|
|
10
|
+
/** Inside the agent image: where `--insecure` is refused unless the host that runs it says so. */
|
|
11
|
+
export function inAgentImage() {
|
|
12
|
+
return existsSync(ZENERA_BWRAP);
|
|
13
|
+
}
|
|
14
|
+
export const SANDBOX_PROVIDERS = {
|
|
15
|
+
bwrap: 'bwrap (this container)',
|
|
16
|
+
podman: 'podman (this machine)',
|
|
17
|
+
};
|
|
18
|
+
/** What zenera-app's tokens say today. */
|
|
19
|
+
export const DEFAULT_ISSUER = 'zenera-app';
|
|
20
|
+
export const DEFAULT_AUDIENCE = 'zenera-runtime';
|
|
21
|
+
const sandbox = defaultSandbox();
|
|
22
|
+
export const DEFAULTS = {
|
|
23
|
+
projectDir: '/project',
|
|
24
|
+
debug: false,
|
|
25
|
+
sandbox,
|
|
26
|
+
sandboxProvider: SANDBOX_PROVIDERS[sandbox],
|
|
27
|
+
hidden: [],
|
|
28
|
+
plugins: async () => [],
|
|
29
|
+
maxRuns: 4,
|
|
30
|
+
maxUserRuns: 2,
|
|
31
|
+
maxRunMinutes: 120,
|
|
32
|
+
runLogMaxBytes: 50 * 1024 * 1024,
|
|
33
|
+
};
|
|
34
|
+
let current;
|
|
35
|
+
export function configure(settings) {
|
|
36
|
+
current = settings;
|
|
37
|
+
}
|
|
38
|
+
export function settings() {
|
|
39
|
+
if (!current) {
|
|
40
|
+
throw new Error('the server is not configured: call createServer() first');
|
|
41
|
+
}
|
|
42
|
+
return current;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Message } from '@ag-ui/core';
|
|
2
|
+
import { type TrajectoryNode } from '@zenera/neo';
|
|
3
|
+
import { Router } from 'express';
|
|
4
|
+
import { type Resolve } from './agui.ts';
|
|
5
|
+
import type { ChatStore } from './store.ts';
|
|
6
|
+
export declare function toMessages(trajectory: TrajectoryNode[], resolve: Resolve): Promise<Message[]>;
|
|
7
|
+
export declare function historyRouter(store: ChatStore): Router;
|
|
8
|
+
//# sourceMappingURL=history.d.ts.map
|
package/dist/history.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { FilePayloadStore, PayloadResolver, } from '@zenera/neo';
|
|
2
|
+
import { Router } from 'express';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { clip } from "./agui.js";
|
|
6
|
+
import { readRunLog, threadRunLogs } from "./runLog.js";
|
|
7
|
+
import { currentRun } from "./runs.js";
|
|
8
|
+
export async function toMessages(trajectory, resolve) {
|
|
9
|
+
const out = [];
|
|
10
|
+
for (const node of trajectory) {
|
|
11
|
+
if (node.type === 'user_input' && !node.synthetic) {
|
|
12
|
+
const texts = await Promise.all(node.content.flatMap((part) => (part.type === 'text' ? [resolve(part.text)] : [])));
|
|
13
|
+
const content = texts.join('\n');
|
|
14
|
+
if (content)
|
|
15
|
+
out.push({ id: node.id, role: 'user', content });
|
|
16
|
+
}
|
|
17
|
+
else if (node.type === 'llm_call') {
|
|
18
|
+
const text = await resolve(node.text);
|
|
19
|
+
const toolCalls = await Promise.all(node.toolCalls.map(async (call) => ({
|
|
20
|
+
id: call.callId,
|
|
21
|
+
type: 'function',
|
|
22
|
+
function: { name: call.name, arguments: await resolve(call.args) },
|
|
23
|
+
})));
|
|
24
|
+
if (!text.trim() && toolCalls.length === 0)
|
|
25
|
+
continue;
|
|
26
|
+
out.push({
|
|
27
|
+
id: node.id,
|
|
28
|
+
role: 'assistant',
|
|
29
|
+
name: node.agent,
|
|
30
|
+
...(text.trim() ? { content: text } : {}),
|
|
31
|
+
...(toolCalls.length ? { toolCalls } : {}),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
else if (node.type === 'tool_result') {
|
|
35
|
+
out.push({
|
|
36
|
+
id: node.id,
|
|
37
|
+
role: 'tool',
|
|
38
|
+
toolCallId: node.callId,
|
|
39
|
+
content: clip(await resolve(node.result)),
|
|
40
|
+
...(node.isError ? { error: 'the tool reported an error' } : {}),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
export function historyRouter(store) {
|
|
47
|
+
const router = Router();
|
|
48
|
+
router.get('/:tid', async (req, res) => {
|
|
49
|
+
const t = store.thread(req);
|
|
50
|
+
const running = currentRun(t);
|
|
51
|
+
const logs = await threadRunLogs(store.runsDir(t));
|
|
52
|
+
if (logs.length > 0) {
|
|
53
|
+
const events = [];
|
|
54
|
+
for (const log of logs) {
|
|
55
|
+
if (log.runId !== running?.runId)
|
|
56
|
+
events.push(...(await readRunLog(log.file)));
|
|
57
|
+
}
|
|
58
|
+
res.json({ threadId: t.id, messages: [], events, running });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const session = store.existingSession(t);
|
|
62
|
+
if (!session || !existsSync(session.state)) {
|
|
63
|
+
res.json({ threadId: t.id, messages: [], events: [], running });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const state = JSON.parse(await readFile(session.state, 'utf8'));
|
|
67
|
+
const payloads = new PayloadResolver(new FilePayloadStore({ dir: session.blobs, id: 'file' }));
|
|
68
|
+
const messages = await toMessages(state.trajectory, (p) => payloads.get(p));
|
|
69
|
+
res.json({ threadId: t.id, messages, events: [], running });
|
|
70
|
+
});
|
|
71
|
+
return router;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=history.js.map
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextFunction, Request, Response } from 'express';
|
|
2
|
+
export declare class HttpError extends Error {
|
|
3
|
+
readonly status: number;
|
|
4
|
+
constructor(status: number, message: string);
|
|
5
|
+
}
|
|
6
|
+
export declare function errorMessage(err: unknown): string;
|
|
7
|
+
export declare function notFound(_req: Request, res: Response): void;
|
|
8
|
+
export declare function errorHandler(err: unknown, req: Request, res: Response, _next: NextFunction): void;
|
|
9
|
+
//# sourceMappingURL=http.d.ts.map
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export class HttpError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
constructor(status, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.status = status;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export function errorMessage(err) {
|
|
9
|
+
const e = err;
|
|
10
|
+
return (e?.stderr || e?.message || String(err)).trim();
|
|
11
|
+
}
|
|
12
|
+
export function notFound(_req, res) {
|
|
13
|
+
res.status(404).json({ error: 'Not found' });
|
|
14
|
+
}
|
|
15
|
+
// Express 5 forwards rejected async handlers here, so routes just throw.
|
|
16
|
+
export function errorHandler(err, req, res, _next) {
|
|
17
|
+
if (res.headersSent) {
|
|
18
|
+
res.end();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const status = err instanceof HttpError ? err.status : (err?.status ?? 500);
|
|
22
|
+
if (status >= 500)
|
|
23
|
+
console.error(`${req.method} ${req.originalUrl}:`, err);
|
|
24
|
+
res.status(status).json({ error: errorMessage(err) });
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { RunIdentity } from '@zenera/engine';
|
|
2
|
+
import type { Request } from 'express';
|
|
3
|
+
import type { CallerClaims } from './auth.ts';
|
|
4
|
+
export interface Identity {
|
|
5
|
+
/** the folder under <root>/users/ */
|
|
6
|
+
userId: string;
|
|
7
|
+
threadId: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function isThreadId(id: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Where a user's things are: their id when it is one safe path segment (a UUID lower-cased, as
|
|
12
|
+
* zenera-app's are), else `u-` and a hash of it — so no id is ever a path of its own.
|
|
13
|
+
*/
|
|
14
|
+
export declare function userFolder(sub: string): string;
|
|
15
|
+
export declare function caller(req: Request): CallerClaims;
|
|
16
|
+
export declare function identify(req: Request): Identity;
|
|
17
|
+
export declare function runUser(req: Request): RunIdentity | null;
|
|
18
|
+
//# sourceMappingURL=identity.d.ts.map
|
package/dist/identity.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { settings } from "./config.js";
|
|
3
|
+
import { HttpError } from "./http.js";
|
|
4
|
+
const SAFE_ID = /^[A-Za-z0-9][\w.@-]{0,99}$/;
|
|
5
|
+
const THREAD_ID = /^[A-Za-z0-9][\w-]{0,79}$/;
|
|
6
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7
|
+
const ROLES = new Set(['owner', 'editor', 'viewer', 'admin']);
|
|
8
|
+
// An email goes into an environment variable, so it is held to the shape of one.
|
|
9
|
+
const EMAIL = /^[^\s@]{1,200}@[^\s@]{1,200}$/;
|
|
10
|
+
const NAME = /^[^\n\r\0]{1,200}$/;
|
|
11
|
+
export function isThreadId(id) {
|
|
12
|
+
return THREAD_ID.test(id);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Where a user's things are: their id when it is one safe path segment (a UUID lower-cased, as
|
|
16
|
+
* zenera-app's are), else `u-` and a hash of it — so no id is ever a path of its own.
|
|
17
|
+
*/
|
|
18
|
+
export function userFolder(sub) {
|
|
19
|
+
if (UUID.test(sub))
|
|
20
|
+
return sub.toLowerCase();
|
|
21
|
+
if (SAFE_ID.test(sub))
|
|
22
|
+
return sub;
|
|
23
|
+
return `u-${createHash('sha256').update(sub).digest('hex').slice(0, 32)}`;
|
|
24
|
+
}
|
|
25
|
+
export function caller(req) {
|
|
26
|
+
const claims = req.res?.locals.caller;
|
|
27
|
+
if (!claims)
|
|
28
|
+
throw new HttpError(401, 'no verified caller');
|
|
29
|
+
return claims;
|
|
30
|
+
}
|
|
31
|
+
export function identify(req) {
|
|
32
|
+
const claims = caller(req);
|
|
33
|
+
const threadId = String(req.params.tid ?? '');
|
|
34
|
+
if (!isThreadId(threadId))
|
|
35
|
+
throw new HttpError(400, 'not a thread id');
|
|
36
|
+
if (claims.thr !== undefined && String(claims.thr).toLowerCase() !== threadId.toLowerCase()) {
|
|
37
|
+
throw new HttpError(403, 'the token is for another thread');
|
|
38
|
+
}
|
|
39
|
+
return { userId: userFolder(claims.sub), threadId };
|
|
40
|
+
}
|
|
41
|
+
export function runUser(req) {
|
|
42
|
+
const claims = caller(req);
|
|
43
|
+
const role = typeof claims.role === 'string' && ROLES.has(claims.role) ? claims.role : undefined;
|
|
44
|
+
const email = typeof claims.email === 'string' && EMAIL.test(claims.email) ? claims.email : undefined;
|
|
45
|
+
const name = typeof claims.name === 'string' && NAME.test(claims.name) ? claims.name : undefined;
|
|
46
|
+
return {
|
|
47
|
+
id: UUID.test(claims.sub) ? claims.sub.toLowerCase() : claims.sub,
|
|
48
|
+
...(role ? { role } : {}),
|
|
49
|
+
...(email ? { email } : {}),
|
|
50
|
+
...(name ? { name } : {}),
|
|
51
|
+
// An unsigned token is only someone's say-so.
|
|
52
|
+
source: settings().auth.mode === 'jwt' ? 'host' : 'local',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=identity.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { createServer, insecureHere, INSECURE_LOCAL_ENV, type AgentServer, type ListenOn, type ServerOptions, } from './server.ts';
|
|
2
|
+
export { configure, settings, DEFAULTS, defaultSandbox, inAgentImage, DEFAULT_AUDIENCE, DEFAULT_ISSUER, SANDBOX_PROVIDERS, type ServerAuth, type ServerSettings, } from './config.ts';
|
|
3
|
+
export { requireCaller, unsignedToken, verifyCaller, type CallerClaims } from './auth.ts';
|
|
4
|
+
export { identify, runUser, userFolder, type Identity } from './identity.ts';
|
|
5
|
+
export { LAYOUT_VERSION, LayoutError, openLayout, type Layout } from './layout.ts';
|
|
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';
|
|
8
|
+
export { historyRouter, toMessages } from './history.ts';
|
|
9
|
+
export { traceRouter, openTrace, stepParam } from './trace.ts';
|
|
10
|
+
export { memoryRouter } from './memory.ts';
|
|
11
|
+
export { AguiTranslator, clip } from './agui.ts';
|
|
12
|
+
export { RunLog, compact, readRunLog, threadRunLogs } from './runLog.ts';
|
|
13
|
+
export * from './runRecords.ts';
|
|
14
|
+
export { applyProjectEnv, withoutProjectEnv } from './projectEnv.ts';
|
|
15
|
+
export { HttpError, errorHandler, notFound } from './http.ts';
|
|
16
|
+
export { envRunPlugins } from './plugins.ts';
|
|
17
|
+
export { serverStatus, startupErrors, type ServerStatus } from './status.ts';
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// @zenera/server: a project's chats over HTTP — the server in the agent image, and the pieces a
|
|
2
|
+
// host builds its own chat routes from (the ZeneraLite editor's Test tab).
|
|
3
|
+
export { createServer, insecureHere, INSECURE_LOCAL_ENV, } from "./server.js";
|
|
4
|
+
export { configure, settings, DEFAULTS, defaultSandbox, inAgentImage, DEFAULT_AUDIENCE, DEFAULT_ISSUER, SANDBOX_PROVIDERS, } from "./config.js";
|
|
5
|
+
export { requireCaller, unsignedToken, verifyCaller } from "./auth.js";
|
|
6
|
+
export { identify, runUser, userFolder } from "./identity.js";
|
|
7
|
+
export { LAYOUT_VERSION, LayoutError, openLayout } from "./layout.js";
|
|
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";
|
|
10
|
+
export { historyRouter, toMessages } from "./history.js";
|
|
11
|
+
export { traceRouter, openTrace, stepParam } from "./trace.js";
|
|
12
|
+
export { memoryRouter } from "./memory.js";
|
|
13
|
+
export { AguiTranslator, clip } from "./agui.js";
|
|
14
|
+
export { RunLog, compact, readRunLog, threadRunLogs } from "./runLog.js";
|
|
15
|
+
export * from "./runRecords.js";
|
|
16
|
+
export { applyProjectEnv, withoutProjectEnv } from "./projectEnv.js";
|
|
17
|
+
export { HttpError, errorHandler, notFound } from "./http.js";
|
|
18
|
+
export { envRunPlugins } from "./plugins.js";
|
|
19
|
+
export { serverStatus, startupErrors } from "./status.js";
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
package/dist/layout.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** 1: users/<user>/{sessions/<thread>/, memory/}. */
|
|
2
|
+
export declare const LAYOUT_VERSION = 1;
|
|
3
|
+
export interface Layout {
|
|
4
|
+
version: number;
|
|
5
|
+
projectId: string;
|
|
6
|
+
createdAt: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class LayoutError extends Error {
|
|
9
|
+
name: string;
|
|
10
|
+
}
|
|
11
|
+
/** `create: false` only reads (a check must not claim a directory): missing is `undefined`. */
|
|
12
|
+
export declare function openLayout(root: string, projectId: string, opts?: {
|
|
13
|
+
create?: boolean;
|
|
14
|
+
}): Layout | undefined;
|
|
15
|
+
//# sourceMappingURL=layout.d.ts.map
|
package/dist/layout.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// The server root's own description: which layout it is in and which project it serves, so a
|
|
2
|
+
// server never reads (or writes over) a root it does not understand. Written on first start.
|
|
3
|
+
// LAYOUT_VERSION - the layout this server writes
|
|
4
|
+
// openLayout - create or check <root>/server.json
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
/** 1: users/<user>/{sessions/<thread>/, memory/}. */
|
|
8
|
+
export const LAYOUT_VERSION = 1;
|
|
9
|
+
export class LayoutError extends Error {
|
|
10
|
+
name = 'LayoutError';
|
|
11
|
+
}
|
|
12
|
+
/** `create: false` only reads (a check must not claim a directory): missing is `undefined`. */
|
|
13
|
+
export function openLayout(root, projectId, opts = {}) {
|
|
14
|
+
const file = join(root, 'server.json');
|
|
15
|
+
if (!existsSync(file)) {
|
|
16
|
+
if (opts.create === false)
|
|
17
|
+
return undefined;
|
|
18
|
+
// Every user's chats: nobody else on the machine reads them.
|
|
19
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
20
|
+
const layout = {
|
|
21
|
+
version: LAYOUT_VERSION,
|
|
22
|
+
projectId,
|
|
23
|
+
createdAt: new Date().toISOString(),
|
|
24
|
+
};
|
|
25
|
+
writeFileSync(`${file}.tmp`, `${JSON.stringify(layout, null, 2)}\n`);
|
|
26
|
+
renameSync(`${file}.tmp`, file);
|
|
27
|
+
return layout;
|
|
28
|
+
}
|
|
29
|
+
let layout;
|
|
30
|
+
try {
|
|
31
|
+
layout = JSON.parse(readFileSync(file, 'utf8'));
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
throw new LayoutError(`${file} is not readable: ${err instanceof Error ? err.message : err}`);
|
|
35
|
+
}
|
|
36
|
+
if (!Number.isInteger(layout.version) || layout.version < 1) {
|
|
37
|
+
throw new LayoutError(`${file} names no layout version`);
|
|
38
|
+
}
|
|
39
|
+
if (layout.version > LAYOUT_VERSION) {
|
|
40
|
+
throw new LayoutError(`${root} is in layout ${layout.version}, written by a newer server; this one knows ${LAYOUT_VERSION}`);
|
|
41
|
+
}
|
|
42
|
+
if (layout.projectId !== projectId) {
|
|
43
|
+
throw new LayoutError(`${root} belongs to project ${layout.projectId}, not ${projectId}`);
|
|
44
|
+
}
|
|
45
|
+
return layout;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=layout.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express middleware allowing the standalone file:// chat to reach an insecure runtime.
|
|
3
|
+
* localFileCors handles Origin: null responses and restricted preflight methods/headers;
|
|
4
|
+
* server.ts installs it before requireCaller so preflights work without bypassing
|
|
5
|
+
* authentication on actual API requests. Secure servers do not enable this CORS policy.
|
|
6
|
+
*/
|
|
7
|
+
import type { NextFunction, Request, Response } from 'express';
|
|
8
|
+
/** Local HTML has an opaque origin. This opt-in exists only for insecure dev servers. */
|
|
9
|
+
export declare function localFileCors(req: Request, res: Response, next: NextFunction): void;
|
|
10
|
+
//# sourceMappingURL=localFileCors.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { settings } from "./config.js";
|
|
2
|
+
import { HttpError } from "./http.js";
|
|
3
|
+
const METHODS = new Set(['GET', 'POST', 'DELETE']);
|
|
4
|
+
const HEADERS = new Set(['authorization', 'content-type', 'accept']);
|
|
5
|
+
/** Local HTML has an opaque origin. This opt-in exists only for insecure dev servers. */
|
|
6
|
+
export function localFileCors(req, res, next) {
|
|
7
|
+
if (settings().auth.mode !== 'insecure' || req.get('origin') !== 'null') {
|
|
8
|
+
next();
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
res.vary('Origin');
|
|
12
|
+
res.set('Access-Control-Allow-Origin', 'null');
|
|
13
|
+
if (req.method !== 'OPTIONS') {
|
|
14
|
+
next();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const method = req.get('access-control-request-method');
|
|
18
|
+
const headers = (req.get('access-control-request-headers') ?? '')
|
|
19
|
+
.split(',')
|
|
20
|
+
.map((header) => header.trim().toLowerCase())
|
|
21
|
+
.filter(Boolean);
|
|
22
|
+
if (!method || !METHODS.has(method) || headers.some((header) => !HEADERS.has(header))) {
|
|
23
|
+
next(new HttpError(403, 'local-file request method or headers are not allowed'));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
res.set('Access-Control-Allow-Methods', [...METHODS].join(', '));
|
|
27
|
+
res.set('Access-Control-Allow-Headers', [...HEADERS].join(', '));
|
|
28
|
+
// Older Chromium versions request this for a local-network preflight.
|
|
29
|
+
if (req.get('access-control-request-private-network') === 'true') {
|
|
30
|
+
res.set('Access-Control-Allow-Private-Network', 'true');
|
|
31
|
+
}
|
|
32
|
+
res.sendStatus(204);
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=localFileCors.js.map
|
package/dist/memory.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type Embedder, type MemoryEdge, type MemoryGrepResult, type MemoryNode, type NodeVia, type ProjectConfig, type ReportNode } from '@zenera/neo';
|
|
2
|
+
import { Router } from 'express';
|
|
3
|
+
import { type ChatStore } from './store.ts';
|
|
4
|
+
export type MemoryStatus = 'disabled' | 'not-created' | 'ready';
|
|
5
|
+
export interface MemoryStats {
|
|
6
|
+
nodes: number;
|
|
7
|
+
edges: number;
|
|
8
|
+
superseded: number;
|
|
9
|
+
files: number;
|
|
10
|
+
embedding?: {
|
|
11
|
+
model: string;
|
|
12
|
+
dimensions: number;
|
|
13
|
+
};
|
|
14
|
+
vectors?: {
|
|
15
|
+
rows: number;
|
|
16
|
+
dims: number;
|
|
17
|
+
bytes: number;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface MemoryOverview {
|
|
21
|
+
status: MemoryStatus;
|
|
22
|
+
stats: MemoryStats;
|
|
23
|
+
kinds: string[];
|
|
24
|
+
audiences: string[];
|
|
25
|
+
}
|
|
26
|
+
export interface MemoryNodesPage {
|
|
27
|
+
status: MemoryStatus;
|
|
28
|
+
nodes: ReportNode[];
|
|
29
|
+
found: number;
|
|
30
|
+
offset: number;
|
|
31
|
+
limit: number;
|
|
32
|
+
truncated: boolean;
|
|
33
|
+
kinds: string[];
|
|
34
|
+
audiences: string[];
|
|
35
|
+
}
|
|
36
|
+
export interface MemoryNodeDetails {
|
|
37
|
+
status: 'ready';
|
|
38
|
+
node: ReportNode;
|
|
39
|
+
edges: MemoryEdge[];
|
|
40
|
+
}
|
|
41
|
+
export interface MemoryGrepResponse extends MemoryGrepResult {
|
|
42
|
+
status: MemoryStatus;
|
|
43
|
+
}
|
|
44
|
+
export interface MemoryRecallNode {
|
|
45
|
+
node: MemoryNode;
|
|
46
|
+
score: number;
|
|
47
|
+
seed: boolean;
|
|
48
|
+
via?: NodeVia;
|
|
49
|
+
stale: boolean;
|
|
50
|
+
}
|
|
51
|
+
export interface MemoryRecallResponse {
|
|
52
|
+
status: MemoryStatus;
|
|
53
|
+
ranking: {
|
|
54
|
+
by: 'meaning' | 'term overlap';
|
|
55
|
+
model?: string;
|
|
56
|
+
reason?: string;
|
|
57
|
+
};
|
|
58
|
+
sees: string[];
|
|
59
|
+
query: {
|
|
60
|
+
text: string;
|
|
61
|
+
kind?: string;
|
|
62
|
+
limit: number;
|
|
63
|
+
hops: number;
|
|
64
|
+
nodes: number;
|
|
65
|
+
minScore?: number;
|
|
66
|
+
};
|
|
67
|
+
seeds: string[];
|
|
68
|
+
truncated: boolean;
|
|
69
|
+
nodes: MemoryRecallNode[];
|
|
70
|
+
edges: MemoryEdge[];
|
|
71
|
+
block: string;
|
|
72
|
+
}
|
|
73
|
+
export interface MemoryReportResponse {
|
|
74
|
+
html: string;
|
|
75
|
+
}
|
|
76
|
+
type EmbedderFactory = (config: ProjectConfig) => Embedder | undefined;
|
|
77
|
+
/** Test-only seam: replaces credential and model resolution for recall. */
|
|
78
|
+
export declare function setRecallEmbedderFactory(factory: EmbedderFactory | undefined): void;
|
|
79
|
+
export declare function memoryRouter(chats: ChatStore): Router;
|
|
80
|
+
export {};
|
|
81
|
+
//# sourceMappingURL=memory.d.ts.map
|