mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 02:26:12 +02:00
Add dynamic secret runtime posture labeling
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
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: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
? {
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user