viber-channel 0.8.10 → 0.8.11

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,119 @@
1
+ /**
2
+ * codex_shell_path — #500: let codex spawn processes on Windows again.
3
+ *
4
+ * codex's Windows sandbox launches every command under a RESTRICTED TOKEN
5
+ * (`CreateProcessAsUserW`). Windows refuses to start an MSIX-PACKAGED binary
6
+ * that way, and the shell codex resolves on JP's machine is PowerShell 7 from
7
+ * the Microsoft Store — packaged under `C:\Program Files\WindowsApps\…`. So
8
+ * every command died with:
9
+ *
10
+ * windows sandbox: runner failed during SpawnChild:
11
+ * CreateProcessAsUserW failed: 5 (Access is denied.)
12
+ *
13
+ * Measured, 20 trials per case (success = absence of `CreateProcessAsUserW`),
14
+ * reproduced independently by a second agent:
15
+ *
16
+ * | target | spawn failures |
17
+ * | pwsh MSIX (package dir OR alias dir) | 20/20 |
18
+ * | powershell 5.1 / cmd.exe / git bash | 0/20 |
19
+ *
20
+ * FIX: drop the `WindowsApps` directories from the PATH of the `codex
21
+ * app-server` CHILD ONLY. codex then resolves `powershell.exe` 5.1 and spawns
22
+ * fine (measured end to end: `hello_from_shell`, exit 0, 722 ms).
23
+ *
24
+ * REJECTED — `-c windows.sandbox="unelevated"`. It also fixes the spawn (0/20
25
+ * failures on the MSIX target), but it switches the sandbox to the legacy
26
+ * backend, and that has two measured costs:
27
+ * - PowerShell drops to **ConstrainedLanguage** (5/5, both agents): .NET
28
+ * method calls, `Add-Type` and type casts fail in agent scripts;
29
+ * - codex's own binary states that "Restricted read-only access requires the
30
+ * elevated Windows sandbox backend" — our `read`/`chat` tiers rely on the
31
+ * read-only sandbox, so they would be SILENTLY weakened, and any future
32
+ * deny-read path (`.viber/auth.json`, `~/.ssh`) would be a no-op.
33
+ * Writes outside the workspace stay blocked in both modes, so the tradeoff is
34
+ * not about writes — it is about keeping the backend our tiers are built on.
35
+ */
36
+
37
+ /**
38
+ * A PATH entry that resolves to Store/MSIX app-execution aliases.
39
+ *
40
+ * Anchored on a whole path SEGMENT: a substring match would also strip
41
+ * `C:\SDK\WindowsAppsSDK\bin` and nobody would understand why six months later.
42
+ * The two real cases stay covered — the package dir and
43
+ * `%LOCALAPPDATA%\Microsoft\WindowsApps`.
44
+ */
45
+ function isWindowsAppsEntry(entry: string): boolean {
46
+ return /[\\/]windowsapps([\\/]|$)/i.test(entry.replace(/[\\/]+$/, ""));
47
+ }
48
+
49
+ export interface SanitizeResult {
50
+ /** The PATH to hand the child. Unchanged when no surgery was needed. */
51
+ path: string;
52
+ /** Entries removed, in their original order. Empty when nothing changed. */
53
+ stripped: string[];
54
+ /** Set when surgery was NEEDED but deliberately skipped — worth logging. */
55
+ skipped?: string;
56
+ }
57
+
58
+ /**
59
+ * Remove the MSIX alias directories from a PATH — but only on Windows, and only
60
+ * when no unpackaged `pwsh.exe` is REACHABLE, i.e. sits on a surviving PATH
61
+ * entry. An MSI PowerShell 7 that exists on disk but is not on the PATH does
62
+ * NOT count: codex resolves its shell through the PATH, so it would still land
63
+ * on the broken packaged one (raised in review — we do not have a measurement
64
+ * showing codex probes fixed locations, so we must not assume it does).
65
+ *
66
+ * Pure: platform, PATH and filesystem probing are all injected, so this never
67
+ * depends on the machine running the test.
68
+ */
69
+ export function sanitizeCodexPath(
70
+ pathValue: string | undefined,
71
+ opts: { platform: string; exists: (p: string) => boolean; sep?: string },
72
+ ): SanitizeResult {
73
+ const sep = opts.sep ?? ";";
74
+ const original = pathValue ?? "";
75
+ if (opts.platform !== "win32") return { path: original, stripped: [] };
76
+
77
+ const entries = original.split(sep);
78
+ const kept = entries.filter((e) => !isWindowsAppsEntry(e));
79
+ const stripped = entries.filter((e) => isWindowsAppsEntry(e) && e.trim() !== "");
80
+ if (stripped.length === 0) return { path: original, stripped: [] };
81
+
82
+ // An unpackaged pwsh on a surviving entry means codex already has a shell it
83
+ // can spawn — leave the PATH intact so the child keeps its Store aliases
84
+ // (python stub, wt, …).
85
+ const onKeptPath = (exe: string): boolean =>
86
+ kept.some((dir) => dir.trim() !== "" && opts.exists(`${dir.replace(/[\\/]+$/, "")}\\${exe}`));
87
+ if (onKeptPath("pwsh.exe")) return { path: original, stripped: [] };
88
+
89
+ // Floor: if not even cmd.exe survives the surgery, codex would have no shell
90
+ // to fall back on — stripping would cost the Store aliases for nothing.
91
+ // PowerShell 5.1 IS removable as an optional feature on recent Windows 11,
92
+ // so this is not purely theoretical. Do nothing, and say so.
93
+ if (!onKeptPath("cmd.exe") && !onKeptPath("powershell.exe")) {
94
+ return { path: original, stripped: [], skipped: "no unpackaged shell would remain on PATH" };
95
+ }
96
+
97
+ return { path: kept.join(sep), stripped };
98
+ }
99
+
100
+ /**
101
+ * Apply {@link sanitizeCodexPath} to a child environment, in place.
102
+ *
103
+ * Windows env keys are case-INSENSITIVE, a JS object's are not: a parent may
104
+ * carry `Path`, `PATH` or even `PaTh`, and handing the child two spellings of
105
+ * the same variable is asking for the wrong one to win. So we read whichever
106
+ * variant exists, then collapse them all into one canonical `PATH`.
107
+ */
108
+ export function applySanitizedPath(
109
+ env: Record<string, string | undefined>,
110
+ opts: { platform: string; exists: (p: string) => boolean; sep?: string },
111
+ ): SanitizeResult {
112
+ const keys = Object.keys(env).filter((k) => k.toLowerCase() === "path");
113
+ const current = keys.map((k) => env[k]).find((v) => v !== undefined);
114
+ const result = sanitizeCodexPath(current, opts);
115
+ if (result.stripped.length === 0) return result;
116
+ for (const k of keys) delete env[k];
117
+ env.PATH = result.path;
118
+ return result;
119
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viber-channel",
3
- "version": "0.8.10",
3
+ "version": "0.8.11",
4
4
  "description": "Voice + text MCP channel between a Claude Code session and the Viber UI (https://viber.dgypx.dev). Push transcripts to Claude; send_message tool delivers text back to the UI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,6 +12,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
12
  import { dirname, join } from "node:path";
13
13
  import { authFilePath, loadAuth } from "./lib/auth.ts";
14
14
  import { CONTRACT_STYLE_NEUTRAL, CONTRACT_STYLE_TOOLS } from "./lib/channel_instructions.ts";
15
+ import { applySanitizedPath } from "./lib/codex_shell_path.ts";
15
16
  import {
16
17
  ConversationMintError,
17
18
  defaultLabel,
@@ -620,6 +621,32 @@ class CodexAppServer {
620
621
  const codexBin = process.env.VIBER_CODEX_BIN?.trim() || "codex";
621
622
  const args = ["app-server"];
622
623
  const childEnv = { ...process.env };
624
+ // #500: codex's Windows sandbox cannot spawn an MSIX-packaged shell (Store
625
+ // PowerShell 7) under a restricted token — every command died with
626
+ // CreateProcessAsUserW failed: 5. Drop the WindowsApps alias dirs from the
627
+ // CHILD's PATH so codex resolves powershell.exe 5.1 instead. The sandbox
628
+ // keeps its `elevated` backend, which our read-only tiers depend on.
629
+ // applySanitizedPath collapses every case-variant of the key into one
630
+ // canonical PATH — handing the child both `Path` and `PATH` would let the
631
+ // stale one win.
632
+ const sanitized = applySanitizedPath(childEnv, {
633
+ platform: process.platform,
634
+ exists: existsSync,
635
+ });
636
+ if (sanitized.stripped.length > 0) {
637
+ process.stderr.write(
638
+ `[viber-codex-bridge] #500: removed ${sanitized.stripped.length} MSIX alias dir(s) from the ` +
639
+ `codex child's PATH so it can spawn processes: ${sanitized.stripped.join(", ")}. ` +
640
+ "The child therefore loses the Store execution aliases — notably WINGET (and wt, ngrok, " +
641
+ "the WSL launchers); node/npm/bun/git/codex/python are unaffected. Installing PowerShell 7 " +
642
+ "from the MSI makes this surgery unnecessary.\n",
643
+ );
644
+ } else if (sanitized.skipped) {
645
+ process.stderr.write(
646
+ `[viber-codex-bridge] #500: MSIX alias dirs left on the codex child's PATH — ${sanitized.skipped}. ` +
647
+ "Commands may fail with CreateProcessAsUserW failed: 5; run scripts/repro-codex-spawn.ps1.\n",
648
+ );
649
+ }
623
650
  if (mcp) {
624
651
  args.push("-c", `mcp_servers.viber.url="${mcp.url}"`);
625
652
  args.push("-c", `mcp_servers.viber.bearer_token_env_var="${mcp.bearerEnvVar}"`);