mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-26 14:50:55 +02:00
v1.88.1.0 fix: harden credential boundaries and owned state (#2942)
* fix(settings): preserve symlinked settings targets
Resolve the selected target for locking, mutation, backup, and rollback; refuse target changes and preserve private modes. Addresses #2830.
* fix(redact): bind masking to original detected spans
Inspired by #2929's anchored-span diagnosis; independently implemented using normalization offsets. Addresses #2930 and the relocation portion of #2912 without changing detection sensitivity.
* fix(evals): exclude operator credentials from prefix admission
Adapts the credential-suffix screen proposed in #2636, with real launched-child regression coverage and deliberate provider-auth exceptions.
* fix(artifacts): retain custom allowlist rules on reinitialization
Preserve the exact user-owned suffix and publish only a successfully assembled replacement. Independently implements the repair reported in #2907.
* test(cso): verify exact masked reads and unmaskable payload refusal
* fix(cso): preserve exact filesystem identities through lease recovery
Preserve 64-bit device/inode identity and nanosecond race checks. Add native NTFS lifecycle coverage for #2927; retain ambiguous legacy-state refusal without claiming Windows PID-reuse recovery is resolved.
* fix(redact): bind pre-push scans to destination and preserve seam context
Uses #2935 (bd07318) as source evidence for push-target range and slice-overlap defects. Independently implemented; no cherry-pick or release metadata adoption.
* test(ci): gate native agent ownership and settings links on macOS
* fix(browse): bind agent lifetimes and cleanup to owned generations
Uses #2931 by Chris Hutton / Claude Fable 5.1 as attributed design input; independently implemented without broad sweeps or copied code. Keep uncertain children and locks rather than deleting foreign state.
* test(ci): include concurrent shutdown controls in the native macOS gate
* v1.88.1.0 fix: harden credential boundaries and owned state
* fix(redact): preserve target provenance and scan boundary semantics
* test(artifacts): read managed rules from atomic allowlist assembly
* fix: preserve native exit observations and fixture prerequisites
* fix: preserve UTF-16 offsets through redaction normalization
This commit is contained in:
@@ -26,7 +26,7 @@ import * as crypto from 'crypto';
|
||||
import { writeSecureFile, restrictFilePermissions, mkdirSecure } from './file-permissions';
|
||||
import { atomicWriteSync, atomicWriteQuiet } from '../../lib/fs-atomic';
|
||||
import { safeUnlink } from './error-handling';
|
||||
import { writeAgentRecord, clearAgentRecord } from './terminal-agent-control';
|
||||
import { writeAgentRecord, readAgentRecord, clearAgentRecord, readAgentStartTime, acquireAgentStateLock } from './terminal-agent-control';
|
||||
import { findAvailablePort } from './port-allocator';
|
||||
import { extractPtyCookie } from './pty-session-cookie';
|
||||
import {
|
||||
@@ -38,6 +38,7 @@ const STATE_FILE = process.env.BROWSE_STATE_FILE || path.join(process.env.HOME |
|
||||
const PORT_FILE = path.join(path.dirname(STATE_FILE), 'terminal-port');
|
||||
const BROWSE_SERVER_PORT = parseInt(process.env.BROWSE_SERVER_PORT || '0', 10);
|
||||
const BROWSE_OWNER_PID = parseInt(process.env.BROWSE_OWNER_PID || '0', 10);
|
||||
const BROWSE_OWNER_START_TIME = process.env.BROWSE_OWNER_START_TIME || (BROWSE_OWNER_PID > 0 ? readAgentStartTime(BROWSE_OWNER_PID) : '');
|
||||
const OWNER_WATCHDOG_MS = parseInt(
|
||||
process.env.GSTACK_TERMINAL_OWNER_WATCHDOG_MS || '15000',
|
||||
10,
|
||||
@@ -51,7 +52,7 @@ const INTERNAL_TOKEN = crypto.randomBytes(32).toString('base64url'); // shared w
|
||||
* header means "legacy caller" and is accepted (backward compat); a
|
||||
* present-but-mismatched header returns 409 stale generation.
|
||||
*/
|
||||
const CURRENT_GEN = crypto.randomBytes(16).toString('base64url');
|
||||
const CURRENT_GEN = process.env.BROWSE_AGENT_GEN || crypto.randomBytes(16).toString('base64url');
|
||||
|
||||
// In-memory attach-token registry. Parent posts /internal/grant after
|
||||
// /pty-session; we validate WS upgrades against this map.
|
||||
@@ -1004,6 +1005,25 @@ function readBrowseToken(): string {
|
||||
|
||||
// Boot.
|
||||
async function main() {
|
||||
const dir = path.dirname(PORT_FILE);
|
||||
if (process.env.BROWSE_AGENT_GEN) {
|
||||
const deadline = Date.now() + 2000;
|
||||
while (Date.now() < deadline) {
|
||||
const pending = readAgentRecord(dir);
|
||||
if (pending?.gen === CURRENT_GEN && pending.pid === process.pid) break;
|
||||
if (pending && pending.gen !== CURRENT_GEN) throw new Error('terminal-agent startup record was replaced');
|
||||
await Bun.sleep(25);
|
||||
}
|
||||
const recorded = readAgentRecord(dir);
|
||||
if (recorded?.pid !== process.pid || recorded.ownerPid !== BROWSE_OWNER_PID || recorded.ownerStartTime !== BROWSE_OWNER_START_TIME) {
|
||||
throw new Error('terminal-agent startup record was not confirmed');
|
||||
}
|
||||
}
|
||||
const pauseFile = process.env.NODE_ENV === 'test' ? process.env.GSTACK_TERMINAL_TEST_PUBLISH_BARRIER : undefined;
|
||||
if (pauseFile) {
|
||||
fs.writeFileSync(`${pauseFile}.ready`, 'ready');
|
||||
while (!fs.existsSync(pauseFile)) await Bun.sleep(10);
|
||||
}
|
||||
writeClaudeAvailable();
|
||||
// #2314: allocate from the shared fixed scan range, then bind. Probe-then-
|
||||
// bind has a TOCTOU window — a concurrent process can take the port between
|
||||
@@ -1032,17 +1052,21 @@ async function main() {
|
||||
|
||||
// Write port file atomically so the parent server can pick it up.
|
||||
// Throws on failure — a boot without a discoverable port file is broken.
|
||||
const dir = path.dirname(PORT_FILE);
|
||||
try { mkdirSecure(dir); } catch {}
|
||||
atomicWriteSync(PORT_FILE, String(port), { mode: 0o600 });
|
||||
restrictFilePermissions(PORT_FILE); // Windows ACL hardening
|
||||
|
||||
// Write identity-based agent record (pid + per-boot gen). Replaces the
|
||||
// v1.43- `pkill -f terminal-agent\.ts` regex teardown that could kill
|
||||
// sibling gstack sessions. Callers (cli.ts spawn site, server.ts
|
||||
// shutdown, the v1.44 watchdog) now route through killAgentByRecord in
|
||||
// terminal-agent-control.ts.
|
||||
writeAgentRecord(dir, { pid: process.pid, gen: CURRENT_GEN, startedAt: Date.now() });
|
||||
const releasePublication = acquireAgentStateLock(dir);
|
||||
let record;
|
||||
try {
|
||||
const current = readAgentRecord(dir);
|
||||
if (current && current.pid !== process.pid && current.pid > 0) throw new Error('terminal-agent record was replaced before bind');
|
||||
record = process.env.BROWSE_AGENT_GEN ? current : {
|
||||
pid: process.pid, gen: CURRENT_GEN, startedAt: Date.now(), startTime: readAgentStartTime(process.pid),
|
||||
ownerPid: BROWSE_OWNER_PID, ownerStartTime: BROWSE_OWNER_START_TIME,
|
||||
};
|
||||
if (!record || record.pid !== process.pid || record.gen !== CURRENT_GEN) throw new Error('terminal-agent record was replaced before bind');
|
||||
if (!process.env.BROWSE_AGENT_GEN) writeAgentRecord(dir, record);
|
||||
writeSecureFile(INTERNAL_TOKEN_FILE, INTERNAL_TOKEN);
|
||||
atomicWriteSync(PORT_FILE, String(port), { mode: 0o600 });
|
||||
restrictFilePermissions(PORT_FILE);
|
||||
} finally { releasePublication(); }
|
||||
|
||||
// Hand the parent the internal token so it can call /internal/grant.
|
||||
// Parent learns INTERNAL_TOKEN via env (TERMINAL_AGENT_INTERNAL_TOKEN below).
|
||||
@@ -1055,9 +1079,16 @@ async function main() {
|
||||
const cleanup = () => {
|
||||
if (cleaningUp) return;
|
||||
cleaningUp = true;
|
||||
safeUnlink(PORT_FILE);
|
||||
safeUnlink(INTERNAL_TOKEN_FILE);
|
||||
clearAgentRecord(dir);
|
||||
try {
|
||||
const releaseCleanup = acquireAgentStateLock(dir, 25);
|
||||
try {
|
||||
if (readAgentRecord(dir)?.gen === CURRENT_GEN) {
|
||||
safeUnlink(PORT_FILE);
|
||||
safeUnlink(INTERNAL_TOKEN_FILE);
|
||||
clearAgentRecord(dir, record);
|
||||
}
|
||||
} finally { releaseCleanup(); }
|
||||
} catch {}
|
||||
process.exit(0);
|
||||
};
|
||||
process.on('SIGTERM', cleanup);
|
||||
@@ -1070,11 +1101,8 @@ async function main() {
|
||||
// the same cleanup path as an intentional shutdown when it disappears.
|
||||
if (BROWSE_OWNER_PID > 0) {
|
||||
const ownerWatchdog = setInterval(() => {
|
||||
try {
|
||||
process.kill(BROWSE_OWNER_PID, 0);
|
||||
} catch {
|
||||
cleanup();
|
||||
}
|
||||
if (!BROWSE_OWNER_START_TIME || readAgentStartTime(BROWSE_OWNER_PID) !== BROWSE_OWNER_START_TIME
|
||||
|| readAgentRecord(dir)?.gen !== CURRENT_GEN) cleanup();
|
||||
}, OWNER_WATCHDOG_MS);
|
||||
(ownerWatchdog as any)?.unref?.();
|
||||
}
|
||||
@@ -1087,10 +1115,6 @@ async function main() {
|
||||
// In practice, the agent generates INTERNAL_TOKEN once at boot and writes it
|
||||
// to a state file the parent reads. This avoids env-passing races. See main().
|
||||
const INTERNAL_TOKEN_FILE = path.join(path.dirname(STATE_FILE), 'terminal-internal-token');
|
||||
try {
|
||||
mkdirSecure(path.dirname(INTERNAL_TOKEN_FILE));
|
||||
writeSecureFile(INTERNAL_TOKEN_FILE, INTERNAL_TOKEN);
|
||||
} catch {}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(`[terminal-agent] boot failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
|
||||
Reference in New Issue
Block a user