loki-mode 11.75.6 → 11.76.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/api/openapi.yaml +8 -0
- package/api/routes/memory.ts +33 -30
- package/api/types/memory.ts +5 -4
- package/autonomy/checklist-verify.py +55 -1
- package/autonomy/edit.sh +17 -0
- package/autonomy/loki +86 -16
- package/autonomy/prd-checklist.sh +38 -2
- package/autonomy/receipt_jwt.py +43 -6
- package/autonomy/run.sh +35 -4
- package/autonomy/verify.sh +19 -0
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +42 -26
- package/docs/CLI-REFERENCE.md +3 -2
- package/docs/INSTALLATION.md +1 -1
- package/loki-ts/dist/loki.js +1331 -1321
- package/mcp/__init__.py +1 -1
- package/package.json +2 -2
- package/packages/control-plane/dist/server.js +69505 -8712
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/claude.sh +23 -0
- package/web-app/dist/assets/{AdminPage-BoaRLMil.js → AdminPage-DoGPqZIa.js} +1 -1
- package/web-app/dist/assets/{Avatar-Dd_pV0UU.js → Avatar-Dyf7xJsz.js} +1 -1
- package/web-app/dist/assets/{Badge-LiBd-DlU.js → Badge-Cm4dADew.js} +1 -1
- package/web-app/dist/assets/{Button-B3B2cJ-0.js → Button-LGxjldY5.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-Ca-gXMlA.js → CockpitPage-DuC9g48J.js} +1 -1
- package/web-app/dist/assets/{ComparePage-S18NXnII.js → ComparePage-DuTrmedK.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-4imoxiMa.js → ErrorBoundary-B0f7NEiT.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-D5MvpcDl.js → EvidenceReceiptPanel-DsE8B9Hw.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-BA5R0l4j.js → GitHubIssuesPanel-aYflOqd4.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-BiNJCqqV.js → GitHubPRsPanel-CCJGck6c.js} +1 -1
- package/web-app/dist/assets/{HomePage-D5vRS7cg.js → HomePage-BJU3PVkJ.js} +2 -2
- package/web-app/dist/assets/{LoginPage-BuH6dIES.js → LoginPage-UzYoF5CJ.js} +1 -1
- package/web-app/dist/assets/{MagicPage-H94Rvqjy.js → MagicPage-5X3hQ7lt.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-zsUDLUJt.js → MetricsPage-CFsTNyOu.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-BQf9w47T.js → NotFoundPage-Ba0nMlqG.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-DrxGycYT.js → ProjectPage-fvU-EYZb.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-BH-hu8UL.js → ProjectsPage-CVEKCorG.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-DA3h82NI.js → SettingsPage-JGbsNmqD.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-Bpe3ZH2B.js → ShowcasePage-Dr2c_V-y.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-BzITDnQr.js → SystemSettingsPage-D9dCc4Ir.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-Db-8W8AZ.js → TeamsPage-B56u-bod.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-DbAeutYo.js → TemplatesPage-CUwqj6ge.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-CBR2hl5j.js → TerminalOutput-Cm920SDi.js} +1 -1
- package/web-app/dist/assets/{activity-D6fSpsvo.js → activity-DDF6juD-.js} +1 -1
- package/web-app/dist/assets/{bell-Ct3zW8ea.js → bell-7MW_oAcb.js} +1 -1
- package/web-app/dist/assets/{bot-BPL0lAoo.js → bot-b63uz2Ov.js} +1 -1
- package/web-app/dist/assets/{check-BQIoOySs.js → check-2oBMAALa.js} +1 -1
- package/web-app/dist/assets/{chevron-left-X9GEFQhb.js → chevron-left-UuhI6pbU.js} +1 -1
- package/web-app/dist/assets/{circle-alert-BlYrEM-R.js → circle-alert-BikRzQA7.js} +1 -1
- package/web-app/dist/assets/{clock-YoKF5Zcs.js → clock-BdkTaAgO.js} +1 -1
- package/web-app/dist/assets/{cloud-BJ84R6Qb.js → cloud-oKg24kTO.js} +1 -1
- package/web-app/dist/assets/{code-xml-DOXjy7X0.js → code-xml-CpDmdnYQ.js} +1 -1
- package/web-app/dist/assets/{database-D_8zZu1l.js → database-BT133u2w.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-D9L0YTw4.js → dollar-sign-khUEgmXm.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-CQZ4_xnj.js → file-code-corner-_TlPqN5g.js} +1 -1
- package/web-app/dist/assets/{file-plus-CkZLRceI.js → file-plus-B5UT0SZU.js} +1 -1
- package/web-app/dist/assets/{globe-BCBfBPhx.js → globe-Chhrgg2K.js} +1 -1
- package/web-app/dist/assets/{hammer-DoEYrFgV.js → hammer-DJ6Z4tiA.js} +1 -1
- package/web-app/dist/assets/{index-DYmM5Sgt.js → index-BprNVRFx.js} +3 -3
- package/web-app/dist/assets/{layers-DaOw10q2.js → layers-CtYWuenz.js} +1 -1
- package/web-app/dist/assets/{loader-circle-Cg12Ljup.js → loader-circle-BRxnh_Va.js} +1 -1
- package/web-app/dist/assets/{lock-BKNqJcEs.js → lock-btaZm2bN.js} +1 -1
- package/web-app/dist/assets/{package-CjdYtATT.js → package-BFU4d6gL.js} +1 -1
- package/web-app/dist/assets/{plus-DF7JroHt.js → plus-BogXrZQi.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-DP_YdlTm.js → refresh-cw-CsdQkR5n.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-B0x_C7lj.js → rotate-ccw-BeLxChlE.js} +1 -1
- package/web-app/dist/assets/{scroll-text-Slp-l4Ku.js → scroll-text-DAXSJc0i.js} +1 -1
- package/web-app/dist/assets/{server-BFtsx7u6.js → server-Cf_yJUqU.js} +1 -1
- package/web-app/dist/assets/{shield-alert-cWCzZfwF.js → shield-alert-B2gh1hM3.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-wM1ZuF0J.js → thumbs-up-DxYjJ1v8.js} +1 -1
- package/web-app/dist/assets/{trash-2-DLrOT3XY.js → trash-2-BaAsSOYk.js} +1 -1
- package/web-app/dist/assets/{trending-down-DRie269l.js → trending-down-Kc-Tady7.js} +1 -1
- package/web-app/dist/assets/{trending-up-CVQxrVnQ.js → trending-up-BDsns895.js} +1 -1
- package/web-app/dist/assets/{upload-BQ9VTjue.js → upload-CCJzLwO9.js} +1 -1
- package/web-app/dist/assets/{usePolling-CbdUuSku.js → usePolling-4aqc_A7F.js} +1 -1
- package/web-app/dist/assets/{user-DzjUWhme.js → user-BzoHXLDe.js} +1 -1
- package/web-app/dist/index.html +1 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Runs an autonomous software factory that knows what it is supposed to deliver and proves it did, with a signed receipt. Use when the user says Loki Mode or asks to build, fix or verify software autonomously.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v11.
|
|
6
|
+
# Loki Mode v11.76.0
|
|
7
7
|
|
|
8
8
|
**Current entry points (Loki 10 engine):** `loki "<task>"`, `loki owner/repo#N` and `loki quick "<task>"` run the Loki 10 engine and end with a signed receipt (`loki verify`). `loki backlog owner/repo --all` runs many issues in parallel. Guide: `docs/v10/GUIDE.md`. The skill instructions below, and `loki start`, describe the previous bash loop. PRD-file starts (`loki start ./prd.md`) and the opencode provider still run on it until FC38-SWEEP ports them; the legacy-engine command and the LOKI_ENGINE switch remain removed. Prefer `loki owner/repo#N` for issues.
|
|
9
9
|
|
|
@@ -475,4 +475,4 @@ Implemented, planned and deprecated items, with the version each landed in: `ski
|
|
|
475
475
|
|
|
476
476
|
---
|
|
477
477
|
|
|
478
|
-
**v11.
|
|
478
|
+
**v11.76.0 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
11.
|
|
1
|
+
11.76.0
|
package/api/openapi.yaml
CHANGED
|
@@ -1166,12 +1166,16 @@ components:
|
|
|
1166
1166
|
properties:
|
|
1167
1167
|
discoveryTokens:
|
|
1168
1168
|
type: integer
|
|
1169
|
+
nullable: true
|
|
1169
1170
|
readTokens:
|
|
1170
1171
|
type: integer
|
|
1172
|
+
nullable: true
|
|
1171
1173
|
ratio:
|
|
1172
1174
|
type: number
|
|
1175
|
+
nullable: true
|
|
1173
1176
|
savingsPercent:
|
|
1174
1177
|
type: number
|
|
1178
|
+
nullable: true
|
|
1175
1179
|
|
|
1176
1180
|
RetrieveResponse:
|
|
1177
1181
|
type: object
|
|
@@ -1194,12 +1198,16 @@ components:
|
|
|
1194
1198
|
properties:
|
|
1195
1199
|
discoveryTokens:
|
|
1196
1200
|
type: integer
|
|
1201
|
+
nullable: true
|
|
1197
1202
|
readTokens:
|
|
1198
1203
|
type: integer
|
|
1204
|
+
nullable: true
|
|
1199
1205
|
ratio:
|
|
1200
1206
|
type: number
|
|
1207
|
+
nullable: true
|
|
1201
1208
|
savingsPercent:
|
|
1202
1209
|
type: number
|
|
1210
|
+
nullable: true
|
|
1203
1211
|
|
|
1204
1212
|
security:
|
|
1205
1213
|
- {}
|
package/api/routes/memory.ts
CHANGED
|
@@ -41,6 +41,24 @@ import {
|
|
|
41
41
|
// Base path for memory storage
|
|
42
42
|
const MEMORY_BASE_PATH = ".loki/memory";
|
|
43
43
|
|
|
44
|
+
// Python helper shared by the token-economics scripts. A session that recorded
|
|
45
|
+
// nothing (all counters zero) is unmeasured: every metric is null, never 0 or
|
|
46
|
+
// a 100% saving. Once anything is recorded the measured values pass through.
|
|
47
|
+
const PY_TOKEN_METRICS = `
|
|
48
|
+
def token_metrics_from(summary):
|
|
49
|
+
m = summary.get('metrics', {})
|
|
50
|
+
keys = ('discovery_tokens', 'read_tokens', 'layer1_loads', 'layer2_loads',
|
|
51
|
+
'layer3_loads', 'cache_hits', 'cache_misses')
|
|
52
|
+
if not any((m.get(k) or 0) > 0 for k in keys):
|
|
53
|
+
return {'discoveryTokens': None, 'readTokens': None, 'ratio': None, 'savingsPercent': None}
|
|
54
|
+
return {
|
|
55
|
+
'discoveryTokens': m.get('discovery_tokens'),
|
|
56
|
+
'readTokens': m.get('read_tokens'),
|
|
57
|
+
'ratio': summary.get('ratio'),
|
|
58
|
+
'savingsPercent': summary.get('savings_percent')
|
|
59
|
+
}
|
|
60
|
+
`;
|
|
61
|
+
|
|
44
62
|
// Default subprocess timeout (30 seconds)
|
|
45
63
|
const SUBPROCESS_TIMEOUT_MS = 30000;
|
|
46
64
|
|
|
@@ -181,6 +199,7 @@ import json
|
|
|
181
199
|
sys.path.insert(0, '.')
|
|
182
200
|
from memory.engine import MemoryEngine
|
|
183
201
|
from memory.token_economics import TokenEconomics
|
|
202
|
+
${PY_TOKEN_METRICS}
|
|
184
203
|
|
|
185
204
|
engine = MemoryEngine('${MEMORY_BASE_PATH}')
|
|
186
205
|
stats = engine.get_stats()
|
|
@@ -195,12 +214,7 @@ try:
|
|
|
195
214
|
te = TokenEconomics('api-session', '${MEMORY_BASE_PATH}')
|
|
196
215
|
te.load()
|
|
197
216
|
summary = te.get_summary()
|
|
198
|
-
token_metrics =
|
|
199
|
-
'discoveryTokens': summary['metrics'].get('discovery_tokens', 0),
|
|
200
|
-
'readTokens': summary['metrics'].get('read_tokens', 0),
|
|
201
|
-
'ratio': summary.get('ratio', 0),
|
|
202
|
-
'savingsPercent': summary.get('savings_percent', 100)
|
|
203
|
-
}
|
|
217
|
+
token_metrics = token_metrics_from(summary)
|
|
204
218
|
except:
|
|
205
219
|
token_metrics = None
|
|
206
220
|
|
|
@@ -746,6 +760,7 @@ from memory.engine import MemoryEngine
|
|
|
746
760
|
from memory.retrieval import MemoryRetrieval
|
|
747
761
|
from memory.storage import MemoryStorage
|
|
748
762
|
from memory.token_economics import TokenEconomics
|
|
763
|
+
${PY_TOKEN_METRICS}
|
|
749
764
|
|
|
750
765
|
storage = MemoryStorage('${MEMORY_BASE_PATH}')
|
|
751
766
|
engine = MemoryEngine(storage=storage, base_path='${MEMORY_BASE_PATH}')
|
|
@@ -775,19 +790,9 @@ try:
|
|
|
775
790
|
te = TokenEconomics('retrieve-session', '${MEMORY_BASE_PATH}')
|
|
776
791
|
te.load()
|
|
777
792
|
summary = te.get_summary()
|
|
778
|
-
token_metrics =
|
|
779
|
-
'discoveryTokens': summary['metrics'].get('discovery_tokens', 0),
|
|
780
|
-
'readTokens': summary['metrics'].get('read_tokens', 0),
|
|
781
|
-
'ratio': summary.get('ratio', 0),
|
|
782
|
-
'savingsPercent': summary.get('savings_percent', 100)
|
|
783
|
-
}
|
|
793
|
+
token_metrics = token_metrics_from(summary)
|
|
784
794
|
except:
|
|
785
|
-
token_metrics = {
|
|
786
|
-
'discoveryTokens': 0,
|
|
787
|
-
'readTokens': 0,
|
|
788
|
-
'ratio': 0,
|
|
789
|
-
'savingsPercent': 100
|
|
790
|
-
}
|
|
795
|
+
token_metrics = token_metrics_from({})
|
|
791
796
|
|
|
792
797
|
result = {
|
|
793
798
|
'memories': memories,
|
|
@@ -840,10 +845,10 @@ print(json.dumps(result, default=str))
|
|
|
840
845
|
const emptyResponse: RetrieveResponse = {
|
|
841
846
|
memories: [],
|
|
842
847
|
tokenMetrics: {
|
|
843
|
-
discoveryTokens:
|
|
844
|
-
readTokens:
|
|
845
|
-
ratio:
|
|
846
|
-
savingsPercent:
|
|
848
|
+
discoveryTokens: null,
|
|
849
|
+
readTokens: null,
|
|
850
|
+
ratio: null,
|
|
851
|
+
savingsPercent: null,
|
|
847
852
|
},
|
|
848
853
|
};
|
|
849
854
|
return successResponse(emptyResponse);
|
|
@@ -949,6 +954,7 @@ import sys
|
|
|
949
954
|
import json
|
|
950
955
|
sys.path.insert(0, '.')
|
|
951
956
|
from memory.token_economics import TokenEconomics
|
|
957
|
+
${PY_TOKEN_METRICS}
|
|
952
958
|
|
|
953
959
|
te = TokenEconomics('api-session', '${MEMORY_BASE_PATH}')
|
|
954
960
|
te.load()
|
|
@@ -957,10 +963,7 @@ summary = te.get_summary()
|
|
|
957
963
|
result = {
|
|
958
964
|
'sessionId': summary.get('session_id', 'unknown'),
|
|
959
965
|
'startedAt': summary.get('started_at', ''),
|
|
960
|
-
|
|
961
|
-
'readTokens': summary['metrics'].get('read_tokens', 0),
|
|
962
|
-
'ratio': summary.get('ratio', 0),
|
|
963
|
-
'savingsPercent': summary.get('savings_percent', 100),
|
|
966
|
+
**token_metrics_from(summary),
|
|
964
967
|
'layer1Loads': summary['metrics'].get('layer1_loads', 0),
|
|
965
968
|
'layer2Loads': summary['metrics'].get('layer2_loads', 0),
|
|
966
969
|
'layer3Loads': summary['metrics'].get('layer3_loads', 0),
|
|
@@ -991,10 +994,10 @@ print(json.dumps(result))
|
|
|
991
994
|
const defaultEconomics: TokenEconomicsDetail = {
|
|
992
995
|
sessionId: "none",
|
|
993
996
|
startedAt: new Date().toISOString(),
|
|
994
|
-
discoveryTokens:
|
|
995
|
-
readTokens:
|
|
996
|
-
ratio:
|
|
997
|
-
savingsPercent:
|
|
997
|
+
discoveryTokens: null,
|
|
998
|
+
readTokens: null,
|
|
999
|
+
ratio: null,
|
|
1000
|
+
savingsPercent: null,
|
|
998
1001
|
layer1Loads: 0,
|
|
999
1002
|
layer2Loads: 0,
|
|
1000
1003
|
layer3Loads: 0,
|
package/api/types/memory.ts
CHANGED
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
// -----------------------------------------------------------------------------
|
|
10
10
|
|
|
11
11
|
export interface TokenMetrics {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
// null until the session recorded token usage; never a fabricated 0 or 100
|
|
13
|
+
discoveryTokens: number | null;
|
|
14
|
+
readTokens: number | null;
|
|
15
|
+
ratio: number | null;
|
|
16
|
+
savingsPercent: number | null;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export interface ThresholdAction {
|
|
@@ -16,10 +16,12 @@ Timeout = item stays 'pending' (not 'failed') to prevent false failures.
|
|
|
16
16
|
Atomic writes: temp file + os.replace() to never produce partial JSON.
|
|
17
17
|
|
|
18
18
|
Usage:
|
|
19
|
-
python3 checklist-verify.py [--checklist PATH] [--timeout SECS]
|
|
19
|
+
python3 checklist-verify.py [--checklist PATH] [--timeout SECS] (--freeze | --expect-digest SHA256)
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
22
|
import argparse
|
|
23
|
+
import hashlib
|
|
24
|
+
import hmac
|
|
23
25
|
import json
|
|
24
26
|
import os
|
|
25
27
|
import re
|
|
@@ -422,6 +424,27 @@ def determine_item_status(verifications: list) -> str:
|
|
|
422
424
|
return "pending"
|
|
423
425
|
|
|
424
426
|
|
|
427
|
+
_DIGEST_RE = re.compile(r"^[0-9a-f]{64}$")
|
|
428
|
+
|
|
429
|
+
# Result fields the verifier writes back; never part of a check definition.
|
|
430
|
+
_RESULT_FIELDS = ("passed", "output", "status", "verified_at")
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def definition_digest(checklist: dict) -> str:
|
|
434
|
+
"""SHA-256 over check definitions only (item ids + verification specs)."""
|
|
435
|
+
defs = []
|
|
436
|
+
for category in checklist.get("categories", []):
|
|
437
|
+
for item in category.get("items", []):
|
|
438
|
+
checks = [
|
|
439
|
+
{k: v for k, v in c.items() if k not in _RESULT_FIELDS}
|
|
440
|
+
for c in item.get("verification", [])
|
|
441
|
+
if isinstance(c, dict)
|
|
442
|
+
]
|
|
443
|
+
defs.append({"id": item.get("id", ""), "verification": checks})
|
|
444
|
+
blob = json.dumps(defs, sort_keys=True, separators=(",", ":"))
|
|
445
|
+
return hashlib.sha256(blob.encode("utf-8")).hexdigest()
|
|
446
|
+
|
|
447
|
+
|
|
425
448
|
def atomic_write_json(path: str, data: dict) -> None:
|
|
426
449
|
"""Write JSON atomically via temp file + os.replace()."""
|
|
427
450
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
@@ -454,6 +477,17 @@ def main():
|
|
|
454
477
|
default=30,
|
|
455
478
|
help="Timeout per check in seconds (default: 30)",
|
|
456
479
|
)
|
|
480
|
+
parser.add_argument(
|
|
481
|
+
"--freeze",
|
|
482
|
+
action="store_true",
|
|
483
|
+
help="Print the digest of the check definitions and exit; writes nothing",
|
|
484
|
+
)
|
|
485
|
+
parser.add_argument(
|
|
486
|
+
"--expect-digest",
|
|
487
|
+
default=None,
|
|
488
|
+
help="Required to verify: the definition digest the harness froze; "
|
|
489
|
+
"a mismatch, or a missing or malformed value, fails closed",
|
|
490
|
+
)
|
|
457
491
|
args = parser.parse_args()
|
|
458
492
|
|
|
459
493
|
checklist_path = args.checklist
|
|
@@ -468,6 +502,26 @@ def main():
|
|
|
468
502
|
print(f"Failed to read checklist: {e}", file=sys.stderr)
|
|
469
503
|
sys.exit(1)
|
|
470
504
|
|
|
505
|
+
if args.freeze:
|
|
506
|
+
print(definition_digest(checklist))
|
|
507
|
+
sys.exit(0)
|
|
508
|
+
|
|
509
|
+
expected = args.expect_digest
|
|
510
|
+
if expected is None or not _DIGEST_RE.match(expected):
|
|
511
|
+
print(
|
|
512
|
+
"Refusing to verify: --expect-digest is missing or not a sha256 hex "
|
|
513
|
+
"digest (fail closed)",
|
|
514
|
+
file=sys.stderr,
|
|
515
|
+
)
|
|
516
|
+
sys.exit(2)
|
|
517
|
+
if not hmac.compare_digest(expected, definition_digest(checklist)):
|
|
518
|
+
print(
|
|
519
|
+
"Checklist hash mismatch: check definitions differ from the frozen "
|
|
520
|
+
"digest; refusing to verify",
|
|
521
|
+
file=sys.stderr,
|
|
522
|
+
)
|
|
523
|
+
sys.exit(3)
|
|
524
|
+
|
|
471
525
|
project_dir = os.getcwd()
|
|
472
526
|
now = datetime.now(timezone.utc).isoformat()
|
|
473
527
|
|
package/autonomy/edit.sh
CHANGED
|
@@ -73,6 +73,14 @@ cmd_edit() {
|
|
|
73
73
|
return "$_rc"
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
# One "<content-hash> <path>" line per untracked, non-ignored file.
|
|
77
|
+
_loki_edit_untracked_hashes() {
|
|
78
|
+
local f
|
|
79
|
+
git ls-files --others --exclude-standard | while IFS= read -r f; do
|
|
80
|
+
[ -f "$f" ] && printf '%s %s\n' "$(git hash-object -- "$f" 2>/dev/null)" "$f"
|
|
81
|
+
done
|
|
82
|
+
}
|
|
83
|
+
|
|
76
84
|
_cmd_edit_run() {
|
|
77
85
|
local do_verify=false do_receipt=false do_commit=false dry=false
|
|
78
86
|
local paths=() change=""
|
|
@@ -125,6 +133,8 @@ _cmd_edit_run() {
|
|
|
125
133
|
local before_head before_status
|
|
126
134
|
before_head="$(git rev-parse HEAD 2>/dev/null || echo none)"
|
|
127
135
|
before_status="$(git status --porcelain)"
|
|
136
|
+
local before_untracked
|
|
137
|
+
before_untracked="$(_loki_edit_untracked_hashes)"
|
|
128
138
|
|
|
129
139
|
local prompt scope=""
|
|
130
140
|
if [ "${#paths[@]}" -gt 0 ]; then
|
|
@@ -184,6 +194,13 @@ print("%s|%s|%s" % (c,err,(d.get("result") or "").strip().replace("\n"," ")[:400
|
|
|
184
194
|
# Files touched by this pass: status delta against the pre-run snapshot.
|
|
185
195
|
local touched
|
|
186
196
|
touched="$(git status --porcelain | grep -vxF -f <(printf '%s\n' "$before_status") 2>/dev/null | sed 's/^...//' || true)"
|
|
197
|
+
# A file untracked before the run keeps the same porcelain line when edited,
|
|
198
|
+
# so also pick up untracked files whose content hash changed.
|
|
199
|
+
local changed_untracked
|
|
200
|
+
changed_untracked="$(_loki_edit_untracked_hashes | grep -vxF -f <(printf '%s\n' "$before_untracked") 2>/dev/null | sed 's/^[^ ]* //' || true)"
|
|
201
|
+
if [ -n "$changed_untracked" ]; then
|
|
202
|
+
touched="$(printf '%s\n%s\n' "$touched" "$changed_untracked" | sed '/^$/d' | sort -u)"
|
|
203
|
+
fi
|
|
187
204
|
[ -n "$touched" ] || touched="$(git diff --name-only)"
|
|
188
205
|
|
|
189
206
|
if [ -z "$touched" ]; then
|
package/autonomy/loki
CHANGED
|
@@ -989,7 +989,7 @@ import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
|
989
989
|
import json, sys, hashlib, urllib.request
|
|
990
990
|
sys.path.insert(0, sys.argv[3])
|
|
991
991
|
try:
|
|
992
|
-
from receipt_jwt import verify_attestation, check_seal_meta, _CRYPTO_AVAILABLE
|
|
992
|
+
from receipt_jwt import verify_attestation, keyset_problem, check_seal_meta, _CRYPTO_AVAILABLE
|
|
993
993
|
except ImportError:
|
|
994
994
|
sys.exit(1) # -> "" : cannot check, never a verdict
|
|
995
995
|
proof = json.load(open(sys.argv[1]))
|
|
@@ -1009,7 +1009,8 @@ except Exception:
|
|
|
1009
1009
|
# A key set that is not {"keys": [{...}, ...]} -- INCLUDING an empty list -- is
|
|
1010
1010
|
# a key-set problem: UNCHECKED, never an accusation against the receipt. An
|
|
1011
1011
|
# empty keyset is an inability to check (no key was published to check
|
|
1012
|
-
# against), not evidence of tampering.
|
|
1012
|
+
# against), not evidence of tampering. The same holds below for a set that
|
|
1013
|
+
# gives zero or several entries, or a non-EdDSA alg, for the token's kid. Checked BEFORE anything below can print
|
|
1013
1014
|
# "bad", so a bad or empty key set can never read as TAMPERED.
|
|
1014
1015
|
_keys = jwks.get("keys") if isinstance(jwks, dict) else None
|
|
1015
1016
|
if not isinstance(_keys, list) or not _keys or not all(isinstance(k, dict) for k in _keys):
|
|
@@ -1024,9 +1025,16 @@ if not isinstance(tok, str):
|
|
|
1024
1025
|
# dependency is UNCHECKED. Same guard in loki_proof_attestation_check below.
|
|
1025
1026
|
if not _CRYPTO_AVAILABLE:
|
|
1026
1027
|
sys.exit(1)
|
|
1027
|
-
#
|
|
1028
|
-
#
|
|
1029
|
-
|
|
1028
|
+
# A key set that cannot give exactly one usable key for the token's kid is a
|
|
1029
|
+
# key-set problem, not an accusation: UNCHECKED.
|
|
1030
|
+
try:
|
|
1031
|
+
if keyset_problem(tok, jwks):
|
|
1032
|
+
sys.exit(1)
|
|
1033
|
+
except Exception:
|
|
1034
|
+
print("bad"); sys.exit(0)
|
|
1035
|
+
# Any other crash inside verification comes from the token: refuse it.
|
|
1036
|
+
# Letting it propagate read as UNCHECKED, which let a forger turn TAMPERED
|
|
1037
|
+
# into "install python3 cryptography".
|
|
1030
1038
|
try:
|
|
1031
1039
|
ok, claims = verify_attestation(tok, jwks)
|
|
1032
1040
|
except Exception:
|
|
@@ -1127,7 +1135,7 @@ import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
|
1127
1135
|
import json, sys, os, hashlib, urllib.request
|
|
1128
1136
|
sys.path.insert(0, sys.argv[3])
|
|
1129
1137
|
try:
|
|
1130
|
-
from receipt_jwt import verify_attestation, check_seal_meta, _CRYPTO_AVAILABLE
|
|
1138
|
+
from receipt_jwt import verify_attestation, keyset_problem, check_seal_meta, _CRYPTO_AVAILABLE
|
|
1131
1139
|
except ImportError:
|
|
1132
1140
|
sys.exit(1) # "" : cannot check
|
|
1133
1141
|
try:
|
|
@@ -1164,7 +1172,8 @@ except Exception:
|
|
|
1164
1172
|
# a key-set problem: NOT CHECKED, never an accusation. An empty keyset is an
|
|
1165
1173
|
# inability to check (no key was published to check against), not evidence of
|
|
1166
1174
|
# tampering. Checked BEFORE anything below can print "bad", so a bad or empty
|
|
1167
|
-
# key set
|
|
1175
|
+
# key set (or one that is ambiguous or wrongly tagged for the token's kid, see
|
|
1176
|
+
# keyset_problem) can never read as FAILED. Same order in loki_remote_attestation_status
|
|
1168
1177
|
# above -- change both.
|
|
1169
1178
|
_keys = jwks.get("keys") if isinstance(jwks, dict) else None
|
|
1170
1179
|
if not isinstance(_keys, list) or not _keys or not all(isinstance(k, dict) for k in _keys):
|
|
@@ -1180,9 +1189,17 @@ if not isinstance(tok, str):
|
|
|
1180
1189
|
# loki_remote_attestation_status above -- change both.
|
|
1181
1190
|
if not _CRYPTO_AVAILABLE:
|
|
1182
1191
|
sys.exit(1) # "" : cannot check
|
|
1183
|
-
#
|
|
1184
|
-
#
|
|
1185
|
-
|
|
1192
|
+
# A key set that cannot give exactly one usable key for the token's kid is a
|
|
1193
|
+
# key-set problem, never an accusation: NOT CHECKED, with the reason on stdout.
|
|
1194
|
+
try:
|
|
1195
|
+
_ksp = keyset_problem(tok, jwks)
|
|
1196
|
+
except Exception:
|
|
1197
|
+
_ksp = None
|
|
1198
|
+
if _ksp:
|
|
1199
|
+
print("keyset: " + _ksp); sys.exit(0)
|
|
1200
|
+
# Any other crash inside verification comes from the token: refuse it.
|
|
1201
|
+
# Letting it propagate read as NOT CHECKED (exit 2), which let a forger turn
|
|
1202
|
+
# FAILED into "could not check".
|
|
1186
1203
|
try:
|
|
1187
1204
|
ok, claims = verify_attestation(tok, jwks)
|
|
1188
1205
|
except Exception:
|
|
@@ -15196,8 +15213,9 @@ EOF
|
|
|
15196
15213
|
|
|
15197
15214
|
# FC-EGRESS-AUDIT-HAND-LISTED: the registry of every env var that names an outbound
|
|
15198
15215
|
# host. _loki_airgap_audit walks it, so a configured webhook, tracker, remote or model
|
|
15199
|
-
# gateway is named in the audit. tests/test-airgap-commands.sh greps autonomy
|
|
15200
|
-
# loki-ts/src for LOKI_*_(URL|HOST|ENDPOINT|WEBHOOK)
|
|
15216
|
+
# gateway is named in the audit. tests/test-airgap-commands.sh greps autonomy/,
|
|
15217
|
+
# loki-ts/src, providers, dashboard and web-app for LOKI_*_(URL|HOST|ENDPOINT|WEBHOOK|ISSUER)
|
|
15218
|
+
# and DATABASE_URL readers and fails on any name that
|
|
15201
15219
|
# is in neither list. Entry: NAME|what it reaches.
|
|
15202
15220
|
_LOKI_AIRGAP_EGRESS_ENV=(
|
|
15203
15221
|
"LOKI_WEBHOOK_URL|custom webhook notifications"
|
|
@@ -15216,6 +15234,9 @@ _LOKI_AIRGAP_EGRESS_ENV=(
|
|
|
15216
15234
|
"JIRA_BASE_URL|Jira write-back (alias of LOKI_JIRA_URL)"
|
|
15217
15235
|
"LOKI_LINEAR_URL|Linear write-back"
|
|
15218
15236
|
"LOKI_MODEL_BASE_URL|model route base URL"
|
|
15237
|
+
"LOKI_AUDIT_SYSLOG_HOST|dashboard audit log forwarding to a remote syslog server"
|
|
15238
|
+
"LOKI_OIDC_ISSUER|dashboard OIDC sign-in (fetches the issuer discovery document and JWKS keys)"
|
|
15239
|
+
"DATABASE_URL|web-app Postgres database server (unset to use local file storage)"
|
|
15219
15240
|
)
|
|
15220
15241
|
# Names the guard skips because they never name an outbound host. NAME|reason.
|
|
15221
15242
|
_LOKI_AIRGAP_EGRESS_NOT=(
|
|
@@ -15242,6 +15263,14 @@ _LOKI_AIRGAP_EGRESS_NOT=(
|
|
|
15242
15263
|
"LOKI_LAST_GIST_URL|internal variable carrying a gist link this run already created"
|
|
15243
15264
|
"LOKI_HOSTED_BODY|internal temp-file path"
|
|
15244
15265
|
"LOKI_HOSTED_EP|internal copy of LOKI_HOSTED_ENDPOINT"
|
|
15266
|
+
"LOKI_DASHBOARD_ALLOWED_HOSTS|the dashboard's own inbound Host-header allowlist, not a destination"
|
|
15267
|
+
"LOKI_HOST_CHILD_PATH|a filesystem path for the host-guard sandbox, not a host"
|
|
15268
|
+
"LOKI_HOST_PROTECTED_ROOT|a filesystem path for the host-guard sandbox, not a host"
|
|
15269
|
+
"LOKI_HOST_RESERVED_PORTS|local ports the host-guard sandbox reserves, not a destination"
|
|
15270
|
+
"LOKI_HOST_RUNTIME_READ_PATHS|filesystem paths for the host-guard sandbox, not a host"
|
|
15271
|
+
"LOKI_HOST_RUNTIME_WRITE_PATHS|filesystem paths for the host-guard sandbox, not a host"
|
|
15272
|
+
"LOKI_HOST_SEATBELT|a boolean switch for the host-guard sandbox, not a host"
|
|
15273
|
+
"LOKI_HOST_SEATBELT_PROBE_SHA|a hash of the host-guard sandbox probe, not a host"
|
|
15245
15274
|
)
|
|
15246
15275
|
|
|
15247
15276
|
# _loki_airgap_configured_egress -- one TSV line per configured outbound host that is
|
|
@@ -21913,6 +21942,18 @@ def _resolve_session_pin(alias):
|
|
|
21913
21942
|
_m = 'opus'
|
|
21914
21943
|
else: # development (and the '*' fallthrough)
|
|
21915
21944
|
_m = _provider_model_development()
|
|
21945
|
+
# Explicit top / floor setup, mirrors claude.sh resolve_model_for_tier: a
|
|
21946
|
+
# planning pin dispatches opus, a fast pin haiku (skipped when
|
|
21947
|
+
# LOKI_ALLOW_HAIKU is explicitly set to anything but 'true'), unless legacy
|
|
21948
|
+
# tier switching or an explicit per-tier pin applies.
|
|
21949
|
+
if os.environ.get('LOKI_LEGACY_TIER_SWITCHING') != 'true':
|
|
21950
|
+
if (_pin_tier == 'planning' and alias == 'planning'
|
|
21951
|
+
and not (os.environ.get('LOKI_CLAUDE_MODEL_PLANNING') or os.environ.get('LOKI_MODEL_PLANNING'))):
|
|
21952
|
+
_m = 'opus'
|
|
21953
|
+
elif (_pin_tier == 'fast' and alias == 'fast'
|
|
21954
|
+
and not (os.environ.get('LOKI_CLAUDE_MODEL_FAST') or os.environ.get('LOKI_MODEL_FAST'))
|
|
21955
|
+
and os.environ.get('LOKI_ALLOW_HAIKU', 'true') == 'true'):
|
|
21956
|
+
_m = 'haiku'
|
|
21916
21957
|
# Apply the shared LOKI_MAX_TIER clamp with the REAL tier and resolved model
|
|
21917
21958
|
# (loki_apply_max_tier_clamp(model, tier)), NOT the alias==alias override
|
|
21918
21959
|
# convention. This is the one place where the two paths legitimately diverge
|
|
@@ -23515,12 +23556,13 @@ main() {
|
|
|
23515
23556
|
# - Non-TTY/CI without --yes (and no LOKI_ULTRAREVIEW=1): REFUSE with exit 2
|
|
23516
23557
|
# and ZERO ultrareview calls (the no-silent-bill guard). Never hang on read.
|
|
23517
23558
|
# - --yes or LOKI_ULTRAREVIEW=1 proceeds without prompting (this command only).
|
|
23518
|
-
# Args: <format> <assume_yes> <timeout_minutes> <target>
|
|
23559
|
+
# Args: <format> <assume_yes> <timeout_minutes> <target> <trust_workspace>
|
|
23519
23560
|
_review_ultra() {
|
|
23520
23561
|
local fmt="${1:-text}"
|
|
23521
23562
|
local assume_yes="${2:-false}"
|
|
23522
23563
|
local timeout_min="${3:-}"
|
|
23523
23564
|
local target="${4:-}"
|
|
23565
|
+
local trust_ws="${5:-false}"
|
|
23524
23566
|
|
|
23525
23567
|
# Make the ultrareview gate predicates available even though autonomy/loki
|
|
23526
23568
|
# does not source claude-flags.sh globally (same on-demand pattern as
|
|
@@ -23605,14 +23647,31 @@ _review_ultra() {
|
|
|
23605
23647
|
if [ -n "$timeout_min" ]; then
|
|
23606
23648
|
ur_argv+=("--timeout" "$timeout_min")
|
|
23607
23649
|
fi
|
|
23650
|
+
# Claude Code 2.1.309+ needs --trust-workspace for scripted runs. Forward it
|
|
23651
|
+
# only when the user passed it AND the CLI lists it; never trust by default.
|
|
23652
|
+
local ur_trust_ok=false
|
|
23653
|
+
if { claude ultrareview --help 2>&1; claude --help 2>&1; } | grep -q -- '--trust-workspace'; then
|
|
23654
|
+
ur_trust_ok=true
|
|
23655
|
+
fi
|
|
23656
|
+
if [ "$trust_ws" = true ]; then
|
|
23657
|
+
if [ "$ur_trust_ok" = true ]; then
|
|
23658
|
+
ur_argv+=("--trust-workspace")
|
|
23659
|
+
else
|
|
23660
|
+
echo "Note: this claude CLI does not list --trust-workspace; not forwarding it." >&2
|
|
23661
|
+
fi
|
|
23662
|
+
fi
|
|
23608
23663
|
if [ -n "$target" ]; then
|
|
23609
23664
|
ur_argv+=("$target")
|
|
23610
23665
|
fi
|
|
23611
23666
|
|
|
23612
23667
|
echo -e "${DIM}Running: claude ${ur_argv[*]}${NC}" >&2
|
|
23613
23668
|
echo "" >&2
|
|
23614
|
-
|
|
23615
|
-
|
|
23669
|
+
local ur_rc=0
|
|
23670
|
+
claude "${ur_argv[@]}" || ur_rc=$?
|
|
23671
|
+
if [ "$ur_rc" -ne 0 ] && [ "$trust_ws" != true ] && [ "$ur_trust_ok" = true ]; then
|
|
23672
|
+
echo "If claude reported an untrusted folder, re-run: loki review --ultra --yes --trust-workspace" >&2
|
|
23673
|
+
fi
|
|
23674
|
+
return "$ur_rc"
|
|
23616
23675
|
}
|
|
23617
23676
|
|
|
23618
23677
|
# v7.38.0: optional native Claude Code Dynamic Workflow passthrough
|
|
@@ -23800,6 +23859,7 @@ cmd_review() {
|
|
|
23800
23859
|
local review_format="text"
|
|
23801
23860
|
local review_severity="all"
|
|
23802
23861
|
local review_ultra=false
|
|
23862
|
+
local review_trust_ws=false
|
|
23803
23863
|
local review_assume_yes=false
|
|
23804
23864
|
local review_timeout=""
|
|
23805
23865
|
local review_risk=false
|
|
@@ -23836,6 +23896,8 @@ cmd_review() {
|
|
|
23836
23896
|
echo " 'loki review <pr-number> --risk' also works. Use --json"
|
|
23837
23897
|
echo " --yes, -y Skip the --ultra confirmation prompt"
|
|
23838
23898
|
echo " --timeout <minutes> --ultra: max minutes to wait (default 30)"
|
|
23899
|
+
echo " --trust-workspace --ultra: forward claude's --trust-workspace (needed for"
|
|
23900
|
+
echo " scripted runs in an untrusted folder; never default)"
|
|
23839
23901
|
echo " --help, -h Show this help"
|
|
23840
23902
|
echo ""
|
|
23841
23903
|
echo "Exit codes:"
|
|
@@ -23857,6 +23919,7 @@ cmd_review() {
|
|
|
23857
23919
|
return 0
|
|
23858
23920
|
;;
|
|
23859
23921
|
--ultra) review_ultra=true; shift ;;
|
|
23922
|
+
--trust-workspace) review_trust_ws=true; shift ;;
|
|
23860
23923
|
--risk) review_risk=true; shift ;;
|
|
23861
23924
|
--json) review_format="json"; shift ;;
|
|
23862
23925
|
--yes|-y) review_assume_yes=true; shift ;;
|
|
@@ -23910,7 +23973,7 @@ cmd_review() {
|
|
|
23910
23973
|
# below: --ultra is a different operation (paid cloud), not a flag that
|
|
23911
23974
|
# augments the local static analysis.
|
|
23912
23975
|
if [ "$review_ultra" = true ]; then
|
|
23913
|
-
_review_ultra "$review_format" "$review_assume_yes" "$review_timeout" "$review_target"
|
|
23976
|
+
_review_ultra "$review_format" "$review_assume_yes" "$review_timeout" "$review_target" "$review_trust_ws"
|
|
23914
23977
|
return $?
|
|
23915
23978
|
fi
|
|
23916
23979
|
|
|
@@ -39218,6 +39281,13 @@ print(json.dumps(json.load(open(os.environ['LOKI_PROOF_JSON'])), indent=2))
|
|
|
39218
39281
|
_pv_rc=1
|
|
39219
39282
|
_pv_attestation_field="ABSENT"
|
|
39220
39283
|
;;
|
|
39284
|
+
keyset:*)
|
|
39285
|
+
echo -e "${YELLOW}attestation: NOT CHECKED -- ${_pv_att#keyset: }.${NC}" >&2
|
|
39286
|
+
echo " The key set at ${_pv_jwks} cannot give one usable key for this receipt." >&2
|
|
39287
|
+
echo " This is not a failure of the receipt. Nothing was proven either way (exit 2)." >&2
|
|
39288
|
+
if [ "$_pv_rc" -eq 0 ]; then _pv_rc=2; fi
|
|
39289
|
+
_pv_attestation_field="NOT CHECKED"
|
|
39290
|
+
;;
|
|
39221
39291
|
*)
|
|
39222
39292
|
# Unreachable key set, unreadable file, missing python
|
|
39223
39293
|
# deps. NOT a verdict: an absent measurement must never
|
|
@@ -106,6 +106,10 @@ fi
|
|
|
106
106
|
# Internal state
|
|
107
107
|
CHECKLIST_DIR=""
|
|
108
108
|
CHECKLIST_FILE=""
|
|
109
|
+
# Digest of the check definitions, frozen by the harness the first time it sees
|
|
110
|
+
# the checklist and held only in this process's memory (never on disk, where an
|
|
111
|
+
# agent could rewrite it). Every verify passes it as --expect-digest.
|
|
112
|
+
CHECKLIST_FROZEN_DIGEST=""
|
|
109
113
|
CHECKLIST_RESULTS_FILE=""
|
|
110
114
|
CHECKLIST_LAST_VERIFY_ITERATION=0
|
|
111
115
|
# Path to the spec/PRD that drove this checklist. Used by the acceptance-oracle
|
|
@@ -1009,6 +1013,27 @@ except Exception:
|
|
|
1009
1013
|
# Verification
|
|
1010
1014
|
#===============================================================================
|
|
1011
1015
|
|
|
1016
|
+
# Freeze the check definitions once. Idempotent: a second call keeps the first
|
|
1017
|
+
# digest, so an edit made after the freeze can never become the new baseline.
|
|
1018
|
+
checklist_freeze_once() {
|
|
1019
|
+
[ "$CHECKLIST_ENABLED" = "true" ] || return 0
|
|
1020
|
+
[ -z "$CHECKLIST_FROZEN_DIGEST" ] || return 0
|
|
1021
|
+
[ -n "$CHECKLIST_FILE" ] && [ -f "$CHECKLIST_FILE" ] || return 0
|
|
1022
|
+
|
|
1023
|
+
local script_dir verify_script _cl_py digest
|
|
1024
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
1025
|
+
verify_script="${script_dir}/checklist-verify.py"
|
|
1026
|
+
[ -f "$verify_script" ] || return 1
|
|
1027
|
+
_cl_py="$(_loki_snapshot_py_tool)" || return 1
|
|
1028
|
+
digest="$("$_cl_py" -I -S "$verify_script" --checklist "$CHECKLIST_FILE" --freeze 2>/dev/null)" || return 1
|
|
1029
|
+
case "$digest" in
|
|
1030
|
+
*[!0-9a-f]* | "") return 1 ;;
|
|
1031
|
+
esac
|
|
1032
|
+
[ "${#digest}" -eq 64 ] || return 1
|
|
1033
|
+
CHECKLIST_FROZEN_DIGEST="$digest"
|
|
1034
|
+
return 0
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1012
1037
|
checklist_verify() {
|
|
1013
1038
|
if [ "$CHECKLIST_ENABLED" != "true" ]; then
|
|
1014
1039
|
return 0
|
|
@@ -1045,11 +1070,22 @@ checklist_verify() {
|
|
|
1045
1070
|
return 1
|
|
1046
1071
|
fi
|
|
1047
1072
|
|
|
1073
|
+
if ! checklist_freeze_once; then
|
|
1074
|
+
log_warn "checklist verification skipped: could not freeze the check definitions (fail closed)"
|
|
1075
|
+
return 1
|
|
1076
|
+
fi
|
|
1077
|
+
|
|
1048
1078
|
log_step "Running PRD checklist verification..."
|
|
1049
1079
|
|
|
1050
|
-
|
|
1080
|
+
local _cl_rc=0 _cl_err
|
|
1081
|
+
_cl_err="$("$_cl_py" -I -S "$verify_script" \
|
|
1051
1082
|
--checklist "$CHECKLIST_FILE" \
|
|
1052
|
-
--
|
|
1083
|
+
--expect-digest "$CHECKLIST_FROZEN_DIGEST" \
|
|
1084
|
+
--timeout "$CHECKLIST_TIMEOUT" 2>&1 >/dev/null)" || _cl_rc=$?
|
|
1085
|
+
if [ "$_cl_rc" -ne 0 ]; then
|
|
1086
|
+
log_warn "checklist verification refused (rc=$_cl_rc): ${_cl_err##*$'\n'}"
|
|
1087
|
+
return "$_cl_rc"
|
|
1088
|
+
fi
|
|
1053
1089
|
|
|
1054
1090
|
CHECKLIST_LAST_VERIFY_ITERATION="${ITERATION_COUNT:-0}"
|
|
1055
1091
|
|