@nomos-arc/arc 0.1.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.
Files changed (160) hide show
  1. package/.claude/settings.local.json +10 -0
  2. package/.nomos-config.json +5 -0
  3. package/CLAUDE.md +108 -0
  4. package/LICENSE +190 -0
  5. package/README.md +569 -0
  6. package/dist/cli.js +21120 -0
  7. package/docs/auth/googel_plan.yaml +1093 -0
  8. package/docs/auth/google_task.md +235 -0
  9. package/docs/auth/hardened_blueprint.yaml +1658 -0
  10. package/docs/auth/red_team_report.yaml +336 -0
  11. package/docs/auth/session_state.yaml +162 -0
  12. package/docs/certificate/cer_enhance_plan.md +605 -0
  13. package/docs/certificate/certificate_report.md +338 -0
  14. package/docs/dev_overview.md +419 -0
  15. package/docs/feature_assessment.md +156 -0
  16. package/docs/how_it_works.md +78 -0
  17. package/docs/infrastructure/map.md +867 -0
  18. package/docs/init/master_plan.md +3581 -0
  19. package/docs/init/red_team_report.md +215 -0
  20. package/docs/init/report_phase_1a.md +304 -0
  21. package/docs/integrity-gate/enhance_drift.md +703 -0
  22. package/docs/integrity-gate/overview.md +108 -0
  23. package/docs/management/manger-task.md +99 -0
  24. package/docs/management/scafffold.md +76 -0
  25. package/docs/map/ATOMIC_BLUEPRINT.md +1349 -0
  26. package/docs/map/RED_TEAM_REPORT.md +159 -0
  27. package/docs/map/map_task.md +147 -0
  28. package/docs/map/semantic_graph_task.md +792 -0
  29. package/docs/map/semantic_master_plan.md +705 -0
  30. package/docs/phase7/TEAM_RED.md +249 -0
  31. package/docs/phase7/plan.md +1682 -0
  32. package/docs/phase7/task.md +275 -0
  33. package/docs/prompts/USAGE.md +312 -0
  34. package/docs/prompts/architect.md +165 -0
  35. package/docs/prompts/executer.md +190 -0
  36. package/docs/prompts/hardener.md +190 -0
  37. package/docs/prompts/red_team.md +146 -0
  38. package/docs/verification/goveranance-overview.md +396 -0
  39. package/docs/verification/governance-overview.md +245 -0
  40. package/docs/verification/verification-arc-ar.md +560 -0
  41. package/docs/verification/verification-architecture.md +560 -0
  42. package/docs/very_next.md +52 -0
  43. package/docs/whitepaper.md +89 -0
  44. package/overview.md +1469 -0
  45. package/package.json +63 -0
  46. package/src/adapters/__tests__/git.test.ts +296 -0
  47. package/src/adapters/__tests__/stdio.test.ts +70 -0
  48. package/src/adapters/git.ts +226 -0
  49. package/src/adapters/pty.ts +159 -0
  50. package/src/adapters/stdio.ts +113 -0
  51. package/src/cli.ts +83 -0
  52. package/src/commands/apply.ts +47 -0
  53. package/src/commands/auth.ts +301 -0
  54. package/src/commands/certificate.ts +89 -0
  55. package/src/commands/discard.ts +24 -0
  56. package/src/commands/drift.ts +116 -0
  57. package/src/commands/index.ts +78 -0
  58. package/src/commands/init.ts +121 -0
  59. package/src/commands/list.ts +75 -0
  60. package/src/commands/map.ts +55 -0
  61. package/src/commands/plan.ts +30 -0
  62. package/src/commands/review.ts +58 -0
  63. package/src/commands/run.ts +63 -0
  64. package/src/commands/search.ts +147 -0
  65. package/src/commands/show.ts +63 -0
  66. package/src/commands/status.ts +59 -0
  67. package/src/core/__tests__/budget.test.ts +213 -0
  68. package/src/core/__tests__/certificate.test.ts +385 -0
  69. package/src/core/__tests__/config.test.ts +191 -0
  70. package/src/core/__tests__/preflight.test.ts +24 -0
  71. package/src/core/__tests__/prompt.test.ts +358 -0
  72. package/src/core/__tests__/review.test.ts +161 -0
  73. package/src/core/__tests__/state.test.ts +362 -0
  74. package/src/core/auth/__tests__/manager.test.ts +166 -0
  75. package/src/core/auth/__tests__/server.test.ts +220 -0
  76. package/src/core/auth/gcp-projects.ts +160 -0
  77. package/src/core/auth/manager.ts +114 -0
  78. package/src/core/auth/server.ts +141 -0
  79. package/src/core/budget.ts +119 -0
  80. package/src/core/certificate.ts +502 -0
  81. package/src/core/config.ts +212 -0
  82. package/src/core/errors.ts +54 -0
  83. package/src/core/factory.ts +49 -0
  84. package/src/core/graph/__tests__/builder.test.ts +272 -0
  85. package/src/core/graph/__tests__/contract-writer.test.ts +175 -0
  86. package/src/core/graph/__tests__/enricher.test.ts +299 -0
  87. package/src/core/graph/__tests__/parser.test.ts +200 -0
  88. package/src/core/graph/__tests__/pipeline.test.ts +202 -0
  89. package/src/core/graph/__tests__/renderer.test.ts +128 -0
  90. package/src/core/graph/__tests__/resolver.test.ts +185 -0
  91. package/src/core/graph/__tests__/scanner.test.ts +231 -0
  92. package/src/core/graph/__tests__/show.test.ts +134 -0
  93. package/src/core/graph/builder.ts +303 -0
  94. package/src/core/graph/constraints.ts +94 -0
  95. package/src/core/graph/contract-writer.ts +93 -0
  96. package/src/core/graph/drift/__tests__/classifier.test.ts +215 -0
  97. package/src/core/graph/drift/__tests__/comparator.test.ts +335 -0
  98. package/src/core/graph/drift/__tests__/drift.test.ts +453 -0
  99. package/src/core/graph/drift/__tests__/reporter.test.ts +203 -0
  100. package/src/core/graph/drift/classifier.ts +165 -0
  101. package/src/core/graph/drift/comparator.ts +205 -0
  102. package/src/core/graph/drift/reporter.ts +77 -0
  103. package/src/core/graph/enricher.ts +251 -0
  104. package/src/core/graph/grammar-paths.ts +30 -0
  105. package/src/core/graph/html-template.ts +493 -0
  106. package/src/core/graph/map-schema.ts +137 -0
  107. package/src/core/graph/parser.ts +336 -0
  108. package/src/core/graph/pipeline.ts +209 -0
  109. package/src/core/graph/renderer.ts +92 -0
  110. package/src/core/graph/resolver.ts +195 -0
  111. package/src/core/graph/scanner.ts +145 -0
  112. package/src/core/logger.ts +46 -0
  113. package/src/core/orchestrator.ts +792 -0
  114. package/src/core/plan-file-manager.ts +66 -0
  115. package/src/core/preflight.ts +64 -0
  116. package/src/core/prompt.ts +173 -0
  117. package/src/core/review.ts +95 -0
  118. package/src/core/state.ts +294 -0
  119. package/src/core/worktree-coordinator.ts +77 -0
  120. package/src/search/__tests__/chunk-extractor.test.ts +339 -0
  121. package/src/search/__tests__/embedder-auth.test.ts +124 -0
  122. package/src/search/__tests__/embedder.test.ts +267 -0
  123. package/src/search/__tests__/graph-enricher.test.ts +178 -0
  124. package/src/search/__tests__/indexer.test.ts +518 -0
  125. package/src/search/__tests__/integration.test.ts +649 -0
  126. package/src/search/__tests__/query-engine.test.ts +334 -0
  127. package/src/search/__tests__/similarity.test.ts +78 -0
  128. package/src/search/__tests__/vector-store.test.ts +281 -0
  129. package/src/search/chunk-extractor.ts +167 -0
  130. package/src/search/embedder.ts +209 -0
  131. package/src/search/graph-enricher.ts +95 -0
  132. package/src/search/indexer.ts +483 -0
  133. package/src/search/lexical-searcher.ts +190 -0
  134. package/src/search/query-engine.ts +225 -0
  135. package/src/search/vector-store.ts +311 -0
  136. package/src/types/index.ts +572 -0
  137. package/src/utils/__tests__/ansi.test.ts +54 -0
  138. package/src/utils/__tests__/frontmatter.test.ts +79 -0
  139. package/src/utils/__tests__/sanitize.test.ts +229 -0
  140. package/src/utils/ansi.ts +19 -0
  141. package/src/utils/context.ts +44 -0
  142. package/src/utils/frontmatter.ts +27 -0
  143. package/src/utils/sanitize.ts +78 -0
  144. package/test/e2e/lifecycle.test.ts +330 -0
  145. package/test/fixtures/mock-planner-hang.ts +5 -0
  146. package/test/fixtures/mock-planner.ts +26 -0
  147. package/test/fixtures/mock-reviewer-bad.ts +8 -0
  148. package/test/fixtures/mock-reviewer-retry.ts +34 -0
  149. package/test/fixtures/mock-reviewer.ts +18 -0
  150. package/test/fixtures/sample-project/src/circular-a.ts +6 -0
  151. package/test/fixtures/sample-project/src/circular-b.ts +6 -0
  152. package/test/fixtures/sample-project/src/config.ts +15 -0
  153. package/test/fixtures/sample-project/src/main.ts +19 -0
  154. package/test/fixtures/sample-project/src/services/product-service.ts +20 -0
  155. package/test/fixtures/sample-project/src/services/user-service.ts +18 -0
  156. package/test/fixtures/sample-project/src/types.ts +14 -0
  157. package/test/fixtures/sample-project/src/utils/index.ts +14 -0
  158. package/test/fixtures/sample-project/src/utils/validate.ts +12 -0
  159. package/tsconfig.json +20 -0
  160. package/vitest.config.ts +12 -0
