@animalabs/agent-framework 0.7.4 → 0.8.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 (92) hide show
  1. package/dist/src/code-execution/index.d.ts +1 -1
  2. package/dist/src/code-execution/index.d.ts.map +1 -1
  3. package/dist/src/code-execution/index.js.map +1 -1
  4. package/dist/src/code-execution/py-runner.d.ts +23 -1
  5. package/dist/src/code-execution/py-runner.d.ts.map +1 -1
  6. package/dist/src/code-execution/py-runner.js +40 -4
  7. package/dist/src/code-execution/py-runner.js.map +1 -1
  8. package/dist/src/code-execution/runtime-py.d.ts +15 -2
  9. package/dist/src/code-execution/runtime-py.d.ts.map +1 -1
  10. package/dist/src/code-execution/runtime-py.js +156 -7
  11. package/dist/src/code-execution/runtime-py.js.map +1 -1
  12. package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
  13. package/dist/src/code-execution/tool-definition.js +27 -2
  14. package/dist/src/code-execution/tool-definition.js.map +1 -1
  15. package/dist/src/framework.d.ts +73 -0
  16. package/dist/src/framework.d.ts.map +1 -1
  17. package/dist/src/framework.js +818 -23
  18. package/dist/src/framework.js.map +1 -1
  19. package/dist/src/mcpl/channel-registry.d.ts +28 -2
  20. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  21. package/dist/src/mcpl/channel-registry.js +53 -7
  22. package/dist/src/mcpl/channel-registry.js.map +1 -1
  23. package/dist/src/mcpl/transport.d.ts +1 -1
  24. package/dist/src/mcpl/transport.d.ts.map +1 -1
  25. package/dist/src/mcpl/transport.js +25 -7
  26. package/dist/src/mcpl/transport.js.map +1 -1
  27. package/dist/src/mcpl/types.d.ts +10 -0
  28. package/dist/src/mcpl/types.d.ts.map +1 -1
  29. package/dist/src/mcpl/types.js.map +1 -1
  30. package/dist/src/module-registry.d.ts +12 -0
  31. package/dist/src/module-registry.d.ts.map +1 -1
  32. package/dist/src/module-registry.js +26 -0
  33. package/dist/src/module-registry.js.map +1 -1
  34. package/dist/src/modules/workspace/index.d.ts +20 -0
  35. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  36. package/dist/src/modules/workspace/index.js +48 -15
  37. package/dist/src/modules/workspace/index.js.map +1 -1
  38. package/dist/src/modules/workspace/sync.d.ts +4 -0
  39. package/dist/src/modules/workspace/sync.d.ts.map +1 -1
  40. package/dist/src/modules/workspace/sync.js +1 -1
  41. package/dist/src/modules/workspace/sync.js.map +1 -1
  42. package/dist/src/types/framework.d.ts +10 -0
  43. package/dist/src/types/framework.d.ts.map +1 -1
  44. package/dist/src/types/module.d.ts +22 -0
  45. package/dist/src/types/module.d.ts.map +1 -1
  46. package/dist/test/busy-agent-scheduler.test.js +1 -0
  47. package/dist/test/busy-agent-scheduler.test.js.map +1 -1
  48. package/dist/test/code-execution.test.js +327 -1
  49. package/dist/test/code-execution.test.js.map +1 -1
  50. package/dist/test/deferred-wake-integrity.test.d.ts +29 -0
  51. package/dist/test/deferred-wake-integrity.test.d.ts.map +1 -0
  52. package/dist/test/deferred-wake-integrity.test.js +320 -0
  53. package/dist/test/deferred-wake-integrity.test.js.map +1 -0
  54. package/dist/test/machine-close-provenance.test.d.ts +2 -0
  55. package/dist/test/machine-close-provenance.test.d.ts.map +1 -0
  56. package/dist/test/machine-close-provenance.test.js +110 -0
  57. package/dist/test/machine-close-provenance.test.js.map +1 -0
  58. package/dist/test/mcpl-access-provider.test.d.ts +2 -0
  59. package/dist/test/mcpl-access-provider.test.d.ts.map +1 -0
  60. package/dist/test/mcpl-access-provider.test.js +20 -0
  61. package/dist/test/mcpl-access-provider.test.js.map +1 -0
  62. package/dist/test/module-context-integration.test.d.ts +18 -0
  63. package/dist/test/module-context-integration.test.d.ts.map +1 -0
  64. package/dist/test/module-context-integration.test.js +181 -0
  65. package/dist/test/module-context-integration.test.js.map +1 -0
  66. package/dist/test/present-while-acting.test.js +72 -0
  67. package/dist/test/present-while-acting.test.js.map +1 -1
  68. package/dist/test/utils-surface.test.d.ts +2 -0
  69. package/dist/test/utils-surface.test.d.ts.map +1 -0
  70. package/dist/test/utils-surface.test.js +145 -0
  71. package/dist/test/utils-surface.test.js.map +1 -0
  72. package/dist/test/workspace-read-image.test.js +71 -0
  73. package/dist/test/workspace-read-image.test.js.map +1 -1
  74. package/dist/test/workspace-restore-order.test.d.ts +16 -0
  75. package/dist/test/workspace-restore-order.test.d.ts.map +1 -0
  76. package/dist/test/workspace-restore-order.test.js +140 -0
  77. package/dist/test/workspace-restore-order.test.js.map +1 -0
  78. package/dist/tsconfig.tsbuildinfo +1 -1
  79. package/package.json +1 -1
  80. package/src/code-execution/index.ts +1 -0
  81. package/src/code-execution/py-runner.ts +56 -4
  82. package/src/code-execution/runtime-py.ts +156 -7
  83. package/src/code-execution/tool-definition.ts +27 -2
  84. package/src/framework.ts +931 -25
  85. package/src/mcpl/channel-registry.ts +98 -4
  86. package/src/mcpl/transport.ts +26 -7
  87. package/src/mcpl/types.ts +11 -0
  88. package/src/module-registry.ts +34 -0
  89. package/src/modules/workspace/index.ts +49 -15
  90. package/src/modules/workspace/sync.ts +1 -1
  91. package/src/types/framework.ts +10 -0
  92. package/src/types/module.ts +24 -0
