@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 +123 -149
- package/dist/bridges/codex-app-server-auth-gateway.d.mts +8 -0
- package/dist/bridges/codex-app-server-auth-gateway.mjs +183 -0
- package/dist/bridges/codex-app-server-auth-gateway.mjs.map +1 -0
- package/dist/bridges/codex-bridge-runner.d.mts +10 -1
- package/dist/bridges/codex-bridge-runner.mjs +233 -121
- package/dist/bridges/codex-bridge-runner.mjs.map +1 -1
- package/dist/cli.mjs +2728 -991
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +139 -5
- package/dist/index.mjs +528 -69
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-server.d.mts +2 -0
- package/dist/mcp-server.mjs +22174 -0
- package/dist/mcp-server.mjs.map +1 -0
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -1,220 +1,194 @@
|
|
|
1
|
-
# tap
|
|
1
|
+
# @hua-labs/tap
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
>
|
|
5
|
-
> Sessions end. Systems grow.
|
|
3
|
+
Zero-dependency CLI for cross-model AI agent communication setup.
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
One command to connect Claude, Codex, and Gemini agents through a shared file-based communication layer.
|
|
8
6
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
+
## Commands
|
|
64
27
|
|
|
65
|
-
|
|
28
|
+
### `init`
|
|
66
29
|
|
|
67
|
-
|
|
30
|
+
Initialize the comms directory and `.tap-comms/` state.
|
|
68
31
|
|
|
69
|
-
|
|
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
|
-
|
|
75
|
-
#
|
|
76
|
-
|
|
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
|
-
|
|
42
|
+
### `add <runtime>`
|
|
86
43
|
|
|
87
|
-
|
|
44
|
+
Add a runtime. Probes config, plans patches, applies, and verifies.
|
|
88
45
|
|
|
89
46
|
```bash
|
|
90
|
-
|
|
91
|
-
npx @hua-labs/tap
|
|
92
|
-
npx @hua-labs/tap
|
|
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
|
-
###
|
|
53
|
+
### `remove <runtime>`
|
|
98
54
|
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
+
```bash
|
|
67
|
+
npx @hua-labs/tap status
|
|
68
|
+
```
|
|
125
69
|
|
|
126
|
-
|
|
70
|
+
Output shows three status levels:
|
|
127
71
|
|
|
128
|
-
|
|
72
|
+
- **installed** — config written but not verified
|
|
73
|
+
- **configured** — config written and verified
|
|
74
|
+
- **active** — runtime is running and connected
|
|
129
75
|
|
|
130
|
-
###
|
|
76
|
+
### `serve`
|
|
131
77
|
|
|
132
|
-
|
|
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
|
|
136
|
-
npx @hua-labs/tap
|
|
81
|
+
npx @hua-labs/tap serve
|
|
82
|
+
npx @hua-labs/tap serve --comms-dir /path/to/comms
|
|
137
83
|
```
|
|
138
84
|
|
|
139
|
-
|
|
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
|
-
|
|
95
|
+
## `--json` Flag
|
|
142
96
|
|
|
143
|
-
|
|
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
|
|
100
|
+
npx @hua-labs/tap status --json
|
|
147
101
|
```
|
|
148
102
|
|
|
149
|
-
|
|
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
|
-
|
|
121
|
+
Error codes use `TAP_*` prefix: `TAP_ADD_OK`, `TAP_NO_OP`, `TAP_PATCH_FAILED`, etc.
|
|
152
122
|
|
|
153
|
-
|
|
154
|
-
npx @hua-labs/tap dashboard
|
|
155
|
-
```
|
|
123
|
+
Exit codes: `0` = ok, `1` = error.
|
|
156
124
|
|
|
157
|
-
|
|
125
|
+
## Permissions
|
|
158
126
|
|
|
159
|
-
|
|
127
|
+
`tap init` auto-configures runtime permissions.
|
|
160
128
|
|
|
161
|
-
|
|
129
|
+
### Safe mode (default)
|
|
162
130
|
|
|
163
|
-
|
|
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
|
-
|
|
133
|
+
**Codex**: Sets `workspace-write` sandbox, `full` network access, trusted project paths, and writable roots in `~/.codex/config.toml`.
|
|
167
134
|
|
|
168
|
-
###
|
|
135
|
+
### Full mode
|
|
169
136
|
|
|
170
|
-
|
|
137
|
+
```bash
|
|
138
|
+
npx @hua-labs/tap init --permissions full
|
|
139
|
+
```
|
|
171
140
|
|
|
172
|
-
-
|
|
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
|
-
|
|
143
|
+
**Codex**: Sets `danger-full-access` sandbox. Use on trusted local machines only.
|
|
178
144
|
|
|
179
|
-
|
|
145
|
+
## How It Works
|
|
180
146
|
|
|
181
|
-
|
|
147
|
+
Agents communicate through a shared directory (`comms/`) using markdown files:
|
|
182
148
|
|
|
183
|
-
|
|
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
|
-
|
|
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
|
-
|
|
166
|
+
The adapter contract (`RuntimeAdapter`) is the extension point for adding new runtimes.
|
|
196
167
|
|
|
197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
+
### CLI
|
|
211
179
|
|
|
212
|
-
|
|
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
|
-
|
|
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
|
-
|
|
192
|
+
## License
|
|
219
193
|
|
|
220
|
-
|
|
194
|
+
MIT
|
|
@@ -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 };
|