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/lib/impl.js
CHANGED
|
@@ -95,6 +95,38 @@ export function validateTagHalo(value) {
|
|
|
95
95
|
* longer would be a mis-typed number rather than a deliberate window.
|
|
96
96
|
*/
|
|
97
97
|
export const APPROVAL_DEADLINE_MAX_MS = 86_400_000;
|
|
98
|
+
/**
|
|
99
|
+
* The shortest REAL wait: the first rung of the card's ladder (30 seconds).
|
|
100
|
+
*
|
|
101
|
+
* The floor for a mode that must not wait zero — see
|
|
102
|
+
* {@link normalizeApprovalWindow}.
|
|
103
|
+
*/
|
|
104
|
+
export const APPROVAL_DEADLINE_MIN_WAIT_MS = 30_000;
|
|
105
|
+
/**
|
|
106
|
+
* Make a window legal for the mode it runs under.
|
|
107
|
+
*
|
|
108
|
+
* `0` means 不等待 — the ask is settled the moment it arrives instead of
|
|
109
|
+
* waiting. That is the **auto** mode's business: `auto` answers with the
|
|
110
|
+
* option the agent's own kinds imply, so the agent CONTINUES, and an
|
|
111
|
+
* unattended deployment gets a sane default instead of a hang.
|
|
112
|
+
*
|
|
113
|
+
* Under `abort` the same zero would refuse every ask the instant it arrived:
|
|
114
|
+
* the agent stops at every gate it meets, which is not a timeout, it is a
|
|
115
|
+
* brick wall. So `abort` never offers 不等待, and a window of `0` reaching it
|
|
116
|
+
* (a config file, a state file written by an older build, a mode switch that
|
|
117
|
+
* left the window behind) normalizes to the shortest real wait.
|
|
118
|
+
*
|
|
119
|
+
* `wait` ignores the window entirely — it never expires.
|
|
120
|
+
*/
|
|
121
|
+
export function normalizeApprovalWindow(mode, ms) {
|
|
122
|
+
return mode === 'abort' && ms === 0 ? APPROVAL_DEADLINE_MIN_WAIT_MS : ms;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The order `auto` mode picks an option in, by the kind the agent declared:
|
|
126
|
+
* the broadest grant first, then the narrow one, then the refusals, then
|
|
127
|
+
* whatever the agent offered instead. See {@link pickAutoOption}.
|
|
128
|
+
*/
|
|
129
|
+
export const AUTO_OPTION_KIND_PRIORITY = ['allow_always', 'allow_once', 'reject_once', 'reject_always'];
|
|
98
130
|
/**
|
|
99
131
|
* Validate ONE approval deadline, shared by every layer that accepts one.
|
|
100
132
|
*
|
|
@@ -113,22 +145,55 @@ export function validateApprovalDeadlineMs(raw, label, code, hint) {
|
|
|
113
145
|
}
|
|
114
146
|
/**
|
|
115
147
|
* Validate a settings-page deadline write. Omitted keys are simply absent from
|
|
116
|
-
* the result — the manager merges, so a card that only
|
|
117
|
-
*
|
|
148
|
+
* the result — the manager merges, so a card that only moved the mode must not
|
|
149
|
+
* reset the number. `reset` is not a deadline and is dropped here.
|
|
118
150
|
*/
|
|
119
151
|
export function validateApprovalDeadlines(value) {
|
|
120
152
|
if (value === null || typeof value !== 'object')
|
|
121
153
|
throw new AcpError('bad_payload', 'acp: approval deadlines must be an object');
|
|
122
|
-
const {
|
|
154
|
+
const { permissionTimeoutMs } = value;
|
|
123
155
|
const out = {};
|
|
124
|
-
if (
|
|
125
|
-
out.
|
|
126
|
-
}
|
|
127
|
-
if (permissionFallbackTimeoutMs !== undefined) {
|
|
128
|
-
out.permissionFallbackTimeoutMs = validateApprovalDeadlineMs(permissionFallbackTimeoutMs, 'permissionFallbackTimeoutMs', 'bad_payload', '(0 = never answer a degraded ask on the human\'s behalf)');
|
|
156
|
+
if (permissionTimeoutMs !== undefined) {
|
|
157
|
+
out.permissionTimeoutMs = validateApprovalDeadlineMs(permissionTimeoutMs, 'permissionTimeoutMs', 'bad_payload', '(0 = 不等待,仅自动推进档可用)');
|
|
129
158
|
}
|
|
130
159
|
return out;
|
|
131
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Validate the mode a write asks for. Three values, and the list is the whole
|
|
163
|
+
* vocabulary: an unanswered ask is either answered for you, kept waiting, or
|
|
164
|
+
* refused. Anything else is a typo, not a new mode.
|
|
165
|
+
*/
|
|
166
|
+
export function validatePermissionTimeoutMode(raw, code) {
|
|
167
|
+
if (raw !== 'abort' && raw !== 'auto' && raw !== 'wait') {
|
|
168
|
+
throw new AcpError(code, 'acp: permissionTimeoutMode must be one of "abort", "auto", "wait"');
|
|
169
|
+
}
|
|
170
|
+
return raw;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The option an expired deadline answers with in `auto` mode.
|
|
174
|
+
*
|
|
175
|
+
* The pick is by the option's KIND, because that is what the agent declared
|
|
176
|
+
* about the choice it is offering: `allow_always` first (one grant covers the
|
|
177
|
+
* rest of the run), then `allow_once` (the smaller grant), then the reject
|
|
178
|
+
* kinds (refuse but let the agent continue), and finally the first option
|
|
179
|
+
* whatever it is — an agent may offer only a custom kind, and refusing to
|
|
180
|
+
* answer at all would hang the turn.
|
|
181
|
+
*
|
|
182
|
+
* `undefined` means NO option could be read (no options, or none with a usable
|
|
183
|
+
* id). The caller then degrades to `abort`: guessing an option is worse than
|
|
184
|
+
* refusing, and hanging is worse than either. OmniTerm dev picks the same way
|
|
185
|
+
* (`pick_auto_option`, `src/acp/permission.rs`).
|
|
186
|
+
*/
|
|
187
|
+
export function pickAutoOption(options) {
|
|
188
|
+
const usable = (option) => option.optionId === '' ? undefined : option;
|
|
189
|
+
for (const kind of AUTO_OPTION_KIND_PRIORITY) {
|
|
190
|
+
const hit = options.find(option => option.kind === kind);
|
|
191
|
+
const picked = hit === undefined ? undefined : usable(hit);
|
|
192
|
+
if (picked !== undefined)
|
|
193
|
+
return picked;
|
|
194
|
+
}
|
|
195
|
+
return options.length === 0 ? undefined : usable(options[0]);
|
|
196
|
+
}
|
|
132
197
|
/**
|
|
133
198
|
* Bound on the number of MCP servers one agent may declare. An MCP server is
|
|
134
199
|
* a process (stdio) or a remote endpoint the agent connects to at session
|
|
@@ -418,17 +483,25 @@ export function validateConfig(raw) {
|
|
|
418
483
|
if (typeof startupTimeoutMs !== 'number' || !Number.isFinite(startupTimeoutMs) || startupTimeoutMs < 1_000 || startupTimeoutMs > 600_000) {
|
|
419
484
|
throw new AcpError('bad_config', 'acp: startupTimeoutMs must be a number in [1000, 600000]');
|
|
420
485
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
//
|
|
426
|
-
// the
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
|
|
430
|
-
const
|
|
431
|
-
|
|
486
|
+
// ONE wait window for both routes. The two legacy keys are folded in when
|
|
487
|
+
// the new one is absent, so an existing `cordis.patch.yml` keeps meaning what
|
|
488
|
+
// it meant instead of silently falling back to the default.
|
|
489
|
+
const legacyWindow = value.nativeApprovalTimeoutMs ?? value.permissionFallbackTimeoutMs;
|
|
490
|
+
// A legacy window of 0 meant "this window never fires". The only way to say
|
|
491
|
+
// that now is the `wait` MODE (0 is 不等待 today), so a legacy zero carries
|
|
492
|
+
// over as the mode — otherwise an unattended deployment would silently turn
|
|
493
|
+
// "wait forever" into "answer immediately".
|
|
494
|
+
const legacyNever = legacyWindow === 0;
|
|
495
|
+
const permissionTimeoutMs = validateApprovalDeadlineMs(legacyNever ? 300_000 : (value.permissionTimeoutMs ?? legacyWindow ?? 300_000), 'permissionTimeoutMs', 'bad_config', '(0 = 不等待,仅自动推进档可用)');
|
|
496
|
+
// What an expired deadline DOES. Same validator as the settings-page write,
|
|
497
|
+
// so a mode the panel can offer is a mode the next boot accepts.
|
|
498
|
+
const permissionTimeoutMode = legacyNever
|
|
499
|
+
? 'wait'
|
|
500
|
+
: validatePermissionTimeoutMode(value.permissionTimeoutMode ?? 'wait', 'bad_config');
|
|
501
|
+
// 不等待 belongs to `auto` (it answers, so the agent continues). Under
|
|
502
|
+
// `abort` a zero window would refuse every ask on arrival — the agent stops
|
|
503
|
+
// at every gate — so it becomes the shortest real wait.
|
|
504
|
+
const effectivePermissionTimeoutMs = normalizeApprovalWindow(permissionTimeoutMode, permissionTimeoutMs);
|
|
432
505
|
const cancelFallbackMs = value.cancelFallbackMs ?? 15_000;
|
|
433
506
|
if (typeof cancelFallbackMs !== 'number' || !Number.isInteger(cancelFallbackMs) || cancelFallbackMs < 0 || cancelFallbackMs > 600_000) {
|
|
434
507
|
throw new AcpError('bad_config', 'acp: cancelFallbackMs must be an integer in [0, 600000] (0 = disable the fallback)');
|
|
@@ -446,9 +519,8 @@ export function validateConfig(raw) {
|
|
|
446
519
|
transcriptMaxEntries: max,
|
|
447
520
|
probeModelsOnBoot,
|
|
448
521
|
startupTimeoutMs,
|
|
449
|
-
permissionTimeoutMs,
|
|
450
|
-
|
|
451
|
-
permissionFallbackTimeoutMs,
|
|
522
|
+
permissionTimeoutMs: effectivePermissionTimeoutMs,
|
|
523
|
+
permissionTimeoutMode,
|
|
452
524
|
cancelFallbackMs,
|
|
453
525
|
staleCancelRetryDelayMs,
|
|
454
526
|
staleCancelMaxRetries,
|
|
@@ -1441,13 +1513,35 @@ export class PermissionLedger {
|
|
|
1441
1513
|
views.push({
|
|
1442
1514
|
requestId: entry.requestId,
|
|
1443
1515
|
sessionId: entry.sessionId,
|
|
1444
|
-
|
|
1445
|
-
options: entry.view.options,
|
|
1516
|
+
...entry.view,
|
|
1446
1517
|
});
|
|
1447
1518
|
}
|
|
1448
1519
|
return views;
|
|
1449
1520
|
}
|
|
1450
1521
|
}
|
|
1522
|
+
/**
|
|
1523
|
+
* The `permission/request` frame for one ask.
|
|
1524
|
+
*
|
|
1525
|
+
* ONE builder for both the live broadcast and the reconnect replay, so the two
|
|
1526
|
+
* can never drift: a client that reconnects must receive exactly the frame the
|
|
1527
|
+
* live one carried (its conversation card filters on `dshSessionId` and shows
|
|
1528
|
+
* `toolName` / `deadlineAt`, so a replay missing them would silently drop the
|
|
1529
|
+
* ask).
|
|
1530
|
+
*/
|
|
1531
|
+
export function permissionRequestFrame(view) {
|
|
1532
|
+
return {
|
|
1533
|
+
type: 'permission/request',
|
|
1534
|
+
requestId: view.requestId,
|
|
1535
|
+
sessionId: view.sessionId,
|
|
1536
|
+
...(view.dshSessionId === undefined ? {} : { dshSessionId: view.dshSessionId }),
|
|
1537
|
+
toolCall: view.toolCall,
|
|
1538
|
+
toolName: view.toolName,
|
|
1539
|
+
...(view.reason === undefined ? {} : { reason: view.reason }),
|
|
1540
|
+
options: view.options,
|
|
1541
|
+
timeoutMode: view.timeoutMode,
|
|
1542
|
+
...(view.deadlineAt === undefined ? {} : { deadlineAt: view.deadlineAt }),
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1451
1545
|
/**
|
|
1452
1546
|
* Frames that re-establish a freshly connected client's view of the pending
|
|
1453
1547
|
* permission asks: every outstanding ask re-sent as its original
|
|
@@ -1455,10 +1549,10 @@ export class PermissionLedger {
|
|
|
1455
1549
|
*
|
|
1456
1550
|
* Why a replay at all: a `permission/request` is broadcast ONCE, when the ask is
|
|
1457
1551
|
* created. A client that connects later — page reload, second tab, stream
|
|
1458
|
-
* outage — never receives it, and
|
|
1459
|
-
*
|
|
1460
|
-
*
|
|
1461
|
-
*
|
|
1552
|
+
* outage — never receives it, and that card (conversation or settings ledger)
|
|
1553
|
+
* is the ONLY answer entry for the ask, so the ask is invisible to exactly the
|
|
1554
|
+
* client that needs it. OmniTerm closes the same hole with
|
|
1555
|
+
* `PermissionManager::pending_events()` + a `PermissionsSynced` marker on
|
|
1462
1556
|
* every WS connect.
|
|
1463
1557
|
*
|
|
1464
1558
|
* Why the terminator is mandatory: the replay is a snapshot, so an ask resolved
|
|
@@ -1467,13 +1561,7 @@ export class PermissionLedger {
|
|
|
1467
1561
|
* `respond` would answer `unknown_request`.
|
|
1468
1562
|
*/
|
|
1469
1563
|
export function permissionReplayFrames(pending) {
|
|
1470
|
-
const requests = pending.map(
|
|
1471
|
-
type: 'permission/request',
|
|
1472
|
-
requestId: p.requestId,
|
|
1473
|
-
sessionId: p.sessionId,
|
|
1474
|
-
toolCall: p.toolCall,
|
|
1475
|
-
options: p.options,
|
|
1476
|
-
}));
|
|
1564
|
+
const requests = pending.map(permissionRequestFrame);
|
|
1477
1565
|
return [...requests, { type: 'permission/synced', requestIds: pending.map(p => p.requestId) }];
|
|
1478
1566
|
}
|
|
1479
1567
|
function ok(value) {
|
|
@@ -1507,12 +1595,7 @@ export async function dispatchRpc(manager, endpoint, payload) {
|
|
|
1507
1595
|
try {
|
|
1508
1596
|
switch (endpoint) {
|
|
1509
1597
|
case 'agents/list':
|
|
1510
|
-
|
|
1511
|
-
// uses it to decide whether the settings panel still owns approvals.
|
|
1512
|
-
return ok({
|
|
1513
|
-
agents: manager.agentViews(),
|
|
1514
|
-
approvalRouted: manager.approvalRouting?.() ?? false,
|
|
1515
|
-
});
|
|
1598
|
+
return ok({ agents: manager.agentViews() });
|
|
1516
1599
|
case 'agents/detect':
|
|
1517
1600
|
return ok({ agents: await (manager.detectAgents?.() ?? Promise.resolve([])) });
|
|
1518
1601
|
case 'agent/save': {
|
|
@@ -1683,10 +1766,10 @@ export async function dispatchRpc(manager, endpoint, payload) {
|
|
|
1683
1766
|
// renders nothing rather than a number it cannot trust.
|
|
1684
1767
|
return ok({ deadlines: manager.approvalDeadlines?.() });
|
|
1685
1768
|
case 'ui/approval-deadlines-set': {
|
|
1686
|
-
const patch =
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
}
|
|
1769
|
+
const patch = {
|
|
1770
|
+
...validateApprovalDeadlines({ permissionTimeoutMs: body.permissionTimeoutMs }),
|
|
1771
|
+
...(body.mode === undefined ? {} : { mode: validatePermissionTimeoutMode(body.mode, 'bad_payload') }),
|
|
1772
|
+
};
|
|
1690
1773
|
await manager.setApprovalDeadlines?.(patch, body.reset === true);
|
|
1691
1774
|
return ok({ deadlines: manager.approvalDeadlines?.() });
|
|
1692
1775
|
}
|
package/lib/index.js
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
import { ACP_PRESET_ID, AcpLlmAdapter, BridgeManager } from "./bridge.js";
|
|
8
8
|
import { EventHub, dispatchRpc, permissionReplayFrames, sessionAgentPreset, sessionAgentPresetFromLog, validateConfig } from "./impl.js";
|
|
9
9
|
export const name = 'dsh-ntr';
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
10
|
+
// Deliberately minimal: every other host service is probed structurally
|
|
11
|
+
// (`ctx.get` + a late `ctx.inject`) so a deployment without one still loads
|
|
12
|
+
// this plugin. ACP permission asks are answered through this plugin's own
|
|
13
|
+
// ledger, so the host approval service is not a collaborator at all.
|
|
14
14
|
export const inject = ['webServer', 'connection'];
|
|
15
15
|
/** How many stored events the cold-read folds before giving up on scanning. */
|
|
16
16
|
const COLD_READ_MAX_EVENTS = 4096;
|
|
@@ -24,8 +24,7 @@ export function apply(ctx, config) {
|
|
|
24
24
|
const manager = new BridgeManager(resolved.agents, hub, resolved.transcriptMaxEntries ?? 600, undefined, undefined, resolved.probeModelsOnBoot ?? true, {
|
|
25
25
|
startupTimeoutMs: resolved.startupTimeoutMs,
|
|
26
26
|
permissionTimeoutMs: resolved.permissionTimeoutMs,
|
|
27
|
-
|
|
28
|
-
permissionFallbackTimeoutMs: resolved.permissionFallbackTimeoutMs,
|
|
27
|
+
permissionTimeoutMode: resolved.permissionTimeoutMode,
|
|
29
28
|
cancelFallbackMs: resolved.cancelFallbackMs,
|
|
30
29
|
staleCancelRetryDelayMs: resolved.staleCancelRetryDelayMs,
|
|
31
30
|
staleCancelMaxRetries: resolved.staleCancelMaxRetries,
|
|
@@ -39,12 +38,20 @@ export function apply(ctx, config) {
|
|
|
39
38
|
// returns to). Only the keys the file actually names are declared.
|
|
40
39
|
const rawConfig = (config ?? {});
|
|
41
40
|
const configuredDeadlines = {};
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
// The window the file actually named — the new key, or the legacy one it
|
|
42
|
+
// folded into (same order `validateConfig` resolves them in, so the card's
|
|
43
|
+
// baseline and the running value never disagree).
|
|
44
|
+
const rawWindow = rawConfig.permissionTimeoutMs
|
|
45
|
+
?? rawConfig.nativeApprovalTimeoutMs
|
|
46
|
+
?? rawConfig.permissionFallbackTimeoutMs;
|
|
47
|
+
if (typeof rawWindow === 'number' && Number.isFinite(rawWindow)) {
|
|
48
|
+
configuredDeadlines.permissionTimeoutMs = rawWindow;
|
|
46
49
|
}
|
|
47
|
-
|
|
50
|
+
// The mode is declared the same way: only when the file actually names it.
|
|
51
|
+
const configuredMode = rawConfig.permissionTimeoutMode === 'abort' || rawConfig.permissionTimeoutMode === 'auto' || rawConfig.permissionTimeoutMode === 'wait'
|
|
52
|
+
? rawConfig.permissionTimeoutMode
|
|
53
|
+
: undefined;
|
|
54
|
+
manager.setConfiguredApprovalDeadlines(configuredDeadlines, configuredMode);
|
|
48
55
|
// Boot hook kept only so this entry point's shape does not change:
|
|
49
56
|
// `bootstrapCatalogs` is intentionally empty — boot never spawns an agent,
|
|
50
57
|
// in any environment and for any `probeModelsOnBoot` value. Catalogs fill
|
|
@@ -79,37 +86,6 @@ export function apply(ctx, config) {
|
|
|
79
86
|
// Late arrival: the registry may still be loading when this plugin applies.
|
|
80
87
|
ctx.inject(['agentPresets'], () => { wirePresetRoster(); });
|
|
81
88
|
}
|
|
82
|
-
/**
|
|
83
|
-
* Native approval routing: answer ACP `session/request_permission` through
|
|
84
|
-
* the host approval chain so the question takes over the chat composer
|
|
85
|
-
* (`ApprovalPanel`) instead of the settings panel.
|
|
86
|
-
*
|
|
87
|
-
* `approval.request` needs the live agent object (its session carries the
|
|
88
|
-
* audit pair, and the ask must land inside an open turn) and the chat
|
|
89
|
-
* session id our adapter already knows. Any missing piece — service absent,
|
|
90
|
-
* unknown session, a rejecting service — degrades to `'unavailable'`, which
|
|
91
|
-
* sends the ask back to the ledger/panel path rather than losing it.
|
|
92
|
-
*/
|
|
93
|
-
const wireApproval = () => {
|
|
94
|
-
const approval = (typeof ctx.get === 'function' ? ctx.get('approval') : ctx.approval);
|
|
95
|
-
const agents = (typeof ctx.get === 'function' ? ctx.get('agents') : ctx.agents);
|
|
96
|
-
if (approval === undefined || agents === undefined)
|
|
97
|
-
return;
|
|
98
|
-
manager.setApprovalRequester(async ({ dshSessionId, toolName, signal }) => {
|
|
99
|
-
if (dshSessionId === undefined)
|
|
100
|
-
return 'unavailable';
|
|
101
|
-
const agent = agents.get(dshSessionId);
|
|
102
|
-
if (agent === undefined)
|
|
103
|
-
return 'unavailable';
|
|
104
|
-
try {
|
|
105
|
-
return await approval.request({ agent, toolName, ...(signal === undefined ? {} : { signal }) });
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
// Never propagate: a failed ask must fall back, not break the turn.
|
|
109
|
-
return 'unavailable';
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
};
|
|
113
89
|
/**
|
|
114
90
|
* Workspace routing: the sidebar's selected workspace IS the chat session's
|
|
115
91
|
* workspace, so the agent's session and its process both run there (see
|
|
@@ -203,11 +179,10 @@ export function apply(ctx, config) {
|
|
|
203
179
|
return undefined;
|
|
204
180
|
}
|
|
205
181
|
};
|
|
206
|
-
wireApproval();
|
|
207
182
|
wireWorkspace();
|
|
208
183
|
if (typeof ctx.inject === 'function') {
|
|
209
|
-
// Late arrival for the workspace collaborators
|
|
210
|
-
//
|
|
184
|
+
// Late arrival for the workspace collaborators: a deployment missing one
|
|
185
|
+
// service must not fail this plugin's activation.
|
|
211
186
|
ctx.inject(['sandboxPolicy', 'sessions'], () => {
|
|
212
187
|
wireWorkspace();
|
|
213
188
|
});
|
|
@@ -220,13 +195,6 @@ export function apply(ctx, config) {
|
|
|
220
195
|
wireAcpMode();
|
|
221
196
|
});
|
|
222
197
|
}
|
|
223
|
-
if (typeof ctx.inject === 'function') {
|
|
224
|
-
// Late arrival: both services may still be loading when this plugin
|
|
225
|
-
// applies (profile order is not ours to control).
|
|
226
|
-
ctx.inject(['approval', 'agents'], () => {
|
|
227
|
-
wireApproval();
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
198
|
const registerLlm = (llmHandle) => {
|
|
231
199
|
ctx.effect(() => {
|
|
232
200
|
// The route exists so the host's driver can reach this adapter at all:
|