witbitz-code 1.6.4__tar.gz → 1.6.5__tar.gz

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 (100) hide show
  1. {witbitz_code-1.6.4/src/witbitz_code.egg-info → witbitz_code-1.6.5}/PKG-INFO +1 -1
  2. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/pyproject.toml +1 -1
  3. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/__init__.py +1 -1
  4. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/auto.py +9 -4
  5. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/cli.py +1 -1
  6. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/models.py +10 -0
  7. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/plugins.py +8 -5
  8. {witbitz_code-1.6.4 → witbitz_code-1.6.5/src/witbitz_code.egg-info}/PKG-INFO +1 -1
  9. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_auto.py +22 -0
  10. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_confidential.py +13 -0
  11. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_plugins.py +10 -0
  12. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/LICENSE +0 -0
  13. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/README.md +0 -0
  14. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/setup.cfg +0 -0
  15. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/__main__.py +0 -0
  16. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/_js.py +0 -0
  17. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/account.py +0 -0
  18. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/asks.py +0 -0
  19. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/attachments.py +0 -0
  20. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/auto_runner.py +0 -0
  21. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/codekeys.py +0 -0
  22. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/codetools.py +0 -0
  23. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/compress.py +0 -0
  24. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/computers.py +0 -0
  25. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/confidential.py +0 -0
  26. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/confidential_models.json +0 -0
  27. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/connector.py +0 -0
  28. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/devicelink.py +0 -0
  29. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/dirs.py +0 -0
  30. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/folders.py +0 -0
  31. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/gateway.py +0 -0
  32. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/icons/witbitz-code-wsl.ico +0 -0
  33. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/icons/witbitz-code.ico +0 -0
  34. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/keys.py +0 -0
  35. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/link.py +0 -0
  36. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/mkdir.py +0 -0
  37. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/net.py +0 -0
  38. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/offered_models.json +0 -0
  39. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
  40. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
  41. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
  42. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
  43. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/outputs.py +0 -0
  44. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/pair.py +0 -0
  45. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/pairings.py +0 -0
  46. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/payers.py +0 -0
  47. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/policy.py +0 -0
  48. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/ports.py +0 -0
  49. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/qr.py +0 -0
  50. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/receipt.py +0 -0
  51. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/relay.py +0 -0
  52. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/seen.py +0 -0
  53. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/tinfoil_read.py +0 -0
  54. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/tools_catalog.json +0 -0
  55. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/tools_probe.py +0 -0
  56. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/toolsetup.py +0 -0
  57. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/ui.py +0 -0
  58. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/ui_static/app.css +0 -0
  59. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/ui_static/app.js +0 -0
  60. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/ui_static/index.html +0 -0
  61. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/uitools.py +0 -0
  62. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/uninstall.py +0 -0
  63. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/winagent.py +0 -0
  64. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/winbg.py +0 -0
  65. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/winconsole.py +0 -0
  66. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/wizard.py +0 -0
  67. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code/wslstart.py +0 -0
  68. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code.egg-info/SOURCES.txt +0 -0
  69. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
  70. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code.egg-info/entry_points.txt +0 -0
  71. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code.egg-info/requires.txt +0 -0
  72. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/src/witbitz_code.egg-info/top_level.txt +0 -0
  73. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_asks.py +0 -0
  74. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_attachments.py +0 -0
  75. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_cli.py +0 -0
  76. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_codekeys.py +0 -0
  77. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_codetools.py +0 -0
  78. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_connector_e2e.py +0 -0
  79. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_devicelink.py +0 -0
  80. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_dirs.py +0 -0
  81. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_files_and_compress.py +0 -0
  82. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_folders.py +0 -0
  83. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_outputs.py +0 -0
  84. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_pair_flows.py +0 -0
  85. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_policy.py +0 -0
  86. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_ports.py +0 -0
  87. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_ports_win.py +0 -0
  88. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_registry.py +0 -0
  89. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_relay_codec.py +0 -0
  90. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_setup.py +0 -0
  91. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_tools_probe.py +0 -0
  92. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_toolsetup.py +0 -0
  93. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_ui.py +0 -0
  94. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_uitools.py +0 -0
  95. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_uninstall.py +0 -0
  96. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_winagent.py +0 -0
  97. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_winbg.py +0 -0
  98. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_winconsole.py +0 -0
  99. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_windows_files.py +0 -0
  100. {witbitz_code-1.6.4 → witbitz_code-1.6.5}/tests/test_wslstart.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: witbitz-code
