@byokit/openclaw 0.19.0 → 0.20.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 +5 -0
- package/README.md +34 -3
- package/dist/helpers.d.ts +25 -0
- package/dist/helpers.js +70 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/kit.d.ts +7 -1
- package/dist/kit.js +12 -0
- package/dist/runs.d.ts +2 -0
- package/dist/runs.js +16 -6
- package/dist/types.d.ts +10 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.20.0 (2026-10-19)
|
|
6
|
+
|
|
7
|
+
- FEAT: `ensureHelper(member, helper, { account, persona })` gives a member several helpers, each with its own persona and its own sessions (`agent:<member>:h-<helper>:…`) on one account the member already signed in, the API-key lane included: runs go to the engine agent already holding that account, so no credential is copied and no sign-in starts. `run({ helper })` speaks as that helper with its persona as the system text; `RunRef.helper` names it on `ToolHost.gate` and `call`, and its approvals stay the member's.
|
|
8
|
+
- SECURITY: helper requests fail closed with a typed `HelperError` before any engine request: an invalid id or persona, caller `system` text on a helper run, an account the member does not hold, a removed account (never moved to another sign-in), a session key outside the helper, a helper key run without `helper`, or `move` of a helper key. A helper gains no tool, builtin or session-key prefix the member's run would not.
|
|
9
|
+
|
|
5
10
|
## 0.19.0 (2026-10-19)
|
|
6
11
|
|
|
7
12
|
- FEAT: connect Composio through the pinned engine's native MCP OAuth flow, with browser URL handoff and native sign-out.
|
package/README.md
CHANGED
|
@@ -403,7 +403,7 @@ Entries:
|
|
|
403
403
|
|
|
404
404
|
| Export | What it does |
|
|
405
405
|
| --- | --- |
|
|
406
|
-
| `OpenClawKit` (`.`) | Prepares, starts, supervises and stops the engine. `call` / `callDynamic` pass through to the Gateway, `onEvent` listens. Helpers: `ensureMember`, `routes`, `providers`, `signedIn`, `signIn`, `signOut`, `removeKey`, `migrateRetainedLogin`, `confirmRetainedLogin`, `toolNames`, `run`, `steer`, `abort`, `approvals`, `onApproval`, `decide`, `allowOnce`, `disallowOnce`, `patchConfig`, `getConfigKey`, `setConfigKey`, `memoryLimited`, `doctorContext` |
|
|
406
|
+
| `OpenClawKit` (`.`) | Prepares, starts, supervises and stops the engine. `call` / `callDynamic` pass through to the Gateway, `onEvent` listens. Helpers: `ensureMember`, `ensureHelper`, `routes`, `providers`, `signedIn`, `signIn`, `signOut`, `removeKey`, `migrateRetainedLogin`, `confirmRetainedLogin`, `toolNames`, `run`, `steer`, `abort`, `approvals`, `onApproval`, `decide`, `allowOnce`, `disallowOnce`, `patchConfig`, `getConfigKey`, `setConfigKey`, `memoryLimited`, `doctorContext` |
|
|
407
407
|
| `ENGINE_VERSION`, `PROTOCOL_VERSION`, `OPERATOR_SCOPES` (`.`) | The pinned engine version, its protocol and the operator scopes the kit connects with |
|
|
408
408
|
| `KitOptions`, `RunSpec`, `RunEvent`, `RunEnd`, `RunUsage`, `PlanWindow`, `Route`, `Approval`, `Decision`, `ToolSpec`, `ToolHost`, `KitState`, ... (`.`) | Public types (docs/runtime-kits.md §5.2) |
|
|
409
409
|
| `GatewayMethods`, `GatewayMethod`, `GatewayParams`, `GatewayResult`, `GatewayEventName`, `GatewayEventPayload` (`.`) | The generated pass-through tables, so an app can write its own generic wrapper over `call` / `onEvent` once |
|
|
@@ -442,11 +442,42 @@ durably revokes its recorded subtree, closes parked gates/active calls, then ask
|
|
|
442
442
|
revokes its subtree. An engine resume with a different run ID, or a dispatch with no retained receipt, is not
|
|
443
443
|
silently granted authority. No prefix grants or `allowOnce` bypass are used.
|
|
444
444
|
|
|
445
|
-
**
|
|
446
|
-
|
|
445
|
+
**Targets keep their own sign-in:** a target retains its own engine credentials, persona, workspace and history; a
|
|
446
|
+
member's own helpers run on the member's account (below). This option never copies credentials or gives helpers
|
|
447
|
+
another person's account. Legacy
|
|
447
448
|
in-flight jobs may retain their exact old session keys and approval owners; all new helper work uses the new
|
|
448
449
|
mapping. Never copy agent SQLite or replay old sent actions. See the [binding contract](../../docs/runtime-kits.md#native-delegated-tool-admission-opt-in).
|
|
449
450
|
|
|
451
|
+
## Helpers on a member's account
|
|
452
|
+
|
|
453
|
+
A member can keep several helpers, each with its own persona and its own conversations, all on one account the
|
|
454
|
+
member already signed in. Nothing is copied and nobody signs in again: a helper's runs go to the engine agent that
|
|
455
|
+
already holds that account, under the helper's own session keys, with the helper's persona as the system text.
|
|
456
|
+
|
|
457
|
+
```ts
|
|
458
|
+
import { OpenClawKit, HelperError } from '@byokit/openclaw';
|
|
459
|
+
|
|
460
|
+
declare const kit: OpenClawKit;
|
|
461
|
+
const research = await kit.ensureHelper('umer', 'research', { account: 'openai', persona: 'You are Umer\'s research helper.' });
|
|
462
|
+
// research.sessionPrefix === 'agent:umer:h-research:'
|
|
463
|
+
const end = await kit.run({ member: 'umer', helper: 'research', sessionKey: `${research.sessionPrefix}main`,
|
|
464
|
+
message: 'Summarise the notes' });
|
|
465
|
+
try {
|
|
466
|
+
await kit.run({ member: 'umer', helper: 'ghost', sessionKey: 'agent:umer:h-ghost:main', message: 'hi' });
|
|
467
|
+
} catch (e) {
|
|
468
|
+
if (e instanceof HelperError) console.log(e.code, end.ok); // 'unknown_helper', before any request
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
`account` is one of `kit.accounts(member)`, the API-key lane (`'<provider>.key'`) included; a key-lane helper's run
|
|
473
|
+
names that account, as any paid run does. A helper's run is bound to its account (pick, rests and locks apply), and
|
|
474
|
+
`RunRef.helper` names it on `ToolHost.gate` and `call`; its approvals are the member's. The persona is the run's whole
|
|
475
|
+
system text: a helper run refuses `system`. Refusals are a typed `HelperError` before any engine request: `invalid`
|
|
476
|
+
(ids, persona, `system`), `unknown_account`, `bound` (another account), `unknown_helper` (not ensured, or its account
|
|
477
|
+
was removed: never moved to another sign-in) and `session` (a key outside the helper, a helper key without `helper`,
|
|
478
|
+
or `move` of one). Helpers share their account's engine agent (its id and workspace files); a helper gains no tool,
|
|
479
|
+
builtin or key prefix the member's run would not. See [Helpers](../../docs/runtime-kits.md#583-helpers).
|
|
480
|
+
|
|
450
481
|
## Owned browser (unqualified handoff)
|
|
451
482
|
|
|
452
483
|
`browser: { executablePath, members }` opts into isolated member Chromium profiles. It requires a closed
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type Binding } from './accounts.ts';
|
|
2
|
+
import type { Account, AccountId, Helper, Member } from './types.ts';
|
|
3
|
+
export declare const HELPER_ID: RegExp;
|
|
4
|
+
/** A session key tail of some helper: only a run naming that helper speaks there. */
|
|
5
|
+
export declare const HELPER_TAIL: RegExp;
|
|
6
|
+
export type HelperErrorCode = 'invalid' | 'unknown_account' | 'bound' | 'unknown_helper' | 'session';
|
|
7
|
+
/** A refused helper request, before any run: `code` says which rule refused it. */
|
|
8
|
+
export declare class HelperError extends Error {
|
|
9
|
+
readonly code: HelperErrorCode;
|
|
10
|
+
constructor(code: HelperErrorCode, message: string);
|
|
11
|
+
}
|
|
12
|
+
export declare function createHelpers(ctx: {
|
|
13
|
+
root: string;
|
|
14
|
+
list(member: Member): Promise<Account[]>;
|
|
15
|
+
}): {
|
|
16
|
+
ensure(member: Member, helper: string, o: {
|
|
17
|
+
account: AccountId;
|
|
18
|
+
persona: string;
|
|
19
|
+
}): Promise<Helper>;
|
|
20
|
+
resolve(member: Member, helper: string): {
|
|
21
|
+
agentId: string;
|
|
22
|
+
binding: Binding;
|
|
23
|
+
persona: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
package/dist/helpers.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Helpers (5.8.3): a member's personas on one account the member already signed in. A helper is its own session
|
|
2
|
+
// namespace `agent:<member>:h-<helper>:` run on the agent holding that account, with its own persona as the run's system
|
|
3
|
+
// text: the pin shares no sign-in between agents (a shared agentDir is refused, authInheritance is global), so no agent
|
|
4
|
+
// is made and nothing is copied. Its record is a kit-owned file; the engine is never asked.
|
|
5
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { loadIndex } from './accounts.js';
|
|
8
|
+
import { KEY_PREFIX, MEMBER_ID } from './members.js';
|
|
9
|
+
export const HELPER_ID = /^[a-z][a-z0-9-]{0,15}$/;
|
|
10
|
+
/** A session key tail of some helper: only a run naming that helper speaks there. */
|
|
11
|
+
export const HELPER_TAIL = /^h-[a-z][a-z0-9-]{0,15}:/;
|
|
12
|
+
const PERSONA_MAX = 8192;
|
|
13
|
+
/** A refused helper request, before any run: `code` says which rule refused it. */
|
|
14
|
+
export class HelperError extends Error {
|
|
15
|
+
code;
|
|
16
|
+
constructor(code, message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'HelperError';
|
|
19
|
+
this.code = code;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function createHelpers(ctx) {
|
|
23
|
+
const valid = (member, helper) => typeof member === 'string' && MEMBER_ID.test(member)
|
|
24
|
+
&& !member.startsWith(KEY_PREFIX) && typeof helper === 'string' && HELPER_ID.test(helper);
|
|
25
|
+
const path = (member, helper) => join(ctx.root, 'helpers', member, `${helper}.json`);
|
|
26
|
+
const read = (member, helper) => {
|
|
27
|
+
try {
|
|
28
|
+
const saved = JSON.parse(readFileSync(path(member, helper), 'utf8'));
|
|
29
|
+
return saved?.v === 1 && typeof saved.account === 'string' && typeof saved.agent === 'string'
|
|
30
|
+
&& typeof saved.persona === 'string' ? saved : undefined;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const view = (member, helper, saved) => ({ member, helper, account: saved.account, agentId: saved.agent, sessionPrefix: `agent:${member}:h-${helper}:` });
|
|
37
|
+
return {
|
|
38
|
+
async ensure(member, helper, o) {
|
|
39
|
+
const persona = o?.persona;
|
|
40
|
+
// The persona is the helper's whole system text: bounded, plain text (newlines and tabs only).
|
|
41
|
+
if (!valid(member, helper) || typeof persona !== 'string' || !persona.trim() || persona.length > PERSONA_MAX
|
|
42
|
+
|| /[\u0000-\u0008\u000b-\u001f\u007f]/.test(persona))
|
|
43
|
+
throw new HelperError('invalid', `invalid helper: ${String(member)}/${String(helper)}`);
|
|
44
|
+
if (!(await ctx.list(member)).some((row) => row.id === o.account))
|
|
45
|
+
throw new HelperError('unknown_account', `unknown account: ${String(o.account)}`);
|
|
46
|
+
const agent = loadIndex(ctx.root, member).file.accounts[o.account]?.agent;
|
|
47
|
+
if (!agent)
|
|
48
|
+
throw new HelperError('unknown_account', `unknown account: ${o.account}`);
|
|
49
|
+
const before = read(member, helper);
|
|
50
|
+
if (before && (before.account !== o.account || before.agent !== agent))
|
|
51
|
+
throw new HelperError('bound', `helper ${helper} uses another account`);
|
|
52
|
+
const saved = { v: 1, account: o.account, agent, persona };
|
|
53
|
+
const file = path(member, helper);
|
|
54
|
+
mkdirSync(join(file, '..'), { recursive: true, mode: 0o700 });
|
|
55
|
+
writeFileSync(`${file}.tmp`, JSON.stringify(saved), { mode: 0o600 });
|
|
56
|
+
renameSync(`${file}.tmp`, file);
|
|
57
|
+
return view(member, helper, saved);
|
|
58
|
+
},
|
|
59
|
+
// A run's helper, fail closed: ensured by this kit, on an account the member's index still names on the same agent.
|
|
60
|
+
// A removed account leaves the helper unusable; it never moves to another sign-in.
|
|
61
|
+
resolve(member, helper) {
|
|
62
|
+
const saved = valid(member, helper) ? read(member, helper) : undefined;
|
|
63
|
+
const entry = saved && loadIndex(ctx.root, member).file.accounts[saved.account];
|
|
64
|
+
if (!saved || entry?.agent !== saved.agent)
|
|
65
|
+
throw new HelperError('unknown_helper', `unknown helper: ${String(helper)}`);
|
|
66
|
+
return { agentId: saved.agent, persona: saved.persona,
|
|
67
|
+
binding: saved.agent === member ? { agentId: member } : { agentId: saved.agent, id: saved.account } };
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
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, 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, RunFrame, RunRef, DelegatedRun, DelegatedEvent, RunSpec, OutputSchema, SchemaOutput, RunUsage, ThinkingAsked, ThinkingReport, ThinkingUsed, SignInOptions, SignInView, TelegramChannel, 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, RunFrame, RunRef, Helper, DelegatedRun, DelegatedEvent, 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';
|
|
@@ -7,6 +7,7 @@ export { OpenClawKit, type KitOptions, type RetainedLogin } from './kit.ts';
|
|
|
7
7
|
export type { AddKeyHandle, AddKeyResult, KeyFailureReason } from './keys.ts';
|
|
8
8
|
export { stateWords, toAccountView, words, type AccountView, type WordKey } from './words.ts';
|
|
9
9
|
export { chooseAccount, consider, resolveSelection, type RoomOf } from './pick.ts';
|
|
10
|
+
export { HelperError, type HelperErrorCode } from './helpers.ts';
|
|
10
11
|
export { AuthStoreUnreadableError, AuthStoreSealSizeError } from './auth-store.ts';
|
|
11
12
|
export { EngineAlreadyRunningError } from './engine-status.ts';
|
|
12
13
|
export { PluginInstallError, type PluginInstallFailure } from './plugins.ts';
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export { ENGINE_VERSION, OPERATOR_SCOPES, PROTOCOL_VERSION } from './constants.j
|
|
|
4
4
|
export { OpenClawKit } from './kit.js';
|
|
5
5
|
export { stateWords, toAccountView, words } from './words.js';
|
|
6
6
|
export { chooseAccount, consider, resolveSelection } from './pick.js';
|
|
7
|
+
export { HelperError } from './helpers.js';
|
|
7
8
|
export { AuthStoreUnreadableError, AuthStoreSealSizeError } from './auth-store.js';
|
|
8
9
|
export { EngineAlreadyRunningError } from './engine-status.js';
|
|
9
10
|
export { PluginInstallError } from './plugins.js';
|
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, ChannelChat, TelegramChannel, ToolHost, ToolSpec } from './types.ts';
|
|
5
|
+
import type { Account, AccountId, AccountPick, Approval, CallOptions, Decision, Defaults, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, Hello, Helper, 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;
|
|
@@ -68,6 +68,7 @@ export declare class OpenClawKit {
|
|
|
68
68
|
attach(member: Member, runId: string, fromSeq: number, signal: AbortSignal): AsyncIterable<import("./types.ts").RunFrame>;
|
|
69
69
|
private installs;
|
|
70
70
|
private accountsCtl?;
|
|
71
|
+
private helpersCtl?;
|
|
71
72
|
private readonly bridge;
|
|
72
73
|
private readonly delegations;
|
|
73
74
|
private readonly keys;
|
|
@@ -129,6 +130,11 @@ export declare class OpenClawKit {
|
|
|
129
130
|
agentId: string;
|
|
130
131
|
workspace: string;
|
|
131
132
|
}>;
|
|
133
|
+
/** One of the member's helpers (5.8.3): its own persona and sessions on the member's `account`, no new sign-in. */
|
|
134
|
+
ensureHelper(member: Member, helper: string, o: {
|
|
135
|
+
account: AccountId;
|
|
136
|
+
persona: string;
|
|
137
|
+
}): Promise<Helper>;
|
|
132
138
|
private signInCtx;
|
|
133
139
|
routes(): RouteView[];
|
|
134
140
|
/** Start Composio's hosted MCP OAuth flow through the pinned engine; return the URL for the app to open. */
|
package/dist/kit.js
CHANGED
|
@@ -14,6 +14,7 @@ import { Engine, canonicalStateDir, refusedOnly } from './engine.js';
|
|
|
14
14
|
import { AuthStoreSealSizeError } from './auth-store.js';
|
|
15
15
|
import { gatewayTransport } from './transport.js';
|
|
16
16
|
import { createMembers } from './members.js';
|
|
17
|
+
import { createHelpers, HELPER_TAIL, HelperError } from './helpers.js';
|
|
17
18
|
import { createAccounts, memberOfAccount } from './accounts.js';
|
|
18
19
|
import { createLocks } from './locks.js';
|
|
19
20
|
import { keyAgentId, KEY_PREFIX, MEMBER_ID } from './members.js';
|
|
@@ -448,6 +449,7 @@ export class OpenClawKit {
|
|
|
448
449
|
}
|
|
449
450
|
installs = Promise.resolve();
|
|
450
451
|
accountsCtl;
|
|
452
|
+
helpersCtl;
|
|
451
453
|
// One bridge and one approval surface per kit: listeners and run registrations survive reconnects (B3).
|
|
452
454
|
// Only the socket itself is per-connection. Approvals always reaches the live transport through this.request().
|
|
453
455
|
bridge;
|
|
@@ -833,6 +835,7 @@ export class OpenClawKit {
|
|
|
833
835
|
ensure: (member) => this.ensureMember(member), locks: this.locks,
|
|
834
836
|
signIn: (agentId, o, on, fresh) => startSignIn({ ...this.signInCtx(), ensure: async () => ({ agentId }), fresh }, agentId, o, on),
|
|
835
837
|
routes: this.routeMap, log: this.o.log, reseal: () => this.engine.authStore.reseal() });
|
|
838
|
+
this.helpersCtl = createHelpers({ root: this.engine.root, list: (member) => this.accounts(member) });
|
|
836
839
|
// The fail-closed tool bridge feeds the kit's single Approval surface (5.9, O5).
|
|
837
840
|
await Promise.race([this.bridge.start(), exit]);
|
|
838
841
|
this.off.push(transport.onEvent((e) => this.approvalsCtl.handleEvent(e)));
|
|
@@ -1068,6 +1071,12 @@ export class OpenClawKit {
|
|
|
1068
1071
|
await attaching;
|
|
1069
1072
|
return result;
|
|
1070
1073
|
}
|
|
1074
|
+
/** One of the member's helpers (5.8.3): its own persona and sessions on the member's `account`, no new sign-in. */
|
|
1075
|
+
ensureHelper(member, helper, o) {
|
|
1076
|
+
if (!this.helpersCtl)
|
|
1077
|
+
return Promise.reject(new Error('gateway not ready'));
|
|
1078
|
+
return this.helpersCtl.ensure(member, helper, o);
|
|
1079
|
+
}
|
|
1071
1080
|
// sign-in (5.7): every body here is the module's, with this kit's transport, members and ports.
|
|
1072
1081
|
signInCtx() {
|
|
1073
1082
|
return { request: (method, params, o) => this.request()(method, params, o),
|
|
@@ -1226,6 +1235,8 @@ export class OpenClawKit {
|
|
|
1226
1235
|
move(ref, to) {
|
|
1227
1236
|
if (!this.accountsCtl)
|
|
1228
1237
|
return Promise.reject(new Error('gateway not ready'));
|
|
1238
|
+
if (HELPER_TAIL.test(String(ref.sessionKey).replace(/^agent:[^:]+:/, '')))
|
|
1239
|
+
return Promise.reject(new HelperError('session', 'refused: a helper\'s session stays on its account'));
|
|
1229
1240
|
return this.accountsCtl.move(ref.member, ref.sessionKey, to);
|
|
1230
1241
|
}
|
|
1231
1242
|
/** Sign in one more account: on the member agent while it holds none of the provider, else on a new sealed agent. */
|
|
@@ -1347,6 +1358,7 @@ export class OpenClawKit {
|
|
|
1347
1358
|
bridge: { register: (run, tools, runId) => this.bridge.register(run, tools && this.o.browser ? [...tools, 'request_sign_in'] : tools, runId) },
|
|
1348
1359
|
tools: new Set(this.toolNames()), delegations: this.delegations,
|
|
1349
1360
|
...(this.accountsCtl ? { accounts: this.accountsCtl } : {}),
|
|
1361
|
+
...(this.helpersCtl ? { helpers: this.helpersCtl } : {}),
|
|
1350
1362
|
locks: this.locks,
|
|
1351
1363
|
routes: this.routeMap,
|
|
1352
1364
|
});
|
package/dist/runs.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Bridge } from './bridge.ts';
|
|
2
|
+
import { type createHelpers } from './helpers.ts';
|
|
2
3
|
import type { Delegations } from './delegations.ts';
|
|
3
4
|
import type { createAccounts } from './accounts.ts';
|
|
4
5
|
import type { createLocks } from './locks.ts';
|
|
@@ -40,6 +41,7 @@ export declare function createRuns(ctx: {
|
|
|
40
41
|
}>;
|
|
41
42
|
bridge: Pick<Bridge, 'register'>;
|
|
42
43
|
delegations?: Delegations;
|
|
44
|
+
helpers?: Pick<ReturnType<typeof createHelpers>, 'resolve'>;
|
|
43
45
|
tools: ReadonlySet<string>;
|
|
44
46
|
accounts?: Pick<ReturnType<typeof createAccounts>, 'bind' | 'keyRefusal' | 'pick' | 'rest'>;
|
|
45
47
|
locks?: ReturnType<typeof createLocks>;
|
package/dist/runs.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Runs: one Gateway run per spec, streamed through typed events, ending in a classified RunEnd (5.8, O8).
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
|
+
import { HELPER_TAIL, HelperError } from './helpers.js';
|
|
4
5
|
import { authStatus } from './auth-status.js';
|
|
5
6
|
import { outputSchema } from './output.js';
|
|
6
7
|
import { words } from './words.js';
|
|
@@ -163,6 +164,14 @@ export function createRuns(ctx) {
|
|
|
163
164
|
// of one of the member's account agents is the member's too; binding below checks the index names it.
|
|
164
165
|
if (!spec.sessionKey.startsWith(`agent:${spec.member}:`) && !(ctx.accounts && !keyAgent && spec.sessionKey.startsWith(`agent:${spec.member}--`)))
|
|
165
166
|
throw new Error(`refused: "${spec.sessionKey}" is not a session of member "${spec.member}"`);
|
|
167
|
+
const tail = spec.sessionKey.slice(spec.sessionKey.indexOf(':', 'agent:'.length) + 1);
|
|
168
|
+
// A helper (5.8.3) speaks only under `agent:<member>:h-<helper>:`, with its persona as the whole system text, and
|
|
169
|
+
// only a run naming that helper speaks there. Refused before any request.
|
|
170
|
+
if (spec.helper !== undefined && (!ctx.helpers || keyAgent || spec.system !== undefined))
|
|
171
|
+
throw new HelperError('invalid', `refused: helper ${String(spec.helper)} runs on its own account with its own persona`);
|
|
172
|
+
const helper = spec.helper === undefined ? undefined : ctx.helpers.resolve(spec.member, spec.helper);
|
|
173
|
+
if (helper ? !spec.sessionKey.startsWith(`agent:${spec.member}:h-${spec.helper}:`) : HELPER_TAIL.test(tail))
|
|
174
|
+
throw new HelperError('session', `refused: "${spec.sessionKey}" is not a session of ${helper ? `helper "${spec.helper}"` : 'this run'}`);
|
|
166
175
|
if (spec.delegates) {
|
|
167
176
|
if (spec.register === false || !ctx.delegations)
|
|
168
177
|
throw new Error('delegation requires a registered run');
|
|
@@ -173,16 +182,16 @@ export function createRuns(ctx) {
|
|
|
173
182
|
throw new Error('refused: idempotencyKey must be a non-empty string');
|
|
174
183
|
const idempotencyKey = spec.idempotencyKey ?? randomUUID();
|
|
175
184
|
const output = preparedOutput ?? (spec.schema === undefined ? undefined : outputSchema(spec.schema));
|
|
176
|
-
const system = [spec.system, output?.prompt].filter(Boolean).join('\n\n');
|
|
185
|
+
const system = [helper?.persona ?? spec.system, output?.prompt].filter(Boolean).join('\n\n');
|
|
177
186
|
const picked = spec.model === undefined ? undefined : account(spec.model);
|
|
178
187
|
for (const need of spec.needs ?? [])
|
|
179
188
|
account(need);
|
|
180
189
|
for (const tool of spec.tools ?? [])
|
|
181
190
|
if (!ctx.tools.has(tool))
|
|
182
191
|
throw new Error(`refused: "${tool}" is not one of this kit's tools`);
|
|
183
|
-
const tail = spec.sessionKey.slice(spec.sessionKey.indexOf(':', 'agent:'.length) + 1);
|
|
184
192
|
// Runs step 1 (5.15): every run first finds the account its session is bound to, the API-key lane included.
|
|
185
|
-
|
|
193
|
+
// A helper is bound to the agent whose sign-in it reads, as a conversation held there is.
|
|
194
|
+
const binding = helper?.binding ?? (ctx.accounts ? await ctx.accounts.bind(spec.member, spec.sessionKey) : undefined);
|
|
186
195
|
// Step 2 on the API-key lane: its own lane binding continues; any other binding is refused.
|
|
187
196
|
if (keyAgent && ctx.accounts && binding) {
|
|
188
197
|
const message = await ctx.accounts.keyRefusal(spec.member, binding);
|
|
@@ -191,7 +200,7 @@ export function createRuns(ctx) {
|
|
|
191
200
|
}
|
|
192
201
|
// A run that waited for its agent's lock across a move (5.15) binds again under it: a changed binding means its
|
|
193
202
|
// session went to another account, so it ends before any event and the caller's retry lands there.
|
|
194
|
-
const moved = async () => !!binding && (await ctx.accounts.bind(spec.member, spec.sessionKey))?.agentId !== binding.agentId;
|
|
203
|
+
const moved = async () => !helper && !!binding && (await ctx.accounts.bind(spec.member, spec.sessionKey))?.agentId !== binding.agentId;
|
|
195
204
|
const movedEnd = { ok: false, kind: 'other', message: words('run.moved') };
|
|
196
205
|
// Step 3: with `account` and `needs` both absent, on a key no account agent holds (or an API-key run, which binds only
|
|
197
206
|
// to its lane), exactly the run on the agent it always was (a bound key under its shared lock).
|
|
@@ -325,8 +334,9 @@ export function createRuns(ctx) {
|
|
|
325
334
|
}
|
|
326
335
|
});
|
|
327
336
|
try {
|
|
328
|
-
|
|
329
|
-
|
|
337
|
+
const ref = { sessionKey, member: spec.member, ...(spec.helper !== undefined ? { helper: spec.helper } : {}) };
|
|
338
|
+
releaseDelegation = spec.delegates ? ctx.delegations.register(ref, idempotencyKey, spec.tools, spec.delegates, spec.sessionKey) : undefined;
|
|
339
|
+
release = spec.register !== false ? ctx.bridge.register(ref, spec.tools, idempotencyKey) : undefined;
|
|
330
340
|
if (picked && !chosen) {
|
|
331
341
|
// The picked account must be the one called: check it is signed in for this member before the run, since
|
|
332
342
|
// the engine would otherwise fail the run only after admitting it. An explicit provider/model is strict on
|
package/dist/types.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface ToolSpec {
|
|
|
19
19
|
export interface RunRef {
|
|
20
20
|
sessionKey: string;
|
|
21
21
|
member: Member;
|
|
22
|
+
helper?: string;
|
|
22
23
|
meta?: unknown;
|
|
23
24
|
runId?: string;
|
|
24
25
|
agentId?: string;
|
|
@@ -31,6 +32,15 @@ export interface RunRef {
|
|
|
31
32
|
runId: string;
|
|
32
33
|
};
|
|
33
34
|
}
|
|
35
|
+
/** One of a member's helpers (5.8.3): its own persona and sessions (`sessionPrefix`) on `account`, run by `agentId`, the
|
|
36
|
+
* engine agent already holding that sign-in. */
|
|
37
|
+
export type Helper = {
|
|
38
|
+
member: Member;
|
|
39
|
+
helper: string;
|
|
40
|
+
account: AccountId;
|
|
41
|
+
agentId: string;
|
|
42
|
+
sessionPrefix: string;
|
|
43
|
+
};
|
|
34
44
|
export type DelegatedRun = RunRef & Required<Pick<RunRef, 'runId' | 'agentId' | 'parent' | 'root'>>;
|
|
35
45
|
export type DelegatedEvent = {
|
|
36
46
|
type: 'admitted' | 'event' | 'ended';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byokit/openclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.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",
|