agentic-runner 2.6.1__tar.gz → 2.7.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 (56) hide show
  1. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/PKG-INFO +1 -1
  2. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/__init__.py +1 -1
  3. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/activities.py +106 -3
  4. agentic_runner-2.7.0/src/agentic_runner/workers/skills.py +164 -0
  5. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/.gitignore +0 -0
  6. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/LICENSE +0 -0
  7. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/README.md +0 -0
  8. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/pyproject.toml +0 -0
  9. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/attempts.py +0 -0
  10. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/callback.py +0 -0
  11. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/child_watcher.py +0 -0
  12. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/cli.py +0 -0
  13. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/config.py +0 -0
  14. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/credentials.py +0 -0
  15. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/device_login_activities.py +0 -0
  16. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/egress.py +0 -0
  17. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/heartbeat_link.py +0 -0
  18. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/hooks.py +0 -0
  19. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/host_store.py +0 -0
  20. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/integrations/__init__.py +0 -0
  21. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/integrations/git/__init__.py +0 -0
  22. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/integrations/git/contracts.py +0 -0
  23. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/integrations/git/evidence.py +0 -0
  24. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/integrations/git/fake_workspace.py +0 -0
  25. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/integrations/git/workspace.py +0 -0
  26. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/integrations/github/__init__.py +0 -0
  27. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/integrations/github/auth.py +0 -0
  28. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/integrations/github/fake_client.py +0 -0
  29. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/integrations/github/gh_client.py +0 -0
  30. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/lifecycle.py +0 -0
  31. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/llm_proxy.py +0 -0
  32. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/mcp.py +0 -0
  33. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/message_store.py +0 -0
  34. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/py.typed +0 -0
  35. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/recipient_key_secret.py +0 -0
  36. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/registration.py +0 -0
  37. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/runtime/__init__.py +0 -0
  38. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/runtime/verifier_command.py +0 -0
  39. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/sealed_box.py +0 -0
  40. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/service.py +0 -0
  41. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/tiny_http.py +0 -0
  42. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/triage_activities.py +0 -0
  43. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/user_sources.py +0 -0
  44. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/workers/__init__.py +0 -0
  45. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/workers/_runtime_support.py +0 -0
  46. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/workers/agent_runtime.py +0 -0
  47. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/workers/claude_runtime.py +0 -0
  48. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/workers/codex_runtime.py +0 -0
  49. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/workers/command_policy.py +0 -0
  50. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/workers/contract_device_login.py +0 -0
  51. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/workers/contract_isolation.py +0 -0
  52. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/workers/fastapi_client.py +0 -0
  53. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/workers/harness_usage.py +0 -0
  54. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/workers/mcp_config.py +0 -0
  55. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/workers/settings.py +0 -0
  56. {agentic_runner-2.6.1 → agentic_runner-2.7.0}/src/agentic_runner/workstation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentic-runner
3
- Version: 2.6.1
3
+ Version: 2.7.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,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__ = "2.6.1"
10
+ __version__ = "2.7.0"
11
11
 
12
12
  __all__ = ["__version__"]
@@ -129,6 +129,14 @@ from agentic_runner.workers.fastapi_client import (
129
129
  )
130
130
  from agentic_runner.workers.harness_usage import extract_claude_result, extract_codex_turn_usage
131
131
  from agentic_runner.workers.mcp_config import McpServerEntry
