Skip to content
Closed
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
19 changes: 19 additions & 0 deletions packages/host-kit/src/session-paths.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { test } from 'vitest';
import assert from 'node:assert/strict';
// oxlint-disable-next-line no-restricted-imports -- asserts a path under os.homedir
import os from 'node:os';
import path from 'node:path';
import { expandSessionPath } from './session-paths.ts';

test('expandSessionPath resolves tilde, relative-with-cwd, and absolute paths', () => {
const homePath = expandSessionPath('~/flows/replay.ad');
assert.equal(homePath.startsWith(os.homedir()), true);
assert.equal(homePath.endsWith(path.join('flows', 'replay.ad')), true);

const relativePath = expandSessionPath('workflows/replay.ad', '/tmp/agent-device-cwd');
assert.equal(relativePath, path.resolve('/tmp/agent-device-cwd', 'workflows/replay.ad'));

const absoluteInput = path.resolve('/tmp', 'agent-device-absolute.ad');
const absolutePath = expandSessionPath(absoluteInput, '/tmp/ignored-cwd');
assert.equal(absolutePath, absoluteInput);
});
162 changes: 161 additions & 1 deletion src/__tests__/daemon-registration-owner.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { afterEach, test, vi } from 'vitest';
import { readCurrentOwnerIdentity } from '@agent-device/host-kit/process';
import { readCurrentOwnerIdentity, isProcessAlive } from '@agent-device/host-kit/process';
import {
tryAcquireDaemonRegistration,
stopAndRetireDaemon,
recoverAbandonedDaemonRegistration,
createOwnedReplayStateDir,
DAEMON_STARTUP_EXIT_CODES,
launchDaemonProcess,
type OwnedReplayStateDir,
} from '../daemon-registration-owner.ts';
import { resolveDaemonPaths, type DaemonPaths } from '../daemon-resolution.ts';
import { readRegisteredDaemonOwnership } from '../daemon-registration.ts';
import { readDaemonShutdownReport } from '../daemon-shutdown-report.ts';
import { mkdtempForTestSync } from './test-utils/tmp-dir.ts';
import { registeredDaemonFixtureArgs } from './test-utils/registered-daemon-fixture.ts';
import { sleep } from '@agent-device/host-kit/retry';
import { stopDaemonProcess } from '../daemon-process.ts';

const fields = {
socketPort: 4210,
Expand Down Expand Up @@ -305,3 +312,156 @@ test.skipIf(process.getuid?.() === 0)(
}
},
);

test('a forged private-directory capability cannot authorize even matching dead metadata removal', async () => {
const paths = resolveDaemonPaths(mkdtempForTestSync('agent-device-private-forgery-'));
replaceInfo(paths, deadIdentity.pid, deadIdentity.startTime);
const before = fs.readFileSync(paths.infoPath, 'utf8');
const result = await stopAndRetireDaemon({
paths,
observed: deadIdentity,
mode: 'force',
ownedStateDir: Object.freeze({ paths }) as OwnedReplayStateDir,
});
assert.equal(result.status, 'retained');
assert.equal(fs.readFileSync(paths.infoPath, 'utf8'), before);
});

test('private retirement closes startup admission, joins the actual child and never recreates a removed directory', async () => {
const ownedStateDir = createOwnedReplayStateDir();
const paths = ownedStateDir.paths;
const args = registeredDaemonFixtureArgs(paths, fields);
const launch = launchDaemonProcess({ paths, args, serverMode: 'socket', ownedStateDir });
let contender: ReturnType<typeof launchDaemonProcess> | undefined;
try {
assert.ok(launch.startTime);
await waitForFixtureFile(paths.infoPath);
contender = launchDaemonProcess({ paths, args, serverMode: 'socket', ownedStateDir });
assert.equal((await contender.exited).exitCode, DAEMON_STARTUP_EXIT_CODES.busy);
const input = {
paths,
observed: { pid: launch.pid, startTime: launch.startTime },
mode: 'graceful' as const,
ownedStateDir,
};
const pending = stopAndRetireDaemon(input);
assert.throws(
() => launchDaemonProcess({ paths, args, serverMode: 'socket', ownedStateDir }),
(error: { details?: { reason?: string } }) =>
error.details?.reason === 'daemon_startup_admission_closed',
);
const result = await pending;
assert.equal(result.status, 'retired', JSON.stringify(result));
if (result.status !== 'retired') assert.fail('retirement not confirmed');
assert.equal(result.removedStateDir, true);
assert.equal((await launch.exited).exitCode, 0);
assert.equal(fs.existsSync(paths.baseDir), false);
assert.deepEqual(await stopAndRetireDaemon(input), result);
assert.equal(fs.existsSync(paths.baseDir), false);
} finally {
await finishPrivateTestDaemons(paths, launch, contender);
}
});

