@1yefuwang1/dsh-worktrees 0.0.0-stage → 0.2.13

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.
@@ -0,0 +1,125 @@
1
+ import { clientRequestSchema } from '@deepseek-ai/dsh-client-connection';
2
+ import { required } from './runtime.js';
3
+ /** Payloads retain their existing 1 MiB bound; allow only bounded wire-envelope headroom. */
4
+ const MAX_BODY_BYTES = 1048576 + 4096;
5
+ function failure(code, message) {
6
+ return { ok: false, error: { code, message, details: {} } };
7
+ }
8
+ function response(rpcId, result) {
9
+ return Response.json({ type: 'server-response', rpcId, result });
10
+ }
11
+ async function readBody(request, signal) {
12
+ if (signal.aborted)
13
+ throw new Error('cancelled');
14
+ const length = request.headers.get('content-length');
15
+ if (length !== null && (!/^\d+$/u.test(length) || Number(length) > MAX_BODY_BYTES))
16
+ throw new Error('limit');
17
+ if (request.body === null)
18
+ throw new Error('json');
19
+ const reader = request.body.getReader();
20
+ const chunks = [];
21
+ let size = 0;
22
+ let rejectAbort;
23
+ const aborted = new Promise((_resolve, reject) => { rejectAbort = reject; });
24
+ // Observe even a late abort after a completed read, without extending teardown.
25
+ void aborted.catch(() => undefined);
26
+ let cancellation;
27
+ const cancelReader = () => cancellation ??= reader.cancel().catch(() => undefined);
28
+ const abort = () => { rejectAbort(new Error('cancelled')); void cancelReader(); };
29
+ signal.addEventListener('abort', abort, { once: true });
30
+ try {
31
+ if (signal.aborted)
32
+ abort();
33
+ while (true) {
34
+ const part = await Promise.race([reader.read(), aborted]);
35
+ if (part.done)
36
+ break;
37
+ size += part.value.byteLength;
38
+ if (size > MAX_BODY_BYTES) {
39
+ void cancelReader();
40
+ throw new Error('limit');
41
+ }
42
+ chunks.push(part.value);
43
+ }
44
+ if (signal.aborted)
45
+ throw new Error('cancelled');
46
+ return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks, size)));
47
+ }
48
+ finally {
49
+ signal.removeEventListener('abort', abort);
50
+ if (cancellation !== undefined)
51
+ await cancellation;
52
+ reader.releaseLock();
53
+ }
54
+ }
55
+ /**
56
+ * A feature owns exact POST routes, never the singleton /api interceptor owned
57
+ * by the API gateway. Keep Connection's public envelope and admitted operator.
58
+ */
59
+ export function registerQuietRpcRoute(owner, endpoint, handler) {
60
+ const connection = required(owner, 'connection');
61
+ const path = `/api/${endpoint}`;
62
+ const shutdown = new AbortController();
63
+ const flights = new Set();
64
+ let closed = false;
65
+ let disposal;
66
+ const stop = owner.effect(() => {
67
+ const unregister = connection.fetch.register({ path, methods: ['POST'], requestBody: 'buffered', fetch: request => {
68
+ const task = (async () => {
69
+ // Recheck the public admission fence for direct Fetch carriers as well.
70
+ const admitted = connection.admit(request);
71
+ if ('rejection' in admitted)
72
+ return new Response(admitted.rejection === 401 ? 'unauthorized' : 'forbidden', { status: admitted.rejection });
73
+ if (admitted.peer.id !== connection.operator.id || admitted.peer.ctx !== connection.operator.ctx)
74
+ return response('invalid-request', failure('FORBIDDEN', 'This request does not belong to the authenticated operator.'));
75
+ if (closed)
76
+ return response('invalid-request', failure('CLOSED', 'The plugin UI route is closing.'));
77
+ if (request.method !== 'POST' || new URL(request.url).pathname !== path)
78
+ return new Response('not found', { status: 404 });
79
+ if (request.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase() !== 'application/json')
80
+ return new Response('content type must be application/json', { status: 415 });
81
+ const signal = AbortSignal.any([request.signal, shutdown.signal]);
82
+ if (signal.aborted)
83
+ return response('invalid-request', failure('CANCELLED', 'The plugin request was cancelled.'));
84
+ let body;
85
+ try {
86
+ body = await readBody(request, signal);
87
+ }
88
+ catch (error) {
89
+ if (signal.aborted)
90
+ return response('invalid-request', failure('CANCELLED', 'The plugin request was cancelled.'));
91
+ if (error instanceof Error && error.message === 'limit')
92
+ return new Response('request body exceeds its bound', { status: 413 });
93
+ return new Response('body is not JSON', { status: 400 });
94
+ }
95
+ const parsed = clientRequestSchema.safeParse(body);
96
+ const rawId = body !== null && typeof body === 'object' && 'rpcId' in body ? body.rpcId : undefined;
97
+ const rpcId = typeof rawId === 'string' && rawId.length <= 256 ? rawId : 'invalid-request';
98
+ if (!parsed.success || parsed.data.rpcId.length < 1 || parsed.data.rpcId.length > 256)
99
+ return response(rpcId, failure('gateway/bad-request', 'Invalid client-request envelope.'));
100
+ if (parsed.data.method !== endpoint)
101
+ return response(parsed.data.rpcId, failure('gateway/bad-request', 'The request method does not match this exact endpoint.'));
102
+ try {
103
+ return response(parsed.data.rpcId, await handler(endpoint, parsed.data.payload, signal, admitted.peer));
104
+ }
105
+ catch {
106
+ return response(parsed.data.rpcId, failure(signal.aborted ? 'CANCELLED' : 'OPERATION_FAILED', signal.aborted ? 'The plugin request was cancelled.' : 'The plugin request failed.'));
107
+ }
108
+ })();
109
+ flights.add(task);
110
+ void task.then(() => flights.delete(task), () => flights.delete(task));
111
+ return task;
112
+ } });
113
+ return () => disposal ??= (async () => {
114
+ closed = true;
115
+ shutdown.abort();
116
+ try {
117
+ await unregister();
118
+ }
119
+ finally {
120
+ await Promise.allSettled([...flights]);
121
+ }
122
+ })();
123
+ }, 'worktrees: exact authenticated RPC route');
124
+ return () => { const released = stop(); return disposal ??= Promise.resolve(released); };
125
+ }
package/dist/rpc.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { registerQuietRpcRoute } from './quiet-rpc.js';
3
+ import type { WorktreeController } from './service.js';
4
+ export declare const WORKTREE_RPC_ENDPOINT = "dsh-worktrees/execute";
5
+ export declare const WORKTREE_PREPARE_ENDPOINT = "dsh-worktrees/prepare";
6
+ export declare const WORKTREE_PROGRESS_ENDPOINT = "dsh-worktrees/progress";
7
+ /** Authenticated UI entry point; it invokes the shared controller, never a command or tool. */
8
+ export declare function registerWorktreeRpc(owner: Context, controller: Pick<WorktreeController, 'execute'>, registerRoute?: typeof registerQuietRpcRoute): () => Promise<void>;
9
+ /** Scope-owned lazy setup with bounded, event-driven observation; never admits a prompt. */
10
+ export declare function registerWorktreeProgressRpc(owner: Context, controller: Pick<WorktreeController, 'execute'>, registerRoute?: typeof registerQuietRpcRoute): () => Promise<void>;
package/dist/rpc.js ADDED
@@ -0,0 +1,418 @@
1
+ import { SessionId } from '@deepseek-ai/dsh-session';
2
+ import { abortIfRequested, failureOf, WorktreeError } from './errors.js';
3
+ import { required } from './runtime.js';
4
+ import { registerQuietRpcRoute } from './quiet-rpc.js';
5
+ import { hashValue, parseRequest } from './schema.js';
6
+ export const WORKTREE_RPC_ENDPOINT = 'dsh-worktrees/execute';
7
+ export const WORKTREE_PREPARE_ENDPOINT = 'dsh-worktrees/prepare';
8
+ export const WORKTREE_PROGRESS_ENDPOINT = 'dsh-worktrees/progress';
9
+ const JSON_LIMIT = 1048576;
10
+ const OPERATION_ID = /^([0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/iu;
11
+ /** Validate before encoding: no accessors, custom prototypes, coercion, omissions, or cycles. */
12
+ function jsonSnapshot(value, code) {
13
+ let bytes = 0;
14
+ let nodes = 0;
15
+ const seen = new Set();
16
+ const invalid = () => { throw new WorktreeError(code, code === 'RESPONSE_LIMIT' ? 'The result exceeds its JSON response bound. Narrow the query or inspect the operation by ID.' : 'A bounded, lossless JSON request is required.'); };
17
+ const charge = (amount) => { bytes += amount; if (bytes > JSON_LIMIT)
18
+ invalid(); };
19
+ const visit = (input, depth) => {
20
+ if (++nodes > 65536 || depth > 16)
21
+ invalid();
22
+ if (input === null || typeof input === 'boolean') {
23
+ charge(input === false ? 5 : 4);
24
+ return;
25
+ }
26
+ if (typeof input === 'string') {
27
+ if (Buffer.byteLength(input) > JSON_LIMIT)
28
+ invalid();
29
+ charge(Buffer.byteLength(JSON.stringify(input)));
30
+ return;
31
+ }
32
+ if (typeof input === 'number') {
33
+ if (!Number.isFinite(input) || Object.is(input, -0) || (Number.isInteger(input) && !Number.isSafeInteger(input)))
34
+ invalid();
35
+ charge(String(input).length);
36
+ return;
37
+ }
38
+ if (typeof input !== 'object' || input === null || seen.has(input))
39
+ invalid();
40
+ const object = input;
41
+ const array = Array.isArray(object);
42
+ const prototype = Object.getPrototypeOf(object);
43
+ if (array ? prototype !== Array.prototype : prototype !== Object.prototype && prototype !== null)
44
+ invalid();
45
+ seen.add(object);
46
+ charge(2);
47
+ const keys = Reflect.ownKeys(object);
48
+ if (keys.length > 65536)
49
+ invalid();
50
+ let elements = 0;
51
+ for (const key of keys) {
52
+ if (array && key === 'length')
53
+ continue;
54
+ if (typeof key !== 'string')
55
+ invalid();
56
+ const name = key;
57
+ const descriptor = Object.getOwnPropertyDescriptor(object, name);
58
+ if (descriptor === undefined || !descriptor.enumerable || !('value' in descriptor))
59
+ invalid();
60
+ if (array && name !== String(elements))
61
+ invalid();
62
+ if (elements++ > 0)
63
+ charge(1);
64
+ if (!array)
65
+ charge(Buffer.byteLength(JSON.stringify(name)) + 1);
66
+ visit(descriptor.value, depth + 1);
67
+ }
68
+ if (array && elements !== object.length)
69
+ invalid();
70
+ seen.delete(object);
71
+ };
72
+ visit(value, 0);
73
+ const text = JSON.stringify(value);
74
+ if (Buffer.byteLength(text) > JSON_LIMIT)
75
+ invalid();
76
+ return JSON.parse(text);
77
+ }
78
+ function parsePayload(payload) {
79
+ const value = jsonSnapshot(payload, 'INVALID_RPC_REQUEST');
80
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
81
+ throw new WorktreeError('INVALID_RPC_REQUEST', 'Expected a versioned worktree request.');
82
+ const input = value;
83
+ if (Object.keys(input).length !== 3 || !Object.hasOwn(input, 'v') || !Object.hasOwn(input, 'actorId') || !Object.hasOwn(input, 'request') || input.v !== 1 || typeof input.actorId !== 'string' || input.actorId.length < 1 || input.actorId.length > 256 || /[\u0000-\u001f\u007f]/u.test(input.actorId))
84
+ throw new WorktreeError('INVALID_RPC_REQUEST', 'Expected v:1, actorId, and request only.');
85
+ try {
86
+ return { actorId: input.actorId, request: parseRequest(input.request) };
87
+ }
88
+ catch {
89
+ throw new WorktreeError('INVALID_RPC_REQUEST', 'The worktree request is invalid.');
90
+ }
91
+ }
92
+ function transportFailure(code, message) {
93
+ return { ok: false, error: { code, message, details: {} } };
94
+ }
95
+ function operationFailure(error) {
96
+ // Only controller-owned failures are public. Never forward arbitrary provider/Git diagnostics.
97
+ if (error instanceof Error && error.name === 'AbortError')
98
+ return { v: 1, ok: false, error: failureOf(error) };
99
+ if (!(error instanceof WorktreeError))
100
+ return { v: 1, ok: false, error: { code: 'OPERATION_FAILED', message: 'The worktree operation failed.' } };
101
+ const failure = failureOf(error);
102
+ if (!/^[A-Z][A-Z0-9_]{0,63}$/u.test(failure.code))
103
+ return { v: 1, ok: false, error: { code: 'OPERATION_FAILED', message: 'The worktree operation failed.' } };
104
+ if (failure.code === 'GIT_FAILED')
105
+ failure.message = 'The Git operation failed; diagnostic output is not exposed by this channel.';
106
+ return { v: 1, ok: false, error: failure };
107
+ }
108
+ /** Authenticated UI entry point; it invokes the shared controller, never a command or tool. */
109
+ export function registerWorktreeRpc(owner, controller, registerRoute = registerQuietRpcRoute) {
110
+ const connection = required(owner, 'connection');
111
+ const resolver = required(owner, 'sessionController');
112
+ const shutdown = new AbortController();
113
+ const flights = new Set();
114
+ let closed = false;
115
+ let disposal;
116
+ const stop = owner.effect(() => {
117
+ const unregister = registerRoute(owner, WORKTREE_RPC_ENDPOINT, async (endpoint, payload, requestSignal, peer) => {
118
+ // Check the admitted operator before parsing input or resolving any Actor. Peer permissions are never used.
119
+ if (endpoint !== WORKTREE_RPC_ENDPOINT || peer.id !== connection.operator.id || peer.ctx !== connection.operator.ctx)
120
+ return transportFailure('FORBIDDEN', 'This request does not belong to the authenticated operator.');
121
+ if (closed)
122
+ return transportFailure('CLOSED', 'The worktree UI channel is closing.');
123
+ const signal = AbortSignal.any([requestSignal, shutdown.signal]);
124
+ const task = (async () => {
125
+ let input;
126
+ try {
127
+ abortIfRequested(signal);
128
+ input = parsePayload(payload);
129
+ }
130
+ catch {
131
+ return signal.aborted ? transportFailure('CANCELLED', 'The worktree request was cancelled.') : transportFailure('INVALID_RPC_REQUEST', 'The worktree request is not valid bounded JSON.');
132
+ }
133
+ let source;
134
+ try {
135
+ source = await resolver.resolveAgent(SessionId(input.actorId));
136
+ }
137
+ catch {
138
+ return signal.aborted ? transportFailure('CANCELLED', 'The worktree request was cancelled.') : transportFailure('SESSION_RESOLUTION_FAILED', 'The invoking session could not be resolved.');
139
+ }
140
+ if (signal.aborted)
141
+ return transportFailure('CANCELLED', 'The worktree request was cancelled.');
142
+ if ('error' in source) {
143
+ const codes = { 'session/not-found': 'The invoking session was not found.', 'session/agent-busy': 'The invoking session is busy.', 'session/writer-held': 'The invoking session is held by another writer.', 'gateway/internal': 'The invoking session could not be resolved.' };
144
+ return transportFailure(Object.hasOwn(codes, source.error.code) ? source.error.code : 'SESSION_RESOLUTION_FAILED', codes[source.error.code] ?? 'The invoking session could not be resolved.');
145
+ }
146
+ if (source.agent.id !== input.actorId)
147
+ return transportFailure('SESSION_CHANGED', 'The resolved session identity changed.');
148
+ let envelope;
149
+ try {
150
+ abortIfRequested(signal);
151
+ const data = await controller.execute(input.request, { agent: source.agent, origin: 'ui', signal });
152
+ abortIfRequested(signal);
153
+ envelope = { v: 1, ok: true, data };
154
+ }
155
+ catch (error) {
156
+ envelope = operationFailure(error);
157
+ }
158
+ try {
159
+ return { ok: true, value: jsonSnapshot(envelope, 'RESPONSE_LIMIT') };
160
+ }
161
+ catch {
162
+ return { ok: true, value: operationFailure(new WorktreeError('RESPONSE_LIMIT', 'The result is not bounded lossless JSON. Narrow the query or inspect the operation by ID.')) };
163
+ }
164
+ })();
165
+ flights.add(task);
166
+ try {
167
+ return await task;
168
+ }
169
+ finally {
170
+ flights.delete(task);
171
+ }
172
+ });
173
+ return () => disposal ??= (async () => {
174
+ closed = true;
175
+ shutdown.abort();
176
+ try {
177
+ await unregister();
178
+ }
179
+ finally {
180
+ await Promise.allSettled([...flights]);
181
+ }
182
+ })();
183
+ }, 'worktrees: authenticated UI RPC');
184
+ return () => {
185
+ const released = stop();
186
+ return disposal ??= Promise.resolve(released);
187
+ };
188
+ }
189
+ const SETUP_STAGES = ['fetching', 'creating', 'naming', 'opening', 'ready'];
190
+ const ACTIVE_LIMIT = 64;
191
+ const WAITER_LIMIT = 128;
192
+ const TERMINAL_LIMIT = 128;
193
+ function parseProgressPayload(payload) {
194
+ const value = jsonSnapshot(payload, 'INVALID_RPC_REQUEST');
195
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
196
+ throw new WorktreeError('INVALID_RPC_REQUEST', 'Expected a versioned progress request.');
197
+ const input = value;
198
+ if (Object.keys(input).length !== 4 || !['v', 'actorId', 'operationId', 'after'].every(key => Object.hasOwn(input, key)) || input.v !== 1 || typeof input.actorId !== 'string' || input.actorId.length < 1 || input.actorId.length > 256 || /[\u0000-\u001f\u007f]/u.test(input.actorId) || typeof input.operationId !== 'string' || !OPERATION_ID.test(input.operationId) || typeof input.after !== 'number' || !Number.isSafeInteger(input.after) || input.after < 0)
199
+ throw new WorktreeError('INVALID_RPC_REQUEST', 'Expected v:1, actorId, operationId UUID, and a nonnegative safe after cursor only.');
200
+ return { actorId: input.actorId, operationId: input.operationId, after: input.after };
201
+ }
202
+ function detachReply(result) {
203
+ return result.ok ? { ok: true, value: jsonSnapshot(result.value, 'RESPONSE_LIMIT') } : { ok: false, error: { ...result.error, details: {} } };
204
+ }
205
+ function snapshotReply(envelope) {
206
+ try {
207
+ return { ok: true, value: jsonSnapshot(envelope, 'RESPONSE_LIMIT') };
208
+ }
209
+ catch {
210
+ return { ok: true, value: operationFailure(new WorktreeError('RESPONSE_LIMIT', 'The result is not bounded lossless JSON. Narrow the query or inspect the operation by ID.')) };
211
+ }
212
+ }
213
+ /** Scope-owned lazy setup with bounded, event-driven observation; never admits a prompt. */
214
+ export function registerWorktreeProgressRpc(owner, controller, registerRoute = registerQuietRpcRoute) {
215
+ const connection = required(owner, 'connection');
216
+ const resolver = required(owner, 'sessionController');
217
+ const shutdown = new AbortController();
218
+ const operations = new Map();
219
+ const terminals = [];
220
+ const waiters = new Set();
221
+ const flights = new Set();
222
+ let active = 0;
223
+ let receiptWaiters = 0;
224
+ let closed = false;
225
+ let disposal;
226
+ const cancelled = () => transportFailure('CANCELLED', 'The worktree request was cancelled.');
227
+ const forbidden = () => transportFailure('FORBIDDEN', 'This operation does not belong to the invoking session.');
228
+ const progressReply = (operation) => snapshotReply({ v: 1, ok: true, data: { operationId: operation.operationId, revision: operation.revision, stages: operation.stages, terminal: operation.terminal } });
229
+ const notify = (operation) => {
230
+ for (const waiter of [...waiters]) {
231
+ if (waiter.operationId !== operation.operationId)
232
+ continue;
233
+ if (waiter.actorId !== operation.actorId)
234
+ waiter.finish(forbidden());
235
+ else if (operation.revision > waiter.after || operation.terminal)
236
+ waiter.finish(progressReply(operation));
237
+ }
238
+ };
239
+ const publish = (operation, progress) => {
240
+ // An observer cannot authorize, mutate effects, or expose arbitrary provider/task fields.
241
+ try {
242
+ const value = jsonSnapshot(progress, 'INVALID_RPC_REQUEST');
243
+ if (operation.terminal || shutdown.signal.aborted || value === null || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !['stage', 'operationId', 'worktreeId'].includes(key)) || value.operationId !== operation.operationId)
244
+ return;
245
+ const index = SETUP_STAGES.indexOf(value.stage);
246
+ const last = operation.stages.at(-1);
247
+ if (index < 0 || (last !== undefined && index <= SETUP_STAGES.indexOf(last.stage)))
248
+ return;
249
+ if (value.worktreeId !== undefined && (typeof value.worktreeId !== 'string' || !OPERATION_ID.test(value.worktreeId)))
250
+ return;
251
+ const boundWorktree = operation.stages.find(stage => stage.worktreeId !== undefined)?.worktreeId;
252
+ if (value.worktreeId !== undefined && boundWorktree !== undefined && value.worktreeId !== boundWorktree)
253
+ return;
254
+ operation.stages.push(Object.freeze({ stage: value.stage, operationId: operation.operationId, ...(value.worktreeId === undefined ? {} : { worktreeId: value.worktreeId }) }));
255
+ operation.revision++;
256
+ notify(operation);
257
+ }
258
+ catch { /* Observation failure cannot change the shared operation's effects. */ }
259
+ };
260
+ const execute = async (input, signal, operation) => {
261
+ try {
262
+ abortIfRequested(signal);
263
+ let source;
264
+ try {
265
+ source = await resolver.resolveAgent(SessionId(input.actorId));
266
+ }
267
+ catch {
268
+ return signal.aborted ? cancelled() : transportFailure('SESSION_RESOLUTION_FAILED', 'The invoking session could not be resolved.');
269
+ }
270
+ if (signal.aborted)
271
+ return cancelled();
272
+ if ('error' in source) {
273
+ const codes = { 'session/not-found': 'The invoking session was not found.', 'session/agent-busy': 'The invoking session is busy.', 'session/writer-held': 'The invoking session is held by another writer.', 'gateway/internal': 'The invoking session could not be resolved.' };
274
+ return transportFailure(Object.hasOwn(codes, source.error.code) ? source.error.code : 'SESSION_RESOLUTION_FAILED', codes[source.error.code] ?? 'The invoking session could not be resolved.');
275
+ }
276
+ if (source.agent.id !== input.actorId)
277
+ return transportFailure('SESSION_CHANGED', 'The resolved session identity changed.');
278
+ let envelope;
279
+ try {
280
+ abortIfRequested(signal);
281
+ const data = await controller.execute(input.request, { agent: source.agent, origin: 'ui', signal, onProgress: progress => publish(operation, progress) });
282
+ abortIfRequested(signal);
283
+ envelope = { v: 1, ok: true, data };
284
+ }
285
+ catch (error) {
286
+ envelope = operationFailure(error);
287
+ }
288
+ return snapshotReply(envelope);
289
+ }
290
+ catch (error) {
291
+ return snapshotReply(operationFailure(error));
292
+ }
293
+ finally {
294
+ // Terminal is a separate revision, including failures before the first phase.
295
+ operation.terminal = true;
296
+ operation.revision++;
297
+ active--;
298
+ notify(operation);
299
+ terminals.push(operation.operationId);
300
+ if (terminals.length > TERMINAL_LIMIT)
301
+ operations.delete(terminals.shift());
302
+ }
303
+ };
304
+ const awaitReceipt = (operation, signal) => {
305
+ if (operation.receipt !== undefined)
306
+ return Promise.resolve(signal.aborted ? cancelled() : detachReply(operation.receipt));
307
+ if (waiters.size + receiptWaiters >= WAITER_LIMIT)
308
+ return Promise.resolve(transportFailure('WAITER_LIMIT', 'The worktree wait capacity is exhausted.'));
309
+ receiptWaiters++;
310
+ return new Promise(resolve => {
311
+ const finish = (result) => {
312
+ if (!operation.receiptWaiters.delete(finish))
313
+ return;
314
+ receiptWaiters--;
315
+ signal.removeEventListener('abort', abort);
316
+ resolve(detachReply(result));
317
+ };
318
+ const abort = () => finish(cancelled());
319
+ // Removable subscriptions avoid retaining cancelled duplicates on the execution Promise.
320
+ operation.receiptWaiters.add(finish);
321
+ signal.addEventListener('abort', abort, { once: true });
322
+ if (signal.aborted)
323
+ abort();
324
+ });
325
+ };
326
+ const handle = (endpoint, payload, requestSignal, peer) => {
327
+ if (![WORKTREE_PREPARE_ENDPOINT, WORKTREE_PROGRESS_ENDPOINT].includes(endpoint) || peer.id !== connection.operator.id || peer.ctx !== connection.operator.ctx)
328
+ return Promise.resolve(transportFailure('FORBIDDEN', 'This request does not belong to the authenticated operator.'));
329
+ if (closed)
330
+ return Promise.resolve(transportFailure('CLOSED', 'The worktree UI channel is closing.'));
331
+ const signal = AbortSignal.any([requestSignal, shutdown.signal]);
332
+ const task = (async () => {
333
+ if (endpoint === WORKTREE_PREPARE_ENDPOINT) {
334
+ let input;
335
+ try {
336
+ abortIfRequested(signal);
337
+ input = parsePayload(payload);
338
+ const request = input.request;
339
+ if (request.action !== 'create' || typeof request.firstPrompt !== 'string' || request.sessionMode !== 'new' || request.requireBlankSource !== true || request.sourceSessionId !== input.actorId)
340
+ throw new WorktreeError('INVALID_RPC_REQUEST', 'Prepare accepts only lazy first-Send create requests.');
341
+ }
342
+ catch {
343
+ return signal.aborted ? cancelled() : transportFailure('INVALID_RPC_REQUEST', 'The worktree prepare request is not valid bounded JSON.');
344
+ }
345
+ const operationId = input.request.operationId;
346
+ const requestHash = hashValue(input.request);
347
+ const existing = operations.get(operationId);
348
+ if (existing !== undefined) {
349
+ if (existing.actorId !== input.actorId)
350
+ return forbidden();
351
+ if (existing.requestHash !== requestHash)
352
+ return transportFailure('OPERATION_CONFLICT', 'The operation ID is already bound to a different request.');
353
+ return awaitReceipt(existing, signal);
354
+ }
355
+ if (active >= ACTIVE_LIMIT)
356
+ return transportFailure('OPERATION_LIMIT', 'The worktree setup capacity is exhausted.');
357
+ // Bind actor and detached immutable request before any lookup or controller effect.
358
+ Object.freeze(input.request);
359
+ const operation = { actorId: input.actorId, requestHash, operationId, revision: 0, stages: [], terminal: false, receiptWaiters: new Set(), task: Promise.resolve(transportFailure('OPERATION_FAILED', 'The worktree operation failed.')) };
360
+ active++;
361
+ operations.set(operationId, operation);
362
+ notify(operation);
363
+ operation.task = Promise.resolve().then(() => execute(input, signal, operation)).then(result => {
364
+ operation.receipt = result;
365
+ for (const finish of [...operation.receiptWaiters])
366
+ finish(result);
367
+ return result;
368
+ });
369
+ return operation.task.then(detachReply);
370
+ }
371
+ let input;
372
+ try {
373
+ abortIfRequested(signal);
374
+ input = parseProgressPayload(payload);
375
+ }
376
+ catch {
377
+ return signal.aborted ? cancelled() : transportFailure('INVALID_RPC_REQUEST', 'The worktree progress request is not valid bounded JSON.');
378
+ }
379
+ const operation = operations.get(input.operationId);
380
+ if (operation !== undefined) {
381
+ if (operation.actorId !== input.actorId)
382
+ return forbidden();
383
+ if (operation.revision > input.after || operation.terminal)
384
+ return progressReply(operation);
385
+ }
386
+ if (waiters.size + receiptWaiters >= WAITER_LIMIT)
387
+ return transportFailure('WAITER_LIMIT', 'The worktree wait capacity is exhausted.');
388
+ return new Promise(resolve => {
389
+ const waiter = { ...input, finish: result => { if (!waiters.delete(waiter))
390
+ return; signal.removeEventListener('abort', abort); resolve(result); } };
391
+ const abort = () => waiter.finish(cancelled());
392
+ waiters.add(waiter);
393
+ signal.addEventListener('abort', abort, { once: true });
394
+ if (signal.aborted)
395
+ abort();
396
+ });
397
+ })();
398
+ flights.add(task);
399
+ void task.then(() => flights.delete(task), () => flights.delete(task));
400
+ return task;
401
+ };
402
+ const stop = owner.effect(() => {
403
+ const unregister = [registerRoute(owner, WORKTREE_PREPARE_ENDPOINT, handle), registerRoute(owner, WORKTREE_PROGRESS_ENDPOINT, handle)];
404
+ return () => disposal ??= (async () => {
405
+ closed = true;
406
+ shutdown.abort();
407
+ try {
408
+ await Promise.allSettled(unregister.map(release => release()));
409
+ }
410
+ finally {
411
+ await Promise.allSettled([...flights]);
412
+ operations.clear();
413
+ terminals.length = 0;
414
+ }
415
+ })();
416
+ }, 'worktrees: bounded authenticated setup progress RPC');
417
+ return () => { const released = stop(); return disposal ??= Promise.resolve(released); };
418
+ }
@@ -0,0 +1,22 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { Agent } from '@deepseek-ai/dsh-agent';
3
+ import type { GitExecutor, WorktreeConfig } from './types.js';
4
+ export interface ExecutionPolicy {
5
+ mode: 'read-only' | 'workspace-write' | 'danger-full-access';
6
+ workspaceRoot: string;
7
+ sessionId?: string;
8
+ }
9
+ export interface PlanCapability {
10
+ get(agent: Agent): {
11
+ active: boolean;
12
+ pending?: boolean;
13
+ };
14
+ }
15
+ export declare function capability<T>(ctx: Context, name: string): T | undefined;
16
+ export declare function required<T>(ctx: Context, name: string): T;
17
+ export declare function policyOf(agent: Agent): ExecutionPolicy;
18
+ export declare function requireFullAccess(agent: Agent): void;
19
+ export declare function planBlocksMutation(agent: Agent): boolean;
20
+ export declare function localProjectPath(agent: Agent, requested?: string, signal?: AbortSignal): Promise<string>;
21
+ /** No native spawn fallback: every production Git command uses the caller's managed capabilities. */
22
+ export declare function createExecutor(agent: Agent, config: WorktreeConfig, shutdown?: AbortSignal): GitExecutor;