@aitne/daemon 0.1.9 → 0.1.11
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/dist/adapters/adapter-watchdog.d.ts +70 -0
- package/dist/adapters/adapter-watchdog.js +115 -0
- package/dist/adapters/discord.d.ts +17 -1
- package/dist/adapters/discord.js +33 -0
- package/dist/adapters/notification-manager.d.ts +27 -1
- package/dist/adapters/notification-manager.js +54 -39
- package/dist/adapters/slack-adapter.d.ts +26 -1
- package/dist/adapters/slack-adapter.js +41 -0
- package/dist/adapters/telegram-adapter.d.ts +18 -1
- package/dist/adapters/telegram-adapter.js +41 -2
- package/dist/adapters/types.d.ts +20 -0
- package/dist/adapters/whatsapp-adapter.d.ts +26 -7
- package/dist/adapters/whatsapp-adapter.js +74 -21
- package/dist/api/env-writer.d.ts +1 -0
- package/dist/api/env-writer.js +17 -7
- package/dist/api/helpers/agent-errors-registry.d.ts +5 -5
- package/dist/api/helpers/agent-errors-registry.js +5 -5
- package/dist/api/routes/agent-schedule.js +5 -1
- package/dist/api/routes/agent.js +33 -12
- package/dist/api/routes/agents/index.js +75 -16
- package/dist/api/routes/agents/views.d.ts +37 -2
- package/dist/api/routes/agents/views.js +64 -2
- package/dist/api/routes/apple-calendar.js +4 -1
- package/dist/api/routes/background-task.d.ts +22 -0
- package/dist/api/routes/background-task.js +338 -0
- package/dist/api/routes/browser-history.js +9 -1
- package/dist/api/routes/calendar.js +12 -2
- package/dist/api/routes/context/path-resolve.js +6 -1
- package/dist/api/routes/context/permissions.js +12 -2
- package/dist/api/routes/context/snapshots.js +0 -3
- package/dist/api/routes/context/write.js +3 -17
- package/dist/api/routes/dashboard/config.js +58 -12
- package/dist/api/routes/dashboard/cost-approvals.js +66 -0
- package/dist/api/routes/dashboard/notifications.js +9 -9
- package/dist/api/routes/dashboard/oauth-google.js +5 -3
- package/dist/api/routes/feedback.d.ts +3 -0
- package/dist/api/routes/feedback.js +349 -0
- package/dist/api/routes/git.js +10 -3
- package/dist/api/routes/github.js +5 -1
- package/dist/api/routes/integrations/crud-patch.js +5 -1
- package/dist/api/routes/integrations-reconcile.js +2 -2
- package/dist/api/routes/mcp.js +65 -13
- package/dist/api/routes/notion.d.ts +1 -1
- package/dist/api/routes/observations.js +7 -7
- package/dist/api/routes/obsidian.d.ts +1 -1
- package/dist/api/routes/receipts.js +5 -1
- package/dist/api/routes/setup-migrate.js +1 -1
- package/dist/api/routes/setup.js +1 -1
- package/dist/api/routes/task-flows.d.ts +1 -1
- package/dist/api/routes/task-flows.js +1 -1
- package/dist/api/routes/tuning.d.ts +29 -0
- package/dist/api/routes/tuning.js +304 -0
- package/dist/api/server.d.ts +44 -16
- package/dist/api/server.js +12 -0
- package/dist/bootstrap/adapters.d.ts +19 -0
- package/dist/bootstrap/adapters.js +61 -0
- package/dist/bootstrap/api.d.ts +5 -3
- package/dist/bootstrap/api.js +45 -13
- package/dist/bootstrap/catchup.d.ts +1 -1
- package/dist/bootstrap/catchup.js +11 -11
- package/dist/bootstrap/event-pipeline.d.ts +11 -0
- package/dist/bootstrap/event-pipeline.js +246 -8
- package/dist/bootstrap/observers.js +9 -6
- package/dist/bootstrap/schedule-helpers.d.ts +104 -6
- package/dist/bootstrap/schedule-helpers.js +172 -19
- package/dist/config.js +32 -12
- package/dist/core/agent-core.d.ts +33 -1
- package/dist/core/agent-core.js +36 -1
- package/dist/core/agents/activity-scan-cadence.d.ts +103 -0
- package/dist/core/agents/activity-scan-cadence.js +127 -0
- package/dist/core/agents/agent-route-override.d.ts +53 -0
- package/dist/core/agents/agent-route-override.js +69 -0
- package/dist/core/agents/builtin-registry.d.ts +51 -14
- package/dist/core/agents/builtin-registry.js +92 -15
- package/dist/core/agents/config-gate-reconcile.d.ts +38 -0
- package/dist/core/agents/config-gate-reconcile.js +51 -0
- package/dist/core/agents/cron-substitute.d.ts +1 -1
- package/dist/core/agents/cron-substitute.js +1 -1
- package/dist/core/agents/custom-routine-migration.d.ts +60 -0
- package/dist/core/agents/custom-routine-migration.js +149 -0
- package/dist/core/agents/firing-blocked.d.ts +1 -1
- package/dist/core/agents/hourly-cadence.d.ts +102 -0
- package/dist/core/agents/hourly-cadence.js +126 -0
- package/dist/core/agents/loader-boot.js +23 -0
- package/dist/core/agents/loader.d.ts +19 -0
- package/dist/core/agents/loader.js +34 -2
- package/dist/core/agents/override-merge.d.ts +1 -1
- package/dist/core/agents/override-merge.js +9 -1
- package/dist/core/agents/recurrence-convert.d.ts +1 -1
- package/dist/core/agents/recurrence-convert.js +1 -1
- package/dist/core/agents/recurring-schedule-adapter.js +8 -0
- package/dist/core/alerts.js +6 -6
- package/dist/core/backends/auth-health-monitor.d.ts +2 -2
- package/dist/core/backends/auth-health-monitor.js +1 -1
- package/dist/core/backends/backend-router.d.ts +27 -1
- package/dist/core/backends/backend-router.js +165 -1
- package/dist/core/backends/claude-code-core.d.ts +71 -31
- package/dist/core/backends/claude-code-core.js +282 -54
- package/dist/core/backends/cli-quota-guards.d.ts +29 -1
- package/dist/core/backends/cli-quota-guards.js +40 -5
- package/dist/core/backends/codex-core.d.ts +6 -0
- package/dist/core/backends/codex-core.js +22 -6
- package/dist/core/backends/failure-spend.d.ts +58 -0
- package/dist/core/backends/failure-spend.js +137 -0
- package/dist/core/backends/gemini-cli-core.d.ts +6 -0
- package/dist/core/backends/gemini-cli-core.js +38 -6
- package/dist/core/backends/model-registry.d.ts +1 -1
- package/dist/core/backends/model-registry.js +4 -4
- package/dist/core/backends/opencode-core.d.ts +1 -1
- package/dist/core/backends/opencode-core.js +5 -5
- package/dist/core/backends/plan-presets.js +47 -18
- package/dist/core/bang-commands/commands-cost.js +3 -1
- package/dist/core/bang-commands/commands-report.js +4 -3
- package/dist/core/bang-commands/commands-research.js +4 -1
- package/dist/core/bang-commands/commands-revert-tuning.d.ts +18 -0
- package/dist/core/bang-commands/commands-revert-tuning.js +63 -0
- package/dist/core/bang-commands/commands-stop-start.js +3 -3
- package/dist/core/bang-commands/commands-task-control.d.ts +19 -0
- package/dist/core/bang-commands/commands-task-control.js +147 -0
- package/dist/core/bang-commands/commands-wiki.js +5 -5
- package/dist/core/bang-commands/index.d.ts +2 -0
- package/dist/core/bang-commands/index.js +12 -0
- package/dist/core/bang-commands/registry.d.ts +12 -0
- package/dist/core/browser-history/research-cluster-fanout.d.ts +28 -14
- package/dist/core/browser-history/research-cluster-fanout.js +39 -16
- package/dist/core/channel-timeline.d.ts +5 -1
- package/dist/core/channel-timeline.js +13 -0
- package/dist/core/context/index-reconciler.js +5 -2
- package/dist/core/context/policy-index-reconciler.d.ts +6 -4
- package/dist/core/context/policy-index-runner.js +25 -6
- package/dist/core/context-builder-calendar.js +10 -2
- package/dist/core/context-builder-conversation.d.ts +8 -1
- package/dist/core/context-builder-conversation.js +41 -7
- package/dist/core/context-builder-yesterday.js +4 -3
- package/dist/core/context-builder.d.ts +7 -2
- package/dist/core/context-builder.js +193 -5
- package/dist/core/context-file-serializer.d.ts +1 -1
- package/dist/core/context-file-serializer.js +1 -1
- package/dist/core/context-health.js +2 -2
- package/dist/core/context-paths.d.ts +11 -1
- package/dist/core/context-paths.js +17 -1
- package/dist/core/context-validation/prepare-write.js +1 -1
- package/dist/core/context-validation/routine-rulebook.d.ts +1 -1
- package/dist/core/context-vault-aliases.d.ts +0 -13
- package/dist/core/context-vault-aliases.js +37 -0
- package/dist/core/custom-routines.d.ts +99 -0
- package/dist/core/custom-routines.js +187 -0
- package/dist/core/daemon-api-cli.js +50 -1
- package/dist/core/day-boundary.d.ts +46 -0
- package/dist/core/day-boundary.js +40 -0
- package/dist/core/dispatcher-activity-scan.d.ts +221 -0
- package/dist/core/dispatcher-activity-scan.js +775 -0
- package/dist/core/dispatcher-error-handling.d.ts +6 -11
- package/dist/core/dispatcher-error-handling.js +38 -62
- package/dist/core/dispatcher-hourly-check.js +6 -1
- package/dist/core/dispatcher-message-handler.d.ts +10 -0
- package/dist/core/dispatcher-message-handler.js +24 -0
- package/dist/core/dispatcher-morning-routine.d.ts +6 -6
- package/dist/core/dispatcher-morning-routine.js +13 -13
- package/dist/core/dispatcher-result-processor.d.ts +33 -0
- package/dist/core/dispatcher-result-processor.js +167 -11
- package/dist/core/dispatcher-scheduled-background-task.d.ts +42 -0
- package/dist/core/dispatcher-scheduled-background-task.js +89 -0
- package/dist/core/dispatcher-scheduled-tasks.d.ts +104 -1
- package/dist/core/dispatcher-scheduled-tasks.js +480 -8
- package/dist/core/dispatcher-task-delivery.d.ts +105 -0
- package/dist/core/dispatcher-task-delivery.js +555 -0
- package/dist/core/dispatcher-types.d.ts +48 -9
- package/dist/core/dispatcher-types.js +3 -3
- package/dist/core/dispatcher.d.ts +112 -31
- package/dist/core/dispatcher.js +297 -60
- package/dist/core/dm-freshness-metrics.d.ts +1 -1
- package/dist/core/drift-effects.js +2 -2
- package/dist/core/feedback/consolidation-prep.d.ts +94 -0
- package/dist/core/feedback/consolidation-prep.js +254 -0
- package/dist/core/feedback/eviction-scorer.d.ts +81 -0
- package/dist/core/feedback/eviction-scorer.js +136 -0
- package/dist/core/feedback/lesson-format.d.ts +79 -0
- package/dist/core/feedback/lesson-format.js +199 -0
- package/dist/core/feedback/lesson-injection.d.ts +98 -0
- package/dist/core/feedback/lesson-injection.js +174 -0
- package/dist/core/feedback/lesson-merge.d.ts +51 -0
- package/dist/core/feedback/lesson-merge.js +88 -0
- package/dist/core/feedback/lesson-store-overview.d.ts +46 -0
- package/dist/core/feedback/lesson-store-overview.js +42 -0
- package/dist/core/feedback/promotion-gate.d.ts +69 -0
- package/dist/core/feedback/promotion-gate.js +117 -0
- package/dist/core/feedback/regeneralization-prep.d.ts +87 -0
- package/dist/core/feedback/regeneralization-prep.js +152 -0
- package/dist/core/feedback/scope-parser.d.ts +86 -0
- package/dist/core/feedback/scope-parser.js +141 -0
- package/dist/core/feedback/self-performance-prep.d.ts +186 -0
- package/dist/core/feedback/self-performance-prep.js +541 -0
- package/dist/core/feedback/tuning-actuator.d.ts +198 -0
- package/dist/core/feedback/tuning-actuator.js +432 -0
- package/dist/core/feedback/tuning-recommender.d.ts +247 -0
- package/dist/core/feedback/tuning-recommender.js +580 -0
- package/dist/core/feedback/tuning-revert-monitor.d.ts +90 -0
- package/dist/core/feedback/tuning-revert-monitor.js +213 -0
- package/dist/core/health-monitor.d.ts +6 -0
- package/dist/core/health-monitor.js +1 -1
- package/dist/core/injection-policy.d.ts +83 -1
- package/dist/core/injection-policy.js +61 -3
- package/dist/core/integration-main-backend.js +4 -0
- package/dist/core/management-md.d.ts +2 -2
- package/dist/core/management-md.js +51 -13
- package/dist/core/morning/orchestrator.d.ts +2 -2
- package/dist/core/morning/orchestrator.js +2 -2
- package/dist/core/notification-gate.d.ts +64 -0
- package/dist/core/notification-gate.js +51 -0
- package/dist/core/notification-rate-limit.d.ts +40 -0
- package/dist/core/notification-rate-limit.js +50 -0
- package/dist/core/policy-files.d.ts +1 -1
- package/dist/core/policy-files.js +2 -2
- package/dist/core/pre-pass-freshness.d.ts +4 -4
- package/dist/core/retention.d.ts +5 -0
- package/dist/core/retention.js +20 -4
- package/dist/core/review-context.d.ts +1 -1
- package/dist/core/review-context.js +10 -5
- package/dist/core/roadmap-write-lock.d.ts +2 -1
- package/dist/core/roadmap-write-lock.js +15 -10
- package/dist/core/routine-acquisition-plan.d.ts +47 -1
- package/dist/core/routine-acquisition-plan.js +78 -20
- package/dist/core/routine-fetch-window-retry.js +7 -4
- package/dist/core/routine-fetch-window-runner.d.ts +39 -3
- package/dist/core/routine-fetch-window-runner.js +264 -13
- package/dist/core/routine-windows.d.ts +2 -2
- package/dist/core/routine-windows.js +8 -5
- package/dist/core/scheduler.d.ts +175 -16
- package/dist/core/scheduler.js +559 -102
- package/dist/core/signal-detector.d.ts +51 -1
- package/dist/core/signal-detector.js +321 -24
- package/dist/core/skills-compiler-denied-tools.js +2 -2
- package/dist/core/skills-compiler-skill-index.d.ts +2 -2
- package/dist/core/skills-compiler-skill-index.js +2 -2
- package/dist/core/skills-compiler-variants.d.ts +1 -1
- package/dist/core/skills-compiler-variants.js +8 -0
- package/dist/core/skills-compiler.d.ts +29 -26
- package/dist/core/skills-compiler.js +117 -81
- package/dist/core/skills-manifest.d.ts +37 -0
- package/dist/core/skills-manifest.js +73 -2
- package/dist/core/sleep-inhibitor.d.ts +79 -0
- package/dist/core/sleep-inhibitor.js +132 -0
- package/dist/core/slim-system-prompt-loader.d.ts +77 -0
- package/dist/core/slim-system-prompt-loader.js +141 -0
- package/dist/core/spawn-gates.d.ts +126 -0
- package/dist/core/spawn-gates.js +180 -0
- package/dist/core/today-direct-writer.d.ts +60 -14
- package/dist/core/today-direct-writer.js +90 -13
- package/dist/core/today-write-lock.d.ts +4 -2
- package/dist/core/today-write-lock.js +30 -20
- package/dist/core/wake-detector.d.ts +55 -0
- package/dist/core/wake-detector.js +80 -0
- package/dist/core/wiki/compile-lock.d.ts +1 -1
- package/dist/core/wiki/compile-lock.js +1 -1
- package/dist/core/wiki/wiki-fts.js +13 -6
- package/dist/core/workdir.js +15 -6
- package/dist/db/activity-scan-signals.d.ts +77 -0
- package/dist/db/activity-scan-signals.js +378 -0
- package/dist/db/agents-store.d.ts +28 -0
- package/dist/db/agents-store.js +62 -0
- package/dist/db/background-task-clarifications-store.d.ts +81 -0
- package/dist/db/background-task-clarifications-store.js +152 -0
- package/dist/db/background-task-store.d.ts +207 -0
- package/dist/db/background-task-store.js +380 -0
- package/dist/db/browser-history-store.d.ts +39 -6
- package/dist/db/browser-history-store.js +51 -7
- package/dist/db/browser-task-clarifications-store.d.ts +12 -0
- package/dist/db/browser-task-clarifications-store.js +35 -5
- package/dist/db/browser-task-store.d.ts +3 -0
- package/dist/db/browser-task-store.js +29 -4
- package/dist/db/deferred-dm.d.ts +86 -0
- package/dist/db/deferred-dm.js +199 -0
- package/dist/db/feedback-signals-store.d.ts +77 -0
- package/dist/db/feedback-signals-store.js +144 -0
- package/dist/db/migrations.js +380 -0
- package/dist/db/observations.d.ts +2 -2
- package/dist/db/observations.js +3 -3
- package/dist/db/schema.js +260 -22
- package/dist/db/voice-transcripts-store.d.ts +1 -1
- package/dist/index.js +86 -29
- package/dist/messaging/browser-task-mcp-notifier.d.ts +12 -70
- package/dist/messaging/browser-task-mcp-notifier.js +30 -151
- package/dist/messaging/browser-task-screenshot-attachment.d.ts +15 -0
- package/dist/messaging/browser-task-screenshot-attachment.js +63 -0
- package/dist/observers/delegated-sync-worker.d.ts +6 -6
- package/dist/observers/delegated-sync-worker.js +10 -10
- package/dist/observers/git-delegated-cron.d.ts +1 -1
- package/dist/observers/git-delegated-cron.js +2 -2
- package/dist/observers/github-poller-classifier.d.ts +3 -3
- package/dist/observers/github-poller-classifier.js +3 -3
- package/dist/observers/imminent-event-scheduler.d.ts +1 -1
- package/dist/observers/imminent-event-scheduler.js +1 -1
- package/dist/observers/mail-poller.d.ts +1 -0
- package/dist/observers/mail-poller.js +42 -3
- package/dist/observers/observation-summarizer/summarizer-client.d.ts +2 -2
- package/dist/observers/observation-summarizer/summarizer-client.js +2 -2
- package/dist/observers/observation-summarizer/worker.d.ts +2 -2
- package/dist/observers/observation-summarizer/worker.js +4 -4
- package/dist/observers/obsidian-watcher.d.ts +1 -1
- package/dist/observers/obsidian-watcher.js +1 -1
- package/dist/safety/agent-write-tracker.d.ts +4 -4
- package/dist/safety/agent-write-tracker.js +4 -4
- package/dist/safety/always-disallowed.d.ts +1 -1
- package/dist/safety/always-disallowed.js +39 -0
- package/dist/safety/audit.d.ts +43 -5
- package/dist/safety/audit.js +86 -18
- package/dist/safety/risk-classifier.d.ts +6 -0
- package/dist/safety/risk-classifier.js +97 -18
- package/dist/scheduler/activity-scan-gate.d.ts +86 -0
- package/dist/scheduler/activity-scan-gate.js +132 -0
- package/dist/services/background-task/background-task-budget.d.ts +80 -0
- package/dist/services/background-task/background-task-budget.js +91 -0
- package/dist/services/background-task/background-task-driver.d.ts +105 -0
- package/dist/services/background-task/background-task-driver.js +416 -0
- package/dist/services/background-task/background-task-runner.d.ts +96 -0
- package/dist/services/background-task/background-task-runner.js +673 -0
- package/dist/services/background-task/background-task-tools.d.ts +84 -0
- package/dist/services/background-task/background-task-tools.js +247 -0
- package/dist/services/background-task/background-task-transition-events.d.ts +43 -0
- package/dist/services/background-task/background-task-transition-events.js +54 -0
- package/dist/services/browser-history/automation/egress-denylist.d.ts +1 -1
- package/dist/services/browser-history/automation/egress-denylist.js +34 -8
- package/dist/services/browser-history/lifecycle/platform.js +44 -2
- package/dist/services/browser-history/managed-chromium/sandbox-launcher.js +0 -1
- package/dist/services/browser-task/browser-task-runner.js +53 -8
- package/dist/services/mcp/probe.js +30 -8
- package/dist/services/observations-batch.d.ts +1 -1
- package/dist/services/observations-batch.js +2 -2
- package/dist/settings/runtime-settings.d.ts +45 -12
- package/dist/settings/runtime-settings.js +215 -40
- package/dist/settings/settings-store.js +11 -3
- package/package.json +4 -4
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background-task worker tools — BACKGROUND_TASK_RUNNER_DESIGN.md §4.1 / §4.3.
|
|
3
|
+
*
|
|
4
|
+
* The generic worker's tool envelope. Unlike browser-task's 11-tool
|
|
5
|
+
* Playwright plane, the background worker gets exactly three tools:
|
|
6
|
+
*
|
|
7
|
+
* - `read_memory(key)` — READ-ONLY access to an allowlisted set of
|
|
8
|
+
* owner memory / profile files so the worker can personalize results
|
|
9
|
+
* itself rather than the brief enumerating everything (§9 / §10.4).
|
|
10
|
+
* The worker MUST NOT write shared memory — results come back as the
|
|
11
|
+
* artifact and the DM agent persists anything memory-worthy.
|
|
12
|
+
* - `ask_user(question, contextSummary)` — write a clarification
|
|
13
|
+
* artifact, park the task (`awaiting_user`), and end the turn. The
|
|
14
|
+
* runner surfaces it through the gated delivery boundary.
|
|
15
|
+
* - `finish(result, draft, notify, significance?)` — WRITE THE ARTIFACT
|
|
16
|
+
* (verbatim `result` + plain `draft` summary + the `notify`
|
|
17
|
+
* disposition the worker evaluated against the spawn-time policy) and
|
|
18
|
+
* complete the task. The runner's reconcile hook reads the artifact
|
|
19
|
+
* and decides delivery.
|
|
20
|
+
*
|
|
21
|
+
* The tools do NOT send DMs or enqueue delivery — they only write to the
|
|
22
|
+
* task store + transition state. Delivery is the runner's job (it owns
|
|
23
|
+
* the `notify` gate + the `task.delivery` enqueue), keeping the
|
|
24
|
+
* disposition decision in one place. This module is store-write glue,
|
|
25
|
+
* excluded from the coverage gate; the pure decision (`notify` evaluation)
|
|
26
|
+
* is the worker's, and the budget arithmetic is covered separately.
|
|
27
|
+
*/
|
|
28
|
+
import type Database from "better-sqlite3";
|
|
29
|
+
import { type McpSdkServerConfigWithInstance } from "@anthropic-ai/claude-agent-sdk";
|
|
30
|
+
import { z } from "zod";
|
|
31
|
+
import { type BackgroundTaskTransitionEmitter } from "./background-task-transition-events.js";
|
|
32
|
+
export declare const BACKGROUND_TASK_MCP_SERVER_NAME = "aitne-task";
|
|
33
|
+
export declare const BACKGROUND_TASK_TOOL_FQNS: readonly ["mcp__aitne-task__read_memory", "mcp__aitne-task__ask_user", "mcp__aitne-task__finish"];
|
|
34
|
+
export declare const MEMORY_KEYS: readonly string[];
|
|
35
|
+
export interface BackgroundTaskRuntime {
|
|
36
|
+
taskId: string;
|
|
37
|
+
db: Database.Database;
|
|
38
|
+
/** Vault root for `read_memory`. */
|
|
39
|
+
contextDir: string;
|
|
40
|
+
/** Clarification TTL in ms (from `backgroundTaskClarificationTtlMinutes`). */
|
|
41
|
+
clarificationTtlMs: number;
|
|
42
|
+
transitionEmitter: BackgroundTaskTransitionEmitter;
|
|
43
|
+
abortSignal: AbortSignal;
|
|
44
|
+
/** Set true once `ask_user` parks the task — read by the runner's
|
|
45
|
+
* post-execute hook to distinguish a clean park from a hang. */
|
|
46
|
+
yieldFlag: {
|
|
47
|
+
current: boolean;
|
|
48
|
+
};
|
|
49
|
+
/** Set true once `finish` writes the artifact — read by the runner to
|
|
50
|
+
* confirm a clean completion vs an SDK-side natural end. */
|
|
51
|
+
finishFlag: {
|
|
52
|
+
current: boolean;
|
|
53
|
+
};
|
|
54
|
+
nowFn?: () => number;
|
|
55
|
+
}
|
|
56
|
+
export declare function createBackgroundTaskRuntime(input: {
|
|
57
|
+
taskId: string;
|
|
58
|
+
db: Database.Database;
|
|
59
|
+
contextDir: string;
|
|
60
|
+
clarificationTtlMs: number;
|
|
61
|
+
abortSignal: AbortSignal;
|
|
62
|
+
transitionEmitter?: BackgroundTaskTransitionEmitter;
|
|
63
|
+
nowFn?: () => number;
|
|
64
|
+
}): BackgroundTaskRuntime;
|
|
65
|
+
/** The three worker tools, bound to a runtime. Exported so tests can
|
|
66
|
+
* invoke a tool's `handler` directly without standing up the MCP
|
|
67
|
+
* transport. */
|
|
68
|
+
export declare function createBackgroundTaskTools(runtime: BackgroundTaskRuntime): (import("@anthropic-ai/claude-agent-sdk").SdkMcpToolDefinition<{
|
|
69
|
+
key: z.ZodEnum<{
|
|
70
|
+
[x: string]: string;
|
|
71
|
+
}>;
|
|
72
|
+
}> | import("@anthropic-ai/claude-agent-sdk").SdkMcpToolDefinition<{
|
|
73
|
+
question: z.ZodString;
|
|
74
|
+
contextSummary: z.ZodOptional<z.ZodString>;
|
|
75
|
+
}> | import("@anthropic-ai/claude-agent-sdk").SdkMcpToolDefinition<{
|
|
76
|
+
result: z.ZodString;
|
|
77
|
+
draft: z.ZodString;
|
|
78
|
+
notify: z.ZodBoolean;
|
|
79
|
+
significance: z.ZodOptional<z.ZodString>;
|
|
80
|
+
}>)[];
|
|
81
|
+
/** Construct the per-task MCP server. Returned config is passed verbatim
|
|
82
|
+
* into `query({ options: { mcpServers: { [BACKGROUND_TASK_MCP_SERVER_NAME]:
|
|
83
|
+
* <return value> } } })`. */
|
|
84
|
+
export declare function createBackgroundTaskMcpServer(runtime: BackgroundTaskRuntime): McpSdkServerConfigWithInstance;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background-task worker tools — BACKGROUND_TASK_RUNNER_DESIGN.md §4.1 / §4.3.
|
|
3
|
+
*
|
|
4
|
+
* The generic worker's tool envelope. Unlike browser-task's 11-tool
|
|
5
|
+
* Playwright plane, the background worker gets exactly three tools:
|
|
6
|
+
*
|
|
7
|
+
* - `read_memory(key)` — READ-ONLY access to an allowlisted set of
|
|
8
|
+
* owner memory / profile files so the worker can personalize results
|
|
9
|
+
* itself rather than the brief enumerating everything (§9 / §10.4).
|
|
10
|
+
* The worker MUST NOT write shared memory — results come back as the
|
|
11
|
+
* artifact and the DM agent persists anything memory-worthy.
|
|
12
|
+
* - `ask_user(question, contextSummary)` — write a clarification
|
|
13
|
+
* artifact, park the task (`awaiting_user`), and end the turn. The
|
|
14
|
+
* runner surfaces it through the gated delivery boundary.
|
|
15
|
+
* - `finish(result, draft, notify, significance?)` — WRITE THE ARTIFACT
|
|
16
|
+
* (verbatim `result` + plain `draft` summary + the `notify`
|
|
17
|
+
* disposition the worker evaluated against the spawn-time policy) and
|
|
18
|
+
* complete the task. The runner's reconcile hook reads the artifact
|
|
19
|
+
* and decides delivery.
|
|
20
|
+
*
|
|
21
|
+
* The tools do NOT send DMs or enqueue delivery — they only write to the
|
|
22
|
+
* task store + transition state. Delivery is the runner's job (it owns
|
|
23
|
+
* the `notify` gate + the `task.delivery` enqueue), keeping the
|
|
24
|
+
* disposition decision in one place. This module is store-write glue,
|
|
25
|
+
* excluded from the coverage gate; the pure decision (`notify` evaluation)
|
|
26
|
+
* is the worker's, and the budget arithmetic is covered separately.
|
|
27
|
+
*/
|
|
28
|
+
import { readFile } from "node:fs/promises";
|
|
29
|
+
import { randomUUID } from "node:crypto";
|
|
30
|
+
import { createSdkMcpServer, tool, } from "@anthropic-ai/claude-agent-sdk";
|
|
31
|
+
import { z } from "zod";
|
|
32
|
+
import { createClarification } from "../../db/background-task-clarifications-store.js";
|
|
33
|
+
import { markAwaitingUser, markTerminal, } from "../../db/background-task-store.js";
|
|
34
|
+
import { CONTEXT_RELATIVE_PATHS, fullPath } from "../../core/context-paths.js";
|
|
35
|
+
import { noopBackgroundTaskTransitionEmitter, } from "./background-task-transition-events.js";
|
|
36
|
+
import { createLogger } from "../../logging.js";
|
|
37
|
+
const logger = createLogger("background-task-tools");
|
|
38
|
+
export const BACKGROUND_TASK_MCP_SERVER_NAME = "aitne-task";
|
|
39
|
+
export const BACKGROUND_TASK_TOOL_FQNS = [
|
|
40
|
+
`mcp__${BACKGROUND_TASK_MCP_SERVER_NAME}__read_memory`,
|
|
41
|
+
`mcp__${BACKGROUND_TASK_MCP_SERVER_NAME}__ask_user`,
|
|
42
|
+
`mcp__${BACKGROUND_TASK_MCP_SERVER_NAME}__finish`,
|
|
43
|
+
];
|
|
44
|
+
/** Per-read output cap so a large memory file can't blow the worker's
|
|
45
|
+
* context window or budget in one tool call. */
|
|
46
|
+
const MEMORY_READ_CHAR_CAP = 8_000;
|
|
47
|
+
/**
|
|
48
|
+
* Allowlist of READ-ONLY memory keys → vault-relative paths. A fixed
|
|
49
|
+
* enum (no user-controlled path component) so there is no traversal
|
|
50
|
+
* surface. Single files only; the worker reads what it needs to
|
|
51
|
+
* personalize a result (the owner's profile, today's state, project
|
|
52
|
+
* context, the management policy).
|
|
53
|
+
*/
|
|
54
|
+
const MEMORY_FILE_ALLOWLIST = {
|
|
55
|
+
today: CONTEXT_RELATIVE_PATHS.today,
|
|
56
|
+
profile: CONTEXT_RELATIVE_PATHS.user.profile,
|
|
57
|
+
people: CONTEXT_RELATIVE_PATHS.user.people,
|
|
58
|
+
work: CONTEXT_RELATIVE_PATHS.user.work,
|
|
59
|
+
goals: CONTEXT_RELATIVE_PATHS.user.goals,
|
|
60
|
+
projects: CONTEXT_RELATIVE_PATHS.projects.index,
|
|
61
|
+
management: CONTEXT_RELATIVE_PATHS.rules.management,
|
|
62
|
+
integrations: CONTEXT_RELATIVE_PATHS.integrations,
|
|
63
|
+
};
|
|
64
|
+
export const MEMORY_KEYS = Object.keys(MEMORY_FILE_ALLOWLIST);
|
|
65
|
+
// The SDK `tool()` helper takes a Zod RAW SHAPE (a `{ key: ZodType }`
|
|
66
|
+
// object), not a `z.object(...)` — mirroring browser-task's schemas.
|
|
67
|
+
const readMemoryArgsSchema = {
|
|
68
|
+
key: z
|
|
69
|
+
.enum(Object.keys(MEMORY_FILE_ALLOWLIST))
|
|
70
|
+
.describe("Which owner memory file to read. One of: "
|
|
71
|
+
+ MEMORY_KEYS.join(", ")
|
|
72
|
+
+ ". Read-only."),
|
|
73
|
+
};
|
|
74
|
+
const askUserArgsSchema = {
|
|
75
|
+
question: z
|
|
76
|
+
.string()
|
|
77
|
+
.min(1)
|
|
78
|
+
.max(2_000)
|
|
79
|
+
.describe("The clarification you need from the owner, phrased plainly. The DM agent weaves this into the conversation."),
|
|
80
|
+
contextSummary: z
|
|
81
|
+
.string()
|
|
82
|
+
.max(2_000)
|
|
83
|
+
.optional()
|
|
84
|
+
.describe("Optional one-paragraph recap of where the task is and why you're stuck, so the owner can answer without re-reading the whole brief."),
|
|
85
|
+
};
|
|
86
|
+
const finishArgsSchema = {
|
|
87
|
+
result: z
|
|
88
|
+
.string()
|
|
89
|
+
.min(1)
|
|
90
|
+
.max(100_000)
|
|
91
|
+
.describe("The FULL, verbatim outcome — every finding, number, URL, and id. Persisted unchanged as the fidelity anchor; precise follow-ups read this. Do NOT summarize here."),
|
|
92
|
+
draft: z
|
|
93
|
+
.string()
|
|
94
|
+
.min(1)
|
|
95
|
+
.max(4_000)
|
|
96
|
+
.describe("A plain, human-readable summary in the owner's language. NOT the final DM — the DM agent uses this as grounding / the idle-send body. 1-4 short paragraphs."),
|
|
97
|
+
notify: z
|
|
98
|
+
.boolean()
|
|
99
|
+
.describe("Your disposition vs the spawn-time notification policy. always ⇒ true (even for a '0 issues' result — the owner asked). if_significant ⇒ true ONLY if the brief's concrete criteria are met. silent ⇒ false. When unsure on always, prefer true."),
|
|
100
|
+
significance: z
|
|
101
|
+
.string()
|
|
102
|
+
.max(500)
|
|
103
|
+
.optional()
|
|
104
|
+
.describe("One line on why notify is true/false (e.g. '2 repos red' / 'no criteria met'). Used in the filed-results digest + audit."),
|
|
105
|
+
};
|
|
106
|
+
export function createBackgroundTaskRuntime(input) {
|
|
107
|
+
return {
|
|
108
|
+
taskId: input.taskId,
|
|
109
|
+
db: input.db,
|
|
110
|
+
contextDir: input.contextDir,
|
|
111
|
+
clarificationTtlMs: input.clarificationTtlMs,
|
|
112
|
+
abortSignal: input.abortSignal,
|
|
113
|
+
transitionEmitter: input.transitionEmitter ?? noopBackgroundTaskTransitionEmitter,
|
|
114
|
+
yieldFlag: { current: false },
|
|
115
|
+
finishFlag: { current: false },
|
|
116
|
+
nowFn: input.nowFn,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function textResult(payload, isError = false) {
|
|
120
|
+
return {
|
|
121
|
+
isError,
|
|
122
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function makeReadMemoryTool(runtime) {
|
|
126
|
+
return tool("read_memory", "Read one owner memory / profile file (read-only) to personalize your result. Keys: "
|
|
127
|
+
+ MEMORY_KEYS.join(", ")
|
|
128
|
+
+ ". You cannot write memory — return everything memory-worthy in finish().", readMemoryArgsSchema, async (args) => {
|
|
129
|
+
const relative = MEMORY_FILE_ALLOWLIST[args.key];
|
|
130
|
+
if (!relative) {
|
|
131
|
+
return textResult({ ok: false, error: "unknown_key", detail: `key must be one of: ${MEMORY_KEYS.join(", ")}` }, true);
|
|
132
|
+
}
|
|
133
|
+
const path = fullPath(runtime.contextDir, relative);
|
|
134
|
+
try {
|
|
135
|
+
const raw = await readFile(path, "utf-8");
|
|
136
|
+
const truncated = raw.length > MEMORY_READ_CHAR_CAP;
|
|
137
|
+
const content = truncated ? raw.slice(0, MEMORY_READ_CHAR_CAP) : raw;
|
|
138
|
+
return textResult({
|
|
139
|
+
ok: true,
|
|
140
|
+
key: args.key,
|
|
141
|
+
truncated,
|
|
142
|
+
content: truncated
|
|
143
|
+
? `${content}\n\n[... truncated at ${MEMORY_READ_CHAR_CAP} chars]`
|
|
144
|
+
: content,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Missing file is normal (a fresh vault may not have every file).
|
|
149
|
+
return textResult({
|
|
150
|
+
ok: true,
|
|
151
|
+
key: args.key,
|
|
152
|
+
truncated: false,
|
|
153
|
+
content: "",
|
|
154
|
+
note: "file not present in the vault yet",
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
function makeAskUserTool(runtime) {
|
|
160
|
+
return tool("ask_user", "Pause for an owner clarification. Writes the question, parks your task, and ends the turn — the DM agent surfaces it and relays the answer back so you resume. Call this and then STOP; do not call further tools this turn.", askUserArgsSchema, async (args) => {
|
|
161
|
+
const now = (runtime.nowFn ?? (() => Date.now()))();
|
|
162
|
+
// running → awaiting_user CAS before writing the clarification row.
|
|
163
|
+
// On a CAS miss the task already transitioned (cancel-while-running)
|
|
164
|
+
// — bail without committing an orphan row the deadline tick would
|
|
165
|
+
// later process for a terminal task.
|
|
166
|
+
const parked = markAwaitingUser(runtime.db, runtime.taskId);
|
|
167
|
+
if (!parked) {
|
|
168
|
+
return textResult({
|
|
169
|
+
ok: false,
|
|
170
|
+
error: "task_not_running",
|
|
171
|
+
detail: "This task is no longer running (it may have been cancelled). Stop now.",
|
|
172
|
+
}, true);
|
|
173
|
+
}
|
|
174
|
+
const id = randomUUID();
|
|
175
|
+
const row = createClarification(runtime.db, {
|
|
176
|
+
id,
|
|
177
|
+
taskId: runtime.taskId,
|
|
178
|
+
question: args.question,
|
|
179
|
+
contextSummary: args.contextSummary ?? null,
|
|
180
|
+
askedAt: now,
|
|
181
|
+
ttlMs: runtime.clarificationTtlMs,
|
|
182
|
+
});
|
|
183
|
+
runtime.yieldFlag.current = true;
|
|
184
|
+
runtime.transitionEmitter.emitFromRow(parked, now);
|
|
185
|
+
return textResult({
|
|
186
|
+
ok: true,
|
|
187
|
+
status: "parked",
|
|
188
|
+
clarificationId: id,
|
|
189
|
+
deadlineAt: row.deadlineAt,
|
|
190
|
+
note: "Your task is parked. STOP now — the owner's answer will resume you.",
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
function makeFinishTool(runtime) {
|
|
195
|
+
return tool("finish", "Done. Writes your artifact (verbatim result + plain draft summary + the notify disposition) and completes the task. Do not call any tool after finish — your session ends here.", finishArgsSchema, async (args) => {
|
|
196
|
+
const now = (runtime.nowFn ?? (() => Date.now()))();
|
|
197
|
+
const terminal = markTerminal(runtime.db, {
|
|
198
|
+
id: runtime.taskId,
|
|
199
|
+
state: "completed",
|
|
200
|
+
outcomeDetail: null,
|
|
201
|
+
finishedAt: now,
|
|
202
|
+
report: args.result,
|
|
203
|
+
draft: args.draft,
|
|
204
|
+
notify: args.notify,
|
|
205
|
+
significance: args.significance ?? null,
|
|
206
|
+
});
|
|
207
|
+
if (!terminal) {
|
|
208
|
+
// CAS miss — the task was already cancelled / timed out. Surface
|
|
209
|
+
// it so the agent stops; the artifact is intentionally not forced
|
|
210
|
+
// onto a terminal row.
|
|
211
|
+
return textResult({
|
|
212
|
+
ok: false,
|
|
213
|
+
error: "task_not_active",
|
|
214
|
+
detail: "This task already reached a terminal state; the result was not stored. Stop now.",
|
|
215
|
+
}, true);
|
|
216
|
+
}
|
|
217
|
+
runtime.finishFlag.current = true;
|
|
218
|
+
runtime.transitionEmitter.emitFromRow(terminal, now);
|
|
219
|
+
return textResult({
|
|
220
|
+
ok: true,
|
|
221
|
+
completed: true,
|
|
222
|
+
notify: args.notify,
|
|
223
|
+
state: terminal.state,
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/** The three worker tools, bound to a runtime. Exported so tests can
|
|
228
|
+
* invoke a tool's `handler` directly without standing up the MCP
|
|
229
|
+
* transport. */
|
|
230
|
+
export function createBackgroundTaskTools(runtime) {
|
|
231
|
+
return [
|
|
232
|
+
makeReadMemoryTool(runtime),
|
|
233
|
+
makeAskUserTool(runtime),
|
|
234
|
+
makeFinishTool(runtime),
|
|
235
|
+
];
|
|
236
|
+
}
|
|
237
|
+
/** Construct the per-task MCP server. Returned config is passed verbatim
|
|
238
|
+
* into `query({ options: { mcpServers: { [BACKGROUND_TASK_MCP_SERVER_NAME]:
|
|
239
|
+
* <return value> } } })`. */
|
|
240
|
+
export function createBackgroundTaskMcpServer(runtime) {
|
|
241
|
+
return createSdkMcpServer({
|
|
242
|
+
name: BACKGROUND_TASK_MCP_SERVER_NAME,
|
|
243
|
+
version: "1.0.0",
|
|
244
|
+
tools: createBackgroundTaskTools(runtime),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
void logger;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background-task SSE transition emitter — BACKGROUND_TASK_RUNNER_DESIGN.md
|
|
3
|
+
* §15 (dashboard). Emits a `background_task` named event on the global
|
|
4
|
+
* `/api/events/stream` on every state transition so a future dashboard
|
|
5
|
+
* surface can invalidate its list/detail queries without per-id polling.
|
|
6
|
+
*
|
|
7
|
+
* Per `project_dashboard_testing`: `background_task` arrives on the
|
|
8
|
+
* default `event` stream with a `kind` field — here the named-event
|
|
9
|
+
* channel is `"background_task"`, payload bounded and ASCII-safe.
|
|
10
|
+
*
|
|
11
|
+
* Mirrors `browser-task-transition-events.ts`: a thin telemetry interface
|
|
12
|
+
* separate from the delivery path (which fans user-facing DMs). The
|
|
13
|
+
* payload never carries the full report — only an 80-char title/brief.
|
|
14
|
+
*/
|
|
15
|
+
import type { BackgroundTaskRow } from "../../db/background-task-store.js";
|
|
16
|
+
export interface BackgroundTaskTransitionPayload {
|
|
17
|
+
taskId: string;
|
|
18
|
+
state: BackgroundTaskRow["state"];
|
|
19
|
+
/** Epoch ms — finishedAt for terminal rows, startedAt for running,
|
|
20
|
+
* createdAt for pending. Cache-busting timestamp only. */
|
|
21
|
+
transitionedAt: number;
|
|
22
|
+
/** First 80 chars of the title (or brief), control chars scrubbed. */
|
|
23
|
+
brief: string;
|
|
24
|
+
outcomeDetail: string | null;
|
|
25
|
+
/** Worker disposition once finished (null while in-flight). */
|
|
26
|
+
notify: boolean | null;
|
|
27
|
+
originatingChannel: string | null;
|
|
28
|
+
}
|
|
29
|
+
/** Minimal broadcaster surface — the impl lives in `api/routes/sse.ts`.
|
|
30
|
+
* Declared structurally so the daemon's `services/background-task/*`
|
|
31
|
+
* modules don't depend on the HTTP layer. */
|
|
32
|
+
export interface BroadcastSink {
|
|
33
|
+
broadcastNamedEvent(event: string, data: unknown): Promise<void> | void;
|
|
34
|
+
}
|
|
35
|
+
export interface BackgroundTaskTransitionEmitter {
|
|
36
|
+
emit(payload: BackgroundTaskTransitionPayload): void;
|
|
37
|
+
/** Extract fields from a row + transitionedAt and emit. Returns the
|
|
38
|
+
* payload (or null when row is null) for test chaining. */
|
|
39
|
+
emitFromRow(row: BackgroundTaskRow | null, transitionedAt: number): BackgroundTaskTransitionPayload | null;
|
|
40
|
+
}
|
|
41
|
+
export declare function briefPayload(row: BackgroundTaskRow, transitionedAt: number): BackgroundTaskTransitionPayload;
|
|
42
|
+
export declare const noopBackgroundTaskTransitionEmitter: BackgroundTaskTransitionEmitter;
|
|
43
|
+
export declare function createBackgroundTaskTransitionEmitter(sink: BroadcastSink | null | undefined): BackgroundTaskTransitionEmitter;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background-task SSE transition emitter — BACKGROUND_TASK_RUNNER_DESIGN.md
|
|
3
|
+
* §15 (dashboard). Emits a `background_task` named event on the global
|
|
4
|
+
* `/api/events/stream` on every state transition so a future dashboard
|
|
5
|
+
* surface can invalidate its list/detail queries without per-id polling.
|
|
6
|
+
*
|
|
7
|
+
* Per `project_dashboard_testing`: `background_task` arrives on the
|
|
8
|
+
* default `event` stream with a `kind` field — here the named-event
|
|
9
|
+
* channel is `"background_task"`, payload bounded and ASCII-safe.
|
|
10
|
+
*
|
|
11
|
+
* Mirrors `browser-task-transition-events.ts`: a thin telemetry interface
|
|
12
|
+
* separate from the delivery path (which fans user-facing DMs). The
|
|
13
|
+
* payload never carries the full report — only an 80-char title/brief.
|
|
14
|
+
*/
|
|
15
|
+
const CONTROL_CHAR_REGEX = new RegExp("[\\x00-\\x1f\\x7f]", "g");
|
|
16
|
+
export function briefPayload(row, transitionedAt) {
|
|
17
|
+
const source = row.title && row.title.length > 0 ? row.title : row.brief;
|
|
18
|
+
const brief = source.replace(CONTROL_CHAR_REGEX, " ").slice(0, 80);
|
|
19
|
+
return {
|
|
20
|
+
taskId: row.id,
|
|
21
|
+
state: row.state,
|
|
22
|
+
transitionedAt,
|
|
23
|
+
brief,
|
|
24
|
+
outcomeDetail: row.outcomeDetail,
|
|
25
|
+
notify: row.notify,
|
|
26
|
+
originatingChannel: row.originatingChannel,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export const noopBackgroundTaskTransitionEmitter = {
|
|
30
|
+
emit() {
|
|
31
|
+
/* no-op */
|
|
32
|
+
},
|
|
33
|
+
emitFromRow(row, transitionedAt) {
|
|
34
|
+
if (!row)
|
|
35
|
+
return null;
|
|
36
|
+
return briefPayload(row, transitionedAt);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
export function createBackgroundTaskTransitionEmitter(sink) {
|
|
40
|
+
if (!sink)
|
|
41
|
+
return noopBackgroundTaskTransitionEmitter;
|
|
42
|
+
return {
|
|
43
|
+
emit(payload) {
|
|
44
|
+
void sink.broadcastNamedEvent("background_task", payload);
|
|
45
|
+
},
|
|
46
|
+
emitFromRow(row, transitionedAt) {
|
|
47
|
+
if (!row)
|
|
48
|
+
return null;
|
|
49
|
+
const payload = briefPayload(row, transitionedAt);
|
|
50
|
+
void sink.broadcastNamedEvent("background_task", payload);
|
|
51
|
+
return payload;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -161,7 +161,7 @@ export declare function shouldDenyEgress(url: string, opts?: {
|
|
|
161
161
|
hostnameDenylist?: ReadonlyArray<RegExp>;
|
|
162
162
|
}): Promise<{
|
|
163
163
|
denied: true;
|
|
164
|
-
reason: "hostname" | "cidr" | "invalid_url";
|
|
164
|
+
reason: "hostname" | "cidr" | "invalid_url" | "resolve_error";
|
|
165
165
|
} | {
|
|
166
166
|
denied: false;
|
|
167
167
|
}>;
|
|
@@ -229,6 +229,10 @@ export const IP_DENYLIST_CIDRS = Object.freeze([
|
|
|
229
229
|
"100.64.0.0/10",
|
|
230
230
|
"224.0.0.0/4",
|
|
231
231
|
"0.0.0.0/8",
|
|
232
|
+
// IETF protocol assignments (RFC 6890) — includes the DNS64 well-known
|
|
233
|
+
// host 192.0.0.171/.170 used to discover the NAT64 prefix. Not globally
|
|
234
|
+
// routable; no legitimate browse target lives here.
|
|
235
|
+
"192.0.0.0/24",
|
|
232
236
|
// IPv6 equivalents
|
|
233
237
|
"::1/128",
|
|
234
238
|
"::/128", // IPv6 unspecified — mirrors IPv4 0.0.0.0/8; routes to ::1 as a connect target on common stacks
|
|
@@ -374,8 +378,20 @@ export function matchesCidrDenylist(ip) {
|
|
|
374
378
|
const bits = ipv6ToBigInt(ip);
|
|
375
379
|
if (bits !== null) {
|
|
376
380
|
const mask96 = ((1n << 96n) - 1n) << 32n; // high 96 bits
|
|
377
|
-
const v4MappedPrefix = 0xffffn << 32n; // ::ffff:0:0
|
|
378
|
-
|
|
381
|
+
const v4MappedPrefix = 0xffffn << 32n; // ::ffff:0:0/96
|
|
382
|
+
// NAT64 well-known prefix `64:ff9b::/96` (RFC 6052) — on NAT64/DNS64
|
|
383
|
+
// networks the entire IPv4 internet is reachable through this prefix,
|
|
384
|
+
// so `64:ff9b::a9fe:a9fe` routes to 169.254.169.254 and
|
|
385
|
+
// `64:ff9b::a00:1` to 10.0.0.1. We must NOT block the whole /96
|
|
386
|
+
// (that would break all IPv4 browsing on such networks) — instead
|
|
387
|
+
// decode the embedded IPv4 (low 32 bits, same position as the
|
|
388
|
+
// v4-mapped form) and run it through the IPv4 deny ranges, so only
|
|
389
|
+
// metadata/private destinations are blocked. Network-specific NAT64
|
|
390
|
+
// prefixes (operator-chosen /32../64) are out of scope — they aren't
|
|
391
|
+
// a guessable SSRF target the way the well-known prefix is.
|
|
392
|
+
const nat64WellKnownPrefix = 0x0064ff9bn << 96n; // 64:ff9b::/96
|
|
393
|
+
const prefix96 = bits & mask96;
|
|
394
|
+
if (prefix96 === v4MappedPrefix || prefix96 === nat64WellKnownPrefix) {
|
|
379
395
|
const embedded = Number(bits & 0xffffffffn);
|
|
380
396
|
const dotted = [
|
|
381
397
|
(embedded >>> 24) & 255,
|
|
@@ -421,16 +437,26 @@ export async function shouldDenyEgress(url, opts) {
|
|
|
421
437
|
return { denied: false };
|
|
422
438
|
}
|
|
423
439
|
if (opts?.resolveIps) {
|
|
424
|
-
let resolved
|
|
440
|
+
let resolved;
|
|
425
441
|
try {
|
|
426
442
|
resolved = await opts.resolveIps(hostname);
|
|
427
443
|
}
|
|
428
444
|
catch {
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
|
|
445
|
+
// Fail CLOSED. This CIDR gate is the primary defence against egress to
|
|
446
|
+
// private / link-local / cloud-metadata IPs (the browser sandbox shares
|
|
447
|
+
// the host network namespace, so there is no packet-layer block behind
|
|
448
|
+
// it). The guard and Chromium resolve the hostname independently — a
|
|
449
|
+
// name that Node's resolver fails on but Chromium's resolver succeeds on
|
|
450
|
+
// (SERVFAIL / timeout / search-domain / IDN differences) would otherwise
|
|
451
|
+
// reach an internal address completely unchecked. Blocking on resolve
|
|
452
|
+
// failure closes that differential-resolver gap; a host that is
|
|
453
|
+
// genuinely unresolvable would fail at the network layer regardless.
|
|
454
|
+
return { denied: true, reason: "resolve_error" };
|
|
455
|
+
}
|
|
456
|
+
if (resolved.length === 0) {
|
|
457
|
+
// No addresses to vet is as unverifiable as a thrown lookup — don't let
|
|
458
|
+
// the host through to Chromium's independent resolution.
|
|
459
|
+
return { denied: true, reason: "resolve_error" };
|
|
434
460
|
}
|
|
435
461
|
for (const ip of resolved) {
|
|
436
462
|
if (matchesCidrDenylist(ip)) {
|
|
@@ -2,7 +2,7 @@ import { accessSync, constants, existsSync, readdirSync } from "node:fs";
|
|
|
2
2
|
import { readlink } from "node:fs/promises";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { homedir, release } from "node:os";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
5
|
+
import { delimiter, dirname, join } from "node:path";
|
|
6
6
|
import { execFile } from "node:child_process";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
8
|
const execFileAsync = promisify(execFile);
|
|
@@ -504,12 +504,54 @@ async function isUnixProcessRunning(binaryPath, userDataDir) {
|
|
|
504
504
|
return false;
|
|
505
505
|
}
|
|
506
506
|
}
|
|
507
|
+
/**
|
|
508
|
+
* Resolve the Windows PowerShell executable defensively.
|
|
509
|
+
*
|
|
510
|
+
* This is the same `powershell.exe → pwsh.exe` probe every other PS call
|
|
511
|
+
* site in the codebase performs (secret-client-factory.ts:findExecutableInPath,
|
|
512
|
+
* doctor.mjs, scripts/lib/read-api-token.mjs). On Windows desktop SKUs the
|
|
513
|
+
* in-box Windows PowerShell 5.1 (`powershell.exe`) is present, but on
|
|
514
|
+
* minimal / headless / Server-Core / Nano hosts — or future builds where
|
|
515
|
+
* 5.1 has been removed — only PowerShell 7+ (`pwsh.exe`) exists, and a
|
|
516
|
+
* hardcoded `powershell.exe` exec throws ENOENT (which the caller's catch
|
|
517
|
+
* swallows, silently reporting the managed Chromium as not-running).
|
|
518
|
+
*
|
|
519
|
+
* Prefer 5.1, fall back to 7+, else keep the default so the first exec
|
|
520
|
+
* surfaces a clear ENOENT rather than masking a misconfigured host.
|
|
521
|
+
*/
|
|
522
|
+
function resolveWindowsPowerShell() {
|
|
523
|
+
const pathValue = process.env.PATH ?? "";
|
|
524
|
+
const exts = process.env.PATHEXT?.split(";").filter(Boolean) ?? [".EXE"];
|
|
525
|
+
const probe = (name) => {
|
|
526
|
+
const hasExt = /\.[A-Za-z0-9]+$/.test(name);
|
|
527
|
+
for (const dir of pathValue.split(delimiter)) {
|
|
528
|
+
if (!dir)
|
|
529
|
+
continue;
|
|
530
|
+
const candidates = hasExt ? [name] : exts.map((e) => `${name}${e}`);
|
|
531
|
+
for (const c of candidates) {
|
|
532
|
+
try {
|
|
533
|
+
accessSync(join(dir, c), constants.X_OK);
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
// keep scanning
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return false;
|
|
542
|
+
};
|
|
543
|
+
return probe("powershell.exe")
|
|
544
|
+
? "powershell.exe"
|
|
545
|
+
: probe("pwsh.exe")
|
|
546
|
+
? "pwsh.exe"
|
|
547
|
+
: "powershell.exe";
|
|
548
|
+
}
|
|
507
549
|
async function isWindowsProcessRunning(binaryPath, userDataDir) {
|
|
508
550
|
const escapedBinary = binaryPath.replace(/'/g, "''");
|
|
509
551
|
const escapedDir = userDataDir.replace(/'/g, "''");
|
|
510
552
|
const command = `Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like '*${escapedBinary}*' -and $_.CommandLine -like '*--user-data-dir=${escapedDir}*' } | Select-Object -First 1`;
|
|
511
553
|
try {
|
|
512
|
-
const { stdout } = await execFileAsync(
|
|
554
|
+
const { stdout } = await execFileAsync(resolveWindowsPowerShell(), [
|
|
513
555
|
"-NoProfile",
|
|
514
556
|
"-Command",
|
|
515
557
|
command,
|
|
@@ -233,7 +233,6 @@ function loadWindowsHelper() {
|
|
|
233
233
|
// a module-scope global — build one via `createRequire` to reach the
|
|
234
234
|
// CommonJS loader. This branch only runs on win32.
|
|
235
235
|
const req = createRequire(import.meta.url);
|
|
236
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
237
236
|
const mod = req("../../../../native/win-appcontainer/loader.js");
|
|
238
237
|
return mod.loadHelper();
|
|
239
238
|
}
|
|
@@ -34,7 +34,7 @@ import { getBrowserTask, markRunning, markRunningFromParked, markTerminal, } fro
|
|
|
34
34
|
import { resolveClarification } from "../../db/browser-task-clarifications-store.js";
|
|
35
35
|
import { createLogger } from "../../logging.js";
|
|
36
36
|
import { prepareDriverHandle, releaseDriverHandle, resumeDriver, runDriver, } from "./browser-task-driver.js";
|
|
37
|
-
import { createInitialSlotState, decideAcquire, decidePark, decideRelease, decideUnpark, } from "./browser-task-slots.js";
|
|
37
|
+
import { createInitialSlotState, decideAcquire, decideCancel, decidePark, decideRelease, decideUnpark, } from "./browser-task-slots.js";
|
|
38
38
|
import { noopBrowserTaskTransitionEmitter, } from "./browser-task-transition-events.js";
|
|
39
39
|
const logger = createLogger("browser-task-runner");
|
|
40
40
|
/**
|
|
@@ -52,6 +52,16 @@ const logger = createLogger("browser-task-runner");
|
|
|
52
52
|
function slotSiteKeyForRow(row) {
|
|
53
53
|
return row.siteKey ?? `__open__:${row.id}`;
|
|
54
54
|
}
|
|
55
|
+
/** True when the slot manager already tracks `taskId` as an active
|
|
56
|
+
* occupant (acquired a slot), even if the DB row still reads `pending`
|
|
57
|
+
* in the narrow acquire→markRunning window. Mirrors the route helper. */
|
|
58
|
+
function slotManagerHasActive(state, taskId) {
|
|
59
|
+
for (const entry of state.active.values()) {
|
|
60
|
+
if (entry.taskId === taskId)
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
55
65
|
export function createSlotStateRef(maxConcurrent) {
|
|
56
66
|
return { state: createInitialSlotState(maxConcurrent) };
|
|
57
67
|
}
|
|
@@ -494,15 +504,50 @@ export function createBrowserTaskRunner(deps) {
|
|
|
494
504
|
// landing in that window is silently dropped and the SDK runs
|
|
495
505
|
// anyway, burning turns / cost.
|
|
496
506
|
//
|
|
497
|
-
//
|
|
498
|
-
//
|
|
499
|
-
// parked states (`awaiting_user`/`final_confirm`) always have a
|
|
500
|
-
// tracked handle, so they take the `handle` branch above. Recording
|
|
501
|
-
// a pending-abort for a `pending` row would leak the map entry —
|
|
502
|
-
// nothing ever consumes it (`runDriverFromPending` only drains it
|
|
503
|
-
// after `prepareDriverHandle`, which a pending row never reaches).
|
|
507
|
+
// The parked states (`awaiting_user`/`final_confirm`) always have a
|
|
508
|
+
// tracked handle, so they take the `handle` branch above.
|
|
504
509
|
pendingAborts.set(taskId, reason || "cancel");
|
|
505
510
|
}
|
|
511
|
+
else if (row.state === "pending") {
|
|
512
|
+
// Queued behind the concurrency cap (or in the narrow acquire→
|
|
513
|
+
// markRunning window). The HTTP `/cancel` route handles `pending`
|
|
514
|
+
// itself, but `!stop <id>` (Phase 4) calls `cancel()` directly — so
|
|
515
|
+
// the runner must own this state too, or the bang path reports a
|
|
516
|
+
// false "Stopping…" while the task keeps running.
|
|
517
|
+
if (slotManagerHasActive(deps.slotStateRef.state, taskId)) {
|
|
518
|
+
// `tryAcquire` already promoted the task (slot active) but
|
|
519
|
+
// `markRunning` hasn't flipped the DB row yet — `decideCancel`
|
|
520
|
+
// would throw on an active occupant. Record the abort intent like
|
|
521
|
+
// the running case; the handle registered at `runDriverFromPending`
|
|
522
|
+
// fires it before the SDK loop begins (and drains the map entry,
|
|
523
|
+
// so it does not leak).
|
|
524
|
+
pendingAborts.set(taskId, reason || "cancel");
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
// Genuinely queued — drop the FIFO entry and write the terminal
|
|
528
|
+
// directly (no slot was held, so no release cascade). Mirrors the
|
|
529
|
+
// route's `isPending` path and the background runner.
|
|
530
|
+
try {
|
|
531
|
+
deps.slotStateRef.state = decideCancel(deps.slotStateRef.state, taskId).state;
|
|
532
|
+
}
|
|
533
|
+
catch (err) {
|
|
534
|
+
/* c8 ignore start -- defensive: slot promoted between the check and here */
|
|
535
|
+
logger.warn({ err, taskId }, "decideCancel on pending row failed (continuing)");
|
|
536
|
+
/* c8 ignore stop */
|
|
537
|
+
}
|
|
538
|
+
const finishedAt = now();
|
|
539
|
+
const terminal = markTerminal(deps.db, {
|
|
540
|
+
id: taskId,
|
|
541
|
+
state: "cancelled",
|
|
542
|
+
outcomeDetail: `cancelled_in_queue:${reason}`,
|
|
543
|
+
report: null,
|
|
544
|
+
finishedAt,
|
|
545
|
+
});
|
|
546
|
+
emitter.emitFromRow(terminal, finishedAt);
|
|
547
|
+
logger.info({ taskId, reason }, "browser-task cancel (pending → cancelled)");
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
506
551
|
if (handle) {
|
|
507
552
|
// Cancel any pending lite-final-confirm tokens issued by this
|
|
508
553
|
// task — otherwise a gate that was mid-flight leaves a token
|