@praneeth_54/agentdoctor 2.0.0 → 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.
Files changed (95) hide show
  1. package/CHANGELOG.md +73 -4
  2. package/README.md +283 -158
  3. package/dist/architecture/contract.d.ts +64 -0
  4. package/dist/architecture/contract.js +373 -0
  5. package/dist/assurance/change.d.ts +211 -0
  6. package/dist/assurance/change.js +624 -0
  7. package/dist/assurance/proof.d.ts +136 -0
  8. package/dist/assurance/proof.js +338 -0
  9. package/dist/auth/index.d.ts +81 -0
  10. package/dist/auth/index.js +130 -0
  11. package/dist/auth/rbac.d.ts +9 -0
  12. package/dist/auth/rbac.js +26 -0
  13. package/dist/cli/commands/architecture.d.ts +6 -0
  14. package/dist/cli/commands/architecture.js +109 -0
  15. package/dist/cli/commands/assurance.d.ts +67 -0
  16. package/dist/cli/commands/assurance.js +210 -0
  17. package/dist/cli/commands/complete.js +2 -2
  18. package/dist/cli/commands/platform.d.ts +2 -0
  19. package/dist/cli/commands/platform.js +5 -1
  20. package/dist/cli/commands/policy-graph-run.d.ts +40 -0
  21. package/dist/cli/commands/policy-graph-run.js +200 -0
  22. package/dist/cli/commands/workspace.d.ts +9 -0
  23. package/dist/cli/commands/workspace.js +82 -0
  24. package/dist/cli/program.js +535 -17
  25. package/dist/constants.d.ts +1 -1
  26. package/dist/constants.js +1 -1
  27. package/dist/contracts/index.d.ts +1 -1
  28. package/dist/contracts/index.js +2 -2
  29. package/dist/core/brain-product/init.js +5 -2
  30. package/dist/core/secrets/scan.d.ts +7 -1
  31. package/dist/core/secrets/scan.js +9 -9
  32. package/dist/coverage/cobertura.d.ts +6 -0
  33. package/dist/coverage/cobertura.js +52 -0
  34. package/dist/coverage/istanbul.d.ts +7 -0
  35. package/dist/coverage/istanbul.js +134 -0
  36. package/dist/coverage/lcov.d.ts +7 -0
  37. package/dist/coverage/lcov.js +59 -0
  38. package/dist/coverage/load.d.ts +9 -0
  39. package/dist/coverage/load.js +39 -0
  40. package/dist/coverage/types.d.ts +23 -0
  41. package/dist/coverage/types.js +5 -0
  42. package/dist/dashboard/server.d.ts +1 -1
  43. package/dist/dashboard/server.js +21 -5
  44. package/dist/enforcement/runner.d.ts +47 -3
  45. package/dist/enforcement/runner.js +420 -16
  46. package/dist/index.d.ts +23 -2
  47. package/dist/index.js +15 -2
  48. package/dist/intelligence/graph/build.d.ts +2 -0
  49. package/dist/intelligence/graph/build.js +49 -5
  50. package/dist/intelligence/graph/incremental.d.ts +68 -0
  51. package/dist/intelligence/graph/incremental.js +234 -0
  52. package/dist/intelligence/resolve/imports.d.ts +36 -0
  53. package/dist/intelligence/resolve/imports.js +245 -0
  54. package/dist/languages/go.d.ts +9 -0
  55. package/dist/languages/go.js +45 -0
  56. package/dist/languages/index.d.ts +11 -0
  57. package/dist/languages/index.js +76 -0
  58. package/dist/languages/php.d.ts +7 -0
  59. package/dist/languages/php.js +193 -0
  60. package/dist/languages/python.d.ts +7 -0
  61. package/dist/languages/python.js +150 -0
  62. package/dist/languages/types.d.ts +42 -0
  63. package/dist/languages/types.js +26 -0
  64. package/dist/languages/typescript.d.ts +3 -0
  65. package/dist/languages/typescript.js +95 -0
  66. package/dist/mcp/intelligence/handlers.d.ts +5 -0
  67. package/dist/mcp/intelligence/handlers.js +229 -40
  68. package/dist/mcp/intelligence/path-safety.d.ts +2 -4
  69. package/dist/mcp/intelligence/path-safety.js +25 -44
  70. package/dist/mcp/intelligence/registry.d.ts +1 -1
  71. package/dist/mcp/intelligence/registry.js +69 -1
  72. package/dist/platform/firewall/evaluate.d.ts +6 -3
  73. package/dist/platform/firewall/evaluate.js +66 -15
  74. package/dist/platform/graph/build.js +19 -23
  75. package/dist/platform/health/analyze.js +23 -17
  76. package/dist/platform/index.js +2 -0
  77. package/dist/platform/test-impact/analyze.d.ts +21 -3
  78. package/dist/platform/test-impact/analyze.js +214 -28
  79. package/dist/platform/tokens/plan.js +39 -20
  80. package/dist/platform/types.d.ts +9 -0
  81. package/dist/policy/compose.d.ts +34 -0
  82. package/dist/policy/compose.js +118 -0
  83. package/dist/policy/packs.js +33 -1
  84. package/dist/security/paths.d.ts +21 -0
  85. package/dist/security/paths.js +105 -0
  86. package/dist/storage/postgres.d.ts +26 -0
  87. package/dist/storage/postgres.js +90 -0
  88. package/dist/storage/provider.d.ts +2 -7
  89. package/dist/storage/provider.js +22 -16
  90. package/dist/storage/sqlite.d.ts +33 -0
  91. package/dist/storage/sqlite.js +77 -0
  92. package/dist/team/auth.js +1 -1
  93. package/dist/workspace/index.d.ts +69 -0
  94. package/dist/workspace/index.js +220 -0
  95. package/package.json +9 -3
