loki-mode 7.129.5 → 8.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +121 -9
  2. package/SKILL.md +66 -5
  3. package/VERSION +1 -1
  4. package/autonomy/app-runner.sh +37 -0
  5. package/autonomy/completion-council.sh +862 -28
  6. package/autonomy/council-v2.sh +39 -0
  7. package/autonomy/crash.sh +3 -2
  8. package/autonomy/grill.sh +42 -0
  9. package/autonomy/hooks/validate-bash.sh +301 -4
  10. package/autonomy/lib/cockpit-render.sh +8 -2
  11. package/autonomy/lib/cr-rematerialize.py +101 -4
  12. package/autonomy/lib/deadline.py +957 -0
  13. package/autonomy/lib/dependency-setup.sh +205 -0
  14. package/autonomy/lib/done-recognition.sh +45 -0
  15. package/autonomy/lib/efficiency_cost.py +13 -6
  16. package/autonomy/lib/no_mock_scan.py +793 -0
  17. package/autonomy/lib/prd-enrich.sh +42 -0
  18. package/autonomy/lib/proof-analytics-props.py +69 -0
  19. package/autonomy/lib/proof-generator.py +326 -92
  20. package/autonomy/lib/proof-template.html +15 -12
  21. package/autonomy/lib/proof-verify.py +169 -105
  22. package/autonomy/lib/requirements_contract.py +848 -0
  23. package/autonomy/lib/sdk-mode.sh +103 -0
  24. package/autonomy/lib/secret-scan.sh +139 -0
  25. package/autonomy/lib/spec-expand.sh +208 -0
  26. package/autonomy/lib/tree_digest.py +266 -0
  27. package/autonomy/lib/voter-agents.sh +58 -8
  28. package/autonomy/lib/workspace_diff.py +141 -0
  29. package/autonomy/loki +196 -17
  30. package/autonomy/playwright-verify.sh +501 -0
  31. package/autonomy/prd-checklist.sh +17 -8
  32. package/autonomy/provider-offer.sh +111 -0
  33. package/autonomy/run.sh +4587 -671
  34. package/autonomy/sandbox.sh +42 -0
  35. package/autonomy/spec-interrogation.sh +148 -5
  36. package/autonomy/spec.sh +118 -1
  37. package/autonomy/telemetry.sh +133 -7
  38. package/autonomy/verify.sh +107 -0
  39. package/bin/loki +110 -3
  40. package/completions/_loki +10 -0
  41. package/completions/loki.bash +1 -1
  42. package/dashboard/__init__.py +1 -1
  43. package/dashboard/audit.py +96 -7
  44. package/dashboard/build_supervisor.py +1619 -0
  45. package/dashboard/control.py +8 -0
  46. package/dashboard/prompt_optimizer.py +7 -0
  47. package/dashboard/server.py +577 -22
  48. package/dashboard/static/trust.html +1 -1
  49. package/docs/ARCHITECTURE-OVERVIEW.md +207 -0
  50. package/docs/AUTONOMI-ECOSYSTEM.md +107 -0
  51. package/docs/INSTALLATION.md +19 -2
  52. package/docs/MODEL-EQUIVALENCE-HARNESS-PLAN.md +70 -0
  53. package/docs/PLANS-INDEX.md +33 -0
  54. package/docs/PRIVACY.md +29 -1
  55. package/docs/SIGNED-RECEIPTS.md +102 -0
  56. package/docs/SONNET5-DEFAULT-PLAN.md +1 -1
  57. package/docs/V8-ACCEPTANCE-TRIAGE-2026-07-24.md +114 -0
  58. package/docs/V8-AGENT-SDK-PLAN.md +692 -0
  59. package/docs/V8-COMPLEXITY-AUDIT-2026-07-24.md +45 -0
  60. package/docs/V8-MAJOR-RELEASE-PLAN.md +137 -0
  61. package/docs/V8-OVERNIGHT-PLAN-2026-07-25.md +82 -0
  62. package/docs/V8-RUNTIME-TRUTH-2026-07-25.md +232 -0
  63. package/docs/V8-SDK-RESEARCH-RAW.md +129 -0
  64. package/docs/alternative-installations.md +4 -4
  65. package/loki-ts/dist/loki.js +674 -285
  66. package/loki-ts/package.json +2 -0
  67. package/mcp/__init__.py +1 -1
  68. package/package.json +7 -6
  69. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  70. package/providers/claude.sh +75 -0
  71. package/providers/codex.sh +85 -13
  72. package/references/sdk-mode.md +106 -0
  73. package/skills/model-selection.md +1 -1
  74. package/skills/providers.md +42 -0
  75. package/skills/quality-gates.md +22 -0
