@indigoai-us/hq-cli 5.345.113 → 5.345.115
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 +24 -0
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +5 -1
- package/dist/commands/bot-jobs.d.ts +1 -11
- package/dist/commands/bot-jobs.js +13 -34
- package/dist/commands/capacity.js +2 -0
- package/dist/commands/core-audit-log-native.js +1 -1
- package/dist/commands/core-checkpoint-native.d.ts +27 -0
- package/dist/commands/core-checkpoint-native.js +85 -2
- package/dist/commands/core-dm-bind-native.js +1 -1
- package/dist/commands/core-git-pack-extension-check-native.js +1 -1
- package/dist/commands/core-job-native-utils.d.ts +2 -2
- package/dist/commands/core-job-native-utils.js +3 -11
- package/dist/commands/core-job-notify-native.js +1 -2
- package/dist/commands/core-job-probe-native.d.ts +2 -3
- package/dist/commands/core-job-probe-native.js +14 -22
- package/dist/commands/core-job-remediate-native.js +1 -2
- package/dist/commands/core-job-run-native.js +58 -54
- package/dist/commands/core-jobs-native.d.ts +1 -3
- package/dist/commands/core-jobs-native.js +16 -271
- package/dist/commands/core-jobs-schedule.d.ts +7 -0
- package/dist/commands/core-jobs-schedule.js +243 -0
- package/dist/commands/core-native-utilities.d.ts +2 -3
- package/dist/commands/core-native-utilities.js +2 -19
- package/dist/commands/core-pin-native.js +1 -1
- package/dist/commands/core-refresh-vault-access-native.js +1 -1
- package/dist/commands/core-resolve-company-command.js +2 -17
- package/dist/commands/core-work-mesh-bind-trusted-native.js +1 -1
- package/dist/commands/core.js +1 -17
- package/dist/commands/lanes.js +1 -0
- package/dist/fast-scaffold.d.ts +1 -1
- package/dist/fast-scaffold.js +0 -38
- package/dist/lib/capacity/lane-login.js +73 -6
- package/dist/lib/capacity/route.d.ts +1 -1
- package/dist/lib/capacity/route.js +5 -1
- package/dist/lib/capacity/sign-in.d.ts +12 -0
- package/dist/lib/capacity/sign-in.js +65 -16
- package/dist/lib/daemon/catalog.d.ts +0 -2
- package/dist/lib/daemon/catalog.js +3 -6
- package/dist/lib/daemon/run.js +0 -7
- package/dist/utils/resident-package-preload.js +2 -7
- package/package.json +1 -1
- package/assets/scaffold/core/scripts/audit-log.sh +0 -384
- package/assets/scaffold/core/scripts/git-pack-extension-check.sh +0 -164
- package/assets/scaffold/core/scripts/hq-dm-bind.sh +0 -590
- package/assets/scaffold/core/scripts/hq-job-notify.sh +0 -389
- package/assets/scaffold/core/scripts/hq-job-probe.sh +0 -865
- package/assets/scaffold/core/scripts/hq-job-remediate.sh +0 -359
- package/assets/scaffold/core/scripts/hq-job-run.sh +0 -1025
- package/assets/scaffold/core/scripts/hq-pin.sh +0 -118
- package/assets/scaffold/core/scripts/jobs-schedule-parse.sh +0 -310
- package/assets/scaffold/core/scripts/jobs-validate.sh +0 -482
- package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +0 -836
- package/assets/scaffold/core/scripts/refresh-vault-access.sh +0 -188
- package/assets/scaffold/core/scripts/resolve-company.sh +0 -331
- package/assets/scaffold/core/scripts/work-mesh-live-bind-trusted.sh +0 -184
- package/dist/commands/core-job-post-exit-assets.d.ts +0 -8
- package/dist/commands/core-job-post-exit-assets.js +0 -37
- package/dist/commands/core-native-jobs-flag.d.ts +0 -4
- package/dist/commands/core-native-jobs-flag.js +0 -7
- package/dist/commands/core-native-utilities-flag.d.ts +0 -4
- package/dist/commands/core-native-utilities-flag.js +0 -7
|
@@ -1,482 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# hq-core: public
|
|
3
|
-
# jobs-validate.sh — validate Outpost scheduled job YAML registry files.
|
|
4
|
-
#
|
|
5
|
-
# Usage:
|
|
6
|
-
# core/scripts/jobs-validate.sh <file-or-dir> [file-or-dir...]
|
|
7
|
-
#
|
|
8
|
-
# Exits 0 when all jobs are valid; non-zero with per-field errors on stderr.
|
|
9
|
-
# Schema: core/knowledge/public/hq-core/outpost-jobs-spec.md
|
|
10
|
-
#
|
|
11
|
-
# Requires: yq (mikefarah), jq. Timezone checked via IANA-looking regex only.
|
|
12
|
-
|
|
13
|
-
set -euo pipefail
|
|
14
|
-
|
|
15
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
-
|
|
17
|
-
# shellcheck source=lib/secret-patterns.sh
|
|
18
|
-
. "$SCRIPT_DIR/lib/secret-patterns.sh"
|
|
19
|
-
|
|
20
|
-
ERRORS=0
|
|
21
|
-
declare -a SEEN_IDS=()
|
|
22
|
-
|
|
23
|
-
die_usage() {
|
|
24
|
-
echo "usage: jobs-validate.sh <file-or-dir> [file-or-dir...]" >&2
|
|
25
|
-
exit 2
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
err() {
|
|
29
|
-
# err <file> <field> <message>
|
|
30
|
-
printf 'jobs-validate: %s: %s: %s\n' "$1" "$2" "$3" >&2
|
|
31
|
-
ERRORS=$((ERRORS + 1))
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
command -v yq >/dev/null 2>&1 || {
|
|
35
|
-
echo "jobs-validate: yq is required (mikefarah/yq)" >&2
|
|
36
|
-
exit 2
|
|
37
|
-
}
|
|
38
|
-
command -v jq >/dev/null 2>&1 || {
|
|
39
|
-
echo "jobs-validate: jq is required" >&2
|
|
40
|
-
exit 2
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
# jq.exe on MSYS may use CRT text mode unless --binary is supported.
|
|
44
|
-
case "${OSTYPE:-}:${MSYSTEM:-}" in
|
|
45
|
-
msys*:*|*:MINGW*)
|
|
46
|
-
# Probe --binary once per script run, in the top-level shell. Most jq
|
|
47
|
-
# calls run inside $(...) subshells, so a cache filled lazily inside jq()
|
|
48
|
-
# would be lost and the probe would repeat on every call.
|
|
49
|
-
if command jq --binary -n 'null' >/dev/null 2>&1; then
|
|
50
|
-
HQ_JQ_BINARY_SUPPORTED=1
|
|
51
|
-
else
|
|
52
|
-
HQ_JQ_BINARY_SUPPORTED=0
|
|
53
|
-
fi
|
|
54
|
-
jq() {
|
|
55
|
-
local stdout_file stderr_file status
|
|
56
|
-
stdout_file="$(mktemp)" || return $?
|
|
57
|
-
stderr_file="$(mktemp)" || { status=$?; rm -f "$stdout_file"; return "$status"; }
|
|
58
|
-
if [[ "$HQ_JQ_BINARY_SUPPORTED" == 1 ]]; then
|
|
59
|
-
if command jq --binary "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
|
|
60
|
-
command cat "$stdout_file"
|
|
61
|
-
command cat "$stderr_file" >&2
|
|
62
|
-
else
|
|
63
|
-
if command jq "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
|
|
64
|
-
command sed -b 's/\r$//' "$stdout_file"
|
|
65
|
-
command sed -b 's/\r$//' "$stderr_file" >&2
|
|
66
|
-
fi
|
|
67
|
-
rm -f "$stdout_file" "$stderr_file"
|
|
68
|
-
return "$status"
|
|
69
|
-
}
|
|
70
|
-
;;
|
|
71
|
-
esac
|
|
72
|
-
|
|
73
|
-
[ "$#" -ge 1 ] || die_usage
|
|
74
|
-
|
|
75
|
-
collect_files() {
|
|
76
|
-
local arg="$1"
|
|
77
|
-
if [ -f "$arg" ]; then
|
|
78
|
-
case "$arg" in
|
|
79
|
-
*.yaml|*.yml) printf '%s\n' "$arg" ;;
|
|
80
|
-
*) err "$arg" "path" "not a .yaml/.yml job file" ;;
|
|
81
|
-
esac
|
|
82
|
-
return
|
|
83
|
-
fi
|
|
84
|
-
if [ -d "$arg" ]; then
|
|
85
|
-
# Prefer find -print0 / sort for stable duplicate-id detection order
|
|
86
|
-
find "$arg" \( -name '*.yaml' -o -name '*.yml' \) -type f | sort
|
|
87
|
-
return
|
|
88
|
-
fi
|
|
89
|
-
err "$arg" "path" "file or directory not found"
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
cron_num_in_range() {
|
|
93
|
-
local n="$1" min="$2" max="$3"
|
|
94
|
-
[[ "$n" =~ ^[0-9]+$ ]] || return 1
|
|
95
|
-
((10#$n >= min && 10#$n <= max))
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
is_valid_cron_field() {
|
|
99
|
-
# $1=value $2=min $3=max — *, */n, a-b, a-b/n, lists, dow/month names
|
|
100
|
-
local v="$1" min="$2" max="$3" part base step a b
|
|
101
|
-
local IFS=','
|
|
102
|
-
set -f
|
|
103
|
-
# shellcheck disable=SC2086
|
|
104
|
-
set -- $v
|
|
105
|
-
set +f
|
|
106
|
-
for part in "$@"; do
|
|
107
|
-
case "$part" in
|
|
108
|
-
\*) continue ;;
|
|
109
|
-
\*/*)
|
|
110
|
-
step="${part#*/}"
|
|
111
|
-
[[ "$step" =~ ^[1-9][0-9]*$ ]] || return 1
|
|
112
|
-
continue
|
|
113
|
-
;;
|
|
114
|
-
sun|mon|tue|wed|thu|fri|sat|SUN|MON|TUE|WED|THU|FRI|SAT)
|
|
115
|
-
[ "$max" -eq 7 ] || return 1
|
|
116
|
-
continue
|
|
117
|
-
;;
|
|
118
|
-
jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)
|
|
119
|
-
[ "$max" -eq 12 ] || return 1
|
|
120
|
-
continue
|
|
121
|
-
;;
|
|
122
|
-
esac
|
|
123
|
-
if [[ "$part" =~ ^([0-9]+)-([0-9]+)(/([1-9][0-9]*))?$ ]]; then
|
|
124
|
-
a="${BASH_REMATCH[1]}"
|
|
125
|
-
b="${BASH_REMATCH[2]}"
|
|
126
|
-
cron_num_in_range "$a" "$min" "$max" || return 1
|
|
127
|
-
cron_num_in_range "$b" "$min" "$max" || return 1
|
|
128
|
-
((10#$a <= 10#$b)) || return 1
|
|
129
|
-
continue
|
|
130
|
-
fi
|
|
131
|
-
if [[ "$part" =~ ^([0-9]+)(/([1-9][0-9]*))?$ ]]; then
|
|
132
|
-
base="${BASH_REMATCH[1]}"
|
|
133
|
-
cron_num_in_range "$base" "$min" "$max" || return 1
|
|
134
|
-
continue
|
|
135
|
-
fi
|
|
136
|
-
return 1
|
|
137
|
-
done
|
|
138
|
-
return 0
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
validate_cron() {
|
|
142
|
-
local file="$1" cron="$2"
|
|
143
|
-
# strip surrounding quotes leftovers
|
|
144
|
-
cron="${cron#"${cron%%[![:space:]]*}"}"
|
|
145
|
-
cron="${cron%"${cron##*[![:space:]]}"}"
|
|
146
|
-
local -a fields
|
|
147
|
-
# noglob: cron uses '*' which must not expand against the filesystem
|
|
148
|
-
set -f
|
|
149
|
-
# shellcheck disable=SC2206
|
|
150
|
-
fields=($cron)
|
|
151
|
-
set +f
|
|
152
|
-
if [ "${#fields[@]}" -ne 5 ]; then
|
|
153
|
-
err "$file" "schedule" "invalid cron — expected 5 fields (minute hour dom month dow), got ${#fields[@]}"
|
|
154
|
-
return
|
|
155
|
-
fi
|
|
156
|
-
is_valid_cron_field "${fields[0]}" 0 59 || err "$file" "schedule" "invalid cron minute field '${fields[0]}'"
|
|
157
|
-
is_valid_cron_field "${fields[1]}" 0 23 || err "$file" "schedule" "invalid cron hour field '${fields[1]}'"
|
|
158
|
-
is_valid_cron_field "${fields[2]}" 1 31 || err "$file" "schedule" "invalid cron day-of-month field '${fields[2]}'"
|
|
159
|
-
is_valid_cron_field "${fields[3]}" 1 12 || err "$file" "schedule" "invalid cron month field '${fields[3]}'"
|
|
160
|
-
is_valid_cron_field "${fields[4]}" 0 7 || err "$file" "schedule" "invalid cron day-of-week field '${fields[4]}'"
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
looks_like_secret_name_ok() {
|
|
164
|
-
# vault key name: UPPER_SNAKE / mixed alnum._:- without whitespace or credential shapes
|
|
165
|
-
local s="$1"
|
|
166
|
-
[[ "$s" =~ ^[A-Za-z][A-Za-z0-9._:/-]{0,127}$ ]] || return 1
|
|
167
|
-
return 0
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
scan_string_for_secrets() {
|
|
171
|
-
local file="$1" field="$2" value="$3"
|
|
172
|
-
local tmp entry pattern name
|
|
173
|
-
tmp="$(mktemp "${TMPDIR:-/tmp}/jobs-validate-secret.XXXXXX")"
|
|
174
|
-
printf '%s' "$value" >"$tmp"
|
|
175
|
-
for entry in "${SECRET_PATTERNS[@]}"; do
|
|
176
|
-
pattern="${entry%%:*}"
|
|
177
|
-
name="${entry#*:}"
|
|
178
|
-
if grep -Eq -e "$pattern" "$tmp" 2>/dev/null; then
|
|
179
|
-
err "$file" "$field" "inline credential-shaped value matched pattern '$name' (names only; never values)"
|
|
180
|
-
fi
|
|
181
|
-
done
|
|
182
|
-
rm -f "$tmp"
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
validate_timezone() {
|
|
186
|
-
local file="$1" tz="$2"
|
|
187
|
-
if [[ ! "$tz" =~ ^[A-Za-z_]+(/[A-Za-z0-9_+-]+)+$ ]] && [[ "$tz" != "UTC" ]]; then
|
|
188
|
-
err "$file" "timezone" "not an IANA-looking timezone '$tz'"
|
|
189
|
-
return
|
|
190
|
-
fi
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
json_type() {
|
|
194
|
-
jq -r "$1 | type" <<<"$2" 2>/dev/null || echo "null"
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
# cwd must be HQ-root-relative: no absolute paths, no "."/".." components.
|
|
198
|
-
# Runtime resolves under HQ_ROOT; validate shape only (directory need not exist).
|
|
199
|
-
assert_cwd_safe() {
|
|
200
|
-
local file="$1" field="$2" cwd="$3"
|
|
201
|
-
[ -z "$cwd" ] && return 0
|
|
202
|
-
case "$cwd" in
|
|
203
|
-
/*)
|
|
204
|
-
err "$file" "$field" "absolute cwd not allowed — use an HQ-root-relative path"
|
|
205
|
-
return
|
|
206
|
-
;;
|
|
207
|
-
esac
|
|
208
|
-
local part
|
|
209
|
-
local IFS='/'
|
|
210
|
-
# shellcheck disable=SC2086
|
|
211
|
-
for part in $cwd; do
|
|
212
|
-
case "$part" in
|
|
213
|
-
''|.) ;;
|
|
214
|
-
..)
|
|
215
|
-
err "$file" "$field" "cwd must not contain '..' path components"
|
|
216
|
-
return
|
|
217
|
-
;;
|
|
218
|
-
esac
|
|
219
|
-
done
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
validate_job_file() {
|
|
223
|
-
local file="$1"
|
|
224
|
-
local json
|
|
225
|
-
if ! json="$(yq -o=json '.' "$file" 2>/dev/null)"; then
|
|
226
|
-
err "$file" "yaml" "failed to parse YAML"
|
|
227
|
-
return
|
|
228
|
-
fi
|
|
229
|
-
# Use jq's exit status instead of its text output: jq.exe writes CRLF on Windows.
|
|
230
|
-
if ! jq -es 'length == 1 and (.[0] | type == "object")' <<<"$json" >/dev/null; then
|
|
231
|
-
err "$file" "yaml" "job root must be a mapping"
|
|
232
|
-
return
|
|
233
|
-
fi
|
|
234
|
-
|
|
235
|
-
local id name schedule timezone runtime timeout notify enabled owner created_at
|
|
236
|
-
id="$(jq -r '.id // empty' <<<"$json")"
|
|
237
|
-
name="$(jq -r '.name // empty' <<<"$json")"
|
|
238
|
-
schedule="$(jq -r '.schedule // empty' <<<"$json")"
|
|
239
|
-
if jq -e 'has("timezone") and ((.timezone | type) != "string" or .timezone == "")' \
|
|
240
|
-
<<<"$json" >/dev/null; then
|
|
241
|
-
err "$file" "timezone" "must be a non-empty string when present"
|
|
242
|
-
fi
|
|
243
|
-
timezone="$(jq -r '.timezone // empty' <<<"$json")"
|
|
244
|
-
runtime="$(jq -r '.runtime // empty' <<<"$json")"
|
|
245
|
-
timeout="$(jq -r '.timeout_seconds // empty' <<<"$json")"
|
|
246
|
-
notify="$(jq -r '.notify // empty' <<<"$json")"
|
|
247
|
-
# jq's // treats boolean false as missing — use has() for enabled.
|
|
248
|
-
enabled="$(jq -r 'if has("enabled") then (.enabled|tostring) else empty end' <<<"$json")"
|
|
249
|
-
owner="$(jq -r '.owner // empty' <<<"$json")"
|
|
250
|
-
created_at="$(jq -r '.created_at // empty' <<<"$json")"
|
|
251
|
-
|
|
252
|
-
[ -n "$id" ] || err "$file" "id" "missing required field"
|
|
253
|
-
[ -n "$name" ] || err "$file" "name" "missing required field"
|
|
254
|
-
[ -n "$schedule" ] || err "$file" "schedule" "missing required field"
|
|
255
|
-
[ -n "$runtime" ] || err "$file" "runtime" "missing required field"
|
|
256
|
-
[ -n "$timeout" ] || err "$file" "timeout_seconds" "missing required field"
|
|
257
|
-
[ -n "$notify" ] || err "$file" "notify" "missing required field"
|
|
258
|
-
[ -n "$enabled" ] || err "$file" "enabled" "missing required field"
|
|
259
|
-
[ -n "$owner" ] || err "$file" "owner" "missing required field"
|
|
260
|
-
[ -n "$created_at" ] || err "$file" "created_at" "missing required field"
|
|
261
|
-
|
|
262
|
-
if [ -n "$id" ]; then
|
|
263
|
-
if [[ ! "$id" =~ ^[a-z][a-z0-9-]{1,62}$ ]]; then
|
|
264
|
-
err "$file" "id" "must match [a-z][a-z0-9-]{1,62}, got '$id'"
|
|
265
|
-
fi
|
|
266
|
-
local seen
|
|
267
|
-
for seen in "${SEEN_IDS[@]+"${SEEN_IDS[@]}"}"; do
|
|
268
|
-
if [ "$seen" = "$id" ]; then
|
|
269
|
-
err "$file" "id" "duplicate id '$id'"
|
|
270
|
-
break
|
|
271
|
-
fi
|
|
272
|
-
done
|
|
273
|
-
SEEN_IDS+=("$id")
|
|
274
|
-
fi
|
|
275
|
-
|
|
276
|
-
# Reject natural-language leftovers — canonical form is 5 cron fields only.
|
|
277
|
-
if [ -n "$schedule" ]; then
|
|
278
|
-
local -a sf
|
|
279
|
-
set -f
|
|
280
|
-
# shellcheck disable=SC2206
|
|
281
|
-
sf=($schedule)
|
|
282
|
-
set +f
|
|
283
|
-
if [ "${#sf[@]}" -ne 5 ]; then
|
|
284
|
-
if [[ "$schedule" =~ [[:alpha:]] ]]; then
|
|
285
|
-
err "$file" "schedule" "natural-language schedule is not allowed — store canonical cron only"
|
|
286
|
-
else
|
|
287
|
-
err "$file" "schedule" "invalid cron — expected 5 fields (minute hour dom month dow), got ${#sf[@]}"
|
|
288
|
-
fi
|
|
289
|
-
else
|
|
290
|
-
validate_cron "$file" "$schedule"
|
|
291
|
-
fi
|
|
292
|
-
fi
|
|
293
|
-
|
|
294
|
-
[ -n "$timezone" ] && validate_timezone "$file" "$timezone"
|
|
295
|
-
|
|
296
|
-
case "$runtime" in
|
|
297
|
-
'' ) ;;
|
|
298
|
-
claude|codex) ;;
|
|
299
|
-
*) err "$file" "runtime" "unknown runtime '$runtime' (allowed: claude|codex)" ;;
|
|
300
|
-
esac
|
|
301
|
-
|
|
302
|
-
# exec
|
|
303
|
-
local exec_type prompt skill
|
|
304
|
-
exec_type="$(json_type '.exec' "$json")"
|
|
305
|
-
if [ "$exec_type" = "null" ]; then
|
|
306
|
-
err "$file" "exec" "missing required field"
|
|
307
|
-
elif [ "$exec_type" != "object" ]; then
|
|
308
|
-
err "$file" "exec" "must be an object with prompt or skill"
|
|
309
|
-
else
|
|
310
|
-
prompt="$(jq -r '.exec.prompt // empty' <<<"$json")"
|
|
311
|
-
skill="$(jq -r '.exec.skill // empty' <<<"$json")"
|
|
312
|
-
if [ -z "$prompt" ] && [ -z "$skill" ]; then
|
|
313
|
-
err "$file" "exec" "must set prompt or skill"
|
|
314
|
-
elif [ -n "$prompt" ] && [ -n "$skill" ]; then
|
|
315
|
-
err "$file" "exec" "set exactly one of prompt or skill, not both"
|
|
316
|
-
fi
|
|
317
|
-
[ -n "$prompt" ] && scan_string_for_secrets "$file" "exec.prompt" "$prompt"
|
|
318
|
-
[ -n "$skill" ] && scan_string_for_secrets "$file" "exec.skill" "$skill"
|
|
319
|
-
# scan args values
|
|
320
|
-
if jq -e '.exec.args != null' <<<"$json" >/dev/null 2>&1; then
|
|
321
|
-
if [ "$(json_type '.exec.args' "$json")" != "object" ]; then
|
|
322
|
-
err "$file" "exec.args" "must be a mapping when present"
|
|
323
|
-
else
|
|
324
|
-
while IFS= read -r aval; do
|
|
325
|
-
[ -z "$aval" ] && continue
|
|
326
|
-
scan_string_for_secrets "$file" "exec.args" "$aval"
|
|
327
|
-
done < <(jq -r '.exec.args | .. | strings' <<<"$json" 2>/dev/null)
|
|
328
|
-
fi
|
|
329
|
-
fi
|
|
330
|
-
surface="$(jq -r '.exec.surface // empty' <<<"$json")"
|
|
331
|
-
case "$surface" in
|
|
332
|
-
''|headless|remote) ;;
|
|
333
|
-
*) err "$file" "exec.surface" "unknown surface '$surface' (allowed: headless|remote)" ;;
|
|
334
|
-
esac
|
|
335
|
-
# reject unknown exec keys
|
|
336
|
-
while IFS= read -r ekey; do
|
|
337
|
-
[ -z "$ekey" ] && continue
|
|
338
|
-
case "$ekey" in
|
|
339
|
-
prompt|skill|args|surface) ;;
|
|
340
|
-
*) err "$file" "exec.$ekey" "unknown exec key (allowed: prompt, skill, args, surface)" ;;
|
|
341
|
-
esac
|
|
342
|
-
done < <(jq -r '.exec | keys[]' <<<"$json")
|
|
343
|
-
fi
|
|
344
|
-
|
|
345
|
-
# timeout
|
|
346
|
-
if [ -n "$timeout" ]; then
|
|
347
|
-
if [[ ! "$timeout" =~ ^[0-9]+$ ]]; then
|
|
348
|
-
err "$file" "timeout_seconds" "must be an integer, got '$timeout'"
|
|
349
|
-
else
|
|
350
|
-
if ((timeout < 60 || timeout > 14400)); then
|
|
351
|
-
err "$file" "timeout_seconds" "must be between 60 and 14400, got $timeout"
|
|
352
|
-
fi
|
|
353
|
-
fi
|
|
354
|
-
fi
|
|
355
|
-
|
|
356
|
-
case "$notify" in
|
|
357
|
-
'' ) ;;
|
|
358
|
-
dm|none|profile) ;;
|
|
359
|
-
*) err "$file" "notify" "unknown notify '$notify' (allowed: dm|none|profile)" ;;
|
|
360
|
-
esac
|
|
361
|
-
|
|
362
|
-
case "$enabled" in
|
|
363
|
-
'' ) ;;
|
|
364
|
-
true|false) ;;
|
|
365
|
-
*)
|
|
366
|
-
# yq may emit boolean; jq -r on bool gives true/false — if someone used string "yes"
|
|
367
|
-
err "$file" "enabled" "must be boolean true|false, got '$enabled'"
|
|
368
|
-
;;
|
|
369
|
-
esac
|
|
370
|
-
|
|
371
|
-
if [ -n "$created_at" ]; then
|
|
372
|
-
if [[ ! "$created_at" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(Z|[+-][0-9]{2}:[0-9]{2})$ ]]; then
|
|
373
|
-
err "$file" "created_at" "must be ISO-8601 datetime, got '$created_at'"
|
|
374
|
-
fi
|
|
375
|
-
fi
|
|
376
|
-
|
|
377
|
-
# requirements — fixed vocabulary
|
|
378
|
-
if jq -e '.requirements != null' <<<"$json" >/dev/null 2>&1; then
|
|
379
|
-
if [ "$(json_type '.requirements' "$json")" != "object" ]; then
|
|
380
|
-
err "$file" "requirements" "must be a mapping"
|
|
381
|
-
else
|
|
382
|
-
local key
|
|
383
|
-
while IFS= read -r key; do
|
|
384
|
-
[ -z "$key" ] && continue
|
|
385
|
-
case "$key" in
|
|
386
|
-
runtime|secrets|company|cwd|tools) ;;
|
|
387
|
-
readiness|last_probe|last_run|failure_class|pending_probe)
|
|
388
|
-
# status-shaped keys under requirements are unknown requirements keys
|
|
389
|
-
err "$file" "requirements.$key" "unknown requirements key (status belongs in hq-pro, not requirements)"
|
|
390
|
-
;;
|
|
391
|
-
*) err "$file" "requirements.$key" "unknown requirements key (allowed: runtime, secrets, company, cwd, tools)" ;;
|
|
392
|
-
esac
|
|
393
|
-
done < <(jq -r '.requirements | keys[]' <<<"$json")
|
|
394
|
-
|
|
395
|
-
local req_runtime
|
|
396
|
-
req_runtime="$(jq -r '.requirements.runtime // empty' <<<"$json")"
|
|
397
|
-
if [ -n "$req_runtime" ]; then
|
|
398
|
-
case "$req_runtime" in
|
|
399
|
-
claude|codex) ;;
|
|
400
|
-
*) err "$file" "requirements.runtime" "unknown runtime '$req_runtime' (allowed: claude|codex)" ;;
|
|
401
|
-
esac
|
|
402
|
-
if [ -n "$runtime" ] && [ "$req_runtime" != "$runtime" ]; then
|
|
403
|
-
err "$file" "requirements.runtime" "mismatches top-level runtime ('$req_runtime' vs '$runtime')"
|
|
404
|
-
fi
|
|
405
|
-
fi
|
|
406
|
-
|
|
407
|
-
if jq -e '.requirements.secrets != null' <<<"$json" >/dev/null 2>&1; then
|
|
408
|
-
if [ "$(json_type '.requirements.secrets' "$json")" != "array" ]; then
|
|
409
|
-
err "$file" "requirements.secrets" "must be an array of vault key names"
|
|
410
|
-
else
|
|
411
|
-
local secret
|
|
412
|
-
while IFS= read -r secret; do
|
|
413
|
-
[ -z "$secret" ] && continue
|
|
414
|
-
scan_string_for_secrets "$file" "requirements.secrets" "$secret"
|
|
415
|
-
if ! looks_like_secret_name_ok "$secret"; then
|
|
416
|
-
# If it already matched a secret pattern, we still want the name-shape error for odd values
|
|
417
|
-
err "$file" "requirements.secrets" "invalid vault key name '$secret' (use names only)"
|
|
418
|
-
fi
|
|
419
|
-
# Heuristic: KEY=value or PEM-ish or long high-entropy blob
|
|
420
|
-
if [[ "$secret" == *"="* ]]; then
|
|
421
|
-
err "$file" "requirements.secrets" "looks like KEY=value inline credential — names only"
|
|
422
|
-
fi
|
|
423
|
-
done < <(jq -r '.requirements.secrets[]?' <<<"$json")
|
|
424
|
-
fi
|
|
425
|
-
fi
|
|
426
|
-
|
|
427
|
-
if jq -e '.requirements.tools != null' <<<"$json" >/dev/null 2>&1; then
|
|
428
|
-
if [ "$(json_type '.requirements.tools' "$json")" != "array" ]; then
|
|
429
|
-
err "$file" "requirements.tools" "must be an array of strings"
|
|
430
|
-
fi
|
|
431
|
-
fi
|
|
432
|
-
|
|
433
|
-
local req_cwd
|
|
434
|
-
req_cwd="$(jq -r '.requirements.cwd // empty' <<<"$json")"
|
|
435
|
-
if [ -n "$req_cwd" ]; then
|
|
436
|
-
assert_cwd_safe "$file" "requirements.cwd" "$req_cwd"
|
|
437
|
-
fi
|
|
438
|
-
|
|
439
|
-
local req_company
|
|
440
|
-
req_company="$(jq -r '.requirements.company // empty' <<<"$json")"
|
|
441
|
-
if [ -n "$req_company" ]; then
|
|
442
|
-
if [[ ! "$req_company" =~ ^[a-z0-9]([a-z0-9_-]*[a-z0-9])?$ ]]; then
|
|
443
|
-
err "$file" "requirements.company" "invalid company slug '$req_company' (lowercase kebab/underscore)"
|
|
444
|
-
fi
|
|
445
|
-
fi
|
|
446
|
-
fi
|
|
447
|
-
fi
|
|
448
|
-
|
|
449
|
-
# Top-level cwd (legacy) — same containment rules
|
|
450
|
-
local top_cwd
|
|
451
|
-
top_cwd="$(jq -r '.cwd // empty' <<<"$json")"
|
|
452
|
-
if [ -n "$top_cwd" ]; then
|
|
453
|
-
assert_cwd_safe "$file" "cwd" "$top_cwd"
|
|
454
|
-
fi
|
|
455
|
-
|
|
456
|
-
# Top-level readiness is informational only — ignore (no error). Documented SoT is hq-pro.
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
# Expand args → file list
|
|
460
|
-
FILES=()
|
|
461
|
-
for arg in "$@"; do
|
|
462
|
-
while IFS= read -r f; do
|
|
463
|
-
[ -z "$f" ] && continue
|
|
464
|
-
FILES+=("$f")
|
|
465
|
-
done < <(collect_files "$arg")
|
|
466
|
-
done
|
|
467
|
-
|
|
468
|
-
if [ "${#FILES[@]}" -eq 0 ]; then
|
|
469
|
-
echo "jobs-validate: no job YAML files found" >&2
|
|
470
|
-
exit 1
|
|
471
|
-
fi
|
|
472
|
-
|
|
473
|
-
for f in "${FILES[@]}"; do
|
|
474
|
-
validate_job_file "$f"
|
|
475
|
-
done
|
|
476
|
-
|
|
477
|
-
if [ "$ERRORS" -gt 0 ]; then
|
|
478
|
-
echo "jobs-validate: $ERRORS error(s)" >&2
|
|
479
|
-
exit 1
|
|
480
|
-
fi
|
|
481
|
-
|
|
482
|
-
exit 0
|