@pushpalsdev/cli 1.0.46 → 1.0.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -62,7 +62,6 @@ _CODEX_WORKAROUND_PATTERNS = (
|
|
|
62
62
|
),
|
|
63
63
|
re.compile(r"\bwithout requiring\b.{0,120}\bcodex\b", re.IGNORECASE),
|
|
64
64
|
re.compile(r"\bavoid(?:ing)?\b.{0,120}\bcodex\b.{0,120}\bcall", re.IGNORECASE),
|
|
65
|
-
re.compile(r"\b(fell back|fallback|worked around|workaround|bypass(?:ed)?|switched to)\b.{0,120}\bcodex\b", re.IGNORECASE),
|
|
66
65
|
)
|
|
67
66
|
_CODEX_WORKAROUND_NEGATION_HINTS = (
|
|
68
67
|
"do not",
|
|
@@ -75,6 +74,15 @@ _CODEX_WORKAROUND_NEGATION_HINTS = (
|
|
|
75
74
|
"explicit failure",
|
|
76
75
|
"codex cli is required infrastructure",
|
|
77
76
|
)
|
|
77
|
+
_REJECTED_EXEC_COMMAND_PATTERN = re.compile(r"exec_command failed for `([^`]+)`", re.IGNORECASE)
|
|
78
|
+
_DISALLOWED_SHELL_WRAPPER_PREFIXES = (
|
|
79
|
+
"/bin/bash -lc ",
|
|
80
|
+
"bash -lc ",
|
|
81
|
+
"sh -lc ",
|
|
82
|
+
"cmd /c ",
|
|
83
|
+
"powershell -command ",
|
|
84
|
+
"pwsh -command ",
|
|
85
|
+
)
|
|
78
86
|
|
|
79
87
|
_VALID_APPROVAL_POLICIES = {"untrusted", "on-failure", "on-request", "never"}
|
|
80
88
|
_VALID_SANDBOX_POLICIES = {"read-only", "workspace-write", "danger-full-access"}
|
|
@@ -942,6 +950,37 @@ def _detect_codex_workaround_signal(*texts: str) -> Optional[str]:
|
|
|
942
950
|
return None
|
|
943
951
|
|
|
944
952
|
|
|
953
|
+
def _normalize_command_text(command: str) -> str:
|
|
954
|
+
return re.sub(r"\s+", " ", str(command or "")).strip()
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
def _is_disallowed_shell_wrapper_command(command: str) -> bool:
|
|
958
|
+
normalized = _normalize_command_text(command).lower()
|
|
959
|
+
return any(normalized.startswith(prefix) for prefix in _DISALLOWED_SHELL_WRAPPER_PREFIXES)
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
def _extract_rejected_exec_command(text: str) -> str:
|
|
963
|
+
match = _REJECTED_EXEC_COMMAND_PATTERN.search(str(text or ""))
|
|
964
|
+
if not match:
|
|
965
|
+
return ""
|
|
966
|
+
return _normalize_command_text(match.group(1))
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def _collect_disallowed_shell_wrapper_rejections(*texts: str) -> List[str]:
|
|
970
|
+
rejected: List[str] = []
|
|
971
|
+
seen: set[str] = set()
|
|
972
|
+
for text in texts:
|
|
973
|
+
command = _extract_rejected_exec_command(str(text or ""))
|
|
974
|
+
if not command or not _is_disallowed_shell_wrapper_command(command):
|
|
975
|
+
continue
|
|
976
|
+
lowered = command.lower()
|
|
977
|
+
if lowered in seen:
|
|
978
|
+
continue
|
|
979
|
+
seen.add(lowered)
|
|
980
|
+
rejected.append(command)
|
|
981
|
+
return rejected
|
|
982
|
+
|
|
983
|
+
|
|
945
984
|
def _read_text_if_exists(path: Path) -> str:
|
|
946
985
|
try:
|
|
947
986
|
if not path.exists():
|
|
@@ -1166,6 +1205,7 @@ def _run_codex_task(
|
|
|
1166
1205
|
stdout_trace_state = _empty_codex_trace()
|
|
1167
1206
|
trace_lock = threading.Lock()
|
|
1168
1207
|
last_activity_at = {"ts": started_at}
|
|
1208
|
+
wrapper_rejection_state: Dict[str, Any] = {"count": 0, "commands": []}
|
|
1169
1209
|
|
|
1170
1210
|
def _drain_stdout() -> None:
|
|
1171
1211
|
stream = proc.stdout
|
|
@@ -1199,6 +1239,21 @@ def _run_codex_task(
|
|
|
1199
1239
|
if chunk == "":
|
|
1200
1240
|
break
|
|
1201
1241
|
stderr_chunks.append(chunk)
|
|
1242
|
+
rejected_commands = _collect_disallowed_shell_wrapper_rejections(chunk)
|
|
1243
|
+
if rejected_commands:
|
|
1244
|
+
with trace_lock:
|
|
1245
|
+
wrapper_rejection_state["count"] = to_int(
|
|
1246
|
+
wrapper_rejection_state.get("count"), 0
|
|
1247
|
+
) + len(rejected_commands)
|
|
1248
|
+
tracked = wrapper_rejection_state.get("commands")
|
|
1249
|
+
if not isinstance(tracked, list):
|
|
1250
|
+
tracked = []
|
|
1251
|
+
for command in rejected_commands:
|
|
1252
|
+
lowered = command.lower()
|
|
1253
|
+
if any(str(item).lower() == lowered for item in tracked):
|
|
1254
|
+
continue
|
|
1255
|
+
tracked.append(command)
|
|
1256
|
+
wrapper_rejection_state["commands"] = tracked[:6]
|
|
1202
1257
|
except Exception:
|
|
1203
1258
|
pass
|
|
1204
1259
|
finally:
|
|
@@ -1226,6 +1281,7 @@ def _run_codex_task(
|
|
|
1226
1281
|
)
|
|
1227
1282
|
next_progress_at = started_at + float(progress_interval_s)
|
|
1228
1283
|
timed_out = False
|
|
1284
|
+
command_policy_rejection_loop = False
|
|
1229
1285
|
|
|
1230
1286
|
while proc.poll() is None:
|
|
1231
1287
|
now = time.monotonic()
|
|
@@ -1234,6 +1290,13 @@ def _run_codex_task(
|
|
|
1234
1290
|
_terminate_active_child()
|
|
1235
1291
|
break
|
|
1236
1292
|
|
|
1293
|
+
with trace_lock:
|
|
1294
|
+
wrapper_rejections = to_int(wrapper_rejection_state.get("count"), 0)
|
|
1295
|
+
if wrapper_rejections >= 3:
|
|
1296
|
+
command_policy_rejection_loop = True
|
|
1297
|
+
_terminate_active_child()
|
|
1298
|
+
break
|
|
1299
|
+
|
|
1237
1300
|
if now >= next_progress_at:
|
|
1238
1301
|
elapsed = int(max(0.0, now - started_at))
|
|
1239
1302
|
with trace_lock:
|
|
@@ -1279,6 +1342,18 @@ def _run_codex_task(
|
|
|
1279
1342
|
part for part in (stdout, stderr, trace_excerpt) if str(part or "").strip()
|
|
1280
1343
|
)
|
|
1281
1344
|
usage = _usage_from_trace_or_estimate(stdout_trace, prompt, usage_output_text, model=model)
|
|
1345
|
+
rejected_shell_wrappers = _collect_disallowed_shell_wrapper_rejections(stdout, stderr)
|
|
1346
|
+
with trace_lock:
|
|
1347
|
+
tracked = wrapper_rejection_state.get("commands")
|
|
1348
|
+
if isinstance(tracked, list):
|
|
1349
|
+
for command in tracked:
|
|
1350
|
+
text = _normalize_command_text(str(command))
|
|
1351
|
+
if not text:
|
|
1352
|
+
continue
|
|
1353
|
+
lowered = text.lower()
|
|
1354
|
+
if any(entry.lower() == lowered for entry in rejected_shell_wrappers):
|
|
1355
|
+
continue
|
|
1356
|
+
rejected_shell_wrappers.append(text)
|
|
1282
1357
|
|
|
1283
1358
|
if timed_out:
|
|
1284
1359
|
detail = (
|
|
@@ -1300,6 +1375,30 @@ def _run_codex_task(
|
|
|
1300
1375
|
last_message = _read_text_if_exists(last_message_path)
|
|
1301
1376
|
log_git_status(repo, log)
|
|
1302
1377
|
|
|
1378
|
+
if command_policy_rejection_loop:
|
|
1379
|
+
command_lines = (
|
|
1380
|
+
"\n".join(f"- {command}" for command in rejected_shell_wrappers[:6])
|
|
1381
|
+
if rejected_shell_wrappers
|
|
1382
|
+
else "- (no command details captured)"
|
|
1383
|
+
)
|
|
1384
|
+
detail = (
|
|
1385
|
+
"Codex repeatedly attempted disallowed shell-wrapper commands that the command "
|
|
1386
|
+
"router rejected. Switch to direct commands only and avoid wrapper retries.\n"
|
|
1387
|
+
f"Rejected commands:\n{command_lines}"
|
|
1388
|
+
)
|
|
1389
|
+
if last_message:
|
|
1390
|
+
detail = f"{detail}\nLast assistant message:\n{last_message}"
|
|
1391
|
+
if trace_excerpt:
|
|
1392
|
+
detail = f"{detail}\n{trace_excerpt}"
|
|
1393
|
+
return {
|
|
1394
|
+
"ok": False,
|
|
1395
|
+
"summary": "openai_codex command policy rejection loop",
|
|
1396
|
+
"stdout": _truncate(stdout),
|
|
1397
|
+
"stderr": _truncate(detail),
|
|
1398
|
+
"exitCode": 6,
|
|
1399
|
+
"usage": usage,
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1303
1402
|
if _INTERRUPTED_SIGNAL is not None:
|
|
1304
1403
|
return {
|
|
1305
1404
|
"ok": False,
|
|
@@ -15,6 +15,7 @@ from openai_codex_executor import (
|
|
|
15
15
|
OpenAICodexRuntimeConfig,
|
|
16
16
|
_resolve_reasoning_effort,
|
|
17
17
|
_build_instruction,
|
|
18
|
+
_collect_disallowed_shell_wrapper_rejections,
|
|
18
19
|
_detect_codex_workaround_signal,
|
|
19
20
|
_extract_usage_counts,
|
|
20
21
|
_load_prompt_template,
|
|
@@ -163,6 +164,12 @@ class OpenAICodexRuntimeConfigTests(unittest.TestCase):
|
|
|
163
164
|
)
|
|
164
165
|
self.assertIsNone(signal)
|
|
165
166
|
|
|
167
|
+
def test_ignores_generic_workaround_language_without_unavailable_codex_context(self) -> None:
|
|
168
|
+
signal = _detect_codex_workaround_signal(
|
|
169
|
+
"This is a workaround case, so I am stopping here until the command router issue is fixed.",
|
|
170
|
+
)
|
|
171
|
+
self.assertIsNone(signal)
|
|
172
|
+
|
|
166
173
|
def test_discovers_repo_root_for_prompt_loading(self) -> None:
|
|
167
174
|
repo_root = _repo_root_for_prompt_loading()
|
|
168
175
|
self.assertTrue((repo_root / "prompts").is_dir())
|
|
@@ -190,6 +197,14 @@ class OpenAICodexRuntimeConfigTests(unittest.TestCase):
|
|
|
190
197
|
{"prompt_tokens": 120, "completion_tokens": 30, "total_tokens": 150},
|
|
191
198
|
)
|
|
192
199
|
|
|
200
|
+
def test_collects_disallowed_shell_wrapper_rejections(self) -> None:
|
|
201
|
+
commands = _collect_disallowed_shell_wrapper_rejections(
|
|
202
|
+
"error=exec_command failed for `/bin/bash -lc pwd`: CreateProcess { message: \"Rejected\" }",
|
|
203
|
+
"error=exec_command failed for `sh -lc \"git diff\"`: Rejected",
|
|
204
|
+
"error=exec_command failed for `pwd`: Rejected",
|
|
205
|
+
)
|
|
206
|
+
self.assertEqual(commands, ["/bin/bash -lc pwd", 'sh -lc "git diff"'])
|
|
207
|
+
|
|
193
208
|
def test_usage_falls_back_to_estimate_when_trace_has_no_usage(self) -> None:
|
|
194
209
|
usage = _usage_from_trace_or_estimate({}, "abc" * 30, "done", model="gpt-5.4")
|
|
195
210
|
self.assertTrue(usage["estimated"])
|