alvin-bot 4.18.0 → 4.18.2
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/AEC-PLUGINS-SOURCES.md +53 -0
- package/CHANGELOG.md +37 -2
- package/DESIGN-SKILLS-SOURCES.md +81 -0
- package/bin/cli.js +1 -1
- package/dist/providers/claude-sdk-provider.js +24 -0
- package/package.json +3 -1
- package/test/allowed-users-gate.test.ts +0 -98
- package/test/alvin-dispatch.test.ts +0 -220
- package/test/async-agent-chunk-flow.test.ts +0 -244
- package/test/async-agent-parser-staleness.test.ts +0 -412
- package/test/async-agent-parser-streamjson.test.ts +0 -273
- package/test/async-agent-parser.test.ts +0 -322
- package/test/async-agent-watcher.test.ts +0 -229
- package/test/background-bypass-integration.test.ts +0 -443
- package/test/background-bypass-stress.test.ts +0 -417
- package/test/background-bypass.test.ts +0 -127
- package/test/browser-webfetch.test.ts +0 -121
- package/test/claude-sdk-provider.test.ts +0 -115
- package/test/claude-sdk-tool-use-id.test.ts +0 -180
- package/test/console-timestamps.test.ts +0 -98
- package/test/cron-progress-ticker.test.ts +0 -76
- package/test/cron-restart-resilience.test.ts +0 -191
- package/test/cron-run-resolver.test.ts +0 -133
- package/test/cron-runjobnow-throw.test.ts +0 -100
- package/test/debounce.test.ts +0 -60
- package/test/delivery-registry.test.ts +0 -71
- package/test/exec-guard-metachars.test.ts +0 -110
- package/test/file-permissions.test.ts +0 -130
- package/test/i18n.test.ts +0 -108
- package/test/list-subagents-merged.test.ts +0 -172
- package/test/memory-extractor.test.ts +0 -151
- package/test/memory-layers.test.ts +0 -169
- package/test/memory-sdk-injection.test.ts +0 -146
- package/test/memory-stress-restart.test.ts +0 -337
- package/test/multi-session-stress.test.ts +0 -255
- package/test/platform-session-key.test.ts +0 -69
- package/test/process-manager.test.ts +0 -186
- package/test/registry.test.ts +0 -201
- package/test/session-pending-background.test.ts +0 -59
- package/test/session-persistence.test.ts +0 -195
- package/test/slack-progress-ticker.test.ts +0 -123
- package/test/slack-slash-command.test.ts +0 -61
- package/test/slack-test-connection.test.ts +0 -176
- package/test/stress-scenarios.test.ts +0 -356
- package/test/stuck-timer.test.ts +0 -116
- package/test/subagent-delivery-markdown-fallback.test.ts +0 -147
- package/test/subagent-delivery-platform-routing.test.ts +0 -232
- package/test/subagent-delivery.test.ts +0 -273
- package/test/subagent-final-text.test.ts +0 -132
- package/test/subagent-stats.test.ts +0 -119
- package/test/subagent-toolset-allowlist.test.ts +0 -146
- package/test/subagents-commands.test.ts +0 -64
- package/test/subagents-config.test.ts +0 -114
- package/test/subagents-depth.test.ts +0 -58
- package/test/subagents-inheritance.test.ts +0 -67
- package/test/subagents-name-resolver.test.ts +0 -122
- package/test/subagents-priority-reject.test.ts +0 -88
- package/test/subagents-queue.test.ts +0 -127
- package/test/subagents-shutdown.test.ts +0 -126
- package/test/subagents-toolset.test.ts +0 -71
- package/test/sync-task-timeout.test.ts +0 -153
- package/test/system-prompt-background-hint.test.ts +0 -65
- package/test/telegram-error-filter.test.ts +0 -85
- package/test/telegram-workspace-command.test.ts +0 -78
- package/test/timing-safe-bearer.test.ts +0 -65
- package/test/watchdog-brake.test.ts +0 -157
- package/test/watcher-pending-count.test.ts +0 -228
- package/test/watcher-zombie-fix.test.ts +0 -252
- package/test/web-server-integration.test.ts +0 -189
- package/test/web-server-resilience.test.ts +0 -118
- package/test/web-server-shutdown.test.ts +0 -117
- package/test/whatsapp-auth-resilience.test.ts +0 -96
- package/test/workspaces.test.ts +0 -196
- package/vitest.config.ts +0 -17
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# AEC Plugin Skills — Installed 22.04.2026
|
|
2
|
+
|
|
3
|
+
54 Skills aus 3 Claude-Code-Plugins, erstellt von **Abhinav Bhardwaj** (GitHub: `Amanbh997`).
|
|
4
|
+
Installiert via direkte Kopie in `~/.claude/skills/` — Claude Code auto-discovered via SKILL.md frontmatter.
|
|
5
|
+
|
|
6
|
+
Vorher: 89 Skills. Nachher: **143 Skills** (+54).
|
|
7
|
+
|
|
8
|
+
## Quellen
|
|
9
|
+
|
|
10
|
+
| Plugin | Repo | Skills | Stars |
|
|
11
|
+
|---|---|---|---|
|
|
12
|
+
| **Urban Design** | [Amanbh997/Urban-Design-Skills-Claude](https://github.com/Amanbh997/Urban-Design-Skills-Claude) | 18 | ⭐ 76 |
|
|
13
|
+
| **Architecture** | [Amanbh997/Skills-Architects](https://github.com/Amanbh997/Skills-Architects) | 18 | ⭐ 118 |
|
|
14
|
+
| **Computational Design** | [Amanbh997/Claude-skills-for-Computational-Designers](https://github.com/Amanbh997/Claude-skills-for-Computational-Designers) | 18 | ⭐ 127 |
|
|
15
|
+
|
|
16
|
+
Jedes Plugin: 35.000+ Zeilen, 7 Python-Calculators, 50+ Theorists, 30+ Tools, hunderte numerische Benchmarks.
|
|
17
|
+
|
|
18
|
+
## Skill-Inventar
|
|
19
|
+
|
|
20
|
+
### Urban Design (18)
|
|
21
|
+
block-and-density · climate-responsive-design · cost-estimation · design-brief · design-evaluation · masterplan-design · mixed-use-programming · mobility-and-transport · precedent-study · public-space-design · site-analysis · street-design · sustainability-scoring · tod-design · urban-calculator · urban-design-foundations · urban-regeneration · zoning-and-codes
|
|
22
|
+
|
|
23
|
+
### Architecture (18)
|
|
24
|
+
accessibility-design · acoustic-design · architect-calculator · architect-foundations · building-codes · building-envelope · building-programming · building-services · building-sustainability · building-typology · concept-design · construction-documentation · daylighting-design · design-theory · fire-life-safety · material-selection · spatial-planning · structural-systems
|
|
25
|
+
|
|
26
|
+
### Computational Design (18)
|
|
27
|
+
algorithmic-patterns · bim-scripting · cd-calculator · cd-foundations · computational-geometry · data-driven-design · design-automation · digital-fabrication · environmental-simulation · facade-computation · generative-design · interoperability · mesh-processing · ml-for-aec · optimization-methods · parametric-modeling · scripting-reference · structural-computation
|
|
28
|
+
|
|
29
|
+
## Update ziehen
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
cd /tmp && rm -rf aec-skills && mkdir aec-skills && cd aec-skills
|
|
33
|
+
git clone --depth 1 https://github.com/Amanbh997/Urban-Design-Skills-Claude.git
|
|
34
|
+
git clone --depth 1 https://github.com/Amanbh997/Skills-Architects.git
|
|
35
|
+
git clone --depth 1 https://github.com/Amanbh997/Claude-skills-for-Computational-Designers.git
|
|
36
|
+
cp -R Urban-Design-Skills-Claude/skills/* ~/.claude/skills/
|
|
37
|
+
cp -R Skills-Architects/skills/* ~/.claude/skills/
|
|
38
|
+
cp -R Claude-skills-for-Computational-Designers/skills/* ~/.claude/skills/
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Wie die Skills ausgelöst werden
|
|
42
|
+
|
|
43
|
+
Jeder Skill hat im SKILL.md frontmatter eine `description`-Zeile die spezifische User-Anfragen matcht. Beispiele:
|
|
44
|
+
|
|
45
|
+
- „Design a mixed-use quarter" → Urban Design Plugin (masterplan-design, tod-design, mixed-use-programming, zoning-and-codes, mobility-and-transport …)
|
|
46
|
+
- „Check if this floor plan meets fire code" → Architecture Plugin (fire-life-safety, building-codes, accessibility-design, construction-documentation …)
|
|
47
|
+
- „Generate a parametric facade pattern in Grasshopper" → Computational Design Plugin (parametric-modeling, facade-computation, algorithmic-patterns, digital-fabrication …)
|
|
48
|
+
|
|
49
|
+
Laut Creator: *"Ask Claude to design a mixed-use quarter and all three plugins kick in working together."*
|
|
50
|
+
|
|
51
|
+
## Quelle
|
|
52
|
+
Reel vom Creator: https://www.instagram.com/reel/DXXhJS2kqAf/
|
|
53
|
+
Uploader: Abhinav Bhardwaj / "Claude for AEC!"
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Alvin Bot are documented here.
|
|
4
4
|
|
|
5
|
+
## [4.18.2] — 2026-04-23
|
|
6
|
+
|
|
7
|
+
### 🐛 Fix: silent empty-stream after OAuth-token rotation
|
|
8
|
+
|
|
9
|
+
**Problem:** After running `/extra-usage`, `/login`, or any other flow that rotates the Claude OAuth token in the macOS Keychain, the Alvin-Bot silently broke for long-lived sessions. The in-memory Claude SDK client held the old token, the CLI subprocess emitted no text chunks (the 401 was swallowed upstream), the stream terminated normally with zero output tokens, and the user saw the fallback `"(Keine Antwort)"` — with no indication that a token refresh was needed.
|
|
10
|
+
|
|
11
|
+
**Fix** (`src/providers/claude-sdk-provider.ts`): In the `result` branch of the SDK stream loop, detect the empty-stream signature (`accumulatedText === ""` and `outputTokens === 0`). When that fires:
|
|
12
|
+
|
|
13
|
+
1. Invalidate the `isAvailable()` cache so the next heartbeat probe spawns a fresh CLI subprocess that reads the current Keychain entry.
|
|
14
|
+
2. Yield an explicit `error` chunk with actionable text so the user sees *"…token rotation — please resend your message"* instead of a silent `"(Keine Antwort)"`.
|
|
15
|
+
|
|
16
|
+
**Applies to:** every token-rotation flow — extra-usage activation, extra-usage expiry, weekly-reset (no rotation → unaffected), manual `claude login`.
|
|
17
|
+
|
|
18
|
+
**Net effect:** Bot self-heals after token changes. A single resend on the user side is enough; no manual restart required.
|
|
19
|
+
|
|
20
|
+
## [4.18.1] — 2026-04-20
|
|
21
|
+
|
|
22
|
+
### 🔒 Privacy-Guard: pre-publish check blocks PII leaks in shipped files
|
|
23
|
+
|
|
24
|
+
Adds an automated gate that runs on every `npm publish` and prevents personal information from accidentally shipping. After the 4.18.0 privacy sanitization, this ensures it never happens again.
|
|
25
|
+
|
|
26
|
+
**New:**
|
|
27
|
+
- `scripts/privacy-check.sh` — scans the exact file list that `npm pack` would ship. Case-insensitive regex match against a patterns file. Any hit fails the publish.
|
|
28
|
+
- `scripts/privacy-patterns.default.txt` — bundled, contains only generic patterns (email shape, IP addresses, postal codes, personal task phrasings). No project or person names — so safe to ship.
|
|
29
|
+
- `package.json` `prepublishOnly` hook — runs the check automatically.
|
|
30
|
+
- `npm run privacy-check` — manual run anytime.
|
|
31
|
+
|
|
32
|
+
**Maintainer-local overrides:** Put `~/.alvin-bot/privacy-patterns.txt` with personal/project-specific patterns. That file is gitignored, never leaves your machine, and takes precedence over the bundled defaults.
|
|
33
|
+
|
|
34
|
+
**CI override:** Set `$ALVIN_PRIVACY_PATTERNS` to an absolute path; takes top precedence over both files above.
|
|
35
|
+
|
|
36
|
+
**Hardening: `.npmignore`** — added `test/` and `vitest.config.ts` to the ignore list. Previously the full test suite shipped with every npm tarball, adding ~2 MB and exposing test fixtures that sometimes referenced internal project names.
|
|
37
|
+
|
|
38
|
+
**CLAUDE.md** — documents the rule and the patterns-file lookup order so future maintenance sessions catch new cases proactively.
|
|
39
|
+
|
|
5
40
|
## [4.18.0] — 2026-04-20
|
|
6
41
|
|
|
7
42
|
### ⚡ Performance + Hardening: medium-priority cleanups from the stability audit
|
|
@@ -169,8 +204,8 @@ The three aliased entries all route through `ClaudeSDKProvider` with different `
|
|
|
169
204
|
|
|
170
205
|
```yaml
|
|
171
206
|
---
|
|
172
|
-
purpose:
|
|
173
|
-
cwd: ~/
|
|
207
|
+
purpose: my-project
|
|
208
|
+
cwd: ~/Projects/my-project
|
|
174
209
|
model: sonnet # opus | sonnet | haiku | claude-opus-4-7 | ...
|
|
175
210
|
---
|
|
176
211
|
```
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Design Skills — Installed 22.04.2026
|
|
2
|
+
|
|
3
|
+
19 neue Skills aus 2 Repos + Bestätigung dass Frontend Design bereits via Anthropic Plugin installiert ist.
|
|
4
|
+
|
|
5
|
+
## Quellen
|
|
6
|
+
|
|
7
|
+
| Skill | Repo | Stars | Status |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| **Impeccable** (18 commands) | [pbakaus/impeccable](https://github.com/pbakaus/impeccable) | ⭐ 21.419 | ✅ Neu installiert |
|
|
10
|
+
| **Design Motion Principles** | [kylezantos/design-motion-principles](https://github.com/kylezantos/design-motion-principles) | ⭐ 293 | ✅ Neu installiert |
|
|
11
|
+
| **Frontend Design** | [anthropics/skills/frontend-design](https://github.com/anthropics/skills) | — | ℹ️ War bereits aktiv (Plugin `frontend-design:frontend-design`) |
|
|
12
|
+
|
|
13
|
+
## Reel-Quelle
|
|
14
|
+
Marc Cleroux auf Instagram — https://www.instagram.com/reel/DXdVUiOjKUv/ (22.04.2026)
|
|
15
|
+
|
|
16
|
+
## Impeccable-Commands (18)
|
|
17
|
+
|
|
18
|
+
Vokabular um Claude bei frontend-design zu steuern wenn dir die richtigen Worte fehlen.
|
|
19
|
+
|
|
20
|
+
| Command | Funktion |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `/impeccable` | Core skill mit 18 commands + 7 Referenz-Domänen |
|
|
23
|
+
| `/adapt` | Responsive / Breakpoints / Touch-Targets |
|
|
24
|
+
| `/animate` | Purposeful motion + micro-interactions |
|
|
25
|
+
| `/audit` | Technische Quality Checks (a11y, perf, responsive) + scored report |
|
|
26
|
+
| `/bolder` | Bland → visuell interessanter |
|
|
27
|
+
| `/clarify` | Unklare UX-Copy verbessern |
|
|
28
|
+
| `/colorize` | Strategische Farbe einführen |
|
|
29
|
+
| `/critique` | UX Design Review (Hierarchie, Klarheit, Resonanz) |
|
|
30
|
+
| `/delight` | Joy-Momente, Personality, Memorable Touches |
|
|
31
|
+
| `/distill` | Auf Essenz reduzieren, Complexity weg |
|
|
32
|
+
| `/harden` | Error Handling, Empty States, Onboarding, i18n, Edge Cases |
|
|
33
|
+
| `/layout` | Layout / Spacing / Visual Rhythm fixen |
|
|
34
|
+
| `/optimize` | Performance (Bundle, Rendering, Images) |
|
|
35
|
+
| `/overdrive` | Technisch ambitioniert — Shaders, Spring Physics, 60fps |
|
|
36
|
+
| `/polish` | Final Quality Pass pre-shipping |
|
|
37
|
+
| `/quieter` | Overstimulating Design beruhigen |
|
|
38
|
+
| `/shape` | UX/UI-Plan vor Code |
|
|
39
|
+
| `/typeset` | Typografie fixen |
|
|
40
|
+
|
|
41
|
+
### 7 Referenz-Domänen in `impeccable`
|
|
42
|
+
typography · color-and-contrast · spatial-design · motion-design · interaction-design · responsive-design · ux-writing
|
|
43
|
+
|
|
44
|
+
## Design Motion Principles
|
|
45
|
+
|
|
46
|
+
Motion-Audit-Skill basierend auf den Philosophien von:
|
|
47
|
+
- **Emil Kowalski** — iOS-native UI polish
|
|
48
|
+
- **Jakub Krehel** — smooth interaction design
|
|
49
|
+
- **Jhey Tompkins** — creative web animation
|
|
50
|
+
|
|
51
|
+
Use case: *"Audit the hover states and transitions on my landing page"* → strukturierter Motion-Report.
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
Geklont + direkt in `~/.claude/skills/` einzeln kopiert (Impeccable folgt dem Pattern aus ihrer README: `cp -r dist/claude-code/.claude/* ~/.claude/`).
|
|
56
|
+
|
|
57
|
+
## Update ziehen
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
cd /tmp && rm -rf design-skills && mkdir design-skills && cd design-skills
|
|
61
|
+
git clone --depth 1 https://github.com/pbakaus/impeccable.git
|
|
62
|
+
git clone --depth 1 https://github.com/kylezantos/design-motion-principles.git
|
|
63
|
+
cp -R impeccable/.claude/skills/. ~/.claude/skills/
|
|
64
|
+
cp -R design-motion-principles/skills/design-motion-principles ~/.claude/skills/
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Stand Skills-Inventar (22.04.2026)
|
|
68
|
+
- Vorher: 143 (nach AEC-Installation)
|
|
69
|
+
- Nachher: **162** (+19)
|
|
70
|
+
|
|
71
|
+
## Nebenbaustelle
|
|
72
|
+
Impeccable bringt zusätzlich eine `.claude/agents/anti-patterns.md` mit — nicht installiert (würde Permission für `~/.claude/agents/` brauchen). Das ist optional: Die 18 Skills funktionieren unabhängig vom Anti-Patterns-Agent. Falls später gewünscht: `cp -R /tmp/design-skills/impeccable/.claude/agents/. ~/.claude/agents/`
|
|
73
|
+
|
|
74
|
+
## Empfohlene Workflow-Kombination
|
|
75
|
+
|
|
76
|
+
Laut Marc Cleroux im Reel ist das Power-Trio:
|
|
77
|
+
1. **shape** (plan before code) — Teil von Impeccable
|
|
78
|
+
2. **frontend-design** (implement mit Geschmack) — schon installiert
|
|
79
|
+
3. **audit** + **critique** + **polish** (iterieren) — Impeccable
|
|
80
|
+
|
|
81
|
+
Bei jedem neuen Frontend-Build: `/shape` → `/impeccable craft` → `/audit` → `/critique` → `/polish` → shippen.
|
package/bin/cli.js
CHANGED
|
@@ -1828,7 +1828,7 @@ switch (cmd) {
|
|
|
1828
1828
|
const searchQuery = process.argv.slice(3).join(" ");
|
|
1829
1829
|
if (!searchQuery) {
|
|
1830
1830
|
console.log("Usage: alvin-bot search <query>");
|
|
1831
|
-
console.log('Example: alvin-bot search "
|
|
1831
|
+
console.log('Example: alvin-bot search "tax document 2024"');
|
|
1832
1832
|
process.exit(1);
|
|
1833
1833
|
}
|
|
1834
1834
|
const { searchSelf, formatSearchResults } = await import("../dist/services/self-search.js");
|
|
@@ -309,6 +309,30 @@ export class ClaudeSDKProvider {
|
|
|
309
309
|
? (usage.input_tokens || 0) + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0)
|
|
310
310
|
: 0;
|
|
311
311
|
const outputTok = usage?.output_tokens || 0;
|
|
312
|
+
// v4.18.2 — Silent-empty-stream detection.
|
|
313
|
+
//
|
|
314
|
+
// If the stream terminated cleanly but produced ZERO text chunks,
|
|
315
|
+
// something went wrong that the SDK didn't surface as an error:
|
|
316
|
+
// most commonly a stale OAuth token after /extra-usage or /login
|
|
317
|
+
// rotated the Keychain entry while our in-memory SDK client was
|
|
318
|
+
// still holding the old one. The CLI subprocess silently gets a
|
|
319
|
+
// 401, emits no text, and we complete the stream with
|
|
320
|
+
// accumulatedText === "". The user sees "(Keine Antwort)".
|
|
321
|
+
//
|
|
322
|
+
// We flip this from silent failure to explicit error. Clearing
|
|
323
|
+
// the availability cache forces the next heartbeat probe to
|
|
324
|
+
// re-check `claude auth status` with a fresh subprocess (which
|
|
325
|
+
// reads the current Keychain entry).
|
|
326
|
+
if (accumulatedText === "" && outputTok === 0) {
|
|
327
|
+
this.invalidateAvailabilityCache();
|
|
328
|
+
yield {
|
|
329
|
+
type: "error",
|
|
330
|
+
error: "Claude returned an empty response. " +
|
|
331
|
+
"This can happen right after /extra-usage, /login, or a token refresh — " +
|
|
332
|
+
"the SDK held a stale auth token. I've invalidated the cache; please resend your message.",
|
|
333
|
+
};
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
312
336
|
yield {
|
|
313
337
|
type: "done",
|
|
314
338
|
text: accumulatedText,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alvin-bot",
|
|
3
|
-
"version": "4.18.
|
|
3
|
+
"version": "4.18.2",
|
|
4
4
|
"description": "Alvin Bot \u2014 Your personal AI agent on Telegram, WhatsApp, Discord, Signal, and Web.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
"test": "vitest run",
|
|
16
16
|
"test:watch": "vitest",
|
|
17
17
|
"test:ui": "vitest --ui",
|
|
18
|
+
"privacy-check": "bash scripts/privacy-check.sh",
|
|
19
|
+
"prepublishOnly": "bash scripts/privacy-check.sh",
|
|
18
20
|
"electron:compile": "tsc -p electron/tsconfig.json",
|
|
19
21
|
"electron:dev": "npm run electron:compile && electron .",
|
|
20
22
|
"electron:build": "npm run build && npm run electron:compile && electron-builder --publish never",
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* v4.12.2 — ALLOWED_USERS startup hard-fail gate.
|
|
3
|
-
*
|
|
4
|
-
* When the Telegram bot token is configured but ALLOWED_USERS is empty,
|
|
5
|
-
* starting the bot would leave it open to any Telegram user sending a DM.
|
|
6
|
-
* Previously this only emitted a console.warn and the bot started anyway.
|
|
7
|
-
*
|
|
8
|
-
* v4.12.2 introduces a pure gate function that decides whether to refuse
|
|
9
|
-
* startup, with two explicit escape hatches:
|
|
10
|
-
* 1. AUTH_MODE=open — user explicitly wants an open bot
|
|
11
|
-
* 2. ALVIN_INSECURE_ACKNOWLEDGED=1 — explicit opt-out for test/scripted envs
|
|
12
|
-
*
|
|
13
|
-
* This test file exercises the pure gate. The actual wiring in src/index.ts
|
|
14
|
-
* is a thin if-block that calls process.exit(1) on deny.
|
|
15
|
-
*/
|
|
16
|
-
import { describe, it, expect } from "vitest";
|
|
17
|
-
import { checkAllowedUsersGate } from "../src/services/allowed-users-gate.js";
|
|
18
|
-
|
|
19
|
-
describe("allowed-users-gate (v4.12.2)", () => {
|
|
20
|
-
it("allows startup when ALLOWED_USERS is populated", () => {
|
|
21
|
-
const result = checkAllowedUsersGate({
|
|
22
|
-
hasTelegram: true,
|
|
23
|
-
allowedUsersCount: 1,
|
|
24
|
-
authMode: "allowlist",
|
|
25
|
-
insecureAcknowledged: false,
|
|
26
|
-
});
|
|
27
|
-
expect(result.allowed).toBe(true);
|
|
28
|
-
expect(result.reason).toBeUndefined();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("BLOCKS startup when telegram enabled but allowedUsers empty (allowlist mode)", () => {
|
|
32
|
-
const result = checkAllowedUsersGate({
|
|
33
|
-
hasTelegram: true,
|
|
34
|
-
allowedUsersCount: 0,
|
|
35
|
-
authMode: "allowlist",
|
|
36
|
-
insecureAcknowledged: false,
|
|
37
|
-
});
|
|
38
|
-
expect(result.allowed).toBe(false);
|
|
39
|
-
expect(result.reason).toContain("ALLOWED_USERS");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("BLOCKS startup when telegram enabled but allowedUsers empty (pairing mode)", () => {
|
|
43
|
-
// Pairing mode needs allowedUsers[0] as the admin for approval routing.
|
|
44
|
-
// Empty array breaks the whole pairing flow.
|
|
45
|
-
const result = checkAllowedUsersGate({
|
|
46
|
-
hasTelegram: true,
|
|
47
|
-
allowedUsersCount: 0,
|
|
48
|
-
authMode: "pairing",
|
|
49
|
-
insecureAcknowledged: false,
|
|
50
|
-
});
|
|
51
|
-
expect(result.allowed).toBe(false);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("ALLOWS startup when AUTH_MODE=open explicitly", () => {
|
|
55
|
-
const result = checkAllowedUsersGate({
|
|
56
|
-
hasTelegram: true,
|
|
57
|
-
allowedUsersCount: 0,
|
|
58
|
-
authMode: "open",
|
|
59
|
-
insecureAcknowledged: false,
|
|
60
|
-
});
|
|
61
|
-
expect(result.allowed).toBe(true);
|
|
62
|
-
expect(result.warning).toContain("open");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("ALLOWS startup when ALVIN_INSECURE_ACKNOWLEDGED=1", () => {
|
|
66
|
-
const result = checkAllowedUsersGate({
|
|
67
|
-
hasTelegram: true,
|
|
68
|
-
allowedUsersCount: 0,
|
|
69
|
-
authMode: "allowlist",
|
|
70
|
-
insecureAcknowledged: true,
|
|
71
|
-
});
|
|
72
|
-
expect(result.allowed).toBe(true);
|
|
73
|
-
expect(result.warning).toContain("INSECURE");
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("ALLOWS startup when telegram is NOT enabled (bot is WebUI-only)", () => {
|
|
77
|
-
// WebUI-only deployments don't have a BOT_TOKEN and don't need
|
|
78
|
-
// ALLOWED_USERS — the gate only applies when hasTelegram === true.
|
|
79
|
-
const result = checkAllowedUsersGate({
|
|
80
|
-
hasTelegram: false,
|
|
81
|
-
allowedUsersCount: 0,
|
|
82
|
-
authMode: "allowlist",
|
|
83
|
-
insecureAcknowledged: false,
|
|
84
|
-
});
|
|
85
|
-
expect(result.allowed).toBe(true);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("reason message mentions ~/.alvin-bot/.env and @userinfobot for operator guidance", () => {
|
|
89
|
-
const result = checkAllowedUsersGate({
|
|
90
|
-
hasTelegram: true,
|
|
91
|
-
allowedUsersCount: 0,
|
|
92
|
-
authMode: "allowlist",
|
|
93
|
-
insecureAcknowledged: false,
|
|
94
|
-
});
|
|
95
|
-
expect(result.reason).toMatch(/\.env|alvin-bot/i);
|
|
96
|
-
expect(result.reason).toMatch(/userinfobot|telegram/i);
|
|
97
|
-
});
|
|
98
|
-
});
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* v4.13 — alvin_dispatch custom-tool service.
|
|
3
|
-
*
|
|
4
|
-
* `dispatchDetachedAgent(input)` spawns a truly independent `claude -p`
|
|
5
|
-
* subprocess that survives the parent handler's abort. This is the
|
|
6
|
-
* architectural replacement for SDK's built-in Task(run_in_background)
|
|
7
|
-
* tool, which was tied to the parent SDK subprocess lifecycle.
|
|
8
|
-
*
|
|
9
|
-
* Contract:
|
|
10
|
-
* - Input: { prompt, description, chatId, userId, sessionKey }
|
|
11
|
-
* - Output (synchronous): { agentId, outputFile, spawned: true }
|
|
12
|
-
* - Side effect: spawns detached subprocess writing stream-json
|
|
13
|
-
* output to outputFile, registers with async-agent-watcher.
|
|
14
|
-
*
|
|
15
|
-
* These tests stub child_process.spawn so they run fast and deterministic.
|
|
16
|
-
* The "real subprocess survives parent" property was verified empirically
|
|
17
|
-
* in Phase A (see plan doc).
|
|
18
|
-
*/
|
|
19
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
20
|
-
import os from "os";
|
|
21
|
-
import fs from "fs";
|
|
22
|
-
import { resolve } from "path";
|
|
23
|
-
|
|
24
|
-
const TEST_DATA_DIR = resolve(
|
|
25
|
-
os.tmpdir(),
|
|
26
|
-
`alvin-dispatch-${process.pid}-${Date.now()}`,
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
interface SpawnRecord {
|
|
30
|
-
cmd: string;
|
|
31
|
-
args: string[];
|
|
32
|
-
opts: {
|
|
33
|
-
detached?: boolean;
|
|
34
|
-
stdio?: unknown;
|
|
35
|
-
cwd?: string;
|
|
36
|
-
env?: Record<string, string | undefined>;
|
|
37
|
-
};
|
|
38
|
-
unreffed: boolean;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let spawned: SpawnRecord[] = [];
|
|
42
|
-
|
|
43
|
-
beforeEach(async () => {
|
|
44
|
-
if (fs.existsSync(TEST_DATA_DIR))
|
|
45
|
-
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
|
46
|
-
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
|
47
|
-
process.env.ALVIN_DATA_DIR = TEST_DATA_DIR;
|
|
48
|
-
spawned = [];
|
|
49
|
-
vi.resetModules();
|
|
50
|
-
|
|
51
|
-
vi.doMock("node:child_process", async () => {
|
|
52
|
-
const actual = await vi.importActual<typeof import("node:child_process")>(
|
|
53
|
-
"node:child_process",
|
|
54
|
-
);
|
|
55
|
-
return {
|
|
56
|
-
...actual,
|
|
57
|
-
spawn: (cmd: string, args: string[], opts: SpawnRecord["opts"]) => {
|
|
58
|
-
const record: SpawnRecord = {
|
|
59
|
-
cmd,
|
|
60
|
-
args,
|
|
61
|
-
opts,
|
|
62
|
-
unreffed: false,
|
|
63
|
-
};
|
|
64
|
-
spawned.push(record);
|
|
65
|
-
return {
|
|
66
|
-
pid: 12345,
|
|
67
|
-
unref() {
|
|
68
|
-
record.unreffed = true;
|
|
69
|
-
},
|
|
70
|
-
on() {},
|
|
71
|
-
kill() {},
|
|
72
|
-
};
|
|
73
|
-
},
|
|
74
|
-
};
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
vi.doMock("../src/services/subagent-delivery.js", () => ({
|
|
78
|
-
deliverSubAgentResult: async () => {},
|
|
79
|
-
attachBotApi: () => {},
|
|
80
|
-
__setBotApiForTest: () => {},
|
|
81
|
-
}));
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
afterEach(async () => {
|
|
85
|
-
try {
|
|
86
|
-
const mod = await import("../src/services/async-agent-watcher.js");
|
|
87
|
-
mod.stopWatcher();
|
|
88
|
-
mod.__resetForTest();
|
|
89
|
-
} catch {
|
|
90
|
-
/* ignore */
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
describe("dispatchDetachedAgent (v4.13)", () => {
|
|
95
|
-
it("spawns claude -p with detached: true and unrefs", async () => {
|
|
96
|
-
const mod = await import("../src/services/alvin-dispatch.js");
|
|
97
|
-
const result = mod.dispatchDetachedAgent({
|
|
98
|
-
prompt: "research X",
|
|
99
|
-
description: "X research",
|
|
100
|
-
chatId: 42,
|
|
101
|
-
userId: 42,
|
|
102
|
-
sessionKey: "s1",
|
|
103
|
-
});
|
|
104
|
-
expect(result.agentId).toMatch(/^alvin-[a-f0-9]{16,}$/);
|
|
105
|
-
expect(result.outputFile).toContain(TEST_DATA_DIR);
|
|
106
|
-
expect(result.spawned).toBe(true);
|
|
107
|
-
|
|
108
|
-
expect(spawned).toHaveLength(1);
|
|
109
|
-
const [s] = spawned;
|
|
110
|
-
expect(s.cmd).toMatch(/claude/);
|
|
111
|
-
expect(s.args).toContain("-p");
|
|
112
|
-
expect(s.args).toContain("research X");
|
|
113
|
-
expect(s.args).toContain("--output-format");
|
|
114
|
-
expect(s.args).toContain("stream-json");
|
|
115
|
-
expect(s.opts.detached).toBe(true);
|
|
116
|
-
expect(s.unreffed).toBe(true);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("returns unique agentIds for concurrent dispatches", async () => {
|
|
120
|
-
const mod = await import("../src/services/alvin-dispatch.js");
|
|
121
|
-
const r1 = mod.dispatchDetachedAgent({
|
|
122
|
-
prompt: "a",
|
|
123
|
-
description: "a",
|
|
124
|
-
chatId: 1,
|
|
125
|
-
userId: 1,
|
|
126
|
-
sessionKey: "s1",
|
|
127
|
-
});
|
|
128
|
-
const r2 = mod.dispatchDetachedAgent({
|
|
129
|
-
prompt: "b",
|
|
130
|
-
description: "b",
|
|
131
|
-
chatId: 1,
|
|
132
|
-
userId: 1,
|
|
133
|
-
sessionKey: "s1",
|
|
134
|
-
});
|
|
135
|
-
expect(r1.agentId).not.toBe(r2.agentId);
|
|
136
|
-
expect(r1.outputFile).not.toBe(r2.outputFile);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("registers the pending agent with the watcher", async () => {
|
|
140
|
-
const mod = await import("../src/services/alvin-dispatch.js");
|
|
141
|
-
const watcher = await import("../src/services/async-agent-watcher.js");
|
|
142
|
-
|
|
143
|
-
mod.dispatchDetachedAgent({
|
|
144
|
-
prompt: "x",
|
|
145
|
-
description: "X audit",
|
|
146
|
-
chatId: 42,
|
|
147
|
-
userId: 42,
|
|
148
|
-
sessionKey: "s1",
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
const pending = watcher.listPendingAgents();
|
|
152
|
-
expect(pending).toHaveLength(1);
|
|
153
|
-
expect(pending[0].description).toBe("X audit");
|
|
154
|
-
expect(pending[0].sessionKey).toBe("s1");
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("increments session.pendingBackgroundCount on dispatch", async () => {
|
|
158
|
-
const mod = await import("../src/services/alvin-dispatch.js");
|
|
159
|
-
const { getSession } = await import("../src/services/session.js");
|
|
160
|
-
|
|
161
|
-
const session = getSession("s-count");
|
|
162
|
-
session.pendingBackgroundCount = 0;
|
|
163
|
-
|
|
164
|
-
mod.dispatchDetachedAgent({
|
|
165
|
-
prompt: "p",
|
|
166
|
-
description: "d",
|
|
167
|
-
chatId: 1,
|
|
168
|
-
userId: 1,
|
|
169
|
-
sessionKey: "s-count",
|
|
170
|
-
});
|
|
171
|
-
expect(session.pendingBackgroundCount).toBe(1);
|
|
172
|
-
|
|
173
|
-
mod.dispatchDetachedAgent({
|
|
174
|
-
prompt: "p2",
|
|
175
|
-
description: "d2",
|
|
176
|
-
chatId: 1,
|
|
177
|
-
userId: 1,
|
|
178
|
-
sessionKey: "s-count",
|
|
179
|
-
});
|
|
180
|
-
expect(session.pendingBackgroundCount).toBe(2);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it("uses stdio redirect so child's stdout goes to outputFile", async () => {
|
|
184
|
-
const mod = await import("../src/services/alvin-dispatch.js");
|
|
185
|
-
mod.dispatchDetachedAgent({
|
|
186
|
-
prompt: "p",
|
|
187
|
-
description: "d",
|
|
188
|
-
chatId: 1,
|
|
189
|
-
userId: 1,
|
|
190
|
-
sessionKey: "s1",
|
|
191
|
-
});
|
|
192
|
-
const [s] = spawned;
|
|
193
|
-
// stdio should be an array with FD redirects (ignore, pipe-to-file, ignore)
|
|
194
|
-
// or similar. We verify it's NOT "inherit" (which would attach to parent).
|
|
195
|
-
expect(s.opts.stdio).not.toBe("inherit");
|
|
196
|
-
expect(s.opts.stdio).not.toBe(undefined);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it("cleans env of CLAUDECODE/CLAUDE_CODE_ENTRYPOINT to prevent nested session errors", async () => {
|
|
200
|
-
const mod = await import("../src/services/alvin-dispatch.js");
|
|
201
|
-
process.env.CLAUDECODE = "1";
|
|
202
|
-
process.env.CLAUDE_CODE_ENTRYPOINT = "cli";
|
|
203
|
-
try {
|
|
204
|
-
mod.dispatchDetachedAgent({
|
|
205
|
-
prompt: "p",
|
|
206
|
-
description: "d",
|
|
207
|
-
chatId: 1,
|
|
208
|
-
userId: 1,
|
|
209
|
-
sessionKey: "s1",
|
|
210
|
-
});
|
|
211
|
-
const [s] = spawned;
|
|
212
|
-
expect(s.opts.env).toBeDefined();
|
|
213
|
-
expect(s.opts.env?.CLAUDECODE).toBeUndefined();
|
|
214
|
-
expect(s.opts.env?.CLAUDE_CODE_ENTRYPOINT).toBeUndefined();
|
|
215
|
-
} finally {
|
|
216
|
-
delete process.env.CLAUDECODE;
|
|
217
|
-
delete process.env.CLAUDE_CODE_ENTRYPOINT;
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
});
|