Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions apps/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,18 @@ export type {
ProposeWorkflowPatchInput,
WorkflowPatchProposal,
} from "./patches/patch-schema.js";
export {
DEFAULT_AGENT_PERMISSION_GRANTS,
PERMISSION_NAMES,
createDefaultPermissionPolicy,
} from "./security/permission-policy.js";
export type {
CommandPermissionEvaluationRequest,
CreateDefaultPermissionPolicyOptions,
PermissionDecision,
PermissionEvaluationRequest,
PermissionEvaluationResult,
PermissionGrants,
PermissionName,
PermissionPolicy,
} from "./security/permission-policy.js";
91 changes: 91 additions & 0 deletions apps/server/src/security/permission-policy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { describe, expect, it } from "vitest";

import {
createDefaultPermissionPolicy,
DEFAULT_AGENT_PERMISSION_GRANTS,
} from "./permission-policy.js";

describe("createDefaultPermissionPolicy", () => {
it("allows read-only behavior for the default agent policy", () => {
const policy = createDefaultPermissionPolicy();

expect(DEFAULT_AGENT_PERMISSION_GRANTS).toEqual({
read: true,
write: false,
safeCommands: true,
arbitraryCommands: false,
network: false,
installDependencies: false,
gitCommit: false,
gitPush: false,
});
expect(policy.evaluate({ permission: "read" })).toMatchObject({
decision: "allow",
requiresApproval: false,
});
});

it("rejects writes, installs, arbitrary commands, commits, pushes, and network by default", () => {
const policy = createDefaultPermissionPolicy();

for (const permission of [
"write",
"arbitraryCommands",
"network",
"installDependencies",
"gitCommit",
"gitPush",
] as const) {
expect(policy.evaluate({ permission })).toMatchObject({
permission,
decision: "deny",
requiresApproval: true,
});
}
});

it("allows safe commands and denies unsafe commands without arbitrary command approval", () => {
const policy = createDefaultPermissionPolicy();

expect(
policy.evaluateCommand({
command: "git status --short",
safe: true,
}),
).toMatchObject({
permission: "safeCommands",
decision: "allow",
requiresApproval: false,
});

expect(
policy.evaluateCommand({
command: "rm -rf dist",
safe: false,
}),
).toMatchObject({
permission: "arbitraryCommands",
decision: "deny",
requiresApproval: true,
reason: 'Permission "arbitraryCommands" is not granted by the active policy.',
});
});

it("allows dangerous permissions when explicitly granted", () => {
const policy = createDefaultPermissionPolicy({
grants: {
write: true,
network: true,
},
});

expect(policy.evaluate({ permission: "write" })).toMatchObject({
decision: "allow",
requiresApproval: false,
});
expect(policy.evaluate({ permission: "network" })).toMatchObject({
decision: "allow",
requiresApproval: false,
});
});
});
105 changes: 105 additions & 0 deletions apps/server/src/security/permission-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
export const PERMISSION_NAMES = [
"read",
"write",
"safeCommands",
"arbitraryCommands",
"network",
"installDependencies",
"gitCommit",
"gitPush",
] as const;

export type PermissionName = (typeof PERMISSION_NAMES)[number];
export type PermissionDecision = "allow" | "deny";

export type PermissionGrants = Readonly<Record<PermissionName, boolean>>;

export interface PermissionEvaluationRequest {
readonly permission: PermissionName;
}

export interface CommandPermissionEvaluationRequest {
readonly command: string;
readonly safe: boolean;
}

export interface PermissionEvaluationResult {
readonly permission: PermissionName;
readonly decision: PermissionDecision;
readonly requiresApproval: boolean;
readonly reason: string;
}

export interface PermissionPolicy {
readonly grants: PermissionGrants;
readonly evaluate: (request: PermissionEvaluationRequest) => PermissionEvaluationResult;
readonly evaluateCommand: (
request: CommandPermissionEvaluationRequest,
) => PermissionEvaluationResult;
}

export interface CreateDefaultPermissionPolicyOptions {
readonly grants?: Partial<PermissionGrants>;
}

export const DEFAULT_AGENT_PERMISSION_GRANTS: PermissionGrants = {
read: true,
write: false,
safeCommands: true,
arbitraryCommands: false,
network: false,
installDependencies: false,
gitCommit: false,
gitPush: false,
};

const DANGEROUS_PERMISSIONS = new Set<PermissionName>([
"write",
"arbitraryCommands",
"network",
"installDependencies",
"gitCommit",
"gitPush",
]);

export function createDefaultPermissionPolicy(
options: CreateDefaultPermissionPolicyOptions = {},
): PermissionPolicy {
const grants = {
...DEFAULT_AGENT_PERMISSION_GRANTS,
...options.grants,
};

return {
grants,
evaluate(request) {
return evaluatePermission(grants, request.permission);
},
evaluateCommand(request) {
return evaluatePermission(grants, request.safe ? "safeCommands" : "arbitraryCommands");
},
};
}

function evaluatePermission(
grants: PermissionGrants,
permission: PermissionName,
): PermissionEvaluationResult {
const granted = grants[permission];

if (granted) {
return {
permission,
decision: "allow",
requiresApproval: false,
reason: `Permission "${permission}" is granted by the active policy.`,
};
}

return {
permission,
decision: "deny",
requiresApproval: DANGEROUS_PERMISSIONS.has(permission),
reason: `Permission "${permission}" is not granted by the active policy.`,
};
}
6 changes: 3 additions & 3 deletions docs/development/task-backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -511,9 +511,9 @@ Status markers:

**Tasks:**

- [ ] Implement permissions for read, write, safe commands, arbitrary commands, network, install dependencies, git commit, and git push.
- [ ] Require approval for dangerous permissions by default.
- [ ] Commit with message `feat: add permission policy`.
- [x] Implement permissions for read, write, safe commands, arbitrary commands, network, install dependencies, git commit, and git push.
- [x] Require approval for dangerous permissions by default.
- [x] Commit with message `feat: add permission policy`.

**Acceptance Criteria:**

Expand Down