package/bin/loki CHANGED
@@ -32,6 +32,19 @@ SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
32
32
  REPO_ROOT=$(cd "$SCRIPT_DIR/.." 2>/dev/null && pwd)
33
33
  BASH_CLI="$REPO_ROOT/autonomy/loki"
34
34
 
35
+ # v8.1: resolve the one-switch SDK mode (LOKI_SDK_MODE=off|judges|full) into the
36
+ # per-site LOKI_SDK_* flags HERE, in the outermost shim, BEFORE the `start`
37
+ # routing fork below reads LOKI_SDK_LOOP. Without this, `LOKI_SDK_MODE=full loki
38
+ # start` would run the judges on the SDK but silently keep the RARV loop on the
39
+ # legacy bash binary, because the routing decision predates autonomy/loki's own
40
+ # resolve. Same idempotent lib all three entrypoints source; exported flags reach
41
+ # the child (bash or Bun). No-op when the mode is unset.
42
+ if [ -f "$REPO_ROOT/autonomy/lib/sdk-mode.sh" ]; then
43
+ # shellcheck source=../autonomy/lib/sdk-mode.sh
44
+ source "$REPO_ROOT/autonomy/lib/sdk-mode.sh"
45
+ loki_sdk_resolve_mode
46
+ fi
47
+
35
48
  # v7.89.0 telemetry TTY fix (council cH_r1): resolve interactivity EXACTLY ONCE
36
49
  # here, while this shim still owns the user's real TTY, and export it as an
37
50
  # explicit signal. The telemetry gates (telemetry.sh / crash.sh / dashboard
@@ -164,7 +177,7 @@ if [ -z "${LOKI_TELEMETRY_DISABLED:-}" ] && [ "${DO_NOT_TRACK:-}" != "1" ] && [
164
177
  _loki_disclose_telemetry_once
165
178
  # Fire-and-forget the installed event; detach all FDs (v7.8.3 broken-pipe fix).
166
179
  ( SCRIPT_DIR="$REPO_ROOT/autonomy"; source "$SCRIPT_DIR/telemetry.sh" 2>/dev/null \
167
- && loki_telemetry "installed" "first_command=${1:-}" 2>/dev/null ) >/dev/null 2>&1 </dev/null &
180
+ && loki_telemetry "installed" "first_command=$(_loki_known_command "${1:-}")" 2>/dev/null ) >/dev/null 2>&1 </dev/null &
168
181
  disown 2>/dev/null || true
169
182
  fi
170
183
  fi
@@ -180,6 +193,45 @@ if [ "${LOKI_PROVIDER:-}" = "gemini" ]; then
180
193
  exit 1
181
194
  fi
182
195
 
196
+ # First-run funnel, stage C: the activation ATTEMPT (first `loki start`).
197
+ #
198
+ # WHY HERE. This shim runs for BOTH routes before any exec, so one emit at this
199
+ # point covers all four dispositions of a start: the Bun SDK fork, the
200
+ # needs-bash divert, LOKI_LEGACY_BASH, and the no-Bun fallthrough. An emit down
201
+ # in run.sh would be bash-route-only and would report installs climbing while
202
+ # starts sat near zero under LOKI_SDK_MODE=full, which is exactly the blind spot
203
+ # this is meant to close. Placed after the gemini guard (that path exits) and
204
+ # before the LOKI_LEGACY_BASH check (which execs).
205
+ #
206
+ # `run` is the deprecated start alias and `quick` is grouped with them in the
207
+ # bash config pre-pass, so all three count as an activation attempt.
208
+ #
209
+ # Gate: the strict EXISTING double gate, unchanged -- telemetry enabled AND
210
+ # analytics explicitly opted in (_loki_analytics_enabled), default OFF even for
211
+ # diagnostics-on users. No new consent knob, no new endpoint.
212
+ #
213
+ # Disclosure runs in the FOREGROUND first (the backgrounded emit has all three
214
+ # FDs detached, so a disclosure printed in there would go to /dev/null and the
215
+ # user would never see it), gated on the same analytics gate the egress uses so
216
+ # we never disclose for an egress that will not happen. This mirrors the
217
+ # cli_command sites below. No arguments are read, so nothing user-authored can
218
+ # reach the payload.
219
+ case "${1:-}" in
220
+ start|run|quick)
221
+ if command -v curl &>/dev/null && [ -f "$REPO_ROOT/autonomy/telemetry.sh" ]; then
222
+ if ( SCRIPT_DIR="$REPO_ROOT/autonomy"; source "$SCRIPT_DIR/telemetry.sh" 2>/dev/null \
223
+ && declare -f _loki_analytics_enabled >/dev/null 2>&1 && _loki_analytics_enabled \
224
+ && [ ! -f "${HOME}/.loki/funnel-start-attempted" ] ); then
225
+ _loki_disclose_telemetry_once
226
+ fi
227
+ ( SCRIPT_DIR="$REPO_ROOT/autonomy"; source "$SCRIPT_DIR/telemetry.sh" 2>/dev/null \
228
+ && loki_emit_funnel_once "start-attempted" "first_start_attempted" \
229
+ "entry=$(_loki_known_command "${1:-}")" 2>/dev/null ) >/dev/null 2>&1 </dev/null &
230
+ disown 2>/dev/null || true
231
+ fi
232
+ ;;
233
+ esac
234
+
183
235
  # Force-fall-through to bash when the rollback flag is set.