@@ -0,0 +1,77 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { resolveRepoRoot } from "../utils/path.js";
4
+ /**
5
+ * SQLite-backed StorageProvider using Node's built-in `node:sqlite` (DatabaseSync).
6
+ * Available on Node 22+; feature-detected at runtime.
7
+ */
8
+ export class SqliteStorageProvider {
9
+ kind = "sqlite";
10
+ db;
11
+ dbPath;
12
+ constructor(db, dbPath) {
13
+ this.db = db;
14
+ this.dbPath = dbPath;
15
+ this.db.exec(`
16
+ CREATE TABLE IF NOT EXISTS kv (
17
+ key TEXT PRIMARY KEY,
18
+ value TEXT NOT NULL
19
+ );
20
+ `);
21
+ }
22
+ get path() {
23
+ return this.dbPath;
24
+ }
25
+ async get(key) {
26
+ const row = this.db.prepare("SELECT value FROM kv WHERE key = ?").get(key);
27
+ return row?.value ?? null;
28
+ }
29
+ async set(key, value) {
30
+ this.db
31
+ .prepare("INSERT INTO kv (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value")
32
+ .run(key, value);
33
+ }
34
+ async delete(key) {
35
+ this.db.prepare("DELETE FROM kv WHERE key = ?").run(key);
36
+ }
37
+ async list(prefix) {
38
+ const rows = this.db
39
+ .prepare("SELECT key FROM kv WHERE key LIKE ? ORDER BY key")
40
+ .all(`${prefix}%`);
41
+ return rows.map((r) => r.key);
42
+ }
43
+ close() {
44
+ this.db.close();
45
+ }
46
+ }
47
+ /**
48
+ * Create SQLite storage at `.agentdoctor/storage.sqlite` when `node:sqlite` is available.
49
+ */
50
+ export async function tryCreateSqliteStorage(rootInput) {
51
+ let DatabaseSync;
52
+ try {
53
+ const mod = await import("node:sqlite");
54
+ if (!mod || typeof mod.DatabaseSync !== "function") {
55
+ return { error: "node:sqlite DatabaseSync is not available in this Node runtime" };
56
+ }
57
+ DatabaseSync = mod.DatabaseSync;
58
+ }
59
+ catch (error) {
60
+ return {
61
+ error: `SQLite storage requires Node.js with node:sqlite (typically Node 22+): ${error instanceof Error ? error.message : String(error)}`,
62
+ };
63
+ }
64
+ try {
65
+ const root = resolveRepoRoot(rootInput);
66
+ const dir = path.join(root, ".agentdoctor");
67
+ await fs.mkdir(dir, { recursive: true });
68
+ const dbPath = path.join(dir, "storage.sqlite");
69
+ const db = new DatabaseSync(dbPath);
70
+ return new SqliteStorageProvider(db, dbPath);
71
+ }
72
+ catch (error) {
73
+ return {
74
+ error: `Failed to open SQLite storage: ${error instanceof Error ? error.message : String(error)}`,
75
+ };
76
+ }
77
+ }
package/dist/team/auth.js CHANGED
@@ -9,7 +9,7 @@ export async function registerLocalDevUser(options) {
9
9
  const storage = options.storage ?? new FilesystemStorageProvider(options.root, ".agentdoctor/team");
10
10
  const salt = randomBytes(16).toString("hex");
11
11
  const user = {
12
- id: `user_${createHash("sha1").update(options.username).digest("hex").slice(0, 10)}`,
12
+ id: `user_${createHash("sha256").update(options.username).digest("hex").slice(0, 10)}`,
13
13
  username: options.username,
14
14
  passwordHash: hashPassword(options.password, salt),
15
15
  salt,
@@ -0,0 +1,69 @@
1
+ export declare const WORKSPACE_SCHEMA_VERSION: "1.0.0";
2
+ export interface WorkspaceModel {
3
+ schemaVersion: typeof WORKSPACE_SCHEMA_VERSION;
4
+ id: string;
5
+ name: string;
6
+ /** Absolute, resolved repository roots that belong to this workspace. */
7
+ repositoryRoots: string[];
8
+ /**
9
+ * When false (default), operations scoped to repo A must not read paths under repo B
10
+ * even if both are members of the same workspace.
11
+ */
12
+ allowCrossRead: boolean;
13
+ createdAt: string;
14
+ updatedAt: string;
15
+ }
16
+ export interface WorkspacePathAccess {
17
+ allowed: boolean;
18
+ reason: string;
19
+ workspaceId?: string;
20
+ requestedRoot?: string;
21
+ targetRoot?: string;
22
+ }
23
+ export declare function initWorkspace(options: {
24
+ controlRoot: string;
25
+ name: string;
26
+ repositoryRoot?: string;
27
+ id?: string;
28
+ allowCrossRead?: boolean;
29
+ }): Promise<WorkspaceModel>;
30
+ export declare function loadWorkspace(controlRoot: string, id: string): Promise<WorkspaceModel | null>;
31
+ export declare function listWorkspaces(controlRoot: string): Promise<WorkspaceModel[]>;
32
+ export declare function addRepositoryToWorkspace(options: {
33
+ controlRoot: string;
34
+ workspaceId: string;
35
+ repositoryRoot: string;
36
+ }): Promise<WorkspaceModel>;
37
+ export declare function removeWorkspace(controlRoot: string, id: string): Promise<boolean>;
38
+ export declare function workspaceStatus(controlRoot: string, id: string): Promise<{
39
+ ok: boolean;
40
+ workspace?: WorkspaceModel;
41
+ missingRoots: string[];
42
+ error?: string;
43
+ }>;
44
+ /**
45
+ * Isolation gate: a path may be read only when it is inside the operation's own
46
+ * repositoryRoot, OR (allowCrossRead && both roots are members of the same workspace).
47
+ */
48
+ export declare function assertWorkspacePathAccess(options: {
49
+ workspace: WorkspaceModel | null;
50
+ operationRoot: string;
51
+ targetPath: string;
52
+ }): WorkspacePathAccess;
53
+ /**
54
+ * Read a file only when workspace isolation allows it.
55
+ */
56
+ export declare function readFileWithinWorkspace(options: {
57
+ workspace: WorkspaceModel | null;
58
+ operationRoot: string;
59
+ targetPath: string;
60
+ maxBytes?: number;
61
+ }): Promise<{
62
+ ok: true;
63
+ content: string;
64
+ access: WorkspacePathAccess;
65
+ } | {
66
+ ok: false;
67
+ access: WorkspacePathAccess;
68
+ error: string;
69
+ }>;
@@ -0,0 +1,220 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { atomicWriteTextFile } from "../utils/fs.js";
5
+ import { isPathInsideRoot, resolveRepoRoot } from "../utils/path.js";
6
+ export const WORKSPACE_SCHEMA_VERSION = "1.0.0";
7
+ function workspacesDir(controlRoot) {
8
+ return path.join(resolveRepoRoot(controlRoot), ".agentdoctor", "workspaces");
9
+ }
10
+ function workspaceFile(controlRoot, id) {
11
+ if (!/^[a-zA-Z0-9_-]{1,64}$/.test(id)) {
12
+ throw new Error("invalid workspace id");
13
+ }
14
+ return path.join(workspacesDir(controlRoot), `${id}.json`);
15
+ }
16
+ function normalizeRoot(rootInput) {
17
+ return resolveRepoRoot(rootInput);
18
+ }
19
+ export async function initWorkspace(options) {
20
+ const controlRoot = resolveRepoRoot(options.controlRoot);
21
+ const id = options.id ??
22
+ `ws_${createHash("sha256").update(`${options.name}:${randomUUID()}`).digest("hex").slice(0, 12)}`;
23
+ const now = new Date().toISOString();
24
+ const roots = [];
25
+ if (options.repositoryRoot) {
26
+ roots.push(normalizeRoot(options.repositoryRoot));
27
+ }
28
+ const model = {
29
+ schemaVersion: WORKSPACE_SCHEMA_VERSION,
30
+ id,
31
+ name: options.name.trim() || id,
32
+ repositoryRoots: roots,
33
+ allowCrossRead: options.allowCrossRead === true,
34
+ createdAt: now,
35
+ updatedAt: now,
36
+ };
37
+ await fs.mkdir(workspacesDir(controlRoot), { recursive: true });
38
+ const target = workspaceFile(controlRoot, id);
39
+ try {
40
+ await fs.access(target);
41
+ throw new Error(`workspace already exists: ${id}`);
42
+ }
43
+ catch (error) {
44
+ if (error instanceof Error && error.message.startsWith("workspace already exists")) {
45
+ throw error;
46
+ }
47
+ }
48
+ await atomicWriteTextFile(target, `${JSON.stringify(model, null, 2)}\n`);
49
+ return model;
50
+ }
51
+ export async function loadWorkspace(controlRoot, id) {
52
+ try {
53
+ const raw = await fs.readFile(workspaceFile(controlRoot, id), "utf8");
54
+ const parsed = JSON.parse(raw);
55
+ if (!parsed ||
56
+ parsed.schemaVersion !== WORKSPACE_SCHEMA_VERSION ||
57
+ !Array.isArray(parsed.repositoryRoots)) {
58
+ return null;
59
+ }
60
+ return {
61
+ ...parsed,
62
+ allowCrossRead: parsed.allowCrossRead === true,
63
+ repositoryRoots: parsed.repositoryRoots.map((r) => normalizeRoot(r)),
64
+ };
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ export async function listWorkspaces(controlRoot) {
71
+ const dir = workspacesDir(controlRoot);
72
+ let entries;
73
+ try {
74
+ entries = await fs.readdir(dir);
75
+ }
76
+ catch {
77
+ return [];
78
+ }
79
+ const out = [];
80
+ for (const name of entries) {
81
+ if (!name.endsWith(".json"))
82
+ continue;
83
+ const id = name.slice(0, -".json".length);
84
+ const ws = await loadWorkspace(controlRoot, id);
85
+ if (ws)
86
+ out.push(ws);
87
+ }
88
+ return out.sort((a, b) => a.id.localeCompare(b.id));
89
+ }
90
+ export async function addRepositoryToWorkspace(options) {
91
+ const ws = await loadWorkspace(options.controlRoot, options.workspaceId);
92
+ if (!ws)
93
+ throw new Error(`workspace not found: ${options.workspaceId}`);
94
+ const root = normalizeRoot(options.repositoryRoot);
95
+ if (!ws.repositoryRoots.includes(root)) {
96
+ ws.repositoryRoots.push(root);
97
+ ws.updatedAt = new Date().toISOString();
98
+ await atomicWriteTextFile(workspaceFile(options.controlRoot, ws.id), `${JSON.stringify(ws, null, 2)}\n`);
99
+ }
100
+ return ws;
101
+ }
102
+ export async function removeWorkspace(controlRoot, id) {
103
+ try {
104
+ await fs.unlink(workspaceFile(controlRoot, id));
105
+ return true;
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ }
111
+ export async function workspaceStatus(controlRoot, id) {
112
+ const ws = await loadWorkspace(controlRoot, id);
113
+ if (!ws)
114
+ return { ok: false, missingRoots: [], error: "workspace not found" };
115
+ const missingRoots = [];
116
+ for (const root of ws.repositoryRoots) {
117
+ try {
118
+ const st = await fs.stat(root);
119
+ if (!st.isDirectory())
120
+ missingRoots.push(root);
121
+ }
122
+ catch {
123
+ missingRoots.push(root);
124
+ }
125
+ }
126
+ return { ok: missingRoots.length === 0, workspace: ws, missingRoots };
127
+ }
128
+ /**
129
+ * Isolation gate: a path may be read only when it is inside the operation's own
130
+ * repositoryRoot, OR (allowCrossRead && both roots are members of the same workspace).
131
+ */
132
+ export function assertWorkspacePathAccess(options) {
133
+ const operationRoot = normalizeRoot(options.operationRoot);
134
+ const target = path.resolve(options.targetPath);
135
+ if (isPathInsideRoot(operationRoot, target)) {
136
+ return {
137
+ allowed: true,
138
+ reason: "inside operation repository root",
139
+ requestedRoot: operationRoot,
140
+ targetRoot: operationRoot,
141
+ ...(options.workspace ? { workspaceId: options.workspace.id } : {}),
142
+ };
143
+ }
144
+ if (!options.workspace) {
145
+ return {
146
+ allowed: false,
147
+ reason: "path outside operation root and no workspace context",
148
+ requestedRoot: operationRoot,
149
+ };
150
+ }
151
+ const members = options.workspace.repositoryRoots.map(normalizeRoot);
152
+ if (!members.includes(operationRoot)) {
153
+ return {
154
+ allowed: false,
155
+ reason: "operation root is not a member of the workspace",
156
+ workspaceId: options.workspace.id,
157
+ requestedRoot: operationRoot,
158
+ };
159
+ }
160
+ const targetMember = members.find((root) => isPathInsideRoot(root, target));
161
+ if (!targetMember) {
162
+ return {
163
+ allowed: false,
164
+ reason: "target path is not under any workspace member root",
165
+ workspaceId: options.workspace.id,
166
+ requestedRoot: operationRoot,
167
+ };
168
+ }
169
+ if (targetMember === operationRoot) {
170
+ return {
171
+ allowed: true,
172
+ reason: "inside operation repository root",
173
+ workspaceId: options.workspace.id,
174
+ requestedRoot: operationRoot,
175
+ targetRoot: targetMember,
176
+ };
177
+ }
178
+ if (!options.workspace.allowCrossRead) {
179
+ return {
180
+ allowed: false,
181
+ reason: "cross-repo read denied: target is under another workspace member and allowCrossRead=false",
182
+ workspaceId: options.workspace.id,
183
+ requestedRoot: operationRoot,
184
+ targetRoot: targetMember,
185
+ };
186
+ }
187
+ return {
188
+ allowed: true,
189
+ reason: "allowCrossRead enabled for same-workspace member",
190
+ workspaceId: options.workspace.id,
191
+ requestedRoot: operationRoot,
192
+ targetRoot: targetMember,
193
+ };
194
+ }
195
+ /**
196
+ * Read a file only when workspace isolation allows it.
197
+ */
198
+ export async function readFileWithinWorkspace(options) {
199
+ const access = assertWorkspacePathAccess({
200
+ workspace: options.workspace,
201
+ operationRoot: options.operationRoot,
202
+ targetPath: options.targetPath,
203
+ });
204
+ if (!access.allowed) {
205
+ return { ok: false, access, error: access.reason };
206
+ }
207
+ try {
208
+ const buf = await fs.readFile(path.resolve(options.targetPath));
209
+ const max = options.maxBytes ?? 1024 * 1024;
210
+ const content = buf.subarray(0, max).toString("utf8");
211
+ return { ok: true, content, access };
212
+ }
213
+ catch (error) {
214
+ return {
215
+ ok: false,
216
+ access,
217
+ error: error instanceof Error ? error.message : String(error),
218
+ };
219
+ }
220
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@praneeth_54/agentdoctor",
3
- "version": "2.0.0",
4
- "description": "Codebase intelligence, repository analysis, safety controls, and MCP tools for developers and engineering teams.",
3
+ "version": "2.0.1",
4
+ "description": "Engineering assurance for AI coding agents — repository intelligence, change evidence, safety controls, and MCP tools.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "agentdoctor": "./dist/cli/index.js"
@@ -40,7 +40,7 @@
40
40
  "validate:project-brain": "tsx validation/project-brain/run.ts",
41
41
  "validate:project-brain:real-world": "tsx validation/project-brain/real-world-run.ts",
42
42
  "benchmark:project-brain": "tsx validation/project-brain/benchmark.ts",
43
- "verify": "npm run typecheck && npm run lint && npm run format:check && npm test && npm run build",
43
+ "verify": "npm run typecheck && npm run lint && npm run format:check && npm run build && npm test",
44
44
  "verify:understanding": "npm run typecheck && npm run test:understanding",
45
45
  "verify:project-brain": "npm run typecheck && npm run test:understanding && npm run validate:project-brain && npm run benchmark:project-brain",
46
46
  "verify:mcp": "npm run typecheck && npm run test:mcp",
@@ -59,6 +59,9 @@
59
59
  "gemini",
60
60
  "aider",
61
61
  "mcp",
62
+ "change-assurance",
63
+ "evidence",
64
+ "engineering-assurance",
62
65
  "security",
63
66
  "linter",
64
67
  "cli",
@@ -80,7 +83,10 @@
80
83
  },
81
84
  "dependencies": {
82
85
  "@modelcontextprotocol/sdk": "^1.30.0",
86
+ "@types/pg": "^8.23.1",
83
87
  "commander": "^14.0.3",
88
+ "jose": "^6.2.12",
89
+ "pg": "^8.23.0",
84
90
  "picocolors": "^1.1.1",
85
91
  "typescript": "^5.9.3"
86
92
  },