@vibelord/lanes 1.2.0 → 1.5.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.
Files changed (46) hide show
  1. package/package.json +1 -1
  2. package/payload/lanes/README.md +7 -5
  3. package/payload/lanes/VERSION +1 -1
  4. package/payload/lanes/bin/lanes +1091 -76
  5. package/payload/lanes/container/hooks/grok-cli-hooks.json +8 -0
  6. package/payload/lanes/container/hooks/grok-hooks.json +12 -0
  7. package/payload/lanes/container/hooks/kimi-hooks.toml +49 -0
  8. package/payload/lanes/container/lane +55 -8
  9. package/payload/lanes/container/lane-mcp +120 -22
  10. package/payload/lanes/container/lane-signal +86 -23
  11. package/payload/lanes/container/lane_core.py +524 -36
  12. package/payload/lanes/container/throttle-pulse +456 -0
  13. package/payload/lanes/docs/ARCHITECTURE.md +357 -34
  14. package/payload/lanes/docs/DESIGN.md +13 -3
  15. package/payload/lanes/docs/INSTALL.md +47 -10
  16. package/payload/lanes/docs/RUNBOOK.md +761 -12
  17. package/payload/lanes/docs/USERGUIDE.md +421 -46
  18. package/payload/lanes/holders.lock +5 -0
  19. package/payload/lanes/install/enable-docs.sh +78 -0
  20. package/payload/lanes/install/host-install.sh +39 -5
  21. package/payload/lanes/libexec/agent_state.py +604 -12
  22. package/payload/lanes/libexec/docs_store.py +995 -0
  23. package/payload/lanes/libexec/docsd.py +2010 -0
  24. package/payload/lanes/libexec/economy_poll.py +6 -2
  25. package/payload/lanes/libexec/harness_grok.py +742 -0
  26. package/payload/lanes/libexec/lanes_lib.py +143 -20
  27. package/payload/lanes/libexec/mdlite.py +575 -0
  28. package/payload/lanes/libexec/members.py +5 -0
  29. package/payload/lanes/libexec/notify.py +65 -5
  30. package/payload/lanes/libexec/sidebar.py +626 -25
  31. package/payload/lanes/libexec/statusd.py +530 -32
  32. package/payload/lanes/libexec/tokens.py +42 -9
  33. package/payload/lanes/libexec/update.py +171 -26
  34. package/payload/lanes/libexec/uploads.py +538 -0
  35. package/payload/lanes/libexec/users.py +31 -3
  36. package/payload/lanes/site/site.example.conf +4 -0
  37. package/payload/lanes/systemd/lanes-docsd.service +29 -0
  38. package/payload/lanes/web/Caddyfile +24 -1
  39. package/payload/lanes/web/README.md +86 -8
  40. package/payload/lanes/web/browser.js +2 -4
  41. package/payload/lanes/web/docs.css +52 -0
  42. package/payload/lanes/web/docs.js +36 -0
  43. package/payload/lanes/web/mobile.js +3 -2
  44. package/payload/lanes/web/upload.css +17 -0
  45. package/payload/lanes/web/upload.js +84 -0
  46. package/payload/manifest.json +5 -3
@@ -24,7 +24,17 @@ sees, and `lane-mcp` may write nothing but protocol frames to its stdout.
24
24
  `shown_key`, `lane_trace`, `canonical_recipe`, the id grammar (each pinned equal to its lanes_lib original by
25
25
  test_lane_mcp_write.HostRules), and `declared_name`, which finds a registry lane's declared terminal in its file.
26
26
  * the agents' MCP entries: `mcpServers.lanes` in ~/.claude.json and `[mcp_servers.lanes]` (command + env_vars) in
27
- ~/.codex/config.toml, planned as a diff, written with a `.bak-<UTC>` copy, atomically and only when missing.
27
+ ~/.codex/config.toml — and (W12) `mcpServers.lanes` in ~/.kimi-code/mcp.json, a `# >>> lanes mcp >>>` block with
28
+ `[mcp_servers.lanes]` in Grok Build's ~/.grok-build/config.toml and the `lanes` item of grok-cli's `mcp.servers` in
29
+ ~/.grok/user-settings.json, those three where the harness's home exists — planned as a diff, written with a
30
+ `.bak-<UTC>` copy, atomically and only when missing. TOML marker blocks (toml_block) are shared with the host's
31
+ `lanes hooks install`.
32
+ * the harness of a process (W12): process_harness / parent_harness over /proc (comm, the grok exe split, a native
33
+ Claude Code file), and whoami's fallback to the agent's own /proc environ (parent_environ) for a harness that strips
34
+ its MCP servers' environment (grok-cli).
35
+ * publish requests (plan W10 W4): `publish` writes ~/.lz/state/publish/<lane>/<id>.json for `lane publish` and
36
+ lane-mcp's `lanes_publish` (the file hashed once, the id in docs_store's grammar: `new_publish_id`), and
37
+ `latest_receipt` reads the `<id>.receipt.json` the host's lanes-docsd writes back (`lanes_status`'s `published`).
28
38
  """
29
39
  import difflib
30
40
  import json
@@ -48,7 +58,9 @@ HOOK_MAX_AGE = 3600 # statusd.SIDECAR_MAX_AGE: an older last event is no a
48
58
  STATUS_FRESH = 60 # status.json `at` is renewed at least every 15 s (statusd.HEARTBEAT); older = not live
49
59
  READ_MAX = 16 << 20 # a projection bigger than this is not ours
50
60
  TMUX_TIMEOUT = 5
51
- IDENTITY = ('LANE_ID', 'LANE_WS', 'THROTTLE_TRACE') # + a lanes/ ANTHROPIC_CUSTOM_HEADERS (identity_free)
61
+ IDENTITY = ('LANE_ID', 'LANE_WS', 'THROTTLE_TRACE', 'LANES_TRACE') # + our trace headers and GROK_HOME (identity_free)
62
+ TRACE_HEADERS = ('ANTHROPIC_CUSTOM_HEADERS', 'KIMI_CODE_CUSTOM_HEADERS') # ours: 'x-throttle-trace: lanes/<ws>/<id>'
63
+ GROK_HOME = CONTAINER_HOME + '/.grok-build' # Grok Build's home in a lane, never grok-cli's ~/.grok (DM-LANES-GROK-HOME)
52
64
 
53
65
 
54
66
  class LaneError(Exception):
@@ -201,9 +213,11 @@ def identity_free(environ, drop=('TMUX',)):
201
213
  a socket's server hands its whole environment to the server's GLOBAL environment, and every later session on
202
214
  that socket created without its own `-e` inherits it (MEASURED, tmux 3.4: DEF-1559). Board sockets are shared by
203
215
  many lanes, so a lane identity in the env of anything that may start one would be stamped on unrelated lanes.
204
- An operator's own ANTHROPIC_CUSTOM_HEADERS that is not a lane trace is theirs and stays."""
216
+ An operator's own ANTHROPIC_CUSTOM_HEADERS / KIMI_CODE_CUSTOM_HEADERS that is not a lane trace is theirs and stays,
217
+ and so is a GROK_HOME that is not the one the recipe stamps (W12)."""
205
218
  return {k: v for k, v in environ.items() if k not in drop and k not in IDENTITY
206
- and not (k == 'ANTHROPIC_CUSTOM_HEADERS' and v.startswith('x-throttle-trace: lanes/'))}
219
+ and not (k in TRACE_HEADERS and v.startswith('x-throttle-trace: lanes/'))
220
+ and not (k == 'GROK_HOME' and v == GROK_HOME)}
207
221
 
208
222
 
209
223
  def tmux_env(environ=None):
@@ -295,7 +309,7 @@ def last_lines(text, lines):
295
309
  def new_session_argv(socket, session, cwd, lane_id, ws, command):
296
310
  """The one tmux client lane-mcp runs WITHOUT -N (`lanes_new`): it creates the session, and starts the socket's
