@rozek/nanoclaw 1.2.17
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/settings.json +1 -0
- package/.claude/skills/add-compact/SKILL.md +135 -0
- package/.claude/skills/add-discord/SKILL.md +203 -0
- package/.claude/skills/add-gmail/SKILL.md +220 -0
- package/.claude/skills/add-image-vision/SKILL.md +94 -0
- package/.claude/skills/add-ollama-tool/SKILL.md +153 -0
- package/.claude/skills/add-parallel/SKILL.md +290 -0
- package/.claude/skills/add-pdf-reader/SKILL.md +104 -0
- package/.claude/skills/add-reactions/SKILL.md +117 -0
- package/.claude/skills/add-slack/SKILL.md +207 -0
- package/.claude/skills/add-telegram/SKILL.md +222 -0
- package/.claude/skills/add-telegram-swarm/SKILL.md +384 -0
- package/.claude/skills/add-voice-transcription/SKILL.md +148 -0
- package/.claude/skills/add-whatsapp/SKILL.md +372 -0
- package/.claude/skills/convert-to-apple-container/SKILL.md +175 -0
- package/.claude/skills/customize/SKILL.md +110 -0
- package/.claude/skills/debug/SKILL.md +349 -0
- package/.claude/skills/get-qodo-rules/SKILL.md +122 -0
- package/.claude/skills/get-qodo-rules/references/output-format.md +41 -0
- package/.claude/skills/get-qodo-rules/references/pagination.md +33 -0
- package/.claude/skills/get-qodo-rules/references/repository-scope.md +26 -0
- package/.claude/skills/qodo-pr-resolver/SKILL.md +326 -0
- package/.claude/skills/qodo-pr-resolver/resources/providers.md +329 -0
- package/.claude/skills/setup/SKILL.md +218 -0
- package/.claude/skills/update-nanoclaw/SKILL.md +235 -0
- package/.claude/skills/update-skills/SKILL.md +130 -0
- package/.claude/skills/use-local-whisper/SKILL.md +152 -0
- package/.claude/skills/x-integration/SKILL.md +417 -0
- package/.claude/skills/x-integration/agent.ts +243 -0
- package/.claude/skills/x-integration/host.ts +159 -0
- package/.claude/skills/x-integration/lib/browser.ts +148 -0
- package/.claude/skills/x-integration/lib/config.ts +62 -0
- package/.claude/skills/x-integration/scripts/like.ts +56 -0
- package/.claude/skills/x-integration/scripts/post.ts +66 -0
- package/.claude/skills/x-integration/scripts/quote.ts +80 -0
- package/.claude/skills/x-integration/scripts/reply.ts +74 -0
- package/.claude/skills/x-integration/scripts/retweet.ts +62 -0
- package/.claude/skills/x-integration/scripts/setup.ts +87 -0
- package/.env.example +1 -0
- package/.github/CODEOWNERS +10 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +14 -0
- package/.github/workflows/bump-version.yml +32 -0
- package/.github/workflows/ci.yml +25 -0
- package/.github/workflows/merge-forward-skills.yml +160 -0
- package/.github/workflows/update-tokens.yml +42 -0
- package/.husky/pre-commit +1 -0
- package/.mcp.json +3 -0
- package/.nvmrc +1 -0
- package/.prettierrc +3 -0
- package/CHANGELOG.md +8 -0
- package/CLAUDE.md +64 -0
- package/CONTRIBUTING.md +23 -0
- package/CONTRIBUTORS.md +15 -0
- package/LICENSE +21 -0
- package/NanoClaw_with_Web-Support.md +290 -0
- package/README.md +261 -0
- package/README_zh.md +200 -0
- package/assets/nanoclaw-favicon.png +0 -0
- package/assets/nanoclaw-icon.png +0 -0
- package/assets/nanoclaw-logo-dark.png +0 -0
- package/assets/nanoclaw-logo.png +0 -0
- package/assets/nanoclaw-profile.jpeg +0 -0
- package/assets/nanoclaw-sales.png +0 -0
- package/assets/social-preview.jpg +0 -0
- package/config-examples/mount-allowlist.json +25 -0
- package/container/Dockerfile +70 -0
- package/container/agent-runner/package-lock.json +1524 -0
- package/container/agent-runner/package.json +21 -0
- package/container/agent-runner/src/index.ts +558 -0
- package/container/agent-runner/src/ipc-mcp-stdio.ts +338 -0
- package/container/agent-runner/tsconfig.json +15 -0
- package/container/build.sh +23 -0
- package/container/skills/agent-browser/SKILL.md +159 -0
- package/container/skills/capabilities/SKILL.md +100 -0
- package/container/skills/status/SKILL.md +104 -0
- package/dist/channels/index.d.ts +2 -0
- package/dist/channels/index.d.ts.map +1 -0
- package/dist/channels/index.js +9 -0
- package/dist/channels/index.js.map +1 -0
- package/dist/channels/registry.d.ts +13 -0
- package/dist/channels/registry.d.ts.map +1 -0
- package/dist/channels/registry.js +11 -0
- package/dist/channels/registry.js.map +1 -0
- package/dist/channels/registry.test.d.ts +2 -0
- package/dist/channels/registry.test.d.ts.map +1 -0
- package/dist/channels/registry.test.js +32 -0
- package/dist/channels/registry.test.js.map +1 -0
- package/dist/channels/web.d.ts +2 -0
- package/dist/channels/web.d.ts.map +1 -0
- package/dist/channels/web.js +1738 -0
- package/dist/channels/web.js.map +1 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +182 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +36 -0
- package/dist/config.js.map +1 -0
- package/dist/container-runner.d.ts +44 -0
- package/dist/container-runner.d.ts.map +1 -0
- package/dist/container-runner.js +467 -0
- package/dist/container-runner.js.map +1 -0
- package/dist/container-runner.test.d.ts +2 -0
- package/dist/container-runner.test.d.ts.map +1 -0
- package/dist/container-runner.test.js +150 -0
- package/dist/container-runner.test.js.map +1 -0
- package/dist/container-runtime.d.ts +22 -0
- package/dist/container-runtime.d.ts.map +1 -0
- package/dist/container-runtime.js +96 -0
- package/dist/container-runtime.js.map +1 -0
- package/dist/container-runtime.test.d.ts +2 -0
- package/dist/container-runtime.test.d.ts.map +1 -0
- package/dist/container-runtime.test.js +93 -0
- package/dist/container-runtime.test.js.map +1 -0
- package/dist/credential-proxy.d.ts +21 -0
- package/dist/credential-proxy.d.ts.map +1 -0
- package/dist/credential-proxy.js +95 -0
- package/dist/credential-proxy.js.map +1 -0
- package/dist/credential-proxy.test.d.ts +2 -0
- package/dist/credential-proxy.test.d.ts.map +1 -0
- package/dist/credential-proxy.test.js +134 -0
- package/dist/credential-proxy.test.js.map +1 -0
- package/dist/db.d.ts +115 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +549 -0
- package/dist/db.js.map +1 -0
- package/dist/db.test.d.ts +2 -0
- package/dist/db.test.d.ts.map +1 -0
- package/dist/db.test.js +360 -0
- package/dist/db.test.js.map +1 -0
- package/dist/env.d.ts +8 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +42 -0
- package/dist/env.js.map +1 -0
- package/dist/formatting.test.d.ts +2 -0
- package/dist/formatting.test.d.ts.map +1 -0
- package/dist/formatting.test.js +183 -0
- package/dist/formatting.test.js.map +1 -0
- package/dist/group-folder.d.ts +5 -0
- package/dist/group-folder.d.ts.map +1 -0
- package/dist/group-folder.js +44 -0
- package/dist/group-folder.js.map +1 -0
- package/dist/group-folder.test.d.ts +2 -0
- package/dist/group-folder.test.d.ts.map +1 -0
- package/dist/group-folder.test.js +29 -0
- package/dist/group-folder.test.js.map +1 -0
- package/dist/group-queue.d.ts +34 -0
- package/dist/group-queue.d.ts.map +1 -0
- package/dist/group-queue.js +263 -0
- package/dist/group-queue.js.map +1 -0
- package/dist/group-queue.test.d.ts +2 -0
- package/dist/group-queue.test.d.ts.map +1 -0
- package/dist/group-queue.test.js +341 -0
- package/dist/group-queue.test.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +518 -0
- package/dist/index.js.map +1 -0
- package/dist/ipc-auth.test.d.ts +2 -0
- package/dist/ipc-auth.test.d.ts.map +1 -0
- package/dist/ipc-auth.test.js +434 -0
- package/dist/ipc-auth.test.js.map +1 -0
- package/dist/ipc.d.ts +32 -0
- package/dist/ipc.d.ts.map +1 -0
- package/dist/ipc.js +311 -0
- package/dist/ipc.js.map +1 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +14 -0
- package/dist/logger.js.map +1 -0
- package/dist/mount-security.d.ts +34 -0
- package/dist/mount-security.d.ts.map +1 -0
- package/dist/mount-security.js +325 -0
- package/dist/mount-security.js.map +1 -0
- package/dist/remote-control.d.ts +32 -0
- package/dist/remote-control.d.ts.map +1 -0
- package/dist/remote-control.js +185 -0
- package/dist/remote-control.js.map +1 -0
- package/dist/remote-control.test.d.ts +2 -0
- package/dist/remote-control.test.d.ts.map +1 -0
- package/dist/remote-control.test.js +321 -0
- package/dist/remote-control.test.js.map +1 -0
- package/dist/router.d.ts +8 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +37 -0
- package/dist/router.js.map +1 -0
- package/dist/routing.test.d.ts +2 -0
- package/dist/routing.test.d.ts.map +1 -0
- package/dist/routing.test.js +81 -0
- package/dist/routing.test.js.map +1 -0
- package/dist/sender-allowlist.d.ts +14 -0
- package/dist/sender-allowlist.d.ts.map +1 -0
- package/dist/sender-allowlist.js +79 -0
- package/dist/sender-allowlist.js.map +1 -0
- package/dist/sender-allowlist.test.d.ts +2 -0
- package/dist/sender-allowlist.test.d.ts.map +1 -0
- package/dist/sender-allowlist.test.js +186 -0
- package/dist/sender-allowlist.test.js.map +1 -0
- package/dist/session-commands.d.ts +47 -0
- package/dist/session-commands.d.ts.map +1 -0
- package/dist/session-commands.js +102 -0
- package/dist/session-commands.js.map +1 -0
- package/dist/session-commands.test.d.ts +2 -0
- package/dist/session-commands.test.d.ts.map +1 -0
- package/dist/session-commands.test.js +190 -0
- package/dist/session-commands.test.js.map +1 -0
- package/dist/task-scheduler.d.ts +22 -0
- package/dist/task-scheduler.d.ts.map +1 -0
- package/dist/task-scheduler.js +210 -0
- package/dist/task-scheduler.js.map +1 -0
- package/dist/task-scheduler.test.d.ts +2 -0
- package/dist/task-scheduler.test.d.ts.map +1 -0
- package/dist/task-scheduler.test.js +107 -0
- package/dist/task-scheduler.test.js.map +1 -0
- package/dist/timezone.d.ts +6 -0
- package/dist/timezone.d.ts.map +1 -0
- package/dist/timezone.js +17 -0
- package/dist/timezone.js.map +1 -0
- package/dist/timezone.test.d.ts +2 -0
- package/dist/timezone.test.d.ts.map +1 -0
- package/dist/timezone.test.js +23 -0
- package/dist/timezone.test.js.map +1 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/docs/APPLE-CONTAINER-NETWORKING.md +90 -0
- package/docs/DEBUG_CHECKLIST.md +143 -0
- package/docs/REQUIREMENTS.md +196 -0
- package/docs/SDK_DEEP_DIVE.md +643 -0
- package/docs/SECURITY.md +122 -0
- package/docs/SPEC.md +785 -0
- package/docs/docker-sandboxes.md +359 -0
- package/docs/nanoclaw-architecture-final.md +1063 -0
- package/docs/nanorepo-architecture.md +168 -0
- package/docs/skills-as-branches.md +662 -0
- package/groups/global/CLAUDE.md +58 -0
- package/groups/main/CLAUDE.md +246 -0
- package/launchd/com.nanoclaw.plist +32 -0
- package/package.json +45 -0
- package/repo-tokens/README.md +113 -0
- package/repo-tokens/action.yml +186 -0
- package/repo-tokens/badge.svg +23 -0
- package/repo-tokens/examples/green.svg +14 -0
- package/repo-tokens/examples/red.svg +14 -0
- package/repo-tokens/examples/yellow-green.svg +14 -0
- package/repo-tokens/examples/yellow.svg +14 -0
- package/scripts/run-migrations.ts +105 -0
- package/setup/container.ts +144 -0
- package/setup/environment.test.ts +121 -0
- package/setup/environment.ts +94 -0
- package/setup/groups.ts +229 -0
- package/setup/index.ts +58 -0
- package/setup/mounts.ts +115 -0
- package/setup/platform.test.ts +120 -0
- package/setup/platform.ts +132 -0
- package/setup/register.test.ts +257 -0
- package/setup/register.ts +177 -0
- package/setup/service.test.ts +187 -0
- package/setup/service.ts +362 -0
- package/setup/status.ts +16 -0
- package/setup/verify.ts +192 -0
- package/setup.sh +161 -0
- package/src/channels/index.ts +12 -0
- package/src/channels/registry.test.ts +42 -0
- package/src/channels/registry.ts +32 -0
- package/src/channels/web.ts +1856 -0
- package/src/cli.ts +209 -0
- package/src/config.ts +73 -0
- package/src/container-runner.test.ts +210 -0
- package/src/container-runner.ts +707 -0
- package/src/container-runtime.test.ts +149 -0
- package/src/container-runtime.ts +127 -0
- package/src/credential-proxy.test.ts +192 -0
- package/src/credential-proxy.ts +125 -0
- package/src/db.test.ts +484 -0
- package/src/db.ts +803 -0
- package/src/env.ts +42 -0
- package/src/formatting.test.ts +256 -0
- package/src/group-folder.test.ts +43 -0
- package/src/group-folder.ts +44 -0
- package/src/group-queue.test.ts +484 -0
- package/src/group-queue.ts +365 -0
- package/src/index.ts +731 -0
- package/src/ipc-auth.test.ts +679 -0
- package/src/ipc.ts +461 -0
- package/src/logger.ts +16 -0
- package/src/mount-security.ts +419 -0
- package/src/remote-control.test.ts +397 -0
- package/src/remote-control.ts +224 -0
- package/src/router.ts +52 -0
- package/src/routing.test.ts +170 -0
- package/src/sender-allowlist.test.ts +216 -0
- package/src/sender-allowlist.ts +128 -0
- package/src/session-commands.test.ts +247 -0
- package/src/session-commands.ts +163 -0
- package/src/task-scheduler.test.ts +129 -0
- package/src/task-scheduler.ts +295 -0
- package/src/timezone.test.ts +29 -0
- package/src/timezone.ts +16 -0
- package/src/types.ts +107 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +7 -0
- package/vitest.skills.config.ts +7 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { CronExpressionParser } from 'cron-parser';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { ASSISTANT_NAME, SCHEDULER_POLL_INTERVAL, TIMEZONE } from './config.js';
|
|
4
|
+
import { runContainerAgent, writeTasksSnapshot, } from './container-runner.js';
|
|
5
|
+
import { getAllTasks, getDueTasks, getTaskById, logTaskRun, updateTask, updateTaskAfterRun, } from './db.js';
|
|
6
|
+
import { resolveGroupFolderPath } from './group-folder.js';
|
|
7
|
+
import { logger } from './logger.js';
|
|
8
|
+
/**
|
|
9
|
+
* Compute the next run time for a recurring task, anchored to the
|
|
10
|
+
* task's scheduled time rather than Date.now() to prevent cumulative
|
|
11
|
+
* drift on interval-based tasks.
|
|
12
|
+
*
|
|
13
|
+
* Co-authored-by: @community-pr-601
|
|
14
|
+
*/
|
|
15
|
+
export function computeNextRun(task) {
|
|
16
|
+
if (task.schedule_type === 'once')
|
|
17
|
+
return null;
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
if (task.schedule_type === 'cron') {
|
|
20
|
+
const interval = CronExpressionParser.parse(task.schedule_value, {
|
|
21
|
+
tz: TIMEZONE,
|
|
22
|
+
});
|
|
23
|
+
return interval.next().toISOString();
|
|
24
|
+
}
|
|
25
|
+
if (task.schedule_type === 'interval') {
|
|
26
|
+
const ms = parseInt(task.schedule_value, 10);
|
|
27
|
+
if (!ms || ms <= 0) {
|
|
28
|
+
// Guard against malformed interval that would cause an infinite loop
|
|
29
|
+
logger.warn({ taskId: task.id, value: task.schedule_value }, 'Invalid interval value');
|
|
30
|
+
return new Date(now + 60_000).toISOString();
|
|
31
|
+
}
|
|
32
|
+
// Anchor to the scheduled time, not now, to prevent drift.
|
|
33
|
+
// Skip past any missed intervals so we always land in the future.
|
|
34
|
+
let next = new Date(task.next_run).getTime() + ms;
|
|
35
|
+
while (next <= now) {
|
|
36
|
+
next += ms;
|
|
37
|
+
}
|
|
38
|
+
return new Date(next).toISOString();
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
async function runTask(task, deps) {
|
|
43
|
+
const startTime = Date.now();
|
|
44
|
+
let groupDir;
|
|
45
|
+
try {
|
|
46
|
+
groupDir = resolveGroupFolderPath(task.group_folder);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
50
|
+
// Stop retry churn for malformed legacy rows.
|
|
51
|
+
updateTask(task.id, { status: 'paused' });
|
|
52
|
+
logger.error({ taskId: task.id, groupFolder: task.group_folder, error }, 'Task has invalid group folder');
|
|
53
|
+
logTaskRun({
|
|
54
|
+
task_id: task.id,
|
|
55
|
+
run_at: new Date().toISOString(),
|
|
56
|
+
duration_ms: Date.now() - startTime,
|
|
57
|
+
status: 'error',
|
|
58
|
+
result: null,
|
|
59
|
+
error,
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
fs.mkdirSync(groupDir, { recursive: true });
|
|
64
|
+
logger.info({ taskId: task.id, group: task.group_folder }, 'Running scheduled task');
|
|
65
|
+
const groups = deps.registeredGroups();
|
|
66
|
+
let group = Object.values(groups).find((g) => g.folder === task.group_folder);
|
|
67
|
+
if (!group) {
|
|
68
|
+
// Fall back to the first isMain group so that legacy tasks created with
|
|
69
|
+
// group_folder='main' still run after a web-channel migration.
|
|
70
|
+
const fallback = Object.values(groups).find((g) => g.isMain === true);
|
|
71
|
+
if (fallback) {
|
|
72
|
+
logger.warn({ taskId: task.id, groupFolder: task.group_folder, fallback: fallback.folder }, 'Group not found for task, falling back to main group');
|
|
73
|
+
group = fallback;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
logger.error({ taskId: task.id, groupFolder: task.group_folder }, 'Group not found for task');
|
|
77
|
+
logTaskRun({
|
|
78
|
+
task_id: task.id,
|
|
79
|
+
run_at: new Date().toISOString(),
|
|
80
|
+
duration_ms: Date.now() - startTime,
|
|
81
|
+
status: 'error',
|
|
82
|
+
result: null,
|
|
83
|
+
error: `Group not found: ${task.group_folder}`,
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Update tasks snapshot for container to read (filtered by group)
|
|
89
|
+
const isMain = group.isMain === true;
|
|
90
|
+
const tasks = getAllTasks();
|
|
91
|
+
writeTasksSnapshot(group.folder, isMain, tasks.map((t) => ({
|
|
92
|
+
id: t.id,
|
|
93
|
+
groupFolder: t.group_folder,
|
|
94
|
+
prompt: t.prompt,
|
|
95
|
+
schedule_type: t.schedule_type,
|
|
96
|
+
schedule_value: t.schedule_value,
|
|
97
|
+
status: t.status,
|
|
98
|
+
next_run: t.next_run,
|
|
99
|
+
})));
|
|
100
|
+
let result = null;
|
|
101
|
+
let error = null;
|
|
102
|
+
// For group context mode, use the group's current session
|
|
103
|
+
const sessions = deps.getSessions();
|
|
104
|
+
const sessionId = task.context_mode === 'group' ? sessions[task.group_folder] : undefined;
|
|
105
|
+
// After the task produces a result, close the container promptly.
|
|
106
|
+
// Tasks are single-turn — no need to wait IDLE_TIMEOUT (30 min) for the
|
|
107
|
+
// query loop to time out. A short delay handles any final MCP calls.
|
|
108
|
+
const TASK_CLOSE_DELAY_MS = 10000;
|
|
109
|
+
let closeTimer = null;
|
|
110
|
+
const scheduleClose = () => {
|
|
111
|
+
if (closeTimer)
|
|
112
|
+
return; // already scheduled
|
|
113
|
+
closeTimer = setTimeout(() => {
|
|
114
|
+
logger.debug({ taskId: task.id }, 'Closing task container after result');
|
|
115
|
+
deps.queue.closeStdin(task.chat_jid);
|
|
116
|
+
}, TASK_CLOSE_DELAY_MS);
|
|
117
|
+
};
|
|
118
|
+
try {
|
|
119
|
+
const output = await runContainerAgent(group, {
|
|
120
|
+
prompt: task.prompt,
|
|
121
|
+
sessionId,
|
|
122
|
+
groupFolder: group.folder,
|
|
123
|
+
chatJid: task.chat_jid,
|
|
124
|
+
isMain,
|
|
125
|
+
isScheduledTask: true,
|
|
126
|
+
assistantName: ASSISTANT_NAME,
|
|
127
|
+
}, (proc, containerName) => deps.onProcess(task.chat_jid, proc, containerName, group.folder), async (streamedOutput) => {
|
|
128
|
+
if (streamedOutput.result) {
|
|
129
|
+
result = streamedOutput.result;
|
|
130
|
+
// Forward result to user (sendMessage handles formatting)
|
|
131
|
+
await deps.sendMessage(task.chat_jid, streamedOutput.result);
|
|
132
|
+
scheduleClose();
|
|
133
|
+
}
|
|
134
|
+
if (streamedOutput.status === 'success') {
|
|
135
|
+
deps.queue.notifyIdle(task.chat_jid);
|
|
136
|
+
scheduleClose(); // Close promptly even when result is null (e.g. IPC-only tasks)
|
|
137
|
+
}
|
|
138
|
+
if (streamedOutput.status === 'error') {
|
|
139
|
+
error = streamedOutput.error || 'Unknown error';
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
if (closeTimer)
|
|
143
|
+
clearTimeout(closeTimer);
|
|
144
|
+
if (output.status === 'error') {
|
|
145
|
+
error = output.error || 'Unknown error';
|
|
146
|
+
}
|
|
147
|
+
else if (output.result) {
|
|
148
|
+
// Result was already forwarded to the user via the streaming callback above
|
|
149
|
+
result = output.result;
|
|
150
|
+
}
|
|
151
|
+
logger.info({ taskId: task.id, durationMs: Date.now() - startTime }, 'Task completed');
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
if (closeTimer)
|
|
155
|
+
clearTimeout(closeTimer);
|
|
156
|
+
error = err instanceof Error ? err.message : String(err);
|
|
157
|
+
logger.error({ taskId: task.id, error }, 'Task failed');
|
|
158
|
+
}
|
|
159
|
+
const durationMs = Date.now() - startTime;
|
|
160
|
+
logTaskRun({
|
|
161
|
+
task_id: task.id,
|
|
162
|
+
run_at: new Date().toISOString(),
|
|
163
|
+
duration_ms: durationMs,
|
|
164
|
+
status: error ? 'error' : 'success',
|
|
165
|
+
result,
|
|
166
|
+
error,
|
|
167
|
+
});
|
|
168
|
+
const nextRun = computeNextRun(task);
|
|
169
|
+
const resultSummary = error
|
|
170
|
+
? `Error: ${error}`
|
|
171
|
+
: result
|
|
172
|
+
? result.slice(0, 200)
|
|
173
|
+
: 'Completed';
|
|
174
|
+
updateTaskAfterRun(task.id, nextRun, resultSummary);
|
|
175
|
+
}
|
|
176
|
+
let schedulerRunning = false;
|
|
177
|
+
export function startSchedulerLoop(deps) {
|
|
178
|
+
if (schedulerRunning) {
|
|
179
|
+
logger.debug('Scheduler loop already running, skipping duplicate start');
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
schedulerRunning = true;
|
|
183
|
+
logger.info('Scheduler loop started');
|
|
184
|
+
const loop = async () => {
|
|
185
|
+
try {
|
|
186
|
+
const dueTasks = getDueTasks();
|
|
187
|
+
if (dueTasks.length > 0) {
|
|
188
|
+
logger.info({ count: dueTasks.length }, 'Found due tasks');
|
|
189
|
+
}
|
|
190
|
+
for (const task of dueTasks) {
|
|
191
|
+
// Re-check task status in case it was paused/cancelled
|
|
192
|
+
const currentTask = getTaskById(task.id);
|
|
193
|
+
if (!currentTask || currentTask.status !== 'active') {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
deps.queue.enqueueTask(currentTask.chat_jid, currentTask.id, () => runTask(currentTask, deps));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
logger.error({ err }, 'Error in scheduler loop');
|
|
201
|
+
}
|
|
202
|
+
setTimeout(loop, SCHEDULER_POLL_INTERVAL);
|
|
203
|
+
};
|
|
204
|
+
loop();
|
|
205
|
+
}
|
|
206
|
+
/** @internal - for tests only. */
|
|
207
|
+
export function _resetSchedulerLoopForTests() {
|
|
208
|
+
schedulerRunning = false;
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=task-scheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-scheduler.js","sourceRoot":"","sources":["../src/task-scheduler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-scheduler.test.d.ts","sourceRoot":"","sources":["../src/task-scheduler.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { _initTestDatabase, createTask, getTaskById } from './db.js';
|
|
3
|
+
import { _resetSchedulerLoopForTests, computeNextRun, startSchedulerLoop, } from './task-scheduler.js';
|
|
4
|
+
describe('task scheduler', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
_initTestDatabase();
|
|
7
|
+
_resetSchedulerLoopForTests();
|
|
8
|
+
vi.useFakeTimers();
|
|
9
|
+
});
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.useRealTimers();
|
|
12
|
+
});
|
|
13
|
+
it('pauses due tasks with invalid group folders to prevent retry churn', async () => {
|
|
14
|
+
createTask({
|
|
15
|
+
id: 'task-invalid-folder',
|
|
16
|
+
group_folder: '../../outside',
|
|
17
|
+
chat_jid: 'bad@g.us',
|
|
18
|
+
prompt: 'run',
|
|
19
|
+
schedule_type: 'once',
|
|
20
|
+
schedule_value: '2026-02-22T00:00:00.000Z',
|
|
21
|
+
context_mode: 'isolated',
|
|
22
|
+
next_run: new Date(Date.now() - 60_000).toISOString(),
|
|
23
|
+
status: 'active',
|
|
24
|
+
created_at: '2026-02-22T00:00:00.000Z',
|
|
25
|
+
});
|
|
26
|
+
const enqueueTask = vi.fn((_groupJid, _taskId, fn) => {
|
|
27
|
+
void fn();
|
|
28
|
+
});
|
|
29
|
+
startSchedulerLoop({
|
|
30
|
+
registeredGroups: () => ({}),
|
|
31
|
+
getSessions: () => ({}),
|
|
32
|
+
queue: { enqueueTask },
|
|
33
|
+
onProcess: () => { },
|
|
34
|
+
sendMessage: async () => { },
|
|
35
|
+
});
|
|
36
|
+
await vi.advanceTimersByTimeAsync(10);
|
|
37
|
+
const task = getTaskById('task-invalid-folder');
|
|
38
|
+
expect(task?.status).toBe('paused');
|
|
39
|
+
});
|
|
40
|
+
it('computeNextRun anchors interval tasks to scheduled time to prevent drift', () => {
|
|
41
|
+
const scheduledTime = new Date(Date.now() - 2000).toISOString(); // 2s ago
|
|
42
|
+
const task = {
|
|
43
|
+
id: 'drift-test',
|
|
44
|
+
group_folder: 'test',
|
|
45
|
+
chat_jid: 'test@g.us',
|
|
46
|
+
prompt: 'test',
|
|
47
|
+
schedule_type: 'interval',
|
|
48
|
+
schedule_value: '60000', // 1 minute
|
|
49
|
+
context_mode: 'isolated',
|
|
50
|
+
next_run: scheduledTime,
|
|
51
|
+
last_run: null,
|
|
52
|
+
last_result: null,
|
|
53
|
+
status: 'active',
|
|
54
|
+
created_at: '2026-01-01T00:00:00.000Z',
|
|
55
|
+
};
|
|
56
|
+
const nextRun = computeNextRun(task);
|
|
57
|
+
expect(nextRun).not.toBeNull();
|
|
58
|
+
// Should be anchored to scheduledTime + 60s, NOT Date.now() + 60s
|
|
59
|
+
const expected = new Date(scheduledTime).getTime() + 60000;
|
|
60
|
+
expect(new Date(nextRun).getTime()).toBe(expected);
|
|
61
|
+
});
|
|
62
|
+
it('computeNextRun returns null for once-tasks', () => {
|
|
63
|
+
const task = {
|
|
64
|
+
id: 'once-test',
|
|
65
|
+
group_folder: 'test',
|
|
66
|
+
chat_jid: 'test@g.us',
|
|
67
|
+
prompt: 'test',
|
|
68
|
+
schedule_type: 'once',
|
|
69
|
+
schedule_value: '2026-01-01T00:00:00.000Z',
|
|
70
|
+
context_mode: 'isolated',
|
|
71
|
+
next_run: new Date(Date.now() - 1000).toISOString(),
|
|
72
|
+
last_run: null,
|
|
73
|
+
last_result: null,
|
|
74
|
+
status: 'active',
|
|
75
|
+
created_at: '2026-01-01T00:00:00.000Z',
|
|
76
|
+
};
|
|
77
|
+
expect(computeNextRun(task)).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
it('computeNextRun skips missed intervals without infinite loop', () => {
|
|
80
|
+
// Task was due 10 intervals ago (missed)
|
|
81
|
+
const ms = 60000;
|
|
82
|
+
const missedBy = ms * 10;
|
|
83
|
+
const scheduledTime = new Date(Date.now() - missedBy).toISOString();
|
|
84
|
+
const task = {
|
|
85
|
+
id: 'skip-test',
|
|
86
|
+
group_folder: 'test',
|
|
87
|
+
chat_jid: 'test@g.us',
|
|
88
|
+
prompt: 'test',
|
|
89
|
+
schedule_type: 'interval',
|
|
90
|
+
schedule_value: String(ms),
|
|
91
|
+
context_mode: 'isolated',
|
|
92
|
+
next_run: scheduledTime,
|
|
93
|
+
last_run: null,
|
|
94
|
+
last_result: null,
|
|
95
|
+
status: 'active',
|
|
96
|
+
created_at: '2026-01-01T00:00:00.000Z',
|
|
97
|
+
};
|
|
98
|
+
const nextRun = computeNextRun(task);
|
|
99
|
+
expect(nextRun).not.toBeNull();
|
|
100
|
+
// Must be in the future
|
|
101
|
+
expect(new Date(nextRun).getTime()).toBeGreaterThan(Date.now());
|
|
102
|
+
// Must be aligned to the original schedule grid
|
|
103
|
+
const offset = (new Date(nextRun).getTime() - new Date(scheduledTime).getTime()) % ms;
|
|
104
|
+
expect(offset).toBe(0);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
//# sourceMappingURL=task-scheduler.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-scheduler.test.js","sourceRoot":"","sources":["../src/task-scheduler.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timezone.d.ts","sourceRoot":"","sources":["../src/timezone.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAWxE"}
|
package/dist/timezone.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a UTC ISO timestamp to a localized display string.
|
|
3
|
+
* Uses the Intl API (no external dependencies).
|
|
4
|
+
*/
|
|
5
|
+
export function formatLocalTime(utcIso, timezone) {
|
|
6
|
+
const date = new Date(utcIso);
|
|
7
|
+
return date.toLocaleString('en-US', {
|
|
8
|
+
timeZone: timezone,
|
|
9
|
+
year: 'numeric',
|
|
10
|
+
month: 'short',
|
|
11
|
+
day: 'numeric',
|
|
12
|
+
hour: 'numeric',
|
|
13
|
+
minute: '2-digit',
|
|
14
|
+
hour12: true,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=timezone.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timezone.js","sourceRoot":"","sources":["../src/timezone.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc,EAAE,QAAgB;IAC9D,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC;IAC9B,OAAO,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE;QAClC,QAAQ,EAAE,QAAQ;QAClB,IAAI,EAAE,SAAS;QACf,KAAK,EAAE,OAAO;QACd,GAAG,EAAE,SAAS;QACd,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,SAAS;QACjB,MAAM,EAAE,IAAI;KACb,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timezone.test.d.ts","sourceRoot":"","sources":["../src/timezone.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { formatLocalTime } from './timezone.js';
|
|
3
|
+
// --- formatLocalTime ---
|
|
4
|
+
describe('formatLocalTime', () => {
|
|
5
|
+
it('converts UTC to local time display', () => {
|
|
6
|
+
// 2026-02-04T18:30:00Z in America/New_York (EST, UTC-5) = 1:30 PM
|
|
7
|
+
const result = formatLocalTime('2026-02-04T18:30:00.000Z', 'America/New_York');
|
|
8
|
+
expect(result).toContain('1:30');
|
|
9
|
+
expect(result).toContain('PM');
|
|
10
|
+
expect(result).toContain('Feb');
|
|
11
|
+
expect(result).toContain('2026');
|
|
12
|
+
});
|
|
13
|
+
it('handles different timezones', () => {
|
|
14
|
+
// Same UTC time should produce different local times
|
|
15
|
+
const utc = '2026-06-15T12:00:00.000Z';
|
|
16
|
+
const ny = formatLocalTime(utc, 'America/New_York');
|
|
17
|
+
const tokyo = formatLocalTime(utc, 'Asia/Tokyo');
|
|
18
|
+
// NY is UTC-4 in summer (EDT), Tokyo is UTC+9
|
|
19
|
+
expect(ny).toContain('8:00');
|
|
20
|
+
expect(tokyo).toContain('9:00');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
//# sourceMappingURL=timezone.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timezone.test.js","sourceRoot":"","sources":["../src/timezone.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEhD,0BAA0B;AAE1B,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,kEAAkE;QAClE,MAAM,MAAM,GAAG,eAAe,CAC5B,0BAA0B,EAC1B,kBAAkB,CACnB,CAAC;QACF,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QACjC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QAC/B,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAChC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;QACrC,qDAAqD;QACrD,MAAM,GAAG,GAAG,0BAA0B,CAAC;QACvC,MAAM,EAAE,GAAG,eAAe,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC;QACpD,MAAM,KAAK,GAAG,eAAe,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;QACjD,8CAA8C;QAC9C,MAAM,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAC7B,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAClC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export interface AdditionalMount {
|
|
2
|
+
hostPath: string;
|
|
3
|
+
containerPath?: string;
|
|
4
|
+
readonly?: boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Mount Allowlist - Security configuration for additional mounts
|
|
8
|
+
* This file should be stored at ~/.config/nanoclaw/mount-allowlist.json
|
|
9
|
+
* and is NOT mounted into any container, making it tamper-proof from agents.
|
|
10
|
+
*/
|
|
11
|
+
export interface MountAllowlist {
|
|
12
|
+
allowedRoots: AllowedRoot[];
|
|
13
|
+
blockedPatterns: string[];
|
|
14
|
+
nonMainReadOnly: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface AllowedRoot {
|
|
17
|
+
path: string;
|
|
18
|
+
allowReadWrite: boolean;
|
|
19
|
+
description?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ContainerConfig {
|
|
22
|
+
additionalMounts?: AdditionalMount[];
|
|
23
|
+
timeout?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface RegisteredGroup {
|
|
26
|
+
name: string;
|
|
27
|
+
folder: string;
|
|
28
|
+
trigger: string;
|
|
29
|
+
added_at: string;
|
|
30
|
+
containerConfig?: ContainerConfig;
|
|
31
|
+
requiresTrigger?: boolean;
|
|
32
|
+
isMain?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface NewMessage {
|
|
35
|
+
id: string;
|
|
36
|
+
chat_jid: string;
|
|
37
|
+
sender: string;
|
|
38
|
+
sender_name: string;
|
|
39
|
+
content: string;
|
|
40
|
+
timestamp: string;
|
|
41
|
+
is_from_me?: boolean;
|
|
42
|
+
is_bot_message?: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface ScheduledTask {
|
|
45
|
+
id: string;
|
|
46
|
+
group_folder: string;
|
|
47
|
+
chat_jid: string;
|
|
48
|
+
prompt: string;
|
|
49
|
+
schedule_type: 'cron' | 'interval' | 'once';
|
|
50
|
+
schedule_value: string;
|
|
51
|
+
context_mode: 'group' | 'isolated';
|
|
52
|
+
next_run: string | null;
|
|
53
|
+
last_run: string | null;
|
|
54
|
+
last_result: string | null;
|
|
55
|
+
status: 'active' | 'paused' | 'completed';
|
|
56
|
+
created_at: string;
|
|
57
|
+
}
|
|
58
|
+
export interface TaskRunLog {
|
|
59
|
+
task_id: string;
|
|
60
|
+
run_at: string;
|
|
61
|
+
duration_ms: number;
|
|
62
|
+
status: 'success' | 'error';
|
|
63
|
+
result: string | null;
|
|
64
|
+
error: string | null;
|
|
65
|
+
}
|
|
66
|
+
export interface Channel {
|
|
67
|
+
name: string;
|
|
68
|
+
connect(): Promise<void>;
|
|
69
|
+
sendMessage(jid: string, text: string): Promise<void>;
|
|
70
|
+
isConnected(): boolean;
|
|
71
|
+
ownsJid(jid: string): boolean;
|
|
72
|
+
disconnect(): Promise<void>;
|
|
73
|
+
setTyping?(jid: string, isTyping: boolean): Promise<void>;
|
|
74
|
+
syncGroups?(force: boolean): Promise<void>;
|
|
75
|
+
}
|
|
76
|
+
export type OnInboundMessage = (chatJid: string, message: NewMessage) => void;
|
|
77
|
+
export type OnChatMetadata = (chatJid: string, timestamp: string, name?: string, channel?: string, isGroup?: boolean) => void;
|
|
78
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Apple Container Networking Setup (macOS 26)
|
|
2
|
+
|
|
3
|
+
Apple Container's vmnet networking requires manual configuration for containers to access the internet. Without this, containers can communicate with the host but cannot reach external services (DNS, HTTPS, APIs).
|
|
4
|
+
|
|
5
|
+
## Quick Setup
|
|
6
|
+
|
|
7
|
+
Run these two commands (requires `sudo`):
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# 1. Enable IP forwarding so the host routes container traffic
|
|
11
|
+
sudo sysctl -w net.inet.ip.forwarding=1
|
|
12
|
+
|
|
13
|
+
# 2. Enable NAT so container traffic gets masqueraded through your internet interface
|
|
14
|
+
echo "nat on en0 from 192.168.64.0/24 to any -> (en0)" | sudo pfctl -ef -
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
> **Note:** Replace `en0` with your active internet interface. Check with: `route get 8.8.8.8 | grep interface`
|
|
18
|
+
|
|
19
|
+
## Making It Persistent
|
|
20
|
+
|
|
21
|
+
These settings reset on reboot. To make them permanent:
|
|
22
|
+
|
|
23
|
+
**IP Forwarding** — add to `/etc/sysctl.conf`:
|
|
24
|
+
```
|
|
25
|
+
net.inet.ip.forwarding=1
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**NAT Rules** — add to `/etc/pf.conf` (before any existing rules):
|
|
29
|
+
```
|
|
30
|
+
nat on en0 from 192.168.64.0/24 to any -> (en0)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then reload: `sudo pfctl -f /etc/pf.conf`
|
|
34
|
+
|
|
35
|
+
## IPv6 DNS Issue
|
|
36
|
+
|
|
37
|
+
By default, DNS resolvers return IPv6 (AAAA) records before IPv4 (A) records. Since our NAT only handles IPv4, Node.js applications inside containers will try IPv6 first and fail.
|
|
38
|
+
|
|
39
|
+
The container image and runner are configured to prefer IPv4 via:
|
|
40
|
+
```
|
|
41
|
+
NODE_OPTIONS=--dns-result-order=ipv4first
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This is set both in the `Dockerfile` and passed via `-e` flag in `container-runner.ts`.
|
|
45
|
+
|
|
46
|
+
## Verification
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Check IP forwarding is enabled
|
|
50
|
+
sysctl net.inet.ip.forwarding
|
|
51
|
+
# Expected: net.inet.ip.forwarding: 1
|
|
52
|
+
|
|
53
|
+
# Test container internet access
|
|
54
|
+
container run --rm --entrypoint curl nanoclaw-agent:latest \
|
|
55
|
+
-s4 --connect-timeout 5 -o /dev/null -w "%{http_code}" https://api.anthropic.com
|
|
56
|
+
# Expected: 404
|
|
57
|
+
|
|
58
|
+
# Check bridge interface (only exists when a container is running)
|
|
59
|
+
ifconfig bridge100
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Troubleshooting
|
|
63
|
+
|
|
64
|
+
| Symptom | Cause | Fix |
|
|
65
|
+
|---------|-------|-----|
|
|
66
|
+
| `curl: (28) Connection timed out` | IP forwarding disabled | `sudo sysctl -w net.inet.ip.forwarding=1` |
|
|
67
|
+
| HTTP works, HTTPS times out | IPv6 DNS resolution | Add `NODE_OPTIONS=--dns-result-order=ipv4first` |
|
|
68
|
+
| `Could not resolve host` | DNS not forwarded | Check bridge100 exists, verify pfctl NAT rules |
|
|
69
|
+
| Container hangs after output | Missing `process.exit(0)` in agent-runner | Rebuild container image |
|
|
70
|
+
|
|
71
|
+
## How It Works
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
Container VM (192.168.64.x)
|
|
75
|
+
│
|
|
76
|
+
├── eth0 → gateway 192.168.64.1
|
|
77
|
+
│
|
|
78
|
+
bridge100 (192.168.64.1) ← host bridge, created by vmnet when container runs
|
|
79
|
+
│
|
|
80
|
+
├── IP forwarding (sysctl) routes packets from bridge100 → en0
|
|
81
|
+
│
|
|
82
|
+
├── NAT (pfctl) masquerades 192.168.64.0/24 → en0's IP
|
|
83
|
+
│
|
|
84
|
+
en0 (your WiFi/Ethernet) → Internet
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## References
|
|
88
|
+
|
|
89
|
+
- [apple/container#469](https://github.com/apple/container/issues/469) — No network from container on macOS 26
|
|
90
|
+
- [apple/container#656](https://github.com/apple/container/issues/656) — Cannot access internet URLs during building
|