pocketshell 0.5.5__tar.gz → 0.5.6__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 (93) hide show
  1. {pocketshell-0.5.5 → pocketshell-0.5.6}/PKG-INFO +1 -1
  2. {pocketshell-0.5.5 → pocketshell-0.5.6}/pyproject.toml +1 -1
  3. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/sessions.py +28 -1
  4. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_sessions_create_routing.py +31 -0
  5. {pocketshell-0.5.5 → pocketshell-0.5.6}/uv.lock +1 -1
  6. {pocketshell-0.5.5 → pocketshell-0.5.6}/.gitignore +0 -0
  7. {pocketshell-0.5.5 → pocketshell-0.5.6}/LICENSE +0 -0
  8. {pocketshell-0.5.5 → pocketshell-0.5.6}/README.md +0 -0
  9. {pocketshell-0.5.5 → pocketshell-0.5.6}/scheduler/README.md +0 -0
  10. {pocketshell-0.5.5 → pocketshell-0.5.6}/scheduler/pocketshell-usage-capture.service +0 -0
  11. {pocketshell-0.5.5 → pocketshell-0.5.6}/scheduler/pocketshell-usage-capture.timer +0 -0
  12. {pocketshell-0.5.5 → pocketshell-0.5.6}/scripts/check-cgroup-cap-proof.sh +0 -0
  13. {pocketshell-0.5.5 → pocketshell-0.5.6}/scripts/check-product-tmux-absent.sh +0 -0
  14. {pocketshell-0.5.5 → pocketshell-0.5.6}/scripts/check-python-ci-lock.sh +0 -0
  15. {pocketshell-0.5.5 → pocketshell-0.5.6}/scripts/sync-readme-exit-codes.py +0 -0
  16. {pocketshell-0.5.5 → pocketshell-0.5.6}/scripts/test-usage-missing-window-mutation.sh +0 -0
  17. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/__init__.py +0 -0
  18. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/__main__.py +0 -0
  19. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/agent_card_push.py +0 -0
  20. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/agent_log.py +0 -0
  21. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/agents.py +0 -0
  22. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/agents_kind.py +0 -0
  23. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/aplexer.py +0 -0
  24. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/cards.py +0 -0
  25. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/cgroup_agents.py +0 -0
  26. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/cli.py +0 -0
  27. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/daemon.py +0 -0
  28. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/engines.py +0 -0
  29. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/env.py +0 -0
  30. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/github.py +0 -0
  31. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/hooks.py +0 -0
  32. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/logs.py +0 -0
  33. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/memcap.py +0 -0
  34. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/profiles.py +0 -0
  35. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/prune_attachments.py +0 -0
  36. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/push.py +0 -0
  37. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/repos.py +0 -0
  38. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/serve.py +0 -0
  39. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/session_enum.py +0 -0
  40. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/tree.py +0 -0
  41. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/usage.py +0 -0
  42. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/usage_capture.py +0 -0
  43. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/usage_reset.py +0 -0
  44. {pocketshell-0.5.5 → pocketshell-0.5.6}/src/pocketshell/workspaces.py +0 -0
  45. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/__init__.py +0 -0
  46. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/conftest.py +0 -0
  47. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/data/quse-0.0.11-usage.json +0 -0
  48. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/data/quse-0.0.13-usage.json +0 -0
  49. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/data/quse-0.0.15-usage.json +0 -0
  50. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/data/quse-0.0.15-usage.ndjson +0 -0
  51. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/data/quse-0.0.15-usage.provenance.txt +0 -0
  52. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/data/quse-0.0.9-usage.json +0 -0
  53. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/data/sshd_anchor/Dockerfile.from-unanchored-base +0 -0
  54. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/fixtures/aplexer/snapshot-agent.json +0 -0
  55. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/fixtures/aplexer/snapshot-crashed-start.json +0 -0
  56. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/fixtures/aplexer/snapshot-liveness-mix.json +0 -0
  57. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/fixtures/aplexer/snapshot-mid-create.json +0 -0
  58. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/fixtures/aplexer/snapshot-post-kill-window.json +0 -0
  59. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/fixtures/aplexer/snapshot-reported-state.json +0 -0
  60. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/fixtures/aplexer/snapshot.json +0 -0
  61. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_agent_card_push.py +0 -0
  62. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_agent_log.py +0 -0
  63. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_agents.py +0 -0
  64. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_agents_kind.py +0 -0
  65. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_aplexer_contract.py +0 -0
  66. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_aplexer_resolution.py +0 -0
  67. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_cards.py +0 -0
  68. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_cards_push_notify.py +0 -0
  69. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_cgroup_agents.py +0 -0
  70. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_cli.py +0 -0
  71. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_daemon.py +0 -0
  72. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_daemon_fallback.py +0 -0
  73. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_engines.py +0 -0
  74. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_env.py +0 -0
  75. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_github.py +0 -0
  76. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_hooks.py +0 -0
  77. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_logs.py +0 -0
  78. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_profiles.py +0 -0
  79. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_prune_attachments.py +0 -0
  80. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_push.py +0 -0
  81. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_repos.py +0 -0
  82. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_serve.py +0 -0
  83. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_session_cgroup_kernel_proof.py +0 -0
  84. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_session_enum.py +0 -0
  85. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_sessions.py +0 -0
  86. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_sessions_attach.py +0 -0
  87. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_sessions_kill.py +0 -0
  88. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_sessions_mem_cap.py +0 -0
  89. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_tree.py +0 -0
  90. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_usage.py +0 -0
  91. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_usage_capture.py +0 -0
  92. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_usage_reset.py +0 -0
  93. {pocketshell-0.5.5 → pocketshell-0.5.6}/tests/test_workspaces.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pocketshell