test('private retirement retains the directory while an earlier actual startup child is still paused', async () => {
const ownedStateDir = createOwnedReplayStateDir();
const paths = ownedStateDir.paths;
const args = registeredDaemonFixtureArgs(paths, fields);
const entry = args[1]!;
const ready = `${paths.baseDir}/paused-startup.ready`;
fs.writeFileSync(
entry,
`import fs from 'node:fs'; fs.writeFileSync(${JSON.stringify(ready)}, 'ready'); setInterval(() => {}, 1000);`,
);
const first = launchDaemonProcess({ paths, args, serverMode: 'socket', ownedStateDir });
let second: ReturnType<typeof launchDaemonProcess> | undefined;
try {
await waitForFixtureFile(ready);
second = launchDaemonProcess({
paths,
args: registeredDaemonFixtureArgs(paths, fields),
serverMode: 'socket',
ownedStateDir,
});
await waitForFixtureFile(paths.infoPath);
const result = await stopAndRetireDaemon({
paths,
observed: { pid: second.pid, startTime: second.startTime ?? null },
mode: 'graceful',
ownedStateDir,
startupJoinTimeoutMs: 0,
});
assert.equal(result.status, 'retained', JSON.stringify(result));
if (result.status !== 'retained') assert.fail('private state unexpectedly retired');
assert.equal(result.reason, 'startup-unconfirmed');
assert.equal(result.termination?.status, 'exited');
assert.equal(fs.existsSync(paths.baseDir), true);
assert.equal(isProcessAlive(first.pid), true);
assert.equal((await second.exited).exitCode, 0);
} finally {
await finishPrivateTestDaemons(paths, first, second);
}
});

for (const contents of [
'{broken',
'{"owner":"default","expiresAt":1e400}',
'{"owner":"default","expiresAt":-1e400}',
...[false, null, 0, {}].map((commitFailure) =>
JSON.stringify({ owner: 'default', expiresAt: Date.now() + 60_000, commitFailure }),
),
]) {
test(`malformed repair evidence (${contents}) retains private state after the actual child has exited`, async () => {
const ownedStateDir = createOwnedReplayStateDir();
const paths = ownedStateDir.paths;
const sessionDir = `${paths.sessionsDir}/default`;
fs.mkdirSync(sessionDir, { recursive: true });
const evidencePath = `${sessionDir}/repair-tombstone.json`;
fs.writeFileSync(evidencePath, contents);
const launch = launchDaemonProcess({
paths,
args: registeredDaemonFixtureArgs(paths, fields),
serverMode: 'socket',
ownedStateDir,
});
try {
await waitForFixtureFile(paths.infoPath);
const result = await stopAndRetireDaemon({
paths,
observed: { pid: launch.pid, startTime: launch.startTime ?? null },
mode: 'graceful',
ownedStateDir,
});
assert.equal(result.status, 'retained', JSON.stringify(result));
if (result.status !== 'retained') assert.fail('repair evidence unexpectedly discarded');
assert.equal(result.termination?.status, 'exited');
assert.equal(result.error?.details?.reason, 'repair_evidence_invalid');
assert.equal(fs.readFileSync(evidencePath, 'utf8'), contents);
await launch.exited;
} finally {
await finishPrivateTestDaemons(paths, launch);
}
});
}

async function waitForFixtureFile(filePath: string): Promise<void> {
const deadline = Date.now() + 2_000;
while (!fs.existsSync(filePath) && Date.now() < deadline) await sleep(20);
assert.equal(fs.existsSync(filePath), true);
}

