@fugood/buttress-server 2.25.0 → 2.25.1-beta.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 +238 -17
- package/config/function-samples/README.md +37 -0
- package/config/function-samples/_auth.ts +48 -0
- package/config/function-samples/host-info.ts +25 -0
- package/config/function-samples/summarize-text.ts +55 -0
- package/config/function-samples/text-to-speech.ts +51 -0
- package/config/function-samples/transcribe-media.ts +65 -0
- package/config/sample.toml +16 -0
- package/lib/cli-update.d.ts +1 -0
- package/lib/functions/auth.d.ts +28 -0
- package/lib/functions/config.d.ts +20 -0
- package/lib/functions/constants.d.ts +17 -0
- package/lib/functions/executor.d.ts +27 -0
- package/lib/functions/files.d.ts +29 -0
- package/lib/functions/index.d.ts +52 -0
- package/lib/functions/input.d.ts +11 -0
- package/lib/functions/libs.d.ts +10 -0
- package/lib/functions/loader.d.ts +48 -0
- package/lib/functions/mcp.d.ts +35 -0
- package/lib/functions/query.d.ts +27 -0
- package/lib/functions/registry.d.ts +35 -0
- package/lib/functions/scaffold.d.ts +18 -0
- package/lib/functions/status.d.ts +119 -0
- package/lib/functions/templates.d.ts +14 -0
- package/lib/functions/transpile.d.ts +23 -0
- package/lib/functions/types.d.ts +196 -0
- package/lib/functions/uploads.d.ts +42 -0
- package/lib/functions/watcher.d.ts +34 -0
- package/lib/index.d.ts +11 -3
- package/lib/index.mjs +347 -63
- package/lib/routes/functions.check.d.ts +13 -0
- package/lib/routes/functions.d.ts +17 -0
- package/lib/routes/generator-cache.d.ts +58 -0
- package/lib/routes/index.d.ts +1 -0
- package/lib/routes/llm-shared.d.ts +22 -19
- package/lib/routes/stt-shared.d.ts +34 -0
- package/lib/routes/tts-shared.d.ts +34 -0
- package/lib/services/create-onnx-init-context.d.ts +10 -0
- package/lib/services/onnx-stt.d.ts +1 -1
- package/lib/services/onnx-tts.d.ts +1 -1
- package/lib/types.d.ts +23 -0
- package/lib/utils/functionsAuthGuard.d.ts +12 -0
- package/lib/utils/update.d.ts +38 -0
- package/package.json +23 -3
- package/public/status.html +116 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { GlobalConfig } from '../types';
|
|
2
|
+
import type { FunctionsConfig } from './types';
|
|
3
|
+
export declare const DEFAULT_TIMEOUT_MS: number;
|
|
4
|
+
export type ResolveFunctionsOptions = {
|
|
5
|
+
/**
|
|
6
|
+
* Directory a relative `dir` is resolved against — the directory holding the
|
|
7
|
+
* `--config` file. Inline-TOML configs have no such directory, so callers
|
|
8
|
+
* pass `process.cwd()`.
|
|
9
|
+
*/
|
|
10
|
+
configDir?: string;
|
|
11
|
+
env?: Record<string, string | undefined>;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the `[functions]` TOML table (plus env overrides) into the shape the
|
|
15
|
+
* runtime uses. Returns null when the feature is not enabled.
|
|
16
|
+
*
|
|
17
|
+
* Enablement follows the same env-or-config idiom as the compatibility
|
|
18
|
+
* endpoints: `ENABLE_FUNCTIONS_ENDPOINT=1` or `[functions] enabled = true`.
|
|
19
|
+
*/
|
|
20
|
+
export declare const resolveFunctionsConfig: (globalConfig: GlobalConfig | undefined, { configDir, env }?: ResolveFunctionsOptions) => FunctionsConfig | null;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared constants for the local function system.
|
|
3
|
+
*
|
|
4
|
+
* Kept apart from `transpile.ts` so discovery and scaffolding can import them
|
|
5
|
+
* without pulling in the native transpiler.
|
|
6
|
+
*/
|
|
7
|
+
/** File extensions a local function (or one of its helper modules) may use. */
|
|
8
|
+
export declare const SUPPORTED_EXTENSIONS: string[];
|
|
9
|
+
/** What a function (file) may be called. Doubles as a path-safety guarantee. */
|
|
10
|
+
export declare const FUNCTION_NAME_PATTERN: RegExp;
|
|
11
|
+
/**
|
|
12
|
+
* Static segments of the functions API a function may not be named after:
|
|
13
|
+
* `POST /functions/mcp` (MCP), `GET /functions/files/*` (downloads) and
|
|
14
|
+
* `POST /functions/upload` (uploads) would shadow — or be shadowed by — a
|
|
15
|
+
* function of the same name.
|
|
16
|
+
*/
|
|
17
|
+
export declare const RESERVED_FUNCTION_NAMES: Set<string>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs a loaded local function with a deadline, a scratch directory, and the
|
|
3
|
+
* capabilities it is allowed to reach (spawn, buttress inference, helper libs).
|
|
4
|
+
*
|
|
5
|
+
* Cancellation is cooperative *plus* forceful: the call's AbortSignal is
|
|
6
|
+
* surfaced to the function (so `fetch` and the inference helpers unwind), and
|
|
7
|
+
* every process the call spawned is terminated. Code that blocks the event loop
|
|
8
|
+
* synchronously cannot be interrupted — function files are trusted, and vm has
|
|
9
|
+
* no way to preempt a running script.
|
|
10
|
+
*/
|
|
11
|
+
import type { FunctionEmit, FunctionRuntime, FunctionsConfig, LoadedFunction } from './types';
|
|
12
|
+
export declare class FunctionTimeoutError extends Error {
|
|
13
|
+
constructor(name: string, timeoutMs: number);
|
|
14
|
+
}
|
|
15
|
+
export declare class FunctionAbortError extends Error {
|
|
16
|
+
constructor(name: string);
|
|
17
|
+
}
|
|
18
|
+
export type ExecuteOptions = {
|
|
19
|
+
runtime: FunctionRuntime;
|
|
20
|
+
functionsConfig: FunctionsConfig;
|
|
21
|
+
/** Streams progress to the caller; ignored on non-streaming surfaces. */
|
|
22
|
+
emit?: FunctionEmit;
|
|
23
|
+
/** Aborts the call early (e.g. the HTTP client disconnected). */
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
callId?: string;
|
|
26
|
+
};
|
|
27
|
+
export declare const executeFunction: (fn: LoadedFunction, input: any, { runtime, functionsConfig, emit, signal, callId }: ExecuteOptions) => Promise<any>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path mapping for the function file-download surface.
|
|
3
|
+
*
|
|
4
|
+
* A call's scratch directory lives at `<temp_file_dir>/functions/<name>-<id>/`;
|
|
5
|
+
* `GET /functions/files/<name>-<id>/<file>` serves what a function wrote there.
|
|
6
|
+
* Both directions of the mapping live here, pure and unit-testable: URL →
|
|
7
|
+
* filesystem path (with strict containment, for the route) and filesystem
|
|
8
|
+
* path → URL (for `context.fileUrl`).
|
|
9
|
+
*/
|
|
10
|
+
export declare const FUNCTIONS_FILES_PREFIX = "/functions/files";
|
|
11
|
+
/**
|
|
12
|
+
* Resolve a raw wildcard path from `GET /functions/files/*` to an absolute
|
|
13
|
+
* path inside `tempRoot`. Null when the path is empty, escapes the root, or
|
|
14
|
+
* cannot be decoded — the route treats all of those as 404.
|
|
15
|
+
*/
|
|
16
|
+
export declare const resolveFunctionsFile: (tempRoot: string, rawPath: string) => string | null;
|
|
17
|
+
/**
|
|
18
|
+
* Build the download URL path for an absolute file inside `tempRoot`.
|
|
19
|
+
* Null when the file is outside it.
|
|
20
|
+
*/
|
|
21
|
+
export declare const functionsFileUrl: (tempRoot: string, absolutePath: string) => string | null;
|
|
22
|
+
/**
|
|
23
|
+
* Tame a client-supplied upload file name into something safe to place on
|
|
24
|
+
* disk. Every upload gets its own directory, so only the name itself needs
|
|
25
|
+
* care: no path segments, no control characters, no dot-prefixed (hidden /
|
|
26
|
+
* traversal-looking) names. The extension survives — ffmpeg and friends use
|
|
27
|
+
* it for format detection.
|
|
28
|
+
*/
|
|
29
|
+
export declare const sanitizeUploadFilename: (original: string) => string;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local function system: user-authored `.ts`/`.js` files in a configured
|
|
3
|
+
* directory, exposed as MCP tools and HTTP endpoints.
|
|
4
|
+
*
|
|
5
|
+
* `createFunctionsService` is the single entry point the server wires up; it
|
|
6
|
+
* owns discovery/reload (registry) and execution (executor), and prepares the
|
|
7
|
+
* directory for authoring on startup.
|
|
8
|
+
*/
|
|
9
|
+
import type { FunctionEmit, FunctionRuntime, FunctionSummary, FunctionsConfig, LoadedAuthFunction } from './types';
|
|
10
|
+
export { resolveFunctionsConfig } from './config';
|
|
11
|
+
export { FunctionNotFoundError } from './registry';
|
|
12
|
+
export { FunctionAbortError, FunctionTimeoutError } from './executor';
|
|
13
|
+
export { FunctionImportError } from './loader';
|
|
14
|
+
export { AUTH_BASENAME } from './auth';
|
|
15
|
+
export type * from './types';
|
|
16
|
+
export type CallOptions = {
|
|
17
|
+
runtime: FunctionRuntime;
|
|
18
|
+
emit?: FunctionEmit;
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
callId?: string;
|
|
21
|
+
/** Which API carried the call — recorded in the status history. */
|
|
22
|
+
surface?: 'http' | 'sse' | 'mcp';
|
|
23
|
+
};
|
|
24
|
+
export type FunctionsService = {
|
|
25
|
+
config: FunctionsConfig;
|
|
26
|
+
dir: string;
|
|
27
|
+
list: () => Promise<FunctionSummary[]>;
|
|
28
|
+
/** One function's summary; rejects when it is unknown or fails to load. */
|
|
29
|
+
describe: (name: string) => Promise<FunctionSummary>;
|
|
30
|
+
call: (name: string, input: any, options: CallOptions) => Promise<any>;
|
|
31
|
+
/**
|
|
32
|
+
* The operator's `_auth` function, or null when none exists. Rejects while
|
|
33
|
+
* an `_auth` file is present but broken — callers treat that as deny-all.
|
|
34
|
+
*/
|
|
35
|
+
getCustomAuth: () => Promise<LoadedAuthFunction | null>;
|
|
36
|
+
/**
|
|
37
|
+
* Live capability summary for `serverInfo`. Mutated in place on every
|
|
38
|
+
* `list()` so the announced count follows the directory instead of freezing
|
|
39
|
+
* at whatever was on disk during startup.
|
|
40
|
+
*/
|
|
41
|
+
stats: {
|
|
42
|
+
enabled: true;
|
|
43
|
+
count: number;
|
|
44
|
+
};
|
|
45
|
+
/** Stop the hot-reload watcher, when one is running. Safe to call always. */
|
|
46
|
+
dispose: () => void;
|
|
47
|
+
};
|
|
48
|
+
export type CreateFunctionsServiceOptions = {
|
|
49
|
+
/** `server.temp_file_dir`; per-call scratch space lives under it. */
|
|
50
|
+
tempFileDir: string;
|
|
51
|
+
};
|
|
52
|
+
export declare const createFunctionsService: (config: FunctionsConfig, { tempFileDir }: CreateFunctionsServiceOptions) => Promise<FunctionsService>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared record contract behind the transport input parsers.
|
|
3
|
+
*
|
|
4
|
+
* `parseQueryInput` and `stageMultipartInput` both fill an input object with
|
|
5
|
+
* externally chosen field names, so plain `input[field] = value` is wrong: a
|
|
6
|
+
* field literally named `__proto__` would hit `Object.prototype`'s legacy
|
|
7
|
+
* setter instead of becoming an input value — silently dropped for strings,
|
|
8
|
+
* and a swapped prototype for object-like values.
|
|
9
|
+
*/
|
|
10
|
+
/** Set an externally named field as a plain own data property. */
|
|
11
|
+
export declare const setInputField: (input: Record<string, any>, field: string, value: any) => void;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper libraries handed to every local function as `context.libs`.
|
|
3
|
+
*
|
|
4
|
+
* These are the node-safe members of the BRICKS data-calculation sandbox bag,
|
|
5
|
+
* so a script author moving between device-side calc scripts and server-side
|
|
6
|
+
* local functions finds the same names. They are stateless singletons — the
|
|
7
|
+
* same instances are shared by every call.
|
|
8
|
+
*/
|
|
9
|
+
export declare const libs: Record<string, any>;
|
|
10
|
+
export declare const LIB_NAMES: string[];
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evaluates transpiled local function modules inside a `node:vm` context.
|
|
3
|
+
*
|
|
4
|
+
* Each function gets its own vm context, so its module-level state is private
|
|
5
|
+
* and a reload simply throws the whole context away. The context is a *clean*
|
|
6
|
+
* global: no `process`, no `require`, no ambient host globals beyond the
|
|
7
|
+
* curated set in `createSandboxGlobals`. That is a clarity boundary, not a
|
|
8
|
+
* security boundary — function files are trusted (an operator puts them on the
|
|
9
|
+
* server's disk, like the TOML config), and they are handed `spawn` anyway.
|
|
10
|
+
*
|
|
11
|
+
* Values crossing back from the vm belong to another realm: `Array.isArray`
|
|
12
|
+
* and `JSON.stringify` work, but `instanceof` against host constructors does
|
|
13
|
+
* not. Never `instanceof`-check a function's return value.
|
|
14
|
+
*/
|
|
15
|
+
export declare class FunctionImportError extends Error {
|
|
16
|
+
constructor(message: string);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Globals visible to function code. Everything here is a host object, so
|
|
20
|
+
* `Buffer.isBuffer` and friends still work on values a function creates.
|
|
21
|
+
*/
|
|
22
|
+
export declare const createSandboxGlobals: (extra?: Record<string, any>) => Record<string, any>;
|
|
23
|
+
export type ResolvedSpecifier = {
|
|
24
|
+
kind: 'builtin';
|
|
25
|
+
id: string;
|
|
26
|
+
} | {
|
|
27
|
+
kind: 'file';
|
|
28
|
+
path: string;
|
|
29
|
+
};
|
|
30
|
+
export declare const resolveSpecifier: (specifier: string, fromFile: string, rootDir: string) => Promise<ResolvedSpecifier>;
|
|
31
|
+
export type ModuleGraph = {
|
|
32
|
+
exports: Record<string, any>;
|
|
33
|
+
/** Every source file in the graph → its mtimeMs at load time. */
|
|
34
|
+
files: Map<string, number>;
|
|
35
|
+
};
|
|
36
|
+
export type LoadOptions = {
|
|
37
|
+
rootDir: string;
|
|
38
|
+
globals?: Record<string, any>;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Transpile and evaluate `entryFile` plus everything it imports, returning the
|
|
42
|
+
* entry's export namespace.
|
|
43
|
+
*
|
|
44
|
+
* Note: a module body that blocks synchronously (e.g. `while (true) {}` at the
|
|
45
|
+
* top level) wedges the event loop — vm has no way to interrupt it. Callers
|
|
46
|
+
* run this inside the per-call deadline so an *async* hang is still bounded.
|
|
47
|
+
*/
|
|
48
|
+
export declare const loadModuleGraph: (entryFile: string, { rootDir, globals }: LoadOptions) => Promise<ModuleGraph>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP surface for local functions.
|
|
3
|
+
*
|
|
4
|
+
* Served from the same Elysia app (and therefore the same port and auth guard)
|
|
5
|
+
* as the HTTP endpoints. The SDK's own Streamable HTTP transport needs Node
|
|
6
|
+
* `IncomingMessage`/`ServerResponse` objects, which Elysia does not expose and
|
|
7
|
+
* which do not exist under Bun at all — so this module pairs the SDK's protocol
|
|
8
|
+
* implementation with a minimal stateless transport that just moves one
|
|
9
|
+
* JSON-RPC message in and its response out.
|
|
10
|
+
*
|
|
11
|
+
* Stateless means a fresh `Server` per POST: no `mcp-session-id` handshake, and
|
|
12
|
+
* `tools/list` always reflects the functions on disk right now. Clients that
|
|
13
|
+
* try to open the optional GET event stream get a 405, which the spec (and the
|
|
14
|
+
* SDK's client transport) treat as "this server has no server-initiated
|
|
15
|
+
* stream".
|
|
16
|
+
*/
|
|
17
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
18
|
+
import type { FunctionRuntime } from './types';
|
|
19
|
+
import type { FunctionsService } from './index';
|
|
20
|
+
export declare const serializeToolResult: (result: unknown) => string;
|
|
21
|
+
export type McpServerOptions = {
|
|
22
|
+
functions: FunctionsService;
|
|
23
|
+
runtime: FunctionRuntime;
|
|
24
|
+
version: string;
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
};
|
|
27
|
+
export declare const createFunctionsMcpServer: ({ functions, runtime, version, signal, }: McpServerOptions) => Server;
|
|
28
|
+
type JsonRpcMessage = Record<string, any>;
|
|
29
|
+
/**
|
|
30
|
+
* Run one JSON-RPC payload (single message or batch) against a fresh server.
|
|
31
|
+
*
|
|
32
|
+
* Returns null when nothing needs to be sent back — the caller answers 202.
|
|
33
|
+
*/
|
|
34
|
+
export declare const handleMcpPayload: (options: McpServerOptions, payload: JsonRpcMessage | JsonRpcMessage[]) => Promise<JsonRpcMessage | JsonRpcMessage[] | null>;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query-string input for `GET /functions/<name>`.
|
|
3
|
+
*
|
|
4
|
+
* The sibling of `stageMultipartInput`: same idea, other transport. An optional
|
|
5
|
+
* `input` parameter carries a JSON object for exact types, and every other
|
|
6
|
+
* parameter overlays it — so `?input={"n":3}¬e=hi` and a multipart body with
|
|
7
|
+
* the same two fields produce the same input object.
|
|
8
|
+
*
|
|
9
|
+
* Query values are always strings, so a bare `?n=3` would hand a handler `"3"`.
|
|
10
|
+
* The function's declared `meta.parameters` schema is the only thing that says
|
|
11
|
+
* otherwise, so it doubles as the coercion table: declared numbers, booleans,
|
|
12
|
+
* arrays and objects are converted, and anything undeclared stays a string.
|
|
13
|
+
* Coercion never rejects — a value that does not fit its declared type is
|
|
14
|
+
* passed through verbatim rather than turned into `NaN` (the handler, which
|
|
15
|
+
* owns validation, sees what the caller actually sent).
|
|
16
|
+
*/
|
|
17
|
+
/** A caller mistake in the query shape — reported as 400, not 500. */
|
|
18
|
+
export declare class QueryInputError extends Error {
|
|
19
|
+
constructor(message: string);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Turn a query string into a function input object.
|
|
23
|
+
*
|
|
24
|
+
* `parameters` is the function's declared JSON Schema, when it has one; without
|
|
25
|
+
* it every value stays a string.
|
|
26
|
+
*/
|
|
27
|
+
export declare const parseQueryInput: (search: URLSearchParams, parameters?: Record<string, any>) => Record<string, any>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discovers local function files and keeps their loaded modules fresh.
|
|
3
|
+
*
|
|
4
|
+
* Discovery is a flat scan of the functions directory: every supported
|
|
5
|
+
* top-level file is one function, named after the file. Subdirectories hold
|
|
6
|
+
* helper modules — importable by functions, never exposed as tools.
|
|
7
|
+
*
|
|
8
|
+
* Reload is lazy and mtime-driven. Before handing back a loaded function we
|
|
9
|
+
* stat every file in its module graph; if any changed, the module is
|
|
10
|
+
* re-transpiled into a fresh vm context. No watchers, identical behavior on
|
|
11
|
+
* every platform, and an edit is picked up by the very next call.
|
|
12
|
+
*/
|
|
13
|
+
import type { FunctionSummary, FunctionsConfig, LoadedFunction } from './types';
|
|
14
|
+
type Entry = {
|
|
15
|
+
name: string;
|
|
16
|
+
file: string;
|
|
17
|
+
loaded: LoadedFunction | null;
|
|
18
|
+
loading: Promise<LoadedFunction> | null;
|
|
19
|
+
/** Signature of the last failure we logged, so a broken file logs once. */
|
|
20
|
+
reportedError: string | null;
|
|
21
|
+
};
|
|
22
|
+
export declare class FunctionNotFoundError extends Error {
|
|
23
|
+
constructor(name: string);
|
|
24
|
+
}
|
|
25
|
+
export type FunctionsRegistry = ReturnType<typeof createFunctionsRegistry>;
|
|
26
|
+
export declare const createFunctionsRegistry: (config: FunctionsConfig) => {
|
|
27
|
+
dir: string;
|
|
28
|
+
config: FunctionsConfig;
|
|
29
|
+
scan: () => Promise<void>;
|
|
30
|
+
get: (name: string) => Promise<LoadedFunction>;
|
|
31
|
+
describe: (name: string) => Promise<FunctionSummary>;
|
|
32
|
+
list: () => Promise<FunctionSummary[]>;
|
|
33
|
+
entries: Map<string, Entry>;
|
|
34
|
+
};
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** One day; function scratch dirs older than this are swept at startup. */
|
|
2
|
+
export declare const TEMP_DIR_MAX_AGE_MS: number;
|
|
3
|
+
/**
|
|
4
|
+
* Ensure the functions directory exists and carries current editor support.
|
|
5
|
+
*
|
|
6
|
+
* The ambient types are always refreshed; the tsconfig and example are only
|
|
7
|
+
* seeded into a directory that has no functions yet.
|
|
8
|
+
*/
|
|
9
|
+
export declare const scaffoldFunctionsDir: (dir: string) => Promise<{
|
|
10
|
+
seeded: boolean;
|
|
11
|
+
}>;
|
|
12
|
+
/**
|
|
13
|
+
* Remove stale per-call scratch directories.
|
|
14
|
+
*
|
|
15
|
+
* A call's `tempDir` outlives the request on purpose (a co-located agent may
|
|
16
|
+
* read the file the function produced), so cleanup is time-based instead.
|
|
17
|
+
*/
|
|
18
|
+
export declare const sweepFunctionTempDirs: (tempRoot: string, maxAgeMs?: number) => Promise<number>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory activity log for the functions surface, shown on `/status`.
|
|
3
|
+
*
|
|
4
|
+
* Counterpart to the backend-core status trackers (model loads, completions):
|
|
5
|
+
* counters since startup plus a bounded newest-first history per category.
|
|
6
|
+
* Everything here is observable metadata — names, paths, sizes, outcome codes.
|
|
7
|
+
* Credentials (tokens, API keys) are never recorded.
|
|
8
|
+
*
|
|
9
|
+
* A module-scoped singleton, like the backend trackers: the guard, the routes
|
|
10
|
+
* and the service all record into the same instance, and the status route
|
|
11
|
+
* snapshots it.
|
|
12
|
+
*/
|
|
13
|
+
export type FunctionCallRecord = {
|
|
14
|
+
timestamp: string;
|
|
15
|
+
name: string;
|
|
16
|
+
surface: 'http' | 'sse' | 'mcp';
|
|
17
|
+
durationMs: number;
|
|
18
|
+
success: boolean;
|
|
19
|
+
error?: string;
|
|
20
|
+
};
|
|
21
|
+
export type FunctionUploadRecord = {
|
|
22
|
+
timestamp: string;
|
|
23
|
+
/** 'upload' = POST /functions/upload, 'call' = multipart function call. */
|
|
24
|
+
source: 'upload' | 'call';
|
|
25
|
+
name: string;
|
|
26
|
+
size: number;
|
|
27
|
+
success: boolean;
|
|
28
|
+
error?: string;
|
|
29
|
+
};
|
|
30
|
+
export type FunctionDownloadRecord = {
|
|
31
|
+
timestamp: string;
|
|
32
|
+
path: string;
|
|
33
|
+
size?: number;
|
|
34
|
+
success: boolean;
|
|
35
|
+
};
|
|
36
|
+
export type FunctionAuthRecord = {
|
|
37
|
+
timestamp: string;
|
|
38
|
+
method: string;
|
|
39
|
+
path: string;
|
|
40
|
+
/**
|
|
41
|
+
* 'workspace' when no custom auth is active, else the `_auth` mode. Absent
|
|
42
|
+
* for decisions made before any auth regime applied (origin block, broken
|
|
43
|
+
* `_auth` file).
|
|
44
|
+
*/
|
|
45
|
+
mode?: 'workspace' | 'both' | 'override';
|
|
46
|
+
/** 'allowed', or the denial error code. */
|
|
47
|
+
outcome: string;
|
|
48
|
+
subject?: string;
|
|
49
|
+
};
|
|
50
|
+
export declare const createFunctionsStatusTracker: (maxHistory?: number) => {
|
|
51
|
+
recordCall(record: Omit<FunctionCallRecord, 'timestamp'>): void;
|
|
52
|
+
recordUpload(record: Omit<FunctionUploadRecord, 'timestamp'>): void;
|
|
53
|
+
recordDownload(record: Omit<FunctionDownloadRecord, 'timestamp'>): void;
|
|
54
|
+
recordAuth(record: Omit<FunctionAuthRecord, 'timestamp'>): void;
|
|
55
|
+
snapshot(): {
|
|
56
|
+
counters: {
|
|
57
|
+
calls: {
|
|
58
|
+
total: number;
|
|
59
|
+
failed: number;
|
|
60
|
+
};
|
|
61
|
+
uploads: {
|
|
62
|
+
total: number;
|
|
63
|
+
failed: number;
|
|
64
|
+
bytes: number;
|
|
65
|
+
};
|
|
66
|
+
downloads: {
|
|
67
|
+
total: number;
|
|
68
|
+
missed: number;
|
|
69
|
+
bytes: number;
|
|
70
|
+
};
|
|
71
|
+
auth: {
|
|
72
|
+
total: number;
|
|
73
|
+
denied: number;
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
history: {
|
|
77
|
+
calls: FunctionCallRecord[];
|
|
78
|
+
uploads: FunctionUploadRecord[];
|
|
79
|
+
downloads: FunctionDownloadRecord[];
|
|
80
|
+
auth: FunctionAuthRecord[];
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
export type FunctionsStatusTracker = ReturnType<typeof createFunctionsStatusTracker>;
|
|
85
|
+
/** The shared instance every functions surface records into. */
|
|
86
|
+
export declare const functionsStatusTracker: {
|
|
87
|
+
recordCall(record: Omit<FunctionCallRecord, 'timestamp'>): void;
|
|
88
|
+
recordUpload(record: Omit<FunctionUploadRecord, 'timestamp'>): void;
|
|
89
|
+
recordDownload(record: Omit<FunctionDownloadRecord, 'timestamp'>): void;
|
|
90
|
+
recordAuth(record: Omit<FunctionAuthRecord, 'timestamp'>): void;
|
|
91
|
+
snapshot(): {
|
|
92
|
+
counters: {
|
|
93
|
+
calls: {
|
|
94
|
+
total: number;
|
|
95
|
+
failed: number;
|
|
96
|
+
};
|
|
97
|
+
uploads: {
|
|
98
|
+
total: number;
|
|
99
|
+
failed: number;
|
|
100
|
+
bytes: number;
|
|
101
|
+
};
|
|
102
|
+
downloads: {
|
|
103
|
+
total: number;
|
|
104
|
+
missed: number;
|
|
105
|
+
bytes: number;
|
|
106
|
+
};
|
|
107
|
+
auth: {
|
|
108
|
+
total: number;
|
|
109
|
+
denied: number;
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
history: {
|
|
113
|
+
calls: FunctionCallRecord[];
|
|
114
|
+
uploads: FunctionUploadRecord[];
|
|
115
|
+
downloads: FunctionDownloadRecord[];
|
|
116
|
+
auth: FunctionAuthRecord[];
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Files scaffolded into the functions directory.
|
|
3
|
+
*
|
|
4
|
+
* `buttress-functions.d.ts` is managed: it is rewritten on every start so the
|
|
5
|
+
* ambient types always match the running server. The tsconfig and the example
|
|
6
|
+
* are written only into a directory that holds no functions yet, so a deleted
|
|
7
|
+
* example never comes back.
|
|
8
|
+
*/
|
|
9
|
+
export declare const TYPES_FILE_NAME = "buttress-functions.d.ts";
|
|
10
|
+
export declare const TSCONFIG_FILE_NAME = "tsconfig.json";
|
|
11
|
+
export declare const EXAMPLE_FILE_NAME = "_example.ts";
|
|
12
|
+
export declare const TYPES_TEMPLATE = "// Generated by @fugood/buttress-server \u2014 do not edit.\n// Rewritten on every server start to match the running version.\n//\n// Local functions are EXPERIMENTAL: this contract may change between\n// releases \u2014 after a server upgrade, re-read this file for the current shape.\n\n/** Result of `context.spawn(...)`. */\ntype ButtressSpawnResult = {\n /** Exit code, or null when the process was killed by a signal. */\n code: number | null\n signal: NodeJS.Signals | null\n stdout: string | Buffer\n stderr: string | Buffer\n /** True when output hit `maxBuffer` and capture stopped early. */\n truncated: boolean\n}\n\ntype ButtressSpawnOptions = {\n cwd?: string\n /** Merged over the server's own environment. */\n env?: Record<string, string | undefined>\n /** Written to stdin, which is then closed. */\n input?: string | Uint8Array\n /** 'utf8' (default) yields strings; 'buffer' yields Buffers. */\n encoding?: 'utf8' | 'buffer'\n /** Per-stream capture cap in bytes (default 8MB). */\n maxBuffer?: number\n onStdout?: (chunk: Buffer) => void\n onStderr?: (chunk: Buffer) => void\n}\n\ntype ButtressCompletionResult = {\n content: string\n reasoning_content?: string\n tool_calls?: any[]\n interrupted?: boolean\n usage: { prompt_tokens: number; completion_tokens: number; total_tokens: number }\n}\n\ntype ButtressFunctionContext = {\n /**\n * Run a child process. Resolves once it exits \u2014 a non-zero `code` is a\n * normal resolution, so check it yourself. Rejects only when the process\n * cannot be started. Every process a call spawns is killed when the call\n * ends or times out.\n */\n spawn: (\n command: string,\n args?: string[],\n options?: ButtressSpawnOptions,\n ) => Promise<ButtressSpawnResult>\n\n buttress: {\n /**\n * Run a chat completion on this server's LLM generator. `messages` are\n * rendered with the model's own chat template, and thinking is off unless\n * you pass `enable_thinking: true` (which fills `reasoning_content`).\n */\n completion: (options: {\n /** Configured `[[generators]]` model; defaults to the first one. */\n model?: string\n messages?: { role: string; content: any }[]\n prompt?: string\n /** Cap on generated tokens. Unbounded when omitted. */\n max_tokens?: number\n temperature?: number\n /** Emit the model's reasoning into `reasoning_content` (default false). */\n enable_thinking?: boolean\n /** Called for every streamed token event. */\n onToken?: (event: { token?: string; content?: string; [key: string]: any }) => void\n /** Any other backend sampling option (`top_p`, `stop`, `tools`, \u2026). */\n [param: string]: any\n }) => Promise<ButtressCompletionResult>\n\n /** Transcribe audio with this server's STT generator. */\n transcribe: (options: {\n /** Must match a configured STT model (`repo_id` or `repo_id:filename`). */\n model?: string\n filePath?: string\n audioData?: Uint8Array | Buffer\n options?: Record<string, any>\n }) => Promise<any>\n\n /**\n * Synthesize speech with this server's TTS generator (onnx-tts). The WAV\n * is written into `tempDir`; return `fileUrl(path)` to let callers\n * download it.\n */\n synthesize: (options: {\n /** Must match a configured TTS model when given. */\n model?: string\n text: string\n /** Backend options; `options.speaker` picks a registered voice. */\n options?: Record<string, any>\n }) => Promise<{ path: string; sampling_rate: number; channels: number }>\n }\n\n fetch: typeof fetch\n /** Server-side log, prefixed with the function name. */\n log: (...args: unknown[]) => void\n /** Emit a progress event. Delivered only to SSE callers; a no-op otherwise. */\n emit: (event: string, data?: unknown) => void\n /** Aborted when the call times out or the caller disconnects. */\n signal: AbortSignal\n env: Record<string, string | undefined>\n /** The `[functions.config]` table from the server config. */\n config: Record<string, any>\n /** Per-call scratch directory, created on first access. */\n tempDir: string\n /**\n * Download URL path (`/functions/files/...`) for a file inside `tempDir`;\n * relative input resolves against it. Callers fetch the URL with the same\n * auth as any function call. Scratch dirs are swept after ~24h.\n */\n fileUrl: (target: string) => string\n /** Absolute path of this functions directory. */\n dir: string\n /** Helper libraries: _, lodash, moment, math, mathjs, voca, chroma, json5, qs, bytes, ms, nanoid, md5. */\n libs: Record<string, any>\n}\n\ntype ButtressFunctionMeta = {\n /** Shown to MCP clients in `tools/list`. */\n description?: string\n /** JSON Schema for the input object, passed to MCP verbatim. */\n parameters?: Record<string, any>\n /** Deadline for this function: ms, or a duration string like \"10m\". */\n timeout?: number | string\n}\n\n// --- Custom auth ------------------------------------------------------------\n// Drop an `_auth.ts` into this directory to gate the /functions endpoints\n// with your own logic. It exports the same shape as a function file:\n//\n// export const meta: ButtressAuthMeta = { mode: 'both' }\n// export default async function (\n// request: ButtressAuthRequest,\n// context: ButtressAuthContext,\n// ): Promise<ButtressAuthResult> { ... }\n//\n// While an `_auth` file exists but fails to load, every call is rejected.\n\ntype ButtressAuthMeta = {\n /**\n * 'both' (default): runs after the built-in workspace auth passes, as an\n * extra gate. 'override': replaces workspace auth \u2014 this function is the\n * only authority (a presented workspace token is still verified into\n * `request.workspaceAuth` so you can choose to honor it).\n */\n mode?: 'override' | 'both'\n}\n\ntype ButtressAuthRequest = {\n method: string\n path: string\n /** Function name for `GET`/`POST /functions/<name>`; undefined for list and MCP. */\n name?: string\n headers: Record<string, string | undefined>\n query: Record<string, string | undefined>\n /** Raw bearer token (Authorization header or `?token=`), if any. */\n token: string | null\n workspaceAuth: {\n /** Whether this server is bound to a workspace. */\n bound: boolean\n /** Whether the caller presented a valid workspace access token. */\n authenticated: boolean\n identity: {\n workspaceId: string\n subjectType: 'ws' | 'dev'\n subjectId: string\n jti?: string\n exp: number\n } | null\n }\n}\n\ntype ButtressAuthContext = {\n /** Server-side log, prefixed with \"_auth\". */\n log: (...args: unknown[]) => void\n fetch: typeof fetch\n env: Record<string, string | undefined>\n /** The `[functions.config]` table, same as `context.config` in functions. */\n config: Record<string, any>\n /** Absolute path of this functions directory. */\n dir: string\n /** Same helper libraries functions get. */\n libs: Record<string, any>\n}\n\n/** Only `true` (or `{ ok: true }`) allows the request; anything else denies. */\ntype ButtressAuthResult =\n | boolean\n | {\n ok: boolean\n /** Response status for a denial, 400-499 (default 403). */\n status?: number\n /** Message returned to the caller on denial. */\n error?: string\n }\n";
|
|
13
|
+
export declare const TSCONFIG_TEMPLATE = "{\n // Editor support for Buttress local functions.\n // Install @types/node here for typings on \"node:*\" imports.\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Bundler\",\n \"lib\": [\"ES2023\"],\n \"strict\": true,\n \"noEmit\": true,\n \"allowJs\": true,\n \"skipLibCheck\": true\n },\n \"include\": [\"**/*.ts\", \"**/*.js\"]\n}\n";
|
|
14
|
+
export declare const EXAMPLE_TEMPLATE = "// Example Buttress local function.\n//\n// Rename (or copy) this file to expose it: files starting with \"_\" are\n// ignored. The tool name is the file name \u2014 \"video-duration.ts\" becomes the\n// tool \"video-duration\", callable over MCP, at POST /functions/video-duration\n// with a JSON body, and at GET /functions/video-duration?path=... with the\n// input in the query string.\n\nexport const meta: ButtressFunctionMeta = {\n description: 'Report the duration of a video file using ffprobe',\n parameters: {\n type: 'object',\n properties: {\n path: { type: 'string', description: 'Absolute path to a video file' },\n },\n required: ['path'],\n },\n timeout: '2m',\n}\n\nexport default async function (\n { path }: { path: string },\n context: ButtressFunctionContext,\n): Promise<{ seconds: number }> {\n const { code, stdout, stderr } = await context.spawn('ffprobe', [\n '-v',\n 'error',\n '-show_entries',\n 'format=duration',\n '-of',\n 'default=noprint_wrappers=1:nokey=1',\n path,\n ])\n\n if (code !== 0) throw new Error(`ffprobe failed (${code}): ${stderr}`)\n\n return { seconds: Number(String(stdout).trim()) }\n}\n";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime transpilation of user-authored local function files.
|
|
3
|
+
*
|
|
4
|
+
* Two passes, both synchronous native (napi) calls:
|
|
5
|
+
* 1. `transformSync` strips TypeScript types (no-op for plain JS).
|
|
6
|
+
* 2. `moduleRunnerTransformSync` rewrites ESM into a flat body that reads and
|
|
7
|
+
* writes `__vite_ssr_*` bindings, which `vm.runInContext` can evaluate on
|
|
8
|
+
* plain Node. `vm.SourceTextModule` (real ESM in a vm) is unavailable on
|
|
9
|
+
* Node without `--experimental-vm-modules`, and `bin/bricks-buttress` runs
|
|
10
|
+
* `node lib/index.mjs` whenever Bun is absent — so this form is the only
|
|
11
|
+
* one that behaves identically on both runtimes.
|
|
12
|
+
*
|
|
13
|
+
* The `moduleRunnerTransformSync` output contract (Vite's module runner) is
|
|
14
|
+
* implemented by `loader.ts`. Keep both files in sync; this module is the only
|
|
15
|
+
* place that touches oxc so swapping transpilers stays a one-file change.
|
|
16
|
+
*/
|
|
17
|
+
export type TranspileResult = {
|
|
18
|
+
code: string;
|
|
19
|
+
/** Static import specifiers, as written in the source. */
|
|
20
|
+
deps: string[];
|
|
21
|
+
dynamicDeps: string[];
|
|
22
|
+
};
|
|
23
|
+
export declare const transpileModule: (file: string, source: string) => TranspileResult;
|