dsh-ntr 0.1.5 → 0.2.0
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 +4 -2
- package/README.md +4 -2
- package/cordis.patch.yml +56 -28
- package/lib/bridge.js +378 -330
- package/lib/client/acp-ui.js +60 -32
- package/lib/client/index.js +17 -0
- package/lib/client/panel.js +257 -103
- package/lib/client/permission-dock.js +72 -0
- package/lib/client/session-controls.js +4 -4
- package/lib/client/store.js +88 -22
- package/lib/client.js +584 -226
- package/lib/client.js.map +1 -1
- package/lib/impl.js +128 -45
- package/lib/index.js +20 -52
- package/lib/types/bridge.d.ts +164 -214
- package/lib/types/client/panel.d.ts +59 -20
- package/lib/types/client/permission-dock.d.ts +31 -0
- package/lib/types/client/store.d.ts +52 -15
- package/lib/types/impl.d.ts +93 -27
- package/lib/types/protocol.d.ts +137 -52
- package/package.json +1 -1
- package/src/bridge.ts +433 -417
- package/src/client/acp-ui.tsx +60 -32
- package/src/client/index.tsx +18 -0
- package/src/client/panel.tsx +395 -197
- package/src/client/permission-dock.tsx +133 -0
- package/src/client/session-controls.tsx +4 -4
- package/src/client/store.ts +112 -36
- package/src/impl.ts +166 -82
- package/src/index.ts +20 -65
- package/src/protocol.ts +143 -52
package/src/impl.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { randomUUID } from './ids.ts'
|
|
9
9
|
import type {
|
|
10
10
|
AcpAgentCatalog, AcpAgentDef, AcpCatalogOption, AcpApprovalDeadlines, AcpConversationChoice, AcpEvent, AcpMcpServerDef, AcpPluginConfig, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentTestResult,
|
|
11
|
-
AgentView, ApprovalDeadlineKey, ChatSessionView, ChatTranscriptView, DetectedAgentInfo, PermissionOptionView, PermissionOutcome, RpcResult,
|
|
11
|
+
AgentView, ApprovalDeadlineKey, ApprovalDeadlinePatch, ChatSessionView, ChatTranscriptView, DetectedAgentInfo, PermissionOptionView, PermissionOutcome, PermissionTimeoutMode, RpcResult,
|
|
12
12
|
} from './protocol.ts'
|
|
13
13
|
import { DEFAULT_TAG_HALO } from './protocol.ts'
|
|
14
14
|
|
|
@@ -103,6 +103,41 @@ export function validateTagHalo(value: unknown): AcpTagHalo {
|
|
|
103
103
|
*/
|
|
104
104
|
export const APPROVAL_DEADLINE_MAX_MS = 86_400_000
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* The shortest REAL wait: the first rung of the card's ladder (30 seconds).
|
|
108
|
+
*
|
|
109
|
+
* The floor for a mode that must not wait zero — see
|
|
110
|
+
* {@link normalizeApprovalWindow}.
|
|
111
|
+
*/
|
|
112
|
+
export const APPROVAL_DEADLINE_MIN_WAIT_MS = 30_000
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Make a window legal for the mode it runs under.
|
|
116
|
+
*
|
|
117
|
+
* `0` means 不等待 — the ask is settled the moment it arrives instead of
|
|
118
|
+
* waiting. That is the **auto** mode's business: `auto` answers with the
|
|
119
|
+
* option the agent's own kinds imply, so the agent CONTINUES, and an
|
|
120
|
+
* unattended deployment gets a sane default instead of a hang.
|
|
121
|
+
*
|
|
122
|
+
* Under `abort` the same zero would refuse every ask the instant it arrived:
|
|
123
|
+
* the agent stops at every gate it meets, which is not a timeout, it is a
|
|
124
|
+
* brick wall. So `abort` never offers 不等待, and a window of `0` reaching it
|
|
125
|
+
* (a config file, a state file written by an older build, a mode switch that
|
|
126
|
+
* left the window behind) normalizes to the shortest real wait.
|
|
127
|
+
*
|
|
128
|
+
* `wait` ignores the window entirely — it never expires.
|
|
129
|
+
*/
|
|
130
|
+
export function normalizeApprovalWindow(mode: PermissionTimeoutMode, ms: number): number {
|
|
131
|
+
return mode === 'abort' && ms === 0 ? APPROVAL_DEADLINE_MIN_WAIT_MS : ms
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The order `auto` mode picks an option in, by the kind the agent declared:
|
|
136
|
+
* the broadest grant first, then the narrow one, then the refusals, then
|
|
137
|
+
* whatever the agent offered instead. See {@link pickAutoOption}.
|
|
138
|
+
*/
|
|
139
|
+
export const AUTO_OPTION_KIND_PRIORITY = ['allow_always', 'allow_once', 'reject_once', 'reject_always'] as const
|
|
140
|
+
|
|
106
141
|
/**
|
|
107
142
|
* Validate ONE approval deadline, shared by every layer that accepts one.
|
|
108
143
|
*
|
|
@@ -122,35 +157,62 @@ export function validateApprovalDeadlineMs(raw: unknown, label: string, code: 'b
|
|
|
122
157
|
|
|
123
158
|
/**
|
|
124
159
|
* Validate a settings-page deadline write. Omitted keys are simply absent from
|
|
125
|
-
* the result — the manager merges, so a card that only
|
|
126
|
-
*
|
|
160
|
+
* the result — the manager merges, so a card that only moved the mode must not
|
|
161
|
+
* reset the number. `reset` is not a deadline and is dropped here.
|
|
127
162
|
*/
|
|
128
163
|
export function validateApprovalDeadlines(value: unknown): Partial<Record<ApprovalDeadlineKey, number>> {
|
|
129
164
|
if (value === null || typeof value !== 'object') throw new AcpError('bad_payload', 'acp: approval deadlines must be an object')
|
|
130
|
-
const {
|
|
131
|
-
nativeApprovalTimeoutMs?: unknown
|
|
132
|
-
permissionFallbackTimeoutMs?: unknown
|
|
133
|
-
}
|
|
165
|
+
const { permissionTimeoutMs } = value as { permissionTimeoutMs?: unknown }
|
|
134
166
|
const out: Partial<Record<ApprovalDeadlineKey, number>> = {}
|
|
135
|
-
if (
|
|
136
|
-
out.
|
|
137
|
-
|
|
138
|
-
'
|
|
167
|
+
if (permissionTimeoutMs !== undefined) {
|
|
168
|
+
out.permissionTimeoutMs = validateApprovalDeadlineMs(
|
|
169
|
+
permissionTimeoutMs,
|
|
170
|
+
'permissionTimeoutMs',
|
|
139
171
|
'bad_payload',
|
|
140
|
-
'(0 =
|
|
141
|
-
)
|
|
142
|
-
}
|
|
143
|
-
if (permissionFallbackTimeoutMs !== undefined) {
|
|
144
|
-
out.permissionFallbackTimeoutMs = validateApprovalDeadlineMs(
|
|
145
|
-
permissionFallbackTimeoutMs,
|
|
146
|
-
'permissionFallbackTimeoutMs',
|
|
147
|
-
'bad_payload',
|
|
148
|
-
'(0 = never answer a degraded ask on the human\'s behalf)',
|
|
172
|
+
'(0 = 不等待,仅自动推进档可用)',
|
|
149
173
|
)
|
|
150
174
|
}
|
|
151
175
|
return out
|
|
152
176
|
}
|
|
153
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Validate the mode a write asks for. Three values, and the list is the whole
|
|
180
|
+
* vocabulary: an unanswered ask is either answered for you, kept waiting, or
|
|
181
|
+
* refused. Anything else is a typo, not a new mode.
|
|
182
|
+
*/
|
|
183
|
+
export function validatePermissionTimeoutMode(raw: unknown, code: 'bad_config' | 'bad_payload'): PermissionTimeoutMode {
|
|
184
|
+
if (raw !== 'abort' && raw !== 'auto' && raw !== 'wait') {
|
|
185
|
+
throw new AcpError(code, 'acp: permissionTimeoutMode must be one of "abort", "auto", "wait"')
|
|
186
|
+
}
|
|
187
|
+
return raw
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The option an expired deadline answers with in `auto` mode.
|
|
192
|
+
*
|
|
193
|
+
* The pick is by the option's KIND, because that is what the agent declared
|
|
194
|
+
* about the choice it is offering: `allow_always` first (one grant covers the
|
|
195
|
+
* rest of the run), then `allow_once` (the smaller grant), then the reject
|
|
196
|
+
* kinds (refuse but let the agent continue), and finally the first option
|
|
197
|
+
* whatever it is — an agent may offer only a custom kind, and refusing to
|
|
198
|
+
* answer at all would hang the turn.
|
|
199
|
+
*
|
|
200
|
+
* `undefined` means NO option could be read (no options, or none with a usable
|
|
201
|
+
* id). The caller then degrades to `abort`: guessing an option is worse than
|
|
202
|
+
* refusing, and hanging is worse than either. OmniTerm dev picks the same way
|
|
203
|
+
* (`pick_auto_option`, `src/acp/permission.rs`).
|
|
204
|
+
*/
|
|
205
|
+
export function pickAutoOption(options: readonly PermissionOptionView[]): PermissionOptionView | undefined {
|
|
206
|
+
const usable = (option: PermissionOptionView): PermissionOptionView | undefined =>
|
|
207
|
+
option.optionId === '' ? undefined : option
|
|
208
|
+
for (const kind of AUTO_OPTION_KIND_PRIORITY) {
|
|
209
|
+
const hit = options.find(option => option.kind === kind)
|
|
210
|
+
const picked = hit === undefined ? undefined : usable(hit)
|
|
211
|
+
if (picked !== undefined) return picked
|
|
212
|
+
}
|
|
213
|
+
return options.length === 0 ? undefined : usable(options[0] as PermissionOptionView)
|
|
214
|
+
}
|
|
215
|
+
|
|
154
216
|
/**
|
|
155
217
|
* Bound on the number of MCP servers one agent may declare. An MCP server is
|
|
156
218
|
* a process (stdio) or a remote endpoint the agent connects to at session
|
|
@@ -437,27 +499,30 @@ export function validateConfig(raw: unknown): AcpPluginConfig {
|
|
|
437
499
|
if (typeof startupTimeoutMs !== 'number' || !Number.isFinite(startupTimeoutMs) || startupTimeoutMs < 1_000 || startupTimeoutMs > 600_000) {
|
|
438
500
|
throw new AcpError('bad_config', 'acp: startupTimeoutMs must be a number in [1000, 600000]')
|
|
439
501
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
//
|
|
445
|
-
// the
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
|
|
449
|
-
const
|
|
450
|
-
value.
|
|
451
|
-
'
|
|
452
|
-
'bad_config',
|
|
453
|
-
'(0 = wait for the host approval chain forever)',
|
|
454
|
-
)
|
|
455
|
-
const permissionFallbackTimeoutMs = validateApprovalDeadlineMs(
|
|
456
|
-
value.permissionFallbackTimeoutMs ?? 300_000,
|
|
457
|
-
'permissionFallbackTimeoutMs',
|
|
502
|
+
// ONE wait window for both routes. The two legacy keys are folded in when
|
|
503
|
+
// the new one is absent, so an existing `cordis.patch.yml` keeps meaning what
|
|
504
|
+
// it meant instead of silently falling back to the default.
|
|
505
|
+
const legacyWindow = value.nativeApprovalTimeoutMs ?? value.permissionFallbackTimeoutMs
|
|
506
|
+
// A legacy window of 0 meant "this window never fires". The only way to say
|
|
507
|
+
// that now is the `wait` MODE (0 is 不等待 today), so a legacy zero carries
|
|
508
|
+
// over as the mode — otherwise an unattended deployment would silently turn
|
|
509
|
+
// "wait forever" into "answer immediately".
|
|
510
|
+
const legacyNever = legacyWindow === 0
|
|
511
|
+
const permissionTimeoutMs = validateApprovalDeadlineMs(
|
|
512
|
+
legacyNever ? 300_000 : (value.permissionTimeoutMs ?? legacyWindow ?? 300_000),
|
|
513
|
+
'permissionTimeoutMs',
|
|
458
514
|
'bad_config',
|
|
459
|
-
'(0 =
|
|
515
|
+
'(0 = 不等待,仅自动推进档可用)',
|
|
460
516
|
)
|
|
517
|
+
// What an expired deadline DOES. Same validator as the settings-page write,
|
|
518
|
+
// so a mode the panel can offer is a mode the next boot accepts.
|
|
519
|
+
const permissionTimeoutMode = legacyNever
|
|
520
|
+
? 'wait'
|
|
521
|
+
: validatePermissionTimeoutMode(value.permissionTimeoutMode ?? 'wait', 'bad_config')
|
|
522
|
+
// 不等待 belongs to `auto` (it answers, so the agent continues). Under
|
|
523
|
+
// `abort` a zero window would refuse every ask on arrival — the agent stops
|
|
524
|
+
// at every gate — so it becomes the shortest real wait.
|
|
525
|
+
const effectivePermissionTimeoutMs = normalizeApprovalWindow(permissionTimeoutMode, permissionTimeoutMs)
|
|
461
526
|
const cancelFallbackMs = value.cancelFallbackMs ?? 15_000
|
|
462
527
|
if (typeof cancelFallbackMs !== 'number' || !Number.isInteger(cancelFallbackMs) || cancelFallbackMs < 0 || cancelFallbackMs > 600_000) {
|
|
463
528
|
throw new AcpError('bad_config', 'acp: cancelFallbackMs must be an integer in [0, 600000] (0 = disable the fallback)')
|
|
@@ -475,9 +540,8 @@ export function validateConfig(raw: unknown): AcpPluginConfig {
|
|
|
475
540
|
transcriptMaxEntries: max,
|
|
476
541
|
probeModelsOnBoot,
|
|
477
542
|
startupTimeoutMs,
|
|
478
|
-
permissionTimeoutMs,
|
|
479
|
-
|
|
480
|
-
permissionFallbackTimeoutMs,
|
|
543
|
+
permissionTimeoutMs: effectivePermissionTimeoutMs,
|
|
544
|
+
permissionTimeoutMode,
|
|
481
545
|
cancelFallbackMs,
|
|
482
546
|
staleCancelRetryDelayMs,
|
|
483
547
|
staleCancelMaxRetries,
|
|
@@ -1515,6 +1579,22 @@ export class EventHub {
|
|
|
1515
1579
|
|
|
1516
1580
|
// ---- Permission ledger ----
|
|
1517
1581
|
|
|
1582
|
+
/** Everything a `permission/request` frame carries besides the ids the ledger owns. */
|
|
1583
|
+
export interface PendingPermissionFields {
|
|
1584
|
+
/** DSH chat session id; absent for `/acp` RPC-only sessions. */
|
|
1585
|
+
dshSessionId?: string
|
|
1586
|
+
toolCall: unknown
|
|
1587
|
+
/** Host-derived human name (`permissionToolName`). */
|
|
1588
|
+
toolName: string
|
|
1589
|
+
/** One-line detail from the agent's declared input, when it has any. */
|
|
1590
|
+
reason?: string
|
|
1591
|
+
options: PermissionOptionView[]
|
|
1592
|
+
/** What an unanswered ask does when its deadline fires. */
|
|
1593
|
+
timeoutMode: PermissionTimeoutMode
|
|
1594
|
+
/** Epoch ms of the deadline; absent in `wait` mode (no deadline). */
|
|
1595
|
+
deadlineAt?: number
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1518
1598
|
export interface PendingPermission {
|
|
1519
1599
|
requestId: string
|
|
1520
1600
|
sessionId: string
|
|
@@ -1524,15 +1604,13 @@ export interface PendingPermission {
|
|
|
1524
1604
|
* client receives (see `permissionReplayFrames`). Absent on entries created
|
|
1525
1605
|
* without a view — those still answer, they just cannot be re-shown.
|
|
1526
1606
|
*/
|
|
1527
|
-
view?:
|
|
1607
|
+
view?: PendingPermissionFields
|
|
1528
1608
|
}
|
|
1529
1609
|
|
|
1530
1610
|
/** One outstanding ask as a reconnecting client must be re-shown it. */
|
|
1531
|
-
export interface PendingPermissionView {
|
|
1611
|
+
export interface PendingPermissionView extends PendingPermissionFields {
|
|
1532
1612
|
requestId: string
|
|
1533
1613
|
sessionId: string
|
|
1534
|
-
toolCall: unknown
|
|
1535
|
-
options: PermissionOptionView[]
|
|
1536
1614
|
}
|
|
1537
1615
|
|
|
1538
1616
|
export class PermissionLedger {
|
|
@@ -1584,14 +1662,37 @@ export class PermissionLedger {
|
|
|
1584
1662
|
views.push({
|
|
1585
1663
|
requestId: entry.requestId,
|
|
1586
1664
|
sessionId: entry.sessionId,
|
|
1587
|
-
|
|
1588
|
-
options: entry.view.options,
|
|
1665
|
+
...entry.view,
|
|
1589
1666
|
})
|
|
1590
1667
|
}
|
|
1591
1668
|
return views
|
|
1592
1669
|
}
|
|
1593
1670
|
}
|
|
1594
1671
|
|
|
1672
|
+
/**
|
|
1673
|
+
* The `permission/request` frame for one ask.
|
|
1674
|
+
*
|
|
1675
|
+
* ONE builder for both the live broadcast and the reconnect replay, so the two
|
|
1676
|
+
* can never drift: a client that reconnects must receive exactly the frame the
|
|
1677
|
+
* live one carried (its conversation card filters on `dshSessionId` and shows
|
|
1678
|
+
* `toolName` / `deadlineAt`, so a replay missing them would silently drop the
|
|
1679
|
+
* ask).
|
|
1680
|
+
*/
|
|
1681
|
+
export function permissionRequestFrame(view: PendingPermissionView): AcpEvent {
|
|
1682
|
+
return {
|
|
1683
|
+
type: 'permission/request',
|
|
1684
|
+
requestId: view.requestId,
|
|
1685
|
+
sessionId: view.sessionId,
|
|
1686
|
+
...(view.dshSessionId === undefined ? {} : { dshSessionId: view.dshSessionId }),
|
|
1687
|
+
toolCall: view.toolCall,
|
|
1688
|
+
toolName: view.toolName,
|
|
1689
|
+
...(view.reason === undefined ? {} : { reason: view.reason }),
|
|
1690
|
+
options: view.options,
|
|
1691
|
+
timeoutMode: view.timeoutMode,
|
|
1692
|
+
...(view.deadlineAt === undefined ? {} : { deadlineAt: view.deadlineAt }),
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1595
1696
|
/**
|
|
1596
1697
|
* Frames that re-establish a freshly connected client's view of the pending
|
|
1597
1698
|
* permission asks: every outstanding ask re-sent as its original
|
|
@@ -1599,10 +1700,10 @@ export class PermissionLedger {
|
|
|
1599
1700
|
*
|
|
1600
1701
|
* Why a replay at all: a `permission/request` is broadcast ONCE, when the ask is
|
|
1601
1702
|
* created. A client that connects later — page reload, second tab, stream
|
|
1602
|
-
* outage — never receives it, and
|
|
1603
|
-
*
|
|
1604
|
-
*
|
|
1605
|
-
*
|
|
1703
|
+
* outage — never receives it, and that card (conversation or settings ledger)
|
|
1704
|
+
* is the ONLY answer entry for the ask, so the ask is invisible to exactly the
|
|
1705
|
+
* client that needs it. OmniTerm closes the same hole with
|
|
1706
|
+
* `PermissionManager::pending_events()` + a `PermissionsSynced` marker on
|
|
1606
1707
|
* every WS connect.
|
|
1607
1708
|
*
|
|
1608
1709
|
* Why the terminator is mandatory: the replay is a snapshot, so an ask resolved
|
|
@@ -1611,13 +1712,7 @@ export class PermissionLedger {
|
|
|
1611
1712
|
* `respond` would answer `unknown_request`.
|
|
1612
1713
|
*/
|
|
1613
1714
|
export function permissionReplayFrames(pending: readonly PendingPermissionView[]): AcpEvent[] {
|
|
1614
|
-
const requests: AcpEvent[] = pending.map(
|
|
1615
|
-
type: 'permission/request' as const,
|
|
1616
|
-
requestId: p.requestId,
|
|
1617
|
-
sessionId: p.sessionId,
|
|
1618
|
-
toolCall: p.toolCall,
|
|
1619
|
-
options: p.options,
|
|
1620
|
-
}))
|
|
1715
|
+
const requests: AcpEvent[] = pending.map(permissionRequestFrame)
|
|
1621
1716
|
return [...requests, { type: 'permission/synced', requestIds: pending.map(p => p.requestId) }]
|
|
1622
1717
|
}
|
|
1623
1718
|
|
|
@@ -1650,18 +1745,18 @@ export interface AcpManager {
|
|
|
1650
1745
|
/** Write the halo preference (persisted with the agent registry). */
|
|
1651
1746
|
setTagHalo?(halo: AcpTagHalo): Promise<void>
|
|
1652
1747
|
/**
|
|
1653
|
-
* The approval
|
|
1654
|
-
* (the `cordis.patch.yml` baseline it outranks
|
|
1655
|
-
*
|
|
1656
|
-
*
|
|
1748
|
+
* The approval deadline in force, with the facts that explain an override
|
|
1749
|
+
* (the `cordis.patch.yml` baseline it outranks). `undefined` on a host half
|
|
1750
|
+
* from before the card existed, which hides the settings card instead of
|
|
1751
|
+
* guessing a value.
|
|
1657
1752
|
*/
|
|
1658
1753
|
approvalDeadlines?(): AcpApprovalDeadlines | undefined
|
|
1659
1754
|
/**
|
|
1660
|
-
* Write
|
|
1661
|
-
* drop the persisted override with `reset` so the config-file baseline
|
|
1755
|
+
* Write the approval deadline, or the mode (omitted keys keep their value),
|
|
1756
|
+
* or drop the persisted override with `reset` so the config-file baseline
|
|
1662
1757
|
* applies again. Applies to the running bridges without a restart.
|
|
1663
1758
|
*/
|
|
1664
|
-
setApprovalDeadlines?(patch:
|
|
1759
|
+
setApprovalDeadlines?(patch: ApprovalDeadlinePatch, reset?: boolean): Promise<void>
|
|
1665
1760
|
detectAgents?(): Promise<DetectedAgentInfo[]>
|
|
1666
1761
|
saveAgent?(def: AcpAgentDef): Promise<void>
|
|
1667
1762
|
deleteAgent?(agentId: string): Promise<void>
|
|
@@ -1697,12 +1792,6 @@ export interface AcpManager {
|
|
|
1697
1792
|
* cache (newly published models appear, withdrawn ones drop out).
|
|
1698
1793
|
*/
|
|
1699
1794
|
cachedAgentModels?(agentId: string): AgentProbeModelsResult | undefined
|
|
1700
|
-
/**
|
|
1701
|
-
* True when ACP permission asks are routed to the host's native approval
|
|
1702
|
-
* service (answered in the chat composer) instead of the settings-panel
|
|
1703
|
-
* ledger. Read-only capability report for `agents/list`.
|
|
1704
|
-
*/
|
|
1705
|
-
approvalRouting?(): boolean
|
|
1706
1795
|
/**
|
|
1707
1796
|
* ACP agent sessions backing one DSH chat session (looked up by chat session
|
|
1708
1797
|
* id), so the composer can offer the agent's OWN mode list. Empty when no
|
|
@@ -1817,12 +1906,7 @@ export async function dispatchRpc(manager: AcpManager, endpoint: string, payload
|
|
|
1817
1906
|
try {
|
|
1818
1907
|
switch (endpoint) {
|
|
1819
1908
|
case 'agents/list':
|
|
1820
|
-
|
|
1821
|
-
// uses it to decide whether the settings panel still owns approvals.
|
|
1822
|
-
return ok({
|
|
1823
|
-
agents: manager.agentViews(),
|
|
1824
|
-
approvalRouted: manager.approvalRouting?.() ?? false,
|
|
1825
|
-
})
|
|
1909
|
+
return ok({ agents: manager.agentViews() })
|
|
1826
1910
|
case 'agents/detect':
|
|
1827
1911
|
return ok({ agents: await (manager.detectAgents?.() ?? Promise.resolve([])) })
|
|
1828
1912
|
case 'agent/save': {
|
|
@@ -1985,10 +2069,10 @@ export async function dispatchRpc(manager: AcpManager, endpoint: string, payload
|
|
|
1985
2069
|
// renders nothing rather than a number it cannot trust.
|
|
1986
2070
|
return ok({ deadlines: manager.approvalDeadlines?.() })
|
|
1987
2071
|
case 'ui/approval-deadlines-set': {
|
|
1988
|
-
const patch =
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
}
|
|
2072
|
+
const patch: ApprovalDeadlinePatch = {
|
|
2073
|
+
...validateApprovalDeadlines({ permissionTimeoutMs: body.permissionTimeoutMs }),
|
|
2074
|
+
...(body.mode === undefined ? {} : { mode: validatePermissionTimeoutMode(body.mode, 'bad_payload') }),
|
|
2075
|
+
}
|
|
1992
2076
|
await manager.setApprovalDeadlines?.(patch, body.reset === true)
|
|
1993
2077
|
return ok({ deadlines: manager.approvalDeadlines?.() })
|
|
1994
2078
|
}
|
package/src/index.ts
CHANGED
|
@@ -11,32 +11,15 @@ import type { ConnectionRpcHandler, HostConnectionHandle } from '@deepseek-ai/ds
|
|
|
11
11
|
import { ACP_PRESET_ID, AcpLlmAdapter, BridgeManager } from './bridge.ts'
|
|
12
12
|
import { EventHub, dispatchRpc, permissionReplayFrames, sessionAgentPreset, sessionAgentPresetFromLog, validateConfig } from './impl.ts'
|
|
13
13
|
import type { AcpEvent, ApprovalDeadlineKey } from './protocol.ts'
|
|
14
|
-
import type { ApprovalOutcome } from './bridge.ts'
|
|
15
14
|
import type { RosterPresetRow, SessionLogLike } from './impl.ts'
|
|
16
15
|
|
|
17
16
|
export const name = 'dsh-ntr'
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
17
|
+
// Deliberately minimal: every other host service is probed structurally
|
|
18
|
+
// (`ctx.get` + a late `ctx.inject`) so a deployment without one still loads
|
|
19
|
+
// this plugin. ACP permission asks are answered through this plugin's own
|
|
20
|
+
// ledger, so the host approval service is not a collaborator at all.
|
|
22
21
|
export const inject = ['webServer', 'connection']
|
|
23
22
|
|
|
24
|
-
/** Structural view of the host approval service (`@deepseek-ai/dsh-user-approval`). */
|
|
25
|
-
interface ApprovalServiceLike {
|
|
26
|
-
request(req: { agent: unknown; toolName: string; reason?: string; signal?: AbortSignal }): Promise<ApprovalOutcome>
|
|
27
|
-
/**
|
|
28
|
-
* Present on the host service, but NEVER read by this plugin: the only call
|
|
29
|
-
* made here is `request` (see `wireApproval`). A deployment-wide default
|
|
30
|
-
* policy is the host's own business.
|
|
31
|
-
*/
|
|
32
|
-
config?: { policy?: string }
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Structural view of the host agent registry (`ctx.agents`), keyed by chat session id. */
|
|
36
|
-
interface AgentsServiceLike {
|
|
37
|
-
get(sessionId: string): unknown | undefined
|
|
38
|
-
}
|
|
39
|
-
|
|
40
23
|
/** Structural view of the host sandbox policy service (`@deepseek-ai/dsh-sandbox-policy`). */
|
|
41
24
|
interface SandboxPolicyLike {
|
|
42
25
|
resolve(request: { session?: unknown }): { mode?: unknown; workspaceRoot?: unknown }
|
|
@@ -99,8 +82,7 @@ export function apply(ctx: Context, config?: unknown): void {
|
|
|
99
82
|
{
|
|
100
83
|
startupTimeoutMs: resolved.startupTimeoutMs,
|
|
101
84
|
permissionTimeoutMs: resolved.permissionTimeoutMs,
|
|
102
|
-
|
|
103
|
-
permissionFallbackTimeoutMs: resolved.permissionFallbackTimeoutMs,
|
|
85
|
+
permissionTimeoutMode: resolved.permissionTimeoutMode,
|
|
104
86
|
cancelFallbackMs: resolved.cancelFallbackMs,
|
|
105
87
|
staleCancelRetryDelayMs: resolved.staleCancelRetryDelayMs,
|
|
106
88
|
staleCancelMaxRetries: resolved.staleCancelMaxRetries,
|
|
@@ -115,11 +97,20 @@ export function apply(ctx: Context, config?: unknown): void {
|
|
|
115
97
|
// returns to). Only the keys the file actually names are declared.
|
|
116
98
|
const rawConfig = (config ?? {}) as Record<string, unknown>
|
|
117
99
|
const configuredDeadlines: Partial<Record<ApprovalDeadlineKey, number>> = {}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
100
|
+
// The window the file actually named — the new key, or the legacy one it
|
|
101
|
+
// folded into (same order `validateConfig` resolves them in, so the card's
|
|
102
|
+
// baseline and the running value never disagree).
|
|
103
|
+
const rawWindow = rawConfig.permissionTimeoutMs
|
|
104
|
+
?? rawConfig.nativeApprovalTimeoutMs
|
|
105
|
+
?? rawConfig.permissionFallbackTimeoutMs
|
|
106
|
+
if (typeof rawWindow === 'number' && Number.isFinite(rawWindow)) {
|
|
107
|
+
configuredDeadlines.permissionTimeoutMs = rawWindow
|
|
121
108
|
}
|
|
122
|
-
|
|
109
|
+
// The mode is declared the same way: only when the file actually names it.
|
|
110
|
+
const configuredMode = rawConfig.permissionTimeoutMode === 'abort' || rawConfig.permissionTimeoutMode === 'auto' || rawConfig.permissionTimeoutMode === 'wait'
|
|
111
|
+
? rawConfig.permissionTimeoutMode
|
|
112
|
+
: undefined
|
|
113
|
+
manager.setConfiguredApprovalDeadlines(configuredDeadlines, configuredMode)
|
|
123
114
|
// Boot hook kept only so this entry point's shape does not change:
|
|
124
115
|
// `bootstrapCatalogs` is intentionally empty — boot never spawns an agent,
|
|
125
116
|
// in any environment and for any `probeModelsOnBoot` value. Catalogs fill
|
|
@@ -158,34 +149,6 @@ export function apply(ctx: Context, config?: unknown): void {
|
|
|
158
149
|
ctx.inject(['agentPresets'], () => { wirePresetRoster() })
|
|
159
150
|
}
|
|
160
151
|
|
|
161
|
-
/**
|
|
162
|
-
* Native approval routing: answer ACP `session/request_permission` through
|
|
163
|
-
* the host approval chain so the question takes over the chat composer
|
|
164
|
-
* (`ApprovalPanel`) instead of the settings panel.
|
|
165
|
-
*
|
|
166
|
-
* `approval.request` needs the live agent object (its session carries the
|
|
167
|
-
* audit pair, and the ask must land inside an open turn) and the chat
|
|
168
|
-
* session id our adapter already knows. Any missing piece — service absent,
|
|
169
|
-
* unknown session, a rejecting service — degrades to `'unavailable'`, which
|
|
170
|
-
* sends the ask back to the ledger/panel path rather than losing it.
|
|
171
|
-
*/
|
|
172
|
-
const wireApproval = (): void => {
|
|
173
|
-
const approval = (typeof ctx.get === 'function' ? ctx.get('approval') : (ctx as unknown as { approval?: ApprovalServiceLike }).approval) as ApprovalServiceLike | undefined
|
|
174
|
-
const agents = (typeof ctx.get === 'function' ? ctx.get('agents') : (ctx as unknown as { agents?: AgentsServiceLike }).agents) as AgentsServiceLike | undefined
|
|
175
|
-
if (approval === undefined || agents === undefined) return
|
|
176
|
-
manager.setApprovalRequester(async ({ dshSessionId, toolName, signal }) => {
|
|
177
|
-
if (dshSessionId === undefined) return 'unavailable'
|
|
178
|
-
const agent = agents.get(dshSessionId)
|
|
179
|
-
if (agent === undefined) return 'unavailable'
|
|
180
|
-
try {
|
|
181
|
-
return await approval.request({ agent, toolName, ...(signal === undefined ? {} : { signal }) })
|
|
182
|
-
} catch {
|
|
183
|
-
// Never propagate: a failed ask must fall back, not break the turn.
|
|
184
|
-
return 'unavailable'
|
|
185
|
-
}
|
|
186
|
-
})
|
|
187
|
-
}
|
|
188
|
-
|
|
189
152
|
/**
|
|
190
153
|
* Workspace routing: the sidebar's selected workspace IS the chat session's
|
|
191
154
|
* workspace, so the agent's session and its process both run there (see
|
|
@@ -274,11 +237,10 @@ export function apply(ctx: Context, config?: unknown): void {
|
|
|
274
237
|
}
|
|
275
238
|
}
|
|
276
239
|
|
|
277
|
-
wireApproval()
|
|
278
240
|
wireWorkspace()
|
|
279
241
|
if (typeof ctx.inject === 'function') {
|
|
280
|
-
// Late arrival for the workspace collaborators
|
|
281
|
-
//
|
|
242
|
+
// Late arrival for the workspace collaborators: a deployment missing one
|
|
243
|
+
// service must not fail this plugin's activation.
|
|
282
244
|
ctx.inject(['sandboxPolicy', 'sessions'], () => {
|
|
283
245
|
wireWorkspace()
|
|
284
246
|
})
|
|
@@ -291,13 +253,6 @@ export function apply(ctx: Context, config?: unknown): void {
|
|
|
291
253
|
wireAcpMode()
|
|
292
254
|
})
|
|
293
255
|
}
|
|
294
|
-
if (typeof ctx.inject === 'function') {
|
|
295
|
-
// Late arrival: both services may still be loading when this plugin
|
|
296
|
-
// applies (profile order is not ours to control).
|
|
297
|
-
ctx.inject(['approval', 'agents'], () => {
|
|
298
|
-
wireApproval()
|
|
299
|
-
})
|
|
300
|
-
}
|
|
301
256
|
|
|
302
257
|
/** Registration handle: callable disposer, plus `replace` to re-announce routes. */
|
|
303
258
|
interface LlmAdapterHandle {
|