Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e747776
fix(channels): preserve external policy authority
apurvvkumaria Aug 24, 2026
07d6553
merge: update snapshot authority base
apurvvkumaria Aug 24, 2026
7e78d1b
merge: refresh snapshot authority base
apurvvkumaria Aug 24, 2026
6e36f4e
merge: update snapshot authority base
apurvvkumaria Aug 24, 2026
123c0ce
Merge branch 'codex/9833-snapshot-authority' into codex/9833-channel-…
cv Aug 24, 2026
e158c18
Merge branch 'codex/9833-snapshot-authority' into codex/9833-channel-…
cv Aug 24, 2026
adeed9d
Merge branch 'codex/9833-snapshot-authority' into codex/9833-channel-…
cv Aug 24, 2026
7333522
merge: refresh channel authority base
apurvvkumaria Aug 24, 2026
9491036
merge: reconcile channel authority updates
apurvvkumaria Aug 24, 2026
82ca2a8
Merge remote-tracking branch 'origin/codex/9833-snapshot-authority' i…
apurvvkumaria Aug 24, 2026
2c2f19c
Merge branch 'codex/9833-snapshot-authority' into codex/9833-channel-…
apurvvkumaria Aug 24, 2026
71be277
Merge branch 'codex/9833-snapshot-authority' into codex/9833-channel-…
apurvvkumaria Aug 24, 2026
b2826ab
merge: restack snapshot authority
apurvvkumaria Aug 25, 2026
b893810
merge: restack snapshot authority
apurvvkumaria Aug 25, 2026
1fe92d4
merge: restack snapshot registry ownership
apurvvkumaria Aug 25, 2026
1f0106f
fix(channels): contain failed authority transitions
senthilr-nv Aug 25, 2026
e8694a8
merge: sync parent into PR #10118
cv Aug 28, 2026
e174b81
Merge branch 'codex/9833-snapshot-authority' into codex/9833-channel-…
cv Aug 28, 2026
a3be3bf
Merge branch 'codex/9833-snapshot-authority' into codex/9833-channel-…
cv Aug 28, 2026
c7574ee
ci: retrigger PR review advisor
cv Aug 28, 2026
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
2 changes: 2 additions & 0 deletions src/lib/actions/sandbox/messaging-host-forward-lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export function ensureMessagingHostForwardAfterRebuild(
sandboxName: string,
plan: SandboxMessagingPlan | null | undefined,
note: (message: string) => void = console.log,
revalidatePolicyAuthority?: (operation: string) => void,
): boolean {
const forward = resolveMessagingHostForward(plan);
if (!forward) return true;
Expand All @@ -67,6 +68,7 @@ export function ensureMessagingHostForwardAfterRebuild(
name,
port,
label,
revalidatePolicyAuthority,
),
note,
});
Expand Down
77 changes: 42 additions & 35 deletions src/lib/actions/sandbox/policy-channel-agent-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import * as policy from "../../policy";
import * as registry from "../../state/registry";
import { addSandboxChannel, startSandboxChannel, stopSandboxChannel } from "./policy-channel";
import { policyChannelDependencies } from "./policy-channel-dependencies";
import * as policyAuthorityPreflight from "./policy-authority/preflight";

