0xray 4.0.42 → 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.
- package/.grok-plugin/plugin.json +1 -1
- package/.plugin/plugin.json +1 -1
- package/.xray/features.json +5 -5
- package/AGENTS-consumer.md +8 -6
- package/AGENTS.md +17 -8
- package/LICENSE +1 -1
- package/README.md +15 -9
- package/SKILLS.md +3 -1
- package/dist/AGENTS.md +17 -8
- package/dist/CHANGELOG.md +83 -11
- package/dist/LICENSE +1 -1
- package/dist/README.md +15 -9
- package/dist/SKILLS.md +3 -1
- package/dist/cli/commands/plugin-commands.d.ts +4 -0
- package/dist/cli/commands/plugin-commands.js +84 -41
- package/dist/core/config-loader.js +4 -4
- package/dist/core/features-config.d.ts +1 -1
- package/dist/core/features-config.js +3 -3
- package/dist/integrations/cursor/hooks/hooks.json +6 -3
- package/dist/integrations/cursor/hooks/pre-tool-use.js +100 -37
- package/dist/integrations/cursor/hooks/xray-cloud-hook.sh +9 -1
- package/dist/integrations/grok/hooks/pre-tool-use.js +2 -58
- package/dist/integrations/hermes-agent/bridge.mjs +14 -1
- package/dist/integrations/hooks/delegation-gate-runtime.mjs +3 -2
- package/dist/integrations/hooks/goggles-mcp.mjs +35 -17
- package/dist/integrations/hooks/goggles-pipeline.mjs +46 -1
- package/dist/integrations/hooks/goggles-planes.json +7 -6
- package/dist/integrations/hooks/pre-tool-speak.mjs +76 -0
- package/dist/integrations/openclaw/hooks/pre-tool-gate-runtime.mjs +21 -4
- package/dist/integrations/openclaw/plugin/xray-pre-tool/index.js +17 -2
- package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/dist/monitoring/{nudge-watchdog.d.ts → nudge-monitor.d.ts} +14 -5
- package/dist/monitoring/{nudge-watchdog.js → nudge-monitor.js} +42 -17
- package/dist/plugin/xray-codex-injection.js +18 -0
- package/dist/processors/implementations/nudge-processor.d.ts +1 -1
- package/dist/processors/implementations/nudge-processor.js +4 -4
- package/docs-site/docs/plates/activity-log.md +1 -1
- package/docs-site/docs/plates/bifrost.md +48 -0
- package/docs-site/docs/plates/boot.md +2 -2
- package/docs-site/docs/plates/centrifuge.md +1 -1
- package/docs-site/docs/plates/cursor-compact.md +1 -1
- package/docs-site/docs/plates/glossary.md +7 -3
- package/docs-site/docs/plates/goggles.md +13 -2
- package/docs-site/docs/plates/governance.md +1 -1
- package/docs-site/docs/plates/grok-compact.md +1 -1
- package/docs-site/docs/plates/grokbot.md +3 -1
- package/docs-site/docs/plates/host-pack.md +1 -1
- package/docs-site/docs/plates/house.md +1 -1
- package/docs-site/docs/plates/index.md +4 -2
- package/docs-site/docs/plates/inference-files.md +1 -1
- package/docs-site/docs/plates/kept-line.md +1 -1
- package/docs-site/docs/plates/kits.md +1 -1
- package/docs-site/docs/plates/lens-gate.md +3 -1
- package/docs-site/docs/plates/lens-page.md +1 -1
- package/docs-site/docs/plates/memory-recall.md +1 -1
- package/docs-site/docs/plates/notes-page.md +2 -2
- package/docs-site/docs/plates/orchestration.md +1 -1
- package/docs-site/docs/plates/package-face.md +1 -1
- package/docs-site/docs/plates/payload-heat.md +1 -1
- package/docs-site/docs/plates/pickup-stamp.md +1 -1
- package/docs-site/docs/plates/pre-tool.md +1 -1
- package/docs-site/docs/plates/processor.md +1 -1
- package/docs-site/docs/plates/record-map.md +1 -1
- package/docs-site/docs/plates/reflection-page.md +1 -1
- package/docs-site/docs/plates/reporting.md +1 -1
- package/docs-site/docs/plates/review.md +1 -1
- package/docs-site/docs/plates/routing.md +1 -1
- package/docs-site/docs/plates/session-capture.md +1 -1
- package/docs-site/docs/plates/site-manual.md +1 -1
- package/docs-site/docs/plates/stamp-plate.md +1 -1
- package/docs-site/docs/plates/station-card.md +1 -1
- package/docs-site/docs/plates/station-heat.md +1 -1
- package/docs-site/docs/plates/suit-organs.md +1 -1
- package/docs-site/docs/plates/suit-settings.md +1 -1
- package/docs-site/docs/plates/suit-wear.md +1 -1
- package/docs-site/docs/plates/suit.md +3 -3
- package/docs-site/docs/plates/trail-state.md +1 -1
- package/docs-site/docs/plates/work-fresh.md +1 -1
- package/docs-site/docs/plates/write-home.md +1 -1
- package/llms.txt +14 -6
- package/package.json +3 -3
- package/scripts/foundry/README.md +2 -2
- package/scripts/foundry/release-gate.mjs +111 -15
- package/scripts/foundry/release.mjs +8 -0
- package/scripts/mjs/verify-pipeline-facets.mjs +1 -1
- package/scripts/node/install-bridges.cjs +28 -12
- package/scripts/node/pack-tmp-suit-proof.mjs +14 -6
- package/src/integrations/openclaw/plugin/xray-pre-tool/index.js +17 -2
- package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/vendor/@0xray/repertoire/README.md +1 -1
- package/xray/agents_template.md +2 -2
- package/xray/features.json +5 -5
- package/docs-site/docs/plates/live-mesh.md +0 -41
|
@@ -152,24 +152,25 @@ export async function pluginStatusCommand(pluginName) {
|
|
|
152
152
|
console.log(` Type: ${m.type}`);
|
|
153
153
|
console.log(` Description: ${m.description}`);
|
|
154
154
|
console.log(` License: ${m.license || "N/A"}`);
|
|
155
|
-
if (m.capabilities) {
|
|
155
|
+
if (Array.isArray(m.capabilities)) {
|
|
156
156
|
console.log("\n Capabilities:");
|
|
157
157
|
for (const cap of m.capabilities) {
|
|
158
|
-
console.log(` - ${cap}`);
|
|
158
|
+
console.log(` - ${String(cap)}`);
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
|
-
if (m.tools) {
|
|
161
|
+
if (Array.isArray(m.tools)) {
|
|
162
162
|
console.log("\n Tools:");
|
|
163
163
|
for (const tool of m.tools) {
|
|
164
|
-
|
|
164
|
+
const row = tool;
|
|
165
|
+
console.log(` - ${String(row.name)}: ${String(row.description)}`);
|
|
165
166
|
}
|
|
166
167
|
}
|
|
167
|
-
if (m.runtime) {
|
|
168
|
+
if (m.runtime && typeof m.runtime === "object" && !Array.isArray(m.runtime)) {
|
|
168
169
|
const runtime = m.runtime;
|
|
169
170
|
console.log("\n Runtime:");
|
|
170
|
-
console.log(` Command: ${runtime.command}`);
|
|
171
|
-
if (runtime.args) {
|
|
172
|
-
console.log(` Args: ${runtime.args.join(" ")}`);
|
|
171
|
+
console.log(` Command: ${String(runtime.command)}`);
|
|
172
|
+
if (Array.isArray(runtime.args)) {
|
|
173
|
+
console.log(` Args: ${runtime.args.map((arg) => String(arg)).join(" ")}`);
|
|
173
174
|
}
|
|
174
175
|
}
|
|
175
176
|
console.log("\n" + "═".repeat(50) + "\n");
|
|
@@ -195,51 +196,93 @@ function validateManifest(manifest) {
|
|
|
195
196
|
}
|
|
196
197
|
}
|
|
197
198
|
}
|
|
198
|
-
function
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
]
|
|
205
|
-
|
|
199
|
+
function parseScalar(raw) {
|
|
200
|
+
const value = raw.trim();
|
|
201
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
202
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
203
|
+
return value.slice(1, -1);
|
|
204
|
+
}
|
|
205
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
206
|
+
const inner = value.slice(1, -1).trim();
|
|
207
|
+
if (!inner)
|
|
208
|
+
return [];
|
|
209
|
+
return inner.split(",").map((part) => parseScalar(part));
|
|
210
|
+
}
|
|
211
|
+
if (value === "true")
|
|
212
|
+
return true;
|
|
213
|
+
if (value === "false")
|
|
214
|
+
return false;
|
|
215
|
+
if (/^-?\d+$/.test(value))
|
|
216
|
+
return Number(value);
|
|
217
|
+
return value;
|
|
218
|
+
}
|
|
219
|
+
/** Subset of YAML used by plugin.yaml: maps, scalar lists, and lists of maps. */
|
|
220
|
+
export function parseYaml(content) {
|
|
221
|
+
const root = {};
|
|
222
|
+
const stack = [{ indent: -1, container: root }];
|
|
223
|
+
for (const line of content.split("\n")) {
|
|
206
224
|
const trimmed = line.trim();
|
|
207
225
|
if (!trimmed || trimmed.startsWith("#"))
|
|
208
226
|
continue;
|
|
209
227
|
const indent = line.search(/\S/);
|
|
210
|
-
|
|
211
|
-
if (isListItem) {
|
|
212
|
-
const value = trimmed.slice(2).trim();
|
|
228
|
+
while (stack.length > 1) {
|
|
213
229
|
const top = stack[stack.length - 1];
|
|
214
|
-
if (top
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (top && currentKey) {
|
|
218
|
-
top.obj[currentKey].push(value);
|
|
219
|
-
}
|
|
220
|
-
continue;
|
|
230
|
+
if (!top || indent > top.indent)
|
|
231
|
+
break;
|
|
232
|
+
stack.pop();
|
|
221
233
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
234
|
+
if (trimmed.startsWith("- ")) {
|
|
235
|
+
let owner = stack[stack.length - 1];
|
|
236
|
+
if (owner &&
|
|
237
|
+
!Array.isArray(owner.container) &&
|
|
238
|
+
Object.keys(owner.container).length === 0 &&
|
|
239
|
+
stack.length > 1) {
|
|
240
|
+
const holder = stack[stack.length - 2];
|
|
241
|
+
const emptyIndent = owner.indent;
|
|
228
242
|
stack.pop();
|
|
243
|
+
if (holder && !Array.isArray(holder.container) && holder.key) {
|
|
244
|
+
const list = [];
|
|
245
|
+
holder.container[holder.key] = list;
|
|
246
|
+
stack.push({ indent: emptyIndent, container: list });
|
|
247
|
+
}
|
|
248
|
+
owner = stack[stack.length - 1];
|
|
229
249
|
}
|
|
230
|
-
|
|
231
|
-
if (!current)
|
|
250
|
+
if (!owner || !Array.isArray(owner.container))
|
|
232
251
|
continue;
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
252
|
+
const rest = trimmed.slice(2).trim();
|
|
253
|
+
const colon = rest.indexOf(":");
|
|
254
|
+
const quoted = rest.startsWith('"') || rest.startsWith("'");
|
|
255
|
+
if (!quoted && colon > 0) {
|
|
256
|
+
const item = {};
|
|
257
|
+
const key = rest.slice(0, colon).trim();
|
|
258
|
+
const value = rest.slice(colon + 1).trim();
|
|
259
|
+
item[key] = value ? parseScalar(value) : {};
|
|
260
|
+
owner.container.push(item);
|
|
261
|
+
stack.push({ indent, container: item, key });
|
|
236
262
|
}
|
|
237
263
|
else {
|
|
238
|
-
|
|
239
|
-
stack.push({ indent, obj: parent[key] });
|
|
264
|
+
owner.container.push(parseScalar(rest));
|
|
240
265
|
}
|
|
241
|
-
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const colonIndex = trimmed.indexOf(":");
|
|
269
|
+
if (colonIndex <= 0)
|
|
270
|
+
continue;
|
|
271
|
+
const key = trimmed.slice(0, colonIndex).trim();
|
|
272
|
+
const value = trimmed.slice(colonIndex + 1).trim();
|
|
273
|
+
const current = stack[stack.length - 1];
|
|
274
|
+
if (!current || Array.isArray(current.container))
|
|
275
|
+
continue;
|
|
276
|
+
if (value) {
|
|
277
|
+
current.container[key] = parseScalar(value);
|
|
278
|
+
current.key = key;
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
const child = {};
|
|
282
|
+
current.container[key] = child;
|
|
283
|
+
current.key = key;
|
|
284
|
+
stack.push({ indent, container: child });
|
|
242
285
|
}
|
|
243
286
|
}
|
|
244
|
-
return
|
|
287
|
+
return root;
|
|
245
288
|
}
|
|
@@ -93,9 +93,9 @@ export class XrayConfigLoader {
|
|
|
93
93
|
}
|
|
94
94
|
const cfg = config;
|
|
95
95
|
return {
|
|
96
|
-
enabled: typeof cfg.enabled === 'boolean' ? cfg.enabled :
|
|
96
|
+
enabled: typeof cfg.enabled === 'boolean' ? cfg.enabled : false,
|
|
97
97
|
interval_minutes: Math.max(5, Math.min(1440, typeof cfg.interval_minutes === 'number' ? cfg.interval_minutes : 60)),
|
|
98
|
-
auto_schedule: typeof cfg.auto_schedule === 'boolean' ? cfg.auto_schedule :
|
|
98
|
+
auto_schedule: typeof cfg.auto_schedule === 'boolean' ? cfg.auto_schedule : false,
|
|
99
99
|
include_health_assessment: typeof cfg.include_health_assessment === 'boolean' ? cfg.include_health_assessment : true,
|
|
100
100
|
include_agent_activities: typeof cfg.include_agent_activities === 'boolean' ? cfg.include_agent_activities : true,
|
|
101
101
|
include_pipeline_operations: typeof cfg.include_pipeline_operations === 'boolean' ? cfg.include_pipeline_operations : true,
|
|
@@ -123,9 +123,9 @@ export class XrayConfigLoader {
|
|
|
123
123
|
session_persistence: true,
|
|
124
124
|
},
|
|
125
125
|
autonomous_reporting: {
|
|
126
|
-
enabled:
|
|
126
|
+
enabled: false,
|
|
127
127
|
interval_minutes: 60,
|
|
128
|
-
auto_schedule:
|
|
128
|
+
auto_schedule: false,
|
|
129
129
|
include_health_assessment: true,
|
|
130
130
|
include_agent_activities: true,
|
|
131
131
|
include_pipeline_operations: true,
|
|
@@ -406,7 +406,7 @@ export interface FeaturesConfig {
|
|
|
406
406
|
enabled?: boolean;
|
|
407
407
|
};
|
|
408
408
|
memory_routing?: MemoryRoutingConfig;
|
|
409
|
-
/**
|
|
409
|
+
/** Marker is opt-in and the shipped default is off. */
|
|
410
410
|
grok_postprocessor_light?: boolean;
|
|
411
411
|
/** P2 — git post-commit session inference capture */
|
|
412
412
|
inference_session_capture?: {
|
|
@@ -342,9 +342,9 @@ export class FeaturesConfigLoader {
|
|
|
342
342
|
no_new_surface: true,
|
|
343
343
|
},
|
|
344
344
|
autonomous_reporting: {
|
|
345
|
-
enabled:
|
|
345
|
+
enabled: false,
|
|
346
346
|
interval_minutes: 60,
|
|
347
|
-
auto_schedule:
|
|
347
|
+
auto_schedule: false,
|
|
348
348
|
include_health_assessment: true,
|
|
349
349
|
include_agent_activities: true,
|
|
350
350
|
include_pipeline_operations: true,
|
|
@@ -410,7 +410,7 @@ export class FeaturesConfigLoader {
|
|
|
410
410
|
max_cache_size_mb: 50,
|
|
411
411
|
},
|
|
412
412
|
inference: {
|
|
413
|
-
description: "
|
|
413
|
+
description: "The cycle runs from the CLI command inference:run and the session wake does not start it.",
|
|
414
414
|
enabled: true,
|
|
415
415
|
workflow_dir: ".xray/inference",
|
|
416
416
|
reports_dir: ".xray/reports",
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
"hooks": {
|
|
4
4
|
"preToolUse": [
|
|
5
5
|
{
|
|
6
|
-
"command": ".cursor/hooks/pre-tool-use.sh"
|
|
6
|
+
"command": ".cursor/hooks/pre-tool-use.sh",
|
|
7
|
+
"failClosed": true
|
|
7
8
|
}
|
|
8
9
|
],
|
|
9
10
|
"preCompact": [
|
|
@@ -18,12 +19,14 @@
|
|
|
18
19
|
],
|
|
19
20
|
"beforeShellExecution": [
|
|
20
21
|
{
|
|
21
|
-
"command": ".cursor/hooks/before-shell-execution.sh"
|
|
22
|
+
"command": ".cursor/hooks/before-shell-execution.sh",
|
|
23
|
+
"failClosed": true
|
|
22
24
|
}
|
|
23
25
|
],
|
|
24
26
|
"beforeReadFile": [
|
|
25
27
|
{
|
|
26
|
-
"command": ".cursor/hooks/before-read-file.sh"
|
|
28
|
+
"command": ".cursor/hooks/before-read-file.sh",
|
|
29
|
+
"failClosed": true
|
|
27
30
|
}
|
|
28
31
|
]
|
|
29
32
|
}
|
|
@@ -5,23 +5,40 @@
|
|
|
5
5
|
* Does not rely on sessionStart (unavailable on managed cloud).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
8
|
+
// Gate modules load inside main()'s try (dynamic import): a missing or broken module must deny, not crash
|
|
9
|
+
// before the handler with no JSON (Cursor would then fail open). Nothing here may import them statically.
|
|
10
|
+
let appendHookActivity = () => {};
|
|
11
|
+
|
|
12
|
+
/** Deny JSON on stdout and exit 0. Used when the gate cannot run at all. */
|
|
13
|
+
function denyHard(reason) {
|
|
14
|
+
try {
|
|
15
|
+
process.stdout.write(`${JSON.stringify({ permission: 'deny', user_message: reason, agent_message: reason })}\n`);
|
|
16
|
+
} finally {
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function cursorHookErrorPermission(err) {
|
|
22
|
+
if (err?.code === 'EACCES') return 'allow'; // code only: a message mentioning EACCES is not a permission error
|
|
23
|
+
return 'deny';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Bootstrap: a fresh checkout has no build yet, so gate modules can be missing.
|
|
27
|
+
// Only the exact build commands may run then, so the hook cannot block the build that creates it.
|
|
28
|
+
const BOOTSTRAP_CMDS = new Set(['npm ci', 'npm run build', 'npm ci && npm run build', 'npm install && npm run build']);
|
|
29
|
+
export function cursorBootstrapPermission(err, event) {
|
|
30
|
+
if (err?.code !== 'ERR_MODULE_NOT_FOUND') return 'deny';
|
|
31
|
+
const cmd = String(event?.tool_input?.command ?? event?.command ?? '').trim().replace(/\s+/g, ' ');
|
|
32
|
+
return BOOTSTRAP_CMDS.has(cmd) ? 'allow' : 'deny';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function readRawEvent() {
|
|
36
|
+
try {
|
|
37
|
+
let raw = '';
|
|
38
|
+
for await (const c of process.stdin) raw += c;
|
|
39
|
+
return JSON.parse(raw);
|
|
40
|
+
} catch { return null; }
|
|
41
|
+
}
|
|
25
42
|
|
|
26
43
|
function finish(root, permission, reason, hint, toolName, extra = {}) {
|
|
27
44
|
const out = { permission };
|
|
@@ -32,22 +49,38 @@ function finish(root, permission, reason, hint, toolName, extra = {}) {
|
|
|
32
49
|
if (hint && permission === 'allow') {
|
|
33
50
|
out.user_message = hint;
|
|
34
51
|
}
|
|
52
|
+
if (extra.hookSpecificOutput) out.hookSpecificOutput = extra.hookSpecificOutput;
|
|
35
53
|
console.log(JSON.stringify(out));
|
|
36
|
-
|
|
37
|
-
tool:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
try {
|
|
55
|
+
appendHookActivity(root, 'cursor-pre-tool-use', permission, permission === 'deny' ? 'error' : 'info', {
|
|
56
|
+
tool: toolName,
|
|
57
|
+
reason: reason || hint || null,
|
|
58
|
+
gate: extra.gate || null,
|
|
59
|
+
livePath: true,
|
|
60
|
+
});
|
|
61
|
+
} catch { /* the verdict is already on stdout; activity logging must not change it */ }
|
|
42
62
|
process.exit(0);
|
|
43
63
|
}
|
|
44
64
|
|
|
45
65
|
async function main() {
|
|
46
|
-
|
|
66
|
+
let fallbackRoot = process.cwd();
|
|
47
67
|
let toolName = 'unknown';
|
|
48
68
|
|
|
49
69
|
try {
|
|
70
|
+
const u = await import('./cursor-hook-utils.js');
|
|
71
|
+
const { lensDecision, repertoireRoute } = await import('../../hooks/pre-tool-speak.mjs');
|
|
72
|
+
const {
|
|
73
|
+
checkCodexPatterns, checkFullTestSuite, cursorGateRoot, cursorHeatRoots, cursorSessionId, cursorToolContext,
|
|
74
|
+
cursorWorkspaceRoot, ensureCursorSessionBoot, evaluatePreToolGate, isShellTool, isWriteTool,
|
|
75
|
+
loadDelegationGateFeatures, loadFeatures, readStdinJson,
|
|
76
|
+
} = u;
|
|
77
|
+
appendHookActivity = u.appendHookActivity;
|
|
78
|
+
fallbackRoot = cursorWorkspaceRoot();
|
|
50
79
|
const event = await readStdinJson();
|
|
80
|
+
// readStdinJson returns {} for empty or malformed stdin; Cursor always sends a JSON object, so deny those.
|
|
81
|
+
if (!event || typeof event !== 'object' || Array.isArray(event) || Object.keys(event).length === 0) {
|
|
82
|
+
finish(fallbackRoot, 'deny', 'preToolUse: empty or malformed hook input — denied', null, toolName, { gate: 'bad-input' });
|
|
83
|
+
}
|
|
51
84
|
const eventRoot = cursorWorkspaceRoot(event);
|
|
52
85
|
const sessionId = cursorSessionId(event);
|
|
53
86
|
const heatRoots = cursorHeatRoots(event);
|
|
@@ -60,7 +93,7 @@ async function main() {
|
|
|
60
93
|
const gateFeatures = loadDelegationGateFeatures(gateRoot, 'cursor');
|
|
61
94
|
const ctx = cursorToolContext(event);
|
|
62
95
|
toolName = ctx.toolName;
|
|
63
|
-
const { content, cmd, toolInput } = ctx;
|
|
96
|
+
const { content, cmd, toolInput, paths } = ctx;
|
|
64
97
|
|
|
65
98
|
const gateBlock = evaluatePreToolGate(toolName, toolInput, {
|
|
66
99
|
projectRoot: gateRoot,
|
|
@@ -77,35 +110,65 @@ async function main() {
|
|
|
77
110
|
if (extraBlock) finish(gateRoot, 'deny', extraBlock, null, toolName);
|
|
78
111
|
}
|
|
79
112
|
|
|
113
|
+
const spoken = [content, cmd, toolInput.pattern, toolInput.query, ...(paths || [])].filter(Boolean).join('\n');
|
|
114
|
+
const lensStop = lensDecision(gateRoot, toolName, spoken);
|
|
115
|
+
if (lensStop?.decision === 'deny') {
|
|
116
|
+
finish(gateRoot, 'deny', lensStop.reason, null, toolName, { gate: lensStop.gate });
|
|
117
|
+
}
|
|
118
|
+
const route = repertoireRoute([toolName, cmd, content].filter(Boolean).join(' '));
|
|
119
|
+
|
|
80
120
|
if (isShellTool(toolName) && cmd) {
|
|
81
121
|
const testHint = checkFullTestSuite(cmd, features);
|
|
82
|
-
if (testHint) finish(gateRoot, 'allow', null, testHint, toolName);
|
|
122
|
+
if (testHint) finish(gateRoot, 'allow', null, testHint, toolName, { ...(route || {}) });
|
|
83
123
|
}
|
|
84
124
|
|
|
85
125
|
if (gateBlock.reason) {
|
|
86
126
|
finish(gateRoot, 'allow', gateBlock.reason, gateBlock.hint, toolName, {
|
|
87
127
|
gate: gateBlock.gate,
|
|
88
128
|
warn: true,
|
|
129
|
+
...(route || {}),
|
|
89
130
|
});
|
|
90
131
|
}
|
|
91
132
|
|
|
92
|
-
finish(gateRoot, 'allow', null, null, toolName);
|
|
133
|
+
finish(gateRoot, 'allow', null, null, toolName, { ...(route || {}) });
|
|
93
134
|
} catch (err) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
135
|
+
if (err?.code === 'ERR_MODULE_NOT_FOUND' && cursorBootstrapPermission(err, await readRawEvent()) === 'allow') {
|
|
136
|
+
console.log(JSON.stringify({ permission: 'allow', user_message: 'preToolUse: build not present — bootstrap build allowed' }));
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
const failOpen = cursorHookErrorPermission(err) === 'allow';
|
|
140
|
+
try {
|
|
141
|
+
appendHookActivity(fallbackRoot, 'cursor-pre-tool-use', 'hook-error', 'error', {
|
|
142
|
+
tool: toolName,
|
|
143
|
+
error: err.message,
|
|
144
|
+
failOpen,
|
|
145
|
+
});
|
|
146
|
+
} catch { /* logging must not turn a deny into a crash */ }
|
|
147
|
+
if (failOpen) {
|
|
148
|
+
// Dest EACCES on a Cloud workspace wrapper must not deny every tool.
|
|
149
|
+
finish(
|
|
150
|
+
fallbackRoot,
|
|
151
|
+
'allow',
|
|
152
|
+
`preToolUse hook error — fail open: ${err.message}`,
|
|
153
|
+
null,
|
|
154
|
+
toolName,
|
|
155
|
+
{ gate: 'hook-error-fail-open' },
|
|
156
|
+
);
|
|
157
|
+
}
|
|
100
158
|
finish(
|
|
101
159
|
fallbackRoot,
|
|
102
|
-
'
|
|
103
|
-
`preToolUse hook error
|
|
160
|
+
'deny',
|
|
161
|
+
`preToolUse hook error blocked the tool: ${err.message}`,
|
|
104
162
|
null,
|
|
105
163
|
toolName,
|
|
106
|
-
{ gate: 'hook-error
|
|
164
|
+
{ gate: 'hook-error' },
|
|
107
165
|
);
|
|
108
166
|
}
|
|
109
167
|
}
|
|
110
168
|
|
|
111
|
-
|
|
169
|
+
const launchedAsCli = Boolean(process.argv[1] && process.argv[1].endsWith('pre-tool-use.js'));
|
|
170
|
+
if (launchedAsCli) {
|
|
171
|
+
process.on('uncaughtException', (err) => denyHard(`preToolUse hook crashed — denied: ${err?.message || err}`));
|
|
172
|
+
process.on('unhandledRejection', (err) => denyHard(`preToolUse hook crashed — denied: ${err?.message || err}`));
|
|
173
|
+
main().catch((err) => denyHard(`preToolUse hook crashed — denied: ${err?.message || err}`));
|
|
174
|
+
}
|
|
@@ -110,7 +110,15 @@ if [ -n "$MILL" ]; then
|
|
|
110
110
|
fi
|
|
111
111
|
|
|
112
112
|
if [ -z "$NODE_BIN" ] || [ -z "$MILL" ] || [ ! -f "${MILL}/${REL}" ]; then
|
|
113
|
-
|
|
113
|
+
# Gate events fail closed: no node or no hook file means the gate cannot run, so the tool is denied.
|
|
114
|
+
case "$EVENT" in
|
|
115
|
+
preToolUse|beforeShellExecution|beforeReadFile)
|
|
116
|
+
printf '{"permission":"deny","user_message":"xray-cloud-hook: node or hook file missing — denied (fail closed)","agent_message":"xray-cloud-hook: node or hook file missing — denied (fail closed)"}\n'
|
|
117
|
+
;;
|
|
118
|
+
*)
|
|
119
|
+
printf '{"permission":"allow","user_message":"xray-cloud-hook: node or hook file missing — %s skipped"}\n' "$EVENT"
|
|
120
|
+
;;
|
|
121
|
+
esac
|
|
114
122
|
exit 0
|
|
115
123
|
fi
|
|
116
124
|
|
|
@@ -22,65 +22,9 @@ import {
|
|
|
22
22
|
} from './grok-hook-utils.js';
|
|
23
23
|
import { appendHookActivity } from './grok-hook-activity.js';
|
|
24
24
|
import { cardStop, lensBeforeResearch } from '../../hooks/goggles-pipeline.mjs';
|
|
25
|
-
import {
|
|
26
|
-
import { existsSync } from 'node:fs';
|
|
27
|
-
import { dirname, join } from 'node:path';
|
|
28
|
-
import { fileURLToPath } from 'node:url';
|
|
29
|
-
|
|
30
|
-
const hookDir = dirname(fileURLToPath(import.meta.url));
|
|
31
|
-
let scoreAndRouteFn;
|
|
32
|
-
|
|
33
|
-
function loadScoreAndRoute() {
|
|
34
|
-
if (scoreAndRouteFn !== undefined) return scoreAndRouteFn;
|
|
35
|
-
scoreAndRouteFn = null;
|
|
36
|
-
const require = createRequire(import.meta.url);
|
|
37
|
-
const candidates = [
|
|
38
|
-
join(hookDir, '../../../nucleus/thin-dispatch.js'),
|
|
39
|
-
join(hookDir, '../../../../dist/nucleus/thin-dispatch.js'),
|
|
40
|
-
];
|
|
41
|
-
for (const candidate of candidates) {
|
|
42
|
-
if (!existsSync(candidate)) continue;
|
|
43
|
-
try {
|
|
44
|
-
const loaded = require(candidate);
|
|
45
|
-
if (typeof loaded.scoreAndRoute === 'function') {
|
|
46
|
-
scoreAndRouteFn = loaded.scoreAndRoute;
|
|
47
|
-
return scoreAndRouteFn;
|
|
48
|
-
}
|
|
49
|
-
} catch {
|
|
50
|
-
/* next built copy */
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Speak only when the organ changes the agent. A match that leaves the agent is quiet. */
|
|
57
|
-
export function repertoireDecisionFields(routed) {
|
|
58
|
-
if (!routed?.memoryRouting?.overridden || !routed.agent) return null;
|
|
59
|
-
const signals = Array.isArray(routed.memoryRouting.signals)
|
|
60
|
-
? routed.memoryRouting.signals.slice(0, 4).filter(Boolean)
|
|
61
|
-
: [];
|
|
62
|
-
const why = signals.length ? ` on ${signals.join(', ')}` : '';
|
|
63
|
-
return {
|
|
64
|
-
agent: routed.agent,
|
|
65
|
-
gate: 'repertoire',
|
|
66
|
-
hookSpecificOutput: {
|
|
67
|
-
hookEventName: 'PreToolUse',
|
|
68
|
-
additionalContext: `Repertoire changed the route to ${routed.agent}${why}. Follow that agent for this work.`,
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
}
|
|
25
|
+
import { repertoireDecisionFields, repertoireRoute } from '../../hooks/pre-tool-speak.mjs';
|
|
72
26
|
|
|
73
|
-
|
|
74
|
-
const text = String(operation || '').slice(0, 500).trim();
|
|
75
|
-
if (!text) return null;
|
|
76
|
-
try {
|
|
77
|
-
const scoreAndRoute = loadScoreAndRoute();
|
|
78
|
-
if (!scoreAndRoute) return null;
|
|
79
|
-
return repertoireDecisionFields(scoreAndRoute(text, {}));
|
|
80
|
-
} catch {
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
27
|
+
export { repertoireDecisionFields };
|
|
84
28
|
|
|
85
29
|
function finish(root, decision, reason, hint, toolName, extra = {}) {
|
|
86
30
|
const out = { decision, ...extra };
|
|
@@ -914,9 +914,22 @@ async function handleDelegationGate(command, projectRoot, logDir) {
|
|
|
914
914
|
logDir,
|
|
915
915
|
`[delegation-gate] deny tool=${tool} gate=${outcome.gate} reason=${outcome.reason}`,
|
|
916
916
|
);
|
|
917
|
+
return { phase: "pre", ...outcome };
|
|
917
918
|
}
|
|
918
919
|
|
|
919
|
-
|
|
920
|
+
const spoken = `${tool} ${JSON.stringify(args)}`.slice(0, 500);
|
|
921
|
+
const { lensDecision, repertoireRoute } = await import("../hooks/pre-tool-speak.mjs");
|
|
922
|
+
const lens = lensDecision(projectRoot, tool, spoken);
|
|
923
|
+
if (lens) {
|
|
924
|
+
return {
|
|
925
|
+
allow: false,
|
|
926
|
+
phase: "pre",
|
|
927
|
+
gate: lens.gate || "lens",
|
|
928
|
+
reason: lens.reason,
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
const route = repertoireRoute(spoken);
|
|
932
|
+
return { phase: "pre", ...outcome, ...(route || {}) };
|
|
920
933
|
} catch (error) {
|
|
921
934
|
const message = `delegation-gate failed: ${error.message || error}`;
|
|
922
935
|
logToActivity(logDir, `[delegation-gate] error ${message}`);
|
|
@@ -68,8 +68,9 @@ function loadDelegationGate() {
|
|
|
68
68
|
].filter(Boolean);
|
|
69
69
|
const found = candidates.find((p) => existsSync(p));
|
|
70
70
|
if (!found) {
|
|
71
|
-
throw
|
|
72
|
-
'delegation-gate.js missing — run npm run build in 0xray or npm install 0xray',
|
|
71
|
+
throw Object.assign(
|
|
72
|
+
new Error('delegation-gate.js missing — run npm run build in 0xray or npm install 0xray'),
|
|
73
|
+
{ code: 'ERR_MODULE_NOT_FOUND' },
|
|
73
74
|
);
|
|
74
75
|
}
|
|
75
76
|
return createRequire(import.meta.url)(found);
|