corpus-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/autofix.d.ts +41 -0
  2. package/dist/autofix.js +159 -0
  3. package/dist/constants.d.ts +9 -0
  4. package/dist/constants.js +9 -0
  5. package/dist/cve-database.json +396 -0
  6. package/dist/cve-patterns.d.ts +54 -0
  7. package/dist/cve-patterns.js +124 -0
  8. package/dist/engine.d.ts +6 -0
  9. package/dist/engine.js +71 -0
  10. package/dist/graph-engine.d.ts +56 -0
  11. package/dist/graph-engine.js +412 -0
  12. package/dist/index.d.ts +17 -0
  13. package/dist/index.js +17 -0
  14. package/dist/log.d.ts +7 -0
  15. package/dist/log.js +33 -0
  16. package/dist/logger.d.ts +1 -0
  17. package/dist/logger.js +12 -0
  18. package/dist/memory.d.ts +67 -0
  19. package/dist/memory.js +261 -0
  20. package/dist/pattern-learner.d.ts +82 -0
  21. package/dist/pattern-learner.js +420 -0
  22. package/dist/scanners/code-safety.d.ts +13 -0
  23. package/dist/scanners/code-safety.js +114 -0
  24. package/dist/scanners/confidence-calibrator.d.ts +25 -0
  25. package/dist/scanners/confidence-calibrator.js +58 -0
  26. package/dist/scanners/context-poisoning.d.ts +18 -0
  27. package/dist/scanners/context-poisoning.js +48 -0
  28. package/dist/scanners/cross-user-firewall.d.ts +10 -0
  29. package/dist/scanners/cross-user-firewall.js +24 -0
  30. package/dist/scanners/dependency-checker.d.ts +15 -0
  31. package/dist/scanners/dependency-checker.js +203 -0
  32. package/dist/scanners/exfiltration-guard.d.ts +19 -0
  33. package/dist/scanners/exfiltration-guard.js +49 -0
  34. package/dist/scanners/index.d.ts +12 -0
  35. package/dist/scanners/index.js +12 -0
  36. package/dist/scanners/injection-firewall.d.ts +12 -0
  37. package/dist/scanners/injection-firewall.js +71 -0
  38. package/dist/scanners/scope-enforcer.d.ts +10 -0
  39. package/dist/scanners/scope-enforcer.js +30 -0
  40. package/dist/scanners/secret-detector.d.ts +34 -0
  41. package/dist/scanners/secret-detector.js +188 -0
  42. package/dist/scanners/session-hijack.d.ts +16 -0
  43. package/dist/scanners/session-hijack.js +53 -0
  44. package/dist/scanners/trust-score.d.ts +34 -0
  45. package/dist/scanners/trust-score.js +164 -0
  46. package/dist/scanners/undo-integrity.d.ts +9 -0
  47. package/dist/scanners/undo-integrity.js +38 -0
  48. package/dist/subprocess.d.ts +10 -0
  49. package/dist/subprocess.js +103 -0
  50. package/dist/types.d.ts +117 -0
  51. package/dist/types.js +16 -0
  52. package/dist/yaml-evaluator.d.ts +12 -0
  53. package/dist/yaml-evaluator.js +105 -0
  54. package/package.json +36 -0
  55. package/src/cve-database.json +396 -0
