@nexus-aethra/dshell-std 0.1.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.
@@ -0,0 +1,753 @@
1
+ /**
2
+ * Every dshell wire contract: the `/api/dshell/*` paths and the shapes that
3
+ * cross them.
4
+ *
5
+ * Layering rule for this file, and for this package as a whole: it declares
6
+ * FACTS, not behaviour. No dsh packages, no runtime state, and no imports except
7
+ * types from this package's own sibling modules — so both halves of every plugin
8
+ * (host route and browser face) can import the same declaration, and a contract
9
+ * change is a compile error on both sides instead of a silent drift. The browser
10
+ * faces used to restate these shapes by hand precisely because there was nowhere
11
+ * shared to put them.
12
+ *
13
+ * Sections below were moved verbatim from the packages' own protocol modules,
14
+ * which now re-export from here; the package-local headers are kept because they
15
+ * carry the reasoning for each shape.
16
+ */
17
+ import type { ShellPosition } from './shell-line.js';
18
+ /**
19
+ * The file navigator's wire vocabulary, shared by the host route and the
20
+ * browser face.
21
+ *
22
+ * Deliberately free of value imports: the browser half imports this module, and
23
+ * anything it pulls in would be bundled into `client.js`.
24
+ *
25
+ * A path on the wire is always the CANONICAL absolute path **in the session's
26
+ * own execution world** — the host path for a local session, the device path
27
+ * for a device-bound one. That is the string the client shows, splits into
28
+ * segments, trims for `..`, and hands back to list again, so both ends agree on
29
+ * one spelling without either knowing which world produced it.
30
+ */
31
+ /** The exact `/api` path the file navigator talks to. */
32
+ export declare const DSHELL_FILES_PATH = "/api/dshell/files";
33
+ /** One entry's kind, in the filesystem seam's own vocabulary. */
34
+ export type DshellFileKind = 'file' | 'directory' | 'other';
35
+ /**
36
+ * A completion row's kind: a filesystem entry, the shell's command word, or a
37
+ * word only the shell itself could name.
38
+ *
39
+ * `command` is not a filesystem kind — a command is a name the shell's world
40
+ * offers, found on its PATH — but it belongs in the same union because it is
41
+ * the same list: the completion menu draws one glyph per kind, and a command
42
+ * must not borrow a file's. `flag` (`-x`/`--long`) and `word` come from the
43
+ * session's own shell (a subcommand, an option, a branch — the shell's
44
+ * completion function knows and this side does not), and `word` is the honest
45
+ * kind for an answer whose nature is unknown.
46
+ */
47
+ export type DshellCompletionKind = DshellFileKind | 'command' | 'flag' | 'word';
48
+ /**
49
+ * Why a completion came back with nothing, as a code rather than a sentence.
50
+ *
51
+ * The route knows the reason; the browser owns the language the reader chose.
52
+ * Sending the reason as one of these and writing the line in the client is the
53
+ * same division the rest of dshell's wire uses.
54
+ */
55
+ export type DshellCompletionNote = 'noMatch' | 'noDirectory' | 'notDirectory' | 'noCommand';
56
+ /** One directory entry, with only the facts a row draws. */
57
+ export interface DshellFileEntry {
58
+ readonly name: string;
59
+ readonly kind: DshellFileKind;
60
+ /** Byte size for a regular file, when the backend reports one. */
61
+ readonly size?: number | undefined;
62
+ }
63
+ /** One directory's listing, as the route answers it. */
64
+ export interface DshellFilesListing {
65
+ /** Canonical absolute path of the listed directory, in the session's world. */
66
+ readonly path: string;
67
+ readonly entries: readonly DshellFileEntry[];
68
+ /** The listing hit the route's entry cap, so entries are missing. */
69
+ readonly truncated: boolean;
70
+ /**
71
+ * Whether the host can move the session's shell into a directory (`cd`).
72
+ *
73
+ * False when the composition has no terminal bridge, which is the only thing
74
+ * that can drive a session's main shell; the pane then draws no jump button
75
+ * rather than one that cannot work. It is a fact about the composition, so it
76
+ * is the same on every listing of one boot.
77
+ */
78
+ readonly canCd: boolean;
79
+ }
80
+ /** One browser face request. */
81
+ export interface DshellFilesRequest {
82
+ /**
83
+ * `list` reads a directory, `cd` sends the session's shell into one,
84
+ * `resolve` canonicalizes a path in the session's world (how the composer
85
+ * learns what `cd` did), and `complete` answers a shell line's last token
86
+ * from that same world.
87
+ *
88
+ * `warm` asks the same question `complete` would and throws the answer away,
89
+ * so that the pieces it needs are already in the host's memory when a Tab
90
+ * arrives. It exists because a device world answers every one of those pieces
91
+ * with a process of its own, and paying for them on the keystroke is what made
92
+ * Tab feel broken there (see the route's cache).
93
+ */
94
+ readonly action: 'list' | 'cd' | 'resolve' | 'complete' | 'warm';
95
+ /** The session whose execution world the path belongs to. */
96
+ readonly sessionId: string;
97
+ /** Absolute path in that world; omitted means the session's own directory. */
98
+ readonly path?: string | undefined;
99
+ /** The directory a relative path resolves against, for `resolve`/`complete`. */
100
+ readonly cwd?: string | undefined;
101
+ /** The composer's draft, for `complete`. */
102
+ readonly line?: string | undefined;
103
+ /** Caret offset within `line`, for `complete`. */
104
+ readonly cursor?: number | undefined;
105
+ /**
106
+ * Which pass of `complete` this is: the fast one (the default) answers from
107
+ * what this side already knows, and `refine` asks the session's own shell for
108
+ * the words only it has (see `DshellCompletion.pending`).
109
+ */
110
+ readonly phase?: 'fast' | 'refine' | undefined;
111
+ /**
112
+ * Whether `warm` may ask the session's own shell too, not only the file
113
+ * system. The switch that governs the oracle lives in the browser (like every
114
+ * other dshell switch), so the browser is the only side that can say whether
115
+ * the probe is wanted; without it, a warm would run a process per context for
116
+ * a reader who turned that off.
117
+ */
118
+ readonly oracle?: boolean | undefined;
119
+ }
120
+ /** One candidate for the shell line's last token. */
121
+ export interface DshellCompletionCandidate {
122
+ readonly name: string;
123
+ readonly kind: DshellCompletionKind;
124
+ /** Byte size for a regular file, when the backend reports one. */
125
+ readonly size?: number | undefined;
126
+ /** A short right-hand hint (kind, size) the list draws. */
127
+ readonly hint?: string | undefined;
128
+ }
129
+ /**
130
+ * One completion answer: the span of the line to replace, plus the candidates.
131
+ *
132
+ * `start`/`end` are offsets into the line the caller sent, so the composer
133
+ * substitutes exactly the basename while everything the user typed before it
134
+ * (including a `~` or a relative prefix) stays as written.
135
+ */
136
+ export interface DshellCompletion {
137
+ readonly start: number;
138
+ readonly end: number;
139
+ /**
140
+ * Where the candidates came from: the directory in the session's world for a
141
+ * path, the literal `PATH` for a command (whose names come from every
142
+ * directory the world's shell searches, not from one).
143
+ */
144
+ readonly dir: string;
145
+ readonly candidates: readonly DshellCompletionCandidate[];
146
+ /** The listing hit the route's cap, so candidates are missing. */
147
+ readonly truncated: boolean;
148
+ /**
149
+ * Where in the line this completion happened, as the SHELL reads it.
150
+ *
151
+ * The host decides it from the whole line (`./shell-line.ts`) because a
152
+ * position is a property of the line and not of the word — `dock` names a
153
+ * command, `cd dock` an argument, `tee > dock` a file. Answering with it keeps
154
+ * the browser from re-deriving the same thing from the token's shape, which is
155
+ * how the two halves of this feature drifted apart once already: the client
156
+ * needs it to know when an empty answer deserves to be shown (`noCommand` on a
157
+ * command word is worth a card, the same miss on a non-path argument is not).
158
+ */
159
+ readonly position: ShellPosition;
160
+ /**
161
+ * Whether a better answer is still coming.
162
+ *
163
+ * Two sources answer this line and one of them is a shell function on the
164
+ * other side of the world, which is worth a round trip but not worth blocking
165
+ * the keystroke. So the fast answer says `pending: true` when the shell could
166
+ * know more than the file system does — a flag, or a bare word in an argument
167
+ * position — and the browser answers it by asking again with `refine`, which
168
+ * is the phase whose answer carries this field false. It also tells the
169
+ * browser not to draw an EMPTY answer yet: a note that says "no matches" while
170
+ * the real answer is in flight is a lie the reader would have to unsee.
171
+ */
172
+ readonly pending?: boolean | undefined;
173
+ /** Why there are no candidates, when the reason is worth showing. */
174
+ readonly note?: DshellCompletionNote | undefined;
175
+ }
176
+ /** One browser face response: whichever subject was asked for, or why none was produced. */
177
+ export interface DshellFilesResponse {
178
+ readonly listing?: DshellFilesListing | undefined;
179
+ /** The directory the session's shell was sent to, for the `cd` action. */
180
+ readonly cdTo?: string | undefined;
181
+ /** The canonical path, for the `resolve` action. */
182
+ readonly resolved?: string | undefined;
183
+ /** The token's candidates, for the `complete` action. */
184
+ readonly completion?: DshellCompletion | undefined;
185
+ /**
186
+ * Acknowledgement of a `warm`: the work it starts runs in the background, so
187
+ * the answer says only that the request was understood. The browser never
188
+ * reads a value out of it — the whole point of a warm is that nobody waits.
189
+ */
190
+ readonly warmed?: boolean | undefined;
191
+ readonly error?: string | undefined;
192
+ }
193
+ /**
194
+ * The file-transfer wire vocabulary: one device session's two file trees, and
195
+ * the copies between them.
196
+ *
197
+ * Separate from `./protocol.ts` because it is a different route with a
198
+ * different subject. The navigator's route answers about ONE world (the
199
+ * session's); this one is about the pair — this machine and the device the
200
+ * session runs on — and about work that outlives one request.
201
+ *
202
+ * Deliberately free of value imports: the browser face imports this module, and
203
+ * anything it pulled in would be bundled into `client.js`. `Buffer`-style host
204
+ * types are not needed on the browser side at all.
205
+ */
206
+ /** The exact `/api` path the transfer view talks to. */
207
+ export declare const DSHELL_TRANSFER_PATH = "/api/dshell/transfer";
208
+ /**
209
+ * Which end of the pair a request names.
210
+ *
211
+ * `local` is the machine the harness runs on — for a device session, the side
212
+ * the user is sitting at. `remote` is the device that session is bound to.
213
+ * Both spell paths in their OWN namespace: a local path is a host path, a
214
+ * remote path is a device path.
215
+ */
216
+ export type TransferSide = 'local' | 'remote';
217
+ /** One entry of one side, with only the facts a row draws. */
218
+ export interface TransferEntry {
219
+ readonly name: string;
220
+ readonly kind: 'file' | 'directory' | 'other';
221
+ /** Byte size for a regular file, when the backend reports one. */
222
+ readonly size?: number | undefined;
223
+ }
224
+ /** One directory listing of one side. */
225
+ export interface TransferListing {
226
+ /** Canonical absolute path of the listed directory, in that side's namespace. */
227
+ readonly path: string;
228
+ readonly entries: readonly TransferEntry[];
229
+ /** The listing hit the route's entry cap, so entries are missing. */
230
+ readonly truncated: boolean;
231
+ }
232
+ /** What the view needs before it can draw two trees. */
233
+ export interface TransferSetup {
234
+ /** The local pane's starting directory: the harness user's home. */
235
+ readonly localRoot: string;
236
+ /** The device pane's starting directory, when the session is bound. */
237
+ readonly remoteRoot?: string | undefined;
238
+ /** The device this session runs on, when it is bound. */
239
+ readonly device?: {
240
+ readonly id: string;
241
+ readonly name: string;
242
+ } | undefined;
243
+ /**
244
+ * Whether a transfer is possible at all.
245
+ *
246
+ * False for a session with no device (nothing to transfer between) and for a
247
+ * binding without a mount directory — the filesystem seam then stays local
248
+ * while the shell seam is remote, so the two sides would silently disagree
249
+ * about which machine a path names.
250
+ */
251
+ readonly canTransfer: boolean;
252
+ /** Why not, when `canTransfer` is false. */
253
+ readonly reason?: string | undefined;
254
+ }
255
+ /** Where one copy is in its life. */
256
+ export type TransferJobState = 'walking' | 'copying' | 'done' | 'failed' | 'cancelled';
257
+ /** One copy, as the view draws it. */
258
+ export interface TransferJobView {
259
+ readonly id: string;
260
+ readonly from: TransferSide;
261
+ readonly to: TransferSide;
262
+ /** The copied entry's own absolute path, in the source side's namespace. */
263
+ readonly fromPath: string;
264
+ /** The destination DIRECTORY, in the destination side's namespace. */
265
+ readonly toDir: string;
266
+ readonly state: TransferJobState;
267
+ /** Entries written so far; for a file, 0 then 1. */
268
+ readonly files: number;
269
+ /** Known after the walk: how many entries the copy will write. */
270
+ readonly totalFiles?: number | undefined;
271
+ /** Bytes written so far. */
272
+ readonly bytes: number;
273
+ /** Known after the walk: how large the copy is. */
274
+ readonly totalBytes?: number | undefined;
275
+ /** Chunked relay progress, when the current file rides it. */
276
+ readonly chunksDone?: number | undefined;
277
+ readonly chunksTotal?: number | undefined;
278
+ /** The entry being written, relative to the copied root; empty for one file. */
279
+ readonly current?: string | undefined;
280
+ /** Source entries that are neither files nor directories, so were not copied. */
281
+ readonly skipped: number;
282
+ /** Why the copy stopped, or why it is waiting for a decision. */
283
+ readonly error?: string | undefined;
284
+ /**
285
+ * The copy stopped because an entry already exists and `overwrite` was not
286
+ * given. The view offers to retry with it instead of showing a bare failure,
287
+ * because a name collision is a question, not an error.
288
+ */
289
+ readonly conflict?: boolean | undefined;
290
+ readonly createdAt: number;
291
+ readonly settledAt?: number | undefined;
292
+ }
293
+ /** One browser face request. */
294
+ export type TransferRequest = {
295
+ readonly action: 'state';
296
+ readonly sessionId: string;
297
+ } | {
298
+ readonly action: 'list';
299
+ readonly sessionId: string;
300
+ readonly side: TransferSide;
301
+ readonly path: string;
302
+ } | {
303
+ readonly action: 'copy';
304
+ readonly sessionId: string;
305
+ readonly from: TransferSide;
306
+ /** The side the bytes land on; always the other one today, named on the wire. */
307
+ readonly to: TransferSide;
308
+ /** The entry to copy: an absolute path in the source side's namespace. */
309
+ readonly fromPath: string;
310
+ /** The destination directory, in the destination side's namespace. */
311
+ readonly toDir: string;
312
+ /** Replace files that already exist; without it a collision stops the copy. */
313
+ readonly overwrite?: boolean | undefined;
314
+ } | {
315
+ readonly action: 'job';
316
+ readonly jobId: string;
317
+ } | {
318
+ readonly action: 'cancel';
319
+ readonly jobId: string;
320
+ };
321
+ /** One browser face response: whichever of the three subjects was asked for. */
322
+ export interface TransferResponse {
323
+ readonly setup?: TransferSetup | undefined;
324
+ readonly listing?: TransferListing | undefined;
325
+ readonly job?: TransferJobView | undefined;
326
+ readonly error?: string | undefined;
327
+ }
328
+ /**
329
+ * The buffer's wire vocabulary, shared by the host route and the browser face.
330
+ *
331
+ * Deliberately free of value imports: the browser half imports this module, and
332
+ * anything it pulls in would be bundled into `client.js`. Identifiers that the
333
+ * host validates against (a request `action`, a ticket `state`) are plain
334
+ * strings here and narrowed on the host side.
335
+ *
336
+ * Session ids travel as plain strings rather than the branded `SessionId`
337
+ * because this is a JSON boundary; the host converts at the edges.
338
+ */
339
+ /** The exact `/api` path the pipe UI talks to. */
340
+ export declare const DSHELL_BUFFER_PATH = "/api/dshell/buffer";
341
+ /** The plugin name every buffer-authored message carries as its provenance. */
342
+ export declare const BUFFER_PLUGIN = "dshell-buffer";
343
+ /** Lifecycle of one deferred request. */
344
+ export type BufferTicketState = 'queued' | 'running' | 'done' | 'failed' | 'timeout' | 'cancelled';
345
+ /** States after which a ticket is settled and never changes again. */
346
+ export declare const SETTLED_STATES: readonly BufferTicketState[];
347
+ /** One directory the grant may confer on an area. */
348
+ export type BufferRight = 'read' | 'write';
349
+ /**
350
+ * One directory (or file) the grant confers on an area, with the rights it
351
+ * holds there.
352
+ *
353
+ * `as` is the area's name in the grantee's buffer namespace: the grantee
354
+ * addresses everything under it as `/name/sub/file` instead of by the
355
+ * granter's real path. One segment, no separators — it is a mount name, not a
356
+ * path. The service ASSIGNS it at creation (the caller's `as`, else the path's
357
+ * last segment, suffixed to stay unique among the names that session already
358
+ * holds), and it is the only handle the two sessions exchange. It is optional
359
+ * in the type alone: state written before names existed has none until the
360
+ * service names it at load.
361
+ */
362
+ export interface BufferArea {
363
+ /** Absolute path in the GRANTER's namespace, or one relative to its cwd. */
364
+ readonly path: string;
365
+ readonly rights: readonly BufferRight[];
366
+ /** The area's name in the grantee's buffer namespace. */
367
+ readonly as?: string | undefined;
368
+ }
369
+ /**
370
+ * A durable link between two sessions, established by the user.
371
+ *
372
+ * Undirected: the user connects two sessions, and either may then delegate to
373
+ * the other. Direction belongs to the ticket and to the grant, not to the link
374
+ * — a "pipe" is a relationship, not a one-way channel.
375
+ */
376
+ export interface BufferLink {
377
+ readonly id: string;
378
+ /** The two connected sessions, ordered as the user created them. */
379
+ readonly a: string;
380
+ readonly b: string;
381
+ readonly label?: string | undefined;
382
+ readonly createdAt: number;
383
+ }
384
+ /** One progress report a worker filed against a ticket. */
385
+ export interface BufferReport {
386
+ readonly time: number;
387
+ readonly text: string;
388
+ readonly kind: 'progress' | 'blocked';
389
+ }
390
+ /**
391
+ * One deferred request from one session to another.
392
+ *
393
+ * The lifecycle fields are mutable by design: the service is the single writer,
394
+ * and a ticket is a state machine advanced in place before it is persisted.
395
+ */
396
+ export interface BufferTicket {
397
+ readonly id: string;
398
+ readonly linkId: string;
399
+ /** The requesting session. */
400
+ readonly from: string;
401
+ /** The working session. */
402
+ readonly to: string;
403
+ readonly subject: string;
404
+ readonly detail?: string | undefined;
405
+ state: BufferTicketState;
406
+ /** Grants this ticket holds open; each is released when it settles. */
407
+ readonly grantIds: readonly string[];
408
+ readonly createdAt: number;
409
+ startedAt?: number | undefined;
410
+ settledAt?: number | undefined;
411
+ /** When the watchdog settles the ticket as `timeout`, if nothing else did. */
412
+ readonly deadlineAt: number;
413
+ result?: string | undefined;
414
+ error?: string | undefined;
415
+ readonly reports: BufferReport[];
416
+ }
417
+ /** A scoped, revocable folder grant, alive while at least one ticket references it. */
418
+ export interface BufferGrant {
419
+ readonly id: string;
420
+ /** The granting session — the tree these areas live in. */
421
+ readonly from: string;
422
+ /** The session allowed to touch them. */
423
+ readonly to: string;
424
+ /** The granter's own account of what the areas are for. */
425
+ readonly description: string;
426
+ readonly areas: readonly BufferArea[];
427
+ /** Outstanding tickets; reaching zero revokes the grant immediately. */
428
+ count: number;
429
+ readonly createdAt: number;
430
+ revokedAt?: number | undefined;
431
+ }
432
+ /**
433
+ * One cross-world chunked transfer, in flight or freshly finished.
434
+ *
435
+ * In-memory only: it exists so progress surfaces (the status card) can show
436
+ * live movement, and entries drop shortly after they settle. Nothing here is
437
+ * durable state — a restart simply loses the progress view, never the data.
438
+ */
439
+ export interface BufferTransfer {
440
+ readonly id: string;
441
+ /** The session whose tool call drives the transfer. */
442
+ readonly sessionId: string;
443
+ /** Human label: source path → destination path. */
444
+ readonly label: string;
445
+ readonly bytesDone: number;
446
+ readonly bytesTotal: number;
447
+ readonly chunksDone: number;
448
+ readonly chunksTotal: number;
449
+ readonly startedAt: number;
450
+ readonly finishedAt?: number | undefined;
451
+ readonly error?: string | undefined;
452
+ }
453
+ /** Everything the pipe UI renders from. */
454
+ export interface BufferState {
455
+ readonly links: readonly BufferLink[];
456
+ readonly tickets: readonly BufferTicket[];
457
+ readonly grants: readonly BufferGrant[];
458
+ /** Transfers in flight, plus the freshly settled ones (pruned after a beat). */
459
+ readonly transfers: readonly BufferTransfer[];
460
+ /**
461
+ * Sessions that have been deleted from dshell but are still in dsh's own
462
+ * session list, because dsh cannot tear a loaded session down: their log is
463
+ * removed at the next start. The pipe UI must not draw them — their pipes are
464
+ * already gone, so they would appear as orphan nodes.
465
+ *
466
+ * Process-lifetime, never persisted: after a restart these ids are purged
467
+ * during composition and leave dsh's list, so there is nothing left to hide.
468
+ * Populated only by the deletion path (`detachSession`), never by archiving —
469
+ * and released again by `restoreSession`, because cancelling a scheduled
470
+ * deletion puts the session back in use while dsh still lists it.
471
+ */
472
+ readonly departed: readonly string[];
473
+ }
474
+ /**
475
+ * One row of the pipe detail page's buffer browser.
476
+ *
477
+ * A ROOT entry (one per mapped area) carries the grant it belongs to and its
478
+ * provenance, so the user can see which side offered it and descend into it;
479
+ * entries below the root are plain directory children.
480
+ */
481
+ export interface BufferUserEntry {
482
+ /** Last segment for children; the mapped name (`as`) for a root. */
483
+ readonly name: string;
484
+ readonly kind: 'directory' | 'file' | 'other';
485
+ readonly size?: number | undefined;
486
+ /** Root entries only: the grant this mapping belongs to. */
487
+ readonly grantId?: string;
488
+ /** Root entries only: the mapped name and its rights. */
489
+ readonly as?: string;
490
+ readonly rights?: readonly string[];
491
+ /** Root entries only: the two ends of the grant (granter → grantee). */
492
+ readonly from?: string;
493
+ readonly to?: string;
494
+ /** Root entries only: the real path behind the mapping, in the granter's world. */
495
+ readonly origin?: string;
496
+ }
497
+ /** One answer to a `buffer-ls` request. */
498
+ export interface BufferListing {
499
+ /** The real path listed (root listings answer `/`). */
500
+ readonly path: string;
501
+ readonly entries: readonly BufferUserEntry[];
502
+ readonly truncated: boolean;
503
+ }
504
+ /** One browser face request. `state` also travels as the GET shape. */
505
+ export type BufferRequest = {
506
+ readonly action: 'state';
507
+ } | {
508
+ readonly action: 'link';
509
+ readonly a: string;
510
+ readonly b: string;
511
+ readonly label?: string;
512
+ } | {
513
+ readonly action: 'unlink';
514
+ readonly linkId: string;
515
+ } | {
516
+ readonly action: 'revoke';
517
+ readonly grantId: string;
518
+ } | {
519
+ readonly action: 'cancel';
520
+ readonly ticketId: string;
521
+ } | {
522
+ readonly action: 'buffer-ls';
523
+ readonly linkId: string;
524
+ readonly grantId?: string;
525
+ readonly path?: string;
526
+ };
527
+ /** One browser face response: the committed state plus an optional refusal. */
528
+ export interface BufferResponse extends BufferState {
529
+ readonly error?: string | undefined;
530
+ /** Present only on a `buffer-ls` request. */
531
+ readonly listing?: BufferListing | undefined;
532
+ }
533
+ /**
534
+ * dshell-ssh wire — one exact `/api/dshell/ssh` route, plus the device shapes
535
+ * both halves agree on.
536
+ *
537
+ * The route carries what must not travel through the settings document: the
538
+ * private keys themselves. A device's durable record is small JSON; its key is
539
+ * a separate file written 0600 under `$DSH_HOME/dshell/ssh/keys/`.
540
+ *
541
+ * This module is imported by the browser half through the package's
542
+ * `./protocol` subpath, so it must stay free of value imports: anything it
543
+ * pulls in would be bundled into the client.
544
+ */
545
+ /** Exact `/api` route path owned by the SSH device registry. */
546
+ export declare const DSHELL_SSH_PATH = "/api/dshell/ssh";
547
+ /** Settings namespace owned by this plugin; also the device card's slot key. */
548
+ export declare const SSH_SETTINGS_NAMESPACE = "dshell-ssh";
549
+ /** How a device authenticates. */
550
+ export type DeviceAuth = 'key' | 'password';
551
+ /** One configured device as the UI sees it — never includes secret material. */
552
+ export interface DeviceView {
553
+ readonly id: string;
554
+ /** Display name the session picker lists. */
555
+ readonly name: string;
556
+ /** Hostname or IP `ssh` connects to. */
557
+ readonly host: string;
558
+ /** TCP port; 22 unless the device listens elsewhere. */
559
+ readonly port: number;
560
+ /** Login user. */
561
+ readonly user: string;
562
+ /** Directory a session bound to this device starts in (the remote path). */
563
+ readonly remoteRoot: string;
564
+ /** Selected login method. */
565
+ readonly auth: DeviceAuth;
566
+ /** Whether the secret for {@link auth} (key or password) is stored. */
567
+ readonly hasSecret: boolean;
568
+ }
569
+ /** One device as submitted by the UI; `key` is write-only. */
570
+ export interface DeviceInput {
571
+ readonly id?: string | undefined;
572
+ readonly name: string;
573
+ readonly host: string;
574
+ readonly port?: number | undefined;
575
+ readonly user: string;
576
+ readonly remoteRoot?: string | undefined;
577
+ /** Login method; defaults to `key` on create. */
578
+ readonly auth?: DeviceAuth | undefined;
579
+ /**
580
+ * PEM/OpenSSH private key contents, used when `auth` is `key`. Omitted keeps
581
+ * the stored secret; empty string removes it (the device then relies on the
582
+ * harness user's own ssh agent and config).
583
+ */
584
+ readonly key?: string | undefined;
585
+ /**
586
+ * Password, used when `auth` is `password`. Same omitted/empty semantics as
587
+ * {@link key}.
588
+ */
589
+ readonly password?: string | undefined;
590
+ }
591
+ /** One session's device binding, kept host-side because execution routing needs it. */
592
+ export interface DeviceBinding {
593
+ readonly sessionId: string;
594
+ readonly deviceId: string;
595
+ /**
596
+ * Directory the session's commands run in on that device, when the session
597
+ * overrides the device's own. Absent means the device's `remoteRoot`.
598
+ */
599
+ readonly remoteRoot?: string | undefined;
600
+ /**
601
+ * Local directory standing in for that remote tree, which is also the
602
+ * session's own working directory. Absent on bindings written before
603
+ * mount directories existed.
604
+ */
605
+ readonly mount?: string | undefined;
606
+ }
607
+ /** One request body the route accepts; `list` is also the GET shape. */
608
+ export type SshRequest = {
609
+ readonly action: 'list';
610
+ } | {
611
+ readonly action: 'save';
612
+ readonly device: DeviceInput;
613
+ } | {
614
+ readonly action: 'delete';
615
+ readonly deviceId: string;
616
+ } | {
617
+ readonly action: 'test';
618
+ readonly deviceId: string;
619
+ /**
620
+ * Session directory to also prove creatable, so a failing `mkdir` is found
621
+ * before a session is created rather than after. Absent checks only the
622
+ * connection.
623
+ */
624
+ readonly remoteRoot?: string | null;
625
+ } | {
626
+ readonly action: 'bind';
627
+ readonly sessionId: string;
628
+ readonly deviceId: string | null;
629
+ /** Remote directory for this session; null or absent uses the device's. */
630
+ readonly remoteRoot?: string | null;
631
+ /** Local mount directory for that tree, as returned by `mount`. */
632
+ readonly mount?: string | null;
633
+ } | {
634
+ /**
635
+ * The local mount directory for one device tree. The rule is host-owned
636
+ * (it depends on `$DSH_HOME`), so the browser asks rather than deriving it.
637
+ */
638
+ readonly action: 'mount';
639
+ readonly deviceId: string;
640
+ readonly remoteRoot?: string | null;
641
+ };
642
+ /** One response body; `error` is a refusal the UI shows verbatim. */
643
+ export interface SshResponse {
644
+ readonly devices: readonly DeviceView[];
645
+ readonly bindings: readonly DeviceBinding[];
646
+ /** Human-readable result of the last `test`, when one was requested. */
647
+ readonly testResult?: string | undefined;
648
+ /** Local mount directory, answering the `mount` action. */
649
+ readonly mountPath?: string | undefined;
650
+ readonly error?: string | undefined;
651
+ }
652
+ /**
653
+ * dshell session-panel wire — design 4.7 follow-up.
654
+ *
655
+ * The sidebar's session rows need two things dsh does not offer: an archive
656
+ * tag that hides a session without touching its log, and a history purge.
657
+ * Both are dshell concepts, so they travel over dshell's own exact route on
658
+ * the shared `/api` channel rather than through the Typert Remote table
659
+ * (whose client artifacts are generated from dsh's own packages).
660
+ *
661
+ * The path sits below the shared channel exactly like file-upload's: the
662
+ * physical carrier applies dsh's trust and authentication policy before the
663
+ * handler ever runs, so the route itself does no authorization.
664
+ *
665
+ * Shared by both halves of the package, so the constant and the shapes live
666
+ * in one file that neither the host nor the client bundle owns.
667
+ */
668
+ /** Exact `/api` route path owned by the dshell session panel. */
669
+ export declare const DSHELL_SESSIONS_PATH = "/api/dshell/sessions";
670
+ /** One request body the route accepts; `list` is also the GET shape. */
671
+ export type SessionRequest = {
672
+ readonly action: 'list';
673
+ } | {
674
+ readonly action: 'archive';
675
+ readonly sessionId: string;
676
+ } | {
677
+ readonly action: 'unarchive';
678
+ readonly sessionId: string;
679
+ } | {
680
+ readonly action: 'delete';
681
+ readonly sessionId: string;
682
+ };
683
+ /**
684
+ * One response body. `archived` rides on every response — the tag set after
685
+ * the request, so one round trip leaves the caller's snapshot current.
686
+ * `error` is a refusal the sidebar shows verbatim; it is not a transport
687
+ * failure, so the response still carries a 2xx status.
688
+ */
689
+ export interface SessionResponse {
690
+ readonly archived: readonly string[];
691
+ /**
692
+ * Archived ids whose log is scheduled for removal at the next start (the
693
+ * session was still loaded in this process, so its writer would have
694
+ * recreated the directory). Always a subset of `archived`.
695
+ */
696
+ readonly pendingPurge?: readonly string[];
697
+ readonly error?: string;
698
+ }
699
+ export declare const DSHELL_PTY_PATH = "/api/dshell/pty";
700
+ export interface DshellPtyRequest {
701
+ readonly action: 'history';
702
+ /** The session whose shell history to read. */
703
+ readonly sessionId: string;
704
+ /**
705
+ * The line being typed, when the caller wants the history searched rather
706
+ * than listed: only commands *starting with* it come back, compared
707
+ * case-insensitively. Omitted (or empty) means the newest commands, which is
708
+ * plain history for an empty composer.
709
+ *
710
+ * The query travels to the host because the host owns the index; a caller
711
+ * that filters locally can only ever see the rows it was sent.
712
+ */
713
+ readonly draft?: string | undefined;
714
+ /**
715
+ * How many matching commands to answer with, newest first (the reply is still
716
+ * ordered oldest-first for the list). Omitted means the route's own cap.
717
+ */
718
+ readonly limit?: number | undefined;
719
+ }
720
+ /** One command the shell ran, as the composer lists it. */
721
+ export interface DshellPtyCommand {
722
+ /** The assembled command line. */
723
+ readonly command: string;
724
+ /** Bash's exit status, or null when the marker carried none. */
725
+ readonly exitCode: number | null;
726
+ /** Epoch ms the command finished. */
727
+ readonly at: number;
728
+ }
729
+ /** One answer: the commands, oldest first (the order they ran in). */
730
+ export interface DshellPtyResponse {
731
+ readonly commands?: readonly DshellPtyCommand[] | undefined;
732
+ readonly error?: string | undefined;
733
+ }
734
+ /**
735
+ * Downstream half of the frame stream: a long-lived GET whose response body is
736
+ * newline-delimited JSON, one bridge frame per line.
737
+ *
738
+ * The wire itself is unchanged — these are the very frames the ws upgrade
739
+ * carries — but a Response body is reachable from any composition the
740
+ * `connection` service is in, while `registerUpgrade` needs `webServer`, which
741
+ * the desktop shell deliberately does not compose. Identity is explicit because
742
+ * an HTTP stream has no socket to key on: both halves carry the client's own
743
+ * `clientId`, and the GET carries the bind (`sessionId` + `stream`) that a ws
744
+ * would have sent as a frame.
745
+ */
746
+ export declare const DSHELL_STREAM_PATH = "/api/dshell/stream";
747
+ /**
748
+ * Upstream half: one POST per control frame (`input`, `resize`, `signal`,
749
+ * `agent-open`, `reconnect`), identified by the same `clientId` the GET opened
750
+ * with. Answers with no body; the stream is the only place results appear.
751
+ */
752
+ export declare const DSHELL_STREAM_SEND_PATH = "/api/dshell/stream/send";
753
+ //# sourceMappingURL=contracts.d.ts.map