viber-channel 0.8.1 → 0.8.2

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.
@@ -297,6 +297,12 @@ export function runPersistentControlStream(
297
297
  };
298
298
 
299
299
  const done = (async () => {
300
+ // #389 step-02: announce ONCE that the instance control stream actually
301
+ // connected server-side (the SSE `connected` open-marker arrived) — proof
302
+ // the agent is ONLINE/reachable, not merely registered. vibe-master's spawn
303
+ // readiness gate polls the bridge log for this line, so a launch that
304
+ // registers then fails to connect (CF/401/network) is NOT reported as ready.
305
+ let announcedOnline = false;
300
306
  try {
301
307
  while (true) {
302
308
  if (signal?.aborted) {
@@ -305,6 +311,13 @@ export function runPersistentControlStream(
305
311
  }
306
312
  try {
307
313
  for await (const ev of subscribeControlStream(opts)) {
314
+ if (ev.event === "connected") {
315
+ if (!announcedOnline) {
316
+ announcedOnline = true;
317
+ log("[control-stream] online — instance control stream connected\n");
318
+ }
319
+ continue;
320
+ }
308
321
  if (ev.event === "stop") {
309
322
  settleFirstReject(new ControlStreamStopped());
310
323
  handlers.onStop?.("stopped");
@@ -314,7 +327,7 @@ export function runPersistentControlStream(
314
327
  handlers.onBeatNow?.();
315
328
  continue;
316
329
  }
317
- if (ev.event !== "join") continue; // connected / ping / unknown
330
+ if (ev.event !== "join") continue; // ping / unknown (connected handled above)
318
331
  const minted = parseJoinPayload(ev.data, log);
319
332
  if (!minted) continue;
320
333
  handlers.onJoin?.(minted);
package/package.json CHANGED
@@ -1,46 +1,46 @@
1
- {
2
- "name": "viber-channel",
3
- "version": "0.8.1",
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
- "type": "module",
6
- "bin": {
7
- "viber-channel": "./viber-channel.ts",
8
- "viber-codex-bridge": "./viber-codex-bridge.ts",
9
- "viber-gemma-bridge": "./viber-gemma-bridge.ts",
10
- "viber-codex-supervisor": "./viber-codex-supervisor.ts"
11
- },
12
- "files": [
13
- "viber-channel.ts",
14
- "viber-codex-bridge.ts",
15
- "viber-gemma-bridge.ts",
16
- "viber-codex-supervisor.ts",
17
- "lib/",
18
- "README.md"
19
- ],
20
- "repository": {
21
- "type": "git",
22
- "url": "git+https://github.com/dgx80/viber.git",
23
- "directory": "viber-channel"
24
- },
25
- "homepage": "https://viber.dgypx.dev",
26
- "bugs": {
27
- "url": "https://github.com/dgx80/viber/issues"
28
- },
29
- "keywords": [
30
- "viber",
31
- "claude-code",
32
- "mcp",
33
- "channel",
34
- "voice",
35
- "transcription"
36
- ],
37
- "license": "MIT",
38
- "scripts": {
39
- "start": "bun run viber-channel.ts",
40
- "start:codex-bridge": "bun run viber-codex-bridge.ts",
41
- "test": "bun test"
42
- },
43
- "dependencies": {
44
- "@modelcontextprotocol/sdk": "^1.0.0"
45
- }
46
- }
1
+ {
2
+ "name": "viber-channel",
3
+ "version": "0.8.2",
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
+ "type": "module",
6
+ "bin": {
7
+ "viber-channel": "./viber-channel.ts",
8
+ "viber-codex-bridge": "./viber-codex-bridge.ts",
9
+ "viber-gemma-bridge": "./viber-gemma-bridge.ts",
10
+ "viber-codex-supervisor": "./viber-codex-supervisor.ts"
11
+ },
12
+ "files": [
13
+ "viber-channel.ts",
14
+ "viber-codex-bridge.ts",
15
+ "viber-gemma-bridge.ts",
16
+ "viber-codex-supervisor.ts",
17
+ "lib/",
18
+ "README.md"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/dgx80/viber.git",
23
+ "directory": "viber-channel"
24
+ },
25
+ "homepage": "https://viber.dgypx.dev",
26
+ "bugs": {
27
+ "url": "https://github.com/dgx80/viber/issues"
28
+ },
29
+ "keywords": [
30
+ "viber",
31
+ "claude-code",
32
+ "mcp",
33
+ "channel",
34
+ "voice",
35
+ "transcription"
36
+ ],
37
+ "license": "MIT",
38
+ "scripts": {
39
+ "start": "bun run viber-channel.ts",
40
+ "start:codex-bridge": "bun run viber-codex-bridge.ts",
41
+ "test": "bun test"
42
+ },
43
+ "dependencies": {
44
+ "@modelcontextprotocol/sdk": "^1.0.0"
45
+ }
46
+ }
@@ -356,7 +356,7 @@ function instructionsForTier(tier: AgentTier): string {
356
356
  return [
357
357
  "You are a bridge-owned Codex agent connected to Viber, in READ-WRITE mode.",
358
358
  "You MAY read files, create/modify files within the workspace, and run commands (including state-changing ones) to carry out the user's requests.",
359
- "The sandbox is workspace-write: writes are confined to the workspace and network access is blocked at the OS level.",
359
+ "The sandbox is workspace-write: writes are confined to the workspace, and network access is ENABLED — so you can run git (including `git push`) and open pull requests (e.g. with `gh`).",
360
360
  "There is NO human approval step, so be deliberate: make only the changes the user asked for, and avoid destructive commands unless explicitly requested.",
361
361
  VOICE_CONCISE_LINE,
362
362
  CHANNEL_TOOLS_LINE,
@@ -385,17 +385,43 @@ function instructionsForTier(tier: AgentTier): string {
385
385
  // Approval policy per tier. `read`/`write` use "never" so Codex runs its commands
386
386
  // WITHOUT a human approver (there is none on the bridge) — the SANDBOX is the real
387
387
  // guardrail (read-only blocks writes/network; workspace-write confines writes to
388
- // the workspace and still blocks network). `chat` keeps "on-request" (no exec anyway).
389
- function approvalForTier(tier: AgentTier): "never" | "on-request" {
388
+ // the workspace). `chat` keeps "on-request" (no exec anyway).
389
+ export function approvalForTier(tier: AgentTier): "never" | "on-request" {
390
390
  return tier === "chat" ? "on-request" : "never";
391
391
  }
392
392
 
393
393
  // Codex OS sandbox per tier. `write` widens to workspace-write (edits confined to
394
- // the workspace, network still blocked); chat/read stay read-only.
395
- function sandboxForTier(tier: AgentTier): "read-only" | "workspace-write" {
394
+ // the workspace); chat/read stay read-only.
395
+ export function sandboxForTier(tier: AgentTier): "read-only" | "workspace-write" {
396
396
  return tier === "write" ? "workspace-write" : "read-only";
397
397
  }
398
398
 
399
+ // Network access per tier (#389 step-03). ONLY the trusted `write` tier gets
400
+ // network — so a codex coder can `git push` and open its PR (git-over-https +
401
+ // `gh`). read/chat stay offline. SECURITY: codex's workspace-write sandbox opens
402
+ // ALL network, not git-only (the app-server sandbox has no per-domain allowlist),
403
+ // so this is a TRUST decision scoped to the write tier, which is already
404
+ // operator-authorized-at-launch for unattended command execution. If codex ever
405
+ // exposes an outbound-domain allowlist, tighten here (follow-up issue).
406
+ export function networkForTier(tier: AgentTier): boolean {
407
+ return tier === "write";
408
+ }
409
+
410
+ // The EXACT `sandboxPolicy` object sent on every `turn/start` (#389 step-03).
411
+ // Per the codex app-server protocol (generate-ts), `networkAccess` lives ONLY on
412
+ // this per-turn policy object (workspaceWrite/readOnly variants) — NOT on
413
+ // thread/start|resume, which take a bare `sandbox` MODE string. So the turn-level
414
+ // policy is the single lever for network. Exported to unit-test the payload/tier.
415
+ export function buildTurnSandboxPolicy(
416
+ tier: AgentTier,
417
+ ):
418
+ | { type: "workspaceWrite"; networkAccess: boolean }
419
+ | { type: "readOnly"; networkAccess: boolean } {
420
+ return sandboxForTier(tier) === "workspace-write"
421
+ ? { type: "workspaceWrite", networkAccess: networkForTier(tier) }
422
+ : { type: "readOnly", networkAccess: false };
423
+ }
424
+
399
425
  const AGENT_TIER: AgentTier = resolveAgentTier();
400
426
  // Identity line (#309 step-17) is appended AFTER the tier instructions so the
401
427
  // permission rules stay first + strongest; the role is framed as descriptive and
@@ -635,10 +661,7 @@ class CodexAppServer {
635
661
  cwd: process.cwd(),
636
662
  runtimeWorkspaceRoots: [process.cwd()],
637
663
  approvalPolicy: AGENT_APPROVAL_POLICY,
638
- sandboxPolicy:
639
- AGENT_SANDBOX === "workspace-write"
640
- ? { type: "workspaceWrite", networkAccess: false }
641
- : { type: "readOnly", networkAccess: false },
664
+ sandboxPolicy: buildTurnSandboxPolicy(AGENT_TIER),
642
665
  developerInstructions: AGENT_INSTRUCTIONS,
643
666
  responsesapiClientMetadata: { source: "viber-codex-bridge" },
644
667
  });
@@ -1115,7 +1138,7 @@ function setupCodexRuntime(deps: {
1115
1138
 
1116
1139
  function logCodexSafety(): void {
1117
1140
  process.stderr.write(
1118
- `${LOG_PREFIX} codex safety: tier=${AGENT_TIER}, sandbox=${AGENT_SANDBOX}, approvalPolicy=${AGENT_APPROVAL_POLICY}, network=false, ` +
1141
+ `${LOG_PREFIX} codex safety: tier=${AGENT_TIER}, sandbox=${AGENT_SANDBOX}, approvalPolicy=${AGENT_APPROVAL_POLICY}, network=${networkForTier(AGENT_TIER)}, ` +
1119
1142
  `instructions=${
1120
1143
  AGENT_TIER === "write"
1121
1144
  ? "read+write/run commands (workspace)"