diff --git a/packages/coding-agent/src/core/agent-session-runtime.ts b/packages/coding-agent/src/core/agent-session-runtime.ts index 1bab48ec2..8c39347d4 100644 --- a/packages/coding-agent/src/core/agent-session-runtime.ts +++ b/packages/coding-agent/src/core/agent-session-runtime.ts @@ -344,6 +344,7 @@ export class AgentSessionRuntime implements SubagentRuntimeHost { rlmDepth: options.rlmDepth, rlmMaxDepth: options.rlmMaxDepth, rlmSessionDir: options.sessionDir, + rlmAllowPush: options.allowPush, rlmParentNodeId: options.rlmParentNodeId, rlmParentAgent: options.parentSession.sessionName ?? options.parentSession.sessionId, semanticParentSessionId: options.parentSession.sessionId, diff --git a/packages/coding-agent/src/core/agent-session-services.ts b/packages/coding-agent/src/core/agent-session-services.ts index 2a96f6eae..59193c3cf 100644 --- a/packages/coding-agent/src/core/agent-session-services.ts +++ b/packages/coding-agent/src/core/agent-session-services.ts @@ -79,6 +79,8 @@ export interface AgentSessionCreationOptions { agentSessionController?: AgentSessionController; rlmDepth?: number; rlmMaxDepth?: number; + /** Let this session's skills publish without a further authorization (spawn allow_push). */ + rlmAllowPush?: boolean; rlmSessionDir?: string; rlmParentNodeId?: string; rlmParentAgent?: string; diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index a7225361a..116a7f4d1 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -535,6 +535,11 @@ export interface AgentSessionConfig { sessionStartEvent?: SessionStartEvent; rlmDepth?: number; rlmMaxDepth?: number; + /** + * Let this session's skills publish (`git push`, force reset) without the parent's + * explicit authorization. Set only by a spawn that passed `allow_push=True`. + */ + rlmAllowPush?: boolean; /** Override the models configuration file. Defaults to /rlm-models.toml. */ rlmModelsFilePath?: string | null; rlmSessionDir?: string; @@ -1544,6 +1549,7 @@ export class AgentSession { private _rlmMaxDepth: number; private _rlmMaxDepthSource: RlmMaxDepthSource; private _rlmSessionDir?: string; + private readonly _rlmAllowPush: boolean; private readonly _semanticEdges: SemanticEdgeRecorder; private _rlmParentNodeId?: string; private _rlmParentAgent?: string; @@ -1664,6 +1670,7 @@ export class AgentSession { this._rlmModelsStore = new RlmModelsStore(config.rlmModelsFilePath); this._hasCalledRlmModels = false; this._rlmSessionDir = config.rlmSessionDir; + this._rlmAllowPush = config.rlmAllowPush ?? false; this._rlmParentNodeId = config.rlmParentNodeId; this._rlmParentAgent = config.rlmParentAgent; this._semanticEdges = new SemanticEdgeRecorder({ @@ -10753,6 +10760,10 @@ export class AgentSession { RLM_MAX_DEPTH: String(this._rlmMaxDepth), RLM_GLOBAL_HARNESS_STATE_DIR: getGlobalHarnessStateDir(), }; + if (this._rlmAllowPush) { + // Capability attenuation (R6): a child may publish only when its parent said so. + env.RLM_ALLOW_PUSH = "1"; + } const rlmSessionDir = this._ensureRlmSessionDir(); if (rlmSessionDir) { env.RLM_SESSION_DIR = rlmSessionDir; @@ -10864,6 +10875,8 @@ export class AgentSession { thinkingLevel?: ThinkingLevel; spawnedByRequestId?: string; remoteHost?: string; + allowPush?: boolean; + cwd?: string; }): CreateRlmSubagentRuntimeOptions { return { parentSession: this, @@ -10888,9 +10901,10 @@ export class AgentSession { rlmParentNodeId: options.id, spawnedByRequestId: options.spawnedByRequestId, remoteHost: options.remoteHost, - // The parent session's own directory, not the process cwd: a child must start - // where its parent works, never in whatever directory the daemon was launched. - cwd: this.sessionManager.getCwd(), + allowPush: options.allowPush, + // An isolated workspace wins; otherwise the parent session's own directory, never + // whatever directory the daemon happened to be launched in. + cwd: options.cwd ?? this.sessionManager.getCwd(), }; } @@ -12067,11 +12081,32 @@ export class AgentSession { if (!this.hasCalledRlmModels) { throw new Error("rlm.spawn requires calling await rlm.models() first to discover available models"); } - const { name: rawName, model: rawModel, thinking: rawThinking, host: rawHost, ...unsupported } = kwargs; + const { + name: rawName, + model: rawModel, + thinking: rawThinking, + host: rawHost, + workspace: rawWorkspace, + allow_push: rawAllowPush, + cwd: rawCwd, + ...unsupported + } = kwargs; const unsupportedKwargs = Object.keys(unsupported); if (unsupportedKwargs.length > 0) { throw new Error(`Unsupported rlm.spawn kwargs: ${unsupportedKwargs.sort().join(", ")}`); } + if (rawAllowPush !== undefined && typeof rawAllowPush !== "boolean") { + throw new Error("allow_push must be a boolean"); + } + if (rawWorkspace !== undefined && rawWorkspace !== "isolated") { + throw new Error('workspace must be "isolated" when provided'); + } + if (rawCwd !== undefined && (typeof rawCwd !== "string" || !rawCwd.trim())) { + throw new Error("cwd must be a non-empty string"); + } + const requestedAllowPush = rawAllowPush === true; + // The runtime creates the checkout and forwards it as `cwd`; a bare cwd is honoured too. + const requestedCwd = typeof rawCwd === "string" && rawCwd.trim() ? rawCwd.trim() : undefined; if (rawModel === undefined) { throw new Error("rlm.spawn requires model="); } @@ -12253,6 +12288,8 @@ export class AgentSession { thinkingLevel, spawnedByRequestId, remoteHost: targetHost, + allowPush: requestedAllowPush, + cwd: requestedCwd, }), onSessionPublished: publishChildSession, }; diff --git a/packages/coding-agent/src/core/prompts/rlm.ts b/packages/coding-agent/src/core/prompts/rlm.ts index e55131e08..60daa7557 100644 --- a/packages/coding-agent/src/core/prompts/rlm.ts +++ b/packages/coding-agent/src/core/prompts/rlm.ts @@ -38,6 +38,7 @@ const REPL_CONTROL_PROMPT = [ "", "Do not assume the REPL is the native runtime of the external thing being investigated. A repository, package, service, dataset, paper, website, benchmark, or API may have its own environment and normal interface. Evaluate external systems through their own interface, then use the REPL to coordinate the process and analyze what comes back.", "", + "A command that would bind a server to a public interface is refused with the loopback form to use instead; when a public bind really is wanted, put `RLM_ALLOW_PUBLIC_LISTEN=1` in that command's environment.", "`bash(command)` starts a shell command in the background and returns a handle immediately: `h = bash('npm test')`. Use `h.pid` / `h.running` for liveness, `h.tail(n)` / `h.output()` for combined stdout+stderr so far, `h.poll()` for a non-blocking result, `h.kill()` to terminate (SIGTERM, escalating to SIGKILL; on Windows kill() uses taskkill /T and detached or reparented descendants may survive), and `await h` (or `await bash('cmd')`) for the completed result with exit_code, output, and duration. `bash(cmd, timeout=)` bounds one command the same way a cancelled await does — TERM then SIGKILL on its process group — and says so in `result.timed_out`; pass it for a known-long build, and pass it whenever you leave a detached handle running, because a cell deadline cannot reach one. Prefer bash() for long-running commands so the turn keeps working. Run shell commands with `bash()`, not `subprocess`/`os.system`: subprocess calls block the kernel, show the user nothing while they run, and spawn processes the harness cannot see or stop.", "", "Important: do not install dependencies into the kernel just to make an external project import or run there. If a project import, test, script, CLI, or dependency check is needed, run it through that project's own environment and normal command interface. For example, in a Python repo use its documented commands, `uv run ...`, `.venv/bin/python ...`, or the active project interpreter from the repo root. Treat failures from that native environment as the relevant result.", @@ -151,7 +152,7 @@ export function buildRlmPrompt(options: RlmPromptOptions): string { } if (hasIpython && installedSkills.includes("edit")) { skillLines.push( - "For targeted existing-file edits, prefer the pre-imported async `edit` skill from the REPL: `old = '''...'''; new = '''...'''; await edit(path=\"pkg/file.py\", old_str=old, new_str=new)`. Use exact old/new strings; if the text contains triple double quotes, use triple single-quoted variables or build `old`/`new` from inspected file slices.", + "For targeted existing-file edits, prefer the pre-imported async `edit` skill from the REPL: `old = '''...'''; new = '''...'''; await edit(path=\"pkg/file.py\", old_str=old, new_str=new)`. Use exact old/new strings; if the text contains triple double quotes, use triple single-quoted variables or build `old`/`new` from inspected file slices. A non-unique `old_str` changes nothing and reports the bounded line ranges of every occurrence: retry once with `line_hint=` instead of guessing the text again.", ); } } diff --git a/packages/coding-agent/src/core/rlm-runtime.ts b/packages/coding-agent/src/core/rlm-runtime.ts index a8bcc44e4..99495a0bc 100644 --- a/packages/coding-agent/src/core/rlm-runtime.ts +++ b/packages/coding-agent/src/core/rlm-runtime.ts @@ -483,6 +483,8 @@ export interface CreateRlmSubagentRuntimeOptions { onSessionPublished?: (session: AgentSession) => void; /** Remote host destination if this subagent's kernel should execute on a remote host over SSH. */ remoteHost?: string; + /** Let the child publish (git push, force reset) without a further authorization. */ + allowPush?: boolean; /** * Working directory the child session, its kernel, and its `bash()` calls start in. * The parent always sets this to its own session cwd, so a child cannot inherit an diff --git a/packages/coding-agent/src/modes/daemon/daemon-mode.ts b/packages/coding-agent/src/modes/daemon/daemon-mode.ts index c53dc11cb..b171aee3f 100644 --- a/packages/coding-agent/src/modes/daemon/daemon-mode.ts +++ b/packages/coding-agent/src/modes/daemon/daemon-mode.ts @@ -2957,6 +2957,7 @@ export class AgentDaemon { rlmDepth: options.rlmDepth, rlmMaxDepth: options.rlmMaxDepth, rlmSessionDir: options.sessionDir, + rlmAllowPush: options.allowPush, rlmParentNodeId: options.rlmParentNodeId, rlmParentAgent: options.parentSession.sessionName ?? options.parentSession.sessionId, semanticParentSessionId: options.parentSession.sessionId,