mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 03:35:09 +02:00
feat(security): wire logAttempt to gstack-telemetry-log (fire-and-forget)
Every local attempt.jsonl write now also triggers a subprocess call to
gstack-telemetry-log with the attack_attempt event type. The binary handles
tier gating internally (community → Supabase upload, anonymous → local
JSONL only, off → no-op), so security.ts doesn't need to re-check.
Binary resolution follows the skill preamble pattern — never relies on PATH,
which breaks in compiled-binary contexts:
1. ~/.claude/skills/gstack/bin/gstack-telemetry-log (global install)
2. .claude/skills/gstack/bin/gstack-telemetry-log (symlinked dev)
3. bin/gstack-telemetry-log (in-repo dev)
Fire-and-forget:
* spawn with stdio: 'ignore', detached: true, unref()
* .on('error') swallows failures
* Missing binary is non-fatal — local attempts.jsonl still gives audit trail
Never throws. Never blocks. Existing 37 security tests pass unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+67
-2
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
|
||||
import { randomBytes, createHash } from 'crypto';
|
||||
import { spawn } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
@@ -258,10 +259,74 @@ function rotateIfNeeded(): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Append an attempt to the local log. Never throws — logging failure should
|
||||
* not break the sidebar. Returns true if the write succeeded.
|
||||
* Try to locate the gstack-telemetry-log binary. Resolution order matches
|
||||
* the existing skill preamble pattern (never relies on PATH — packaged
|
||||
* binary layouts can break that).
|
||||
*
|
||||
* Order:
|
||||
* 1. ~/.claude/skills/gstack/bin/gstack-telemetry-log (global install)
|
||||
* 2. .claude/skills/gstack/bin/gstack-telemetry-log (symlinked dev)
|
||||
* 3. bin/gstack-telemetry-log (in-repo dev)
|
||||
*/
|
||||
function findTelemetryBinary(): string | null {
|
||||
const candidates = [
|
||||
path.join(os.homedir(), '.claude', 'skills', 'gstack', 'bin', 'gstack-telemetry-log'),
|
||||
path.resolve(process.cwd(), '.claude', 'skills', 'gstack', 'bin', 'gstack-telemetry-log'),
|
||||
path.resolve(process.cwd(), 'bin', 'gstack-telemetry-log'),
|
||||
];
|
||||
for (const c of candidates) {
|
||||
try {
|
||||
fs.accessSync(c, fs.constants.X_OK);
|
||||
return c;
|
||||
} catch {
|
||||
// try next
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire-and-forget subprocess invocation of gstack-telemetry-log with the
|
||||
* attack_attempt event type. The binary handles tier gating internally
|
||||
* (community → upload, anonymous → local only, off → no-op), so we don't
|
||||
* need to re-check here.
|
||||
*
|
||||
* Never throws. Never blocks. If the binary isn't found or spawn fails, the
|
||||
* local attempts.jsonl write from logAttempt() still gives us the audit trail.
|
||||
*/
|
||||
function reportAttemptTelemetry(record: AttemptRecord): void {
|
||||
const bin = findTelemetryBinary();
|
||||
if (!bin) return;
|
||||
try {
|
||||
const child = spawn(bin, [
|
||||
'--event-type', 'attack_attempt',
|
||||
'--url-domain', record.urlDomain || '',
|
||||
'--payload-hash', record.payloadHash,
|
||||
'--confidence', String(record.confidence),
|
||||
'--layer', record.layer,
|
||||
'--verdict', record.verdict,
|
||||
], {
|
||||
stdio: 'ignore',
|
||||
detached: true,
|
||||
});
|
||||
// unref so this subprocess doesn't hold the event loop open
|
||||
child.unref();
|
||||
child.on('error', () => { /* swallow — telemetry must never break sidebar */ });
|
||||
} catch {
|
||||
// Spawn failure is non-fatal.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append an attempt to the local log AND fire telemetry via
|
||||
* gstack-telemetry-log (which respects the user's telemetry tier setting).
|
||||
* Never throws — logging failure should not break the sidebar.
|
||||
* Returns true if the local write succeeded.
|
||||
*/
|
||||
export function logAttempt(record: AttemptRecord): boolean {
|
||||
// Fire telemetry first, async — even if local write fails, we still want
|
||||
// the event reported (it goes to a different directory anyway).
|
||||
reportAttemptTelemetry(record);
|
||||
try {
|
||||
fs.mkdirSync(SECURITY_DIR, { recursive: true, mode: 0o700 });
|
||||
rotateIfNeeded();
|
||||
|
||||
Reference in New Issue
Block a user