dsh-ntr 0.1.5 → 0.1.6
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.en.md +3 -1
- package/README.md +3 -1
- package/cordis.patch.yml +56 -28
- package/lib/bridge.js +366 -147
- package/lib/client/panel.js +220 -61
- package/lib/client/store.js +30 -10
- package/lib/client.js +288 -131
- package/lib/client.js.map +1 -1
- package/lib/impl.js +98 -26
- package/lib/index.js +22 -9
- package/lib/types/bridge.d.ts +153 -109
- package/lib/types/client/panel.d.ts +47 -1
- package/lib/types/client/store.d.ts +10 -3
- package/lib/types/impl.d.ts +62 -10
- package/lib/types/protocol.d.ts +115 -45
- package/package.json +1 -1
- package/src/bridge.ts +422 -196
- package/src/client/panel.tsx +342 -140
- package/src/client/store.ts +31 -11
- package/src/impl.ts +117 -53
- package/src/index.ts +23 -9
- package/src/protocol.ts +116 -45
package/lib/bridge.js
CHANGED
|
@@ -12,7 +12,7 @@ import { delimiter, dirname, join } from 'node:path';
|
|
|
12
12
|
import { Writable, Readable } from 'node:stream';
|
|
13
13
|
import { accessSync, constants, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
14
14
|
import { client, methods, ndJsonStream, PROTOCOL_VERSION, RequestError, } from '@agentclientprotocol/sdk';
|
|
15
|
-
import { AcpError, EventHub, PermissionLedger, Transcript, acpToolCallDelta, acpToolCallFromUpdate, buildAdapterModelId, childEnv, isAcpToolContinuation, isUnderPath, mergeCatalogShape, parseCatalogOptions, usableConfigOptions, quoteCommandToken, randomUUID, resolveFsPath, resolveSessionCwd, selectForwardPrompt, acpPresetStatusFromRoster, splitAdapterModelId, updateContentToText, describeUnhandledUpdate, BOOKKEEPING_UPDATES, validateAgentDef, validateTagHalo, validateApprovalDeadlines, } from "./impl.js";
|
|
15
|
+
import { AcpError, EventHub, PermissionLedger, Transcript, acpToolCallDelta, acpToolCallFromUpdate, buildAdapterModelId, childEnv, isAcpToolContinuation, isUnderPath, mergeCatalogShape, parseCatalogOptions, usableConfigOptions, pickAutoOption, quoteCommandToken, randomUUID, resolveFsPath, resolveSessionCwd, selectForwardPrompt, acpPresetStatusFromRoster, splitAdapterModelId, updateContentToText, describeUnhandledUpdate, BOOKKEEPING_UPDATES, validateAgentDef, validateTagHalo, validateApprovalDeadlines, validatePermissionTimeoutMode, normalizeApprovalWindow, } from "./impl.js";
|
|
16
16
|
import { DEFAULT_TAG_HALO } from "./protocol.js";
|
|
17
17
|
const STDERR_TAIL_LINES = 40;
|
|
18
18
|
/** Single fs/read_text_file answer cap — agents reading huge files must page via `line`/`limit`. */
|
|
@@ -73,9 +73,8 @@ function toAcpMcpServers(defs) {
|
|
|
73
73
|
}
|
|
74
74
|
const DEFAULT_TUNING = {
|
|
75
75
|
startupTimeoutMs: 60_000,
|
|
76
|
-
permissionTimeoutMs:
|
|
77
|
-
|
|
78
|
-
nativeApprovalTimeoutMs: 300_000,
|
|
76
|
+
permissionTimeoutMs: 300_000,
|
|
77
|
+
permissionTimeoutMode: 'wait',
|
|
79
78
|
cancelFallbackMs: 15_000,
|
|
80
79
|
staleCancelRetryDelayMs: 600,
|
|
81
80
|
staleCancelMaxRetries: 2,
|
|
@@ -116,19 +115,17 @@ export class AgentBridge {
|
|
|
116
115
|
*/
|
|
117
116
|
ledgerFallbacks = new Map;
|
|
118
117
|
startupTimeoutMs;
|
|
119
|
-
permissionTimeoutMs;
|
|
120
118
|
/**
|
|
121
|
-
* The
|
|
122
|
-
* settings page rewrites them on a
|
|
123
|
-
* ({@link AgentBridge.applyApprovalDeadlines}), so a user
|
|
124
|
-
* does not have to kill every live session to see it take
|
|
125
|
-
* read per ask
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* holding an armed timer keeps the window it was started with.
|
|
119
|
+
* The ONE approval deadline, and the mode it settles with. Neither is
|
|
120
|
+
* readonly, unlike their neighbors: the settings page rewrites them on a
|
|
121
|
+
* running bridge ({@link AgentBridge.applyApprovalDeadlines}), so a user
|
|
122
|
+
* editing the card does not have to kill every live session to see it take
|
|
123
|
+
* effect. Both are read per ask, so a reassignment reaches every ask from
|
|
124
|
+
* that moment on. An ask already holding an armed timer keeps the window it
|
|
125
|
+
* was started with.
|
|
129
126
|
*/
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
permissionTimeoutMs;
|
|
128
|
+
permissionTimeoutMode;
|
|
132
129
|
cancelFallbackMs;
|
|
133
130
|
staleCancelRetryDelayMs;
|
|
134
131
|
staleCancelMaxRetries;
|
|
@@ -195,9 +192,10 @@ export class AgentBridge {
|
|
|
195
192
|
this.sessionWorkspace = sessionWorkspace;
|
|
196
193
|
const merged = { ...DEFAULT_TUNING, ...tuning };
|
|
197
194
|
this.startupTimeoutMs = merged.startupTimeoutMs;
|
|
198
|
-
this.
|
|
199
|
-
|
|
200
|
-
|
|
195
|
+
this.permissionTimeoutMode = merged.permissionTimeoutMode;
|
|
196
|
+
// 不等待 (0) is the `auto` mode's business; under `abort` it would refuse
|
|
197
|
+
// every ask on arrival, so the window is floored to the shortest real wait.
|
|
198
|
+
this.permissionTimeoutMs = normalizeApprovalWindow(this.permissionTimeoutMode, merged.permissionTimeoutMs);
|
|
201
199
|
this.cancelFallbackMs = merged.cancelFallbackMs;
|
|
202
200
|
this.staleCancelRetryDelayMs = merged.staleCancelRetryDelayMs;
|
|
203
201
|
this.staleCancelMaxRetries = merged.staleCancelMaxRetries;
|
|
@@ -206,20 +204,24 @@ export class AgentBridge {
|
|
|
206
204
|
this.probeIdleTimeoutMs = merged.probeIdleTimeoutMs;
|
|
207
205
|
}
|
|
208
206
|
/**
|
|
209
|
-
* Adopt a rewritten approval deadline (the settings page's write).
|
|
210
|
-
*
|
|
211
|
-
* construction, because a live bridge cannot meaningfully adopt a
|
|
212
|
-
* or cancel window mid-flight.
|
|
207
|
+
* Adopt a rewritten approval deadline or mode (the settings page's write).
|
|
208
|
+
* Only the window and the mode live here; every other tuning value is
|
|
209
|
+
* fixed at construction, because a live bridge cannot meaningfully adopt a
|
|
210
|
+
* new startup or cancel window mid-flight.
|
|
213
211
|
*
|
|
214
212
|
* Deliberately does NOT re-arm anything: an ask that already holds a timer
|
|
215
213
|
* keeps the window it was started with, and the next ask reads the new value.
|
|
216
214
|
* Re-arming would move the goalposts under a human who is already deciding.
|
|
217
215
|
*/
|
|
218
216
|
applyApprovalDeadlines(patch) {
|
|
219
|
-
if (patch.
|
|
220
|
-
this.
|
|
221
|
-
if (patch.
|
|
222
|
-
this.
|
|
217
|
+
if (patch.permissionTimeoutMs !== undefined)
|
|
218
|
+
this.permissionTimeoutMs = patch.permissionTimeoutMs;
|
|
219
|
+
if (patch.mode !== undefined)
|
|
220
|
+
this.permissionTimeoutMode = patch.mode;
|
|
221
|
+
// A mode switch can leave a window behind that the new mode cannot run
|
|
222
|
+
// (不等待 under `abort`): floor it, so the ask waits instead of refusing
|
|
223
|
+
// on arrival.
|
|
224
|
+
this.permissionTimeoutMs = normalizeApprovalWindow(this.permissionTimeoutMode, this.permissionTimeoutMs);
|
|
223
225
|
}
|
|
224
226
|
/**
|
|
225
227
|
* The idle-release window this agent honors for its CHAT process: its own
|
|
@@ -1053,6 +1055,23 @@ export class AgentBridge {
|
|
|
1053
1055
|
this.touch();
|
|
1054
1056
|
return native.response;
|
|
1055
1057
|
}
|
|
1058
|
+
// Deadline: how long this ask may sit unanswered. ONE window for every
|
|
1059
|
+
// route — an ask that never reached the conversation included. They have
|
|
1060
|
+
// no other answer entry, so exempting them would be the very invisible
|
|
1061
|
+
// hang this mechanism exists to bound.
|
|
1062
|
+
//
|
|
1063
|
+
// What happens when it fires is the MODE's business: refused, answered
|
|
1064
|
+
// with the option the agent's own kinds imply, or (in `wait`) never.
|
|
1065
|
+
// The mode is read once, here, for the same reason the window is: an ask
|
|
1066
|
+
// that started under one mode keeps it.
|
|
1067
|
+
const mode = this.permissionTimeoutMode;
|
|
1068
|
+
const timeoutMs = this.permissionTimeoutMs;
|
|
1069
|
+
// 不等待(0)只属自动推进:请求到达即按那一档收场,不建 ledger 条目、不发卡
|
|
1070
|
+
// ——问都来不及问。超时中止没有这一档(它会立刻拒绝每一问,agent 在每个门口
|
|
1071
|
+
// 都停住),它的窗口最短是真实等待。
|
|
1072
|
+
if (mode === 'auto' && timeoutMs === 0) {
|
|
1073
|
+
return this.settleExpiredAsk(session, mode, toolCall, options, 0);
|
|
1074
|
+
}
|
|
1056
1075
|
return new Promise(resolve => {
|
|
1057
1076
|
let timer;
|
|
1058
1077
|
const requestId = this.permissions.create(session.sessionId, (outcome) => {
|
|
@@ -1074,42 +1093,32 @@ export class AgentBridge {
|
|
|
1074
1093
|
toolCall,
|
|
1075
1094
|
options: options.map((o) => ({ optionId: o.optionId, name: o.name, kind: o.kind })),
|
|
1076
1095
|
});
|
|
1077
|
-
// Deadline (`permissionTimeoutMs`): an approval left unanswered is
|
|
1078
|
-
// answered `cancelled` on the human's behalf instead of hanging the turn
|
|
1079
|
-
// forever. Off by default — approvals belong to a person. The one
|
|
1080
|
-
// exception is a DEGRADED native route: the conversation just failed to
|
|
1081
|
-
// show the question, so waiting forever would hang the turn behind an
|
|
1082
|
-
// ask nobody can see there, and the fallback deadline applies instead
|
|
1083
|
-
// (see `BridgeTuning.permissionFallbackTimeoutMs`). `native-timeout` is
|
|
1084
|
-
// the same degradation reached from the other side: the host took the
|
|
1085
|
-
// ask and never answered it.
|
|
1086
|
-
const timeoutMs = this.permissionTimeoutMs > 0
|
|
1087
|
-
? this.permissionTimeoutMs
|
|
1088
|
-
: native.reason === 'unavailable' || native.reason === 'native-timeout'
|
|
1089
|
-
? this.permissionFallbackTimeoutMs
|
|
1090
|
-
: 0;
|
|
1091
1096
|
// Recorded before the broadcast: the hub is synchronous, and the chat
|
|
1092
1097
|
// line `streamPrompt` folds from this frame words itself by the path
|
|
1093
1098
|
// this ask actually took.
|
|
1094
|
-
this.ledgerFallbacks.set(requestId, {
|
|
1095
|
-
|
|
1096
|
-
timeoutMs,
|
|
1097
|
-
...(native.reason === 'native-timeout' ? { waitedMs: this.nativeApprovalTimeoutMs } : {}),
|
|
1098
|
-
});
|
|
1099
|
-
if (timeoutMs > 0) {
|
|
1099
|
+
this.ledgerFallbacks.set(requestId, { reason: native.reason, timeoutMs, mode });
|
|
1100
|
+
if (timeoutMs > 0 && mode !== 'wait') {
|
|
1100
1101
|
timer = setTimeout(() => {
|
|
1101
1102
|
if (!this.permissions.has(requestId))
|
|
1102
1103
|
return;
|
|
1104
|
+
const picked = mode === 'auto'
|
|
1105
|
+
? pickAutoOption(options.map(option => ({ optionId: option.optionId, name: option.name, kind: option.kind })))
|
|
1106
|
+
: undefined;
|
|
1107
|
+
const tool = permissionToolName(toolCall);
|
|
1103
1108
|
this.hub.broadcast({
|
|
1104
1109
|
type: 'agent/notice',
|
|
1105
1110
|
agentId: this.def.id,
|
|
1106
1111
|
level: 'info',
|
|
1107
|
-
// Name what was missed, the way a human
|
|
1108
|
-
// sat unanswered, and
|
|
1109
|
-
//
|
|
1110
|
-
text:
|
|
1112
|
+
// Name what was missed and what was done about it, the way a human
|
|
1113
|
+
// needs it: which operation sat unanswered, and what the plugin
|
|
1114
|
+
// answered instead (OmniTerm's reaper writes the same notice).
|
|
1115
|
+
text: picked === undefined
|
|
1116
|
+
? `acp: permission request for "${tool}" timed out after ${String(Math.round(timeoutMs / 1000))}s; refused`
|
|
1117
|
+
: `acp: permission request for "${tool}" timed out after ${String(Math.round(timeoutMs / 1000))}s; answered "${picked.name}"`,
|
|
1111
1118
|
});
|
|
1112
|
-
this.permissions.respond(requestId,
|
|
1119
|
+
this.permissions.respond(requestId, picked === undefined
|
|
1120
|
+
? { outcome: 'cancelled' }
|
|
1121
|
+
: { outcome: 'selected', optionId: picked.optionId });
|
|
1113
1122
|
}, timeoutMs);
|
|
1114
1123
|
timer.unref?.();
|
|
1115
1124
|
}
|
|
@@ -1138,19 +1147,19 @@ export class AgentBridge {
|
|
|
1138
1147
|
* open turn, so a session started over `/acp` (no chat turn) cannot be
|
|
1139
1148
|
* routed and must stay on the ledger path.
|
|
1140
1149
|
*
|
|
1141
|
-
* The ask is raced against {@link BridgeTuning.
|
|
1150
|
+
* The ask is raced against {@link BridgeTuning.permissionTimeoutMs}. A
|
|
1142
1151
|
* routed ask has exactly one answer entry — the composer takeover — and no
|
|
1143
1152
|
* broadcast at all, so a host that takes the question and never renders it
|
|
1144
1153
|
* would leave the turn hanging behind something invisible. Past the deadline
|
|
1145
|
-
* the ask is
|
|
1146
|
-
*
|
|
1147
|
-
*
|
|
1148
|
-
*
|
|
1149
|
-
* human mid-decision finds the same question in the settings panel.
|
|
1154
|
+
* the ask is settled RIGHT THERE, by
|
|
1155
|
+
* {@link BridgeTuning.permissionTimeoutMode}: refused (`abort`), answered
|
|
1156
|
+
* with the option the agent's own kinds imply (`auto`), or never expiring
|
|
1157
|
+
* at all (`wait`).
|
|
1150
1158
|
*
|
|
1151
|
-
*
|
|
1152
|
-
*
|
|
1153
|
-
* the
|
|
1159
|
+
* It is NOT handed somewhere else. An earlier revision withdrew the ask and
|
|
1160
|
+
* re-asked it on the settings ledger, which made the question travel to a
|
|
1161
|
+
* page the user was not looking at — and then cancelled it there a few
|
|
1162
|
+
* minutes later anyway. A deadline now ends the ask where it was asked.
|
|
1154
1163
|
*
|
|
1155
1164
|
* @returns the agent-facing response, or the reason the ask must fall back
|
|
1156
1165
|
* to the ledger (see {@link LedgerFallbackReason}).
|
|
@@ -1161,6 +1170,16 @@ export class AgentBridge {
|
|
|
1161
1170
|
return { reason: 'no-requester' };
|
|
1162
1171
|
if (session.dshSessionId === undefined)
|
|
1163
1172
|
return { reason: 'no-chat-session' };
|
|
1173
|
+
// The mode is read ONCE, here: it decides both whether a deadline exists
|
|
1174
|
+
// and what happens when it fires. An ask that started under one mode keeps
|
|
1175
|
+
// it — the same rule the window follows (an ask already holding a timer
|
|
1176
|
+
// keeps the one it started with).
|
|
1177
|
+
const mode = this.permissionTimeoutMode;
|
|
1178
|
+
// 不等待(0)只属自动推进:没有问人的必要。直接按那一档收场,composer 接管
|
|
1179
|
+
// 根本不必出现——它只会闪一下就没了,问了也来不及答。超时中止没有这一档。
|
|
1180
|
+
if (mode === 'auto' && this.permissionTimeoutMs === 0) {
|
|
1181
|
+
return { response: this.settleExpiredAsk(session, mode, toolCall, options, 0) };
|
|
1182
|
+
}
|
|
1164
1183
|
// One signal carries both withdrawals: the agent's own cancel (whatever
|
|
1165
1184
|
// aborted the ACP request) and the deadline. The host's waterfall races its
|
|
1166
1185
|
// answerers against this signal, so aborting is a clean withdrawal rather
|
|
@@ -1169,22 +1188,32 @@ export class AgentBridge {
|
|
|
1169
1188
|
const askSignal = signal === undefined ? withdraw.signal : AbortSignal.any([signal, withdraw.signal]);
|
|
1170
1189
|
let timer;
|
|
1171
1190
|
let timedOut = false;
|
|
1172
|
-
|
|
1191
|
+
// `wait` never expires, so it never arms a timer at all. A window of `0`
|
|
1192
|
+
// never reaches here: 不等待 was already settled above.
|
|
1193
|
+
const windowMs = this.permissionTimeoutMs;
|
|
1194
|
+
const deadline = mode !== 'wait' && windowMs > 0
|
|
1173
1195
|
? new Promise(resolve => {
|
|
1174
1196
|
timer = setTimeout(() => {
|
|
1175
1197
|
timedOut = true;
|
|
1176
1198
|
withdraw.abort();
|
|
1177
1199
|
resolve({ timedOut: true });
|
|
1178
|
-
},
|
|
1200
|
+
}, windowMs);
|
|
1179
1201
|
timer.unref?.();
|
|
1180
1202
|
})
|
|
1181
1203
|
: undefined;
|
|
1182
1204
|
let outcome;
|
|
1183
1205
|
try {
|
|
1206
|
+
const row = (typeof toolCall === 'object' && toolCall !== null ? toolCall : {});
|
|
1184
1207
|
const asked = requester({
|
|
1185
1208
|
dshSessionId: session.dshSessionId,
|
|
1186
1209
|
agentId: this.def.id,
|
|
1187
1210
|
toolName: permissionToolName(toolCall),
|
|
1211
|
+
reason: permissionAskReason(toolCall),
|
|
1212
|
+
// The ACP call id is the id the adapter streamed on the host tool card
|
|
1213
|
+
// (`TurnStreamController` fills `tool-call` blocks from
|
|
1214
|
+
// `snapshot.callId`), so the host's approval detail slot can attach
|
|
1215
|
+
// the operation's own card to this ask.
|
|
1216
|
+
...(typeof row.toolCallId === 'string' && row.toolCallId !== '' ? { callId: row.toolCallId } : {}),
|
|
1188
1217
|
signal: askSignal,
|
|
1189
1218
|
});
|
|
1190
1219
|
// A withdrawn ask settles on the host's own schedule; dropping it here
|
|
@@ -1195,8 +1224,9 @@ export class AgentBridge {
|
|
|
1195
1224
|
: await Promise.race([asked.then(o => ({ outcome: o })), deadline]);
|
|
1196
1225
|
// The flag, not the race winner, decides: the host may settle `cancelled`
|
|
1197
1226
|
// from the very abort the deadline caused, and that is still a timeout.
|
|
1198
|
-
if (timedOut || 'timedOut' in settled)
|
|
1199
|
-
return {
|
|
1227
|
+
if (timedOut || 'timedOut' in settled) {
|
|
1228
|
+
return { response: this.settleExpiredAsk(session, mode, toolCall, options, windowMs) };
|
|
1229
|
+
}
|
|
1200
1230
|
outcome = settled.outcome;
|
|
1201
1231
|
}
|
|
1202
1232
|
catch {
|
|
@@ -1224,6 +1254,57 @@ export class AgentBridge {
|
|
|
1224
1254
|
});
|
|
1225
1255
|
return { response };
|
|
1226
1256
|
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Settle an ask whose deadline fired, WHERE IT WAS ASKED.
|
|
1259
|
+
*
|
|
1260
|
+
* `abort` refuses it — the operation does not happen and the agent carries
|
|
1261
|
+
* on with that answer. `auto` answers it with the option the agent's own
|
|
1262
|
+
* kinds imply ({@link pickAutoOption}) so the agent CONTINUES; it says which
|
|
1263
|
+
* option it picked, because an unattended approval nobody looked at has to
|
|
1264
|
+
* be visible afterwards. A mode that cannot pick an option (no options, or
|
|
1265
|
+
* none with a usable id) degrades to `abort`: refusing is safer than
|
|
1266
|
+
* guessing, and hanging is worse than either.
|
|
1267
|
+
*
|
|
1268
|
+
* The `permission/resolved` frame keeps the event stream symmetric with the
|
|
1269
|
+
* ledger path, so the browser drops any card it may hold for this session.
|
|
1270
|
+
*/
|
|
1271
|
+
settleExpiredAsk(session, mode, toolCall, options, timeoutMs) {
|
|
1272
|
+
const picked = mode === 'auto'
|
|
1273
|
+
? pickAutoOption(options.map(option => ({ optionId: option.optionId, name: option.name, kind: option.kind })))
|
|
1274
|
+
: undefined;
|
|
1275
|
+
const response = picked === undefined
|
|
1276
|
+
? { outcome: { outcome: 'cancelled' } }
|
|
1277
|
+
: { outcome: { outcome: 'selected', optionId: picked.optionId } };
|
|
1278
|
+
const tool = permissionToolName(toolCall);
|
|
1279
|
+
// 不等待(0)不是「等了 0 秒」,是没等:两种说法的区别就在这里。
|
|
1280
|
+
const waited = timeoutMs > 0
|
|
1281
|
+
? `timed out after ${String(Math.round(timeoutMs / 1000))}s`
|
|
1282
|
+
: 'was not waited for';
|
|
1283
|
+
this.hub.broadcast({
|
|
1284
|
+
type: 'agent/notice',
|
|
1285
|
+
agentId: this.def.id,
|
|
1286
|
+
level: 'info',
|
|
1287
|
+
// Name what was missed and what was done about it — the way a human
|
|
1288
|
+
// needs it: which operation sat unanswered, and what the plugin answered
|
|
1289
|
+
// instead. OmniTerm's reaper writes the same "you missed this" notice.
|
|
1290
|
+
text: picked === undefined
|
|
1291
|
+
? `acp: permission request for "${tool}" ${waited}; refused`
|
|
1292
|
+
: `acp: permission request for "${tool}" ${waited}; answered "${picked.name}"`,
|
|
1293
|
+
});
|
|
1294
|
+
// The conversation's only trace of the expiry: the composer card is going
|
|
1295
|
+
// away and nothing else says why.
|
|
1296
|
+
this.hub.broadcast({
|
|
1297
|
+
type: 'permission/expired',
|
|
1298
|
+
sessionId: session.sessionId,
|
|
1299
|
+
toolCall,
|
|
1300
|
+
timeoutMs,
|
|
1301
|
+
outcome: response.outcome.outcome === 'selected'
|
|
1302
|
+
? { outcome: 'selected', optionId: response.outcome.optionId }
|
|
1303
|
+
: { outcome: 'cancelled' },
|
|
1304
|
+
...(picked === undefined ? {} : { optionName: picked.name }),
|
|
1305
|
+
});
|
|
1306
|
+
return response;
|
|
1307
|
+
}
|
|
1227
1308
|
settlePermissions(session) {
|
|
1228
1309
|
this.permissions.settleFor(p => p.sessionId === session.sessionId);
|
|
1229
1310
|
}
|
|
@@ -1361,6 +1442,59 @@ export function permissionToolName(toolCall) {
|
|
|
1361
1442
|
return row.kind;
|
|
1362
1443
|
return 'tool call';
|
|
1363
1444
|
}
|
|
1445
|
+
/** Longest reason line the approval card headline carries. */
|
|
1446
|
+
const APPROVAL_REASON_MAX = 240;
|
|
1447
|
+
/** Input keys that name the operation in one string, most specific first. */
|
|
1448
|
+
const APPROVAL_REASON_INPUT_KEYS = [
|
|
1449
|
+
'command',
|
|
1450
|
+
'cmd',
|
|
1451
|
+
'path',
|
|
1452
|
+
'file_path',
|
|
1453
|
+
'filePath',
|
|
1454
|
+
'pattern',
|
|
1455
|
+
'query',
|
|
1456
|
+
'url',
|
|
1457
|
+
'description',
|
|
1458
|
+
];
|
|
1459
|
+
/**
|
|
1460
|
+
* One line saying WHAT the native approval card is about.
|
|
1461
|
+
*
|
|
1462
|
+
* WHY: the host renders `reason ?? "工具 <name> 请求越权执行"`. Without a
|
|
1463
|
+
* reason the human only sees the tool's NAME ("Bash") and cannot judge the
|
|
1464
|
+
* request; the ACP tool call already declares the missing half — its title and
|
|
1465
|
+
* (for execute/edit tools) a short scalar in `rawInput`. Only ONE declared
|
|
1466
|
+
* string is used, never a serialized object or a whole file, and the line is
|
|
1467
|
+
* capped so a pathological input cannot fill the card or the audit log.
|
|
1468
|
+
*/
|
|
1469
|
+
export function permissionAskReason(toolCall) {
|
|
1470
|
+
const row = (typeof toolCall === 'object' && toolCall !== null ? toolCall : {});
|
|
1471
|
+
const title = permissionToolName(toolCall);
|
|
1472
|
+
const raw = row.rawInput;
|
|
1473
|
+
const input = raw !== null && typeof raw === 'object' && !Array.isArray(raw)
|
|
1474
|
+
? raw
|
|
1475
|
+
: typeof raw === 'string' && raw.length <= APPROVAL_REASON_MAX * 4
|
|
1476
|
+
// Some agents send the arguments as a JSON string; read one scalar out
|
|
1477
|
+
// of it instead of echoing the blob.
|
|
1478
|
+
? (() => {
|
|
1479
|
+
try {
|
|
1480
|
+
const parsed = JSON.parse(raw);
|
|
1481
|
+
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
|
|
1482
|
+
? parsed
|
|
1483
|
+
: undefined;
|
|
1484
|
+
}
|
|
1485
|
+
catch {
|
|
1486
|
+
return undefined;
|
|
1487
|
+
}
|
|
1488
|
+
})()
|
|
1489
|
+
: undefined;
|
|
1490
|
+
const detail = input === undefined
|
|
1491
|
+
? undefined
|
|
1492
|
+
: APPROVAL_REASON_INPUT_KEYS
|
|
1493
|
+
.map(key => input[key])
|
|
1494
|
+
.find((value) => typeof value === 'string' && value !== '');
|
|
1495
|
+
const line = detail === undefined || detail === title ? title : `${title} · ${detail}`;
|
|
1496
|
+
return line.length > APPROVAL_REASON_MAX ? `${line.slice(0, APPROVAL_REASON_MAX - 1)}…` : line;
|
|
1497
|
+
}
|
|
1364
1498
|
/**
|
|
1365
1499
|
* Translate a native approval outcome into the ACP option the agent must see.
|
|
1366
1500
|
* `'allowed-once'` is the chain's only grant; a rejection prefers the once-variant.
|
|
@@ -2319,24 +2453,36 @@ class TurnStreamController {
|
|
|
2319
2453
|
}
|
|
2320
2454
|
else if (row.type === 'permission/request') {
|
|
2321
2455
|
const typed = event;
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
return;
|
|
2328
|
-
}
|
|
2329
|
-
if (seg.hasCard) {
|
|
2330
|
-
// The card must stay the segment's last block: park the line for the
|
|
2331
|
-
// next segment instead of streaming it below the card.
|
|
2332
|
-
this.pendingProse.push(line);
|
|
2333
|
-
this.finishSegment(seg, { kind: 'stop' });
|
|
2334
|
-
return;
|
|
2335
|
-
}
|
|
2336
|
-
if (line !== '')
|
|
2337
|
-
this.emitProse(seg, 'reasoning', line);
|
|
2456
|
+
this.pushProse(this.permissionLine(typed));
|
|
2457
|
+
}
|
|
2458
|
+
else if (row.type === 'permission/expired') {
|
|
2459
|
+
const typed = event;
|
|
2460
|
+
this.pushProse(this.expiredLine(typed));
|
|
2338
2461
|
}
|
|
2339
2462
|
}
|
|
2463
|
+
/**
|
|
2464
|
+
* Put one narrative line into the turn's prose stream.
|
|
2465
|
+
*
|
|
2466
|
+
* A card-bearing segment must stay the segment's LAST block, so a line that
|
|
2467
|
+
* arrives while a card is open is parked for the next segment instead of
|
|
2468
|
+
* streaming below the card. A line that arrives between segments waits for
|
|
2469
|
+
* the next one to open.
|
|
2470
|
+
*/
|
|
2471
|
+
pushProse(line) {
|
|
2472
|
+
if (line === '')
|
|
2473
|
+
return;
|
|
2474
|
+
const seg = this.segment;
|
|
2475
|
+
if (seg === undefined || seg.finished) {
|
|
2476
|
+
this.pendingProse.push(line);
|
|
2477
|
+
return;
|
|
2478
|
+
}
|
|
2479
|
+
if (seg.hasCard) {
|
|
2480
|
+
this.pendingProse.push(line);
|
|
2481
|
+
this.finishSegment(seg, { kind: 'stop' });
|
|
2482
|
+
return;
|
|
2483
|
+
}
|
|
2484
|
+
this.emitProse(seg, 'reasoning', line);
|
|
2485
|
+
}
|
|
2340
2486
|
/** Events that arrive between segments: recorded, replayed into the next one. */
|
|
2341
2487
|
recordWithoutSegment(update) {
|
|
2342
2488
|
const kind = String(update.sessionUpdate ?? '');
|
|
@@ -2476,25 +2622,44 @@ class TurnStreamController {
|
|
|
2476
2622
|
: 'permission';
|
|
2477
2623
|
// The line must not point at a place with no answer entry, and the bridge
|
|
2478
2624
|
// knows which path this ask took (`ledgerFallbackOf`, recorded before the
|
|
2479
|
-
// frame was broadcast). On the
|
|
2480
|
-
//
|
|
2481
|
-
//
|
|
2482
|
-
//
|
|
2483
|
-
//
|
|
2625
|
+
// frame was broadcast). On the ledger paths the settings panel's card is
|
|
2626
|
+
// the answer entry. A DEGRADED native route has to say why the
|
|
2627
|
+
// conversation is not showing the question — as an OBSERVED FACT, not a
|
|
2628
|
+
// guessed cause.
|
|
2629
|
+
//
|
|
2630
|
+
// The last clause says what happens if nobody answers, and it follows the
|
|
2631
|
+
// MODE: refused, answered automatically, or (in `wait`) nothing at all.
|
|
2484
2632
|
const fallback = this.bridge.ledgerFallbackOf(event.requestId);
|
|
2485
|
-
const why = fallback === undefined
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2633
|
+
const why = fallback?.reason === 'unavailable' ? 'the approval service is unavailable' : undefined;
|
|
2634
|
+
const lead = why === undefined ? 'please approve in Settings' : `${why}; approve in Settings`;
|
|
2635
|
+
if (fallback === undefined || fallback.timeoutMs <= 0 || fallback.mode === 'wait') {
|
|
2636
|
+
return `\n> 🔐 [Permission Required: ${title}] - ${lead}\n`;
|
|
2637
|
+
}
|
|
2638
|
+
const otherwise = fallback.mode === 'auto'
|
|
2639
|
+
? 'this request is answered automatically'
|
|
2640
|
+
: 'this request is cancelled';
|
|
2641
|
+
return `\n> 🔐 [Permission Required: ${title}] - ${lead} within ${String(Math.round(fallback.timeoutMs / 1000))}s or ${otherwise}\n`;
|
|
2642
|
+
}
|
|
2643
|
+
/**
|
|
2644
|
+
* The line for an ask the plugin settled itself when its deadline fired.
|
|
2645
|
+
*
|
|
2646
|
+
* The composer card is disappearing, so the conversation has to say what
|
|
2647
|
+
* happened to the question — otherwise the user is left with a vanished
|
|
2648
|
+
* card and no explanation. The wording follows the outcome: refused, or
|
|
2649
|
+
* answered with the option the agent's own kinds implied.
|
|
2650
|
+
*/
|
|
2651
|
+
expiredLine(event) {
|
|
2652
|
+
const title = typeof event.toolCall?.title === 'string'
|
|
2653
|
+
? String(event.toolCall.title)
|
|
2654
|
+
: 'permission';
|
|
2655
|
+
const waited = event.timeoutMs > 0
|
|
2656
|
+
? `no answer within ${String(Math.round(event.timeoutMs / 1000))}s`
|
|
2657
|
+
: 'not waited for';
|
|
2658
|
+
if (event.outcome.outcome === 'selected') {
|
|
2659
|
+
const picked = event.optionName === undefined ? event.outcome.optionId : event.optionName;
|
|
2660
|
+
return `\n> 🔐 [Permission Required: ${title}] - ${waited}; answered "${picked}" automatically, continuing\n`;
|
|
2661
|
+
}
|
|
2662
|
+
return `\n> 🔐 [Permission Required: ${title}] - ${waited}; refused\n`;
|
|
2498
2663
|
}
|
|
2499
2664
|
}
|
|
2500
2665
|
/** Chunk text shared by the controller's fold (string / object / array forms). */
|
|
@@ -2539,17 +2704,23 @@ export class BridgeManager {
|
|
|
2539
2704
|
/** The agent tag's halo preference (persisted; see `persistAgents`). */
|
|
2540
2705
|
tagHaloSetting = { ...DEFAULT_TAG_HALO };
|
|
2541
2706
|
/**
|
|
2542
|
-
* What `cordis.patch.yml` asked for
|
|
2543
|
-
*
|
|
2707
|
+
* What `cordis.patch.yml` asked for — ONLY if it actually named the key.
|
|
2708
|
+
* This is the deployment baseline, kept apart from
|
|
2544
2709
|
* {@link deadlineOverride} so the settings card can tell the user which of
|
|
2545
|
-
* the two is winning. Empty when the file says nothing about
|
|
2546
|
-
*
|
|
2710
|
+
* the two is winning. Empty when the file says nothing about the window, in
|
|
2711
|
+
* which case {@link baselineDeadline} (the built-in default) applies.
|
|
2547
2712
|
*/
|
|
2548
2713
|
configuredDeadlines = {};
|
|
2549
2714
|
/**
|
|
2550
|
-
* The
|
|
2551
|
-
*
|
|
2552
|
-
*
|
|
2715
|
+
* The mode the config file named, if it named one. Kept apart from the
|
|
2716
|
+
* override for the same reason the window is: the card has to be able
|
|
2717
|
+
* to say "the file asks for `abort`, the value set here wins".
|
|
2718
|
+
*/
|
|
2719
|
+
configuredMode;
|
|
2720
|
+
/**
|
|
2721
|
+
* The window before any override — the tuning this manager was constructed
|
|
2722
|
+
* with, which already carries the validated config file (the host entry
|
|
2723
|
+
* passes `validateConfig`'s output) or the built-in default.
|
|
2553
2724
|
*
|
|
2554
2725
|
* Held separately from {@link configuredDeadlines} because the two answer
|
|
2555
2726
|
* different questions: this one is what RUNS when nothing is overridden, that
|
|
@@ -2557,17 +2728,23 @@ export class BridgeManager {
|
|
|
2557
2728
|
* and a bare default does not deserve). A manager built without tuning at all
|
|
2558
2729
|
* — a direct consumer, a test — gets `DEFAULT_TUNING` here and stays correct.
|
|
2559
2730
|
*/
|
|
2560
|
-
|
|
2731
|
+
baselineDeadline;
|
|
2732
|
+
/**
|
|
2733
|
+
* The mode before any override — the tuning this manager was constructed
|
|
2734
|
+
* with, which already carries the validated config file (the host entry
|
|
2735
|
+
* passes `validateConfig`'s output) or the built-in default.
|
|
2736
|
+
*/
|
|
2737
|
+
baselineMode;
|
|
2561
2738
|
/**
|
|
2562
2739
|
* The settings page's own write, persisted with the agent registry. Merged
|
|
2563
|
-
* OVER {@link
|
|
2740
|
+
* OVER {@link baselineDeadline}: once a user picks a window in the UI, that
|
|
2564
2741
|
* pick is what runs, and the config file becomes the value the reset button
|
|
2565
2742
|
* returns to. Without this split a hand-edited `cordis.patch.yml` and a UI
|
|
2566
2743
|
* write would fight with no way to tell which one is live.
|
|
2567
2744
|
*/
|
|
2568
2745
|
deadlineOverride = {};
|
|
2569
2746
|
/**
|
|
2570
|
-
* Tuning in force, defaults applied. The
|
|
2747
|
+
* Tuning in force, defaults applied. The approval deadline is rewritten
|
|
2571
2748
|
* IN PLACE by {@link applyDeadlineTuning}, which is why this is a live object
|
|
2572
2749
|
* rather than the constructor's parameter: every bridge built afterwards
|
|
2573
2750
|
* inherits the rewrite instead of the boot-time value.
|
|
@@ -2624,13 +2801,11 @@ export class BridgeManager {
|
|
|
2624
2801
|
// `applyDeadlineTuning`), and every bridge built afterwards must inherit
|
|
2625
2802
|
// the rewrite instead of the boot-time value.
|
|
2626
2803
|
this.tuning = { ...DEFAULT_TUNING, ...tuning };
|
|
2627
|
-
// The pre-override deadline
|
|
2628
|
-
//
|
|
2629
|
-
// only this
|
|
2630
|
-
this.
|
|
2631
|
-
|
|
2632
|
-
permissionFallbackTimeoutMs: this.tuning.permissionFallbackTimeoutMs,
|
|
2633
|
-
};
|
|
2804
|
+
// The pre-override deadline value, snapshotted before anything can rewrite
|
|
2805
|
+
// it. Everything else in `tuning` is fixed for the manager's lifetime, so
|
|
2806
|
+
// only this needs a snapshot.
|
|
2807
|
+
this.baselineDeadline = this.tuning.permissionTimeoutMs;
|
|
2808
|
+
this.baselineMode = this.tuning.permissionTimeoutMode;
|
|
2634
2809
|
for (const def of defs) {
|
|
2635
2810
|
this.defsMap.set(def.id, def);
|
|
2636
2811
|
}
|
|
@@ -3469,7 +3644,31 @@ export class BridgeManager {
|
|
|
3469
3644
|
// validator accepts must be the value that runs (a UI write that did
|
|
3470
3645
|
// not survive a restart would read as a broken setting).
|
|
3471
3646
|
try {
|
|
3472
|
-
|
|
3647
|
+
const raw = parsed.approvalDeadlines ?? {};
|
|
3648
|
+
// The OLD card wrote the two legacy windows, and its 「关」 preset
|
|
3649
|
+
// wrote a zero — which then meant "this window never fires". The only
|
|
3650
|
+
// way to say that now is the `wait` MODE (a zero means 不等待 today),
|
|
3651
|
+
// so a legacy zero carries over as the mode instead of silently
|
|
3652
|
+
// turning "wait forever" into "answer immediately".
|
|
3653
|
+
const legacy = raw;
|
|
3654
|
+
const legacyWindow = legacy.nativeApprovalTimeoutMs ?? legacy.permissionFallbackTimeoutMs;
|
|
3655
|
+
const legacyNever = legacyWindow === 0;
|
|
3656
|
+
const window = legacyNever
|
|
3657
|
+
? undefined
|
|
3658
|
+
: validateApprovalDeadlines(raw).permissionTimeoutMs;
|
|
3659
|
+
const mode = legacyNever
|
|
3660
|
+
? 'wait'
|
|
3661
|
+
: (raw.mode === undefined
|
|
3662
|
+
? undefined
|
|
3663
|
+
: validatePermissionTimeoutMode(raw.mode, 'bad_payload'));
|
|
3664
|
+
// A stored window must be legal for the mode it runs under: 不等待
|
|
3665
|
+
// (0) belongs to `auto`; under `abort` it would refuse every ask on
|
|
3666
|
+
// arrival, so it floors to the shortest real wait.
|
|
3667
|
+
const effectiveMode = mode ?? this.baselineMode;
|
|
3668
|
+
this.deadlineOverride = {
|
|
3669
|
+
...(window === undefined ? {} : { permissionTimeoutMs: normalizeApprovalWindow(effectiveMode, window) }),
|
|
3670
|
+
...(mode === undefined ? {} : { mode }),
|
|
3671
|
+
};
|
|
3473
3672
|
}
|
|
3474
3673
|
catch {
|
|
3475
3674
|
this.deadlineOverride = {};
|
|
@@ -4527,7 +4726,7 @@ export class BridgeManager {
|
|
|
4527
4726
|
this.persistAgents();
|
|
4528
4727
|
}
|
|
4529
4728
|
/**
|
|
4530
|
-
* Declare the deployment baseline for the
|
|
4729
|
+
* Declare the deployment baseline for the approval deadline — the values
|
|
4531
4730
|
* `cordis.patch.yml` names, passed RAW (only the keys it actually contains).
|
|
4532
4731
|
*
|
|
4533
4732
|
* Kept separate from the validated tuning because "the file says 600000" and
|
|
@@ -4537,36 +4736,46 @@ export class BridgeManager {
|
|
|
4537
4736
|
* also moves the baseline, so a manager built without tuning (a direct
|
|
4538
4737
|
* consumer, a test) still runs what the deployment asked for.
|
|
4539
4738
|
*/
|
|
4540
|
-
setConfiguredApprovalDeadlines(values) {
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4739
|
+
setConfiguredApprovalDeadlines(values, mode) {
|
|
4740
|
+
// The baseline must be legal for the mode it runs under, or the card would
|
|
4741
|
+
// advertise a number the deployment cannot actually run (不等待 under
|
|
4742
|
+
// `abort`) and the reset button would restore it.
|
|
4743
|
+
const effectiveMode = mode ?? this.configuredMode ?? this.baselineMode;
|
|
4744
|
+
const raw = values.permissionTimeoutMs;
|
|
4745
|
+
const window = raw === undefined ? undefined : normalizeApprovalWindow(effectiveMode, raw);
|
|
4746
|
+
this.configuredDeadlines = window === undefined ? {} : { permissionTimeoutMs: window };
|
|
4747
|
+
if (window !== undefined)
|
|
4748
|
+
this.baselineDeadline = window;
|
|
4749
|
+
if (mode !== undefined) {
|
|
4750
|
+
this.configuredMode = mode;
|
|
4751
|
+
this.baselineMode = mode;
|
|
4752
|
+
}
|
|
4546
4753
|
this.applyDeadlineTuning();
|
|
4547
4754
|
}
|
|
4548
4755
|
/**
|
|
4549
|
-
* The approval
|
|
4550
|
-
* user did not pick: the config-file baseline an override is outranking
|
|
4551
|
-
*
|
|
4552
|
-
*
|
|
4553
|
-
*
|
|
4756
|
+
* The approval deadline in force, plus the facts that explain a value the
|
|
4757
|
+
* user did not pick: the config-file baseline an override is outranking. The
|
|
4758
|
+
* mode rides along with its own baseline, for the same reason: the card has
|
|
4759
|
+
* to be able to say "the file asks for `abort`, the value set here wins".
|
|
4760
|
+
*
|
|
4761
|
+
* Everything is reported only when it is actually in play, so the card can
|
|
4762
|
+
* stay silent about a plain default.
|
|
4554
4763
|
*/
|
|
4555
4764
|
approvalDeadlines() {
|
|
4556
4765
|
const deadlines = {
|
|
4557
|
-
|
|
4558
|
-
|
|
4766
|
+
permissionTimeoutMs: this.tuning.permissionTimeoutMs,
|
|
4767
|
+
mode: this.tuning.permissionTimeoutMode,
|
|
4559
4768
|
};
|
|
4560
4769
|
if (Object.keys(this.configuredDeadlines).length > 0)
|
|
4561
4770
|
deadlines.configured = { ...this.configuredDeadlines };
|
|
4562
|
-
if (this.
|
|
4563
|
-
deadlines.
|
|
4771
|
+
if (this.configuredMode !== undefined)
|
|
4772
|
+
deadlines.configuredMode = this.configuredMode;
|
|
4564
4773
|
return deadlines;
|
|
4565
4774
|
}
|
|
4566
4775
|
/**
|
|
4567
|
-
* Write
|
|
4568
|
-
* drop the persisted override with `reset` so the config-file baseline —
|
|
4569
|
-
* the built-in default — applies again.
|
|
4776
|
+
* Write the approval deadline, or the mode (omitted keys keep their value),
|
|
4777
|
+
* or drop the persisted override with `reset` so the config-file baseline —
|
|
4778
|
+
* or the built-in default — applies again.
|
|
4570
4779
|
*
|
|
4571
4780
|
* HOT: the rewrite reaches every live bridge, so a user changing the window
|
|
4572
4781
|
* never has to kill a running session to see it take effect (the one thing an
|
|
@@ -4577,25 +4786,35 @@ export class BridgeManager {
|
|
|
4577
4786
|
async setApprovalDeadlines(patch, reset = false) {
|
|
4578
4787
|
// Merged BEFORE validating, so the whole resulting override passes the same
|
|
4579
4788
|
// gate the config file does (an override written by an older build is
|
|
4580
|
-
// re-gated here rather than trusted).
|
|
4581
|
-
//
|
|
4789
|
+
// re-gated here rather than trusted). The validators also drop unknown
|
|
4790
|
+
// keys, which is what keeps a stale payload from poisoning state.
|
|
4582
4791
|
const merged = reset ? {} : { ...this.deadlineOverride, ...patch };
|
|
4583
|
-
|
|
4792
|
+
const window = validateApprovalDeadlines(merged).permissionTimeoutMs;
|
|
4793
|
+
const mode = merged.mode === undefined
|
|
4794
|
+
? undefined
|
|
4795
|
+
: validatePermissionTimeoutMode(merged.mode, 'bad_payload');
|
|
4796
|
+
// The stored override must be legal for the mode it runs under, not just
|
|
4797
|
+
// legal on its own: 不等待 (0) belongs to `auto`, and a mode switch can
|
|
4798
|
+
// leave it behind. Flooring it HERE (not only in the running value) keeps
|
|
4799
|
+
// the card's number and the running number the same number.
|
|
4800
|
+
const effectiveMode = mode ?? this.baselineMode;
|
|
4801
|
+
this.deadlineOverride = {
|
|
4802
|
+
...(window === undefined ? {} : { permissionTimeoutMs: normalizeApprovalWindow(effectiveMode, window) }),
|
|
4803
|
+
...(mode === undefined ? {} : { mode }),
|
|
4804
|
+
};
|
|
4584
4805
|
this.applyDeadlineTuning();
|
|
4585
4806
|
this.persistAgents();
|
|
4586
4807
|
}
|
|
4587
4808
|
/**
|
|
4588
|
-
* Recompute the
|
|
4589
|
-
* the result to every live bridge.
|
|
4809
|
+
* Recompute the deadline and the mode from override → deployment baseline,
|
|
4810
|
+
* and push the result to every live bridge.
|
|
4590
4811
|
*/
|
|
4591
4812
|
applyDeadlineTuning() {
|
|
4592
|
-
const
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
this.tuning.permissionFallbackTimeoutMs = permissionFallbackTimeoutMs;
|
|
4598
|
-
const patch = { nativeApprovalTimeoutMs, permissionFallbackTimeoutMs };
|
|
4813
|
+
const permissionTimeoutMode = this.deadlineOverride.mode ?? this.baselineMode;
|
|
4814
|
+
const permissionTimeoutMs = normalizeApprovalWindow(permissionTimeoutMode, this.deadlineOverride.permissionTimeoutMs ?? this.baselineDeadline);
|
|
4815
|
+
this.tuning.permissionTimeoutMs = permissionTimeoutMs;
|
|
4816
|
+
this.tuning.permissionTimeoutMode = permissionTimeoutMode;
|
|
4817
|
+
const patch = { permissionTimeoutMs, mode: permissionTimeoutMode };
|
|
4599
4818
|
for (const bridge of this.bridges.values())
|
|
4600
4819
|
bridge.applyApprovalDeadlines(patch);
|
|
4601
4820
|
}
|