Add low-trust review containment (#7530)
## Thinking Path > - Paperclip is a control plane for AI-agent companies, so execution policy and trust boundaries are part of the product's safety contract. > - Low-trust review work needs narrower authority than normal same-company agents because hostile PRs, comments, attachments, and generated output can carry prompt-injection payloads. > - The current V1 shape gives trusted workers broad company context, which is useful for normal execution but too permissive for a reviewer assigned to hostile content. > - This branch adds a `low_trust_review` preset, source-trust tagging, route-level containment, and quarantine handling so low-trust output does not automatically flow into higher-trust wake context. > - The branch has been rebased onto current `origin/master`, and the low-trust migration was renumbered to `0097_low_trust_source_trust.sql` to avoid collisions with existing `0091` through `0096` migrations. > - Greptile feedback was addressed by tightening low-trust detection, preserving project-level trust policy checks, fixing issue-kind promotion lookup, removing duplicate post-lease isolation assertion, documenting fail-closed source-trust behavior, bounding ancestry checks, enforcing runtime issue context for CEOs, awaiting accepted-plan monitor authorization, and making low-trust issue source-trust tagging atomic. > - The benefit is a first production slice of deny-by-default review containment with regression coverage for the main control-plane pivot surfaces. Fixes #7531. ## What Changed - Added shared trust-policy types and validators, plus database/source-trust fields for issues, comments, documents, and work products. - Implemented server enforcement for low-trust issue scope, agent self-view redaction, secret/plugin/runtime denial paths, promotion checks, and quarantined continuation/wake context. - Added focused low-trust regression tests for resolver behavior, source trust, route authorization, heartbeat preflight ordering, runtime containment, and quarantine redaction. - Added board UI affordances for selecting/reviewing the low-trust preset and surfacing source-trust badges in relevant issue views. - Added `doc/LOW-TRUST-PRESETS.md`, updated `doc/SPEC-implementation.md`, and committed the low-trust review contract plan under `doc/plans/`. - Rebasing note: the original `0097_low_trust_source_trust.sql` migration was renamed to `0097_low_trust_source_trust.sql`; the SQL uses `ADD COLUMN IF NOT EXISTS` so users who already applied the old-numbered migration are not broken by the renumbered migration. ## Verification - Rebased branch onto current `origin/master` and force-pushed with lease to `origin/PAP-10211-low-trust-agent` at head `2719f31e3`. - Confirmed the PR diff does not include `pnpm-lock.yaml` or `.github/workflows` changes. - Resolved upstream UI/comment conflicts by preserving deleted-comment tombstone behavior and low-trust source-trust badges/metadata. - Renumbered the low-trust source-trust migration to `0097_low_trust_source_trust.sql`; the SQL uses `ADD COLUMN IF NOT EXISTS` so users who already applied an old-numbered copy are not broken. - `pnpm exec vitest run ui/src/lib/issue-chat-messages.test.ts server/src/__tests__/heartbeat-workspace-session.test.ts` - `pnpm exec vitest run server/src/__tests__/source-trust.test.ts server/src/__tests__/workspace-runtime-service-authz.test.ts ui/src/lib/trust-policy-ui.test.ts ui/src/components/TrustPresetSection.test.tsx` - `pnpm run typecheck:build-gaps` - `git diff --check` - GitHub checks pass on head `2719f31e3`: build, typecheck/release registry, general tests, serialized server suites, e2e, canary, verify, policy/review, Socket, and Snyk. - Greptile Review passes with Confidence Score 5/5 and zero unresolved Greptile review threads. - No design screenshots/images were added because the task explicitly says not to add them unless they are specifically part of the work. ## Risks - Medium risk: this touches shared trust-policy contracts, server authorization paths, heartbeat context generation, migration metadata, and UI preset controls. - Low-trust containment is intentionally deny-by-default; legitimate future review workflows may need explicit allowlisted exceptions. - Plugin/runtime/security surfaces are broad, so regression tests cover the current known routes but future integrations must route through the same containment layer. - The PR is ready for review; GitHub checks are green and Greptile is 5/5. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex, GPT-5 coding agent, tool-enabled shell and GitHub CLI workflow. ## 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 run tests locally and they pass - [x] I have added or updated tests where applicable - [x] UI changes are covered by focused tests; no screenshots were added per task instruction not to add design images unless specifically required - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
AGENT_DEFAULT_MAX_CONCURRENT_RUNS,
|
||||
ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY,
|
||||
MODEL_PROFILE_KEYS,
|
||||
envBindingSchema,
|
||||
isEnvironmentDriverSupportedForAdapter,
|
||||
type BillingType,
|
||||
type EnvironmentLeaseStatus,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
type ModelProfileKey,
|
||||
type RoutineRevisionSnapshotV1,
|
||||
type RunLivenessState,
|
||||
type SourceTrustMetadata,
|
||||
} from "@paperclipai/shared";
|
||||
import {
|
||||
agents,
|
||||
@@ -153,6 +155,10 @@ import {
|
||||
import { recoveryService } from "./recovery/service.js";
|
||||
import { productivityReviewService } from "./productivity-review.js";
|
||||
import { withAgentStartLock } from "./agent-start-lock.js";
|
||||
import {
|
||||
redactQuarantinedBodyForHigherTrust,
|
||||
sanitizeQuarantinedCommentForHigherTrust,
|
||||
} from "./source-trust.js";
|
||||
import {
|
||||
redactCurrentUserText,
|
||||
redactCurrentUserValue,
|
||||
@@ -173,6 +179,11 @@ import { environmentService } from "./environments.js";
|
||||
import { environmentRuntimeService } from "./environment-runtime.js";
|
||||
import { environmentRunOrchestrator } from "./environment-run-orchestrator.js";
|
||||
import { isUnsafeSessionWorkspaceCwd } from "./session-workspace-cwd.js";
|
||||
import {
|
||||
assertLowTrustRuntimeServicesAllowed,
|
||||
assertLowTrustWorkspaceIsolation,
|
||||
} from "./low-trust-runtime-containment.js";
|
||||
import { resolveCoreTrustPreset, type TrustPresetResolution } from "./trust-preset-resolver.js";
|
||||
import type { PluginWorkerManager } from "./plugin-worker-manager.js";
|
||||
|
||||
const MAX_LIVE_LOG_CHUNK_BYTES = 8 * 1024;
|
||||
@@ -336,6 +347,9 @@ type RuntimeConfigSecretResolver = Pick<
|
||||
"resolveAdapterConfigForRuntime" | "resolveEnvBindings"
|
||||
>;
|
||||
|
||||
const LOW_TRUST_SENSITIVE_ENV_KEY_RE =
|
||||
/(api[-_]?key|access[-_]?token|auth(?:_?token)?|authorization|bearer|secret|passwd|password|credential|jwt|private[-_]?key|cookie|connectionstring)/i;
|
||||
|
||||
function isPaperclipRuntimeEnvKey(key: string) {
|
||||
return key.startsWith("PAPERCLIP_");
|
||||
}
|
||||
@@ -356,6 +370,24 @@ function stripPaperclipRuntimeEnvFromAdapterConfig(config: Record<string, unknow
|
||||
};
|
||||
}
|
||||
|
||||
function assertLowTrustEnvConfigAllowed(envValue: unknown, source: string) {
|
||||
const record = stripPaperclipRuntimeEnvBindings(envValue);
|
||||
if (!record) return;
|
||||
for (const [key, rawBinding] of Object.entries(record)) {
|
||||
const parsed = envBindingSchema.safeParse(rawBinding);
|
||||
if (!parsed.success) continue;
|
||||
const binding = parsed.data;
|
||||
const isPlainBinding =
|
||||
typeof binding === "string" ||
|
||||
(typeof binding === "object" && binding !== null && binding.type === "plain");
|
||||
if (isPlainBinding && LOW_TRUST_SENSITIVE_ENV_KEY_RE.test(key)) {
|
||||
throw new HttpError(422, `Low-trust execution cannot use inline sensitive env value ${source}.${key}`, {
|
||||
code: "low_trust_inline_sensitive_env_denied",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveExecutionRunAdapterConfig(input: {
|
||||
companyId: string;
|
||||
agentId?: string | null;
|
||||
@@ -367,10 +399,19 @@ export async function resolveExecutionRunAdapterConfig(input: {
|
||||
projectEnv: unknown;
|
||||
routineEnv?: unknown;
|
||||
secretsSvc: RuntimeConfigSecretResolver;
|
||||
trustPreset?: TrustPresetResolution;
|
||||
}) {
|
||||
const executionRunConfig = stripPaperclipRuntimeEnvFromAdapterConfig(input.executionRunConfig);
|
||||
const projectEnv = stripPaperclipRuntimeEnvBindings(input.projectEnv);
|
||||
const routineEnv = stripPaperclipRuntimeEnvBindings(input.routineEnv);
|
||||
const lowTrustAllowedBindingIds = input.trustPreset?.kind === "low_trust_review"
|
||||
? input.trustPreset.boundary.allowedSecretBindingIds ?? []
|
||||
: undefined;
|
||||
if (input.trustPreset?.kind === "low_trust_review") {
|
||||
assertLowTrustEnvConfigAllowed(executionRunConfig.env, "agent.env");
|
||||
assertLowTrustEnvConfigAllowed(projectEnv, "project.env");
|
||||
assertLowTrustEnvConfigAllowed(routineEnv, "routine.env");
|
||||
}
|
||||
const { config: resolvedConfig, secretKeys, manifest } = await input.secretsSvc.resolveAdapterConfigForRuntime(
|
||||
input.companyId,
|
||||
executionRunConfig,
|
||||
@@ -382,6 +423,7 @@ export async function resolveExecutionRunAdapterConfig(input: {
|
||||
actorId: input.agentId,
|
||||
issueId: input.issueId ?? null,
|
||||
heartbeatRunId: input.heartbeatRunId ?? null,
|
||||
...(lowTrustAllowedBindingIds !== undefined ? { allowedBindingIds: lowTrustAllowedBindingIds } : {}),
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
@@ -397,6 +439,7 @@ export async function resolveExecutionRunAdapterConfig(input: {
|
||||
actorId: input.agentId ?? null,
|
||||
issueId: input.issueId ?? null,
|
||||
heartbeatRunId: input.heartbeatRunId ?? null,
|
||||
...(lowTrustAllowedBindingIds !== undefined ? { allowedBindingIds: lowTrustAllowedBindingIds } : {}),
|
||||
}
|
||||
: undefined,
|
||||
)
|
||||
@@ -422,6 +465,7 @@ export async function resolveExecutionRunAdapterConfig(input: {
|
||||
actorId: input.agentId ?? null,
|
||||
issueId: input.issueId ?? null,
|
||||
heartbeatRunId: input.heartbeatRunId ?? null,
|
||||
...(lowTrustAllowedBindingIds !== undefined ? { allowedBindingIds: lowTrustAllowedBindingIds } : {}),
|
||||
}
|
||||
: undefined,
|
||||
)
|
||||
@@ -710,6 +754,79 @@ function buildExecutionWorkspaceConfigSnapshot(
|
||||
return hasSnapshot ? snapshot : null;
|
||||
}
|
||||
|
||||
export function stripHostWorkspaceProvisionForLowTrustSandbox(input: {
|
||||
config: Record<string, unknown>;
|
||||
trustPreset: TrustPresetResolution;
|
||||
selectedEnvironmentDriver: string | null | undefined;
|
||||
}): Record<string, unknown> {
|
||||
if (input.trustPreset.kind !== "low_trust_review") return input.config;
|
||||
if (input.selectedEnvironmentDriver !== "sandbox") return input.config;
|
||||
|
||||
const workspaceStrategy = parseObject(input.config.workspaceStrategy);
|
||||
if (typeof workspaceStrategy.provisionCommand !== "string") return input.config;
|
||||
|
||||
const nextWorkspaceStrategy = { ...workspaceStrategy };
|
||||
delete nextWorkspaceStrategy.provisionCommand;
|
||||
|
||||
return {
|
||||
...input.config,
|
||||
workspaceStrategy: nextWorkspaceStrategy,
|
||||
};
|
||||
}
|
||||
|
||||
export async function preflightLowTrustWorkspaceIsolation(input: {
|
||||
db?: Db;
|
||||
trustPreset: TrustPresetResolution;
|
||||
isolatedWorkspacesEnabled: boolean;
|
||||
effectiveExecutionWorkspaceMode: string | null | undefined;
|
||||
issue: { companyId: string; id?: string | null; projectId?: string | null } | null;
|
||||
resolveSelectedEnvironmentDriver: () => Promise<string | null | undefined>;
|
||||
}): Promise<string | null> {
|
||||
if (input.trustPreset.kind !== "denied" && input.trustPreset.kind !== "low_trust_review") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedEnvironmentDriver =
|
||||
input.trustPreset.kind === "low_trust_review"
|
||||
? await input.resolveSelectedEnvironmentDriver()
|
||||
: null;
|
||||
|
||||
await assertLowTrustWorkspaceIsolation({
|
||||
db: input.db,
|
||||
resolution: input.trustPreset,
|
||||
isolatedWorkspacesEnabled: input.isolatedWorkspacesEnabled,
|
||||
effectiveExecutionWorkspaceMode: input.effectiveExecutionWorkspaceMode,
|
||||
selectedEnvironmentDriver,
|
||||
issue: input.issue,
|
||||
});
|
||||
|
||||
return selectedEnvironmentDriver ?? null;
|
||||
}
|
||||
|
||||
export async function resolveWorkspaceAfterLowTrustPreflight<TWorkspace>(input: {
|
||||
db?: Db;
|
||||
trustPreset: TrustPresetResolution;
|
||||
isolatedWorkspacesEnabled: boolean;
|
||||
effectiveExecutionWorkspaceMode: string | null | undefined;
|
||||
issue: { companyId: string; id?: string | null; projectId?: string | null } | null;
|
||||
resolveSelectedEnvironmentDriver: () => Promise<string | null | undefined>;
|
||||
resolveWorkspace: () => Promise<TWorkspace>;
|
||||
}): Promise<{ selectedEnvironmentDriver: string | null; workspace: TWorkspace }> {
|
||||
const selectedEnvironmentDriver = await preflightLowTrustWorkspaceIsolation({
|
||||
db: input.db,
|
||||
trustPreset: input.trustPreset,
|
||||
isolatedWorkspacesEnabled: input.isolatedWorkspacesEnabled,
|
||||
effectiveExecutionWorkspaceMode: input.effectiveExecutionWorkspaceMode,
|
||||
issue: input.issue,
|
||||
resolveSelectedEnvironmentDriver: input.resolveSelectedEnvironmentDriver,
|
||||
});
|
||||
|
||||
return {
|
||||
selectedEnvironmentDriver,
|
||||
workspace: await input.resolveWorkspace(),
|
||||
};
|
||||
}
|
||||
|
||||
function deriveRepoNameFromRepoUrl(repoUrl: string | null): string | null {
|
||||
const trimmed = repoUrl?.trim() ?? "";
|
||||
if (!trimmed) return null;
|
||||
@@ -2050,6 +2167,7 @@ export async function buildPaperclipWakePayload(input: {
|
||||
key: string;
|
||||
title: string | null;
|
||||
body: string;
|
||||
sourceTrust?: SourceTrustMetadata | null;
|
||||
updatedAt: Date;
|
||||
}
|
||||
| null;
|
||||
@@ -2061,8 +2179,11 @@ export async function buildPaperclipWakePayload(input: {
|
||||
status: string;
|
||||
priority: string;
|
||||
workMode: string;
|
||||
projectId?: string | null;
|
||||
executionPolicy?: unknown;
|
||||
}
|
||||
| null;
|
||||
exposeLowTrustRaw?: boolean;
|
||||
}) {
|
||||
const executionStage = parseObject(input.contextSnapshot.executionStage);
|
||||
const commentIds = extractWakeCommentIds(input.contextSnapshot);
|
||||
@@ -2105,6 +2226,7 @@ export async function buildPaperclipWakePayload(input: {
|
||||
deletedByAgentId: issueComments.deletedByAgentId,
|
||||
deletedByUserId: issueComments.deletedByUserId,
|
||||
deletedByRunId: issueComments.deletedByRunId,
|
||||
sourceTrust: issueComments.sourceTrust,
|
||||
createdAt: issueComments.createdAt,
|
||||
})
|
||||
.from(issueComments)
|
||||
@@ -2120,6 +2242,10 @@ export async function buildPaperclipWakePayload(input: {
|
||||
let remainingBodyChars = MAX_INLINE_WAKE_COMMENT_BODY_TOTAL_CHARS;
|
||||
let truncated = false;
|
||||
let missingCommentCount = 0;
|
||||
const safeContinuationSummary =
|
||||
continuationSummary && !input.exposeLowTrustRaw
|
||||
? redactQuarantinedBodyForHigherTrust(continuationSummary)
|
||||
: continuationSummary;
|
||||
|
||||
for (const commentId of commentIds) {
|
||||
const row = commentsById.get(commentId);
|
||||
@@ -2134,7 +2260,8 @@ export async function buildPaperclipWakePayload(input: {
|
||||
}
|
||||
|
||||
const deletedAt = row.deletedAt ?? null;
|
||||
const fullBody = deletedAt ? "" : row.body;
|
||||
const safeRow = deletedAt || input.exposeLowTrustRaw ? row : sanitizeQuarantinedCommentForHigherTrust(row);
|
||||
const fullBody = deletedAt ? "" : safeRow.body;
|
||||
const allowedBodyChars = Math.min(MAX_INLINE_WAKE_COMMENT_BODY_CHARS, remainingBodyChars);
|
||||
if (allowedBodyChars <= 0) {
|
||||
truncated = true;
|
||||
@@ -2152,13 +2279,14 @@ export async function buildPaperclipWakePayload(input: {
|
||||
authorType: row.authorType ?? (row.authorAgentId ? "agent" : row.authorUserId ? "user" : "system"),
|
||||
body,
|
||||
bodyTruncated,
|
||||
presentation: deletedAt ? null : row.presentation ?? null,
|
||||
metadata: deletedAt ? null : row.metadata ?? null,
|
||||
presentation: deletedAt ? null : safeRow.presentation ?? null,
|
||||
metadata: deletedAt ? null : safeRow.metadata ?? null,
|
||||
deletedAt: deletedAt ? deletedAt.toISOString() : null,
|
||||
deletedByType: deletedAt ? row.deletedByType ?? null : null,
|
||||
deletedByAgentId: deletedAt ? row.deletedByAgentId ?? null : null,
|
||||
deletedByUserId: deletedAt ? row.deletedByUserId ?? null : null,
|
||||
deletedByRunId: deletedAt ? row.deletedByRunId ?? null : null,
|
||||
sourceTrust: row.sourceTrust ?? null,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
author: row.authorAgentId
|
||||
? { type: "agent", id: row.authorAgentId }
|
||||
@@ -2261,16 +2389,17 @@ export async function buildPaperclipWakePayload(input: {
|
||||
? input.contextSnapshot.unresolvedBlockerSummaries
|
||||
: [],
|
||||
executionStage: Object.keys(executionStage).length > 0 ? executionStage : null,
|
||||
continuationSummary: continuationSummary
|
||||
continuationSummary: safeContinuationSummary
|
||||
? {
|
||||
key: continuationSummary.key,
|
||||
title: continuationSummary.title,
|
||||
key: safeContinuationSummary.key,
|
||||
title: safeContinuationSummary.title,
|
||||
body:
|
||||
continuationSummary.body.length > 4_000
|
||||
? continuationSummary.body.slice(0, 4_000)
|
||||
: continuationSummary.body,
|
||||
bodyTruncated: continuationSummary.body.length > 4_000,
|
||||
updatedAt: continuationSummary.updatedAt.toISOString(),
|
||||
safeContinuationSummary.body.length > 4_000
|
||||
? safeContinuationSummary.body.slice(0, 4_000)
|
||||
: safeContinuationSummary.body,
|
||||
bodyTruncated: safeContinuationSummary.body.length > 4_000,
|
||||
sourceTrust: safeContinuationSummary.sourceTrust ?? null,
|
||||
updatedAt: safeContinuationSummary.updatedAt.toISOString(),
|
||||
}
|
||||
: null,
|
||||
commentIds,
|
||||
@@ -2768,6 +2897,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
executionWorkspacePreference: issues.executionWorkspacePreference,
|
||||
assigneeAgentId: issues.assigneeAgentId,
|
||||
assigneeAdapterOverrides: issues.assigneeAdapterOverrides,
|
||||
executionPolicy: issues.executionPolicy,
|
||||
executionWorkspaceSettings: issues.executionWorkspaceSettings,
|
||||
originKind: issues.originKind,
|
||||
originId: issues.originId,
|
||||
@@ -7248,6 +7378,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
deletedByAgentId: issueComments.deletedByAgentId,
|
||||
deletedByUserId: issueComments.deletedByUserId,
|
||||
deletedByRunId: issueComments.deletedByRunId,
|
||||
sourceTrust: issueComments.sourceTrust,
|
||||
})
|
||||
.from(issueComments)
|
||||
.where(and(
|
||||
@@ -7326,6 +7457,25 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
parseProjectExecutionWorkspacePolicy(projectContext?.executionWorkspacePolicy),
|
||||
isolatedWorkspacesEnabled,
|
||||
);
|
||||
const trustPreset = resolveCoreTrustPreset({
|
||||
companyId: agent.companyId,
|
||||
agent: {
|
||||
companyId: agent.companyId,
|
||||
permissions: agent.permissions,
|
||||
},
|
||||
project: projectContext
|
||||
? {
|
||||
companyId: agent.companyId,
|
||||
executionWorkspacePolicy: projectExecutionWorkspacePolicy,
|
||||
}
|
||||
: null,
|
||||
issue: issueContext
|
||||
? {
|
||||
companyId: agent.companyId,
|
||||
executionPolicy: issueContext.executionPolicy,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
const taskSession = taskKey
|
||||
? await getTaskSession(agent.companyId, agent.id, agent.adapterType, taskKey)
|
||||
: null;
|
||||
@@ -7351,17 +7501,15 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
sessionCodec.deserialize(taskSessionForRun?.sessionParamsJson ?? null),
|
||||
);
|
||||
const config = parseObject(agent.adapterConfig);
|
||||
const requestedExecutionWorkspaceMode = resolveExecutionWorkspaceMode({
|
||||
const resolvedExecutionWorkspaceMode = resolveExecutionWorkspaceMode({
|
||||
projectPolicy: projectExecutionWorkspacePolicy,
|
||||
issueSettings: issueExecutionWorkspaceSettings,
|
||||
legacyUseProjectWorkspace: issueAssigneeOverrides?.useProjectWorkspace ?? null,
|
||||
});
|
||||
const resolvedWorkspace = await resolveWorkspaceForRun(
|
||||
agent,
|
||||
context,
|
||||
previousSessionParams,
|
||||
{ useProjectWorkspace: requestedExecutionWorkspaceMode !== "agent_default" },
|
||||
);
|
||||
const requestedExecutionWorkspaceMode =
|
||||
trustPreset.kind === "low_trust_review" && resolvedExecutionWorkspaceMode === "shared_workspace"
|
||||
? "isolated_workspace"
|
||||
: resolvedExecutionWorkspaceMode;
|
||||
const issueRef = issueContext
|
||||
? {
|
||||
id: issueContext.id,
|
||||
@@ -7380,12 +7528,22 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
const continuationSummary = issueRef
|
||||
? await getIssueContinuationSummaryDocument(db, issueRef.id)
|
||||
: null;
|
||||
const exposeLowTrustRaw = trustPreset.kind === "low_trust_review";
|
||||
const safeContinuationSummary =
|
||||
continuationSummary && !exposeLowTrustRaw
|
||||
? redactQuarantinedBodyForHigherTrust(continuationSummary)
|
||||
: continuationSummary;
|
||||
const safeWakeCommentContext =
|
||||
wakeCommentContext && !exposeLowTrustRaw
|
||||
? sanitizeQuarantinedCommentForHigherTrust(wakeCommentContext)
|
||||
: wakeCommentContext;
|
||||
if (continuationSummary) {
|
||||
context.paperclipContinuationSummary = {
|
||||
key: continuationSummary.key,
|
||||
title: continuationSummary.title,
|
||||
body: continuationSummary.body,
|
||||
updatedAt: continuationSummary.updatedAt.toISOString(),
|
||||
key: safeContinuationSummary!.key,
|
||||
title: safeContinuationSummary!.title,
|
||||
body: safeContinuationSummary!.body,
|
||||
sourceTrust: safeContinuationSummary!.sourceTrust ?? null,
|
||||
updatedAt: safeContinuationSummary!.updatedAt.toISOString(),
|
||||
};
|
||||
} else {
|
||||
delete context.paperclipContinuationSummary;
|
||||
@@ -7403,8 +7561,11 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
status: issueRef.status,
|
||||
priority: issueRef.priority,
|
||||
workMode: issueRef.workMode,
|
||||
projectId: issueRef.projectId,
|
||||
executionPolicy: issueContext?.executionPolicy ?? null,
|
||||
}
|
||||
: null,
|
||||
exposeLowTrustRaw,
|
||||
});
|
||||
if (paperclipWakePayload) {
|
||||
context[PAPERCLIP_WAKE_PAYLOAD_KEY] = paperclipWakePayload;
|
||||
@@ -7421,7 +7582,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
description: issueRef.description,
|
||||
}
|
||||
: null,
|
||||
wakeComment: wakeCommentContext,
|
||||
wakeComment: safeWakeCommentContext,
|
||||
interaction: {
|
||||
kind: readNonEmptyString(context.interactionKind),
|
||||
status: readNonEmptyString(context.interactionStatus),
|
||||
@@ -7442,7 +7603,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
delete context.paperclipIssue;
|
||||
}
|
||||
if (wakeCommentContext) {
|
||||
context.paperclipWakeComment = wakeCommentContext;
|
||||
context.paperclipWakeComment = safeWakeCommentContext;
|
||||
} else {
|
||||
delete context.paperclipWakeComment;
|
||||
}
|
||||
@@ -7504,6 +7665,37 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
? persistedExecutionWorkspaceMode
|
||||
: requestedExecutionWorkspaceMode;
|
||||
const selectedEnvironmentId = environmentResolution.environmentId;
|
||||
const {
|
||||
selectedEnvironmentDriver: lowTrustPreflightEnvironmentDriver,
|
||||
workspace: resolvedWorkspace,
|
||||
} = await resolveWorkspaceAfterLowTrustPreflight({
|
||||
db,
|
||||
trustPreset,
|
||||
isolatedWorkspacesEnabled,
|
||||
effectiveExecutionWorkspaceMode,
|
||||
issue: issueRef
|
||||
? {
|
||||
companyId: agent.companyId,
|
||||
id: issueRef.id,
|
||||
projectId: issueRef.projectId,
|
||||
}
|
||||
: null,
|
||||
resolveSelectedEnvironmentDriver: async () => {
|
||||
const preflightEnvironment = await envOrchestrator.resolveEnvironment({
|
||||
companyId: agent.companyId,
|
||||
selectedEnvironmentId,
|
||||
defaultEnvironmentId: defaultEnvironment.id,
|
||||
});
|
||||
return preflightEnvironment.driver;
|
||||
},
|
||||
resolveWorkspace: () =>
|
||||
resolveWorkspaceForRun(
|
||||
agent,
|
||||
context,
|
||||
previousSessionParams,
|
||||
{ useProjectWorkspace: requestedExecutionWorkspaceMode !== "agent_default" },
|
||||
),
|
||||
});
|
||||
const workspaceManagedConfig = shouldReuseExisting
|
||||
? { ...config }
|
||||
: buildExecutionWorkspaceAdapterConfig({
|
||||
@@ -7567,6 +7759,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
projectEnv: projectContext?.env ?? null,
|
||||
routineEnv: routineEnvContext.env,
|
||||
secretsSvc,
|
||||
trustPreset,
|
||||
});
|
||||
if (secretManifest.length > 0) {
|
||||
context.paperclipSecrets = {
|
||||
@@ -7589,6 +7782,11 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
...effectiveResolvedConfig,
|
||||
paperclipRuntimeSkills: runtimeSkillEntries,
|
||||
};
|
||||
const hostExecutionWorkspaceConfig = stripHostWorkspaceProvisionForLowTrustSandbox({
|
||||
config: runtimeConfig,
|
||||
trustPreset,
|
||||
selectedEnvironmentDriver: lowTrustPreflightEnvironmentDriver,
|
||||
});
|
||||
const workspaceOperationRecorder = workspaceOperationsSvc.createRecorder({
|
||||
companyId: agent.companyId,
|
||||
heartbeatRunId: run.id,
|
||||
@@ -7637,7 +7835,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
: null;
|
||||
const executionWorkspace = reusedExecutionWorkspace ?? await realizeExecutionWorkspace({
|
||||
base: executionWorkspaceBase,
|
||||
config: runtimeConfig,
|
||||
config: hostExecutionWorkspaceConfig,
|
||||
issue: issueRef,
|
||||
agent: {
|
||||
id: agent.id,
|
||||
@@ -7902,6 +8100,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||
)
|
||||
: [];
|
||||
})();
|
||||
assertLowTrustRuntimeServicesAllowed({
|
||||
resolution: trustPreset,
|
||||
runtimeServiceCount: runtimeServiceIntents.length,
|
||||
});
|
||||
if (runtimeServiceIntents.length > 0) {
|
||||
context.paperclipRuntimeServiceIntents = runtimeServiceIntents;
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user