@pushpalsdev/cli 1.0.46 → 1.0.48
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/dist/pushpals-cli.js
CHANGED
|
@@ -1523,6 +1523,9 @@ var MONITOR_POLL_MS = 2000;
|
|
|
1523
1523
|
var HTTP_TIMEOUT_MS = 2500;
|
|
1524
1524
|
var LOCALBUDDY_TIMEOUT_MS = 4000;
|
|
1525
1525
|
var SSE_RECONNECT_MS = 1500;
|
|
1526
|
+
var DOCKER_VERSION_PROBE_TIMEOUT_MS = 1e4;
|
|
1527
|
+
var WORKERPAL_IMAGE_INSPECT_TIMEOUT_MS = 15000;
|
|
1528
|
+
var WORKERPAL_IMAGE_BUILD_TIMEOUT_MS = 10 * 60000;
|
|
1526
1529
|
var DEFAULT_RUNTIME_BOOT_TIMEOUT_MS = 90000;
|
|
1527
1530
|
var DEFAULT_RUNTIME_BOOT_POLL_MS = 1000;
|
|
1528
1531
|
var DEFAULT_SERVER_BOOT_TIMEOUT_MS = 20000;
|
|
@@ -2948,7 +2951,7 @@ async function resolveWorkerpalDockerProbe(cwd, env, platform = process.platform
|
|
|
2948
2951
|
const candidates = resolveRuntimeDockerExecutableCandidates(env, platform);
|
|
2949
2952
|
const failures = [];
|
|
2950
2953
|
for (const candidate of candidates) {
|
|
2951
|
-
const result = await runCommandWithEnv([candidate, "version", "--format", "{{.Server.Version}}"], cwd, env);
|
|
2954
|
+
const result = await runCommandWithEnv([candidate, "version", "--format", "{{.Server.Version}}"], cwd, env, DOCKER_VERSION_PROBE_TIMEOUT_MS);
|
|
2952
2955
|
if (result.ok) {
|
|
2953
2956
|
const version = result.stdout.trim();
|
|
2954
2957
|
return {
|
|
@@ -3127,7 +3130,10 @@ async function cleanupLingeringPushPalsGitWorktrees(opts) {
|
|
|
3127
3130
|
removed
|
|
3128
3131
|
};
|
|
3129
3132
|
}
|
|
3130
|
-
|
|
3133
|
+
function isMissingDockerImageDetail(detail) {
|
|
3134
|
+
return /\b(no such object|no such image|not found)\b/i.test(String(detail ?? ""));
|
|
3135
|
+
}
|
|
3136
|
+
async function inspectDockerImageRuntimeTag(dockerExecutable, imageName, cwd, env, timeoutMs = WORKERPAL_IMAGE_INSPECT_TIMEOUT_MS) {
|
|
3131
3137
|
const inspect = await runCommandWithEnv([
|
|
3132
3138
|
dockerExecutable,
|
|
3133
3139
|
"image",
|
|
@@ -3135,11 +3141,23 @@ async function inspectDockerImageRuntimeTag(dockerExecutable, imageName, cwd, en
|
|
|
3135
3141
|
"--format",
|
|
3136
3142
|
`{{ index .Config.Labels "${WORKERPAL_SANDBOX_RUNTIME_TAG_LABEL}" }}`,
|
|
3137
3143
|
imageName
|
|
3138
|
-
], cwd, env);
|
|
3139
|
-
if (!inspect.ok)
|
|
3140
|
-
|
|
3144
|
+
], cwd, env, timeoutMs);
|
|
3145
|
+
if (!inspect.ok) {
|
|
3146
|
+
const detail = inspect.stderr || inspect.stdout || `exit ${inspect.exitCode}`;
|
|
3147
|
+
if (isMissingDockerImageDetail(detail)) {
|
|
3148
|
+
return { status: "missing", runtimeTag: "" };
|
|
3149
|
+
}
|
|
3150
|
+
return {
|
|
3151
|
+
status: "failed",
|
|
3152
|
+
runtimeTag: "",
|
|
3153
|
+
detail: `failed to inspect local WorkerPal sandbox image ${imageName}: ${detail}`
|
|
3154
|
+
};
|
|
3155
|
+
}
|
|
3141
3156
|
const value = inspect.stdout.trim();
|
|
3142
|
-
return
|
|
3157
|
+
return {
|
|
3158
|
+
status: "ok",
|
|
3159
|
+
runtimeTag: value === "<no value>" ? "" : value
|
|
3160
|
+
};
|
|
3143
3161
|
}
|
|
3144
3162
|
async function ensureWorkerpalDockerImageReady(opts) {
|
|
3145
3163
|
const runtimeTag = String(opts.runtimeTag ?? "").trim();
|
|
@@ -3160,7 +3178,15 @@ async function ensureWorkerpalDockerImageReady(opts) {
|
|
|
3160
3178
|
const dockerExecutable = resolveConfiguredDockerExecutable(opts.env, opts.platform ?? process.platform);
|
|
3161
3179
|
const inspectImageRuntimeTagFn = opts.inspectImageRuntimeTagFn ?? inspectDockerImageRuntimeTag;
|
|
3162
3180
|
const runCommandWithEnvFn = opts.runCommandWithEnvFn ?? runCommandWithEnv;
|
|
3163
|
-
|
|
3181
|
+
console.log(`[pushpals] Checking WorkerPal sandbox image ${opts.dockerImage} for runtimeTag=${runtimeTag}...`);
|
|
3182
|
+
const inspection = await inspectImageRuntimeTagFn(dockerExecutable, opts.dockerImage, sandbox.root, opts.env);
|
|
3183
|
+
if (inspection.status === "failed") {
|
|
3184
|
+
return {
|
|
3185
|
+
ok: false,
|
|
3186
|
+
detail: inspection.detail
|
|
3187
|
+
};
|
|
3188
|
+
}
|
|
3189
|
+
const existingRuntimeTag = inspection.runtimeTag;
|
|
3164
3190
|
if (existingRuntimeTag === runtimeTag) {
|
|
3165
3191
|
return {
|
|
3166
3192
|
ok: true,
|
|
@@ -3180,7 +3206,7 @@ async function ensureWorkerpalDockerImageReady(opts) {
|
|
|
3180
3206
|
"-t",
|
|
3181
3207
|
opts.dockerImage,
|
|
3182
3208
|
"."
|
|
3183
|
-
], sandbox.root, opts.env);
|
|
3209
|
+
], sandbox.root, opts.env, WORKERPAL_IMAGE_BUILD_TIMEOUT_MS);
|
|
3184
3210
|
if (!build.ok) {
|
|
3185
3211
|
const detail = build.stderr || build.stdout || `docker build exited ${build.exitCode}`;
|
|
3186
3212
|
return {
|
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"])
|