berthos-sdk 0.2.1__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.
@@ -0,0 +1,121 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship.
44
+
45
+ "Contribution" shall mean any work of authorship, including
46
+ the original version of the Work and any modifications or additions
47
+ to that Work or Derivative Works thereof, that is intentionally
48
+ submitted to Licensor for inclusion in the Work by the copyright owner
49
+ or by an individual or Legal Entity authorized to submit on behalf of
50
+ the copyright owner.
51
+
52
+ "Contributor" shall mean Licensor and any individual or Legal Entity
53
+ on behalf of whom a Contribution has been received by Licensor and
54
+ subsequently incorporated within the Work.
55
+
56
+ 2. Grant of Copyright License. Subject to the terms and conditions of
57
+ this License, each Contributor hereby grants to You a perpetual,
58
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
59
+ copyright license to reproduce, prepare Derivative Works of,
60
+ publicly display, publicly perform, sublicense, and distribute the
61
+ Work and such Derivative Works in Source or Object form.
62
+
63
+ 3. Grant of Patent License. Subject to the terms and conditions of
64
+ this License, each Contributor hereby grants to You a perpetual,
65
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
66
+ (except as stated in this section) patent license to make, have made,
67
+ use, offer to sell, sell, import, and otherwise transfer the Work.
68
+
69
+ 4. Redistribution. You may reproduce and distribute copies of the
70
+ Work or Derivative Works thereof in any medium, with or without
71
+ modifications, and in Source or Object form, provided that You
72
+ meet the following conditions: (a) You must give any other
73
+ recipients of the Work or Derivative Works a copy of this License;
74
+ (b) You must cause any modified files to carry prominent notices
75
+ stating that You changed the files; (c) You must retain, in the
76
+ Source form of any Derivative Works that You distribute, all
77
+ copyright, patent, trademark, and attribution notices from the
78
+ Source form of the Work; (d) If the Work includes a "NOTICE" text
79
+ file, any Derivative Works You distribute must include a readable
80
+ copy of the attribution notices contained within such NOTICE file.
81
+
82
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
83
+ any Contribution intentionally submitted for inclusion in the Work
84
+ by You to the Licensor shall be under the terms and conditions of
85
+ this License, without any additional terms or conditions.
86
+
87
+ 6. Trademarks. This License does not grant permission to use the trade
88
+ names, trademarks, service marks, or product names of the Licensor.
89
+
90
+ 7. Disclaimer of Warranty. Unless required by applicable law or
91
+ agreed to in writing, Licensor provides the Work on an "AS IS" BASIS,
92
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
93
+ implied.
94
+
95
+ 8. Limitation of Liability. In no event and under no legal theory,
96
+ whether in tort, negligence, contract, or otherwise, shall any
97
+ Contributor be liable to You for damages, including any direct,
98
+ indirect, special, incidental, or consequential damages arising
99
+ as a result of this License or out of the use or inability to use
100
+ the Work.
101
+
102
+ 9. Accepting Warranty or Additional Liability. While redistributing
103
+ the Work or Derivative Works thereof, You may choose to offer,
104
+ and charge a fee for, acceptance of support, warranty, indemnity,
105
+ or other liability obligations consistent with this License.
106
+
107
+ END OF TERMS AND CONDITIONS
108
+
109
+ Copyright 2026 Berth Contributors
110
+
111
+ Licensed under the Apache License, Version 2.0 (the "License");
112
+ you may not use this file except in compliance with the License.
113
+ You may obtain a copy of the License at
114
+
115
+ http://www.apache.org/licenses/LICENSE-2.0
116
+
117
+ Unless required by applicable law or agreed to in writing, software
118
+ distributed under the License is distributed on an "AS IS" BASIS,
119
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
120
+ See the License for the specific language governing permissions and
121
+ limitations under the License.
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: berthos-sdk
3
+ Version: 0.2.1
4
+ Summary: Python resident-app SDK for Berth — reuses the same wire protocols as @berthos/sdk (RPC newline-JSON framing, the manifest shape), with idiomatic Python runtime glue rather than a port of the TypeScript code.
5
+ License-Expression: Apache-2.0
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: pyyaml>=6.0
10
+ Requires-Dist: pydantic>=2.0
11
+ Requires-Dist: protobuf>=6.30
12
+ Dynamic: license-file
13
+
14
+ # berthos-sdk
15
+
16
+ Build a Berth resident app in Python. It uses the same `berth.yml` manifest, RPC protocol and context bus as the TypeScript SDK, `@berthos/sdk`.
17
+
18
+ [Berth](https://github.com/Ash20pk/BerthOS) runs an AI agent's tools in a sandbox. Each tool declares what it may touch in a `berth.yml` manifest, and the Linux kernel enforces it.
19
+
20
+ ```sh
21
+ pip install berthos-sdk
22
+ ```
23
+
24
+ Needs Python 3.11+. The import name is `berth_sdk`.
25
+
26
+ ## Usage
27
+
28
+ ```python
29
+ from berth_sdk import define_app
30
+ from pydantic import BaseModel
31
+
32
+
33
+ class GreetInput(BaseModel):
34
+ name: str
35
+
36
+
37
+ class GreetOutput(BaseModel):
38
+ message: str
39
+
40
+
41
+ def setup(app):
42
+ app.export(
43
+ "greet",
44
+ lambda inp: GreetOutput(message=f"hello {inp.name}"),
45
+ input_model=GreetInput,
46
+ output_model=GreetOutput,
47
+ )
48
+
49
+
50
+ app = define_app(setup)
51
+ ```
52
+
53
+ Declare `greet` under `exports` in `berth.yml` too. Without `input_model`, a handler receives the raw input dict. Run the app with `berth dev` from [`@berthos/cli`](https://www.npmjs.com/package/@berthos/cli); the [`hello-world-py`](https://github.com/Ash20pk/BerthOS/tree/main/apps/hello-world-py) app is a complete example.
54
+
55
+ ## Docs
56
+
57
+ [Python SDK reference](https://github.com/Ash20pk/BerthOS/blob/main/docs/sdk-python-reference.md) · [Python and the context bus](https://github.com/Ash20pk/BerthOS/blob/main/docs/sdk-python-context-bus-reference.md) · [Repo](https://github.com/Ash20pk/BerthOS)
@@ -0,0 +1,44 @@
1
+ # berthos-sdk
2
+
3
+ Build a Berth resident app in Python. It uses the same `berth.yml` manifest, RPC protocol and context bus as the TypeScript SDK, `@berthos/sdk`.
4
+
5
+ [Berth](https://github.com/Ash20pk/BerthOS) runs an AI agent's tools in a sandbox. Each tool declares what it may touch in a `berth.yml` manifest, and the Linux kernel enforces it.
6
+
7
+ ```sh
8
+ pip install berthos-sdk
9
+ ```
10
+
11
+ Needs Python 3.11+. The import name is `berth_sdk`.
12
+
13
+ ## Usage
14
+
15
+ ```python
16
+ from berth_sdk import define_app
17
+ from pydantic import BaseModel
18
+
19
+
20
+ class GreetInput(BaseModel):
21
+ name: str
22
+
23
+
24
+ class GreetOutput(BaseModel):
25
+ message: str
26
+
27
+
28
+ def setup(app):
29
+ app.export(
30
+ "greet",
31
+ lambda inp: GreetOutput(message=f"hello {inp.name}"),
32
+ input_model=GreetInput,
33
+ output_model=GreetOutput,
34
+ )
35
+
36
+
37
+ app = define_app(setup)
38
+ ```
39
+
40
+ Declare `greet` under `exports` in `berth.yml` too. Without `input_model`, a handler receives the raw input dict. Run the app with `berth dev` from [`@berthos/cli`](https://www.npmjs.com/package/@berthos/cli); the [`hello-world-py`](https://github.com/Ash20pk/BerthOS/tree/main/apps/hello-world-py) app is a complete example.
41
+
42
+ ## Docs
43
+
44
+ [Python SDK reference](https://github.com/Ash20pk/BerthOS/blob/main/docs/sdk-python-reference.md) · [Python and the context bus](https://github.com/Ash20pk/BerthOS/blob/main/docs/sdk-python-context-bus-reference.md) · [Repo](https://github.com/Ash20pk/BerthOS)
@@ -0,0 +1,13 @@
1
+ from .app import AppContext, BerthApp, define_app
2
+ from .manifest import BerthManifest, ExportSpec, load_manifest, matches_capability, parse_capability
3
+
4
+ __all__ = [
5
+ "AppContext",
6
+ "BerthApp",
7
+ "define_app",
8
+ "BerthManifest",
9
+ "ExportSpec",
10
+ "load_manifest",
11
+ "matches_capability",
12
+ "parse_capability",
13
+ ]
@@ -0,0 +1,65 @@
1
+ """Mirrors @berthos/sdk's app.ts (defineApp/BerthApp/ExportDefinition). Python
2
+ has no default-export convention, so the equivalent authoring pattern is a
3
+ module-level `app = define_app(...)` — runtime.py looks for that attribute
4
+ name on the imported app module.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from typing import Any, Callable, Optional
11
+
12
+ from .manifest import BerthManifest
13
+
14
+
15
+ @dataclass
16
+ class ExportDefinition:
17
+ name: str
18
+ handler: Callable[[Any], Any]
19
+ # Optional pydantic BaseModel subclasses — validated the same way zod
20
+ # schemas validate input/output in the TS SDK's ExportDefinition.
21
+ input_model: Optional[type] = None
22
+ output_model: Optional[type] = None
23
+
24
+
25
+ class AppContext:
26
+ """Semantic-fs's tag/query control API is deliberately out of scope for
27
+ this SDK — see docs/sdk-python-reference.md. Direct file I/O against the
28
+ FUSE-mounted /context already works from any process with zero SDK code,
29
+ which is this SDK's honest v1 scope for context access. `context_bus` is
30
+ real (see context_bus.py / local_context_bus.py, wired in by
31
+ runtime.py)."""
32
+
33
+ def __init__(self, manifest: BerthManifest, context_bus: Any) -> None:
34
+ self.manifest = manifest
35
+ self.context_bus = context_bus
36
+
37
+
38
+ class BerthApp:
39
+ def __init__(self) -> None:
40
+ self.exports: dict[str, ExportDefinition] = {}
41
+ self.on_install_hooks: list[Callable[[], None]] = []
42
+ self.on_agent_ready_hooks: list[Callable[[AppContext], None]] = []
43
+
44
+ def export(
45
+ self,
46
+ name: str,
47
+ handler: Callable[[Any], Any],
48
+ input_model: Optional[type] = None,
49
+ output_model: Optional[type] = None,
50
+ ) -> None:
51
+ if name in self.exports:
52
+ raise ValueError(f'export "{name}" is already registered')
53
+ self.exports[name] = ExportDefinition(name, handler, input_model, output_model)
54
+
55
+ def on_install(self, fn: Callable[[], None]) -> None:
56
+ self.on_install_hooks.append(fn)
57
+
58
+ def on_agent_ready(self, fn: Callable[[AppContext], None]) -> None:
59
+ self.on_agent_ready_hooks.append(fn)
60
+
61
+
62
+ def define_app(setup: Callable[[BerthApp], None]) -> BerthApp:
63
+ app = BerthApp()
64
+ setup(app)
65
+ return app
@@ -0,0 +1,114 @@
1
+ """Real context-bus client — talks to the same Rust daemon @berthos/sdk's
2
+ unix-socket.ts does, over the same wire contract: a Unix socket carrying
3
+ length-prefixed (4-byte big-endian length + protobuf bytes) Envelope frames,
4
+ per proto/context_bus.proto. Unlike rpc.py's protocol (plain newline-JSON,
5
+ reused as-is), this one needs real protobuf codegen — see
6
+ scripts/gen_proto.sh for how context_bus_pb2.py is generated, and why
7
+ `python3 -m grpc_tools.protoc` is used instead of a bare system `protoc`
8
+ (version-compatibility reasons documented there).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import socket
15
+ import threading
16
+ import time
17
+ from collections import defaultdict
18
+ from typing import Any, Callable, Optional
19
+
20
+ from . import context_bus_pb2 as pb
21
+
22
+ CONNECT_TIMEOUT_S = 2.0
23
+ CONNECT_RETRY_INTERVAL_S = 0.1
24
+
25
+
26
+ class ContextBusClient:
27
+ """Same interface shape as @berthos/sdk's ContextBusClient
28
+ (register/publish/subscribe) — resident app code doesn't change based on
29
+ which implementation runtime.py wires in, matching the TS SDK's own
30
+ local-vs-real symmetry."""
31
+
32
+ def __init__(self, sock: socket.socket) -> None:
33
+ self._sock = sock
34
+ self._lock = threading.Lock()
35
+ self._handlers: dict[str, list[Callable[[Any], None]]] = defaultdict(list)
36
+ self._read_buffer = b""
37
+ self._reader_thread = threading.Thread(target=self._read_loop, daemon=True)
38
+ self._reader_thread.start()
39
+
40
+ def _read_loop(self) -> None:
41
+ while True:
42
+ try:
43
+ chunk = self._sock.recv(4096)
44
+ except OSError:
45
+ return
46
+ if not chunk:
47
+ return
48
+ self._read_buffer += chunk
49
+ while len(self._read_buffer) >= 4:
50
+ length = int.from_bytes(self._read_buffer[:4], "big")
51
+ if len(self._read_buffer) < 4 + length:
52
+ break
53
+ frame = self._read_buffer[4 : 4 + length]
54
+ self._read_buffer = self._read_buffer[4 + length :]
55
+ self._handle_frame(frame)
56
+
57
+ def _handle_frame(self, frame: bytes) -> None:
58
+ envelope = pb.Envelope()
59
+ envelope.ParseFromString(frame)
60
+ if envelope.HasField("event"):
61
+ topic = envelope.event.topic
62
+ raw_payload = envelope.event.payload
63
+ payload = json.loads(raw_payload.decode("utf-8")) if raw_payload else None
64
+ for handler in list(self._handlers.get(topic, [])):
65
+ handler(payload)
66
+
67
+ def _send(self, **kwargs: Any) -> None:
68
+ envelope = pb.Envelope(**kwargs)
69
+ encoded = envelope.SerializeToString()
70
+ with self._lock:
71
+ self._sock.sendall(len(encoded).to_bytes(4, "big") + encoded)
72
+
73
+ # Plain synchronous methods, not `async def` — unlike the TS SDK
74
+ # (Node's I/O model makes everything naturally async), this runtime has
75
+ # no event loop: app hooks are called as plain sync functions, and the
76
+ # underlying socket write is already a blocking sendall(). Making these
77
+ # `async def` would just produce un-awaited coroutine objects that never
78
+ # actually run when called from a sync hook.
79
+ def register(self, app: str) -> None:
80
+ self._send(register=pb.RegisterRequest(app=app))
81
+
82
+ def publish(self, topic: str, payload: Any) -> None:
83
+ encoded_payload = json.dumps(payload if payload is not None else None).encode("utf-8")
84
+ self._send(publish=pb.PublishRequest(topic=topic, payload=encoded_payload))
85
+
86
+ def subscribe(self, topic: str, handler: Callable[[Any], None]) -> Callable[[], None]:
87
+ is_first = topic not in self._handlers or len(self._handlers[topic]) == 0
88
+ self._handlers[topic].append(handler)
89
+ if is_first:
90
+ self._send(subscribe=pb.SubscribeRequest(topic=topic))
91
+
92
+ def unsubscribe() -> None:
93
+ handlers = self._handlers.get(topic, [])
94
+ if handler in handlers:
95
+ handlers.remove(handler)
96
+ if not handlers:
97
+ self._send(unsubscribe=pb.UnsubscribeRequest(topic=topic))
98
+
99
+ return unsubscribe
100
+
101
+
102
+ def create_unix_socket_context_bus(socket_path: str) -> ContextBusClient:
103
+ deadline = time.monotonic() + CONNECT_TIMEOUT_S
104
+ last_error: Optional[OSError] = None
105
+ while time.monotonic() < deadline:
106
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
107
+ try:
108
+ sock.connect(socket_path)
109
+ return ContextBusClient(sock)
110
+ except OSError as err:
111
+ last_error = err
112
+ sock.close()
113
+ time.sleep(CONNECT_RETRY_INTERVAL_S)
114
+ raise TimeoutError(f"timed out connecting to {socket_path} after {CONNECT_TIMEOUT_S}s: {last_error}")
@@ -0,0 +1,48 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # NO CHECKED-IN PROTOBUF GENCODE
4
+ # source: context_bus.proto
5
+ # Protobuf Python Version: 6.31.1
6
+ """Generated protocol buffer code."""
7
+ from google.protobuf import descriptor as _descriptor
8
+ from google.protobuf import descriptor_pool as _descriptor_pool
9
+ from google.protobuf import runtime_version as _runtime_version
10
+ from google.protobuf import symbol_database as _symbol_database
11
+ from google.protobuf.internal import builder as _builder
12
+ _runtime_version.ValidateProtobufRuntimeVersion(
13
+ _runtime_version.Domain.PUBLIC,
14
+ 6,
15
+ 31,
16
+ 1,
17
+ '',
18
+ 'context_bus.proto'
19
+ )
20
+ # @@protoc_insertion_point(imports)
21
+
22
+ _sym_db = _symbol_database.Default()
23
+
24
+
25
+
26
+
27
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x11\x63ontext_bus.proto\x12\x10\x62\x65rth.contextbus\"\x1e\n\x0fRegisterRequest\x12\x0b\n\x03\x61pp\x18\x01 \x01(\t\"0\n\x0ePublishRequest\x12\r\n\x05topic\x18\x01 \x01(\t\x12\x0f\n\x07payload\x18\x02 \x01(\x0c\"!\n\x10SubscribeRequest\x12\r\n\x05topic\x18\x01 \x01(\t\"#\n\x12UnsubscribeRequest\x12\r\n\x05topic\x18\x01 \x01(\t\"\'\n\x05\x45vent\x12\r\n\x05topic\x18\x01 \x01(\t\x12\x0f\n\x07payload\x18\x02 \x01(\x0c\" \n\x03\x41\x63k\x12\n\n\x02ok\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\xc4\x02\n\x08\x45nvelope\x12\x35\n\x08register\x18\x01 \x01(\x0b\x32!.berth.contextbus.RegisterRequestH\x00\x12\x33\n\x07publish\x18\x02 \x01(\x0b\x32 .berth.contextbus.PublishRequestH\x00\x12\x37\n\tsubscribe\x18\x03 \x01(\x0b\x32\".berth.contextbus.SubscribeRequestH\x00\x12;\n\x0bunsubscribe\x18\x04 \x01(\x0b\x32$.berth.contextbus.UnsubscribeRequestH\x00\x12(\n\x05\x65vent\x18\x05 \x01(\x0b\x32\x17.berth.contextbus.EventH\x00\x12$\n\x03\x61\x63k\x18\x06 \x01(\x0b\x32\x15.berth.contextbus.AckH\x00\x42\x06\n\x04kindb\x06proto3')
28
+
29
+ _globals = globals()
30
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
31
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'context_bus_pb2', _globals)
32
+ if not _descriptor._USE_C_DESCRIPTORS:
33
+ DESCRIPTOR._loaded_options = None
34
+ _globals['_REGISTERREQUEST']._serialized_start=39
35
+ _globals['_REGISTERREQUEST']._serialized_end=69
36
+ _globals['_PUBLISHREQUEST']._serialized_start=71
37
+ _globals['_PUBLISHREQUEST']._serialized_end=119
38
+ _globals['_SUBSCRIBEREQUEST']._serialized_start=121
39
+ _globals['_SUBSCRIBEREQUEST']._serialized_end=154
40
+ _globals['_UNSUBSCRIBEREQUEST']._serialized_start=156
41
+ _globals['_UNSUBSCRIBEREQUEST']._serialized_end=191
42
+ _globals['_EVENT']._serialized_start=193
43
+ _globals['_EVENT']._serialized_end=232
44
+ _globals['_ACK']._serialized_start=234
45
+ _globals['_ACK']._serialized_end=266
46
+ _globals['_ENVELOPE']._serialized_start=269
47
+ _globals['_ENVELOPE']._serialized_end=593
48
+ # @@protoc_insertion_point(module_scope)
@@ -0,0 +1,117 @@
1
+ """Mirrors @berthos/sdk's generate-capability-policy.ts exactly (same policy
2
+ shape, same deny-by-default network/opt-in read-path rules, same
3
+ per-app baseline write/read paths) — agent-init (Rust) reads whichever
4
+ one ran, TypeScript or Python, without caring which wrote it. Invoked as
5
+ `python3 -m berth_sdk.generate_capability_policy`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ from pathlib import Path
13
+
14
+ from .manifest import load_manifest, parse_capability
15
+
16
+ # Per-app, not container-wide: this used to be all of /tmp for every app, which
17
+ # let one app reach another's RPC socket. See the TypeScript original for why a
18
+ # narrower Landlock policy is only half the fix (DAC is the other half) and for
19
+ # the socket layout these two directories belong to.
20
+ #
21
+ # /dev/null is the one entry that stays shared, and it is here rather than in
22
+ # TERMINAL_WRITE_PATHS because opening it read-write is what any process does
23
+ # when it redirects a child's stdio to it — see the TypeScript original for the
24
+ # strace this came from, and for why /dev/tty is deliberately absent
25
+ #
26
+ def _baseline_write_paths(app_name: str) -> list[str]:
27
+ return ["/dev/null", f"/tmp/{app_name}", f"/run/berth/{app_name}"]
28
+
29
+ # Added only for an app declaring terminal:* — the one thing that capability
30
+ # compiles into the kernel policy. Without it a tmux server cannot allocate a
31
+ # pty on a Landlock-enforcing kernel.
32
+ TERMINAL_WRITE_PATHS = ["/dev/pts", "/dev/ptmx"]
33
+
34
+ # Where github-api-broker.cjs writes the CA an app declaring github:* is told
35
+ # to trust. Read-granted only for such an app, and only because that CA moved
36
+ # out of /tmp (which the read baseline covers in full) into /run/berth —
37
+ # Kept in step with that script's own default.
38
+ GITHUB_BROKER_CERT_DIR = "/run/berth/github-api-broker"
39
+
40
+
41
+ # /tmp stays fully readable even though it is no longer fully writable: reads
42
+ # are not what 1.4 was about, and statting a daemon control socket before
43
+ # connecting to it needs them.
44
+ def _baseline_read_paths(app_name: str) -> list[str]:
45
+ return ["/usr", "/lib", "/etc", "/proc", "/dev", "/tmp", f"/run/berth/{app_name}", str(Path.cwd())]
46
+
47
+
48
+ def _strip_trailing_glob(scope: str) -> str:
49
+ return scope[:-2] if scope.endswith("/*") else scope
50
+
51
+
52
+
53
+ def main() -> None:
54
+ manifest_path = os.environ.get("BERTH_MANIFEST_PATH", str(Path.cwd() / "berth.yml"))
55
+ policy_path = Path(os.environ.get("BERTH_CAPABILITY_POLICY", str(Path.cwd() / ".berth" / "capability-policy.json")))
56
+
57
+ manifest = load_manifest(manifest_path)
58
+ effective_capabilities = list(manifest.capabilities)
59
+
60
+ write_paths = set(_baseline_write_paths(manifest.name))
61
+ declared_read_paths: set[str] = set()
62
+ network_ports: set[int] = set()
63
+ network_unrestricted = False
64
+ needs_github_broker_ca = False
65
+
66
+ for capability in effective_capabilities:
67
+ parsed = parse_capability(capability)
68
+ if parsed.namespace == "filesystem" and parsed.action == "write":
69
+ write_paths.add(_strip_trailing_glob(parsed.scope))
70
+ elif parsed.namespace == "filesystem" and parsed.action == "read":
71
+ declared_read_paths.add(_strip_trailing_glob(parsed.scope))
72
+ elif parsed.namespace == "network" and parsed.action == "connect":
73
+ if parsed.scope == "*":
74
+ network_unrestricted = True
75
+ continue
76
+ try:
77
+ port = int(parsed.scope)
78
+ except ValueError:
79
+ port = -1
80
+ if 0 < port <= 65535:
81
+ network_ports.add(port)
82
+ else:
83
+ print(f'[berth:capability-policy] WARNING: ignoring invalid network:connect scope "{parsed.scope}" (expected a port 1-65535, or "*")')
84
+ elif parsed.namespace == "terminal":
85
+ write_paths.update(TERMINAL_WRITE_PATHS)
86
+ elif parsed.namespace == "github":
87
+ needs_github_broker_ca = True
88
+
89
+ baseline_reads = set(_baseline_read_paths(manifest.name))
90
+ if needs_github_broker_ca:
91
+ baseline_reads.add(GITHUB_BROKER_CERT_DIR)
92
+ read_paths = sorted(baseline_reads | declared_read_paths) if declared_read_paths else []
93
+
94
+ policy = {
95
+ "appName": manifest.name,
96
+ "declaredCapabilities": effective_capabilities,
97
+ "writePaths": sorted(write_paths),
98
+ "readPaths": read_paths,
99
+ "networkPorts": sorted(network_ports),
100
+ "networkUnrestricted": network_unrestricted,
101
+ }
102
+
103
+ policy_path.parent.mkdir(parents=True, exist_ok=True)
104
+ policy_path.write_text(json.dumps(policy, indent=2))
105
+
106
+ if network_unrestricted:
107
+ network_summary = "networkPorts=* (unrestricted)"
108
+ elif network_ports:
109
+ network_summary = f"networkPorts={', '.join(str(p) for p in sorted(network_ports))}"
110
+ else:
111
+ network_summary = "networkPorts=(none — network denied by default)"
112
+ read_summary = f"; readPaths={', '.join(read_paths)}" if read_paths else ""
113
+ print(f"[berth:capability-policy] wrote {policy_path}: writePaths={', '.join(sorted(write_paths))}{read_summary}; {network_summary}")
114
+
115
+
116
+ if __name__ == "__main__":
117
+ main()
@@ -0,0 +1,36 @@
1
+ """Mirrors @berthos/sdk's context-bus/local.ts — an in-process no-op fallback
2
+ used when the real Unix-socket daemon isn't reachable (e.g. running an app
3
+ outside a sandbox during a quick script/test)."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ from collections import defaultdict
9
+ from typing import Any, Callable
10
+
11
+ logger = logging.getLogger("berth.context_bus.local")
12
+
13
+
14
+ class LocalContextBus:
15
+ def __init__(self) -> None:
16
+ self._handlers: dict[str, list[Callable[[Any], None]]] = defaultdict(list)
17
+
18
+ # Plain synchronous methods — see context_bus.py's own comment on why
19
+ # these deliberately aren't `async def` in this runtime.
20
+ def register(self, app: str) -> None:
21
+ logger.debug('register app="%s" (no-op — real daemon not connected)', app)
22
+
23
+ def publish(self, topic: str, payload: Any) -> None:
24
+ logger.debug('publish topic="%s"', topic)
25
+ for handler in list(self._handlers.get(topic, [])):
26
+ handler(payload)
27
+
28
+ def subscribe(self, topic: str, handler: Callable[[Any], None]) -> Callable[[], None]:
29
+ logger.debug('subscribe topic="%s"', topic)
30
+ self._handlers[topic].append(handler)
31
+
32
+ def unsubscribe() -> None:
33
+ if handler in self._handlers[topic]:
34
+ self._handlers[topic].remove(handler)
35
+
36
+ return unsubscribe
@@ -0,0 +1,93 @@
1
+ """Mirrors @berthos/manifest-schema's schema.ts/capability.ts — the manifest
2
+ shape and capability-string grammar are plain data (YAML + a
3
+ namespace:action:scope string), not TypeScript-specific, so a Python
4
+ implementation validates the exact same shape rather than porting any code.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from typing import Literal
11
+
12
+ import yaml
13
+ from pydantic import BaseModel, Field, field_validator
14
+
15
+ CAPABILITY_RE = re.compile(r"^[a-z0-9_-]+:[a-z0-9_-]+:.+$")
16
+ NAME_RE = re.compile(r"^[a-z0-9-]+$")
17
+ VERSION_RE = re.compile(r"^\d+\.\d+\.\d+$")
18
+
19
+ JsonPrimitiveType = Literal["string", "number", "boolean", "object", "array"]
20
+
21
+
22
+ class ExportSpec(BaseModel):
23
+ name: str
24
+ input: dict[str, JsonPrimitiveType] = Field(default_factory=dict)
25
+ output: dict[str, JsonPrimitiveType] = Field(default_factory=dict)
26
+
27
+
28
+ class BerthManifest(BaseModel):
29
+ name: str
30
+ version: str
31
+ description: str = ""
32
+ capabilities: list[str] = Field(default_factory=list)
33
+ exports: list[ExportSpec] = Field(default_factory=list)
34
+ on_install: list[str] = Field(default_factory=list)
35
+ on_agent_ready: list[str] = Field(default_factory=list)
36
+
37
+ @field_validator("name")
38
+ @classmethod
39
+ def _validate_name(cls, v: str) -> str:
40
+ if not NAME_RE.match(v):
41
+ raise ValueError("name must be lowercase alphanumeric with dashes")
42
+ return v
43
+
44
+ @field_validator("version")
45
+ @classmethod
46
+ def _validate_version(cls, v: str) -> str:
47
+ if not VERSION_RE.match(v):
48
+ raise ValueError("version must be semver (x.y.z)")
49
+ return v
50
+
51
+ @field_validator("capabilities")
52
+ @classmethod
53
+ def _validate_capabilities(cls, v: list[str]) -> list[str]:
54
+ for cap in v:
55
+ if not CAPABILITY_RE.match(cap):
56
+ raise ValueError(f"capability must be 'namespace:action:scope', got {cap!r}")
57
+ return v
58
+
59
+
60
+ def load_manifest(path: str) -> BerthManifest:
61
+ with open(path, "r", encoding="utf-8") as f:
62
+ raw = yaml.safe_load(f) or {}
63
+ return BerthManifest.model_validate(raw)
64
+
65
+
66
+ class ParsedCapability:
67
+ __slots__ = ("namespace", "action", "scope")
68
+
69
+ def __init__(self, namespace: str, action: str, scope: str) -> None:
70
+ self.namespace = namespace
71
+ self.action = action
72
+ self.scope = scope
73
+
74
+
75
+ def parse_capability(capability: str) -> ParsedCapability:
76
+ parts = capability.split(":")
77
+ if len(parts) < 3:
78
+ raise ValueError(f'invalid capability string "{capability}": expected \'namespace:action:scope\'')
79
+ namespace, action, *scope_parts = parts
80
+ return ParsedCapability(namespace, action, ":".join(scope_parts))
81
+
82
+
83
+ def _glob_to_regex(glob: str) -> re.Pattern[str]:
84
+ escaped = re.escape(glob).replace(r"\*", ".*")
85
+ return re.compile(f"^{escaped}$")
86
+
87
+
88
+ def matches_capability(granted: str, requested: str) -> bool:
89
+ g = parse_capability(granted)
90
+ r = parse_capability(requested)
91
+ if g.namespace != r.namespace or g.action != r.action:
92
+ return False
93
+ return bool(_glob_to_regex(g.scope).match(r.scope))
@@ -0,0 +1,108 @@
1
+ """The identical line-delimited JSON RPC protocol from @berthos/sdk's rpc.ts —
2
+ {id, export, input} in, {id, result} or {id, error} out — over stdio and,
3
+ optionally, a Unix socket. No length-prefix, no protobuf: this is the
4
+ simplest of the two wire protocols this SDK reuses (see context_bus.py for
5
+ the other, protobuf-framed one), so a straight re-implementation rather than
6
+ anything requiring codegen.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import socketserver
14
+ import sys
15
+ import threading
16
+ from typing import Any, Callable, Optional
17
+
18
+ from .app import BerthApp
19
+
20
+ RpcRequest = dict[str, Any]
21
+ RpcResponse = dict[str, Any]
22
+
23
+
24
+ def invoke_export(app: BerthApp, request: RpcRequest) -> RpcResponse:
25
+ export_def = app.exports.get(request.get("export"))
26
+ if export_def is None:
27
+ return {"id": request.get("id"), "error": f'no such export "{request.get("export")}"'}
28
+
29
+ try:
30
+ raw_input = request.get("input")
31
+ parsed_input = export_def.input_model.model_validate(raw_input) if export_def.input_model else raw_input
32
+ result = export_def.handler(parsed_input)
33
+
34
+ if export_def.output_model is not None and not isinstance(result, export_def.output_model):
35
+ result = export_def.output_model.model_validate(result)
36
+ output_payload = result.model_dump() if hasattr(result, "model_dump") else result
37
+
38
+ return {"id": request.get("id"), "result": output_payload}
39
+ except Exception as err: # matches rpc.ts's catch-all — reported back to the caller, not raised
40
+ return {"id": request.get("id"), "error": str(err)}
41
+
42
+
43
+ def _handle_line(app: BerthApp, line: str, write: Callable[[str], None]) -> None:
44
+ line = line.strip()
45
+ if not line:
46
+ return
47
+ try:
48
+ request = json.loads(line)
49
+ except json.JSONDecodeError:
50
+ print(f"[berth:runtime] ignoring non-JSON RPC line: {line}", file=sys.stderr)
51
+ return
52
+ response = invoke_export(app, request)
53
+ write(json.dumps(response))
54
+
55
+
56
+ class _RpcSocketHandler(socketserver.StreamRequestHandler):
57
+ def handle(self) -> None:
58
+ app: BerthApp = self.server.app # type: ignore[attr-defined]
59
+
60
+ def write(resp: str) -> None:
61
+ self.wfile.write((resp + "\n").encode("utf-8"))
62
+ self.wfile.flush()
63
+
64
+ while True:
65
+ raw = self.rfile.readline()
66
+ if not raw:
67
+ break
68
+ line = raw.decode("utf-8")
69
+ if not line.strip():
70
+ continue
71
+ _handle_line(app, line, write)
72
+
73
+
74
+ class _RpcUnixStreamServer(socketserver.ThreadingUnixStreamServer):
75
+ daemon_threads = True
76
+
77
+
78
+ def start_socket_server(app: BerthApp, socket_path: str) -> None:
79
+ try:
80
+ os.unlink(socket_path)
81
+ except FileNotFoundError:
82
+ pass
83
+
84
+ server = _RpcUnixStreamServer(socket_path, _RpcSocketHandler)
85
+ server.app = app # type: ignore[attr-defined]
86
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
87
+ thread.start()
88
+ print(f"[berth:runtime] RPC server also listening on {socket_path}", file=sys.stderr)
89
+
90
+
91
+ def start_rpc_server(app: BerthApp, socket_path: Optional[str] = None) -> None:
92
+ """Starts the (optional) socket server and logs readiness — does NOT
93
+ block. serve_stdio_forever() is the blocking call, run last in
94
+ runtime.py's boot sequence so "ready" logs before it, matching rpc.ts's
95
+ ordering (its own startRpcServer() is non-blocking; Node's event loop
96
+ is what keeps the process alive)."""
97
+ print("[berth:runtime] RPC server listening on stdio", file=sys.stderr)
98
+ if socket_path:
99
+ start_socket_server(app, socket_path)
100
+
101
+
102
+ def serve_stdio_forever(app: BerthApp) -> None:
103
+ def write(resp: str) -> None:
104
+ sys.stdout.write(resp + "\n")
105
+ sys.stdout.flush()
106
+
107
+ for line in sys.stdin:
108
+ _handle_line(app, line, write)
@@ -0,0 +1,37 @@
1
+ """Mirrors @berthos/sdk's run-lifecycle.ts: reports whether a browser:*
2
+ capability is declared, and separately whether a browser:navigate:*/
3
+ network:host:* capability is declared (entrypoint.sh's own trigger for
4
+ starting the egress broker), via the last stdout line ("1,1" / "0,1" / etc.)
5
+ — entrypoint.sh's Python branch parses this exactly like it already does for
6
+ the Node lifecycle script.
7
+
8
+ It used to also execute the manifest's on_install commands, once, tracked by
9
+ a marker file. That ran as uid 0 with no Landlock domain applied, before one
10
+ could exist; on_install is now a Docker build layer for
11
+ both build targets, and nothing runs it at container boot. See the fuller
12
+ note in run-lifecycle.ts. The marker file went with it — there is no longer a
13
+ boot-time action to run at most once.
14
+
15
+ Invoked as `python3 -m berth_sdk.run_lifecycle`.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ from pathlib import Path
22
+
23
+ from .manifest import load_manifest
24
+
25
+
26
+ def main() -> None:
27
+ manifest_path = os.environ.get("BERTH_MANIFEST_PATH", str(Path.cwd() / "berth.yml"))
28
+
29
+ manifest = load_manifest(manifest_path)
30
+
31
+ needs_browser = any(cap.startswith("browser:") for cap in manifest.capabilities)
32
+ needs_egress_broker = any(cap.startswith("browser:navigate:") or cap.startswith("network:host:") for cap in manifest.capabilities)
33
+ print(f"{'1' if needs_browser else '0'},{'1' if needs_egress_broker else '0'}")
34
+
35
+
36
+ if __name__ == "__main__":
37
+ main()
@@ -0,0 +1,94 @@
1
+ """Mirrors @berthos/sdk's runtime.ts boot sequence: load manifest -> import the
2
+ app module -> assert exports match manifest -> run hooks -> serve RPC. The
3
+ orchestration itself is idiomatic Python (importlib, not a port of Node's
4
+ dynamic import()) — only the wire protocols (manifest shape, RPC framing)
5
+ are shared with the TypeScript SDK, not this glue code.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib.util
11
+ import os
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ from .app import AppContext, BerthApp
16
+ from .context_bus import create_unix_socket_context_bus
17
+ from .local_context_bus import LocalContextBus
18
+ from .manifest import load_manifest
19
+ from .rpc import serve_stdio_forever, start_rpc_server
20
+
21
+
22
+ def _create_context_bus():
23
+ """Real context-bus daemon if reachable (entrypoint.sh starts it before
24
+ this runtime — same file/env contract @berthos/sdk's runtime.ts uses);
25
+ falls back to a local no-op otherwise, so an app never hard-fails just
26
+ because it's running outside a sandbox with the daemon (e.g. a bare
27
+ `python3 -m berth_sdk.runtime` during a quick script)."""
28
+ socket_path = os.environ.get("BERTH_CONTEXT_BUS_SOCKET", "/tmp/berth-context-bus.sock")
29
+ try:
30
+ return create_unix_socket_context_bus(socket_path)
31
+ except Exception as err: # noqa: BLE001 - genuinely any connection failure should fall back, not crash boot
32
+ print(
33
+ f"[berth:runtime] context-bus daemon not reachable at {socket_path} ({err}) — falling back to local no-op",
34
+ file=sys.stderr,
35
+ )
36
+ return LocalContextBus()
37
+
38
+
39
+ def _assert_exports_match_manifest(app: BerthApp, declared_exports: list[str]) -> None:
40
+ code_exports = set(app.exports.keys())
41
+ manifest_exports = set(declared_exports)
42
+
43
+ missing_in_code = sorted(manifest_exports - code_exports)
44
+ missing_in_manifest = sorted(code_exports - manifest_exports)
45
+
46
+ if missing_in_code or missing_in_manifest:
47
+ problems = []
48
+ if missing_in_code:
49
+ problems.append(f"declared in berth.yml but not implemented: {', '.join(missing_in_code)}")
50
+ if missing_in_manifest:
51
+ problems.append(f"implemented in code but not declared in berth.yml: {', '.join(missing_in_manifest)}")
52
+ raise RuntimeError(f"exports mismatch between berth.yml and app code — {'; '.join(problems)}")
53
+
54
+
55
+ def main() -> None:
56
+ app_root = Path(os.getcwd())
57
+ manifest_path = os.environ.get("BERTH_MANIFEST_PATH", str(app_root / "berth.yml"))
58
+ app_entry = os.environ.get("BERTH_APP_ENTRY", str(app_root / "src" / "app.py"))
59
+
60
+ print(f"[berth:runtime] loading manifest from {manifest_path}", file=sys.stderr)
61
+ manifest = load_manifest(manifest_path)
62
+
63
+ print(f"[berth:runtime] loading app entry {app_entry}", file=sys.stderr)
64
+ spec = importlib.util.spec_from_file_location("berth_app_entry", app_entry)
65
+ if spec is None or spec.loader is None:
66
+ raise RuntimeError(f"could not load a Python module from {app_entry}")
67
+ module = importlib.util.module_from_spec(spec)
68
+ spec.loader.exec_module(module)
69
+
70
+ app = getattr(module, "app", None)
71
+ if not isinstance(app, BerthApp):
72
+ raise RuntimeError(f"{app_entry} must define a module-level `app = define_app(...)`")
73
+
74
+ _assert_exports_match_manifest(app, [e.name for e in manifest.exports])
75
+
76
+ for hook in app.on_install_hooks:
77
+ hook()
78
+
79
+ context_bus = _create_context_bus()
80
+ ctx = AppContext(manifest, context_bus)
81
+ for hook in app.on_agent_ready_hooks:
82
+ hook(ctx)
83
+
84
+ start_rpc_server(app, socket_path=os.environ.get("BERTH_RPC_SOCKET"))
85
+ print(f'[berth:runtime] "{manifest.name}" ready', file=sys.stderr)
86
+
87
+ # Blocks forever, reading stdio RPC requests — this is what keeps the
88
+ # process alive, same role Node's active readline listener plays in
89
+ # rpc.ts (its event loop just never empties).
90
+ serve_stdio_forever(app)
91
+
92
+
93
+ if __name__ == "__main__":
94
+ main()
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: berthos-sdk
3
+ Version: 0.2.1
4
+ Summary: Python resident-app SDK for Berth — reuses the same wire protocols as @berthos/sdk (RPC newline-JSON framing, the manifest shape), with idiomatic Python runtime glue rather than a port of the TypeScript code.
5
+ License-Expression: Apache-2.0
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: pyyaml>=6.0
10
+ Requires-Dist: pydantic>=2.0
11
+ Requires-Dist: protobuf>=6.30
12
+ Dynamic: license-file
13
+
14
+ # berthos-sdk
15
+
16
+ Build a Berth resident app in Python. It uses the same `berth.yml` manifest, RPC protocol and context bus as the TypeScript SDK, `@berthos/sdk`.
17
+
18
+ [Berth](https://github.com/Ash20pk/BerthOS) runs an AI agent's tools in a sandbox. Each tool declares what it may touch in a `berth.yml` manifest, and the Linux kernel enforces it.
19
+
20
+ ```sh
21
+ pip install berthos-sdk
22
+ ```
23
+
24
+ Needs Python 3.11+. The import name is `berth_sdk`.
25
+
26
+ ## Usage
27
+
28
+ ```python
29
+ from berth_sdk import define_app
30
+ from pydantic import BaseModel
31
+
32
+
33
+ class GreetInput(BaseModel):
34
+ name: str
35
+
36
+
37
+ class GreetOutput(BaseModel):
38
+ message: str
39
+
40
+
41
+ def setup(app):
42
+ app.export(
43
+ "greet",
44
+ lambda inp: GreetOutput(message=f"hello {inp.name}"),
45
+ input_model=GreetInput,
46
+ output_model=GreetOutput,
47
+ )
48
+
49
+
50
+ app = define_app(setup)
51
+ ```
52
+
53
+ Declare `greet` under `exports` in `berth.yml` too. Without `input_model`, a handler receives the raw input dict. Run the app with `berth dev` from [`@berthos/cli`](https://www.npmjs.com/package/@berthos/cli); the [`hello-world-py`](https://github.com/Ash20pk/BerthOS/tree/main/apps/hello-world-py) app is a complete example.
54
+
55
+ ## Docs
56
+
57
+ [Python SDK reference](https://github.com/Ash20pk/BerthOS/blob/main/docs/sdk-python-reference.md) · [Python and the context bus](https://github.com/Ash20pk/BerthOS/blob/main/docs/sdk-python-context-bus-reference.md) · [Repo](https://github.com/Ash20pk/BerthOS)
@@ -0,0 +1,18 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ berth_sdk/__init__.py
5
+ berth_sdk/app.py
6
+ berth_sdk/context_bus.py
7
+ berth_sdk/context_bus_pb2.py
8
+ berth_sdk/generate_capability_policy.py
9
+ berth_sdk/local_context_bus.py
10
+ berth_sdk/manifest.py
11
+ berth_sdk/rpc.py
12
+ berth_sdk/run_lifecycle.py
13
+ berth_sdk/runtime.py
14
+ berthos_sdk.egg-info/PKG-INFO
15
+ berthos_sdk.egg-info/SOURCES.txt
16
+ berthos_sdk.egg-info/dependency_links.txt
17
+ berthos_sdk.egg-info/requires.txt
18
+ berthos_sdk.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ pyyaml>=6.0
2
+ pydantic>=2.0
3
+ protobuf>=6.30
@@ -0,0 +1 @@
1
+ berth_sdk
@@ -0,0 +1,20 @@
1
+ [project]
2
+ name = "berthos-sdk"
3
+ version = "0.2.1"
4
+ description = "Python resident-app SDK for Berth — reuses the same wire protocols as @berthos/sdk (RPC newline-JSON framing, the manifest shape), with idiomatic Python runtime glue rather than a port of the TypeScript code."
5
+ readme = "README.md"
6
+ license = "Apache-2.0"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.11"
9
+ dependencies = [
10
+ "pyyaml>=6.0",
11
+ "pydantic>=2.0",
12
+ "protobuf>=6.30",
13
+ ]
14
+
15
+ [build-system]
16
+ requires = ["setuptools>=68"]
17
+ build-backend = "setuptools.build_meta"
18
+
19
+ [tool.setuptools.packages.find]
20
+ include = ["berth_sdk*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+