@cyanautomation/kaseki-agent 1.64.0 → 1.64.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/kaseki-agent.sh +54 -168
- package/package.json +1 -1
- package/scripts/collect-feedback.js +12 -9
- package/scripts/kaseki-setup-host.sh +96 -38
- package/scripts/scouting-allowlist.js +229 -0
package/kaseki-agent.sh
CHANGED
|
@@ -470,143 +470,15 @@ validate_scouting_artifact_with_node() {
|
|
|
470
470
|
local final_artifact="$2"
|
|
471
471
|
local validation_error_file="$3"
|
|
472
472
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
function actualType(value) {
|
|
482
|
-
if (value === null) return "null";
|
|
483
|
-
if (Array.isArray(value)) return "array";
|
|
484
|
-
return typeof value;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function appendValidationFailure(reasonCode, error) {
|
|
488
|
-
fs.appendFileSync(jsonlLog, JSON.stringify({
|
|
489
|
-
timestamp: new Date().toISOString(),
|
|
490
|
-
reason_code: reasonCode,
|
|
491
|
-
...error,
|
|
492
|
-
}) + "\n");
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function summarize(errors) {
|
|
496
|
-
const critical = errors.filter((error) => error.severity === "critical").length;
|
|
497
|
-
const warning = errors.filter((error) => error.severity === "warning").length;
|
|
498
|
-
const counts = [];
|
|
499
|
-
if (critical) counts.push(`${critical} critical`);
|
|
500
|
-
if (warning) counts.push(`${warning} warning`);
|
|
501
|
-
const fields = errors.slice(0, 2).map((error) => error.field).join(", ");
|
|
502
|
-
const suffix = errors.length > 2 ? `, +${errors.length - 2} more` : "";
|
|
503
|
-
return `${counts.join(", ")} scouting validation ${errors.length === 1 ? "error" : "errors"}: ${fields}${suffix}`;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
let artifact;
|
|
507
|
-
try {
|
|
508
|
-
artifact = JSON.parse(fs.readFileSync(input, "utf8"));
|
|
509
|
-
} catch (err) {
|
|
510
|
-
const reasonCode = "malformed_json";
|
|
511
|
-
const error = {
|
|
512
|
-
field: "root",
|
|
513
|
-
expected: "exactly one valid JSON object",
|
|
514
|
-
actual: err && err.message ? String(err.message) : "JSON parse failed",
|
|
515
|
-
severity: "critical",
|
|
516
|
-
suggestion: "ensure exactly one valid JSON object is written to /results/scouting-candidate.json",
|
|
517
|
-
};
|
|
518
|
-
appendValidationFailure(reasonCode, error);
|
|
519
|
-
fs.writeFileSync(errorLog, JSON.stringify({
|
|
520
|
-
reason_code: reasonCode,
|
|
521
|
-
details: summarize([error]),
|
|
522
|
-
errors: [error],
|
|
523
|
-
}) + "\n");
|
|
524
|
-
process.exit(1);
|
|
473
|
+
node "$SCOUTING_ALLOWLIST_HELPER" validate \
|
|
474
|
+
"$candidate_artifact" \
|
|
475
|
+
"$final_artifact" \
|
|
476
|
+
"$validation_error_file" \
|
|
477
|
+
"/results/scouting-validation-errors.jsonl" \
|
|
478
|
+
>/dev/null \
|
|
479
|
+
2>> /results/scouting-stderr.log
|
|
525
480
|
}
|
|
526
481
|
|
|
527
|
-
const errors = [];
|
|
528
|
-
const addError = (field, expected, actual, severity, suggestion) => {
|
|
529
|
-
errors.push({ field, expected, actual, severity, suggestion });
|
|
530
|
-
};
|
|
531
|
-
const arrayKeys = ["requirements", "relevant_files", "observations", "plan", "validation", "risks", "test_impact"];
|
|
532
|
-
|
|
533
|
-
if (!artifact || Array.isArray(artifact) || typeof artifact !== "object") {
|
|
534
|
-
addError("root", "object", actualType(artifact), "critical", "Scouting artifact must be a JSON object, not an array/null/primitive");
|
|
535
|
-
} else {
|
|
536
|
-
if (typeof artifact.task !== "string" || !artifact.task.trim()) {
|
|
537
|
-
addError("task", "non-empty string", typeof artifact.task === "string" ? "empty string" : actualType(artifact.task), "critical", "task must be a non-empty string describing the requested work");
|
|
538
|
-
}
|
|
539
|
-
for (const key of arrayKeys) {
|
|
540
|
-
if (!Array.isArray(artifact[key])) {
|
|
541
|
-
addError(key, "array", actualType(artifact[key]), "critical", `${key} must be an array in the scouting handoff`);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
if (Array.isArray(artifact.relevant_files)) {
|
|
545
|
-
artifact.relevant_files.forEach((item, index) => {
|
|
546
|
-
if (!item || typeof item.path !== "string" || typeof item.reason !== "string") {
|
|
547
|
-
addError(`relevant_files[${index}]`, "object with string path and string reason", actualType(item), "warning", "Each relevant_files entry must include path and reason strings");
|
|
548
|
-
}
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
if (Array.isArray(artifact.test_impact)) {
|
|
552
|
-
artifact.test_impact.forEach((item, index) => {
|
|
553
|
-
if (!item || typeof item.path !== "string" || !item.path.trim() || typeof item.reason !== "string" || !item.reason.trim()) {
|
|
554
|
-
addError(`test_impact[${index}]`, "object with non-empty string path and non-empty string reason", actualType(item), "critical", "Each test_impact entry must include the impacted test path and expectation reason strings");
|
|
555
|
-
}
|
|
556
|
-
// Validate optional test_examples field
|
|
557
|
-
if (item.test_examples !== undefined) {
|
|
558
|
-
if (!Array.isArray(item.test_examples)) {
|
|
559
|
-
addError(`test_impact[${index}].test_examples`, "array of example objects or undefined", actualType(item.test_examples), "warning", "test_examples must be an array of objects with type, pattern, description, before, and after fields");
|
|
560
|
-
} else {
|
|
561
|
-
item.test_examples.forEach((example, exIdx) => {
|
|
562
|
-
if (!example || typeof example !== "object" || !["added_assertion", "modified_assertion", "added_test_case", "added_pattern"].includes(example.type)) {
|
|
563
|
-
addError(`test_impact[${index}].test_examples[${exIdx}].type`, "added_assertion|modified_assertion|added_test_case|added_pattern", actualType(example && example.type), "warning", "Each test_example must have a valid type");
|
|
564
|
-
}
|
|
565
|
-
if (!example || typeof example.pattern !== "string") {
|
|
566
|
-
addError(`test_impact[${index}].test_examples[${exIdx}].pattern`, "string", actualType(example && example.pattern), "warning", "Each test_example must have a pattern string");
|
|
567
|
-
}
|
|
568
|
-
if (!example || typeof example.before !== "string" || typeof example.after !== "string") {
|
|
569
|
-
addError(`test_impact[${index}].test_examples[${exIdx}]`, "before and after strings", "missing or invalid", "warning", "Each test_example must have before and after code snippets");
|
|
570
|
-
}
|
|
571
|
-
});
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// Validate suggested_allowlist (optional but if present, must be valid)
|
|
578
|
-
if (artifact.suggested_allowlist) {
|
|
579
|
-
if (typeof artifact.suggested_allowlist !== "object" || Array.isArray(artifact.suggested_allowlist)) {
|
|
580
|
-
addError("suggested_allowlist", "object", actualType(artifact.suggested_allowlist), "warning", "suggested_allowlist must be an object with agent_patterns and validation_patterns arrays");
|
|
581
|
-
} else {
|
|
582
|
-
if (!Array.isArray(artifact.suggested_allowlist.agent_patterns)) {
|
|
583
|
-
addError("suggested_allowlist.agent_patterns", "array of strings", actualType(artifact.suggested_allowlist.agent_patterns), "warning", "agent_patterns must be an array of glob pattern strings");
|
|
584
|
-
} else if (!artifact.suggested_allowlist.agent_patterns.every((p) => typeof p === "string")) {
|
|
585
|
-
addError("suggested_allowlist.agent_patterns", "array of strings", "array with non-strings", "warning", "All agent_patterns entries must be strings");
|
|
586
|
-
}
|
|
587
|
-
if (!Array.isArray(artifact.suggested_allowlist.validation_patterns)) {
|
|
588
|
-
addError("suggested_allowlist.validation_patterns", "array of strings", actualType(artifact.suggested_allowlist.validation_patterns), "warning", "validation_patterns must be an array of glob pattern strings");
|
|
589
|
-
} else if (!artifact.suggested_allowlist.validation_patterns.every((p) => typeof p === "string")) {
|
|
590
|
-
addError("suggested_allowlist.validation_patterns", "array of strings", "array with non-strings", "warning", "All validation_patterns entries must be strings");
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
if (errors.length) {
|
|
597
|
-
const onlyTaskMissing = errors.length === 1 && errors[0].field === "task";
|
|
598
|
-
const reasonCode = onlyTaskMissing ? "missing_required_fields" : "schema_mismatch";
|
|
599
|
-
for (const error of errors) appendValidationFailure(reasonCode, error);
|
|
600
|
-
fs.writeFileSync(errorLog, JSON.stringify({
|
|
601
|
-
reason_code: reasonCode,
|
|
602
|
-
details: summarize(errors),
|
|
603
|
-
errors,
|
|
604
|
-
}) + "\n");
|
|
605
|
-
process.exit(1);
|
|
606
|
-
}
|
|
607
|
-
fs.writeFileSync(output, JSON.stringify(artifact, null, 2) + "\n");
|
|
608
|
-
' "$candidate_artifact" "$final_artifact" "$validation_error_file" 2>> /results/scouting-stderr.log
|
|
609
|
-
}
|
|
610
482
|
# Validate scouting artifact and emit structured reason code
|
|
611
483
|
validate_scouting_artifact() {
|
|
612
484
|
local candidate_artifact="$1"
|
|
@@ -1258,52 +1130,58 @@ run_expectation_mismatch_detector() {
|
|
|
1258
1130
|
fi
|
|
1259
1131
|
}
|
|
1260
1132
|
|
|
1133
|
+
resolve_allowlist_helper() {
|
|
1134
|
+
local script_dir="$1"
|
|
1135
|
+
local script_relative_helper="$script_dir/scripts/allowlist-helper.sh"
|
|
1136
|
+
local fallback_helper="${KASEKI_ALLOWLIST_HELPER_FALLBACK:-/app/scripts/allowlist-helper.sh}"
|
|
1137
|
+
|
|
1138
|
+
if [ -r "$script_relative_helper" ]; then
|
|
1139
|
+
printf '%s\n' "$script_relative_helper"
|
|
1140
|
+
return 0
|
|
1141
|
+
fi
|
|
1142
|
+
|
|
1143
|
+
if [ -r "$fallback_helper" ]; then
|
|
1144
|
+
printf '%s\n' "$fallback_helper"
|
|
1145
|
+
return 0
|
|
1146
|
+
fi
|
|
1147
|
+
|
|
1148
|
+
printf 'ERROR: Allowlist helper is not readable. Expected packaged helper at %s or fallback helper at %s. This worker image or mounted template is incomplete; rebuild the image or restore scripts/allowlist-helper.sh.\n' \
|
|
1149
|
+
"$script_relative_helper" \
|
|
1150
|
+
"$fallback_helper" >&2
|
|
1151
|
+
return 66
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1261
1154
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
1262
|
-
ALLOWLIST_HELPER="$SCRIPT_DIR
|
|
1263
|
-
|
|
1264
|
-
|
|
1155
|
+
ALLOWLIST_HELPER="$(resolve_allowlist_helper "$SCRIPT_DIR")"
|
|
1156
|
+
allowlist_helper_status=$?
|
|
1157
|
+
if [ "$allowlist_helper_status" -ne 0 ]; then
|
|
1158
|
+
exit "$allowlist_helper_status"
|
|
1159
|
+
fi
|
|
1160
|
+
SCOUTING_ALLOWLIST_HELPER="$SCRIPT_DIR/scripts/scouting-allowlist.js"
|
|
1161
|
+
if [ ! -r "$SCOUTING_ALLOWLIST_HELPER" ] && [ -r /app/scripts/scouting-allowlist.js ]; then
|
|
1162
|
+
SCOUTING_ALLOWLIST_HELPER="/app/scripts/scouting-allowlist.js"
|
|
1265
1163
|
fi
|
|
1266
1164
|
# shellcheck source=scripts/allowlist-helper.sh
|
|
1267
1165
|
. "$ALLOWLIST_HELPER"
|
|
1166
|
+
if [ "${KASEKI_AGENT_HELPER_RESOLUTION_CHECK:-0}" = "1" ]; then
|
|
1167
|
+
build_allowlist_regex "${KASEKI_CHANGED_FILES_ALLOWLIST:-}" >/dev/null
|
|
1168
|
+
printf 'allowlist_helper=%s\n' "$ALLOWLIST_HELPER"
|
|
1169
|
+
exit 0
|
|
1170
|
+
fi
|
|
1268
1171
|
|
|
1269
1172
|
derive_allowlist_from_scouting() {
|
|
1270
|
-
local scouting_artifact
|
|
1173
|
+
local scouting_artifact
|
|
1271
1174
|
scouting_artifact="${1:?missing scouting artifact path}"
|
|
1272
|
-
|
|
1175
|
+
|
|
1273
1176
|
if [ ! -f "$scouting_artifact" ]; then
|
|
1274
1177
|
printf 'derive_allowlist_from_scouting: scouting artifact not found: %s\n' "$scouting_artifact" >&2
|
|
1275
1178
|
return 1
|
|
1276
1179
|
fi
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
agent_patterns="$(node -e "
|
|
1280
|
-
try {
|
|
1281
|
-
const fs = require('node:fs');
|
|
1282
|
-
const artifact = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
|
|
1283
|
-
if (artifact && artifact.suggested_allowlist && Array.isArray(artifact.suggested_allowlist.agent_patterns)) {
|
|
1284
|
-
console.log(artifact.suggested_allowlist.agent_patterns.join(' '));
|
|
1285
|
-
}
|
|
1286
|
-
} catch (e) {
|
|
1287
|
-
console.error('Error parsing scouting artifact:', e.message);
|
|
1288
|
-
}
|
|
1289
|
-
" "$scouting_artifact" 2>/dev/null)"
|
|
1290
|
-
|
|
1291
|
-
validation_patterns="$(node -e "
|
|
1292
|
-
try {
|
|
1293
|
-
const fs = require('node:fs');
|
|
1294
|
-
const artifact = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
|
|
1295
|
-
if (artifact && artifact.suggested_allowlist && Array.isArray(artifact.suggested_allowlist.validation_patterns)) {
|
|
1296
|
-
console.log(artifact.suggested_allowlist.validation_patterns.join(' '));
|
|
1297
|
-
}
|
|
1298
|
-
} catch (e) {
|
|
1299
|
-
console.error('Error parsing scouting artifact:', e.message);
|
|
1300
|
-
}
|
|
1301
|
-
" "$scouting_artifact" 2>/dev/null)"
|
|
1302
|
-
|
|
1303
|
-
printf '%s\n' "$agent_patterns"
|
|
1304
|
-
printf '%s\n' "$validation_patterns"
|
|
1180
|
+
|
|
1181
|
+
node "$SCOUTING_ALLOWLIST_HELPER" derive "$scouting_artifact"
|
|
1305
1182
|
}
|
|
1306
1183
|
|
|
1184
|
+
|
|
1307
1185
|
validate_allowlist_patterns() {
|
|
1308
1186
|
local patterns_str test_regex
|
|
1309
1187
|
patterns_str="${1:-}"
|
|
@@ -4542,6 +4420,14 @@ Determine if the agent successfully met the requirements specified in the goal-s
|
|
|
4542
4420
|
|
|
4543
4421
|
For each dimension, provide evidence by citing specific file locations, test names, or validation results.
|
|
4544
4422
|
|
|
4423
|
+
## Success Criteria Assessment Contract
|
|
4424
|
+
|
|
4425
|
+
Use this contract when reading the goal-setting artifact:
|
|
4426
|
+
|
|
4427
|
+
- GOAL_CHECK_CONTRACT_PER_CRITERION_SMART: Assess each success criterion or acceptance criterion independently against all five SMART dimensions (Specific, Measurable, Achievable, Relevant, Time-bound), not just the overall goal.
|
|
4428
|
+
- GOAL_CHECK_CONTRACT_EVIDENCE_REQUIRED: For every met or unmet criterion assessment, cite concrete evidence from the diff, changed files, tests, validation results, or other listed artifacts.
|
|
4429
|
+
- GOAL_CHECK_CONTRACT_MEASURABLE_ACCEPTANCE_CRITERIA: Treat measurable acceptance criteria as observable pass/fail conditions; flag vague goals or intent statements as insufficient unless the implementation evidence maps them to concrete, verifiable outcomes.
|
|
4430
|
+
|
|
4545
4431
|
## Evidence Requirements
|
|
4546
4432
|
|
|
4547
4433
|
Evidence must be SPECIFIC and VERIFIABLE. Reference concrete artifacts:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyanautomation/kaseki-agent",
|
|
3
|
-
"version": "1.64.
|
|
3
|
+
"version": "1.64.1",
|
|
4
4
|
"description": "Admin/helper/doctor toolbox and local API client for Kaseki diagnostics, setup, and API-backed coding-agent task workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
* node collect-feedback.js run-evaluation <instance_name> <run_evaluation_json> <metadata_json>
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
12
13
|
|
|
13
|
-
function parseJson(filePath) {
|
|
14
|
+
export function parseJson(filePath) {
|
|
14
15
|
try {
|
|
15
16
|
if (!filePath || !fs.existsSync(filePath)) {
|
|
16
17
|
return {};
|
|
@@ -22,7 +23,7 @@ function parseJson(filePath) {
|
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
function collectGoalCheckFeedback(instanceName, goalSettingPath, goalCheckPath, metadataPath) {
|
|
26
|
+
export function collectGoalCheckFeedback(instanceName, goalSettingPath, goalCheckPath, metadataPath) {
|
|
26
27
|
const goalSetting = parseJson(goalSettingPath);
|
|
27
28
|
const goalCheck = parseJson(goalCheckPath);
|
|
28
29
|
const metadata = parseJson(metadataPath);
|
|
@@ -67,7 +68,7 @@ function collectGoalCheckFeedback(instanceName, goalSettingPath, goalCheckPath,
|
|
|
67
68
|
return feedback;
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
function collectRunEvaluationFeedback(instanceName, runEvaluationPath, metadataPath) {
|
|
71
|
+
export function collectRunEvaluationFeedback(instanceName, runEvaluationPath, metadataPath) {
|
|
71
72
|
const runEvaluation = parseJson(runEvaluationPath);
|
|
72
73
|
const metadata = parseJson(metadataPath);
|
|
73
74
|
|
|
@@ -88,7 +89,7 @@ function collectRunEvaluationFeedback(instanceName, runEvaluationPath, metadataP
|
|
|
88
89
|
return feedback;
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
function extractOutcomes(metadata) {
|
|
92
|
+
export function extractOutcomes(metadata) {
|
|
92
93
|
return {
|
|
93
94
|
validation_passed: metadata.validation_passed === true,
|
|
94
95
|
coding_attempts: metadata.coding_attempts || 1,
|
|
@@ -97,7 +98,7 @@ function extractOutcomes(metadata) {
|
|
|
97
98
|
};
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
function computeCorrelationNotes(qualityScore, verdict, outcomes) {
|
|
101
|
+
export function computeCorrelationNotes(qualityScore, verdict, outcomes) {
|
|
101
102
|
const notes = [];
|
|
102
103
|
|
|
103
104
|
if (qualityScore >= 85 && !verdict.met) {
|
|
@@ -116,14 +117,14 @@ function computeCorrelationNotes(qualityScore, verdict, outcomes) {
|
|
|
116
117
|
return notes;
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
function parseStageValues(stageValues) {
|
|
120
|
+
export function parseStageValues(stageValues) {
|
|
120
121
|
return stageValues.map((s) => ({
|
|
121
122
|
stage: s.stage || 'unknown',
|
|
122
123
|
value: s.value || 'unknown',
|
|
123
124
|
}));
|
|
124
125
|
}
|
|
125
126
|
|
|
126
|
-
function parseImprovements(improvements) {
|
|
127
|
+
export function parseImprovements(improvements) {
|
|
127
128
|
return improvements.map((imp) => ({
|
|
128
129
|
category: imp.category || 'unknown',
|
|
129
130
|
priority: imp.priority || 'medium',
|
|
@@ -164,4 +165,6 @@ function main() {
|
|
|
164
165
|
console.log(JSON.stringify(feedback));
|
|
165
166
|
}
|
|
166
167
|
|
|
167
|
-
|
|
168
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
169
|
+
main();
|
|
170
|
+
}
|
|
@@ -256,29 +256,46 @@ run_checkout_freshness_probe() {
|
|
|
256
256
|
# Phase 4: Use parallel privilege tool testing when running as root
|
|
257
257
|
# Runs setpriv, runuser, and sudo in parallel; returns on first success
|
|
258
258
|
# This reduces probe time from ~6 seconds (sequential 3×2s timeouts) to ~2 seconds (first success)
|
|
259
|
+
local probe_exit_status=0
|
|
259
260
|
if [ "$(id -u)" -eq "$KASEKI_CONTAINER_UID" ] && [ "$(id -g)" -eq "$KASEKI_CONTAINER_GID" ]; then
|
|
260
|
-
"${probe_command[@]}" >/dev/null 2>"$stderr_file"
|
|
261
|
+
if "${probe_command[@]}" >/dev/null 2>"$stderr_file"; then
|
|
262
|
+
probe_exit_status=0
|
|
263
|
+
else
|
|
264
|
+
probe_exit_status=$?
|
|
265
|
+
fi
|
|
261
266
|
elif [ "$(id -u)" -eq 0 ]; then
|
|
262
267
|
# Phase 4: Parallel privilege tool testing
|
|
263
|
-
run_privilege_tools_parallel "$checkout_dir" "$stderr_file" "$resolved_user_name" "$resolved_group_name" "${probe_command[@]}"
|
|
268
|
+
if run_privilege_tools_parallel "$checkout_dir" "$stderr_file" "$resolved_user_name" "$resolved_group_name" "${probe_command[@]}"; then
|
|
269
|
+
probe_exit_status=0
|
|
270
|
+
else
|
|
271
|
+
probe_exit_status=$?
|
|
272
|
+
fi
|
|
264
273
|
else
|
|
265
|
-
"${probe_command[@]}" >/dev/null 2>"$stderr_file"
|
|
274
|
+
if "${probe_command[@]}" >/dev/null 2>"$stderr_file"; then
|
|
275
|
+
probe_exit_status=0
|
|
276
|
+
else
|
|
277
|
+
probe_exit_status=$?
|
|
278
|
+
fi
|
|
266
279
|
fi
|
|
267
280
|
|
|
268
|
-
if [
|
|
281
|
+
if [ "$probe_exit_status" -eq 0 ]; then
|
|
282
|
+
probe_status="ok"
|
|
283
|
+
probe_detail="Checkout freshness probe passed for ${checkout_dir} as UID:GID ${KASEKI_CONTAINER_UID}:${KASEKI_CONTAINER_GID}."
|
|
284
|
+
else
|
|
269
285
|
probe_status="failed"
|
|
270
286
|
local stderr_tail
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
287
|
+
if [ -s "$stderr_file" ]; then
|
|
288
|
+
stderr_tail="$(tail -n 1 "$stderr_file" | tr -d '\r')"
|
|
289
|
+
else
|
|
290
|
+
stderr_tail="probe command exited with status ${probe_exit_status} without stderr output"
|
|
291
|
+
fi
|
|
292
|
+
if printf '%s' "$stderr_tail" | grep -Eiq 'unknown user|unknown group|no passwd entry|user .* does not exist|group .* does not exist|sudo: .*unknown|sudo: .*invalid|runuser: .*does not exist|runuser: user .* does not exist|runuser: group .* does not exist|unable to initialize policy plugin|error initializing audit plugin|sudoers_audit|unable to set user context|timed out|no usable privilege tool'; then
|
|
293
|
+
probe_detail="Checkout freshness probe failed: probe could not impersonate UID:GID ${KASEKI_CONTAINER_UID}:${KASEKI_CONTAINER_GID} due to host user/group mapping, host privilege-tool configuration (including sudo policy/audit plugins), or timeout: ${stderr_tail}"
|
|
294
|
+
probe_remediation="Fix host privilege-tool configuration for sudo/policy/audit plugins (for example sudoers_audit), or configure another valid host method to run commands as UID:GID ${KASEKI_CONTAINER_UID}:${KASEKI_CONTAINER_GID} (and ensure passwd/group mappings exist for that UID/GID), then rerun ./scripts/kaseki-setup-host.sh --fix. If the issue is timeout, try increasing KASEKI_PRIV_TOOL_TIMEOUT."
|
|
275
295
|
else
|
|
276
296
|
probe_detail="Checkout freshness probe failed when running git metadata access as UID:GID ${KASEKI_CONTAINER_UID}:${KASEKI_CONTAINER_GID}: ${stderr_tail}"
|
|
277
297
|
probe_remediation="Fix ownership/permissions so ${checkout_dir} and ${checkout_dir}/.git are readable by UID:GID ${KASEKI_CONTAINER_UID}:${KASEKI_CONTAINER_GID}."
|
|
278
298
|
fi
|
|
279
|
-
else
|
|
280
|
-
probe_status="ok"
|
|
281
|
-
probe_detail="Checkout freshness probe passed for ${checkout_dir} as UID:GID ${KASEKI_CONTAINER_UID}:${KASEKI_CONTAINER_GID}."
|
|
282
299
|
fi
|
|
283
300
|
cleanup_probe_stderr_file
|
|
284
301
|
|
|
@@ -352,71 +369,112 @@ run_privilege_tools_parallel() {
|
|
|
352
369
|
local resolved_group_name="$4"
|
|
353
370
|
shift 4
|
|
354
371
|
local probe_command=("$@")
|
|
355
|
-
|
|
372
|
+
: >"$stderr_file"
|
|
373
|
+
|
|
356
374
|
local temp_dir
|
|
357
375
|
temp_dir=$(mktemp -d)
|
|
358
|
-
export temp_dir # Make accessible to trap handler in subshell contexts
|
|
359
376
|
local success_marker="$temp_dir/success"
|
|
360
377
|
local pids=()
|
|
361
|
-
|
|
378
|
+
local failure_stderr_files=()
|
|
379
|
+
local setpriv_stderr="$temp_dir/setpriv.stderr"
|
|
380
|
+
local runuser_stderr="$temp_dir/runuser.stderr"
|
|
381
|
+
local sudo_stderr="$temp_dir/sudo.stderr"
|
|
382
|
+
|
|
362
383
|
cleanup_parallel() {
|
|
363
|
-
|
|
364
|
-
rm -rf "$temp_dir"
|
|
384
|
+
rm -rf "${temp_dir:-}"
|
|
365
385
|
}
|
|
366
|
-
|
|
367
|
-
|
|
386
|
+
|
|
387
|
+
stop_parallel_jobs() {
|
|
388
|
+
local pid
|
|
389
|
+
for pid in "${pids[@]}"; do
|
|
390
|
+
kill "$pid" 2>/dev/null || true
|
|
391
|
+
done
|
|
392
|
+
for pid in "${pids[@]}"; do
|
|
393
|
+
wait "$pid" 2>/dev/null || true
|
|
394
|
+
done
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
copy_selected_failure_stderr() {
|
|
398
|
+
local candidate
|
|
399
|
+
for candidate in "${failure_stderr_files[@]}"; do
|
|
400
|
+
if [ -s "$candidate" ]; then
|
|
401
|
+
cp "$candidate" "$stderr_file"
|
|
402
|
+
return 0
|
|
403
|
+
fi
|
|
404
|
+
done
|
|
405
|
+
printf 'no usable privilege tool succeeded for %s\n' "$checkout_dir" >"$stderr_file"
|
|
406
|
+
}
|
|
407
|
+
|
|
368
408
|
# Test 1: setpriv (fastest, preferred)
|
|
369
409
|
if [ "$(id -u)" -eq 0 ] && command -v setpriv >/dev/null 2>&1; then
|
|
410
|
+
failure_stderr_files+=("$setpriv_stderr")
|
|
370
411
|
(
|
|
371
|
-
timeout "$KASEKI_PRIV_TOOL_TIMEOUT" setpriv --reuid "$KASEKI_CONTAINER_UID" --regid "$KASEKI_CONTAINER_GID" --clear-groups -- "${probe_command[@]}" >/dev/null 2>"$
|
|
412
|
+
if timeout "$KASEKI_PRIV_TOOL_TIMEOUT" setpriv --reuid "$KASEKI_CONTAINER_UID" --regid "$KASEKI_CONTAINER_GID" --clear-groups -- "${probe_command[@]}" >/dev/null 2>"$setpriv_stderr"; then
|
|
413
|
+
touch "$success_marker"
|
|
414
|
+
fi
|
|
372
415
|
) &
|
|
373
416
|
pids+=("$!")
|
|
374
417
|
fi
|
|
375
|
-
|
|
418
|
+
|
|
376
419
|
# Test 2: runuser (if resolved user/group available)
|
|
377
420
|
if [ "$(id -u)" -eq 0 ] && command -v runuser >/dev/null 2>&1 && [ -n "$resolved_user_name" ] && [ -n "$resolved_group_name" ]; then
|
|
421
|
+
failure_stderr_files+=("$runuser_stderr")
|
|
378
422
|
(
|
|
379
|
-
timeout "$KASEKI_PRIV_TOOL_TIMEOUT" runuser -u "$resolved_user_name" -g "$resolved_group_name" -- "${probe_command[@]}" >/dev/null 2>"$
|
|
423
|
+
if timeout "$KASEKI_PRIV_TOOL_TIMEOUT" runuser -u "$resolved_user_name" -g "$resolved_group_name" -- "${probe_command[@]}" >/dev/null 2>"$runuser_stderr"; then
|
|
424
|
+
touch "$success_marker"
|
|
425
|
+
fi
|
|
380
426
|
) &
|
|
381
427
|
pids+=("$!")
|
|
382
428
|
fi
|
|
383
|
-
|
|
429
|
+
|
|
384
430
|
# Test 3: sudo (fallback, slowest)
|
|
385
431
|
if [ "$(id -u)" -eq 0 ] && command -v sudo >/dev/null 2>&1; then
|
|
432
|
+
failure_stderr_files+=("$sudo_stderr")
|
|
386
433
|
(
|
|
387
434
|
if [ -n "$resolved_user_name" ] && [ -n "$resolved_group_name" ]; then
|
|
388
|
-
timeout "$KASEKI_PRIV_TOOL_TIMEOUT" sudo -u "$resolved_user_name" -g "$resolved_group_name" -- "${probe_command[@]}" >/dev/null 2>"$
|
|
435
|
+
timeout "$KASEKI_PRIV_TOOL_TIMEOUT" sudo -u "$resolved_user_name" -g "$resolved_group_name" -- "${probe_command[@]}" >/dev/null 2>"$sudo_stderr"
|
|
389
436
|
elif [ -n "$resolved_user_name" ]; then
|
|
390
|
-
timeout "$KASEKI_PRIV_TOOL_TIMEOUT" sudo -u "$resolved_user_name" -- "${probe_command[@]}" >/dev/null 2>"$
|
|
437
|
+
timeout "$KASEKI_PRIV_TOOL_TIMEOUT" sudo -u "$resolved_user_name" -- "${probe_command[@]}" >/dev/null 2>"$sudo_stderr"
|
|
391
438
|
else
|
|
392
|
-
timeout "$KASEKI_PRIV_TOOL_TIMEOUT" sudo -u "#${KASEKI_CONTAINER_UID}" -g "${KASEKI_CONTAINER_GID}" -- "${probe_command[@]}" >/dev/null 2>"$
|
|
439
|
+
timeout "$KASEKI_PRIV_TOOL_TIMEOUT" sudo -u "#${KASEKI_CONTAINER_UID}" -g "${KASEKI_CONTAINER_GID}" -- "${probe_command[@]}" >/dev/null 2>"$sudo_stderr"
|
|
393
440
|
fi && touch "$success_marker"
|
|
394
441
|
) &
|
|
395
442
|
pids+=("$!")
|
|
396
443
|
fi
|
|
397
|
-
|
|
444
|
+
|
|
445
|
+
if [ "${#pids[@]}" -eq 0 ]; then
|
|
446
|
+
copy_selected_failure_stderr
|
|
447
|
+
cleanup_parallel
|
|
448
|
+
return 1
|
|
449
|
+
fi
|
|
450
|
+
|
|
398
451
|
# Wait for any process to succeed (check success marker while processes run)
|
|
399
|
-
local
|
|
400
|
-
local
|
|
401
|
-
while [ $
|
|
452
|
+
local wait_attempt=0
|
|
453
|
+
local max_wait_attempts=$(( (KASEKI_PRIV_TOOL_TIMEOUT + 1) * 10 ))
|
|
454
|
+
while [ "$wait_attempt" -lt "$max_wait_attempts" ]; do
|
|
402
455
|
if [ -f "$success_marker" ]; then
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
kill "$pid" 2>/dev/null || true
|
|
406
|
-
done
|
|
456
|
+
stop_parallel_jobs
|
|
457
|
+
cleanup_parallel
|
|
407
458
|
return 0
|
|
408
459
|
fi
|
|
409
460
|
sleep 0.1
|
|
410
|
-
|
|
461
|
+
wait_attempt=$(( wait_attempt + 1 ))
|
|
411
462
|
done
|
|
412
|
-
|
|
463
|
+
|
|
413
464
|
# Wait for all processes to complete
|
|
414
465
|
for pid in "${pids[@]}"; do
|
|
415
466
|
wait "$pid" 2>/dev/null || true
|
|
416
467
|
done
|
|
417
|
-
|
|
418
|
-
# Check if any succeeded
|
|
419
|
-
|
|
468
|
+
|
|
469
|
+
# Check if any succeeded. On success, leave the shared stderr empty so failed
|
|
470
|
+
# parallel attempts cannot turn a successful privilege probe into a failure.
|
|
471
|
+
if [ -f "$success_marker" ]; then
|
|
472
|
+
cleanup_parallel
|
|
473
|
+
return 0
|
|
474
|
+
fi
|
|
475
|
+
|
|
476
|
+
copy_selected_failure_stderr
|
|
477
|
+
cleanup_parallel
|
|
420
478
|
return 1
|
|
421
479
|
}
|
|
422
480
|
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_CHANGED_FILES_ALLOWLIST = 'src/lib/parser.ts tests/parser.validation.ts';
|
|
7
|
+
export const DEFAULT_VALIDATION_ALLOWLIST = '';
|
|
8
|
+
|
|
9
|
+
export function actualType(value) {
|
|
10
|
+
if (value === null) return 'null';
|
|
11
|
+
if (Array.isArray(value)) return 'array';
|
|
12
|
+
return typeof value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function summarize(errors) {
|
|
16
|
+
const critical = errors.filter((error) => error.severity === 'critical').length;
|
|
17
|
+
const warning = errors.filter((error) => error.severity === 'warning').length;
|
|
18
|
+
const counts = [];
|
|
19
|
+
if (critical) counts.push(`${critical} critical`);
|
|
20
|
+
if (warning) counts.push(`${warning} warning`);
|
|
21
|
+
const fields = errors.slice(0, 2).map((error) => error.field).join(', ');
|
|
22
|
+
const suffix = errors.length > 2 ? `, +${errors.length - 2} more` : '';
|
|
23
|
+
return `${counts.join(', ')} scouting validation ${errors.length === 1 ? 'error' : 'errors'}: ${fields}${suffix}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function appendJsonl(file, value) {
|
|
27
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
28
|
+
fs.appendFileSync(file, JSON.stringify(value) + '\n');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readArtifact(inputPath) {
|
|
32
|
+
return JSON.parse(fs.readFileSync(inputPath, 'utf8'));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function validateScoutingArtifactObject(artifact) {
|
|
36
|
+
const errors = [];
|
|
37
|
+
const addError = (field, expected, actual, severity, suggestion) => {
|
|
38
|
+
errors.push({ field, expected, actual, severity, suggestion });
|
|
39
|
+
};
|
|
40
|
+
const arrayKeys = ['requirements', 'relevant_files', 'observations', 'plan', 'validation', 'risks', 'test_impact'];
|
|
41
|
+
|
|
42
|
+
if (!artifact || Array.isArray(artifact) || typeof artifact !== 'object') {
|
|
43
|
+
addError('root', 'object', actualType(artifact), 'critical', 'Scouting artifact must be a JSON object, not an array/null/primitive');
|
|
44
|
+
} else {
|
|
45
|
+
if (typeof artifact.task !== 'string' || !artifact.task.trim()) {
|
|
46
|
+
addError('task', 'non-empty string', typeof artifact.task === 'string' ? 'empty string' : actualType(artifact.task), 'critical', 'task must be a non-empty string describing the requested work');
|
|
47
|
+
}
|
|
48
|
+
for (const key of arrayKeys) {
|
|
49
|
+
if (!Array.isArray(artifact[key])) {
|
|
50
|
+
addError(key, 'array', actualType(artifact[key]), 'critical', `${key} must be an array in the scouting handoff`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (Array.isArray(artifact.relevant_files)) {
|
|
54
|
+
artifact.relevant_files.forEach((item, index) => {
|
|
55
|
+
if (!item || typeof item.path !== 'string' || typeof item.reason !== 'string') {
|
|
56
|
+
addError(`relevant_files[${index}]`, 'object with string path and string reason', actualType(item), 'warning', 'Each relevant_files entry must include path and reason strings');
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(artifact.test_impact)) {
|
|
61
|
+
artifact.test_impact.forEach((item, index) => {
|
|
62
|
+
if (!item || typeof item.path !== 'string' || !item.path.trim() || typeof item.reason !== 'string' || !item.reason.trim()) {
|
|
63
|
+
addError(`test_impact[${index}]`, 'object with non-empty string path and non-empty string reason', actualType(item), 'critical', 'Each test_impact entry must include the impacted test path and expectation reason strings');
|
|
64
|
+
}
|
|
65
|
+
if (item && typeof item === 'object' && item.test_examples !== undefined) {
|
|
66
|
+
if (!Array.isArray(item.test_examples)) {
|
|
67
|
+
addError(`test_impact[${index}].test_examples`, 'array of example objects or undefined', actualType(item.test_examples), 'warning', 'test_examples must be an array of objects with type, pattern, description, before, and after fields');
|
|
68
|
+
} else {
|
|
69
|
+
item.test_examples.forEach((example, exIdx) => {
|
|
70
|
+
if (!example || typeof example !== 'object' || !['added_assertion', 'modified_assertion', 'added_test_case', 'added_pattern'].includes(example.type)) {
|
|
71
|
+
addError(`test_impact[${index}].test_examples[${exIdx}].type`, 'added_assertion|modified_assertion|added_test_case|added_pattern', actualType(example && example.type), 'warning', 'Each test_example must have a valid type');
|
|
72
|
+
}
|
|
73
|
+
if (!example || typeof example.pattern !== 'string') {
|
|
74
|
+
addError(`test_impact[${index}].test_examples[${exIdx}].pattern`, 'string', actualType(example && example.pattern), 'warning', 'Each test_example must have a pattern string');
|
|
75
|
+
}
|
|
76
|
+
if (!example || typeof example.before !== 'string' || typeof example.after !== 'string') {
|
|
77
|
+
addError(`test_impact[${index}].test_examples[${exIdx}]`, 'before and after strings', 'missing or invalid', 'warning', 'Each test_example must have before and after code snippets');
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (artifact.suggested_allowlist) {
|
|
86
|
+
if (typeof artifact.suggested_allowlist !== 'object' || Array.isArray(artifact.suggested_allowlist)) {
|
|
87
|
+
addError('suggested_allowlist', 'object', actualType(artifact.suggested_allowlist), 'warning', 'suggested_allowlist must be an object with agent_patterns and validation_patterns arrays');
|
|
88
|
+
} else {
|
|
89
|
+
if (!Array.isArray(artifact.suggested_allowlist.agent_patterns)) {
|
|
90
|
+
addError('suggested_allowlist.agent_patterns', 'array of strings', actualType(artifact.suggested_allowlist.agent_patterns), 'warning', 'agent_patterns must be an array of glob pattern strings');
|
|
91
|
+
} else if (!artifact.suggested_allowlist.agent_patterns.every((p) => typeof p === 'string')) {
|
|
92
|
+
addError('suggested_allowlist.agent_patterns', 'array of strings', 'array with non-strings', 'warning', 'All agent_patterns entries must be strings');
|
|
93
|
+
}
|
|
94
|
+
if (!Array.isArray(artifact.suggested_allowlist.validation_patterns)) {
|
|
95
|
+
addError('suggested_allowlist.validation_patterns', 'array of strings', actualType(artifact.suggested_allowlist.validation_patterns), 'warning', 'validation_patterns must be an array of glob pattern strings');
|
|
96
|
+
} else if (!artifact.suggested_allowlist.validation_patterns.every((p) => typeof p === 'string')) {
|
|
97
|
+
addError('suggested_allowlist.validation_patterns', 'array of strings', 'array with non-strings', 'warning', 'All validation_patterns entries must be strings');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (errors.length) {
|
|
104
|
+
const onlyTaskMissing = errors.length === 1 && errors[0].field === 'task';
|
|
105
|
+
return {
|
|
106
|
+
status: 'rejected',
|
|
107
|
+
reason_code: onlyTaskMissing ? 'missing_required_fields' : 'schema_mismatch',
|
|
108
|
+
details: summarize(errors),
|
|
109
|
+
errors,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { status: 'ok', reason_code: 'valid', details: 'artifact validation passed', errors: [] };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function validateScoutingArtifact(inputPath, outputPath, options = {}) {
|
|
117
|
+
let artifact;
|
|
118
|
+
try {
|
|
119
|
+
artifact = readArtifact(inputPath);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
const error = {
|
|
122
|
+
field: 'root',
|
|
123
|
+
expected: 'exactly one valid JSON object',
|
|
124
|
+
actual: err && err.message ? String(err.message) : 'JSON parse failed',
|
|
125
|
+
severity: 'critical',
|
|
126
|
+
suggestion: 'ensure exactly one valid JSON object is written to /results/scouting-candidate.json',
|
|
127
|
+
};
|
|
128
|
+
const result = { status: 'rejected', reason_code: 'malformed_json', details: summarize([error]), errors: [error] };
|
|
129
|
+
writeValidationArtifacts(result, options);
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const result = validateScoutingArtifactObject(artifact);
|
|
134
|
+
if (result.status === 'ok' && outputPath) {
|
|
135
|
+
fs.writeFileSync(outputPath, JSON.stringify(artifact, null, 2) + '\n');
|
|
136
|
+
}
|
|
137
|
+
writeValidationArtifacts(result, options);
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function writeValidationArtifacts(result, options) {
|
|
142
|
+
const { errorLog, jsonlLog } = options;
|
|
143
|
+
if (result.status === 'ok') return;
|
|
144
|
+
if (jsonlLog) {
|
|
145
|
+
for (const error of result.errors) {
|
|
146
|
+
appendJsonl(jsonlLog, { timestamp: new Date().toISOString(), reason_code: result.reason_code, ...error });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (errorLog) {
|
|
150
|
+
fs.writeFileSync(errorLog, JSON.stringify({ reason_code: result.reason_code, details: result.details, errors: result.errors }) + '\n');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function deriveAllowlistFromScoutingArtifact(artifact) {
|
|
155
|
+
return {
|
|
156
|
+
agentAllowlist: artifact && artifact.suggested_allowlist && Array.isArray(artifact.suggested_allowlist.agent_patterns)
|
|
157
|
+
? artifact.suggested_allowlist.agent_patterns.join(' ')
|
|
158
|
+
: '',
|
|
159
|
+
validationAllowlist: artifact && artifact.suggested_allowlist && Array.isArray(artifact.suggested_allowlist.validation_patterns)
|
|
160
|
+
? artifact.suggested_allowlist.validation_patterns.join(' ')
|
|
161
|
+
: '',
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function deriveAllowlistFromScouting(inputPath) {
|
|
166
|
+
return deriveAllowlistFromScoutingArtifact(readArtifact(inputPath));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function mergeAllowlists(scoutingPatterns = '', userPatterns = '') {
|
|
170
|
+
if (scoutingPatterns && userPatterns) return `${scoutingPatterns} ${userPatterns}`;
|
|
171
|
+
return scoutingPatterns || userPatterns || '';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function deriveScoutingAllowlistOrDefault(inputPath, options = {}) {
|
|
175
|
+
const defaultChangedFilesAllowlist = options.defaultChangedFilesAllowlist ?? DEFAULT_CHANGED_FILES_ALLOWLIST;
|
|
176
|
+
const defaultValidationAllowlist = options.defaultValidationAllowlist ?? DEFAULT_VALIDATION_ALLOWLIST;
|
|
177
|
+
const validation = validateScoutingArtifact(inputPath, undefined, options);
|
|
178
|
+
|
|
179
|
+
if (validation.status !== 'ok') {
|
|
180
|
+
return {
|
|
181
|
+
validation,
|
|
182
|
+
agentAllowlist: defaultChangedFilesAllowlist,
|
|
183
|
+
validationAllowlist: defaultValidationAllowlist,
|
|
184
|
+
source: 'default_after_rejection',
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const derived = deriveAllowlistFromScouting(inputPath);
|
|
189
|
+
return {
|
|
190
|
+
validation,
|
|
191
|
+
agentAllowlist: mergeAllowlists(derived.agentAllowlist, defaultChangedFilesAllowlist),
|
|
192
|
+
validationAllowlist: mergeAllowlists(derived.validationAllowlist, defaultValidationAllowlist),
|
|
193
|
+
source: derived.agentAllowlist || derived.validationAllowlist ? 'merged_scouting' : 'default_after_absent_suggestion',
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function printJson(value) {
|
|
198
|
+
process.stdout.write(JSON.stringify(value));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function main(argv) {
|
|
202
|
+
const [command, ...args] = argv;
|
|
203
|
+
if (command === 'validate') {
|
|
204
|
+
const [inputPath, outputPath, errorLog, jsonlLog] = args;
|
|
205
|
+
const result = validateScoutingArtifact(inputPath, outputPath, { errorLog, jsonlLog });
|
|
206
|
+
printJson(result);
|
|
207
|
+
process.exitCode = result.status === 'ok' ? 0 : 1;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (command === 'derive') {
|
|
211
|
+
const result = deriveAllowlistFromScouting(args[0]);
|
|
212
|
+
process.stdout.write(`${result.agentAllowlist}\n${result.validationAllowlist}\n`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (command === 'orchestrate') {
|
|
216
|
+
printJson(deriveScoutingAllowlistOrDefault(args[0], {
|
|
217
|
+
defaultChangedFilesAllowlist: args[1] ?? DEFAULT_CHANGED_FILES_ALLOWLIST,
|
|
218
|
+
defaultValidationAllowlist: args[2] ?? DEFAULT_VALIDATION_ALLOWLIST,
|
|
219
|
+
}));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
process.stderr.write('usage: scouting-allowlist.js <validate|derive|orchestrate> ...\n');
|
|
223
|
+
process.exitCode = 64;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
227
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === thisFile) {
|
|
228
|
+
main(process.argv.slice(2));
|
|
229
|
+
}
|