leos-agent 10.2.0 → 10.7.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.
@@ -8,6 +8,7 @@ regressions remain visible without a tokenizer or network access.
8
8
  """
9
9
 
10
10
  import argparse
11
+ import importlib.util
11
12
  import json
12
13
  import re
13
14
  import sys
@@ -19,9 +20,19 @@ ROOT = Path(__file__).resolve().parent.parent
19
20
  # the before/after output in the change that raises it.
20
21
  LIMITS = {
21
22
  "global_policy_bytes": 4_500,
23
+ # What an unconfigured machine actually installs. Rendering the routing region
24
+ # per harness dropped this below the old whole-file figure of 4497, and it must
25
+ # stay there: the model config exists to save money, so it may not cost
26
+ # always-loaded bytes to have. A configured harness exceeds this only by the
27
+ # length of the model names chosen, which is bounded and deliberate.
28
+ "rendered_policy_bytes": 4_497,
22
29
  "codex_implicit_skill_metadata_bytes": 600,
23
30
  "claude_implicit_skill_metadata_bytes": 800,
24
31
  "codex_agent_description_bytes": 550,
32
+ "claude_agent_description_bytes": 550,
33
+ # Command descriptions are listed alongside skills in Claude Code and Cursor,
34
+ # so they are always-loaded context on the same terms as skill metadata.
35
+ "command_description_bytes": 400,
25
36
  "review_dispatch_bytes": 3_500,
26
37
  }
27
38
 
@@ -69,6 +80,20 @@ def agent_description(path):
69
80
  return match.group(1)
70
81
 
71
82
 
83
+ def rendered_policy():
84
+ """The installed payload body per harness, with no routing config present.
85
+
86
+ This is what a session actually loads -- rules/preferences.md on disk keeps a
87
+ harness-neutral default in its routing region, and the installer narrows it to
88
+ one harness. Measured with the config forced empty so the number is a property
89
+ of the repository, not of whoever runs it.
90
+ """
91
+ spec = importlib.util.spec_from_file_location("leo_install_measure", ROOT / "scripts" / "leo-install.py")
92
+ installer = importlib.util.module_from_spec(spec)
93
+ spec.loader.exec_module(installer)
94
+ return {h: byte_len(installer.payload_body(ROOT, h, {})) for h in installer.HARNESSES}
95
+
96
+
72
97
  def measurements():
73
98
  portable = sorted((ROOT / "skills").glob("*/SKILL.md"))
74
99
  claude_only = sorted((ROOT / "skills-claude").glob("*/SKILL.md"))
@@ -77,11 +102,26 @@ def measurements():
77
102
  review_fm, review_body = frontmatter(ROOT / "skills" / "review-pr" / "SKILL.md")
78
103
  del review_fm
79
104
  agent_paths = sorted((ROOT / "payload" / "codex-agents").glob("*.toml"))
105
+ # Claude Code lists every plugin agent's name and description in the parent's
106
+ # always-loaded agent roster, so they are part of the static footprint too.
107
+ claude_agent_paths = sorted((ROOT / "agents").glob("*.md"))
108
+ claude_agent_bytes = 0
109
+ for path in claude_agent_paths:
110
+ fm, _ = frontmatter(path)
111
+ claude_agent_bytes += byte_len(field(fm, "name")) + byte_len(field(fm, "description"))
112
+ command_paths = sorted((ROOT / "commands").glob("*.md")) + sorted((ROOT / "commands-claude").glob("*.md"))
113
+ command_bytes = 0
114
+ for path in command_paths:
115
+ fm, _ = frontmatter(path)
116
+ command_bytes += byte_len(field(fm, "description"))
80
117
  return {
81
118
  "global_policy_bytes": byte_len(policy_body.strip()),
119
+ "rendered_policy_bytes": max(rendered_policy().values()),
82
120
  "codex_implicit_skill_metadata_bytes": skill_metadata_bytes(portable, codex_implicit),
83
121
  "claude_implicit_skill_metadata_bytes": skill_metadata_bytes(portable + claude_only, claude_implicit),
84
122
  "codex_agent_description_bytes": sum(byte_len(agent_description(path)) for path in agent_paths),
123
+ "claude_agent_description_bytes": claude_agent_bytes,
124
+ "command_description_bytes": command_bytes,
85
125
  "review_dispatch_bytes": byte_len(review_body.strip()),
86
126
  }
87
127
 
@@ -99,6 +139,9 @@ def main(argv=None):
99
139
  print("Static prompt footprint (bytes; tokens are roughly bytes / 4 for this prose)")
100
140
  for name, value in values.items():
101
141
  print(f" {name:38} {value:5} limit {LIMITS[name]:5}")
142
+ print(" rendered_policy_bytes is the worst case across harnesses; each one installs:")
143
+ for harness, value in sorted(rendered_policy().items()):
144
+ print(f" {harness:38} {value:5}")
102
145
  print("This excludes conversation history, tool output, cache effects, and subagent work.")
103
146
 
104
147
  over = {name: (value, LIMITS[name]) for name, value in values.items() if value > LIMITS[name]}
@@ -0,0 +1,420 @@
1
+ #!/usr/bin/env python3
2
+ """routing: per-machine model routing for leos-agent's economical tier.
3
+
4
+ The tier only ever had teeth on Claude Code and Codex, because those are the two
5
+ harnesses whose model names the payload could hardcode. Every other harness fell
6
+ through to "use the current model", so fan-outs there ran at full price. Which
7
+ models a harness actually offers varies by machine and by what an IT department
8
+ allows, so the mapping cannot ship in the plugin -- it is machine-local config.
9
+
10
+ CONFIG lives beside the rest of leos-agent's data, at
11
+ ${LEOS_AGENT_LOCAL_PATH:-$HOME/.leos-agent-local}/routing.json, never inside the
12
+ plugin: an upgrade, a reinstall, or an uninstall must never take it. Only `set`
13
+ and `unset` write it, and only when someone runs them -- the installer reads and
14
+ never writes, so an upgrade, a reinstall, or an --uninstall cannot touch it. A
15
+ missing file is not an error -- it means "the shipped defaults", which is
16
+ exactly the behaviour that predates this file.
17
+
18
+ {"cursor": {"runner": "grok-code-fast-1", "executor": "claude-sonnet-4.6"},
19
+ "opencode": {"runner": "anthropic/claude-haiku-4-5"},
20
+ "codex": {"runner": {"model": "gpt-5.6-luna", "effort": "low"}}}
21
+
22
+ Keys are harness names; each holds "runner" and/or "executor", independently. A
23
+ bare string is shorthand for {"model": ...}. Model strings are free-form and
24
+ never checked against a known-model list -- whatever the harness accepts goes in
25
+ verbatim. Only the keys are validated, and an unknown one is a hard error: a
26
+ typo that silently left a harness on the expensive model is the one failure this
27
+ file exists to prevent.
28
+
29
+ READ AT INSTALL TIME, NOT AT RUN TIME. leo-install.py renders the result into
30
+ the payload block it already writes, so a session pays nothing to know its own
31
+ routing -- no config read, no extra turn, no bytes beyond the dispatch line the
32
+ payload was always going to carry.
33
+
34
+ routing.py show [--harness H] what is configured, resolved
35
+ routing.py render --harness H the exact stanza the installer would inject
36
+ routing.py path the config file's path
37
+ routing.py set --harness H --runner M [--runner-effort E]
38
+ [--executor M] [--executor-effort E]
39
+ routing.py unset --harness H [--runner] [--executor]
40
+
41
+ Exit codes: 0 ok, 1 on a malformed config or a refused write, 2 on bad usage.
42
+ """
43
+ import argparse
44
+ import copy
45
+ import json
46
+ import os
47
+ import sys
48
+
49
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
50
+ # Same data root, and the same locking and atomic-write primitives: both
51
+ # machine-local JSON files are written the one way, so a half-written config can
52
+ # never survive a crash and two concurrent writers cannot lose an update.
53
+ from state import _data_root, _locked, atomic_write # noqa: E402
54
+
55
+ # The canonical harness list. leo-install.py imports it from here rather than
56
+ # the other way round: it imports this module to render, and its own filename is
57
+ # not an importable one.
58
+ HARNESSES = ("claude", "codex", "cursor", "hermes", "pi", "opencode")
59
+
60
+ CONFIG_NAME = "routing.json"
61
+ ROLES = ("runner", "executor")
62
+ FIELDS = ("model", "effort")
63
+
64
+ # Harnesses whose economical tier ships with models already baked in: Claude
65
+ # Code reads agents/*.md, Codex reads the installed profile TOMLs. Everything
66
+ # else inherits unless this file says otherwise.
67
+ BAKED = ("claude", "codex")
68
+
69
+
70
+ def config_path():
71
+ return os.path.join(_data_root(), CONFIG_NAME)
72
+
73
+
74
+ def _bad(message):
75
+ sys.exit(f"routing: {config_path()}: {message}")
76
+
77
+
78
+ def read_raw():
79
+ """The document exactly as written, or {} when there is no file.
80
+
81
+ set/unset merge into THIS rather than into load()'s output: load()
82
+ normalises a bare model string into an object and fills in effort: None, so
83
+ writing its result back would rewrite every other harness's entry as a side
84
+ effect of touching one. Reading twice is the price of leaving the rest of
85
+ Leo's file exactly as he wrote it.
86
+ """
87
+ try:
88
+ with open(config_path()) as fh:
89
+ return json.load(fh)
90
+ except FileNotFoundError:
91
+ return {}
92
+ except json.JSONDecodeError as exc:
93
+ _bad(f"is not valid JSON ({exc})")
94
+ except OSError as exc:
95
+ _bad(exc.strerror or str(exc))
96
+
97
+
98
+ def load(harnesses=HARNESSES):
99
+ """Parse and validate the config. Returns {} when there is no file."""
100
+ return validate(read_raw(), harnesses)
101
+
102
+
103
+ def validate(data, harnesses=HARNESSES):
104
+ """Resolve a parsed document into {harness: {role: {model, effort}}}.
105
+
106
+ Shared by the read path and the write path, so a `set` can never produce a
107
+ file that `load` would go on to reject.
108
+ """
109
+ if not isinstance(data, dict):
110
+ _bad(f"top level is {type(data).__name__}, expected an object keyed by harness")
111
+
112
+ out = {}
113
+ for harness, entry in data.items():
114
+ if harness not in harnesses:
115
+ _bad(f"{harness!r} is not a harness; expected one of {', '.join(sorted(harnesses))}")
116
+ if not isinstance(entry, dict):
117
+ _bad(f"{harness}: expected an object with 'runner' and/or 'executor'")
118
+ roles = {}
119
+ for role, value in entry.items():
120
+ if role not in ROLES:
121
+ _bad(f"{harness}.{role}: unknown key; expected {' or '.join(ROLES)}")
122
+ if isinstance(value, str):
123
+ value = {"model": value}
124
+ if not isinstance(value, dict):
125
+ _bad(f"{harness}.{role}: expected a model name or an object, got {type(value).__name__}")
126
+ for field in value:
127
+ if field not in FIELDS:
128
+ _bad(f"{harness}.{role}.{field}: unknown field; expected {' or '.join(FIELDS)}")
129
+ model = value.get("model")
130
+ if not isinstance(model, str) or not model.strip():
131
+ _bad(f"{harness}.{role}: needs a non-empty 'model'")
132
+ effort = value.get("effort")
133
+ if effort is not None and (not isinstance(effort, str) or not effort.strip()):
134
+ _bad(f"{harness}.{role}.effort: must be a non-empty string when present")
135
+ roles[role] = {"model": model.strip(), "effort": effort.strip() if effort else None}
136
+ if roles:
137
+ out[harness] = roles
138
+ return out
139
+
140
+
141
+ def profile(config, harness, role):
142
+ """The configured {model, effort} for one role, or None."""
143
+ return (config.get(harness) or {}).get(role)
144
+
145
+
146
+ def _named(entry):
147
+ """`model` or `model`/effort, for prose."""
148
+ if entry.get("effort"):
149
+ return f"`{entry['model']}`/{entry['effort']}"
150
+ return f"`{entry['model']}`"
151
+
152
+
153
+ def stanza(harness, config):
154
+ """The dispatch lines for one harness. No trailing newline.
155
+
156
+ Kept deliberately short: this text is always-loaded on every turn of every
157
+ session, so it must cost less than the multi-harness prose it replaces.
158
+ """
159
+ runner = profile(config, harness, "runner")
160
+ executor = profile(config, harness, "executor")
161
+
162
+ if harness == "claude":
163
+ # The Agent tool's model parameter overrides the agent definition's
164
+ # frontmatter, so a machine can retarget the tier without the installer
165
+ # ever writing into the plugin-owned agents/ directory.
166
+ if not runner and not executor:
167
+ return 'On Claude Code pass `subagent_type: "leo-runner"` or `"leo-executor"`;\nthe agent definitions carry the models.'
168
+ overrides = ", and ".join(
169
+ f'`subagent_type: "leo-{role}"` with `model: "{entry["model"]}"`'
170
+ for role, entry in (("runner", runner), ("executor", executor))
171
+ if entry
172
+ )
173
+ kept = "" if (runner and executor) else "\nThe other profile keeps the model its agent definition carries."
174
+ return "On Claude Code pass " + overrides + "." + kept
175
+
176
+ if harness == "codex":
177
+ # Config reaches Codex through the installed profile TOMLs, so the prose
178
+ # is the same either way -- and stays one line.
179
+ return "On Codex the installed `leo-runner` and `leo-executor` profiles carry\nthe models."
180
+
181
+ if not runner and not executor:
182
+ return "No cheaper profile is configured here: use the current model, and say\nrouting could not be applied."
183
+
184
+ if runner and executor:
185
+ assignment = f"Dispatch leo-runner at {_named(runner)} and leo-executor at {_named(executor)}."
186
+ elif runner:
187
+ assignment = f"Dispatch leo-runner at {_named(runner)}; leo-executor inherits."
188
+ else:
189
+ assignment = f"Dispatch leo-executor at {_named(executor)}; leo-runner inherits."
190
+ return assignment + "\nWhere this harness cannot set a model per spawn, inherit and say so."
191
+
192
+
193
+ def _entry(model, effort):
194
+ """The on-disk shape for one role. No `effort: null` when there is none."""
195
+ entry = {"model": model}
196
+ if effort:
197
+ entry["effort"] = effort
198
+ return entry
199
+
200
+
201
+ def apply_set(data, harness, roles):
202
+ """roles is {role: (model, effort)}; each named role is replaced whole.
203
+
204
+ Mutates and returns the document it is given; edit() hands it a copy.
205
+
206
+ Wholesale, not merged: a `set` that kept a previously configured effort
207
+ would make it sticky and invisible. The caller prints what it displaced.
208
+ """
209
+ entry = dict(data.get(harness) or {})
210
+ for role, (model, effort) in roles.items():
211
+ entry[role] = _entry(model, effort)
212
+ data[harness] = entry
213
+ return data
214
+
215
+
216
+ def apply_unset(data, harness, roles):
217
+ """roles is a tuple of role names, or () for the whole harness.
218
+
219
+ Mutates and returns the document it is given; edit() hands it a copy.
220
+ """
221
+ if harness not in data:
222
+ return data
223
+ if not roles:
224
+ del data[harness]
225
+ return data
226
+ entry = dict(data[harness])
227
+ for role in roles:
228
+ entry.pop(role, None)
229
+ # An empty harness key is a shape load() never returns, so never leave one.
230
+ if entry:
231
+ data[harness] = entry
232
+ else:
233
+ del data[harness]
234
+ return data
235
+
236
+
237
+ def edit(mutate, write=True):
238
+ """Read-modify-write under state's flock. Returns (before, after).
239
+
240
+ The lock and the directory creation live only on the write path, so a
241
+ --dry-run and every read subcommand still create nothing in the data root.
242
+ """
243
+ if not write:
244
+ before = _sound()
245
+ return before, mutate(copy.deepcopy(before))
246
+ with _locked(config_path()):
247
+ before = _sound()
248
+ after = mutate(copy.deepcopy(before))
249
+ if after != before:
250
+ # Never write something load() would go on to reject.
251
+ validate(after)
252
+ atomic_write(config_path(), after)
253
+ return before, after
254
+
255
+
256
+ def _sound():
257
+ """The raw document, refused unless it is one a write could safely edit.
258
+
259
+ Validating what is already there before touching it means a file Leo broke
260
+ by hand is reported, never repaired by overwriting -- and it keeps the
261
+ mutators free to assume the shape they were written for.
262
+ """
263
+ data = read_raw()
264
+ validate(data)
265
+ return data
266
+
267
+
268
+ def _described(entry):
269
+ """`model` plus `effort=x`, for the column output."""
270
+ if not entry:
271
+ return ""
272
+ effort = f" effort={entry['effort']}" if entry.get("effort") else ""
273
+ return f"{entry['model']}{effort}"
274
+
275
+
276
+ def _resolved(data, harness, role):
277
+ """One role of a raw document, in the normalised shape, or None."""
278
+ value = (data.get(harness) or {}).get(role)
279
+ if isinstance(value, str):
280
+ return {"model": value}
281
+ return value
282
+
283
+
284
+ def _fallback(harness):
285
+ return "shipped default" if harness in BAKED else "inherits the current model"
286
+
287
+
288
+ NOTES = {
289
+ "claude": (
290
+ "note: claude bakes its models into agents/*.md; this layers a per-dispatch\n"
291
+ " `model:` override on top and never rewrites the plugin."
292
+ ),
293
+ "codex": (
294
+ "note: codex bakes its models into the installed profile TOMLs; this\n"
295
+ " substitutes them there, and effort lands as model_reasoning_effort."
296
+ ),
297
+ }
298
+
299
+
300
+ def _finish(args, before, after, root_hint=True):
301
+ """The trailing lines every write subcommand shares."""
302
+ path = config_path()
303
+ if args.dry_run:
304
+ print(json.dumps(after, indent=1, sort_keys=True))
305
+ print("dry run; nothing written")
306
+ return 0
307
+ if after == before:
308
+ where = f"{path} is already current" if os.path.exists(path) else f"no config at {path}"
309
+ print(f"nothing to write; {where}")
310
+ return 0
311
+ print(f"wrote {path}")
312
+ if root_hint:
313
+ installer = os.path.join(os.path.dirname(os.path.abspath(__file__)), "leo-install.py")
314
+ print(f"next: python3 {installer} {args.harness}")
315
+ return 0
316
+
317
+
318
+ def cmd_set(args):
319
+ roles = {}
320
+ for role in ROLES:
321
+ model = getattr(args, role)
322
+ effort = getattr(args, f"{role}_effort")
323
+ if model is None:
324
+ if effort is not None:
325
+ sys.exit(f"routing: --{role}-effort needs --{role} in the same command")
326
+ continue
327
+ if not model.strip():
328
+ _bad(f"{args.harness}.{role}: needs a non-empty 'model'")
329
+ if effort is not None and not effort.strip():
330
+ _bad(f"{args.harness}.{role}.effort: must be a non-empty string when present")
331
+ roles[role] = (model.strip(), effort.strip() if effort else None)
332
+ if not roles:
333
+ sys.exit("routing: set needs --runner and/or --executor")
334
+
335
+ before, after = edit(lambda d: apply_set(d, args.harness, roles), write=not args.dry_run)
336
+ for role in ROLES:
337
+ if role not in roles:
338
+ continue
339
+ was = _resolved(before, args.harness, role)
340
+ now = _resolved(after, args.harness, role)
341
+ verb = "unchanged" if was == now else "set"
342
+ suffix = f" (was {_described(was)})" if was and was != now else ""
343
+ print(f"{verb:9} {args.harness:9} {role:9} {_described(now)}{suffix}")
344
+ if args.harness in NOTES and after != before:
345
+ print(NOTES[args.harness])
346
+ return _finish(args, before, after)
347
+
348
+
349
+ def cmd_unset(args):
350
+ roles = tuple(role for role in ROLES if getattr(args, role))
351
+ before, after = edit(lambda d: apply_unset(d, args.harness, roles), write=not args.dry_run)
352
+ touched = roles or ROLES
353
+ for role in touched:
354
+ was = _resolved(before, args.harness, role)
355
+ if was:
356
+ print(f"{'unset':9} {args.harness:9} {role:9} {_described(was)} -> {_fallback(args.harness)}")
357
+ if before == after:
358
+ print(f"{'unchanged':9} {args.harness:9} {'(both)':9} nothing configured")
359
+ return _finish(args, before, after)
360
+
361
+
362
+ def cmd_show(args):
363
+ config = load()
364
+ if not config:
365
+ print(f"no routing config at {config_path()}; every harness uses its shipped default")
366
+ return
367
+ for harness in sorted(config):
368
+ if args.harness and harness != args.harness:
369
+ continue
370
+ for role in ROLES:
371
+ entry = config[harness].get(role)
372
+ if entry:
373
+ effort = f" effort={entry['effort']}" if entry["effort"] else ""
374
+ print(f"{harness:9} {role:9} {entry['model']}{effort}")
375
+ for harness in sorted(set(BAKED) - set(config)):
376
+ if not args.harness or harness == args.harness:
377
+ print(f"{harness:9} {'(both)':9} shipped default")
378
+
379
+
380
+ def main(argv):
381
+ parser = argparse.ArgumentParser(prog="routing.py", description=__doc__.splitlines()[0])
382
+ sub = parser.add_subparsers(dest="mode", required=True)
383
+ show = sub.add_parser("show", help="what is configured, resolved")
384
+ show.add_argument("--harness", choices=HARNESSES)
385
+ render = sub.add_parser("render", help="the stanza the installer would inject")
386
+ render.add_argument("--harness", choices=HARNESSES, required=True)
387
+ sub.add_parser("path", help="the config file's path")
388
+
389
+ setter = sub.add_parser("set", help="point one harness's roles at models on this machine")
390
+ setter.add_argument("--harness", choices=HARNESSES, required=True)
391
+ setter.add_argument("--runner", metavar="MODEL", help="replaces the runner entry whole")
392
+ setter.add_argument("--runner-effort", metavar="E", help="needs --runner; omitting it clears any effort")
393
+ setter.add_argument("--executor", metavar="MODEL", help="replaces the executor entry whole")
394
+ setter.add_argument("--executor-effort", metavar="E", help="needs --executor; omitting it clears any effort")
395
+ setter.add_argument("--dry-run", action="store_true", help="show the result, write nothing")
396
+
397
+ unsetter = sub.add_parser("unset", help="drop a harness's roles, back to its shipped default")
398
+ unsetter.add_argument("--harness", choices=HARNESSES, required=True)
399
+ unsetter.add_argument("--runner", action="store_true")
400
+ unsetter.add_argument("--executor", action="store_true")
401
+ unsetter.add_argument("--dry-run", action="store_true", help="show the result, write nothing")
402
+
403
+ args = parser.parse_args(argv)
404
+ # An explicit branch per mode: a bare else would silently route a new
405
+ # subcommand into render and crash on an attribute it does not have.
406
+ if args.mode == "path":
407
+ print(config_path())
408
+ elif args.mode == "show":
409
+ cmd_show(args)
410
+ elif args.mode == "render":
411
+ print(stanza(args.harness, load(HARNESSES)))
412
+ elif args.mode == "set":
413
+ return cmd_set(args)
414
+ elif args.mode == "unset":
415
+ return cmd_unset(args)
416
+ return 0
417
+
418
+
419
+ if __name__ == "__main__":
420
+ sys.exit(main(sys.argv[1:]))
package/scripts/state.py CHANGED
@@ -43,13 +43,16 @@ def state_file(name):
43
43
  if "/" in name or "\\" in name or ".." in name or os.path.isabs(name):
44
44
  sys.exit(f"state: {name!r} is not a valid state name (no slashes, no .., not absolute)")
45
45
  root = _data_root()
46
- os.makedirs(root, exist_ok=True)
46
+ # 0700: the root holds handoffs and per-repo state — project context that
47
+ # is nobody else's business on a shared machine. Applies on creation only;
48
+ # an existing directory keeps whatever Leo set on it.
49
+ os.makedirs(root, mode=0o700, exist_ok=True)
47
50
  return os.path.join(root, f"{name}.json")
48
51
 
49
52
 
50
53
  @contextlib.contextmanager
51
54
  def _locked(path):
52
- os.makedirs(os.path.dirname(path), exist_ok=True)
55
+ os.makedirs(os.path.dirname(path), mode=0o700, exist_ok=True)
53
56
  fd = os.open(path + ".lock", os.O_CREAT | os.O_RDWR, 0o600)
54
57
  try:
55
58
  fcntl.flock(fd, fcntl.LOCK_EX)
@@ -88,12 +91,14 @@ def deep_merge(base, patch):
88
91
 
89
92
 
90
93
  def atomic_write(path, data):
91
- os.makedirs(os.path.dirname(path), exist_ok=True)
94
+ os.makedirs(os.path.dirname(path), mode=0o700, exist_ok=True)
92
95
  fd, tmp = tempfile.mkstemp(dir=os.path.dirname(path), suffix=".tmp")
93
96
  try:
94
97
  with os.fdopen(fd, "w") as fh:
95
98
  json.dump(data, fh, indent=1, sort_keys=True)
96
99
  fh.write("\n")
100
+ fh.flush()
101
+ os.fsync(fh.fileno())
97
102
  os.replace(tmp, path)
98
103
  except BaseException:
99
104
  os.unlink(tmp)