Skip to content

Commit a904bb6

Browse files
fix: close remaining idle lifecycle review gaps
Why: Recover PR #1 after the automated review loop stopped and resolve its four remaining actionable public-safety and deterministic-selection findings. Changed: Scan home paths in candidate names with redacted diagnostics, inspect every distribution entry including empty directories, normalize idle state-load failures into a public-safe BrokerError with a local cause, and fall back to host order unless both release timestamps are valid. Verification: npm run test:broker-core (228 passed); node --test client/test/public-surface.test.mjs client/test/install-distribution.test.mjs (27 passed); npm run verify:public-surface; git diff --check. Affected: broker-core/index.mjs, broker-core/test/broker-core.test.mjs, client/public-surface.mjs, client/test/public-surface.test.mjs, client/test/install-distribution.test.mjs, scripts/package_distribution.sh. Refs: #1 #1 (comment) #1 (comment) #1 (comment) #1 (comment) Session: task-sessions/20260813-pr1-recovery
1 parent e804785 commit a904bb6

6 files changed

Lines changed: 236 additions & 15 deletions

File tree

broker-core/index.mjs

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1716,12 +1716,13 @@ function sortCandidates(candidates) {
17161716
if (tierComparison !== 0) {
17171717
return tierComparison;
17181718
}
1719-
const releaseComparison = compareNullableTimestamps(
1720-
right.registryEntry.lastLeaseReleasedAt,
1721-
left.registryEntry.lastLeaseReleasedAt,
1722-
);
1723-
if (releaseComparison !== 0) {
1724-
return releaseComparison;
1719+
const leftReleasedAt = Date.parse(left.registryEntry.lastLeaseReleasedAt ?? "");
1720+
const rightReleasedAt = Date.parse(right.registryEntry.lastLeaseReleasedAt ?? "");
1721+
if (Number.isFinite(leftReleasedAt) && Number.isFinite(rightReleasedAt)) {
1722+
const releaseComparison = rightReleasedAt - leftReleasedAt;
1723+
if (releaseComparison !== 0) {
1724+
return releaseComparison;
1725+
}
17251726
}
17261727
return left.index - right.index;
17271728
});
@@ -4058,15 +4059,34 @@ export function disableIdlePolicyBroker(paths, options = {}) {
40584059
});
40594060
}
40604061