@@ -0,0 +1,229 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ sanitizeByPatterns,
4
+ calculateEntropy,
5
+ detectHighEntropyStrings,
6
+ sanitizeForPty,
7
+ scanFileForSecrets,
8
+ sanitizeEnv,
9
+ } from '../sanitize.js';
10
+
11
+ // ── Pattern-based sanitization ───────────────────────────────────────────────
12
+
13
+ describe('sanitizeByPatterns', () => {
14
+ it('redacts API_KEY=sk-abc123 using a key=value pattern', () => {
15
+ const { output, matches } = sanitizeByPatterns(
16
+ 'config: API_KEY=sk-abc123 end',
17
+ ['API_KEY=[\\w-]+'],
18
+ );
19
+ expect(output).toBe('config: [REDACTED] end');
20
+ expect(matches).toContain('API_KEY=sk-abc123');
21
+ });
22
+
23
+ it('redacts Bearer token header', () => {
24
+ const { output, matches } = sanitizeByPatterns(
25
+ 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.payload.sig',
26
+ ['Bearer [A-Za-z0-9._-]+'],
27
+ );
28
+ expect(output).toContain('[REDACTED]');
29
+ expect(matches.length).toBeGreaterThan(0);
30
+ });
31
+
32
+ it('redacts PEM private key header', () => {
33
+ const input = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgk=\n-----END PRIVATE KEY-----';
34
+ const { output, matches } = sanitizeByPatterns(
35
+ input,
36
+ ['-----BEGIN PRIVATE KEY-----'],
37
+ );
38
+ expect(output).not.toContain('-----BEGIN PRIVATE KEY-----');
39
+ expect(matches).toHaveLength(1);
40
+ });
41
+
42
+ it('uses a custom label when provided', () => {
43
+ const { output } = sanitizeByPatterns('secret=abc', ['secret=[\\w]+'], '***');
44
+ expect(output).toBe('***');
45
+ });
46
+
47
+ it('returns empty matches array when no patterns match', () => {
48
+ const { output, matches } = sanitizeByPatterns('hello world', ['NOMATCH']);
49
+ expect(output).toBe('hello world');
50
+ expect(matches).toHaveLength(0);
51
+ });
52
+
53
+ it('handles multiple patterns independently', () => {
54
+ const { output, matches } = sanitizeByPatterns(
55
+ 'TOKEN=abc SECRET=xyz',
56
+ ['TOKEN=[\\w]+', 'SECRET=[\\w]+'],
57
+ );
58
+ expect(output).toBe('[REDACTED] [REDACTED]');
59
+ expect(matches).toHaveLength(2);
60
+ });
61
+ });
62
+
63
+ // ── Shannon entropy ───────────────────────────────────────────────────────────
64
+
65
+ describe('calculateEntropy', () => {
66
+ it('returns 0 for a single repeated character', () => {
67
+ expect(calculateEntropy('aaaa')).toBe(0);
68
+ });
69
+
70
+ it('returns a positive value for mixed characters', () => {
71
+ expect(calculateEntropy('abcd')).toBeGreaterThan(0);
72
+ });
73
+
74
+ it('returns higher entropy for more varied strings', () => {
75
+ const low = calculateEntropy('aaaaaaaabaaa');
76
+ const high = calculateEntropy('aAbBcCdDeEfF');
77
+ expect(high).toBeGreaterThan(low);
78
+ });
79
+ });
80
+
81
+ describe('detectHighEntropyStrings', () => {
82
+ it('flags a 36-char GitHub PAT (high entropy)', () => {
83
+ // ghp_ + 36 random alphanumeric chars = high entropy
84
+ const token = 'ghp_' + 'xK9mQ2rL5tP8wN3vB6hY0sU1dA4jE7fC'.slice(0, 32);
85
+ const results = detectHighEntropyStrings(token, 4.5);
86
+ expect(results.length).toBeGreaterThan(0);
87
+ });
88
+
89
+ it('ignores 32 identical characters (entropy = 0, below threshold)', () => {
90
+ const lowEntropy = 'a'.repeat(32);
91
+ const results = detectHighEntropyStrings(lowEntropy, 4.5);
92
+ expect(results).toHaveLength(0);
93
+ });
94
+
95
+ it('ignores strings shorter than 32 characters', () => {
96
+ const results = detectHighEntropyStrings('abcdefghij', 4.5);
97
+ expect(results).toHaveLength(0);
98
+ });
99
+
100
+ it('returns empty array for plain prose', () => {
101
+ const results = detectHighEntropyStrings('This is a normal sentence without tokens.', 4.5);
102
+ expect(results).toHaveLength(0);
103
+ });
104
+ });
105
+
106
+ // ── PTY prompt sanitization ──────────────────────────────────────────────────
107
+
108
+ describe('sanitizeForPty', () => {
109
+ it('strips C0 control characters (SOH, BEL, BS, VT)', () => {
110
+ expect(sanitizeForPty('a\x01b\x07c\x08d\x0Be')).toBe('abcde');
111
+ });
112
+
113
+ it('preserves newlines (\\n)', () => {
114
+ expect(sanitizeForPty('line1\nline2')).toBe('line1\nline2');
115
+ });
116
+
117
+ it('preserves tabs (\\t)', () => {
118
+ expect(sanitizeForPty('\tcol1\tcol2')).toBe('\tcol1\tcol2');
119
+ });
120
+
121
+ it('strips CSI ANSI sequences', () => {
122
+ expect(sanitizeForPty('\x1b[0mhello\x1b[32mworld\x1b[0m')).toBe('helloworld');
123
+ });
124
+
125
+ it('strips OSC sequences', () => {
126
+ expect(sanitizeForPty('\x1b]0;window title\x07prompt text')).toBe('prompt text');
127
+ });
128
+
129
+ it('passes plain text through unchanged', () => {
130
+ const plain = 'Plan: refactor the auth module to use JWT.';
131
+ expect(sanitizeForPty(plain)).toBe(plain);
132
+ });
133
+ });
134
+
135
+ // ── File secret scanning ─────────────────────────────────────────────────────
136
+
137
+ describe('scanFileForSecrets', () => {
138
+ it('returns matching pattern strings for .env content', () => {
139
+ const envContent = 'OPENAI_API_KEY=sk-abc123\nDATABASE_URL=postgres://user:pass@host/db';
140
+ const patterns = ['OPENAI_API_KEY', 'DATABASE_URL', 'GITHUB_TOKEN'];
141
+ const found = scanFileForSecrets(envContent, patterns);
142
+ expect(found).toContain('OPENAI_API_KEY');
143
+ expect(found).toContain('DATABASE_URL');
144
+ expect(found).not.toContain('GITHUB_TOKEN');
145
+ });
146
+
147
+ it('returns empty array when no secrets found', () => {
148
+ const safeContent = 'const x = 42;\nconst y = "hello";';
149
+ const patterns = ['OPENAI_API_KEY', 'DATABASE_URL'];
150
+ expect(scanFileForSecrets(safeContent, patterns)).toHaveLength(0);
151
+ });
152
+
153
+ it('returns all matching patterns, not just the first', () => {
154
+ const content = 'OPENAI_API_KEY=sk-x\nGITHUB_TOKEN=ghp_y';
155
+ const patterns = ['OPENAI_API_KEY', 'GITHUB_TOKEN'];
156
+ const found = scanFileForSecrets(content, patterns);
157
+ expect(found).toHaveLength(2);
158
+ });
159
+ });
160
+
161
+ // ── Environment variable sanitization ─────────────────────────────────────────
162
+
163
+ describe('sanitizeEnv', () => {
164
+ it('removes OPENAI_API_KEY from env', () => {
165
+ const env = { OPENAI_API_KEY: 'sk-abc', PATH: '/usr/bin' };
166
+ const result = sanitizeEnv(env, []);
167
+ expect(result['OPENAI_API_KEY']).toBeUndefined();
168
+ expect(result['PATH']).toBe('/usr/bin');
169
+ });
170
+
171
+ it('removes AWS_SECRET_ACCESS_KEY from env', () => {
172
+ const env = { AWS_SECRET_ACCESS_KEY: 'AKIAIOSFODNN7EXAMPLE', HOME: '/home/user' };
173
+ const result = sanitizeEnv(env, []);
174
+ expect(result['AWS_SECRET_ACCESS_KEY']).toBeUndefined();
175
+ expect(result['HOME']).toBe('/home/user');
176
+ });
177
+
178
+ it('keeps COLORTERM, PATH, HOME, LANG untouched', () => {
179
+ const env = {
180
+ COLORTERM: 'truecolor',
181
+ PATH: '/usr/local/bin:/usr/bin',
182
+ HOME: '/home/user',
183
+ LANG: 'en_US.UTF-8',
184
+ };
185
+ const result = sanitizeEnv(env, []);
186
+ expect(result['COLORTERM']).toBe('truecolor');
187
+ expect(result['PATH']).toBe('/usr/local/bin:/usr/bin');
188
+ expect(result['HOME']).toBe('/home/user');
189
+ expect(result['LANG']).toBe('en_US.UTF-8');
190
+ });
191
+
192
+ it('denylist removes CUSTOM_SECRET but keeps CUSTOM_VALUE', () => {
193
+ const env = {
194
+ CUSTOM_SECRET: 'topsecret',
195
+ CUSTOM_VALUE: 'not-sensitive',
196
+ };
197
+ const result = sanitizeEnv(env, ['CUSTOM_SECRET']);
198
+ expect(result['CUSTOM_SECRET']).toBeUndefined();
199
+ expect(result['CUSTOM_VALUE']).toBe('not-sensitive');
200
+ });
201
+
202
+ it('denylist matching is case-insensitive on key names', () => {
203
+ const env = { My_Api_Token: 'abc123', Other: 'keep' };
204
+ const result = sanitizeEnv(env, ['my_api_token']);
205
+ expect(result['My_Api_Token']).toBeUndefined();
206
+ expect(result['Other']).toBe('keep');
207
+ });
208
+
209
+ it('skips env entries with undefined values', () => {
210
+ const env: Record<string, string | undefined> = { PATH: '/usr/bin', MAYBE: undefined };
211
+ const result = sanitizeEnv(env, []);
212
+ expect(result['MAYBE']).toBeUndefined();
213
+ expect(result['PATH']).toBe('/usr/bin');
214
+ });
215
+
216
+ it('removes DATABASE_URL from env', () => {
217
+ const env = { DATABASE_URL: 'postgres://u:p@h/db', NODE_ENV: 'production' };
218
+ const result = sanitizeEnv(env, []);
219
+ expect(result['DATABASE_URL']).toBeUndefined();
220
+ expect(result['NODE_ENV']).toBe('production');
221
+ });
222
+
223
+ it('removes SSH_AUTH_SOCK from env', () => {
224
+ const env = { SSH_AUTH_SOCK: '/tmp/ssh-agent.sock', TERM: 'xterm-256color' };
225
+ const result = sanitizeEnv(env, []);
226
+ expect(result['SSH_AUTH_SOCK']).toBeUndefined();
227
+ expect(result['TERM']).toBe('xterm-256color');
228
+ });
229
+ });
@@ -0,0 +1,19 @@
1
+ /**
2
+ * stripAnsi — remove all ANSI / VT escape sequences from a string.
3
+ *
4
+ * Three-pass removal:
5
+ * 1. CSI sequences e.g. \x1b[0m, \x1b[38;5;200m, \x1b[2J
6
+ * 2. OSC sequences e.g. \x1b]0;title\x07
7
+ * 3. Remaining C0 control chars (excluding \n and \t which are meaningful in logs)
8
+ *
9
+ * Reused by: PtyAdapter, StdioAdapter, Logger file transport.
10
+ */
11
+ export function stripAnsi(input: string): string {
12
+ return input
13
+ // 1. CSI sequences: ESC [ … <final byte A-Za-z>
14
+ .replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
15
+ // 2. OSC sequences: ESC ] … BEL
16
+ .replace(/\x1b\][^\x07]*\x07/g, '')
17
+ // 3. Remaining C0 control chars except \n (\x0A) and \t (\x09)
18
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
19
+ }
@@ -0,0 +1,44 @@
1
+ import * as fs from 'fs';
2
+
3
+ /**
4
+ * Extracts the list of relative file paths that were changed in a git diff.
5
+ * Parses `--- a/<path>` lines, skipping `/dev/null` (new files have no "a" path).
6
+ * Returns unique relative paths only.
7
+ */
8
+ export function extractChangedFilePaths(diff: string): string[] {
9
+ const seen = new Set<string>();
10
+ const results: string[] = [];
11
+ for (const line of diff.split('\n')) {
12
+ const match = line.match(/^--- a\/(.+)$/);
13
+ if (match && match[1] !== '/dev/null') {
14
+ const filePath = match[1];
15
+ if (!seen.has(filePath)) {
16
+ seen.add(filePath);
17
+ results.push(filePath);
18
+ }
19
+ }
20
+ }
21
+ return results;
22
+ }
23
+
24
+ /**
25
+ * Escapes special regex characters in a string for safe use in grep patterns.
26
+ * RT2-5.1 fix: Used in full relative path import scan to prevent path separators
27
+ * and dots from being interpreted as regex metacharacters.
28
+ */
29
+ export function escapeRegex(str: string): string {
30
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
31
+ }
32
+
33
+ /**
34
+ * Reads the first 50 lines of a file for context injection snippets.
35
+ * Returns empty string if the file cannot be read.
36
+ */
37
+ export function readFirst50Lines(filePath: string): string {
38
+ try {
39
+ const content = fs.readFileSync(filePath, 'utf8');
40
+ return content.split('\n').slice(0, 50).join('\n');
41
+ } catch {
42
+ return '';
43
+ }
44
+ }
@@ -0,0 +1,27 @@
1
+ import * as fs from 'fs/promises';
2
+ import matter from 'gray-matter';
3
+ import { z } from 'zod';
4
+ import { NomosError } from '../core/errors.js';
5
+ import type { TaskFrontmatter } from '../types/index.js';
6
+
7
+ const TaskFrontmatterSchema = z.object({
8
+ title: z.string().min(1),
9
+ priority: z.enum(['high', 'medium', 'low']),
10
+ context_files: z.array(z.string()).optional(),
11
+ status: z.string().optional(),
12
+ });
13
+
14
+ export async function parseTaskFile(
15
+ filePath: string,
16
+ ): Promise<{ frontmatter: TaskFrontmatter; body: string }> {
17
+ const raw = await fs.readFile(filePath, 'utf8');
18
+ const parsed = matter(raw);
19
+ const result = TaskFrontmatterSchema.safeParse(parsed.data);
20
+ if (!result.success) {
21
+ throw new NomosError(
22
+ 'invalid_frontmatter',
23
+ `Invalid frontmatter in ${filePath}: ${result.error.message}`,
24
+ );
25
+ }
26
+ return { frontmatter: result.data as TaskFrontmatter, body: parsed.content };
27
+ }
@@ -0,0 +1,78 @@
1
+ // ── Pattern-based sanitization ───────────────────────────────────────────────
2
+ export function sanitizeByPatterns(
3
+ input: string,
4
+ patterns: string[],
5
+ label: string = '[REDACTED]',
6
+ ): { output: string; matches: string[] } {
7
+ const matches: string[] = [];
8
+ let output = input;
9
+ for (const pattern of patterns) {
10
+ const re = new RegExp(pattern, 'g');
11
+ output = output.replace(re, (match) => { matches.push(match); return label; });
12
+ }
13
+ return { output, matches };
14
+ }
15
+
16
+ // ── Shannon entropy ───────────────────────────────────────────────────────────
17
+ export function calculateEntropy(str: string): number {
18
+ const freq = new Map<string, number>();
19
+ for (const ch of str) freq.set(ch, (freq.get(ch) ?? 0) + 1);
20
+ let entropy = 0;
21
+ for (const count of freq.values()) {
22
+ const p = count / str.length;
23
+ entropy -= p * Math.log2(p);
24
+ }
25
+ return entropy;
26
+ }
27
+
28
+ export function detectHighEntropyStrings(input: string, threshold: number): string[] {
29
+ const candidates = input.match(/[a-zA-Z0-9_-]{32,}/g) ?? [];
30
+ return candidates.filter(s => calculateEntropy(s) >= threshold);
31
+ }
32
+
33
+ // ── PTY prompt sanitization ──────────────────────────────────────────────────
34
+ // Used to clean prompt content before it becomes a CLI argument.
35
+ // Even though the prompt is passed via -p flag (not PTY stdin), the content
36
+ // must be free of terminal escape sequences that could corrupt argument parsing.
37
+ export function sanitizeForPty(prompt: string): string {
38
+ // Order matters: strip full ANSI sequences first (they start with \x1b which is
39
+ // inside the C0 range). Stripping C0 first would eat the ESC byte and leave
40
+ // bracket artifacts like "[0m" in the output.
41
+ return prompt
42
+ .replace(/\x1b\[[0-9;]*[A-Za-z]/g, '') // CSI sequences
43
+ .replace(/\x1b\][^\x07]*\x07/g, '') // OSC sequences
44
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ''); // C0 control chars except \n \t
45
+ }
46
+
47
+ // ── File secret scanning ─────────────────────────────────────────────────────
48
+ // RT2-5.1 fix: Returns matching pattern strings (not just boolean) so the error
49
+ // message can tell the user WHICH patterns triggered. Empty array = no secrets found.
50
+ export function scanFileForSecrets(content: string, patterns: string[]): string[] {
51
+ return patterns.filter(p => new RegExp(p).test(content));
52
+ }
53
+
54
+ // ── Environment variable sanitization ─────────────────────────────────────────
55
+ // C3 fix: Match against env var NAMES only — never against 'key=value' strings.
56
+ // Matching 'key=value' causes false positives (e.g., pattern 'TOKEN' deletes 'COLORTERM').
57
+ const ALWAYS_DENY: RegExp[] = [
58
+ /^(ANTHROPIC|OPENAI|AWS|AZURE|GCP|GOOGLE|GITHUB|GITLAB|HUGGING_?FACE)_.*?(KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)/i,
59
+ /^NOMOS_.*?SECRET/i,
60
+ /^(DATABASE_URL|REDIS_URL|MONGO_URI|DB_PASSWORD)$/i,
61
+ /^(SSH_AUTH_SOCK|GPG_TTY)$/i,
62
+ ];
63
+
64
+ export function sanitizeEnv(
65
+ env: Record<string, string | undefined>,
66
+ denylist: string[],
67
+ ): Record<string, string> {
68
+ const compiledDeny = denylist.map(p => new RegExp(p, 'i'));
69
+ const result: Record<string, string> = {};
70
+ for (const [key, value] of Object.entries(env)) {
71
+ if (value === undefined) continue;
72
+ const denied =
73
+ ALWAYS_DENY.some(re => re.test(key)) ||
74
+ compiledDeny.some(re => re.test(key));
75
+ if (!denied) result[key] = value;
76
+ }
77
+ return result;
78
+ }