alter-runtime 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.
- alter_runtime/__init__.py +11 -0
- alter_runtime/adapters/__init__.py +19 -0
- alter_runtime/adapters/claude_jsonl_watcher.py +545 -0
- alter_runtime/adapters/git_watcher.py +457 -0
- alter_runtime/adapters/household/__init__.py +29 -0
- alter_runtime/adapters/household/_base.py +138 -0
- alter_runtime/adapters/household/compost/__init__.py +17 -0
- alter_runtime/adapters/household/compost/adapter.py +81 -0
- alter_runtime/adapters/household/compost/storage.py +75 -0
- alter_runtime/adapters/household/compost/tests/__init__.py +0 -0
- alter_runtime/adapters/household/compost/tests/test_adapter.py +62 -0
- alter_runtime/adapters/household/compost/tests/test_storage.py +23 -0
- alter_runtime/adapters/household/compost/tests/test_traits.py +38 -0
- alter_runtime/adapters/household/compost/traits.py +79 -0
- alter_runtime/adapters/household/self_hoster/__init__.py +30 -0
- alter_runtime/adapters/household/self_hoster/adapter.py +248 -0
- alter_runtime/adapters/household/self_hoster/storage.py +83 -0
- alter_runtime/adapters/household/self_hoster/tests/__init__.py +0 -0
- alter_runtime/adapters/household/self_hoster/tests/test_adapter.py +216 -0
- alter_runtime/adapters/household/self_hoster/tests/test_storage.py +25 -0
- alter_runtime/adapters/household/self_hoster/tests/test_traits.py +55 -0
- alter_runtime/adapters/household/self_hoster/traits.py +105 -0
- alter_runtime/adapters/household/tapo_ecosystem/__init__.py +22 -0
- alter_runtime/adapters/household/tapo_ecosystem/adapter.py +98 -0
- alter_runtime/adapters/household/tapo_ecosystem/storage.py +95 -0
- alter_runtime/adapters/household/tapo_ecosystem/tests/__init__.py +0 -0
- alter_runtime/adapters/household/tapo_ecosystem/tests/test_adapter.py +55 -0
- alter_runtime/adapters/household/tapo_ecosystem/tests/test_storage.py +28 -0
- alter_runtime/adapters/household/tapo_ecosystem/tests/test_traits.py +45 -0
- alter_runtime/adapters/household/tapo_ecosystem/traits.py +97 -0
- alter_runtime/adapters/household/workshop_tools/__init__.py +25 -0
- alter_runtime/adapters/household/workshop_tools/adapter.py +77 -0
- alter_runtime/adapters/household/workshop_tools/storage.py +92 -0
- alter_runtime/adapters/household/workshop_tools/tests/__init__.py +0 -0
- alter_runtime/adapters/household/workshop_tools/tests/test_adapter.py +48 -0
- alter_runtime/adapters/household/workshop_tools/tests/test_storage.py +26 -0
- alter_runtime/adapters/household/workshop_tools/tests/test_traits.py +45 -0
- alter_runtime/adapters/household/workshop_tools/traits.py +95 -0
- alter_runtime/adapters/worktree_watcher.py +378 -0
- alter_runtime/atlas/__init__.py +48 -0
- alter_runtime/atlas/base.py +102 -0
- alter_runtime/atlas/ledger.py +196 -0
- alter_runtime/atlas/observations.py +136 -0
- alter_runtime/atlas/schema.py +106 -0
- alter_runtime/cap_cache.py +392 -0
- alter_runtime/cli.py +517 -0
- alter_runtime/clients/__init__.py +0 -0
- alter_runtime/clients/token_usage_client.py +273 -0
- alter_runtime/config.py +648 -0
- alter_runtime/consent.py +425 -0
- alter_runtime/daemon.py +518 -0
- alter_runtime/floor_loop.py +335 -0
- alter_runtime/floor_preflight.py +734 -0
- alter_runtime/http_auth.py +173 -0
- alter_runtime/notifiers/__init__.py +18 -0
- alter_runtime/notifiers/desktop.py +321 -0
- alter_runtime/sdk/__init__.py +12 -0
- alter_runtime/sdk/client.py +399 -0
- alter_runtime/service_install.py +616 -0
- alter_runtime/services/__init__.py +59 -0
- alter_runtime/services/launchd/com.alter.runtime.plist.in +90 -0
- alter_runtime/services/systemd/alter-runtime.service.in +74 -0
- alter_runtime/services/systemd/cf-access-env.conf.in +29 -0
- alter_runtime/sockets/__init__.py +20 -0
- alter_runtime/sockets/dbus.py +272 -0
- alter_runtime/sockets/unix.py +702 -0
- alter_runtime/subscribers/__init__.py +58 -0
- alter_runtime/subscribers/active_sessions_cron_emitter.py +313 -0
- alter_runtime/subscribers/active_sessions_do_publisher.py +1159 -0
- alter_runtime/subscribers/active_sessions_gc.py +432 -0
- alter_runtime/subscribers/active_sessions_writer.py +446 -0
- alter_runtime/subscribers/adapters_writer.py +415 -0
- alter_runtime/subscribers/agent_frames.py +461 -0
- alter_runtime/subscribers/bus.py +188 -0
- alter_runtime/subscribers/cache_writer.py +347 -0
- alter_runtime/subscribers/ceremony_echo.py +290 -0
- alter_runtime/subscribers/do_sse.py +864 -0
- alter_runtime/subscribers/ebpf.py +506 -0
- alter_runtime/subscribers/inbox_writer.py +469 -0
- alter_runtime/subscribers/mcp_fallback.py +391 -0
- alter_runtime/subscribers/presence_writer.py +426 -0
- alter_runtime/subscribers/session_presence.py +467 -0
- alter_runtime/subscribers/sse.py +125 -0
- alter_runtime/subscribers/weave_intent_writer.py +608 -0
- alter_runtime/update_loop.py +519 -0
- alter_runtime/weave/__init__.py +21 -0
- alter_runtime/weave/resolver.py +544 -0
- alter_runtime-0.3.0.dist-info/METADATA +289 -0
- alter_runtime-0.3.0.dist-info/RECORD +92 -0
- alter_runtime-0.3.0.dist-info/WHEEL +4 -0
- alter_runtime-0.3.0.dist-info/entry_points.txt +2 -0
- alter_runtime-0.3.0.dist-info/licenses/LICENSE +190 -0
alter_runtime/cli.py
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
"""Command-line entrypoint for alter-runtime.
|
|
2
|
+
|
|
3
|
+
Subcommands:
|
|
4
|
+
init Generate Ed25519 device keypair, install host service unit,
|
|
5
|
+
verify alter-cli session, ensure XDG directories exist.
|
|
6
|
+
start Enable + start the host service unit (systemd/launchd/Windows Service).
|
|
7
|
+
stop Stop the host service unit.
|
|
8
|
+
status Report current daemon state (running? socket reachable? last event?).
|
|
9
|
+
daemon Run the supervisor in the foreground (for systemd Type=exec or debugging).
|
|
10
|
+
query Query the current materialised view (attunement, warmth, trust tier).
|
|
11
|
+
ingest Manually ingest a signal (for testing).
|
|
12
|
+
|
|
13
|
+
Matches the shape of the alter-cli (Node/TS) command handler at
|
|
14
|
+
/mnt/personal/Workspaces/alter-cli/src/index.ts:24-79 - simple switch-on-verb,
|
|
15
|
+
no external CLI framework dependency (argparse is stdlib).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import asyncio
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
import sys
|
|
25
|
+
from collections.abc import Sequence
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
from alter_runtime.config import (
|
|
29
|
+
cache_dir,
|
|
30
|
+
config_dir,
|
|
31
|
+
ensure_directories,
|
|
32
|
+
keypair_path,
|
|
33
|
+
load_config,
|
|
34
|
+
load_session,
|
|
35
|
+
unix_socket_path,
|
|
36
|
+
)
|
|
37
|
+
from alter_runtime.daemon import run_daemon
|
|
38
|
+
from alter_runtime.service_install import current_platform, service_status
|
|
39
|
+
from alter_runtime.service_install import install as install_service
|
|
40
|
+
from alter_runtime.service_install import uninstall as uninstall_service
|
|
41
|
+
|
|
42
|
+
__all__ = ["build_parser", "main"]
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger("alter_runtime.cli")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Argument parser
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
53
|
+
parser = argparse.ArgumentParser(
|
|
54
|
+
prog="alter-runtime",
|
|
55
|
+
description=(
|
|
56
|
+
"~Alter Identity Runtime - L3 local sovereign daemon. "
|
|
57
|
+
"See packages/alter-runtime/README.md for architectural context."
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"--log-level",
|
|
62
|
+
default="INFO",
|
|
63
|
+
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
|
|
64
|
+
help="Logging verbosity (overrides ALTER_RUNTIME_LOG_LEVEL).",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
sub = parser.add_subparsers(dest="command", metavar="<command>")
|
|
68
|
+
sub.required = True
|
|
69
|
+
|
|
70
|
+
# init
|
|
71
|
+
p_init = sub.add_parser(
|
|
72
|
+
"init",
|
|
73
|
+
help="Bootstrap: generate keypair, ensure dirs, verify session.",
|
|
74
|
+
)
|
|
75
|
+
p_init.add_argument(
|
|
76
|
+
"--force",
|
|
77
|
+
action="store_true",
|
|
78
|
+
help="Regenerate the keypair even if one already exists.",
|
|
79
|
+
)
|
|
80
|
+
p_init.add_argument(
|
|
81
|
+
"--install-service",
|
|
82
|
+
action="store_true",
|
|
83
|
+
help=(
|
|
84
|
+
"Also install the host service unit (systemd user unit on Linux, "
|
|
85
|
+
"launchd LaunchAgent on macOS) without starting it."
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# start
|
|
90
|
+
p_start = sub.add_parser(
|
|
91
|
+
"start",
|
|
92
|
+
help="Install + enable + start the host service unit.",
|
|
93
|
+
)
|
|
94
|
+
p_start.add_argument(
|
|
95
|
+
"--dry-run",
|
|
96
|
+
action="store_true",
|
|
97
|
+
help="Render the unit file and print the service commands without touching the filesystem.",
|
|
98
|
+
)
|
|
99
|
+
p_start.add_argument(
|
|
100
|
+
"--yes",
|
|
101
|
+
action="store_true",
|
|
102
|
+
help="Skip the interactive consent prompt (CI / package-manager use).",
|
|
103
|
+
)
|
|
104
|
+
p_start.add_argument(
|
|
105
|
+
"--reinstall",
|
|
106
|
+
action="store_true",
|
|
107
|
+
help=(
|
|
108
|
+
"Reinstall a previously-uninstalled artefact, bypassing the "
|
|
109
|
+
"deletion-permanence tombstone."
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# stop
|
|
114
|
+
sub.add_parser("stop", help="Disable + stop the host service unit.")
|
|
115
|
+
|
|
116
|
+
# status
|
|
117
|
+
sub.add_parser("status", help="Report daemon state and local connectivity.")
|
|
118
|
+
|
|
119
|
+
# daemon (foreground)
|
|
120
|
+
sub.add_parser("daemon", help="Run the supervisor in the foreground.")
|
|
121
|
+
|
|
122
|
+
# query
|
|
123
|
+
p_query = sub.add_parser("query", help="Query the current materialised view.")
|
|
124
|
+
p_query.add_argument(
|
|
125
|
+
"field",
|
|
126
|
+
nargs="?",
|
|
127
|
+
choices=["handle", "attunement", "warmth", "income", "trust_tier", "all"],
|
|
128
|
+
default="all",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# ingest
|
|
132
|
+
p_ingest = sub.add_parser("ingest", help="Manually ingest a signal (for testing).")
|
|
133
|
+
p_ingest.add_argument("--kind", required=True, help="Signal kind (e.g. tool_invocation).")
|
|
134
|
+
p_ingest.add_argument(
|
|
135
|
+
"--payload",
|
|
136
|
+
default="{}",
|
|
137
|
+
help="JSON-encoded payload for the signal.",
|
|
138
|
+
)
|
|
139
|
+
p_ingest.add_argument("--producer", default="cli:manual", help="Producer identifier.")
|
|
140
|
+
|
|
141
|
+
return parser
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
# Command handlers
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def cmd_init(args: argparse.Namespace) -> int:
|
|
150
|
+
"""Bootstrap the daemon environment."""
|
|
151
|
+
ensure_directories()
|
|
152
|
+
logger.info("config dir: %s", config_dir())
|
|
153
|
+
logger.info("cache dir: %s", cache_dir())
|
|
154
|
+
logger.info("unix socket: %s", unix_socket_path())
|
|
155
|
+
|
|
156
|
+
# Keypair
|
|
157
|
+
kp_path = keypair_path()
|
|
158
|
+
if kp_path.exists() and not args.force:
|
|
159
|
+
logger.info("keypair already exists at %s (use --force to regenerate)", kp_path)
|
|
160
|
+
else:
|
|
161
|
+
_generate_keypair(kp_path)
|
|
162
|
+
logger.info("keypair generated at %s", kp_path)
|
|
163
|
+
|
|
164
|
+
# Session check
|
|
165
|
+
session = load_session()
|
|
166
|
+
if session is None:
|
|
167
|
+
print(
|
|
168
|
+
"\nNo alter-cli session found. Run `alter login` to authenticate,\n"
|
|
169
|
+
"then re-run `alter-runtime init` to complete bootstrap.",
|
|
170
|
+
file=sys.stderr,
|
|
171
|
+
)
|
|
172
|
+
return 1
|
|
173
|
+
|
|
174
|
+
print(f"\nReady. handle={session.handle} consent_tier=L{session.consent_tier}")
|
|
175
|
+
|
|
176
|
+
if args.install_service:
|
|
177
|
+
try:
|
|
178
|
+
result = install_service(enable=False, dry_run=False)
|
|
179
|
+
except (FileNotFoundError, NotImplementedError) as exc:
|
|
180
|
+
print(f"service install skipped: {exc}", file=sys.stderr)
|
|
181
|
+
else:
|
|
182
|
+
print(f"service unit written to {result.unit_path} (not started)")
|
|
183
|
+
|
|
184
|
+
print("Next: `alter-runtime start` to launch the host service.")
|
|
185
|
+
return 0
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def cmd_start(args: argparse.Namespace) -> int:
|
|
189
|
+
"""Install + enable + start the host service unit."""
|
|
190
|
+
import importlib.metadata
|
|
191
|
+
|
|
192
|
+
from alter_runtime.consent import (
|
|
193
|
+
ConsentDeclined,
|
|
194
|
+
UserDeletedArtefact,
|
|
195
|
+
enumerate_artefacts,
|
|
196
|
+
print_consent_screen,
|
|
197
|
+
prompt_consent,
|
|
198
|
+
remove_tombstone,
|
|
199
|
+
write_manifest_entry,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
platform = current_platform()
|
|
203
|
+
if platform not in ("linux", "darwin"):
|
|
204
|
+
print(
|
|
205
|
+
f"alter-runtime start is not yet supported on platform={platform!r}. "
|
|
206
|
+
"Run `alter-runtime daemon` in the foreground for debugging.",
|
|
207
|
+
file=sys.stderr,
|
|
208
|
+
)
|
|
209
|
+
return 1
|
|
210
|
+
|
|
211
|
+
# C1 - consent gate. Enumerate artefacts, render the consent screen, and
|
|
212
|
+
# wait for explicit confirmation before touching the filesystem.
|
|
213
|
+
# Skipped on --dry-run (no writes happen) and bypassed by --yes.
|
|
214
|
+
if not args.dry_run:
|
|
215
|
+
artefacts = enumerate_artefacts()
|
|
216
|
+
print_consent_screen(artefacts, uninstall_cmd="alter-runtime stop")
|
|
217
|
+
try:
|
|
218
|
+
prompt_consent(yes=args.yes)
|
|
219
|
+
except ConsentDeclined:
|
|
220
|
+
print("Install cancelled.")
|
|
221
|
+
return 1
|
|
222
|
+
else:
|
|
223
|
+
artefacts = enumerate_artefacts()
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
result = install_service(
|
|
227
|
+
enable=True,
|
|
228
|
+
dry_run=args.dry_run,
|
|
229
|
+
reinstall=getattr(args, "reinstall", False),
|
|
230
|
+
)
|
|
231
|
+
except UserDeletedArtefact as exc:
|
|
232
|
+
print(
|
|
233
|
+
f" {exc.args[0]} was previously deleted by the user.\n"
|
|
234
|
+
" ALTER will not recreate it automatically.\n"
|
|
235
|
+
" To reinstall, run: alter-runtime start --reinstall",
|
|
236
|
+
file=sys.stderr,
|
|
237
|
+
)
|
|
238
|
+
return 1
|
|
239
|
+
except FileNotFoundError as exc:
|
|
240
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
241
|
+
return 1
|
|
242
|
+
except NotImplementedError as exc:
|
|
243
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
244
|
+
return 1
|
|
245
|
+
|
|
246
|
+
if args.dry_run:
|
|
247
|
+
print("--- rendered unit ---")
|
|
248
|
+
print(result.rendered)
|
|
249
|
+
print("--- service commands ---")
|
|
250
|
+
for cmd in result.service_commands:
|
|
251
|
+
print(" ".join(cmd))
|
|
252
|
+
return 0
|
|
253
|
+
|
|
254
|
+
# C2 - post-install manifest. Write a manifest entry for every artefact
|
|
255
|
+
# that was successfully installed.
|
|
256
|
+
try:
|
|
257
|
+
version = importlib.metadata.version("alter-runtime")
|
|
258
|
+
except importlib.metadata.PackageNotFoundError:
|
|
259
|
+
version = "unknown"
|
|
260
|
+
|
|
261
|
+
for artefact in artefacts:
|
|
262
|
+
try:
|
|
263
|
+
write_manifest_entry(artefact, version=version)
|
|
264
|
+
except OSError as exc:
|
|
265
|
+
# Manifest write failure is non-fatal - the install succeeded.
|
|
266
|
+
print(f" warning: could not write manifest entry: {exc}", file=sys.stderr)
|
|
267
|
+
|
|
268
|
+
# C3 - if --reinstall was passed and succeeded, clear the tombstone so
|
|
269
|
+
# subsequent starts don't need --reinstall again.
|
|
270
|
+
if getattr(args, "reinstall", False):
|
|
271
|
+
for artefact in artefacts:
|
|
272
|
+
try:
|
|
273
|
+
remove_tombstone(artefact["path"])
|
|
274
|
+
except OSError:
|
|
275
|
+
pass
|
|
276
|
+
|
|
277
|
+
print(f"installed {result.unit_path}")
|
|
278
|
+
for cmd, exit_code in result.command_results:
|
|
279
|
+
tag = "ok" if exit_code == 0 else f"exit={exit_code}"
|
|
280
|
+
print(f" {' '.join(cmd)} [{tag}]")
|
|
281
|
+
# Non-zero exit for any service command is non-fatal - the unit file
|
|
282
|
+
# was still written, so re-running `alter-runtime start` or enabling
|
|
283
|
+
# the unit manually will pick up where we left off.
|
|
284
|
+
return 0
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def cmd_stop(_args: argparse.Namespace) -> int:
|
|
288
|
+
"""Disable + stop the host service unit."""
|
|
289
|
+
from alter_runtime.consent import _sha256_file, write_tombstone
|
|
290
|
+
from alter_runtime.service_install import (
|
|
291
|
+
launchd_plist_install_path,
|
|
292
|
+
systemd_unit_install_path,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
platform = current_platform()
|
|
296
|
+
if platform not in ("linux", "darwin"):
|
|
297
|
+
print(
|
|
298
|
+
f"alter-runtime stop is not yet supported on platform={platform!r}.",
|
|
299
|
+
file=sys.stderr,
|
|
300
|
+
)
|
|
301
|
+
return 1
|
|
302
|
+
|
|
303
|
+
# C3 - tombstone the unit file before uninstalling so that a subsequent
|
|
304
|
+
# `alter-runtime start` does not silently recreate it.
|
|
305
|
+
if platform == "linux":
|
|
306
|
+
unit_path = systemd_unit_install_path()
|
|
307
|
+
else:
|
|
308
|
+
unit_path = launchd_plist_install_path()
|
|
309
|
+
|
|
310
|
+
if unit_path.exists():
|
|
311
|
+
sha = _sha256_file(unit_path)
|
|
312
|
+
try:
|
|
313
|
+
write_tombstone(unit_path, sha256_at_deletion=sha, source="user_explicit")
|
|
314
|
+
except OSError as exc:
|
|
315
|
+
print(f" warning: could not write tombstone: {exc}", file=sys.stderr)
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
results = uninstall_service()
|
|
319
|
+
except NotImplementedError as exc:
|
|
320
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
321
|
+
return 1
|
|
322
|
+
|
|
323
|
+
for cmd, exit_code in results:
|
|
324
|
+
tag = "ok" if exit_code == 0 else f"exit={exit_code}"
|
|
325
|
+
print(f" {' '.join(cmd)} [{tag}]")
|
|
326
|
+
print("service unit removed")
|
|
327
|
+
return 0
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def cmd_status(_args: argparse.Namespace) -> int:
|
|
331
|
+
"""Report daemon state - does the socket exist? Is the session valid?"""
|
|
332
|
+
lines: list[str] = []
|
|
333
|
+
|
|
334
|
+
socket = unix_socket_path()
|
|
335
|
+
lines.append(f"unix socket: {socket}")
|
|
336
|
+
if socket.exists():
|
|
337
|
+
lines.append(" state: present")
|
|
338
|
+
else:
|
|
339
|
+
lines.append(" state: absent (daemon not running or socket path mismatched)")
|
|
340
|
+
|
|
341
|
+
kp = keypair_path()
|
|
342
|
+
lines.append(f"keypair: {kp}")
|
|
343
|
+
lines.append(f" state: {'present' if kp.exists() else 'absent'}")
|
|
344
|
+
|
|
345
|
+
session = load_session()
|
|
346
|
+
if session is None:
|
|
347
|
+
lines.append("session: none (run `alter login`)")
|
|
348
|
+
else:
|
|
349
|
+
lines.append(f"session: handle={session.handle} tier=L{session.consent_tier}")
|
|
350
|
+
lines.append(f" api: {session.api}")
|
|
351
|
+
lines.append(f" expires: {session.jwt_expires_at}")
|
|
352
|
+
|
|
353
|
+
config = load_config()
|
|
354
|
+
lines.append(f"config: {config_dir() / 'runtime.yaml'}")
|
|
355
|
+
lines.append(f" do_sse: {config.do_sse_endpoint}")
|
|
356
|
+
lines.append(f" mcp_fallback: {config.mcp_fallback_endpoint}")
|
|
357
|
+
lines.append(f" log_level: {config.log_level}")
|
|
358
|
+
|
|
359
|
+
svc = service_status()
|
|
360
|
+
lines.append(f"service unit: {svc.unit_path}")
|
|
361
|
+
lines.append(f" platform: {svc.platform}")
|
|
362
|
+
lines.append(f" installed: {svc.installed}")
|
|
363
|
+
lines.append(f" active: {svc.active} ({svc.status_line})")
|
|
364
|
+
|
|
365
|
+
print("\n".join(lines))
|
|
366
|
+
return 0
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def cmd_daemon(_args: argparse.Namespace) -> int:
|
|
370
|
+
"""Run the supervisor in the foreground."""
|
|
371
|
+
try:
|
|
372
|
+
asyncio.run(run_daemon())
|
|
373
|
+
except KeyboardInterrupt:
|
|
374
|
+
logger.info("interrupted")
|
|
375
|
+
return 130
|
|
376
|
+
return 0
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def cmd_query(args: argparse.Namespace) -> int:
|
|
380
|
+
"""Query the local cache / daemon socket for current field state.
|
|
381
|
+
|
|
382
|
+
Wave 1 skeleton: reads only the local cache file that `scripts/alter-identity.sh`
|
|
383
|
+
already maintains at ~/.cache/alter/identity.json. Wave 2 stream 2b wires a
|
|
384
|
+
live socket query.
|
|
385
|
+
"""
|
|
386
|
+
cache_file = cache_dir() / "identity.json"
|
|
387
|
+
if not cache_file.exists():
|
|
388
|
+
print("no cached identity state - run `alter-runtime daemon` or `alter-identity.sh`")
|
|
389
|
+
return 1
|
|
390
|
+
|
|
391
|
+
try:
|
|
392
|
+
data = json.loads(cache_file.read_text())
|
|
393
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
394
|
+
print(f"failed to read cache: {exc}", file=sys.stderr)
|
|
395
|
+
return 1
|
|
396
|
+
|
|
397
|
+
field = args.field
|
|
398
|
+
if field == "all":
|
|
399
|
+
print(json.dumps(data, indent=2))
|
|
400
|
+
return 0
|
|
401
|
+
|
|
402
|
+
alias = {
|
|
403
|
+
"handle": "handle",
|
|
404
|
+
"attunement": "attunement",
|
|
405
|
+
"warmth": "level",
|
|
406
|
+
"income": "income",
|
|
407
|
+
"trust_tier": "trust_tier",
|
|
408
|
+
}
|
|
409
|
+
key = alias.get(field, field)
|
|
410
|
+
value = data.get(key)
|
|
411
|
+
if value is None:
|
|
412
|
+
print(f"field '{field}' not present in local cache")
|
|
413
|
+
return 1
|
|
414
|
+
print(value)
|
|
415
|
+
return 0
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def cmd_ingest(args: argparse.Namespace) -> int:
|
|
419
|
+
"""Manually ingest a signal.
|
|
420
|
+
|
|
421
|
+
Wave 1 skeleton: validates the payload shape and prints what would be
|
|
422
|
+
sent. Wave 2 stream 2b wires the actual socket send + DO ingest path.
|
|
423
|
+
"""
|
|
424
|
+
try:
|
|
425
|
+
payload = json.loads(args.payload)
|
|
426
|
+
except json.JSONDecodeError as exc:
|
|
427
|
+
print(f"invalid --payload JSON: {exc}", file=sys.stderr)
|
|
428
|
+
return 1
|
|
429
|
+
|
|
430
|
+
session = load_session()
|
|
431
|
+
if session is None:
|
|
432
|
+
print("no session - run `alter login`", file=sys.stderr)
|
|
433
|
+
return 1
|
|
434
|
+
|
|
435
|
+
envelope = {
|
|
436
|
+
"v": "alter1",
|
|
437
|
+
"handle": session.handle,
|
|
438
|
+
"kind": args.kind,
|
|
439
|
+
"payload": payload,
|
|
440
|
+
"producer": args.producer,
|
|
441
|
+
"consent_tier": session.consent_tier,
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
print("Would ingest:")
|
|
445
|
+
print(json.dumps(envelope, indent=2))
|
|
446
|
+
print("\n(Wave 2 wires the actual send path via unix socket → daemon → DO.)")
|
|
447
|
+
return 0
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
# ---------------------------------------------------------------------------
|
|
451
|
+
# Main dispatcher
|
|
452
|
+
# ---------------------------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
COMMANDS: dict[str, object] = {
|
|
456
|
+
"init": cmd_init,
|
|
457
|
+
"start": cmd_start,
|
|
458
|
+
"stop": cmd_stop,
|
|
459
|
+
"status": cmd_status,
|
|
460
|
+
"daemon": cmd_daemon,
|
|
461
|
+
"query": cmd_query,
|
|
462
|
+
"ingest": cmd_ingest,
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
467
|
+
parser = build_parser()
|
|
468
|
+
args = parser.parse_args(argv)
|
|
469
|
+
|
|
470
|
+
logging.basicConfig(
|
|
471
|
+
level=args.log_level.upper(),
|
|
472
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
473
|
+
datefmt="%Y-%m-%dT%H:%M:%S%z",
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
handler = COMMANDS.get(args.command)
|
|
477
|
+
if handler is None:
|
|
478
|
+
parser.print_help(sys.stderr)
|
|
479
|
+
return 2
|
|
480
|
+
return handler(args) # type: ignore[operator]
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
# ---------------------------------------------------------------------------
|
|
484
|
+
# Keypair generation (stub - Wave 1 writes a placeholder; Wave 2 wires real
|
|
485
|
+
# Ed25519 generation via `cryptography` package)
|
|
486
|
+
# ---------------------------------------------------------------------------
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _generate_keypair(path: Path) -> None:
|
|
490
|
+
"""Generate a placeholder keypair file.
|
|
491
|
+
|
|
492
|
+
Wave 1 skeleton: writes a JSON scaffold without actually generating an
|
|
493
|
+
Ed25519 key. Wave 2 stream 2c will use `cryptography` to generate a
|
|
494
|
+
real keypair and bind it to the sovereign passkey root (per
|
|
495
|
+
Invisible Infrastructure P2).
|
|
496
|
+
"""
|
|
497
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
498
|
+
placeholder = {
|
|
499
|
+
"version": 1,
|
|
500
|
+
"algorithm": "ed25519",
|
|
501
|
+
"generated_at": None,
|
|
502
|
+
"public_key_b64": None,
|
|
503
|
+
"private_key_b64": None,
|
|
504
|
+
"_notice": (
|
|
505
|
+
"Wave 1 scaffold - real keypair generation lands in Wave 2 stream 2c "
|
|
506
|
+
"with the cryptography package and sovereign passkey binding."
|
|
507
|
+
),
|
|
508
|
+
}
|
|
509
|
+
path.write_text(json.dumps(placeholder, indent=2))
|
|
510
|
+
try:
|
|
511
|
+
path.chmod(0o600)
|
|
512
|
+
except (OSError, NotImplementedError):
|
|
513
|
+
pass
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
if __name__ == "__main__":
|
|
517
|
+
raise SystemExit(main())
|
|
File without changes
|