sinapse-ai 1.7.0 → 1.8.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/CLAUDE.md +5 -11
- package/.claude/hooks/README.md +14 -1
- package/.claude/hooks/code-intel-pretool.cjs +115 -0
- package/.claude/hooks/enforce-delegation.cjs +31 -3
- package/.claude/hooks/enforce-framework-boundary.cjs +324 -0
- package/.claude/hooks/enforce-permission-mode.cjs +249 -0
- package/.claude/hooks/secret-scanning.cjs +34 -43
- package/.claude/hooks/synapse-engine.cjs +23 -23
- package/.claude/hooks/telemetry-post-tool.cjs +128 -0
- package/.claude/hooks/telemetry-stop.cjs +132 -0
- package/.claude/hooks/verify-packages.cjs +9 -2
- package/.claude/rules/hook-governance.md +2 -0
- package/.sinapse-ai/cli/commands/health/index.js +24 -0
- package/.sinapse-ai/core/README.md +11 -0
- package/.sinapse-ai/core/config/config-loader.js +19 -0
- package/.sinapse-ai/core/execution/build-orchestrator.js +4 -1
- package/.sinapse-ai/core/execution/parallel-executor.js +7 -1
- package/.sinapse-ai/core/execution/subagent-dispatcher.js +126 -28
- package/.sinapse-ai/core/execution/wave-executor.js +4 -1
- package/.sinapse-ai/core/grounding/README.md +71 -11
- package/.sinapse-ai/core/health-check/checks/project/framework-config.js +38 -2
- package/.sinapse-ai/core/health-check/checks/project/package-json.js +47 -3
- package/.sinapse-ai/core/health-check/checks/services/gemini-cli.js +117 -0
- package/.sinapse-ai/core/health-check/checks/services/index.js +2 -0
- package/.sinapse-ai/core/health-check/healers/index.js +40 -3
- package/.sinapse-ai/core/ideation/ideation-engine.js +170 -121
- package/.sinapse-ai/core/ids/gate-evaluator.js +318 -0
- package/.sinapse-ai/core/ids/gates/g5-semantic-handshake.js +190 -0
- package/.sinapse-ai/core/ids/gates/g6-ci-integrity.js +162 -0
- package/.sinapse-ai/core/ids/index.js +30 -0
- package/.sinapse-ai/core/memory/__tests__/active-modules.verify.js +11 -0
- package/.sinapse-ai/core/orchestration/agent-invoker.js +29 -6
- package/.sinapse-ai/core/orchestration/brownfield-handler.js +36 -3
- package/.sinapse-ai/core/orchestration/executors/epic-3-executor.js +76 -5
- package/.sinapse-ai/core/orchestration/executors/epic-4-executor.js +63 -17
- package/.sinapse-ai/core/orchestration/executors/epic-6-executor.js +153 -41
- package/.sinapse-ai/core/orchestration/executors/epic-executor.js +40 -0
- package/.sinapse-ai/core/orchestration/greenfield-handler.js +87 -3
- package/.sinapse-ai/core/orchestration/master-orchestrator.js +105 -7
- package/.sinapse-ai/core/orchestration/parallel-executor.js +6 -1
- package/.sinapse-ai/core/orchestration/workflow-executor.js +41 -0
- package/.sinapse-ai/core/registry/squad-agent-resolver.js +253 -0
- package/.sinapse-ai/core/telemetry/ids-sink.js +188 -0
- package/.sinapse-ai/core/utils/output-formatter.js +8 -290
- package/.sinapse-ai/core-config.yaml +49 -1
- package/.sinapse-ai/data/entity-registry.yaml +15081 -13735
- package/.sinapse-ai/data/registry-update-log.jsonl +86 -0
- package/.sinapse-ai/development/agents/developer.md +2 -0
- package/.sinapse-ai/development/agents/devops.md +9 -0
- package/.sinapse-ai/development/external-executors/README.md +18 -0
- package/.sinapse-ai/development/external-executors/codex.md +56 -0
- package/.sinapse-ai/development/scripts/populate-entity-registry.js +65 -9
- package/.sinapse-ai/development/scripts/squad/squad-downloader.js +54 -11
- package/.sinapse-ai/development/tasks/delegate-to-external-executor.md +152 -0
- package/.sinapse-ai/development/tasks/github-devops-pre-push-quality-gate.md +46 -29
- package/.sinapse-ai/development/tasks/update-sinapse.md +3 -3
- package/.sinapse-ai/hooks/sinapse-brand-grounding.cjs +4 -7
- package/.sinapse-ai/hooks/sinapse-ds-grounding.cjs +4 -7
- package/.sinapse-ai/hooks/sinapse-vault-grounding.cjs +4 -7
- package/.sinapse-ai/infrastructure/integrations/ai-providers/ai-provider-factory.js +4 -1
- package/.sinapse-ai/infrastructure/integrations/ai-providers/claude-provider.js +57 -55
- package/.sinapse-ai/infrastructure/scripts/ide-sync/gemini-commands.js +298 -0
- package/.sinapse-ai/infrastructure/scripts/ide-sync/index.js +127 -6
- package/.sinapse-ai/infrastructure/scripts/ide-sync/persona-renderer.js +97 -0
- package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/antigravity.js +121 -0
- package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/cursor.js +119 -0
- package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/github-copilot.js +191 -0
- package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/kimi.js +448 -0
- package/.sinapse-ai/install-manifest.yaml +158 -90
- package/.sinapse-ai/scripts/pm.sh +18 -6
- package/bin/cli.js +17 -0
- package/bin/commands/agents.js +96 -0
- package/bin/commands/doctor.js +15 -0
- package/bin/commands/ideate.js +129 -0
- package/bin/commands/uninstall.js +40 -0
- package/bin/postinstall.js +50 -4
- package/bin/sinapse.js +146 -2
- package/bin/utils/secret-scanner-core.js +253 -0
- package/bin/utils/staged-secret-scan.js +106 -40
- package/package.json +13 -3
- package/packages/installer/src/installer/git-hooks-installer.js +384 -0
- package/packages/installer/src/installer/sinapse-ai-installer.js +16 -0
- package/packages/installer/src/wizard/ide-config-generator.js +23 -0
- package/packages/installer/src/wizard/validators.js +38 -1
- package/packages/installer/tests/unit/artifact-copy-pipeline/artifact-copy-pipeline.test.js +5 -1
- package/packages/installer/tests/unit/git-hooks-installer.test.js +262 -0
- package/scripts/eval-runner.js +422 -0
- package/scripts/generate-install-manifest.js +13 -9
- package/scripts/generate-synapse-runtime.js +51 -0
- package/scripts/validate-all.js +1 -0
- package/scripts/validate-evals.js +466 -0
- package/scripts/validate-schemas.js +539 -0
- package/scripts/validate-squad-orqx.js +9 -2
- package/.sinapse-ai/development/scripts/elicitation-engine.js +0 -385
- package/.sinapse-ai/development/scripts/elicitation-session-manager.js +0 -300
- package/.sinapse-ai/development/tasks/test-validation-task.md +0 -172
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook: Enforce Permission Mode — PreToolUse guard
|
|
6
|
+
*
|
|
7
|
+
* RULE: When the project is in "explore" (read-only) mode, Write and Edit
|
|
8
|
+
* operations are BLOCKED. Read operations are always allowed.
|
|
9
|
+
* Bash commands classified as write/delete/execute are also blocked.
|
|
10
|
+
*
|
|
11
|
+
* Protocol (Claude Code PreToolUse):
|
|
12
|
+
* exit 0 → allow (operation proceeds)
|
|
13
|
+
* exit 2 → block (message shown to model via stderr)
|
|
14
|
+
*
|
|
15
|
+
* Fail-open policy:
|
|
16
|
+
* - If the permission config cannot be loaded, the hook allows (exit 0).
|
|
17
|
+
* - If mode is "ask" or "auto", the hook allows (exit 0).
|
|
18
|
+
* - Any unexpected error: exit 0.
|
|
19
|
+
*
|
|
20
|
+
* @module enforce-permission-mode
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Project root resolution
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
function projectRoot() {
|
|
31
|
+
return process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Lightweight mode loader (no yaml dependency — parses the simple key: value)
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse `permissions.mode` from a minimal YAML config without requiring js-yaml.
|
|
40
|
+
* Handles:
|
|
41
|
+
* permissions:
|
|
42
|
+
* mode: explore
|
|
43
|
+
*
|
|
44
|
+
* Returns 'ask' (the safe default) if the file is missing, unreadable, or
|
|
45
|
+
* the key is absent.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} configPath
|
|
48
|
+
* @returns {string} mode name
|
|
49
|
+
*/
|
|
50
|
+
function loadPermissionMode(configPath) {
|
|
51
|
+
try {
|
|
52
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
53
|
+
// Match "mode: <value>" under a "permissions:" block
|
|
54
|
+
const match = content.match(/^\s*mode:\s*["']?(\w+)["']?\s*$/m);
|
|
55
|
+
if (match) {
|
|
56
|
+
return match[1].toLowerCase();
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// File missing or unreadable — fail-open
|
|
60
|
+
}
|
|
61
|
+
return 'ask';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Operation classifier (mirrors OperationGuard.classifyOperation)
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
const SAFE_COMMANDS = [
|
|
69
|
+
'git status', 'git log', 'git diff', 'git branch', 'git show',
|
|
70
|
+
'git ls-files', 'git remote -v',
|
|
71
|
+
'ls', 'pwd', 'cat', 'head', 'tail', 'wc', 'find', 'grep', 'which',
|
|
72
|
+
'file', 'stat',
|
|
73
|
+
'npm list', 'npm outdated', 'npm audit', 'npm view', 'npm search',
|
|
74
|
+
'yarn list', 'yarn info', 'bun pm ls',
|
|
75
|
+
'node --version', 'npm --version', 'yarn --version', 'bun --version',
|
|
76
|
+
'git --version', 'python --version', 'python3 --version',
|
|
77
|
+
'uname', 'whoami', 'hostname', 'date', 'uptime', 'df -h', 'free -h',
|
|
78
|
+
'env', 'printenv',
|
|
79
|
+
'curl -I', 'ping -c', 'nslookup', 'dig',
|
|
80
|
+
'ps aux', 'top -l 1', 'htop',
|
|
81
|
+
'gh auth status', 'gh repo view', 'gh pr list', 'gh pr view',
|
|
82
|
+
'gh issue list', 'gh issue view', 'gh api',
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const DESTRUCTIVE_PATTERNS = [
|
|
86
|
+
/\brm\s+(-[rf]+\s+)?/,
|
|
87
|
+
/\brmdir\b/,
|
|
88
|
+
/\bunlink\b/,
|
|
89
|
+
/\bgit\s+reset\s+--hard\b/,
|
|
90
|
+
/\bgit\s+push\s+--force\b/,
|
|
91
|
+
/\bgit\s+push\s+-f\b/,
|
|
92
|
+
/\bgit\s+clean\s+-[fd]+/,
|
|
93
|
+
/\bgit\s+checkout\s+\.\s*$/,
|
|
94
|
+
/\bgit\s+restore\s+\.\s*$/,
|
|
95
|
+
/\bgit\s+stash\s+drop\b/,
|
|
96
|
+
/\bgit\s+branch\s+-[dD]\b/,
|
|
97
|
+
/\bDROP\s+(TABLE|DATABASE|INDEX|VIEW)\b/i,
|
|
98
|
+
/\bDELETE\s+FROM\b/i,
|
|
99
|
+
/\bTRUNCATE\b/i,
|
|
100
|
+
/\bALTER\s+TABLE\b.*\bDROP\b/i,
|
|
101
|
+
/\bkill\s+-9\b/,
|
|
102
|
+
/\bkillall\b/,
|
|
103
|
+
/\bshutdown\b/,
|
|
104
|
+
/\breboot\b/,
|
|
105
|
+
/\bnpm\s+uninstall\b/,
|
|
106
|
+
/\byarn\s+remove\b/,
|
|
107
|
+
/\bbun\s+remove\b/,
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
const WRITE_PATTERNS = [
|
|
111
|
+
/[^<]>/,
|
|
112
|
+
/>>/,
|
|
113
|
+
/\bmkdir\b/,
|
|
114
|
+
/\btouch\b/,
|
|
115
|
+
/\bmv\b/,
|
|
116
|
+
/\bcp\b/,
|
|
117
|
+
/\bln\b/,
|
|
118
|
+
/\bchmod\b/,
|
|
119
|
+
/\bchown\b/,
|
|
120
|
+
/\bsed\s+-i\b/,
|
|
121
|
+
/\bawk\s+-i\b/,
|
|
122
|
+
/\bgit\s+add\b/,
|
|
123
|
+
/\bgit\s+commit\b/,
|
|
124
|
+
/\bgit\s+push\b/,
|
|
125
|
+
/\bgit\s+merge\b/,
|
|
126
|
+
/\bgit\s+rebase\b/,
|
|
127
|
+
/\bgit\s+cherry-pick\b/,
|
|
128
|
+
/\bgit\s+stash\b/,
|
|
129
|
+
/\bnpm\s+install\b/,
|
|
130
|
+
/\bnpm\s+i\b/,
|
|
131
|
+
/\bnpm\s+ci\b/,
|
|
132
|
+
/\byarn\s+add\b/,
|
|
133
|
+
/\byarn\s+install\b/,
|
|
134
|
+
/\bbun\s+install\b/,
|
|
135
|
+
/\bbun\s+add\b/,
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Classify a Bash command as read/write/delete/execute.
|
|
140
|
+
* @param {string} command
|
|
141
|
+
* @returns {string}
|
|
142
|
+
*/
|
|
143
|
+
function classifyBashCommand(command) {
|
|
144
|
+
const normalized = command.trim().toLowerCase();
|
|
145
|
+
for (const safe of SAFE_COMMANDS) {
|
|
146
|
+
if (normalized.startsWith(safe.toLowerCase())) return 'read';
|
|
147
|
+
}
|
|
148
|
+
for (const pat of DESTRUCTIVE_PATTERNS) {
|
|
149
|
+
if (pat.test(command)) return 'delete';
|
|
150
|
+
}
|
|
151
|
+
for (const pat of WRITE_PATTERNS) {
|
|
152
|
+
if (pat.test(command)) return 'write';
|
|
153
|
+
}
|
|
154
|
+
return 'execute';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Classify a tool call into an operation type.
|
|
159
|
+
* @param {string} tool
|
|
160
|
+
* @param {Object} params
|
|
161
|
+
* @returns {string}
|
|
162
|
+
*/
|
|
163
|
+
function classifyOperation(tool, params) {
|
|
164
|
+
if (['Read', 'Glob', 'Grep', 'WebFetch', 'WebSearch'].includes(tool)) return 'read';
|
|
165
|
+
if (['Write', 'Edit', 'NotebookEdit'].includes(tool)) return 'write';
|
|
166
|
+
if (tool === 'Bash') return classifyBashCommand(params.command || '');
|
|
167
|
+
if (tool === 'Task') {
|
|
168
|
+
const readOnlyAgents = ['Explore', 'Plan', 'claude-code-guide'];
|
|
169
|
+
return readOnlyAgents.includes(params.subagent_type) ? 'read' : 'execute';
|
|
170
|
+
}
|
|
171
|
+
if (tool.startsWith('mcp__')) return 'execute';
|
|
172
|
+
return 'read'; // default to read (safe)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Main
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
function main() {
|
|
180
|
+
// Parse stdin — fail-open on any parse error
|
|
181
|
+
let input;
|
|
182
|
+
try {
|
|
183
|
+
input = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
184
|
+
} catch {
|
|
185
|
+
process.exit(0);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const toolName = (input.tool_name || '').trim();
|
|
189
|
+
const toolInput = input.tool_input || {};
|
|
190
|
+
|
|
191
|
+
// Resolve project root
|
|
192
|
+
const root = projectRoot();
|
|
193
|
+
const configPath = path.join(root, '.sinapse', 'config.yaml');
|
|
194
|
+
|
|
195
|
+
// Load permission mode — fail-open if unresolvable
|
|
196
|
+
let mode;
|
|
197
|
+
try {
|
|
198
|
+
mode = loadPermissionMode(configPath);
|
|
199
|
+
} catch {
|
|
200
|
+
process.exit(0);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Only enforce in explore mode. All other modes: allow.
|
|
204
|
+
if (mode !== 'explore') {
|
|
205
|
+
process.exit(0);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Classify the operation
|
|
209
|
+
let operation;
|
|
210
|
+
try {
|
|
211
|
+
operation = classifyOperation(toolName, toolInput);
|
|
212
|
+
} catch {
|
|
213
|
+
process.exit(0); // fail-open
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// In explore mode, only reads are allowed
|
|
217
|
+
if (operation === 'read') {
|
|
218
|
+
process.exit(0);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Build context for the block message
|
|
222
|
+
let detail = '';
|
|
223
|
+
if (toolName === 'Bash' && toolInput.command) {
|
|
224
|
+
const cmd = toolInput.command;
|
|
225
|
+
detail = `\nCommand: ${cmd.length > 120 ? cmd.slice(0, 120) + '...' : cmd}`;
|
|
226
|
+
} else if (toolInput.file_path) {
|
|
227
|
+
detail = `\nFile: ${toolInput.file_path}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// BLOCK — exit 2
|
|
231
|
+
process.stderr.write(
|
|
232
|
+
`\nPERMISSION-MODE BLOCK [Explore / Read-Only]\n` +
|
|
233
|
+
`Tool: ${toolName} Operation: ${operation}${detail}\n` +
|
|
234
|
+
`\n` +
|
|
235
|
+
`The project is in Explore (read-only) mode. Write, Edit, and destructive\n` +
|
|
236
|
+
`Bash commands are not allowed in this mode.\n` +
|
|
237
|
+
`\n` +
|
|
238
|
+
`To enable writes, change the permission mode:\n` +
|
|
239
|
+
` *mode ask — confirm before each change\n` +
|
|
240
|
+
` *mode auto — full autonomy (no prompts)\n` +
|
|
241
|
+
`\n` +
|
|
242
|
+
`Or update .sinapse/config.yaml:\n` +
|
|
243
|
+
` permissions:\n` +
|
|
244
|
+
` mode: ask\n`,
|
|
245
|
+
);
|
|
246
|
+
process.exit(2);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
main();
|
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
* exit 0 → allow
|
|
11
11
|
* exit 2 → block (message shown to model via stderr)
|
|
12
12
|
*
|
|
13
|
-
*
|
|
13
|
+
* fail-CLOSED: if the scanner cannot load or stdin cannot be parsed, BLOCK
|
|
14
|
+
* (exit 2) rather than silently allowing an unscanned write.
|
|
15
|
+
*
|
|
16
|
+
* Detection logic is shared with the git pre-commit scanner via
|
|
17
|
+
* bin/utils/secret-scanner-core.js (20+ named patterns + Shannon-entropy
|
|
18
|
+
* backstop + placeholder allowlist + lockfile-hash allowlist + redaction).
|
|
14
19
|
*
|
|
15
20
|
* @module secret-scanning
|
|
16
21
|
*/
|
|
@@ -19,38 +24,23 @@ const fs = require('fs');
|
|
|
19
24
|
const path = require('path');
|
|
20
25
|
|
|
21
26
|
// ---------------------------------------------------------------------------
|
|
22
|
-
//
|
|
27
|
+
// Shared detection core (single source of truth for patterns + entropy)
|
|
23
28
|
// ---------------------------------------------------------------------------
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
// Private Keys
|
|
40
|
-
{ name: 'RSA Private Key', pattern: /-----BEGIN RSA PRIVATE KEY-----/ },
|
|
41
|
-
{ name: 'SSH Private Key', pattern: /-----BEGIN OPENSSH PRIVATE KEY-----/ },
|
|
42
|
-
{ name: 'PGP Private Key', pattern: /-----BEGIN PGP PRIVATE KEY BLOCK-----/ },
|
|
43
|
-
{ name: 'EC Private Key', pattern: /-----BEGIN EC PRIVATE KEY-----/ },
|
|
44
|
-
|
|
45
|
-
// Connection Strings
|
|
46
|
-
{ name: 'DB Connection String', pattern: /(?:postgres|mysql|mongodb|redis):\/\/[^:]+:[^@]+@[^/\s]+/i },
|
|
47
|
-
{ name: 'Supabase DB URL', pattern: /postgresql:\/\/postgres\.[A-Za-z0-9]+:[^@]+@/i },
|
|
48
|
-
|
|
49
|
-
// Generic Patterns (broader, lower confidence)
|
|
50
|
-
{ name: 'Hardcoded Password', pattern: /(?:password|passwd|pwd)\s*[=:]\s*['"][^'"]{8,}['"]/i },
|
|
51
|
-
{ name: 'Bearer Token', pattern: /[Bb]earer\s+[A-Za-z0-9_\-.]{20,}/ },
|
|
52
|
-
{ name: 'Basic Auth', pattern: /[Bb]asic\s+[A-Za-z0-9+/=]{20,}/ },
|
|
53
|
-
];
|
|
30
|
+
let core;
|
|
31
|
+
try {
|
|
32
|
+
// Hook lives at <root>/.claude/hooks/; core lives at <root>/bin/utils/.
|
|
33
|
+
core = require(path.join(__dirname, '..', '..', 'bin', 'utils', 'secret-scanner-core.js'));
|
|
34
|
+
} catch (err) {
|
|
35
|
+
// fail-CLOSED: cannot scan → do not allow the write.
|
|
36
|
+
process.stderr.write(
|
|
37
|
+
'\nSECRET SCANNING BLOCK: scanner failed to load (fail-closed).\n' +
|
|
38
|
+
String(err && err.message ? err.message : err) + '\n',
|
|
39
|
+
);
|
|
40
|
+
process.exit(2);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { scanContent } = core;
|
|
54
44
|
|
|
55
45
|
/** Files that are expected to contain secret-like patterns */
|
|
56
46
|
const EXEMPT_PATHS = [
|
|
@@ -94,14 +84,9 @@ function isScannable(rel) {
|
|
|
94
84
|
return SCANNABLE_EXTENSIONS.some((ext) => rel.endsWith(ext));
|
|
95
85
|
}
|
|
96
86
|
|
|
97
|
-
function scanForSecrets(content) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (pattern.test(content)) {
|
|
101
|
-
findings.push(name);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return findings;
|
|
87
|
+
function scanForSecrets(content, filePath) {
|
|
88
|
+
// Delegates to the shared, hardened core. Returns redacted findings.
|
|
89
|
+
return scanContent(content, { filePath: filePath || '' });
|
|
105
90
|
}
|
|
106
91
|
|
|
107
92
|
// ---------------------------------------------------------------------------
|
|
@@ -113,7 +98,9 @@ function main() {
|
|
|
113
98
|
try {
|
|
114
99
|
input = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
115
100
|
} catch {
|
|
116
|
-
|
|
101
|
+
// fail-CLOSED: unparseable hook input → block rather than allow blindly.
|
|
102
|
+
process.stderr.write('\nSECRET SCANNING BLOCK: could not parse hook input (fail-closed).\n');
|
|
103
|
+
process.exit(2);
|
|
117
104
|
}
|
|
118
105
|
|
|
119
106
|
const toolName = input.tool_name || '';
|
|
@@ -135,14 +122,18 @@ function main() {
|
|
|
135
122
|
const content = toolInput.content || toolInput.new_string || '';
|
|
136
123
|
if (!content) process.exit(0);
|
|
137
124
|
|
|
138
|
-
const findings = scanForSecrets(content);
|
|
125
|
+
const findings = scanForSecrets(content, rel);
|
|
139
126
|
if (findings.length === 0) process.exit(0);
|
|
140
127
|
|
|
141
|
-
// BLOCK
|
|
128
|
+
// BLOCK — secrets are reported REDACTED (the core never returns raw values).
|
|
129
|
+
const lines = findings.map((f) => {
|
|
130
|
+
const ent = f.entropy ? ` (entropy ${f.entropy})` : '';
|
|
131
|
+
return ` - ${f.name}: ${f.redacted}${ent}`;
|
|
132
|
+
});
|
|
142
133
|
process.stderr.write(
|
|
143
134
|
`\nSECRET SCANNING BLOCK: Potential secrets detected!\n` +
|
|
144
135
|
`File: ${rel}\n` +
|
|
145
|
-
`Found
|
|
136
|
+
`Found:\n${lines.join('\n')}\n` +
|
|
146
137
|
`\n` +
|
|
147
138
|
`DO NOT commit secrets to code. Instead:\n` +
|
|
148
139
|
` - Use environment variables (.env) for local dev\n` +
|
|
@@ -39,10 +39,32 @@ function readStdin() {
|
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/** Write to stdout robustly across real and mocked (Jest) streams. */
|
|
43
|
+
function writeStdout(output) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
let settled = false;
|
|
46
|
+
const finish = (err) => {
|
|
47
|
+
if (settled) return;
|
|
48
|
+
settled = true;
|
|
49
|
+
if (err) reject(err); else resolve();
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
const flushed = process.stdout.write(output, (err) => finish(err));
|
|
53
|
+
if (flushed) setImmediate(() => finish());
|
|
54
|
+
else if (typeof process.stdout.once === 'function') process.stdout.once('drain', () => finish());
|
|
55
|
+
} catch (err) {
|
|
56
|
+
finish(err);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
42
61
|
/** Main hook execution pipeline. */
|
|
43
62
|
async function main() {
|
|
44
63
|
const input = await readStdin();
|
|
45
64
|
const runtime = resolveHookRuntime(input);
|
|
65
|
+
// Silent exit (empty stdout = valid "no context") when no runtime is
|
|
66
|
+
// resolvable — missing cwd or missing `.synapse/`. Only NON-empty output
|
|
67
|
+
// must conform to the hook schema.
|
|
46
68
|
if (!runtime) return;
|
|
47
69
|
|
|
48
70
|
const result = await runtime.engine.process(input.prompt, runtime.session);
|
|
@@ -62,29 +84,7 @@ async function main() {
|
|
|
62
84
|
}
|
|
63
85
|
|
|
64
86
|
const output = JSON.stringify(buildHookOutput(result.xml));
|
|
65
|
-
|
|
66
|
-
// Write output robustly across real process.stdout and mocked Jest streams.
|
|
67
|
-
// Some mocks return boolean but never invoke callback; handle both patterns.
|
|
68
|
-
await new Promise((resolve, reject) => {
|
|
69
|
-
let settled = false;
|
|
70
|
-
const finish = (err) => {
|
|
71
|
-
if (settled) return;
|
|
72
|
-
settled = true;
|
|
73
|
-
if (err) reject(err);
|
|
74
|
-
else resolve();
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
const flushed = process.stdout.write(output, (err) => finish(err));
|
|
79
|
-
if (flushed) {
|
|
80
|
-
setImmediate(() => finish());
|
|
81
|
-
} else if (typeof process.stdout.once === 'function') {
|
|
82
|
-
process.stdout.once('drain', () => finish());
|
|
83
|
-
}
|
|
84
|
-
} catch (err) {
|
|
85
|
-
finish(err);
|
|
86
|
-
}
|
|
87
|
-
});
|
|
87
|
+
await writeStdout(output);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/** Entry point runner — lets Node exit naturally after stdout flush. */
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* telemetry-post-tool.cjs
|
|
4
|
+
* PostToolUse hook — accumulates per-session DORA counters into a state file.
|
|
5
|
+
*
|
|
6
|
+
* FAIL-OPEN TOTAL: any error is swallowed; hook always exits 0 so it never
|
|
7
|
+
* blocks the main Claude Code flow.
|
|
8
|
+
*
|
|
9
|
+
* State path: <projectRoot>/.sinapse/telemetry/sessions/<sessionId>.json
|
|
10
|
+
* Written by: this hook (PostToolUse)
|
|
11
|
+
* Read+cleared by: telemetry-stop.cjs (Stop)
|
|
12
|
+
*
|
|
13
|
+
* Registered in HOOK_EVENT_MAP as: PostToolUse, no matcher, timeout 3.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
|
|
22
|
+
// ─── constants ───────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
const FILE_WRITE_TOOLS = new Set(['Write', 'Edit', 'NotebookEdit']);
|
|
25
|
+
|
|
26
|
+
// ─── helpers ─────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
function resolveProjectRoot() {
|
|
29
|
+
// CLAUDE_PROJECT_DIR is injected by Claude Code into hook environment
|
|
30
|
+
const fromEnv = process.env.CLAUDE_PROJECT_DIR || process.env.SINAPSE_PROJECT_DIR;
|
|
31
|
+
if (fromEnv) return fromEnv;
|
|
32
|
+
// Fallback: cwd at the time the hook is spawned
|
|
33
|
+
return process.cwd();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveSessionId(data) {
|
|
37
|
+
return (
|
|
38
|
+
process.env.CLAUDE_SESSION_ID ||
|
|
39
|
+
process.env.CLAUDE_CODE_SESSION_ID ||
|
|
40
|
+
data.session_id ||
|
|
41
|
+
data.sessionId ||
|
|
42
|
+
'unknown'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function sessionStatePath(projectRoot, sessionId) {
|
|
47
|
+
return path.join(projectRoot, '.sinapse', 'telemetry', 'sessions', `${sessionId}.json`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readState(statePath) {
|
|
51
|
+
try {
|
|
52
|
+
const raw = fs.readFileSync(statePath, 'utf8');
|
|
53
|
+
return JSON.parse(raw);
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function writeState(statePath, state) {
|
|
60
|
+
try {
|
|
61
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
62
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf8');
|
|
63
|
+
} catch {
|
|
64
|
+
// fail-open: ignore write errors
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── main ────────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
function main() {
|
|
71
|
+
try {
|
|
72
|
+
// Read event payload from stdin (Claude Code injects it as JSON)
|
|
73
|
+
let raw = '';
|
|
74
|
+
try {
|
|
75
|
+
raw = fs.readFileSync('/dev/stdin', 'utf8');
|
|
76
|
+
} catch {
|
|
77
|
+
// On Windows there's no /dev/stdin — read from fd 0
|
|
78
|
+
try {
|
|
79
|
+
const buf = Buffer.alloc(64 * 1024);
|
|
80
|
+
let totalBytes = 0;
|
|
81
|
+
let bytesRead = 0;
|
|
82
|
+
// Read synchronously until EOF
|
|
83
|
+
while (true) {
|
|
84
|
+
try {
|
|
85
|
+
bytesRead = fs.readSync(0, buf, totalBytes, buf.length - totalBytes, null);
|
|
86
|
+
if (bytesRead === 0) break;
|
|
87
|
+
totalBytes += bytesRead;
|
|
88
|
+
} catch {
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
raw = buf.slice(0, totalBytes).toString('utf8');
|
|
93
|
+
} catch {
|
|
94
|
+
raw = '{}';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const data = (() => { try { return JSON.parse(raw); } catch { return {}; } })();
|
|
99
|
+
|
|
100
|
+
const projectRoot = resolveProjectRoot();
|
|
101
|
+
const sessionId = resolveSessionId(data);
|
|
102
|
+
const toolName = data.tool_name || data.toolName || '';
|
|
103
|
+
const statePath = sessionStatePath(projectRoot, sessionId);
|
|
104
|
+
|
|
105
|
+
// Read existing state or bootstrap it
|
|
106
|
+
const existing = readState(statePath);
|
|
107
|
+
const state = existing || {
|
|
108
|
+
startedAt: Date.now(),
|
|
109
|
+
toolExecutions: 0,
|
|
110
|
+
filesModified: 0,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Increment counters
|
|
114
|
+
state.toolExecutions += 1;
|
|
115
|
+
if (FILE_WRITE_TOOLS.has(toolName)) {
|
|
116
|
+
state.filesModified += 1;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
writeState(statePath, state);
|
|
120
|
+
} catch {
|
|
121
|
+
// fail-open: swallow any top-level error
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Always exit 0 — observability hooks are never fail-closed
|
|
125
|
+
process.exit(0);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
main();
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* telemetry-stop.cjs
|
|
4
|
+
* Stop hook — reads per-session DORA state and appends a session-end line
|
|
5
|
+
* to the JSONL sink, then cleans up the state file.
|
|
6
|
+
*
|
|
7
|
+
* FAIL-OPEN TOTAL: any error is swallowed; hook always exits 0.
|
|
8
|
+
*
|
|
9
|
+
* Sink path: <projectRoot>/.sinapse/telemetry/gate-decisions.jsonl
|
|
10
|
+
* State path: <projectRoot>/.sinapse/telemetry/sessions/<sessionId>.json
|
|
11
|
+
*
|
|
12
|
+
* Registered in HOOK_EVENT_MAP as: Stop, no matcher, timeout 5.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
// ─── helpers ─────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
function resolveProjectRoot() {
|
|
23
|
+
const fromEnv = process.env.CLAUDE_PROJECT_DIR || process.env.SINAPSE_PROJECT_DIR;
|
|
24
|
+
if (fromEnv) return fromEnv;
|
|
25
|
+
return process.cwd();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveSessionId(data) {
|
|
29
|
+
return (
|
|
30
|
+
process.env.CLAUDE_SESSION_ID ||
|
|
31
|
+
process.env.CLAUDE_CODE_SESSION_ID ||
|
|
32
|
+
data.session_id ||
|
|
33
|
+
data.sessionId ||
|
|
34
|
+
'unknown'
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sinkPath(projectRoot) {
|
|
39
|
+
return path.join(projectRoot, '.sinapse', 'telemetry', 'gate-decisions.jsonl');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sessionStatePath(projectRoot, sessionId) {
|
|
43
|
+
return path.join(projectRoot, '.sinapse', 'telemetry', 'sessions', `${sessionId}.json`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function appendLine(filePath, record) {
|
|
47
|
+
try {
|
|
48
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
49
|
+
fs.appendFileSync(filePath, JSON.stringify(record) + '\n', 'utf8');
|
|
50
|
+
} catch {
|
|
51
|
+
// fail-open: ignore write errors
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readState(statePath) {
|
|
56
|
+
try {
|
|
57
|
+
const raw = fs.readFileSync(statePath, 'utf8');
|
|
58
|
+
return JSON.parse(raw);
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function deleteState(statePath) {
|
|
65
|
+
try {
|
|
66
|
+
fs.unlinkSync(statePath);
|
|
67
|
+
} catch {
|
|
68
|
+
// fail-open
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── main ────────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
function main() {
|
|
75
|
+
try {
|
|
76
|
+
// Read event payload from stdin
|
|
77
|
+
let raw = '';
|
|
78
|
+
try {
|
|
79
|
+
raw = fs.readFileSync('/dev/stdin', 'utf8');
|
|
80
|
+
} catch {
|
|
81
|
+
try {
|
|
82
|
+
const buf = Buffer.alloc(64 * 1024);
|
|
83
|
+
let totalBytes = 0;
|
|
84
|
+
let bytesRead = 0;
|
|
85
|
+
while (true) {
|
|
86
|
+
try {
|
|
87
|
+
bytesRead = fs.readSync(0, buf, totalBytes, buf.length - totalBytes, null);
|
|
88
|
+
if (bytesRead === 0) break;
|
|
89
|
+
totalBytes += bytesRead;
|
|
90
|
+
} catch {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
raw = buf.slice(0, totalBytes).toString('utf8');
|
|
95
|
+
} catch {
|
|
96
|
+
raw = '{}';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const data = (() => { try { return JSON.parse(raw); } catch { return {}; } })();
|
|
101
|
+
|
|
102
|
+
const projectRoot = resolveProjectRoot();
|
|
103
|
+
const sessionId = resolveSessionId(data);
|
|
104
|
+
const statePath = sessionStatePath(projectRoot, sessionId);
|
|
105
|
+
const state = readState(statePath);
|
|
106
|
+
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
const startedAt = state ? state.startedAt : now;
|
|
109
|
+
|
|
110
|
+
// Build DORA session-end record with OTel GenAI field names
|
|
111
|
+
const record = {
|
|
112
|
+
timestamp: new Date(now).toISOString(),
|
|
113
|
+
'gen_ai.operation.name': 'session.end',
|
|
114
|
+
'gen_ai.request.model': 'sinapse-hooks',
|
|
115
|
+
'gen_ai.usage.input_tokens': 0,
|
|
116
|
+
'gen_ai.usage.output_tokens': 0,
|
|
117
|
+
sessionId,
|
|
118
|
+
'dora.toolExecutions': state ? state.toolExecutions : 0,
|
|
119
|
+
'dora.filesModified': state ? state.filesModified : 0,
|
|
120
|
+
'dora.sessionDurationMs': now - startedAt,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
appendLine(sinkPath(projectRoot), record);
|
|
124
|
+
deleteState(statePath);
|
|
125
|
+
} catch {
|
|
126
|
+
// fail-open: swallow any top-level error
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
process.exit(0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
main();
|