@securecode-ai/mcp 0.5.5 → 0.5.7

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 (186) hide show
  1. package/README.md +52 -12
  2. package/dist/api/types.d.ts +7 -0
  3. package/dist/approval/auditLog.d.ts +1 -1
  4. package/dist/approval/auditLog.js +11 -3
  5. package/dist/approval/auditLog.js.map +1 -1
  6. package/dist/approval/broker.d.ts +7 -2
  7. package/dist/approval/broker.js +239 -110
  8. package/dist/approval/broker.js.map +1 -1
  9. package/dist/approval/policy.d.ts +10 -0
  10. package/dist/approval/policy.js +104 -0
  11. package/dist/approval/policy.js.map +1 -0
  12. package/dist/approval/types.d.ts +11 -2
  13. package/dist/approval/types.js +10 -1
  14. package/dist/approval/types.js.map +1 -1
  15. package/dist/attack/agentScanExecutor.d.ts +35 -1
  16. package/dist/attack/agentScanExecutor.js +405 -76
  17. package/dist/attack/agentScanExecutor.js.map +1 -1
  18. package/dist/attack/agentScanLoop.d.ts +20 -0
  19. package/dist/attack/agentScanLoop.js +1264 -60
  20. package/dist/attack/agentScanLoop.js.map +1 -1
  21. package/dist/attack/agentScanProtocol.d.ts +382 -6
  22. package/dist/attack/agentScanProtocol.js +110 -4
  23. package/dist/attack/agentScanProtocol.js.map +1 -1
  24. package/dist/attack/agentTrace.d.ts +73 -0
  25. package/dist/attack/agentTrace.js +228 -0
  26. package/dist/attack/agentTrace.js.map +1 -0
  27. package/dist/attack/architectureScoutExecutor.d.ts +18 -0
  28. package/dist/attack/architectureScoutExecutor.js +211 -0
  29. package/dist/attack/architectureScoutExecutor.js.map +1 -0
  30. package/dist/attack/architectureScoutLoop.d.ts +36 -0
  31. package/dist/attack/architectureScoutLoop.js +403 -0
  32. package/dist/attack/architectureScoutLoop.js.map +1 -0
  33. package/dist/attack/architectureScoutProtocol.d.ts +209 -0
  34. package/dist/attack/architectureScoutProtocol.js +47 -0
  35. package/dist/attack/architectureScoutProtocol.js.map +1 -0
  36. package/dist/attack/candidateStore.d.ts +95 -0
  37. package/dist/attack/candidateStore.js +231 -0
  38. package/dist/attack/candidateStore.js.map +1 -0
  39. package/dist/attack/evidenceLedger.d.ts +67 -0
  40. package/dist/attack/evidenceLedger.js +192 -0
  41. package/dist/attack/evidenceLedger.js.map +1 -0
  42. package/dist/attack/finishGate.d.ts +62 -0
  43. package/dist/attack/finishGate.js +209 -0
  44. package/dist/attack/finishGate.js.map +1 -0
  45. package/dist/attack/fixCodeMerge.d.ts +27 -0
  46. package/dist/attack/fixCodeMerge.js +42 -0
  47. package/dist/attack/fixCodeMerge.js.map +1 -0
  48. package/dist/attack/fixVerifyLoop.d.ts +55 -0
  49. package/dist/attack/fixVerifyLoop.js +187 -0
  50. package/dist/attack/fixVerifyLoop.js.map +1 -0
  51. package/dist/attack/investigationProfiles.d.ts +36 -0
  52. package/dist/attack/investigationProfiles.js +144 -0
  53. package/dist/attack/investigationProfiles.js.map +1 -0
  54. package/dist/attack/investigationState.d.ts +227 -0
  55. package/dist/attack/investigationState.js +666 -0
  56. package/dist/attack/investigationState.js.map +1 -0
  57. package/dist/attack/mutationOperators.d.ts +22 -0
  58. package/dist/attack/mutationOperators.js +170 -0
  59. package/dist/attack/mutationOperators.js.map +1 -0
  60. package/dist/attack/mutationTest.d.ts +33 -0
  61. package/dist/attack/mutationTest.js +110 -0
  62. package/dist/attack/mutationTest.js.map +1 -0
  63. package/dist/attack/proofGate.d.ts +20 -0
  64. package/dist/attack/proofGate.js +104 -0
  65. package/dist/attack/proofGate.js.map +1 -0
  66. package/dist/attack/proofTypes.d.ts +57 -0
  67. package/dist/attack/proofTypes.js +32 -0
  68. package/dist/attack/proofTypes.js.map +1 -0
  69. package/dist/attack/protocolValidator.d.ts +50 -0
  70. package/dist/attack/protocolValidator.js +427 -0
  71. package/dist/attack/protocolValidator.js.map +1 -0
  72. package/dist/attack/qualityMetrics.d.ts +133 -0
  73. package/dist/attack/qualityMetrics.js +226 -0
  74. package/dist/attack/qualityMetrics.js.map +1 -0
  75. package/dist/attack/scanScheduler.d.ts +52 -0
  76. package/dist/attack/scanScheduler.js +265 -0
  77. package/dist/attack/scanScheduler.js.map +1 -0
  78. package/dist/attack/scanState.d.ts +58 -0
  79. package/dist/attack/scanState.js +102 -0
  80. package/dist/attack/scanState.js.map +1 -0
  81. package/dist/attack/searchIntent.d.ts +16 -0
  82. package/dist/attack/searchIntent.js +57 -0
  83. package/dist/attack/searchIntent.js.map +1 -0
  84. package/dist/attack/verifyLoop.d.ts +43 -0
  85. package/dist/attack/verifyLoop.js +314 -14
  86. package/dist/attack/verifyLoop.js.map +1 -1
  87. package/dist/attack/workItem.d.ts +57 -0
  88. package/dist/attack/workItem.js +225 -0
  89. package/dist/attack/workItem.js.map +1 -0
  90. package/dist/audit/findingReviewQueue.d.ts +109 -0
  91. package/dist/audit/findingReviewQueue.js +335 -0
  92. package/dist/audit/findingReviewQueue.js.map +1 -0
  93. package/dist/audit/scanAuditLog.d.ts +160 -0
  94. package/dist/audit/scanAuditLog.js +404 -0
  95. package/dist/audit/scanAuditLog.js.map +1 -0
  96. package/dist/dependency/dependencyChecker.js +35 -9
  97. package/dist/dependency/dependencyChecker.js.map +1 -1
  98. package/dist/dependency/exploitPriority.d.ts +33 -0
  99. package/dist/dependency/exploitPriority.js +78 -0
  100. package/dist/dependency/exploitPriority.js.map +1 -0
  101. package/dist/dependency/finding.d.ts +16 -0
  102. package/dist/dependency/osvClient.js +2 -0
  103. package/dist/dependency/osvClient.js.map +1 -1
  104. package/dist/dependency/types.d.ts +12 -1
  105. package/dist/mcp/server.js +13 -2
  106. package/dist/mcp/server.js.map +1 -1
  107. package/dist/mcp/tools.d.ts +1 -0
  108. package/dist/mcp/tools.js +127 -6
  109. package/dist/mcp/tools.js.map +1 -1
  110. package/dist/project-map/agentMemory.d.ts +83 -1
  111. package/dist/project-map/agentMemory.js +197 -7
  112. package/dist/project-map/agentMemory.js.map +1 -1
  113. package/dist/project-map/architectureContext.d.ts +202 -0
  114. package/dist/project-map/architectureContext.js +421 -0
  115. package/dist/project-map/architectureContext.js.map +1 -0
  116. package/dist/project-map/blastRadius.d.ts +54 -0
  117. package/dist/project-map/blastRadius.js +196 -0
  118. package/dist/project-map/blastRadius.js.map +1 -0
  119. package/dist/project-map/callGraphExtractor.d.ts +13 -0
  120. package/dist/project-map/callGraphExtractor.js +222 -0
  121. package/dist/project-map/callGraphExtractor.js.map +1 -0
  122. package/dist/project-map/capabilityRegistry.d.ts +46 -0
  123. package/dist/project-map/capabilityRegistry.js +91 -0
  124. package/dist/project-map/capabilityRegistry.js.map +1 -0
  125. package/dist/project-map/findTests.d.ts +29 -0
  126. package/dist/project-map/findTests.js +236 -0
  127. package/dist/project-map/findTests.js.map +1 -0
  128. package/dist/project-map/handlerInventory.d.ts +112 -0
  129. package/dist/project-map/handlerInventory.js +184 -0
  130. package/dist/project-map/handlerInventory.js.map +1 -0
  131. package/dist/project-map/implementationResolver.d.ts +43 -0
  132. package/dist/project-map/implementationResolver.js +217 -0
  133. package/dist/project-map/implementationResolver.js.map +1 -0
  134. package/dist/project-map/mapContext.d.ts +6 -1
  135. package/dist/project-map/mapContext.js +1 -0
  136. package/dist/project-map/mapContext.js.map +1 -1
  137. package/dist/project-map/scanCache.d.ts +57 -3
  138. package/dist/project-map/scanCache.js +61 -2
  139. package/dist/project-map/scanCache.js.map +1 -1
  140. package/dist/project-map/symbolIndex.d.ts +39 -0
  141. package/dist/project-map/symbolIndex.js +385 -0
  142. package/dist/project-map/symbolIndex.js.map +1 -0
  143. package/dist/tooling/agentEvalScoring.d.ts +77 -0
  144. package/dist/tooling/agentEvalScoring.js +140 -0
  145. package/dist/tooling/agentEvalScoring.js.map +1 -0
  146. package/dist/tooling/agentRegression.d.ts +53 -0
  147. package/dist/tooling/agentRegression.js +99 -0
  148. package/dist/tooling/agentRegression.js.map +1 -0
  149. package/dist/tools/agentScan.d.ts +69 -0
  150. package/dist/tools/agentScan.js +702 -43
  151. package/dist/tools/agentScan.js.map +1 -1
  152. package/dist/tools/findingReviewTools.d.ts +22 -0
  153. package/dist/tools/findingReviewTools.js +121 -0
  154. package/dist/tools/findingReviewTools.js.map +1 -0
  155. package/dist/tools/fix.js +1 -1
  156. package/dist/tools/fix.js.map +1 -1
  157. package/dist/tools/map.js +191 -1
  158. package/dist/tools/map.js.map +1 -1
  159. package/dist/tools/runTests.d.ts +2 -0
  160. package/dist/tools/runTests.js +29 -0
  161. package/dist/tools/runTests.js.map +1 -0
  162. package/dist/utils/effectMock.d.ts +1 -0
  163. package/dist/utils/effectMock.js +162 -0
  164. package/dist/utils/effectMock.js.map +1 -0
  165. package/dist/utils/gitContext.d.ts +26 -0
  166. package/dist/utils/gitContext.js +317 -0
  167. package/dist/utils/gitContext.js.map +1 -0
  168. package/dist/utils/localTestRunner.d.ts +57 -2
  169. package/dist/utils/localTestRunner.js +122 -115
  170. package/dist/utils/localTestRunner.js.map +1 -1
  171. package/dist/utils/securityConfig.d.ts +17 -0
  172. package/dist/utils/securityConfig.js +192 -0
  173. package/dist/utils/securityConfig.js.map +1 -0
  174. package/dist/utils/testCommandPolicy.d.ts +36 -0
  175. package/dist/utils/testCommandPolicy.js +252 -0
  176. package/dist/utils/testCommandPolicy.js.map +1 -0
  177. package/dist/utils/testRunner.d.ts +56 -0
  178. package/dist/utils/testRunner.js +246 -0
  179. package/dist/utils/testRunner.js.map +1 -0
  180. package/dist/utils/testSafety.d.ts +40 -2
  181. package/dist/utils/testSafety.js +205 -26
  182. package/dist/utils/testSafety.js.map +1 -1
  183. package/dist/utils/verificationSandbox.d.ts +113 -0
  184. package/dist/utils/verificationSandbox.js +656 -0
  185. package/dist/utils/verificationSandbox.js.map +1 -0
  186. package/package.json +1 -1
