@dharma-ai-labs/agent-fabric 0.2.202 → 0.2.204
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/AGENT_FABRIC_ONBOARDING.md +2 -0
- package/README.md +8 -0
- package/dist/codexBoundSession.d.ts +3 -0
- package/dist/codexBoundSession.d.ts.map +1 -1
- package/dist/codexBoundSession.js +2 -1
- package/dist/codexBoundSession.js.map +1 -1
- package/dist/codexBoundSession.test.js +262 -0
- package/dist/codexBoundSession.test.js.map +1 -1
- package/dist/codexInboxSession.d.ts +3 -1
- package/dist/codexInboxSession.d.ts.map +1 -1
- package/dist/codexInboxSession.js +7 -3
- package/dist/codexInboxSession.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -6
- package/dist/index.js.map +1 -1
- package/dist/namedSessionBudget.d.ts +1 -0
- package/dist/namedSessionBudget.d.ts.map +1 -1
- package/dist/namedSessionBudget.js +17 -2
- package/dist/namedSessionBudget.js.map +1 -1
- package/dist/namedSessionBudget.test.js +29 -0
- package/dist/namedSessionBudget.test.js.map +1 -1
- package/dist/namedSessionService.d.ts +1 -0
- package/dist/namedSessionService.d.ts.map +1 -1
- package/dist/namedSessionService.js +3 -1
- package/dist/namedSessionService.js.map +1 -1
- package/dist/namedSessionService.test.js +17 -2
- package/dist/namedSessionService.test.js.map +1 -1
- package/package.json +4 -4
|
@@ -67,6 +67,8 @@ On a **source-authorized** Linux Codex client, complete bootstrap also starts on
|
|
|
67
67
|
|
|
68
68
|
For local coding requests, use `dharma sessions work --name <name> --workspace-id <workspace-id> --work-id <unique-work-id> --prompt "<bounded coding request>"`. This runs in the same retained conversation under the signed policy's approved writable folders. Each peer question uses a separate read-only turn profile with network disabled; it cannot inherit the previous local turn's write permissions. Work and questions are serialized, and signed package activation waits for the task boundary. Keep requests bounded by the session allowance; reserved costs remain liabilities until actual usage is reconciled.
|
|
69
69
|
|
|
70
|
+
A reserved peer cost is not proof that the peer executed. If transport fails before server acceptance, the named session can reuse that exact-cost peer reservation only for the still-queued signed question and a fresh server acceptance. It never clears the reservation, reuses local-work or legacy accounting, accepts a replayed acknowledgement as new execution authority, or automatically reruns an expired question or an uncertain already-accepted turn. Report an explicit unresolved result rather than repeatedly asking the same question or increasing its allowance.
|
|
71
|
+
|
|
70
72
|
New named Codex conversations expose `dharma_peer_ask` and `dharma_peer_reply` tools during local coding turns. Supply the exact peer binding and task IDs, category and bounded question; inspect the returned state rather than treating `queued` as an answer. These tools delegate through the enrolled owner, not model-readable credentials or network access. They are unavailable in incoming read-only question turns, cannot approve devices or change policy, and stop on revocation or uncertain delivery. Obtain peer binding IDs from their attributed readiness/status receipts; role discovery alone does not prove a live conversation. Existing conversations require verification of retained tool availability after restart; do not create a replacement enrollment to hide a failure.
|
|
71
73
|
|
|
72
74
|
For bounded named questions and answers in local-analysis mode, current enrolled workspace/repository source authorization is checked independently of automatic-learning consent. The current whole-repository peer sandbox requires whole-repository source approval; partial paths, revoked/expired/unavailable policy, metadata-only mode, excluded paths and secret-bearing text stay blocked. Do not enable organization-wide native-history ingestion to work around a peer failure. Peer messages do not become accepted learning observations. The task ID is local correlation, not execution authority; the signed read-only offer, exact target/session, expiry, owner and budget still govern execution.
|
package/README.md
CHANGED
|
@@ -336,6 +336,14 @@ they do not establish full context, the executed model, or actual skill use, and
|
|
|
336
336
|
local coding work. Queue failures preserve the actual coding result; a failed or
|
|
337
337
|
interrupted provider turn retains its reservation and is not replayed on restart.
|
|
338
338
|
|
|
339
|
+
For named-session peer questions, a transport failure before server acceptance
|
|
340
|
+
does not require clearing the reserved cost. The same exact-cost peer reservation
|
|
341
|
+
can be reused only while its signed offer remains queued and the server confirms
|
|
342
|
+
a fresh acceptance. A replayed acceptance cannot authorize another provider turn.
|
|
343
|
+
Peer reservations have a separate internal namespace; local-work and legacy
|
|
344
|
+
reservations remain distinct liabilities and retain their replay guards. An
|
|
345
|
+
expired question or an uncertain already-accepted turn is not automatically rerun.
|
|
346
|
+
|
|
339
347
|
## Manual enrollment
|
|
340
348
|
|
|
341
349
|
Manual browser-confirmed enrollment remains available when no one-time grant is
|
|
@@ -9,6 +9,7 @@ interface CodexBoundSessionInput {
|
|
|
9
9
|
openTransport(): Promise<CodexStdioTransport>;
|
|
10
10
|
verifier: SessionQuestionVerifier;
|
|
11
11
|
budget: CodexSessionBudget;
|
|
12
|
+
questionBudget?: CodexSessionBudget;
|
|
12
13
|
localWriteRoots?: string[];
|
|
13
14
|
additionalFilesystemRules?: Readonly<Record<string, 'read' | 'deny'>>;
|
|
14
15
|
localToolHandler?: CodexToolHandler;
|
|
@@ -37,6 +38,7 @@ export declare function openCodexBoundSession(input: CodexBoundSessionInput): Pr
|
|
|
37
38
|
question: unknown;
|
|
38
39
|
now?: Date;
|
|
39
40
|
timeoutMs?: number;
|
|
41
|
+
repositoryContext?: string;
|
|
40
42
|
}): Promise<{
|
|
41
43
|
answer: string;
|
|
42
44
|
answerHash: string;
|
|
@@ -52,6 +54,7 @@ export declare function openCodexBoundSession(input: CodexBoundSessionInput): Pr
|
|
|
52
54
|
}>;
|
|
53
55
|
export declare function runCodexBoundSessionQuestion(input: CodexBoundSessionInput & {
|
|
54
56
|
question: unknown;
|
|
57
|
+
repositoryContext?: string;
|
|
55
58
|
now?: Date;
|
|
56
59
|
timeoutMs?: number;
|
|
57
60
|
}): Promise<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codexBoundSession.d.ts","sourceRoot":"","sources":["../src/codexBoundSession.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoC,KAAK,uBAAuB,EAAE,MAAM,wCAAwC,CAAC;AACxH,OAAO,KAAK,EAAE,4BAA4B,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAC3H,OAAO,EAA8E,KAAK,kBAAkB,EAAE,KAAK,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,2EAA2E,CAAC;AACnP,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6EAA6E,CAAC;AAEvH,UAAU,sBAAsB;IAC9B,KAAK,EAAE,UAAU,GAAG,gBAAgB,CAAC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,4BAA4B,CAAC;IAEvC,aAAa,IAAI,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAC9C,QAAQ,EAAE,uBAAuB,CAAC;IAClC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,yBAAyB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC;IACtE,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IAEpC,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,wBAAsB,qBAAqB,CAAC,KAAK,EAAE,sBAAsB;;;qBA2C9C;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,wBAAwB,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAC5G,cAAc,CAAC,EAAE,qBAAqB,CAAA;KAAE;;;;;;;;;;yBAiBf;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,GAAG,CAAC,EAAE,IAAI,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"codexBoundSession.d.ts","sourceRoot":"","sources":["../src/codexBoundSession.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoC,KAAK,uBAAuB,EAAE,MAAM,wCAAwC,CAAC;AACxH,OAAO,KAAK,EAAE,4BAA4B,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAC3H,OAAO,EAA8E,KAAK,kBAAkB,EAAE,KAAK,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,2EAA2E,CAAC;AACnP,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6EAA6E,CAAC;AAEvH,UAAU,sBAAsB;IAC9B,KAAK,EAAE,UAAU,GAAG,gBAAgB,CAAC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,4BAA4B,CAAC;IAEvC,aAAa,IAAI,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAC9C,QAAQ,EAAE,uBAAuB,CAAC;IAClC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,cAAc,CAAC,EAAE,kBAAkB,CAAC;IACpC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,yBAAyB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC;IACtE,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IAEpC,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,wBAAsB,qBAAqB,CAAC,KAAK,EAAE,sBAAsB;;;qBA2C9C;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,wBAAwB,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAC5G,cAAc,CAAC,EAAE,qBAAqB,CAAA;KAAE;;;;;;;;;;yBAiBf;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,GAAG,CAAC,EAAE,IAAI,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;KAAE;;;;;;;;;;;;GAmC/G;AAED,wBAAsB,4BAA4B,CAAC,KAAK,EAAE,sBAAsB,GAAG;IACjF,QAAQ,EAAE,OAAO,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,GAAG,CAAC,EAAE,IAAI,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;;;;;;;;;;;GAmBA"}
|
|
@@ -111,8 +111,9 @@ export async function openCodexBoundSession(input) {
|
|
|
111
111
|
try {
|
|
112
112
|
const result = await runCodexBridgeQuestion({
|
|
113
113
|
transport, binding: codexBinding, question: request.question,
|
|
114
|
-
verifier: input.verifier, exclusiveLease: lease, budget: input.budget,
|
|
114
|
+
verifier: input.verifier, exclusiveLease: lease, budget: input.questionBudget ?? input.budget,
|
|
115
115
|
now: request.now, timeoutMs: request.timeoutMs,
|
|
116
|
+
repositoryContext: request.repositoryContext,
|
|
116
117
|
additionalFilesystemRules: input.additionalFilesystemRules,
|
|
117
118
|
});
|
|
118
119
|
if (closing)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codexBoundSession.js","sourceRoot":"","sources":["../src/codexBoundSession.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gCAAgC,EAAgC,MAAM,wCAAwC,CAAC;AAExH,OAAO,EAAE,+BAA+B,EAAE,sBAAsB,EAAE,iBAAiB,EAA8E,MAAM,2EAA2E,CAAC;
|
|
1
|
+
{"version":3,"file":"codexBoundSession.js","sourceRoot":"","sources":["../src/codexBoundSession.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,6 +3,7 @@ import { createHash, generateKeyPairSync, randomBytes } from 'node:crypto';
|
|
|
3
3
|
import { mkdtemp } from 'node:fs/promises';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join, resolve } from 'node:path';
|
|
6
|
+
import { createServer } from 'node:http';
|
|
6
7
|
import test, { describe } from 'node:test';
|
|
7
8
|
import { canonicalize, signCanonicalObject } from '@dharma-ai-labs/agent-fabric-contracts';
|
|
8
9
|
import { LocalVault } from '@dharma-ai-labs/agent-fabric-local-vault';
|
|
@@ -10,6 +11,7 @@ import { openCodexBoundSession, runCodexBoundSessionQuestion } from './codexBoun
|
|
|
10
11
|
import { openCodexInboxSession } from './codexInboxSession.js';
|
|
11
12
|
import { reconcileProviderSessionReply } from './providerSessionReplyRecovery.js';
|
|
12
13
|
import { NamedPeerContentAuthorizationError } from './namedPeerContentAuthorization.js';
|
|
14
|
+
import { NamedSessionBudget } from './namedSessionBudget.js';
|
|
13
15
|
const { privateKey, publicKey } = generateKeyPairSync('ed25519');
|
|
14
16
|
const ids = {
|
|
15
17
|
repositoryBindingId: '40000000-0000-4000-8000-000000000001',
|
|
@@ -216,6 +218,90 @@ describe('qualified Linux retained Codex consumer', { skip: process.platform !==
|
|
|
216
218
|
f.vault.close();
|
|
217
219
|
}
|
|
218
220
|
});
|
|
221
|
+
for (const scenario of ['current', 'denied', 'forged', 'expired'])
|
|
222
|
+
test('peer context is loaded lazily per offered question before acceptance: '
|
|
223
|
+
+ scenario, async () => {
|
|
224
|
+
const deniedContext = scenario === 'denied';
|
|
225
|
+
const f = await fixture(), remote = fakeTransport(f.binding);
|
|
226
|
+
let offered = false, accepted = false, loads = 0, reserves = 0;
|
|
227
|
+
let q = signedQuestion(f.binding, f.now), context = 'CURRENT_SIGNED_V8';
|
|
228
|
+
if (scenario === 'forged')
|
|
229
|
+
q = { ...q, signature: 'invalid-fixture-signature' };
|
|
230
|
+
if (scenario === 'expired') {
|
|
231
|
+
const { signature: _signature, ...unsigned } = q;
|
|
232
|
+
const expired = { ...unsigned, expiresAt: new Date(f.now.getTime() - 1).toISOString() };
|
|
233
|
+
q = { ...expired, signature: signCanonicalObject(expired, privateKey) };
|
|
234
|
+
}
|
|
235
|
+
const prompts = [];
|
|
236
|
+
const request = remote.transport.request.bind(remote.transport);
|
|
237
|
+
remote.transport.request = async (method, params) => {
|
|
238
|
+
if (method === 'turn/start')
|
|
239
|
+
prompts.push(JSON.stringify(params.input));
|
|
240
|
+
return request(method, params);
|
|
241
|
+
};
|
|
242
|
+
const channelTransport = { async signedPost(route, input) {
|
|
243
|
+
const body = input;
|
|
244
|
+
const envelope = { ok: true, organizationId: f.binding.organizationId, correlationId: ids.threadId };
|
|
245
|
+
if (route.endsWith('provider-sessions'))
|
|
246
|
+
return { ...envelope, registration: {
|
|
247
|
+
bindingId: f.binding.bindingId, workspaceId: f.binding.workspaceId, endpointId: f.binding.endpointId,
|
|
248
|
+
repositoryBindingId: f.binding.repositoryBindingId, membershipId: f.binding.membershipId,
|
|
249
|
+
deviceId: f.binding.deviceId, provider: 'codex', mode: 'bridge_owned', revision: 1, state: 'attached',
|
|
250
|
+
leaseUntil: new Date(Date.now() + 60000).toISOString(), replay: false
|
|
251
|
+
} };
|
|
252
|
+
if (body.action === 'inbox')
|
|
253
|
+
return { ...envelope, result: { offers: offered && !accepted ? [q] : [] } };
|
|
254
|
+
if (body.action === 'accept')
|
|
255
|
+
accepted = true;
|
|
256
|
+
return { ...envelope, result: { questionId: q.questionId, taskId: q.taskId,
|
|
257
|
+
targetBindingId: f.binding.bindingId, state: body.action === 'reply' ? 'answered' : 'accepted', replay: false } };
|
|
258
|
+
} };
|
|
259
|
+
const inbox = await openCodexInboxSession({ ...f, bindingId: f.binding.bindingId, expectedRevision: 0,
|
|
260
|
+
channelTransport, authorizeContent: async () => true,
|
|
261
|
+
budget: { reserve: async () => { reserves++; return true; } }, openTransport: async () => remote.transport });
|
|
262
|
+
const options = { loadRepositoryContext: async () => {
|
|
263
|
+
assert.equal(accepted, false);
|
|
264
|
+
loads++;
|
|
265
|
+
if (deniedContext)
|
|
266
|
+
throw new Error('synthetic_context_denied');
|
|
267
|
+
return context;
|
|
268
|
+
} };
|
|
269
|
+
try {
|
|
270
|
+
assert.equal((await Reflect.apply(inbox.runNext, inbox, [options])).state, 'idle');
|
|
271
|
+
assert.equal(loads, 0);
|
|
272
|
+
offered = true;
|
|
273
|
+
if (scenario === 'forged' || scenario === 'expired') {
|
|
274
|
+
await assert.rejects(Reflect.apply(inbox.runNext, inbox, [options]), /provider_session_channel_response/);
|
|
275
|
+
assert.equal(loads, 0);
|
|
276
|
+
assert.equal(reserves, 0);
|
|
277
|
+
assert.equal(accepted, false);
|
|
278
|
+
assert.equal(prompts.length, 0);
|
|
279
|
+
}
|
|
280
|
+
else if (deniedContext) {
|
|
281
|
+
await assert.rejects(Reflect.apply(inbox.runNext, inbox, [options]), /synthetic_context_denied/);
|
|
282
|
+
assert.equal(loads, 1);
|
|
283
|
+
assert.equal(reserves, 0);
|
|
284
|
+
assert.equal(accepted, false);
|
|
285
|
+
assert.equal(prompts.length, 0);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
assert.equal((await Reflect.apply(inbox.runNext, inbox, [options])).state, 'answered');
|
|
289
|
+
assert.equal(loads, 1);
|
|
290
|
+
assert.match(prompts[0], /CURRENT_SIGNED_V8/);
|
|
291
|
+
context = 'CURRENT_SIGNED_V9';
|
|
292
|
+
accepted = false;
|
|
293
|
+
q = signedQuestion(f.binding, f.now, '40000000-0000-4000-8000-000000000020');
|
|
294
|
+
assert.equal((await Reflect.apply(inbox.runNext, inbox, [options])).state, 'answered');
|
|
295
|
+
assert.equal(loads, 2);
|
|
296
|
+
assert.match(prompts[1], /CURRENT_SIGNED_V9/);
|
|
297
|
+
assert.doesNotMatch(prompts[1], /CURRENT_SIGNED_V8/);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
await inbox.close();
|
|
302
|
+
f.vault.close();
|
|
303
|
+
}
|
|
304
|
+
});
|
|
219
305
|
test('default inbox startup recovers the server revision with its retained local owner and no provider turn', async () => {
|
|
220
306
|
const f = await fixture(), remote = fakeTransport(f.binding), actions = [];
|
|
221
307
|
let reserves = 0;
|
|
@@ -684,6 +770,182 @@ describe('qualified Linux retained Codex consumer', { skip: process.platform !==
|
|
|
684
770
|
f.vault.close();
|
|
685
771
|
}
|
|
686
772
|
});
|
|
773
|
+
test('an unaccepted question recovers its existing reservation only through a fresh acceptance', async () => {
|
|
774
|
+
const f = await fixture(), question = signedQuestion(f.binding, f.now);
|
|
775
|
+
const path = join(f.root, 'question-budget.sqlite');
|
|
776
|
+
let budget = new NamedSessionBudget(path, 'question-recovery', 50);
|
|
777
|
+
let accepts = 0, accepted = false;
|
|
778
|
+
const remotes = [];
|
|
779
|
+
const channelTransport = { async signedPost(route, input) {
|
|
780
|
+
const body = input;
|
|
781
|
+
const envelope = { ok: true, organizationId: f.binding.organizationId, correlationId: ids.threadId };
|
|
782
|
+
if (route.endsWith('provider-sessions'))
|
|
783
|
+
return { ...envelope, registration: {
|
|
784
|
+
bindingId: f.binding.bindingId, workspaceId: f.binding.workspaceId, endpointId: f.binding.endpointId,
|
|
785
|
+
repositoryBindingId: f.binding.repositoryBindingId, membershipId: f.binding.membershipId,
|
|
786
|
+
deviceId: f.binding.deviceId, provider: 'codex', mode: 'bridge_owned',
|
|
787
|
+
revision: Number(body.expectedRevision) + 1, state: 'attached',
|
|
788
|
+
leaseUntil: new Date(Date.now() + 60_000).toISOString(), replay: false
|
|
789
|
+
} };
|
|
790
|
+
if (body.action === 'inbox')
|
|
791
|
+
return { ...envelope, result: { offers: accepted ? [] : [question] } };
|
|
792
|
+
if (body.action === 'accept') {
|
|
793
|
+
accepts++;
|
|
794
|
+
if (accepts === 1)
|
|
795
|
+
throw new Error('private-transport-error-must-not-be-exposed');
|
|
796
|
+
assert.equal(accepted, false);
|
|
797
|
+
accepted = true;
|
|
798
|
+
}
|
|
799
|
+
return { ...envelope, result: { questionId: question.questionId, taskId: question.taskId,
|
|
800
|
+
targetBindingId: f.binding.bindingId, state: body.action === 'reply' ? 'answered' : 'accepted', replay: false } };
|
|
801
|
+
} };
|
|
802
|
+
const open = async () => {
|
|
803
|
+
const remote = fakeTransport(f.binding);
|
|
804
|
+
remotes.push(remote);
|
|
805
|
+
const options = { ...f, bindingId: f.binding.bindingId, expectedRevision: 0,
|
|
806
|
+
budget: { reserve: async (id, cents) => budget.reserve(id, cents) },
|
|
807
|
+
questionBudget: { reserve: async (id, cents) => budget.reserveUnacceptedQuestion(id, cents) },
|
|
808
|
+
channelTransport, authorizeContent: async () => true, openTransport: async () => remote.transport };
|
|
809
|
+
return openCodexInboxSession(options);
|
|
810
|
+
};
|
|
811
|
+
let inbox = await open();
|
|
812
|
+
try {
|
|
813
|
+
await assert.rejects(inbox.runNext(), /provider_session_channel_uncertain/);
|
|
814
|
+
assert.equal(accepted, false);
|
|
815
|
+
assert.equal(remotes[0].calls.includes('turn/start'), false);
|
|
816
|
+
assert.equal(budget.status().reservedCents, 25);
|
|
817
|
+
await inbox.close();
|
|
818
|
+
budget.close();
|
|
819
|
+
budget = new NamedSessionBudget(path, 'question-recovery', 50);
|
|
820
|
+
inbox = await open();
|
|
821
|
+
assert.equal((await inbox.runNext()).state, 'answered');
|
|
822
|
+
assert.equal(accepts, 2);
|
|
823
|
+
assert.equal(remotes.flatMap(remote => remote.calls).filter(call => call === 'turn/start').length, 1);
|
|
824
|
+
assert.equal(budget.status().reservedCents, 25);
|
|
825
|
+
assert.equal(budget.reserve(question.questionId, 25), true, 'a distinct local-work purpose needs its own reservation');
|
|
826
|
+
assert.equal(budget.reserve(question.questionId, 25), false, 'local-work reservation replay stays forbidden');
|
|
827
|
+
assert.equal(budget.status().reservedCents, 50);
|
|
828
|
+
}
|
|
829
|
+
finally {
|
|
830
|
+
await inbox.close();
|
|
831
|
+
budget.close();
|
|
832
|
+
f.vault.close();
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
for (const lostAt of ['before_commit', 'after_commit'])
|
|
836
|
+
test(`HTTP acceptance loss ${lostAt} preserves cost and never repeats execution`, async () => {
|
|
837
|
+
const f = await fixture(), question = signedQuestion(f.binding, f.now);
|
|
838
|
+
const path = join(f.root, 'http-question-budget.sqlite');
|
|
839
|
+
let budget = new NamedSessionBudget(path, 'http-question-recovery', 50);
|
|
840
|
+
let accepted = false, accepts = 0;
|
|
841
|
+
const remotes = [];
|
|
842
|
+
const server = createServer(async (request, response) => {
|
|
843
|
+
const chunks = [];
|
|
844
|
+
for await (const chunk of request)
|
|
845
|
+
chunks.push(Buffer.from(chunk));
|
|
846
|
+
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
847
|
+
const envelope = { ok: true, organizationId: f.binding.organizationId, correlationId: ids.threadId };
|
|
848
|
+
const send = (value) => { response.setHeader('content-type', 'application/json'); response.end(JSON.stringify(value)); };
|
|
849
|
+
assert.equal(body.bindingId, f.binding.bindingId);
|
|
850
|
+
assert.equal(body.workspaceId, f.binding.workspaceId);
|
|
851
|
+
if (request.url?.endsWith('provider-sessions')) {
|
|
852
|
+
send({ ...envelope, registration: {
|
|
853
|
+
bindingId: f.binding.bindingId, workspaceId: f.binding.workspaceId, endpointId: f.binding.endpointId,
|
|
854
|
+
repositoryBindingId: f.binding.repositoryBindingId, membershipId: f.binding.membershipId,
|
|
855
|
+
deviceId: f.binding.deviceId, provider: 'codex', mode: 'bridge_owned',
|
|
856
|
+
revision: Number(body.expectedRevision) + 1, state: 'attached',
|
|
857
|
+
leaseUntil: new Date(Date.now() + 60_000).toISOString(), replay: false
|
|
858
|
+
} });
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (body.action === 'inbox') {
|
|
862
|
+
send({ ...envelope, result: { offers: accepted ? [] : [question] } });
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
if (body.action === 'accept') {
|
|
866
|
+
accepts++;
|
|
867
|
+
if (accepts === 1) {
|
|
868
|
+
accepted = lostAt === 'after_commit';
|
|
869
|
+
request.socket.destroy();
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
assert.equal(accepted, false);
|
|
873
|
+
accepted = true;
|
|
874
|
+
}
|
|
875
|
+
send({ ...envelope, result: { questionId: question.questionId, taskId: question.taskId,
|
|
876
|
+
targetBindingId: f.binding.bindingId, state: body.action === 'reply' ? 'answered' : 'accepted', replay: false } });
|
|
877
|
+
});
|
|
878
|
+
await new Promise(resolveListen => server.listen(0, '127.0.0.1', resolveListen));
|
|
879
|
+
const origin = `http://127.0.0.1:${server.address().port}`;
|
|
880
|
+
const channelTransport = { async signedPost(route, body) {
|
|
881
|
+
const response = await fetch(origin + route, { method: 'POST', body: JSON.stringify(body),
|
|
882
|
+
headers: { 'content-type': 'application/json' }, signal: AbortSignal.timeout(3000) });
|
|
883
|
+
assert.equal(response.status, 200);
|
|
884
|
+
return await response.json();
|
|
885
|
+
} };
|
|
886
|
+
const open = async () => {
|
|
887
|
+
const remote = fakeTransport(f.binding);
|
|
888
|
+
remotes.push(remote);
|
|
889
|
+
return openCodexInboxSession({ ...f, bindingId: f.binding.bindingId, expectedRevision: 0,
|
|
890
|
+
budget: { reserve: async (id, cents) => budget.reserve(id, cents) },
|
|
891
|
+
questionBudget: { reserve: async (id, cents) => budget.reserveUnacceptedQuestion(id, cents) },
|
|
892
|
+
channelTransport, authorizeContent: async () => true, openTransport: async () => remote.transport });
|
|
893
|
+
};
|
|
894
|
+
let inbox = await open();
|
|
895
|
+
try {
|
|
896
|
+
await assert.rejects(inbox.runNext(), /provider_session_channel_uncertain/);
|
|
897
|
+
assert.equal(remotes[0].calls.includes('turn/start'), false);
|
|
898
|
+
assert.equal(budget.status().reservedCents, 25);
|
|
899
|
+
await inbox.close();
|
|
900
|
+
budget.close();
|
|
901
|
+
budget = new NamedSessionBudget(path, 'http-question-recovery', 50);
|
|
902
|
+
inbox = await open();
|
|
903
|
+
assert.equal((await inbox.runNext()).state, lostAt === 'before_commit' ? 'answered' : 'idle');
|
|
904
|
+
assert.equal(accepts, lostAt === 'before_commit' ? 2 : 1);
|
|
905
|
+
assert.equal(remotes.flatMap(remote => remote.calls).filter(call => call === 'turn/start').length, lostAt === 'before_commit' ? 1 : 0);
|
|
906
|
+
assert.equal(budget.status().reservedCents, 25);
|
|
907
|
+
}
|
|
908
|
+
finally {
|
|
909
|
+
await inbox.close();
|
|
910
|
+
budget.close();
|
|
911
|
+
f.vault.close();
|
|
912
|
+
server.closeAllConnections();
|
|
913
|
+
await new Promise(resolveClose => server.close(() => resolveClose()));
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
test('a replayed acceptance never authorizes another provider turn', async () => {
|
|
917
|
+
const f = await fixture(), question = signedQuestion(f.binding, f.now), remote = fakeTransport(f.binding);
|
|
918
|
+
let accepts = 0;
|
|
919
|
+
const channelTransport = { async signedPost(route, input) {
|
|
920
|
+
const body = input;
|
|
921
|
+
const envelope = { ok: true, organizationId: f.binding.organizationId, correlationId: ids.threadId };
|
|
922
|
+
if (route.endsWith('provider-sessions'))
|
|
923
|
+
return { ...envelope, registration: {
|
|
924
|
+
bindingId: f.binding.bindingId, workspaceId: f.binding.workspaceId, endpointId: f.binding.endpointId,
|
|
925
|
+
repositoryBindingId: f.binding.repositoryBindingId, membershipId: f.binding.membershipId,
|
|
926
|
+
deviceId: f.binding.deviceId, provider: 'codex', mode: 'bridge_owned',
|
|
927
|
+
revision: Number(body.expectedRevision) + 1, state: 'attached',
|
|
928
|
+
leaseUntil: new Date(Date.now() + 60_000).toISOString(), replay: false
|
|
929
|
+
} };
|
|
930
|
+
if (body.action === 'inbox')
|
|
931
|
+
return { ...envelope, result: { offers: [question] } };
|
|
932
|
+
if (body.action === 'accept')
|
|
933
|
+
accepts++;
|
|
934
|
+
return { ...envelope, result: { questionId: question.questionId, taskId: question.taskId,
|
|
935
|
+
targetBindingId: f.binding.bindingId, state: body.action === 'reply' ? 'answered' : 'accepted', replay: true } };
|
|
936
|
+
} };
|
|
937
|
+
const inbox = await openCodexInboxSession({ ...f, bindingId: f.binding.bindingId, expectedRevision: 0,
|
|
938
|
+
channelTransport, authorizeContent: async () => true, openTransport: async () => remote.transport });
|
|
939
|
+
try {
|
|
940
|
+
await assert.rejects(inbox.runNext(), /replayed/);
|
|
941
|
+
assert.equal(accepts, 1);
|
|
942
|
+
assert.equal(remote.calls.includes('turn/start'), false);
|
|
943
|
+
}
|
|
944
|
+
finally {
|
|
945
|
+
await inbox.close();
|
|
946
|
+
f.vault.close();
|
|
947
|
+
}
|
|
948
|
+
});
|
|
687
949
|
test('bound dispatch resolves one encrypted binding and releases a completed turn', async () => {
|
|
688
950
|
const f = await fixture();
|
|
689
951
|
const remote = fakeTransport(f.binding);
|