@@ -0,0 +1,117 @@
1
+ /** The outcome of evaluating an agent action against all policies. */
2
+ export type Verdict = 'PASS' | 'BLOCK' | 'CONFIRM';
3
+ /** Reason categories for BLOCK verdicts. Human-readable by design. */
4
+ export type BlockReason = 'DESTRUCTIVE_ACTION' | 'AUTO_SEND' | 'EXTERNAL_WRITE' | 'SCOPE_VIOLATION' | 'LOW_CONFIDENCE' | 'RATE_EXCEEDED' | 'UNKNOWN_ACTION' | 'CUSTOM';
5
+ /** Result returned by a single policy check. */
6
+ export interface PolicyResult {
7
+ verdict: Verdict;
8
+ blockReason: BlockReason | null;
9
+ /** Human-readable message, safe to show to end users. */
10
+ message: string;
11
+ /** Which policy produced this result. */
12
+ policyName: string;
13
+ requiresConfirmation: boolean;
14
+ }
15
+ /** A single YAML policy rule. */
16
+ export interface PolicyRule {
17
+ name: string;
18
+ verdict: Verdict;
19
+ message: string;
20
+ blockReason?: BlockReason;
21
+ trigger: {
22
+ actionType?: string | string[];
23
+ actionContains?: string | string[];
24
+ actionStartsWith?: string | string[];
25
+ contextKey?: string;
26
+ contextValueIn?: unknown[];
27
+ contextValueBelow?: number;
28
+ contextValueAbove?: number;
29
+ userApproved?: boolean;
30
+ };
31
+ }
32
+ /** A complete corpus.policy.yaml file parsed into memory. */
33
+ export interface PolicyFile {
34
+ agent: string;
35
+ version: string;
36
+ rules: PolicyRule[];
37
+ /** Path to a .jac file for complex rules evaluated after YAML rules. */
38
+ jacExtension?: string;
39
+ /** Allowed action prefixes for scope guard. */
40
+ allowedActions?: string[];
41
+ /** Rate limits per action type. */
42
+ rateLimits?: Record<string, {
43
+ count: number;
44
+ windowSeconds: number;
45
+ }>;
46
+ }
47
+ /**
48
+ * What the developer passes to corpus.protect().
49
+ * actionPayload is accepted but never stored or logged.
50
+ */
51
+ export interface CorpusInput {
52
+ /** What the agent wants to do. Maps to policy rule triggers. */
53
+ actionType: string;
54
+ /** The data the action will operate on. Never stored by Corpus. */
55
+ actionPayload?: Record<string, unknown>;
56
+ /**
57
+ * Arbitrary key-value context from the agent.
58
+ * Use this to pass intent, confidence, source, etc.
59
+ */
60
+ context?: Record<string, unknown>;
61
+ /** True if the user has already explicitly confirmed this action. */
62
+ userApproved?: boolean;
63
+ }
64
+ /** What gets stored in the database. Intentionally minimal. No user content. */
65
+ export interface ActionLogEntry {
66
+ id: string;
67
+ projectSlug: string;
68
+ actionType: string;
69
+ verdict: Verdict;
70
+ userDecision: 'CONFIRMED' | 'CANCELLED' | null;
71
+ policyTriggered: string;
72
+ blockReason: BlockReason | null;
73
+ durationMs: number;
74
+ timestamp: string;
75
+ }
76
+ export interface CorpusConfig {
77
+ projectSlug: string;
78
+ apiEndpoint: string;
79
+ apiKey: string;
80
+ /**
81
+ * observe: records verdicts but never blocks execution. Use during beta.
82
+ * enforce: fully enforces BLOCK and CONFIRM verdicts.
83
+ */
84
+ mode: 'observe' | 'enforce';
85
+ /** Path to corpus.policy.yaml or a directory of policy files. */
86
+ policyPath: string;
87
+ thresholds?: {
88
+ /** Actions with context.confidence below this are BLOCK. Default: 0.50 */
89
+ confidenceBlock?: number;
90
+ /** Actions with context.confidence below this are CONFIRM. Default: 0.72 */
91
+ confidenceConfirm?: number;
92
+ };
93
+ timeouts?: {
94
+ /** Max ms for Jac policy subprocess. Default: 200 */
95
+ policyEvalMs?: number;
96
+ /** Undo window in ms after action executes. Default: 5000 */
97
+ undoWindowMs?: number;
98
+ };
99
+ }
100
+ /**
101
+ * Callbacks the developer implements to connect Corpus to their UI.
102
+ * All hooks are optional in observe mode.
103
+ */
104
+ export interface CorpusHooks<TResult = unknown> {
105
+ onConfirmRequired?: (message: string, confirm: () => Promise<TResult | null>, cancel: () => void) => void;
106
+ onBlocked?: (message: string, reason: BlockReason | null) => void;
107
+ onUndoAvailable?: (message: string, undo: () => Promise<void>, msRemaining: number) => void;
108
+ onPolicyEvalComplete?: (result: PolicyResult, durationMs: number) => void;
109
+ }
110
+ export declare class CorpusBlockedError extends Error {
111
+ readonly policyName: string;
112
+ readonly blockReason: BlockReason | null;
113
+ constructor(policyName: string, blockReason: BlockReason | null, message: string);
114
+ }
115
+ export declare class CorpusConfigError extends Error {
116
+ constructor(message: string);
117
+ }
package/dist/types.js ADDED
@@ -0,0 +1,16 @@
1
+ export class CorpusBlockedError extends Error {
2
+ policyName;
3
+ blockReason;
4
+ constructor(policyName, blockReason, message) {
5
+ super(`[corpus] Policy '${policyName}' blocked this action: ${message}`);
6
+ this.name = 'CorpusBlockedError';
7
+ this.policyName = policyName;
8
+ this.blockReason = blockReason;
9
+ }
10
+ }
11
+ export class CorpusConfigError extends Error {
12
+ constructor(message) {
13
+ super(`[corpus] Config error: ${message}`);
14
+ this.name = 'CorpusConfigError';
15
+ }
16
+ }
@@ -0,0 +1,12 @@
1
+ import type { PolicyFile, PolicyResult, CorpusInput } from './types.js';
2
+ /**
3
+ * Evaluates a CorpusInput against a PolicyFile.
4
+ * Returns the first matching rule's verdict, or PASS if no rule matches.
5
+ * Rules are evaluated in order. First match wins.
6
+ */
7
+ export declare function evaluateYamlPolicies(input: CorpusInput, policyFile: PolicyFile): PolicyResult;
8
+ /**
9
+ * Loads and parses a corpus.policy.yaml file.
10
+ * Throws if the file is missing or malformed.
11
+ */
12
+ export declare function loadPolicyFile(policyPath: string): Promise<PolicyFile>;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Evaluates a CorpusInput against a PolicyFile.
3
+ * Returns the first matching rule's verdict, or PASS if no rule matches.
4
+ * Rules are evaluated in order. First match wins.
5
+ */
6
+ export function evaluateYamlPolicies(input, policyFile) {
7
+ for (const rule of policyFile.rules) {
8
+ if (ruleMatches(rule, input)) {
9
+ return {
10
+ verdict: rule.verdict,
11
+ blockReason: rule.blockReason ?? null,
12
+ message: rule.message,
13
+ policyName: rule.name,
14
+ requiresConfirmation: rule.verdict === 'CONFIRM',
15
+ };
16
+ }
17
+ }
18
+ return {
19
+ verdict: 'PASS',
20
+ blockReason: null,
21
+ message: '',
22
+ policyName: '__no_match__',
23
+ requiresConfirmation: false,
24
+ };
25
+ }
26
+ function ruleMatches(rule, input) {
27
+ if (!rule.trigger)
28
+ return false;
29
+ const t = rule.trigger;
30
+ if (!input.actionType)
31
+ return false;
32
+ const actionLower = input.actionType.toLowerCase();
33
+ // actionType: exact match (string or array)
34
+ if (t.actionType !== undefined) {
35
+ const types = Array.isArray(t.actionType) ? t.actionType : [t.actionType];
36
+ if (!types.some((a) => actionLower === a.toLowerCase()))
37
+ return false;
38
+ }
39
+ // actionContains: substring match
40
+ if (t.actionContains !== undefined) {
41
+ const needles = Array.isArray(t.actionContains) ? t.actionContains : [t.actionContains];
42
+ if (!needles.some((n) => actionLower.includes(n.toLowerCase())))
43
+ return false;
44
+ }
45
+ // actionStartsWith: prefix match
46
+ if (t.actionStartsWith !== undefined) {
47
+ const prefixes = Array.isArray(t.actionStartsWith) ? t.actionStartsWith : [t.actionStartsWith];
48
+ if (!prefixes.some((p) => actionLower.startsWith(p.toLowerCase())))
49
+ return false;
50
+ }
51
+ // contextKey + contextValue* checks
52
+ if (t.contextKey !== undefined) {
53
+ const ctxValue = input.context?.[t.contextKey];
54
+ if (ctxValue === undefined)
55
+ return false;
56
+ if (t.contextValueIn !== undefined) {
57
+ // Use loose comparison to handle YAML type coercion (number vs string)
58
+ if (!t.contextValueIn.some((v) => String(v) === String(ctxValue)))
59
+ return false;
60
+ }
61
+ if (t.contextValueBelow !== undefined) {
62
+ if (typeof ctxValue !== 'number' || ctxValue >= t.contextValueBelow)
63
+ return false;
64
+ }
65
+ if (t.contextValueAbove !== undefined) {
66
+ if (typeof ctxValue !== 'number' || ctxValue <= t.contextValueAbove)
67
+ return false;
68
+ }
69
+ }
70
+ // userApproved check
71
+ if (t.userApproved !== undefined) {
72
+ if ((input.userApproved ?? false) !== t.userApproved)
73
+ return false;
74
+ }
75
+ return true;
76
+ }
77
+ /**
78
+ * Loads and parses a corpus.policy.yaml file.
79
+ * Throws if the file is missing or malformed.
80
+ */
81
+ export async function loadPolicyFile(policyPath) {
82
+ const { readFile } = await import('fs/promises');
83
+ const { parse } = await import('yaml');
84
+ const raw = await readFile(policyPath, 'utf-8');
85
+ const parsed = parse(raw);
86
+ if (!parsed.agent || !Array.isArray(parsed.rules)) {
87
+ throw new Error(`Invalid policy file at ${policyPath}. Must have 'agent' and 'rules' fields.`);
88
+ }
89
+ if (!parsed.version) {
90
+ parsed.version = '1.0';
91
+ }
92
+ // Validate individual rules have required fields
93
+ for (let i = 0; i < parsed.rules.length; i++) {
94
+ const rule = parsed.rules[i];
95
+ if (!rule.name || !rule.verdict || !rule.trigger) {
96
+ throw new Error(`Invalid rule at index ${i} in ${policyPath}. Rules must have 'name', 'verdict', and 'trigger' fields.`);
97
+ }
98
+ // Normalize verdict to uppercase (PASS, BLOCK, CONFIRM)
99
+ rule.verdict = rule.verdict.toUpperCase();
100
+ if (!['PASS', 'BLOCK', 'CONFIRM'].includes(rule.verdict)) {
101
+ throw new Error(`Invalid verdict '${rule.verdict}' in rule '${rule.name}' at ${policyPath}. Must be PASS, BLOCK, or CONFIRM.`);
102
+ }
103
+ }
104
+ return parsed;
105
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "corpus-core",
3
+ "version": "0.1.0",
4
+ "description": "Core security scanning engine for Corpus — the immune system for AI-generated code",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src/cve-database.json"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "test": "vitest run"
21
+ },
22
+ "dependencies": {
23
+ "yaml": "^2.3.4"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^20.11.0",
27
+ "typescript": "^5.3.0",
28
+ "vitest": "^1.2.0"
29
+ },
30
+ "keywords": ["ai", "security", "trust-score", "scanner", "policy-engine", "secrets", "pii", "cve", "vibe-coding"],
31
+ "repository": { "type": "git", "url": "https://github.com/fluentflier/corpus" },
32
+ "license": "MIT",
33
+ "publishConfig": {
34
+ "access": "public"
35
+ }
36
+ }