mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 10:48:12 +02:00
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work.
> - The native runner exposes tools for company tasks.
> - API search and call tools cover operations without a dedicated tool.
> - The current default hides these tools unless an operator sets an
environment variable.
> - This pull request enables the tools when that variable is absent.
> - Operators can still disable the tools or restrict them to selected
companies.
## Linked Issues or Issue Description
Refs #13003, which added the guarded API tools.
**What happened?**
The native runner does not advertise `search_api` or `call_api` with the
default server configuration.
**Expected behavior**
The tools are available without a special environment variable. Existing
authorization checks still apply.
**Steps to reproduce**
Remove `PAPERCLIP_RUNNER_API_TOOLS_ENABLED` and
`PAPERCLIP_RUNNER_API_TOOLS_COMPANY_IDS`. Create a normal runner
authority. Inspect its tool definitions.
**Paperclip version or commit**
a6c4e7a.
## What Changed
- Enable API tools when the server flag is absent.
- Keep explicit disable, invalid-value rejection, company restrictions,
and binding restrictions.
- Test default tool definitions and run HTTP integration tests without
the enabling flag.
- Update operator and hiring documentation. The existing shared gate
also controls `hire_agent`.
- Use the same policy in the E2E evidence summary so an unset flag is
not reported as disabled.
- Isolate default-availability tests from operator environment
variables.
## Verification
- Red test: two new rollout policy assertions failed before the fix.
- Focused policy, authority, and HTTP tests: 3 files passed; 41 tests
passed, 2 skipped. The two runnerd transport cases require a Rust-built
binary absent from this workspace.
- Command: `pnpm exec vitest run
server/src/services/native-runtime/runner-api-rollout.test.ts
server/src/services/native-runtime/paperclip-runner-tool-authority.test.ts
server/src/services/native-runtime/runner-api.integration.test.ts`.
- Attempted `pnpm -r typecheck`: blocked by missing `cargo` in this
workspace.
- Attempted `pnpm build`: terminated at the 4 GiB memory limit.
- Attempted `pnpm test:run`: stopped after memory pressure to run
focused tests alone.
- Repeated the 41 passing focused tests with an inherited disabled flag
and a foreign-company restriction; test isolation passed.
- `pnpm test:e2e:runner:unit`: 44 files and 543 tests passed.
- `pnpm test:e2e:runner:typecheck` exceeded the workspace memory limit,
including a retry with bounded Go memory settings.
- CI results will be recorded before handoff.
## Risks
- More native runs can discover API tools and the existing `hire_agent`
tool by default.
- The change does not remove company, run, mode, credential, lifecycle,
or approval checks.
- Explicit operator restrictions still take precedence. No database
migration is required.
## Model Used
OpenAI Codex agent. The runtime does not expose the exact model ID or
context-window size. Used reasoning, repository tools, shell execution,
and tests.
## 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 searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
199 lines
6.3 KiB
TypeScript
199 lines
6.3 KiB
TypeScript
import path from "node:path";
|
|
import { chatNeedsApiTools, isManagedHiringCase } from "./chat-cases.js";
|
|
import { CREDENTIAL_NAMES } from "./types.js";
|
|
import type { MatrixExecution } from "./types.js";
|
|
|
|
const DATABASE_KEYS = ["DATABASE_URL", "DATABASE_MIGRATION_URL"] as const;
|
|
const AMBIENT_PAPERCLIP_CREDENTIAL_KEYS = [
|
|
"PAPERCLIP_API_KEY",
|
|
"PAPERCLIP_AGENT_API_KEY",
|
|
"PAPERCLIP_TASK_BRIDGE_TOKEN",
|
|
"PAPERCLIP_SETUP_TOKEN",
|
|
"PAPERCLIP_SECRETS_MASTER_KEY",
|
|
"PAPERCLIP_SECRETS_MASTER_KEY_FILE",
|
|
] as const;
|
|
const GENERATED_SERVER_SECRET_KEYS = [
|
|
"PAPERCLIP_AGENT_JWT_SECRET",
|
|
"PAPERCLIP_DECISION_SIGNING_SECRET",
|
|
"PAPERCLIP_TOOL_ACTION_SIGNING_SECRET",
|
|
"BETTER_AUTH_SECRET",
|
|
] as const;
|
|
const AMBIENT_EXTERNAL_STATE_KEYS = [
|
|
"PAPERCLIP_STORAGE_S3_BUCKET",
|
|
"PAPERCLIP_STORAGE_S3_REGION",
|
|
"PAPERCLIP_STORAGE_S3_ENDPOINT",
|
|
"PAPERCLIP_STORAGE_S3_PREFIX",
|
|
"PAPERCLIP_STORAGE_S3_FORCE_PATH_STYLE",
|
|
] as const;
|
|
const PROVIDER_SECRET_KEY = /^(?:OPENAI|ANTHROPIC|OPENROUTER|DAYTONA)(?:_|$)/;
|
|
|
|
export function runnerE2EServerControlPaths(temporaryRoot: string) {
|
|
const controlDirectory = path.join(temporaryRoot, "control");
|
|
return {
|
|
controlDirectory,
|
|
restartRequestPath: path.join(
|
|
controlDirectory,
|
|
"server-restart.request.json",
|
|
),
|
|
restartAcknowledgementPath: path.join(
|
|
controlDirectory,
|
|
"server-restart.ack.json",
|
|
),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Native cells use the debug binary produced once by build:runner-binaries.
|
|
* Preserve an explicit override for release builds and developer workflows.
|
|
*/
|
|
export function resolvePaperclipRunnerBinaryForHarness(
|
|
executions: readonly MatrixExecution[],
|
|
repositoryRoot: string,
|
|
configuredPath = process.env.PAPERCLIP_RUNNER_BINARY,
|
|
platform: NodeJS.Platform = process.platform,
|
|
): string | undefined {
|
|
if (configuredPath?.trim()) return configuredPath;
|
|
if (
|
|
!executions.some((execution) => execution.profile.generation === "native")
|
|
) {
|
|
return undefined;
|
|
}
|
|
|
|
return path.join(
|
|
repositoryRoot,
|
|
"packages",
|
|
"paperclip-runner",
|
|
"runner",
|
|
"target",
|
|
"debug",
|
|
platform === "win32" ? "paperclip-runnerd.exe" : "paperclip-runnerd",
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Remote native cells stage the same controller-owned binary whose digest is
|
|
* authorized by the PRP control plane. Local cells launch it directly.
|
|
*/
|
|
export function resolvePaperclipRemoteRunnerBinaryForHarness(
|
|
executions: readonly MatrixExecution[],
|
|
runnerBinary: string | undefined,
|
|
configuredPath = process.env.PAPERCLIP_RUNNER_REMOTE_BINARY_PATH,
|
|
platform: NodeJS.Platform = process.platform,
|
|
): string | undefined {
|
|
if (configuredPath?.trim()) return configuredPath;
|
|
if (!runnerBinary) return undefined;
|
|
// Daytona runs Linux. A default debug binary built by a macOS developer is
|
|
// Mach-O and cannot be staged into that sandbox. Leave the remote override
|
|
// unset so the pinned Daytona image's verified runnerd is discovered instead.
|
|
if (platform !== "linux") return undefined;
|
|
return executions.some(
|
|
(execution) =>
|
|
execution.profile.generation === "native" &&
|
|
execution.environment.expectedExecutionTarget.kind === "remote",
|
|
)
|
|
? runnerBinary
|
|
: undefined;
|
|
}
|
|
|
|
/**
|
|
* Keep fixture-only provider switches scoped to the one isolated harness that
|
|
* needs them. In particular, the pinned legacy OpenCode model is routed by the
|
|
* paid gateway and may not appear in OpenCode's public model catalog.
|
|
*/
|
|
export function buildRunnerE2EProcessEnvironment(
|
|
source: NodeJS.ProcessEnv,
|
|
executions: readonly MatrixExecution[],
|
|
): NodeJS.ProcessEnv {
|
|
const result = { ...source };
|
|
// Announcements are unrelated to the scenarios and obscure screenshot evidence.
|
|
result.PAPERCLIP_ANNOUNCEMENTS_ENABLED = "false";
|
|
delete result.OPENCODE_ALLOW_ALL_MODELS;
|
|
// These stories explicitly require the native API surface. Other suites
|
|
// retain the server default or any supplied operator restriction.
|
|
if (executions.some((e) => isManagedHiringCase(e.suite.id, e.task.id) || chatNeedsApiTools(e.suite.id, e.task.id))) {
|
|
result.PAPERCLIP_RUNNER_API_TOOLS_ENABLED = "true";
|
|
}
|
|
if (
|
|
executions.length > 0 &&
|
|
executions.every(
|
|
(execution) =>
|
|
execution.profile.generation === "legacy" &&
|
|
execution.profile.provider === "opencode",
|
|
)
|
|
) {
|
|
result.OPENCODE_ALLOW_ALL_MODELS = "true";
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Build the environment inherited by the Paperclip server. Paid credentials
|
|
* deliberately stay in the launcher/Playwright process and cross the server
|
|
* boundary only once, in the encrypted company-secrets API request.
|
|
*/
|
|
export function buildPaperclipServerEnvironment(
|
|
source: NodeJS.ProcessEnv,
|
|
overrides: NodeJS.ProcessEnv = {},
|
|
): NodeJS.ProcessEnv {
|
|
const result = { ...source };
|
|
for (const key of Object.keys(result)) {
|
|
if (PROVIDER_SECRET_KEY.test(key)) delete result[key];
|
|
}
|
|
for (const key of [
|
|
...CREDENTIAL_NAMES,
|
|
...DATABASE_KEYS,
|
|
...AMBIENT_PAPERCLIP_CREDENTIAL_KEYS,
|
|
...AMBIENT_EXTERNAL_STATE_KEYS,
|
|
]) {
|
|
delete result[key];
|
|
}
|
|
for (const key of GENERATED_SERVER_SECRET_KEYS) delete result[key];
|
|
Object.assign(result, overrides);
|
|
return result;
|
|
}
|
|
|
|
export function assertIsolatedServerEnvironment(
|
|
env: NodeJS.ProcessEnv,
|
|
expected: {
|
|
temporaryRoot: string;
|
|
paperclipHome: string;
|
|
configPath: string;
|
|
},
|
|
) {
|
|
const home = env.PAPERCLIP_HOME;
|
|
const config = env.PAPERCLIP_CONFIG;
|
|
if (home !== expected.paperclipHome || config !== expected.configPath) {
|
|
throw new Error(
|
|
"Paperclip server environment does not use the allocated home/config paths",
|
|
);
|
|
}
|
|
if (
|
|
!home.startsWith(`${expected.temporaryRoot}/`) ||
|
|
!config.startsWith(`${expected.temporaryRoot}/`)
|
|
) {
|
|
throw new Error(
|
|
"Paperclip server paths escape the isolated temporary root",
|
|
);
|
|
}
|
|
if (env.XDG_CACHE_HOME !== path.join(expected.temporaryRoot, "xdg-cache")) {
|
|
throw new Error(
|
|
"Paperclip server cache does not use the allocated temporary root",
|
|
);
|
|
}
|
|
for (const key of [
|
|
...CREDENTIAL_NAMES,
|
|
...DATABASE_KEYS,
|
|
...AMBIENT_PAPERCLIP_CREDENTIAL_KEYS,
|
|
...AMBIENT_EXTERNAL_STATE_KEYS,
|
|
]) {
|
|
if (env[key])
|
|
throw new Error(
|
|
`Paperclip server environment unexpectedly contains ${key}`,
|
|
);
|
|
}
|
|
for (const key of GENERATED_SERVER_SECRET_KEYS) {
|
|
if (!env[key])
|
|
throw new Error(`Paperclip server environment is missing ${key}`);
|
|
}
|
|
}
|