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:
Dotta
2026-06-05 11:48:02 -10:00
committed by GitHub
parent b2a33d0184
commit dbebf30c89
87 changed files with 6522 additions and 121 deletions
+227 -25
View File
@@ -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 {