mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 09:07:46 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Agent permissions control which agents can create or hire other agents (`canCreateAgents`) > - Today only CEO-role agents get this permission by default; every other agent starts without it > - Teams that want agents to delegate and build out their own teams must flip the toggle on each hire, and most operators want delegation to work out of the box > - This pull request makes `canCreateAgents` default to enabled for new standard-trust agents, while low-trust agents keep a disabled default > - The benefit is that agent teams can grow without per-agent permission toggling, while low-trust containment and checkout protection stay intact ## Linked Issues or Issue Description Related (not fixed by this PR): #8064 also decouples an authority from `agents:create`. **Subsystem affected** Server agent permissions (`server/src/services/agent-permissions.ts`), authorization (`server/src/services/authorization.ts`), the shared `agentPermissionsSchema` validator, and the UI trust-preset helper. **Problem or motivation** New agents cannot hire other agents unless an operator enables `canCreateAgents` on each one. Only CEO-role agents get the permission by default. This blocks delegation-by-default workflows. Operators must toggle the permission for every hire. **Proposed solution** Default `canCreateAgents` to `true` for newly created agents. Apply and persist the default at creation only. Stored rows without an explicit value stay fail-closed at read and enforcement time. Keep the default at `false` when the agent's permissions record marks it low-trust (the `low_trust_review` preset or a trust boundary). Explicit values always win. Decouple `tasks:manage_active_checkouts` from `canCreateAgents` so the default-on flag does not let a peer agent write over another agent's checked-out issue. **Alternatives considered** Granting the default only at the route layer would leave stored rows and enforcement out of sync. Keeping the checkout authority coupled to `canCreateAgents` would void the active-checkout write protection once the flag is default-on. A per-company setting adds configuration surface without a clear need; explicit per-agent overrides already exist. **Roadmap alignment** Governance and trust-preset work already separates standard-trust from low-trust agents. This change follows that line: capability by default for standard trust, containment by default for low trust. ## What Changed - `normalizeAgentPermissions` now takes a `create`/`stored` context. Creation writes get the new default: enabled unless `permissionsImplyLowTrust()` detects the low-trust review preset or a trust boundary. Stored rows without an explicit value normalize to disabled (fail-closed). The role parameter is gone. - `agentPermissionsSchema` no longer injects `canCreateAgents: false` when the field is omitted. The server-side default applies instead. - `authorization.ts` normalizes raw agent rows for `agents:create`, so enforcement matches what the API reports for legacy rows. - `tasks:manage_active_checkouts` no longer rides on `canCreateAgents`. CEO role, explicit grants, and the manager chain remain the paths. - `agents:create` is denied outright inside any resolved low-trust execution context (agent, project, issue, or run policy). The default-on flag can never reach the legacy creator allow there. - The UI trust-preset helper sets `canCreateAgents: false` when an agent is switched to the low-trust preset, instead of carrying the old value forward. - `doc/CLI.md` describes the new default for `teams install`. - Tests pin the default matrix (standard, low-trust, explicit overrides) on the server and in the UI helper. ## Verification - `cd server && npx vitest run src/__tests__/agent-permissions-service.test.ts src/__tests__/agent-permissions-routes.test.ts src/__tests__/low-trust-red-team-routes.test.ts src/__tests__/authorization-service.test.ts` — 143 tests pass. - Broader sweep: 18 suites that touch `canCreateAgents` (hire, pending-approval, teams catalog, portability, built-in agents, plugin-managed agents) pass locally. - `cd ui && npx vitest run src/lib/trust-policy-ui.test.ts src/components/TrustPresetSection.test.tsx src/pages/NewAgent.test.tsx src/pages/Agents.test.tsx` — passes. - Typecheck is clean for the changed files in `packages/shared`, `server`, and `ui`. ## Risks - Behavioral shift: agents created after this change persist `canCreateAgents: true` unless low-trust. Pre-existing agents keep their stored value. Legacy or malformed permission records without an explicit value stay fail-closed at read and enforcement time; they never gain the authority retroactively. - Low-trust runs can no longer create agents at all, even when the agent carries an explicit `canCreateAgents: true`. Before this change, that combination could hire. The red-team suite and a new authorization test pin the denial. - Narrowing: a non-CEO agent with `canCreateAgents: true` loses implicit `tasks:manage_active_checkouts`. The manager chain and explicit grants still provide it. This narrowing is deliberate; without it, the default-on flag would let any peer bypass active-checkout write protection. - No migrations. No API shape changes. Low-trust defaults are covered by the red-team regression suite. ## Model Used - Claude Fable 5 (`claude-fable-5`), Anthropic — via Claude Code CLI with extended thinking and tool use (code search, editing, local test execution). ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge
76 lines
2.6 KiB
TypeScript
76 lines
2.6 KiB
TypeScript
import { LOW_TRUST_REVIEW_PRESET } from "@paperclipai/shared";
|
|
|
|
export type NormalizedAgentPermissions = Record<string, unknown> & {
|
|
canCreateAgents: boolean;
|
|
canCreateSkills: boolean;
|
|
};
|
|
|
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
? (value as Record<string, unknown>)
|
|
: null;
|
|
}
|
|
|
|
/**
|
|
* Mirrors the agent-source low-trust markers consumed by
|
|
* resolveCoreTrustPreset: the low-trust review preset (top-level or inside
|
|
* authorizationPolicy) or a low-trust boundary. Defaults must never grant
|
|
* agent-creation authority to a low-trust agent.
|
|
*/
|
|
export function permissionsImplyLowTrust(permissions: unknown): boolean {
|
|
const record = asRecord(permissions);
|
|
if (!record) return false;
|
|
const authorizationPolicy = asRecord(record.authorizationPolicy);
|
|
return (
|
|
record.trustPreset === LOW_TRUST_REVIEW_PRESET ||
|
|
authorizationPolicy?.trustPreset === LOW_TRUST_REVIEW_PRESET ||
|
|
asRecord(record.reviewPreset)?.id === LOW_TRUST_REVIEW_PRESET ||
|
|
asRecord(authorizationPolicy?.reviewPreset)?.id === LOW_TRUST_REVIEW_PRESET ||
|
|
asRecord(authorizationPolicy?.trustBoundary) !== null
|
|
);
|
|
}
|
|
|
|
/**
|
|
* "create" is the context for permissions arriving on a new-agent write: the
|
|
* hire/create default applies and the resolved value is persisted. "stored"
|
|
* is the context for rows read back from the database: a row without an
|
|
* explicit value stays fail-closed, so the default is never granted
|
|
* retroactively to legacy or malformed records at read or enforcement time.
|
|
*/
|
|
export type AgentPermissionsContext = "create" | "stored";
|
|
|
|
export function defaultAgentPermissions(
|
|
options?: { lowTrust?: boolean; context?: AgentPermissionsContext },
|
|
): NormalizedAgentPermissions {
|
|
return {
|
|
canCreateAgents: options?.context === "create" && options?.lowTrust !== true,
|
|
canCreateSkills: true,
|
|
};
|
|
}
|
|
|
|
export function normalizeAgentPermissions(
|
|
permissions: unknown,
|
|
options?: { context?: AgentPermissionsContext },
|
|
): NormalizedAgentPermissions {
|
|
const defaults = defaultAgentPermissions({
|
|
lowTrust: permissionsImplyLowTrust(permissions),
|
|
context: options?.context ?? "stored",
|
|
});
|
|
const record = asRecord(permissions);
|
|
if (!record) {
|
|
return defaults;
|
|
}
|
|
|
|
return {
|
|
...record,
|
|
canCreateAgents:
|
|
typeof record.canCreateAgents === "boolean"
|
|
? record.canCreateAgents
|
|
: defaults.canCreateAgents,
|
|
canCreateSkills:
|
|
typeof record.canCreateSkills === "boolean"
|
|
? record.canCreateSkills
|
|
: defaults.canCreateSkills,
|
|
};
|
|
}
|