package/README.md CHANGED
@@ -8,6 +8,12 @@ Standalone MCP (Model Context Protocol) server for SecureCode AI. Provides secur
8
8
  npm install -g @securecode-ai/mcp
9
9
  ```
10
10
 
11
+ Or use directly with npx (no install needed):
12
+
13
+ ```bash
14
+ npx @securecode-ai/mcp scan src/app.ts
15
+ ```
16
+
11
17
  ## Quick Start
12
18
 
13
19
  ```bash
@@ -61,6 +67,22 @@ securecode-mcp scan src/app.ts --json
61
67
 
62
68
  ### Cursor / Windsurf
63
69
 
70
+ ```json
71
+ {
72
+ "mcpServers": {
73
+ "securecode": {
74
+ "command": "npx",
75
+ "args": ["-y", "@securecode-ai/mcp@latest", "serve", "--workspace", "/path/to/your/project"],
76
+ "env": {
77
+ "SECURECODE_API_TOKEN": "your-api-token-here"
78
+ }
79
+ }
80
+ }
81
+ }
82
+ ```
83
+
84
+ Or if installed globally:
85
+
64
86
  ```json
65
87
  {
66
88
  "mcpServers": {
@@ -78,14 +100,14 @@ securecode-mcp scan src/app.ts --json
78
100
  claude mcp add securecode -s user -- securecode-mcp serve --workspace /path/to/your/project
79
101
  ```