async function finishPrivateTestDaemons(
paths: DaemonPaths,
...launches: (ReturnType<typeof launchDaemonProcess> | undefined)[]
): Promise<void> {
for (const launch of launches) {
if (!launch) continue;
const termination = await stopDaemonProcess(
{ pid: launch.pid, startTime: launch.startTime ?? null },
{ mode: 'force', termTimeoutMs: 0, killTimeoutMs: 2_000 },
);
assert.notEqual(termination.status, 'retained', JSON.stringify(termination));
await launch.exited;
}
fs.rmSync(paths.baseDir, { recursive: true, force: true });
}
84 changes: 84 additions & 0 deletions src/__tests__/test-utils/registered-daemon-fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { vi } from 'vitest';
import type { runCmdDetachedMonitored } from '@agent-device/host-kit/command';
import { readProcessStartTime } from '@agent-device/host-kit/process';
import { stopDaemonProcess } from '../../daemon-process.ts';
import type { DaemonPaths } from '../../daemon-resolution.ts';
import type { DaemonRegistrationFields } from '../../daemon-registration-owner.ts';

const actualCommand = await vi.importActual<typeof import('@agent-device/host-kit/command')>(
'@agent-device/host-kit/command',
);
const children = new Map<
string,
{ launch: ReturnType<typeof runCmdDetachedMonitored>; startTime: string | null }
>();

/** A real registration owner, advertising the caller's HTTP fixture and joining before deletion. */
export function registeredDaemonFixtureArgs(
paths: DaemonPaths,
fields: DaemonRegistrationFields,
): string[] {
const entry = path.join(paths.baseDir, 'dist', 'src', 'internal', 'daemon.js');
fs.mkdirSync(path.dirname(entry), { recursive: true });
fs.writeFileSync(path.join(paths.baseDir, 'package.json'), '{"type":"module"}');
const registrationUrl = new URL('../../daemon-registration-owner.ts', import.meta.url).href;
fs.writeFileSync(
entry,
`import fs from 'node:fs';
import path from 'node:path';
import { tryAcquireDaemonRegistration } from ${JSON.stringify(registrationUrl)};
const paths = ${JSON.stringify(paths)};
const acquired = await tryAcquireDaemonRegistration(paths);
if (acquired.status !== 'acquired') process.exit(75);
process.on('SIGTERM', async () => {
const deferred = path.join(paths.baseDir, 'repair-on-shutdown.json');
if (fs.existsSync(deferred)) {
const dir = path.join(paths.sessionsDir, 'default');
fs.mkdirSync(dir, { recursive: true });
fs.copyFileSync(deferred, path.join(dir, 'repair-tombstone.json'));
}
await acquired.owner.finish();
process.exit(0);
});
acquired.owner.publish(${JSON.stringify(fields)});
setInterval(() => {}, 1000);
`,
);
return ['--experimental-strip-types', entry];
}

export function spawnRegisteredDaemonFixture(
paths: DaemonPaths,
fields: DaemonRegistrationFields,
options: Parameters<typeof runCmdDetachedMonitored>[2],
): ReturnType<typeof runCmdDetachedMonitored> {
const child = actualCommand.runCmdDetachedMonitored(
process.execPath,
registeredDaemonFixtureArgs(paths, fields),
options,
);
children.set(paths.baseDir, { launch: child, startTime: readProcessStartTime(child.pid) });
return child;
}

export async function finishRegisteredDaemonFixture(stateDir: string): Promise<void> {
const owned = children.get(stateDir);
if (owned) {
const child = owned.launch;
const termination = await stopDaemonProcess(
{ pid: child.pid, startTime: owned.startTime },
{ mode: 'force', termTimeoutMs: 0, killTimeoutMs: 2_000 },
);
assert.notEqual(termination.status, 'retained', JSON.stringify(termination));
await child.exited;
children.delete(stateDir);
}
fs.rmSync(stateDir, { recursive: true, force: true });
}

export async function finishRegisteredDaemonFixtures(): Promise<void> {
for (const stateDir of children.keys()) await finishRegisteredDaemonFixture(stateDir);
}
Loading
Loading