mixdog 1.1.0 → 1.1.2
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/README.md +13 -6
- package/package.json +1 -2
- package/scripts/latest-guard.mjs +63 -0
- package/scripts/lib/run-node-tests.mjs +103 -27
- package/scripts/lib/test-failure-summary.mjs +7 -2
- package/scripts/merge-mac-update-feed.mjs +107 -0
- package/scripts/mobile-release.mjs +210 -0
- package/scripts/post-release-check.mjs +353 -0
- package/scripts/publish-asset-release.mjs +43 -0
- package/scripts/release-guard.mjs +63 -0
- package/scripts/release-paths.mjs +39 -2
- package/scripts/test.mjs +29 -7
- package/src/defaults/shipped-history.json +2 -0
- package/src/defaults/skills/computer-use/SKILL.md +1 -1
- package/src/defaults/skills/setup/references/actions.md +1 -0
- package/src/headless-role.mjs +2 -2
- package/src/lib/keychain-cjs.cjs +582 -17
- package/src/lib/rules-builder.cjs +0 -23
- package/src/rules/lead/LEAD.md +2 -2
- package/src/runtime/agent/orchestrator/config-storage.mjs +32 -4
- package/src/runtime/agent/orchestrator/config.mjs +18 -0
- package/src/runtime/agent/orchestrator/context/instruction-files.mjs +130 -0
- package/src/runtime/agent/orchestrator/context/role-instructions.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/lib/open-login-url.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-login.mjs +254 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +218 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +239 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +233 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth-login.mjs +247 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +225 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +542 -0
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +13 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +14 -6
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +99 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +4 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +2 -7
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +4 -0
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +15 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +1 -5
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +12 -14
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +11 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-call-ids.mjs +90 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +19 -11
- package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +1 -5
- package/src/runtime/agent/orchestrator/session/read-dedup.mjs +0 -1
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-status.mjs +23 -0
- package/src/runtime/agent/orchestrator/session/turn-effort.mjs +18 -0
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +11 -8
- package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +9 -4
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/bashism-rewrite.mjs +245 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/command-preflight.mjs +13 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/result-format.mjs +49 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -7
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +61 -8
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +28 -3
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +73 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +17 -6
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +23 -5
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +93 -43
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +32 -6
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +12 -15
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +124 -91
- package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +19 -4
- package/src/runtime/channels/index.mjs +1 -5
- package/src/runtime/channels/lib/runtime-paths.mjs +8 -23
- package/src/runtime/channels/lib/tool-dispatch.mjs +1 -13
- package/src/runtime/channels/lib/worker-ipc.mjs +2 -2
- package/src/runtime/channels/lib/worker-main.mjs +4 -34
- package/src/runtime/computer-bridge/client.mjs +25 -1
- package/src/runtime/computer-bridge/linux-action-capabilities.mjs +84 -0
- package/src/runtime/computer-bridge/linux-readiness.d.mts +52 -0
- package/src/runtime/computer-bridge/linux-readiness.mjs +411 -0
- package/src/runtime/computer-bridge/test-linux-display.mjs +5 -0
- package/src/runtime/effort-judge/compact-tokenizer.mjs +13 -8
- package/src/runtime/effort-judge/judge-input.mjs +5 -2
- package/src/runtime/effort-judge/model-install.mjs +9 -3
- package/src/runtime/effort-judge/model-manifest.json +7 -7
- package/src/runtime/memory/lib/embedding-worker.mjs +9 -3
- package/src/runtime/memory/lib/pg/supervisor.mjs +3 -0
- package/src/runtime/shared/config.mjs +50 -2
- package/src/runtime/shared/developer-options.mjs +7 -0
- package/src/runtime/shared/instruction-files-mode.mjs +9 -0
- package/src/runtime/shared/llm/model-catalog.mjs +2 -1
- package/src/runtime/shared/llm/openai-daybreak.mjs +79 -0
- package/src/runtime/shared/pristine-execution-contract.json +9 -0
- package/src/runtime/shared/pristine-execution.mjs +1 -1
- package/src/runtime/shared/provider-accounts.mjs +2 -0
- package/src/runtime/shared/resource-admission.mjs +10 -3
- package/src/runtime/shared/schedule-model-ref.mjs +11 -0
- package/src/runtime/shared/semver.d.mts +1 -0
- package/src/runtime/shared/semver.mjs +57 -0
- package/src/runtime/shared/session-approval-wait.mjs +35 -0
- package/src/runtime/shared/shell-display.mjs +4 -2
- package/src/runtime/shared/tool-surface.mjs +2 -2
- package/src/runtime/shared/turn-worktree-snapshot.mjs +11 -10
- package/src/runtime/shared/update-checker.mjs +2 -31
- package/src/session-runtime/boot/apis.mjs +5 -0
- package/src/session-runtime/boot/begin.mjs +0 -2
- package/src/session-runtime/boot/tools.mjs +5 -4
- package/src/session-runtime/model-recency.mjs +3 -0
- package/src/session-runtime/provider-auth/login.mjs +10 -10
- package/src/session-runtime/provider-auth/setup.mjs +22 -2
- package/src/session-runtime/provider-models/row-hydration.mjs +12 -4
- package/src/session-runtime/provider-readiness.mjs +4 -0
- package/src/session-runtime/resource-mcp-api.mjs +7 -3
- package/src/session-runtime/runtime-bootstrap.mjs +0 -4
- package/src/session-runtime/runtime-facade.mjs +5 -0
- package/src/session-runtime/runtime-paths.mjs +0 -1
- package/src/session-runtime/schedule-session-run.mjs +3 -1
- package/src/session-runtime/services/agent-tool/index-write-queue.mjs +55 -14
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +15 -3
- package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +2 -0
- package/src/session-runtime/services/agent-tool/worker-index/approval-wait.mjs +97 -0
- package/src/session-runtime/services/agent-tool/worker-index/mutation-batch.mjs +4 -2
- package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -2
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +3 -0
- package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +58 -2
- package/src/session-runtime/services/agent-tool/worker-index.mjs +69 -15
- package/src/session-runtime/services/agent-tool/worker-rows.mjs +2 -0
- package/src/session-runtime/services/channel-admin.mjs +4 -2
- package/src/session-runtime/services/channel-worker.mjs +1 -1
- package/src/session-runtime/services/provider-admin.mjs +80 -17
- package/src/session-runtime/services/seeds.mjs +7 -10
- package/src/session-runtime/services/usage-dashboard-model.mjs +6 -0
- package/src/session-runtime/settings-builtin-tools-api.mjs +15 -0
- package/src/session-runtime/settings-profile-api.mjs +27 -2
- package/src/session-runtime/setup-tool/executor.mjs +5 -0
- package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
- package/src/session-runtime/starter-guide-state.mjs +62 -0
- package/src/session-runtime/tool-policy-refresh.mjs +3 -0
- package/src/session-runtime/webhook-session-run.mjs +3 -1
- package/src/session-runtime/workflow-agents-api/onboarding.mjs +3 -2
- package/src/standalone/channel-binding.mjs +1 -2
- package/src/standalone/channel-transport/call-dispatch.mjs +4 -10
- package/src/standalone/daemon-channels-loader.mjs +2 -3
- package/src/standalone/local-session-runtime.mjs +2 -78
- package/src/standalone/session-protocol.mjs +2 -0
- package/src/tui/app/model-options.mjs +13 -1
- package/src/tui/app/model-picker/route-selection/fast-parameters.mjs +10 -4
- package/src/tui/app/provider-setup/oauth-actions.mjs +5 -0
- package/src/tui/dist/index.mjs +84 -34
- package/src/tui/session/context-state.mjs +2 -0
- package/src/tui/session/live-share/owner.mjs +17 -0
- package/src/tui/session/oauth-flows.mjs +2 -0
- package/src/tui/session/session-api/extensions.mjs +12 -8
- package/src/tui/session/session-api/integrations.mjs +9 -8
- package/src/tui/session/session-api/routes.mjs +2 -2
- package/src/tui/session/session-api/settings.mjs +1 -0
- package/src/tui/session/session-api/shared.mjs +10 -6
- package/src/tui/session/tool-card-results/grouped-result.mjs +11 -2
- package/src/tui/session/transcript-spill/spill-dir.mjs +22 -6
- package/src/tui/spinner-meta.mjs +26 -10
- package/src/runtime/channels/tool-defs.mjs +0 -17
|
@@ -291,6 +291,29 @@ export function assignUiDiffFromMessage(target, source) {
|
|
|
291
291
|
return target;
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
/** Structured side channel a tool result may carry next to its text (the
|
|
295
|
+
* Computer Use capability refusal): a stable code plus the capability names.
|
|
296
|
+
* Anything else is dropped so stored history stays small and predictable. */
|
|
297
|
+
export function toolMetaFromMessage(source) {
|
|
298
|
+
const meta = source?.toolMeta ?? source?.content?.toolMeta;
|
|
299
|
+
if (!meta || typeof meta !== 'object' || typeof meta.code !== 'string' || !meta.code) return undefined;
|
|
300
|
+
const missing = Array.isArray(meta.missing)
|
|
301
|
+
? meta.missing.filter((name) => typeof name === 'string' && name)
|
|
302
|
+
: [];
|
|
303
|
+
return { code: meta.code, missing };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function toolMetaPatchFromMessage(source) {
|
|
307
|
+
const value = toolMetaFromMessage(source);
|
|
308
|
+
return value === undefined ? {} : { toolMeta: value };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function assignToolMetaFromMessage(target, source) {
|
|
312
|
+
const value = toolMetaFromMessage(source);
|
|
313
|
+
if (value !== undefined && target) target.toolMeta = value;
|
|
314
|
+
return target;
|
|
315
|
+
}
|
|
316
|
+
|
|
294
317
|
/** Stored/member records only copy a string uiDiff; other types stay omitted. */
|
|
295
318
|
export function stringUiDiffPatch(value) {
|
|
296
319
|
return typeof value === 'string' ? { uiDiff: value } : {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The effort the runtime resolved for the turn in flight: the newest message of
|
|
3
|
+
* the current turn carrying `meta.effortConfiguration.effort` — a step-judged
|
|
4
|
+
* tool result (step-auto-effort.mjs markStepEffort) or, before any step, the
|
|
5
|
+
* turn's own user message (prepareTurnEffortConfiguration). Null when the
|
|
6
|
+
* turn has no recorded value (auto effort off, or a model without it).
|
|
7
|
+
*/
|
|
8
|
+
export function currentTurnEffort(session) {
|
|
9
|
+
const live = Array.isArray(session?.liveTurnMessages) ? session.liveTurnMessages : session?.messages;
|
|
10
|
+
if (!Array.isArray(live)) return null;
|
|
11
|
+
for (let i = live.length - 1; i >= 0; i--) {
|
|
12
|
+
const message = live[i];
|
|
13
|
+
const effort = message?.meta?.effortConfiguration?.effort;
|
|
14
|
+
if (typeof effort === 'string' && effort) return effort;
|
|
15
|
+
if (message?.role === 'user') return null;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
@@ -183,9 +183,7 @@ function splitListedAbsoluteWindowsPaths(value) {
|
|
|
183
183
|
.trim()
|
|
184
184
|
.replace(/^\[([\s\S]*)\]$/, '$1')
|
|
185
185
|
.replace(/^[\s,;]+|[\s,;]+$/g, '');
|
|
186
|
-
const pieces = body
|
|
187
|
-
.split(/[\s,;]+(?=["']?[A-Za-z]:[\\/])/)
|
|
188
|
-
.map((piece) => piece.replace(/^(["'])(.*)\1$/, '$2'));
|
|
186
|
+
const pieces = body.split(/[\s,;]+(?=["']?[A-Za-z]:[\\/])/).map((piece) => piece.replace(/^(["'])(.*)\1$/, '$2'));
|
|
189
187
|
return pieces.every((piece) => /^[A-Za-z]:[\\/]/.test(piece) && !/[A-Za-z]:[\\/]/.test(piece.slice(3)))
|
|
190
188
|
? pieces
|
|
191
189
|
: null;
|
|
@@ -448,7 +446,7 @@ function guardGrep(a) {
|
|
|
448
446
|
}
|
|
449
447
|
if (hasOwn(a, k) && Array.isArray(a[k])) {
|
|
450
448
|
if (a[k].length === 0 || a[k].length > PUBLIC_PATH_BATCH_LIMIT || !a[k].every(isNonEmptyString)) {
|
|
451
|
-
return `Error: grep arg "${k}" must contain 1-${PUBLIC_PATH_BATCH_LIMIT} non-empty path strings`;
|
|
449
|
+
return `Error: grep arg "${k}" must contain 1-${PUBLIC_PATH_BATCH_LIMIT} non-empty path strings (${a[k].length} given, max ${PUBLIC_PATH_BATCH_LIMIT})`;
|
|
452
450
|
}
|
|
453
451
|
continue;
|
|
454
452
|
}
|
|
@@ -627,10 +625,12 @@ function normalizePublicReadTargets(a) {
|
|
|
627
625
|
return 'Error: read arg "file_path" must contain at least 1 target';
|
|
628
626
|
}
|
|
629
627
|
const windowValue = (value, fallback) => (value === undefined ? fallback : value);
|
|
628
|
+
// limit:0 is rejected by the public schema; read it as "no explicit limit".
|
|
629
|
+
const limitValue = (value, fallback) => (value === 0 ? fallback : windowValue(value, fallback));
|
|
630
630
|
const validOffset = (value) => Number.isSafeInteger(value) && value >= 1;
|
|
631
631
|
const validLimit = (value) => Number.isInteger(value) && value >= 1 && value <= MAX_INT;
|
|
632
632
|
const defaultOffset = windowValue(a.offset, 1);
|
|
633
|
-
const defaultLimit =
|
|
633
|
+
const defaultLimit = limitValue(a.limit, READ_GUARD_DEFAULT_LIMIT);
|
|
634
634
|
if (!validOffset(defaultOffset) || !validLimit(defaultLimit)) {
|
|
635
635
|
return `Error: read ${READ_WINDOW_RANGE_TEXT}`;
|
|
636
636
|
}
|
|
@@ -648,7 +648,7 @@ function normalizePublicReadTargets(a) {
|
|
|
648
648
|
return `Error: read target ${index + 1} must be a path string or {file_path, offset?, limit?}`;
|
|
649
649
|
}
|
|
650
650
|
const offset = windowValue(record.offset, defaultOffset);
|
|
651
|
-
const limit =
|
|
651
|
+
const limit = limitValue(record.limit, defaultLimit);
|
|
652
652
|
if (!validOffset(offset) || !validLimit(limit)) {
|
|
653
653
|
return `Error: read target ${index + 1} ${READ_WINDOW_RANGE_TEXT}`;
|
|
654
654
|
}
|
|
@@ -784,8 +784,11 @@ function guardShell(a) {
|
|
|
784
784
|
if (unsupported) {
|
|
785
785
|
return `Error: shell arg "${unsupported}" is unsupported; use only command, timeout_ms, and wait_ms`;
|
|
786
786
|
}
|
|
787
|
-
|
|
788
|
-
|
|
787
|
+
// Over-max is an unambiguous "wait as long as allowed"; clamp like the other
|
|
788
|
+
// saturating integer args instead of failing the call.
|
|
789
|
+
if (hasOwn(a, 'wait_ms') && Number.isInteger(a.wait_ms) && a.wait_ms > 120000) a.wait_ms = 120000;
|
|
790
|
+
if (hasOwn(a, 'wait_ms') && (!Number.isInteger(a.wait_ms) || a.wait_ms < 1000)) {
|
|
791
|
+
return `Error: shell arg "wait_ms" must be an integer from 15000 to 120000, e.g. wait_ms:30000 (got ${JSON.stringify(a.wait_ms)})`;
|
|
789
792
|
}
|
|
790
793
|
if (!hasOwn(a, 'command')) {
|
|
791
794
|
return 'Error: shell requires "command"';
|
|
@@ -95,16 +95,20 @@ function abortReasonOf(signal) {
|
|
|
95
95
|
|
|
96
96
|
// The payload lands in a 'wx' temp file: an existing temp file (random
|
|
97
97
|
// collision or a pre-existing symlink at the temp path) is rejected instead of
|
|
98
|
-
// silently truncated.
|
|
99
|
-
//
|
|
100
|
-
// scenarios on shared tmp dirs.
|
|
98
|
+
// silently truncated. Failed creation owns nothing: cleanup must leave an
|
|
99
|
+
// existing entry alone, including a symlink at the random temporary path.
|
|
101
100
|
async function writeTempFile(tmp, content, { mode, fsync }) {
|
|
102
101
|
let fh = null;
|
|
102
|
+
let ownsTemp = false;
|
|
103
103
|
try {
|
|
104
104
|
if (payloadByteLength(content) > STREAMING_THRESHOLD_BYTES) {
|
|
105
105
|
// Streaming path: avoid buffering the entire payload through
|
|
106
106
|
// fh.writeFile (which copies into a single Buffer).
|
|
107
107
|
const ws = createWriteStream(tmp, { flags: 'wx', mode });
|
|
108
|
+
// Stream construction precedes open; only the open event proves ownership.
|
|
109
|
+
ws.once('open', () => {
|
|
110
|
+
ownsTemp = true;
|
|
111
|
+
});
|
|
108
112
|
let source = content; // assume it's a Readable unless it is bytes/text
|
|
109
113
|
if (Buffer.isBuffer(content)) source = Readable.from([content]);
|
|
110
114
|
else if (typeof content === 'string') source = Readable.from([Buffer.from(content, 'utf-8')]);
|
|
@@ -117,6 +121,7 @@ async function writeTempFile(tmp, content, { mode, fsync }) {
|
|
|
117
121
|
}
|
|
118
122
|
} else {
|
|
119
123
|
fh = await fsPromises.open(tmp, 'wx', mode);
|
|
124
|
+
ownsTemp = true;
|
|
120
125
|
await fh.writeFile(content);
|
|
121
126
|
if (fsync) await fh.sync();
|
|
122
127
|
await fh.close();
|
|
@@ -128,7 +133,7 @@ async function writeTempFile(tmp, content, { mode, fsync }) {
|
|
|
128
133
|
} catch {
|
|
129
134
|
/* already closed */
|
|
130
135
|
}
|
|
131
|
-
await unlinkQuietly(tmp);
|
|
136
|
+
if (ownsTemp) await unlinkQuietly(tmp);
|
|
132
137
|
throw writeErr;
|
|
133
138
|
}
|
|
134
139
|
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// Pre-execution rewrite of bash-isms that ALWAYS fail in Windows PowerShell, so
|
|
2
|
+
// the first attempt succeeds instead of costing a failed call and a retry.
|
|
3
|
+
// Same placement and reporting as the wmic rewrite: the caller swaps in
|
|
4
|
+
// `command` and prints `note` above the output.
|
|
5
|
+
//
|
|
6
|
+
// What is rewritten (only outside quotes, here-strings and comments):
|
|
7
|
+
// <cmd> <<'EOF'\n…\nEOF → @'\n…\n'@ | <cmd> (python/python3/py/node only)
|
|
8
|
+
// 2>/dev/null >/dev/null &>/dev/null → 2>$null >$null *>$null
|
|
9
|
+
// </dev/null → removed (the shell's stdin is already at EOF)
|
|
10
|
+
// `| head`/`| tail`/`| wc` are deliberately NOT rewritten: any wrapper that
|
|
11
|
+
// could fall back to a cmdlet changes the streaming semantics of an existing
|
|
12
|
+
// `head`. They only get the failure hint (result-format.mjs).
|
|
13
|
+
//
|
|
14
|
+
// Anything not exactly one of these forms (`<<-`, `cat <<EOF > file`, an
|
|
15
|
+
// expanding unquoted heredoc body, unbalanced quotes, ...) makes the whole
|
|
16
|
+
// command pass through untouched.
|
|
17
|
+
|
|
18
|
+
const PYTHON_STAGE = /^(?:python3?|py)(?:\.exe)?(?:\s+-[uBsSEIOqbd]+)*(?:\s+-)?$/i;
|
|
19
|
+
const NODE_STAGE = /^node(?:\.exe)?(?:\s+--[\w-]+(?:=[\w.-]+)?)*(?:\s+-)?$/i;
|
|
20
|
+
|
|
21
|
+
const HEREDOC_OPENER = /^<<-?[ \t]*(?:'([A-Za-z_]\w*)'|"([A-Za-z_]\w*)"|([A-Za-z_]\w*))/;
|
|
22
|
+
|
|
23
|
+
/** Marks each character 1 when it is live PowerShell code and 0 when it sits in a
|
|
24
|
+
* string, here-string, comment or after a backtick. null when the text cannot be
|
|
25
|
+
* tokenised with confidence (unterminated string, `$(` inside a double quote). */
|
|
26
|
+
function codeMask(text) {
|
|
27
|
+
const mask = new Uint8Array(text.length);
|
|
28
|
+
// Bash heredoc bodies start on the line after their `<<DELIM` opener and are
|
|
29
|
+
// opaque text (quotes inside them must not be tokenised).
|
|
30
|
+
const pending = [];
|
|
31
|
+
let i = 0;
|
|
32
|
+
while (i < text.length) {
|
|
33
|
+
const ch = text[i];
|
|
34
|
+
if (ch === '`') {
|
|
35
|
+
i += 2;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (ch === '<' && text[i + 1] === '<') {
|
|
39
|
+
mask[i] = mask[i + 1] = 1;
|
|
40
|
+
const opener = HEREDOC_OPENER.exec(text.slice(i));
|
|
41
|
+
if (opener) pending.push(opener[1] || opener[2] || opener[3]);
|
|
42
|
+
i += 2;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (ch === '\n' && pending.length) {
|
|
46
|
+
mask[i++] = 1;
|
|
47
|
+
for (const delimiter of pending.splice(0)) {
|
|
48
|
+
for (;;) {
|
|
49
|
+
if (i >= text.length) return null;
|
|
50
|
+
const nl = text.indexOf('\n', i);
|
|
51
|
+
const stop = nl < 0 ? text.length : nl;
|
|
52
|
+
const isTerminator = text.slice(i, stop).replace(/\r$/, '') === delimiter;
|
|
53
|
+
i = nl < 0 ? text.length : nl + 1;
|
|
54
|
+
if (isTerminator) break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (ch === '<' && text[i + 1] === '#') {
|
|
60
|
+
const end = text.indexOf('#>', i + 2);
|
|
61
|
+
if (end < 0) return null;
|
|
62
|
+
i = end + 2;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (ch === '#' && (i === 0 || /[\s;({|]/.test(text[i - 1]))) {
|
|
66
|
+
const end = text.indexOf('\n', i);
|
|
67
|
+
i = end < 0 ? text.length : end;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (ch === '@' && (text[i + 1] === "'" || text[i + 1] === '"')) {
|
|
71
|
+
const quote = text[i + 1];
|
|
72
|
+
const open = /^[ \t]*\r?\n/.exec(text.slice(i + 2));
|
|
73
|
+
if (open) {
|
|
74
|
+
const close = text.indexOf(`\n${quote}@`, i + 2 + open[0].length - 1);
|
|
75
|
+
if (close < 0) return null;
|
|
76
|
+
i = close + 3;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (ch === "'") {
|
|
81
|
+
i++;
|
|
82
|
+
for (;;) {
|
|
83
|
+
const end = text.indexOf("'", i);
|
|
84
|
+
if (end < 0) return null;
|
|
85
|
+
if (text[end + 1] === "'") {
|
|
86
|
+
i = end + 2;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
i = end + 1;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (ch === '"') {
|
|
95
|
+
i++;
|
|
96
|
+
for (;;) {
|
|
97
|
+
if (i >= text.length) return null;
|
|
98
|
+
const c = text[i];
|
|
99
|
+
if (c === '`') {
|
|
100
|
+
i += 2;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (c === '$' && text[i + 1] === '(') return null;
|
|
104
|
+
if (c === '"') {
|
|
105
|
+
if (text[i + 1] === '"') {
|
|
106
|
+
i += 2;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
i++;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
i++;
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
mask[i++] = 1;
|
|
117
|
+
}
|
|
118
|
+
return mask;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// The text with every non-code character replaced by NUL, offsets unchanged, so
|
|
122
|
+
// regexes only ever see live code.
|
|
123
|
+
function codeOnly(text, mask) {
|
|
124
|
+
let out = '';
|
|
125
|
+
for (let i = 0; i < text.length; i++) out += mask[i] ? text[i] : '\0';
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** One heredoc → here-string rewrite, or { skip } when the heredoc is not one of
|
|
130
|
+
* the exactly-safe shapes. */
|
|
131
|
+
function rewriteOneHeredoc(text, at) {
|
|
132
|
+
const opener = /^<<(?![<-])[ \t]*(?:'([A-Za-z_]\w*)'|"([A-Za-z_]\w*)"|([A-Za-z_]\w*))/.exec(text.slice(at));
|
|
133
|
+
if (!opener) return { skip: true };
|
|
134
|
+
const delimiter = opener[1] || opener[2] || opener[3];
|
|
135
|
+
const expanding = Boolean(opener[3]);
|
|
136
|
+
const lineStart = text.lastIndexOf('\n', at - 1) + 1;
|
|
137
|
+
const lineEnd = text.indexOf('\n', at) < 0 ? text.length : text.indexOf('\n', at);
|
|
138
|
+
const before = text.slice(lineStart, at);
|
|
139
|
+
const rest = text.slice(at + opener[0].length, lineEnd).replace(/\r$/, '');
|
|
140
|
+
if (/['"`$<]/.test(rest) || lineEnd >= text.length) return { skip: true };
|
|
141
|
+
// The stage is what follows the last live `;` / `&&` on the opener line.
|
|
142
|
+
const beforeMask = codeMask(before);
|
|
143
|
+
if (!beforeMask) return { skip: true };
|
|
144
|
+
let stageFrom = 0;
|
|
145
|
+
for (let i = 0; i < before.length; i++) {
|
|
146
|
+
if (!beforeMask[i]) continue;
|
|
147
|
+
if (before[i] === ';') stageFrom = i + 1;
|
|
148
|
+
else if (before[i] === '&' && before[i + 1] === '&') stageFrom = ++i + 1;
|
|
149
|
+
else if (before[i] === '|' || before[i] === '&') return { skip: true };
|
|
150
|
+
}
|
|
151
|
+
const stage = before.slice(stageFrom).trim();
|
|
152
|
+
if (!PYTHON_STAGE.test(stage) && !NODE_STAGE.test(stage)) return { skip: true };
|
|
153
|
+
// Body: the lines up to the first line that is exactly the delimiter.
|
|
154
|
+
const bodyStart = lineEnd + 1;
|
|
155
|
+
let cursor = bodyStart;
|
|
156
|
+
let termStart = -1;
|
|
157
|
+
let termEnd = text.length;
|
|
158
|
+
while (cursor <= text.length) {
|
|
159
|
+
const nl = text.indexOf('\n', cursor);
|
|
160
|
+
const lineStop = nl < 0 ? text.length : nl;
|
|
161
|
+
if (text.slice(cursor, lineStop).replace(/\r$/, '') === delimiter) {
|
|
162
|
+
termStart = cursor;
|
|
163
|
+
termEnd = lineStop;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
if (nl < 0) break;
|
|
167
|
+
cursor = nl + 1;
|
|
168
|
+
}
|
|
169
|
+
if (termStart < 0) return { skip: true };
|
|
170
|
+
const body = text.slice(bodyStart, termStart).replace(/\r?\n$/, '');
|
|
171
|
+
// `'@` at a line start would end the here-string early; an unquoted heredoc
|
|
172
|
+
// body is only literal when bash would expand nothing in it.
|
|
173
|
+
if (/^'@/m.test(body)) return { skip: true };
|
|
174
|
+
if (expanding && /[$`]|\\[\\$`\n]|\\$/.test(body)) return { skip: true };
|
|
175
|
+
const prefix = before.slice(0, stageFrom);
|
|
176
|
+
const replacement = `${prefix}${prefix && !/\s$/.test(prefix) ? ' ' : ''}@'\n${body}\n'@ | ${stage}${rest ? ` ${rest.trim()}` : ''}`;
|
|
177
|
+
return {
|
|
178
|
+
text: text.slice(0, lineStart) + replacement + text.slice(termEnd),
|
|
179
|
+
note: `[rewritten: "${stage} <<${opener[1] || opener[2] ? `'${delimiter}'` : delimiter}" → "@'…'@ | ${stage}"]`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function rewriteHeredocs(text, notes) {
|
|
184
|
+
let current = text;
|
|
185
|
+
for (let guard = 0; guard < 20; guard++) {
|
|
186
|
+
const mask = codeMask(current);
|
|
187
|
+
if (!mask) return null;
|
|
188
|
+
let at = -1;
|
|
189
|
+
for (let i = current.indexOf('<<'); i >= 0; i = current.indexOf('<<', i + 1)) {
|
|
190
|
+
if (mask[i]) {
|
|
191
|
+
at = i;
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (at < 0) return current;
|
|
196
|
+
const done = rewriteOneHeredoc(current, at);
|
|
197
|
+
if (done.skip) return null;
|
|
198
|
+
notes.push(done.note);
|
|
199
|
+
current = done.text;
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const DEVNULL_REDIRECT = /(>>?)[ \t]*\/dev\/null(?![\w/.\\-])/g;
|
|
205
|
+
const DEVNULL_STDIN = /[ \t]*(?<!<)<[ \t]*\/dev\/null(?![\w/.\\-])/g;
|
|
206
|
+
|
|
207
|
+
// All edits are computed on the code-only view and applied back to front.
|
|
208
|
+
function collectEdits(text, mask, notes) {
|
|
209
|
+
const code = codeOnly(text, mask);
|
|
210
|
+
const edits = [];
|
|
211
|
+
for (const m of code.matchAll(DEVNULL_REDIRECT)) {
|
|
212
|
+
// `&>` (both streams) and a 1>/2> stream prefix belong to the redirection.
|
|
213
|
+
const before = code[m.index - 1];
|
|
214
|
+
const prefixed = before === '&' || ((before === '1' || before === '2') && !/\w/.test(code[m.index - 2] ?? ''));
|
|
215
|
+
const start = prefixed ? m.index - 1 : m.index;
|
|
216
|
+
let streamPrefix = '';
|
|
217
|
+
if (prefixed) streamPrefix = before === '&' ? '*' : before;
|
|
218
|
+
const next = `${streamPrefix}${m[1]}$null`;
|
|
219
|
+
edits.push({ start, end: m.index + m[0].length, text: next });
|
|
220
|
+
notes.push(`[rewritten: "${text.slice(start, m.index + m[0].length)}" → "${next}"]`);
|
|
221
|
+
}
|
|
222
|
+
for (const m of code.matchAll(DEVNULL_STDIN)) {
|
|
223
|
+
edits.push({ start: m.index, end: m.index + m[0].length, text: '' });
|
|
224
|
+
notes.push('[rewritten: "</dev/null" → "" (stdin is already at EOF)]');
|
|
225
|
+
}
|
|
226
|
+
return edits.sort((a, b) => b.start - a.start);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** `{ command, note }` when something was rewritten, else null. Callers gate on
|
|
230
|
+
* Windows + a PowerShell shell; nothing here inspects the platform. */
|
|
231
|
+
export function rewritePowerShellBashisms(command) {
|
|
232
|
+
const original = typeof command === 'string' ? command : '';
|
|
233
|
+
if (!original.includes('/dev/null') && !original.includes('<<')) return null;
|
|
234
|
+
const notes = [];
|
|
235
|
+
const text = rewriteHeredocs(original, notes);
|
|
236
|
+
if (text === null) return null;
|
|
237
|
+
const mask = codeMask(text);
|
|
238
|
+
if (!mask) return null;
|
|
239
|
+
let out = text;
|
|
240
|
+
for (const edit of collectEdits(text, mask, notes)) {
|
|
241
|
+
out = out.slice(0, edit.start) + edit.text + out.slice(edit.end);
|
|
242
|
+
}
|
|
243
|
+
if (out === original) return null;
|
|
244
|
+
return { command: out, note: [...new Set(notes)].join('\n') };
|
|
245
|
+
}
|
|
@@ -2,6 +2,7 @@ import { maybeRewriteWmicProcessCommand } from '../../shell-policy.mjs';
|
|
|
2
2
|
import { checkExecPolicyMessage } from '../../bash-policy-scan.mjs';
|
|
3
3
|
import { preflightPowerShellHygiene } from '../shell-analysis.mjs';
|
|
4
4
|
import { resolveShellFor } from '../shell-runtime.mjs';
|
|
5
|
+
import { rewritePowerShellBashisms } from './bashism-rewrite.mjs';
|
|
5
6
|
import { formatShellToolFailure } from './result-format.mjs';
|
|
6
7
|
|
|
7
8
|
// Policy + shell-specific rewrites before any shell dispatch. Returns either
|
|
@@ -40,7 +41,18 @@ export function prepareShellCommand(args) {
|
|
|
40
41
|
if (psHygiene.block) return { failure: formatShellToolFailure(psHygiene.block) };
|
|
41
42
|
command = psHygiene.command;
|
|
42
43
|
|
|
44
|
+
// Bash-isms that always fail in Windows PowerShell (heredoc to a script
|
|
45
|
+
// interpreter, /dev/null redirections, `| head/tail -N`, `| wc -l`) are
|
|
46
|
+
// rewritten before the first run; same shell gate and reporting as wmic.
|
|
47
|
+
// Effects and result analysis keep reading the command as the caller wrote it.
|
|
48
|
+
const analysisCommand = command;
|
|
49
|
+
const bashismRewrite =
|
|
50
|
+
process.platform === 'win32' && resolvedSpec.shellType === 'powershell' && !wmicRewrite?.command
|
|
51
|
+
? rewritePowerShellBashisms(command)
|
|
52
|
+
: null;
|
|
53
|
+
if (bashismRewrite) command = bashismRewrite.command;
|
|
54
|
+
|
|
43
55
|
const execPolicyBlock = checkExecPolicyMessage(command);
|
|
44
56
|
if (execPolicyBlock) return { failure: formatShellToolFailure(execPolicyBlock) };
|
|
45
|
-
return { command, resolvedSpec, wmicRewrite };
|
|
57
|
+
return { command, analysisCommand, resolvedSpec, wmicRewrite, bashismRewrite };
|
|
46
58
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
SURVIVING_DESCENDANTS_WARNING,
|
|
11
11
|
_composeShellFailure,
|
|
12
12
|
_finalizeShellResult,
|
|
13
|
+
_powershellBashismHint,
|
|
13
14
|
_placeDestructiveWarningsAfterStatus,
|
|
14
15
|
_prependDestructiveWarning,
|
|
15
16
|
_shellFailureStatus,
|
|
@@ -152,11 +153,12 @@ export function renderCompletedResult({
|
|
|
152
153
|
losslessCompaction
|
|
153
154
|
)}${rescueNote}`;
|
|
154
155
|
if (outcome.statusMarker) {
|
|
156
|
+
const bashismHint = commandExitedNonzero || isReallyErrored ? _powershellBashismHint(analysisCommand, stderr) : '';
|
|
155
157
|
const failure = _composeShellFailure(
|
|
156
158
|
`${outcome.statusMarker}${outcome.outcomeNote}`,
|
|
157
159
|
isReallyErrored ? 'Error: ' : '',
|
|
158
160
|
warningBlock,
|
|
159
|
-
payload
|
|
161
|
+
bashismHint ? `${payload}\n${bashismHint}` : payload
|
|
160
162
|
);
|
|
161
163
|
return _finalizeShellResult(completedExit, _placeDestructiveWarningsAfterStatus(command, failure));
|
|
162
164
|
}
|
|
@@ -2,12 +2,12 @@ import { wrapCommandWithSnapshot } from '../../shell-snapshot.mjs';
|
|
|
2
2
|
import { buildPowerShellFilterTeePlan } from '../shell-analysis.mjs';
|
|
3
3
|
import { normalizeErrorMessage } from '../path-diagnostics.mjs';
|
|
4
4
|
import { planDirectExeSpawn } from '../shell-direct-exe.mjs';
|
|
5
|
+
import { POWERSHELL_UTF8_PREFIX } from '../../../../../shared/shell-display.mjs';
|
|
5
6
|
import { buildShellSpawnEnv } from './spawn-env.mjs';
|
|
6
7
|
import { formatShellToolFailure } from './result-format.mjs';
|
|
7
8
|
|
|
8
9
|
function prefixPowerShellUtf8(command) {
|
|
9
|
-
const prefix =
|
|
10
|
-
'[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; $OutputEncoding=[System.Text.Encoding]::UTF8;';
|
|
10
|
+
const prefix = POWERSHELL_UTF8_PREFIX;
|
|
11
11
|
const text = String(command || '');
|
|
12
12
|
return text.trimStart().startsWith(prefix) ? text : `${prefix}\n${text}`;
|
|
13
13
|
}
|
|
@@ -114,6 +114,55 @@ export function _exitClassDiagnostic(exitCode) {
|
|
|
114
114
|
return '';
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
const _PS_UNIX_EQUIVALENTS = {
|
|
118
|
+
head: 'Select-Object -First N',
|
|
119
|
+
tail: 'Select-Object -Last N',
|
|
120
|
+
wc: 'Measure-Object -Line',
|
|
121
|
+
grep: 'Select-String (or the grep tool)',
|
|
122
|
+
sed: 'the edit tool',
|
|
123
|
+
awk: 'ForEach-Object / the edit tool',
|
|
124
|
+
pgrep: 'Get-Process -Name <name>',
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// One-line PowerShell equivalent for a failed command whose stderr is the
|
|
128
|
+
// shell's own "bash-ism" error. Only ever appended to an already failed run.
|
|
129
|
+
// Detection is locale-independent: the command text plus the error record's
|
|
130
|
+
// structural markers, never the localized message prose. PowerShell keeps these
|
|
131
|
+
// in English under any UI language: the `ParserError:` header or the
|
|
132
|
+
// `RedirectionNotSupported`/`MissingFileSpecification` ids for parse failures,
|
|
133
|
+
// and for a missing command either `ObjectNotFound: (name:String)` (detailed
|
|
134
|
+
// view) or a `name:` header followed by the quoted `'name'` (concise view).
|
|
135
|
+
function _missingCommandNames(err) {
|
|
136
|
+
const names = /\bCommandNotFoundException\b/.test(err)
|
|
137
|
+
? [...err.matchAll(/\bObjectNotFound: \(([\w.-]+):String\)/g)].map((m) => m[1])
|
|
138
|
+
: [];
|
|
139
|
+
for (const m of err.matchAll(/^([\w.-]+): ?\r?\n?((?:.*\r?\n){0,4})/gm)) {
|
|
140
|
+
if (m[2].includes(`'${m[1]}'`)) names.push(m[1]);
|
|
141
|
+
}
|
|
142
|
+
return names;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function _powershellBashismHint(command, stderr) {
|
|
146
|
+
if (process.platform !== 'win32') return '';
|
|
147
|
+
const err = String(stderr || '');
|
|
148
|
+
const cmd = String(command || '');
|
|
149
|
+
const parserRejected = /^ParserError:|\b(?:RedirectionNotSupported|MissingFileSpecification)\b/m.test(err);
|
|
150
|
+
if (/<\s*\/dev\/null/.test(cmd) && parserRejected) {
|
|
151
|
+
return 'hint: PowerShell has no `<` redirection; drop </dev/null.';
|
|
152
|
+
}
|
|
153
|
+
if (/<<-?\s*['"]?\w+/.test(cmd) && parserRejected) {
|
|
154
|
+
return "hint: PowerShell has no heredoc; use @'\\n…\\n'@ | python - (or node -).";
|
|
155
|
+
}
|
|
156
|
+
if (/2>\s*\/dev\/null/.test(cmd) && /dev\\null/i.test(err)) return 'hint: use 2>$null in PowerShell.';
|
|
157
|
+
for (const name of _missingCommandNames(err)) {
|
|
158
|
+
const key = name.toLowerCase();
|
|
159
|
+
if (Object.hasOwn(_PS_UNIX_EQUIVALENTS, key) && new RegExp(`(^|[\\s|;&(])${key}(\\s|$)`).test(cmd)) {
|
|
160
|
+
return `hint: no \`${key}\` in PowerShell; use ${_PS_UNIX_EQUIVALENTS[key]}.`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return '';
|
|
164
|
+
}
|
|
165
|
+
|
|
117
166
|
export function _composeShellFailure(statusMarker, errorPrefix, warningBlock, payload) {
|
|
118
167
|
return `${errorPrefix}${statusMarker}${warningBlock ? `\n${warningBlock}` : ''}\n\n${payload}`;
|
|
119
168
|
}
|
|
@@ -63,10 +63,10 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
63
63
|
const bashWorkDir = workDir;
|
|
64
64
|
const prepared = prepareShellCommand(args);
|
|
65
65
|
if (prepared.failure) return prepared.failure;
|
|
66
|
-
const { resolvedSpec, wmicRewrite } = prepared;
|
|
66
|
+
const { resolvedSpec, wmicRewrite, bashismRewrite } = prepared;
|
|
67
67
|
// Effects are analyzed on the command as the caller meant it, before the
|
|
68
68
|
// hoist turns it into a file run.
|
|
69
|
-
const analysisCommand = prepared.
|
|
69
|
+
const analysisCommand = prepared.analysisCommand;
|
|
70
70
|
const hoisted = hoistInlineScript(prepared.command, resolvedSpec.shellType);
|
|
71
71
|
const command = hoisted.command;
|
|
72
72
|
|
|
@@ -138,7 +138,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
138
138
|
...rendering,
|
|
139
139
|
analysisCommand,
|
|
140
140
|
timeout: deadlines.timeout,
|
|
141
|
-
rewriteNote: wmicRewrite?.note
|
|
141
|
+
rewriteNote: [wmicRewrite?.note, bashismRewrite?.note].filter(Boolean).join('\n'),
|
|
142
142
|
});
|
|
143
143
|
} finally {
|
|
144
144
|
combinedAbort?.cleanup?.();
|
|
@@ -30,7 +30,7 @@ export const TASK_WAIT_TIMEOUT_MIN_MS = 10_000;
|
|
|
30
30
|
export const TASK_WAIT_TIMEOUT_MAX_MS = 600_000;
|
|
31
31
|
const _shellSyntaxCheat =
|
|
32
32
|
process.platform === 'win32'
|
|
33
|
-
?
|
|
33
|
+
? " PowerShell: ; between independent commands, if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } between dependent ones; no heredoc, </dev/null or | head/tail/wc (Select-Object -First/-Last); multiline python/node via @'…'@ | python - (node -); 2>$null; /c/→C:\\; $PID is reserved."
|
|
34
34
|
: ' Bash: chain dependent commands with &&; multiline input via a quoted heredoc delimiter, not extra quoting/escape layers.';
|
|
35
35
|
// Process-stable switch used to describe foreground-only execution accurately.
|
|
36
36
|
const _shellBackgroundDisabled = envFlag('MIXDOG_SHELL_DISABLE_BACKGROUND_TASKS');
|
|
@@ -146,7 +146,7 @@ export const BUILTIN_TOOLS = [
|
|
|
146
146
|
openWorldHint: true,
|
|
147
147
|
compressible: true,
|
|
148
148
|
},
|
|
149
|
-
description: `Run programs, builds, tests and computation. Never use shell commands or scripts for work covered by dedicated tools: cat/head/tail→read, ls→list, find→glob, filename lookup→find, grep/rg→grep, code structure→code_graph, file edits/writes (sed/awk/redirection)→edit/apply_patch, Git→git when that tool is on the surface. Tool names are not shell commands. ${_shellBackgroundDisabled ? 'Commands run in the foreground until completion.' : 'After a 15s foreground window (not a timeout; wait_ms extends it up to 120s for one call, e.g. a test suite), unfinished work continues under task_id; use task wait, not read polling. If no execution slot frees within the admission wait, the command is queued as a background task with a task_id.'}`,
|
|
149
|
+
description: `Run programs, builds, tests and computation. Never use shell commands or scripts for work covered by dedicated tools: cat/head/tail→read, ls→list, find→glob, filename lookup→find, grep/rg→grep, code structure→code_graph, file edits/writes (sed/awk/redirection)→edit/apply_patch, Git→git when that tool is on the surface. Tool names are not shell commands. ${_shellBackgroundDisabled ? 'Commands run in the foreground until completion.' : 'After a 15s foreground window (not a timeout; wait_ms extends it up to 120s for one call, e.g. a test suite), unfinished work continues under task_id; use task wait, not read polling. If no execution slot frees within the admission wait, the command is queued as a background task with a task_id. Dev servers/watchers: pass timeout_ms or stop them with task cancel when done.'}`,
|
|
150
150
|
inputSchema: {
|
|
151
151
|
type: 'object',
|
|
152
152
|
properties: {
|
|
@@ -232,8 +232,7 @@ export const BUILTIN_TOOLS = [
|
|
|
232
232
|
properties: {
|
|
233
233
|
pattern: {
|
|
234
234
|
anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, maxItems: 10 }],
|
|
235
|
-
description:
|
|
236
|
-
'Ripgrep regex(es); escape literal metacharacters. Invalid regex is reported, never reinterpreted.',
|
|
235
|
+
description: 'Ripgrep regex(es); escape literal metacharacters.',
|
|
237
236
|
},
|
|
238
237
|
path: {
|
|
239
238
|
anyOf: [
|
|
@@ -241,7 +240,7 @@ export const BUILTIN_TOOLS = [
|
|
|
241
240
|
{ type: 'array', minItems: 1, maxItems: PUBLIC_PATH_BATCH_LIMIT, items: { type: 'string' } },
|
|
242
241
|
],
|
|
243
242
|
description:
|
|
244
|
-
'Plain file/directory scopes; omit for Project root. Every pattern searches every path; pattern/path pairs need separate calls.
|
|
243
|
+
'Plain file/directory scopes; omit for Project root. Every pattern searches every path; pattern/path pairs need separate calls.',
|
|
245
244
|
},
|
|
246
245
|
glob: {
|
|
247
246
|
type: 'string',
|
|
@@ -331,7 +330,7 @@ export const BUILTIN_TOOLS = [
|
|
|
331
330
|
compressible: true,
|
|
332
331
|
},
|
|
333
332
|
description:
|
|
334
|
-
'Fuzzy filename/directory path lookup; returns paths only. Only when the target path is unknown and cannot be directly resolved but a fragment is known; general discovery: glob.
|
|
333
|
+
'Fuzzy filename/directory path lookup; returns paths only. Only when the target path is unknown and cannot be directly resolved but a fragment is known; general discovery: glob.',
|
|
335
334
|
inputSchema: {
|
|
336
335
|
type: 'object',
|
|
337
336
|
properties: {
|
|
@@ -370,7 +369,10 @@ export const BUILTIN_TOOLS = [
|
|
|
370
369
|
description: 'Known directory; defaults to the current Project.',
|
|
371
370
|
},
|
|
372
371
|
hidden: { type: 'boolean', description: 'Include dotfiles.' },
|
|
373
|
-
meta: {
|
|
372
|
+
meta: {
|
|
373
|
+
type: 'boolean',
|
|
374
|
+
description: 'Per-entry size bytes, UTC mtime, octal mode; text files also get a line count.',
|
|
375
|
+
},
|
|
374
376
|
limit: {
|
|
375
377
|
type: 'integer',
|
|
376
378
|
minimum: 0,
|