@fugood/buttress-server 2.25.0-beta.9 → 2.25.1-beta.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 +506 -35
- 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 +25 -0
- package/lib/autodiscover/index.d.ts +20 -0
- package/lib/autodiscover/sign.d.ts +10 -0
- package/lib/autodiscover/types.d.ts +32 -0
- package/lib/autodiscover/udp.d.ts +22 -0
- package/lib/cli.d.ts +3 -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 +50 -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/registry.d.ts +34 -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 +36 -0
- package/lib/index.mjs +1539 -64
- package/lib/package.d.ts +7 -0
- package/lib/routes/anthropic-messages.d.ts +55 -0
- package/lib/routes/file.d.ts +10 -0
- package/lib/routes/functions.check.d.ts +13 -0
- package/lib/routes/functions.d.ts +16 -0
- package/lib/routes/generator-cache.d.ts +42 -0
- package/lib/routes/index.d.ts +6 -0
- package/lib/routes/info.check.d.ts +1 -0
- package/lib/routes/info.d.ts +4 -0
- package/lib/routes/llm-shared.d.ts +57 -0
- package/lib/routes/openai-compat.d.ts +17 -0
- package/lib/routes/status.d.ts +4 -0
- package/lib/routes/stt-shared.d.ts +36 -0
- package/lib/routes/tts-shared.d.ts +36 -0
- package/lib/services/common.d.ts +29 -0
- package/lib/services/create-llm-service.d.ts +24 -0
- package/lib/services/create-onnx-init-context.d.ts +10 -0
- package/lib/services/ggml-llm.d.ts +5 -0
- package/lib/services/ggml-stt.d.ts +26 -0
- package/lib/services/index.d.ts +39 -0
- package/lib/services/mlx-llm.d.ts +5 -0
- package/lib/services/onnx-stt.d.ts +77 -0
- package/lib/services/onnx-tts.d.ts +48 -0
- package/lib/types.d.ts +181 -0
- package/lib/utils/SessionFileManager.d.ts +16 -0
- package/lib/utils/buttressAuth.d.ts +25 -0
- package/lib/utils/config.d.ts +21 -0
- package/lib/utils/functionsAuthGuard.d.ts +12 -0
- package/lib/utils/httpAuthGuard.d.ts +1 -0
- package/lib/utils/net.d.ts +6 -0
- package/lib/utils/router.d.ts +2 -0
- package/lib/utils/serialize.d.ts +2 -0
- package/lib/utils/serverCaps.d.ts +4 -0
- package/lib/utils/sessionGuard.d.ts +33 -0
- package/lib/utils/test-caps.d.ts +61 -0
- package/lib/utils/workspaceState.d.ts +21 -0
- package/package.json +21 -8
- package/public/status.html +278 -1
- package/lib/chunk-C8PTHxhX.mjs +0 -2
- package/lib/index.d.mts +0 -370
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Custom auth for the /functions endpoints.
|
|
2
|
+
//
|
|
3
|
+
// Unlike other "_"-prefixed files, "_auth" is special: dropping it into the
|
|
4
|
+
// functions directory activates it. `meta.mode` picks how it composes with
|
|
5
|
+
// the built-in workspace auth:
|
|
6
|
+
//
|
|
7
|
+
// - 'both' (default): runs after workspace auth passes — an extra gate that
|
|
8
|
+
// can only narrow access (e.g. per-function allow-lists).
|
|
9
|
+
// - 'override': fully replaces workspace auth — this function alone decides.
|
|
10
|
+
// A presented workspace token is still verified into
|
|
11
|
+
// `request.workspaceAuth` so the function can choose to honor it.
|
|
12
|
+
//
|
|
13
|
+
// This sample uses 'override' to keep workspace tokens working while ALSO
|
|
14
|
+
// accepting static API keys for callers that have none. Configure the keys in
|
|
15
|
+
// the server TOML:
|
|
16
|
+
//
|
|
17
|
+
// [functions.config]
|
|
18
|
+
// api_keys = ["replace-with-a-long-random-string"]
|
|
19
|
+
//
|
|
20
|
+
// Callers then send `X-Api-Key: <key>` (or `Authorization: Bearer <key>`).
|
|
21
|
+
// With no api_keys configured it denies everything except valid workspace
|
|
22
|
+
// tokens — it never fails open.
|
|
23
|
+
|
|
24
|
+
export const meta: ButtressAuthMeta = {
|
|
25
|
+
mode: 'override',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default async function authorize(
|
|
29
|
+
request: ButtressAuthRequest,
|
|
30
|
+
context: ButtressAuthContext,
|
|
31
|
+
): Promise<ButtressAuthResult> {
|
|
32
|
+
// A valid workspace access token keeps working like before.
|
|
33
|
+
if (request.workspaceAuth.authenticated) return true
|
|
34
|
+
|
|
35
|
+
const keys = context.config.api_keys
|
|
36
|
+
if (!Array.isArray(keys) || keys.length === 0) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
error: 'No workspace token and no [functions.config] api_keys configured',
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Headers reach this function lower-cased.
|
|
44
|
+
const presented = request.headers['x-api-key'] || request.token
|
|
45
|
+
if (typeof presented === 'string' && keys.includes(presented)) return true
|
|
46
|
+
|
|
47
|
+
return { ok: false, status: 401, error: 'Invalid or missing API key' }
|
|
48
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Report facts about the machine this server runs on.
|
|
2
|
+
//
|
|
3
|
+
// A good first function to try: it needs no external tools, no configured
|
|
4
|
+
// generators and no input.
|
|
5
|
+
//
|
|
6
|
+
// curl -X POST http://<host>:<port>/functions/host-info
|
|
7
|
+
|
|
8
|
+
import os from 'node:os'
|
|
9
|
+
|
|
10
|
+
export const meta: ButtressFunctionMeta = {
|
|
11
|
+
description: 'Report hostname, OS, CPU, memory and uptime of the Buttress host',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default async function (_input: unknown, context: ButtressFunctionContext) {
|
|
15
|
+
const { bytes, ms } = context.libs
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
hostname: os.hostname(),
|
|
19
|
+
platform: `${os.platform()} ${os.release()} (${os.arch()})`,
|
|
20
|
+
cpus: os.cpus().length,
|
|
21
|
+
memory: { total: bytes(os.totalmem()), free: bytes(os.freemem()) },
|
|
22
|
+
uptime: ms(Math.round(os.uptime()) * 1000, { long: true }),
|
|
23
|
+
load: os.loadavg().map((value) => Number(value.toFixed(2))),
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Summarize text with this server's own LLM generator — no API keys, the
|
|
2
|
+
// model already configured under [[generators]] does the work in-process.
|
|
3
|
+
//
|
|
4
|
+
// Needs at least one LLM generator (e.g. ggml-llm) in the server config.
|
|
5
|
+
//
|
|
6
|
+
// curl -X POST http://<host>:<port>/functions/summarize-text \
|
|
7
|
+
// -H 'Content-Type: application/json' \
|
|
8
|
+
// -d '{"text": "…", "style": "bullets"}'
|
|
9
|
+
|
|
10
|
+
export const meta: ButtressFunctionMeta = {
|
|
11
|
+
description: 'Summarize a piece of text with the local LLM',
|
|
12
|
+
parameters: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
text: { type: 'string', description: 'The text to summarize' },
|
|
16
|
+
style: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
enum: ['bullets', 'paragraph'],
|
|
19
|
+
default: 'bullets',
|
|
20
|
+
description: 'Shape of the summary',
|
|
21
|
+
},
|
|
22
|
+
model: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
description: 'Optional model override (repo_id of a configured generator)',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
required: ['text'],
|
|
28
|
+
},
|
|
29
|
+
timeout: '5m',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type Input = { text: string; style?: 'bullets' | 'paragraph'; model?: string }
|
|
33
|
+
|
|
34
|
+
export default async function (
|
|
35
|
+
{ text, style = 'bullets', model }: Input,
|
|
36
|
+
context: ButtressFunctionContext,
|
|
37
|
+
) {
|
|
38
|
+
const target = style === 'paragraph' ? 'one short paragraph' : '3-5 concise bullet points'
|
|
39
|
+
|
|
40
|
+
const { content, usage } = await context.buttress.completion({
|
|
41
|
+
model,
|
|
42
|
+
// A summary is bounded work; without a cap a model that starts repeating
|
|
43
|
+
// itself runs to the context limit before the call's deadline stops it.
|
|
44
|
+
max_tokens: 512,
|
|
45
|
+
messages: [
|
|
46
|
+
{
|
|
47
|
+
role: 'system',
|
|
48
|
+
content: `Summarize the user's text as ${target}. Reply with the summary only.`,
|
|
49
|
+
},
|
|
50
|
+
{ role: 'user', content: text },
|
|
51
|
+
],
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
return { summary: content.trim(), usage }
|
|
55
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Synthesize speech with this server's own TTS generator and hand back a
|
|
2
|
+
// download URL — the caller never needs filesystem access to the server.
|
|
3
|
+
//
|
|
4
|
+
// Needs an onnx-tts generator in the server config, e.g.:
|
|
5
|
+
//
|
|
6
|
+
// [[generators]]
|
|
7
|
+
// type = "onnx-tts"
|
|
8
|
+
// [generators.model]
|
|
9
|
+
// repo_id = "Xenova/speecht5_tts"
|
|
10
|
+
//
|
|
11
|
+
// curl -X POST http://<host>:<port>/functions/text-to-speech \
|
|
12
|
+
// -H 'Content-Type: application/json' \
|
|
13
|
+
// -d '{"text": "Hello from Buttress"}'
|
|
14
|
+
// # → { "result": { "url": "/functions/files/…", … } }
|
|
15
|
+
// curl -OJ "http://<host>:<port>$url" # same auth headers as the call
|
|
16
|
+
|
|
17
|
+
export const meta: ButtressFunctionMeta = {
|
|
18
|
+
description: 'Synthesize speech from text with the local TTS model; returns a download URL',
|
|
19
|
+
parameters: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
text: { type: 'string', description: 'Text to speak' },
|
|
23
|
+
speaker: { type: 'string', description: 'Optional registered speaker id' },
|
|
24
|
+
model: { type: 'string', description: 'Optional TTS model override' },
|
|
25
|
+
},
|
|
26
|
+
required: ['text'],
|
|
27
|
+
},
|
|
28
|
+
timeout: '5m',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type Input = { text: string; speaker?: string; model?: string }
|
|
32
|
+
|
|
33
|
+
export default async function ({ text, speaker, model }: Input, context: ButtressFunctionContext) {
|
|
34
|
+
const {
|
|
35
|
+
path: audioPath,
|
|
36
|
+
sampling_rate,
|
|
37
|
+
channels,
|
|
38
|
+
} = await context.buttress.synthesize({
|
|
39
|
+
text,
|
|
40
|
+
model,
|
|
41
|
+
options: speaker ? { speaker } : {},
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
// Download with the same Authorization the function call used; the file
|
|
46
|
+
// stays available until the scratch-dir sweep (~24h).
|
|
47
|
+
url: context.fileUrl(audioPath),
|
|
48
|
+
sampling_rate,
|
|
49
|
+
channels,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Transcribe any audio or video file: ffmpeg extracts 16 kHz mono WAV audio
|
|
2
|
+
// into the per-call scratch directory, then this server's STT generator
|
|
3
|
+
// transcribes it. Progress reaches SSE callers (`?stream=1`) via context.emit.
|
|
4
|
+
//
|
|
5
|
+
// Needs `ffmpeg` on the server's PATH and an STT generator (ggml-stt or
|
|
6
|
+
// onnx-stt) in the server config.
|
|
7
|
+
//
|
|
8
|
+
// Remote callers send the media inline — a multipart body stages every file
|
|
9
|
+
// field and injects its server-local path into the input, so one request
|
|
10
|
+
// does the whole job (bump [server] max_body_size for large files):
|
|
11
|
+
//
|
|
12
|
+
// curl -X POST 'http://<host>:<port>/functions/transcribe-media?stream=1' \
|
|
13
|
+
// -F file=@interview.mp4
|
|
14
|
+
//
|
|
15
|
+
// To reuse one file across several calls, stage it once instead and pass the
|
|
16
|
+
// returned path as {"file": "<path>"}:
|
|
17
|
+
//
|
|
18
|
+
// curl -F file=@interview.mp4 http://<host>:<port>/functions/upload
|
|
19
|
+
|
|
20
|
+
import path from 'node:path'
|
|
21
|
+
|
|
22
|
+
export const meta: ButtressFunctionMeta = {
|
|
23
|
+
description: 'Transcribe an audio/video file using ffmpeg and the local STT model',
|
|
24
|
+
parameters: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
file: { type: 'string', description: 'Absolute path to a media file on the server' },
|
|
28
|
+
model: { type: 'string', description: 'Optional STT model override' },
|
|
29
|
+
},
|
|
30
|
+
required: ['file'],
|
|
31
|
+
},
|
|
32
|
+
timeout: '15m',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type Input = { file: string; model?: string }
|
|
36
|
+
|
|
37
|
+
export default async function ({ file, model }: Input, context: ButtressFunctionContext) {
|
|
38
|
+
const wavPath = path.join(context.tempDir, 'audio.wav')
|
|
39
|
+
|
|
40
|
+
context.emit('stage', 'extract-audio')
|
|
41
|
+
// -hide_banner/-loglevel error keep stderr to the actual failure, so a
|
|
42
|
+
// thrown error reaches the caller instead of ffmpeg's build configuration.
|
|
43
|
+
const { code, stderr } = await context.spawn('ffmpeg', [
|
|
44
|
+
'-hide_banner',
|
|
45
|
+
'-loglevel',
|
|
46
|
+
'error',
|
|
47
|
+
'-y',
|
|
48
|
+
'-i',
|
|
49
|
+
file,
|
|
50
|
+
'-vn',
|
|
51
|
+
'-ac',
|
|
52
|
+
'1',
|
|
53
|
+
'-ar',
|
|
54
|
+
'16000',
|
|
55
|
+
wavPath,
|
|
56
|
+
])
|
|
57
|
+
if (code !== 0) {
|
|
58
|
+
throw new Error(`ffmpeg failed (${code}): ${String(stderr).slice(-2000)}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
context.emit('stage', 'transcribe')
|
|
62
|
+
const transcription = await context.buttress.transcribe({ model, filePath: wavPath })
|
|
63
|
+
|
|
64
|
+
return { transcription }
|
|
65
|
+
}
|
package/config/sample.toml
CHANGED
|
@@ -29,6 +29,22 @@ enabled = true
|
|
|
29
29
|
# cors_allowed_origins = ["http://localhost:3000", "https://example.com"]
|
|
30
30
|
# cors_allowed_origins = "*"
|
|
31
31
|
|
|
32
|
+
# Local functions (EXPERIMENTAL): .ts/.js files exposed as MCP tools and HTTP
|
|
33
|
+
# endpoints. Interfaces may change between releases.
|
|
34
|
+
# See the "Local Functions" section of README.md; ready-to-copy examples live
|
|
35
|
+
# in config/function-samples/. An optional _auth.ts in the functions directory
|
|
36
|
+
# customizes auth (extra gate, or full replacement of workspace auth).
|
|
37
|
+
# [functions]
|
|
38
|
+
# enabled = true
|
|
39
|
+
# dir = "./functions" # relative to this config file
|
|
40
|
+
# default_timeout = "5m" # per-call deadline; meta.timeout overrides it
|
|
41
|
+
# hot_reload = false # watch the dir and reload eagerly on change
|
|
42
|
+
# allow_unauthenticated = false # UNBOUND servers reject calls unless this is true
|
|
43
|
+
# cors_allowed_origins = ["http://localhost:3000"] # browsers are blocked unless listed
|
|
44
|
+
# [functions.config] # free-form; reaches functions as context.config
|
|
45
|
+
# api_base = "https://example.internal"
|
|
46
|
+
# api_keys = ["a-long-random-string"] # e.g. consumed by function-samples/_auth.ts
|
|
47
|
+
|
|
32
48
|
[runtime]
|
|
33
49
|
cache_dir = "./.buttress-cache"
|
|
34
50
|
# huggingface_token = "hf_xx"
|
|
@@ -73,6 +89,15 @@ quantization = "mxfp4"
|
|
|
73
89
|
download = true
|
|
74
90
|
n_ctx = 12800 # Max: 131072
|
|
75
91
|
|
|
92
|
+
# Optional separate draft model for speculative decoding. Buttress includes the
|
|
93
|
+
# draft model in pre-downloads and memory planning.
|
|
94
|
+
# model_draft = { repo_id = "org/draft-model-GGUF", filename = "draft-q8_0.gguf" }
|
|
95
|
+
# speculative = { type = "draft-mtp" }
|
|
96
|
+
# spec_draft_n_max = 4
|
|
97
|
+
# spec_draft_n_gpu_layers = -1
|
|
98
|
+
# spec_draft_cache_type_k = "f16"
|
|
99
|
+
# spec_draft_cache_type_v = "f16"
|
|
100
|
+
|
|
76
101
|
[[generators]]
|
|
77
102
|
type = "ggml-llm"
|
|
78
103
|
[generators.backend]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AutodiscoverConfig } from '../types';
|
|
2
|
+
import type { GetServerInfoFn } from './types';
|
|
3
|
+
import { type AnnounceSigner } from './udp';
|
|
4
|
+
export type { GetServerInfoFn } from './types';
|
|
5
|
+
export { signEnvelope, buildAnnounceSigner, type AnnounceSigner } from './sign';
|
|
6
|
+
/**
|
|
7
|
+
* Autodiscover service that manages discovery transports.
|
|
8
|
+
* Currently supports UDP announcements/responses.
|
|
9
|
+
* HTTP discovery is handled by the info route.
|
|
10
|
+
*/
|
|
11
|
+
export declare class AutodiscoverService {
|
|
12
|
+
private config;
|
|
13
|
+
private getServerInfo;
|
|
14
|
+
private signer;
|
|
15
|
+
private transports;
|
|
16
|
+
private started;
|
|
17
|
+
constructor(config: AutodiscoverConfig, getServerInfo: GetServerInfoFn, signer: AnnounceSigner | null);
|
|
18
|
+
start(): Promise<void>;
|
|
19
|
+
stop(): Promise<void>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import type { WorkspaceState } from '../utils/workspaceState';
|
|
3
|
+
import { type IDiscoveryProtocol } from './types';
|
|
4
|
+
export interface AnnounceSigner {
|
|
5
|
+
kid: string;
|
|
6
|
+
privateKey: crypto.KeyObject;
|
|
7
|
+
}
|
|
8
|
+
export declare const canonicalBytes: (t: 'ANNOUNCE' | 'RESPONSE', d: unknown, ts: number) => Buffer;
|
|
9
|
+
export declare const signEnvelope: (signer: AnnounceSigner | null, t: 'ANNOUNCE' | 'RESPONSE', d: IDiscoveryProtocol['d']) => IDiscoveryProtocol | null;
|
|
10
|
+
export declare const buildAnnounceSigner: (workspaceState: WorkspaceState) => AnnounceSigner | null;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ServerInfo } from '../types';
|
|
2
|
+
export declare const PROTOCOL_VERSION = "2.0";
|
|
3
|
+
export declare const DEFAULT_PORT = 8089;
|
|
4
|
+
export interface ITransport {
|
|
5
|
+
name: string;
|
|
6
|
+
start(): Promise<void>;
|
|
7
|
+
stop(): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
interface DiscoveryAnnounce {
|
|
10
|
+
info: Partial<ServerInfo>;
|
|
11
|
+
}
|
|
12
|
+
export interface DiscoveryRequest {
|
|
13
|
+
id: string;
|
|
14
|
+
filters?: {
|
|
15
|
+
generators?: string[];
|
|
16
|
+
min_version?: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
interface DiscoveryResponse {
|
|
20
|
+
request_id: string;
|
|
21
|
+
info: Partial<ServerInfo>;
|
|
22
|
+
}
|
|
23
|
+
export interface IDiscoveryProtocol {
|
|
24
|
+
t: 'ANNOUNCE' | 'QUERY' | 'RESPONSE';
|
|
25
|
+
v: string;
|
|
26
|
+
d: DiscoveryAnnounce | DiscoveryRequest | DiscoveryResponse;
|
|
27
|
+
ts?: number;
|
|
28
|
+
kid?: string;
|
|
29
|
+
sig?: string;
|
|
30
|
+
}
|
|
31
|
+
export type GetServerInfoFn = () => ServerInfo;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AutodiscoverConfig } from '../types';
|
|
2
|
+
import { ITransport, GetServerInfoFn } from './types';
|
|
3
|
+
import { type AnnounceSigner } from './sign';
|
|
4
|
+
export type { AnnounceSigner } from './sign';
|
|
5
|
+
export declare class UdpTransport implements ITransport {
|
|
6
|
+
name: string;
|
|
7
|
+
private receiver;
|
|
8
|
+
private senders;
|
|
9
|
+
private announcementTimer;
|
|
10
|
+
private config;
|
|
11
|
+
private getServerInfo;
|
|
12
|
+
private port;
|
|
13
|
+
private signer;
|
|
14
|
+
constructor(config: AutodiscoverConfig['udp'], getServerInfo: GetServerInfoFn, signer: AnnounceSigner | null);
|
|
15
|
+
start(): Promise<void>;
|
|
16
|
+
stop(): Promise<void>;
|
|
17
|
+
private bindReceiver;
|
|
18
|
+
private createSenders;
|
|
19
|
+
private handleMessage;
|
|
20
|
+
private sendAnnouncement;
|
|
21
|
+
private sendResponse;
|
|
22
|
+
}
|
package/lib/cli.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional operator-supplied auth for the `/functions` surface.
|
|
3
|
+
*
|
|
4
|
+
* Dropping `_auth.ts` (or `.js`) into the functions directory activates it —
|
|
5
|
+
* the underscore keeps it out of tool discovery like any other helper file.
|
|
6
|
+
* `meta.mode` decides how it composes with the built-in workspace auth:
|
|
7
|
+
*
|
|
8
|
+
* - 'both' (default): workspace auth runs first, unchanged; the custom
|
|
9
|
+
* function is an additional gate, so it can only narrow access.
|
|
10
|
+
* - 'override': the custom function is the only authority. A presented
|
|
11
|
+
* workspace token is still verified so the function can honor it via
|
|
12
|
+
* `request.workspaceAuth` (e.g. accept workspace tokens OR an API key).
|
|
13
|
+
*
|
|
14
|
+
* Failure never widens access: while an `_auth` file exists but does not
|
|
15
|
+
* load, every call is rejected until it is fixed. Reload follows the same
|
|
16
|
+
* mtime-driven lazy scheme as function files, and deleting the file reverts
|
|
17
|
+
* the server to plain workspace auth on the next request.
|
|
18
|
+
*/
|
|
19
|
+
import type { FunctionsConfig, LoadedAuthFunction } from './types';
|
|
20
|
+
export declare const AUTH_BASENAME = "_auth";
|
|
21
|
+
export type AuthLoader = {
|
|
22
|
+
/**
|
|
23
|
+
* Current auth function, or null when no `_auth` file exists. Throws when
|
|
24
|
+
* the file is present but unloadable — callers must treat that as deny-all.
|
|
25
|
+
*/
|
|
26
|
+
get: () => Promise<LoadedAuthFunction | null>;
|
|
27
|
+
};
|
|
28
|
+
export declare const createAuthLoader: (config: FunctionsConfig) => AuthLoader;
|
|
@@ -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,50 @@
|
|
|
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
|
+
call: (name: string, input: any, options: CallOptions) => Promise<any>;
|
|
29
|
+
/**
|
|
30
|
+
* The operator's `_auth` function, or null when none exists. Rejects while
|
|
31
|
+
* an `_auth` file is present but broken — callers treat that as deny-all.
|
|
32
|
+
*/
|
|
33
|
+
getCustomAuth: () => Promise<LoadedAuthFunction | null>;
|
|
34
|
+
/**
|
|
35
|
+
* Live capability summary for `serverInfo`. Mutated in place on every
|
|
36
|
+
* `list()` so the announced count follows the directory instead of freezing
|
|
37
|
+
* at whatever was on disk during startup.
|
|
38
|
+
*/
|
|
39
|
+
stats: {
|
|
40
|
+
enabled: true;
|
|
41
|
+
count: number;
|
|
42
|
+
};
|
|
43
|
+
/** Stop the hot-reload watcher, when one is running. Safe to call always. */
|
|
44
|
+
dispose: () => void;
|
|
45
|
+
};
|
|
46
|
+
export type CreateFunctionsServiceOptions = {
|
|
47
|
+
/** `server.temp_file_dir`; per-call scratch space lives under it. */
|
|
48
|
+
tempFileDir: string;
|
|
49
|
+
};
|
|
50
|
+
export declare const createFunctionsService: (config: FunctionsConfig, { tempFileDir }: CreateFunctionsServiceOptions) => Promise<FunctionsService>;
|
|
@@ -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 {};
|