Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
dfa9376
fix(policy): enforce external policy authority
apurvvkumaria Aug 24, 2026
ad1c8f7
test(policy): model managed authority preflight
apurvvkumaria Aug 24, 2026
2b18b39
merge: sync main
apurvvkumaria Aug 24, 2026
58d9477
fix(policy): fail closed on empty authority
apurvvkumaria Aug 24, 2026
864145b
fix(policy): guard blueprint and Shields mutations
apurvvkumaria Aug 24, 2026
eae0cc1
test(blueprint): model policy authority preflight
apurvvkumaria Aug 24, 2026
192c23f
fix(policy): retain incomplete blueprint transitions
apurvvkumaria Aug 24, 2026
b06fe62
test(blueprint): keep policy mutations visible
apurvvkumaria Aug 24, 2026
915db84
test(policy): cover authority refusal states
apurvvkumaria Aug 24, 2026
c7bd1f0
fix(shields): report external policy recovery
apurvvkumaria Aug 24, 2026
5078283
fix(policy): reconcile interrupted mutations
apurvvkumaria Aug 24, 2026
548f8fd
test(blueprint): cover policy reconciliation refusals
apurvvkumaria Aug 24, 2026
1246705
Merge branch 'main' into codex/9833-policy-authority-foundation
cv Aug 24, 2026
e4f9bcd
Merge branch 'main' into codex/9833-policy-authority-foundation
cv Aug 24, 2026
6976407
Merge branch 'main' into codex/9833-policy-authority-foundation
cv Aug 24, 2026
c7743a5
Merge branch 'main' into codex/9833-policy-authority-foundation
cv Aug 24, 2026
2729d14
Merge branch 'main' into codex/9833-policy-authority-foundation
cv Aug 24, 2026
6f71bdb
fix(policy): close authority review gaps
apurvvkumaria Aug 24, 2026
6cf16bb
test(e2e): retain blueprint policy call evidence
apurvvkumaria Aug 24, 2026
2e017cc
merge: refresh policy authority foundation from main
apurvvkumaria Aug 24, 2026
5379892
test(e2e): retain blueprint policy call trace
apurvvkumaria Aug 24, 2026
ae0c6e9
test(ci): rebalance policy authority coverage shard
apurvvkumaria Aug 24, 2026
a8ad43e
Merge remote-tracking branch 'origin/main' into codex/9833-policy-aut…
apurvvkumaria Aug 24, 2026
977c91c
test(ci): rebalance policy coverage shards
apurvvkumaria Aug 24, 2026
173e584
fix(policy): close authority recovery review gaps
apurvvkumaria Aug 24, 2026
1d06701
Merge branch 'main' into codex/9833-policy-authority-foundation
cv Aug 24, 2026
90544c5
Merge branch 'main' into codex/9833-policy-authority-foundation
cv Aug 24, 2026
a0fa36d
fix(policy): preserve external Shields recovery
apurvvkumaria Aug 24, 2026
b42ce40
Merge remote-tracking branch 'origin/main' into codex/9833-policy-aut…
apurvvkumaria Aug 24, 2026
ceae05e
fix(shields): retain locked policy recovery
apurvvkumaria Aug 25, 2026
c9205ff
fix(policy): preserve recovery receipts
apurvvkumaria Aug 25, 2026
c9f39a8
merge: resolve conflicts with main
github-actions[bot] Aug 25, 2026
4916a4f
merge: resolve conflicts with main
github-actions[bot] Aug 25, 2026
8f352cd
merge: resolve conflicts with main
github-actions[bot] Aug 25, 2026
bd34fbf
Merge branch 'main' into codex/9833-policy-authority-foundation
cv Aug 25, 2026
1ffcc22
merge: refresh policy authority foundation from main
apurvvkumaria Aug 25, 2026
ddc203d
fix(policy): close Shields recovery review gaps
apurvvkumaria Aug 25, 2026
ce946c5
Merge branch 'main' into codex/9833-policy-authority-foundation
cv Aug 25, 2026
2d7f290
fix(policy): retire recovery artifacts atomically
apurvvkumaria Aug 25, 2026
2a8b9bb
fix(policy): preserve recovery binding on cleanup failure
senthilr-nv Aug 25, 2026
fb05bda
fix(policy): restore recovery handoff on state failure
senthilr-nv Aug 25, 2026
224573f
fix(policy): remove redundant recovery condition
senthilr-nv Aug 25, 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: 1 addition & 1 deletion ci/source-architecture-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"src/lib/inference/vllm.ts": 21,
"src/lib/onboard.ts": 201,
"src/lib/onboard/machine/handlers/sandbox.ts": 21,
"src/lib/policy/index.ts": 22,
"src/lib/policy/index.ts": 23,
"src/lib/sandbox/config.ts": 22,
"src/lib/shields/index.ts": 25
}
Expand Down
117 changes: 65 additions & 52 deletions nemoclaw/src/blueprint/runner-identity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
MATCHING_INFERENCE_ROUTE_LISTING,
MATCHING_RUNTIME_PROVIDER_LISTING,
providersV2EnabledResult,
resultWithBlueprintPolicyAuthority,
successResult,
} from "./runner-test-fixtures.js";

