@coderifts/agent-hooks 0.3.2 → 0.3.3
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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +9 -0
- package/claude-code/marketplace-entry.json +1 -1
- package/contract-write.mjs +61 -4
- package/gate.js +11 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.3 — 2026-10-08
|
|
4
|
+
### Fixed
|
|
5
|
+
- **A plain `mcp.json` is decided by its content (contract-write 1.3.0, P65c).** 0.3.2 skipped every `mcp.json` by
|
|
6
|
+
name, which also skipped a server's tool manifest of that name. By name only `.mcp.json`, `.cursor/mcp.json`,
|
|
7
|
+
`.vscode/mcp.json`, `claude_desktop_config.json` and `(cline_)mcp_settings.json` stay skipped (never read). Any
|
|
8
|
+
other `mcp.json` is read on disk: `mcpServers` / `servers` and no `tools` → a client configuration, nothing sent;
|
|
9
|
+
`tools`, any other shape, or unparseable → checked as a contract. A manifest rewritten into a client configuration
|
|
10
|
+
is checked as the manifest removed, without the client side. The same on the OpenClaw tool shapes (`write_file` …).
|
|
11
|
+
|
|
3
12
|
## 0.3.2 — 2026-10-06
|
|
4
13
|
### Fixed
|
|
5
14
|
- **An MCP client configuration is never treated as a contract (contract-write 1.2.0, P65).** `.mcp.json`, `mcp.json`
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"repo": "coderifts/agent-hooks"
|
|
6
6
|
},
|
|
7
7
|
"description": "Host adapters for CodeRifts: Claude Code PreToolUse asks before Write/Edit of OpenAPI, AsyncAPI, GraphQL, protobuf, or MCP manifest files. Not the GitHub Action (coderifts/contract-gate) and not the guard library (@coderifts/agent-guard).",
|
|
8
|
-
"version": "0.3.
|
|
8
|
+
"version": "0.3.3",
|
|
9
9
|
"author": {
|
|
10
10
|
"name": "CodeRifts",
|
|
11
11
|
"email": "peter@coderifts.com"
|
package/contract-write.mjs
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
// written by scripts/generate-contract-write-copies.js, byte for byte (the CommonJS twin is a
|
|
28
28
|
// mechanical transform), and its --check fails on any difference. Do not edit a copy.
|
|
29
29
|
|
|
30
|
-
export const CONTRACT_WRITE_VERSION = '1.
|
|
30
|
+
export const CONTRACT_WRITE_VERSION = '1.3.0';
|
|
31
31
|
|
|
32
32
|
/** What a shell write to a named contract file gets, and what an unnamed one gets. */
|
|
33
33
|
export const SHELL_NAMED_DECISION = 'refuse';
|
|
@@ -45,12 +45,47 @@ export const CONTRACT_EXT = /\.(ya?ml|json|graphql|gql|proto)$/i;
|
|
|
45
45
|
* 1.2.0 (2026-10-06, the Claude directory's hold MCP_FORWARDS_CREDENTIAL_ENV): an MCP CLIENT
|
|
46
46
|
* configuration file — the list of servers a client starts, with their `env` credentials — is not a
|
|
47
47
|
* contract. `.mcp.json` and `mcp.json` matched the list above (".json" + "mcp") as mcp_manifest, so an
|
|
48
|
-
* Edit of one sent its whole text, tokens included, to preflight.
|
|
49
|
-
* read to decide; it wins over the project's own `schema:` list too. MCP tool manifests are unchanged.
|
|
48
|
+
* Edit of one sent its whole text, tokens included, to preflight. MCP tool manifests are unchanged.
|
|
50
49
|
* P65 (2026-10-06): the required check uses this same pattern — index.cjs requires it from here, and its
|
|
51
50
|
* looksLikeContractPath says no first, word for word as below.
|
|
51
|
+
*
|
|
52
|
+
* 1.3.0 (P65c, 2026-10-07): by NAME only the names that are a client configuration and nothing else —
|
|
53
|
+
* `.mcp.json` anywhere, `.cursor/mcp.json`, `.vscode/mcp.json`, `claude_desktop_config.json`,
|
|
54
|
+
* `(cline_)mcp_settings.json`. Those are never read, and they win over the project's own `schema:` list.
|
|
55
|
+
* Any other `mcp.json` (MCP_JSON_BY_CONTENT) is also the name a server's tool manifest carries
|
|
56
|
+
* (coderifts.com's own), so it is a candidate, read where it already is, and decided by mcpJsonKind:
|
|
57
|
+
* the hooks read it on disk and never send a client configuration; the required check decides after its
|
|
58
|
+
* own read (isClientConfigContent), and a client-configuration side counts as no file at all.
|
|
52
59
|
*/
|
|
53
|
-
export const MCP_CLIENT_CONFIG = /(^|\/)(
|
|
60
|
+
export const MCP_CLIENT_CONFIG = /(^|\/)(\.mcp\.json|\.cursor\/mcp\.json|\.vscode\/mcp\.json|claude_desktop_config\.json|(cline_)?mcp_settings\.json)$/i;
|
|
61
|
+
|
|
62
|
+
/** A plain `mcp.json` that MCP_CLIENT_CONFIG does not take by name: decided by its content. */
|
|
63
|
+
export const MCP_JSON_BY_CONTENT = /(^|\/)mcp\.json$/i;
|
|
64
|
+
|
|
65
|
+
const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 'client_config' when the text is a JSON object with `mcpServers` or `servers` (an object) and no
|
|
69
|
+
* `tools`; otherwise 'contract' — a `tools` key, any other shape, and text that does not parse
|
|
70
|
+
* (fail-closed: what cannot be read as a client configuration is checked as a contract).
|
|
71
|
+
*/
|
|
72
|
+
export function mcpJsonKind(text) {
|
|
73
|
+
let doc;
|
|
74
|
+
try {
|
|
75
|
+
doc = JSON.parse(String(text ?? '').replace(/^\uFEFF/, ''));
|
|
76
|
+
} catch {
|
|
77
|
+
return 'contract';
|
|
78
|
+
}
|
|
79
|
+
if (!isObject(doc) || 'tools' in doc) return 'contract';
|
|
80
|
+
return isObject(doc.mcpServers) || isObject(doc.servers) ? 'client_config' : 'contract';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** True when `path` is decided by content and `text` (one side, present) is a client configuration. */
|
|
84
|
+
export function isClientConfigContent(path, text) {
|
|
85
|
+
if (typeof text !== 'string' || text === '') return false;
|
|
86
|
+
const rel = normalizePath(path);
|
|
87
|
+
return MCP_JSON_BY_CONTENT.test(rel) && !MCP_CLIENT_CONFIG.test(rel) && mcpJsonKind(text) === 'client_config';
|
|
88
|
+
}
|
|
54
89
|
|
|
55
90
|
export function looksLikeContractPath(p) {
|
|
56
91
|
const s = String(p || '').toLowerCase();
|
|
@@ -689,9 +724,30 @@ export async function decideToolCall(call, io = {}) {
|
|
|
689
724
|
if (after === null) {
|
|
690
725
|
return { action: 'refuse', reason: 'edit_does_not_apply', rel, type, why: `the ${kind} does not apply to ${rel} as it is on disk (${afterFailure(kind, input, before)}), so there is no after text to check` };
|
|
691
726
|
}
|
|
727
|
+
// 1.3.0 (P65c): a plain mcp.json is decided by content, here, before anything is sent. A side that is
|
|
728
|
+
// a client configuration counts as no file: both such → pass; one → the manifest added or removed.
|
|
729
|
+
const clientBefore = isClientConfigContent(rel, before);
|
|
730
|
+
const clientAfter = isClientConfigContent(rel, after);
|
|
731
|
+
if (clientBefore || clientAfter) {
|
|
732
|
+
const b = clientBefore ? '' : before;
|
|
733
|
+
const a = clientAfter ? '' : after;
|
|
734
|
+
if (b === '' && a === '') return { action: 'pass', reason: 'mcp_client_config', rel };
|
|
735
|
+
return { action: 'gate', path: filePath, rel, type, before: b, after: a };
|
|
736
|
+
}
|
|
692
737
|
return { action: 'gate', path: filePath, rel, type, before, after };
|
|
693
738
|
}
|
|
694
739
|
|
|
740
|
+
/** A shell write to a plain mcp.json that is a client configuration on disk is not a contract write. */
|
|
741
|
+
async function clientConfigOnDisk(rel, io) {
|
|
742
|
+
if (!MCP_JSON_BY_CONTENT.test(rel) || !io.readFile) return false;
|
|
743
|
+
const root = normalizePath(io.cwd || '');
|
|
744
|
+
try {
|
|
745
|
+
return isClientConfigContent(rel, await io.readFile(root ? `${root}/${rel}` : rel));
|
|
746
|
+
} catch {
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
695
751
|
/** Is there a contract file under `scope`? Unknown (no reader, a limit hit) counts as yes. */
|
|
696
752
|
async function mayHoldContract(scope, io, opts) {
|
|
697
753
|
const glob = /[*?]/.test(scope) ? globToRegExp(normalizePath(scope)) : null;
|
|
@@ -719,6 +775,7 @@ async function decideShell(command, io, opts) {
|
|
|
719
775
|
const inTree = scopeInTree(target, io);
|
|
720
776
|
if (inTree === null) continue;
|
|
721
777
|
const type = inTree !== UNKNOWN && !/[*?]/.test(inTree) ? contractType(inTree, opts) : null;
|
|
778
|
+
if (type && await clientConfigOnDisk(inTree, io)) continue;
|
|
722
779
|
if (type) named.push({ path: target, type, by });
|
|
723
780
|
else scopes.set(inTree, [...(scopes.get(inTree) || []), by]);
|
|
724
781
|
}
|
package/gate.js
CHANGED
|
@@ -11,7 +11,7 @@ import { isAbsolute, resolve } from "node:path";
|
|
|
11
11
|
// command writes. A byte copy of @coderifts/contract-path's contract-write.mjs, written by the app's
|
|
12
12
|
// scripts/generate-contract-write-copies.js; contract-write.sha256 beside it is checked by
|
|
13
13
|
// test/contract-write-copy.test.js.
|
|
14
|
-
import { contractType, decideToolCall, toolKind } from "./contract-write.mjs";
|
|
14
|
+
import { contractType, decideToolCall, isClientConfigContent, toolKind } from "./contract-write.mjs";
|
|
15
15
|
|
|
16
16
|
/** Tools whose params carry a path and a new file body. */
|
|
17
17
|
export const DEFAULT_TOOL_SHAPES = Object.freeze({
|
|
@@ -324,12 +324,21 @@ export function createGate(config = {}, deps = {}) {
|
|
|
324
324
|
);
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
+
// 0.3.3 (P65c): a plain mcp.json is decided by its content (contract-write's function), as on the
|
|
328
|
+
// Claude Code path. A side that is an MCP client configuration counts as no file and is never sent;
|
|
329
|
+
// both such sides → this gate never claimed the call.
|
|
330
|
+
const clientBefore = isClientConfigContent(target.path, before);
|
|
331
|
+
const clientAfter = isClientConfigContent(target.path, after);
|
|
332
|
+
const sentBefore = clientBefore ? "" : before;
|
|
333
|
+
const sentAfter = clientAfter ? "" : after;
|
|
334
|
+
if ((clientBefore || clientAfter) && sentBefore === "" && sentAfter === "") return undefined;
|
|
335
|
+
|
|
327
336
|
const outcome = await call({
|
|
328
337
|
endpoint,
|
|
329
338
|
apiKey,
|
|
330
339
|
timeoutMs,
|
|
331
340
|
operation,
|
|
332
|
-
artifact: { id: target.path, type: target.type, before, after },
|
|
341
|
+
artifact: { id: target.path, type: target.type, before: sentBefore, after: sentAfter },
|
|
333
342
|
});
|
|
334
343
|
return decide(outcome, { operation, path: target.path });
|
|
335
344
|
};
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coderifts/agent-hooks",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Ask CodeRifts before an agent writes a contract artifact (OpenClaw before_tool_call and Claude Code PreToolUse).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/coderifts/agent-hooks#readme",
|