Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- Changed the Python kernel to receive an allowlisted environment instead of the full host environment, so provider credentials such as `PRIME_API_KEY` are no longer visible to kernel or `bash()` cells; `kernel.envPassthrough` admits extra variables.
- Added `--no-kernel-snapshots` and the `kernel.stateSnapshots` setting to disable persisting and reviving Python kernel state for a session.
- Fixed kernel state snapshots persisting variables equal to a credential from the host environment; such names are now skipped and reported.
- Changed new session transcripts to be created owner-only (0600), matching the kernel snapshot files.
1 change: 1 addition & 0 deletions packages/coding-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,7 @@ Use `prime-agent model list [search]` to list available models.
| `--fork <path\|id>` | Fork specific session file or partial UUID into a new session |
| `--session-dir <dir>` | Custom session storage directory |
| `--no-session` | Ephemeral mode (don't save) |
| `--no-kernel-snapshots` | Do not persist or revive Python kernel state for this session |

Use `prime-agent session export <file> [output]` to export a saved session to HTML.

Expand Down
4 changes: 3 additions & 1 deletion packages/coding-agent/docs/rlm-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,9 @@ Exact artifact files are created only when their features are used. Non-persiste

The REPL runtime process executes model-generated Python and `bash()` commands with the worker's OS permissions. The process boundary isolates protocol and lifecycle concerns; it is not a security sandbox. Installed Python packages, skills, and extensions are trusted code. Use an external sandbox or restricted execution environment when the workspace or generated code is untrusted.

Provider credentials are resolved by the TypeScript host. The bounded model catalog crosses into Python as metadata; the full auth store does not.
Provider credentials are resolved by the TypeScript host. The bounded model catalog crosses into Python as metadata; the full auth store does not. The kernel process is spawned with an allowlisted environment rather than the host's full environment: shell and locale essentials, `RLM_*`, `PRIME_AGENT_*`, Python/uv/pip/git variables, and whatever the session injects (subagent depth, bash shell, the websearch key). Provider API keys and other credential-shaped variables are dropped; `kernel.envPassthrough` admits extra names. `bash()` children inherit the kernel environment.

Kernel state snapshots (`kernel-state.dill`) are owner-only files. The host sends the snapshot request digests of the credential values it saw at spawn, and the runtime skips any top-level `str`/`bytes` name holding one of them (reported in `skipped`). `kernel.stateSnapshots: false` or `--no-kernel-snapshots` disables snapshots for a session.

## Failure Modes

Expand Down
10 changes: 10 additions & 0 deletions packages/coding-agent/docs/sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ Use `/session` in interactive mode to see the current session file, session ID,

For the JSONL file format and SessionManager API, see [Session Format](session-format.md).

Transcripts contain tool output and anything the model echoed, so new session files are created owner-only (`0600`). Existing files keep the mode they already have.

## Kernel State Snapshots

Persisted sessions also save the Python kernel namespace to `~/.prime/agent/session-artifacts/<session-id>/kernel-state.dill` (owner-only), with the saved and skipped names listed in `kernel-state.json`. Resuming the session revives those variables. Anything assigned in the kernel is persisted this way, so removing a value from the transcript does not remove it from the snapshot.

Two safeguards apply automatically: the kernel never inherits provider credentials from the host environment (see [Kernel settings](settings.md#kernel)), and a top-level variable whose value equals a credential the host saw in its environment is skipped and reported instead of being written.

To keep no kernel state on disk for a session, start it with `--no-kernel-snapshots`, or set `kernel.stateSnapshots` to `false` in [Settings](settings.md#kernel). The kernel stderr log is still written to the artifact directory. Deleting a session with `/resume` removes its artifact directory as well; to scrub a single snapshot by hand, delete `kernel-state.dill` and `kernel-state.json`.

## Session Commands

| Command | Description |
Expand Down
18 changes: 18 additions & 0 deletions packages/coding-agent/docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,24 @@ When a provider requests a retry delay longer than `retry.provider.maxRetryDelay

Normally the package manager's global modules location is queried using `root -g`. As a special case, if the first element of `npmCommand` is `"bun"`, the modules location will instead be queried with `pm bin -g`.

### Kernel

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `kernel.stateSnapshots` | boolean | `true` | Persist the Python kernel namespace to `session-artifacts/<id>/kernel-state.dill` and revive it on resume. `false` disables writing and restoring the snapshot for the session (same as `--no-kernel-snapshots`). |
| `kernel.envPassthrough` | string[] | `[]` | Extra host environment variable names the kernel (and its `bash()` commands) may inherit, as exact names or `PREFIX*` globs. |

```json
{
"kernel": {
"stateSnapshots": false,
"envPassthrough": ["DATABASE_URL", "MYAPP_*"]
}
}
```

The kernel does not inherit the host environment wholesale. It receives an allowlist (`PATH`, `HOME`, locale and terminal variables, temp dirs, proxy and TLS settings, common toolchain homes, `RLM_*`, `PRIME_AGENT_*`, `PYTHON*`, `UV_*`, `PIP_*`, `LC_*`, `XDG_*`, `GIT_*`, and the Windows system set) plus what the session injects. Provider credentials such as `PRIME_API_KEY`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `*_TOKEN`, and AWS/GCP credential variables are never inherited; list a name in `kernel.envPassthrough` only when model-run code genuinely needs it. Values of credentials present in the host environment are also never written to the kernel snapshot: a top-level variable equal to one of them is reported as skipped in `kernel-state.json`.

### Daemon

| Setting | Type | Default | Description |
Expand Down
3 changes: 3 additions & 0 deletions packages/coding-agent/docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ Use `prime-agent model list [search]` to list available models.
| `--fork <path\|id>` | Fork a session file or partial UUID into a new session |
| `--session-dir <dir>` | Custom session storage directory |
| `--no-session` | Ephemeral mode; do not save |
| `--no-kernel-snapshots` | Do not persist or revive Python kernel state (`kernel-state.dill`) for this session; see [Sessions](sessions.md#kernel-state-snapshots) |

Use `prime-agent session export <file> [output]` to export a session to HTML.

Expand Down Expand Up @@ -365,6 +366,8 @@ prime-agent --tools ipython -p "Review the code"
| `PRIME_AGENT_KERNEL_PYTHON` | Use an existing Python environment with `prime-agent-runtime` instead of bootstrapping `~/.prime/agent/kernel-venv` |
| `VISUAL`, `EDITOR` | External editor for Ctrl+G |

Provider credentials in the environment are read by the TypeScript host only. The Python kernel receives an allowlisted environment without them; see [Kernel settings](settings.md#kernel) for the allowlist and `kernel.envPassthrough`.

The remaining `PI_*` variables are compatibility names still read by the current runtime. They do not change the application name, command, or default `~/.prime/agent` configuration path.

## Design Principles
Expand Down
2 changes: 1 addition & 1 deletion packages/coding-agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"postinstall": "node postinstall.cjs",
"prepublishOnly": "npm run clean && npm run build",
"bundle": "node scripts/bundle.mjs",
"test:kernel": "vitest --run --no-file-parallelism --tagsFilter kernel-heavy test/acp-kernel-features.test.ts test/acp-cold-cli.test.ts test/kernel-goal-skill.test.ts test/repl-kernel-state-roundtrip.test.ts test/repl-kernel-mcp-shutdown.test.ts"
"test:kernel": "vitest --run --no-file-parallelism --tagsFilter kernel-heavy test/acp-kernel-features.test.ts test/acp-cold-cli.test.ts test/kernel-goal-skill.test.ts test/repl-kernel-state-roundtrip.test.ts test/repl-kernel-secret-retention.test.ts test/repl-kernel-mcp-shutdown.test.ts"
},
"dependencies": {
"@agentclientprotocol/sdk": "^1.3.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Live docs: `inference/overview.md`, `inference/usage.md`, `inference/adapter-dep
export PRIME_API_KEY="your-api-key-here"
```

Note: the Python kernel does not inherit `PRIME_API_KEY` (or other provider keys) from the host environment. The `prime` CLI still authenticates from `~/.prime/config.json`; for direct API calls from kernel code, read the key from `~/.prime/config.json` (`api_key`) or ask the user to add `PRIME_API_KEY` to `kernel.envPassthrough` in settings.

## Via the CLI (recommended for evaluations)

```bash
Expand Down
3 changes: 3 additions & 0 deletions packages/coding-agent/src/cli/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface Args {
mode?: Mode;
daemonSocket?: string;
noSession?: boolean;
noKernelSnapshots?: boolean;
fork?: string;
sessionDir?: string;
models?: string[];
Expand Down Expand Up @@ -143,6 +144,8 @@ export function parseArgs(args: string[]): Args {
result.appendSystemPrompt.push(args[++i]);
} else if (arg === "--no-session") {
result.noSession = true;
} else if (arg === "--no-kernel-snapshots") {
result.noKernelSnapshots = true;
} else if (arg === "--fork" && i + 1 < args.length) {
result.fork = args[++i];
} else if (arg === "--session-dir" && i + 1 < args.length) {
Expand Down
1 change: 1 addition & 0 deletions packages/coding-agent/src/cli/command-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ const TOP_LEVEL_OPTION_GROUPS: ReadonlyArray<{ heading: string; options: readonl
["--fork <path|id>", "Fork a saved session into a new session"],
["--session-dir <dir>", "Use a custom session directory"],
["--no-session", "Do not save the session"],
["--no-kernel-snapshots", "Do not persist or revive Python kernel state for this session"],
["--goal <objective>", "Seed a persistent goal for a new root session"],
["--goal-token-budget <n>", "Set a positive token budget for --goal"],
],
Expand Down
4 changes: 4 additions & 0 deletions packages/coding-agent/src/cli/daemon-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ const SESSION_BOOLEAN_FLAGS = new Set([
"--continue",
"-c",
"--no-session",
"--no-kernel-snapshots",
"--no-tools",
"-nt",
"--no-builtin-tools",
Expand Down Expand Up @@ -542,6 +543,9 @@ function parseSessionOption(
case "-nc":
config.noContextFiles = true;
return boolean(arg);
case "--no-kernel-snapshots":
config.noKernelSnapshots = true;
return boolean(arg);
case "--goal": {
const value = readValue(arg);
if (!value.trim()) {
Expand Down
3 changes: 3 additions & 0 deletions packages/coding-agent/src/core/agent-session-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export interface AgentSessionRuntimeConfig {
themes?: string[];
noThemes?: boolean;
noContextFiles?: boolean;
/** Disable persisting/reviving the Python kernel namespace (kernel-state.dill) for the session. */
noKernelSnapshots?: boolean;
autonomous?: AgentAutonomousConfig;
extensionFlagValues?: Record<string, boolean | string>;
/**
Expand Down Expand Up @@ -90,6 +92,7 @@ export function mergeAgentSessionRuntimeConfig(
themes: cloneArray(override.themes ?? base.themes),
noThemes: override.noThemes ?? base.noThemes,
noContextFiles: override.noContextFiles ?? base.noContextFiles,
noKernelSnapshots: override.noKernelSnapshots ?? base.noKernelSnapshots,
autonomous: mergeAutonomousConfig(base.autonomous, override.autonomous),
extensionFlagValues:
base.extensionFlagValues || override.extensionFlagValues
Expand Down
2 changes: 2 additions & 0 deletions packages/coding-agent/src/core/agent-session-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ export interface AgentSessionCreationOptions {
subagentRuntimeHost?: SubagentRuntimeHost;
rlmHeartbeatController?: AgentRlmHeartbeatController;
prewarmIpythonKernel?: boolean;
/** Override for the kernel.stateSnapshots setting; false disables kernel-state.dill for this session. */
kernelStateSnapshots?: boolean;
autonomous?: AgentAutonomousConfig;
serializedRefine?: boolean;
executionMode?: AgentExecutionMode;
Expand Down
26 changes: 22 additions & 4 deletions packages/coding-agent/src/core/agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,8 @@ export interface AgentSessionConfig {
subagentRuntimeHost?: SubagentRuntimeHost;
autonomous?: AgentAutonomousConfig;
prewarmIpythonKernel?: boolean;
/** Override for the kernel.stateSnapshots setting; false disables kernel-state.dill for this session. */
kernelStateSnapshots?: boolean;
autoRefineReviewer?: AutoRefineReviewer;
/**
* When true, auto-refine runs synchronously between turns at the
Expand Down Expand Up @@ -1242,6 +1244,7 @@ export class AgentSession {
/** True once the runtime has been built once; later builds are in-process rebuilds (/reload). */
private _ipythonRuntimeBuilt = false;
private readonly _prewarmIpythonKernel: boolean;
private readonly _kernelStateSnapshotsOverride: boolean | undefined;
private _rlmDepth: number;
private readonly _configuredRlmMaxDepth: number | undefined;
private _rlmMaxDepth: number;
Expand Down Expand Up @@ -1355,6 +1358,7 @@ export class AgentSession {
this._rlmMaxDepth = resolvedRlmMaxDepth.maxDepth;
this._rlmMaxDepthSource = resolvedRlmMaxDepth.source;
this._prewarmIpythonKernel = (config.prewarmIpythonKernel ?? false) && this._rlmDepth === 0;
this._kernelStateSnapshotsOverride = config.kernelStateSnapshots;
this._autoRefineReviewer = config.autoRefineReviewer;
this._serializedRefine = config.serializedRefine ?? false;
this._rlmSessionDir = config.rlmSessionDir;
Expand Down Expand Up @@ -9427,18 +9431,21 @@ export class AgentSession {
// reload can't restore from a snapshot the old kernel is still writing.
const previousDispose = this._ipythonKernelProvisioner?.dispose();
this._ipythonKernelSnapshotDir = this.sessionManager.getSessionArtifactDir();
const stateSnapshots = this._kernelStateSnapshotsEnabled();
// Only surface the "revived from your previous session" notice on the first
// build (a genuine resume). A later rebuild (/reload) restores state silently
// for continuity — the conversation is unchanged, so there's nothing to flag.
const notifyRestore = !this._ipythonRuntimeBuilt;
this._ipythonKernelProvisioner = new IpythonKernelProvisioner(this._cwd, {
env: this._rlmKernelEnv(),
hostEnvPassthrough: this.settingsManager.getKernelEnvPassthrough(),
commandPrefix: this.settingsManager.getShellCommandPrefix(),
shellPath: this.settingsManager.getShellPath(),
sessionId: this.sessionId,
hostHandlers: this._createKernelHostHandlers(),
pythonSkills,
snapshotDir: this._ipythonKernelSnapshotDir,
stateSnapshots,
readyGate: previousDispose,
onRestore: notifyRestore ? (result) => this._onIpythonStateRestored(result) : undefined,
});
Expand Down Expand Up @@ -9512,7 +9519,9 @@ export class AgentSession {
// came back before the first turn, rather than a turn later when the kernel
// would otherwise lazily start on first use.
const hasSnapshot =
!!this._ipythonKernelSnapshotDir && existsSync(snapshotPathIn(this._ipythonKernelSnapshotDir));
this._kernelStateSnapshotsEnabled() &&
!!this._ipythonKernelSnapshotDir &&
existsSync(snapshotPathIn(this._ipythonKernelSnapshotDir));
if ((this._prewarmIpythonKernel || hasSnapshot) && this.getActiveToolNames().includes("ipython")) {
this._ipythonKernelProvisioner?.prewarm();
}
Expand Down Expand Up @@ -9713,6 +9722,11 @@ export class AgentSession {
}
}

/** CLI override first, then the kernel.stateSnapshots setting (default on). */
private _kernelStateSnapshotsEnabled(): boolean {
return this._kernelStateSnapshotsOverride ?? this.settingsManager.getKernelStateSnapshots();
}

private _rlmKernelEnv(): Record<string, string> {
// Kernel env is provisioning-time only: RLM_MAX_DEPTH may be stale in an already-running kernel;
// the TypeScript-side spawn check remains authoritative.
Expand All @@ -9738,14 +9752,18 @@ export class AgentSession {
env.PRIME_AGENT_CODING_AGENT_DIR = this._agentDir;
}

if (process.env[SERPER_ENV_VAR]?.trim()) {
return;
}
// Inject only when a websearch skill (bundled or custom) is actually loaded,
// so the key isn't exposed to kernels that can't use it.
if (!this._resourceLoader.getSkills().skills.some((skill) => skill.name === WEBSEARCH_SKILL_NAME)) {
return;
}
// The kernel no longer inherits the host env wholesale, so a host-provided
// key must be handed over explicitly; it still wins over the stored credential.
const fromHost = process.env[SERPER_ENV_VAR]?.trim();
if (fromHost) {
env[SERPER_ENV_VAR] = fromHost;
return;
}
const cred = this._modelRegistry.authStorage.get(SERPER_CREDENTIAL_ID);
if (cred?.type !== "api_key") {
return;
Expand Down
9 changes: 9 additions & 0 deletions packages/coding-agent/src/core/kernel/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,11 @@
export {
buildKernelEnv,
collectCredentialDigests,
credentialDigest,
droppedCredentialEnvNames,
isCredentialEnvName,
KERNEL_ENV_CREDENTIAL_NAMES,
type KernelEnvOptions,
} from "./kernel-env.js";
export { ReplKernelManager } from "./repl-manager.js";
export * from "./shared.js";
Loading
Loading