@dsh-enhanced/assistant-delivery 0.1.3
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/LICENSE +21 -0
- package/README.md +225 -0
- package/cordis.patch.yml +29 -0
- package/lib/agent-runtime.d.ts +112 -0
- package/lib/agent-runtime.d.ts.map +1 -0
- package/lib/agent-runtime.js +1866 -0
- package/lib/agent-runtime.js.map +1 -0
- package/lib/canonical.d.ts +18 -0
- package/lib/canonical.d.ts.map +1 -0
- package/lib/canonical.js +83 -0
- package/lib/canonical.js.map +1 -0
- package/lib/coordinator.d.ts +124 -0
- package/lib/coordinator.d.ts.map +1 -0
- package/lib/coordinator.js +676 -0
- package/lib/coordinator.js.map +1 -0
- package/lib/inbound-images.d.ts +38 -0
- package/lib/inbound-images.d.ts.map +1 -0
- package/lib/inbound-images.js +267 -0
- package/lib/inbound-images.js.map +1 -0
- package/lib/index.d.ts +12 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +12 -0
- package/lib/index.js.map +1 -0
- package/lib/operator.d.ts +23 -0
- package/lib/operator.d.ts.map +1 -0
- package/lib/operator.js +38 -0
- package/lib/operator.js.map +1 -0
- package/lib/service.d.ts +269 -0
- package/lib/service.d.ts.map +1 -0
- package/lib/service.js +1698 -0
- package/lib/service.js.map +1 -0
- package/lib/session-commands.d.ts +17 -0
- package/lib/session-commands.d.ts.map +1 -0
- package/lib/session-commands.js +39 -0
- package/lib/session-commands.js.map +1 -0
- package/lib/sqlite.d.ts +8 -0
- package/lib/sqlite.d.ts.map +1 -0
- package/lib/sqlite.js +411 -0
- package/lib/sqlite.js.map +1 -0
- package/lib/store.d.ts +353 -0
- package/lib/store.d.ts.map +1 -0
- package/lib/store.js +2614 -0
- package/lib/store.js.map +1 -0
- package/lib/tools.d.ts +4 -0
- package/lib/tools.d.ts.map +1 -0
- package/lib/tools.js +54 -0
- package/lib/tools.js.map +1 -0
- package/lib/types.d.ts +329 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/lib/version.d.ts +2 -0
- package/lib/version.d.ts.map +1 -0
- package/lib/version.js +2 -0
- package/lib/version.js.map +1 -0
- package/package.json +122 -0
package/lib/service.js
ADDED
|
@@ -0,0 +1,1698 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute } from 'node:path';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { Context, Service } from '@deepseek-ai/cordis';
|
|
5
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
6
|
+
import { approvalReviewerOf, isAutoReviewEscalation, } from '@dsh-enhanced/assistant-policy';
|
|
7
|
+
import { DeliveryAdapterRegistry, DeliveryCoordinator, InboundCoordinator, } from './coordinator.js';
|
|
8
|
+
import { DeliveryStore, DeliveryStoreError } from './store.js';
|
|
9
|
+
import { DshDeliveryRuntime } from './agent-runtime.js';
|
|
10
|
+
import { InboundImageMaterializer } from './inbound-images.js';
|
|
11
|
+
import { registerDeliveryTools } from './tools.js';
|
|
12
|
+
import { canonicalConversation, externalPrincipalId } from './canonical.js';
|
|
13
|
+
import { isExactDeliveryCommand, parseDeliveryCommand } from './session-commands.js';
|
|
14
|
+
export class AssistantDeliveryError extends Error {
|
|
15
|
+
code;
|
|
16
|
+
constructor(code, message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.name = 'AssistantDeliveryError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const configSchema = Schema.object({
|
|
23
|
+
databasePath: Schema.string().required(),
|
|
24
|
+
spoolPath: Schema.string().required(),
|
|
25
|
+
schedulerEnabled: Schema.boolean().default(true),
|
|
26
|
+
tickIntervalMs: Schema.number().step(1).min(100).default(1_000),
|
|
27
|
+
leaseMs: Schema.number().step(1).min(1_000).default(30_000),
|
|
28
|
+
maxAttempts: Schema.number().step(1).min(1).max(100).default(5),
|
|
29
|
+
maxConcurrency: Schema.number().step(1).min(1).max(100).default(4),
|
|
30
|
+
maxTextBytes: Schema.number().step(1).min(1).max(16 * 1024 * 1024).default(65_536),
|
|
31
|
+
retryBaseMs: Schema.number().step(1).min(1).default(1_000),
|
|
32
|
+
retryMaxMs: Schema.number().step(1).min(1).default(300_000),
|
|
33
|
+
pairingTtlMs: Schema.number().step(1).min(1).max(86_400_000).default(600_000),
|
|
34
|
+
pairingMaxAttempts: Schema.number().step(1).min(1).max(10).default(5),
|
|
35
|
+
defaultWorkspace: Schema.string().default(process.cwd()),
|
|
36
|
+
defaultAgentPreset: Schema.string().min(1).default('standard'),
|
|
37
|
+
policyRef: Schema.string().min(1).default('owner-dm'),
|
|
38
|
+
agentProvider: Schema.string().min(1).default('deepseek-official'),
|
|
39
|
+
agentModel: Schema.string().min(1).default('deepseek-v4-flash'),
|
|
40
|
+
toolCapableProviders: Schema.array(Schema.string().pattern(/^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/u)).default(['deepseek-official']),
|
|
41
|
+
unknownRouteToolCalls: Schema.union(['allow', 'deny']).default('allow'),
|
|
42
|
+
agentMaxOutputTokens: Schema.number().step(1).min(1).default(8_192),
|
|
43
|
+
modelPickerTtlMs: Schema.number().step(1).min(60_000).max(86_400_000).default(900_000),
|
|
44
|
+
permissionPickerTtlMs: Schema.number().step(1).min(60_000).max(86_400_000).default(900_000),
|
|
45
|
+
toolApprovalTtlMs: Schema.number().step(1).min(1_000).max(300_000).default(300_000),
|
|
46
|
+
});
|
|
47
|
+
function policyDenied(decision) {
|
|
48
|
+
return new AssistantDeliveryError('policy-denied', `assistant-delivery policy denied operation: ${decision.reasonCode}`);
|
|
49
|
+
}
|
|
50
|
+
function pickerIncludesRoute(picker, route) {
|
|
51
|
+
const model = picker.models.find(candidate => candidate.provider === route.provider && candidate.id === route.model);
|
|
52
|
+
return model !== undefined && (route.reasoningEffort === undefined || model.effortIds.includes(route.reasoningEffort));
|
|
53
|
+
}
|
|
54
|
+
function isModelSelectionPayload(value) {
|
|
55
|
+
if (value === null || typeof value !== 'object')
|
|
56
|
+
return false;
|
|
57
|
+
const payload = value;
|
|
58
|
+
const principal = payload.principal;
|
|
59
|
+
return typeof payload.callbackEventId === 'string'
|
|
60
|
+
&& typeof payload.callbackChatId === 'string'
|
|
61
|
+
&& typeof payload.bindingId === 'string'
|
|
62
|
+
&& principal !== undefined
|
|
63
|
+
&& typeof principal.channel === 'string'
|
|
64
|
+
&& typeof principal.account === 'string'
|
|
65
|
+
&& typeof principal.tenant === 'string'
|
|
66
|
+
&& typeof principal.user === 'string'
|
|
67
|
+
&& typeof payload.provider === 'string'
|
|
68
|
+
&& typeof payload.modelProvider === 'string'
|
|
69
|
+
&& typeof payload.model === 'string'
|
|
70
|
+
&& (payload.reasoningEffort === null || typeof payload.reasoningEffort === 'string')
|
|
71
|
+
&& Number.isSafeInteger(payload.expectedRevision);
|
|
72
|
+
}
|
|
73
|
+
const toolApprovalArgumentBytes = 16 * 1024;
|
|
74
|
+
const toolApprovalIdentityBytes = 512;
|
|
75
|
+
const toolApprovalReasonBytes = 2 * 1024;
|
|
76
|
+
const permissionEventTypes = [
|
|
77
|
+
'permission/preset',
|
|
78
|
+
'sandbox/mode',
|
|
79
|
+
'approval/policy',
|
|
80
|
+
'assistant-policy/approval-reviewer',
|
|
81
|
+
];
|
|
82
|
+
const approvalOutcomes = ['allowed-once', 'rejected', 'cancelled', 'unavailable'];
|
|
83
|
+
function currentPermissionEvents(agent) {
|
|
84
|
+
const latest = new Map();
|
|
85
|
+
for (const event of agent.session.events) {
|
|
86
|
+
const type = String(event.type);
|
|
87
|
+
if (!permissionEventTypes.includes(type))
|
|
88
|
+
continue;
|
|
89
|
+
latest.set(type, { seq: event.seq, type, data: event.data });
|
|
90
|
+
}
|
|
91
|
+
return permissionEventTypes.map(type => latest.get(type) ?? null);
|
|
92
|
+
}
|
|
93
|
+
function signalAborted(signal) {
|
|
94
|
+
return signal?.aborted === true;
|
|
95
|
+
}
|
|
96
|
+
function sameToolApprovalAuthority(left, right) {
|
|
97
|
+
return right !== undefined
|
|
98
|
+
&& right.adapter === left.adapter
|
|
99
|
+
&& right.binding.id === left.binding.id
|
|
100
|
+
&& right.binding.version === left.binding.version
|
|
101
|
+
&& right.binding.generation === left.binding.generation
|
|
102
|
+
&& right.policyEmergencyVersion === left.policyEmergencyVersion
|
|
103
|
+
&& right.reviewRoute === left.reviewRoute
|
|
104
|
+
&& right.routeKind === left.routeKind
|
|
105
|
+
&& right.routeToken === left.routeToken
|
|
106
|
+
&& right.actionHash === left.actionHash;
|
|
107
|
+
}
|
|
108
|
+
function serializeCodeDispatchArguments(value) {
|
|
109
|
+
try {
|
|
110
|
+
const serialized = JSON.stringify(value);
|
|
111
|
+
return typeof serialized === 'string' ? serialized : undefined;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function exactToolApprovalCall(events, openTurn, callId, toolName) {
|
|
118
|
+
const matches = [];
|
|
119
|
+
let settled = false;
|
|
120
|
+
for (const event of events) {
|
|
121
|
+
if (event.seq <= openTurn.seq)
|
|
122
|
+
continue;
|
|
123
|
+
if (event.type === 'tool/call'
|
|
124
|
+
&& event.data.turn === openTurn.data.turn
|
|
125
|
+
&& String(event.data.callId) === callId) {
|
|
126
|
+
matches.push({
|
|
127
|
+
seq: event.seq,
|
|
128
|
+
name: event.data.name,
|
|
129
|
+
arguments: event.data.arguments,
|
|
130
|
+
hashIdentity: ['tool/call', event.seq, event.data.turn, event.data.step,
|
|
131
|
+
callId, event.data.name, event.data.arguments],
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
if (event.type === 'tool/code-dispatch-start' && String(event.data.subCallId) === callId) {
|
|
135
|
+
const rootCallId = String(event.data.rootCallId);
|
|
136
|
+
const parentCallId = String(event.data.parentCallId);
|
|
137
|
+
const argumentsJson = serializeCodeDispatchArguments(event.data.arguments);
|
|
138
|
+
if (argumentsJson === undefined
|
|
139
|
+
|| Buffer.byteLength(rootCallId) > toolApprovalIdentityBytes
|
|
140
|
+
|| Buffer.byteLength(parentCallId) > toolApprovalIdentityBytes)
|
|
141
|
+
return undefined;
|
|
142
|
+
matches.push({
|
|
143
|
+
seq: event.seq,
|
|
144
|
+
name: event.data.name,
|
|
145
|
+
arguments: argumentsJson,
|
|
146
|
+
hashIdentity: ['tool/code-dispatch-start', event.seq, rootCallId, parentCallId,
|
|
147
|
+
callId, event.data.name, argumentsJson],
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (event.type === 'tool/result'
|
|
151
|
+
&& String(event.data.message.source.callId) === callId)
|
|
152
|
+
settled = true;
|
|
153
|
+
if (event.type === 'tool/code-dispatch'
|
|
154
|
+
&& String(event.data.subCallId) === callId)
|
|
155
|
+
settled = true;
|
|
156
|
+
}
|
|
157
|
+
const exact = matches.length === 1 ? matches[0] : undefined;
|
|
158
|
+
if (exact === undefined || settled || exact.name !== toolName
|
|
159
|
+
|| Buffer.byteLength(exact.arguments) > toolApprovalArgumentBytes)
|
|
160
|
+
return undefined;
|
|
161
|
+
return exact;
|
|
162
|
+
}
|
|
163
|
+
export class AssistantDeliveryService extends Service {
|
|
164
|
+
static Config = configSchema;
|
|
165
|
+
deliveryStore;
|
|
166
|
+
policy;
|
|
167
|
+
registry;
|
|
168
|
+
outbound;
|
|
169
|
+
inbound;
|
|
170
|
+
config;
|
|
171
|
+
ownerId = `assistant-delivery-${randomUUID()}`;
|
|
172
|
+
bindingFlights = new Map();
|
|
173
|
+
conversationTransitions = new Map();
|
|
174
|
+
agentApprovalBindings = new WeakMap();
|
|
175
|
+
toolApprovalControllers = new Set();
|
|
176
|
+
modelSelectionFlight;
|
|
177
|
+
modelSelectionRetryTimer;
|
|
178
|
+
runtime;
|
|
179
|
+
timer;
|
|
180
|
+
active = true;
|
|
181
|
+
constructor(ctx, input) {
|
|
182
|
+
super(ctx, 'assistantDelivery');
|
|
183
|
+
let config;
|
|
184
|
+
try {
|
|
185
|
+
config = AssistantDeliveryService.Config(input);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
throw new Error(`assistant-delivery: invalid configuration: ${String(error)}`, { cause: error });
|
|
189
|
+
}
|
|
190
|
+
if (!isAbsolute(config.databasePath) || !isAbsolute(config.spoolPath) || !isAbsolute(config.defaultWorkspace)) {
|
|
191
|
+
throw new Error('assistant-delivery: databasePath, spoolPath, and defaultWorkspace must be absolute paths');
|
|
192
|
+
}
|
|
193
|
+
const policy = ctx.get('assistantPolicy');
|
|
194
|
+
if (policy === undefined)
|
|
195
|
+
throw new Error('assistant-delivery: assistantPolicy service is required');
|
|
196
|
+
mkdirSync(config.spoolPath, { recursive: true, mode: 0o700 });
|
|
197
|
+
chmodSync(config.spoolPath, 0o700);
|
|
198
|
+
this.config = config;
|
|
199
|
+
this.policy = policy;
|
|
200
|
+
this.deliveryStore = new DeliveryStore({ path: config.databasePath, maxTextBytes: config.maxTextBytes });
|
|
201
|
+
this.registry = new DeliveryAdapterRegistry({
|
|
202
|
+
accept: async (envelope) => {
|
|
203
|
+
const result = await this.acceptInbound(envelope);
|
|
204
|
+
return { duplicate: result.duplicate, inboxId: result.inboxId, status: result.status };
|
|
205
|
+
},
|
|
206
|
+
receipt: async (receipt) => {
|
|
207
|
+
if (!this.active)
|
|
208
|
+
return;
|
|
209
|
+
this.deliveryStore.recordReceipt(receipt);
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
const imageMaterializer = new InboundImageMaterializer({
|
|
213
|
+
store: this.deliveryStore,
|
|
214
|
+
registry: this.registry,
|
|
215
|
+
getAttachments: () => ctx.get('attachments'),
|
|
216
|
+
isAuthorized: (binding, envelope) => this.isInboundAuthorized(binding, envelope),
|
|
217
|
+
});
|
|
218
|
+
this.outbound = new DeliveryCoordinator({ store: this.deliveryStore, registry: this.registry,
|
|
219
|
+
ownerId: this.ownerId, leaseMs: config.leaseMs, maxAttempts: config.maxAttempts,
|
|
220
|
+
maxConcurrency: config.maxConcurrency, retryBaseMs: config.retryBaseMs, retryMaxMs: config.retryMaxMs });
|
|
221
|
+
this.inbound = new InboundCoordinator({ store: this.deliveryStore, processor: () => this.runtime,
|
|
222
|
+
ownerId: this.ownerId, leaseMs: config.leaseMs, maxAttempts: config.maxAttempts,
|
|
223
|
+
maxConcurrency: config.maxConcurrency, retryBaseMs: config.retryBaseMs, retryMaxMs: config.retryMaxMs });
|
|
224
|
+
ctx.inject(['agents', 'sessions', 'llm'], runtimeCtx => {
|
|
225
|
+
const unregister = this.registerInboundRuntime(new DshDeliveryRuntime(runtimeCtx, policy, {
|
|
226
|
+
sessionNamespace: this.deliveryStore.instanceId(),
|
|
227
|
+
workspace: config.defaultWorkspace, agentPreset: config.defaultAgentPreset, policyRef: config.policyRef,
|
|
228
|
+
getAgentPresets: () => runtimeCtx.get('agentPresets'),
|
|
229
|
+
provider: config.agentProvider, model: config.agentModel, maxOutputTokens: config.agentMaxOutputTokens,
|
|
230
|
+
toolCapableProviders: new Set(config.toolCapableProviders),
|
|
231
|
+
unknownRouteToolCalls: config.unknownRouteToolCalls ?? 'allow',
|
|
232
|
+
modelPickerTtlMs: config.modelPickerTtlMs,
|
|
233
|
+
permissionPickerTtlMs: config.permissionPickerTtlMs,
|
|
234
|
+
getModelSelection: conversation => this.deliveryStore.getModelSelection(conversation),
|
|
235
|
+
imageMaterializer,
|
|
236
|
+
isInboundAuthorized: (binding, envelope) => this.isInboundAuthorized(binding, envelope),
|
|
237
|
+
isPermissionController: (binding, envelope) => this.isPermissionController(binding, envelope),
|
|
238
|
+
authorizePermissionReply: (binding, envelope) => this.authorizePermissionReply(binding, envelope),
|
|
239
|
+
beginModelCommand: conversation => this.deliveryStore.beginModelCommand(conversation),
|
|
240
|
+
commitModelCommand: input => this.deliveryStore.commitModelCommand(input),
|
|
241
|
+
progress: async (binding, eventId, update) => {
|
|
242
|
+
const adapter = this.registry.get(binding.conversation.channel, binding.conversation.account);
|
|
243
|
+
await adapter?.progress?.({
|
|
244
|
+
bindingId: binding.id,
|
|
245
|
+
target: { conversation: binding.conversation, principal: binding.principal },
|
|
246
|
+
eventId,
|
|
247
|
+
update,
|
|
248
|
+
});
|
|
249
|
+
},
|
|
250
|
+
replyCommand: (binding, eventId, reply, idempotencyKey) => {
|
|
251
|
+
this.replyCommand(binding, {
|
|
252
|
+
idempotencyKey: idempotencyKey ?? this.inboundReplyIdempotencyKey(binding, eventId),
|
|
253
|
+
text: reply.text,
|
|
254
|
+
replyToEventId: eventId,
|
|
255
|
+
...(reply.format === undefined ? {} : { format: reply.format }),
|
|
256
|
+
...(reply.modelPicker === undefined ? {} : { modelPicker: reply.modelPicker }),
|
|
257
|
+
...(reply.permissionPicker === undefined ? {} : { permissionPicker: reply.permissionPicker }),
|
|
258
|
+
});
|
|
259
|
+
},
|
|
260
|
+
reply: (agent, eventId, reply) => {
|
|
261
|
+
this.reply(agent, {
|
|
262
|
+
idempotencyKey: `inbound:${eventId}:reply`,
|
|
263
|
+
text: reply.text,
|
|
264
|
+
replyToEventId: eventId,
|
|
265
|
+
...(reply.format === undefined ? {} : { format: reply.format }),
|
|
266
|
+
...(reply.modelPicker === undefined ? {} : { modelPicker: reply.modelPicker }),
|
|
267
|
+
...(reply.permissionPicker === undefined ? {} : { permissionPicker: reply.permissionPicker }),
|
|
268
|
+
});
|
|
269
|
+
},
|
|
270
|
+
}));
|
|
271
|
+
void this.drainModelSelectionSettlements();
|
|
272
|
+
return unregister;
|
|
273
|
+
});
|
|
274
|
+
ctx.inject(['approval'], approvalCtx => approvalCtx.on('approval/request', (request, next) => this.requestToolApproval(approvalCtx, request, next)));
|
|
275
|
+
ctx.inject(['tools'], toolsCtx => registerDeliveryTools(toolsCtx, this));
|
|
276
|
+
if (config.schedulerEnabled)
|
|
277
|
+
this.start();
|
|
278
|
+
ctx.effect(() => async () => {
|
|
279
|
+
this.active = false;
|
|
280
|
+
for (const controller of this.toolApprovalControllers) {
|
|
281
|
+
controller.abort(new Error('assistant-delivery is stopping'));
|
|
282
|
+
}
|
|
283
|
+
await this.stopInternal();
|
|
284
|
+
await this.registry.stop();
|
|
285
|
+
this.deliveryStore.close();
|
|
286
|
+
}, 'assistant-delivery.runtime');
|
|
287
|
+
}
|
|
288
|
+
issuePairing(operatorId, principal) {
|
|
289
|
+
this.assertActive();
|
|
290
|
+
const decision = this.policy.authorize({ subject: { kind: 'external', id: `local:${operatorId}` },
|
|
291
|
+
action: 'pair.issue', resource: { kind: 'message', id: 'pairing' }, context: { initiator: 'foreground' } });
|
|
292
|
+
if (decision.effect !== 'allow')
|
|
293
|
+
throw policyDenied(decision);
|
|
294
|
+
return this.deliveryStore.issuePairing(principal, { ttlMs: this.config.pairingTtlMs,
|
|
295
|
+
maxAttempts: this.config.pairingMaxAttempts });
|
|
296
|
+
}
|
|
297
|
+
confirmPairing(input) {
|
|
298
|
+
this.assertActive();
|
|
299
|
+
const decision = this.policy.authorize({ subject: { kind: 'external', id: externalPrincipalId(input.principal) },
|
|
300
|
+
action: 'pair.confirm', resource: { kind: 'message', id: 'pairing' }, context: { initiator: 'external' } });
|
|
301
|
+
if (decision.effect !== 'allow')
|
|
302
|
+
throw policyDenied(decision);
|
|
303
|
+
return this.deliveryStore.confirmPairing(input);
|
|
304
|
+
}
|
|
305
|
+
linkPrincipal(input) {
|
|
306
|
+
this.assertActive();
|
|
307
|
+
const decision = this.policy.authorize({ subject: { kind: 'external', id: `local:${input.operatorId}` },
|
|
308
|
+
action: 'pair.link', resource: { kind: 'message', id: 'principal-link' }, context: { initiator: 'foreground' } });
|
|
309
|
+
if (decision.effect !== 'allow')
|
|
310
|
+
throw policyDenied(decision);
|
|
311
|
+
return this.deliveryStore.linkPrincipal(input);
|
|
312
|
+
}
|
|
313
|
+
validateApprovalSettlement(input) {
|
|
314
|
+
const binding = this.deliveryStore.getBinding(input.bindingId);
|
|
315
|
+
const current = this.deliveryStore.getPrincipal(input.principal);
|
|
316
|
+
if (binding?.status !== 'active' || current?.status !== 'active' || current.role !== 'owner'
|
|
317
|
+
|| binding.conversation.chat !== input.callbackChatId
|
|
318
|
+
|| JSON.stringify(binding.principal) !== JSON.stringify(current.principal)
|
|
319
|
+
|| JSON.stringify(binding.principal) !== JSON.stringify(input.principal)) {
|
|
320
|
+
throw new AssistantDeliveryError('missing-binding', 'approval callback principal or chat does not own the active binding');
|
|
321
|
+
}
|
|
322
|
+
const approval = this.deliveryStore.getApprovalIntent(input.operationId, input.bindingId);
|
|
323
|
+
if (approval === undefined
|
|
324
|
+
|| approval.proposalId !== input.proposalId
|
|
325
|
+
|| approval.expectedVersion !== input.expectedVersion
|
|
326
|
+
|| approval.diffHash !== input.diffHash) {
|
|
327
|
+
throw new AssistantDeliveryError('missing-binding', 'approval callback does not match a persisted approval operation');
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
approval,
|
|
331
|
+
principal: externalPrincipalId(current.principal),
|
|
332
|
+
payload: { callbackEventId: input.callbackEventId, callbackChatId: input.callbackChatId,
|
|
333
|
+
bindingId: input.bindingId, principal: current.principal, proposalId: input.proposalId,
|
|
334
|
+
expectedVersion: input.expectedVersion, diffHash: input.diffHash,
|
|
335
|
+
decision: input.decision, reason: input.reason },
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
settleApproval(input) {
|
|
339
|
+
this.assertActive();
|
|
340
|
+
const { approval, payload, principal } = this.validateApprovalSettlement(input);
|
|
341
|
+
if (Date.now() >= approval.expiresAt) {
|
|
342
|
+
throw new AssistantDeliveryError('missing-binding', 'approval callback expired before settlement');
|
|
343
|
+
}
|
|
344
|
+
let settlement;
|
|
345
|
+
try {
|
|
346
|
+
settlement = this.deliveryStore.beginApprovalSettlement({ operationId: input.operationId, payload,
|
|
347
|
+
createIfMissing: false });
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
if (!(error instanceof DeliveryStoreError) || error.code !== 'not-found')
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
const proposal = this.policy.getProposal(input.proposalId);
|
|
354
|
+
const immutableProposalMatches = (candidate) => candidate !== undefined
|
|
355
|
+
&& candidate.principal === principal
|
|
356
|
+
&& candidate.expiresAt === approval.expiresAt
|
|
357
|
+
&& candidate.diffHash === approval.diffHash
|
|
358
|
+
&& candidate.summary === approval.title;
|
|
359
|
+
const pendingDecision = proposal !== undefined
|
|
360
|
+
&& immutableProposalMatches(proposal)
|
|
361
|
+
&& proposal.status === 'pending'
|
|
362
|
+
&& Date.now() < proposal.expiresAt
|
|
363
|
+
&& proposal.version === approval.expectedVersion;
|
|
364
|
+
const terminalReplay = (candidate) => candidate !== undefined
|
|
365
|
+
&& immutableProposalMatches(candidate)
|
|
366
|
+
&& approval.expectedVersion < Number.MAX_SAFE_INTEGER
|
|
367
|
+
&& candidate.version === approval.expectedVersion + 1
|
|
368
|
+
&& candidate.status === input.decision
|
|
369
|
+
&& candidate.decidedBy === principal
|
|
370
|
+
&& candidate.decisionReason === input.reason;
|
|
371
|
+
const terminalDecision = terminalReplay(proposal);
|
|
372
|
+
if (!pendingDecision && !terminalDecision) {
|
|
373
|
+
throw new AssistantDeliveryError('missing-binding', 'approval proposal no longer matches the persisted operation');
|
|
374
|
+
}
|
|
375
|
+
if (settlement === undefined) {
|
|
376
|
+
if (!pendingDecision) {
|
|
377
|
+
throw new AssistantDeliveryError('missing-binding', 'terminal approval has no prior durable settlement');
|
|
378
|
+
}
|
|
379
|
+
settlement = this.deliveryStore.beginApprovalSettlement({ operationId: input.operationId, payload });
|
|
380
|
+
}
|
|
381
|
+
if (settlement.result !== undefined) {
|
|
382
|
+
if (!terminalReplay(this.policy.getProposal(input.proposalId))) {
|
|
383
|
+
throw new AssistantDeliveryError('missing-binding', 'completed approval no longer matches Policy');
|
|
384
|
+
}
|
|
385
|
+
return settlement.result;
|
|
386
|
+
}
|
|
387
|
+
if (pendingDecision) {
|
|
388
|
+
const authorization = this.policy.authorize({ subject: { kind: 'external', id: principal },
|
|
389
|
+
action: 'approval.decide', resource: { kind: 'message', id: input.bindingId },
|
|
390
|
+
context: { initiator: 'external' } }, { idempotencyKey: `approval-callback:${input.operationId}` });
|
|
391
|
+
if (authorization.effect !== 'allow')
|
|
392
|
+
throw policyDenied(authorization);
|
|
393
|
+
}
|
|
394
|
+
const result = this.policy.decideProposal({ proposalId: input.proposalId, principal,
|
|
395
|
+
expectedVersion: input.expectedVersion, decision: input.decision, reason: input.reason });
|
|
396
|
+
return this.deliveryStore.completeApprovalSettlement({ operationId: input.operationId,
|
|
397
|
+
payloadHash: settlement.payloadHash, result });
|
|
398
|
+
}
|
|
399
|
+
recoverApprovalSettlement(input) {
|
|
400
|
+
this.assertActive();
|
|
401
|
+
const { approval, payload, principal } = this.validateApprovalSettlement(input);
|
|
402
|
+
let settlement;
|
|
403
|
+
try {
|
|
404
|
+
settlement = this.deliveryStore.beginApprovalSettlement({ operationId: input.operationId, payload,
|
|
405
|
+
createIfMissing: false });
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
if (error instanceof DeliveryStoreError && error.code === 'not-found')
|
|
409
|
+
return undefined;
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
const proposal = this.policy.getProposal(input.proposalId);
|
|
413
|
+
const exactTerminal = proposal !== undefined
|
|
414
|
+
&& proposal.principal === principal
|
|
415
|
+
&& proposal.expiresAt === approval.expiresAt
|
|
416
|
+
&& proposal.diffHash === approval.diffHash
|
|
417
|
+
&& proposal.summary === approval.title
|
|
418
|
+
&& approval.expectedVersion < Number.MAX_SAFE_INTEGER
|
|
419
|
+
&& proposal.version === approval.expectedVersion + 1
|
|
420
|
+
&& proposal.status === input.decision
|
|
421
|
+
&& proposal.decidedBy === principal
|
|
422
|
+
&& proposal.decisionReason === input.reason;
|
|
423
|
+
if (!exactTerminal)
|
|
424
|
+
return undefined;
|
|
425
|
+
if (settlement.result !== undefined) {
|
|
426
|
+
return settlement.result;
|
|
427
|
+
}
|
|
428
|
+
const result = this.policy.decideProposal({ proposalId: input.proposalId, principal,
|
|
429
|
+
expectedVersion: input.expectedVersion, decision: input.decision, reason: input.reason });
|
|
430
|
+
return this.deliveryStore.completeApprovalSettlement({ operationId: input.operationId,
|
|
431
|
+
payloadHash: settlement.payloadHash, result });
|
|
432
|
+
}
|
|
433
|
+
settleModelSelection(input) {
|
|
434
|
+
this.assertActive();
|
|
435
|
+
const binding = this.deliveryStore.getBinding(input.bindingId);
|
|
436
|
+
const current = this.deliveryStore.getPrincipal(input.principal);
|
|
437
|
+
if (binding?.status !== 'active' || current?.status !== 'active'
|
|
438
|
+
|| binding.conversation.chat !== input.callbackChatId
|
|
439
|
+
|| JSON.stringify(binding.principal) !== JSON.stringify(current.principal)) {
|
|
440
|
+
throw new AssistantDeliveryError('missing-binding', 'model callback principal or chat does not own the active binding');
|
|
441
|
+
}
|
|
442
|
+
const picker = this.deliveryStore.getModelPicker(input.operationId, input.bindingId);
|
|
443
|
+
const expected = {
|
|
444
|
+
revision: input.expectedRevision,
|
|
445
|
+
provider: input.modelProvider,
|
|
446
|
+
model: input.model,
|
|
447
|
+
...(input.reasoningEffort === undefined ? {} : { reasoningEffort: input.reasoningEffort }),
|
|
448
|
+
};
|
|
449
|
+
if (picker === undefined || !pickerIncludesRoute(picker, expected)) {
|
|
450
|
+
throw new AssistantDeliveryError('missing-binding', 'model picker confirmation is unavailable or invalid');
|
|
451
|
+
}
|
|
452
|
+
const authorization = this.policy.authorize({ subject: { kind: 'external', id: externalPrincipalId(current.principal) },
|
|
453
|
+
action: 'ingest', resource: { kind: 'message', id: `model-selection:${binding.id}` },
|
|
454
|
+
context: { initiator: 'external' } }, { idempotencyKey: `model-selection:${input.operationId}:${input.callbackEventId}` });
|
|
455
|
+
if (authorization.effect !== 'allow')
|
|
456
|
+
throw policyDenied(authorization);
|
|
457
|
+
const payload = {
|
|
458
|
+
callbackEventId: input.callbackEventId,
|
|
459
|
+
callbackChatId: input.callbackChatId,
|
|
460
|
+
bindingId: input.bindingId,
|
|
461
|
+
principal: current.principal,
|
|
462
|
+
provider: input.provider,
|
|
463
|
+
modelProvider: input.modelProvider,
|
|
464
|
+
model: input.model,
|
|
465
|
+
reasoningEffort: input.reasoningEffort ?? null,
|
|
466
|
+
expectedRevision: input.expectedRevision,
|
|
467
|
+
};
|
|
468
|
+
const settlement = this.deliveryStore.beginModelSelectionSettlement({
|
|
469
|
+
operationId: input.operationId,
|
|
470
|
+
bindingId: input.bindingId,
|
|
471
|
+
expected,
|
|
472
|
+
payload,
|
|
473
|
+
createIfMissing: Date.now() < picker.expiresAt,
|
|
474
|
+
});
|
|
475
|
+
if (settlement.result !== undefined)
|
|
476
|
+
return settlement.result;
|
|
477
|
+
void this.drainModelSelectionSettlements();
|
|
478
|
+
return { status: 'pending' };
|
|
479
|
+
}
|
|
480
|
+
advanceModelPickerForCallback(input) {
|
|
481
|
+
this.assertActive();
|
|
482
|
+
const binding = this.deliveryStore.getBinding(input.bindingId);
|
|
483
|
+
const current = this.deliveryStore.getPrincipal(input.principal);
|
|
484
|
+
if (binding?.status !== 'active' || current?.status !== 'active'
|
|
485
|
+
|| binding.conversation.chat !== input.callbackChatId
|
|
486
|
+
|| JSON.stringify(binding.principal) !== JSON.stringify(current.principal)) {
|
|
487
|
+
throw new AssistantDeliveryError('missing-binding', 'model callback principal or chat does not own the active binding');
|
|
488
|
+
}
|
|
489
|
+
const picker = this.deliveryStore.getModelPicker(input.operationId, input.bindingId);
|
|
490
|
+
if (picker === undefined || Date.now() >= picker.expiresAt
|
|
491
|
+
|| !pickerIncludesRoute(picker, input.expected) || !pickerIncludesRoute(picker, input.next)) {
|
|
492
|
+
throw new AssistantDeliveryError('missing-binding', 'model picker catalog or route is unavailable');
|
|
493
|
+
}
|
|
494
|
+
return this.deliveryStore.advanceModelPicker({
|
|
495
|
+
operationId: input.operationId,
|
|
496
|
+
bindingId: input.bindingId,
|
|
497
|
+
expected: input.expected,
|
|
498
|
+
next: input.next,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
getModelPickerForCallback(input) {
|
|
502
|
+
this.assertActive();
|
|
503
|
+
const binding = this.deliveryStore.getBinding(input.bindingId);
|
|
504
|
+
const current = this.deliveryStore.getPrincipal(input.principal);
|
|
505
|
+
if (binding?.status !== 'active' || current?.status !== 'active'
|
|
506
|
+
|| binding.conversation.chat !== input.callbackChatId
|
|
507
|
+
|| JSON.stringify(binding.principal) !== JSON.stringify(current.principal)) {
|
|
508
|
+
throw new AssistantDeliveryError('missing-binding', 'model callback principal or chat does not own the active binding');
|
|
509
|
+
}
|
|
510
|
+
const picker = this.deliveryStore.getModelPicker(input.operationId, input.bindingId);
|
|
511
|
+
return picker === undefined || Date.now() >= picker.expiresAt ? undefined : picker;
|
|
512
|
+
}
|
|
513
|
+
async settlePermissionSelection(input) {
|
|
514
|
+
this.assertActive();
|
|
515
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._@:-]{0,255}$/u.test(input.callbackEventId)
|
|
516
|
+
|| !/^[A-Za-z0-9][A-Za-z0-9._@/-]{0,511}$/u.test(input.cardMessageId)
|
|
517
|
+
|| !/^[a-f0-9]{64}$/u.test(input.expectedStateHash)
|
|
518
|
+
|| !Number.isSafeInteger(input.bindingVersion) || input.bindingVersion < 1
|
|
519
|
+
|| !Number.isSafeInteger(input.emergencyStopVersion) || input.emergencyStopVersion < 0
|
|
520
|
+
|| !Number.isSafeInteger(input.issuedAt) || input.issuedAt < 1
|
|
521
|
+
|| !Number.isSafeInteger(input.expiresAt) || input.expiresAt <= input.issuedAt
|
|
522
|
+
|| (input.targetLevel !== 'ask' && input.targetLevel !== 'auto' && input.targetLevel !== 'full')) {
|
|
523
|
+
throw new AssistantDeliveryError('missing-binding', 'permission picker callback shape is invalid');
|
|
524
|
+
}
|
|
525
|
+
const resolveController = () => {
|
|
526
|
+
const emergencyStop = this.policy.getEmergencyStop();
|
|
527
|
+
const binding = this.deliveryStore.getBinding(input.bindingId);
|
|
528
|
+
const principal = this.deliveryStore.getPrincipal(input.principal);
|
|
529
|
+
const active = binding === undefined ? undefined : this.deliveryStore.getActiveBinding(binding.conversation);
|
|
530
|
+
if (binding?.status !== 'active' || active?.id !== binding.id
|
|
531
|
+
|| principal?.status !== 'active' || principal.role !== 'owner')
|
|
532
|
+
return undefined;
|
|
533
|
+
const record = this.deliveryStore.getPermissionPickerRecord(input.operationId, input.bindingId);
|
|
534
|
+
const picker = record?.intent.permissionPicker;
|
|
535
|
+
if (record === undefined || picker === undefined
|
|
536
|
+
|| binding.version !== input.bindingVersion || picker.bindingVersion !== input.bindingVersion
|
|
537
|
+
|| binding.sessionId !== input.sessionId || picker.sessionId !== input.sessionId
|
|
538
|
+
|| binding.conversation.kind !== 'dm' || binding.conversation.chat !== input.callbackChatId
|
|
539
|
+
|| externalPrincipalId(binding.principal) !== externalPrincipalId(input.principal)
|
|
540
|
+
|| externalPrincipalId(record.intent.target.principal) !== externalPrincipalId(input.principal)
|
|
541
|
+
|| JSON.stringify(record.intent.target.conversation) !== JSON.stringify(binding.conversation)
|
|
542
|
+
|| picker.operationId !== input.operationId
|
|
543
|
+
|| picker.issuedAt !== input.issuedAt || picker.expiresAt !== input.expiresAt
|
|
544
|
+
|| picker.expectedStateHash !== input.expectedStateHash
|
|
545
|
+
|| picker.emergencyStopVersion !== input.emergencyStopVersion
|
|
546
|
+
|| emergencyStop.enabled || emergencyStop.version !== picker.emergencyStopVersion
|
|
547
|
+
|| Date.now() >= picker.expiresAt
|
|
548
|
+
|| !['accepted', 'delivered', 'read'].includes(record.status)
|
|
549
|
+
|| record.providerMessageId !== input.cardMessageId)
|
|
550
|
+
return undefined;
|
|
551
|
+
return { binding, picker, principal };
|
|
552
|
+
};
|
|
553
|
+
const controller = resolveController();
|
|
554
|
+
if (controller === undefined) {
|
|
555
|
+
throw new AssistantDeliveryError('missing-binding', 'permission picker no longer owns the active session');
|
|
556
|
+
}
|
|
557
|
+
const { binding, picker, principal } = controller;
|
|
558
|
+
const command = input.targetLevel === 'full'
|
|
559
|
+
? '/permission full confirm'
|
|
560
|
+
: `/permission ${input.targetLevel}`;
|
|
561
|
+
const envelope = {
|
|
562
|
+
channel: binding.conversation.channel,
|
|
563
|
+
account: binding.conversation.account,
|
|
564
|
+
eventId: input.cardMessageId,
|
|
565
|
+
occurredAt: picker.issuedAt,
|
|
566
|
+
principal: { ...principal.principal },
|
|
567
|
+
conversation: { ...binding.conversation },
|
|
568
|
+
kind: 'command',
|
|
569
|
+
text: command,
|
|
570
|
+
metadata: {
|
|
571
|
+
'permission-picker-operation': picker.operationId,
|
|
572
|
+
'permission-picker-state': picker.expectedStateHash,
|
|
573
|
+
'permission-picker-expires-at': String(picker.expiresAt),
|
|
574
|
+
'permission-picker-emergency-version': String(picker.emergencyStopVersion),
|
|
575
|
+
'permission-picker-callback': input.callbackEventId,
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
if (!this.isPermissionController(binding, envelope)) {
|
|
579
|
+
throw new AssistantDeliveryError('missing-binding', 'permission picker owner authorization was revoked');
|
|
580
|
+
}
|
|
581
|
+
const accepted = this.deliveryStore.acceptInbound(envelope);
|
|
582
|
+
let inbox = accepted.record;
|
|
583
|
+
if (inbox.status === 'dead_letter') {
|
|
584
|
+
throw new AssistantDeliveryError('runtime-conflict', 'permission picker selection previously failed');
|
|
585
|
+
}
|
|
586
|
+
if (!['received', 'authorized'].includes(inbox.status)) {
|
|
587
|
+
return { duplicate: accepted.duplicate, inboxId: inbox.id, status: inbox.status };
|
|
588
|
+
}
|
|
589
|
+
return await this.runConversationTransition(binding.conversation, async () => {
|
|
590
|
+
await this.recoverPendingInbound(envelope, inbox.id);
|
|
591
|
+
inbox = this.deliveryStore.getInbox(inbox.id);
|
|
592
|
+
if (inbox.status === 'dead_letter') {
|
|
593
|
+
throw new AssistantDeliveryError('runtime-conflict', 'permission picker selection previously failed');
|
|
594
|
+
}
|
|
595
|
+
if (!['received', 'authorized'].includes(inbox.status)) {
|
|
596
|
+
return { duplicate: accepted.duplicate, inboxId: inbox.id, status: inbox.status };
|
|
597
|
+
}
|
|
598
|
+
const current = resolveController();
|
|
599
|
+
if (current === undefined || !this.isPermissionController(current.binding, envelope)) {
|
|
600
|
+
inbox = this.deliveryStore.deadLetterInbox(inbox.id, 'permission-session-superseded');
|
|
601
|
+
throw new AssistantDeliveryError('missing-binding', 'permission picker no longer owns the active session');
|
|
602
|
+
}
|
|
603
|
+
if (!this.authorizePermissionReply(current.binding, envelope)) {
|
|
604
|
+
inbox = this.deliveryStore.deadLetterInbox(inbox.id, 'permission-reply-authorization-denied');
|
|
605
|
+
throw new AssistantDeliveryError('policy-denied', 'permission picker terminal reply authorization was denied');
|
|
606
|
+
}
|
|
607
|
+
const decision = this.policy.authorize({
|
|
608
|
+
subject: { kind: 'external', id: externalPrincipalId(input.principal) },
|
|
609
|
+
action: 'ingest',
|
|
610
|
+
resource: { kind: 'message', id: `inbound:${envelope.channel}/${envelope.account}` },
|
|
611
|
+
context: { initiator: 'external' },
|
|
612
|
+
}, { idempotencyKey: `message-inbound:${inbox.id}` });
|
|
613
|
+
if (decision.effect !== 'allow' || !this.isPermissionController(current.binding, envelope)) {
|
|
614
|
+
inbox = this.deliveryStore.deadLetterInbox(inbox.id, decision.effect !== 'allow' ? `policy-${decision.reasonCode}` : 'permission-authorization-revoked');
|
|
615
|
+
throw new AssistantDeliveryError('policy-denied', 'permission picker owner authorization was revoked');
|
|
616
|
+
}
|
|
617
|
+
inbox = this.deliveryStore.queueInbox(inbox.id, current.binding.id);
|
|
618
|
+
return { duplicate: accepted.duplicate, inboxId: inbox.id, status: inbox.status };
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
resolveDeadLetter(input) {
|
|
622
|
+
this.assertActive();
|
|
623
|
+
const decision = this.policy.authorize({ subject: { kind: 'external', id: `local:${input.operatorId}` },
|
|
624
|
+
action: 'delivery.resolve', resource: { kind: 'message', id: `${input.kind}:${input.id}` },
|
|
625
|
+
context: { initiator: 'foreground' } }, { idempotencyKey: `delivery-resolve:${input.kind}:${input.id}:${input.expectedAttemptCount}:${input.resolution}` });
|
|
626
|
+
if (decision.effect !== 'allow')
|
|
627
|
+
throw policyDenied(decision);
|
|
628
|
+
return input.kind === 'inbox'
|
|
629
|
+
? this.deliveryStore.resolveInbox({ inboxId: input.id, expectedAttemptCount: input.expectedAttemptCount,
|
|
630
|
+
resolution: input.resolution })
|
|
631
|
+
: this.deliveryStore.resolveOutbox({ outboxId: input.id, expectedAttemptCount: input.expectedAttemptCount,
|
|
632
|
+
resolution: input.resolution });
|
|
633
|
+
}
|
|
634
|
+
registerInboundRuntime(runtime) {
|
|
635
|
+
this.assertActive();
|
|
636
|
+
if (this.runtime !== undefined)
|
|
637
|
+
throw new AssistantDeliveryError('runtime-conflict', 'inbound runtime is already registered');
|
|
638
|
+
this.runtime = runtime;
|
|
639
|
+
let active = true;
|
|
640
|
+
return () => {
|
|
641
|
+
if (!active)
|
|
642
|
+
return;
|
|
643
|
+
active = false;
|
|
644
|
+
if (this.runtime === runtime)
|
|
645
|
+
this.runtime = undefined;
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
registerAdapter(adapter) {
|
|
649
|
+
this.assertActive();
|
|
650
|
+
return this.registry.register(adapter);
|
|
651
|
+
}
|
|
652
|
+
health() {
|
|
653
|
+
this.assertActive();
|
|
654
|
+
return { ...this.deliveryStore.health(), adapters: this.registry.size() };
|
|
655
|
+
}
|
|
656
|
+
isInboundAuthorized(binding, envelope) {
|
|
657
|
+
if (!this.active)
|
|
658
|
+
return false;
|
|
659
|
+
const current = this.deliveryStore.getBinding(binding.id);
|
|
660
|
+
const active = this.deliveryStore.getActiveBinding(envelope.conversation);
|
|
661
|
+
const principal = this.deliveryStore.getPrincipal(envelope.principal);
|
|
662
|
+
const envelopePrincipal = externalPrincipalId(envelope.principal);
|
|
663
|
+
if (current?.status !== 'active'
|
|
664
|
+
|| active?.id !== current.id
|
|
665
|
+
|| current.version !== binding.version
|
|
666
|
+
|| current.sessionId !== binding.sessionId
|
|
667
|
+
|| current.generation !== binding.generation
|
|
668
|
+
|| principal?.status !== 'active'
|
|
669
|
+
|| externalPrincipalId(current.principal) !== envelopePrincipal
|
|
670
|
+
|| externalPrincipalId(binding.principal) !== envelopePrincipal
|
|
671
|
+
|| JSON.stringify(current.conversation) !== JSON.stringify(envelope.conversation)
|
|
672
|
+
|| envelope.channel !== current.conversation.channel
|
|
673
|
+
|| envelope.account !== current.conversation.account) {
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
return this.policy.evaluate({
|
|
677
|
+
subject: { kind: 'external', id: envelopePrincipal },
|
|
678
|
+
action: 'ingest',
|
|
679
|
+
resource: { kind: 'message', id: `inbound:${envelope.channel}/${envelope.account}` },
|
|
680
|
+
context: { initiator: 'external' },
|
|
681
|
+
}).effect === 'allow';
|
|
682
|
+
}
|
|
683
|
+
isPermissionController(binding, envelope) {
|
|
684
|
+
if (!this.isInboundAuthorized(binding, envelope))
|
|
685
|
+
return false;
|
|
686
|
+
const principal = this.deliveryStore.getPrincipal(binding.principal);
|
|
687
|
+
if (principal?.status !== 'active' || principal.role !== 'owner')
|
|
688
|
+
return false;
|
|
689
|
+
// A permission mutation and its mandatory terminal reply are one control
|
|
690
|
+
// operation. Prove the exact bound Agent can reply before accepting a card
|
|
691
|
+
// callback or allowing the runtime to append any permission event.
|
|
692
|
+
return this.policy.evaluate({
|
|
693
|
+
subject: {
|
|
694
|
+
kind: 'agent',
|
|
695
|
+
id: binding.agentPreset,
|
|
696
|
+
workspace: binding.workspace,
|
|
697
|
+
principal: externalPrincipalId(binding.principal),
|
|
698
|
+
},
|
|
699
|
+
action: 'reply',
|
|
700
|
+
resource: { kind: 'message', id: binding.id },
|
|
701
|
+
context: { initiator: 'external' },
|
|
702
|
+
}).effect === 'allow';
|
|
703
|
+
}
|
|
704
|
+
authorizePermissionReply(binding, envelope) {
|
|
705
|
+
if (!this.isPermissionController(binding, envelope))
|
|
706
|
+
return false;
|
|
707
|
+
const decision = this.policy.authorize({
|
|
708
|
+
subject: {
|
|
709
|
+
kind: 'agent',
|
|
710
|
+
id: binding.agentPreset,
|
|
711
|
+
workspace: binding.workspace,
|
|
712
|
+
principal: externalPrincipalId(binding.principal),
|
|
713
|
+
},
|
|
714
|
+
action: 'reply',
|
|
715
|
+
resource: { kind: 'message', id: binding.id },
|
|
716
|
+
context: { initiator: 'external' },
|
|
717
|
+
}, { idempotencyKey: `message-reply:${this.inboundReplyIdempotencyKey(binding, envelope.eventId)}` });
|
|
718
|
+
return decision.effect === 'allow' && this.isPermissionController(binding, envelope);
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Provider event ids are scoped to an adapter account, while Outbox and the
|
|
722
|
+
* Policy ledger share one database-wide idempotency namespace. Anchor every
|
|
723
|
+
* command reply to the accepted Inbox row, whose id is stable across reopen.
|
|
724
|
+
*/
|
|
725
|
+
inboundReplyIdempotencyKey(binding, eventId) {
|
|
726
|
+
const inbox = this.deliveryStore.getInboxByProviderEvent(binding.conversation.channel, binding.conversation.account, eventId);
|
|
727
|
+
if (inbox === undefined
|
|
728
|
+
|| (inbox.bindingId !== undefined && inbox.bindingId !== binding.id)
|
|
729
|
+
|| externalPrincipalId(inbox.envelope.principal) !== externalPrincipalId(binding.principal)
|
|
730
|
+
|| JSON.stringify(inbox.envelope.conversation) !== JSON.stringify(binding.conversation)) {
|
|
731
|
+
throw new AssistantDeliveryError('missing-binding', 'control reply does not belong to a durable Inbox on the active binding');
|
|
732
|
+
}
|
|
733
|
+
return `inbound:${inbox.id}:reply`;
|
|
734
|
+
}
|
|
735
|
+
hasActivePrincipal(binding) {
|
|
736
|
+
return this.deliveryStore.getPrincipal(binding.principal)?.status === 'active';
|
|
737
|
+
}
|
|
738
|
+
toolApprovalRoute(agent) {
|
|
739
|
+
const direct = this.deliveryStore.getBindingBySession(String(agent.session.id));
|
|
740
|
+
const delegatedEntry = this.agentApprovalBindings.get(agent);
|
|
741
|
+
const delegated = delegatedEntry === undefined
|
|
742
|
+
? undefined
|
|
743
|
+
: this.deliveryStore.getBinding(delegatedEntry.bindingId);
|
|
744
|
+
if (direct === undefined && delegatedEntry === undefined)
|
|
745
|
+
return { state: 'none' };
|
|
746
|
+
if ((delegatedEntry !== undefined && delegated === undefined)
|
|
747
|
+
|| (direct !== undefined && delegated !== undefined && direct.id !== delegated.id)) {
|
|
748
|
+
return { state: 'invalid' };
|
|
749
|
+
}
|
|
750
|
+
const binding = direct ?? delegated;
|
|
751
|
+
if (binding?.status !== 'active')
|
|
752
|
+
return { state: 'invalid' };
|
|
753
|
+
return direct === undefined
|
|
754
|
+
? { state: 'bound', binding, kind: 'delegated', token: delegatedEntry.token }
|
|
755
|
+
: { state: 'bound', binding, kind: 'direct' };
|
|
756
|
+
}
|
|
757
|
+
resolveToolApprovalAuthority(ctx, request) {
|
|
758
|
+
if (!this.active || request.callId === undefined)
|
|
759
|
+
return undefined;
|
|
760
|
+
const emergencyStop = this.policy.getEmergencyStop();
|
|
761
|
+
if (emergencyStop.enabled)
|
|
762
|
+
return undefined;
|
|
763
|
+
const agents = ctx.get('agents');
|
|
764
|
+
const sessions = ctx.get('sessions');
|
|
765
|
+
const agent = request.agent;
|
|
766
|
+
const events = agent.session.events;
|
|
767
|
+
const reviewer = approvalReviewerOf(events);
|
|
768
|
+
const reviewRoute = reviewer === 'user'
|
|
769
|
+
? 'user'
|
|
770
|
+
: reviewer === 'auto-review' && isAutoReviewEscalation(request)
|
|
771
|
+
? 'auto-escalation'
|
|
772
|
+
: undefined;
|
|
773
|
+
const sessionId = String(agent.session.id);
|
|
774
|
+
const route = this.toolApprovalRoute(agent);
|
|
775
|
+
if (route.state !== 'bound')
|
|
776
|
+
return undefined;
|
|
777
|
+
const { binding } = route;
|
|
778
|
+
if (agents === undefined || sessions === undefined
|
|
779
|
+
|| String(agent.id) !== sessionId
|
|
780
|
+
|| agents.get(agent.id) !== agent
|
|
781
|
+
|| sessions.get(agent.id) !== agent.session
|
|
782
|
+
|| (route.kind === 'direct' && binding.sessionId !== sessionId)
|
|
783
|
+
|| agent.session.header.cwd !== binding.workspace
|
|
784
|
+
|| agent.session.header.agentPreset !== binding.agentPreset
|
|
785
|
+
|| binding.conversation.kind !== 'dm'
|
|
786
|
+
|| reviewRoute === undefined) {
|
|
787
|
+
return undefined;
|
|
788
|
+
}
|
|
789
|
+
const active = this.deliveryStore.getActiveBinding(binding.conversation);
|
|
790
|
+
const owner = this.deliveryStore.getPrincipal(binding.principal);
|
|
791
|
+
if (active?.id !== binding.id || active.version !== binding.version
|
|
792
|
+
|| active.sessionId !== binding.sessionId || active.generation !== binding.generation
|
|
793
|
+
|| JSON.stringify(active.principal) !== JSON.stringify(binding.principal)
|
|
794
|
+
|| JSON.stringify(active.conversation) !== JSON.stringify(binding.conversation)
|
|
795
|
+
|| owner?.status !== 'active' || owner.role !== 'owner'
|
|
796
|
+
|| JSON.stringify(owner.principal) !== JSON.stringify(binding.principal)) {
|
|
797
|
+
return undefined;
|
|
798
|
+
}
|
|
799
|
+
const callId = String(request.callId);
|
|
800
|
+
if (Buffer.byteLength(callId) > toolApprovalIdentityBytes
|
|
801
|
+
|| Buffer.byteLength(request.toolName) > toolApprovalIdentityBytes
|
|
802
|
+
|| (request.reason !== undefined && Buffer.byteLength(request.reason) > toolApprovalReasonBytes)) {
|
|
803
|
+
return undefined;
|
|
804
|
+
}
|
|
805
|
+
const openTurn = events.findLast(event => event.type === 'turn/start' || event.type === 'turn/end');
|
|
806
|
+
if (openTurn?.type !== 'turn/start')
|
|
807
|
+
return undefined;
|
|
808
|
+
const call = exactToolApprovalCall(events, openTurn, callId, request.toolName);
|
|
809
|
+
if (call === undefined)
|
|
810
|
+
return undefined;
|
|
811
|
+
const decided = new Set(events.filter(event => event.type === 'approval/decided')
|
|
812
|
+
.map(event => String(event.data.id)));
|
|
813
|
+
const asked = events.filter((event) => event.type === 'approval/asked'
|
|
814
|
+
&& event.seq > call.seq
|
|
815
|
+
&& event.seq > openTurn.seq
|
|
816
|
+
&& event.data.toolName === request.toolName
|
|
817
|
+
&& event.data.callId !== undefined
|
|
818
|
+
&& String(event.data.callId) === callId
|
|
819
|
+
&& event.data.reason === request.reason
|
|
820
|
+
&& !decided.has(String(event.data.id)));
|
|
821
|
+
if (asked.length !== 1)
|
|
822
|
+
return undefined;
|
|
823
|
+
const ask = asked[0];
|
|
824
|
+
const requestHeader = events.findLast(event => event.type === 'request/header');
|
|
825
|
+
const adapter = this.registry.get(binding.conversation.channel, binding.conversation.account);
|
|
826
|
+
if (adapter?.capabilities.toolApprovals !== true || adapter.requestToolApproval === undefined)
|
|
827
|
+
return undefined;
|
|
828
|
+
const header = agent.session.header;
|
|
829
|
+
const actionHash = createHash('sha256').update(JSON.stringify([
|
|
830
|
+
'assistant-delivery/tool-approval',
|
|
831
|
+
3,
|
|
832
|
+
['binding', binding.id, binding.version, binding.generation, binding.sessionId,
|
|
833
|
+
binding.workspace, binding.agentPreset, binding.policyRef,
|
|
834
|
+
[binding.conversation.channel, binding.conversation.account, binding.conversation.tenant,
|
|
835
|
+
binding.conversation.kind, binding.conversation.chat, binding.conversation.thread ?? null],
|
|
836
|
+
[binding.principal.channel, binding.principal.account, binding.principal.tenant, binding.principal.user]],
|
|
837
|
+
['owner', owner.id, owner.version, owner.role, owner.status, owner.linkedToId ?? null,
|
|
838
|
+
[owner.principal.channel, owner.principal.account, owner.principal.tenant, owner.principal.user]],
|
|
839
|
+
['session', header.version, sessionId, header.createdAt, header.cwd ?? null,
|
|
840
|
+
header.parentSession ?? null, header.seedLength ?? null, header.origin ?? null,
|
|
841
|
+
header.delegationDepth ?? null, header.agentPreset ?? null],
|
|
842
|
+
['route-kind', route.kind],
|
|
843
|
+
['review-route', reviewRoute],
|
|
844
|
+
['turn', openTurn.seq, openTurn.data.turn],
|
|
845
|
+
['call', ...call.hashIdentity],
|
|
846
|
+
['ask', ask.seq, String(ask.data.id), ask.data.toolName, String(ask.data.callId), ask.data.reason ?? null],
|
|
847
|
+
['request-route', requestHeader === undefined ? null : [requestHeader.seq,
|
|
848
|
+
requestHeader.data.header.config.provider, requestHeader.data.header.config.model,
|
|
849
|
+
requestHeader.data.header.config.reasoningEffort ?? null]],
|
|
850
|
+
['reviewer', reviewer],
|
|
851
|
+
['policy-emergency-stop', emergencyStop.version, emergencyStop.enabled],
|
|
852
|
+
['permission-events', currentPermissionEvents(agent)],
|
|
853
|
+
])).digest('hex');
|
|
854
|
+
return { adapter, binding, reviewRoute, routeKind: route.kind,
|
|
855
|
+
...(route.token === undefined ? {} : { routeToken: route.token }),
|
|
856
|
+
policyEmergencyVersion: emergencyStop.version,
|
|
857
|
+
actionHash, arguments: call.arguments, callId };
|
|
858
|
+
}
|
|
859
|
+
async requestToolApproval(ctx, request, next) {
|
|
860
|
+
let initialRoute;
|
|
861
|
+
try {
|
|
862
|
+
initialRoute = this.toolApprovalRoute(request.agent);
|
|
863
|
+
}
|
|
864
|
+
catch {
|
|
865
|
+
return 'unavailable';
|
|
866
|
+
}
|
|
867
|
+
if (initialRoute.state === 'none')
|
|
868
|
+
return next();
|
|
869
|
+
let reviewer;
|
|
870
|
+
try {
|
|
871
|
+
reviewer = approvalReviewerOf(request.agent.session.events);
|
|
872
|
+
}
|
|
873
|
+
catch {
|
|
874
|
+
return 'unavailable';
|
|
875
|
+
}
|
|
876
|
+
const reviewRoute = reviewer === 'user'
|
|
877
|
+
? 'user'
|
|
878
|
+
: reviewer === 'auto-review' && isAutoReviewEscalation(request)
|
|
879
|
+
? 'auto-escalation'
|
|
880
|
+
: undefined;
|
|
881
|
+
if (reviewRoute === undefined)
|
|
882
|
+
return reviewer === 'auto-review' ? next() : 'unavailable';
|
|
883
|
+
if (initialRoute.state !== 'bound')
|
|
884
|
+
return 'unavailable';
|
|
885
|
+
if (signalAborted(request.signal))
|
|
886
|
+
return 'cancelled';
|
|
887
|
+
let authority;
|
|
888
|
+
try {
|
|
889
|
+
authority = this.resolveToolApprovalAuthority(ctx, request);
|
|
890
|
+
}
|
|
891
|
+
catch {
|
|
892
|
+
return 'unavailable';
|
|
893
|
+
}
|
|
894
|
+
if (authority === undefined || authority.binding.id !== initialRoute.binding.id)
|
|
895
|
+
return 'unavailable';
|
|
896
|
+
const sessions = ctx.get('sessions');
|
|
897
|
+
if (sessions === undefined)
|
|
898
|
+
return 'unavailable';
|
|
899
|
+
try {
|
|
900
|
+
if (!await sessions.flush(request.agent.session))
|
|
901
|
+
return 'unavailable';
|
|
902
|
+
}
|
|
903
|
+
catch {
|
|
904
|
+
return 'unavailable';
|
|
905
|
+
}
|
|
906
|
+
if (signalAborted(request.signal))
|
|
907
|
+
return 'cancelled';
|
|
908
|
+
let persisted;
|
|
909
|
+
try {
|
|
910
|
+
persisted = this.resolveToolApprovalAuthority(ctx, request);
|
|
911
|
+
}
|
|
912
|
+
catch {
|
|
913
|
+
return 'unavailable';
|
|
914
|
+
}
|
|
915
|
+
if (!sameToolApprovalAuthority(authority, persisted))
|
|
916
|
+
return 'unavailable';
|
|
917
|
+
const operationId = `tool-approval:${randomUUID()}`;
|
|
918
|
+
const expiresAt = Date.now() + this.config.toolApprovalTtlMs;
|
|
919
|
+
const controller = new AbortController();
|
|
920
|
+
let requestCancelled = false;
|
|
921
|
+
const onRequestAbort = () => {
|
|
922
|
+
requestCancelled = true;
|
|
923
|
+
controller.abort(request.signal?.reason);
|
|
924
|
+
};
|
|
925
|
+
request.signal?.addEventListener('abort', onRequestAbort, { once: true });
|
|
926
|
+
const aborted = new Promise(resolve => {
|
|
927
|
+
controller.signal.addEventListener('abort', () => {
|
|
928
|
+
resolve(requestCancelled ? 'cancelled' : 'unavailable');
|
|
929
|
+
}, { once: true });
|
|
930
|
+
});
|
|
931
|
+
const timeout = setTimeout(() => {
|
|
932
|
+
controller.abort(new Error('assistant-delivery tool approval timed out'));
|
|
933
|
+
}, this.config.toolApprovalTtlMs);
|
|
934
|
+
timeout.unref?.();
|
|
935
|
+
this.toolApprovalControllers.add(controller);
|
|
936
|
+
const adapterRequest = Object.freeze({
|
|
937
|
+
operationId,
|
|
938
|
+
bindingId: authority.binding.id,
|
|
939
|
+
target: Object.freeze({
|
|
940
|
+
conversation: Object.freeze({ ...authority.binding.conversation }),
|
|
941
|
+
principal: Object.freeze({ ...authority.binding.principal }),
|
|
942
|
+
}),
|
|
943
|
+
expiresAt,
|
|
944
|
+
actionHash: authority.actionHash,
|
|
945
|
+
toolName: request.toolName,
|
|
946
|
+
callId: authority.callId,
|
|
947
|
+
...(request.reason === undefined ? {} : { reason: request.reason }),
|
|
948
|
+
arguments: authority.arguments,
|
|
949
|
+
});
|
|
950
|
+
let outcome;
|
|
951
|
+
try {
|
|
952
|
+
const answer = Promise.resolve()
|
|
953
|
+
.then(() => authority.adapter.requestToolApproval(adapterRequest, controller.signal))
|
|
954
|
+
.then(value => value, () => 'unavailable');
|
|
955
|
+
outcome = await Promise.race([answer, aborted]);
|
|
956
|
+
}
|
|
957
|
+
finally {
|
|
958
|
+
clearTimeout(timeout);
|
|
959
|
+
request.signal?.removeEventListener('abort', onRequestAbort);
|
|
960
|
+
this.toolApprovalControllers.delete(controller);
|
|
961
|
+
}
|
|
962
|
+
if (signalAborted(request.signal))
|
|
963
|
+
return 'cancelled';
|
|
964
|
+
if (controller.signal.aborted || !this.active || Date.now() >= expiresAt)
|
|
965
|
+
return 'unavailable';
|
|
966
|
+
let current;
|
|
967
|
+
try {
|
|
968
|
+
current = this.resolveToolApprovalAuthority(ctx, request);
|
|
969
|
+
}
|
|
970
|
+
catch {
|
|
971
|
+
return 'unavailable';
|
|
972
|
+
}
|
|
973
|
+
if (current === undefined || !sameToolApprovalAuthority(authority, current))
|
|
974
|
+
return 'unavailable';
|
|
975
|
+
if (outcome === 'allowed-once') {
|
|
976
|
+
try {
|
|
977
|
+
// The tool guard ran before the native tool raised this approval. Do not
|
|
978
|
+
// consume its budget a second time, but do re-check the mutable hard stop
|
|
979
|
+
// as close as possible to releasing the owner's one-shot grant.
|
|
980
|
+
const emergencyStop = this.policy.getEmergencyStop();
|
|
981
|
+
// The owner granted this call; a later hard-stop is a Policy veto, not
|
|
982
|
+
// a user rejection. `rejected` is reserved for the signed reject action
|
|
983
|
+
// because upstream otherwise renders the false claim "the user rejected".
|
|
984
|
+
if (emergencyStop.enabled)
|
|
985
|
+
return 'unavailable';
|
|
986
|
+
if (emergencyStop.version !== current.policyEmergencyVersion)
|
|
987
|
+
return 'unavailable';
|
|
988
|
+
}
|
|
989
|
+
catch {
|
|
990
|
+
return 'unavailable';
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
return approvalOutcomes.includes(outcome) ? outcome : 'unavailable';
|
|
994
|
+
}
|
|
995
|
+
async acceptInbound(envelope) {
|
|
996
|
+
this.assertActive();
|
|
997
|
+
const accepted = this.deliveryStore.acceptInbound(envelope);
|
|
998
|
+
let record = accepted.record;
|
|
999
|
+
if (!['received', 'authorized'].includes(record.status)) {
|
|
1000
|
+
return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
|
|
1001
|
+
}
|
|
1002
|
+
if (!this.deliveryStore.isAuthorizedPrincipal(envelope.principal)) {
|
|
1003
|
+
record = this.deliveryStore.deadLetterInbox(record.id, 'unauthorized-principal');
|
|
1004
|
+
return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
|
|
1005
|
+
}
|
|
1006
|
+
const decision = this.policy.authorize({ subject: { kind: 'external', id: externalPrincipalId(envelope.principal) },
|
|
1007
|
+
action: 'ingest', resource: { kind: 'message', id: `inbound:${envelope.channel}/${envelope.account}` },
|
|
1008
|
+
context: { initiator: 'external' } }, { idempotencyKey: `message-inbound:${record.id}` });
|
|
1009
|
+
if (decision.effect !== 'allow') {
|
|
1010
|
+
record = this.deliveryStore.deadLetterInbox(record.id, `policy-${decision.reasonCode}`);
|
|
1011
|
+
return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
|
|
1012
|
+
}
|
|
1013
|
+
const sessionCommand = parseDeliveryCommand(envelope);
|
|
1014
|
+
if (isExactDeliveryCommand(sessionCommand, 'new', 'clear')) {
|
|
1015
|
+
return await this.runConversationTransition(envelope.conversation, async () => {
|
|
1016
|
+
await this.recoverPendingInbound(envelope, record.id);
|
|
1017
|
+
// A provider retry can arrive while the first copy is waiting for the
|
|
1018
|
+
// old generation to drain. Re-read after acquiring the transition:
|
|
1019
|
+
// once that copy queued the command, its rotation is already complete
|
|
1020
|
+
// and this duplicate must not create yet another generation.
|
|
1021
|
+
const currentRecord = this.deliveryStore.getInbox(record.id);
|
|
1022
|
+
if (!['received', 'authorized'].includes(currentRecord.status)) {
|
|
1023
|
+
return {
|
|
1024
|
+
duplicate: accepted.duplicate,
|
|
1025
|
+
inboxId: currentRecord.id,
|
|
1026
|
+
status: currentRecord.status,
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
record = currentRecord;
|
|
1030
|
+
const binding = await this.bindingForInbound(envelope, record.id);
|
|
1031
|
+
if (binding === undefined) {
|
|
1032
|
+
const failed = this.deliveryStore.getInbox(record.id);
|
|
1033
|
+
return { duplicate: accepted.duplicate, inboxId: failed.id, status: failed.status };
|
|
1034
|
+
}
|
|
1035
|
+
// First invalidate all old work that cannot have crossed the Agent
|
|
1036
|
+
// dispatch gate. Then cancel and drain any turn that did cross it.
|
|
1037
|
+
// Rotation is last, so no fresh generation can execute beside the old.
|
|
1038
|
+
const fence = this.deliveryStore.cancelUndispatchedInboxBefore({
|
|
1039
|
+
bindingId: binding.id,
|
|
1040
|
+
beforeInboxId: record.id,
|
|
1041
|
+
failureCode: 'new-session-before-dispatch',
|
|
1042
|
+
});
|
|
1043
|
+
await this.runtime?.cancelActive?.(binding, 'new');
|
|
1044
|
+
await this.inbound.cancelUndispatchedClaims(fence.claimedInboxIds, 'new');
|
|
1045
|
+
const rotated = await this.rotateBinding(envelope, binding, record.id);
|
|
1046
|
+
record = this.deliveryStore.getInbox(record.id);
|
|
1047
|
+
if (record.status !== 'queued' || record.bindingId !== rotated.id) {
|
|
1048
|
+
throw new AssistantDeliveryError('runtime-conflict', 'new-session binding committed without its command Inbox');
|
|
1049
|
+
}
|
|
1050
|
+
return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
if (isExactDeliveryCommand(sessionCommand, 'stop')) {
|
|
1054
|
+
return await this.runConversationTransition(envelope.conversation, async () => {
|
|
1055
|
+
await this.recoverPendingInbound(envelope, record.id);
|
|
1056
|
+
const currentRecord = this.deliveryStore.getInbox(record.id);
|
|
1057
|
+
if (!['received', 'authorized'].includes(currentRecord.status)) {
|
|
1058
|
+
return {
|
|
1059
|
+
duplicate: accepted.duplicate,
|
|
1060
|
+
inboxId: currentRecord.id,
|
|
1061
|
+
status: currentRecord.status,
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
record = currentRecord;
|
|
1065
|
+
const binding = await this.bindingForInbound(envelope, record.id);
|
|
1066
|
+
if (binding === undefined) {
|
|
1067
|
+
const failed = this.deliveryStore.getInbox(record.id);
|
|
1068
|
+
return { duplicate: accepted.duplicate, inboxId: failed.id, status: failed.status };
|
|
1069
|
+
}
|
|
1070
|
+
const fence = this.deliveryStore.cancelUndispatchedInboxBefore({
|
|
1071
|
+
bindingId: binding.id,
|
|
1072
|
+
beforeInboxId: record.id,
|
|
1073
|
+
failureCode: 'user-stopped-before-dispatch',
|
|
1074
|
+
});
|
|
1075
|
+
// Holding the transition while the live turn drains prevents a later
|
|
1076
|
+
// message from overtaking the durable stop acknowledgement.
|
|
1077
|
+
await this.runtime?.cancelActive?.(binding, 'stop');
|
|
1078
|
+
await this.inbound.cancelUndispatchedClaims(fence.claimedInboxIds, 'stop');
|
|
1079
|
+
record = this.deliveryStore.queueInbox(record.id, binding.id);
|
|
1080
|
+
return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
// All accept-side binding decisions join this short conversation lock.
|
|
1084
|
+
// It serializes only durable admission (not Agent execution), and makes a
|
|
1085
|
+
// queued `/stop` or `/new` an unambiguous boundary for later arrivals.
|
|
1086
|
+
return await this.runConversationTransition(envelope.conversation, async () => {
|
|
1087
|
+
await this.recoverPendingInbound(envelope, record.id);
|
|
1088
|
+
const currentRecord = this.deliveryStore.getInbox(record.id);
|
|
1089
|
+
if (!['received', 'authorized'].includes(currentRecord.status)) {
|
|
1090
|
+
return {
|
|
1091
|
+
duplicate: accepted.duplicate,
|
|
1092
|
+
inboxId: currentRecord.id,
|
|
1093
|
+
status: currentRecord.status,
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
record = currentRecord;
|
|
1097
|
+
const binding = await this.bindingForInbound(envelope, record.id);
|
|
1098
|
+
if (binding === undefined) {
|
|
1099
|
+
const failed = this.deliveryStore.getInbox(record.id);
|
|
1100
|
+
return { duplicate: accepted.duplicate, inboxId: failed.id, status: failed.status };
|
|
1101
|
+
}
|
|
1102
|
+
record = this.deliveryStore.queueInbox(record.id, binding.id);
|
|
1103
|
+
return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
enqueueBackground(input) {
|
|
1107
|
+
this.assertActive();
|
|
1108
|
+
const binding = this.deliveryStore.getBinding(input.bindingId);
|
|
1109
|
+
if (binding === undefined || binding.status !== 'active' || !this.hasActivePrincipal(binding)) {
|
|
1110
|
+
throw new AssistantDeliveryError('missing-binding', 'delivery binding does not exist or is revoked');
|
|
1111
|
+
}
|
|
1112
|
+
if (binding.workspace !== input.workspace) {
|
|
1113
|
+
throw new AssistantDeliveryError('missing-binding', 'delivery workspace does not match the active binding');
|
|
1114
|
+
}
|
|
1115
|
+
const decision = this.policy.authorize({ subject: { kind: 'background', id: input.sourceId,
|
|
1116
|
+
workspace: input.workspace, principal: externalPrincipalId(binding.principal) }, action: 'send',
|
|
1117
|
+
resource: { kind: 'message', id: binding.id }, context: { initiator: 'background' } }, { idempotencyKey: `message-send:${input.idempotencyKey}` });
|
|
1118
|
+
if (decision.effect !== 'allow')
|
|
1119
|
+
throw policyDenied(decision);
|
|
1120
|
+
if (!this.hasActivePrincipal(binding)) {
|
|
1121
|
+
throw new AssistantDeliveryError('missing-binding', 'delivery binding principal is no longer active');
|
|
1122
|
+
}
|
|
1123
|
+
return this.deliveryStore.enqueue({ idempotencyKey: input.idempotencyKey, bindingId: binding.id,
|
|
1124
|
+
target: { conversation: binding.conversation, principal: binding.principal }, text: input.text,
|
|
1125
|
+
format: input.format ?? 'plain' });
|
|
1126
|
+
}
|
|
1127
|
+
enqueueApproval(input) {
|
|
1128
|
+
this.assertActive();
|
|
1129
|
+
const binding = this.deliveryStore.getBinding(input.bindingId);
|
|
1130
|
+
if (binding === undefined || binding.status !== 'active') {
|
|
1131
|
+
throw new AssistantDeliveryError('missing-binding', 'delivery binding does not exist or is revoked');
|
|
1132
|
+
}
|
|
1133
|
+
if (binding.workspace !== input.workspace) {
|
|
1134
|
+
throw new AssistantDeliveryError('missing-binding', 'delivery workspace does not match the active binding');
|
|
1135
|
+
}
|
|
1136
|
+
const owner = this.deliveryStore.getPrincipal(binding.principal);
|
|
1137
|
+
const principal = externalPrincipalId(binding.principal);
|
|
1138
|
+
const proposal = this.policy.getProposal(input.approval.proposalId);
|
|
1139
|
+
if (owner?.status !== 'active' || owner.role !== 'owner'
|
|
1140
|
+
|| proposal === undefined || proposal.status !== 'pending' || Date.now() >= proposal.expiresAt
|
|
1141
|
+
|| proposal.principal !== principal
|
|
1142
|
+
|| proposal.version !== input.approval.expectedVersion
|
|
1143
|
+
|| proposal.expiresAt !== input.approval.expiresAt
|
|
1144
|
+
|| proposal.summary !== input.approval.title
|
|
1145
|
+
|| proposal.diffHash !== input.approval.diffHash
|
|
1146
|
+
|| createHash('sha256').update(input.text).digest('hex') !== proposal.diffHash) {
|
|
1147
|
+
throw new AssistantDeliveryError('missing-binding', 'approval card does not match its active owner and Policy proposal');
|
|
1148
|
+
}
|
|
1149
|
+
const decision = this.policy.authorize({ subject: { kind: 'background', id: input.sourceId,
|
|
1150
|
+
workspace: input.workspace, principal }, action: 'approval.send',
|
|
1151
|
+
resource: { kind: 'message', id: binding.id }, context: { initiator: 'background' } }, { idempotencyKey: `message-approval:${input.idempotencyKey}` });
|
|
1152
|
+
if (decision.effect !== 'allow')
|
|
1153
|
+
throw policyDenied(decision);
|
|
1154
|
+
return this.deliveryStore.enqueue({ idempotencyKey: input.idempotencyKey, bindingId: binding.id,
|
|
1155
|
+
target: { conversation: binding.conversation, principal: binding.principal }, text: input.text,
|
|
1156
|
+
format: 'approval', approval: input.approval });
|
|
1157
|
+
}
|
|
1158
|
+
replyCommand(bindingInput, input) {
|
|
1159
|
+
this.assertActive();
|
|
1160
|
+
const binding = this.deliveryStore.getBinding(bindingInput.id);
|
|
1161
|
+
if (binding === undefined || binding.status !== 'active' || binding.sessionId !== bindingInput.sessionId
|
|
1162
|
+
|| !this.hasActivePrincipal(binding)) {
|
|
1163
|
+
throw new AssistantDeliveryError('missing-binding', 'control command binding does not exist or is no longer active');
|
|
1164
|
+
}
|
|
1165
|
+
const legacyInboundKey = `inbound:${input.replyToEventId}:reply`;
|
|
1166
|
+
const idempotencyKey = input.idempotencyKey === legacyInboundKey
|
|
1167
|
+
? this.inboundReplyIdempotencyKey(binding, input.replyToEventId)
|
|
1168
|
+
: input.idempotencyKey;
|
|
1169
|
+
const decision = this.policy.authorize({
|
|
1170
|
+
subject: {
|
|
1171
|
+
kind: 'agent',
|
|
1172
|
+
id: binding.agentPreset,
|
|
1173
|
+
workspace: binding.workspace,
|
|
1174
|
+
principal: externalPrincipalId(binding.principal),
|
|
1175
|
+
},
|
|
1176
|
+
action: 'reply',
|
|
1177
|
+
resource: { kind: 'message', id: binding.id },
|
|
1178
|
+
context: { initiator: 'external' },
|
|
1179
|
+
}, { idempotencyKey: `message-reply:${idempotencyKey}` });
|
|
1180
|
+
if (decision.effect !== 'allow')
|
|
1181
|
+
throw policyDenied(decision);
|
|
1182
|
+
if (!this.hasActivePrincipal(binding)) {
|
|
1183
|
+
throw new AssistantDeliveryError('missing-binding', 'control command binding principal is no longer active');
|
|
1184
|
+
}
|
|
1185
|
+
const format = this.replyFormat(binding.conversation, input.format) ?? 'plain';
|
|
1186
|
+
return this.deliveryStore.enqueue({
|
|
1187
|
+
idempotencyKey,
|
|
1188
|
+
bindingId: binding.id,
|
|
1189
|
+
target: { conversation: binding.conversation, principal: binding.principal },
|
|
1190
|
+
text: input.text,
|
|
1191
|
+
format,
|
|
1192
|
+
...(input.modelPicker === undefined ? {} : { modelPicker: input.modelPicker }),
|
|
1193
|
+
...(format !== 'permission-picker' || input.permissionPicker === undefined
|
|
1194
|
+
? {}
|
|
1195
|
+
: { permissionPicker: input.permissionPicker }),
|
|
1196
|
+
replyToEventId: input.replyToEventId,
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Answers are authored as Markdown, but the coordinator drops an intent whose format the adapter
|
|
1201
|
+
* does not declare (`unsupported-format`). Ask for Markdown only where the channel renders it and
|
|
1202
|
+
* degrade to plain text elsewhere, so a reply is never lost to a capability mismatch.
|
|
1203
|
+
*/
|
|
1204
|
+
replyFormat(conversation, requested) {
|
|
1205
|
+
if (requested === 'permission-picker') {
|
|
1206
|
+
const adapter = this.registry.get(conversation.channel, conversation.account);
|
|
1207
|
+
return adapter?.capabilities.formats.includes('permission-picker') === true ? requested : 'plain';
|
|
1208
|
+
}
|
|
1209
|
+
if (requested !== 'markdown')
|
|
1210
|
+
return requested;
|
|
1211
|
+
const adapter = this.registry.get(conversation.channel, conversation.account);
|
|
1212
|
+
return adapter?.capabilities.formats.includes('markdown') === true ? 'markdown' : 'plain';
|
|
1213
|
+
}
|
|
1214
|
+
reply(agent, input) {
|
|
1215
|
+
this.assertActive();
|
|
1216
|
+
const binding = agent === undefined ? undefined : this.deliveryStore.getBindingBySession(String(agent.session.id));
|
|
1217
|
+
if (agent === undefined || binding === undefined || binding.status !== 'active'
|
|
1218
|
+
|| agent.session.header.cwd !== binding.workspace
|
|
1219
|
+
|| agent.session.header.agentPreset !== binding.agentPreset
|
|
1220
|
+
|| !this.hasActivePrincipal(binding)) {
|
|
1221
|
+
throw new AssistantDeliveryError('missing-binding', 'Agent session is not bound to an active delivery route');
|
|
1222
|
+
}
|
|
1223
|
+
let idempotencyKey = input.idempotencyKey;
|
|
1224
|
+
if (input.replyToEventId !== undefined
|
|
1225
|
+
&& input.idempotencyKey === `inbound:${input.replyToEventId}:reply`) {
|
|
1226
|
+
idempotencyKey = this.inboundReplyIdempotencyKey(binding, input.replyToEventId);
|
|
1227
|
+
}
|
|
1228
|
+
const decision = this.policy.authorizeAgent(agent, 'reply', { kind: 'message', id: binding.id }, { idempotencyKey: `message-reply:${idempotencyKey}` });
|
|
1229
|
+
if (decision.effect !== 'allow')
|
|
1230
|
+
throw policyDenied(decision);
|
|
1231
|
+
if (!this.hasActivePrincipal(binding)) {
|
|
1232
|
+
throw new AssistantDeliveryError('missing-binding', 'Agent delivery principal is no longer active');
|
|
1233
|
+
}
|
|
1234
|
+
const format = this.replyFormat(binding.conversation, input.format) ?? 'plain';
|
|
1235
|
+
return this.deliveryStore.enqueue({ idempotencyKey, bindingId: binding.id,
|
|
1236
|
+
target: { conversation: binding.conversation, principal: binding.principal }, text: input.text,
|
|
1237
|
+
format,
|
|
1238
|
+
...(input.modelPicker === undefined ? {} : { modelPicker: input.modelPicker }),
|
|
1239
|
+
...(format !== 'permission-picker' || input.permissionPicker === undefined
|
|
1240
|
+
? {}
|
|
1241
|
+
: { permissionPicker: input.permissionPicker }),
|
|
1242
|
+
...(input.replyToEventId === undefined ? {} : { replyToEventId: input.replyToEventId }) });
|
|
1243
|
+
}
|
|
1244
|
+
history(agent, input) {
|
|
1245
|
+
this.assertActive();
|
|
1246
|
+
const binding = agent === undefined ? undefined : this.deliveryStore.getBindingBySession(String(agent.session.id));
|
|
1247
|
+
if (agent === undefined || binding === undefined || binding.status !== 'active'
|
|
1248
|
+
|| agent.session.header.cwd !== binding.workspace
|
|
1249
|
+
|| agent.session.header.agentPreset !== binding.agentPreset) {
|
|
1250
|
+
throw new AssistantDeliveryError('missing-binding', 'Agent session has no active matching delivery binding');
|
|
1251
|
+
}
|
|
1252
|
+
const decision = this.policy.authorizeAgent(agent, 'history', { kind: 'message', id: binding.id });
|
|
1253
|
+
if (decision.effect !== 'allow')
|
|
1254
|
+
throw policyDenied(decision);
|
|
1255
|
+
const query = input.limit === undefined ? { bindingId: binding.id } : { bindingId: binding.id, limit: input.limit };
|
|
1256
|
+
return { binding, inbox: this.deliveryStore.listInbox(query), outbox: this.deliveryStore.listOutbox(query) };
|
|
1257
|
+
}
|
|
1258
|
+
bindAgentApprovalRoute(agent, input) {
|
|
1259
|
+
this.assertActive();
|
|
1260
|
+
const binding = this.deliveryStore.getBinding(input.bindingId);
|
|
1261
|
+
const owner = binding === undefined ? undefined : this.deliveryStore.getPrincipal(binding.principal);
|
|
1262
|
+
if (agent === undefined || binding?.status !== 'active' || owner?.status !== 'active' || owner.role !== 'owner'
|
|
1263
|
+
|| agent.session.header.cwd !== binding.workspace
|
|
1264
|
+
|| agent.session.header.agentPreset !== binding.agentPreset) {
|
|
1265
|
+
throw new AssistantDeliveryError('missing-binding', 'Agent identity does not match an active owner approval route');
|
|
1266
|
+
}
|
|
1267
|
+
const direct = this.deliveryStore.getBindingBySession(String(agent.session.id));
|
|
1268
|
+
if (direct !== undefined && direct.id !== binding.id) {
|
|
1269
|
+
throw new AssistantDeliveryError('missing-binding', 'Agent session is already bound to another approval route');
|
|
1270
|
+
}
|
|
1271
|
+
const token = Symbol('assistant-delivery.approval-binding');
|
|
1272
|
+
this.agentApprovalBindings.set(agent, { bindingId: binding.id, token });
|
|
1273
|
+
let active = true;
|
|
1274
|
+
return () => {
|
|
1275
|
+
if (!active)
|
|
1276
|
+
return;
|
|
1277
|
+
active = false;
|
|
1278
|
+
if (this.agentApprovalBindings.get(agent)?.token === token)
|
|
1279
|
+
this.agentApprovalBindings.delete(agent);
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
prepareAgentApproval(agent, input) {
|
|
1283
|
+
this.assertActive();
|
|
1284
|
+
const sourceId = input.sourceId.trim();
|
|
1285
|
+
const direct = agent === undefined ? undefined : this.deliveryStore.getBindingBySession(String(agent.session.id));
|
|
1286
|
+
const delegatedId = agent === undefined ? undefined : this.agentApprovalBindings.get(agent)?.bindingId;
|
|
1287
|
+
const delegated = delegatedId === undefined ? undefined : this.deliveryStore.getBinding(delegatedId);
|
|
1288
|
+
if (direct !== undefined && delegated !== undefined && direct.id !== delegated.id) {
|
|
1289
|
+
throw new AssistantDeliveryError('missing-binding', 'Agent approval routes conflict');
|
|
1290
|
+
}
|
|
1291
|
+
const binding = direct ?? delegated;
|
|
1292
|
+
const owner = binding === undefined ? undefined : this.deliveryStore.getPrincipal(binding.principal);
|
|
1293
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u.test(sourceId)
|
|
1294
|
+
|| binding?.status !== 'active' || owner?.status !== 'active' || owner.role !== 'owner'
|
|
1295
|
+
|| agent?.session.header.cwd !== binding.workspace
|
|
1296
|
+
|| agent.session.header.agentPreset !== binding.agentPreset) {
|
|
1297
|
+
throw new AssistantDeliveryError('missing-binding', 'Agent session has no authenticated active owner approval route');
|
|
1298
|
+
}
|
|
1299
|
+
return Object.freeze({ sourceId, bindingId: binding.id, workspace: binding.workspace,
|
|
1300
|
+
principal: externalPrincipalId(binding.principal) });
|
|
1301
|
+
}
|
|
1302
|
+
async tick() {
|
|
1303
|
+
this.assertActive();
|
|
1304
|
+
await this.inbound.tick();
|
|
1305
|
+
await this.drainModelSelectionSettlements();
|
|
1306
|
+
this.drainApprovalDispatches();
|
|
1307
|
+
await this.outbound.tick();
|
|
1308
|
+
}
|
|
1309
|
+
async whenIdle() {
|
|
1310
|
+
this.assertActive();
|
|
1311
|
+
await Promise.all([this.inbound.whenIdle(), this.outbound.whenIdle(), this.modelSelectionFlight]);
|
|
1312
|
+
}
|
|
1313
|
+
start() {
|
|
1314
|
+
this.assertActive();
|
|
1315
|
+
if (this.timer !== undefined)
|
|
1316
|
+
return;
|
|
1317
|
+
this.timer = setInterval(() => void this.tick(), this.config.tickIntervalMs);
|
|
1318
|
+
this.timer.unref?.();
|
|
1319
|
+
void this.tick();
|
|
1320
|
+
}
|
|
1321
|
+
async stop() {
|
|
1322
|
+
this.assertActive();
|
|
1323
|
+
await this.stopInternal();
|
|
1324
|
+
}
|
|
1325
|
+
async ensureBinding(envelope) {
|
|
1326
|
+
const key = JSON.stringify(envelope.conversation);
|
|
1327
|
+
const existing = this.bindingFlights.get(key);
|
|
1328
|
+
if (existing !== undefined)
|
|
1329
|
+
return existing;
|
|
1330
|
+
const runtime = this.runtime;
|
|
1331
|
+
if (runtime === undefined)
|
|
1332
|
+
throw new AssistantDeliveryError('runtime-unavailable', 'inbound runtime is unavailable');
|
|
1333
|
+
const promise = (async () => {
|
|
1334
|
+
const current = this.deliveryStore.getActiveBinding(envelope.conversation);
|
|
1335
|
+
if (current !== undefined)
|
|
1336
|
+
return current;
|
|
1337
|
+
const generation = this.deliveryStore.nextBindingGeneration(envelope.conversation);
|
|
1338
|
+
const created = await runtime.createSession({ envelope, generation, signal: new AbortController().signal });
|
|
1339
|
+
this.assertActive();
|
|
1340
|
+
return this.deliveryStore.createBinding({ conversation: envelope.conversation, principal: envelope.principal,
|
|
1341
|
+
workspace: created.workspace, agentPreset: created.agentPreset, sessionId: created.sessionId,
|
|
1342
|
+
policyRef: created.policyRef, expectedGeneration: generation });
|
|
1343
|
+
})().finally(() => this.bindingFlights.delete(key));
|
|
1344
|
+
this.bindingFlights.set(key, promise);
|
|
1345
|
+
return promise;
|
|
1346
|
+
}
|
|
1347
|
+
async bindingForInbound(envelope, inboxId) {
|
|
1348
|
+
let binding = this.deliveryStore.getActiveBinding(envelope.conversation);
|
|
1349
|
+
if (binding === undefined) {
|
|
1350
|
+
if (this.runtime === undefined) {
|
|
1351
|
+
this.deliveryStore.deadLetterInbox(inboxId, 'runtime-unavailable');
|
|
1352
|
+
return undefined;
|
|
1353
|
+
}
|
|
1354
|
+
binding = await this.ensureBinding(envelope);
|
|
1355
|
+
}
|
|
1356
|
+
if (externalPrincipalId(binding.principal) !== externalPrincipalId(envelope.principal)) {
|
|
1357
|
+
this.deliveryStore.deadLetterInbox(inboxId, 'binding-principal-mismatch');
|
|
1358
|
+
return undefined;
|
|
1359
|
+
}
|
|
1360
|
+
return binding;
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Finish admissions that survived a process exit before their queue/rotate
|
|
1364
|
+
* commit. The provider need not retransmit them: any later message in the
|
|
1365
|
+
* same conversation first drives every older received row forward in its
|
|
1366
|
+
* original durable Inbox order. This includes ordinary messages that were
|
|
1367
|
+
* already persisted behind a blocked `/stop` or `/new` transition.
|
|
1368
|
+
*/
|
|
1369
|
+
async recoverPendingInbound(boundary, beforeInboxId) {
|
|
1370
|
+
while (true) {
|
|
1371
|
+
const pending = this.deliveryStore.findPendingInboundBefore({
|
|
1372
|
+
conversation: boundary.conversation,
|
|
1373
|
+
principal: boundary.principal,
|
|
1374
|
+
beforeInboxId,
|
|
1375
|
+
});
|
|
1376
|
+
if (pending === undefined)
|
|
1377
|
+
return;
|
|
1378
|
+
if (!this.deliveryStore.isAuthorizedPrincipal(pending.envelope.principal)) {
|
|
1379
|
+
this.deliveryStore.deadLetterInbox(pending.id, 'unauthorized-principal');
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
const decision = this.policy.authorize({
|
|
1383
|
+
subject: { kind: 'external', id: externalPrincipalId(pending.envelope.principal) },
|
|
1384
|
+
action: 'ingest',
|
|
1385
|
+
resource: { kind: 'message', id: `inbound:${pending.envelope.channel}/${pending.envelope.account}` },
|
|
1386
|
+
context: { initiator: 'external' },
|
|
1387
|
+
}, { idempotencyKey: `message-inbound:${pending.id}` });
|
|
1388
|
+
if (decision.effect !== 'allow') {
|
|
1389
|
+
this.deliveryStore.deadLetterInbox(pending.id, `policy-${decision.reasonCode}`);
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
const command = parseDeliveryCommand(pending.envelope);
|
|
1393
|
+
const reset = isExactDeliveryCommand(command, 'new', 'clear');
|
|
1394
|
+
const stop = isExactDeliveryCommand(command, 'stop');
|
|
1395
|
+
const binding = await this.bindingForInbound(pending.envelope, pending.id);
|
|
1396
|
+
if (binding === undefined)
|
|
1397
|
+
continue;
|
|
1398
|
+
if (!reset && !stop) {
|
|
1399
|
+
this.deliveryStore.queueInbox(pending.id, binding.id);
|
|
1400
|
+
continue;
|
|
1401
|
+
}
|
|
1402
|
+
const fence = this.deliveryStore.cancelUndispatchedInboxBefore({
|
|
1403
|
+
bindingId: binding.id,
|
|
1404
|
+
beforeInboxId: pending.id,
|
|
1405
|
+
failureCode: reset ? 'new-session-before-dispatch' : 'user-stopped-before-dispatch',
|
|
1406
|
+
});
|
|
1407
|
+
const cancellationCommand = reset ? 'new' : 'stop';
|
|
1408
|
+
await this.runtime?.cancelActive?.(binding, cancellationCommand);
|
|
1409
|
+
await this.inbound.cancelUndispatchedClaims(fence.claimedInboxIds, cancellationCommand);
|
|
1410
|
+
if (reset) {
|
|
1411
|
+
await this.rotateBinding(pending.envelope, binding, pending.id);
|
|
1412
|
+
}
|
|
1413
|
+
else {
|
|
1414
|
+
this.deliveryStore.queueInbox(pending.id, binding.id);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
async runConversationTransition(conversation, task) {
|
|
1419
|
+
const key = JSON.stringify(canonicalConversation(conversation));
|
|
1420
|
+
const previous = this.conversationTransitions.get(key) ?? Promise.resolve();
|
|
1421
|
+
let release;
|
|
1422
|
+
const own = new Promise(resolve => { release = resolve; });
|
|
1423
|
+
const fence = previous.catch(() => { }).then(() => own);
|
|
1424
|
+
this.conversationTransitions.set(key, fence);
|
|
1425
|
+
await previous.catch(() => { });
|
|
1426
|
+
try {
|
|
1427
|
+
return await task();
|
|
1428
|
+
}
|
|
1429
|
+
finally {
|
|
1430
|
+
release();
|
|
1431
|
+
if (this.conversationTransitions.get(key) === fence)
|
|
1432
|
+
this.conversationTransitions.delete(key);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
async rotateBinding(envelope, previous, commandInboxId) {
|
|
1436
|
+
const key = `${JSON.stringify(envelope.conversation)}:/new:${envelope.eventId}`;
|
|
1437
|
+
const existing = this.bindingFlights.get(key);
|
|
1438
|
+
if (existing !== undefined)
|
|
1439
|
+
return existing;
|
|
1440
|
+
const runtime = this.runtime;
|
|
1441
|
+
if (runtime === undefined)
|
|
1442
|
+
throw new AssistantDeliveryError('runtime-unavailable', 'inbound runtime is unavailable');
|
|
1443
|
+
const promise = (async () => {
|
|
1444
|
+
const current = this.deliveryStore.getActiveBinding(envelope.conversation);
|
|
1445
|
+
if (current === undefined)
|
|
1446
|
+
throw new AssistantDeliveryError('missing-binding', 'active binding disappeared during /new');
|
|
1447
|
+
if (current.id !== previous.id)
|
|
1448
|
+
return current;
|
|
1449
|
+
const generation = this.deliveryStore.nextBindingGeneration(envelope.conversation);
|
|
1450
|
+
if (generation !== current.generation + 1) {
|
|
1451
|
+
throw new AssistantDeliveryError('runtime-conflict', 'binding generation changed before /new session creation');
|
|
1452
|
+
}
|
|
1453
|
+
const created = await runtime.createSession({ envelope, generation,
|
|
1454
|
+
previous: current, signal: new AbortController().signal });
|
|
1455
|
+
this.assertActive();
|
|
1456
|
+
return this.deliveryStore.rotateBindingAndQueueCommand({
|
|
1457
|
+
bindingId: current.id,
|
|
1458
|
+
expectedVersion: current.version,
|
|
1459
|
+
sessionId: created.sessionId,
|
|
1460
|
+
inboxId: commandInboxId,
|
|
1461
|
+
}).binding;
|
|
1462
|
+
})().finally(() => this.bindingFlights.delete(key));
|
|
1463
|
+
this.bindingFlights.set(key, promise);
|
|
1464
|
+
return promise;
|
|
1465
|
+
}
|
|
1466
|
+
async stopInternal() {
|
|
1467
|
+
if (this.timer !== undefined)
|
|
1468
|
+
clearInterval(this.timer);
|
|
1469
|
+
if (this.modelSelectionRetryTimer !== undefined)
|
|
1470
|
+
clearTimeout(this.modelSelectionRetryTimer);
|
|
1471
|
+
this.timer = undefined;
|
|
1472
|
+
this.modelSelectionRetryTimer = undefined;
|
|
1473
|
+
await Promise.all([this.inbound.stop(), this.outbound.stop(), this.modelSelectionFlight]);
|
|
1474
|
+
}
|
|
1475
|
+
drainApprovalDispatches() {
|
|
1476
|
+
const cursor = this.deliveryStore.getApprovalDispatchCursor();
|
|
1477
|
+
const dispatches = this.policy.listPendingApprovalDispatches(100, cursor.after);
|
|
1478
|
+
for (const dispatch of dispatches) {
|
|
1479
|
+
try {
|
|
1480
|
+
this.enqueueApproval({
|
|
1481
|
+
sourceId: dispatch.sourceId,
|
|
1482
|
+
workspace: dispatch.workspace,
|
|
1483
|
+
bindingId: dispatch.bindingId,
|
|
1484
|
+
idempotencyKey: `approval-card:${dispatch.proposalId}`,
|
|
1485
|
+
text: dispatch.diff,
|
|
1486
|
+
approval: {
|
|
1487
|
+
operationId: `approval:${dispatch.proposalId}`,
|
|
1488
|
+
proposalId: dispatch.proposalId,
|
|
1489
|
+
expectedVersion: dispatch.proposalVersion,
|
|
1490
|
+
expiresAt: dispatch.expiresAt,
|
|
1491
|
+
title: dispatch.summary,
|
|
1492
|
+
diffHash: dispatch.diffHash,
|
|
1493
|
+
},
|
|
1494
|
+
});
|
|
1495
|
+
this.policy.markApprovalDispatchEnqueued(dispatch.proposalId, dispatch.version);
|
|
1496
|
+
}
|
|
1497
|
+
catch {
|
|
1498
|
+
// One invalid/revoked route must not starve later durable dispatches.
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
const last = dispatches.at(-1);
|
|
1502
|
+
const after = dispatches.length === 100 && last !== undefined
|
|
1503
|
+
? { createdAt: last.createdAt, proposalId: last.proposalId }
|
|
1504
|
+
: undefined;
|
|
1505
|
+
if (after === undefined && cursor.after === undefined)
|
|
1506
|
+
return;
|
|
1507
|
+
try {
|
|
1508
|
+
this.deliveryStore.advanceApprovalDispatchCursor({ expectedVersion: cursor.version,
|
|
1509
|
+
...(after === undefined ? {} : { after }) });
|
|
1510
|
+
}
|
|
1511
|
+
catch (error) {
|
|
1512
|
+
// Another Host sharing the same durable Delivery DB won the scan fence.
|
|
1513
|
+
if (error instanceof DeliveryStoreError && error.code === 'stale-fence')
|
|
1514
|
+
return;
|
|
1515
|
+
throw error;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
drainModelSelectionSettlements() {
|
|
1519
|
+
if (this.modelSelectionRetryTimer !== undefined) {
|
|
1520
|
+
clearTimeout(this.modelSelectionRetryTimer);
|
|
1521
|
+
this.modelSelectionRetryTimer = undefined;
|
|
1522
|
+
}
|
|
1523
|
+
if (this.modelSelectionFlight !== undefined) {
|
|
1524
|
+
return this.modelSelectionFlight.then(() => this.drainModelSelectionSettlements());
|
|
1525
|
+
}
|
|
1526
|
+
const flight = this.runModelSelectionSettlements()
|
|
1527
|
+
.finally(() => { if (this.modelSelectionFlight === flight)
|
|
1528
|
+
this.modelSelectionFlight = undefined; });
|
|
1529
|
+
this.modelSelectionFlight = flight;
|
|
1530
|
+
return flight;
|
|
1531
|
+
}
|
|
1532
|
+
async runModelSelectionSettlements() {
|
|
1533
|
+
const llm = this.ctx.get('llm');
|
|
1534
|
+
if (llm === undefined)
|
|
1535
|
+
return;
|
|
1536
|
+
while (this.active) {
|
|
1537
|
+
const claims = this.deliveryStore.claimModelSelectionSettlements({
|
|
1538
|
+
ownerId: this.ownerId,
|
|
1539
|
+
leaseMs: this.config.leaseMs,
|
|
1540
|
+
limit: this.config.maxConcurrency,
|
|
1541
|
+
});
|
|
1542
|
+
if (claims.length === 0) {
|
|
1543
|
+
const next = this.deliveryStore.nextModelSelectionClaimAt();
|
|
1544
|
+
if (next !== undefined && this.modelSelectionRetryTimer === undefined) {
|
|
1545
|
+
const delay = Math.max(1, Math.min(2_147_483_647, next - Date.now()));
|
|
1546
|
+
this.modelSelectionRetryTimer = setTimeout(() => {
|
|
1547
|
+
this.modelSelectionRetryTimer = undefined;
|
|
1548
|
+
if (this.active)
|
|
1549
|
+
void this.drainModelSelectionSettlements();
|
|
1550
|
+
}, delay);
|
|
1551
|
+
this.modelSelectionRetryTimer.unref?.();
|
|
1552
|
+
}
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
await Promise.allSettled(claims.map(async (claim) => {
|
|
1556
|
+
const finishWithoutReply = (result) => this.deliveryStore.completeModelSelectionSettlement({
|
|
1557
|
+
operationId: claim.operationId,
|
|
1558
|
+
payloadHash: claim.payloadHash,
|
|
1559
|
+
result,
|
|
1560
|
+
ownerId: this.ownerId,
|
|
1561
|
+
fencingToken: claim.fencingToken,
|
|
1562
|
+
});
|
|
1563
|
+
const payload = claim.payload;
|
|
1564
|
+
if (!isModelSelectionPayload(payload) || payload.bindingId !== claim.bindingId) {
|
|
1565
|
+
finishWithoutReply({ status: 'rejected', reason: 'authorization-revoked' });
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
const ownsBinding = (binding) => {
|
|
1569
|
+
const principal = this.deliveryStore.getPrincipal(payload.principal);
|
|
1570
|
+
return binding?.status === 'active'
|
|
1571
|
+
&& principal?.status === 'active'
|
|
1572
|
+
&& binding.conversation.chat === payload.callbackChatId
|
|
1573
|
+
&& JSON.stringify(binding.principal) === JSON.stringify(principal.principal);
|
|
1574
|
+
};
|
|
1575
|
+
// The callback claim already consumed any authorization budget. Workers
|
|
1576
|
+
// re-evaluate current rules/emergency-stop without charging it again.
|
|
1577
|
+
const authorized = (binding) => this.policy.evaluate({
|
|
1578
|
+
subject: { kind: 'external', id: externalPrincipalId(payload.principal) },
|
|
1579
|
+
action: 'ingest',
|
|
1580
|
+
resource: { kind: 'message', id: `model-selection:${binding.id}` },
|
|
1581
|
+
context: { initiator: 'external' },
|
|
1582
|
+
}).effect === 'allow';
|
|
1583
|
+
const complete = (binding, result, text, selection) => {
|
|
1584
|
+
const reply = (content) => ({
|
|
1585
|
+
idempotencyKey: `model-selection:${payload.callbackEventId}:reply`,
|
|
1586
|
+
bindingId: binding.id,
|
|
1587
|
+
target: { conversation: binding.conversation, principal: binding.principal },
|
|
1588
|
+
text: content,
|
|
1589
|
+
format: 'plain',
|
|
1590
|
+
});
|
|
1591
|
+
const superseded = { status: 'rejected', reason: 'selection-superseded' };
|
|
1592
|
+
return this.deliveryStore.completeModelSelectionSettlement({
|
|
1593
|
+
operationId: claim.operationId,
|
|
1594
|
+
payloadHash: claim.payloadHash,
|
|
1595
|
+
result,
|
|
1596
|
+
...(selection === undefined ? {} : { selection: { conversation: binding.conversation, route: selection } }),
|
|
1597
|
+
reply: reply(text),
|
|
1598
|
+
superseded: {
|
|
1599
|
+
result: superseded,
|
|
1600
|
+
reply: reply('该模型选择已被更晚的操作取代,未更改当前模型。'),
|
|
1601
|
+
},
|
|
1602
|
+
ownerId: this.ownerId,
|
|
1603
|
+
fencingToken: claim.fencingToken,
|
|
1604
|
+
});
|
|
1605
|
+
};
|
|
1606
|
+
const initialBinding = this.deliveryStore.getBinding(claim.bindingId);
|
|
1607
|
+
if (!ownsBinding(initialBinding) || !authorized(initialBinding)) {
|
|
1608
|
+
finishWithoutReply({ status: 'rejected', reason: 'authorization-revoked' });
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
if (payload.provider !== payload.modelProvider) {
|
|
1612
|
+
complete(initialBinding, { status: 'rejected', reason: 'provider-model-mismatch' }, `分组 ${payload.provider} 与模型 ${payload.modelProvider}/${payload.model} 不匹配,请重新发送 /model。`);
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
if (!llm.listProviders().some(provider => provider.id === payload.provider)) {
|
|
1616
|
+
complete(initialBinding, { status: 'rejected', reason: 'provider-unavailable' }, `模型分组 ${payload.provider} 当前不可用,请重新发送 /model。`);
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
let leaseLost = false;
|
|
1620
|
+
const renewLease = () => {
|
|
1621
|
+
try {
|
|
1622
|
+
if (!this.deliveryStore.renewModelSelectionSettlement({
|
|
1623
|
+
operationId: claim.operationId,
|
|
1624
|
+
ownerId: this.ownerId,
|
|
1625
|
+
fencingToken: claim.fencingToken,
|
|
1626
|
+
leaseMs: this.config.leaseMs,
|
|
1627
|
+
}))
|
|
1628
|
+
leaseLost = true;
|
|
1629
|
+
}
|
|
1630
|
+
catch {
|
|
1631
|
+
leaseLost = true;
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
const heartbeat = setInterval(renewLease, Math.max(1, Math.floor(this.config.leaseMs / 3)));
|
|
1635
|
+
heartbeat.unref?.();
|
|
1636
|
+
const resolveController = new AbortController();
|
|
1637
|
+
let resolveTimeout;
|
|
1638
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
1639
|
+
resolveTimeout = setTimeout(() => {
|
|
1640
|
+
const error = new Error('model selection resolution timed out');
|
|
1641
|
+
resolveController.abort(error);
|
|
1642
|
+
reject(error);
|
|
1643
|
+
}, this.config.leaseMs);
|
|
1644
|
+
});
|
|
1645
|
+
let resolved;
|
|
1646
|
+
try {
|
|
1647
|
+
resolved = await Promise.race([
|
|
1648
|
+
llm.resolveModelInfo(payload.provider, payload.model, resolveController.signal),
|
|
1649
|
+
deadline,
|
|
1650
|
+
]);
|
|
1651
|
+
}
|
|
1652
|
+
catch {
|
|
1653
|
+
// Model availability is persisted below, after refreshing authority.
|
|
1654
|
+
}
|
|
1655
|
+
finally {
|
|
1656
|
+
if (resolveTimeout !== undefined)
|
|
1657
|
+
clearTimeout(resolveTimeout);
|
|
1658
|
+
clearInterval(heartbeat);
|
|
1659
|
+
}
|
|
1660
|
+
renewLease();
|
|
1661
|
+
if (leaseLost)
|
|
1662
|
+
return;
|
|
1663
|
+
const binding = this.deliveryStore.getBinding(claim.bindingId);
|
|
1664
|
+
if (!ownsBinding(binding)) {
|
|
1665
|
+
finishWithoutReply({ status: 'rejected', reason: 'authorization-revoked' });
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
if (!authorized(binding)) {
|
|
1669
|
+
finishWithoutReply({ status: 'rejected', reason: 'authorization-revoked' });
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
if (resolved === undefined) {
|
|
1673
|
+
complete(binding, { status: 'rejected', reason: 'model-unavailable' }, `模型 ${payload.provider}/${payload.model} 当前不可用,请重新发送 /model。`);
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
if (payload.reasoningEffort !== null
|
|
1677
|
+
&& !resolved.reasoning?.efforts.some(effort => String(effort.id) === payload.reasoningEffort)) {
|
|
1678
|
+
complete(binding, { status: 'rejected', reason: 'invalid-effort' }, `模型 ${payload.provider}/${payload.model} 不支持 effort “${payload.reasoningEffort}”,请重新发送 /model。`);
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
const selection = {
|
|
1682
|
+
provider: payload.provider,
|
|
1683
|
+
model: payload.model,
|
|
1684
|
+
...(payload.reasoningEffort === null ? {} : { reasoningEffort: payload.reasoningEffort }),
|
|
1685
|
+
};
|
|
1686
|
+
complete(binding, { status: 'selected', selection }, `已切换到 ${payload.provider}/${payload.model}${payload.reasoningEffort === null
|
|
1687
|
+
? ',effort:默认'
|
|
1688
|
+
: `,effort:${payload.reasoningEffort}`}。下一条消息起生效,上下文保留。`, selection);
|
|
1689
|
+
}));
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
assertActive() {
|
|
1693
|
+
if (!this.active)
|
|
1694
|
+
throw new AssistantDeliveryError('disposed', 'assistant-delivery service is disposed');
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
export const Config = AssistantDeliveryService.Config;
|
|
1698
|
+
//# sourceMappingURL=service.js.map
|