@praneeth_54/agentdoctor 2.0.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +123 -1
- package/README.md +510 -260
- package/dist/agent/approvals.d.ts +24 -0
- package/dist/agent/approvals.js +64 -0
- package/dist/agent/chat/deterministic.d.ts +6 -0
- package/dist/agent/chat/deterministic.js +180 -0
- package/dist/agent/chat/index.d.ts +7 -0
- package/dist/agent/chat/index.js +5 -0
- package/dist/agent/chat/memory.d.ts +44 -0
- package/dist/agent/chat/memory.js +103 -0
- package/dist/agent/chat/project-summary.d.ts +16 -0
- package/dist/agent/chat/project-summary.js +70 -0
- package/dist/agent/chat/prompts.d.ts +6 -0
- package/dist/agent/chat/prompts.js +39 -0
- package/dist/agent/chat/response.d.ts +19 -0
- package/dist/agent/chat/response.js +109 -0
- package/dist/agent/chat/service.d.ts +42 -0
- package/dist/agent/chat/service.js +246 -0
- package/dist/agent/chat/types.d.ts +48 -0
- package/dist/agent/chat/types.js +1 -0
- package/dist/agent/context/retrieve.d.ts +21 -0
- package/dist/agent/context/retrieve.js +117 -0
- package/dist/agent/context/truth.d.ts +6 -0
- package/dist/agent/context/truth.js +19 -0
- package/dist/agent/context/types.d.ts +23 -0
- package/dist/agent/context/types.js +4 -0
- package/dist/agent/index.d.ts +28 -0
- package/dist/agent/index.js +15 -0
- package/dist/agent/loop.d.ts +53 -0
- package/dist/agent/loop.js +253 -0
- package/dist/agent/modes.d.ts +18 -0
- package/dist/agent/modes.js +102 -0
- package/dist/agent/plan.d.ts +30 -0
- package/dist/agent/plan.js +121 -0
- package/dist/agent/roles.d.ts +12 -0
- package/dist/agent/roles.js +138 -0
- package/dist/agent/runtime.d.ts +83 -0
- package/dist/agent/runtime.js +293 -0
- package/dist/agent/state.d.ts +30 -0
- package/dist/agent/state.js +95 -0
- package/dist/agent/student.d.ts +56 -0
- package/dist/agent/student.js +230 -0
- package/dist/agent/tools/execute.d.ts +20 -0
- package/dist/agent/tools/execute.js +365 -0
- package/dist/agent/tools/index.d.ts +6 -0
- package/dist/agent/tools/index.js +5 -0
- package/dist/agent/tools/registry.d.ts +7 -0
- package/dist/agent/tools/registry.js +212 -0
- package/dist/agent/tools/run.d.ts +24 -0
- package/dist/agent/tools/run.js +44 -0
- package/dist/agent/tools/types.d.ts +32 -0
- package/dist/agent/tools/types.js +10 -0
- package/dist/agent/tools/write.d.ts +24 -0
- package/dist/agent/tools/write.js +121 -0
- package/dist/agent/verify.d.ts +29 -0
- package/dist/agent/verify.js +210 -0
- package/dist/ai/config.d.ts +22 -0
- package/dist/ai/config.js +69 -0
- package/dist/ai/index.d.ts +17 -0
- package/dist/ai/index.js +52 -0
- package/dist/ai/providers/mock.d.ts +20 -0
- package/dist/ai/providers/mock.js +84 -0
- package/dist/ai/providers/none.d.ts +6 -0
- package/dist/ai/providers/none.js +23 -0
- package/dist/ai/providers/openai-compatible.d.ts +21 -0
- package/dist/ai/providers/openai-compatible.js +151 -0
- package/dist/ai/redact.d.ts +9 -0
- package/dist/ai/redact.js +61 -0
- package/dist/ai/types.d.ts +71 -0
- package/dist/ai/types.js +6 -0
- package/dist/cli/commands/agent.d.ts +29 -0
- package/dist/cli/commands/agent.js +164 -0
- package/dist/cli/commands/chat.d.ts +14 -0
- package/dist/cli/commands/chat.js +153 -0
- package/dist/cli/commands/learn.d.ts +12 -0
- package/dist/cli/commands/learn.js +107 -0
- package/dist/cli/commands/product.d.ts +19 -0
- package/dist/cli/commands/product.js +147 -0
- package/dist/cli/commands/start.d.ts +15 -0
- package/dist/cli/commands/start.js +80 -0
- package/dist/cli/program.js +233 -0
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/dashboard/server.d.ts +6 -0
- package/dist/dashboard/server.js +333 -43
- package/dist/enforcement/runner.d.ts +5 -0
- package/dist/enforcement/runner.js +51 -4
- package/dist/index.d.ts +17 -0
- package/dist/index.js +10 -0
- package/dist/intelligence/graph/build.js +36 -8
- package/dist/intelligence/resolve/imports.js +1 -1
- package/dist/languages/dart.d.ts +10 -0
- package/dist/languages/dart.js +99 -0
- package/dist/languages/go.d.ts +13 -7
- package/dist/languages/go.js +91 -26
- package/dist/languages/index.d.ts +5 -1
- package/dist/languages/index.js +13 -36
- package/dist/languages/java.d.ts +10 -0
- package/dist/languages/java.js +80 -0
- package/dist/languages/kotlin.d.ts +10 -0
- package/dist/languages/kotlin.js +85 -0
- package/dist/languages/php.d.ts +1 -0
- package/dist/languages/php.js +11 -2
- package/dist/languages/python.d.ts +6 -2
- package/dist/languages/python.js +39 -9
- package/dist/languages/rust.d.ts +10 -0
- package/dist/languages/rust.js +93 -0
- package/dist/languages/types.d.ts +4 -3
- package/dist/mcp/agent/registry.d.ts +13 -0
- package/dist/mcp/agent/registry.js +320 -0
- package/dist/mcp/agentdoctor/server.js +8 -1
- package/dist/mcp/intelligence/handlers.d.ts +3 -0
- package/dist/mcp/intelligence/handlers.js +28 -0
- package/dist/mcp/intelligence/registry.d.ts +1 -1
- package/dist/mcp/intelligence/registry.js +30 -1
- package/dist/product/api/doctor.d.ts +14 -0
- package/dist/product/api/doctor.js +185 -0
- package/dist/product/api/openapi.d.ts +7 -0
- package/dist/product/api/openapi.js +122 -0
- package/dist/product/approval/model.d.ts +30 -0
- package/dist/product/approval/model.js +64 -0
- package/dist/product/approval/session.d.ts +49 -0
- package/dist/product/approval/session.js +134 -0
- package/dist/product/database/doctor.d.ts +30 -0
- package/dist/product/database/doctor.js +184 -0
- package/dist/product/decisions/ledger.d.ts +24 -0
- package/dist/product/decisions/ledger.js +110 -0
- package/dist/product/deps/analyze.d.ts +46 -0
- package/dist/product/deps/analyze.js +137 -0
- package/dist/product/deps/lockfiles.d.ts +25 -0
- package/dist/product/deps/lockfiles.js +200 -0
- package/dist/product/discovery/roots.d.ts +42 -0
- package/dist/product/discovery/roots.js +215 -0
- package/dist/product/dna/build.d.ts +50 -0
- package/dist/product/dna/build.js +255 -0
- package/dist/product/eval/lab.d.ts +17 -0
- package/dist/product/eval/lab.js +218 -0
- package/dist/product/events/doctor.d.ts +21 -0
- package/dist/product/events/doctor.js +147 -0
- package/dist/product/evidence-scan.d.ts +12 -0
- package/dist/product/evidence-scan.js +46 -0
- package/dist/product/evolution/timeline.d.ts +29 -0
- package/dist/product/evolution/timeline.js +123 -0
- package/dist/product/features/intelligence.d.ts +23 -0
- package/dist/product/features/intelligence.js +158 -0
- package/dist/product/forensic/mode.d.ts +25 -0
- package/dist/product/forensic/mode.js +70 -0
- package/dist/product/graph/enrich-languages.d.ts +20 -0
- package/dist/product/graph/enrich-languages.js +193 -0
- package/dist/product/health/code-health.d.ts +20 -0
- package/dist/product/health/code-health.js +149 -0
- package/dist/product/index.d.ts +69 -0
- package/dist/product/index.js +35 -0
- package/dist/product/ledger/change-ledger.d.ts +23 -0
- package/dist/product/ledger/change-ledger.js +64 -0
- package/dist/product/map/software-map.d.ts +17 -0
- package/dist/product/map/software-map.js +95 -0
- package/dist/product/memory/institutional.d.ts +16 -0
- package/dist/product/memory/institutional.js +87 -0
- package/dist/product/ops/incident.d.ts +20 -0
- package/dist/product/ops/incident.js +61 -0
- package/dist/product/ops/infra.d.ts +15 -0
- package/dist/product/ops/infra.js +112 -0
- package/dist/product/org/model.d.ts +34 -0
- package/dist/product/org/model.js +195 -0
- package/dist/product/privacy/doctor.d.ts +13 -0
- package/dist/product/privacy/doctor.js +90 -0
- package/dist/product/requirements/trace.d.ts +21 -0
- package/dist/product/requirements/trace.js +166 -0
- package/dist/product/search/index.d.ts +29 -0
- package/dist/product/search/index.js +116 -0
- package/dist/product/search/software-search.d.ts +19 -0
- package/dist/product/search/software-search.js +100 -0
- package/dist/product/security/doctor.d.ts +23 -0
- package/dist/product/security/doctor.js +124 -0
- package/dist/product/self/diagnose.d.ts +15 -0
- package/dist/product/self/diagnose.js +82 -0
- package/dist/product/techdebt/roadmap.d.ts +20 -0
- package/dist/product/techdebt/roadmap.js +118 -0
- package/dist/product/testbrain/analyze.d.ts +29 -0
- package/dist/product/testbrain/analyze.js +129 -0
- package/dist/product/truth.d.ts +12 -0
- package/dist/product/truth.js +30 -0
- package/dist/product/twin/digital-twin.d.ts +23 -0
- package/dist/product/twin/digital-twin.js +52 -0
- package/dist/product/twin/store.d.ts +21 -0
- package/dist/product/twin/store.js +70 -0
- package/dist/product/whatif/engine.d.ts +28 -0
- package/dist/product/whatif/engine.js +70 -0
- package/dist/security/paths.js +43 -15
- package/dist/utils/fs.js +10 -2
- package/package.json +1 -1
|
@@ -11,6 +11,9 @@ import { redactSecrets } from "../../platform/security/redact.js";
|
|
|
11
11
|
import { resolveRepoRoot } from "../../utils/path.js";
|
|
12
12
|
import { CONTRACTS_VERSION } from "../../contracts/index.js";
|
|
13
13
|
import { assertSafeRepoTarget } from "./path-safety.js";
|
|
14
|
+
import { buildProjectDna } from "../../product/dna/build.js";
|
|
15
|
+
import { buildSoftwareMap } from "../../product/map/software-map.js";
|
|
16
|
+
import { analyzeWhatIf } from "../../product/whatif/engine.js";
|
|
14
17
|
function requireString(args, key) {
|
|
15
18
|
const value = args[key];
|
|
16
19
|
if (typeof value !== "string")
|
|
@@ -362,3 +365,28 @@ export async function handleGraphQueryTool(rootInput, args) {
|
|
|
362
365
|
],
|
|
363
366
|
});
|
|
364
367
|
}
|
|
368
|
+
export async function handleProjectDnaTool(rootInput) {
|
|
369
|
+
const root = resolveRepoRoot(rootInput);
|
|
370
|
+
const dna = await buildProjectDna(root);
|
|
371
|
+
return redactDeep({ ok: true, dna, limitations: dna.limitations });
|
|
372
|
+
}
|
|
373
|
+
export async function handleSoftwareMapTool(rootInput) {
|
|
374
|
+
const root = resolveRepoRoot(rootInput);
|
|
375
|
+
const map = await buildSoftwareMap(root);
|
|
376
|
+
return redactDeep({ ok: true, map, limitations: map.limitations });
|
|
377
|
+
}
|
|
378
|
+
export async function handleWhatIfTool(rootInput, args) {
|
|
379
|
+
const root = resolveRepoRoot(rootInput);
|
|
380
|
+
const target = requireString(args, "target");
|
|
381
|
+
if (!target) {
|
|
382
|
+
return invalidArgument("target required (repo-relative path or symbol)");
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
assertSafeRepoTarget(root, target);
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
return pathEscape();
|
|
389
|
+
}
|
|
390
|
+
const report = await analyzeWhatIf(root, target);
|
|
391
|
+
return redactDeep({ ok: true, report, limitations: report.limitations });
|
|
392
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
-
export declare const INTELLIGENCE_MCP_TOOL_NAMES: readonly ["repo_overview", "codebase_search", "symbol_lookup", "dependency_lookup", "call_graph_lookup", "test_impact", "refactor_impact", "code_health", "architecture_info", "architecture_check", "knowledge_retrieve", "policy_evaluate", "change_analyze", "proof_inspect", "evidence_inspect", "graph_query"];
|
|
2
|
+
export declare const INTELLIGENCE_MCP_TOOL_NAMES: readonly ["repo_overview", "codebase_search", "symbol_lookup", "dependency_lookup", "call_graph_lookup", "test_impact", "refactor_impact", "code_health", "architecture_info", "architecture_check", "knowledge_retrieve", "policy_evaluate", "change_analyze", "proof_inspect", "evidence_inspect", "graph_query", "project_dna", "software_map", "what_if"];
|
|
3
3
|
export type IntelligenceMcpToolName = (typeof INTELLIGENCE_MCP_TOOL_NAMES)[number];
|
|
4
4
|
export declare function listIntelligenceMcpTools(): Tool[];
|
|
5
5
|
export declare function invokeIntelligenceMcpTool(root: string, name: string, args: Record<string, unknown>): Promise<{
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { handleArchitectureCheckTool, handleArchitectureTool, handleCallGraphLookup, handleChangeAnalyzeTool, handleCodeHealthTool, handleCodebaseSearch, handleDependencyLookup, handleEvidenceInspectTool, handleGraphQueryTool, handleKnowledgeTool, handlePolicyEvalTool, handleProofInspectTool, handleRefactorImpactTool, handleRepoOverview, handleSymbolLookup, handleTestImpactTool, } from "./handlers.js";
|
|
1
|
+
import { handleArchitectureCheckTool, handleArchitectureTool, handleCallGraphLookup, handleChangeAnalyzeTool, handleCodeHealthTool, handleCodebaseSearch, handleDependencyLookup, handleEvidenceInspectTool, handleGraphQueryTool, handleKnowledgeTool, handlePolicyEvalTool, handleProofInspectTool, handleRefactorImpactTool, handleRepoOverview, handleSymbolLookup, handleTestImpactTool, handleProjectDnaTool, handleSoftwareMapTool, handleWhatIfTool, } from "./handlers.js";
|
|
2
2
|
export const INTELLIGENCE_MCP_TOOL_NAMES = [
|
|
3
3
|
"repo_overview",
|
|
4
4
|
"codebase_search",
|
|
@@ -16,6 +16,9 @@ export const INTELLIGENCE_MCP_TOOL_NAMES = [
|
|
|
16
16
|
"proof_inspect",
|
|
17
17
|
"evidence_inspect",
|
|
18
18
|
"graph_query",
|
|
19
|
+
"project_dna",
|
|
20
|
+
"software_map",
|
|
21
|
+
"what_if",
|
|
19
22
|
];
|
|
20
23
|
const emptyObjectSchema = {
|
|
21
24
|
type: "object",
|
|
@@ -166,6 +169,26 @@ export function listIntelligenceMcpTools() {
|
|
|
166
169
|
additionalProperties: false,
|
|
167
170
|
},
|
|
168
171
|
},
|
|
172
|
+
{
|
|
173
|
+
name: "project_dna",
|
|
174
|
+
description: "READ: Deterministic project DNA fingerprint (languages, frameworks, monorepo). No shell.",
|
|
175
|
+
inputSchema: emptyObjectSchema,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: "software_map",
|
|
179
|
+
description: "READ: Navigable software map tree from discovery heuristics. No shell.",
|
|
180
|
+
inputSchema: emptyObjectSchema,
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: "what_if",
|
|
184
|
+
description: "READ: What-if impact analysis for a repo-relative path or symbol (graph-backed). Path validated.",
|
|
185
|
+
inputSchema: {
|
|
186
|
+
type: "object",
|
|
187
|
+
properties: { target: { type: "string" } },
|
|
188
|
+
required: ["target"],
|
|
189
|
+
additionalProperties: false,
|
|
190
|
+
},
|
|
191
|
+
},
|
|
169
192
|
];
|
|
170
193
|
}
|
|
171
194
|
export async function invokeIntelligenceMcpTool(root, name, args) {
|
|
@@ -203,6 +226,12 @@ export async function invokeIntelligenceMcpTool(root, name, args) {
|
|
|
203
226
|
return { structured: await handleEvidenceInspectTool(root, args), isError: false };
|
|
204
227
|
case "graph_query":
|
|
205
228
|
return { structured: await handleGraphQueryTool(root, args), isError: false };
|
|
229
|
+
case "project_dna":
|
|
230
|
+
return { structured: await handleProjectDnaTool(root), isError: false };
|
|
231
|
+
case "software_map":
|
|
232
|
+
return { structured: await handleSoftwareMapTool(root), isError: false };
|
|
233
|
+
case "what_if":
|
|
234
|
+
return { structured: await handleWhatIfTool(root, args), isError: false };
|
|
206
235
|
default:
|
|
207
236
|
return {
|
|
208
237
|
structured: {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ProductEvidence, TruthLabel } from "../truth.js";
|
|
2
|
+
export interface ApiEndpointFinding {
|
|
3
|
+
method: string;
|
|
4
|
+
pathPattern: string;
|
|
5
|
+
framework: string;
|
|
6
|
+
truth: TruthLabel;
|
|
7
|
+
evidence: ProductEvidence[];
|
|
8
|
+
}
|
|
9
|
+
export interface ApiDoctorReport {
|
|
10
|
+
root: string;
|
|
11
|
+
endpoints: ApiEndpointFinding[];
|
|
12
|
+
limitations: string[];
|
|
13
|
+
}
|
|
14
|
+
export declare function analyzeApiSurface(rootInput: string): Promise<ApiDoctorReport>;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { lineNumberAt, loadProjectSourceFiles } from "../evidence-scan.js";
|
|
2
|
+
import { discoverOpenApiEndpoints } from "./openapi.js";
|
|
3
|
+
const ROUTE_PATTERNS = [
|
|
4
|
+
{
|
|
5
|
+
framework: "express",
|
|
6
|
+
regex: /\.(?:get|post|put|patch|delete|all)\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
7
|
+
pathGroup: 1,
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
framework: "express-router",
|
|
11
|
+
regex: /router\.(?:get|post|put|patch|delete|all)\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
12
|
+
pathGroup: 1,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
framework: "fastify",
|
|
16
|
+
regex: /fastify\.(?:get|post|put|patch|delete)\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
17
|
+
pathGroup: 1,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
framework: "nestjs",
|
|
21
|
+
regex: /@(Get|Post|Put|Patch|Delete|All)\(\s*['"`]([^'"`]+)['"`]?\s*\)/g,
|
|
22
|
+
methodGroup: 1,
|
|
23
|
+
pathGroup: 2,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
framework: "nestjs",
|
|
27
|
+
regex: /@(Get|Post|Put|Patch|Delete|All)\(\)/g,
|
|
28
|
+
methodGroup: 1,
|
|
29
|
+
pathGroup: 0,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
framework: "laravel",
|
|
33
|
+
regex: /Route::(get|post|put|patch|delete|any)\(\s*['"]([^'"]+)['"]/gi,
|
|
34
|
+
methodGroup: 1,
|
|
35
|
+
pathGroup: 2,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
framework: "flask",
|
|
39
|
+
regex: /@(?:app|bp|blueprint)\.(?:route|get|post|put|patch|delete)\(\s*['"]([^'"]+)['"]/gi,
|
|
40
|
+
pathGroup: 1,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
framework: "flask",
|
|
44
|
+
regex: /@(?:app|bp)\.(get|post|put|patch|delete)\(\s*['"]([^'"]+)['"]/gi,
|
|
45
|
+
methodGroup: 1,
|
|
46
|
+
pathGroup: 2,
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
function inferMethodFromMatch(full, pattern, groups) {
|
|
50
|
+
const methodRaw = pattern.methodGroup !== undefined ? groups[pattern.methodGroup] : undefined;
|
|
51
|
+
if (methodRaw) {
|
|
52
|
+
return methodRaw.toUpperCase();
|
|
53
|
+
}
|
|
54
|
+
const lower = full.toLowerCase();
|
|
55
|
+
for (const m of ["get", "post", "put", "patch", "delete", "all"]) {
|
|
56
|
+
if (lower.includes(`.${m}(`) || lower.includes(`@${m}`) || lower.includes(`::${m}(`)) {
|
|
57
|
+
return m === "all" ? "ALL" : m.toUpperCase();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return pattern.defaultMethod ?? "UNKNOWN";
|
|
61
|
+
}
|
|
62
|
+
function scanFile(relativePath, content) {
|
|
63
|
+
const findings = [];
|
|
64
|
+
for (const pattern of ROUTE_PATTERNS) {
|
|
65
|
+
pattern.regex.lastIndex = 0;
|
|
66
|
+
let match;
|
|
67
|
+
while ((match = pattern.regex.exec(content)) !== null) {
|
|
68
|
+
const pathGroup = pattern.pathGroup;
|
|
69
|
+
let pathPattern = pathGroup > 0 ? (match[pathGroup] ?? "") : "/";
|
|
70
|
+
if (!pathPattern && pattern.framework === "nestjs")
|
|
71
|
+
pathPattern = "/";
|
|
72
|
+
if (!pathPattern)
|
|
73
|
+
continue;
|
|
74
|
+
const method = inferMethodFromMatch(match[0], pattern, match);
|
|
75
|
+
const line = lineNumberAt(content, match.index);
|
|
76
|
+
findings.push({
|
|
77
|
+
method,
|
|
78
|
+
pathPattern,
|
|
79
|
+
framework: pattern.framework,
|
|
80
|
+
truth: "INFERRED",
|
|
81
|
+
evidence: [
|
|
82
|
+
{
|
|
83
|
+
path: relativePath,
|
|
84
|
+
line,
|
|
85
|
+
excerpt: match[0].trim().slice(0, 160),
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return findings;
|
|
92
|
+
}
|
|
93
|
+
function scanGraphqlSurface(relativePath, content) {
|
|
94
|
+
const findings = [];
|
|
95
|
+
const typeQueryRe = /type\s+Query\s*\{/g;
|
|
96
|
+
let match;
|
|
97
|
+
while ((match = typeQueryRe.exec(content)) !== null) {
|
|
98
|
+
findings.push({
|
|
99
|
+
method: "GRAPHQL",
|
|
100
|
+
pathPattern: "Query",
|
|
101
|
+
framework: "graphql-sdl",
|
|
102
|
+
truth: "INFERRED",
|
|
103
|
+
evidence: [
|
|
104
|
+
{
|
|
105
|
+
path: relativePath,
|
|
106
|
+
line: lineNumberAt(content, match.index),
|
|
107
|
+
excerpt: match[0].trim(),
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
const gqlRe = /gql`\s*([\s\S]*?)`/g;
|
|
113
|
+
while ((match = gqlRe.exec(content)) !== null) {
|
|
114
|
+
const inner = match[1] ?? "";
|
|
115
|
+
const opMatch = /(?:query|mutation|subscription)\s+(\w+)/i.exec(inner);
|
|
116
|
+
const opName = opMatch?.[1] ?? "anonymous";
|
|
117
|
+
findings.push({
|
|
118
|
+
method: "GRAPHQL",
|
|
119
|
+
pathPattern: opName,
|
|
120
|
+
framework: "graphql-tag",
|
|
121
|
+
truth: "INFERRED",
|
|
122
|
+
evidence: [
|
|
123
|
+
{
|
|
124
|
+
path: relativePath,
|
|
125
|
+
line: lineNumberAt(content, match.index),
|
|
126
|
+
excerpt: match[0].trim().slice(0, 160),
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return findings;
|
|
132
|
+
}
|
|
133
|
+
const SOURCE_FILTER = (rel) => {
|
|
134
|
+
const lower = rel.toLowerCase();
|
|
135
|
+
if (lower.endsWith(".graphql") || lower.endsWith(".gql"))
|
|
136
|
+
return true;
|
|
137
|
+
if (lower.endsWith(".php") && (lower.includes("routes/") || lower.includes("route")))
|
|
138
|
+
return true;
|
|
139
|
+
if (lower.endsWith(".py") &&
|
|
140
|
+
(lower.includes("routes") || lower.includes("views") || lower.includes("app"))) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
if (lower.endsWith(".ts") ||
|
|
144
|
+
lower.endsWith(".tsx") ||
|
|
145
|
+
lower.endsWith(".js") ||
|
|
146
|
+
lower.endsWith(".jsx")) {
|
|
147
|
+
return (lower.includes("route") ||
|
|
148
|
+
lower.includes("controller") ||
|
|
149
|
+
lower.includes("api/") ||
|
|
150
|
+
lower.includes("server") ||
|
|
151
|
+
lower.includes("app.module") ||
|
|
152
|
+
lower.includes("graphql") ||
|
|
153
|
+
lower.includes("schema"));
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
};
|
|
157
|
+
export async function analyzeApiSurface(rootInput) {
|
|
158
|
+
const { root, files, limitations: scanLimits, } = await loadProjectSourceFiles(rootInput, SOURCE_FILTER);
|
|
159
|
+
const limitations = [
|
|
160
|
+
"Endpoint discovery uses regex on route declarations; middleware, prefixes, and global mounts are not resolved.",
|
|
161
|
+
"Dynamic route paths and framework-specific route files may be missed.",
|
|
162
|
+
"GraphQL schema extraction (PARTIAL): static SDL `type Query` and gql` template literals only — not live gateway introspection.",
|
|
163
|
+
...scanLimits,
|
|
164
|
+
];
|
|
165
|
+
const endpoints = [];
|
|
166
|
+
for (const file of files) {
|
|
167
|
+
endpoints.push(...scanFile(file.relativePath, file.content));
|
|
168
|
+
endpoints.push(...scanGraphqlSurface(file.relativePath, file.content));
|
|
169
|
+
}
|
|
170
|
+
const openApi = await discoverOpenApiEndpoints(root);
|
|
171
|
+
limitations.push(...openApi.limitations);
|
|
172
|
+
endpoints.push(...openApi.endpoints);
|
|
173
|
+
const seen = new Set();
|
|
174
|
+
const deduped = endpoints.filter((e) => {
|
|
175
|
+
const key = `${e.method}:${e.pathPattern}:${e.framework}:${e.evidence[0]?.path}:${e.evidence[0]?.line}`;
|
|
176
|
+
if (seen.has(key))
|
|
177
|
+
return false;
|
|
178
|
+
seen.add(key);
|
|
179
|
+
return true;
|
|
180
|
+
});
|
|
181
|
+
deduped.sort((a, b) => a.pathPattern === b.pathPattern
|
|
182
|
+
? a.method.localeCompare(b.method)
|
|
183
|
+
: a.pathPattern.localeCompare(b.pathPattern));
|
|
184
|
+
return { root, endpoints: deduped, limitations };
|
|
185
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ProductEvidence, TruthLabel } from "../truth.js";
|
|
2
|
+
import type { ApiEndpointFinding } from "./doctor.js";
|
|
3
|
+
export declare function discoverOpenApiEndpoints(rootInput: string, maxFileSizeBytes?: number): Promise<{
|
|
4
|
+
endpoints: ApiEndpointFinding[];
|
|
5
|
+
limitations: string[];
|
|
6
|
+
}>;
|
|
7
|
+
export type { TruthLabel, ProductEvidence };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { detectProject } from "../../detectors/project.js";
|
|
3
|
+
import { readTextFile } from "../../utils/fs.js";
|
|
4
|
+
import { resolveRepoRoot } from "../../utils/path.js";
|
|
5
|
+
const OPENAPI_NAMES = new Set([
|
|
6
|
+
"openapi.json",
|
|
7
|
+
"openapi.yaml",
|
|
8
|
+
"openapi.yml",
|
|
9
|
+
"swagger.json",
|
|
10
|
+
"swagger.yaml",
|
|
11
|
+
"swagger.yml",
|
|
12
|
+
]);
|
|
13
|
+
function basenameLower(rel) {
|
|
14
|
+
return path.basename(rel).toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
function extractJsonOpenApiPaths(content, relativePath) {
|
|
17
|
+
let doc;
|
|
18
|
+
try {
|
|
19
|
+
doc = JSON.parse(content);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
const paths = doc.paths;
|
|
25
|
+
if (!paths || typeof paths !== "object")
|
|
26
|
+
return [];
|
|
27
|
+
const out = [];
|
|
28
|
+
for (const [pathPattern, methods] of Object.entries(paths)) {
|
|
29
|
+
if (!methods || typeof methods !== "object")
|
|
30
|
+
continue;
|
|
31
|
+
for (const method of Object.keys(methods)) {
|
|
32
|
+
const lower = method.toLowerCase();
|
|
33
|
+
if (!["get", "post", "put", "patch", "delete", "head", "options", "trace"].includes(lower)) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
out.push({
|
|
37
|
+
method: lower.toUpperCase(),
|
|
38
|
+
pathPattern,
|
|
39
|
+
framework: "openapi",
|
|
40
|
+
truth: "VERIFIED",
|
|
41
|
+
evidence: [{ path: relativePath, excerpt: `paths.${pathPattern}.${lower}` }],
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
/** Minimal YAML path/method extraction without a YAML library */
|
|
48
|
+
function extractYamlOpenApiPaths(content, relativePath) {
|
|
49
|
+
const out = [];
|
|
50
|
+
const lines = content.split(/\r?\n/);
|
|
51
|
+
let inPaths = false;
|
|
52
|
+
let currentPath = null;
|
|
53
|
+
const methodRe = /^\s{2,6}(get|post|put|patch|delete|head|options|trace):\s*$/i;
|
|
54
|
+
for (let i = 0; i < lines.length; i++) {
|
|
55
|
+
const line = lines[i];
|
|
56
|
+
if (/^paths:\s*$/.test(line.trim())) {
|
|
57
|
+
inPaths = true;
|
|
58
|
+
currentPath = null;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (!inPaths)
|
|
62
|
+
continue;
|
|
63
|
+
if (/^[a-zA-Z]/.test(line) && !line.startsWith(" ")) {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
const pathKey = /^\s{2}(\/[^\s:]+):\s*$/.exec(line);
|
|
67
|
+
if (pathKey) {
|
|
68
|
+
currentPath = pathKey[1];
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const methodMatch = methodRe.exec(line);
|
|
72
|
+
if (methodMatch && currentPath) {
|
|
73
|
+
const method = methodMatch[1].toUpperCase();
|
|
74
|
+
out.push({
|
|
75
|
+
method,
|
|
76
|
+
pathPattern: currentPath,
|
|
77
|
+
framework: "openapi",
|
|
78
|
+
truth: "VERIFIED",
|
|
79
|
+
evidence: [{ path: relativePath, line: i + 1, excerpt: line.trim() }],
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
export async function discoverOpenApiEndpoints(rootInput, maxFileSizeBytes = 512 * 1024) {
|
|
86
|
+
const root = resolveRepoRoot(rootInput);
|
|
87
|
+
const detection = await detectProject(root, maxFileSizeBytes);
|
|
88
|
+
const limitations = [
|
|
89
|
+
"OpenAPI discovery reads static spec files only — mounted servers and merged specs are not resolved.",
|
|
90
|
+
];
|
|
91
|
+
const endpoints = [];
|
|
92
|
+
for (const entry of detection.discovery.files) {
|
|
93
|
+
const rel = entry.relativePath.replace(/\\/g, "/");
|
|
94
|
+
if (!OPENAPI_NAMES.has(basenameLower(rel)))
|
|
95
|
+
continue;
|
|
96
|
+
const text = await readTextFile(path.join(root, rel), maxFileSizeBytes);
|
|
97
|
+
if (text === null)
|
|
98
|
+
continue;
|
|
99
|
+
const lower = basenameLower(rel);
|
|
100
|
+
if (lower.endsWith(".json")) {
|
|
101
|
+
endpoints.push(...extractJsonOpenApiPaths(text, rel));
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
endpoints.push(...extractYamlOpenApiPaths(text, rel));
|
|
105
|
+
if (!endpoints.length) {
|
|
106
|
+
limitations.push(`YAML spec ${rel} had no paths block detected (minimal parser)`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const seen = new Set();
|
|
111
|
+
const deduped = endpoints.filter((e) => {
|
|
112
|
+
const key = `${e.method}:${e.pathPattern}:${e.evidence[0]?.path}`;
|
|
113
|
+
if (seen.has(key))
|
|
114
|
+
return false;
|
|
115
|
+
seen.add(key);
|
|
116
|
+
return true;
|
|
117
|
+
});
|
|
118
|
+
deduped.sort((a, b) => a.pathPattern === b.pathPattern
|
|
119
|
+
? a.method.localeCompare(b.method)
|
|
120
|
+
: a.pathPattern.localeCompare(b.pathPattern));
|
|
121
|
+
return { endpoints: deduped, limitations };
|
|
122
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type ApprovalState = "pending" | "approved" | "denied" | "expired";
|
|
2
|
+
export type ApprovalRisk = "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
|
|
3
|
+
export interface ApprovalRecord {
|
|
4
|
+
id: string;
|
|
5
|
+
action: string;
|
|
6
|
+
reason: string;
|
|
7
|
+
resources: string[];
|
|
8
|
+
risk: ApprovalRisk;
|
|
9
|
+
requirement?: string;
|
|
10
|
+
state: ApprovalState;
|
|
11
|
+
actor?: string;
|
|
12
|
+
createdAt: string;
|
|
13
|
+
decidedAt?: string;
|
|
14
|
+
/** Must be true from trusted CLI/MCP session layer — never accept caller approved=true alone */
|
|
15
|
+
approvedByHuman: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface ApprovalEvaluationInput {
|
|
18
|
+
record: Omit<ApprovalRecord, "id" | "createdAt" | "state" | "approvedByHuman">;
|
|
19
|
+
/** Explicit human gate from CLI `--approve` or authenticated session layer */
|
|
20
|
+
approvedByHuman?: boolean;
|
|
21
|
+
actor?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ApprovalEvaluationResult {
|
|
24
|
+
record: ApprovalRecord;
|
|
25
|
+
allowed: boolean;
|
|
26
|
+
message: string;
|
|
27
|
+
limitations: string[];
|
|
28
|
+
}
|
|
29
|
+
export declare function evaluateApprovalRecord(input: ApprovalEvaluationInput): ApprovalEvaluationResult;
|
|
30
|
+
export declare function formatApprovalRecordSummary(record: ApprovalRecord): string;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const LIMITATIONS = [
|
|
2
|
+
"ApprovalRecord is an explicit abstraction — MCP/model callers cannot set approvedByHuman=true without a trusted session.",
|
|
3
|
+
"CLI passes approvedByHuman via --approve; dashboard chat does not grant approval.",
|
|
4
|
+
"This module does not persist approvals — use change ledger / audit trails separately.",
|
|
5
|
+
];
|
|
6
|
+
function newId() {
|
|
7
|
+
return `apr_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
8
|
+
}
|
|
9
|
+
export function evaluateApprovalRecord(input) {
|
|
10
|
+
const approvedByHuman = input.approvedByHuman === true;
|
|
11
|
+
const now = new Date().toISOString();
|
|
12
|
+
const base = {
|
|
13
|
+
id: newId(),
|
|
14
|
+
action: input.record.action,
|
|
15
|
+
reason: input.record.reason,
|
|
16
|
+
resources: [...input.record.resources],
|
|
17
|
+
risk: input.record.risk,
|
|
18
|
+
...(input.record.requirement !== undefined ? { requirement: input.record.requirement } : {}),
|
|
19
|
+
state: "pending",
|
|
20
|
+
...(input.actor !== undefined ? { actor: input.actor } : {}),
|
|
21
|
+
createdAt: now,
|
|
22
|
+
approvedByHuman,
|
|
23
|
+
};
|
|
24
|
+
if (!approvedByHuman) {
|
|
25
|
+
return {
|
|
26
|
+
record: { ...base, state: "pending", approvedByHuman: false },
|
|
27
|
+
allowed: false,
|
|
28
|
+
message: "Human approval required — approvedByHuman must be true from trusted CLI/MCP session (not from model output).",
|
|
29
|
+
limitations: LIMITATIONS,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (input.record.risk === "CRITICAL" && !input.record.reason.trim()) {
|
|
33
|
+
return {
|
|
34
|
+
record: { ...base, state: "denied", approvedByHuman: true, decidedAt: now },
|
|
35
|
+
allowed: false,
|
|
36
|
+
message: "CRITICAL actions require a non-empty reason even when approvedByHuman is true.",
|
|
37
|
+
limitations: LIMITATIONS,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
record: {
|
|
42
|
+
...base,
|
|
43
|
+
state: "approved",
|
|
44
|
+
approvedByHuman: true,
|
|
45
|
+
decidedAt: now,
|
|
46
|
+
},
|
|
47
|
+
allowed: true,
|
|
48
|
+
message: "Explicit human approval recorded.",
|
|
49
|
+
limitations: LIMITATIONS,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function formatApprovalRecordSummary(record) {
|
|
53
|
+
return [
|
|
54
|
+
`Approval ${record.id}`,
|
|
55
|
+
`Action: ${record.action}`,
|
|
56
|
+
`Risk: ${record.risk}`,
|
|
57
|
+
`State: ${record.state}`,
|
|
58
|
+
`approvedByHuman: ${record.approvedByHuman}`,
|
|
59
|
+
record.requirement ? `Requirement: ${record.requirement}` : "",
|
|
60
|
+
record.resources.length ? `Resources: ${record.resources.join(", ")}` : "",
|
|
61
|
+
]
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.join("\n");
|
|
64
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ApprovalRisk } from "./model.js";
|
|
2
|
+
/**
|
|
3
|
+
* Trusted approval grants — MCP/agent writes must present a grant token
|
|
4
|
+
* issued by CLI `--approve` or an authenticated session, not a bare approved=true.
|
|
5
|
+
*/
|
|
6
|
+
export interface ApprovalGrant {
|
|
7
|
+
token: string;
|
|
8
|
+
root: string;
|
|
9
|
+
action: string;
|
|
10
|
+
resources: string[];
|
|
11
|
+
risk: ApprovalRisk;
|
|
12
|
+
planHash: string;
|
|
13
|
+
actor: string;
|
|
14
|
+
createdAt: string;
|
|
15
|
+
expiresAt: string;
|
|
16
|
+
consumed: boolean;
|
|
17
|
+
scope: "write" | "execute" | "write+execute";
|
|
18
|
+
}
|
|
19
|
+
export declare function hashPlanPayload(payload: string): string;
|
|
20
|
+
/** Stable plan hash for a file write operation (path + content). */
|
|
21
|
+
export declare function hashFileWritePlan(action: string, relativePath: string, content: string): string;
|
|
22
|
+
export declare function issueApprovalGrant(options: {
|
|
23
|
+
root: string;
|
|
24
|
+
action: string;
|
|
25
|
+
resources: string[];
|
|
26
|
+
risk: ApprovalRisk;
|
|
27
|
+
planHash: string;
|
|
28
|
+
actor?: string;
|
|
29
|
+
scope?: ApprovalGrant["scope"];
|
|
30
|
+
ttlMs?: number;
|
|
31
|
+
}): Promise<ApprovalGrant>;
|
|
32
|
+
export interface ConsumeApprovalResult {
|
|
33
|
+
ok: boolean;
|
|
34
|
+
reason: string;
|
|
35
|
+
grant?: ApprovalGrant;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Validate and optionally consume a grant token for a write/execute action.
|
|
39
|
+
* Bare `approved: true` without a valid token is NEVER sufficient.
|
|
40
|
+
*/
|
|
41
|
+
export declare function consumeApprovalGrant(options: {
|
|
42
|
+
root: string;
|
|
43
|
+
token: string | undefined;
|
|
44
|
+
action: string;
|
|
45
|
+
resources: string[];
|
|
46
|
+
/** Required for MCP/agent writes — binds token to exact operation payload */
|
|
47
|
+
requirePlanHash: string;
|
|
48
|
+
consume?: boolean;
|
|
49
|
+
}): Promise<ConsumeApprovalResult>;
|