dsh-ntr 0.1.6 → 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 +1 -1
- package/README.md +1 -1
- package/cordis.patch.yml +18 -18
- package/lib/bridge.js +120 -291
- package/lib/client/acp-ui.js +60 -32
- package/lib/client/index.js +17 -0
- package/lib/client/panel.js +37 -42
- package/lib/client/permission-dock.js +72 -0
- package/lib/client/session-controls.js +4 -4
- package/lib/client/store.js +58 -12
- package/lib/client.js +296 -95
- package/lib/client.js.map +1 -1
- package/lib/impl.js +30 -19
- package/lib/index.js +6 -51
- package/lib/types/bridge.d.ts +60 -154
- package/lib/types/client/panel.d.ts +12 -19
- package/lib/types/client/permission-dock.d.ts +31 -0
- package/lib/types/client/store.d.ts +42 -12
- package/lib/types/impl.d.ts +31 -17
- package/lib/types/protocol.d.ts +27 -12
- package/package.json +1 -1
- package/src/bridge.ts +147 -357
- package/src/client/acp-ui.tsx +60 -32
- package/src/client/index.tsx +18 -0
- package/src/client/panel.tsx +54 -58
- package/src/client/permission-dock.tsx +133 -0
- package/src/client/session-controls.tsx +4 -4
- package/src/client/store.ts +81 -25
- package/src/impl.ts +49 -29
- package/src/index.ts +6 -65
- package/src/protocol.ts +32 -12
package/src/client/store.ts
CHANGED
|
@@ -59,11 +59,65 @@ export type Block =
|
|
|
59
59
|
| { kind: 'event'; event: string; text: string; raw?: string; status?: string }
|
|
60
60
|
| { kind: 'notice'; tone: 'info' | 'error'; text: string }
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* One permission ask waiting for a human. Every field is copied VERBATIM from
|
|
64
|
+
* the host's `permission/request` frame: the host owns what the ask is called
|
|
65
|
+
* (`toolName` / `reason`), which options the agent offered (order included),
|
|
66
|
+
* and whether the ask carries a deadline. The browser does not derive a title
|
|
67
|
+
* from `toolCall` and does not second-guess the agent's option labels.
|
|
68
|
+
*/
|
|
62
69
|
export interface PendingPermission {
|
|
63
70
|
requestId: string
|
|
71
|
+
/** ACP session id. */
|
|
64
72
|
sessionId: string
|
|
65
|
-
|
|
73
|
+
/** DSH chat session id; absent for `/acp` RPC-only sessions (settings-only ask). */
|
|
74
|
+
dshSessionId?: string
|
|
75
|
+
toolName: string
|
|
76
|
+
reason?: string
|
|
66
77
|
options: Array<{ optionId: string; name: string; kind: string }>
|
|
78
|
+
timeoutMode: PermissionTimeoutMode
|
|
79
|
+
/** Epoch ms; absent in `wait` mode. */
|
|
80
|
+
deadlineAt?: number
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Returned by {@link deadlineRemainingLabel} once the deadline has passed. */
|
|
84
|
+
export const PERMISSION_DEADLINE_EXPIRED = 'expired'
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The asks that belong to one DSH chat conversation. An ask without a
|
|
88
|
+
* `dshSessionId` belongs to no conversation (an `/acp` RPC-only session), so it
|
|
89
|
+
* is deliberately absent from every conversation's dock and stays in the
|
|
90
|
+
* settings-panel ledger.
|
|
91
|
+
*/
|
|
92
|
+
export function permissionsForSession(
|
|
93
|
+
permissions: readonly PendingPermission[],
|
|
94
|
+
dshSessionId: string,
|
|
95
|
+
): PendingPermission[] {
|
|
96
|
+
return permissions.filter(permission => permission.dshSessionId === dshSessionId)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Compact countdown for one ask, or `undefined` when there is nothing to count
|
|
101
|
+
* down (`wait` mode, or a frame without a deadline). Pure so the formatting is
|
|
102
|
+
* testable without a clock: the dock passes `Date.now()` on its tick.
|
|
103
|
+
*
|
|
104
|
+
* Past the deadline the answer is {@link PERMISSION_DEADLINE_EXPIRED} rather
|
|
105
|
+
* than a negative number — the host has already armed the expiry, and a card
|
|
106
|
+
* counting backwards would claim a deadline that no longer exists.
|
|
107
|
+
*/
|
|
108
|
+
export function deadlineRemainingLabel(
|
|
109
|
+
deadlineAt: number | undefined,
|
|
110
|
+
now: number,
|
|
111
|
+
mode: PermissionTimeoutMode,
|
|
112
|
+
): string | undefined {
|
|
113
|
+
if (mode === 'wait' || deadlineAt === undefined) return undefined
|
|
114
|
+
const remainingMs = deadlineAt - now
|
|
115
|
+
if (remainingMs <= 0) return PERMISSION_DEADLINE_EXPIRED
|
|
116
|
+
const seconds = Math.ceil(remainingMs / 1000)
|
|
117
|
+
if (seconds < 60) return `${String(seconds)}s`
|
|
118
|
+
const minutes = Math.floor(seconds / 60)
|
|
119
|
+
const rest = seconds % 60
|
|
120
|
+
return rest === 0 ? `${String(minutes)}m` : `${String(minutes)}m ${String(rest)}s`
|
|
67
121
|
}
|
|
68
122
|
|
|
69
123
|
/**
|
|
@@ -137,17 +191,6 @@ export interface UiState {
|
|
|
137
191
|
* that never answers, …) reads as a dead button.
|
|
138
192
|
*/
|
|
139
193
|
probeErrors: Record<string, string>
|
|
140
|
-
/**
|
|
141
|
-
* Host reports that it HAS the native approval chain (permission asks are
|
|
142
|
-
* answered in the chat composer). This is a capability report, not a
|
|
143
|
-
* per-ask promise: an individual ask still falls back to the settings-panel
|
|
144
|
-
* ledger when the service is unavailable this time, or when the session has
|
|
145
|
-
* no chat identity behind it (`AgentBridge.askNativeApproval`). The panel
|
|
146
|
-
* therefore answers ANY pending ask, whatever this flag says — see
|
|
147
|
-
* `permissionLedgerCard` in panel.tsx. False on an older host or when the
|
|
148
|
-
* approval service is missing, in which case the ledger is the only path.
|
|
149
|
-
*/
|
|
150
|
-
approvalRouted: boolean
|
|
151
194
|
/**
|
|
152
195
|
* ACP agent sessions backing the chat session the composer control sits in.
|
|
153
196
|
* Only meaningful together with {@link chatSessionsFor}: the control reads it
|
|
@@ -539,7 +582,6 @@ export class AcpStore {
|
|
|
539
582
|
seatCatalogs: {},
|
|
540
583
|
probingModels: {},
|
|
541
584
|
probeErrors: {},
|
|
542
|
-
approvalRouted: false,
|
|
543
585
|
chatSessions: [],
|
|
544
586
|
tagHalo: { ...DEFAULT_TAG_HALO },
|
|
545
587
|
approvalDeadlines: undefined,
|
|
@@ -741,8 +783,8 @@ export class AcpStore {
|
|
|
741
783
|
/** Snapshot refresh used on panel open and after a stream outage. */
|
|
742
784
|
async resync(): Promise<void> {
|
|
743
785
|
try {
|
|
744
|
-
const [{ agents
|
|
745
|
-
this.call<{ agents: AgentView[]
|
|
786
|
+
const [{ agents }, { sessions }, defaultAgentInfo, detectResult, presetResult, tagHaloResult, deadlinesResult] = await Promise.all([
|
|
787
|
+
this.call<{ agents: AgentView[] }>('agents/list'),
|
|
746
788
|
this.call<{ sessions: SessionView[] }>('sessions/list'),
|
|
747
789
|
// The plugin's OWN default ACP agent — a plugin-private id, not a DSH
|
|
748
790
|
// model route (see `defaultAgent` on the host half).
|
|
@@ -766,7 +808,6 @@ export class AcpStore {
|
|
|
766
808
|
sessions,
|
|
767
809
|
defaultAgent: defaultAgentInfo?.agentId,
|
|
768
810
|
detectedAgents,
|
|
769
|
-
approvalRouted: approvalRouted === true,
|
|
770
811
|
preset: presetResult?.preset ?? undefined,
|
|
771
812
|
tagHalo: tagHaloResult?.halo ?? { ...DEFAULT_TAG_HALO },
|
|
772
813
|
approvalDeadlines: deadlinesResult.deadlines,
|
|
@@ -1483,11 +1524,20 @@ export class AcpStore {
|
|
|
1483
1524
|
}
|
|
1484
1525
|
}
|
|
1485
1526
|
|
|
1527
|
+
/**
|
|
1528
|
+
* Answer one ask (`optionId` absent = cancel it). A failure is reported
|
|
1529
|
+
* through the store's notice — the settings panel renders that — AND
|
|
1530
|
+
* rethrown, so the conversation dock can pin the message next to the card
|
|
1531
|
+
* that failed instead of leaving it on a settings page the user may not
|
|
1532
|
+
* have open. Callers that cannot show it (the settings panel) swallow it
|
|
1533
|
+
* after the notice.
|
|
1534
|
+
*/
|
|
1486
1535
|
async respondPermission(requestId: string, optionId?: string): Promise<void> {
|
|
1487
1536
|
try {
|
|
1488
1537
|
await this.call('permission/respond', { requestId, ...(optionId === undefined ? {} : { optionId }) })
|
|
1489
1538
|
} catch (error) {
|
|
1490
1539
|
this.flashError(error)
|
|
1540
|
+
throw error
|
|
1491
1541
|
}
|
|
1492
1542
|
}
|
|
1493
1543
|
|
|
@@ -1851,16 +1901,22 @@ export class AcpStore {
|
|
|
1851
1901
|
return this.loadAgentCatalog(event.agentId)
|
|
1852
1902
|
}
|
|
1853
1903
|
case 'permission/request': {
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1904
|
+
// Verbatim copy of the host's frame. The host decides what the ask is
|
|
1905
|
+
// called (`toolName` / `reason`) and which options exist; deriving a
|
|
1906
|
+
// title from `toolCall` here used to make the card say something the
|
|
1907
|
+
// host never said.
|
|
1908
|
+
const permission: PendingPermission = {
|
|
1909
|
+
requestId: event.requestId,
|
|
1910
|
+
sessionId: event.sessionId,
|
|
1911
|
+
...(event.dshSessionId === undefined ? {} : { dshSessionId: event.dshSessionId }),
|
|
1912
|
+
toolName: event.toolName,
|
|
1913
|
+
...(event.reason === undefined ? {} : { reason: event.reason }),
|
|
1914
|
+
options: event.options,
|
|
1915
|
+
timeoutMode: event.timeoutMode,
|
|
1916
|
+
...(event.deadlineAt === undefined ? {} : { deadlineAt: event.deadlineAt }),
|
|
1917
|
+
}
|
|
1857
1918
|
this.mutate({
|
|
1858
|
-
permissions: [...this.state.permissions.filter(p => p.requestId !== event.requestId),
|
|
1859
|
-
requestId: event.requestId,
|
|
1860
|
-
sessionId: event.sessionId,
|
|
1861
|
-
title,
|
|
1862
|
-
options: event.options,
|
|
1863
|
-
}],
|
|
1919
|
+
permissions: [...this.state.permissions.filter(p => p.requestId !== event.requestId), permission],
|
|
1864
1920
|
})
|
|
1865
1921
|
return Promise.resolve()
|
|
1866
1922
|
}
|
package/src/impl.ts
CHANGED
|
@@ -1579,6 +1579,22 @@ export class EventHub {
|
|
|
1579
1579
|
|
|
1580
1580
|
// ---- Permission ledger ----
|
|
1581
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
|
+
|
|
1582
1598
|
export interface PendingPermission {
|
|
1583
1599
|
requestId: string
|
|
1584
1600
|
sessionId: string
|
|
@@ -1588,15 +1604,13 @@ export interface PendingPermission {
|
|
|
1588
1604
|
* client receives (see `permissionReplayFrames`). Absent on entries created
|
|
1589
1605
|
* without a view — those still answer, they just cannot be re-shown.
|
|
1590
1606
|
*/
|
|
1591
|
-
view?:
|
|
1607
|
+
view?: PendingPermissionFields
|
|
1592
1608
|
}
|
|
1593
1609
|
|
|
1594
1610
|
/** One outstanding ask as a reconnecting client must be re-shown it. */
|
|
1595
|
-
export interface PendingPermissionView {
|
|
1611
|
+
export interface PendingPermissionView extends PendingPermissionFields {
|
|
1596
1612
|
requestId: string
|
|
1597
1613
|
sessionId: string
|
|
1598
|
-
toolCall: unknown
|
|
1599
|
-
options: PermissionOptionView[]
|
|
1600
1614
|
}
|
|
1601
1615
|
|
|
1602
1616
|
export class PermissionLedger {
|
|
@@ -1648,14 +1662,37 @@ export class PermissionLedger {
|
|
|
1648
1662
|
views.push({
|
|
1649
1663
|
requestId: entry.requestId,
|
|
1650
1664
|
sessionId: entry.sessionId,
|
|
1651
|
-
|
|
1652
|
-
options: entry.view.options,
|
|
1665
|
+
...entry.view,
|
|
1653
1666
|
})
|
|
1654
1667
|
}
|
|
1655
1668
|
return views
|
|
1656
1669
|
}
|
|
1657
1670
|
}
|
|
1658
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
|
+
|
|
1659
1696
|
/**
|
|
1660
1697
|
* Frames that re-establish a freshly connected client's view of the pending
|
|
1661
1698
|
* permission asks: every outstanding ask re-sent as its original
|
|
@@ -1663,10 +1700,10 @@ export class PermissionLedger {
|
|
|
1663
1700
|
*
|
|
1664
1701
|
* Why a replay at all: a `permission/request` is broadcast ONCE, when the ask is
|
|
1665
1702
|
* created. A client that connects later — page reload, second tab, stream
|
|
1666
|
-
* outage — never receives it, and
|
|
1667
|
-
*
|
|
1668
|
-
*
|
|
1669
|
-
*
|
|
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
|
|
1670
1707
|
* every WS connect.
|
|
1671
1708
|
*
|
|
1672
1709
|
* Why the terminator is mandatory: the replay is a snapshot, so an ask resolved
|
|
@@ -1675,13 +1712,7 @@ export class PermissionLedger {
|
|
|
1675
1712
|
* `respond` would answer `unknown_request`.
|
|
1676
1713
|
*/
|
|
1677
1714
|
export function permissionReplayFrames(pending: readonly PendingPermissionView[]): AcpEvent[] {
|
|
1678
|
-
const requests: AcpEvent[] = pending.map(
|
|
1679
|
-
type: 'permission/request' as const,
|
|
1680
|
-
requestId: p.requestId,
|
|
1681
|
-
sessionId: p.sessionId,
|
|
1682
|
-
toolCall: p.toolCall,
|
|
1683
|
-
options: p.options,
|
|
1684
|
-
}))
|
|
1715
|
+
const requests: AcpEvent[] = pending.map(permissionRequestFrame)
|
|
1685
1716
|
return [...requests, { type: 'permission/synced', requestIds: pending.map(p => p.requestId) }]
|
|
1686
1717
|
}
|
|
1687
1718
|
|
|
@@ -1761,12 +1792,6 @@ export interface AcpManager {
|
|
|
1761
1792
|
* cache (newly published models appear, withdrawn ones drop out).
|
|
1762
1793
|
*/
|
|
1763
1794
|
cachedAgentModels?(agentId: string): AgentProbeModelsResult | undefined
|
|
1764
|
-
/**
|
|
1765
|
-
* True when ACP permission asks are routed to the host's native approval
|
|
1766
|
-
* service (answered in the chat composer) instead of the settings-panel
|
|
1767
|
-
* ledger. Read-only capability report for `agents/list`.
|
|
1768
|
-
*/
|
|
1769
|
-
approvalRouting?(): boolean
|
|
1770
1795
|
/**
|
|
1771
1796
|
* ACP agent sessions backing one DSH chat session (looked up by chat session
|
|
1772
1797
|
* id), so the composer can offer the agent's OWN mode list. Empty when no
|
|
@@ -1881,12 +1906,7 @@ export async function dispatchRpc(manager: AcpManager, endpoint: string, payload
|
|
|
1881
1906
|
try {
|
|
1882
1907
|
switch (endpoint) {
|
|
1883
1908
|
case 'agents/list':
|
|
1884
|
-
|
|
1885
|
-
// uses it to decide whether the settings panel still owns approvals.
|
|
1886
|
-
return ok({
|
|
1887
|
-
agents: manager.agentViews(),
|
|
1888
|
-
approvalRouted: manager.approvalRouting?.() ?? false,
|
|
1889
|
-
})
|
|
1909
|
+
return ok({ agents: manager.agentViews() })
|
|
1890
1910
|
case 'agents/detect':
|
|
1891
1911
|
return ok({ agents: await (manager.detectAgents?.() ?? Promise.resolve([])) })
|
|
1892
1912
|
case 'agent/save': {
|
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; callId?: 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 }
|
|
@@ -166,40 +149,6 @@ export function apply(ctx: Context, config?: unknown): void {
|
|
|
166
149
|
ctx.inject(['agentPresets'], () => { wirePresetRoster() })
|
|
167
150
|
}
|
|
168
151
|
|
|
169
|
-
/**
|
|
170
|
-
* Native approval routing: answer ACP `session/request_permission` through
|
|
171
|
-
* the host approval chain so the question takes over the chat composer
|
|
172
|
-
* (`ApprovalPanel`) instead of the settings panel.
|
|
173
|
-
*
|
|
174
|
-
* `approval.request` needs the live agent object (its session carries the
|
|
175
|
-
* audit pair, and the ask must land inside an open turn) and the chat
|
|
176
|
-
* session id our adapter already knows. Any missing piece — service absent,
|
|
177
|
-
* unknown session, a rejecting service — degrades to `'unavailable'`, which
|
|
178
|
-
* sends the ask back to the ledger/panel path rather than losing it.
|
|
179
|
-
*/
|
|
180
|
-
const wireApproval = (): void => {
|
|
181
|
-
const approval = (typeof ctx.get === 'function' ? ctx.get('approval') : (ctx as unknown as { approval?: ApprovalServiceLike }).approval) as ApprovalServiceLike | undefined
|
|
182
|
-
const agents = (typeof ctx.get === 'function' ? ctx.get('agents') : (ctx as unknown as { agents?: AgentsServiceLike }).agents) as AgentsServiceLike | undefined
|
|
183
|
-
if (approval === undefined || agents === undefined) return
|
|
184
|
-
manager.setApprovalRequester(async ({ dshSessionId, toolName, reason, callId, signal }) => {
|
|
185
|
-
if (dshSessionId === undefined) return 'unavailable'
|
|
186
|
-
const agent = agents.get(dshSessionId)
|
|
187
|
-
if (agent === undefined) return 'unavailable'
|
|
188
|
-
try {
|
|
189
|
-
return await approval.request({
|
|
190
|
-
agent,
|
|
191
|
-
toolName,
|
|
192
|
-
...(reason === undefined ? {} : { reason }),
|
|
193
|
-
...(callId === undefined ? {} : { callId }),
|
|
194
|
-
...(signal === undefined ? {} : { signal }),
|
|
195
|
-
})
|
|
196
|
-
} catch {
|
|
197
|
-
// Never propagate: a failed ask must fall back, not break the turn.
|
|
198
|
-
return 'unavailable'
|
|
199
|
-
}
|
|
200
|
-
})
|
|
201
|
-
}
|
|
202
|
-
|
|
203
152
|
/**
|
|
204
153
|
* Workspace routing: the sidebar's selected workspace IS the chat session's
|
|
205
154
|
* workspace, so the agent's session and its process both run there (see
|
|
@@ -288,11 +237,10 @@ export function apply(ctx: Context, config?: unknown): void {
|
|
|
288
237
|
}
|
|
289
238
|
}
|
|
290
239
|
|
|
291
|
-
wireApproval()
|
|
292
240
|
wireWorkspace()
|
|
293
241
|
if (typeof ctx.inject === 'function') {
|
|
294
|
-
// Late arrival for the workspace collaborators
|
|
295
|
-
//
|
|
242
|
+
// Late arrival for the workspace collaborators: a deployment missing one
|
|
243
|
+
// service must not fail this plugin's activation.
|
|
296
244
|
ctx.inject(['sandboxPolicy', 'sessions'], () => {
|
|
297
245
|
wireWorkspace()
|
|
298
246
|
})
|
|
@@ -305,13 +253,6 @@ export function apply(ctx: Context, config?: unknown): void {
|
|
|
305
253
|
wireAcpMode()
|
|
306
254
|
})
|
|
307
255
|
}
|
|
308
|
-
if (typeof ctx.inject === 'function') {
|
|
309
|
-
// Late arrival: both services may still be loading when this plugin
|
|
310
|
-
// applies (profile order is not ours to control).
|
|
311
|
-
ctx.inject(['approval', 'agents'], () => {
|
|
312
|
-
wireApproval()
|
|
313
|
-
})
|
|
314
|
-
}
|
|
315
256
|
|
|
316
257
|
/** Registration handle: callable disposer, plus `replace` to re-announce routes. */
|
|
317
258
|
interface LlmAdapterHandle {
|
package/src/protocol.ts
CHANGED
|
@@ -332,18 +332,44 @@ export type AcpEvent =
|
|
|
332
332
|
* same as unset — no `agentId@choice` variants.
|
|
333
333
|
*/
|
|
334
334
|
| { type: 'agent/exposure'; agentId: string; exposedModels: string[]; autoDiscoverModels: boolean }
|
|
335
|
-
|
|
335
|
+
/**
|
|
336
|
+
* One outstanding ask, broadcast ONCE when it is created and replayed as-is
|
|
337
|
+
* to every freshly connected client (see `permission/synced`).
|
|
338
|
+
*
|
|
339
|
+
* Every ACP permission ask carries everything the conversation card needs:
|
|
340
|
+
* who owns it (`dshSessionId`), what it is about (`toolCall` + the derived
|
|
341
|
+
* `toolName` / `reason`), the options to render verbatim, and the deadline in
|
|
342
|
+
* force. The host never answers on its own outside an expired `abort` /
|
|
343
|
+
* `auto` deadline, so this frame (and its replay) is the answer entry.
|
|
344
|
+
*/
|
|
345
|
+
| {
|
|
346
|
+
type: 'permission/request'
|
|
347
|
+
requestId: string
|
|
348
|
+
/** ACP session id the ask belongs to. */
|
|
349
|
+
sessionId: string
|
|
350
|
+
/** DSH chat session id; absent for `/acp` RPC-only sessions. */
|
|
351
|
+
dshSessionId?: string
|
|
352
|
+
/** Raw ACP `ToolCallUpdate` the agent sent — never summarized away. */
|
|
353
|
+
toolCall: unknown
|
|
354
|
+
/** Host-derived human name (`permissionToolName`, `_meta` fallback included). */
|
|
355
|
+
toolName: string
|
|
356
|
+
/** One-line detail from the agent's declared input, when it has any. */
|
|
357
|
+
reason?: string
|
|
358
|
+
options: PermissionOptionView[]
|
|
359
|
+
/** What an unanswered ask does when its deadline fires. */
|
|
360
|
+
timeoutMode: PermissionTimeoutMode
|
|
361
|
+
/** Epoch ms of the deadline; absent in `wait` mode (no deadline). */
|
|
362
|
+
deadlineAt?: number
|
|
363
|
+
}
|
|
336
364
|
| { type: 'permission/resolved'; requestId: string; sessionId: string; outcome: PermissionOutcome }
|
|
337
365
|
/**
|
|
338
366
|
* An ask whose deadline fired and was settled by the plugin itself — refused
|
|
339
367
|
* (`abort`), or answered with the option the agent's own kinds imply
|
|
340
368
|
* (`auto`).
|
|
341
369
|
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
* ledger path does not need it — its card sits on the settings panel and the
|
|
346
|
-
* chat line already pointed there.
|
|
370
|
+
* This is the conversation's record of the expiry: the card it was answering
|
|
371
|
+
* is gone, so without this frame the user would see it vanish with nothing
|
|
372
|
+
* said about it.
|
|
347
373
|
*/
|
|
348
374
|
| {
|
|
349
375
|
type: 'permission/expired'
|
|
@@ -429,12 +455,6 @@ export type RpcResult<T> = {
|
|
|
429
455
|
|
|
430
456
|
export interface AgentsListResult {
|
|
431
457
|
agents: AgentView[]
|
|
432
|
-
/**
|
|
433
|
-
* True when permission asks are routed to the host approval chain and the
|
|
434
|
-
* browser must stop rendering the settings-panel permission card. False (or
|
|
435
|
-
* absent on an older host) keeps the degraded ledger path visible.
|
|
436
|
-
*/
|
|
437
|
-
approvalRouted?: boolean
|
|
438
458
|
}
|
|
439
459
|
export type SessionPromptResult = { accepted: true; turnId: string }
|
|
440
460
|
export type TranscriptResult = { session: SessionView; entries: Array<{ seq: number; update: SessionUpdate }> }
|