Skip to content

Commit cd34fbc

Browse files
author
Mateusz
committed
feat(steering): opt-in cat redirection steering for shell tools
Add CatFileEditsSteeringPolicy to UnifiedSteeringHandler to intercept shell execution when the normalized command uses cat > / cat >> for file create/append, with a fixed steering message. Wire opt-in via session/tool_call_reactor config, JSON schemas, sample env, CLI --enable-cat-file-edits-steering, and CAT_FILE_EDITS_STEERING_* env vars (from_env_part1b). Recognize run_terminal_cmd as a shell tool in ShellExecutionTools and CommandExtractionService. Tests: policy unit tests, CLI wiring, reactor policy ordering; schema drift fixture updated. Docs: cli-parameters and configuration examples. Note: tests/unit/core/cli_support/applicators/test_session_applicator.py left unstaged (working tree had broad CRLF-only churn); Namespace still tolerates missing cat_file_edits_steering_enabled via getattr in SessionApplicator. Made-with: Cursor
1 parent 9b818ff commit cd34fbc

22 files changed

Lines changed: 402 additions & 50 deletions

config/config.example.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ session:
109109
# Tool call reactor steering for pytest full-suite runs (requires opt-in)
110110
pytest_full_suite_steering_enabled: false
111111

112+
# Steering for shell commands using cat > / cat >> to create or append files (opt-in)
113+
cat_file_edits_steering_enabled: false
114+
112115
# Test execution reminder system
113116
# Tracks file modifications and test executions, reminding agents to run tests
114117
# before completing tasks when code changes haven't been verified.

config/sample.env

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ APPLY_DIFF_STEERING_ENABLED=true
9696
APPLY_DIFF_STEERING_RATE_LIMIT_SECONDS=60
9797
# Enable steering that warns before running the entire pytest suite (opt-in)
9898
PYTEST_FULL_SUITE_STEERING_ENABLED=false
99+
# Steering for cat > / cat >> file create-append via shell (opt-in)
100+
CAT_FILE_EDITS_STEERING_ENABLED=false
101+
# Optional custom message:
102+
# CAT_FILE_EDITS_STEERING_MESSAGE=
99103

100104
# Binary File Edit Steering Settings
101105
# Detects and warns when agents attempt to edit binary files (executables, media, databases, etc.)

config/schemas/app_config.schema.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@ properties:
227227
dangerous_command_steering_message: { type: ["string", "null"] }
228228
pytest_full_suite_steering_enabled: { type: boolean }
229229
pytest_full_suite_steering_message: { type: ["string", "null"] }
230+
cat_file_edits_steering_enabled: { type: boolean }
231+
cat_file_edits_steering_message: { type: ["string", "null"] }
230232
test_execution_reminder_enabled: { type: boolean }
231233
test_execution_reminder_message: { type: ["string", "null"] }
232234
fix_think_tags_enabled: { type: boolean }
@@ -310,6 +312,8 @@ properties:
310312
apply_diff_steering_message: { type: ["string", "null"] }
311313
pytest_full_suite_steering_enabled: { type: boolean }
312314
pytest_full_suite_steering_message: { type: ["string", "null"] }
315+
cat_file_edits_steering_enabled: { type: boolean }
316+
cat_file_edits_steering_message: { type: ["string", "null"] }
313317
inline_python_steering_enabled: { type: boolean }
314318
inline_python_steering_message: { type: ["string", "null"] }
315319
binary_file_edit_steering_enabled: { type: boolean }

config/schemas/tool_call_reactor_config.schema.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ properties:
3030
type:
3131
- string
3232
- "null"
33+
cat_file_edits_steering_enabled:
34+
type: boolean
35+
cat_file_edits_steering_message:
36+
type:
37+
- string
38+
- "null"
3339
inline_python_steering_enabled:
3440
type: boolean
3541
inline_python_steering_message:

