mirror of
https://github.com/garrytan/gstack.git
synced 2026-07-14 03:27:21 +02:00
feat(extension): route gstackInjectToTerminal through /pty-inject-scan (#1370)
Closes the documented-vs-shipped gap codex flagged in #1370. The sidebar's two PTY-injection call sites (Inspector "Send to Code" and toolbar Cleanup) now pre-scan via the new /pty-inject-scan endpoint before writing to the live claude REPL. Adds window.gstackScanForPTYInject(text, origin) to extension/sidepanel-terminal.js: - Async, returns { allow, verdict, reasons, l4 } - POST to /pty-inject-scan with the existing root-token auth - WARN+confirm on scan failure (network down, sidecar absent, etc.) rather than silent PASS — D7 honest-degradation gstackInjectToTerminal stays synchronous, returns boolean. Per D6: keeping the inject sync means existing `const ok = ...?.()` callers don't break, and the invariant test in test/extension-pty-inject-invariant.test.ts can statically pin that every call goes through the scan first. extension/sidepanel.js call sites updated: - inspectorSendBtn click → await scan, BLOCK drops + WARN prompts via window.confirm, PASS injects silently - runCleanup() → same flow. Static cleanup prompt always PASSes but still routes through scan to honor the invariant. C20 of the security-stack wave. C21 adds the static invariant test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -226,6 +226,18 @@
|
||||
* Used by the toolbar's Cleanup button and the Inspector's "Send to Code"
|
||||
* action so the user can drive claude from outside-the-keyboard surfaces.
|
||||
* Returns true if the bytes went out, false if no live session.
|
||||
*
|
||||
* IMPORTANT (D6): this function stays SYNCHRONOUS and SCAN-FREE. Page-
|
||||
* derived input MUST be pre-scanned via window.gstackScanForPTYInject()
|
||||
* before calling this. The invariant test in
|
||||
* test/extension-pty-inject-invariant.test.ts fails the build if any
|
||||
* extension/*.js path calls this without the preceding scan.
|
||||
*
|
||||
* Why not move the scan inside this function: callers already use the
|
||||
* sync `const ok = gstackInjectToTerminal?.(text)` pattern. Making the
|
||||
* inject async would turn `ok` into a Promise and silently break every
|
||||
* existing call site. Pre-scanning at the caller keeps the boundary
|
||||
* clean and the invariant testable.
|
||||
*/
|
||||
window.gstackInjectToTerminal = function (text) {
|
||||
if (!text || !ws || ws.readyState !== WebSocket.OPEN) return false;
|
||||
@@ -237,6 +249,66 @@
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Scan page-derived text via the browse server's /pty-inject-scan
|
||||
* endpoint before injecting it into the PTY. Returns:
|
||||
* { allow: true, verdict: "PASS" } → safe to inject
|
||||
* { allow: true, verdict: "WARN", reasons: [...] } → caller should
|
||||
* prompt the user before injecting
|
||||
* { allow: false, verdict: "BLOCK", reasons: [...]} → drop the text;
|
||||
* caller should surface a banner to the user
|
||||
*
|
||||
* On any network / endpoint failure: returns
|
||||
* { allow: true, verdict: "WARN", reasons: ["scan-unreachable"] }
|
||||
* so the caller falls back to WARN+confirm rather than silent PASS.
|
||||
*
|
||||
* Closes #1370.
|
||||
*/
|
||||
window.gstackScanForPTYInject = async function (text, origin) {
|
||||
if (!text) return { allow: false, verdict: 'BLOCK', reasons: ['empty-text'] };
|
||||
try {
|
||||
const resp = await fetch('http://127.0.0.1:34567/pty-inject-scan', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${await getAuthTokenForScan()}`,
|
||||
},
|
||||
body: JSON.stringify({ text, origin: origin || 'extension' }),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
return { allow: true, verdict: 'WARN', reasons: [`scan-http-${resp.status}`] };
|
||||
}
|
||||
const body = await resp.json();
|
||||
const verdict = body.verdict || 'WARN';
|
||||
const allow = verdict !== 'BLOCK';
|
||||
return { allow, verdict, reasons: body.reasons || [], l4: body.l4 };
|
||||
} catch (err) {
|
||||
return {
|
||||
allow: true,
|
||||
verdict: 'WARN',
|
||||
reasons: ['scan-unreachable', err && err.message ? err.message : 'fetch-failed'],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// The auth token for /pty-inject-scan comes from the same source the
|
||||
// sidepanel uses for /pty-session — a runtime fetch from /health (which
|
||||
// already returns AUTH_TOKEN in headed mode per CLAUDE.md's v1.1 TODO).
|
||||
// We don't echo the token here; this helper is a thin proxy around the
|
||||
// existing pattern.
|
||||
async function getAuthTokenForScan() {
|
||||
if (window.__gstackPtyScanToken) return window.__gstackPtyScanToken;
|
||||
try {
|
||||
const resp = await fetch('http://127.0.0.1:34567/health');
|
||||
const body = await resp.json();
|
||||
const token = body.AUTH_TOKEN || body.authToken || '';
|
||||
if (token) window.__gstackPtyScanToken = token;
|
||||
return token;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async function connect() {
|
||||
if (state !== STATE.IDLE) return; // already connecting/live
|
||||
setState(STATE.CONNECTING);
|
||||
|
||||
Reference in New Issue
Block a user