Expand All @@ -36,8 +37,10 @@ vi.mock("node:fs", async (importOriginal) => {
const memory = inMemoryFsMethods(store, { realpaths, spy: vi.fn });
return {
...original,
existsSync: memory.existsSync,
mkdirSync: memory.mkdirSync,
readFileSync: memory.readFileSync,
renameSync: memory.renameSync,
writeFileSync: memory.writeFileSync,
readdirSync: memory.readdirSync,
realpathSync: memory.realpathSync,
Expand All @@ -53,9 +56,8 @@ vi.mock("./ssrf.js", async (importOriginal) => {
};
});

const { actionApply, actionPlan, actionRollback, actionStatus, loadBlueprint } = await import(
"./runner.js"
);
const { actionApply, actionPlan, actionRollback, actionStatus, loadBlueprint } =
await import("./runner.js");

const matchingProvider = MATCHING_RUNTIME_PROVIDER_LISTING;
const matchingInferenceProvider = MATCHING_INFERENCE_PROVIDER_LISTING;
Expand All @@ -82,10 +84,27 @@ function responseQueue(
]);
mockExeca.mockImplementation(async (_command: string, args: string[]) => {
const command = args.join(" ");
return responses.get(command)?.shift() ?? fallbacks.get(command) ?? success;
const fallback = responses.get(command)?.shift() ?? fallbacks.get(command) ?? success;
return fallback.exitCode === undefined
? fallback
: resultWithBlueprintPolicyAuthority(args, {
...fallback,
exitCode: fallback.exitCode ?? 1,
});
});
}

