Skip to content

Commit c0c5429

Browse files
authored
Harden stitch enable: guard, null mcp, OAuth opt-out (#17)
Match cmd_serena_enable and refuse to overwrite an existing stitch entry. Write through the normalized mcp dict so "mcp": null fails closed instead of raising TypeError. Set oauth: false in API-key mode so a bad STITCH_API_KEY surfaces as an auth error rather than opening a browser OAuth flow. Also make the missing-release-tag test skip explicitly when v$VERSION exists in the checkout; it previously passed in CI only because actions/checkout does not fetch tags at fetch-depth 1.
1 parent 3d32e75 commit c0c5429

3 files changed

Lines changed: 52 additions & 1 deletion

File tree

lib/install.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1302,6 +1302,9 @@ def cmd_stitch_enable(oauth: bool = False) -> int:
13021302
"enabled": True,
13031303
}
13041304
if not oauth:
1305+
# API-key mode: suppress OpenCode's automatic OAuth-on-401 so a bad key
1306+
# surfaces as an auth error instead of starting a browser OAuth flow.
1307+
spec["oauth"] = False
13051308
spec["headers"] = {
13061309
"X-Goog-Api-Key": "{env:STITCH_API_KEY}",
13071310
}
@@ -1324,6 +1327,9 @@ def cmd_stitch_enable(oauth: bool = False) -> int:
13241327
mcp = data.get("mcp") or {}
13251328
if not isinstance(mcp, dict):
13261329
die("OPENCODE_CONFIG_INVALID mcp")
1330+
if "stitch" in mcp:
1331+
info("stitch MCP already present; not overwriting (run `stitch disable` first to change auth mode)")
1332+
return 0
13271333
if jsonc.contains_comments(raw):
13281334
try:
13291335
merged = jsonc.upsert_mcp_servers(raw, {"stitch": spec})
@@ -1332,7 +1338,8 @@ def cmd_stitch_enable(oauth: bool = False) -> int:
13321338
except Exception as exc:
13331339
die(f"OPENCODE_CONFIG_JSONC_SURGICAL_FAILED: {exc}")
13341340
else:
1335-
data.setdefault("mcp", {})["stitch"] = spec
1341+
mcp["stitch"] = spec
1342+
data["mcp"] = mcp
13361343
path.write_text(jsonc.dumps(data), encoding="utf-8")
13371344
info(f"enabled stitch MCP in {path}")
13381345
return 0

tests/test_install.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,38 @@ def test_stitch_enable_requires_env_or_oauth(self):
306306
with self.assertRaises(SystemExit):
307307
cmd_stitch_enable(oauth=False)
308308

309+
def test_stitch_enable_does_not_overwrite_existing_entry(self):
310+
cfg = self.tmp / ".config" / "opencode" / "opencode.jsonc"
311+
cfg.parent.mkdir(parents=True, exist_ok=True)
312+
original = (
313+
"{\n"
314+
' "mcp": {\n'
315+
' "stitch": {\n'
316+
' "type": "remote",\n'
317+
' "url": "https://stitch.googleapis.com/mcp",\n'
318+
' "enabled": true,\n'
319+
' "headers": {"Authorization": "{env:MY_OWN_TOKEN}"}\n'
320+
" }\n"
321+
" }\n"
322+
"}\n"
323+
)
324+
cfg.write_text(original, encoding="utf-8")
325+
os.environ["STITCH_API_KEY"] = "mock_secret_key_12345"
326+
try:
327+
self.assertEqual(cmd_stitch_enable(oauth=False), 0)
328+
finally:
329+
os.environ.pop("STITCH_API_KEY", None)
330+
self.assertEqual(cfg.read_text(encoding="utf-8"), original)
331+
332+
def test_stitch_enable_null_mcp_fails_closed(self):
333+
cfg = self.tmp / ".config" / "opencode" / "opencode.jsonc"
334+
cfg.parent.mkdir(parents=True, exist_ok=True)
335+
original = '{\n "mcp": null\n}\n'
336+
cfg.write_text(original, encoding="utf-8")
337+
self.assertEqual(cmd_stitch_enable(oauth=True), 0)
338+
data = jsonc.loads(cfg.read_text(encoding="utf-8"))
339+
self.assertEqual(data["mcp"]["stitch"]["url"], "https://stitch.googleapis.com/mcp")
340+
309341
def test_stitch_enable_with_env_key(self):
310342
os.environ["STITCH_API_KEY"] = "mock_secret_key_12345"
311343
try:

tests/test_release_artifacts.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,18 @@ def test_builder_version_tag_mismatch(self):
171171

172172
def test_builder_missing_release_tag_fails_closed(self):
173173
head = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip()
174+
version = (ROOT / "VERSION").read_text(encoding="utf-8").strip()
175+
expected_tag = f"v{version}"
176+
tag_exists = (
177+
subprocess.run(
178+
["git", "rev-parse", "-q", "--verify", f"refs/tags/{expected_tag}"],
179+
cwd=ROOT,
180+
capture_output=True,
181+
).returncode
182+
== 0
183+
)
184+
if tag_exists:
185+
self.skipTest(f"{expected_tag} exists in this checkout; missing-tag path not reachable")
174186
proc = _run_builder("--sha", head)
175187
self.assertNotEqual(proc.returncode, 0)
176188
self.assertIn("SOURCE_REF_MISMATCH", proc.stderr)

0 commit comments

Comments
 (0)