aegis-desktop 1.0.13 → 1.0.15

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 (96) hide show
  1. package/SOURCE-OFFER.txt +9 -0
  2. package/bin/aegis.js +1 -32
  3. package/lib/account-key.js +1 -56
  4. package/lib/auto.js +1 -430
  5. package/lib/avatar/companion.js +1 -698
  6. package/lib/avatar/cosmetics.js +1 -532
  7. package/lib/avatar/events.js +1 -344
  8. package/lib/avatar/holder-turn.js +1 -350
  9. package/lib/avatar/holders.js +1 -612
  10. package/lib/avatar/identity.js +1 -641
  11. package/lib/avatar/level.js +1 -368
  12. package/lib/avatar/mirror.js +1 -524
  13. package/lib/avatar/persona.js +1 -388
  14. package/lib/avatar/profile.js +1 -977
  15. package/lib/avatar/register.js +1 -307
  16. package/lib/avatar/store.js +1 -472
  17. package/lib/avatar/tiers.js +1 -38
  18. package/lib/avatar/transfer.js +1 -415
  19. package/lib/avatar/turn.js +1 -216
  20. package/lib/avatar/voice.js +1 -328
  21. package/lib/avatar/xp.js +1 -404
  22. package/lib/deep-link.js +1 -120
  23. package/lib/fingerprint.js +2 -347
  24. package/lib/google-connect.js +1 -169
  25. package/lib/local/agents.js +1 -105
  26. package/lib/local/autonomous.js +1 -1045
  27. package/lib/local/claude-pro.js +1 -993
  28. package/lib/local/context.js +1 -95
  29. package/lib/local/engine.js +1 -3228
  30. package/lib/local/entitlement.js +1 -654
  31. package/lib/local/git-scope.js +1 -228
  32. package/lib/local/local.js +1 -2135
  33. package/lib/local/prompt.js +1 -275
  34. package/lib/local/queue.js +1 -560
  35. package/lib/local/session-rounds.js +1 -208
  36. package/lib/local/shell.js +1 -680
  37. package/lib/local/telemetry.js +1 -470
  38. package/lib/local/tools.js +1 -1238
  39. package/lib/local/turn-guard.js +1 -300
  40. package/lib/local/worktree-lock.js +1 -235
  41. package/lib/provider-key-mirror.js +1 -252
  42. package/lib/quick-launcher.js +1 -84
  43. package/lib/settings.js +1 -890
  44. package/lib/sync/memory-queue.js +1 -60
  45. package/lib/sync/persist-gate.js +1 -148
  46. package/lib/sync/sessions.js +1 -106
  47. package/lib/window-state.js +1 -66
  48. package/main.js +3 -4390
  49. package/package.json +3 -5
  50. package/preload.js +3 -628
  51. package/renderer/app.js +1 -6560
  52. package/renderer/avatar/assemble.js +1 -335
  53. package/renderer/avatar/avatar.js +1 -380
  54. package/renderer/avatar/cards.js +1 -141
  55. package/renderer/avatar/hud.js +1 -206
  56. package/renderer/avatar/machine.js +1 -213
  57. package/renderer/avatar/pane.js +1 -439
  58. package/renderer/avatar/parts.js +1 -437
  59. package/renderer/budget.js +1 -168
  60. package/renderer/companion.js +1 -223
  61. package/renderer/diff.js +1 -359
  62. package/renderer/diffview.js +1 -336
  63. package/renderer/fingerprint.js +3 -720
  64. package/renderer/markdown.js +1 -168
  65. package/renderer/provider-toolbar.js +1 -134
  66. package/renderer/providers.js +1 -259
  67. package/renderer/quick.js +1 -191
  68. package/renderer/stream-policy.js +1 -306
  69. package/renderer/transcript-view.js +1 -323
  70. package/renderer/usage.js +1 -1239
  71. package/vendor/aegis.js +1 -2054
  72. package/vendor/attest.js +1 -442
  73. package/vendor/authorship-key.js +1 -410
  74. package/vendor/auto-provision.js +1 -134
  75. package/vendor/brain-catalog.js +1 -268
  76. package/vendor/byok-catalog.js +1 -230
  77. package/vendor/credentials.js +1 -494
  78. package/vendor/device-fingerprint.js +1 -141
  79. package/vendor/drive-connect.js +1 -85
  80. package/vendor/drive-upload.js +1 -174
  81. package/vendor/env-file.js +1 -1273
  82. package/vendor/fingerprint.js +1 -1064
  83. package/vendor/foreign-memory.js +1 -669
  84. package/vendor/google-signin.js +1 -125
  85. package/vendor/hooks.js +1 -430
  86. package/vendor/model-discovery.js +1 -330
  87. package/vendor/platform.js +1 -937
  88. package/vendor/prefs.js +1 -351
  89. package/vendor/presence-constants.js +1 -107
  90. package/vendor/presence.js +1 -438
  91. package/vendor/record-vault.js +3 -1055
  92. package/vendor/session-store.js +1 -548
  93. package/vendor/topup.js +1 -297
  94. package/vendor/touch.js +1 -458
  95. package/vendor/update.js +1 -704
  96. package/vendor/user-models.js +1 -381
@@ -1,682 +1,3 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
2
  // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
- // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
- // Conveying a modified copy, or running one as a network service, owes the
5
- // corresponding source under this licence (§5/§13) — see LICENSING.md.
6
- 'use strict';
7
-
8
- /**
9
- * shell.js — a persistent shell session for the `exec` tool, ported from
10
- * the upstream engine reference (client half of the same design).
11
- *
12
- * The desktop `exec` tool used to spawn ONE process per call: `cd /foo` in
13
- * one turn had no effect on the next call, so a model that wanted to work
14
- * inside a subdirectory had to prefix every single command with `cd X &&`.
15
- * This keeps ONE long-lived shell per chat turn — a probed POSIX sh/bash on
16
- * macOS/Linux, PowerShell on Windows (no bash there by default) — feeding it
17
- * commands over stdin and framing each command's output with a per-session
18
- * random sentinel printed alongside the exit code. On POSIX, stderr is merged
19
- * into stdout (`exec 2>&1`) so ordering is preserved; PowerShell's stderr pipe
20
- * is merged the same way by listening on both streams.
21
- *
22
- * Phase 30.3 — the shell is PROBED, never assumed. Until this step the session
23
- * spawned `process.env.SHELL || '/bin/bash'` and then fed it brace groups, `exec
24
- * 2>&1` and `$?` — all POSIX-only. Windows was handled; a POSIX host whose
25
- * `$SHELL` is fish, csh or nushell was not, and those three reject that syntax
26
- * outright (a brace group to fish is not a syntax error you can ignore: it
27
- * silently runs the wrong thing and eats the sentinel, hanging the session to
28
- * its timeout). So `resolveSessionShell()` below only ever returns a shell it
29
- * has actually EXECUTED once with a POSIX-only probe script, and the dialect the
30
- * script builder uses follows the verified dialect — `this.shell.kind` — rather
31
- * than the OS. A `$SHELL` naming a known-non-POSIX shell (fish/csh/tcsh/nu/
32
- * xonsh) is never adopted and never probed; it is named in the evidence trail
33
- * and in the caveat, and if no sh/bash can be verified at all the session
34
- * REFUSES with a message naming it, instead of quietly feeding it POSIX syntax.
35
- *
36
- * Self-contained (node:child_process + node:crypto + the shared
37
- * client/platform.js) and never throws: run() always resolves
38
- * { content, isError }. If the session can't start or dies, run() falls back to
39
- * a one-shot spawn so `exec` keeps working.
40
- */
41
-
42
- const { spawn, spawnSync } = require('node:child_process');
43
- const crypto = require('node:crypto');
44
-
45
- // The ONE platform module (client/platform.js, Phase 30.1) — resolved the same
46
- // two ways this repo resolves every shared module (see tools.js/engine.js):
47
- // repo-relative in a checkout and in the CLI's mirrored vendor tree, then the
48
- // staged app-dir copy a packaged desktop build carries. Executable lookup goes
49
- // through it (`resolveExecutable`) rather than through a path literal, so the
50
- // Windows half of that search is exercised by tests on this Linux host.
51
- let platform;
52
- try {
53
- platform = require('../../../client/platform.js');
54
- } catch (err) {
55
- try {
56
- platform = require('../../vendor/platform.js');
57
- } catch {
58
- throw new Error(
59
- `shell.js: cannot load the shared platform module (client/platform.js, ` +
60
- `or its staged copy): ${err && err.message ? err.message : err}`
61
- );
62
- }
63
- }
64
-
65
- const IS_WIN32 = platform.IS_WIN32;
66
- const OUTPUT_CAP = 30_000;
67
- const MAX_TIMEOUT = 600_000;
68
- const DEFAULT_TIMEOUT = 120_000;
69
-
70
- // ---------------------------------------------------------------------------
71
- // Dialect resolution (Phase 30.3)
72
- // ---------------------------------------------------------------------------
73
-
74
- /**
75
- * The shells whose `-c` we are allowed to treat as POSIX sh. Anything else —
76
- * fish, csh/tcsh, nushell, xonsh, elvish, pwsh — is a different LANGUAGE, not a
77
- * differently-named sh: it is never spawned for a session and never handed the
78
- * brace-group framing below.
79
- */
80
- const POSIX_SHELL_NAMES = ['bash', 'sh', 'dash', 'ksh', 'zsh'];
81
-
82
- /** Substitute candidates, in order, when `$SHELL` is unusable. */
83
- const POSIX_FALLBACK_NAMES = ['bash', 'sh'];
84
-
85
- /**
86
- * The probe script: builtins only (no PATH, no filesystem, no output), and
87
- * POSIX-only syntax — fish, csh and nu all reject this construct, so a clean
88
- * exit 0 is evidence that `-c` really runs POSIX sh here, rather than evidence
89
- * that a binary with the right name exists. Same probe the engine's Phase 21b
90
- * resolution uses.
91
- */
92
- const POSIX_PROBE = 'if [ 1 -eq 1 ]; then exit 0; fi';
93
- const SHELL_PROBE_TIMEOUT = 4000;
94
-
95
- /** Last path segment of a shell reference, minus a `.exe`, lower-cased. */
96
- function shellName(cmd) {
97
- return String(cmd || '')
98
- .trim()
99
- .split(/[\\/]/)
100
- .pop()
101
- .toLowerCase()
102
- .replace(/\.exe$/, '');
103
- }
104
-
105
- /** True for the shells whose `-c` may be treated as POSIX sh. */
106
- function isPosixShellName(cmd) {
107
- return POSIX_SHELL_NAMES.includes(shellName(cmd));
108
- }
109
-
110
- /** The real probe: spawn `cmd -c <POSIX_PROBE>`, true only on a clean exit 0. */
111
- function probePosixShell(cmd, { env, timeout = SHELL_PROBE_TIMEOUT } = {}) {
112
- try {
113
- const res = spawnSync(cmd, ['-c', POSIX_PROBE], {
114
- env,
115
- encoding: 'utf8',
116
- timeout,
117
- windowsHide: true,
118
- stdio: 'ignore',
119
- });
120
- return !res.error && res.status === 0;
121
- } catch {
122
- return false;
123
- }
124
- }
125
-
126
- /** Normalize the injected seams, defaulting each to the real thing. */
127
- function shellSeams(options) {
128
- const o = options && typeof options === 'object' ? options : {};
129
- return {
130
- platform: typeof o.platform === 'string' && o.platform ? o.platform : process.platform,
131
- env: o.env && typeof o.env === 'object' ? o.env : process.env,
132
- exists: typeof o.exists === 'function' ? o.exists : undefined,
133
- probe: typeof o.probe === 'function' ? o.probe : undefined,
134
- injected: typeof o.probe === 'function' || typeof o.exists === 'function',
135
- };
136
- }
137
-
138
- /**
139
- * Try one candidate. Returns a verified spec or null, recording why in `tried`
140
- * and every shell it actually executed in `executed`.
141
- *
142
- * The name gate comes FIRST and applies to every candidate, including `$SHELL`:
143
- * fish is refused on its name, before a probe could ever adopt it, so there is
144
- * no ordering in which a fish gets spawned and then handed POSIX syntax.
145
- */
146
- function considerPosixCandidate(o, candidate, tried, executed) {
147
- const raw = String(candidate.cmd || '').trim();
148
- if (!raw) {
149
- tried.push({ candidate: candidate.source || '(unset)', ok: false, reason: 'unset' });
150
- return null;
151
- }
152
- if (!isPosixShellName(raw)) {
153
- tried.push({
154
- candidate: raw,
155
- ok: false,
156
- reason:
157
- `${candidate.source || 'candidate'} is not a POSIX shell — ${shellName(raw) || raw} is a ` +
158
- 'different language (fish/csh/nu), so it was never executed and never sent POSIX syntax',
159
- });
160
- return null;
161
- }
162
- const resolved = platform.resolveExecutable(raw, {
163
- platform: o.platform,
164
- env: o.env,
165
- exists: o.exists,
166
- });
167
- if (!resolved) {
168
- tried.push({
169
- candidate: raw,
170
- ok: false,
171
- reason: /[\\/]/.test(raw) ? 'not present' : 'not found on PATH',
172
- });
173
- return null;
174
- }
175
- const ok = o.probe
176
- ? Boolean(o.probe(resolved, ['-c', POSIX_PROBE]))
177
- : probePosixShell(resolved, { env: o.env });
178
- executed.push(resolved);
179
- if (!ok) {
180
- tried.push({
181
- candidate: resolved,
182
- ok: false,
183
- reason: `failed the POSIX probe (\`-c\` with \`${POSIX_PROBE}\` did not exit 0) — refused`,
184
- });
185
- return null;
186
- }
187
- tried.push({
188
- candidate: resolved,
189
- ok: true,
190
- reason: 'verified: one `-c` POSIX script exited 0',
191
- });
192
- return { ok: true, cmd: resolved, arg: '-c', kind: 'posix-sh', source: candidate.source, verified: true };
193
- }
194
-
195
- /**
196
- * Resolve the session shell on a POSIX host, with the evidence for the answer.
197
- *
198
- * Returns { ok, cmd, arg, kind, source, verified, unsupported, caveat, error,
199
- * tried[] }:
200
- *
201
- * - `$SHELL` wins when it NAMES a POSIX shell and passes the probe;
202
- * - a `$SHELL` naming fish/csh/nu/xonsh (or anything else non-POSIX, including
203
- * garbage) is refused by name, recorded in `tried`, and named in `caveat`
204
- * when a substitute is found and in `error` when none can be;
205
- * - when `$SHELL` is unusable the POSIX dialect runs through an explicit
206
- * `bash`/`sh` found on PATH — resolved by client/platform.js, not by a path
207
- * literal — and only after that substitute passed the probe itself;
208
- * - `ok:false` means NO shell was verified: the caller must report `error`
209
- * (which names the unsupported `$SHELL`) instead of spawning anything.
210
- */
211
- function resolvePosixSessionShell(o) {
212
- const tried = [];
213
- const executed = [];
214
- const envShell = String((o.env && o.env.SHELL) || '').trim();
215
- const candidates = [];
216
-
217
- if (!envShell) {
218
- tried.push({ candidate: '$SHELL', ok: false, reason: '$SHELL is unset' });
219
- } else {
220
- candidates.push({ cmd: envShell, source: '$SHELL' });
221
- }
222
- for (const name of POSIX_FALLBACK_NAMES) {
223
- candidates.push({ cmd: name, source: `PATH:${name}` });
224
- }
225
-
226
- let hit = null;
227
- for (const candidate of candidates) {
228
- hit = considerPosixCandidate(o, candidate, tried, executed);
229
- if (hit) break;
230
- }
231
-
232
- const unsupported = envShell && !isPosixShellName(envShell) ? envShell : null;
233
- if (hit) {
234
- return {
235
- ...hit,
236
- unsupported,
237
- caveat: unsupported
238
- ? `$SHELL is ${unsupported}, which is not a POSIX shell — the session runs the POSIX ` +
239
- `dialect through ${hit.cmd} instead, and ${unsupported} is never sent brace groups, ` +
240
- '`exec 2>&1` or `$?`'
241
- : null,
242
- error: null,
243
- executed,
244
- tried,
245
- };
246
- }
247
-
248
- const error = unsupported
249
- ? `exec: refusing to run — $SHELL is "${unsupported}", which is not a POSIX shell ` +
250
- '(fish/csh/nushell are a different language), and no sh/bash could be verified on PATH ' +
251
- 'to run the POSIX dialect instead. Set SHELL to bash or sh, or put one on PATH.'
252
- : `exec: no POSIX shell available — $SHELL is ${envShell ? `"${envShell}"` : 'unset'} and no ` +
253
- 'sh/bash passed the POSIX probe; set SHELL to bash or sh, or put one on PATH.';
254
- return {
255
- ok: false,
256
- cmd: null,
257
- arg: null,
258
- kind: null,
259
- source: 'refused',
260
- verified: false,
261
- unsupported,
262
- caveat: null,
263
- error,
264
- executed,
265
- tried,
266
- };
267
- }
268
-
269
- // Verified verdicts are cached per (platform, $SHELL, PATH) so the probe runs
270
- // once per process — but never when a seam was injected: a test double must not
271
- // be able to poison the real verdicts.
272
- const SHELL_CACHE = new Map();
273
-
274
- /**
275
- * Which shell runs the session here, and the evidence for that answer.
276
- *
277
- * Windows keeps PowerShell (there is no bash there by default), exactly as
278
- * before; everywhere else the answer comes from resolvePosixSessionShell above
279
- * and is only ever a shell that passed the POSIX probe. `options` is the same
280
- * injected seam the shared platform module takes —
281
- * `{ platform, env, exists, probe }` — so every branch is exercised on this
282
- * Linux host (test/shell-dialect.test.mjs) rather than on a runner this project
283
- * does not have.
284
- */
285
- function resolveSessionShell(options = {}) {
286
- const o = shellSeams(options);
287
- if (o.platform === 'win32') {
288
- const cmdSpec = platform.shellSpec({ platform: 'win32', env: o.env });
289
- const powershell =
290
- platform.resolveExecutable('powershell', { platform: 'win32', env: o.env, exists: o.exists }) ||
291
- 'powershell.exe';
292
- // ONE-SHOT NOW SPEAKS THE SESSION'S DIALECT TOO (2026-02-14 report, item 1).
293
- //
294
- // This used to be `oneShot: cmdSpec` — PowerShell for the session, cmd.exe
295
- // for every one-shot — and that mismatch is what made `exec` silently wrong
296
- // mid-turn. When the live session died (sentinel timeout, killed child, a
297
- // rejected stdin write) the degrade paths below run one-shot, so from that
298
- // moment on every remaining command in the turn was parsed by cmd.exe while
299
- // the model kept emitting PowerShell quoting. cmd.exe answers
300
- // `'"Write-Output \"' is not recognized as an internal or external command`
301
- // — localized, so on sv-SE it reads `Felaktig syntax för filnamn, katalognamn
302
- // eller volymetikett`, which looks like a filesystem fault and not a shell
303
- // change — and nothing in the tool result said the shell had switched.
304
- //
305
- // `-Command` accepts exactly the quoting the session accepts, so a
306
- // degradation is now a transport change and not a language change. cmd.exe
307
- // is no longer reachable from this path at all: if PowerShell cannot be
308
- // resolved the spawn fails loudly (oneShotShell reports `error: …`) instead
309
- // of quietly running the command in a shell that cannot read it.
310
- const oneShot = {
311
- cmd: powershell,
312
- arg: '-Command',
313
- kind: 'powershell',
314
- source: 'powershell.exe',
315
- };
316
- return {
317
- ok: true,
318
- cmd: powershell,
319
- arg: '-',
320
- kind: 'powershell',
321
- source: 'powershell.exe',
322
- verified: true,
323
- unsupported: null,
324
- caveat: null,
325
- error: null,
326
- oneShot,
327
- executed: [],
328
- tried: [
329
- { candidate: powershell, ok: true, reason: 'the Windows default (PowerShell session AND one-shot)' },
330
- {
331
- candidate: cmdSpec.cmd,
332
- ok: false,
333
- reason: 'cmd.exe is never used for one-shot: it cannot parse the PowerShell quoting the session teaches',
334
- },
335
- ],
336
- };
337
- }
338
-
339
- const key = [o.platform, o.env && o.env.SHELL, o.env && o.env.PATH].join('\u0000');
340
- if (!o.injected && SHELL_CACHE.has(key)) return SHELL_CACHE.get(key);
341
- const spec = resolvePosixSessionShell(o);
342
- if (!o.injected) SHELL_CACHE.set(key, spec);
343
- return spec;
344
- }
345
-
346
- function cap(s) {
347
- return s.length > OUTPUT_CAP ? `${s.slice(0, OUTPUT_CAP)}\n… (truncated)` : s;
348
- }
349
-
350
- // The buffer is BOUNDED WHILE IT FILLS, not just when it is reported.
351
- //
352
- // `cap()` above runs on close, which is fine for the text the user sees but
353
- // useless as a memory bound: between the spawn and the exit the raw stream is
354
- // accumulated in this process with no ceiling at all, so a command that never
355
- // stops printing (`yes`, `cat /dev/zero`, a runaway build log) grows the MAIN
356
- // process's heap for as long as the timeout allows — up to 10 minutes — before
357
- // the truncation it was always going to get. A tool call must not be able to
358
- // OOM the app it runs in.
359
- //
360
- // The window kept is head + tail, because neither end may be dropped:
361
- // - the HEAD is what the user and the model are shown (OUTPUT_CAP already
362
- // says only that much is reported);
363
- // - the TAIL is where the exit sentinel lives for a session (`printf
364
- // '__AEGIS_SH_…EXIT:%d'`) — dropping it would not merely lose output, it
365
- // would hang the session until the timeout, because the resolver scans the
366
- // buffer for a marker that can never arrive.
367
- // 64 KiB of tail is ~1500× the sentinel line, and surviving a chunk boundary
368
- // needs only the sentinel's own length.
369
- const TAIL_KEEP = 64 * 1024;
370
- const BUFFER_CEILING = OUTPUT_CAP + TAIL_KEEP;
371
- const ELISION = '\n… (earlier output elided — command produced more than the buffer ceiling)\n';
372
-
373
- /** Append one stream chunk to `prev`, keeping the bounded head+tail window.
374
- * Pure, so the streaming paths and their tests agree on the policy. */
375
- function appendBounded(prev, chunk) {
376
- const next = prev + chunk;
377
- if (next.length <= BUFFER_CEILING) return next;
378
- // Elide the MIDDLE: head stays visible, tail keeps the sentinel reachable.
379
- // The marker makes the missing bytes explicit rather than letting the two
380
- // halves read as contiguous output.
381
- return next.slice(0, OUTPUT_CAP) + ELISION + next.slice(-TAIL_KEEP);
382
- }
383
-
384
- /**
385
- * The spec for a ONE-SHOT command (no live session) — the same resolution, so
386
- * the `exec` tool's no-session path and this module's fallback cannot disagree
387
- * about which shell speaks the dialect the command was written in.
388
- *
389
- * Windows keeps cmd.exe (`/d /s /c`), exactly what it used before; everywhere
390
- * else the answer is a PROBED POSIX sh/bash, and `{ ok:false, error }` when
391
- * nothing could be verified — the caller reports `error` (which names the
392
- * unsupported `$SHELL`) instead of shelling out in a language it does not speak.
393
- */
394
- function oneShotShellSpec(options = {}) {
395
- const spec = resolveSessionShell(options);
396
- if (!spec.ok) {
397
- return { ok: false, cmd: null, arg: null, kind: null, error: spec.error, spec };
398
- }
399
- if (spec.kind === 'powershell') {
400
- // The session's own one-shot spec (PowerShell `-Command`), so the `exec`
401
- // tool's no-session path and this module's fallback cannot disagree about
402
- // the dialect — which is exactly how cmd.exe used to sneak back in.
403
- const shot = spec.oneShot || platform.shellSpec({ platform: 'win32', env: shellSeams(options).env });
404
- return { ok: true, cmd: shot.cmd, arg: shot.arg, kind: shot.kind || 'powershell', error: null, spec };
405
- }
406
- return { ok: true, cmd: spec.cmd, arg: spec.arg, kind: 'posix-sh', error: null, spec };
407
- }
408
-
409
- /**
410
- * One-shot fallback — the pre-session behavior, used when no live session.
411
- *
412
- * The dialect is resolved the same way the session's is: a POSIX `-c` runs in a
413
- * verified sh/bash (never in an unprobed `$SHELL`), and when nothing can be
414
- * verified the refusal names the shell that was refused rather than shelling out
415
- * to it in a language it does not speak.
416
- */
417
- function oneShotShell({ command, timeout = DEFAULT_TIMEOUT, working_directory, shellOptions } = {}) {
418
- return new Promise((resolve) => {
419
- const o = shellSeams(shellOptions);
420
- const spec = resolveSessionShell(shellOptions);
421
- if (!spec || !spec.ok) {
422
- resolve({ content: (spec && spec.error) || 'exec: no usable shell', isError: true });
423
- return;
424
- }
425
- const call = spec.kind === 'powershell' ? spec.oneShot : spec;
426
- const child = spawn(call.cmd, [call.arg, String(command || '')], {
427
- cwd: working_directory || process.cwd(),
428
- timeout: Math.min(Number(timeout) || DEFAULT_TIMEOUT, MAX_TIMEOUT),
429
- env: o.env,
430
- });
431
- let out = '';
432
- let err = '';
433
- // Bounded both ways: a one-shot command with no live session is just as
434
- // capable of printing forever as a session command (appendBounded's header).
435
- child.stdout.on('data', (d) => { out = appendBounded(out, d.toString()); });
436
- child.stderr.on('data', (d) => { err = appendBounded(err, d.toString()); });
437
- child.on('error', (e) => resolve({ content: `error: ${e.message}`, isError: true }));
438
- child.on('close', (code) => {
439
- const body = `${out}${err ? `\n${err}` : ''}`.trim() || `(exit ${code})`;
440
- resolve({ content: cap(body), isError: code !== 0 });
441
- });
442
- });
443
- }
444
-
445
- class ShellSession {
446
- constructor({ cwd, shellOptions } = {}) {
447
- this.sentinel = `__AEGIS_SH_${crypto.randomBytes(8).toString('hex')}__`;
448
- this.marker = `${this.sentinel}EXIT:`;
449
- this.buf = '';
450
- this.alive = false;
451
- this._pending = null; // resolver-scan for the in-flight command
452
- this.cwd = cwd || process.cwd();
453
- this.disposed = false; // an explicit dispose() must NOT be self-healed
454
- this._respawns = 0; // one self-heal attempt per session, never a loop
455
- this._degradedNoted = false; // the degrade marker is emitted once per session
456
- this.shellOptions = shellOptions || {};
457
- // Resolved BEFORE anything is spawned or written: `shell.kind` is what the
458
- // command framing below branches on, so an unverified `$SHELL` can never
459
- // receive POSIX syntax — it is never spawned at all.
460
- this.shell = resolveSessionShell(this.shellOptions);
461
- this.shellError = this.shell.ok ? null : this.shell.error;
462
- this._start(cwd);
463
- }
464
-
465
- _start(cwd) {
466
- if (!this.shell || !this.shell.ok) {
467
- // Refused: no shell was verified, so nothing is spawned. run() reports
468
- // this.shellError (which names the unsupported $SHELL).
469
- this.alive = false;
470
- return;
471
- }
472
- const env = shellSeams(this.shellOptions).env;
473
- try {
474
- this.child = this.shell.kind === 'powershell'
475
- // -Command - reads the script from stdin as a non-interactive session,
476
- // so there are no prompts to pollute output, but state still persists.
477
- ? spawn(this.shell.cmd, ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', '-'], {
478
- cwd: cwd || process.cwd(),
479
- env,
480
- stdio: ['pipe', 'pipe', 'pipe'],
481
- })
482
- // No args → the shell reads commands from stdin as a non-interactive
483
- // script, so there are no prompts to pollute output, but state persists.
484
- // This is a shell that PASSED the POSIX probe (see resolveSessionShell).
485
- : spawn(this.shell.cmd, [], {
486
- cwd: cwd || process.cwd(),
487
- env,
488
- stdio: ['pipe', 'pipe', 'pipe'],
489
- });
490
- } catch {
491
- this.alive = false;
492
- return;
493
- }
494
- this.alive = true;
495
- const onData = (d) => {
496
- // Bounded — see appendBounded. The tail is kept precisely so the sentinel
497
- // this session's resolver scans for survives a command that floods stdout.
498
- this.buf = appendBounded(this.buf, d.toString());
499
- if (this._pending) this._pending();
500
- };
501
- this.child.stdout.on('data', onData);
502
- this.child.stderr.on('data', onData);
503
- this.child.on('exit', () => { this.alive = false; if (this._pending) this._pending(true); });
504
- this.child.on('error', () => { this.alive = false; if (this._pending) this._pending(true); });
505
- // Merge stderr into stdout for the whole session so output ordering is
506
- // faithful and the sentinel (printed to fd1) always lands after it.
507
- // PowerShell errors already arrive on the stderr pipe onData folds in
508
- // above, so only a POSIX session needs the explicit redirect — and `exec`
509
- // redirection is POSIX, which is why this is gated on the VERIFIED dialect
510
- // rather than on `!IS_WIN32`.
511
- if (this.shell.kind === 'posix-sh') {
512
- try { this.child.stdin.write('exec 2>&1\n'); } catch { this.alive = false; }
513
- }
514
- }
515
-
516
- /**
517
- * Run one command in the session. Resolves { content, isError }. A dead or
518
- * unstartable session (or a working_directory that must not persist)
519
- * degrades gracefully. `working_directory` is scoped to this one command (a
520
- * subshell on POSIX, Push-Location/Pop-Location on PowerShell) so it doesn't
521
- * move the session's cwd.
522
- */
523
- run(command, { timeout = DEFAULT_TIMEOUT, working_directory } = {}) {
524
- const posixSession = this.shell && this.shell.kind === 'posix-sh' && this.shell.verified;
525
- if (!this.alive || !this.child || ((this.shell && this.shell.kind === 'posix-sh') && !posixSession)) {
526
- if (!this.alive && this.shellError) {
527
- return Promise.resolve({ content: this.shellError, isError: true });
528
- }
529
- // SELF-HEAL BEFORE DEGRADING. The old path latched: once the live session
530
- // died (a timeout kill, a child exit, a spawn error) every remaining
531
- // command in the turn ran one-shot, and on Windows that one-shot used to
532
- // be cmd.exe — so the agent stayed silently wrong for the rest of the
533
- // turn. Re-probe and re-spawn ONCE instead, and only then fall through.
534
- if (!this.alive && !this.disposed && this._respawns < 1) {
535
- this._respawns += 1;
536
- this.buf = '';
537
- this.shell = resolveSessionShell(this.shellOptions);
538
- this.shellError = this.shell.ok ? null : this.shell.error;
539
- this._start(this.cwd);
540
- if (this.alive) return this.run(command, { timeout, working_directory });
541
- }
542
- return oneShotShell({ command, timeout, working_directory, shellOptions: this.shellOptions })
543
- .then((result) => this._noteDegraded(result, !this.alive ? 'the persistent session is not running' : 'no usable session dialect'));
544
- }
545
- let cmd = String(command || '');
546
-
547
- return new Promise((resolve) => {
548
- let settled = false;
549
- const finish = (result) => {
550
- if (settled) return;
551
- settled = true;
552
- clearTimeout(timer);
553
- this._pending = null;
554
- resolve(result);
555
- };
556
-
557
- const timer = setTimeout(() => {
558
- // A hung command (e.g. one that reads stdin) can never reach the
559
- // sentinel — kill the poisoned session and report what we have.
560
- const partial = cap(this.buf.trim());
561
- this.dispose();
562
- finish({ content: `${partial}${partial ? '\n' : ''}(timed out after ${timeout}ms)`, isError: true });
563
- }, Math.min(Number(timeout) || DEFAULT_TIMEOUT, MAX_TIMEOUT));
564
-
565
- this._pending = (died) => {
566
- if (died && !this.buf.includes(this.marker)) {
567
- finish({ content: cap(this.buf.trim()) || '(shell session ended)', isError: true });
568
- return;
569
- }
570
- const idx = this.buf.indexOf(this.marker);
571
- if (idx === -1) return; // sentinel not here yet — wait for more data
572
- const after = this.buf.slice(idx + this.marker.length);
573
- const nl = after.indexOf('\n');
574
- if (nl === -1) return; // exit-code line still arriving
575
- const code = parseInt(after.slice(0, nl), 10);
576
- const output = this.buf.slice(0, idx).replace(/\n+$/, '');
577
- this.buf = after.slice(nl + 1); // leftover for the next command (usually '')
578
- finish({ content: cap(output || `(exit ${code})`), isError: code !== 0 });
579
- };
580
-
581
- // Send the command, then print the sentinel + exit code on its own line.
582
- let script;
583
- if (!posixSession) {
584
- // Push-Location/Pop-Location scope working_directory to this one
585
- // command without moving the session's persistent cwd. $__c captures
586
- // the real exit status before Pop-Location's own success would
587
- // otherwise overwrite $?/$LASTEXITCODE.
588
- const body = cmd.trim() ? cmd : 'Write-Output $null';
589
- const exitCapture =
590
- '$__c = if ($LASTEXITCODE -ne $null) { $LASTEXITCODE } elseif ($?) { 0 } else { 1 }';
591
- script = working_directory
592
- ? `Push-Location -LiteralPath ${pshq(working_directory)}\ntry {\n${body}\n${exitCapture}\n} finally {\nPop-Location\n}\n`
593
- : `${body}\n${exitCapture}\n`;
594
- script += `Write-Output ("${this.marker}" + $__c)\n`;
595
- } else {
596
- // POSIX dialect, sent ONLY to a shell that passed the POSIX probe
597
- // (posixSession above) — never to fish/csh/nu. The command runs in a
598
- // brace group with its stdin redirected from /dev/null: a group (not a
599
- // subshell) keeps cd/export state persisting, and `</dev/null` stops the
600
- // command from consuming the control channel. Without this, any
601
- // stdin-reading command (cat, read, a REPL, ssh, a y/n prompt) swallows
602
- // the sentinel line that follows it — hanging the whole session until
603
- // the timeout, and echoing commands (cat) even splice the sentinel into
604
- // their output and mis-resolve with garbage. working_directory runs in a
605
- // subshell (parens, not the brace group) so it doesn't move the
606
- // session's cwd.
607
- if (working_directory) cmd = `( cd ${shq(working_directory)} && ${cmd} )`;
608
- const body = cmd.trim() ? cmd : ':';
609
- script = `{ ${body}\n} </dev/null\nprintf '\\n%s%d\\n' '${this.sentinel}EXIT:' "$?"\n`;
610
- }
611
- try {
612
- this.child.stdin.write(script);
613
- } catch {
614
- this.alive = false;
615
- oneShotShell({ command, timeout, working_directory, shellOptions: this.shellOptions })
616
- .then((r) => finish(this._noteDegraded(r, 'the session stdin rejected the command')));
617
- return;
618
- }
619
- // Data may already be buffered (fast commands) — scan once now.
620
- if (this._pending) this._pending();
621
- });
622
- }
623
-
624
- /**
625
- * Mark a result that came from the one-shot fallback rather than the live
626
- * session, so the model is TOLD the shell changed and can stop trusting its
627
- * quoting contract.
628
- *
629
- * The 2026-02-14 report's worst finding was precisely that this happened
630
- * silently: "nothing in the result tells the agent the shell changed" while
631
- * the agent "degrades to a one-shot cmd.exe — and stays there for the
632
- * remainder of the turn", so it kept emitting PowerShell quoting, got
633
- * localized cmd.exe syntax errors, and finally blamed the user's machine.
634
- * Emitted ONCE per session so a degraded turn stays readable.
635
- */
636
- _noteDegraded(result, reason) {
637
- const r = result && typeof result === 'object' ? result : { content: String(result == null ? '' : result), isError: true };
638
- if (this._degradedNoted) return r;
639
- this._degradedNoted = true;
640
- const via = this.shell && this.shell.cmd ? this.shell.cmd : 'unknown shell';
641
- const was = this.shell && this.shell.kind ? this.shell.kind : 'none';
642
- const marker = `[shell_degraded] ran one-shot via ${via} (${reason}); session dialect was ${was} — quoting contracts may differ`;
643
- return { ...r, content: `${marker}\n${r.content || ''}` };
644
- }
645
-
646
- dispose() {
647
- this.disposed = true;
648
- this.alive = false;
649
- if (this.child) {
650
- try { this.child.stdin.end(); } catch { /* already gone */ }
651
- try { this.child.kill('SIGKILL'); } catch { /* already gone */ }
652
- this.child = null;
653
- }
654
- }
655
- }
656
-
657
- // Minimal single-quote shell-escape for a path.
658
- function shq(s) {
659
- return `'${String(s).replace(/'/g, `'\\''`)}'`;
660
- }
661
-
662
- // Minimal single-quote escape for a PowerShell -LiteralPath (double up ').
663
- function pshq(s) {
664
- return `'${String(s).replace(/'/g, "''")}'`;
665
- }
666
-
667
- module.exports = {
668
- ShellSession,
669
- oneShotShell,
670
- IS_WIN32,
671
- // Phase 30.3: the dialect probe and its verdict, exported so a test can
672
- // assert on the resolution without spawning a session.
673
- resolveSessionShell,
674
- oneShotShellSpec,
675
- isPosixShellName,
676
- POSIX_SHELL_NAMES,
677
- POSIX_PROBE,
678
- appendBounded,
679
- OUTPUT_CAP,
680
- BUFFER_CEILING,
681
- TAIL_KEEP,
682
- };
3
+ "use strict";const{spawn:spawn,spawnSync:spawnSync}=require("node:child_process"),crypto=require("node:crypto");let platform;try{platform=require("../../../client/platform.js")}catch(e){try{platform=require("../../vendor/platform.js")}catch{throw new Error(`shell.js: cannot load the shared platform module (client/platform.js, or its staged copy): ${e&&e.message?e.message:e}`)}}const IS_WIN32=platform.IS_WIN32,OUTPUT_CAP=3e4,MAX_TIMEOUT=6e5,DEFAULT_TIMEOUT=12e4,POSIX_SHELL_NAMES=["bash","sh","dash","ksh","zsh"],POSIX_FALLBACK_NAMES=["bash","sh"],POSIX_PROBE="if [ 1 -eq 1 ]; then exit 0; fi",SHELL_PROBE_TIMEOUT=4e3;function shellName(e){return String(e||"").trim().split(/[\\/]/).pop().toLowerCase().replace(/\.exe$/,"")}function isPosixShellName(e){return POSIX_SHELL_NAMES.includes(shellName(e))}function probePosixShell(e,{env:s,timeout:t=SHELL_PROBE_TIMEOUT}={}){try{const n=spawnSync(e,["-c",POSIX_PROBE],{env:s,encoding:"utf8",timeout:t,windowsHide:!0,stdio:"ignore"});return!n.error&&0===n.status}catch{return!1}}function shellSeams(e){const s=e&&"object"==typeof e?e:{};return{platform:"string"==typeof s.platform&&s.platform?s.platform:process.platform,env:s.env&&"object"==typeof s.env?s.env:process.env,exists:"function"==typeof s.exists?s.exists:void 0,probe:"function"==typeof s.probe?s.probe:void 0,injected:"function"==typeof s.probe||"function"==typeof s.exists}}function considerPosixCandidate(e,s,t,n){const i=String(s.cmd||"").trim();if(!i)return t.push({candidate:s.source||"(unset)",ok:!1,reason:"unset"}),null;if(!isPosixShellName(i))return t.push({candidate:i,ok:!1,reason:`${s.source||"candidate"} is not a POSIX shell — ${shellName(i)||i} is a different language (fish/csh/nu), so it was never executed and never sent POSIX syntax`}),null;const o=platform.resolveExecutable(i,{platform:e.platform,env:e.env,exists:e.exists});if(!o)return t.push({candidate:i,ok:!1,reason:/[\\/]/.test(i)?"not present":"not found on PATH"}),null;const r=e.probe?Boolean(e.probe(o,["-c",POSIX_PROBE])):probePosixShell(o,{env:e.env});return n.push(o),r?(t.push({candidate:o,ok:!0,reason:"verified: one `-c` POSIX script exited 0"}),{ok:!0,cmd:o,arg:"-c",kind:"posix-sh",source:s.source,verified:!0}):(t.push({candidate:o,ok:!1,reason:`failed the POSIX probe (\`-c\` with \`${POSIX_PROBE}\` did not exit 0) — refused`}),null)}function resolvePosixSessionShell(e){const s=[],t=[],n=String(e.env&&e.env.SHELL||"").trim(),i=[];n?i.push({cmd:n,source:"$SHELL"}):s.push({candidate:"$SHELL",ok:!1,reason:"$SHELL is unset"});for(const e of POSIX_FALLBACK_NAMES)i.push({cmd:e,source:`PATH:${e}`});let o=null;for(const n of i)if(o=considerPosixCandidate(e,n,s,t),o)break;const r=n&&!isPosixShellName(n)?n:null;return o?{...o,unsupported:r,caveat:r?`$SHELL is ${r}, which is not a POSIX shell — the session runs the POSIX dialect through ${o.cmd} instead, and ${r} is never sent brace groups, \`exec 2>&1\` or \`$?\``:null,error:null,executed:t,tried:s}:{ok:!1,cmd:null,arg:null,kind:null,source:"refused",verified:!1,unsupported:r,caveat:null,error:r?`exec: refusing to run — $SHELL is "${r}", which is not a POSIX shell (fish/csh/nushell are a different language), and no sh/bash could be verified on PATH to run the POSIX dialect instead. Set SHELL to bash or sh, or put one on PATH.`:`exec: no POSIX shell available — $SHELL is ${n?`"${n}"`:"unset"} and no sh/bash passed the POSIX probe; set SHELL to bash or sh, or put one on PATH.`,executed:t,tried:s}}const SHELL_CACHE=new Map;function resolveSessionShell(e={}){const s=shellSeams(e);if("win32"===s.platform){const e=platform.shellSpec({platform:"win32",env:s.env}),t=platform.resolveExecutable("powershell",{platform:"win32",env:s.env,exists:s.exists})||"powershell.exe";return{ok:!0,cmd:t,arg:"-",kind:"powershell",source:"powershell.exe",verified:!0,unsupported:null,caveat:null,error:null,oneShot:{cmd:t,arg:"-Command",kind:"powershell",source:"powershell.exe"},executed:[],tried:[{candidate:t,ok:!0,reason:"the Windows default (PowerShell session AND one-shot)"},{candidate:e.cmd,ok:!1,reason:"cmd.exe is never used for one-shot: it cannot parse the PowerShell quoting the session teaches"}]}}const t=[s.platform,s.env&&s.env.SHELL,s.env&&s.env.PATH].join("\0");if(!s.injected&&SHELL_CACHE.has(t))return SHELL_CACHE.get(t);const n=resolvePosixSessionShell(s);return s.injected||SHELL_CACHE.set(t,n),n}function cap(e){return e.length>OUTPUT_CAP?`${e.slice(0,OUTPUT_CAP)}\n… (truncated)`:e}const TAIL_KEEP=65536,BUFFER_CEILING=OUTPUT_CAP+TAIL_KEEP,ELISION="\n… (earlier output elided — command produced more than the buffer ceiling)\n";function appendBounded(e,s){const t=e+s;return t.length<=BUFFER_CEILING?t:t.slice(0,OUTPUT_CAP)+ELISION+t.slice(-TAIL_KEEP)}function oneShotShellSpec(e={}){const s=resolveSessionShell(e);if(!s.ok)return{ok:!1,cmd:null,arg:null,kind:null,error:s.error,spec:s};if("powershell"===s.kind){const t=s.oneShot||platform.shellSpec({platform:"win32",env:shellSeams(e).env});return{ok:!0,cmd:t.cmd,arg:t.arg,kind:t.kind||"powershell",error:null,spec:s}}return{ok:!0,cmd:s.cmd,arg:s.arg,kind:"posix-sh",error:null,spec:s}}function oneShotShell({command:e,timeout:s=12e4,working_directory:t,shellOptions:n}={}){return new Promise((i=>{const o=shellSeams(n),r=resolveSessionShell(n);if(!r||!r.ok)return void i({content:r&&r.error||"exec: no usable shell",isError:!0});const l="powershell"===r.kind?r.oneShot:r,h=spawn(l.cmd,[l.arg,String(e||"")],{cwd:t||process.cwd(),timeout:Math.min(Number(s)||12e4,6e5),env:o.env});let c="",d="";h.stdout.on("data",(e=>{c=appendBounded(c,e.toString())})),h.stderr.on("data",(e=>{d=appendBounded(d,e.toString())})),h.on("error",(e=>i({content:`error: ${e.message}`,isError:!0}))),h.on("close",(e=>{const s=`${c}${d?`\n${d}`:""}`.trim()||`(exit ${e})`;i({content:cap(s),isError:0!==e})}))}))}class ShellSession{constructor({cwd:e,shellOptions:s}={}){this.sentinel=`__AEGIS_SH_${crypto.randomBytes(8).toString("hex")}__`,this.marker=`${this.sentinel}EXIT:`,this.buf="",this.alive=!1,this._pending=null,this.cwd=e||process.cwd(),this.disposed=!1,this._respawns=0,this._degradedNoted=!1,this.shellOptions=s||{},this.shell=resolveSessionShell(this.shellOptions),this.shellError=this.shell.ok?null:this.shell.error,this._start(e)}_start(e){if(!this.shell||!this.shell.ok)return void(this.alive=!1);const s=shellSeams(this.shellOptions).env;try{this.child="powershell"===this.shell.kind?spawn(this.shell.cmd,["-NoLogo","-NoProfile","-NonInteractive","-Command","-"],{cwd:e||process.cwd(),env:s,stdio:["pipe","pipe","pipe"]}):spawn(this.shell.cmd,[],{cwd:e||process.cwd(),env:s,stdio:["pipe","pipe","pipe"]})}catch{return void(this.alive=!1)}this.alive=!0;const t=e=>{this.buf=appendBounded(this.buf,e.toString()),this._pending&&this._pending()};if(this.child.stdout.on("data",t),this.child.stderr.on("data",t),this.child.on("exit",(()=>{this.alive=!1,this._pending&&this._pending(!0)})),this.child.on("error",(()=>{this.alive=!1,this._pending&&this._pending(!0)})),"posix-sh"===this.shell.kind)try{this.child.stdin.write("exec 2>&1\n")}catch{this.alive=!1}}run(e,{timeout:s=12e4,working_directory:t}={}){const n=this.shell&&"posix-sh"===this.shell.kind&&this.shell.verified;if(!this.alive||!this.child||this.shell&&"posix-sh"===this.shell.kind&&!n)return!this.alive&&this.shellError?Promise.resolve({content:this.shellError,isError:!0}):!this.alive&&!this.disposed&&this._respawns<1&&(this._respawns+=1,this.buf="",this.shell=resolveSessionShell(this.shellOptions),this.shellError=this.shell.ok?null:this.shell.error,this._start(this.cwd),this.alive)?this.run(e,{timeout:s,working_directory:t}):oneShotShell({command:e,timeout:s,working_directory:t,shellOptions:this.shellOptions}).then((e=>this._noteDegraded(e,this.alive?"no usable session dialect":"the persistent session is not running")));let i=String(e||"");return new Promise((o=>{let r=!1;const l=e=>{r||(r=!0,clearTimeout(h),this._pending=null,o(e))},h=setTimeout((()=>{const e=cap(this.buf.trim());this.dispose(),l({content:`${e}${e?"\n":""}(timed out after ${s}ms)`,isError:!0})}),Math.min(Number(s)||12e4,6e5));let c;if(this._pending=e=>{if(e&&!this.buf.includes(this.marker))return void l({content:cap(this.buf.trim())||"(shell session ended)",isError:!0});const s=this.buf.indexOf(this.marker);if(-1===s)return;const t=this.buf.slice(s+this.marker.length),n=t.indexOf("\n");if(-1===n)return;const i=parseInt(t.slice(0,n),10),o=this.buf.slice(0,s).replace(/\n+$/,"");this.buf=t.slice(n+1),l({content:cap(o||`(exit ${i})`),isError:0!==i})},n)t&&(i=`( cd ${shq(t)} && ${i} )`),c=`{ ${i.trim()?i:":"}\n} </dev/null\nprintf '\\n%s%d\\n' '${this.sentinel}EXIT:' "$?"\n`;else{const e=i.trim()?i:"Write-Output $null",s="$__c = if ($LASTEXITCODE -ne $null) { $LASTEXITCODE } elseif ($?) { 0 } else { 1 }";c=t?`Push-Location -LiteralPath ${pshq(t)}\ntry {\n${e}\n${s}\n} finally {\nPop-Location\n}\n`:`${e}\n${s}\n`,c+=`Write-Output ("${this.marker}" + $__c)\n`}try{this.child.stdin.write(c)}catch{return this.alive=!1,void oneShotShell({command:e,timeout:s,working_directory:t,shellOptions:this.shellOptions}).then((e=>l(this._noteDegraded(e,"the session stdin rejected the command"))))}this._pending&&this._pending()}))}_noteDegraded(e,s){const t=e&&"object"==typeof e?e:{content:String(null==e?"":e),isError:!0};if(this._degradedNoted)return t;this._degradedNoted=!0;const n=`[shell_degraded] ran one-shot via ${this.shell&&this.shell.cmd?this.shell.cmd:"unknown shell"} (${s}); session dialect was ${this.shell&&this.shell.kind?this.shell.kind:"none"} — quoting contracts may differ`;return{...t,content:`${n}\n${t.content||""}`}}dispose(){if(this.disposed=!0,this.alive=!1,this.child){try{this.child.stdin.end()}catch{}try{this.child.kill("SIGKILL")}catch{}this.child=null}}}function shq(e){return`'${String(e).replace(/'/g,"'\\''")}'`}function pshq(e){return`'${String(e).replace(/'/g,"''")}'`}module.exports={ShellSession:ShellSession,oneShotShell:oneShotShell,IS_WIN32:IS_WIN32,resolveSessionShell:resolveSessionShell,oneShotShellSpec:oneShotShellSpec,isPosixShellName:isPosixShellName,POSIX_SHELL_NAMES:POSIX_SHELL_NAMES,POSIX_PROBE:POSIX_PROBE,appendBounded:appendBounded,OUTPUT_CAP:OUTPUT_CAP,BUFFER_CEILING:BUFFER_CEILING,TAIL_KEEP:TAIL_KEEP};