80
102
 
81
- ## Tools (11)
103
+ ## Tools (16)
82
104
 
83
105
  ### Scanning
84
106
 
85
107
  | Tool | Description | Approval |
86
108
  |------|-------------|----------|
87
109
  | `securecode.scan` | Scan code for vulnerabilities (AI pipeline) | No |
88
- | `securecode.agent-scan` | Agent-mode deep scan with 10 tools + sandbox proof | No |
110
+ | `securecode.agent-scan` | Agent-mode deep scan with 20+ tools, structured proof, and sandbox verification | No |
89
111
  | `securecode.scan-batch` | Scan multiple files in one call | No |
90
112
  | `securecode.scan-secrets` | Scan for hardcoded secrets and PII (local, no AI) | No |
91
113
  | `securecode.scan-dependencies` | Scan lockfiles for known vulnerabilities (OSV/NVD) | No |
@@ -94,7 +116,7 @@ claude mcp add securecode -s user -- securecode-mcp serve --workspace /path/to/y
94
116
 
95
117
  | Tool | Description | Approval |
96
118
  |------|-------------|----------|
97
- | `securecode.map` | Get the Project Map (endpoints, middleware, auth) | No |
119
+ | `securecode.map` | Build project map: endpoints, middleware, auth, architecture context | No |
98
120
 
99
121
  ### Fixes & Testing
100
122
 
@@ -102,6 +124,7 @@ claude mcp add securecode -s user -- securecode-mcp serve --workspace /path/to/y
102
124
  |------|-------------|----------|
103
125
  | `securecode.fix` | Generate a patch for a specific finding | Yes |
104
126
  | `securecode.attack` | Endpoint red-team testing (beta) | Yes |
127
+ | `securecode.run-tests` | Run tests in sandbox for verification | Yes |
105
128
 
106
129
  ### Agent Memory (FP Learning)
107
130
 
@@ -112,6 +135,14 @@ claude mcp add securecode -s user -- securecode-mcp serve --workspace /path/to/y
112
135
  | `securecode.clear-agent-memory` | Clear all agent memory (or one FP by ID) | No |
113
136
  | `securecode.add-known-fact` | Add a project fact for faster investigations | No |
114
137
 
138
+ ### Finding Review
139
+
140
+ | Tool | Description | Approval |
141
+ |------|-------------|----------|
142
+ | `securecode.review-findings` | Review the finding queue for a workspace | No |
143
+ | `securecode.decide-finding` | Accept or reject a finding in the review queue | No |
144
+ | `securecode.clear-finding-reviews` | Clear all finding reviews for a workspace | No |
145
+
115
146
  ### How Agent Memory Works
116
147
 
117
148
  When the agent reports a false positive, dismiss it with `record-false-positive`. The agent stores the pattern in `.securecode/agent-memory.json` and will not report similar patterns in future scans of that workspace.
