@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,57 @@
|
|
|
1
|
+
import type { AgentActionRequest, FirewallDecision, FirewallVerdict, PlatformSeverity } from "../types.js";
|
|
2
|
+
export interface FirewallPolicyRule {
|
|
3
|
+
id: string;
|
|
4
|
+
actionTypes?: AgentActionRequest["type"][];
|
|
5
|
+
match?: {
|
|
6
|
+
commandContains?: string[];
|
|
7
|
+
commandPatterns?: string[];
|
|
8
|
+
pathPrefix?: string[];
|
|
9
|
+
pathContains?: string[];
|
|
10
|
+
};
|
|
11
|
+
decision: FirewallDecision;
|
|
12
|
+
reason: string;
|
|
13
|
+
riskLevel: PlatformSeverity;
|
|
14
|
+
}
|
|
15
|
+
export interface FirewallPolicyDocument {
|
|
16
|
+
version: "2.0";
|
|
17
|
+
defaultDecision: FirewallDecision;
|
|
18
|
+
/** When true and policy file is invalid, deny all actions instead of falling back to defaults. */
|
|
19
|
+
failClosed?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* When set, shell actions that are not already blocked must match at least one allowlist entry
|
|
22
|
+
* (substring match, case-insensitive) or they receive require-approval.
|
|
23
|
+
*/
|
|
24
|
+
shellAllowlist?: string[];
|
|
25
|
+
rules: FirewallPolicyRule[];
|
|
26
|
+
}
|
|
27
|
+
export interface PolicyLoadResult {
|
|
28
|
+
policy: FirewallPolicyDocument;
|
|
29
|
+
source: "file" | "default" | "fail-closed-deny" | "composed";
|
|
30
|
+
validationError?: string;
|
|
31
|
+
composeSources?: Array<"builtin" | "repo">;
|
|
32
|
+
}
|
|
33
|
+
declare const EVALUATE_ONLY = "Evaluate-only: no commands are executed and no agents are intercepted.";
|
|
34
|
+
declare const DEFAULT_POLICY: FirewallPolicyDocument;
|
|
35
|
+
export declare function validateFirewallPolicy(raw: unknown): {
|
|
36
|
+
ok: boolean;
|
|
37
|
+
policy?: FirewallPolicyDocument;
|
|
38
|
+
error?: string;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Load local action-policy document.
|
|
42
|
+
* Layers: composed builtin pack + `.agentdoctor/policy.json` (fail-closed on malformed repo policy),
|
|
43
|
+
* then optional platform `firewall-policy.json` overlay (highest precedence when valid).
|
|
44
|
+
* Invalid platform file + failClosed → deny-all.
|
|
45
|
+
* Invalid platform file + default → composed policy (or DEFAULT_POLICY when compose unavailable).
|
|
46
|
+
*/
|
|
47
|
+
export declare function loadFirewallPolicy(root: string, options?: {
|
|
48
|
+
failClosed?: boolean;
|
|
49
|
+
}): Promise<PolicyLoadResult>;
|
|
50
|
+
/**
|
|
51
|
+
* Module C — Action Policy Evaluator (compatibility: firewall).
|
|
52
|
+
* Evaluates policies only; never executes the requested action and does not intercept agents.
|
|
53
|
+
*/
|
|
54
|
+
export declare function evaluateAgentAction(root: string, action: AgentActionRequest, options?: {
|
|
55
|
+
failClosed?: boolean;
|
|
56
|
+
}): Promise<FirewallVerdict>;
|
|
57
|
+
export { DEFAULT_POLICY, EVALUATE_ONLY };
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { platformDir, writeJsonArtifact } from "../store.js";
|
|
4
|
+
import { resolveRepoRoot } from "../../utils/path.js";
|
|
5
|
+
import { composePolicy } from "../../policy/compose.js";
|
|
6
|
+
const EVALUATE_ONLY = "Evaluate-only: no commands are executed and no agents are intercepted.";
|
|
7
|
+
const DEFAULT_POLICY = {
|
|
8
|
+
version: "2.0",
|
|
9
|
+
defaultDecision: "require-approval",
|
|
10
|
+
failClosed: false,
|
|
11
|
+
shellAllowlist: [
|
|
12
|
+
"npm test",
|
|
13
|
+
"npm run test",
|
|
14
|
+
"npm --version",
|
|
15
|
+
"npm -v",
|
|
16
|
+
"vitest",
|
|
17
|
+
"eslint",
|
|
18
|
+
"tsc",
|
|
19
|
+
"prettier",
|
|
20
|
+
],
|
|
21
|
+
rules: [
|
|
22
|
+
{
|
|
23
|
+
id: "block-secret-paths",
|
|
24
|
+
actionTypes: ["file-modify", "file-delete", "file-create", "secret-access"],
|
|
25
|
+
match: {
|
|
26
|
+
pathContains: [
|
|
27
|
+
".env",
|
|
28
|
+
"id_rsa",
|
|
29
|
+
"id_ed25519",
|
|
30
|
+
"credentials",
|
|
31
|
+
"secrets/",
|
|
32
|
+
".pem",
|
|
33
|
+
"service-account",
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
decision: "block",
|
|
37
|
+
reason: "Secret-sensitive path",
|
|
38
|
+
riskLevel: "critical",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "deny-prod-deploy",
|
|
42
|
+
actionTypes: ["deploy", "infra"],
|
|
43
|
+
decision: "block",
|
|
44
|
+
reason: "Production/infra changes require out-of-band process",
|
|
45
|
+
riskLevel: "critical",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "network-default-deny",
|
|
49
|
+
actionTypes: ["network"],
|
|
50
|
+
decision: "deny-network",
|
|
51
|
+
reason: "Network access denied by default",
|
|
52
|
+
riskLevel: "high",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "package-install-approval",
|
|
56
|
+
actionTypes: ["package-install"],
|
|
57
|
+
decision: "require-approval",
|
|
58
|
+
reason: "Package installs change supply chain",
|
|
59
|
+
riskLevel: "high",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "allow-safe-dev-paths",
|
|
63
|
+
actionTypes: ["file-modify", "file-create"],
|
|
64
|
+
match: { pathPrefix: ["src/", "tests/", "docs/"] },
|
|
65
|
+
decision: "allow",
|
|
66
|
+
reason: "Allowlisted development paths",
|
|
67
|
+
riskLevel: "low",
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
const BUILTIN_SHELL_BLOCKS = [
|
|
72
|
+
{
|
|
73
|
+
id: "builtin-rm-recursive",
|
|
74
|
+
patterns: [
|
|
75
|
+
/\brm\s+(-[^\s]*f[^\s]*r|-[^\s]*r[^\s]*f|--recursive)\b/i,
|
|
76
|
+
/\brm\s+-rf\b/i,
|
|
77
|
+
/\brm\s+-fr\b/i,
|
|
78
|
+
],
|
|
79
|
+
reason: "Recursive deletion pattern",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "builtin-disk-format",
|
|
83
|
+
patterns: [/\bmkfs(\.\w+)?\b/i, /\bdd\s+if=/i, /\bshred\b/i],
|
|
84
|
+
reason: "Disk-formatting / destructive device write pattern",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: "builtin-fork-bomb",
|
|
88
|
+
patterns: [/:\(\)\s*\{\s*:\|:&\s*\}\s*;?\s*:/, /\bfork\s*\(\s*\)\s*while/i],
|
|
89
|
+
reason: "Fork-bomb pattern",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "builtin-deploy",
|
|
93
|
+
patterns: [
|
|
94
|
+
/\b(kubectl\s+apply|helm\s+upgrade|terraform\s+apply|pulumi\s+up)\b/i,
|
|
95
|
+
/\b(aws\s+s3\s+sync|gcloud\s+deploy|az\s+deployment)\b/i,
|
|
96
|
+
/\bdocker\s+push\b/i,
|
|
97
|
+
/\bnpm\s+publish\b/i,
|
|
98
|
+
],
|
|
99
|
+
reason: "Deployment / publish command pattern",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "builtin-network-exfil",
|
|
103
|
+
patterns: [
|
|
104
|
+
/\bcurl\b.*\b(-d|--data|--upload-file|-T)\b/i,
|
|
105
|
+
/\bwget\b.*\b(--post-data|--body-data)\b/i,
|
|
106
|
+
/\bnc\s+-l\b/i,
|
|
107
|
+
/\bcurl\b.*\bhttps?:\/\/[^\s]*(webhook|exfil|pastebin|evil)/i,
|
|
108
|
+
],
|
|
109
|
+
reason: "Network exfiltration-style pattern",
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
function isFirewallDecision(value) {
|
|
113
|
+
return (value === "allow" ||
|
|
114
|
+
value === "block" ||
|
|
115
|
+
value === "require-approval" ||
|
|
116
|
+
value === "sandbox-only" ||
|
|
117
|
+
value === "deny-network" ||
|
|
118
|
+
value === "deny-secrets");
|
|
119
|
+
}
|
|
120
|
+
export function validateFirewallPolicy(raw) {
|
|
121
|
+
if (!raw || typeof raw !== "object")
|
|
122
|
+
return { ok: false, error: "policy must be an object" };
|
|
123
|
+
const doc = raw;
|
|
124
|
+
if (doc.version !== "2.0")
|
|
125
|
+
return { ok: false, error: 'policy.version must be "2.0"' };
|
|
126
|
+
if (!Array.isArray(doc.rules))
|
|
127
|
+
return { ok: false, error: "policy.rules must be an array" };
|
|
128
|
+
if (!isFirewallDecision(doc.defaultDecision)) {
|
|
129
|
+
return { ok: false, error: "policy.defaultDecision is invalid" };
|
|
130
|
+
}
|
|
131
|
+
for (const [i, rule] of doc.rules.entries()) {
|
|
132
|
+
if (!rule || typeof rule !== "object") {
|
|
133
|
+
return { ok: false, error: `rules[${i}] must be an object` };
|
|
134
|
+
}
|
|
135
|
+
const r = rule;
|
|
136
|
+
if (typeof r.id !== "string" || !r.id)
|
|
137
|
+
return { ok: false, error: `rules[${i}].id required` };
|
|
138
|
+
if (!isFirewallDecision(r.decision)) {
|
|
139
|
+
return { ok: false, error: `rules[${i}].decision invalid` };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (doc.shellAllowlist !== undefined && !Array.isArray(doc.shellAllowlist)) {
|
|
143
|
+
return { ok: false, error: "shellAllowlist must be an array of strings" };
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
ok: true,
|
|
147
|
+
policy: {
|
|
148
|
+
version: "2.0",
|
|
149
|
+
defaultDecision: doc.defaultDecision,
|
|
150
|
+
failClosed: Boolean(doc.failClosed),
|
|
151
|
+
...(Array.isArray(doc.shellAllowlist)
|
|
152
|
+
? { shellAllowlist: doc.shellAllowlist.filter((s) => typeof s === "string") }
|
|
153
|
+
: {}),
|
|
154
|
+
rules: doc.rules,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function denyAllPolicy(reason) {
|
|
159
|
+
return {
|
|
160
|
+
version: "2.0",
|
|
161
|
+
defaultDecision: "block",
|
|
162
|
+
failClosed: true,
|
|
163
|
+
rules: [
|
|
164
|
+
{
|
|
165
|
+
id: "fail-closed-deny-all",
|
|
166
|
+
decision: "block",
|
|
167
|
+
reason,
|
|
168
|
+
riskLevel: "critical",
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Load local action-policy document.
|
|
175
|
+
* Layers: composed builtin pack + `.agentdoctor/policy.json` (fail-closed on malformed repo policy),
|
|
176
|
+
* then optional platform `firewall-policy.json` overlay (highest precedence when valid).
|
|
177
|
+
* Invalid platform file + failClosed → deny-all.
|
|
178
|
+
* Invalid platform file + default → composed policy (or DEFAULT_POLICY when compose unavailable).
|
|
179
|
+
*/
|
|
180
|
+
export async function loadFirewallPolicy(root, options) {
|
|
181
|
+
const resolved = resolveRepoRoot(root);
|
|
182
|
+
const composed = await composePolicy({ root: resolved });
|
|
183
|
+
if (composed.validationError && composed.failClosed) {
|
|
184
|
+
return {
|
|
185
|
+
policy: composed.policy,
|
|
186
|
+
source: "fail-closed-deny",
|
|
187
|
+
validationError: composed.validationError,
|
|
188
|
+
composeSources: composed.sources,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const file = path.join(platformDir(resolved), "firewall-policy.json");
|
|
192
|
+
try {
|
|
193
|
+
const raw = await fs.readFile(file, "utf8");
|
|
194
|
+
let parsed;
|
|
195
|
+
try {
|
|
196
|
+
parsed = JSON.parse(raw);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
const failClosed = options?.failClosed === true || composed.failClosed;
|
|
200
|
+
if (failClosed) {
|
|
201
|
+
return {
|
|
202
|
+
policy: denyAllPolicy("Malformed policy JSON; fail-closed deny"),
|
|
203
|
+
source: "fail-closed-deny",
|
|
204
|
+
validationError: "Malformed JSON",
|
|
205
|
+
composeSources: composed.sources,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
policy: composed.policy,
|
|
210
|
+
source: "composed",
|
|
211
|
+
validationError: "Malformed platform firewall-policy.json; using composed policy",
|
|
212
|
+
composeSources: composed.sources,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const validated = validateFirewallPolicy(parsed);
|
|
216
|
+
if (!validated.ok || !validated.policy) {
|
|
217
|
+
const fileWantsFailClosed = parsed &&
|
|
218
|
+
typeof parsed === "object" &&
|
|
219
|
+
parsed.failClosed === true;
|
|
220
|
+
const failClosed = options?.failClosed === true || fileWantsFailClosed || composed.failClosed;
|
|
221
|
+
if (failClosed) {
|
|
222
|
+
return {
|
|
223
|
+
policy: denyAllPolicy(`Invalid policy: ${validated.error}`),
|
|
224
|
+
source: "fail-closed-deny",
|
|
225
|
+
...(validated.error ? { validationError: validated.error } : {}),
|
|
226
|
+
composeSources: composed.sources,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
policy: composed.policy,
|
|
231
|
+
source: "composed",
|
|
232
|
+
...(validated.error
|
|
233
|
+
? { validationError: `Invalid platform policy; using composed (${validated.error})` }
|
|
234
|
+
: {}),
|
|
235
|
+
composeSources: composed.sources,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
// Platform file rules first (highest precedence), then composed builtin+repo rules.
|
|
239
|
+
const merged = {
|
|
240
|
+
version: "2.0",
|
|
241
|
+
defaultDecision: validated.policy.defaultDecision,
|
|
242
|
+
failClosed: validated.policy.failClosed === true || composed.policy.failClosed === true,
|
|
243
|
+
...(validated.policy.shellAllowlist
|
|
244
|
+
? { shellAllowlist: validated.policy.shellAllowlist }
|
|
245
|
+
: composed.policy.shellAllowlist
|
|
246
|
+
? { shellAllowlist: composed.policy.shellAllowlist }
|
|
247
|
+
: {}),
|
|
248
|
+
rules: [...validated.policy.rules, ...composed.policy.rules],
|
|
249
|
+
};
|
|
250
|
+
return {
|
|
251
|
+
policy: merged,
|
|
252
|
+
source: "file",
|
|
253
|
+
composeSources: composed.sources,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// No platform file: prefer repo-composed policy when `.agentdoctor/policy.json` exists.
|
|
258
|
+
// Otherwise seed DEFAULT_POLICY (includes allow-safe-dev-paths). Do not treat packName
|
|
259
|
+
// alone as a reason to skip DEFAULT_POLICY — compose always names a builtin pack.
|
|
260
|
+
if (composed.sources.includes("repo")) {
|
|
261
|
+
return {
|
|
262
|
+
policy: composed.policy,
|
|
263
|
+
source: "composed",
|
|
264
|
+
composeSources: composed.sources,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
await writeJsonArtifact(resolved, "firewall-policy.json", DEFAULT_POLICY);
|
|
268
|
+
return { policy: DEFAULT_POLICY, source: "default", composeSources: composed.sources };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function ruleMatches(rule, action) {
|
|
272
|
+
if (rule.actionTypes && !rule.actionTypes.includes(action.type))
|
|
273
|
+
return false;
|
|
274
|
+
const cmd = action.params.command ?? "";
|
|
275
|
+
const target = action.params.path ?? action.params.target ?? "";
|
|
276
|
+
const match = rule.match;
|
|
277
|
+
if (!match)
|
|
278
|
+
return true;
|
|
279
|
+
if (match.commandContains?.length) {
|
|
280
|
+
if (!match.commandContains.some((p) => cmd.toLowerCase().includes(p.toLowerCase()))) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (match.commandPatterns?.length) {
|
|
285
|
+
if (!match.commandPatterns.some((p) => {
|
|
286
|
+
try {
|
|
287
|
+
return new RegExp(p, "i").test(cmd);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
})) {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (match.pathPrefix?.length) {
|
|
297
|
+
if (!match.pathPrefix.some((p) => target.startsWith(p)))
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
if (match.pathContains?.length) {
|
|
301
|
+
if (!match.pathContains.some((p) => target.toLowerCase().includes(p.toLowerCase()))) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
function matchBuiltinShell(cmd) {
|
|
308
|
+
for (const rule of BUILTIN_SHELL_BLOCKS) {
|
|
309
|
+
if (rule.patterns.some((re) => re.test(cmd))) {
|
|
310
|
+
return { id: rule.id, reason: rule.reason };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
function verdict(actionId, decision, reason, riskLevel, policyId) {
|
|
316
|
+
return {
|
|
317
|
+
actionId,
|
|
318
|
+
decision,
|
|
319
|
+
reason: `${reason}. ${EVALUATE_ONLY}`,
|
|
320
|
+
riskLevel,
|
|
321
|
+
...(policyId ? { policyId } : {}),
|
|
322
|
+
approvalStatus: decision === "require-approval" ? "pending" : "not-required",
|
|
323
|
+
executionResult: "not-executed",
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Module C — Action Policy Evaluator (compatibility: firewall).
|
|
328
|
+
* Evaluates policies only; never executes the requested action and does not intercept agents.
|
|
329
|
+
*/
|
|
330
|
+
export async function evaluateAgentAction(root, action, options) {
|
|
331
|
+
const loaded = await loadFirewallPolicy(root, options);
|
|
332
|
+
if (action.type === "shell") {
|
|
333
|
+
const cmd = action.params.command ?? "";
|
|
334
|
+
const builtin = matchBuiltinShell(cmd);
|
|
335
|
+
if (builtin) {
|
|
336
|
+
return verdict(action.actionId, "block", builtin.reason, "critical", builtin.id);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
for (const rule of loaded.policy.rules) {
|
|
340
|
+
if (!ruleMatches(rule, action))
|
|
341
|
+
continue;
|
|
342
|
+
return verdict(action.actionId, rule.decision, rule.reason, rule.riskLevel, rule.id);
|
|
343
|
+
}
|
|
344
|
+
if (action.type === "shell" && loaded.policy.shellAllowlist?.length) {
|
|
345
|
+
const cmd = (action.params.command ?? "").toLowerCase();
|
|
346
|
+
const allowed = loaded.policy.shellAllowlist.some((entry) => cmd.includes(entry.toLowerCase()));
|
|
347
|
+
if (!allowed) {
|
|
348
|
+
return verdict(action.actionId, "require-approval", "Shell command not on local allowlist", "medium", "shell-allowlist");
|
|
349
|
+
}
|
|
350
|
+
return verdict(action.actionId, "allow", "Shell command matches local allowlist", "low", "shell-allowlist");
|
|
351
|
+
}
|
|
352
|
+
const reason = loaded.source === "fail-closed-deny"
|
|
353
|
+
? `Fail-closed deny (${loaded.validationError ?? "invalid policy"})`
|
|
354
|
+
: loaded.source === "default" && loaded.validationError
|
|
355
|
+
? `No specific rule matched; default policy applied (fallback after: ${loaded.validationError})`
|
|
356
|
+
: "No specific rule matched; default policy applied";
|
|
357
|
+
return verdict(action.actionId, loaded.policy.defaultDecision, reason, loaded.source === "fail-closed-deny" ? "critical" : "medium", loaded.source === "fail-closed-deny" ? "fail-closed-deny-all" : "default");
|
|
358
|
+
}
|
|
359
|
+
export { DEFAULT_POLICY, EVALUATE_ONLY };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RepositoryGraph } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Module A — Repository Intelligence (local graph MVP).
|
|
4
|
+
* Symbol extraction is heuristic (regex), not a full language AST.
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildRepositoryGraph(rootInput: string): Promise<RepositoryGraph>;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { readTextFile } from "../../utils/fs.js";
|
|
5
|
+
import { resolveRepoRoot, toPosixRelative } from "../../utils/path.js";
|
|
6
|
+
const SKIP = new Set([
|
|
7
|
+
"node_modules",
|
|
8
|
+
".git",
|
|
9
|
+
"dist",
|
|
10
|
+
"coverage",
|
|
11
|
+
".agentdoctor",
|
|
12
|
+
"vendor",
|
|
13
|
+
".next",
|
|
14
|
+
"build",
|
|
15
|
+
]);
|
|
16
|
+
const CODE_EXT = new Set([
|
|
17
|
+
".ts",
|
|
18
|
+
".tsx",
|
|
19
|
+
".js",
|
|
20
|
+
".jsx",
|
|
21
|
+
".mjs",
|
|
22
|
+
".cjs",
|
|
23
|
+
".py",
|
|
24
|
+
".go",
|
|
25
|
+
".rs",
|
|
26
|
+
".java",
|
|
27
|
+
".kt",
|
|
28
|
+
".php",
|
|
29
|
+
".rb",
|
|
30
|
+
]);
|
|
31
|
+
function nodeId(kind, key) {
|
|
32
|
+
return `${kind}:${createHash("sha1").update(key).digest("hex").slice(0, 12)}`;
|
|
33
|
+
}
|
|
34
|
+
async function walk(root, dir, files, dirs, limit) {
|
|
35
|
+
if (files.length >= limit)
|
|
36
|
+
return;
|
|
37
|
+
let entries;
|
|
38
|
+
try {
|
|
39
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
if (SKIP.has(entry.name))
|
|
46
|
+
continue;
|
|
47
|
+
const absolute = path.join(dir, entry.name);
|
|
48
|
+
if (entry.isSymbolicLink())
|
|
49
|
+
continue;
|
|
50
|
+
if (entry.isDirectory()) {
|
|
51
|
+
dirs.push(absolute);
|
|
52
|
+
await walk(root, absolute, files, dirs, limit);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (entry.isFile())
|
|
56
|
+
files.push(absolute);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function extractSymbols(content, rel) {
|
|
60
|
+
const nodes = [];
|
|
61
|
+
const fnRe = /(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_][A-Za-z0-9_]*)|([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:async\s*)?\(/g;
|
|
62
|
+
const classRe = /(?:export\s+)?class\s+([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
63
|
+
let m;
|
|
64
|
+
while ((m = fnRe.exec(content))) {
|
|
65
|
+
const name = m[1] ?? m[2];
|
|
66
|
+
if (!name || name.length < 2)
|
|
67
|
+
continue;
|
|
68
|
+
nodes.push({
|
|
69
|
+
id: nodeId("function", `${rel}:${name}`),
|
|
70
|
+
kind: "function",
|
|
71
|
+
label: name,
|
|
72
|
+
path: rel,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
while ((m = classRe.exec(content))) {
|
|
76
|
+
const name = m[1];
|
|
77
|
+
nodes.push({
|
|
78
|
+
id: nodeId("class", `${rel}:${name}`),
|
|
79
|
+
kind: "class",
|
|
80
|
+
label: name,
|
|
81
|
+
path: rel,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return nodes;
|
|
85
|
+
}
|
|
86
|
+
function extractImports(content, rel) {
|
|
87
|
+
const edges = [];
|
|
88
|
+
const dependencyNodes = [];
|
|
89
|
+
const importRe = /(?:import\s+.*?from\s+|require\()\s*['"]([^'"]+)['"]|from\s+['"]([^'"]+)['"]/g;
|
|
90
|
+
let m;
|
|
91
|
+
const fromId = nodeId("file", rel);
|
|
92
|
+
const seen = new Set();
|
|
93
|
+
while ((m = importRe.exec(content))) {
|
|
94
|
+
const spec = m[1] ?? m[2];
|
|
95
|
+
if (!spec)
|
|
96
|
+
continue;
|
|
97
|
+
const toId = nodeId("dependency", `${rel}->${spec}`);
|
|
98
|
+
edges.push({
|
|
99
|
+
id: nodeId("edge", `${fromId}->${toId}`),
|
|
100
|
+
from: fromId,
|
|
101
|
+
to: toId,
|
|
102
|
+
kind: "imports",
|
|
103
|
+
evidence: spec.startsWith(".") ? "verified" : "inferred",
|
|
104
|
+
});
|
|
105
|
+
if (!seen.has(toId)) {
|
|
106
|
+
seen.add(toId);
|
|
107
|
+
dependencyNodes.push({
|
|
108
|
+
id: toId,
|
|
109
|
+
kind: "dependency",
|
|
110
|
+
label: spec,
|
|
111
|
+
path: rel,
|
|
112
|
+
meta: { specifier: spec },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { edges, dependencyNodes };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Module A — Repository Intelligence (local graph MVP).
|
|
120
|
+
* Symbol extraction is heuristic (regex), not a full language AST.
|
|
121
|
+
*/
|
|
122
|
+
export async function buildRepositoryGraph(rootInput) {
|
|
123
|
+
const root = resolveRepoRoot(rootInput);
|
|
124
|
+
const files = [];
|
|
125
|
+
const dirs = [];
|
|
126
|
+
await walk(root, root, files, dirs, 2_000);
|
|
127
|
+
const nodes = [];
|
|
128
|
+
const edges = [];
|
|
129
|
+
const limitations = [
|
|
130
|
+
"Symbol extraction is regex-heuristic for common languages; not a full AST",
|
|
131
|
+
"Call-graph and multi-repo federation are extension points (not fully built)",
|
|
132
|
+
];
|
|
133
|
+
for (const dir of dirs) {
|
|
134
|
+
const rel = toPosixRelative(root, dir) || ".";
|
|
135
|
+
nodes.push({
|
|
136
|
+
id: nodeId("directory", rel),
|
|
137
|
+
kind: "directory",
|
|
138
|
+
label: path.basename(dir),
|
|
139
|
+
path: rel,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
for (const absolute of files) {
|
|
143
|
+
const rel = toPosixRelative(root, absolute);
|
|
144
|
+
const ext = path.extname(rel).toLowerCase();
|
|
145
|
+
const base = path.basename(rel).toLowerCase();
|
|
146
|
+
let kind = "file";
|
|
147
|
+
if (/\.(test|spec)\./i.test(rel) || rel.includes("__tests__"))
|
|
148
|
+
kind = "test";
|
|
149
|
+
else if (/\.(md|rst|txt)$/i.test(ext))
|
|
150
|
+
kind = "doc";
|
|
151
|
+
else if (/\.(yml|yaml|toml|json|env)$/i.test(ext) ||
|
|
152
|
+
base.startsWith("dockerfile") ||
|
|
153
|
+
base === "makefile")
|
|
154
|
+
kind = "config";
|
|
155
|
+
const fileId = nodeId("file", rel);
|
|
156
|
+
nodes.push({ id: fileId, kind, label: path.basename(rel), path: rel });
|
|
157
|
+
if (CODE_EXT.has(ext)) {
|
|
158
|
+
// open+fstat+read via readTextFile — avoids exists/stat→read TOCTOU
|
|
159
|
+
const content = await readTextFile(absolute, 256 * 1024);
|
|
160
|
+
if (content === null)
|
|
161
|
+
continue;
|
|
162
|
+
nodes.push(...extractSymbols(content, rel));
|
|
163
|
+
const { edges: importEdges, dependencyNodes } = extractImports(content, rel);
|
|
164
|
+
edges.push(...importEdges);
|
|
165
|
+
for (const dep of dependencyNodes) {
|
|
166
|
+
if (!nodes.some((n) => n.id === dep.id))
|
|
167
|
+
nodes.push(dep);
|
|
168
|
+
}
|
|
169
|
+
if (/router\.|app\.(get|post|put|delete)|@app\.(get|post)/i.test(content)) {
|
|
170
|
+
nodes.push({
|
|
171
|
+
id: nodeId("api", rel),
|
|
172
|
+
kind: "api",
|
|
173
|
+
label: `api:${path.basename(rel)}`,
|
|
174
|
+
path: rel,
|
|
175
|
+
meta: { evidence: "inferred" },
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
nodes.sort((a, b) => a.id.localeCompare(b.id));
|
|
181
|
+
edges.sort((a, b) => a.id.localeCompare(b.id));
|
|
182
|
+
return {
|
|
183
|
+
root,
|
|
184
|
+
generatedAt: new Date().toISOString(),
|
|
185
|
+
nodes,
|
|
186
|
+
edges,
|
|
187
|
+
limitations,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PlatformFinding } from "../types.js";
|
|
2
|
+
import type { RepositoryGraph } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Module B — Code health & risk (deterministic heuristics with evidence).
|
|
5
|
+
*/
|
|
6
|
+
export declare function analyzeCodeHealth(rootInput: string, graph: RepositoryGraph): Promise<PlatformFinding[]>;
|
|
7
|
+
export declare function scoreFileRisk(findings: PlatformFinding[], filePath: string): number;
|