dominds 1.25.11 → 1.25.13

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.
Files changed (115) hide show
  1. package/README.md +1 -1
  2. package/README.zh.md +1 -1
  3. package/dist/access-control.js +9 -5
  4. package/dist/docs/team_mgmt-toolset.md +1 -1
  5. package/dist/docs/team_mgmt-toolset.zh.md +1 -1
  6. package/dist/llm/kernel-driver/sideDialog.d.ts +1 -0
  7. package/dist/llm/kernel-driver/sideDialog.js +29 -11
  8. package/dist/llm/kernel-driver/tellask-special.js +3 -0
  9. package/dist/persistence.d.ts +1 -0
  10. package/dist/persistence.js +23 -11
  11. package/dist/server/api-routes.js +65 -0
  12. package/dist/server/dialog-forensics-routes.d.ts +2 -0
  13. package/dist/server/dialog-forensics-routes.js +549 -0
  14. package/dist/server/dominds-self-update.js +54 -31
  15. package/dist/tools/builtins.js +14 -16
  16. package/dist/tools/os.d.ts +14 -0
  17. package/dist/tools/os.js +508 -101
  18. package/dist/tools/prompts/codex_inspect_and_patch_tools/en/tools.md +2 -1
  19. package/dist/tools/prompts/codex_inspect_and_patch_tools/zh/tools.md +2 -1
  20. package/package.json +3 -3
  21. package/webapp/dist/assets/{_basePickBy-BF9Zg9uq.js → _basePickBy-B1-brAPU.js} +3 -3
  22. package/webapp/dist/assets/{_basePickBy-BF9Zg9uq.js.map → _basePickBy-B1-brAPU.js.map} +1 -1
  23. package/webapp/dist/assets/{_baseUniq-CFjISsgz.js → _baseUniq-BuB0jmKD.js} +2 -2
  24. package/webapp/dist/assets/{_baseUniq-CFjISsgz.js.map → _baseUniq-BuB0jmKD.js.map} +1 -1
  25. package/webapp/dist/assets/{arc-BVyYGzE7.js → arc-CvsBvjnB.js} +2 -2
  26. package/webapp/dist/assets/{arc-BVyYGzE7.js.map → arc-CvsBvjnB.js.map} +1 -1
  27. package/webapp/dist/assets/{architectureDiagram-2XIMDMQ5-SEmNTU1b.js → architectureDiagram-2XIMDMQ5-B-xGuLbb.js} +7 -7
  28. package/webapp/dist/assets/{architectureDiagram-2XIMDMQ5-SEmNTU1b.js.map → architectureDiagram-2XIMDMQ5-B-xGuLbb.js.map} +1 -1
  29. package/webapp/dist/assets/{blockDiagram-WCTKOSBZ-BndD4gLF.js → blockDiagram-WCTKOSBZ-Bu53fwsa.js} +7 -7
  30. package/webapp/dist/assets/{blockDiagram-WCTKOSBZ-BndD4gLF.js.map → blockDiagram-WCTKOSBZ-Bu53fwsa.js.map} +1 -1
  31. package/webapp/dist/assets/{c4Diagram-IC4MRINW-fGAz7umu.js → c4Diagram-IC4MRINW-D9-FJ4LB.js} +3 -3
  32. package/webapp/dist/assets/{c4Diagram-IC4MRINW-fGAz7umu.js.map → c4Diagram-IC4MRINW-D9-FJ4LB.js.map} +1 -1
  33. package/webapp/dist/assets/{channel-Blt7S1Sn.js → channel-D1VPurpu.js} +2 -2
  34. package/webapp/dist/assets/{channel-Blt7S1Sn.js.map → channel-D1VPurpu.js.map} +1 -1
  35. package/webapp/dist/assets/{chunk-4BX2VUAB-C2FKcyob.js → chunk-4BX2VUAB-DB14wcWS.js} +2 -2
  36. package/webapp/dist/assets/{chunk-4BX2VUAB-C2FKcyob.js.map → chunk-4BX2VUAB-DB14wcWS.js.map} +1 -1
  37. package/webapp/dist/assets/{chunk-55IACEB6-CN8ZmdUP.js → chunk-55IACEB6-C5KCE85A.js} +2 -2
  38. package/webapp/dist/assets/{chunk-55IACEB6-CN8ZmdUP.js.map → chunk-55IACEB6-C5KCE85A.js.map} +1 -1
  39. package/webapp/dist/assets/{chunk-FMBD7UC4-B9Uq2tt2.js → chunk-FMBD7UC4-Bb6zm0iH.js} +2 -2
  40. package/webapp/dist/assets/{chunk-FMBD7UC4-B9Uq2tt2.js.map → chunk-FMBD7UC4-Bb6zm0iH.js.map} +1 -1
  41. package/webapp/dist/assets/{chunk-JSJVCQXG-vVrXi8LV.js → chunk-JSJVCQXG-CJPrv6fM.js} +2 -2
  42. package/webapp/dist/assets/{chunk-JSJVCQXG-vVrXi8LV.js.map → chunk-JSJVCQXG-CJPrv6fM.js.map} +1 -1
  43. package/webapp/dist/assets/{chunk-KX2RTZJC-DtZmdBq3.js → chunk-KX2RTZJC-8J1Swk7E.js} +2 -2
  44. package/webapp/dist/assets/{chunk-KX2RTZJC-DtZmdBq3.js.map → chunk-KX2RTZJC-8J1Swk7E.js.map} +1 -1
  45. package/webapp/dist/assets/{chunk-NQ4KR5QH-C3rU1XEw.js → chunk-NQ4KR5QH-DySHY4x9.js} +4 -4
  46. package/webapp/dist/assets/{chunk-NQ4KR5QH-C3rU1XEw.js.map → chunk-NQ4KR5QH-DySHY4x9.js.map} +1 -1
  47. package/webapp/dist/assets/{chunk-QZHKN3VN-CeHQK_vs.js → chunk-QZHKN3VN-CTO5Z-4P.js} +2 -2
  48. package/webapp/dist/assets/{chunk-QZHKN3VN-CeHQK_vs.js.map → chunk-QZHKN3VN-CTO5Z-4P.js.map} +1 -1
  49. package/webapp/dist/assets/{chunk-WL4C6EOR-Bb8GUwSo.js → chunk-WL4C6EOR-Ci_Ec8Ax.js} +6 -6
  50. package/webapp/dist/assets/{chunk-WL4C6EOR-Bb8GUwSo.js.map → chunk-WL4C6EOR-Ci_Ec8Ax.js.map} +1 -1
  51. package/webapp/dist/assets/{classDiagram-VBA2DB6C-CmP7X8Fj.js → classDiagram-VBA2DB6C-CxAxBvv7.js} +7 -7
  52. package/webapp/dist/assets/{classDiagram-VBA2DB6C-CmP7X8Fj.js.map → classDiagram-VBA2DB6C-CxAxBvv7.js.map} +1 -1
  53. package/webapp/dist/assets/{classDiagram-v2-RAHNMMFH-CmP7X8Fj.js → classDiagram-v2-RAHNMMFH-CxAxBvv7.js} +7 -7
  54. package/webapp/dist/assets/{classDiagram-v2-RAHNMMFH-CmP7X8Fj.js.map → classDiagram-v2-RAHNMMFH-CxAxBvv7.js.map} +1 -1
  55. package/webapp/dist/assets/{clone-CzGKO47U.js → clone-ePiNaiNY.js} +2 -2
  56. package/webapp/dist/assets/{clone-CzGKO47U.js.map → clone-ePiNaiNY.js.map} +1 -1
  57. package/webapp/dist/assets/{cose-bilkent-S5V4N54A-3wbordhk.js → cose-bilkent-S5V4N54A-j-Ex-Sef.js} +2 -2
  58. package/webapp/dist/assets/{cose-bilkent-S5V4N54A-3wbordhk.js.map → cose-bilkent-S5V4N54A-j-Ex-Sef.js.map} +1 -1
  59. package/webapp/dist/assets/{dagre-KLK3FWXG-DrM7hFR5.js → dagre-KLK3FWXG-ihZ2wOCM.js} +7 -7
  60. package/webapp/dist/assets/{dagre-KLK3FWXG-DrM7hFR5.js.map → dagre-KLK3FWXG-ihZ2wOCM.js.map} +1 -1
  61. package/webapp/dist/assets/{diagram-E7M64L7V-D-bQEVk2.js → diagram-E7M64L7V-Cp4GQGS7.js} +8 -8
  62. package/webapp/dist/assets/{diagram-E7M64L7V-D-bQEVk2.js.map → diagram-E7M64L7V-Cp4GQGS7.js.map} +1 -1
  63. package/webapp/dist/assets/{diagram-IFDJBPK2-MaU_xQfR.js → diagram-IFDJBPK2-B70cgyS5.js} +7 -7
  64. package/webapp/dist/assets/{diagram-IFDJBPK2-MaU_xQfR.js.map → diagram-IFDJBPK2-B70cgyS5.js.map} +1 -1
  65. package/webapp/dist/assets/{diagram-P4PSJMXO-BLklmc9h.js → diagram-P4PSJMXO-DMOv7eKE.js} +7 -7
  66. package/webapp/dist/assets/{diagram-P4PSJMXO-BLklmc9h.js.map → diagram-P4PSJMXO-DMOv7eKE.js.map} +1 -1
  67. package/webapp/dist/assets/{erDiagram-INFDFZHY-DHIVFsNv.js → erDiagram-INFDFZHY-BKpXWjIc.js} +5 -5
  68. package/webapp/dist/assets/{erDiagram-INFDFZHY-DHIVFsNv.js.map → erDiagram-INFDFZHY-BKpXWjIc.js.map} +1 -1
  69. package/webapp/dist/assets/{flowDiagram-PKNHOUZH-HbMxSdg1.js → flowDiagram-PKNHOUZH-DgrItj0h.js} +7 -7
  70. package/webapp/dist/assets/{flowDiagram-PKNHOUZH-HbMxSdg1.js.map → flowDiagram-PKNHOUZH-DgrItj0h.js.map} +1 -1
  71. package/webapp/dist/assets/{ganttDiagram-A5KZAMGK-CxBETPNC.js → ganttDiagram-A5KZAMGK-7-8hlYsT.js} +3 -3
  72. package/webapp/dist/assets/{ganttDiagram-A5KZAMGK-CxBETPNC.js.map → ganttDiagram-A5KZAMGK-7-8hlYsT.js.map} +1 -1
  73. package/webapp/dist/assets/{gitGraphDiagram-K3NZZRJ6-DPV-fFTC.js → gitGraphDiagram-K3NZZRJ6-cPSaCUUk.js} +8 -8
  74. package/webapp/dist/assets/{gitGraphDiagram-K3NZZRJ6-DPV-fFTC.js.map → gitGraphDiagram-K3NZZRJ6-cPSaCUUk.js.map} +1 -1
  75. package/webapp/dist/assets/{graph-C0MlCXJg.js → graph-CAlg3tEk.js} +3 -3
  76. package/webapp/dist/assets/{graph-C0MlCXJg.js.map → graph-CAlg3tEk.js.map} +1 -1
  77. package/webapp/dist/assets/{index-CzHjX_nj.js → index-DLTS_eOh.js} +123 -47
  78. package/webapp/dist/assets/{index-CzHjX_nj.js.map → index-DLTS_eOh.js.map} +1 -1
  79. package/webapp/dist/assets/{infoDiagram-LFFYTUFH-ChTC2kD-.js → infoDiagram-LFFYTUFH-CHJHvxMC.js} +6 -6
  80. package/webapp/dist/assets/{infoDiagram-LFFYTUFH-ChTC2kD-.js.map → infoDiagram-LFFYTUFH-CHJHvxMC.js.map} +1 -1
  81. package/webapp/dist/assets/{ishikawaDiagram-PHBUUO56--aJd3LM6.js → ishikawaDiagram-PHBUUO56-S8N-XZ8E.js} +2 -2
  82. package/webapp/dist/assets/{ishikawaDiagram-PHBUUO56--aJd3LM6.js.map → ishikawaDiagram-PHBUUO56-S8N-XZ8E.js.map} +1 -1
  83. package/webapp/dist/assets/{journeyDiagram-4ABVD52K-Bzd3cZTs.js → journeyDiagram-4ABVD52K-ChHNpMtH.js} +5 -5
  84. package/webapp/dist/assets/{journeyDiagram-4ABVD52K-Bzd3cZTs.js.map → journeyDiagram-4ABVD52K-ChHNpMtH.js.map} +1 -1
  85. package/webapp/dist/assets/{kanban-definition-K7BYSVSG-DiFHcs58.js → kanban-definition-K7BYSVSG-Cqxd99wZ.js} +3 -3
  86. package/webapp/dist/assets/{kanban-definition-K7BYSVSG-DiFHcs58.js.map → kanban-definition-K7BYSVSG-Cqxd99wZ.js.map} +1 -1
  87. package/webapp/dist/assets/{layout-B9Hsf17G.js → layout-uOLcVthp.js} +5 -5
  88. package/webapp/dist/assets/{layout-B9Hsf17G.js.map → layout-uOLcVthp.js.map} +1 -1
  89. package/webapp/dist/assets/{linear-6xqU78Yu.js → linear-Ga_f4H_w.js} +2 -2
  90. package/webapp/dist/assets/{linear-6xqU78Yu.js.map → linear-Ga_f4H_w.js.map} +1 -1
  91. package/webapp/dist/assets/{mindmap-definition-YRQLILUH-BUI6M5up.js → mindmap-definition-YRQLILUH-TSH7wOlZ.js} +4 -4
  92. package/webapp/dist/assets/{mindmap-definition-YRQLILUH-BUI6M5up.js.map → mindmap-definition-YRQLILUH-TSH7wOlZ.js.map} +1 -1
  93. package/webapp/dist/assets/{pieDiagram-SKSYHLDU-FCGp31Lg.js → pieDiagram-SKSYHLDU-DPXszqns.js} +8 -8
  94. package/webapp/dist/assets/{pieDiagram-SKSYHLDU-FCGp31Lg.js.map → pieDiagram-SKSYHLDU-DPXszqns.js.map} +1 -1
  95. package/webapp/dist/assets/{quadrantDiagram-337W2JSQ-bJcMGXM6.js → quadrantDiagram-337W2JSQ-BgA_GVhR.js} +3 -3
  96. package/webapp/dist/assets/{quadrantDiagram-337W2JSQ-bJcMGXM6.js.map → quadrantDiagram-337W2JSQ-BgA_GVhR.js.map} +1 -1
  97. package/webapp/dist/assets/{requirementDiagram-Z7DCOOCP-BghuL9nW.js → requirementDiagram-Z7DCOOCP-CM-47daj.js} +4 -4
  98. package/webapp/dist/assets/{requirementDiagram-Z7DCOOCP-BghuL9nW.js.map → requirementDiagram-Z7DCOOCP-CM-47daj.js.map} +1 -1
  99. package/webapp/dist/assets/{sankeyDiagram-WA2Y5GQK-DpuFpv6d.js → sankeyDiagram-WA2Y5GQK-CxDCwHAj.js} +2 -2
  100. package/webapp/dist/assets/{sankeyDiagram-WA2Y5GQK-DpuFpv6d.js.map → sankeyDiagram-WA2Y5GQK-CxDCwHAj.js.map} +1 -1
  101. package/webapp/dist/assets/{sequenceDiagram-2WXFIKYE-BM6rPANl.js → sequenceDiagram-2WXFIKYE-1UJP6Fff.js} +4 -4
  102. package/webapp/dist/assets/{sequenceDiagram-2WXFIKYE-BM6rPANl.js.map → sequenceDiagram-2WXFIKYE-1UJP6Fff.js.map} +1 -1
  103. package/webapp/dist/assets/{stateDiagram-RAJIS63D-C_jQSre0.js → stateDiagram-RAJIS63D-B2aqr0KQ.js} +9 -9
  104. package/webapp/dist/assets/{stateDiagram-RAJIS63D-C_jQSre0.js.map → stateDiagram-RAJIS63D-B2aqr0KQ.js.map} +1 -1
  105. package/webapp/dist/assets/{stateDiagram-v2-FVOUBMTO-BbQxj-LI.js → stateDiagram-v2-FVOUBMTO-DvmkevVb.js} +5 -5
  106. package/webapp/dist/assets/{stateDiagram-v2-FVOUBMTO-BbQxj-LI.js.map → stateDiagram-v2-FVOUBMTO-DvmkevVb.js.map} +1 -1
  107. package/webapp/dist/assets/{timeline-definition-YZTLITO2-qVPiYzDY.js → timeline-definition-YZTLITO2-CaOrqzT1.js} +3 -3
  108. package/webapp/dist/assets/{timeline-definition-YZTLITO2-qVPiYzDY.js.map → timeline-definition-YZTLITO2-CaOrqzT1.js.map} +1 -1
  109. package/webapp/dist/assets/{treemap-KZPCXAKY-CH_Gjw5E.js → treemap-KZPCXAKY-CWs_8GJm.js} +5 -5
  110. package/webapp/dist/assets/{treemap-KZPCXAKY-CH_Gjw5E.js.map → treemap-KZPCXAKY-CWs_8GJm.js.map} +1 -1
  111. package/webapp/dist/assets/{vennDiagram-LZ73GAT5-rpOhNWvx.js → vennDiagram-LZ73GAT5-BuBxFDz6.js} +2 -2
  112. package/webapp/dist/assets/{vennDiagram-LZ73GAT5-rpOhNWvx.js.map → vennDiagram-LZ73GAT5-BuBxFDz6.js.map} +1 -1
  113. package/webapp/dist/assets/{xychartDiagram-JWTSCODW-D1TcBJrI.js → xychartDiagram-JWTSCODW-ChjL-_2b.js} +3 -3
  114. package/webapp/dist/assets/{xychartDiagram-JWTSCODW-D1TcBJrI.js.map → xychartDiagram-JWTSCODW-ChjL-_2b.js.map} +1 -1
  115. package/webapp/dist/index.html +1 -1
package/dist/tools/os.js CHANGED
@@ -15,6 +15,8 @@ exports.resolveShellCmdSpawnSpecForTests = resolveShellCmdSpawnSpecForTests;
15
15
  exports.detectWindowsShellUsageWarningForTests = detectWindowsShellUsageWarningForTests;
16
16
  exports.formatShellExecutionErrorForTests = formatShellExecutionErrorForTests;
17
17
  exports.resolveReadonlyShellSpawnSpecForTests = resolveReadonlyShellSpawnSpecForTests;
18
+ exports.validateReadonlyShellCommandForTests = validateReadonlyShellCommandForTests;
19
+ exports.detectReadonlyShellForbiddenHiddenDirAccessForTests = detectReadonlyShellForbiddenHiddenDirAccessForTests;
18
20
  const time_1 = require("@longrun-ai/kernel/utils/time");
19
21
  const child_process_1 = require("child_process");
20
22
  const crypto_1 = __importDefault(require("crypto"));
@@ -1194,7 +1196,7 @@ const readonlyShellSchema = {
1194
1196
  properties: {
1195
1197
  command: {
1196
1198
  type: 'string',
1197
- description: 'Read-only shell command (allowed prefixes: cat, rg, sed, ls, nl, wc, head, tail, stat, file, uname, whoami, id, echo, pwd, which, date, diff, realpath, readlink, printf, cut, sort, uniq, tr, awk, shasum, sha256sum, md5sum, uuid, git show, git status, git diff, git log, git blame, find, tree, jq, true; exact version probes: node --version|-v, python3 --version|-V; also allows: git -C <relative-path> <show|status|diff|log|blame> ...; also allows: cd <relative-path> && <allowed command...> (or ||); command chains via |/&&/|| are validated segment-by-segment)',
1199
+ description: 'Read-only shell command (common allowed prefixes: cat, rg, sed, ls, nl, wc, head, tail, stat, file, uname, whoami, id, echo, pwd, which, date, diff, realpath, readlink, printf, cut, sort, uniq, tr, awk, shasum, sha256sum, md5sum, uuid, git show, git status, git diff, git log, git blame, find, tree, jq, true; Windows also allows: where, fc, findstr, dir, type, more, ver; exact version probes: node --version|-v, python3/python/py --version|-V; also allows: git -C <relative-path> <show|status|diff|log|blame> ...; also allows: cd <relative-path> && <allowed command...> (or ||); command chains via |/&&/|| are validated segment-by-segment)',
1198
1200
  },
1199
1201
  timeout_ms: {
1200
1202
  type: 'number',
@@ -1621,7 +1623,7 @@ exports.shellCmdTool = {
1621
1623
  }
1622
1624
  },
1623
1625
  };
1624
- const readonlyShellAllowedPrefixes = [
1626
+ const readonlyShellCommonAllowedPrefixes = [
1625
1627
  'cat',
1626
1628
  'rg',
1627
1629
  'sed',
@@ -1662,22 +1664,42 @@ const readonlyShellAllowedPrefixes = [
1662
1664
  'jq',
1663
1665
  'true',
1664
1666
  ];
1665
- function isAllowedReadonlyShellVersionProbe(command) {
1666
- const tokens = splitShellTokens(command);
1667
+ const readonlyShellWindowsAllowedPrefixes = [
1668
+ 'where',
1669
+ 'fc',
1670
+ 'findstr',
1671
+ 'dir',
1672
+ 'type',
1673
+ 'more',
1674
+ 'ver',
1675
+ ];
1676
+ function getReadonlyShellAllowedPrefixes(platform = process.platform) {
1677
+ if (platform === 'win32') {
1678
+ return [...readonlyShellCommonAllowedPrefixes, ...readonlyShellWindowsAllowedPrefixes];
1679
+ }
1680
+ return readonlyShellCommonAllowedPrefixes;
1681
+ }
1682
+ function isAllowedReadonlyShellVersionProbe(command, platform = process.platform) {
1683
+ const tokens = splitShellTokens(command, platform);
1667
1684
  if (tokens.length !== 2)
1668
1685
  return false;
1669
- const cmd = tokens[0]?.text ?? '';
1686
+ const cmdRaw = tokens[0]?.text ?? '';
1687
+ const cmd = platform === 'win32' ? cmdRaw.toLowerCase() : cmdRaw;
1670
1688
  const flag = tokens[1]?.text ?? '';
1671
1689
  if (cmd === 'node')
1672
1690
  return flag === '--version' || flag === '-v';
1673
- if (cmd === 'python3')
1691
+ if (cmd === 'python3' || cmd === 'python' || cmd === 'py') {
1674
1692
  return flag === '--version' || flag === '-V';
1693
+ }
1675
1694
  return false;
1676
1695
  }
1677
- function validateReadonlyShellCommand(command) {
1678
- return validateReadonlyShellCommandInternal(command.trimStart(), 0);
1696
+ function validateReadonlyShellCommand(command, platform = process.platform) {
1697
+ return validateReadonlyShellCommandInternal(command.trimStart(), platform, 0);
1679
1698
  }
1680
- function validateReadonlyShellCommandInternal(command, depth) {
1699
+ function validateReadonlyShellCommandForTests(command, platform) {
1700
+ return validateReadonlyShellCommand(command, platform);
1701
+ }
1702
+ function validateReadonlyShellCommandInternal(command, platform, depth) {
1681
1703
  if (depth > 8) {
1682
1704
  return {
1683
1705
  ok: false,
@@ -1688,8 +1710,8 @@ function validateReadonlyShellCommandInternal(command, depth) {
1688
1710
  };
1689
1711
  }
1690
1712
  const trimmed = command.trimStart();
1691
- if (trimmed.startsWith('cd ')) {
1692
- const parsed = parseCdChain(trimmed);
1713
+ if (startsWithReadonlyShellCommand(trimmed, 'cd', platform)) {
1714
+ const parsed = parseCdChain(trimmed, platform);
1693
1715
  if (!parsed) {
1694
1716
  return { ok: false, failure: { reason: 'INVALID_CD_SYNTAX', rejectedSegment: trimmed } };
1695
1717
  }
@@ -1703,9 +1725,9 @@ function validateReadonlyShellCommandInternal(command, depth) {
1703
1725
  },
1704
1726
  };
1705
1727
  }
1706
- return validateReadonlyShellCommandInternal(parsed.rest, depth + 1);
1728
+ return validateReadonlyShellCommandInternal(parsed.rest, platform, depth + 1);
1707
1729
  }
1708
- const chainParsed = splitTopLevelReadonlyShellChain(trimmed);
1730
+ const chainParsed = splitTopLevelReadonlyShellChain(trimmed, platform);
1709
1731
  if (!chainParsed.ok) {
1710
1732
  return {
1711
1733
  ok: false,
@@ -1717,23 +1739,27 @@ function validateReadonlyShellCommandInternal(command, depth) {
1717
1739
  }
1718
1740
  if (chainParsed.segments.length > 1) {
1719
1741
  for (const segment of chainParsed.segments) {
1720
- const segmentValidation = validateReadonlyShellCommandInternal(segment, depth + 1);
1742
+ const segmentValidation = validateReadonlyShellCommandInternal(segment, platform, depth + 1);
1721
1743
  if (!segmentValidation.ok) {
1722
1744
  return segmentValidation;
1723
1745
  }
1724
1746
  }
1725
1747
  return { ok: true };
1726
1748
  }
1727
- if (trimmed.startsWith('git -C ')) {
1749
+ if (startsWithReadonlyShellCommand(trimmed, 'git', platform) && /^git\s+-C\s+/iu.test(trimmed)) {
1728
1750
  // Allow a narrow, read-only subset of `git -C <dir> <subcommand> ...` as long as <dir> looks
1729
1751
  // like a safe *relative* path (no absolute paths / parent traversal). This avoids accidentally
1730
1752
  // inspecting outside the rtws with `git -C /...`.
1731
1753
  const tokens = trimmed.split(/\s+/g);
1732
1754
  // Expected: git -C <dir> <subcommand> ...
1733
- if (tokens.length >= 4 && tokens[0] === 'git' && tokens[1] === '-C') {
1755
+ const gitCommand = tokens[0] ?? '';
1756
+ const gitFlag = tokens[1] ?? '';
1757
+ const isGitCommand = platform === 'win32' ? gitCommand.toLowerCase() === 'git' : gitCommand === 'git';
1758
+ if (tokens.length >= 4 && isGitCommand && gitFlag === '-C') {
1734
1759
  const dirRaw = tokens[2] ?? '';
1735
1760
  const dir = dirRaw.replace(/^["']|["']$/g, '');
1736
- const subcommand = tokens[3] ?? '';
1761
+ const subcommandRaw = tokens[3] ?? '';
1762
+ const subcommand = platform === 'win32' ? subcommandRaw.toLowerCase() : subcommandRaw;
1737
1763
  if (!isSafeRelativePath(dir)) {
1738
1764
  return { ok: false, failure: { reason: 'GIT_C_UNSAFE_PATH', rejectedSegment: trimmed } };
1739
1765
  }
@@ -1742,6 +1768,12 @@ function validateReadonlyShellCommandInternal(command, depth) {
1742
1768
  subcommand === 'diff' ||
1743
1769
  subcommand === 'log' ||
1744
1770
  subcommand === 'blame') {
1771
+ if (hasUnsafeReadonlyShellCommandOptions(trimmed, platform)) {
1772
+ return {
1773
+ ok: false,
1774
+ failure: { reason: 'UNSAFE_SHELL_SYNTAX', rejectedSegment: trimmed },
1775
+ };
1776
+ }
1745
1777
  return { ok: true };
1746
1778
  }
1747
1779
  return {
@@ -1751,17 +1783,93 @@ function validateReadonlyShellCommandInternal(command, depth) {
1751
1783
  }
1752
1784
  return { ok: false, failure: { reason: 'GIT_C_INVALID', rejectedSegment: trimmed } };
1753
1785
  }
1754
- if (isAllowedReadonlyShellVersionProbe(trimmed)) {
1786
+ if (isAllowedReadonlyShellVersionProbe(trimmed, platform)) {
1755
1787
  return { ok: true };
1756
1788
  }
1757
- for (const prefix of readonlyShellAllowedPrefixes) {
1758
- if (trimmed === prefix || trimmed.startsWith(`${prefix} `)) {
1789
+ for (const prefix of getReadonlyShellAllowedPrefixes(platform)) {
1790
+ if (matchesReadonlyShellPrefix(trimmed, prefix, platform)) {
1791
+ if (hasUnsafeReadonlyShellCommandOptions(trimmed, platform)) {
1792
+ return {
1793
+ ok: false,
1794
+ failure: { reason: 'UNSAFE_SHELL_SYNTAX', rejectedSegment: trimmed },
1795
+ };
1796
+ }
1759
1797
  return { ok: true };
1760
1798
  }
1761
1799
  }
1762
1800
  return { ok: false, failure: { reason: 'COMMAND_NOT_ALLOWLISTED', rejectedSegment: trimmed } };
1763
1801
  }
1764
- function splitTopLevelReadonlyShellChain(command) {
1802
+ function hasUnsafeReadonlyShellCommandOptions(command, platform) {
1803
+ const tokens = splitShellTokens(command, platform);
1804
+ const cmdRaw = tokens[0]?.text ?? '';
1805
+ const cmd = platform === 'win32' ? cmdRaw.toLowerCase() : cmdRaw;
1806
+ const args = tokens.slice(1).map((token) => token.text);
1807
+ if (cmd === 'awk') {
1808
+ return args.some((arg) => /\bsystem\s*\(/.test(arg) ||
1809
+ arg.includes('>') ||
1810
+ arg.includes('|') ||
1811
+ arg === '-i' ||
1812
+ arg.startsWith('-i') ||
1813
+ arg === '--include' ||
1814
+ arg.startsWith('--include=') ||
1815
+ arg === '-l' ||
1816
+ arg.startsWith('-l') ||
1817
+ arg === '--load' ||
1818
+ arg.startsWith('--load='));
1819
+ }
1820
+ if (cmd === 'sed') {
1821
+ return args.some((arg) => arg === '-i' ||
1822
+ arg.startsWith('-i') ||
1823
+ arg === '--in-place' ||
1824
+ arg.startsWith('--in-place=') ||
1825
+ looksLikeSedWriteScript(arg));
1826
+ }
1827
+ if (cmd === 'find') {
1828
+ return args.some((arg) => [
1829
+ '-delete',
1830
+ '-exec',
1831
+ '-execdir',
1832
+ '-ok',
1833
+ '-okdir',
1834
+ '-fprint',
1835
+ '-fprint0',
1836
+ '-fprintf',
1837
+ '-fls',
1838
+ ].includes(arg));
1839
+ }
1840
+ if (cmd === 'git') {
1841
+ return args.some((arg) => arg === '-c' ||
1842
+ arg === '--config-env' ||
1843
+ arg.startsWith('--config-env=') ||
1844
+ arg === '--exec-path' ||
1845
+ arg.startsWith('--exec-path=') ||
1846
+ arg === '--paginate' ||
1847
+ arg === '--output' ||
1848
+ arg.startsWith('--output=') ||
1849
+ arg === '--ext-diff' ||
1850
+ arg === '--external-diff' ||
1851
+ arg === '--textconv');
1852
+ }
1853
+ if (cmd === 'sort') {
1854
+ return args.some((arg) => arg === '-o' || arg.startsWith('-o') || arg.startsWith('--output'));
1855
+ }
1856
+ if (cmd === 'rg') {
1857
+ return args.some((arg) => arg === '--pre' ||
1858
+ arg.startsWith('--pre=') ||
1859
+ arg === '--hostname-bin' ||
1860
+ arg.startsWith('--hostname-bin='));
1861
+ }
1862
+ if (cmd === 'date') {
1863
+ return args.some((arg) => arg === '-s' || arg.startsWith('-s') || arg === '--set' || arg.startsWith('--set='));
1864
+ }
1865
+ return false;
1866
+ }
1867
+ function looksLikeSedWriteScript(script) {
1868
+ const commandParts = script.split(/[;{}]/g);
1869
+ return commandParts.some((part) => /^\s*(?:[0-9,$!+~\/\\-]+|\/(?:\\.|[^/])*\/)?w(?:\s|$)/.test(part) ||
1870
+ /s(.)(?:\\.|(?!\1).)*\1(?:\\.|(?!\1).)*\1[0-9gIpMew]*w/.test(part));
1871
+ }
1872
+ function splitTopLevelReadonlyShellChain(command, platform = process.platform) {
1765
1873
  const segments = [];
1766
1874
  let quote = null;
1767
1875
  let escape = false;
@@ -1775,6 +1883,7 @@ function splitTopLevelReadonlyShellChain(command) {
1775
1883
  };
1776
1884
  for (let i = 0; i < command.length; i++) {
1777
1885
  const ch = command[i] ?? '';
1886
+ const next = command[i + 1] ?? '';
1778
1887
  if (escape) {
1779
1888
  escape = false;
1780
1889
  continue;
@@ -1783,20 +1892,38 @@ function splitTopLevelReadonlyShellChain(command) {
1783
1892
  if (ch === quote) {
1784
1893
  quote = null;
1785
1894
  }
1786
- else if (ch === '\\' && quote === '"') {
1895
+ else if (quote === '"' &&
1896
+ (ch === '`' || (ch === '$' && next === '(') || (platform === 'win32' && ch === '%'))) {
1897
+ return {
1898
+ ok: false,
1899
+ reason: 'UNSAFE_SHELL_SYNTAX',
1900
+ rejectedSegment: command.slice(segmentStart).trim() || command.trim(),
1901
+ };
1902
+ }
1903
+ else if (ch === '\\' && quote === '"' && platform !== 'win32') {
1787
1904
  escape = true;
1788
1905
  }
1789
1906
  continue;
1790
1907
  }
1791
- if (ch === '\\') {
1908
+ if (ch === '\\' && platform !== 'win32') {
1792
1909
  escape = true;
1793
1910
  continue;
1794
1911
  }
1795
- if (ch === "'" || ch === '"') {
1912
+ if (ch === '"' || (ch === "'" && platform !== 'win32')) {
1796
1913
  quote = ch;
1797
1914
  continue;
1798
1915
  }
1799
- const next = command[i + 1] ?? '';
1916
+ if (ch === '`' ||
1917
+ (ch === '$' && next === '(') ||
1918
+ ch === '<' ||
1919
+ ch === '>' ||
1920
+ (platform === 'win32' && ch === '%')) {
1921
+ return {
1922
+ ok: false,
1923
+ reason: 'UNSAFE_SHELL_SYNTAX',
1924
+ rejectedSegment: command.slice(segmentStart).trim() || command.trim(),
1925
+ };
1926
+ }
1800
1927
  if ((ch === '&' && next === '&') || (ch === '|' && next === '|')) {
1801
1928
  if (!pushSegment(i)) {
1802
1929
  return {
@@ -1861,15 +1988,28 @@ function splitTopLevelReadonlyShellChain(command) {
1861
1988
  function isSafeRelativePath(dir) {
1862
1989
  const hasParentTraversal = /(^|[\\/])\.\.([\\/]|$)/.test(dir);
1863
1990
  const isAbsoluteOrHome = dir.startsWith('/') ||
1991
+ dir.startsWith('\\') ||
1864
1992
  dir.startsWith('~') ||
1865
- /^[A-Za-z]:[\\/]/.test(dir) ||
1993
+ /^[A-Za-z]:/.test(dir) ||
1866
1994
  dir.startsWith('\\\\');
1867
1995
  return !isAbsoluteOrHome && !hasParentTraversal && dir.trim() !== '';
1868
1996
  }
1869
- function parseCdChain(command) {
1997
+ function startsWithReadonlyShellCommand(command, executable, platform) {
1998
+ return matchesReadonlyShellPrefix(command, executable, platform);
1999
+ }
2000
+ function matchesReadonlyShellPrefix(command, prefix, platform) {
2001
+ const normalizedCommand = platform === 'win32' ? command.toLowerCase() : command;
2002
+ const normalizedPrefix = platform === 'win32' ? prefix.toLowerCase() : prefix;
2003
+ return (normalizedCommand === normalizedPrefix ||
2004
+ new RegExp(`^${escapeRegexLiteral(normalizedPrefix)}\\s`, 'u').test(normalizedCommand));
2005
+ }
2006
+ function escapeRegexLiteral(value) {
2007
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2008
+ }
2009
+ function parseCdChain(command, platform) {
1870
2010
  // Supports: cd <dir> && <rest> or cd <dir> || <rest>
1871
- // `<dir>` may be single/double-quoted; `<rest>` must be non-empty.
1872
- if (!command.startsWith('cd '))
2011
+ // `<dir>` may be quoted; Windows cmd.exe only treats double quotes as quotes.
2012
+ if (!/^cd\s+/iu.test(command))
1873
2013
  return null;
1874
2014
  let i = 2;
1875
2015
  while (i < command.length && /\s/.test(command[i] ?? ''))
@@ -1878,7 +2018,7 @@ function parseCdChain(command) {
1878
2018
  return null;
1879
2019
  const start = i;
1880
2020
  const first = command[i] ?? '';
1881
- if (first === '"' || first === "'") {
2021
+ if (first === '"' || (first === "'" && platform !== 'win32')) {
1882
2022
  const quote = first;
1883
2023
  i++;
1884
2024
  while (i < command.length && command[i] !== quote)
@@ -1907,7 +2047,7 @@ function parseCdChain(command) {
1907
2047
  return null;
1908
2048
  return { dir, rest };
1909
2049
  }
1910
- function splitShellTokens(command) {
2050
+ function splitShellTokens(command, platform = process.platform) {
1911
2051
  const out = [];
1912
2052
  let buf = '';
1913
2053
  let quote = null;
@@ -1929,7 +2069,7 @@ function splitShellTokens(command) {
1929
2069
  buf += ch;
1930
2070
  continue;
1931
2071
  }
1932
- if (ch === "'" || ch === '"') {
2072
+ if (ch === '"' || (ch === "'" && platform !== 'win32')) {
1933
2073
  quote = ch;
1934
2074
  tokenQuoted = true;
1935
2075
  continue;
@@ -1943,9 +2083,12 @@ function splitShellTokens(command) {
1943
2083
  push();
1944
2084
  return out;
1945
2085
  }
2086
+ function normalizeReadonlyShellToken(token, platform) {
2087
+ return platform === 'win32' ? token.toLowerCase() : token;
2088
+ }
1946
2089
  function firstReadonlyShellToken(segment) {
1947
2090
  const tokens = splitShellTokens(segment.trim());
1948
- return tokens[0]?.text ?? '';
2091
+ return normalizeReadonlyShellToken(tokens[0]?.text ?? '', process.platform);
1949
2092
  }
1950
2093
  function getReadonlyShellSuggestionEn(failure) {
1951
2094
  const token = firstReadonlyShellToken(failure.rejectedSegment);
@@ -1957,6 +2100,9 @@ function getReadonlyShellSuggestionEn(failure) {
1957
2100
  failure.reason === 'CHAIN_PARSE_TRAILING_ESCAPE') {
1958
2101
  return 'Fix shell quoting first, then run an allowlisted segment (for example: `ls` or `rg <pattern>`).';
1959
2102
  }
2103
+ if (failure.reason === 'UNSAFE_SHELL_SYNTAX') {
2104
+ return 'Do not use redirects, command substitution, or Windows environment expansion in `readonly_shell`; run a plain allowlisted inspection command.';
2105
+ }
1960
2106
  if (failure.reason === 'INVALID_CD_SYNTAX' || failure.reason === 'UNSAFE_RELATIVE_PATH') {
1961
2107
  return 'Use `cd <relative-path> && <allowed command...>`.';
1962
2108
  }
@@ -1970,8 +2116,8 @@ function getReadonlyShellSuggestionEn(failure) {
1970
2116
  }
1971
2117
  if (token === 'node')
1972
2118
  return 'Only version probes are allowed: `node --version || true`.';
1973
- if (token === 'python3' || token === 'python') {
1974
- return 'Only version probes are allowed: `python3 --version || true`.';
2119
+ if (token === 'python3' || token === 'python' || token === 'py') {
2120
+ return 'Only version probes are allowed: `python3 --version || true` (or `python --version` / `py --version` on Windows).';
1975
2121
  }
1976
2122
  if (token === 'false')
1977
2123
  return 'Use `true` as fallback (for example: `ls || true`).';
@@ -1992,6 +2138,9 @@ function getReadonlyShellSuggestionZh(failure) {
1992
2138
  failure.reason === 'CHAIN_PARSE_TRAILING_ESCAPE') {
1993
2139
  return '先修正引号/转义,再执行白名单子命令(例如:`ls` 或 `rg <pattern>`)。';
1994
2140
  }
2141
+ if (failure.reason === 'UNSAFE_SHELL_SYNTAX') {
2142
+ return '请勿在 `readonly_shell` 中使用重定向或命令替换;请直接运行白名单检查命令。';
2143
+ }
1995
2144
  if (failure.reason === 'INVALID_CD_SYNTAX' || failure.reason === 'UNSAFE_RELATIVE_PATH') {
1996
2145
  return '请使用 `cd <相对路径> && <允许命令...>`。';
1997
2146
  }
@@ -2005,8 +2154,8 @@ function getReadonlyShellSuggestionZh(failure) {
2005
2154
  }
2006
2155
  if (token === 'node')
2007
2156
  return '仅允许版本探针:`node --version || true`。';
2008
- if (token === 'python3' || token === 'python') {
2009
- return '仅允许版本探针:`python3 --version || true`。';
2157
+ if (token === 'python3' || token === 'python' || token === 'py') {
2158
+ return '仅允许版本探针:`python3 --version || true`(Windows 上也可用 `python --version` / `py --version`)。';
2010
2159
  }
2011
2160
  if (token === 'false')
2012
2161
  return '兜底请用 `true`(例如:`ls || true`)。';
@@ -2020,8 +2169,9 @@ function getReadonlyShellSuggestionZh(failure) {
2020
2169
  function normalizeRelFromRtwsRoot(relPath) {
2021
2170
  return relPath.replace(/\\/g, '/').replace(/^\/+/, '');
2022
2171
  }
2023
- function detectForbiddenRtwsRootHiddenDir(relFromRoot) {
2024
- const normalized = normalizeRelFromRtwsRoot(relFromRoot);
2172
+ function detectForbiddenRtwsRootHiddenDir(relFromRoot, platform = process.platform) {
2173
+ const rawNormalized = normalizeRelFromRtwsRoot(relFromRoot);
2174
+ const normalized = platform === 'win32' ? rawNormalized.toLowerCase() : rawNormalized;
2025
2175
  if (normalized === '.minds' || normalized.startsWith('.minds/'))
2026
2176
  return '.minds';
2027
2177
  if (normalized === '.dialogs' || normalized.startsWith('.dialogs/'))
@@ -2032,26 +2182,42 @@ function resolveRelFromRtwsRoot(workspaceRootAbs, baseDirRel, token) {
2032
2182
  const abs = path_1.default.resolve(workspaceRootAbs, baseDirRel, token);
2033
2183
  return path_1.default.relative(workspaceRootAbs, abs);
2034
2184
  }
2035
- function detectReadonlyShellForbiddenHiddenDirAccess(workspaceRootAbs, command) {
2185
+ function detectReadonlyShellForbiddenHiddenDirAccess(workspaceRootAbs, command, platform = process.platform) {
2036
2186
  // Deny access to rtws-root `.minds/**` and `.dialogs/**` only.
2037
2187
  // Nested rtws (e.g. `ux-rtws/.minds/**`, `ux-rtws/.dialogs/**`) remains allowed.
2038
2188
  let baseDirRel = '.';
2039
2189
  let rest = command.trimStart();
2040
2190
  // Evaluate chained `cd ... && ...` prefixes and track base dir.
2041
- while (rest.startsWith('cd ')) {
2042
- const parsed = parseCdChain(rest);
2191
+ while (startsWithReadonlyShellCommand(rest, 'cd', platform)) {
2192
+ const parsed = parseCdChain(rest, platform);
2043
2193
  if (!parsed)
2044
2194
  break;
2045
2195
  const dir = parsed.dir.replace(/^["']|["']$/g, '');
2046
2196
  const relFromRoot = resolveRelFromRtwsRoot(workspaceRootAbs, baseDirRel, dir);
2047
- const forbidden = detectForbiddenRtwsRootHiddenDir(relFromRoot);
2197
+ const forbidden = detectForbiddenRtwsRootHiddenDir(relFromRoot, platform);
2048
2198
  if (forbidden)
2049
2199
  return forbidden;
2050
2200
  baseDirRel = path_1.default.join(baseDirRel, dir);
2051
2201
  rest = parsed.rest.trimStart();
2052
2202
  }
2053
- const tokens = splitShellTokens(rest);
2054
- const cmd = tokens[0]?.text ?? '';
2203
+ const chainParsed = splitTopLevelReadonlyShellChain(rest, platform);
2204
+ if (chainParsed.ok && chainParsed.segments.length > 1) {
2205
+ for (const segment of chainParsed.segments) {
2206
+ const forbidden = detectReadonlyShellForbiddenHiddenDirAccessInSegment(workspaceRootAbs, baseDirRel, segment, platform);
2207
+ if (forbidden)
2208
+ return forbidden;
2209
+ }
2210
+ return null;
2211
+ }
2212
+ return detectReadonlyShellForbiddenHiddenDirAccessInSegment(workspaceRootAbs, baseDirRel, rest, platform);
2213
+ }
2214
+ function detectReadonlyShellForbiddenHiddenDirAccessForTests(workspaceRootAbs, command, platform) {
2215
+ return detectReadonlyShellForbiddenHiddenDirAccess(workspaceRootAbs, command, platform);
2216
+ }
2217
+ function detectReadonlyShellForbiddenHiddenDirAccessInSegment(workspaceRootAbs, baseDirRel, segment, platform) {
2218
+ const tokens = splitShellTokens(segment, platform);
2219
+ const cmdRaw = tokens[0]?.text ?? '';
2220
+ const cmd = platform === 'win32' ? cmdRaw.toLowerCase() : cmdRaw;
2055
2221
  if (!cmd)
2056
2222
  return null;
2057
2223
  const tokenText = (i) => {
@@ -2060,101 +2226,332 @@ function detectReadonlyShellForbiddenHiddenDirAccess(workspaceRootAbs, command)
2060
2226
  return null;
2061
2227
  return v.text;
2062
2228
  };
2063
- // Handle the special allowed form: `git -C <dir> <subcommand> ...`
2064
- if (cmd === 'git' && tokenText(1) === '-C') {
2065
- const dirToken = tokenText(2);
2066
- if (dirToken) {
2067
- const relFromRoot = resolveRelFromRtwsRoot(workspaceRootAbs, baseDirRel, dirToken);
2068
- const forbidden = detectForbiddenRtwsRootHiddenDir(relFromRoot);
2069
- if (forbidden)
2070
- return forbidden;
2071
- }
2072
- return null;
2073
- }
2074
2229
  const checkPathToken = (raw) => {
2075
2230
  const trimmed = raw.trim();
2076
2231
  if (trimmed === '' || trimmed === '-' || trimmed === '--')
2077
2232
  return null;
2078
2233
  const relFromRoot = resolveRelFromRtwsRoot(workspaceRootAbs, baseDirRel, trimmed);
2079
- return detectForbiddenRtwsRootHiddenDir(relFromRoot);
2234
+ return detectForbiddenRtwsRootHiddenDir(relFromRoot, platform);
2080
2235
  };
2236
+ const checkGitPathspecToken = (raw, gitBaseDirRel) => {
2237
+ let pathspec = raw.trim();
2238
+ if (pathspec === '' || pathspec === '-' || pathspec === '--')
2239
+ return null;
2240
+ if (pathspec.startsWith(':(')) {
2241
+ const magicEnd = pathspec.indexOf(')');
2242
+ if (magicEnd >= 0)
2243
+ pathspec = pathspec.slice(magicEnd + 1);
2244
+ }
2245
+ if (pathspec.startsWith(':/'))
2246
+ pathspec = pathspec.slice(2);
2247
+ if (pathspec.startsWith(':'))
2248
+ pathspec = pathspec.slice(1);
2249
+ const relFromRoot = resolveRelFromRtwsRoot(workspaceRootAbs, gitBaseDirRel, pathspec);
2250
+ return detectForbiddenRtwsRootHiddenDir(relFromRoot, platform);
2251
+ };
2252
+ if (cmd === 'git') {
2253
+ let gitBaseDirRel = baseDirRel;
2254
+ let argsStart = 2;
2255
+ if (tokenText(1) === '-C') {
2256
+ const dirToken = tokenText(2);
2257
+ if (dirToken) {
2258
+ const relFromRoot = resolveRelFromRtwsRoot(workspaceRootAbs, baseDirRel, dirToken);
2259
+ const forbidden = detectForbiddenRtwsRootHiddenDir(relFromRoot, platform);
2260
+ if (forbidden)
2261
+ return forbidden;
2262
+ gitBaseDirRel = path_1.default.join(baseDirRel, dirToken);
2263
+ }
2264
+ argsStart = 4;
2265
+ }
2266
+ for (let index = argsStart; index < tokens.length; index++) {
2267
+ const token = tokenText(index);
2268
+ if (!token || token === '--' || token.startsWith('-'))
2269
+ continue;
2270
+ const forbidden = checkGitPathspecToken(token, gitBaseDirRel);
2271
+ if (forbidden)
2272
+ return forbidden;
2273
+ }
2274
+ return null;
2275
+ }
2081
2276
  // Command-specific parsing to avoid false-positives where `.minds` is just a pattern/filter.
2082
2277
  if (cmd === 'rg') {
2083
- // `rg [OPTIONS] PATTERN [PATH ...]`
2084
- let i = 1;
2085
- while (i < tokens.length) {
2086
- const t = tokenText(i);
2087
- if (!t)
2278
+ // `rg [OPTIONS] PATTERN [PATH ...]`; `rg --files [PATH ...]` has no pattern.
2279
+ let index = 1;
2280
+ let filesMode = false;
2281
+ let patternConsumed = false;
2282
+ while (index < tokens.length) {
2283
+ const token = tokenText(index);
2284
+ if (!token)
2088
2285
  break;
2089
- if (t === '--') {
2090
- i += 1;
2286
+ if (token === '--') {
2287
+ index += 1;
2091
2288
  break;
2092
2289
  }
2093
- if (t.startsWith('-')) {
2094
- i += 1;
2290
+ if (token === '--files') {
2291
+ filesMode = true;
2292
+ index += 1;
2095
2293
  continue;
2096
2294
  }
2097
- // First non-flag token is PATTERN (do not treat as a path).
2098
- i += 1;
2099
- break;
2295
+ if (token === '-g' || token === '--glob' || token === '--iglob' || token === '-f') {
2296
+ const optionValue = tokenText(index + 1);
2297
+ if (optionValue) {
2298
+ const forbidden = checkPathToken(optionValue);
2299
+ if (forbidden)
2300
+ return forbidden;
2301
+ }
2302
+ index += 2;
2303
+ continue;
2304
+ }
2305
+ if ((token.startsWith('-g') || token.startsWith('-f')) && token.length > 2) {
2306
+ const optionValue = token.slice(2);
2307
+ const forbidden = checkPathToken(optionValue);
2308
+ if (forbidden)
2309
+ return forbidden;
2310
+ index += 1;
2311
+ continue;
2312
+ }
2313
+ if (token.startsWith('--glob=') ||
2314
+ token.startsWith('--iglob=') ||
2315
+ token.startsWith('--file=')) {
2316
+ const optionValue = token.slice(token.indexOf('=') + 1);
2317
+ const forbidden = checkPathToken(optionValue);
2318
+ if (forbidden)
2319
+ return forbidden;
2320
+ index += 1;
2321
+ continue;
2322
+ }
2323
+ if (token === '-e' || token === '--regexp') {
2324
+ index += 2;
2325
+ continue;
2326
+ }
2327
+ if (token.startsWith('--regexp=')) {
2328
+ index += 1;
2329
+ continue;
2330
+ }
2331
+ if (token.startsWith('-')) {
2332
+ index += 1;
2333
+ continue;
2334
+ }
2335
+ if (!filesMode && !patternConsumed) {
2336
+ patternConsumed = true;
2337
+ index += 1;
2338
+ continue;
2339
+ }
2340
+ const forbidden = checkPathToken(token);
2341
+ if (forbidden)
2342
+ return forbidden;
2343
+ index += 1;
2100
2344
  }
2101
- for (; i < tokens.length; i++) {
2102
- const t = tokenText(i);
2103
- if (!t)
2345
+ for (; index < tokens.length; index++) {
2346
+ const token = tokenText(index);
2347
+ if (!token)
2104
2348
  continue;
2105
- const forbidden = checkPathToken(t);
2349
+ const forbidden = checkPathToken(token);
2106
2350
  if (forbidden)
2107
2351
  return forbidden;
2108
2352
  }
2109
2353
  return null;
2110
2354
  }
2111
2355
  if (cmd === 'jq') {
2112
- // `jq [OPTIONS] FILTER [FILE ...]`
2113
- let i = 1;
2114
- while (i < tokens.length) {
2115
- const t = tokenText(i);
2116
- if (!t)
2356
+ // `jq [OPTIONS] FILTER [FILE ...]`; some options read files before FILTER.
2357
+ let index = 1;
2358
+ while (index < tokens.length) {
2359
+ const token = tokenText(index);
2360
+ if (!token)
2117
2361
  break;
2118
- if (t === '--') {
2119
- i += 1;
2362
+ if (token === '--') {
2363
+ index += 1;
2120
2364
  break;
2121
2365
  }
2122
- if (t.startsWith('-')) {
2123
- i += 1;
2366
+ if (token === '-f' || token === '--from-file') {
2367
+ const optionValue = tokenText(index + 1);
2368
+ if (optionValue) {
2369
+ const forbidden = checkPathToken(optionValue);
2370
+ if (forbidden)
2371
+ return forbidden;
2372
+ }
2373
+ index += 2;
2374
+ continue;
2375
+ }
2376
+ if (token.startsWith('--from-file=')) {
2377
+ const optionValue = token.slice(token.indexOf('=') + 1);
2378
+ const forbidden = checkPathToken(optionValue);
2379
+ if (forbidden)
2380
+ return forbidden;
2381
+ index += 1;
2382
+ continue;
2383
+ }
2384
+ if (token === '--slurpfile' || token === '--rawfile' || token === '--argfile') {
2385
+ const fileValue = tokenText(index + 2);
2386
+ if (fileValue) {
2387
+ const forbidden = checkPathToken(fileValue);
2388
+ if (forbidden)
2389
+ return forbidden;
2390
+ }
2391
+ index += 3;
2392
+ continue;
2393
+ }
2394
+ if (token === '--arg' || token === '--argjson') {
2395
+ index += 3;
2396
+ continue;
2397
+ }
2398
+ if (token.startsWith('-')) {
2399
+ index += 1;
2124
2400
  continue;
2125
2401
  }
2126
2402
  // First non-flag token is FILTER (do not treat as a file path).
2127
- i += 1;
2403
+ index += 1;
2128
2404
  break;
2129
2405
  }
2130
- for (; i < tokens.length; i++) {
2131
- const t = tokenText(i);
2132
- if (!t)
2406
+ for (; index < tokens.length; index++) {
2407
+ const token = tokenText(index);
2408
+ if (!token)
2133
2409
  continue;
2134
- const forbidden = checkPathToken(t);
2410
+ const forbidden = checkPathToken(token);
2135
2411
  if (forbidden)
2136
2412
  return forbidden;
2137
2413
  }
2138
2414
  return null;
2139
2415
  }
2416
+ if (cmd === 'where') {
2417
+ // Windows `where /r <dir> <pattern>` recursively searches a directory.
2418
+ for (let index = 1; index < tokens.length; index++) {
2419
+ const token = tokenText(index);
2420
+ if (!token)
2421
+ continue;
2422
+ if (token.toLowerCase() === '/r' || token.toLowerCase() === '-r') {
2423
+ const optionValue = tokenText(index + 1);
2424
+ if (optionValue) {
2425
+ const forbidden = checkPathToken(optionValue);
2426
+ if (forbidden)
2427
+ return forbidden;
2428
+ }
2429
+ index += 1;
2430
+ }
2431
+ }
2432
+ return null;
2433
+ }
2434
+ if (cmd === 'findstr') {
2435
+ // `findstr [OPTIONS] STRINGS [FILE ...]` — treat the first non-option as the pattern.
2436
+ let index = 1;
2437
+ while (index < tokens.length) {
2438
+ const token = tokenText(index);
2439
+ if (!token)
2440
+ break;
2441
+ const findstrOptionPath = /^[/\-](?:f|g|d):(.+)$/iu.exec(token);
2442
+ if (findstrOptionPath) {
2443
+ const optionValue = findstrOptionPath[1] ?? '';
2444
+ const optionPaths = /^[/\-]d:/iu.test(token) ? optionValue.split(';') : [optionValue];
2445
+ for (const optionPath of optionPaths) {
2446
+ const forbidden = checkPathToken(optionPath);
2447
+ if (forbidden)
2448
+ return forbidden;
2449
+ }
2450
+ index += 1;
2451
+ continue;
2452
+ }
2453
+ if (token.startsWith('/') || token.startsWith('-')) {
2454
+ index += 1;
2455
+ continue;
2456
+ }
2457
+ index += 1;
2458
+ break;
2459
+ }
2460
+ for (; index < tokens.length; index++) {
2461
+ const token = tokenText(index);
2462
+ if (!token)
2463
+ continue;
2464
+ const forbidden = checkPathToken(token);
2465
+ if (forbidden)
2466
+ return forbidden;
2467
+ }
2468
+ return null;
2469
+ }
2470
+ if (cmd === 'awk') {
2471
+ let index = 1;
2472
+ let programConsumed = false;
2473
+ while (index < tokens.length) {
2474
+ const token = tokenText(index);
2475
+ if (!token)
2476
+ break;
2477
+ if (token === '-f' || token === '--file') {
2478
+ const optionValue = tokenText(index + 1);
2479
+ if (optionValue) {
2480
+ const forbidden = checkPathToken(optionValue);
2481
+ if (forbidden)
2482
+ return forbidden;
2483
+ }
2484
+ programConsumed = true;
2485
+ index += 2;
2486
+ continue;
2487
+ }
2488
+ if (token.startsWith('-f') && token.length > 2) {
2489
+ const optionValue = token.slice(2);
2490
+ const forbidden = checkPathToken(optionValue);
2491
+ if (forbidden)
2492
+ return forbidden;
2493
+ programConsumed = true;
2494
+ index += 1;
2495
+ continue;
2496
+ }
2497
+ if (token.startsWith('--file=')) {
2498
+ const optionValue = token.slice(token.indexOf('=') + 1);
2499
+ const forbidden = checkPathToken(optionValue);
2500
+ if (forbidden)
2501
+ return forbidden;
2502
+ programConsumed = true;
2503
+ index += 1;
2504
+ continue;
2505
+ }
2506
+ if (token === '-v' || token === '-F') {
2507
+ index += 2;
2508
+ continue;
2509
+ }
2510
+ if (token.startsWith('-F') && token.length > 2) {
2511
+ index += 1;
2512
+ continue;
2513
+ }
2514
+ if (token.startsWith('-')) {
2515
+ index += 1;
2516
+ continue;
2517
+ }
2518
+ if (!programConsumed) {
2519
+ programConsumed = true;
2520
+ index += 1;
2521
+ continue;
2522
+ }
2523
+ const forbidden = checkPathToken(token);
2524
+ if (forbidden)
2525
+ return forbidden;
2526
+ index += 1;
2527
+ }
2528
+ return null;
2529
+ }
2140
2530
  if (cmd === 'find') {
2141
- // `find [path ...] [expression]` — only treat the initial paths as path roots.
2142
- for (let i = 1; i < tokens.length; i++) {
2143
- const t = tokenText(i);
2144
- if (!t)
2531
+ // `find [global-options] [path ...] [expression]` — treat only initial roots as paths.
2532
+ for (let index = 1; index < tokens.length; index++) {
2533
+ const token = tokenText(index);
2534
+ if (!token)
2145
2535
  continue;
2146
- if (t.startsWith('-'))
2536
+ if (token === '-H' || token === '-L' || token === '-P' || /^-O[0-9]$/.test(token)) {
2537
+ continue;
2538
+ }
2539
+ if (token === '-D') {
2540
+ index += 1;
2541
+ continue;
2542
+ }
2543
+ if (token.startsWith('-'))
2147
2544
  break;
2148
- if (t === '!' || t === '(' || t === ')')
2545
+ if (token === '!' || token === '(' || token === ')')
2149
2546
  break;
2150
- const forbidden = checkPathToken(t);
2547
+ const forbidden = checkPathToken(token);
2151
2548
  if (forbidden)
2152
2549
  return forbidden;
2153
2550
  }
2154
2551
  return null;
2155
2552
  }
2156
2553
  // Default conservative: treat non-flag args as potential paths for common file-inspection commands.
2157
- // This intentionally does NOT block `echo/printf/awk/...` where args are data, not paths.
2554
+ // This intentionally does NOT block `echo/printf/...` where args are data, not paths.
2158
2555
  const pathLikeCommands = new Set([
2159
2556
  'cat',
2160
2557
  'ls',
@@ -2167,8 +2564,18 @@ function detectReadonlyShellForbiddenHiddenDirAccess(workspaceRootAbs, command)
2167
2564
  'diff',
2168
2565
  'realpath',
2169
2566
  'readlink',
2567
+ 'cut',
2568
+ 'sort',
2569
+ 'uniq',
2570
+ 'shasum',
2571
+ 'sha256sum',
2572
+ 'md5sum',
2170
2573
  'tree',
2171
2574
  'sed',
2575
+ 'dir',
2576
+ 'type',
2577
+ 'more',
2578
+ 'fc',
2172
2579
  ]);
2173
2580
  if (pathLikeCommands.has(cmd)) {
2174
2581
  for (let i = 1; i < tokens.length; i++) {
@@ -2187,10 +2594,10 @@ function detectReadonlyShellForbiddenHiddenDirAccess(workspaceRootAbs, command)
2187
2594
  exports.readonlyShellTool = {
2188
2595
  type: 'func',
2189
2596
  name: 'readonly_shell',
2190
- description: 'Execute a read-only shell command from a small allowlist. Only exact version probes are allowed for node/python (no scripts such as `node -e` or `python3 -c`). Command chains via |/&&/|| are validated segment-by-segment. Commands outside the allowlist are rejected.',
2597
+ description: 'Execute a read-only shell command from a small allowlist. On Windows this runs through cmd.exe, so use allowlisted cmd/PATH commands such as `rg`, `git`, `dir`, `type`, or `where`. Only exact version probes are allowed for node/python (no scripts such as `node -e` or `python3 -c`). Command chains via |/&&/|| are validated segment-by-segment. Commands outside the allowlist are rejected.',
2191
2598
  descriptionI18n: {
2192
- en: 'Execute a read-only shell command from a small allowlist. Only exact version probes are allowed for node/python (no scripts such as `node -e` or `python3 -c`). Command chains via |/&&/|| are validated segment-by-segment. You are explicitly authorized to call this tool yourself (no delegation). Commands outside the allowlist are rejected.',
2193
- zh: '执行只读 shell 命令,仅允许少量白名单命令前缀。对 node/python 仅允许版本探针(不允许 `node -e` / `python3 -c` 这类脚本)。通过 |/&&/|| 串联时会按子命令逐段校验。你已被明确授权自行调用该工具(无需委派)。不在允许列表内的命令会被拒绝。',
2599
+ en: 'Execute a read-only shell command from a small allowlist. On Windows this runs through cmd.exe, so use allowlisted cmd/PATH commands such as `rg`, `git`, `dir`, `type`, or `where`. Only exact version probes are allowed for node/python (no scripts such as `node -e` or `python3 -c`). Command chains via |/&&/|| are validated segment-by-segment. You are explicitly authorized to call this tool yourself (no delegation). Commands outside the allowlist are rejected.',
2600
+ zh: '执行只读 shell 命令,仅允许少量白名单命令前缀。Windows 上通过 cmd.exe 执行,请使用白名单内且 cmd/PATH 可用的命令,例如 `rg`、`git`、`dir`、`type` 或 `where`。对 node/python 仅允许版本探针(不允许 `node -e` / `python3 -c` 这类脚本)。通过 |/&&/|| 串联时会按子命令逐段校验。你已被明确授权自行调用该工具(无需委派)。不在允许列表内的命令会被拒绝。',
2194
2601
  },
2195
2602
  parameters: readonlyShellSchema,
2196
2603
  async call(dlg, caller, args) {
@@ -2206,15 +2613,15 @@ exports.readonlyShellTool = {
2206
2613
  }
2207
2614
  const validation = validateReadonlyShellCommand(command);
2208
2615
  if (!validation.ok) {
2209
- const allowedList = readonlyShellAllowedPrefixes.join(', ');
2616
+ const allowedList = getReadonlyShellAllowedPrefixes().join(', ');
2210
2617
  const rejectedSegment = validation.failure.rejectedSegment.trim();
2211
2618
  const rejectedSegmentOrCommand = rejectedSegment === '' ? command : rejectedSegment;
2212
2619
  const suggestion = language === 'zh'
2213
2620
  ? getReadonlyShellSuggestionZh(validation.failure)
2214
2621
  : getReadonlyShellSuggestionEn(validation.failure);
2215
2622
  return (0, tool_1.toolFailure)(prependShellWarning(language === 'zh'
2216
- ? `❌ readonly_shell 仅允许以下命令前缀:${allowedList}\n另外允许(仅版本探针):node --version|-v、python3 --version|-V\n脚本执行(如 node -e / python3 -c)一律拒绝。\n另外允许:git -C <相对路径> <show|status|diff|log|blame> ...\n另外允许:cd <相对路径> && <允许命令...>(或 ||)\n说明:通过 |/&&/|| 串联时会按子命令逐段校验。\n被拒子命令段:${rejectedSegmentOrCommand}\n允许的等价写法:${suggestion}\n收到:${command}`
2217
- : `❌ readonly_shell only allows these command prefixes: ${allowedList}\nAlso allowed (exact version probes only): node --version|-v, python3 --version|-V\nNode/python scripts (for example: node -e, python3 -c) are rejected.\nAlso allowed: git -C <relative-path> <show|status|diff|log|blame> ...\nAlso allowed: cd <relative-path> && <allowed command...> (or ||)\nNote: chains via |/&&/|| are validated segment-by-segment.\nRejected segment: ${rejectedSegmentOrCommand}\nAllowed equivalent: ${suggestion}\nGot: ${command}`, warning));
2623
+ ? `❌ readonly_shell 仅允许以下命令前缀:${allowedList}\n另外允许(仅版本探针):node --version|-v、python3/python/py --version|-V\n脚本执行(如 node -e / python3 -c)一律拒绝。\n另外允许:git -C <相对路径> <show|status|diff|log|blame> ...\n另外允许:cd <相对路径> && <允许命令...>(或 ||)\nWindows 上通过 cmd.exe 执行;请使用该 shell/PATH 中可用的白名单命令。\n说明:通过 |/&&/|| 串联时会按子命令逐段校验。\n被拒子命令段:${rejectedSegmentOrCommand}\n允许的等价写法:${suggestion}\n收到:${command}`
2624
+ : `❌ readonly_shell only allows these command prefixes: ${allowedList}\nAlso allowed (exact version probes only): node --version|-v, python3/python/py --version|-V\nNode/python scripts (for example: node -e, python3 -c) are rejected.\nAlso allowed: git -C <relative-path> <show|status|diff|log|blame> ...\nAlso allowed: cd <relative-path> && <allowed command...> (or ||)\nOn Windows this runs through cmd.exe; use allowlisted commands available in that shell/PATH.\nNote: chains via |/&&/|| are validated segment-by-segment.\nRejected segment: ${rejectedSegmentOrCommand}\nAllowed equivalent: ${suggestion}\nGot: ${command}`, warning));
2218
2625
  }
2219
2626
  const forbiddenHiddenDir = detectReadonlyShellForbiddenHiddenDirAccess(path_1.default.resolve(process.cwd()), command);
2220
2627
  if (forbiddenHiddenDir) {