@danielblomma/cortex-mcp 2.4.0 → 2.4.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 +201 -0
- package/README.md +19 -3
- package/bin/cortex.mjs +308 -75
- package/bin/daemon-control.mjs +162 -0
- package/package.json +7 -3
- package/scaffold/mcp/dist/.cortex-build-hash +1 -0
- package/scaffold/mcp/dist/cli/enterprise-setup.js +134 -0
- package/scaffold/mcp/dist/cli/govern.js +937 -0
- package/scaffold/mcp/dist/cli/query.js +409 -0
- package/scaffold/mcp/dist/cli/run.js +295 -0
- package/scaffold/mcp/dist/cli/stage.js +308 -0
- package/scaffold/mcp/dist/cli/telemetry-test.js +141 -0
- package/scaffold/mcp/dist/cli/ungoverned-detector.js +133 -0
- package/scaffold/mcp/dist/contextEntities.js +282 -0
- package/scaffold/mcp/dist/core/audit/query.js +72 -0
- package/scaffold/mcp/dist/core/audit/writer.js +28 -0
- package/scaffold/mcp/dist/core/config.js +235 -0
- package/scaffold/mcp/dist/core/enterprise-host-identity.js +193 -0
- package/scaffold/mcp/dist/core/enterprise-identity.js +18 -0
- package/scaffold/mcp/dist/core/enterprise-rotation.js +158 -0
- package/scaffold/mcp/dist/core/govern-paths.js +21 -0
- package/scaffold/mcp/dist/core/index.js +14 -0
- package/scaffold/mcp/dist/core/license.js +278 -0
- package/scaffold/mcp/dist/core/policy/enforce.js +66 -0
- package/scaffold/mcp/dist/core/policy/injection.js +172 -0
- package/scaffold/mcp/dist/core/policy/store.js +179 -0
- package/scaffold/mcp/dist/core/rbac/check.js +30 -0
- package/scaffold/mcp/dist/core/secure-endpoint.js +25 -0
- package/scaffold/mcp/dist/core/telemetry/collector.js +285 -0
- package/scaffold/mcp/dist/core/telemetry/state-dir.js +31 -0
- package/scaffold/mcp/dist/core/validators/builtins.js +632 -0
- package/scaffold/mcp/dist/core/validators/config.js +44 -0
- package/scaffold/mcp/dist/core/validators/engine.js +120 -0
- package/scaffold/mcp/dist/core/validators/evaluators/code_comments.js +236 -0
- package/scaffold/mcp/dist/core/validators/evaluators/regex.js +116 -0
- package/scaffold/mcp/dist/core/workflow/artifact-io.js +103 -0
- package/scaffold/mcp/dist/core/workflow/capabilities.js +88 -0
- package/scaffold/mcp/dist/core/workflow/default-workflows.js +102 -0
- package/scaffold/mcp/dist/core/workflow/enforcement.js +164 -0
- package/scaffold/mcp/dist/core/workflow/envelope.js +132 -0
- package/scaffold/mcp/dist/core/workflow/index.js +11 -0
- package/scaffold/mcp/dist/core/workflow/mcp-tools.js +175 -0
- package/scaffold/mcp/dist/core/workflow/resolution.js +69 -0
- package/scaffold/mcp/dist/core/workflow/run-lifecycle.js +123 -0
- package/scaffold/mcp/dist/core/workflow/schemas.js +141 -0
- package/scaffold/mcp/dist/core/workflow/synced-capability-registry.js +60 -0
- package/scaffold/mcp/dist/core/workflow/synced-registry.js +60 -0
- package/scaffold/mcp/dist/daemon/capability-sync-checker.js +242 -0
- package/scaffold/mcp/dist/daemon/client.js +130 -0
- package/scaffold/mcp/dist/daemon/egress-proxy.js +288 -0
- package/scaffold/mcp/dist/daemon/global-host-events.js +222 -0
- package/scaffold/mcp/dist/daemon/heartbeat-pusher.js +116 -0
- package/scaffold/mcp/dist/daemon/heartbeat-tracker.js +165 -0
- package/scaffold/mcp/dist/daemon/host-events-pusher.js +249 -0
- package/scaffold/mcp/dist/daemon/main.js +449 -0
- package/scaffold/mcp/dist/daemon/paths.js +36 -0
- package/scaffold/mcp/dist/daemon/project-service-registry.js +78 -0
- package/scaffold/mcp/dist/daemon/protocol.js +8 -0
- package/scaffold/mcp/dist/daemon/server.js +180 -0
- package/scaffold/mcp/dist/daemon/skill-sync-checker.js +557 -0
- package/scaffold/mcp/dist/daemon/sync-checker.js +165 -0
- package/scaffold/mcp/dist/daemon/ungoverned-scanner.js +136 -0
- package/scaffold/mcp/dist/daemon/workflow-sync-checker.js +249 -0
- package/scaffold/mcp/dist/defaults.js +6 -0
- package/scaffold/mcp/dist/embed.js +627 -0
- package/scaffold/mcp/dist/embedScheduler.js +479 -0
- package/scaffold/mcp/dist/embeddings.js +167 -0
- package/scaffold/mcp/dist/enterprise/audit/push.js +66 -0
- package/scaffold/mcp/dist/enterprise/index.js +327 -0
- package/scaffold/mcp/dist/enterprise/model/deploy.js +27 -0
- package/scaffold/mcp/dist/enterprise/policy/sync.js +129 -0
- package/scaffold/mcp/dist/enterprise/privacy/boundary.js +184 -0
- package/scaffold/mcp/dist/enterprise/reviews/changed-files.js +33 -0
- package/scaffold/mcp/dist/enterprise/reviews/pattern-context.js +202 -0
- package/scaffold/mcp/dist/enterprise/reviews/policy-selection.js +46 -0
- package/scaffold/mcp/dist/enterprise/reviews/push.js +102 -0
- package/scaffold/mcp/dist/enterprise/reviews/trust-state.js +186 -0
- package/scaffold/mcp/dist/enterprise/telemetry/sync.js +57 -0
- package/scaffold/mcp/dist/enterprise/tools/enterprise.js +826 -0
- package/scaffold/mcp/dist/enterprise/tools/harness.js +40 -0
- package/scaffold/mcp/dist/enterprise/tools/walk.js +73 -0
- package/scaffold/mcp/dist/enterprise/violations/push.js +77 -0
- package/scaffold/mcp/dist/enterprise/workflow/push.js +44 -0
- package/scaffold/mcp/dist/enterprise/workflow/state.js +329 -0
- package/scaffold/mcp/dist/frontmatter.js +33 -0
- package/scaffold/mcp/dist/graph.js +769 -0
- package/scaffold/mcp/dist/graphCsv.js +55 -0
- package/scaffold/mcp/dist/graphMetrics.js +8 -0
- package/scaffold/mcp/dist/hooks/permission-request.js +89 -0
- package/scaffold/mcp/dist/hooks/post-tool-use.js +105 -0
- package/scaffold/mcp/dist/hooks/pre-compact.js +29 -0
- package/scaffold/mcp/dist/hooks/pre-tool-use.js +78 -0
- package/scaffold/mcp/dist/hooks/session-end.js +43 -0
- package/scaffold/mcp/dist/hooks/session-start.js +41 -0
- package/scaffold/mcp/dist/hooks/shared.js +194 -0
- package/scaffold/mcp/dist/hooks/stop.js +28 -0
- package/scaffold/mcp/dist/hooks/user-prompt-submit.js +33 -0
- package/scaffold/mcp/dist/impactPresentation.js +137 -0
- package/scaffold/mcp/dist/impactRanking.js +191 -0
- package/scaffold/mcp/dist/impactResponse.js +30 -0
- package/scaffold/mcp/dist/impactResults.js +105 -0
- package/scaffold/mcp/dist/impactSeed.js +20 -0
- package/scaffold/mcp/dist/impactTraversal.js +64 -0
- package/scaffold/mcp/dist/jsonl.js +77 -0
- package/scaffold/mcp/dist/loadGraph.js +759 -0
- package/scaffold/mcp/dist/lruCache.js +38 -0
- package/scaffold/mcp/dist/paths.js +97 -0
- package/scaffold/mcp/dist/patternEvidence.js +272 -0
- package/scaffold/mcp/dist/plugin.js +81 -0
- package/scaffold/mcp/dist/presets.js +78 -0
- package/scaffold/mcp/dist/relatedResponse.js +18 -0
- package/scaffold/mcp/dist/relatedTraversal.js +78 -0
- package/scaffold/mcp/dist/rules.js +23 -0
- package/scaffold/mcp/dist/search.js +212 -0
- package/scaffold/mcp/dist/searchCore.js +457 -0
- package/scaffold/mcp/dist/searchResults.js +230 -0
- package/scaffold/mcp/dist/server.js +317 -0
- package/scaffold/mcp/dist/types.js +1 -0
- package/scaffold/mcp/package-lock.json +321 -200
- package/scaffold/mcp/package.json +15 -6
- package/scaffold/mcp/src/cli/enterprise-setup.ts +82 -8
- package/scaffold/mcp/src/cli/govern.ts +137 -52
- package/scaffold/mcp/src/cli/run.ts +53 -19
- package/scaffold/mcp/src/cli/stage.ts +8 -3
- package/scaffold/mcp/src/core/config.ts +9 -2
- package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
- package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
- package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
- package/scaffold/mcp/src/core/govern-paths.ts +30 -0
- package/scaffold/mcp/src/core/license.ts +186 -17
- package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
- package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
- package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
- package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
- package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
- package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
- package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
- package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
- package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
- package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
- package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
- package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
- package/scaffold/mcp/src/daemon/main.ts +99 -25
- package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
- package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
- package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
- package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
- package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
- package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
- package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
- package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
- package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
- package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
- package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
- package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
- package/scaffold/mcp/src/plugin.ts +20 -0
- package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
- package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
- package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
- package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
- package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
- package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
- package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
- package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
- package/scaffold/mcp/tests/license.test.mjs +367 -0
- package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
- package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
- package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
- package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
- package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
- package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
- package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
- package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const registry = new Map();
|
|
2
|
+
const genericRegistry = new Map();
|
|
3
|
+
export function registerValidator(def) {
|
|
4
|
+
registry.set(def.policyId, def);
|
|
5
|
+
}
|
|
6
|
+
export function getValidator(policyId) {
|
|
7
|
+
return registry.get(policyId);
|
|
8
|
+
}
|
|
9
|
+
export function getRegisteredPolicyIds() {
|
|
10
|
+
return [...registry.keys()];
|
|
11
|
+
}
|
|
12
|
+
export function registerGenericEvaluator(def) {
|
|
13
|
+
genericRegistry.set(def.type, def);
|
|
14
|
+
}
|
|
15
|
+
export function getGenericEvaluator(type) {
|
|
16
|
+
return genericRegistry.get(type);
|
|
17
|
+
}
|
|
18
|
+
export function getRegisteredEvaluatorTypes() {
|
|
19
|
+
return [...genericRegistry.keys()];
|
|
20
|
+
}
|
|
21
|
+
function resolvePolicySeverity(policy, fallback) {
|
|
22
|
+
if (!policy.severity)
|
|
23
|
+
return fallback;
|
|
24
|
+
return policy.severity === "block" ? "error" : policy.severity;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Run validators for every enforced policy. Dispatch order per policy:
|
|
28
|
+
* 1. If the policy has a `type`, look it up in the generic evaluator
|
|
29
|
+
* registry (cortex-web custom rules use this path).
|
|
30
|
+
* 2. Otherwise, look up a name-based validator by policy id
|
|
31
|
+
* (predefined rules shipped with the plugin use this path).
|
|
32
|
+
* 3. If neither path yields an implementation, emit a warning so the
|
|
33
|
+
* gap is visible instead of silent.
|
|
34
|
+
*
|
|
35
|
+
* Accepts either `Set<string>` (legacy id-only callers) or an array of
|
|
36
|
+
* `EnforcedPolicy` objects carrying `type` + `config` from the
|
|
37
|
+
* cortex-web policy sync. Set inputs are normalized to entries with
|
|
38
|
+
* null type/config, so they always route to the name-based registry.
|
|
39
|
+
*/
|
|
40
|
+
export async function runValidators(enforced, ctx, validatorConfigs) {
|
|
41
|
+
const policies = enforced instanceof Set
|
|
42
|
+
? [...enforced].map((id) => ({ id }))
|
|
43
|
+
: enforced;
|
|
44
|
+
const results = [];
|
|
45
|
+
for (const policy of policies) {
|
|
46
|
+
if (policy.type) {
|
|
47
|
+
const evaluator = genericRegistry.get(policy.type);
|
|
48
|
+
if (!evaluator) {
|
|
49
|
+
results.push({
|
|
50
|
+
policy_id: policy.id,
|
|
51
|
+
pass: false,
|
|
52
|
+
severity: "warning",
|
|
53
|
+
message: `No evaluator registered for type "${policy.type}"`,
|
|
54
|
+
detail: "This policy declares a generic evaluator type that is not " +
|
|
55
|
+
"implemented in this version of the enterprise plugin. " +
|
|
56
|
+
"Upgrade the plugin or change the rule type.",
|
|
57
|
+
});
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const result = await evaluator.check(ctx, policy.config ?? {});
|
|
62
|
+
results.push({
|
|
63
|
+
policy_id: policy.id,
|
|
64
|
+
pass: result.pass,
|
|
65
|
+
severity: resolvePolicySeverity(policy, result.severity),
|
|
66
|
+
message: result.message,
|
|
67
|
+
detail: result.detail,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
results.push({
|
|
72
|
+
policy_id: policy.id,
|
|
73
|
+
pass: false,
|
|
74
|
+
severity: "error",
|
|
75
|
+
message: `Evaluator error: ${err instanceof Error ? err.message : String(err)}`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const def = registry.get(policy.id);
|
|
81
|
+
if (!def) {
|
|
82
|
+
results.push({
|
|
83
|
+
policy_id: policy.id,
|
|
84
|
+
pass: false,
|
|
85
|
+
severity: "warning",
|
|
86
|
+
message: "No validator implementation registered for this policy",
|
|
87
|
+
detail: "This policy is enforced but the server-side check is missing. " +
|
|
88
|
+
"Either install an enterprise plugin that provides it, or disable " +
|
|
89
|
+
"enforcement in the policy dashboard.",
|
|
90
|
+
});
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const options = validatorConfigs[policy.id] ?? {};
|
|
94
|
+
try {
|
|
95
|
+
const result = await def.check(ctx, options);
|
|
96
|
+
results.push({
|
|
97
|
+
policy_id: policy.id,
|
|
98
|
+
pass: result.pass,
|
|
99
|
+
severity: resolvePolicySeverity(policy, result.severity),
|
|
100
|
+
message: result.message,
|
|
101
|
+
detail: result.detail,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
results.push({
|
|
106
|
+
policy_id: policy.id,
|
|
107
|
+
pass: false,
|
|
108
|
+
severity: "error",
|
|
109
|
+
message: `Validator error: ${err instanceof Error ? err.message : String(err)}`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const summary = {
|
|
114
|
+
total: results.length,
|
|
115
|
+
passed: results.filter((r) => r.pass).length,
|
|
116
|
+
failed: results.filter((r) => !r.pass && r.severity === "error").length,
|
|
117
|
+
warnings: results.filter((r) => !r.pass && r.severity === "warning").length,
|
|
118
|
+
};
|
|
119
|
+
return { results, summary };
|
|
120
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { statSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { registerGenericEvaluator, } from "../engine.js";
|
|
4
|
+
const LANGUAGES = [
|
|
5
|
+
{
|
|
6
|
+
name: "TypeScript/JavaScript",
|
|
7
|
+
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
|
|
8
|
+
functionPatterns: [
|
|
9
|
+
/^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s*(\w+)\s*(?:<[^>]*>)?\s*\(/,
|
|
10
|
+
/^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*(?::\s*[\w<>,\s[\]|&]+)?\s*=\s*(?:async\s*)?(?:<[^>]*>)?\s*\([^)]*\)\s*(?::\s*[\w<>,\s[\]|&]+\s*)?=>/,
|
|
11
|
+
/^\s*(?:public|private|protected)?\s*(?:static\s+)?(?:readonly\s+)?(?:async\s+)?(?:\*\s*)?(\w+)\s*(?:<[^>]*>)?\s*\([^)]*\)\s*(?::\s*[\w<>,\s[\]|&]+\s*)?\{/,
|
|
12
|
+
],
|
|
13
|
+
lineCommentPrefix: "//",
|
|
14
|
+
blockCommentStart: "/*",
|
|
15
|
+
blockCommentEnd: "*/",
|
|
16
|
+
endStyle: "braces",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: "Python",
|
|
20
|
+
extensions: [".py"],
|
|
21
|
+
functionPatterns: [
|
|
22
|
+
/^(\s*)(?:async\s+)?def\s+(\w+)\s*\(/,
|
|
23
|
+
],
|
|
24
|
+
lineCommentPrefix: "#",
|
|
25
|
+
blockCommentStart: "",
|
|
26
|
+
blockCommentEnd: "",
|
|
27
|
+
endStyle: "indent",
|
|
28
|
+
allowsDocstring: true,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "Go",
|
|
32
|
+
extensions: [".go"],
|
|
33
|
+
functionPatterns: [
|
|
34
|
+
/^func\s+(?:\([^)]+\)\s+)?(\w+)\s*\(/,
|
|
35
|
+
],
|
|
36
|
+
lineCommentPrefix: "//",
|
|
37
|
+
blockCommentStart: "/*",
|
|
38
|
+
blockCommentEnd: "*/",
|
|
39
|
+
endStyle: "braces",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "Rust",
|
|
43
|
+
extensions: [".rs"],
|
|
44
|
+
functionPatterns: [
|
|
45
|
+
/^\s*(?:pub(?:\s*\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(?:extern\s+(?:"[^"]*"\s+)?)?fn\s+(\w+)/,
|
|
46
|
+
],
|
|
47
|
+
lineCommentPrefix: "//",
|
|
48
|
+
blockCommentStart: "/*",
|
|
49
|
+
blockCommentEnd: "*/",
|
|
50
|
+
endStyle: "braces",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "C#",
|
|
54
|
+
extensions: [".cs"],
|
|
55
|
+
functionPatterns: [
|
|
56
|
+
// Require at least one access/modifier keyword so we don't match
|
|
57
|
+
// arbitrary `name(args)` calls. Return type is optional to also
|
|
58
|
+
// match constructors.
|
|
59
|
+
/^\s*(?:(?:public|private|protected|internal|static|async|override|virtual|sealed|abstract|new|partial)\s+)+(?:[\w<>\[\],?\s]+?\s+)?(\w+)\s*\([^)]*\)\s*(?:\{|where|:|=>|$)/,
|
|
60
|
+
],
|
|
61
|
+
lineCommentPrefix: "//",
|
|
62
|
+
blockCommentStart: "/*",
|
|
63
|
+
blockCommentEnd: "*/",
|
|
64
|
+
endStyle: "braces",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "Java",
|
|
68
|
+
extensions: [".java"],
|
|
69
|
+
functionPatterns: [
|
|
70
|
+
/^\s*(?:(?:public|private|protected|static|final|abstract|synchronized|native)\s+)+(?:[\w<>\[\],?\s]+?\s+)?(\w+)\s*\([^)]*\)\s*(?:\{|throws|$)/,
|
|
71
|
+
],
|
|
72
|
+
lineCommentPrefix: "//",
|
|
73
|
+
blockCommentStart: "/*",
|
|
74
|
+
blockCommentEnd: "*/",
|
|
75
|
+
endStyle: "braces",
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
function pickLanguage(file) {
|
|
79
|
+
const lower = file.toLowerCase();
|
|
80
|
+
for (const lang of LANGUAGES) {
|
|
81
|
+
if (lang.extensions.some((ext) => lower.endsWith(ext))) {
|
|
82
|
+
return lang;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
// Scan for a preceding comment within `lookback` non-blank lines above
|
|
88
|
+
// `startLine` (exclusive). Returns true if a comment is found.
|
|
89
|
+
function hasPrecedingComment(lines, startLine, lang, lookback) {
|
|
90
|
+
let checked = 0;
|
|
91
|
+
for (let i = startLine - 1; i >= 0 && checked < lookback; i -= 1) {
|
|
92
|
+
const trimmed = lines[i].trim();
|
|
93
|
+
if (trimmed === "")
|
|
94
|
+
continue;
|
|
95
|
+
checked += 1;
|
|
96
|
+
if (trimmed.startsWith(lang.lineCommentPrefix))
|
|
97
|
+
return true;
|
|
98
|
+
if (lang.blockCommentStart) {
|
|
99
|
+
if (trimmed.endsWith(lang.blockCommentEnd))
|
|
100
|
+
return true;
|
|
101
|
+
if (trimmed.startsWith(lang.blockCommentStart))
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
// First non-blank non-comment line above → no leading comment.
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
function isDocstringLine(line) {
|
|
110
|
+
const t = line.trim();
|
|
111
|
+
return t.startsWith('"""') || t.startsWith("'''") || t.startsWith('"') || t.startsWith("'");
|
|
112
|
+
}
|
|
113
|
+
// Walk forward from `startLine` (the function declaration line) and return
|
|
114
|
+
// the (exclusive) end-of-function line. Handles both brace and indent
|
|
115
|
+
// styles. Naive — comments/strings may contain braces and skew the
|
|
116
|
+
// counter; acceptable tradeoff without per-language AST parsing.
|
|
117
|
+
function findFunctionEnd(lines, startLine, lang, indentMatch) {
|
|
118
|
+
if (lang.endStyle === "indent") {
|
|
119
|
+
const baseIndent = (indentMatch ?? "").length;
|
|
120
|
+
for (let i = startLine + 1; i < lines.length; i += 1) {
|
|
121
|
+
const line = lines[i];
|
|
122
|
+
if (line.trim() === "")
|
|
123
|
+
continue;
|
|
124
|
+
const indent = line.length - line.trimStart().length;
|
|
125
|
+
if (indent <= baseIndent)
|
|
126
|
+
return i;
|
|
127
|
+
}
|
|
128
|
+
return lines.length;
|
|
129
|
+
}
|
|
130
|
+
// Braces
|
|
131
|
+
let depth = 0;
|
|
132
|
+
let seenOpen = false;
|
|
133
|
+
for (let i = startLine; i < lines.length; i += 1) {
|
|
134
|
+
const line = lines[i];
|
|
135
|
+
for (let j = 0; j < line.length; j += 1) {
|
|
136
|
+
const c = line[j];
|
|
137
|
+
if (c === "{") {
|
|
138
|
+
depth += 1;
|
|
139
|
+
seenOpen = true;
|
|
140
|
+
}
|
|
141
|
+
else if (c === "}") {
|
|
142
|
+
depth -= 1;
|
|
143
|
+
if (seenOpen && depth === 0)
|
|
144
|
+
return i + 1;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return lines.length;
|
|
149
|
+
}
|
|
150
|
+
function scanFile(content, file, lang, minLines) {
|
|
151
|
+
const lines = content.split("\n");
|
|
152
|
+
const hits = [];
|
|
153
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
154
|
+
const line = lines[i];
|
|
155
|
+
for (const pattern of lang.functionPatterns) {
|
|
156
|
+
const m = line.match(pattern);
|
|
157
|
+
if (!m)
|
|
158
|
+
continue;
|
|
159
|
+
// For Python, capture group 1 is indent, 2 is name; for others, 1 is name.
|
|
160
|
+
const name = lang.endStyle === "indent" ? m[2] ?? "<anonymous>" : m[1] ?? "<anonymous>";
|
|
161
|
+
const indent = lang.endStyle === "indent" ? m[1] ?? "" : undefined;
|
|
162
|
+
const endLine = findFunctionEnd(lines, i, lang, indent);
|
|
163
|
+
const funcLineCount = endLine - i;
|
|
164
|
+
if (funcLineCount < minLines)
|
|
165
|
+
break;
|
|
166
|
+
if (hasPrecedingComment(lines, i, lang, 3))
|
|
167
|
+
break;
|
|
168
|
+
if (lang.allowsDocstring && lines[i + 1] && isDocstringLine(lines[i + 1]))
|
|
169
|
+
break;
|
|
170
|
+
hits.push({ file, line: i + 1, name, lineCount: funcLineCount });
|
|
171
|
+
break; // one match per line is enough
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return hits;
|
|
175
|
+
}
|
|
176
|
+
registerGenericEvaluator({
|
|
177
|
+
type: "code_comments",
|
|
178
|
+
async check(ctx, config) {
|
|
179
|
+
const files = ctx.changedFiles ?? [];
|
|
180
|
+
if (files.length === 0) {
|
|
181
|
+
return { pass: true, severity: "info", message: "No changed files to scan" };
|
|
182
|
+
}
|
|
183
|
+
const minLines = typeof config.min_lines === "number" && config.min_lines > 0 ? config.min_lines : 15;
|
|
184
|
+
const severity = config.severity === "error" || config.severity === "warning" || config.severity === "info"
|
|
185
|
+
? config.severity
|
|
186
|
+
: "warning";
|
|
187
|
+
const allowlist = Array.isArray(config.allowlist_paths)
|
|
188
|
+
? config.allowlist_paths.filter((p) => typeof p === "string")
|
|
189
|
+
: ["tests/", "test/", "__tests__/", "fixtures/", "docs/"];
|
|
190
|
+
const maxBytes = typeof config.max_scan_bytes === "number" ? config.max_scan_bytes : 2_000_000;
|
|
191
|
+
// Optional language filter (by name, case-insensitive). Absent = all.
|
|
192
|
+
const wantedLanguages = Array.isArray(config.languages)
|
|
193
|
+
? new Set(config.languages.filter((l) => typeof l === "string").map((l) => l.toLowerCase()))
|
|
194
|
+
: null;
|
|
195
|
+
const allHits = [];
|
|
196
|
+
let scanned = 0;
|
|
197
|
+
for (const file of files) {
|
|
198
|
+
if (allowlist.some((p) => file.includes(p)))
|
|
199
|
+
continue;
|
|
200
|
+
const lang = pickLanguage(file);
|
|
201
|
+
if (!lang)
|
|
202
|
+
continue;
|
|
203
|
+
if (wantedLanguages && !wantedLanguages.has(lang.name.toLowerCase()))
|
|
204
|
+
continue;
|
|
205
|
+
const abs = join(ctx.projectRoot, file);
|
|
206
|
+
try {
|
|
207
|
+
const stat = statSync(abs);
|
|
208
|
+
if (stat.size > maxBytes)
|
|
209
|
+
continue;
|
|
210
|
+
const content = readFileSync(abs, "utf8");
|
|
211
|
+
scanned += 1;
|
|
212
|
+
allHits.push(...scanFile(content, file, lang, minLines));
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// unreadable — skip
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (allHits.length === 0) {
|
|
219
|
+
return {
|
|
220
|
+
pass: true,
|
|
221
|
+
severity: "info",
|
|
222
|
+
message: `No undocumented functions (${minLines}+ lines) in ${scanned} changed file${scanned === 1 ? "" : "s"}`,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
const detail = allHits
|
|
226
|
+
.slice(0, 30)
|
|
227
|
+
.map((h) => `${h.file}:${h.line} — ${h.name} (${h.lineCount} lines)`)
|
|
228
|
+
.join("\n") + (allHits.length > 30 ? `\n... and ${allHits.length - 30} more` : "");
|
|
229
|
+
return {
|
|
230
|
+
pass: false,
|
|
231
|
+
severity,
|
|
232
|
+
message: `${allHits.length} function${allHits.length === 1 ? "" : "s"} of ${minLines}+ lines without preceding comment`,
|
|
233
|
+
detail,
|
|
234
|
+
};
|
|
235
|
+
},
|
|
236
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { statSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { registerGenericEvaluator, } from "../engine.js";
|
|
4
|
+
const BINARY_SNIFF_BYTES = 512;
|
|
5
|
+
const DEFAULT_MAX_BYTES = 2_000_000;
|
|
6
|
+
const SEVERITIES = new Set(["error", "warning", "info"]);
|
|
7
|
+
function parseConfig(raw) {
|
|
8
|
+
const pattern = raw.pattern;
|
|
9
|
+
if (typeof pattern !== "string" || pattern.length === 0) {
|
|
10
|
+
return { error: "config.pattern must be a non-empty string" };
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
new RegExp(pattern, typeof raw.flags === "string" ? raw.flags : undefined);
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
return { error: `config.pattern is not a valid regex: ${err instanceof Error ? err.message : String(err)}` };
|
|
17
|
+
}
|
|
18
|
+
const severity = raw.severity;
|
|
19
|
+
if (severity !== undefined && (typeof severity !== "string" || !SEVERITIES.has(severity))) {
|
|
20
|
+
return { error: 'config.severity must be one of "error", "warning", "info"' };
|
|
21
|
+
}
|
|
22
|
+
if (raw.file_pattern !== undefined && typeof raw.file_pattern !== "string") {
|
|
23
|
+
return { error: "config.file_pattern must be a string" };
|
|
24
|
+
}
|
|
25
|
+
if (raw.file_pattern) {
|
|
26
|
+
try {
|
|
27
|
+
new RegExp(raw.file_pattern);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
return { error: `config.file_pattern is not a valid regex: ${err instanceof Error ? err.message : String(err)}` };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
pattern,
|
|
35
|
+
flags: typeof raw.flags === "string" ? raw.flags : undefined,
|
|
36
|
+
file_pattern: typeof raw.file_pattern === "string" ? raw.file_pattern : undefined,
|
|
37
|
+
severity: severity,
|
|
38
|
+
message: typeof raw.message === "string" ? raw.message : undefined,
|
|
39
|
+
max_matches_per_file: typeof raw.max_matches_per_file === "number" ? raw.max_matches_per_file : undefined,
|
|
40
|
+
max_scan_bytes: typeof raw.max_scan_bytes === "number" ? raw.max_scan_bytes : undefined,
|
|
41
|
+
allowlist_paths: Array.isArray(raw.allowlist_paths)
|
|
42
|
+
? raw.allowlist_paths.filter((p) => typeof p === "string")
|
|
43
|
+
: undefined,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
registerGenericEvaluator({
|
|
47
|
+
type: "regex",
|
|
48
|
+
async check(ctx, rawConfig) {
|
|
49
|
+
const parsed = parseConfig(rawConfig);
|
|
50
|
+
if ("error" in parsed) {
|
|
51
|
+
return { pass: false, severity: "error", message: `Invalid regex config: ${parsed.error}` };
|
|
52
|
+
}
|
|
53
|
+
const files = ctx.changedFiles ?? [];
|
|
54
|
+
if (files.length === 0) {
|
|
55
|
+
return { pass: true, severity: "info", message: "No changed files to scan" };
|
|
56
|
+
}
|
|
57
|
+
const severity = parsed.severity ?? "warning";
|
|
58
|
+
const allowlist = parsed.allowlist_paths ?? ["tests/", "test/", "__tests__/", "fixtures/", "docs/"];
|
|
59
|
+
const maxBytes = parsed.max_scan_bytes ?? DEFAULT_MAX_BYTES;
|
|
60
|
+
const maxMatches = parsed.max_matches_per_file ?? 20;
|
|
61
|
+
// A regex used purely to filter file paths should be anchored by the
|
|
62
|
+
// caller if needed; we compile as-is to give them full control.
|
|
63
|
+
const fileRe = parsed.file_pattern ? new RegExp(parsed.file_pattern) : null;
|
|
64
|
+
const contentRe = new RegExp(parsed.pattern, parsed.flags);
|
|
65
|
+
const hits = [];
|
|
66
|
+
let scanned = 0;
|
|
67
|
+
for (const file of files) {
|
|
68
|
+
if (allowlist.some((p) => file.includes(p)))
|
|
69
|
+
continue;
|
|
70
|
+
if (fileRe && !fileRe.test(file))
|
|
71
|
+
continue;
|
|
72
|
+
const abs = join(ctx.projectRoot, file);
|
|
73
|
+
try {
|
|
74
|
+
const stat = statSync(abs);
|
|
75
|
+
if (stat.size > maxBytes)
|
|
76
|
+
continue;
|
|
77
|
+
const buf = readFileSync(abs);
|
|
78
|
+
const sniff = buf.subarray(0, Math.min(buf.length, BINARY_SNIFF_BYTES));
|
|
79
|
+
if (sniff.includes(0))
|
|
80
|
+
continue;
|
|
81
|
+
const content = buf.toString("utf8");
|
|
82
|
+
scanned += 1;
|
|
83
|
+
const lines = content.split("\n");
|
|
84
|
+
let fileMatches = 0;
|
|
85
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
86
|
+
// Compile a per-line test with the `g` flag stripped so we don't
|
|
87
|
+
// hop across repeated state; callers supply single-line patterns.
|
|
88
|
+
const lineRe = new RegExp(contentRe.source, (contentRe.flags || "").replace(/g/g, ""));
|
|
89
|
+
if (lineRe.test(lines[i])) {
|
|
90
|
+
hits.push(`${file}:${i + 1}`);
|
|
91
|
+
fileMatches += 1;
|
|
92
|
+
if (fileMatches >= maxMatches)
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Unreadable file — skip
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (hits.length === 0) {
|
|
102
|
+
return {
|
|
103
|
+
pass: true,
|
|
104
|
+
severity: "info",
|
|
105
|
+
message: `No regex matches in ${scanned} changed file${scanned === 1 ? "" : "s"}`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const messageStem = parsed.message ?? "Pattern match";
|
|
109
|
+
return {
|
|
110
|
+
pass: false,
|
|
111
|
+
severity,
|
|
112
|
+
message: `${messageStem}: ${hits.length} match${hits.length === 1 ? "" : "es"} in ${scanned} file${scanned === 1 ? "" : "s"}`,
|
|
113
|
+
detail: hits.slice(0, 30).join("\n") + (hits.length > 30 ? `\n... and ${hits.length - 30} more` : ""),
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import * as yaml from "js-yaml";
|
|
4
|
+
import { runStateSchema, stageArtifactFrontmatterSchema, } from "./schemas.js";
|
|
5
|
+
/**
|
|
6
|
+
* Filesystem layout for one workflow run:
|
|
7
|
+
*
|
|
8
|
+
* <cwd>/.agents/<task-id>/
|
|
9
|
+
* plan.md
|
|
10
|
+
* review.md
|
|
11
|
+
* ...
|
|
12
|
+
* state.json
|
|
13
|
+
*
|
|
14
|
+
* All paths in this module are relative to the project's <cwd>. The caller
|
|
15
|
+
* is responsible for choosing cwd; we never assume process.cwd() here so
|
|
16
|
+
* tests can target a tmp directory.
|
|
17
|
+
*/
|
|
18
|
+
export const AGENTS_DIR = ".agents";
|
|
19
|
+
export const STATE_FILENAME = "state.json";
|
|
20
|
+
export function runDir(cwd, taskId) {
|
|
21
|
+
return join(cwd, AGENTS_DIR, taskId);
|
|
22
|
+
}
|
|
23
|
+
export function stateFilePath(cwd, taskId) {
|
|
24
|
+
return join(runDir(cwd, taskId), STATE_FILENAME);
|
|
25
|
+
}
|
|
26
|
+
export function artifactPath(cwd, taskId, artifactName) {
|
|
27
|
+
return join(runDir(cwd, taskId), artifactName);
|
|
28
|
+
}
|
|
29
|
+
const FRONTMATTER_OPEN = /^---\s*\r?\n/;
|
|
30
|
+
const FRONTMATTER_CLOSE = /\r?\n---\s*\r?\n/;
|
|
31
|
+
export function parseStageArtifact(text) {
|
|
32
|
+
if (!FRONTMATTER_OPEN.test(text)) {
|
|
33
|
+
throw new Error("Stage artifact is missing YAML frontmatter (--- ... ---)");
|
|
34
|
+
}
|
|
35
|
+
const afterOpen = text.replace(FRONTMATTER_OPEN, "");
|
|
36
|
+
const closeMatch = afterOpen.match(FRONTMATTER_CLOSE);
|
|
37
|
+
if (!closeMatch || closeMatch.index === undefined) {
|
|
38
|
+
throw new Error("Stage artifact frontmatter is not terminated (--- ... ---)");
|
|
39
|
+
}
|
|
40
|
+
const yamlText = afterOpen.slice(0, closeMatch.index);
|
|
41
|
+
const body = afterOpen
|
|
42
|
+
.slice(closeMatch.index + closeMatch[0].length)
|
|
43
|
+
.replace(/^\s+/, "")
|
|
44
|
+
.replace(/\s+$/, "");
|
|
45
|
+
let raw;
|
|
46
|
+
try {
|
|
47
|
+
raw = yaml.load(yamlText);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
throw new Error(`Failed to parse stage artifact frontmatter as YAML: ${err instanceof Error ? err.message : String(err)}`);
|
|
51
|
+
}
|
|
52
|
+
const parsed = stageArtifactFrontmatterSchema.safeParse(raw);
|
|
53
|
+
if (!parsed.success) {
|
|
54
|
+
throw new Error(`Stage artifact frontmatter does not match schema: ${parsed.error.message}`);
|
|
55
|
+
}
|
|
56
|
+
return { frontmatter: parsed.data, body };
|
|
57
|
+
}
|
|
58
|
+
export function readStageArtifact(cwd, taskId, artifactName) {
|
|
59
|
+
const path = artifactPath(cwd, taskId, artifactName);
|
|
60
|
+
const text = readFileSync(path, "utf8");
|
|
61
|
+
return parseStageArtifact(text);
|
|
62
|
+
}
|
|
63
|
+
export function renderStageArtifact(frontmatter, body) {
|
|
64
|
+
const yamlText = yaml.dump(frontmatter, {
|
|
65
|
+
lineWidth: 100,
|
|
66
|
+
noRefs: true,
|
|
67
|
+
sortKeys: false,
|
|
68
|
+
});
|
|
69
|
+
const trimmedBody = body.trim();
|
|
70
|
+
return `---\n${yamlText}---\n\n${trimmedBody}\n`;
|
|
71
|
+
}
|
|
72
|
+
export function writeStageArtifact(cwd, taskId, artifactName, frontmatter, body) {
|
|
73
|
+
const dir = runDir(cwd, taskId);
|
|
74
|
+
mkdirSync(dir, { recursive: true });
|
|
75
|
+
const path = artifactPath(cwd, taskId, artifactName);
|
|
76
|
+
writeFileSync(path, renderStageArtifact(frontmatter, body), "utf8");
|
|
77
|
+
return path;
|
|
78
|
+
}
|
|
79
|
+
export function readRunState(cwd, taskId) {
|
|
80
|
+
const path = stateFilePath(cwd, taskId);
|
|
81
|
+
if (!existsSync(path))
|
|
82
|
+
return null;
|
|
83
|
+
let raw;
|
|
84
|
+
try {
|
|
85
|
+
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
throw new Error(`Failed to parse run state at ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
89
|
+
}
|
|
90
|
+
const parsed = runStateSchema.safeParse(raw);
|
|
91
|
+
if (!parsed.success) {
|
|
92
|
+
throw new Error(`Run state at ${path} does not match schema: ${parsed.error.message}`);
|
|
93
|
+
}
|
|
94
|
+
return parsed.data;
|
|
95
|
+
}
|
|
96
|
+
export function writeRunState(cwd, state) {
|
|
97
|
+
const validated = runStateSchema.parse(state);
|
|
98
|
+
const dir = runDir(cwd, validated.task_id);
|
|
99
|
+
mkdirSync(dir, { recursive: true });
|
|
100
|
+
const path = stateFilePath(cwd, validated.task_id);
|
|
101
|
+
writeFileSync(path, JSON.stringify(validated, null, 2) + "\n", "utf8");
|
|
102
|
+
return path;
|
|
103
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Capability registry for the harness. A capability is a least-privilege
|
|
4
|
+
* profile referenced by a workflow stage's `capability` field. The
|
|
5
|
+
* pre-tool-use hook reads the active stage's capability and uses it to
|
|
6
|
+
* gate the agent's tool calls.
|
|
7
|
+
*
|
|
8
|
+
* The capability defines:
|
|
9
|
+
* - read_globs paths the agent may read (empty = no restriction)
|
|
10
|
+
* - write_globs paths the agent may modify (empty = read-only)
|
|
11
|
+
* - tools_allowed which tool names the agent may call (empty = all)
|
|
12
|
+
*
|
|
13
|
+
* Glob patterns use minimatch syntax. The harness ships a default set
|
|
14
|
+
* keyed by the names referenced in default-workflows.ts; orgs can ship
|
|
15
|
+
* additional capabilities later via cortex-web sync.
|
|
16
|
+
*/
|
|
17
|
+
const slugSchema = z
|
|
18
|
+
.string()
|
|
19
|
+
.min(1)
|
|
20
|
+
.max(80)
|
|
21
|
+
.regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$/);
|
|
22
|
+
export const capabilityDefinitionSchema = z.object({
|
|
23
|
+
name: slugSchema,
|
|
24
|
+
description: z.string().min(1).max(500),
|
|
25
|
+
read_globs: z.array(z.string().min(1)).default([]),
|
|
26
|
+
write_globs: z.array(z.string().min(1)).default([]),
|
|
27
|
+
tools_allowed: z.array(z.string().min(1)).default([]),
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* Default capability profiles that ship with Cortex. Names match the
|
|
31
|
+
* `capability` fields referenced by SECURE_BUILD_WORKFLOW.
|
|
32
|
+
*
|
|
33
|
+
* tools_allowed is intentionally empty for most profiles to mean
|
|
34
|
+
* "no per-tool restriction beyond what the file globs already imply".
|
|
35
|
+
* The hook layer checks file paths first, tool name second.
|
|
36
|
+
*/
|
|
37
|
+
export const DEFAULT_CAPABILITIES = {
|
|
38
|
+
planner: {
|
|
39
|
+
name: "planner",
|
|
40
|
+
description: "Read-only profile for stages that produce planning artifacts. " +
|
|
41
|
+
"Can read the whole repo and call context tools, cannot modify any files.",
|
|
42
|
+
read_globs: ["**"],
|
|
43
|
+
write_globs: [],
|
|
44
|
+
tools_allowed: [],
|
|
45
|
+
},
|
|
46
|
+
reviewer: {
|
|
47
|
+
name: "reviewer",
|
|
48
|
+
description: "Read-only profile for review stages. Same access as planner; the " +
|
|
49
|
+
"review artifact itself is written by the harness, not by an Edit tool.",
|
|
50
|
+
read_globs: ["**"],
|
|
51
|
+
write_globs: [],
|
|
52
|
+
tools_allowed: [],
|
|
53
|
+
},
|
|
54
|
+
builder: {
|
|
55
|
+
name: "builder",
|
|
56
|
+
description: "Build profile. May edit source and test files but not config, " +
|
|
57
|
+
"lockfiles, CI workflows, or anything outside the obvious app surface.",
|
|
58
|
+
read_globs: ["**"],
|
|
59
|
+
write_globs: ["src/**", "tests/**", "test/**", "lib/**", "app/**", "components/**"],
|
|
60
|
+
tools_allowed: [],
|
|
61
|
+
},
|
|
62
|
+
tester: {
|
|
63
|
+
name: "tester",
|
|
64
|
+
description: "Mutation/test profile. Read-only on production code, may edit only " +
|
|
65
|
+
"test files. Used by mutation-testing or coverage-improvement stages.",
|
|
66
|
+
read_globs: ["**"],
|
|
67
|
+
write_globs: ["tests/**", "test/**", "**/*.test.ts", "**/*.test.tsx", "**/*.test.mjs", "**/*.spec.ts"],
|
|
68
|
+
tools_allowed: [],
|
|
69
|
+
},
|
|
70
|
+
"security-reviewer": {
|
|
71
|
+
name: "security-reviewer",
|
|
72
|
+
description: "Security review profile. Read-only across the repo. Produces a " +
|
|
73
|
+
"report artifact; no source modifications allowed.",
|
|
74
|
+
read_globs: ["**"],
|
|
75
|
+
write_globs: [],
|
|
76
|
+
tools_allowed: [],
|
|
77
|
+
},
|
|
78
|
+
human: {
|
|
79
|
+
name: "human",
|
|
80
|
+
description: "Sentinel capability for human approval stages. The harness does not " +
|
|
81
|
+
"invoke an agent for this stage; the human writes the artifact directly. " +
|
|
82
|
+
"If a tool call somehow reaches the hook under this capability, it is " +
|
|
83
|
+
"blocked because no automation should be running.",
|
|
84
|
+
read_globs: [],
|
|
85
|
+
write_globs: [],
|
|
86
|
+
tools_allowed: [],
|
|
87
|
+
},
|
|
88
|
+
};
|