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.
- orreth_agent/__init__.py +20 -0
- orreth_agent/capability.py +51 -0
- orreth_agent/chassis.py +190 -0
- orreth_agent/client.py +212 -0
- orreth_agent/craft.py +100 -0
- orreth_agent/crypto.py +70 -0
- orreth_agent/mind.py +209 -0
- orreth_agent-0.1.0.dist-info/METADATA +74 -0
- orreth_agent-0.1.0.dist-info/RECORD +11 -0
- orreth_agent-0.1.0.dist-info/WHEEL +4 -0
- orreth_agent-0.1.0.dist-info/licenses/LICENSE +202 -0
orreth_agent/__init__.py
ADDED
|
@@ -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
|
orreth_agent/chassis.py
ADDED
|
@@ -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,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.
|