@bookedsolid/rea 0.1.0 → 0.2.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/.husky/commit-msg +130 -0
- package/.husky/pre-push +128 -0
- package/README.md +5 -5
- package/agents/codex-adversarial.md +23 -8
- package/commands/codex-review.md +2 -2
- package/dist/audit/append.d.ts +62 -0
- package/dist/audit/append.js +189 -0
- package/dist/audit/codex-event.d.ts +28 -0
- package/dist/audit/codex-event.js +15 -0
- package/dist/cli/doctor.d.ts +60 -1
- package/dist/cli/doctor.js +459 -20
- package/dist/cli/index.js +35 -5
- package/dist/cli/init.d.ts +13 -0
- package/dist/cli/init.js +278 -67
- package/dist/cli/install/canonical.d.ts +43 -0
- package/dist/cli/install/canonical.js +101 -0
- package/dist/cli/install/claude-md.d.ts +48 -0
- package/dist/cli/install/claude-md.js +93 -0
- package/dist/cli/install/commit-msg.d.ts +30 -0
- package/dist/cli/install/commit-msg.js +102 -0
- package/dist/cli/install/copy.d.ts +169 -0
- package/dist/cli/install/copy.js +455 -0
- package/dist/cli/install/fs-safe.d.ts +91 -0
- package/dist/cli/install/fs-safe.js +347 -0
- package/dist/cli/install/manifest-io.d.ts +12 -0
- package/dist/cli/install/manifest-io.js +44 -0
- package/dist/cli/install/manifest-schema.d.ts +83 -0
- package/dist/cli/install/manifest-schema.js +80 -0
- package/dist/cli/install/reagent.d.ts +59 -0
- package/dist/cli/install/reagent.js +160 -0
- package/dist/cli/install/settings-merge.d.ts +91 -0
- package/dist/cli/install/settings-merge.js +239 -0
- package/dist/cli/install/sha.d.ts +9 -0
- package/dist/cli/install/sha.js +21 -0
- package/dist/cli/serve.d.ts +11 -0
- package/dist/cli/serve.js +72 -6
- package/dist/cli/upgrade.d.ts +67 -0
- package/dist/cli/upgrade.js +509 -0
- package/dist/gateway/downstream-pool.d.ts +39 -0
- package/dist/gateway/downstream-pool.js +93 -0
- package/dist/gateway/downstream.d.ts +80 -0
- package/dist/gateway/downstream.js +196 -0
- package/dist/gateway/middleware/audit-types.d.ts +10 -0
- package/dist/gateway/middleware/audit.js +14 -0
- package/dist/gateway/middleware/injection.d.ts +59 -2
- package/dist/gateway/middleware/injection.js +91 -14
- package/dist/gateway/middleware/kill-switch.d.ts +20 -5
- package/dist/gateway/middleware/kill-switch.js +57 -35
- package/dist/gateway/middleware/redact.d.ts +83 -6
- package/dist/gateway/middleware/redact.js +133 -46
- package/dist/gateway/observability/codex-probe.d.ts +110 -0
- package/dist/gateway/observability/codex-probe.js +234 -0
- package/dist/gateway/observability/codex-telemetry.d.ts +93 -0
- package/dist/gateway/observability/codex-telemetry.js +221 -0
- package/dist/gateway/redact-safe/match-timeout.d.ts +83 -0
- package/dist/gateway/redact-safe/match-timeout.js +179 -0
- package/dist/gateway/reviewers/claude-self.d.ts +99 -0
- package/dist/gateway/reviewers/claude-self.js +316 -0
- package/dist/gateway/reviewers/codex.d.ts +64 -0
- package/dist/gateway/reviewers/codex.js +80 -0
- package/dist/gateway/reviewers/select.d.ts +64 -0
- package/dist/gateway/reviewers/select.js +102 -0
- package/dist/gateway/reviewers/types.d.ts +85 -0
- package/dist/gateway/reviewers/types.js +14 -0
- package/dist/gateway/server.d.ts +51 -0
- package/dist/gateway/server.js +258 -0
- package/dist/gateway/session.d.ts +9 -0
- package/dist/gateway/session.js +17 -0
- package/dist/policy/loader.d.ts +59 -0
- package/dist/policy/loader.js +65 -0
- package/dist/policy/profiles.d.ts +80 -0
- package/dist/policy/profiles.js +94 -0
- package/dist/policy/types.d.ts +38 -0
- package/dist/registry/loader.d.ts +98 -0
- package/dist/registry/loader.js +153 -0
- package/dist/registry/types.d.ts +44 -0
- package/dist/registry/types.js +6 -0
- package/dist/scripts/read-policy-field.d.ts +36 -0
- package/dist/scripts/read-policy-field.js +96 -0
- package/hooks/push-review-gate.sh +627 -17
- package/package.json +13 -2
- package/profiles/bst-internal-no-codex.yaml +40 -0
- package/profiles/bst-internal.yaml +23 -0
- package/profiles/client-engagement.yaml +23 -0
- package/profiles/lit-wc.yaml +17 -0
- package/profiles/minimal.yaml +11 -0
- package/profiles/open-source-no-codex.yaml +33 -0
- package/profiles/open-source.yaml +18 -0
- package/scripts/lint-safe-regex.mjs +78 -0
- package/scripts/postinstall.mjs +131 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-process session identifier. One `rea serve` invocation = one session_id.
|
|
3
|
+
* Matches how Claude Code's long-running sessions work today. Revisit when we
|
|
4
|
+
* add a streamable-HTTP transport that might serve multiple reconnecting
|
|
5
|
+
* clients.
|
|
6
|
+
*/
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
let sessionId = null;
|
|
9
|
+
export function currentSessionId() {
|
|
10
|
+
if (sessionId === null)
|
|
11
|
+
sessionId = crypto.randomUUID();
|
|
12
|
+
return sessionId;
|
|
13
|
+
}
|
|
14
|
+
/** Exposed for tests only — resets the module-level id. */
|
|
15
|
+
export function __resetSessionForTests() {
|
|
16
|
+
sessionId = null;
|
|
17
|
+
}
|
package/dist/policy/loader.d.ts
CHANGED
|
@@ -23,6 +23,43 @@ declare const PolicySchema: z.ZodObject<{
|
|
|
23
23
|
delegate_to_subagent?: string[] | undefined;
|
|
24
24
|
max_bash_output_lines?: number | undefined;
|
|
25
25
|
}>>;
|
|
26
|
+
review: z.ZodOptional<z.ZodObject<{
|
|
27
|
+
codex_required: z.ZodOptional<z.ZodBoolean>;
|
|
28
|
+
}, "strict", z.ZodTypeAny, {
|
|
29
|
+
codex_required?: boolean | undefined;
|
|
30
|
+
}, {
|
|
31
|
+
codex_required?: boolean | undefined;
|
|
32
|
+
}>>;
|
|
33
|
+
redact: z.ZodOptional<z.ZodObject<{
|
|
34
|
+
match_timeout_ms: z.ZodOptional<z.ZodNumber>;
|
|
35
|
+
patterns: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
36
|
+
name: z.ZodString;
|
|
37
|
+
regex: z.ZodString;
|
|
38
|
+
flags: z.ZodOptional<z.ZodString>;
|
|
39
|
+
}, "strict", z.ZodTypeAny, {
|
|
40
|
+
name: string;
|
|
41
|
+
regex: string;
|
|
42
|
+
flags?: string | undefined;
|
|
43
|
+
}, {
|
|
44
|
+
name: string;
|
|
45
|
+
regex: string;
|
|
46
|
+
flags?: string | undefined;
|
|
47
|
+
}>, "many">>;
|
|
48
|
+
}, "strict", z.ZodTypeAny, {
|
|
49
|
+
match_timeout_ms?: number | undefined;
|
|
50
|
+
patterns?: {
|
|
51
|
+
name: string;
|
|
52
|
+
regex: string;
|
|
53
|
+
flags?: string | undefined;
|
|
54
|
+
}[] | undefined;
|
|
55
|
+
}, {
|
|
56
|
+
match_timeout_ms?: number | undefined;
|
|
57
|
+
patterns?: {
|
|
58
|
+
name: string;
|
|
59
|
+
regex: string;
|
|
60
|
+
flags?: string | undefined;
|
|
61
|
+
}[] | undefined;
|
|
62
|
+
}>>;
|
|
26
63
|
}, "strict", z.ZodTypeAny, {
|
|
27
64
|
version: string;
|
|
28
65
|
profile: string;
|
|
@@ -39,6 +76,17 @@ declare const PolicySchema: z.ZodObject<{
|
|
|
39
76
|
delegate_to_subagent: string[];
|
|
40
77
|
max_bash_output_lines?: number | undefined;
|
|
41
78
|
} | undefined;
|
|
79
|
+
review?: {
|
|
80
|
+
codex_required?: boolean | undefined;
|
|
81
|
+
} | undefined;
|
|
82
|
+
redact?: {
|
|
83
|
+
match_timeout_ms?: number | undefined;
|
|
84
|
+
patterns?: {
|
|
85
|
+
name: string;
|
|
86
|
+
regex: string;
|
|
87
|
+
flags?: string | undefined;
|
|
88
|
+
}[] | undefined;
|
|
89
|
+
} | undefined;
|
|
42
90
|
}, {
|
|
43
91
|
version: string;
|
|
44
92
|
profile: string;
|
|
@@ -55,6 +103,17 @@ declare const PolicySchema: z.ZodObject<{
|
|
|
55
103
|
delegate_to_subagent?: string[] | undefined;
|
|
56
104
|
max_bash_output_lines?: number | undefined;
|
|
57
105
|
} | undefined;
|
|
106
|
+
review?: {
|
|
107
|
+
codex_required?: boolean | undefined;
|
|
108
|
+
} | undefined;
|
|
109
|
+
redact?: {
|
|
110
|
+
match_timeout_ms?: number | undefined;
|
|
111
|
+
patterns?: {
|
|
112
|
+
name: string;
|
|
113
|
+
regex: string;
|
|
114
|
+
flags?: string | undefined;
|
|
115
|
+
}[] | undefined;
|
|
116
|
+
} | undefined;
|
|
58
117
|
}>;
|
|
59
118
|
/**
|
|
60
119
|
* Async policy loader with TTL cache and mtime-based invalidation.
|
package/dist/policy/loader.js
CHANGED
|
@@ -3,6 +3,7 @@ import fsPromises from 'node:fs/promises';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { parse as parseYaml } from 'yaml';
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
+
import safeRegex from 'safe-regex';
|
|
6
7
|
import { AutonomyLevel } from './types.js';
|
|
7
8
|
const LEVEL_ORDER = {
|
|
8
9
|
[AutonomyLevel.L0]: 0,
|
|
@@ -14,6 +15,37 @@ const ContextProtectionSchema = z.object({
|
|
|
14
15
|
delegate_to_subagent: z.array(z.string()).default([]),
|
|
15
16
|
max_bash_output_lines: z.number().int().positive().optional(),
|
|
16
17
|
});
|
|
18
|
+
/**
|
|
19
|
+
* G11.2: minimal review policy. Only `codex_required` is recognized today;
|
|
20
|
+
* G11.4 will expand this (profile defaults, reviewer pin, token caps).
|
|
21
|
+
* Kept strict so a typo (`codex_require`) fails loudly instead of silently
|
|
22
|
+
* defaulting.
|
|
23
|
+
*/
|
|
24
|
+
const ReviewPolicySchema = z
|
|
25
|
+
.object({
|
|
26
|
+
codex_required: z.boolean().optional(),
|
|
27
|
+
})
|
|
28
|
+
.strict();
|
|
29
|
+
/**
|
|
30
|
+
* G3: user-supplied redaction pattern. `name` is audit-stable; `regex` is a
|
|
31
|
+
* raw pattern source (no leading/trailing slashes); `flags` follows JS
|
|
32
|
+
* RegExp flag semantics. Every pattern is passed through `safe-regex` at
|
|
33
|
+
* load time — a flagged pattern rejects the entire policy load with an
|
|
34
|
+
* error that names the offender.
|
|
35
|
+
*/
|
|
36
|
+
const UserRedactPatternSchema = z
|
|
37
|
+
.object({
|
|
38
|
+
name: z.string().min(1),
|
|
39
|
+
regex: z.string().min(1),
|
|
40
|
+
flags: z.string().optional(),
|
|
41
|
+
})
|
|
42
|
+
.strict();
|
|
43
|
+
const RedactPolicySchema = z
|
|
44
|
+
.object({
|
|
45
|
+
match_timeout_ms: z.number().int().positive().optional(),
|
|
46
|
+
patterns: z.array(UserRedactPatternSchema).optional(),
|
|
47
|
+
})
|
|
48
|
+
.strict();
|
|
17
49
|
const PolicySchema = z
|
|
18
50
|
.object({
|
|
19
51
|
version: z.string(),
|
|
@@ -28,6 +60,8 @@ const PolicySchema = z
|
|
|
28
60
|
notification_channel: z.string().default(''),
|
|
29
61
|
injection_detection: z.enum(['block', 'warn']).optional(),
|
|
30
62
|
context_protection: ContextProtectionSchema.optional(),
|
|
63
|
+
review: ReviewPolicySchema.optional(),
|
|
64
|
+
redact: RedactPolicySchema.optional(),
|
|
31
65
|
})
|
|
32
66
|
.strict();
|
|
33
67
|
const DEFAULT_CACHE_TTL_MS = 30_000;
|
|
@@ -58,6 +92,34 @@ function applyMaxCeiling(policy) {
|
|
|
58
92
|
}
|
|
59
93
|
return policy;
|
|
60
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* G3: run every user-supplied redact pattern through `safe-regex`. A flagged
|
|
97
|
+
* pattern rejects the entire policy load with an error that names the
|
|
98
|
+
* offender. Also verifies the pattern actually compiles — a malformed regex
|
|
99
|
+
* source is a clear policy authoring bug and should fail loud.
|
|
100
|
+
*/
|
|
101
|
+
function checkUserRedactPatterns(policy, policyPath) {
|
|
102
|
+
const patterns = policy.redact?.patterns;
|
|
103
|
+
if (!patterns || patterns.length === 0)
|
|
104
|
+
return;
|
|
105
|
+
for (const entry of patterns) {
|
|
106
|
+
let compiled;
|
|
107
|
+
try {
|
|
108
|
+
compiled = new RegExp(entry.regex, entry.flags);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
throw new Error(`Invalid redact pattern "${entry.name}" at ${policyPath}: ` +
|
|
112
|
+
`cannot compile regex ${JSON.stringify(entry.regex)}` +
|
|
113
|
+
(entry.flags ? ` with flags ${JSON.stringify(entry.flags)}` : '') +
|
|
114
|
+
` — ${err instanceof Error ? err.message : String(err)}`);
|
|
115
|
+
}
|
|
116
|
+
if (!safeRegex(compiled)) {
|
|
117
|
+
throw new Error(`Unsafe redact pattern "${entry.name}" at ${policyPath}: ` +
|
|
118
|
+
`safe-regex flagged ${JSON.stringify(entry.regex)} as potentially ReDoS-vulnerable. ` +
|
|
119
|
+
`Rewrite with bounded quantifiers / no nested repetition / no disjoint alternation.`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
61
123
|
function parseRawPolicy(raw, policyPath) {
|
|
62
124
|
let parsed;
|
|
63
125
|
try {
|
|
@@ -73,6 +135,9 @@ function parseRawPolicy(raw, policyPath) {
|
|
|
73
135
|
catch (zodErr) {
|
|
74
136
|
throw new Error(`Invalid policy schema at ${policyPath}: ${zodErr instanceof Error ? zodErr.message : zodErr}`);
|
|
75
137
|
}
|
|
138
|
+
// G3: reject unsafe user-supplied redaction patterns. This runs BEFORE
|
|
139
|
+
// stripUndefined so the error references the user-authored field exactly.
|
|
140
|
+
checkUserRedactPatterns(parsedPolicy, policyPath);
|
|
76
141
|
return applyMaxCeiling(stripUndefined(parsedPolicy));
|
|
77
142
|
}
|
|
78
143
|
function policyPathFor(baseDir) {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile schema + merge helper used at `rea init` time.
|
|
3
|
+
*
|
|
4
|
+
* A profile is a named set of policy defaults shipped with the package under
|
|
5
|
+
* `profiles/*.yaml`. Profiles are NOT a runtime indirection — at init time the
|
|
6
|
+
* chosen profile is materialized literally into `.rea/policy.yaml`, so the
|
|
7
|
+
* resulting file is self-contained and survives `npm uninstall @bookedsolid/rea`.
|
|
8
|
+
*
|
|
9
|
+
* Merge order (lowest to highest precedence):
|
|
10
|
+
* hardDefaults ← profile ← reagentTranslation ← wizardAnswers
|
|
11
|
+
*
|
|
12
|
+
* Hard defaults come from this module; profile YAMLs come from `profiles/`;
|
|
13
|
+
* reagent translation is applied by `cli/install/reagent.ts`; wizard answers
|
|
14
|
+
* come from `cli/init.ts` (interactive or `--yes`).
|
|
15
|
+
*/
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { AutonomyLevel } from './types.js';
|
|
18
|
+
/**
|
|
19
|
+
* Profile is PolicySchema with every field optional. Strict mode still rejects
|
|
20
|
+
* unknown keys so a typo in a profile YAML fails loudly at init time rather
|
|
21
|
+
* than silently getting dropped on the floor.
|
|
22
|
+
*/
|
|
23
|
+
export declare const ProfileSchema: z.ZodObject<{
|
|
24
|
+
autonomy_level: z.ZodOptional<z.ZodNativeEnum<typeof AutonomyLevel>>;
|
|
25
|
+
max_autonomy_level: z.ZodOptional<z.ZodNativeEnum<typeof AutonomyLevel>>;
|
|
26
|
+
promotion_requires_human_approval: z.ZodOptional<z.ZodBoolean>;
|
|
27
|
+
block_ai_attribution: z.ZodOptional<z.ZodBoolean>;
|
|
28
|
+
blocked_paths: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
29
|
+
notification_channel: z.ZodOptional<z.ZodString>;
|
|
30
|
+
injection_detection: z.ZodOptional<z.ZodEnum<["block", "warn"]>>;
|
|
31
|
+
context_protection: z.ZodOptional<z.ZodObject<{
|
|
32
|
+
delegate_to_subagent: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
33
|
+
max_bash_output_lines: z.ZodOptional<z.ZodNumber>;
|
|
34
|
+
}, "strict", z.ZodTypeAny, {
|
|
35
|
+
delegate_to_subagent?: string[] | undefined;
|
|
36
|
+
max_bash_output_lines?: number | undefined;
|
|
37
|
+
}, {
|
|
38
|
+
delegate_to_subagent?: string[] | undefined;
|
|
39
|
+
max_bash_output_lines?: number | undefined;
|
|
40
|
+
}>>;
|
|
41
|
+
}, "strict", z.ZodTypeAny, {
|
|
42
|
+
autonomy_level?: AutonomyLevel | undefined;
|
|
43
|
+
max_autonomy_level?: AutonomyLevel | undefined;
|
|
44
|
+
promotion_requires_human_approval?: boolean | undefined;
|
|
45
|
+
block_ai_attribution?: boolean | undefined;
|
|
46
|
+
blocked_paths?: string[] | undefined;
|
|
47
|
+
notification_channel?: string | undefined;
|
|
48
|
+
injection_detection?: "block" | "warn" | undefined;
|
|
49
|
+
context_protection?: {
|
|
50
|
+
delegate_to_subagent?: string[] | undefined;
|
|
51
|
+
max_bash_output_lines?: number | undefined;
|
|
52
|
+
} | undefined;
|
|
53
|
+
}, {
|
|
54
|
+
autonomy_level?: AutonomyLevel | undefined;
|
|
55
|
+
max_autonomy_level?: AutonomyLevel | undefined;
|
|
56
|
+
promotion_requires_human_approval?: boolean | undefined;
|
|
57
|
+
block_ai_attribution?: boolean | undefined;
|
|
58
|
+
blocked_paths?: string[] | undefined;
|
|
59
|
+
notification_channel?: string | undefined;
|
|
60
|
+
injection_detection?: "block" | "warn" | undefined;
|
|
61
|
+
context_protection?: {
|
|
62
|
+
delegate_to_subagent?: string[] | undefined;
|
|
63
|
+
max_bash_output_lines?: number | undefined;
|
|
64
|
+
} | undefined;
|
|
65
|
+
}>;
|
|
66
|
+
export type Profile = z.infer<typeof ProfileSchema>;
|
|
67
|
+
/** Hard defaults applied before any profile or wizard answer. */
|
|
68
|
+
export declare const HARD_DEFAULTS: Profile;
|
|
69
|
+
/**
|
|
70
|
+
* Shallow merge: `override` wins per top-level key when defined.
|
|
71
|
+
* Arrays are replaced, not concatenated — a profile that declares
|
|
72
|
+
* `blocked_paths` fully owns that list.
|
|
73
|
+
*/
|
|
74
|
+
export declare function mergeProfiles(base: Profile, override: Profile): Profile;
|
|
75
|
+
/**
|
|
76
|
+
* Resolve `profiles/${name}.yaml` relative to the package root. Returns `null`
|
|
77
|
+
* when the profile file is absent; callers should fall through to hard defaults
|
|
78
|
+
* in that case and print a warning.
|
|
79
|
+
*/
|
|
80
|
+
export declare function loadProfile(name: string): Profile | null;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile schema + merge helper used at `rea init` time.
|
|
3
|
+
*
|
|
4
|
+
* A profile is a named set of policy defaults shipped with the package under
|
|
5
|
+
* `profiles/*.yaml`. Profiles are NOT a runtime indirection — at init time the
|
|
6
|
+
* chosen profile is materialized literally into `.rea/policy.yaml`, so the
|
|
7
|
+
* resulting file is self-contained and survives `npm uninstall @bookedsolid/rea`.
|
|
8
|
+
*
|
|
9
|
+
* Merge order (lowest to highest precedence):
|
|
10
|
+
* hardDefaults ← profile ← reagentTranslation ← wizardAnswers
|
|
11
|
+
*
|
|
12
|
+
* Hard defaults come from this module; profile YAMLs come from `profiles/`;
|
|
13
|
+
* reagent translation is applied by `cli/install/reagent.ts`; wizard answers
|
|
14
|
+
* come from `cli/init.ts` (interactive or `--yes`).
|
|
15
|
+
*/
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { parse as parseYaml } from 'yaml';
|
|
19
|
+
import { z } from 'zod';
|
|
20
|
+
import { AutonomyLevel } from './types.js';
|
|
21
|
+
import { PKG_ROOT } from '../cli/utils.js';
|
|
22
|
+
const ContextProtectionProfileSchema = z
|
|
23
|
+
.object({
|
|
24
|
+
delegate_to_subagent: z.array(z.string()).optional(),
|
|
25
|
+
max_bash_output_lines: z.number().int().positive().optional(),
|
|
26
|
+
})
|
|
27
|
+
.strict();
|
|
28
|
+
/**
|
|
29
|
+
* Profile is PolicySchema with every field optional. Strict mode still rejects
|
|
30
|
+
* unknown keys so a typo in a profile YAML fails loudly at init time rather
|
|
31
|
+
* than silently getting dropped on the floor.
|
|
32
|
+
*/
|
|
33
|
+
export const ProfileSchema = z
|
|
34
|
+
.object({
|
|
35
|
+
autonomy_level: z.nativeEnum(AutonomyLevel).optional(),
|
|
36
|
+
max_autonomy_level: z.nativeEnum(AutonomyLevel).optional(),
|
|
37
|
+
promotion_requires_human_approval: z.boolean().optional(),
|
|
38
|
+
block_ai_attribution: z.boolean().optional(),
|
|
39
|
+
blocked_paths: z.array(z.string()).optional(),
|
|
40
|
+
notification_channel: z.string().optional(),
|
|
41
|
+
injection_detection: z.enum(['block', 'warn']).optional(),
|
|
42
|
+
context_protection: ContextProtectionProfileSchema.optional(),
|
|
43
|
+
})
|
|
44
|
+
.strict();
|
|
45
|
+
/** Hard defaults applied before any profile or wizard answer. */
|
|
46
|
+
export const HARD_DEFAULTS = {
|
|
47
|
+
autonomy_level: AutonomyLevel.L1,
|
|
48
|
+
max_autonomy_level: AutonomyLevel.L2,
|
|
49
|
+
promotion_requires_human_approval: true,
|
|
50
|
+
block_ai_attribution: true,
|
|
51
|
+
blocked_paths: ['.env', '.env.*'],
|
|
52
|
+
notification_channel: '',
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Shallow merge: `override` wins per top-level key when defined.
|
|
56
|
+
* Arrays are replaced, not concatenated — a profile that declares
|
|
57
|
+
* `blocked_paths` fully owns that list.
|
|
58
|
+
*/
|
|
59
|
+
export function mergeProfiles(base, override) {
|
|
60
|
+
const merged = { ...base };
|
|
61
|
+
for (const [k, v] of Object.entries(override)) {
|
|
62
|
+
if (v !== undefined) {
|
|
63
|
+
merged[k] = v;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return merged;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Resolve `profiles/${name}.yaml` relative to the package root. Returns `null`
|
|
70
|
+
* when the profile file is absent; callers should fall through to hard defaults
|
|
71
|
+
* in that case and print a warning.
|
|
72
|
+
*/
|
|
73
|
+
export function loadProfile(name) {
|
|
74
|
+
const profilePath = path.join(PKG_ROOT, 'profiles', `${name}.yaml`);
|
|
75
|
+
if (!fs.existsSync(profilePath))
|
|
76
|
+
return null;
|
|
77
|
+
const raw = fs.readFileSync(profilePath, 'utf8');
|
|
78
|
+
let parsed;
|
|
79
|
+
try {
|
|
80
|
+
parsed = parseYaml(raw);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
throw new Error(`Failed to parse profile YAML at ${profilePath}: ${err instanceof Error ? err.message : err}`);
|
|
84
|
+
}
|
|
85
|
+
// Empty YAML file → parseYaml returns null; treat as empty profile.
|
|
86
|
+
if (parsed === null || parsed === undefined)
|
|
87
|
+
return {};
|
|
88
|
+
try {
|
|
89
|
+
return ProfileSchema.parse(parsed);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
throw new Error(`Invalid profile schema at ${profilePath}: ${err instanceof Error ? err.message : err}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
package/dist/policy/types.d.ts
CHANGED
|
@@ -18,6 +18,42 @@ export interface ContextProtection {
|
|
|
18
18
|
delegate_to_subagent: string[];
|
|
19
19
|
max_bash_output_lines?: number;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Review policy knobs. G11.2 only needs `codex_required` as an optional
|
|
23
|
+
* signal to the reviewer selector; G11.4 will flesh this out into a full
|
|
24
|
+
* first-class no-Codex mode (profile defaults, init defaults, etc.).
|
|
25
|
+
*/
|
|
26
|
+
export interface ReviewPolicy {
|
|
27
|
+
/**
|
|
28
|
+
* When `false`, the selector treats ClaudeSelfReviewer as the preferred
|
|
29
|
+
* reviewer (not degraded). When `true` or unset, Codex is preferred and
|
|
30
|
+
* a ClaudeSelfReviewer result is marked `degraded: true` in the audit
|
|
31
|
+
* log. Default when unset is `true` (Codex required).
|
|
32
|
+
*/
|
|
33
|
+
codex_required?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* User-supplied redaction pattern entry. Each pattern has a stable `name` used
|
|
37
|
+
* in audit events, a raw `regex` source string, and optional `flags`. The
|
|
38
|
+
* loader validates every pattern via `safe-regex` at load time (G3) — any
|
|
39
|
+
* pattern flagged unsafe fails the load with a specific error naming the
|
|
40
|
+
* offender. The `regex` source is compiled inside a `SafeRegex` timeout
|
|
41
|
+
* wrapper by the gateway at middleware-creation time.
|
|
42
|
+
*/
|
|
43
|
+
export interface UserRedactPattern {
|
|
44
|
+
name: string;
|
|
45
|
+
regex: string;
|
|
46
|
+
flags?: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Redaction policy knobs (G3). `match_timeout_ms` bounds every per-call regex
|
|
50
|
+
* execution; `patterns` are user-supplied regexes layered on top of the
|
|
51
|
+
* built-in SECRET_PATTERNS.
|
|
52
|
+
*/
|
|
53
|
+
export interface RedactPolicy {
|
|
54
|
+
match_timeout_ms?: number;
|
|
55
|
+
patterns?: UserRedactPattern[];
|
|
56
|
+
}
|
|
21
57
|
export interface Policy {
|
|
22
58
|
version: string;
|
|
23
59
|
profile: string;
|
|
@@ -31,4 +67,6 @@ export interface Policy {
|
|
|
31
67
|
notification_channel: string;
|
|
32
68
|
injection_detection?: 'block' | 'warn';
|
|
33
69
|
context_protection?: ContextProtection;
|
|
70
|
+
review?: ReviewPolicy;
|
|
71
|
+
redact?: RedactPolicy;
|
|
34
72
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry loader — parses `.rea/registry.yaml`, validates with zod, and
|
|
3
|
+
* caches the result with the same TTL + mtime-invalidation pattern as
|
|
4
|
+
* `src/policy/loader.ts`. Keep the two loaders structurally similar; if one
|
|
5
|
+
* gets a new invariant (e.g. cross-process locking), the other probably
|
|
6
|
+
* needs it too.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { Tier } from '../policy/types.js';
|
|
10
|
+
import type { Registry } from './types.js';
|
|
11
|
+
declare const RegistryServerSchema: z.ZodObject<{
|
|
12
|
+
name: z.ZodString;
|
|
13
|
+
command: z.ZodString;
|
|
14
|
+
args: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
15
|
+
env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
16
|
+
env_passthrough: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
17
|
+
tier_overrides: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNativeEnum<typeof Tier>>>;
|
|
18
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
19
|
+
}, "strict", z.ZodTypeAny, {
|
|
20
|
+
name: string;
|
|
21
|
+
command: string;
|
|
22
|
+
args: string[];
|
|
23
|
+
env: Record<string, string>;
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
env_passthrough?: string[] | undefined;
|
|
26
|
+
tier_overrides?: Record<string, Tier> | undefined;
|
|
27
|
+
}, {
|
|
28
|
+
name: string;
|
|
29
|
+
command: string;
|
|
30
|
+
args?: string[] | undefined;
|
|
31
|
+
env?: Record<string, string> | undefined;
|
|
32
|
+
env_passthrough?: string[] | undefined;
|
|
33
|
+
tier_overrides?: Record<string, Tier> | undefined;
|
|
34
|
+
enabled?: boolean | undefined;
|
|
35
|
+
}>;
|
|
36
|
+
declare const RegistrySchema: z.ZodObject<{
|
|
37
|
+
version: z.ZodLiteral<"1">;
|
|
38
|
+
servers: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
39
|
+
name: z.ZodString;
|
|
40
|
+
command: z.ZodString;
|
|
41
|
+
args: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
42
|
+
env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
43
|
+
env_passthrough: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
44
|
+
tier_overrides: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNativeEnum<typeof Tier>>>;
|
|
45
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
46
|
+
}, "strict", z.ZodTypeAny, {
|
|
47
|
+
name: string;
|
|
48
|
+
command: string;
|
|
49
|
+
args: string[];
|
|
50
|
+
env: Record<string, string>;
|
|
51
|
+
enabled: boolean;
|
|
52
|
+
env_passthrough?: string[] | undefined;
|
|
53
|
+
tier_overrides?: Record<string, Tier> | undefined;
|
|
54
|
+
}, {
|
|
55
|
+
name: string;
|
|
56
|
+
command: string;
|
|
57
|
+
args?: string[] | undefined;
|
|
58
|
+
env?: Record<string, string> | undefined;
|
|
59
|
+
env_passthrough?: string[] | undefined;
|
|
60
|
+
tier_overrides?: Record<string, Tier> | undefined;
|
|
61
|
+
enabled?: boolean | undefined;
|
|
62
|
+
}>, "many">>;
|
|
63
|
+
reviewer: z.ZodOptional<z.ZodEnum<["codex", "claude-self"]>>;
|
|
64
|
+
}, "strict", z.ZodTypeAny, {
|
|
65
|
+
version: "1";
|
|
66
|
+
servers: {
|
|
67
|
+
name: string;
|
|
68
|
+
command: string;
|
|
69
|
+
args: string[];
|
|
70
|
+
env: Record<string, string>;
|
|
71
|
+
enabled: boolean;
|
|
72
|
+
env_passthrough?: string[] | undefined;
|
|
73
|
+
tier_overrides?: Record<string, Tier> | undefined;
|
|
74
|
+
}[];
|
|
75
|
+
reviewer?: "codex" | "claude-self" | undefined;
|
|
76
|
+
}, {
|
|
77
|
+
version: "1";
|
|
78
|
+
servers?: {
|
|
79
|
+
name: string;
|
|
80
|
+
command: string;
|
|
81
|
+
args?: string[] | undefined;
|
|
82
|
+
env?: Record<string, string> | undefined;
|
|
83
|
+
env_passthrough?: string[] | undefined;
|
|
84
|
+
tier_overrides?: Record<string, Tier> | undefined;
|
|
85
|
+
enabled?: boolean | undefined;
|
|
86
|
+
}[] | undefined;
|
|
87
|
+
reviewer?: "codex" | "claude-self" | undefined;
|
|
88
|
+
}>;
|
|
89
|
+
/**
|
|
90
|
+
* Async registry loader with TTL cache and mtime-based invalidation.
|
|
91
|
+
* Mirrors the contract of `loadPolicyAsync` — see its header for the
|
|
92
|
+
* security/concurrency rationale.
|
|
93
|
+
*/
|
|
94
|
+
export declare function loadRegistryAsync(baseDir: string): Promise<Registry>;
|
|
95
|
+
/** Synchronous loader — for CLI startup paths. No cache. */
|
|
96
|
+
export declare function loadRegistry(baseDir: string): Registry;
|
|
97
|
+
export declare function invalidateRegistryCache(baseDir?: string): void;
|
|
98
|
+
export { RegistrySchema, RegistryServerSchema };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry loader — parses `.rea/registry.yaml`, validates with zod, and
|
|
3
|
+
* caches the result with the same TTL + mtime-invalidation pattern as
|
|
4
|
+
* `src/policy/loader.ts`. Keep the two loaders structurally similar; if one
|
|
5
|
+
* gets a new invariant (e.g. cross-process locking), the other probably
|
|
6
|
+
* needs it too.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import fsPromises from 'node:fs/promises';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { parse as parseYaml } from 'yaml';
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { Tier } from '../policy/types.js';
|
|
14
|
+
/**
|
|
15
|
+
* Regex used to refuse passthrough of var names that look like secrets.
|
|
16
|
+
* Explicit `env:` mapping is the escape hatch — if a user types the value into
|
|
17
|
+
* the registry, the operator has consciously authorized it. Passthrough pulls
|
|
18
|
+
* from the host environment silently, so we refuse secret-looking names there.
|
|
19
|
+
*/
|
|
20
|
+
const SECRET_NAME_HEURISTIC = /(TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL)/i;
|
|
21
|
+
const RegistryServerSchema = z
|
|
22
|
+
.object({
|
|
23
|
+
name: z
|
|
24
|
+
.string()
|
|
25
|
+
.regex(/^[a-z0-9][a-z0-9-]*$/, 'server name must be lowercase-kebab and cannot start with a dash'),
|
|
26
|
+
command: z.string().min(1),
|
|
27
|
+
args: z.array(z.string()).default([]),
|
|
28
|
+
env: z.record(z.string()).default({}),
|
|
29
|
+
env_passthrough: z
|
|
30
|
+
.array(z
|
|
31
|
+
.string()
|
|
32
|
+
.regex(/^[A-Za-z_][A-Za-z0-9_]*$/, 'env var name must match POSIX identifier syntax')
|
|
33
|
+
.refine((name) => !SECRET_NAME_HEURISTIC.test(name), (name) => ({
|
|
34
|
+
message: `env_passthrough refuses secret-looking name "${name}" — use an explicit env: mapping instead`,
|
|
35
|
+
})))
|
|
36
|
+
.optional(),
|
|
37
|
+
tier_overrides: z.record(z.nativeEnum(Tier)).optional(),
|
|
38
|
+
enabled: z.boolean().default(true),
|
|
39
|
+
})
|
|
40
|
+
.strict();
|
|
41
|
+
/**
|
|
42
|
+
* G11.2: optional `reviewer:` pin at the registry top level. Values are
|
|
43
|
+
* intentionally constrained to the two reviewers the selector knows how
|
|
44
|
+
* to dispatch — an unknown string (e.g. a typo'd `claude_self`) is
|
|
45
|
+
* rejected at parse time, same as `env_passthrough` does for secret-looking
|
|
46
|
+
* names. If we grow a third reviewer, extend this enum alongside the
|
|
47
|
+
* RegistryReviewer type.
|
|
48
|
+
*/
|
|
49
|
+
const RegistryReviewerSchema = z.enum(['codex', 'claude-self']);
|
|
50
|
+
const RegistrySchema = z
|
|
51
|
+
.object({
|
|
52
|
+
version: z.literal('1'),
|
|
53
|
+
servers: z.array(RegistryServerSchema).default([]),
|
|
54
|
+
reviewer: RegistryReviewerSchema.optional(),
|
|
55
|
+
})
|
|
56
|
+
.strict();
|
|
57
|
+
const DEFAULT_CACHE_TTL_MS = 30_000;
|
|
58
|
+
const REA_DIR = '.rea';
|
|
59
|
+
const REGISTRY_FILE = 'registry.yaml';
|
|
60
|
+
const cache = new Map();
|
|
61
|
+
const inflight = new Map();
|
|
62
|
+
function registryPathFor(baseDir) {
|
|
63
|
+
return path.join(baseDir, REA_DIR, REGISTRY_FILE);
|
|
64
|
+
}
|
|
65
|
+
function stripUndefined(input) {
|
|
66
|
+
const servers = input.servers.map((s) => {
|
|
67
|
+
const out = {
|
|
68
|
+
name: s.name,
|
|
69
|
+
command: s.command,
|
|
70
|
+
args: s.args,
|
|
71
|
+
env: s.env,
|
|
72
|
+
enabled: s.enabled,
|
|
73
|
+
};
|
|
74
|
+
if (s.env_passthrough !== undefined)
|
|
75
|
+
out.env_passthrough = s.env_passthrough;
|
|
76
|
+
if (s.tier_overrides !== undefined)
|
|
77
|
+
out.tier_overrides = s.tier_overrides;
|
|
78
|
+
return out;
|
|
79
|
+
});
|
|
80
|
+
const out = { version: input.version, servers };
|
|
81
|
+
if (input.reviewer !== undefined)
|
|
82
|
+
out.reviewer = input.reviewer;
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
function parseRaw(raw, filePath) {
|
|
86
|
+
let parsed;
|
|
87
|
+
try {
|
|
88
|
+
parsed = parseYaml(raw);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
throw new Error(`Failed to parse registry YAML at ${filePath}: ${err instanceof Error ? err.message : err}`);
|
|
92
|
+
}
|
|
93
|
+
// Empty file → treat as an empty registry so `rea init` + no edits still works.
|
|
94
|
+
const normalized = parsed === null || parsed === undefined ? { version: '1', servers: [] } : parsed;
|
|
95
|
+
try {
|
|
96
|
+
return stripUndefined(RegistrySchema.parse(normalized));
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
throw new Error(`Invalid registry schema at ${filePath}: ${err instanceof Error ? err.message : err}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function readFromDisk(baseDir, filePath, currentMtime) {
|
|
103
|
+
const raw = await fsPromises.readFile(filePath, 'utf8');
|
|
104
|
+
const registry = parseRaw(raw, filePath);
|
|
105
|
+
cache.set(baseDir, { registry, cachedAt: Date.now(), mtimeMs: currentMtime });
|
|
106
|
+
return registry;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Async registry loader with TTL cache and mtime-based invalidation.
|
|
110
|
+
* Mirrors the contract of `loadPolicyAsync` — see its header for the
|
|
111
|
+
* security/concurrency rationale.
|
|
112
|
+
*/
|
|
113
|
+
export async function loadRegistryAsync(baseDir) {
|
|
114
|
+
const filePath = registryPathFor(baseDir);
|
|
115
|
+
const ttlMs = Number(process.env.REA_REGISTRY_CACHE_TTL_MS ?? DEFAULT_CACHE_TTL_MS);
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
let currentMtime;
|
|
118
|
+
try {
|
|
119
|
+
const stat = await fsPromises.stat(filePath);
|
|
120
|
+
currentMtime = stat.mtimeMs;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
throw new Error(`Registry file not found: ${filePath}`);
|
|
124
|
+
}
|
|
125
|
+
const cached = cache.get(baseDir);
|
|
126
|
+
if (cached !== undefined && cached.mtimeMs === currentMtime && now - cached.cachedAt < ttlMs) {
|
|
127
|
+
return cached.registry;
|
|
128
|
+
}
|
|
129
|
+
const pending = inflight.get(baseDir);
|
|
130
|
+
if (pending)
|
|
131
|
+
return pending;
|
|
132
|
+
const read = readFromDisk(baseDir, filePath, currentMtime).finally(() => {
|
|
133
|
+
inflight.delete(baseDir);
|
|
134
|
+
});
|
|
135
|
+
inflight.set(baseDir, read);
|
|
136
|
+
return read;
|
|
137
|
+
}
|
|
138
|
+
/** Synchronous loader — for CLI startup paths. No cache. */
|
|
139
|
+
export function loadRegistry(baseDir) {
|
|
140
|
+
const filePath = registryPathFor(baseDir);
|
|
141
|
+
if (!fs.existsSync(filePath)) {
|
|
142
|
+
throw new Error(`Registry file not found: ${filePath}`);
|
|
143
|
+
}
|
|
144
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
145
|
+
return parseRaw(raw, filePath);
|
|
146
|
+
}
|
|
147
|
+
export function invalidateRegistryCache(baseDir) {
|
|
148
|
+
if (baseDir === undefined)
|
|
149
|
+
cache.clear();
|
|
150
|
+
else
|
|
151
|
+
cache.delete(baseDir);
|
|
152
|
+
}
|
|
153
|
+
export { RegistrySchema, RegistryServerSchema };
|