184
236
  if [ "${LOKI_LEGACY_BASH:-0}" = "1" ] || [ "${LOKI_LEGACY_BASH:-}" = "true" ]; then
185
237
  exec "$BASH_CLI" "$@"
@@ -191,6 +243,61 @@ if ! command -v bun &>/dev/null; then
191
243
  exec "$BASH_CLI" "$@"
192
244
  fi
193
245
 
246
+ # v8 Phase 4: route `start` to the Bun RARV loop ONLY when LOKI_SDK_LOOP is on.
247
+ # Default (unset) keeps `loki start` on the bash cmd_start, byte-identical. This
248
+ # guard is AFTER the legacy check (LOKI_LEGACY_BASH=1 still force-overrides to
249
+ # bash above) and AFTER the bun-presence check (a bun-less host already fell to
250
+ # bash). Accepts 1|true to match the flag's documented =1 usage; bash cannot
251
+ # cheaply reproduce truthy()'s yes/on spellings, so those are not honored here
252
+ # (documented divergence, same as other bin/loki bool guards). v8.1: LOKI_SDK_LOOP
253
+ # may also be set by the mode resolver above (LOKI_SDK_MODE=full -> LOOP=1), so
254
+ # `LOKI_SDK_MODE=full loki start` now correctly routes the loop to Bun here.
255
+ #
256
+ # RUN-25 iter 2 (T3(c) pre-flight): the Bun runAutonomous loop handles the RARV
257
+ # loop only. Orchestration flags that do pre-loop work the Bun loop does NOT do
258
+ # (git-worktree parallel streams, GitHub/issue import, Docker sandbox, the API
259
+ # server, backgrounding, alternate spec sources) must run on the bash route so the
260
+ # user gets FULL behavior even with the loop flag on. Scan the start args for any
261
+ # such flag (or an issue-ref positional) and, if present, DIVERT back to bash
262
+ # instead of taking the Bun fork. This is the "pre-loop handled, not rejected"
263
+ # guarantee that makes the eventual loop default-flip a no-regression. Runner
264
+ # flags (--simple, --allow-haiku, --regen-prd, --skip-memory, provider knobs) are
265
+ # handled by start.ts and do NOT divert. See artifacts/ITER1-FLAG-RECONCILE.md.
266
+ _loki_start_needs_bash() {
267
+ # $@ = the start args (already know $1 = start). Return 0 (divert to bash) if
268
+ # any bash-only orchestration flag or an issue-ref spec is present.
269
+ local a
270
+ for a in "$@"; do
271
+ case "$a" in
272
+ --parallel|--bg|--background|--github|--api|--sandbox \
273
+ |--bmad-project|--openspec \
274
+ |--mirofish|--mirofish-docker|--mirofish-rounds|--mirofish-timeout|--mirofish-bg \
275
+ |--issue|--dry-run|--no-start|--output|--worktree|-w|--pr|--ship|--detach|-d)
276
+ return 0 ;;
277
+ --isolation)
278
+ # value follows; only worktree/docker/sandbox divert (none stays Bun).
279
+ _loki_iso_next=1 ;;
280
+ worktree|wt|docker|sandbox)
281
+ [ "${_loki_iso_next:-0}" = "1" ] && { unset _loki_iso_next; return 0; }
282
+ _loki_iso_next=0 ;;
283
+ *)
284
+ # issue-ref positional: owner/repo#N, a tracker URL, or *#<digits>.
285
+ case "$a" in
286
+ */*\#[0-9]*|http*://*/issues/*|http*://*/browse/*) return 0 ;;
287
+ esac
288
+ _loki_iso_next=0 ;;
289
+ esac
290
+ done
291
+ unset _loki_iso_next
292
+ return 1
293
+ }
294
+ if [ "${1:-}" = "start" ] && { [ "${LOKI_SDK_LOOP:-}" = "1" ] || [ "${LOKI_SDK_LOOP:-}" = "true" ]; }; then
295
+ if _loki_start_needs_bash "${@:2}"; then
296
+ exec "$BASH_CLI" "$@"
297
+ fi
298
+ exec bun "$BUN_CLI" "$@"
299
+ fi
300
+
194
301
  # CLI consolidation (Phase A): `trust detail` is the grouped form of the
195
302
  # trust-metrics breakdown. The Bun `trust` handler only knows the trajectory
196
303
  # view (it rejects unknown args), and Phase A moves no handler logic, so the
@@ -249,7 +356,7 @@ if [ "${1:-}" = "report" ]; then
249
356
  && declare -f _loki_telemetry_enabled >/dev/null 2>&1 && _loki_telemetry_enabled ); then
250
357
  _loki_disclose_telemetry_once
251
358
  fi
252
- ( SCRIPT_DIR="$REPO_ROOT/autonomy"; source "$SCRIPT_DIR/telemetry.sh" 2>/dev/null && loki_telemetry "cli_command" "command=${1:-}" 2>/dev/null ) >/dev/null 2>&1 </dev/null &
359
+ ( SCRIPT_DIR="$REPO_ROOT/autonomy"; source "$SCRIPT_DIR/telemetry.sh" 2>/dev/null && loki_telemetry "cli_command" "command=$(_loki_known_command "${1:-}")" 2>/dev/null ) >/dev/null 2>&1 </dev/null &
253
360
  disown 2>/dev/null || true
254
361
  fi
255
362
  exec bun "$BUN_CLI" "$@"
@@ -291,7 +398,7 @@ case "${1:-}" in
291
398
  && declare -f _loki_telemetry_enabled >/dev/null 2>&1 && _loki_telemetry_enabled ); then
292
399
  _loki_disclose_telemetry_once
293
400
  fi
294
- ( SCRIPT_DIR="$REPO_ROOT/autonomy"; source "$SCRIPT_DIR/telemetry.sh" 2>/dev/null && loki_telemetry "cli_command" "command=${1:-}" 2>/dev/null ) >/dev/null 2>&1 </dev/null &
401
+ ( SCRIPT_DIR="$REPO_ROOT/autonomy"; source "$SCRIPT_DIR/telemetry.sh" 2>/dev/null && loki_telemetry "cli_command" "command=$(_loki_known_command "${1:-}")" 2>/dev/null ) >/dev/null 2>&1 </dev/null &
295
402
  disown 2>/dev/null || true
296
403
  fi
297
404
  exec bun "$BUN_CLI" "$@"
package/completions/_loki CHANGED
@@ -126,10 +126,12 @@ function _loki_commands {
126
126
  'monitor:Monitor Docker Compose services with auto-fix'
127
127
  'demo:Interactive 60-second demo'
128
128
  'tour:Offline sample receipt replay (no key, no spend)'
129
+ 'welcome:Friendly first-run opener (what Loki is, next steps)'
129
130
  'init:Interactive PRD builder'
130
131
  'stop:Stop execution'
131
132
  'pause:Pause execution'
132
133
  'resume:Resume execution'
134
+ 'steer:Steer a running build with a note (needs LOKI_PROMPT_INJECTION=1)'
133
135
  'status:Show status'
134
136
  'next:Run the right next step for the current build'
135
137
  'ship:Finish the build (gates + PR advice) in one command'
@@ -144,18 +146,26 @@ function _loki_commands {
144
146
  'notify:Notification commands'
145
147
  'import:Import GitHub issues'
146
148
  'issue:GitHub issue commands'
149
+ 'github:GitHub integration commands'
147
150
  'config:Config commands'
148
151
  'provider:Provider commands'
149
152
  'reset:Reset session state'
150
153
  'memory:Memory commands'
151
154
  'compound:Knowledge compounding commands'
155
+ 'checkpoint:Snapshot and restore session state'
152
156
  'council:Completion council commands'
153
157
  'dogfood:Self-development statistics'
154
158
  'projects:Project registry commands'
155
159
  'enterprise:Enterprise feature commands'
160
+ 'secrets:Secrets and API key management'
161
+ 'cockpit:Live cockpit view of a running build'
162
+ 'secure:Secure-by-default gate (findings and waivers)'
163
+ 'own:Finish-and-own ownership handoff'
156
164
  'telemetry:OpenTelemetry management'
157
165
  'agent:Agent type dispatch'
158
166
  'doctor:Check system prerequisites'
167
+ 'watchdog:Watchdog process supervision'
168
+ 'docs:Documentation commands'
159
169
  'onboard:Analyze repo and generate CLAUDE.md'
160
170
  'metrics:Session productivity report'
161
171
  'share:Share session report as GitHub Gist'
@@ -5,7 +5,7 @@ _loki_completion() {
5
5
  _init_completion || return
6
6
 
7
7
  # Main subcommands (must match autonomy/loki main case statement)
8
- local main_commands="start quick monitor demo tour init stop pause resume status next ship dashboard web serve api sandbox notify import github issue config provider reset memory compound checkpoint council dogfood projects enterprise secrets doctor watchdog audit metrics syslog onboard share proof explain plan report cost kpis stats test ci watch telemetry agent context ctx code run export review optimize heal modernize migrate cluster worktree wt trigger failover remote deploy docker mcp magic assets analyze compliance crash open otel preview quickstart rc rollback self-update sentrux setup-skill spec state template trust trust-metrics ultracode update verify voice why wiki bench cleanup logs grill docs cp version completions help"
8
+ local main_commands="start quick monitor demo tour welcome init stop pause resume steer status next ship dashboard web serve api sandbox notify import github issue config provider reset memory compound checkpoint council dogfood projects enterprise secrets cockpit secure own doctor watchdog audit metrics syslog onboard share proof explain plan report cost kpis stats test ci watch telemetry agent context ctx code run export review optimize heal modernize migrate cluster worktree wt trigger failover remote deploy docker mcp magic assets analyze compliance crash open otel preview quickstart rc rollback self-update sentrux setup-skill spec state template trust trust-metrics ultracode update verify voice why wiki bench cleanup logs grill docs cp version completions help"
9
9
 
10
10
  # 1. If we are on the first argument (subcommand)
11
11
  if [[ $cword -eq 1 ]]; then
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.129.5"
10
+ __version__ = "8.0.1"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -22,6 +22,15 @@ import os
22
22
  import socket
23
23
  import sys
24
24
  import threading
25
+
26
+ # POSIX-only. Used to serialize the tamper-evident chain append ACROSS PROCESSES
27
+ # (threading.Lock cannot). Imported defensively so this module still loads on a
28
+ # platform without it; the writer degrades to thread-only serialization rather
29
+ # than refusing to record an audit entry.
30
+ try:
31
+ import fcntl
32
+ except ImportError: # pragma: no cover - non-POSIX
33
+ fcntl = None # type: ignore[assignment]
25
34
  from datetime import datetime, timezone
26
35
  from pathlib import Path
27
36
  from typing import Any, Optional
@@ -59,6 +68,40 @@ _last_hash: str = "0" * 64 # Genesis hash
59
68
  _hash_lock = threading.Lock()
60
69
 
61
70
 
71
+ def _tail_chain_hash(fh) -> "str | None":
72
+ """Return the _integrity_hash of the LAST well-formed line in an open log.
73
+
74
+ Called while holding the exclusive flock, so what it reads is the true chain
75
+ tip on disk rather than whatever this process last remembered. That is the
76
+ whole point: a second writer process must chain from the first writer's
77
+ entry, not from its own frozen import-time tip.
78
+
79
+ Returns None when there is nothing to chain from -- an empty file, or a tail
80
+ this function cannot parse. None means "no opinion", and the caller keeps its
81
+ existing _last_hash. It deliberately does NOT return a genesis or an empty
82
+ string on a parse failure: inventing a tip is how a chain silently re-roots,
83
+ which reads downstream as tampering. Scanning backwards means a torn final
84
+ line (a process killed mid-append) is skipped rather than treated as the tip.
85
+ """
86
+ try:
87
+ fh.seek(0)
88
+ lines = fh.read().splitlines()
89
+ except (OSError, UnicodeDecodeError):
90
+ return None
91
+
92
+ for raw in reversed(lines):
93
+ raw = raw.strip()
94
+ if not raw:
95
+ continue
96
+ try:
97
+ h = json.loads(raw).get("_integrity_hash")
98
+ except (ValueError, AttributeError):
99
+ continue # torn or foreign line; keep looking back
100
+ if isinstance(h, str) and h:
101
+ return h
102
+ return None
103
+
104
+
62
105
  def _recover_last_hash() -> str:
63
106
  """Recover the last integrity hash from the most recent audit log file.
64
107
 
@@ -281,16 +324,62 @@ def log_event(
281
324
  # change and is intentionally not done here.
282
325
  global _last_hash
283
326
  with _hash_lock:
284
- if INTEGRITY_ENABLED:
285
- entry_json = json.dumps(entry, sort_keys=True, default=str)
286
- entry["_integrity_hash"] = _compute_chain_hash(entry_json, _last_hash)
287
- _last_hash = entry["_integrity_hash"]
288
-
289
327
  log_file = _get_current_log_file()
290
328
  _rotate_logs_if_needed(log_file)
291
329
 
292
- with open(log_file, "a") as f:
293
- f.write(json.dumps(entry) + "\n")
330
+ if not INTEGRITY_ENABLED:
331
+ with open(log_file, "a") as f:
332
+ f.write(json.dumps(entry) + "\n")
333
+ _forward_to_syslog(entry)
334
+ return entry
335
+
336
+ # CROSS-PROCESS serialization. _hash_lock is a threading.Lock, which has
337
+ # no meaning across processes, and _last_hash is resolved ONCE at import
338
+ # (see the _recover_last_hash call near the top of this module). Together
339
+ # those meant every concurrently-live writer PROCESS chained from its own
340
+ # frozen tip, silently forking the chain at write time with no tampering
341
+ # involved. Measured on a real machine: 25 of 67 audit files internally
342
+ # chain-broken, ~10k entries, spanning months and still occurring.
343
+ #
344
+ # That is not a cosmetic bug. This chain is the tamper-evidence: a
345
+ # verifier cannot distinguish "two writers raced" from "someone edited
346
+ # the log", so a self-inflicted fork burns the very signal the chain
347
+ # exists to provide. It fails LOUD rather than silent, which is the safe
348
+ # direction, but it destroys the guarantee either way.
349
+ #
350
+ # Fix: take an exclusive flock on the log file and re-derive the true
351
+ # tail INSIDE it, so the tip we chain from is whatever actually landed on
352
+ # disk, not what this process remembered. Best effort by design -- a
353
+ # platform without flock, or a filesystem that refuses it, degrades to
354
+ # the previous behavior rather than dropping the audit record. Losing an
355
+ # audit entry is worse than a fork.
356
+ with open(log_file, "a+") as f:
357
+ locked = False
358
+ try:
359
+ fcntl.flock(f.fileno(), fcntl.LOCK_EX)
360
+ locked = True
361
+ except (OSError, AttributeError, NameError):
362
+ pass # no flock here; fall through unserialized
363
+
364
+ try:
365
+ if locked:
366
+ tail = _tail_chain_hash(f)
367
+ if tail is not None:
368
+ _last_hash = tail
369
+
370
+ entry_json = json.dumps(entry, sort_keys=True, default=str)
371
+ entry["_integrity_hash"] = _compute_chain_hash(entry_json, _last_hash)
372
+ _last_hash = entry["_integrity_hash"]
373
+
374
+ f.seek(0, os.SEEK_END)
375
+ f.write(json.dumps(entry) + "\n")
376
+ f.flush()
377
+ finally:
378
+ if locked:
379
+ try:
380
+ fcntl.flock(f.fileno(), fcntl.LOCK_UN)
381
+ except OSError:
382
+ pass
294
383
 
295
384
  # Forward to syslog if configured (outside the lock: fire-and-forget and
296
385
  # must never extend the critical section / block other writers).