@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
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { type EvidenceManifest, type VerificationStatus } from "./change.js";
|
|
2
|
+
/** Hash integrity of evidence artifacts — separate from engineering checks. */
|
|
3
|
+
export type IntegrityStatus = "HASH_INTEGRITY_VERIFIED" | "HASH_INTEGRITY_FAILED" | "NOT_CHECKED";
|
|
4
|
+
/** Outcome of architecture + policy engineering checks (never claims correctness). */
|
|
5
|
+
export type EngineeringChecksStatus = "NOT_RUN" | "PASSED" | "FAILED" | "INSUFFICIENT_EVIDENCE";
|
|
6
|
+
/**
|
|
7
|
+
* Proof verification lifecycle. INTEGRITY_VERIFIED means hash match only.
|
|
8
|
+
* ENGINEERING_CHECKS_* means architecture+policy ran — not that the change is correct.
|
|
9
|
+
*/
|
|
10
|
+
export type ProofVerificationState = "NOT_RUN" | "ANALYSIS_COMPLETE" | "EVIDENCE_PRODUCED" | "INTEGRITY_VERIFIED" | "ENGINEERING_CHECKS_PASSED" | "ENGINEERING_CHECKS_FAILED" | "INSUFFICIENT_EVIDENCE";
|
|
11
|
+
/** Engineering correctness is never claimed in this release. */
|
|
12
|
+
export type CorrectnessStatus = "ENGINEERING_CORRECTNESS_NOT_CLAIMED";
|
|
13
|
+
export interface ChangeProof {
|
|
14
|
+
proofId: string;
|
|
15
|
+
changeId: string;
|
|
16
|
+
repository: string;
|
|
17
|
+
baseRevision: string | null;
|
|
18
|
+
targetRevision: string | null;
|
|
19
|
+
producer: string;
|
|
20
|
+
producerVersion: string;
|
|
21
|
+
timestamp: string;
|
|
22
|
+
inputs: {
|
|
23
|
+
evidenceDirectory: string;
|
|
24
|
+
evidenceFiles: string[];
|
|
25
|
+
manifestSchemaVersion: string;
|
|
26
|
+
};
|
|
27
|
+
findings: {
|
|
28
|
+
verificationStatus: VerificationStatus;
|
|
29
|
+
fileCount: number;
|
|
30
|
+
hashMatchCount: number;
|
|
31
|
+
hashMismatchCount: number;
|
|
32
|
+
};
|
|
33
|
+
architectureEvidence: {
|
|
34
|
+
present: boolean;
|
|
35
|
+
name: string;
|
|
36
|
+
};
|
|
37
|
+
policyEvidence: {
|
|
38
|
+
present: boolean;
|
|
39
|
+
name: string;
|
|
40
|
+
};
|
|
41
|
+
securityEvidence: {
|
|
42
|
+
present: boolean;
|
|
43
|
+
name: string;
|
|
44
|
+
};
|
|
45
|
+
knowledgeEvidence: {
|
|
46
|
+
present: boolean;
|
|
47
|
+
name: string;
|
|
48
|
+
};
|
|
49
|
+
testEvidence: {
|
|
50
|
+
present: boolean;
|
|
51
|
+
name: string;
|
|
52
|
+
};
|
|
53
|
+
gitEvidence: {
|
|
54
|
+
present: boolean;
|
|
55
|
+
name: string;
|
|
56
|
+
};
|
|
57
|
+
hashes: {
|
|
58
|
+
evidenceManifestSha256: string | null;
|
|
59
|
+
proofContentSha256: string;
|
|
60
|
+
artifactHashes: Array<{
|
|
61
|
+
name: string;
|
|
62
|
+
sha256: string;
|
|
63
|
+
}>;
|
|
64
|
+
};
|
|
65
|
+
parentEvidence?: {
|
|
66
|
+
previousProofHash: string;
|
|
67
|
+
};
|
|
68
|
+
/** Hash integrity only — never means the change is correct. */
|
|
69
|
+
integrityStatus: IntegrityStatus;
|
|
70
|
+
/** Architecture + policy check outcome — separate from integrity. */
|
|
71
|
+
engineeringChecksStatus: EngineeringChecksStatus;
|
|
72
|
+
/** Lifecycle state combining evidence / integrity / engineering checks. */
|
|
73
|
+
verificationState: ProofVerificationState;
|
|
74
|
+
engineeringChecks?: {
|
|
75
|
+
architecture: {
|
|
76
|
+
contractPresent: boolean;
|
|
77
|
+
violations: number;
|
|
78
|
+
importEdgesChecked: number;
|
|
79
|
+
limitations: string[];
|
|
80
|
+
};
|
|
81
|
+
policy: {
|
|
82
|
+
decision: string;
|
|
83
|
+
reason: string;
|
|
84
|
+
policyId: string | null;
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
correctnessStatus: CorrectnessStatus;
|
|
88
|
+
limitations: string[];
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Build a ChangeProof from an existing evidence bundle.
|
|
92
|
+
* integrityStatus reflects hash re-check; engineeringChecksStatus is separate;
|
|
93
|
+
* correctnessStatus is always NOT_CLAIMED.
|
|
94
|
+
*/
|
|
95
|
+
export declare function buildProofFromEvidence(rootInput: string, changeId: string): Promise<ChangeProof>;
|
|
96
|
+
export declare function resolveProofId(rootInput: string, proofIdOrChangeId: string): Promise<{
|
|
97
|
+
proof: ChangeProof;
|
|
98
|
+
path: string;
|
|
99
|
+
} | {
|
|
100
|
+
error: string;
|
|
101
|
+
}>;
|
|
102
|
+
export declare function inspectProof(rootInput: string, proofIdOrChangeId: string): Promise<{
|
|
103
|
+
ok: boolean;
|
|
104
|
+
proof?: ChangeProof;
|
|
105
|
+
path?: string;
|
|
106
|
+
error?: string;
|
|
107
|
+
}>;
|
|
108
|
+
/**
|
|
109
|
+
* Re-hash evidence artifacts and compare to the proof's recorded hashes.
|
|
110
|
+
* Never upgrades correctnessStatus. Tampering → HASH_INTEGRITY_FAILED.
|
|
111
|
+
*/
|
|
112
|
+
export declare function verifyProof(rootInput: string, proofIdOrChangeId: string): Promise<{
|
|
113
|
+
ok: boolean;
|
|
114
|
+
integrityStatus: IntegrityStatus;
|
|
115
|
+
engineeringChecksStatus: EngineeringChecksStatus;
|
|
116
|
+
verificationState: ProofVerificationState;
|
|
117
|
+
correctnessStatus: CorrectnessStatus;
|
|
118
|
+
proof?: ChangeProof;
|
|
119
|
+
checked: Array<{
|
|
120
|
+
name: string;
|
|
121
|
+
match: boolean;
|
|
122
|
+
expected?: string;
|
|
123
|
+
actual?: string;
|
|
124
|
+
}>;
|
|
125
|
+
error?: string;
|
|
126
|
+
}>;
|
|
127
|
+
/**
|
|
128
|
+
* Human-readable explanation of a ChangeProof (does not claim correctness).
|
|
129
|
+
*/
|
|
130
|
+
export declare function explainProof(proof: ChangeProof): string;
|
|
131
|
+
export declare function exportProof(rootInput: string, proofIdOrChangeId: string, outFile: string): Promise<{
|
|
132
|
+
ok: boolean;
|
|
133
|
+
path?: string;
|
|
134
|
+
error?: string;
|
|
135
|
+
}>;
|
|
136
|
+
export type { EvidenceManifest };
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { PACKAGE_VERSION } from "../constants.js";
|
|
5
|
+
import { resolveRepoRoot } from "../utils/path.js";
|
|
6
|
+
import { atomicWriteTextFile } from "../utils/fs.js";
|
|
7
|
+
import { checkArchitectureAtRoot, loadArchitectureContract } from "../architecture/contract.js";
|
|
8
|
+
import { buildIntelligenceGraph } from "../intelligence/graph/build.js";
|
|
9
|
+
import { evaluateAgentAction } from "../platform/firewall/evaluate.js";
|
|
10
|
+
import { inspectEvidence, verifyEvidence, } from "./change.js";
|
|
11
|
+
function sha256(content) {
|
|
12
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
13
|
+
}
|
|
14
|
+
function proofsDir(root) {
|
|
15
|
+
return path.join(root, ".agentdoctor", "proofs");
|
|
16
|
+
}
|
|
17
|
+
function proofPath(root, proofId) {
|
|
18
|
+
return path.join(proofsDir(root), `${proofId}.json`);
|
|
19
|
+
}
|
|
20
|
+
async function readJsonFile(filePath) {
|
|
21
|
+
try {
|
|
22
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
23
|
+
return JSON.parse(raw);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function listProofFiles(root) {
|
|
30
|
+
try {
|
|
31
|
+
const entries = await fs.readdir(proofsDir(root));
|
|
32
|
+
return entries.filter((e) => e.endsWith(".json")).sort();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function presentFlag(files, name) {
|
|
39
|
+
return { present: files.includes(name), name };
|
|
40
|
+
}
|
|
41
|
+
async function runEngineeringChecks(root) {
|
|
42
|
+
const limitations = [
|
|
43
|
+
"Engineering checks are architecture contract + policy evaluate only",
|
|
44
|
+
"Passing engineering checks does not prove correctness, security, or completeness",
|
|
45
|
+
];
|
|
46
|
+
const contractLoaded = await loadArchitectureContract(root);
|
|
47
|
+
const graph = await buildIntelligenceGraph({ root, mode: "auto" });
|
|
48
|
+
const arch = await checkArchitectureAtRoot(root, graph);
|
|
49
|
+
const policy = await evaluateAgentAction(root, {
|
|
50
|
+
actionId: `proof-engineering-${Date.now()}`,
|
|
51
|
+
agentId: "agentdoctor",
|
|
52
|
+
timestamp: new Date().toISOString(),
|
|
53
|
+
type: "shell",
|
|
54
|
+
params: { command: "agentdoctor proof build" },
|
|
55
|
+
repositoryRoot: root,
|
|
56
|
+
});
|
|
57
|
+
const details = {
|
|
58
|
+
architecture: {
|
|
59
|
+
contractPresent: Boolean(contractLoaded),
|
|
60
|
+
violations: arch.violations.length,
|
|
61
|
+
importEdgesChecked: arch.importEdgesChecked,
|
|
62
|
+
limitations: arch.limitations.slice(0, 8),
|
|
63
|
+
},
|
|
64
|
+
policy: {
|
|
65
|
+
decision: policy.decision,
|
|
66
|
+
reason: policy.reason,
|
|
67
|
+
policyId: policy.policyId ?? null,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
if (!contractLoaded) {
|
|
71
|
+
return {
|
|
72
|
+
status: "INSUFFICIENT_EVIDENCE",
|
|
73
|
+
details,
|
|
74
|
+
limitations: [
|
|
75
|
+
...limitations,
|
|
76
|
+
"No architecture contract found — engineering checks require architecture + policy",
|
|
77
|
+
...arch.limitations.slice(0, 4),
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const archOk = arch.violations.length === 0;
|
|
82
|
+
const policyOk = policy.decision === "allow";
|
|
83
|
+
if (archOk && policyOk) {
|
|
84
|
+
return {
|
|
85
|
+
status: "PASSED",
|
|
86
|
+
details,
|
|
87
|
+
limitations: [...limitations, ...arch.limitations.slice(0, 4)],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
status: "FAILED",
|
|
92
|
+
details,
|
|
93
|
+
limitations: [
|
|
94
|
+
...limitations,
|
|
95
|
+
...(archOk ? [] : [`Architecture violations: ${arch.violations.length}`]),
|
|
96
|
+
...(policyOk ? [] : [`Policy decision: ${policy.decision} (${policy.reason})`]),
|
|
97
|
+
...arch.limitations.slice(0, 4),
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function deriveVerificationState(integrityStatus, engineeringChecksStatus) {
|
|
102
|
+
if (integrityStatus === "HASH_INTEGRITY_FAILED") {
|
|
103
|
+
return "EVIDENCE_PRODUCED";
|
|
104
|
+
}
|
|
105
|
+
if (integrityStatus !== "HASH_INTEGRITY_VERIFIED") {
|
|
106
|
+
return "EVIDENCE_PRODUCED";
|
|
107
|
+
}
|
|
108
|
+
if (engineeringChecksStatus === "PASSED") {
|
|
109
|
+
return "ENGINEERING_CHECKS_PASSED";
|
|
110
|
+
}
|
|
111
|
+
if (engineeringChecksStatus === "FAILED") {
|
|
112
|
+
return "ENGINEERING_CHECKS_FAILED";
|
|
113
|
+
}
|
|
114
|
+
if (engineeringChecksStatus === "INSUFFICIENT_EVIDENCE") {
|
|
115
|
+
return "INSUFFICIENT_EVIDENCE";
|
|
116
|
+
}
|
|
117
|
+
return "INTEGRITY_VERIFIED";
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Build a ChangeProof from an existing evidence bundle.
|
|
121
|
+
* integrityStatus reflects hash re-check; engineeringChecksStatus is separate;
|
|
122
|
+
* correctnessStatus is always NOT_CLAIMED.
|
|
123
|
+
*/
|
|
124
|
+
export async function buildProofFromEvidence(rootInput, changeId) {
|
|
125
|
+
const root = resolveRepoRoot(rootInput);
|
|
126
|
+
const inspected = await inspectEvidence({ root, changeId });
|
|
127
|
+
if (!inspected.ok || !inspected.manifest) {
|
|
128
|
+
throw new Error(inspected.error ?? `Evidence bundle not found for ${changeId}`);
|
|
129
|
+
}
|
|
130
|
+
const hashCheck = await verifyEvidence({ root, changeId });
|
|
131
|
+
const integrityStatus = hashCheck.ok
|
|
132
|
+
? "HASH_INTEGRITY_VERIFIED"
|
|
133
|
+
: hashCheck.checked.length > 0
|
|
134
|
+
? "HASH_INTEGRITY_FAILED"
|
|
135
|
+
: "NOT_CHECKED";
|
|
136
|
+
const eng = integrityStatus === "HASH_INTEGRITY_VERIFIED"
|
|
137
|
+
? await runEngineeringChecks(root)
|
|
138
|
+
: {
|
|
139
|
+
status: "NOT_RUN",
|
|
140
|
+
details: {
|
|
141
|
+
architecture: {
|
|
142
|
+
contractPresent: false,
|
|
143
|
+
violations: 0,
|
|
144
|
+
importEdgesChecked: 0,
|
|
145
|
+
limitations: ["Engineering checks skipped because evidence integrity failed"],
|
|
146
|
+
},
|
|
147
|
+
policy: { decision: "not-run", reason: "integrity failed", policyId: null },
|
|
148
|
+
},
|
|
149
|
+
limitations: ["Engineering checks not run when integrity fails"],
|
|
150
|
+
};
|
|
151
|
+
const engineeringChecksStatus = eng.status;
|
|
152
|
+
const verificationState = deriveVerificationState(integrityStatus, engineeringChecksStatus);
|
|
153
|
+
const files = inspected.presentFiles;
|
|
154
|
+
const manifest = inspected.manifest;
|
|
155
|
+
let manifestSha = null;
|
|
156
|
+
try {
|
|
157
|
+
const raw = await fs.readFile(path.join(inspected.directory, "manifest.json"), "utf8");
|
|
158
|
+
manifestSha = sha256(raw);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
manifestSha = null;
|
|
162
|
+
}
|
|
163
|
+
const previous = await findLatestProofForChange(root, changeId);
|
|
164
|
+
const proofId = `prf_${createHash("sha256")
|
|
165
|
+
.update(`${changeId}:${Date.now()}:${randomUUID()}`)
|
|
166
|
+
.digest("hex")
|
|
167
|
+
.slice(0, 16)}`;
|
|
168
|
+
const limitations = [
|
|
169
|
+
"ChangeProof records artifact integrity and optional engineering checks",
|
|
170
|
+
"HASH_INTEGRITY_VERIFIED never means the change is correct or safe",
|
|
171
|
+
"ENGINEERING_CHECKS_PASSED means architecture+policy evaluate succeeded — not product correctness",
|
|
172
|
+
...eng.limitations,
|
|
173
|
+
];
|
|
174
|
+
const draft = {
|
|
175
|
+
proofId,
|
|
176
|
+
changeId,
|
|
177
|
+
repository: root,
|
|
178
|
+
baseRevision: manifest.baseRevision,
|
|
179
|
+
targetRevision: manifest.targetRevision,
|
|
180
|
+
producer: "agentdoctor",
|
|
181
|
+
producerVersion: PACKAGE_VERSION,
|
|
182
|
+
timestamp: new Date().toISOString(),
|
|
183
|
+
inputs: {
|
|
184
|
+
evidenceDirectory: inspected.directory,
|
|
185
|
+
evidenceFiles: files,
|
|
186
|
+
manifestSchemaVersion: manifest.schemaVersion,
|
|
187
|
+
},
|
|
188
|
+
findings: {
|
|
189
|
+
verificationStatus: hashCheck.verificationStatus,
|
|
190
|
+
fileCount: manifest.files.length,
|
|
191
|
+
hashMatchCount: hashCheck.checked.filter((c) => c.match).length,
|
|
192
|
+
hashMismatchCount: hashCheck.checked.filter((c) => !c.match).length,
|
|
193
|
+
},
|
|
194
|
+
architectureEvidence: presentFlag(files, "architecture.json"),
|
|
195
|
+
policyEvidence: presentFlag(files, "policy.json"),
|
|
196
|
+
securityEvidence: presentFlag(files, "security.json"),
|
|
197
|
+
knowledgeEvidence: presentFlag(files, "knowledge.json"),
|
|
198
|
+
testEvidence: presentFlag(files, "tests.json"),
|
|
199
|
+
gitEvidence: presentFlag(files, "git.json"),
|
|
200
|
+
hashes: {
|
|
201
|
+
evidenceManifestSha256: manifestSha,
|
|
202
|
+
artifactHashes: manifest.files.map((f) => ({ name: f.name, sha256: f.sha256 })),
|
|
203
|
+
},
|
|
204
|
+
...(previous
|
|
205
|
+
? { parentEvidence: { previousProofHash: previous.hashes.proofContentSha256 } }
|
|
206
|
+
: {}),
|
|
207
|
+
integrityStatus,
|
|
208
|
+
engineeringChecksStatus,
|
|
209
|
+
verificationState,
|
|
210
|
+
engineeringChecks: eng.details,
|
|
211
|
+
correctnessStatus: "ENGINEERING_CORRECTNESS_NOT_CLAIMED",
|
|
212
|
+
limitations,
|
|
213
|
+
};
|
|
214
|
+
const withoutProofHash = { ...draft, hashes: { ...draft.hashes, proofContentSha256: "" } };
|
|
215
|
+
const proofContentSha256 = sha256(JSON.stringify(withoutProofHash));
|
|
216
|
+
const proof = {
|
|
217
|
+
...draft,
|
|
218
|
+
hashes: {
|
|
219
|
+
evidenceManifestSha256: manifestSha,
|
|
220
|
+
proofContentSha256,
|
|
221
|
+
artifactHashes: draft.hashes.artifactHashes,
|
|
222
|
+
},
|
|
223
|
+
correctnessStatus: "ENGINEERING_CORRECTNESS_NOT_CLAIMED",
|
|
224
|
+
};
|
|
225
|
+
await fs.mkdir(proofsDir(root), { recursive: true });
|
|
226
|
+
await atomicWriteTextFile(proofPath(root, proofId), `${JSON.stringify(proof, null, 2)}\n`);
|
|
227
|
+
return proof;
|
|
228
|
+
}
|
|
229
|
+
async function findLatestProofForChange(root, changeId) {
|
|
230
|
+
const files = await listProofFiles(root);
|
|
231
|
+
let latest = null;
|
|
232
|
+
for (const name of files) {
|
|
233
|
+
const raw = await readJsonFile(path.join(proofsDir(root), name));
|
|
234
|
+
if (!raw || typeof raw !== "object")
|
|
235
|
+
continue;
|
|
236
|
+
const p = raw;
|
|
237
|
+
if (p.changeId !== changeId)
|
|
238
|
+
continue;
|
|
239
|
+
if (!latest || (p.timestamp ?? "") > (latest.timestamp ?? "")) {
|
|
240
|
+
latest = p;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return latest;
|
|
244
|
+
}
|
|
245
|
+
export async function resolveProofId(rootInput, proofIdOrChangeId) {
|
|
246
|
+
const root = resolveRepoRoot(rootInput);
|
|
247
|
+
const direct = path.join(proofsDir(root), `${proofIdOrChangeId}.json`);
|
|
248
|
+
const directRaw = await readJsonFile(direct);
|
|
249
|
+
if (directRaw && typeof directRaw === "object") {
|
|
250
|
+
return { proof: directRaw, path: direct };
|
|
251
|
+
}
|
|
252
|
+
const latest = await findLatestProofForChange(root, proofIdOrChangeId);
|
|
253
|
+
if (latest) {
|
|
254
|
+
return { proof: latest, path: proofPath(root, latest.proofId) };
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
error: `No proof found for id ${proofIdOrChangeId} (tried proofId and changeId)`,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
export async function inspectProof(rootInput, proofIdOrChangeId) {
|
|
261
|
+
const resolved = await resolveProofId(rootInput, proofIdOrChangeId);
|
|
262
|
+
if ("error" in resolved) {
|
|
263
|
+
return { ok: false, error: resolved.error };
|
|
264
|
+
}
|
|
265
|
+
return { ok: true, proof: resolved.proof, path: resolved.path };
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Re-hash evidence artifacts and compare to the proof's recorded hashes.
|
|
269
|
+
* Never upgrades correctnessStatus. Tampering → HASH_INTEGRITY_FAILED.
|
|
270
|
+
*/
|
|
271
|
+
export async function verifyProof(rootInput, proofIdOrChangeId) {
|
|
272
|
+
const resolved = await resolveProofId(rootInput, proofIdOrChangeId);
|
|
273
|
+
if ("error" in resolved) {
|
|
274
|
+
return {
|
|
275
|
+
ok: false,
|
|
276
|
+
integrityStatus: "HASH_INTEGRITY_FAILED",
|
|
277
|
+
engineeringChecksStatus: "NOT_RUN",
|
|
278
|
+
verificationState: "NOT_RUN",
|
|
279
|
+
correctnessStatus: "ENGINEERING_CORRECTNESS_NOT_CLAIMED",
|
|
280
|
+
checked: [],
|
|
281
|
+
error: resolved.error,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
const proof = resolved.proof;
|
|
285
|
+
const evidenceCheck = await verifyEvidence({
|
|
286
|
+
root: rootInput,
|
|
287
|
+
changeId: proof.changeId,
|
|
288
|
+
});
|
|
289
|
+
const checked = evidenceCheck.checked;
|
|
290
|
+
const ok = evidenceCheck.ok;
|
|
291
|
+
const integrityStatus = ok ? "HASH_INTEGRITY_VERIFIED" : "HASH_INTEGRITY_FAILED";
|
|
292
|
+
const engineeringChecksStatus = proof.engineeringChecksStatus ?? "NOT_RUN";
|
|
293
|
+
const verificationState = ok
|
|
294
|
+
? deriveVerificationState(integrityStatus, engineeringChecksStatus)
|
|
295
|
+
: "EVIDENCE_PRODUCED";
|
|
296
|
+
return {
|
|
297
|
+
ok,
|
|
298
|
+
integrityStatus,
|
|
299
|
+
engineeringChecksStatus,
|
|
300
|
+
verificationState,
|
|
301
|
+
correctnessStatus: "ENGINEERING_CORRECTNESS_NOT_CLAIMED",
|
|
302
|
+
proof,
|
|
303
|
+
checked,
|
|
304
|
+
...(evidenceCheck.error ? { error: evidenceCheck.error } : {}),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Human-readable explanation of a ChangeProof (does not claim correctness).
|
|
309
|
+
*/
|
|
310
|
+
export function explainProof(proof) {
|
|
311
|
+
const lines = [
|
|
312
|
+
`ChangeProof ${proof.proofId}`,
|
|
313
|
+
` changeId: ${proof.changeId}`,
|
|
314
|
+
` repository: ${proof.repository}`,
|
|
315
|
+
` base → target: ${proof.baseRevision ?? "(none)"} → ${proof.targetRevision ?? "unknown"}`,
|
|
316
|
+
` integrityStatus: ${proof.integrityStatus}`,
|
|
317
|
+
` engineeringChecksStatus: ${proof.engineeringChecksStatus}`,
|
|
318
|
+
` verificationState: ${proof.verificationState}`,
|
|
319
|
+
` correctnessStatus: ${proof.correctnessStatus}`,
|
|
320
|
+
` evidence files: ${proof.inputs.evidenceFiles.length}`,
|
|
321
|
+
` hash matches: ${proof.findings.hashMatchCount}/${proof.findings.fileCount}`,
|
|
322
|
+
];
|
|
323
|
+
if (proof.engineeringChecks) {
|
|
324
|
+
lines.push(` architecture: contract=${proof.engineeringChecks.architecture.contractPresent} violations=${proof.engineeringChecks.architecture.violations}`, ` policy: ${proof.engineeringChecks.policy.decision} (${proof.engineeringChecks.policy.reason})`);
|
|
325
|
+
}
|
|
326
|
+
lines.push("", "Integrity verifies evidence artifact hashes only.", "Engineering checks (when present) evaluate architecture contract + policy — not that the change is correct.", "Limitations:", ...(proof.limitations ?? []).slice(0, 10).map((l) => ` - ${l}`));
|
|
327
|
+
return lines.join("\n");
|
|
328
|
+
}
|
|
329
|
+
export async function exportProof(rootInput, proofIdOrChangeId, outFile) {
|
|
330
|
+
const inspected = await inspectProof(rootInput, proofIdOrChangeId);
|
|
331
|
+
if (!inspected.ok || !inspected.proof) {
|
|
332
|
+
return { ok: false, error: inspected.error ?? "proof not found" };
|
|
333
|
+
}
|
|
334
|
+
const target = path.resolve(outFile);
|
|
335
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
336
|
+
await atomicWriteTextFile(target, `${JSON.stringify(inspected.proof, null, 2)}\n`);
|
|
337
|
+
return { ok: true, path: target };
|
|
338
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth providers for AgentDoctor.
|
|
3
|
+
*
|
|
4
|
+
* Honesty:
|
|
5
|
+
* - LocalIdentityProvider wraps local-dev team auth (scrypt) — not enterprise SSO.
|
|
6
|
+
* - OidcIdentityProvider validates bearer JWTs (iss/aud/exp/nbf + JWKS) — IMPLEMENTED for the
|
|
7
|
+
* token-validation path. Full browser OAuth redirect / callback server is EXPERIMENTAL /
|
|
8
|
+
* incomplete and must not be claimed as production SSO.
|
|
9
|
+
*/
|
|
10
|
+
import { type JWTPayload, type JSONWebKeySet } from "jose";
|
|
11
|
+
import { type SessionToken, type TeamRole, type TeamUser } from "../team/auth.js";
|
|
12
|
+
import type { StorageProvider } from "../contracts/index.js";
|
|
13
|
+
import { type RbacPermission, type RbacRole } from "./rbac.js";
|
|
14
|
+
export interface IdentityClaims {
|
|
15
|
+
subject: string;
|
|
16
|
+
email?: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
roles: RbacRole[];
|
|
19
|
+
issuer?: string;
|
|
20
|
+
raw?: JWTPayload;
|
|
21
|
+
}
|
|
22
|
+
export interface IdentityProvider {
|
|
23
|
+
readonly kind: "local" | "oidc";
|
|
24
|
+
resolveIdentity(input: Record<string, unknown>): Promise<IdentityClaims | null>;
|
|
25
|
+
}
|
|
26
|
+
export declare class LocalIdentityProvider implements IdentityProvider {
|
|
27
|
+
readonly kind: "local";
|
|
28
|
+
private readonly root;
|
|
29
|
+
private readonly storage;
|
|
30
|
+
constructor(options: {
|
|
31
|
+
root: string;
|
|
32
|
+
storage?: StorageProvider;
|
|
33
|
+
});
|
|
34
|
+
register(options: {
|
|
35
|
+
username: string;
|
|
36
|
+
password: string;
|
|
37
|
+
role?: TeamRole;
|
|
38
|
+
}): Promise<TeamUser>;
|
|
39
|
+
login(options: {
|
|
40
|
+
username: string;
|
|
41
|
+
password: string;
|
|
42
|
+
}): Promise<SessionToken>;
|
|
43
|
+
authorizeSession(token: string, minRole: TeamRole): Promise<{
|
|
44
|
+
ok: boolean;
|
|
45
|
+
user?: TeamUser;
|
|
46
|
+
reason: string;
|
|
47
|
+
}>;
|
|
48
|
+
resolveIdentity(input: Record<string, unknown>): Promise<IdentityClaims | null>;
|
|
49
|
+
}
|
|
50
|
+
export interface OidcIdentityProviderOptions {
|
|
51
|
+
issuer: string;
|
|
52
|
+
audience: string | string[];
|
|
53
|
+
/**
|
|
54
|
+
* Inject JWKS for unit tests / air-gapped validation.
|
|
55
|
+
* When omitted, discovery/JWKS fetch is used (requires network / full_network).
|
|
56
|
+
*/
|
|
57
|
+
jwks?: JSONWebKeySet;
|
|
58
|
+
/** Override JWKS URI when not using discovery (optional). */
|
|
59
|
+
jwksUri?: string;
|
|
60
|
+
/** Map token claim (default: "roles") to RbacRole[]. */
|
|
61
|
+
rolesClaim?: string;
|
|
62
|
+
clockToleranceSec?: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* OIDC/JWT validation provider (jose).
|
|
66
|
+
* IMPLEMENTED: signature + iss + aud + exp + nbf validation.
|
|
67
|
+
* EXPERIMENTAL: browser OAuth authorization-code redirect / local callback server
|
|
68
|
+
* is not a complete production SSO product in this package.
|
|
69
|
+
*/
|
|
70
|
+
export declare class OidcIdentityProvider implements IdentityProvider {
|
|
71
|
+
readonly kind: "oidc";
|
|
72
|
+
private readonly issuer;
|
|
73
|
+
private readonly audience;
|
|
74
|
+
private readonly rolesClaim;
|
|
75
|
+
private readonly clockToleranceSec;
|
|
76
|
+
private readonly getKey;
|
|
77
|
+
constructor(options: OidcIdentityProviderOptions);
|
|
78
|
+
resolveIdentity(input: Record<string, unknown>): Promise<IdentityClaims | null>;
|
|
79
|
+
authorizePermission(claims: IdentityClaims, permission: RbacPermission): Promise<boolean>;
|
|
80
|
+
}
|
|
81
|
+
export { hasRbacPermission, RBAC_ROLE_RANK, type RbacPermission, type RbacRole } from "./rbac.js";
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth providers for AgentDoctor.
|
|
3
|
+
*
|
|
4
|
+
* Honesty:
|
|
5
|
+
* - LocalIdentityProvider wraps local-dev team auth (scrypt) — not enterprise SSO.
|
|
6
|
+
* - OidcIdentityProvider validates bearer JWTs (iss/aud/exp/nbf + JWKS) — IMPLEMENTED for the
|
|
7
|
+
* token-validation path. Full browser OAuth redirect / callback server is EXPERIMENTAL /
|
|
8
|
+
* incomplete and must not be claimed as production SSO.
|
|
9
|
+
*/
|
|
10
|
+
import { createRemoteJWKSet, jwtVerify, createLocalJWKSet, } from "jose";
|
|
11
|
+
import { authenticateLocalDev, authorize, registerLocalDevUser, } from "../team/auth.js";
|
|
12
|
+
import { hasRbacPermission } from "./rbac.js";
|
|
13
|
+
export class LocalIdentityProvider {
|
|
14
|
+
kind = "local";
|
|
15
|
+
root;
|
|
16
|
+
storage;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.root = options.root;
|
|
19
|
+
this.storage = options.storage;
|
|
20
|
+
}
|
|
21
|
+
async register(options) {
|
|
22
|
+
return registerLocalDevUser({
|
|
23
|
+
root: this.root,
|
|
24
|
+
username: options.username,
|
|
25
|
+
password: options.password,
|
|
26
|
+
...(options.role ? { role: options.role } : {}),
|
|
27
|
+
...(this.storage ? { storage: this.storage } : {}),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async login(options) {
|
|
31
|
+
return authenticateLocalDev({
|
|
32
|
+
root: this.root,
|
|
33
|
+
username: options.username,
|
|
34
|
+
password: options.password,
|
|
35
|
+
...(this.storage ? { storage: this.storage } : {}),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async authorizeSession(token, minRole) {
|
|
39
|
+
return authorize(this.root, token, minRole, this.storage);
|
|
40
|
+
}
|
|
41
|
+
async resolveIdentity(input) {
|
|
42
|
+
const token = typeof input.token === "string" ? input.token : null;
|
|
43
|
+
if (!token)
|
|
44
|
+
return null;
|
|
45
|
+
const auth = await authorize(this.root, token, "viewer", this.storage);
|
|
46
|
+
if (!auth.ok || !auth.user)
|
|
47
|
+
return null;
|
|
48
|
+
return {
|
|
49
|
+
subject: auth.user.id,
|
|
50
|
+
name: auth.user.username,
|
|
51
|
+
roles: mapTeamRoleToRbac(auth.user.role),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function mapTeamRoleToRbac(role) {
|
|
56
|
+
switch (role) {
|
|
57
|
+
case "owner":
|
|
58
|
+
case "admin":
|
|
59
|
+
return ["admin"];
|
|
60
|
+
case "member":
|
|
61
|
+
return ["developer"];
|
|
62
|
+
case "viewer":
|
|
63
|
+
return ["viewer"];
|
|
64
|
+
default:
|
|
65
|
+
return ["viewer"];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* OIDC/JWT validation provider (jose).
|
|
70
|
+
* IMPLEMENTED: signature + iss + aud + exp + nbf validation.
|
|
71
|
+
* EXPERIMENTAL: browser OAuth authorization-code redirect / local callback server
|
|
72
|
+
* is not a complete production SSO product in this package.
|
|
73
|
+
*/
|
|
74
|
+
export class OidcIdentityProvider {
|
|
75
|
+
kind = "oidc";
|
|
76
|
+
issuer;
|
|
77
|
+
audience;
|
|
78
|
+
rolesClaim;
|
|
79
|
+
clockToleranceSec;
|
|
80
|
+
getKey;
|
|
81
|
+
constructor(options) {
|
|
82
|
+
this.issuer = options.issuer;
|
|
83
|
+
this.audience = options.audience;
|
|
84
|
+
this.rolesClaim = options.rolesClaim ?? "roles";
|
|
85
|
+
this.clockToleranceSec = options.clockToleranceSec ?? 5;
|
|
86
|
+
if (options.jwks) {
|
|
87
|
+
this.getKey = createLocalJWKSet(options.jwks);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const uri = options.jwksUri ??
|
|
91
|
+
new URL("/.well-known/jwks.json", options.issuer.endsWith("/") ? options.issuer : `${options.issuer}/`).toString();
|
|
92
|
+
this.getKey = createRemoteJWKSet(new URL(uri));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async resolveIdentity(input) {
|
|
96
|
+
const token = typeof input.token === "string" ? input.token : null;
|
|
97
|
+
if (!token)
|
|
98
|
+
return null;
|
|
99
|
+
try {
|
|
100
|
+
const { payload } = await jwtVerify(token, this.getKey, {
|
|
101
|
+
issuer: this.issuer,
|
|
102
|
+
audience: this.audience,
|
|
103
|
+
clockTolerance: this.clockToleranceSec,
|
|
104
|
+
});
|
|
105
|
+
const roles = normalizeRoles(payload[this.rolesClaim]);
|
|
106
|
+
return {
|
|
107
|
+
subject: String(payload.sub ?? ""),
|
|
108
|
+
...(typeof payload.email === "string" ? { email: payload.email } : {}),
|
|
109
|
+
...(typeof payload.name === "string" ? { name: payload.name } : {}),
|
|
110
|
+
roles: roles.length > 0 ? roles : ["viewer"],
|
|
111
|
+
issuer: typeof payload.iss === "string" ? payload.iss : this.issuer,
|
|
112
|
+
raw: payload,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async authorizePermission(claims, permission) {
|
|
120
|
+
return claims.roles.some((role) => hasRbacPermission(role, permission));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function normalizeRoles(value) {
|
|
124
|
+
const allowed = new Set(["viewer", "developer", "reviewer", "policy-admin", "admin"]);
|
|
125
|
+
const list = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
|
|
126
|
+
return list
|
|
127
|
+
.map((v) => String(v).toLowerCase())
|
|
128
|
+
.filter((r) => allowed.has(r));
|
|
129
|
+
}
|
|
130
|
+
export { hasRbacPermission, RBAC_ROLE_RANK } from "./rbac.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role-based access control for AgentDoctor surfaces.
|
|
3
|
+
* Not enterprise SSO — roles are applied after a verified identity (local session or OIDC JWT).
|
|
4
|
+
*/
|
|
5
|
+
export type RbacRole = "viewer" | "developer" | "reviewer" | "policy-admin" | "admin";
|
|
6
|
+
export type RbacPermission = "read-findings" | "read-audit" | "export-reports" | "approve-actions" | "manage-policies" | "admin-settings";
|
|
7
|
+
export declare const RBAC_ROLE_RANK: Record<RbacRole, number>;
|
|
8
|
+
export declare function hasRbacPermission(role: RbacRole, permission: RbacPermission): boolean;
|
|
9
|
+
export declare function roleAtLeast(role: RbacRole, required: RbacRole): boolean;
|