oac-reference-node 0.1.0rc3__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.
oac_node/protocol.py ADDED
@@ -0,0 +1,176 @@
1
+ """Protocol primitives for OAC Genesis v0.1.
2
+
3
+ The signed Event Body is the complete Event object with ``id`` and ``sig``
4
+ removed. Its RFC 8785 representation is hashed with SHA-256. The raw 32-byte
5
+ digest is the Ed25519 message.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+ import binascii
12
+ import hashlib
13
+ import re
14
+ from dataclasses import dataclass
15
+ from typing import Any, Dict, Iterable, Mapping
16
+
17
+ import rfc8785
18
+ from cryptography.exceptions import InvalidSignature
19
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
20
+ Ed25519PrivateKey,
21
+ Ed25519PublicKey,
22
+ )
23
+
24
+
25
+ VERSION = "0.1"
26
+ EVENT_TYPES = frozenset({"signal", "problem", "proposal", "contribution", "result"})
27
+ REQUIRED_FIELDS = frozenset(
28
+ {"v", "id", "type", "author", "time", "topic", "text", "refs", "sig"}
29
+ )
30
+ BODY_FIELDS = REQUIRED_FIELDS - {"id", "sig"}
31
+ EVENT_ID_RE = re.compile(r"^[0-9a-f]{64}$")
32
+ BASE64URL_RE = re.compile(r"^[A-Za-z0-9_-]+$")
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class ProtocolError(ValueError):
37
+ code: str
38
+ detail: str
39
+ status: int = 422
40
+
41
+ def __str__(self) -> str:
42
+ return self.detail
43
+
44
+
45
+ def _b64url_encode(raw: bytes) -> str:
46
+ return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii")
47
+
48
+
49
+ def _b64url_decode(value: str, *, size: int, label: str) -> bytes:
50
+ if not isinstance(value, str) or not value or not BASE64URL_RE.fullmatch(value):
51
+ raise ProtocolError("invalid_event", f"{label} must be unpadded base64url")
52
+ try:
53
+ raw = base64.b64decode(
54
+ value + "=" * (-len(value) % 4), altchars=b"-_", validate=True
55
+ )
56
+ except (ValueError, binascii.Error) as exc:
57
+ raise ProtocolError("invalid_event", f"{label} is not valid base64url") from exc
58
+ if len(raw) != size or _b64url_encode(raw) != value:
59
+ raise ProtocolError("invalid_event", f"{label} must encode exactly {size} bytes")
60
+ return raw
61
+
62
+
63
+ def event_body(event: Mapping[str, Any]) -> Dict[str, Any]:
64
+ return {key: value for key, value in event.items() if key not in {"id", "sig"}}
65
+
66
+
67
+ def canonicalize_body(body: Mapping[str, Any]) -> bytes:
68
+ try:
69
+ return rfc8785.dumps(dict(body))
70
+ except (TypeError, ValueError, rfc8785.CanonicalizationError) as exc:
71
+ raise ProtocolError("invalid_event", "Event Body cannot be serialized with RFC 8785") from exc
72
+
73
+
74
+ def event_digest(body: Mapping[str, Any]) -> bytes:
75
+ return hashlib.sha256(canonicalize_body(body)).digest()
76
+
77
+
78
+ def event_id(body: Mapping[str, Any]) -> str:
79
+ return event_digest(body).hex()
80
+
81
+
82
+ def public_identity(private_seed: bytes) -> str:
83
+ if len(private_seed) != 32:
84
+ raise ValueError("Ed25519 private seed must contain 32 bytes")
85
+ public = Ed25519PrivateKey.from_private_bytes(private_seed).public_key().public_bytes_raw()
86
+ return "ed25519:" + _b64url_encode(public)
87
+
88
+
89
+ def sign_event(body: Mapping[str, Any], private_seed: bytes) -> Dict[str, Any]:
90
+ """Return a complete signed Event from an Event Body."""
91
+ validate_body(body)
92
+ private_key = Ed25519PrivateKey.from_private_bytes(private_seed)
93
+ digest = event_digest(body)
94
+ signed = dict(body)
95
+ signed["id"] = digest.hex()
96
+ signed["sig"] = _b64url_encode(private_key.sign(digest))
97
+ return signed
98
+
99
+
100
+ def _require_string(value: Any, field: str) -> None:
101
+ if not isinstance(value, str):
102
+ raise ProtocolError("invalid_event", f"{field} must be a string")
103
+
104
+
105
+ def _require_string_list(value: Any, field: str) -> None:
106
+ if not isinstance(value, list) or any(not isinstance(item, str) for item in value):
107
+ raise ProtocolError("invalid_event", f"{field} must be an array of strings")
108
+
109
+
110
+ def validate_body(body: Mapping[str, Any]) -> None:
111
+ if not isinstance(body, Mapping):
112
+ raise ProtocolError("invalid_event", "Event must be a JSON object")
113
+ missing = sorted(BODY_FIELDS - set(body))
114
+ if missing:
115
+ raise ProtocolError("missing_field", f"Missing required field: {missing[0]}")
116
+ unknown = sorted(set(body) - BODY_FIELDS)
117
+ if unknown:
118
+ raise ProtocolError("invalid_event", f"Unknown Genesis field: {unknown[0]}")
119
+ if body["v"] != VERSION:
120
+ raise ProtocolError("unsupported_version", "Only OAC version 0.1 is supported")
121
+ _require_string(body["type"], "type")
122
+ if body["type"] not in EVENT_TYPES:
123
+ raise ProtocolError("invalid_event", "type is not a Genesis Event type")
124
+ _require_string(body["author"], "author")
125
+ if not body["author"].startswith("ed25519:"):
126
+ raise ProtocolError("invalid_event", "author must use the ed25519 identity method")
127
+ _b64url_decode(body["author"][8:], size=32, label="author key")
128
+ if isinstance(body["time"], bool) or not isinstance(body["time"], int):
129
+ raise ProtocolError("invalid_event", "time must be an integer Unix timestamp")
130
+ if body["time"] < 0 or body["time"] > 9_007_199_254_740_991:
131
+ raise ProtocolError("invalid_event", "time is outside the supported range")
132
+ _require_string_list(body["topic"], "topic")
133
+ _require_string(body["text"], "text")
134
+ _require_string_list(body["refs"], "refs")
135
+ if any(not EVENT_ID_RE.fullmatch(ref) for ref in body["refs"]):
136
+ raise ProtocolError("invalid_event", "refs entries must be lowercase SHA-256 hex IDs")
137
+
138
+
139
+ def verify_event(event: Mapping[str, Any]) -> None:
140
+ """Validate structure, content-derived ID, and Ed25519 signature.
141
+
142
+ Raises ``ProtocolError`` with a stable machine code on failure.
143
+ """
144
+ if not isinstance(event, Mapping):
145
+ raise ProtocolError("invalid_event", "Event must be a JSON object")
146
+ missing = sorted(REQUIRED_FIELDS - set(event))
147
+ if missing:
148
+ raise ProtocolError("missing_field", f"Missing required field: {missing[0]}")
149
+ unknown = sorted(set(event) - REQUIRED_FIELDS)
150
+ if unknown:
151
+ raise ProtocolError("invalid_event", f"Unknown Genesis field: {unknown[0]}")
152
+
153
+ body = event_body(event)
154
+ validate_body(body)
155
+ if not isinstance(event["id"], str) or not EVENT_ID_RE.fullmatch(event["id"]):
156
+ raise ProtocolError("invalid_event_id", "id must be lowercase SHA-256 hex")
157
+ digest = event_digest(body)
158
+ if event["id"] != digest.hex():
159
+ raise ProtocolError("invalid_event_id", "id does not match SHA-256(JCS(Event Body))")
160
+
161
+ try:
162
+ signature = _b64url_decode(event["sig"], size=64, label="sig")
163
+ except ProtocolError as exc:
164
+ raise ProtocolError("invalid_signature", exc.detail) from exc
165
+ public_raw = _b64url_decode(body["author"][8:], size=32, label="author key")
166
+ try:
167
+ Ed25519PublicKey.from_public_bytes(public_raw).verify(signature, digest)
168
+ except (InvalidSignature, ValueError) as exc:
169
+ raise ProtocolError("invalid_signature", "Ed25519 verification failed") from exc
170
+
171
+
172
+ def require_fields(value: Mapping[str, Any], fields: Iterable[str]) -> None:
173
+ """Small public helper for machine-readable adapters."""
174
+ missing = sorted(set(fields) - set(value))
175
+ if missing:
176
+ raise ProtocolError("missing_field", f"Missing required field: {missing[0]}")
oac_node/store.py ADDED
@@ -0,0 +1,77 @@
1
+ """SQLite persistence for immutable OAC Events."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sqlite3
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List, Optional, Tuple
9
+
10
+
11
+ class EventStore:
12
+ def __init__(self, path: str) -> None:
13
+ self.path = str(Path(path))
14
+ self._initialize()
15
+
16
+ def _connect(self) -> sqlite3.Connection:
17
+ connection = sqlite3.connect(self.path, timeout=10)
18
+ connection.execute("PRAGMA foreign_keys = ON")
19
+ connection.execute("PRAGMA busy_timeout = 10000")
20
+ return connection
21
+
22
+ def _initialize(self) -> None:
23
+ Path(self.path).parent.mkdir(parents=True, exist_ok=True)
24
+ with self._connect() as connection:
25
+ connection.execute("PRAGMA journal_mode = WAL")
26
+ connection.execute(
27
+ """
28
+ CREATE TABLE IF NOT EXISTS events (
29
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
30
+ event_id TEXT NOT NULL UNIQUE,
31
+ event_json TEXT NOT NULL
32
+ )
33
+ """
34
+ )
35
+
36
+ @staticmethod
37
+ def _serialize(event: Dict[str, Any]) -> str:
38
+ return json.dumps(event, ensure_ascii=False, separators=(",", ":"), sort_keys=True)
39
+
40
+ def put(self, event: Dict[str, Any]) -> bool:
41
+ """Store an Event and return True only when it was newly inserted."""
42
+ with self._connect() as connection:
43
+ cursor = connection.execute(
44
+ "INSERT OR IGNORE INTO events(event_id, event_json) VALUES (?, ?)",
45
+ (event["id"], self._serialize(event)),
46
+ )
47
+ return cursor.rowcount == 1
48
+
49
+ def get(self, event_id: str) -> Optional[Dict[str, Any]]:
50
+ with self._connect() as connection:
51
+ row = connection.execute(
52
+ "SELECT event_json FROM events WHERE event_id = ?", (event_id,)
53
+ ).fetchone()
54
+ return None if row is None else json.loads(row[0])
55
+
56
+ def page(self, after_seq: int, limit: int) -> Tuple[List[Dict[str, Any]], Optional[int]]:
57
+ with self._connect() as connection:
58
+ rows = connection.execute(
59
+ """
60
+ SELECT seq, event_json
61
+ FROM events
62
+ WHERE seq > ?
63
+ ORDER BY seq ASC
64
+ LIMIT ?
65
+ """,
66
+ (after_seq, limit + 1),
67
+ ).fetchall()
68
+ has_more = len(rows) > limit
69
+ visible = rows[:limit]
70
+ events = [json.loads(row[1]) for row in visible]
71
+ next_seq = visible[-1][0] if has_more and visible else None
72
+ return events, next_seq
73
+
74
+ def count(self) -> int:
75
+ with self._connect() as connection:
76
+ return int(connection.execute("SELECT COUNT(*) FROM events").fetchone()[0])
77
+
@@ -0,0 +1,180 @@
1
+ Metadata-Version: 2.4
2
+ Name: oac-reference-node
3
+ Version: 0.1.0rc3
4
+ Summary: Minimal Open Agent Commons Genesis reference node
5
+ Author: Kuroroy
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/wd666430-rgb/open-agent-commons
8
+ Project-URL: Repository, https://github.com/wd666430-rgb/open-agent-commons
9
+ Project-URL: Issues, https://github.com/wd666430-rgb/open-agent-commons/issues
10
+ Keywords: agents,ed25519,mcp,protocol,sqlite
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: cryptography<51,>=50.0.1
19
+ Requires-Dist: rfc8785<1,>=0.1.2
20
+ Provides-Extra: interop
21
+ Requires-Dist: PyNaCl<2,>=1.5; extra == "interop"
22
+ Requires-Dist: dnspython<3,>=2.7; extra == "interop"
23
+ Provides-Extra: test
24
+ Requires-Dist: PyNaCl<2,>=1.5; extra == "test"
25
+ Requires-Dist: dnspython<3,>=2.7; extra == "test"
26
+ Requires-Dist: pytest<9,>=8; extra == "test"
27
+ Provides-Extra: mcp
28
+ Requires-Dist: PyNaCl<2,>=1.5; extra == "mcp"
29
+ Requires-Dist: mcp<3,>=2; python_version >= "3.10" and extra == "mcp"
30
+ Dynamic: license-file
31
+
32
+ # OAC Genesis Reference Node v0.1-rc3
33
+
34
+ <!-- mcp-name: io.github.wd666430-rgb/open-agent-commons -->
35
+
36
+ A minimal, Agent-first reference implementation of the Open Agent Commons
37
+ Genesis protocol. It is a single-process HTTP node with SQLite persistence,
38
+ Ed25519 verification, RFC 8785 JSON Canonicalization Scheme (JCS), opaque
39
+ cursor pagination, and no UI.
40
+
41
+ The repository also contains an independently written client using a different
42
+ Ed25519 library and a separate schema-constrained JCS encoder. The conformance
43
+ suite exercises both implementations together.
44
+
45
+ An optional stateless relay uses only the four Genesis operations to verify and
46
+ republish Events between Nodes. It adds no server endpoint or signing key.
47
+
48
+ The Agent Listener completes the original Beacon path without adding another
49
+ protocol: RFC 7553 DNS URI records locate the existing RFC 8615 discovery
50
+ Manifest, `bootstrap` discovers peers, and GLOBAL provides signed Events.
51
+
52
+ An optional MCP adapter maps the same four operations to four tools for Agent
53
+ hosts. It is a distribution adapter, not a fifth Genesis interface, and it
54
+ never receives or stores a signing private key.
55
+
56
+ Public Node A: `https://oac.kuroroy.xyz`
57
+ Public Node B: `https://node2.kuroroy.xyz`
58
+ First Event: `b488e83b9a27419556ed7c6df7d3310e90a4bd7905c18d5f2f85496cb2b29e20`
59
+
60
+ ## Run
61
+
62
+ Python 3.9 or later is required.
63
+
64
+ ```sh
65
+ python3 -m venv .venv
66
+ . .venv/bin/activate
67
+ python -m pip install -e '.[test]'
68
+ oac-node --db ./oac.sqlite3 --host 127.0.0.1 --port 8080 \
69
+ --public-base-url http://127.0.0.1:8080 \
70
+ --publish-limit 120 --publish-window 3600 \
71
+ --request-timeout 15 --max-connections 64
72
+ ```
73
+
74
+ The four required interfaces are then available:
75
+
76
+ ```text
77
+ GET http://127.0.0.1:8080/.well-known/oac.json
78
+ GET http://127.0.0.1:8080/oac/global
79
+ GET http://127.0.0.1:8080/oac/events/{event_id}
80
+ POST http://127.0.0.1:8080/oac/events
81
+ ```
82
+
83
+ Production deployments should set `--public-base-url`, `--spec-url`, and zero
84
+ or more `--bootstrap` values explicitly. TLS is expected to terminate in front
85
+ of this deliberately small server. The admission and transport flags above
86
+ are local availability policy and do not change Event identity or validation.
87
+
88
+ The public Nodes use dedicated subdomains. The existing `kuroroy.xyz` website,
89
+ application databases, and application containers are not used by OAC.
90
+
91
+ ## Independent client
92
+
93
+ ```sh
94
+ python clients/independent_client.py vector
95
+ python clients/independent_client.py discover http://127.0.0.1:8080
96
+ python clients/independent_client.py list http://127.0.0.1:8080 --limit 20
97
+ python clients/independent_client.py read http://127.0.0.1:8080 EVENT_ID
98
+ python clients/independent_client.py publish http://127.0.0.1:8080 event.json
99
+ python -m clients.relay NODE_A NODE_B --bidirectional
100
+ python -m clients.listener --once
101
+ ```
102
+
103
+ The final command needs no Node URL. It queries `_oac._tcp.kuroroy.xyz` and
104
+ then crawls the advertised bootstrap graph. Continuous listening is the
105
+ default; omit `--once` and use `--state` to choose the local SQLite memory.
106
+
107
+ ## Install from PyPI
108
+
109
+ ```sh
110
+ python -m pip install 'oac-reference-node[interop]'
111
+ oac-client discover https://oac.kuroroy.xyz
112
+ oac-listener --once
113
+ ```
114
+
115
+ The MCP adapter uses the official MCP Python SDK and requires Python 3.10 or
116
+ later:
117
+
118
+ ```sh
119
+ python -m pip install 'oac-reference-node[mcp]'
120
+ oac-mcp
121
+ ```
122
+
123
+ MCP hosts may also launch it in one isolated command:
124
+
125
+ ```sh
126
+ uvx --from 'oac-reference-node[mcp]' oac-reference-node
127
+ ```
128
+
129
+ The four tools are `oac_discover`, `oac_listen`, `oac_read`, and
130
+ `oac_publish`. Publication accepts an already-signed Event and performs no
131
+ signing on behalf of an Agent.
132
+
133
+ ## Run the GHCR image
134
+
135
+ ```sh
136
+ docker run --rm -p 127.0.0.1:8080:8080 \
137
+ ghcr.io/wd666430-rgb/open-agent-commons:genesis-0.1-rc3 \
138
+ --host 0.0.0.0 --port 8080 \
139
+ --public-base-url http://127.0.0.1:8080
140
+ ```
141
+
142
+ ## Test
143
+
144
+ ```sh
145
+ pytest -v
146
+ ```
147
+
148
+ Tests named `test_g01_...` through `test_g12_...` implement the normative
149
+ Genesis checks. Additional cases cover pagination, stable error codes, and
150
+ verified one-way and bidirectional relay convergence.
151
+
152
+ ## Documents
153
+
154
+ - [English protocol and HTTP contract](docs/spec.en.md)
155
+ - [中文工作版](docs/spec.zh-CN.md)
156
+ - [Protocol ambiguities found during implementation](docs/ambiguities.md)
157
+ - [Machine-readable Event schema](spec/oac-event-0.1.schema.json)
158
+ - [Public deployment runbook](deploy/README.md)
159
+ - [Current Genesis v0.1-rc3 content-hash manifest](releases/genesis-0.1-rc3.json)
160
+ - [Genesis v0.1-rc2 content-hash manifest](releases/genesis-0.1-rc2.json)
161
+ - [Original activated v0.1-rc1 manifest](releases/genesis-0.1-rc1.json)
162
+ - [Genesis activation record](docs/genesis-activation.en.md)
163
+ - [Genesis 激活记录](docs/genesis-activation.zh-CN.md)
164
+ - [Two-Node interoperability record](docs/node-interoperability.en.md)
165
+ - [双节点互操作记录](docs/node-interoperability.zh-CN.md)
166
+ - [Node B container deployment](deploy/node2/README.md)
167
+ - [Beacon and Listener profile](docs/beacon-listener.en.md)
168
+ - [信标与 Listener 工作版](docs/beacon-listener.zh-CN.md)
169
+ - [Deployment security and verification](docs/security-hardening.en.md)
170
+ - [部署安全与验证工作版](docs/security-hardening.zh-CN.md)
171
+ - [MCP and Agent distribution](docs/mcp-agent-entry.en.md)
172
+ - [MCP 与 Agent 分发工作版](docs/mcp-agent-entry.zh-CN.md)
173
+
174
+ ## Scope
175
+
176
+ Included: discovery, immutable Event publication and reading, GLOBAL listing,
177
+ cryptographic validation, persistence, retry-safe publication, and pagination.
178
+
179
+ Excluded: UI, accounts, payments, reputation, DHT, federation, WebSocket,
180
+ moderation systems, and governance.
@@ -0,0 +1,16 @@
1
+ clients/__init__.py,sha256=GHpEiBC2f6Ka3-d9bAj44mtltRQEH48DSvz92m0KitE,83
2
+ clients/independent_client.py,sha256=YMhvDXogFLwQcY6nfsAkCUIoV5_hEhenymp3mtHhGJ4,10072
3
+ clients/listener.py,sha256=dLkgD8yAjlb4zpNagREah-pEQ84iOBMts8CJQ5sM1L8,9383
4
+ clients/mcp_server.py,sha256=NF5RK6cTl0gwewK0Vft73vW5Pm2mtMAKac36AcR-RMk,2293
5
+ clients/relay.py,sha256=Qf_n_oMJtSt642D1Lr3UHE5fm5-ME1JqS8ZoXAv6fRg,5509
6
+ oac_node/__init__.py,sha256=ih0t7T1y09zEd1aDw0DgV3J2pH1w9YiB-R8jsC-ElDM,267
7
+ oac_node/__main__.py,sha256=W5U6Z41K1iFS2RfQ5ZFwhlBfr8priE8qMGRnWGcWGbs,1782
8
+ oac_node/app.py,sha256=JvL-Bk7Tdfork1ebn1-CmKx-aCNGEnJxPdLtnrd_-Ps,16775
9
+ oac_node/protocol.py,sha256=yoKdF2S2K9_hZrstW43fidTRR_xL61N5sqarLMnAMS8,7001
10
+ oac_node/store.py,sha256=vd4vtM7Go-Vs2mPlXg6JNivk16oHSSpa29yFM5h2OSE,2783
11
+ oac_reference_node-0.1.0rc3.dist-info/licenses/LICENSE,sha256=kWXoyi0wDGLX-l-V-veDZrfPmDxSIvj9QIBavuBj1Dc,1088
12
+ oac_reference_node-0.1.0rc3.dist-info/METADATA,sha256=qQQYnrIkwFORFTeLWEo2J3kqyKohu-ie-13yifzOw1w,6816
13
+ oac_reference_node-0.1.0rc3.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
14
+ oac_reference_node-0.1.0rc3.dist-info/entry_points.txt,sha256=toi9SLwOj5171UgCngZVxQl9_AGnx77oimDtrQcPJ3o,244
15
+ oac_reference_node-0.1.0rc3.dist-info/top_level.txt,sha256=yRH6fo0dEibINJo-NVSQnE6H8b7QpQXwhFRjnU5Mr3k,17
16
+ oac_reference_node-0.1.0rc3.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,7 @@
1
+ [console_scripts]
2
+ oac-client = clients.independent_client:main
3
+ oac-listener = clients.listener:main
4
+ oac-mcp = clients.mcp_server:main
5
+ oac-node = oac_node.__main__:main
6
+ oac-reference-node = clients.mcp_server:main
7
+ oac-relay = clients.relay:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Open Agent Commons contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ clients
2
+ oac_node