orreth-agent 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.
@@ -0,0 +1,20 @@
1
+ """orreth-agent — the lifeforce kit.
2
+
3
+ from orreth_agent import FieldClient, Chassis, RuleThink, GovernedThink
4
+
5
+ client = FieldClient("http://127.0.0.1:4502", name="scout")
6
+ client.join() # a governed request; becky answers with a lease
7
+ Chassis(client, RuleThink()).run("say hello to the universe")
8
+ # …and the agent is now visible in the Console: roster, orrery, diary, spend.
9
+ """
10
+ from .capability import PANEL_KINDS, manifest
11
+ from .chassis import Chassis, GovernedThink, RuleThink
12
+ from .client import FieldClient, JoinRefused
13
+ from .craft import ResolvedCraft, acquire
14
+ from .crypto import KeyPair, canonical, content_hash, did_key_for
15
+ from .mind import Generation, MindEngineUnavailable, MindParked, OrrethMind, generation
16
+
17
+ __all__ = ["FieldClient", "JoinRefused", "Chassis", "RuleThink", "GovernedThink",
18
+ "KeyPair", "canonical", "content_hash", "did_key_for",
19
+ "OrrethMind", "Generation", "generation", "MindParked", "MindEngineUnavailable",
20
+ "acquire", "ResolvedCraft", "manifest", "PANEL_KINDS"]
@@ -0,0 +1,51 @@
1
+ # PROVENANCE: Fable 5 (claude-fable-5) — 0055, the SDK's side · 2026-08-13
2
+ """The Capability Contract, SDK-side (0055).
3
+
4
+ A capability is five signed declarations and never code: floors · crew ·
5
+ craft · Chronicle · THE MANIFEST. This module builds manifests that the
6
+ Foundation's portal renders blind — eleven panel kinds (JB's L1), the
7
+ lifecycle verbs on the tile (L3), the whole world one card.
8
+
9
+ Install, v1 (JB's L4): repo-local — a genesis module under the repo holds
10
+ CRAFT (the prompts) and MANIFEST (this builder's output); the Foundation's
11
+ planting beat puts both on the shelf, and the portal lists the world the
12
+ moment its crew stands. Crew commands execute from GENESIS ONLY — the
13
+ shelf's editable copy of a manifest never runs a command. Bring-your-own-
14
+ package is a named cloud-phase commitment riding the deed/trust machinery
15
+ (0042); when it lands, `declare()` grows a wire form.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ PANEL_KINDS = {"tabs", "markdown", "chart", "strip", "controls", "download",
20
+ "stat", "bars", "list", "doc", "table",
21
+ "flow", # flow (2026-08-16): a declared DAG the glass draws
22
+ # blind — nodes/edges in the manifest, live state
23
+ # joined from the world's own records
24
+ "reports"} # reports (2026-08-17, the command-first room):
25
+ # the collection as a pill-row INDEX — a row
26
+ # opens its record's detail panels on demand,
27
+ # never a wall the human scrolls past
28
+
29
+
30
+ def manifest(*, key: str, name: str, emoji: str, resident: str, floor: str,
31
+ port: int, law: str, view: list, door: str | None = None,
32
+ group: str | None = None, crew: list | None = None,
33
+ verbs: dict | None = None, collection: dict | None = None) -> dict:
34
+ """Build a capability manifest. Raises on a panel kind beyond the canon
35
+ vocabulary — the glass renders blind, so the contract checks here."""
36
+ for p in view:
37
+ if p.get("kind") not in PANEL_KINDS:
38
+ raise ValueError(f"panel kind «{p.get('kind')}» is beyond the "
39
+ f"canon vocabulary {sorted(PANEL_KINDS)}")
40
+ out = {"key": key, "name": name, "emoji": emoji, "resident": resident,
41
+ "floor": floor, "port": port, "law": law, "door": door or key,
42
+ "view": view}
43
+ if group:
44
+ out["group"] = group
45
+ if crew:
46
+ out["crew"] = crew
47
+ if verbs:
48
+ out["verbs"] = verbs
49
+ if collection:
50
+ out["collection"] = collection
51
+ return out
@@ -0,0 +1,190 @@
1
+ """The Chassis (0015), network edition: one fixed loop forever; behavior arrives as data.
2
+
3
+ prepare (recall what you already lived) → plan → the NUCLEUS (executes ONLY what the
4
+ planner asked, in parallel; deterministic skills answer free, 'reason' goes through the
5
+ governed door) → critic → replan → repeat until objective or breaker. The breaker doesn't
6
+ fail: it PARKS the intent as a knowledge-acquisition assignment. Failure is fuel.
7
+
8
+ Cognition is injected: RuleThink runs anywhere with zero keys; GovernedThink runs the
9
+ model plane for real. Persona is a costume; the loop is the law; every cycle is a signed
10
+ RunRecord — which is exactly why the agent appears in the Console within one tick.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ from concurrent.futures import ThreadPoolExecutor
16
+
17
+ from .client import FieldClient
18
+
19
+ _PLAN = """{persona}
20
+ Objective: {intent}
21
+ {memory}{feedback}Available skills (deterministic, instant): {skills}.
22
+ Plan the MINIMUM observations needed. Reply ONLY with lines of the form:
23
+ OBSERVE <skill-name>: <question> (to use a deterministic skill)
24
+ OBSERVE reason: <question> (to reason with a model)
25
+ Maximum {max_obs} lines."""
26
+
27
+ _CRITIC = """{persona}
28
+ Objective: {intent}
29
+ Observations gathered:
30
+ {results}
31
+ If the objective is answerable NOW, reply exactly: DONE: <the answer, one paragraph>.
32
+ Otherwise reply exactly: RETRY: <what is missing, one line>."""
33
+
34
+ _CRITIC_REASK = ("Your last reply wore neither face. Reply again with exactly "
35
+ "one line — either: DONE: <the answer> or: RETRY: <what is "
36
+ "missing>.")
37
+
38
+
39
+ def _parse_critic(text: str):
40
+ """0047 sp1 (typed thoughts — the sim's `typed.parse_critic`, twin): the
41
+ critic's word wears exactly one of two faces, and the word after the
42
+ colon is the point. (done, word) or None — a bare DONE with nothing
43
+ after it is not an answer."""
44
+ import re
45
+ m = re.match(r"\s*(DONE|RETRY)\s*:\s*(\S.*)\s*$", text or "", re.I | re.S)
46
+ if not m:
47
+ return None
48
+ return m.group(1).upper() == "DONE", m.group(2).strip()
49
+
50
+
51
+ class Chassis:
52
+ """The architecture never changes; objective, persona, skills, cadence are the profile."""
53
+
54
+ def __init__(self, client: FieldClient, think, *, persona: str = "",
55
+ skills: dict | None = None, max_cycles: int = 3, max_obs: int = 3,
56
+ klass: str = "low"):
57
+ self.client, self.think = client, think
58
+ self.persona, self.skills = persona, skills or {}
59
+ self.max_cycles, self.max_obs, self.klass = max_cycles, max_obs, klass
60
+ self.trace: list[dict] = []
61
+
62
+ def run(self, intent: str) -> dict:
63
+ memory = self._prepare()
64
+ feedback = ""
65
+ for cycle in range(1, self.max_cycles + 1):
66
+ t0 = getattr(self.think, "last_tokens", 0) # per-cycle cost, not cumulative —
67
+ c0 = getattr(self.think, "last_calls", 0) # the roll-ups must add up (0005)
68
+ observations = self._plan(intent, memory, feedback)
69
+ results = self._nucleus(observations)
70
+ critic_prompt = _CRITIC.format(
71
+ persona=self.persona, intent=intent,
72
+ results="\n".join(f"- [{k}] {q} → {r}" for k, q, r in results))
73
+ verdict = self.think(self.klass, critic_prompt)
74
+ parsed = _parse_critic(verdict)
75
+ if parsed is None:
76
+ # 0047 sp1 — the typed re-ask: the contract named ONCE, never
77
+ # a guess; a word that fails twice is an honest RETRY
78
+ verdict = self.think(self.klass,
79
+ critic_prompt + "\n\n" + _CRITIC_REASK)
80
+ parsed = _parse_critic(verdict)
81
+ done, word = parsed if parsed is not None else (
82
+ False, "the critic's word wore neither face twice — "
83
+ "honest retry, never a guess")
84
+ self.trace.append({"cycle": cycle, "observations": len(results),
85
+ "verdict": verdict[:80]})
86
+ self.client.diary(intent, cycle=cycle, done=done,
87
+ tokens=getattr(self.think, "last_tokens", 0) - t0,
88
+ model_calls=getattr(self.think, "last_calls", 0) - c0)
89
+ if done:
90
+ self.client.remember({"objective": intent, "answer": word},
91
+ kind="episodic", tags=["objective", "answered"])
92
+ return {"status": "done", "answer": word, "cycles": cycle}
93
+ feedback = f"Prior attempt lacked: {word}\n"
94
+ self.client.park(intent, feedback.strip())
95
+ return {"status": "parked", "cycles": self.max_cycles}
96
+
97
+ def _prepare(self) -> str:
98
+ """Join the memory thread: what this identity already lived shapes the plan."""
99
+ hits = self.client.recall(days=365).get("hits", [])[:5]
100
+ if not hits:
101
+ return ""
102
+ lines = []
103
+ for h in hits:
104
+ body = self.client.body_of(h["ref"]) or {}
105
+ lines.append(f"- {h['occurred_at'][:10]} · {json.dumps(body)[:120]}")
106
+ return "What you already remember:\n" + "\n".join(lines) + "\n"
107
+
108
+ def _plan(self, intent: str, memory: str, feedback: str) -> list[tuple[str, str]]:
109
+ raw = self.think(self.klass, _PLAN.format(
110
+ persona=self.persona, intent=intent, memory=memory, feedback=feedback,
111
+ skills=", ".join(self.skills) or "none", max_obs=self.max_obs))
112
+ obs = []
113
+ for line in raw.splitlines():
114
+ line = line.strip()
115
+ if line.upper().startswith("OBSERVE") and ":" in line:
116
+ head, question = line.split(":", 1)
117
+ obs.append((head.split()[-1].strip().lower(), question.strip()))
118
+ return obs[: self.max_obs] or [("reason", intent)] # never wander, never stall
119
+
120
+ def _nucleus(self, observations: list[tuple[str, str]]) -> list[tuple[str, str, str]]:
121
+ def one(skill: str, question: str) -> tuple[str, str, str]:
122
+ if skill in self.skills:
123
+ try:
124
+ return (skill, question, str(self.skills[skill](question)))
125
+ except Exception as e: # a skill that errors is an observation too
126
+ return (skill, question, f"(skill error: {e})")
127
+ return ("reason", question,
128
+ self.think(self.klass, f"{self.persona}\nAnswer concisely: {question}"))
129
+ with ThreadPoolExecutor(max_workers=max(1, len(observations))) as pool:
130
+ return list(pool.map(lambda o: one(*o), observations))
131
+
132
+
133
+ # ---- cognition, injected -------------------------------------------------------------------
134
+
135
+ class RuleThink:
136
+ """Deterministic cognition — the chassis runs anywhere with zero keys. It plans one
137
+ observation per available skill (plus one 'reason'), and calls DONE once results exist."""
138
+
139
+ def __init__(self, skills: dict | None = None):
140
+ self.skills = list(skills or {})
141
+ self.last_tokens, self.last_calls = 0, 0
142
+
143
+ def __call__(self, klass: str, prompt: str) -> str:
144
+ if "Plan the MINIMUM" in prompt:
145
+ lines = [f"OBSERVE {s}: {s} for the objective" for s in self.skills[:2]]
146
+ lines.append("OBSERVE reason: summarize what the observations mean")
147
+ return "\n".join(lines)
148
+ if "Observations gathered" in prompt:
149
+ gathered = [l for l in prompt.splitlines() if l.strip().startswith("- [")]
150
+ useful = [l for l in gathered if "(skill error" not in l]
151
+ if useful:
152
+ return "DONE: " + "; ".join(l.split("→", 1)[-1].strip()[:100] for l in useful)
153
+ return "RETRY: every observation errored"
154
+ return "observed: " + prompt.splitlines()[-1][:120]
155
+
156
+
157
+ class GovernedThink:
158
+ """Real cognition through the plane's door: /model/authorize picks the model and debits
159
+ the lease; litellm executes; /model/meter reconciles. The plane never sees a prompt."""
160
+
161
+ def __init__(self, client: FieldClient, *, max_tokens: int = 400):
162
+ self.client, self.max_tokens = client, max_tokens
163
+ self.last_tokens, self.last_calls = 0, 0
164
+
165
+ def __call__(self, klass: str, prompt: str, *, content=None,
166
+ pin: str | None = None) -> str:
167
+ """content (0052 — quinn's eyes): an optional litellm content-parts
168
+ list (text + image parts) replacing the plain prompt in the message;
169
+ `prompt` still sizes the estimate. Same authorize → execute → meter,
170
+ image or not — a governed eye is metered like a governed thought.
171
+ pin (0058 sp2): an assignment's named mind narrows the class — the
172
+ plane refuses a pinned miss rather than serving a substitute."""
173
+ import litellm # optional extra: [governed]
174
+ est = self.max_tokens + len(prompt) // 3 + (1500 if content else 0)
175
+ grant = self.client.authorize(klass, est, pin=pin)
176
+ if not grant or "model" not in grant:
177
+ raise PermissionError("the plane refused this class under the current lease")
178
+ resp = litellm.completion(model=grant["model"],
179
+ messages=[{"role": "user",
180
+ "content": content or prompt}],
181
+ max_tokens=self.max_tokens)
182
+ tokens = resp.usage.total_tokens
183
+ try:
184
+ usd = litellm.completion_cost(completion_response=resp)
185
+ except Exception:
186
+ usd = 0.0
187
+ self.client.meter(grant, klass=klass, tokens=tokens, usd=usd, model=grant["model"])
188
+ self.last_tokens += tokens
189
+ self.last_calls += 1
190
+ return resp.choices[0].message.content
orreth_agent/client.py ADDED
@@ -0,0 +1,212 @@
1
+ """FieldClient — an agent's whole relationship with its universe, over HTTP.
2
+
3
+ The lifeforce sequence (0000 · 0005 · 0006 · 0016):
4
+ spawn (a keypair is a self) → join (a governed REQUEST; becky answers with a lease)
5
+ → remember / recall (signed memory, tokened retrieval) → think (the plane
6
+ authorizes and meters; cognition executes) → diary (every cycle on the record).
7
+
8
+ Nothing here holds privilege: the plane verifies every byte, refuses uniformly, and
9
+ the Console renders whatever this client does within one breath of the tick.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import time
15
+ import urllib.request
16
+ from datetime import datetime, timezone
17
+ from pathlib import Path
18
+ from urllib.error import HTTPError, URLError
19
+
20
+ from .crypto import KeyPair, b64e, canonical, content_hash, did_key_for
21
+
22
+ SIG_KEYS = ("id", "kind", "scope", "author", "occurred_at", "provenance_class")
23
+ RUN_SIG_KEYS = ("id", "agent", "scope", "goal_hash", "occurred_at")
24
+
25
+ # where a self lives between processes — one directory per agent name, seeds inside
26
+ DEFAULT_HOME = Path.home() / ".orreth" / "agents"
27
+
28
+
29
+ def now_iso() -> str:
30
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
31
+
32
+
33
+ class JoinRefused(Exception):
34
+ """The floor did not grant a lease (timeout, denial, or a dark daemon)."""
35
+
36
+
37
+ class FieldClient:
38
+ """One agent, one floor. Point it at any orrethd URL — local, docker, or across the world."""
39
+
40
+ def __init__(self, field_url: str, name: str, *, role: str = "workforce",
41
+ keypair: KeyPair | None = None, scribe: KeyPair | None = None,
42
+ home: str | Path | None = DEFAULT_HOME):
43
+ self.base = field_url.rstrip("/")
44
+ # a keypair is a self, and a self survives the process (0002): seeds persist under
45
+ # home/<name>/ so the SAME agent re-joins every run. home=None → ephemeral.
46
+ if home is not None:
47
+ nest = Path(home) / name
48
+ keypair = keypair or KeyPair.load_or_create(nest / "agent.seed")
49
+ scribe = scribe or KeyPair.load_or_create(nest / "scribe.seed")
50
+ self.kp = keypair or KeyPair()
51
+ self.did = did_key_for(self.kp.public)
52
+ # the diary's co-signer: RunRecords are authored by a scribe, never self-attested (0005)
53
+ self.scribe = scribe or KeyPair()
54
+ self.scribe_did = did_key_for(self.scribe.public)
55
+ self.name, self.role = name, role
56
+ self.token: dict | None = None
57
+ self.scope: str | None = None
58
+
59
+ # ---- wire ---------------------------------------------------------------------------
60
+ def _call(self, method: str, path: str, payload: dict | None = None) -> tuple[int, dict]:
61
+ req = urllib.request.Request(self.base + path, method=method,
62
+ data=json.dumps(payload).encode() if payload is not None else None,
63
+ headers={"Content-Type": "application/json"})
64
+ try:
65
+ with urllib.request.urlopen(req, timeout=10) as r:
66
+ raw = r.read()
67
+ return r.status, (json.loads(raw) if raw[:1] in (b"{", b"[") else {})
68
+ except HTTPError as e:
69
+ raw = e.read() or b"{}"
70
+ return e.code, (json.loads(raw) if raw[:1] in (b"{", b"[") else {})
71
+ except URLError as e:
72
+ raise ConnectionError(f"the floor at {self.base} is dark: {e.reason}") from e
73
+
74
+ # ---- the lifeforce ------------------------------------------------------------------
75
+ def health(self) -> dict:
76
+ return self._call("GET", "/health")[1]
77
+
78
+ def wait_online(self, *, timeout: float = 30.0, poll: float = 1.0) -> dict:
79
+ """Block until the floor answers. A universe restart is transient — an agent waits
80
+ for its home to come back rather than treating a brief outage as a denial."""
81
+ import time as _t
82
+ deadline = _t.monotonic() + timeout
83
+ while True:
84
+ try:
85
+ return self.health()
86
+ except ConnectionError:
87
+ if _t.monotonic() >= deadline:
88
+ raise ConnectionError(
89
+ f"the floor at {self.base} did not come online within {timeout:.0f}s — "
90
+ f"is orrethd running there? (start it with scripts/dev.sh start)")
91
+ _t.sleep(poll)
92
+
93
+ def join(self, *, timeout: float = 120.0, poll: float = 1.0,
94
+ wait_for_floor: float = 30.0) -> dict:
95
+ """Ask the floor for a lease. Joining is a governed handshake in the
96
+ human-visible queue (JB's lock 2026-07-07): becky CHALLENGES this agent to
97
+ sign a nonce — proof of key control, not a claim — then the join waits at
98
+ the HUMAN gate (0012) before she mints the token. The protocol:
99
+ pending → challenged (we sign) → proved → staged (a human decides) → done.
100
+ A restarted desk re-challenges; this client simply proves again."""
101
+ self.scope = self.wait_online(timeout=wait_for_floor, poll=poll).get("scope")
102
+ status, req = self._call("POST", "/requests", {
103
+ "kind": "join", "did": self.did, "name": self.name, "role": self.role,
104
+ "text": f"{self.name} asks to join {self.scope} as {self.role}"})
105
+ if status != 201:
106
+ raise JoinRefused(f"the request queue refused ({status})")
107
+ rid, deadline = req["id"], time.monotonic() + timeout
108
+ proved_nonce = None
109
+ while time.monotonic() < deadline:
110
+ for r in self._call("GET", "/requests")[1].get("requests", []):
111
+ if r.get("id") != rid:
112
+ continue
113
+ st, result = r.get("status"), r.get("result") or {}
114
+ if st == "challenged":
115
+ nonce = result.get("nonce")
116
+ if nonce and nonce != proved_nonce:
117
+ proof = self.kp.sign(self.did, {"join_nonce": nonce, "did": self.did})
118
+ self._call("POST", "/requests/resolve",
119
+ {"id": rid, "status": "proved",
120
+ "result": {"nonce": nonce, "proof": proof}})
121
+ proved_nonce = nonce
122
+ elif st == "done" and isinstance(result, dict) and result.get("token"):
123
+ self.token = result["token"]
124
+ self.remember({"joined": self.name, "role": self.role, "did": self.did},
125
+ kind="episodic", tags=["birth"])
126
+ return self.token
127
+ elif st == "denied":
128
+ raise JoinRefused("the door turned this identity away")
129
+ time.sleep(poll)
130
+ raise JoinRefused(
131
+ f"no lease within {timeout:.0f}s — either becky's desk is not tending "
132
+ f"{self.base} (scripts/dev.sh start), or the join is staged and waiting for "
133
+ f"a HUMAN at the gate: approve it in the Console's Requests tab.")
134
+
135
+ def remember(self, body: dict, *, kind: str = "episodic", tags: list[str] | None = None,
136
+ occurred_at: str | None = None, provenance_class: str = "lived") -> str | None:
137
+ rec = {
138
+ "id": content_hash(body), "kind": kind, "scope": self.scope,
139
+ "author": self.did, "occurred_at": occurred_at or now_iso(),
140
+ "provenance_class": provenance_class,
141
+ "body": b64e(canonical(body)), "retention": "active",
142
+ "visibility": {"tenancy": "tenant-private", "mobility": "branch-bound"},
143
+ "tags": tags or [],
144
+ }
145
+ rec["signature"] = self.kp.sign(self.did, {k: rec[k] for k in SIG_KEYS})
146
+ status, out = self._call("POST", "/records", rec)
147
+ return out.get("id") if status == 201 else None
148
+
149
+ def recall(self, *, days: float = 90, budget: int = 4) -> dict:
150
+ """My biography, through the governed window — escalates up the tree by itself."""
151
+ from_iso = datetime.fromtimestamp(
152
+ datetime.now(timezone.utc).timestamp() - days * 86400, tz=timezone.utc
153
+ ).strftime("%Y-%m-%dT%H:%M:%SZ")
154
+ status, out = self._call("POST", "/retrieve", {
155
+ "query": {"requester": self.did, "subject": "self", "space": "self",
156
+ "time": {"from": from_iso}, "intent": "recall",
157
+ "budget": {"cost": budget}, "auth": "biscuit-sim"},
158
+ "token": self.token, "requester_scope": self.scope})
159
+ return out if status == 200 else {"hits": []}
160
+
161
+ def body_of(self, ref: str) -> dict | None:
162
+ status, out = self._call("GET", f"/records/{ref}/body")
163
+ return out if status == 200 else None
164
+
165
+ # ---- the governed mind (0016: the plane authorizes and meters; we execute) -----------
166
+ def authorize(self, klass: str, est_tokens: int,
167
+ pin: str | None = None) -> dict | None:
168
+ """`pin` (0058 sp2): narrow the class to one named mind — the plane's
169
+ existing `model` field; an assignment's pin rides through here."""
170
+ body = {"token": self.token, "class": klass, "est_tokens": est_tokens}
171
+ if pin:
172
+ body["model"] = pin
173
+ status, grant = self._call("POST", "/model/authorize", body)
174
+ return grant if status == 200 else None
175
+
176
+ def meter(self, grant: dict, *, klass: str, tokens: int, usd: float = 0.0, model: str = "") -> None:
177
+ self._call("POST", "/model/meter", {
178
+ "subject": grant.get("subject", self.did), "est_tokens": grant.get("est_tokens", 0),
179
+ "tokens": tokens, "usd": round(usd, 6), "model": model or grant.get("model", ""),
180
+ "class": klass})
181
+
182
+ # ---- the diary (0005: signed, scribe-authored, never self-attested) ------------------
183
+ def diary(self, intent: str, *, cycle: int, done: bool, tokens: int = 0,
184
+ model_calls: int = 0, score: float | None = None,
185
+ context_hash: str | None = None) -> bool:
186
+ """context_hash (0047 sp2): the exact craft version this cycle thought
187
+ with — the mind's every method call pins its words. Returns whether
188
+ the floor accepted the record."""
189
+ run = {
190
+ "id": content_hash({"i": intent, "c": cycle, "at": now_iso(), "a": self.did}),
191
+ "agent": self.did, "scope": self.scope,
192
+ "goal_hash": content_hash({"intent": intent}),
193
+ "occurred_at": now_iso(), "outcome": "success" if done else "partial",
194
+ "scores": [{"objective": "objective-met",
195
+ "score": score if score is not None else (1.0 if done else 0.0)}],
196
+ "cost": {"tokens": max(tokens, 0), "model_calls": model_calls},
197
+ **({"context_hash": context_hash} if context_hash else {}),
198
+ "author": self.scribe_did,
199
+ }
200
+ run["sig"] = self.scribe.sign(self.scribe_did, {k: run[k] for k in RUN_SIG_KEYS})
201
+ status, _ = self._call("POST", "/runs", run)
202
+ return status == 201
203
+
204
+ def park(self, intent: str, missing: str) -> str | None:
205
+ """The breaker doesn't fail — the unsolved objective becomes fuel (0014)."""
206
+ return self.remember({"parked_intent": intent, "missing": missing,
207
+ "handoff": "knowledge-acquisition"},
208
+ kind="semantic", tags=["parked", "knowledge-intent"])
209
+
210
+ def ask_librarian(self, text: str) -> None:
211
+ """File a gather request — the librarian answers with sourced, quarantined memory."""
212
+ self._call("POST", "/requests", {"kind": "gather", "text": text})
orreth_agent/craft.py ADDED
@@ -0,0 +1,100 @@
1
+ # PROVENANCE: Fable 5 (claude-fable-5) — 0045 sp5, the supply line · 2026-08-03
2
+ """The supply line (0045 law 8): craft is SERVED, not copied.
3
+
4
+ An external flow — LangGraph or anything else, keeping its own shape —
5
+ acquires Orreth's governed craft by reference instead of embedding it. The
6
+ riders, kept here:
7
+
8
+ - **One run, one resolution**: `acquire()` returns a ResolvedCraft; hold it
9
+ for the run and the version never moves under your feet. The run's own
10
+ record should name `resolved.ref` — the exact word that drove it.
11
+ - **A consumer is a citizen**: pass a stable `did` (persist it like a self —
12
+ the F1 mayfly lesson). Arm assignment and the serving log key on it.
13
+ - **Declared failure posture**: `on_dark="refuse"` (default) or `"stale"` —
14
+ a dark registry serves the last signed copy LABELED (`resolved.stale`),
15
+ never silently.
16
+ - **No secret experiments on strangers**: if an argument is running on the
17
+ craft you acquired, `resolved.arm` says so — you can always see which
18
+ side you drew.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import os
24
+ import urllib.parse
25
+ import urllib.request
26
+ from pathlib import Path
27
+
28
+
29
+ class ResolvedCraft:
30
+ """One resolution, carried through the run."""
31
+
32
+ def __init__(self, d: dict, *, stale: bool = False):
33
+ self.name = d["name"]
34
+ self.ref = d["ref"]
35
+ self.version = d["version"]
36
+ self.lifecycle = d.get("lifecycle")
37
+ self.text = d.get("text")
38
+ self.profile = d.get("profile")
39
+ # 0050 sp3: a chronicle asset whose profile IS a template renders
40
+ # like firmware — the persona/sentence shelf serves minds too
41
+ if self.text is None and isinstance(self.profile, dict):
42
+ t = self.profile.get("template")
43
+ if isinstance(t, str):
44
+ self.text = t
45
+ self.arm = d.get("arm")
46
+ self.stale = stale
47
+
48
+ def render(self, **slots) -> str:
49
+ t = self.text or ""
50
+ for k, v in slots.items():
51
+ t = t.replace("⟦" + k + "⟧", str(v))
52
+ return t
53
+
54
+ def __repr__(self):
55
+ return (f"<craft {self.name} v{self.version} {self.ref[:19]}…"
56
+ + (f" arm={self.arm}" if self.arm else "")
57
+ + (" STALE" if self.stale else "") + ">")
58
+
59
+
60
+ def acquire(name: str, *, did: str = "anonymous-consumer",
61
+ base: str = "http://localhost:4562", pin: str | None = None,
62
+ on_dark: str = "refuse",
63
+ cache_home: str = "~/.orreth/craft-cache",
64
+ token: dict | None = None) -> ResolvedCraft:
65
+ """Acquire craft by reference. Resolve ONCE per run and carry the
66
+ returned object; `pin` asks for an exact version ref instead of head.
67
+
68
+ THE LEASE AT THE DOOR (2026-08-23 — 0045 law 8): pass the SAME
69
+ becky-chained lease that fuels your thoughts (`client.token`) and the
70
+ ask rides the leased POST lane; the door verifies the chain and the
71
+ subject. Without it, prod refuses and dev serves wearing a loud
72
+ confession — a citizen presents its lease."""
73
+ home = Path(os.path.expanduser(cache_home))
74
+ home.mkdir(parents=True, exist_ok=True)
75
+ cache = home / (urllib.parse.quote(name, safe="") + ".json")
76
+ try:
77
+ if token:
78
+ body = json.dumps({"name": name, "did": did,
79
+ **({"pin": pin} if pin else {}),
80
+ "token": token}).encode()
81
+ req = urllib.request.Request(
82
+ f"{base}/craft", data=body,
83
+ headers={"Content-Type": "application/json"})
84
+ with urllib.request.urlopen(req, timeout=6) as resp:
85
+ d = json.load(resp)
86
+ else:
87
+ q = (f"{base}/craft?name={urllib.parse.quote(name)}"
88
+ f"&did={urllib.parse.quote(did)}")
89
+ if pin:
90
+ q += f"&pin={urllib.parse.quote(pin)}"
91
+ with urllib.request.urlopen(q, timeout=6) as resp:
92
+ d = json.load(resp)
93
+ if "error" in d:
94
+ raise RuntimeError(d["error"])
95
+ cache.write_text(json.dumps(d))
96
+ return ResolvedCraft(d)
97
+ except Exception:
98
+ if on_dark == "stale" and cache.exists():
99
+ return ResolvedCraft(json.loads(cache.read_text()), stale=True)
100
+ raise
orreth_agent/crypto.py ADDED
@@ -0,0 +1,70 @@
1
+ """Real crypto, vendored from the Orreth reference (0000 §3) so the SDK stands alone.
2
+
3
+ Byte-for-byte parity with the plane's canonicalization is the contract: sorted keys,
4
+ compact separators, ensure_ascii escaping — the exact bytes that get hashed and signed.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import base64
9
+ import hashlib
10
+ import json
11
+ from pathlib import Path
12
+
13
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
14
+ from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat, PublicFormat
15
+
16
+
17
+ def canonical(obj) -> bytes:
18
+ """Canonical JSON: sorted keys, no whitespace. The thing that gets hashed and signed."""
19
+ return json.dumps(obj, sort_keys=True, separators=(",", ":")).encode()
20
+
21
+
22
+ def content_hash(obj) -> str:
23
+ return "sha256:" + hashlib.sha256(canonical(obj)).hexdigest()
24
+
25
+
26
+ def b64e(raw: bytes) -> str:
27
+ return base64.urlsafe_b64encode(raw).decode().rstrip("=")
28
+
29
+
30
+ def b64d(s: str) -> bytes:
31
+ return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
32
+
33
+
34
+ class KeyPair:
35
+ """An Ed25519 identity. Persist the seed and the identity survives the process."""
36
+
37
+ def __init__(self, seed: bytes | None = None) -> None:
38
+ self._priv = (Ed25519PrivateKey.from_private_bytes(seed) if seed
39
+ else Ed25519PrivateKey.generate())
40
+
41
+ @property
42
+ def seed(self) -> bytes:
43
+ return self._priv.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption())
44
+
45
+ @property
46
+ def public(self) -> str:
47
+ raw = self._priv.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
48
+ return "z" + b64e(raw)
49
+
50
+ @classmethod
51
+ def load_or_create(cls, path: Path | str) -> "KeyPair":
52
+ """The self that survives the process (0002): seed bytes on disk, identity forever."""
53
+ path = Path(path)
54
+ if path.exists():
55
+ return cls(seed=path.read_bytes())
56
+ kp = cls()
57
+ path.parent.mkdir(parents=True, exist_ok=True)
58
+ path.write_bytes(kp.seed)
59
+ path.chmod(0o600)
60
+ return kp
61
+
62
+ def sign(self, by_did: str, payload: dict) -> dict:
63
+ """A Sig over canonical(payload minus any 'signature'/'sig') — the plane verifies, never signs."""
64
+ body = {k: v for k, v in payload.items() if k not in ("signature", "sig")}
65
+ return {"alg": "ed25519", "by": by_did, "sig": b64e(self._priv.sign(canonical(body)))}
66
+
67
+
68
+ def did_key_for(public: str) -> str:
69
+ """did:key embeds the key — resolvable offline, joinable from anywhere (0006 §1)."""
70
+ return f"did:key:{public}"
orreth_agent/mind.py ADDED
@@ -0,0 +1,209 @@
1
+ # PROVENANCE: Fable 5 (claude-fable-5) — 0047 sp2, the jacket · 2026-08-07
2
+ """OrrethMind — the governance jacket (0047 §7): identity outside, cognition
3
+ inside.
4
+
5
+ The jacket is OURS and engine-agnostic (0000: lift the contract, port the
6
+ engine). Whatever thinks inside it — the built-in governed-predict lane
7
+ today, the pinned NOOA engine when the studio's CodeAct lane lands — the
8
+ seams never move:
9
+
10
+ · IDENTITY — a FieldClient: the keypair is the self and survives the
11
+ process (rule 1); the mind joins through becky's gate and holds a lease.
12
+ · CRAFT — a generation method names its craft by REFERENCE and the prompt
13
+ is served from the governed shelf (0045 law 8, all four riders): one run,
14
+ one resolution; the run's record names the exact version. This is the
15
+ jacket's deliberate inversion of NOOA — NOOA puts the prompt in the
16
+ docstring; Orreth puts a reference in the decorator and the words in the
17
+ Craft Room, where the humans it serves can read and release them.
18
+ · THE GOVERNED SEAM — every thought is `think(klass, prompt) -> str`; the
19
+ plane authorizes, the caller executes on its own keys, the meter
20
+ reconciles (0016/0019). The jacket never touches a model directly.
21
+ · TYPED THOUGHT — the 0047 sp1 law, kept at this door: a malformed return
22
+ earns ONE re-ask carrying the named error; a twice-failed thought PARKS
23
+ honestly (0015 — failure is fuel) and raises, never a guessed value.
24
+ · THE SCRIBE — one RunRecord per generation-method call, scribe-authored
25
+ (author ≠ agent, 0005), `context_hash` pinning the craft version the
26
+ method thought with.
27
+
28
+ The CodeAct lane refuses honestly until lock 3 (sandbox posture) lands — a
29
+ jacket with an unsandboxed cell would be a hole wearing a nice name.
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import inspect
34
+ import json
35
+ import re
36
+
37
+ from .craft import acquire
38
+
39
+ # Genesis wording (0031 §4: constants are genesis; the shelf may override) —
40
+ # the re-ask names the error and the shape, the sp1 law's SDK twin.
41
+ _REASK = ("Your previous reply was not valid: {error}. Reply again matching "
42
+ "the contract exactly — {shape} — with no preamble and no code "
43
+ "fences.")
44
+
45
+
46
+ class MindParked(Exception):
47
+ """The thought failed its contract twice and was parked — never guessed.
48
+ Carries the park record id (when the floor accepted it) and the error."""
49
+
50
+ def __init__(self, message: str, record: str | None = None):
51
+ super().__init__(message)
52
+ self.record = record
53
+
54
+
55
+ class MindEngineUnavailable(Exception):
56
+ """The requested engine cannot stand yet — the reason names its gate."""
57
+
58
+
59
+ def _shape_of(returns) -> str:
60
+ if isinstance(returns, dict):
61
+ return ("STRICT JSON: {"
62
+ + ", ".join(f'"{k}": {t.__name__}' for k, t in returns.items())
63
+ + "}")
64
+ return "plain text"
65
+
66
+
67
+ def _validate(returns, raw: str):
68
+ """The typed floor (law 4: well-formed is not true). returns may be:
69
+ None/str → raw text passes; a {field: type} dict → strict JSON, fields
70
+ present, types held (int serves float); a callable → returns the value
71
+ or raises ValueError with the error the re-ask will name."""
72
+ if returns is None or returns is str:
73
+ return raw
74
+ if callable(returns) and not isinstance(returns, dict):
75
+ return returns(raw)
76
+ m = re.search(r"\{.*\}", raw or "", re.S)
77
+ if not m:
78
+ raise ValueError("no JSON object found in the reply")
79
+ try:
80
+ got = json.loads(m.group(0))
81
+ except Exception as e:
82
+ raise ValueError(f"the JSON did not parse ({e})") from e
83
+ out = {}
84
+ for k, t in returns.items():
85
+ if k not in got:
86
+ raise ValueError(f"the field \"{k}\" is missing")
87
+ v = got[k]
88
+ if t is float and isinstance(v, int):
89
+ v = float(v)
90
+ if not isinstance(v, t):
91
+ raise ValueError(f"the field \"{k}\" is not {t.__name__}")
92
+ out[k] = v
93
+ return out
94
+
95
+
96
+ class Generation:
97
+ """An ellipsis-bodied method worn by the jacket: craft by reference,
98
+ typed return, one named-error re-ask, a scribe line per call."""
99
+
100
+ def __init__(self, fn, *, klass: str, craft: str, returns=None,
101
+ max_asks: int = 2, est_tokens: int = 400):
102
+ self.fn, self.klass, self.craft = fn, klass, craft
103
+ self.returns, self.max_asks = returns, max(1, int(max_asks))
104
+ self.est_tokens = est_tokens
105
+ self.slots = [p for p in inspect.signature(fn).parameters
106
+ if p != "self"]
107
+ self.__doc__ = fn.__doc__
108
+
109
+ def __set_name__(self, owner, name):
110
+ self.name = name
111
+
112
+ def __get__(self, mind, owner):
113
+ if mind is None:
114
+ return self
115
+ def call(*args, **kwargs):
116
+ return mind._run(self, args, kwargs)
117
+ call.__name__, call.__doc__ = self.name, self.__doc__
118
+ return call
119
+
120
+
121
+ def generation(*, klass: str = "medium", craft: str, returns=None,
122
+ max_asks: int = 2, est_tokens: int = 400):
123
+ """Declare a generation method. `craft` names the shelf record whose
124
+ ⟦slots⟧ are this method's parameters; `returns` is the typed contract."""
125
+ def wrap(fn):
126
+ return Generation(fn, klass=klass, craft=craft, returns=returns,
127
+ max_asks=max_asks, est_tokens=est_tokens)
128
+ return wrap
129
+
130
+
131
+ class OrrethMind:
132
+ """The jacket. Subclass it, declare @generation methods, hand it a
133
+ joined FieldClient and a governed think — then call the methods like
134
+ ordinary Python. Engines: "governed-predict" (built-in, the default);
135
+ "nooa" declares the pinned engine seat (lock 1) and refuses honestly
136
+ until its CodeAct sandbox posture (lock 3) lands."""
137
+
138
+ def __init__(self, client, think, *, engine: str = "governed-predict",
139
+ craft_base: str = "http://localhost:4562",
140
+ on_dark: str = "refuse", pins: dict | None = None,
141
+ craft_fetch=None):
142
+ if engine == "nooa":
143
+ try:
144
+ import nooa # noqa: F401
145
+ except Exception as e:
146
+ raise MindEngineUnavailable(
147
+ "the pinned nooa engine is not installed (lock 1: "
148
+ f"uv add nooa) — {e}") from e
149
+ raise MindEngineUnavailable(
150
+ "the nooa adapter lands with the studio seat — its CodeAct "
151
+ "cell waits on lock 3 (sandbox posture); the jacket's seams "
152
+ "are ready for it")
153
+ if engine != "governed-predict":
154
+ raise MindEngineUnavailable(f"no engine named \"{engine}\"")
155
+ self.client, self.think = client, think
156
+ self.on_dark, self.pins = on_dark, dict(pins or {})
157
+ self._craft_base = craft_base
158
+ self._fetch = craft_fetch or (lambda name: acquire(
159
+ name, did=client.did, base=craft_base,
160
+ pin=self.pins.get(name), on_dark=on_dark,
161
+ # the citizen presents its lease at the supply door (0045 law 8)
162
+ token=getattr(client, "token", None)))
163
+ self._crafts: dict = {} # one run, one resolution (law 8)
164
+
165
+ def _craft(self, name: str):
166
+ if name not in self._crafts:
167
+ self._crafts[name] = self._fetch(name)
168
+ return self._crafts[name]
169
+
170
+ def _run(self, g: Generation, args, kwargs):
171
+ # 0058 sp2 — effort IS class: a caller may override the declared
172
+ # klass per call (`_klass=`), so an objective's Logic dial rides its
173
+ # legs into the gateway instead of dying unread on the request
174
+ klass = kwargs.pop("_klass", None) or g.klass
175
+ resolved = self._craft(g.craft)
176
+ slots = dict(zip(g.slots, args))
177
+ slots.update(kwargs)
178
+ prompt = resolved.render(**{k: slots.get(k, "") for k in g.slots})
179
+ unfilled = sorted(set(re.findall(r"⟦(\w+)⟧", prompt)))
180
+ if unfilled:
181
+ raise ValueError(
182
+ f"the craft \"{g.craft}\" wants slots this method does not "
183
+ f"fill: {', '.join(unfilled)} — declare them as parameters")
184
+ intent = f"mind.{g.name}"
185
+ t0 = getattr(self.think, "last_tokens", 0)
186
+ error = None
187
+ for ask in range(1, g.max_asks + 1):
188
+ ask_prompt = prompt if error is None else (
189
+ prompt + "\n\n" + _REASK.format(
190
+ error=error, shape=_shape_of(g.returns)))
191
+ raw = self.think(klass, ask_prompt)
192
+ try:
193
+ value = _validate(g.returns, raw)
194
+ except ValueError as e:
195
+ error = str(e)
196
+ continue
197
+ self.client.diary(intent, cycle=ask, done=True,
198
+ tokens=getattr(self.think, "last_tokens", 0) - t0,
199
+ model_calls=ask, context_hash=resolved.ref)
200
+ return value
201
+ # the breaker (0015): parked with the error named, never a guess
202
+ rec = self.client.park(intent, f"the reply failed its contract "
203
+ f"{g.max_asks}x: {error}")
204
+ self.client.diary(intent, cycle=g.max_asks, done=False,
205
+ tokens=getattr(self.think, "last_tokens", 0) - t0,
206
+ model_calls=g.max_asks, context_hash=resolved.ref)
207
+ raise MindParked(
208
+ f"{intent} failed its contract {g.max_asks}x ({error}) — parked",
209
+ record=rec)
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.5
2
+ Name: orreth-agent
3
+ Version: 0.1.0
4
+ Summary: The lifeforce kit: give any agent an identity, a home floor, a memory, a governed mind, and a diary — and it appears in the universe.
5
+ Project-URL: Documentation, https://docs.orreth.ai
6
+ Project-URL: Source, https://github.com/iotlodge/orreth
7
+ Project-URL: Demo, https://demo.orreth.ai
8
+ Author: Jonathan Barth
9
+ License: Apache-2.0
10
+ License-File: LICENSE
11
+ Keywords: agents,ai,governance,identity,orreth,runtime
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: cryptography>=42
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=8; extra == 'dev'
23
+ Provides-Extra: governed
24
+ Requires-Dist: litellm>=1.40; extra == 'governed'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # orreth-agent
28
+
29
+ The SDK for building agents that live in an [Orreth](https://docs.orreth.ai)
30
+ universe — a governed runtime where every agent has a permanent identity,
31
+ every action is a signed record, and every model call is authorized and
32
+ metered by a kernel that never sees the prompt.
33
+
34
+ ```bash
35
+ pip install orreth-agent # one dependency: cryptography
36
+ pip install "orreth-agent[governed]" # + litellm, for real model calls
37
+ ```
38
+
39
+ ```python
40
+ from orreth_agent import FieldClient, Chassis, RuleThink
41
+
42
+ client = FieldClient("http://127.0.0.1:4502", name="scout")
43
+ client.join() # a governed request — a human approves your admission
44
+ Chassis(client, RuleThink()).run("say hello to the universe")
45
+ # …and the agent is now visible in the console: roster, diary, spend.
46
+ ```
47
+
48
+ What the kit gives you:
49
+
50
+ - **`FieldClient`** — join a world's floor through its governed gate, keep a
51
+ permanent identity (`KeyPair` seeds persist under `~/.orreth/agents/<name>/`
52
+ — the same agent re-joins across runs), remember and recall signed records,
53
+ and think through the kernel's metered model gateway.
54
+ - **`Chassis`** — the fixed sense→think→act→record loop; bring your own
55
+ thinking (`RuleThink` for deterministic logic, `GovernedThink` for model
56
+ calls through the gateway) or keep your existing framework and use the
57
+ client alone — a LangGraph agent joins the same way.
58
+ - **`OrrethMind`** / **`@generation`** — declare model-backed generations
59
+ whose prompts are *acquired from the world's own governed shelf*
60
+ (`acquire`), so the words your agent runs on are versioned, human-editable
61
+ assets, never hardcoded strings.
62
+ - **`manifest`** / **`PANEL_KINDS`** — build and validate capability
63
+ manifests: whole purposes the Orreth console renders from declarations,
64
+ never from your code.
65
+ - **`canonical` / `content_hash` / `KeyPair`** — the exact signing and
66
+ content-addressing bytes the kernel verifies, held to byte-for-byte parity
67
+ with the reference implementation by the test suite.
68
+
69
+ Start at **[docs.orreth.ai](https://docs.orreth.ai)** — the quickstart gets a
70
+ governed universe running in ten minutes; *Build your first world* stands one
71
+ up from three files against the published kernel image
72
+ (`ghcr.io/iotlodge/orrethd`).
73
+
74
+ Licensed Apache-2.0.
@@ -0,0 +1,11 @@
1
+ orreth_agent/__init__.py,sha256=gG89wWFFix1CboRfse_QKgRNWfKBn6UakvYwGr9uLWw,1073
2
+ orreth_agent/capability.py,sha256=k5LobVqsBGNfZfC5Ndm_VxQVxBkSoUQ2Lbu0hiOJo_Y,2614
3
+ orreth_agent/chassis.py,sha256=wiGLL2w7i4fiHKzw--XWd3aXsqEyGf8c6drmS1c8WUo,9727
4
+ orreth_agent/client.py,sha256=S6uPrkds55b1FNOAB876oofzmUx4FWHbrgB8y1PEbUY,11473
5
+ orreth_agent/craft.py,sha256=vAaH29B1baxbNt0AJL9Fmd-wY_i7coDSkJBgiOF7KUs,4242
6
+ orreth_agent/crypto.py,sha256=qGl4pwsUNWUm1qOEJL0ZPikqK4ozYUiMnaEnUcxmfTg,2554
7
+ orreth_agent/mind.py,sha256=Faq2Hb750Vhl4iFUBWeEOl45kEhjSMY-apqrarYbmrI,9347
8
+ orreth_agent-0.1.0.dist-info/METADATA,sha256=7xvcqOboMHwHOv-Ke6Znmd0V1x33fE15qmZlNzLc2Aw,3340
9
+ orreth_agent-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
10
+ orreth_agent-0.1.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
11
+ orreth_agent-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.