From 2922f65ba1544aeb815b070d756d08f39b8543e1 Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 8 Oct 2026 02:52:40 -0500 Subject: [PATCH] fix(hermes): pin v7 permission policy in the independent server launch Co-Authored-By: Paperclip --- .../native-runtime/hermes-connection.test.ts | 25 +++++++++++++++++++ .../native-runtime/native-execution-input.ts | 9 +++++++ .../native-runtime/native-session-executor.ts | 6 ++--- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/server/src/services/native-runtime/hermes-connection.test.ts b/server/src/services/native-runtime/hermes-connection.test.ts index 7f7fa2f6b85..4b40c9a51e4 100644 --- a/server/src/services/native-runtime/hermes-connection.test.ts +++ b/server/src/services/native-runtime/hermes-connection.test.ts @@ -1,4 +1,7 @@ import { describe, expect, it } from "vitest"; +import { buildNativeExecutionInput, isNativeAcpxPermissionModePinned } from "./native-execution-input.js"; +import { nativeRuntimeContextFixture } from "./runtime-context.test-fixture.js"; +import { parseNativeExecutionInput } from "../../vendor/paperclip-runner/index.js"; import { projectHermesConnection, restoreHermesCredential } from "./hermes-connection.js"; import type { AiConnectionMetadata } from "@paperclipai/shared"; describe("Hermes Connections projection", () => { @@ -63,3 +66,25 @@ describe("Hermes Connections projection", () => { expect(() => restoreHermesCredential("xai", original, JSON.stringify(refreshed))).toThrow("account identity"); }); }); + +// Use the server constructor and the same policy projection as its transport factory. +describe("server-launched Hermes permission policy", () => { + it.each(["approve-all", "approve-reads"] as const)("pins v7 %s policy before native transport admission", permissionMode => { + const input = buildNativeExecutionInput({ + companyId: "company", runId: "run", agentId: "agent", + issue: { id: "issue", identifier: "HERMES-1", title: "Server launch", description: null, workMode: "standard" }, + taskPrompt: "Run the task.", workspace: { id: "workspace", cwd: "/workspace", repoUrl: null, repoRef: null, branchName: null }, + normalizedSessionId: null, provider: "acpx", acpxAgent: "hermes", model: "hermes-fixture", + hermesConnectionFingerprint: "1".repeat(64), acpxPermissionMode: permissionMode, + completionContract: { id: "contract", sha256: "a".repeat(64), schemaVersion: "paperclip.run-result.v1", + contract: { revision: "1", objective: "Run the task.", criteria: [{ id: "output", requirement: "Run the task." }] } }, + runtimeContext: nativeRuntimeContextFixture(), + }); + expect(input.schema).toBe("paperclip.native-execution-input.v7"); + expect(input.provider).toMatchObject({ kind: "acpx", agent: "hermes", permissionMode }); + expect(isNativeAcpxPermissionModePinned(input)).toBe(true); + expect(isNativeAcpxPermissionModePinned(parseNativeExecutionInput({ ...input, schema: "paperclip.native-execution-input.v6" }))).toBe(true); + expect(isNativeAcpxPermissionModePinned({ ...input, schema: "paperclip.native-execution-input.v3" })).toBe(false); + expect(isNativeAcpxPermissionModePinned({ ...input, provider: { kind: "codex", model: null, approvalPolicy: "never" } })).toBe(false); + }); +}); diff --git a/server/src/services/native-runtime/native-execution-input.ts b/server/src/services/native-runtime/native-execution-input.ts index 064b9d9d30e..5af43fa1dc3 100644 --- a/server/src/services/native-runtime/native-execution-input.ts +++ b/server/src/services/native-runtime/native-execution-input.ts @@ -6,6 +6,7 @@ import type { NativeAcpxAgent, NativeAcpxPermissionMode, NativeCodexApprovalPolicy, + NativeExecutionInput, NativeExecutionInputV5, NativeExecutionInputV6, NativeExecutionInputV7, @@ -33,6 +34,14 @@ const NATIVE_GITHUB_ATTACHMENT_RECOVERY_GUIDANCE = [ "A task URL missing from your prompt or tool results is not evidence that no task link can be provided; do not claim that a link is unavailable merely because you cannot see its URL. Do not invent a URL or promise that a link will appear. Briefly explain the unavailable input and ask the user to attach it directly to this Paperclip task or paste the needed text. Never infer the file's contents or substitute an older file.", ].join("\n"); +/** Persisted provider policy is authoritative only in the explicit permission contracts. */ +export function isNativeAcpxPermissionModePinned(input: Pick): boolean { + return input.provider.kind === "acpx" && [ + "paperclip.native-execution-input.v4", "paperclip.native-execution-input.v5", + "paperclip.native-execution-input.v6", "paperclip.native-execution-input.v7", + ].includes(input.schema); +} + /** Closed constructor: callers cannot spread legacy context or environment data. */ export interface BuildNativeExecutionInput { companyId: string; diff --git a/server/src/services/native-runtime/native-session-executor.ts b/server/src/services/native-runtime/native-session-executor.ts index 1dee6fb689f..7f082108c2b 100644 --- a/server/src/services/native-runtime/native-session-executor.ts +++ b/server/src/services/native-runtime/native-session-executor.ts @@ -1,3 +1,4 @@ +import { isNativeAcpxPermissionModePinned } from "./native-execution-input.js"; import { agents } from "@paperclipai/db"; import { dotRunnerBroker } from "../dot-runner-broker.js"; import { configuredEnvironment } from "../../vendor/paperclip-runner/index.js"; @@ -12961,10 +12962,7 @@ async function createRunnerdBackendWithinSessionClaim( acpxCandidateProfile: resolveAcpxQualification(input.execution.provider, process.env), acpxPermissionMode: input.execution.provider.permissionMode, acpxMode: input.execution.provider.mode, - acpxPermissionModePinned: - input.execution.schema === "paperclip.native-execution-input.v4" || - input.execution.schema === "paperclip.native-execution-input.v5" || - input.execution.schema === "paperclip.native-execution-input.v6", + acpxPermissionModePinned: isNativeAcpxPermissionModePinned(input.execution), acpxRuntimeDirectory: remoteRunnerFilesystemRoot ? posix.join(remoteRunnerFilesystemRoot, "acpx") : resolve(