297
311
  server when none runs. The lane's identity reaches the session only through these `-e` flags, the canonical
298
- recipe's four (the client's own environment is creation_env(), an allow-list: whatever client starts a server
312
+ recipe's seven (the client's own environment is creation_env(), an allow-list: whatever client starts a server
299
313
  hands its environment to every later session there, DEF-1559, DEF-1802). `command` is one shell-command string,
300
314
  run by the server's default shell like the recipe's command."""
301
315
  trace = lane_trace(lane_id, ws)
@@ -303,7 +317,9 @@ def new_session_argv(socket, session, cwd, lane_id, ws, command):
303
317
  raise LaneError(f'invalid lane trace: lanes/{ws}/{lane_id}', 'invalid_name')
304
318
  return ['tmux', '-L', socket, 'new-session', '-d', '-s', session, '-x', '100', '-y', '30', '-c', cwd,
305
319
  '-e', f'LANE_ID={lane_id}', '-e', f'LANE_WS={ws}',
306
- '-e', f'ANTHROPIC_CUSTOM_HEADERS=x-throttle-trace: {trace}', '-e', f'THROTTLE_TRACE={trace}', command]
320
+ '-e', f'ANTHROPIC_CUSTOM_HEADERS=x-throttle-trace: {trace}', '-e', f'THROTTLE_TRACE={trace}',
321
+ '-e', f'KIMI_CODE_CUSTOM_HEADERS=x-throttle-trace: {trace}', '-e', f'LANES_TRACE={trace}',
322
+ '-e', f'GROK_HOME={GROK_HOME}', command]
307
323
 
308
324
 
309
325
  CREATION_ENV = ('HOME', 'USER', 'LOGNAME', 'PATH', 'LANG', 'LANGUAGE', 'TZ', 'TMUX_TMPDIR') # and every LC_*
@@ -376,8 +392,9 @@ def lane_trace(lane_id, ws):
376
392
 
377
393
  def canonical_recipe(lane_id, ws, socket, session, folder, cmd='bash -l'):
378
394
  """lanes_lib.canonical_recipe: the create recipe that gives a new tmux lane its identity (LANE_ID, LANE_WS, the
379
- lanes/<ws>/<id> trace as ANTHROPIC_CUSTOM_HEADERS and THROTTLE_TRACE) through `-e` flags; `new -A` attaches when
380
- the session exists. LaneError where the host raises LanesError."""
395
+ lanes/<ws>/<id> trace as ANTHROPIC_CUSTOM_HEADERS, THROTTLE_TRACE, KIMI_CODE_CUSTOM_HEADERS and LANES_TRACE, and
396
+ GROK_HOME) through `-e` flags; `new -A` attaches when the session exists. LaneError where the host raises
397
+ LanesError."""
381
398
  trace = lane_trace(lane_id, ws)
382
399
  if trace is None:
383
400
  raise LaneError(f'Invalid lane trace: lanes/{ws}/{lane_id} (allowed: A-Z a-z 0-9 . _ - /, at most 256)',
@@ -393,7 +410,8 @@ def canonical_recipe(lane_id, ws, socket, session, folder, cmd='bash -l'):
393
410
  header = shlex.quote(f'x-throttle-trace: {trace}')
394
411
  return (f'tmux -L {shlex.quote(socket)} new -ADs {shlex.quote(session)} -c {shlex.quote(folder)} '
395
412
  f'-e LANE_ID={lane_id} -e LANE_WS={ws} -e ANTHROPIC_CUSTOM_HEADERS={header} '
396
- f'-e THROTTLE_TRACE={trace} {cmd}')
413
+ f'-e THROTTLE_TRACE={trace} -e KIMI_CODE_CUSTOM_HEADERS={header} -e LANES_TRACE={trace} '
414
+ f'-e GROK_HOME={GROK_HOME} {cmd}')
397
415
 
398
416
 
399
417
  def lane_name_problem(name):
@@ -466,17 +484,101 @@ def lane_kind(entry):
466
484
  return 'registered' if entry.get('registered_session') else 'declared'
467
485
 
468
486
 