4062+
function loadPublicSafeIdleState(loader) {
4063+
try {
4064+
return loader();
4065+
} catch (error) {
4066+
if (error instanceof BrokerError) {
4067+
throw error;
4068+
}
4069+
const readError = new BrokerError("Idle broker state could not be read.", {
4070+
reasonCode: "internal-error",
4071+
});
4072+
Object.defineProperty(readError, "cause", {
4073+
configurable: true,
4074+
value: error,
4075+
writable: true,
4076+
});
4077+
throw readError;
4078+
}
4079+
}
4080+
40614081
export function idleStatusBroker(paths, options = {}) {
40624082
return withLeaseMutationLock(paths, () => {
40634083
const timestamp = nowIso(options.now);
4064-
const state = loadBrokerState(paths, {
4084+
const state = loadPublicSafeIdleState(() => loadBrokerState(paths, {
40654085
...stateLoadOptions(options, timestamp),
40664086
registryPersistenceDetails: {
40674087
command: "idle.status",
40684088
},
4069-
});
4089+
}));
40704090
return {
40714091
command: "idle.status",
40724092
ok: true,
@@ -4210,12 +4230,12 @@ export function reconcileIdleBroker(paths, options = {}) {
42104230
status: "not_configured",
42114231
};
42124232
}
4213-
const state = loadBrokerState(paths, {
4233+
const state = loadPublicSafeIdleState(() => loadBrokerState(paths, {
42144234
...stateLoadOptions(options, timestamp),
42154235
registryPersistenceDetails: {
42164236
command: "idle.reconcile",
42174237
},
4218-
});
4238+
}));
42194239
const candidates = idleEligibleCandidates(state, policy, timestamp);
42204240
return {
42214241
configured: true,
@@ -4239,7 +4259,8 @@ export function cleanupIdleBroker(paths, options = {}) {
42394259
if (options.apply !== true) {
42404260
return withLeaseMutationLock(paths, () => {
42414261
const timestamp = nowIso(options.now);
4242-
const state = readBrokerStateSnapshot(paths, stateLoadOptions(options, timestamp));
4262+
const state = loadPublicSafeIdleState(() =>
4263+
readBrokerStateSnapshot(paths, stateLoadOptions(options, timestamp)));
42434264
return idleCleanupPlan(state).publicPlan;
42444265
}, {
42454266
now: nowIso(options.now),
@@ -4257,12 +4278,12 @@ export function cleanupIdleBroker(paths, options = {}) {
42574278
}
42584279
return withLeaseMutationLock(paths, () => {
42594280
const timestamp = nowIso(options.now);
4260-
const state = loadBrokerState(paths, {
4281+
const state = loadPublicSafeIdleState(() => loadBrokerState(paths, {
42614282
...stateLoadOptions(options, timestamp),
42624283
registryPersistenceDetails: {
42634284
command: "idle.cleanup",
42644285
},
4265-
});
4286+
}));
42664287
const plan = idleCleanupPlan(state);
42674288
if (options.confirmPlanId !== plan.publicPlan.planId) {
42684289
throw new BrokerError("Idle cleanup confirmation is stale; rerun preview and confirm the current plan.", {

broker-core/test/broker-core.test.mjs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3828,6 +3828,29 @@ test("shutdown candidates prefer the most recently released compatible alias", (
38283828
assert.equal(selected.alias, second.alias);
38293829
});
38303830

3831+
test("candidate selection falls back to host order when either release timestamp is missing", () => {
3832+
const paths = makePaths();
3833+
writeBaseHostConfig(paths.hostConfigPath);
3834+
writeBaseProject(paths.projectFilePath);
3835+
const resolvedPaths = brokerPaths(paths);
3836+
initBroker(resolvedPaths, runtimeOptions(paths, { processExists: () => true }));
3837+
const registry = readJson(resolvedPaths.registryPath);
3838+
registry.aliases["ui-1"].lastLeaseReleasedAt = null;
3839+
registry.aliases["ui-2"].lastLeaseReleasedAt = "2026-01-01T00:00:00.000Z";
3840+
writeJson(resolvedPaths.registryPath, registry);
3841+
3842+
const selected = acquireLeaseBroker(resolvedPaths, {
3843+
actorId: "agent-host-order",
3844+
actorType: "agent",
3845+
ownerPid: process.pid,
3846+
processExists: (pid) => pid === process.pid,
3847+
purposeId: "agent-ui-session",
3848+
simctlAdapter: paths.simctl.adapter,
3849+
}).lease;
3850+
3851+
assert.equal(selected.alias, "ui-1");
3852+
});
3853+
38313854
test("idle policy is absent by default, strictly bounded, and stored outside project state", () => {
38323855
const paths = makePaths();
38333856
writeBaseHostConfig(paths.hostConfigPath);
@@ -3981,6 +4004,101 @@ test("idle policy read errors stay public-safe while preserving diagnostics", (t
39814004
}
39824005
});
39834006

4007+
test("idle state read errors stay public-safe across status, reconcile, and cleanup", (t) => {
4008+
const originalReadFileSync = fs.readFileSync;
4009+
t.after(() => {
4010+
fs.readFileSync = originalReadFileSync;
4011+
});
4012+
4013+
const scenarios = [
4014+
{
4015+
name: "host config during status",
4016+
prepare(paths, resolvedPaths) {
4017+
return {
4018+
operation: () => idleStatusBroker(resolvedPaths, runtimeOptions(paths, { processExists: () => true })),
4019+
targetPath: resolvedPaths.hostConfigPath,
4020+
};
4021+
},
4022+
},
4023+
{
4024+
name: "registry during status",
4025+
prepare(paths, resolvedPaths) {
4026+
return {
4027+
operation: () => idleStatusBroker(resolvedPaths, runtimeOptions(paths, { processExists: () => true })),
4028+
targetPath: resolvedPaths.registryPath,
4029+
};
4030+
},
4031+
},
4032+
{
4033+
name: "pin during reconciliation",
4034+
prepare(paths, resolvedPaths) {
4035+
enableIdlePolicyBroker(resolvedPaths, {
4036+
actorId: "operator",
4037+
actorType: "human",
4038+
graceSeconds: 60,
4039+
});
4040+
const pin = createPinBroker(resolvedPaths, runtimeOptions(paths, {
4041+
actorId: "operator",
4042+
actorType: "human",
4043+
alias: "ui-1",
4044+
processExists: () => true,
4045+
purposeId: "agent-ui-session",
4046+
})).pin;
4047+
return {
4048+
operation: () => reconcileIdleBroker(resolvedPaths, runtimeOptions(paths, { processExists: () => true })),
4049+
targetPath: path.join(resolvedPaths.pinsDir, `${pin.pinId}.json`),
4050+
};
4051+
},
4052+
},
4053+
{
4054+
name: "lease during cleanup preview",
4055+
prepare(paths, resolvedPaths) {
4056+
const lease = acquireLeaseBroker(resolvedPaths, runtimeOptions(paths, {
4057+
actorId: "agent-cleanup-read",
4058+
actorType: "agent",
4059+
ownerPid: process.pid,
4060+
processExists: (pid) => pid === process.pid,
4061+
purposeId: "agent-ui-session",
4062+
})).lease;
4063+
return {
4064+
operation: () => cleanupIdleBroker(resolvedPaths, runtimeOptions(paths, {
4065+
processExists: (pid) => pid === process.pid,
4066+
})),
4067+
targetPath: path.join(resolvedPaths.leasesDir, `${lease.leaseId}.json`),
4068+
};
4069+
},
4070+
},
4071+
];
4072+
4073+
for (const scenario of scenarios) {
4074+
fs.readFileSync = originalReadFileSync;
4075+
const paths = makePaths();
4076+
writeBaseHostConfig(paths.hostConfigPath);
4077+
writeBaseProject(paths.projectFilePath);
4078+
const resolvedPaths = brokerPaths(paths);
4079+
initBroker(resolvedPaths, runtimeOptions(paths, { processExists: () => true }));
4080+
const { operation, targetPath } = scenario.prepare(paths, resolvedPaths);
4081+
fs.readFileSync = (filePath, ...args) => {
4082+
if (filePath === targetPath) {
4083+
const error = new Error(`EACCES: permission denied, open '${targetPath}'`);
4084+
error.code = "EACCES";
4085+
throw error;
4086+
}
4087+
return originalReadFileSync(filePath, ...args);
4088+
};
4089+
4090+
assert.throws(operation, (error) => {
4091+
const serialized = JSON.stringify(error.payload);
4092+
return error instanceof BrokerError
4093+
&& error.payload?.reasonCode === "internal-error"
4094+
&& error.payload?.error === "Idle broker state could not be read."
4095+
&& serialized.includes(paths.root) === false
4096+
&& "stack" in error.payload === false
4097+
&& error.cause?.message.includes(targetPath);
4098+
}, scenario.name);
4099+
}
4100+
});
4101+
39844102
test("malformed idle policy shapes reject non-number grace durations", () => {
39854103
const paths = makePaths();
39864104
writeBaseHostConfig(paths.hostConfigPath);

client/public-surface.mjs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ export function scanPublicSurface({
223223

224224
const diagnosticPathFor = (normalizedRelativeFile) => {
225225
let diagnosticPath = normalizedRelativeFile;
226-
for (const rule of denylistRules) {
226+
for (const rule of [...builtInRules, ...denylistRules]) {
227227
diagnosticPath = diagnosticPath.split(rule.value).join("[redacted]");
228228
}
229229
return diagnosticPath;
@@ -261,6 +261,15 @@ export function scanPublicSurface({
261261

262262
const scanRelativePath = (normalizedRelativeFile) => {
263263
const diagnosticPath = diagnosticPathFor(normalizedRelativeFile);
264+
for (const rule of builtInRules) {
265+
if (normalizedRelativeFile.includes(rule.value)) {
266+
addIssue({
267+
line: 1,
268+
path: diagnosticPath,
269+
rule: rule.label,
270+
});
271+
}
272+
}
264273
for (const rule of denylistRules) {
265274
if (normalizedRelativeFile.includes(rule.value)) {
266275
addIssue({

client/test/install-distribution.test.mjs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,57 @@ test("package_distribution scans copied payload filenames containing newlines",
432432
assert.equal(result.stderr.includes(root), false);
433433
});
434434

435+
test("package_distribution scans empty copied payload directories", (t) => {
436+
const root = makeTempDir();
437+
const outputDir = path.join(root, "out");
438+
const fakePathDir = path.join(root, "fake-path");
439+
const fakeSecurityPath = path.join(fakePathDir, "security");
440+
const privateDirectoryRoot = path.resolve("client/.package-distribution-public-surface-empty-directory-test");
441+
const privateDirectoryPath = path.join(privateDirectoryRoot, ...root.split(path.sep).filter(Boolean));
442+
const appSource = path.resolve("DerivedData/SimulatorBrokerApp/Build/Products/Release/SimulatorBrokerApp.app");
443+
const createdAppSource = !fs.existsSync(appSource);
444+
445+
assert.equal(fs.existsSync(privateDirectoryRoot), false);
446+
fs.mkdirSync(fakePathDir, { recursive: true });
447+
fs.writeFileSync(fakeSecurityPath, [
448+
"#!/usr/bin/env bash",
449+
"printf ' 1) ABC \"Developer ID Application: Example (TEAMID)\"\\n'",
450+
"",
451+
].join("\n"));
452+
fs.chmodSync(fakeSecurityPath, 0o755);
453+
fs.mkdirSync(appSource, { recursive: true });
454+
fs.mkdirSync(privateDirectoryPath, { recursive: true });
455+
t.after(() => {
456+
fs.rmSync(privateDirectoryRoot, { force: true, recursive: true });
457+
if (createdAppSource) {
458+
fs.rmSync(path.resolve("DerivedData/SimulatorBrokerApp"), { force: true, recursive: true });
459+
}
460+
});
461+
462+
const result = spawnSync("bash", [
463+
path.resolve("scripts/package_distribution.sh"),
464+
"--skip-build",
465+
"--output-dir",
466+
outputDir,
467+
"--team-id",
468+
"TEAMID",
469+
"--signing-identity",
470+
"Developer ID Application: Example (TEAMID)",
471+
], {
472+
encoding: "utf8",
473+
env: {
474+
...process.env,
475+
HOME: root,
476+
PATH: `${fakePathDir}${path.delimiter}${process.env.PATH ?? ""}`,
477+
},
478+
});
479+
480+
assert.notEqual(result.status, 0);
481+
assert.match(result.stderr, /Distribution public surface verification failed/);
482+
assert.match(result.stderr, /\[local-home-path\]/);
483+
assert.equal(result.stderr.includes(root), false);
484+
});
485+
435486
test("package scripts reject path-like archive names before cleanup", () => {
436487
for (const scriptPath of ["scripts/package_local.sh", "scripts/package_distribution.sh"]) {
437488
const root = makeTempDir();

client/test/public-surface.test.mjs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,28 @@ test("public surface scan applies local denylist values to relative filenames wi
198198
assert.equal(JSON.stringify(report).includes(privateMarker), false);
199199
});
200200

201+
test("public surface scan applies the built-in home rule to relative filenames without echoing it", () => {
202+
const root = makeTempDir();
203+
const localHome = path.posix.join(path.posix.sep, "Users", "synthetic-operator");
204+
const relativeFile = `captures${localHome}/notes.md`;
205+
fs.mkdirSync(path.dirname(path.join(root, relativeFile)), { recursive: true });
206+
fs.writeFileSync(path.join(root, relativeFile), "public notes\n");
207+
208+
const report = scanPublicSurface({
209+
files: [relativeFile],
210+
homePath: localHome,
211+
root,
212+
});
213+
214+
assert.equal(report.ok, false);
215+
assert.deepEqual(report.issues, [{
216+
line: 1,
217+
path: "captures[redacted]/notes.md",
218+
rule: "local-home-path",
219+
}]);
220+
assert.equal(JSON.stringify(report).includes(localHome), false);
221+
});
222+
201223
test("public surface scan rejects tracked broker state artifacts and ignores binary content", () => {
202224
const root = makeTempDir();
203225
fs.mkdirSync(path.join(root, "fixtures"));

scripts/package_distribution.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ scan_distribution_public_surface() {
5959

6060
(
6161
cd "$bundle_root"
62-
find . \( -type f -o -type l \) -print0 | LC_ALL=C sort -z > "$files_path"
62+
find . -print0 | LC_ALL=C sort -z > "$files_path"
6363
)
6464

6565
node --input-type=module - "$repo_root" "$bundle_root" "$files_path" "$repo_root/.public-safety.local" <<'EOF'

0 commit comments

Comments
 (0)