mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-26 22:51:47 +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:
@@ -1,4 +1,4 @@
|
||||
import { describe, test, expect, beforeEach, beforeAll, afterAll } from 'bun:test';
|
||||
import { describe, test, expect, beforeEach, beforeAll, afterAll, spyOn } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
@@ -219,4 +219,83 @@ describe('buildFetchHandler ownsTerminalAgent gate', () => {
|
||||
// match cannot be satisfied by the JSDoc reference earlier in the file.
|
||||
expect(source).toMatch(/ownsTerminalAgent:\s*true,\s*\/\/\s*CLI spawns terminal-agent\.ts/);
|
||||
});
|
||||
|
||||
test('5. shutdown cannot remove a successor published after its current-record read', async () => {
|
||||
writeSentinels();
|
||||
const ready = path.join(fixtureDir, 'competitor-ready');
|
||||
const script = path.join(fixtureDir, 'competitor.ts');
|
||||
fs.writeFileSync(script, `
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { acquireAgentStateLock } from ${JSON.stringify(path.resolve(import.meta.dir, '../src/terminal-agent-control.ts'))};
|
||||
const stateDir = process.argv[2];
|
||||
fs.writeFileSync(${JSON.stringify(ready)}, 'ready');
|
||||
const release = acquireAgentStateLock(stateDir);
|
||||
try {
|
||||
fs.writeFileSync(path.join(stateDir, 'terminal-port'), 'successor-port');
|
||||
fs.writeFileSync(path.join(stateDir, 'terminal-internal-token'), 'synthetic-successor-token');
|
||||
fs.writeFileSync(path.join(stateDir, 'terminal-agent-pid'), JSON.stringify({ pid: process.pid, gen: 'successor', startedAt: Date.now() }));
|
||||
} finally { release(); }
|
||||
`);
|
||||
const originalRead = fs.readFileSync;
|
||||
let recordReads = 0;
|
||||
let actor: ReturnType<typeof Bun.spawn> | undefined;
|
||||
const reader = spyOn(fs, 'readFileSync').mockImplementation(((file: fs.PathOrFileDescriptor, options?: any) => {
|
||||
const result = originalRead(file as any, options);
|
||||
if (String(file) === AGENT_RECORD_FILE && ++recordReads === 2) {
|
||||
actor = Bun.spawn([process.execPath, script, stateDir], { stdio: ['ignore', 'ignore', 'ignore'] });
|
||||
const deadline = Date.now() + 3000;
|
||||
while (!fs.existsSync(ready) && Date.now() < deadline) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
||||
if (!fs.existsSync(ready)) throw new Error('competitor never reached publication');
|
||||
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 150);
|
||||
}
|
||||
return result;
|
||||
}) as typeof fs.readFileSync);
|
||||
try {
|
||||
const handle = buildFetchHandler(makeMinimalConfig({ ownsTerminalAgent: true }));
|
||||
await withStubs(async () => runShutdown(handle));
|
||||
expect(recordReads).toBeGreaterThanOrEqual(2);
|
||||
expect(actor).toBeDefined();
|
||||
expect(await Promise.race([actor!.exited.then(() => true), Bun.sleep(5000).then(() => false)])).toBe(true);
|
||||
expect(readIfExists(PORT_FILE)).toBe('successor-port');
|
||||
expect(readIfExists(TOKEN_FILE)).toBe('synthetic-successor-token');
|
||||
expect(JSON.parse(readIfExists(AGENT_RECORD_FILE)!)).toMatchObject({ gen: 'successor' });
|
||||
} finally {
|
||||
reader.mockRestore();
|
||||
try { actor?.kill('SIGKILL'); } catch {}
|
||||
fs.rmSync(ready, { force: true });
|
||||
fs.rmSync(script, { force: true });
|
||||
}
|
||||
}, 15000);
|
||||
|
||||
test('6. unavailable state lock retains agent files rather than guessing ownership', async () => {
|
||||
writeSentinels();
|
||||
const originalOpen = fs.openSync;
|
||||
const opened = spyOn(fs, 'openSync').mockImplementation(((file: fs.PathLike, flags: string | number, mode?: number) => {
|
||||
if (String(file) === path.join(stateDir, 'terminal-agent-pid.lock')) {
|
||||
throw Object.assign(new Error('synthetic lock denial'), { code: 'EACCES' });
|
||||
}
|
||||
return originalOpen(file, flags as any, mode);
|
||||
}) as typeof fs.openSync);
|
||||
try {
|
||||
const handle = buildFetchHandler(makeMinimalConfig({ ownsTerminalAgent: true }));
|
||||
await withStubs(async () => runShutdown(handle));
|
||||
expect(readIfExists(PORT_FILE)).toBe(SENTINEL_PORT);
|
||||
expect(readIfExists(TOKEN_FILE)).toBe(SENTINEL_TOKEN);
|
||||
expect(readIfExists(AGENT_RECORD_FILE)).not.toBeNull();
|
||||
} finally { opened.mockRestore(); }
|
||||
});
|
||||
|
||||
test('7. late state takeover is not removed after browser close', async () => {
|
||||
fs.mkdirSync(stateDir, { recursive: true });
|
||||
fs.writeFileSync(fixtureConfig.stateFile, JSON.stringify({ pid: process.pid }));
|
||||
const successor = { pid: process.pid, instanceId: 'synthetic-late-successor' };
|
||||
const browserManager = new BrowserManager();
|
||||
browserManager.close = async () => { fs.writeFileSync(fixtureConfig.stateFile, JSON.stringify(successor)); };
|
||||
try {
|
||||
const handle = buildFetchHandler(makeMinimalConfig({ browserManager, ownsTerminalAgent: false }));
|
||||
await withStubs(async () => runShutdown(handle));
|
||||
expect(JSON.parse(fs.readFileSync(fixtureConfig.stateFile, 'utf8'))).toEqual(successor);
|
||||
} finally { fs.rmSync(fixtureConfig.stateFile, { force: true }); }
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user