ds-harness-remote 0.4.3 → 0.4.4
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 +57 -9
- package/bin/ds-harness-remote.js +11 -0
- package/cordis.patch.yml +22 -0
- package/dist/cli.d.ts +31 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +283 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex/domain.d.ts +4 -2
- package/dist/codex/domain.d.ts.map +1 -1
- package/dist/codex/domain.js +192 -68
- package/dist/codex/domain.js.map +1 -1
- package/dist/codex/method-policy.d.ts +7 -7
- package/dist/codex/virtual-harness.d.ts +3 -0
- package/dist/codex/virtual-harness.d.ts.map +1 -1
- package/dist/codex/virtual-harness.js +89 -20
- package/dist/codex/virtual-harness.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1147 -258
- package/dist/index.js.map +4 -4
- package/dist/server-api.d.ts.map +1 -1
- package/dist/server-api.js +15 -2
- package/dist/server-api.js.map +1 -1
- package/dist/server-credentials.d.ts +2 -2
- package/dist/service.d.ts +4 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +34 -2
- package/dist/service.js.map +1 -1
- package/dist/tui-command.d.ts +13 -0
- package/dist/tui-command.d.ts.map +1 -0
- package/dist/tui-command.js +378 -0
- package/dist/tui-command.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dsh-plugin.json +41 -0
- package/package.json +6 -1
- package/public.d.ts +1 -0
package/dist/codex/domain.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { realpath, stat } from 'node:fs/promises';
|
|
2
3
|
import { homedir } from 'node:os';
|
|
3
|
-
import { isAbsolute, join } from 'node:path';
|
|
4
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
5
|
+
import { deriveCodexCwdWorkspaces } from '@dsh-remote/client-core';
|
|
4
6
|
import { RpcError } from '../safe-error.js';
|
|
5
7
|
import { CodexAppServerClient, CodexAppServerError, } from './app-server.js';
|
|
6
8
|
import { isThreadMutation, parseCodexCall, threadIdFromParams, } from './method-policy.js';
|
|
@@ -9,6 +11,7 @@ import { paginateCodexNativeHistory, projectCodexNativeHistory } from './virtual
|
|
|
9
11
|
const APPROVAL_TTL_MS = 5 * 60_000;
|
|
10
12
|
const DEFAULT_RESTART_DELAYS_MS = [1_000, 2_000, 4_000, 8_000, 15_000];
|
|
11
13
|
const CODEX_PAGE_LIMIT = 100;
|
|
14
|
+
const MAX_CODEX_PAGES = 32;
|
|
12
15
|
const CODEX_HISTORY_PAGE_LIMIT = 25;
|
|
13
16
|
const MAX_CODEX_HISTORY_PAGES = 64;
|
|
14
17
|
/**
|
|
@@ -25,6 +28,7 @@ export class CodexRemoteDomain {
|
|
|
25
28
|
unsubscribeInbound;
|
|
26
29
|
unsubscribeUnavailable;
|
|
27
30
|
peers = new Map();
|
|
31
|
+
peerDeviceIds = new Map();
|
|
28
32
|
turnOwners = new Map();
|
|
29
33
|
approvals = new Map();
|
|
30
34
|
approvalExpiryTimer;
|
|
@@ -72,6 +76,7 @@ export class CodexRemoteDomain {
|
|
|
72
76
|
return undefined;
|
|
73
77
|
const bridge = new CodexPeerBridge(this, context, publish, this.logger);
|
|
74
78
|
this.peers.set(context.connectionId, bridge);
|
|
79
|
+
this.peerDeviceIds.set(context.connectionId, context.peerDeviceId);
|
|
75
80
|
return bridge;
|
|
76
81
|
}
|
|
77
82
|
async call(connectionId, input) {
|
|
@@ -85,16 +90,16 @@ export class CodexRemoteDomain {
|
|
|
85
90
|
return sanitizeProjectList(await this.callUpstream(call.method, call.params));
|
|
86
91
|
}
|
|
87
92
|
if (call.method === 'thread/list') {
|
|
88
|
-
|
|
93
|
+
const result = sanitizeThreadList(await this.callUpstream(call.method, call.params));
|
|
94
|
+
return filterThreadListByWorkspaceAuthority(result, await this.readWorkspaceAuthority(result.data));
|
|
89
95
|
}
|
|
90
96
|
if (call.method === 'thread/start') {
|
|
91
97
|
const cwd = await this.requireCodexWorkspacePath(call.params.cwd);
|
|
92
|
-
const permission = codexPermission(call.params);
|
|
98
|
+
const permission = codexPermission(call.params, 'workspace-write');
|
|
93
99
|
const result = await this.callUpstream(call.method, {
|
|
94
100
|
...permission.params,
|
|
95
101
|
cwd,
|
|
96
|
-
|
|
97
|
-
sandbox: permission.sandbox,
|
|
102
|
+
...codexThreadPermissionParams(permission),
|
|
98
103
|
serviceName: 'deepseek_harness_remote',
|
|
99
104
|
});
|
|
100
105
|
if (extractThread(result)?.id === undefined) {
|
|
@@ -105,11 +110,15 @@ export class CodexRemoteDomain {
|
|
|
105
110
|
const threadId = threadIdFromParams(call.params);
|
|
106
111
|
if (call.method === 'dsh/sessionHistory') {
|
|
107
112
|
const thread = await this.readThreadForHistory(connectionId, threadId);
|
|
108
|
-
|
|
113
|
+
const page = paginateCodexNativeHistory(projectCodexNativeHistory(thread, `codex:${threadId}`), {
|
|
109
114
|
beforeSeq: optionalInteger(call.params.beforeSeq),
|
|
110
115
|
throughSeq: optionalInteger(call.params.throughSeq),
|
|
111
116
|
maxMessages: optionalInteger(call.params.maxMessages),
|
|
112
117
|
});
|
|
118
|
+
const activeTurnId = typeof page.activeTurnId === 'string'
|
|
119
|
+
? page.activeTurnId
|
|
120
|
+
: this.turnOwners.get(threadId)?.turnId;
|
|
121
|
+
return activeTurnId === undefined ? page : { ...page, activeTurnId };
|
|
113
122
|
}
|
|
114
123
|
const allowedThread = threadId === undefined ? undefined : await this.readKnownThread(threadId);
|
|
115
124
|
if (call.method === 'thread/read') {
|
|
@@ -125,8 +134,11 @@ export class CodexRemoteDomain {
|
|
|
125
134
|
return this.callUpstream(call.method, call.params);
|
|
126
135
|
}
|
|
127
136
|
let claimed = false;
|
|
137
|
+
let previousOwner;
|
|
128
138
|
if (isThreadMutation(call.method) && threadId !== undefined) {
|
|
129
|
-
|
|
139
|
+
const claim = this.claimTurn(threadId, connectionId, call.method, typeof call.params.turnId === 'string' ? call.params.turnId : undefined);
|
|
140
|
+
claimed = claim.claimed;
|
|
141
|
+
previousOwner = claim.previous;
|
|
130
142
|
}
|
|
131
143
|
try {
|
|
132
144
|
const permission = codexPermission(call.params);
|
|
@@ -134,31 +146,20 @@ export class CodexRemoteDomain {
|
|
|
134
146
|
? {
|
|
135
147
|
...permission.params,
|
|
136
148
|
...(allowedThread.cwd === undefined ? {} : { cwd: allowedThread.cwd }),
|
|
137
|
-
|
|
138
|
-
sandbox: permission.sandbox,
|
|
149
|
+
...codexThreadPermissionParams(permission),
|
|
139
150
|
excludeTurns: true,
|
|
140
151
|
}
|
|
141
152
|
: call.method === 'thread/fork' && allowedThread !== undefined
|
|
142
153
|
? {
|
|
143
154
|
...permission.params,
|
|
144
155
|
...(allowedThread.cwd === undefined ? {} : { cwd: allowedThread.cwd }),
|
|
145
|
-
|
|
146
|
-
sandbox: permission.sandbox,
|
|
156
|
+
...codexThreadPermissionParams(permission),
|
|
147
157
|
}
|
|
148
158
|
: call.method === 'turn/start'
|
|
149
159
|
? {
|
|
150
160
|
...permission.params,
|
|
151
161
|
...(allowedThread?.cwd === undefined ? {} : { cwd: allowedThread.cwd }),
|
|
152
|
-
|
|
153
|
-
sandboxPolicy: permission.sandbox === 'danger-full-access'
|
|
154
|
-
? { type: 'dangerFullAccess' }
|
|
155
|
-
: {
|
|
156
|
-
type: 'workspaceWrite',
|
|
157
|
-
writableRoots: allowedThread?.cwd === undefined ? [] : [allowedThread.cwd],
|
|
158
|
-
networkAccess: false,
|
|
159
|
-
excludeTmpdirEnvVar: false,
|
|
160
|
-
excludeSlashTmp: false,
|
|
161
|
-
},
|
|
162
|
+
...codexTurnPermissionParams(permission, allowedThread?.cwd),
|
|
162
163
|
}
|
|
163
164
|
: permission.params;
|
|
164
165
|
let result;
|
|
@@ -172,7 +173,8 @@ export class CodexRemoteDomain {
|
|
|
172
173
|
if (call.method === 'thread/resume'
|
|
173
174
|
&& allowedThread !== undefined
|
|
174
175
|
&& error instanceof RpcError
|
|
175
|
-
&& error.code === 'CODEX_UPSTREAM_ERROR'
|
|
176
|
+
&& error.code === 'CODEX_UPSTREAM_ERROR'
|
|
177
|
+
&& call.params.permissionPreset === undefined) {
|
|
176
178
|
return { thread: allowedThread.thread };
|
|
177
179
|
}
|
|
178
180
|
throw error;
|
|
@@ -180,11 +182,18 @@ export class CodexRemoteDomain {
|
|
|
180
182
|
if (call.method === 'thread/resume' || call.method === 'thread/fork' || call.method === 'thread/unarchive') {
|
|
181
183
|
await this.assertResultThreadAllowed(result);
|
|
182
184
|
}
|
|
185
|
+
if (call.method === 'turn/start' && threadId !== undefined) {
|
|
186
|
+
this.rememberTurnId(threadId, connectionId, extractTurnId(result));
|
|
187
|
+
}
|
|
183
188
|
return result;
|
|
184
189
|
}
|
|
185
190
|
catch (error) {
|
|
186
|
-
if (claimed && threadId !== undefined)
|
|
187
|
-
|
|
191
|
+
if (claimed && threadId !== undefined) {
|
|
192
|
+
if (previousOwner === undefined)
|
|
193
|
+
this.turnOwners.delete(threadId);
|
|
194
|
+
else
|
|
195
|
+
this.turnOwners.set(threadId, previousOwner);
|
|
196
|
+
}
|
|
188
197
|
throw mapAppServerError(error);
|
|
189
198
|
}
|
|
190
199
|
}
|
|
@@ -204,8 +213,9 @@ export class CodexRemoteDomain {
|
|
|
204
213
|
const bridge = this.peers.get(connectionId);
|
|
205
214
|
if (bridge !== undefined)
|
|
206
215
|
this.peers.delete(connectionId);
|
|
216
|
+
this.peerDeviceIds.delete(connectionId);
|
|
207
217
|
for (const [threadId, owner] of this.turnOwners) {
|
|
208
|
-
if (owner === connectionId)
|
|
218
|
+
if (owner.connectionId === connectionId)
|
|
209
219
|
this.turnOwners.delete(threadId);
|
|
210
220
|
}
|
|
211
221
|
const appServer = this.appServer;
|
|
@@ -228,6 +238,7 @@ export class CodexRemoteDomain {
|
|
|
228
238
|
for (const bridge of [...this.peers.values()])
|
|
229
239
|
await bridge.closeAll();
|
|
230
240
|
this.peers.clear();
|
|
241
|
+
this.peerDeviceIds.clear();
|
|
231
242
|
this.turnOwners.clear();
|
|
232
243
|
if (this.approvalExpiryTimer !== undefined)
|
|
233
244
|
clearTimeout(this.approvalExpiryTimer);
|
|
@@ -359,6 +370,13 @@ export class CodexRemoteDomain {
|
|
|
359
370
|
const threadId = extractThreadId(message.params);
|
|
360
371
|
if (message.method === 'turn/completed' && threadId !== undefined)
|
|
361
372
|
this.turnOwners.delete(threadId);
|
|
373
|
+
if (message.method === 'turn/started' && threadId !== undefined) {
|
|
374
|
+
const owner = this.turnOwners.get(threadId);
|
|
375
|
+
const turnId = extractTurnId(message.params);
|
|
376
|
+
if (owner !== undefined) {
|
|
377
|
+
this.turnOwners.set(threadId, { ...owner, ...(turnId === undefined ? {} : { turnId }) });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
362
380
|
if (message.method === 'serverRequest/resolved')
|
|
363
381
|
this.resolveUpstreamApproval(message.params);
|
|
364
382
|
if (threadId === undefined)
|
|
@@ -378,7 +396,7 @@ export class CodexRemoteDomain {
|
|
|
378
396
|
}
|
|
379
397
|
const threadId = extractThreadId(message.params);
|
|
380
398
|
const owner = threadId === undefined ? undefined : this.turnOwners.get(threadId);
|
|
381
|
-
const peer = owner === undefined ? undefined : this.peers.get(owner);
|
|
399
|
+
const peer = owner === undefined ? undefined : this.peers.get(owner.connectionId);
|
|
382
400
|
if (threadId === undefined || owner === undefined || peer === undefined || !peer.hasThreadSubscription(threadId)) {
|
|
383
401
|
await appServer.respond(message.id, { decision: 'decline' });
|
|
384
402
|
return;
|
|
@@ -386,7 +404,7 @@ export class CodexRemoteDomain {
|
|
|
386
404
|
const requestHandle = randomUUID();
|
|
387
405
|
this.approvals.set(requestHandle, {
|
|
388
406
|
upstreamId: message.id,
|
|
389
|
-
connectionId: owner,
|
|
407
|
+
connectionId: owner.connectionId,
|
|
390
408
|
threadId,
|
|
391
409
|
method: message.method,
|
|
392
410
|
expiresAt: Date.now() + APPROVAL_TTL_MS,
|
|
@@ -406,7 +424,7 @@ export class CodexRemoteDomain {
|
|
|
406
424
|
}
|
|
407
425
|
}
|
|
408
426
|
async readKnownThread(threadId) {
|
|
409
|
-
const authority = await this.
|
|
427
|
+
const authority = await this.readWorkspaceAuthority();
|
|
410
428
|
let result;
|
|
411
429
|
try {
|
|
412
430
|
result = await this.callUpstream('thread/read', { threadId, includeTurns: false });
|
|
@@ -421,7 +439,7 @@ export class CodexRemoteDomain {
|
|
|
421
439
|
return { thread: listed, ...(typeof listed.cwd === 'string' && listed.cwd.length > 0 ? { cwd: listed.cwd } : {}) };
|
|
422
440
|
}
|
|
423
441
|
const thread = extractThread(result);
|
|
424
|
-
if (thread === undefined || thread.id !== threadId || !
|
|
442
|
+
if (thread === undefined || thread.id !== threadId || !isThreadAllowedByWorkspaceAuthority(thread, authority)) {
|
|
425
443
|
throw new RpcError('CODEX_THREAD_NOT_ALLOWED', 'The Codex thread is not available through this Remote Host.');
|
|
426
444
|
}
|
|
427
445
|
return { thread, ...(typeof thread.cwd === 'string' && thread.cwd.length > 0 ? { cwd: thread.cwd } : {}) };
|
|
@@ -532,31 +550,48 @@ export class CodexRemoteDomain {
|
|
|
532
550
|
if (thread === undefined || typeof thread.id !== 'string') {
|
|
533
551
|
throw new RpcError('CODEX_INVALID_RESPONSE', 'Codex App Server returned an invalid thread.');
|
|
534
552
|
}
|
|
535
|
-
if (!
|
|
553
|
+
if (!isThreadAllowedByWorkspaceAuthority(thread, await this.readWorkspaceAuthority())) {
|
|
536
554
|
throw new RpcError('CODEX_THREAD_NOT_ALLOWED', 'The Codex thread is not available through this Remote Host.');
|
|
537
555
|
}
|
|
538
556
|
}
|
|
539
|
-
claimTurn(threadId, connectionId, method) {
|
|
557
|
+
claimTurn(threadId, connectionId, method, turnId) {
|
|
540
558
|
const owner = this.turnOwners.get(threadId);
|
|
559
|
+
const peerDeviceId = this.peerDeviceIds.get(connectionId) ?? connectionId;
|
|
560
|
+
const nextOwner = { connectionId, peerDeviceId, ...(turnId === undefined ? {} : { turnId }) };
|
|
541
561
|
if (method === 'turn/interrupt') {
|
|
542
562
|
if (owner === undefined) {
|
|
543
563
|
// A replacement connection may recover an already-running persisted
|
|
544
564
|
// turn. Claim only for this explicit mutation; passive viewers still
|
|
545
565
|
// remain observers and cannot steal a live connection's lease.
|
|
546
|
-
this.turnOwners.set(threadId,
|
|
547
|
-
return true;
|
|
566
|
+
this.turnOwners.set(threadId, nextOwner);
|
|
567
|
+
return { claimed: true };
|
|
548
568
|
}
|
|
549
|
-
if (owner !== connectionId)
|
|
569
|
+
if (owner.connectionId !== connectionId && owner.peerDeviceId !== peerDeviceId) {
|
|
550
570
|
throw new RpcError('CODEX_TURN_OWNED', 'Only the connection that started this Codex turn can interrupt it.');
|
|
551
|
-
|
|
571
|
+
}
|
|
572
|
+
if (owner.connectionId !== connectionId) {
|
|
573
|
+
this.turnOwners.set(threadId, { ...owner, connectionId, peerDeviceId, ...(turnId === undefined ? {} : { turnId }) });
|
|
574
|
+
return { claimed: true, previous: owner };
|
|
575
|
+
}
|
|
576
|
+
if (turnId !== undefined && owner.turnId === undefined)
|
|
577
|
+
this.turnOwners.set(threadId, { ...owner, turnId });
|
|
578
|
+
return { claimed: false };
|
|
552
579
|
}
|
|
553
|
-
if (owner !== undefined && owner !== connectionId) {
|
|
580
|
+
if (owner !== undefined && owner.connectionId !== connectionId && owner.peerDeviceId !== peerDeviceId) {
|
|
554
581
|
throw new RpcError('CODEX_TURN_OWNED', 'Another Remote connection owns the active Codex turn.');
|
|
555
582
|
}
|
|
556
|
-
if (owner === connectionId)
|
|
557
|
-
return false;
|
|
558
|
-
this.turnOwners.set(threadId, connectionId);
|
|
559
|
-
return true;
|
|
583
|
+
if (owner?.connectionId === connectionId)
|
|
584
|
+
return { claimed: false };
|
|
585
|
+
this.turnOwners.set(threadId, owner === undefined ? nextOwner : { ...owner, connectionId, peerDeviceId });
|
|
586
|
+
return { claimed: true, previous: owner };
|
|
587
|
+
}
|
|
588
|
+
rememberTurnId(threadId, connectionId, turnId) {
|
|
589
|
+
if (turnId === undefined)
|
|
590
|
+
return;
|
|
591
|
+
const owner = this.turnOwners.get(threadId);
|
|
592
|
+
if (owner === undefined || owner.connectionId !== connectionId)
|
|
593
|
+
return;
|
|
594
|
+
this.turnOwners.set(threadId, { ...owner, turnId });
|
|
560
595
|
}
|
|
561
596
|
hasSubscriber(threadId) {
|
|
562
597
|
return [...this.peers.values()].some(peer => peer.hasThreadSubscription(threadId));
|
|
@@ -609,31 +644,60 @@ export class CodexRemoteDomain {
|
|
|
609
644
|
throw mapAppServerError(error);
|
|
610
645
|
}
|
|
611
646
|
}
|
|
612
|
-
async
|
|
647
|
+
async readWorkspaceAuthority(listedThreads) {
|
|
613
648
|
const projectIds = new Set();
|
|
614
649
|
const roots = [];
|
|
615
650
|
let cursor;
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
651
|
+
try {
|
|
652
|
+
for (let page = 0; page < MAX_CODEX_PAGES; page += 1) {
|
|
653
|
+
const result = sanitizeProjectList(await this.callUpstream('project/list', {
|
|
654
|
+
limit: CODEX_PAGE_LIMIT,
|
|
655
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
656
|
+
}));
|
|
657
|
+
for (const project of result.data) {
|
|
658
|
+
const projectRoots = project.roots.map(root => root.path).filter(path => isAbsolute(path));
|
|
659
|
+
if (projectRoots.length === 0)
|
|
660
|
+
continue;
|
|
661
|
+
projectIds.add(project.id);
|
|
662
|
+
roots.push(...projectRoots);
|
|
663
|
+
}
|
|
664
|
+
cursor = typeof result.nextCursor === 'string' && result.nextCursor.length > 0 ? result.nextCursor : undefined;
|
|
665
|
+
if (cursor === undefined)
|
|
666
|
+
break;
|
|
627
667
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
if (!isProjectListFallbackError(error))
|
|
671
|
+
throw error;
|
|
672
|
+
}
|
|
673
|
+
if (roots.length > 0)
|
|
674
|
+
return { projectIds, roots };
|
|
675
|
+
const threads = listedThreads === undefined ? [] : [...listedThreads];
|
|
676
|
+
if (listedThreads === undefined) {
|
|
677
|
+
cursor = undefined;
|
|
678
|
+
for (let page = 0; page < MAX_CODEX_PAGES; page += 1) {
|
|
679
|
+
const result = sanitizeThreadList(await this.callUpstream('thread/list', {
|
|
680
|
+
limit: CODEX_PAGE_LIMIT,
|
|
681
|
+
sortKey: 'updated_at',
|
|
682
|
+
sortDirection: 'desc',
|
|
683
|
+
archived: false,
|
|
684
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
685
|
+
}));
|
|
686
|
+
threads.push(...result.data);
|
|
687
|
+
cursor = typeof result.nextCursor === 'string' && result.nextCursor.length > 0 ? result.nextCursor : undefined;
|
|
688
|
+
if (cursor === undefined)
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
for (const workspace of deriveCodexCwdWorkspaces(threads)) {
|
|
693
|
+
if (isAbsolute(workspace.path))
|
|
694
|
+
roots.push(workspace.path);
|
|
631
695
|
}
|
|
632
696
|
return { projectIds, roots };
|
|
633
697
|
}
|
|
634
698
|
async findKnownThreadInList(threadId, authority) {
|
|
635
699
|
let cursor;
|
|
636
|
-
for (let page = 0; page <
|
|
700
|
+
for (let page = 0; page < MAX_CODEX_PAGES; page += 1) {
|
|
637
701
|
const result = sanitizeThreadList(await this.callUpstream('thread/list', {
|
|
638
702
|
limit: 100,
|
|
639
703
|
sortKey: 'updated_at',
|
|
@@ -643,7 +707,7 @@ export class CodexRemoteDomain {
|
|
|
643
707
|
}));
|
|
644
708
|
const thread = result.data.find(item => item.id === threadId);
|
|
645
709
|
if (thread !== undefined) {
|
|
646
|
-
return
|
|
710
|
+
return isThreadAllowedByWorkspaceAuthority(thread, authority) ? thread : undefined;
|
|
647
711
|
}
|
|
648
712
|
cursor = typeof result.nextCursor === 'string' && result.nextCursor.length > 0 ? result.nextCursor : undefined;
|
|
649
713
|
if (cursor === undefined)
|
|
@@ -655,16 +719,34 @@ export class CodexRemoteDomain {
|
|
|
655
719
|
if (!isAbsolute(path)) {
|
|
656
720
|
throw new RpcError('CODEX_PATH_NOT_ALLOWED', 'The CodeX working directory is not available as a Workspace.');
|
|
657
721
|
}
|
|
658
|
-
const paths = await this.
|
|
659
|
-
const
|
|
660
|
-
|
|
722
|
+
const paths = await this.listCodexWorkspacePaths();
|
|
723
|
+
const lexicalCandidate = resolve(path);
|
|
724
|
+
let candidate;
|
|
725
|
+
try {
|
|
726
|
+
candidate = await realpath(path);
|
|
727
|
+
if (!(await stat(candidate)).isDirectory())
|
|
728
|
+
throw new Error('not a directory');
|
|
729
|
+
}
|
|
730
|
+
catch {
|
|
661
731
|
throw new RpcError('CODEX_PATH_NOT_ALLOWED', 'The CodeX working directory is not available as a Workspace.');
|
|
662
732
|
}
|
|
663
|
-
|
|
733
|
+
for (const root of paths.values()) {
|
|
734
|
+
try {
|
|
735
|
+
if (!containsCodexPath(resolve(root), lexicalCandidate))
|
|
736
|
+
continue;
|
|
737
|
+
const canonicalRoot = await realpath(root);
|
|
738
|
+
if (containsCodexPath(canonicalRoot, candidate))
|
|
739
|
+
return lexicalCandidate;
|
|
740
|
+
}
|
|
741
|
+
catch {
|
|
742
|
+
// A stale project or Thread cwd is not authority for a new Thread.
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
throw new RpcError('CODEX_PATH_NOT_ALLOWED', 'The CodeX working directory is not available as a Workspace.');
|
|
664
746
|
}
|
|
665
|
-
async
|
|
747
|
+
async listCodexWorkspacePaths() {
|
|
666
748
|
const paths = new Map();
|
|
667
|
-
const authority = await this.
|
|
749
|
+
const authority = await this.readWorkspaceAuthority();
|
|
668
750
|
for (const root of authority.roots)
|
|
669
751
|
paths.set(normalizeCodexPathForCompare(root), root);
|
|
670
752
|
return paths;
|
|
@@ -749,7 +831,7 @@ function sanitizeThreadList(result) {
|
|
|
749
831
|
: {}),
|
|
750
832
|
};
|
|
751
833
|
}
|
|
752
|
-
function
|
|
834
|
+
function filterThreadListByWorkspaceAuthority(result, authority) {
|
|
753
835
|
if (!isRecord(result) || !Array.isArray(result.data)) {
|
|
754
836
|
throw new RpcError('CODEX_INVALID_RESPONSE', 'Codex App Server returned an invalid thread list.');
|
|
755
837
|
}
|
|
@@ -757,7 +839,7 @@ function filterThreadListByProjectAuthority(result, authority) {
|
|
|
757
839
|
...result,
|
|
758
840
|
data: result.data
|
|
759
841
|
.map(record => isRecord(record) ? record : undefined)
|
|
760
|
-
.filter((thread) => (thread !== undefined &&
|
|
842
|
+
.filter((thread) => (thread !== undefined && isThreadAllowedByWorkspaceAuthority(thread, authority))),
|
|
761
843
|
};
|
|
762
844
|
}
|
|
763
845
|
function sanitizeProjectList(result) {
|
|
@@ -789,7 +871,7 @@ function sanitizeProjectList(result) {
|
|
|
789
871
|
...(typeof result.nextCursor === 'string' && result.nextCursor.length > 0 ? { nextCursor: result.nextCursor } : { nextCursor: null }),
|
|
790
872
|
};
|
|
791
873
|
}
|
|
792
|
-
function
|
|
874
|
+
function isThreadAllowedByWorkspaceAuthority(thread, authority) {
|
|
793
875
|
const projectId = typeof thread.projectId === 'string' ? thread.projectId : undefined;
|
|
794
876
|
if (projectId !== undefined && authority.projectIds.has(projectId))
|
|
795
877
|
return true;
|
|
@@ -809,6 +891,15 @@ function normalizeCodexPathForCompare(path) {
|
|
|
809
891
|
function extractThread(result) {
|
|
810
892
|
return isRecord(result) && isRecord(result.thread) ? result.thread : undefined;
|
|
811
893
|
}
|
|
894
|
+
function extractTurnId(result) {
|
|
895
|
+
if (!isRecord(result))
|
|
896
|
+
return undefined;
|
|
897
|
+
if (typeof result.turnId === 'string' && result.turnId.length > 0)
|
|
898
|
+
return result.turnId;
|
|
899
|
+
if (isRecord(result.turn) && typeof result.turn.id === 'string' && result.turn.id.length > 0)
|
|
900
|
+
return result.turn.id;
|
|
901
|
+
return undefined;
|
|
902
|
+
}
|
|
812
903
|
function extractThreadId(params) {
|
|
813
904
|
if (!isRecord(params))
|
|
814
905
|
return undefined;
|
|
@@ -823,15 +914,44 @@ function extractThreadId(params) {
|
|
|
823
914
|
function optionalInteger(value) {
|
|
824
915
|
return typeof value === 'number' && Number.isSafeInteger(value) ? value : undefined;
|
|
825
916
|
}
|
|
826
|
-
function codexPermission(params) {
|
|
917
|
+
function codexPermission(params, fallbackPreset) {
|
|
827
918
|
const { permissionPreset, ...rest } = params;
|
|
828
|
-
const
|
|
919
|
+
const preset = permissionPreset === 'workspace-write' || permissionPreset === 'danger-full-access'
|
|
920
|
+
? permissionPreset
|
|
921
|
+
: fallbackPreset;
|
|
922
|
+
if (preset === undefined)
|
|
923
|
+
return { params: mapCodexImageInputs(rest) };
|
|
924
|
+
const fullAccess = preset === 'danger-full-access';
|
|
829
925
|
return {
|
|
830
926
|
params: mapCodexImageInputs(rest),
|
|
831
927
|
approvalPolicy: fullAccess ? 'never' : 'on-request',
|
|
832
928
|
sandbox: fullAccess ? 'danger-full-access' : 'workspace-write',
|
|
833
929
|
};
|
|
834
930
|
}
|
|
931
|
+
function codexThreadPermissionParams(permission) {
|
|
932
|
+
if (permission.approvalPolicy === undefined || permission.sandbox === undefined)
|
|
933
|
+
return {};
|
|
934
|
+
return {
|
|
935
|
+
approvalPolicy: permission.approvalPolicy,
|
|
936
|
+
sandbox: permission.sandbox,
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
function codexTurnPermissionParams(permission, cwd) {
|
|
940
|
+
if (permission.approvalPolicy === undefined || permission.sandbox === undefined)
|
|
941
|
+
return {};
|
|
942
|
+
return {
|
|
943
|
+
approvalPolicy: permission.approvalPolicy,
|
|
944
|
+
sandboxPolicy: permission.sandbox === 'danger-full-access'
|
|
945
|
+
? { type: 'dangerFullAccess' }
|
|
946
|
+
: {
|
|
947
|
+
type: 'workspaceWrite',
|
|
948
|
+
writableRoots: cwd === undefined ? [] : [cwd],
|
|
949
|
+
networkAccess: false,
|
|
950
|
+
excludeTmpdirEnvVar: false,
|
|
951
|
+
excludeSlashTmp: false,
|
|
952
|
+
},
|
|
953
|
+
};
|
|
954
|
+
}
|
|
835
955
|
function mapCodexImageInputs(params) {
|
|
836
956
|
if (!Array.isArray(params.input))
|
|
837
957
|
return params;
|
|
@@ -875,6 +995,10 @@ function isHistoryReadRecoverable(error) {
|
|
|
875
995
|
return error instanceof RpcError
|
|
876
996
|
&& ['METHOD_NOT_ALLOWED', 'METHOD_NOT_FOUND', 'CODEX_UPSTREAM_ERROR', 'CODEX_REQUEST_TIMEOUT', 'CODEX_THREAD_BUSY'].includes(error.code);
|
|
877
997
|
}
|
|
998
|
+
function isProjectListFallbackError(error) {
|
|
999
|
+
return error instanceof RpcError
|
|
1000
|
+
&& ['METHOD_NOT_ALLOWED', 'METHOD_NOT_FOUND', 'CODEX_UPSTREAM_ERROR'].includes(error.code);
|
|
1001
|
+
}
|
|
878
1002
|
function canTryNextBinary(error) {
|
|
879
1003
|
return !(error instanceof RpcError) || !['CODEX_AUTH_REQUIRED', 'CODEX_CLOSED'].includes(error.code);
|
|
880
1004
|
}
|