Implement low-trust preset resolver

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-06-05 18:06:39 +00:00
1 parent fff3832a01
commit 516f79dee9
16 files changed
+774 -3

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
# Low-Trust Presets
Paperclip ships core trust preset names so containment decisions are enforced in
Community Edition even when EE policy editing is unavailable.
## Presets
- `standard`: the default V1 company-visible collaboration model. This preserves
existing behavior for normal agents.
- `low_trust_review`: an opt-in containment preset for automated work that may
consume hostile or prompt-injected input, such as untrusted pull requests,
external tickets, dependency diffs, or generated review output.
## Boundary Model
`low_trust_review` is resolved from existing JSON policy fields:
- agent permissions: `permissions.trustPreset` and
`permissions.authorizationPolicy.trustBoundary`
- project policy:
`executionWorkspacePolicy.authorizationPolicy.trustBoundary`
- issue/run policy: `executionPolicy.authorizationPolicy.trustBoundary`
The resolver intersects those sources. Narrower wins. A low-trust preset must
resolve to a concrete company-local project, root issue, or issue-id scope. If a
policy source names another company, uses an unsupported preset, or lacks that
scope for risky access, Paperclip fails closed.
## Containment, Not Privacy
This is containment for hostile automated work. It is not a general project,
issue, or human privacy system.
V1 standard work remains company-visible by default: board users and in-company
actors can inspect company work objects unless a separate access-control feature
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.
+4
View File
@@ -45,6 +45,10 @@ These decisions close open questions from `SPEC.md` for V1.
| Budget enforcement | Soft alerts + hard limit auto-pause |
| Deployment modes | Canonical model is `local_trusted` + `authenticated` with `private/public` exposure policy (see `doc/DEPLOYMENT-MODES.md`) |
Low-trust agent presets are containment controls for hostile automated work, not
general project or issue privacy controls. The core preset resolver contract is
documented in `doc/LOW-TRUST-PRESETS.md`.
## 4. Current Baseline (Repo Snapshot)
As of 2026-02-17, the repo already includes:
+21
View File
@@ -7,6 +7,20 @@ export {
parseFrontmatterMarkdown,
type MarkdownDoc,
} from "./frontmatter.js";
export {
TRUST_PRESETS,
DEFAULT_TRUST_PRESET,
LOW_TRUST_REVIEW_PRESET,
LOW_TRUST_REVIEW_PRESET_VERSION,
LOW_TRUST_REVIEW_RAW_OUTPUT_DISPOSITION,
LOW_TRUST_TOOL_CLASSES,
type TrustPreset,
type LowTrustToolClass,
type LowTrustOutputPromotionTarget,
type LowTrustBoundary,
type LowTrustReviewPresetPolicy,
type TrustAuthorizationPolicy,
} from "./trust-policy.js";
export {
COMPANY_STATUSES,
DEFAULT_COMPANY_ATTACHMENT_MAX_BYTES,
@@ -817,8 +831,15 @@ export {
instanceExperimentalSettingsSchema,
patchInstanceExperimentalSettingsSchema,
issueGraphLivenessAutoRecoveryRequestSchema,
trustPresetSchema,
lowTrustBoundarySchema,
lowTrustReviewPresetPolicySchema,
trustAuthorizationPolicySchema,
type PatchInstanceExperimentalSettings,
type IssueGraphLivenessAutoRecoveryRequest,
type TrustPresetInput,
type LowTrustBoundaryInput,
type TrustAuthorizationPolicyInput,
} from "./validators/index.js";
export {
+45
View File
@@ -0,0 +1,45 @@
export const TRUST_PRESETS = ["standard", "low_trust_review"] as const;
export type TrustPreset = (typeof TRUST_PRESETS)[number];
export const DEFAULT_TRUST_PRESET = "standard" as const;
export const LOW_TRUST_REVIEW_PRESET = "low_trust_review" as const;
export const LOW_TRUST_REVIEW_PRESET_VERSION = 1 as const;
export const LOW_TRUST_REVIEW_RAW_OUTPUT_DISPOSITION = "quarantine" as const;
export const LOW_TRUST_TOOL_CLASSES = [
"git.read",
"github.pr.read",
"tests.local",
] as const;
export type LowTrustToolClass = (typeof LOW_TRUST_TOOL_CLASSES)[number] | string;
export interface LowTrustOutputPromotionTarget {
type: "issue";
issueId: string;
}
export interface LowTrustBoundary {
mode: typeof LOW_TRUST_REVIEW_PRESET;
companyId?: string;
projectIds?: string[];
rootIssueId?: string;
issueIds?: string[];
allowedAgentIds?: string[];
allowedSecretBindingIds?: string[];
allowedToolClasses?: LowTrustToolClass[];
outputPromotionTarget?: LowTrustOutputPromotionTarget;
}
export interface LowTrustReviewPresetPolicy {
id: typeof LOW_TRUST_REVIEW_PRESET;
version: typeof LOW_TRUST_REVIEW_PRESET_VERSION;
rawOutputDisposition: typeof LOW_TRUST_REVIEW_RAW_OUTPUT_DISPOSITION;
}
export interface TrustAuthorizationPolicy extends Record<string, unknown> {
trustPreset?: TrustPreset;
reviewPreset?: LowTrustReviewPresetPolicy;
trustBoundary?: LowTrustBoundary;
}
+7 -1
View File
@@ -9,9 +9,15 @@ import type {
CompanyMembership,
PrincipalPermissionGrant,
} from "./access.js";
import type {
TrustAuthorizationPolicy,
TrustPreset,
} from "../trust-policy.js";
export interface AgentPermissions {
export interface AgentPermissions extends Record<string, unknown> {
canCreateAgents: boolean;
trustPreset?: TrustPreset;
authorizationPolicy?: TrustAuthorizationPolicy;
}
export interface AgentModelProfileConfig {
+14
View File
@@ -40,6 +40,20 @@ export {
MIN_ISSUE_GRAPH_LIVENESS_AUTO_RECOVERY_LOOKBACK_HOURS,
MAX_ISSUE_GRAPH_LIVENESS_AUTO_RECOVERY_LOOKBACK_HOURS,
} from "./instance.js";
export {
TRUST_PRESETS,
DEFAULT_TRUST_PRESET,
LOW_TRUST_REVIEW_PRESET,
LOW_TRUST_REVIEW_PRESET_VERSION,
LOW_TRUST_REVIEW_RAW_OUTPUT_DISPOSITION,
LOW_TRUST_TOOL_CLASSES,
type TrustPreset,
type LowTrustToolClass,
type LowTrustOutputPromotionTarget,
type LowTrustBoundary,
type LowTrustReviewPresetPolicy,
type TrustAuthorizationPolicy,
} from "../trust-policy.js";
export type {
CompanySkillSourceType,
CompanySkillTrustLevel,
+6
View File
@@ -30,6 +30,10 @@ import type { Goal } from "./goal.js";
import type { Project, ProjectWorkspace } from "./project.js";
import type { ExecutionWorkspace, IssueExecutionWorkspaceSettings } from "./workspace-runtime.js";
import type { IssueWorkProduct } from "./work-product.js";
import type {
LowTrustReviewPresetPolicy,
TrustAuthorizationPolicy,
} from "../trust-policy.js";
export type { IssueWorkMode };
@@ -441,6 +445,8 @@ export interface IssueExecutionPolicy {
commentRequired: boolean;
stages: IssueExecutionStage[];
monitor?: IssueExecutionMonitorPolicy | null;
reviewPreset?: LowTrustReviewPresetPolicy;
authorizationPolicy?: TrustAuthorizationPolicy;
}
export interface IssueExecutionMonitorState {
@@ -1,3 +1,5 @@
import type { TrustAuthorizationPolicy } from "../trust-policy.js";
export type ExecutionWorkspaceStrategyType =
| "project_primary"
| "git_worktree"
@@ -155,6 +157,7 @@ export interface ProjectExecutionWorkspacePolicy {
pullRequestPolicy?: Record<string, unknown> | null;
runtimePolicy?: Record<string, unknown> | null;
cleanupPolicy?: Record<string, unknown> | null;
authorizationPolicy?: TrustAuthorizationPolicy | null;
}
export interface IssueExecutionWorkspaceSettings {
+4 -1
View File
@@ -7,10 +7,13 @@ import {
} from "../constants.js";
import { agentAdapterTypeSchema } from "../adapter-type.js";
import { envConfigSchema } from "./secret.js";
import { trustAuthorizationPolicySchema, trustPresetSchema } from "./trust-policy.js";
export const agentPermissionsSchema = z.object({
canCreateAgents: z.boolean().optional().default(false),
});
trustPreset: trustPresetSchema.optional(),
authorizationPolicy: trustAuthorizationPolicySchema.optional(),
}).catchall(z.unknown());
export const agentInstructionsBundleModeSchema = z.enum(["managed", "external"]);
+10
View File
@@ -146,6 +146,16 @@ export {
type CompanyPortabilityImport,
} from "./company-portability.js";
export {
trustPresetSchema,
lowTrustBoundarySchema,
lowTrustReviewPresetPolicySchema,
trustAuthorizationPolicySchema,
type TrustPresetInput,
type LowTrustBoundaryInput,
type TrustAuthorizationPolicyInput,
} from "./trust-policy.js";
export {
createAgentSchema,
createAgentHireSchema,
+3
View File
@@ -27,6 +27,7 @@ import {
MODEL_PROFILE_KEYS,
} from "../constants.js";
import { multilineTextSchema } from "./text.js";
import { lowTrustReviewPresetPolicySchema, trustAuthorizationPolicySchema } from "./trust-policy.js";
export const issueBlockedInboxStateSchema = z.enum([
"needs_attention",
@@ -197,6 +198,8 @@ export const issueExecutionPolicySchema = z.object({
commentRequired: z.boolean().optional().default(true),
stages: z.array(issueExecutionStageSchema).default([]),
monitor: issueExecutionMonitorPolicySchema.optional().nullable(),
reviewPreset: lowTrustReviewPresetPolicySchema.optional(),
authorizationPolicy: trustAuthorizationPolicySchema.optional(),
});
export const issueExecutionMonitorStateSchema = z.object({
@@ -1,6 +1,7 @@
import { z } from "zod";
import { PROJECT_STATUSES } from "../constants.js";
import { envConfigSchema } from "./secret.js";
import { trustAuthorizationPolicySchema } from "./trust-policy.js";
const executionWorkspaceStrategySchema = z
.object({
@@ -26,6 +27,7 @@ export const projectExecutionWorkspacePolicySchema = z
pullRequestPolicy: z.record(z.string(), z.unknown()).optional().nullable(),
runtimePolicy: z.record(z.string(), z.unknown()).optional().nullable(),
cleanupPolicy: z.record(z.string(), z.unknown()).optional().nullable(),
authorizationPolicy: trustAuthorizationPolicySchema.optional().nullable(),
})
.strict();
@@ -0,0 +1,42 @@
import { z } from "zod";
import {
LOW_TRUST_REVIEW_PRESET,
LOW_TRUST_REVIEW_PRESET_VERSION,
LOW_TRUST_REVIEW_RAW_OUTPUT_DISPOSITION,
TRUST_PRESETS,
} from "../trust-policy.js";
export const trustPresetSchema = z.enum(TRUST_PRESETS);
export const lowTrustOutputPromotionTargetSchema = z.object({
type: z.literal("issue"),
issueId: z.string().uuid(),
}).strict();
export const lowTrustBoundarySchema = z.object({
mode: z.literal(LOW_TRUST_REVIEW_PRESET),
companyId: z.string().uuid().optional(),
projectIds: z.array(z.string().uuid()).optional(),
rootIssueId: z.string().uuid().optional(),
issueIds: z.array(z.string().uuid()).optional(),
allowedAgentIds: z.array(z.string().uuid()).optional(),
allowedSecretBindingIds: z.array(z.string().uuid()).optional(),
allowedToolClasses: z.array(z.string().trim().min(1)).optional(),
outputPromotionTarget: lowTrustOutputPromotionTargetSchema.optional(),
}).strict();
export const lowTrustReviewPresetPolicySchema = z.object({
id: z.literal(LOW_TRUST_REVIEW_PRESET),
version: z.literal(LOW_TRUST_REVIEW_PRESET_VERSION),
rawOutputDisposition: z.literal(LOW_TRUST_REVIEW_RAW_OUTPUT_DISPOSITION),
}).strict();
export const trustAuthorizationPolicySchema = z.object({
trustPreset: trustPresetSchema.optional(),
reviewPreset: lowTrustReviewPresetPolicySchema.optional(),
trustBoundary: lowTrustBoundarySchema.optional(),
}).catchall(z.unknown());
export type TrustPresetInput = z.infer<typeof trustPresetSchema>;
export type LowTrustBoundaryInput = z.infer<typeof lowTrustBoundarySchema>;
export type TrustAuthorizationPolicyInput = z.infer<typeof trustAuthorizationPolicySchema>;
@@ -0,0 +1,222 @@
import { describe, expect, it } from "vitest";
import {
agentPermissionsSchema,
type LowTrustBoundary,
LOW_TRUST_REVIEW_RAW_OUTPUT_DISPOSITION,
LOW_TRUST_REVIEW_PRESET,
} from "@paperclipai/shared";
import { normalizeIssueExecutionPolicy } from "../services/issue-execution-policy.js";
import {
isIssueWithinLowTrustBoundary,
resolveCoreTrustPreset,
} from "../services/trust-preset-resolver.js";
const companyId = "11111111-1111-4111-8111-111111111111";
const otherCompanyId = "22222222-2222-4222-8222-222222222222";
const projectA = "33333333-3333-4333-8333-333333333333";
const projectB = "44444444-4444-4444-8444-444444444444";
const projectC = "55555555-5555-4555-8555-555555555555";
const rootIssueId = "66666666-6666-4666-8666-666666666666";
const issueA = "77777777-7777-4777-8777-777777777777";
const issueB = "88888888-8888-4888-8888-888888888888";
const issueC = "99999999-9999-4999-8999-999999999999";
const agentA = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
const agentB = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb";
function lowTrustBoundary(input: Partial<Omit<LowTrustBoundary, "mode">>): LowTrustBoundary {
return {
mode: LOW_TRUST_REVIEW_PRESET,
companyId,
...input,
};
}
function boundaryPolicy(boundary: ReturnType<typeof lowTrustBoundary>) {
return {
authorizationPolicy: {
trustBoundary: boundary,
},
};
}
describe("resolveCoreTrustPreset", () => {
it("defaults to standard with no boundary", () => {
const result = resolveCoreTrustPreset({
companyId,
agent: { companyId, permissions: { canCreateAgents: false } },
});
expect(result).toMatchObject({
kind: "standard",
preset: "standard",
boundary: null,
});
});
it("intersects low-trust agent, project, and issue policy boundaries", () => {
const result = resolveCoreTrustPreset({
companyId,
agent: {
companyId,
permissions: {
trustPreset: LOW_TRUST_REVIEW_PRESET,
authorizationPolicy: {
managedBy: "core-trust-preset",
trustBoundary: lowTrustBoundary({
projectIds: [projectA, projectB],
rootIssueId,
issueIds: [issueA, issueB],
allowedAgentIds: [agentA, agentB],
allowedToolClasses: ["git.read", "tests.local"],
}),
},
},
},
project: {
companyId,
executionWorkspacePolicy: boundaryPolicy(lowTrustBoundary({
projectIds: [projectB, projectC],
issueIds: [issueB, issueC],
allowedAgentIds: [agentB],
allowedToolClasses: ["git.read"],
})),
},
issue: {
companyId,
executionPolicy: {
authorizationPolicy: {
trustBoundary: lowTrustBoundary({
issueIds: [issueB],
allowedToolClasses: ["git.read", "github.pr.read"],
}),
},
},
},
});
expect(result.kind).toBe("low_trust_review");
if (result.kind !== "low_trust_review") throw new Error("expected low-trust result");
expect(result.boundary).toMatchObject({
companyId,
mode: LOW_TRUST_REVIEW_PRESET,
rootIssueId,
projectIds: [projectB],
issueIds: [issueB],
allowedAgentIds: [agentB],
allowedToolClasses: ["git.read"],
});
expect(isIssueWithinLowTrustBoundary(result.boundary, { companyId, id: issueB, projectId: projectB })).toBe(true);
expect(isIssueWithinLowTrustBoundary(result.boundary, { companyId, id: issueC, projectId: projectC })).toBe(false);
});
it("fails closed for unknown presets", () => {
const result = resolveCoreTrustPreset({
companyId,
agent: {
companyId,
permissions: {
trustPreset: "trusted_but_weird",
},
},
});
expect(result).toMatchObject({
kind: "denied",
reason: "unsupported_trust_preset",
source: "agent",
});
});
it("fails closed when low-trust has no concrete project or issue scope", () => {
const result = resolveCoreTrustPreset({
companyId,
agent: {
companyId,
permissions: {
trustPreset: LOW_TRUST_REVIEW_PRESET,
authorizationPolicy: {
trustBoundary: lowTrustBoundary({ allowedToolClasses: ["git.read"] }),
},
},
},
});
expect(result).toMatchObject({
kind: "denied",
reason: "missing_low_trust_boundary_scope",
});
});
it("denies cross-company policy sources and boundaries", () => {
const sourceMismatch = resolveCoreTrustPreset({
companyId,
project: {
companyId: otherCompanyId,
executionWorkspacePolicy: boundaryPolicy(lowTrustBoundary({ projectIds: [projectA] })),
},
});
expect(sourceMismatch).toMatchObject({
kind: "denied",
reason: "cross_company_boundary",
source: "project",
});
const boundaryMismatch = resolveCoreTrustPreset({
companyId,
issue: {
companyId,
executionPolicy: {
authorizationPolicy: {
trustBoundary: {
mode: LOW_TRUST_REVIEW_PRESET,
companyId: otherCompanyId,
rootIssueId,
},
},
},
},
});
expect(boundaryMismatch).toMatchObject({
kind: "denied",
reason: "cross_company_boundary",
source: "issue",
});
});
it("normalizes and preserves trust policy JSON alongside existing policy data", () => {
const permissions = agentPermissionsSchema.parse({
canCreateAgents: false,
trustPreset: LOW_TRUST_REVIEW_PRESET,
authorizationPolicy: {
managedBy: "ee-permissions",
customEeField: { mode: "visualized" },
trustBoundary: lowTrustBoundary({ rootIssueId }),
},
});
expect(permissions.authorizationPolicy?.customEeField).toEqual({ mode: "visualized" });
const executionPolicy = normalizeIssueExecutionPolicy({
reviewPreset: {
id: LOW_TRUST_REVIEW_PRESET,
version: 1,
rawOutputDisposition: LOW_TRUST_REVIEW_RAW_OUTPUT_DISPOSITION,
},
authorizationPolicy: {
managedBy: "core-trust-preset",
trustBoundary: lowTrustBoundary({ rootIssueId }),
},
});
expect(executionPolicy).toMatchObject({
stages: [],
reviewPreset: {
id: LOW_TRUST_REVIEW_PRESET,
rawOutputDisposition: LOW_TRUST_REVIEW_RAW_OUTPUT_DISPOSITION,
},
authorizationPolicy: {
managedBy: "core-trust-preset",
trustBoundary: { rootIssueId },
},
});
});
});
@@ -387,13 +387,18 @@ export function normalizeIssueExecutionPolicy(input: unknown): IssueExecutionPol
}
: null;
if (stages.length === 0 && !monitor) return null;
const reviewPreset = parsed.data.reviewPreset;
const authorizationPolicy = parsed.data.authorizationPolicy;
if (stages.length === 0 && !monitor && !reviewPreset && !authorizationPolicy) return null;
return {
mode: parsed.data.mode ?? "normal",
commentRequired: true,
stages,
...(monitor ? { monitor } : {}),
...(reviewPreset ? { reviewPreset } : {}),
...(authorizationPolicy ? { authorizationPolicy } : {}),
};
}
@@ -0,0 +1,347 @@
import {
DEFAULT_TRUST_PRESET,
LOW_TRUST_REVIEW_PRESET,
type LowTrustBoundary,
type TrustPreset,
lowTrustBoundarySchema,
lowTrustReviewPresetPolicySchema,
trustAuthorizationPolicySchema,
trustPresetSchema,
} from "@paperclipai/shared";
type JsonRecord = Record<string, unknown>;
export type TrustPresetPolicySource = "agent" | "project" | "issue" | "run";
export type ResolveCoreTrustPresetInput = {
companyId: string;
agent?: {
companyId?: string | null;
permissions?: unknown;
} | null;
project?: {
companyId?: string | null;
executionWorkspacePolicy?: unknown;
} | null;
issue?: {
companyId?: string | null;
executionPolicy?: unknown;
} | null;
run?: {
companyId?: string | null;
executionPolicy?: unknown;
} | null;
};
export type TrustPresetDenyReason =
| "unsupported_trust_preset"
| "invalid_authorization_policy"
| "invalid_low_trust_boundary"
| "cross_company_boundary"
| "conflicting_low_trust_boundary"
| "missing_low_trust_boundary_scope";
export type TrustPresetResolution =
| {
kind: "standard";
preset: typeof DEFAULT_TRUST_PRESET;
boundary: null;
sourcePresets: Partial<Record<TrustPresetPolicySource, TrustPreset>>;
}
| {
kind: "low_trust_review";
preset: typeof LOW_TRUST_REVIEW_PRESET;
boundary: LowTrustBoundary & { companyId: string };
sourcePresets: Partial<Record<TrustPresetPolicySource, TrustPreset>>;
}
| {
kind: "denied";
reason: TrustPresetDenyReason;
source: TrustPresetPolicySource | null;
detail: string;
sourcePresets: Partial<Record<TrustPresetPolicySource, TrustPreset>>;
};
type ParsedPolicySource = {
source: TrustPresetPolicySource;
companyId: string | null;
rawPolicy: JsonRecord | null;
authorizationPolicy: JsonRecord | null;
trustPreset: TrustPreset | null;
boundary: LowTrustBoundary | null;
impliesLowTrust: boolean;
};
function asRecord(value: unknown): JsonRecord | null {
return typeof value === "object" && value !== null && !Array.isArray(value)
? value as JsonRecord
: null;
}
function deny(
reason: TrustPresetDenyReason,
source: TrustPresetPolicySource | null,
detail: string,
sourcePresets: Partial<Record<TrustPresetPolicySource, TrustPreset>>,
): TrustPresetResolution {
return { kind: "denied", reason, source, detail, sourcePresets };
}
function isTrustPresetResolution(value: unknown): value is TrustPresetResolution {
if (!value || typeof value !== "object") return false;
const kind = (value as { kind?: unknown }).kind;
return kind === "standard" || kind === "low_trust_review" || kind === "denied";
}
function parsePreset(
value: unknown,
source: TrustPresetPolicySource,
sourcePresets: Partial<Record<TrustPresetPolicySource, TrustPreset>>,
): TrustPresetResolution | TrustPreset | null {
if (value === undefined || value === null) return null;
const parsed = trustPresetSchema.safeParse(value);
if (!parsed.success) {
return deny(
"unsupported_trust_preset",
source,
`Unsupported trust preset in ${source} policy.`,
sourcePresets,
);
}
return parsed.data;
}
function parseReviewPresetId(
value: unknown,
source: TrustPresetPolicySource,
sourcePresets: Partial<Record<TrustPresetPolicySource, TrustPreset>>,
): TrustPresetResolution | TrustPreset | null {
if (value === undefined || value === null) return null;
const parsed = lowTrustReviewPresetPolicySchema.safeParse(value);
if (!parsed.success) {
return deny(
"unsupported_trust_preset",
source,
`Unsupported review preset in ${source} policy.`,
sourcePresets,
);
}
return parsed.data.id;
}
function parseAuthorizationPolicy(
value: unknown,
source: TrustPresetPolicySource,
sourcePresets: Partial<Record<TrustPresetPolicySource, TrustPreset>>,
): TrustPresetResolution | JsonRecord | null {
if (value === undefined || value === null) return null;
const parsed = trustAuthorizationPolicySchema.safeParse(value);
if (!parsed.success) {
return deny(
"invalid_authorization_policy",
source,
`Invalid authorization policy in ${source} policy.`,
sourcePresets,
);
}
return parsed.data as JsonRecord;
}
function parseBoundary(
value: unknown,
source: TrustPresetPolicySource,
sourcePresets: Partial<Record<TrustPresetPolicySource, TrustPreset>>,
): TrustPresetResolution | LowTrustBoundary | null {
if (value === undefined || value === null) return null;
const parsed = lowTrustBoundarySchema.safeParse(value);
if (!parsed.success) {
return deny(
"invalid_low_trust_boundary",
source,
`Invalid low-trust boundary in ${source} policy.`,
sourcePresets,
);
}
return parsed.data;
}
function parseSource(
source: TrustPresetPolicySource,
companyId: string | null | undefined,
rawPolicy: JsonRecord | null,
authorizationPolicyInput: unknown,
sourcePresets: Partial<Record<TrustPresetPolicySource, TrustPreset>>,
): ParsedPolicySource | TrustPresetResolution {
const topPreset = parsePreset(rawPolicy?.trustPreset, source, sourcePresets);
if (isTrustPresetResolution(topPreset)) return topPreset;
const topReviewPreset = parseReviewPresetId(rawPolicy?.reviewPreset, source, sourcePresets);
if (isTrustPresetResolution(topReviewPreset)) return topReviewPreset;
const authorizationPolicy = parseAuthorizationPolicy(authorizationPolicyInput, source, sourcePresets);
if (isTrustPresetResolution(authorizationPolicy)) return authorizationPolicy;
const authPreset = parsePreset(authorizationPolicy?.trustPreset, source, sourcePresets);
if (isTrustPresetResolution(authPreset)) return authPreset;
const authReviewPreset = parseReviewPresetId(authorizationPolicy?.reviewPreset, source, sourcePresets);
if (isTrustPresetResolution(authReviewPreset)) return authReviewPreset;
const boundary = parseBoundary(authorizationPolicy?.trustBoundary, source, sourcePresets);
if (isTrustPresetResolution(boundary)) return boundary;
const trustPreset = topPreset ?? topReviewPreset ?? authPreset ?? authReviewPreset;
if (trustPreset) sourcePresets[source] = trustPreset;
return {
source,
companyId: companyId ?? null,
rawPolicy,
authorizationPolicy,
trustPreset,
boundary,
impliesLowTrust: trustPreset === LOW_TRUST_REVIEW_PRESET || Boolean(boundary),
};
}
function normalizeSet(values: readonly string[] | undefined): string[] | undefined {
if (values === undefined) return undefined;
return [...new Set(values)].sort();
}
function intersectSets(left: string[] | undefined, right: string[] | undefined): string[] | undefined {
const normalizedRight = normalizeSet(right);
if (normalizedRight === undefined) return left;
const normalizedLeft = normalizeSet(left);
if (normalizedLeft === undefined) return normalizedRight;
const rightSet = new Set(normalizedRight);
return normalizedLeft.filter((value) => rightSet.has(value));
}
function mergeBoundary(
current: (LowTrustBoundary & { companyId: string }) | null,
next: LowTrustBoundary,
companyId: string,
source: TrustPresetPolicySource,
sourcePresets: Partial<Record<TrustPresetPolicySource, TrustPreset>>,
): (LowTrustBoundary & { companyId: string }) | TrustPresetResolution {
if (next.companyId && next.companyId !== companyId) {
return deny(
"cross_company_boundary",
source,
"Low-trust boundary refers to a different company.",
sourcePresets,
);
}
const base = current ?? { mode: LOW_TRUST_REVIEW_PRESET, companyId };
if (base.rootIssueId && next.rootIssueId && base.rootIssueId !== next.rootIssueId) {
return deny(
"conflicting_low_trust_boundary",
source,
"Low-trust boundary root issue scopes do not overlap.",
sourcePresets,
);
}
return {
...base,
projectIds: intersectSets(base.projectIds, next.projectIds),
rootIssueId: base.rootIssueId ?? next.rootIssueId,
issueIds: intersectSets(base.issueIds, next.issueIds),
allowedAgentIds: intersectSets(base.allowedAgentIds, next.allowedAgentIds),
allowedSecretBindingIds: intersectSets(base.allowedSecretBindingIds, next.allowedSecretBindingIds),
allowedToolClasses: intersectSets(base.allowedToolClasses, next.allowedToolClasses),
outputPromotionTarget: next.outputPromotionTarget ?? base.outputPromotionTarget,
};
}
function hasBoundaryScope(boundary: LowTrustBoundary): boolean {
return Boolean(boundary.rootIssueId)
|| Boolean(boundary.projectIds?.length)
|| Boolean(boundary.issueIds?.length);
}
export function resolveCoreTrustPreset(input: ResolveCoreTrustPresetInput): TrustPresetResolution {
const sourcePresets: Partial<Record<TrustPresetPolicySource, TrustPreset>> = {};
const sources: ParsedPolicySource[] = [];
const agentPermissions = asRecord(input.agent?.permissions);
const agent = parseSource("agent", input.agent?.companyId, agentPermissions, agentPermissions?.authorizationPolicy, sourcePresets);
if ("kind" in agent) return agent;
sources.push(agent);
const projectPolicy = asRecord(input.project?.executionWorkspacePolicy);
const project = parseSource("project", input.project?.companyId, projectPolicy, projectPolicy?.authorizationPolicy, sourcePresets);
if ("kind" in project) return project;
sources.push(project);
const issuePolicy = asRecord(input.issue?.executionPolicy);
const issue = parseSource("issue", input.issue?.companyId, issuePolicy, issuePolicy?.authorizationPolicy, sourcePresets);
if ("kind" in issue) return issue;
sources.push(issue);
const runPolicy = asRecord(input.run?.executionPolicy);
const run = parseSource("run", input.run?.companyId, runPolicy, runPolicy?.authorizationPolicy, sourcePresets);
if ("kind" in run) return run;
sources.push(run);
for (const source of sources) {
if (source.companyId && source.companyId !== input.companyId) {
return deny(
"cross_company_boundary",
source.source,
"Policy source belongs to a different company.",
sourcePresets,
);
}
}
const effectivePreset = sources.some((source) => source.impliesLowTrust)
? LOW_TRUST_REVIEW_PRESET
: DEFAULT_TRUST_PRESET;
if (effectivePreset === DEFAULT_TRUST_PRESET) {
return {
kind: "standard",
preset: DEFAULT_TRUST_PRESET,
boundary: null,
sourcePresets,
};
}
let boundary: (LowTrustBoundary & { companyId: string }) | null = null;
for (const source of sources) {
if (!source.boundary) continue;
const merged = mergeBoundary(boundary, source.boundary, input.companyId, source.source, sourcePresets);
if (isTrustPresetResolution(merged)) return merged;
boundary = merged;
}
if (!boundary || !hasBoundaryScope(boundary)) {
return deny(
"missing_low_trust_boundary_scope",
null,
"Low-trust review requires a concrete project, root issue, or issue-id boundary.",
sourcePresets,
);
}
return {
kind: "low_trust_review",
preset: LOW_TRUST_REVIEW_PRESET,
boundary,
sourcePresets,
};
}
export function isIssueWithinLowTrustBoundary(
boundary: LowTrustBoundary & { companyId: string },
issue: { companyId: string; id?: string | null; projectId?: string | null },
): boolean {
if (issue.companyId !== boundary.companyId) return false;
if (issue.id && issue.id === boundary.rootIssueId) return true;
if (issue.id && boundary.issueIds?.includes(issue.id)) return true;
if (issue.projectId && boundary.projectIds?.includes(issue.projectId)) return true;
return false;
}