@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.
- package/CHANGELOG.md +73 -4
- package/README.md +283 -158
- 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/complete.js +2 -2
- package/dist/cli/commands/platform.d.ts +2 -0
- package/dist/cli/commands/platform.js +5 -1
- 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/workspace.d.ts +9 -0
- package/dist/cli/commands/workspace.js +82 -0
- package/dist/cli/program.js +535 -17
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/index.js +2 -2
- package/dist/core/brain-product/init.js +5 -2
- package/dist/core/secrets/scan.d.ts +7 -1
- package/dist/core/secrets/scan.js +9 -9
- 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 +1 -1
- package/dist/dashboard/server.js +21 -5
- package/dist/enforcement/runner.d.ts +47 -3
- package/dist/enforcement/runner.js +420 -16
- package/dist/index.d.ts +23 -2
- package/dist/index.js +15 -2
- package/dist/intelligence/graph/build.d.ts +2 -0
- package/dist/intelligence/graph/build.js +49 -5
- 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/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/intelligence/handlers.d.ts +5 -0
- package/dist/mcp/intelligence/handlers.js +229 -40
- package/dist/mcp/intelligence/path-safety.d.ts +2 -4
- package/dist/mcp/intelligence/path-safety.js +25 -44
- package/dist/mcp/intelligence/registry.d.ts +1 -1
- package/dist/mcp/intelligence/registry.js +69 -1
- package/dist/platform/firewall/evaluate.d.ts +6 -3
- package/dist/platform/firewall/evaluate.js +66 -15
- package/dist/platform/graph/build.js +19 -23
- package/dist/platform/health/analyze.js +23 -17
- package/dist/platform/index.js +2 -0
- package/dist/platform/test-impact/analyze.d.ts +21 -3
- package/dist/platform/test-impact/analyze.js +214 -28
- package/dist/platform/tokens/plan.js +39 -20
- package/dist/platform/types.d.ts +9 -0
- package/dist/policy/compose.d.ts +34 -0
- package/dist/policy/compose.js +118 -0
- package/dist/policy/packs.js +33 -1
- 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 +2 -7
- package/dist/storage/provider.js +22 -16
- package/dist/storage/sqlite.d.ts +33 -0
- package/dist/storage/sqlite.js +77 -0
- package/dist/team/auth.js +1 -1
- package/dist/workspace/index.d.ts +69 -0
- package/dist/workspace/index.js +220 -0
- package/package.json +9 -3
|
@@ -1,46 +1,394 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { createHmac } from "node:crypto";
|
|
3
|
+
import { execFile, spawn } from "node:child_process";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { realpathSync } from "node:fs";
|
|
3
8
|
import { evaluateAgentAction, EVALUATE_ONLY } from "../platform/firewall/evaluate.js";
|
|
9
|
+
import { resolveRepoRoot } from "../utils/path.js";
|
|
10
|
+
import { PathEscapeError, assertInsideRepo } from "../security/paths.js";
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
13
|
+
const DEFAULT_MAX_BUFFER = 1024 * 1024;
|
|
14
|
+
const OUTPUT_TRUNCATE = 32_768;
|
|
15
|
+
const ENV_ALLOWLIST = new Set(["PATH", "NODE_ENV", "HOME", "LANG"]);
|
|
16
|
+
/**
|
|
17
|
+
* Parse a command string into argv without invoking a shell.
|
|
18
|
+
* Supports simple double/single quotes; rejects unclosed quotes.
|
|
19
|
+
*/
|
|
20
|
+
export function parseArgv(command) {
|
|
21
|
+
const trimmed = command.trim();
|
|
22
|
+
if (!trimmed)
|
|
23
|
+
return [];
|
|
24
|
+
const args = [];
|
|
25
|
+
let current = "";
|
|
26
|
+
let quote = null;
|
|
27
|
+
for (let i = 0; i < trimmed.length; i += 1) {
|
|
28
|
+
const ch = trimmed[i];
|
|
29
|
+
if (quote) {
|
|
30
|
+
if (ch === quote) {
|
|
31
|
+
quote = null;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
current += ch;
|
|
35
|
+
}
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (ch === '"' || ch === "'") {
|
|
39
|
+
quote = ch;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (/\s/.test(ch)) {
|
|
43
|
+
if (current.length > 0) {
|
|
44
|
+
args.push(current);
|
|
45
|
+
current = "";
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
current += ch;
|
|
50
|
+
}
|
|
51
|
+
if (quote) {
|
|
52
|
+
throw new Error("unclosed quote in command");
|
|
53
|
+
}
|
|
54
|
+
if (current.length > 0)
|
|
55
|
+
args.push(current);
|
|
56
|
+
return args;
|
|
57
|
+
}
|
|
58
|
+
export function buildAllowlistedEnv(source = process.env) {
|
|
59
|
+
const env = {};
|
|
60
|
+
for (const key of ENV_ALLOWLIST) {
|
|
61
|
+
const value = source[key];
|
|
62
|
+
if (typeof value === "string")
|
|
63
|
+
env[key] = value;
|
|
64
|
+
}
|
|
65
|
+
for (const [key, value] of Object.entries(source)) {
|
|
66
|
+
if (key.startsWith("AGENTDOCTOR_") && typeof value === "string") {
|
|
67
|
+
env[key] = value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return env;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Reject path-like args that resolve outside the repository root.
|
|
74
|
+
*/
|
|
75
|
+
export function assertArgsInsideRoot(args, root) {
|
|
76
|
+
for (const arg of args) {
|
|
77
|
+
if (arg.startsWith("-"))
|
|
78
|
+
continue;
|
|
79
|
+
const looksLikePath = arg.includes("..") || arg.includes("/") || arg.includes("\\") || path.isAbsolute(arg);
|
|
80
|
+
if (!looksLikePath)
|
|
81
|
+
continue;
|
|
82
|
+
try {
|
|
83
|
+
assertInsideRepo(root, arg);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error instanceof PathEscapeError) {
|
|
87
|
+
throw new Error("path escape rejected");
|
|
88
|
+
}
|
|
89
|
+
throw new Error("path escape rejected");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function truncate(text, max = OUTPUT_TRUNCATE) {
|
|
94
|
+
if (text.length <= max)
|
|
95
|
+
return text;
|
|
96
|
+
return `${text.slice(0, max)}\n…[truncated ${text.length - max} chars]`;
|
|
97
|
+
}
|
|
98
|
+
function resolveRealRoot(rootInput) {
|
|
99
|
+
const resolved = resolveRepoRoot(rootInput);
|
|
100
|
+
try {
|
|
101
|
+
return realpathSync(resolved);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return resolved;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function appendExecutionAudit(root, record) {
|
|
108
|
+
const auditPath = path.join(root, ".agentdoctor", "audit", "execution.jsonl");
|
|
109
|
+
await fs.mkdir(path.dirname(auditPath), { recursive: true });
|
|
110
|
+
let previousHash = null;
|
|
111
|
+
try {
|
|
112
|
+
const existing = await fs.readFile(auditPath, "utf8");
|
|
113
|
+
const lines = existing.trim().split("\n").filter(Boolean);
|
|
114
|
+
const last = lines[lines.length - 1];
|
|
115
|
+
if (last) {
|
|
116
|
+
const parsed = JSON.parse(last);
|
|
117
|
+
previousHash = typeof parsed.hash === "string" ? parsed.hash : null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
previousHash = null;
|
|
122
|
+
}
|
|
123
|
+
const { hash, payload } = appendAuditChain(previousHash, record);
|
|
124
|
+
const line = `${JSON.stringify({ hash, previousHash, record: JSON.parse(payload) })}\n`;
|
|
125
|
+
await fs.appendFile(auditPath, line, "utf8");
|
|
126
|
+
}
|
|
127
|
+
async function executeArgv(options) {
|
|
128
|
+
const start = Date.now();
|
|
129
|
+
const env = buildAllowlistedEnv();
|
|
130
|
+
const cwd = options.root;
|
|
131
|
+
if (options.useShell) {
|
|
132
|
+
const command = options.argv.join(" ");
|
|
133
|
+
return await new Promise((resolve) => {
|
|
134
|
+
const child = spawn(command, {
|
|
135
|
+
cwd,
|
|
136
|
+
env,
|
|
137
|
+
shell: true,
|
|
138
|
+
windowsHide: true,
|
|
139
|
+
});
|
|
140
|
+
let stdout = "";
|
|
141
|
+
let stderr = "";
|
|
142
|
+
let settled = false;
|
|
143
|
+
const timer = setTimeout(() => {
|
|
144
|
+
child.kill("SIGKILL");
|
|
145
|
+
if (!settled) {
|
|
146
|
+
settled = true;
|
|
147
|
+
resolve({
|
|
148
|
+
stdout: truncate(stdout),
|
|
149
|
+
stderr: truncate(stderr || "timeout"),
|
|
150
|
+
exitCode: null,
|
|
151
|
+
durationMs: Date.now() - start,
|
|
152
|
+
failed: true,
|
|
153
|
+
error: `timeout after ${options.timeoutMs}ms`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}, options.timeoutMs);
|
|
157
|
+
child.stdout?.on("data", (chunk) => {
|
|
158
|
+
stdout += chunk.toString("utf8");
|
|
159
|
+
if (stdout.length > options.maxBuffer) {
|
|
160
|
+
child.kill("SIGKILL");
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
child.stderr?.on("data", (chunk) => {
|
|
164
|
+
stderr += chunk.toString("utf8");
|
|
165
|
+
if (stderr.length > options.maxBuffer) {
|
|
166
|
+
child.kill("SIGKILL");
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
child.on("error", (err) => {
|
|
170
|
+
if (settled)
|
|
171
|
+
return;
|
|
172
|
+
settled = true;
|
|
173
|
+
clearTimeout(timer);
|
|
174
|
+
resolve({
|
|
175
|
+
stdout: truncate(stdout),
|
|
176
|
+
stderr: truncate(stderr),
|
|
177
|
+
exitCode: null,
|
|
178
|
+
durationMs: Date.now() - start,
|
|
179
|
+
failed: true,
|
|
180
|
+
error: err.message,
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
child.on("close", (code) => {
|
|
184
|
+
if (settled)
|
|
185
|
+
return;
|
|
186
|
+
settled = true;
|
|
187
|
+
clearTimeout(timer);
|
|
188
|
+
resolve({
|
|
189
|
+
stdout: truncate(stdout),
|
|
190
|
+
stderr: truncate(stderr),
|
|
191
|
+
exitCode: code,
|
|
192
|
+
durationMs: Date.now() - start,
|
|
193
|
+
failed: code !== 0,
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
const [file, ...args] = options.argv;
|
|
199
|
+
if (!file) {
|
|
200
|
+
return {
|
|
201
|
+
stdout: "",
|
|
202
|
+
stderr: "empty command",
|
|
203
|
+
exitCode: null,
|
|
204
|
+
durationMs: Date.now() - start,
|
|
205
|
+
failed: true,
|
|
206
|
+
error: "empty command",
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const result = await execFileAsync(file, args, {
|
|
211
|
+
cwd,
|
|
212
|
+
env,
|
|
213
|
+
timeout: options.timeoutMs,
|
|
214
|
+
maxBuffer: options.maxBuffer,
|
|
215
|
+
windowsHide: true,
|
|
216
|
+
shell: false,
|
|
217
|
+
});
|
|
218
|
+
return {
|
|
219
|
+
stdout: truncate(typeof result.stdout === "string" ? result.stdout : String(result.stdout)),
|
|
220
|
+
stderr: truncate(typeof result.stderr === "string" ? result.stderr : String(result.stderr)),
|
|
221
|
+
exitCode: 0,
|
|
222
|
+
durationMs: Date.now() - start,
|
|
223
|
+
failed: false,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
const err = error;
|
|
228
|
+
const exitCode = typeof err.code === "number" ? err.code : null;
|
|
229
|
+
return {
|
|
230
|
+
stdout: truncate(typeof err.stdout === "string" ? err.stdout : err.stdout ? String(err.stdout) : ""),
|
|
231
|
+
stderr: truncate(typeof err.stderr === "string" ? err.stderr : err.stderr ? String(err.stderr) : ""),
|
|
232
|
+
exitCode,
|
|
233
|
+
durationMs: Date.now() - start,
|
|
234
|
+
failed: true,
|
|
235
|
+
error: err.killed
|
|
236
|
+
? `timeout after ${options.timeoutMs}ms`
|
|
237
|
+
: (err.message ?? "execution failed"),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
4
241
|
/**
|
|
5
242
|
* AgentDoctor-controlled command runner.
|
|
6
|
-
*
|
|
243
|
+
* Executes only when executeIfAllowed===true AND policy decision===allow.
|
|
244
|
+
* Default path uses shell:false with argv parsing (no shell metacharacters).
|
|
7
245
|
*/
|
|
8
246
|
export async function runControlledCommand(options) {
|
|
247
|
+
const root = resolveRealRoot(options.root);
|
|
248
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
249
|
+
const maxBuffer = options.maxBuffer ?? DEFAULT_MAX_BUFFER;
|
|
250
|
+
const useShell = options.useShell === true;
|
|
251
|
+
let argv;
|
|
252
|
+
try {
|
|
253
|
+
argv =
|
|
254
|
+
options.argv && options.argv.length > 0 ? options.argv : parseArgv(options.command ?? "");
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
258
|
+
return buildFailedResult(root, message, "not-executed");
|
|
259
|
+
}
|
|
260
|
+
const commandStr = argv.join(" ");
|
|
261
|
+
if (!commandStr) {
|
|
262
|
+
return buildFailedResult(root, "empty command", "not-executed");
|
|
263
|
+
}
|
|
9
264
|
const action = {
|
|
10
265
|
actionId: randomUUID(),
|
|
11
266
|
agentId: "enforcement-runner",
|
|
12
267
|
timestamp: new Date().toISOString(),
|
|
13
268
|
type: "shell",
|
|
14
|
-
params: { command:
|
|
15
|
-
repositoryRoot:
|
|
269
|
+
params: { command: commandStr },
|
|
270
|
+
repositoryRoot: root,
|
|
16
271
|
};
|
|
17
|
-
const verdict = await evaluateAgentAction(
|
|
272
|
+
const verdict = await evaluateAgentAction(root, action, {
|
|
18
273
|
failClosed: options.failClosed === true,
|
|
19
274
|
});
|
|
20
275
|
const blocked = verdict.decision === "block" || verdict.decision === "deny-network";
|
|
21
|
-
|
|
276
|
+
if (blocked) {
|
|
277
|
+
const decision = toDecision(verdict, "blocked-by-enforcement");
|
|
278
|
+
await appendExecutionAudit(root, {
|
|
279
|
+
id: auditRecordId(`${commandStr}:${Date.now()}`),
|
|
280
|
+
command: commandStr,
|
|
281
|
+
argv,
|
|
282
|
+
executionStatus: "blocked-by-enforcement",
|
|
283
|
+
decision: verdict.decision,
|
|
284
|
+
reason: verdict.reason,
|
|
285
|
+
at: new Date().toISOString(),
|
|
286
|
+
});
|
|
287
|
+
return {
|
|
288
|
+
decision,
|
|
289
|
+
enforced: true,
|
|
290
|
+
notice: `Blocked by AgentDoctor-controlled runner. ${verdict.reason}`,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
if (!options.executeIfAllowed || verdict.decision !== "allow") {
|
|
294
|
+
const decision = toDecision(verdict, "not-executed");
|
|
295
|
+
return {
|
|
296
|
+
decision,
|
|
297
|
+
enforced: false,
|
|
298
|
+
notice: verdict.decision === "allow"
|
|
299
|
+
? `Policy allow; runner did not execute (executeIfAllowed=false). ${EVALUATE_ONLY}`
|
|
300
|
+
: `Policy ${verdict.decision}; runner did not execute. ${EVALUATE_ONLY}`,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
if (!useShell) {
|
|
304
|
+
try {
|
|
305
|
+
assertArgsInsideRoot(argv, root);
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
309
|
+
const decision = toDecision(verdict, "execution-failed", message);
|
|
310
|
+
await appendExecutionAudit(root, {
|
|
311
|
+
id: auditRecordId(`${commandStr}:${Date.now()}`),
|
|
312
|
+
command: commandStr,
|
|
313
|
+
argv,
|
|
314
|
+
executionStatus: "execution-failed",
|
|
315
|
+
reason: message,
|
|
316
|
+
at: new Date().toISOString(),
|
|
317
|
+
});
|
|
318
|
+
return {
|
|
319
|
+
decision,
|
|
320
|
+
enforced: false,
|
|
321
|
+
notice: `Execution failed: ${message}`,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const execResult = await executeArgv({
|
|
326
|
+
root,
|
|
327
|
+
argv,
|
|
328
|
+
timeoutMs,
|
|
329
|
+
maxBuffer,
|
|
330
|
+
useShell,
|
|
331
|
+
});
|
|
332
|
+
const executionStatus = execResult.failed ? "execution-failed" : "executed";
|
|
333
|
+
const decision = toDecision(verdict, executionStatus, execResult.error, useShell ? "HIGH RISK: executed via shell=true (explicit --shell flag)" : undefined);
|
|
334
|
+
await appendExecutionAudit(root, {
|
|
335
|
+
id: auditRecordId(`${commandStr}:${Date.now()}`),
|
|
336
|
+
command: commandStr,
|
|
337
|
+
argv,
|
|
338
|
+
useShell,
|
|
339
|
+
executionStatus,
|
|
340
|
+
exitCode: execResult.exitCode,
|
|
341
|
+
durationMs: execResult.durationMs,
|
|
342
|
+
stdout: execResult.stdout,
|
|
343
|
+
stderr: execResult.stderr,
|
|
344
|
+
...(execResult.error ? { error: execResult.error } : {}),
|
|
345
|
+
at: new Date().toISOString(),
|
|
346
|
+
});
|
|
347
|
+
return {
|
|
348
|
+
decision,
|
|
349
|
+
enforced: false,
|
|
350
|
+
notice: execResult.failed
|
|
351
|
+
? `Execution failed${execResult.error ? `: ${execResult.error}` : ""}`
|
|
352
|
+
: useShell
|
|
353
|
+
? "Executed under AgentDoctor control with shell=true (HIGH RISK)."
|
|
354
|
+
: "Executed under AgentDoctor control (shell=false).",
|
|
355
|
+
stdout: execResult.stdout,
|
|
356
|
+
stderr: execResult.stderr,
|
|
357
|
+
exitCode: execResult.exitCode,
|
|
358
|
+
durationMs: execResult.durationMs,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function toDecision(verdict, executionStatus, extraReason, riskNote) {
|
|
362
|
+
return {
|
|
22
363
|
actionId: verdict.actionId,
|
|
23
364
|
decision: verdict.decision,
|
|
24
|
-
reason: verdict.reason,
|
|
365
|
+
reason: [verdict.reason, extraReason, riskNote].filter(Boolean).join(" | "),
|
|
25
366
|
...(verdict.policyId ? { policyId: verdict.policyId } : {}),
|
|
26
367
|
policyVersion: "2.0",
|
|
27
368
|
inputClassification: "shell",
|
|
28
369
|
evidence: [{ id: "ev_policy", kind: "observed", detail: verdict.reason }],
|
|
29
370
|
riskLevel: verdict.riskLevel,
|
|
30
|
-
executionStatus
|
|
31
|
-
? "blocked-by-enforcement"
|
|
32
|
-
: options.executeIfAllowed && verdict.decision === "allow"
|
|
33
|
-
? "not-executed"
|
|
34
|
-
: "not-executed",
|
|
371
|
+
executionStatus,
|
|
35
372
|
approvalStatus: verdict.approvalStatus,
|
|
36
373
|
timestamp: new Date().toISOString(),
|
|
37
374
|
};
|
|
375
|
+
}
|
|
376
|
+
function buildFailedResult(_root, message, executionStatus) {
|
|
38
377
|
return {
|
|
39
|
-
decision
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
:
|
|
378
|
+
decision: {
|
|
379
|
+
actionId: randomUUID(),
|
|
380
|
+
decision: "block",
|
|
381
|
+
reason: message,
|
|
382
|
+
policyVersion: "2.0",
|
|
383
|
+
inputClassification: "shell",
|
|
384
|
+
evidence: [{ id: "ev_parse", kind: "observed", detail: message }],
|
|
385
|
+
riskLevel: "high",
|
|
386
|
+
executionStatus,
|
|
387
|
+
approvalStatus: "not-required",
|
|
388
|
+
timestamp: new Date().toISOString(),
|
|
389
|
+
},
|
|
390
|
+
enforced: executionStatus === "blocked-by-enforcement",
|
|
391
|
+
notice: message,
|
|
44
392
|
};
|
|
45
393
|
}
|
|
46
394
|
/** Append-only tamper-evident audit chain (HMAC over previous hash). */
|
|
@@ -54,3 +402,59 @@ export function appendAuditChain(previousHash, record, secret = "local-dev-audit
|
|
|
54
402
|
export function auditRecordId(parts) {
|
|
55
403
|
return `audit_${createHash("sha1").update(parts).digest("hex").slice(0, 12)}`;
|
|
56
404
|
}
|
|
405
|
+
/**
|
|
406
|
+
* Explain what the controlled runner would do for a command (evaluate-only).
|
|
407
|
+
* Does not execute.
|
|
408
|
+
*/
|
|
409
|
+
export async function explainControlledRun(options) {
|
|
410
|
+
const root = resolveRealRoot(options.root);
|
|
411
|
+
let argv;
|
|
412
|
+
try {
|
|
413
|
+
argv =
|
|
414
|
+
options.argv && options.argv.length > 0 ? options.argv : parseArgv(options.command ?? "");
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
418
|
+
return {
|
|
419
|
+
command: options.command ?? "",
|
|
420
|
+
argv: [],
|
|
421
|
+
useShell: options.useShell === true,
|
|
422
|
+
decision: "block",
|
|
423
|
+
reason: message,
|
|
424
|
+
policyId: null,
|
|
425
|
+
riskLevel: "high",
|
|
426
|
+
wouldExecute: false,
|
|
427
|
+
executionResult: "not-executed",
|
|
428
|
+
notice: EVALUATE_ONLY,
|
|
429
|
+
meaning: "Command could not be parsed for controlled execution.",
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
const commandStr = argv.join(" ");
|
|
433
|
+
const action = {
|
|
434
|
+
actionId: randomUUID(),
|
|
435
|
+
agentId: "enforcement-explain",
|
|
436
|
+
timestamp: new Date().toISOString(),
|
|
437
|
+
type: "shell",
|
|
438
|
+
params: { command: commandStr },
|
|
439
|
+
repositoryRoot: root,
|
|
440
|
+
};
|
|
441
|
+
const verdict = await evaluateAgentAction(root, action, {
|
|
442
|
+
failClosed: options.failClosed === true,
|
|
443
|
+
});
|
|
444
|
+
const wouldExecute = verdict.decision === "allow";
|
|
445
|
+
return {
|
|
446
|
+
command: commandStr,
|
|
447
|
+
argv,
|
|
448
|
+
useShell: options.useShell === true,
|
|
449
|
+
decision: verdict.decision,
|
|
450
|
+
reason: verdict.reason,
|
|
451
|
+
policyId: verdict.policyId ?? null,
|
|
452
|
+
riskLevel: verdict.riskLevel,
|
|
453
|
+
wouldExecute,
|
|
454
|
+
executionResult: "not-executed",
|
|
455
|
+
notice: EVALUATE_ONLY,
|
|
456
|
+
meaning: wouldExecute
|
|
457
|
+
? "Policy allow — `agentdoctor run` would execute under AgentDoctor control (still not claimed as enterprise interception)."
|
|
458
|
+
: "Policy would block or require approval — controlled runner will not execute.",
|
|
459
|
+
};
|
|
460
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,9 +2,22 @@ export { CONTRACTS_VERSION, DEFAULT_FEATURE_FLAGS } from "./contracts/index.js";
|
|
|
2
2
|
export { buildIntelligenceGraph } from "./intelligence/graph/build.js";
|
|
3
3
|
export { analyzeGitIntelligence } from "./intelligence/git/analyze.js";
|
|
4
4
|
export { buildC4Views } from "./architecture/c4.js";
|
|
5
|
+
export { loadArchitectureContract, checkArchitecture, checkArchitectureAtRoot, initArchitecture, explainArchitecture, DEFAULT_ARCHITECTURE_CONTRACT, } from "./architecture/contract.js";
|
|
6
|
+
export type { ArchitectureContract, ArchitectureViolation, ArchitectureCheckResult, } from "./architecture/contract.js";
|
|
7
|
+
export { loadCoverage, parseLcov, parseIstanbul, parseCobertura } from "./coverage/load.js";
|
|
8
|
+
export type { NormalizedCoverage, CoverageFormat, CoverageFile } from "./coverage/types.js";
|
|
9
|
+
export { parseSourceFile, languageAdapters, detectLanguage } from "./languages/index.js";
|
|
10
|
+
export type { LanguageAdapter, ParseResult, LanguageId } from "./languages/index.js";
|
|
5
11
|
export { createKnowledgeRecord, listKnowledge, transitionKnowledge, retrieveAuthoritative, } from "./knowledge/store.js";
|
|
6
|
-
export { FilesystemStorageProvider, MemoryStorageProvider } from "./storage/provider.js";
|
|
7
|
-
export {
|
|
12
|
+
export { FilesystemStorageProvider, MemoryStorageProvider, tryCreateSqliteStorage, tryCreatePostgresStorage, } from "./storage/provider.js";
|
|
13
|
+
export { SqliteStorageProvider } from "./storage/sqlite.js";
|
|
14
|
+
export { PostgresStorageProvider } from "./storage/postgres.js";
|
|
15
|
+
export { LocalIdentityProvider, OidcIdentityProvider, hasRbacPermission } from "./auth/index.js";
|
|
16
|
+
export type { IdentityClaims, IdentityProvider, RbacRole, RbacPermission } from "./auth/index.js";
|
|
17
|
+
export { initWorkspace, listWorkspaces, loadWorkspace, addRepositoryToWorkspace, removeWorkspace, workspaceStatus, assertWorkspacePathAccess, readFileWithinWorkspace, } from "./workspace/index.js";
|
|
18
|
+
export type { WorkspaceModel } from "./workspace/index.js";
|
|
19
|
+
export { composePolicy, explainComposedDecision } from "./policy/compose.js";
|
|
20
|
+
export { runControlledCommand, parseArgv, assertArgsInsideRoot, explainControlledRun, } from "./enforcement/runner.js";
|
|
8
21
|
export { registerLocalDevUser, authenticateLocalDev, authorize } from "./team/auth.js";
|
|
9
22
|
export { runProjectInit, listProposals, reviewProposal } from "./core/brain-product/init.js";
|
|
10
23
|
export { collectOpsHealth } from "./ops/health.js";
|
|
@@ -14,6 +27,14 @@ export { scan } from "./core/scanner/scan.js";
|
|
|
14
27
|
export { verify } from "./core/verify/verify.js";
|
|
15
28
|
export { compareFindings } from "./core/verify/compare.js";
|
|
16
29
|
export { PACKAGE_VERSION } from "./constants.js";
|
|
30
|
+
export { analyzeChange, verifyChange, inspectEvidence, verifyEvidence, explainChange, diffChange, changeStatus, deriveGraphChangeImpact, derivePackageImpact, EVIDENCE_SCHEMA_VERSION, } from "./assurance/change.js";
|
|
31
|
+
export type { ChangeAssessment, EvidenceManifest, VerificationStatus } from "./assurance/change.js";
|
|
32
|
+
export { buildProofFromEvidence, inspectProof, verifyProof, exportProof, explainProof, } from "./assurance/proof.js";
|
|
33
|
+
export type { ChangeProof, IntegrityStatus, CorrectnessStatus, EngineeringChecksStatus, ProofVerificationState, } from "./assurance/proof.js";
|
|
34
|
+
export { assertInsideRepo, resolveSafeRepoPath, safeRelPath, PathEscapeError, } from "./security/paths.js";
|
|
35
|
+
export { resolveImportSpecifier, resolveImportEdges, loadTsconfigPaths, } from "./intelligence/resolve/imports.js";
|
|
36
|
+
export type { ImportConfidence, ImportResolutionResult, TsconfigPathsConfig, } from "./intelligence/resolve/imports.js";
|
|
37
|
+
export { graphBuild, graphUpdate, graphRebuild, graphStatus, } from "./intelligence/graph/incremental.js";
|
|
17
38
|
export type { ScanOptions, ScanResult, RepositoryInfo, Finding, FindingsSummary, Scores, AgentPresence, AgentDetectionResult, AgentSecurityAnalysisMode, CliOptions, ExitCode, } from "./types/index.js";
|
|
18
39
|
export type { VerifyResult, VerifyOptions } from "./core/verify/verify.js";
|
|
19
40
|
export type { VerifyFindingRef, FindingCompareResult } from "./core/verify/compare.js";
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,17 @@ export { CONTRACTS_VERSION, DEFAULT_FEATURE_FLAGS } from "./contracts/index.js";
|
|
|
2
2
|
export { buildIntelligenceGraph } from "./intelligence/graph/build.js";
|
|
3
3
|
export { analyzeGitIntelligence } from "./intelligence/git/analyze.js";
|
|
4
4
|
export { buildC4Views } from "./architecture/c4.js";
|
|
5
|
+
export { loadArchitectureContract, checkArchitecture, checkArchitectureAtRoot, initArchitecture, explainArchitecture, DEFAULT_ARCHITECTURE_CONTRACT, } from "./architecture/contract.js";
|
|
6
|
+
export { loadCoverage, parseLcov, parseIstanbul, parseCobertura } from "./coverage/load.js";
|
|
7
|
+
export { parseSourceFile, languageAdapters, detectLanguage } from "./languages/index.js";
|
|
5
8
|
export { createKnowledgeRecord, listKnowledge, transitionKnowledge, retrieveAuthoritative, } from "./knowledge/store.js";
|
|
6
|
-
export { FilesystemStorageProvider, MemoryStorageProvider } from "./storage/provider.js";
|
|
7
|
-
export {
|
|
9
|
+
export { FilesystemStorageProvider, MemoryStorageProvider, tryCreateSqliteStorage, tryCreatePostgresStorage, } from "./storage/provider.js";
|
|
10
|
+
export { SqliteStorageProvider } from "./storage/sqlite.js";
|
|
11
|
+
export { PostgresStorageProvider } from "./storage/postgres.js";
|
|
12
|
+
export { LocalIdentityProvider, OidcIdentityProvider, hasRbacPermission } from "./auth/index.js";
|
|
13
|
+
export { initWorkspace, listWorkspaces, loadWorkspace, addRepositoryToWorkspace, removeWorkspace, workspaceStatus, assertWorkspacePathAccess, readFileWithinWorkspace, } from "./workspace/index.js";
|
|
14
|
+
export { composePolicy, explainComposedDecision } from "./policy/compose.js";
|
|
15
|
+
export { runControlledCommand, parseArgv, assertArgsInsideRoot, explainControlledRun, } from "./enforcement/runner.js";
|
|
8
16
|
export { registerLocalDevUser, authenticateLocalDev, authorize } from "./team/auth.js";
|
|
9
17
|
export { runProjectInit, listProposals, reviewProposal } from "./core/brain-product/init.js";
|
|
10
18
|
export { collectOpsHealth } from "./ops/health.js";
|
|
@@ -14,6 +22,11 @@ export { scan } from "./core/scanner/scan.js";
|
|
|
14
22
|
export { verify } from "./core/verify/verify.js";
|
|
15
23
|
export { compareFindings } from "./core/verify/compare.js";
|
|
16
24
|
export { PACKAGE_VERSION } from "./constants.js";
|
|
25
|
+
export { analyzeChange, verifyChange, inspectEvidence, verifyEvidence, explainChange, diffChange, changeStatus, deriveGraphChangeImpact, derivePackageImpact, EVIDENCE_SCHEMA_VERSION, } from "./assurance/change.js";
|
|
26
|
+
export { buildProofFromEvidence, inspectProof, verifyProof, exportProof, explainProof, } from "./assurance/proof.js";
|
|
27
|
+
export { assertInsideRepo, resolveSafeRepoPath, safeRelPath, PathEscapeError, } from "./security/paths.js";
|
|
28
|
+
export { resolveImportSpecifier, resolveImportEdges, loadTsconfigPaths, } from "./intelligence/resolve/imports.js";
|
|
29
|
+
export { graphBuild, graphUpdate, graphRebuild, graphStatus, } from "./intelligence/graph/incremental.js";
|
|
17
30
|
export { EXIT_CODES } from "./types/index.js";
|
|
18
31
|
export { agentRegistry } from "./agents/registry.js";
|
|
19
32
|
export { detectAgents } from "./agents/detect-agents.js";
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { RepositoryGraph } from "../../platform/types.js";
|
|
2
2
|
export type GraphBuilderMode = "regex" | "typescript-ast" | "auto";
|
|
3
|
+
export declare function nodeId(kind: string, key: string): string;
|
|
4
|
+
export declare function listTsFiles(root: string, limit?: number): Promise<string[]>;
|
|
3
5
|
/**
|
|
4
6
|
* TypeScript compiler API based graph enrichment.
|
|
5
7
|
* Falls back to regex graph when mode=regex or on failure.
|
|
@@ -5,10 +5,18 @@ import ts from "typescript";
|
|
|
5
5
|
import { resolveRepoRoot, toPosixRelative } from "../../utils/path.js";
|
|
6
6
|
import { buildRepositoryGraph } from "../../platform/graph/build.js";
|
|
7
7
|
import { CONTRACTS_VERSION } from "../../contracts/index.js";
|
|
8
|
-
|
|
8
|
+
import { loadTsconfigPaths, resolveImportSpecifier, } from "../resolve/imports.js";
|
|
9
|
+
function evidenceForConfidence(confidence) {
|
|
10
|
+
if (confidence === "EXACT")
|
|
11
|
+
return "verified";
|
|
12
|
+
if (confidence === "RESOLVED" || confidence === "INFERRED")
|
|
13
|
+
return "inferred";
|
|
14
|
+
return "unknown";
|
|
15
|
+
}
|
|
16
|
+
export function nodeId(kind, key) {
|
|
9
17
|
return `${kind}:${createHash("sha1").update(key).digest("hex").slice(0, 12)}`;
|
|
10
18
|
}
|
|
11
|
-
async function listTsFiles(root, limit = 400) {
|
|
19
|
+
export async function listTsFiles(root, limit = 400) {
|
|
12
20
|
const out = [];
|
|
13
21
|
const skip = new Set(["node_modules", ".git", "dist", "coverage", ".agentdoctor", "vendor"]);
|
|
14
22
|
async function walk(dir) {
|
|
@@ -78,6 +86,10 @@ export async function buildIntelligenceGraph(options) {
|
|
|
78
86
|
const nodes = [];
|
|
79
87
|
const edges = [];
|
|
80
88
|
const seen = new Set();
|
|
89
|
+
// TypeScript uses `/` on Windows; path.join uses `\`. Normalize before matching.
|
|
90
|
+
const normalizeFsPath = (p) => path.normalize(p).toLowerCase();
|
|
91
|
+
const fileSet = new Set(files.map(normalizeFsPath));
|
|
92
|
+
const tsconfigPaths = loadTsconfigPaths(root);
|
|
81
93
|
const pushNode = (n) => {
|
|
82
94
|
if (seen.has(n.id))
|
|
83
95
|
return;
|
|
@@ -87,7 +99,7 @@ export async function buildIntelligenceGraph(options) {
|
|
|
87
99
|
for (const sf of program.getSourceFiles()) {
|
|
88
100
|
if (sf.isDeclarationFile)
|
|
89
101
|
continue;
|
|
90
|
-
if (!
|
|
102
|
+
if (!fileSet.has(normalizeFsPath(sf.fileName)))
|
|
91
103
|
continue;
|
|
92
104
|
const rel = toPosixRelative(root, sf.fileName);
|
|
93
105
|
const fileId = nodeId("file", rel);
|
|
@@ -143,21 +155,52 @@ export async function buildIntelligenceGraph(options) {
|
|
|
143
155
|
node.moduleSpecifier &&
|
|
144
156
|
ts.isStringLiteral(node.moduleSpecifier)) {
|
|
145
157
|
const spec = node.moduleSpecifier.text;
|
|
158
|
+
const resolution = resolveImportSpecifier({
|
|
159
|
+
root,
|
|
160
|
+
fromFile: rel,
|
|
161
|
+
specifier: spec,
|
|
162
|
+
tsconfig: tsconfigPaths,
|
|
163
|
+
});
|
|
164
|
+
// Specifier dependency node (always — records the literal import).
|
|
146
165
|
const toId = nodeId("dependency", `${rel}->${spec}`);
|
|
147
166
|
pushNode({
|
|
148
167
|
id: toId,
|
|
149
168
|
kind: "dependency",
|
|
150
169
|
label: spec,
|
|
151
170
|
path: rel,
|
|
152
|
-
meta: {
|
|
171
|
+
meta: {
|
|
172
|
+
specifier: spec,
|
|
173
|
+
parser: "typescript-ast",
|
|
174
|
+
confidence: resolution.confidence,
|
|
175
|
+
...(resolution.resolvedPath ? { resolvedPath: resolution.resolvedPath } : {}),
|
|
176
|
+
...(resolution.via ? { via: resolution.via } : {}),
|
|
177
|
+
},
|
|
153
178
|
});
|
|
154
179
|
edges.push({
|
|
155
180
|
id: nodeId("edge", `${fileId}->${toId}`),
|
|
156
181
|
from: fileId,
|
|
157
182
|
to: toId,
|
|
158
183
|
kind: "imports",
|
|
159
|
-
evidence:
|
|
184
|
+
evidence: evidenceForConfidence(resolution.confidence),
|
|
160
185
|
});
|
|
186
|
+
// Concrete file→file edge only when resolved — never invent UNRESOLVED targets.
|
|
187
|
+
if (resolution.resolvedPath && resolution.confidence !== "UNRESOLVED") {
|
|
188
|
+
const targetFileId = nodeId("file", resolution.resolvedPath);
|
|
189
|
+
pushNode({
|
|
190
|
+
id: targetFileId,
|
|
191
|
+
kind: "file",
|
|
192
|
+
label: path.basename(resolution.resolvedPath),
|
|
193
|
+
path: resolution.resolvedPath,
|
|
194
|
+
meta: { importConfidence: resolution.confidence },
|
|
195
|
+
});
|
|
196
|
+
edges.push({
|
|
197
|
+
id: nodeId("edge", `${fileId}->file:${resolution.resolvedPath}`),
|
|
198
|
+
from: fileId,
|
|
199
|
+
to: targetFileId,
|
|
200
|
+
kind: "imports",
|
|
201
|
+
evidence: evidenceForConfidence(resolution.confidence),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
161
204
|
}
|
|
162
205
|
if (ts.isCallExpression(node) &&
|
|
163
206
|
ts.isIdentifier(node.expression) &&
|
|
@@ -195,6 +238,7 @@ export async function buildIntelligenceGraph(options) {
|
|
|
195
238
|
limitations: [
|
|
196
239
|
"TypeScript AST builder uses the TypeScript compiler API for .ts/.tsx only",
|
|
197
240
|
"Cross-file call resolution is identifier-based (not full type-checker binding)",
|
|
241
|
+
"Import edges use EXACT|RESOLVED|INFERRED confidence; UNRESOLVED never invents file targets",
|
|
198
242
|
"Non-TypeScript languages fall back to regex graph when requested via auto+empty TS set",
|
|
199
243
|
`analysisVersion=${CONTRACTS_VERSION}`,
|
|
200
244
|
],
|