@praneeth_54/agentdoctor 2.1.0 → 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 +57 -3
- package/README.md +489 -290
- package/dist/agent/chat/deterministic.d.ts +6 -0
- package/dist/agent/chat/deterministic.js +180 -0
- package/dist/agent/chat/service.js +8 -14
- package/dist/agent/index.d.ts +2 -0
- package/dist/agent/index.js +1 -0
- package/dist/agent/loop.d.ts +2 -0
- package/dist/agent/loop.js +31 -0
- package/dist/agent/roles.d.ts +12 -0
- package/dist/agent/roles.js +138 -0
- package/dist/agent/runtime.d.ts +21 -5
- package/dist/agent/runtime.js +141 -28
- package/dist/agent/tools/execute.d.ts +2 -0
- package/dist/agent/tools/execute.js +10 -0
- package/dist/ai/config.js +1 -0
- package/dist/ai/redact.d.ts +1 -0
- package/dist/ai/redact.js +47 -7
- package/dist/ai/types.d.ts +1 -1
- package/dist/cli/commands/agent.d.ts +2 -0
- package/dist/cli/commands/agent.js +19 -8
- package/dist/cli/commands/chat.js +2 -4
- 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 +141 -0
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/dashboard/server.js +255 -53
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -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 +84 -24
- 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/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 +1 -1
- package/dist/mcp/agent/registry.js +109 -23
- 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/utils/fs.js +10 -2
- package/package.json +1 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Lightweight line-oriented Rust scan — not syn/rustc AST. */
|
|
2
|
+
export function parseRustSource(filePath, source) {
|
|
3
|
+
const lines = source.split(/\r?\n/);
|
|
4
|
+
const limitations = [
|
|
5
|
+
"Rust parsing uses a lightweight line scanner — not syn or tree-sitter-rust; calls/types are partial.",
|
|
6
|
+
"Full rustc/syn or tree-sitter-rust integration remains an EXTERNAL enhancement path (not bundled).",
|
|
7
|
+
];
|
|
8
|
+
let moduleName = "";
|
|
9
|
+
const imports = [];
|
|
10
|
+
const symbols = [];
|
|
11
|
+
for (let i = 0; i < lines.length; i++) {
|
|
12
|
+
const trimmed = lines[i].trim();
|
|
13
|
+
if (!moduleName && trimmed.startsWith("mod ") && !trimmed.includes("{")) {
|
|
14
|
+
moduleName = trimmed.slice("mod ".length).split(/\s+/)[0]?.replace(";", "") ?? "";
|
|
15
|
+
}
|
|
16
|
+
if (trimmed.startsWith("use ") && trimmed.endsWith(";")) {
|
|
17
|
+
const spec = trimmed.slice("use ".length, -1).trim();
|
|
18
|
+
if (spec)
|
|
19
|
+
imports.push({ fromFile: filePath, specifier: spec, evidence: "ast" });
|
|
20
|
+
}
|
|
21
|
+
const structMatch = /^(?:pub\s+)?(?:struct|enum|trait|type)\s+(\w+)/.exec(trimmed);
|
|
22
|
+
if (structMatch) {
|
|
23
|
+
let kind = "struct";
|
|
24
|
+
if (trimmed.includes("enum"))
|
|
25
|
+
kind = "enum";
|
|
26
|
+
else if (trimmed.includes("trait"))
|
|
27
|
+
kind = "trait";
|
|
28
|
+
else if (trimmed.includes("type"))
|
|
29
|
+
kind = "type";
|
|
30
|
+
symbols.push({
|
|
31
|
+
id: `rust:${kind}:${filePath}:${structMatch[1]}`,
|
|
32
|
+
name: structMatch[1],
|
|
33
|
+
kind,
|
|
34
|
+
file: filePath,
|
|
35
|
+
line: i + 1,
|
|
36
|
+
evidence: "ast",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const fnMatch = /^(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/.exec(trimmed);
|
|
40
|
+
if (fnMatch) {
|
|
41
|
+
symbols.push({
|
|
42
|
+
id: `rust:function:${filePath}:${fnMatch[1]}`,
|
|
43
|
+
name: fnMatch[1],
|
|
44
|
+
kind: "function",
|
|
45
|
+
file: filePath,
|
|
46
|
+
line: i + 1,
|
|
47
|
+
evidence: "ast",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const implMatch = /^impl(?:<[^>]+>)?\s+(?:\w+::)*(\w+)/.exec(trimmed);
|
|
51
|
+
if (implMatch) {
|
|
52
|
+
symbols.push({
|
|
53
|
+
id: `rust:impl:${filePath}:${implMatch[1]}`,
|
|
54
|
+
name: implMatch[1],
|
|
55
|
+
kind: "impl",
|
|
56
|
+
file: filePath,
|
|
57
|
+
line: i + 1,
|
|
58
|
+
evidence: "ast",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { moduleName, imports, symbols, limitations };
|
|
63
|
+
}
|
|
64
|
+
const RUST_CAPABILITIES = () => ({
|
|
65
|
+
parse: "supported",
|
|
66
|
+
symbols: "supported",
|
|
67
|
+
definitions: "unsupported",
|
|
68
|
+
references: "unsupported",
|
|
69
|
+
imports: "supported",
|
|
70
|
+
calls: "partial",
|
|
71
|
+
diagnostics: "unsupported",
|
|
72
|
+
types: "partial",
|
|
73
|
+
});
|
|
74
|
+
export const rustAdapter = {
|
|
75
|
+
id: "rust",
|
|
76
|
+
extensions: [".rs"],
|
|
77
|
+
capabilities: RUST_CAPABILITIES,
|
|
78
|
+
async parse(filePath, source) {
|
|
79
|
+
const summary = parseRustSource(filePath, source);
|
|
80
|
+
const ok = Boolean(summary.moduleName || summary.symbols.length > 0 || summary.imports.length > 0);
|
|
81
|
+
return {
|
|
82
|
+
language: "rust",
|
|
83
|
+
file: filePath,
|
|
84
|
+
ok,
|
|
85
|
+
capabilities: rustAdapter.capabilities(),
|
|
86
|
+
symbols: summary.symbols,
|
|
87
|
+
imports: summary.imports,
|
|
88
|
+
calls: [],
|
|
89
|
+
diagnostics: ok ? [] : ["no mod, use, or declarations detected"],
|
|
90
|
+
limitations: summary.limitations,
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Unsupported capabilities must be reported as unsupported, never as inferred AST.
|
|
4
4
|
*/
|
|
5
5
|
export type LanguageId = "typescript" | "javascript" | "python" | "java" | "kotlin" | "go" | "rust" | "php" | "dart" | "unknown";
|
|
6
|
-
export type AdapterCapability = "parse" | "symbols" | "definitions" | "references" | "imports" | "calls" | "diagnostics";
|
|
6
|
+
export type AdapterCapability = "parse" | "symbols" | "definitions" | "references" | "imports" | "calls" | "diagnostics" | "types";
|
|
7
7
|
export interface LanguageSymbol {
|
|
8
8
|
id: string;
|
|
9
9
|
name: string;
|
|
@@ -22,11 +22,12 @@ export interface LanguageCall {
|
|
|
22
22
|
callee: string;
|
|
23
23
|
evidence: "ast" | "unsupported";
|
|
24
24
|
}
|
|
25
|
+
export type AdapterCapabilityLevel = "supported" | "unsupported" | "partial";
|
|
25
26
|
export interface ParseResult {
|
|
26
27
|
language: LanguageId;
|
|
27
28
|
file: string;
|
|
28
29
|
ok: boolean;
|
|
29
|
-
capabilities: Partial<Record<AdapterCapability,
|
|
30
|
+
capabilities: Partial<Record<AdapterCapability, AdapterCapabilityLevel>>;
|
|
30
31
|
symbols: LanguageSymbol[];
|
|
31
32
|
imports: LanguageImport[];
|
|
32
33
|
calls: LanguageCall[];
|
|
@@ -36,7 +37,7 @@ export interface ParseResult {
|
|
|
36
37
|
export interface LanguageAdapter {
|
|
37
38
|
id: LanguageId;
|
|
38
39
|
extensions: string[];
|
|
39
|
-
capabilities(): Partial<Record<AdapterCapability,
|
|
40
|
+
capabilities(): Partial<Record<AdapterCapability, AdapterCapabilityLevel>>;
|
|
40
41
|
parse(filePath: string, source: string): Promise<ParseResult>;
|
|
41
42
|
}
|
|
42
43
|
export declare function detectLanguage(filePath: string): LanguageId;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
2
|
import type { ModelProvider } from "../../ai/types.js";
|
|
3
|
-
export declare const AGENT_MCP_TOOL_NAMES: readonly ["project_context", "project_ask", "code_search", "file_read", "file_create", "file_edit", "agent_plan", "change_verify"];
|
|
3
|
+
export declare const AGENT_MCP_TOOL_NAMES: readonly ["project_context", "project_ask", "code_search", "file_read", "file_create", "file_edit", "approval_issue", "agent_plan", "change_verify"];
|
|
4
4
|
export type AgentMcpToolName = (typeof AGENT_MCP_TOOL_NAMES)[number];
|
|
5
5
|
export declare function listAgentMcpTools(): Tool[];
|
|
6
6
|
export declare function invokeAgentMcpTool(root: string, name: string, args: Record<string, unknown>, options?: {
|
|
@@ -4,6 +4,8 @@ import { buildAgentPlan } from "../../agent/plan.js";
|
|
|
4
4
|
import { executeAgentTool, newToolCall } from "../../agent/tools/index.js";
|
|
5
5
|
import { retrieveProjectContext } from "../../agent/context/retrieve.js";
|
|
6
6
|
import { summarizeProjectForChat } from "../../agent/chat/project-summary.js";
|
|
7
|
+
import { consumeApprovalGrant, issueApprovalGrant, hashPlanPayload, hashFileWritePlan, } from "../../product/approval/session.js";
|
|
8
|
+
import { assertForensicReadOnly } from "../../product/forensic/mode.js";
|
|
7
9
|
export const AGENT_MCP_TOOL_NAMES = [
|
|
8
10
|
"project_context",
|
|
9
11
|
"project_ask",
|
|
@@ -11,6 +13,7 @@ export const AGENT_MCP_TOOL_NAMES = [
|
|
|
11
13
|
"file_read",
|
|
12
14
|
"file_create",
|
|
13
15
|
"file_edit",
|
|
16
|
+
"approval_issue",
|
|
14
17
|
"agent_plan",
|
|
15
18
|
"change_verify",
|
|
16
19
|
];
|
|
@@ -58,30 +61,53 @@ export function listAgentMcpTools() {
|
|
|
58
61
|
},
|
|
59
62
|
{
|
|
60
63
|
name: "file_create",
|
|
61
|
-
description: "WRITE: Create file
|
|
64
|
+
description: "WRITE: Create file. Requires approvalToken from issueApprovalGrant / CLI --approve. Bare approved=true is rejected.",
|
|
62
65
|
inputSchema: {
|
|
63
66
|
type: "object",
|
|
64
67
|
properties: {
|
|
65
68
|
path: { type: "string" },
|
|
66
69
|
content: { type: "string" },
|
|
67
|
-
|
|
70
|
+
approvalToken: { type: "string" },
|
|
71
|
+
approved: {
|
|
72
|
+
type: "boolean",
|
|
73
|
+
description: "Legacy flag — insufficient alone; approvalToken required",
|
|
74
|
+
},
|
|
68
75
|
},
|
|
69
|
-
required: ["path", "content", "
|
|
76
|
+
required: ["path", "content", "approvalToken"],
|
|
70
77
|
additionalProperties: false,
|
|
71
78
|
},
|
|
72
79
|
},
|
|
73
80
|
{
|
|
74
81
|
name: "file_edit",
|
|
75
|
-
description: "WRITE: Edit file
|
|
82
|
+
description: "WRITE: Edit file. Requires approvalToken from issueApprovalGrant / CLI --approve.",
|
|
76
83
|
inputSchema: {
|
|
77
84
|
type: "object",
|
|
78
85
|
properties: {
|
|
79
86
|
path: { type: "string" },
|
|
80
87
|
content: { type: "string" },
|
|
81
88
|
oldContent: { type: "string" },
|
|
82
|
-
|
|
89
|
+
approvalToken: { type: "string" },
|
|
90
|
+
approved: {
|
|
91
|
+
type: "boolean",
|
|
92
|
+
description: "Legacy flag — insufficient alone; approvalToken required",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
required: ["path", "approvalToken"],
|
|
96
|
+
additionalProperties: false,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "approval_issue",
|
|
101
|
+
description: "TRUSTED: Issue a short-lived approval grant for writes. Requires AGENTDOCTOR_MCP_TRUSTED_APPROVE=1 in the MCP server environment (not model-settable).",
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: "object",
|
|
104
|
+
properties: {
|
|
105
|
+
action: { type: "string" },
|
|
106
|
+
resources: { type: "array", items: { type: "string" } },
|
|
107
|
+
planHash: { type: "string" },
|
|
108
|
+
risk: { type: "string", enum: ["LOW", "MEDIUM", "HIGH", "CRITICAL"] },
|
|
83
109
|
},
|
|
84
|
-
required: ["
|
|
110
|
+
required: ["action", "resources", "planHash"],
|
|
85
111
|
additionalProperties: false,
|
|
86
112
|
},
|
|
87
113
|
},
|
|
@@ -126,18 +152,6 @@ export async function invokeAgentMcpTool(root, name, args, options) {
|
|
|
126
152
|
};
|
|
127
153
|
}
|
|
128
154
|
const provider = options?.provider ?? createModelProvider(loadAiConfig());
|
|
129
|
-
if (provider.id === "none") {
|
|
130
|
-
return {
|
|
131
|
-
structured: {
|
|
132
|
-
ok: false,
|
|
133
|
-
error: {
|
|
134
|
-
code: "provider_none",
|
|
135
|
-
message: "AI chat is not configured. Set AGENTDOCTOR_AI_PROVIDER=mock (or openai-compatible). Silent mock fallback is disabled.",
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
isError: true,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
155
|
const chat = new ChatService({
|
|
142
156
|
root,
|
|
143
157
|
provider,
|
|
@@ -147,7 +161,7 @@ export async function invokeAgentMcpTool(root, name, args, options) {
|
|
|
147
161
|
const response = await chat.ask(question);
|
|
148
162
|
return {
|
|
149
163
|
structured: { ok: response.status === "ok", response },
|
|
150
|
-
isError: response.status !== "ok",
|
|
164
|
+
isError: response.status !== "ok" && response.status !== "provider-none",
|
|
151
165
|
};
|
|
152
166
|
}
|
|
153
167
|
finally {
|
|
@@ -163,11 +177,23 @@ export async function invokeAgentMcpTool(root, name, args, options) {
|
|
|
163
177
|
return { structured: result, isError: !result.ok };
|
|
164
178
|
}
|
|
165
179
|
case "file_create": {
|
|
166
|
-
|
|
180
|
+
assertForensicReadOnly(process.env.AGENTDOCTOR_FORENSIC_MODE === "1");
|
|
181
|
+
const token = typeof args.approvalToken === "string" ? args.approvalToken : undefined;
|
|
182
|
+
const pathArg = typeof args.path === "string" ? args.path : "";
|
|
183
|
+
const content = typeof args.content === "string" ? args.content : "";
|
|
184
|
+
const planHash = hashFileWritePlan("file_create", pathArg, content);
|
|
185
|
+
const gate = await consumeApprovalGrant({
|
|
186
|
+
root,
|
|
187
|
+
token,
|
|
188
|
+
action: "file_create",
|
|
189
|
+
resources: [pathArg],
|
|
190
|
+
requirePlanHash: planHash,
|
|
191
|
+
});
|
|
192
|
+
if (!gate.ok) {
|
|
167
193
|
return {
|
|
168
194
|
structured: {
|
|
169
195
|
ok: false,
|
|
170
|
-
error: { code: "approval_required", message:
|
|
196
|
+
error: { code: "approval_required", message: gate.reason },
|
|
171
197
|
},
|
|
172
198
|
isError: true,
|
|
173
199
|
};
|
|
@@ -176,11 +202,23 @@ export async function invokeAgentMcpTool(root, name, args, options) {
|
|
|
176
202
|
return { structured: result, isError: !result.ok };
|
|
177
203
|
}
|
|
178
204
|
case "file_edit": {
|
|
179
|
-
|
|
205
|
+
assertForensicReadOnly(process.env.AGENTDOCTOR_FORENSIC_MODE === "1");
|
|
206
|
+
const token = typeof args.approvalToken === "string" ? args.approvalToken : undefined;
|
|
207
|
+
const pathArg = typeof args.path === "string" ? args.path : "";
|
|
208
|
+
const content = typeof args.content === "string" ? args.content : "";
|
|
209
|
+
const planHash = hashFileWritePlan("file_edit", pathArg, content);
|
|
210
|
+
const gate = await consumeApprovalGrant({
|
|
211
|
+
root,
|
|
212
|
+
token,
|
|
213
|
+
action: "file_edit",
|
|
214
|
+
resources: [pathArg],
|
|
215
|
+
requirePlanHash: planHash,
|
|
216
|
+
});
|
|
217
|
+
if (!gate.ok) {
|
|
180
218
|
return {
|
|
181
219
|
structured: {
|
|
182
220
|
ok: false,
|
|
183
|
-
error: { code: "approval_required", message:
|
|
221
|
+
error: { code: "approval_required", message: gate.reason },
|
|
184
222
|
},
|
|
185
223
|
isError: true,
|
|
186
224
|
};
|
|
@@ -192,6 +230,54 @@ export async function invokeAgentMcpTool(root, name, args, options) {
|
|
|
192
230
|
}), { allowWrite: true, approvedByHuman: true });
|
|
193
231
|
return { structured: result, isError: !result.ok };
|
|
194
232
|
}
|
|
233
|
+
case "approval_issue": {
|
|
234
|
+
if (process.env.AGENTDOCTOR_MCP_TRUSTED_APPROVE !== "1") {
|
|
235
|
+
return {
|
|
236
|
+
structured: {
|
|
237
|
+
ok: false,
|
|
238
|
+
error: {
|
|
239
|
+
code: "trust_required",
|
|
240
|
+
message: "approval_issue requires AGENTDOCTOR_MCP_TRUSTED_APPROVE=1 in the MCP host environment",
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
isError: true,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const resources = Array.isArray(args.resources)
|
|
247
|
+
? args.resources.filter((r) => typeof r === "string")
|
|
248
|
+
: [];
|
|
249
|
+
const action = typeof args.action === "string" ? args.action : "file_edit";
|
|
250
|
+
const planHash = typeof args.planHash === "string" && args.planHash.length >= 8
|
|
251
|
+
? args.planHash
|
|
252
|
+
: hashPlanPayload(action);
|
|
253
|
+
const risk = (typeof args.risk === "string" ? args.risk : "MEDIUM");
|
|
254
|
+
try {
|
|
255
|
+
const grant = await issueApprovalGrant({
|
|
256
|
+
root,
|
|
257
|
+
action,
|
|
258
|
+
resources,
|
|
259
|
+
risk,
|
|
260
|
+
planHash,
|
|
261
|
+
actor: "mcp-trusted-host",
|
|
262
|
+
});
|
|
263
|
+
return {
|
|
264
|
+
structured: { ok: true, token: grant.token, expiresAt: grant.expiresAt, planHash },
|
|
265
|
+
isError: false,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
return {
|
|
270
|
+
structured: {
|
|
271
|
+
ok: false,
|
|
272
|
+
error: {
|
|
273
|
+
code: "invalid_grant",
|
|
274
|
+
message: error instanceof Error ? error.message : String(error),
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
isError: true,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
195
281
|
case "agent_plan": {
|
|
196
282
|
const goal = typeof args.goal === "string" ? args.goal : "";
|
|
197
283
|
const plan = await buildAgentPlan({ root, goal });
|
|
@@ -14,3 +14,6 @@ export declare function handleArchitectureCheckTool(rootInput: string): Promise<
|
|
|
14
14
|
export declare function handleProofInspectTool(rootInput: string, args: Record<string, unknown>): Promise<unknown>;
|
|
15
15
|
export declare function handleEvidenceInspectTool(rootInput: string, args: Record<string, unknown>): Promise<unknown>;
|
|
16
16
|
export declare function handleGraphQueryTool(rootInput: string, args: Record<string, unknown>): Promise<unknown>;
|
|
17
|
+
export declare function handleProjectDnaTool(rootInput: string): Promise<unknown>;
|
|
18
|
+
export declare function handleSoftwareMapTool(rootInput: string): Promise<unknown>;
|
|
19
|
+
export declare function handleWhatIfTool(rootInput: string, args: Record<string, unknown>): Promise<unknown>;
|
|
@@ -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 };
|