@byokit/openclaw 0.1.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/LICENSE +202 -0
  3. package/README.md +174 -0
  4. package/dist/approvals.d.ts +29 -0
  5. package/dist/approvals.js +200 -0
  6. package/dist/bridge.d.ts +78 -0
  7. package/dist/bridge.js +345 -0
  8. package/dist/classify.d.ts +4 -0
  9. package/dist/classify.js +14 -0
  10. package/dist/config.d.ts +13 -0
  11. package/dist/config.js +74 -0
  12. package/dist/constants.d.ts +3 -0
  13. package/dist/constants.js +13 -0
  14. package/dist/device.d.ts +63 -0
  15. package/dist/device.js +192 -0
  16. package/dist/engine.d.ts +43 -0
  17. package/dist/engine.js +212 -0
  18. package/dist/generated/events.d.ts +61 -0
  19. package/dist/generated/events.js +1 -0
  20. package/dist/generated/methods.d.ts +2368 -0
  21. package/dist/generated/methods.js +1 -0
  22. package/dist/generated/params.d.ts +1717 -0
  23. package/dist/generated/params.js +5 -0
  24. package/dist/generated/report.json +1356 -0
  25. package/dist/index.d.ts +4 -0
  26. package/dist/index.js +5 -0
  27. package/dist/kit.d.ts +108 -0
  28. package/dist/kit.js +655 -0
  29. package/dist/link.d.ts +24 -0
  30. package/dist/link.js +273 -0
  31. package/dist/members.d.ts +11 -0
  32. package/dist/members.js +23 -0
  33. package/dist/migrate.d.ts +22 -0
  34. package/dist/migrate.js +118 -0
  35. package/dist/notices.d.ts +9 -0
  36. package/dist/notices.js +75 -0
  37. package/dist/routes.d.ts +9 -0
  38. package/dist/routes.js +15 -0
  39. package/dist/routes.json +398 -0
  40. package/dist/runs.d.ts +14 -0
  41. package/dist/runs.js +110 -0
  42. package/dist/signin.d.ts +24 -0
  43. package/dist/signin.js +251 -0
  44. package/dist/testing/contract.d.ts +12 -0
  45. package/dist/testing/contract.js +250 -0
  46. package/dist/testing/fake-gateway.d.ts +19 -0
  47. package/dist/testing/fake-gateway.js +378 -0
  48. package/dist/testing/index.d.ts +3 -0
  49. package/dist/testing/index.js +4 -0
  50. package/dist/testing/model-stub.d.ts +29 -0
  51. package/dist/testing/model-stub.js +182 -0
  52. package/dist/transport.d.ts +22 -0
  53. package/dist/transport.js +94 -0
  54. package/dist/types.d.ts +125 -0
  55. package/dist/types.js +2 -0
  56. package/dist/words.d.ts +12 -0
  57. package/dist/words.js +26 -0
  58. package/dist/words.json +20 -0
  59. package/engine/package-lock.json +4680 -0
  60. package/engine/package.json +1 -0
  61. package/package.json +68 -0
  62. package/plugin/index.js +183 -0
  63. package/plugin/package.json +1 -0
  64. package/policy/policy.mjs +197 -0
