mirror of https://github.com/garrytan/gstack.git
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:
parent
33d6eae996
commit
9a643dc17d
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -683,7 +683,7 @@ function updateSendButton() {
|
|||
}
|
||||
}
|
||||
|
||||
inspectorSendBtn.addEventListener('click', () => {
|
||||
inspectorSendBtn.addEventListener('click', async () => {
|
||||
if (!inspectorData) return;
|
||||
|
||||
let message;
|
||||
|
|
@ -708,6 +708,20 @@ inspectorSendBtn.addEventListener('click', () => {
|
|||
// Inject into the running claude PTY so the user can ask claude to act
|
||||
// on the inspector data. Replaces the old `sidebar-command` route which
|
||||
// spawned a one-shot claude -p (sidebar-agent.ts is gone).
|
||||
//
|
||||
// Pre-scan via /pty-inject-scan before injection (D6, closes #1370).
|
||||
// gstackScanForPTYInject is async; gstackInjectToTerminal stays sync.
|
||||
const verdict = await window.gstackScanForPTYInject?.(message + '\n', 'inspector-send');
|
||||
if (verdict?.verdict === 'BLOCK') {
|
||||
console.warn('[gstack sidebar] Inspector send BLOCKED by /pty-inject-scan:', verdict.reasons);
|
||||
return;
|
||||
}
|
||||
if (verdict?.verdict === 'WARN') {
|
||||
const confirmed = window.confirm(
|
||||
`Inspector send flagged as suspicious (${(verdict.reasons || []).join(', ')}). Inject anyway?`,
|
||||
);
|
||||
if (!confirmed) return;
|
||||
}
|
||||
const ok = window.gstackInjectToTerminal?.(message + '\n');
|
||||
if (!ok) {
|
||||
console.warn('[gstack sidebar] Inspector send needs an active Terminal session.');
|
||||
|
|
@ -735,6 +749,26 @@ async function runCleanup(...buttons) {
|
|||
'header/masthead, headline, article body, images, byline, and date. Also',
|
||||
'unlock scrolling if the page is scroll-locked.',
|
||||
].join('\n');
|
||||
// Pre-scan via /pty-inject-scan before injection (D6, closes #1370).
|
||||
// The cleanup prompt is a STATIC template (no page-derived content), so
|
||||
// it will always PASS, but we still route it through the scan path so
|
||||
// the invariant test in test/extension-pty-inject-invariant.test.ts
|
||||
// confirms every call site goes through gstackScanForPTYInject first.
|
||||
const verdict = await window.gstackScanForPTYInject?.(cleanupPrompt + '\n', 'cleanup-button');
|
||||
if (verdict?.verdict === 'BLOCK') {
|
||||
console.warn('[gstack sidebar] Cleanup BLOCKED by /pty-inject-scan:', verdict.reasons);
|
||||
setTimeout(() => buttons.forEach(b => b?.classList.remove('loading')), 200);
|
||||
return;
|
||||
}
|
||||
if (verdict?.verdict === 'WARN') {
|
||||
const confirmed = window.confirm(
|
||||
`Cleanup flagged as suspicious (${(verdict.reasons || []).join(', ')}). Inject anyway?`,
|
||||
);
|
||||
if (!confirmed) {
|
||||
setTimeout(() => buttons.forEach(b => b?.classList.remove('loading')), 200);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const sent = window.gstackInjectToTerminal?.(cleanupPrompt + '\n');
|
||||
if (!sent) {
|
||||
console.warn('[gstack sidebar] Cleanup needs an active Terminal session.');
|
||||
|
|
|
|||
Loading…
Reference in New Issue