lark-remote 0.5.3 → 0.5.5

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.
Files changed (66) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.en.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/bridge/approval-coordinator.d.ts +18 -1
  5. package/dist/bridge/approval-coordinator.js +57 -10
  6. package/dist/bridge/index.js +90 -17
  7. package/dist/card/approval-render.js +18 -0
  8. package/dist/card/run-state.js +10 -5
  9. package/dist/config/index.d.ts +16 -0
  10. package/dist/config/index.js +46 -6
  11. package/dist/config/opencode-config.d.ts +3 -3
  12. package/dist/config/opencode-config.js +38 -22
  13. package/dist/inbound/authorize.d.ts +34 -0
  14. package/dist/inbound/authorize.js +31 -0
  15. package/dist/index.js +116 -63
  16. package/dist/instance-lock.d.ts +9 -0
  17. package/dist/instance-lock.js +113 -42
  18. package/dist/router/config/dsh.d.ts +51 -21
  19. package/dist/router/config/dsh.js +255 -66
  20. package/dist/router/config/opencode.js +16 -6
  21. package/dist/router/config/types.d.ts +6 -3
  22. package/dist/router/index.d.ts +1 -0
  23. package/dist/router/index.js +13 -5
  24. package/dist/runner/codex/app-server/translator.js +21 -7
  25. package/dist/runner/common/acp/base-acp-runner.d.ts +12 -0
  26. package/dist/runner/common/acp/base-acp-runner.js +83 -8
  27. package/dist/runner/common/acp/protocol-types.d.ts +10 -2
  28. package/dist/runner/common/acp/protocol-types.js +3 -1
  29. package/dist/runner/common/connection-based-runner.d.ts +15 -0
  30. package/dist/runner/common/connection-based-runner.js +40 -5
  31. package/dist/runner/common/jsonrpc/connection-manager.d.ts +15 -0
  32. package/dist/runner/common/jsonrpc/connection-manager.js +83 -50
  33. package/dist/runner/dsh/approval.d.ts +53 -0
  34. package/dist/runner/dsh/approval.js +161 -0
  35. package/dist/runner/dsh/auth.d.ts +36 -0
  36. package/dist/runner/dsh/auth.js +118 -0
  37. package/dist/runner/dsh/client.d.ts +161 -42
  38. package/dist/runner/dsh/client.js +551 -244
  39. package/dist/runner/dsh/daemon.d.ts +85 -0
  40. package/dist/runner/dsh/daemon.js +272 -0
  41. package/dist/runner/dsh/merge.d.ts +9 -0
  42. package/dist/runner/dsh/merge.js +51 -0
  43. package/dist/runner/dsh/runner.d.ts +94 -11
  44. package/dist/runner/dsh/runner.js +553 -67
  45. package/dist/runner/dsh/translator.d.ts +43 -3
  46. package/dist/runner/dsh/translator.js +115 -38
  47. package/dist/runner/dsh/types.d.ts +189 -27
  48. package/dist/runner/dsh/types.js +11 -5
  49. package/dist/runner/kimi/acp/runner.js +15 -3
  50. package/dist/runner/opencode/acp/error-monitor.d.ts +2 -2
  51. package/dist/runner/opencode/acp/error-monitor.js +2 -2
  52. package/dist/runner/opencode/acp/runner.d.ts +9 -0
  53. package/dist/runner/opencode/acp/runner.js +40 -1
  54. package/dist/runner/opencode/acp/translator.d.ts +4 -2
  55. package/dist/runner/opencode/acp/translator.js +12 -3
  56. package/dist/runner/pi/rpc/translator.d.ts +2 -0
  57. package/dist/runner/pi/rpc/translator.js +27 -1
  58. package/dist/runner/types.d.ts +34 -0
  59. package/dist/runner/types.js +24 -0
  60. package/dist/session/codex/rollout-reader.js +44 -7
  61. package/dist/session/dsh/sessions.d.ts +45 -6
  62. package/dist/session/dsh/sessions.js +171 -27
  63. package/dist/session/opencode/sessions.d.ts +18 -7
  64. package/dist/session/opencode/sessions.js +294 -15
  65. package/dist/session/pi/sessions.js +6 -2
  66. package/package.json +3 -1
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Correlate DSH's durable approval audit with its live answer waterfall.
3
+ *
4
+ * `session/follow` carries `approval/asked` / `approval/decided`; `$events`
5
+ * carries the answerable `approval/request` waterfall. The audit id is the
6
+ * stable identity shown on the Feishu card; the waterfall contributes the
7
+ * ephemeral `eventId` + `clientId` needed to answer it. Both are correlated by
8
+ * `callId`, with an order-independent pairing so either transport may arrive
9
+ * first.
10
+ */
11
+ export class DshApprovalTracker {
12
+ audits = new Map();
13
+ waterfallByCallId = new Map();
14
+ bindings = new Map();
15
+ approvalIdByEventId = new Map();
16
+ waiters = new Map();
17
+ /** Record `approval/asked`; pair immediately when its waterfall is known. */
18
+ onAuditAsked(audit) {
19
+ const existing = this.bindings.get(audit.approvalId);
20
+ if (existing)
21
+ return existing;
22
+ this.audits.set(audit.approvalId, audit);
23
+ if (audit.callId) {
24
+ const waterfall = this.waterfallByCallId.get(audit.callId);
25
+ if (waterfall)
26
+ return this.bind(audit, waterfall);
27
+ }
28
+ return undefined;
29
+ }
30
+ /** Record an `approval/request` waterfall; pair when its audit is known. */
31
+ onWaterfall(waterfall) {
32
+ const existingApprovalId = this.approvalIdByEventId.get(waterfall.eventId);
33
+ if (existingApprovalId) {
34
+ // A reconnect re-delivers the same pending event with a new Client
35
+ // identity. Keep the card binding but refresh the answer channel.
36
+ const binding = this.bindings.get(existingApprovalId);
37
+ if (binding) {
38
+ binding.waterfall.clientId = waterfall.clientId;
39
+ if (waterfall.reason)
40
+ binding.waterfall.reason = waterfall.reason;
41
+ if (waterfall.callId) {
42
+ this.waterfallByCallId.set(waterfall.callId, binding.waterfall);
43
+ }
44
+ }
45
+ return undefined;
46
+ }
47
+ if (waterfall.callId)
48
+ this.waterfallByCallId.set(waterfall.callId, waterfall);
49
+ if (waterfall.callId) {
50
+ for (const audit of this.audits.values()) {
51
+ if (audit.callId === waterfall.callId && !this.bindings.has(audit.approvalId)) {
52
+ return this.bind(audit, waterfall);
53
+ }
54
+ }
55
+ // Some approval types omit callId on the durable audit. Pair with the
56
+ // oldest unbound audit for the same tool before falling back to Web UI.
57
+ for (const audit of this.audits.values()) {
58
+ if (!audit.callId &&
59
+ !this.bindings.has(audit.approvalId) &&
60
+ audit.toolName === waterfall.toolName) {
61
+ return this.bind(audit, waterfall);
62
+ }
63
+ }
64
+ }
65
+ // A missing callId is unusual but not fatal: pair with the oldest unbound
66
+ // audit for the same tool, preserving the audit id on the card.
67
+ if (!waterfall.callId) {
68
+ for (const audit of this.audits.values()) {
69
+ if (!this.bindings.has(audit.approvalId) && audit.toolName === waterfall.toolName) {
70
+ return this.bind(audit, waterfall);
71
+ }
72
+ }
73
+ }
74
+ return undefined;
75
+ }
76
+ /** Mark an audit resolved (local response or an external decision). */
77
+ onDecided(approvalId) {
78
+ const binding = this.bindings.get(approvalId);
79
+ this.audits.delete(approvalId);
80
+ this.bindings.delete(approvalId);
81
+ if (binding) {
82
+ this.approvalIdByEventId.delete(binding.waterfall.eventId);
83
+ if (binding.waterfall.callId) {
84
+ const stored = this.waterfallByCallId.get(binding.waterfall.callId);
85
+ if (stored?.eventId === binding.waterfall.eventId) {
86
+ this.waterfallByCallId.delete(binding.waterfall.callId);
87
+ }
88
+ }
89
+ this.resolveWaiters(approvalId, undefined);
90
+ }
91
+ return binding;
92
+ }
93
+ /** Mark a waterfall cancelled by the Host (another client answered it). */
94
+ onCancelled(eventId) {
95
+ const approvalId = this.approvalIdByEventId.get(eventId);
96
+ if (!approvalId)
97
+ return undefined;
98
+ return this.onDecided(approvalId);
99
+ }
100
+ getBinding(approvalId) {
101
+ return this.bindings.get(approvalId);
102
+ }
103
+ /**
104
+ * Wait for a waterfall to arrive after the audit card was rendered. The
105
+ * audit is appended before the waterfall is dispatched, so this window is
106
+ * normally tiny; the bound keeps a fast button click from failing.
107
+ */
108
+ async waitForBinding(approvalId, timeoutMs, signal) {
109
+ const current = this.bindings.get(approvalId);
110
+ if (current)
111
+ return current;
112
+ if (signal?.aborted)
113
+ return undefined;
114
+ return new Promise((resolve) => {
115
+ let settled = false;
116
+ const finish = (binding) => {
117
+ if (settled)
118
+ return;
119
+ settled = true;
120
+ clearTimeout(timer);
121
+ signal?.removeEventListener('abort', onAbort);
122
+ const set = this.waiters.get(approvalId);
123
+ set?.delete(finish);
124
+ if (set && set.size === 0)
125
+ this.waiters.delete(approvalId);
126
+ resolve(binding);
127
+ };
128
+ const onAbort = () => finish(undefined);
129
+ const timer = setTimeout(() => finish(undefined), timeoutMs);
130
+ signal?.addEventListener('abort', onAbort, { once: true });
131
+ const set = this.waiters.get(approvalId) ?? new Set();
132
+ set.add(finish);
133
+ this.waiters.set(approvalId, set);
134
+ });
135
+ }
136
+ /** Drop all in-flight state when a run ends. */
137
+ clear() {
138
+ for (const approvalId of [...this.waiters.keys()])
139
+ this.resolveWaiters(approvalId, undefined);
140
+ this.audits.clear();
141
+ this.waterfallByCallId.clear();
142
+ this.bindings.clear();
143
+ this.approvalIdByEventId.clear();
144
+ }
145
+ bind(audit, waterfall) {
146
+ const binding = { audit, waterfall };
147
+ this.bindings.set(audit.approvalId, binding);
148
+ this.approvalIdByEventId.set(waterfall.eventId, audit.approvalId);
149
+ this.resolveWaiters(audit.approvalId, binding);
150
+ return binding;
151
+ }
152
+ resolveWaiters(approvalId, binding) {
153
+ const set = this.waiters.get(approvalId);
154
+ if (!set)
155
+ return;
156
+ this.waiters.delete(approvalId);
157
+ for (const waiter of [...set])
158
+ waiter(binding);
159
+ }
160
+ }
161
+ //# sourceMappingURL=approval.js.map
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Persistent DSH browser authentication.
3
+ *
4
+ * `dsh web` rotates its process launch token on every restart. The signed
5
+ * browser cookie is different: it is verified with the durable
6
+ * `client-connection/browser-session` secret in `$DSH_HOME/.credentials.yaml`
7
+ * and does not identify a process-local operator peer. DSH accepts that cookie
8
+ * for both `/api/*` and `/api/remote.mux`, so minting a fresh v1 cookie from
9
+ * the persisted secret survives a DSH restart without copying a new token.
10
+ */
11
+ /** Credential record that owns the browser-cookie signing secret. */
12
+ export declare const DSH_BROWSER_CREDENTIAL_KEY = "client-connection/browser-session";
13
+ /** Short lifetime keeps every request safely inside DSH's configurable max age. */
14
+ export declare const DSH_COOKIE_LIFETIME_MS: number;
15
+ /**
16
+ * Resolve DSH's credentials file using the same `$DSH_HOME` convention.
17
+ * A configured home (agents.dsh.dshHome) wins over the environment so the
18
+ * file matches the `DSH_HOME` lark-remote passes to a process it starts.
19
+ */
20
+ export declare function resolveDshCredentialsFile(env?: NodeJS.ProcessEnv, configuredHome?: string): string;
21
+ /** Whether a filesystem error means the credentials file is absent. */
22
+ export declare function isDshCredentialsNotFound(error: unknown): boolean;
23
+ /** Read and validate the browser-cookie signing secret synchronously. */
24
+ export declare function readDshBrowserSecretSync(file: string): Buffer;
25
+ /** Read and validate the browser-cookie signing secret asynchronously. */
26
+ export declare function readDshBrowserSecret(file: string): Promise<Buffer>;
27
+ /** Cookie name binds the signed value to one host:port authority. */
28
+ export declare function dshCookieName(authority: string): string;
29
+ /**
30
+ * Mint a v1 browser cookie accepted by DSH's `/api` admission path.
31
+ *
32
+ * DSH verifies `v1.<base64url(payload)>.<base64url(hmac-sha256(body))>` with
33
+ * the durable browser-session secret. The short default lifetime is refreshed
34
+ * on the next request, so no cookie is persisted by lark-remote.
35
+ */
36
+ export declare function mintDshBrowserCookie(baseUrl: string, secret: Buffer, issuedAt?: number, lifetimeMs?: number): string;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Persistent DSH browser authentication.
3
+ *
4
+ * `dsh web` rotates its process launch token on every restart. The signed
5
+ * browser cookie is different: it is verified with the durable
6
+ * `client-connection/browser-session` secret in `$DSH_HOME/.credentials.yaml`
7
+ * and does not identify a process-local operator peer. DSH accepts that cookie
8
+ * for both `/api/*` and `/api/remote.mux`, so minting a fresh v1 cookie from
9
+ * the persisted secret survives a DSH restart without copying a new token.
10
+ */
11
+ import { createHash, createHmac } from 'node:crypto';
12
+ import fs from 'node:fs';
13
+ import os from 'node:os';
14
+ import path from 'node:path';
15
+ import YAML from 'yaml';
16
+ /** Credential record that owns the browser-cookie signing secret. */
17
+ export const DSH_BROWSER_CREDENTIAL_KEY = 'client-connection/browser-session';
18
+ /** Short lifetime keeps every request safely inside DSH's configurable max age. */
19
+ export const DSH_COOKIE_LIFETIME_MS = 60 * 60 * 1000;
20
+ const SECRET_BYTES = 32;
21
+ const COOKIE_PREFIX = 'dsh-auth-';
22
+ function isRecord(value) {
23
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
24
+ }
25
+ /**
26
+ * Resolve DSH's credentials file using the same `$DSH_HOME` convention.
27
+ * A configured home (agents.dsh.dshHome) wins over the environment so the
28
+ * file matches the `DSH_HOME` lark-remote passes to a process it starts.
29
+ */
30
+ export function resolveDshCredentialsFile(env = process.env, configuredHome) {
31
+ const home = configuredHome?.trim() || env.DSH_HOME?.trim();
32
+ return path.join(home && home.length > 0 ? home : path.join(os.homedir(), '.dsh'), '.credentials.yaml');
33
+ }
34
+ /** Whether a filesystem error means the credentials file is absent. */
35
+ export function isDshCredentialsNotFound(error) {
36
+ return error?.code === 'ENOENT';
37
+ }
38
+ function assertOwnerOnly(file, stats) {
39
+ if (!stats.isFile()) {
40
+ throw new Error(`DSH credentials file ${file} is not a regular file`);
41
+ }
42
+ // Windows has no POSIX mode bits; DSH itself skips the same check there.
43
+ if (process.platform === 'win32')
44
+ return;
45
+ if ((stats.mode & 0o077) !== 0) {
46
+ throw new Error(`DSH credentials file ${file} is readable beyond its owner ` +
47
+ `(mode ${(stats.mode & 0o777).toString(8)}); run "chmod 600 ${file}"`);
48
+ }
49
+ }
50
+ function parseBrowserSecret(text, file) {
51
+ let document;
52
+ try {
53
+ document = YAML.parse(text);
54
+ }
55
+ catch {
56
+ // The parser's message quotes the offending source line, which is secret.
57
+ throw new Error(`DSH credentials file ${file} is not valid YAML`);
58
+ }
59
+ if (!isRecord(document) || document.version !== 1 || !isRecord(document.records)) {
60
+ throw new Error(`DSH credentials file ${file} has an unsupported format`);
61
+ }
62
+ const record = document.records[DSH_BROWSER_CREDENTIAL_KEY];
63
+ if (!isRecord(record) ||
64
+ record.kind !== 'grant' ||
65
+ !isRecord(record.payload) ||
66
+ record.payload.version !== 1 ||
67
+ typeof record.payload.secret !== 'string') {
68
+ throw new Error(`DSH credentials file ${file} does not contain ${DSH_BROWSER_CREDENTIAL_KEY}`);
69
+ }
70
+ const secret = Buffer.from(record.payload.secret, 'base64url');
71
+ if (secret.byteLength !== SECRET_BYTES ||
72
+ secret.toString('base64url') !== record.payload.secret) {
73
+ throw new Error(`DSH credentials file ${file} contains an invalid ${DSH_BROWSER_CREDENTIAL_KEY} secret`);
74
+ }
75
+ return secret;
76
+ }
77
+ /** Read and validate the browser-cookie signing secret synchronously. */
78
+ export function readDshBrowserSecretSync(file) {
79
+ assertOwnerOnly(file, fs.statSync(file));
80
+ return parseBrowserSecret(fs.readFileSync(file, 'utf-8'), file);
81
+ }
82
+ /** Read and validate the browser-cookie signing secret asynchronously. */
83
+ export async function readDshBrowserSecret(file) {
84
+ const stats = await fs.promises.stat(file);
85
+ assertOwnerOnly(file, stats);
86
+ const text = await fs.promises.readFile(file, 'utf-8');
87
+ return parseBrowserSecret(text, file);
88
+ }
89
+ /** Cookie name binds the signed value to one host:port authority. */
90
+ export function dshCookieName(authority) {
91
+ return COOKIE_PREFIX + createHash('sha256').update(authority).digest('base64url');
92
+ }
93
+ /**
94
+ * Mint a v1 browser cookie accepted by DSH's `/api` admission path.
95
+ *
96
+ * DSH verifies `v1.<base64url(payload)>.<base64url(hmac-sha256(body))>` with
97
+ * the durable browser-session secret. The short default lifetime is refreshed
98
+ * on the next request, so no cookie is persisted by lark-remote.
99
+ */
100
+ export function mintDshBrowserCookie(baseUrl, secret, issuedAt = Date.now(), lifetimeMs = DSH_COOKIE_LIFETIME_MS) {
101
+ if (secret.byteLength !== SECRET_BYTES) {
102
+ throw new Error('DSH browser-session secret must be 32 bytes');
103
+ }
104
+ if (!Number.isSafeInteger(issuedAt) || !Number.isSafeInteger(lifetimeMs) || lifetimeMs <= 0) {
105
+ throw new Error('DSH browser cookie lifetime must be a positive safe integer');
106
+ }
107
+ const authority = new URL(baseUrl).host;
108
+ if (!authority)
109
+ throw new Error(`DSH host ${baseUrl} has no authority`);
110
+ const expiresAt = issuedAt + lifetimeMs;
111
+ if (!Number.isSafeInteger(expiresAt)) {
112
+ throw new Error('DSH browser cookie expiry exceeds the safe integer range');
113
+ }
114
+ const body = Buffer.from(JSON.stringify({ version: 1, authority, issuedAt, expiresAt }), 'utf-8').toString('base64url');
115
+ const signature = createHmac('sha256', secret).update(body).digest('base64url');
116
+ return `${dshCookieName(authority)}=v1.${body}.${signature}`;
117
+ }
118
+ //# sourceMappingURL=auth.js.map
@@ -1,45 +1,126 @@
1
1
  /**
2
- * DshClient — HTTP client for the DSH Web Host proxy.
2
+ * DshClient — HTTP/WebSocket client for the DSH Web Host.
3
3
  *
4
- * Two transports:
5
- * 1. unary POST /api/<method> (client-request → server-response).
6
- * 2. SSE GET /api/events.mux (all-session aggregated stream). Frames are
7
- * ServerRequest envelopes; `method` equals the MuxFrame's `type`.
4
+ * DSH 0.2.x guardrails:
5
+ * 1. `/api/*` and `/api/remote.mux` require a signed `dsh-auth-*` cookie.
6
+ * We mint the v1 cookie from DSH's durable browser-session secret; the
7
+ * launch token remains a fallback via `GET /?token=<launch token>`.
8
+ * 2. Unary calls are POST `/api/<namespace>/<method>` envelopes whose
9
+ * `payload` is `{args:<request>}`.
10
+ * 3. Live session data is a logical stream over `/api/remote.mux`:
11
+ * `{type:'open',streamId,endpoint:'session/follow',payload}` →
12
+ * `{type:'item',streamId,value}` / `{type:'end',streamId}`.
13
+ * The Gateway-owned `$events` stream carries answerable
14
+ * `approval/request` waterfalls; answers go back through
15
+ * `$events/result`.
8
16
  *
9
- * The client is stateless apart from the base URL: each `run()` opens its own
10
- * mux subscription and closes it on terminal. `sessionEvents()` wraps the raw
11
- * mux with per-session filtering plus reconnect + history-replay dedup
12
- * (DSH v1 has no `since` resume — reconnection = reopen stream + refetch
13
- * history, dedup by event seq).
17
+ * The client is stateless apart from the base URL and auth cookie: each
18
+ * `run()` opens its own remote mux subscription and closes it on terminal.
19
+ * `sessionEvents()` adds per-session filtering, reconnect + follow-snapshot
20
+ * replay, and seq-based dedup.
14
21
  */