function agentFixture(name: string): defs.AgentDefinition {
return { name } as defs.AgentDefinition;
Expand Down Expand Up @@ -76,6 +77,9 @@ beforeEach(() => {
saveCredentialMock = vi.spyOn(store, "saveCredential").mockImplementation(() => undefined);
promptMock = vi.spyOn(store, "prompt").mockResolvedValue("");
rebuildMock = vi.spyOn(policyChannelDependencies, "rebuildSandbox").mockResolvedValue(undefined);
vi.spyOn(policyAuthorityPreflight, "preflightSandboxPolicyAuthority").mockReturnValue(
"nemoclaw-managed",
);
});

afterEach(() => {
Expand Down Expand Up @@ -155,39 +159,42 @@ describe("channel lifecycle agent gate", () => {
it.each([
["start", ["googlechat"], () => startSandboxChannel("da-test", { channel: "googlechat" })],
["stop", [], () => stopSandboxChannel("da-test", { channel: "googlechat" })],
])("rejects a stale channel during %s before reading channel state or mutating the sandbox", async (_verb, disabledChannels, run) => {
// googlechat now supports openclaw + hermes, so exercise the unsupported-pair
// lifecycle gate with a non-messaging custom agent (supported by no channel).
getSandboxMock.mockReturnValue({ name: "da-test", agent: "custom-agent" });
vi.spyOn(defs, "loadAgent").mockReturnValue(agentFixture("custom-agent"));
const configuredChannelsMock = vi
.spyOn(registry, "getConfiguredMessagingChannelsFromEntry")
.mockReturnValue(["googlechat"]);
const disabledChannelsMock = vi
.spyOn(registry, "getDisabledChannels")
.mockReturnValue(disabledChannels);

let caught: unknown;
try {
await run();
} catch (err) {
caught = err;
}

expect(exitCodeFromError(caught)).toBe(1);
const errorText = (errSpy.mock.calls as unknown[][])
.map((call) => call.map(String).join(" "))
.join("\n");
expect(errorText).toMatch(/Channel 'googlechat' does not support agent 'custom-agent'/);
expect(errorText).toMatch(/Channel-supported agents: openclaw, hermes/);
expect(errorText).toMatch(/Channels supported by agent 'custom-agent': \(none\)/);

expect(configuredChannelsMock).not.toHaveBeenCalled();
expect(disabledChannelsMock).not.toHaveBeenCalled();
expect(loadPresetForSandboxMock).not.toHaveBeenCalled();
expect(applyPresetMock).not.toHaveBeenCalled();
expect(updateSandboxMock).not.toHaveBeenCalled();
expect(rebuildMock).not.toHaveBeenCalled();
expect(runOpenshellMock).not.toHaveBeenCalled();
});
])(
"rejects a stale channel during %s before reading channel state or mutating the sandbox",
async (_verb, disabledChannels, run) => {
// googlechat now supports openclaw + hermes, so exercise the unsupported-pair
// lifecycle gate with a non-messaging custom agent (supported by no channel).
getSandboxMock.mockReturnValue({ name: "da-test", agent: "custom-agent" });
vi.spyOn(defs, "loadAgent").mockReturnValue(agentFixture("custom-agent"));
const configuredChannelsMock = vi
.spyOn(registry, "getConfiguredMessagingChannelsFromEntry")
.mockReturnValue(["googlechat"]);
const disabledChannelsMock = vi
.spyOn(registry, "getDisabledChannels")
.mockReturnValue(disabledChannels);

let caught: unknown;
try {
await run();
} catch (err) {
caught = err;
}

expect(exitCodeFromError(caught)).toBe(1);
const errorText = (errSpy.mock.calls as unknown[][])
.map((call) => call.map(String).join(" "))
.join("\n");
expect(errorText).toMatch(/Channel 'googlechat' does not support agent 'custom-agent'/);
expect(errorText).toMatch(/Channel-supported agents: openclaw, hermes/);
expect(errorText).toMatch(/Channels supported by agent 'custom-agent': \(none\)/);

expect(configuredChannelsMock).not.toHaveBeenCalled();
expect(disabledChannelsMock).not.toHaveBeenCalled();
expect(loadPresetForSandboxMock).not.toHaveBeenCalled();
expect(applyPresetMock).not.toHaveBeenCalled();
expect(updateSandboxMock).not.toHaveBeenCalled();
expect(rebuildMock).not.toHaveBeenCalled();
expect(runOpenshellMock).not.toHaveBeenCalled();
},
);
});
2 changes: 1 addition & 1 deletion src/lib/actions/sandbox/policy-channel-cleanup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// Cleanup-path regression: with the new strict `toMessagingAgentId` semantics,
// stale messaging state on a non-messaging agent must still be cleanable
// without raising MessagingAgentNotSupportedError. `channels remove` should
// strip the stored messaging plan from the registry, `channels pause/resume`
// strip the stored messaging plan from the registry, `channels stop/start`
// should fail closed (no throw, no plan mutation).

import { afterEach, beforeEach, describe, expect, it, type MockInstance, vi } from "vitest";
Expand Down
Loading
Loading