gaslamp 1.0.1 → 1.1.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 +10 -0
- package/package.json +1 -1
- package/src/server.mjs +45 -6
- package/src/setup.mjs +6 -1
package/README.md
CHANGED
|
@@ -63,11 +63,21 @@ A Codex→Claude consult with no `sandbox` arg runs with your own Claude config
|
|
|
63
63
|
would. Per-call overrides: `read-only` (advisory, no edits) or `danger-full-access`
|
|
64
64
|
(full read/write). Claude has no filesystem sandbox, so there's no `workspace-write`.
|
|
65
65
|
|
|
66
|
+
## Recursion
|
|
67
|
+
|
|
68
|
+
A consult is **one hop** by default: a Claude reached through gaslamp can't turn
|
|
69
|
+
around and consult Codex, and a Codex reached through gaslamp can't consult Claude
|
|
70
|
+
back. (The Claude→Codex registration tags the consulted Codex with
|
|
71
|
+
`GASLAMP_NESTED=1`, and the spawned Claude is denied the bridge with
|
|
72
|
+
`--disallowedTools mcp__gaslamp`.) Only the top-level agent you drive can open a
|
|
73
|
+
consult. Set `GASLAMP_ALLOW_RECURSION=1` to restore unbounded, symmetric handoffs.
|
|
74
|
+
|
|
66
75
|
## Env knobs
|
|
67
76
|
|
|
68
77
|
| var | default | meaning |
|
|
69
78
|
|-----|---------|---------|
|
|
70
79
|
| `GASLAMP_ALLOWED_TOOLS` | `Read Grep Glob WebFetch WebSearch` | tools the `read-only` override permits |
|
|
80
|
+
| `GASLAMP_ALLOW_RECURSION` | unset | allow a consulted agent to consult back (off = one hop) |
|
|
71
81
|
| `GASLAMP_LOGFILE` | `~/.codex/gaslamp.log` | transcript path; `off` to disable |
|
|
72
82
|
| `GASLAMP_DEBUG` | unset | verbose stderr (raw JSON-RPC) |
|
|
73
83
|
| `CLAUDE_BIN` | autodetected | path to the `claude` binary |
|
package/package.json
CHANGED
package/src/server.mjs
CHANGED
|
@@ -10,8 +10,14 @@
|
|
|
10
10
|
// - Per-call `sandbox` arg; OMITTING it defers to the user's own Claude config
|
|
11
11
|
// (~/.claude settings: permission defaultMode, allow/deny rules, model, MCP,
|
|
12
12
|
// CLAUDE.md), the mirror of Codex deferring to `sandbox_mode` in config.toml.
|
|
13
|
-
// - No bespoke timeout
|
|
14
|
-
//
|
|
13
|
+
// - No bespoke timeout — a consulted Claude loads the user's full config/MCP,
|
|
14
|
+
// exactly as a consulted Codex does.
|
|
15
|
+
// - Recursion guard (default on): a consult is one hop. The Claude we spawn is
|
|
16
|
+
// denied the Claude→Codex bridge (`--disallowedTools mcp__gaslamp`, a deny
|
|
17
|
+
// rule that holds even under --dangerously-skip-permissions), and if THIS
|
|
18
|
+
// server is itself running under a consulted Codex (GASLAMP_NESTED, set on
|
|
19
|
+
// the Claude→Codex registration by `gaslamp setup`) it refuses the call.
|
|
20
|
+
// GASLAMP_ALLOW_RECURSION=1 restores the old unbounded symmetric handoff.
|
|
15
21
|
// - The lone Claude-only step (no Codex analog, so not an asymmetry):
|
|
16
22
|
// ANTHROPIC_API_KEY is stripped from the child env so keychain OAuth is
|
|
17
23
|
// authoritative. A stale env key 401s every call otherwise.
|
|
@@ -57,6 +63,14 @@ const ALLOWED_TOOLS = (process.env.GASLAMP_ALLOWED_TOOLS || "Read Grep Glob WebF
|
|
|
57
63
|
.split(/[\s,]+/).filter(Boolean).join(",");
|
|
58
64
|
const DEBUG = !!process.env.GASLAMP_DEBUG;
|
|
59
65
|
|
|
66
|
+
// Recursion guard: a consult is one hop unless explicitly opted out of.
|
|
67
|
+
// ALLOW_RECURSION — restore the old symmetric, unbounded handoff.
|
|
68
|
+
// NESTED — this server is running under a Codex that is itself a
|
|
69
|
+
// gaslamp consult (tagged GASLAMP_NESTED=1 on the
|
|
70
|
+
// Claude→Codex registration); refuse to hand work back.
|
|
71
|
+
const ALLOW_RECURSION = !!process.env.GASLAMP_ALLOW_RECURSION;
|
|
72
|
+
const NESTED = !!process.env.GASLAMP_NESTED && !ALLOW_RECURSION;
|
|
73
|
+
|
|
60
74
|
// Persistent consultation transcript (the lightweight "watch them talk" log).
|
|
61
75
|
// Defaults to ~/.codex/gaslamp.log; set GASLAMP_LOGFILE=off to disable.
|
|
62
76
|
const LOGFILE = process.env.GASLAMP_LOGFILE === "off" ? null
|
|
@@ -78,6 +92,20 @@ function send(msg) { process.stdout.write(JSON.stringify(msg) + "\n"); }
|
|
|
78
92
|
function reply(id, result) { send({ jsonrpc: "2.0", id, result }); }
|
|
79
93
|
function fail(id, code, message) { send({ jsonrpc: "2.0", id, error: { code, message } }); }
|
|
80
94
|
|
|
95
|
+
// Refuse a consult that would be a second hop: this server runs under a Codex
|
|
96
|
+
// that is itself a gaslamp consult. Returned as a readable tool result (not a
|
|
97
|
+
// protocol error) so the consulting Codex sees why, mirroring the claude-error
|
|
98
|
+
// shape in runClaude.
|
|
99
|
+
function refuseNested(id, name) {
|
|
100
|
+
const text =
|
|
101
|
+
"Recursive gaslamp consultation is disabled. This Codex is itself a gaslamp " +
|
|
102
|
+
"consult (GASLAMP_NESTED is set), so it can't hand work back to Claude — a " +
|
|
103
|
+
"consult is one hop. Set GASLAMP_ALLOW_RECURSION=1 to allow nested handoffs.";
|
|
104
|
+
log("refuse nested", name);
|
|
105
|
+
transcript(`← refused ${name} (nested; recursion disabled)`);
|
|
106
|
+
reply(id, { content: [{ type: "text", text }], structuredContent: { sessionId: null, content: text }, isError: true });
|
|
107
|
+
}
|
|
108
|
+
|
|
81
109
|
// Mirrors codex's `{ threadId, content }` outputSchema. sessionId may be null on
|
|
82
110
|
// a hard failure, so only `content` is required.
|
|
83
111
|
const OUTPUT_SCHEMA = {
|
|
@@ -99,10 +127,12 @@ const TOOLS = [
|
|
|
99
127
|
{
|
|
100
128
|
name: "gaslamp",
|
|
101
129
|
title: "Claude",
|
|
130
|
+
// Mirrors codex mcp-server's `codex` description ("Run a Codex session.
|
|
131
|
+
// Accepts configuration parameters matching the Codex Config struct.") so the
|
|
132
|
+
// two directions read as twins. "config" (not "Config struct") because Claude
|
|
133
|
+
// Code has no such struct and gaslamp takes no `config` param — honest mirror.
|
|
102
134
|
description:
|
|
103
|
-
"
|
|
104
|
-
"`cwd` and returns a summary of what it found or changed. Returns a sessionId; pass it to " +
|
|
105
|
-
"gaslamp-reply to continue the same thread with full context.",
|
|
135
|
+
"Run a Claude session. Accepts configuration parameters matching the Claude Code config.",
|
|
106
136
|
inputSchema: {
|
|
107
137
|
type: "object",
|
|
108
138
|
additionalProperties: false,
|
|
@@ -119,7 +149,9 @@ const TOOLS = [
|
|
|
119
149
|
{
|
|
120
150
|
name: "gaslamp-reply",
|
|
121
151
|
title: "Claude Reply",
|
|
122
|
-
|
|
152
|
+
// Mirrors codex's `codex-reply`: "Continue a Codex conversation by providing
|
|
153
|
+
// the thread id and prompt." (thread id → session id).
|
|
154
|
+
description: "Continue a Claude conversation by providing the session id and prompt.",
|
|
123
155
|
inputSchema: {
|
|
124
156
|
type: "object",
|
|
125
157
|
additionalProperties: false,
|
|
@@ -150,6 +182,12 @@ function runClaude({ prompt, cwd, model, sandbox, resume }, id) {
|
|
|
150
182
|
}
|
|
151
183
|
// else (omitted): pass no permission flag — the consulted Claude uses the
|
|
152
184
|
// user's own ~/.claude config, mirroring Codex deferring to config.toml.
|
|
185
|
+
|
|
186
|
+
// Recursion guard: deny the consulted Claude the Claude→Codex bridge so it
|
|
187
|
+
// can't open a further sub-session. A bare server name removes all of
|
|
188
|
+
// gaslamp's tools from its context, and deny rules hold even under
|
|
189
|
+
// --dangerously-skip-permissions. GASLAMP_ALLOW_RECURSION=1 opts back in.
|
|
190
|
+
if (!ALLOW_RECURSION) args.push("--disallowedTools", "mcp__gaslamp");
|
|
153
191
|
if (resume) args.push("--resume", resume);
|
|
154
192
|
if (model) args.push("--model", model);
|
|
155
193
|
|
|
@@ -204,6 +242,7 @@ function handle(msg) {
|
|
|
204
242
|
} else if (method === "tools/call") {
|
|
205
243
|
const name = params?.name;
|
|
206
244
|
const a = params?.arguments || {};
|
|
245
|
+
if (NESTED && (name === "gaslamp" || name === "gaslamp-reply")) return refuseNested(id, name);
|
|
207
246
|
if (name === "gaslamp") {
|
|
208
247
|
if (!a.prompt) return fail(id, -32602, "missing required arg: prompt");
|
|
209
248
|
runClaude({ prompt: a.prompt, cwd: a.cwd, model: a.model, sandbox: a.sandbox }, id);
|
package/src/setup.mjs
CHANGED
|
@@ -106,10 +106,15 @@ export function runSetup(argv = []) {
|
|
|
106
106
|
if (okCodex) patchCodexTimeouts(); // codex mcp add can't persist these; do it ourselves
|
|
107
107
|
|
|
108
108
|
// --- Claude -> Codex: claude gets gaslamp's codex tools (user scope) --------
|
|
109
|
+
// `--env GASLAMP_NESTED=1` tags every Codex spawned as a Claude consult as
|
|
110
|
+
// nested, so gaslamp's server.mjs refuses if that consulted Codex tries to
|
|
111
|
+
// hand work back to Claude — a consult is one hop (GASLAMP_ALLOW_RECURSION=1
|
|
112
|
+
// opts back in). Placement is safe: the server name `gaslamp` precedes --env
|
|
113
|
+
// and `--` follows it, so the name is never misread as a KEY=VALUE pair.
|
|
109
114
|
remove("claude", "gaslamp", ["-s", "user"]);
|
|
110
115
|
remove("claude", "codex", ["-s", "user"]); // legacy name
|
|
111
116
|
const okClaude = run("claude -> codex", "claude",
|
|
112
|
-
["mcp", "add", "gaslamp", "-s", "user", "--", "codex", "mcp-server"]);
|
|
117
|
+
["mcp", "add", "gaslamp", "-s", "user", "--env", "GASLAMP_NESTED=1", "--", "codex", "mcp-server"]);
|
|
113
118
|
|
|
114
119
|
console.log();
|
|
115
120
|
if (okCodex && okClaude) {
|