agentic-runner 3.22.0__tar.gz → 3.23.0__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 (78) hide show
  1. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/PKG-INFO +2 -2
  2. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/pyproject.toml +1 -1
  3. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/__init__.py +1 -1
  4. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/activities.py +9 -5
  5. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/heartbeat_link.py +69 -24
  6. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/.gitignore +0 -0
  7. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/LICENSE +0 -0
  8. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/README.md +0 -0
  9. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/attempts.py +0 -0
  10. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/auth_mode.py +0 -0
  11. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/build.py +0 -0
  12. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/callback.py +0 -0
  13. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/child_watcher.py +0 -0
  14. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/cli.py +0 -0
  15. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/cli_floor.py +0 -0
  16. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/config.py +0 -0
  17. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/credentials.py +0 -0
  18. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/device_login_activities.py +0 -0
  19. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/egress.py +0 -0
  20. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/harness_self_test.py +0 -0
  21. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/hooks.py +0 -0
  22. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/host_store.py +0 -0
  23. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/integrations/__init__.py +0 -0
  24. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/integrations/git/__init__.py +0 -0
  25. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/integrations/git/contracts.py +0 -0
  26. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/integrations/git/evidence.py +0 -0
  27. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/integrations/git/fake_workspace.py +0 -0
  28. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/integrations/git/workspace.py +0 -0
  29. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/integrations/github/__init__.py +0 -0
  30. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/integrations/github/auth.py +0 -0
  31. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/integrations/github/fake_client.py +0 -0
  32. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/integrations/github/gh_client.py +0 -0
  33. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/integrations/oauth/__init__.py +0 -0
  34. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/integrations/oauth/fake_provider.py +0 -0
  35. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/launch.py +0 -0
  36. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/lifecycle.py +0 -0
  37. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/llm_proxy.py +0 -0
  38. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/mcp.py +0 -0
  39. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/message_store.py +0 -0
  40. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/oauth_connectors.py +0 -0
  41. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/private_state.py +0 -0
  42. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/py.typed +0 -0
  43. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/recipient_key_secret.py +0 -0
  44. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/registration.py +0 -0
  45. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/runner_limit.py +0 -0
  46. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/runtime/__init__.py +0 -0
  47. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/runtime/verifier_command.py +0 -0
  48. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/sealed_box.py +0 -0
  49. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/service.py +0 -0
  50. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/skill.py +0 -0
  51. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/skills/agentic-runner-setup/SKILL.md +0 -0
  52. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/testing/__init__.py +0 -0
  53. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/testing/__main__.py +0 -0
  54. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/testing/control_plane.py +0 -0
  55. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/testing/plugin.py +0 -0
  56. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/testing/test_conformance.py +0 -0
  57. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/testing/workflow.py +0 -0
  58. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/tiny_http.py +0 -0
  59. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/triage_activities.py +0 -0
  60. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/usage_windows.py +0 -0
  61. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/user_sources.py +0 -0
  62. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/workers/__init__.py +0 -0
  63. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/workers/_runtime_support.py +0 -0
  64. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/workers/acp_runtime.py +0 -0
  65. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/workers/agent_runtime.py +0 -0
  66. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/workers/claude_runtime.py +0 -0
  67. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/workers/claude_sign_in.py +0 -0
  68. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/workers/codex_runtime.py +0 -0
  69. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/workers/command_policy.py +0 -0
  70. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/workers/contract_device_login.py +0 -0
  71. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/workers/contract_isolation.py +0 -0
  72. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/workers/fastapi_client.py +0 -0
  73. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/workers/harness_outcome.py +0 -0
  74. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/workers/harness_usage.py +0 -0
  75. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/workers/mcp_config.py +0 -0
  76. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/workers/settings.py +0 -0
  77. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/workers/skills.py +0 -0
  78. {agentic_runner-3.22.0 → agentic_runner-3.23.0}/src/agentic_runner/workstation.py +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentic-runner
3
- Version: 3.22.0
3
+ Version: 3.23.0
4
4
  Summary: The Agentic OS Runner: an activity-only Temporal executor for workspaces, Agent Runtimes and verb seams
5
5
  License-Expression: AGPL-3.0-only
6
6
  License-File: LICENSE
7
7
  Requires-Python: >=3.12
