Add dynamic secret runtime posture labeling

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Nicky LeachandPaperclip committed 2026-06-25 22:20:06 +00:00
1 parent d2492cf628
commit 9b5a735bb6
8 files changed
+197 -2

No files matched your search

@@ -24,6 +24,10 @@ const manifest: PaperclipPluginManifestV1 = {
description:
"Provisions Daytona sandboxes with configurable image or snapshot selection, startup timeouts, and lease reuse.",
supportsReusableLeases: true,
isolation: {
kind: "container",
hardBoundary: true,
},
configSchema: {
type: "object",
properties: {
+9
View File
@@ -144,6 +144,15 @@ export interface PluginEnvironmentDriverDeclaration {
* behavior even if their config schema exposes a reuse-like setting.
*/
supportsReusableLeases?: boolean;
/**
* Optional isolation claim used by the host to decide whether runtime secret
* material may be described as protected by a hard boundary. Omit this unless
* the driver actually runs work behind the declared boundary.
*/
isolation?: {
kind: "separate_uid" | "namespace" | "container" | "vm" | "remote";
hardBoundary?: boolean;
};
/** JSON Schema describing the driver's provider-specific configuration. */
configSchema: JsonSchema;
}
+4
View File
@@ -126,6 +126,10 @@ export const pluginEnvironmentDriverDeclarationSchema = z.object({
displayName: z.string().min(1).max(100),
description: z.string().max(500).optional(),
supportsReusableLeases: z.boolean().optional(),
isolation: z.object({
kind: z.enum(["separate_uid", "namespace", "container", "vm", "remote"]),
hardBoundary: z.boolean().optional(),
}).optional(),
configSchema: jsonSchemaSchema,
});
@@ -277,6 +277,83 @@ describeEmbeddedPostgres("environmentRuntimeService", () => {
};
}
it("reports hard secret posture only for plugin-backed sandbox providers with a hard isolation claim", async () => {
await db.insert(plugins).values({
id: randomUUID(),
pluginKey: "acme.hard-sandbox-provider",
packageName: "@acme/hard-sandbox-provider",
version: "1.0.0",
apiVersion: 1,
categories: ["automation"],
manifestJson: {
id: "acme.hard-sandbox-provider",
apiVersion: 1,
version: "1.0.0",
displayName: "Hard Sandbox Provider",
description: "Test provider with a declared container boundary",
author: "Paperclip",
categories: ["automation"],
capabilities: ["environment.drivers.register"],
entrypoints: { worker: "dist/worker.js" },
environmentDrivers: [
{
driverKey: "hard-plugin",
kind: "sandbox_provider",
displayName: "Hard Plugin",
isolation: {
kind: "container",
hardBoundary: true,
},
configSchema: {
type: "object",
properties: {
image: { type: "string" },
},
},
},
],
},
status: "ready",
installOrder: 1,
config: {},
createdAt: new Date(),
updatedAt: new Date(),
});
const { environment } = await seedEnvironment({
driver: "sandbox",
config: {
provider: "hard-plugin",
image: "paperclip:test",
},
});
await expect(runtime.resolveSecretPosture(environment)).resolves.toBe("hard");
});
it("reports soft secret posture for the built-in fake sandbox provider", async () => {
const { environment } = await seedEnvironment({
driver: "sandbox",
config: {
provider: "fake",
image: "ubuntu:24.04",
},
});
await expect(runtime.resolveSecretPosture(environment)).resolves.toBe("soft");
});
it("defaults unknown sandbox providers to soft secret posture", async () => {
const { environment } = await seedEnvironment({
driver: "sandbox",
config: {
provider: "missing-plugin",
image: "paperclip:test",
},
});
await expect(runtime.resolveSecretPosture(environment)).resolves.toBe("soft");
});
async function seedReusablePluginSandboxLease() {
const pluginId = randomUUID();
const { companyId, agentId, environment: baseEnvironment, runId } = await seedEnvironment();
@@ -118,6 +118,7 @@ describe("resolveExecutionRunAdapterConfig", () => {
"secret-project",
"secret-routine",
]);
expect(result.secretManifest.every((entry) => entry.posture === "soft")).toBe(true);
expect(JSON.stringify(result.secretManifest)).not.toContain("agent-only");
expect(JSON.stringify(result.secretManifest)).not.toContain("environment-only");
expect(JSON.stringify(result.secretManifest)).not.toContain("project-only");
@@ -132,6 +133,44 @@ describe("resolveExecutionRunAdapterConfig", () => {
});
});
it("labels runtime secret manifest entries with the selected environment posture", async () => {
const resolveAdapterConfigForRuntime = vi.fn().mockResolvedValue({
config: { env: { AGENT_SECRET: "resolved" } },
secretKeys: new Set(["AGENT_SECRET"]),
manifest: [
{
configPath: "env.AGENT_SECRET",
envKey: "AGENT_SECRET",
secretId: "secret-agent",
secretKey: "agent-secret",
version: 1,
provider: "local_encrypted",
posture: "soft",
outcome: "success",
},
],
});
const result = await resolveExecutionRunAdapterConfig({
companyId: "company-1",
agentId: "agent-1",
executionRunConfig: { env: { AGENT_SECRET: { type: "secret_ref", secretId: "secret-agent" } } },
projectEnv: null,
secretPosture: "hard",
secretsSvc: {
resolveAdapterConfigForRuntime,
resolveEnvBindings: vi.fn(),
} as any,
});
expect(result.secretManifest).toEqual([
expect.objectContaining({
secretId: "secret-agent",
posture: "hard",
}),
]);
});
it("drops Paperclip runtime-owned env before resolving environment, agent, project, and routine overlays", async () => {
const resolveAdapterConfigForRuntime = vi.fn(async (_companyId, config: Record<string, unknown>) => ({
config: {
@@ -8,6 +8,7 @@ import type {
EnvironmentLeaseStatus,
ExecutionWorkspace,
PluginEnvironmentConfig,
PluginEnvironmentDriverDeclaration,
SandboxEnvironmentConfig,
} from "@paperclipai/shared";
import type {
@@ -1113,6 +1114,53 @@ function sandboxConfigForLeaseMetadata(config: SandboxEnvironmentConfig): Record
return { ...config };
}
export type RuntimeSecretPosture = "hard" | "soft";
function pluginDriverReportsHardIsolation(
driver: PluginEnvironmentDriverDeclaration | null | undefined,
): boolean {
return driver?.isolation?.hardBoundary === true;
}
async function resolvePluginEnvironmentHardIsolation(input: {
db: Db;
config: PluginEnvironmentConfig;
}): Promise<boolean> {
const plugin = await pluginRegistryService(input.db).getByKey(input.config.pluginKey);
if (!plugin || plugin.status !== "ready") return false;
const driver = plugin.manifestJson.environmentDrivers?.find(
(candidate) => candidate.driverKey === input.config.driverKey,
);
return pluginDriverReportsHardIsolation(driver);
}
export async function resolveEnvironmentRuntimeSecretPosture(input: {
db: Db;
environment: Pick<Environment, "driver" | "config">;
}): Promise<RuntimeSecretPosture> {
try {
const parsed = parseEnvironmentDriverConfig(input.environment as Environment);
if (parsed.driver === "sandbox") {
if (isBuiltinSandboxProvider(parsed.config.provider)) return "soft";
const resolved = await resolvePluginSandboxProviderDriverByKey({
db: input.db,
driverKey: parsed.config.provider,
requireRunning: false,
});
return pluginDriverReportsHardIsolation(resolved?.driver) ? "hard" : "soft";
}
if (parsed.driver === "plugin") {
return await resolvePluginEnvironmentHardIsolation({
db: input.db,
config: parsed.config,
}) ? "hard" : "soft";
}
} catch {
return "soft";
}
return "soft";
}
function tryParseCurrentPluginConfig(environment: Environment): PluginEnvironmentConfig | null {
if (environment.driver !== "plugin") {
return null;
@@ -1438,6 +1486,10 @@ export function environmentRuntimeService(
return {
getDriver,
async resolveSecretPosture(environment: Pick<Environment, "driver" | "config">): Promise<RuntimeSecretPosture> {
return await resolveEnvironmentRuntimeSecretPosture({ db, environment });
},
async acquireRunLease(input: {
companyId: string;
environment: Environment;
+8 -2
View File
@@ -192,7 +192,7 @@ import {
import { extractSkillMentionIds, isUuidLike } from "@paperclipai/shared";
import { environmentService } from "./environments.js";
import { parseExecutionPolicyBootstrapEnv } from "./execution-policy-bootstrap.js";
import { environmentRuntimeService } from "./environment-runtime.js";
import { environmentRuntimeService, type RuntimeSecretPosture } from "./environment-runtime.js";
import { skillVersionSelectionMap } from "./runtime-skill-selections.js";
import { environmentRunOrchestrator } from "./environment-run-orchestrator.js";
import { isUnsafeSessionWorkspaceCwd } from "./session-workspace-cwd.js";
@@ -504,6 +504,7 @@ export async function resolveExecutionRunAdapterConfig(input: {
projectEnv: unknown;
routineEnv?: unknown;
secretsSvc: RuntimeConfigSecretResolver;
secretPosture?: RuntimeSecretPosture;
trustPreset?: TrustPresetResolution;
requiredScopedEnvBinding?: {
keys: string[];
@@ -728,6 +729,7 @@ export async function resolveExecutionRunAdapterConfig(input: {
secretKeys.add(key);
}
}
const secretPosture = input.secretPosture ?? "soft";
return {
resolvedConfig,
secretKeys,
@@ -736,7 +738,7 @@ export async function resolveExecutionRunAdapterConfig(input: {
...(manifest ?? []),
...(projectEnvResolution.manifest ?? []),
...(routineEnvResolution.manifest ?? []),
],
].map((entry) => ({ ...entry, posture: secretPosture })),
};
}
@@ -8905,6 +8907,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
: selectedEnvironmentId
? await environmentsSvc.getById(selectedEnvironmentId)
: null;
const runtimeSecretPosture = selectedEnvironmentForConfig
? await environmentRuntime.resolveSecretPosture(selectedEnvironmentForConfig)
: "soft";
const runScopedMentionedSkillKeys = await resolveRunScopedMentionedSkillKeys({
db,
companyId: agent.companyId,
@@ -8928,6 +8933,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
projectEnv: projectContext?.env ?? null,
routineEnv: routineEnvContext.env,
secretsSvc,
secretPosture: runtimeSecretPosture,
trustPreset,
requiredScopedEnvBinding: pushCapabilityPreflightRequired
? {
+4
View File
@@ -246,6 +246,7 @@ export type RuntimeSecretManifestEntry = {
secretKey: string;
version: number;
provider: SecretProvider;
posture: "hard" | "soft";
outcome: "success" | "failure";
errorCode?: string | null;
};
@@ -819,6 +820,7 @@ export function secretService(db: Db) {
secretKey: secret.key,
version: resolvedVersion,
provider: providerId,
posture: "soft",
outcome: "success",
},
};
@@ -864,6 +866,7 @@ export function secretService(db: Db) {
secretKey: input.secret.key,
version: 0,
provider: input.secret.provider,
posture: "soft",
outcome: "success",
},
};
@@ -915,6 +918,7 @@ export function secretService(db: Db) {
secretKey: input.secret.key,
version: 0,
provider: input.secret.provider,
posture: "soft",
outcome: "success",
},
};