alvin-bot 4.12.1 โ 4.12.3
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/CHANGELOG.md +145 -0
- package/README.md +42 -5
- package/dist/handlers/async-agent-chunk-handler.js +17 -0
- package/dist/handlers/background-bypass.js +75 -0
- package/dist/handlers/message.js +127 -16
- package/dist/index.js +75 -3
- package/dist/providers/claude-sdk-provider.js +4 -1
- package/dist/services/allowed-users-gate.js +56 -0
- package/dist/services/async-agent-watcher.js +25 -0
- package/dist/services/cron.js +17 -0
- package/dist/services/exec-guard.js +26 -1
- package/dist/services/fallback-order.js +4 -1
- package/dist/services/file-permissions.js +93 -0
- package/dist/services/session-persistence.js +19 -2
- package/dist/services/session.js +2 -0
- package/dist/services/subagents.js +23 -5
- package/dist/services/timing-safe-bearer.js +51 -0
- package/dist/web/doctor-api.js +8 -2
- package/dist/web/server.js +7 -3
- package/dist/web/setup-api.js +5 -2
- package/docs/security.md +279 -0
- package/package.json +4 -1
- package/test/allowed-users-gate.test.ts +98 -0
- package/test/async-agent-chunk-flow.test.ts +113 -0
- package/test/background-bypass-integration.test.ts +443 -0
- package/test/background-bypass-stress.test.ts +417 -0
- package/test/background-bypass.test.ts +127 -0
- package/test/exec-guard-metachars.test.ts +110 -0
- package/test/file-permissions.test.ts +130 -0
- package/test/session-pending-background.test.ts +59 -0
- package/test/subagent-toolset-allowlist.test.ts +146 -0
- package/test/subagents-toolset.test.ts +22 -2
- package/test/timing-safe-bearer.test.ts +65 -0
- package/test/watcher-pending-count.test.ts +228 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,151 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Alvin Bot are documented here.
|
|
4
4
|
|
|
5
|
+
## [4.12.3] โ 2026-04-15
|
|
6
|
+
|
|
7
|
+
### ๐ Patch: Background sub-agent no longer blocks the main Telegram session
|
|
8
|
+
|
|
9
|
+
**The bug Ali reported:** After launching an async sub-agent (`run_in_background: true`), sending any follow-up message to the bot silently stalled for 2+ minutes before being processed. v4.12.1/v4.12.2 attempted a prompt-hint mitigation but did NOT address the architectural root cause.
|
|
10
|
+
|
|
11
|
+
**Root cause (re-diagnosed with live SDK event logs):** The Claude Agent SDK's CLI subprocess stays alive for the full duration of a background task so it can inject the `<task-notification>` inline into the NEXT assistant turn. While that subprocess idles, Alvin's query iterator is still being drained, `session.isProcessing` stays `true`, and every new user message gets pushed into the 3-slot queue โ which doesn't auto-drain. From the user's perspective: send "A" โ nothing happens for 2 minutes.
|
|
12
|
+
|
|
13
|
+
**The fix (architectural workaround):** New session field `pendingBackgroundCount` tracks the number of background agents currently in-flight. When a new message arrives while `isProcessing=true` AND the counter is `>0`, the handler:
|
|
14
|
+
|
|
15
|
+
1. **Aborts the blocked query** instead of queueing. The old SDK subprocess dies; the background task's own detached subprocess keeps writing to its `output_file`.
|
|
16
|
+
2. **Starts a fresh SDK session** (`resume: null`) for the new message so it doesn't inherit the block. Recent conversation history is carried forward via the bridge preamble so Claude retains context.
|
|
17
|
+
3. **Relies on the existing `async-agent-watcher` (v4.10.0)** to poll the background task's `output_file` and deliver the result as a separate Telegram message via `subagent-delivery.ts`. The watcher decrements the counter when it delivers, so subsequent messages go back to normal SDK-resume behavior.
|
|
18
|
+
|
|
19
|
+
**Net effect:** Sending "A" during a 5-minute research task now gets processed in ~200ms instead of after 5 minutes. The background research still delivers its result via a separate message when ready.
|
|
20
|
+
|
|
21
|
+
### Technical details
|
|
22
|
+
|
|
23
|
+
**New module** `src/handlers/background-bypass.ts` โ pure state-machine helpers:
|
|
24
|
+
- `shouldBypassQueue(state)` โ returns true when `isProcessing=true`, `pendingBackgroundCount>0`, and an unaborted `abortController` exists
|
|
25
|
+
- `shouldBypassSdkResume(state)` โ returns true when `pendingBackgroundCount>0`, signalling the next query should pass `sessionId=null`
|
|
26
|
+
- `waitUntilProcessingFalse(session, timeoutMs, tickMs)` โ poll-waits for the old handler's `finally` block to flip the flag before the new query starts
|
|
27
|
+
|
|
28
|
+
**`src/services/session.ts`** โ new field `pendingBackgroundCount: number` (default 0, reset on `/new`). Not persisted across restarts โ the watcher re-hydrates its own state file and delivery still works, and starting a fresh counter after restart avoids stale drift.
|
|
29
|
+
|
|
30
|
+
**`src/services/async-agent-watcher.ts`** โ `PendingAsyncAgent` gets an optional `sessionKey` field. On every delivery path (completed/failed/timeout), a new `decrementPendingCount(sessionKey)` helper clamps the counter at 0 using `Math.max`. Missing/unknown session keys are a no-op (backwards compatible with pre-v4.12.3 persisted state files).
|
|
31
|
+
|
|
32
|
+
**`src/handlers/async-agent-chunk-handler.ts`** โ `TurnContext` gets `sessionKey`. When `registerPendingAgent` is called, the counter is incremented in the same function.
|
|
33
|
+
|
|
34
|
+
**`src/handlers/message.ts`** (Telegram):
|
|
35
|
+
- Computes `sessionKey` once at the top of the handler and passes it everywhere
|
|
36
|
+
- `if (session.isProcessing)` branch now checks `shouldBypassQueue` first โ if true, aborts + waits for cleanup + falls through to process the new message. If false, queues as before.
|
|
37
|
+
- When queueing, the handler now sends a text reply (`"โณ Eine Anfrage lรคuft gerade. Deine Nachricht ist in der Warteschlange..."`) in addition to the ๐ reaction, so the user sees what happened (reactions alone were too subtle)
|
|
38
|
+
- New `bypassResume` variable controls whether `queryOpts.sessionId` is `null` (fresh session) or `session.sessionId` (normal resume)
|
|
39
|
+
- Bridge preamble now has two modes: the existing "SDK recovery" mode that bridges fallback turns, plus a new "bypass" mode that bridges the last 10 turns when starting a fresh session mid-conversation
|
|
40
|
+
- New `_bypassAbortFired` session flag + `bypassAborted` local flag ensure that the old handler silently absorbs the abort error instead of showing a confusing "request cancelled" reply, and the fresh handler's finalize/broadcast/๐ reaction path is skipped for the aborted turn
|
|
41
|
+
|
|
42
|
+
### Known limitations
|
|
43
|
+
|
|
44
|
+
- **Platform coverage**: bypass path is Telegram-only in v4.12.3. Slack/Discord/WhatsApp handlers (`src/handlers/platform-message.ts`) don't currently handle `tool_result` chunks at all, so async agents can't be registered on those platforms. That's a pre-existing limitation that will be fixed in a future release.
|
|
45
|
+
- **SDK behavior dependency**: the fix assumes the background task's own subprocess is detached from the parent SDK query's `AbortController`. Empirically this holds (the watcher delivers results even after bypass-abort), but if a future SDK release changes this we'd need to either stop using `run_in_background` and rely on a pure Alvin-side background dispatch (bigger change) or add a targeted `process.kill` for the parent only, keeping the child alive.
|
|
46
|
+
- **Restart mid-flight**: if the bot restarts while a background agent is pending, the session's counter starts at 0 on restart. The watcher re-hydrates its own state file and still delivers the result correctly, but the session's "is this blocked?" signal is lost, so the first post-restart message might use SDK resume on the old (possibly-blocked) session ID. Minor cosmetic issue, not a data loss.
|
|
47
|
+
|
|
48
|
+
### Testing
|
|
49
|
+
|
|
50
|
+
- **Baseline**: 396 tests (v4.12.2)
|
|
51
|
+
- **New tests**: +40
|
|
52
|
+
- `test/session-pending-background.test.ts` โ 4 tests (counter wiring, reset, clamp)
|
|
53
|
+
- `test/watcher-pending-count.test.ts` โ 6 tests (decrement on delivery/timeout/failure, missing sessionKey, multi-agent)
|
|
54
|
+
- `test/async-agent-chunk-flow.test.ts` โ +3 tests (sessionKey propagation, counter stacking, non-async no-op)
|
|
55
|
+
- `test/background-bypass.test.ts` โ 12 tests (pure helpers: shouldBypassQueue, shouldBypassSdkResume, waitUntilProcessingFalse)
|
|
56
|
+
- `test/background-bypass-integration.test.ts` โ 6 tests (full lifecycle, stress, session isolation)
|
|
57
|
+
- `test/background-bypass-stress.test.ts` โ 9 tests (100 parallel sessions, 200 churn cycles, extreme drift, /new during pending, ephemeral session, mixed rollout, timing edge cases, high load 50ร4 agents)
|
|
58
|
+
- **Total**: 436 tests, all green, TSC clean
|
|
59
|
+
|
|
60
|
+
### Files changed
|
|
61
|
+
|
|
62
|
+
- **NEW**: `src/handlers/background-bypass.ts`
|
|
63
|
+
- **NEW tests**: `test/session-pending-background.test.ts`, `test/watcher-pending-count.test.ts`, `test/background-bypass.test.ts`, `test/background-bypass-integration.test.ts`, `test/background-bypass-stress.test.ts`
|
|
64
|
+
- **Modified**: `src/handlers/message.ts` (bypass wiring + visible queue reply), `src/handlers/async-agent-chunk-handler.ts` (sessionKey + counter increment), `src/services/async-agent-watcher.ts` (sessionKey in PendingAsyncAgent + decrement on delivery), `src/services/session.ts` (pendingBackgroundCount field + _bypassAbortFired flag), `src/services/session-persistence.ts` (counter not persisted โ reset on restart), `test/async-agent-chunk-flow.test.ts` (new assertions)
|
|
65
|
+
- **Version**: `package.json` 4.12.2 โ 4.12.3
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## [4.12.2] โ 2026-04-15
|
|
70
|
+
|
|
71
|
+
### ๐ Security patch: file permissions, ALLOWED_USERS hard-fail, exec-guard hardening, CVE updates
|
|
72
|
+
|
|
73
|
+
This is the first **formal security release** of Alvin Bot, motivated by a comprehensive audit after v4.12.1 production deployment. The audit surfaced real issues that needed fixing before the bot could be safely installed on multi-user dev servers or shared by external users. All fixes are additive and backwards-compatible โ existing single-user installs see no behavior change except improved security.
|
|
74
|
+
|
|
75
|
+
#### CRITICAL CVE โ axios 1.14.0 โ 1.15.0 (CVSS 10.0)
|
|
76
|
+
|
|
77
|
+
Transitive dependency via `@slack/bolt`. Two CVEs closed:
|
|
78
|
+
- GHSA-fvcv-3m26-pcqx โ Cloud Metadata Exfiltration via Header Injection Chain (CVSS 10.0)
|
|
79
|
+
- GHSA-3p68-rc4w-qgx5 โ NO_PROXY Hostname Normalization Bypass โ SSRF
|
|
80
|
+
|
|
81
|
+
Fix: `npm update @slack/bolt` (4.6.0 โ 4.7.0) + `package.json overrides: axios ^1.15.0` to force transitive updates in `@slack/web-api` and `@whiskeysockets/baileys`. Post-fix `npm audit` shows **0 critical, 2 high remaining** (`basic-ftp` HIGH โ never invoked by Alvin, `electron` HIGH โ devDep only, tracked as Phase 18).
|
|
82
|
+
|
|
83
|
+
Also updated `@anthropic-ai/claude-agent-sdk` 0.2.97 โ 0.2.109 (MODERATE: GHSA-5474-4w2j-mq4c Path Validation Sandbox Escape).
|
|
84
|
+
|
|
85
|
+
#### CRITICAL โ File permissions on sensitive files (0o600)
|
|
86
|
+
|
|
87
|
+
Pre-v4.12.2 `~/.alvin-bot/.env`, `state/sessions.json`, memory logs, cron-jobs.json were written with the default umask โ typically 0o644 on Linux/macOS, meaning any other user on the same machine could read BOT_TOKEN + all API keys, full conversation history, cron prompts, and encrypted sudo credentials.
|
|
88
|
+
|
|
89
|
+
**Fix**: new `src/services/file-permissions.ts` with `writeSecure()`, `ensureSecureMode()`, `auditSensitiveFiles()`. All `.env` writes in setup-api, doctor-api, server, fallback-order, session-persistence now use `writeSecure()`. Startup audit in `index.ts` chmod-repairs the full sensitive-file list idempotently on every boot.
|
|
90
|
+
|
|
91
|
+
#### CRITICAL โ ALLOWED_USERS startup hard-fail
|
|
92
|
+
|
|
93
|
+
Pre-v4.12.2 Alvin started with BOT_TOKEN set but ALLOWED_USERS empty with only a console.warn โ leaving the bot "configured but unguarded".
|
|
94
|
+
|
|
95
|
+
**Fix**: new pure gate function `src/services/allowed-users-gate.ts`. `src/index.ts` refuses to start with a clear error message. Two explicit escape hatches: `AUTH_MODE=open` or `ALVIN_INSECURE_ACKNOWLEDGED=1`.
|
|
96
|
+
|
|
97
|
+
#### HIGH โ Webhook bearer token timing-safe comparison
|
|
98
|
+
|
|
99
|
+
`src/web/server.ts` POST /api/webhook previously used naive `authHeader !== "Bearer " + token` leaking comparison position via timing side-channel.
|
|
100
|
+
|
|
101
|
+
**Fix**: new `src/services/timing-safe-bearer.ts` wraps `crypto.timingSafeEqual` with strict "Bearer <token>" format, empty-expected rejection, length-mismatch dummy comparison.
|
|
102
|
+
|
|
103
|
+
#### HIGH โ Exec-guard shell metacharacter rejection
|
|
104
|
+
|
|
105
|
+
`checkExecAllowed()` only inspected the first word โ `echo safe; rm -rf /` passed as "echo". Trivially bypassable via `&&`, `|`, `` ` ``, `$(...)`, redirects.
|
|
106
|
+
|
|
107
|
+
**Fix**: allowlist mode rejects any command containing `;`, `&`, `|`, `` ` ``, `$(...)`, `{...}`, `<`, `>`. Operators who need shell pipelines set `EXEC_SECURITY=full` explicitly.
|
|
108
|
+
|
|
109
|
+
#### HIGH โ Cron shell-job execGuard integration
|
|
110
|
+
|
|
111
|
+
Pre-v4.12.2 cron `type: "shell"` bypassed the exec-guard entirely. **Fix**: cron.ts case "shell" now calls `checkExecAllowed()` before `execSync()` and sends a blocked-notification on deny.
|
|
112
|
+
|
|
113
|
+
#### MEDIUM โ Sub-agent toolset allowlist (readonly, research)
|
|
114
|
+
|
|
115
|
+
`SubAgentConfig.toolset` widened from `"full"` to `"full" | "readonly" | "research"`:
|
|
116
|
+
- `readonly` โ Read, Glob, Grep only (no write, shell, network)
|
|
117
|
+
- `research` โ readonly + WebSearch, WebFetch
|
|
118
|
+
- `full` โ unchanged default
|
|
119
|
+
|
|
120
|
+
New `QueryOptions.allowedTools?: string[]` honored by `claude-sdk-provider`. Other providers ignore it.
|
|
121
|
+
|
|
122
|
+
#### NEW โ `docs/security.md` threat model + hardening guide (279 lines)
|
|
123
|
+
|
|
124
|
+
First formal security documentation covering: TL;DR safety table, capability surface, attacker model, trust boundaries, hardening step-by-step, shell execution policy, file permissions list, sub-agent presets, prompt injection honesty section, Phase 18 pending work, security issue reporting, incident response playbook. Public doc, shipped with the repo.
|
|
125
|
+
|
|
126
|
+
#### NEW โ README Security section rewrite
|
|
127
|
+
|
|
128
|
+
Replaced thin bullet list with a boxed warning ("Alvin has full shell + filesystem access") and four sub-sections: access control, execution hardening, data hardening, known limitations. Links to docs/security.md.
|
|
129
|
+
|
|
130
|
+
#### Testing
|
|
131
|
+
|
|
132
|
+
**396 tests total** (350 baseline from v4.12.1 + 46 new). All green. Build clean.
|
|
133
|
+
|
|
134
|
+
- 10 `test/file-permissions.test.ts`
|
|
135
|
+
- 7 `test/allowed-users-gate.test.ts`
|
|
136
|
+
- 10 `test/timing-safe-bearer.test.ts`
|
|
137
|
+
- 13 `test/exec-guard-metachars.test.ts`
|
|
138
|
+
- 4 `test/subagent-toolset-allowlist.test.ts`
|
|
139
|
+
- 2 extended `test/subagents-toolset.test.ts` (readonly + research)
|
|
140
|
+
|
|
141
|
+
#### Phase 18 (deferred, tracked in README Roadmap)
|
|
142
|
+
|
|
143
|
+
- Electron 35 โ 41+ upgrade (Desktop build, 6 CVEs)
|
|
144
|
+
- Prompt injection defense strategy (design debate, not code filter)
|
|
145
|
+
- TypeScript 5 โ 6 upgrade
|
|
146
|
+
- MCP plugin sandboxing (architectural v5.0)
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
5
150
|
## [4.12.1] โ 2026-04-15
|
|
6
151
|
|
|
7
152
|
### ๐ Patch: Sync sub-agent timeout + workspace command menu
|
package/README.md
CHANGED
|
@@ -659,17 +659,54 @@ alvin-bot version # Show version
|
|
|
659
659
|
- [ ] Workspace cloning / templates โ `/workspace clone alev-b as homes-dev` spins up a new workspace from an existing one
|
|
660
660
|
- [ ] Slack slash commands (`/alvin workspace`, `/alvin status`, `/alvin new`) โ native Slack command integration via Bolt
|
|
661
661
|
- [ ] Daily log decay / archive โ older daily logs move to cold storage after N days
|
|
662
|
+
- [ ] **Phase 18** โ Security + Platform hardening (from v4.12.1 audit, prioritized)
|
|
663
|
+
- [ ] **P1 โ Electron major upgrade** (35 โ 41+) โ fixes 1 HIGH + 5 MODERATE Electron CVEs in the Desktop-Build path. Major version jump, requires full rebuild + test of `.dmg` flow. Separate release (likely bundled with Windows `.exe` work).
|
|
664
|
+
- [ ] **P1 โ Prompt injection defense strategy** โ not a single fix but a design debate: heuristic filters vs allow-list vs no-sandbox-accept-the-risk. Currently handled as a documented design-constraint (README security section), not as a code filter. When we decide the policy, implement it across all message entry points.
|
|
665
|
+
- [ ] **P2 โ TypeScript 5 โ 6 upgrade** โ major release, likely breaking changes in strict mode. Needs a dedicated release + test sweep. Low priority since 5.x is still supported.
|
|
666
|
+
- [ ] **P0 for v5.0 โ MCP plugin sandboxing** โ currently MCP servers run with full Node privileges. Plan: run each MCP in a child process with restricted FS + network policy (similar to deno-permission model). Architectural change, v5.0 territory.
|
|
662
667
|
|
|
663
668
|
---
|
|
664
669
|
|
|
665
670
|
## ๐ Security
|
|
666
671
|
|
|
667
|
-
|
|
672
|
+
> ### โ ๏ธ Important: Alvin has full shell + filesystem access
|
|
673
|
+
>
|
|
674
|
+
> Alvin Bot is an **autonomous AI agent** built on the Claude Agent SDK with shell, filesystem, and network access to the machine it runs on. This is by design โ it's the point of the project. But it means:
|
|
675
|
+
>
|
|
676
|
+
> - **Treat the bot like `sudo` access** โ only install it on machines where you'd trust Claude Code to run without supervision.
|
|
677
|
+
> - **Never expose the Web UI (port 3100) to the internet** without HTTPS, rate limiting, and a strong `WEB_PASSWORD`. It binds to `localhost` by default.
|
|
678
|
+
> - **On multi-user systems**, verify `~/.alvin-bot/.env` is chmod `600` (v4.12.2+ enforces this automatically on startup).
|
|
679
|
+
> - **`ALLOWED_USERS` is your first line of defense** โ v4.12.2+ refuses to start if it's empty and Telegram is enabled.
|
|
680
|
+
>
|
|
681
|
+
> **Read the full threat model and hardening guide:** [`docs/security.md`](docs/security.md)
|
|
682
|
+
|
|
683
|
+
### Access control
|
|
684
|
+
|
|
685
|
+
- **User whitelist** โ Only `ALLOWED_USERS` can interact with the bot (hard-enforced at startup since v4.12.2)
|
|
668
686
|
- **WhatsApp group approval** โ Per-group participant whitelist + owner approval gate via Telegram (with WhatsApp DM / Discord / Signal fallback). Group members never see the approval process.
|
|
669
|
-
- **
|
|
670
|
-
- **
|
|
671
|
-
|
|
672
|
-
|
|
687
|
+
- **Slack allowlist** โ `SLACK_ALLOWED_USERS` restricts who can DM or @mention the bot in Slack
|
|
688
|
+
- **DM pairing** โ Optional 6-digit code flow for new users via owner approval (`AUTH_MODE=pairing`)
|
|
689
|
+
|
|
690
|
+
### Execution hardening
|
|
691
|
+
|
|
692
|
+
- **`EXEC_SECURITY=allowlist`** (default) โ Shell commands must match a whitelist of safe binaries and **cannot contain shell metacharacters** (`;`, `|`, `&`, `` ` ``, `$(...)`, redirects). Rejected by v4.12.2's exec-guard metachar filter.
|
|
693
|
+
- **Cron shell jobs** go through the same exec-guard (v4.12.2+) โ cron is no longer a bypass vector.
|
|
694
|
+
- **Sub-agent toolset presets** โ spawn sub-agents with `toolset: "readonly"` or `"research"` to restrict what they can do, regardless of the parent's privileges.
|
|
695
|
+
- **Timing-safe webhook auth** โ `POST /api/webhook` uses `crypto.timingSafeEqual` (v4.12.2+) to prevent timing side-channel token extraction.
|
|
696
|
+
|
|
697
|
+
### Data hardening
|
|
698
|
+
|
|
699
|
+
- **Self-hosted** โ Your data stays on your machine. No cloud sync, no external logging of prompts or responses.
|
|
700
|
+
- **No telemetry** โ Zero tracking, zero analytics, zero phone-home.
|
|
701
|
+
- **File permissions** โ `.env`, `sessions.json`, memory logs, cron jobs, and all sensitive state files are chmod `0o600` on every write and repaired at startup (v4.12.2+).
|
|
702
|
+
- **Owner protection** โ Owner account cannot be deleted via UI.
|
|
703
|
+
- **Encrypted sudo credentials** โ If you enable sudo exec, passwords are stored encrypted with an XOR key in a separate file, both chmod `0o600`.
|
|
704
|
+
|
|
705
|
+
### Known limitations (documented honestly)
|
|
706
|
+
|
|
707
|
+
- **Prompt injection** cannot be reliably filtered โ we document this as a capability tradeoff rather than pretending to solve it. See `docs/security.md` for the full discussion.
|
|
708
|
+
- **Not yet hardened for public-internet deployment** โ current scope is "on your own machine". VPS deployment works but requires additional reverse-proxy + TLS + rate-limit setup that we don't automate.
|
|
709
|
+
- **Electron Desktop build** has known CVEs (Phase 18 roadmap). The primary distribution is npm global install, not Desktop โ if you don't use the Desktop wrapper, you're not affected.
|
|
673
710
|
|
|
674
711
|
---
|
|
675
712
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseAsyncLaunchedToolResult } from "../services/async-agent-parser.js";
|
|
2
2
|
import { registerPendingAgent } from "../services/async-agent-watcher.js";
|
|
3
|
+
import { getAllSessions } from "../services/session.js";
|
|
3
4
|
/**
|
|
4
5
|
* Inspect a stream chunk; if it's an Agent async_launched tool_result,
|
|
5
6
|
* register the pending agent with the watcher.
|
|
@@ -29,5 +30,21 @@ export function handleToolResultChunk(chunk, ctx) {
|
|
|
29
30
|
chatId: ctx.chatId,
|
|
30
31
|
userId: ctx.userId,
|
|
31
32
|
toolUseId: chunk.toolUseId ?? null,
|
|
33
|
+
sessionKey: ctx.sessionKey,
|
|
32
34
|
});
|
|
35
|
+
// v4.12.3 โ Increment the session's pendingBackgroundCount so the
|
|
36
|
+
// main handler knows a background task is tying up the SDK's CLI
|
|
37
|
+
// subprocess. The watcher decrements this when it delivers the result.
|
|
38
|
+
// Guarded: missing sessionKey or unknown session is a no-op.
|
|
39
|
+
if (ctx.sessionKey) {
|
|
40
|
+
try {
|
|
41
|
+
const s = getAllSessions().get(ctx.sessionKey);
|
|
42
|
+
if (s) {
|
|
43
|
+
s.pendingBackgroundCount = (s.pendingBackgroundCount ?? 0) + 1;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
/* never let counter updates break registration */
|
|
48
|
+
}
|
|
49
|
+
}
|
|
33
50
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v4.12.3 โ Background-agent bypass helpers.
|
|
3
|
+
*
|
|
4
|
+
* Pure state-machine helpers used by the Telegram + platform message
|
|
5
|
+
* handlers to decide whether to:
|
|
6
|
+
* 1. Abort a running query instead of queueing the next user message,
|
|
7
|
+
* when the running query is blocked waiting for a background
|
|
8
|
+
* task-notification (SDK's CLI subprocess stays alive for the full
|
|
9
|
+
* duration of the background task).
|
|
10
|
+
* 2. Start the next SDK query with a fresh session (sessionId=null)
|
|
11
|
+
* when any background agent is still pending, so the new query
|
|
12
|
+
* doesn't inherit the old session's block.
|
|
13
|
+
*
|
|
14
|
+
* These are separated into their own module so they can be unit tested
|
|
15
|
+
* without a grammy Context mock.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Decide whether to bypass the normal "queue this message" branch and
|
|
19
|
+
* interrupt the running query so the new message can proceed immediately.
|
|
20
|
+
*
|
|
21
|
+
* True when:
|
|
22
|
+
* - A query is currently running (`isProcessing`)
|
|
23
|
+
* - At least one background agent is pending in this session
|
|
24
|
+
* - An unaborted abortController exists to cancel the running query
|
|
25
|
+
*
|
|
26
|
+
* Otherwise false โ fall back to the normal queue/drop behavior.
|
|
27
|
+
*/
|
|
28
|
+
export function shouldBypassQueue(state) {
|
|
29
|
+
if (!state.isProcessing)
|
|
30
|
+
return false;
|
|
31
|
+
if (state.pendingBackgroundCount <= 0)
|
|
32
|
+
return false;
|
|
33
|
+
const ac = state.abortController;
|
|
34
|
+
if (!ac)
|
|
35
|
+
return false;
|
|
36
|
+
if (ac.signal.aborted)
|
|
37
|
+
return false;
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Decide whether the next SDK query should skip `resume: sessionId`
|
|
42
|
+
* and start a fresh session instead. Needed when a background agent is
|
|
43
|
+
* still pending โ resuming the original session would inherit its block
|
|
44
|
+
* (the SDK's CLI subprocess for that session is waiting to deliver the
|
|
45
|
+
* task-notification inline). A fresh session has no such block and
|
|
46
|
+
* proceeds immediately. Context is preserved via the bridge preamble
|
|
47
|
+
* (buildBridgeMessage in message.ts).
|
|
48
|
+
*/
|
|
49
|
+
export function shouldBypassSdkResume(state) {
|
|
50
|
+
return state.pendingBackgroundCount > 0;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Poll-wait until `session.isProcessing` becomes false (or the timeout
|
|
54
|
+
* elapses). Returns true if the flag flipped, false on timeout.
|
|
55
|
+
*
|
|
56
|
+
* Used by the bypass path: after calling `abort()` on the running query,
|
|
57
|
+
* we wait for its finally block to run and flip isProcessing=false
|
|
58
|
+
* before starting the new query. The handler's own message loop is the
|
|
59
|
+
* one flipping the flag, so we just have to yield the event loop and
|
|
60
|
+
* re-check.
|
|
61
|
+
*
|
|
62
|
+
* Timeouts above 0 are recommended. Default tick interval is 50ms which
|
|
63
|
+
* is short enough that the fall-through feels instant to the user.
|
|
64
|
+
*/
|
|
65
|
+
export async function waitUntilProcessingFalse(session, timeoutMs, tickMs = 50) {
|
|
66
|
+
if (!session.isProcessing)
|
|
67
|
+
return true;
|
|
68
|
+
const start = Date.now();
|
|
69
|
+
while (session.isProcessing) {
|
|
70
|
+
if (Date.now() - start >= timeoutMs)
|
|
71
|
+
return false;
|
|
72
|
+
await new Promise((resolve) => setTimeout(resolve, tickMs));
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
package/dist/handlers/message.js
CHANGED
|
@@ -18,6 +18,7 @@ import { t } from "../i18n.js";
|
|
|
18
18
|
import { isHarmlessTelegramError } from "../util/telegram-error-filter.js";
|
|
19
19
|
import { handleToolResultChunk } from "./async-agent-chunk-handler.js";
|
|
20
20
|
import { createStuckTimer } from "./stuck-timer.js";
|
|
21
|
+
import { shouldBypassQueue, shouldBypassSdkResume, waitUntilProcessingFalse, } from "./background-bypass.js";
|
|
21
22
|
/**
|
|
22
23
|
* Stuck-only timeout โ NO absolute cap.
|
|
23
24
|
*
|
|
@@ -152,7 +153,8 @@ export async function handleMessage(ctx) {
|
|
|
152
153
|
text = `[Replying to previous message: "${quotedText}"]\n\n${text}`;
|
|
153
154
|
}
|
|
154
155
|
const userId = ctx.from.id;
|
|
155
|
-
const
|
|
156
|
+
const sessionKey = buildSessionKey("telegram", ctx.chat.id, userId);
|
|
157
|
+
const session = getSession(sessionKey);
|
|
156
158
|
// Track user profile
|
|
157
159
|
touchProfile(userId, ctx.from?.first_name, ctx.from?.username, "telegram", text);
|
|
158
160
|
// Sync session language from persistent profile (on first message)
|
|
@@ -163,15 +165,54 @@ export async function handleMessage(ctx) {
|
|
|
163
165
|
session.language = profile.language;
|
|
164
166
|
}
|
|
165
167
|
if (session.isProcessing) {
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
// v4.12.3 โ If a background agent is pending, the running query is
|
|
169
|
+
// almost certainly just the SDK's CLI subprocess sitting idle waiting
|
|
170
|
+
// for the task-notification to be ready (can take 5+ minutes for long
|
|
171
|
+
// audits). Don't queue โ abort the blocked query and fall through so
|
|
172
|
+
// the new message gets processed immediately. The background task
|
|
173
|
+
// itself continues in its detached subprocess; the async-agent watcher
|
|
174
|
+
// delivers the result via subagent-delivery.ts when ready.
|
|
175
|
+
if (shouldBypassQueue({
|
|
176
|
+
isProcessing: session.isProcessing,
|
|
177
|
+
pendingBackgroundCount: session.pendingBackgroundCount,
|
|
178
|
+
abortController: session.abortController,
|
|
179
|
+
})) {
|
|
180
|
+
console.log(`[v4.12.3 bypass] aborting blocked query for ${sessionKey} โ ` +
|
|
181
|
+
`${session.pendingBackgroundCount} background agent(s) pending`);
|
|
182
|
+
// Mark the abort as a bypass so the old handler's error branch
|
|
183
|
+
// doesn't surface a "request cancelled" reply to the user.
|
|
184
|
+
session._bypassAbortFired = true;
|
|
185
|
+
try {
|
|
186
|
+
session.abortController.abort();
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
/* ignore */
|
|
190
|
+
}
|
|
191
|
+
// Wait briefly for the old handler's finally to run. If it hangs
|
|
192
|
+
// (>5s, shouldn't happen), we fall through anyway โ worst case is
|
|
193
|
+
// a brief overlap where both handlers run.
|
|
194
|
+
await waitUntilProcessingFalse(session, 5000);
|
|
195
|
+
// Fall through to start a fresh query below.
|
|
170
196
|
}
|
|
171
197
|
else {
|
|
172
|
-
|
|
198
|
+
// Normal queue behavior. v4.12.3 โ emit a text reply in addition
|
|
199
|
+
// to the reaction so the user actually sees that their message
|
|
200
|
+
// was received and is waiting. Reactions alone are too subtle.
|
|
201
|
+
if (session.messageQueue.length < 3) {
|
|
202
|
+
session.messageQueue.push(text);
|
|
203
|
+
await react(ctx, "๐");
|
|
204
|
+
try {
|
|
205
|
+
await ctx.reply("โณ Eine Anfrage lรคuft gerade. Deine Nachricht ist in der Warteschlange und wird als Nรคchstes bearbeitet.");
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
/* harmless grammy race */
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
await ctx.reply("โณ Warteschlange voll (3 Nachrichten). Bitte warten oder /cancel.");
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
173
215
|
}
|
|
174
|
-
return;
|
|
175
216
|
}
|
|
176
217
|
// Consume queued messages (sent while previous query was processing)
|
|
177
218
|
if (session.messageQueue.length > 0) {
|
|
@@ -180,9 +221,23 @@ export async function handleMessage(ctx) {
|
|
|
180
221
|
}
|
|
181
222
|
session.isProcessing = true;
|
|
182
223
|
session.abortController = new AbortController();
|
|
224
|
+
// v4.12.3 โ Clear any stale bypass flag from a previous aborted turn.
|
|
225
|
+
// The flag is set by the bypass path right before it calls abort(),
|
|
226
|
+
// read by the OLD handler's error path, and cleared here by the NEW
|
|
227
|
+
// handler so it doesn't misclassify future non-bypass aborts. Use
|
|
228
|
+
// `delete` so TypeScript doesn't narrow the flag to literal `false`
|
|
229
|
+
// for the rest of this function (it's mutated from the bypass path in
|
|
230
|
+
// another handler invocation, so the type stays `boolean | undefined`).
|
|
231
|
+
delete session._bypassAbortFired;
|
|
183
232
|
const streamer = new TelegramStreamer(ctx.chat.id, ctx.api, ctx.message?.message_id);
|
|
184
233
|
let finalText = "";
|
|
185
234
|
let timedOut = false;
|
|
235
|
+
// v4.12.3 โ Tracks whether the current turn ended because the bypass
|
|
236
|
+
// path aborted us. When true, skip the finalize/broadcast/๐ reaction
|
|
237
|
+
// flow at the bottom of the handler since the user isn't waiting on
|
|
238
|
+
// this turn anymore. Explicit `boolean` type so TS doesn't narrow to
|
|
239
|
+
// the literal `false` and reject the later comparison.
|
|
240
|
+
let bypassAborted = false;
|
|
186
241
|
const typingInterval = setInterval(() => {
|
|
187
242
|
ctx.api.sendChatAction(ctx.chat.id, "typing").catch(() => { });
|
|
188
243
|
}, 4000);
|
|
@@ -280,22 +335,49 @@ export async function handleMessage(ctx) {
|
|
|
280
335
|
session.checkpointHintsInjected++;
|
|
281
336
|
}
|
|
282
337
|
}
|
|
338
|
+
// v4.12.3 โ If a background agent is still pending, skip SDK resume.
|
|
339
|
+
// The OLD SDK session is blocked waiting to deliver the
|
|
340
|
+
// task-notification inline; resuming it would inherit that block.
|
|
341
|
+
// Start a fresh SDK session and rely on the bridge preamble below
|
|
342
|
+
// to carry recent history so Claude has context.
|
|
343
|
+
const bypassResume = isSDK && shouldBypassSdkResume({
|
|
344
|
+
pendingBackgroundCount: session.pendingBackgroundCount,
|
|
345
|
+
});
|
|
346
|
+
if (bypassResume) {
|
|
347
|
+
console.log(`[v4.12.3 bypass] starting fresh SDK session for ${sessionKey} โ ` +
|
|
348
|
+
`${session.pendingBackgroundCount} background agent(s) still pending`);
|
|
349
|
+
}
|
|
283
350
|
// B2 Bridge-Message: if SDK is active but there are non-SDK turns since
|
|
284
351
|
// the last SDK turn, prepend a catch-up preamble so the SDK sees what
|
|
285
352
|
// happened during the failover. We defensively clamp the index against
|
|
286
353
|
// history bounds in case compaction shrank the array under our feet.
|
|
354
|
+
//
|
|
355
|
+
// v4.12.3 โ Bypass-resume path also gets a bridge: since we're starting
|
|
356
|
+
// a fresh SDK session, Claude has no prior context from this chat.
|
|
357
|
+
// Bridge the last BYPASS_BRIDGE_TURNS entries so it knows what we were
|
|
358
|
+
// just talking about.
|
|
359
|
+
const BYPASS_BRIDGE_TURNS = 10;
|
|
287
360
|
let bridgedPrompt = text;
|
|
288
361
|
if (isSDK) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
362
|
+
let gapStart;
|
|
363
|
+
let gapEnd;
|
|
364
|
+
if (bypassResume) {
|
|
365
|
+
gapEnd = session.history.length - 1;
|
|
366
|
+
gapStart = Math.max(0, gapEnd - BYPASS_BRIDGE_TURNS);
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
const anchor = Math.min(session.lastSdkHistoryIndex, session.history.length - 1);
|
|
370
|
+
gapStart = Math.max(0, anchor + 1);
|
|
371
|
+
// gapEnd excludes the user message we just added (history.length - 1).
|
|
372
|
+
gapEnd = session.history.length - 1;
|
|
373
|
+
}
|
|
293
374
|
if (gapEnd > gapStart) {
|
|
294
375
|
const gapTurns = session.history.slice(gapStart, gapEnd);
|
|
295
376
|
const bridge = buildBridgeMessage(gapTurns);
|
|
296
377
|
if (bridge) {
|
|
297
378
|
bridgedPrompt = bridge + text;
|
|
298
|
-
console.log(`[bridge]
|
|
379
|
+
console.log(`[bridge] ${bypassResume ? "bypass" : "SDK recovery"}: ` +
|
|
380
|
+
`injecting ${gapTurns.length} turn(s) into prompt`);
|
|
299
381
|
}
|
|
300
382
|
}
|
|
301
383
|
}
|
|
@@ -307,8 +389,8 @@ export async function handleMessage(ctx) {
|
|
|
307
389
|
abortSignal: session.abortController.signal,
|
|
308
390
|
// User's UI locale โ registry uses it to localize failure messages.
|
|
309
391
|
locale: session.language,
|
|
310
|
-
// SDK-specific
|
|
311
|
-
sessionId: isSDK ? session.sessionId : null,
|
|
392
|
+
// SDK-specific. v4.12.3 โ bypass resume when background pending.
|
|
393
|
+
sessionId: isSDK && !bypassResume ? session.sessionId : null,
|
|
312
394
|
// Unified history: SDK ignores it (uses filesystem-resume instead),
|
|
313
395
|
// non-SDK providers use it for context. Keeping it populated for both
|
|
314
396
|
// means a failover from SDK โ Ollama keeps the conversation context.
|
|
@@ -418,9 +500,12 @@ export async function handleMessage(ctx) {
|
|
|
418
500
|
// hand them off to the async-agent watcher. The watcher will
|
|
419
501
|
// poll the outputFile and deliver the result as a separate
|
|
420
502
|
// Telegram message when the background agent finishes.
|
|
503
|
+
// v4.12.3 โ Forward sessionKey so the watcher can route the
|
|
504
|
+
// delivery-complete decrement back to the right session.
|
|
421
505
|
handleToolResultChunk(chunk, {
|
|
422
506
|
chatId: ctx.chat.id,
|
|
423
507
|
userId,
|
|
508
|
+
sessionKey,
|
|
424
509
|
lastToolUseInput: lastAgentToolUseInput,
|
|
425
510
|
});
|
|
426
511
|
// Reset the captured input โ only the immediately following
|
|
@@ -447,6 +532,15 @@ export async function handleMessage(ctx) {
|
|
|
447
532
|
await ctx.reply(`โก _${chunk.failedProvider} unavailable โ switching to ${chunk.providerName}_`, { parse_mode: "Markdown" });
|
|
448
533
|
break;
|
|
449
534
|
case "error":
|
|
535
|
+
// v4.12.3 โ If the bypass path aborted us, swallow the error
|
|
536
|
+
// silently. The new handler is already preparing to process
|
|
537
|
+
// the user's next message; showing a cancellation notice here
|
|
538
|
+
// would be misleading.
|
|
539
|
+
if (session._bypassAbortFired === true &&
|
|
540
|
+
chunk.error?.toLowerCase().includes("abort")) {
|
|
541
|
+
bypassAborted = true;
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
450
544
|
// If our stuck-timer fired, the abort travels up as a registry
|
|
451
545
|
// mid-stream error chunk. Prefer the explicit stuck message over
|
|
452
546
|
// the generic one so the user understands this was a real hang,
|
|
@@ -460,6 +554,11 @@ export async function handleMessage(ctx) {
|
|
|
460
554
|
break;
|
|
461
555
|
}
|
|
462
556
|
}
|
|
557
|
+
if (bypassAborted) {
|
|
558
|
+
// v4.12.3 โ Bypass path took over; don't finalize, don't react ๐.
|
|
559
|
+
// Just clean up and return. The finally block still fires.
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
463
562
|
await streamer.finalize(finalText);
|
|
464
563
|
emit("message:sent", { userId, text: finalText, platform: "telegram" });
|
|
465
564
|
// v4.5.0: tell observers the response is complete.
|
|
@@ -499,14 +598,26 @@ export async function handleMessage(ctx) {
|
|
|
499
598
|
catch (err) {
|
|
500
599
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
501
600
|
const lang = session.language;
|
|
502
|
-
|
|
503
|
-
|
|
601
|
+
// v4.12.3 โ If this handler was interrupted by the bypass path
|
|
602
|
+
// (another handler aborted us to process a new message while a
|
|
603
|
+
// background agent is pending), silently absorb the abort error.
|
|
604
|
+
// Showing "request cancelled" would be misleading โ from the
|
|
605
|
+
// user's point of view, nothing was cancelled, their new message
|
|
606
|
+
// is just being processed.
|
|
607
|
+
const absorbBypassAbort = errorMsg.includes("abort") && session._bypassAbortFired === true;
|
|
608
|
+
if (absorbBypassAbort) {
|
|
609
|
+
// Do NOT react ๐ or reply โ just clean up silently.
|
|
610
|
+
}
|
|
611
|
+
else if (timedOut) {
|
|
612
|
+
await react(ctx, "๐");
|
|
504
613
|
await ctx.reply(t("bot.error.timeoutStuck", lang, { min: STUCK_TIMEOUT_MINUTES }));
|
|
505
614
|
}
|
|
506
615
|
else if (errorMsg.includes("abort")) {
|
|
616
|
+
await react(ctx, "๐");
|
|
507
617
|
await ctx.reply(t("bot.error.requestCancelled", lang));
|
|
508
618
|
}
|
|
509
619
|
else if (!isHarmlessTelegramError(err)) {
|
|
620
|
+
await react(ctx, "๐");
|
|
510
621
|
// Drop benign grammy races ("message is not modified", etc.)
|
|
511
622
|
// instead of surfacing them as "Fehler: ..." replies.
|
|
512
623
|
await ctx.reply(`${t("bot.error.prefix", lang)} ${errorMsg}`);
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,57 @@ if (hasLegacyData()) {
|
|
|
20
20
|
}
|
|
21
21
|
// 3. Seed defaults for any files that don't exist yet (fresh install)
|
|
22
22
|
seedDefaults();
|
|
23
|
+
// 3a. v4.12.2 โ Audit + repair permissions on sensitive files. On multi-user
|
|
24
|
+
// systems, files written pre-v4.12.2 may have 0o644 / 0o666 mode โ i.e.
|
|
25
|
+
// readable by other users on the same machine. This routine chmod-repairs
|
|
26
|
+
// them to 0o600 (owner read/write only) at every startup. Idempotent for
|
|
27
|
+
// already-secure files; silent no-op for missing files.
|
|
28
|
+
import { auditSensitiveFiles } from "./services/file-permissions.js";
|
|
29
|
+
import { ENV_FILE as SEC_ENV, SESSIONS_STATE_FILE, MEMORY_FILE, CRON_FILE as SEC_CRON } from "./paths.js";
|
|
30
|
+
import { readdirSync } from "fs";
|
|
31
|
+
import { resolve as pathResolve } from "path";
|
|
32
|
+
import { MEMORY_DIR as SEC_MEM_DIR, DATA_DIR as SEC_DATA_DIR } from "./paths.js";
|
|
33
|
+
{
|
|
34
|
+
const sensitivePaths = [SEC_ENV, SESSIONS_STATE_FILE, MEMORY_FILE, SEC_CRON];
|
|
35
|
+
// Also audit every daily-log markdown file โ they contain full conversation history
|
|
36
|
+
try {
|
|
37
|
+
if (readdirSync.length !== undefined) {
|
|
38
|
+
for (const entry of readdirSync(SEC_MEM_DIR)) {
|
|
39
|
+
if (entry.endsWith(".md") && !entry.startsWith(".")) {
|
|
40
|
+
sensitivePaths.push(pathResolve(SEC_MEM_DIR, entry));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// memory dir missing โ fine
|
|
47
|
+
}
|
|
48
|
+
// Also include async-agents state, delivery queue, and sudo credentials
|
|
49
|
+
const optionalPaths = [
|
|
50
|
+
pathResolve(SEC_DATA_DIR, "state", "async-agents.json"),
|
|
51
|
+
pathResolve(SEC_DATA_DIR, "delivery-queue.json"),
|
|
52
|
+
pathResolve(SEC_DATA_DIR, "data", ".sudo-enc"),
|
|
53
|
+
pathResolve(SEC_DATA_DIR, "data", ".sudo-key"),
|
|
54
|
+
pathResolve(SEC_DATA_DIR, "data", "access.json"),
|
|
55
|
+
pathResolve(SEC_DATA_DIR, "data", "approved-users.json"),
|
|
56
|
+
];
|
|
57
|
+
sensitivePaths.push(...optionalPaths);
|
|
58
|
+
const auditResults = auditSensitiveFiles(sensitivePaths);
|
|
59
|
+
const repaired = auditResults.filter(r => r.status === "repaired");
|
|
60
|
+
if (repaired.length > 0) {
|
|
61
|
+
console.log(`๐ file-permissions: repaired ${repaired.length} sensitive file(s) to 0o600`);
|
|
62
|
+
for (const r of repaired) {
|
|
63
|
+
console.log(` ${r.path} (was 0o${r.previousMode})`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const errors = auditResults.filter(r => r.status === "error");
|
|
67
|
+
if (errors.length > 0) {
|
|
68
|
+
console.warn(`โ ๏ธ file-permissions: ${errors.length} file(s) could not be repaired:`);
|
|
69
|
+
for (const r of errors) {
|
|
70
|
+
console.warn(` ${r.path}: ${r.error}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
23
74
|
// 4. Crash-loop brake check โ if we've crashed N times in a short window,
|
|
24
75
|
// refuse to start, write an alert file, and unload our LaunchAgent so
|
|
25
76
|
// launchd stops retrying. Runs BEFORE any expensive init so a broken
|
|
@@ -35,9 +86,30 @@ if (!hasTelegram) {
|
|
|
35
86
|
console.warn("โ ๏ธ BOT_TOKEN not set โ Telegram disabled. WebUI + Cron still active.");
|
|
36
87
|
console.warn(" Run 'alvin-bot setup' or set BOT_TOKEN in ~/.alvin-bot/.env");
|
|
37
88
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
89
|
+
// v4.12.2 โ ALLOWED_USERS startup gate. Refuses to start when Telegram is
|
|
90
|
+
// configured but no user allowlist is set, because that would leave the bot
|
|
91
|
+
// open to any Telegram user with full shell/filesystem access via prompt
|
|
92
|
+
// injection. See src/services/allowed-users-gate.ts for the pure decision
|
|
93
|
+
// function + tests.
|
|
94
|
+
{
|
|
95
|
+
const { checkAllowedUsersGate } = await import("./services/allowed-users-gate.js");
|
|
96
|
+
const gate = checkAllowedUsersGate({
|
|
97
|
+
hasTelegram,
|
|
98
|
+
allowedUsersCount: config.allowedUsers.length,
|
|
99
|
+
authMode: config.authMode,
|
|
100
|
+
insecureAcknowledged: process.env.ALVIN_INSECURE_ACKNOWLEDGED === "1",
|
|
101
|
+
});
|
|
102
|
+
if (!gate.allowed) {
|
|
103
|
+
console.error("");
|
|
104
|
+
console.error("โ CRITICAL: Alvin Bot refusing to start.");
|
|
105
|
+
console.error("");
|
|
106
|
+
console.error(" " + gate.reason);
|
|
107
|
+
console.error("");
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
if (gate.warning) {
|
|
111
|
+
console.warn("โ ๏ธ " + gate.warning);
|
|
112
|
+
}
|
|
41
113
|
}
|
|
42
114
|
// Check if the chosen provider has a corresponding API key.
|
|
43
115
|
// Keys here MUST match the registry keys from src/providers/registry.ts
|