@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.
- package/CHANGELOG.md +95 -7
- package/README.md +163 -538
- 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/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 +15 -0
- package/dist/cli/commands/platform.js +191 -0
- package/dist/cli/commands/v2.d.ts +55 -0
- package/dist/cli/commands/v2.js +278 -0
- package/dist/cli/program.js +809 -2
- 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 +232 -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 +23 -0
- package/dist/core/secrets/scan.js +172 -0
- package/dist/dashboard/server.d.ts +24 -0
- package/dist/dashboard/server.js +386 -0
- package/dist/enforcement/runner.d.ts +23 -0
- package/dist/enforcement/runner.js +56 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +12 -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 +13 -0
- package/dist/intelligence/graph/build.js +215 -0
- package/dist/knowledge/store.d.ts +25 -0
- package/dist/knowledge/store.js +91 -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 +11 -0
- package/dist/mcp/intelligence/handlers.js +175 -0
- package/dist/mcp/intelligence/path-safety.d.ts +11 -0
- package/dist/mcp/intelligence/path-safety.js +82 -0
- package/dist/mcp/intelligence/registry.d.ts +8 -0
- package/dist/mcp/intelligence/registry.js +160 -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 +54 -0
- package/dist/platform/firewall/evaluate.js +308 -0
- package/dist/platform/graph/build.d.ts +6 -0
- package/dist/platform/graph/build.js +193 -0
- package/dist/platform/health/analyze.d.ts +7 -0
- package/dist/platform/health/analyze.js +186 -0
- package/dist/platform/index.d.ts +27 -0
- package/dist/platform/index.js +87 -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 +32 -0
- package/dist/platform/test-impact/analyze.js +164 -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 +89 -0
- package/dist/platform/types.d.ts +111 -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/packs.d.ts +9 -0
- package/dist/policy/packs.js +59 -0
- package/dist/reporters/terminal/report.js +38 -5
- package/dist/storage/provider.d.ts +35 -0
- package/dist/storage/provider.js +103 -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/package.json +8 -4
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { resolveRepoRoot } from "../../utils/path.js";
|
|
3
|
+
function git(root, args) {
|
|
4
|
+
const r = spawnSync("git", args, { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 });
|
|
5
|
+
return { ok: r.status === 0, out: typeof r.stdout === "string" ? r.stdout : "" };
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Module J — Repository time machine (git-backed).
|
|
9
|
+
*/
|
|
10
|
+
export async function compareCommits(options) {
|
|
11
|
+
const root = resolveRepoRoot(options.root);
|
|
12
|
+
const left = options.left.trim();
|
|
13
|
+
const right = options.right.trim();
|
|
14
|
+
if (!isSafeGitRef(left) || !isSafeGitRef(right)) {
|
|
15
|
+
return {
|
|
16
|
+
root,
|
|
17
|
+
left,
|
|
18
|
+
right,
|
|
19
|
+
changedFiles: [],
|
|
20
|
+
summary: "Invalid git ref",
|
|
21
|
+
limitations: ["Refs must be non-empty and must not start with '-'"],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const probe = git(root, ["rev-parse", "--is-inside-work-tree"]);
|
|
25
|
+
if (!probe.ok || probe.out.trim() !== "true") {
|
|
26
|
+
return {
|
|
27
|
+
root,
|
|
28
|
+
left,
|
|
29
|
+
right,
|
|
30
|
+
changedFiles: [],
|
|
31
|
+
summary: "Git not available",
|
|
32
|
+
limitations: ["Requires a git repository"],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const diff = git(root, ["diff", "--name-only", `${left}...${right}`]);
|
|
36
|
+
const files = diff.ok
|
|
37
|
+
? diff.out
|
|
38
|
+
.split(/\r?\n/)
|
|
39
|
+
.map((l) => l.trim())
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
: [];
|
|
42
|
+
return {
|
|
43
|
+
root,
|
|
44
|
+
left,
|
|
45
|
+
right,
|
|
46
|
+
changedFiles: files,
|
|
47
|
+
summary: `${files.length} path(s) differ between ${left} and ${right}`,
|
|
48
|
+
limitations: [
|
|
49
|
+
"Architecture/risk trend overlays are extension points; this MVP lists path diffs",
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function isSafeGitRef(ref) {
|
|
54
|
+
return Boolean(ref) && !ref.includes("\0") && !ref.startsWith("-") && !/[\r\n]/.test(ref);
|
|
55
|
+
}
|
|
56
|
+
export async function listRecentCommits(rootInput, limit = 20) {
|
|
57
|
+
const root = resolveRepoRoot(rootInput);
|
|
58
|
+
const log = git(root, ["log", `-${limit}`, "--pretty=format:%H %s"]);
|
|
59
|
+
if (!log.ok)
|
|
60
|
+
return [];
|
|
61
|
+
return log.out.split(/\r?\n/).filter(Boolean);
|
|
62
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { RepositoryGraph } from "../types.js";
|
|
2
|
+
export interface ContextPlan {
|
|
3
|
+
root: string;
|
|
4
|
+
budgetTokens: number;
|
|
5
|
+
estimatedTokens: number;
|
|
6
|
+
selected: Array<{
|
|
7
|
+
path: string;
|
|
8
|
+
tokens: number;
|
|
9
|
+
relevance: number;
|
|
10
|
+
reason: string;
|
|
11
|
+
}>;
|
|
12
|
+
excluded: Array<{
|
|
13
|
+
path: string;
|
|
14
|
+
reason: string;
|
|
15
|
+
}>;
|
|
16
|
+
recommendations: string[];
|
|
17
|
+
limitations: string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Module L — Token / context optimization.
|
|
21
|
+
* Never includes secret-like paths.
|
|
22
|
+
*/
|
|
23
|
+
export declare function planContext(options: {
|
|
24
|
+
root: string;
|
|
25
|
+
graph: RepositoryGraph;
|
|
26
|
+
query: string;
|
|
27
|
+
budgetTokens?: number;
|
|
28
|
+
}): Promise<ContextPlan>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveRepoRoot, isPathInsideRoot } from "../../utils/path.js";
|
|
4
|
+
function estimateTokens(text) {
|
|
5
|
+
return Math.ceil(text.length / 4);
|
|
6
|
+
}
|
|
7
|
+
const SECRETISH = /\.env|id_rsa|credentials|secrets\//i;
|
|
8
|
+
/**
|
|
9
|
+
* Module L — Token / context optimization.
|
|
10
|
+
* Never includes secret-like paths.
|
|
11
|
+
*/
|
|
12
|
+
export async function planContext(options) {
|
|
13
|
+
const root = resolveRepoRoot(options.root);
|
|
14
|
+
const budget = options.budgetTokens ?? 8_000;
|
|
15
|
+
const q = options.query.toLowerCase();
|
|
16
|
+
const selected = [];
|
|
17
|
+
const excluded = [];
|
|
18
|
+
let used = 0;
|
|
19
|
+
const candidates = options.graph.nodes
|
|
20
|
+
.filter((n) => (n.kind === "file" || n.kind === "doc" || n.kind === "config") && n.path)
|
|
21
|
+
.map((n) => n.path)
|
|
22
|
+
.sort();
|
|
23
|
+
for (const rel of candidates) {
|
|
24
|
+
if (rel.includes("\0") || path.isAbsolute(rel) || rel.split(/[/\\]/).includes("..")) {
|
|
25
|
+
excluded.push({ path: rel, reason: "Unsafe path rejected" });
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (SECRETISH.test(rel)) {
|
|
29
|
+
excluded.push({ path: rel, reason: "Secret-like path excluded" });
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const relevance = (rel.toLowerCase().includes(q) ? 0.6 : 0) +
|
|
33
|
+
(/\.(ts|tsx|md)$/i.test(rel) ? 0.2 : 0.05) +
|
|
34
|
+
(rel.startsWith("src/") ? 0.15 : 0);
|
|
35
|
+
if (relevance < 0.25) {
|
|
36
|
+
excluded.push({ path: rel, reason: "Low relevance to query" });
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const absolute = path.resolve(root, rel);
|
|
41
|
+
if (!isPathInsideRoot(root, absolute)) {
|
|
42
|
+
excluded.push({ path: rel, reason: "Path escapes repository root" });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const st = await fs.stat(absolute);
|
|
46
|
+
const lst = await fs.lstat(absolute);
|
|
47
|
+
if (lst.isSymbolicLink()) {
|
|
48
|
+
excluded.push({ path: rel, reason: "Symlink excluded" });
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (st.size > 100_000) {
|
|
52
|
+
excluded.push({ path: rel, reason: "File too large for context budget" });
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const text = await fs.readFile(absolute, "utf8");
|
|
56
|
+
const tokens = estimateTokens(text);
|
|
57
|
+
if (used + tokens > budget) {
|
|
58
|
+
excluded.push({ path: rel, reason: "Would exceed token budget" });
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
selected.push({
|
|
62
|
+
path: rel,
|
|
63
|
+
tokens,
|
|
64
|
+
relevance,
|
|
65
|
+
reason: relevance >= 0.6 ? "Path matches query" : "Likely relevant source/doc",
|
|
66
|
+
});
|
|
67
|
+
used += tokens;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
excluded.push({ path: rel, reason: "Unreadable" });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
selected.sort((a, b) => b.relevance - a.relevance || a.path.localeCompare(b.path));
|
|
74
|
+
return {
|
|
75
|
+
root,
|
|
76
|
+
budgetTokens: budget,
|
|
77
|
+
estimatedTokens: used,
|
|
78
|
+
selected,
|
|
79
|
+
excluded: excluded.slice(0, 200),
|
|
80
|
+
recommendations: [
|
|
81
|
+
used > budget * 0.9 ? "Increase budget or narrow query" : "Budget headroom available",
|
|
82
|
+
"Prefer Project Brain claims over dumping entire directories",
|
|
83
|
+
],
|
|
84
|
+
limitations: [
|
|
85
|
+
"Token estimate is chars/4 heuristic",
|
|
86
|
+
"Relevance is lexical, not embedding-based",
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentDoctor 2.0 platform contracts.
|
|
3
|
+
* Local-first: no mandatory cloud, DB, or API keys.
|
|
4
|
+
*/
|
|
5
|
+
export type EvidenceKind = "verified" | "inferred" | "unknown";
|
|
6
|
+
export interface EvidenceRef {
|
|
7
|
+
kind: EvidenceKind;
|
|
8
|
+
path?: string;
|
|
9
|
+
detail: string;
|
|
10
|
+
line?: number;
|
|
11
|
+
}
|
|
12
|
+
export type PlatformSeverity = "critical" | "high" | "medium" | "low" | "info";
|
|
13
|
+
export interface PlatformFinding {
|
|
14
|
+
id: string;
|
|
15
|
+
module: string;
|
|
16
|
+
severity: PlatformSeverity;
|
|
17
|
+
title: string;
|
|
18
|
+
message: string;
|
|
19
|
+
recommendation: string;
|
|
20
|
+
confidence: number;
|
|
21
|
+
evidence: EvidenceRef[];
|
|
22
|
+
falsePositiveWarning?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface GraphNode {
|
|
25
|
+
id: string;
|
|
26
|
+
kind: "file" | "directory" | "function" | "class" | "module" | "api" | "test" | "dependency" | "doc" | "config" | "service" | "commit" | "owner";
|
|
27
|
+
label: string;
|
|
28
|
+
path?: string;
|
|
29
|
+
meta?: Record<string, string | number | boolean>;
|
|
30
|
+
}
|
|
31
|
+
export interface GraphEdge {
|
|
32
|
+
id: string;
|
|
33
|
+
from: string;
|
|
34
|
+
to: string;
|
|
35
|
+
kind: string;
|
|
36
|
+
evidence: EvidenceKind;
|
|
37
|
+
}
|
|
38
|
+
export interface RepositoryGraph {
|
|
39
|
+
root: string;
|
|
40
|
+
generatedAt: string;
|
|
41
|
+
nodes: GraphNode[];
|
|
42
|
+
edges: GraphEdge[];
|
|
43
|
+
limitations: string[];
|
|
44
|
+
}
|
|
45
|
+
export type FirewallDecision = "allow" | "block" | "require-approval" | "sandbox-only" | "deny-network" | "deny-secrets";
|
|
46
|
+
export type AgentActionType = "shell" | "file-create" | "file-modify" | "file-delete" | "git" | "package-install" | "database" | "network" | "docker" | "infra" | "secret-access" | "config-change" | "deploy";
|
|
47
|
+
export interface AgentActionRequest {
|
|
48
|
+
actionId: string;
|
|
49
|
+
agentId: string;
|
|
50
|
+
userId?: string;
|
|
51
|
+
timestamp: string;
|
|
52
|
+
type: AgentActionType;
|
|
53
|
+
params: Record<string, string>;
|
|
54
|
+
repositoryRoot: string;
|
|
55
|
+
}
|
|
56
|
+
export interface FirewallVerdict {
|
|
57
|
+
actionId: string;
|
|
58
|
+
decision: FirewallDecision;
|
|
59
|
+
reason: string;
|
|
60
|
+
riskLevel: PlatformSeverity;
|
|
61
|
+
policyId?: string;
|
|
62
|
+
approvalStatus: "not-required" | "pending" | "approved" | "denied";
|
|
63
|
+
executionResult: "not-executed";
|
|
64
|
+
}
|
|
65
|
+
export type LocalRole = "admin" | "developer" | "reviewer" | "auditor" | "readonly";
|
|
66
|
+
export interface PlatformSnapshot {
|
|
67
|
+
version: "2.0";
|
|
68
|
+
root: string;
|
|
69
|
+
generatedAt: string;
|
|
70
|
+
graph: RepositoryGraph;
|
|
71
|
+
findings: PlatformFinding[];
|
|
72
|
+
readiness: ReadinessReport;
|
|
73
|
+
testImpact?: TestImpactSnapshot;
|
|
74
|
+
limitations: string[];
|
|
75
|
+
}
|
|
76
|
+
/** Persisted subset of Module H test-impact analysis. */
|
|
77
|
+
export interface TestImpactSnapshot {
|
|
78
|
+
gitAvailable: boolean;
|
|
79
|
+
changedFiles: string[];
|
|
80
|
+
recommendedTests: Array<{
|
|
81
|
+
testPath: string;
|
|
82
|
+
reason: string;
|
|
83
|
+
confidence: number;
|
|
84
|
+
evidence: EvidenceKind;
|
|
85
|
+
exists: boolean;
|
|
86
|
+
relatedModules: string[];
|
|
87
|
+
}>;
|
|
88
|
+
relatedModules: string[];
|
|
89
|
+
missingTestWarnings: Array<{
|
|
90
|
+
sourceFile: string;
|
|
91
|
+
message: string;
|
|
92
|
+
relatedModules: string[];
|
|
93
|
+
}>;
|
|
94
|
+
uncoveredAreas: string[];
|
|
95
|
+
skipRisk: string;
|
|
96
|
+
limitations: string[];
|
|
97
|
+
}
|
|
98
|
+
export interface ReadinessCategory {
|
|
99
|
+
id: string;
|
|
100
|
+
title: string;
|
|
101
|
+
score: number;
|
|
102
|
+
evidence: EvidenceRef[];
|
|
103
|
+
gaps: string[];
|
|
104
|
+
recommendations: string[];
|
|
105
|
+
}
|
|
106
|
+
export interface ReadinessReport {
|
|
107
|
+
root: string;
|
|
108
|
+
categories: ReadinessCategory[];
|
|
109
|
+
overallNote: string;
|
|
110
|
+
limitations: string[];
|
|
111
|
+
}
|
|
@@ -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,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,59 @@
|
|
|
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
|
+
match: { commandContains: ["rm -rf", "git push --force", "drop table"] },
|
|
25
|
+
decision: "block",
|
|
26
|
+
reason: "baseline-safe: destructive operation",
|
|
27
|
+
riskLevel: "critical",
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
"fail-closed-ci": {
|
|
32
|
+
version: "2.0",
|
|
33
|
+
defaultDecision: "block",
|
|
34
|
+
failClosed: true,
|
|
35
|
+
shellAllowlist: ["npm test", "npm run verify", "vitest"],
|
|
36
|
+
rules: [
|
|
37
|
+
{
|
|
38
|
+
id: "ci-deny-network",
|
|
39
|
+
actionTypes: ["network"],
|
|
40
|
+
decision: "block",
|
|
41
|
+
reason: "fail-closed-ci: network blocked",
|
|
42
|
+
riskLevel: "high",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "ci-deny-deploy",
|
|
46
|
+
actionTypes: ["deploy", "infra"],
|
|
47
|
+
decision: "block",
|
|
48
|
+
reason: "fail-closed-ci: deploy blocked",
|
|
49
|
+
riskLevel: "critical",
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
export function listPolicyPacks() {
|
|
55
|
+
return Object.keys(POLICY_PACKS);
|
|
56
|
+
}
|
|
57
|
+
export function getPolicyPack(name) {
|
|
58
|
+
return POLICY_PACKS[name] ?? null;
|
|
59
|
+
}
|