@byokit/openclaw 0.14.4 → 0.15.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/CHANGELOG.md +17 -0
- package/README.md +27 -0
- package/dist/bridge.d.ts +4 -1
- package/dist/bridge.js +16 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +12 -1
- package/dist/engine.d.ts +1 -1
- package/dist/engine.js +9 -2
- package/dist/index.d.ts +1 -1
- package/dist/kit.d.ts +6 -1
- package/dist/kit.js +20 -1
- package/dist/testing/fake-telegram.d.ts +19 -0
- package/dist/testing/fake-telegram.js +72 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/dist/types.d.ts +19 -0
- package/package.json +5 -5
- package/plugin/index.js +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.15.0 (2026-10-17)
|
|
6
|
+
|
|
7
|
+
- Dependency update: pins @byokit/secrets 0.5.2.
|
|
8
|
+
- Dependency update: pins @byokit/seal 0.3.1.
|
|
9
|
+
- Dependency update: pins @byokit/discover 0.8.4.
|
|
10
|
+
- Dependency update: pins @byokit/accounts 0.25.0.
|
|
11
|
+
|
|
12
|
+
- FEAT: turn on one chat channel. `channels.telegram: { token, allowFrom, member, onMessage }` (off by default) hands every Telegram direct message from the people you list to your app's `onMessage`, never to an engine agent: a message naming a helper, or a chat command such as `/new`, is just text for your handler. `kit.reply(chat, text)` answers the chat. Groups, chat commands and channel config writes stay off, and the bot token reaches the engine only as process env, never config, disk or logs. `fakeTelegram()` in `./testing` stands in for the Bot API.
|
|
13
|
+
|
|
14
|
+
## 0.14.5 (2026-10-17)
|
|
15
|
+
|
|
16
|
+
- FIX: adding a Go plan key that names no workspace (a bound service key the console refuses to list) now clears the standing workspace header before its probe, instead of carrying the previous workspace on its calls. A failed add's cleanup now drops the workspace header with the key, instead of leaving it behind for the next key.
|
|
17
|
+
- FEAT: `kit.installPlugin(pluginId)` installs the external provider plugin a route needs (`readiness: 'needs_plugin'`, such as Z.AI's GLM Coding Plan routes) into the kit's own engine, at exactly the engine's version and with the pinned npm integrity (`engine/plugins.json`). The plugin joins `plugins.allow` across restarts and `routes()` then reports its routes ready, so `addKey` reaches the engine's key test. A version or integrity mismatch rejects with `PluginInstallError` (`code: 'version' | 'integrity'`) and installs nothing; discovery never installs. The GLM Coding Plan sign-ins are subscription; the plain Z.AI key rows stay API key (billed per use), explicit opt-in.
|
|
18
|
+
- FIX: the install policy no longer blocks every `@openclaw/` npm plugin at its dependency-tree check; plugin installs are allowed only at a pinned spec and integrity.
|
|
19
|
+
- FIX: `removeAccount` and a cancelled/failed `addAccount` no longer resolve while the removed agent is still on the roster. The engine answers `agents.delete` before its roster hot-reload applies (the same lag as after `agents.create`), so an immediate `agents.list` could still show the gone agent and a cancelled ChatGPT add flaked as "the cancelled agent is deleted". Both delete paths now wait until the roster drops the agent (`untilUnlisted`, the `untilListed` mirror: 25 ms poll, ~5 s bound, plain error naming the agent on timeout). The fake gateway's `rosterApplyMs` now lags the delete roster the same way for engine parity.
|
|
20
|
+
- FIX: a Go plan key pasted over the link signs in with its workspace. The wizard sign-in listed no workspaces, so a key acting for several was refused on the activation probe and every run (403 Workspace selection is required). The sign-in now lists the key's workspaces the way `addKey` does (one used unasked; several asked through the same hidden paste; refused or unreadable listing fails before anything changes) and writes the pick as the provider header before activation.
|
|
21
|
+
|
|
5
22
|
## 0.14.4 (2026-10-17)
|
|
6
23
|
|
|
7
24
|
- Dependency update: pins @byokit/relay 0.8.0.
|
package/README.md
CHANGED
|
@@ -430,6 +430,33 @@ Public live view/thumbnail APIs are model-free; source tests alone do not prove
|
|
|
430
430
|
The opt-in physical fixture retains raw JPEG/control inputs before decoding, pipe chunk order, complete
|
|
431
431
|
synthetic provider bodies and authoritative SQLite transcripts, including failures.
|
|
432
432
|
|
|
433
|
+
## One chat channel (Telegram)
|
|
434
|
+
|
|
435
|
+
Off by default. Turn it on and every Telegram message from the people you list reaches your app, never an engine
|
|
436
|
+
agent: the kit claims each one before the engine dispatches it, so a message that names a helper, or a chat command
|
|
437
|
+
such as `/new`, is just text for your handler. You decide who answers (Crewhouse: Chief) and reply with `kit.reply`.
|
|
438
|
+
|
|
439
|
+
```ts
|
|
440
|
+
import { OpenClawKit, type ChannelMessage } from '@byokit/openclaw';
|
|
441
|
+
|
|
442
|
+
declare const stateDir: string, botToken: string, chiefThread: string;
|
|
443
|
+
const kit: OpenClawKit = new OpenClawKit({ stateDir, channels: { telegram: {
|
|
444
|
+
token: botToken, // from BotFather; the engine gets it as process env, never config, disk or logs
|
|
445
|
+
allowFrom: ['123456789'], // numeric Telegram user ids; everyone else is dropped
|
|
446
|
+
member: 'm1', // the member your replies are sent as
|
|
447
|
+
onMessage: async ({ chat, text }: ChannelMessage) => {
|
|
448
|
+
const end = await kit.run({ member: 'm1', sessionKey: chiefThread, message: text }, () => {});
|
|
449
|
+
if (end.ok) await kit.reply(chat, end.text);
|
|
450
|
+
},
|
|
451
|
+
} } });
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
Direct messages only: groups, chat commands, channel config writes and Telegram exec approvals stay off. `apiRoot`
|
|
455
|
+
points the channel at a self-hosted Bot API server; tests use `fakeTelegram()` from `@byokit/openclaw/testing`, a
|
|
456
|
+
loopback Bot API, so no bot or account is needed. An agent the gate lets run shell commands can read the token from
|
|
457
|
+
the engine's environment, as it can the gateway token. Telegram is the only channel so far; another one will be a
|
|
458
|
+
new key of `channels`.
|
|
459
|
+
|
|
433
460
|
## One config key, narrowed
|
|
434
461
|
|
|
435
462
|
`patchConfig` is the only whole-config writer, and it can only start from `config.get`, whose result redacts
|
package/dist/bridge.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Approval, Decision, Member, RunRef, ToolHost, ToolSpec } from './types.ts';
|
|
1
|
+
import type { Approval, ChannelMessage, Decision, Member, RunRef, ToolHost, ToolSpec } from './types.ts';
|
|
2
2
|
/**
|
|
3
3
|
* Asks must expire before the plugin's 195 s gate timeout, or the run gets a generic block while the approval
|
|
4
4
|
* lingers (N3). The kit passes its `approvalTimeoutMs` through; anything above is clamped to this.
|
|
@@ -25,6 +25,7 @@ export declare function writePlugin(dir: string, o: {
|
|
|
25
25
|
paramPrefix: string;
|
|
26
26
|
gateBuiltins: boolean;
|
|
27
27
|
browser?: boolean;
|
|
28
|
+
channels?: string[];
|
|
28
29
|
}): void;
|
|
29
30
|
export declare class Bridge {
|
|
30
31
|
private readonly path;
|
|
@@ -37,6 +38,7 @@ export declare class Bridge {
|
|
|
37
38
|
private readonly onAskGone;
|
|
38
39
|
private readonly beforeAgentRun?;
|
|
39
40
|
private readonly browserCapabilities?;
|
|
41
|
+
private readonly onChannelMessage?;
|
|
40
42
|
private server?;
|
|
41
43
|
private closing;
|
|
42
44
|
private readonly sockets;
|
|
@@ -58,6 +60,7 @@ export declare class Bridge {
|
|
|
58
60
|
onAskGone(id: string): void;
|
|
59
61
|
beforeAgentRun?: (key: string, runId?: string) => Promise<boolean>;
|
|
60
62
|
browserCapabilities?: () => readonly string[];
|
|
63
|
+
onChannelMessage?: (m: ChannelMessage) => void;
|
|
61
64
|
});
|
|
62
65
|
start(): Promise<void>;
|
|
63
66
|
stop(): void;
|
package/dist/bridge.js
CHANGED
|
@@ -47,6 +47,8 @@ export function writePlugin(dir, o) {
|
|
|
47
47
|
// true: engine builtins (web_fetch, memory, ...) are gated too, not only the app's tools.
|
|
48
48
|
gateBuiltins: o.gateBuiltins,
|
|
49
49
|
...(o.browser ? { browser: true } : {}),
|
|
50
|
+
// The channels whose every inbound message the plugin claims for the app (5.18).
|
|
51
|
+
...(o.channels?.length ? { channels: o.channels } : {}),
|
|
50
52
|
tools: o.tools.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters })),
|
|
51
53
|
};
|
|
52
54
|
for (const [file, data] of [['openclaw.plugin.json', manifest], ['tools.json', table]]) {
|
|
@@ -67,6 +69,7 @@ export class Bridge {
|
|
|
67
69
|
onAskGone;
|
|
68
70
|
beforeAgentRun;
|
|
69
71
|
browserCapabilities;
|
|
72
|
+
onChannelMessage;
|
|
70
73
|
server;
|
|
71
74
|
closing = Promise.resolve();
|
|
72
75
|
sockets = new Set();
|
|
@@ -90,6 +93,7 @@ export class Bridge {
|
|
|
90
93
|
this.onAskGone = o.onAskGone;
|
|
91
94
|
this.beforeAgentRun = o.beforeAgentRun;
|
|
92
95
|
this.browserCapabilities = o.browserCapabilities;
|
|
96
|
+
this.onChannelMessage = o.onChannelMessage;
|
|
93
97
|
}
|
|
94
98
|
async start() {
|
|
95
99
|
if (this.server)
|
|
@@ -270,6 +274,18 @@ export class Bridge {
|
|
|
270
274
|
.then(allow => this.reply(socket, { allow }), () => this.deny(socket, 'run unavailable'));
|
|
271
275
|
return;
|
|
272
276
|
}
|
|
277
|
+
if (message.kind === 'channel-message') {
|
|
278
|
+
const { channel, chat, from, text } = message;
|
|
279
|
+
if (!this.onChannelMessage || this.stopped || channel !== 'telegram' || typeof chat !== 'string' || !chat
|
|
280
|
+
|| typeof from !== 'string' || typeof text !== 'string')
|
|
281
|
+
return this.deny(socket, 'channel unavailable');
|
|
282
|
+
this.reply(socket, { ok: true });
|
|
283
|
+
try {
|
|
284
|
+
this.onChannelMessage({ chat: { channel, id: chat }, from, text });
|
|
285
|
+
}
|
|
286
|
+
catch { /* the app's handler, not the engine's */ }
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
273
289
|
if (message.kind === 'gate')
|
|
274
290
|
return void this.gate(socket, message);
|
|
275
291
|
if (message.kind === 'call')
|
package/dist/config.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export declare function reconcileConfig(saved: object | undefined, o: {
|
|
|
20
20
|
}>;
|
|
21
21
|
tools: string[];
|
|
22
22
|
};
|
|
23
|
+
channels?: KitOptions['channels'];
|
|
23
24
|
}): object;
|
|
24
25
|
/** Caller-owned task namespaces, including the engine key-agent rewrite; never an entire member. */
|
|
25
26
|
export declare function appRecoveryPrefixes(ownership?: KitOptions['appOwnedSessions']): string[];
|
package/dist/config.js
CHANGED
|
@@ -46,6 +46,16 @@ export function reconcileConfig(saved, o) {
|
|
|
46
46
|
models: { catalogRefresh: { enabled: false } }, logging: { file: join(o.stateDir, 'logs', 'openclaw-events.log') },
|
|
47
47
|
});
|
|
48
48
|
c.channels ??= {};
|
|
49
|
+
const telegram = o.channels?.telegram;
|
|
50
|
+
if (telegram) {
|
|
51
|
+
// The one channel the engine starts (5.18), written whole: no app key can widen who reaches it or where it routes.
|
|
52
|
+
// DMs only, from listed ids; no groups, chat commands, channel config writes or exec approvals. The bridge claims
|
|
53
|
+
// every message before dispatch, so these are the second wall. The token is process env, never this file.
|
|
54
|
+
c.channels = { telegram: { enabled: true, dmPolicy: 'allowlist', allowFrom: [...telegram.allowFrom],
|
|
55
|
+
groupPolicy: 'disabled', configWrites: false, commands: { native: false, nativeSkills: false },
|
|
56
|
+
execApprovals: { enabled: false }, ...(telegram.apiRoot ? { apiRoot: telegram.apiRoot } : {}) } };
|
|
57
|
+
c.commands = { ...object(c.commands) ? c.commands : {}, allowFrom: { ...c.commands?.allowFrom, telegram: [] } };
|
|
58
|
+
}
|
|
49
59
|
c.agents ??= {};
|
|
50
60
|
c.agents.defaults ??= {};
|
|
51
61
|
// Auth isolation (5.15): left unset, the pin makes the first agent the inherited owner once the roster grows, and
|
|
@@ -87,7 +97,8 @@ export function reconcileConfig(saved, o) {
|
|
|
87
97
|
// Plus every explicitly offered route the app names, so an opted-in sign-in (OpenRouter) is not blocked.
|
|
88
98
|
...offeredPlugins(o.offered),
|
|
89
99
|
// Plus every external plugin the kit installed (B7): an app's own allow list must not disable it at the next boot.
|
|
90
|
-
...o.plugins ?? []
|
|
100
|
+
...o.plugins ?? [],
|
|
101
|
+
...telegram ? ['telegram'] : []])];
|
|
91
102
|
c.plugins.entries ??= {};
|
|
92
103
|
c.plugins.entries[o.pluginId] = merge(c.plugins.entries[o.pluginId] ?? {}, { hooks: { timeouts: { before_tool_call: 200_000 } } });
|
|
93
104
|
if (o.browser) {
|
package/dist/engine.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { KitState, ToolSpec } from './types.ts';
|
|
|
5
5
|
/** The app's stateDir as an absolute path with no symlinked ancestor (macOS /tmp and /var are symlinks):
|
|
6
6
|
* the native Claude login's accounts store refuses any other, so a relative or symlinked stateDir failed it. */
|
|
7
7
|
export declare function canonicalStateDir(dir: string): string;
|
|
8
|
-
export type EngineOptions = Pick<KitOptions, 'stateDir' | 'engineDir' | 'npmPath' | 'enginePath' | 'config' | 'offered' | 'appOwnedSessions' | 'installPolicy' | 'log' | 'bridge' | 'authSeal'> & {
|
|
8
|
+
export type EngineOptions = Pick<KitOptions, 'stateDir' | 'engineDir' | 'npmPath' | 'enginePath' | 'config' | 'offered' | 'appOwnedSessions' | 'installPolicy' | 'log' | 'bridge' | 'authSeal' | 'channels'> & {
|
|
9
9
|
pluginId: string;
|
|
10
10
|
tools: ToolSpec[];
|
|
11
11
|
gateBuiltins?: boolean;
|
package/dist/engine.js
CHANGED
|
@@ -286,12 +286,14 @@ export class Engine {
|
|
|
286
286
|
}
|
|
287
287
|
// The bridge manifest and tool table follow the app's tools on every prepare (O5).
|
|
288
288
|
writePlugin(pluginDir, { id: this.o.pluginId, tools: this.o.tools, paramPrefix: this.paramPrefix,
|
|
289
|
-
gateBuiltins: this.o.gateBuiltins !== false, browser: !!this.o.browserConfig
|
|
289
|
+
gateBuiltins: this.o.gateBuiltins !== false, browser: !!this.o.browserConfig,
|
|
290
|
+
channels: this.o.channels?.telegram ? ['telegram'] : [] });
|
|
290
291
|
const path = join(this.root, 'openclaw.json');
|
|
291
292
|
const saved = existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : undefined;
|
|
292
293
|
const config = reconcileConfig(saved, { root: this.root, stateDir: this.o.stateDir, port: this.port, pluginId: this.o.pluginId,
|
|
293
294
|
pluginDir, policyPath: join(kitDir, 'policy', 'policy.mjs'), app: this.o.config, offered: this.o.offered,
|
|
294
|
-
installPolicy: this.o.installPolicy, browser: this.o.browserConfig?.(), plugins: installedPlugins(this.root)
|
|
295
|
+
installPolicy: this.o.installPolicy, browser: this.o.browserConfig?.(), plugins: installedPlugins(this.root),
|
|
296
|
+
channels: this.o.channels });
|
|
295
297
|
putChanged(path, JSON.stringify(config, null, 2) + '\n');
|
|
296
298
|
}
|
|
297
299
|
/** B7: the engine's own installer for one pinned external plugin, in the isolated engine env with the kit's npm
|
|
@@ -508,6 +510,11 @@ export class Engine {
|
|
|
508
510
|
}
|
|
509
511
|
this.state('starting');
|
|
510
512
|
const { entry, env } = this.doctorContext();
|
|
513
|
+
// A turned-on channel (5.18) starts in the gateway alone; doctor and installs keep channels off and never see the token.
|
|
514
|
+
if (this.o.channels?.telegram) {
|
|
515
|
+
delete env.OPENCLAW_SKIP_CHANNELS;
|
|
516
|
+
env.TELEGRAM_BOT_TOKEN = this.o.channels.telegram.token;
|
|
517
|
+
}
|
|
511
518
|
const fd = openSync(join(this.o.stateDir, 'logs', 'openclaw.log'), 'a', 0o600);
|
|
512
519
|
const usageDir = join(this.root, 'usage'), bootId = randomUUID();
|
|
513
520
|
try {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { ENGINE_VERSION, OPERATOR_SCOPES, PROTOCOL_VERSION } from './constants.ts';
|
|
2
|
-
export type { Account, AccountId, AccountPick, AccountRef, Approval, CallOptions, Considered, Decision, Defaults, ModelInfo, MoveResult, PickWhy, Room, RoomSpan, RunSelection, SignInState, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, GateResult, Hello, KitState, KitEventName, KitEventPayload, BrowserPing, LearningCapture, LearningMode, Member, PlanWindow, Route, RunEnd, RunEvent, RunRef, RunSpec, OutputSchema, SchemaOutput, RunUsage, ThinkingAsked, ThinkingReport, ThinkingUsed, SignInOptions, SignInView, ToolHost, ToolSpec, } from './types.ts';
|
|
2
|
+
export type { Account, AccountId, AccountPick, AccountRef, Approval, CallOptions, ChannelChat, ChannelMessage, Considered, Decision, Defaults, ModelInfo, MoveResult, PickWhy, Room, RoomSpan, RunSelection, SignInState, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, GateResult, Hello, KitState, KitEventName, KitEventPayload, BrowserPing, LearningCapture, LearningMode, Member, PlanWindow, Route, RunEnd, RunEvent, RunRef, RunSpec, OutputSchema, SchemaOutput, RunUsage, ThinkingAsked, ThinkingReport, ThinkingUsed, SignInOptions, SignInView, TelegramChannel, ToolHost, ToolSpec, } from './types.ts';
|
|
3
3
|
export type { GatewayMethods } from './generated/methods.ts';
|
|
4
4
|
export type { JsonValue } from './output.ts';
|
|
5
5
|
export type { RouteFacts, RouteView } from './routes.ts';
|
package/dist/kit.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { SealingAdapter } from '@byokit/secrets';
|
|
|
2
2
|
import { type AddKeyHandle } from './keys.ts';
|
|
3
3
|
import { type RouteView } from './routes.ts';
|
|
4
4
|
import type { BrowserHost, BrowserOptions } from './browser.ts';
|
|
5
|
-
import type { Account, AccountId, AccountPick, Approval, CallOptions, Decision, Defaults, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, Hello, KitState, KitEventName, KitEventPayload, LearningCapture, LearningMode, Member, ModelInfo, MoveResult, Room, RunEnd, RunEvent, RunRef, RunSelection, RunSpec, OutputSchema, SchemaOutput, SignInOptions, SignInView, ToolHost, ToolSpec } from './types.ts';
|
|
5
|
+
import type { Account, AccountId, AccountPick, Approval, CallOptions, Decision, Defaults, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, Hello, KitState, KitEventName, KitEventPayload, LearningCapture, LearningMode, Member, ModelInfo, MoveResult, Room, RunEnd, RunEvent, RunRef, RunSelection, RunSpec, OutputSchema, SchemaOutput, SignInOptions, SignInView, ChannelChat, TelegramChannel, ToolHost, ToolSpec } from './types.ts';
|
|
6
6
|
export type KitOptions = {
|
|
7
7
|
stateDir: string;
|
|
8
8
|
authSeal?: SealingAdapter;
|
|
@@ -26,6 +26,9 @@ export type KitOptions = {
|
|
|
26
26
|
keyPrefixes: string[];
|
|
27
27
|
};
|
|
28
28
|
browser?: BrowserOptions;
|
|
29
|
+
channels?: {
|
|
30
|
+
telegram?: TelegramChannel;
|
|
31
|
+
};
|
|
29
32
|
installPolicy?: {
|
|
30
33
|
trustedSkills: string;
|
|
31
34
|
ownRoots: string[];
|
|
@@ -209,6 +212,8 @@ export declare class OpenClawKit {
|
|
|
209
212
|
patchConfig(patch: object, o?: {
|
|
210
213
|
agentId?: string;
|
|
211
214
|
}): Promise<void>;
|
|
215
|
+
/** Answers one chat on a turned-on channel (5.18), through the engine's own outbound send. */
|
|
216
|
+
reply(chat: ChannelChat, text: string): Promise<void>;
|
|
212
217
|
memoryLimited(member: Member): boolean;
|
|
213
218
|
getConfigKey(key: string): unknown;
|
|
214
219
|
setConfigKey(key: string, value: unknown): unknown;
|
package/dist/kit.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { openCodeWorkspaces } from '@byokit/accounts/opencode-go';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { readFileSync } from 'node:fs';
|
|
3
4
|
import { join } from 'node:path';
|
|
4
5
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
@@ -647,6 +648,13 @@ export class OpenClawKit {
|
|
|
647
648
|
if (o.browser && (!o.browser.executablePath.startsWith('/') || (o.browser.members !== 'all'
|
|
648
649
|
&& (!Array.isArray(o.browser.members) || o.browser.members.some(m => !MEMBER_ID.test(m) || m.startsWith(KEY_PREFIX))))))
|
|
649
650
|
throw new Error('invalid browser options');
|
|
651
|
+
const telegram = o.channels?.telegram;
|
|
652
|
+
// The token never reaches an error, a log or the config (5.18).
|
|
653
|
+
if (telegram && (typeof telegram.token !== 'string' || !/^\d+:[\w-]+$/.test(telegram.token)
|
|
654
|
+
|| !Array.isArray(telegram.allowFrom) || !telegram.allowFrom.length || telegram.allowFrom.some(id => typeof id !== 'string' || !/^\d+$/.test(id))
|
|
655
|
+
|| typeof telegram.member !== 'string' || !MEMBER_ID.test(telegram.member) || telegram.member.startsWith(KEY_PREFIX)
|
|
656
|
+
|| typeof telegram.onMessage !== 'function' || (telegram.apiRoot !== undefined && !/^https?:\/\/[^\s]+$/.test(telegram.apiRoot))))
|
|
657
|
+
throw new Error('invalid telegram channel options');
|
|
650
658
|
// The engine lowercases and alias-maps a tool name before the gate hook sees it (bash -> exec, cron ->
|
|
651
659
|
// automations); a name it would rewrite reaches the gate as a builtin, so refuse it here.
|
|
652
660
|
for (const t of o.tools ?? []) {
|
|
@@ -711,6 +719,7 @@ export class OpenClawKit {
|
|
|
711
719
|
} } : {}),
|
|
712
720
|
permitted: o.permitted ?? (() => true), approvalTimeoutMs: o.approvalTimeoutMs ?? 180_000,
|
|
713
721
|
memberOf: (ref) => this.memberOf(ref),
|
|
722
|
+
...(o.channels?.telegram ? { onChannelMessage: (m) => o.channels.telegram.onMessage(m) } : {}),
|
|
714
723
|
onAsk: (a) => this.approvalsCtl.add(a), onAskGone: (id) => this.approvalsCtl.remove(id) });
|
|
715
724
|
slot.bridge = this.bridge;
|
|
716
725
|
}
|
|
@@ -1298,7 +1307,7 @@ export class OpenClawKit {
|
|
|
1298
1307
|
port: Number(readFileSync(join(this.engine.root, 'port'), 'utf8')), pluginId: this.o.plugin?.id ?? 'byokit',
|
|
1299
1308
|
pluginDir: join(this.engine.root, 'plugin'), policyPath: fileURLToPath(new URL('../policy/policy.mjs', import.meta.url)), app: patch,
|
|
1300
1309
|
offered: this.o.offered, installPolicy: this.o.installPolicy, plugins: installedPlugins(this.engine.root),
|
|
1301
|
-
...(this.o.browser ? { browser: this.browserConfig() } : {}) });
|
|
1310
|
+
...(this.o.browser ? { browser: this.browserConfig() } : {}), channels: this.o.channels });
|
|
1302
1311
|
if (this.o.browser && !browserToolPolicySafe(safe, this.toolNames()))
|
|
1303
1312
|
throw new Error('browser tool policy refused');
|
|
1304
1313
|
if (this.o.browser) {
|
|
@@ -1308,6 +1317,16 @@ export class OpenClawKit {
|
|
|
1308
1317
|
}
|
|
1309
1318
|
await this.request()('config.patch', { raw: JSON.stringify(safe), baseHash: response.hash, ...(o?.agentId ? { agentId: o.agentId } : {}) });
|
|
1310
1319
|
}
|
|
1320
|
+
/** Answers one chat on a turned-on channel (5.18), through the engine's own outbound send. */
|
|
1321
|
+
async reply(chat, text) {
|
|
1322
|
+
if (chat?.channel !== 'telegram' || !this.o.channels?.telegram)
|
|
1323
|
+
throw new Error(`channel is not turned on: ${String(chat?.channel)}`);
|
|
1324
|
+
if (typeof chat.id !== 'string' || !chat.id || typeof text !== 'string' || !text)
|
|
1325
|
+
throw new Error('reply needs a chat and text');
|
|
1326
|
+
// The engine needs one owning agent for an outbound send once several exist; the reply is the member's.
|
|
1327
|
+
const { agentId } = await this.ensureMember(this.o.channels.telegram.member);
|
|
1328
|
+
await this.request()('send', { channel: chat.channel, to: chat.id, message: text, agentId, idempotencyKey: randomUUID() });
|
|
1329
|
+
}
|
|
1311
1330
|
memoryLimited(member) {
|
|
1312
1331
|
const config = JSON.parse(readFileSync(join(this.engine.root, 'openclaw.json'), 'utf8'));
|
|
1313
1332
|
return configMemoryLimited(config, member);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type FakeTelegramCall = {
|
|
2
|
+
method: string;
|
|
3
|
+
body: Record<string, unknown>;
|
|
4
|
+
};
|
|
5
|
+
export type FakeTelegram = {
|
|
6
|
+
url: string;
|
|
7
|
+
calls: FakeTelegramCall[];
|
|
8
|
+
sent: {
|
|
9
|
+
chat: string;
|
|
10
|
+
text: string;
|
|
11
|
+
}[];
|
|
12
|
+
/** Queues one private message from user `from` to the bot; the next getUpdates returns it. */
|
|
13
|
+
message(from: string, text: string): void;
|
|
14
|
+
close(): Promise<void>;
|
|
15
|
+
};
|
|
16
|
+
export declare function fakeTelegram(o: {
|
|
17
|
+
token: string;
|
|
18
|
+
username?: string;
|
|
19
|
+
}): Promise<FakeTelegram>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// A loopback Telegram Bot API (5.18): the engine's Telegram channel long-polls it exactly as it would
|
|
2
|
+
// api.telegram.org, so a channel test needs no bot, no account and no network. Point `channels.telegram.apiRoot`
|
|
3
|
+
// at `url`. Only the token it was made with is accepted; every other token answers 401 as Telegram does.
|
|
4
|
+
import { createServer } from 'node:http';
|
|
5
|
+
export function fakeTelegram(o) {
|
|
6
|
+
const calls = [], sent = [];
|
|
7
|
+
const updates = [];
|
|
8
|
+
let waiters = [];
|
|
9
|
+
let nextUpdate = 1, nextMessage = 1;
|
|
10
|
+
const wake = () => { const w = waiters; waiters = []; for (const fn of w)
|
|
11
|
+
fn(); };
|
|
12
|
+
const server = createServer((req, res) => {
|
|
13
|
+
let raw = '';
|
|
14
|
+
req.on('data', chunk => { raw += chunk; });
|
|
15
|
+
req.on('end', async () => {
|
|
16
|
+
const answer = (status, body) => {
|
|
17
|
+
if (res.writableEnded)
|
|
18
|
+
return;
|
|
19
|
+
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify(body));
|
|
20
|
+
};
|
|
21
|
+
const match = /^\/bot([^/]+)\/([A-Za-z]+)$/.exec(new URL(req.url ?? '/', 'http://x').pathname);
|
|
22
|
+
if (!match || match[1] !== o.token)
|
|
23
|
+
return answer(401, { ok: false, error_code: 401, description: 'Unauthorized' });
|
|
24
|
+
let body = {};
|
|
25
|
+
try {
|
|
26
|
+
body = raw ? JSON.parse(raw) : {};
|
|
27
|
+
}
|
|
28
|
+
catch { /* a form body: no fields this fake reads */ }
|
|
29
|
+
const method = match[2];
|
|
30
|
+
calls.push({ method, body });
|
|
31
|
+
const ok = (result) => answer(200, { ok: true, result });
|
|
32
|
+
if (method === 'getMe')
|
|
33
|
+
return ok({ id: 7000001, is_bot: true, first_name: 'Chief', username: o.username ?? 'byokit_test_bot',
|
|
34
|
+
can_join_groups: false, can_read_all_group_messages: false, supports_inline_queries: false });
|
|
35
|
+
if (method === 'sendMessage') {
|
|
36
|
+
const chat = String(body.chat_id), text = String(body.text ?? '');
|
|
37
|
+
sent.push({ chat, text });
|
|
38
|
+
return ok({ message_id: nextMessage++, date: Math.floor(Date.now() / 1000), chat: { id: Number(chat), type: 'private' }, text });
|
|
39
|
+
}
|
|
40
|
+
if (method === 'getUpdates') {
|
|
41
|
+
const offset = Number(body.offset ?? 0);
|
|
42
|
+
const pending = () => updates.filter(u => u.update_id >= offset);
|
|
43
|
+
if (!pending().length && Number(body.timeout ?? 0) > 0) {
|
|
44
|
+
await new Promise(resolve => {
|
|
45
|
+
const timer = setTimeout(resolve, Math.min(Number(body.timeout), 30) * 1000);
|
|
46
|
+
waiters.push(() => { clearTimeout(timer); resolve(); });
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return ok(pending());
|
|
50
|
+
}
|
|
51
|
+
// deleteWebhook, setMyCommands, sendChatAction, ...: accepted with nothing to report.
|
|
52
|
+
return ok(true);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
server.once('error', reject);
|
|
57
|
+
server.listen(0, '127.0.0.1', () => {
|
|
58
|
+
const address = server.address();
|
|
59
|
+
const port = address && typeof address !== 'string' ? address.port : 0;
|
|
60
|
+
resolve({
|
|
61
|
+
url: `http://127.0.0.1:${port}`, calls, sent,
|
|
62
|
+
message(from, text) {
|
|
63
|
+
const id = Number(from), date = Math.floor(Date.now() / 1000);
|
|
64
|
+
updates.push({ update_id: nextUpdate++, message: { message_id: nextMessage++, date, text,
|
|
65
|
+
chat: { id, type: 'private', first_name: 'Umer' }, from: { id, is_bot: false, first_name: 'Umer' } } });
|
|
66
|
+
wake();
|
|
67
|
+
},
|
|
68
|
+
close: () => new Promise(done => { wake(); server.closeAllConnections(); server.close(() => done()); }),
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -2,3 +2,4 @@ export { fakeGateway, type FakeGateway, type FakeHandler, type FakeParams, type
|
|
|
2
2
|
export { fakeBrowserHost, createBrowserHostFake, browserHostContract, memorySignInStore, type FakeBrowserHost, type FakeBrowserHostOptions, type BrowserFixture } from './browser.ts';
|
|
3
3
|
export { openclawContract, type ContractFixture } from './contract.ts';
|
|
4
4
|
export { STUB_USAGE, startModelStub, useModelStub, releaseStub, stubHolding, toolCalls, type ModelStub, type ModelStubOptions, type StubCall, type StubRequest } from './model-stub.ts';
|
|
5
|
+
export { fakeTelegram, type FakeTelegram, type FakeTelegramCall } from './fake-telegram.ts';
|
package/dist/testing/index.js
CHANGED
|
@@ -3,3 +3,4 @@ export { fakeGateway } from './fake-gateway.js';
|
|
|
3
3
|
export { fakeBrowserHost, createBrowserHostFake, browserHostContract, memorySignInStore } from './browser.js';
|
|
4
4
|
export { openclawContract } from './contract.js';
|
|
5
5
|
export { STUB_USAGE, startModelStub, useModelStub, releaseStub, stubHolding, toolCalls } from './model-stub.js';
|
|
6
|
+
export { fakeTelegram } from './fake-telegram.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -258,6 +258,25 @@ export type CallOptions = {
|
|
|
258
258
|
timeoutMs?: number | null;
|
|
259
259
|
signal?: AbortSignal;
|
|
260
260
|
};
|
|
261
|
+
/** A chat on a channel the kit turned on (5.18): `id` is the channel's own chat id. `kit.reply` takes it back. */
|
|
262
|
+
export type ChannelChat = {
|
|
263
|
+
channel: 'telegram';
|
|
264
|
+
id: string;
|
|
265
|
+
};
|
|
266
|
+
/** One inbound channel message. The kit claims it before the engine dispatches, so no agent runs from a channel. */
|
|
267
|
+
export type ChannelMessage = {
|
|
268
|
+
chat: ChannelChat;
|
|
269
|
+
from: string;
|
|
270
|
+
text: string;
|
|
271
|
+
};
|
|
272
|
+
/** `KitOptions.channels.telegram` (5.18): a bot's direct messages from listed people, handed to the app only. */
|
|
273
|
+
export type TelegramChannel = {
|
|
274
|
+
token: string;
|
|
275
|
+
allowFrom: readonly string[];
|
|
276
|
+
member: Member;
|
|
277
|
+
apiRoot?: string;
|
|
278
|
+
onMessage(message: ChannelMessage): void;
|
|
279
|
+
};
|
|
261
280
|
/** Kit-owned invalidation only: no origin, URL, input value or lease secret. */
|
|
262
281
|
export type BrowserPing = {
|
|
263
282
|
member: Member;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byokit/openclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "The OpenClaw runtime kit: the pinned engine's full operator surface with plain-words helpers for members, sign-in, runs and approvals.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -60,12 +60,12 @@
|
|
|
60
60
|
"prepack": "tsc -b && node ../../scripts/fix-words-dts.cjs"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@byokit/accounts": "0.
|
|
63
|
+
"@byokit/accounts": "0.25.0",
|
|
64
64
|
"@byokit/pair": "0.10.2",
|
|
65
65
|
"@byokit/relay": "0.8.0",
|
|
66
|
-
"@byokit/discover": "0.8.
|
|
67
|
-
"@byokit/seal": "0.3.
|
|
68
|
-
"@byokit/secrets": "0.5.
|
|
66
|
+
"@byokit/discover": "0.8.4",
|
|
67
|
+
"@byokit/seal": "0.3.1",
|
|
68
|
+
"@byokit/secrets": "0.5.2",
|
|
69
69
|
"@openclaw/gateway-client": "2026.8.35",
|
|
70
70
|
"@openclaw/gateway-protocol": "2026.8.35",
|
|
71
71
|
"ws": "8.22.0"
|
package/plugin/index.js
CHANGED
|
@@ -18,6 +18,7 @@ const TOOLS = new Map(table.tools.map((t) => [t.name, t]));
|
|
|
18
18
|
// Absent (a table from before the flag) reads as true: fail closed.
|
|
19
19
|
const GATE_BUILTINS = table.gateBuiltins !== false;
|
|
20
20
|
const BROWSER = table.browser === true;
|
|
21
|
+
const CHANNELS = new Set(table.channels ?? []);
|
|
21
22
|
let browserCapabilities = [];
|
|
22
23
|
function scrubBrowserCapabilities(value) {
|
|
23
24
|
let serialized = JSON.stringify(value).replace(/\b(?:wss?|https?):\/\/[^\s"'<>\\]+/g, url => {
|
|
@@ -167,6 +168,15 @@ export default {
|
|
|
167
168
|
api.on('tool_result_persist', event => ({ message: scrubBrowserCapabilities(event.message) }));
|
|
168
169
|
api.on('before_message_write', event => ({ message: scrubBrowserCapabilities(event.message) }));
|
|
169
170
|
}
|
|
171
|
+
// A turned-on channel's messages belong to the app (5.18): claim each one before the engine dispatches it to any
|
|
172
|
+
// agent, hand it to the host without waiting, and stay claimed when the host is gone, so no agent ever runs from it.
|
|
173
|
+
if (CHANNELS.size) api.on('before_dispatch', (event, ctx) => {
|
|
174
|
+
const channel = ctx?.channelId ?? event?.channel;
|
|
175
|
+
if (!CHANNELS.has(channel)) return undefined;
|
|
176
|
+
void bridgeRequest({ kind: 'channel-message', channel, chat: ctx?.conversationId, from: event?.senderId ?? ctx?.senderId,
|
|
177
|
+
text: event?.content }, { timeoutMs: 10_000 }).catch(() => {});
|
|
178
|
+
return { handled: true };
|
|
179
|
+
});
|
|
170
180
|
for (const spec of table.tools) {
|
|
171
181
|
api.registerTool({
|
|
172
182
|
name: spec.name,
|