@@ -125,17 +156,25 @@ Memory is per-workspace, user-owned, and deletable. No cross-tenant leakage.
125
156
 
126
157
  ## Agent Scan Architecture
127
158
 
128
- The agent scan (`securecode.agent-scan`) is an AI investigator that:
159
+ The agent scan (`securecode.agent-scan`) is an AI security investigator that:
160
+
161
+ 1. **Maps** the project architecture (architecture scout with trust boundaries, security controls, risks)
162
+ 2. **Reads** the target file and related files across the codebase
163
+ 3. **Traces** data flows (taint tracking, cross-file flow with structured source→sink→hop chains)
164
+ 4. **Checks** guards, endpoint policies, and configuration
165
+ 5. **Verifies** threat model applicability and capability reachability
166
+ 6. **Self-critiques** before reporting (selfCritique field)
167
+ 7. **Gets reviewed** by an independent critique LLM
168
+ 8. **Proves** findings in a sandbox (PROVEN/UNPROVEN)
169
+ 9. **Generates fixes** for proven findings
129
170
 
130
- 1. **Reads** the target file and related files (10 tools)
131
- 2. **Traces** data flows (taint tracking, cross-file)
132
- 3. **Checks** guards and endpoint policies
133
- 4. **Self-critiques** before reporting (selfCritique field)
134
- 5. **Gets reviewed** by an independent critique LLM
135
- 6. **Proves** findings in a sandbox (PROVEN/UNPROVEN)
136
- 7. **Generates fixes** for proven findings
171
+ Agent tools (20+): `read_file`, `search_code`, `trace_flow`, `trace_flow_cross_file`, `check_guard`, `check_policy`, `get_endpoints`, `list_imports`, `list_files`, `call_graph`, `git_blame`, `git_history`, `git_diff`, `check_dependencies`, `read_config`, `find_definition`, `find_references`, `find_tests`, `run_tests`, `finish`.
137
172
 
138
- Agent tools: `read_file`, `search_code`, `trace_flow`, `trace_flow_cross_file`, `check_guard`, `check_policy`, `get_endpoints`, `list_imports`, `list_files`, `finish`.
173
+ The deterministic control plane enforces proof quality:
174
+ - Every finding requires source, reachability, control, threat-model, and impact evidence
175
+ - Unproven concerns become investigation notes, not findings
176
+ - Architecture risks expand across related files (callers, implementations, sinks)
177
+ - The finish gate rejects finish while proof requirements remain unsatisfied
139
178
 
140
179
  Languages: JavaScript, TypeScript, Python (partial).
141
180
 
@@ -145,6 +184,7 @@ Languages: JavaScript, TypeScript, Python (partial).
145
184
  |----------|---------|-------------|
146
185
  | `SECURECODE_API_TOKEN` | — | API token (alternative to login) |
147
186
  | `SECURECODE_API_URL` | `https://api.usesecurecode.tech` | API base URL |
187
+ | `SECURECODE_ATTACK_ENABLED` | — | Set to `1` to enable the attack tool |
148
188
 
149
189
  ## Security
150
190
 
@@ -184,18 +184,25 @@ export interface SandboxProveResponse {
184
184
  exploitStatus?: number;
185
185
  };
186
186
  scanCredits?: number;
187
+ proofEvidence?: import('../attack/proofTypes').ProofEvidence;
188
+ proofGateResult?: import('../attack/proofTypes').ProofGateResult;
187
189
  }
188
190
  export interface VerifyGenerateResponse {
189
191
  canTest: boolean;
190
192
  testScript?: string;
193
+ setupScript?: string | null;
191
194
  runner?: string;
192
195
  description?: string;
193
196
  skipReason?: string;
194
197
  scanCredits?: number;
198
+ /** Dollar cost the API charged for this generate call (credits → USD). */
199
+ costUsd?: number;
195
200
  }
196
201
  export interface VerifyAnalyzeResponse {
197
202
  verdict: 'PROVEN' | 'UNPROVEN' | 'INCONCLUSIVE';
198
203
  reason: string;
199
204
  shouldRetry: boolean;
200
205
  scanCredits?: number;
206
+ /** Dollar cost the API charged for this analyze call (credits → USD). */
207
+ costUsd?: number;
201
208
  }
@@ -1,3 +1,3 @@
1
1
  import type { AuditEntry } from './types';
2
- export declare function appendAudit(entry: AuditEntry): void;
2
+ export declare function appendAudit(entry: AuditEntry): boolean;
3
3
  export declare function readAudit(limit?: number): AuditEntry[];
@@ -40,6 +40,8 @@ const path = __importStar(require("path"));
40
40
  const os = __importStar(require("os"));
41
41
  const AUDIT_DIR = path.join(os.homedir(), '.securecode');
42
42
  const AUDIT_FILE = path.join(AUDIT_DIR, 'approval-audit.log');
