@lovrozagar/grunt 0.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/.agents/agents/grunt.md +36 -0
- package/.agents/agents/implementer.md +40 -0
- package/.agents/agents/orchestrator.md +50 -0
- package/.agents/agents/thinker.md +27 -0
- package/.agents/hooks.json +26 -0
- package/.agents/mcp_config.json +3 -0
- package/.claude/agents/grunt.md +44 -0
- package/.claude/agents/implementer.md +46 -0
- package/.claude/agents/orchestrator.md +55 -0
- package/.claude/agents/thinker.md +34 -0
- package/.claude/settings.json +36 -0
- package/.codex/agents/grunt.toml +30 -0
- package/.codex/agents/implementer.toml +34 -0
- package/.codex/agents/orchestrator.toml +45 -0
- package/.codex/agents/thinker.toml +23 -0
- package/.codex/config.toml +2 -0
- package/.codex/hooks.json +26 -0
- package/.grok/agents/grunt.md +31 -0
- package/.grok/agents/implementer.md +35 -0
- package/.grok/agents/orchestrator.md +47 -0
- package/.grok/agents/thinker.md +25 -0
- package/.grok/config.toml +6 -0
- package/.grok/global-settings.toml +35 -0
- package/.grok/hooks/orchestrate-parent.js +476 -0
- package/.grok/hooks/orchestrate-parent.json +60 -0
- package/.grok/hooks/rtk-grok.js +137 -0
- package/.grok/hooks/rtk.json +16 -0
- package/.grok/hooks/rulesync.json +3 -0
- package/.grok/parent.md +1 -0
- package/.grok/roles/grunt.toml +5 -0
- package/.grok/roles/implementer.toml +5 -0
- package/.grok/roles/thinker.toml +5 -0
- package/.grok/skills/explain/SKILL.md +15 -0
- package/.grok/skills/implement-plan/SKILL.md +91 -0
- package/.grok/skills/parent/SKILL.md +12 -0
- package/.grok/skills/shared/plan-format.md +158 -0
- package/.grok/skills/write-plan/SKILL.md +84 -0
- package/.mcp.json +3 -0
- package/.rulesync/global-settings/antigravity.json +1 -0
- package/.rulesync/global-settings/claude.json +1 -0
- package/.rulesync/global-settings/codex.toml +1 -0
- package/.rulesync/global-settings/manifest.json +37 -0
- package/.rulesync/hooks.jsonc +22 -0
- package/.rulesync/mcp-policy.jsonc +15 -0
- package/.rulesync/reference/cascade.md +85 -0
- package/.rulesync/reference/hooks.md +19 -0
- package/.rulesync/reference/map.md +57 -0
- package/.rulesync/reference/rtk.md +23 -0
- package/.rulesync/rules/CLAUDE.md +13 -0
- package/.rulesync/rules/overview.md +23 -0
- package/.rulesync/skills/explain/SKILL.md +15 -0
- package/.rulesync/skills/parent/SKILL.md +12 -0
- package/.rulesync/skills/terse/SKILL.md +8 -0
- package/.rulesync/subagents/grunt.md +51 -0
- package/.rulesync/subagents/implementer.md +54 -0
- package/.rulesync/subagents/orchestrator.md +69 -0
- package/.rulesync/subagents/thinker.md +44 -0
- package/AGENTS.md +12 -0
- package/CLAUDE.md +7 -0
- package/LICENSE +21 -0
- package/README.md +56 -0
- package/bin/grunt.js +4 -0
- package/cli/grunt.mjs +54 -0
- package/cli/grunt.test.ts +117 -0
- package/cli/init.mjs +113 -0
- package/cli/init.test.ts +268 -0
- package/package.json +77 -0
- package/scripts/check-globals.mjs +98 -0
- package/scripts/emit-mcp-policy.mjs +434 -0
- package/scripts/gate-fat-tools.mjs +485 -0
- package/scripts/grunt-job.mjs +482 -0
- package/scripts/parse-need.mjs +75 -0
- package/scripts/persist-plan.mjs +309 -0
- package/scripts/purge-global-mcps.mjs +291 -0
- package/scripts/scrub-spawn-prompt.mjs +219 -0
- package/scripts/scrub-text +58 -0
- package/scripts/scrub-text-lib.mjs +210 -0
- package/scripts/sync-global-settings.mjs +239 -0
- package/scripts/telemetry.mjs +38 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Assert home Grok globals match SSOT; project config has no [features]/[agent]. */
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import { parse as parseToml } from "smol-toml";
|
|
7
|
+
|
|
8
|
+
const APPLY_HINT = "npm run sync:globals:apply";
|
|
9
|
+
const PROJECT_REL = ".grok/config.toml";
|
|
10
|
+
const HOME_REL = ".grok/config.toml";
|
|
11
|
+
|
|
12
|
+
function isPlainObject(v) {
|
|
13
|
+
return v != null && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseTomlFile(abs, label) {
|
|
17
|
+
let raw;
|
|
18
|
+
try {
|
|
19
|
+
raw = fs.readFileSync(abs, "utf8");
|
|
20
|
+
} catch (err) {
|
|
21
|
+
if (err && err.code === "ENOENT") return { ok: false, missing: true };
|
|
22
|
+
return { ok: false, error: `unreadable ${label}` };
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const v = parseToml(raw);
|
|
26
|
+
if (!isPlainObject(v)) return { ok: false, error: `invalid ${label}` };
|
|
27
|
+
return { ok: true, value: v };
|
|
28
|
+
} catch {
|
|
29
|
+
return { ok: false, error: `unreadable ${label}` };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function checkGlobals({ home, workspaceRoot } = {}) {
|
|
34
|
+
const homeDir = home || process.env.HOME;
|
|
35
|
+
const ws = workspaceRoot || process.cwd();
|
|
36
|
+
if (!homeDir) {
|
|
37
|
+
return { ok: false, error: `HOME required; ${APPLY_HINT}` };
|
|
38
|
+
}
|
|
39
|
+
const homeConfig = path.join(homeDir, HOME_REL);
|
|
40
|
+
const homeParsed = parseTomlFile(homeConfig, "home ~/.grok/config.toml");
|
|
41
|
+
if (!homeParsed.ok) {
|
|
42
|
+
if (homeParsed.missing) {
|
|
43
|
+
return { ok: false, error: `missing ~/.grok/config.toml; ${APPLY_HINT}` };
|
|
44
|
+
}
|
|
45
|
+
return { ok: false, error: `${homeParsed.error}; ${APPLY_HINT}` };
|
|
46
|
+
}
|
|
47
|
+
const agentName = homeParsed.value.agent && homeParsed.value.agent.name;
|
|
48
|
+
const compaction =
|
|
49
|
+
homeParsed.value.features && homeParsed.value.features.two_pass_compaction;
|
|
50
|
+
if (agentName !== "orchestrator" || compaction !== true) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
error: `home [agent].name must be orchestrator and [features].two_pass_compaction must be true; ${APPLY_HINT}`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const projectConfig = path.join(ws, PROJECT_REL);
|
|
58
|
+
const projectParsed = parseTomlFile(projectConfig, "project .grok/config.toml");
|
|
59
|
+
if (!projectParsed.ok) {
|
|
60
|
+
if (projectParsed.missing) {
|
|
61
|
+
return { ok: false, error: "missing project .grok/config.toml" };
|
|
62
|
+
}
|
|
63
|
+
return { ok: false, error: projectParsed.error };
|
|
64
|
+
}
|
|
65
|
+
if (
|
|
66
|
+
Object.prototype.hasOwnProperty.call(projectParsed.value, "features") ||
|
|
67
|
+
Object.prototype.hasOwnProperty.call(projectParsed.value, "agent")
|
|
68
|
+
) {
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
error: "project .grok/config.toml must not set [features] or [agent]",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return { ok: true };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function main() {
|
|
78
|
+
try {
|
|
79
|
+
const result = checkGlobals({
|
|
80
|
+
home: process.env.HOME,
|
|
81
|
+
workspaceRoot: process.cwd(),
|
|
82
|
+
});
|
|
83
|
+
if (!result.ok) {
|
|
84
|
+
process.stderr.write((result.error || "check-globals failed") + "\n");
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
return 0;
|
|
88
|
+
} catch (err) {
|
|
89
|
+
process.stderr.write(String(err && err.message ? err.message : err) + "\n");
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
95
|
+
const invoked = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
|
96
|
+
if (invoked === thisFile || import.meta.url === pathToFileURL(invoked).href) {
|
|
97
|
+
process.exit(main());
|
|
98
|
+
}
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Emit host MCP deny-default artifacts from .rulesync/mcp-policy.jsonc. */
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
7
|
+
|
|
8
|
+
export const POLICY_REL = ".rulesync/mcp-policy.jsonc";
|
|
9
|
+
const GROK_REL = ".grok/config.toml";
|
|
10
|
+
const CLAUDE_SETTINGS_REL = ".claude/settings.json";
|
|
11
|
+
const MCP_JSON_REL = ".mcp.json";
|
|
12
|
+
const CODEX_REL = ".codex/config.toml";
|
|
13
|
+
const AGENTS_MCP_REL = ".agents/mcp_config.json";
|
|
14
|
+
const AGENT_IDS = ["orchestrator", "implementer", "thinker", "grunt"];
|
|
15
|
+
const MCP_DENY = "mcp__*";
|
|
16
|
+
const GROK_HEADER =
|
|
17
|
+
"# Generated by emit-mcp-policy.mjs from .rulesync/mcp-policy.jsonc.\n";
|
|
18
|
+
|
|
19
|
+
function isPlainObject(v) {
|
|
20
|
+
return v != null && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function stripJsonc(text) {
|
|
24
|
+
return String(text ?? "")
|
|
25
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
26
|
+
.replace(/\/\/.*$/gm, "");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function parseArgv(argv) {
|
|
30
|
+
const args = Array.isArray(argv) ? argv : [];
|
|
31
|
+
let check = false;
|
|
32
|
+
for (const a of args) {
|
|
33
|
+
if (a === "--check") {
|
|
34
|
+
check = true;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
return { ok: false, error: `unknown flag: ${a}` };
|
|
38
|
+
}
|
|
39
|
+
return { ok: true, check };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function deepEqual(a, b) {
|
|
43
|
+
if (Object.is(a, b)) return true;
|
|
44
|
+
if (typeof a !== typeof b) return false;
|
|
45
|
+
if (a == null || b == null) return a === b;
|
|
46
|
+
if (Array.isArray(a)) {
|
|
47
|
+
if (!Array.isArray(b) || a.length !== b.length) return false;
|
|
48
|
+
return a.every((v, i) => deepEqual(v, b[i]));
|
|
49
|
+
}
|
|
50
|
+
if (typeof a === "object") {
|
|
51
|
+
if (!isPlainObject(a) || !isPlainObject(b)) return false;
|
|
52
|
+
const ak = Object.keys(a);
|
|
53
|
+
const bk = Object.keys(b);
|
|
54
|
+
if (ak.length !== bk.length) return false;
|
|
55
|
+
return ak.every((k) => Object.prototype.hasOwnProperty.call(b, k) && deepEqual(a[k], b[k]));
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** desired keys must be present on disk; extra disk keys allowed. */
|
|
61
|
+
export function requiredDeepMatch(disk, desired) {
|
|
62
|
+
if (Array.isArray(desired)) return deepEqual(disk, desired);
|
|
63
|
+
if (desired != null && typeof desired === "object") {
|
|
64
|
+
if (!isPlainObject(disk)) return false;
|
|
65
|
+
for (const [k, v] of Object.entries(desired)) {
|
|
66
|
+
if (!requiredDeepMatch(disk[k], v)) return false;
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return deepEqual(disk, desired);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readText(abs) {
|
|
74
|
+
try {
|
|
75
|
+
return fs.readFileSync(abs, "utf8");
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (err && err.code === "ENOENT") return null;
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseJsonFile(raw, fallback) {
|
|
83
|
+
if (raw == null || !String(raw).trim()) return fallback;
|
|
84
|
+
const v = JSON.parse(raw);
|
|
85
|
+
return isPlainObject(v) ? v : fallback;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseTomlFile(raw, fallback) {
|
|
89
|
+
if (raw == null || !String(raw).trim()) return fallback;
|
|
90
|
+
const v = parseToml(String(raw));
|
|
91
|
+
return isPlainObject(v) ? v : fallback;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function jsonOut(obj) {
|
|
95
|
+
return JSON.stringify(obj, null, 2) + "\n";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function tomlOut(obj) {
|
|
99
|
+
const body = stringifyToml(obj);
|
|
100
|
+
const text = body.endsWith("\n") ? body : body + "\n";
|
|
101
|
+
return GROK_HEADER + text;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function cloneJson(v) {
|
|
105
|
+
return v == null ? v : JSON.parse(JSON.stringify(v));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function pickServers(servers, allow) {
|
|
109
|
+
const src = isPlainObject(servers) ? servers : {};
|
|
110
|
+
const out = {};
|
|
111
|
+
for (const name of allow) {
|
|
112
|
+
if (Object.prototype.hasOwnProperty.call(src, name)) {
|
|
113
|
+
out[name] = cloneJson(src[name]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function loadPolicy(workspaceRoot) {
|
|
120
|
+
const ws = workspaceRoot || process.cwd();
|
|
121
|
+
const p = path.join(ws, POLICY_REL);
|
|
122
|
+
const raw = fs.readFileSync(p, "utf8");
|
|
123
|
+
const policy = JSON.parse(stripJsonc(raw));
|
|
124
|
+
if (!isPlainObject(policy)) {
|
|
125
|
+
throw new Error("invalid policy");
|
|
126
|
+
}
|
|
127
|
+
return policy;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function validatePolicy(policy) {
|
|
131
|
+
if (!isPlainObject(policy)) return { ok: false, error: "invalid policy" };
|
|
132
|
+
if (policy.default !== "deny") {
|
|
133
|
+
return { ok: false, error: 'default must be "deny"' };
|
|
134
|
+
}
|
|
135
|
+
if (!Array.isArray(policy.allow)) {
|
|
136
|
+
return { ok: false, error: "allow must be an array" };
|
|
137
|
+
}
|
|
138
|
+
for (const name of policy.allow) {
|
|
139
|
+
if (typeof name !== "string" || !name) {
|
|
140
|
+
return { ok: false, error: "allow names must be non-empty strings" };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const servers = policy.servers;
|
|
144
|
+
if (servers != null && !isPlainObject(servers)) {
|
|
145
|
+
return { ok: false, error: "servers must be an object" };
|
|
146
|
+
}
|
|
147
|
+
const serverKeys = isPlainObject(servers) ? Object.keys(servers) : [];
|
|
148
|
+
if (serverKeys.length > 0 && policy.allow.length > 0) {
|
|
149
|
+
const missing = policy.allow.filter((n) => !Object.prototype.hasOwnProperty.call(servers, n));
|
|
150
|
+
if (missing.length) {
|
|
151
|
+
return { ok: false, error: `allow names missing from servers: ${missing.join(", ")}` };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (policy.agents != null) {
|
|
155
|
+
if (!isPlainObject(policy.agents)) {
|
|
156
|
+
return { ok: false, error: "agents must be an object" };
|
|
157
|
+
}
|
|
158
|
+
for (const id of AGENT_IDS) {
|
|
159
|
+
const row = policy.agents[id];
|
|
160
|
+
if (row != null && (!isPlainObject(row) || row.inherit !== false)) {
|
|
161
|
+
return { ok: false, error: `agents.${id}.inherit must be false` };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { ok: true };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function stringList(v) {
|
|
169
|
+
return Array.isArray(v) ? v.filter((s) => typeof s === "string") : [];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isMcpToolRule(s) {
|
|
173
|
+
return s === "MCPTool" || s.startsWith("MCPTool(");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function desiredGrok(disk, policy) {
|
|
177
|
+
const obj = isPlainObject(disk) ? { ...disk } : {};
|
|
178
|
+
delete obj.features;
|
|
179
|
+
delete obj.permissions;
|
|
180
|
+
|
|
181
|
+
const plugins = isPlainObject(obj.plugins) ? { ...obj.plugins } : {};
|
|
182
|
+
delete plugins.deny_default;
|
|
183
|
+
if (policy.plugins && policy.plugins.denyDefault === true) {
|
|
184
|
+
plugins.enabled = [];
|
|
185
|
+
obj.plugins = plugins;
|
|
186
|
+
} else if (Object.keys(plugins).length) {
|
|
187
|
+
obj.plugins = plugins;
|
|
188
|
+
} else {
|
|
189
|
+
delete obj.plugins;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const allow = Array.isArray(policy.allow) ? policy.allow : [];
|
|
193
|
+
const perm = isPlainObject(obj.permission) ? { ...obj.permission } : {};
|
|
194
|
+
const denyKeep = stringList(perm.deny).filter((s) => s !== "MCPTool");
|
|
195
|
+
perm.deny = [...denyKeep, "MCPTool"];
|
|
196
|
+
const allowKeep = stringList(perm.allow).filter((s) => !isMcpToolRule(s));
|
|
197
|
+
const mcpAllows = allow.map((name) => `MCPTool(${name}__*)`);
|
|
198
|
+
const nextAllow = [...mcpAllows, ...allowKeep];
|
|
199
|
+
if (nextAllow.length) perm.allow = nextAllow;
|
|
200
|
+
else delete perm.allow;
|
|
201
|
+
obj.permission = perm;
|
|
202
|
+
|
|
203
|
+
if (isPlainObject(obj.mcp)) {
|
|
204
|
+
const mcp = { ...obj.mcp };
|
|
205
|
+
delete mcp.servers;
|
|
206
|
+
if (Object.keys(mcp).length) obj.mcp = mcp;
|
|
207
|
+
else delete obj.mcp;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const allowSet = new Set(allow);
|
|
211
|
+
const native = isPlainObject(obj.mcp_servers) ? { ...obj.mcp_servers } : {};
|
|
212
|
+
const nested =
|
|
213
|
+
isPlainObject(disk) && isPlainObject(disk.mcp) && isPlainObject(disk.mcp.servers) ? disk.mcp.servers : {};
|
|
214
|
+
const leftoverNames = new Set([...Object.keys(native), ...Object.keys(nested)]);
|
|
215
|
+
const servers = {};
|
|
216
|
+
for (const name of leftoverNames) {
|
|
217
|
+
const prev = isPlainObject(native[name])
|
|
218
|
+
? native[name]
|
|
219
|
+
: isPlainObject(nested[name])
|
|
220
|
+
? nested[name]
|
|
221
|
+
: {};
|
|
222
|
+
servers[name] = allowSet.has(name) ? { ...prev } : { ...prev, enabled: false };
|
|
223
|
+
}
|
|
224
|
+
const picked = pickServers(policy.servers, allow);
|
|
225
|
+
for (const [name, spec] of Object.entries(picked)) {
|
|
226
|
+
servers[name] = isPlainObject(spec) ? { ...spec } : spec;
|
|
227
|
+
}
|
|
228
|
+
if (Object.keys(servers).length === 0) {
|
|
229
|
+
delete obj.mcp_servers;
|
|
230
|
+
} else {
|
|
231
|
+
obj.mcp_servers = servers;
|
|
232
|
+
}
|
|
233
|
+
return obj;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function desiredClaudeSettings(disk, policy) {
|
|
237
|
+
const obj = isPlainObject(disk) ? { ...disk } : {};
|
|
238
|
+
obj.enableAllProjectMcpServers = false;
|
|
239
|
+
obj.enabledMcpjsonServers = [...policy.allow];
|
|
240
|
+
const permissions = isPlainObject(obj.permissions) ? { ...obj.permissions } : {};
|
|
241
|
+
const deny = Array.isArray(permissions.deny) ? [...permissions.deny] : [];
|
|
242
|
+
if (!deny.includes(MCP_DENY)) deny.push(MCP_DENY);
|
|
243
|
+
permissions.deny = deny;
|
|
244
|
+
obj.permissions = permissions;
|
|
245
|
+
return obj;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function desiredMcpJson(policy) {
|
|
249
|
+
return { mcpServers: pickServers(policy.servers, policy.allow) };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function desiredCodex(disk, policy) {
|
|
253
|
+
const obj = isPlainObject(disk) ? { ...disk } : {};
|
|
254
|
+
if (!isPlainObject(obj.agents)) {
|
|
255
|
+
obj.agents = { max_depth: 1 };
|
|
256
|
+
} else {
|
|
257
|
+
obj.agents = { ...obj.agents };
|
|
258
|
+
if (obj.agents.max_depth == null) obj.agents.max_depth = 1;
|
|
259
|
+
}
|
|
260
|
+
if (!policy.allow.length) {
|
|
261
|
+
delete obj.mcp_servers;
|
|
262
|
+
} else {
|
|
263
|
+
obj.mcp_servers = pickServers(policy.servers, policy.allow);
|
|
264
|
+
}
|
|
265
|
+
return obj;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function desiredAgentsMcp(policy) {
|
|
269
|
+
return { mcpServers: pickServers(policy.servers, policy.allow) };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function grokCheckOk(disk, desired, policy) {
|
|
273
|
+
if (!isPlainObject(disk)) return false;
|
|
274
|
+
if (Object.prototype.hasOwnProperty.call(disk, "features")) return false;
|
|
275
|
+
if (Array.isArray(disk.permissions)) return false;
|
|
276
|
+
if (isPlainObject(disk.plugins) && Object.prototype.hasOwnProperty.call(disk.plugins, "deny_default")) {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
if (isPlainObject(disk.mcp) && Object.prototype.hasOwnProperty.call(disk.mcp, "servers")) {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
const deny = isPlainObject(disk.permission) && Array.isArray(disk.permission.deny)
|
|
283
|
+
? disk.permission.deny
|
|
284
|
+
: [];
|
|
285
|
+
if (!deny.includes("MCPTool")) return false;
|
|
286
|
+
const allow = isPlainObject(disk.permission) && Array.isArray(disk.permission.allow)
|
|
287
|
+
? disk.permission.allow
|
|
288
|
+
: [];
|
|
289
|
+
for (const name of policy.allow) {
|
|
290
|
+
if (!allow.includes(`MCPTool(${name}__*)`)) return false;
|
|
291
|
+
}
|
|
292
|
+
if (policy.plugins && policy.plugins.denyDefault === true) {
|
|
293
|
+
if (!requiredDeepMatch(disk, { plugins: { enabled: [] } })) return false;
|
|
294
|
+
}
|
|
295
|
+
if (desired.mcp_servers) {
|
|
296
|
+
if (!requiredDeepMatch(disk, { mcp_servers: desired.mcp_servers })) return false;
|
|
297
|
+
} else if (isPlainObject(disk.mcp_servers) && Object.keys(disk.mcp_servers).length > 0) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function claudeCheckOk(disk, desired) {
|
|
304
|
+
if (!isPlainObject(disk)) return false;
|
|
305
|
+
if (disk.enableAllProjectMcpServers !== false) return false;
|
|
306
|
+
if (!deepEqual(disk.enabledMcpjsonServers, desired.enabledMcpjsonServers)) return false;
|
|
307
|
+
const deny = disk.permissions && Array.isArray(disk.permissions.deny) ? disk.permissions.deny : [];
|
|
308
|
+
return deny.includes(MCP_DENY);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function writeOrCheck({ check, abs, nextText, diskRaw }) {
|
|
312
|
+
if (check) {
|
|
313
|
+
return diskRaw === nextText;
|
|
314
|
+
}
|
|
315
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
316
|
+
fs.writeFileSync(abs, nextText);
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function emitMcpPolicy({ workspaceRoot, check = false } = {}) {
|
|
321
|
+
const ws = workspaceRoot || process.cwd();
|
|
322
|
+
const policy = loadPolicy(ws);
|
|
323
|
+
const valid = validatePolicy(policy);
|
|
324
|
+
if (!valid.ok) {
|
|
325
|
+
return { ok: false, check, error: valid.error };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const grokAbs = path.join(ws, GROK_REL);
|
|
329
|
+
const claudeAbs = path.join(ws, CLAUDE_SETTINGS_REL);
|
|
330
|
+
const mcpAbs = path.join(ws, MCP_JSON_REL);
|
|
331
|
+
const codexAbs = path.join(ws, CODEX_REL);
|
|
332
|
+
const agentsAbs = path.join(ws, AGENTS_MCP_REL);
|
|
333
|
+
|
|
334
|
+
const grokRaw = readText(grokAbs);
|
|
335
|
+
const claudeRaw = readText(claudeAbs);
|
|
336
|
+
const mcpRaw = readText(mcpAbs);
|
|
337
|
+
const codexRaw = readText(codexAbs);
|
|
338
|
+
const agentsRaw = readText(agentsAbs);
|
|
339
|
+
|
|
340
|
+
const grokDisk = parseTomlFile(grokRaw, {});
|
|
341
|
+
const claudeDisk = parseJsonFile(claudeRaw, {});
|
|
342
|
+
const mcpDisk = mcpRaw == null ? null : parseJsonFile(mcpRaw, {});
|
|
343
|
+
const codexDisk = parseTomlFile(codexRaw, {});
|
|
344
|
+
const agentsDisk = agentsRaw == null ? null : parseJsonFile(agentsRaw, {});
|
|
345
|
+
|
|
346
|
+
const grokDesired = desiredGrok(grokDisk, policy);
|
|
347
|
+
const claudeDesired = desiredClaudeSettings(claudeDisk, policy);
|
|
348
|
+
const mcpDesired = desiredMcpJson(policy);
|
|
349
|
+
const codexDesired = desiredCodex(codexDisk, policy);
|
|
350
|
+
const agentsDesired = desiredAgentsMcp(policy);
|
|
351
|
+
|
|
352
|
+
if (check) {
|
|
353
|
+
const drift = [];
|
|
354
|
+
if (grokRaw == null || !grokCheckOk(grokDisk, grokDesired, policy)) drift.push(GROK_REL);
|
|
355
|
+
if (claudeRaw == null || !claudeCheckOk(claudeDisk, claudeDesired)) drift.push(CLAUDE_SETTINGS_REL);
|
|
356
|
+
if (mcpDisk == null || !deepEqual(mcpDisk, mcpDesired)) drift.push(MCP_JSON_REL);
|
|
357
|
+
const codexOk =
|
|
358
|
+
codexRaw != null &&
|
|
359
|
+
requiredDeepMatch(codexDisk, {
|
|
360
|
+
agents: { max_depth: codexDesired.agents.max_depth },
|
|
361
|
+
}) &&
|
|
362
|
+
(policy.allow.length
|
|
363
|
+
? deepEqual(codexDisk.mcp_servers, codexDesired.mcp_servers)
|
|
364
|
+
: !Object.prototype.hasOwnProperty.call(codexDisk, "mcp_servers"));
|
|
365
|
+
if (!codexOk) drift.push(CODEX_REL);
|
|
366
|
+
if (agentsDisk == null || !deepEqual(agentsDisk, agentsDesired)) drift.push(AGENTS_MCP_REL);
|
|
367
|
+
if (drift.length) {
|
|
368
|
+
return { ok: false, check: true, error: `drift: ${drift.join(", ")}`, drift };
|
|
369
|
+
}
|
|
370
|
+
return { ok: true, check: true, drift: [] };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
writeOrCheck({
|
|
374
|
+
check: false,
|
|
375
|
+
abs: grokAbs,
|
|
376
|
+
nextText: tomlOut(grokDesired),
|
|
377
|
+
diskRaw: grokRaw,
|
|
378
|
+
});
|
|
379
|
+
writeOrCheck({
|
|
380
|
+
check: false,
|
|
381
|
+
abs: claudeAbs,
|
|
382
|
+
nextText: jsonOut(claudeDesired),
|
|
383
|
+
diskRaw: claudeRaw,
|
|
384
|
+
});
|
|
385
|
+
writeOrCheck({
|
|
386
|
+
check: false,
|
|
387
|
+
abs: mcpAbs,
|
|
388
|
+
nextText: jsonOut(mcpDesired),
|
|
389
|
+
diskRaw: mcpRaw,
|
|
390
|
+
});
|
|
391
|
+
const codexBody = stringifyToml(codexDesired);
|
|
392
|
+
const codexText = codexBody.endsWith("\n") ? codexBody : codexBody + "\n";
|
|
393
|
+
writeOrCheck({
|
|
394
|
+
check: false,
|
|
395
|
+
abs: codexAbs,
|
|
396
|
+
nextText: codexText,
|
|
397
|
+
diskRaw: codexRaw,
|
|
398
|
+
});
|
|
399
|
+
writeOrCheck({
|
|
400
|
+
check: false,
|
|
401
|
+
abs: agentsAbs,
|
|
402
|
+
nextText: jsonOut(agentsDesired),
|
|
403
|
+
diskRaw: agentsRaw,
|
|
404
|
+
});
|
|
405
|
+
return { ok: true, check: false };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function main() {
|
|
409
|
+
try {
|
|
410
|
+
const parsed = parseArgv(process.argv.slice(2));
|
|
411
|
+
if (!parsed.ok) {
|
|
412
|
+
process.stderr.write((parsed.error || "invalid argv") + "\n");
|
|
413
|
+
return 1;
|
|
414
|
+
}
|
|
415
|
+
const result = emitMcpPolicy({
|
|
416
|
+
workspaceRoot: process.cwd(),
|
|
417
|
+
check: parsed.check,
|
|
418
|
+
});
|
|
419
|
+
if (!result.ok) {
|
|
420
|
+
process.stderr.write((result.error || "emit failed") + "\n");
|
|
421
|
+
return 1;
|
|
422
|
+
}
|
|
423
|
+
return 0;
|
|
424
|
+
} catch (err) {
|
|
425
|
+
process.stderr.write(String(err && err.message ? err.message : err) + "\n");
|
|
426
|
+
return 1;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
431
|
+
const invoked = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
|
432
|
+
if (invoked === thisFile || import.meta.url === pathToFileURL(invoked).href) {
|
|
433
|
+
process.exit(main());
|
|
434
|
+
}
|