Skip to content
46 changes: 46 additions & 0 deletions src/lib/onboard/managed-bootstrap/adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
type ManagedBootstrapAuthorityStore,
type ManagedBootstrapCompletionReceipt,
type ManagedBootstrapCreateReceipt,
type ManagedBootstrapDurablePreparationReceipt,
type ManagedBootstrapFinalizationReceipt,
type ManagedBootstrapHeldWorkloadHandle,
type ManagedBootstrapObservedSnapshot,
Expand All @@ -27,6 +28,8 @@ import {
prepareManagedBootstrapSequence,
recoverManagedBootstrapTransactions,
renderManagedBootstrapHeldCommand,
sameManagedBootstrapCompletionReceipt,
sameManagedBootstrapDurablePreparationReceipt,
} from "./adapter";

const IDENTITY = "1".repeat(64);
Expand All @@ -41,6 +44,10 @@ const ACTIVATED_HASH = createHash("sha256").update(ACTIVATED_SPEC_JSON, "utf8").
const RUNTIME_ID = "7".repeat(64);
const PREPARED_ID = "8".repeat(64);

function reverseKeys<T extends object>(value: T): T {
return Object.fromEntries(Object.entries(value).reverse()) as T;
}

function requestFor(agent: ManagedStartupAgent) {
return createManagedStartupRootApplyRequest({
agent,
Expand Down Expand Up @@ -354,6 +361,45 @@ async function captureFailure<T>(promise: Promise<T>) {
}

describe("managed bootstrap adapter contract", () => {
it("compares provider-neutral durable receipts by canonical value", () => {
const handle = handleFor(requestFor("hermes"));
const preparation: ManagedBootstrapDurablePreparationReceipt = {
schemaVersion: MANAGED_BOOTSTRAP_SCHEMA_VERSION,
sandbox: handle.sandbox,
bootstrapIdentity: handle.bootstrapIdentity,
authorityFingerprint: "a".repeat(64),
recordId: "mxc-durable-authority",
recordedAt: "2026-07-29T12:00:30.000Z",
};
const reorderedPreparation = reverseKeys({
...preparation,
sandbox: reverseKeys({ ...preparation.sandbox }),
});
expect(sameManagedBootstrapDurablePreparationReceipt(preparation, reorderedPreparation)).toBe(
true,
);
expect(
sameManagedBootstrapDurablePreparationReceipt(preparation, {
...reorderedPreparation,
recordId: "changed-authority",
}),
).toBe(false);

const completion = completionFor(requestFor("hermes"), handle);
const reorderedCompletion = reverseKeys({
...completion,
image: reverseKeys({ ...completion.image }),
sandbox: reverseKeys({ ...completion.sandbox }),
});
expect(sameManagedBootstrapCompletionReceipt(completion, reorderedCompletion)).toBe(true);
expect(
sameManagedBootstrapCompletionReceipt(completion, {
...reorderedCompletion,
transactionPending: false,
}),
).toBe(false);
});

it.each(
MANAGED_STARTUP_AGENTS,
)("prepares, durably records, and only then activates %s through a provider-neutral adapter", async (agent) => {
Expand Down
16 changes: 16 additions & 0 deletions src/lib/onboard/managed-bootstrap/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,22 @@ function canonicalJson(value: unknown): string {
.join(",")}}`;
}

/** Compare durable provider receipts by canonical value, independent of object key order. */
export function sameManagedBootstrapDurablePreparationReceipt(
left: ManagedBootstrapDurablePreparationReceipt,
right: ManagedBootstrapDurablePreparationReceipt,
): boolean {
return canonicalJson(left) === canonicalJson(right);
}

/** Compare completion receipts by canonical value, independent of object key order. */
export function sameManagedBootstrapCompletionReceipt(
left: ManagedBootstrapCompletionReceipt,
right: ManagedBootstrapCompletionReceipt,
): boolean {
return canonicalJson(left) === canonicalJson(right);
}

export function assertManagedBootstrapIdentity(value: string): void {
if (!SHA256_RE.test(value)) {
protocolFail("identity must be 32 random bytes encoded as lowercase hex");
Expand Down
239 changes: 238 additions & 1 deletion src/lib/onboard/managed-bootstrap/docker-journal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import {
DOCKER_MANAGED_BOOTSTRAP_FINALIZATION_SCHEMA_VERSION,
DOCKER_MANAGED_BOOTSTRAP_JOURNAL_DIRECTORY,
DOCKER_MANAGED_BOOTSTRAP_JOURNAL_SCHEMA_VERSION,
type DockerManagedBootstrapFinalizationContext,
type DockerManagedBootstrapFinalizationRecord,
type DockerManagedBootstrapJournal,
DockerManagedBootstrapLegacyRecordRequiresAgentError,
parseDockerManagedBootstrapFinalizationRecord,
parseDockerManagedBootstrapJournal,
serializeDockerManagedBootstrapFinalizationRecord,
Expand All @@ -22,6 +24,12 @@ import {

const roots: string[] = [];
const IDENTITY = "1".repeat(64);
const OTHER_IDENTITY = "0".repeat(64);

function reverseKeys<T extends object>(value: T): T {
return Object.fromEntries(Object.entries(value).reverse()) as T;
}

const journal = Object.freeze({
schemaVersion: DOCKER_MANAGED_BOOTSTRAP_JOURNAL_SCHEMA_VERSION,
phase: "staged",
Expand Down Expand Up @@ -99,6 +107,75 @@ const finalization = Object.freeze({
},
} satisfies DockerManagedBootstrapFinalizationRecord);

const finalizationContext = Object.freeze({
bootstrapIdentity: finalization.bootstrapIdentity,
providerId: finalization.providerId,
agent: finalization.agent,
sandbox: finalization.sandbox,
planFingerprint: finalization.planFingerprint,
profileFingerprint: finalization.profileFingerprint,
imageReference: finalization.imageReference,
} satisfies DockerManagedBootstrapFinalizationContext);

function legacyJournalV1() {
return Object.freeze({
schemaVersion: 1 as const,
phase: journal.phase,
bootstrapIdentity: journal.bootstrapIdentity,
sandbox: journal.sandbox,
profileFingerprint: journal.profileFingerprint,
imageReference: journal.imageReference,
runtimeImageContentId: journal.runtimeImageContentId,
originalRuntimeId: journal.originalRuntimeId,
replacementRuntimeId: journal.replacementRuntimeId,
originalName: journal.originalName,
replacementStagingName: journal.replacementStagingName,
backupName: journal.backupName,
originalSpecHash: journal.originalSpecHash,
replacementSpecHash: journal.replacementSpecHash,
});
}

function legacyJournalV2() {
return Object.freeze({
schemaVersion: 2 as const,
phase: journal.phase,
bootstrapIdentity: journal.bootstrapIdentity,
providerId: journal.providerId,
sandbox: journal.sandbox,
planFingerprint: journal.planFingerprint,
profileFingerprint: journal.profileFingerprint,
imageReference: journal.imageReference,
runtimeImageContentId: journal.runtimeImageContentId,
originalRuntimeId: journal.originalRuntimeId,
replacementRuntimeId: journal.replacementRuntimeId,
originalName: journal.originalName,
replacementStagingName: journal.replacementStagingName,
backupName: journal.backupName,
originalSpecHash: journal.originalSpecHash,
replacementSpecHash: journal.replacementSpecHash,
rollbackTargetRuntimeId: journal.rollbackTargetRuntimeId,
rollbackTargetSpecHash: journal.rollbackTargetSpecHash,
preparationReceipt: journal.preparationReceipt,
commitReceipt: journal.commitReceipt,
});
}

function legacyFinalizationV1() {
return Object.freeze({
schemaVersion: 1 as const,
phase: finalization.phase,
bootstrapIdentity: finalization.bootstrapIdentity,
providerId: finalization.providerId,
sandbox: finalization.sandbox,
planFingerprint: finalization.planFingerprint,
profileFingerprint: finalization.profileFingerprint,
imageReference: finalization.imageReference,
commitReceipt: finalization.commitReceipt,
cleanupReceipt: finalization.cleanupReceipt,
});
}

function readPinnedPrivateFile(target: string): { readonly mode: number; readonly text: string } {
const descriptor = fs.openSync(target, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
try {
Expand Down Expand Up @@ -273,7 +350,12 @@ describe("Docker managed bootstrap journal", () => {

const restarted = createFileDockerManagedBootstrapJournalStore(root);
expect(restarted.listUnfinished()).toEqual([completed]);
expect(restarted.recordCompletion(IDENTITY, finalization.commitReceipt)).toEqual(completed);
const reorderedReceipt = reverseKeys({
...finalization.commitReceipt,
image: reverseKeys({ ...finalization.commitReceipt.image }),
sandbox: reverseKeys({ ...finalization.commitReceipt.sandbox }),
});
expect(restarted.recordCompletion(IDENTITY, reorderedReceipt)).toEqual(completed);
expect(() =>
restarted.recordCompletion(IDENTITY, {
...finalization.commitReceipt,
Expand All @@ -282,6 +364,161 @@ describe("Docker managed bootstrap journal", () => {
).toThrow("completion receipt changed");
});

it("retains exact journal, decision, and finalization atomic leftovers during enumeration", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-docker-journal-"));
roots.push(root);
const store = createFileDockerManagedBootstrapJournalStore(root);
store.create(journal);
const directory = path.join(root, DOCKER_MANAGED_BOOTSTRAP_JOURNAL_DIRECTORY);
const leftovers = [
`.${IDENTITY}.json.123.a0.tmp`,
`.${IDENTITY}.json.decision.123.a0.tmp`,
`.${IDENTITY}.json.finalized.123.a0.tmp`,
];
for (const name of leftovers)
fs.writeFileSync(path.join(directory, name), "orphan\n", { mode: 0o600 });

expect(store.listUnfinished()).toEqual([journal]);
for (const name of leftovers) expect(fs.existsSync(path.join(directory, name))).toBe(true);
});

it.each([
`.${IDENTITY}.json.commit.123.a0.tmp`,
`.${IDENTITY}.json.decision.pid.a0.tmp`,
`.${IDENTITY}.json.finalized.123.A0.tmp`,
`${IDENTITY}.json.decision.123.a0.tmp`,
`.${IDENTITY}.json.decision.123.a0.tmp.extra`,
])("rejects and retains near-miss atomic entry %s", (name) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-docker-journal-"));
roots.push(root);
const store = createFileDockerManagedBootstrapJournalStore(root);
expect(store.listUnfinished()).toEqual([]);
const target = path.join(root, DOCKER_MANAGED_BOOTSTRAP_JOURNAL_DIRECTORY, name);
fs.writeFileSync(target, "near miss\n", { mode: 0o600 });

expect(() => store.listUnfinished()).toThrow("unsupported entry");
expect(fs.existsSync(target)).toBe(true);
});

it.each([
[1, legacyJournalV1],
[2, legacyJournalV2],
] as const)("fails typed and closed for exact legacy journal schema %i", (schemaVersion, legacy) => {
const serialized = `${JSON.stringify(legacy())}\n`;
let failure: unknown;
try {
parseDockerManagedBootstrapJournal(serialized);
} catch (error) {
failure = error;
}
expect(failure).toBeInstanceOf(DockerManagedBootstrapLegacyRecordRequiresAgentError);
expect(failure).toMatchObject({
bootstrapIdentity: IDENTITY,
recordKind: "journal",
schemaVersion,
});

const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-docker-journal-"));
roots.push(root);
const store = createFileDockerManagedBootstrapJournalStore(root);
expect(store.listUnfinished()).toEqual([]);
const target = path.join(root, DOCKER_MANAGED_BOOTSTRAP_JOURNAL_DIRECTORY, `${IDENTITY}.json`);
fs.writeFileSync(target, serialized, { mode: 0o600 });
expect(() => store.listUnfinished()).toThrowError(
DockerManagedBootstrapLegacyRecordRequiresAgentError,
);
expect(readPinnedPrivateFile(target).text).toBe(serialized);
});

it("does not classify a malformed legacy journal as upgradeable authority", () => {
const malformed = { ...legacyJournalV2(), agent: "hermes" };
expect(() => parseDockerManagedBootstrapJournal(`${JSON.stringify(malformed)}\n`)).toThrow(
"legacy journal schema is invalid",
);
try {
parseDockerManagedBootstrapJournal(`${JSON.stringify(malformed)}\n`);
} catch (error) {
expect(error).not.toBeInstanceOf(DockerManagedBootstrapLegacyRecordRequiresAgentError);
}
});

it("upgrades legacy finalization only with exact immutable transaction context", () => {
const serialized = `${JSON.stringify(legacyFinalizationV1())}\n`;
expect(() => parseDockerManagedBootstrapFinalizationRecord(serialized)).toThrowError(
DockerManagedBootstrapLegacyRecordRequiresAgentError,
);
expect(() =>
parseDockerManagedBootstrapFinalizationRecord(serialized, {
...finalizationContext,
planFingerprint: "0".repeat(64),
}),
).toThrowError(DockerManagedBootstrapLegacyRecordRequiresAgentError);
expect(parseDockerManagedBootstrapFinalizationRecord(serialized, finalizationContext)).toEqual(
finalization,
);

const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-docker-journal-"));
roots.push(root);
const store = createFileDockerManagedBootstrapJournalStore(root);
expect(store.listUnfinished()).toEqual([]);
const target = path.join(
root,
DOCKER_MANAGED_BOOTSTRAP_JOURNAL_DIRECTORY,
`${IDENTITY}.json.finalized`,
);
fs.writeFileSync(target, serialized, { mode: 0o600 });
expect(() => store.loadFinalization(IDENTITY)).toThrowError(
DockerManagedBootstrapLegacyRecordRequiresAgentError,
);
expect(() => store.recordFinalization(finalization)).toThrowError(
DockerManagedBootstrapLegacyRecordRequiresAgentError,
);
expect(readPinnedPrivateFile(target).text).toBe(serialized);

store.recordFinalization(finalization, finalizationContext);
expect(store.loadFinalization(IDENTITY)).toEqual(finalization);
expect(readPinnedPrivateFile(target).text).toBe(
serializeDockerManagedBootstrapFinalizationRecord(finalization),
);
});

it("rejects a current finalization that contradicts supplied durable context", () => {
const wrongAgent = Object.freeze({ ...finalization, agent: "openclaw" as const });
expect(() =>
parseDockerManagedBootstrapFinalizationRecord(
serializeDockerManagedBootstrapFinalizationRecord(wrongAgent),
finalizationContext,
),
).toThrow("does not match supplied durable context");
});

it("rejects current journal and finalization records stored under another identity", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-docker-journal-"));
roots.push(root);
const store = createFileDockerManagedBootstrapJournalStore(root);
expect(store.listUnfinished()).toEqual([]);
const directory = path.join(root, DOCKER_MANAGED_BOOTSTRAP_JOURNAL_DIRECTORY);
const misplacedJournal = path.join(directory, `${OTHER_IDENTITY}.json`);
const misplacedFinalization = `${misplacedJournal}.finalized`;
fs.writeFileSync(misplacedJournal, serializeDockerManagedBootstrapJournal(journal), {
mode: 0o600,
});
fs.writeFileSync(
misplacedFinalization,
serializeDockerManagedBootstrapFinalizationRecord(finalization),
{ mode: 0o600 },
);

expect(() => store.load(OTHER_IDENTITY)).toThrow(
"journal bootstrap identity does not match its file name",
);
expect(() => store.loadFinalization(OTHER_IDENTITY)).toThrow(
"finalization bootstrap identity does not match its file name",
);
expect(fs.existsSync(misplacedJournal)).toBe(true);
expect(fs.existsSync(misplacedFinalization)).toBe(true);
});

it("fails closed when enumeration encounters an unsupported state entry", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-docker-journal-"));
roots.push(root);
Expand Down
Loading
Loading