43
+ const MAX_ENTRIES = 200;
44
+ let auditWriteFailed = false;
43
45
  function appendAudit(entry) {
44
46
  try {
45
47
  if (!fs.existsSync(AUDIT_DIR)) {
@@ -47,12 +49,18 @@ function appendAudit(entry) {
47
49
  }
48
50
  const line = JSON.stringify(entry) + '\n';
49
51
  fs.appendFileSync(AUDIT_FILE, line, { mode: 0o600 });
52
+ auditWriteFailed = false;
53
+ return true;
50
54
  }
51
- catch {
52
- // best effort — audit failure must not block the operation
55
+ catch (err) {
56
+ if (!auditWriteFailed) {
57
+ auditWriteFailed = true;
58
+ console.warn(`[securecode] Audit write failed: ${err?.message || err}. Further audit failures will be silent.`);
59
+ }
60
+ return false;
53
61
  }
54
62
  }
55
- function readAudit(limit = 100) {
63
+ function readAudit(limit = MAX_ENTRIES) {
56
64
  try {
57
65
  if (!fs.existsSync(AUDIT_FILE))
58
66
  return [];
@@ -1 +1 @@
1
- {"version":3,"file":"auditLog.js","sourceRoot":"","sources":["../../src/approval/auditLog.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAQA,kCAUC;AAED,8BASC;AA7BD,uCAAyB;AACzB,2CAA6B;AAC7B,uCAAyB;AAGzB,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,aAAa,CAAC,CAAC;AACzD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,oBAAoB,CAAC,CAAC;AAE9D,SAAgB,WAAW,CAAC,KAAiB;IACzC,IAAI,CAAC;QACD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC5B,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC9D,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;QAC1C,EAAE,CAAC,cAAc,CAAC,UAAU,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACzD,CAAC;IAAC,MAAM,CAAC;QACL,2DAA2D;IAC/D,CAAC;AACL,CAAC;AAED,SAAgB,SAAS,CAAC,QAAgB,GAAG;IACzC,IAAI,CAAC;QACD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC;YAAE,OAAO,EAAE,CAAC;QAC1C,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QAChD,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACrD,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAe,CAAC,CAAC;IAC7E,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,EAAE,CAAC;IACd,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"auditLog.js","sourceRoot":"","sources":["../../src/approval/auditLog.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAWA,kCAgBC;AAED,8BASC;AAtCD,uCAAyB;AACzB,2CAA6B;AAC7B,uCAAyB;AAGzB,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,aAAa,CAAC,CAAC;AACzD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,oBAAoB,CAAC,CAAC;AAC9D,MAAM,WAAW,GAAG,GAAG,CAAC;AAExB,IAAI,gBAAgB,GAAG,KAAK,CAAC;AAE7B,SAAgB,WAAW,CAAC,KAAiB;IACzC,IAAI,CAAC;QACD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC5B,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC9D,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;QAC1C,EAAE,CAAC,cAAc,CAAC,UAAU,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACrD,gBAAgB,GAAG,KAAK,CAAC;QACzB,OAAO,IAAI,CAAC;IAChB,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAChB,IAAI,CAAC,gBAAgB,EAAE,CAAC;YACpB,gBAAgB,GAAG,IAAI,CAAC;YACxB,OAAO,CAAC,IAAI,CAAC,oCAAoC,GAAG,EAAE,OAAO,IAAI,GAAG,0CAA0C,CAAC,CAAC;QACpH,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;AACL,CAAC;AAED,SAAgB,SAAS,CAAC,QAAgB,WAAW;IACjD,IAAI,CAAC;QACD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC;YAAE,OAAO,EAAE,CAAC;QAC1C,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QAChD,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACrD,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAe,CAAC,CAAC;IAC7E,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,EAAE,CAAC;IACd,CAAC;AACL,CAAC"}
@@ -1,4 +1,4 @@
1
- import { type ApprovalResult } from './types';
1
+ import { type ApprovalResult, type OperationCategory } from './types';
2
2
  export declare class ApprovalBroker {
3
3
  private server;
4
4
  private port;
@@ -6,6 +6,11 @@ export declare class ApprovalBroker {
6
6
  start(): Promise<number>;
7
7
  stop(): Promise<void>;
8
8
  getPort(): number;
9
- requestApproval(tool: string, summary: string, operationParts: unknown[], timeoutMs?: number): Promise<ApprovalResult>;
9
+ requestApproval(tool: string, summary: string, operationParts: unknown[], timeoutMs?: number, category?: OperationCategory, workspaceRoot?: string | null): Promise<ApprovalResult>;
10
+ private settle;
11
+ private isLocalOrigin;
12
+ private isLocalHost;
10
13
  private handle;
14
+ private handleDetails;
15
+ private handleDecide;
11
16
  }
@@ -38,6 +38,9 @@ const http = __importStar(require("http"));
38
38
  const child_process_1 = require("child_process");
39
39
  const types_1 = require("./types");
40
40
  const auditLog_1 = require("./auditLog");
41
+ const MAX_PENDING = 16;
42
+ const MAX_BODY_BYTES = 4096;
43
+ const LOOPBACK = '127.0.0.1';
41
44
  const APPROVAL_PAGE_HTML = `<!DOCTYPE html>
42
45
  <html>
43
46
  <head>
@@ -55,6 +58,7 @@ button { padding: 10px 24px; border: none; border-radius: 6px; font-size: 14px;
55
58
  .result { margin-top: 24px; font-size: 16px; font-weight: 600; }
56
59
  .timeout { color: #d97706; }
57
60
  .expired { display: none; color: #dc2626; }
61
+ .error { color: #dc2626; }
58
62
  </style>
59
63
  </head>
60
64
  <body>
@@ -70,42 +74,65 @@ button { padding: 10px 24px; border: none; border-radius: 6px; font-size: 14px;
70
74
  <script>
71
75
  const params = new URLSearchParams(window.location.search);
72
76
  const reqId = params.get('id');
73
- document.getElementById('tool').textContent = params.get('tool') || '';
74
- document.getElementById('hash').textContent = params.get('hash') || '';
75
- document.getElementById('summary').textContent = decodeURIComponent(params.get('summary') || '');
77
+ let decisionToken = '';
76
78
 
77
- async function respond(approved) {
78
- document.getElementById('buttons').style.display = 'none';
79
- const res = document.getElementById('result');
80
- try {
81
- const r = await fetch('/decide', {
82
- method: 'POST',
83
- headers: {'Content-Type': 'application/json'},
84
- body: JSON.stringify({ id: reqId, approved })
85
- });
86
- const data = await r.json();
87
- if (data.ok) {
88
- res.textContent = approved ? 'Approved. You can close this page.' : 'Denied. You can close this page.';
89
- res.style.color = approved ? '#16a34a' : '#dc2626';
90
- } else {
91
- res.textContent = 'Error: ' + (data.error || 'unknown');
92
- res.style.color = '#dc2626';
93
- }
94
- } catch (e) {
95
- res.textContent = 'Error: ' + e.message;
96
- res.style.color = '#dc2626';
97
- }
79
+ async function loadDetails() {
80
+ try {
81
+ const r = await fetch('/details?id=' + encodeURIComponent(reqId || ''));
82
+ const data = await r.json();
83
+ if (!data.ok) {
84
+ document.getElementById('buttons').style.display = 'none';
85
+ const el = document.getElementById('result');
86
+ el.textContent = 'Error: ' + (data.error || 'unknown');
87
+ el.style.color = '#dc2626';
88
+ return;
89
+ }
90
+ document.getElementById('tool').textContent = data.tool || '';
91
+ document.getElementById('hash').textContent = data.operationHash || '';
92
+ document.getElementById('summary').textContent = data.summary || '';
93
+ decisionToken = data.decisionToken || '';
94
+ if (data.expiresAt && Date.now() > data.expiresAt) {
95
+ document.getElementById('buttons').style.display = 'none';
96
+ document.getElementById('expired').style.display = 'block';
97
+ }
98
+ } catch (e) {
99
+ document.getElementById('buttons').style.display = 'none';
100
+ const el = document.getElementById('result');
101
+ el.textContent = 'Error loading details: ' + e.message;
102
+ el.style.color = '#dc2626';
103
+ }
98
104
  }
99
105
 
100
- const expiresAt = parseInt(params.get('exp') || '0');
101
- if (expiresAt && Date.now() > expiresAt) {
102
- document.getElementById('buttons').style.display = 'none';
103
- document.getElementById('expired').style.display = 'block';
106
+ async function respond(approved) {
107
+ document.getElementById('buttons').style.display = 'none';
108
+ const res = document.getElementById('result');
109
+ try {
110
+ const r = await fetch('/decide', {
111
+ method: 'POST',
112
+ headers: {'Content-Type': 'application/json'},
113
+ body: JSON.stringify({ id: reqId, decisionToken, approved })
114
+ });
115
+ const data = await r.json();
116
+ if (data.ok) {
117
+ res.textContent = approved ? 'Approved. You can close this page.' : 'Denied. You can close this page.';
118
+ res.style.color = approved ? '#16a34a' : '#dc2626';
119
+ } else {
120
+ res.textContent = 'Error: ' + (data.error || 'unknown');
121
+ res.style.color = '#dc2626';
122
+ }
123
+ } catch (e) {
124
+ res.textContent = 'Error: ' + e.message;
125
+ res.style.color = '#dc2626';
126
+ }
104
127
  }
128
+
129
+ loadDetails();
105
130
  </script>
106
131
  </body>
107
132
  </html>`;
108
133
  function openBrowser(url) {
134
+ if (process.env.SECURECODE_TEST_MODE === '1')
135
+ return;
109
136
  const platform = process.platform;
110
137
  try {
111
138
  if (platform === 'darwin') {
@@ -122,14 +149,23 @@ function openBrowser(url) {
122
149
  // If browser open fails, the user can copy the URL from stdout
123
150
  }
124
151
  }
152
+ const SECURITY_HEADERS = {
153
+ 'Content-Security-Policy': "default-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'",
154
+ 'X-Content-Type-Options': 'nosniff',
155
+ 'Cache-Control': 'no-store',
156
+ 'Referrer-Policy': 'no-referrer',
157
+ };
125
158
  class ApprovalBroker {
126
159
  server = null;
127
160
  port = 0;
128
161
  pending = new Map();
129
162
  async start() {
130
- return new Promise((resolve) => {
163
+ return new Promise((resolve, reject) => {
131
164
  this.server = http.createServer((req, res) => this.handle(req, res));
132
- this.server.listen(0, '127.0.0.1', () => {
165
+ this.server.once('error', (err) => {
166
+ reject(err);
167
+ });
168
+ this.server.listen(0, LOOPBACK, () => {
133
169
  const addr = this.server.address();
134
170
  this.port = addr.port;
135
171
  resolve(this.port);
@@ -137,6 +173,9 @@ class ApprovalBroker {
137
173
  });
138
174
  }
139
175
  async stop() {
176
+ for (const [, entry] of this.pending) {
177
+ this.settle(entry, false, 'shutdown', 'Broker shutting down');
178
+ }
140
179
  return new Promise((resolve) => {
141
180
  if (this.server) {
142
181
  this.server.close(() => resolve());
@@ -149,104 +188,194 @@ class ApprovalBroker {
149
188
  getPort() {
150
189
  return this.port;
151
190
  }
152
- requestApproval(tool, summary, operationParts, timeoutMs = 60_000) {
191
+ requestApproval(tool, summary, operationParts, timeoutMs = 60_000, category = 'paid-generation', workspaceRoot = null) {
153
192
  return new Promise((resolve) => {
154
- const req = (0, types_1.createApprovalRequest)(tool, summary, operationParts, timeoutMs);
155
- const startTime = Date.now();
156
- this.pending.set(req.id, { req, resolve });
157
- const url = `http://127.0.0.1:${this.port}/?id=${req.id}&tool=${encodeURIComponent(tool)}&hash=${req.operationHash}&summary=${encodeURIComponent(summary)}&exp=${req.expiresAt}`;
158
- console.error(`[securecode] Approval required for ${tool}. Open: ${url}`);
159
- openBrowser(url);
193
+ if (this.pending.size >= MAX_PENDING) {
194
+ resolve({
195
+ approved: false,
196
+ reason: 'Too many pending approval requests',
197
+ requestId: '',
198
+ duration: 0,
199
+ category,
200
+ });
201
+ return;
202
+ }
203
+ const req = (0, types_1.createApprovalRequest)(tool, summary, operationParts, timeoutMs, category, workspaceRoot);
204
+ const startTime = req.createdAt;
160
205
  const timer = setTimeout(() => {
161
206
  const entry = this.pending.get(req.id);
162
- if (entry) {
163
- this.pending.delete(req.id);
164
- const result = {
165
- approved: false,
166
- reason: 'Request timed out',
167
- requestId: req.id,
168
- duration: Date.now() - startTime,
169
- };
170
- (0, auditLog_1.appendAudit)({
171
- timestamp: new Date().toISOString(),
172
- requestId: req.id,
173
- tool: req.tool,
174
- operationHash: req.operationHash,
175
- approved: false,
176
- reason: 'timeout',
177
- durationMs: result.duration,
178
- });
179
- resolve(result);
207
+ if (entry && !entry.settled) {
208
+ this.settle(entry, false, 'timeout', 'Request timed out');
180
209
  }
181
210
  }, timeoutMs + 5000);
182
- const origResolve = resolve;
183
- this.pending.set(req.id, {
184
- req,
185
- resolve: (r) => {
186
- clearTimeout(timer);
187
- origResolve(r);
188
- },
189
- });
211
+ const entry = { req, resolve, timer, settled: false };
212
+ this.pending.set(req.id, entry);
213
+ const url = `http://${LOOPBACK}:${this.port}/?id=${req.id}`;
214
+ console.error(`[securecode] Approval required for ${tool}. Open: ${url}`);
215
+ openBrowser(url);
190
216
  });
191
217
  }
218
+ settle(entry, approved, reason, humanReason) {
219
+ if (entry.settled)
220
+ return;
221
+ entry.settled = true;
222
+ clearTimeout(entry.timer);
223
+ this.pending.delete(entry.req.id);
224
+ const duration = Date.now() - entry.req.createdAt;
225
+ const result = {
226
+ approved,
227
+ reason: humanReason,
228
+ requestId: entry.req.id,
229
+ duration,
230
+ category: entry.req.category,
231
+ };
232
+ const audit = {
233
+ timestamp: new Date().toISOString(),
234
+ requestId: entry.req.id,
235
+ tool: entry.req.tool,
236
+ operationHash: entry.req.operationHash,
237
+ category: entry.req.category,
238
+ workspaceId: entry.req.workspaceId,
239
+ approved,
240
+ reason,
241
+ durationMs: duration,
242
+ };
243
+ (0, auditLog_1.appendAudit)(audit);
244
+ entry.resolve(result);
245
+ }
246
+ isLocalOrigin(req) {
247
+ const origin = req.headers.origin;
248
+ if (!origin)
249
+ return true; // non-browser client
250
+ try {
251
+ const u = new URL(origin);
252
+ return u.hostname === LOOPBACK;
253
+ }
254
+ catch {
255
+ return false;
256
+ }
257
+ }
258
+ isLocalHost(req) {
259
+ const host = req.headers.host || '';
260
+ try {
261
+ const { hostname } = new URL(`http://${host}`);
262
+ return hostname === LOOPBACK || hostname === 'localhost';
263
+ }
264
+ catch {
265
+ return false;
266
+ }
267
+ }
192
268
  handle(req, res) {
269
+ if (!this.isLocalHost(req)) {
270
+ res.writeHead(403, SECURITY_HEADERS);
271
+ res.end('Forbidden');
272
+ return;
273
+ }
193
274
  const url = req.url || '/';
194
275
  if (req.method === 'GET' && (url === '/' || url.startsWith('/?'))) {
195
- res.writeHead(200, { 'Content-Type': 'text/html' });
276
+ res.writeHead(200, { 'Content-Type': 'text/html', ...SECURITY_HEADERS });
196
277
  res.end(APPROVAL_PAGE_HTML);
197
278
  return;
198
279
  }
280
+ if (req.method === 'GET' && url.startsWith('/details')) {
281
+ this.handleDetails(req, res);
282
+ return;
283
+ }
199
284
  if (req.method === 'POST' && req.url === '/decide') {
200
- let body = '';
201
- req.on('data', (c) => { body += c; });
202
- req.on('end', () => {
203
- try {
204
- const data = JSON.parse(body);
205
- const requestId = data.id;
206
- const entry = requestId ? this.pending.get(requestId) : undefined;
207
- if (!entry || !requestId) {
208
- res.writeHead(200, { 'Content-Type': 'application/json' });
209
- res.end(JSON.stringify({ ok: false, error: 'Request not found or already answered' }));
210
- return;
211
- }
212
- if ((0, types_1.isExpired)(entry.req)) {
213
- this.pending.delete(requestId);
214
- res.writeHead(200, { 'Content-Type': 'application/json' });
215
- res.end(JSON.stringify({ ok: false, error: 'Request expired' }));
216
- return;
217
- }
218
- this.pending.delete(requestId);
219
- const approved = data.approved === true;
220
- const startTime = entry.req.createdAt;
221
- const result = {
222
- approved,
223
- reason: approved ? 'User approved' : 'User denied',
224
- requestId: entry.req.id,
225
- duration: Date.now() - startTime,
226
- };
227
- (0, auditLog_1.appendAudit)({
228
- timestamp: new Date().toISOString(),
229
- requestId: entry.req.id,
230
- tool: entry.req.tool,
231
- operationHash: entry.req.operationHash,
232
- approved,
233
- reason: approved ? 'approved' : 'denied',
234
- durationMs: result.duration,
235
- });
236
- entry.resolve(result);
237
- res.writeHead(200, { 'Content-Type': 'application/json' });
238
- res.end(JSON.stringify({ ok: true }));
239
- }
240
- catch {
241
- res.writeHead(400, { 'Content-Type': 'application/json' });
242
- res.end(JSON.stringify({ ok: false, error: 'Invalid request body' }));
243
- }
244
- });
285
+ this.handleDecide(req, res);
245
286
  return;
246
287
  }
247
- res.writeHead(404);
288
+ res.writeHead(404, SECURITY_HEADERS);
248
289
  res.end('Not found');
249
290
  }
291
+ handleDetails(req, res) {
292
+ const parsed = new URL(req.url || '', `http://${LOOPBACK}`);
293
+ const id = parsed.searchParams.get('id');
294
+ const entry = id ? this.pending.get(id) : undefined;
295
+ if (!entry || !id) {
296
+ res.writeHead(200, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
297
+ res.end(JSON.stringify({ ok: false, error: 'Request not found or already answered' }));
298
+ return;
299
+ }
300
+ if ((0, types_1.isExpired)(entry.req)) {
301
+ res.writeHead(200, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
302
+ res.end(JSON.stringify({ ok: false, error: 'Request expired' }));
303
+ return;
304
+ }
305
+ res.writeHead(200, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
306
+ res.end(JSON.stringify({
307
+ ok: true,
308
+ tool: entry.req.tool,
309
+ summary: entry.req.summary,
310
+ operationHash: entry.req.operationHash,
311
+ expiresAt: entry.req.expiresAt,
312
+ decisionToken: entry.req.decisionToken,
313
+ }));
314
+ }
315
+ handleDecide(req, res) {
316
+ if (!this.isLocalOrigin(req)) {
317
+ res.writeHead(403, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
318
+ res.end(JSON.stringify({ ok: false, error: 'Forbidden origin' }));
319
+ return;
320
+ }
321
+ const ct = req.headers['content-type'] || '';
322
+ if (!ct.includes('application/json')) {
323
+ res.writeHead(400, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
324
+ res.end(JSON.stringify({ ok: false, error: 'Content-Type must be application/json' }));
325
+ return;
326
+ }
327
+ let body = '';
328
+ let tooLarge = false;
329
+ req.on('data', (c) => {
330
+ body += c;
331
+ if (body.length > MAX_BODY_BYTES) {
332
+ tooLarge = true;
333
+ req.destroy();
334
+ }
335
+ });
336
+ req.on('end', () => {
337
+ if (tooLarge) {
338
+ res.writeHead(413, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
339
+ res.end(JSON.stringify({ ok: false, error: 'Body too large' }));
340
+ return;
341
+ }
342
+ try {
343
+ const data = JSON.parse(body);
344
+ const requestId = data.id;
345
+ const entry = requestId ? this.pending.get(requestId) : undefined;
346
+ if (!entry || !requestId) {
347
+ res.writeHead(200, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
348
+ res.end(JSON.stringify({ ok: false, error: 'Request not found or already answered' }));
349
+ return;
350
+ }
351
+ if (entry.settled) {
352
+ res.writeHead(200, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
353
+ res.end(JSON.stringify({ ok: false, error: 'Request already answered' }));
354
+ return;
355
+ }
356
+ if (!data.decisionToken || data.decisionToken !== entry.req.decisionToken) {
357
+ this.settle(entry, false, 'invalid-token', 'Invalid decision token');
358
+ res.writeHead(200, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
359
+ res.end(JSON.stringify({ ok: false, error: 'Invalid decision token' }));
360
+ return;
361
+ }
362
+ if ((0, types_1.isExpired)(entry.req)) {
363
+ this.settle(entry, false, 'expired', 'Request expired');
364
+ res.writeHead(200, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
365
+ res.end(JSON.stringify({ ok: false, error: 'Request expired' }));
366
+ return;
367
+ }
368
+ const approved = data.approved === true;
369
+ this.settle(entry, approved, approved ? 'approved' : 'denied', approved ? 'User approved' : 'User denied');
370
+ res.writeHead(200, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
371
+ res.end(JSON.stringify({ ok: true }));
372
+ }
373
+ catch {
374
+ res.writeHead(400, { 'Content-Type': 'application/json', ...SECURITY_HEADERS });
375
+ res.end(JSON.stringify({ ok: false, error: 'Invalid request body' }));
376
+ }
377
+ });
378
+ }
250
379
  }
251
380
  exports.ApprovalBroker = ApprovalBroker;
252
381
  //# sourceMappingURL=broker.js.map