15
- import type { DshModelCatalogValue, DshModelSelection, DshPresetListValue, DshServerRequest, DshStreamItem, DshTokenUsage } from './types.js';
22
+ import type { DshApprovalStreamItem, DshModelCatalogValue, DshModelSelection, DshPermissionCatalogValue, DshPresetListValue, DshRemoteEventOutcome, DshSessionPageValue, DshStreamItem, DshTokenUsage, DshCommandResult } from './types.js';
16
23
  interface WebSocketMessageEvent {
17
24
  data: unknown;
18
25
  }
19
- interface WebSocketLike {
26
+ export interface WebSocketOptions {
27
+ headers?: Record<string, string>;
28
+ }
29
+ export interface WebSocketLike {
30
+ onopen: ((event: unknown) => void) | null;
20
31
  onmessage: ((event: WebSocketMessageEvent) => void) | null;
21
32
  onerror: ((event: unknown) => void) | null;
22
33
  onclose: ((event: unknown) => void) | null;
34
+ /** Set by transports that expose an HTTP rejection before the handshake. */
35
+ onunexpectedresponse?: ((event: {
36
+ statusCode?: number;
37
+ }) => void) | null;
38
+ send(data: string): void;
23
39
  close(): void;
24
40
  }
25
- export type WebSocketFactory = (url: string) => WebSocketLike;
41
+ export type WebSocketFactory = (url: string, options?: WebSocketOptions) => WebSocketLike;
26
42
  export declare class DshError extends Error {
27
43
  readonly code: string;
28
44
  readonly details: Record<string, unknown>;
29
45
  constructor(code: string, message: string, details?: Record<string, unknown>);
30
46
  }
47
+ export interface DshClientOptions {
48
+ /** Configured DSH home; selects `.credentials.yaml` when no explicit file is given. */
49
+ dshHome?: string;
50
+ /**
51
+ * Optional DSH Web launch token. Accepts either the bare token or the full
52
+ * URL printed by `dsh web` (the `token` query parameter is extracted).
53
+ */
54
+ token?: string;
55
+ /**
56
+ * DSH credentials file containing the durable browser-session signing
57
+ * secret. Defaults to `$DSH_HOME/.credentials.yaml` or
58
+ * `~/.dsh/.credentials.yaml`. When readable, this is preferred over the
59
+ * per-process launch token and survives `dsh web` restarts.
60
+ */
61
+ credentialsFile?: string;
62
+ /** Injectable credentials reader (tests). */
63
+ readCredentials?: (file: string) => Promise<Buffer>;
64
+ /** Injectable clock (tests). */
65
+ now?: () => number;
66
+ /** Lifetime of minted browser cookies. */
67
+ cookieLifetimeMs?: number;
68
+ fetchImpl?: typeof fetch;
69
+ webSocketImpl?: WebSocketFactory;
70
+ /**
71
+ * Unary request timeout. DSH accepts the HTTP connection but may never answer
72
+ * (e.g. a stuck `session/prompt`); without a timeout the run generator stays
73
+ * parked in `await unary()` forever, so /stop and the idle watchdog cannot
74
+ * release the bridge's workspace queue.
75
+ */
76
+ requestTimeoutMs?: number;
77
+ }
78
+ /** Extract the `token` query parameter from a pasted `dsh web` URL. */
79
+ export declare function extractDshToken(raw: string | undefined): string;
80
+ export declare function dshArgs(method: string, payload: unknown): Record<string, unknown>;
31
81
  export declare class DshClient {
32
82
  readonly baseUrl: string;
83
+ private readonly token;
84
+ private readonly credentialsFile;
85
+ private readonly readCredentials;
86
+ private readonly now;
87
+ private readonly cookieLifetimeMs;
33
88
  private readonly fetchImpl;
34
89
  private readonly webSocketImpl;
35
- constructor(baseUrl: string, fetchImpl?: typeof fetch, webSocketImpl?: WebSocketFactory);
36
- /** Perform a unary POST and return the ok value; throws DshError on failure. */
37
- unary(method: string, payload: unknown): Promise<unknown>;
90
+ private readonly requestTimeoutMs;
91
+ private cookiePromise?;
92
+ constructor(baseUrl: string, options?: DshClientOptions);
93
+ /**
94
+ * Resolve a DSH browser cookie. The durable credentials file is read on every
95
+ * call so a rotated signing secret is picked up without restarting
96
+ * lark-remote. Token bootstrap remains a fallback for hosts without that
97
+ * file; unlike `/connect`, the index handshake mints a v1 cookie that DSH
98
+ * accepts for API and WebSocket requests across its own restarts.
99
+ */
100
+ private authCookie;
101
+ /** Mint a v1 browser cookie from DSH's durable signing secret. */
102
+ private cookieFromCredentials;
103
+ /**
104
+ * Perform a unary POST and return the ok value; throws DshError on failure.
105
+ *
106
+ * `signal` aborts the request when the run is stopped; a timeout (default
107
+ * `requestTimeoutMs`) guards against a server that accepted the connection
108
+ * but never answers. Both are composed into the request's AbortSignal.
109
+ */
110
+ unary(method: string, payload: unknown, opts?: {
111
+ signal?: AbortSignal;
112
+ timeoutMs?: number;
113
+ }): Promise<unknown>;
114
+ /** Distinguish "run stopped" (abort) from "server did not answer" (timeout). */
115
+ private abortError;
38
116
  /** Create a session. Pass `agentPreset` to pin the session's preset (fixed at
39
117
  * creation). */
40
118
  createSession(payload: {
41
119
  cwd: string;
42
120
  agentPreset?: string;
121
+ }, opts?: {
122
+ signal?: AbortSignal;
123
+ timeoutMs?: number;
43
124
  }): Promise<{
44
125
  sessionId: string;
45
126
  agentPreset?: string;
@@ -48,46 +129,84 @@ export declare class DshClient {
48
129
  * server-side). Failures are surfaced as DshError for the caller to downgrade. */
49
130
  selectModel(payload: DshModelSelection & {
50
131
  sessionId: string;
132
+ }, opts?: {
133
+ signal?: AbortSignal;
134
+ timeoutMs?: number;
51
135
  }): Promise<DshModelSelection>;
52
- /** Read the model catalog (session-independent, llm.models). */
53
- listModels(): Promise<DshModelCatalogValue>;
54
- /** Read the preset catalog (agentPreset.list). */
55
- listPresets(): Promise<DshPresetListValue>;
136
+ /** Read the model catalog (session/modelCatalog). */
137
+ listModels(opts?: {
138
+ signal?: AbortSignal;
139
+ timeoutMs?: number;
140
+ }): Promise<DshModelCatalogValue>;
56
141
  /**
57
- * Return the highest event `seq` currently persisted in session.history.
142
+ * Execute one slash command against a session (commands/execute).
143
+ *
144
+ * This is the only way to run a DSH command: `session/prompt` turns the text
145
+ * into an ordinary user message and never routes it through the command
146
+ * registry, so `/compact` sent as a prompt would just be model input.
58
147
  *
59
- * The caller snapshots this before `session.prompt`. `session.history` is a
60
- * full-session replay rather than a delta, so this high-water mark is what
61
- * lets a subsequent replay skip every already-completed turn.
148
+ * The RPC settles when the handler settles, not when it is accepted: DSH's
149
+ * `/compact` waits for the compaction to finish. Callers therefore must pass
150
+ * an explicit `timeoutMs` (0 = no unary budget) and rely on their own
151
+ * cancellation/watchdog instead of the 60s default.
62
152
  */
63
- latestEventSeq(sessionId: string): Promise<number>;
153
+ executeCommand(sessionId: string, line: string, opts?: {
154
+ signal?: AbortSignal;
155
+ timeoutMs?: number;
156
+ }): Promise<DshCommandResult | undefined>;
157
+ /** Read the preset catalog (agentPresets/list). */
158
+ listPresets(): Promise<DshPresetListValue>;
159
+ /** Read the permission preset catalog (permissionPresets/catalog). */
160
+ listPermissionPresets(): Promise<DshPermissionCatalogValue>;
161
+ /** Answer one forwarded approval waterfall ($events/result). */
162
+ answerRemoteEvent(clientId: string, eventId: string, outcome: DshRemoteEventOutcome, opts?: {
163
+ signal?: AbortSignal;
164
+ timeoutMs?: number;
165
+ }): Promise<void>;
64
166
  /**
65
- * Raw SSE mux stream: yields ServerRequest envelopes until the stream ends,
66
- * errors, or `signal` aborts. Does NOT reconnect (the caller owns retry).
167
+ * Return the highest event `seq` currently persisted for the session.
168
+ *
169
+ * `session/projections` is the unary watermark (the old `session.history`
170
+ * high-water scan is gone in 0.2.x). The caller snapshots this before
171
+ * `session/prompt` so a follow snapshot can skip already-completed turns.
67
172
  */
68
- mux(signal: AbortSignal): AsyncGenerator<DshServerRequest>;
173
+ latestEventSeq(sessionId: string, opts?: {
174
+ signal?: AbortSignal;
175
+ timeoutMs?: number;
176
+ }): Promise<number>;
177
+ /** Read one cold history page. `throughSeq` normally comes from
178
+ * `latestEventSeq()`; `session/page` needs an inclusive cursor. */
179
+ page(sessionId: string, throughSeq: number, maxMessages: number, opts?: {
180
+ signal?: AbortSignal;
181
+ timeoutMs?: number;
182
+ }): Promise<DshSessionPageValue>;
69
183
  /**
70
- * WebSocket mux stream. The real DSH `/api/events.mux` endpoint is a
71
- * WebSocket upgrade, not HTTP SSE: each text message is already a
72
- * `DshServerRequest` JSON envelope. This yields those envelopes and ends on
73
- * close/abort, letting `sessionEvents()` reconnect through the same
74
- * history-replay loop.
184
+ * One logical stream over `/api/remote.mux`. The WebSocket must carry the
185
+ * same auth cookie as unary calls. Ends on the server's `end` frame, socket
186
+ * close, error, or `signal` abort.
75
187
  */
76
- muxWebSocket(signal: AbortSignal): AsyncGenerator<DshServerRequest>;
188
+ private muxStream;
189
+ /** One logical `session/follow` stream. */
190
+ private followStream;
77
191
  /**
78
- * Replay session.history events that are newer than `state.lastSeq`, updating
79
- * `state.lastSeq` as it goes. Yields each non-duplicate SessionEvent. Used for
80
- * both the initial baseline (first subscription) and reconnect gap-fill.
192
+ * Consume the Gateway-owned `$events` stream. The stream is owner-only and
193
+ * answer-capable; each waterfall for another session is delegated with
194
+ * `next` so a different lark-remote workspace cannot park it.
81
195
  */
82
- private replayHistory;
196
+ approvalEvents(sessionId: string, signal: AbortSignal): AsyncGenerator<DshApprovalStreamItem>;
197
+ /** Translate one durable event into a stream item (approvals are surfaced
198
+ * separately; everything else is forwarded to the runner). */
199
+ private eventItem;
83
200
  /**
84
- * Per-session subscription: yields session events (and approval notices) for
85
- * `sessionId`, auto-reconnecting on stream drop/error and replaying
86
- * `session.history` to fill the gap (dedup by seq). Terminates on turn/end
87
- * or `signal` abort.
201
+ * Per-session subscription: yields session events, assistant stream chunks
202
+ * and approval notices for `sessionId`, auto-reconnecting on stream
203
+ * drop/error and replaying the follow snapshot (dedup by seq). Terminates on
204
+ * turn/end or `signal` abort.
88
205
  */
89
206
  sessionEvents(sessionId: string, signal: AbortSignal, initialSeq?: number): AsyncGenerator<DshStreamItem>;
90
207
  /** Extract TokenUsage from an assistant/message data payload (absent → undefined). */
91
208
  static usageOf(data: Record<string, unknown>): DshTokenUsage | undefined;
92
209
  }
210
+ /** Stable client-side prompt identity persisted on the accepted user message. */
211
+ export declare function newPromptRequestId(): string;
93
212
  export {};