amkernel 0.1.0__py3-none-any.whl

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.
amkernel/workspace.py ADDED
@@ -0,0 +1,305 @@
1
+ """Descriptor-confined workspace and environment prelude helpers.
2
+
3
+ These helpers are a safe default for generated code, not a sandbox. User Python
4
+ retains unrestricted access to ordinary same-user filesystem and process APIs.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import re
11
+ import secrets
12
+ import stat
13
+ from dataclasses import dataclass
14
+ from typing import Final, cast
15
+
16
+ MAX_READ_BYTES: Final = 67_108_864
17
+ _ENV_KEY = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
18
+
19
+
20
+ class WorkspaceError(ValueError):
21
+ """A confined workspace operation is invalid or unsafe."""
22
+
23
+
24
+ class ReadError(WorkspaceError):
25
+ """A confined read could not safely return a regular file."""
26
+
27
+
28
+ class ReadLimitError(ReadError):
29
+ """A regular file exceeds the requested read limit."""
30
+
31
+
32
+ class WriteError(WorkspaceError):
33
+ """A confined atomic write could not safely replace a regular file."""
34
+
35
+
36
+ class EnvError(ValueError):
37
+ """An environment helper argument is invalid."""
38
+
39
+
40
+ class _Missing:
41
+ def __repr__(self) -> str:
42
+ return "<missing>"
43
+
44
+
45
+ _MISSING = _Missing()
46
+
47
+
48
+ @dataclass(frozen=True, slots=True)
49
+ class _RootIdentity:
50
+ device: int
51
+ inode: int
52
+
53
+
54
+ class Workspace:
55
+ """Pin one launch directory and create fresh callable prelude helpers."""
56
+
57
+ def __init__(self, launch_directory: str) -> None:
58
+ self._require_platform_support()
59
+ canonical = launch_directory
60
+ flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW
61
+ try:
62
+ descriptor = os.open(".", flags)
63
+ metadata = os.fstat(descriptor)
64
+ except OSError as error:
65
+ raise WorkspaceError("workspace initialization failed") from error
66
+ if not stat.S_ISDIR(metadata.st_mode):
67
+ os.close(descriptor)
68
+ raise WorkspaceError("workspace root is not a regular directory")
69
+ self._canonical = canonical
70
+ self._descriptor = descriptor
71
+ self._identity = _RootIdentity(metadata.st_dev, metadata.st_ino)
72
+
73
+ def close(self) -> None:
74
+ descriptor = self._descriptor
75
+ if descriptor < 0:
76
+ return
77
+ self._descriptor = -1
78
+ os.close(descriptor)
79
+
80
+ def helpers(self) -> tuple[ReadHelper, WriteHelper, EnvHelper]:
81
+ return ReadHelper(self), WriteHelper(self), EnvHelper()
82
+
83
+ def read(self, path: object, *, binary: bool, limit: int) -> str | bytes:
84
+ if type(binary) is not bool:
85
+ raise ReadError("binary must be a boolean")
86
+ if type(limit) is not int or not 0 <= limit <= MAX_READ_BYTES:
87
+ raise ReadError("limit must be an integer from 0 through 67108864")
88
+ parent, name = self._open_parent(path, ReadError)
89
+ descriptor = -1
90
+ try:
91
+ descriptor = os.open(
92
+ name,
93
+ os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK,
94
+ dir_fd=parent,
95
+ )
96
+ metadata = os.fstat(descriptor)
97
+ if not stat.S_ISREG(metadata.st_mode):
98
+ raise ReadError("read target is not a regular file")
99
+ remaining = limit + 1
100
+ data = bytearray()
101
+ while remaining:
102
+ chunk = os.read(descriptor, min(65_536, remaining))
103
+ if not chunk:
104
+ break
105
+ data.extend(chunk)
106
+ remaining -= len(chunk)
107
+ if len(data) > limit:
108
+ raise ReadLimitError("read target exceeds limit")
109
+ raw = bytes(data)
110
+ if binary:
111
+ return raw
112
+ try:
113
+ return raw.decode("utf-8", errors="strict")
114
+ except UnicodeDecodeError as error:
115
+ raise ReadError("read target is not valid UTF-8") from error
116
+ except ReadError:
117
+ raise
118
+ except OSError as error:
119
+ raise ReadError("workspace read failed") from error
120
+ finally:
121
+ if descriptor >= 0:
122
+ os.close(descriptor)
123
+ os.close(parent)
124
+
125
+ def write(self, path: object, content: object) -> int:
126
+ data = self._write_bytes(content)
127
+ parent, name = self._open_parent(path, WriteError)
128
+ temporary = f".amkernel-write-{secrets.token_hex(16)}"
129
+ descriptor = -1
130
+ temporary_created = False
131
+ try:
132
+ before = self._destination_identity(parent, name)
133
+ descriptor = os.open(
134
+ temporary,
135
+ os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
136
+ 0o600,
137
+ dir_fd=parent,
138
+ )
139
+ temporary_created = True
140
+ view = memoryview(data)
141
+ while view:
142
+ written = os.write(descriptor, view)
143
+ if written <= 0:
144
+ raise WriteError("workspace write made no progress")
145
+ view = view[written:]
146
+ os.fchmod(descriptor, 0o600)
147
+ os.close(descriptor)
148
+ descriptor = -1
149
+ if self._destination_identity(parent, name) != before:
150
+ raise WriteError("write destination changed during replacement")
151
+ os.rename(temporary, name, src_dir_fd=parent, dst_dir_fd=parent)
152
+ temporary = ""
153
+ final = os.stat(name, dir_fd=parent, follow_symlinks=False)
154
+ if not stat.S_ISREG(final.st_mode):
155
+ raise WriteError("write destination is not a regular file")
156
+ return len(data)
157
+ except WriteError:
158
+ raise
159
+ except OSError as error:
160
+ raise WriteError("workspace write failed") from error
161
+ finally:
162
+ if descriptor >= 0:
163
+ os.close(descriptor)
164
+ if temporary_created and temporary:
165
+ try:
166
+ os.unlink(temporary, dir_fd=parent)
167
+ except FileNotFoundError:
168
+ pass
169
+ except OSError:
170
+ pass
171
+ os.close(parent)
172
+
173
+ def _open_parent(self, path: object, error_type: type[WorkspaceError]) -> tuple[int, str]:
174
+ components = self._components(path, error_type)
175
+ current = os.dup(self._descriptor)
176
+ try:
177
+ for component in components[:-1]:
178
+ following = os.open(
179
+ component,
180
+ os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
181
+ dir_fd=current,
182
+ )
183
+ metadata = os.fstat(following)
184
+ if not stat.S_ISDIR(metadata.st_mode):
185
+ os.close(following)
186
+ raise error_type("workspace parent is not a regular directory")
187
+ os.close(current)
188
+ current = following
189
+ return current, components[-1]
190
+ except error_type:
191
+ os.close(current)
192
+ raise
193
+ except OSError as error:
194
+ os.close(current)
195
+ raise error_type("workspace path resolution failed") from error
196
+
197
+ def _components(self, path: object, error_type: type[WorkspaceError]) -> tuple[str, ...]:
198
+ try:
199
+ raw = os.fspath(cast(str | os.PathLike[str], path))
200
+ except TypeError as error:
201
+ raise error_type("path must be a string or path-like value") from error
202
+ if not isinstance(raw, str) or not raw or "\x00" in raw:
203
+ raise error_type("path must be a nonempty NUL-free text path")
204
+ if os.path.isabs(raw):
205
+ try:
206
+ if os.path.commonpath((self._canonical, raw)) != self._canonical:
207
+ raise error_type("absolute path is outside the launch workspace")
208
+ except ValueError as error:
209
+ raise error_type("absolute path is outside the launch workspace") from error
210
+ relative = raw[len(self._canonical) :]
211
+ if relative.startswith(os.sep):
212
+ relative = relative[1:]
213
+ else:
214
+ relative = raw
215
+ components = tuple(relative.split(os.sep))
216
+ if not components or any(part in {"", ".", ".."} for part in components):
217
+ raise error_type("path contains an empty or traversal component")
218
+ return components
219
+
220
+ @staticmethod
221
+ def _write_bytes(content: object) -> bytes:
222
+ if isinstance(content, str):
223
+ return content.encode("utf-8", errors="strict")
224
+ if isinstance(content, bytes):
225
+ return content
226
+ raise WriteError("content must be text or bytes")
227
+
228
+ @staticmethod
229
+ def _destination_identity(parent: int, name: str) -> tuple[int, int] | None:
230
+ try:
231
+ metadata = os.stat(name, dir_fd=parent, follow_symlinks=False)
232
+ except FileNotFoundError:
233
+ return None
234
+ if not stat.S_ISREG(metadata.st_mode):
235
+ raise WriteError("write destination is not a regular file")
236
+ return metadata.st_dev, metadata.st_ino
237
+
238
+ @staticmethod
239
+ def _require_platform_support() -> None:
240
+ required = (os.open, os.stat, os.unlink)
241
+ if not hasattr(os, "O_DIRECTORY") or not hasattr(os, "O_NOFOLLOW"):
242
+ raise WorkspaceError("descriptor-relative no-follow workspace helpers are unavailable")
243
+ if any(function not in os.supports_dir_fd for function in required):
244
+ raise WorkspaceError("descriptor-relative no-follow workspace helpers are unavailable")
245
+ if os.replace not in os.supports_dir_fd and os.rename not in os.supports_dir_fd:
246
+ raise WorkspaceError("descriptor-relative atomic replacement is unavailable")
247
+
248
+
249
+ class ReadHelper:
250
+ """Descriptor-confined convenience read; unrestricted Python is not sandboxed."""
251
+
252
+ def __init__(self, workspace: Workspace) -> None:
253
+ self._workspace = workspace
254
+
255
+ def __call__(
256
+ self, path: object, *, binary: bool = False, limit: int = 1_048_576
257
+ ) -> str | bytes:
258
+ return self._workspace.read(path, binary=binary, limit=limit)
259
+
260
+ def __repr__(self) -> str:
261
+ return "<AMKernel helper read>"
262
+
263
+
264
+ class WriteHelper:
265
+ """Descriptor-confined atomic write; unrestricted Python is not sandboxed."""
266
+
267
+ def __init__(self, workspace: Workspace) -> None:
268
+ self._workspace = workspace
269
+
270
+ def __call__(self, path: object, content: object) -> int:
271
+ return self._workspace.write(path, content)
272
+
273
+ def __repr__(self) -> str:
274
+ return "<AMKernel helper write>"
275
+
276
+
277
+ class EnvHelper:
278
+ """Convenient process environment access; this is not a credential boundary."""
279
+
280
+ def __call__(self, key: object = _MISSING, value: object = _MISSING) -> object:
281
+ if key is _MISSING:
282
+ if value is not _MISSING:
283
+ raise EnvError("value requires a key")
284
+ return dict(os.environ)
285
+ if value is _MISSING:
286
+ name = self._key(key)
287
+ return os.environ.get(name)
288
+ name = self._key(key)
289
+ previous = os.environ.get(name)
290
+ if value is None:
291
+ os.environ.pop(name, None)
292
+ return previous
293
+ if not isinstance(value, str) or "\x00" in value:
294
+ raise EnvError("environment value must be NUL-free text or None")
295
+ os.environ[name] = value
296
+ return previous
297
+
298
+ def __repr__(self) -> str:
299
+ return "<AMKernel helper env>"
300
+
301
+ @staticmethod
302
+ def _key(key: object) -> str:
303
+ if not isinstance(key, str) or _ENV_KEY.fullmatch(key) is None:
304
+ raise EnvError("environment key must be a portable nonempty name")
305
+ return key
@@ -0,0 +1,105 @@
1
+ Metadata-Version: 2.3
2
+ Name: amkernel
3
+ Version: 0.1.0
4
+ Summary: Persistent Python execution for agentic automation.
5
+ Classifier: Programming Language :: Python :: 3 :: Only
6
+ Classifier: Programming Language :: Python :: 3.11
7
+ Classifier: Programming Language :: Python :: 3.12
8
+ Classifier: Programming Language :: Python :: 3.13
9
+ Requires-Dist: mcp>=2.3,<3
10
+ Requires-Dist: pillow>=12,<13
11
+ Requires-Dist: pydantic>=2.14,<3
12
+ Requires-Dist: tomlkit>=0.13,<1
13
+ Requires-Python: >=3.11, <3.14
14
+ Description-Content-Type: text/markdown
15
+
16
+ # AMKernel
17
+
18
+ **AMKernel — Automation MCP Kernel**
19
+
20
+ > Persistent Python execution for agentic automation.
21
+ >
22
+ > 面向 Agent 自动化的持久 Python Execution MCP。
23
+
24
+ AMKernel is a persistent Python execution MCP for agentic automation. Mobile automation through Appium is its first experimental vertical.
25
+
26
+ AMKernel 是一个面向 Agent 自动化的持久 Python Execution MCP。基于 Appium 的移动自动化是它的第一个实验方向。
27
+
28
+ Agents use ordinary Python automation libraries while retaining live objects, imports, and state across tool calls.
29
+
30
+ ## Why the name
31
+
32
+ - **Automation** — Appium, Playwright, Windows, macOS, and other automation environments. / Appium、Playwright、Windows、macOS 等自动化场景。
33
+ - **MCP** — the standard interface that attaches AMKernel to different Coding Agents. / 挂载到不同 Coding Agent 的标准接口。
34
+ - **Kernel** — a persistent Python namespace with object reuse, incremental execution, and interrupt recovery. / 持久 Python namespace、对象复用、增量执行和中断恢复。
35
+
36
+ ```text
37
+ Coding Agent
38
+ → MCP
39
+ → persistent Python kernel
40
+ → automation library
41
+ → external system
42
+ ```
43
+
44
+ The first experimental vertical is mobile automation through Appium:
45
+
46
+ ```text
47
+ Coding Agent
48
+ → AMKernel
49
+ → Appium Python Client
50
+ → Appium Server
51
+ → XCUITest / UiAutomator2
52
+ → mobile device
53
+ ```
54
+
55
+ Future experiments may apply the same kernel to Playwright, Selenium, pywinauto, PyAutoGUI, macOS Accessibility, and other Python automation ecosystems. Those platforms do not belong in the core until experiments establish shared requirements.
56
+
57
+ ## Project principles
58
+
59
+ - Python execution is the primary Agent interface.
60
+ - State persists across MCP calls.
61
+ - The Agent uses official automation libraries directly.
62
+ - The kernel reports truthful execution and survival state.
63
+ - Text, structured values, images, and large artifacts are first-class outputs.
64
+ - Platform knowledge starts in project skills and experiments, not core abstractions.
65
+ - AMKernel is local stdio code execution first, not a remote multi-tenant sandbox.
66
+ - The core grows from measured experimental needs, not hypothetical platform unification.
67
+
68
+ ## Install for a project
69
+
70
+ AMKernel uses one user-level `uv`-managed Runtime and project-scoped Agent activation. From the project directory, run:
71
+
72
+ ```bash
73
+ uvx --from amkernel amkernel install
74
+ ```
75
+
76
+ The installer explains the changes, installs `.agents/skills/amkernel`, writes the detected OMP, Pi, or Codex project MCP entry, and asks you to restart the Coding Agent. It does not modify the project's `.venv`, install Appium Server or platform drivers, start a device, or create a session.
77
+
78
+ Use `amkernel status`, `amkernel doctor`, `amkernel update`, and `amkernel uninstall` for the managed lifecycle. `amkernel uninstall --global` is explicit because it affects every activated project.
79
+
80
+ ## Current phase
81
+
82
+ Stage 2 agent-quality output is implemented through the packaged stdio server and the single `python_exec` tool. One persistent Runtime now provides bounded live progress, recoverable stream artifacts, strict JSON and raster displays, explicit artifacts, requested-package diagnostics, descriptor-rooted workspace helpers, hard output-limit intervention, and truthful lifecycle/cleanup results.
83
+
84
+ Artifact resource URIs are ephemeral. Read and verify every manifest part before another large-output call or server shutdown. AMKernel detects Store tampering when a resource is read, but unrestricted same-user Python can access host-visible files; AMKernel is not a sandbox.
85
+
86
+ ### Supported environments
87
+
88
+ - CPython 3.11, 3.12, and 3.13.
89
+ - Current GitHub-hosted Ubuntu and macOS environments, where the native lifecycle and installed-wheel official-client matrices are required release evidence.
90
+
91
+ CPython 3.14, Windows, and other operating systems remain unsupported. The support claim is intentionally narrower than generic Linux or macOS package classifiers.
92
+
93
+ Canonical documents:
94
+
95
+ - [`docs/architecture.md`](docs/architecture.md) — generic AMKernel core architecture.
96
+ - [`docs/development.md`](docs/development.md) — agent roles, change ownership, development workflow, and quality gates.
97
+ - [`docs/stage-1-implementation-plan.md`](docs/stage-1-implementation-plan.md) — accepted Stage 1 contracts, dependency order, ownership, behavior scenarios, and risk traceability.
98
+ - [`docs/stage-2-implementation-plan.md`](docs/stage-2-implementation-plan.md) — accepted Stage 2 output, artifact, lifecycle, evidence, and support contracts.
99
+ - [`docs/project-management.md`](docs/project-management.md) — GitHub-backed workflow, Kanban semantics, task contracts, ADR, and risk governance.
100
+ - [GitHub Project](https://github.com/orgs/FSQ-lab/projects/2) — canonical milestone, status, stage, type, priority, and queue.
101
+ - [GitHub Issues](https://github.com/FSQ-lab/amkernel/issues) — canonical work-item, dependency, blocker, risk, and verification records.
102
+ - [`docs/decisions/`](docs/decisions/) — accepted architecture and implementation decisions.
103
+ - [`docs/experiments/mobile-appium.md`](docs/experiments/mobile-appium.md) — first Appium/mobile experiment and success criteria.
104
+
105
+ Implementation follows the interfaces, invariants, and delivery order accepted in these documents.
@@ -0,0 +1,23 @@
1
+ amkernel/__init__.py,sha256=NZ2VfLSZOXUHDLzkcMCAEN0-O83vjS0WY5hyZRb0Q6g,126
2
+ amkernel/__main__.py,sha256=kIn6W2lkzwpctvu7ty50ImtePhM1nIj6SLUnN4gvPEc,96
3
+ amkernel/artifact_store.py,sha256=6auaHmX03UKpJvPVhEEESlejOdqhr-bynlFMc3aU38w,57315
4
+ amkernel/cli.py,sha256=SMxSCGeMJcpQ3E4_-i-auy2cMs8Ff6bOJHKx_OB_Y-s,7064
5
+ amkernel/distribution.py,sha256=W4YdHOk11CWugahHeGE4JK23SRITQ6vo2xqrT9XBqzg,23049
6
+ amkernel/output.py,sha256=Rv3H-U9IhXMTjlxG0XPILx9P5DJIXJgwO1pBK49Fovw,16370
7
+ amkernel/protocol.py,sha256=prvE52snNbVpxeyLHXJExc6au0SkRnlGC0Xtv5Plsp8,54431
8
+ amkernel/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ amkernel/runner.py,sha256=FyLRe_Ki9-8u49W0P79tM3ZClRyZq1kc-IubREwPsRg,42279
10
+ amkernel/runtime.py,sha256=_W2lH0ha9SK7p142CiUlyjfT04uqr8ZyGAEgQfTwqus,22227
11
+ amkernel/server.py,sha256=55MNmvsq1G15MssaD6ogF-hxGPAhNsS6FVZrRL7Popk,26755
12
+ amkernel/skill/SKILL.md,sha256=GB-PcyXqXHXSgcAdhYb51BvPSBGLFbXGjOG9TsGWM7Y,10838
13
+ amkernel/skill/references/amkernel-appium.md,sha256=s2yd34H-DcwimEk2CMVuLQIn_-xluSar4BhrBXLlVYw,2462
14
+ amkernel/skill/references/official-skills.json,sha256=W7T8_uhDKyC3KnelqklLNAxAJwjRD859LLQU5mwcMrI,22440
15
+ amkernel/skill/references/official-sources.json,sha256=tbbHj-duz3FQD87FZ8HhoB5gwsa24CTQYyhmA_qQNIU,834
16
+ amkernel/skill/scripts/verify_official_skills.py,sha256=BJ2-nPUgWNVKMEgkJJSwfLFsHMbmAB0OtciNgfG0RuU,9889
17
+ amkernel/structured_output.py,sha256=cjY4AXWhxiRWHrTIbyw_s0OS6HKe_rMDjQjHSFGwoAk,11937
18
+ amkernel/supervisor.py,sha256=PyevwQsgV5AhqKUTWGRnAOPNxpTjx762EtpD7Bf5BN8,54853
19
+ amkernel/workspace.py,sha256=HFMh-FQ-Q6EP38YeeGaYLS9D5WiavKL5NvYDmq8WQP0,11344
20
+ amkernel-0.1.0.dist-info/WHEEL,sha256=4OL6Foqnnp3xRY5wMkjgc25_i5YJC6dKsC6LPcjqEoU,80
21
+ amkernel-0.1.0.dist-info/entry_points.txt,sha256=-kjjp2LXqOgnIRsKrR3WQwd_usST1EGfGx_xAcB30c4,48
22
+ amkernel-0.1.0.dist-info/METADATA,sha256=NY7Qqx25pK5SwCvLz2CNpGiIoOmTkObzWmQ_ksxNGtM,5802
23
+ amkernel-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.5
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ amkernel = amkernel.cli:main
3
+