lazyclaw 6.0.0 → 6.0.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/channels-discord/index.mjs +76 -0
- package/channels-discord/package.json +14 -0
- package/channels-email/index.mjs +109 -0
- package/channels-email/package.json +14 -0
- package/channels-signal/index.mjs +69 -0
- package/channels-signal/package.json +9 -0
- package/channels-voice/index.mjs +81 -0
- package/channels-voice/package.json +9 -0
- package/channels-whatsapp/index.mjs +70 -0
- package/channels-whatsapp/package.json +13 -0
- package/commands/agents.mjs +669 -0
- package/commands/auth_nodes.mjs +323 -0
- package/commands/automation.mjs +582 -0
- package/commands/channels.mjs +255 -0
- package/commands/chat.mjs +1217 -0
- package/commands/config.mjs +315 -0
- package/commands/daemon.mjs +260 -0
- package/commands/misc.mjs +128 -0
- package/commands/providers.mjs +511 -0
- package/commands/sessions.mjs +343 -0
- package/commands/setup.mjs +741 -0
- package/commands/skills.mjs +218 -0
- package/commands/workflow.mjs +661 -0
- package/daemon/lib/auth.mjs +58 -0
- package/daemon/lib/cost.mjs +30 -0
- package/daemon/lib/provider.mjs +69 -0
- package/daemon/lib/respond.mjs +83 -0
- package/daemon/route_table.mjs +96 -0
- package/daemon/routes/_deps.mjs +36 -0
- package/daemon/routes/config.mjs +99 -0
- package/daemon/routes/conversation.mjs +371 -0
- package/daemon/routes/meta.mjs +239 -0
- package/daemon/routes/ops.mjs +185 -0
- package/daemon/routes/providers.mjs +211 -0
- package/daemon/routes/rates.mjs +90 -0
- package/daemon/routes/registry.mjs +223 -0
- package/daemon/routes/sessions.mjs +213 -0
- package/daemon/routes/skills.mjs +260 -0
- package/daemon/routes/workflows.mjs +224 -0
- package/dotenv_min.mjs +23 -0
- package/first_run.mjs +15 -0
- package/goals_cron.mjs +37 -0
- package/lib/args.mjs +216 -0
- package/lib/config.mjs +113 -0
- package/lib/registry_boot.mjs +55 -0
- package/package.json +14 -1
- package/secure_write.mjs +46 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Auth-token + origin gate helpers for the daemon. Pure — used only by
|
|
2
|
+
// the pre-switch middleware in makeHandler.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Constant-time string equality. Plain `===` would short-circuit on the
|
|
6
|
+
* first mismatching byte, leaking timing info that lets an attacker on
|
|
7
|
+
* a shared host narrow the secret one byte at a time. We compare every
|
|
8
|
+
* byte with XOR + accumulator.
|
|
9
|
+
*/
|
|
10
|
+
export function constantTimeEqual(a, b) {
|
|
11
|
+
const aStr = String(a ?? '');
|
|
12
|
+
const bStr = String(b ?? '');
|
|
13
|
+
if (aStr.length !== bStr.length) return false;
|
|
14
|
+
let diff = 0;
|
|
15
|
+
for (let i = 0; i < aStr.length; i++) {
|
|
16
|
+
diff |= aStr.charCodeAt(i) ^ bStr.charCodeAt(i);
|
|
17
|
+
}
|
|
18
|
+
return diff === 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isAuthorized(req, expectedToken) {
|
|
22
|
+
if (!expectedToken) return true; // auth disabled
|
|
23
|
+
const header = req.headers['authorization'] || '';
|
|
24
|
+
const m = /^Bearer\s+(.+)$/i.exec(header);
|
|
25
|
+
if (!m) return false;
|
|
26
|
+
return constantTimeEqual(m[1].trim(), expectedToken);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Origin gate — protect against DNS-rebinding / CSRF where a page in
|
|
31
|
+
* the user's browser posts to 127.0.0.1:<our port>. Browsers always
|
|
32
|
+
* attach `Origin` for cross-origin POSTs (and increasingly for GETs);
|
|
33
|
+
* CLI tools (curl, fetch from a script) usually don't.
|
|
34
|
+
*
|
|
35
|
+
* Policy:
|
|
36
|
+
* - No `Origin` header → assume non-browser caller, allow.
|
|
37
|
+
* - `Origin` set → must be in `allowedOrigins`. Empty allowlist
|
|
38
|
+
* means "reject all browser-originated requests" — the default,
|
|
39
|
+
* because the daemon is designed for CLI/script callers.
|
|
40
|
+
* - `allowLoopback: true` (set by `lazyclaw dashboard`) additionally
|
|
41
|
+
* accepts any `Origin` that looks like loopback (`http://127.0.0.1:*`,
|
|
42
|
+
* `http://localhost:*`, `http://[::1]:*`). Safe because the daemon
|
|
43
|
+
* binds only to 127.0.0.1, so an attacker can't reach us with a
|
|
44
|
+
* loopback Origin unless they're already on the box. DNS rebinding
|
|
45
|
+
* can't forge `127.0.0.1` as a hostname — that resolves before
|
|
46
|
+
* `fetch()` ever issues the request.
|
|
47
|
+
*
|
|
48
|
+
* Returns true when the request should proceed, false when it should
|
|
49
|
+
* be rejected with 403.
|
|
50
|
+
*/
|
|
51
|
+
const LOOPBACK_ORIGIN_RE = /^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/i;
|
|
52
|
+
export function isOriginAllowed(req, allowedOrigins, allowLoopback) {
|
|
53
|
+
const origin = req.headers['origin'];
|
|
54
|
+
if (!origin) return true;
|
|
55
|
+
if (allowLoopback && LOOPBACK_ORIGIN_RE.test(origin)) return true;
|
|
56
|
+
if (!allowedOrigins || allowedOrigins.length === 0) return false;
|
|
57
|
+
return allowedOrigins.includes(origin);
|
|
58
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Cost-cap enforcement + per-handler metrics accumulation for the daemon.
|
|
2
|
+
// Pure — operates only on the passed-in metrics/costCap objects.
|
|
3
|
+
|
|
4
|
+
// Has the cumulative cost in any capped currency reached the cap?
|
|
5
|
+
// Returns the offending currency + amount + cap so the caller can
|
|
6
|
+
// surface it cleanly, or null when no cap is breached.
|
|
7
|
+
export function checkCostCap(metrics, costCap) {
|
|
8
|
+
if (!costCap) return null;
|
|
9
|
+
for (const [cur, cap] of Object.entries(costCap)) {
|
|
10
|
+
if (!Number.isFinite(cap) || cap <= 0) continue;
|
|
11
|
+
const spent = metrics.costsByCurrency[cur] || 0;
|
|
12
|
+
if (spent >= cap) return { currency: cur, spent: Math.round(spent * 1_000_000) / 1_000_000, cap };
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Bump per-handler metrics from a single request's cost+usage. Keys
|
|
18
|
+
// cost by currency so heterogeneous fleets (USD-priced anthropic, EUR
|
|
19
|
+
// regional contracts) don't silently sum mismatched numbers. Tokens
|
|
20
|
+
// are unit-free → single counter.
|
|
21
|
+
export function accumulateMetricsFromCost(metrics, usage, cost) {
|
|
22
|
+
if (cost && Number.isFinite(cost.cost)) {
|
|
23
|
+
const cur = cost.currency || 'USD';
|
|
24
|
+
metrics.costsByCurrency[cur] = (metrics.costsByCurrency[cur] || 0) + cost.cost;
|
|
25
|
+
}
|
|
26
|
+
if (usage) {
|
|
27
|
+
if (Number.isFinite(usage.inputTokens)) metrics.tokensTotal.inputTokens += usage.inputTokens;
|
|
28
|
+
if (Number.isFinite(usage.outputTokens)) metrics.tokensTotal.outputTokens += usage.outputTokens;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Provider resolution for the daemon: composes the base provider with
|
|
2
|
+
// opt-in cache / fallback / retry wrappers per request.
|
|
3
|
+
|
|
4
|
+
import { PROVIDERS } from '../../providers/registry.mjs';
|
|
5
|
+
import { withRateLimitRetry } from '../../providers/retry.mjs';
|
|
6
|
+
import { withFallback } from '../../providers/fallback.mjs';
|
|
7
|
+
import { withResponseCache } from '../../providers/cache.mjs';
|
|
8
|
+
|
|
9
|
+
// Resolve the provider for a request. Composes opt-in wrappers in this
|
|
10
|
+
// order (innermost first):
|
|
11
|
+
// 1. cache — wraps the base provider so cache hits never trigger
|
|
12
|
+
// fallback or retry (a hit is a successful response).
|
|
13
|
+
// 2. fallback — chain of alternates; pre-yield recoverable errors fall
|
|
14
|
+
// through; mid-stream errors bubble.
|
|
15
|
+
// 3. retry — outermost so each retry covers the full chain (retry
|
|
16
|
+
// exhausts → 429 to client).
|
|
17
|
+
// `cachedByName` is a per-handler Map shared across requests so the
|
|
18
|
+
// cache state actually persists between calls. Without that the cache
|
|
19
|
+
// would be empty on every request.
|
|
20
|
+
//
|
|
21
|
+
// Returns { provider } on success or { error } when the primary or any
|
|
22
|
+
// listed fallback name is unknown.
|
|
23
|
+
export function resolveProvider(body, providerName, cachedByName, logger) {
|
|
24
|
+
if (!PROVIDERS[providerName]) return { error: `unknown provider: ${providerName}` };
|
|
25
|
+
// The decorator callbacks emit one debug line each — useful for ops who
|
|
26
|
+
// set --log debug to diagnose why a request is slow or which provider
|
|
27
|
+
// actually served it. With the default level (info) these are silent.
|
|
28
|
+
const dbg = (msg, fields) => { if (logger) logger.debug(msg, fields); };
|
|
29
|
+
const wrapWithCache = (name) => {
|
|
30
|
+
if (!cachedByName) return PROVIDERS[name];
|
|
31
|
+
if (!cachedByName.has(name)) {
|
|
32
|
+
cachedByName.set(name, withResponseCache(PROVIDERS[name], {
|
|
33
|
+
maxEntries: cachedByName._opts?.maxEntries,
|
|
34
|
+
ttlMs: cachedByName._opts?.ttlMs,
|
|
35
|
+
onHit: ({ keyHash, size }) => dbg('cache.hit', { provider: name, keyHash: keyHash.slice(0, 12), size }),
|
|
36
|
+
onMiss: ({ keyHash }) => dbg('cache.miss', { provider: name, keyHash: keyHash.slice(0, 12) }),
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
return cachedByName.get(name);
|
|
40
|
+
};
|
|
41
|
+
// Cache only when the request explicitly opts in. The handler-level
|
|
42
|
+
// Map is shared so two requests with body.cache=true to the same base
|
|
43
|
+
// provider hit the same cache.
|
|
44
|
+
const useCache = !!body?.cache;
|
|
45
|
+
let prov = useCache ? wrapWithCache(providerName) : PROVIDERS[providerName];
|
|
46
|
+
if (Array.isArray(body?.fallback) && body.fallback.length > 0) {
|
|
47
|
+
const chain = [prov];
|
|
48
|
+
for (const name of body.fallback) {
|
|
49
|
+
if (!PROVIDERS[name]) return { error: `unknown fallback provider: ${name}` };
|
|
50
|
+
chain.push(useCache ? wrapWithCache(name) : PROVIDERS[name]);
|
|
51
|
+
}
|
|
52
|
+
prov = withFallback(chain, {
|
|
53
|
+
onFallback: ({ from, to, err }) => dbg('provider.fallback', {
|
|
54
|
+
from, to, errorCode: err?.code || null, errorMsg: String(err?.message || err).slice(0, 120),
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const r = body?.retry;
|
|
59
|
+
if (r && Number.isFinite(r.attempts) && r.attempts > 0) {
|
|
60
|
+
prov = withRateLimitRetry(prov, {
|
|
61
|
+
attempts: r.attempts,
|
|
62
|
+
maxBackoffMs: r.maxBackoffMs,
|
|
63
|
+
onRetry: ({ attempt, retryAfterMs, err }) => dbg('provider.retry', {
|
|
64
|
+
attempt, retryAfterMs, errorCode: err?.code || null,
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return { provider: prov };
|
|
69
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// HTTP request/response helpers for the daemon: body readers, JSON/SSE
|
|
2
|
+
// writers, provider-error status mapping, and a small fs existence check.
|
|
3
|
+
// Pure — no daemon state — so any route module can import these freely.
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
|
|
7
|
+
export async function fileExists(p) {
|
|
8
|
+
try { await fs.promises.access(p); return true; }
|
|
9
|
+
catch { return false; }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function readJson(req) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
let buf = '';
|
|
15
|
+
req.setEncoding('utf8');
|
|
16
|
+
req.on('data', d => { buf += d; if (buf.length > 5 * 1024 * 1024) { reject(new Error('body too large')); req.destroy(); } });
|
|
17
|
+
req.on('end', () => {
|
|
18
|
+
if (!buf) return resolve({});
|
|
19
|
+
try { resolve(JSON.parse(buf)); }
|
|
20
|
+
catch (e) { reject(new Error(`invalid JSON body: ${e.message}`)); }
|
|
21
|
+
});
|
|
22
|
+
req.on('error', reject);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Raw body reader — used for `PUT /skills/<name>` where the body is
|
|
27
|
+
// markdown rather than JSON. Same 1 MiB cap as the CLI's `--from-url`
|
|
28
|
+
// path so HTTP can't sneak past the safeguard the CLI enforces.
|
|
29
|
+
export const SKILL_MAX_BYTES = 1_048_576;
|
|
30
|
+
export function readTextBody(req, maxBytes = SKILL_MAX_BYTES) {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
let buf = '';
|
|
33
|
+
req.setEncoding('utf8');
|
|
34
|
+
req.on('data', d => {
|
|
35
|
+
buf += d;
|
|
36
|
+
if (buf.length > maxBytes) {
|
|
37
|
+
reject(new Error(`body exceeds ${maxBytes} bytes`));
|
|
38
|
+
req.destroy();
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
req.on('end', () => resolve(buf));
|
|
42
|
+
req.on('error', reject);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function writeJson(res, status, obj, extraHeaders = {}) {
|
|
47
|
+
const body = JSON.stringify(obj);
|
|
48
|
+
res.writeHead(status, {
|
|
49
|
+
'content-type': 'application/json; charset=utf-8',
|
|
50
|
+
'content-length': Buffer.byteLength(body),
|
|
51
|
+
...extraHeaders,
|
|
52
|
+
});
|
|
53
|
+
res.end(body);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Map provider error codes to HTTP statuses so clients can branch on
|
|
57
|
+
// res.status instead of parsing error messages. Returns
|
|
58
|
+
// { status, headers? } so 429 can attach a Retry-After.
|
|
59
|
+
//
|
|
60
|
+
// Exported for unit testing without spinning up an actual provider that
|
|
61
|
+
// would only fail under live network conditions.
|
|
62
|
+
export function statusForProviderError(err) {
|
|
63
|
+
if (err?.code === 'INVALID_KEY') return { status: 401 };
|
|
64
|
+
if (err?.code === 'RATE_LIMIT') {
|
|
65
|
+
const retrySeconds = Math.max(1, Math.ceil((err.retryAfterMs || 1000) / 1000));
|
|
66
|
+
return { status: 429, headers: { 'retry-after': String(retrySeconds) } };
|
|
67
|
+
}
|
|
68
|
+
if (err?.status && err.status >= 400 && err.status < 600) return { status: err.status };
|
|
69
|
+
return { status: 502 };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function writeSseHead(res) {
|
|
73
|
+
res.writeHead(200, {
|
|
74
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
75
|
+
'cache-control': 'no-cache, no-transform',
|
|
76
|
+
'connection': 'close',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function writeSse(res, event, data) {
|
|
81
|
+
if (event) res.write(`event: ${event}\n`);
|
|
82
|
+
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
83
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Ordered route table for the daemon. The dispatch loop in makeHandler
|
|
2
|
+
// walks this array top-to-bottom and runs the first entry whose `m`
|
|
3
|
+
// predicate matches the request — FIRST MATCH WINS.
|
|
4
|
+
//
|
|
5
|
+
// The order here is IDENTICAL to the original makeHandler `switch (true)`
|
|
6
|
+
// case order. Several predicates are exclusion-based (e.g.
|
|
7
|
+
// `providerMatch[1] !== 'test'`, `configKeyMatch[1] !== 'validate'`) and
|
|
8
|
+
// rely on that ordering to stay correct. DO NOT REORDER.
|
|
9
|
+
//
|
|
10
|
+
// `m(c)` receives the per-request dispatch context built in makeHandler
|
|
11
|
+
// (route/method/url + the pre-computed path-param regex matches). `h(c)`
|
|
12
|
+
// is the matching route handler.
|
|
13
|
+
|
|
14
|
+
import * as meta from './routes/meta.mjs';
|
|
15
|
+
import * as providers from './routes/providers.mjs';
|
|
16
|
+
import * as rates from './routes/rates.mjs';
|
|
17
|
+
import * as config from './routes/config.mjs';
|
|
18
|
+
import * as sessions from './routes/sessions.mjs';
|
|
19
|
+
import * as workflows from './routes/workflows.mjs';
|
|
20
|
+
import * as skills from './routes/skills.mjs';
|
|
21
|
+
import * as conversation from './routes/conversation.mjs';
|
|
22
|
+
import * as registry from './routes/registry.mjs';
|
|
23
|
+
import * as ops from './routes/ops.mjs';
|
|
24
|
+
|
|
25
|
+
export const ROUTES = [
|
|
26
|
+
{ m: (c) => c.route === 'GET /' || c.route === 'GET /dashboard', h: meta.dashboard },
|
|
27
|
+
{ m: (c) => c.route === 'GET /dashboard.css', h: meta.dashboardCss },
|
|
28
|
+
{ m: (c) => c.route === 'GET /dashboard.js', h: meta.dashboardJs },
|
|
29
|
+
{ m: (c) => c.route === 'GET /version', h: meta.version },
|
|
30
|
+
{ m: (c) => c.route === 'POST /exec/request', h: conversation.execRequest },
|
|
31
|
+
{ m: (c) => c.route === 'GET /health' || c.route === 'GET /healthz', h: meta.health },
|
|
32
|
+
{ m: (c) => c.route === 'GET /metrics', h: meta.metrics },
|
|
33
|
+
{ m: (c) => c.route === 'GET /providers', h: providers.providersList },
|
|
34
|
+
{ m: (c) => c.req.method === 'GET' && !!c.providerMatch && c.providerMatch[1] !== 'test', h: providers.providerGet },
|
|
35
|
+
{ m: (c) => c.route === 'GET /providers/test', h: providers.providersTest },
|
|
36
|
+
{ m: (c) => c.req.method === 'GET' && !!c.providerTestMatch, h: providers.providerTest },
|
|
37
|
+
{ m: (c) => c.route === 'POST /providers', h: providers.providersCreate },
|
|
38
|
+
{ m: (c) => c.req.method === 'DELETE' && !!c.providerMatch && c.providerMatch[1] !== 'test', h: providers.providerDelete },
|
|
39
|
+
{ m: (c) => c.route === 'GET /rates', h: rates.ratesList },
|
|
40
|
+
{ m: (c) => c.route === 'GET /rates/validate', h: rates.ratesValidate },
|
|
41
|
+
{ m: (c) => c.route === 'GET /rates/shape', h: rates.ratesShape },
|
|
42
|
+
{ m: (c) => c.req.method === 'PUT' && !!c.ratesKeyMatch && c.ratesKeyMatch[1] !== 'validate' && c.ratesKeyMatch[1] !== 'shape', h: rates.ratePut },
|
|
43
|
+
{ m: (c) => c.req.method === 'DELETE' && !!c.ratesKeyMatch && c.ratesKeyMatch[1] !== 'validate' && c.ratesKeyMatch[1] !== 'shape', h: rates.rateDelete },
|
|
44
|
+
{ m: (c) => c.route === 'GET /status', h: meta.status },
|
|
45
|
+
{ m: (c) => c.route === 'GET /config/validate', h: config.configValidate },
|
|
46
|
+
{ m: (c) => c.route === 'GET /config', h: config.configGet },
|
|
47
|
+
{ m: (c) => c.req.method === 'GET' && !!c.configKeyMatch && c.configKeyMatch[1] !== 'validate', h: config.configKeyGet },
|
|
48
|
+
{ m: (c) => c.req.method === 'PUT' && !!c.configKeyMatch && c.configKeyMatch[1] !== 'validate', h: config.configKeyPut },
|
|
49
|
+
{ m: (c) => c.req.method === 'DELETE' && !!c.configKeyMatch && c.configKeyMatch[1] !== 'validate', h: config.configKeyDelete },
|
|
50
|
+
{ m: (c) => c.route === 'GET /doctor', h: meta.doctor },
|
|
51
|
+
{ m: (c) => c.route === 'GET /sessions', h: sessions.sessionsList },
|
|
52
|
+
{ m: (c) => c.route === 'GET /sessions/search', h: sessions.sessionsSearch },
|
|
53
|
+
{ m: (c) => c.req.method === 'GET' && !!c.sessionExportMatch, h: sessions.sessionExport },
|
|
54
|
+
{ m: (c) => c.req.method === 'GET' && !!c.sessionMatch, h: sessions.sessionGet },
|
|
55
|
+
{ m: (c) => c.route === 'GET /workflows/aggregate', h: workflows.workflowsAggregate },
|
|
56
|
+
{ m: (c) => c.route === 'GET /workflows', h: workflows.workflowsList },
|
|
57
|
+
{ m: (c) => c.req.method === 'GET' && !!c.workflowMatch, h: workflows.workflowGet },
|
|
58
|
+
{ m: (c) => c.route === 'GET /skills', h: skills.skillsList },
|
|
59
|
+
{ m: (c) => c.route === 'GET /skills/suggestions', h: skills.skillsSuggestions },
|
|
60
|
+
{ m: (c) => c.route === 'POST /skills/synth', h: skills.skillsSynth },
|
|
61
|
+
{ m: (c) => c.route === 'GET /skills/search', h: skills.skillsSearch },
|
|
62
|
+
{ m: (c) => c.req.method === 'GET' && !!c.skillMatch, h: skills.skillGet },
|
|
63
|
+
{ m: (c) => c.req.method === 'PUT' && !!c.skillMatch, h: skills.skillPut },
|
|
64
|
+
{ m: (c) => c.req.method === 'DELETE' && !!c.skillMatch, h: skills.skillDelete },
|
|
65
|
+
{ m: (c) => c.req.method === 'DELETE' && !!c.sessionMatch, h: sessions.sessionDelete },
|
|
66
|
+
{ m: (c) => c.req.method === 'DELETE' && !!c.workflowMatch, h: workflows.workflowDelete },
|
|
67
|
+
{ m: (c) => c.route === 'POST /chat', h: conversation.chat },
|
|
68
|
+
{ m: (c) => c.route === 'POST /inbound', h: conversation.inbound },
|
|
69
|
+
{ m: (c) => c.route === 'POST /handoff', h: conversation.handoff },
|
|
70
|
+
{ m: (c) => c.route === 'POST /agent', h: conversation.agent },
|
|
71
|
+
{ m: (c) => c.route === 'GET /agents', h: registry.agentsList },
|
|
72
|
+
{ m: (c) => c.route === 'POST /agents', h: registry.agentsCreate },
|
|
73
|
+
{ m: (c) => c.req.method === 'GET' && /^\/agents\/([^/]+)$/.test(c.url.pathname), h: registry.agentGet },
|
|
74
|
+
{ m: (c) => c.req.method === 'PATCH' && /^\/agents\/([^/]+)$/.test(c.url.pathname), h: registry.agentPatch },
|
|
75
|
+
{ m: (c) => c.req.method === 'DELETE' && /^\/agents\/([^/]+)$/.test(c.url.pathname), h: registry.agentDelete },
|
|
76
|
+
{ m: (c) => c.req.method === 'GET' && /^\/agents\/([^/]+)\/memory$/.test(c.url.pathname), h: registry.agentMemoryGet },
|
|
77
|
+
{ m: (c) => c.req.method === 'PUT' && /^\/agents\/([^/]+)\/memory$/.test(c.url.pathname), h: registry.agentMemoryPut },
|
|
78
|
+
{ m: (c) => c.req.method === 'DELETE' && /^\/agents\/([^/]+)\/memory$/.test(c.url.pathname), h: registry.agentMemoryDelete },
|
|
79
|
+
{ m: (c) => c.route === 'GET /teams', h: registry.teamsList },
|
|
80
|
+
{ m: (c) => c.route === 'POST /teams', h: registry.teamsCreate },
|
|
81
|
+
{ m: (c) => c.req.method === 'GET' && /^\/teams\/([^/]+)$/.test(c.url.pathname), h: registry.teamGet },
|
|
82
|
+
{ m: (c) => c.req.method === 'PATCH' && /^\/teams\/([^/]+)$/.test(c.url.pathname), h: registry.teamPatch },
|
|
83
|
+
{ m: (c) => c.req.method === 'DELETE' && /^\/teams\/([^/]+)$/.test(c.url.pathname), h: registry.teamDelete },
|
|
84
|
+
{ m: (c) => c.route === 'GET /tasks', h: registry.tasksList },
|
|
85
|
+
{ m: (c) => c.req.method === 'GET' && /^\/tasks\/([^/]+)\/transcript$/.test(c.url.pathname), h: registry.taskTranscript },
|
|
86
|
+
{ m: (c) => c.req.method === 'GET' && /^\/tasks\/([^/]+)$/.test(c.url.pathname), h: registry.taskGet },
|
|
87
|
+
{ m: (c) => c.req.method === 'DELETE' && /^\/tasks\/([^/]+)$/.test(c.url.pathname), h: registry.taskDelete },
|
|
88
|
+
{ m: (c) => c.req.method === 'POST' && /^\/tasks\/([^/]+)\/(done|abandon)$/.test(c.url.pathname), h: registry.taskAction },
|
|
89
|
+
{ m: (c) => c.route === 'GET /trainer/status', h: ops.trainerStatus },
|
|
90
|
+
{ m: (c) => c.route === 'GET /recall', h: ops.recall },
|
|
91
|
+
{ m: (c) => c.route === 'GET /sandbox', h: ops.sandboxList },
|
|
92
|
+
{ m: (c) => c.req.method === 'POST' && /^\/sandbox\/([^/]+)\/test$/.test(c.url.pathname), h: ops.sandboxTest },
|
|
93
|
+
{ m: (c) => c.route === 'POST /sandbox/use', h: ops.sandboxUse },
|
|
94
|
+
{ m: (c) => c.route === 'GET /channels', h: ops.channels },
|
|
95
|
+
{ m: (c) => c.route === 'POST /index/rebuild', h: ops.indexRebuild },
|
|
96
|
+
];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Dependency barrel for daemon route modules. Re-exports every
|
|
2
|
+
// module-level helper/import that route handler bodies reference, so each
|
|
3
|
+
// route module can pull them with a single import and the handler bodies
|
|
4
|
+
// stay byte-identical to their original form in makeHandler.
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import nodePath from 'node:path';
|
|
8
|
+
export { fs, nodePath };
|
|
9
|
+
|
|
10
|
+
export { PROVIDERS, PROVIDER_INFO, maskApiKey } from '../../providers/registry.mjs';
|
|
11
|
+
export { costFromUsage, RATE_CARD_SHAPE } from '../../providers/rates.mjs';
|
|
12
|
+
export {
|
|
13
|
+
composeSystemPrompt, listSkills, loadSkill, skillPath, installSkill,
|
|
14
|
+
removeSkill, parseFrontmatter, defaultConfigDir as skillsDefaultConfigDir,
|
|
15
|
+
} from '../../skills.mjs';
|
|
16
|
+
export * as indexDb from '../../mas/index_db.mjs';
|
|
17
|
+
export * as skillSynth from '../../mas/skill_synth.mjs';
|
|
18
|
+
export { listBackends as sandboxListBackends } from '../../sandbox/index.mjs';
|
|
19
|
+
export {
|
|
20
|
+
summarizeState, listSessions as listWorkflowSessions,
|
|
21
|
+
loadStateFile as loadWorkflowState, aggregateNodeStats,
|
|
22
|
+
} from '../../workflow/summary.mjs';
|
|
23
|
+
export { validateConfig } from '../../config-validate.mjs';
|
|
24
|
+
export { validateRates } from '../../rates-validate.mjs';
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
fileExists, readJson, readTextBody, writeJson, writeSseHead, writeSse,
|
|
28
|
+
statusForProviderError,
|
|
29
|
+
} from '../lib/respond.mjs';
|
|
30
|
+
export { checkCostCap, accumulateMetricsFromCost } from '../lib/cost.mjs';
|
|
31
|
+
export { resolveProvider } from '../lib/provider.mjs';
|
|
32
|
+
// F5/F6 — cross-channel handoff: the threads store + the rollback-aware
|
|
33
|
+
// migration helper, so the conversation routes can bind inbound messages to a
|
|
34
|
+
// persistent thread/session and re-point them across channels.
|
|
35
|
+
export { openThreads } from '../../channels/threads.mjs';
|
|
36
|
+
export { handoffWithRollback } from '../../channels/handoff.mjs';
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Daemon route handlers (config), extracted verbatim from makeHandler (D5).
|
|
2
|
+
// Each handler takes the per-request dispatch context `c` and returns the
|
|
3
|
+
// HTTP response. Bodies are unchanged; only the dispatch wrapper is new.
|
|
4
|
+
import { fs, nodePath, PROVIDERS, PROVIDER_INFO, maskApiKey, costFromUsage, RATE_CARD_SHAPE, composeSystemPrompt, listSkills, loadSkill, skillPath, installSkill, removeSkill, parseFrontmatter, skillsDefaultConfigDir, indexDb, skillSynth, sandboxListBackends, summarizeState, listWorkflowSessions, loadWorkflowState, aggregateNodeStats, validateConfig, validateRates, fileExists, readJson, readTextBody, writeJson, writeSseHead, writeSse, statusForProviderError, checkCostCap, accumulateMetricsFromCost, resolveProvider } from './_deps.mjs';
|
|
5
|
+
|
|
6
|
+
export async function configValidate(c) {
|
|
7
|
+
const { ctx, logger, metrics, gateway, costCap, cachedByName, gwConfigDir, nudgeSuggestionsRing, workflowStateDir, req, res, method, path, route, url, sessionMatch, providerMatch, providerTestMatch, sessionExportMatch, skillMatch, workflowMatch, configKeyMatch, ratesKeyMatch } = c;
|
|
8
|
+
// Mirror of v3.39's `lazyclaw config validate`. Same shape
|
|
9
|
+
// (single source of truth in config-validate.mjs). HTTP
|
|
10
|
+
// status reflects ok/issues so a UI's "config status"
|
|
11
|
+
// badge can branch on HTTP code: 200 = ok, 422 = issues
|
|
12
|
+
// (Unprocessable Entity — semantically right for "the
|
|
13
|
+
// config you supplied is malformed").
|
|
14
|
+
const cfg = ctx.readConfig();
|
|
15
|
+
const { ok, issues, warnings } = validateConfig(cfg, PROVIDERS);
|
|
16
|
+
return writeJson(res, ok ? 200 : 422, {
|
|
17
|
+
ok,
|
|
18
|
+
keys: Object.keys(cfg),
|
|
19
|
+
issues,
|
|
20
|
+
warnings,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function configGet(c) {
|
|
25
|
+
const { ctx, logger, metrics, gateway, costCap, cachedByName, gwConfigDir, nudgeSuggestionsRing, workflowStateDir, req, res, method, path, route, url, sessionMatch, providerMatch, providerTestMatch, sessionExportMatch, skillMatch, workflowMatch, configKeyMatch, ratesKeyMatch } = c;
|
|
26
|
+
// Mirror of `lazyclaw config list`. Returns every stored key
|
|
27
|
+
// with the api-key value masked — lets a dashboard or script
|
|
28
|
+
// inspect the active configuration without shelling to the CLI.
|
|
29
|
+
const cfg = ctx.readConfig();
|
|
30
|
+
const safe = { ...cfg };
|
|
31
|
+
if (safe['api-key']) safe['api-key'] = maskApiKey(safe['api-key']);
|
|
32
|
+
return writeJson(res, 200, safe);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function configKeyGet(c) {
|
|
36
|
+
const { ctx, logger, metrics, gateway, costCap, cachedByName, gwConfigDir, nudgeSuggestionsRing, workflowStateDir, req, res, method, path, route, url, sessionMatch, providerMatch, providerTestMatch, sessionExportMatch, skillMatch, workflowMatch, configKeyMatch, ratesKeyMatch } = c;
|
|
37
|
+
// Mirror of `lazyclaw config get <key>`. The `!== 'validate'`
|
|
38
|
+
// guard ensures the literal GET /config/validate case (above)
|
|
39
|
+
// is never shadowed by this dynamic handler.
|
|
40
|
+
const key = configKeyMatch[1];
|
|
41
|
+
const cfg = ctx.readConfig();
|
|
42
|
+
if (!(key in cfg)) {
|
|
43
|
+
return writeJson(res, 404, { error: 'key not found', key });
|
|
44
|
+
}
|
|
45
|
+
const raw = cfg[key];
|
|
46
|
+
const value = key === 'api-key' ? maskApiKey(raw) : raw;
|
|
47
|
+
return writeJson(res, 200, { key, value });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function configKeyPut(c) {
|
|
51
|
+
const { ctx, logger, metrics, gateway, costCap, cachedByName, gwConfigDir, nudgeSuggestionsRing, workflowStateDir, req, res, method, path, route, url, sessionMatch, providerMatch, providerTestMatch, sessionExportMatch, skillMatch, workflowMatch, configKeyMatch, ratesKeyMatch } = c;
|
|
52
|
+
// PUT /config/<key> body: { value: <any> }
|
|
53
|
+
// Mirror of `lazyclaw config set <key> <value>`. Re-validates the
|
|
54
|
+
// whole config after the write so we never persist a broken state.
|
|
55
|
+
// Nested cargo (customProviders / rates / authProfiles) goes
|
|
56
|
+
// through its own dedicated endpoint — guarded here so a
|
|
57
|
+
// dashboard PUT can't bypass schema validation.
|
|
58
|
+
if (typeof ctx.writeConfig !== 'function') {
|
|
59
|
+
return writeJson(res, 405, { error: 'mutation disabled' });
|
|
60
|
+
}
|
|
61
|
+
const key = configKeyMatch[1];
|
|
62
|
+
if (key === 'customProviders' || key === 'rates' || key === 'authProfiles') {
|
|
63
|
+
return writeJson(res, 400, {
|
|
64
|
+
error: `use the dedicated endpoint for "${key}" — POST /providers · PUT /rates/<key> · authProfiles via CLI`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
let body;
|
|
68
|
+
try { body = await readJson(req); }
|
|
69
|
+
catch (e) { return writeJson(res, 400, { error: e?.message || String(e) }); }
|
|
70
|
+
const value = body && Object.prototype.hasOwnProperty.call(body, 'value') ? body.value : undefined;
|
|
71
|
+
const cfg = ctx.readConfig();
|
|
72
|
+
if (value === null || value === undefined) delete cfg[key];
|
|
73
|
+
else cfg[key] = value;
|
|
74
|
+
const v = validateConfig(cfg, PROVIDERS);
|
|
75
|
+
if (!v.ok) return writeJson(res, 422, v);
|
|
76
|
+
ctx.writeConfig(cfg);
|
|
77
|
+
const stored = key === 'api-key' && typeof cfg[key] === 'string' ? maskApiKey(cfg[key]) : cfg[key];
|
|
78
|
+
return writeJson(res, 200, { ok: true, key, value: stored });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function configKeyDelete(c) {
|
|
82
|
+
const { ctx, logger, metrics, gateway, costCap, cachedByName, gwConfigDir, nudgeSuggestionsRing, workflowStateDir, req, res, method, path, route, url, sessionMatch, providerMatch, providerTestMatch, sessionExportMatch, skillMatch, workflowMatch, configKeyMatch, ratesKeyMatch } = c;
|
|
83
|
+
// DELETE /config/<key> — same as `lazyclaw config delete`.
|
|
84
|
+
// Idempotent: 200 with `removed: false` when the key wasn't
|
|
85
|
+
// present.
|
|
86
|
+
if (typeof ctx.writeConfig !== 'function') {
|
|
87
|
+
return writeJson(res, 405, { error: 'mutation disabled' });
|
|
88
|
+
}
|
|
89
|
+
const key = configKeyMatch[1];
|
|
90
|
+
if (key === 'customProviders' || key === 'rates' || key === 'authProfiles') {
|
|
91
|
+
return writeJson(res, 400, { error: `delete via the dedicated endpoint for "${key}"` });
|
|
92
|
+
}
|
|
93
|
+
const cfg = ctx.readConfig();
|
|
94
|
+
if (!(key in cfg)) return writeJson(res, 200, { ok: true, key, removed: false });
|
|
95
|
+
delete cfg[key];
|
|
96
|
+
ctx.writeConfig(cfg);
|
|
97
|
+
return writeJson(res, 200, { ok: true, key, removed: true });
|
|
98
|
+
}
|
|
99
|
+
|