132
+ from agentic_runner.workers.skills import (
133
+ SkillDelivery,
134
+ delivery_for,
135
+ first_digest_mismatch,
136
+ remove_skills,
137
+ skills_preamble,
138
+ write_skills,
139
+ )
132
140
  from agentic_runner_contracts.activity_io import (
133
141
  LEARNER_CHILDREN_BUDGET_BYTES,
134
142
  LEARNING_FAILED,
@@ -238,6 +246,7 @@ from agentic_runner_contracts.routing import (
238
246
  from agentic_runner_contracts.runtime_context import (
239
247
  EGRESS_ALLOW_LIST_KEY,
240
248
  McpServerSpec,
249
+ SkillVersionSpec,
241
250
  WorkerRuntimeContext,
242
251
  WorkerRuntimeContextResolver,
243
252
  work_branch_name,
@@ -491,6 +500,8 @@ class _RuntimeContextState:
491
500
  experience: str = ""
492
501
  experience_lesson_ids: tuple[str, ...] = ()
493
502
  experience_truncated: bool = False
503
+ # Console-v2 issue 28: the Skill versions attached to the Agent.
504
+ skills: tuple[SkillVersionSpec, ...] = ()
494
505
  workspace_path: Path | None = None
495
506
  branch_head_sha: str | None = None
496
507
  # The Autonomy Policy's tier source (PRD issue 14) -- see WorkerRuntimeContext for
@@ -533,6 +544,9 @@ class _DirectiveAttempt:
533
544
  # PRD issue 58: what config assembly wrote for the CLI, and where it may reach.
534
545
  mcp_servers: tuple[McpServerEntry, ...] | None = None
535
546
  egress_allow_list: tuple[str, ...] = ()
547
+ # Console-v2 issue 28: the attached Skills, for a runtime with no skills directory to
548
+ # write them into. Prefixed to the Directive prompt; empty when there is none.
549
+ skills_preamble: str = ""
536
550
 
537
551
  async def run_environment_hook(self) -> None:
538
552
  self.extra_env.update(await self.hooks.environment())
@@ -1325,6 +1339,7 @@ _ROUTING_EVIDENCE_SOURCE = "runner.routing"
1325
1339
  PRIOR_ATTEMPT_ALIVE_EVIDENCE_SOURCE = "runner.prior_attempt_alive"
1326
1340
  DIRECTIVE_TOKEN_EVIDENCE_SOURCE = "runner.directive_token"
1327
1341
  AGENT_RUNTIME_EVIDENCE_SOURCE = "runner.agent_runtime"
1342
+ SKILLS_EVIDENCE_SOURCE = "runner.skills"
1328
1343
 
1329
1344
 
1330
1345
  class _EvidenceWriter(Protocol):
@@ -1793,7 +1808,8 @@ class RunnerRalphActivities:
1793
1808
  extra_env=attempt.env,
1794
1809
  mcp_servers=attempt.mcp_servers,
1795
1810
  egress_allow_list=attempt.egress_allow_list,
1796
- prompt=_directive_prompt(
1811
+ prompt=attempt.skills_preamble
1812
+ + _directive_prompt(
1797
1813
  completion_criteria=runtime_state.completion_criteria,
1798
1814
  pr_body=request.pr_body,
1799
1815
  persona_preamble=_persona_prompt_preamble(
@@ -2258,6 +2274,7 @@ class RunnerRalphActivities:
2258
2274
  work_record_id, source=source, payload=dict(payload)
2259
2275
  )
2260
2276
 
2277
+ await self._refuse_skill_digest_mismatch(work_record_id, state)
2261
2278
  credentials = await self._resolve_credentials(work_record_id, state)
2262
2279
  if credentials is not None:
2263
2280
  # Names only, at the moment they resolved (22 A1's Evidence): a support read
@@ -2321,6 +2338,13 @@ class RunnerRalphActivities:
2321
2338
  credentials=credentials,
2322
2339
  env=callback_env,
2323
2340
  )
2341
+ delivered_preamble = await self._deliver_skills(
2342
+ stack,
2343
+ work_record_id=work_record_id,
2344
+ state=state,
2345
+ sandbox=sandbox,
2346
+ directive_number=directive_number,
2347
+ )
2324
2348
  session = await stack.enter_async_context(
2325
2349
  DirectiveHookSession(
2326
2350
  hooks=self._hooks,
@@ -2342,8 +2366,85 @@ class RunnerRalphActivities:
2342
2366
  credentials=credentials,
2343
2367
  mcp_servers=mcp_servers,
2344
2368
  egress_allow_list=egress_allow_list,
2369
+ skills_preamble=delivered_preamble,
2345
2370
  )
2346
2371
 
2372
+ async def _refuse_skill_digest_mismatch(
2373
+ self, work_record_id: str, state: _RuntimeContextState
2374
+ ) -> None:
2375
+ """A Skill whose body is not the version that was published fails the Directive
2376
+ non-retryably (console-v2 issue 28): a retry would fetch the same body, and
2377
+ running on it would hand the Agent prompt material no person reviewed."""
2378
+
2379
+ mismatch = first_digest_mismatch(state.skills)
2380
+ if mismatch is None:
2381
+ return
2382
+ await _raise_recorded(
2383
+ ApplicationError(
2384
+ f"Skill {mismatch.slug} version {mismatch.version} does not match its sha256",
2385
+ non_retryable=True,
2386
+ ),
2387
+ self._fastapi_client,
2388
+ work_record_id,
2389
+ source=SKILLS_EVIDENCE_SOURCE,
2390
+ actor=self._actor(),
2391
+ payload={
2392
+ "event": "skills.digest_mismatch",
2393
+ "agent_id": state.agent_id,
2394
+ "slug": mismatch.slug,
2395
+ "version": mismatch.version,
2396
+ "sha256": mismatch.sha256,
2397
+ },
2398
+ )
2399
+
2400
+ async def _deliver_skills(
2401
+ self,
2402
+ stack: contextlib.AsyncExitStack,
2403
+ *,
2404
+ work_record_id: str,
2405
+ state: _RuntimeContextState,
2406
+ sandbox: DirectiveSandbox | None,
2407
+ directive_number: int,
2408
+ ) -> str:
2409
+ """Put the attached Skills where this runtime reads them; return any preamble.
2410
+
2411
+ Written files are removed on the attempt's exit, success or failure. A Runner
2412
+ with no Contract sandbox has no harness root it owns, so it falls back to the
2413
+ preamble rather than writing into a shared ``CODEX_HOME``.
2414
+ """
2415
+
2416
+ if not state.skills:
2417
+ return ""
2418
+ delivery = delivery_for(state.cli_kind)
2419
+ if delivery is SkillDelivery.DIRECTORY and sandbox is None:
2420
+ delivery = SkillDelivery.PROMPT_PREAMBLE
2421
+ preamble = ""
2422
+ if delivery is SkillDelivery.DIRECTORY:
2423
+ assert sandbox is not None
2424
+ # Registered before the write, so a write that fails half-way is cleaned too.
2425
+ stack.push_async_callback(
2426
+ remove_skills, sandbox.harness_config_dir, state.skills, uid=sandbox.uid
2427
+ )
2428
+ await write_skills(sandbox.harness_config_dir, state.skills, uid=sandbox.uid)
2429
+ else:
2430
+ preamble = skills_preamble(state.skills)
2431
+ await self._fastapi_client.append_evidence(
2432
+ work_record_id,
2433
+ source=SKILLS_EVIDENCE_SOURCE,
2434
+ payload={
2435
+ "event": "skills.delivered",
2436
+ "directive_number": directive_number,
2437
+ "agent_id": state.agent_id,
2438
+ "cli_kind": state.cli_kind,
2439
+ "delivery": delivery.value,
2440
+ "skills": [
2441
+ {"slug": skill.slug, "version": skill.version, "sha256": skill.sha256}
2442
+ for skill in state.skills
2443
+ ],
2444
+ },
2445
+ )
2446
+ return preamble
2447
+
2347
2448
  async def _plan_mcp(
2348
2449
  self,
2349
2450
  work_record_id: str,
@@ -2995,6 +3096,7 @@ class RunnerRalphActivities:
2995
3096
  experience=runtime_context.experience,
2996
3097
  experience_lesson_ids=tuple(runtime_context.experience_lesson_ids),
2997
3098
  experience_truncated=runtime_context.experience_truncated,
3099
+ skills=tuple(runtime_context.skills),
2998
3100
  workspace_path=resolved_workspace,
2999
3101
  branch_head_sha=branch_head_sha,
3000
3102
  product_id=runtime_context.product_id,
@@ -4253,7 +4355,8 @@ class RunnerRalphActivities:
4253
4355
  extra_env=attempt.env,
4254
4356
  mcp_servers=attempt.mcp_servers,
4255
4357
  egress_allow_list=attempt.egress_allow_list,
4256
- prompt=_learning_prompt(
4358
+ prompt=attempt.skills_preamble
4359
+ + _learning_prompt(
4257
4360
  request,
4258
4361
  base_branch=state.base_branch or request.base_ref,
4259
4362
  transcript=self._learner_transcript(state, request.work_record_id),
@@ -4488,7 +4591,7 @@ class RunnerRalphActivities:
4488
4591
  extra_env=attempt.env,
4489
4592
  mcp_servers=attempt.mcp_servers,
4490
4593
  egress_allow_list=attempt.egress_allow_list,
4491
- prompt=await kind.prompt(state, prompt_notes),
4594
+ prompt=attempt.skills_preamble + await kind.prompt(state, prompt_notes),
4492
4595
  base_branch=request.base_ref,
4493
4596
  work_branch=state.work_branch,
4494
4597
  )
@@ -0,0 +1,164 @@
1
+ """The Agent's attached Skills, delivered to its Agent Runtime for one Directive.
2
+
3
+ Console-v2 issue 28, ADR-0017 §1. Where each runtime reads a Skill was measured against the
4
+ Runner's own argv, not assumed (local-agents 02 asked the same question over ACP):
5
+
6
+ - **Codex** (codex-cli 0.159.2): ``$CODEX_HOME/skills/<slug>/SKILL.md`` is advertised to the
7
+ model under ``codex exec``, with and without ``--ignore-user-config``. The Runner writes
8
+ there, in the Contract's harness root, and removes it after the Directive.
9
+ - **Claude Code** (2.1.292): the Runner runs ``claude --print --bare``, and ``--bare`` drops
10
+ the Skill tool. ``$CLAUDE_CONFIG_DIR/skills/<slug>/SKILL.md`` is then neither advertised
11
+ nor resolved by ``/<slug>``, and ``--tools …,Skill`` does not bring the tool back. Claude
12
+ Code therefore gets the bodies as a prompt preamble. Dropping ``--bare`` would also
13
+ re-enable the Workspace's hooks and ``CLAUDE.md``, which is not this module's call.
14
+
15
+ The harness root is per Contract, not per Directive, so two concurrent Directives of one
16
+ Contract share its ``skills`` directory.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import asyncio
22
+ import hashlib
23
+ import json
24
+ import sys
25
+ from collections.abc import Sequence
26
+ from enum import StrEnum
27
+ from pathlib import Path
28
+ from typing import Final
29
+
30
+ from agentic_runner_contracts.runtime_context import SkillVersionSpec
31
+
32
+ __all__ = [
33
+ "SKILLS_DIR",
34
+ "SkillDelivery",
35
+ "SkillDeliveryError",
36
+ "delivery_for",
37
+ "first_digest_mismatch",
38
+ "remove_skills",
39
+ "skills_preamble",
40
+ "write_skills",
41
+ ]
42
+
43
+ SKILLS_DIR: Final[str] = "skills"
44
+
45
+
46
+ class SkillDelivery(StrEnum):
47
+ DIRECTORY = "directory"
48
+ PROMPT_PREAMBLE = "prompt_preamble"
49
+
50
+
51
+ _DELIVERY_BY_CLI_KIND: Final[dict[str, SkillDelivery]] = {
52
+ "codex_cli": SkillDelivery.DIRECTORY,
53
+ "claude_code": SkillDelivery.PROMPT_PREAMBLE,
54
+ }
55
+
56
+
57
+ class SkillDeliveryError(RuntimeError):
58
+ """Raised when the Skills could not be written into, or removed from, the harness root."""
59
+
60
+
61
+ def delivery_for(cli_kind: str) -> SkillDelivery:
62
+ return _DELIVERY_BY_CLI_KIND.get(cli_kind, SkillDelivery.PROMPT_PREAMBLE)
63
+
64
+
65
+ def first_digest_mismatch(skills: Sequence[SkillVersionSpec]) -> SkillVersionSpec | None:
66
+ for skill in skills:
67
+ if hashlib.sha256(skill.body.encode("utf-8")).hexdigest() != skill.sha256:
68
+ return skill
69
+ return None
70
+
71
+
72
+ def skills_preamble(skills: Sequence[SkillVersionSpec]) -> str:
73
+ """The bodies, for a runtime with no skills directory. Empty Skills, empty preamble,
74
+ so a Directive without Skills keeps a byte-identical prompt."""
75
+
76
+ if not skills:
77
+ return ""
78
+ sections = "".join(
79
+ f"Skill {skill.slug} (version {skill.version}):\n{skill.body.strip()}\n\n"
80
+ for skill in skills
81
+ )
82
+ return (
83
+ "The Skills below are attached to you. Follow each one where it applies.\n\n"
84
+ f"{sections}"
85
+ "The pipeline rules take precedence over the Skills above: edit files only, do not "
86
+ "run git commit or git push, and do not open a pull request — the pipeline commits, "
87
+ "pushes, and manages the PR.\n\n"
88
+ )
89
+
90
+
91
+ # Both run as the Contract's uid, never the Runner's. The Contract owns its harness root, so
92
+ # an Agent could have left a symlink there; a write or an rmtree by the Runner -- with
93
+ # CAP_CHOWN and CAP_DAC_OVERRIDE -- would follow it out of the tree. As the Contract's uid
94
+ # it can only reach what the Contract already could.
95
+ _WRITE_SCRIPT: Final[str] = """
96
+ import json, os, sys
97
+ root, skills = json.load(sys.stdin)
98
+ os.makedirs(root, mode=0o700, exist_ok=True)
99
+ for slug, body in skills:
100
+ directory = os.path.join(root, slug)
101
+ os.makedirs(directory, mode=0o700, exist_ok=True)
102
+ path = os.path.join(directory, "SKILL.md")
103
+ flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC | os.O_NOFOLLOW
104
+ with os.fdopen(os.open(path, flags, 0o600), "w", encoding="utf-8") as handle:
105
+ handle.write(body)
106
+ """
107
+
108
+ _REMOVE_SCRIPT: Final[str] = """
109
+ import json, os, shutil, sys
110
+ root, slugs = json.load(sys.stdin)
111
+ for slug in slugs:
112
+ shutil.rmtree(os.path.join(root, slug), ignore_errors=True)
113
+ left = [slug for slug in slugs if os.path.lexists(os.path.join(root, slug))]
114
+ if left:
115
+ sys.exit("not removed: " + ", ".join(left))
116
+ """
117
+
118
+
119
+ def _skills_root(harness_config_dir: Path) -> str:
120
+ return str(harness_config_dir / SKILLS_DIR)
121
+
122
+
123
+ async def write_skills(
124
+ harness_config_dir: Path, skills: Sequence[SkillVersionSpec], *, uid: int | None
125
+ ) -> None:
126
+ """Write each Skill as ``<harness>/skills/<slug>/SKILL.md``, as the Contract's uid."""
127
+
128
+ await _run_as(
129
+ uid,
130
+ _WRITE_SCRIPT,
131
+ [_skills_root(harness_config_dir), [[skill.slug, skill.body] for skill in skills]],
132
+ )
133
+
134
+
135
+ async def remove_skills(
136
+ harness_config_dir: Path, skills: Sequence[SkillVersionSpec], *, uid: int | None
137
+ ) -> None:
138
+ """Remove what :func:`write_skills` wrote. The ``skills`` directory itself stays:
139
+ Codex keeps its own ``.system`` Skills there."""
140
+
141
+ await _run_as(
142
+ uid,
143
+ _REMOVE_SCRIPT,
144
+ [_skills_root(harness_config_dir), [skill.slug for skill in skills]],
145
+ )
146
+
147
+
148
+ async def _run_as(uid: int | None, script: str, payload: object) -> None:
149
+ identity: dict[str, object] = (
150
+ {} if uid is None else {"user": uid, "group": uid, "extra_groups": []}
151
+ )
152
+ process = await asyncio.create_subprocess_exec(
153
+ sys.executable,
154
+ "-I",
155
+ "-c",
156
+ script,
157
+ stdin=asyncio.subprocess.PIPE,
158
+ stdout=asyncio.subprocess.DEVNULL,
159
+ stderr=asyncio.subprocess.PIPE,
160
+ **identity, # type: ignore[arg-type]
161
+ )
162
+ _, stderr = await process.communicate(json.dumps(payload).encode("utf-8"))
163
+ if process.returncode != 0:
164
+ raise SkillDeliveryError(stderr.decode("utf-8", errors="replace").strip()[-500:])
File without changes
File without changes