feat: authorize workflow contract authoring

This commit is contained in:
lda
2026-08-14 17:43:14 +07:00 Verified
parent bc23a722c3
commit bd226a2fa7
20 changed files with 1506 additions and 14 deletions
@@ -48,14 +48,18 @@ const OperationNameSchema = v.union([
v.literal("workflow.capabilities.call"),
v.literal("workflow.draft_workspaces.list"),
v.literal("workflow.draft_workspaces.get"),
v.literal("workflow.draft_workspaces.inspect_authoring_contract"),
v.literal("workflow.draft_workspaces.create_empty"),
v.literal("workflow.draft_workspaces.create_from_capability"),
v.literal("workflow.draft_workspaces.add_step_from_capability"),
v.literal("workflow.draft_workspaces.update_capability_step"),
v.literal("workflow.draft_workspaces.set_route"),
v.literal("workflow.draft_workspaces.set_contract"),
v.literal("workflow.draft_workspaces.set_start"),
v.literal("workflow.draft_workspaces.set_step_input_bindings"),
v.literal("workflow.draft_workspaces.set_step_output_bindings"),
v.literal("workflow.draft_workspaces.validate"),
v.literal("workflow.draft_workspaces.set_workflow_output_bindings"),
v.literal("workflow.artifacts.list"),
v.literal("workflow.artifacts.inspect"),
v.literal("workflow.deployments.list"),
+38
View File
@@ -260,6 +260,10 @@ describe("POST /api/rpc", () => {
operation: "workflow.draft_workspaces.get",
params: { workspace_id: "draft-1" },
},
{
operation: "workflow.draft_workspaces.inspect_authoring_contract",
params: { workspace_id: "draft-1", revision: 1 },
},
{
operation: "workflow.draft_workspaces.create_empty",
params: { workspace_id: "draft-1", name: "draft.workflow" },
@@ -299,6 +303,14 @@ describe("POST /api/rpc", () => {
target: "__end__",
},
},
{
operation: "workflow.draft_workspaces.set_contract",
params: { workspace_id: "draft-1", revision: 1, outcomes: ["ok"] },
},
{
operation: "workflow.draft_workspaces.set_start",
params: { workspace_id: "draft-1", revision: 1, step_id: "echo" },
},
{
operation: "workflow.draft_workspaces.set_step_input_bindings",
params: {
@@ -321,6 +333,14 @@ describe("POST /api/rpc", () => {
operation: "workflow.draft_workspaces.validate",
params: { workspace_id: "draft-1" },
},
{
operation: "workflow.draft_workspaces.set_workflow_output_bindings",
params: {
workspace_id: "draft-1",
revision: 1,
bindings: [{ source: "result", target: "state.result" }],
},
},
{ operation: "workflow.runs.start", params: {} },
{ operation: "workflow.runs.resume", params: {} },
] as const)("authorizes the allowlisted operation $operation", async ({
@@ -467,6 +487,24 @@ describe("POST /api/rpc", () => {
expect(body.error.code).toBe("unknown_operation");
});
it.each(["workflow.draft_workspaces.patch", "workflow.draft_workspaces.replace_document"])(
"does not authorize adjacent generic operation %s",
async (operation) => {
const res = await app.request("/api/rpc", {
method: "POST",
headers: validConsoleHeaders,
body: JSON.stringify({
operation,
target: "http://127.0.0.1:8000/rpc",
}),
});
expect(res.status).toBe(400);
const body = await res.json();
expect(body.error.code).toBe("unknown_operation");
},
);
it("returns 400 for invalid JSON body", async () => {
const res = await app.request("/api/rpc", {
method: "POST",
@@ -15,14 +15,18 @@ describe("browser operation policy", () => {
"workflow.capabilities.inspect",
"workflow.draft_workspaces.list",
"workflow.draft_workspaces.get",
"workflow.draft_workspaces.inspect_authoring_contract",
"workflow.draft_workspaces.create_empty",
"workflow.draft_workspaces.create_from_capability",
"workflow.draft_workspaces.add_step_from_capability",
"workflow.draft_workspaces.update_capability_step",
"workflow.draft_workspaces.set_route",
"workflow.draft_workspaces.set_contract",
"workflow.draft_workspaces.set_start",
"workflow.draft_workspaces.set_step_input_bindings",
"workflow.draft_workspaces.set_step_output_bindings",
"workflow.draft_workspaces.validate",
"workflow.draft_workspaces.set_workflow_output_bindings",
"workflow.artifacts.list",
"workflow.artifacts.inspect",
"workflow.deployments.list",
@@ -41,6 +45,9 @@ describe("browser operation policy", () => {
expect(browserAllowedOperationNames).not.toContain(
"workflow.draft_workspaces.replace_document",
);
expect(browserAllowedOperationNames).not.toContain(
"workflow.draft_workspaces.patch",
);
expect(browserAllowedOperationNames).not.toContain(
"workflow.draft_workspaces.remove_step",
);
@@ -13,14 +13,18 @@ export const browserAllowedOperationNames = defineBrowserOperationNames([
"workflow.capabilities.inspect",
"workflow.draft_workspaces.list",
"workflow.draft_workspaces.get",
"workflow.draft_workspaces.inspect_authoring_contract",
"workflow.draft_workspaces.create_empty",
"workflow.draft_workspaces.create_from_capability",
"workflow.draft_workspaces.add_step_from_capability",
"workflow.draft_workspaces.update_capability_step",
"workflow.draft_workspaces.set_route",
"workflow.draft_workspaces.set_contract",
"workflow.draft_workspaces.set_start",
"workflow.draft_workspaces.set_step_input_bindings",
"workflow.draft_workspaces.set_step_output_bindings",
"workflow.draft_workspaces.validate",
"workflow.draft_workspaces.set_workflow_output_bindings",
"workflow.artifacts.list",
"workflow.artifacts.inspect",
"workflow.deployments.list",