Compare commits

...

14 Commits

Author SHA1 Message Date
gavrielc afa07f0566 Merge branch 'main' into cleanup/cli-protocol-vocab 2026-07-04 19:34:42 +03:00
github-actions[bot] 8059ee4eec docs: update token count to 207k tokens · 104% of context window 2026-07-04 16:34:08 +00:00
gavrielc 41c486cacc Merge branch 'main' into cleanup/cli-protocol-vocab 2026-07-04 19:34:02 +03:00
gavrielc 190a7d4f43 Merge pull request #2935 from nanocoai/cleanup/remove-dead-v1-config
Delete dead v1 config knobs and the broken pnpm auth script
2026-07-04 19:33:53 +03:00
gavrielc e3d2d43b0e Merge branch 'main' into cleanup/remove-dead-v1-config 2026-07-04 19:33:11 +03:00
github-actions[bot] afde23bbe6 chore: bump version to 2.1.32 2026-07-04 16:32:51 +00:00
gavrielc 776bc14ca0 Merge pull request #2934 from nanocoai/cleanup/reachable-perimeter-env
Make the security-perimeter env vars reachable under the shipped service
2026-07-04 19:32:37 +03:00
gavrielc a9461afef1 Merge branch 'main' into cleanup/reachable-perimeter-env 2026-07-04 19:32:28 +03:00
github-actions[bot] ca81558ec8 chore: bump version to 2.1.31 2026-07-04 16:30:56 +00:00
github-actions[bot] 2f88e4fa15 docs: update token count to 208k tokens · 104% of context window 2026-07-04 16:30:54 +00:00
gavrielc 9b0d1dd044 Merge pull request #2933 from nanocoai/feat/approval-button-styles
feat(approvals): colored buttons on approval cards (Slack primary/danger)
2026-07-04 19:30:42 +03:00
gavrielc 855f204d8a Clean up dead ncl CLI protocol vocabulary
Remove scaffold-era vocabulary in the ncl protocol that no code produces:
the duplicate `Access` union in crud.ts (now imported from registry.ts),
the unused `'hidden'` access level, and the never-emitted `'permission-denied'`
and `'not-found'` error codes. Also refresh three stale docstrings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 16:05:43 +03:00
gavrielc d4ca8a4ea6 chore(config): remove dead v1 config knobs and broken pnpm auth script
These config exports in src/config.ts have zero readers and advertise
controls that v2 removed:

- CONTAINER_TIMEOUT / CONTAINER_MAX_OUTPUT_SIZE / IDLE_TIMEOUT /
  MAX_CONCURRENT_CONTAINERS / MAX_MESSAGES_PER_PROMPT
- the trigger block (escapeRegex, buildTriggerPattern, DEFAULT_TRIGGER,
  getTriggerPattern, TRIGGER_PATTERN)

Also remove the `auth` script from package.json — it pointed at the
deleted src/whatsapp-auth.ts, so `pnpm run auth` only errored — and fix
the stale MAX_MESSAGES_PER_PROMPT reference in the container's
messages-in.ts comment (the value comes from container.json's
maxMessagesPerPrompt).

