@dsh-enhanced/assistant-delivery 0.1.67 → 0.1.70
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/README.md +10 -1
- package/lib/attention.d.ts +5 -0
- package/lib/attention.d.ts.map +1 -1
- package/lib/index.d.ts +3 -3
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -2
- package/lib/index.js.map +1 -1
- package/lib/native-web-owner.d.ts +15 -0
- package/lib/native-web-owner.d.ts.map +1 -1
- package/lib/native-web-owner.js +13 -1
- package/lib/native-web-owner.js.map +1 -1
- package/lib/service.d.ts +36 -0
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +130 -14
- package/lib/service.js.map +1 -1
- package/lib/types.d.ts +6 -0
- package/lib/types.d.ts.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +13 -10
package/lib/service.js
CHANGED
|
@@ -446,6 +446,23 @@ const ownerRequestedChangeArgumentBytes = 64 * 1024;
|
|
|
446
446
|
const toolApprovalIdentityBytes = 512;
|
|
447
447
|
/** Bounded owner-ordered linked fallback list for one report route. */
|
|
448
448
|
const maxLinkedReportFallback = 8;
|
|
449
|
+
/**
|
|
450
|
+
* Host-native tool arguments normalized exactly as Delivery attests them:
|
|
451
|
+
* JSON.parse then JSON.stringify, preserving key order and rejecting any
|
|
452
|
+
* non-object payload. Other components must hash this exact text instead of
|
|
453
|
+
* re-canonicalizing it, otherwise a foreground witness cannot be matched.
|
|
454
|
+
*/
|
|
455
|
+
export function normalizeForegroundToolArguments(raw) {
|
|
456
|
+
try {
|
|
457
|
+
const parsed = JSON.parse(raw);
|
|
458
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
459
|
+
return undefined;
|
|
460
|
+
return JSON.stringify(parsed);
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
return undefined;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
449
466
|
function inspectUnsettledNativeCall(events, turn, callId, toolName, argumentsJson, argumentBytes = toolApprovalArgumentBytes) {
|
|
450
467
|
if (!callId || !toolName || Buffer.byteLength(callId) > toolApprovalIdentityBytes
|
|
451
468
|
|| Buffer.byteLength(toolName) > toolApprovalIdentityBytes
|
|
@@ -474,16 +491,9 @@ function inspectUnsettledNativeCall(events, turn, callId, toolName, argumentsJso
|
|
|
474
491
|
|| events.filter(event => event.type === 'step/start' && event.data.turn === turn
|
|
475
492
|
&& event.data.step === call.data.step).length !== 1)
|
|
476
493
|
return undefined;
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const parsed = JSON.parse(call.data.arguments);
|
|
480
|
-
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
481
|
-
return undefined;
|
|
482
|
-
normalizedArguments = JSON.stringify(parsed);
|
|
483
|
-
}
|
|
484
|
-
catch {
|
|
494
|
+
const normalizedArguments = normalizeForegroundToolArguments(call.data.arguments);
|
|
495
|
+
if (normalizedArguments === undefined)
|
|
485
496
|
return undefined;
|
|
486
|
-
}
|
|
487
497
|
const argumentsDigest = createHash('sha256').update(normalizedArguments).digest('hex');
|
|
488
498
|
if (argumentsDigest !== createHash('sha256').update(argumentsJson).digest('hex'))
|
|
489
499
|
return undefined;
|
|
@@ -2104,10 +2114,53 @@ export class AssistantDeliveryService extends Service {
|
|
|
2104
2114
|
completeExecution: (inboxId, input) => this.completeForegroundTaskExecution(inboxId, input),
|
|
2105
2115
|
recordExecutionModelSelection: (inboxId, input) => this.deliveryStore.recordForegroundExecutionModelSelection({ inboxId, ...input }),
|
|
2106
2116
|
claimed: (agent, envelope, turn) => this.capturePreferenceTurn(agent, { kind: 'delivery', channel: envelope.channel, account: envelope.account, eventId: envelope.eventId }, turn),
|
|
2107
|
-
notifications: binding => this.deliveryStore.listOutbox({ bindingId: binding.id, limit: 50 }).filter(record => record.status === 'accepted'
|
|
2108
|
-
//
|
|
2117
|
+
notifications: binding => this.deliveryStore.listOutbox({ bindingId: binding.id, limit: 50 }).filter(record => (record.status === 'accepted' || record.status === 'delivered')
|
|
2118
|
+
// Acknowledged notices stay listed until read; withdrawal only gates the send attempt.
|
|
2109
2119
|
&& record.intent.metadata?.['dsh.native-notice'] === 'v1' && this.#notificationGuard(record, { withdrawal: false }) === undefined)
|
|
2110
|
-
.map(record => ({ id: record.id, text: record.intent.text, createdAt: record.createdAt
|
|
2120
|
+
.map(record => ({ id: record.id, text: record.intent.text, createdAt: record.createdAt,
|
|
2121
|
+
status: record.status === 'delivered' ? 'delivered' : 'accepted' })),
|
|
2122
|
+
acknowledgeNotices: (binding, ids, status) => {
|
|
2123
|
+
// Validate the whole batch against this binding and the exact send-path
|
|
2124
|
+
// guard before writing anything: one foreign/forged/expired id, drifted
|
|
2125
|
+
// owner/binding/route/policy or withdrawn notice rejects the batch with
|
|
2126
|
+
// zero receipts.
|
|
2127
|
+
const byId = new Map(this.deliveryStore.listOutbox({ bindingId: binding.id, limit: 100 }).map(record => [record.id, record]));
|
|
2128
|
+
const targets = ids.map(id => {
|
|
2129
|
+
const record = byId.get(id);
|
|
2130
|
+
if (record === undefined || record.intent.metadata?.['dsh.native-notice'] !== 'v1'
|
|
2131
|
+
|| (record.status !== 'accepted' && record.status !== 'delivered' && record.status !== 'read')
|
|
2132
|
+
|| record.providerMessageId === undefined) {
|
|
2133
|
+
throw new Error('assistant-delivery: native notice acknowledgement is invalid');
|
|
2134
|
+
}
|
|
2135
|
+
// Re-run the exact send-path guard: expiry, owner/binding version or
|
|
2136
|
+
// generation drift, route/policy change and withdrawal all fail closed
|
|
2137
|
+
// before any receipt is written. withdrawal:true is required so a
|
|
2138
|
+
// withdrawn notice cannot be acknowledged after it was sent.
|
|
2139
|
+
const refused = this.#nativeNoticeGuard(record.intent, { withdrawal: true });
|
|
2140
|
+
if (refused !== undefined)
|
|
2141
|
+
throw new Error(`assistant-delivery: native notice acknowledgement is ${refused.failureCode}`);
|
|
2142
|
+
return record;
|
|
2143
|
+
});
|
|
2144
|
+
const rank = { accepted: 1, delivered: 2, read: 3 };
|
|
2145
|
+
for (const record of targets) {
|
|
2146
|
+
// A late lower-rank ack is a no-op: it must not append a stale receipt
|
|
2147
|
+
// after read/delivered nor downgrade the outbox state.
|
|
2148
|
+
if (rank[status] <= rank[record.status])
|
|
2149
|
+
continue;
|
|
2150
|
+
try {
|
|
2151
|
+
this.deliveryStore.recordReceipt({ channel: binding.conversation.channel, account: binding.conversation.account,
|
|
2152
|
+
providerMessageId: record.providerMessageId, status, occurredAt: Date.now() });
|
|
2153
|
+
}
|
|
2154
|
+
catch (error) {
|
|
2155
|
+
// The web client re-acks every rendered notice. A repeat with a new
|
|
2156
|
+
// timestamp is an idempotent no-op, not a client error; other
|
|
2157
|
+
// storage failures still propagate.
|
|
2158
|
+
if (!(error instanceof DeliveryStoreError) || error.code !== 'idempotency-conflict')
|
|
2159
|
+
throw error;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
return Object.freeze({ acknowledged: targets.length });
|
|
2163
|
+
},
|
|
2111
2164
|
}, config);
|
|
2112
2165
|
this.nativeWebOwner = access;
|
|
2113
2166
|
this.nativeWebBound = true;
|
|
@@ -2198,6 +2251,55 @@ export class AssistantDeliveryService extends Service {
|
|
|
2198
2251
|
return Object.freeze({ protocol: 'assistant-delivery/foreground-tool-input/v1',
|
|
2199
2252
|
attestation, ...page });
|
|
2200
2253
|
}
|
|
2254
|
+
/**
|
|
2255
|
+
* The live caller's unique configured owner route. A caller never names a
|
|
2256
|
+
* route here: zero matching routes (anonymous, unconfigured, or failed
|
|
2257
|
+
* revalidation) and more than one matching route (ambiguous owner authority)
|
|
2258
|
+
* both fail closed.
|
|
2259
|
+
*/
|
|
2260
|
+
ownerRouteForAgent(agent) {
|
|
2261
|
+
if (!this.active || typeof agent !== 'object' || agent === null || agent.session === undefined)
|
|
2262
|
+
return undefined;
|
|
2263
|
+
let resolved;
|
|
2264
|
+
try {
|
|
2265
|
+
for (const authorityId of this.ownerRoutes.keys()) {
|
|
2266
|
+
if (this.validateOwnerAgentForRoute(agent, authorityId) === undefined)
|
|
2267
|
+
continue;
|
|
2268
|
+
if (resolved !== undefined)
|
|
2269
|
+
return undefined;
|
|
2270
|
+
resolved = authorityId;
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
catch {
|
|
2274
|
+
return undefined;
|
|
2275
|
+
}
|
|
2276
|
+
return resolved;
|
|
2277
|
+
}
|
|
2278
|
+
/** Host-only proof resolved from the caller Agent's unique live owner route. */
|
|
2279
|
+
inspectOwnerForegroundToolCallForAgent(agent, input) {
|
|
2280
|
+
if (!input || typeof input !== 'object' || Reflect.ownKeys(input).some(key => typeof key !== 'string'
|
|
2281
|
+
|| !['callId', 'toolName', 'argumentsJson'].includes(key)))
|
|
2282
|
+
return undefined;
|
|
2283
|
+
const authorityId = this.ownerRouteForAgent(agent);
|
|
2284
|
+
if (authorityId === undefined)
|
|
2285
|
+
return undefined;
|
|
2286
|
+
return this.inspectOwnerForegroundToolCall({ agent, authorityId, callId: input.callId,
|
|
2287
|
+
toolName: input.toolName, argumentsJson: input.argumentsJson });
|
|
2288
|
+
}
|
|
2289
|
+
/** Read-only human-input pages resolved from the caller Agent's unique live
|
|
2290
|
+
* owner route; the caller cannot select an Inbox, route, owner or replacement text. */
|
|
2291
|
+
readOwnerForegroundToolInputForAgent(agent, input) {
|
|
2292
|
+
if (!input || typeof input !== 'object' || Reflect.ownKeys(input).some(key => typeof key !== 'string'
|
|
2293
|
+
|| !['callId', 'toolName', 'argumentsJson', 'offset', 'maxBytes'].includes(key)))
|
|
2294
|
+
return undefined;
|
|
2295
|
+
const authorityId = this.ownerRouteForAgent(agent);
|
|
2296
|
+
if (authorityId === undefined)
|
|
2297
|
+
return undefined;
|
|
2298
|
+
return this.readOwnerForegroundToolInput({ agent, authorityId, callId: input.callId,
|
|
2299
|
+
toolName: input.toolName, argumentsJson: input.argumentsJson,
|
|
2300
|
+
...(input.offset === undefined ? {} : { offset: input.offset }),
|
|
2301
|
+
...(input.maxBytes === undefined ? {} : { maxBytes: input.maxBytes }) });
|
|
2302
|
+
}
|
|
2201
2303
|
/** Host-only, read-only source proof for an owner's live memory or wiki tool call. */
|
|
2202
2304
|
inspectOwnerRequestedChange(execution, input) {
|
|
2203
2305
|
if (!this.active || !execution || typeof execution !== 'object'
|
|
@@ -5268,7 +5370,7 @@ export class AssistantDeliveryService extends Service {
|
|
|
5268
5370
|
throw new AssistantDeliveryError('runtime-conflict', 'invalid notification outcome lookup');
|
|
5269
5371
|
}
|
|
5270
5372
|
return Object.freeze({ protocol: NOTIFICATION_OUTCOME_FACTS_PROTOCOL,
|
|
5271
|
-
facts: this.
|
|
5373
|
+
facts: this.notificationOutcomeFacts([...new Set(input.outboxIds)]), hasMore: false, nextAfterSequence: null });
|
|
5272
5374
|
}
|
|
5273
5375
|
const afterSequence = input.afterSequence ?? 0;
|
|
5274
5376
|
const limit = input.limit ?? 100;
|
|
@@ -5276,10 +5378,24 @@ export class AssistantDeliveryService extends Service {
|
|
|
5276
5378
|
throw new AssistantDeliveryError('runtime-conflict', 'invalid notification outcome scan');
|
|
5277
5379
|
}
|
|
5278
5380
|
const page = this.deliveryStore.attentionAudit.list(afterSequence, limit);
|
|
5279
|
-
const facts = this.
|
|
5381
|
+
const facts = this.notificationOutcomeFacts(page.entries.map(entry => entry.outboxId));
|
|
5280
5382
|
return Object.freeze({ protocol: NOTIFICATION_OUTCOME_FACTS_PROTOCOL, facts, hasMore: page.hasMore,
|
|
5281
5383
|
nextAfterSequence: page.entries.length === 0 ? null : page.entries.at(-1).sequence });
|
|
5282
5384
|
}
|
|
5385
|
+
/**
|
|
5386
|
+
* Attach the registered adapter's read-receipt completeness to each fact.
|
|
5387
|
+
* The Delivery store owns transport facts but not adapter capabilities; only
|
|
5388
|
+
* this read boundary can combine them. An undeclared capability stays absent
|
|
5389
|
+
* so older Evaluation classifiers keep their pinned v1 digests.
|
|
5390
|
+
*/
|
|
5391
|
+
notificationOutcomeFacts(outboxIds) {
|
|
5392
|
+
return this.deliveryStore.attentionAudit.outcomeFactsFor(outboxIds).map(fact => {
|
|
5393
|
+
const binding = this.deliveryStore.getBinding(fact.bindingId);
|
|
5394
|
+
const readReceipts = binding === undefined ? undefined
|
|
5395
|
+
: this.registry.get(binding.conversation.channel, binding.conversation.account)?.capabilities.readReceipts;
|
|
5396
|
+
return readReceipts === undefined ? fact : Object.freeze({ ...fact, readReceipts });
|
|
5397
|
+
});
|
|
5398
|
+
}
|
|
5283
5399
|
/**
|
|
5284
5400
|
* Read-only WP19 stage-2c progress audit page in append order; never exposes
|
|
5285
5401
|
* progress text. Entries are process-local (`durable: false`), bounded, and
|