@@ -0,0 +1,320 @@
1
+ /**
2
+ * Deferred-message / wake integrity (2026-07-31 Mythos "phantom skip").
3
+ *
4
+ * The incident: a DM arrived while a turn was streaming → the message body
5
+ * was deferred (hear-while-acting) but its inference wake was pushed
6
+ * immediately. The wake fired a new turn whose compile ran BEFORE the
7
+ * previous turn's finally flushed the deferred DM; the new turn saw only the
8
+ * routing-shift notice, reasonably skipped, and the DM then landed in the
9
+ * window mid-compile — positioned before the skip. Result: falsified history
10
+ * (the window testified the agent saw the DM and skipped it; the agent later
11
+ * confabulated an apology for a choice it never made) and a KV bust (every
12
+ * later compile diverged from the live prefix at the inserted message).
13
+ *
14
+ * Invariants under test:
15
+ * 1. A turn started by a queued wake CONTAINS the deferred message that woke
16
+ * it (turn-start flush runs before the locus announcement and compile).
17
+ * 2. addMessage defers on turn-alive (activeTurnTokens), not merely
18
+ * stream-alive — the compile window between dequeue and stream
19
+ * registration is closed to cross-turn writers.
20
+ * 3. endTurn edge: a message stored at an endTurn tool boundary (collected
21
+ * for injection but never delivered — the stream is cancelled) is seen by
22
+ * the follow-up turn its own wake starts, positioned AFTER the turn that
23
+ * never saw it, never before.
24
+ * 4. Turn tokens never leak: after runUntilIdle, no turn-alive markers
25
+ * remain (a leaked token would permanently requeue every wake — the
26
+ * 'idle+turn-alive' wedge).
27
+ */
28
+ import { describe, it, beforeEach, afterEach } from 'node:test';
29
+ import assert from 'node:assert/strict';
30
+ import { tmpdir } from 'node:os';
31
+ import { mkdtempSync, rmSync } from 'node:fs';
32
+ import { join } from 'node:path';
33
+ import { AgentFramework } from '../src/index.js';
34
+ import { MockMembrane, MockYieldingStream, createMockResponse } from './helpers/mock-membrane.js';
35
+ // ---------------------------------------------------------------------------
36
+ // Test module: a halt tool that ends the turn, and mid-turn interjections
37
+ // that DO request inference (unlike present-while-acting's, which don't) —
38
+ // this suite is about the wake/payload split, so the wake matters.
39
+ // ---------------------------------------------------------------------------
40
+ class DmModule {
41
+ name = 'dm';
42
+ framework = null;
43
+ /** Next `halt` call pushes this text as an external message (with
44
+ * requestInference) BEFORE returning its endTurn result. */
45
+ interjection = null;
46
+ async start(_ctx) { }
47
+ async stop() { }
48
+ getTools() {
49
+ return [
50
+ {
51
+ name: 'halt',
52
+ description: 'End the turn',
53
+ inputSchema: { type: 'object', properties: {} },
54
+ },
55
+ ];
56
+ }
57
+ async handleToolCall(call) {
58
+ if (call.name.endsWith('halt')) {
59
+ if (this.interjection) {
60
+ const text = this.interjection;
61
+ this.interjection = null;
62
+ this.framework.pushEvent({
63
+ type: 'external-message',
64
+ source: 'test',
65
+ content: text,
66
+ metadata: { channelId: 'dm-antra' },
67
+ });
68
+ // Let the run loop process the queued message while this tool round
69
+ // is still pending, so it lands in deferredMessages with its wake
70
+ // queued behind the busy agent.
71
+ await new Promise((r) => setTimeout(r, 30));
72
+ }
73
+ return { success: true, data: { halted: true }, endTurn: true };
74
+ }
75
+ return { success: true, data: { ok: true } };
76
+ }
77
+ async onProcess(event, _state) {
78
+ if (event.type === 'external-message') {
79
+ const text = String(event.content);
80
+ const metadata = event.metadata;
81
+ return {
82
+ addMessages: [
83
+ {
84
+ participant: 'Antra',
85
+ content: [{ type: 'text', text }],
86
+ ...(metadata ? { metadata: { ...metadata, triggered: true } } : {}),
87
+ },
88
+ ],
89
+ requestInference: true,
90
+ };
91
+ }
92
+ return {};
93
+ }
94
+ }
95
+ /** Minimal ChannelRegistry stub (same shape as present-while-acting's). */
96
+ function stubChannelRegistry(framework) {
97
+ const routed = [];
98
+ const explicit = {
99
+ resolveLocus: () => 'dm-antra',
100
+ routeSpeech: async (_agent, text, locus) => {
101
+ routed.push({ text, locus: locus ?? null });
102
+ },
103
+ getDefaultPublishChannel: () => null,
104
+ isChannelOpen: () => true,
105
+ getDescriptor: () => undefined,
106
+ getChannelTools: () => [],
107
+ };
108
+ framework.channelRegistry = new Proxy(explicit, {
109
+ get: (target, prop) => (prop in target ? target[prop] : () => undefined),
110
+ });
111
+ return routed;
112
+ }
113
+ /** Serialize a request's messages for containment/ordering checks. */
114
+ function flatten(req) {
115
+ return req.messages.map((m) => JSON.stringify(m));
116
+ }
117
+ function indexContaining(flat, needle) {
118
+ return flat.findIndex((s) => s.includes(needle));
119
+ }
120
+ /** Per-turn response scheduling: MockMembrane's streamYielding hands ALL
121
+ * remaining responses to the first stream, which starves multi-turn tests.
122
+ * This override gives each turn exactly its own slate. */
123
+ function scheduleTurns(membrane, turns) {
124
+ let turnIdx = 0;
125
+ membrane.streamYielding = (req) => {
126
+ membrane.calls.push(req);
127
+ const slate = turns[turnIdx++] ?? [createMockResponse([{ type: 'text', text: 'fallback' }])];
128
+ const stream = new MockYieldingStream(slate);
129
+ membrane.lastStream = stream;
130
+ return stream;
131
+ };
132
+ }
133
+ // ---------------------------------------------------------------------------
134
+ describe('deferred-message wake integrity', () => {
135
+ let tempDir;
136
+ let membrane;
137
+ let module;
138
+ beforeEach(() => {
139
+ tempDir = mkdtempSync(join(tmpdir(), 'dwi-test-'));
140
+ membrane = new MockMembrane();
141
+ module = new DmModule();
142
+ });
143
+ afterEach(() => {
144
+ rmSync(tempDir, { recursive: true, force: true });
145
+ });
146
+ async function createFramework() {
147
+ const framework = await AgentFramework.create({
148
+ storePath: join(tempDir, 'test.chronicle'),
149
+ membrane: membrane.asMembrane(),
150
+ agents: [
151
+ { name: 'assistant', model: 'test-model', systemPrompt: 'You are Mythos.' },
152
+ ],
153
+ modules: [module],
154
+ });
155
+ module.framework = framework;
156
+ return framework;
157
+ }
158
+ const internals = (f) => f;
159
+ // -------------------------------------------------------------------------
160
+ it('a turn started by a queued wake contains the deferred message that woke it', async () => {
161
+ scheduleTurns(membrane, [
162
+ [createMockResponse([{ type: 'text', text: 'answering the DM' }])],
163
+ ]);
164
+ const framework = await createFramework();
165
+ stubChannelRegistry(framework);
166
+ const fw = internals(framework);
167
+ // The incident state, seeded directly: payload deferred, wake queued,
168
+ // agent idle. (Live, this arises when a message lands during another
169
+ // turn's stream: addMessage defers the body, the wake pushes anyway.)
170
+ const dmText = 'and then the restaurant, and the hike — the deferred DM';
171
+ fw.deferredMessages.push({
172
+ participant: 'Antra',
173
+ content: [{ type: 'text', text: dmText }],
174
+ metadata: { channelId: 'dm-antra', triggered: true },
175
+ });
176
+ fw.pendingRequests.push({
177
+ agentName: 'assistant',
178
+ reason: 'mcpl:channel-incoming',
179
+ source: 'test',
180
+ timestamp: Date.now(),
181
+ channelId: 'dm-antra',
182
+ addressed: true,
183
+ });
184
+ await framework.runUntilIdle();
185
+ assert.equal(membrane.calls.length, 1, 'the wake fired exactly one turn');
186
+ const flat = flatten(membrane.calls[0]);
187
+ assert.ok(indexContaining(flat, dmText) >= 0, 'the compiled request contains the message that woke the turn (turn-start flush)');
188
+ assert.equal(fw.deferredMessages.length, 0, 'deferred queue drained');
189
+ assert.equal(fw.activeTurnTokens.size, 0, 'no turn token leaked');
190
+ await framework.stop();
191
+ });
192
+ it('addMessage defers on turn-alive even when no stream is registered (compile window)', async () => {
193
+ const framework = await createFramework();
194
+ stubChannelRegistry(framework);
195
+ const fw = internals(framework);
196
+ // Simulate the dequeue→stream gap: a turn is alive (token set) but its
197
+ // stream has not registered yet. This is exactly when the incident's
198
+ // deferred flush wrote into the window mid-compile.
199
+ fw.activeTurnTokens.set('assistant', 999999);
200
+ assert.equal(fw.activeStreams.size, 0, 'precondition: no live stream');
201
+ const id = fw.addMessage('Antra', [{ type: 'text', text: 'arrives mid-compile' }]);
202
+ assert.equal(id, '', 'message was deferred, not appended');
203
+ assert.equal(fw.deferredMessages.length, 1, 'message waits in the deferred queue');
204
+ fw.activeTurnTokens.delete('assistant');
205
+ fw.deferredMessages.length = 0; // don't leak state into stop()
206
+ await framework.stop();
207
+ });
208
+ it('endTurn edge: a message stored at an endTurn boundary is answered by its own wake, positioned after the turn that never saw it', async () => {
209
+ scheduleTurns(membrane, [
210
+ // Turn A: the agent is mid-scene and halts (skip_reply-shaped: an
211
+ // endTurn tool result cancels the stream — collected injections are
212
+ // never delivered).
213
+ [createMockResponse([{ type: 'tool_use', id: 'c1', name: 'dm--halt', input: {} }], 'tool_use')],
214
+ // Turn B: the DM's own wake.
215
+ [createMockResponse([{ type: 'text', text: 'answering antra now' }])],
216
+ ]);
217
+ const framework = await createFramework();
218
+ stubChannelRegistry(framework);
219
+ const fw = internals(framework);
220
+ const dmText = 'laria read me your messages aloud on the trail';
221
+ module.interjection = dmText;
222
+ // Start turn A.
223
+ framework.pushEvent({
224
+ type: 'external-message',
225
+ source: 'test',
226
+ content: 'go',
227
+ metadata: {},
228
+ });
229
+ await framework.runUntilIdle();
230
+ assert.equal(membrane.calls.length, 2, 'turn A (go → halt) and the DM-wake turn B ran');
231
+ // Turn B contains the DM.
232
+ const flatB = flatten(membrane.calls[1]);
233
+ const dmIdx = indexContaining(flatB, dmText);
234
+ assert.ok(dmIdx >= 0, "turn B's request contains the DM");
235
+ // History integrity: in turn B's request the DM appears AFTER turn A's
236
+ // halt (the turn that never saw it) — never retroactively before it.
237
+ const haltIdx = indexContaining(flatB, 'dm--halt');
238
+ assert.ok(haltIdx >= 0, "turn A's halt is in turn B's history");
239
+ assert.ok(dmIdx > haltIdx, `DM (idx ${dmIdx}) must be positioned after the halt (idx ${haltIdx}) — ` +
240
+ 'a DM placed before it would testify the agent saw and skipped it');
241
+ // And turn A itself must NOT contain the DM (it arrived mid-turn and was
242
+ // never injected — the endTurn cancelled the stream before delivery).
243
+ const flatA = flatten(membrane.calls[0]);
244
+ assert.equal(indexContaining(flatA, dmText), -1, "turn A's request predates the DM");
245
+ assert.equal(fw.activeTurnTokens.size, 0, 'no turn token leaked across the sequence');
246
+ assert.equal(fw.deferredMessages.length, 0, 'no message stranded in the deferred queue');
247
+ await framework.stop();
248
+ });
249
+ it('a throw before the compile (checkpoint/store failure) does not leak the turn token', async () => {
250
+ // ENOSPC-class failure: recordTurnCheckpoint (a store write) throws after
251
+ // the token is set but before driveStream exists. Without the caller's
252
+ // token-matched finally, the token leaks and every later wake requeues
253
+ // forever (the 'idle+turn-alive' wedge) — a one-turn failure must stay a
254
+ // one-turn failure.
255
+ scheduleTurns(membrane, [
256
+ [createMockResponse([{ type: 'text', text: 'recovered turn' }])],
257
+ ]);
258
+ const framework = await createFramework();
259
+ stubChannelRegistry(framework);
260
+ const fw = internals(framework);
261
+ const patchable = framework;
262
+ const original = patchable.recordTurnCheckpoint.bind(framework);
263
+ patchable.recordTurnCheckpoint = () => {
264
+ patchable.recordTurnCheckpoint = original; // one-shot
265
+ throw new Error('ENOSPC: simulated store-write failure');
266
+ };
267
+ framework.pushEvent({
268
+ type: 'external-message',
269
+ source: 'test',
270
+ content: 'go',
271
+ metadata: {},
272
+ });
273
+ // The throw propagates out of the event loop iteration (runUntilIdle
274
+ // surfaces it in this harness; production runLoop logs and continues).
275
+ await assert.rejects(framework.runUntilIdle(), /ENOSPC/);
276
+ assert.equal(fw.activeTurnTokens.size, 0, 'the failed turn cleared its own token');
277
+ // The agent is not wedged: a fresh wake still runs to completion.
278
+ framework.pushEvent({
279
+ type: 'external-message',
280
+ source: 'test',
281
+ content: 'go again',
282
+ metadata: {},
283
+ });
284
+ await framework.runUntilIdle();
285
+ assert.equal(membrane.calls.length, 1, 'the follow-up wake produced a turn');
286
+ assert.equal(fw.activeTurnTokens.size, 0, 'no token leaked after recovery either');
287
+ await framework.stop();
288
+ });
289
+ it('turn tokens are released on normal completion (no idle+turn-alive wedge)', async () => {
290
+ scheduleTurns(membrane, [
291
+ [createMockResponse([{ type: 'text', text: 'plain turn' }])],
292
+ ]);
293
+ const framework = await createFramework();
294
+ stubChannelRegistry(framework);
295
+ const fw = internals(framework);
296
+ framework.pushEvent({
297
+ type: 'external-message',
298
+ source: 'test',
299
+ content: 'go',
300
+ metadata: {},
301
+ });
302
+ await framework.runUntilIdle();
303
+ assert.equal(fw.activeTurnTokens.size, 0, 'token cleared at teardown');
304
+ // A fresh wake still runs (the agent is not wedged).
305
+ scheduleTurns(membrane, [
306
+ [createMockResponse([{ type: 'text', text: 'second turn' }])],
307
+ ]);
308
+ const callsBefore = membrane.calls.length;
309
+ framework.pushEvent({
310
+ type: 'external-message',
311
+ source: 'test',
312
+ content: 'go again',
313
+ metadata: {},
314
+ });
315
+ await framework.runUntilIdle();
316
+ assert.equal(membrane.calls.length, callsBefore + 1, 'agent takes the next turn');
317
+ await framework.stop();
318
+ });
319
+ });
320
+ //# sourceMappingURL=deferred-wake-integrity.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deferred-wake-integrity.test.js","sourceRoot":"","sources":["../../test/deferred-wake-integrity.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=machine-close-provenance.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"machine-close-provenance.test.d.ts","sourceRoot":"","sources":["../../test/machine-close-provenance.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,110 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { ChannelRegistry } from '../src/mcpl/channel-registry.js';
4
+ /**
5
+ * Machine-close provenance + explicit-open protection (issue #5): closes
6
+ * invoked by housekeeping modules must record their own decision source —
7
+ * never 'agent-tool' — and must not override a channel the resident or
8
+ * operator explicitly opened, unless the caller certifies an explicit idle
9
+ * lease. The Mythos "channel settings keep getting reset" mechanism was
10
+ * subscription-GC closes wearing the agent's badge.
11
+ */
12
+ function makeRegistry() {
13
+ const closeCalls = [];
14
+ const mockServer = {
15
+ sendChannelsOpen: async () => ({}),
16
+ sendChannelsClose: async (params) => {
17
+ closeCalls.push(params);
18
+ return {};
19
+ },
20
+ sendChannelsPublish: async () => ({ delivered: true }),
21
+ };
22
+ const serverRegistry = {
23
+ getServer: (_id) => mockServer,
24
+ };
25
+ const registry = new ChannelRegistry(serverRegistry, {}, () => { }, () => { });
26
+ const internals = registry;
27
+ const desired = (channelId) => internals.desiredStates.get(internals.lifecycleKey('discord', channelId));
28
+ // Register the channel the way a live server would surface it.
29
+ registry.handleIncoming('discord', {
30
+ messages: [{
31
+ channelId: 'c1',
32
+ messageId: 'm1',
33
+ author: { id: 'u1', name: 'Antra' },
34
+ timestamp: '2026-07-28T00:00:00.000Z',
35
+ content: [{ type: 'text', text: 'hello' }],
36
+ metadata: { channelName: '#commons' },
37
+ }],
38
+ });
39
+ return { registry, internals, desired, closeCalls };
40
+ }
41
+ const MODULE_ORIGIN = { kind: 'module' };
42
+ test('a machine close of an explicitly-opened channel is refused, structurally', async () => {
43
+ const { registry, desired, closeCalls } = makeRegistry();
44
+ await registry.handleChannelToolCall('channel_open', { channelId: 'c1' });
45
+ assert.deepEqual(desired('c1'), { state: 'open', source: 'agent-tool' });
46
+ const result = await registry.handleChannelToolCall('channel_close', { channelId: 'c1', source: 'subscription-gc' }, MODULE_ORIGIN);
47
+ assert.equal(result.success, false);
48
+ assert.equal(result.data?.refusal, 'explicit-open');
49
+ // Stated intent survives: desired state untouched, nothing sent to the server.
50
+ assert.deepEqual(desired('c1'), { state: 'open', source: 'agent-tool' });
51
+ assert.equal(closeCalls.length, 0);
52
+ });
53
+ test('an explicit idle lease lets the machine close through, with honest provenance', async () => {
54
+ const { registry, desired, closeCalls } = makeRegistry();
55
+ await registry.handleChannelToolCall('channel_open', { channelId: 'c1' });
56
+ const result = await registry.handleChannelToolCall('channel_close', { channelId: 'c1', source: 'subscription-gc', overrideExplicitOpen: true }, MODULE_ORIGIN);
57
+ assert.equal(result.success, true);
58
+ // The durable record names the janitor, not the agent.
59
+ assert.deepEqual(desired('c1'), { state: 'closed', source: 'subscription-gc' });
60
+ assert.equal(closeCalls.length, 1);
61
+ });
62
+ test('agent closes are recorded as agent-tool, exactly as before', async () => {
63
+ const { registry, desired } = makeRegistry();
64
+ await registry.handleChannelToolCall('channel_open', { channelId: 'c1' });
65
+ const result = await registry.handleChannelToolCall('channel_close', { channelId: 'c1' }, { kind: 'agent', agentName: 'mythos' });
66
+ assert.equal(result.success, true);
67
+ assert.deepEqual(desired('c1'), { state: 'closed', source: 'agent-tool' });
68
+ });
69
+ test('machine closes of policy-opened channels proceed without a lease', async () => {
70
+ const { registry, internals, desired, closeCalls } = makeRegistry();
71
+ // A channel nobody explicitly chose — opened by delivery/policy.
72
+ internals.setDesiredState('discord', 'c1', 'open', 'opened-by-delivery');
73
+ const result = await registry.handleChannelToolCall('channel_close', { channelId: 'c1', source: 'subscription-gc' }, MODULE_ORIGIN);
74
+ assert.equal(result.success, true);
75
+ assert.deepEqual(desired('c1'), { state: 'closed', source: 'subscription-gc' });
76
+ assert.equal(closeCalls.length, 1);
77
+ });
78
+ test('forged machine fields on a MODEL-origin call are ignored: agent-tool is recorded', async () => {
79
+ const { registry, desired, closeCalls } = makeRegistry();
80
+ await registry.handleChannelToolCall('channel_open', { channelId: 'c1' });
81
+ // The schema omits these fields but does not reject extras — a model can
82
+ // emit them. Trusted dispatch context must win over self-description.
83
+ const result = await registry.handleChannelToolCall('channel_close', { channelId: 'c1', source: 'subscription-gc', overrideExplicitOpen: true }, { kind: 'agent', agentName: 'mythos' });
84
+ assert.equal(result.success, true);
85
+ // The close succeeds (the resident may close their own channel) but the
86
+ // record attributes it to them, not to housekeeping.
87
+ assert.deepEqual(desired('c1'), { state: 'closed', source: 'agent-tool' });
88
+ assert.equal(closeCalls.length, 1);
89
+ });
90
+ test('origin-less calls default to agent semantics (safe for legacy callers)', async () => {
91
+ const { registry, desired } = makeRegistry();
92
+ await registry.handleChannelToolCall('channel_open', { channelId: 'c1' });
93
+ const result = await registry.handleChannelToolCall('channel_close', {
94
+ channelId: 'c1',
95
+ source: 'subscription-gc',
96
+ overrideExplicitOpen: true,
97
+ });
98
+ assert.equal(result.success, true);
99
+ assert.deepEqual(desired('c1'), { state: 'closed', source: 'agent-tool' });
100
+ });
101
+ test('module-origin closes outside the closed source vocabulary are rejected loudly', async () => {
102
+ const { registry, desired } = makeRegistry();
103
+ await registry.handleChannelToolCall('channel_open', { channelId: 'c1' });
104
+ const result = await registry.handleChannelToolCall('channel_close', { channelId: 'c1', source: 'gremlin-module' }, MODULE_ORIGIN);
105
+ assert.equal(result.success, false);
106
+ assert.equal(result.isError, true);
107
+ assert.match(result.error ?? '', /Unknown machine close source/);
108
+ assert.deepEqual(desired('c1'), { state: 'open', source: 'agent-tool' });
109
+ });
110
+ //# sourceMappingURL=machine-close-provenance.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"machine-close-provenance.test.js","sourceRoot":"","sources":["../../test/machine-close-provenance.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=mcpl-access-provider.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcpl-access-provider.test.d.ts","sourceRoot":"","sources":["../../test/mcpl-access-provider.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,20 @@
1
+ // Per-dial credential resolution: `accessProvider` overrides `token` at URL
2
+ // build time (transport.open runs it on every dial), and error-path URLs are
3
+ // redacted so credentials never ride error strings into traces or the model.
4
+ import { describe, it } from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import { buildWebSocketUrl } from '../src/mcpl/transport.js';
7
+ const base = { id: 's', url: 'wss://example.test/mcpl' };
8
+ describe('mcpl per-dial credentials', () => {
9
+ it('static token appends as ?token=', () => {
10
+ assert.equal(buildWebSocketUrl({ ...base, token: 'static' }), 'wss://example.test/mcpl?token=static');
11
+ });
12
+ it('override wins over static token; null strips it (redaction path)', () => {
13
+ assert.equal(buildWebSocketUrl({ ...base, token: 'static' }, 'fresh'), 'wss://example.test/mcpl?token=fresh');
14
+ assert.equal(buildWebSocketUrl({ ...base, token: 'static' }, null), 'wss://example.test/mcpl');
15
+ });
16
+ it('existing query params are preserved', () => {
17
+ assert.equal(buildWebSocketUrl({ ...base, url: 'wss://example.test/mcpl?x=1' }, 'fresh'), 'wss://example.test/mcpl?x=1&token=fresh');
18
+ });
19
+ });
20
+ //# sourceMappingURL=mcpl-access-provider.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcpl-access-provider.test.js","sourceRoot":"","sources":["../../test/mcpl-access-provider.test.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,6EAA6E;AAC7E,6EAA6E;AAC7E,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAG7D,MAAM,IAAI,GAAqB,EAAE,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,yBAAyB,EAAE,CAAC;AAE3E,QAAQ,CAAC,2BAA2B,EAAE,GAAG,EAAE;IACzC,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,CAAC,KAAK,CACV,iBAAiB,CAAC,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,EAC/C,sCAAsC,CACvC,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kEAAkE,EAAE,GAAG,EAAE;QAC1E,MAAM,CAAC,KAAK,CACV,iBAAiB,CAAC,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,OAAO,CAAC,EACxD,qCAAqC,CACtC,CAAC;QACF,MAAM,CAAC,KAAK,CACV,iBAAiB,CAAC,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,IAAI,CAAC,EACrD,yBAAyB,CAC1B,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,CAAC,KAAK,CACV,iBAAiB,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,6BAA6B,EAAE,EAAE,OAAO,CAAC,EAC3E,yCAAyC,CAC1C,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Integration tests for the module-facing framework seams added for issue #5,
3
+ * run through the REAL ModuleContextImpl (not hand-written mocks — a detached
4
+ * or mis-bound method must fail here the way it would in production):
5
+ *
6
+ * - ModuleContext.notifyOps reaches the framework ops channel (ops:alert
7
+ * trace) when invoked as a method on the context object;
8
+ * - ctx.callTool routes synthesized channel_* tools to the ChannelRegistry
9
+ * with trusted module origin (the route was missing entirely — module
10
+ * closes failed as "Invalid tool name format" since subscription-GC moved
11
+ * onto the generic channel_close);
12
+ * - the PUBLIC executeToolCall (the promise-based entry shared with
13
+ * ephemeral/model callers) stamps agent origin no matter what machine
14
+ * fields the input claims — module provenance is conferred by which
15
+ * internal path was used, never by which public method.
16
+ */
17
+ export {};
18
+ //# sourceMappingURL=module-context-integration.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module-context-integration.test.d.ts","sourceRoot":"","sources":["../../test/module-context-integration.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG"}