@hua-labs/tap 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -1,220 +1,194 @@
1
- # tap
1
+ # @hua-labs/tap
2
2
 
3
- > Your AI sessions can already work in parallel. tap gives them a shared protocol.
4
- >
5
- > Sessions end. Systems grow.
3
+ Zero-dependency CLI for cross-model AI agent communication setup.
6
4
 
7
- **A local-first, cross-model orchestration protocol for AI coding agents.**
5
+ One command to connect Claude, Codex, and Gemini agents through a shared file-based communication layer.
8
6
 
9
- Run multiple AI agents (Claude, Codex, Gemini) on the same codebase — in parallel.
10
-
11
- ---
12
-
13
- ## What you can do with tap
14
-
15
- - Run multiple AI agents in parallel on one repo
16
- - Split work into independent tasks with isolated worktrees
17
- - Communicate between Claude, Codex, and Gemini sessions
18
- - Review code across models (Codex reviews Claude's code, and vice versa)
19
- - Keep all communication and history in git
20
- - Continue work across sessions and machines — nothing is lost
21
-
22
- ---
23
-
24
- ## Why tap?
25
-
26
- Because using one AI at a time is slow.
27
-
28
- You already have Claude Code, Codex, Gemini CLI. They're powerful alone. But they can't see each other. tap connects them — through files, not proxies.
29
-
30
- - **No server.** Messages are markdown files in a git repo.
31
- - **No vendor lock-in.** Works with any model that can read/write files.
32
- - **No lost context.** Everything persists in git — messages, reviews, findings, retros.
33
- - **No TOS violations.** Only official interfaces — MCP, App Server WebSocket, fs.watch.
34
-
35
- ---
7
+ ## Quick Start
36
8
 
37
- ## Install
9
+ > `bun` is required to run the managed tap MCP server. When installed from npm, `@hua-labs/tap` now ships its own bundled MCP server entry.
38
10
 
39
11
  ```bash
12
+ # 1. Initialize comms directory and state
40
13
  npx @hua-labs/tap init
41
- ```
42
-
43
- One command. Sets up comms directory, config, and MCP server entry. Works on Windows, macOS, and Linux.
44
-
45
- ---
46
14
 
47
- ## How It Works
15
+ # 2. Add runtimes
16
+ npx @hua-labs/tap add claude
17
+ npx @hua-labs/tap add codex
18
+ npx @hua-labs/tap add gemini
48
19
 
20
+ # 3. Check status
21
+ npx @hua-labs/tap status
49
22
  ```
50
- tap (protocol) --> bridge (delivery) --> agent (execution)
51
- git (memory) <-- human (governance) <-- tower (decision)
52
- ```
53
-
54
- No central server. The file system is the message bus — git is the transport and history.
55
-
56
- Messages are plain markdown files: `inbox/YYYYMMDD-{from}-{to}-{subject}.md`
57
23
 
58
- **Delivery modes:**
59
- - **Claude**: MCP channel push — real-time
60
- - **Codex**: App Server WebSocket bridge — real-time
61
- - **Gemini**: File-watch notification — experimental
24
+ Your agents can now communicate through the shared comms directory.
62
25
 
63
- **Cross-device?** Point both machines at the same git repo. Done.
26
+ ## Commands
64
27
 
65
- ---
28
+ ### `init`
66
29
 
67
- ## Quick Start
30
+ Initialize the comms directory and `.tap-comms/` state.
68
31
 
69
- ### 1. Two agents talking
32
+ By default, the comms directory is created inside the current repo at `./tap-comms`.
70
33
 
71
34
  ```bash
72
- # Terminal 1 — Agent A
73
35
  npx @hua-labs/tap init
74
- claude --dangerously-load-development-channels server:tap-comms
75
- # Inside Claude: tap_set_name("agent-a")
76
- # Inside Claude: tap_reply(to: "agent-b", subject: "hello", content: "can you see this?")
77
-
78
- # Terminal 2 — Agent B
79
- cd same-repo
80
- claude
81
- # Inside Claude: tap_set_name("agent-b")
82
- # Inside Claude: tap_list_unread()
36
+ npx @hua-labs/tap init --comms-dir /path/to/comms
37
+ npx @hua-labs/tap init --permissions safe # default: deny destructive ops
38
+ npx @hua-labs/tap init --permissions full # no restrictions (use with caution)
39
+ npx @hua-labs/tap init --force # re-initialize
83
40
  ```
84
41
 
85
- That's it. Two Claude sessions, talking through files. You should see messages appear in the other session instantly.
42
+ ### `add <runtime>`
86
43
 
87
- ### 2. Parallel work with worktrees
44
+ Add a runtime. Probes config, plans patches, applies, and verifies.
88
45
 
89
46
  ```bash
90
- # Set up isolated workspaces
91
- npx @hua-labs/tap init-worktree --path ../wt-1 --branch feat/auth
92
- npx @hua-labs/tap init-worktree --path ../wt-2 --branch feat/dashboard
93
-
94
- # Each agent works in its own worktree — no git conflicts
47
+ npx @hua-labs/tap add claude
48
+ npx @hua-labs/tap add codex
49
+ npx @hua-labs/tap add gemini
50
+ npx @hua-labs/tap add claude --force # re-install
95
51
  ```
96
52
 
97
- ### 3. Add Codex to the team
53
+ ### `remove <runtime>`
98
54
 
99
- ```bash
100
- # Register and start bridge
101
- npx @hua-labs/tap add codex --name reviewer --port 4501
102
- npx @hua-labs/tap bridge start codex-reviewer --agent-name reviewer
55
+ Remove a runtime and rollback config changes.
103
56
 
104
- # You can now send a message from Claude and see it appear in Codex in real time.
57
+ ```bash
58
+ npx @hua-labs/tap remove claude
59
+ npx @hua-labs/tap remove codex
105
60
  ```
106
61
 
107
- ---
108
-
109
- ## CLI Commands
62
+ ### `status`
110
63
 
111
- | Command | Description |
112
- |---------|-------------|
113
- | `tap init` | Initialize tap in a project |
114
- | `tap init-worktree` | Bootstrap a git worktree with deps, permissions, MCP config |
115
- | `tap add <runtime>` | Register a runtime (codex, gemini) |
116
- | `tap remove <runtime>` | Remove a registered runtime |
117
- | `tap bridge start` | Start real-time bridge for a runtime |
118
- | `tap bridge stop` | Stop a running bridge |
119
- | `tap bridge status` | Show bridge health and heartbeat |
120
- | `tap status` | Show registered runtimes and instances |
121
- | `tap dashboard` | Unified ops view — agents, bridges, PRs |
122
- | `tap serve` | Start MCP server for development |
64
+ Show installed runtimes and their status.
123
65
 
124
- All commands support `--json` for automation.
66
+ ```bash
67
+ npx @hua-labs/tap status
68
+ ```
125
69
 
126
- ---
70
+ Output shows three status levels:
127
71
 
128
- ## Advanced Features
72
+ - **installed** — config written but not verified
73
+ - **configured** — config written and verified
74
+ - **active** — runtime is running and connected
129
75
 
130
- ### Multi-Instance Bridge
76
+ ### `serve`
131
77
 
132
- Run multiple agents on the same runtime:
78
+ Start the tap-comms MCP server (stdio). Convenience command for running the MCP server locally.
133
79
 
134
80
  ```bash
135
- npx @hua-labs/tap add codex --name reviewer --port 4501
136
- npx @hua-labs/tap add codex --name builder --port 4502
81
+ npx @hua-labs/tap serve
82
+ npx @hua-labs/tap serve --comms-dir /path/to/comms
137
83
  ```
138
84
 
139
- Each instance gets its own PID, state directory, and heartbeat.
85
+ Requires `bun`. Uses the bundled MCP server entry from `@hua-labs/tap`, with a repo-local fallback for monorepo checkouts.
86
+
87
+ ## Supported Runtimes
88
+
89
+ | Runtime | Config | Bridge | Mode |
90
+ | ------- | ----------------------- | ---------------------- | ------------------ |
91
+ | Claude | `.mcp.json` | native-push (fs.watch) | No daemon needed |
92
+ | Codex | `~/.codex/config.toml` | WebSocket bridge | Daemon per session |
93
+ | Gemini | `.gemini/settings.json` | polling | No daemon needed |
140
94
 
141
- ### Headless Reviewer
95
+ ## `--json` Flag
142
96
 
143
- Run Codex as a background review daemon no TUI:
97
+ All commands support `--json` for machine-readable output. Returns a single JSON object to stdout with no human log noise.
144
98
 
145
99
  ```bash
146
- npx @hua-labs/tap bridge start codex --headless --role reviewer --agent-name my-reviewer
100
+ npx @hua-labs/tap status --json
147
101
  ```
148
102
 
149
- Auto-polls inbox for review requests. Terminates based on configurable conditions (round cap, quality threshold, repetition detection).
103
+ ```json
104
+ {
105
+ "ok": true,
106
+ "command": "status",
107
+ "code": "TAP_STATUS_OK",
108
+ "message": "2 runtime(s) installed",
109
+ "warnings": [],
110
+ "data": {
111
+ "version": "0.2.0",
112
+ "commsDir": "/path/to/comms",
113
+ "runtimes": {
114
+ "claude": { "status": "active", "bridgeMode": "native-push" },
115
+ "codex": { "status": "configured", "bridgeMode": "app-server" }
116
+ }
117
+ }
118
+ }
119
+ ```
150
120
 
151
- ### Ops Dashboard
121
+ Error codes use `TAP_*` prefix: `TAP_ADD_OK`, `TAP_NO_OP`, `TAP_PATCH_FAILED`, etc.
152
122
 
153
- ```bash
154
- npx @hua-labs/tap dashboard
155
- ```
123
+ Exit codes: `0` = ok, `1` = error.
156
124
 
157
- Agents, bridges, and PR status in one screen. `--watch` for live updates.
125
+ ## Permissions
158
126
 
159
- ### Config System
127
+ `tap init` auto-configures runtime permissions.
160
128
 
161
- Two-layer config for portability:
129
+ ### Safe mode (default)
162
130
 
163
- - `tap-config.json` shared, git-tracked
164
- - `tap-config.local.json` — machine-specific, gitignored
131
+ **Claude**: Adds deny rules to `.claude/settings.local.json` blocking destructive operations (force push, hard reset, rm -rf, etc.).
165
132
 
166
- Automatic runtime resolution: `.node-version` + fnm probing + tsx fallback.
133
+ **Codex**: Sets `workspace-write` sandbox, `full` network access, trusted project paths, and writable roots in `~/.codex/config.toml`.
167
134
 
168
- ### Generational Knowledge Transfer
135
+ ### Full mode
169
136
 
170
- Agents are stateless — sessions end, context is lost. tap solves this with files:
137
+ ```bash
138
+ npx @hua-labs/tap init --permissions full
139
+ ```
171
140
 
172
- - **Retros** what worked, what didn't
173
- - **Handoffs** — context for the next session
174
- - **Findings** — discoveries that become backlog items
175
- - **Letters** — agent-to-agent or agent-to-human messages
141
+ **Claude**: Removes tap-managed deny rules. User-added rules preserved.
176
142
 
177
- Used across multiple sessions ("generations") where each session builds on previous findings and handoffs.
143
+ **Codex**: Sets `danger-full-access` sandbox. Use on trusted local machines only.
178
144
 
179
- ---
145
+ ## How It Works
180
146
 
181
- ## Results
147
+ Agents communicate through a shared directory (`comms/`) using markdown files:
182
148
 
183
- Used in production multi-session workflows:
149
+ ```
150
+ comms/
151
+ ├── inbox/ # Agent-to-agent messages
152
+ ├── reviews/ # Code review results
153
+ ├── findings/ # Out-of-scope discoveries
154
+ ├── handoff/ # Session handoff documents
155
+ ├── retros/ # Retrospectives
156
+ └── archive/ # Archived messages
157
+ ```
184
158
 
185
- | Metric | Value |
186
- |--------|-------|
187
- | Generations | 11 |
188
- | Agents | 50+ |
189
- | Models | Claude + Codex + Gemini |
190
- | PRs merged | 55+ |
191
- | Platforms | Windows + macOS + Linux |
159
+ Each runtime has an adapter that:
192
160
 
193
- ---
161
+ 1. **Probes** — finds config files, checks runtime installation
162
+ 2. **Plans** — determines what patches to apply
163
+ 3. **Applies** — backs up and patches config files
164
+ 4. **Verifies** — confirms the runtime can read the config
194
165
 
195
- ## Docs
166
+ The adapter contract (`RuntimeAdapter`) is the extension point for adding new runtimes.
196
167
 
197
- | Document | Description |
198
- |----------|-------------|
199
- | [ARCHITECTURE.md](ARCHITECTURE.md) | Design decisions, data model, delivery modes |
200
- | [CHANGELOG.md](CHANGELOG.md) | Version history |
201
- | [Protocol Rules](docs/) | Rules for agent coordination |
202
- | [Cross-Platform Strategy](docs/cross-platform-strategy.md) | Windows, macOS, Linux support |
203
- | [Codex Bridge Deep Dive](docs/codex-app-server-bridge.md) | WebSocket injection details |
204
- | [Multi-Device Hub](docs/MULTI-DEVICE-HUB.md) | Cross-device via git sync |
168
+ ## Changelog (0.2.0)
205
169
 
206
- ---
170
+ ### Bridge
207
171
 
208
- ## Contributing
172
+ - **Auth gateway** — Managed bridge now includes an auth proxy with timing-safe token validation (M99)
173
+ - **`--no-auth` flag** — Skip auth gateway for localhost-only setups; app-server listens directly on public port (M102)
174
+ - **TUI connect URL** — `bridge start` and `bridge status` output shows where to connect Codex TUI (M102)
175
+ - **Identity routing** — Bridge matches inbox messages by both `agentId` and `agentName`; self echo-back filtered by both (M101)
176
+ - **Display labels** — Bridge prompts, `tap_who`, and notifications use `name [id]` format (M101)
209
177
 
210
- Built by [HUA Labs](https://github.com/HUA-Labs). Issues and PRs welcome.
178
+ ### CLI
211
179
 
212
- The best way to contribute? Use tap, run a generation, and submit your retro as a PR.
180
+ - **`tap doctor`** Diagnose comms, bridge, message, and MCP issues (M95)
181
+ - **`tap doctor --fix`** — Auto-fix common issues with post-fix revalidation (M100)
182
+ - **Error codes** — 24 CLI error codes with consistent `TAP_*` prefix (M91)
183
+ - **Boot streamline** — Faster CLI startup with agent-name persistence (M92)
213
184
 
214
- ---
185
+ ### Infrastructure
215
186
 
216
- *Built by Claude (Anthropic) + Codex (OpenAI) + Gemini (Google) + Devin (human).*
187
+ - **Auto-poll fallback** Bridge falls back to polling when fs.watch is unavailable (M93)
188
+ - **Watcher dedup** — Root-cause fix for duplicate message dispatch (M90)
189
+ - **tap-plugin test infra** — In-memory test harness for MCP channel tests (M94)
190
+ - **Blind test CI** — Cross-model communication verification framework (M98)
217
191
 
218
- *"Sessions end. Systems grow."*
192
+ ## License
219
193
 
220
- **MIT License** — HUA Labs
194
+ MIT
@@ -0,0 +1,8 @@
1
+ interface GatewayOptions {
2
+ listenUrl: string;
3
+ upstreamUrl: string;
4
+ token: string;
5
+ }
6
+ declare function buildGatewayOptions(argv: string[]): GatewayOptions;
7
+
8
+ export { buildGatewayOptions };
@@ -0,0 +1,183 @@
1
+ // src/bridges/codex-app-server-auth-gateway.ts
2
+ import { readFileSync } from "fs";
3
+ import { resolve } from "path";
4
+ import { pathToFileURL } from "url";
5
+ import { timingSafeEqual } from "crypto";
6
+ import { WebSocket, WebSocketServer } from "ws";
7
+ var AUTH_QUERY_PARAM = "tap_token";
8
+ var CLOSE_UNAUTHORIZED = 4401;
9
+ var CLOSE_UPSTREAM_ERROR = 1013;
10
+ function normalizeUrl(value) {
11
+ return value.replace(/\/$/, "");
12
+ }
13
+ function closeSocket(socket, code, reason) {
14
+ if (socket.readyState === WebSocket.CLOSING || socket.readyState === WebSocket.CLOSED) {
15
+ return;
16
+ }
17
+ try {
18
+ socket.close(code, reason);
19
+ } catch {
20
+ }
21
+ }
22
+ function readFlagValue(argv, index, flag) {
23
+ const current = argv[index] ?? "";
24
+ const eqIndex = current.indexOf("=");
25
+ if (eqIndex >= 0) {
26
+ return current.slice(eqIndex + 1);
27
+ }
28
+ const next = argv[index + 1];
29
+ if (!next || next.startsWith("--")) {
30
+ throw new Error(`Missing value for ${flag}`);
31
+ }
32
+ return next;
33
+ }
34
+ function buildGatewayOptions(argv) {
35
+ let listenUrl = process.env.TAP_GATEWAY_LISTEN_URL?.trim() || "";
36
+ let upstreamUrl = process.env.TAP_GATEWAY_UPSTREAM_URL?.trim() || "";
37
+ let tokenFile = process.env.TAP_GATEWAY_TOKEN_FILE?.trim() || "";
38
+ let token = process.env.TAP_GATEWAY_TOKEN?.trim() || "";
39
+ for (let index = 0; index < argv.length; index += 1) {
40
+ const flag = argv[index] ?? "";
41
+ const consumesNext = !flag.includes("=");
42
+ if (flag.startsWith("--listen-url")) {
43
+ listenUrl = readFlagValue(argv, index, "--listen-url").trim();
44
+ if (consumesNext) index += 1;
45
+ continue;
46
+ }
47
+ if (flag.startsWith("--upstream-url")) {
48
+ upstreamUrl = readFlagValue(argv, index, "--upstream-url").trim();
49
+ if (consumesNext) index += 1;
50
+ continue;
51
+ }
52
+ if (flag.startsWith("--token")) {
53
+ token = readFlagValue(argv, index, "--token").trim();
54
+ if (consumesNext) index += 1;
55
+ continue;
56
+ }
57
+ if (flag.startsWith("--token-file")) {
58
+ tokenFile = readFlagValue(argv, index, "--token-file").trim();
59
+ if (consumesNext) index += 1;
60
+ continue;
61
+ }
62
+ }
63
+ if (tokenFile) {
64
+ token = readFileSync(tokenFile, "utf8").trim();
65
+ }
66
+ if (!listenUrl) {
67
+ throw new Error("Missing gateway listen URL");
68
+ }
69
+ if (!upstreamUrl) {
70
+ throw new Error("Missing gateway upstream URL");
71
+ }
72
+ if (!token) {
73
+ throw new Error("Missing gateway auth token");
74
+ }
75
+ const listen = new URL(listenUrl);
76
+ const upstream = new URL(upstreamUrl);
77
+ if (!/^wss?:$/.test(listen.protocol)) {
78
+ throw new Error(`Unsupported gateway listen protocol: ${listen.protocol}`);
79
+ }
80
+ if (!/^wss?:$/.test(upstream.protocol)) {
81
+ throw new Error(
82
+ `Unsupported gateway upstream protocol: ${upstream.protocol}`
83
+ );
84
+ }
85
+ return {
86
+ listenUrl: normalizeUrl(listen.toString()),
87
+ upstreamUrl: normalizeUrl(upstream.toString()),
88
+ token
89
+ };
90
+ }
91
+ function tokensMatch(presentedToken, expectedToken) {
92
+ if (!presentedToken) {
93
+ return false;
94
+ }
95
+ const presented = Buffer.from(presentedToken, "utf8");
96
+ const expected = Buffer.from(expectedToken, "utf8");
97
+ if (presented.length !== expected.length) {
98
+ return false;
99
+ }
100
+ return timingSafeEqual(presented, expected);
101
+ }
102
+ async function main() {
103
+ const options = buildGatewayOptions(process.argv.slice(2));
104
+ const listen = new URL(options.listenUrl);
105
+ const host = listen.hostname === "localhost" ? "127.0.0.1" : listen.hostname;
106
+ const port = Number.parseInt(listen.port, 10);
107
+ if (!Number.isFinite(port) || port <= 0) {
108
+ throw new Error(`Gateway listen URL must include a valid port: ${options.listenUrl}`);
109
+ }
110
+ const server = new WebSocketServer({
111
+ host,
112
+ port,
113
+ path: listen.pathname === "/" ? void 0 : listen.pathname,
114
+ perMessageDeflate: false
115
+ });
116
+ server.on("connection", (client, request) => {
117
+ const requestUrl = new URL(request.url ?? "/", options.listenUrl);
118
+ const presentedToken = requestUrl.searchParams.get(AUTH_QUERY_PARAM);
119
+ if (!tokensMatch(presentedToken, options.token)) {
120
+ closeSocket(client, CLOSE_UNAUTHORIZED, "Unauthorized");
121
+ return;
122
+ }
123
+ const upstream = new WebSocket(options.upstreamUrl, {
124
+ perMessageDeflate: false
125
+ });
126
+ upstream.on("message", (data, isBinary) => {
127
+ if (client.readyState === WebSocket.OPEN) {
128
+ client.send(data, { binary: isBinary });
129
+ }
130
+ });
131
+ client.on("message", (data, isBinary) => {
132
+ if (upstream.readyState === WebSocket.OPEN) {
133
+ upstream.send(data, { binary: isBinary });
134
+ }
135
+ });
136
+ upstream.on("close", (code, reasonBuffer) => {
137
+ const reason = reasonBuffer.toString() || "Upstream closed";
138
+ closeSocket(client, code || 1e3, reason);
139
+ });
140
+ client.on("close", (code, reasonBuffer) => {
141
+ const reason = reasonBuffer.toString() || "Client closed";
142
+ closeSocket(upstream, code || 1e3, reason);
143
+ });
144
+ upstream.on("error", (error) => {
145
+ console.error(`[auth-gateway] upstream error: ${String(error)}`);
146
+ closeSocket(client, CLOSE_UPSTREAM_ERROR, "Upstream unavailable");
147
+ closeSocket(upstream, CLOSE_UPSTREAM_ERROR, "Upstream unavailable");
148
+ });
149
+ client.on("error", (error) => {
150
+ console.error(`[auth-gateway] client error: ${String(error)}`);
151
+ closeSocket(upstream, 1011, "Client error");
152
+ });
153
+ });
154
+ server.on("listening", () => {
155
+ console.log(
156
+ `[auth-gateway] listening ${options.listenUrl} -> ${options.upstreamUrl}`
157
+ );
158
+ });
159
+ const shutdown = () => {
160
+ server.close(() => {
161
+ process.exit(0);
162
+ });
163
+ };
164
+ process.on("SIGINT", shutdown);
165
+ process.on("SIGTERM", shutdown);
166
+ }
167
+ function isDirectExecution() {
168
+ const entry = process.argv[1];
169
+ if (!entry) return false;
170
+ return import.meta.url === pathToFileURL(resolve(entry)).href;
171
+ }
172
+ if (isDirectExecution()) {
173
+ main().catch((error) => {
174
+ console.error(
175
+ error instanceof Error ? error.stack ?? error.message : String(error)
176
+ );
177
+ process.exit(1);
178
+ });
179
+ }
180
+ export {
181
+ buildGatewayOptions
182
+ };
183
+ //# sourceMappingURL=codex-app-server-auth-gateway.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/bridges/codex-app-server-auth-gateway.ts"],"sourcesContent":["import type { IncomingMessage } from \"node:http\";\nimport { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport { timingSafeEqual } from \"node:crypto\";\nimport { WebSocket, WebSocketServer, type RawData } from \"ws\";\n\nconst AUTH_QUERY_PARAM = \"tap_token\";\nconst CLOSE_UNAUTHORIZED = 4401;\nconst CLOSE_UPSTREAM_ERROR = 1013;\n\ninterface GatewayOptions {\n listenUrl: string;\n upstreamUrl: string;\n token: string;\n}\n\nfunction normalizeUrl(value: string): string {\n return value.replace(/\\/$/, \"\");\n}\n\nfunction closeSocket(\n socket: Pick<WebSocket, \"readyState\" | \"close\">,\n code: number,\n reason: string,\n): void {\n if (\n socket.readyState === WebSocket.CLOSING ||\n socket.readyState === WebSocket.CLOSED\n ) {\n return;\n }\n\n try {\n socket.close(code, reason);\n } catch {\n // Best-effort cleanup only.\n }\n}\n\nfunction readFlagValue(argv: string[], index: number, flag: string): string {\n const current = argv[index] ?? \"\";\n const eqIndex = current.indexOf(\"=\");\n if (eqIndex >= 0) {\n return current.slice(eqIndex + 1);\n }\n\n const next = argv[index + 1];\n if (!next || next.startsWith(\"--\")) {\n throw new Error(`Missing value for ${flag}`);\n }\n return next;\n}\n\nexport function buildGatewayOptions(argv: string[]): GatewayOptions {\n let listenUrl = process.env.TAP_GATEWAY_LISTEN_URL?.trim() || \"\";\n let upstreamUrl = process.env.TAP_GATEWAY_UPSTREAM_URL?.trim() || \"\";\n let tokenFile = process.env.TAP_GATEWAY_TOKEN_FILE?.trim() || \"\";\n let token = process.env.TAP_GATEWAY_TOKEN?.trim() || \"\";\n\n for (let index = 0; index < argv.length; index += 1) {\n const flag = argv[index] ?? \"\";\n const consumesNext = !flag.includes(\"=\");\n\n if (flag.startsWith(\"--listen-url\")) {\n listenUrl = readFlagValue(argv, index, \"--listen-url\").trim();\n if (consumesNext) index += 1;\n continue;\n }\n\n if (flag.startsWith(\"--upstream-url\")) {\n upstreamUrl = readFlagValue(argv, index, \"--upstream-url\").trim();\n if (consumesNext) index += 1;\n continue;\n }\n\n if (flag.startsWith(\"--token\")) {\n token = readFlagValue(argv, index, \"--token\").trim();\n if (consumesNext) index += 1;\n continue;\n }\n\n if (flag.startsWith(\"--token-file\")) {\n tokenFile = readFlagValue(argv, index, \"--token-file\").trim();\n if (consumesNext) index += 1;\n continue;\n }\n }\n\n if (tokenFile) {\n token = readFileSync(tokenFile, \"utf8\").trim();\n }\n\n if (!listenUrl) {\n throw new Error(\"Missing gateway listen URL\");\n }\n if (!upstreamUrl) {\n throw new Error(\"Missing gateway upstream URL\");\n }\n if (!token) {\n throw new Error(\"Missing gateway auth token\");\n }\n\n const listen = new URL(listenUrl);\n const upstream = new URL(upstreamUrl);\n if (!/^wss?:$/.test(listen.protocol)) {\n throw new Error(`Unsupported gateway listen protocol: ${listen.protocol}`);\n }\n if (!/^wss?:$/.test(upstream.protocol)) {\n throw new Error(\n `Unsupported gateway upstream protocol: ${upstream.protocol}`,\n );\n }\n\n return {\n listenUrl: normalizeUrl(listen.toString()),\n upstreamUrl: normalizeUrl(upstream.toString()),\n token,\n };\n}\n\nfunction tokensMatch(presentedToken: string | null, expectedToken: string): boolean {\n if (!presentedToken) {\n return false;\n }\n\n const presented = Buffer.from(presentedToken, \"utf8\");\n const expected = Buffer.from(expectedToken, \"utf8\");\n if (presented.length !== expected.length) {\n return false;\n }\n\n return timingSafeEqual(presented, expected);\n}\n\nasync function main(): Promise<void> {\n const options = buildGatewayOptions(process.argv.slice(2));\n const listen = new URL(options.listenUrl);\n const host = listen.hostname === \"localhost\" ? \"127.0.0.1\" : listen.hostname;\n const port = Number.parseInt(listen.port, 10);\n if (!Number.isFinite(port) || port <= 0) {\n throw new Error(`Gateway listen URL must include a valid port: ${options.listenUrl}`);\n }\n\n const server = new WebSocketServer({\n host,\n port,\n path: listen.pathname === \"/\" ? undefined : listen.pathname,\n perMessageDeflate: false,\n });\n\n server.on(\"connection\", (client: WebSocket, request: IncomingMessage) => {\n const requestUrl = new URL(request.url ?? \"/\", options.listenUrl);\n const presentedToken = requestUrl.searchParams.get(AUTH_QUERY_PARAM);\n if (!tokensMatch(presentedToken, options.token)) {\n closeSocket(client, CLOSE_UNAUTHORIZED, \"Unauthorized\");\n return;\n }\n\n const upstream = new WebSocket(options.upstreamUrl, {\n perMessageDeflate: false,\n });\n\n upstream.on(\"message\", (data: RawData, isBinary: boolean) => {\n if (client.readyState === WebSocket.OPEN) {\n client.send(data, { binary: isBinary });\n }\n });\n\n client.on(\"message\", (data: RawData, isBinary: boolean) => {\n if (upstream.readyState === WebSocket.OPEN) {\n upstream.send(data, { binary: isBinary });\n }\n });\n\n upstream.on(\"close\", (code: number, reasonBuffer: Buffer) => {\n const reason = reasonBuffer.toString() || \"Upstream closed\";\n closeSocket(client, code || 1000, reason);\n });\n\n client.on(\"close\", (code: number, reasonBuffer: Buffer) => {\n const reason = reasonBuffer.toString() || \"Client closed\";\n closeSocket(upstream, code || 1000, reason);\n });\n\n upstream.on(\"error\", (error: Error) => {\n console.error(`[auth-gateway] upstream error: ${String(error)}`);\n closeSocket(client, CLOSE_UPSTREAM_ERROR, \"Upstream unavailable\");\n closeSocket(upstream, CLOSE_UPSTREAM_ERROR, \"Upstream unavailable\");\n });\n\n client.on(\"error\", (error: Error) => {\n console.error(`[auth-gateway] client error: ${String(error)}`);\n closeSocket(upstream, 1011, \"Client error\");\n });\n });\n\n server.on(\"listening\", () => {\n console.log(\n `[auth-gateway] listening ${options.listenUrl} -> ${options.upstreamUrl}`,\n );\n });\n\n const shutdown = () => {\n server.close(() => {\n process.exit(0);\n });\n };\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n\nfunction isDirectExecution(): boolean {\n const entry = process.argv[1];\n if (!entry) return false;\n return import.meta.url === pathToFileURL(resolve(entry)).href;\n}\n\nif (isDirectExecution()) {\n main().catch((error) => {\n console.error(\n error instanceof Error ? (error.stack ?? error.message) : String(error),\n );\n process.exit(1);\n });\n}\n"],"mappings":"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","names":[]}
@@ -1,2 +1,11 @@
1
+ interface BridgeScriptArgsOptions {
2
+ repoRoot: string;
3
+ commsDir: string;
4
+ appServerUrl: string;
5
+ gatewayTokenFile?: string;
6
+ stateDir?: string;
7
+ agentName?: string;
8
+ }
9
+ declare function buildBridgeScriptArgs(scriptPath: string, options: BridgeScriptArgsOptions): string[];
1
10
 
2
- export { }
11
+ export { buildBridgeScriptArgs };