Compare commits

...

9 Commits

Author SHA1 Message Date
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 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
6 changed files with 37 additions and 40 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",
+4 -4
View File
@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="90" height="20" role="img" aria-label="207k tokens, 104% of context window">
<title>207k tokens, 104% of context window</title>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="90" height="20" role="img" aria-label="208k tokens, 104% of context window">
<title>208k tokens, 104% of context window</title>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
@@ -15,8 +15,8 @@
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
<text aria-hidden="true" x="26" y="15" fill="#010101" fill-opacity=".3">tokens</text>
<text x="26" y="14">tokens</text>
<text aria-hidden="true" x="71" y="15" fill="#010101" fill-opacity=".3">207k</text>
<text x="71" y="14">207k</text>
<text aria-hidden="true" x="71" y="15" fill="#010101" fill-opacity=".3">208k</text>
<text x="71" y="14">208k</text>
</g>
</g>
</a>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

+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 {