@animalabs/agent-framework 0.7.4 → 0.9.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/dist/src/agent.d.ts +10 -0
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +33 -3
- package/dist/src/agent.js.map +1 -1
- package/dist/src/code-execution/index.d.ts +1 -1
- package/dist/src/code-execution/index.d.ts.map +1 -1
- package/dist/src/code-execution/index.js.map +1 -1
- package/dist/src/code-execution/py-runner.d.ts +23 -1
- package/dist/src/code-execution/py-runner.d.ts.map +1 -1
- package/dist/src/code-execution/py-runner.js +40 -4
- package/dist/src/code-execution/py-runner.js.map +1 -1
- package/dist/src/code-execution/runtime-py.d.ts +15 -2
- package/dist/src/code-execution/runtime-py.d.ts.map +1 -1
- package/dist/src/code-execution/runtime-py.js +156 -7
- package/dist/src/code-execution/runtime-py.js.map +1 -1
- package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
- package/dist/src/code-execution/tool-definition.js +27 -2
- package/dist/src/code-execution/tool-definition.js.map +1 -1
- package/dist/src/framework.d.ts +234 -5
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1818 -188
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +11 -0
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +66 -0
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/capability-grant.d.ts +115 -0
- package/dist/src/mcpl/capability-grant.d.ts.map +1 -0
- package/dist/src/mcpl/capability-grant.js +291 -0
- package/dist/src/mcpl/capability-grant.js.map +1 -0
- package/dist/src/mcpl/capability-mask.d.ts +51 -0
- package/dist/src/mcpl/capability-mask.d.ts.map +1 -0
- package/dist/src/mcpl/capability-mask.js +190 -0
- package/dist/src/mcpl/capability-mask.js.map +1 -0
- package/dist/src/mcpl/channel-registry.d.ts +30 -2
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +216 -43
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.d.ts +8 -3
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.js +28 -8
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
- package/dist/src/mcpl/errors.d.ts +6 -0
- package/dist/src/mcpl/errors.d.ts.map +1 -1
- package/dist/src/mcpl/errors.js +6 -0
- package/dist/src/mcpl/errors.js.map +1 -1
- package/dist/src/mcpl/feature-set-manager.d.ts +10 -1
- package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
- package/dist/src/mcpl/feature-set-manager.js +45 -4
- package/dist/src/mcpl/feature-set-manager.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts +10 -14
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.js +58 -67
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +1 -1
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/prose-stream-router.d.ts +9 -0
- package/dist/src/mcpl/prose-stream-router.d.ts.map +1 -1
- package/dist/src/mcpl/prose-stream-router.js +16 -0
- package/dist/src/mcpl/prose-stream-router.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +20 -2
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +93 -9
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +231 -32
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/server-registry.d.ts.map +1 -1
- package/dist/src/mcpl/server-registry.js +4 -3
- package/dist/src/mcpl/server-registry.js.map +1 -1
- package/dist/src/mcpl/tags.d.ts +24 -0
- package/dist/src/mcpl/tags.d.ts.map +1 -0
- package/dist/src/mcpl/tags.js +52 -0
- package/dist/src/mcpl/tags.js.map +1 -0
- package/dist/src/mcpl/transport.d.ts +1 -1
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +35 -7
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +169 -64
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js +16 -2
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +12 -0
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +26 -0
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +20 -0
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +48 -15
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/sync.d.ts +4 -0
- package/dist/src/modules/workspace/sync.d.ts.map +1 -1
- package/dist/src/modules/workspace/sync.js +1 -1
- package/dist/src/modules/workspace/sync.js.map +1 -1
- package/dist/src/refusal-reactions.d.ts +27 -0
- package/dist/src/refusal-reactions.d.ts.map +1 -0
- package/dist/src/refusal-reactions.js +35 -0
- package/dist/src/refusal-reactions.js.map +1 -0
- package/dist/src/tool-result-history.d.ts +10 -0
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +16 -5
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/types/agent.d.ts +35 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +26 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +22 -0
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +24 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/busy-agent-scheduler.test.js +1 -0
- package/dist/test/busy-agent-scheduler.test.js.map +1 -1
- package/dist/test/channel-registry-routing.test.js +38 -10
- package/dist/test/channel-registry-routing.test.js.map +1 -1
- package/dist/test/channel-registry-typing.test.js +10 -1
- package/dist/test/channel-registry-typing.test.js.map +1 -1
- package/dist/test/checkpoint-attribution.test.js +168 -8
- package/dist/test/checkpoint-attribution.test.js.map +1 -1
- package/dist/test/code-execution.test.js +327 -1
- package/dist/test/code-execution.test.js.map +1 -1
- package/dist/test/conversation-fork-injection-scoping.test.js +8 -3
- package/dist/test/conversation-fork-injection-scoping.test.js.map +1 -1
- package/dist/test/deferred-wake-integrity.test.d.ts +29 -0
- package/dist/test/deferred-wake-integrity.test.d.ts.map +1 -0
- package/dist/test/deferred-wake-integrity.test.js +320 -0
- package/dist/test/deferred-wake-integrity.test.js.map +1 -0
- package/dist/test/event-gate-selfwake.test.d.ts +15 -0
- package/dist/test/event-gate-selfwake.test.d.ts.map +1 -0
- package/dist/test/event-gate-selfwake.test.js +125 -0
- package/dist/test/event-gate-selfwake.test.js.map +1 -0
- package/dist/test/machine-close-provenance.test.d.ts +2 -0
- package/dist/test/machine-close-provenance.test.d.ts.map +1 -0
- package/dist/test/machine-close-provenance.test.js +116 -0
- package/dist/test/machine-close-provenance.test.js.map +1 -0
- package/dist/test/mcpl-access-provider.test.d.ts +2 -0
- package/dist/test/mcpl-access-provider.test.d.ts.map +1 -0
- package/dist/test/mcpl-access-provider.test.js +20 -0
- package/dist/test/mcpl-access-provider.test.js.map +1 -0
- package/dist/test/mcpl-awareness-barrier.test.js +10 -0
- package/dist/test/mcpl-awareness-barrier.test.js.map +1 -1
- package/dist/test/mcpl-capability-grant.test.d.ts +10 -0
- package/dist/test/mcpl-capability-grant.test.d.ts.map +1 -0
- package/dist/test/mcpl-capability-grant.test.js +109 -0
- package/dist/test/mcpl-capability-grant.test.js.map +1 -0
- package/dist/test/mcpl-capability-scoping.test.d.ts +2 -0
- package/dist/test/mcpl-capability-scoping.test.d.ts.map +1 -0
- package/dist/test/mcpl-capability-scoping.test.js +266 -0
- package/dist/test/mcpl-capability-scoping.test.js.map +1 -0
- package/dist/test/mcpl-gate-roundtrip.test.js +13 -0
- package/dist/test/mcpl-gate-roundtrip.test.js.map +1 -1
- package/dist/test/mcpl-list-status.test.d.ts +2 -0
- package/dist/test/mcpl-list-status.test.d.ts.map +1 -0
- package/dist/test/mcpl-list-status.test.js +78 -0
- package/dist/test/mcpl-list-status.test.js.map +1 -0
- package/dist/test/mcpl-manifest-changed.test.d.ts +2 -0
- package/dist/test/mcpl-manifest-changed.test.d.ts.map +1 -0
- package/dist/test/mcpl-manifest-changed.test.js +177 -0
- package/dist/test/mcpl-manifest-changed.test.js.map +1 -0
- package/dist/test/mcpl-reconnect-events.test.js +27 -2
- package/dist/test/mcpl-reconnect-events.test.js.map +1 -1
- package/dist/test/mcpl-reconnect-reregistration.test.js +53 -17
- package/dist/test/mcpl-reconnect-reregistration.test.js.map +1 -1
- package/dist/test/mcpl-request-timeout.test.js +1 -1
- package/dist/test/mcpl-request-timeout.test.js.map +1 -1
- package/dist/test/module-context-integration.test.d.ts +18 -0
- package/dist/test/module-context-integration.test.d.ts.map +1 -0
- package/dist/test/module-context-integration.test.js +181 -0
- package/dist/test/module-context-integration.test.js.map +1 -0
- package/dist/test/physical-window-restart.test.d.ts +14 -0
- package/dist/test/physical-window-restart.test.d.ts.map +1 -0
- package/dist/test/physical-window-restart.test.js +222 -0
- package/dist/test/physical-window-restart.test.js.map +1 -0
- package/dist/test/present-while-acting.test.js +270 -0
- package/dist/test/present-while-acting.test.js.map +1 -1
- package/dist/test/refusal-reaction-baseline.test.d.ts +2 -0
- package/dist/test/refusal-reaction-baseline.test.d.ts.map +1 -0
- package/dist/test/refusal-reaction-baseline.test.js +74 -0
- package/dist/test/refusal-reaction-baseline.test.js.map +1 -0
- package/dist/test/state-scaling-e2e.test.js +7 -1
- package/dist/test/state-scaling-e2e.test.js.map +1 -1
- package/dist/test/state-scaling.test.js +9 -1
- package/dist/test/state-scaling.test.js.map +1 -1
- package/dist/test/tool-result-spill.test.d.ts +13 -0
- package/dist/test/tool-result-spill.test.d.ts.map +1 -0
- package/dist/test/tool-result-spill.test.js +463 -0
- package/dist/test/tool-result-spill.test.js.map +1 -0
- package/dist/test/trunk-channel-routing.test.js +45 -0
- package/dist/test/trunk-channel-routing.test.js.map +1 -1
- package/dist/test/utils-surface.test.d.ts +2 -0
- package/dist/test/utils-surface.test.d.ts.map +1 -0
- package/dist/test/utils-surface.test.js +145 -0
- package/dist/test/utils-surface.test.js.map +1 -0
- package/dist/test/workspace-read-image.test.js +71 -0
- package/dist/test/workspace-read-image.test.js.map +1 -1
- package/dist/test/workspace-restore-order.test.d.ts +16 -0
- package/dist/test/workspace-restore-order.test.d.ts.map +1 -0
- package/dist/test/workspace-restore-order.test.js +140 -0
- package/dist/test/workspace-restore-order.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/agent.ts +37 -3
- package/src/code-execution/index.ts +1 -0
- package/src/code-execution/py-runner.ts +56 -4
- package/src/code-execution/runtime-py.ts +156 -7
- package/src/code-execution/tool-definition.ts +27 -2
- package/src/framework.ts +2080 -203
- package/src/gate/event-gate.ts +72 -0
- package/src/index.ts +1 -0
- package/src/mcpl/capability-grant.ts +319 -0
- package/src/mcpl/capability-mask.ts +228 -0
- package/src/mcpl/channel-registry.ts +265 -43
- package/src/mcpl/checkpoint-manager.ts +28 -8
- package/src/mcpl/errors.ts +7 -0
- package/src/mcpl/feature-set-manager.ts +50 -5
- package/src/mcpl/hook-orchestrator.ts +64 -86
- package/src/mcpl/index.ts +0 -2
- package/src/mcpl/prose-stream-router.ts +17 -0
- package/src/mcpl/push-handler.ts +18 -2
- package/src/mcpl/server-connection.ts +262 -34
- package/src/mcpl/server-registry.ts +4 -3
- package/src/mcpl/tags.ts +50 -0
- package/src/mcpl/transport.ts +37 -7
- package/src/mcpl/types.ts +187 -78
- package/src/module-registry.ts +34 -0
- package/src/modules/workspace/index.ts +49 -15
- package/src/modules/workspace/sync.ts +1 -1
- package/src/refusal-reactions.ts +37 -0
- package/src/tool-result-history.ts +17 -5
- package/src/types/agent.ts +36 -0
- package/src/types/framework.ts +27 -0
- package/src/types/module.ts +24 -0
- package/src/types/trace.ts +22 -0
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* - Expose synthesized tools: channel_list, channel_open, channel_close, channel_publish
|
|
13
13
|
* - Build channel context for beforeInference params
|
|
14
14
|
*/
|
|
15
|
+
import { expandCoreTags } from './tags.js';
|
|
16
|
+
import { CapabilityGrant } from './capability-grant.js';
|
|
15
17
|
// ============================================================================
|
|
16
18
|
// Typing indicator interval (Discord typing lasts ~10s, so 7s keeps it alive)
|
|
17
19
|
// ============================================================================
|
|
@@ -166,7 +168,8 @@ const CHANNEL_TOOL_DEFINITIONS = [
|
|
|
166
168
|
description: 'End your turn WITHOUT sending anything to any channel or surface. Use when you have ' +
|
|
167
169
|
'read the messages but deliberately choose not to reply right now — ambient chatter, ' +
|
|
168
170
|
'nothing to add, or you are waiting. Any plain text you wrote this turn stays private ' +
|
|
169
|
-
'and is NOT posted. To reply instead, just write plain text (no tool call).'
|
|
171
|
+
'and is NOT posted. To reply instead, just write plain text (no tool call). ' +
|
|
172
|
+
'To end this turn but come back on your own shortly, set wake_in_seconds.',
|
|
170
173
|
inputSchema: {
|
|
171
174
|
type: 'object',
|
|
172
175
|
properties: {
|
|
@@ -174,14 +177,21 @@ const CHANNEL_TOOL_DEFINITIONS = [
|
|
|
174
177
|
type: 'string',
|
|
175
178
|
description: 'Optional private note on why you are not replying (not sent anywhere).',
|
|
176
179
|
},
|
|
180
|
+
wake_in_seconds: {
|
|
181
|
+
type: 'number',
|
|
182
|
+
description: 'Optional self-wake: if nothing else wakes you first, you wake again after this ' +
|
|
183
|
+
'many seconds (1 = go again almost immediately; clamped to 1–3600). Any other ' +
|
|
184
|
+
'wake before then cancels it. Omit to stay idle until the next external wake.',
|
|
185
|
+
},
|
|
177
186
|
},
|
|
178
187
|
required: [],
|
|
179
188
|
},
|
|
180
189
|
},
|
|
181
190
|
];
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
191
|
+
/** The machine decision sources a module-origin channel_close may record.
|
|
192
|
+
* Closed on purpose: honest provenance means a small, auditable vocabulary,
|
|
193
|
+
* not arbitrary self-description. */
|
|
194
|
+
export const MACHINE_CLOSE_SOURCES = new Set(['subscription-gc', 'housekeeping']);
|
|
185
195
|
export class ChannelRegistry {
|
|
186
196
|
serverRegistry;
|
|
187
197
|
featureSetManager;
|
|
@@ -245,7 +255,18 @@ export class ChannelRegistry {
|
|
|
245
255
|
*/
|
|
246
256
|
async handleRegister(serverId, params, responder) {
|
|
247
257
|
const registeredIds = [];
|
|
258
|
+
// §14.5: per-descriptor authorization with ITEMIZED results — one entry
|
|
259
|
+
// per submitted descriptor. Strict 0.5 servers treat a result without
|
|
260
|
+
// `results` (or a descriptor missing from it) as rejected, so the shape
|
|
261
|
+
// is interop-critical, not decorative. Rejection here is per-descriptor
|
|
262
|
+
// validation; the method-level channels.register gate already ran at the
|
|
263
|
+
// connection (§14.1).
|
|
264
|
+
const results = [];
|
|
248
265
|
for (const channel of params.channels) {
|
|
266
|
+
if (!channel || typeof channel.id !== 'string' || channel.id.length === 0) {
|
|
267
|
+
results.push({ id: String(channel?.id ?? ''), accepted: false, reason: 'invalid descriptor: missing id' });
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
249
270
|
const key = `${serverId}:${channel.id}`;
|
|
250
271
|
this.channels.set(key, {
|
|
251
272
|
serverId,
|
|
@@ -253,10 +274,11 @@ export class ChannelRegistry {
|
|
|
253
274
|
open: false,
|
|
254
275
|
});
|
|
255
276
|
registeredIds.push(channel.id);
|
|
277
|
+
results.push({ id: channel.id, accepted: true });
|
|
256
278
|
}
|
|
257
279
|
// Respond before reconciliation — the server blocks on this response and
|
|
258
280
|
// can't process channels/open until it arrives.
|
|
259
|
-
const result = { registered: registeredIds };
|
|
281
|
+
const result = { registered: registeredIds, results };
|
|
260
282
|
responder?.respond(result);
|
|
261
283
|
// One-time migration from the retired recipe policy, then reconcile the
|
|
262
284
|
// server to Chronicle-backed desired state.
|
|
@@ -275,37 +297,67 @@ export class ChannelRegistry {
|
|
|
275
297
|
* Processes added (register + reconcile), removed (delete + stop typing),
|
|
276
298
|
* and updated (replace descriptor) channels.
|
|
277
299
|
*/
|
|
278
|
-
async handleChanged(serverId, params) {
|
|
279
|
-
//
|
|
300
|
+
async handleChanged(serverId, params, responder) {
|
|
301
|
+
// §14.5: dual-mode. Request form answers ITEMIZED per-descriptor
|
|
302
|
+
// results for added channels; Notification form filters itemwise with a
|
|
303
|
+
// diagnostic. Either way, added descriptors are validated exactly like
|
|
304
|
+
// channels/register — a changed-set cannot smuggle in what register
|
|
305
|
+
// would have rejected (PR #79 review blocker 5).
|
|
306
|
+
const addedResults = [];
|
|
307
|
+
// Process removed channels — itemized truthfully: removal of a channel
|
|
308
|
+
// we never had is reported, not silently accepted.
|
|
280
309
|
if (params.removed) {
|
|
281
310
|
for (const channelId of params.removed) {
|
|
282
311
|
const key = `${serverId}:${channelId}`;
|
|
283
|
-
this.channels.delete(key);
|
|
312
|
+
const existed = this.channels.delete(key);
|
|
284
313
|
this.stopTyping(channelId);
|
|
314
|
+
addedResults.push({ id: channelId, accepted: existed, reason: existed ? undefined : 'not registered' });
|
|
285
315
|
}
|
|
286
316
|
}
|
|
287
|
-
// Process updated channels
|
|
317
|
+
// Process updated channels — validated itemwise exactly like added
|
|
318
|
+
// (§14.5: a changed-set cannot smuggle in what register would reject),
|
|
319
|
+
// and every submitted descriptor gets a verdict: a strict server reads
|
|
320
|
+
// absence from `results` as rejection.
|
|
288
321
|
if (params.updated) {
|
|
289
322
|
for (const channel of params.updated) {
|
|
323
|
+
if (!channel || typeof channel.id !== 'string' || channel.id.length === 0) {
|
|
324
|
+
addedResults.push({ id: String(channel?.id ?? ''), accepted: false, reason: 'invalid descriptor: missing id' });
|
|
325
|
+
this.emitTraceFn({ type: 'mcpl:channel-descriptor-rejected', serverId, reason: 'missing id (update)' });
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
290
328
|
const key = `${serverId}:${channel.id}`;
|
|
291
329
|
const existing = this.channels.get(key);
|
|
292
|
-
if (existing) {
|
|
293
|
-
|
|
330
|
+
if (!existing) {
|
|
331
|
+
addedResults.push({ id: channel.id, accepted: false, reason: 'not registered — update rejected (§14.5)' });
|
|
332
|
+
this.emitTraceFn({ type: 'mcpl:channel-descriptor-rejected', serverId, channelId: channel.id, reason: 'update for unregistered channel' });
|
|
333
|
+
continue;
|
|
294
334
|
}
|
|
335
|
+
existing.descriptor = channel;
|
|
336
|
+
addedResults.push({ id: channel.id, accepted: true });
|
|
295
337
|
}
|
|
296
338
|
}
|
|
297
|
-
// Process added channels (register
|
|
339
|
+
// Process added channels (validate per-descriptor, register, reconcile)
|
|
340
|
+
const accepted = [];
|
|
298
341
|
if (params.added) {
|
|
299
342
|
for (const channel of params.added) {
|
|
343
|
+
if (!channel || typeof channel.id !== 'string' || channel.id.length === 0) {
|
|
344
|
+
addedResults.push({ id: String(channel?.id ?? ''), accepted: false, reason: 'invalid descriptor: missing id' });
|
|
345
|
+
this.emitTraceFn({ type: 'mcpl:channel-descriptor-rejected', serverId, reason: 'missing id' });
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
300
348
|
const key = `${serverId}:${channel.id}`;
|
|
301
349
|
this.channels.set(key, {
|
|
302
350
|
serverId,
|
|
303
351
|
descriptor: channel,
|
|
304
352
|
open: false,
|
|
305
353
|
});
|
|
354
|
+
addedResults.push({ id: channel.id, accepted: true });
|
|
355
|
+
accepted.push(channel);
|
|
306
356
|
}
|
|
307
|
-
|
|
357
|
+
if (accepted.length > 0)
|
|
358
|
+
await this.reconcileChannels(serverId, accepted);
|
|
308
359
|
}
|
|
360
|
+
responder?.respond({ results: addedResults });
|
|
309
361
|
this.emitTraceFn({
|
|
310
362
|
type: 'mcpl:channels-changed',
|
|
311
363
|
serverId,
|
|
@@ -323,12 +375,11 @@ export class ChannelRegistry {
|
|
|
323
375
|
handleIncoming(serverId, params, responder) {
|
|
324
376
|
const results = [];
|
|
325
377
|
for (const message of params.messages) {
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
//
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
this.defaultPublishThreadId = message.threadId;
|
|
378
|
+
// §14.5 FIRST, before ANY semantic processing: admission against the
|
|
379
|
+
// actually-registered channel precedes tag expansion and content
|
|
380
|
+
// conversion — decoding an unregistered sender's payload (including
|
|
381
|
+
// inline base64) is allocation and parsing work done for a message
|
|
382
|
+
// that must be rejected (Sol, PR #79 re-review blocker 2).
|
|
332
383
|
// Lazy-register the channel if we've never seen it. A channel can deliver
|
|
333
384
|
// an incoming message before its channels/register (boot enumeration) or
|
|
334
385
|
// channels/changed (post-boot create / View-permission grant) round-trip
|
|
@@ -340,31 +391,43 @@ export class ChannelRegistry {
|
|
|
340
391
|
// is reachable. The inbound message carries enough to make it publishable,
|
|
341
392
|
// so register it here; a later authoritative channels/register or
|
|
342
393
|
// channels/changed will overwrite this descriptor with the richer one.
|
|
394
|
+
// §14.5: channels/incoming is validated against the ACTUALLY
|
|
395
|
+
// REGISTERED channel. An unknown claimed channelId is rejected
|
|
396
|
+
// per-message with an itemized result — never minted by its first
|
|
397
|
+
// message (PR #79 review blocker 5: lazy-registration let a server
|
|
398
|
+
// self-attest a channel identity without ever passing
|
|
399
|
+
// channels/register authorization). A server whose channel genuinely
|
|
400
|
+
// exists registers it first; discord's DM case goes through
|
|
401
|
+
// ensureChannelRegistered on push/event, which creates a CLOSED
|
|
402
|
+
// routable entry rather than an open self-attested one.
|
|
343
403
|
const incomingKey = `${serverId}:${message.channelId}`;
|
|
344
404
|
if (!this.channels.has(incomingKey)) {
|
|
345
|
-
const channelLabel = typeof message.metadata?.channelName === 'string' && message.metadata.channelName
|
|
346
|
-
? message.metadata.channelName
|
|
347
|
-
: message.channelId;
|
|
348
|
-
this.channels.set(incomingKey, {
|
|
349
|
-
serverId,
|
|
350
|
-
descriptor: {
|
|
351
|
-
id: message.channelId,
|
|
352
|
-
type: serverId,
|
|
353
|
-
label: channelLabel,
|
|
354
|
-
direction: 'bidirectional',
|
|
355
|
-
address: message.threadId ? { threadId: message.threadId } : undefined,
|
|
356
|
-
metadata: { lazyRegistered: true },
|
|
357
|
-
},
|
|
358
|
-
open: true,
|
|
359
|
-
});
|
|
360
405
|
this.emitTraceFn({
|
|
361
|
-
type: 'mcpl:channel-
|
|
406
|
+
type: 'mcpl:channel-incoming-rejected',
|
|
362
407
|
serverId,
|
|
363
408
|
channelId: message.channelId,
|
|
364
|
-
|
|
409
|
+
reason: 'unknown channel — not registered (§14.5)',
|
|
365
410
|
});
|
|
411
|
+
results.push({
|
|
412
|
+
messageId: message.messageId,
|
|
413
|
+
accepted: false,
|
|
414
|
+
reason: `unknown channel "${message.channelId}" — register it first (§14.5)`,
|
|
415
|
+
});
|
|
416
|
+
continue;
|
|
366
417
|
}
|
|
367
|
-
|
|
418
|
+
// ACCEPTED from here down: semantic processing only for admitted
|
|
419
|
+
// messages. §16.3 core-tag closure, then content conversion.
|
|
420
|
+
if (message.tags)
|
|
421
|
+
message.tags = expandCoreTags(message.tags);
|
|
422
|
+
const convertedContent = message.content.map(convertBlock);
|
|
423
|
+
// Track default publish channel (most recent ACCEPTED incoming) —
|
|
424
|
+
// deliberately after §14.5 validation: a rejected message from an
|
|
425
|
+
// unregistered channel must not retarget outbound speech (the locus is
|
|
426
|
+
// exactly the authority a self-attested channel would be stealing).
|
|
427
|
+
this.defaultPublishChannel = message.channelId;
|
|
428
|
+
this.defaultPublishMessageId = message.messageId;
|
|
429
|
+
this.defaultPublishThreadId = message.threadId;
|
|
430
|
+
{
|
|
368
431
|
// A server sending channels/incoming is authoritative evidence that
|
|
369
432
|
// the transport is actually open. This repairs transient status only;
|
|
370
433
|
// durable desired state still changes exclusively through lifecycle
|
|
@@ -604,15 +667,24 @@ export class ChannelRegistry {
|
|
|
604
667
|
}
|
|
605
668
|
/**
|
|
606
669
|
* Handle a call to one of the synthesized channel tools.
|
|
670
|
+
*
|
|
671
|
+
* `origin` is TRUSTED DISPATCH CONTEXT, supplied by the framework's own
|
|
672
|
+
* routing (dispatchChannelToolCall and the public executeToolCall for
|
|
673
|
+
* model-origin calls, the framework's private ModuleRegistry closure for
|
|
674
|
+
* module ctx.callTool) — never derived from tool input. Machine
|
|
675
|
+
* provenance on channel_close is honored only for module origin; a
|
|
676
|
+
* model-origin call carrying the same fields is recorded as the agent
|
|
677
|
+
* decision it actually is. Absent origin is treated as agent-origin (the
|
|
678
|
+
* untrusted-safe default).
|
|
607
679
|
*/
|
|
608
|
-
async handleChannelToolCall(toolName, input) {
|
|
680
|
+
async handleChannelToolCall(toolName, input, origin) {
|
|
609
681
|
switch (toolName) {
|
|
610
682
|
case 'channel_list':
|
|
611
683
|
return this.handleToolList();
|
|
612
684
|
case 'channel_open':
|
|
613
685
|
return this.handleToolOpen(input);
|
|
614
686
|
case 'channel_close':
|
|
615
|
-
return this.handleToolClose(input);
|
|
687
|
+
return this.handleToolClose(input, origin);
|
|
616
688
|
case 'channel_decline':
|
|
617
689
|
return this.handleToolDecline(input);
|
|
618
690
|
case 'channel_publish':
|
|
@@ -838,6 +910,18 @@ export class ChannelRegistry {
|
|
|
838
910
|
const server = this.serverRegistry.getServer(serverId);
|
|
839
911
|
if (!server)
|
|
840
912
|
return;
|
|
913
|
+
// §14.1: channels/open + channels/close require channels.lifecycle.
|
|
914
|
+
// Skipping reconciliation for an ungranted server is the enforcement —
|
|
915
|
+
// its channels simply stay in whatever state the server chose, and the
|
|
916
|
+
// host never directs lifecycle it was not granted authority over.
|
|
917
|
+
if (!CapabilityGrant.of(server).has('channels.lifecycle')) {
|
|
918
|
+
this.emitTraceFn({
|
|
919
|
+
type: 'mcpl:channel-reconcile-skipped',
|
|
920
|
+
serverId,
|
|
921
|
+
reason: 'channels.lifecycle not in effective grant (§14.1)',
|
|
922
|
+
});
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
841
925
|
// Channels the subscription policy freshly admitted in THIS pass —
|
|
842
926
|
// announced to the agent as one batched notice after the loop, so a
|
|
843
927
|
// first boot on a new policy doesn't produce one notice per channel.
|
|
@@ -913,6 +997,11 @@ export class ChannelRegistry {
|
|
|
913
997
|
* (typing timer lifecycle is still managed for when the callback is wired).
|
|
914
998
|
*/
|
|
915
999
|
sendTypingNotification(serverId, channelId) {
|
|
1000
|
+
// §14.1: channels/typing requires channels.typing in the grant. Silent
|
|
1001
|
+
// skip — a typing indicator is cosmetic and per-7s, so a diagnostic per
|
|
1002
|
+
// tick would be noise.
|
|
1003
|
+
if (!CapabilityGrant.of(this.serverRegistry.getServer(serverId)).has('channels.typing'))
|
|
1004
|
+
return;
|
|
916
1005
|
if (this.sendTypingFn) {
|
|
917
1006
|
this.sendTypingFn(serverId, channelId, this.typingMetadata.get(channelId));
|
|
918
1007
|
}
|
|
@@ -922,6 +1011,12 @@ export class ChannelRegistry {
|
|
|
922
1011
|
// ==========================================================================
|
|
923
1012
|
// Private: Channel Lookup
|
|
924
1013
|
// ==========================================================================
|
|
1014
|
+
/** Descriptors registered by one server — the §14.3 channels/list answer. */
|
|
1015
|
+
descriptorsForServer(serverId) {
|
|
1016
|
+
return [...this.channels.values()]
|
|
1017
|
+
.filter((e) => e.serverId === serverId)
|
|
1018
|
+
.map((e) => e.descriptor);
|
|
1019
|
+
}
|
|
925
1020
|
/** Server id owning a registered channel (by descriptor id), or null. */
|
|
926
1021
|
getChannelServerId(channelId) {
|
|
927
1022
|
return this.findChannelEntry(channelId)?.serverId ?? null;
|
|
@@ -1001,6 +1096,11 @@ export class ChannelRegistry {
|
|
|
1001
1096
|
if (!server) {
|
|
1002
1097
|
throw new Error(`Server not found: ${entry.serverId}`);
|
|
1003
1098
|
}
|
|
1099
|
+
if (!CapabilityGrant.of(server).has('channels.lifecycle')) {
|
|
1100
|
+
// Desired state is already recorded — intent sticks; reconciliation
|
|
1101
|
+
// will retry if the grant later widens (§6.7 expansion-on-receipt).
|
|
1102
|
+
throw new Error(`channels.lifecycle not in "${entry.serverId}"'s effective grant (§14.1)`);
|
|
1103
|
+
}
|
|
1004
1104
|
const result = await server.sendChannelsOpen({
|
|
1005
1105
|
channelId: entry.descriptor.id,
|
|
1006
1106
|
type: entry.descriptor.type,
|
|
@@ -1221,7 +1321,7 @@ export class ChannelRegistry {
|
|
|
1221
1321
|
};
|
|
1222
1322
|
}
|
|
1223
1323
|
}
|
|
1224
|
-
async handleToolClose(input) {
|
|
1324
|
+
async handleToolClose(input, origin) {
|
|
1225
1325
|
const resolved = this.resolveToolChannelEntry(input.channelId, input.serverId);
|
|
1226
1326
|
const entry = resolved.entry;
|
|
1227
1327
|
if (!entry) {
|
|
@@ -1238,7 +1338,43 @@ export class ChannelRegistry {
|
|
|
1238
1338
|
data: { channelId: input.channelId, status: 'already closed' },
|
|
1239
1339
|
};
|
|
1240
1340
|
}
|
|
1241
|
-
|
|
1341
|
+
// Provenance comes from trusted dispatch, not from self-description:
|
|
1342
|
+
// only module-origin calls may record a machine source, and only from
|
|
1343
|
+
// the closed vocabulary. Agent-origin (or origin-less — the safe
|
|
1344
|
+
// default for any legacy caller) records 'agent-tool' and has its
|
|
1345
|
+
// machine fields ignored entirely.
|
|
1346
|
+
const isModuleOrigin = origin?.kind === 'module';
|
|
1347
|
+
let closeSource = 'agent-tool';
|
|
1348
|
+
if (isModuleOrigin && input.source !== undefined) {
|
|
1349
|
+
if (!MACHINE_CLOSE_SOURCES.has(input.source)) {
|
|
1350
|
+
return {
|
|
1351
|
+
success: false,
|
|
1352
|
+
isError: true,
|
|
1353
|
+
error: `Unknown machine close source '${input.source}'. Machine closes must use one of: ` +
|
|
1354
|
+
`${[...MACHINE_CLOSE_SOURCES].join(', ')}.`,
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
closeSource = input.source;
|
|
1358
|
+
}
|
|
1359
|
+
// Housekeeping must not override stated intent (issue #5: GC closes wore
|
|
1360
|
+
// the agent's badge and reset explicitly-opened doors). A machine-sourced
|
|
1361
|
+
// close of a channel whose current desired state is an agent/operator
|
|
1362
|
+
// 'open' is refused — structurally, so the caller can stand down rather
|
|
1363
|
+
// than retry — unless it certifies an explicit idle lease.
|
|
1364
|
+
if (isModuleOrigin && closeSource !== 'agent-tool' && !input.overrideExplicitOpen) {
|
|
1365
|
+
const current = this.desiredStates.get(this.lifecycleKey(entry.serverId, input.channelId));
|
|
1366
|
+
if (current?.state === 'open' && current.source === 'agent-tool') {
|
|
1367
|
+
return {
|
|
1368
|
+
success: false,
|
|
1369
|
+
isError: false,
|
|
1370
|
+
error: `Channel ${input.channelId} was explicitly opened (source: agent-tool); ` +
|
|
1371
|
+
`a '${closeSource}' close does not override stated intent. Machine closes ` +
|
|
1372
|
+
`of explicit opens require an explicit idle lease (overrideExplicitOpen).`,
|
|
1373
|
+
data: { refusal: 'explicit-open', channelId: input.channelId },
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
this.setDesiredState(entry.serverId, input.channelId, 'closed', closeSource);
|
|
1242
1378
|
this.stopTyping(input.channelId);
|
|
1243
1379
|
const server = this.serverRegistry.getServer(entry.serverId);
|
|
1244
1380
|
if (!server) {
|
|
@@ -1249,6 +1385,13 @@ export class ChannelRegistry {
|
|
|
1249
1385
|
};
|
|
1250
1386
|
}
|
|
1251
1387
|
try {
|
|
1388
|
+
if (!CapabilityGrant.of(server).has('channels.lifecycle')) {
|
|
1389
|
+
return {
|
|
1390
|
+
success: false,
|
|
1391
|
+
error: `channels.lifecycle not in "${entry.serverId}"'s effective grant (§14.1)`,
|
|
1392
|
+
isError: true,
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1252
1395
|
await server.sendChannelsClose({ channelId: input.channelId });
|
|
1253
1396
|
entry.open = false;
|
|
1254
1397
|
return {
|
|
@@ -1288,6 +1431,9 @@ export class ChannelRegistry {
|
|
|
1288
1431
|
if (!server) {
|
|
1289
1432
|
acknowledgmentError = `Server not found: ${entry.serverId}`;
|
|
1290
1433
|
}
|
|
1434
|
+
else if (!CapabilityGrant.of(server).has('channels.acknowledge')) {
|
|
1435
|
+
acknowledgmentError = `channels.acknowledge not in "${entry.serverId}"'s effective grant (§14.1)`;
|
|
1436
|
+
}
|
|
1291
1437
|
else {
|
|
1292
1438
|
try {
|
|
1293
1439
|
const result = await server.sendChannelsAcknowledge({
|
|
@@ -1354,6 +1500,13 @@ export class ChannelRegistry {
|
|
|
1354
1500
|
* overloaded use of `think` for staying silent.
|
|
1355
1501
|
*/
|
|
1356
1502
|
handleToolSkipReply(input) {
|
|
1503
|
+
// The self-wake itself is armed by the framework (which owns the
|
|
1504
|
+
// EventGate) BEFORE this dispatch; when the gate is absent the framework
|
|
1505
|
+
// strips the field so this confirmation stays truthful.
|
|
1506
|
+
const wakeSecs = Number(input.wake_in_seconds);
|
|
1507
|
+
const selfWake = Number.isFinite(wakeSecs) && wakeSecs > 0
|
|
1508
|
+
? Math.max(1, Math.min(3600, Math.floor(wakeSecs)))
|
|
1509
|
+
: undefined;
|
|
1357
1510
|
return {
|
|
1358
1511
|
success: true,
|
|
1359
1512
|
// The note says "ended the turn" — make it TRUE. Without endTurn the
|
|
@@ -1367,7 +1520,9 @@ export class ChannelRegistry {
|
|
|
1367
1520
|
// on Fable: ~60% of skip_reply result bytes were verbatim input echo).
|
|
1368
1521
|
data: {
|
|
1369
1522
|
skipped: true,
|
|
1370
|
-
note:
|
|
1523
|
+
note: selfWake !== undefined
|
|
1524
|
+
? `Turn ended; nothing sent. Self-wake in ~${selfWake}s unless something wakes you first.`
|
|
1525
|
+
: 'Turn ended; nothing sent.',
|
|
1371
1526
|
},
|
|
1372
1527
|
};
|
|
1373
1528
|
}
|
|
@@ -1424,7 +1579,12 @@ export class ChannelRegistry {
|
|
|
1424
1579
|
if (matches.length !== 1)
|
|
1425
1580
|
return null;
|
|
1426
1581
|
const server = this.serverRegistry.getServer(matches[0].serverId);
|
|
1427
|
-
|
|
1582
|
+
// §5.4: the GRANT gates streaming, not the raw advertisement. The old
|
|
1583
|
+
// `capabilities?.channels?.streaming` check was doubly wrong: undefined
|
|
1584
|
+
// for the boolean `channels: true` shape (masking discord-mcpl's latent
|
|
1585
|
+
// double-post, AUDIT-001), and pre-policy it would have sent before the
|
|
1586
|
+
// server was told anything was granted.
|
|
1587
|
+
if (!CapabilityGrant.of(server).has('channels.streaming'))
|
|
1428
1588
|
return null;
|
|
1429
1589
|
return server;
|
|
1430
1590
|
}
|
|
@@ -1508,6 +1668,12 @@ export class ChannelRegistry {
|
|
|
1508
1668
|
if (!server) {
|
|
1509
1669
|
return fail(channelId, `server "${entry.serverId}" not found`);
|
|
1510
1670
|
}
|
|
1671
|
+
// §14.1: channels/publish requires channels.publish in the grant. The
|
|
1672
|
+
// host not sending is the enforcement — a send to an ungranted server
|
|
1673
|
+
// would have it act on authority it was never told it has.
|
|
1674
|
+
if (!CapabilityGrant.of(server).has('channels.publish')) {
|
|
1675
|
+
return fail(channelId, `channels.publish not in "${entry.serverId}"'s effective grant (§14.1)`);
|
|
1676
|
+
}
|
|
1511
1677
|
const publishParams = {
|
|
1512
1678
|
conversationId,
|
|
1513
1679
|
channelId,
|
|
@@ -1575,6 +1741,13 @@ export class ChannelRegistry {
|
|
|
1575
1741
|
};
|
|
1576
1742
|
}
|
|
1577
1743
|
try {
|
|
1744
|
+
if (!CapabilityGrant.of(server).has('channels.publish')) {
|
|
1745
|
+
return {
|
|
1746
|
+
success: false,
|
|
1747
|
+
error: `channels.publish not in "${server.id}"'s effective grant (§14.1)`,
|
|
1748
|
+
isError: true,
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1578
1751
|
const publishParams = {
|
|
1579
1752
|
conversationId: '', // Framework will fill this when wired
|
|
1580
1753
|
channelId,
|