8
- Requires-Dist: agentic-runner-contracts==3.22.0
8
+ Requires-Dist: agentic-runner-contracts==3.23.0
9
9
  Requires-Dist: cryptography<51,>=50
10
10
  Requires-Dist: httpx<1.0,>=0.27
11
11
  Requires-Dist: pydantic-settings<3.0,>=2.0
@@ -10,7 +10,7 @@ dependencies = [
10
10
  # Exactly this release's contracts: the two ship as one version, and an unpinned
11
11
  # requirement let `uv tool upgrade` keep an older contracts wheel the Runner cannot
12
12
  # import from (QTS-1473). `scripts/set-version.py` moves it with the version.
13
- "agentic-runner-contracts==3.22.0",
13
+ "agentic-runner-contracts==3.23.0",
14
14
  # Imported directly by `registration.py` (the Ed25519 identity that signs every
15
15
  # heartbeat); already present transitively through pyjwt[crypto], declared because
16
16
  # we import it.
@@ -7,6 +7,6 @@ stay on the platform; this package imports none of them, and a CI partition test
7
7
  (``tests/test_package_partition.py``) fails the build if that changes.
8
8
  """
9
9
 
10
- __version__ = "3.22.0"
10
+ __version__ = "3.23.0"
11
11
 
12
12
  __all__ = ["__version__"]
@@ -3661,8 +3661,9 @@ class RunnerRalphActivities:
3661
3661
 
3662
3662
  The Grant snapshot is the one thing that no longer arrives either way when this
3663
3663
  Runner has a heartbeat stream (PRD issue 44): the control plane pushes it on
3664
- change and the link holds the current one per Agent, so narrowing bites at the
3665
- next *verb* rather than the next Directive. ``grant_snapshot`` carried on the
3664
+ change and the link holds the current one per Agent and Contract, read under the
3665
+ Contract this context names for the Work Record (QTS-1706), so narrowing bites at
3666
+ the next *verb* rather than the next Directive. ``grant_snapshot`` carried on the
3666
3667
  input is the link-less host's source and is ignored wherever a link is attached.
3667
3668
  """
3668
3669
 
@@ -3677,7 +3678,7 @@ class RunnerRalphActivities:
3677
3678
  work_record_id=work_record_id,
3678
3679
  )
3679
3680
  snapshot_payload = (
3680
- self._heartbeat_link.payload_for(runtime_context.agent_id)
3681
+ self._heartbeat_link.payload_for(runtime_context.agent_id, runtime_context.contract_id)
3681
3682
  if self._heartbeat_link is not None
3682
3683
  else dict(grant_snapshot or {})
3683
3684
  )
@@ -4151,7 +4152,10 @@ class RunnerRalphActivities:
4151
4152
 
4152
4153
  if self._heartbeat_link is None:
4153
4154
  return state.grant_snapshot
4154
- return self._heartbeat_link.snapshot_for(state.agent_id) or state.grant_snapshot
4155
+ return (
4156
+ self._heartbeat_link.snapshot_for(state.agent_id, state.contract_id)
4157
+ or state.grant_snapshot
4158
+ )
4155
4159
 
