Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/bounded-agent/manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,7 @@ describe('prepareBoundedAgents', () => {
probe: expect.stringMatching(/^[0-9a-f]{64}$/),
});
expect(capabilities.query).not.toBe(capabilities.probe);
expect(fs.statSync(capabilityPath).mode & 0o777).toBe(0o600);
expect(probeSbxUnixSocket).toHaveBeenCalledWith('bounded-agent');
});
});
Expand Down
22 changes: 5 additions & 17 deletions src/bounded-agent/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ import {
resolveBoundedAgentPrimaryBackend,
serializeBoundedAgentRuntimeTelemetry,
} from './runtime-matrix';
import {
type SbxIngressCapabilities,
writeSbxIngressCapabilitiesFile,
} from '../bounded-execution/sbx-ingress-capabilities';

/**
* Bounded-agent lifecycle orchestration.
Expand Down Expand Up @@ -163,29 +167,13 @@ export interface PrepareBoundedAgentsDeps {
assertPrimaryAvailable?: typeof assertPrimaryRuntimeAvailable;
}

interface SbxIngressCapabilities {
version: 1;
query: string;
probe: string;
}

function writeSbxIngressCapabilities(paths: BoundedAgentPaths): void {
const capabilities: SbxIngressCapabilities = {
version: 1,
query: crypto.randomBytes(32).toString('hex'),
probe: crypto.randomBytes(32).toString('hex'),
};
const fd = fs.openSync(
paths.capabilityPath,
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW,
0o600,
);
try {
fs.writeSync(fd, JSON.stringify(capabilities));
fs.fchmodSync(fd, 0o600);
} finally {
fs.closeSync(fd);
}
writeSbxIngressCapabilitiesFile(paths.capabilityPath, capabilities);
}

/**
Expand Down
69 changes: 69 additions & 0 deletions src/bounded-execution/sbx-ingress-capabilities.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import * as fs from 'fs';
import {
type SbxIngressCapabilities,
writeSbxIngressCapabilitiesFile,
} from './sbx-ingress-capabilities';

const capabilities: SbxIngressCapabilities = {
version: 1,
query: 'a'.repeat(64),
probe: 'b'.repeat(64),
};

function createFileOps(overrides: Record<string, jest.Mock> = {}) {
return {
open: overrides.open ?? jest.fn(() => 42),
write: overrides.write ?? jest.fn(),
chmod: overrides.chmod ?? jest.fn(),
close: overrides.close ?? jest.fn(),
};
}

describe('writeSbxIngressCapabilitiesFile', () => {
it('writes the unchanged payload with exclusive no-follow creation and mode hardening', () => {
const fileOps = createFileOps();

writeSbxIngressCapabilitiesFile('/private/capabilities.json', capabilities, fileOps);

expect(fileOps.open).toHaveBeenCalledWith(
'/private/capabilities.json',
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW,
0o600,
);
expect(fileOps.write).toHaveBeenCalledWith(42, JSON.stringify(capabilities));
expect(fileOps.chmod).toHaveBeenCalledWith(42, 0o600);
expect(fileOps.close).toHaveBeenCalledWith(42);
expect(fileOps.write.mock.invocationCallOrder[0])
.toBeLessThan(fileOps.chmod.mock.invocationCallOrder[0]);
expect(fileOps.chmod.mock.invocationCallOrder[0])
.toBeLessThan(fileOps.close.mock.invocationCallOrder[0]);
});

it.each(['write', 'chmod'] as const)('closes the descriptor and propagates a %s failure', (operation) => {
const failure = new Error(`${operation} failed`);
const fileOps = createFileOps({ [operation]: jest.fn(() => { throw failure; }) });

expect(() => writeSbxIngressCapabilitiesFile('/private/capabilities.json', capabilities, fileOps))
.toThrow(failure);
expect(fileOps.close).toHaveBeenCalledWith(42);
});

it('propagates an open failure without attempting file operations', () => {
const failure = new Error('open failed');
const fileOps = createFileOps({ open: jest.fn(() => { throw failure; }) });

expect(() => writeSbxIngressCapabilitiesFile('/private/capabilities.json', capabilities, fileOps))
.toThrow(failure);
expect(fileOps.write).not.toHaveBeenCalled();
expect(fileOps.chmod).not.toHaveBeenCalled();
expect(fileOps.close).not.toHaveBeenCalled();
});

it('propagates a close failure', () => {
const failure = new Error('close failed');
const fileOps = createFileOps({ close: jest.fn(() => { throw failure; }) });

expect(() => writeSbxIngressCapabilitiesFile('/private/capabilities.json', capabilities, fileOps))
.toThrow(failure);
});
});
45 changes: 45 additions & 0 deletions src/bounded-execution/sbx-ingress-capabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as fs from 'fs';

export interface SbxIngressCapabilities {
version: 1;
query: string;
probe: string;
}

interface SbxIngressCapabilityFileOps {
open(path: string, flags: number, mode: number): number;
write(fd: number, content: string): void;
chmod(fd: number, mode: number): void;
close(fd: number): void;
}

const defaultFileOps: SbxIngressCapabilityFileOps = {
open: fs.openSync,
write: fs.writeSync,
chmod: fs.fchmodSync,
close: fs.closeSync,
};

/**
* Atomically creates an sbx ingress capability file without following symlinks.
*
* The explicit chmod hardens the final inode mode independently of the process
* umask, and closing in finally ensures write/chmod failures do not leak the fd.
*/
export function writeSbxIngressCapabilitiesFile(
capabilityPath: string,
capabilities: SbxIngressCapabilities,
fileOps: SbxIngressCapabilityFileOps = defaultFileOps,
): void {
const fd = fileOps.open(
capabilityPath,
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW,
0o600,
);
try {
fileOps.write(fd, JSON.stringify(capabilities));
fileOps.chmod(fd, 0o600);
} finally {
fileOps.close(fd);
}
}
4 changes: 3 additions & 1 deletion src/bounded-query/manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,14 @@ describe('prepareBoundedQueries', () => {
expect(fs.existsSync(paths.capabilityPath)).toBe(!supported);
if (!supported) {
const raw = fs.readFileSync(paths.capabilityPath, 'utf8');
const capabilities = JSON.parse(raw);
expect(raw).not.toContain('GH_TOKEN');
expect(JSON.parse(raw)).toEqual({
expect(capabilities).toEqual({
version: 1,
query: expect.stringMatching(/^[0-9a-f]{64}$/),
probe: expect.stringMatching(/^[0-9a-f]{64}$/),
});
expect(capabilities.query).not.toBe(capabilities.probe);
expect(fs.statSync(paths.capabilityPath).mode & 0o777).toBe(0o600);
}
});
Expand Down
22 changes: 5 additions & 17 deletions src/bounded-query/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ import {
resolveBoundedQueryPrimaryBackend,
serializeBoundedQueryRuntimeTelemetry,
} from './runtime-matrix';
import {
type SbxIngressCapabilities,
writeSbxIngressCapabilitiesFile,
} from '../bounded-execution/sbx-ingress-capabilities';

/**
* Bounded-query lifecycle orchestration.
Expand Down Expand Up @@ -156,29 +160,13 @@ export interface PrepareBoundedQueriesDeps {
assertPrimaryAvailable?: typeof assertPrimaryRuntimeAvailable;
}

interface SbxIngressCapabilities {
version: 1;
query: string;
probe: string;
}

function writeSbxIngressCapabilities(paths: BoundedQueryPaths): void {
const capabilities: SbxIngressCapabilities = {
version: 1,
query: crypto.randomBytes(32).toString('hex'),
probe: crypto.randomBytes(32).toString('hex'),
};
const fd = fs.openSync(
paths.capabilityPath,
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW,
0o600,
);
try {
fs.writeSync(fd, JSON.stringify(capabilities));
fs.fchmodSync(fd, 0o600);
} finally {
fs.closeSync(fd);
}
writeSbxIngressCapabilitiesFile(paths.capabilityPath, capabilities);
}

/**
Expand Down
Loading