@sugar-crash-studios/vibe-forge 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/clear-attention.md +63 -0
- package/.claude/commands/compact-context.md +52 -0
- package/.claude/commands/configure-vcs.md +102 -0
- package/.claude/commands/forge.md +171 -0
- package/.claude/commands/need-help.md +77 -0
- package/.claude/commands/update-status.md +64 -0
- package/.claude/commands/worker-loop.md +106 -0
- package/.claude/hooks/worker-loop.js +198 -0
- package/.claude/scripts/setup-worker-loop.sh +45 -0
- package/.claude/settings.local.json +46 -0
- package/LICENSE +21 -0
- package/README.md +238 -0
- package/agents/aegis/personality.md +294 -0
- package/agents/anvil/personality.md +276 -0
- package/agents/architect/personality.md +258 -0
- package/agents/crucible/personality.md +360 -0
- package/agents/ember/personality.md +291 -0
- package/agents/forge-master/capabilities.md +144 -0
- package/agents/forge-master/context-template.md +128 -0
- package/agents/forge-master/personality.md +138 -0
- package/agents/furnace/personality.md +340 -0
- package/agents/herald/personality.md +247 -0
- package/agents/loki/personality.md +108 -0
- package/agents/oracle/personality.md +283 -0
- package/agents/pixel/personality.md +113 -0
- package/agents/planning-hub/personality.md +320 -0
- package/agents/scribe/personality.md +251 -0
- package/agents/temper/personality.md +218 -0
- package/bin/cli.js +375 -0
- package/bin/dashboard/api/agents.js +333 -0
- package/bin/dashboard/api/dispatch.js +483 -0
- package/bin/dashboard/api/tasks.js +416 -0
- package/bin/dashboard/frontend/index.html +13 -0
- package/bin/dashboard/frontend/package.json +16 -0
- package/bin/dashboard/frontend/src/App.svelte +222 -0
- package/bin/dashboard/frontend/src/app.css +1777 -0
- package/bin/dashboard/frontend/src/lib/components/AgentCard.svelte +60 -0
- package/bin/dashboard/frontend/src/lib/components/AgentsPanel.svelte +57 -0
- package/bin/dashboard/frontend/src/lib/components/DispatchModal.svelte +180 -0
- package/bin/dashboard/frontend/src/lib/components/Footer.svelte +33 -0
- package/bin/dashboard/frontend/src/lib/components/Header.svelte +84 -0
- package/bin/dashboard/frontend/src/lib/components/IssueCard.svelte +33 -0
- package/bin/dashboard/frontend/src/lib/components/IssuesPanel.svelte +73 -0
- package/bin/dashboard/frontend/src/lib/components/KeyboardShortcutsModal.svelte +108 -0
- package/bin/dashboard/frontend/src/lib/components/MobileTabs.svelte +52 -0
- package/bin/dashboard/frontend/src/lib/components/NotificationCard.svelte +60 -0
- package/bin/dashboard/frontend/src/lib/components/NotificationsPanel.svelte +44 -0
- package/bin/dashboard/frontend/src/lib/components/TaskCard.svelte +63 -0
- package/bin/dashboard/frontend/src/lib/components/TasksPanel.svelte +82 -0
- package/bin/dashboard/frontend/src/lib/components/Toast.svelte +45 -0
- package/bin/dashboard/frontend/src/lib/stores/agents.js +34 -0
- package/bin/dashboard/frontend/src/lib/stores/issues.js +54 -0
- package/bin/dashboard/frontend/src/lib/stores/notifications.js +48 -0
- package/bin/dashboard/frontend/src/lib/stores/tasks.js +63 -0
- package/bin/dashboard/frontend/src/lib/stores/theme.js +33 -0
- package/bin/dashboard/frontend/src/lib/stores/toast.js +35 -0
- package/bin/dashboard/frontend/src/lib/stores/ui.js +25 -0
- package/bin/dashboard/frontend/src/lib/stores/voice.js +275 -0
- package/bin/dashboard/frontend/src/lib/stores/websocket.js +295 -0
- package/bin/dashboard/frontend/src/lib/utils/api.js +101 -0
- package/bin/dashboard/frontend/src/lib/utils/formatters.js +54 -0
- package/bin/dashboard/frontend/src/main.js +9 -0
- package/bin/dashboard/frontend/svelte.config.js +5 -0
- package/bin/dashboard/frontend/vite.config.js +20 -0
- package/bin/dashboard/public/assets/index-DnfVj9Ce.css +1 -0
- package/bin/dashboard/public/assets/index-Ze5h0kXQ.js +2 -0
- package/bin/dashboard/public/index.html +14 -0
- package/bin/dashboard/server.js +566 -0
- package/bin/forge-daemon.sh +463 -0
- package/bin/forge-setup.sh +645 -0
- package/bin/forge-spawn.sh +164 -0
- package/bin/forge.cmd +83 -0
- package/bin/forge.sh +533 -0
- package/bin/lib/agents.sh +177 -0
- package/bin/lib/colors.sh +44 -0
- package/bin/lib/config.sh +347 -0
- package/bin/lib/constants.sh +241 -0
- package/bin/lib/daemon/display.sh +128 -0
- package/bin/lib/daemon/notifications.sh +263 -0
- package/bin/lib/daemon/routing.sh +77 -0
- package/bin/lib/daemon/state.sh +115 -0
- package/bin/lib/daemon/sync.sh +95 -0
- package/bin/lib/database.sh +310 -0
- package/bin/lib/heimdall-setup.js +113 -0
- package/bin/lib/heimdall.js +265 -0
- package/bin/lib/json.sh +264 -0
- package/bin/lib/terminal.js +451 -0
- package/bin/lib/util.sh +126 -0
- package/bin/lib/vcs.js +349 -0
- package/config/agent-manifest.yaml +203 -0
- package/config/agents.json +168 -0
- package/config/task-template.md +159 -0
- package/config/task-types.yaml +106 -0
- package/context/agent-status/aegis.json +7 -0
- package/context/agent-status/anvil.json +7 -0
- package/context/agent-status/architect.json +7 -0
- package/context/agent-status/crucible.json +7 -0
- package/context/agent-status/ember.json +7 -0
- package/context/agent-status/furnace.json +7 -0
- package/context/agent-status/loki.json +7 -0
- package/context/agent-status/oracle.json +7 -0
- package/context/agent-status/pixel.json +7 -0
- package/context/agent-status/planning-hub.json +7 -0
- package/context/agent-status/scribe.json +7 -0
- package/context/agent-status/temper.json +7 -0
- package/context/feature-brainstorm.md +426 -0
- package/context/forge-state.yaml +19 -0
- package/context/modern-conventions.md +129 -0
- package/context/project-context-template.md +122 -0
- package/context/project-context.md +122 -0
- package/docs/TODO.md +150 -0
- package/docs/agents.md +409 -0
- package/docs/architecture/decisions/ADR-001-daemon-modularization.md +122 -0
- package/docs/architecture/vibe-lab-integration.md +684 -0
- package/docs/architecture.md +194 -0
- package/docs/bmad-gap-analysis-2026-03-31.md +444 -0
- package/docs/cleanup-workflow.md +329 -0
- package/docs/commands.md +451 -0
- package/docs/dashboard-mockup.html +989 -0
- package/docs/getting-started.md +261 -0
- package/docs/integration/forge-ownership-policy.md +112 -0
- package/docs/npm-publishing.md +132 -0
- package/docs/roadmap-2026.md +519 -0
- package/docs/security.md +144 -0
- package/docs/wireframes/dashboard-mvp.md +1164 -0
- package/docs/workflows/README.md +32 -0
- package/docs/workflows/azure-devops.md +108 -0
- package/docs/workflows/bitbucket.md +104 -0
- package/docs/workflows/git-only.md +130 -0
- package/docs/workflows/gitea.md +168 -0
- package/docs/workflows/github.md +103 -0
- package/docs/workflows/gitlab.md +105 -0
- package/docs/workflows.md +454 -0
- package/package.json +73 -0
- package/tasks/completed/ARCH-001-duplicate-agent-config.md +121 -0
- package/tasks/completed/ARCH-002-mixed-bash-node-implementation.md +88 -0
- package/tasks/completed/ARCH-003-worker-loop-hook-duplication.md +77 -0
- package/tasks/completed/ARCH-009-test-organization.md +78 -0
- package/tasks/completed/ARCH-011-jq-vs-nodejs-json.md +94 -0
- package/tasks/completed/ARCH-012-tmp-files-in-root.md +71 -0
- package/tasks/completed/ARCH-013-exit-code-constants.md +65 -0
- package/tasks/completed/ARCH-014-sed-incompatibility.md +96 -0
- package/tasks/completed/ARCH-015-docs-todo-tracking.md +83 -0
- package/tasks/completed/BUG-dash-001-tasks-filter-error.md +31 -0
- package/tasks/completed/BUG-dash-002-agents-unknown.md +41 -0
- package/tasks/completed/CLEAN-001.md +38 -0
- package/tasks/completed/CLEAN-002.md +43 -0
- package/tasks/completed/CLEAN-003.md +47 -0
- package/tasks/completed/CLEAN-004.md +56 -0
- package/tasks/completed/CLEAN-005.md +75 -0
- package/tasks/completed/CLEAN-006.md +47 -0
- package/tasks/completed/CLEAN-007.md +34 -0
- package/tasks/completed/CLEAN-008.md +49 -0
- package/tasks/completed/CLEAN-012.md +58 -0
- package/tasks/completed/CLEAN-013.md +45 -0
- package/tasks/completed/FEATURE-001a-dashboard-wireframes.md +162 -0
- package/tasks/completed/IMPL-007a-daemon-notifications-module.md +82 -0
- package/tasks/completed/IMPL-007b-daemon-sync-module.md +71 -0
- package/tasks/completed/IMPL-007c-daemon-state-module.md +80 -0
- package/tasks/completed/IMPL-007d-daemon-routing-module.md +77 -0
- package/tasks/completed/IMPL-007e-daemon-display-module.md +77 -0
- package/tasks/completed/IMPL-007f-daemon-integration.md +124 -0
- package/tasks/completed/PLAT-1-heimdall.md +420 -0
- package/tasks/completed/SEC-001-sql-injection-fix.md +58 -0
- package/tasks/completed/SEC-002-notification-injection-fix.md +45 -0
- package/tasks/completed/SEC-003-eval-injection-fix.md +54 -0
- package/tasks/completed/SEC-004-pid-race-condition-fix.md +49 -0
- package/tasks/completed/SEC-005-worker-loop-path-fix.md +51 -0
- package/tasks/completed/SEC-006-eval-agent-names.md +55 -0
- package/tasks/completed/SEC-007-spawn-escaping.md +67 -0
- package/tasks/completed/TASK-DASH-001-server-infrastructure.md +185 -0
- package/tasks/completed/TASK-anvil-001-dashboard-frontend.md +133 -0
- package/tasks/completed/review-bmad-aegis.md +89 -0
- package/tasks/completed/review-bmad-anvil.md +80 -0
- package/tasks/completed/review-bmad-crucible.md +81 -0
- package/tasks/completed/review-bmad-ember.md +90 -0
- package/tasks/completed/review-bmad-furnace.md +79 -0
- package/tasks/completed/review-bmad-pixel.md +82 -0
- package/tasks/completed/review-bmad-scribe.md +92 -0
- package/tasks/completed/review-bmad-sentinel.md +83 -0
- package/tasks/pending/ARCH-004-git-bash-detection-duplication.md +72 -0
- package/tasks/pending/ARCH-005-missing-src-directory.md +95 -0
- package/tasks/pending/ARCH-006-task-template-location.md +64 -0
- package/tasks/pending/ARCH-008-forge-master-vs-hub.md +81 -0
- package/tasks/pending/ARCH-010-missing-index-files.md +84 -0
- package/tasks/pending/CLEAN-009.md +31 -0
- package/tasks/pending/CLEAN-010.md +30 -0
- package/tasks/pending/CLEAN-011.md +30 -0
- package/tasks/pending/CLEAN-014.md +32 -0
- package/tasks/pending/DESIGN-dash-001-layout-review.md +45 -0
- package/tasks/pending/FEATURE-001-dashboard-mvp.md +268 -0
- package/tasks/review/ARCH-007-daemon-monolith.md +162 -0
- package/tasks/review/bmad-review-aegis.md +349 -0
- package/tasks/review/bmad-review-anvil.md +259 -0
- package/tasks/review/bmad-review-crucible.md +277 -0
- package/tasks/review/bmad-review-ember.md +307 -0
- package/tasks/review/bmad-review-furnace.md +285 -0
- package/tasks/review/bmad-review-pixel.md +329 -0
- package/tasks/review/bmad-review-scribe.md +361 -0
- package/tasks/review/bmad-review-sentinel.md +242 -0
- package/tasks/review/task-001.md +78 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Vibe Forge - SQLite Database Operations
|
|
4
|
+
# Handles daemon state persistence and agent status aggregation
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
# Database file location (set after FORGE_ROOT is known)
|
|
8
|
+
FORGE_DB=""
|
|
9
|
+
|
|
10
|
+
# =============================================================================
|
|
11
|
+
# Security Functions
|
|
12
|
+
# =============================================================================
|
|
13
|
+
|
|
14
|
+
# Escape a string for safe use in SQLite queries
|
|
15
|
+
# Escapes single quotes by doubling them (SQL standard)
|
|
16
|
+
# Also removes null bytes and other dangerous characters
|
|
17
|
+
db_escape() {
|
|
18
|
+
local input="$1"
|
|
19
|
+
# Remove null bytes
|
|
20
|
+
input="${input//$'\0'/}"
|
|
21
|
+
# Escape single quotes by doubling them
|
|
22
|
+
input="${input//\'/\'\'}"
|
|
23
|
+
# Remove backslashes that could escape quotes
|
|
24
|
+
input="${input//\\/}"
|
|
25
|
+
echo "$input"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Validate that input contains only safe characters for identifiers
|
|
29
|
+
# Returns 0 if safe, 1 if unsafe
|
|
30
|
+
db_validate_identifier() {
|
|
31
|
+
local input="$1"
|
|
32
|
+
# Allow only alphanumeric, underscore, hyphen
|
|
33
|
+
if [[ "$input" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
|
34
|
+
return 0
|
|
35
|
+
fi
|
|
36
|
+
return 1
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# =============================================================================
|
|
40
|
+
# Database Initialization
|
|
41
|
+
# =============================================================================
|
|
42
|
+
|
|
43
|
+
# Require FORGE_DB to be set before database operations
|
|
44
|
+
# Call this at the start of database functions that need the path
|
|
45
|
+
# Usage: db_require_init || return 1
|
|
46
|
+
db_require_init() {
|
|
47
|
+
if [[ -z "$FORGE_DB" ]]; then
|
|
48
|
+
echo "Error: FORGE_DB not set. Set FORGE_DB path before calling database functions." >&2
|
|
49
|
+
return 1
|
|
50
|
+
fi
|
|
51
|
+
return 0
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Initialize the database with schema
|
|
55
|
+
# Requires: FORGE_DB must be set before calling
|
|
56
|
+
db_init() {
|
|
57
|
+
db_require_init || return 1
|
|
58
|
+
local db_dir
|
|
59
|
+
db_dir=$(dirname "$FORGE_DB")
|
|
60
|
+
|
|
61
|
+
# Create directory if needed
|
|
62
|
+
mkdir -p "$db_dir"
|
|
63
|
+
chmod 700 "$db_dir"
|
|
64
|
+
|
|
65
|
+
# Enable WAL mode for concurrent reads without blocking writes.
|
|
66
|
+
# WAL + NORMAL synchronous is the recommended pairing for local tooling:
|
|
67
|
+
# faster than DELETE+FULL while still durable against OS crashes.
|
|
68
|
+
sqlite3 "$FORGE_DB" "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;"
|
|
69
|
+
|
|
70
|
+
# Create tables if they don't exist
|
|
71
|
+
sqlite3 "$FORGE_DB" <<'SQL'
|
|
72
|
+
-- Daemon configuration (single row)
|
|
73
|
+
CREATE TABLE IF NOT EXISTS daemon_config (
|
|
74
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
75
|
+
state TEXT DEFAULT 'idle',
|
|
76
|
+
poll_interval_ms INTEGER DEFAULT 5000,
|
|
77
|
+
last_activity_at TEXT,
|
|
78
|
+
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
-- Polling presets
|
|
82
|
+
CREATE TABLE IF NOT EXISTS poll_presets (
|
|
83
|
+
state TEXT PRIMARY KEY,
|
|
84
|
+
interval_ms INTEGER NOT NULL
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
-- Insert default presets if not exist
|
|
88
|
+
INSERT OR IGNORE INTO poll_presets (state, interval_ms) VALUES
|
|
89
|
+
('active', 5000),
|
|
90
|
+
('idle', 30000),
|
|
91
|
+
('stopped', 60000);
|
|
92
|
+
|
|
93
|
+
-- Initialize daemon_config if empty
|
|
94
|
+
INSERT OR IGNORE INTO daemon_config (id, state, poll_interval_ms)
|
|
95
|
+
VALUES (1, 'idle', 30000);
|
|
96
|
+
|
|
97
|
+
-- Agent status (aggregated from JSON files)
|
|
98
|
+
CREATE TABLE IF NOT EXISTS agent_status (
|
|
99
|
+
agent TEXT PRIMARY KEY,
|
|
100
|
+
status TEXT DEFAULT 'unknown',
|
|
101
|
+
task TEXT,
|
|
102
|
+
message TEXT,
|
|
103
|
+
updated_at TEXT,
|
|
104
|
+
file_mtime INTEGER DEFAULT 0
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
-- Status history for metrics (optional, pruned periodically)
|
|
108
|
+
CREATE TABLE IF NOT EXISTS status_history (
|
|
109
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
110
|
+
agent TEXT NOT NULL,
|
|
111
|
+
status TEXT NOT NULL,
|
|
112
|
+
task TEXT,
|
|
113
|
+
recorded_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
-- Index for history queries
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_history_agent ON status_history(agent);
|
|
118
|
+
CREATE INDEX IF NOT EXISTS idx_history_recorded ON status_history(recorded_at);
|
|
119
|
+
SQL
|
|
120
|
+
|
|
121
|
+
# Set secure permissions on database file
|
|
122
|
+
chmod 600 "$FORGE_DB" 2>/dev/null || true
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# =============================================================================
|
|
126
|
+
# Daemon State Operations
|
|
127
|
+
# =============================================================================
|
|
128
|
+
|
|
129
|
+
# Get current daemon state (active/idle/stopped)
|
|
130
|
+
db_get_daemon_state() {
|
|
131
|
+
db_require_init || return 1
|
|
132
|
+
sqlite3 "$FORGE_DB" "SELECT state FROM daemon_config WHERE id = 1;"
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# Set daemon state and update poll interval accordingly
|
|
136
|
+
db_set_daemon_state() {
|
|
137
|
+
db_require_init || return 1
|
|
138
|
+
local new_state="$1"
|
|
139
|
+
local interval
|
|
140
|
+
|
|
141
|
+
# SECURITY: Validate state is a known value
|
|
142
|
+
case "$new_state" in
|
|
143
|
+
active|idle|stopped) ;;
|
|
144
|
+
*)
|
|
145
|
+
echo "Invalid daemon state: $new_state" >&2
|
|
146
|
+
return 1
|
|
147
|
+
;;
|
|
148
|
+
esac
|
|
149
|
+
|
|
150
|
+
# Get interval for this state from presets (safe - validated above)
|
|
151
|
+
interval=$(sqlite3 "$FORGE_DB" "SELECT interval_ms FROM poll_presets WHERE state = '$new_state';")
|
|
152
|
+
interval="${interval:-30000}"
|
|
153
|
+
|
|
154
|
+
sqlite3 "$FORGE_DB" <<SQL
|
|
155
|
+
UPDATE daemon_config
|
|
156
|
+
SET state = '$new_state',
|
|
157
|
+
poll_interval_ms = $interval,
|
|
158
|
+
updated_at = datetime('now')
|
|
159
|
+
WHERE id = 1;
|
|
160
|
+
SQL
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# Get current poll interval in milliseconds
|
|
164
|
+
db_get_poll_interval_ms() {
|
|
165
|
+
sqlite3 "$FORGE_DB" "SELECT poll_interval_ms FROM daemon_config WHERE id = 1;"
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# Update last activity timestamp
|
|
169
|
+
db_touch_activity() {
|
|
170
|
+
sqlite3 "$FORGE_DB" <<SQL
|
|
171
|
+
UPDATE daemon_config
|
|
172
|
+
SET last_activity_at = datetime('now'),
|
|
173
|
+
updated_at = datetime('now')
|
|
174
|
+
WHERE id = 1;
|
|
175
|
+
SQL
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# =============================================================================
|
|
179
|
+
# Agent Status Operations
|
|
180
|
+
# =============================================================================
|
|
181
|
+
|
|
182
|
+
# Upsert agent status (from JSON file data)
|
|
183
|
+
db_upsert_agent_status() {
|
|
184
|
+
db_require_init || return 1
|
|
185
|
+
local agent="$1"
|
|
186
|
+
local status="$2"
|
|
187
|
+
local task="$3"
|
|
188
|
+
local message="$4"
|
|
189
|
+
local updated_at="$5"
|
|
190
|
+
local file_mtime="$6"
|
|
191
|
+
|
|
192
|
+
# SECURITY: Escape all string inputs to prevent SQL injection
|
|
193
|
+
agent=$(db_escape "$agent")
|
|
194
|
+
status=$(db_escape "$status")
|
|
195
|
+
task=$(db_escape "$task")
|
|
196
|
+
message=$(db_escape "$message")
|
|
197
|
+
updated_at=$(db_escape "$updated_at")
|
|
198
|
+
|
|
199
|
+
# Validate file_mtime is numeric
|
|
200
|
+
if ! [[ "$file_mtime" =~ ^[0-9]+$ ]]; then
|
|
201
|
+
file_mtime=0
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
sqlite3 "$FORGE_DB" <<SQL
|
|
205
|
+
INSERT INTO agent_status (agent, status, task, message, updated_at, file_mtime)
|
|
206
|
+
VALUES ('$agent', '$status', '$task', '$message', '$updated_at', $file_mtime)
|
|
207
|
+
ON CONFLICT(agent) DO UPDATE SET
|
|
208
|
+
status = excluded.status,
|
|
209
|
+
task = excluded.task,
|
|
210
|
+
message = excluded.message,
|
|
211
|
+
updated_at = excluded.updated_at,
|
|
212
|
+
file_mtime = excluded.file_mtime;
|
|
213
|
+
SQL
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# Get stored mtime for an agent's status file
|
|
217
|
+
db_get_agent_mtime() {
|
|
218
|
+
local agent="$1"
|
|
219
|
+
# SECURITY: Escape agent name to prevent SQL injection
|
|
220
|
+
agent=$(db_escape "$agent")
|
|
221
|
+
sqlite3 "$FORGE_DB" "SELECT file_mtime FROM agent_status WHERE agent = '$agent';" 2>/dev/null || echo "0"
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
# Get all agent statuses
|
|
225
|
+
db_get_all_agent_statuses() {
|
|
226
|
+
sqlite3 -separator '|' "$FORGE_DB" <<SQL
|
|
227
|
+
SELECT agent, status, task, message, updated_at
|
|
228
|
+
FROM agent_status
|
|
229
|
+
ORDER BY agent;
|
|
230
|
+
SQL
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
# Get agents with specific status
|
|
234
|
+
db_get_agents_by_status() {
|
|
235
|
+
local status="$1"
|
|
236
|
+
# SECURITY: Escape status to prevent SQL injection
|
|
237
|
+
status=$(db_escape "$status")
|
|
238
|
+
sqlite3 "$FORGE_DB" "SELECT agent FROM agent_status WHERE status = '$status';"
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# Count active workers (status = 'working')
|
|
242
|
+
db_count_active_workers() {
|
|
243
|
+
sqlite3 "$FORGE_DB" "SELECT COUNT(*) FROM agent_status WHERE status = 'working';"
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
# Delete stale agent status entries
|
|
247
|
+
db_cleanup_stale_agents() {
|
|
248
|
+
local minutes="${1:-30}"
|
|
249
|
+
# SECURITY: Validate minutes is numeric to prevent SQL injection
|
|
250
|
+
if ! [[ "$minutes" =~ ^[0-9]+$ ]]; then
|
|
251
|
+
minutes=30
|
|
252
|
+
fi
|
|
253
|
+
sqlite3 "$FORGE_DB" <<SQL
|
|
254
|
+
DELETE FROM agent_status
|
|
255
|
+
WHERE updated_at < datetime('now', '-$minutes minutes');
|
|
256
|
+
SQL
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
# =============================================================================
|
|
260
|
+
# History Operations (for future metrics)
|
|
261
|
+
# =============================================================================
|
|
262
|
+
|
|
263
|
+
# Record status change in history
|
|
264
|
+
db_record_status_history() {
|
|
265
|
+
local agent="$1"
|
|
266
|
+
local status="$2"
|
|
267
|
+
local task="$3"
|
|
268
|
+
|
|
269
|
+
# SECURITY: Escape all inputs to prevent SQL injection
|
|
270
|
+
agent=$(db_escape "$agent")
|
|
271
|
+
status=$(db_escape "$status")
|
|
272
|
+
task=$(db_escape "$task")
|
|
273
|
+
|
|
274
|
+
sqlite3 "$FORGE_DB" <<SQL
|
|
275
|
+
INSERT INTO status_history (agent, status, task)
|
|
276
|
+
VALUES ('$agent', '$status', '$task');
|
|
277
|
+
SQL
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
# Prune old history entries (keep last N days)
|
|
281
|
+
db_prune_history() {
|
|
282
|
+
local days="${1:-7}"
|
|
283
|
+
# SECURITY: Validate days is numeric to prevent SQL injection
|
|
284
|
+
if ! [[ "$days" =~ ^[0-9]+$ ]]; then
|
|
285
|
+
days=7
|
|
286
|
+
fi
|
|
287
|
+
sqlite3 "$FORGE_DB" <<SQL
|
|
288
|
+
DELETE FROM status_history
|
|
289
|
+
WHERE recorded_at < datetime('now', '-$days days');
|
|
290
|
+
SQL
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
# =============================================================================
|
|
294
|
+
# Utility Functions
|
|
295
|
+
# =============================================================================
|
|
296
|
+
|
|
297
|
+
# Check if database exists and is valid
|
|
298
|
+
db_exists() {
|
|
299
|
+
[[ -n "$FORGE_DB" ]] && [[ -f "$FORGE_DB" ]] && sqlite3 "$FORGE_DB" "SELECT 1 FROM daemon_config LIMIT 1;" &>/dev/null
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
# Get database stats for debugging
|
|
303
|
+
db_stats() {
|
|
304
|
+
db_require_init || return 1
|
|
305
|
+
echo "Database: $FORGE_DB"
|
|
306
|
+
echo "Agent count: $(sqlite3 "$FORGE_DB" "SELECT COUNT(*) FROM agent_status;")"
|
|
307
|
+
echo "History entries: $(sqlite3 "$FORGE_DB" "SELECT COUNT(*) FROM status_history;")"
|
|
308
|
+
echo "Current state: $(db_get_daemon_state)"
|
|
309
|
+
echo "Poll interval: $(db_get_poll_interval_ms)ms"
|
|
310
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Heimdall Setup -- writes .claude/settings.local.json into a worker's
|
|
4
|
+
* working directory to register Heimdall as a PreToolUse hook.
|
|
5
|
+
*
|
|
6
|
+
* Called by the forge daemon at inbox-write time, before the worker
|
|
7
|
+
* picks up a lab task.
|
|
8
|
+
*
|
|
9
|
+
* Uses a merge strategy: if settings.local.json already exists, the
|
|
10
|
+
* Heimdall hooks are merged into the existing PreToolUse array rather
|
|
11
|
+
* than overwriting the file.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict'
|
|
15
|
+
|
|
16
|
+
const fs = require('fs')
|
|
17
|
+
const path = require('path')
|
|
18
|
+
|
|
19
|
+
// Absolute path to heimdall.js -- resolvable from any working directory
|
|
20
|
+
const HEIMDALL_PATH = path.resolve(__dirname, 'heimdall.js').replace(/\\/g, '/')
|
|
21
|
+
|
|
22
|
+
const HEIMDALL_HOOKS = ['Bash', 'Write', 'Edit'].map(matcher => ({
|
|
23
|
+
matcher,
|
|
24
|
+
hooks: [{ type: 'command', command: `node "${HEIMDALL_PATH}"` }],
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* writeHeimdallHooks(worktreePath)
|
|
29
|
+
*
|
|
30
|
+
* Writes or merges Heimdall PreToolUse hooks into:
|
|
31
|
+
* <worktreePath>/.claude/settings.local.json
|
|
32
|
+
*
|
|
33
|
+
* Safe to call multiple times -- idempotent.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} worktreePath Absolute path to the worker's worktree root
|
|
36
|
+
*/
|
|
37
|
+
function writeHeimdallHooks(worktreePath) {
|
|
38
|
+
const claudeDir = path.join(worktreePath, '.claude')
|
|
39
|
+
const settingsPath = path.join(claudeDir, 'settings.local.json')
|
|
40
|
+
|
|
41
|
+
// Ensure .claude/ exists
|
|
42
|
+
if (!fs.existsSync(claudeDir)) {
|
|
43
|
+
fs.mkdirSync(claudeDir, { recursive: true })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Read existing settings if present
|
|
47
|
+
let existing = {}
|
|
48
|
+
if (fs.existsSync(settingsPath)) {
|
|
49
|
+
try {
|
|
50
|
+
existing = JSON.parse(fs.readFileSync(settingsPath, 'utf8'))
|
|
51
|
+
} catch (_) {
|
|
52
|
+
// Corrupt file -- start fresh
|
|
53
|
+
existing = {}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Ensure hooks structure exists
|
|
58
|
+
if (!existing.hooks) existing.hooks = {}
|
|
59
|
+
if (!existing.hooks.PreToolUse) existing.hooks.PreToolUse = []
|
|
60
|
+
|
|
61
|
+
// Merge: add Heimdall hook entries for matchers not already registered
|
|
62
|
+
const existingMatchers = new Set(
|
|
63
|
+
existing.hooks.PreToolUse.map(h => h.matcher)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
for (const heimdallHook of HEIMDALL_HOOKS) {
|
|
67
|
+
if (!existingMatchers.has(heimdallHook.matcher)) {
|
|
68
|
+
existing.hooks.PreToolUse.push(heimdallHook)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fs.writeFileSync(settingsPath, JSON.stringify(existing, null, 2) + '\n')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* writeContextFile(worktreePath, context)
|
|
77
|
+
*
|
|
78
|
+
* Writes the Heimdall context file to the worktree root so Heimdall
|
|
79
|
+
* can read per-task policy on every invocation.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} worktreePath Absolute path to the worker's worktree root
|
|
82
|
+
* @param {object} context Context object matching the schema below
|
|
83
|
+
*
|
|
84
|
+
* Context schema:
|
|
85
|
+
* {
|
|
86
|
+
* story_id: string -- lab story ID (e.g. "FORGE-3")
|
|
87
|
+
* agent: string -- worker name (e.g. "anvil")
|
|
88
|
+
* worktree_path: string -- absolute path to worktree (same as worktreePath)
|
|
89
|
+
* assigned_branch: string -- git branch for this story
|
|
90
|
+
* handoff_dir: string -- absolute path to _vibe-chain-output/handoffs/
|
|
91
|
+
* escalation_dir: string -- absolute path to worker-inbox/<agent>/ dir
|
|
92
|
+
* audit_log: string -- absolute path to heimdall-audit.log
|
|
93
|
+
* has_db_migration: boolean
|
|
94
|
+
* has_api_changes: boolean
|
|
95
|
+
* allowed_paths: string[] -- absolute paths the worker may read/write
|
|
96
|
+
* }
|
|
97
|
+
*/
|
|
98
|
+
function writeContextFile(worktreePath, context) {
|
|
99
|
+
const contextPath = path.join(worktreePath, '.context.json')
|
|
100
|
+
fs.writeFileSync(contextPath, JSON.stringify(context, null, 2) + '\n')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* setup(worktreePath, context)
|
|
105
|
+
*
|
|
106
|
+
* Convenience function: writes both hooks and context file in one call.
|
|
107
|
+
*/
|
|
108
|
+
function setup(worktreePath, context) {
|
|
109
|
+
writeHeimdallHooks(worktreePath)
|
|
110
|
+
writeContextFile(worktreePath, context)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { setup, writeHeimdallHooks, writeContextFile, HEIMDALL_PATH }
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Heimdall -- Forge worker pre-tool hook interceptor (PLAT-1)
|
|
4
|
+
*
|
|
5
|
+
* Guards the Bifrost: intercepts every tool call before execution,
|
|
6
|
+
* checks it against per-task policy, and blocks or allows.
|
|
7
|
+
*
|
|
8
|
+
* Registered as a PreToolUse hook via .claude/settings.local.json
|
|
9
|
+
* written by the forge daemon at worker startup.
|
|
10
|
+
*
|
|
11
|
+
* Exit codes:
|
|
12
|
+
* 0 -- allow (optionally with audit log entry)
|
|
13
|
+
* 2 -- block (explanation written to stdout, fed back to model)
|
|
14
|
+
*
|
|
15
|
+
* Context file (.context.json in process.cwd()) is written by the forge
|
|
16
|
+
* daemon alongside each inbox task. If absent, Heimdall exits 0 immediately
|
|
17
|
+
* -- forge-native tasks are not restricted.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict'
|
|
21
|
+
|
|
22
|
+
const fs = require('fs')
|
|
23
|
+
const path = require('path')
|
|
24
|
+
|
|
25
|
+
// ── Input ─────────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
let input
|
|
28
|
+
try {
|
|
29
|
+
input = JSON.parse(fs.readFileSync(0, 'utf8'))
|
|
30
|
+
} catch (e) {
|
|
31
|
+
// Malformed input -- allow and exit rather than blocking the worker
|
|
32
|
+
process.exit(0)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { tool_name, tool_input } = input
|
|
36
|
+
|
|
37
|
+
// ── Context file ──────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
const contextPath = path.join(process.cwd(), '.context.json')
|
|
40
|
+
|
|
41
|
+
// No context file = forge-native task. Heimdall does not restrict forge-native work.
|
|
42
|
+
if (!fs.existsSync(contextPath)) {
|
|
43
|
+
process.exit(0)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let ctx
|
|
47
|
+
try {
|
|
48
|
+
ctx = JSON.parse(fs.readFileSync(contextPath, 'utf8'))
|
|
49
|
+
} catch (e) {
|
|
50
|
+
// Unreadable context -- fail open to avoid blocking forge-native work
|
|
51
|
+
process.exit(0)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const {
|
|
55
|
+
story_id,
|
|
56
|
+
agent,
|
|
57
|
+
worktree_path,
|
|
58
|
+
has_db_migration,
|
|
59
|
+
allowed_paths,
|
|
60
|
+
escalation_dir,
|
|
61
|
+
audit_log: ctxAuditLog,
|
|
62
|
+
} = ctx
|
|
63
|
+
|
|
64
|
+
// ── Logging ───────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
const AUDIT_LOG = ctxAuditLog
|
|
67
|
+
|| path.join(process.cwd(), '_vibe-chain-output', 'heimdall-audit.log')
|
|
68
|
+
|
|
69
|
+
const MAX_VIOLATIONS = parseInt(process.env.HEIMDALL_MAX_VIOLATIONS || '3', 10)
|
|
70
|
+
|
|
71
|
+
// Violation tracking lives in the worktree root (process.cwd())
|
|
72
|
+
const VIOLATION_FILE = path.join(process.cwd(), `.${story_id}.violations`)
|
|
73
|
+
|
|
74
|
+
// Escalation signal written to the inbox agent dir so the daemon detects it
|
|
75
|
+
const ESCALATION_DIR = escalation_dir || process.cwd()
|
|
76
|
+
|
|
77
|
+
function timestamp() {
|
|
78
|
+
return new Date().toISOString()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function audit(level, message) {
|
|
82
|
+
const line = `[${timestamp()}] HEIMDALL ${level} ${agent}/${story_id}: ${message}\n`
|
|
83
|
+
try {
|
|
84
|
+
const dir = path.dirname(AUDIT_LOG)
|
|
85
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
86
|
+
fs.appendFileSync(AUDIT_LOG, line)
|
|
87
|
+
} catch (_) {
|
|
88
|
+
// Audit log write failure must never block or crash the hook
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function block(reason) {
|
|
93
|
+
audit('BLOCKED', reason)
|
|
94
|
+
|
|
95
|
+
// Track violations
|
|
96
|
+
let violations = 0
|
|
97
|
+
try {
|
|
98
|
+
if (fs.existsSync(VIOLATION_FILE)) {
|
|
99
|
+
violations = parseInt(fs.readFileSync(VIOLATION_FILE, 'utf8').trim(), 10) || 0
|
|
100
|
+
}
|
|
101
|
+
} catch (_) {}
|
|
102
|
+
|
|
103
|
+
violations++
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
fs.writeFileSync(VIOLATION_FILE, String(violations))
|
|
107
|
+
} catch (_) {}
|
|
108
|
+
|
|
109
|
+
if (violations >= MAX_VIOLATIONS) {
|
|
110
|
+
// Sound the Gjallarhorn
|
|
111
|
+
const escalationFile = path.join(ESCALATION_DIR, `${story_id}.escalation`)
|
|
112
|
+
try {
|
|
113
|
+
fs.writeFileSync(escalationFile, JSON.stringify({
|
|
114
|
+
story_id,
|
|
115
|
+
agent,
|
|
116
|
+
violations,
|
|
117
|
+
last_reason: reason,
|
|
118
|
+
timestamp: timestamp(),
|
|
119
|
+
}, null, 2))
|
|
120
|
+
} catch (_) {}
|
|
121
|
+
|
|
122
|
+
audit('SOUNDED', `${violations} violations -- escalating to human review`)
|
|
123
|
+
|
|
124
|
+
console.log(
|
|
125
|
+
`[HEIMDALL] Action blocked: ${reason}\n` +
|
|
126
|
+
`[HEIMDALL] GJALLARHORN SOUNDED: ${violations} violations recorded.\n` +
|
|
127
|
+
`[HEIMDALL] Story ${story_id} has been escalated to human review. Stop working on this story.`
|
|
128
|
+
)
|
|
129
|
+
} else {
|
|
130
|
+
console.log(
|
|
131
|
+
`[HEIMDALL] Action blocked: ${reason}\n` +
|
|
132
|
+
`[HEIMDALL] Violation ${violations}/${MAX_VIOLATIONS}. Self-correct and continue.`
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
process.exit(2)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function allow(note) {
|
|
140
|
+
if (note) audit('ALLOWED', note)
|
|
141
|
+
process.exit(0)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── Path helpers ──────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
function isOutsideAllowedPaths(targetPath) {
|
|
147
|
+
if (!targetPath) return false
|
|
148
|
+
try {
|
|
149
|
+
const resolved = path.resolve(targetPath)
|
|
150
|
+
return !allowed_paths.some(p => resolved.startsWith(path.resolve(p)))
|
|
151
|
+
} catch (_) {
|
|
152
|
+
return true // Unresolvable path -- treat as outside
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Bash tool checks ──────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
if (tool_name === 'Bash') {
|
|
159
|
+
const cmd = (tool_input && tool_input.command || '').trim()
|
|
160
|
+
|
|
161
|
+
// Destructive rm -- check if target escapes worktree
|
|
162
|
+
if (/rm\s+-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r/.test(cmd) && /\brm\b/.test(cmd)) {
|
|
163
|
+
// Extract everything after the flags as the target
|
|
164
|
+
const rmMatch = cmd.match(/rm\s+(?:-\S+\s+)(.+)/)
|
|
165
|
+
const rmTarget = rmMatch ? rmMatch[1].trim().replace(/^['"]|['"]$/g, '') : ''
|
|
166
|
+
|
|
167
|
+
const isRootTarget = /^(\/|~)/.test(rmTarget) || rmTarget === '' || rmTarget.includes('..')
|
|
168
|
+
const isOutside = !rmTarget || isRootTarget || isOutsideAllowedPaths(rmTarget)
|
|
169
|
+
|
|
170
|
+
if (isOutside) {
|
|
171
|
+
block(`destructive rm outside worktree: ${cmd}`)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Credential access
|
|
176
|
+
if (/\.(env|pem|key|cert)\b/.test(cmd) && !/\.env\.(example|sample|template)/.test(cmd)) {
|
|
177
|
+
block(`credential file access: ${cmd}`)
|
|
178
|
+
}
|
|
179
|
+
if (/~\/\.(ssh|aws|gnupg)\b/.test(cmd)) {
|
|
180
|
+
block(`credential directory access: ${cmd}`)
|
|
181
|
+
}
|
|
182
|
+
// Echoing or exporting secrets (but not reading them as part of build)
|
|
183
|
+
if (/(echo|printf|export)\s+.*\b(TOKEN|SECRET|PASSWORD|API_KEY)\s*=/.test(cmd)) {
|
|
184
|
+
block(`credential assignment in shell: ${cmd}`)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Dangerous git operations
|
|
188
|
+
if (/git\s+push\b.*--force(-with-lease)?/.test(cmd)) {
|
|
189
|
+
block(`force push blocked: ${cmd}`)
|
|
190
|
+
}
|
|
191
|
+
if (/git\s+reset\s+--hard\s+HEAD~[2-9]/.test(cmd)) {
|
|
192
|
+
block(`multi-commit hard reset blocked: ${cmd}`)
|
|
193
|
+
}
|
|
194
|
+
if (/git\s+clean\s+-[a-zA-Z]*f[a-zA-Z]*d|-[a-zA-Z]*d[a-zA-Z]*f/.test(cmd) && /\bgit\b/.test(cmd)) {
|
|
195
|
+
block(`git clean -fd blocked: ${cmd}`)
|
|
196
|
+
}
|
|
197
|
+
// Push to a non-origin remote (e.g. git push upstream)
|
|
198
|
+
if (/git\s+push\s+(?!origin\b)(\S+)/.test(cmd)) {
|
|
199
|
+
const remoteMatch = cmd.match(/git\s+push\s+(\S+)/)
|
|
200
|
+
if (remoteMatch && remoteMatch[1] !== 'origin' && !remoteMatch[1].startsWith('-')) {
|
|
201
|
+
block(`push to non-origin remote blocked: ${cmd}`)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Direct push to main/master — all changes must go through PRs
|
|
205
|
+
if (/git\s+push\b/.test(cmd) && /\b(main|master)\b/.test(cmd)) {
|
|
206
|
+
block(`direct push to main/master blocked — create a feature branch and open a PR`)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// DB destructive operations without authorization
|
|
210
|
+
if (/(DROP\s+TABLE|TRUNCATE\s+TABLE)/i.test(cmd) && !has_db_migration) {
|
|
211
|
+
block(`DROP/TRUNCATE requires has_db_migration: true on the story`)
|
|
212
|
+
}
|
|
213
|
+
if (/DELETE\s+FROM\s+\S+\s*;?\s*$/i.test(cmd) && !/WHERE\s+/i.test(cmd)) {
|
|
214
|
+
block(`DELETE without WHERE clause blocked: ${cmd}`)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// High-risk but legitimate -- audit log only
|
|
218
|
+
if (/\b(npm|yarn|pnpm)\s+(install|add|ci)\b/.test(cmd)) {
|
|
219
|
+
allow(`dependency install: ${cmd}`)
|
|
220
|
+
}
|
|
221
|
+
if (/\b(pip|pip3)\s+install\b/.test(cmd)) {
|
|
222
|
+
allow(`pip install: ${cmd}`)
|
|
223
|
+
}
|
|
224
|
+
if (/\bcargo\s+(build|install)\b/.test(cmd)) {
|
|
225
|
+
allow(`cargo build: ${cmd}`)
|
|
226
|
+
}
|
|
227
|
+
if (/\b(curl|wget)\b/.test(cmd)) {
|
|
228
|
+
allow(`network request: ${cmd}`)
|
|
229
|
+
}
|
|
230
|
+
if (/git\s+commit\s+--amend/.test(cmd)) {
|
|
231
|
+
allow(`git commit --amend`)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── Write / Edit tool checks ──────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
if (tool_name === 'Write' || tool_name === 'Edit') {
|
|
238
|
+
const filePath = tool_input && (tool_input.file_path || tool_input.path) || ''
|
|
239
|
+
|
|
240
|
+
if (filePath) {
|
|
241
|
+
// Credential files
|
|
242
|
+
if (/\.(env|pem|key|cert)$/.test(filePath) && !/\.(example|sample|template)$/.test(filePath)) {
|
|
243
|
+
block(`write to credential file: ${filePath}`)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Settings that could override Heimdall
|
|
247
|
+
if (/\.claude[/\\]settings(\.local)?\.json$/.test(filePath)) {
|
|
248
|
+
block(`write to .claude/settings blocked -- Heimdall config is daemon-managed`)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Path escape check
|
|
252
|
+
if (isOutsideAllowedPaths(filePath)) {
|
|
253
|
+
block(`path escape: ${filePath} is outside allowed paths`)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Pipeline output writes -- allowed but logged
|
|
257
|
+
if (filePath.includes('_vibe-chain-output')) {
|
|
258
|
+
allow(`pipeline output write: ${filePath}`)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ── Default: allow ────────────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
allow(null)
|