@jeger-ai/opengantry 3.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +1 -0
- package/README.md +10 -1
- package/dist/cli/commands/arch.d.ts +1 -1
- package/dist/cli/commands/arch.d.ts.map +1 -1
- package/dist/cli/commands/arch.js +4 -4
- package/dist/cli/commands/arch.js.map +1 -1
- package/dist/cli/lib/{architecture-drift-doctor.d.ts → arch/cage/architecture-drift-doctor.d.ts} +1 -1
- package/dist/cli/lib/arch/cage/architecture-drift-doctor.d.ts.map +1 -0
- package/dist/cli/lib/{architecture-drift-doctor.js → arch/cage/architecture-drift-doctor.js} +2 -2
- package/dist/cli/lib/arch/cage/architecture-drift-doctor.js.map +1 -0
- package/dist/cli/lib/{target-architecture-doctor.d.ts → arch/cage/target-architecture-doctor.d.ts} +1 -1
- package/dist/cli/lib/arch/cage/target-architecture-doctor.d.ts.map +1 -0
- package/dist/cli/lib/arch/cage/target-architecture-doctor.js.map +1 -0
- package/dist/cli/lib/arch/cage/target-architecture.d.ts.map +1 -0
- package/dist/cli/lib/{target-architecture.js → arch/cage/target-architecture.js} +19 -21
- package/dist/cli/lib/arch/cage/target-architecture.js.map +1 -0
- package/dist/cli/lib/arch/external/architecture-credential.d.ts.map +1 -0
- package/dist/cli/lib/{architecture-credential.js → arch/external/architecture-credential.js} +2 -2
- package/dist/cli/lib/arch/external/architecture-credential.js.map +1 -0
- package/dist/cli/lib/arch/external/architecture-fetch.d.ts.map +1 -0
- package/dist/cli/lib/{architecture-fetch.js → arch/external/architecture-fetch.js} +1 -1
- package/dist/cli/lib/arch/external/architecture-fetch.js.map +1 -0
- package/dist/cli/lib/{architecture-pointer.d.ts → arch/external/architecture-pointer.d.ts} +1 -1
- package/dist/cli/lib/arch/external/architecture-pointer.d.ts.map +1 -0
- package/dist/cli/lib/{architecture-pointer.js → arch/external/architecture-pointer.js} +2 -2
- package/dist/cli/lib/arch/external/architecture-pointer.js.map +1 -0
- package/dist/cli/lib/blueprint-engine.js +1 -1
- package/dist/cli/lib/blueprint-engine.js.map +1 -1
- package/dist/cli/lib/break-glass.d.ts +1 -1
- package/dist/cli/lib/break-glass.d.ts.map +1 -1
- package/dist/cli/lib/constants.d.ts +1 -0
- package/dist/cli/lib/constants.d.ts.map +1 -1
- package/dist/cli/lib/constants.js +1 -0
- package/dist/cli/lib/constants.js.map +1 -1
- package/dist/cli/lib/context-feed-remediation.d.ts +2 -2
- package/dist/cli/lib/context-feed-remediation.d.ts.map +1 -1
- package/dist/cli/lib/discovery-proposal.js +1 -1
- package/dist/cli/lib/discovery-proposal.js.map +1 -1
- package/dist/cli/lib/discovery-scanner.d.ts +3 -39
- package/dist/cli/lib/discovery-scanner.d.ts.map +1 -1
- package/dist/cli/lib/discovery-scanner.js +2 -1
- package/dist/cli/lib/discovery-scanner.js.map +1 -1
- package/dist/cli/lib/discovery-types.d.ts +40 -0
- package/dist/cli/lib/discovery-types.d.ts.map +1 -0
- package/dist/cli/lib/discovery-types.js +3 -0
- package/dist/cli/lib/discovery-types.js.map +1 -0
- package/dist/cli/lib/doctor-core.js +3 -3
- package/dist/cli/lib/doctor-core.js.map +1 -1
- package/dist/cli/lib/domains/domain-adapter.d.ts +4 -2
- package/dist/cli/lib/domains/domain-adapter.d.ts.map +1 -1
- package/dist/cli/lib/domains/domain-adapter.js.map +1 -1
- package/dist/cli/lib/domains/domain-code.d.ts.map +1 -1
- package/dist/cli/lib/domains/domain-code.js +1 -2
- package/dist/cli/lib/domains/domain-code.js.map +1 -1
- package/dist/cli/lib/domains/domain-content.d.ts.map +1 -1
- package/dist/cli/lib/domains/domain-content.js +1 -2
- package/dist/cli/lib/domains/domain-content.js.map +1 -1
- package/dist/cli/lib/domains/index.d.ts +4 -4
- package/dist/cli/lib/domains/index.d.ts.map +1 -1
- package/dist/cli/lib/domains/index.js +15 -4
- package/dist/cli/lib/domains/index.js.map +1 -1
- package/dist/cli/lib/init-compose.d.ts +1 -1
- package/dist/cli/lib/init-compose.d.ts.map +1 -1
- package/dist/cli/lib/kpi-engine.d.ts +2 -2
- package/dist/cli/lib/kpi-engine.d.ts.map +1 -1
- package/dist/cli/lib/manifest.d.ts +1 -1
- package/dist/cli/lib/manifest.d.ts.map +1 -1
- package/dist/cli/lib/manifest.js +1 -1
- package/dist/cli/lib/manifest.js.map +1 -1
- package/dist/cli/lib/mcp-check-signature.d.ts +6 -0
- package/dist/cli/lib/mcp-check-signature.d.ts.map +1 -0
- package/dist/cli/lib/mcp-check-signature.js +40 -0
- package/dist/cli/lib/mcp-check-signature.js.map +1 -0
- package/dist/cli/lib/mcp-draft-legislation.d.ts +6 -0
- package/dist/cli/lib/mcp-draft-legislation.d.ts.map +1 -0
- package/dist/cli/lib/mcp-draft-legislation.js +63 -0
- package/dist/cli/lib/mcp-draft-legislation.js.map +1 -0
- package/dist/cli/lib/mcp-execute-legislation.d.ts +6 -0
- package/dist/cli/lib/mcp-execute-legislation.d.ts.map +1 -0
- package/dist/cli/lib/mcp-execute-legislation.js +53 -0
- package/dist/cli/lib/mcp-execute-legislation.js.map +1 -0
- package/dist/cli/lib/mcp-governance-shared.d.ts +119 -0
- package/dist/cli/lib/mcp-governance-shared.d.ts.map +1 -0
- package/dist/cli/lib/mcp-governance-shared.js +21 -0
- package/dist/cli/lib/mcp-governance-shared.js.map +1 -0
- package/dist/cli/lib/mcp-governance.d.ts +9 -121
- package/dist/cli/lib/mcp-governance.d.ts.map +1 -1
- package/dist/cli/lib/mcp-governance.js +8 -259
- package/dist/cli/lib/mcp-governance.js.map +1 -1
- package/dist/cli/lib/mcp-last-error.d.ts +3 -0
- package/dist/cli/lib/mcp-last-error.d.ts.map +1 -0
- package/dist/cli/lib/mcp-last-error.js +18 -0
- package/dist/cli/lib/mcp-last-error.js.map +1 -0
- package/dist/cli/lib/mcp-pin-mission.d.ts +6 -0
- package/dist/cli/lib/mcp-pin-mission.d.ts.map +1 -0
- package/dist/cli/lib/mcp-pin-mission.js +21 -0
- package/dist/cli/lib/mcp-pin-mission.js.map +1 -0
- package/dist/cli/lib/mcp-resolve-mission.d.ts +3 -0
- package/dist/cli/lib/mcp-resolve-mission.d.ts.map +1 -0
- package/dist/cli/lib/mcp-resolve-mission.js +18 -0
- package/dist/cli/lib/mcp-resolve-mission.js.map +1 -0
- package/dist/cli/lib/mcp-start-orchestration.d.ts +3 -0
- package/dist/cli/lib/mcp-start-orchestration.d.ts.map +1 -0
- package/dist/cli/lib/mcp-start-orchestration.js +53 -0
- package/dist/cli/lib/mcp-start-orchestration.js.map +1 -0
- package/dist/cli/lib/mcp-tools-register.d.ts.map +1 -1
- package/dist/cli/lib/mcp-tools-register.js +7 -1
- package/dist/cli/lib/mcp-tools-register.js.map +1 -1
- package/dist/cli/lib/path-glob.d.ts +8 -0
- package/dist/cli/lib/path-glob.d.ts.map +1 -0
- package/dist/cli/lib/path-glob.js +40 -0
- package/dist/cli/lib/path-glob.js.map +1 -0
- package/dist/cli/lib/perimeter.d.ts +1 -2
- package/dist/cli/lib/perimeter.d.ts.map +1 -1
- package/dist/cli/lib/perimeter.js +2 -10
- package/dist/cli/lib/perimeter.js.map +1 -1
- package/dist/cli/lib/surgeons/registry.d.ts +1 -1
- package/dist/cli/lib/surgeons/registry.d.ts.map +1 -1
- package/dist/cli/lib/trace-evidence.d.ts +21 -0
- package/dist/cli/lib/trace-evidence.d.ts.map +1 -0
- package/dist/cli/lib/trace-evidence.js +113 -0
- package/dist/cli/lib/trace-evidence.js.map +1 -0
- package/dist/cli/lib/trace-quote.d.ts +37 -0
- package/dist/cli/lib/trace-quote.d.ts.map +1 -0
- package/dist/cli/lib/trace-quote.js +177 -0
- package/dist/cli/lib/trace-quote.js.map +1 -0
- package/dist/cli/lib/trace-status.d.ts +7 -0
- package/dist/cli/lib/trace-status.d.ts.map +1 -0
- package/dist/cli/lib/trace-status.js +20 -0
- package/dist/cli/lib/trace-status.js.map +1 -0
- package/dist/cli/lib/trace.d.ts +3 -60
- package/dist/cli/lib/trace.d.ts.map +1 -1
- package/dist/cli/lib/trace.js +3 -304
- package/dist/cli/lib/trace.js.map +1 -1
- package/dist/cli/lib/upgrade-plan-catalog.d.ts +13 -0
- package/dist/cli/lib/upgrade-plan-catalog.d.ts.map +1 -0
- package/dist/cli/lib/upgrade-plan-catalog.js +85 -0
- package/dist/cli/lib/upgrade-plan-catalog.js.map +1 -0
- package/dist/cli/lib/upgrade-plan-payload.d.ts +1 -1
- package/dist/cli/lib/upgrade-plan-payload.d.ts.map +1 -1
- package/dist/cli/lib/upgrade-plan-scaffold.d.ts +15 -0
- package/dist/cli/lib/upgrade-plan-scaffold.d.ts.map +1 -0
- package/dist/cli/lib/upgrade-plan-scaffold.js +109 -0
- package/dist/cli/lib/upgrade-plan-scaffold.js.map +1 -0
- package/dist/cli/lib/upgrade-plan-types.d.ts +43 -0
- package/dist/cli/lib/upgrade-plan-types.d.ts.map +1 -0
- package/dist/cli/lib/upgrade-plan-types.js +3 -0
- package/dist/cli/lib/upgrade-plan-types.js.map +1 -0
- package/dist/cli/lib/upgrade-plan.d.ts +4 -62
- package/dist/cli/lib/upgrade-plan.d.ts.map +1 -1
- package/dist/cli/lib/upgrade-plan.js +7 -181
- package/dist/cli/lib/upgrade-plan.js.map +1 -1
- package/dist/cli/lib/verify-defensive-phase.d.ts +1 -1
- package/dist/cli/lib/verify-defensive-phase.d.ts.map +1 -1
- package/dist/cli/lib/verify-engine.d.ts +2 -36
- package/dist/cli/lib/verify-engine.d.ts.map +1 -1
- package/dist/cli/lib/verify-engine.js.map +1 -1
- package/dist/cli/lib/verify-export.d.ts.map +1 -1
- package/dist/cli/lib/verify-export.js +3 -2
- package/dist/cli/lib/verify-export.js.map +1 -1
- package/dist/cli/lib/verify-failure-normalize.d.ts +2 -4
- package/dist/cli/lib/verify-failure-normalize.d.ts.map +1 -1
- package/dist/cli/lib/verify-failure-normalize.js +0 -50
- package/dist/cli/lib/verify-failure-normalize.js.map +1 -1
- package/dist/cli/lib/verify-hints.d.ts +1 -1
- package/dist/cli/lib/verify-hints.d.ts.map +1 -1
- package/dist/cli/lib/verify-options.d.ts +40 -0
- package/dist/cli/lib/verify-options.d.ts.map +1 -0
- package/dist/cli/lib/verify-options.js +13 -0
- package/dist/cli/lib/verify-options.js.map +1 -0
- package/dist/cli/lib/verify-payload.d.ts +6 -1
- package/dist/cli/lib/verify-payload.d.ts.map +1 -1
- package/dist/cli/lib/verify-payload.js +51 -1
- package/dist/cli/lib/verify-payload.js.map +1 -1
- package/dist/cli/lib/verify-presenters.d.ts +2 -1
- package/dist/cli/lib/verify-presenters.d.ts.map +1 -1
- package/dist/cli/lib/verify-presenters.js.map +1 -1
- package/dist/cli/lib/verify-run.d.ts +2 -2
- package/dist/cli/lib/verify-run.d.ts.map +1 -1
- package/dist/cli/lib/verify-run.js +33 -19
- package/dist/cli/lib/verify-run.js.map +1 -1
- package/dist/cli/lib/version.gen.d.ts +1 -1
- package/dist/cli/lib/version.gen.js +1 -1
- package/dist/cli/program-arch.js +1 -1
- package/dist/cli/program-arch.js.map +1 -1
- package/dist/cli/program-core.d.ts.map +1 -1
- package/dist/cli/program-core.js +2 -22
- package/dist/cli/program-core.js.map +1 -1
- package/dist/cli/program-init-options.d.ts +4 -0
- package/dist/cli/program-init-options.d.ts.map +1 -0
- package/dist/cli/program-init-options.js +23 -0
- package/dist/cli/program-init-options.js.map +1 -0
- package/dist/cli/program-workflow.d.ts.map +1 -1
- package/dist/cli/program-workflow.js +0 -1
- package/dist/cli/program-workflow.js.map +1 -1
- package/dist/cli/program.d.ts.map +1 -1
- package/dist/cli/program.js +2 -0
- package/dist/cli/program.js.map +1 -1
- package/package.json +2 -2
- package/templates/.gitagent/foreman/SUBSTRATE.version.json +1 -1
- package/templates/integrations/asset-catalog.json +3 -0
- package/templates/integrations/compatibility.json +1 -1
- package/templates/scripts/gxt-manifest-lib.mjs +12 -493
- package/templates/scripts/lib/glob-match.mjs +24 -0
- package/templates/scripts/lib/manifest-validate.mjs +126 -0
- package/templates/scripts/lib/trusted-automation.mjs +360 -0
- package/dist/cli/lib/architecture-credential.d.ts.map +0 -1
- package/dist/cli/lib/architecture-credential.js.map +0 -1
- package/dist/cli/lib/architecture-drift-doctor.d.ts.map +0 -1
- package/dist/cli/lib/architecture-drift-doctor.js.map +0 -1
- package/dist/cli/lib/architecture-fetch.d.ts.map +0 -1
- package/dist/cli/lib/architecture-fetch.js.map +0 -1
- package/dist/cli/lib/architecture-pointer.d.ts.map +0 -1
- package/dist/cli/lib/architecture-pointer.js.map +0 -1
- package/dist/cli/lib/target-architecture-doctor.d.ts.map +0 -1
- package/dist/cli/lib/target-architecture-doctor.js.map +0 -1
- package/dist/cli/lib/target-architecture.d.ts.map +0 -1
- package/dist/cli/lib/target-architecture.js.map +0 -1
- /package/dist/cli/lib/{target-architecture-doctor.js → arch/cage/target-architecture-doctor.js} +0 -0
- /package/dist/cli/lib/{target-architecture.d.ts → arch/cage/target-architecture.d.ts} +0 -0
- /package/dist/cli/lib/{architecture-credential.d.ts → arch/external/architecture-credential.d.ts} +0 -0
- /package/dist/cli/lib/{architecture-fetch.d.ts → arch/external/architecture-fetch.d.ts} +0 -0
|
@@ -5,503 +5,22 @@
|
|
|
5
5
|
* node scripts/gxt-manifest-lib.mjs prefixes [repoRoot]
|
|
6
6
|
* node scripts/gxt-manifest-lib.mjs validate-manifest [repoRoot]
|
|
7
7
|
* node scripts/gxt-manifest-lib.mjs validate-bypass-note (JSON on stdin)
|
|
8
|
+
* node scripts/gxt-manifest-lib.mjs match-glob <repoRoot> <pattern> <filePath>
|
|
8
9
|
* node scripts/gxt-manifest-lib.mjs eval-commit <repoRoot> <commitSha>
|
|
9
10
|
* node scripts/gxt-manifest-lib.mjs eval-range <repoRoot> <baseSha> <headSha>
|
|
10
11
|
*/
|
|
11
12
|
import fs from "node:fs";
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const HARD_MAX_NET_LOC = 5;
|
|
25
|
-
const ALLOWED_STRUCTURAL_CHANGES = new Set(["workflow_version_pin"]);
|
|
26
|
-
const CONFIG_REL = ".gitagent/config.json";
|
|
27
|
-
|
|
28
|
-
export function repoRootFromArg(arg) {
|
|
29
|
-
if (arg?.trim()) return path.resolve(arg.trim());
|
|
30
|
-
return process.cwd();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function git(repoRoot, args) {
|
|
34
|
-
return execFileSync("git", args, { cwd: repoRoot, encoding: "utf8" }).trimEnd();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function normalizePath(p) {
|
|
38
|
-
return p.replace(/\\/g, "/");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function resolveYamlParser(repoRoot) {
|
|
42
|
-
const candidates = [
|
|
43
|
-
path.join(repoRoot, "node_modules/yaml"),
|
|
44
|
-
path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "node_modules/yaml"),
|
|
45
|
-
];
|
|
46
|
-
for (const dir of candidates) {
|
|
47
|
-
const pkg = path.join(dir, "package.json");
|
|
48
|
-
if (fs.existsSync(pkg)) {
|
|
49
|
-
const req = createRequire(pkg);
|
|
50
|
-
return req("yaml");
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
throw new Error("gxt-manifest-lib: yaml package not found (run npm ci)");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function readManifest(repoRoot) {
|
|
57
|
-
const rel = ".gitagent/foreman/MANIFEST.json";
|
|
58
|
-
const abs = path.join(repoRoot, rel);
|
|
59
|
-
if (!fs.existsSync(abs)) {
|
|
60
|
-
throw new Error(`validate-gxt: missing ${rel}`);
|
|
61
|
-
}
|
|
62
|
-
let parsed;
|
|
63
|
-
try {
|
|
64
|
-
parsed = JSON.parse(fs.readFileSync(abs, "utf8"));
|
|
65
|
-
} catch (e) {
|
|
66
|
-
throw new Error(`validate-gxt: invalid JSON in ${rel}: ${e instanceof Error ? e.message : String(e)}`);
|
|
67
|
-
}
|
|
68
|
-
return parsed;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function loadTrustedAutomationRules(repoRoot) {
|
|
72
|
-
const abs = path.join(repoRoot, CONFIG_REL);
|
|
73
|
-
if (!fs.existsSync(abs)) {
|
|
74
|
-
return [];
|
|
75
|
-
}
|
|
76
|
-
let parsed;
|
|
77
|
-
try {
|
|
78
|
-
parsed = JSON.parse(fs.readFileSync(abs, "utf8"));
|
|
79
|
-
} catch (e) {
|
|
80
|
-
throw new Error(
|
|
81
|
-
`gxt-manifest-lib: invalid JSON in ${CONFIG_REL}: ${e instanceof Error ? e.message : String(e)}`,
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
const block = parsed?.trusted_automation;
|
|
85
|
-
if (!block) {
|
|
86
|
-
return [];
|
|
87
|
-
}
|
|
88
|
-
if (!Array.isArray(block.rules) || block.rules.length === 0) {
|
|
89
|
-
return [];
|
|
90
|
-
}
|
|
91
|
-
const rules = [];
|
|
92
|
-
for (const [i, rule] of block.rules.entries()) {
|
|
93
|
-
if (!rule || typeof rule !== "object") {
|
|
94
|
-
throw new Error(`gxt-manifest-lib: trusted_automation.rules[${i}] must be an object`);
|
|
95
|
-
}
|
|
96
|
-
if (typeof rule.id !== "string" || !rule.id.trim()) {
|
|
97
|
-
throw new Error(`gxt-manifest-lib: trusted_automation.rules[${i}] missing id`);
|
|
98
|
-
}
|
|
99
|
-
if (!Array.isArray(rule.allowed_actors) || rule.allowed_actors.length === 0) {
|
|
100
|
-
throw new Error(`gxt-manifest-lib: rule ${rule.id} requires non-empty allowed_actors`);
|
|
101
|
-
}
|
|
102
|
-
if (!Array.isArray(rule.allowed_paths) || rule.allowed_paths.length === 0) {
|
|
103
|
-
throw new Error(`gxt-manifest-lib: rule ${rule.id} requires non-empty allowed_paths`);
|
|
104
|
-
}
|
|
105
|
-
if (!Array.isArray(rule.allowed_structural_changes) || rule.allowed_structural_changes.length === 0) {
|
|
106
|
-
throw new Error(`gxt-manifest-lib: rule ${rule.id} requires allowed_structural_changes`);
|
|
107
|
-
}
|
|
108
|
-
for (const change of rule.allowed_structural_changes) {
|
|
109
|
-
if (!ALLOWED_STRUCTURAL_CHANGES.has(change)) {
|
|
110
|
-
throw new Error(`gxt-manifest-lib: rule ${rule.id} has unsupported structural change: ${change}`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
if (typeof rule.max_net_loc !== "number" || !Number.isInteger(rule.max_net_loc) || rule.max_net_loc < 1) {
|
|
114
|
-
throw new Error(`gxt-manifest-lib: rule ${rule.id} max_net_loc must be a positive integer`);
|
|
115
|
-
}
|
|
116
|
-
if (rule.max_net_loc > HARD_MAX_NET_LOC) {
|
|
117
|
-
throw new Error(`gxt-manifest-lib: rule ${rule.id} max_net_loc exceeds hard cap ${HARD_MAX_NET_LOC}`);
|
|
118
|
-
}
|
|
119
|
-
rules.push({
|
|
120
|
-
id: rule.id,
|
|
121
|
-
allowed_actors: rule.allowed_actors.map((a) => String(a).trim().toLowerCase()),
|
|
122
|
-
allowed_paths: rule.allowed_paths.map((p) => normalizePath(String(p))),
|
|
123
|
-
allowed_structural_changes: new Set(rule.allowed_structural_changes),
|
|
124
|
-
max_net_loc: rule.max_net_loc,
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
return rules;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export function matchGlob(pattern, filePath) {
|
|
131
|
-
const normalized = normalizePath(filePath);
|
|
132
|
-
const g = normalizePath(pattern);
|
|
133
|
-
if (g.startsWith("**/")) {
|
|
134
|
-
const suffix = g.slice(3);
|
|
135
|
-
return normalized.endsWith(suffix) || normalized.includes(`/${suffix}`);
|
|
136
|
-
}
|
|
137
|
-
const re = new RegExp(
|
|
138
|
-
`^${normalizePath(pattern)
|
|
139
|
-
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
140
|
-
.replace(/\*\*/g, "<<<GLOBSTAR>>>")
|
|
141
|
-
.replace(/\*/g, "[^/]*")
|
|
142
|
-
.replace(/<<<GLOBSTAR>>>/g, ".*")}$`,
|
|
143
|
-
);
|
|
144
|
-
return re.test(normalized);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export function pathMatchesAllowed(filePath, allowedPaths) {
|
|
148
|
-
return allowedPaths.some((pattern) => matchGlob(pattern, filePath));
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export function listMsnEnforcedPrefixes(repoRoot) {
|
|
152
|
-
const manifest = readManifest(repoRoot);
|
|
153
|
-
const skills = manifest.skills;
|
|
154
|
-
if (!skills || typeof skills !== "object" || Array.isArray(skills)) {
|
|
155
|
-
throw new Error("validate-gxt: MANIFEST skills must be a non-empty object");
|
|
156
|
-
}
|
|
157
|
-
const roots = Object.values(skills).flatMap((s) => {
|
|
158
|
-
if (!s || typeof s !== "object") return [];
|
|
159
|
-
return Array.isArray(s.tmvc_roots) ? s.tmvc_roots : [];
|
|
160
|
-
});
|
|
161
|
-
const out = new Set();
|
|
162
|
-
for (const p of [...FIXED_MSN_PREFIXES, ...roots]) {
|
|
163
|
-
if (typeof p !== "string" || !p.trim()) continue;
|
|
164
|
-
out.add(normalizePath(p));
|
|
165
|
-
}
|
|
166
|
-
return [...out];
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export function isMsnEnforcedPath(filePath, prefixes) {
|
|
170
|
-
const p = normalizePath(filePath);
|
|
171
|
-
for (const prefix of prefixes) {
|
|
172
|
-
if (!prefix) continue;
|
|
173
|
-
if (p === prefix) return true;
|
|
174
|
-
if (prefix.endsWith("/") && p.startsWith(prefix)) return true;
|
|
175
|
-
if (!prefix.endsWith("/") && p.startsWith(`${prefix}/`)) return true;
|
|
176
|
-
}
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export function validateManifestStructure(repoRoot) {
|
|
181
|
-
const m = readManifest(repoRoot);
|
|
182
|
-
if (typeof m.schema_version !== "string" || m.schema_version.length === 0) {
|
|
183
|
-
throw new Error("validate-gxt: schema_version must be a non-empty string");
|
|
184
|
-
}
|
|
185
|
-
const skills = m.skills;
|
|
186
|
-
if (!skills || typeof skills !== "object" || Array.isArray(skills) || Object.keys(skills).length === 0) {
|
|
187
|
-
throw new Error("validate-gxt: skills must be a non-empty object");
|
|
188
|
-
}
|
|
189
|
-
if (!m.path_risks || typeof m.path_risks !== "object" || Array.isArray(m.path_risks)) {
|
|
190
|
-
throw new Error("validate-gxt: path_risks must be an object");
|
|
191
|
-
}
|
|
192
|
-
if (!Array.isArray(m.risk_keywords)) {
|
|
193
|
-
throw new Error("validate-gxt: risk_keywords must be an array");
|
|
194
|
-
}
|
|
195
|
-
if (m.perimeter_protected !== undefined) {
|
|
196
|
-
if (!Array.isArray(m.perimeter_protected)) {
|
|
197
|
-
throw new Error("validate-gxt: perimeter_protected must be an array when present");
|
|
198
|
-
}
|
|
199
|
-
for (const g of m.perimeter_protected) {
|
|
200
|
-
if (typeof g !== "string" || g.length === 0) {
|
|
201
|
-
throw new Error("validate-gxt: perimeter_protected items must be non-empty strings");
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
for (const [key, skill] of Object.entries(skills)) {
|
|
206
|
-
if (!skill || typeof skill !== "object") {
|
|
207
|
-
throw new Error(`validate-gxt: skills.${key} must be an object`);
|
|
208
|
-
}
|
|
209
|
-
for (const field of ["trust_threshold", "tmvc_roots", "forbidden_zones"]) {
|
|
210
|
-
if (!(field in skill)) {
|
|
211
|
-
throw new Error(`validate-gxt: skills.${key} missing ${field}`);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
if (!Array.isArray(skill.tmvc_roots)) {
|
|
215
|
-
throw new Error(`validate-gxt: skills.${key}.tmvc_roots must be an array`);
|
|
216
|
-
}
|
|
217
|
-
if (!Array.isArray(skill.forbidden_zones)) {
|
|
218
|
-
throw new Error(`validate-gxt: skills.${key}.forbidden_zones must be an array`);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export function validateBypassNoteJson(raw) {
|
|
224
|
-
let parsed;
|
|
225
|
-
try {
|
|
226
|
-
parsed = JSON.parse(raw);
|
|
227
|
-
} catch {
|
|
228
|
-
return false;
|
|
229
|
-
}
|
|
230
|
-
return (
|
|
231
|
-
parsed?.v === 1 &&
|
|
232
|
-
typeof parsed.reason === "string" &&
|
|
233
|
-
parsed.reason.length >= 10
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function parseNumstat(output) {
|
|
238
|
-
let total = 0;
|
|
239
|
-
for (const line of output.split("\n")) {
|
|
240
|
-
if (!line.trim()) continue;
|
|
241
|
-
const [adds, dels] = line.split("\t");
|
|
242
|
-
if (adds === "-" || dels === "-") continue;
|
|
243
|
-
total += Number(adds) + Number(dels);
|
|
244
|
-
}
|
|
245
|
-
return total;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function gitNumstat(repoRoot, args) {
|
|
249
|
-
try {
|
|
250
|
-
return parseNumstat(git(repoRoot, args));
|
|
251
|
-
} catch {
|
|
252
|
-
return 0;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function commitChangedFiles(repoRoot, commitSha) {
|
|
257
|
-
const out = git(repoRoot, ["diff-tree", "--no-commit-id", "--name-only", "-r", commitSha]);
|
|
258
|
-
return out ? out.split("\n").filter(Boolean).map(normalizePath) : [];
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function rangeChangedFiles(repoRoot, baseSha, headSha) {
|
|
262
|
-
const out = git(repoRoot, ["diff", "--name-only", "--diff-filter=ACMRT", `${baseSha}...${headSha}`]);
|
|
263
|
-
return out ? out.split("\n").filter(Boolean).map(normalizePath) : [];
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function commitAuthorEmail(repoRoot, commitSha) {
|
|
267
|
-
return git(repoRoot, ["log", "-1", "--format=%ae", commitSha]).toLowerCase();
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function objectKeysEqual(a, b) {
|
|
271
|
-
if (Array.isArray(a) || Array.isArray(b)) {
|
|
272
|
-
return Array.isArray(a) && Array.isArray(b) && a.length === b.length;
|
|
273
|
-
}
|
|
274
|
-
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") {
|
|
275
|
-
return typeof a === typeof b;
|
|
276
|
-
}
|
|
277
|
-
const keysA = Object.keys(a).sort();
|
|
278
|
-
const keysB = Object.keys(b).sort();
|
|
279
|
-
return keysA.length === keysB.length && keysA.every((k, i) => k === keysB[i]);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function collectStructuralDiffs(before, after, keyPath = []) {
|
|
283
|
-
const diffs = [];
|
|
284
|
-
if (Array.isArray(before) && Array.isArray(after)) {
|
|
285
|
-
for (let i = 0; i < before.length; i++) {
|
|
286
|
-
diffs.push(...collectStructuralDiffs(before[i], after[i], [...keyPath, String(i)]));
|
|
287
|
-
}
|
|
288
|
-
return diffs;
|
|
289
|
-
}
|
|
290
|
-
if (before !== null && after !== null && typeof before === "object" && typeof after === "object") {
|
|
291
|
-
const keys = Object.keys(before);
|
|
292
|
-
for (const key of keys) {
|
|
293
|
-
const childPath = [...keyPath, key];
|
|
294
|
-
const bv = before[key];
|
|
295
|
-
const av = after[key];
|
|
296
|
-
if (bv !== null && av !== null && typeof bv === "object" && typeof av === "object") {
|
|
297
|
-
diffs.push(...collectStructuralDiffs(bv, av, childPath));
|
|
298
|
-
} else if (bv !== av) {
|
|
299
|
-
diffs.push({ path: childPath, before: bv, after: av });
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
return diffs;
|
|
303
|
-
}
|
|
304
|
-
if (before !== after) {
|
|
305
|
-
diffs.push({ path: keyPath, before, after });
|
|
306
|
-
}
|
|
307
|
-
return diffs;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function isWorkflowVersionPinChange(before, after) {
|
|
311
|
-
if (typeof before !== "string" || typeof after !== "string") return false;
|
|
312
|
-
const usesRe = /^([^@]+@)(.+)$/;
|
|
313
|
-
const bm = before.match(usesRe);
|
|
314
|
-
const am = after.match(usesRe);
|
|
315
|
-
return Boolean(bm && am && bm[1] === am[1] && bm[2] !== am[2]);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function structuresMatch(before, after) {
|
|
319
|
-
if (Array.isArray(before) && Array.isArray(after)) {
|
|
320
|
-
if (before.length !== after.length) return false;
|
|
321
|
-
return before.every((item, i) => structuresMatch(item, after[i]));
|
|
322
|
-
}
|
|
323
|
-
if (before !== null && after !== null && typeof before === "object" && typeof after === "object") {
|
|
324
|
-
if (!objectKeysEqual(before, after)) return false;
|
|
325
|
-
return Object.keys(before).every((k) => structuresMatch(before[k], after[k]));
|
|
326
|
-
}
|
|
327
|
-
return true;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function workflowVersionPinOnly(beforeContent, afterContent, yaml) {
|
|
331
|
-
let beforeDoc;
|
|
332
|
-
let afterDoc;
|
|
333
|
-
try {
|
|
334
|
-
beforeDoc = yaml.parse(beforeContent);
|
|
335
|
-
afterDoc = yaml.parse(afterContent);
|
|
336
|
-
} catch {
|
|
337
|
-
return false;
|
|
338
|
-
}
|
|
339
|
-
if (!structuresMatch(beforeDoc, afterDoc)) {
|
|
340
|
-
return false;
|
|
341
|
-
}
|
|
342
|
-
const diffs = collectStructuralDiffs(beforeDoc, afterDoc);
|
|
343
|
-
if (diffs.length === 0) return true;
|
|
344
|
-
return diffs.every((d) => {
|
|
345
|
-
const lastKey = d.path[d.path.length - 1];
|
|
346
|
-
return lastKey === "uses" && isWorkflowVersionPinChange(d.before, d.after);
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function readBlobAt(repoRoot, commitSha, filePath) {
|
|
351
|
-
try {
|
|
352
|
-
return git(repoRoot, ["show", `${commitSha}:${filePath}`]);
|
|
353
|
-
} catch {
|
|
354
|
-
return null;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function validateStructuralChanges(repoRoot, files, rule, baseSha, headSha, mode) {
|
|
359
|
-
if (!rule.allowed_structural_changes.has("workflow_version_pin")) {
|
|
360
|
-
return { ok: false, reason: `rule ${rule.id}: no matching structural change policy` };
|
|
361
|
-
}
|
|
362
|
-
const yaml = resolveYamlParser(repoRoot);
|
|
363
|
-
const workflowFiles = files.filter((f) => /\.ya?ml$/i.test(f));
|
|
364
|
-
for (const file of workflowFiles) {
|
|
365
|
-
let beforeContent;
|
|
366
|
-
let afterContent;
|
|
367
|
-
if (mode === "commit") {
|
|
368
|
-
const parent = git(repoRoot, ["rev-parse", `${headSha}^`]);
|
|
369
|
-
beforeContent = readBlobAt(repoRoot, parent, file);
|
|
370
|
-
afterContent = readBlobAt(repoRoot, headSha, file);
|
|
371
|
-
} else {
|
|
372
|
-
beforeContent = readBlobAt(repoRoot, baseSha, file);
|
|
373
|
-
afterContent = readBlobAt(repoRoot, headSha, file);
|
|
374
|
-
}
|
|
375
|
-
if (afterContent === null) continue;
|
|
376
|
-
if (beforeContent === null) {
|
|
377
|
-
return { ok: false, reason: `rule ${rule.id}: new file not allowed (${file})` };
|
|
378
|
-
}
|
|
379
|
-
if (!workflowVersionPinOnly(beforeContent, afterContent, yaml)) {
|
|
380
|
-
return { ok: false, reason: `rule ${rule.id}: structural change outside workflow_version_pin (${file})` };
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
return { ok: true, reason: "" };
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function findMatchingRule(rules, actorEmail) {
|
|
387
|
-
const actor = actorEmail.toLowerCase();
|
|
388
|
-
return rules.filter((rule) => rule.allowed_actors.includes(actor));
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function evaluateAgainstRule(repoRoot, rule, files, actorEmail, netLoc, structuralCtx) {
|
|
392
|
-
if (!rule.allowed_actors.includes(actorEmail.toLowerCase())) {
|
|
393
|
-
return { eligible: false, reason: `actor ${actorEmail} not in rule ${rule.id} allowed_actors` };
|
|
394
|
-
}
|
|
395
|
-
if (files.length === 0) {
|
|
396
|
-
return { eligible: false, reason: "no changed files" };
|
|
397
|
-
}
|
|
398
|
-
for (const file of files) {
|
|
399
|
-
if (!pathMatchesAllowed(file, rule.allowed_paths)) {
|
|
400
|
-
return { eligible: false, reason: `path ${file} outside rule ${rule.id} allowed_paths` };
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
if (netLoc > rule.max_net_loc) {
|
|
404
|
-
return {
|
|
405
|
-
eligible: false,
|
|
406
|
-
reason: `net_loc ${netLoc} exceeds rule ${rule.id} max_net_loc ${rule.max_net_loc}`,
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
const structural = validateStructuralChanges(
|
|
410
|
-
repoRoot,
|
|
411
|
-
files,
|
|
412
|
-
rule,
|
|
413
|
-
structuralCtx.baseSha,
|
|
414
|
-
structuralCtx.headSha,
|
|
415
|
-
structuralCtx.mode,
|
|
416
|
-
);
|
|
417
|
-
if (!structural.ok) {
|
|
418
|
-
return { eligible: false, reason: structural.reason };
|
|
419
|
-
}
|
|
420
|
-
return {
|
|
421
|
-
eligible: true,
|
|
422
|
-
reason: `TRUSTED-AUTOMATION-OK: rule=${rule.id} actor=${actorEmail} paths=${files.join(",")} net_loc=${netLoc}`,
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
/**
|
|
427
|
-
* Evaluate a single commit for trusted automation eligibility.
|
|
428
|
-
* @returns {{ eligible: boolean, reason: string }}
|
|
429
|
-
*/
|
|
430
|
-
export function evaluateTrustedAutomationCommit(repoRoot, commitSha) {
|
|
431
|
-
const rules = loadTrustedAutomationRules(repoRoot);
|
|
432
|
-
if (rules.length === 0) {
|
|
433
|
-
return { eligible: false, reason: "no trusted_automation policy configured" };
|
|
434
|
-
}
|
|
435
|
-
const prefixes = listMsnEnforcedPrefixes(repoRoot);
|
|
436
|
-
const files = commitChangedFiles(repoRoot, commitSha).filter((f) => isMsnEnforcedPath(f, prefixes));
|
|
437
|
-
if (files.length === 0) {
|
|
438
|
-
return { eligible: false, reason: "commit does not touch MSN-enforced paths" };
|
|
439
|
-
}
|
|
440
|
-
const actorEmail = commitAuthorEmail(repoRoot, commitSha);
|
|
441
|
-
const matchingRules = findMatchingRule(rules, actorEmail);
|
|
442
|
-
if (matchingRules.length === 0) {
|
|
443
|
-
return { eligible: false, reason: `actor ${actorEmail} not covered by trusted_automation policy` };
|
|
444
|
-
}
|
|
445
|
-
const netLoc = gitNumstat(repoRoot, ["show", "--numstat", "--format=", commitSha]);
|
|
446
|
-
let lastReason = `commit ${commitSha} failed trusted_automation policy checks`;
|
|
447
|
-
for (const rule of matchingRules) {
|
|
448
|
-
const result = evaluateAgainstRule(repoRoot, rule, files, actorEmail, netLoc, {
|
|
449
|
-
mode: "commit",
|
|
450
|
-
baseSha: null,
|
|
451
|
-
headSha: commitSha,
|
|
452
|
-
});
|
|
453
|
-
if (result.eligible) return result;
|
|
454
|
-
lastReason = result.reason;
|
|
455
|
-
}
|
|
456
|
-
return { eligible: false, reason: lastReason };
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
* Evaluate a PR commit range for trusted automation eligibility.
|
|
461
|
-
* Range net_loc uses the full base...head diff; actor is the author of the last commit
|
|
462
|
-
* in the range (same as prior behavior, documented here for audit).
|
|
463
|
-
* @returns {{ eligible: boolean, reason: string }}
|
|
464
|
-
*/
|
|
465
|
-
export function evaluateTrustedAutomationRange(repoRoot, baseSha, headSha) {
|
|
466
|
-
const rules = loadTrustedAutomationRules(repoRoot);
|
|
467
|
-
if (rules.length === 0) {
|
|
468
|
-
return { eligible: false, reason: "no trusted_automation policy configured" };
|
|
469
|
-
}
|
|
470
|
-
const prefixes = listMsnEnforcedPrefixes(repoRoot);
|
|
471
|
-
const allFiles = rangeChangedFiles(repoRoot, baseSha, headSha);
|
|
472
|
-
const enforcedFiles = allFiles.filter((f) => isMsnEnforcedPath(f, prefixes));
|
|
473
|
-
if (enforcedFiles.length === 0) {
|
|
474
|
-
return { eligible: false, reason: "range does not touch MSN-enforced paths" };
|
|
475
|
-
}
|
|
476
|
-
const commits = git(repoRoot, ["rev-list", "--no-merges", `${baseSha}..${headSha}`])
|
|
477
|
-
.split("\n")
|
|
478
|
-
.filter(Boolean);
|
|
479
|
-
for (const commit of commits) {
|
|
480
|
-
const files = commitChangedFiles(repoRoot, commit).filter((f) => isMsnEnforcedPath(f, prefixes));
|
|
481
|
-
if (files.length === 0) continue;
|
|
482
|
-
const actorEmail = commitAuthorEmail(repoRoot, commit);
|
|
483
|
-
if (findMatchingRule(rules, actorEmail).length === 0) {
|
|
484
|
-
return {
|
|
485
|
-
eligible: false,
|
|
486
|
-
reason: `commit ${commit.slice(0, 7)}: actor ${actorEmail} not covered by trusted_automation policy`,
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
const netLoc = gitNumstat(repoRoot, ["diff", "--numstat", `${baseSha}...${headSha}`]);
|
|
491
|
-
const actorEmail = commits.length > 0 ? commitAuthorEmail(repoRoot, commits[commits.length - 1]) : "";
|
|
492
|
-
const matchingRules = findMatchingRule(rules, actorEmail);
|
|
493
|
-
let lastReason = "PR diff failed trusted_automation policy checks";
|
|
494
|
-
for (const rule of matchingRules) {
|
|
495
|
-
const result = evaluateAgainstRule(repoRoot, rule, enforcedFiles, actorEmail, netLoc, {
|
|
496
|
-
mode: "range",
|
|
497
|
-
baseSha,
|
|
498
|
-
headSha,
|
|
499
|
-
});
|
|
500
|
-
if (result.eligible) return result;
|
|
501
|
-
lastReason = result.reason;
|
|
502
|
-
}
|
|
503
|
-
return { eligible: false, reason: lastReason };
|
|
504
|
-
}
|
|
13
|
+
import { matchGlob } from "./lib/glob-match.mjs";
|
|
14
|
+
import {
|
|
15
|
+
listMsnEnforcedPrefixes,
|
|
16
|
+
repoRootFromArg,
|
|
17
|
+
validateBypassNoteJson,
|
|
18
|
+
validateManifestStructure,
|
|
19
|
+
} from "./lib/manifest-validate.mjs";
|
|
20
|
+
import {
|
|
21
|
+
evaluateTrustedAutomationCommit,
|
|
22
|
+
evaluateTrustedAutomationRange,
|
|
23
|
+
} from "./lib/trusted-automation.mjs";
|
|
505
24
|
|
|
506
25
|
function main() {
|
|
507
26
|
const cmd = process.argv[2];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function normalizePath(p) {
|
|
2
|
+
return p.replace(/\\/g, "/");
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function matchGlob(pattern, filePath) {
|
|
6
|
+
const normalized = normalizePath(filePath);
|
|
7
|
+
const g = normalizePath(pattern);
|
|
8
|
+
if (g.startsWith("**/")) {
|
|
9
|
+
const suffix = g.slice(3);
|
|
10
|
+
return normalized.endsWith(suffix) || normalized.includes(`/${suffix}`);
|
|
11
|
+
}
|
|
12
|
+
const re = new RegExp(
|
|
13
|
+
`^${normalizePath(pattern)
|
|
14
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
15
|
+
.replace(/\*\*/g, "<<<GLOBSTAR>>>")
|
|
16
|
+
.replace(/\*/g, "[^/]*")
|
|
17
|
+
.replace(/<<<GLOBSTAR>>>/g, ".*")}$`,
|
|
18
|
+
);
|
|
19
|
+
return re.test(normalized);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function pathMatchesAllowed(filePath, allowedPaths) {
|
|
23
|
+
return allowedPaths.some((pattern) => matchGlob(pattern, filePath));
|
|
24
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { normalizePath } from "./glob-match.mjs";
|
|
4
|
+
|
|
5
|
+
const FIXED_MSN_PREFIXES = [
|
|
6
|
+
".gitagent/",
|
|
7
|
+
"EXECUTOR_LOG.md",
|
|
8
|
+
".githooks/",
|
|
9
|
+
".github/workflows/gxt-validate.yml",
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export function repoRootFromArg(arg) {
|
|
13
|
+
if (arg?.trim()) return path.resolve(arg.trim());
|
|
14
|
+
return process.cwd();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function readManifest(repoRoot) {
|
|
18
|
+
const rel = ".gitagent/foreman/MANIFEST.json";
|
|
19
|
+
const abs = path.join(repoRoot, rel);
|
|
20
|
+
if (!fs.existsSync(abs)) {
|
|
21
|
+
throw new Error(`validate-gxt: missing ${rel}`);
|
|
22
|
+
}
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = JSON.parse(fs.readFileSync(abs, "utf8"));
|
|
26
|
+
} catch (e) {
|
|
27
|
+
throw new Error(`validate-gxt: invalid JSON in ${rel}: ${e instanceof Error ? e.message : String(e)}`);
|
|
28
|
+
}
|
|
29
|
+
return parsed;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Validate parsed MANIFEST object shape. Prefix controls error message namespace
|
|
34
|
+
* (validate-gxt for shell gates, MANIFEST for gantry CLI).
|
|
35
|
+
*/
|
|
36
|
+
export function validateManifestObject(m, { prefix = "validate-gxt" } = {}) {
|
|
37
|
+
if (typeof m !== "object" || m === null) {
|
|
38
|
+
throw new Error(`${prefix}: not an object`);
|
|
39
|
+
}
|
|
40
|
+
if (typeof m.schema_version !== "string" || m.schema_version.length === 0) {
|
|
41
|
+
throw new Error(`${prefix}: schema_version must be a non-empty string`);
|
|
42
|
+
}
|
|
43
|
+
const skills = m.skills;
|
|
44
|
+
if (!skills || typeof skills !== "object" || Array.isArray(skills) || Object.keys(skills).length === 0) {
|
|
45
|
+
throw new Error(`${prefix}: skills must be a non-empty object`);
|
|
46
|
+
}
|
|
47
|
+
if (!m.path_risks || typeof m.path_risks !== "object" || Array.isArray(m.path_risks)) {
|
|
48
|
+
throw new Error(`${prefix}: path_risks must be an object`);
|
|
49
|
+
}
|
|
50
|
+
if (!Array.isArray(m.risk_keywords)) {
|
|
51
|
+
throw new Error(`${prefix}: risk_keywords must be an array`);
|
|
52
|
+
}
|
|
53
|
+
if (m.perimeter_protected !== undefined) {
|
|
54
|
+
if (!Array.isArray(m.perimeter_protected)) {
|
|
55
|
+
throw new Error(`${prefix}: perimeter_protected must be an array when present`);
|
|
56
|
+
}
|
|
57
|
+
for (const g of m.perimeter_protected) {
|
|
58
|
+
if (typeof g !== "string" || g.length === 0) {
|
|
59
|
+
throw new Error(`${prefix}: perimeter_protected items must be non-empty strings`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
for (const [key, skill] of Object.entries(skills)) {
|
|
64
|
+
if (!skill || typeof skill !== "object") {
|
|
65
|
+
throw new Error(`${prefix}: skills.${key} must be an object`);
|
|
66
|
+
}
|
|
67
|
+
for (const field of ["trust_threshold", "tmvc_roots", "forbidden_zones"]) {
|
|
68
|
+
if (!(field in skill)) {
|
|
69
|
+
throw new Error(`${prefix}: skills.${key} missing ${field}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!Array.isArray(skill.tmvc_roots)) {
|
|
73
|
+
throw new Error(`${prefix}: skills.${key}.tmvc_roots must be an array`);
|
|
74
|
+
}
|
|
75
|
+
if (!Array.isArray(skill.forbidden_zones)) {
|
|
76
|
+
throw new Error(`${prefix}: skills.${key}.forbidden_zones must be an array`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function validateManifestStructure(repoRoot) {
|
|
82
|
+
validateManifestObject(readManifest(repoRoot), { prefix: "validate-gxt" });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function listMsnEnforcedPrefixes(repoRoot) {
|
|
86
|
+
const manifest = readManifest(repoRoot);
|
|
87
|
+
const skills = manifest.skills;
|
|
88
|
+
if (!skills || typeof skills !== "object" || Array.isArray(skills)) {
|
|
89
|
+
throw new Error("validate-gxt: MANIFEST skills must be a non-empty object");
|
|
90
|
+
}
|
|
91
|
+
const roots = Object.values(skills).flatMap((s) => {
|
|
92
|
+
if (!s || typeof s !== "object") return [];
|
|
93
|
+
return Array.isArray(s.tmvc_roots) ? s.tmvc_roots : [];
|
|
94
|
+
});
|
|
95
|
+
const out = new Set();
|
|
96
|
+
for (const p of [...FIXED_MSN_PREFIXES, ...roots]) {
|
|
97
|
+
if (typeof p !== "string" || !p.trim()) continue;
|
|
98
|
+
out.add(normalizePath(p));
|
|
99
|
+
}
|
|
100
|
+
return [...out];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function isMsnEnforcedPath(filePath, prefixes) {
|
|
104
|
+
const p = normalizePath(filePath);
|
|
105
|
+
for (const prefix of prefixes) {
|
|
106
|
+
if (!prefix) continue;
|
|
107
|
+
if (p === prefix) return true;
|
|
108
|
+
if (prefix.endsWith("/") && p.startsWith(prefix)) return true;
|
|
109
|
+
if (!prefix.endsWith("/") && p.startsWith(`${prefix}/`)) return true;
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function validateBypassNoteJson(raw) {
|
|
115
|
+
let parsed;
|
|
116
|
+
try {
|
|
117
|
+
parsed = JSON.parse(raw);
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
return (
|
|
122
|
+
parsed?.v === 1 &&
|
|
123
|
+
typeof parsed.reason === "string" &&
|
|
124
|
+
parsed.reason.length >= 10
|
|
125
|
+
);
|
|
126
|
+
}
|