@praneeth_54/agentdoctor 1.1.1 → 2.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.
Files changed (170) hide show
  1. package/CHANGELOG.md +95 -7
  2. package/README.md +163 -538
  3. package/dist/agents/aider/adapter.d.ts +1 -0
  4. package/dist/agents/aider/adapter.js +1 -0
  5. package/dist/agents/aider/detector.d.ts +14 -0
  6. package/dist/agents/aider/detector.js +120 -0
  7. package/dist/agents/copilot/adapter.d.ts +1 -0
  8. package/dist/agents/copilot/adapter.js +1 -0
  9. package/dist/agents/copilot/detector.d.ts +13 -0
  10. package/dist/agents/copilot/detector.js +113 -0
  11. package/dist/agents/gemini/adapter.d.ts +1 -0
  12. package/dist/agents/gemini/adapter.js +1 -0
  13. package/dist/agents/gemini/detector.d.ts +13 -0
  14. package/dist/agents/gemini/detector.js +151 -0
  15. package/dist/agents/registry.js +13 -1
  16. package/dist/agents/types.d.ts +1 -1
  17. package/dist/agents/windsurf/adapter.d.ts +1 -0
  18. package/dist/agents/windsurf/adapter.js +1 -0
  19. package/dist/agents/windsurf/detector.d.ts +14 -0
  20. package/dist/agents/windsurf/detector.js +119 -0
  21. package/dist/architecture/c4.d.ts +22 -0
  22. package/dist/architecture/c4.js +62 -0
  23. package/dist/cli/commands/brain.d.ts +12 -0
  24. package/dist/cli/commands/brain.js +162 -0
  25. package/dist/cli/commands/complete.d.ts +23 -0
  26. package/dist/cli/commands/complete.js +232 -0
  27. package/dist/cli/commands/explain.js +1 -1
  28. package/dist/cli/commands/fix.js +22 -2
  29. package/dist/cli/commands/mcp.d.ts +8 -0
  30. package/dist/cli/commands/mcp.js +14 -0
  31. package/dist/cli/commands/platform.d.ts +15 -0
  32. package/dist/cli/commands/platform.js +191 -0
  33. package/dist/cli/commands/v2.d.ts +55 -0
  34. package/dist/cli/commands/v2.js +278 -0
  35. package/dist/cli/program.js +809 -2
  36. package/dist/constants.d.ts +3 -2
  37. package/dist/constants.js +5 -1
  38. package/dist/contracts/adapters.d.ts +7 -0
  39. package/dist/contracts/adapters.js +60 -0
  40. package/dist/contracts/index.d.ts +162 -0
  41. package/dist/contracts/index.js +18 -0
  42. package/dist/core/baseline/store.d.ts +48 -0
  43. package/dist/core/baseline/store.js +163 -0
  44. package/dist/core/brain-cli/service.d.ts +26 -0
  45. package/dist/core/brain-cli/service.js +175 -0
  46. package/dist/core/brain-product/init.d.ts +45 -0
  47. package/dist/core/brain-product/init.js +232 -0
  48. package/dist/core/changes/analyze.d.ts +41 -0
  49. package/dist/core/changes/analyze.js +223 -0
  50. package/dist/core/context-health/analyze.d.ts +15 -0
  51. package/dist/core/context-health/analyze.js +234 -0
  52. package/dist/core/fix/apply.d.ts +21 -4
  53. package/dist/core/fix/apply.js +118 -18
  54. package/dist/core/fix/backup.d.ts +30 -0
  55. package/dist/core/fix/backup.js +193 -0
  56. package/dist/core/fix/plan.d.ts +6 -3
  57. package/dist/core/fix/plan.js +113 -9
  58. package/dist/core/fix/render.d.ts +2 -0
  59. package/dist/core/fix/render.js +36 -21
  60. package/dist/core/fix/run.js +5 -1
  61. package/dist/core/fix/safe-target.d.ts +14 -0
  62. package/dist/core/fix/safe-target.js +81 -0
  63. package/dist/core/fix/types.d.ts +12 -1
  64. package/dist/core/fix/types.js +2 -0
  65. package/dist/core/fix/writers/claude-settings.js +3 -1
  66. package/dist/core/fix/writers/codex-config.d.ts +3 -0
  67. package/dist/core/fix/writers/codex-config.js +24 -3
  68. package/dist/core/fix/writers/cursorignore.js +3 -1
  69. package/dist/core/fix/writers/simple-ignore.d.ts +16 -0
  70. package/dist/core/fix/writers/simple-ignore.js +68 -0
  71. package/dist/core/monorepo/detect.d.ts +13 -0
  72. package/dist/core/monorepo/detect.js +121 -0
  73. package/dist/core/rules/build-context.js +22 -3
  74. package/dist/core/rules/context/generated-directory.js +11 -1
  75. package/dist/core/rules/context/large-instruction-file.js +6 -0
  76. package/dist/core/rules/context/large-log-file.js +16 -1
  77. package/dist/core/rules/ignore.d.ts +11 -1
  78. package/dist/core/rules/ignore.js +22 -1
  79. package/dist/core/rules/instructions/empty-instructions.js +7 -0
  80. package/dist/core/rules/security/env-file-exposure.js +23 -2
  81. package/dist/core/rules/security/private-key-file.js +1 -1
  82. package/dist/core/scanner/scan.js +1 -1
  83. package/dist/core/schemas/validate.d.ts +6 -0
  84. package/dist/core/schemas/validate.js +62 -0
  85. package/dist/core/scoring/compute-scores.d.ts +1 -1
  86. package/dist/core/scoring/compute-scores.js +5 -1
  87. package/dist/core/scoring/placeholder.d.ts +1 -1
  88. package/dist/core/scoring/placeholder.js +5 -1
  89. package/dist/core/secrets/scan.d.ts +23 -0
  90. package/dist/core/secrets/scan.js +172 -0
  91. package/dist/dashboard/server.d.ts +24 -0
  92. package/dist/dashboard/server.js +386 -0
  93. package/dist/enforcement/runner.d.ts +23 -0
  94. package/dist/enforcement/runner.js +56 -0
  95. package/dist/index.d.ts +13 -0
  96. package/dist/index.js +12 -0
  97. package/dist/integrations/github/pr-analyze.d.ts +28 -0
  98. package/dist/integrations/github/pr-analyze.js +98 -0
  99. package/dist/integrations/local-ai/provider.d.ts +45 -0
  100. package/dist/integrations/local-ai/provider.js +104 -0
  101. package/dist/intelligence/git/analyze.d.ts +34 -0
  102. package/dist/intelligence/git/analyze.js +136 -0
  103. package/dist/intelligence/graph/build.d.ts +13 -0
  104. package/dist/intelligence/graph/build.js +215 -0
  105. package/dist/knowledge/store.d.ts +25 -0
  106. package/dist/knowledge/store.js +91 -0
  107. package/dist/mcp/agentdoctor/server.d.ts +16 -0
  108. package/dist/mcp/agentdoctor/server.js +67 -0
  109. package/dist/mcp/brain/session.js +7 -7
  110. package/dist/mcp/intelligence/handlers.d.ts +11 -0
  111. package/dist/mcp/intelligence/handlers.js +175 -0
  112. package/dist/mcp/intelligence/path-safety.d.ts +11 -0
  113. package/dist/mcp/intelligence/path-safety.js +82 -0
  114. package/dist/mcp/intelligence/registry.d.ts +8 -0
  115. package/dist/mcp/intelligence/registry.js +160 -0
  116. package/dist/ops/health.d.ts +18 -0
  117. package/dist/ops/health.js +51 -0
  118. package/dist/platform/ai-quality/analyze.d.ts +9 -0
  119. package/dist/platform/ai-quality/analyze.js +65 -0
  120. package/dist/platform/architecture/drift.d.ts +17 -0
  121. package/dist/platform/architecture/drift.js +74 -0
  122. package/dist/platform/auth/local.d.ts +15 -0
  123. package/dist/platform/auth/local.js +58 -0
  124. package/dist/platform/context-security/analyze.d.ts +6 -0
  125. package/dist/platform/context-security/analyze.js +97 -0
  126. package/dist/platform/firewall/evaluate.d.ts +54 -0
  127. package/dist/platform/firewall/evaluate.js +308 -0
  128. package/dist/platform/graph/build.d.ts +6 -0
  129. package/dist/platform/graph/build.js +193 -0
  130. package/dist/platform/health/analyze.d.ts +7 -0
  131. package/dist/platform/health/analyze.js +186 -0
  132. package/dist/platform/index.d.ts +27 -0
  133. package/dist/platform/index.js +87 -0
  134. package/dist/platform/knowledge/analyze.d.ts +20 -0
  135. package/dist/platform/knowledge/analyze.js +96 -0
  136. package/dist/platform/provenance/build.d.ts +44 -0
  137. package/dist/platform/provenance/build.js +65 -0
  138. package/dist/platform/readiness/scorecard.d.ts +5 -0
  139. package/dist/platform/readiness/scorecard.js +98 -0
  140. package/dist/platform/refactor/impact.d.ts +20 -0
  141. package/dist/platform/refactor/impact.js +60 -0
  142. package/dist/platform/reports/export.d.ts +12 -0
  143. package/dist/platform/reports/export.js +92 -0
  144. package/dist/platform/security/redact.d.ts +15 -0
  145. package/dist/platform/security/redact.js +87 -0
  146. package/dist/platform/sessions/store.d.ts +32 -0
  147. package/dist/platform/sessions/store.js +97 -0
  148. package/dist/platform/store.d.ts +6 -0
  149. package/dist/platform/store.js +57 -0
  150. package/dist/platform/test-impact/analyze.d.ts +32 -0
  151. package/dist/platform/test-impact/analyze.js +164 -0
  152. package/dist/platform/time-machine/compare.d.ts +17 -0
  153. package/dist/platform/time-machine/compare.js +62 -0
  154. package/dist/platform/tokens/plan.d.ts +28 -0
  155. package/dist/platform/tokens/plan.js +89 -0
  156. package/dist/platform/types.d.ts +111 -0
  157. package/dist/platform/types.js +5 -0
  158. package/dist/plugins/runtime.d.ts +36 -0
  159. package/dist/plugins/runtime.js +101 -0
  160. package/dist/plugins/sdk.d.ts +29 -0
  161. package/dist/plugins/sdk.js +114 -0
  162. package/dist/policy/packs.d.ts +9 -0
  163. package/dist/policy/packs.js +59 -0
  164. package/dist/reporters/terminal/report.js +38 -5
  165. package/dist/storage/provider.d.ts +35 -0
  166. package/dist/storage/provider.js +103 -0
  167. package/dist/team/auth.d.ts +35 -0
  168. package/dist/team/auth.js +70 -0
  169. package/dist/types/index.d.ts +5 -1
  170. package/package.json +8 -4
