@bookedsolid/rea 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.
- package/LICENSE +21 -0
- package/README.md +339 -0
- package/SECURITY.md +104 -0
- package/THREAT_MODEL.md +245 -0
- package/agents/accessibility-engineer.md +101 -0
- package/agents/backend-engineer.md +126 -0
- package/agents/code-reviewer.md +144 -0
- package/agents/codex-adversarial.md +107 -0
- package/agents/frontend-specialist.md +84 -0
- package/agents/qa-engineer.md +138 -0
- package/agents/rea-orchestrator.md +101 -0
- package/agents/security-engineer.md +108 -0
- package/agents/technical-writer.md +140 -0
- package/agents/typescript-specialist.md +111 -0
- package/commands/codex-review.md +104 -0
- package/commands/freeze.md +81 -0
- package/commands/halt-check.md +120 -0
- package/commands/rea.md +52 -0
- package/commands/review.md +79 -0
- package/dist/cli/check.d.ts +1 -0
- package/dist/cli/check.js +66 -0
- package/dist/cli/doctor.d.ts +1 -0
- package/dist/cli/doctor.js +93 -0
- package/dist/cli/freeze.d.ts +8 -0
- package/dist/cli/freeze.js +61 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +65 -0
- package/dist/cli/init.d.ts +6 -0
- package/dist/cli/init.js +237 -0
- package/dist/cli/serve.d.ts +1 -0
- package/dist/cli/serve.js +19 -0
- package/dist/cli/utils.d.ts +23 -0
- package/dist/cli/utils.js +51 -0
- package/dist/config/tier-map.d.ts +11 -0
- package/dist/config/tier-map.js +108 -0
- package/dist/config/types.d.ts +24 -0
- package/dist/config/types.js +1 -0
- package/dist/gateway/circuit-breaker.d.ts +43 -0
- package/dist/gateway/circuit-breaker.js +86 -0
- package/dist/gateway/middleware/audit-types.d.ts +16 -0
- package/dist/gateway/middleware/audit-types.js +1 -0
- package/dist/gateway/middleware/audit.d.ts +12 -0
- package/dist/gateway/middleware/audit.js +98 -0
- package/dist/gateway/middleware/blocked-paths.d.ts +12 -0
- package/dist/gateway/middleware/blocked-paths.js +117 -0
- package/dist/gateway/middleware/chain.d.ts +28 -0
- package/dist/gateway/middleware/chain.js +40 -0
- package/dist/gateway/middleware/circuit-breaker.d.ts +11 -0
- package/dist/gateway/middleware/circuit-breaker.js +43 -0
- package/dist/gateway/middleware/injection.d.ts +22 -0
- package/dist/gateway/middleware/injection.js +128 -0
- package/dist/gateway/middleware/kill-switch.d.ts +10 -0
- package/dist/gateway/middleware/kill-switch.js +58 -0
- package/dist/gateway/middleware/policy.d.ts +12 -0
- package/dist/gateway/middleware/policy.js +70 -0
- package/dist/gateway/middleware/rate-limit.d.ts +12 -0
- package/dist/gateway/middleware/rate-limit.js +31 -0
- package/dist/gateway/middleware/redact.d.ts +16 -0
- package/dist/gateway/middleware/redact.js +128 -0
- package/dist/gateway/middleware/result-size-cap.d.ts +13 -0
- package/dist/gateway/middleware/result-size-cap.js +48 -0
- package/dist/gateway/middleware/session.d.ts +10 -0
- package/dist/gateway/middleware/session.js +18 -0
- package/dist/gateway/middleware/tier.d.ts +6 -0
- package/dist/gateway/middleware/tier.js +10 -0
- package/dist/gateway/rate-limiter.d.ts +36 -0
- package/dist/gateway/rate-limiter.js +75 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/policy/loader.d.ts +80 -0
- package/dist/policy/loader.js +146 -0
- package/dist/policy/types.d.ts +34 -0
- package/dist/policy/types.js +19 -0
- package/hooks/_lib/common.sh +105 -0
- package/hooks/_lib/halt-check.sh +39 -0
- package/hooks/_lib/policy-read.sh +79 -0
- package/hooks/architecture-review-gate.sh +84 -0
- package/hooks/attribution-advisory.sh +126 -0
- package/hooks/blocked-paths-enforcer.sh +176 -0
- package/hooks/changeset-security-gate.sh +143 -0
- package/hooks/commit-review-gate.sh +166 -0
- package/hooks/dangerous-bash-interceptor.sh +362 -0
- package/hooks/dependency-audit-gate.sh +118 -0
- package/hooks/env-file-protection.sh +110 -0
- package/hooks/pr-issue-link-gate.sh +65 -0
- package/hooks/push-review-gate.sh +120 -0
- package/hooks/secret-scanner.sh +229 -0
- package/hooks/security-disclosure-gate.sh +146 -0
- package/hooks/settings-protection.sh +147 -0
- package/package.json +93 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const WINDOW_MS = 60_000;
|
|
2
|
+
/**
|
|
3
|
+
* In-memory per-server rate limiter and concurrency cap.
|
|
4
|
+
*
|
|
5
|
+
* Concurrency: tracks active in-flight calls. If at the limit, new calls
|
|
6
|
+
* are rejected immediately with a structured error.
|
|
7
|
+
*
|
|
8
|
+
* Rate: sliding window — calls in the last 60 seconds must not exceed the
|
|
9
|
+
* configured calls_per_minute. 0 means unlimited for either dimension.
|
|
10
|
+
*/
|
|
11
|
+
export class RateLimiter {
|
|
12
|
+
state = new Map();
|
|
13
|
+
constructor(gatewayConfig) {
|
|
14
|
+
if (!gatewayConfig)
|
|
15
|
+
return;
|
|
16
|
+
for (const [name, serverCfg] of Object.entries(gatewayConfig.servers)) {
|
|
17
|
+
this.state.set(name, {
|
|
18
|
+
activeCalls: 0,
|
|
19
|
+
callTimestamps: [],
|
|
20
|
+
maxConcurrent: serverCfg.max_concurrent_calls ?? 0,
|
|
21
|
+
callsPerMinute: serverCfg.calls_per_minute ?? 0,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Try to acquire a slot for a call to `serverName`.
|
|
27
|
+
* Returns null on success, or a LimitExceededError if rejected.
|
|
28
|
+
*/
|
|
29
|
+
tryAcquire(serverName) {
|
|
30
|
+
let s = this.state.get(serverName);
|
|
31
|
+
if (!s) {
|
|
32
|
+
s = {
|
|
33
|
+
activeCalls: 0,
|
|
34
|
+
callTimestamps: [],
|
|
35
|
+
maxConcurrent: 0,
|
|
36
|
+
callsPerMinute: 0,
|
|
37
|
+
};
|
|
38
|
+
this.state.set(serverName, s);
|
|
39
|
+
}
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
s.callTimestamps = s.callTimestamps.filter((t) => now - t < WINDOW_MS);
|
|
42
|
+
if (s.callsPerMinute > 0 && s.callTimestamps.length >= s.callsPerMinute) {
|
|
43
|
+
return {
|
|
44
|
+
type: 'rate',
|
|
45
|
+
serverName,
|
|
46
|
+
current: s.callTimestamps.length,
|
|
47
|
+
limit: s.callsPerMinute,
|
|
48
|
+
message: `Rate limit exceeded for server "${serverName}": ${s.callTimestamps.length}/${s.callsPerMinute} calls in the last 60s`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (s.maxConcurrent > 0 && s.activeCalls >= s.maxConcurrent) {
|
|
52
|
+
return {
|
|
53
|
+
type: 'concurrency',
|
|
54
|
+
serverName,
|
|
55
|
+
current: s.activeCalls,
|
|
56
|
+
limit: s.maxConcurrent,
|
|
57
|
+
message: `Concurrency limit exceeded for server "${serverName}": ${s.activeCalls}/${s.maxConcurrent} active calls`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
s.activeCalls++;
|
|
61
|
+
s.callTimestamps.push(now);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
/** Release a previously acquired concurrency slot. No-op for unknown servers. */
|
|
65
|
+
release(serverName) {
|
|
66
|
+
const s = this.state.get(serverName);
|
|
67
|
+
if (!s)
|
|
68
|
+
return;
|
|
69
|
+
if (s.activeCalls > 0)
|
|
70
|
+
s.activeCalls--;
|
|
71
|
+
}
|
|
72
|
+
getState(serverName) {
|
|
73
|
+
return this.state.get(serverName);
|
|
74
|
+
}
|
|
75
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { AutonomyLevel } from './types.js';
|
|
3
|
+
import type { Policy } from './types.js';
|
|
4
|
+
declare const PolicySchema: z.ZodObject<{
|
|
5
|
+
version: z.ZodString;
|
|
6
|
+
profile: z.ZodString;
|
|
7
|
+
installed_by: z.ZodString;
|
|
8
|
+
installed_at: z.ZodString;
|
|
9
|
+
autonomy_level: z.ZodNativeEnum<typeof AutonomyLevel>;
|
|
10
|
+
max_autonomy_level: z.ZodNativeEnum<typeof AutonomyLevel>;
|
|
11
|
+
promotion_requires_human_approval: z.ZodBoolean;
|
|
12
|
+
block_ai_attribution: z.ZodDefault<z.ZodBoolean>;
|
|
13
|
+
blocked_paths: z.ZodArray<z.ZodString, "many">;
|
|
14
|
+
notification_channel: z.ZodDefault<z.ZodString>;
|
|
15
|
+
injection_detection: z.ZodOptional<z.ZodEnum<["block", "warn"]>>;
|
|
16
|
+
context_protection: z.ZodOptional<z.ZodObject<{
|
|
17
|
+
delegate_to_subagent: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
18
|
+
max_bash_output_lines: z.ZodOptional<z.ZodNumber>;
|
|
19
|
+
}, "strip", z.ZodTypeAny, {
|
|
20
|
+
delegate_to_subagent: string[];
|
|
21
|
+
max_bash_output_lines?: number | undefined;
|
|
22
|
+
}, {
|
|
23
|
+
delegate_to_subagent?: string[] | undefined;
|
|
24
|
+
max_bash_output_lines?: number | undefined;
|
|
25
|
+
}>>;
|
|
26
|
+
}, "strict", z.ZodTypeAny, {
|
|
27
|
+
version: string;
|
|
28
|
+
profile: string;
|
|
29
|
+
installed_by: string;
|
|
30
|
+
installed_at: string;
|
|
31
|
+
autonomy_level: AutonomyLevel;
|
|
32
|
+
max_autonomy_level: AutonomyLevel;
|
|
33
|
+
promotion_requires_human_approval: boolean;
|
|
34
|
+
block_ai_attribution: boolean;
|
|
35
|
+
blocked_paths: string[];
|
|
36
|
+
notification_channel: string;
|
|
37
|
+
injection_detection?: "block" | "warn" | undefined;
|
|
38
|
+
context_protection?: {
|
|
39
|
+
delegate_to_subagent: string[];
|
|
40
|
+
max_bash_output_lines?: number | undefined;
|
|
41
|
+
} | undefined;
|
|
42
|
+
}, {
|
|
43
|
+
version: string;
|
|
44
|
+
profile: string;
|
|
45
|
+
installed_by: string;
|
|
46
|
+
installed_at: string;
|
|
47
|
+
autonomy_level: AutonomyLevel;
|
|
48
|
+
max_autonomy_level: AutonomyLevel;
|
|
49
|
+
promotion_requires_human_approval: boolean;
|
|
50
|
+
blocked_paths: string[];
|
|
51
|
+
block_ai_attribution?: boolean | undefined;
|
|
52
|
+
notification_channel?: string | undefined;
|
|
53
|
+
injection_detection?: "block" | "warn" | undefined;
|
|
54
|
+
context_protection?: {
|
|
55
|
+
delegate_to_subagent?: string[] | undefined;
|
|
56
|
+
max_bash_output_lines?: number | undefined;
|
|
57
|
+
} | undefined;
|
|
58
|
+
}>;
|
|
59
|
+
/**
|
|
60
|
+
* Async policy loader with TTL cache and mtime-based invalidation.
|
|
61
|
+
*
|
|
62
|
+
* TTL is configurable via REA_POLICY_CACHE_TTL_MS.
|
|
63
|
+
*
|
|
64
|
+
* SECURITY: mtime invalidation ensures a tightened policy takes effect on the next call.
|
|
65
|
+
* CONCURRENCY: inflightReads map guarantees at most one disk read per baseDir at a time.
|
|
66
|
+
*/
|
|
67
|
+
export declare function loadPolicyAsync(baseDir: string): Promise<Policy>;
|
|
68
|
+
/**
|
|
69
|
+
* Synchronous policy loader — for CLI startup paths that must be sync.
|
|
70
|
+
* Does NOT use the cache — always reads from disk.
|
|
71
|
+
*
|
|
72
|
+
* Prefer loadPolicyAsync for middleware and any async context.
|
|
73
|
+
*/
|
|
74
|
+
export declare function loadPolicy(baseDir: string): Policy;
|
|
75
|
+
/**
|
|
76
|
+
* Invalidate the cache for a given baseDir.
|
|
77
|
+
* Exposed for testing — production code relies on TTL and mtime invalidation.
|
|
78
|
+
*/
|
|
79
|
+
export declare function invalidatePolicyCache(baseDir?: string): void;
|
|
80
|
+
export { PolicySchema };
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import fsPromises from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { parse as parseYaml } from 'yaml';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { AutonomyLevel } from './types.js';
|
|
7
|
+
const LEVEL_ORDER = {
|
|
8
|
+
[AutonomyLevel.L0]: 0,
|
|
9
|
+
[AutonomyLevel.L1]: 1,
|
|
10
|
+
[AutonomyLevel.L2]: 2,
|
|
11
|
+
[AutonomyLevel.L3]: 3,
|
|
12
|
+
};
|
|
13
|
+
const ContextProtectionSchema = z.object({
|
|
14
|
+
delegate_to_subagent: z.array(z.string()).default([]),
|
|
15
|
+
max_bash_output_lines: z.number().int().positive().optional(),
|
|
16
|
+
});
|
|
17
|
+
const PolicySchema = z
|
|
18
|
+
.object({
|
|
19
|
+
version: z.string(),
|
|
20
|
+
profile: z.string(),
|
|
21
|
+
installed_by: z.string(),
|
|
22
|
+
installed_at: z.string(),
|
|
23
|
+
autonomy_level: z.nativeEnum(AutonomyLevel),
|
|
24
|
+
max_autonomy_level: z.nativeEnum(AutonomyLevel),
|
|
25
|
+
promotion_requires_human_approval: z.boolean(),
|
|
26
|
+
block_ai_attribution: z.boolean().default(false),
|
|
27
|
+
blocked_paths: z.array(z.string()),
|
|
28
|
+
notification_channel: z.string().default(''),
|
|
29
|
+
injection_detection: z.enum(['block', 'warn']).optional(),
|
|
30
|
+
context_protection: ContextProtectionSchema.optional(),
|
|
31
|
+
})
|
|
32
|
+
.strict();
|
|
33
|
+
const DEFAULT_CACHE_TTL_MS = 30_000;
|
|
34
|
+
const POLICY_DIR = '.rea';
|
|
35
|
+
const POLICY_FILE = 'policy.yaml';
|
|
36
|
+
/**
|
|
37
|
+
* SECURITY: Cache never serves a more permissive policy than disk.
|
|
38
|
+
* mtime invalidation ensures policy tightening takes effect before TTL expires.
|
|
39
|
+
*/
|
|
40
|
+
const policyCache = new Map();
|
|
41
|
+
const inflightReads = new Map();
|
|
42
|
+
/**
|
|
43
|
+
* Convert `{ key: undefined }` to omitted keys so Policy satisfies
|
|
44
|
+
* exactOptionalPropertyTypes. Zod defaults produce explicit undefined.
|
|
45
|
+
*/
|
|
46
|
+
function stripUndefined(input) {
|
|
47
|
+
const result = {};
|
|
48
|
+
for (const [k, v] of Object.entries(input)) {
|
|
49
|
+
if (v !== undefined)
|
|
50
|
+
result[k] = v;
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
function applyMaxCeiling(policy) {
|
|
55
|
+
if (LEVEL_ORDER[policy.autonomy_level] > LEVEL_ORDER[policy.max_autonomy_level]) {
|
|
56
|
+
console.error(`[rea] WARNING: autonomy_level ${policy.autonomy_level} exceeds max_autonomy_level ${policy.max_autonomy_level} — clamping to ${policy.max_autonomy_level}`);
|
|
57
|
+
return { ...policy, autonomy_level: policy.max_autonomy_level };
|
|
58
|
+
}
|
|
59
|
+
return policy;
|
|
60
|
+
}
|
|
61
|
+
function parseRawPolicy(raw, policyPath) {
|
|
62
|
+
let parsed;
|
|
63
|
+
try {
|
|
64
|
+
parsed = parseYaml(raw);
|
|
65
|
+
}
|
|
66
|
+
catch (yamlErr) {
|
|
67
|
+
throw new Error(`Failed to parse policy YAML at ${policyPath}: ${yamlErr instanceof Error ? yamlErr.message : yamlErr}`);
|
|
68
|
+
}
|
|
69
|
+
let parsedPolicy;
|
|
70
|
+
try {
|
|
71
|
+
parsedPolicy = PolicySchema.parse(parsed);
|
|
72
|
+
}
|
|
73
|
+
catch (zodErr) {
|
|
74
|
+
throw new Error(`Invalid policy schema at ${policyPath}: ${zodErr instanceof Error ? zodErr.message : zodErr}`);
|
|
75
|
+
}
|
|
76
|
+
return applyMaxCeiling(stripUndefined(parsedPolicy));
|
|
77
|
+
}
|
|
78
|
+
function policyPathFor(baseDir) {
|
|
79
|
+
return path.join(baseDir, POLICY_DIR, POLICY_FILE);
|
|
80
|
+
}
|
|
81
|
+
async function readPolicyFromDisk(baseDir, policyPath, currentMtime) {
|
|
82
|
+
const raw = await fsPromises.readFile(policyPath, 'utf8');
|
|
83
|
+
const policy = parseRawPolicy(raw, policyPath);
|
|
84
|
+
policyCache.set(baseDir, { policy, cachedAt: Date.now(), mtimeMs: currentMtime });
|
|
85
|
+
return policy;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Async policy loader with TTL cache and mtime-based invalidation.
|
|
89
|
+
*
|
|
90
|
+
* TTL is configurable via REA_POLICY_CACHE_TTL_MS.
|
|
91
|
+
*
|
|
92
|
+
* SECURITY: mtime invalidation ensures a tightened policy takes effect on the next call.
|
|
93
|
+
* CONCURRENCY: inflightReads map guarantees at most one disk read per baseDir at a time.
|
|
94
|
+
*/
|
|
95
|
+
export async function loadPolicyAsync(baseDir) {
|
|
96
|
+
const policyPath = policyPathFor(baseDir);
|
|
97
|
+
const ttlMs = Number(process.env.REA_POLICY_CACHE_TTL_MS ?? DEFAULT_CACHE_TTL_MS);
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
let currentMtime;
|
|
100
|
+
try {
|
|
101
|
+
const stat = await fsPromises.stat(policyPath);
|
|
102
|
+
currentMtime = stat.mtimeMs;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
throw new Error(`Policy file not found: ${policyPath}`);
|
|
106
|
+
}
|
|
107
|
+
const cached = policyCache.get(baseDir);
|
|
108
|
+
if (cached !== undefined && cached.mtimeMs === currentMtime && now - cached.cachedAt < ttlMs) {
|
|
109
|
+
return cached.policy;
|
|
110
|
+
}
|
|
111
|
+
const inflight = inflightReads.get(baseDir);
|
|
112
|
+
if (inflight)
|
|
113
|
+
return inflight;
|
|
114
|
+
const read = readPolicyFromDisk(baseDir, policyPath, currentMtime).finally(() => {
|
|
115
|
+
inflightReads.delete(baseDir);
|
|
116
|
+
});
|
|
117
|
+
inflightReads.set(baseDir, read);
|
|
118
|
+
return read;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Synchronous policy loader — for CLI startup paths that must be sync.
|
|
122
|
+
* Does NOT use the cache — always reads from disk.
|
|
123
|
+
*
|
|
124
|
+
* Prefer loadPolicyAsync for middleware and any async context.
|
|
125
|
+
*/
|
|
126
|
+
export function loadPolicy(baseDir) {
|
|
127
|
+
const policyPath = policyPathFor(baseDir);
|
|
128
|
+
if (!fs.existsSync(policyPath)) {
|
|
129
|
+
throw new Error(`Policy file not found: ${policyPath}`);
|
|
130
|
+
}
|
|
131
|
+
const raw = fs.readFileSync(policyPath, 'utf8');
|
|
132
|
+
return parseRawPolicy(raw, policyPath);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Invalidate the cache for a given baseDir.
|
|
136
|
+
* Exposed for testing — production code relies on TTL and mtime invalidation.
|
|
137
|
+
*/
|
|
138
|
+
export function invalidatePolicyCache(baseDir) {
|
|
139
|
+
if (baseDir === undefined) {
|
|
140
|
+
policyCache.clear();
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
policyCache.delete(baseDir);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
export { PolicySchema };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export declare enum Tier {
|
|
2
|
+
Read = "read",
|
|
3
|
+
Write = "write",
|
|
4
|
+
Destructive = "destructive"
|
|
5
|
+
}
|
|
6
|
+
export declare enum AutonomyLevel {
|
|
7
|
+
L0 = "L0",
|
|
8
|
+
L1 = "L1",
|
|
9
|
+
L2 = "L2",
|
|
10
|
+
L3 = "L3"
|
|
11
|
+
}
|
|
12
|
+
export declare enum InvocationStatus {
|
|
13
|
+
Allowed = "allowed",
|
|
14
|
+
Denied = "denied",
|
|
15
|
+
Error = "error"
|
|
16
|
+
}
|
|
17
|
+
export interface ContextProtection {
|
|
18
|
+
delegate_to_subagent: string[];
|
|
19
|
+
max_bash_output_lines?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface Policy {
|
|
22
|
+
version: string;
|
|
23
|
+
profile: string;
|
|
24
|
+
installed_by: string;
|
|
25
|
+
installed_at: string;
|
|
26
|
+
autonomy_level: AutonomyLevel;
|
|
27
|
+
max_autonomy_level: AutonomyLevel;
|
|
28
|
+
promotion_requires_human_approval: boolean;
|
|
29
|
+
block_ai_attribution: boolean;
|
|
30
|
+
blocked_paths: string[];
|
|
31
|
+
notification_channel: string;
|
|
32
|
+
injection_detection?: 'block' | 'warn';
|
|
33
|
+
context_protection?: ContextProtection;
|
|
34
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export var Tier;
|
|
2
|
+
(function (Tier) {
|
|
3
|
+
Tier["Read"] = "read";
|
|
4
|
+
Tier["Write"] = "write";
|
|
5
|
+
Tier["Destructive"] = "destructive";
|
|
6
|
+
})(Tier || (Tier = {}));
|
|
7
|
+
export var AutonomyLevel;
|
|
8
|
+
(function (AutonomyLevel) {
|
|
9
|
+
AutonomyLevel["L0"] = "L0";
|
|
10
|
+
AutonomyLevel["L1"] = "L1";
|
|
11
|
+
AutonomyLevel["L2"] = "L2";
|
|
12
|
+
AutonomyLevel["L3"] = "L3";
|
|
13
|
+
})(AutonomyLevel || (AutonomyLevel = {}));
|
|
14
|
+
export var InvocationStatus;
|
|
15
|
+
(function (InvocationStatus) {
|
|
16
|
+
InvocationStatus["Allowed"] = "allowed";
|
|
17
|
+
InvocationStatus["Denied"] = "denied";
|
|
18
|
+
InvocationStatus["Error"] = "error";
|
|
19
|
+
})(InvocationStatus || (InvocationStatus = {}));
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# hooks/_lib/common.sh — shared utilities for rea hooks
|
|
3
|
+
# Source via: source "$(dirname "$0")/_lib/common.sh"
|
|
4
|
+
|
|
5
|
+
# Find the .rea/ directory by walking up from CLAUDE_PROJECT_DIR or cwd
|
|
6
|
+
rea_root() {
|
|
7
|
+
local dir="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
8
|
+
while [[ "$dir" != "/" ]]; do
|
|
9
|
+
if [[ -d "$dir/.rea" ]]; then
|
|
10
|
+
printf '%s' "$dir"
|
|
11
|
+
return 0
|
|
12
|
+
fi
|
|
13
|
+
dir=$(dirname "$dir")
|
|
14
|
+
done
|
|
15
|
+
# Fallback to CLAUDE_PROJECT_DIR or cwd
|
|
16
|
+
printf '%s' "${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Exit with code 2 if .rea/HALT exists
|
|
20
|
+
check_halt() {
|
|
21
|
+
local root
|
|
22
|
+
root=$(rea_root)
|
|
23
|
+
local halt_file="${root}/.rea/HALT"
|
|
24
|
+
if [ -f "$halt_file" ]; then
|
|
25
|
+
printf 'REA HALT: %s\nAll agent operations suspended. Run: rea unfreeze\n' \
|
|
26
|
+
"$(head -c 1024 "$halt_file" 2>/dev/null || echo 'Reason unknown')" >&2
|
|
27
|
+
exit 2
|
|
28
|
+
fi
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Verify jq is available, exit 2 if not
|
|
32
|
+
require_jq() {
|
|
33
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
34
|
+
printf 'REA ERROR: jq is required but not installed.\n' >&2
|
|
35
|
+
printf 'Install: brew install jq OR apt-get install -y jq\n' >&2
|
|
36
|
+
exit 2
|
|
37
|
+
fi
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Build a structured JSON response for hook output
|
|
41
|
+
# Usage: json_output "status" "message" ["decision"]
|
|
42
|
+
# status: "block" | "allow" | "advisory"
|
|
43
|
+
# message: human-readable description
|
|
44
|
+
# decision: optional additionalContext for the agent
|
|
45
|
+
json_output() {
|
|
46
|
+
local status="$1"
|
|
47
|
+
local message="$2"
|
|
48
|
+
local decision="${3:-}"
|
|
49
|
+
|
|
50
|
+
if [[ "$status" == "block" ]]; then
|
|
51
|
+
printf '%s\n' "$message" >&2
|
|
52
|
+
if [[ -n "$decision" ]]; then
|
|
53
|
+
printf '%s\n' "$decision" >&2
|
|
54
|
+
fi
|
|
55
|
+
exit 2
|
|
56
|
+
elif [[ "$status" == "advisory" ]]; then
|
|
57
|
+
printf '%s\n' "$message" >&2
|
|
58
|
+
exit 0
|
|
59
|
+
else
|
|
60
|
+
exit 0
|
|
61
|
+
fi
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Exit 0 (skip) if the project's tech_profile does not match the expected type.
|
|
65
|
+
# Usage: check_project_type "lit-wc"
|
|
66
|
+
# Reads tech_profile from .rea/policy.yaml; if absent or mismatched, exits 0.
|
|
67
|
+
check_project_type() {
|
|
68
|
+
local expected_type="$1"
|
|
69
|
+
local root
|
|
70
|
+
root=$(rea_root)
|
|
71
|
+
local policy="${root}/.rea/policy.yaml"
|
|
72
|
+
if [[ ! -f "$policy" ]]; then
|
|
73
|
+
exit 0
|
|
74
|
+
fi
|
|
75
|
+
local actual_type
|
|
76
|
+
actual_type=$(grep -E '^tech_profile:' "$policy" 2>/dev/null | sed 's/^tech_profile:[[:space:]]*//' | tr -d '"' || echo "")
|
|
77
|
+
if [[ -z "$actual_type" || "$actual_type" != "$expected_type" ]]; then
|
|
78
|
+
exit 0
|
|
79
|
+
fi
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Score a diff for triage purposes
|
|
83
|
+
# Reads from stdin (expects unified diff output)
|
|
84
|
+
# Returns: "trivial" (<20 lines), "standard" (20-200), "significant" (>200)
|
|
85
|
+
# Also checks for sensitive paths — upgrades to "significant" if found
|
|
86
|
+
triage_score() {
|
|
87
|
+
local diff_input
|
|
88
|
+
diff_input=$(cat)
|
|
89
|
+
local line_count
|
|
90
|
+
line_count=$(printf '%s' "$diff_input" | grep -cE '^\+[^+]|^-[^-]' 2>/dev/null || echo "0")
|
|
91
|
+
|
|
92
|
+
# Check for sensitive paths
|
|
93
|
+
local sensitive=0
|
|
94
|
+
if printf '%s' "$diff_input" | grep -qE '^\+\+\+ .*(\.rea/|\.claude/|\.env|auth|security|\.github/workflows)'; then
|
|
95
|
+
sensitive=1
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
if [[ $sensitive -eq 1 ]] || [[ $line_count -gt 200 ]]; then
|
|
99
|
+
printf 'significant'
|
|
100
|
+
elif [[ $line_count -ge 20 ]]; then
|
|
101
|
+
printf 'standard'
|
|
102
|
+
else
|
|
103
|
+
printf 'trivial'
|
|
104
|
+
fi
|
|
105
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# hooks/_lib/halt-check.sh — HALT gate helper for rea hooks
|
|
3
|
+
# Source via: source "$(dirname "$0")/_lib/halt-check.sh"
|
|
4
|
+
#
|
|
5
|
+
# Every hook that can block a tool call must gate on .rea/HALT. When the file
|
|
6
|
+
# exists, all agent operations are suspended — the hook must deny the tool
|
|
7
|
+
# call with a clear error that surfaces the file's contents so the operator
|
|
8
|
+
# knows why the system was frozen.
|
|
9
|
+
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
# Find the .rea/ directory by walking up from CLAUDE_PROJECT_DIR or cwd.
|
|
13
|
+
# Falls back to CLAUDE_PROJECT_DIR or the current working directory when no
|
|
14
|
+
# .rea/ ancestor is found (which is fine — check_halt will simply no-op).
|
|
15
|
+
rea_root() {
|
|
16
|
+
local dir="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
17
|
+
while [[ "$dir" != "/" ]]; do
|
|
18
|
+
if [[ -d "$dir/.rea" ]]; then
|
|
19
|
+
printf '%s' "$dir"
|
|
20
|
+
return 0
|
|
21
|
+
fi
|
|
22
|
+
dir=$(dirname "$dir")
|
|
23
|
+
done
|
|
24
|
+
printf '%s' "${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Exit with code 2 (deny) if .rea/HALT exists.
|
|
28
|
+
# Prints the first 1024 bytes of HALT to stderr so the operator sees the
|
|
29
|
+
# reason the system was frozen. Safe to call from any hook.
|
|
30
|
+
check_halt() {
|
|
31
|
+
local root
|
|
32
|
+
root=$(rea_root)
|
|
33
|
+
local halt_file="${root}/.rea/HALT"
|
|
34
|
+
if [ -f "$halt_file" ]; then
|
|
35
|
+
printf 'REA HALT: %s\nAll agent operations suspended. Run: rea unfreeze\n' \
|
|
36
|
+
"$(head -c 1024 "$halt_file" 2>/dev/null || echo 'Reason unknown')" >&2
|
|
37
|
+
exit 2
|
|
38
|
+
fi
|
|
39
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# hooks/_lib/policy-read.sh — policy.yaml read helpers for rea hooks
|
|
3
|
+
# Source via: source "$(dirname "$0")/_lib/policy-read.sh"
|
|
4
|
+
#
|
|
5
|
+
# Minimal shell-only parsers for .rea/policy.yaml. Keeps hooks dependency-free
|
|
6
|
+
# (no yq required). Functions assume the caller has already resolved the
|
|
7
|
+
# project root via rea_root() from halt-check.sh, but will re-derive it if
|
|
8
|
+
# REA_ROOT is unset.
|
|
9
|
+
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
# Resolve the path to .rea/policy.yaml for the current project.
|
|
13
|
+
# Prints an empty string if no policy file is found — callers should treat
|
|
14
|
+
# a missing policy as "default / advisory" rather than an error.
|
|
15
|
+
policy_path() {
|
|
16
|
+
local root="${REA_ROOT:-}"
|
|
17
|
+
if [[ -z "$root" ]]; then
|
|
18
|
+
if command -v rea_root >/dev/null 2>&1; then
|
|
19
|
+
root=$(rea_root)
|
|
20
|
+
else
|
|
21
|
+
root="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
22
|
+
fi
|
|
23
|
+
fi
|
|
24
|
+
local policy="${root}/.rea/policy.yaml"
|
|
25
|
+
if [[ -f "$policy" ]]; then
|
|
26
|
+
printf '%s' "$policy"
|
|
27
|
+
fi
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Read a top-level scalar field from policy.yaml.
|
|
31
|
+
# Usage: policy_scalar "autonomy_level"
|
|
32
|
+
# Strips surrounding quotes. Prints empty string when unset or no policy.
|
|
33
|
+
policy_scalar() {
|
|
34
|
+
local key="$1"
|
|
35
|
+
local policy
|
|
36
|
+
policy=$(policy_path)
|
|
37
|
+
[[ -z "$policy" ]] && return 0
|
|
38
|
+
grep -E "^${key}:" "$policy" 2>/dev/null \
|
|
39
|
+
| head -n1 \
|
|
40
|
+
| sed -E "s/^${key}:[[:space:]]*//; s/^[\"']//; s/[\"']$//"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Test whether a boolean-valued top-level field is true.
|
|
44
|
+
# Usage: policy_bool_true "block_ai_attribution" && ...
|
|
45
|
+
# Returns 0 when the value is literal "true", 1 otherwise (including missing).
|
|
46
|
+
policy_bool_true() {
|
|
47
|
+
local key="$1"
|
|
48
|
+
local value
|
|
49
|
+
value=$(policy_scalar "$key")
|
|
50
|
+
[[ "$value" == "true" ]]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Read a list of scalars from a top-level sequence block.
|
|
54
|
+
# Usage: mapfile -t patterns < <(policy_list "delegate_to_subagent")
|
|
55
|
+
# Handles inline "[]" as empty. Stops at the first non-"-" continuation line.
|
|
56
|
+
policy_list() {
|
|
57
|
+
local key="$1"
|
|
58
|
+
local policy
|
|
59
|
+
policy=$(policy_path)
|
|
60
|
+
[[ -z "$policy" ]] && return 0
|
|
61
|
+
local in_block=0
|
|
62
|
+
while IFS= read -r line; do
|
|
63
|
+
if printf '%s' "$line" | grep -qE "^[[:space:]]*${key}:"; then
|
|
64
|
+
if printf '%s' "$line" | grep -qE "${key}:[[:space:]]*\[\]"; then
|
|
65
|
+
return 0
|
|
66
|
+
fi
|
|
67
|
+
in_block=1
|
|
68
|
+
continue
|
|
69
|
+
fi
|
|
70
|
+
if [[ $in_block -eq 1 ]]; then
|
|
71
|
+
if printf '%s' "$line" | grep -qE '^[[:space:]]*-[[:space:]]'; then
|
|
72
|
+
printf '%s' "$line" | sed -E "s/^[[:space:]]*-[[:space:]]*//; s/^[\"']//; s/[\"']$//"
|
|
73
|
+
printf '\n'
|
|
74
|
+
else
|
|
75
|
+
return 0
|
|
76
|
+
fi
|
|
77
|
+
fi
|
|
78
|
+
done < "$policy"
|
|
79
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PostToolUse hook: architecture-review-gate.sh
|
|
3
|
+
# Fires AFTER every Write or Edit tool call.
|
|
4
|
+
# Lightweight advisory: flags when writing to architecture-sensitive paths.
|
|
5
|
+
# Does NOT block — only returns advisory context.
|
|
6
|
+
#
|
|
7
|
+
# Exit codes:
|
|
8
|
+
# 0 = always (advisory only, never blocks)
|
|
9
|
+
|
|
10
|
+
set -uo pipefail
|
|
11
|
+
|
|
12
|
+
# ── 1. Read ALL stdin immediately ─────────────────────────────────────────────
|
|
13
|
+
INPUT=$(cat)
|
|
14
|
+
|
|
15
|
+
# ── 2. Dependency check ──────────────────────────────────────────────────────
|
|
16
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# ── 3. HALT check ────────────────────────────────────────────────────────────
|
|
21
|
+
REA_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
22
|
+
HALT_FILE="${REA_ROOT}/.rea/HALT"
|
|
23
|
+
if [ -f "$HALT_FILE" ]; then
|
|
24
|
+
printf 'REA HALT: %s\nAll agent operations suspended. Run: rea unfreeze\n' \
|
|
25
|
+
"$(head -c 1024 "$HALT_FILE" 2>/dev/null || echo 'Reason unknown')" >&2
|
|
26
|
+
exit 2
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# ── 4. Check if enabled ──────────────────────────────────────────────────────
|
|
30
|
+
POLICY_FILE="${REA_ROOT}/.rea/policy.yaml"
|
|
31
|
+
if [[ -f "$POLICY_FILE" ]]; then
|
|
32
|
+
if grep -qE 'architecture_advisory:[[:space:]]*false' "$POLICY_FILE" 2>/dev/null; then
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# ── 5. Extract file path ─────────────────────────────────────────────────────
|
|
38
|
+
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
39
|
+
|
|
40
|
+
if [[ -z "$FILE_PATH" ]]; then
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Normalize to relative path
|
|
45
|
+
if [[ "$FILE_PATH" == "$REA_ROOT"/* ]]; then
|
|
46
|
+
FILE_PATH="${FILE_PATH#$REA_ROOT/}"
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# ── 6. Check architecture-sensitive paths ─────────────────────────────────────
|
|
50
|
+
ARCH_PATTERNS=(
|
|
51
|
+
'src/types/'
|
|
52
|
+
'src/gateway/'
|
|
53
|
+
'src/config/'
|
|
54
|
+
'src/cli/commands/init/'
|
|
55
|
+
'hooks/_lib/'
|
|
56
|
+
'templates/'
|
|
57
|
+
'profiles/'
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
MATCHED=""
|
|
61
|
+
for pattern in "${ARCH_PATTERNS[@]}"; do
|
|
62
|
+
if [[ "$FILE_PATH" == "$pattern"* ]]; then
|
|
63
|
+
MATCHED="$pattern"
|
|
64
|
+
break
|
|
65
|
+
fi
|
|
66
|
+
done
|
|
67
|
+
|
|
68
|
+
if [[ -z "$MATCHED" ]]; then
|
|
69
|
+
exit 0
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# ── 7. Advisory output ───────────────────────────────────────────────────────
|
|
73
|
+
{
|
|
74
|
+
printf 'ARCHITECTURE ADVISORY: Sensitive path modified\n'
|
|
75
|
+
printf '\n'
|
|
76
|
+
printf ' File: %s\n' "$FILE_PATH"
|
|
77
|
+
printf ' Category: %s\n' "$MATCHED"
|
|
78
|
+
printf '\n'
|
|
79
|
+
printf ' This file is in an architecture-sensitive directory.\n'
|
|
80
|
+
printf ' Consider: Does this change maintain backward compatibility?\n'
|
|
81
|
+
printf ' Consider: Should this be reviewed by the principal-engineer agent?\n'
|
|
82
|
+
} >&2
|
|
83
|
+
|
|
84
|
+
exit 0
|