mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-18 11:40:39 +09:00
Let adapters declare runtime command spec for remote provisioning (#5141)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies, running
adapter
> commands like `claude`, `codex`, `pi` either locally or on remote
runtimes
> (SSH hosts, sandboxes, etc.)
> - On a fresh remote runtime — particularly an ephemeral sandbox — the
> adapter's CLI may not be installed yet. Today operators handle this
via
> external configuration (e.g. a project-level `provisionCommand` shell
> script) that has to know about every adapter the operator might want
to use
> - This means every adapter has its own well-known npm package, but
operators
> end up writing duplicate provision shell scripts that paste together
> `npm install -g @anthropic-ai/claude-code`, `npm install -g
@openai/codex`,
> etc. — knowledge the adapter itself already has
> - This PR moves that knowledge into the adapter modules: each adapter
declares
> how its runtime command should be detected and (if applicable)
installed
> via `getRuntimeCommandSpec(config)`. The execution path runs the
adapter's
> own install command on remote sandbox targets before launching, so a
fresh
> sandbox bootstraps itself instead of requiring a hand-written
provision script
> - The benefit is fewer footguns for operators provisioning remote
runtimes,
> and a clean place for new adapters to plug in their install recipe
## What Changed
- New types in `packages/adapter-utils/src/types.ts`:
- `AdapterRuntimeCommandSpec` describing `command`, optional
`detectCommand`, and optional `installCommand`
- Optional `getRuntimeCommandSpec(config)` on `ServerAdapterModule`
- Optional `runtimeCommandSpec` on `AdapterExecutionContext` so adapters
receive the resolved spec at execute time
- New helper `ensureAdapterExecutionTargetRuntimeCommandInstalled(...)`
in
`packages/adapter-utils/src/execution-target.ts` that runs the install
command
on remote targets when `transport === "sandbox"`. SSH and local targets
are
no-ops. Throws on timeout or non-zero exit so failures surface early.
- Each of `claude-local`, `codex-local`, `cursor-local`, `gemini-local`,
`opencode-local`, `pi-local`'s `execute.ts` now reads
`ctx.runtimeCommandSpec?.installCommand` and calls the helper before
launching
the adapter command.
- `server/src/adapters/registry.ts` declares `getRuntimeCommandSpec` for
each
adapter:
- claude/codex/gemini/opencode/pi-local: `npm install -g <package>`
recipe via
a shared `buildNpmRuntimeCommandSpec` helper, with a defensive guard
that
only auto-installs when the configured `command` matches the well-known
fallback (custom binaries are left alone).
- cursor-local: declares `command` only; no auto-install (no public npm
package), preserving the existing manual setup.
- `server/src/services/heartbeat.ts` resolves the spec via
`adapter.getRuntimeCommandSpec?.(runtimeConfig)` and passes it through
to
`AdapterExecutionContext`.
- Tests added in `execution-target.test.ts` (~75 lines), e2b
`plugin.test.ts` (~32 lines), and `environment-run-orchestrator.test.ts`
(~76 lines).
## Verification
- `pnpm --filter @paperclipai/adapter-utils test`
- `pnpm --filter @paperclipai/server test --
environment-run-orchestrator`
- `pnpm --filter @paperclipai/sandbox-providers-e2b test`
- Manual QA: run an adapter (claude/codex/etc.) against a fresh
sandbox-backed
environment that does NOT have the adapter CLI pre-installed. Confirm
the
install runs once at the start of the agent run and the adapter then
launches
successfully. Re-run on the same sandbox; confirm the install command is
idempotent and the second run starts faster.
- Confirm SSH and local execution paths are unaffected (gated by
`transport === "sandbox"`).
## Risks
- Behavioural shift on sandbox runs: a new install step now runs at the
start
of every sandbox agent run for adapters with `installCommand` set. The
install commands are idempotent (`if ! command -v X >/dev/null 2>&1;
then
npm install -g <pkg>; fi`), so this is fast on warm sandboxes. On a cold
sandbox, the first run takes longer.
- Operators who used the legacy project-level `provisionCommand` to
install
adapter CLIs can drop that part of their script; the adapter handles it
now.
Existing scripts continue to work — installs are idempotent.
- The cursor-local adapter has no auto-install (no public npm package).
Behaviour for cursor-local on sandboxes is unchanged.
- New optional surface on `ServerAdapterModule`. Plugins that don't
implement
`getRuntimeCommandSpec` retain previous behaviour (no auto-install).
## Model Used
- OpenAI GPT-5.4 (reasoning effort: high) via Codex CLI
- Provider: OpenAI
- Used to author the code changes in this PR
## 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
- [ ] If this change affects the UI, I have included before/after
screenshots — N/A
- [ ] I have updated relevant documentation to reflect my changes — N/A
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
This commit is contained in:
parent
2dce81fbf6
commit
90631b09b3
14 changed files with 383 additions and 14 deletions
|
|
@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import * as ssh from "./ssh.js";
|
import * as ssh from "./ssh.js";
|
||||||
import {
|
import {
|
||||||
adapterExecutionTargetUsesManagedHome,
|
adapterExecutionTargetUsesManagedHome,
|
||||||
|
ensureAdapterExecutionTargetRuntimeCommandInstalled,
|
||||||
resolveAdapterExecutionTargetCwd,
|
resolveAdapterExecutionTargetCwd,
|
||||||
runAdapterExecutionTargetShellCommand,
|
runAdapterExecutionTargetShellCommand,
|
||||||
} from "./execution-target.js";
|
} from "./execution-target.js";
|
||||||
|
|
@ -161,6 +162,80 @@ describe("runAdapterExecutionTargetShellCommand", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("ensureAdapterExecutionTargetRuntimeCommandInstalled", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("runs install commands for sandbox targets", async () => {
|
||||||
|
const runner = {
|
||||||
|
execute: vi.fn(async () => ({
|
||||||
|
exitCode: 0,
|
||||||
|
signal: null,
|
||||||
|
timedOut: false,
|
||||||
|
stdout: "",
|
||||||
|
stderr: "",
|
||||||
|
pid: null,
|
||||||
|
startedAt: new Date().toISOString(),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
await ensureAdapterExecutionTargetRuntimeCommandInstalled({
|
||||||
|
runId: "run-install",
|
||||||
|
target: {
|
||||||
|
kind: "remote",
|
||||||
|
transport: "sandbox",
|
||||||
|
providerKey: "e2b",
|
||||||
|
remoteCwd: "/remote/workspace",
|
||||||
|
runner,
|
||||||
|
},
|
||||||
|
installCommand: "npm install -g @google/gemini-cli",
|
||||||
|
cwd: "/local/workspace",
|
||||||
|
env: { PATH: "/usr/bin" },
|
||||||
|
timeoutSec: 30,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(runner.execute).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
command: "sh",
|
||||||
|
args: ["-lc", "npm install -g @google/gemini-cli"],
|
||||||
|
cwd: "/remote/workspace",
|
||||||
|
env: { PATH: "/usr/bin" },
|
||||||
|
timeoutMs: 30_000,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips install commands for SSH targets", async () => {
|
||||||
|
const runSshCommandSpy = vi.spyOn(ssh, "runSshCommand").mockResolvedValue({
|
||||||
|
stdout: "",
|
||||||
|
stderr: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
await ensureAdapterExecutionTargetRuntimeCommandInstalled({
|
||||||
|
runId: "run-skip",
|
||||||
|
target: {
|
||||||
|
kind: "remote",
|
||||||
|
transport: "ssh",
|
||||||
|
remoteCwd: "/srv/paperclip/workspace",
|
||||||
|
spec: {
|
||||||
|
host: "ssh.example.test",
|
||||||
|
port: 22,
|
||||||
|
username: "ssh-user",
|
||||||
|
remoteCwd: "/srv/paperclip/workspace",
|
||||||
|
remoteWorkspacePath: "/srv/paperclip/workspace",
|
||||||
|
privateKey: null,
|
||||||
|
knownHosts: null,
|
||||||
|
strictHostKeyChecking: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
installCommand: "npm install -g @google/gemini-cli",
|
||||||
|
cwd: "/tmp/local",
|
||||||
|
env: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(runSshCommandSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("resolveAdapterExecutionTargetCwd", () => {
|
describe("resolveAdapterExecutionTargetCwd", () => {
|
||||||
const sshTarget = {
|
const sshTarget = {
|
||||||
kind: "remote" as const,
|
kind: "remote" as const,
|
||||||
|
|
|
||||||
|
|
@ -393,6 +393,60 @@ export async function readAdapterExecutionTargetHomeDir(
|
||||||
return homeDir.length > 0 ? homeDir : null;
|
return homeDir.length > 0 ? homeDir : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function ensureAdapterExecutionTargetRuntimeCommandInstalled(input: {
|
||||||
|
runId: string;
|
||||||
|
target: AdapterExecutionTarget | null | undefined;
|
||||||
|
installCommand?: string | null;
|
||||||
|
detectCommand?: string | null;
|
||||||
|
cwd: string;
|
||||||
|
env: Record<string, string>;
|
||||||
|
timeoutSec?: number;
|
||||||
|
graceSec?: number;
|
||||||
|
onLog?: AdapterExecutionTargetShellOptions["onLog"];
|
||||||
|
}): Promise<void> {
|
||||||
|
const installCommand = input.installCommand?.trim();
|
||||||
|
if (!installCommand || input.target?.kind !== "remote" || input.target.transport !== "sandbox") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const detectCommand = input.detectCommand?.trim();
|
||||||
|
if (detectCommand) {
|
||||||
|
const probe = await runAdapterExecutionTargetShellCommand(
|
||||||
|
input.runId,
|
||||||
|
input.target,
|
||||||
|
`command -v ${shellQuote(detectCommand)} >/dev/null 2>&1`,
|
||||||
|
{
|
||||||
|
cwd: input.cwd,
|
||||||
|
env: input.env,
|
||||||
|
timeoutSec: input.timeoutSec,
|
||||||
|
graceSec: input.graceSec,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!probe.timedOut && probe.exitCode === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await runAdapterExecutionTargetShellCommand(
|
||||||
|
input.runId,
|
||||||
|
input.target,
|
||||||
|
installCommand,
|
||||||
|
{
|
||||||
|
cwd: input.cwd,
|
||||||
|
env: input.env,
|
||||||
|
timeoutSec: input.timeoutSec,
|
||||||
|
graceSec: input.graceSec,
|
||||||
|
onLog: input.onLog,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (result.timedOut) {
|
||||||
|
throw new Error(`Timed out while installing the adapter runtime command via: ${installCommand}`);
|
||||||
|
}
|
||||||
|
if ((result.exitCode ?? 0) !== 0) {
|
||||||
|
throw new Error(`Failed to install the adapter runtime command via: ${installCommand}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function ensureAdapterExecutionTargetFile(
|
export async function ensureAdapterExecutionTargetFile(
|
||||||
runId: string,
|
runId: string,
|
||||||
target: AdapterExecutionTarget | null | undefined,
|
target: AdapterExecutionTarget | null | undefined,
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ export type {
|
||||||
ConfigFieldOption,
|
ConfigFieldOption,
|
||||||
ConfigFieldSchema,
|
ConfigFieldSchema,
|
||||||
AdapterConfigSchema,
|
AdapterConfigSchema,
|
||||||
|
AdapterRuntimeCommandSpec,
|
||||||
ServerAdapterModule,
|
ServerAdapterModule,
|
||||||
QuotaWindow,
|
QuotaWindow,
|
||||||
ProviderQuotaResult,
|
ProviderQuotaResult,
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,7 @@ export interface AdapterExecutionContext {
|
||||||
runtime: AdapterRuntime;
|
runtime: AdapterRuntime;
|
||||||
config: Record<string, unknown>;
|
config: Record<string, unknown>;
|
||||||
context: Record<string, unknown>;
|
context: Record<string, unknown>;
|
||||||
|
runtimeCommandSpec?: AdapterRuntimeCommandSpec | null;
|
||||||
executionTarget?: AdapterExecutionTarget | null;
|
executionTarget?: AdapterExecutionTarget | null;
|
||||||
/**
|
/**
|
||||||
* Legacy remote transport view. Prefer `executionTarget`, which is the
|
* Legacy remote transport view. Prefer `executionTarget`, which is the
|
||||||
|
|
@ -328,6 +329,23 @@ export interface AdapterConfigSchema {
|
||||||
fields: ConfigFieldSchema[];
|
fields: ConfigFieldSchema[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdapterRuntimeCommandSpec {
|
||||||
|
/**
|
||||||
|
* The command Paperclip should execute for this adapter in the current config.
|
||||||
|
*/
|
||||||
|
command: string;
|
||||||
|
/**
|
||||||
|
* Optional command name/path to probe for availability before launch.
|
||||||
|
* Defaults to `command` when omitted by the consumer.
|
||||||
|
*/
|
||||||
|
detectCommand?: string | null;
|
||||||
|
/**
|
||||||
|
* Optional shell snippet that can install or expose the adapter command in a
|
||||||
|
* fresh remote runtime. It should be idempotent.
|
||||||
|
*/
|
||||||
|
installCommand?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ServerAdapterModule {
|
export interface ServerAdapterModule {
|
||||||
type: string;
|
type: string;
|
||||||
execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult>;
|
execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult>;
|
||||||
|
|
@ -406,6 +424,11 @@ export interface ServerAdapterModule {
|
||||||
* rather than reading config.paperclipRuntimeSkills.
|
* rather than reading config.paperclipRuntimeSkills.
|
||||||
*/
|
*/
|
||||||
requiresMaterializedRuntimeSkills?: boolean;
|
requiresMaterializedRuntimeSkills?: boolean;
|
||||||
|
/**
|
||||||
|
* Optional: describe how this adapter's runtime command should be launched
|
||||||
|
* and provisioned in fresh remote environments such as sandboxes.
|
||||||
|
*/
|
||||||
|
getRuntimeCommandSpec?: (config: Record<string, unknown>) => AdapterRuntimeCommandSpec | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
adapterExecutionTargetUsesPaperclipBridge,
|
adapterExecutionTargetUsesPaperclipBridge,
|
||||||
describeAdapterExecutionTarget,
|
describeAdapterExecutionTarget,
|
||||||
ensureAdapterExecutionTargetCommandResolvable,
|
ensureAdapterExecutionTargetCommandResolvable,
|
||||||
|
ensureAdapterExecutionTargetRuntimeCommandInstalled,
|
||||||
prepareAdapterExecutionTargetRuntime,
|
prepareAdapterExecutionTargetRuntime,
|
||||||
readAdapterExecutionTarget,
|
readAdapterExecutionTarget,
|
||||||
resolveAdapterExecutionTargetCommandForLogs,
|
resolveAdapterExecutionTargetCommandForLogs,
|
||||||
|
|
@ -61,8 +62,10 @@ interface ClaudeExecutionInput {
|
||||||
agent: AdapterExecutionContext["agent"];
|
agent: AdapterExecutionContext["agent"];
|
||||||
config: Record<string, unknown>;
|
config: Record<string, unknown>;
|
||||||
context: Record<string, unknown>;
|
context: Record<string, unknown>;
|
||||||
|
runtimeCommandSpec?: AdapterExecutionContext["runtimeCommandSpec"];
|
||||||
executionTarget?: ReturnType<typeof readAdapterExecutionTarget>;
|
executionTarget?: ReturnType<typeof readAdapterExecutionTarget>;
|
||||||
authToken?: string;
|
authToken?: string;
|
||||||
|
onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClaudeRuntimeConfig {
|
interface ClaudeRuntimeConfig {
|
||||||
|
|
@ -112,7 +115,8 @@ function resolveClaudeBillingType(env: Record<string, string>): "api" | "subscri
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildClaudeRuntimeConfig(input: ClaudeExecutionInput): Promise<ClaudeRuntimeConfig> {
|
async function buildClaudeRuntimeConfig(input: ClaudeExecutionInput): Promise<ClaudeRuntimeConfig> {
|
||||||
const { runId, agent, config, context, executionTarget, authToken } = input;
|
const { runId, agent, config, context, runtimeCommandSpec, executionTarget, authToken } = input;
|
||||||
|
const onLog = input.onLog ?? (async () => {});
|
||||||
|
|
||||||
const command = asString(config.command, "claude");
|
const command = asString(config.command, "claude");
|
||||||
const workspaceContext = parseObject(context.paperclipWorkspace);
|
const workspaceContext = parseObject(context.paperclipWorkspace);
|
||||||
|
|
@ -239,7 +243,24 @@ async function buildClaudeRuntimeConfig(input: ClaudeExecutionInput): Promise<Cl
|
||||||
env.PAPERCLIP_API_KEY = authToken;
|
env.PAPERCLIP_API_KEY = authToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
const runtimeEnv = ensurePathInEnv({ ...process.env, ...env });
|
const runtimeEnv = Object.fromEntries(
|
||||||
|
Object.entries(ensurePathInEnv({ ...process.env, ...env })).filter(
|
||||||
|
(entry): entry is [string, string] => typeof entry[1] === "string",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const timeoutSec = asNumber(config.timeoutSec, 0);
|
||||||
|
const graceSec = asNumber(config.graceSec, 20);
|
||||||
|
await ensureAdapterExecutionTargetRuntimeCommandInstalled({
|
||||||
|
runId,
|
||||||
|
target: executionTarget,
|
||||||
|
installCommand: runtimeCommandSpec?.installCommand,
|
||||||
|
detectCommand: runtimeCommandSpec?.detectCommand,
|
||||||
|
cwd,
|
||||||
|
env: runtimeEnv,
|
||||||
|
timeoutSec,
|
||||||
|
graceSec,
|
||||||
|
onLog,
|
||||||
|
});
|
||||||
await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv);
|
await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv);
|
||||||
const resolvedCommand = await resolveAdapterExecutionTargetCommandForLogs(command, executionTarget, cwd, runtimeEnv);
|
const resolvedCommand = await resolveAdapterExecutionTargetCommandForLogs(command, executionTarget, cwd, runtimeEnv);
|
||||||
const loggedEnv = buildInvocationEnvForLogs(env, {
|
const loggedEnv = buildInvocationEnvForLogs(env, {
|
||||||
|
|
@ -248,8 +269,6 @@ async function buildClaudeRuntimeConfig(input: ClaudeExecutionInput): Promise<Cl
|
||||||
resolvedCommand,
|
resolvedCommand,
|
||||||
});
|
});
|
||||||
|
|
||||||
const timeoutSec = asNumber(config.timeoutSec, 0);
|
|
||||||
const graceSec = asNumber(config.graceSec, 20);
|
|
||||||
const extraArgs = (() => {
|
const extraArgs = (() => {
|
||||||
const fromExtraArgs = asStringArray(config.extraArgs);
|
const fromExtraArgs = asStringArray(config.extraArgs);
|
||||||
if (fromExtraArgs.length > 0) return fromExtraArgs;
|
if (fromExtraArgs.length > 0) return fromExtraArgs;
|
||||||
|
|
@ -335,8 +354,10 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
agent,
|
agent,
|
||||||
config,
|
config,
|
||||||
context,
|
context,
|
||||||
|
runtimeCommandSpec: ctx.runtimeCommandSpec,
|
||||||
executionTarget,
|
executionTarget,
|
||||||
authToken,
|
authToken,
|
||||||
|
onLog,
|
||||||
});
|
});
|
||||||
const {
|
const {
|
||||||
command,
|
command,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
adapterExecutionTargetUsesPaperclipBridge,
|
adapterExecutionTargetUsesPaperclipBridge,
|
||||||
describeAdapterExecutionTarget,
|
describeAdapterExecutionTarget,
|
||||||
ensureAdapterExecutionTargetCommandResolvable,
|
ensureAdapterExecutionTargetCommandResolvable,
|
||||||
|
ensureAdapterExecutionTargetRuntimeCommandInstalled,
|
||||||
prepareAdapterExecutionTargetRuntime,
|
prepareAdapterExecutionTargetRuntime,
|
||||||
readAdapterExecutionTarget,
|
readAdapterExecutionTarget,
|
||||||
resolveAdapterExecutionTargetCommandForLogs,
|
resolveAdapterExecutionTargetCommandForLogs,
|
||||||
|
|
@ -481,7 +482,22 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const billingType = resolveCodexBillingType(effectiveEnv);
|
const billingType = resolveCodexBillingType(effectiveEnv);
|
||||||
const runtimeEnv = ensurePathInEnv(effectiveEnv);
|
const runtimeEnv = Object.fromEntries(
|
||||||
|
Object.entries(ensurePathInEnv(effectiveEnv)).filter(
|
||||||
|
(entry): entry is [string, string] => typeof entry[1] === "string",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await ensureAdapterExecutionTargetRuntimeCommandInstalled({
|
||||||
|
runId,
|
||||||
|
target: executionTarget,
|
||||||
|
installCommand: ctx.runtimeCommandSpec?.installCommand,
|
||||||
|
detectCommand: ctx.runtimeCommandSpec?.detectCommand,
|
||||||
|
cwd,
|
||||||
|
env: runtimeEnv,
|
||||||
|
timeoutSec: asNumber(config.timeoutSec, 0),
|
||||||
|
graceSec: asNumber(config.graceSec, 20),
|
||||||
|
onLog,
|
||||||
|
});
|
||||||
await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv);
|
await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv);
|
||||||
const resolvedCommand = await resolveAdapterExecutionTargetCommandForLogs(command, executionTarget, cwd, runtimeEnv);
|
const resolvedCommand = await resolveAdapterExecutionTargetCommandForLogs(command, executionTarget, cwd, runtimeEnv);
|
||||||
const loggedEnv = buildInvocationEnvForLogs(env, {
|
const loggedEnv = buildInvocationEnvForLogs(env, {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
adapterExecutionTargetUsesPaperclipBridge,
|
adapterExecutionTargetUsesPaperclipBridge,
|
||||||
describeAdapterExecutionTarget,
|
describeAdapterExecutionTarget,
|
||||||
ensureAdapterExecutionTargetCommandResolvable,
|
ensureAdapterExecutionTargetCommandResolvable,
|
||||||
|
ensureAdapterExecutionTargetRuntimeCommandInstalled,
|
||||||
prepareAdapterExecutionTargetRuntime,
|
prepareAdapterExecutionTargetRuntime,
|
||||||
readAdapterExecutionTarget,
|
readAdapterExecutionTarget,
|
||||||
readAdapterExecutionTargetHomeDir,
|
readAdapterExecutionTargetHomeDir,
|
||||||
|
|
@ -307,6 +308,17 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
}
|
}
|
||||||
const timeoutSec = asNumber(config.timeoutSec, 0);
|
const timeoutSec = asNumber(config.timeoutSec, 0);
|
||||||
const graceSec = asNumber(config.graceSec, 20);
|
const graceSec = asNumber(config.graceSec, 20);
|
||||||
|
await ensureAdapterExecutionTargetRuntimeCommandInstalled({
|
||||||
|
runId,
|
||||||
|
target: executionTarget,
|
||||||
|
installCommand: ctx.runtimeCommandSpec?.installCommand,
|
||||||
|
detectCommand: ctx.runtimeCommandSpec?.detectCommand,
|
||||||
|
cwd,
|
||||||
|
env,
|
||||||
|
timeoutSec,
|
||||||
|
graceSec,
|
||||||
|
onLog,
|
||||||
|
});
|
||||||
// Probe the sandbox before the managed-home override so we discover
|
// Probe the sandbox before the managed-home override so we discover
|
||||||
// cursor-agent from the real system HOME (e.g. ~/.local/bin/cursor-agent).
|
// cursor-agent from the real system HOME (e.g. ~/.local/bin/cursor-agent).
|
||||||
// The managed HOME set later is for runtime isolation, not for finding the CLI.
|
// The managed HOME set later is for runtime isolation, not for finding the CLI.
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
adapterExecutionTargetUsesPaperclipBridge,
|
adapterExecutionTargetUsesPaperclipBridge,
|
||||||
describeAdapterExecutionTarget,
|
describeAdapterExecutionTarget,
|
||||||
ensureAdapterExecutionTargetCommandResolvable,
|
ensureAdapterExecutionTargetCommandResolvable,
|
||||||
|
ensureAdapterExecutionTargetRuntimeCommandInstalled,
|
||||||
prepareAdapterExecutionTargetRuntime,
|
prepareAdapterExecutionTargetRuntime,
|
||||||
readAdapterExecutionTarget,
|
readAdapterExecutionTarget,
|
||||||
readAdapterExecutionTargetHomeDir,
|
readAdapterExecutionTargetHomeDir,
|
||||||
|
|
@ -273,7 +274,24 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const billingType = resolveGeminiBillingType(effectiveEnv);
|
const billingType = resolveGeminiBillingType(effectiveEnv);
|
||||||
const runtimeEnv = ensurePathInEnv(effectiveEnv);
|
const runtimeEnv = Object.fromEntries(
|
||||||
|
Object.entries(ensurePathInEnv(effectiveEnv)).filter(
|
||||||
|
(entry): entry is [string, string] => typeof entry[1] === "string",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const timeoutSec = asNumber(config.timeoutSec, 0);
|
||||||
|
const graceSec = asNumber(config.graceSec, 20);
|
||||||
|
await ensureAdapterExecutionTargetRuntimeCommandInstalled({
|
||||||
|
runId,
|
||||||
|
target: executionTarget,
|
||||||
|
installCommand: ctx.runtimeCommandSpec?.installCommand,
|
||||||
|
detectCommand: ctx.runtimeCommandSpec?.detectCommand,
|
||||||
|
cwd,
|
||||||
|
env: runtimeEnv,
|
||||||
|
timeoutSec,
|
||||||
|
graceSec,
|
||||||
|
onLog,
|
||||||
|
});
|
||||||
await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv);
|
await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv);
|
||||||
const resolvedCommand = await resolveAdapterExecutionTargetCommandForLogs(command, executionTarget, cwd, runtimeEnv);
|
const resolvedCommand = await resolveAdapterExecutionTargetCommandForLogs(command, executionTarget, cwd, runtimeEnv);
|
||||||
let loggedEnv = buildInvocationEnvForLogs(env, {
|
let loggedEnv = buildInvocationEnvForLogs(env, {
|
||||||
|
|
@ -282,8 +300,6 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
resolvedCommand,
|
resolvedCommand,
|
||||||
});
|
});
|
||||||
|
|
||||||
const timeoutSec = asNumber(config.timeoutSec, 0);
|
|
||||||
const graceSec = asNumber(config.graceSec, 20);
|
|
||||||
const extraArgs = (() => {
|
const extraArgs = (() => {
|
||||||
const fromExtraArgs = asStringArray(config.extraArgs);
|
const fromExtraArgs = asStringArray(config.extraArgs);
|
||||||
if (fromExtraArgs.length > 0) return fromExtraArgs;
|
if (fromExtraArgs.length > 0) return fromExtraArgs;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
adapterExecutionTargetUsesPaperclipBridge,
|
adapterExecutionTargetUsesPaperclipBridge,
|
||||||
describeAdapterExecutionTarget,
|
describeAdapterExecutionTarget,
|
||||||
ensureAdapterExecutionTargetCommandResolvable,
|
ensureAdapterExecutionTargetCommandResolvable,
|
||||||
|
ensureAdapterExecutionTargetRuntimeCommandInstalled,
|
||||||
prepareAdapterExecutionTargetRuntime,
|
prepareAdapterExecutionTargetRuntime,
|
||||||
readAdapterExecutionTarget,
|
readAdapterExecutionTarget,
|
||||||
readAdapterExecutionTargetHomeDir,
|
readAdapterExecutionTargetHomeDir,
|
||||||
|
|
@ -302,6 +303,19 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
(entry): entry is [string, string] => typeof entry[1] === "string",
|
(entry): entry is [string, string] => typeof entry[1] === "string",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
const timeoutSec = asNumber(config.timeoutSec, 0);
|
||||||
|
const graceSec = asNumber(config.graceSec, 20);
|
||||||
|
await ensureAdapterExecutionTargetRuntimeCommandInstalled({
|
||||||
|
runId,
|
||||||
|
target: executionTarget,
|
||||||
|
installCommand: ctx.runtimeCommandSpec?.installCommand,
|
||||||
|
detectCommand: ctx.runtimeCommandSpec?.detectCommand,
|
||||||
|
cwd,
|
||||||
|
env: runtimeEnv,
|
||||||
|
timeoutSec,
|
||||||
|
graceSec,
|
||||||
|
onLog,
|
||||||
|
});
|
||||||
await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv);
|
await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv);
|
||||||
const resolvedCommand = await resolveAdapterExecutionTargetCommandForLogs(command, executionTarget, cwd, runtimeEnv);
|
const resolvedCommand = await resolveAdapterExecutionTargetCommandForLogs(command, executionTarget, cwd, runtimeEnv);
|
||||||
let loggedEnv = buildInvocationEnvForLogs(preparedRuntimeConfig.env, {
|
let loggedEnv = buildInvocationEnvForLogs(preparedRuntimeConfig.env, {
|
||||||
|
|
@ -309,9 +323,6 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
includeRuntimeKeys: ["HOME"],
|
includeRuntimeKeys: ["HOME"],
|
||||||
resolvedCommand,
|
resolvedCommand,
|
||||||
});
|
});
|
||||||
const timeoutSec = asNumber(config.timeoutSec, 0);
|
|
||||||
const graceSec = asNumber(config.graceSec, 20);
|
|
||||||
|
|
||||||
if (!executionTargetIsRemote) {
|
if (!executionTargetIsRemote) {
|
||||||
await ensureOpenCodeModelConfiguredAndAvailable({
|
await ensureOpenCodeModelConfiguredAndAvailable({
|
||||||
model,
|
model,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
describeAdapterExecutionTarget,
|
describeAdapterExecutionTarget,
|
||||||
ensureAdapterExecutionTargetCommandResolvable,
|
ensureAdapterExecutionTargetCommandResolvable,
|
||||||
ensureAdapterExecutionTargetFile,
|
ensureAdapterExecutionTargetFile,
|
||||||
|
ensureAdapterExecutionTargetRuntimeCommandInstalled,
|
||||||
prepareAdapterExecutionTargetRuntime,
|
prepareAdapterExecutionTargetRuntime,
|
||||||
readAdapterExecutionTarget,
|
readAdapterExecutionTarget,
|
||||||
resolveAdapterExecutionTargetCommandForLogs,
|
resolveAdapterExecutionTargetCommandForLogs,
|
||||||
|
|
@ -347,6 +348,19 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
(entry): entry is [string, string] => typeof entry[1] === "string",
|
(entry): entry is [string, string] => typeof entry[1] === "string",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
const timeoutSec = asNumber(config.timeoutSec, 0);
|
||||||
|
const graceSec = asNumber(config.graceSec, 20);
|
||||||
|
await ensureAdapterExecutionTargetRuntimeCommandInstalled({
|
||||||
|
runId,
|
||||||
|
target: executionTarget,
|
||||||
|
installCommand: ctx.runtimeCommandSpec?.installCommand,
|
||||||
|
detectCommand: ctx.runtimeCommandSpec?.detectCommand,
|
||||||
|
cwd,
|
||||||
|
env: runtimeEnv,
|
||||||
|
timeoutSec,
|
||||||
|
graceSec,
|
||||||
|
onLog,
|
||||||
|
});
|
||||||
await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv);
|
await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv);
|
||||||
const resolvedCommand = await resolveAdapterExecutionTargetCommandForLogs(command, executionTarget, cwd, runtimeEnv);
|
const resolvedCommand = await resolveAdapterExecutionTargetCommandForLogs(command, executionTarget, cwd, runtimeEnv);
|
||||||
let loggedEnv = buildInvocationEnvForLogs(env, {
|
let loggedEnv = buildInvocationEnvForLogs(env, {
|
||||||
|
|
@ -364,8 +378,6 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeoutSec = asNumber(config.timeoutSec, 0);
|
|
||||||
const graceSec = asNumber(config.graceSec, 20);
|
|
||||||
const extraArgs = (() => {
|
const extraArgs = (() => {
|
||||||
const fromExtraArgs = asStringArray(config.extraArgs);
|
const fromExtraArgs = asStringArray(config.extraArgs);
|
||||||
if (fromExtraArgs.length > 0) return fromExtraArgs;
|
if (fromExtraArgs.length > 0) return fromExtraArgs;
|
||||||
|
|
|
||||||
|
|
@ -420,6 +420,85 @@ describe("environmentRunOrchestrator — realizeForRun", () => {
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("runs project-level provision commands for ssh environments", async () => {
|
||||||
|
mockBuildWorkspaceRealizationRequest.mockReturnValue({
|
||||||
|
version: 1,
|
||||||
|
adapterType: "gemini_local",
|
||||||
|
companyId: "company-1",
|
||||||
|
environmentId: "env-1",
|
||||||
|
executionWorkspaceId: null,
|
||||||
|
issueId: null,
|
||||||
|
heartbeatRunId: "run-1",
|
||||||
|
requestedMode: null,
|
||||||
|
source: {
|
||||||
|
kind: "project_primary",
|
||||||
|
localPath: "/workspace/project",
|
||||||
|
projectId: null,
|
||||||
|
projectWorkspaceId: null,
|
||||||
|
repoUrl: null,
|
||||||
|
repoRef: null,
|
||||||
|
strategy: "project_primary",
|
||||||
|
branchName: null,
|
||||||
|
worktreePath: null,
|
||||||
|
},
|
||||||
|
runtimeOverlay: {
|
||||||
|
provisionCommand: "npm install -g @google/gemini-cli",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mockResolveEnvironmentExecutionTarget.mockResolvedValue({
|
||||||
|
kind: "remote",
|
||||||
|
transport: "ssh",
|
||||||
|
remoteCwd: "/remote/workspace",
|
||||||
|
environmentId: "env-1",
|
||||||
|
leaseId: "lease-1",
|
||||||
|
spec: {
|
||||||
|
host: "ssh.example.test",
|
||||||
|
port: 22,
|
||||||
|
username: "ssh-user",
|
||||||
|
remoteCwd: "/remote/workspace",
|
||||||
|
remoteWorkspacePath: "/remote/workspace",
|
||||||
|
privateKey: null,
|
||||||
|
knownHosts: null,
|
||||||
|
strictHostKeyChecking: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const runtime = makeMockRuntime({
|
||||||
|
realizeWorkspace: vi.fn().mockResolvedValue({
|
||||||
|
cwd: "/remote/workspace",
|
||||||
|
metadata: {
|
||||||
|
workspaceRealization: {
|
||||||
|
version: 1,
|
||||||
|
transport: "ssh",
|
||||||
|
remote: { path: "/remote/workspace" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const orchestrator = environmentRunOrchestrator(mockDb, { environmentRuntime: runtime });
|
||||||
|
|
||||||
|
await orchestrator.realizeForRun(makeRealizeInput({
|
||||||
|
environment: makeEnvironment("ssh"),
|
||||||
|
lease: makeLease({
|
||||||
|
provider: "ssh",
|
||||||
|
metadata: {
|
||||||
|
driver: "ssh",
|
||||||
|
remoteCwd: "/remote/workspace",
|
||||||
|
remoteWorkspacePath: "/remote/workspace",
|
||||||
|
host: "ssh.example.test",
|
||||||
|
port: 22,
|
||||||
|
username: "ssh-user",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(runtime.execute).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
command: "bash",
|
||||||
|
args: ["-lc", "npm install -g @google/gemini-cli"],
|
||||||
|
}));
|
||||||
|
expect(mockResolveEnvironmentExecutionTarget).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
it("surfaces remote provision command failures before resolving the adapter target", async () => {
|
it("surfaces remote provision command failures before resolving the adapter target", async () => {
|
||||||
mockBuildWorkspaceRealizationRequest.mockReturnValue({
|
mockBuildWorkspaceRealizationRequest.mockReturnValue({
|
||||||
version: 1,
|
version: 1,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { AdapterModelProfileDefinition, ServerAdapterModule } from "./types.js";
|
import type { AdapterModelProfileDefinition, AdapterRuntimeCommandSpec, ServerAdapterModule } from "./types.js";
|
||||||
import { getAdapterSessionManagement } from "@paperclipai/adapter-utils";
|
import { getAdapterSessionManagement } from "@paperclipai/adapter-utils";
|
||||||
import {
|
import {
|
||||||
execute as acpxExecute,
|
execute as acpxExecute,
|
||||||
|
|
@ -113,6 +113,44 @@ import { getDisabledAdapterTypes } from "../services/adapter-plugin-store.js";
|
||||||
import { processAdapter } from "./process/index.js";
|
import { processAdapter } from "./process/index.js";
|
||||||
import { httpAdapter } from "./http/index.js";
|
import { httpAdapter } from "./http/index.js";
|
||||||
|
|
||||||
|
function readConfiguredCommand(config: Record<string, unknown>, fallback: string): string {
|
||||||
|
const value = typeof config.command === "string" ? config.command.trim() : "";
|
||||||
|
return value.length > 0 ? value : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasPathSeparator(command: string): boolean {
|
||||||
|
return command.includes("/") || command.includes("\\");
|
||||||
|
}
|
||||||
|
|
||||||
|
function shellQuote(value: string): string {
|
||||||
|
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildNpmRuntimeCommandSpec(
|
||||||
|
config: Record<string, unknown>,
|
||||||
|
fallbackCommand: string,
|
||||||
|
packageName: string,
|
||||||
|
): AdapterRuntimeCommandSpec {
|
||||||
|
const command = readConfiguredCommand(config, fallbackCommand);
|
||||||
|
const canSelfInstall = !hasPathSeparator(command) && command === fallbackCommand;
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
detectCommand: command,
|
||||||
|
installCommand: canSelfInstall
|
||||||
|
? `if ! command -v ${shellQuote(command)} >/dev/null 2>&1; then npm install -g ${shellQuote(packageName)}; fi`
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCursorRuntimeCommandSpec(config: Record<string, unknown>): AdapterRuntimeCommandSpec {
|
||||||
|
const command = readConfiguredCommand(config, "agent");
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
detectCommand: command,
|
||||||
|
installCommand: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeHermesConfig<T extends { config?: unknown; agent?: unknown }>(ctx: T): T {
|
function normalizeHermesConfig<T extends { config?: unknown; agent?: unknown }>(ctx: T): T {
|
||||||
const config =
|
const config =
|
||||||
ctx && typeof ctx === "object" && "config" in ctx && ctx.config && typeof ctx.config === "object"
|
ctx && typeof ctx === "object" && "config" in ctx && ctx.config && typeof ctx.config === "object"
|
||||||
|
|
@ -159,6 +197,8 @@ const claudeLocalAdapter: ServerAdapterModule = {
|
||||||
supportsInstructionsBundle: true,
|
supportsInstructionsBundle: true,
|
||||||
instructionsPathKey: "instructionsFilePath",
|
instructionsPathKey: "instructionsFilePath",
|
||||||
requiresMaterializedRuntimeSkills: false,
|
requiresMaterializedRuntimeSkills: false,
|
||||||
|
getRuntimeCommandSpec: (config) =>
|
||||||
|
buildNpmRuntimeCommandSpec(config, "claude", "@anthropic-ai/claude-code"),
|
||||||
agentConfigurationDoc: claudeAgentConfigurationDoc,
|
agentConfigurationDoc: claudeAgentConfigurationDoc,
|
||||||
getQuotaWindows: claudeGetQuotaWindows,
|
getQuotaWindows: claudeGetQuotaWindows,
|
||||||
};
|
};
|
||||||
|
|
@ -195,6 +235,7 @@ const codexLocalAdapter: ServerAdapterModule = {
|
||||||
supportsInstructionsBundle: true,
|
supportsInstructionsBundle: true,
|
||||||
instructionsPathKey: "instructionsFilePath",
|
instructionsPathKey: "instructionsFilePath",
|
||||||
requiresMaterializedRuntimeSkills: false,
|
requiresMaterializedRuntimeSkills: false,
|
||||||
|
getRuntimeCommandSpec: (config) => buildNpmRuntimeCommandSpec(config, "codex", "@openai/codex"),
|
||||||
agentConfigurationDoc: codexAgentConfigurationDoc,
|
agentConfigurationDoc: codexAgentConfigurationDoc,
|
||||||
getQuotaWindows: codexGetQuotaWindows,
|
getQuotaWindows: codexGetQuotaWindows,
|
||||||
};
|
};
|
||||||
|
|
@ -214,6 +255,7 @@ const cursorLocalAdapter: ServerAdapterModule = {
|
||||||
supportsInstructionsBundle: true,
|
supportsInstructionsBundle: true,
|
||||||
instructionsPathKey: "instructionsFilePath",
|
instructionsPathKey: "instructionsFilePath",
|
||||||
requiresMaterializedRuntimeSkills: true,
|
requiresMaterializedRuntimeSkills: true,
|
||||||
|
getRuntimeCommandSpec: buildCursorRuntimeCommandSpec,
|
||||||
agentConfigurationDoc: cursorAgentConfigurationDoc,
|
agentConfigurationDoc: cursorAgentConfigurationDoc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -231,6 +273,8 @@ const geminiLocalAdapter: ServerAdapterModule = {
|
||||||
supportsInstructionsBundle: true,
|
supportsInstructionsBundle: true,
|
||||||
instructionsPathKey: "instructionsFilePath",
|
instructionsPathKey: "instructionsFilePath",
|
||||||
requiresMaterializedRuntimeSkills: true,
|
requiresMaterializedRuntimeSkills: true,
|
||||||
|
getRuntimeCommandSpec: (config) =>
|
||||||
|
buildNpmRuntimeCommandSpec(config, "gemini", "@google/gemini-cli"),
|
||||||
agentConfigurationDoc: geminiAgentConfigurationDoc,
|
agentConfigurationDoc: geminiAgentConfigurationDoc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -260,6 +304,7 @@ const openCodeLocalAdapter: ServerAdapterModule = {
|
||||||
supportsInstructionsBundle: true,
|
supportsInstructionsBundle: true,
|
||||||
instructionsPathKey: "instructionsFilePath",
|
instructionsPathKey: "instructionsFilePath",
|
||||||
requiresMaterializedRuntimeSkills: true,
|
requiresMaterializedRuntimeSkills: true,
|
||||||
|
getRuntimeCommandSpec: (config) => buildNpmRuntimeCommandSpec(config, "opencode", "opencode-ai"),
|
||||||
agentConfigurationDoc: openCodeAgentConfigurationDoc,
|
agentConfigurationDoc: openCodeAgentConfigurationDoc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -278,6 +323,8 @@ const piLocalAdapter: ServerAdapterModule = {
|
||||||
supportsInstructionsBundle: true,
|
supportsInstructionsBundle: true,
|
||||||
instructionsPathKey: "instructionsFilePath",
|
instructionsPathKey: "instructionsFilePath",
|
||||||
requiresMaterializedRuntimeSkills: true,
|
requiresMaterializedRuntimeSkills: true,
|
||||||
|
getRuntimeCommandSpec: (config) =>
|
||||||
|
buildNpmRuntimeCommandSpec(config, "pi", "@mariozechner/pi-coding-agent"),
|
||||||
agentConfigurationDoc: piAgentConfigurationDoc,
|
agentConfigurationDoc: piAgentConfigurationDoc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,5 +30,6 @@ export type {
|
||||||
ConfigFieldOption,
|
ConfigFieldOption,
|
||||||
ConfigFieldSchema,
|
ConfigFieldSchema,
|
||||||
AdapterConfigSchema,
|
AdapterConfigSchema,
|
||||||
|
AdapterRuntimeCommandSpec,
|
||||||
ServerAdapterModule,
|
ServerAdapterModule,
|
||||||
} from "@paperclipai/adapter-utils";
|
} from "@paperclipai/adapter-utils";
|
||||||
|
|
|
||||||
|
|
@ -6984,6 +6984,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
|
||||||
runtime: runtimeForAdapter,
|
runtime: runtimeForAdapter,
|
||||||
config: runtimeConfig,
|
config: runtimeConfig,
|
||||||
context,
|
context,
|
||||||
|
runtimeCommandSpec: adapter.getRuntimeCommandSpec?.(runtimeConfig) ?? null,
|
||||||
executionTarget,
|
executionTarget,
|
||||||
executionTransport: remoteExecution
|
executionTransport: remoteExecution
|
||||||
? { remoteExecution: remoteExecution as unknown as Record<string, unknown> }
|
? { remoteExecution: remoteExecution as unknown as Record<string, unknown> }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue