0xray 4.0.41 → 4.1.0

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 (148) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.opencode/skills/orchestrator/SKILL.md +12 -0
  3. package/.plugin/plugin.json +1 -1
  4. package/.xray/features.json +5 -5
  5. package/.xray/features.schema.json +1 -1
  6. package/AGENTS-consumer.md +10 -6
  7. package/AGENTS.md +19 -8
  8. package/LICENSE +1 -1
  9. package/README.md +65 -19
  10. package/SKILLS.md +5 -1
  11. package/dist/AGENTS.md +19 -8
  12. package/dist/CHANGELOG.md +85 -0
  13. package/dist/LICENSE +1 -1
  14. package/dist/README.md +65 -19
  15. package/dist/SKILLS.md +5 -1
  16. package/dist/cli/commands/openclaw-install.js +1 -1
  17. package/dist/cli/commands/plugin-commands.d.ts +4 -0
  18. package/dist/cli/commands/plugin-commands.js +84 -41
  19. package/dist/cli/index.js +12 -0
  20. package/dist/core/config-loader.js +4 -4
  21. package/dist/core/features-config.d.ts +1 -1
  22. package/dist/core/features-config.js +3 -3
  23. package/dist/enforcement/index.d.ts +4 -0
  24. package/dist/enforcement/index.js +4 -0
  25. package/dist/enforcement/validators/code-quality-validators.js +6 -4
  26. package/dist/governance/llm-governance-provider.d.ts +7 -1
  27. package/dist/governance/llm-governance-provider.js +72 -14
  28. package/dist/governance/local-confer.d.ts +4 -1
  29. package/dist/governance/local-confer.js +22 -1
  30. package/dist/integrations/cursor/hooks/hooks.json +6 -3
  31. package/dist/integrations/cursor/hooks/pre-tool-use.js +100 -37
  32. package/dist/integrations/cursor/hooks/xray-cloud-hook.sh +9 -1
  33. package/dist/integrations/grok/grok-cli.js +10 -37
  34. package/dist/integrations/grok/hooks/grok-hook-utils.js +8 -1
  35. package/dist/integrations/grok/hooks/pre-tool-use.js +2 -58
  36. package/dist/integrations/hermes-agent/bridge.mjs +14 -1
  37. package/dist/integrations/hooks/confer-hook-runtime.mjs +23 -1
  38. package/dist/integrations/hooks/delegation-gate-runtime.mjs +3 -2
  39. package/dist/integrations/hooks/goggles-mcp.mjs +35 -17
  40. package/dist/integrations/hooks/goggles-pipeline.mjs +46 -1
  41. package/dist/integrations/hooks/goggles-planes.json +11 -7
  42. package/dist/integrations/hooks/pre-tool-speak.mjs +76 -0
  43. package/dist/integrations/openclaw/hooks/pre-tool-gate-runtime.mjs +21 -4
  44. package/dist/integrations/openclaw/plugin/xray-pre-tool/index.js +17 -2
  45. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  46. package/dist/mcps/architect-tools.server.d.ts +6 -0
  47. package/dist/mcps/architect-tools.server.js +19 -2
  48. package/dist/mcps/config/server-config-registry.js +8 -6
  49. package/dist/mcps/governance.server.d.ts +1 -0
  50. package/dist/mcps/governance.server.js +46 -0
  51. package/dist/mcps/knowledge-skills/code-review.server.js +7 -0
  52. package/dist/mcps/knowledge-skills/security-audit.server.js +5 -3
  53. package/dist/mcps/mcp-client.js +1 -1
  54. package/dist/mcps/orchestrator/handlers/complexity-handler.js +2 -2
  55. package/dist/mcps/researcher.server.js +12 -3
  56. package/dist/monitoring/{nudge-watchdog.d.ts → nudge-monitor.d.ts} +14 -5
  57. package/dist/monitoring/{nudge-watchdog.js → nudge-monitor.js} +42 -17
  58. package/dist/nucleus/autonomy-kernel.d.ts +2 -0
  59. package/dist/nucleus/autonomy-kernel.js +17 -13
  60. package/dist/nucleus/confer-host.d.ts +35 -0
  61. package/dist/nucleus/confer-host.js +55 -0
  62. package/dist/nucleus/confer.d.ts +19 -3
  63. package/dist/nucleus/confer.js +111 -55
  64. package/dist/nucleus/lead-dev-plan-persistence.js +4 -2
  65. package/dist/nucleus/suit-temperament.d.ts +1 -1
  66. package/dist/nucleus/suit-temperament.js +2 -2
  67. package/dist/nucleus/synthesis-consult-receipt.d.ts +7 -1
  68. package/dist/nucleus/synthesis-consult-receipt.js +86 -35
  69. package/dist/plugin/xray-codex-injection.js +18 -0
  70. package/dist/processors/implementations/nudge-processor.d.ts +1 -1
  71. package/dist/processors/implementations/nudge-processor.js +4 -4
  72. package/dist/reporting/framework-reporting-system.js +2 -5
  73. package/dist/reporting/log-parser.d.ts +2 -0
  74. package/dist/reporting/log-parser.js +7 -6
  75. package/dist/skills/orchestrator/SKILL.md +12 -0
  76. package/docs-site/docs/plates/activity-log.md +1 -1
  77. package/docs-site/docs/plates/bifrost.md +48 -0
  78. package/docs-site/docs/plates/boot.md +2 -2
  79. package/docs-site/docs/plates/centrifuge.md +1 -1
  80. package/docs-site/docs/plates/cursor-compact.md +1 -1
  81. package/docs-site/docs/plates/glossary.md +7 -3
  82. package/docs-site/docs/plates/goggles.md +13 -2
  83. package/docs-site/docs/plates/governance.md +1 -1
  84. package/docs-site/docs/plates/grok-compact.md +1 -1
  85. package/docs-site/docs/plates/grokbot.md +3 -1
  86. package/docs-site/docs/plates/host-pack.md +1 -1
  87. package/docs-site/docs/plates/house.md +1 -1
  88. package/docs-site/docs/plates/index.md +4 -2
  89. package/docs-site/docs/plates/inference-files.md +1 -1
  90. package/docs-site/docs/plates/kept-line.md +1 -1
  91. package/docs-site/docs/plates/kits.md +1 -1
  92. package/docs-site/docs/plates/lens-gate.md +3 -1
  93. package/docs-site/docs/plates/lens-page.md +1 -1
  94. package/docs-site/docs/plates/memory-recall.md +1 -1
  95. package/docs-site/docs/plates/notes-page.md +2 -2
  96. package/docs-site/docs/plates/orchestration.md +1 -1
  97. package/docs-site/docs/plates/package-face.md +1 -1
  98. package/docs-site/docs/plates/payload-heat.md +1 -1
  99. package/docs-site/docs/plates/pickup-stamp.md +1 -1
  100. package/docs-site/docs/plates/pre-tool.md +1 -1
  101. package/docs-site/docs/plates/processor.md +1 -1
  102. package/docs-site/docs/plates/record-map.md +1 -1
  103. package/docs-site/docs/plates/reflection-page.md +1 -1
  104. package/docs-site/docs/plates/reporting.md +1 -1
  105. package/docs-site/docs/plates/review.md +1 -1
  106. package/docs-site/docs/plates/routing.md +1 -1
  107. package/docs-site/docs/plates/session-capture.md +1 -1
  108. package/docs-site/docs/plates/site-manual.md +1 -1
  109. package/docs-site/docs/plates/stamp-plate.md +1 -1
  110. package/docs-site/docs/plates/station-card.md +1 -1
  111. package/docs-site/docs/plates/station-heat.md +1 -1
  112. package/docs-site/docs/plates/suit-organs.md +1 -1
  113. package/docs-site/docs/plates/suit-settings.md +1 -1
  114. package/docs-site/docs/plates/suit-wear.md +1 -1
  115. package/docs-site/docs/plates/suit.md +3 -3
  116. package/docs-site/docs/plates/trail-state.md +1 -1
  117. package/docs-site/docs/plates/work-fresh.md +1 -1
  118. package/docs-site/docs/plates/write-home.md +1 -1
  119. package/llms.txt +16 -6
  120. package/package.json +4 -4
  121. package/scripts/foundry/README.md +2 -2
  122. package/scripts/foundry/release-gate.mjs +111 -15
  123. package/scripts/foundry/release.mjs +8 -0
  124. package/scripts/mjs/verify-confer-core.mjs +153 -29
  125. package/scripts/mjs/verify-pipeline-facets.mjs +1 -1
  126. package/scripts/mjs/verify-user-aside-core.mjs +3 -4
  127. package/scripts/node/bridge-mcp-wiring.cjs +236 -5
  128. package/scripts/node/confer-switch.cjs +66 -0
  129. package/scripts/node/install-bridges.cjs +44 -53
  130. package/scripts/node/pack-tmp-suit-proof.mjs +14 -6
  131. package/src/integrations/openclaw/plugin/xray-pre-tool/index.js +17 -2
  132. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  133. package/src/mcps/architect-tools.server.ts +26 -2
  134. package/src/mcps/config/__tests__/server-config-registry.test.ts +15 -6
  135. package/src/mcps/config/server-config-registry.ts +9 -6
  136. package/src/mcps/governance.server.ts +49 -0
  137. package/src/mcps/knowledge-skills/code-review.server.ts +8 -0
  138. package/src/mcps/knowledge-skills/security-audit.server.ts +11 -12
  139. package/src/mcps/mcp-client.ts +1 -1
  140. package/src/mcps/orchestrator/handlers/complexity-handler.ts +2 -1
  141. package/src/mcps/researcher.server.ts +19 -10
  142. package/src/skills/orchestrator/SKILL.md +12 -0
  143. package/vendor/@0xray/repertoire/README.md +1 -1
  144. package/vendor/@0xray/repertoire/scripts/run-live-synthesis-checkpoint.mjs +5 -51
  145. package/xray/agents_template.md +2 -2
  146. package/xray/features.json +5 -5
  147. package/xray/features.schema.json +1 -1
  148. package/docs-site/docs/plates/live-mesh.md +0 -41
