mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-01 15:12:18 +01:00
Harden low-trust config authorization
This commit is contained in:
1 parent
eb4a49a6b6
commit
9ec1a1b41e
5 files changed
+107
-2
No files matched your search
@@ -37,6 +37,10 @@ changes that behavior. Low-trust containment instead limits what the low-trust
|
||||
agent can read or mutate through the Paperclip API and prevents raw untrusted
|
||||
output from being automatically promoted into higher-trust agent context.
|
||||
|
||||
Low-trust agents cannot read or mutate agent configuration, instruction bundles,
|
||||
or company skill configuration through direct grants. Configuration changes from
|
||||
low-trust work must go through higher-trust review and promotion paths instead.
|
||||
|
||||
## Runtime Containment
|
||||
|
||||
Managed `low_trust_review` runs fail closed unless Paperclip can enforce the
|
||||
|
||||
@@ -717,6 +717,69 @@ describeEmbeddedPostgres("authorization service", () => {
|
||||
})).resolves.toMatchObject({ allowed: false, reason: "deny_low_trust_boundary" });
|
||||
});
|
||||
|
||||
it("blocks low-trust configuration actions before evaluating explicit change grants", async () => {
|
||||
const company = await createCompany(db, "LowTrustConfigGrants");
|
||||
const project = await createProject(db, company.id, "Allowed");
|
||||
const targetAgent = await createAgent(db, company.id);
|
||||
const actorAgent = await createAgent(db, company.id, {
|
||||
role: "ceo",
|
||||
permissions: {
|
||||
trustPreset: LOW_TRUST_REVIEW_PRESET,
|
||||
authorizationPolicy: {
|
||||
trustBoundary: {
|
||||
mode: LOW_TRUST_REVIEW_PRESET,
|
||||
companyId: company.id,
|
||||
projectIds: [project.id],
|
||||
allowedAgentIds: [targetAgent.id],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await grantAgentPermission(db, company.id, actorAgent.id, "agents:configure");
|
||||
await db.insert(principalPermissionGrants).values({
|
||||
companyId: company.id,
|
||||
principalType: "agent",
|
||||
principalId: actorAgent.id,
|
||||
permissionKey: "skills:create",
|
||||
grantedByUserId: null,
|
||||
});
|
||||
|
||||
const authz = authorizationService(db);
|
||||
const actor = { type: "agent" as const, agentId: actorAgent.id, companyId: company.id, source: "agent_key" as const };
|
||||
|
||||
await expect(authz.decide({
|
||||
actor,
|
||||
action: "agent:read",
|
||||
resource: { type: "agent", companyId: company.id, agentId: targetAgent.id },
|
||||
})).resolves.toMatchObject({ allowed: true, reason: "allow_low_trust_boundary" });
|
||||
|
||||
await expect(authz.decide({
|
||||
actor,
|
||||
action: "agent_config:read",
|
||||
resource: { type: "agent", companyId: company.id, agentId: targetAgent.id },
|
||||
})).resolves.toMatchObject({ allowed: false, reason: "deny_low_trust_boundary" });
|
||||
|
||||
await expect(authz.decide({
|
||||
actor,
|
||||
action: "agent_config:read",
|
||||
resource: { type: "agent", companyId: company.id, agentId: actorAgent.id },
|
||||
})).resolves.toMatchObject({ allowed: false, reason: "deny_low_trust_boundary" });
|
||||
|
||||
await expect(authz.decide({
|
||||
actor,
|
||||
action: "agent_config:update",
|
||||
resource: { type: "agent", companyId: company.id, agentId: targetAgent.id },
|
||||
scope: { requiresChangeGrant: true, consentedChange: true },
|
||||
})).resolves.toMatchObject({ allowed: false, reason: "deny_low_trust_boundary" });
|
||||
|
||||
await expect(authz.decide({
|
||||
actor,
|
||||
action: "skill_config:update",
|
||||
resource: { type: "company", companyId: company.id },
|
||||
scope: { consentedChange: true },
|
||||
})).resolves.toMatchObject({ allowed: false, reason: "deny_low_trust_boundary" });
|
||||
});
|
||||
|
||||
it("denies simple-mode assignment when the target agent requires protected-assignment approval", async () => {
|
||||
const company = await createCompany(db, "ProtectedAssignment");
|
||||
const actorAgent = await createAgent(db, company.id, { role: "engineer" });
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
approvals,
|
||||
assets,
|
||||
companies,
|
||||
companyMemberships,
|
||||
companySkills,
|
||||
createDb,
|
||||
documentAnnotationComments,
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
issues,
|
||||
issueThreadInteractions,
|
||||
issueWorkProducts,
|
||||
principalPermissionGrants,
|
||||
projects,
|
||||
} from "@paperclipai/db";
|
||||
import { ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY, LOW_TRUST_REVIEW_PRESET } from "@paperclipai/shared";
|
||||
@@ -653,6 +655,8 @@ describeEmbeddedPostgres("low-trust red-team HTTP route regression suite", () =>
|
||||
await deleteHeartbeatRunsAndWakeupsAfterActivityLogDrains(db);
|
||||
await db.delete(issues);
|
||||
await db.delete(agentRuntimeState);
|
||||
await db.delete(principalPermissionGrants);
|
||||
await db.delete(companyMemberships);
|
||||
await db.delete(agents);
|
||||
await db.delete(projects);
|
||||
await db.delete(companySkills);
|
||||
@@ -782,6 +786,29 @@ describeEmbeddedPostgres("low-trust red-team HTTP route regression suite", () =>
|
||||
|
||||
it("restricts low-trust self inspection without changing standard-agent visibility", async () => {
|
||||
const fixture = await seedLowTrustFixture(db);
|
||||
await db.insert(companyMemberships).values({
|
||||
companyId: fixture.company.id,
|
||||
principalType: "agent",
|
||||
principalId: fixture.agents.lowTrust.id,
|
||||
status: "active",
|
||||
membershipRole: "member",
|
||||
});
|
||||
await db.insert(principalPermissionGrants).values([
|
||||
{
|
||||
companyId: fixture.company.id,
|
||||
principalType: "agent",
|
||||
principalId: fixture.agents.lowTrust.id,
|
||||
permissionKey: "agents:configure",
|
||||
grantedByUserId: null,
|
||||
},
|
||||
{
|
||||
companyId: fixture.company.id,
|
||||
principalType: "agent",
|
||||
principalId: fixture.agents.lowTrust.id,
|
||||
permissionKey: "skills:create",
|
||||
grantedByUserId: null,
|
||||
},
|
||||
]);
|
||||
|
||||
const lowTrustRes = await request(createApp(db, agentActor(fixture))).get("/api/agents/me");
|
||||
expect(lowTrustRes.status, JSON.stringify(lowTrustRes.body)).toBe(200);
|
||||
@@ -810,6 +837,16 @@ describeEmbeddedPostgres("low-trust red-team HTTP route regression suite", () =>
|
||||
expect(lowTrustSelfByIdRes.body).not.toHaveProperty("access");
|
||||
expectNoCanary(lowTrustSelfByIdRes.body, fixture.canaries.agentConfig);
|
||||
|
||||
const lowTrustPeerConfigRes = await request(createApp(db, agentActor(fixture)))
|
||||
.get(`/api/agents/${fixture.agents.collaborator.id}/configuration`);
|
||||
expect(lowTrustPeerConfigRes.status, JSON.stringify(lowTrustPeerConfigRes.body)).toBe(403);
|
||||
expectNoCanary(lowTrustPeerConfigRes.body, fixture.canaries.agentConfig);
|
||||
|
||||
const lowTrustSelfBundleRes = await request(createApp(db, agentActor(fixture)))
|
||||
.get(`/api/agents/${fixture.agents.lowTrust.id}/instructions-bundle`);
|
||||
expect(lowTrustSelfBundleRes.status, JSON.stringify(lowTrustSelfBundleRes.body)).toBe(403);
|
||||
expectNoCanary(lowTrustSelfBundleRes.body, fixture.canaries.agentConfig);
|
||||
|
||||
const standardActor = agentActor(fixture, fixture.agents.standard.id);
|
||||
const standardRes = await request(createApp(db, { ...standardActor, runId: null })).get("/api/agents/me");
|
||||
expect(standardRes.status, JSON.stringify(standardRes.body)).toBe(200);
|
||||
|
||||
@@ -839,8 +839,6 @@ export function agentRoutes(
|
||||
if (!actorAgent || actorAgent.companyId !== targetAgent.companyId) {
|
||||
throw forbidden("Agent key cannot access another company");
|
||||
}
|
||||
if (actorAgent.id === targetAgent.id) return;
|
||||
|
||||
const decision = await access.decide({
|
||||
actor: req.actor,
|
||||
action: "agent_config:read",
|
||||
|
||||
@@ -889,6 +889,9 @@ export function authorizationService(db: Db) {
|
||||
|
||||
if (
|
||||
input.action === "company_scope:read" ||
|
||||
input.action === "agent_config:read" ||
|
||||
input.action === "agent_config:update" ||
|
||||
input.action === "skill_config:update" ||
|
||||
input.action === "runtime:manage" ||
|
||||
input.action === "secrets:read"
|
||||
) {
|
||||
|
||||
Reference in new issue
Block a user