Merge branch 'main' into main

This commit is contained in:
gavrielc
2026-05-04 18:32:09 +03:00
committed by GitHub
4 changed files with 47 additions and 9 deletions

View File

@@ -34,7 +34,11 @@ const SDK_DISALLOWED_TOOLS = [
'ExitWorktree',
];
// Tool allowlist for NanoClaw agent containers
// Tool allowlist for NanoClaw agent containers. MCP-tool entries are derived
// at the call site from the registered `mcpServers` map so that any server
// added via `add_mcp_server` (or wired in container.json directly) is
// reachable to the agent — without this, the SDK's allowedTools filter
// silently drops every MCP namespace not listed here.
const TOOL_ALLOWLIST = [
'Bash',
'Read',
@@ -54,9 +58,15 @@ const TOOL_ALLOWLIST = [
'ToolSearch',
'Skill',
'NotebookEdit',
'mcp__nanoclaw__*',
];
// MCP server names are sanitized by the SDK when forming tool prefixes:
// any character outside [A-Za-z0-9_-] becomes '_'. Mirror that here so our
// allowlist patterns match what the SDK actually exposes.
function mcpAllowPattern(serverName: string): string {
return `mcp__${serverName.replace(/[^a-zA-Z0-9_-]/g, '_')}__*`;
}
interface SDKUserMessage {
type: 'user';
message: { role: 'user'; content: string };
@@ -277,7 +287,10 @@ export class ClaudeProvider implements AgentProvider {
resume: input.continuation,
pathToClaudeCodeExecutable: '/pnpm/claude',
systemPrompt: instructions ? { type: 'preset' as const, preset: 'claude_code' as const, append: instructions } : undefined,
allowedTools: TOOL_ALLOWLIST,
allowedTools: [
...TOOL_ALLOWLIST,
...Object.keys(this.mcpServers).map(mcpAllowPattern),
],
disallowedTools: SDK_DISALLOWED_TOOLS,
env: this.env,
permissionMode: 'bypassPermissions',

View File

@@ -200,6 +200,33 @@ if [ "$LOW_MEM" = true ] || [ "$LOW_DISK" = true ]; then
esac
fi
# ─── pre-flight: Google Cloud VM warning (Linux) ──────────────────────
# NanoClaw is known to not run reliably on Google Compute Engine instances.
# Warn early — before the root check or bootstrap spinner — so users can
# switch providers before sinking time into setup. Detection uses DMI
# (no network round-trip), which on GCE reports "Google" / "Google
# Compute Engine".
if [ "$(uname -s)" = "Linux" ] \
&& { grep -qi 'Google' /sys/class/dmi/id/product_name 2>/dev/null \
|| grep -qi 'Google' /sys/class/dmi/id/sys_vendor 2>/dev/null; }; then
printf ' %s\n' "$(red 'Warning: Google Cloud VM detected.')"
printf ' %s\n' "$(dim 'Google blocks sudo commands, so NanoClaw is unlikely to run successfully on this VM.')"
printf ' %s\n\n' "$(dim 'If you want to run NanoClaw successfully, switch to a different provider (Hetzner, Hostinger, exe.dev and others..).')"
read -r -p " $(bold 'Try anyway?') [y/N] " GCE_ANS </dev/tty
case "${GCE_ANS:-N}" in
[Yy]*)
ph_event setup_gce_continued
printf '\n'
;;
*)
ph_event setup_gce_aborted
printf '\n %s\n\n' "$(dim 'Aborted. Re-run on a non-GCE host to continue.')"
exit 1
;;
esac
fi
# ─── pre-flight: root user warning (Linux) ────────────────────────────
if [ "$(uname -s)" = "Linux" ] && [ "$(id -u)" -eq 0 ]; then
printf ' %s\n' \

View File

@@ -1,6 +1,6 @@
{
"name": "nanoclaw",
"version": "2.0.28",
"version": "2.0.29",
"description": "Personal Claude assistant. Lightweight, secure, customizable.",
"type": "module",
"packageManager": "pnpm@10.33.0",

View File

@@ -23,7 +23,7 @@ import { emit as phEmit } from './diagnostics.js';
import type { StepResult, SpinnerLabels } from './runner.js';
import { dumpTranscriptOnFailure, spawnStep, writeStepEntry } from './runner.js';
import * as setupLog from '../logs.js';
import { brandBody, fitToWidth } from './theme.js';
import { brandBody, fitToWidth, fmtDuration } from './theme.js';
const WINDOW_SIZE = 3;
const SPINNER_FRAMES = ['◒', '◐', '◓', '◑'];
@@ -85,9 +85,8 @@ async function runUnderWindow(
const redraw = (): void => {
if (stallPromptActive) return;
out.write(`\x1b[${WINDOW_SIZE + 1}A`);
const elapsed = Math.round((Date.now() - start) / 1000);
const icon = SPINNER_FRAMES[frameIdx % SPINNER_FRAMES.length];
const suffix = ` (${elapsed}s)`;
const suffix = ` (${fmtDuration(Date.now() - start)})`;
const header = fitToWidth(labels.running, suffix);
out.write(`\x1b[2K${k.cyan(icon)} ${header}${k.dim(suffix)}\n`);
@@ -164,8 +163,7 @@ async function runUnderWindow(
out.write(SHOW_CURSOR);
process.off('exit', restoreCursorOnExit);
const elapsed = Math.round((Date.now() - start) / 1000);
const suffix = ` (${elapsed}s)`;
const suffix = ` (${fmtDuration(Date.now() - start)})`;
if (result.ok) {
const isSkipped = result.terminal?.fields.STATUS === 'skipped';
const msg = isSkipped && labels.skipped ? labels.skipped : labels.done;