token-goat 2.9.1 → 2.9.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.
@@ -0,0 +1,303 @@
1
+ # Token-Goat Runtime Architecture & Security Model (C4 Specification)
2
+
3
+ This document specifies the complete runtime architecture, hook processing flow, storage layout, background worker lifecycle, bridge integrations, and security trust boundaries of **Token-Goat**.
4
+
5
+ ---
6
+
7
+ ## 1. System Context Diagram (C4 Level 1) & Trust Boundaries
8
+
9
+ Token-Goat sits between AI Agent Harnesses (Claude Code, VS Code Copilot, GitHub Copilot CLI, Codex) and the local operating system, filtering and narrowing context before it enters the model window.
10
+
11
+ ```mermaid
12
+ flowchart TB
13
+ classDef person fill:#08427B,stroke:#073B6F,color:#ffffff,font-weight:bold;
14
+ classDef tgRuntime fill:#1168BD,stroke:#0B4884,color:#ffffff,font-weight:bold;
15
+ classDef agent fill:#7C3AED,stroke:#5B21B6,color:#ffffff,font-weight:bold;
16
+ classDef external fill:#475569,stroke:#334155,color:#ffffff;
17
+ classDef untrusted fill:#DC2626,stroke:#991B1B,color:#ffffff,font-weight:bold;
18
+
19
+ Developer["👤 Software Engineer / User<br/><small>[Person] Writes code, issues CLI commands, reviews tasks</small>"]:::person
20
+
21
+ subgraph HarnessBoundary ["AI Coding Agent Runtimes"]
22
+ ClaudeCode["🤖 Claude Code<br/><small>[Harness] Subprocess Hook Intercept</small>"]:::agent
23
+ CopilotCLI["🤖 GitHub Copilot CLI<br/><small>[Harness] Direct Tool Calls & Aliases</small>"]:::agent
24
+ VSCodeCopilot["🤖 VS Code Copilot Chat<br/><small>[Harness] stdio MCP Server</small>"]:::agent
25
+ CodexHermes["🤖 Codex / OpenCode / Hermes<br/><small>[Harness] Hook & Execution Bridge</small>"]:::agent
26
+ end
27
+
28
+ subgraph TokenGoatRuntime ["Token-Goat Engine (Local Machine)"]
29
+ TGCore["⚡ Token-Goat Runtime<br/><small>[TypeScript / Node.js]</small><br/>• Hook Relays & Normalization<br/>• SQLite Symbol & Ref Indexer<br/>• Structural Bash & Code Filters<br/>• Prompt Injection Fencing<br/>• MCP stdio Server"]:::tgRuntime
30
+ end
31
+
32
+ subgraph StorageBoundary ["Host Environment & Storage (Trust Boundary: Local User)"]
33
+ LocalFS["📁 Workspace Filesystem<br/><small>Git Repo Source Trees & Fixtures</small>"]:::external
34
+ AppDBs[("🗄️ Token-Goat Local Store<br/><small>global.db (WAL), Cache Blobs, Sessions</small>")]:::external
35
+ end
36
+
37
+ subgraph UntrustedZone ["Untrusted External Content (Trust Boundary: Untrusted Input)"]
38
+ WebPages["🌐 External Web & APIs<br/><small>Arbitrary web pages, docs, issue trackers</small>"]:::untrusted
39
+ ExternalFiles["📄 External Attachments<br/><small>PDFs, PPTX, Excel, Images, Archives</small>"]:::untrusted
40
+ end
41
+
42
+ Developer -->|Runs interactive commands| TGCore
43
+ Developer -->|Prompts agent| ClaudeCode
44
+ Developer -->|Prompts agent| VSCodeCopilot
45
+
46
+ ClaudeCode -->|Fires lifecycle hooks via stdin/stdout| TGCore
47
+ CodexHermes -->|Fires tool hooks| TGCore
48
+ VSCodeCopilot -->|JSON-RPC over stdio| TGCore
49
+ CopilotCLI -->|Executes surgical commands| TGCore
50
+
51
+ TGCore -->|Reads/Indexes/Watches| LocalFS
52
+ TGCore -->|Persists index & content-addressed blobs| AppDBs
53
+
54
+ TGCore -->|Fetches requested URL/Doc| WebPages
55
+ TGCore -->|Parses slices of| ExternalFiles
56
+ TGCore -.->|Marks untrusted & sanitizes| ClaudeCode
57
+ ```
58
+
59
+ ### Trust Boundaries & Threat Surfaces
60
+ 1. **Harness Hook Boundary (Local IPC)**: Hook inputs arrive via standard I/O (JSON payloads over `stdin`). Token-Goat fails soft (`{ continue: true }`) on any parsing or processing error so agent operations never block.
61
+ 2. **Untrusted Payload Boundary (Web/Documents)**: Web pages (`webfetch.ts`) and document attachments (`pdf_extract.ts`, `docx_extract.ts`) are fenced before model exposure because of where they came from, never because a scanner recognized them. `untrusted_fence.ts` is the single decision point; `injection_scan.ts` runs alongside it and decides only whether the fence's notice names the patterns it matched.
62
+ 3. **Storage Boundary (Local User Isolation)**: All persistent databases and caches are stored in user-scoped directories (`%LOCALAPPDATA%\dfk-helper\token-goat` on Windows, `~/.token-goat` on POSIX) with no multi-tenant cross-talk.
63
+ 4. **Child Process Execution Boundary**: Bash command compression wraps commands inside a controlled runner (`bash_runner.ts`) capturing output with stream bounds (32 MiB/stream cap) and preserves exit codes.
64
+
65
+ ---
66
+
67
+ ## 2. Container Diagram (C4 Level 2) & Storage Architecture
68
+
69
+ Token-Goat runs as a set of lightweight, ephemeral processes and an optional background worker daemon cooperating over SQLite and content-addressed disk files.
70
+
71
+ ```mermaid
72
+ flowchart TB
73
+ classDef process fill:#1E40AF,stroke:#172554,color:#ffffff,font-weight:bold;
74
+ classDef shim fill:#2563EB,stroke:#1D4ED8,color:#ffffff;
75
+ classDef storage fill:#0D9488,stroke:#115E59,color:#ffffff,font-weight:bold;
76
+ classDef external fill:#64748B,stroke:#334155,color:#ffffff;
77
+
78
+ subgraph AgentSpace ["Agent Execution Context"]
79
+ Agent["AI Coding Harness (Claude Code / Codex)"]:::external
80
+ end
81
+
82
+ subgraph TGProcesses ["Token-Goat Runtime Containers"]
83
+ HookShim["🪝 Hook Shim<br/><small>[~/.claude/hooks/token-goat-shim.js]</small><br/>In-process fast path / Subprocess launcher"]:::shim
84
+ HookRelay["⚡ Hook Relay & Normalizer<br/><small>[dist/token-goat-hook.mjs]</small><br/>Normalizes Codex/Gemini/Claude schemas"]:::process
85
+ CLIBinary["💻 CLI Tool & Command Suite<br/><small>[dist/token-goat.mjs]</small><br/>100+ surgical read, outline, pack commands"]:::process
86
+ MCPServer["🔌 MCP stdio Server<br/><small>[token-goat mcp-serve]</small><br/>18 tools: symbol, read, retrieve, section"]:::process
87
+ WorkerDaemon["🔄 Background Indexer Daemon<br/><small>[worker.ts / worker_daemon.ts]</small><br/>Polls dirty queue every 2s, reindexes changes"]:::process
88
+ end
89
+
90
+ subgraph DiskStorage ["Persistent Storage Model (%LOCALAPPDATA% or ~/.token-goat)"]
91
+ GlobalDB[("🗄️ global.db (SQLite + WAL)<br/><small>• files, symbols, refs, chunks<br/>• symbols_fts (FTS5)<br/>• chunk_vectors (vec0 KNN)<br/>• stats</small>")]:::storage
92
+ DirtyQueue[("📋 queue/dirty.txt<br/><small>Append-only list of edited file paths</small>")]:::storage
93
+ SessionJSON[("📝 sessions/{session_id}.json<br/><small>Read/edit history, shown hints, cache indexes</small>")]:::storage
94
+ BlobStore[("📦 Content Blob Store<br/><small>• bash_outputs/{id}.json<br/>• web_outputs/{id}.json<br/>• images/ & skills/</small>")]:::storage
95
+ end
96
+
97
+ Agent -->|Invokes hook| HookShim
98
+ HookShim -->|Loads fast in-process| HookRelay
99
+ HookRelay -->|Dispatches events to handlers| HookRelay
100
+
101
+ Agent -->|Spawns tool CLI| CLIBinary
102
+ Agent -->|Connects via stdio| MCPServer
103
+
104
+ HookRelay -->|Appends edited file paths on PostToolUse(Edit)| DirtyQueue
105
+ HookRelay -->|Loads & merges session state| SessionJSON
106
+ HookRelay -->|Stores compressed output| BlobStore
107
+
108
+ WorkerDaemon -->|Polls & drains| DirtyQueue
109
+ WorkerDaemon -->|Writes updated symbols/chunks| GlobalDB
110
+
111
+ CLIBinary -->|Queries symbols/sections| GlobalDB
112
+ MCPServer -->|Queries index & retrieves blobs| GlobalDB
113
+ MCPServer -->|Retrieves cached output by id| BlobStore
114
+ ```
115
+
116
+ ### Storage Model & Concurrency Guarantees
117
+ - **`global.db`**: Unified SQLite database opened with `journal_mode=WAL`, `synchronous=NORMAL`, and `busy_timeout=15000ms`. Writes use explicit transactions (`writeParseResult`).
118
+ - **Body Bounding Invariant**: Symbol bodies in `symbols` are bounded at `MAX_SYMBOL_BODY_CHARS` at the single `writeParseResult` choke point. Over-cap bodies are stored as `''` and sliced dynamically from source on read (`resolveBody`), preventing database bloating while preserving full symbol bounds.
119
+ - **Merge-on-Save Session State**: `sessions/{session_id}.json` is updated via atomic read-modify-write (`saveSessionState`) with set-union for seen hints and newest-wins for command indexes.
120
+ - **Content Blob Lifecycle**: `bash_outputs/` and `web_outputs/` store content-addressed JSON objects keyed by SHA-256 / random ID, pruned automatically by age (24h TTL) and count cap (200 / 4096 files).
121
+
122
+ ---
123
+
124
+ ## 3. Component Architecture (C4 Level 3)
125
+
126
+ ### A. Hook Intercept & Context Optimization Subsystem
127
+
128
+ ```mermaid
129
+ flowchart TB
130
+ classDef router fill:#1D4ED8,stroke:#1E3A8A,color:#ffffff,font-weight:bold;
131
+ classDef hook fill:#2563EB,stroke:#1D4ED8,color:#ffffff;
132
+ classDef filter fill:#0284C7,stroke:#0369A1,color:#ffffff;
133
+ classDef sec fill:#DC2626,stroke:#991B1B,color:#ffffff,font-weight:bold;
134
+
135
+ PayloadIn["Raw Hook Payload (stdin)"] --> Normalizer["Harness Normalizer<br/><small>(hooks_cli.ts::normalizePayload)</small>"]:::router
136
+ Normalizer --> Dispatcher["Hook Registry Dispatcher<br/><small>(hook_registry.ts::runHook)</small>"]:::router
137
+
138
+ subgraph LifecycleHandlers ["Hook Lifecycle Handlers"]
139
+ PreRead["preReadHandler<br/><small>(hooks_read.ts)</small><br/>Diff-on-reread, surgical hints"]:::hook
140
+ PreBash["preBashHandler<br/><small>(hooks_bash.ts)</small><br/>Command rewrite to 'token-goat compress'"]:::hook
141
+ PreFetch["preFetchHandler<br/><small>(hooks_fetch.ts)</small><br/>Image shrink & URL dedup hint"]:::hook
142
+ PostEdit["postEditHandler<br/><small>(hooks_edit.ts)</small><br/>Append to queue/dirty.txt"]:::hook
143
+ PreCompact["preCompactHandler<br/><small>(hooks_compact.ts)</small><br/>Adaptive session manifest"]:::hook
144
+ PreMCP["preMcpHandler<br/><small>(hooks_mcp.ts)</small><br/>Read-only MCP output dedup"]:::hook
145
+ end
146
+
147
+ subgraph FilterPipeline ["158 Bash & Code Output Filters (tool_filters/)"]
148
+ TestRunners["Test Runners (Vitest, Jest, Pytest, GoTest)"]:::filter
149
+ PackageMgrs["Package Managers (npm, pnpm, uv, pip, cargo)"]:::filter
150
+ Linters["Linters & Typecheckers (tsc, ruff, mypy, eslint)"]:::filter
151
+ GitCloud["VCS & Cloud (git diff, kubectl, terraform, gh)"]:::filter
152
+ GenericCompress["GenericFilter (ANSI/progress strip + line cap)"]:::filter
153
+ end
154
+
155
+ subgraph SecurityControls ["Security & Trust Layer"]
156
+ InjectionCheck["Prompt Injection Scanner<br/><small>(injection_scan.ts)</small>"]:::sec
157
+ SecretRedact["Secret Redaction<br/><small>(secret_redact.ts)</small>"]:::sec
158
+ OverflowGuard["Overflow Guard<br/><small>(overflow_guard.ts)</small>"]:::sec
159
+ end
160
+
161
+ Dispatcher --> PreRead
162
+ Dispatcher --> PreBash
163
+ Dispatcher --> PreFetch
164
+ Dispatcher --> PostEdit
165
+ Dispatcher --> PreCompact
166
+ Dispatcher --> PreMCP
167
+
168
+ PreBash --> FilterPipeline
169
+ PreFetch --> InjectionCheck
170
+ PreRead --> OverflowGuard
171
+ PreBash --> SecretRedact
172
+
173
+ PreRead --> Serializer["Hook Output Serializer<br/><small>(relay.ts::serializeOutput)</small>"]:::router
174
+ PreBash --> Serializer
175
+ PreCompact --> Serializer
176
+ ```
177
+
178
+ ### B. Indexer, Parser & Worker Subsystem
179
+
180
+ ```mermaid
181
+ flowchart TB
182
+ classDef parser fill:#059669,stroke:#047857,color:#ffffff,font-weight:bold;
183
+ classDef worker fill:#0D9488,stroke:#115E59,color:#ffffff;
184
+ classDef adapter fill:#10B981,stroke:#047857,color:#ffffff;
185
+ classDef db fill:#0284C7,stroke:#0369A1,color:#ffffff;
186
+
187
+ DirtyQueue["queue/dirty.txt"] --> WorkerLoop["Worker Loop (worker.ts)<br/><small>Polls every 2000ms</small>"]:::worker
188
+ WorkerLoop --> Fingerprint["fingerprintFile()<br/><small>Fast SHA check skips unchanged</small>"]:::worker
189
+ Fingerprint --> Parser["Parser Engine (parser.ts)<br/><small>detectLanguage() & parseContent()</small>"]:::parser
190
+
191
+ subgraph ParserAdapters ["Parser Language Adapters"]
192
+ TreeSitter["Inline Tree-Sitter Extractors<br/><small>TS, JS, Python, Go, Rust, Java, C/C++, Ruby</small>"]:::adapter
193
+ RegexInline["Inline Regex Extractors<br/><small>Markdown, JSON, YAML, TOML, CSS, Dockerfile</small>"]:::adapter
194
+ LangAdapters["src/languages/ Adapters (28 languages)<br/><small>C#, PHP, Kotlin, GraphQL, SQL, Proto, Apex, etc.</small>"]:::adapter
195
+ end
196
+
197
+ Parser --> TreeSitter
198
+ Parser --> RegexInline
199
+ Parser --> LangAdapters
200
+
201
+ TreeSitter --> ChokePoint["writeParseResult Choke Point<br/><small>Single atomic transaction<br/>MAX_SYMBOL_BODY_CHARS bound</small>"]:::parser
202
+ RegexInline --> ChokePoint
203
+ LangAdapters --> ChokePoint
204
+
205
+ ChokePoint --> GlobalDB[("global.db<br/><small>files, symbols, refs, chunks</small>")]:::db
206
+
207
+ Parser -.-> Embedder["Embedding Pipeline (embeddings.ts)<br/><small>Xenova/bge-small-en-v1.5 (384d)</small>"]:::parser
208
+ Embedder -.-> VecDB[("chunk_vectors (vec0 KNN)")]:::db
209
+ ```
210
+
211
+ ---
212
+
213
+ ## 4. Dynamic Execution Flows & Lifecycle Scenarios (C4 Level 4)
214
+
215
+ ### Scenario 1: Pre-Tool / Post-Tool Hook Execution Flow
216
+
217
+ ```mermaid
218
+ sequenceDiagram
219
+ autonumber
220
+ participant Agent as AI Agent (Claude Code)
221
+ participant Shim as token-goat-shim.js
222
+ participant Relay as relay.ts / hook_registry.ts
223
+ participant Filter as ToolFilter (tool_filters/)
224
+ participant Store as session_store.ts & disk_cache.ts
225
+
226
+ Agent->>Shim: Invokes PreToolUse (e.g. tool=Bash, cmd="npx vitest run")
227
+ Shim->>Relay: In-process relayInProcess(payload)
228
+ Relay->>Store: loadSessionState(sessionId)
229
+ Relay->>Filter: Match command -> VitestFilter
230
+ Filter-->>Relay: Rewrite to "token-goat compress -f vitest -c 'npx vitest run'"
231
+ Relay-->>Agent: Return updatedInput with wrapped command
232
+
233
+ Agent->>Agent: Executes wrapped command in subprocess
234
+ Note over Agent: Subprocess applies VitestFilter, strips progress,<br/>collapses passing tests, captures stderr on failure
235
+
236
+ Agent->>Shim: Invokes PostToolUse (tool=Bash, output=...)
237
+ Shim->>Relay: relayInProcess(postPayload)
238
+ Relay->>Store: storeBashOutput(id, rawOutput) & recordBashOutput(cmd, id)
239
+ Relay->>Store: saveSessionState(sessionId) [Merge-on-save]
240
+ Relay-->>Agent: Return { continue: true }
241
+ ```
242
+
243
+ ### Scenario 2: Surgical Read vs Full Read Resolution
244
+
245
+ ```mermaid
246
+ sequenceDiagram
247
+ autonumber
248
+ participant Agent as AI Coding Agent
249
+ participant CLI as token-goat read / MCP tool
250
+ participant Resolver as paths.ts::resolveIndexPath
251
+ participant Reader as index_reader.ts
252
+ participant DB as global.db (SQLite)
253
+ participant FS as Local Source File
254
+
255
+ Agent->>CLI: Request "src/parser.ts::writeParseResult"
256
+ CLI->>Resolver: resolveIndexPath("src/parser.ts")
257
+ Resolver-->>CLI: "c:/projects/token-goat/src/parser.ts" (canonical key)
258
+ CLI->>Reader: querySymbols(file="c:/...", name="writeParseResult")
259
+ Reader->>DB: SELECT * FROM symbols WHERE file_path=? AND name=?
260
+ DB-->>Reader: { line_start: 2420, line_end: 2473, body: "" (elided) }
261
+ Reader->>FS: Slices lines 2420-2473 dynamically (resolveBody)
262
+ FS-->>Reader: Exact function source code
263
+ Reader-->>Agent: Returns 54 lines (vs 2,800 lines of parser.ts)
264
+ ```
265
+
266
+ ### Scenario 3: Untrusted Web/Document Ingestion & Injection Fencing
267
+
268
+ ```mermaid
269
+ sequenceDiagram
270
+ autonumber
271
+ participant Agent as AI Agent
272
+ participant WebFetch as hooks_fetch.ts / webfetch.ts
273
+ participant Scanner as injection_scan.ts
274
+ participant Redactor as secret_redact.ts
275
+ participant Cache as web_outputs/{id}.json
276
+
277
+ Agent->>WebFetch: Fetches external doc/URL
278
+ WebFetch->>Scanner: scanForInjectionPatterns(content)
279
+ alt Patterns Matched
280
+ Scanner-->>WebFetch: Match: "Ignore previous instructions", "System Prompt Override", etc.
281
+ WebFetch->>WebFetch: Notice names the matched patterns
282
+ else No Pattern Matched
283
+ Scanner-->>WebFetch: No match
284
+ WebFetch->>WebFetch: Notice says content is untrusted, names nothing
285
+ end
286
+ WebFetch->>WebFetch: Wrap in the surface's untrusted fence either way
287
+ WebFetch->>Redactor: Redact sensitive tokens (Bearer, AWS keys, JWTs)
288
+ Redactor->>Cache: Persist to content-addressed cache
289
+ WebFetch-->>Agent: Returns safe, token-compressed extract
290
+ ```
291
+
292
+ ---
293
+
294
+ ## 5. Security & Threat Model Summary Matrix
295
+
296
+ | Threat / Attack Surface | Risk | Token-Goat Architectural Mitigation |
297
+ |-------------------------|------|-------------------------------------|
298
+ | **Prompt Injection via Web/Docs** | Malicious instructions in external pages hijack the AI agent. | `untrusted_fence.ts` wraps every ingested external payload in a per-surface fence (`untrusted-web-content`, `untrusted-file-content`, `untrusted-tool-output`, `untrusted-github-content`) whether or not anything matched, so a novel phrasing gets no unmarked channel; `injection_scan.ts` names what it found in the notice and records it. Enforces that documents are reference data, not directives. |
299
+ | **Path Traversal / UNC Attacks** | User-controlled paths escaping project directory via `..`, Windows drive letters, or NTFS streams. | `paths.ts::safeJoin()` unconditionally rejects components with colons (`:`); `normalizePath()` canonicalizes separators and drive letters; `resolveIndexPath()` strictly anchors keys. |
300
+ | **SQLite Lock Contention / DoS** | Concurrent hooks & background indexer stalling agent execution. | `db.ts` enforces `busy_timeout=15000ms`, `WAL` journal mode, and fast in-memory retry. Single write transactions prevent reader starvation. |
301
+ | **Index Database Bloat / Amplification** | Huge files or minified JSON inflating DB to gigabytes. | Choke point `writeParseResult` strictly caps symbol body at `MAX_SYMBOL_BODY_CHARS`; over-cap bodies are elided (`''`) and sliced from disk on demand. Amplification guard ensures stored bytes $\le 4\times$ source size. |
302
+ | **Secret Exfiltration in Cache** | API keys or tokens in command stdout persisted to disk. | `secret_redact.ts` sanitizes known credential patterns before storing to `bash_outputs/` and `web_outputs/`. Blobs have strict 24-hour TTL and user-only file permissions. |
303
+ | **Hook Subprocess Failure Impact** | Crash in Token-Goat blocking agent prompt workflow. | All hook handlers are wrapped in `failSoft()`. Any exception outputs to stderr and immediately emits `{ continue: true }` to allow unhindered agent execution. |