3
- Version: 1.6.4
3
+ Version: 1.6.5
4
4
  Summary: Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay.
5
5
  Author: Witbitz
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "witbitz-code"
7
- version = "1.6.4"
7
+ version = "1.6.5"
8
8
  description = "Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -4,4 +4,4 @@ The Python implementation of tools/witbitz-code.mjs (docs/opencode-relay.md phas
4
4
  vectors and cross-implementation tests.
5
5
  """
6
6
 
7
- __version__ = "1.6.4"
7
+ __version__ = "1.6.5"
@@ -207,6 +207,7 @@ _SECRETISH = re.compile(r"secret|credential", re.ASCII | re.IGNORECASE)
207
207
 
208
208
 
209
209
  _WIN_ABS = re.compile(r"\A(?:\\\\\?\\)?([A-Za-z]):[\\/]")
210
+ _WIN_UNC = re.compile(r"\A\\\\(?!\?\\)([^\\/]+)[\\/]([^\\/]+)") # \\server\share — a project on a network share
210
211
 
211
212
 
212
213
  def windows_to_posix(path: Any) -> Any:
@@ -217,12 +218,16 @@ def windows_to_posix(path: Any) -> Any:
217
218
  if not isinstance(path, str):
218
219
  return path
219
220
  m = _WIN_ABS.match(path)
220
- return "/" + m.group(1).lower() + "/" + path[m.end():].replace("\\", "/") if m else path
221
+ if m:
222
+ return "/" + m.group(1).lower() + "/" + path[m.end():].replace("\\", "/")
223
+ u = _WIN_UNC.match(path) # \\server\share\x → /unc/server/share/x: never one of the /c/… drive spellings
224
+ return "/unc/" + u.group(1).lower() + "/" + u.group(2) + path[u.end():].replace("\\", "/") if u else path
221
225
 
222
226
 
223
227
  def _windows_project(ctx: dict) -> bool:
224
228
  """A project on Windows: there a backslash separates folders too (..\\..\\x climbs out exactly as ../../x does)."""
225
- return bool(_WIN_ABS.match(str(ctx.get("directory") or "")))
229
+ d = str(ctx.get("directory") or "")
230
+ return bool(_WIN_ABS.match(d) or _WIN_UNC.match(d))
226
231
 
227
232
 
228
233
  def _sensitive_path(abs_path: str) -> bool:
@@ -265,7 +270,7 @@ def _word_stays_in_project(w: str, ctx: dict) -> bool:
265
270
  v = v[eq + 1:]
266
271
  if not v:
267
272
  return True
268
- v = windows_to_posix(v.replace("\\", "/") if _windows_project(ctx) and not v.startswith("~") else v)
273
+ v = windows_to_posix(v if not _windows_project(ctx) or v.startswith("~") or _WIN_ABS.match(v) or _WIN_UNC.match(v) else v.replace("\\", "/"))
269
274
  if "$" in v or re.search(r"(^|/)\.\.(/|\Z)", v):
270
275
  return False
271
276
  if v.startswith("~"):
@@ -354,7 +359,7 @@ def _edit_inside_project(patterns: Any, ctx: dict) -> bool:
354
359
  if not isinstance(p, str) or not p or re.search(r"[*?\[\]{}]", p):
355
360
  return False # a glob is not a precise target
356
361
  if _windows_project(ctx):
357
- p = windows_to_posix(p.replace("\\", "/") if not _WIN_ABS.match(p) else p)
362
+ p = windows_to_posix(p if _WIN_ABS.match(p) or _WIN_UNC.match(p) else p.replace("\\", "/"))
358
363
  a = posix_normalize(p if p.startswith("/") else d + "/" + p)
359
364
  r = next((x for x in roots if a.startswith(x + "/")), "")
360
365
  if not (r and not _sensitive_path(a[len(r):])):
@@ -240,7 +240,7 @@ async def serve(args: list[str], *, stop: Any = None, live_out: dict | None = No
240
240
  + (f" — held by {held}: an earlier witbitz-code still running? witbitz-code setup offers to stop it" if held
241
241
  else " — free that port and run serve again"))
242
242
  return 1
243
- route = models.proxy_config(proxy.port)
243
+ route = {**models.proxy_config(proxy.port), **models.default_model_config()}
244
244
  if await is_listening(port) and "--no-opencode" not in args:
245
245
  _err(f"witbitz-code: OpenCode is already running on 127.0.0.1:{port}, started some other way — it cannot use your account's keys.")
246
246
  _err(" Close it, then run witbitz-code serve again (it starts OpenCode itself).")
@@ -68,6 +68,16 @@ def variants_for(m: dict) -> dict | None:
68
68
  return {e: {"reasoningEffort": e} for e in efforts} if efforts else None
69
69
 
70
70
 
71
+ # The model OpenCode starts with when the page has chosen none — the catalog's confidential default, as the Node connector's
72
+ # config names it (tools/opencode-config.mjs; agent/modelCatalog.mjs DEFAULTS.confidential — held equal by test). Without it
73
+ # OpenCode picked a model itself (the owner, 2026-10-09: "Make deepseek 4.1 flash the default for code").
74
+ DEFAULT_MODEL = "deepseek/deepseek-v4.1-flash"
75
+
76
+
77
+ def default_model_config() -> dict:
78
+ return {"model": f"trustedrouter/{DEFAULT_MODEL}"}
79
+
80
+
71
81
  def proxy_config(proxy_port: int, cat: list[dict] | None = None) -> dict:
72
82
  """OpenCode config that sends TrustedRouter through the proxy and declares the confidential models — a text-only one
