@praneeth_54/agentdoctor 1.1.1 → 2.0.1
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 +164 -7
- package/README.md +263 -513
- package/dist/agents/aider/adapter.d.ts +1 -0
- package/dist/agents/aider/adapter.js +1 -0
- package/dist/agents/aider/detector.d.ts +14 -0
- package/dist/agents/aider/detector.js +120 -0
- package/dist/agents/copilot/adapter.d.ts +1 -0
- package/dist/agents/copilot/adapter.js +1 -0
- package/dist/agents/copilot/detector.d.ts +13 -0
- package/dist/agents/copilot/detector.js +113 -0
- package/dist/agents/gemini/adapter.d.ts +1 -0
- package/dist/agents/gemini/adapter.js +1 -0
- package/dist/agents/gemini/detector.d.ts +13 -0
- package/dist/agents/gemini/detector.js +151 -0
- package/dist/agents/registry.js +13 -1
- package/dist/agents/types.d.ts +1 -1
- package/dist/agents/windsurf/adapter.d.ts +1 -0
- package/dist/agents/windsurf/adapter.js +1 -0
- package/dist/agents/windsurf/detector.d.ts +14 -0
- package/dist/agents/windsurf/detector.js +119 -0
- package/dist/architecture/c4.d.ts +22 -0
- package/dist/architecture/c4.js +62 -0
- package/dist/architecture/contract.d.ts +64 -0
- package/dist/architecture/contract.js +373 -0
- package/dist/assurance/change.d.ts +211 -0
- package/dist/assurance/change.js +624 -0
- package/dist/assurance/proof.d.ts +136 -0
- package/dist/assurance/proof.js +338 -0
- package/dist/auth/index.d.ts +81 -0
- package/dist/auth/index.js +130 -0
- package/dist/auth/rbac.d.ts +9 -0
- package/dist/auth/rbac.js +26 -0
- package/dist/cli/commands/architecture.d.ts +6 -0
- package/dist/cli/commands/architecture.js +109 -0
- package/dist/cli/commands/assurance.d.ts +67 -0
- package/dist/cli/commands/assurance.js +210 -0
- package/dist/cli/commands/brain.d.ts +12 -0
- package/dist/cli/commands/brain.js +162 -0
- package/dist/cli/commands/complete.d.ts +23 -0
- package/dist/cli/commands/complete.js +232 -0
- package/dist/cli/commands/explain.js +1 -1
- package/dist/cli/commands/fix.js +22 -2
- package/dist/cli/commands/mcp.d.ts +8 -0
- package/dist/cli/commands/mcp.js +14 -0
- package/dist/cli/commands/platform.d.ts +17 -0
- package/dist/cli/commands/platform.js +195 -0
- package/dist/cli/commands/policy-graph-run.d.ts +40 -0
- package/dist/cli/commands/policy-graph-run.js +200 -0
- package/dist/cli/commands/v2.d.ts +55 -0
- package/dist/cli/commands/v2.js +278 -0
- package/dist/cli/commands/workspace.d.ts +9 -0
- package/dist/cli/commands/workspace.js +82 -0
- package/dist/cli/program.js +1328 -3
- package/dist/constants.d.ts +3 -2
- package/dist/constants.js +5 -1
- package/dist/contracts/adapters.d.ts +7 -0
- package/dist/contracts/adapters.js +60 -0
- package/dist/contracts/index.d.ts +162 -0
- package/dist/contracts/index.js +18 -0
- package/dist/core/baseline/store.d.ts +48 -0
- package/dist/core/baseline/store.js +163 -0
- package/dist/core/brain-cli/service.d.ts +26 -0
- package/dist/core/brain-cli/service.js +175 -0
- package/dist/core/brain-product/init.d.ts +45 -0
- package/dist/core/brain-product/init.js +235 -0
- package/dist/core/changes/analyze.d.ts +41 -0
- package/dist/core/changes/analyze.js +223 -0
- package/dist/core/context-health/analyze.d.ts +15 -0
- package/dist/core/context-health/analyze.js +234 -0
- package/dist/core/fix/apply.d.ts +21 -4
- package/dist/core/fix/apply.js +118 -18
- package/dist/core/fix/backup.d.ts +30 -0
- package/dist/core/fix/backup.js +193 -0
- package/dist/core/fix/plan.d.ts +6 -3
- package/dist/core/fix/plan.js +113 -9
- package/dist/core/fix/render.d.ts +2 -0
- package/dist/core/fix/render.js +36 -21
- package/dist/core/fix/run.js +5 -1
- package/dist/core/fix/safe-target.d.ts +14 -0
- package/dist/core/fix/safe-target.js +81 -0
- package/dist/core/fix/types.d.ts +12 -1
- package/dist/core/fix/types.js +2 -0
- package/dist/core/fix/writers/claude-settings.js +3 -1
- package/dist/core/fix/writers/codex-config.d.ts +3 -0
- package/dist/core/fix/writers/codex-config.js +24 -3
- package/dist/core/fix/writers/cursorignore.js +3 -1
- package/dist/core/fix/writers/simple-ignore.d.ts +16 -0
- package/dist/core/fix/writers/simple-ignore.js +68 -0
- package/dist/core/monorepo/detect.d.ts +13 -0
- package/dist/core/monorepo/detect.js +121 -0
- package/dist/core/rules/build-context.js +22 -3
- package/dist/core/rules/context/generated-directory.js +11 -1
- package/dist/core/rules/context/large-instruction-file.js +6 -0
- package/dist/core/rules/context/large-log-file.js +16 -1
- package/dist/core/rules/ignore.d.ts +11 -1
- package/dist/core/rules/ignore.js +22 -1
- package/dist/core/rules/instructions/empty-instructions.js +7 -0
- package/dist/core/rules/security/env-file-exposure.js +23 -2
- package/dist/core/rules/security/private-key-file.js +1 -1
- package/dist/core/scanner/scan.js +1 -1
- package/dist/core/schemas/validate.d.ts +6 -0
- package/dist/core/schemas/validate.js +62 -0
- package/dist/core/scoring/compute-scores.d.ts +1 -1
- package/dist/core/scoring/compute-scores.js +5 -1
- package/dist/core/scoring/placeholder.d.ts +1 -1
- package/dist/core/scoring/placeholder.js +5 -1
- package/dist/core/secrets/scan.d.ts +29 -0
- package/dist/core/secrets/scan.js +172 -0
- package/dist/coverage/cobertura.d.ts +6 -0
- package/dist/coverage/cobertura.js +52 -0
- package/dist/coverage/istanbul.d.ts +7 -0
- package/dist/coverage/istanbul.js +134 -0
- package/dist/coverage/lcov.d.ts +7 -0
- package/dist/coverage/lcov.js +59 -0
- package/dist/coverage/load.d.ts +9 -0
- package/dist/coverage/load.js +39 -0
- package/dist/coverage/types.d.ts +23 -0
- package/dist/coverage/types.js +5 -0
- package/dist/dashboard/server.d.ts +24 -0
- package/dist/dashboard/server.js +402 -0
- package/dist/enforcement/runner.d.ts +67 -0
- package/dist/enforcement/runner.js +460 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +25 -0
- package/dist/integrations/github/pr-analyze.d.ts +28 -0
- package/dist/integrations/github/pr-analyze.js +98 -0
- package/dist/integrations/local-ai/provider.d.ts +45 -0
- package/dist/integrations/local-ai/provider.js +104 -0
- package/dist/intelligence/git/analyze.d.ts +34 -0
- package/dist/intelligence/git/analyze.js +136 -0
- package/dist/intelligence/graph/build.d.ts +15 -0
- package/dist/intelligence/graph/build.js +259 -0
- package/dist/intelligence/graph/incremental.d.ts +68 -0
- package/dist/intelligence/graph/incremental.js +234 -0
- package/dist/intelligence/resolve/imports.d.ts +36 -0
- package/dist/intelligence/resolve/imports.js +245 -0
- package/dist/knowledge/store.d.ts +25 -0
- package/dist/knowledge/store.js +91 -0
- package/dist/languages/go.d.ts +9 -0
- package/dist/languages/go.js +45 -0
- package/dist/languages/index.d.ts +11 -0
- package/dist/languages/index.js +76 -0
- package/dist/languages/php.d.ts +7 -0
- package/dist/languages/php.js +193 -0
- package/dist/languages/python.d.ts +7 -0
- package/dist/languages/python.js +150 -0
- package/dist/languages/types.d.ts +42 -0
- package/dist/languages/types.js +26 -0
- package/dist/languages/typescript.d.ts +3 -0
- package/dist/languages/typescript.js +95 -0
- package/dist/mcp/agentdoctor/server.d.ts +16 -0
- package/dist/mcp/agentdoctor/server.js +67 -0
- package/dist/mcp/brain/session.js +7 -7
- package/dist/mcp/intelligence/handlers.d.ts +16 -0
- package/dist/mcp/intelligence/handlers.js +364 -0
- package/dist/mcp/intelligence/path-safety.d.ts +9 -0
- package/dist/mcp/intelligence/path-safety.js +63 -0
- package/dist/mcp/intelligence/registry.d.ts +8 -0
- package/dist/mcp/intelligence/registry.js +228 -0
- package/dist/ops/health.d.ts +18 -0
- package/dist/ops/health.js +51 -0
- package/dist/platform/ai-quality/analyze.d.ts +9 -0
- package/dist/platform/ai-quality/analyze.js +65 -0
- package/dist/platform/architecture/drift.d.ts +17 -0
- package/dist/platform/architecture/drift.js +74 -0
- package/dist/platform/auth/local.d.ts +15 -0
- package/dist/platform/auth/local.js +58 -0
- package/dist/platform/context-security/analyze.d.ts +6 -0
- package/dist/platform/context-security/analyze.js +97 -0
- package/dist/platform/firewall/evaluate.d.ts +57 -0
- package/dist/platform/firewall/evaluate.js +359 -0
- package/dist/platform/graph/build.d.ts +6 -0
- package/dist/platform/graph/build.js +189 -0
- package/dist/platform/health/analyze.d.ts +7 -0
- package/dist/platform/health/analyze.js +192 -0
- package/dist/platform/index.d.ts +27 -0
- package/dist/platform/index.js +89 -0
- package/dist/platform/knowledge/analyze.d.ts +20 -0
- package/dist/platform/knowledge/analyze.js +96 -0
- package/dist/platform/provenance/build.d.ts +44 -0
- package/dist/platform/provenance/build.js +65 -0
- package/dist/platform/readiness/scorecard.d.ts +5 -0
- package/dist/platform/readiness/scorecard.js +98 -0
- package/dist/platform/refactor/impact.d.ts +20 -0
- package/dist/platform/refactor/impact.js +60 -0
- package/dist/platform/reports/export.d.ts +12 -0
- package/dist/platform/reports/export.js +92 -0
- package/dist/platform/security/redact.d.ts +15 -0
- package/dist/platform/security/redact.js +87 -0
- package/dist/platform/sessions/store.d.ts +32 -0
- package/dist/platform/sessions/store.js +97 -0
- package/dist/platform/store.d.ts +6 -0
- package/dist/platform/store.js +57 -0
- package/dist/platform/test-impact/analyze.d.ts +50 -0
- package/dist/platform/test-impact/analyze.js +350 -0
- package/dist/platform/time-machine/compare.d.ts +17 -0
- package/dist/platform/time-machine/compare.js +62 -0
- package/dist/platform/tokens/plan.d.ts +28 -0
- package/dist/platform/tokens/plan.js +108 -0
- package/dist/platform/types.d.ts +120 -0
- package/dist/platform/types.js +5 -0
- package/dist/plugins/runtime.d.ts +36 -0
- package/dist/plugins/runtime.js +101 -0
- package/dist/plugins/sdk.d.ts +29 -0
- package/dist/plugins/sdk.js +114 -0
- package/dist/policy/compose.d.ts +34 -0
- package/dist/policy/compose.js +118 -0
- package/dist/policy/packs.d.ts +9 -0
- package/dist/policy/packs.js +91 -0
- package/dist/reporters/terminal/report.js +38 -5
- package/dist/security/paths.d.ts +21 -0
- package/dist/security/paths.js +105 -0
- package/dist/storage/postgres.d.ts +26 -0
- package/dist/storage/postgres.js +90 -0
- package/dist/storage/provider.d.ts +30 -0
- package/dist/storage/provider.js +109 -0
- package/dist/storage/sqlite.d.ts +33 -0
- package/dist/storage/sqlite.js +77 -0
- package/dist/team/auth.d.ts +35 -0
- package/dist/team/auth.js +70 -0
- package/dist/types/index.d.ts +5 -1
- package/dist/workspace/index.d.ts +69 -0
- package/dist/workspace/index.js +220 -0
- package/package.json +15 -5
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface PluginAnalyzerNote {
|
|
2
|
+
pluginId: string;
|
|
3
|
+
severity: "info" | "warning";
|
|
4
|
+
message: string;
|
|
5
|
+
}
|
|
6
|
+
export interface PluginAnalyzerResult {
|
|
7
|
+
pluginId: string;
|
|
8
|
+
ok: boolean;
|
|
9
|
+
notes: PluginAnalyzerNote[];
|
|
10
|
+
error?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface PluginAnalyzerContext {
|
|
13
|
+
repoRoot: string;
|
|
14
|
+
/** Relative paths the host already knows about — plugins must not scan arbitrary FS */
|
|
15
|
+
knownRelativePaths: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
export type PluginAnalyzeFn = (ctx: PluginAnalyzerContext) => Promise<{
|
|
18
|
+
notes?: Array<{
|
|
19
|
+
severity?: string;
|
|
20
|
+
message: string;
|
|
21
|
+
}>;
|
|
22
|
+
}> | {
|
|
23
|
+
notes?: Array<{
|
|
24
|
+
severity?: string;
|
|
25
|
+
message: string;
|
|
26
|
+
}>;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Run analyzer-capability plugins with per-plugin error isolation and timeouts.
|
|
30
|
+
* Declarative JSON entry (`hooks.json`) or JS/MJS `analyze` export.
|
|
31
|
+
* No network APIs are provided; crashes do not abort other plugins.
|
|
32
|
+
*/
|
|
33
|
+
export declare function runPluginAnalyzers(rootInput: string, options?: {
|
|
34
|
+
knownRelativePaths?: string[];
|
|
35
|
+
timeoutMs?: number;
|
|
36
|
+
}): Promise<PluginAnalyzerResult[]>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { discoverPlugins } from "./sdk.js";
|
|
5
|
+
import { isPathInsideRoot, resolveRepoRoot } from "../utils/path.js";
|
|
6
|
+
/**
|
|
7
|
+
* Run analyzer-capability plugins with per-plugin error isolation and timeouts.
|
|
8
|
+
* Declarative JSON entry (`hooks.json`) or JS/MJS `analyze` export.
|
|
9
|
+
* No network APIs are provided; crashes do not abort other plugins.
|
|
10
|
+
*/
|
|
11
|
+
export async function runPluginAnalyzers(rootInput, options) {
|
|
12
|
+
const repoRoot = resolveRepoRoot(rootInput);
|
|
13
|
+
const plugins = await discoverPlugins(repoRoot);
|
|
14
|
+
const known = options?.knownRelativePaths ?? [];
|
|
15
|
+
const timeoutMs = options?.timeoutMs ?? 3_000;
|
|
16
|
+
const results = [];
|
|
17
|
+
for (const plugin of plugins) {
|
|
18
|
+
if (!plugin.ok) {
|
|
19
|
+
results.push({
|
|
20
|
+
pluginId: plugin.manifest.id,
|
|
21
|
+
ok: false,
|
|
22
|
+
notes: [],
|
|
23
|
+
error: plugin.errors.join("; ") || "invalid plugin",
|
|
24
|
+
});
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (!plugin.manifest.capabilities.includes("analyzer")) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
results.push(await runOneAnalyzer(repoRoot, plugin.manifest, plugin.root, known, timeoutMs));
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
results.push({
|
|
35
|
+
pluginId: plugin.manifest.id,
|
|
36
|
+
ok: false,
|
|
37
|
+
notes: [],
|
|
38
|
+
error: error instanceof Error ? error.message : String(error),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return results.sort((a, b) => a.pluginId.localeCompare(b.pluginId));
|
|
43
|
+
}
|
|
44
|
+
async function runOneAnalyzer(repoRoot, manifest, pluginRoot, knownRelativePaths, timeoutMs) {
|
|
45
|
+
const entry = manifest.entry ?? "hooks.json";
|
|
46
|
+
const entryPath = path.resolve(pluginRoot, entry);
|
|
47
|
+
if (!isPathInsideRoot(pluginRoot, entryPath)) {
|
|
48
|
+
return {
|
|
49
|
+
pluginId: manifest.id,
|
|
50
|
+
ok: false,
|
|
51
|
+
notes: [],
|
|
52
|
+
error: "plugin entry escapes plugin root",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const ctx = {
|
|
56
|
+
repoRoot,
|
|
57
|
+
knownRelativePaths,
|
|
58
|
+
};
|
|
59
|
+
if (entry.endsWith(".json")) {
|
|
60
|
+
const raw = JSON.parse(await fs.readFile(entryPath, "utf8"));
|
|
61
|
+
return normalizeNotes(manifest, raw.notes ?? []);
|
|
62
|
+
}
|
|
63
|
+
const mod = await withTimeout(import(pathToFileURL(entryPath).href), timeoutMs);
|
|
64
|
+
const analyze = mod.analyze ??
|
|
65
|
+
mod.default;
|
|
66
|
+
if (typeof analyze !== "function") {
|
|
67
|
+
return {
|
|
68
|
+
pluginId: manifest.id,
|
|
69
|
+
ok: false,
|
|
70
|
+
notes: [],
|
|
71
|
+
error: "plugin entry missing analyze() export",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const out = await withTimeout(Promise.resolve(analyze(ctx)), timeoutMs);
|
|
75
|
+
return normalizeNotes(manifest, out?.notes ?? []);
|
|
76
|
+
}
|
|
77
|
+
function normalizeNotes(manifest, notes) {
|
|
78
|
+
return {
|
|
79
|
+
pluginId: manifest.id,
|
|
80
|
+
ok: true,
|
|
81
|
+
notes: notes
|
|
82
|
+
.filter((n) => typeof n.message === "string" && n.message.trim().length > 0)
|
|
83
|
+
.map((n) => ({
|
|
84
|
+
pluginId: manifest.id,
|
|
85
|
+
severity: n.severity === "warning" ? "warning" : "info",
|
|
86
|
+
message: n.message.slice(0, 500),
|
|
87
|
+
})),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function withTimeout(promise, ms) {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const timer = setTimeout(() => reject(new Error(`plugin timed out after ${ms}ms`)), ms);
|
|
93
|
+
promise.then((value) => {
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
resolve(value);
|
|
96
|
+
}, (error) => {
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
reject(error);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type PluginCapability = "rule" | "reporter" | "analyzer";
|
|
2
|
+
export interface AgentDoctorPluginManifest {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
version: string;
|
|
6
|
+
apiVersion: "2.0";
|
|
7
|
+
capabilities: PluginCapability[];
|
|
8
|
+
/** Declared only — runtime does not grant network/fs beyond the sandbox root. */
|
|
9
|
+
requestedPermissions?: Array<"read-repo" | "none">;
|
|
10
|
+
entry?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface PluginLoadResult {
|
|
13
|
+
manifest: AgentDoctorPluginManifest;
|
|
14
|
+
root: string;
|
|
15
|
+
ok: boolean;
|
|
16
|
+
errors: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface PluginSandbox {
|
|
19
|
+
repoRoot: string;
|
|
20
|
+
pluginRoot: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function validatePluginManifest(raw: unknown): {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
manifest?: AgentDoctorPluginManifest;
|
|
25
|
+
errors: string[];
|
|
26
|
+
};
|
|
27
|
+
/** Discover plugins under .agentdoctor/plugins/<id>/plugin.json (validated, no network). */
|
|
28
|
+
export declare function discoverPlugins(rootInput: string): Promise<PluginLoadResult[]>;
|
|
29
|
+
export declare function createPluginSandbox(repoRoot: string, pluginRoot: string): PluginSandbox;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveRepoRoot, isPathInsideRoot } from "../utils/path.js";
|
|
4
|
+
const ALLOWED_CAPABILITIES = new Set(["rule", "reporter", "analyzer"]);
|
|
5
|
+
export function validatePluginManifest(raw) {
|
|
6
|
+
const errors = [];
|
|
7
|
+
if (!raw || typeof raw !== "object") {
|
|
8
|
+
return { ok: false, errors: ["manifest must be an object"] };
|
|
9
|
+
}
|
|
10
|
+
const m = raw;
|
|
11
|
+
if (typeof m.id !== "string" || !m.id.trim())
|
|
12
|
+
errors.push("id required");
|
|
13
|
+
if (typeof m.name !== "string" || !m.name.trim())
|
|
14
|
+
errors.push("name required");
|
|
15
|
+
if (typeof m.version !== "string" || !m.version.trim())
|
|
16
|
+
errors.push("version required");
|
|
17
|
+
if (m.apiVersion !== "2.0")
|
|
18
|
+
errors.push('apiVersion must be "2.0"');
|
|
19
|
+
if (!Array.isArray(m.capabilities) || m.capabilities.length === 0) {
|
|
20
|
+
errors.push("capabilities required");
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
for (const c of m.capabilities) {
|
|
24
|
+
if (!ALLOWED_CAPABILITIES.has(c)) {
|
|
25
|
+
errors.push(`unsupported capability: ${String(c)}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (errors.length > 0)
|
|
30
|
+
return { ok: false, errors };
|
|
31
|
+
const manifest = {
|
|
32
|
+
id: String(m.id),
|
|
33
|
+
name: String(m.name),
|
|
34
|
+
version: String(m.version),
|
|
35
|
+
apiVersion: "2.0",
|
|
36
|
+
capabilities: m.capabilities,
|
|
37
|
+
};
|
|
38
|
+
if (Array.isArray(m.requestedPermissions)) {
|
|
39
|
+
manifest.requestedPermissions = m.requestedPermissions;
|
|
40
|
+
}
|
|
41
|
+
if (typeof m.entry === "string") {
|
|
42
|
+
manifest.entry = m.entry;
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
ok: true,
|
|
46
|
+
errors: [],
|
|
47
|
+
manifest,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Discover plugins under .agentdoctor/plugins/<id>/plugin.json (validated, no network). */
|
|
51
|
+
export async function discoverPlugins(rootInput) {
|
|
52
|
+
const root = resolveRepoRoot(rootInput);
|
|
53
|
+
const pluginsRoot = path.join(root, ".agentdoctor", "plugins");
|
|
54
|
+
let dirs = [];
|
|
55
|
+
try {
|
|
56
|
+
const entries = await fs.readdir(pluginsRoot, { withFileTypes: true });
|
|
57
|
+
dirs = entries.filter((e) => e.isDirectory()).map((e) => path.join(pluginsRoot, e.name));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
const results = [];
|
|
63
|
+
for (const dir of dirs) {
|
|
64
|
+
if (!isPathInsideRoot(pluginsRoot, dir))
|
|
65
|
+
continue;
|
|
66
|
+
const manifestPath = path.join(dir, "plugin.json");
|
|
67
|
+
try {
|
|
68
|
+
const raw = JSON.parse(await fs.readFile(manifestPath, "utf8"));
|
|
69
|
+
const validated = validatePluginManifest(raw);
|
|
70
|
+
if (!validated.ok || !validated.manifest) {
|
|
71
|
+
results.push({
|
|
72
|
+
manifest: {
|
|
73
|
+
id: path.basename(dir),
|
|
74
|
+
name: path.basename(dir),
|
|
75
|
+
version: "0.0.0",
|
|
76
|
+
apiVersion: "2.0",
|
|
77
|
+
capabilities: ["analyzer"],
|
|
78
|
+
},
|
|
79
|
+
root: dir,
|
|
80
|
+
ok: false,
|
|
81
|
+
errors: validated.errors,
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
results.push({
|
|
86
|
+
manifest: validated.manifest,
|
|
87
|
+
root: dir,
|
|
88
|
+
ok: true,
|
|
89
|
+
errors: [],
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
results.push({
|
|
94
|
+
manifest: {
|
|
95
|
+
id: path.basename(dir),
|
|
96
|
+
name: path.basename(dir),
|
|
97
|
+
version: "0.0.0",
|
|
98
|
+
apiVersion: "2.0",
|
|
99
|
+
capabilities: ["analyzer"],
|
|
100
|
+
},
|
|
101
|
+
root: dir,
|
|
102
|
+
ok: false,
|
|
103
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return results.sort((a, b) => a.manifest.id.localeCompare(b.manifest.id));
|
|
108
|
+
}
|
|
109
|
+
export function createPluginSandbox(repoRoot, pluginRoot) {
|
|
110
|
+
return {
|
|
111
|
+
repoRoot: resolveRepoRoot(repoRoot),
|
|
112
|
+
pluginRoot: path.resolve(pluginRoot),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { FirewallPolicyDocument } from "../platform/firewall/evaluate.js";
|
|
2
|
+
export interface ComposedPolicyResult {
|
|
3
|
+
policy: FirewallPolicyDocument;
|
|
4
|
+
sources: Array<"builtin" | "repo">;
|
|
5
|
+
packName: string | null;
|
|
6
|
+
validationError?: string;
|
|
7
|
+
failClosed: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Merge builtin (global pack) + repo `.agentdoctor/policy.json`.
|
|
11
|
+
* Precedence: repo rules are prepended (evaluated first); repo defaultDecision /
|
|
12
|
+
* failClosed / shellAllowlist override builtin when present.
|
|
13
|
+
* Malformed repo policy → fail-closed deny-all (never silently ignore).
|
|
14
|
+
*/
|
|
15
|
+
export declare function composePolicy(options: {
|
|
16
|
+
root: string;
|
|
17
|
+
packName?: string;
|
|
18
|
+
}): Promise<ComposedPolicyResult>;
|
|
19
|
+
export declare function explainComposedDecision(options: {
|
|
20
|
+
command: string;
|
|
21
|
+
decision: string;
|
|
22
|
+
reason: string;
|
|
23
|
+
policyId?: string | null;
|
|
24
|
+
sources: Array<"builtin" | "repo">;
|
|
25
|
+
failClosed: boolean;
|
|
26
|
+
}): {
|
|
27
|
+
command: string;
|
|
28
|
+
decision: string;
|
|
29
|
+
reason: string;
|
|
30
|
+
policyId: string | null;
|
|
31
|
+
sources: Array<"builtin" | "repo">;
|
|
32
|
+
failClosed: boolean;
|
|
33
|
+
meaning: string;
|
|
34
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getPolicyPack, POLICY_PACKS } from "./packs.js";
|
|
4
|
+
import { resolveRepoRoot } from "../utils/path.js";
|
|
5
|
+
const REPO_POLICY_PATH = ".agentdoctor/policy.json";
|
|
6
|
+
function denyAll(reason) {
|
|
7
|
+
return {
|
|
8
|
+
version: "2.0",
|
|
9
|
+
defaultDecision: "block",
|
|
10
|
+
failClosed: true,
|
|
11
|
+
rules: [
|
|
12
|
+
{
|
|
13
|
+
id: "compose-fail-closed",
|
|
14
|
+
decision: "block",
|
|
15
|
+
reason,
|
|
16
|
+
riskLevel: "critical",
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function isFirewallPolicyDocument(value) {
|
|
22
|
+
if (!value || typeof value !== "object")
|
|
23
|
+
return false;
|
|
24
|
+
const doc = value;
|
|
25
|
+
if (doc.version !== "2.0")
|
|
26
|
+
return false;
|
|
27
|
+
if (typeof doc.defaultDecision !== "string")
|
|
28
|
+
return false;
|
|
29
|
+
if (!Array.isArray(doc.rules))
|
|
30
|
+
return false;
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Merge builtin (global pack) + repo `.agentdoctor/policy.json`.
|
|
35
|
+
* Precedence: repo rules are prepended (evaluated first); repo defaultDecision /
|
|
36
|
+
* failClosed / shellAllowlist override builtin when present.
|
|
37
|
+
* Malformed repo policy → fail-closed deny-all (never silently ignore).
|
|
38
|
+
*/
|
|
39
|
+
export async function composePolicy(options) {
|
|
40
|
+
const root = resolveRepoRoot(options.root);
|
|
41
|
+
const packName = options.packName ?? "baseline-safe";
|
|
42
|
+
const builtin = getPolicyPack(packName) ?? POLICY_PACKS["baseline-safe"];
|
|
43
|
+
const sources = ["builtin"];
|
|
44
|
+
const repoPath = path.join(root, REPO_POLICY_PATH);
|
|
45
|
+
let raw = null;
|
|
46
|
+
try {
|
|
47
|
+
raw = await fs.readFile(repoPath, "utf8");
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
raw = null;
|
|
51
|
+
}
|
|
52
|
+
if (raw === null) {
|
|
53
|
+
return {
|
|
54
|
+
policy: structuredClone(builtin),
|
|
55
|
+
sources,
|
|
56
|
+
packName,
|
|
57
|
+
failClosed: Boolean(builtin.failClosed),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = JSON.parse(raw);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return {
|
|
66
|
+
policy: denyAll("Malformed repo .agentdoctor/policy.json; fail-closed"),
|
|
67
|
+
sources: ["builtin", "repo"],
|
|
68
|
+
packName,
|
|
69
|
+
validationError: "Malformed JSON",
|
|
70
|
+
failClosed: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (!isFirewallPolicyDocument(parsed)) {
|
|
74
|
+
return {
|
|
75
|
+
policy: denyAll("Invalid repo .agentdoctor/policy.json shape; fail-closed"),
|
|
76
|
+
sources: ["builtin", "repo"],
|
|
77
|
+
packName,
|
|
78
|
+
validationError: "Invalid policy document",
|
|
79
|
+
failClosed: true,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
sources.push("repo");
|
|
83
|
+
const repoRules = Array.isArray(parsed.rules) ? parsed.rules : [];
|
|
84
|
+
const merged = {
|
|
85
|
+
version: "2.0",
|
|
86
|
+
defaultDecision: parsed.defaultDecision ?? builtin.defaultDecision,
|
|
87
|
+
failClosed: parsed.failClosed === true || builtin.failClosed === true,
|
|
88
|
+
...(parsed.shellAllowlist
|
|
89
|
+
? { shellAllowlist: parsed.shellAllowlist }
|
|
90
|
+
: builtin.shellAllowlist
|
|
91
|
+
? { shellAllowlist: builtin.shellAllowlist }
|
|
92
|
+
: {}),
|
|
93
|
+
// Repo rules first (higher precedence), then builtin.
|
|
94
|
+
rules: [...repoRules, ...builtin.rules],
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
policy: merged,
|
|
98
|
+
sources,
|
|
99
|
+
packName,
|
|
100
|
+
failClosed: Boolean(merged.failClosed),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export function explainComposedDecision(options) {
|
|
104
|
+
const meaning = options.decision === "allow"
|
|
105
|
+
? "Allowlisted or matched an allow rule — still evaluate-only unless agentdoctor run executes it."
|
|
106
|
+
: options.decision === "block" || options.decision === "deny-network"
|
|
107
|
+
? "Would be blocked by the AgentDoctor-controlled runner if executed through it."
|
|
108
|
+
: "Requires human approval or further policy before controlled execution.";
|
|
109
|
+
return {
|
|
110
|
+
command: options.command,
|
|
111
|
+
decision: options.decision,
|
|
112
|
+
reason: options.reason,
|
|
113
|
+
policyId: options.policyId ?? null,
|
|
114
|
+
sources: options.sources,
|
|
115
|
+
failClosed: options.failClosed,
|
|
116
|
+
meaning,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FirewallPolicyDocument } from "../platform/firewall/evaluate.js";
|
|
2
|
+
/**
|
|
3
|
+
* Named policy packs for local evaluation. Packs are declarative documents —
|
|
4
|
+
* loading a pack does not enable runtime interception unless an AgentDoctor
|
|
5
|
+
* enforcement boundary (controlled runner / CI wrapper) is used.
|
|
6
|
+
*/
|
|
7
|
+
export declare const POLICY_PACKS: Record<string, FirewallPolicyDocument>;
|
|
8
|
+
export declare function listPolicyPacks(): string[];
|
|
9
|
+
export declare function getPolicyPack(name: string): FirewallPolicyDocument | null;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named policy packs for local evaluation. Packs are declarative documents —
|
|
3
|
+
* loading a pack does not enable runtime interception unless an AgentDoctor
|
|
4
|
+
* enforcement boundary (controlled runner / CI wrapper) is used.
|
|
5
|
+
*/
|
|
6
|
+
export const POLICY_PACKS = {
|
|
7
|
+
"baseline-safe": {
|
|
8
|
+
version: "2.0",
|
|
9
|
+
defaultDecision: "require-approval",
|
|
10
|
+
failClosed: false,
|
|
11
|
+
shellAllowlist: ["npm test", "npm run test", "vitest", "eslint", "tsc", "prettier"],
|
|
12
|
+
rules: [
|
|
13
|
+
{
|
|
14
|
+
id: "pack-block-secrets",
|
|
15
|
+
actionTypes: ["file-modify", "file-delete", "secret-access"],
|
|
16
|
+
match: { pathContains: [".env", "id_rsa", "credentials", ".pem"] },
|
|
17
|
+
decision: "block",
|
|
18
|
+
reason: "baseline-safe: secret-sensitive path",
|
|
19
|
+
riskLevel: "critical",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "pack-block-destructive",
|
|
23
|
+
actionTypes: ["shell"],
|
|
24
|
+
// Force-push is gated as require-approval (human gate), not a hard block —
|
|
25
|
+
// builtin shell blocks already cover rm -rf; keep drop table here.
|
|
26
|
+
match: { commandContains: ["rm -rf", "drop table"] },
|
|
27
|
+
decision: "block",
|
|
28
|
+
reason: "baseline-safe: destructive operation",
|
|
29
|
+
riskLevel: "critical",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "pack-force-push-approval",
|
|
33
|
+
actionTypes: ["shell"],
|
|
34
|
+
match: { commandContains: ["git push --force", "git push -f"] },
|
|
35
|
+
decision: "require-approval",
|
|
36
|
+
reason: "baseline-safe: force-push requires approval",
|
|
37
|
+
riskLevel: "high",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "pack-allow-safe-dev-paths",
|
|
41
|
+
actionTypes: ["file-modify", "file-create"],
|
|
42
|
+
match: { pathPrefix: ["src/", "tests/", "docs/"] },
|
|
43
|
+
decision: "allow",
|
|
44
|
+
reason: "baseline-safe: allowlisted development paths",
|
|
45
|
+
riskLevel: "low",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "pack-deny-prod-deploy",
|
|
49
|
+
actionTypes: ["deploy", "infra"],
|
|
50
|
+
decision: "block",
|
|
51
|
+
reason: "baseline-safe: production/infra changes require out-of-band process",
|
|
52
|
+
riskLevel: "critical",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "pack-network-default-deny",
|
|
56
|
+
actionTypes: ["network"],
|
|
57
|
+
decision: "deny-network",
|
|
58
|
+
reason: "baseline-safe: network access denied by default",
|
|
59
|
+
riskLevel: "high",
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
"fail-closed-ci": {
|
|
64
|
+
version: "2.0",
|
|
65
|
+
defaultDecision: "block",
|
|
66
|
+
failClosed: true,
|
|
67
|
+
shellAllowlist: ["npm test", "npm run verify", "vitest"],
|
|
68
|
+
rules: [
|
|
69
|
+
{
|
|
70
|
+
id: "ci-deny-network",
|
|
71
|
+
actionTypes: ["network"],
|
|
72
|
+
decision: "block",
|
|
73
|
+
reason: "fail-closed-ci: network blocked",
|
|
74
|
+
riskLevel: "high",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "ci-deny-deploy",
|
|
78
|
+
actionTypes: ["deploy", "infra"],
|
|
79
|
+
decision: "block",
|
|
80
|
+
reason: "fail-closed-ci: deploy blocked",
|
|
81
|
+
riskLevel: "critical",
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
export function listPolicyPacks() {
|
|
87
|
+
return Object.keys(POLICY_PACKS);
|
|
88
|
+
}
|
|
89
|
+
export function getPolicyPack(name) {
|
|
90
|
+
return POLICY_PACKS[name] ?? null;
|
|
91
|
+
}
|
|
@@ -7,7 +7,39 @@ import { formatPackageManager, formatPackageManagers } from "../../detectors/pac
|
|
|
7
7
|
import { colors, symbolFail, symbolInfo, symbolOk, symbolWarn } from "../../utils/colors.js";
|
|
8
8
|
import { sanitizeTerminalText } from "../../security/redaction.js";
|
|
9
9
|
const DEFAULT_FINDING_LIMIT = 12;
|
|
10
|
-
const LIMITED_ANALYSIS_NOTE = "
|
|
10
|
+
const LIMITED_ANALYSIS_NOTE = "Agent-specific exposure checks are limited (no supported agent config); repository hygiene still applied.";
|
|
11
|
+
/** Agents with an implemented Safe Fix writer (detect-only agents are excluded). */
|
|
12
|
+
const SAFE_FIX_AGENT_IDS = new Set([
|
|
13
|
+
"cursor",
|
|
14
|
+
"claude-code",
|
|
15
|
+
"codex",
|
|
16
|
+
"gemini-cli",
|
|
17
|
+
"aider",
|
|
18
|
+
]);
|
|
19
|
+
function renderAgentCoverageSummary(result) {
|
|
20
|
+
const configured = result.agents.filter((a) => a.configured);
|
|
21
|
+
const detectedOnly = result.agents.filter((a) => a.detected && !a.configured);
|
|
22
|
+
const withFix = configured.filter((a) => SAFE_FIX_AGENT_IDS.has(a.id));
|
|
23
|
+
const detectOnlyConfigured = configured.filter((a) => !SAFE_FIX_AGENT_IDS.has(a.id));
|
|
24
|
+
const lines = [];
|
|
25
|
+
lines.push("");
|
|
26
|
+
if (result.agentSecurityAnalysis === "limited") {
|
|
27
|
+
lines.push(colors.dim(" Coverage: 0 configured · universal hygiene still runs (limited agent analysis)"));
|
|
28
|
+
return lines;
|
|
29
|
+
}
|
|
30
|
+
const parts = [`${configured.length} configured`];
|
|
31
|
+
if (detectedOnly.length > 0) {
|
|
32
|
+
parts.push(`${detectedOnly.length} detected-only`);
|
|
33
|
+
}
|
|
34
|
+
if (withFix.length > 0) {
|
|
35
|
+
parts.push(`Safe Fix: ${withFix.map((a) => a.displayName).join(", ")}`);
|
|
36
|
+
}
|
|
37
|
+
if (detectOnlyConfigured.length > 0) {
|
|
38
|
+
parts.push(`detect-only (no Safe Fix writer): ${detectOnlyConfigured.map((a) => a.displayName).join(", ")}`);
|
|
39
|
+
}
|
|
40
|
+
lines.push(colors.dim(` Coverage: ${parts.join(" · ")}`));
|
|
41
|
+
return lines;
|
|
42
|
+
}
|
|
11
43
|
function groupFindings(findings) {
|
|
12
44
|
return {
|
|
13
45
|
critical: findings.filter((f) => f.severity === "critical"),
|
|
@@ -152,6 +184,7 @@ export function renderTerminalReport(result, options = {}) {
|
|
|
152
184
|
}
|
|
153
185
|
}
|
|
154
186
|
}
|
|
187
|
+
lines.push(...renderAgentCoverageSummary(result));
|
|
155
188
|
lines.push("");
|
|
156
189
|
const grouped = groupFindings(result.findings);
|
|
157
190
|
const total = result.summary.total;
|
|
@@ -164,10 +197,10 @@ export function renderTerminalReport(result, options = {}) {
|
|
|
164
197
|
}
|
|
165
198
|
if (total === 0) {
|
|
166
199
|
if (limited) {
|
|
167
|
-
lines.push(` ${
|
|
200
|
+
lines.push(` ${symbolOk()} No repository-hygiene findings — agent readiness not scored (no supported agent config)`);
|
|
168
201
|
lines.push("");
|
|
169
|
-
lines.push(colors.dim("
|
|
170
|
-
lines.push(colors.dim("
|
|
202
|
+
lines.push(colors.dim(" Optional next: add project agent config (`.cursor/`, `CLAUDE.md`, `AGENTS.md`, `.github/copilot-instructions.md`, `.windsurf/rules/`, `GEMINI.md`, or `.aider.conf.yml`)"));
|
|
203
|
+
lines.push(colors.dim(" so agent exposure checks run fully. CLI, Action, and Project Brain MCP still work without an agent."));
|
|
171
204
|
}
|
|
172
205
|
else {
|
|
173
206
|
lines.push(` ${symbolOk()} No findings`);
|
|
@@ -217,7 +250,7 @@ export function renderTerminalReport(result, options = {}) {
|
|
|
217
250
|
lines.push("");
|
|
218
251
|
if (result.scoringAvailable && result.scores) {
|
|
219
252
|
if (limited) {
|
|
220
|
-
lines.push(` Readiness: n/a — configure Cursor, Claude Code, or
|
|
253
|
+
lines.push(` Readiness: n/a — configure a supported agent (Cursor, Claude Code, Codex, Copilot, Windsurf, Gemini CLI, or Aider) before treating scores as agent readiness`);
|
|
221
254
|
lines.push(colors.dim(` Numeric scores remain in JSON (overall ${result.scores.overall}/100) for repository-risk findings only`));
|
|
222
255
|
}
|
|
223
256
|
else {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare class PathEscapeError extends Error {
|
|
2
|
+
readonly code = "PATH_ESCAPE";
|
|
3
|
+
constructor(message?: string);
|
|
4
|
+
}
|
|
5
|
+
declare function tryDecodeUri(value: string): string | null;
|
|
6
|
+
/**
|
|
7
|
+
* Lexical hostility checks shared by resolve/assert helpers.
|
|
8
|
+
* Does not leak the candidate path in thrown messages.
|
|
9
|
+
*/
|
|
10
|
+
export declare function rejectHostilePathInput(candidate: string): void;
|
|
11
|
+
/**
|
|
12
|
+
* Resolve a user-supplied path and require it to remain inside the repository root.
|
|
13
|
+
* Rejects `..`, URI-encoded traversal, null bytes, Windows-drive absolutes, and
|
|
14
|
+
* symlink realpath escapes. Error messages never echo the hostile candidate.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveSafeRepoPath(rootInput: string, candidate: string): string;
|
|
17
|
+
/** Assert `candidate` resolves inside `root`; throws PathEscapeError otherwise. */
|
|
18
|
+
export declare function assertInsideRepo(rootInput: string, candidate: string): void;
|
|
19
|
+
/** Resolve safely and return a POSIX path relative to the repo root. */
|
|
20
|
+
export declare function safeRelPath(rootInput: string, absoluteOrRel: string): string;
|
|
21
|
+
export { tryDecodeUri as tryDecodeUriComponent };
|