469
- def whoami(root, environ=None):
487
+ # The harnesses a process tree can name (W12; lane-signal's HARNESS_COMMS and parent chain, pinned equal by
488
+ # test_lane_core). A `grok` is Grok Build when its executable is $GROK_HOME/downloads/grok-linux-x86_64, else grok-cli.
489
+ HARNESS_COMMS = {'claude': 'claude', 'codex': 'codex', 'kimi': 'kimi', 'kimi-code': 'kimi', 'grok': 'grok'}
490
+ HARNESSES = ('claude', 'codex', 'kimi', 'grok', 'grok-cli', 'unknown')
491
+ GROK_BUILD_EXE = '/downloads/grok-linux-x86_64'
492
+ CLAUDE_EXE = '/claude/versions/' # native Claude Code before 2.1.299 kept its file's name, the version, as comm (MEASURED)
493
+ PARENT_DEPTH = 8
494
+ ENVIRON_MAX = 1 << 20 # /proc/<pid>/environ read at most this far (the kernel caps an environment far below it)
495
+ WHOAMI_VARS = ('LANE_ID', 'LANE_WS', 'TMUX', 'TMUX_PANE') # what whoami reads (MCP_ENV_VARS passes the same)
496
+
497
+
498
+ def _stat(proc, pid):
499
+ """(comm, ppid) of /proc/<pid>/stat, or None."""
500
+ data = read_regular(Path(proc) / str(pid) / 'stat', 1 << 16)
501
+ if data is None:
502
+ return None
503
+ text = data.decode('utf-8', 'replace')
504
+ try:
505
+ return text[text.index('(') + 1:text.rindex(')')], int(text.rsplit(')', 1)[1].split()[1])
506
+ except (ValueError, IndexError):
507
+ return None
508
+
509
+
510
+ def _exe(proc, pid):
511
+ try:
512
+ return os.readlink(str(Path(proc) / str(pid) / 'exe')).removesuffix(' (deleted)')
513
+ except OSError:
514
+ return ''
515
+
516
+
517
+ def process_harness(proc, pid):
518
+ """The harness process `pid` is, or None: its comm in HARNESS_COMMS (a `grok` split by /proc/<pid>/exe), else a
519
+ native Claude Code file (CLAUDE_EXE) as its executable."""
520
+ found = _stat(proc, pid)
521
+ if not found:
522
+ return None
523
+ harness = HARNESS_COMMS.get(found[0])
524
+ if harness == 'grok':
525
+ return 'grok' if _exe(proc, pid).endswith(GROK_BUILD_EXE) else 'grok-cli'
526
+ return harness or ('claude' if CLAUDE_EXE in _exe(proc, pid) else None)
527
+
528
+
529
+ def parent_harness(proc, pid):
530
+ """The harness of `pid` or the first of its ancestors that is one (at most PARENT_DEPTH processes), else None."""
531
+ for _step in range(PARENT_DEPTH):
532
+ if not isinstance(pid, int) or pid <= 1:
533
+ return None
534
+ harness = process_harness(proc, pid)
535
+ if harness:
536
+ return harness
537
+ found = _stat(proc, pid)
538
+ pid = found[1] if found else 0
539
+ return None
540
+
541
+
542
+ def parent_environ(proc, pid, names=WHOAMI_VARS):
543
+ """{name: value} of `names` in /proc/<pid>/environ when `pid` is a known harness (HARNESS_COMMS) — {} otherwise.
544
+ The read is bounded (ENVIRON_MAX) and never follows a link; entries are NUL-split, and one that is not UTF-8 or
545
+ holds no `=` is skipped. The whoami fallback for an agent that hands its MCP servers a stripped environment
546
+ (grok-cli, W12 DEF-1992): its own environment, set by the lane's recipe, still names the lane."""
547
+ if not isinstance(pid, int) or pid <= 1 or not process_harness(proc, pid):
548
+ return {}
549
+ data = read_regular(Path(proc) / str(pid) / 'environ', ENVIRON_MAX)
550
+ found = {}
551
+ for item in (data or b'').split(b'\0'):
552
+ key, sep, value = item.partition(b'=')
553
+ try:
554
+ key, value = key.decode('utf-8'), value.decode('utf-8')
555
+ except UnicodeDecodeError:
556
+ continue
557
+ if sep and key in names and value and key not in found:
558
+ found[key] = value
559
+ return found
560
+
561
+
562
+ def whoami(root, environ=None, proc=Path('/proc'), parent=None):
470
563
  """Which lane this process runs in: {'lane', 'found_by', 'kind', 'entry' | 'session', 'why'?}.
471
564
  $LANE_ID first (a canonical recipe's `-e`, or lane-run for a zmx lane); else the pane's session, asked of $TMUX's
472
565
  socket (`display -p -t $TMUX_PANE '#S'`), matched by (socket name, session) in the registry, then among this
473
- container's unregistered sessions (status.json). Not in a lane is an answer (`lane` null), not an error."""
566
+ container's unregistered sessions (status.json). Not in a lane is an answer (`lane` null), not an error.
567
+ `parent` (lane-mcp passes its own parent, the agent, when it reads its own environment): when `environ` holds
568
+ neither LANE_ID nor TMUX/TMUX_PANE, the ones the parent's /proc environ holds stand in — only when the parent is a
569
+ known harness (parent_environ; grok-cli strips its MCP servers' environment, DEF-1992); `found_by` then says
570
+ `parent:LANE_ID` / `parent:tmux`."""
474
571
  environ = os.environ if environ is None else environ
572
+ via = ''
573
+ if parent is not None and not environ.get('LANE_ID') and not (environ.get('TMUX') and environ.get('TMUX_PANE')):
574
+ inherited = parent_environ(proc, parent)
575
+ if inherited.get('LANE_ID') or (inherited.get('TMUX') and inherited.get('TMUX_PANE')):
576
+ environ, via = dict(environ, **inherited), 'parent:'
475
577
  lanes = registry_lanes(root)
476
578
  lane = environ.get('LANE_ID') or ''
477
579
  if LANE_RE.fullmatch(lane):
478
580
  entry = lanes.get(lane)
479
- return {'lane': lane, 'found_by': 'LANE_ID', 'kind': lane_kind(entry) if entry else None, 'entry': entry,
581
+ return {'lane': lane, 'found_by': via + 'LANE_ID', 'kind': lane_kind(entry) if entry else None, 'entry': entry,
480
582
  **({} if entry else {'why': f'LANE_ID={lane} is not in ~/.lz/registry.json (a zmx lane, or the '
481
583
  'registry has not been synced since the lane was added)'})}
482
584
  socket_path, pane = (environ.get('TMUX') or '').split(',', 1)[0], environ.get('TMUX_PANE') or ''
@@ -493,11 +595,12 @@ def whoami(root, environ=None):
493
595
  socket = os.path.basename(socket_path)
494
596
  for lane_id, entry in lanes.items():
495
597
  if tmux_target(entry) == (socket, session):
496
- return {'lane': lane_id, 'found_by': 'tmux', 'kind': lane_kind(entry), 'entry': entry}
598
+ return {'lane': lane_id, 'found_by': via + 'tmux', 'kind': lane_kind(entry), 'entry': entry}
497
599
  for row in container_status(root)['unregistered']:
498
600
  tmux = row.get('tmux') or {}
499
601
  if (tmux.get('socket'), tmux.get('session')) == (socket, session):
500
- return {'lane': row['id'], 'found_by': 'tmux', 'kind': 'unregistered', 'entry': None, 'session': row}
602
+ return {'lane': row['id'], 'found_by': via + 'tmux', 'kind': 'unregistered', 'entry': None,
603
+ 'session': row}
501
604
  return {'lane': None, 'found_by': None, 'kind': None, 'entry': None, 'tmux': {'socket': socket, 'session': session},
502
605
  'why': 'this tmux session is neither a registry lane nor an unregistered session statusd has seen'}
503
606
 
@@ -910,8 +1013,13 @@ def terminals_add(text, terminal, entry=None):
910
1013
  MCP_NAME = 'lanes'
911
1014
  MCP_COMMAND = LZ + '/bin/lane-mcp'
912
1015
  MCP_ENTRY = {'command': MCP_COMMAND}
913
- MCP_AGENTS = ('claude', 'codex')
914
- MCP_FILES = {'claude': '.claude.json', 'codex': '.codex/config.toml'}
1016
+ MCP_AGENTS = ('claude', 'codex', 'kimi', 'grok', 'grok-cli')
1017
+ MCP_FILES = {'claude': '.claude.json', 'codex': '.codex/config.toml', 'kimi': '.kimi-code/mcp.json',
1018
+ 'grok': '.grok-build/config.toml', 'grok-cli': '.grok/user-settings.json'}
1019
+ # W12: claude and codex get their entry in every home; kimi, grok (Grok Build, GROK_HOME) and grok-cli only where their
1020
+ # home directory already is a real directory (the harness is installed there), unless named (`--agent`).
1021
+ MCP_ALWAYS = ('claude', 'codex')
1022
+ MCP_HOMES = {'kimi': '.kimi-code', 'grok': '.grok-build', 'grok-cli': '.grok'}
915
1023
  TOML_HEADER = re.compile(r'''^[ \t]*\[[ \t]*mcp_servers[ \t]*\.[ \t]*(?:lanes|"lanes"|'lanes')[ \t]*\][ \t]*(?:#.*)?$''')
916
1024
  # What Codex must hand lane-mcp. Codex starts a stdio MCP server with a CLEARED environment: its defaults (HOME,
917
1025
  # LOGNAME, PATH, SHELL, USER, LANG, LC_ALL, TERM, TMPDIR, TZ) plus the names in the table's `env_vars` and the
@@ -950,26 +1058,24 @@ def _an_older_ours(entry):
950
1058
  and os.path.basename(entry['command']) == 'lane-mcp' and not entry.get('args') and 'url' not in entry
951
1059
 
952
1060
 
953
- def claude_mcp(text, command=MCP_COMMAND):
954
- """~/.claude.json with `mcpServers.lanes = {"command": <command>}` -> (new text, why). The text is unchanged when
955
- an equivalent entry exists; an entry of the same name that starts something else is never replaced (why says
956
- so). The edit is the one member, inserted in the file's own layout; the result must parse back to the input plus
957
- that member, else it is refused."""
1061
+ def _json_mcp(text, command, what):
1062
+ """`what` (a JSON file in Claude Code's `mcpServers` shape) with `mcpServers.lanes = {"command": <command>}` ->
1063
+ (new text, why): claude_mcp and kimi_mcp."""
958
1064
  entry = {'command': command}
959
1065
  if text is None or not text.strip():
960
1066
  return json.dumps({'mcpServers': {MCP_NAME: entry}}, indent=2) + '\n', None
961
- doc = strict_json(text, '~/.claude.json')
1067
+ doc = strict_json(text, what)
962
1068
  if not isinstance(doc, dict):
963
- return text, '~/.claude.json is not a JSON object — left alone'
1069
+ return text, f'{what} is not a JSON object — left alone'
964
1070
  servers = doc.get('mcpServers')
965
1071
  if servers is not None and not isinstance(servers, dict):
966
- return text, 'mcpServers in ~/.claude.json is not an object — left alone'
1072
+ return text, f'mcpServers in {what} is not an object — left alone'
967
1073
  current = (servers or {}).get(MCP_NAME)
968
1074
  if _ours(current, command):
969
1075
  return text, None
970
1076
  if current is not None and not _an_older_ours(current):
971
1077
  shown = current.get('command') if isinstance(current, dict) else current
972
- return text, (f'mcpServers.{MCP_NAME} in ~/.claude.json is not the entry Lanes writes (command {shown!r}) — '
1078
+ return text, (f'mcpServers.{MCP_NAME} in {what} is not the entry Lanes writes (command {shown!r}) — '
973
1079
  'left alone (remove it to let Lanes add its own)')
974
1080
  if servers is None:
975
1081
  after = set_member(text, [], 'mcpServers', {MCP_NAME: entry})
@@ -977,12 +1083,27 @@ def claude_mcp(text, command=MCP_COMMAND):
977
1083
  after = set_member(text, ['mcpServers'], MCP_NAME, entry)
978
1084
  expected = dict(doc)
979
1085
  expected['mcpServers'] = dict(servers or {}, **{MCP_NAME: entry})
980
- if strict_json(after, '~/.claude.json') != expected:
1086
+ if strict_json(after, what) != expected:
981
1087
  return text, 'the entry could not be added without changing anything else — add it by hand'
982
1088
  return after, None
983
1089
 
984
1090
 
985
- def _toml_loads(text):
1091
+ def claude_mcp(text, command=MCP_COMMAND):
1092
+ """~/.claude.json with `mcpServers.lanes = {"command": <command>}` -> (new text, why). The text is unchanged when
1093
+ an equivalent entry exists; an entry of the same name that starts something else is never replaced (why says
1094
+ so). The edit is the one member, inserted in the file's own layout; the result must parse back to the input plus
1095
+ that member, else it is refused."""
1096
+ return _json_mcp(text, command, '~/.claude.json')
1097
+
1098
+
1099
+ def kimi_mcp(text, command=MCP_COMMAND):
1100
+ """~/.kimi-code/mcp.json (Kimi Code's user MCP file, `resolveKimiHome()/mcp.json`; MEASURED in the 2.1.1 binary,
1101
+ 2026-10-15: a JSON object whose `mcpServers` object maps a name to a server) the way claude_mcp edits
1102
+ ~/.claude.json. Kimi Code starts a stdio server with its own environment, so the lane's variables arrive."""
1103
+ return _json_mcp(text, command, '~/.kimi-code/mcp.json')
1104
+
1105
+
1106
+ def _toml_loads(text, what='~/.codex/config.toml'):
986
1107
  try:
987
1108
  import tomllib
988
1109
  except ImportError: # Python < 3.11 on the host: the textual rules below still hold
@@ -990,7 +1111,7 @@ def _toml_loads(text):
990
1111
  try:
991
1112
  return tomllib.loads(text)
992
1113
  except tomllib.TOMLDecodeError as error:
993
- raise LaneError(f'~/.codex/config.toml is not valid TOML ({error}) — left alone')
1114
+ raise LaneError(f'{what} is not valid TOML ({error}) — left alone')
994
1115
 
995
1116
 
996
1117
  TOML_COMMAND = re.compile(r'''^[ \t]*command[ \t]*=[ \t]*"([^"\\]*)"[ \t]*(?:#.*)?$''')
@@ -1082,14 +1203,206 @@ def codex_mcp(text, command=MCP_COMMAND):
1082
1203
  return after, None
1083
1204
 
1084
1205
 
1085
- MCP_EDITORS = {'claude': claude_mcp, 'codex': codex_mcp}
1206
+ # ------------------------------------------------------------------------------------------------ TOML marker blocks
1207
+ # W12: what Lanes adds to a TOML file another program owns (Kimi Code's config.toml and tui.toml, Grok Build's
1208
+ # config.toml) is ONE block between `# >>> lanes <name> >>>` and `# <<< lanes <name> <<<` lines: appended at the end the
1209
+ # first time, replaced in place after that; nothing outside the markers is ever rewritten. A table another writer put
1210
+ # INSIDE the block (a toml_edit writer such as Throttle's Kimi adapter appends its `[[hooks]]` after the last table,
1211
+ # which can be ours) is kept, moved just below the end marker; anything else inside the markers that is not a table,
1212
+ # and markers that are not one ordered pair, refuse the file.
1213
+
1214
+ BLOCK_BEGIN = '# >>> lanes {name} >>>'
1215
+ BLOCK_END = '# <<< lanes {name} <<<'
1216
+ _TOML_TABLE = re.compile(r'^[ \t]*\[')
1217
+
1218
+
1219
+ def _block_rows(text, name):
1220
+ """(rows, begin index, end index) of the block `name` in `text`; (rows, None, None) when it has none. LaneError
1221
+ when its markers are not exactly one ordered pair."""
1222
+ begin, end = BLOCK_BEGIN.format(name=name), BLOCK_END.format(name=name)
1223
+ rows = (text or '').splitlines(True)
1224
+ marks = [(i, row.strip()) for i, row in enumerate(rows) if row.strip() in (begin, end)]
1225
+ if not marks:
1226
+ return rows, None, None
1227
+ if [mark for _i, mark in marks] != [begin, end]:
1228
+ raise LaneError(f'the `{begin}` … `{end}` lines are not one pair — fix the file by hand', 'source_invalid')
1229
+ return rows, marks[0][0], marks[1][0]
1230
+
1231
+
1232
+ def without_block(text, name):
1233
+ """`text` without the block `name` (its markers and every line between them): what the file says apart from us."""
1234
+ rows, first, last = _block_rows(text, name)
1235
+ return text or '' if first is None else ''.join(rows[:first] + rows[last + 1:])
1236
+
1237
+
1238
+ def toml_block(text, name, body, ours=lambda table: True):
1239
+ """`text` with the block `name` holding exactly `body` (whole lines) -> new text. `ours(table text)` says whether a
1240
+ table found inside an existing block is ours (replaced by `body`) or another writer's (kept, moved below the
1241
+ block). LaneError for a key or value inside the block that belongs to no table, or for broken markers."""
1242
+ begin, end = BLOCK_BEGIN.format(name=name), BLOCK_END.format(name=name)
1243
+ block = f'{begin}\n{body}{end}\n'
1244
+ if text is None or not text.strip():
1245
+ return block
1246
+ rows, first, last = _block_rows(text, name)
1247
+ if first is None:
1248
+ return text + ('' if text.endswith('\n') else '\n') + '\n' + block
1249
+ tables = []
1250
+ for row in rows[first + 1:last]:
1251
+ if _TOML_TABLE.match(row):
1252
+ tables.append([row])
1253
+ elif tables:
1254
+ tables[-1].append(row)
1255
+ elif row.strip() and not row.lstrip().startswith('#'):
1256
+ raise LaneError(f'the lanes {name} block holds a line outside any table ({row.strip()[:60]!r}) — fix the '
1257
+ 'file by hand', 'source_invalid')
1258
+ moved = ''.join(''.join(table) for table in tables if not ours(''.join(table)))
1259
+ return ''.join(rows[:first]) + block + moved + ''.join(rows[last + 1:])
1260
+
1261
+
1262
+ def _lanes_command(table):
1263
+ """A table of a lanes block that is ours: it starts lane-mcp or runs lane-signal."""
1264
+ return '/.lz/bin/lane-signal' in table or '/.lz/bin/lane-mcp' in table
1265
+
1266
+
1267
+ def grok_mcp(text, command=MCP_COMMAND):
1268
+ """Grok Build's $GROK_HOME/config.toml (~/.grok-build/config.toml in a lane, DM-LANES-GROK-HOME) with a
1269
+ `[mcp_servers.lanes]` table that starts <command> -> (new text, why). Written as the marker block `mcp`
1270
+ (toml_block) rather than through `grok mcp add` (the same table; the binary need not be there, and the file edit is
1271
+ what a test can check). Grok Build starts a stdio server with its own environment plus GROK_SESSION_ID (MEASURED in
1272
+ its source, evidence 2026-10-15 §5), so no `env` is needed. A `lanes` server defined outside the block is present
1273
+ when it starts this command with no args, else left alone. The input must parse (tomllib) and the result must parse
1274
+ to the input plus that table; without tomllib any `mcp_servers` outside the block refuses."""
1275
+ what = '~/.grok-build/config.toml'
1276
+ body = f'[mcp_servers.{MCP_NAME}]\ncommand = {json.dumps(command)}\n'
1277
+ try:
1278
+ parsed = _toml_loads(text or '', what)
1279
+ outside = without_block(text, 'mcp')
1280
+ rest = _toml_loads(outside, what)
1281
+ except LaneError as error:
1282
+ return text, str(error)
1283
+ if rest is None:
1284
+ if re.search(r'(?m)^[ \t]*\[*[ \t]*mcp_servers\b|^[ \t]*mcp_servers[ \t]*[.=]', outside):
1285
+ return text, (f'{what} defines mcp_servers in a form this Python (no tomllib) cannot read safely — add '
1286
+ f'the [mcp_servers.{MCP_NAME}] table by hand')
1287
+ else:
1288
+ servers = rest.get('mcp_servers')
1289
+ if servers is not None and not isinstance(servers, dict):
1290
+ return text, f'mcp_servers in {what} is not a table — left alone'
1291
+ current = (servers or {}).get(MCP_NAME)
1292
+ if current is not None:
1293
+ if _ours(current, command) and parsed == rest:
1294
+ return text, None
1295
+ shown = current.get('command') if isinstance(current, dict) else current
1296
+ return text, (f'mcp_servers.{MCP_NAME} in {what} is not the entry Lanes writes (command {shown!r}) — '
1297
+ 'left alone (remove it to let Lanes add its own)')
1298
+ try:
1299
+ after = toml_block(text, 'mcp', body, _lanes_command)
1300
+ except LaneError as error:
1301
+ return text, f'{what}: {error}'
1302
+ if rest is not None:
1303
+ expected = dict(rest, mcp_servers=dict(rest.get('mcp_servers') or {}, **{MCP_NAME: {'command': command}}))
1304
+ try:
1305
+ ok = _toml_loads(after, what) == expected
1306
+ except LaneError:
1307
+ ok = False
1308
+ if not ok:
1309
+ return text, 'the table could not be added without changing anything else — add it by hand'
1310
+ return after, None
1311
+
1312
+
1313
+ def _grok_cli_ours(entry, command):
1314
+ """A grok-cli server entry that already starts this lane-mcp: id `lanes`, stdio, our command, no args (its
1315
+ `enabled`, `label` and `env` are the person's to change: a disabled entry stays disabled)."""
1316
+ return isinstance(entry, dict) and entry.get('id') == MCP_NAME and entry.get('command') == command \
1317
+ and not entry.get('args') and entry.get('transport') == 'stdio' and 'url' not in entry
1318
+
1319
+
1320
+ def append_item(text, path, value):
1321
+ """`text` with `value` appended to the array at `path` (keys from the root), laid out like its items."""
1322
+ root = scan(text)
1323
+ node = root
1324
+ for step in path:
1325
+ node = node.get(step) if node.kind == 'object' else None
1326
+ if node is None:
1327
+ raise LaneError(f'no array at {".".join(path)}', 'source_invalid')
1328
+ if node.kind != 'array':
1329
+ raise LaneError(f'no array at {".".join(path)}', 'source_invalid')
1330
+ return _append(text, root, node, lambda indent, unit: _render(value, indent, unit))
1331
+
1332
+
1333
+ def grok_cli_mcp(text, command=MCP_COMMAND):
1334
+ """grok-cli's ~/.grok/user-settings.json (os.homedir()/.grok, whatever GROK_HOME says; MEASURED in the 1.1.7
1335
+ binary's source, 2026-10-15: `mcp.servers` is an array of {id, label, enabled, transport, command, args, env, cwd},
1336
+ each stdio server started as `grok-cli-<id>` with ONLY HOME, LOGNAME, PATH, SHELL, TERM and USER of grok-cli's
1337
+ environment plus the entry's `env`) with one server `lanes` -> (new text, why). The entry's `env` is `{}`: a file
1338
+ every lane shares can carry no lane's identity, and nothing secret goes in it; lane-mcp names its lane through
1339
+ whoami's parent fallback (grok-cli's own environment, DEF-1992). The file holds grok-cli's `apiKey`: the edit is the
1340
+ one member or item, every other byte kept, and the result must parse back to the input plus that entry."""
1341
+ what = '~/.grok/user-settings.json'
1342
+ entry = {'id': MCP_NAME, 'label': 'Lanes', 'enabled': True, 'transport': 'stdio', 'command': command, 'args': [],
1343
+ 'env': {}}
1344
+ if text is None or not text.strip():
1345
+ return json.dumps({'mcp': {'servers': [entry]}}, indent=2) + '\n', None
1346
+ doc = strict_json(text, what)
1347
+ if not isinstance(doc, dict):
1348
+ return text, f'{what} is not a JSON object — left alone'
1349
+ mcp = doc.get('mcp')
1350
+ servers = mcp.get('servers') if isinstance(mcp, dict) else None
1351
+ if (mcp is not None and not isinstance(mcp, dict)) or (servers is not None and not isinstance(servers, list)):
1352
+ return text, f'mcp.servers in {what} is not an array — left alone'
1353
+ named = [i for i, item in enumerate(servers or []) if isinstance(item, dict) and item.get('id') == MCP_NAME]
1354
+ if len(named) > 1:
1355
+ return text, f'{what} lists the server id {MCP_NAME!r} twice — left alone; fix it by hand'
1356
+ expected = dict(doc)
1357
+ if named:
1358
+ current = servers[named[0]]
1359
+ if _grok_cli_ours(current, command):
1360
+ return text, None
1361
+ if not _an_older_ours(current):
1362
+ return text, (f'the mcp server {MCP_NAME!r} in {what} is not the entry Lanes writes (command '
1363
+ f'{current.get("command")!r}) — left alone (remove it to let Lanes add its own)')
1364
+ item = scan(text).get('mcp').get('servers').items[named[0]]
1365
+ root = scan(text)
1366
+ after = text[:item.start] + _render(entry, _line_indent(text, item.start), _file_unit(text, root)) \
1367
+ + text[item.end:]
1368
+ expected['mcp'] = dict(mcp, servers=[entry if i == named[0] else s for i, s in enumerate(servers)])
1369
+ elif mcp is None:
1370
+ after = set_member(text, [], 'mcp', {'servers': [entry]})
1371
+ expected['mcp'] = {'servers': [entry]}
1372
+ elif servers is None:
1373
+ after = set_member(text, ['mcp'], 'servers', [entry])
1374
+ expected['mcp'] = dict(mcp, servers=[entry])
1375
+ else:
1376
+ after = append_item(text, ['mcp', 'servers'], entry)
1377
+ expected['mcp'] = dict(mcp, servers=list(servers) + [entry])
1378
+ if strict_json(after, what) != expected:
1379
+ return text, 'the entry could not be added without changing anything else — add it by hand'
1380
+ return after, None
1086
1381
 
1087
1382
 
1088
- def mcp_plan(home, agents=MCP_AGENTS, command=MCP_COMMAND):
1089
- """[{agent, path, before, after, why}] for each agent's config under `home` (pure: reads only). `after == before`
1090
- with no `why` = already there; a `why` = left alone, with the reason."""
1383
+ MCP_EDITORS = {'claude': claude_mcp, 'codex': codex_mcp, 'kimi': kimi_mcp, 'grok': grok_mcp, 'grok-cli': grok_cli_mcp}
1384
+
1385
+
1386
+ def mcp_agents(home):
1387
+ """The agents whose entry an install writes by default: MCP_ALWAYS, plus each of kimi, grok and grok-cli whose
1388
+ home directory (MCP_HOMES) is a real directory under `home` — never a link (the host writes through the volume)."""
1389
+ found = list(MCP_ALWAYS)
1390
+ for agent in MCP_AGENTS:
1391
+ if agent not in found:
1392
+ try:
1393
+ info = os.lstat(str(Path(home) / MCP_HOMES[agent]))
1394
+ except OSError:
1395
+ continue
1396
+ if stat.S_ISDIR(info.st_mode):
1397
+ found.append(agent)
1398
+ return tuple(found)
1399
+
1400
+
1401
+ def mcp_plan(home, agents=None, command=MCP_COMMAND):
1402
+ """[{agent, path, before, after, why}] for each agent's config under `home` (pure: reads only); `agents` defaults
1403
+ to mcp_agents(home). `after == before` with no `why` = already there; a `why` = left alone, with the reason."""
1091
1404
  plan = []
1092
- for agent in agents:
1405
+ for agent in (mcp_agents(home) if agents is None else agents):
1093
1406
  path = Path(home) / MCP_FILES[agent]
1094
1407
  try:
1095
1408
  _ident, before = _snapshot(path, home)
@@ -1111,14 +1424,14 @@ def mcp_diff(item):
1111
1424
  f'{path} (with the lanes MCP server)'))
