Skip to content

Commit 7d9a72a

Browse files
committed
fix: address 8 PR review comments
1. approval.ts: mkdir ~/.hyperagent/ before writing (first-run case) 2. config.ts: hash includes allowTools, denyTools, env key names 3. approval.ts: isMCPApproved checks tool list matches approved tools 4. plugin-adapter.ts: quote property names that aren't valid JS identifiers 5. plugins/mcp/index.ts: fix comment to match implementation (sentinel module) 6. index.ts: module_info author fixed to 'system' (was 'mcp') 7. validator.rs: arrow handler param check distinguishes () => from (event) => 8. validator.rs: strip_js_comments guards against regex literal // sequences Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
1 parent b9521d9 commit 7d9a72a

7 files changed

Lines changed: 111 additions & 19 deletions

File tree

plugins/mcp/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,12 @@ Note: MCP servers are OS processes (not micro-VM sandboxed).
5656
// ── Host functions ───────────────────────────────────────────────────
5757

5858
/**
59-
* The MCP gateway plugin provides no host functions.
60-
* Its hostModules array is empty — it exists solely to gate the
61-
* MCP subsystem via the plugin approval flow.
59+
* The MCP gateway plugin provides a single sentinel host module,
60+
* `mcp-gateway`, used to signal that the MCP subsystem is enabled
61+
* via the plugin approval flow.
6262
*
6363
* Individual MCP servers register their own host modules dynamically
64-
* when enabled via /mcp enable.
64+
* (`host:mcp-<name>`) when enabled via /mcp enable.
6565
*/
6666
export function createHostFunctions(
6767
_config?: MCPGatewayConfig,

src/agent/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3902,7 +3902,7 @@ const moduleInfoTool = defineTool("module_info", {
39023902
return {
39033903
name: `mcp-${serverName}`,
39043904
description: `MCP server "${serverName}" — ${conn.tools.length} tool(s)`,
3905-
author: "mcp",
3905+
author: "system",
39063906
importAs: `import { ... } from "host:mcp-${serverName}"`,
39073907
exports: conn.tools.map((t) => ({
39083908
name: t.name,

src/agent/mcp/approval.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
// before a server can be connected. Approval is invalidated if the
55
// server config (command + args) changes.
66

7-
import { readFileSync, writeFileSync, existsSync } from "node:fs";
8-
import { join } from "node:path";
7+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
8+
import { join, dirname } from "node:path";
99
import { homedir } from "node:os";
1010

1111
import type {
@@ -36,6 +36,11 @@ export function loadMCPApprovalStore(): MCPApprovalStore {
3636
*/
3737
function saveMCPApprovalStore(store: MCPApprovalStore): void {
3838
try {
39+
// Ensure directory exists (first-run case)
40+
const dir = dirname(APPROVAL_FILE);
41+
if (!existsSync(dir)) {
42+
mkdirSync(dir, { recursive: true, mode: 0o700 });
43+
}
3944
writeFileSync(APPROVAL_FILE, JSON.stringify(store, null, 2), {
4045
mode: 0o600,
4146
});
@@ -53,12 +58,27 @@ export function isMCPApproved(
5358
name: string,
5459
config: MCPServerConfig,
5560
store: MCPApprovalStore,
61+
currentTools?: string[],
5662
): boolean {
5763
const record = store[name];
5864
if (!record) return false;
5965

6066
const currentHash = computeMCPConfigHash(name, config);
61-
return record.configHash === currentHash;
67+
if (record.configHash !== currentHash) return false;
68+
69+
// If tool list is provided, verify it matches approval-time tools
70+
if (currentTools && Array.isArray(record.approvedTools)) {
71+
const approved = [...record.approvedTools].sort();
72+
const current = [...currentTools].sort();
73+
if (
74+
approved.length !== current.length ||
75+
!approved.every((t, i) => t === current[i])
76+
) {
77+
return false; // Tool set changed — re-approval required
78+
}
79+
}
80+
81+
return true;
6282
}
6383

6484
/**

src/agent/mcp/config.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -223,16 +223,22 @@ function substituteEnvVars(value: string): string {
223223

224224
/**
225225
* Compute a config hash for approval validation.
226-
* Hash = SHA-256(name + command + JSON.stringify(args)).
227-
* Config changes invalidate the approval.
226+
* Includes name, command, args, tool filtering, and env key names.
227+
* Any execution-affecting config change invalidates the approval.
228228
*/
229229
export function computeMCPConfigHash(
230230
name: string,
231231
config: MCPServerConfig,
232232
): string {
233-
return createHash("sha256")
234-
.update(name, "utf8")
235-
.update(config.command, "utf8")
236-
.update(JSON.stringify(config.args ?? []), "utf8")
237-
.digest("hex");
233+
return (
234+
createHash("sha256")
235+
.update(name, "utf8")
236+
.update(config.command, "utf8")
237+
.update(JSON.stringify(config.args ?? []), "utf8")
238+
.update(JSON.stringify(config.allowTools ?? []), "utf8")
239+
.update(JSON.stringify(config.denyTools ?? []), "utf8")
240+
// Hash env key names (not values — secrets stay out of the hash)
241+
.update(JSON.stringify(Object.keys(config.env ?? {}).sort()), "utf8")
242+
.digest("hex")
243+
);
238244
}

src/agent/mcp/plugin-adapter.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,11 @@ function generateInputInterface(tool: MCPToolSchema): string | null {
138138
const desc = propSchema.description
139139
? ` /** ${propSchema.description} */\n`
140140
: "";
141-
lines.push(`${desc} ${propName}${optional}: ${tsType};`);
141+
// Quote property names that aren't valid JS identifiers
142+
const safeName = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(propName)
143+
? propName
144+
: `"${propName}"`;
145+
lines.push(`${desc} ${safeName}${optional}: ${tsType};`);
142146
}
143147

144148
lines.push("}");

src/code-validator/guest/runtime/src/validator.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -923,10 +923,18 @@ fn strip_js_comments(source: &str) -> String {
923923
}
924924
}
925925
// Single-line comment — replace with spaces (preserve line structure)
926+
// Guard: if preceded by a backslash, this might be inside a regex
927+
// literal (e.g. /https?:\/\//). Skip the comment treatment.
926928
b'/' if i + 1 < len && bytes[i + 1] == b'/' => {
927-
while i < len && bytes[i] != b'\n' {
928-
out.push(' ');
929+
if i > 0 && bytes[i - 1] == b'\\' {
930+
// Likely inside a regex literal — keep as-is
931+
out.push('/');
929932
i += 1;
933+
} else {
934+
while i < len && bytes[i] != b'\n' {
935+
out.push(' ');
936+
i += 1;
937+
}
930938
}
931939
}
932940
// Block comment — replace with spaces, preserve newlines
@@ -1085,10 +1093,29 @@ fn check_handler_has_param(source: &str) -> bool {
10851093
}
10861094
}
10871095
// Arrow: const handler = (event) => or handler = event =>
1096+
// Must verify it actually has a parameter, not just () =>
10881097
if (trimmed.contains("const handler") || trimmed.contains("let handler"))
10891098
&& trimmed.contains("=>")
10901099
{
1091-
return true;
1100+
// Check for non-empty parens: handler = (something) =>
1101+
if let Some(eq_pos) = trimmed.find('=') {
1102+
let after_eq = trimmed[eq_pos + 1..].trim_start();
1103+
// Skip any second '=' (==)
1104+
if after_eq.starts_with('=') {
1105+
// Not an assignment — skip
1106+
} else if after_eq.starts_with('(') {
1107+
// Parenthesised params: check for non-empty (x) vs ()
1108+
if let Some(close) = after_eq.find(')') {
1109+
let params = after_eq[1..close].trim();
1110+
if !params.is_empty() {
1111+
return true;
1112+
}
1113+
}
1114+
} else if !after_eq.starts_with("=>") {
1115+
// Bare param without parens: handler = event => ...
1116+
return true;
1117+
}
1118+
}
10921119
}
10931120
}
10941121
false

tests/mcp.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,41 @@ describe("computeMCPConfigHash", () => {
149149
const hash2 = computeMCPConfigHash("beta", config);
150150
expect(hash1).not.toBe(hash2);
151151
});
152+
153+
it("changes when allowTools change", () => {
154+
const base = { command: "node" };
155+
const hash1 = computeMCPConfigHash("test", {
156+
...base,
157+
allowTools: ["a"],
158+
});
159+
const hash2 = computeMCPConfigHash("test", {
160+
...base,
161+
allowTools: ["a", "b"],
162+
});
163+
expect(hash1).not.toBe(hash2);
164+
});
165+
166+
it("changes when denyTools change", () => {
167+
const base = { command: "node" };
168+
const hash1 = computeMCPConfigHash("test", {
169+
...base,
170+
denyTools: ["x"],
171+
});
172+
const hash2 = computeMCPConfigHash("test", { ...base, denyTools: [] });
173+
expect(hash1).not.toBe(hash2);
174+
});
175+
176+
it("changes when env keys change (not values)", () => {
177+
const hash1 = computeMCPConfigHash("test", {
178+
command: "node",
179+
env: { TOKEN: "secret1" },
180+
});
181+
const hash2 = computeMCPConfigHash("test", {
182+
command: "node",
183+
env: { API_KEY: "secret2" },
184+
});
185+
expect(hash1).not.toBe(hash2);
186+
});
152187
});
153188

154189
// ── Sanitisation ─────────────────────────────────────────────────────

0 commit comments

Comments
 (0)