@@ -869,7 +869,125 @@ function resolveOpenClawPluginDir(packageRoot) {
869
869
  ].find((p) => fs.existsSync(path.join(p, "index.js"))) || null;
870
870
  }
871
871
 
872
- function installOpenClawHostWear(packageRoot) {
872
+ function isCheckoutOpenClawPluginPath(pluginPath) {
873
+ const normalized = String(pluginPath || "").replace(/\\/g, "/");
874
+ return (
875
+ normalized.includes("/src/integrations/openclaw/plugin/xray-pre-tool") &&
876
+ !normalized.includes("/node_modules/")
877
+ );
878
+ }
879
+
880
+ function isInstalledOpenClawPluginPath(pluginPath) {
881
+ const normalized = String(pluginPath || "").replace(/\\/g, "/");
882
+ return (
883
+ normalized.includes("/node_modules/") &&
884
+ normalized.includes("/integrations/openclaw/plugin/xray-pre-tool")
885
+ );
886
+ }
887
+
888
+ /** Installed package first. A checkout src path drops once any installed plugin is present. */
889
+ function settleOpenClawPluginPaths(paths, wornPluginDir) {
890
+ const list = (Array.isArray(paths) ? paths : []).map((entry) => String(entry));
891
+ const wornIsInstalled = Boolean(wornPluginDir && isInstalledOpenClawPluginPath(wornPluginDir));
892
+ const dropCheckout =
893
+ wornIsInstalled || list.some((entry) => isInstalledOpenClawPluginPath(entry));
894
+ const next = [];
895
+ const seen = new Set();
896
+ const push = (entry) => {
897
+ if (!entry) return;
898
+ const resolved = path.resolve(String(entry));
899
+ if (dropCheckout && isCheckoutOpenClawPluginPath(resolved)) return;
900
+ if (seen.has(resolved)) return;
901
+ seen.add(resolved);
902
+ next.push(resolved);
903
+ };
904
+ if (wornIsInstalled) push(wornPluginDir);
905
+ for (const entry of list) push(entry);
906
+ return next;
907
+ }
908
+
909
+ function resolveWornOpenClawPlugin(targetDir, packageRoot) {
910
+ const candidates = [];
911
+ if (targetDir) {
912
+ candidates.push(
913
+ path.join(targetDir, "node_modules", "0xray", "dist", "integrations", "openclaw", "plugin", "xray-pre-tool"),
914
+ path.join(targetDir, "node_modules", "0xray", "src", "integrations", "openclaw", "plugin", "xray-pre-tool"),
915
+ );
916
+ }
917
+ const fromPackage = resolveOpenClawPluginDir(packageRoot);
918
+ if (fromPackage) candidates.push(fromPackage);
919
+ return candidates.find((dir) => fs.existsSync(path.join(dir, "index.js"))) || null;
920
+ }
921
+
922
+ function replaceJsonPathsArray(raw, before, next) {
923
+ const keyAt = raw.indexOf('"paths"');
924
+ if (keyAt < 0) return null;
925
+ const bracket = raw.indexOf("[", keyAt);
926
+ if (bracket < 0) return null;
927
+ let depth = 0;
928
+ let end = -1;
929
+ for (let i = bracket; i < raw.length; i += 1) {
930
+ const ch = raw[i];
931
+ if (ch === "[") depth += 1;
932
+ else if (ch === "]") {
933
+ depth -= 1;
934
+ if (depth === 0) {
935
+ end = i;
936
+ break;
937
+ }
938
+ }
939
+ }
940
+ if (end < 0) return null;
941
+ let parsed;
942
+ try {
943
+ parsed = JSON.parse(raw.slice(bracket, end + 1));
944
+ } catch {
945
+ return null;
946
+ }
947
+ if (JSON.stringify(parsed) !== JSON.stringify(before)) return null;
948
+ const lineStart = raw.lastIndexOf("\n", keyAt) + 1;
949
+ const indent = (raw.slice(lineStart, keyAt).match(/^\s*/) || [""])[0];
950
+ const itemIndent = `${indent} `;
951
+ const body = next
952
+ .map((entry) => `${itemIndent}${JSON.stringify(entry)}`)
953
+ .join(",\n");
954
+ const array = `[\n${body}\n${indent}]`;
955
+ return `${raw.slice(0, bracket)}${array}${raw.slice(end + 1)}`;
956
+ }
957
+
958
+ /** Rewrite plugins.load.paths in place. Leaves the rest of the config file alone. */
959
+ function pinOpenClawPluginLoad(configPath, wornPluginDir) {
960
+ if (!configPath || !fs.existsSync(configPath)) return null;
961
+ const raw = fs.readFileSync(configPath, "utf8");
962
+ const config = JSON.parse(raw);
963
+ if (!config.plugins || typeof config.plugins !== "object") return null;
964
+ if (!config.plugins.load || typeof config.plugins.load !== "object") config.plugins.load = {};
965
+ const before = Array.isArray(config.plugins.load.paths) ? config.plugins.load.paths : [];
966
+ const next = settleOpenClawPluginPaths(before, wornPluginDir);
967
+ if (JSON.stringify(before) === JSON.stringify(next)) return next;
968
+ const replaced = replaceJsonPathsArray(raw, before, next);
969
+ if (replaced == null) return null;
970
+ fs.writeFileSync(configPath, replaced);
971
+ return next;
972
+ }
973
+
974
+ function recordOpenClawProjectPlugin(targetDir, packageRoot) {
975
+ const pluginDir = resolveWornOpenClawPlugin(targetDir, packageRoot);
976
+ if (!pluginDir || !isInstalledOpenClawPluginPath(pluginDir)) return null;
977
+ const configPath = path.join(targetDir, ".xray", "config", "openclaw.json");
978
+ if (!fs.existsSync(configPath)) return null;
979
+ let config;
980
+ try {
981
+ config = JSON.parse(fs.readFileSync(configPath, "utf8"));
982
+ } catch {
983
+ return null;
984
+ }
985
+ config.pluginPath = path.resolve(pluginDir);
986
+ fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
987
+ return config.pluginPath;
988
+ }
989
+
990
+ function installOpenClawHostWear(packageRoot, targetDir) {
873
991
  if (millSuit.isIsolatedHome()) return null;
874
992
  const source = resolveOpenClawPreToolHookSource(packageRoot);
875
993
  if (!source) return null;
@@ -878,6 +996,11 @@ function installOpenClawHostWear(packageRoot) {
878
996
  fs.mkdirSync(hookDir, { recursive: true });
879
997
  const dest = path.join(hookDir, "xray-pre-tool.mjs");
880
998
  fs.copyFileSync(source, dest);
999
+ const worn = resolveWornOpenClawPlugin(targetDir, packageRoot);
1000
+ const hookRoot =
1001
+ worn && isInstalledOpenClawPluginPath(worn) && targetDir
1002
+ ? path.join(path.resolve(targetDir), "node_modules", "0xray")
1003
+ : packageRoot;
881
1004
  fs.writeFileSync(
882
1005
  path.join(hookDir, "xray-pre-tool.json"),
883
1006
  `${JSON.stringify(
@@ -887,17 +1010,16 @@ function installOpenClawHostWear(packageRoot) {
887
1010
  args: [dest],
888
1011
  stdin: "json",
889
1012
  blockExitCode: 2,
890
- env: { XRAY_AI_PATH: packageRoot },
1013
+ env: { XRAY_AI_PATH: hookRoot },
891
1014
  },
892
1015
  null,
893
1016
  2,
894
1017
  )}\n`,
895
1018
  );
896
1019
 
897
- const pluginSrc = resolveOpenClawPluginDir(packageRoot);
898
- if (pluginSrc) {
1020
+ if (worn && isInstalledOpenClawPluginPath(worn)) {
899
1021
  try {
900
- execFileSync("openclaw", ["plugins", "install", "-l", pluginSrc], {
1022
+ execFileSync("openclaw", ["plugins", "install", "-l", path.resolve(worn)], {
901
1023
  stdio: "pipe",
902
1024
  encoding: "utf8",
903
1025
  timeout: 60000,
@@ -915,6 +1037,13 @@ function installOpenClawHostWear(packageRoot) {
915
1037
  /* enable is best-effort */
916
1038
  }
917
1039
  }
1040
+ const configPath = resolveOpenClawConfigPath();
1041
+ if (fs.existsSync(configPath)) {
1042
+ pinOpenClawPluginLoad(
1043
+ configPath,
1044
+ worn && isInstalledOpenClawPluginPath(worn) ? worn : null,
1045
+ );
1046
+ }
918
1047
  return dest;
919
1048
  }
920
1049
 
@@ -979,6 +1108,102 @@ function wireOpenClawBridge(targetDir) {
979
1108
  return syncOpenClawMcpRegistry(targetDir);
980
1109
  }
981
1110
 
1111
+ function tomlString(value) {
1112
+ return JSON.stringify(String(value));
1113
+ }
1114
+
1115
+ function suitServerNames() {
1116
+ return new Set(XRAY_MCP_SERVERS.map((server) => server.name));
1117
+ }
1118
+
1119
+ function isSuitMcpHeader(line, names) {
1120
+ const match = /^\[([^\]]+)\]\s*$/.exec(line.trim());
1121
+ if (!match) return false;
1122
+ const parts = match[1].split(".");
1123
+ return parts[0] === "mcp_servers" && names.has(parts[1]);
1124
+ }
1125
+
1126
+ function stripSuitMcpBlocks(toml, names) {
1127
+ const kept = [];
1128
+ let skip = false;
1129
+ for (const line of String(toml || "").split(/\r?\n/)) {
1130
+ if (/^\s*\[/.test(line)) skip = isSuitMcpHeader(line, names);
1131
+ if (!skip) kept.push(line);
1132
+ }
1133
+ return kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
1134
+ }
1135
+
1136
+ function suitMcpBlock(server, launch) {
1137
+ const args = launch.args.map((arg) => ` ${tomlString(arg)},`).join("\n");
1138
+ const lines = [
1139
+ `[mcp_servers.${server.name}]`,
1140
+ `command = ${tomlString(launch.command)}`,
1141
+ "args = [",
1142
+ args,
1143
+ "]",
1144
+ "enabled = true",
1145
+ ];
1146
+ const env = launch.env && typeof launch.env === "object" ? launch.env : {};
1147
+ const envKeys = Object.keys(env);
1148
+ if (envKeys.length > 0) {
1149
+ lines.push("", `[mcp_servers.${server.name}.env]`);
1150
+ for (const key of envKeys) lines.push(`${key} = ${tomlString(env[key])}`);
1151
+ }
1152
+ return lines.join("\n");
1153
+ }
1154
+
1155
+ /**
1156
+ * Project `.grok/config.toml` replaces a same-named user server.
1157
+ * The launch is the worn package, not `npx -y 0xray`.
1158
+ */
1159
+ function writeProjectSuitMcp(targetDir) {
1160
+ const root = path.resolve(targetDir);
1161
+ const names = suitServerNames();
1162
+ const blocks = XRAY_MCP_SERVERS.map((server) =>
1163
+ suitMcpBlock(server, scopedMcpLaunch(root, server.mcpCmd, server.env)),
1164
+ );
1165
+ const grokDir = path.join(root, ".grok");
1166
+ fs.mkdirSync(grokDir, { recursive: true });
1167
+ const tomlPath = path.join(grokDir, "config.toml");
1168
+ const existing = fs.existsSync(tomlPath) ? fs.readFileSync(tomlPath, "utf8") : "";
1169
+ const kept = stripSuitMcpBlocks(existing, names);
1170
+ const body = [kept, blocks.join("\n\n")].filter((part) => part && part.trim()).join("\n\n");
1171
+ fs.writeFileSync(tomlPath, `${body.trim()}\n`);
1172
+ return tomlPath;
1173
+ }
1174
+
1175
+ function grokConfigHome(env) {
1176
+ const source = env || process.env;
1177
+ const home = source.HOME || source.USERPROFILE;
1178
+ return home ? path.resolve(home) : os.homedir();
1179
+ }
1180
+
1181
+ /** Grok 1.0.50 has no --trust flag. An untrusted folder skips project MCP. */
1182
+ function trustGrokFolder(targetDir, env) {
1183
+ const home = grokConfigHome(env);
1184
+ const grokDir = path.join(home, ".grok");
1185
+ fs.mkdirSync(grokDir, { recursive: true });
1186
+ const file = path.join(grokDir, "trusted_folders.toml");
1187
+ let text = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
1188
+ const folders = [path.resolve(targetDir)];
1189
+ try {
1190
+ const real = fs.realpathSync(folders[0]);
1191
+ if (real !== folders[0]) folders.push(real);
1192
+ } catch {
1193
+ /* the path is the one Grok will see */
1194
+ }
1195
+ const now = Math.floor(Date.now() / 1000);
1196
+ let added = 0;
1197
+ for (const folder of folders) {
1198
+ const header = `[folders.${tomlString(folder)}]`;
1199
+ if (text.includes(header)) continue;
1200
+ text = `${text.replace(/\s*$/, "")}\n${header}\ntrusted = true\ndecided_at = ${now}\n`;
1201
+ added += 1;
1202
+ }
1203
+ if (added > 0) fs.writeFileSync(file, text.endsWith("\n") ? text : `${text}\n`);
1204
+ return file;
1205
+ }
1206
+
982
1207
  module.exports = {
983
1208
  XRAY_MCP_SERVERS,
984
1209
  HERMES_CONFIG_PATH,
@@ -1025,5 +1250,11 @@ module.exports = {
1025
1250
  resolveOpenClawPreToolHookSource,
1026
1251
  resolveOpenClawPluginDir,
1027
1252
  installOpenClawHostWear,
1253
+ settleOpenClawPluginPaths,
1254
+ pinOpenClawPluginLoad,
1255
+ recordOpenClawProjectPlugin,
1256
+ resolveWornOpenClawPlugin,
1028
1257
  maybeWriteOpenClawCliBackend,
1258
+ writeProjectSuitMcp,
1259
+ trustGrokFolder,
1029
1260
  };
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Turn confer on or off in the worn .xray/features.json.
4
+ * A missing key stays off until this runs. Synthesis stays untouched.
5
+ */
6
+ "use strict";
7
+
8
+ const fs = require("fs");
9
+ const path = require("path");
10
+
11
+ function fail(message) {
12
+ process.stderr.write(`${message}\n`);
13
+ process.exit(1);
14
+ }
15
+
16
+ function orchestrationBlock(data) {
17
+ const current = data.multi_agent_orchestration;
18
+ if (current && typeof current === "object" && !Array.isArray(current)) return current;
19
+ const created = {};
20
+ data.multi_agent_orchestration = created;
21
+ return created;
22
+ }
23
+
24
+ function conferBlock(orch) {
25
+ const current = orch.confer;
26
+ if (current && typeof current === "object" && !Array.isArray(current)) return current;
27
+ const created = {};
28
+ orch.confer = created;
29
+ return created;
30
+ }
31
+
32
+ function main() {
33
+ const state = process.argv[2];
34
+ if (state !== "on" && state !== "off") fail("confer: say on or off");
35
+
36
+ const featuresPath = path.join(process.cwd(), ".xray", "features.json");
37
+ if (!fs.existsSync(featuresPath)) {
38
+ fail("confer: .xray/features.json is missing. Run npx 0xray wear first.");
39
+ }
40
+
41
+ let data;
42
+ try {
43
+ data = JSON.parse(fs.readFileSync(featuresPath, "utf8"));
44
+ } catch {
45
+ fail("confer: .xray/features.json is not valid JSON");
46
+ }
47
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
48
+ fail("confer: .xray/features.json is not valid JSON");
49
+ }
50
+
51
+ const orch = orchestrationBlock(data);
52
+ const confer = conferBlock(orch);
53
+ if (state === "on") {
54
+ confer.enabled = true;
55
+ if (confer.on_synthesis !== false) confer.on_synthesis = true;
56
+ } else {
57
+ confer.enabled = false;
58
+ confer.on_synthesis = false;
59
+ orch.confer_on_synthesis = false;
60
+ }
61
+
62
+ fs.writeFileSync(featuresPath, `${JSON.stringify(data, null, 2)}\n`);
63
+ process.stdout.write(`confer: ${state}\n`);
64
+ }
65
+
66
+ if (require.main === module) main();
@@ -7,7 +7,7 @@
7
7
 
8
8
  const fs = require("fs");
9
9
  const path = require("path");
10
- const { execFileSync, execSync } = require("child_process");
10
+ const { execFileSync } = require("child_process");
11
11
  const {
12
12
  wantsCostume,
13
13
  isIsolatedHome,
@@ -22,18 +22,19 @@ const {
22
22
  wireOpencodeBridge,
23
23
  wireOpenClawBridge,
24
24
  deployPortableProjectMcpJson,
25
- scopedMcpLaunch,
26
25
  mergeMcpMap,
27
26
  mergeNamedRecords,
28
27
  jsonDeepEqual,
29
28
  copyHermesFindProjectRootHelper,
30
29
  copyHermesHookRuntimes,
31
30
  installOpenClawHostWear,
31
+ recordOpenClawProjectPlugin,
32
32
  maybeWriteOpenClawCliBackend,
33
33
  isEphemeralInstallRoot,
34
34
  enableMemoryRoutingIfResolves,
35
35
  pinLauncherPackageRoot,
36
- XRAY_MCP_SERVERS,
36
+ writeProjectSuitMcp,
37
+ trustGrokFolder,
37
38
  } = require("./bridge-mcp-wiring.cjs");
38
39
 
39
40
  const wearIo = { writes: 0, skips: 0 };
@@ -484,42 +485,11 @@ function findGrokPluginSource(packageRoot) {
484
485
  return candidates.find((p) => fs.existsSync(p));
485
486
  }
486
487
 
487
- function registerGrokMcpServers(targetDir, log, pluginDirs) {
488
- try {
489
- execSync("which grok", { stdio: "ignore" });
490
- } catch {
491
- log("grok-bridge", "grok CLI not on PATH — plugin .mcp.json still configured", "info");
492
- return;
493
- }
494
-
495
- for (const s of XRAY_MCP_SERVERS) {
496
- try {
497
- const launch = scopedMcpLaunch(targetDir, s.mcpCmd, s.env);
498
- const argv = ["mcp", "add", s.name, "--command", launch.command, "--args", launch.args[0], ...launch.args.slice(1)];
499
- for (const [key, value] of Object.entries(launch.env)) {
500
- argv.push("--env", `${key}=${value}`);
501
- }
502
- execFileSync("grok", argv, { stdio: "pipe" });
503
- log("grok-bridge", `registered ${s.name} (mcp-launch)`, "info");
504
- } catch {
505
- // already registered or grok config conflict — non-blocking
506
- }
507
- }
508
-
509
- const dirs =
510
- Array.isArray(pluginDirs) && pluginDirs.length > 0
511
- ? pluginDirs
512
- : [path.join(targetDir, ".grok", "plugins", "0xray")];
513
- for (const pluginDir of dirs) {
514
- if (!fs.existsSync(pluginDir)) continue;
515
- try {
516
- execSync(`grok plugins trust "${pluginDir}"`, { stdio: "ignore" });
517
- log("grok-bridge", "plugin trusted", "info", { path: pluginDir });
518
- break;
519
- } catch {
520
- // best-effort
521
- }
522
- }
488
+ function registerGrokMcpServers(targetDir, log, env) {
489
+ const tomlPath = writeProjectSuitMcp(targetDir);
490
+ log("grok-bridge", "project mcp config written", "info", { path: tomlPath });
491
+ const trustPath = trustGrokFolder(targetDir, env || process.env);
492
+ log("grok-bridge", "folder trusted", "info", { path: trustPath });
523
493
  }
524
494
 
525
495
 
@@ -742,8 +712,8 @@ function installGrokBridge(targetDir, packageRoot, log, opts) {
742
712
  if (globalCopied > 0) {
743
713
  log("grok-bridge", `global skills synced (${globalCopied})`, "info", { path: grokGlobalSkills });
744
714
  }
745
- registerGrokMcpServers(targetDir, log, targets);
746
715
  }
716
+ registerGrokMcpServers(targetDir, log, env);
747
717
  }
748
718
 
749
719
  function installHermesBridge(targetDir, packageRoot, log) {
@@ -881,7 +851,7 @@ function installOpenclawBridge(targetDir, packageRoot, log) {
881
851
  }
882
852
 
883
853
  if (!ephemeralOpenclaw && !isolatedOpenclaw) {
884
- const hook = installOpenClawHostWear(packageRoot);
854
+ const hook = installOpenClawHostWear(packageRoot, targetDir);
885
855
  if (hook) log("openclaw-bridge", "PreToolUse hook installed", "info", { path: hook });
886
856
  if (maybeWriteOpenClawCliBackend()) {
887
857
  log("openclaw-bridge", "opencode-cli backend written", "info");
@@ -895,6 +865,11 @@ function installOpenclawBridge(targetDir, packageRoot, log) {
895
865
  "info",
896
866
  );
897
867
  }
868
+
869
+ const recorded = recordOpenClawProjectPlugin(targetDir, packageRoot);
870
+ if (recorded) {
871
+ log("openclaw-bridge", "project plugin path recorded", "info", { path: recorded });
872
+ }
898
873
  }
899
874
 
900
875
  const XRAY_CONFIG_FILES = ["codex.json", "features.json", "features.schema.json", "config.json"];
@@ -1041,16 +1016,18 @@ const CURSOR_HOOK_EVENTS = [
1041
1016
  ["beforeReadFile", "before-read-file.sh"],
1042
1017
  ];
1043
1018
 
1019
+ const CURSOR_GATE_EVENTS = new Set(["preToolUse", "beforeShellExecution", "beforeReadFile"]);
1020
+
1044
1021
  const CURSOR_WEAR_BACKUP = "hooks.json.xray-before";
1045
1022
 
1046
1023
  const CLOUD_SAFE_CURSOR_HOOKS = {
1047
1024
  version: 1,
1048
1025
  hooks: {
1049
- preToolUse: [{ command: ".cursor/hooks/pre-tool-use.sh" }],
1026
+ preToolUse: [{ command: ".cursor/hooks/pre-tool-use.sh", failClosed: true }],
1050
1027
  preCompact: [{ command: ".cursor/hooks/pre-compact.sh" }],
1051
1028
  afterFileEdit: [{ command: ".cursor/hooks/after-file-edit.sh" }],
1052
- beforeShellExecution: [{ command: ".cursor/hooks/before-shell-execution.sh" }],
1053
- beforeReadFile: [{ command: ".cursor/hooks/before-read-file.sh" }],
1029
+ beforeShellExecution: [{ command: ".cursor/hooks/before-shell-execution.sh", failClosed: true }],
1030
+ beforeReadFile: [{ command: ".cursor/hooks/before-read-file.sh", failClosed: true }],
1054
1031
  },
1055
1032
  };
1056
1033
 
@@ -1096,6 +1073,9 @@ function mergeCloudSafeCursorHooks(existing) {
1096
1073
  !Array.isArray(current) || !current.some((h) => isRelativeCursorHookCommand(h && h.command));
1097
1074
  if (!current || leftover || missingRelative) {
1098
1075
  next.hooks[name] = entries;
1076
+ } else if (entries[0].failClosed) {
1077
+ // Gate events fail closed: our relative entry gets failClosed even on an existing hooks.json (re-wear upgrades it).
1078
+ next.hooks[name] = current.map((h) => (h && isRelativeCursorHookCommand(h.command) ? { ...h, failClosed: true } : h));
1099
1079
  }
1100
1080
  }
1101
1081
  return next;
@@ -1304,7 +1284,10 @@ function installedHookCommand(fromDir, packageRoot, scriptName) {
1304
1284
  function installedCursorHookEntries(fromDir, packageRoot) {
1305
1285
  const hooks = {};
1306
1286
  for (const [event, script] of CURSOR_HOOK_EVENTS) {
1307
- hooks[event] = [{ command: installedHookCommand(fromDir, packageRoot, script) }];
1287
+ const entry = { command: installedHookCommand(fromDir, packageRoot, script) };
1288
+ // Gate events fail closed: Cursor denies when the hook crashes or prints no JSON.
1289
+ if (CURSOR_GATE_EVENTS.has(event)) entry.failClosed = true;
1290
+ hooks[event] = [entry];
1308
1291
  }
1309
1292
  return hooks;
1310
1293
  }
@@ -1635,8 +1618,10 @@ function ownedDeletionEdits(tokens, elements) {
1635
1618
  return edits;
1636
1619
  }
1637
1620
 
1638
- function renderEventEntry(command) {
1639
- return `{ "command": ${JSON.stringify(command)} }`;
1621
+ function renderEventEntry(command, failClosed) {
1622
+ return failClosed
1623
+ ? `{ "command": ${JSON.stringify(command)}, "failClosed": true }`
1624
+ : `{ "command": ${JSON.stringify(command)} }`;
1640
1625
  }
1641
1626
 
1642
1627
  function editJsoncHooks(text, xrayByEvent, targetDir) {
@@ -1652,7 +1637,7 @@ function editJsoncHooks(text, xrayByEvent, targetDir) {
1652
1637
  const hooksPair = root.pairs.find((pair) => pair.key === "hooks");
1653
1638
  if (!hooksPair) {
1654
1639
  const inner = Object.entries(xrayByEvent)
1655
- .map(([event, entries]) => `"${event}": [${renderEventEntry(entries[0].command)}]`)
1640
+ .map(([event, entries]) => `"${event}": [${renderEventEntry(entries[0].command, entries[0].failClosed)}]`)
1656
1641
  .join(", ");
1657
1642
  const needsComma = root.pairs.length > 0;
1658
1643
  edits.push({
@@ -1667,9 +1652,10 @@ function editJsoncHooks(text, xrayByEvent, targetDir) {
1667
1652
  const missing = [];
1668
1653
  for (const [event, entries] of Object.entries(xrayByEvent)) {
1669
1654
  const command = entries[0].command;
1655
+ const failClosed = entries[0].failClosed === true;
1670
1656
  const pair = hooksObj.pairs.find((item) => item.key === event);
1671
1657
  if (!pair) {
1672
- missing.push([event, command]);
1658
+ missing.push([event, command, failClosed]);
1673
1659
  continue;
1674
1660
  }
1675
1661
  if (tokens[pair.valIdx].type !== "[") throw new Error(`${event} is not an array`);
@@ -1691,7 +1677,7 @@ function editJsoncHooks(text, xrayByEvent, targetDir) {
1691
1677
  edits.push({
1692
1678
  start: tokens[pair.valEnd].start,
1693
1679
  end: tokens[pair.valEnd].start,
1694
- insert: `${elements.length > 0 ? ", " : ""}${renderEventEntry(command)}`,
1680
+ insert: `${elements.length > 0 ? ", " : ""}${renderEventEntry(command, failClosed)}`,
1695
1681
  });
1696
1682
  continue;
1697
1683
  }
@@ -1703,13 +1689,17 @@ function editJsoncHooks(text, xrayByEvent, targetDir) {
1703
1689
  insert: JSON.stringify(command),
1704
1690
  });
1705
1691
  }
1692
+ if (failClosed && !objectPairs(tokens, first.el.startIdx).pairs.some((p) => p.key === "failClosed")) {
1693
+ // Re-wear upgrades an older entry: gate events fail closed.
1694
+ edits.push({ start: first.cmd.token.end, end: first.cmd.token.end, insert: ', "failClosed": true' });
1695
+ }
1706
1696
  if (owned.length > 1) {
1707
1697
  edits.push(...ownedDeletionEdits(tokens, owned.slice(1).map((item) => item.el)));
1708
1698
  }
1709
1699
  }
1710
1700
  if (missing.length > 0) {
1711
1701
  const rendered = missing
1712
- .map(([event, command]) => `"${event}": [${renderEventEntry(command)}]`)
1702
+ .map(([event, command, failClosed]) => `"${event}": [${renderEventEntry(command, failClosed)}]`)
1713
1703
  .join(", ");
1714
1704
  edits.push({
1715
1705
  start: tokens[hooksObj.closeIdx].start,
@@ -1803,20 +1793,21 @@ function mergeInstalledCursorHooks(existing, xrayByEvent) {
1803
1793
  continue;
1804
1794
  }
1805
1795
  const command = shipped[0].command;
1796
+ const extra = shipped[0].failClosed ? { failClosed: true } : {};
1806
1797
  let replaced = false;
1807
1798
  const next = [];
1808
1799
  for (const entry of current) {
1809
1800
  const cmd = entry && typeof entry === "object" ? entry.command : "";
1810
1801
  if (isXrayHookCommand(cmd)) {
1811
1802
  if (!replaced) {
1812
- next.push({ ...entry, command });
1803
+ next.push({ ...entry, command, ...extra });
1813
1804
  replaced = true;
1814
1805
  }
1815
1806
  continue;
1816
1807
  }
1817
1808
  next.push(entry);
1818
1809
  }
1819
- if (!replaced) next.push({ command });
1810
+ if (!replaced) next.push({ command, ...extra });
1820
1811
  hooks[name] = next;
1821
1812
  }
1822
1813
  const doc = {};
@@ -39,17 +39,25 @@ export function foundryCliPath(millRoot) {
39
39
  return path.join(millRoot, "cli.js");
40
40
  }
41
41
 
42
- export function runFoundryCli(millRoot, consumerRoot, args) {
42
+ /** Clean-room foundry env. The machine home is empty so a plugin on the real machine cannot fail the seat inspect. */
43
+ export function proofFoundryEnv(consumerRoot, baseEnv = process.env) {
43
44
  const isolatedHome = path.join(consumerRoot, ".proof-home");
45
+ const machineHome = path.join(consumerRoot, ".proof-machine");
44
46
  fs.mkdirSync(isolatedHome, { recursive: true });
47
+ fs.mkdirSync(machineHome, { recursive: true });
48
+ return {
49
+ ...baseEnv,
50
+ FOUNDRY_ROOT: consumerRoot,
51
+ HOME: isolatedHome,
52
+ FOUNDRY_MACHINE_HOME: machineHome,
53
+ };
54
+ }
55
+
56
+ export function runFoundryCli(millRoot, consumerRoot, args) {
45
57
  return spawnSync(process.execPath, [foundryCliPath(millRoot), ...args], {
46
58
  cwd: consumerRoot,
47
59
  encoding: "utf8",
48
- env: {
49
- ...process.env,
50
- FOUNDRY_ROOT: consumerRoot,
51
- HOME: isolatedHome,
52
- },
60
+ env: proofFoundryEnv(consumerRoot),
53
61
  });
54
62
  }
55
63
 
@@ -2,7 +2,7 @@ import { createRequire } from 'node:module';
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import { dirname, join } from 'node:path';
5
- import { fileURLToPath } from 'node:url';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
6
6
  import { definePluginEntry } from 'openclaw/plugin-sdk/plugin-entry';
7
7
 
8
8
  const require = createRequire(import.meta.url);
@@ -39,7 +39,7 @@ export default definePluginEntry({
39
39
  register(api) {
40
40
  api.on(
41
41
  'before_tool_call',
42
- (event, ctx) => {
42
+ async (event, ctx) => {
43
43
  try {
44
44
  const xrayRoot = resolveXrayRoot();
45
45
  if (!xrayRoot) {
@@ -61,6 +61,21 @@ export default definePluginEntry({
61
61
  if (!outcome.allow) {
62
62
  return { block: true, blockReason: outcome.reason || '0xRay constitution' };
63
63
  }
64
+ const spoken = `${event.toolName} ${JSON.stringify(event.params || {})}`.slice(0, 500);
65
+ const speakPath = [
66
+ join(xrayRoot, 'dist/integrations/hooks/pre-tool-speak.mjs'),
67
+ join(xrayRoot, 'src/integrations/hooks/pre-tool-speak.mjs'),
68
+ ].find((candidate) => existsSync(candidate));
69
+ if (!speakPath) return undefined;
70
+ const speak = await import(pathToFileURL(speakPath).href);
71
+ const lens = speak.lensDecision(projectRoot, event.toolName, spoken);
72
+ if (lens) {
73
+ return { block: true, blockReason: lens.reason };
74
+ }
75
+ const route = speak.repertoireRoute(spoken);
76
+ if (route?.hookSpecificOutput) {
77
+ return { block: false, hookSpecificOutput: route.hookSpecificOutput };
78
+ }
64
79
  return undefined;
65
80
  } catch (err) {
66
81
  return {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xray-pre-tool",
3
- "version": "4.0.41",
3
+ "version": "4.1.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "0xRay constitution + temperament PreToolUse for OpenClaw",