@praneeth_54/agentdoctor 3.0.0 → 3.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 +30 -1
- package/README.md +9 -8
- package/dist/agent/context/retrieve.d.ts +2 -1
- package/dist/agent/context/retrieve.js +14 -1
- package/dist/agent/tools/execute.js +39 -43
- package/dist/agent/tools/write.d.ts +1 -1
- package/dist/agent/tools/write.js +18 -4
- package/dist/ai/providers/adversarial-local.d.ts +7 -0
- package/dist/ai/providers/adversarial-local.js +146 -0
- package/dist/cli/commands/agent.js +13 -3
- package/dist/cli/commands/architecture.js +7 -2
- package/dist/cli/commands/brain.js +7 -2
- package/dist/cli/commands/chat.js +13 -3
- package/dist/cli/commands/complete.js +13 -3
- package/dist/cli/commands/fix.js +6 -0
- package/dist/cli/commands/learn.js +7 -2
- package/dist/cli/commands/mcp.js +7 -3
- package/dist/cli/commands/platform.js +7 -2
- package/dist/cli/commands/policy-graph-run.js +7 -0
- package/dist/cli/commands/product.js +13 -3
- package/dist/cli/commands/scan.js +6 -0
- package/dist/cli/commands/start.js +7 -1
- package/dist/cli/commands/v2.js +61 -11
- package/dist/cli/commands/verify.js +6 -0
- package/dist/cli/safe-root.d.ts +17 -0
- package/dist/cli/safe-root.js +32 -0
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/core/brain-cli/service.js +5 -2
- package/dist/core/monorepo/detect.js +10 -0
- package/dist/core/secrets/scan.js +9 -0
- package/dist/core/understanding/brain/storage/store.d.ts +4 -0
- package/dist/core/understanding/brain/storage/store.js +22 -2
- package/dist/dashboard/page.d.ts +2 -0
- package/dist/dashboard/page.js +53 -0
- package/dist/dashboard/server.js +13 -238
- package/dist/dashboard/ui/client.d.ts +2 -0
- package/dist/dashboard/ui/client.js +2 -0
- package/dist/dashboard/ui/styles.d.ts +2 -0
- package/dist/dashboard/ui/styles.js +138 -0
- package/dist/discovery/files.js +13 -0
- package/dist/intelligence/graph/build.js +10 -30
- package/dist/intelligence/graph/incremental.d.ts +2 -0
- package/dist/intelligence/graph/incremental.js +9 -0
- package/dist/mcp/intelligence/path-safety.d.ts +6 -1
- package/dist/mcp/intelligence/path-safety.js +55 -2
- package/dist/platform/graph/build.js +9 -0
- package/dist/product/decisions/ledger.d.ts +13 -1
- package/dist/product/decisions/ledger.js +41 -10
- package/dist/product/discovery/roots.d.ts +10 -0
- package/dist/product/discovery/roots.js +23 -7
- package/dist/product/graph/enrich-languages.js +7 -32
- package/dist/product/index.d.ts +4 -1
- package/dist/product/index.js +2 -1
- package/dist/product/map/software-map.js +2 -1
- package/dist/product/memory/institutional.js +5 -0
- package/dist/product/search/software-search.js +5 -0
- package/dist/product/twin/store.js +9 -1
- package/dist/product/whatif/engine.js +22 -1
- package/dist/project/ownership.d.ts +65 -0
- package/dist/project/ownership.js +169 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
import { createHash } from "node:crypto";
|
|
4
3
|
import ts from "typescript";
|
|
4
|
+
import { discoverFiles } from "../../discovery/files.js";
|
|
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";
|
|
@@ -45,35 +45,15 @@ async function applyLanguageEnrichment(graph, root) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
export async function listTsFiles(root, limit = 400) {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
catch {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
for (const e of entries) {
|
|
61
|
-
if (skip.has(e.name))
|
|
62
|
-
continue;
|
|
63
|
-
const abs = path.join(dir, e.name);
|
|
64
|
-
if (e.isSymbolicLink())
|
|
65
|
-
continue;
|
|
66
|
-
if (e.isDirectory()) {
|
|
67
|
-
await walk(abs);
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
if (/\.(ts|tsx|mts|cts)$/i.test(e.name) && !e.name.endsWith(".d.ts")) {
|
|
71
|
-
out.push(abs);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
await walk(root);
|
|
76
|
-
return out.sort();
|
|
48
|
+
const discovered = await discoverFiles({ root });
|
|
49
|
+
return discovered.files
|
|
50
|
+
.filter((f) => {
|
|
51
|
+
const name = path.basename(f.relativePath);
|
|
52
|
+
return /\.(ts|tsx|mts|cts)$/i.test(name) && !name.endsWith(".d.ts");
|
|
53
|
+
})
|
|
54
|
+
.map((f) => f.absolutePath)
|
|
55
|
+
.sort()
|
|
56
|
+
.slice(0, limit);
|
|
77
57
|
}
|
|
78
58
|
/**
|
|
79
59
|
* TypeScript compiler API based graph enrichment.
|
|
@@ -12,6 +12,8 @@ export interface GraphSnapshot {
|
|
|
12
12
|
nodes: GraphNode[];
|
|
13
13
|
edges: GraphEdge[];
|
|
14
14
|
limitations: string[];
|
|
15
|
+
/** Reject cached graphs built under a prior ownership boundary. */
|
|
16
|
+
ownershipBoundaryVersion?: number;
|
|
15
17
|
}
|
|
16
18
|
export interface GraphStatus {
|
|
17
19
|
present: boolean;
|
|
@@ -3,6 +3,7 @@ import { spawnSync } from "node:child_process";
|
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { PACKAGE_VERSION } from "../../constants.js";
|
|
6
|
+
import { OWNERSHIP_BOUNDARY_VERSION } from "../../project/ownership.js";
|
|
6
7
|
import { resolveRepoRoot, toPosixRelative } from "../../utils/path.js";
|
|
7
8
|
import { atomicWriteTextFile } from "../../utils/fs.js";
|
|
8
9
|
import { buildIntelligenceGraph, listTsFiles } from "./build.js";
|
|
@@ -75,6 +76,13 @@ async function loadSnapshot(root) {
|
|
|
75
76
|
if (!isValidSnapshot(parsed)) {
|
|
76
77
|
return { ok: false, corrupt: true, error: "index.json failed schema checks" };
|
|
77
78
|
}
|
|
79
|
+
if (parsed.ownershipBoundaryVersion !== OWNERSHIP_BOUNDARY_VERSION) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
corrupt: false,
|
|
83
|
+
error: `graph index ownership boundary is stale (have ${String(parsed.ownershipBoundaryVersion ?? "missing")}, need ${OWNERSHIP_BOUNDARY_VERSION})`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
78
86
|
return { ok: true, snapshot: parsed };
|
|
79
87
|
}
|
|
80
88
|
catch (error) {
|
|
@@ -109,6 +117,7 @@ async function buildAndPersist(root, mode = "auto") {
|
|
|
109
117
|
...graph.limitations,
|
|
110
118
|
"Incremental index persists file hashes for change detection",
|
|
111
119
|
],
|
|
120
|
+
ownershipBoundaryVersion: OWNERSHIP_BOUNDARY_VERSION,
|
|
112
121
|
};
|
|
113
122
|
const saved = await persistSnapshot(root, snapshot);
|
|
114
123
|
return { snapshot, path: saved };
|
|
@@ -4,6 +4,11 @@ export { tryDecodeUriComponent };
|
|
|
4
4
|
* Validate a user-supplied repository-relative or in-repo target.
|
|
5
5
|
* Returns a normalized path relative to root (POSIX) when the target is path-like and safe,
|
|
6
6
|
* or null when the target is treated as a non-path identifier (graph id / label).
|
|
7
|
-
* Throws a safe Error (no outside path leakage) when a path-like target escapes the root
|
|
7
|
+
* Throws a safe Error (no outside path leakage) when a path-like target escapes the root
|
|
8
|
+
* or is outside project ownership (.private, AgentDoctorOS, fixtures, validation).
|
|
8
9
|
*/
|
|
9
10
|
export declare function assertSafeRepoTarget(root: string, rawTarget: string): string | null;
|
|
11
|
+
/**
|
|
12
|
+
* Async ownership gate including nested-repository ancestry (for MCP file-path tools).
|
|
13
|
+
*/
|
|
14
|
+
export declare function assertSafeOwnedRepoTarget(root: string, rawTarget: string): Promise<string | null>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { assertProjectOwnedRepoPath, classifyRelativePathOwnership, ProjectOwnershipError, } from "../../project/ownership.js";
|
|
2
4
|
import { PathEscapeError, rejectHostilePathInput, resolveSafeRepoPath, tryDecodeUriComponent, } from "../../security/paths.js";
|
|
3
5
|
import { sanitizeForOutput } from "../../utils/path.js";
|
|
4
6
|
export { tryDecodeUriComponent };
|
|
@@ -6,7 +8,8 @@ export { tryDecodeUriComponent };
|
|
|
6
8
|
* Validate a user-supplied repository-relative or in-repo target.
|
|
7
9
|
* Returns a normalized path relative to root (POSIX) when the target is path-like and safe,
|
|
8
10
|
* or null when the target is treated as a non-path identifier (graph id / label).
|
|
9
|
-
* Throws a safe Error (no outside path leakage) when a path-like target escapes the root
|
|
11
|
+
* Throws a safe Error (no outside path leakage) when a path-like target escapes the root
|
|
12
|
+
* or is outside project ownership (.private, AgentDoctorOS, fixtures, validation).
|
|
10
13
|
*/
|
|
11
14
|
export function assertSafeRepoTarget(root, rawTarget) {
|
|
12
15
|
if (typeof rawTarget !== "string" || rawTarget.includes("\0")) {
|
|
@@ -52,12 +55,62 @@ export function assertSafeRepoTarget(root, rawTarget) {
|
|
|
52
55
|
const primary = candidates[candidates.length - 1];
|
|
53
56
|
try {
|
|
54
57
|
const abs = resolveSafeRepoPath(root, primary);
|
|
55
|
-
|
|
58
|
+
const lexicalRel = path.isAbsolute(primary)
|
|
59
|
+
? path.relative(root, abs).split(path.sep).join("/")
|
|
60
|
+
: primary.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
61
|
+
// Lexical non-owned prefixes always deny.
|
|
62
|
+
if (classifyRelativePathOwnership(lexicalRel) !== "project_owned") {
|
|
63
|
+
throw new Error("path outside project ownership");
|
|
64
|
+
}
|
|
65
|
+
// Realpath-relative classification catches symlink aliases into non-owned trees.
|
|
66
|
+
let realRoot = root;
|
|
67
|
+
let realAbs = abs;
|
|
68
|
+
try {
|
|
69
|
+
realRoot = fs.realpathSync(root);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
/* keep */
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
realAbs = fs.realpathSync(abs);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
/* keep */
|
|
79
|
+
}
|
|
80
|
+
const realRel = path.relative(realRoot, realAbs).split(path.sep).join("/") || ".";
|
|
81
|
+
if (realRel.startsWith("..") ||
|
|
82
|
+
path.isAbsolute(realRel) ||
|
|
83
|
+
classifyRelativePathOwnership(realRel) !== "project_owned") {
|
|
84
|
+
throw new Error("path outside project ownership");
|
|
85
|
+
}
|
|
86
|
+
return realRel || lexicalRel;
|
|
56
87
|
}
|
|
57
88
|
catch (error) {
|
|
58
89
|
if (error instanceof PathEscapeError) {
|
|
59
90
|
throw new Error(error.message);
|
|
60
91
|
}
|
|
92
|
+
if (error instanceof Error && error.message === "path outside project ownership") {
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
61
95
|
throw new Error("invalid target");
|
|
62
96
|
}
|
|
63
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Async ownership gate including nested-repository ancestry (for MCP file-path tools).
|
|
100
|
+
*/
|
|
101
|
+
export async function assertSafeOwnedRepoTarget(root, rawTarget) {
|
|
102
|
+
const rel = assertSafeRepoTarget(root, rawTarget);
|
|
103
|
+
if (rel === null)
|
|
104
|
+
return null;
|
|
105
|
+
try {
|
|
106
|
+
const abs = resolveSafeRepoPath(root, rel);
|
|
107
|
+
await assertProjectOwnedRepoPath(root, abs, rel);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (error instanceof PathEscapeError || error instanceof ProjectOwnershipError) {
|
|
111
|
+
throw new Error(error.message);
|
|
112
|
+
}
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
return rel;
|
|
116
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
+
import { decideDirectoryTraversal } from "../../project/ownership.js";
|
|
4
5
|
import { readTextFile } from "../../utils/fs.js";
|
|
5
6
|
import { resolveRepoRoot, toPosixRelative } from "../../utils/path.js";
|
|
6
7
|
const SKIP = new Set([
|
|
@@ -48,6 +49,14 @@ async function walk(root, dir, files, dirs, limit) {
|
|
|
48
49
|
if (entry.isSymbolicLink())
|
|
49
50
|
continue;
|
|
50
51
|
if (entry.isDirectory()) {
|
|
52
|
+
const relativeDir = toPosixRelative(root, absolute);
|
|
53
|
+
const traversal = await decideDirectoryTraversal({
|
|
54
|
+
projectRoot: root,
|
|
55
|
+
absoluteDir: absolute,
|
|
56
|
+
relativeDir,
|
|
57
|
+
});
|
|
58
|
+
if (!traversal.traverse)
|
|
59
|
+
continue;
|
|
51
60
|
dirs.push(absolute);
|
|
52
61
|
await walk(root, absolute, files, dirs, limit);
|
|
53
62
|
continue;
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
+
import { type ProjectOwnershipClass } from "../../project/ownership.js";
|
|
1
2
|
import type { ProductEvidence, TruthLabel } from "../truth.js";
|
|
3
|
+
/** Why a decision document is attributed to the current project. */
|
|
4
|
+
export type DecisionSourceKind = "adr_file" | "decision_ledger" | "architecture_document" | "internal_note" | "unknown";
|
|
2
5
|
export interface DecisionRecord {
|
|
3
6
|
id: string;
|
|
4
7
|
title: string;
|
|
5
8
|
status?: string;
|
|
9
|
+
/** Wire-compat provenance channel. */
|
|
6
10
|
source: "adr-file" | "ledger";
|
|
11
|
+
sourceKind: DecisionSourceKind;
|
|
7
12
|
truth: TruthLabel;
|
|
13
|
+
/** Explicit meaning for UI — never “architecture proven correct”. */
|
|
14
|
+
truthMeaning: string;
|
|
15
|
+
projectRoot: string;
|
|
16
|
+
ownership: ProjectOwnershipClass;
|
|
8
17
|
evidence: ProductEvidence[];
|
|
9
18
|
recordedAt?: string;
|
|
10
19
|
bodyExcerpt?: string;
|
|
@@ -16,9 +25,12 @@ export interface DecisionLedgerResult {
|
|
|
16
25
|
limitations: string[];
|
|
17
26
|
}
|
|
18
27
|
export declare function parseAdrDecisions(rootInput: string, maxFileSizeBytes?: number): Promise<DecisionRecord[]>;
|
|
19
|
-
export declare function appendDecisionLedgerEntry(rootInput: string, record: Omit<DecisionRecord, "source" | "truth"> & {
|
|
28
|
+
export declare function appendDecisionLedgerEntry(rootInput: string, record: Omit<DecisionRecord, "source" | "truth" | "sourceKind" | "truthMeaning" | "projectRoot" | "ownership"> & {
|
|
20
29
|
source?: DecisionRecord["source"];
|
|
30
|
+
sourceKind?: DecisionSourceKind;
|
|
21
31
|
truth?: TruthLabel;
|
|
32
|
+
truthMeaning?: string;
|
|
33
|
+
ownership?: ProjectOwnershipClass;
|
|
22
34
|
}): Promise<DecisionRecord>;
|
|
23
35
|
export declare function loadDecisionLedger(rootInput: string): Promise<DecisionLedgerResult>;
|
|
24
36
|
export declare function resolveDecisionPath(rootInput: string, candidate: string): string;
|
|
@@ -2,21 +2,26 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { DEFAULT_MAX_FILE_SIZE_BYTES } from "../../constants.js";
|
|
4
4
|
import { detectProject } from "../../detectors/project.js";
|
|
5
|
-
import {
|
|
5
|
+
import { classifyRelativePathOwnership, isProjectOwnedRelativePath, verifiedDecisionTruthMeaning, } from "../../project/ownership.js";
|
|
6
6
|
import { resolveSafeRepoPath } from "../../security/paths.js";
|
|
7
|
+
import { pathExists, readTextFile } from "../../utils/fs.js";
|
|
7
8
|
import { resolveRepoRoot } from "../../utils/path.js";
|
|
8
|
-
import { pathExists } from "../../utils/fs.js";
|
|
9
9
|
function ledgerFile(root) {
|
|
10
10
|
return path.join(resolveRepoRoot(root), ".agentdoctor", "decisions", "entries.jsonl");
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* ADR-shaped paths under the current project-owned tree only.
|
|
14
|
+
* Nested foreign trees with an adr directory segment are excluded by ownership
|
|
15
|
+
* before this runs; this still refuses internal_docs / private paths as defense in depth.
|
|
16
|
+
*/
|
|
12
17
|
function isAdrPath(relativePath) {
|
|
18
|
+
if (!isProjectOwnedRelativePath(relativePath))
|
|
19
|
+
return false;
|
|
13
20
|
const norm = relativePath.replace(/\\/g, "/").toLowerCase();
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
norm.includes("/adr/") ||
|
|
17
|
-
/^adr-\d+/i.test(path.basename(relativePath)));
|
|
21
|
+
const base = path.basename(relativePath);
|
|
22
|
+
return norm.startsWith("docs/adr/") || norm.startsWith("adr/") || /^adr[-_]\d+/i.test(base);
|
|
18
23
|
}
|
|
19
|
-
function parseAdr(relativePath, content) {
|
|
24
|
+
function parseAdr(projectRoot, relativePath, content) {
|
|
20
25
|
const lines = content.split(/\r?\n/);
|
|
21
26
|
let title = "";
|
|
22
27
|
let status;
|
|
@@ -31,12 +36,17 @@ function parseAdr(relativePath, content) {
|
|
|
31
36
|
if (!title)
|
|
32
37
|
return null;
|
|
33
38
|
const id = relativePath.replace(/[^\w./-]/g, "_");
|
|
39
|
+
const ownership = classifyRelativePathOwnership(relativePath);
|
|
34
40
|
return {
|
|
35
41
|
id,
|
|
36
42
|
title,
|
|
37
43
|
...(status !== undefined ? { status } : {}),
|
|
38
44
|
source: "adr-file",
|
|
45
|
+
sourceKind: "adr_file",
|
|
39
46
|
truth: "VERIFIED",
|
|
47
|
+
truthMeaning: verifiedDecisionTruthMeaning(),
|
|
48
|
+
projectRoot,
|
|
49
|
+
ownership,
|
|
40
50
|
evidence: [{ path: relativePath, line: 1, excerpt: title.slice(0, 120) }],
|
|
41
51
|
bodyExcerpt: content.slice(0, 400),
|
|
42
52
|
};
|
|
@@ -52,7 +62,7 @@ export async function parseAdrDecisions(rootInput, maxFileSizeBytes = DEFAULT_MA
|
|
|
52
62
|
const text = await readTextFile(path.join(root, rel), maxFileSizeBytes);
|
|
53
63
|
if (!text)
|
|
54
64
|
continue;
|
|
55
|
-
const parsed = parseAdr(rel, text);
|
|
65
|
+
const parsed = parseAdr(root, rel, text);
|
|
56
66
|
if (parsed)
|
|
57
67
|
decisions.push(parsed);
|
|
58
68
|
}
|
|
@@ -66,7 +76,11 @@ export async function appendDecisionLedgerEntry(rootInput, record) {
|
|
|
66
76
|
const full = {
|
|
67
77
|
...record,
|
|
68
78
|
source: record.source ?? "ledger",
|
|
79
|
+
sourceKind: record.sourceKind ?? "decision_ledger",
|
|
69
80
|
truth: record.truth ?? "VERIFIED",
|
|
81
|
+
truthMeaning: record.truthMeaning ?? verifiedDecisionTruthMeaning(),
|
|
82
|
+
projectRoot: root,
|
|
83
|
+
ownership: record.ownership ?? "project_owned",
|
|
70
84
|
recordedAt: record.recordedAt ?? new Date().toISOString(),
|
|
71
85
|
};
|
|
72
86
|
await fs.appendFile(ledgerFile(root), `${JSON.stringify(full)}\n`, "utf8");
|
|
@@ -76,7 +90,9 @@ export async function loadDecisionLedger(rootInput) {
|
|
|
76
90
|
const root = resolveRepoRoot(rootInput);
|
|
77
91
|
const limitations = [
|
|
78
92
|
"ADR parsing reads markdown titles and Status lines only.",
|
|
79
|
-
"
|
|
93
|
+
"Only project-owned trees are scanned; nested repositories and private/validation checkouts are excluded.",
|
|
94
|
+
"VERIFIED means the ADR file was found and parsed — not that the architecture decision is independently proven correct.",
|
|
95
|
+
"Ledger JSONL entries are merged with discovered ADR files (deduped by id) when present and project-owned.",
|
|
80
96
|
];
|
|
81
97
|
const fromAdr = await parseAdrDecisions(root);
|
|
82
98
|
const fromLedger = [];
|
|
@@ -87,7 +103,22 @@ export async function loadDecisionLedger(rootInput) {
|
|
|
87
103
|
if (!line.trim())
|
|
88
104
|
continue;
|
|
89
105
|
try {
|
|
90
|
-
|
|
106
|
+
const raw = JSON.parse(line);
|
|
107
|
+
const evidencePath = raw.evidence?.[0]?.path ?? raw.id;
|
|
108
|
+
if (!isProjectOwnedRelativePath(String(evidencePath)) &&
|
|
109
|
+
!isProjectOwnedRelativePath(String(raw.id))) {
|
|
110
|
+
limitations.push(`Skipped non-owned ledger entry: ${raw.id}`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
fromLedger.push({
|
|
114
|
+
...raw,
|
|
115
|
+
source: raw.source ?? "ledger",
|
|
116
|
+
sourceKind: raw.sourceKind ?? "decision_ledger",
|
|
117
|
+
truth: raw.truth ?? "VERIFIED",
|
|
118
|
+
truthMeaning: raw.truthMeaning ?? verifiedDecisionTruthMeaning(),
|
|
119
|
+
projectRoot: raw.projectRoot ?? root,
|
|
120
|
+
ownership: raw.ownership ?? classifyRelativePathOwnership(String(evidencePath)),
|
|
121
|
+
});
|
|
91
122
|
}
|
|
92
123
|
catch {
|
|
93
124
|
limitations.push("Skipped malformed decision ledger line.");
|
|
@@ -25,6 +25,16 @@ export interface DiscoverProjectOptions {
|
|
|
25
25
|
/** Auto-select highest score when exactly one strong candidate. */
|
|
26
26
|
autoSelect?: boolean;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* True when `cwd` is the user home directory or a similarly unsafe broad folder
|
|
30
|
+
* (Desktop / Downloads / Documents). Used to refuse silent whole-home scans.
|
|
31
|
+
*/
|
|
32
|
+
export declare function classifyBroadUserScanRoot(cwdInput: string, homeInput?: string): {
|
|
33
|
+
blocked: true;
|
|
34
|
+
reason: string;
|
|
35
|
+
} | {
|
|
36
|
+
blocked: false;
|
|
37
|
+
};
|
|
28
38
|
/**
|
|
29
39
|
* Discover likely project roots from cwd without silently scanning home/Desktop/Downloads
|
|
30
40
|
* or unbounded parent trees.
|
|
@@ -84,6 +84,26 @@ function scoreMarkers(markers) {
|
|
|
84
84
|
}
|
|
85
85
|
return score;
|
|
86
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* True when `cwd` is the user home directory or a similarly unsafe broad folder
|
|
89
|
+
* (Desktop / Downloads / Documents). Used to refuse silent whole-home scans.
|
|
90
|
+
*/
|
|
91
|
+
export function classifyBroadUserScanRoot(cwdInput, homeInput) {
|
|
92
|
+
const cwd = path.resolve(cwdInput);
|
|
93
|
+
const home = normalizeHome(homeInput ?? os.homedir());
|
|
94
|
+
const baselower = path.basename(cwd).toLowerCase();
|
|
95
|
+
if (cwd === home ||
|
|
96
|
+
SUSPICIOUS_DIR_NAMES.has(baselower) ||
|
|
97
|
+
isUnderHomeSubtree(cwd, home, "Desktop") ||
|
|
98
|
+
isUnderHomeSubtree(cwd, home, "Downloads") ||
|
|
99
|
+
isUnderHomeSubtree(cwd, home, "Documents")) {
|
|
100
|
+
return {
|
|
101
|
+
blocked: true,
|
|
102
|
+
reason: "Refusing to scan home / Desktop / Downloads / Documents (or similarly broad user folders) as a project root. Navigate into a project directory or pass an explicit project path.",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return { blocked: false };
|
|
106
|
+
}
|
|
87
107
|
/**
|
|
88
108
|
* Discover likely project roots from cwd without silently scanning home/Desktop/Downloads
|
|
89
109
|
* or unbounded parent trees.
|
|
@@ -96,12 +116,8 @@ export async function discoverProjectRoots(options = {}) {
|
|
|
96
116
|
"Discovery uses shallow marker probes and bounded entry counts — not a full repository index.",
|
|
97
117
|
"Candidates are VERIFIED only when marker files/directories exist on disk.",
|
|
98
118
|
];
|
|
99
|
-
const
|
|
100
|
-
if (
|
|
101
|
-
SUSPICIOUS_DIR_NAMES.has(baselower) ||
|
|
102
|
-
isUnderHomeSubtree(cwd, home, "Desktop") ||
|
|
103
|
-
isUnderHomeSubtree(cwd, home, "Downloads") ||
|
|
104
|
-
isUnderHomeSubtree(cwd, home, "Documents")) {
|
|
119
|
+
const broad = classifyBroadUserScanRoot(cwd, home);
|
|
120
|
+
if (broad.blocked) {
|
|
105
121
|
const estimatedEntries = await countEntriesShallow(cwd, Math.min(maxEntries, 5_000));
|
|
106
122
|
return {
|
|
107
123
|
cwd,
|
|
@@ -109,7 +125,7 @@ export async function discoverProjectRoots(options = {}) {
|
|
|
109
125
|
candidates: [],
|
|
110
126
|
selected: null,
|
|
111
127
|
blocked: true,
|
|
112
|
-
blockReason:
|
|
128
|
+
blockReason: broad.reason,
|
|
113
129
|
estimatedEntries,
|
|
114
130
|
limitations,
|
|
115
131
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
import { createHash } from "node:crypto";
|
|
4
3
|
import { getAdapterForFile, parseSourceFile } from "../../languages/index.js";
|
|
4
|
+
import { discoverFiles } from "../../discovery/files.js";
|
|
5
5
|
import { readTextFile } from "../../utils/fs.js";
|
|
6
6
|
import { resolveRepoRoot, toPosixRelative } from "../../utils/path.js";
|
|
7
7
|
function nodeId(kind, key) {
|
|
@@ -9,37 +9,12 @@ function nodeId(kind, key) {
|
|
|
9
9
|
}
|
|
10
10
|
const LANG_FILE_RE = /\.(py|php|go|java|kt|kts|rs|dart)$/i;
|
|
11
11
|
async function listLanguageSourceFiles(root, limit = 120) {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
for (const e of entries) {
|
|
25
|
-
if (out.length >= limit)
|
|
26
|
-
return;
|
|
27
|
-
if (skip.has(e.name))
|
|
28
|
-
continue;
|
|
29
|
-
const abs = path.join(dir, e.name);
|
|
30
|
-
if (e.isSymbolicLink())
|
|
31
|
-
continue;
|
|
32
|
-
if (e.isDirectory()) {
|
|
33
|
-
await walk(abs);
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
if (LANG_FILE_RE.test(e.name)) {
|
|
37
|
-
out.push(abs);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
await walk(root);
|
|
42
|
-
return out.sort();
|
|
12
|
+
const discovered = await discoverFiles({ root });
|
|
13
|
+
return discovered.files
|
|
14
|
+
.filter((f) => LANG_FILE_RE.test(f.relativePath))
|
|
15
|
+
.map((f) => f.absolutePath)
|
|
16
|
+
.sort()
|
|
17
|
+
.slice(0, limit);
|
|
43
18
|
}
|
|
44
19
|
function evidenceFromAdapter(kind) {
|
|
45
20
|
if (kind === "ast")
|
package/dist/product/index.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ export { analyzeInfra } from "./ops/infra.js";
|
|
|
39
39
|
export type { InfraReport, InfraArtifact } from "./ops/infra.js";
|
|
40
40
|
export { buildIncidentHypotheses } from "./ops/incident.js";
|
|
41
41
|
export type { IncidentReport, IncidentTimelineItem } from "./ops/incident.js";
|
|
42
|
-
export { discoverProjectRoots, resolveStartedProjectRoot } from "./discovery/roots.js";
|
|
42
|
+
export { discoverProjectRoots, resolveStartedProjectRoot, classifyBroadUserScanRoot, } from "./discovery/roots.js";
|
|
43
43
|
export type { ProjectDiscoveryReport, ProjectCandidate, DiscoverProjectOptions, } from "./discovery/roots.js";
|
|
44
44
|
export { buildProjectDna, persistProjectDna } from "./dna/build.js";
|
|
45
45
|
export type { ProjectDna } from "./dna/build.js";
|
|
@@ -67,3 +67,6 @@ export { evaluateApprovalRecord, formatApprovalRecordSummary } from "./approval/
|
|
|
67
67
|
export type { ApprovalRecord, ApprovalState, ApprovalRisk, ApprovalEvaluationInput, ApprovalEvaluationResult, } from "./approval/model.js";
|
|
68
68
|
export { issueApprovalGrant, consumeApprovalGrant, hashPlanPayload, hashFileWritePlan, } from "./approval/session.js";
|
|
69
69
|
export type { ApprovalGrant, ConsumeApprovalResult } from "./approval/session.js";
|
|
70
|
+
export { classifyRelativePathOwnership, decideDirectoryTraversal, isNestedRepositoryRoot, isProjectOwnedRelativePath, isUnderNestedRepository, assertProjectOwnedRepoPath, ProjectOwnershipError, OWNERSHIP_BOUNDARY_VERSION, verifiedDecisionTruthMeaning, } from "../project/ownership.js";
|
|
71
|
+
export type { ProjectOwnershipClass, DirectoryTraversalDecision } from "../project/ownership.js";
|
|
72
|
+
export type { DecisionSourceKind } from "./decisions/ledger.js";
|
package/dist/product/index.js
CHANGED
|
@@ -19,7 +19,7 @@ export { runEvalLab } from "./eval/lab.js";
|
|
|
19
19
|
export { diagnoseAgentDoctorSelf } from "./self/diagnose.js";
|
|
20
20
|
export { analyzeInfra } from "./ops/infra.js";
|
|
21
21
|
export { buildIncidentHypotheses } from "./ops/incident.js";
|
|
22
|
-
export { discoverProjectRoots, resolveStartedProjectRoot } from "./discovery/roots.js";
|
|
22
|
+
export { discoverProjectRoots, resolveStartedProjectRoot, classifyBroadUserScanRoot, } from "./discovery/roots.js";
|
|
23
23
|
export { buildProjectDna, persistProjectDna } from "./dna/build.js";
|
|
24
24
|
export { enrichGraphWithLanguageAdapters } from "./graph/enrich-languages.js";
|
|
25
25
|
export { analyzeSecuritySurface } from "./security/doctor.js";
|
|
@@ -33,3 +33,4 @@ export { buildSoftwareEvolutionTimeline } from "./evolution/timeline.js";
|
|
|
33
33
|
export { queryMemory } from "./memory/institutional.js";
|
|
34
34
|
export { evaluateApprovalRecord, formatApprovalRecordSummary } from "./approval/model.js";
|
|
35
35
|
export { issueApprovalGrant, consumeApprovalGrant, hashPlanPayload, hashFileWritePlan, } from "./approval/session.js";
|
|
36
|
+
export { classifyRelativePathOwnership, decideDirectoryTraversal, isNestedRepositoryRoot, isProjectOwnedRelativePath, isUnderNestedRepository, assertProjectOwnedRepoPath, ProjectOwnershipError, OWNERSHIP_BOUNDARY_VERSION, verifiedDecisionTruthMeaning, } from "../project/ownership.js";
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { DEFAULT_MAX_FILE_SIZE_BYTES } from "../../constants.js";
|
|
4
4
|
import { detectProject } from "../../detectors/project.js";
|
|
5
5
|
import { detectMonorepo } from "../../detectors/monorepo.js";
|
|
6
|
+
import { isProjectOwnedRelativePath } from "../../project/ownership.js";
|
|
6
7
|
import { resolveRepoRoot } from "../../utils/path.js";
|
|
7
8
|
function nodeId(prefix, p) {
|
|
8
9
|
return `${prefix}:${p.replace(/[^\w./-]/g, "_")}`;
|
|
@@ -11,7 +12,7 @@ async function topLevelEntries(root) {
|
|
|
11
12
|
try {
|
|
12
13
|
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
13
14
|
return entries
|
|
14
|
-
.filter((e) => e.isDirectory() && !e.name.startsWith("."))
|
|
15
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith(".") && isProjectOwnedRelativePath(e.name))
|
|
15
16
|
.map((e) => e.name)
|
|
16
17
|
.sort();
|
|
17
18
|
}
|
|
@@ -19,6 +19,11 @@ export async function queryMemory(rootInput, query) {
|
|
|
19
19
|
const brain = await loadLatestBrain(root);
|
|
20
20
|
if (brain) {
|
|
21
21
|
for (const bh of searchBrain(brain, q).slice(0, 15)) {
|
|
22
|
+
if (bh.text.includes(".private/") ||
|
|
23
|
+
bh.text.includes("AgentDoctorOS/") ||
|
|
24
|
+
bh.text.includes("oss-validation/")) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
22
27
|
hits.push({
|
|
23
28
|
source: "brain",
|
|
24
29
|
id: bh.id,
|
|
@@ -22,6 +22,11 @@ export async function searchSymbolsAndConcepts(rootInput, query, options) {
|
|
|
22
22
|
const brain = await loadLatestBrain(root);
|
|
23
23
|
if (brain) {
|
|
24
24
|
for (const bh of searchBrain(brain, q)) {
|
|
25
|
+
if (bh.text.includes(".private/") ||
|
|
26
|
+
bh.text.includes("AgentDoctorOS/") ||
|
|
27
|
+
bh.text.includes("oss-validation/")) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
25
30
|
hits.push({
|
|
26
31
|
kind: "brain",
|
|
27
32
|
path: ".agentdoctor/project-brain",
|
|
@@ -5,6 +5,7 @@ import { DEFAULT_MAX_FILE_SIZE_BYTES } from "../../constants.js";
|
|
|
5
5
|
import { atomicWriteTextFile, pathExists, readJsonFile } from "../../utils/fs.js";
|
|
6
6
|
import { resolveRepoRoot } from "../../utils/path.js";
|
|
7
7
|
import { buildSoftwareDigitalTwinSnapshot } from "./digital-twin.js";
|
|
8
|
+
import { OWNERSHIP_BOUNDARY_VERSION } from "../../project/ownership.js";
|
|
8
9
|
function twinDir(root) {
|
|
9
10
|
return path.join(resolveRepoRoot(root), ".agentdoctor", "twin");
|
|
10
11
|
}
|
|
@@ -12,7 +13,10 @@ function snapshotPath(root) {
|
|
|
12
13
|
return path.join(twinDir(root), "snapshot.json");
|
|
13
14
|
}
|
|
14
15
|
function hashInputs(changedFiles) {
|
|
15
|
-
const payload =
|
|
16
|
+
const payload = [
|
|
17
|
+
`ownership-boundary:${OWNERSHIP_BOUNDARY_VERSION}`,
|
|
18
|
+
...(changedFiles ?? []).slice().sort(),
|
|
19
|
+
].join("\n");
|
|
16
20
|
return createHash("sha256").update(payload).digest("hex").slice(0, 16);
|
|
17
21
|
}
|
|
18
22
|
export async function loadTwinSnapshot(rootInput) {
|
|
@@ -22,6 +26,10 @@ export async function loadTwinSnapshot(rootInput) {
|
|
|
22
26
|
return null;
|
|
23
27
|
if (parsed.data.meta.root !== root)
|
|
24
28
|
return null;
|
|
29
|
+
// Reject twins built under a prior ownership boundary (or any mismatched hash).
|
|
30
|
+
const expected = hashInputs(parsed.data.meta.changedFiles);
|
|
31
|
+
if (parsed.data.meta.invalidationHash !== expected)
|
|
32
|
+
return null;
|
|
25
33
|
return parsed.data;
|
|
26
34
|
}
|
|
27
35
|
export async function saveTwinSnapshot(rootInput, twin, meta) {
|
|
@@ -1,16 +1,37 @@
|
|
|
1
1
|
import { deriveGraphChangeImpact } from "../../assurance/change.js";
|
|
2
2
|
import { buildIntelligenceGraph } from "../../intelligence/graph/build.js";
|
|
3
|
+
import { assertProjectOwnedRepoPath, classifyRelativePathOwnership, ProjectOwnershipError, } from "../../project/ownership.js";
|
|
4
|
+
import { PathEscapeError, resolveSafeRepoPath } from "../../security/paths.js";
|
|
3
5
|
import { resolveRepoRoot } from "../../utils/path.js";
|
|
4
6
|
import { minProductTruth } from "../truth.js";
|
|
5
7
|
function normalizeTarget(target) {
|
|
6
8
|
return target.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
7
9
|
}
|
|
10
|
+
async function assertOwnedWhatIfTarget(root, target) {
|
|
11
|
+
const normalized = normalizeTarget(target);
|
|
12
|
+
const lexical = classifyRelativePathOwnership(normalized);
|
|
13
|
+
if (lexical !== "project_owned") {
|
|
14
|
+
throw new ProjectOwnershipError(lexical, `what-if target outside project ownership (${lexical})`);
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const abs = resolveSafeRepoPath(root, normalized);
|
|
18
|
+
await assertProjectOwnedRepoPath(root, abs, normalized);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error instanceof PathEscapeError || error instanceof ProjectOwnershipError) {
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
// Non-existent owned path remains a valid hypothetical change target.
|
|
25
|
+
}
|
|
26
|
+
return normalized;
|
|
27
|
+
}
|
|
8
28
|
export async function analyzeWhatIf(rootInput, target, options) {
|
|
9
29
|
const root = resolveRepoRoot(rootInput);
|
|
10
|
-
const normalized =
|
|
30
|
+
const normalized = await assertOwnedWhatIfTarget(root, target);
|
|
11
31
|
const limitations = [
|
|
12
32
|
"Impact analysis uses intelligence graph import/call edges when available.",
|
|
13
33
|
"Symbol-level precision depends on graph node coverage.",
|
|
34
|
+
"What-if targets must be project-owned paths (containment alone is not ownership).",
|
|
14
35
|
];
|
|
15
36
|
let graph = options?.graph;
|
|
16
37
|
if (!graph && options?.buildGraph !== false) {
|