@@ -0,0 +1,172 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { resolveRepoRoot, toPosixRelative } from "../../utils/path.js";
4
+ const SECRET_PATTERNS = [
5
+ {
6
+ id: "aws-access-key",
7
+ re: /\bAKIA[0-9A-Z]{16}\b/g,
8
+ severity: "critical",
9
+ },
10
+ {
11
+ id: "generic-api-key",
12
+ re: /\b(?:api[_-]?key|secret[_-]?key|access[_-]?token)\s*[:=]\s*['"]?[A-Za-z0-9_-]{16,}['"]?/gi,
13
+ severity: "warning",
14
+ },
15
+ {
16
+ id: "private-key-header",
17
+ re: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g,
18
+ severity: "critical",
19
+ },
20
+ {
21
+ id: "github-pat",
22
+ re: /\bghp_[A-Za-z0-9]{36}\b/g,
23
+ severity: "critical",
24
+ },
25
+ ];
26
+ const SKIP_DIR = new Set([
27
+ "node_modules",
28
+ ".git",
29
+ "dist",
30
+ "coverage",
31
+ ".agentdoctor",
32
+ "vendor",
33
+ ".next",
34
+ ]);
35
+ const TEXT_EXT = new Set([
36
+ ".ts",
37
+ ".tsx",
38
+ ".js",
39
+ ".jsx",
40
+ ".mjs",
41
+ ".cjs",
42
+ ".json",
43
+ ".yml",
44
+ ".yaml",
45
+ ".md",
46
+ ".env",
47
+ ".txt",
48
+ ".toml",
49
+ ".ini",
50
+ ".cfg",
51
+ ".py",
52
+ ".go",
53
+ ".rs",
54
+ ".java",
55
+ ".kt",
56
+ ".php",
57
+ ".rb",
58
+ ".sh",
59
+ ]);
60
+ function redactMatch(line, match) {
61
+ const idx = line.indexOf(match);
62
+ if (idx < 0) {
63
+ return "[REDACTED]";
64
+ }
65
+ const before = line.slice(0, idx).slice(-40);
66
+ const after = line.slice(idx + match.length).slice(0, 20);
67
+ return `${before}[REDACTED]${after}`.trim();
68
+ }
69
+ async function walkFiles(root, dir, out, limit) {
70
+ if (out.length >= limit)
71
+ return;
72
+ let entries;
73
+ try {
74
+ entries = await fs.readdir(dir, { withFileTypes: true });
75
+ }
76
+ catch {
77
+ return;
78
+ }
79
+ for (const entry of entries) {
80
+ if (out.length >= limit)
81
+ return;
82
+ if (entry.name.startsWith(".") && entry.name !== ".env" && entry.name !== ".env.local") {
83
+ if (SKIP_DIR.has(entry.name))
84
+ continue;
85
+ }
86
+ if (SKIP_DIR.has(entry.name))
87
+ continue;
88
+ const absolute = path.join(dir, entry.name);
89
+ if (entry.isSymbolicLink())
90
+ continue;
91
+ if (entry.isDirectory()) {
92
+ await walkFiles(root, absolute, out, limit);
93
+ continue;
94
+ }
95
+ if (!entry.isFile())
96
+ continue;
97
+ const ext = path.extname(entry.name).toLowerCase();
98
+ if (!TEXT_EXT.has(ext) && !entry.name.startsWith(".env"))
99
+ continue;
100
+ out.push(absolute);
101
+ }
102
+ }
103
+ /**
104
+ * Opt-in content secret scan. Findings never include raw secret values.
105
+ */
106
+ export async function scanSecrets(options) {
107
+ const root = resolveRepoRoot(options.root);
108
+ if (!options.enabled) {
109
+ return {
110
+ root,
111
+ enabled: false,
112
+ filesScanned: 0,
113
+ findings: [],
114
+ limitations: ["Content secret scanning disabled (pass --secrets to enable)"],
115
+ };
116
+ }
117
+ const maxFiles = options.maxFiles ?? 400;
118
+ const files = [];
119
+ await walkFiles(root, root, files, maxFiles);
120
+ const findings = [];
121
+ const limitations = [];
122
+ if (files.length >= maxFiles) {
123
+ limitations.push(`Scan capped at ${maxFiles} files`);
124
+ }
125
+ for (const absolute of files) {
126
+ let content;
127
+ try {
128
+ const st = await fs.stat(absolute);
129
+ if (st.size > 512 * 1024)
130
+ continue;
131
+ content = await fs.readFile(absolute, "utf8");
132
+ }
133
+ catch {
134
+ continue;
135
+ }
136
+ const lines = content.split(/\r?\n/);
137
+ const rel = toPosixRelative(root, absolute);
138
+ for (let i = 0; i < lines.length; i += 1) {
139
+ const line = lines[i] ?? "";
140
+ for (const pattern of SECRET_PATTERNS) {
141
+ pattern.re.lastIndex = 0;
142
+ let match;
143
+ while ((match = pattern.re.exec(line)) !== null) {
144
+ findings.push({
145
+ file: rel,
146
+ line: i + 1,
147
+ ruleId: pattern.id,
148
+ redactedSnippet: redactMatch(line, match[0] ?? ""),
149
+ severity: pattern.severity,
150
+ });
151
+ if (findings.length >= 200) {
152
+ limitations.push("Finding cap reached (200)");
153
+ return {
154
+ root,
155
+ enabled: true,
156
+ filesScanned: files.length,
157
+ findings,
158
+ limitations,
159
+ };
160
+ }
161
+ }
162
+ }
163
+ }
164
+ }
165
+ return {
166
+ root,
167
+ enabled: true,
168
+ filesScanned: files.length,
169
+ findings,
170
+ limitations,
171
+ };
172
+ }
@@ -0,0 +1,24 @@
1
+ export interface DashboardServerOptions {
2
+ root: string;
3
+ host?: string;
4
+ port?: number;
5
+ /** Read-only by default; mutating routes are never registered. */
6
+ readOnly?: boolean;
7
+ /**
8
+ * Unsafe opt-in to bind outside loopback. Not an enterprise security boundary.
9
+ * Local ?user= role selection remains spoofable.
10
+ */
11
+ allowNonLoopback?: boolean;
12
+ }
13
+ export declare function isLoopbackHost(host: string): boolean;
14
+ /**
15
+ * Local read-only HTTP dashboard. Never applies Safe Fix or mutates the repo.
16
+ * Defaults to loopback binding; non-loopback requires explicit allowNonLoopback.
17
+ */
18
+ export declare function startDashboardServer(options: DashboardServerOptions): Promise<{
19
+ host: string;
20
+ port: number;
21
+ close: () => Promise<void>;
22
+ }>;
23
+ /** Write a static snapshot HTML for offline viewing (optional helper). */
24
+ export declare function writeDashboardSnapshot(rootInput: string, outputPath: string): Promise<string>;
@@ -0,0 +1,386 @@
1
+ import http from "node:http";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { getBrainStatus } from "../core/brain-cli/service.js";
5
+ import { listFixAudits } from "../core/fix/backup.js";
6
+ import { listNamedBaselines } from "../core/baseline/store.js";
7
+ import { detectMonorepo } from "../core/monorepo/detect.js";
8
+ import { scan } from "../core/scanner/scan.js";
9
+ import { resolveRepoRoot } from "../utils/path.js";
10
+ import { agentRegistry } from "../agents/registry.js";
11
+ import { listSessions } from "../platform/sessions/store.js";
12
+ import { platformDir, readJsonIfExists } from "../platform/store.js";
13
+ import { canAccess, loadLocalAuthConfig, resolveRole } from "../platform/auth/local.js";
14
+ import { EVALUATE_ONLY } from "../platform/firewall/evaluate.js";
15
+ import { redactSecrets, sanitizeFindingsForExport } from "../platform/security/redact.js";
16
+ import { buildIntelligenceGraph } from "../intelligence/graph/build.js";
17
+ import { analyzeGitIntelligence } from "../intelligence/git/analyze.js";
18
+ import { buildC4Views } from "../architecture/c4.js";
19
+ import { listKnowledge } from "../knowledge/store.js";
20
+ import { CONTRACTS_VERSION } from "../contracts/index.js";
21
+ import { collectOpsHealth } from "../ops/health.js";
22
+ import { listPolicyPacks } from "../policy/packs.js";
23
+ import { sanitizeForOutput } from "../utils/path.js";
24
+ function safeJsonError(error) {
25
+ const raw = error instanceof Error ? error.message : String(error);
26
+ const { text } = redactSecrets(sanitizeForOutput(raw));
27
+ return text.slice(0, 240);
28
+ }
29
+ function pathnameLooksHostile(pathname) {
30
+ const lower = pathname.toLowerCase();
31
+ const decoded = (() => {
32
+ try {
33
+ return decodeURIComponent(pathname);
34
+ }
35
+ catch {
36
+ return pathname;
37
+ }
38
+ })();
39
+ return (pathname.includes("\0") ||
40
+ decoded.includes("\0") ||
41
+ pathname.includes("..") ||
42
+ decoded.includes("..") ||
43
+ lower.includes("%2e%2e") ||
44
+ lower.includes("%2e.") ||
45
+ lower.includes(".%2e") ||
46
+ pathname.includes("\\") ||
47
+ decoded.includes("\\"));
48
+ }
49
+ function redactGraphSamples(nodes) {
50
+ return nodes.map((n) => {
51
+ const next = { ...n };
52
+ if (typeof next.label === "string") {
53
+ next.label = redactSecrets(next.label).text;
54
+ }
55
+ if (typeof next.path === "string") {
56
+ next.path = redactSecrets(next.path).text;
57
+ }
58
+ return next;
59
+ });
60
+ }
61
+ export function isLoopbackHost(host) {
62
+ const normalized = host.trim().toLowerCase();
63
+ return (normalized === "127.0.0.1" ||
64
+ normalized === "::1" ||
65
+ normalized === "localhost" ||
66
+ normalized === "0:0:0:0:0:0:0:1");
67
+ }
68
+ function htmlPage() {
69
+ return `<!doctype html>
70
+ <html lang="en">
71
+ <head>
72
+ <meta charset="utf-8" />
73
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
74
+ <title>AgentDoctor Dashboard</title>
75
+ <style>
76
+ :root { --bg:#0f1419; --fg:#e7ecf1; --muted:#9aa7b5; --accent:#3d9cfd; --card:#1a222c; }
77
+ * { box-sizing: border-box; }
78
+ body { margin:0; font-family: ui-sans-serif, system-ui, sans-serif; background:var(--bg); color:var(--fg); }
79
+ header { padding:1.25rem 1.5rem; border-bottom:1px solid #243040; }
80
+ h1 { margin:0; font-size:1.25rem; letter-spacing:0.02em; }
81
+ p { color:var(--muted); margin:0.35rem 0 0; }
82
+ main { padding:1.5rem; display:grid; gap:1rem; max-width:1100px; margin:0 auto; }
83
+ section { background:var(--card); border-radius:10px; padding:1rem 1.1rem; }
84
+ h2 { margin:0 0 0.75rem; font-size:1rem; }
85
+ pre { white-space:pre-wrap; word-break:break-word; font-size:0.85rem; color:#d5dde6; }
86
+ .muted { color:var(--muted); }
87
+ a { color:var(--accent); }
88
+ .notice { border-left:3px solid var(--accent); padding-left:0.75rem; margin-top:0.75rem; }
89
+ </style>
90
+ </head>
91
+ <body>
92
+ <header>
93
+ <h1>AgentDoctor 2.0</h1>
94
+ <p>Local read-only dashboard — Safety + Brain + intelligence. No cloud writes.</p>
95
+ <p class="notice muted">Action Policy Evaluator is evaluate-only. Local ?user= is not authentication. Team local-dev auth is separate and not SSO.</p>
96
+ </header>
97
+ <main>
98
+ <nav aria-label="Views" style="display:flex;flex-wrap:wrap;gap:0.75rem;font-size:0.9rem">
99
+ <a href="#overview">Overview</a>
100
+ <a href="#scan">Safety scan</a>
101
+ <a href="#platform">Platform</a>
102
+ <a href="#brain">Brain</a>
103
+ <a href="#graph">Graph</a>
104
+ <a href="#knowledge">Knowledge</a>
105
+ <a href="#meta">Audit / baselines</a>
106
+ </nav>
107
+ <section id="overview">
108
+ <h2>Overview</h2>
109
+ <pre id="status" class="muted">Loading…</pre>
110
+ </section>
111
+ <section id="scan">
112
+ <h2>Safety scan</h2>
113
+ <pre id="scanBody" class="muted">Loading…</pre>
114
+ </section>
115
+ <section id="platform">
116
+ <h2>Platform snapshot (incl. test-impact)</h2>
117
+ <pre id="platformBody" class="muted">Loading…</pre>
118
+ </section>
119
+ <section id="brain">
120
+ <h2>Project Brain</h2>
121
+ <pre id="brainBody" class="muted">Loading…</pre>
122
+ </section>
123
+ <section id="graph">
124
+ <h2>Intelligence graph / git / C4</h2>
125
+ <pre id="graphBody" class="muted">Loading…</pre>
126
+ </section>
127
+ <section id="knowledge">
128
+ <h2>Governed knowledge</h2>
129
+ <pre id="knowledgeBody" class="muted">Loading…</pre>
130
+ </section>
131
+ <section id="meta">
132
+ <h2>Safe Fix audit / baselines / sessions</h2>
133
+ <pre id="metaBody" class="muted">Loading…</pre>
134
+ </section>
135
+ </main>
136
+ <script>
137
+ async function load() {
138
+ const [status, scan, brain, meta, platform, graph, knowledge] = await Promise.all([
139
+ fetch('/api/status').then(r => r.json()),
140
+ fetch('/api/scan').then(r => r.json()),
141
+ fetch('/api/brain').then(r => r.json()),
142
+ fetch('/api/meta').then(r => r.json()),
143
+ fetch('/api/platform').then(r => r.json()),
144
+ fetch('/api/v2/graph').then(r => r.json()),
145
+ fetch('/api/v2/knowledge').then(r => r.json()),
146
+ ]);
147
+ document.getElementById('status').textContent = JSON.stringify(status, null, 2);
148
+ document.getElementById('scanBody').textContent = JSON.stringify(scan, null, 2);
149
+ document.getElementById('brainBody').textContent = JSON.stringify(brain, null, 2);
150
+ document.getElementById('metaBody').textContent = JSON.stringify(meta, null, 2);
151
+ document.getElementById('platformBody').textContent = JSON.stringify(platform, null, 2);
152
+ document.getElementById('graphBody').textContent = JSON.stringify(graph, null, 2);
153
+ document.getElementById('knowledgeBody').textContent = JSON.stringify(knowledge, null, 2);
154
+ }
155
+ load().catch(err => {
156
+ document.getElementById('status').textContent = String(err);
157
+ });
158
+ </script>
159
+ </body>
160
+ </html>`;
161
+ }
162
+ function sendJson(res, status, body) {
163
+ const payload = JSON.stringify(body, null, 2);
164
+ res.writeHead(status, {
165
+ "Content-Type": "application/json; charset=utf-8",
166
+ "Cache-Control": "no-store",
167
+ });
168
+ res.end(payload);
169
+ }
170
+ /**
171
+ * Local read-only HTTP dashboard. Never applies Safe Fix or mutates the repo.
172
+ * Defaults to loopback binding; non-loopback requires explicit allowNonLoopback.
173
+ */
174
+ export async function startDashboardServer(options) {
175
+ const root = resolveRepoRoot(options.root);
176
+ const host = options.host ?? "127.0.0.1";
177
+ const port = options.port ?? 8787;
178
+ if (!isLoopbackHost(host) && options.allowNonLoopback !== true) {
179
+ throw new Error(`Refusing to bind dashboard to non-loopback host "${host}". Pass allowNonLoopback / --allow-non-loopback to override (unsafe; not an enterprise security boundary).`);
180
+ }
181
+ const server = http.createServer(async (req, res) => {
182
+ try {
183
+ const rawUrl = req.url ?? "/";
184
+ if (pathnameLooksHostile(rawUrl.split("?")[0] ?? rawUrl)) {
185
+ sendJson(res, 400, { error: "invalid path" });
186
+ return;
187
+ }
188
+ const url = new URL(rawUrl, `http://${host}:${port}`);
189
+ if (req.method !== "GET") {
190
+ sendJson(res, 405, { error: "read-only dashboard; GET only" });
191
+ return;
192
+ }
193
+ if (pathnameLooksHostile(url.pathname)) {
194
+ sendJson(res, 400, { error: "invalid path" });
195
+ return;
196
+ }
197
+ if (url.pathname === "/" || url.pathname === "/index.html") {
198
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
199
+ res.end(htmlPage());
200
+ return;
201
+ }
202
+ if (url.pathname === "/api/status") {
203
+ const mono = await detectMonorepo(root);
204
+ const health = await collectOpsHealth(root);
205
+ sendJson(res, 200, {
206
+ root,
207
+ readOnly: true,
208
+ host,
209
+ loopbackOnly: isLoopbackHost(host),
210
+ contractsVersion: CONTRACTS_VERSION,
211
+ roleNote: "Local ?user= role selection is not authentication",
212
+ actionPolicyNote: EVALUATE_ONLY,
213
+ adapters: agentRegistry.map((a) => a.id),
214
+ policyPacks: listPolicyPacks(),
215
+ ops: health,
216
+ monorepo: {
217
+ isMonorepo: mono.isMonorepo,
218
+ tool: mono.tool,
219
+ packages: mono.packages.length,
220
+ },
221
+ });
222
+ return;
223
+ }
224
+ if (url.pathname === "/api/scan") {
225
+ const result = await scan({ cwd: root });
226
+ sendJson(res, 200, {
227
+ summary: result.summary,
228
+ scores: result.scores,
229
+ findingCount: result.findings.length,
230
+ findings: result.findings.slice(0, 50).map((f) => ({
231
+ id: f.id,
232
+ ruleId: f.ruleId,
233
+ severity: f.severity,
234
+ title: f.title,
235
+ path: f.evidence?.path ?? null,
236
+ })),
237
+ });
238
+ return;
239
+ }
240
+ if (url.pathname === "/api/brain") {
241
+ sendJson(res, 200, await getBrainStatus(root));
242
+ return;
243
+ }
244
+ if (url.pathname === "/api/meta") {
245
+ const audits = await listFixAudits(root);
246
+ const baselines = await listNamedBaselines(root);
247
+ const sessions = await listSessions(root);
248
+ sendJson(res, 200, {
249
+ fixAudits: audits.slice(0, 20).map((a) => ({
250
+ id: a.id,
251
+ mode: a.mode,
252
+ createdAt: a.createdAt,
253
+ entryCount: a.entries.length,
254
+ })),
255
+ baselines,
256
+ sessions: sessions.slice(0, 50),
257
+ });
258
+ return;
259
+ }
260
+ if (url.pathname === "/api/platform") {
261
+ const auth = await loadLocalAuthConfig(root);
262
+ const role = resolveRole(auth, url.searchParams.get("user") ?? undefined);
263
+ if (!canAccess(role, "read-findings")) {
264
+ sendJson(res, 403, { error: "forbidden" });
265
+ return;
266
+ }
267
+ const snapshot = await readJsonIfExists(path.join(platformDir(root), "snapshots", "latest.json"));
268
+ const canExport = canAccess(role, "export-reports");
269
+ const safeFindings = snapshot
270
+ ? sanitizeFindingsForExport(snapshot.findings).slice(0, 30)
271
+ : [];
272
+ sendJson(res, 200, {
273
+ role,
274
+ hasSnapshot: snapshot !== null,
275
+ findingCount: snapshot?.findings.length ?? 0,
276
+ graphNodes: snapshot?.graph.nodes.length ?? 0,
277
+ readiness: snapshot?.readiness.categories ?? [],
278
+ testImpact: snapshot?.testImpact
279
+ ? {
280
+ gitAvailable: snapshot.testImpact.gitAvailable,
281
+ changedFiles: snapshot.testImpact.changedFiles.length,
282
+ recommendedTests: snapshot.testImpact.recommendedTests.length,
283
+ missingTestWarnings: snapshot.testImpact.missingTestWarnings.length,
284
+ relatedModules: snapshot.testImpact.relatedModules,
285
+ skipRisk: snapshot.testImpact.skipRisk,
286
+ sampleRecommended: canExport
287
+ ? snapshot.testImpact.recommendedTests.slice(0, 20)
288
+ : [],
289
+ sampleMissing: canExport
290
+ ? snapshot.testImpact.missingTestWarnings.slice(0, 10)
291
+ : [],
292
+ }
293
+ : null,
294
+ limitations: [
295
+ ...(snapshot?.limitations ?? [
296
+ "No platform snapshot yet — run: agentdoctor platform scan",
297
+ ]),
298
+ "Local ?user= role selection is not authentication — spoofable on shared hosts",
299
+ EVALUATE_ONLY,
300
+ ],
301
+ sampleFindings: canExport
302
+ ? safeFindings.map((f) => ({
303
+ id: f.id,
304
+ module: f.module,
305
+ severity: f.severity,
306
+ title: f.title,
307
+ }))
308
+ : [],
309
+ });
310
+ return;
311
+ }
312
+ if (url.pathname === "/api/v2/graph") {
313
+ const graph = await buildIntelligenceGraph({ root, mode: "auto" });
314
+ sendJson(res, 200, {
315
+ contractsVersion: CONTRACTS_VERSION,
316
+ builder: graph.builder,
317
+ nodeCount: graph.nodes.length,
318
+ edgeCount: graph.edges.length,
319
+ sampleNodes: redactGraphSamples(graph.nodes.slice(0, 40)),
320
+ sampleEdges: graph.edges.slice(0, 40),
321
+ limitations: graph.limitations,
322
+ });
323
+ return;
324
+ }
325
+ if (url.pathname === "/api/v2/health") {
326
+ sendJson(res, 200, await analyzeGitIntelligence(root));
327
+ return;
328
+ }
329
+ if (url.pathname === "/api/v2/c4") {
330
+ const graph = await buildIntelligenceGraph({ root, mode: "auto" });
331
+ sendJson(res, 200, {
332
+ views: buildC4Views(graph),
333
+ label: "Inferred/proposed — not approved architecture facts",
334
+ });
335
+ return;
336
+ }
337
+ if (url.pathname === "/api/v2/knowledge") {
338
+ const records = await listKnowledge(root);
339
+ sendJson(res, 200, {
340
+ count: records.length,
341
+ records: records.slice(0, 100).map((r) => ({
342
+ id: r.id,
343
+ title: r.title,
344
+ status: r.status,
345
+ version: r.version,
346
+ })),
347
+ });
348
+ return;
349
+ }
350
+ if (url.pathname === "/api/v2/projects" || url.pathname === "/api/v2/workspaces") {
351
+ sendJson(res, 200, {
352
+ mode: "local-single-repo",
353
+ root,
354
+ notice: "Multi-repo workspaces are partially implemented (storage abstraction). Cloud org workspaces unsupported.",
355
+ });
356
+ return;
357
+ }
358
+ sendJson(res, 404, { error: "not found" });
359
+ }
360
+ catch (error) {
361
+ sendJson(res, 500, { error: safeJsonError(error) });
362
+ }
363
+ });
364
+ await new Promise((resolve, reject) => {
365
+ server.listen(port, host, () => resolve());
366
+ server.on("error", reject);
367
+ });
368
+ const address = server.address();
369
+ const boundPort = typeof address === "object" && address && typeof address.port === "number"
370
+ ? address.port
371
+ : port;
372
+ return {
373
+ host,
374
+ port: boundPort,
375
+ close: async () => new Promise((resolve, reject) => {
376
+ server.close((err) => (err ? reject(err) : resolve()));
377
+ }),
378
+ };
379
+ }
380
+ /** Write a static snapshot HTML for offline viewing (optional helper). */
381
+ export async function writeDashboardSnapshot(rootInput, outputPath) {
382
+ const absolute = path.resolve(outputPath);
383
+ await fs.mkdir(path.dirname(absolute), { recursive: true });
384
+ await fs.writeFile(absolute, htmlPage(), "utf8");
385
+ return absolute;
386
+ }
@@ -0,0 +1,23 @@
1
+ import type { PolicyDecisionContract } from "../contracts/index.js";
2
+ export interface EnforcementResult {
3
+ decision: PolicyDecisionContract;
4
+ enforced: boolean;
5
+ notice: string;
6
+ }
7
+ /**
8
+ * AgentDoctor-controlled command runner.
9
+ * Only this boundary can mark execution as blocked-by-enforcement when it refuses to run.
10
+ */
11
+ export declare function runControlledCommand(options: {
12
+ root: string;
13
+ command: string;
14
+ failClosed?: boolean;
15
+ /** When true, actually attempt execution only if policy allows — still default false. */
16
+ executeIfAllowed?: boolean;
17
+ }): Promise<EnforcementResult>;
18
+ /** Append-only tamper-evident audit chain (HMAC over previous hash). */
19
+ export declare function appendAuditChain(previousHash: string | null, record: unknown, secret?: string): {
20
+ hash: string;
21
+ payload: string;
22
+ };
23
+ export declare function auditRecordId(parts: string): string;
@@ -0,0 +1,56 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { createHmac } from "node:crypto";
3
+ import { evaluateAgentAction, EVALUATE_ONLY } from "../platform/firewall/evaluate.js";
4
+ /**
5
+ * AgentDoctor-controlled command runner.
6
+ * Only this boundary can mark execution as blocked-by-enforcement when it refuses to run.
7
+ */
8
+ export async function runControlledCommand(options) {
9
+ const action = {
10
+ actionId: randomUUID(),
11
+ agentId: "enforcement-runner",
12
+ timestamp: new Date().toISOString(),
13
+ type: "shell",
14
+ params: { command: options.command },
15
+ repositoryRoot: options.root,
16
+ };
17
+ const verdict = await evaluateAgentAction(options.root, action, {
18
+ failClosed: options.failClosed === true,
19
+ });
20
+ const blocked = verdict.decision === "block" || verdict.decision === "deny-network";
21
+ const decision = {
22
+ actionId: verdict.actionId,
23
+ decision: verdict.decision,
24
+ reason: verdict.reason,
25
+ ...(verdict.policyId ? { policyId: verdict.policyId } : {}),
26
+ policyVersion: "2.0",
27
+ inputClassification: "shell",
28
+ evidence: [{ id: "ev_policy", kind: "observed", detail: verdict.reason }],
29
+ riskLevel: verdict.riskLevel,
30
+ executionStatus: blocked
31
+ ? "blocked-by-enforcement"
32
+ : options.executeIfAllowed && verdict.decision === "allow"
33
+ ? "not-executed"
34
+ : "not-executed",
35
+ approvalStatus: verdict.approvalStatus,
36
+ timestamp: new Date().toISOString(),
37
+ };
38
+ return {
39
+ decision,
40
+ enforced: blocked,
41
+ notice: blocked
42
+ ? `Blocked by AgentDoctor-controlled runner. ${EVALUATE_ONLY}`
43
+ : `Policy ${verdict.decision}; runner did not execute. ${EVALUATE_ONLY}`,
44
+ };
45
+ }
46
+ /** Append-only tamper-evident audit chain (HMAC over previous hash). */
47
+ export function appendAuditChain(previousHash, record, secret = "local-dev-audit-key") {
48
+ const body = JSON.stringify(record);
49
+ const hash = createHmac("sha256", secret)
50
+ .update(`${previousHash ?? "GENESIS"}:${body}`)
51
+ .digest("hex");
52
+ return { hash, payload: body };
53
+ }
54
+ export function auditRecordId(parts) {
55
+ return `audit_${createHash("sha1").update(parts).digest("hex").slice(0, 12)}`;
56
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,15 @@
1
+ export { CONTRACTS_VERSION, DEFAULT_FEATURE_FLAGS } from "./contracts/index.js";
2
+ export { buildIntelligenceGraph } from "./intelligence/graph/build.js";
3
+ export { analyzeGitIntelligence } from "./intelligence/git/analyze.js";
4
+ export { buildC4Views } from "./architecture/c4.js";
5
+ export { createKnowledgeRecord, listKnowledge, transitionKnowledge, retrieveAuthoritative, } from "./knowledge/store.js";
6
+ export { FilesystemStorageProvider, MemoryStorageProvider } from "./storage/provider.js";
7
+ export { runControlledCommand } from "./enforcement/runner.js";
8
+ export { registerLocalDevUser, authenticateLocalDev, authorize } from "./team/auth.js";
9
+ export { runProjectInit, listProposals, reviewProposal } from "./core/brain-product/init.js";
10
+ export { collectOpsHealth } from "./ops/health.js";
11
+ export { listPolicyPacks, getPolicyPack } from "./policy/packs.js";
12
+ export { listIntelligenceMcpTools, INTELLIGENCE_MCP_TOOL_NAMES, } from "./mcp/intelligence/registry.js";
1
13
  export { scan } from "./core/scanner/scan.js";
2
14
  export { verify } from "./core/verify/verify.js";
3
15
  export { compareFindings } from "./core/verify/compare.js";
@@ -12,6 +24,7 @@ export { ruleRegistry, getRuleById } from "./core/rules/registry.js";
12
24
  export { runRules } from "./core/rules/run-rules.js";
13
25
  export { buildFixPlan } from "./core/fix/plan.js";
14
26
  export { applyFixPlan } from "./core/fix/apply.js";
27
+ export type { ApplyFixPlanOptions } from "./core/fix/apply.js";
15
28
  export { runFix } from "./core/fix/run.js";
16
29
  export { evaluatePolicy, evaluateScanPolicy, evaluateVerifyPolicy, parseFailOnRules, parseSeverityGate, } from "./core/policy/evaluate.js";
17
30
  export type { PolicyOptions, PolicyViolation, PolicyViolationCode, PolicyInput, } from "./core/policy/evaluate.js";