-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathIntelligenceController.java
More file actions
158 lines (144 loc) · 7 KB
/
Copy pathIntelligenceController.java
File metadata and controls
158 lines (144 loc) · 7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
package io.github.randomcodespace.iq.api;
import io.github.randomcodespace.iq.config.CodeIqConfig;
import io.github.randomcodespace.iq.intelligence.evidence.EvidencePack;
import io.github.randomcodespace.iq.intelligence.evidence.EvidencePackAssembler;
import io.github.randomcodespace.iq.intelligence.evidence.EvidencePackRequest;
import io.github.randomcodespace.iq.intelligence.provenance.ArtifactMetadata;
import io.github.randomcodespace.iq.intelligence.provenance.ArtifactMetadataProvider;
import io.github.randomcodespace.iq.intelligence.query.CapabilityMatrix;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import java.io.IOException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.Map;
/**
* Intelligence REST API — evidence packs, artifact metadata, and capability matrix.
* Read-only. Active only in the {@code serving} profile.
*/
@RestController
@RequestMapping("/api/intelligence")
@Profile("serving")
public class IntelligenceController {
private final EvidencePackAssembler assembler;
private final ArtifactMetadataProvider artifactMetadataProvider;
private final CodeIqConfig config;
public IntelligenceController(
@org.springframework.beans.factory.annotation.Autowired(required = false)
EvidencePackAssembler assembler,
@org.springframework.beans.factory.annotation.Autowired(required = false)
ArtifactMetadataProvider artifactMetadataProvider,
CodeIqConfig config) {
this.assembler = assembler;
this.artifactMetadataProvider = artifactMetadataProvider;
this.config = config;
}
/**
* Assemble an evidence pack for a symbol or file path.
*
* <p>At least one of {@code symbol} or {@code file} must be provided.
* The {@code file} parameter is path-traversal guarded with the same two-stage
* (lexical {@code normalize} then {@link Path#toRealPath} re-check) guard used
* by {@code GraphController.readFile} and the MCP {@code read_file} tool, so a
* symlink inside the indexed repo cannot be used to leak off-tree files.
*
* @param symbol symbol name to look up
* @param file file path relative to repo root (path traversal guarded)
* @param maxSnippetLines max lines per snippet (optional, capped at config limit)
* @param includeRefs whether to include cross-reference nodes (default false)
* @return assembled evidence pack
*/
@GetMapping("/evidence")
public EvidencePack getEvidence(
@RequestParam(required = false) String symbol,
@RequestParam(required = false) String file,
@RequestParam(required = false) Integer maxSnippetLines,
@RequestParam(defaultValue = "false") boolean includeRefs) {
requireAssembler();
// 400 when both are absent
if ((symbol == null || symbol.isBlank()) && (file == null || file.isBlank())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"At least one of 'symbol' or 'file' must be provided.");
}
// Path-traversal guard on file param: two-stage lexical + symlink check.
if (file != null && !file.isBlank()) {
Path root;
try {
root = Path.of(config.getRootPath()).toRealPath();
} catch (IOException e) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,
"Failed to resolve codebase root: " + e.getMessage());
}
Path candidate = root.resolve(file).normalize();
if (!candidate.startsWith(root)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Invalid file path: path traversal detected.");
}
// Resolve symlinks if the file exists on disk and re-check containment.
// If the file is logical-only (graph reference, no on-disk file), the
// lexical guard above is sufficient — there is no symlink to traverse.
try {
Path real = candidate.toRealPath();
if (!real.startsWith(root)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Invalid file path: path traversal detected.");
}
} catch (NoSuchFileException ignored) {
// file may exist only as a graph reference — lexical guard already passed
} catch (IOException e) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,
"Failed to resolve file path: " + e.getMessage());
}
}
EvidencePackRequest request = new EvidencePackRequest(symbol, file, maxSnippetLines, includeRefs);
return assembler.assemble(request, currentArtifactMetadata());
}
/**
* Returns the artifact metadata loaded at serve startup.
*/
@GetMapping("/manifest")
public ArtifactMetadata getManifest() {
if (artifactMetadataProvider == null) {
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
"Artifact metadata unavailable. Run 'enrich' first.");
}
return artifactMetadataProvider.current();
}
/**
* Returns the full capability matrix as a JSON object.
* Optionally filter by language.
*
* @param language optional language filter (e.g. "java", "python")
*/
@GetMapping("/capabilities")
public Map<String, Object> getCapabilities(
@RequestParam(required = false) String language) {
Map<String, Object> result = new java.util.LinkedHashMap<>();
if (language != null && !language.isBlank()) {
result.put("language", language.strip().toLowerCase());
result.put("capabilities", CapabilityMatrix.forLanguage(language).entrySet().stream()
.collect(java.util.stream.Collectors.toMap(
e -> e.getKey().name().toLowerCase(),
e -> e.getValue().name(),
(a, b) -> a,
java.util.TreeMap::new)));
} else {
result.put("matrix", CapabilityMatrix.asSerializableMap());
}
return result;
}
private void requireAssembler() {
if (assembler == null) {
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
"Intelligence service unavailable. Run 'enrich' first.");
}
}
private ArtifactMetadata currentArtifactMetadata() {
return artifactMetadataProvider != null ? artifactMetadataProvider.current() : null;
}
}