package/dist/signin.js ADDED
@@ -0,0 +1,251 @@
1
+ // Sign-in: the kit drives OpenClaw's own setup wizard loop and holds the ChatGPT callback port during a browser
2
+ // sign-in; credentials never pass through kit or app (D11, 5.7).
3
+ import { randomUUID } from 'node:crypto';
4
+ import { createServer } from 'node:http';
5
+ import { words } from "./words.js";
6
+ // The pin's wizard contract (2026.8.1): steps arrive only from `wizard.next` (wizard.status answers {status, error}
7
+ // and never carries a step), and one session at a time holds the gateway's single setup admission.
8
+ const BUSY = /setup is already in progress|SETUP_ADMISSION_BUSY/i;
9
+ const START_MS = 60_000;
10
+ const PULL_MS = 120_000;
11
+ const STATUS_MS = 20_000;
12
+ const CANCEL_MS = 10_000;
13
+ const TURNS = 200;
14
+ const PASTE_MS = 15 * 60_000;
15
+ const cut = (value) => (value instanceof Error ? value.message : String(value)).slice(0, 200);
16
+ const pullOptions = (signal) => ({ timeoutMs: PULL_MS, signal });
17
+ /**
18
+ * Drive the engine's provider-owned login. `on` sees every view the person watches; `done` resolves once, and
19
+ * `paste` hands a returned browser address to a waiting wizard text step. Every exit short of done cancels this
20
+ * session and its callback listener only.
21
+ */
22
+ export function signIn(ctx, member, o, on) {
23
+ const via = o.via ?? 'browser';
24
+ const owner = new AbortController();
25
+ const { signal } = owner;
26
+ const wantsCallback = via === 'browser' && o.authChoice === 'openai';
27
+ let server;
28
+ let sessionId = '';
29
+ let starting;
30
+ let releasing;
31
+ let settleStart;
32
+ const startingSettled = new Promise((resolve) => { settleStart = resolve; });
33
+ let over = false;
34
+ let settle;
35
+ let pasteIn;
36
+ let returned;
37
+ const done = new Promise((resolve) => { settle = resolve; });
38
+ const say = (v) => on({ state: 'waiting', via, ...v });
39
+ const closeCallback = async () => {
40
+ const held = server;
41
+ server = undefined;
42
+ if (held)
43
+ await new Promise((closed) => held.close(() => closed()));
44
+ };
45
+ const finish = async (view) => {
46
+ if (over)
47
+ return;
48
+ over = true;
49
+ owner.abort();
50
+ await closeCallback();
51
+ on(view);
52
+ settle(view);
53
+ };
54
+ const release = () => {
55
+ releasing ??= (async () => {
56
+ // A cancel never goes out early: the pinned engine holds one setup admission per gateway and registers the
57
+ // session only as `openclaw.setup.auth.start` settles, so a cancel sent sooner is answered `wizard not found`
58
+ // and leaves the person locked out of signing in. Waiting for that settlement is what frees the admission.
59
+ await startingSettled;
60
+ if (sessionId)
61
+ await ctx.request('wizard.cancel', { sessionId }, { timeoutMs: CANCEL_MS }).catch(() => { });
62
+ })();
63
+ return releasing;
64
+ };
65
+ const paste = (text) => {
66
+ if (pasteIn)
67
+ pasteIn(text);
68
+ else
69
+ returned = text; // the browser came back before the wizard asked; held for the text step
70
+ };
71
+ const cancel = () => {
72
+ // Stop the drive now; the start itself is never aborted, and release waits for it to settle before cancelling,
73
+ // so the gateway's one setup admission is freed by a cancel that the engine can actually find.
74
+ owner.abort();
75
+ void (async () => { await release(); await finish({ state: 'failed', via, why: 'declined' }); })();
76
+ };
77
+ /** Hold 127.0.0.1:<callbackPort> for the sign-in's life; a taken port is why the sign-in cannot start. */
78
+ const holdCallback = () => {
79
+ let took = false;
80
+ const held = createServer((req, res) => {
81
+ const host = req.headers.host ?? `localhost:${ctx.callbackPort}`;
82
+ const address = `http://${host}${req.url ?? ''}`;
83
+ let url;
84
+ try {
85
+ url = new URL(address);
86
+ }
87
+ catch {
88
+ url = new URL(`http://${host}/`);
89
+ }
90
+ // Only the provider's own callback is pasted: a favicon or any other local page is not a redirect back.
91
+ if (took || req.method !== 'GET' || url.pathname !== '/auth/callback' || !url.searchParams.has('code')) {
92
+ res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' });
93
+ res.end('Not found');
94
+ return;
95
+ }
96
+ took = true;
97
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
98
+ res.end('<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width">' +
99
+ `<title>Sign-in</title><body style="font:18px system-ui;margin:3em auto;max-width:26em;padding:0 1em;text-align:center">${words('signin.returned')}</body>`);
100
+ paste(address);
101
+ });
102
+ return new Promise((open, refuse) => {
103
+ held.once('error', refuse);
104
+ held.listen(ctx.callbackPort, '127.0.0.1', () => { held.off('error', refuse); held.on('error', () => { }); open(held); });
105
+ });
106
+ };
107
+ const drive = async () => {
108
+ if (wantsCallback) {
109
+ server = await holdCallback().catch(() => undefined);
110
+ if (!server)
111
+ return { state: 'failed', via, why: 'busy' };
112
+ }
113
+ const { agentId } = await ctx.ensure(member);
114
+ // The session id is the client's to choose (5.7), and the start request carries no abort signal: the engine
115
+ // registers the session as it settles, so a cancel waits for it (release) rather than racing it.
116
+ sessionId = `byokit-${randomUUID()}`;
117
+ let started;
118
+ try {
119
+ starting = ctx.request('openclaw.setup.auth.start', { sessionId, agentId, authChoice: o.authChoice }, { timeoutMs: START_MS });
120
+ started = await starting;
121
+ }
122
+ finally {
123
+ settleStart();
124
+ }
125
+ if (started.done)
126
+ return { state: 'done', via };
127
+ // wizard.next, never wizard.status: the status method answers {status, error} and carries no step.
128
+ const pull = () => ctx.request('wizard.next', { sessionId }, pullOptions(signal));
129
+ const answer = (step, value) => ctx.request('wizard.next', { sessionId, answer: { stepId: step.id, ...(value === undefined ? {} : { value }) } }, pullOptions(signal));
130
+ const waitForPaste = () => new Promise((resolve) => {
131
+ if (returned !== undefined) {
132
+ const text = returned;
133
+ returned = undefined;
134
+ resolve(text);
135
+ return;
136
+ }
137
+ let timer;
138
+ let settled = false;
139
+ const stop = (text) => {
140
+ if (settled)
141
+ return;
142
+ settled = true;
143
+ if (timer)
144
+ clearTimeout(timer);
145
+ signal.removeEventListener('abort', onAbort);
146
+ pasteIn = undefined;
147
+ resolve(text);
148
+ };
149
+ const onAbort = () => stop(undefined);
150
+ timer = setTimeout(() => stop(undefined), PASTE_MS);
151
+ pasteIn = (text) => stop(text);
152
+ signal.addEventListener('abort', onAbort, { once: true });
153
+ });
154
+ let step;
155
+ let terminal;
156
+ for (let turns = 0; turns < TURNS && !signal.aborted; turns++) {
157
+ if (!step) {
158
+ const next = await pull();
159
+ if (next.done) {
160
+ terminal = next;
161
+ break;
162
+ }
163
+ step = next.step;
164
+ continue;
165
+ }
166
+ const current = step;
167
+ step = undefined;
168
+ if (current.deviceCode) {
169
+ // The code is on the card; acknowledging it lets the engine poll the provider itself.
170
+ say({ ...(current.deviceCode.code ? { code: current.deviceCode.code } : {}), ...(current.externalUrl ? { url: current.externalUrl } : {}) });
171
+ const next = await answer(current);
172
+ if (next.done) {
173
+ terminal = next;
174
+ break;
175
+ }
176
+ step = next.step;
177
+ continue;
178
+ }
179
+ if (current.type === 'text' && !current.sensitive) {
180
+ const value = await waitForPaste();
181
+ if (value === undefined)
182
+ return { state: 'failed', via, why: signal.aborted ? 'declined' : 'expired' };
183
+ const next = await answer(current, value);
184
+ if (next.done) {
185
+ terminal = next;
186
+ break;
187
+ }
188
+ step = next.step;
189
+ if (next.error)
190
+ say({ error: cut(next.error) });
191
+ continue;
192
+ }
193
+ if (current.type === 'note' || current.type === 'confirm' || current.type === 'select' || current.type === 'action') {
194
+ if (current.externalUrl)
195
+ say({ url: current.externalUrl });
196
+ const next = await answer(current);
197
+ if (next.done) {
198
+ terminal = next;
199
+ break;
200
+ }
201
+ step = next.step;
202
+ if (next.error)
203
+ say({ error: cut(next.error) });
204
+ continue;
205
+ }
206
+ // progress and anything gateway-driven: it advances by itself
207
+ const next = await pull();
208
+ if (next.done) {
209
+ terminal = next;
210
+ break;
211
+ }
212
+ step = next.step;
213
+ }
214
+ if (signal.aborted)
215
+ return { state: 'failed', via, why: 'declined' };
216
+ if (!terminal)
217
+ return { state: 'failed', via, why: 'expired' };
218
+ const error = String(terminal.error ?? (terminal.status === 'error' ? 'Sign-in failed' : ''));
219
+ return error ? { state: 'failed', via, why: 'failed', error: cut(error) } : { state: 'done', via };
220
+ };
221
+ void (async () => {
222
+ let view;
223
+ try {
224
+ view = await drive();
225
+ }
226
+ catch (error) {
227
+ if (signal.aborted)
228
+ view = { state: 'failed', via, why: 'declined' };
229
+ else if (BUSY.test(cut(error)))
230
+ view = { state: 'failed', via, why: 'busy' };
231
+ else
232
+ view = { state: 'failed', via, why: 'failed', error: cut(error) };
233
+ }
234
+ settleStart();
235
+ if (view.state !== 'done')
236
+ await release();
237
+ await finish(view);
238
+ })();
239
+ return { paste, cancel, done };
240
+ }
241
+ /** The providers this member's engine reports as signed in, from `models.authStatus` (a string or `{ provider }`). */
242
+ export async function providers(ctx, member, refresh) {
243
+ const { agentId } = await ctx.ensure(member);
244
+ const status = await ctx.request('models.authStatus', { agentId, ...(refresh ? { refresh: true } : {}) }, { timeoutMs: STATUS_MS });
245
+ const names = (status?.providers ?? []).map((entry) => (typeof entry === 'string' ? entry : entry?.provider));
246
+ return [...new Set(names.filter((name) => typeof name === 'string' && name !== ''))];
247
+ }
248
+ export async function signOut(ctx, member, provider) {
249
+ const { agentId } = await ctx.ensure(member);
250
+ await ctx.request('models.authLogout', { provider, agentId }, { timeoutMs: STATUS_MS });
251
+ }
@@ -0,0 +1,12 @@
1
+ import type { OpenClawKit } from '../kit.ts';
2
+ import { type ModelStub } from './model-stub.ts';
3
+ export type ContractFixture = {
4
+ kit: OpenClawKit;
5
+ model?: ModelStub;
6
+ peer?: {
7
+ request(method: string, params?: unknown): Promise<unknown>;
8
+ };
9
+ };
10
+ export declare function openclawContract(make: () => Promise<ContractFixture>, o?: {
11
+ skipDeviceCode?: string;
12
+ }): void;
@@ -0,0 +1,250 @@
1
+ // The contract suite (4.5, 5.11): the same assertions run against the fake in npm test and against the real pinned
2
+ // engine in the engine job. Every case drives only the kit's public surface, so a pass on the fake and a pass on the
3
+ // engine mean the same thing. O11 wires the calls; the cases are not run before then.
4
+ //
5
+ // `make` must return a started kit with the tool `note` registered and a host whose gate, for every tool (engine
6
+ // builtins included), asks for `{ mode: 'ask', ... }` input, denies `{ mode: 'deny', ... }` input and allows the rest,
7
+ // and whose calls answer `note: <input.text>`. An optional model is a started model stub the kit was pointed at with `useModelStub`.
8
+ import { test } from 'node:test';
9
+ import assert from 'node:assert/strict';
10
+ import { PROTOCOL_VERSION } from "../constants.js";
11
+ import { GATEWAY_CAPS } from "../transport.js";
12
+ import { releaseStub, stubHolding } from "./model-stub.js";
13
+ // The generated method table arrives with O2; until then kit.call's typed surface accepts no method names, so the
14
+ // suite drives pass-through through this string-typed view of the same runtime path.
15
+ const call = (kit, method, params) => kit.call(method, params);
16
+ const REQUIRED_METHODS = [
17
+ 'agent', 'agent.wait', 'agents.create', 'agents.list', 'chat.abort', 'config.get', 'config.patch',
18
+ 'exec.approval.request', 'exec.approval.resolve', 'health', 'models.authStatus', 'openclaw.setup.auth.start',
19
+ 'plugin.approval.resolve', 'question.resolve', 'wizard.cancel', 'wizard.next',
20
+ ];
21
+ // Driven by the kit but not advertised in hello (O11 probe, e.g. sessions.steer): routable, just not listed.
22
+ // The engine job's generated.test.ts audits the full generated table against hello instead of asserting it here.
23
+ const REQUIRED_EVENTS = [
24
+ 'agent',
25
+ 'exec.approval.requested', 'exec.approval.resolved',
26
+ 'plugin.approval.requested', 'plugin.approval.resolved',
27
+ 'question.requested', 'question.resolved',
28
+ ];
29
+ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
30
+ async function until(there, ms = 5_000) {
31
+ for (let waited = 0; waited < ms; waited += 50) {
32
+ if (there())
33
+ return;
34
+ await wait(50);
35
+ }
36
+ throw new Error('the expected state never arrived');
37
+ }
38
+ export function openclawContract(make, o) {
39
+ test('contract: hello carries the protocol and every method and event the kit drives', async () => {
40
+ const { kit } = await make();
41
+ try {
42
+ const hello = kit.hello;
43
+ assert.ok(hello, 'the kit has no hello');
44
+ assert.equal(hello.protocol, PROTOCOL_VERSION);
45
+ // The connection must be an approval client or native approvals never arrive (B7).
46
+ assert.ok(GATEWAY_CAPS.includes('approvals'), 'operator caps lack approvals');
47
+ for (const method of REQUIRED_METHODS)
48
+ assert.ok(hello.methods.includes(method), `hello is missing ${method}`);
49
+ for (const event of REQUIRED_EVENTS)
50
+ assert.ok(hello.events.includes(event), `hello is missing ${event}`);
51
+ }
52
+ finally {
53
+ await kit.stop();
54
+ }
55
+ });
56
+ test('contract: members are created, cached and validated', async () => {
57
+ const { kit } = await make();
58
+ try {
59
+ const first = await kit.ensureMember('m1');
60
+ assert.equal(first.agentId, 'm1');
61
+ const again = await kit.ensureMember('m1');
62
+ assert.equal(again.agentId, 'm1');
63
+ await assert.rejects(kit.ensureMember('M1'));
64
+ }
65
+ finally {
66
+ await kit.stop();
67
+ }
68
+ });
69
+ test('contract: a member cannot run in another member\'s session', async () => {
70
+ const { kit } = await make();
71
+ try {
72
+ await kit.ensureMember('m1');
73
+ await assert.rejects(kit.run({ member: 'm1', sessionKey: 'agent:m2:other:1', message: 'hello' }));
74
+ }
75
+ finally {
76
+ await kit.stop();
77
+ }
78
+ });
79
+ // The device-code happy path needs a person at the device URL (the fake auto-completes it, 5.11).
80
+ // Unattended runs (the engine job) skip it with the reason and prove sign-in live instead (O11 lab).
81
+ const deviceCodeName = `contract: device-code sign-in shows a code and finishes signed in${o?.skipDeviceCode ? ` (skipped: ${o.skipDeviceCode})` : ''}`;
82
+ test(deviceCodeName, { ...(o?.skipDeviceCode ? { skip: true } : {}) }, async () => {
83
+ const { kit } = await make();
84
+ try {
85
+ await kit.ensureMember('m1');
86
+ const views = [];
87
+ const signIn = kit.signIn('m1', { authChoice: 'openai-device-code', via: 'code' }, (v) => views.push(v));
88
+ const done = await signIn.done;
89
+ assert.equal(done.state, 'done');
90
+ assert.ok(views.some((v) => v.state === 'waiting' && v.code), 'no device code was shown');
91
+ assert.equal(await kit.signedIn('m1', 'openai'), true);
92
+ }
93
+ finally {
94
+ await kit.stop();
95
+ }
96
+ });
97
+ test('contract: a person-cancelled sign-in fails cleanly', async () => {
98
+ const { kit } = await make();
99
+ try {
100
+ await kit.ensureMember('m1');
101
+ const signIn = kit.signIn('m1', { authChoice: 'openai-device-code', via: 'code' }, () => { });
102
+ signIn.cancel();
103
+ const done = await signIn.done;
104
+ assert.equal(done.state, 'failed');
105
+ }
106
+ finally {
107
+ await kit.stop();
108
+ }
109
+ });
110
+ test('contract: a run streams text and ends ok', async () => {
111
+ const { kit } = await make();
112
+ try {
113
+ await kit.ensureMember('m1');
114
+ const events = [];
115
+ const end = await kit.run({ member: 'm1', sessionKey: 'agent:m1:contract:1', message: 'hello contract' }, (e) => events.push(e));
116
+ assert.ok(end.ok, JSON.stringify(end));
117
+ if (end.ok)
118
+ assert.ok(end.text.length > 0, 'the run ended without text');
119
+ assert.ok(events.some((e) => e.type === 'text'), 'no text event arrived');
120
+ }
121
+ finally {
122
+ await kit.stop();
123
+ }
124
+ });
125
+ test('contract: a gated tool ask parks, approves and runs', async () => {
126
+ const { kit } = await make();
127
+ try {
128
+ await kit.ensureMember('m1');
129
+ const events = [];
130
+ const pending = kit.run({ member: 'm1', sessionKey: 'agent:m1:contract:2', message: '[tool note {"mode":"ask","text":"hi"}]' }, (e) => events.push(e));
131
+ // A real turn reaches the gate only after a model round trip; the fake answers at once.
132
+ await until(() => kit.approvals('m1').length > 0, 120_000);
133
+ const approval = kit.approvals('m1')[0];
134
+ assert.equal(approval.source, 'gate');
135
+ assert.equal(approval.tool, 'note');
136
+ await kit.decide(approval.id, { allow: true });
137
+ const end = await pending;
138
+ assert.ok(end.ok, JSON.stringify(end));
139
+ assert.ok(events.some((e) => e.type === 'tool' && e.name === 'note' && e.phase === 'start'));
140
+ assert.ok(events.some((e) => e.type === 'tool' && e.name === 'note' && e.phase === 'end'));
141
+ }
142
+ finally {
143
+ releaseStub();
144
+ await kit.stop();
145
+ }
146
+ });
147
+ test('contract: a denied tool is blocked without an approval', async () => {
148
+ const { kit } = await make();
149
+ try {
150
+ await kit.ensureMember('m1');
151
+ const end = await kit.run({ member: 'm1', sessionKey: 'agent:m1:contract:3', message: '[tool note {"mode":"deny","text":"x"}]' });
152
+ assert.ok(end.ok, JSON.stringify(end));
153
+ assert.equal(kit.approvals().length, 0);
154
+ }
155
+ finally {
156
+ await kit.stop();
157
+ }
158
+ });
159
+ test('contract: an engine builtin reaches the app gate and a denial blocks it', async () => {
160
+ const { kit } = await make();
161
+ try {
162
+ await kit.ensureMember('m1');
163
+ const pending = kit.run({ member: 'm1', sessionKey: 'agent:m1:contract:5',
164
+ message: '[tool web_fetch {"mode":"ask","url":"https://example.invalid/"}]' });
165
+ await until(() => kit.approvals('m1').length > 0);
166
+ const approval = kit.approvals('m1')[0];
167
+ assert.equal(approval.source, 'gate');
168
+ assert.equal(approval.tool, 'web_fetch');
169
+ await kit.decide(approval.id, { allow: false, reason: 'no fetching' });
170
+ const end = await pending;
171
+ assert.ok(end.ok, JSON.stringify(end));
172
+ assert.equal(kit.approvals().length, 0);
173
+ }
174
+ finally {
175
+ releaseStub();
176
+ await kit.stop();
177
+ }
178
+ });
179
+ test('contract: abort ends a run aborted', async () => {
180
+ const { kit, model } = await make();
181
+ try {
182
+ await kit.ensureMember('m1');
183
+ const key = 'agent:m1:contract:4';
184
+ if (model) {
185
+ // With the model stub the turn holds (`ask permission`), so the abort cannot race the run's end.
186
+ const pending = kit.run({ member: 'm1', sessionKey: key, message: 'ask permission to continue' });
187
+ await until(() => stubHolding(), 120_000);
188
+ const aborted = kit.abort(key).finally(() => releaseStub());
189
+ assert.deepEqual(await pending, { ok: false, aborted: true });
190
+ await aborted;
191
+ }
192
+ else {
193
+ const pending = kit.run({ member: 'm1', sessionKey: key, message: 'hello' }, (e) => {
194
+ if (e.type === 'text')
195
+ void kit.abort(key);
196
+ });
197
+ assert.deepEqual(await pending, { ok: false, aborted: true });
198
+ }
199
+ }
200
+ finally {
201
+ releaseStub();
202
+ await kit.stop();
203
+ }
204
+ });
205
+ test('contract: a native exec approval round-trips', async () => {
206
+ const { kit, peer } = await make();
207
+ try {
208
+ // Raised by another party (a second connection): the engine broadcasts requested events to the
209
+ // kit's approval-capable connection, never back to the requester (O11). On the fake both hear it.
210
+ const raise = peer ? peer.request.bind(peer) : call.bind(null, kit);
211
+ // The real engine holds the request open until the approval resolves, so it stays in flight.
212
+ const pending = raise('exec.approval.request', { id: 'contract-exec-1', command: 'echo contract',
213
+ ask: 'contract approval', agentId: 'm1', sessionKey: 'agent:m1:contract:9' });
214
+ await until(() => kit.approvals().some((a) => a.id === 'contract-exec-1'), 120_000);
215
+ assert.equal(kit.approvals()[0].source, 'exec');
216
+ assert.equal(kit.approvals('m1')[0].id, 'contract-exec-1');
217
+ await kit.decide('contract-exec-1', { allow: true });
218
+ assert.equal((await pending).id, 'contract-exec-1');
219
+ await until(() => kit.approvals().length === 0);
220
+ }
221
+ finally {
222
+ await kit.stop();
223
+ }
224
+ });
225
+ test('contract: call passes health through', async () => {
226
+ const { kit } = await make();
227
+ try {
228
+ const health = await call(kit, 'health');
229
+ assert.ok(health && typeof health === 'object', 'health did not answer');
230
+ }
231
+ finally {
232
+ await kit.stop();
233
+ }
234
+ });
235
+ test('contract: patchConfig keeps the memory invariants', async () => {
236
+ const { kit } = await make();
237
+ try {
238
+ await kit.patchConfig({ memory: { search: { provider: 'openai', fallback: 'openai' } } });
239
+ const config = await call(kit, 'config.get');
240
+ assert.equal(config.config.memory.search.provider, 'none');
241
+ assert.equal(config.config.memory.search.fallback, 'none');
242
+ await kit.patchConfig({ agents: { entries: { m1: { memory: { search: { provider: 'openai' } } } } } });
243
+ const after = await call(kit, 'config.get');
244
+ assert.equal(after.config.agents.entries.m1.memory.search.provider, 'none');
245
+ }
246
+ finally {
247
+ await kit.stop();
248
+ }
249
+ });
250
+ }
@@ -0,0 +1,19 @@
1
+ import type { GatewayTransport } from '../types.ts';
2
+ import type { KitOptions } from '../kit.ts';
3
+ type Handler = (params: any, bridgeSock: string) => unknown;
4
+ /** Pre-registered handlers for `fakeGateway(script)`, on top of the defaults (a same-name entry replaces one). */
5
+ export type FakeScript = Record<string, Handler>;
6
+ export declare function fakeGateway(script?: FakeScript): {
7
+ factory: NonNullable<KitOptions['transport']>;
8
+ calls: {
9
+ method: string;
10
+ params: unknown;
11
+ }[];
12
+ emit(event: string, payload?: unknown): void;
13
+ failNext(method: string, message: string): void;
14
+ drop(why: string): void;
15
+ handle(method: string, fn: Handler): void;
16
+ } & {
17
+ transport: GatewayTransport;
18
+ };
19
+ export {};