Skip to content

Add plain REPL sessions, model catalog, CLI subcommands, and tool approval policy#6

Merged
doanbactam merged 2 commits into
masterfrom
2026-06-16/analyze-project-and-compare-with-cli-tools
Jun 16, 2026
Merged

Add plain REPL sessions, model catalog, CLI subcommands, and tool approval policy#6
doanbactam merged 2 commits into
masterfrom
2026-06-16/analyze-project-and-compare-with-cli-tools

Conversation

@doanbactam

@doanbactam doanbactam commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Motivation

  • Provide a simple, non-TUI interactive plain REPL that persists sessions as JSONL so users can resume, list, and inspect transcripts.
  • Surface additional CLI helpers for model/catalog inspection, context indexing, verify runs, serving a local read-only API, and release-readiness checks to support local workflows and CI.
  • Prevent accidental mutations or command execution in the plain REPL by introducing a runtime approval policy for mutating tools.
  • Improve provider discovery and housekeeping utilities (model catalog, session listing ordering, and small doctor/orchestrator cleanups).

Description

  • Added forge-cli/src/plain.rs implementing a plain REPL with session logging (.forge/sessions/*.jsonl), commands like /approve, /verify, /diff, /undo, and resume/list helpers wired into the CLI via a new Session subcommand and --session flag for repl.
  • Introduced a provider model catalog in provider::catalog with MODEL_CATALOG, list_models, and default_model, plus a list_models CLI command and formatted table output.
  • Extended the EventLoop (forge-core) with a ToolPolicy and with_repl_turn/with_task helpers, and enforced approvals for write_file, diff_edit, and run_command in execute_tool_with_result to require session approval when configured.
  • Added sandbox preview support via Sandbox::preview_diff_edit and PatchPreview, a minimal serve HTTP server (forge-cli/src/serve.rs), release_check tooling (forge-cli/src/release_check.rs), and assorted CLI subcommands (Models, Context, Verify, Serve, ReleaseCheck), plus small refactors (task sorting, doctor checks, and exports).

Testing

  • Ran the workspace unit test suite with cargo test which exercised new tests in provider::catalog, release_check, serve helpers, and forge-core event loop policy tests, and the test run completed successfully.
  • New unit tests include the model filtering in provider::catalog, the release_check::model_catalog_check_passes test, the serve::list_sessions behavior, and an event loop test verifying writes are blocked without approval, all of which passed.

Codex Task


Open in Devin Review

@doanbactam

Copy link
Copy Markdown
Contributor Author

@codex

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions

Copy link
Copy Markdown

Forge PR Review

Error: No API key for provider 'zai'. Pass --api-key or set ZAI_API_KEY. (Use --provider mock for offline runs.)
Review generation failed

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

Open in Devin Review

Comment thread forge-cli/src/plain.rs Outdated
Comment thread forge-cli/src/main.rs
Comment on lines +856 to +858
let provider = create_provider_instance(&provider_name, &model, &api_key)?;
let _ = context;
let context = context::ContextEngine::new(&project_path)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 launch_plain_mode discards the caller's Arc<Mutex>

In forge-cli/src/main.rs:856-858, the context parameter (an Arc<Mutex<ContextEngine>> potentially wrapped by a file watcher) is immediately discarded with let _ = context; and a fresh ContextEngine is created. If --watch was enabled, the file watcher task continues updating the discarded context object while the REPL uses an independent one. This doesn't cause a crash but means file watching is silently ineffective in plain mode.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread forge-cli/src/serve.rs
Comment on lines +36 to +49
pub fn serve(config: ServeConfig) -> Result<()> {
let listener = TcpListener::bind(&config.bind)
.with_context(|| format!("failed to bind Forge local server at {}", config.bind))?;
println!("Forge local server listening on http://{}", config.bind);
println!("Routes: {}", ROUTES.join(", "));

for stream in listener.incoming() {
let stream = stream?;
handle_stream(stream, &config.project_path)?;
if config.once {
break;
}
}
Ok(())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Serve module has no authentication on configurable bind address

The serve command (forge-cli/src/serve.rs:36-49) binds a plain TCP HTTP server with no authentication. The default bind address is 127.0.0.1:4545 (localhost-only), which is safe. However, the --bind flag allows binding to any address (e.g., 0.0.0.0:4545), which would expose session data and model info to the network without auth. The endpoints are read-only, so the risk is information disclosure rather than mutation. The docs say 'local read-only' but the CLI doesn't enforce localhost.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@doanbactam doanbactam merged commit ef40d6e into master Jun 16, 2026
2 of 3 checks passed
@github-actions

Copy link
Copy Markdown

Forge PR Review

Error: No API key for provider 'zai'. Pass --api-key or set ZAI_API_KEY. (Use --provider mock for offline runs.)
Review generation failed

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 new potential issues.

Open in Devin Review

Comment thread forge-cli/src/main.rs

async fn run_verify_command(project_path: &str, dry_run: bool, json: bool) -> Result<()> {
let workdir = std::path::Path::new(project_path);
let commands = verify::detect_verify_commands(workdir).unwrap_or_default();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 forge verify --dry-run uses detect_verify_commands instead of resolve_verify_commands, showing wrong commands

The run_verify_command function at forge-cli/src/main.rs:716 calls verify::detect_verify_commands(workdir) to determine which commands to display in dry-run mode and to include in the commands field of JSON reports. However, the actual execution path (line 739-740) goes through BuildVerifier::verify, which internally calls resolve_verify_commands (verify/src/verifier.rs:120). resolve_verify_commands first checks forge.toml for explicit [verify].commands before falling back to auto-detection, while detect_verify_commands only does auto-detection and ignores forge.toml entirely. This means forge verify --dry-run can report different commands than what forge verify would actually execute — e.g. a project with [verify] commands = ["make all"] in forge.toml and a Cargo.toml present would show cargo build --quiet / cargo test --quiet in dry-run, but actually run make all.

Suggested change
let commands = verify::detect_verify_commands(workdir).unwrap_or_default();
let commands = verify::resolve_verify_commands(workdir).unwrap_or_default();
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread forge-cli/src/plain.rs
Comment on lines +161 to +162
restored_state.approve_session =
policy.allow_writes.unwrap_or(false) && policy.allow_commands.unwrap_or(false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Permission config collapses separate allow_writes/allow_commands into a single boolean, losing granularity

The PermissionSection config struct supports separate allow_writes and allow_commands fields, and the ToolPolicy struct in the event loop also has separate fields. However, at forge-cli/src/plain.rs:161-162, the two config values are collapsed into a single approve_session boolean via &&, and at lines 259-264, this single boolean is mapped identically to both ToolPolicy.allow_writes and ToolPolicy.allow_commands. This means a config like allow_writes=true, allow_commands=false results in both being false (because true && false = false). The user's intent to pre-approve writes while blocking commands is silently ignored.

Prompt for agents
The ReplState struct uses a single `approve_session: bool` to track both write and command permissions, but the config (PermissionSection) and the ToolPolicy both support separate allow_writes and allow_commands fields. To fix this, split `approve_session` in ReplState into two fields (e.g., `approve_writes: bool` and `approve_commands: bool`), initialize them independently from the policy at line 161-162, map them independently to ToolPolicy at lines 259-264, and update the /approve command handler to either toggle both or support per-category approval (e.g., `/approve writes`, `/approve commands`). Files affected: forge-cli/src/plain.rs (ReplState struct, run_plain_repl initialization, ToolPolicy construction, handle_command /approve branch).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +96 to +102
fn check_context_store_parent(config: &ReleaseCheckConfig) -> ReleaseCheck {
let forge_dir = config.project_path.join(".forge");
ReleaseCheck {
name: "forge state dir",
passed: forge_dir.exists() || config.project_path.exists(),
detail: forge_dir.display().to_string(),
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 release_check::check_context_store_parent always passes when project_path exists

At forge-cli/src/release_check.rs:100, the check forge_dir.exists() || config.project_path.exists() will pass as long as the project directory itself exists, even if .forge doesn't. The || makes the .forge existence check meaningless in practice since the project path almost always exists (it defaults to .). This may be intentional as a minimal sanity check, but the check name 'forge state dir' suggests it should verify .forge specifically.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@stage-review

stage-review Bot commented Jun 18, 2026

Copy link
Copy Markdown

Ready to review this PR? Stage has broken it down into 7 individual chapters for you:

Title
1 Establish provider model catalog
2 Add sandbox diff preview support
3 Implement tool approval policy in EventLoop
4 Create plain REPL and session management
5 Add release check and serve utilities
6 Wire new subcommands into CLI
7 Perform minor refactors and cleanups
Open in Stage

Chapters generated by Stage for commit cef443e on Jun 18, 2026 9:49am UTC.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant