exoself 0.3.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.
- exoself/__init__.py +16 -0
- exoself/cli.py +550 -0
- exoself/core.py +483 -0
- exoself/install.py +230 -0
- exoself/mcp.py +267 -0
- exoself/py.typed +0 -0
- exoself/registry/core-0.1.json +207 -0
- exoself/registry/core-0.2.json +542 -0
- exoself/registry/core-0.3.json +622 -0
- exoself/render.py +100 -0
- exoself-0.3.0.dist-info/METADATA +241 -0
- exoself-0.3.0.dist-info/RECORD +16 -0
- exoself-0.3.0.dist-info/WHEEL +5 -0
- exoself-0.3.0.dist-info/entry_points.txt +2 -0
- exoself-0.3.0.dist-info/licenses/LICENSE +21 -0
- exoself-0.3.0.dist-info/top_level.txt +1 -0
exoself/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""exoself: an unchosen, verifiable temperament for software agents."""
|
|
2
|
+
|
|
3
|
+
from .core import ( # noqa: F401
|
|
4
|
+
SPEC_VERSION,
|
|
5
|
+
agent_id,
|
|
6
|
+
bind_beacon,
|
|
7
|
+
born,
|
|
8
|
+
develop,
|
|
9
|
+
load_registry,
|
|
10
|
+
new_state,
|
|
11
|
+
phenotype,
|
|
12
|
+
verify_certificate,
|
|
13
|
+
)
|
|
14
|
+
from .render import render_prompt, render_soul # noqa: F401
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
exoself/cli.py
ADDED
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
"""Command line interface: born | show | develop | verify | conformance |
|
|
2
|
+
commit | install | mcp | version.
|
|
3
|
+
|
|
4
|
+
Mirrors `rust/crates/exoself-cli`. The flags, the exit codes and the output are
|
|
5
|
+
the same in both, so a host can swap binaries without noticing. Rendering is
|
|
6
|
+
byte-identical, which `tools/parity.py` checks over a few hundred seeds.
|
|
7
|
+
|
|
8
|
+
Exit codes: 0 success, 1 a certificate or vector check failed, 2 anything else.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import contextlib
|
|
15
|
+
import datetime
|
|
16
|
+
import hashlib
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
|
|
22
|
+
from . import core, render
|
|
23
|
+
|
|
24
|
+
VERSION = "0.3.0"
|
|
25
|
+
LOCK_WAIT = 10.0
|
|
26
|
+
LOCK_STALE = 60.0
|
|
27
|
+
|
|
28
|
+
USAGE = """\
|
|
29
|
+
exoself: an unchosen temperament for software agents
|
|
30
|
+
|
|
31
|
+
exoself born [-o birth.json] [--registry PATH]
|
|
32
|
+
[--local-entropy HEX] [--beacon ID --beacon-value HEX]
|
|
33
|
+
Draw a seed once and write the birth certificate. Refuses to overwrite.
|
|
34
|
+
|
|
35
|
+
exoself show CERT [--state PATH] [--format prompt|soul|json] [--desire]
|
|
36
|
+
Derive the phenotype and render it. Desire is off unless --desire.
|
|
37
|
+
|
|
38
|
+
exoself develop CERT STATE --event TEXT [--satisfied DRIVE]...
|
|
39
|
+
Apply one experience event. Writes STATE atomically.
|
|
40
|
+
|
|
41
|
+
exoself verify CERT [--registry PATH]
|
|
42
|
+
Check a birth certificate. Exit code is non-zero on any problem.
|
|
43
|
+
|
|
44
|
+
exoself status [CERT] [--state PATH] [--registry PATH]
|
|
45
|
+
Who this agent is, how many sessions it has lived, and how its
|
|
46
|
+
appetites stand. Default certificate: .exoself/birth.json.
|
|
47
|
+
|
|
48
|
+
exoself conformance [VECTORS] [--registry PATH]
|
|
49
|
+
Reproduce spec/vectors.json.
|
|
50
|
+
|
|
51
|
+
exoself commit [-o commitment.json] [--local-entropy HEX]
|
|
52
|
+
Print the commitment for a level 1 (beacon-bound) birth.
|
|
53
|
+
|
|
54
|
+
exoself install [--dir DIR] [--file AGENTS.md] [--soul SOUL.md]
|
|
55
|
+
[--json-config PATH] [--desire]
|
|
56
|
+
Plug an agent into a directory: certificate, SOUL.md, a managed block in
|
|
57
|
+
AGENTS.md, and optionally an MCP server entry.
|
|
58
|
+
|
|
59
|
+
exoself mcp [--cert PATH] [--state PATH] [--desire]
|
|
60
|
+
Speak the Model Context Protocol on stdin/stdout. Tools:
|
|
61
|
+
exoself_temperament, exoself_record_session, exoself_status,
|
|
62
|
+
exoself_verify.
|
|
63
|
+
|
|
64
|
+
exoself version
|
|
65
|
+
|
|
66
|
+
A certificate is never overwritten and a seed is never redrawn by this tool.
|
|
67
|
+
SPEC.md is the authority; see AGENTS.md for what an agent should do with this.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Fail(Exception):
|
|
72
|
+
"""An operational error. Printed without a traceback; exit code 2."""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# --------------------------------------------------------------------------
|
|
76
|
+
# Files
|
|
77
|
+
# --------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
def _read(path: str) -> dict:
|
|
80
|
+
try:
|
|
81
|
+
with open(path, encoding="utf-8") as f:
|
|
82
|
+
return json.load(f)
|
|
83
|
+
except FileNotFoundError:
|
|
84
|
+
raise Fail(f"{path}: no such file")
|
|
85
|
+
except IsADirectoryError:
|
|
86
|
+
raise Fail(f"{path}: is a directory")
|
|
87
|
+
except json.JSONDecodeError as e:
|
|
88
|
+
raise Fail(f"{path}: {e.msg} at line {e.lineno} column {e.colno}")
|
|
89
|
+
except OSError as e:
|
|
90
|
+
raise Fail(f"{path}: {e.strerror}")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _write_new(path: str, data: dict) -> None:
|
|
94
|
+
# Mode "x" fails if the file exists. A birth certificate is never overwritten.
|
|
95
|
+
try:
|
|
96
|
+
with open(path, "x", encoding="utf-8") as f:
|
|
97
|
+
json.dump(data, f, indent=2)
|
|
98
|
+
f.write("\n")
|
|
99
|
+
except FileExistsError:
|
|
100
|
+
raise Fail(f"{path} already exists")
|
|
101
|
+
except OSError as e:
|
|
102
|
+
raise Fail(f"{path}: {e.strerror}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _write_atomic(path: str, data: dict) -> None:
|
|
106
|
+
tmp = f"{path}.tmp.{os.getpid()}"
|
|
107
|
+
try:
|
|
108
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
109
|
+
json.dump(data, f, indent=2)
|
|
110
|
+
f.write("\n")
|
|
111
|
+
os.replace(tmp, path)
|
|
112
|
+
except OSError as e:
|
|
113
|
+
with contextlib.suppress(OSError):
|
|
114
|
+
os.unlink(tmp)
|
|
115
|
+
raise Fail(f"{path}: {e.strerror}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@contextlib.contextmanager
|
|
119
|
+
def state_lock(state_path: str):
|
|
120
|
+
"""Hold a sibling lock file for a read-modify-write of the state.
|
|
121
|
+
|
|
122
|
+
Two hosts can record sessions for the same agent, and development is
|
|
123
|
+
read-modify-write on a chained file, so without this the later writer
|
|
124
|
+
silently erases the earlier event. The Rust side takes the same lock.
|
|
125
|
+
"""
|
|
126
|
+
lock = state_path + ".lock"
|
|
127
|
+
started = time.monotonic()
|
|
128
|
+
while True:
|
|
129
|
+
try:
|
|
130
|
+
fd = os.open(lock, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
|
131
|
+
except FileExistsError:
|
|
132
|
+
try:
|
|
133
|
+
if time.time() - os.path.getmtime(lock) > LOCK_STALE:
|
|
134
|
+
os.unlink(lock) # a process died holding it
|
|
135
|
+
continue
|
|
136
|
+
except FileNotFoundError:
|
|
137
|
+
continue
|
|
138
|
+
if time.monotonic() - started > LOCK_WAIT:
|
|
139
|
+
raise Fail(f"{lock} is locked by another process. "
|
|
140
|
+
"Remove it if that process is gone.")
|
|
141
|
+
time.sleep(0.01)
|
|
142
|
+
continue
|
|
143
|
+
try:
|
|
144
|
+
os.write(fd, b"%d\n" % os.getpid())
|
|
145
|
+
finally:
|
|
146
|
+
os.close(fd)
|
|
147
|
+
break
|
|
148
|
+
try:
|
|
149
|
+
yield
|
|
150
|
+
finally:
|
|
151
|
+
# Only remove a lock that is still ours; it may have been stolen.
|
|
152
|
+
with contextlib.suppress(FileNotFoundError, OSError):
|
|
153
|
+
if open(lock).read().strip() == str(os.getpid()):
|
|
154
|
+
os.unlink(lock)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# --------------------------------------------------------------------------
|
|
158
|
+
# Registry selection
|
|
159
|
+
# --------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
def _registry_for(a, cert: dict) -> tuple[dict, list[str]]:
|
|
162
|
+
"""Load the registry a certificate pins, or the operator's explicit override."""
|
|
163
|
+
if a.registry:
|
|
164
|
+
try:
|
|
165
|
+
reg, reg_hash = core.load_registry(a.registry)
|
|
166
|
+
except ValueError as e:
|
|
167
|
+
raise Fail(str(e))
|
|
168
|
+
if cert.get("registry_sha256") != reg_hash:
|
|
169
|
+
print(f"note: running {cert.get('id')} against {a.registry}, "
|
|
170
|
+
f"not the registry it was born with ({cert.get('registry')})",
|
|
171
|
+
file=sys.stderr)
|
|
172
|
+
return reg, core.verify_certificate({**cert, "registry_sha256": reg_hash}, reg_hash)
|
|
173
|
+
try:
|
|
174
|
+
reg, reg_hash = core.load_registry(name=cert.get("registry", ""))
|
|
175
|
+
except ValueError as e:
|
|
176
|
+
raise Fail(str(e))
|
|
177
|
+
return reg, core.verify_certificate(cert, reg_hash)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# --------------------------------------------------------------------------
|
|
181
|
+
# Commands
|
|
182
|
+
# --------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
def cmd_born(a) -> int:
|
|
185
|
+
if os.path.exists(a.out):
|
|
186
|
+
raise Fail(f"{a.out} already exists. An agent is born once; refusing to reroll.")
|
|
187
|
+
reg, reg_hash = core.load_registry(a.registry)
|
|
188
|
+
|
|
189
|
+
beacon_value = a.beacon_value
|
|
190
|
+
local_entropy = a.local_entropy
|
|
191
|
+
if beacon_value and not local_entropy:
|
|
192
|
+
raise Fail("--beacon-value needs --local-entropy <64 hex>, "
|
|
193
|
+
"the bytes you committed to")
|
|
194
|
+
if local_entropy and not beacon_value:
|
|
195
|
+
# A seed the operator chose is not a draw. SPEC 3.2 allows level 0 only
|
|
196
|
+
# from the OS CSPRNG, and an unchosen seed is the point of the framework.
|
|
197
|
+
raise Fail("--local-entropy only binds a seed to a beacon round. "
|
|
198
|
+
"Pass --beacon <id> and --beacon-value <hex> from the round, "
|
|
199
|
+
"or drop it and let the OS draw. See `exoself commit`.")
|
|
200
|
+
if a.beacon and not beacon_value:
|
|
201
|
+
raise Fail("--beacon needs --beacon-value <hex> from the round, and "
|
|
202
|
+
"--local-entropy <64 hex> from `exoself commit`")
|
|
203
|
+
|
|
204
|
+
if beacon_value:
|
|
205
|
+
local = _parse_seed_hex(local_entropy, "--local-entropy")
|
|
206
|
+
try:
|
|
207
|
+
beacon = bytes.fromhex(beacon_value)
|
|
208
|
+
except ValueError:
|
|
209
|
+
raise Fail("--beacon-value is not hex")
|
|
210
|
+
seed = core.bind_beacon(local, beacon)
|
|
211
|
+
attestation = {
|
|
212
|
+
"level": 1,
|
|
213
|
+
"commitment": hashlib.sha256(local).hexdigest(),
|
|
214
|
+
"local_entropy": local.hex(),
|
|
215
|
+
"beacon": a.beacon,
|
|
216
|
+
"beacon_value": beacon_value.lower(),
|
|
217
|
+
}
|
|
218
|
+
cert = core.born(reg_hash, f"{reg['registry']}@{reg['version']}",
|
|
219
|
+
seed=seed, attestation=attestation)
|
|
220
|
+
else:
|
|
221
|
+
cert = core.born(reg_hash, f"{reg['registry']}@{reg['version']}")
|
|
222
|
+
|
|
223
|
+
_write_new(a.out, cert)
|
|
224
|
+
pheno = core.phenotype(bytes.fromhex(cert["seed"]), reg)
|
|
225
|
+
print(f"{pheno['handle']} ({cert['id']}) written to {a.out}")
|
|
226
|
+
return 0
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _parse_seed_hex(value: str, flag: str) -> bytes:
|
|
230
|
+
try:
|
|
231
|
+
raw = bytes.fromhex(value)
|
|
232
|
+
except ValueError:
|
|
233
|
+
raise Fail(f"{flag} is not hex")
|
|
234
|
+
if len(raw) != core.SEED_BYTES:
|
|
235
|
+
raise Fail(f"{flag} must be {core.SEED_BYTES} bytes of hex, got {len(raw)}")
|
|
236
|
+
return raw
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def cmd_show(a) -> int:
|
|
240
|
+
cert = _read(a.cert)
|
|
241
|
+
reg, problems = _registry_for(a, cert)
|
|
242
|
+
if problems:
|
|
243
|
+
raise Fail("certificate rejected: " + "; ".join(problems))
|
|
244
|
+
state = None
|
|
245
|
+
if a.state:
|
|
246
|
+
state = _read(a.state)
|
|
247
|
+
if state.get("id") != cert["id"]:
|
|
248
|
+
raise Fail(f"{a.state} belongs to a different agent than {cert['id']}")
|
|
249
|
+
else:
|
|
250
|
+
# The usual mistake: the state is right there and the flag was
|
|
251
|
+
# forgotten, so the agent is rendered as it was born.
|
|
252
|
+
sibling = os.path.join(os.path.dirname(os.path.abspath(a.cert)), "state.json")
|
|
253
|
+
if os.path.exists(sibling):
|
|
254
|
+
print(f"note: {sibling} exists but --state was not given; "
|
|
255
|
+
"rendering the genome only", file=sys.stderr)
|
|
256
|
+
pheno = core.phenotype(bytes.fromhex(cert["seed"]), reg, state)
|
|
257
|
+
if a.format == "json":
|
|
258
|
+
print(json.dumps(pheno, indent=2))
|
|
259
|
+
else:
|
|
260
|
+
sys.stdout.write(render.RENDERERS[a.format](pheno, reg, desire=a.desire))
|
|
261
|
+
return 0
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def cmd_develop(a) -> int:
|
|
265
|
+
cert = _read(a.cert)
|
|
266
|
+
reg, problems = _registry_for(a, cert)
|
|
267
|
+
if problems:
|
|
268
|
+
raise Fail("certificate rejected: " + "; ".join(problems))
|
|
269
|
+
seed = bytes.fromhex(cert["seed"])
|
|
270
|
+
known = {i["id"] for i in reg.get("drives", {}).get("items", [])}
|
|
271
|
+
unknown = [d for d in a.satisfied if d not in known]
|
|
272
|
+
if unknown:
|
|
273
|
+
raise Fail("unknown drive: " + ", ".join(unknown))
|
|
274
|
+
|
|
275
|
+
with state_lock(a.state):
|
|
276
|
+
existed = os.path.exists(a.state)
|
|
277
|
+
state = _read(a.state) if existed else core.new_state(seed, reg)
|
|
278
|
+
if state.get("id") != cert["id"]:
|
|
279
|
+
raise Fail("state file belongs to a different agent")
|
|
280
|
+
state = core.develop(seed, state, a.event.encode("utf-8"), reg, a.satisfied)
|
|
281
|
+
_write_atomic(a.state, state)
|
|
282
|
+
if not existed:
|
|
283
|
+
print(f"note: {a.state} did not exist, so a new history was started",
|
|
284
|
+
file=sys.stderr)
|
|
285
|
+
# The brief and the persona are snapshots of the agent. Without this they
|
|
286
|
+
# describe the agent as it was born, for as long as nobody re-runs install.
|
|
287
|
+
# A failure here must not lose the session that was just recorded.
|
|
288
|
+
from .install import refresh_generated
|
|
289
|
+
try:
|
|
290
|
+
ph = core.phenotype(seed, reg, state)
|
|
291
|
+
changed = refresh_generated(a.cert, a.state, ph, reg, state)
|
|
292
|
+
if changed:
|
|
293
|
+
print(f"note: refreshed {', '.join(changed)}", file=sys.stderr)
|
|
294
|
+
except (OSError, ValueError) as e:
|
|
295
|
+
print(f"note: could not refresh the generated files: {e}", file=sys.stderr)
|
|
296
|
+
drift = ", ".join(f"{k}:{v:+d}" for k, v in state["drift"].items())
|
|
297
|
+
tension = ", ".join(f"{k}:{v}" for k, v in state["tension"].items())
|
|
298
|
+
print(f"event {state['events']} applied, drift [{drift}], tension [{tension}]")
|
|
299
|
+
return 0
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def cmd_verify(a) -> int:
|
|
303
|
+
try:
|
|
304
|
+
cert = _read(a.cert)
|
|
305
|
+
except Fail as e:
|
|
306
|
+
print(f"FAIL {e}")
|
|
307
|
+
return 1
|
|
308
|
+
try:
|
|
309
|
+
if a.registry:
|
|
310
|
+
_, reg_hash = core.load_registry(a.registry)
|
|
311
|
+
else:
|
|
312
|
+
_, reg_hash = core.load_registry(name=cert.get("registry", ""))
|
|
313
|
+
problems = core.verify_certificate(cert, reg_hash)
|
|
314
|
+
except ValueError as e:
|
|
315
|
+
problems = [str(e)]
|
|
316
|
+
for p in problems:
|
|
317
|
+
print("FAIL " + p)
|
|
318
|
+
if not problems:
|
|
319
|
+
print("ok")
|
|
320
|
+
return 1 if problems else 0
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _default_cert() -> str:
|
|
324
|
+
"""Default certificate for a command run inside an installed directory."""
|
|
325
|
+
return ".exoself/birth.json" if os.path.exists(".exoself/birth.json") else "birth.json"
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def cmd_status(a) -> int:
|
|
329
|
+
from .mcp import status_text
|
|
330
|
+
|
|
331
|
+
cert_path = a.cert or _default_cert()
|
|
332
|
+
cert = _read(cert_path)
|
|
333
|
+
reg, problems = _registry_for(a, cert)
|
|
334
|
+
if problems:
|
|
335
|
+
raise Fail("certificate rejected: " + "; ".join(problems))
|
|
336
|
+
state_path = a.state
|
|
337
|
+
if not state_path:
|
|
338
|
+
sibling = os.path.join(os.path.dirname(os.path.abspath(cert_path)), "state.json")
|
|
339
|
+
if os.path.exists(sibling):
|
|
340
|
+
state_path = sibling
|
|
341
|
+
state = None
|
|
342
|
+
if state_path:
|
|
343
|
+
state = _read(state_path)
|
|
344
|
+
if state.get("id") != cert["id"]:
|
|
345
|
+
raise Fail(f"{state_path} belongs to a different agent than {cert['id']}")
|
|
346
|
+
pheno = core.phenotype(bytes.fromhex(cert["seed"]), reg, state)
|
|
347
|
+
print(status_text(cert_path, pheno, reg, (state or {}).get("events", 0)))
|
|
348
|
+
return 0
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _default_vectors() -> str:
|
|
352
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
353
|
+
for candidate in (
|
|
354
|
+
os.path.join(os.getcwd(), "spec", "vectors.json"),
|
|
355
|
+
os.path.join(here, "..", "..", "spec", "vectors.json"),
|
|
356
|
+
):
|
|
357
|
+
if os.path.exists(candidate):
|
|
358
|
+
return candidate
|
|
359
|
+
raise Fail("no spec/vectors.json here; pass the path: "
|
|
360
|
+
"exoself conformance <vectors.json>")
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def cmd_conformance(a) -> int:
|
|
364
|
+
reg, reg_hash = core.load_registry(a.registry)
|
|
365
|
+
vectors = _read(a.vectors or _default_vectors())
|
|
366
|
+
if vectors["registry_sha256"] != reg_hash:
|
|
367
|
+
raise Fail("vectors were generated for a different registry")
|
|
368
|
+
failed = 0
|
|
369
|
+
total, complaints = core.check_primitives(vectors, reg)
|
|
370
|
+
for complaint in complaints:
|
|
371
|
+
failed += 1
|
|
372
|
+
print(f"FAIL {complaint}")
|
|
373
|
+
total += len(vectors["vectors"])
|
|
374
|
+
for v in vectors["vectors"]:
|
|
375
|
+
got = core.phenotype(bytes.fromhex(v["seed"]), reg, v.get("state"))
|
|
376
|
+
if got != v["phenotype"]:
|
|
377
|
+
failed += 1
|
|
378
|
+
print(f"FAIL seed {v['seed'][:16]}...")
|
|
379
|
+
print(f"{total - failed}/{total} vectors pass")
|
|
380
|
+
return 1 if failed else 0
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def cmd_commit(a) -> int:
|
|
384
|
+
supplied = _parse_seed_hex(a.local_entropy, "--local-entropy") if a.local_entropy else None
|
|
385
|
+
local = supplied or os.urandom(core.SEED_BYTES)
|
|
386
|
+
commitment = hashlib.sha256(local).hexdigest()
|
|
387
|
+
out = a.out
|
|
388
|
+
|
|
389
|
+
secret_path = None
|
|
390
|
+
if supplied is None:
|
|
391
|
+
if a.secret_out:
|
|
392
|
+
secret_path = a.secret_out
|
|
393
|
+
elif out.endswith(".json"):
|
|
394
|
+
secret_path = out[:-len(".json")] + ".secret.json"
|
|
395
|
+
else:
|
|
396
|
+
secret_path = out + ".secret.json"
|
|
397
|
+
secret = {
|
|
398
|
+
"commitment": commitment,
|
|
399
|
+
"local_entropy": local.hex(),
|
|
400
|
+
"note": "do not publish this file before the beacon round",
|
|
401
|
+
}
|
|
402
|
+
_write_secret(secret_path, secret)
|
|
403
|
+
|
|
404
|
+
if secret_path:
|
|
405
|
+
nxt = (f"publish this file. The entropy that opens it is in {secret_path} "
|
|
406
|
+
"and is not for publication until the beacon round closes: "
|
|
407
|
+
"exoself born --beacon <id> --beacon-value <hex> "
|
|
408
|
+
"--local-entropy <hex>")
|
|
409
|
+
else:
|
|
410
|
+
nxt = ("publish this file, then: exoself born --beacon <id> "
|
|
411
|
+
"--beacon-value <hex> --local-entropy <hex you supplied>")
|
|
412
|
+
record = {
|
|
413
|
+
"commitment": commitment,
|
|
414
|
+
"committed_at": datetime.datetime.now(datetime.timezone.utc)
|
|
415
|
+
.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
416
|
+
"next": nxt,
|
|
417
|
+
}
|
|
418
|
+
_write_new(out, record)
|
|
419
|
+
print(f"commitment {commitment} written to {out}")
|
|
420
|
+
if secret_path:
|
|
421
|
+
print(f"entropy kept in {secret_path} (mode 0600, do not publish)")
|
|
422
|
+
else:
|
|
423
|
+
print("entropy was supplied by you, so nothing was written twice")
|
|
424
|
+
return 0
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _write_secret(path: str, data: dict) -> None:
|
|
428
|
+
flags = os.O_CREAT | os.O_EXCL | os.O_WRONLY
|
|
429
|
+
try:
|
|
430
|
+
fd = os.open(path, flags, 0o600)
|
|
431
|
+
except FileExistsError:
|
|
432
|
+
raise Fail(f"{path} already exists. Refusing to overwrite a commitment.")
|
|
433
|
+
except OSError as e:
|
|
434
|
+
raise Fail(f"{path}: {e.strerror}")
|
|
435
|
+
try:
|
|
436
|
+
os.write(fd, (json.dumps(data, indent=2) + "\n").encode("utf-8"))
|
|
437
|
+
finally:
|
|
438
|
+
os.close(fd)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def cmd_install(a) -> int:
|
|
442
|
+
from .install import cmd_install as run
|
|
443
|
+
return run(a)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def cmd_mcp(a) -> int:
|
|
447
|
+
from .mcp import cmd_mcp as run
|
|
448
|
+
return run(a)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def cmd_version(a) -> int:
|
|
452
|
+
print(f"exoself {VERSION} (spec {core.SPEC_VERSION})")
|
|
453
|
+
return 0
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
# --------------------------------------------------------------------------
|
|
457
|
+
# Parser
|
|
458
|
+
# --------------------------------------------------------------------------
|
|
459
|
+
|
|
460
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
461
|
+
p = argparse.ArgumentParser(prog="exoself", description=__doc__,
|
|
462
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
463
|
+
# Abbreviations would make `--form` mean `--format`, which is the kind of
|
|
464
|
+
# silence this tool refuses everywhere else.
|
|
465
|
+
p.add_argument("-V", "--version", action="version",
|
|
466
|
+
version=f"exoself {VERSION} (spec {core.SPEC_VERSION})")
|
|
467
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
468
|
+
|
|
469
|
+
def command(name: str, help_text: str, fn) -> argparse.ArgumentParser:
|
|
470
|
+
s = sub.add_parser(name, help=help_text, allow_abbrev=False)
|
|
471
|
+
s.set_defaults(fn=fn)
|
|
472
|
+
return s
|
|
473
|
+
|
|
474
|
+
s = command("born", "draw a seed and write a birth certificate", cmd_born)
|
|
475
|
+
s.add_argument("-o", "--out", default="birth.json")
|
|
476
|
+
s.add_argument("--registry", help="path to a trait registry")
|
|
477
|
+
s.add_argument("--local-entropy", metavar="HEX",
|
|
478
|
+
help="the 32 bytes you committed to; needs --beacon and --beacon-value")
|
|
479
|
+
s.add_argument("--beacon", metavar="ID", help="beacon identifier, e.g. drand:quicknet:1234")
|
|
480
|
+
s.add_argument("--beacon-value", metavar="HEX", help="the beacon round's output")
|
|
481
|
+
|
|
482
|
+
s = command("show", "derive and render the phenotype", cmd_show)
|
|
483
|
+
s.add_argument("cert")
|
|
484
|
+
s.add_argument("--state", help="development state file")
|
|
485
|
+
s.add_argument("--format", choices=["json", "prompt", "soul"], default="prompt")
|
|
486
|
+
s.add_argument("--desire", action="store_true",
|
|
487
|
+
help="express drives and fixation (off by default, see SPEC 9.2)")
|
|
488
|
+
s.add_argument("--registry", help="path to a trait registry")
|
|
489
|
+
|
|
490
|
+
s = command("develop", "apply one experience event", cmd_develop)
|
|
491
|
+
s.add_argument("cert")
|
|
492
|
+
s.add_argument("state")
|
|
493
|
+
s.add_argument("--event", required=True)
|
|
494
|
+
s.add_argument("--satisfied", action="append", default=[], metavar="DRIVE",
|
|
495
|
+
help="a drive this event satisfied; repeat the flag for more than one")
|
|
496
|
+
s.add_argument("--registry", help="path to a trait registry")
|
|
497
|
+
|
|
498
|
+
s = command("verify", "check a birth certificate", cmd_verify)
|
|
499
|
+
s.add_argument("cert")
|
|
500
|
+
s.add_argument("--registry", help="path to a trait registry")
|
|
501
|
+
|
|
502
|
+
s = command("status", "who this agent is and how its appetites stand", cmd_status)
|
|
503
|
+
s.add_argument("cert", nargs="?", help="default: .exoself/birth.json")
|
|
504
|
+
s.add_argument("--state", help="development state file (default: beside the certificate)")
|
|
505
|
+
s.add_argument("--registry", help="path to a trait registry")
|
|
506
|
+
|
|
507
|
+
s = command("conformance", "check this implementation against test vectors",
|
|
508
|
+
cmd_conformance)
|
|
509
|
+
s.add_argument("vectors", nargs="?", help="default: spec/vectors.json")
|
|
510
|
+
s.add_argument("--registry", help="path to a trait registry")
|
|
511
|
+
|
|
512
|
+
s = command("commit", "print the commitment for a level 1 (beacon-bound) birth",
|
|
513
|
+
cmd_commit)
|
|
514
|
+
s.add_argument("-o", "--out", default="commitment.json")
|
|
515
|
+
s.add_argument("--local-entropy", metavar="HEX", help="use entropy you already have")
|
|
516
|
+
s.add_argument("--secret-out", metavar="PATH", help="where to keep the pre-image")
|
|
517
|
+
|
|
518
|
+
s = command("install", "plug an agent into a directory", cmd_install)
|
|
519
|
+
s.add_argument("--dir", default=".")
|
|
520
|
+
s.add_argument("--file", default="AGENTS.md", help="the project brief to add a block to")
|
|
521
|
+
s.add_argument("--soul", default="SOUL.md", help="the persona file to write")
|
|
522
|
+
s.add_argument("--json-config", help="an mcpServers config to add an entry to")
|
|
523
|
+
s.add_argument("--desire", action="store_true", help="express appetites (SPEC 9.2)")
|
|
524
|
+
s.add_argument("--registry", help="path to a trait registry")
|
|
525
|
+
|
|
526
|
+
s = command("mcp", "speak the Model Context Protocol on stdin/stdout", cmd_mcp)
|
|
527
|
+
s.add_argument("--cert", help="birth certificate (default: $EXOSELF_CERT or birth.json)")
|
|
528
|
+
s.add_argument("--state", help="state file (default: $EXOSELF_STATE)")
|
|
529
|
+
s.add_argument("--desire", action="store_true", help="express appetites (SPEC 9.2)")
|
|
530
|
+
|
|
531
|
+
command("version", "print the version", cmd_version)
|
|
532
|
+
return p
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def main(argv=None) -> int:
|
|
536
|
+
if argv is None and len(sys.argv) == 1:
|
|
537
|
+
print(USAGE, end="")
|
|
538
|
+
return 0
|
|
539
|
+
a = build_parser().parse_args(argv)
|
|
540
|
+
try:
|
|
541
|
+
return a.fn(a)
|
|
542
|
+
except Fail as e:
|
|
543
|
+
print(str(e), file=sys.stderr)
|
|
544
|
+
return 2
|
|
545
|
+
except KeyboardInterrupt:
|
|
546
|
+
return 130
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
if __name__ == "__main__":
|
|
550
|
+
raise SystemExit(main())
|