lazyclaw 4.2.2 → 5.0.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.ko.md +44 -0
- package/README.md +172 -353
- package/agents.mjs +19 -3
- package/channels/handoff.mjs +41 -0
- package/channels/loader.mjs +124 -0
- package/channels/matrix.mjs +417 -0
- package/channels/telegram.mjs +362 -0
- package/channels/threads.mjs +116 -0
- package/cli.mjs +730 -27
- package/daemon.mjs +111 -0
- package/gateway/device_auth.mjs +664 -0
- package/gateway/http_gateway.mjs +304 -0
- package/mas/agent_memory.mjs +35 -34
- package/mas/agent_turn.mjs +30 -1
- package/mas/confidence.mjs +108 -0
- package/mas/index_db.mjs +242 -0
- package/mas/mention_router.mjs +75 -4
- package/mas/nudge.mjs +97 -0
- package/mas/prompt_stack.mjs +80 -0
- package/mas/provider_adapters.mjs +83 -0
- package/mas/redact.mjs +46 -0
- package/mas/skill_synth.mjs +331 -0
- package/mas/tool_runner.mjs +19 -48
- package/mas/tools/browser.mjs +77 -0
- package/mas/tools/clarify.mjs +36 -0
- package/mas/tools/coding.mjs +109 -0
- package/mas/tools/delegation.mjs +53 -0
- package/mas/tools/edit.mjs +36 -0
- package/mas/tools/git.mjs +110 -0
- package/mas/tools/ha.mjs +34 -0
- package/mas/tools/learning.mjs +168 -0
- package/mas/tools/media.mjs +105 -0
- package/mas/tools/os.mjs +152 -0
- package/mas/tools/patch.mjs +91 -0
- package/mas/tools/recall.mjs +103 -0
- package/mas/tools/registry.mjs +93 -0
- package/mas/tools/scheduling.mjs +62 -0
- package/mas/tools/skill_view.mjs +43 -0
- package/mas/tools/web.mjs +137 -0
- package/mas/toolsets.mjs +64 -0
- package/mas/trajectory_export.mjs +169 -0
- package/mas/trajectory_store.mjs +179 -0
- package/mas/user_modeler.mjs +108 -0
- package/package.json +22 -3
- package/providers/codex_cli.mjs +200 -0
- package/providers/gemini_cli.mjs +179 -0
- package/providers/registry.mjs +61 -1
- package/sandbox/base.mjs +82 -0
- package/sandbox/confiners/bubblewrap.mjs +21 -0
- package/sandbox/confiners/firejail.mjs +16 -0
- package/sandbox/confiners/landlock.mjs +14 -0
- package/sandbox/confiners/seatbelt.mjs +28 -0
- package/sandbox/daytona.mjs +37 -0
- package/sandbox/docker.mjs +91 -0
- package/sandbox/index.mjs +67 -0
- package/sandbox/local.mjs +59 -0
- package/sandbox/modal.mjs +53 -0
- package/sandbox/singularity.mjs +39 -0
- package/sandbox/ssh.mjs +56 -0
- package/sandbox.mjs +11 -127
- package/scripts/hermes-import.mjs +111 -0
- package/scripts/migrate-v5.mjs +342 -0
- package/scripts/openclaw-import.mjs +71 -0
- package/sessions.mjs +20 -1
- package/skills.mjs +101 -8
- package/skills_curator.mjs +323 -0
- package/workspace.mjs +18 -3
package/daemon.mjs
CHANGED
|
@@ -21,11 +21,14 @@ import { withFallback } from './providers/fallback.mjs';
|
|
|
21
21
|
import { withResponseCache } from './providers/cache.mjs';
|
|
22
22
|
import { costFromUsage, RATE_CARD_SHAPE } from './providers/rates.mjs';
|
|
23
23
|
import { composeSystemPrompt, listSkills, loadSkill, skillPath, installSkill, removeSkill } from './skills.mjs';
|
|
24
|
+
import { ChallengeRegistry } from './gateway/device_auth.mjs';
|
|
25
|
+
import { createGateway } from './gateway/http_gateway.mjs';
|
|
24
26
|
import { TokenBucketLimiter } from './ratelimit.mjs';
|
|
25
27
|
import { createLogger } from './logger.mjs';
|
|
26
28
|
import { summarizeState, listSessions as listWorkflowSessions, loadStateFile as loadWorkflowState, aggregateNodeStats } from './workflow/summary.mjs';
|
|
27
29
|
import { validateConfig } from './config-validate.mjs';
|
|
28
30
|
import { validateRates } from './rates-validate.mjs';
|
|
31
|
+
import * as nudge from './mas/nudge.mjs';
|
|
29
32
|
|
|
30
33
|
// Resolve the provider for a request. Composes opt-in wrappers in this
|
|
31
34
|
// order (innermost first):
|
|
@@ -321,6 +324,23 @@ export function makeHandler(ctx) {
|
|
|
321
324
|
costsByCurrency: /** @type {Record<string, number>} */({}),
|
|
322
325
|
tokensTotal: { inputTokens: 0, outputTokens: 0 },
|
|
323
326
|
};
|
|
327
|
+
// Device gateway (Phase 27). The ChallengeRegistry is a per-process
|
|
328
|
+
// singleton: a challenge minted by one request is consumed by a later
|
|
329
|
+
// one, so it must outlive a single call.
|
|
330
|
+
const gwConfigDir = typeof ctx.sessionsDirGetter === 'function' ? ctx.sessionsDirGetter() : undefined;
|
|
331
|
+
const gateway = createGateway({ configDir: gwConfigDir, challengeRegistry: new ChallengeRegistry(), heartbeatMs: 25000 });
|
|
332
|
+
// Phase B nudge loop — scans recent.jsonl every 5 min and pushes
|
|
333
|
+
// nudge.suggest_skill onto the SSE bus so the curator can prompt.
|
|
334
|
+
const _nudgeLoop = nudge.startNudgeLoop({
|
|
335
|
+
configDir: gwConfigDir,
|
|
336
|
+
emit: (event) => {
|
|
337
|
+
try { gateway.broadcast?.('nudge.suggest_skill', event); }
|
|
338
|
+
catch (err) { logger?.warn?.('nudge_emit_failed', { err: err.message }); }
|
|
339
|
+
},
|
|
340
|
+
logger,
|
|
341
|
+
});
|
|
342
|
+
process.on('SIGTERM', () => { _nudgeLoop.stop(); });
|
|
343
|
+
process.on('SIGINT', () => { _nudgeLoop.stop(); });
|
|
324
344
|
return async function handler(req, res) {
|
|
325
345
|
// Capture method+path before any handler logic runs; req.url survives
|
|
326
346
|
// the response but capturing now keeps the log line stable even if a
|
|
@@ -361,6 +381,31 @@ export function makeHandler(ctx) {
|
|
|
361
381
|
if (!isOriginAllowed(req, allowedOrigins, allowLoopback)) {
|
|
362
382
|
return writeJson(res, 403, { error: 'forbidden origin' });
|
|
363
383
|
}
|
|
384
|
+
// Device gateway (Phase 27) — routed BEFORE the shared auth-token
|
|
385
|
+
// gate. Companion-node auth is the gateway's own Ed25519 device-auth
|
|
386
|
+
// (challenge/sign/approve + bearer token); the only unauthenticated
|
|
387
|
+
// route, /gateway/connect/challenge, returns nothing but a random
|
|
388
|
+
// nonce. The bypass decision uses the NORMALIZED pathname (the same
|
|
389
|
+
// one the gateway routes on) so a dot-segment path like
|
|
390
|
+
// `/gateway/../sessions` can't skip the auth-token gate — it
|
|
391
|
+
// normalizes to `/sessions`, fails this prefix test, and falls
|
|
392
|
+
// through to the protected handler. When the limiter is enabled,
|
|
393
|
+
// gateway traffic uses its own key namespace so an unauthenticated
|
|
394
|
+
// flood can't drain the authenticated user's per-IP budget.
|
|
395
|
+
let gwPath = '';
|
|
396
|
+
try { gwPath = new URL(req.url || '/', 'http://localhost').pathname; } catch { gwPath = ''; }
|
|
397
|
+
if (gwPath.startsWith('/gateway/')) {
|
|
398
|
+
if (limiter) {
|
|
399
|
+
const key = 'gw:' + (req.socket?.remoteAddress || 'no-socket');
|
|
400
|
+
const verdict = limiter.consume(key);
|
|
401
|
+
if (!verdict.allowed) {
|
|
402
|
+
metrics.rateLimitDenied += 1;
|
|
403
|
+
const retrySeconds = Math.max(1, Math.ceil(verdict.retryAfterMs / 1000));
|
|
404
|
+
return writeJson(res, 429, { error: 'rate limit exceeded', retryAfterMs: verdict.retryAfterMs }, { 'retry-after': String(retrySeconds) });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return await gateway.handle(req, res, { readBody: readTextBody });
|
|
408
|
+
}
|
|
364
409
|
// Authentication gate — when authToken is set, every request must
|
|
365
410
|
// present `Authorization: Bearer <token>`. This is opt-in because
|
|
366
411
|
// the default deployment is loopback-only single-user; the token
|
|
@@ -430,6 +475,26 @@ export function makeHandler(ctx) {
|
|
|
430
475
|
}
|
|
431
476
|
case route === 'GET /version':
|
|
432
477
|
return writeJson(res, 200, { version: ctx.version(), nodeVersion: process.version, platform: `${process.platform}-${process.arch}` });
|
|
478
|
+
case route === 'POST /exec/request': {
|
|
479
|
+
// Remote exec-approval bridge. This route is AUTH-TOKEN-GATED
|
|
480
|
+
// (above), so only the trusted local operator/CLI can REQUEST an
|
|
481
|
+
// approval; a paired mobile device RESOLVES it over the gateway
|
|
482
|
+
// (POST /gateway/exec/resolve). The route long-polls: it awaits
|
|
483
|
+
// the device's decision (or the approval's timeout) and returns
|
|
484
|
+
// { approved, by, reason }.
|
|
485
|
+
let body;
|
|
486
|
+
try { body = await readJson(req); }
|
|
487
|
+
catch (e) { return writeJson(res, 400, { error: `invalid JSON body: ${e.message}` }); }
|
|
488
|
+
if (!body || typeof body.tool !== 'string' || !body.tool) {
|
|
489
|
+
return writeJson(res, 400, { error: 'tool is required' });
|
|
490
|
+
}
|
|
491
|
+
const { promise } = gateway.requestApproval(
|
|
492
|
+
{ tool: body.tool, args: body.args, agentId: body.agentId, summary: body.summary },
|
|
493
|
+
{ timeoutMs: Number.isFinite(+body.timeoutMs) ? +body.timeoutMs : undefined },
|
|
494
|
+
);
|
|
495
|
+
const result = await promise;
|
|
496
|
+
return writeJson(res, 200, result);
|
|
497
|
+
}
|
|
433
498
|
case route === 'GET /health':
|
|
434
499
|
// Conventional liveness check — always 200 if the process
|
|
435
500
|
// is alive enough to hit the route. No config inspection
|
|
@@ -1486,6 +1551,52 @@ export function makeHandler(ctx) {
|
|
|
1486
1551
|
}, m.headers || {});
|
|
1487
1552
|
}
|
|
1488
1553
|
}
|
|
1554
|
+
case route === 'POST /inbound': {
|
|
1555
|
+
// Generic inbound bridge — a stable, channel-agnostic relay
|
|
1556
|
+
// target so ANY platform (a Discord/WhatsApp/etc. bot the user
|
|
1557
|
+
// runs elsewhere) can forward a message in and get one reply,
|
|
1558
|
+
// without lazyclaw shipping that platform's SDK. Auth-token
|
|
1559
|
+
// gated like every non-gateway route; additionally pairing-gated
|
|
1560
|
+
// on senderId when a pairing allowlist is configured.
|
|
1561
|
+
const breach = checkCostCap(metrics, costCap);
|
|
1562
|
+
if (breach) return writeJson(res, 402, { error: 'cost cap exceeded', currency: breach.currency, spent: breach.spent, cap: breach.cap });
|
|
1563
|
+
let body;
|
|
1564
|
+
try { body = await readJson(req); }
|
|
1565
|
+
catch (e) { return writeJson(res, 400, { error: `invalid JSON body: ${e.message}` }); }
|
|
1566
|
+
const text = typeof body.text === 'string' ? body.text.trim() : '';
|
|
1567
|
+
if (!text) return writeJson(res, 400, { error: 'text is required' });
|
|
1568
|
+
const cfg = ctx.readConfig();
|
|
1569
|
+
// Pairing gate: when the operator has paired any senders, the
|
|
1570
|
+
// relay must identify an allowlisted senderId.
|
|
1571
|
+
const allow = Array.isArray(cfg.pairing) ? cfg.pairing.map((p) => String(p && p.id)) : [];
|
|
1572
|
+
if (allow.length > 0) {
|
|
1573
|
+
const sender = String(body.senderId || '');
|
|
1574
|
+
if (!sender || !allow.includes(sender)) return writeJson(res, 403, { error: 'sender not paired' });
|
|
1575
|
+
}
|
|
1576
|
+
const provName = body.provider || cfg.provider || 'mock';
|
|
1577
|
+
const resolved = resolveProvider(body, provName, cachedByName, logger);
|
|
1578
|
+
if (resolved.error) return writeJson(res, 400, { error: resolved.error });
|
|
1579
|
+
let acc = '';
|
|
1580
|
+
let inboundUsage = null;
|
|
1581
|
+
try {
|
|
1582
|
+
for await (const chunk of resolved.provider.sendMessage(
|
|
1583
|
+
[{ role: 'user', content: text }],
|
|
1584
|
+
{ apiKey: cfg['api-key'], model: body.model || cfg.model, onUsage: (u) => { inboundUsage = u; } },
|
|
1585
|
+
)) acc += chunk;
|
|
1586
|
+
} catch (err) {
|
|
1587
|
+
const m = statusForProviderError(err);
|
|
1588
|
+
return writeJson(res, m.status, { error: err?.message || String(err), code: err?.code || null }, m.headers || {});
|
|
1589
|
+
}
|
|
1590
|
+
// Feed the running spend total so the cost cap can actually trip
|
|
1591
|
+
// on /inbound traffic (mirrors POST /agent / POST /chat).
|
|
1592
|
+
if (inboundUsage && cfg.rates) {
|
|
1593
|
+
try {
|
|
1594
|
+
const c = costFromUsage({ provider: provName, model: body.model || cfg.model, usage: inboundUsage }, cfg.rates);
|
|
1595
|
+
if (c) accumulateMetricsFromCost(metrics, inboundUsage, c);
|
|
1596
|
+
} catch { /* cost is best-effort; never block a reply on it */ }
|
|
1597
|
+
}
|
|
1598
|
+
return writeJson(res, 200, { reply: acc, threadId: body.threadId || null });
|
|
1599
|
+
}
|
|
1489
1600
|
case route === 'POST /agent': {
|
|
1490
1601
|
const breach = checkCostCap(metrics, costCap);
|
|
1491
1602
|
if (breach) {
|