4156
4160
  async def _require_live_link(
4157
4161
  self,
@@ -4175,7 +4179,7 @@ class RunnerRalphActivities:
4175
4179
  if self._heartbeat_link is None:
4176
4180
  return
4177
4181
  try:
4178
- await self._heartbeat_link.require_ready(state.agent_id, verb=verb)
4182
+ await self._heartbeat_link.require_ready(state.agent_id, state.contract_id, verb=verb)
4179
4183
  except SeamUnavailableError as error:
4180
4184
  await self._fastapi_client.append_evidence(
4181
4185
  work_record_id,
@@ -8,9 +8,11 @@ changed.
8
8
 
9
9
  Three things live together because they are one fact about "may this verb run now":
10
10
 
11
- * **The per-Agent store.** Snapshots are keyed by Agent and read only by that Agent's verb
12
- evaluations (17's structural rule): a Runner shared by many Contracts never mixes them,
13
- and a push *replaces* an Agent's snapshot rather than merging into it.
11
+ * **The per-Agent, per-Contract store.** Snapshots are keyed by Agent and the Contract
12
+ they decide for, and read only by that Agent's verb evaluations on a Work Record under
13
+ that Contract (17's structural rule, QTS-1706): a Runner shared by many Contracts never
14
+ mixes them -- not even for an Organisation Agent working under two Owners' Contracts at
15
+ once -- and a push *replaces* a snapshot rather than merging into it.
14
16
  * **The acknowledgement.** Each snapshot carries a version, and every heartbeat says which
15
17
  versions this Runner holds, which is how the control plane tells "applied" from "sent".
16
18
  * **The liveness clock.** Staleness is a property of the *link*, not a per-snapshot TTL
@@ -89,6 +91,10 @@ class HeartbeatStream(Protocol):
89
91
  async def exchange(self, applied: Sequence[AppliedSnapshot]) -> HeartbeatAck: ...
90
92
 
91
93
 
94
+ # (agent_id, contract_id); the Contract is None for a push from a control plane below 3.23.
95
+ _Key = tuple[str, str | None]
96
+
97
+
92
98
  @dataclass(frozen=True, slots=True)
93
99
  class _Held:
94
100
  version: str
@@ -120,7 +126,7 @@ class HeartbeatLink:
120
126
  # Eviction is by read rather than by push because an Agent whose Work Record
121
127
  # finished is never read again, while one under a running Directive is read at
122
128
  # every verb -- and `max_concurrent_directives` is itself bounded by this number.
123
- self._held: OrderedDict[str, _Held] = OrderedDict()
129
+ self._held: OrderedDict[_Key, _Held] = OrderedDict()
124
130
  self._tags: dict[str, str] | None = None
125
131
  self._last_ack: float | None = None
126
132
  self._seen_monotonic, self._seen_wall = self._clock()
@@ -128,26 +134,44 @@ class HeartbeatLink:
128
134
 
129
135
  # ------------------------------------------------------------------ the store
130
136
 
131
- def snapshot_for(self, agent_id: str | None) -> GrantSnapshot | None:
132
- """This Agent's pushed snapshot, or ``None`` if none was ever pushed."""
137
+ def snapshot_for(self, agent_id: str | None, contract_id: str | None) -> GrantSnapshot | None:
138
+ """This Agent's pushed snapshot for this Contract, or ``None`` if none was pushed."""
133
139
 
134
- held = self._touch(agent_id)
140
+ held = self._touch(agent_id, contract_id)
135
141
  return held.snapshot if held else None
136
142
 
137
- def payload_for(self, agent_id: str | None) -> dict[str, Any]:
143
+ def payload_for(self, agent_id: str | None, contract_id: str | None) -> dict[str, Any]:
138
144
  """The snapshot exactly as the control plane wrote it (ADR-0013 §3)."""
139
145
 
140
- held = self._touch(agent_id)
146
+ held = self._touch(agent_id, contract_id)
141
147
  return dict(held.payload) if held else {}
142
148
 
143
- def _touch(self, agent_id: str | None) -> _Held | None:
144
- if agent_id is None:
149
+ def _touch(self, agent_id: str | None, contract_id: str | None) -> _Held | None:
150
+ found = self._find(agent_id, contract_id)
151
+ if found is None:
145
152
  return None
146
- held = self._held.get(agent_id)
147
- if held is not None:
148
- self._held.move_to_end(agent_id)
153
+ key, held = found
154
+ self._held.move_to_end(key)
149
155
  return held
150
156
 
157
+ def _find(self, agent_id: str | None, contract_id: str | None) -> tuple[_Key, _Held] | None:
158
+ """The snapshot deciding this Agent's verbs on a Work Record under this Contract.
159
+
160
+ Never another Contract's: a snapshot an older control plane pushed without a
161
+ Contract answers only for the Contract it names itself, or for none when it is the
162
+ held snapshot that refuses every verb.
163
+ """
164
+
165
+ if agent_id is None:
166
+ return None
167
+ if contract_id is not None and (agent_id, contract_id) in self._held:
168
+ return (agent_id, contract_id), self._held[(agent_id, contract_id)]
169
+ unkeyed = self._held.get((agent_id, None))
170
+ # The payload, not the parsed snapshot: `from_payload` reads a null as "None".
171
+ if unkeyed is None or unkeyed.payload.get("contract_id") not in (None, contract_id):
172
+ return None
173
+ return (agent_id, None), unkeyed
174
+
151
175
  @property
152
176
  def tags(self) -> dict[str, str] | None:
153
177
  """This Runner's Tags as the last ack carried them (ADR-0022 §13).
@@ -160,28 +184,45 @@ class HeartbeatLink:
160
184
 
161
185
  def applied(self) -> list[AppliedSnapshot]:
162
186
  return [
163
- AppliedSnapshot(agent_id=UUID(agent_id), version=held.version)
164
- for agent_id, held in sorted(self._held.items())
187
+ AppliedSnapshot(
188
+ agent_id=UUID(agent_id),
189
+ contract_id=None if contract_id is None else UUID(contract_id),
190
+ version=held.version,
191
+ )
192
+ for (agent_id, contract_id), held in sorted(
193
+ self._held.items(), key=lambda item: (item[0][0], item[0][1] or "")
194
+ )
165
195
  ]
166
196
 
167
197
  def apply(self, ack: HeartbeatAck) -> None:
168
- """Apply one ack's pushes: snapshots per Agent, ceilings per Contract, the Tags.
198
+ """Apply one ack's pushes: snapshots per Agent and Contract, ceilings per Contract,
199
+ the Tags.
169
200
 
170
201
  Replacement, never a merge. A narrowed Grant that merged into what it narrowed
171
202
  would leave the wider entry standing, which is the one mistake this whole layer
172
- exists to prevent.
203
+ exists to prevent. For the same reason a push without a Contract, from a control
204
+ plane below 3.23, replaces every snapshot of its Agent, and a push with one
205
+ replaces the Agent's snapshot that had none.
173
206
  """
174
207
 
175
208
  if ack.tags is not None:
176
209
  self._tags = dict(ack.tags)
177
210
  for push in ack.grant_pushes:
178
211
  agent_id = str(push.agent_id)
179
- self._held[agent_id] = _Held(
212
+ key: _Key = (agent_id, None if push.contract_id is None else str(push.contract_id))
213
+ replaced = (
214
+ [entry for entry in self._held if entry[0] == agent_id]
215
+ if push.contract_id is None
216
+ else [(agent_id, None)]
217
+ )
218
+ for stale in replaced:
219
+ self._held.pop(stale, None)
220
+ self._held[key] = _Held(
180
221
  version=push.version,
181
222
  payload=dict(push.snapshot),
182
223
  snapshot=GrantSnapshot.from_payload(push.snapshot),
183
224
  )
184
- self._held.move_to_end(agent_id)
225
+ self._held.move_to_end(key)
185
226
  while len(self._held) > SNAPSHOT_PUSH_MAX:
186
227
  self._held.popitem(last=False)
187
228
  if self._ceilings is None:
@@ -210,13 +251,16 @@ class HeartbeatLink:
210
251
  self._woke = False
211
252
  return ack
212
253
 
213
- async def require_ready(self, agent_id: str | None, *, verb: str) -> None:
254
+ async def require_ready(
255
+ self, agent_id: str | None, contract_id: str | None, *, verb: str
256
+ ) -> None:
214
257
  """Refuse a privileged verb this Runner is in no position to decide.
215
258
 
216
259
  Order matters: a wake is answered with a heartbeat *before* staleness is read, so
217
260
  a laptop that slept re-hears the control plane instead of acting on a snapshot
218
261
  from before the sleep (23 item 4); and a Runner holding no snapshot for an Agent
219
- refuses rather than running it unattenuated -- absence is never permission.
262
+ under this Work Record's Contract refuses rather than running it unattenuated, or
263
+ on another Contract's chain -- absence is never permission.
220
264
  """
221
265
 
222
266
  if self._observe_wake():
@@ -236,10 +280,11 @@ class HeartbeatLink:
236
280
  f"{'none' if age is None else f'{age:.0f}s'} old, past the "
237
281
  f"{self._stale_after:.0f}s liveness bound",
238
282
  )
239
- if agent_id is not None and agent_id not in self._held:
283
+ if agent_id is not None and self._find(agent_id, contract_id) is None:
240
284
  raise SeamUnavailableError(
241
285
  SNAPSHOT_MISSING_REASON,
242
- f"{verb!r} is refused: no Grant snapshot has been pushed for Agent {agent_id}",
286
+ f"{verb!r} is refused: no Grant snapshot has been pushed for Agent {agent_id} "
287
+ f"under Contract {contract_id}",
243
288
  )
244
289
 
245
290
  def _age(self) -> float | None:
File without changes