1112
1425
 
1113
1426
 
1114
- def mcp_apply(home, agents=MCP_AGENTS, command=MCP_COMMAND):
1427
+ def mcp_apply(home, agents=None, command=MCP_COMMAND):
1115
1428
  """Write each agent's entry where it is missing -> [{agent, path, status, backup, why}], status one of `added`,
1116
1429
  `present`, `refused`. Each file is re-planned from its bytes at write time (edit_file: re-read before the
1117
1430
  write, one retry), kept as <file>.bak-<UTC> first when it existed, replaced atomically with its own mode (a new
1118
1431
  one 0600; a new ~/.codex is 0700). A symbolic link below `home` (the file, or ~/.codex) refuses that file.
1119
- Never raises for one file: the other still gets its entry."""
1432
+ Never raises for one file: the other still gets its entry. `agents` defaults to mcp_agents(home)."""
1120
1433
  results = []
1121
- for agent in agents:
1434
+ for agent in (mcp_agents(home) if agents is None else agents):
1122
1435
  path = Path(home) / MCP_FILES[agent]
1123
1436
  refusal = []
1124
1437
 
@@ -1138,3 +1451,178 @@ def mcp_apply(home, agents=MCP_AGENTS, command=MCP_COMMAND):
1138
1451
  results.append({'agent': agent, 'path': path, 'status': status, 'backup': saved,
1139
1452
  'why': refusal[0] if refusal else None})
1140
1453
  return results
1454
+
1455
+
1456
+ # ------------------------------------------------------------------------------------------------ publishing (W10 W4)
1457
+ # `lane publish <file>` and lane-mcp's `lanes_publish` put a file of this container on the Lanes docs page. Nothing here
1458
+ # copies the file anywhere: the request names it, and the host's lanes-docsd reads it through /proc/<container init>/root
1459
+ # on its own 5 s cycle (it trusts nothing in the request: the path is resolved inside the container again, the bytes
1460
+ # hashed again), stores it in the docs store and writes `<id>.receipt.json` back beside the request, which it removes.
1461
+ #
1462
+ # ~/.lz/state/publish/<lane id>/<id>.json the request (DM-LANES-PUBLISH-REQUEST): {v: 1, path, title, kind,
1463
+ # claimed_sha256, mime, bytes, for}, `path` the file's absolute path
1464
+ # in this container with every link resolved
1465
+ # ~/.lz/state/publish/<lane id>/<id>.receipt.json the host's answer (DM-LANES-PUBLISH-RECEIPT): {v: 1, id, request,
1466
+ # sha256, bytes, url, at, outcome: ok|refused, reason?}
1467
+
1468
+ PUBLISH_KINDS = ('md', 'html', 'svg', 'file') # docs_store.KINDS without `upload` (a person's upload is the host's)
1469
+ PUBLISH_MAX = 32 << 20 # docsd.PUBLISH_LIMIT: a larger file is refused here and on the host
1470
+ PUBLISH_PICKUP = 'lanes-docsd' # what takes a request (docsd.Pickup)
1471
+ PUBLISH_LATENCY_S = 5 # docsd.PICKUP_INTERVAL: one pick-up cycle
1472
+ PUBLISH_DIR = ('state', 'publish') # under ~/.lz
1473
+ PUBLISH_ID = re.compile(r'[a-z0-9]{10,20}') # docs_store.ID_RE
1474
+ RECEIPT_SUFFIX = '.receipt.json'
1475
+ TITLE_MAX = 200 # docs_store.MAX_TITLE
1476
+ LOGIN_MAX = 254 # members.MAX_LOGIN
1477
+ ID_TIME = '0123456789abcdefghijklmnopqrstuvwxyz' # docs_store.BASE36: epoch seconds, zero-padded to 6
1478
+ ID_RANDOM = 'abcdefghijklmnopqrstuvwxyz234567' # docs_store.BASE32: 6 random characters
1479
+ KIND_BY_SUFFIX = {'.md': 'md', '.markdown': 'md', '.html': 'html', '.htm': 'html', '.svg': 'svg'}
1480
+ MIME_BY_SUFFIX = {'.md': 'text/markdown', '.markdown': 'text/markdown', '.html': 'text/html', '.htm': 'text/html',
1481
+ '.svg': 'image/svg+xml', '.txt': 'text/plain', '.json': 'application/json', '.csv': 'text/csv',
1482
+ '.pdf': 'application/pdf', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
1483
+ '.gif': 'image/gif', '.webp': 'image/webp'} # = libexec/docs_store.MIME_BY_SUFFIX (DEF-1920)
1484
+ REFUSED_TEXT = frozenset(('Cc', 'Cf', 'Cs', 'Co', 'Cn', 'Zl', 'Zp')) # members.REFUSED_CATEGORIES
1485
+
1486
+
1487
+ def new_publish_id(now=None):
1488
+ """A request id in docs_store's grammar (`new_id`: base36 epoch seconds zero-padded to 6, then 6 random base32),
1489
+ pinned equal to it by test_lane_publish. The container cannot import the host's store."""
1490
+ import secrets
1491
+ seconds, digits = int(time.time() if now is None else now), ''
1492
+ while True:
1493
+ seconds, rest = divmod(seconds, 36)
1494
+ digits = ID_TIME[rest] + digits
1495
+ if not seconds:
1496
+ break
1497
+ return digits.rjust(6, '0') + ''.join(secrets.choice(ID_RANDOM) for _ in range(6))
1498
+
1499
+
1500
+ def publish_kind(path, kind=None):
1501
+ """`kind` when given (one of PUBLISH_KINDS), else from the file name: .md/.markdown md, .html/.htm html, .svg svg,
1502
+ anything else file. LaneError `invalid_kind`."""
1503
+ if kind is None or kind == '':
1504
+ return KIND_BY_SUFFIX.get(os.path.splitext(str(path))[1].lower(), 'file')
1505
+ if kind not in PUBLISH_KINDS:
1506
+ raise LaneError(f'kind {kind!r} is not one of {", ".join(PUBLISH_KINDS)}', 'invalid_kind')
1507
+ return kind
1508
+
1509
+
1510
+ def _one_line(value, what, limit, blanks, code):
1511
+ """`value` when it is one printable line of at most `limit` characters without outer blanks (members._text's rule;
1512
+ `blanks`: inner spaces allowed), else LaneError `code`. The host checks it again."""
1513
+ if (not isinstance(value, str) or not value or len(value) > limit or value != value.strip()
1514
+ or any(unicodedata.category(char) in REFUSED_TEXT for char in value)
1515
+ or (not blanks and any(char.isspace() for char in value))):
1516
+ raise LaneError(f'{what} is one line of at most {limit} printable characters'
1517
+ + ('' if blanks else ' without blanks'), code)
1518
+ return value
1519
+
1520
+
1521
+ def publish_dir(root, lane):
1522
+ return Path(root).joinpath(*PUBLISH_DIR, lane)
1523
+
1524
+
1525
+ def publish(root, home, path, lane, title=None, for_login=None, kind=None, now=None):
1526
+ """Write a publish request for the file at `path` (as this process reaches it) from `lane` -> {id, lane, path,
1527
+ kind, title, for, bytes, sha256, request, receipt}: `path` the file's container path, `request`/`receipt` the
1528
+ container paths of the request and of the receipt lanes-docsd writes. `root` is ~/.lz and `home` /home/coder as
1529
+ this process reaches them (themselves in a container, test directories otherwise).
1530
+
1531
+ The file must resolve (os.path.realpath) inside `home` to a regular file of at most PUBLISH_MAX bytes; it is read
1532
+ once, never through a link at its last component and never blocking on a FIFO, to hash it (`claimed_sha256`, which
1533
+ the host compares with its own). Refusals (LaneError): `not_in_lane`, `invalid_title`, `invalid_for`,
1534
+ `invalid_kind`, `outside_home`, `not_a_file`, `too_large`. The request is replaced atomically (0644); nothing else
1535
+ is written."""
1536
+ import hashlib
1537
+ import mimetypes
1538
+ if not isinstance(lane, str) or not LANE_RE.fullmatch(lane):
1539
+ raise LaneError('this agent runs in no lane to publish from', 'not_in_lane')
1540
+ title = None if title in (None, '') else _one_line(title, 'a title', TITLE_MAX, True, 'invalid_title')
1541
+ for_login = None if for_login in (None, '') else _one_line(for_login, 'a login', LOGIN_MAX, False, 'invalid_for')
1542
+ base = os.path.realpath(str(home))
1543
+ real = os.path.realpath(str(path))
1544
+ if not real.startswith(base.rstrip(os.sep) + os.sep):
1545
+ raise LaneError(f'{path} is not a file under {CONTAINER_HOME} (with every link resolved): only this '
1546
+ 'container\'s home can be published', 'outside_home')
1547
+ kind = publish_kind(real, kind)
1548
+ try:
1549
+ fd = os.open(real, os.O_RDONLY | os.O_NONBLOCK | getattr(os, 'O_NOFOLLOW', 0) | getattr(os, 'O_CLOEXEC', 0))
1550
+ except OSError as error:
1551
+ raise LaneError(f'{path}: {error.strerror or "cannot be opened"}', 'not_a_file') from None
1552
+ digest, size = hashlib.sha256(), 0
1553
+ try:
1554
+ info = os.fstat(fd)
1555
+ if not stat.S_ISREG(info.st_mode):
1556
+ raise LaneError(f'{path} is not a regular file', 'not_a_file')
1557
+ if info.st_size > PUBLISH_MAX:
1558
+ raise LaneError(f'{path} is larger than {PUBLISH_MAX >> 20} MiB', 'too_large')
1559
+ while True:
1560
+ chunk = os.read(fd, 1 << 16)
1561
+ if not chunk:
1562
+ break
1563
+ size += len(chunk)
1564
+ if size > PUBLISH_MAX:
1565
+ raise LaneError(f'{path} is larger than {PUBLISH_MAX >> 20} MiB', 'too_large')
1566
+ digest.update(chunk)
1567
+ finally:
1568
+ os.close(fd)
1569
+ container = CONTAINER_HOME + '/' + os.path.relpath(real, base).replace(os.sep, '/')
1570
+ name = os.path.basename(real)
1571
+ # The same table as libexec/docs_store.MIME_BY_SUFFIX: Python's built-in map gained `.md` only in 3.12, so the
1572
+ # request's `mime` must not depend on the interpreter (the host compares nothing on it, but receipts carry it).
1573
+ mime, encoding = MIME_BY_SUFFIX.get(os.path.splitext(name)[1].lower()), None
1574
+ if not mime:
1575
+ mime, encoding = mimetypes.MimeTypes().guess_type(name) # the built-in table, as the host's
1576
+ request = {'v': 1, 'path': container, 'title': title, 'kind': kind, 'claimed_sha256': digest.hexdigest(),
1577
+ 'mime': mime if mime and not encoding else 'application/octet-stream', 'bytes': size, 'for': for_login}
1578
+ directory = publish_dir(root, lane)
1579
+ directory.mkdir(parents=True, exist_ok=True)
1580
+ for _ in range(16):
1581
+ request_id = new_publish_id(now)
1582
+ if not any(os.path.lexists(str(directory / (request_id + suffix))) for suffix in ('.json', RECEIPT_SUFFIX)):
1583
+ break
1584
+ else:
1585
+ raise LaneError('could not mint a free request id', 'refused')
1586
+ atomic_replace(directory / f'{request_id}.json', json.dumps(request, ensure_ascii=False, indent=1) + '\n', 0o644)
1587
+ where = LZ + '/' + '/'.join(PUBLISH_DIR) + f'/{lane}/{request_id}'
1588
+ return {'id': request_id, 'lane': lane, 'path': container, 'kind': kind, 'title': title, 'for': for_login,
1589
+ 'bytes': size, 'sha256': request['claimed_sha256'], 'request': where + '.json',
1590
+ 'receipt': where + RECEIPT_SUFFIX}
1591
+
1592
+
1593
+ def latest_receipt(root, lane):
1594
+ """The newest receipt lanes-docsd wrote back for `lane` (~/.lz/state/publish/<lane>/<id>.receipt.json; the latest
1595
+ `at` — whole seconds —, then the file written last, then the id) as lanes_status shows it -> {id, url, at,
1596
+ outcome, request, reason?}, or None (no receipt yet, or none that reads as one). Each file is read as a regular
1597
+ file only (read_regular: no link, no FIFO)."""
1598
+ if not isinstance(lane, str) or not LANE_RE.fullmatch(lane):
1599
+ return None
1600
+ directory = publish_dir(root, lane)
1601
+ try:
1602
+ names = os.listdir(str(directory))
1603
+ except OSError:
1604
+ return None
1605
+ best = None
1606
+ for name in names:
1607
+ request_id = name[:-len(RECEIPT_SUFFIX)] if name.endswith(RECEIPT_SUFFIX) else ''
1608
+ if not PUBLISH_ID.fullmatch(request_id):
1609
+ continue
1610
+ receipt = read_json(directory / name)
1611
+ if not (isinstance(receipt, dict) and receipt.get('v') == 1 and isinstance(receipt.get('at'), str)
1612
+ and receipt.get('outcome') in ('ok', 'refused')):
1613
+ continue
1614
+ try:
1615
+ written = os.stat(str(directory / name), follow_symlinks=False).st_mtime_ns
1616
+ except OSError:
1617
+ continue
1618
+ key = (receipt['at'], written, request_id)
1619
+ if best is None or key > best[0]:
1620
+ best = (key, request_id, receipt)
1621
+ if best is None:
1622
+ return None
1623
+ _key, request_id, receipt = best
1624
+ out = {'id': receipt.get('id'), 'url': receipt.get('url'), 'at': receipt['at'], 'outcome': receipt['outcome'],
1625
+ 'request': request_id}
1626
+ if receipt['outcome'] == 'refused':
1627
+ out['reason'] = receipt.get('reason')
1628
+ return out