3
- Version: 0.5.5
3
+ Version: 0.5.6
4
4
  Summary: Unified server-side Python utility for the PocketShell Android and desktop clients.
5
5
  Project-URL: Homepage, https://github.com/PocketShell-io/pocketshell-cli
6
6
  Project-URL: Issues, https://github.com/PocketShell-io/pocketshell-cli/issues
@@ -11,7 +11,7 @@ name = "pocketshell"
11
11
  # since the extraction (PocketShell-io/pocketshell#2643): the first
12
12
  # self-governed release is 0.5.5, continuing from the last app-coupled
13
13
  # release 0.5.4 so `uv tool upgrade pocketshell` stays monotonic.
14
- version = "0.5.5"
14
+ version = "0.5.6"
15
15
  description = "Unified server-side Python utility for the PocketShell Android and desktop clients."
16
16
  readme = "README.md"
17
17
  requires-python = ">=3.11"
@@ -9,6 +9,7 @@ import sys
9
9
  import time
10
10
  from dataclasses import dataclass
11
11
  from typing import Any, Mapping, Optional, Sequence
12
+ from uuid import uuid4
12
13
 
13
14
  import click
14
15
 
@@ -181,7 +182,28 @@ def aplexer_start_argv(
181
182
  profile: Optional[str],
182
183
  memory_bytes: Optional[int],
183
184
  ) -> list[str]:
184
- """Build the detached ``a --json start`` invocation."""
185
+ """Build the detached ``a --json start`` invocation.
186
+
187
+ A capped start is wrapped in a transient per-user systemd scope
188
+ (``systemd-run --user --scope``). aplexer's worker moves the workload
189
+ into its delegated scope by writing its own pgid into that scope's
190
+ ``cgroup.procs`` from a pre_exec hook, and cgroup-v2 grants that write
191
+ only on the common ancestor of the writer's cgroup and the target.
192
+ Spawned straight from an SSH session, the worker lives in
193
+ ``session-*.scope``, the workload scope lands under
194
+ ``user@<uid>.service/app.slice``, and their common ancestor
195
+ (``user-<uid>.slice``) is root-owned — so every capped create died with
196
+ ``spawn workload: Permission denied (os error 13)`` (#2625; the same
197
+ runner gap PR #2590 documented from the CI side). Inside a user-manager
198
+ scope the common ancestor is the user-owned ``app.slice`` — the shape
199
+ every session still had under the tmux arm's ``robust.slice`` before
200
+ #2561 removed tmux. The scope becomes the worker's home for the
201
+ session's whole life (aplexer's record discloses it as
202
+ ``worker_cgroup``), and ``--collect`` reaps the unit once the session
203
+ is gone. Only capped starts wrap: an uncapped ``a start`` (the Docker
204
+ fixture's ``--mem none``) performs no cgroup migration and must keep
205
+ working on hosts with no user manager at all.
206
+ """
185
207
  argv = [aplexer_path, "--json", "start", "--workspace", workspace, "--tag", tag]