function nonAuthorityCommandLines(): string[] {
const authorityCommands = new Set([
"openshell status",
"openshell policy list -g test-gateway --global --limit 1",
"openshell policy get -g test-gateway --full --output json test-sandbox",
]);
return mockExeca.mock.calls
.map(([command, args]) => [command, ...(args ?? [])].join(" "))
.filter((command) => !authorityCommands.has(command));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function blueprint(overrides: Record<string, unknown> = {}): Parameters<typeof actionApply>[1] {
return {
components: {
Expand Down Expand Up @@ -123,7 +142,10 @@ describe("blueprint identity wrapper", () => {
realpaths.clear();
vi.clearAllMocks();
mockExeca.mockImplementation(async (_command: string, args: string[]) =>
args.join(" ") === "settings get --global --json" ? providersV2Enabled : success,
resultWithBlueprintPolicyAuthority(
args,
args.join(" ") === "settings get --global --json" ? providersV2Enabled : success,
),
);
process.env.NEMOCLAW_BLUEPRINT_PATH = "/blueprint";
store.set("/blueprint", { type: "dir" });
Expand Down Expand Up @@ -294,9 +316,7 @@ describe("blueprint identity wrapper", () => {
"provider refresh configure acme-okta-runtime --credential-key OKTA_ACCESS_TOKEN --strategy oauth2-refresh-token --material client_id=client-id --secret-material-env refresh_token=OKTA_REFRESH_TOKEN --secret-material-env client_secret=OKTA_CLIENT_SECRET",
),
).toBeLessThan(commands.indexOf("sandbox provider attach test-sandbox acme-okta-runtime"));
expect(
commands.indexOf("sandbox provider attach test-sandbox acme-okta-runtime"),
).toBeLessThan(
expect(commands.indexOf("sandbox provider attach test-sandbox acme-okta-runtime")).toBeLessThan(
commands.indexOf(
"provider refresh rotate acme-okta-runtime --credential-key OKTA_ACCESS_TOKEN",
),
Expand Down Expand Up @@ -337,9 +357,7 @@ describe("blueprint identity wrapper", () => {
/Failed to inspect sandbox 'test-sandbox'.*gateway configuration not found/,
);

const commandLines = mockExeca.mock.calls.map(([command, args]) =>
[command, ...(args ?? [])].join(" "),
);
const commandLines = nonAuthorityCommandLines();
expect(commandLines).toEqual(["openshell sandbox get test-sandbox"]);
});

Expand All @@ -358,9 +376,7 @@ describe("blueprint identity wrapper", () => {
/Sandbox 'test-sandbox' is not reusable.*Ready phase.*Provisioning/,
);

const commandLines = mockExeca.mock.calls.map(([command, args]) =>
[command, ...(args ?? [])].join(" "),
);
const commandLines = nonAuthorityCommandLines();
expect(commandLines).toEqual(["openshell sandbox get test-sandbox"]);
});

Expand All @@ -375,38 +391,41 @@ describe("blueprint identity wrapper", () => {
{ exitCode: 0, stdout: "Name: test-sandbox\nPhase: Provisioning", stderr: "" },
/Sandbox 'test-sandbox' is not reusable.*Ready phase.*Provisioning/,
],
])("fails closed when a concurrently created sandbox %s", async (_label, racedSandbox, expectedError) => {
process.env.OKTA_CLIENT_ID = "client-id";
process.env.OKTA_REFRESH_TOKEN = "refresh-secret";
process.env.OKTA_CLIENT_SECRET = "client-secret";
responseQueue([
["sandbox get test-sandbox", [failureResult("sandbox not found"), racedSandbox]],
[
"provider get acme-okta-runtime",
])(
"fails closed when a concurrently created sandbox %s",
async (_label, racedSandbox, expectedError) => {
process.env.OKTA_CLIENT_ID = "client-id";
process.env.OKTA_REFRESH_TOKEN = "refresh-secret";
process.env.OKTA_CLIENT_SECRET = "client-secret";
responseQueue([
["sandbox get test-sandbox", [failureResult("sandbox not found"), racedSandbox]],
[
failureResult("provider not found"),
...Array.from({ length: 4 }, () => ({
exitCode: 0,
stdout: matchingProvider,
stderr: "",
})),
"provider get acme-okta-runtime",
[
failureResult("provider not found"),
...Array.from({ length: 4 }, () => ({
exitCode: 0,
stdout: matchingProvider,
stderr: "",
})),
],
],
],
[
"sandbox create --from openclaw --name test-sandbox --forward 18789",
[failureResult("sandbox already exists")],
],
]);
[
"sandbox create --from openclaw --name test-sandbox --forward 18789",
[failureResult("sandbox already exists")],
],
]);

await expect(actionApply("default", blueprint({ identity: oktaIdentity() }))).rejects.toThrow(
expectedError,
);
await expect(actionApply("default", blueprint({ identity: oktaIdentity() }))).rejects.toThrow(
expectedError,
);

const commands = mockExeca.mock.calls.map(([, args]) => (args ?? []).join(" "));
expect(commands.filter((command) => command === "sandbox get test-sandbox")).toHaveLength(2);
expect(commands).not.toContain("sandbox provider attach test-sandbox acme-okta-runtime");
expect(commands).toContain("provider delete acme-okta-runtime");
});
const commands = mockExeca.mock.calls.map(([, args]) => (args ?? []).join(" "));
expect(commands.filter((command) => command === "sandbox get test-sandbox")).toHaveLength(2);
expect(commands).not.toContain("sandbox provider attach test-sandbox acme-okta-runtime");
expect(commands).toContain("provider delete acme-okta-runtime");
},
);

it("fails before identity mutation when a reused sandbox's inference provider cannot be inspected", async () => {
process.env.OKTA_CLIENT_ID = "client-id";
Expand All @@ -424,9 +443,7 @@ describe("blueprint identity wrapper", () => {
/Failed to inspect inference provider 'test-provider'.*gateway configuration not found/,
);

const commandLines = mockExeca.mock.calls.map(([command, args]) =>
[command, ...(args ?? [])].join(" "),
);
const commandLines = nonAuthorityCommandLines();
expect(commandLines).toEqual([
"openshell sandbox get test-sandbox",
"openshell provider get test-provider",
Expand Down Expand Up @@ -454,9 +471,7 @@ describe("blueprint identity wrapper", () => {
/Inference provider 'test-provider' does not match the requested non-secret binding/,
);

const commandLines = mockExeca.mock.calls.map(([command, args]) =>
[command, ...(args ?? [])].join(" "),
);
const commandLines = nonAuthorityCommandLines();
expect(commandLines).toEqual([
"openshell sandbox get test-sandbox",
"openshell provider get test-provider",
Expand All @@ -483,9 +498,7 @@ describe("blueprint identity wrapper", () => {
/Failed to inspect the active inference route.*gateway route inspection unavailable/,
);

const commandLines = mockExeca.mock.calls.map(([command, args]) =>
[command, ...(args ?? [])].join(" "),
);
const commandLines = nonAuthorityCommandLines();
expect(commandLines).toEqual([
"openshell sandbox get test-sandbox",
"openshell provider get test-provider",
Expand Down Expand Up @@ -786,7 +799,7 @@ describe("blueprint identity wrapper", () => {
{ exitCode: 0, stdout: matchingProvider, stderr: "" },
],
],
["policy get --base test-sandbox", [failureResult("policy read rejected")]],
["policy get -g test-gateway --base test-sandbox", [failureResult("policy read rejected")]],
]);

await expect(
Expand Down
6 changes: 6 additions & 0 deletions nemoclaw/src/blueprint/runner-mock-fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ export function inMemoryFsMethods(store: Map<string, RunnerFsEntry>, options?: I
writeFileSync: spy((p: string, data: string) => {
store.set(p, { type: "file", content: String(data) });
}),
renameSync: spy((source: string, destination: string) => {
const entry = store.get(source);
if (!entry) return missingEntry(source);
store.set(destination, entry);
store.delete(source);
}),
readdirSync: (p: string) => {
const prefix = p.endsWith("/") ? p : `${p}/`;
const entries = new Set(
Expand Down
11 changes: 9 additions & 2 deletions nemoclaw/src/blueprint/runner-name-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import {
inMemoryFsMethods,
resolvedEndpointFor,
} from "./runner-mock-fixtures.js";
import { minimalBlueprint, successResult } from "./runner-test-fixtures.js";
import {
minimalBlueprint,
resultWithBlueprintPolicyAuthority,
successResult,
} from "./runner-test-fixtures.js";

const { store, addFile, addDir } = createRunnerFsStore();

Expand All @@ -36,6 +40,7 @@ vi.mock("node:fs", async (importOriginal) => {
existsSync: memory.existsSync,
mkdirSync: memory.mkdirSync,
readFileSync: memory.readFileSync,
renameSync: memory.renameSync,
writeFileSync: memory.writeFileSync,
readdirSync: memory.readdirSync,
};
Expand Down Expand Up @@ -114,7 +119,9 @@ describe("blueprint name validation (fail-closed integration)", () => {
stdout.reset();
vi.clearAllMocks();
vi.spyOn(process.stdout, "write").mockImplementation(stdout.write);
mockExeca.mockResolvedValue(successResult());
mockExeca.mockImplementation(async (_command: string, args: string[]) =>
resultWithBlueprintPolicyAuthority(args, successResult()),
);
});

afterEach(() => {
Expand Down
Loading
Loading