@linzumi/cli 0.0.43-beta → 0.0.44-beta
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 +1 -1
- package/dist/index.js +370 -21
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -2671,6 +2671,7 @@ function initialChannelSessionState(cursor, rootSeq, kandanThreadId, codexThread
|
|
|
2671
2671
|
pendingApprovalRequests: new Map,
|
|
2672
2672
|
approvalPromptChain: Promise.resolve(),
|
|
2673
2673
|
pendingPortForwardRequests: new Map,
|
|
2674
|
+
portForwardPreviousProcessingStates: new Map,
|
|
2674
2675
|
queuedPortForwardCandidates: new Map,
|
|
2675
2676
|
approvedForwardPorts: new Set,
|
|
2676
2677
|
approvedForwardTargets: new Map,
|
|
@@ -2995,6 +2996,9 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
|
|
|
2995
2996
|
state.pendingPortForwardRequests.delete(control.requestId);
|
|
2996
2997
|
if (control.decision === "deny") {
|
|
2997
2998
|
state.dismissedForwardTargets.set(request.port, approvedTargetFromRequest(request));
|
|
2999
|
+
await publishForwardPortResolvedEvent(args, request, {
|
|
3000
|
+
decision: "deny"
|
|
3001
|
+
});
|
|
2998
3002
|
await publishMessageStateForPortForwardResult(args, state, request, "failed");
|
|
2999
3003
|
args.log("port_forward.request_denied", {
|
|
3000
3004
|
request_id: control.requestId,
|
|
@@ -3014,7 +3018,10 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
|
|
|
3014
3018
|
processName: processIdentity?.appName ?? null,
|
|
3015
3019
|
processIconKey: processIdentity?.iconKey ?? null
|
|
3016
3020
|
});
|
|
3017
|
-
await publishForwardPortResolvedEvent(args, request,
|
|
3021
|
+
await publishForwardPortResolvedEvent(args, request, {
|
|
3022
|
+
decision: "approve",
|
|
3023
|
+
capabilities
|
|
3024
|
+
});
|
|
3018
3025
|
await publishMessageStateForPortForwardResult(args, state, request, "processed");
|
|
3019
3026
|
await publishPortForwardReadyMessage(args, state, payloadContext, request);
|
|
3020
3027
|
args.log("port_forward.request_approved", {
|
|
@@ -3099,12 +3106,14 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
|
|
|
3099
3106
|
candidate
|
|
3100
3107
|
});
|
|
3101
3108
|
state.pendingPortForwardRequests.set(requestId, request);
|
|
3109
|
+
if (state.activeProcessingState !== undefined) {
|
|
3110
|
+
state.portForwardPreviousProcessingStates.set(requestId, state.activeProcessingState);
|
|
3111
|
+
}
|
|
3102
3112
|
const processIdentity = guessCanonicalProcessFromCommand(candidate.command);
|
|
3103
3113
|
const processIconPath = processIdentity?.iconKey === undefined ? undefined : `/web/process-icons/${processIdentity.iconKey}.png`;
|
|
3104
3114
|
const processName = processIdentity?.appName ?? label;
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
status: "processing",
|
|
3115
|
+
state.activeProcessingState = {
|
|
3116
|
+
seq: sourceSeq,
|
|
3108
3117
|
reason: "awaiting approval",
|
|
3109
3118
|
approval: {
|
|
3110
3119
|
requestId,
|
|
@@ -3133,7 +3142,13 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
|
|
|
3133
3142
|
allowedActorSlug: args.options.channelSession.listenUser,
|
|
3134
3143
|
allowedActorUserId: args.options.channelSession.listenUser.toLowerCase() === (payloadContext.runnerIdentity.actorUsername ?? "").toLowerCase() ? payloadContext.runnerIdentity.actorUserId : undefined
|
|
3135
3144
|
}
|
|
3145
|
+
};
|
|
3146
|
+
await publishMessageState(args, state.kandanThreadId, sourceSeq, {
|
|
3147
|
+
status: "processing",
|
|
3148
|
+
reason: "awaiting approval",
|
|
3149
|
+
approval: state.activeProcessingState.approval
|
|
3136
3150
|
});
|
|
3151
|
+
await publishForwardPortRequestedEvent(args, state, request, processIdentity);
|
|
3137
3152
|
args.log("port_forward.request_pending", {
|
|
3138
3153
|
request_id: requestId,
|
|
3139
3154
|
port: candidate.port,
|
|
@@ -3174,6 +3189,10 @@ async function expireLostPendingPortForwardRequest(args, state, payloadContext,
|
|
|
3174
3189
|
return;
|
|
3175
3190
|
}
|
|
3176
3191
|
state.pendingPortForwardRequests.delete(request.requestId);
|
|
3192
|
+
await publishForwardPortResolvedEvent(args, request, {
|
|
3193
|
+
decision: "expired",
|
|
3194
|
+
reason: "listener_exited"
|
|
3195
|
+
});
|
|
3177
3196
|
await publishMessageStateForPortForwardResult(args, state, request, "failed", "port_forward_listener_exited");
|
|
3178
3197
|
args.log("port_forward.pending_request_expired", {
|
|
3179
3198
|
request_id: request.requestId,
|
|
@@ -3220,17 +3239,23 @@ async function publishPortForwardReadyMessage(args, state, payloadContext, reque
|
|
|
3220
3239
|
}
|
|
3221
3240
|
}, args.log);
|
|
3222
3241
|
}
|
|
3223
|
-
async function publishForwardPortRequestedEvent(args, request) {
|
|
3242
|
+
async function publishForwardPortRequestedEvent(args, state, request, processIdentity) {
|
|
3224
3243
|
await pushOptional(args.kandan, args.topic, "forward_port_requested", {
|
|
3225
3244
|
instanceId: args.instanceId,
|
|
3226
3245
|
requestId: request.requestId,
|
|
3246
|
+
sourceSeq: request.sourceSeq,
|
|
3227
3247
|
port: request.port,
|
|
3228
3248
|
pid: request.pid,
|
|
3229
3249
|
command: request.command,
|
|
3250
|
+
codexThreadId: state.codexThreadId ?? null,
|
|
3251
|
+
kandanThreadId: state.kandanThreadId ?? null,
|
|
3252
|
+
channelSlug: args.options.channelSession.channelSlug ?? null,
|
|
3253
|
+
...processIdentity?.appName === undefined ? {} : { processName: processIdentity.appName },
|
|
3254
|
+
...processIdentity?.iconKey === undefined ? {} : { processIconKey: processIdentity.iconKey },
|
|
3230
3255
|
...request.cwd === undefined ? {} : { cwd: request.cwd }
|
|
3231
3256
|
}, args.log);
|
|
3232
3257
|
}
|
|
3233
|
-
async function publishForwardPortResolvedEvent(args, request,
|
|
3258
|
+
async function publishForwardPortResolvedEvent(args, request, result) {
|
|
3234
3259
|
await pushOptional(args.kandan, args.topic, "forward_port_resolved", {
|
|
3235
3260
|
instanceId: args.instanceId,
|
|
3236
3261
|
requestId: request.requestId,
|
|
@@ -3238,18 +3263,52 @@ async function publishForwardPortResolvedEvent(args, request, capabilities) {
|
|
|
3238
3263
|
pid: request.pid,
|
|
3239
3264
|
command: request.command,
|
|
3240
3265
|
...request.cwd === undefined ? {} : { cwd: request.cwd },
|
|
3241
|
-
|
|
3242
|
-
|
|
3266
|
+
sourceSeq: request.sourceSeq,
|
|
3267
|
+
decision: result.decision,
|
|
3268
|
+
...result.reason === undefined ? {} : { reason: result.reason },
|
|
3269
|
+
...result.capabilities === undefined ? {} : { capabilities: result.capabilities }
|
|
3243
3270
|
}, args.log);
|
|
3244
3271
|
}
|
|
3245
3272
|
async function publishMessageStateForPortForwardResult(args, state, request, status, failedReason = "port_forward_denied") {
|
|
3246
3273
|
if (state.kandanThreadId === undefined) {
|
|
3247
3274
|
return;
|
|
3248
3275
|
}
|
|
3276
|
+
const previousProcessingState = state.portForwardPreviousProcessingStates.get(request.requestId);
|
|
3277
|
+
state.portForwardPreviousProcessingStates.delete(request.requestId);
|
|
3249
3278
|
const activeProcessingState = state.activeProcessingState;
|
|
3250
3279
|
if (activeProcessingState !== undefined && activeProcessingState.seq === request.sourceSeq) {
|
|
3251
|
-
|
|
3252
|
-
|
|
3280
|
+
const resolvingActiveApproval = activeProcessingState.reason === "awaiting approval" && activeProcessingState.approval.requestId === request.requestId;
|
|
3281
|
+
if (resolvingActiveApproval) {
|
|
3282
|
+
state.activeProcessingState = undefined;
|
|
3283
|
+
if (previousProcessingState !== undefined && previousProcessingState.seq === request.sourceSeq) {
|
|
3284
|
+
state.activeProcessingState = previousProcessingState;
|
|
3285
|
+
await publishMessageState(args, state.kandanThreadId, request.sourceSeq, processingMessageStateFromActive(previousProcessingState), undefined, undefined, state.codexThreadId);
|
|
3286
|
+
return;
|
|
3287
|
+
}
|
|
3288
|
+
switch (state.turn.status) {
|
|
3289
|
+
case "active":
|
|
3290
|
+
case "completing":
|
|
3291
|
+
if (state.turn.queuedSeq === request.sourceSeq) {
|
|
3292
|
+
state.activeProcessingState = {
|
|
3293
|
+
seq: request.sourceSeq,
|
|
3294
|
+
reason: "running terminal command"
|
|
3295
|
+
};
|
|
3296
|
+
await publishMessageState(args, state.kandanThreadId, request.sourceSeq, {
|
|
3297
|
+
status: "processing",
|
|
3298
|
+
reason: "running terminal command"
|
|
3299
|
+
});
|
|
3300
|
+
return;
|
|
3301
|
+
}
|
|
3302
|
+
break;
|
|
3303
|
+
case "idle":
|
|
3304
|
+
case "starting":
|
|
3305
|
+
break;
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
if (!resolvingActiveApproval) {
|
|
3309
|
+
await publishMessageState(args, state.kandanThreadId, request.sourceSeq, processingMessageStateFromActive(activeProcessingState), undefined, undefined, state.codexThreadId);
|
|
3310
|
+
return;
|
|
3311
|
+
}
|
|
3253
3312
|
}
|
|
3254
3313
|
switch (status) {
|
|
3255
3314
|
case "processed":
|
|
@@ -3419,7 +3478,7 @@ function kandanMessageClaimKey(args, threadId, seq) {
|
|
|
3419
3478
|
function claimKandanMessage(args, state, event) {
|
|
3420
3479
|
const threadId = event.threadId;
|
|
3421
3480
|
if (threadId === undefined) {
|
|
3422
|
-
return
|
|
3481
|
+
return true;
|
|
3423
3482
|
}
|
|
3424
3483
|
const key = kandanMessageClaimKey(args, threadId, event.seq);
|
|
3425
3484
|
if (claimedKandanMessageKeys.has(key)) {
|
|
@@ -3428,8 +3487,9 @@ function claimKandanMessage(args, state, event) {
|
|
|
3428
3487
|
claimedKandanMessageKeys.add(key);
|
|
3429
3488
|
state.claimedKandanMessageKeys.add(key);
|
|
3430
3489
|
if (state.claimedKandanMessageKeys.size > maxClaimedKandanMessageKeys) {
|
|
3431
|
-
const
|
|
3432
|
-
|
|
3490
|
+
const overflow = state.claimedKandanMessageKeys.size - maxClaimedKandanMessageKeys;
|
|
3491
|
+
const expiredKeys = Array.from(state.claimedKandanMessageKeys).slice(0, overflow);
|
|
3492
|
+
for (const expiredKey of expiredKeys) {
|
|
3433
3493
|
state.claimedKandanMessageKeys.delete(expiredKey);
|
|
3434
3494
|
claimedKandanMessageKeys.delete(expiredKey);
|
|
3435
3495
|
}
|
|
@@ -3604,7 +3664,16 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
|
|
|
3604
3664
|
new_codex_thread_id: newCodexThreadId
|
|
3605
3665
|
});
|
|
3606
3666
|
await postCodexThreadReboundMessage(args, state, payloadContext, oldCodexThreadId, newCodexThreadId);
|
|
3607
|
-
|
|
3667
|
+
try {
|
|
3668
|
+
state.pendingReconnectContextInjection = await fetchReconnectContextInjection(args, state);
|
|
3669
|
+
} catch (contextError) {
|
|
3670
|
+
state.pendingReconnectContextInjection = undefined;
|
|
3671
|
+
args.log("codex.thread_reconnect_context_failed", {
|
|
3672
|
+
kandan_thread_id: state.kandanThreadId,
|
|
3673
|
+
codex_thread_id: newCodexThreadId,
|
|
3674
|
+
message: contextError instanceof Error ? contextError.message : String(contextError)
|
|
3675
|
+
});
|
|
3676
|
+
}
|
|
3608
3677
|
requeuePendingKandanMessageFront(state.queue, next);
|
|
3609
3678
|
state.turn = { status: "idle" };
|
|
3610
3679
|
await drainKandanMessageQueue(args, state, payloadContext);
|
|
@@ -4963,6 +5032,9 @@ async function refreshActiveProcessingState(args, state, turnId, reason) {
|
|
|
4963
5032
|
if (state.activeProcessingState?.seq === seq && state.activeProcessingState.reason === reason) {
|
|
4964
5033
|
return;
|
|
4965
5034
|
}
|
|
5035
|
+
if (state.activeProcessingState?.seq === seq && state.activeProcessingState.reason === "awaiting approval") {
|
|
5036
|
+
return;
|
|
5037
|
+
}
|
|
4966
5038
|
state.activeProcessingState = { seq, reason };
|
|
4967
5039
|
await publishMessageState(args, state.kandanThreadId, seq, {
|
|
4968
5040
|
status: "processing",
|
|
@@ -4974,6 +5046,14 @@ async function refreshActiveProcessingHeartbeat(args, state) {
|
|
|
4974
5046
|
if (activeProcessingState === undefined || state.kandanThreadId === undefined) {
|
|
4975
5047
|
return;
|
|
4976
5048
|
}
|
|
5049
|
+
if (activeProcessingState.reason === "awaiting approval") {
|
|
5050
|
+
switch (activeProcessingState.approval.kind) {
|
|
5051
|
+
case "local_runner_port_forward":
|
|
5052
|
+
return;
|
|
5053
|
+
default:
|
|
5054
|
+
break;
|
|
5055
|
+
}
|
|
5056
|
+
}
|
|
4977
5057
|
await publishMessageState(args, state.kandanThreadId, activeProcessingState.seq, processingMessageStateFromActive(activeProcessingState), undefined, undefined, state.codexThreadId);
|
|
4978
5058
|
}
|
|
4979
5059
|
function clearActiveProcessingState(state, seq) {
|
|
@@ -8183,7 +8263,7 @@ function realpathOrResolved(pathValue) {
|
|
|
8183
8263
|
}
|
|
8184
8264
|
|
|
8185
8265
|
// src/version.ts
|
|
8186
|
-
var linzumiCliVersion = "0.0.
|
|
8266
|
+
var linzumiCliVersion = "0.0.44-beta";
|
|
8187
8267
|
var linzumiCliVersionText = `linzumi ${linzumiCliVersion}`;
|
|
8188
8268
|
|
|
8189
8269
|
// src/runnerLock.ts
|
|
@@ -8552,6 +8632,8 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
8552
8632
|
const allowedForwardPorts = options.allowedForwardPorts ?? [];
|
|
8553
8633
|
const liveForwardPorts = new Set(allowedForwardPorts);
|
|
8554
8634
|
const managedForwardPorts = new Set;
|
|
8635
|
+
const kandanControlPort = explicitUrlPort(options.kandanUrl);
|
|
8636
|
+
const suppressedForwardPorts = () => suppressedForwardPortsForRunner(kandanControlPort, managedForwardPorts);
|
|
8555
8637
|
const forwardPortAttributions = new Map;
|
|
8556
8638
|
const setForwardPortAttribution = (port, attribution) => {
|
|
8557
8639
|
forwardPortAttributions.set(port, {
|
|
@@ -8559,7 +8641,8 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
8559
8641
|
codexThreadId: attribution.codexThreadId ?? null,
|
|
8560
8642
|
channelSlug: attribution.channelSlug ?? null,
|
|
8561
8643
|
processName: attribution.processName ?? null,
|
|
8562
|
-
processIconKey: attribution.processIconKey ?? null
|
|
8644
|
+
processIconKey: attribution.processIconKey ?? null,
|
|
8645
|
+
localEditor: attribution.localEditor === true
|
|
8563
8646
|
});
|
|
8564
8647
|
};
|
|
8565
8648
|
const clearForwardPortAttribution = (port) => {
|
|
@@ -8573,7 +8656,8 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
8573
8656
|
codexThreadId: attribution?.codexThreadId ?? null,
|
|
8574
8657
|
channelSlug: attribution?.channelSlug ?? null,
|
|
8575
8658
|
processName: attribution?.processName ?? null,
|
|
8576
|
-
processIconKey: attribution?.processIconKey ?? null
|
|
8659
|
+
processIconKey: attribution?.processIconKey ?? null,
|
|
8660
|
+
localEditor: attribution?.localEditor === true
|
|
8577
8661
|
};
|
|
8578
8662
|
});
|
|
8579
8663
|
const allowedCwds = { value: [...options.allowedCwds] };
|
|
@@ -8584,11 +8668,35 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
8584
8668
|
value: { status: "disabled" }
|
|
8585
8669
|
};
|
|
8586
8670
|
const localEditorGeneration = { value: 0 };
|
|
8671
|
+
const localEditorForwardState = {
|
|
8672
|
+
watcher: undefined,
|
|
8673
|
+
approvedPorts: new Set,
|
|
8674
|
+
approvedTargets: new Map,
|
|
8675
|
+
dismissedTargets: new Map,
|
|
8676
|
+
pendingRequests: new Map,
|
|
8677
|
+
queuedCandidates: new Map
|
|
8678
|
+
};
|
|
8587
8679
|
cleanup.actions.push(() => {
|
|
8588
8680
|
if (localEditorState.value.status === "running") {
|
|
8589
8681
|
localEditorState.value.process.kill("SIGINT");
|
|
8590
8682
|
localEditorState.value.collaboration?.process.kill("SIGINT");
|
|
8591
8683
|
}
|
|
8684
|
+
localEditorForwardState.watcher?.close();
|
|
8685
|
+
});
|
|
8686
|
+
const pendingEditorForwardPortRequests = () => Array.from(localEditorForwardState.pendingRequests.values()).sort((left, right) => left.port - right.port).map((request) => {
|
|
8687
|
+
const processIdentity = guessCanonicalProcessFromCommand(request.command);
|
|
8688
|
+
const label = processIdentity?.appName ?? portForwardPromptLabel(approvedTargetFromRequest(request));
|
|
8689
|
+
return {
|
|
8690
|
+
requestId: request.requestId,
|
|
8691
|
+
port: request.port,
|
|
8692
|
+
pid: request.pid,
|
|
8693
|
+
command: request.command,
|
|
8694
|
+
...request.cwd === undefined ? {} : { cwd: request.cwd },
|
|
8695
|
+
summary: `Make ${label} on port ${request.port} accessible on Linzumi?`,
|
|
8696
|
+
reason: portForwardPromptReason(approvedTargetFromRequest(request)),
|
|
8697
|
+
...processIdentity?.appName === undefined ? {} : { processName: processIdentity.appName },
|
|
8698
|
+
...processIdentity?.iconKey === undefined ? {} : { processIconKey: processIdentity.iconKey }
|
|
8699
|
+
};
|
|
8592
8700
|
});
|
|
8593
8701
|
const capabilitiesPayload = () => ({
|
|
8594
8702
|
codexAppServer: true,
|
|
@@ -8600,6 +8708,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
8600
8708
|
portForwarding: liveForwardPorts.size > 0,
|
|
8601
8709
|
allowedPorts: Array.from(liveForwardPorts).sort((left, right) => left - right),
|
|
8602
8710
|
forwardedPortAttributions: buildForwardPortAttributionPayload(),
|
|
8711
|
+
pendingEditorForwardPortRequests: pendingEditorForwardPortRequests(),
|
|
8603
8712
|
toolStatus: options.dependencyStatus === undefined ? null : dependencyStatusPayload(options.dependencyStatus),
|
|
8604
8713
|
editorRuntime: options.dependencyStatus?.editorRuntime === undefined ? null : dependencyStatusPayload(options.dependencyStatus).editorRuntime,
|
|
8605
8714
|
...localEditorCapabilities(options.editorRuntime, allowedCwds.value, localEditorState.value)
|
|
@@ -8611,6 +8720,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
8611
8720
|
const joinPayload = () => ({
|
|
8612
8721
|
clientName: "kandan-local-codex-runner",
|
|
8613
8722
|
clientId,
|
|
8723
|
+
runnerPid: process.pid,
|
|
8614
8724
|
version: linzumiCliVersion,
|
|
8615
8725
|
workspace: runnerWorkspaceSlug(options) ?? null,
|
|
8616
8726
|
channel: options.channelSession?.channelSlug ?? null,
|
|
@@ -8648,12 +8758,204 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
8648
8758
|
});
|
|
8649
8759
|
});
|
|
8650
8760
|
};
|
|
8761
|
+
const pushEditorPortForwardEvent = (event, payload) => {
|
|
8762
|
+
kandan.push(topic, event, payload).catch((error) => {
|
|
8763
|
+
log(`kandan.${event}_push_failed`, {
|
|
8764
|
+
message: error instanceof Error ? error.message : String(error)
|
|
8765
|
+
});
|
|
8766
|
+
});
|
|
8767
|
+
};
|
|
8768
|
+
const revokeEditorForwardPort = (port, reason) => {
|
|
8769
|
+
const target = localEditorForwardState.approvedTargets.get(port);
|
|
8770
|
+
localEditorForwardState.approvedPorts.delete(port);
|
|
8771
|
+
localEditorForwardState.approvedTargets.delete(port);
|
|
8772
|
+
liveForwardPorts.delete(port);
|
|
8773
|
+
clearForwardPortAttribution(port);
|
|
8774
|
+
pushEditorPortForwardEvent("forward_port_revoked", {
|
|
8775
|
+
instanceId,
|
|
8776
|
+
port,
|
|
8777
|
+
...target === undefined ? {} : {
|
|
8778
|
+
pid: target.pid,
|
|
8779
|
+
command: target.command,
|
|
8780
|
+
...target.cwd === undefined ? {} : { cwd: target.cwd }
|
|
8781
|
+
},
|
|
8782
|
+
reason,
|
|
8783
|
+
capabilities: revocationCapabilities(capabilitiesPayload(), port)
|
|
8784
|
+
});
|
|
8785
|
+
};
|
|
8786
|
+
const clearEditorPortForwarding = (reason) => {
|
|
8787
|
+
localEditorForwardState.watcher?.close();
|
|
8788
|
+
localEditorForwardState.watcher = undefined;
|
|
8789
|
+
localEditorForwardState.pendingRequests.clear();
|
|
8790
|
+
localEditorForwardState.queuedCandidates.clear();
|
|
8791
|
+
const revokedPorts = Array.from(localEditorForwardState.approvedPorts);
|
|
8792
|
+
for (const port of revokedPorts) {
|
|
8793
|
+
revokeEditorForwardPort(port, reason);
|
|
8794
|
+
}
|
|
8795
|
+
localEditorForwardState.dismissedTargets.clear();
|
|
8796
|
+
return revokedPorts;
|
|
8797
|
+
};
|
|
8798
|
+
const drainQueuedEditorPortForwardPrompt = () => {
|
|
8799
|
+
if (localEditorForwardState.pendingRequests.size > 0 || localEditorForwardState.queuedCandidates.size === 0) {
|
|
8800
|
+
return;
|
|
8801
|
+
}
|
|
8802
|
+
const next = Array.from(localEditorForwardState.queuedCandidates.values()).sort((left, right) => left.port - right.port)[0];
|
|
8803
|
+
if (next === undefined) {
|
|
8804
|
+
return;
|
|
8805
|
+
}
|
|
8806
|
+
localEditorForwardState.queuedCandidates.delete(next.port);
|
|
8807
|
+
publishEditorPortForwardPrompt(next);
|
|
8808
|
+
if (localEditorForwardState.pendingRequests.size === 0) {
|
|
8809
|
+
drainQueuedEditorPortForwardPrompt();
|
|
8810
|
+
}
|
|
8811
|
+
};
|
|
8812
|
+
const publishEditorPortForwardPrompt = (candidate) => {
|
|
8813
|
+
const review = reviewPortForwardCandidate({
|
|
8814
|
+
candidate,
|
|
8815
|
+
threadBound: true,
|
|
8816
|
+
suppressedPorts: new Set(suppressedForwardPorts()),
|
|
8817
|
+
approvedPorts: localEditorForwardState.approvedPorts,
|
|
8818
|
+
approvedTargets: localEditorForwardState.approvedTargets,
|
|
8819
|
+
dismissedTargets: localEditorForwardState.dismissedTargets,
|
|
8820
|
+
pendingRequests: Array.from(localEditorForwardState.pendingRequests.values())
|
|
8821
|
+
});
|
|
8822
|
+
switch (review.type) {
|
|
8823
|
+
case "skip":
|
|
8824
|
+
return;
|
|
8825
|
+
case "remember_approved_target":
|
|
8826
|
+
localEditorForwardState.approvedTargets.set(review.target.port, review.target);
|
|
8827
|
+
return;
|
|
8828
|
+
case "revoke_and_prompt":
|
|
8829
|
+
if (localEditorForwardState.pendingRequests.size > 0) {
|
|
8830
|
+
localEditorForwardState.queuedCandidates.set(candidate.port, candidate);
|
|
8831
|
+
return;
|
|
8832
|
+
}
|
|
8833
|
+
revokeEditorForwardPort(review.revoked.port, review.reason);
|
|
8834
|
+
break;
|
|
8835
|
+
case "prompt":
|
|
8836
|
+
if (localEditorForwardState.pendingRequests.size > 0) {
|
|
8837
|
+
localEditorForwardState.queuedCandidates.set(candidate.port, candidate);
|
|
8838
|
+
return;
|
|
8839
|
+
}
|
|
8840
|
+
break;
|
|
8841
|
+
}
|
|
8842
|
+
const requestId = `editor-port-forward-${randomUUID3()}`;
|
|
8843
|
+
const request = pendingRequestFromCandidate({
|
|
8844
|
+
requestId,
|
|
8845
|
+
sourceSeq: 0,
|
|
8846
|
+
candidate
|
|
8847
|
+
});
|
|
8848
|
+
localEditorForwardState.pendingRequests.set(request.port, request);
|
|
8849
|
+
const processIdentity = guessCanonicalProcessFromCommand(candidate.command);
|
|
8850
|
+
pushEditorPortForwardEvent("forward_port_requested", {
|
|
8851
|
+
instanceId,
|
|
8852
|
+
requestId,
|
|
8853
|
+
source: "local_editor",
|
|
8854
|
+
port: request.port,
|
|
8855
|
+
pid: request.pid,
|
|
8856
|
+
command: request.command,
|
|
8857
|
+
...request.cwd === undefined ? {} : { cwd: request.cwd },
|
|
8858
|
+
...processIdentity?.appName === undefined ? {} : { processName: processIdentity.appName },
|
|
8859
|
+
...processIdentity?.iconKey === undefined ? {} : { processIconKey: processIdentity.iconKey },
|
|
8860
|
+
capabilities: capabilitiesPayload()
|
|
8861
|
+
});
|
|
8862
|
+
};
|
|
8863
|
+
const expireLostEditorPortForwardCandidate = (candidate) => {
|
|
8864
|
+
const queuedCandidate = localEditorForwardState.queuedCandidates.get(candidate.port);
|
|
8865
|
+
if (queuedCandidate !== undefined && sameForwardCandidate(queuedCandidate, candidate)) {
|
|
8866
|
+
localEditorForwardState.queuedCandidates.delete(candidate.port);
|
|
8867
|
+
return;
|
|
8868
|
+
}
|
|
8869
|
+
const pendingRequest = localEditorForwardState.pendingRequests.get(candidate.port);
|
|
8870
|
+
if (pendingRequest !== undefined && sameForwardCandidate(approvedTargetFromRequest(pendingRequest), candidate)) {
|
|
8871
|
+
localEditorForwardState.pendingRequests.delete(candidate.port);
|
|
8872
|
+
pushEditorPortForwardEvent("forward_port_revoked", {
|
|
8873
|
+
instanceId,
|
|
8874
|
+
port: candidate.port,
|
|
8875
|
+
pid: candidate.pid,
|
|
8876
|
+
command: candidate.command,
|
|
8877
|
+
...candidate.cwd === undefined ? {} : { cwd: candidate.cwd },
|
|
8878
|
+
reason: "listener_exited",
|
|
8879
|
+
capabilities: revocationCapabilities(capabilitiesPayload(), candidate.port)
|
|
8880
|
+
});
|
|
8881
|
+
drainQueuedEditorPortForwardPrompt();
|
|
8882
|
+
return;
|
|
8883
|
+
}
|
|
8884
|
+
if (localEditorForwardState.approvedPorts.has(candidate.port)) {
|
|
8885
|
+
revokeEditorForwardPort(candidate.port, "listener_exited");
|
|
8886
|
+
}
|
|
8887
|
+
};
|
|
8888
|
+
const resolveEditorPortForwardRequest = (control) => {
|
|
8889
|
+
const request = Array.from(localEditorForwardState.pendingRequests.values()).find((request2) => request2.requestId === control.requestId);
|
|
8890
|
+
if (request === undefined) {
|
|
8891
|
+
return;
|
|
8892
|
+
}
|
|
8893
|
+
localEditorForwardState.pendingRequests.delete(request.port);
|
|
8894
|
+
if (control.decision === "deny") {
|
|
8895
|
+
localEditorForwardState.dismissedTargets.set(request.port, approvedTargetFromRequest(request));
|
|
8896
|
+
pushEditorPortForwardEvent("forward_port_resolved", {
|
|
8897
|
+
instanceId,
|
|
8898
|
+
requestId: request.requestId,
|
|
8899
|
+
source: "local_editor",
|
|
8900
|
+
port: request.port,
|
|
8901
|
+
pid: request.pid,
|
|
8902
|
+
command: request.command,
|
|
8903
|
+
...request.cwd === undefined ? {} : { cwd: request.cwd },
|
|
8904
|
+
decision: "deny",
|
|
8905
|
+
capabilities: capabilitiesPayload()
|
|
8906
|
+
});
|
|
8907
|
+
drainQueuedEditorPortForwardPrompt();
|
|
8908
|
+
return { instanceId, ok: true };
|
|
8909
|
+
}
|
|
8910
|
+
localEditorForwardState.approvedPorts.add(request.port);
|
|
8911
|
+
localEditorForwardState.approvedTargets.set(request.port, approvedTargetFromRequest(request));
|
|
8912
|
+
liveForwardPorts.add(request.port);
|
|
8913
|
+
const processIdentity = guessCanonicalProcessFromCommand(request.command);
|
|
8914
|
+
setForwardPortAttribution(request.port, {
|
|
8915
|
+
localEditor: true,
|
|
8916
|
+
processName: processIdentity?.appName ?? null,
|
|
8917
|
+
processIconKey: processIdentity?.iconKey ?? null
|
|
8918
|
+
});
|
|
8919
|
+
pushEditorPortForwardEvent("forward_port_resolved", {
|
|
8920
|
+
instanceId,
|
|
8921
|
+
requestId: request.requestId,
|
|
8922
|
+
source: "local_editor",
|
|
8923
|
+
port: request.port,
|
|
8924
|
+
pid: request.pid,
|
|
8925
|
+
command: request.command,
|
|
8926
|
+
...request.cwd === undefined ? {} : { cwd: request.cwd },
|
|
8927
|
+
decision: "approve",
|
|
8928
|
+
capabilities: capabilitiesPayload()
|
|
8929
|
+
});
|
|
8930
|
+
drainQueuedEditorPortForwardPrompt();
|
|
8931
|
+
return { instanceId, ok: true, port: request.port };
|
|
8932
|
+
};
|
|
8933
|
+
const startLocalEditorPortForwardWatcher = (state) => {
|
|
8934
|
+
if (state.process.pid === undefined) {
|
|
8935
|
+
log("port_forward.local_editor_watch_skipped", {
|
|
8936
|
+
reason: "editor_pid_missing"
|
|
8937
|
+
});
|
|
8938
|
+
return;
|
|
8939
|
+
}
|
|
8940
|
+
localEditorForwardState.watcher?.close();
|
|
8941
|
+
localEditorForwardState.watcher = startPortForwardWatcher({
|
|
8942
|
+
rootPid: state.process.pid,
|
|
8943
|
+
onCandidate: publishEditorPortForwardPrompt,
|
|
8944
|
+
onCandidateLost: expireLostEditorPortForwardCandidate,
|
|
8945
|
+
onError: (error) => {
|
|
8946
|
+
log("port_forward.local_editor_watch_failed", {
|
|
8947
|
+
message: error.message
|
|
8948
|
+
});
|
|
8949
|
+
}
|
|
8950
|
+
});
|
|
8951
|
+
};
|
|
8651
8952
|
const watchLocalEditorExit = (state, generation, initialStatusPushed) => {
|
|
8652
8953
|
const handleExit = () => {
|
|
8653
8954
|
if (localEditorGeneration.value !== generation || localEditorState.value.status !== "running" || localEditorState.value.process !== state.process) {
|
|
8654
8955
|
return;
|
|
8655
8956
|
}
|
|
8656
8957
|
localEditorState.value = { status: "disabled" };
|
|
8958
|
+
const revokedEditorPorts = clearEditorPortForwarding("editor_exited");
|
|
8657
8959
|
liveForwardPorts.delete(state.port);
|
|
8658
8960
|
managedForwardPorts.delete(state.port);
|
|
8659
8961
|
if (state.collaboration !== undefined) {
|
|
@@ -8666,7 +8968,11 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
8666
8968
|
cwd: state.cwd,
|
|
8667
8969
|
capabilities: {
|
|
8668
8970
|
...capabilitiesPayload(),
|
|
8669
|
-
revokedPorts: state.collaboration === undefined ? [state.port] : [
|
|
8971
|
+
revokedPorts: state.collaboration === undefined ? [state.port, ...revokedEditorPorts] : [
|
|
8972
|
+
state.port,
|
|
8973
|
+
state.collaboration.serverPort,
|
|
8974
|
+
...revokedEditorPorts
|
|
8975
|
+
]
|
|
8670
8976
|
}
|
|
8671
8977
|
});
|
|
8672
8978
|
};
|
|
@@ -8724,7 +9030,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
8724
9030
|
launchTui: options.launchTui,
|
|
8725
9031
|
enablePortForwardWatch: true,
|
|
8726
9032
|
initialForwardPorts: allowedForwardPorts,
|
|
8727
|
-
suppressedForwardPorts
|
|
9033
|
+
suppressedForwardPorts,
|
|
8728
9034
|
onForwardPortApproved: (port, attribution) => {
|
|
8729
9035
|
liveForwardPorts.add(port);
|
|
8730
9036
|
setForwardPortAttribution(port, attribution);
|
|
@@ -8779,7 +9085,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
8779
9085
|
launchTui: false,
|
|
8780
9086
|
enablePortForwardWatch: true,
|
|
8781
9087
|
initialForwardPorts: allowedForwardPorts,
|
|
8782
|
-
suppressedForwardPorts
|
|
9088
|
+
suppressedForwardPorts,
|
|
8783
9089
|
onForwardPortApproved: (port, attribution) => {
|
|
8784
9090
|
liveForwardPorts.add(port);
|
|
8785
9091
|
setForwardPortAttribution(port, attribution);
|
|
@@ -8950,6 +9256,11 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
8950
9256
|
if (result.ok) {
|
|
8951
9257
|
localEditorGeneration.value += 1;
|
|
8952
9258
|
const editorGeneration = localEditorGeneration.value;
|
|
9259
|
+
const restartedEditorRevokedPorts = localEditorState.value.status === "running" ? [
|
|
9260
|
+
localEditorState.value.port,
|
|
9261
|
+
...localEditorState.value.collaboration === undefined ? [] : [localEditorState.value.collaboration.serverPort],
|
|
9262
|
+
...clearEditorPortForwarding("editor_restarted")
|
|
9263
|
+
] : [];
|
|
8953
9264
|
if (localEditorState.value.status === "running") {
|
|
8954
9265
|
liveForwardPorts.delete(localEditorState.value.port);
|
|
8955
9266
|
managedForwardPorts.delete(localEditorState.value.port);
|
|
@@ -8969,10 +9280,14 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
8969
9280
|
instanceId,
|
|
8970
9281
|
requestId: control.requestId,
|
|
8971
9282
|
ok: true,
|
|
8972
|
-
capabilities:
|
|
9283
|
+
capabilities: {
|
|
9284
|
+
...capabilitiesPayload(),
|
|
9285
|
+
...restartedEditorRevokedPorts.length === 0 ? {} : { revokedPorts: restartedEditorRevokedPorts }
|
|
9286
|
+
},
|
|
8973
9287
|
...result.event
|
|
8974
9288
|
});
|
|
8975
9289
|
if (result.state.status === "running") {
|
|
9290
|
+
startLocalEditorPortForwardWatcher(result.state);
|
|
8976
9291
|
watchLocalEditorExit(result.state, editorGeneration, initialStatusPushed);
|
|
8977
9292
|
}
|
|
8978
9293
|
return initialStatusPushed;
|
|
@@ -9005,6 +9320,17 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
9005
9320
|
pushHeartbeat();
|
|
9006
9321
|
return;
|
|
9007
9322
|
}
|
|
9323
|
+
if (isResolvePortForwardRequestControl(control)) {
|
|
9324
|
+
const response = resolveEditorPortForwardRequest(control);
|
|
9325
|
+
if (response !== undefined) {
|
|
9326
|
+
kandan.push(topic, "codex_response", response).catch((error) => {
|
|
9327
|
+
log("kandan.control_response_push_failed", {
|
|
9328
|
+
message: error instanceof Error ? error.message : String(error)
|
|
9329
|
+
});
|
|
9330
|
+
});
|
|
9331
|
+
return;
|
|
9332
|
+
}
|
|
9333
|
+
}
|
|
9008
9334
|
if (isSetPortForwardEnabledControl(control)) {
|
|
9009
9335
|
switch (control.enabled) {
|
|
9010
9336
|
case true:
|
|
@@ -9014,6 +9340,8 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
9014
9340
|
liveForwardPorts.delete(control.port);
|
|
9015
9341
|
managedForwardPorts.delete(control.port);
|
|
9016
9342
|
clearForwardPortAttribution(control.port);
|
|
9343
|
+
localEditorForwardState.approvedPorts.delete(control.port);
|
|
9344
|
+
localEditorForwardState.approvedTargets.delete(control.port);
|
|
9017
9345
|
kandan.push(topic, "forward_port_revoked", {
|
|
9018
9346
|
instanceId,
|
|
9019
9347
|
port: control.port,
|
|
@@ -9205,6 +9533,24 @@ function codexTuiArgs(codexUrl, codexThreadId, session, fast) {
|
|
|
9205
9533
|
const overrides = codexTuiConfigArgs(session, fast);
|
|
9206
9534
|
return codexThreadId === undefined ? ["--remote", codexUrl, ...overrides] : ["resume", "--remote", codexUrl, ...overrides, codexThreadId];
|
|
9207
9535
|
}
|
|
9536
|
+
function suppressedForwardPortsForRunner(kandanControlPort, managedForwardPorts) {
|
|
9537
|
+
const suppressedPorts = new Set(managedForwardPorts);
|
|
9538
|
+
if (kandanControlPort !== undefined) {
|
|
9539
|
+
suppressedPorts.add(kandanControlPort);
|
|
9540
|
+
}
|
|
9541
|
+
return Array.from(suppressedPorts).sort((left, right) => left - right);
|
|
9542
|
+
}
|
|
9543
|
+
function explicitUrlPort(url) {
|
|
9544
|
+
const parsed = new URL(url);
|
|
9545
|
+
if (parsed.port === "") {
|
|
9546
|
+
return;
|
|
9547
|
+
}
|
|
9548
|
+
const port = Number.parseInt(parsed.port, 10);
|
|
9549
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
9550
|
+
throw new Error(`invalid Kandan URL port: ${url}`);
|
|
9551
|
+
}
|
|
9552
|
+
return port;
|
|
9553
|
+
}
|
|
9208
9554
|
function codexTuiConfigArgs(session, fast) {
|
|
9209
9555
|
const modelArgs = session?.model === undefined ? [] : ["--model", session.model];
|
|
9210
9556
|
const reasoningArgs = session?.reasoningEffort === undefined ? [] : ["-c", `model_reasoning_effort="${session.reasoningEffort}"`];
|
|
@@ -9621,6 +9967,9 @@ function isUpdateRunnerConfigControl(control) {
|
|
|
9621
9967
|
function isSetPortForwardEnabledControl(control) {
|
|
9622
9968
|
return control.type === "set_port_forward_enabled" && Number.isInteger(control.port) && control.port > 0 && control.port <= 65535;
|
|
9623
9969
|
}
|
|
9970
|
+
function isResolvePortForwardRequestControl(control) {
|
|
9971
|
+
return control.type === "resolve_port_forward_request" && typeof control.requestId === "string" && (control.decision === "approve" || control.decision === "deny");
|
|
9972
|
+
}
|
|
9624
9973
|
function normalizeAllowedCwds(values) {
|
|
9625
9974
|
return Array.from(new Set(values.flatMap((value) => {
|
|
9626
9975
|
const normalized = value.trim();
|
package/package.json
CHANGED