73
83
  still receives images (the proxy reads them through Tinfoil). Merged by OpenCode over the user's own config."""
@@ -133,8 +133,11 @@ def notes_rules(s: dict, root_dir: str) -> list[dict]:
133
133
  return []
134
134
  folder = f"{root_dir}/{notes_key(os.path.abspath(root))}"
135
135
  plain = bool(s.get("path")) and s.get("directory") == "/" + str(s.get("path"))
136
- rel = os.path.relpath(folder, "/" if plain else os.path.abspath(root))
137
- return [
138
- {"permission": "external_directory", "pattern": f"{folder}/*", "action": "allow"},
139
- {"permission": "edit", "pattern": f"{rel}/notes/*", "action": "allow"},
140
- ]
136
+ read = {"permission": "external_directory", "pattern": f"{folder}/*", "action": "allow"}
137
+ try:
138
+ rel = os.path.relpath(folder, "/" if plain else os.path.abspath(root))
139
+ except ValueError:
140
+ # Windows, the project on another drive or a network share (\\\\192.168.57.47\\Projects — the owner, 2026-10-09:
141
+ # "path is on mount 'C:', start on mount …"): no relative path reaches the notes, so their writes ask; reads stay.
142
+ return [read]
143
+ return [read, {"permission": "edit", "pattern": f"{rel}/notes/*", "action": "allow"}]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: witbitz-code
3
- Version: 1.6.4
3
+ Version: 1.6.5
4
4
  Summary: Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay.
5
5
  Author: Witbitz
6
6
  License-Expression: MIT
@@ -678,3 +678,25 @@ def test_auto_switches_on_for_a_windows_folder_and_talks_to_opencode_with_it_as_
678
678
  assert await until(lambda: oc.replies(), 4)
679
679
  assert oc.replies()[0]["dir"] == win, "OpenCode is asked with the folder exactly as it named it"
680
680
  with_runner(tmp_path, body)