186
208
  if engine:
187
209
  argv.extend(["--engine", engine])
@@ -189,6 +211,11 @@ def aplexer_start_argv(
189
211
  argv.extend(["--profile", profile])
190
212
  if memory_bytes is not None:
191
213
  argv.extend(["--memory", str(memory_bytes)])
214
+ argv = [
215
+ "systemd-run", "--user", "--scope",
216
+ "--unit", f"aplexer-launch-{uuid4().hex}",
217
+ "--collect", "--",
218
+ ] + argv
192
219
  return argv
193
220
 
194
221
 
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
  from pathlib import Path
7
+ from types import SimpleNamespace
7
8
 
8
9
  from click.testing import CliRunner
9
10
 
@@ -35,6 +36,7 @@ def test_create_uses_only_aplexer_and_emits_schema_three(monkeypatch, tmp_path:
35
36
  monkeypatch.setattr(sessions, "_resolve_aplexer", lambda: _resolution("/fake/a"))
36
37
  monkeypatch.setattr(sessions, "_aplexer_snapshot", lambda: [])
37
38
  monkeypatch.setattr(sessions._memcap, "resolve_session_mem_bytes", lambda **_: 123)
39
+ monkeypatch.setattr(sessions, "uuid4", lambda: SimpleNamespace(hex="deadbeefcafe"))
38
40
 
39
41
  def start(argv):
40
42
  calls.append(list(argv))
@@ -55,12 +57,41 @@ def test_create_uses_only_aplexer_and_emits_schema_three(monkeypatch, tmp_path:
55
57
  "created": True,
56
58
  }
57
59
  assert calls == [[
60
+ "systemd-run", "--user", "--scope",
61
+ "--unit", "aplexer-launch-deadbeefcafe", "--collect", "--",
58
62
  "/fake/a", "--json", "start", "--workspace", str(tmp_path),
59
63
  "--tag", "shell", "--engine", "codex", "--memory", "123",
60
64
  ]]
61
65
  assert "backend" not in result.output.lower()
62
66
 
63
67
 
68
+ def test_capped_start_wraps_the_worker_in_a_unique_user_scope() -> None:
69
+ first = sessions.aplexer_start_argv(
70
+ aplexer_path="/fake/a", workspace="/work/project", tag="shell",
71
+ engine=None, profile=None, memory_bytes=123,
72
+ )
73
+ second = sessions.aplexer_start_argv(
74
+ aplexer_path="/fake/a", workspace="/work/project", tag="shell",
75
+ engine=None, profile=None, memory_bytes=123,
76
+ )
77
+ for argv in (first, second):
78
+ assert argv[:7] == [
79
+ "systemd-run", "--user", "--scope", "--unit", argv[4], "--collect", "--",
80
+ ]
81
+ assert argv[4].startswith("aplexer-launch-")
82
+ assert "--memory" in argv[7:]
83
+ assert first[4] != second[4]
84
+
85
+
86
+ def test_uncapped_start_is_not_wrapped() -> None:
87
+ argv = sessions.aplexer_start_argv(
88
+ aplexer_path="/fake/a", workspace="/work/project", tag="shell",
89
+ engine=None, profile=None, memory_bytes=None,
90
+ )
91
+ assert argv == ["/fake/a", "--json", "start", "--workspace", "/work/project", "--tag", "shell"]
92
+ assert not any("systemd-run" in part for part in argv)
93
+
94
+
64
95
  def test_create_reuses_a_live_record_without_starting_again(monkeypatch, tmp_path: Path) -> None:
65
96
  starts: list[list[str]] = []
66
97
  monkeypatch.setattr(sessions, "_resolve_aplexer", lambda: _resolution("/fake/a"))
@@ -218,7 +218,7 @@ wheels = [
218
218
 
219
219
  [[package]]
220
220
  name = "pocketshell"
221
- version = "0.5.5"
221
+ version = "0.5.6"
222
222
  source = { editable = "." }
223
223
  dependencies = [
224
224
  { name = "aplexer", marker = "sys_platform == 'linux'" },
File without changes
File without changes
File without changes