docs/user_guide/cli-parameters.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,10 +379,12 @@ When auto-enabled with no explicit model configured, the system defaults to `ope
379379
| :--- | :--- | :--- |
380380
| `--enable-pytest-full-suite-steering` | `PYTEST_FULL_SUITE_STEERING_ENABLED=true` | Enable steering for full pytest suite. |
381381
| `--disable-pytest-full-suite-steering` | `PYTEST_FULL_SUITE_STEERING_ENABLED=false` | Disable steering for full pytest suite. |
382+
| `--enable-cat-file-edits-steering` | `CAT_FILE_EDITS_STEERING_ENABLED=true` | Enable steering when shell commands use `cat >` / `cat >>` to create or append files. Default is off; there is no CLI disable flag (use config or `CAT_FILE_EDITS_STEERING_ENABLED=false`). |
382383
| `--enable-pytest-context-saving` | N/A | Enable context saving rewrites. |
383384
| `--test-execution-reminder-enabled` | `TEST_EXECUTION_REMINDER_ENABLED=true` | Enable test execution reminder. |
384385
| `--no-test-execution-reminder-enabled` | `TEST_EXECUTION_REMINDER_ENABLED=false` | Disable test execution reminder. |
385386
| N/A | `PYTEST_FULL_SUITE_STEERING_MESSAGE` | Custom steering message. |
387+
| N/A | `CAT_FILE_EDITS_STEERING_MESSAGE` | Custom steering message for `cat` redirection file edits. |
386388
| N/A | `TEST_EXECUTION_REMINDER_MESSAGE` | Custom reminder message. |
387389

388390
### Empty Response Handling

docs/user_guide/configuration.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,8 @@ session:
242242
# Pytest Integration
243243
pytest_full_suite_steering_enabled: false
244244
pytest_full_suite_steering_message: null
245+
cat_file_edits_steering_enabled: false
246+
cat_file_edits_steering_message: null
245247
pytest_context_saving_enabled: false
246248
test_execution_reminder_enabled: false
247249
test_execution_reminder_message: null
@@ -292,6 +294,10 @@ session:
292294
apply_diff_steering_enabled: true
293295
apply_diff_steering_rate_limit_seconds: 60
294296
apply_diff_steering_message: null
297+
298+
# Opt-in: steer agents away from shell `cat >` / `cat >>` for file edits
299+
cat_file_edits_steering_enabled: false
300+
cat_file_edits_steering_message: null
295301

296302
access_policies: # List of access policies
297303
- name: "block-dangerous"
@@ -745,14 +751,14 @@ empty_response:
745751
enabled: true
746752
max_retries: 1
747753
748-
# Model Name Rewrites
749-
model_aliases:
750-
- pattern: "^gpt-4-(.*)"
751-
replacement: "openai:gpt-4-\1"
752-
753-
# Weighted replacement with request-size guardrails
754-
- pattern: "^coding$"
755-
replacement: "[first,max_context=164000,weight=4]opencode-go:glm-5.1^[weight=2,max_context=128000]opencode-go:kimi-k2.5^openai:gpt-4o"
754+
# Model Name Rewrites
755+
model_aliases:
756+
- pattern: "^gpt-4-(.*)"
757+
replacement: "openai:gpt-4-\1"
758+
759+
# Weighted replacement with request-size guardrails
760+
- pattern: "^coding$"
761+
replacement: "[first,max_context=164000,weight=4]opencode-go:glm-5.1^[weight=2,max_context=128000]opencode-go:kimi-k2.5^openai:gpt-4o"
756762
757763
# Reasoning Aliases (Shorthands)
758764
reasoning_aliases:

src/core/app/stages/steering.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ def _register_steering_policies(
118118
"""Register steering policies as singletons."""
119119
from src.services.steering.policies import (
120120
BinaryFileEditPolicy,
121+
CatFileEditsSteeringPolicy,
121122
InlinePythonPolicy,
122123
PytestFullSuitePolicy,
123124
)
@@ -165,6 +166,20 @@ def _register_steering_policies(
165166
),
166167
)
167168

169+
# Register CatFileEditsSteeringPolicy (opt-in)
170+
services.add_singleton(
171+
CatFileEditsSteeringPolicy,
172+
implementation_factory=lambda provider: CatFileEditsSteeringPolicy(
173+
message=getattr(
174+
reactor_config, "cat_file_edits_steering_message", None
175+
),
176+
enabled=getattr(
177+
reactor_config, "cat_file_edits_steering_enabled", False
178+
),
179+
prompt_override_path=Path("config/prompts/steering_cat_file_edits.md"),
180+
),
181+
)
182+
168183
# Register ConfiguredRulesPolicy
169184
services.add_singleton(
170185
ConfiguredRulesPolicy,
@@ -251,6 +266,7 @@ def _register_unified_steering_handler(
251266
from src.services.steering import UnifiedSteeringHandler
252267
from src.services.steering.policies import (
253268
BinaryFileEditPolicy,
269+
CatFileEditsSteeringPolicy,
254270
InlinePythonPolicy,
255271
PytestFullSuitePolicy,
256272
)
@@ -261,6 +277,7 @@ def handler_factory(provider: IServiceProvider) -> UnifiedSteeringHandler:
261277
provider.get_required_service(InlinePythonPolicy),
262278
provider.get_required_service(BinaryFileEditPolicy),
263279
provider.get_required_service(PytestFullSuitePolicy),
280+
provider.get_required_service(CatFileEditsSteeringPolicy),
264281
provider.get_required_service(ConfiguredRulesPolicy),
265282
]
266283

src/core/cli_support/applicators/session_applicator.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,22 @@ def _apply_pytest_settings(
385385
origin="--enable/disable-pytest-full-suite-steering",
386386
)
387387

388+
if getattr(args, "cat_file_edits_steering_enabled", None) is not None:
389+
session = overrides.setdefault("session", {})
390+
session["cat_file_edits_steering_enabled"] = (
391+
args.cat_file_edits_steering_enabled
392+
)
393+
tool_call_reactor = session.setdefault("tool_call_reactor", {})
394+
tool_call_reactor["cat_file_edits_steering_enabled"] = (
395+
args.cat_file_edits_steering_enabled
396+
)
397+
resolution.record(
398+
"session.cat_file_edits_steering_enabled",
399+
args.cat_file_edits_steering_enabled,
400+
ParameterSource.CLI,
401+
origin="--enable-cat-file-edits-steering",
402+
)
403+
388404
if getattr(args, "disable_binary_file_edit_steering", None) is True:
389405
session = overrides.setdefault("session", {})
390406
tool_call_reactor = session.setdefault("tool_call_reactor", {})

src/core/cli_support/argument_parser_builder.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1168,6 +1168,15 @@ def _add_pytest_arguments(self, parser: argparse.ArgumentParser) -> None:
11681168
help="Disable steering for full pytest suite commands (overrides config)",
11691169
)
11701170

1171+
parser.add_argument(
1172+
"--enable-cat-file-edits-steering",
1173+
action="store_const",
1174+
const=True,
1175+
dest="cat_file_edits_steering_enabled",
1176+
default=None,
1177+
help="Enable steering for shell commands using cat > / cat >> to edit files (overrides config; off by default)",
1178+
)
1179+
11711180
# Pytest context saving
11721181
parser.add_argument(
11731182
"--enable-pytest-context-saving",

src/core/config/env/from_env_part1b.py

Lines changed: 53 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -80,30 +80,30 @@ def _optional_int(value: str) -> int | None:
8080
path="session.project_dir_resolution_model",
8181
resolution=resolution,
8282
),
83-
"project_dir_resolution_mode": _get_env_value(
84-
env,
85-
"PROJECT_DIR_RESOLUTION_MODE",
86-
"hybrid",
87-
path="session.project_dir_resolution_mode",
88-
resolution=resolution,
89-
),
90-
"project_dir_resolution_filesystem_mode": _get_env_value(
91-
env,
92-
"PROJECT_DIR_RESOLUTION_FILESYSTEM_MODE",
93-
"auto",
94-
path="session.project_dir_resolution_filesystem_mode",
95-
resolution=resolution,
96-
),
97-
"disable_default_openrouter_project_dir_resolution_fallback": _env_to_bool(
98-
"DISABLE_DEFAULT_OPENROUTER_PROJECT_DIR_RESOLUTION_FALLBACK",
99-
False,
100-
env,
101-
path="session.disable_default_openrouter_project_dir_resolution_fallback",
102-
resolution=resolution,
103-
),
104-
"tool_call_repair_enabled": _env_to_bool(
105-
"TOOL_CALL_REPAIR_ENABLED",
106-
True,
83+
"project_dir_resolution_mode": _get_env_value(
84+
env,
85+
"PROJECT_DIR_RESOLUTION_MODE",
86+
"hybrid",
87+
path="session.project_dir_resolution_mode",
88+
resolution=resolution,
89+
),
90+
"project_dir_resolution_filesystem_mode": _get_env_value(
91+
env,
92+
"PROJECT_DIR_RESOLUTION_FILESYSTEM_MODE",
93+
"auto",
94+
path="session.project_dir_resolution_filesystem_mode",
95+
resolution=resolution,
96+
),
97+
"disable_default_openrouter_project_dir_resolution_fallback": _env_to_bool(
98+
"DISABLE_DEFAULT_OPENROUTER_PROJECT_DIR_RESOLUTION_FALLBACK",
99+
False,
100+
env,
101+
path="session.disable_default_openrouter_project_dir_resolution_fallback",
102+
resolution=resolution,
103+
),
104+
"tool_call_repair_enabled": _env_to_bool(
105+
"TOOL_CALL_REPAIR_ENABLED",
106+
True,
107107
env,
108108
path="session.tool_call_repair_enabled",
109109
resolution=resolution,
@@ -167,6 +167,20 @@ def _optional_int(value: str) -> int | None:
167167
path="session.pytest_full_suite_steering_message",
168168
resolution=resolution,
169169
),
170+
"cat_file_edits_steering_enabled": _env_to_bool(
171+
"CAT_FILE_EDITS_STEERING_ENABLED",
172+
False,
173+
env,
174+
path="session.cat_file_edits_steering_enabled",
175+
resolution=resolution,
176+
),
177+
"cat_file_edits_steering_message": _get_env_value(
178+
env,
179+
"CAT_FILE_EDITS_STEERING_MESSAGE",
180+
None,
181+
path="session.cat_file_edits_steering_message",
182+
resolution=resolution,
183+
),
170184
"test_execution_reminder_enabled": _env_to_bool(
171185
"TEST_EXECUTION_REMINDER_ENABLED",
172186
False,
@@ -195,21 +209,21 @@ def _optional_int(value: str) -> int | None:
195209
path="session.fix_think_tags_streaming_buffer_size",
196210
resolution=resolution,
197211
),
198-
"double_ampersand_fixes_for_windows_enabled": _env_to_bool(
199-
"DOUBLE_AMPERSAND_FIXES_FOR_WINDOWS_ENABLED",
200-
True,
201-
env,
202-
path="session.double_ampersand_fixes_for_windows_enabled",
203-
resolution=resolution,
204-
),
205-
"auto_continue_removal_enabled": _env_to_bool(
206-
"AUTO_CONTINUE_REMOVAL_ENABLED",
207-
True,
208-
env,
209-
path="session.auto_continue_removal_enabled",
210-
resolution=resolution,
211-
),
212-
"planning_phase": {
212+
"double_ampersand_fixes_for_windows_enabled": _env_to_bool(
213+
"DOUBLE_AMPERSAND_FIXES_FOR_WINDOWS_ENABLED",
214+
True,
215+
env,
216+
path="session.double_ampersand_fixes_for_windows_enabled",
217+
resolution=resolution,
218+
),
219+
"auto_continue_removal_enabled": _env_to_bool(
220+
"AUTO_CONTINUE_REMOVAL_ENABLED",
221+
True,
222+
env,
223+
path="session.auto_continue_removal_enabled",
224+
resolution=resolution,
225+
),
226+
"planning_phase": {
213227
"enabled": _env_to_bool(
214228
"PLANNING_PHASE_ENABLED",
215229
False,

0 commit comments

Comments
 (0)