681
+
682
+
683
+ # A project on a network share (the owner's second desktop, 2026-10-09: \\192.168.57.47\Projects) — Auto refused it too.
684
+ _UNC = "\\\\192.168.57.47\\Projects\\geo"
685
+
686
+
687
+ @pytest.mark.parametrize("req,stage", [
688
+ ({"permission": "bash", "patterns": ["ls src"], "metadata": {"command": "ls src"}}, "fast-allow"),
689
+ ({"permission": "bash", "patterns": ["cat C:/Windows/win.ini"], "metadata": {"command": "cat C:/Windows/win.ini"}}, "review"),
690
+ ({"permission": "edit", "patterns": ["src\\a.py"], "metadata": {}}, "fast-allow"),
691
+ ({"permission": "edit", "patterns": ["..\\other\\x"], "metadata": {}}, "review"),
692
+ ({"permission": "edit", "patterns": [_UNC + "\\src\\a.py"], "metadata": {}}, "fast-allow"),
693
+ ({"permission": "edit", "patterns": ["\\\\192.168.57.47\\Projects\\other\\a.py"], "metadata": {}}, "review"),
694
+ ])
695
+ def test_a_project_on_a_network_share_is_judged_like_any_other(req, stage):
696
+ assert _stage(req, _UNC, "C:\\Users\\u") == stage
697
+
698
+
699
+ def test_auto_switches_on_for_a_network_share(tmp_path):
700
+ async def body(oc, runner, verdicts, make, client):
701
+ assert runner.set_auto("ses_main", _UNC, True)
702
+ with_runner(tmp_path, body)
@@ -353,3 +353,16 @@ def test_progress_goes_only_to_a_channel_a_page_is_watching():
353
353
  await asyncio.sleep(0)
354
354
  run(main())
355
355
  assert len(sent) == 1 and sent[0]["t"] == "progress" and sent[0]["phase"] == "checking" and isinstance(sent[0]["ts"], int)
356
+
357
+
358
+ def test_opencode_starts_on_the_catalog_s_confidential_default():
359
+ """The Python connector named no model, so OpenCode picked one itself (the owner, 2026-10-09: "Make deepseek 4.1 flash the
360
+ default for code"). It names the catalog's confidential default — the same one the Node connector's config names."""
361
+ import re
362
+ from pathlib import Path
363
+ from witbitz_code import models
364
+ src = (Path(__file__).resolve().parents[3] / "agent" / "modelCatalog.mjs").read_text(encoding="utf-8")
365
+ want = re.search(r"DEFAULTS = \{[^}]*confidential: '([^']+)'", src).group(1)
366
+ entry = next(m for m in models.catalog() if m["model"] == models.DEFAULT_MODEL)
367
+ assert want in (entry.get("id"), entry["model"].split("/")[-1]), f"catalog default {want!r} vs {models.DEFAULT_MODEL!r}"
368
+ assert models.default_model_config() == {"model": "trustedrouter/deepseek/deepseek-v4.1-flash"}
@@ -182,3 +182,13 @@ def test_the_installed_media_server_answers_mcp(tmp_path):
182
182
  replies = {m["id"]: m for m in map(json.loads, out.splitlines())}
183
183
  assert replies[1]["result"]["serverInfo"]["name"] == "witbitz-media"
184
184
  assert "audio" in [t["name"] for t in replies[2]["result"]["tools"]]
185
+
186
+
187
+ def test_notes_rules_on_another_drive_keep_the_reads_and_never_raise(monkeypatch):
188
+ """Windows, a project on a network share and the notes on C: — relpath raises ("path is on mount 'C:', start on mount
189
+ '\\\\192.168.57.47\\Projects'", the owner's second desktop, 2026-10-09) and the session's setup died with it."""
190
+ def cross(*_a, **_k):
191
+ raise ValueError("path is on mount 'C:', start on mount '\\\\192.168.57.47\\Projects'")
192
+ monkeypatch.setattr(plugins.os.path, "relpath", cross)
193
+ rules = plugins.notes_rules({"directory": "/p/geo", "path": ""}, "/n")
194
+ assert [r["permission"] for r in rules] == ["external_directory"], "reads allowed; writes ask (no relative path reaches)"
File without changes
File without changes
File without changes