codehere 0.8.0 → 1.0.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/README.md +37 -184
- package/dist/application/use-cases/check-claimed-tests.js +81 -0
- package/dist/application/use-cases/detect-test-weakening.js +237 -0
- package/dist/application/use-cases/rerun-tests.js +329 -0
- package/dist/index.js +74 -1004
- package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -265
- package/dist/presentation/cli/commands/check-command.js +251 -0
- package/dist/presentation/cli/commands/help-command.js +22 -286
- package/dist/ui.js +19 -438
- package/dist/utils/version.js +0 -12
- package/package.json +18 -27
- package/dist/application/use-cases/domain-trust.js +0 -110
- package/dist/application/use-cases/generate-final-report.js +0 -134
- package/dist/application/use-cases/generate-narration.js +0 -128
- package/dist/application/use-cases/generate-thread-title.js +0 -178
- package/dist/application/use-cases/narrator-prompt-template.js +0 -158
- package/dist/application/use-cases/thread-export.js +0 -155
- package/dist/application/use-cases/verify-claim.js +0 -274
- package/dist/application/use-cases/write-pass.js +0 -277
- package/dist/domain/entities/task.js +0 -61
- package/dist/domain/services/codebase-detector.js +0 -91
- package/dist/error-handler.js +0 -251
- package/dist/infrastructure/completion/bash-completion.js +0 -25
- package/dist/infrastructure/completion/fish-completion.js +0 -22
- package/dist/infrastructure/completion/zsh-completion.js +0 -30
- package/dist/infrastructure/config/config-manager.js +0 -173
- package/dist/infrastructure/config/product-context.js +0 -67
- package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
- package/dist/infrastructure/context/adapters/figma-source.js +0 -163
- package/dist/infrastructure/context/adapters/github-issue-source.js +0 -159
- package/dist/infrastructure/context/adapters/linear-source.js +0 -190
- package/dist/infrastructure/context/context-resolver.js +0 -144
- package/dist/infrastructure/context/context-source.js +0 -114
- package/dist/infrastructure/context/oauth-flow.js +0 -207
- package/dist/infrastructure/context/oauth-store.js +0 -119
- package/dist/infrastructure/context/registry-bootstrap.js +0 -99
- package/dist/infrastructure/context/write-pass-resolver.js +0 -106
- package/dist/infrastructure/cost/budget-config.js +0 -113
- package/dist/infrastructure/cost/cost-router.js +0 -104
- package/dist/infrastructure/cost/spend-aggregator.js +0 -147
- package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -888
- package/dist/infrastructure/hooks/hook-mode.js +0 -71
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -43
- package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
- package/dist/infrastructure/hooks/session-analyzer.js +0 -177
- package/dist/infrastructure/observability/execution-tracer.js +0 -227
- package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
- package/dist/infrastructure/orchestration/agent-installer.js +0 -101
- package/dist/infrastructure/orchestration/agents-config-seed.js +0 -56
- package/dist/infrastructure/orchestration/aider-adapter.js +0 -447
- package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
- package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -478
- package/dist/infrastructure/orchestration/codex-adapter.js +0 -538
- package/dist/infrastructure/orchestration/conductor-adapter.js +0 -377
- package/dist/infrastructure/orchestration/context-handoff.js +0 -144
- package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
- package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
- package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
- package/dist/infrastructure/orchestration/failover-config.js +0 -77
- package/dist/infrastructure/orchestration/failover-policy.js +0 -106
- package/dist/infrastructure/orchestration/gemini-adapter.js +0 -583
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -558
- package/dist/infrastructure/orchestration/graphify-context.js +0 -254
- package/dist/infrastructure/orchestration/memory-injector.js +0 -380
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -233
- package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
- package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
- package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
- package/dist/infrastructure/orchestration/opencode-adapter.js +0 -510
- package/dist/infrastructure/orchestration/progress-aggregator.js +0 -689
- package/dist/infrastructure/orchestration/readiness.js +0 -103
- package/dist/infrastructure/orchestration/session-manager.js +0 -1091
- package/dist/infrastructure/orchestration/session-persistence.js +0 -258
- package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -46
- package/dist/infrastructure/protocols/cloud-sync.js +0 -207
- package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
- package/dist/infrastructure/protocols/mcp-server-impl.js +0 -2853
- package/dist/infrastructure/protocols/probe-port.js +0 -36
- package/dist/infrastructure/protocols/thread-phase.js +0 -105
- package/dist/infrastructure/protocols/ui-page-v2.js +0 -4418
- package/dist/infrastructure/protocols/ui-page.js +0 -2377
- package/dist/infrastructure/protocols/verify-page.js +0 -983
- package/dist/infrastructure/security/file-collection.js +0 -118
- package/dist/infrastructure/security/input-validator.js +0 -152
- package/dist/infrastructure/security/permissions-config.js +0 -234
- package/dist/infrastructure/security/trust-scorer.js +0 -657
- package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
- package/dist/infrastructure/storage/embeddings-path.js +0 -37
- package/dist/infrastructure/storage/project-thread-migration.js +0 -237
- package/dist/infrastructure/storage/project-thread-mirror.js +0 -524
- package/dist/infrastructure/storage/project-thread-reader.js +0 -172
- package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
- package/dist/infrastructure/storage/task-repository.js +0 -206
- package/dist/monitoring.js +0 -212
- package/dist/presentation/cli/commands/audit-command.js +0 -126
- package/dist/presentation/cli/commands/budget-command.js +0 -79
- package/dist/presentation/cli/commands/config-command.js +0 -125
- package/dist/presentation/cli/commands/context-source-command.js +0 -198
- package/dist/presentation/cli/commands/hook-command.js +0 -265
- package/dist/presentation/cli/commands/init-command.js +0 -578
- package/dist/presentation/cli/commands/memory-command.js +0 -1031
- package/dist/presentation/cli/commands/narrator-command.js +0 -101
- package/dist/presentation/cli/commands/permission-command.js +0 -116
- package/dist/presentation/cli/commands/protocols-builder.js +0 -254
- package/dist/presentation/cli/commands/protocols-command.js +0 -84
- package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
- package/dist/presentation/cli/commands/report-command.js +0 -387
- package/dist/presentation/cli/commands/scan-tool-command.js +0 -94
- package/dist/presentation/cli/commands/setup-command.js +0 -307
- package/dist/presentation/cli/commands/status-command.js +0 -661
- package/dist/presentation/cli/commands/thread-command.js +0 -288
- package/dist/presentation/cli/commands/trust-command.js +0 -306
- package/dist/presentation/cli/commands/verify-command.js +0 -233
- package/dist/presentation/cli/error-display.js +0 -336
- package/dist/presentation/cli/repl.js +0 -797
- package/dist/presentation/cli/task-deliverable.js +0 -127
- package/dist/testing/cassette.js +0 -125
- package/dist/testing/replay-adapter.js +0 -166
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Canonical file-collection rules for the trust scanner.
|
|
3
|
-
*
|
|
4
|
-
* Historic bug: IGNORE_DIRS and the test-file filter lived in three places
|
|
5
|
-
* (trust-command, mcp-helpers, report-command) and drifted. The v0.3.0-alpha
|
|
6
|
-
* shipreadiness fix patched two of them; report-command kept the old rules
|
|
7
|
-
* and produced different finding counts than `trust` for the same directory.
|
|
8
|
-
*
|
|
9
|
-
* Single source of truth now — every scanner entry point imports from here.
|
|
10
|
-
*/
|
|
11
|
-
import { readdirSync, statSync } from 'fs';
|
|
12
|
-
import { join, extname } from 'path';
|
|
13
|
-
export const CODE_EXTENSIONS = new Set([
|
|
14
|
-
'.ts',
|
|
15
|
-
'.tsx',
|
|
16
|
-
'.js',
|
|
17
|
-
'.jsx',
|
|
18
|
-
'.mjs',
|
|
19
|
-
'.cjs',
|
|
20
|
-
'.py',
|
|
21
|
-
'.rb',
|
|
22
|
-
'.go',
|
|
23
|
-
'.java',
|
|
24
|
-
'.cs',
|
|
25
|
-
'.php',
|
|
26
|
-
'.sh',
|
|
27
|
-
'.bash',
|
|
28
|
-
'.zsh',
|
|
29
|
-
'.sql',
|
|
30
|
-
'.html',
|
|
31
|
-
'.vue',
|
|
32
|
-
'.svelte',
|
|
33
|
-
]);
|
|
34
|
-
export const IGNORE_DIRS = new Set([
|
|
35
|
-
// JS/TS
|
|
36
|
-
'node_modules',
|
|
37
|
-
'dist',
|
|
38
|
-
'build',
|
|
39
|
-
'.next',
|
|
40
|
-
'.nuxt',
|
|
41
|
-
'.svelte-kit',
|
|
42
|
-
'.turbo',
|
|
43
|
-
'.parcel-cache',
|
|
44
|
-
// Python
|
|
45
|
-
'venv',
|
|
46
|
-
'__pycache__',
|
|
47
|
-
'.pytest_cache',
|
|
48
|
-
'.ruff_cache',
|
|
49
|
-
'.mypy_cache',
|
|
50
|
-
'.tox',
|
|
51
|
-
'__pypackages__',
|
|
52
|
-
// Rust / Go / PHP / Java / .NET
|
|
53
|
-
'target',
|
|
54
|
-
'vendor',
|
|
55
|
-
'.gradle',
|
|
56
|
-
'obj',
|
|
57
|
-
// VCS / tooling / coverage / codehere
|
|
58
|
-
'.git',
|
|
59
|
-
'.hg',
|
|
60
|
-
'.svn',
|
|
61
|
-
'coverage',
|
|
62
|
-
'.cache',
|
|
63
|
-
'.codehere',
|
|
64
|
-
'.codehere-cache',
|
|
65
|
-
// Cloned reference repos used for dogfooding / comparison — not the user's code.
|
|
66
|
-
// Codehere convention; mirrors how `.git` is hidden tooling-data, not source.
|
|
67
|
-
'.oss-repos',
|
|
68
|
-
// Test fixtures / mocks (intentionally-vulnerable code used as scanner inputs)
|
|
69
|
-
'fixtures',
|
|
70
|
-
'test-fixtures',
|
|
71
|
-
'__fixtures__',
|
|
72
|
-
'__mocks__',
|
|
73
|
-
]);
|
|
74
|
-
// Test files contain deliberate vulnerability patterns as scanner inputs —
|
|
75
|
-
// SQL injection strings, hardcoded "credentials" etc. — that inflate the
|
|
76
|
-
// finding count without being real production risks. Skip by default.
|
|
77
|
-
export const TEST_FILE_RE = /\.(test|spec)\.[cm]?[jt]sx?$/;
|
|
78
|
-
export function shouldIgnoreDir(name) {
|
|
79
|
-
if (IGNORE_DIRS.has(name))
|
|
80
|
-
return true;
|
|
81
|
-
// Python virtualenv variants — .venv, .venv311, .venv-py3, venv3.11, env-py3
|
|
82
|
-
if (name.startsWith('.venv') || name.startsWith('venv'))
|
|
83
|
-
return true;
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
export function shouldIgnoreFile(name) {
|
|
87
|
-
return TEST_FILE_RE.test(name);
|
|
88
|
-
}
|
|
89
|
-
export function isCodeFile(name) {
|
|
90
|
-
return CODE_EXTENSIONS.has(extname(name)) && !shouldIgnoreFile(name);
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Walk a directory and return paths of code files worth scanning.
|
|
94
|
-
* If `target` is a single file, it is returned as-is (explicit targets
|
|
95
|
-
* are never excluded — user pointed at it on purpose).
|
|
96
|
-
*/
|
|
97
|
-
export function collectCodeFiles(target) {
|
|
98
|
-
const stat = statSync(target);
|
|
99
|
-
if (stat.isFile())
|
|
100
|
-
return [target];
|
|
101
|
-
const files = [];
|
|
102
|
-
function walk(dir) {
|
|
103
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
104
|
-
if (shouldIgnoreDir(entry.name))
|
|
105
|
-
continue;
|
|
106
|
-
const full = join(dir, entry.name);
|
|
107
|
-
if (entry.isDirectory()) {
|
|
108
|
-
walk(full);
|
|
109
|
-
}
|
|
110
|
-
else if (isCodeFile(entry.name)) {
|
|
111
|
-
files.push(full);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
walk(target);
|
|
116
|
-
return files;
|
|
117
|
-
}
|
|
118
|
-
//# sourceMappingURL=file-collection.js.map
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Infrastructure: Input Validator
|
|
3
|
-
* Enterprise-grade input validation and sanitization
|
|
4
|
-
*
|
|
5
|
-
* Clean Architecture: Infrastructure Layer
|
|
6
|
-
* Security: Prevents injection attacks, validates user input
|
|
7
|
-
*/
|
|
8
|
-
/**
|
|
9
|
-
* Validate file path to prevent directory traversal
|
|
10
|
-
*/
|
|
11
|
-
export function validateFilePath(filepath) {
|
|
12
|
-
const errors = [];
|
|
13
|
-
// Check for directory traversal attempts
|
|
14
|
-
if (filepath.includes('..') || filepath.includes('~')) {
|
|
15
|
-
errors.push('Directory traversal detected in file path');
|
|
16
|
-
}
|
|
17
|
-
// Check for absolute paths (should be relative)
|
|
18
|
-
if (filepath.startsWith('/') || /^[A-Z]:\\/.test(filepath)) {
|
|
19
|
-
errors.push('Absolute paths not allowed');
|
|
20
|
-
}
|
|
21
|
-
// Check for null bytes
|
|
22
|
-
if (filepath.includes('\0')) {
|
|
23
|
-
errors.push('Null bytes not allowed in file path');
|
|
24
|
-
}
|
|
25
|
-
// Check for dangerous characters
|
|
26
|
-
const dangerousChars = /[<>:"|?*\x00-\x1f]/;
|
|
27
|
-
if (dangerousChars.test(filepath)) {
|
|
28
|
-
errors.push('Invalid characters in file path');
|
|
29
|
-
}
|
|
30
|
-
// Sanitize path
|
|
31
|
-
let sanitized = filepath
|
|
32
|
-
.replace(/\.\./g, '') // Remove ..
|
|
33
|
-
.replace(/~/g, '') // Remove ~
|
|
34
|
-
.replace(/\0/g, '') // Remove null bytes
|
|
35
|
-
.replace(/[<>:"|?*\x00-\x1f]/g, '_'); // Replace dangerous chars
|
|
36
|
-
// Normalize path separators
|
|
37
|
-
sanitized = sanitized.replace(/\\/g, '/');
|
|
38
|
-
return {
|
|
39
|
-
valid: errors.length === 0,
|
|
40
|
-
errors,
|
|
41
|
-
sanitized: errors.length > 0 ? sanitized : undefined,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Validate query string to prevent injection
|
|
46
|
-
*/
|
|
47
|
-
export function validateQuery(query) {
|
|
48
|
-
const errors = [];
|
|
49
|
-
// Check for null bytes
|
|
50
|
-
if (query.includes('\0')) {
|
|
51
|
-
errors.push('Null bytes not allowed in query');
|
|
52
|
-
}
|
|
53
|
-
// Check for command injection patterns
|
|
54
|
-
const commandInjectionPatterns = [
|
|
55
|
-
/[;&|`$(){}[\]]/, // Shell metacharacters
|
|
56
|
-
/<script/i, // XSS attempts
|
|
57
|
-
/javascript:/i, // JavaScript protocol
|
|
58
|
-
/on\w+\s*=/i, // Event handlers
|
|
59
|
-
];
|
|
60
|
-
for (const pattern of commandInjectionPatterns) {
|
|
61
|
-
if (pattern.test(query)) {
|
|
62
|
-
errors.push('Potentially dangerous characters detected in query');
|
|
63
|
-
break;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
// Sanitize query
|
|
67
|
-
let sanitized = query
|
|
68
|
-
.replace(/\0/g, '') // Remove null bytes
|
|
69
|
-
.replace(/[;&|`$(){}[\]]/g, '') // Remove shell metacharacters
|
|
70
|
-
.replace(/<script/gi, '<script') // Escape script tags
|
|
71
|
-
.replace(/javascript:/gi, '') // Remove javascript protocol
|
|
72
|
-
.replace(/on\w+\s*=/gi, ''); // Remove event handlers
|
|
73
|
-
// Trim and limit length
|
|
74
|
-
sanitized = sanitized.trim();
|
|
75
|
-
const MAX_QUERY_LENGTH = 10000; // Reasonable limit
|
|
76
|
-
if (sanitized.length > MAX_QUERY_LENGTH) {
|
|
77
|
-
errors.push(`Query exceeds maximum length of ${MAX_QUERY_LENGTH} characters`);
|
|
78
|
-
sanitized = sanitized.substring(0, MAX_QUERY_LENGTH);
|
|
79
|
-
}
|
|
80
|
-
return {
|
|
81
|
-
valid: errors.length === 0,
|
|
82
|
-
errors,
|
|
83
|
-
sanitized: errors.length > 0 ? sanitized : undefined,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Validate API key format
|
|
88
|
-
*/
|
|
89
|
-
export function validateAPIKey(apiKey) {
|
|
90
|
-
const errors = [];
|
|
91
|
-
if (!apiKey || apiKey.trim().length === 0) {
|
|
92
|
-
errors.push('API key is required');
|
|
93
|
-
}
|
|
94
|
-
// Check minimum length
|
|
95
|
-
if (apiKey.length < 10) {
|
|
96
|
-
errors.push('API key is too short');
|
|
97
|
-
}
|
|
98
|
-
// Check for null bytes
|
|
99
|
-
if (apiKey.includes('\0')) {
|
|
100
|
-
errors.push('Null bytes not allowed in API key');
|
|
101
|
-
}
|
|
102
|
-
// Check for whitespace (API keys shouldn't have spaces)
|
|
103
|
-
if (apiKey !== apiKey.trim()) {
|
|
104
|
-
errors.push('API key should not have leading or trailing whitespace');
|
|
105
|
-
}
|
|
106
|
-
return {
|
|
107
|
-
valid: errors.length === 0,
|
|
108
|
-
errors,
|
|
109
|
-
sanitized: errors.length > 0 ? apiKey.trim() : undefined,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Validate file content before processing
|
|
114
|
-
*/
|
|
115
|
-
export function validateFileContent(content, maxSize = 10 * 1024 * 1024) {
|
|
116
|
-
const errors = [];
|
|
117
|
-
// Check size
|
|
118
|
-
const sizeInBytes = new Blob([content]).size;
|
|
119
|
-
if (sizeInBytes > maxSize) {
|
|
120
|
-
errors.push(`File content exceeds maximum size of ${maxSize} bytes`);
|
|
121
|
-
}
|
|
122
|
-
// Check for null bytes (shouldn't be in text files)
|
|
123
|
-
if (content.includes('\0')) {
|
|
124
|
-
errors.push('Null bytes detected in file content');
|
|
125
|
-
}
|
|
126
|
-
return {
|
|
127
|
-
valid: errors.length === 0,
|
|
128
|
-
errors,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Sanitize user input for logging (prevent sensitive data leakage)
|
|
133
|
-
*/
|
|
134
|
-
export function sanitizeForLogging(input) {
|
|
135
|
-
// Remove potential secrets (API keys, tokens, etc.)
|
|
136
|
-
let sanitized = input
|
|
137
|
-
// Remove API keys (Cohere format: alphanumeric, 40+ chars)
|
|
138
|
-
.replace(/[A-Za-z0-9]{40,}/g, '[REDACTED]')
|
|
139
|
-
// Remove email addresses
|
|
140
|
-
.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '[EMAIL_REDACTED]')
|
|
141
|
-
// Remove potential tokens
|
|
142
|
-
.replace(/token[=:]\s*[A-Za-z0-9_-]+/gi, 'token=[REDACTED]')
|
|
143
|
-
.replace(/password[=:]\s*[^\s]+/gi, 'password=[REDACTED]')
|
|
144
|
-
.replace(/secret[=:]\s*[^\s]+/gi, 'secret=[REDACTED]');
|
|
145
|
-
// Limit length for logging
|
|
146
|
-
const MAX_LOG_LENGTH = 1000;
|
|
147
|
-
if (sanitized.length > MAX_LOG_LENGTH) {
|
|
148
|
-
sanitized = sanitized.substring(0, MAX_LOG_LENGTH) + '...[TRUNCATED]';
|
|
149
|
-
}
|
|
150
|
-
return sanitized;
|
|
151
|
-
}
|
|
152
|
-
//# sourceMappingURL=input-validator.js.map
|
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Per-cwd permissions config — P18 slice 1 primitive.
|
|
3
|
-
*
|
|
4
|
-
* Reads `.codehere/permissions.yaml` from a project's cwd and lets the
|
|
5
|
-
* trust-scorer SKIP findings whose matched text fits an allow pattern.
|
|
6
|
-
*
|
|
7
|
-
* Use case: codehere blocks `rm -rf node_modules` because the pattern
|
|
8
|
-
* matches `rm -rf /`. The user knows it's safe in THIS cwd. They run
|
|
9
|
-
* `codehere permission allow "rm -rf node_modules"` (slice 2 CLI) and
|
|
10
|
-
* future blocks for that exact pattern in this cwd are skipped.
|
|
11
|
-
*
|
|
12
|
-
* Schema (.codehere/permissions.yaml):
|
|
13
|
-
*
|
|
14
|
-
* allow:
|
|
15
|
-
* - pattern: "rm -rf node_modules"
|
|
16
|
-
* reason: "OK to nuke deps in this repo"
|
|
17
|
-
* addedAt: 2026-05-08T20:00:00Z
|
|
18
|
-
* - pattern: "ANTHROPIC_API_KEY"
|
|
19
|
-
* reason: "Doc explains env vars; not actually leaking"
|
|
20
|
-
*
|
|
21
|
-
* Allow patterns are STRINGS not regex — exact substring match against
|
|
22
|
-
* the audit-finding `matched` field. Keeps the schema simple +
|
|
23
|
-
* non-technical-user friendly. Regex support is a v0.7 follow-up.
|
|
24
|
-
*
|
|
25
|
-
* Per claim_user_persona HIGH: the YAML is human-readable; non-
|
|
26
|
-
* technical users can edit it directly. Per overconfidence-guard:
|
|
27
|
-
* we DON'T claim the allow-list is "secure" — it's user CONSENT,
|
|
28
|
-
* not codehere certification.
|
|
29
|
-
*/
|
|
30
|
-
import { existsSync, readFileSync } from 'fs';
|
|
31
|
-
import { join } from 'path';
|
|
32
|
-
const EMPTY = { allow: [], deny: [] };
|
|
33
|
-
/**
|
|
34
|
-
* Load .codehere/permissions.yaml from a cwd. Returns empty config if
|
|
35
|
-
* the file doesn't exist or fails to parse. Defensive: never throws —
|
|
36
|
-
* caller treats absence as "no overrides", which is the safest default
|
|
37
|
-
* (the scanner blocks normally).
|
|
38
|
-
*/
|
|
39
|
-
export function loadPermissions(cwd) {
|
|
40
|
-
if (!cwd || typeof cwd !== 'string')
|
|
41
|
-
return EMPTY;
|
|
42
|
-
const file = join(cwd, '.codehere', 'permissions.yaml');
|
|
43
|
-
if (!existsSync(file))
|
|
44
|
-
return EMPTY;
|
|
45
|
-
let raw;
|
|
46
|
-
try {
|
|
47
|
-
raw = readFileSync(file, 'utf-8');
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
return EMPTY;
|
|
51
|
-
}
|
|
52
|
-
return parsePermissionsYaml(raw);
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Tiny YAML subset parser — only handles the shape this file uses
|
|
56
|
-
* (top-level allow/deny lists of {pattern, reason, addedAt} objects).
|
|
57
|
-
* No external dep; the codehere repo already avoids the `yaml` package
|
|
58
|
-
* for read paths to keep cold-start fast. Format errors yield empty
|
|
59
|
-
* config (same posture as missing file).
|
|
60
|
-
*/
|
|
61
|
-
export function parsePermissionsYaml(raw) {
|
|
62
|
-
const cfg = { allow: [], deny: [] };
|
|
63
|
-
let bucket = null;
|
|
64
|
-
let pending = null;
|
|
65
|
-
for (const line of raw.split('\n')) {
|
|
66
|
-
const stripped = line.replace(/#.*$/, '').trimEnd();
|
|
67
|
-
if (!stripped.trim())
|
|
68
|
-
continue;
|
|
69
|
-
// Top-level key: "allow:" or "deny:"
|
|
70
|
-
const top = stripped.match(/^(allow|deny)\s*:\s*$/);
|
|
71
|
-
if (top) {
|
|
72
|
-
// Flush any pending entry into the previous bucket
|
|
73
|
-
if (pending && bucket && pending.pattern)
|
|
74
|
-
cfg[bucket].push(pending);
|
|
75
|
-
pending = null;
|
|
76
|
-
bucket = top[1];
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (!bucket)
|
|
80
|
-
continue;
|
|
81
|
-
// List entry start: " - pattern: ..."
|
|
82
|
-
const listStart = stripped.match(/^\s*-\s+pattern\s*:\s*(.+)$/);
|
|
83
|
-
if (listStart) {
|
|
84
|
-
if (pending && pending.pattern)
|
|
85
|
-
cfg[bucket].push(pending);
|
|
86
|
-
pending = { pattern: unquote(listStart[1]) };
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
// Continuation field on a pending entry
|
|
90
|
-
const cont = stripped.match(/^\s+(reason|addedAt)\s*:\s*(.+)$/);
|
|
91
|
-
if (cont && pending) {
|
|
92
|
-
const key = cont[1];
|
|
93
|
-
pending[key] = unquote(cont[2]);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
if (pending && bucket && pending.pattern)
|
|
97
|
-
cfg[bucket].push(pending);
|
|
98
|
-
return cfg;
|
|
99
|
-
}
|
|
100
|
-
function unquote(s) {
|
|
101
|
-
const t = s.trim();
|
|
102
|
-
if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'"))) {
|
|
103
|
-
return t.slice(1, -1);
|
|
104
|
-
}
|
|
105
|
-
return t;
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Check whether a finding's matched text is allow-listed in this cwd.
|
|
109
|
-
* Substring match — exact-text patterns are simpler for non-technical
|
|
110
|
-
* users than regex. Returns the matching entry (for audit-trail
|
|
111
|
-
* provenance) or null if no match.
|
|
112
|
-
*/
|
|
113
|
-
export function isAllowed(matchedText, cwd) {
|
|
114
|
-
const cfg = loadPermissions(cwd);
|
|
115
|
-
for (const entry of cfg.allow) {
|
|
116
|
-
if (entry.pattern && matchedText.includes(entry.pattern))
|
|
117
|
-
return entry;
|
|
118
|
-
}
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Filter a findings list against the allow-list for a cwd. Returns
|
|
123
|
-
* the findings that survived (i.e., were NOT allow-listed). Used by
|
|
124
|
-
* the trust-scorer / pre-prompt-scan to drop findings that the user
|
|
125
|
-
* has explicitly approved.
|
|
126
|
-
*
|
|
127
|
-
* Side-effect-free: the allow-listed findings are dropped from the
|
|
128
|
-
* caller's perspective. The audit log writer can still record the
|
|
129
|
-
* full finding list IF the caller wants (then mark which were
|
|
130
|
-
* allow-listed). This function is the simple "what should we still
|
|
131
|
-
* block on" filter.
|
|
132
|
-
*/
|
|
133
|
-
export function filterAllowed(findings, cwd) {
|
|
134
|
-
if (findings.length === 0)
|
|
135
|
-
return { surviving: [], allowed: [] };
|
|
136
|
-
const cfg = loadPermissions(cwd);
|
|
137
|
-
if (cfg.allow.length === 0)
|
|
138
|
-
return { surviving: findings, allowed: [] };
|
|
139
|
-
const surviving = [];
|
|
140
|
-
const allowed = [];
|
|
141
|
-
for (const f of findings) {
|
|
142
|
-
const hit = cfg.allow.find((e) => e.pattern && f.matched.includes(e.pattern));
|
|
143
|
-
if (hit)
|
|
144
|
-
allowed.push({ finding: f, entry: hit });
|
|
145
|
-
else
|
|
146
|
-
surviving.push(f);
|
|
147
|
-
}
|
|
148
|
-
return { surviving, allowed };
|
|
149
|
-
}
|
|
150
|
-
// ── Write surface (P18 slice 3) ─────────────────────────────────────────────
|
|
151
|
-
// Shared by the CLI (`codehere permission allow ...`) and the HTTP write
|
|
152
|
-
// endpoint (POST /api/permissions). Single source of truth for the
|
|
153
|
-
// idempotent-add semantics + the YAML serializer.
|
|
154
|
-
import { mkdirSync, writeFileSync } from 'fs';
|
|
155
|
-
/**
|
|
156
|
-
* Idempotent add — if the pattern already exists in the bucket, returns
|
|
157
|
-
* status='already_present' with the existing entry. Otherwise appends +
|
|
158
|
-
* writes the file. Writes via Write tool semantics (overwrites the
|
|
159
|
-
* whole YAML each time; entries are O(N) so re-serialization is cheap).
|
|
160
|
-
*/
|
|
161
|
-
export function addPermissionEntry(input) {
|
|
162
|
-
const bucket = input.bucket ?? 'allow';
|
|
163
|
-
const cfg = loadPermissions(input.cwd);
|
|
164
|
-
const list = cfg[bucket];
|
|
165
|
-
const existing = list.find((e) => e.pattern === input.pattern);
|
|
166
|
-
if (existing) {
|
|
167
|
-
return {
|
|
168
|
-
status: 'already_present',
|
|
169
|
-
bucket,
|
|
170
|
-
entry: existing,
|
|
171
|
-
filePath: join(input.cwd, '.codehere', 'permissions.yaml'),
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
const entry = {
|
|
175
|
-
pattern: input.pattern,
|
|
176
|
-
...(input.reason ? { reason: input.reason } : {}),
|
|
177
|
-
addedAt: new Date().toISOString(),
|
|
178
|
-
};
|
|
179
|
-
cfg[bucket].push(entry);
|
|
180
|
-
const filePath = writePermissionsConfig(input.cwd, cfg);
|
|
181
|
-
return { status: 'added', bucket, entry, filePath };
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Idempotent remove — drops every entry whose pattern matches across
|
|
185
|
-
* BOTH buckets. Returns count of removed entries.
|
|
186
|
-
*/
|
|
187
|
-
export function removePermissionEntry(cwd, pattern) {
|
|
188
|
-
const cfg = loadPermissions(cwd);
|
|
189
|
-
const before = cfg.allow.length + cfg.deny.length;
|
|
190
|
-
cfg.allow = cfg.allow.filter((e) => e.pattern !== pattern);
|
|
191
|
-
cfg.deny = cfg.deny.filter((e) => e.pattern !== pattern);
|
|
192
|
-
const after = cfg.allow.length + cfg.deny.length;
|
|
193
|
-
const removed = before - after;
|
|
194
|
-
if (removed > 0)
|
|
195
|
-
writePermissionsConfig(cwd, cfg);
|
|
196
|
-
return { removed, filePath: join(cwd, '.codehere', 'permissions.yaml') };
|
|
197
|
-
}
|
|
198
|
-
function writePermissionsConfig(cwd, cfg) {
|
|
199
|
-
const dir = join(cwd, '.codehere');
|
|
200
|
-
if (!existsSync(dir))
|
|
201
|
-
mkdirSync(dir, { recursive: true });
|
|
202
|
-
const filePath = join(dir, 'permissions.yaml');
|
|
203
|
-
writeFileSync(filePath, serializePermissionsYaml(cfg), 'utf-8');
|
|
204
|
-
return filePath;
|
|
205
|
-
}
|
|
206
|
-
export function serializePermissionsYaml(cfg) {
|
|
207
|
-
const out = [];
|
|
208
|
-
out.push('# codehere permissions — per-folder allow/deny list');
|
|
209
|
-
out.push('# See: codehere permission --help');
|
|
210
|
-
out.push('');
|
|
211
|
-
for (const bucket of ['allow', 'deny']) {
|
|
212
|
-
out.push(`${bucket}:`);
|
|
213
|
-
if (cfg[bucket].length === 0) {
|
|
214
|
-
out.push(' []');
|
|
215
|
-
}
|
|
216
|
-
else {
|
|
217
|
-
for (const e of cfg[bucket]) {
|
|
218
|
-
out.push(` - pattern: ${quoteYaml(e.pattern)}`);
|
|
219
|
-
if (e.reason)
|
|
220
|
-
out.push(` reason: ${quoteYaml(e.reason)}`);
|
|
221
|
-
if (e.addedAt)
|
|
222
|
-
out.push(` addedAt: ${quoteYaml(e.addedAt)}`);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
out.push('');
|
|
226
|
-
}
|
|
227
|
-
return out.join('\n');
|
|
228
|
-
}
|
|
229
|
-
function quoteYaml(s) {
|
|
230
|
-
if (s.includes('"') && !s.includes("'"))
|
|
231
|
-
return `'${s}'`;
|
|
232
|
-
return `"${s.replace(/"/g, '\\"')}"`;
|
|
233
|
-
}
|
|
234
|
-
//# sourceMappingURL=permissions-config.js.map
|