Grep confirms no importers in src/, container/, scripts/, setup/, or
tests. Precedent: 0283391.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 16:05:34 +03:00
gavrielc 7615d7d846 Make security-perimeter env vars reachable under the shipped service
Egress-lockdown and CPU/memory limits were read from process.env only, but
the shipped launchd/systemd service sets just PATH+HOME and the host never
loads .env into process.env — so these knobs could not be turned on the
supported way. Route them through the same readEnvFile path as the other
config keys, keeping process.env precedence so dev-mode/nohup installs are
unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 16:05:27 +03:00
9 changed files with 50 additions and 47 deletions
+1 -2
View File
@@ -56,7 +56,7 @@ function getMaxMessagesPerPrompt(): number {
* Reads from inbound.db (read-only), filters against processing_ack in outbound.db
* to skip messages already picked up by this or a previous container run.
*
* Returns the most recent `MAX_MESSAGES_PER_PROMPT` pending rows in
* Returns the most recent `maxMessagesPerPrompt` pending rows in
* chronological order, regardless of their `trigger` flag: accumulated
* context (trigger=0) rides along with the wake-eligible rows so the agent
* sees the prior context it missed. Host's countDueMessages gates waking on
@@ -163,4 +163,3 @@ export function findQuestionResponse(questionId: string): MessageInRow | undefin
inbound.close();
}
}
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "nanoclaw",
"version": "2.1.30",
"version": "2.1.32",
"description": "Personal Claude assistant. Lightweight, secure, customizable.",
"type": "module",
"packageManager": "pnpm@10.33.0",
@@ -21,7 +21,6 @@
"setup:auto": "tsx setup/auto.ts",
"ncl": "tsx src/cli/client.ts",
"chat": "tsx scripts/chat.ts",
"auth": "tsx src/whatsapp-auth.ts",
"lint": "eslint src/",
"lint:fix": "eslint src/ --fix",
"test": "vitest run",
+1 -1
View File
@@ -27,7 +27,7 @@ register({
if (cliScope === 'group') {
resources = resources.filter((r) => GROUP_SCOPE_RESOURCES.has(r.plural));
}
const commands = listCommands().filter((c) => c.access !== 'hidden' && !c.resource);
const commands = listCommands().filter((c) => !c.resource);
const lines: string[] = [];
+1 -2
View File
@@ -10,14 +10,13 @@ import { randomUUID } from 'crypto';
import { getDb } from '../db/connection.js';
import { register } from './registry.js';
import type { Access } from './registry.js';
import type { CallerContext } from './frame.js';
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export type Access = 'open' | 'approval' | 'hidden';
export interface ColumnDef {
name: string;
type: 'string' | 'number' | 'boolean' | 'json';
+1 -3
View File
@@ -11,7 +11,7 @@
export type RequestFrame = {
/** Correlation key set by the client. */
id: string;
/** Registry name, e.g. "list-groups". */
/** Registry name, e.g. "groups-list". */
command: string;
/** Command-specific. Each command's parseArgs validates. */
args: Record<string, unknown>;
@@ -24,10 +24,8 @@ export type ResponseFrame =
export type ErrorCode =
| 'unknown-command'
| 'invalid-args'
| 'permission-denied'
| 'forbidden'
| 'approval-pending'
| 'not-found'
| 'handler-error'
| 'transport-error';
+14 -5
View File
@@ -1,19 +1,28 @@
/**
* Command registry — single source of truth for what `ncl` can do.
*
* Each command file under `commands/` calls `register()` at top level,
* and `commands/index.ts` imports them all for side effects so the
* registry is populated before the host's CLI server accepts connections.
* Most commands come from resource modules under `resources/`, which call
* `registerResource()` (one `register()` per CRUD verb); the top-level `help`
* command and the per-resource help commands register directly. The barrel
* `commands/index.ts` imports the resource barrel for its side effects and then
* registers the help commands, so the registry is populated before the host's
* CLI server accepts connections.
*/
import type { CallerContext } from './frame.js';
export type Access = 'open' | 'approval' | 'hidden';
export type Access = 'open' | 'approval';
export type CommandDef<TArgs = unknown, TData = unknown> = {
name: string;
description: string;
access: Access;
/** Resource this command belongs to (for help grouping). */
/**
* The group-scope whitelist key. Under `cli_scope: 'group'` the dispatcher
* only lets an agent run commands whose `resource` is on the whitelist
* (`groups`, `sessions`, `destinations`, `members`); it also drives help
* grouping. Omitting `resource` exempts the command from the whitelist —
* that's how general commands like `help` stay reachable in group scope.
*/
resource?: string;
/**
* Set on the auto-generated `list` / `get` handlers (see `registerResource`).
+21 -24
View File
@@ -6,7 +6,18 @@ import { getContainerImageBase, getDefaultContainerImage, getInstallSlug } from
import { isValidTimezone } from './timezone.js';
// Read config values from .env (falls back to process.env).
const envConfig = readEnvFile(['ASSISTANT_NAME', 'ASSISTANT_HAS_OWN_NUMBER', 'ONECLI_URL', 'ONECLI_API_KEY', 'TZ']);
const envConfig = readEnvFile([
'ASSISTANT_NAME',
'ASSISTANT_HAS_OWN_NUMBER',
'ONECLI_URL',
'ONECLI_API_KEY',
'TZ',
'CONTAINER_CPU_LIMIT',
'CONTAINER_MEMORY_LIMIT',
'NANOCLAW_EGRESS_LOCKDOWN',
'NANOCLAW_EGRESS_NETWORK',
'ONECLI_GATEWAY_CONTAINER',
]);
export const ASSISTANT_NAME = process.env.ASSISTANT_NAME || envConfig.ASSISTANT_NAME || 'Andy';
export const ASSISTANT_HAS_OWN_NUMBER =
@@ -37,35 +48,21 @@ export const CONTAINER_IMAGE = process.env.CONTAINER_IMAGE || getDefaultContaine
// cleanupOrphans only reaps containers from this install, not peers.
export const INSTALL_SLUG = getInstallSlug(PROJECT_ROOT);
export const CONTAINER_INSTALL_LABEL = `nanoclaw-install=${INSTALL_SLUG}`;
export const CONTAINER_TIMEOUT = parseInt(process.env.CONTAINER_TIMEOUT || '1800000', 10);
export const CONTAINER_MAX_OUTPUT_SIZE = parseInt(process.env.CONTAINER_MAX_OUTPUT_SIZE || '10485760', 10); // 10MB default
export const ONECLI_URL = process.env.ONECLI_URL || envConfig.ONECLI_URL;
export const ONECLI_API_KEY = process.env.ONECLI_API_KEY || envConfig.ONECLI_API_KEY;
export const MAX_MESSAGES_PER_PROMPT = Math.max(1, parseInt(process.env.MAX_MESSAGES_PER_PROMPT || '10', 10) || 10);
export const IDLE_TIMEOUT = parseInt(process.env.IDLE_TIMEOUT || '1800000', 10); // 30min default — how long to keep container alive after last result
export const MAX_CONCURRENT_CONTAINERS = Math.max(1, parseInt(process.env.MAX_CONCURRENT_CONTAINERS || '5', 10) || 5);
// Per-container resource caps, passed through to `docker run`. Default empty =
// no flag added = today's unbounded behavior (don't OOM existing OSS workloads).
// Operators opt in: CONTAINER_CPU_LIMIT=2, CONTAINER_MEMORY_LIMIT=8g.
export const CONTAINER_CPU_LIMIT = process.env.CONTAINER_CPU_LIMIT || '';
export const CONTAINER_MEMORY_LIMIT = process.env.CONTAINER_MEMORY_LIMIT || '';
export const CONTAINER_CPU_LIMIT = process.env.CONTAINER_CPU_LIMIT || envConfig.CONTAINER_CPU_LIMIT || '';
export const CONTAINER_MEMORY_LIMIT = process.env.CONTAINER_MEMORY_LIMIT || envConfig.CONTAINER_MEMORY_LIMIT || '';
function escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
export function buildTriggerPattern(trigger: string): RegExp {
return new RegExp(`^${escapeRegex(trigger.trim())}\\b`, 'i');
}
export const DEFAULT_TRIGGER = `@${ASSISTANT_NAME}`;
export function getTriggerPattern(trigger?: string): RegExp {
const normalizedTrigger = trigger?.trim();
return buildTriggerPattern(normalizedTrigger || DEFAULT_TRIGGER);
}
export const TRIGGER_PATTERN = buildTriggerPattern(DEFAULT_TRIGGER);
// Egress lockdown — force all agent traffic through the OneCLI gateway on a
// no-internet Docker network. Off by default; consumed by src/egress-lockdown.ts.
export const EGRESS_LOCKDOWN = (process.env.NANOCLAW_EGRESS_LOCKDOWN || envConfig.NANOCLAW_EGRESS_LOCKDOWN) === 'true';
export const EGRESS_NETWORK =
process.env.NANOCLAW_EGRESS_NETWORK || envConfig.NANOCLAW_EGRESS_NETWORK || 'nanoclaw-egress';
export const ONECLI_GATEWAY_CONTAINER =
process.env.ONECLI_GATEWAY_CONTAINER || envConfig.ONECLI_GATEWAY_CONTAINER || 'onecli';
// Timezone for scheduled tasks, message formatting, etc.
// Validates each candidate is a real IANA identifier before accepting.
+6 -2
View File
@@ -73,8 +73,12 @@ describe('per-container resource limits (structural)', () => {
it('defaults both knobs to empty string in config (no flag = unbounded)', () => {
const cfg = fs.readFileSync(path.join(process.cwd(), 'src', 'config.ts'), 'utf-8');
expect(cfg).toContain("CONTAINER_CPU_LIMIT = process.env.CONTAINER_CPU_LIMIT || ''");
expect(cfg).toContain("CONTAINER_MEMORY_LIMIT = process.env.CONTAINER_MEMORY_LIMIT || ''");
expect(cfg).toContain(
"CONTAINER_CPU_LIMIT = process.env.CONTAINER_CPU_LIMIT || envConfig.CONTAINER_CPU_LIMIT || ''",
);
expect(cfg).toContain(
"CONTAINER_MEMORY_LIMIT = process.env.CONTAINER_MEMORY_LIMIT || envConfig.CONTAINER_MEMORY_LIMIT || ''",
);
});
});
+4 -6
View File
@@ -9,15 +9,13 @@
*/
import { execFileSync } from 'child_process';
import { EGRESS_LOCKDOWN, EGRESS_NETWORK, ONECLI_GATEWAY_CONTAINER } from './config.js';
import { CONTAINER_RUNTIME_BIN } from './container-runtime.js';
import { log } from './log.js';
/** Locked-down, no-internet network agents are placed on. */
export const EGRESS_NETWORK = process.env.NANOCLAW_EGRESS_NETWORK || 'nanoclaw-egress';
/** The OneCLI gateway container attached as the only egress hop. */
const ONECLI_GATEWAY_CONTAINER = process.env.ONECLI_GATEWAY_CONTAINER || 'onecli';
/** Off by default; set NANOCLAW_EGRESS_LOCKDOWN=true to opt in. */
const EGRESS_LOCKDOWN = process.env.NANOCLAW_EGRESS_LOCKDOWN === 'true';
// Perimeter knobs (locked-down network, gateway container, on/off flag) are read
// via config.ts so they honor .env under the shipped service, not just process.env.
export { EGRESS_NETWORK };
/** Raised when lockdown is requested but can't be established. */
export class EgressLockdownError extends Error {