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.
- package/.grok-plugin/plugin.json +1 -1
- package/.opencode/skills/orchestrator/SKILL.md +12 -0
- package/.plugin/plugin.json +1 -1
- package/.xray/features.json +5 -5
- package/.xray/features.schema.json +1 -1
- package/AGENTS-consumer.md +10 -6
- package/AGENTS.md +19 -8
- package/LICENSE +1 -1
- package/README.md +65 -19
- package/SKILLS.md +5 -1
- package/dist/AGENTS.md +19 -8
- package/dist/CHANGELOG.md +85 -0
- package/dist/LICENSE +1 -1
- package/dist/README.md +65 -19
- package/dist/SKILLS.md +5 -1
- package/dist/cli/commands/openclaw-install.js +1 -1
- package/dist/cli/commands/plugin-commands.d.ts +4 -0
- package/dist/cli/commands/plugin-commands.js +84 -41
- package/dist/cli/index.js +12 -0
- 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/enforcement/index.d.ts +4 -0
- package/dist/enforcement/index.js +4 -0
- package/dist/enforcement/validators/code-quality-validators.js +6 -4
- package/dist/governance/llm-governance-provider.d.ts +7 -1
- package/dist/governance/llm-governance-provider.js +72 -14
- package/dist/governance/local-confer.d.ts +4 -1
- package/dist/governance/local-confer.js +22 -1
- 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/grok-cli.js +10 -37
- package/dist/integrations/grok/hooks/grok-hook-utils.js +8 -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/confer-hook-runtime.mjs +23 -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 +11 -7
- 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/mcps/architect-tools.server.d.ts +6 -0
- package/dist/mcps/architect-tools.server.js +19 -2
- package/dist/mcps/config/server-config-registry.js +8 -6
- package/dist/mcps/governance.server.d.ts +1 -0
- package/dist/mcps/governance.server.js +46 -0
- package/dist/mcps/knowledge-skills/code-review.server.js +7 -0
- package/dist/mcps/knowledge-skills/security-audit.server.js +5 -3
- package/dist/mcps/mcp-client.js +1 -1
- package/dist/mcps/orchestrator/handlers/complexity-handler.js +2 -2
- package/dist/mcps/researcher.server.js +12 -3
- 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/nucleus/autonomy-kernel.d.ts +2 -0
- package/dist/nucleus/autonomy-kernel.js +17 -13
- package/dist/nucleus/confer-host.d.ts +35 -0
- package/dist/nucleus/confer-host.js +55 -0
- package/dist/nucleus/confer.d.ts +19 -3
- package/dist/nucleus/confer.js +111 -55
- package/dist/nucleus/lead-dev-plan-persistence.js +4 -2
- package/dist/nucleus/suit-temperament.d.ts +1 -1
- package/dist/nucleus/suit-temperament.js +2 -2
- package/dist/nucleus/synthesis-consult-receipt.d.ts +7 -1
- package/dist/nucleus/synthesis-consult-receipt.js +86 -35
- 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/dist/reporting/framework-reporting-system.js +2 -5
- package/dist/reporting/log-parser.d.ts +2 -0
- package/dist/reporting/log-parser.js +7 -6
- package/dist/skills/orchestrator/SKILL.md +12 -0
- 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 +16 -6
- package/package.json +4 -4
- 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-confer-core.mjs +153 -29
- package/scripts/mjs/verify-pipeline-facets.mjs +1 -1
- package/scripts/mjs/verify-user-aside-core.mjs +3 -4
- package/scripts/node/bridge-mcp-wiring.cjs +236 -5
- package/scripts/node/confer-switch.cjs +66 -0
- package/scripts/node/install-bridges.cjs +44 -53
- 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/src/mcps/architect-tools.server.ts +26 -2
- package/src/mcps/config/__tests__/server-config-registry.test.ts +15 -6
- package/src/mcps/config/server-config-registry.ts +9 -6
- package/src/mcps/governance.server.ts +49 -0
- package/src/mcps/knowledge-skills/code-review.server.ts +8 -0
- package/src/mcps/knowledge-skills/security-audit.server.ts +11 -12
- package/src/mcps/mcp-client.ts +1 -1
- package/src/mcps/orchestrator/handlers/complexity-handler.ts +2 -1
- package/src/mcps/researcher.server.ts +19 -10
- package/src/skills/orchestrator/SKILL.md +12 -0
- package/vendor/@0xray/repertoire/README.md +1 -1
- package/vendor/@0xray/repertoire/scripts/run-live-synthesis-checkpoint.mjs +5 -51
- package/xray/agents_template.md +2 -2
- package/xray/features.json +5 -5
- package/xray/features.schema.json +1 -1
- package/docs-site/docs/plates/live-mesh.md +0 -41
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import type { GovernanceRole, GovernanceVote } from "./llm-governance-provider.js";
|
|
1
|
+
import type { GovernanceMiss, GovernanceRole, GovernanceVote } from "./llm-governance-provider.js";
|
|
2
2
|
export type LocalConferInput = {
|
|
3
3
|
role: GovernanceRole;
|
|
4
4
|
highConfidenceTrapPresent?: boolean | undefined;
|
|
5
5
|
recommendedAgent?: string | null | undefined;
|
|
6
6
|
llmConfigured?: boolean | undefined;
|
|
7
|
+
miss?: GovernanceMiss | undefined;
|
|
7
8
|
};
|
|
8
9
|
/** Receipt when nested LLM is absent or returned no vote. Never approve — analyze_proposal feeds mergeVotes. */
|
|
9
10
|
export declare function localConferVote(input: LocalConferInput): GovernanceVote;
|
|
11
|
+
/** Stop a model from planting a second DECISION or Verdict line inside the reasoning body. */
|
|
12
|
+
export declare function sanitizeGovernanceReasoning(reasoning: string): string;
|
|
10
13
|
export declare function formatGovernanceVoteText(vote: GovernanceVote, extra?: string): string;
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
/** Receipt when nested LLM is absent or returned no vote. Never approve — analyze_proposal feeds mergeVotes. */
|
|
2
2
|
export function localConferVote(input) {
|
|
3
3
|
const role = input.role;
|
|
4
|
+
if (input.miss && input.miss !== "not-configured") {
|
|
5
|
+
return {
|
|
6
|
+
decision: "abstain",
|
|
7
|
+
confidence: 0.5,
|
|
8
|
+
reasoning: `Local confer (${role}); nested LLM did not produce a vote (${input.miss}).`,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
4
11
|
if (input.llmConfigured) {
|
|
5
12
|
return {
|
|
6
13
|
decision: "abstain",
|
|
@@ -22,6 +29,20 @@ export function localConferVote(input) {
|
|
|
22
29
|
reasoning: `Local confer (${role}); nested LLM not configured.`,
|
|
23
30
|
};
|
|
24
31
|
}
|
|
32
|
+
function abstainedWithoutUsableVote(vote) {
|
|
33
|
+
if (vote.decision !== "abstain")
|
|
34
|
+
return false;
|
|
35
|
+
return /nested LLM not configured|did not produce a vote|returned no vote/i.test(vote.reasoning);
|
|
36
|
+
}
|
|
37
|
+
/** Stop a model from planting a second DECISION or Verdict line inside the reasoning body. */
|
|
38
|
+
export function sanitizeGovernanceReasoning(reasoning) {
|
|
39
|
+
return reasoning
|
|
40
|
+
.split("\n")
|
|
41
|
+
.map((line) => /^\s*(?:[-*•]\s*)?(?:Verdict:|DECISION:)/i.test(line) ? `> ${line.trim()}` : line)
|
|
42
|
+
.join("\n");
|
|
43
|
+
}
|
|
25
44
|
export function formatGovernanceVoteText(vote, extra = "") {
|
|
26
|
-
|
|
45
|
+
const reviewLine = abstainedWithoutUsableVote(vote) ? "\nVerdict: UNREVIEWED" : "";
|
|
46
|
+
const reasoning = sanitizeGovernanceReasoning(vote.reasoning);
|
|
47
|
+
return `DECISION: ${vote.decision}\nCONFIDENCE: ${vote.confidence.toFixed(2)}\nREASONING: ${reasoning}${reviewLine}${extra}`;
|
|
27
48
|
}
|
|
@@ -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
|
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
import { frameworkLogger } from '../../core/framework-logger.js';
|
|
18
18
|
import fs from 'fs';
|
|
19
19
|
import path from 'path';
|
|
20
|
-
import { execSync } from 'child_process';
|
|
21
20
|
import { createRequire } from 'module';
|
|
22
21
|
import { fileURLToPath } from 'node:url';
|
|
23
22
|
import { syncBuiltinSkills } from '../../cli/commands/skill-install.js';
|
|
@@ -27,27 +26,13 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
27
26
|
const packageRoot = path.resolve(__dirname, '..', '..', '..');
|
|
28
27
|
const requireCjs = createRequire(import.meta.url);
|
|
29
28
|
const { resolveConsumerTargetDir, patchGrokHooks } = requireCjs(path.join(packageRoot, 'scripts/node/install-bridges.cjs'));
|
|
30
|
-
const {
|
|
31
|
-
function registerGrokMcpServers(targetDir) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
for (const s of XRAY_MCP_SERVERS) {
|
|
40
|
-
try {
|
|
41
|
-
const envEntries = { ...s.env, XRAY_ROOT: targetDir };
|
|
42
|
-
const envFlags = Object.entries(envEntries)
|
|
43
|
-
.map(([k, v]) => `--env "${k}=${v}"`)
|
|
44
|
-
.join(' ');
|
|
45
|
-
execSync(`grok mcp add ${s.name} --command npx --args "-y" "0xray" "mcp" "${s.mcpCmd}" ${envFlags}`, { stdio: 'pipe' });
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
// already registered or grok config conflict — non-blocking
|
|
49
|
-
}
|
|
50
|
-
}
|
|
29
|
+
const { resolveRepertoireMcp, resolveGogglesMcp } = requireCjs(path.join(packageRoot, 'scripts/node/bridge-mcp-wiring.cjs'));
|
|
30
|
+
function registerGrokMcpServers(targetDir, env) {
|
|
31
|
+
const wiring = requireCjs(path.join(packageRoot, 'scripts/node/bridge-mcp-wiring.cjs'));
|
|
32
|
+
const tomlPath = wiring.writeProjectSuitMcp(targetDir);
|
|
33
|
+
const trustPath = wiring.trustGrokFolder(targetDir, env);
|
|
34
|
+
process.stdout.write(`Project Grok MCP written to ${tomlPath}\n`);
|
|
35
|
+
process.stdout.write(`Grok folder trust written to ${trustPath}\n`);
|
|
51
36
|
}
|
|
52
37
|
export async function installForGrokCLI(options = {}) {
|
|
53
38
|
frameworkLogger.log('grok-integration', 'install-start', 'info', {
|
|
@@ -115,21 +100,9 @@ export async function installForGrokCLI(options = {}) {
|
|
|
115
100
|
writeProjectRepertoireMcp(targetDir);
|
|
116
101
|
writeProjectGogglesMcp(targetDir);
|
|
117
102
|
mintAfterWear(targetDir);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
console.log('\x1b[32m✓ Auto-trusted the 0xray plugin with Grok CLI\x1b[0m');
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
console.log('\nPlease run this command to fully trust the plugin:');
|
|
125
|
-
console.log(` grok plugins trust "${primary}"`);
|
|
126
|
-
}
|
|
127
|
-
if (isolated) {
|
|
128
|
-
registerGrokMcpServers(targetDir);
|
|
129
|
-
console.log('\x1b[32m✓ Registered 7 xray MCP servers with Grok CLI (npx)\x1b[0m');
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
console.log('\x1b[32m✓ Project-scoped Grok plugin worn (machine ~/.grok/plugins/0xray not written)\x1b[0m');
|
|
103
|
+
registerGrokMcpServers(targetDir, env);
|
|
104
|
+
if (!isolated) {
|
|
105
|
+
process.stdout.write('Project-scoped Grok plugin worn (machine ~/.grok/plugins/0xray not written)\n');
|
|
133
106
|
}
|
|
134
107
|
console.log('\n✅ 0xRay is now installed as a first-class Grok CLI plugin!');
|
|
135
108
|
console.log('Restart Grok or run `grok` to load the new hooks and MCP servers.');
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
updatePlanTodoStatusInPlace,
|
|
20
20
|
loadDelegationGateFeatures,
|
|
21
21
|
} from '../../hooks/delegation-gate-runtime.mjs';
|
|
22
|
-
import { isConferPendingForSession } from '../../hooks/confer-hook-runtime.mjs';
|
|
22
|
+
import { isConferPendingForSession, readConferReviewHint } from '../../hooks/confer-hook-runtime.mjs';
|
|
23
23
|
import { getActiveUserAsideBoot } from '../../hooks/user-aside-hook-runtime.mjs';
|
|
24
24
|
import {
|
|
25
25
|
applyStationHeat,
|
|
@@ -308,6 +308,7 @@ export function buildSessionBootPayload(root, source = '0xray/grok-session-start
|
|
|
308
308
|
const sessionId =
|
|
309
309
|
extra.sessionId || process.env.GROK_SESSION_ID || process.env.GROK_SESSION || null;
|
|
310
310
|
const conferPending = loadConferPending(root, sessionId);
|
|
311
|
+
const conferReview = readConferReviewHint(root);
|
|
311
312
|
const userAsideBoot = getActiveUserAsideBoot(root, sessionId);
|
|
312
313
|
const gateFeatures = loadDelegationGateFeatures(root, 'grok');
|
|
313
314
|
const frontier = gateFeatures.ceremony === 'lite';
|
|
@@ -332,6 +333,12 @@ export function buildSessionBootPayload(root, source = '0xray/grok-session-start
|
|
|
332
333
|
...heat,
|
|
333
334
|
...(siblingRoots.length > 0 ? { siblingWorkspaceRoots: siblingRoots } : {}),
|
|
334
335
|
...(conferPending ? { conferPending: true, conferTrigger: 'analyze-complexity at synthesis checkpoint' } : {}),
|
|
336
|
+
...(conferReview
|
|
337
|
+
? {
|
|
338
|
+
conferReview,
|
|
339
|
+
conferReviewHint: 'UNREVIEWED — abstain without a governance LLM blocks the consult todo',
|
|
340
|
+
}
|
|
341
|
+
: {}),
|
|
335
342
|
...(userAsideBoot ?? {}),
|
|
336
343
|
sessionId,
|
|
337
344
|
...extra,
|
|
@@ -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}`);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Lazy-load confer SSOT for Grok session-boot hints.
|
|
4
4
|
*/
|
|
5
5
|
import { createRequire } from 'node:module';
|
|
6
|
-
import { existsSync } from 'node:fs';
|
|
6
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
7
7
|
import { dirname, join, resolve } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
|
|
@@ -46,4 +46,26 @@ export function isConferPendingForSession(projectRoot, sessionId = null) {
|
|
|
46
46
|
const mod = loadConfer();
|
|
47
47
|
if (!mod?.isConferPending) return false;
|
|
48
48
|
return mod.isConferPending(projectRoot, sessionId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 'UNREVIEWED' when a consult receipt recorded abstain-without-model. */
|
|
52
|
+
export function readConferReviewHint(projectRoot) {
|
|
53
|
+
const stateDir = join(projectRoot, '.xray', 'state');
|
|
54
|
+
if (!existsSync(stateDir)) return null;
|
|
55
|
+
let names;
|
|
56
|
+
try {
|
|
57
|
+
names = readdirSync(stateDir);
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
for (const name of names) {
|
|
62
|
+
if (!name.startsWith('synthesis-consult-') || !name.endsWith('.json')) continue;
|
|
63
|
+
try {
|
|
64
|
+
const receipt = JSON.parse(readFileSync(join(stateDir, name), 'utf8'));
|
|
65
|
+
if (receipt && receipt.verdict === 'UNREVIEWED') return 'UNREVIEWED';
|
|
66
|
+
} catch {
|
|
67
|
+
/* unreadable receipt is not a hint */
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
49
71
|
}
|
|
@@ -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);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Tools: look, status_lens. Same organ as the CLI.
|
|
4
4
|
*/
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
6
|
-
import {
|
|
6
|
+
import { dirname, join, resolve } from 'node:path';
|
|
7
7
|
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
8
8
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
9
9
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
@@ -12,17 +12,9 @@ import * as bundled from './goggles-pipeline.mjs';
|
|
|
12
12
|
|
|
13
13
|
const HERE = fileURLToPath(new URL('.', import.meta.url));
|
|
14
14
|
const SERVER_NAME = 'goggles';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
'house',
|
|
19
|
-
'boot',
|
|
20
|
-
'governance',
|
|
21
|
-
'memory-recall',
|
|
22
|
-
'orchestration',
|
|
23
|
-
'processor',
|
|
24
|
-
'reporting',
|
|
25
|
-
];
|
|
15
|
+
/** Card planes come from the organ, not a hand-kept copy. */
|
|
16
|
+
const CARD_PLANES = Array.isArray(bundled.CARD_PLANES) ? bundled.CARD_PLANES : [];
|
|
17
|
+
const CARD_OUTERS = ['digest', 'triage'];
|
|
26
18
|
const SCOPES = ['ecosystem', 'part', 'one flow', 'one artifact'];
|
|
27
19
|
|
|
28
20
|
const LOOK_SCHEMA = {
|
|
@@ -31,7 +23,7 @@ const LOOK_SCHEMA = {
|
|
|
31
23
|
properties: {
|
|
32
24
|
kind: {
|
|
33
25
|
type: 'string',
|
|
34
|
-
description: '
|
|
26
|
+
description: 'Use "0" or "actuality" for the lens. Send kind, outer, or plane: an empty look is refused. Never with outer: kind and outer together is refused.',
|
|
35
27
|
enum: ['0', 'actuality'],
|
|
36
28
|
},
|
|
37
29
|
outer: {
|
|
@@ -46,7 +38,7 @@ const LOOK_SCHEMA = {
|
|
|
46
38
|
},
|
|
47
39
|
plane: {
|
|
48
40
|
type: 'string',
|
|
49
|
-
description: 'Card plane for digest or triage only. Not the lens.',
|
|
41
|
+
description: 'Card plane for digest or triage only. Not the lens. A plane with any other outer is refused.',
|
|
50
42
|
enum: CARD_PLANES,
|
|
51
43
|
},
|
|
52
44
|
},
|
|
@@ -55,7 +47,7 @@ const LOOK_SCHEMA = {
|
|
|
55
47
|
export const TOOLS = [
|
|
56
48
|
{
|
|
57
49
|
name: 'look',
|
|
58
|
-
description: 'Kind 0 is quiet on a match. It checks the six outer names. One outer is a reading. digest and triage return a card.',
|
|
50
|
+
description: 'Kind 0 is quiet on a match. It checks the six outer names, then sweeps every card plane for missing shipped files and names the first drift. One outer is a reading. digest and triage return a card. Refused: an empty look; kind with outer; a plane with an outer other than digest or triage.',
|
|
59
51
|
inputSchema: LOOK_SCHEMA,
|
|
60
52
|
},
|
|
61
53
|
{
|
|
@@ -152,6 +144,15 @@ function checkArgs(args) {
|
|
|
152
144
|
if (args.plane != null && !CARD_PLANES.includes(args.plane)) {
|
|
153
145
|
return deny('invalid_args', 'plane is a card plane.');
|
|
154
146
|
}
|
|
147
|
+
if (args.kind == null && args.outer == null && args.plane == null) {
|
|
148
|
+
return deny('invalid_args', 'Name a kind, one outer, or one plane.');
|
|
149
|
+
}
|
|
150
|
+
if (args.kind != null && args.outer != null) {
|
|
151
|
+
return deny('invalid_args', 'Send kind or outer, not both.');
|
|
152
|
+
}
|
|
153
|
+
if (args.plane != null && args.outer != null && !CARD_OUTERS.includes(args.outer)) {
|
|
154
|
+
return deny('invalid_args', 'plane goes with digest or triage only.');
|
|
155
|
+
}
|
|
155
156
|
return null;
|
|
156
157
|
}
|
|
157
158
|
|
|
@@ -161,7 +162,7 @@ function publicCard(view) {
|
|
|
161
162
|
if (Array.isArray(view.empty) && view.empty.length) notes.push(`Empty: ${view.empty.join(', ')}`);
|
|
162
163
|
if (view.exam) notes.push(view.exam);
|
|
163
164
|
}
|
|
164
|
-
const skills = view.skills ?
|
|
165
|
+
const skills = view.skills ? String(view.skills) : '';
|
|
165
166
|
const cascade = view.cascade
|
|
166
167
|
? {
|
|
167
168
|
plane: view.cascade.plane,
|
|
@@ -227,7 +228,8 @@ export async function answerLook(raw, root) {
|
|
|
227
228
|
const here = projectRoot(root);
|
|
228
229
|
if (pureKind0(args)) {
|
|
229
230
|
api.maintainLens(here);
|
|
230
|
-
const line = api.actualityLine();
|
|
231
|
+
const line = api.actualityLine() || cardDrift(api, here);
|
|
232
|
+
if (line) writeLens(api, here, line);
|
|
231
233
|
if (!line) return quiet('kind0');
|
|
232
234
|
return { ok: true, quiet: false, mode: 'kind0', content: line };
|
|
233
235
|
}
|
|
@@ -269,6 +271,22 @@ export async function answerLook(raw, root) {
|
|
|
269
271
|
};
|
|
270
272
|
}
|
|
271
273
|
|
|
274
|
+
/** Kind 0 card sweep: triage's file check on every card plane, shipped paths only. */
|
|
275
|
+
function cardDrift(api, here) {
|
|
276
|
+
if (typeof api.shippedDrift !== 'function') return '';
|
|
277
|
+
try {
|
|
278
|
+
return api.shippedDrift(here);
|
|
279
|
+
} catch {
|
|
280
|
+
return '';
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function writeLens(api, here, line) {
|
|
285
|
+
const file = api.lensPath(here);
|
|
286
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
287
|
+
writeFileSync(file, `${line}\n`);
|
|
288
|
+
}
|
|
289
|
+
|
|
272
290
|
function lensBody(root) {
|
|
273
291
|
const file = join(root, '.xray', 'state', 'LENS.md');
|
|
274
292
|
if (!existsSync(file)) return { found: false, text: '' };
|