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
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Append-only read ledger for Atlas substrate reads.
|
|
2
|
+
|
|
3
|
+
Every adapter read (or dry-run) emits a ``LedgerEntry`` to the JSONL file at
|
|
4
|
+
``~/.local/state/alter/atlas/reads.jsonl``. The ledger records *what was read,
|
|
5
|
+
when, under which consent grant, and the content-free hash of the resulting
|
|
6
|
+
signature*. It is the local surface the user can inspect with ``alter atlas
|
|
7
|
+
show`` and the source the Mirror renderer cites for observation timestamps.
|
|
8
|
+
|
|
9
|
+
Honesty clause - this ledger is a SOFT SIGNAL, not a cryptographic audit.
|
|
10
|
+
|
|
11
|
+
Per substrate spec §4.3: the egress-capable parent daemon writes this ledger.
|
|
12
|
+
When the parent daemon is the attacker, the ledger is an honest-witness weakness
|
|
13
|
+
- the daemon can simply omit or rewrite entries. Meaningful external auditing
|
|
14
|
+
arrives when ``alter-ebpf`` (Patent M) ships on-user-device and can witness
|
|
15
|
+
``bprm_check_security`` + network syscalls sourced from the daemon UID from
|
|
16
|
+
outside the daemon's control boundary.
|
|
17
|
+
|
|
18
|
+
Until then the ledger is "tell me when I'm lying" telemetry - useful for
|
|
19
|
+
honest operation and revoke-and-shred bookkeeping, NOT a tamper-evident proof.
|
|
20
|
+
Do not describe it as audit-grade, cryptographic, or anti-forgery anywhere.
|
|
21
|
+
|
|
22
|
+
The ledger also supports per-stream shred: revoking a stream (per D-CGT1
|
|
23
|
+
revocability) rewrites the JSONL with stream entries removed. This mirrors
|
|
24
|
+
the revoke-and-shred behaviour described in spec §4.4.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import tempfile
|
|
32
|
+
from dataclasses import asdict, dataclass
|
|
33
|
+
from datetime import datetime, timezone
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Iterator, Literal
|
|
36
|
+
|
|
37
|
+
from alter_runtime.atlas.schema import ProvenanceClass, SubstrateStream, utcnow
|
|
38
|
+
|
|
39
|
+
LedgerAction = Literal["dry_run", "read", "shredded"]
|
|
40
|
+
|
|
41
|
+
DEFAULT_LEDGER_PATH = Path.home() / ".local" / "state" / "alter" / "atlas" / "reads.jsonl"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class LedgerEntry:
|
|
46
|
+
"""One row of the read ledger.
|
|
47
|
+
|
|
48
|
+
Fields are named for JSONL stability - bumps add fields, never rename.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
action: LedgerAction
|
|
52
|
+
stream: SubstrateStream
|
|
53
|
+
provenance: ProvenanceClass
|
|
54
|
+
extraction_version: int
|
|
55
|
+
at: datetime
|
|
56
|
+
consent_grant_id: str | None
|
|
57
|
+
manifest_hash: str | None
|
|
58
|
+
signature_hash: str | None
|
|
59
|
+
bytes_read: int
|
|
60
|
+
note: str = ""
|
|
61
|
+
schema_version: int = 1
|
|
62
|
+
|
|
63
|
+
def to_json(self) -> str:
|
|
64
|
+
payload = asdict(self)
|
|
65
|
+
payload["stream"] = self.stream.value
|
|
66
|
+
payload["provenance"] = self.provenance.value
|
|
67
|
+
payload["at"] = self.at.astimezone(timezone.utc).isoformat()
|
|
68
|
+
return json.dumps(payload, separators=(",", ":"), sort_keys=True)
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def from_json(cls, line: str) -> "LedgerEntry":
|
|
72
|
+
raw = json.loads(line)
|
|
73
|
+
return cls(
|
|
74
|
+
action=raw["action"],
|
|
75
|
+
stream=SubstrateStream(raw["stream"]),
|
|
76
|
+
provenance=ProvenanceClass(raw["provenance"]),
|
|
77
|
+
extraction_version=int(raw["extraction_version"]),
|
|
78
|
+
at=datetime.fromisoformat(raw["at"]),
|
|
79
|
+
consent_grant_id=raw.get("consent_grant_id"),
|
|
80
|
+
manifest_hash=raw.get("manifest_hash"),
|
|
81
|
+
signature_hash=raw.get("signature_hash"),
|
|
82
|
+
bytes_read=int(raw.get("bytes_read", 0)),
|
|
83
|
+
note=raw.get("note", ""),
|
|
84
|
+
schema_version=int(raw.get("schema_version", 1)),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Ledger:
|
|
89
|
+
"""Append-only JSONL read ledger.
|
|
90
|
+
|
|
91
|
+
Soft-signal, local-only. See module docstring for the honesty clause.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(self, path: Path | None = None) -> None:
|
|
95
|
+
self.path = path or DEFAULT_LEDGER_PATH
|
|
96
|
+
|
|
97
|
+
def _ensure_parent(self) -> None:
|
|
98
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
|
|
100
|
+
def append(self, entry: LedgerEntry) -> None:
|
|
101
|
+
"""Atomically append one entry.
|
|
102
|
+
|
|
103
|
+
POSIX guarantees ``write()`` of <= PIPE_BUF bytes with ``O_APPEND`` is
|
|
104
|
+
atomic against concurrent writers. A JSONL row is well below PIPE_BUF
|
|
105
|
+
(4096 on Linux) for these fixed-size fields, so we rely on that
|
|
106
|
+
without explicit locking.
|
|
107
|
+
"""
|
|
108
|
+
self._ensure_parent()
|
|
109
|
+
fd = os.open(self.path, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
|
|
110
|
+
try:
|
|
111
|
+
os.write(fd, (entry.to_json() + "\n").encode("utf-8"))
|
|
112
|
+
finally:
|
|
113
|
+
os.close(fd)
|
|
114
|
+
|
|
115
|
+
def read_all(self) -> Iterator[LedgerEntry]:
|
|
116
|
+
if not self.path.exists():
|
|
117
|
+
return
|
|
118
|
+
with self.path.open("r", encoding="utf-8") as fh:
|
|
119
|
+
for line in fh:
|
|
120
|
+
line = line.strip()
|
|
121
|
+
if not line:
|
|
122
|
+
continue
|
|
123
|
+
yield LedgerEntry.from_json(line)
|
|
124
|
+
|
|
125
|
+
def read_stream(self, stream: SubstrateStream) -> list[LedgerEntry]:
|
|
126
|
+
return [e for e in self.read_all() if e.stream == stream]
|
|
127
|
+
|
|
128
|
+
def latest_for_stream(self, stream: SubstrateStream) -> LedgerEntry | None:
|
|
129
|
+
latest: LedgerEntry | None = None
|
|
130
|
+
for entry in self.read_stream(stream):
|
|
131
|
+
if latest is None or entry.at > latest.at:
|
|
132
|
+
latest = entry
|
|
133
|
+
return latest
|
|
134
|
+
|
|
135
|
+
def shred_stream(self, stream: SubstrateStream, reason: str = "user_revoke") -> int:
|
|
136
|
+
"""Remove all entries for ``stream`` and append a single shred marker.
|
|
137
|
+
|
|
138
|
+
Returns the number of entries removed. The marker entry preserves
|
|
139
|
+
provenance that a shred happened (for dashboard display) without
|
|
140
|
+
retaining the shredded content hashes.
|
|
141
|
+
"""
|
|
142
|
+
if not self.path.exists():
|
|
143
|
+
return 0
|
|
144
|
+
kept: list[str] = []
|
|
145
|
+
removed = 0
|
|
146
|
+
with self.path.open("r", encoding="utf-8") as fh:
|
|
147
|
+
for line in fh:
|
|
148
|
+
stripped = line.strip()
|
|
149
|
+
if not stripped:
|
|
150
|
+
continue
|
|
151
|
+
try:
|
|
152
|
+
entry = LedgerEntry.from_json(stripped)
|
|
153
|
+
except (KeyError, ValueError):
|
|
154
|
+
kept.append(line.rstrip("\n"))
|
|
155
|
+
continue
|
|
156
|
+
if entry.stream == stream:
|
|
157
|
+
removed += 1
|
|
158
|
+
continue
|
|
159
|
+
kept.append(stripped)
|
|
160
|
+
|
|
161
|
+
self._ensure_parent()
|
|
162
|
+
tmp_fd, tmp_path = tempfile.mkstemp(
|
|
163
|
+
prefix="reads.", suffix=".jsonl.tmp", dir=self.path.parent
|
|
164
|
+
)
|
|
165
|
+
try:
|
|
166
|
+
with os.fdopen(tmp_fd, "w", encoding="utf-8") as tmp:
|
|
167
|
+
for row in kept:
|
|
168
|
+
tmp.write(row + "\n")
|
|
169
|
+
marker = LedgerEntry(
|
|
170
|
+
action="shredded",
|
|
171
|
+
stream=stream,
|
|
172
|
+
provenance=ProvenanceClass.ACTIVE,
|
|
173
|
+
extraction_version=0,
|
|
174
|
+
at=utcnow(),
|
|
175
|
+
consent_grant_id=None,
|
|
176
|
+
manifest_hash=None,
|
|
177
|
+
signature_hash=None,
|
|
178
|
+
bytes_read=0,
|
|
179
|
+
note=reason,
|
|
180
|
+
)
|
|
181
|
+
tmp.write(marker.to_json() + "\n")
|
|
182
|
+
os.chmod(tmp_path, 0o600)
|
|
183
|
+
os.replace(tmp_path, self.path)
|
|
184
|
+
except Exception:
|
|
185
|
+
with suppress_errors():
|
|
186
|
+
os.unlink(tmp_path)
|
|
187
|
+
raise
|
|
188
|
+
return removed
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class suppress_errors:
|
|
192
|
+
def __enter__(self) -> "suppress_errors":
|
|
193
|
+
return self
|
|
194
|
+
|
|
195
|
+
def __exit__(self, exc_type, exc, tb) -> bool:
|
|
196
|
+
return True
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Observation renderer - local-only citation list from one or more Signatures.
|
|
2
|
+
|
|
3
|
+
The observation surface is NOT a dashboard, NOT a reading in the tarot sense,
|
|
4
|
+
NOT a therapy session. It is a citation list: specific observations tied to
|
|
5
|
+
specific counts, rendered on-device with zero network round-trip.
|
|
6
|
+
|
|
7
|
+
Tone discipline - OBSERVATION, never INTERPRETATION (spec §6).
|
|
8
|
+
|
|
9
|
+
The cringe kill-zone is any line where Alter names a pattern FOR the user,
|
|
10
|
+
reads meaning into content, or delivers an aphorism. PKM-native users have
|
|
11
|
+
finely-tuned detectors for exactly this move ("you've built a slip-box",
|
|
12
|
+
"you don't want Spanish, you want the version of you that speaks Spanish").
|
|
13
|
+
|
|
14
|
+
The renderer's voice: numbers, timestamps, paths - then stop. User supplies
|
|
15
|
+
meaning. Marcus Aurelius plus an engineer.
|
|
16
|
+
|
|
17
|
+
This module exposes:
|
|
18
|
+
|
|
19
|
+
- ``Observation`` - one line of output carried alongside its slot citations
|
|
20
|
+
(so the user can trace each line back to the exact coefficient).
|
|
21
|
+
- ``render_observations(...)`` - compose header + body from a list of observations.
|
|
22
|
+
- ``lint_observation_line(line)`` - soft-lint for known interpretive phrases.
|
|
23
|
+
Returns a list of reasons the line is likely regressive (adapter-side tests
|
|
24
|
+
can raise on non-empty; CI honesty-clause audit uses the same list).
|
|
25
|
+
|
|
26
|
+
Adapters own the observation templates (they know what their coefficients
|
|
27
|
+
mean); this module only enforces rendering shape + the lint floor.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
from datetime import datetime, timezone
|
|
34
|
+
from typing import Sequence
|
|
35
|
+
|
|
36
|
+
from alter_runtime.atlas.schema import Signature
|
|
37
|
+
|
|
38
|
+
OBSERVATION_PREFIX = "- " # em-dash + space, per spec §6 example
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class Observation:
|
|
43
|
+
"""One rendered observation line.
|
|
44
|
+
|
|
45
|
+
``citations`` lists coefficient names from the signature this line reads
|
|
46
|
+
- used for dashboard trace-back ("this line came from which numbers?")
|
|
47
|
+
and for the post-render smoke-test that every observation can be cited.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
line: str
|
|
51
|
+
citations: tuple[str, ...] = ()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
FORBIDDEN_PHRASES: tuple[str, ...] = (
|
|
55
|
+
"you've built",
|
|
56
|
+
"you have built",
|
|
57
|
+
"you're a",
|
|
58
|
+
"you are a",
|
|
59
|
+
"what you really want",
|
|
60
|
+
"what you truly want",
|
|
61
|
+
"the version of you",
|
|
62
|
+
"deep down",
|
|
63
|
+
"slip-box",
|
|
64
|
+
"zettelkasten",
|
|
65
|
+
"second brain",
|
|
66
|
+
"night owl",
|
|
67
|
+
"early bird",
|
|
68
|
+
"ghost ambitions",
|
|
69
|
+
"shadow self",
|
|
70
|
+
"your archetype",
|
|
71
|
+
"your type",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def lint_observation_line(line: str) -> list[str]:
|
|
76
|
+
"""Return reasons this line likely violates §6 observation-only discipline.
|
|
77
|
+
|
|
78
|
+
This is a soft lint - it does not catch every regression (novel
|
|
79
|
+
interpretive phrasings will slip past) but it catches the spec-named
|
|
80
|
+
kill-zone phrases and common second-person verdict openers. Adapter
|
|
81
|
+
tests should assert this returns ``[]`` for every shipped template.
|
|
82
|
+
"""
|
|
83
|
+
reasons: list[str] = []
|
|
84
|
+
lower = line.lower()
|
|
85
|
+
for phrase in FORBIDDEN_PHRASES:
|
|
86
|
+
if phrase in lower:
|
|
87
|
+
reasons.append(f"forbidden phrase: {phrase!r}")
|
|
88
|
+
stripped = lower.lstrip("- -")
|
|
89
|
+
for opener in ("you've ", "you are ", "you're "):
|
|
90
|
+
if stripped.startswith(opener):
|
|
91
|
+
reasons.append(
|
|
92
|
+
f"second-person verdict opener: {opener!r} - prefer counts/timestamps/paths"
|
|
93
|
+
)
|
|
94
|
+
break
|
|
95
|
+
return reasons
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def render_observations(
|
|
99
|
+
handle: str,
|
|
100
|
+
signatures: Sequence[Signature],
|
|
101
|
+
observations: Sequence[Observation],
|
|
102
|
+
rendered_at: datetime | None = None,
|
|
103
|
+
) -> str:
|
|
104
|
+
"""Compose the observation text.
|
|
105
|
+
|
|
106
|
+
Output shape (spec §6 example):
|
|
107
|
+
|
|
108
|
+
Atlas / ~blake / 2026-04-17
|
|
109
|
+
|
|
110
|
+
- 47 of your 83 commits this year landed between 23:00 and 02:00
|
|
111
|
+
- You commit on Tuesday (19) and Sunday (14) more than any other day
|
|
112
|
+
- Your commit verbs: feat 31 · fix 22 · refactor 14 · chore 9 · docs 7
|
|
113
|
+
|
|
114
|
+
The header uses the ``~handle`` form per D-ID1–ID8 namespace convention.
|
|
115
|
+
Observations render in the order supplied - adapters are expected to
|
|
116
|
+
pre-order by most to least surprising count.
|
|
117
|
+
|
|
118
|
+
This function does not enforce ``lint_observation_line`` - that is the
|
|
119
|
+
adapter's responsibility in its unit test. Render-time failures in prod
|
|
120
|
+
would block the observation surface for a user whose machine produced an
|
|
121
|
+
edge-case template; soft-lint at test time, not runtime.
|
|
122
|
+
"""
|
|
123
|
+
handle_display = handle if handle.startswith("~") else f"~{handle}"
|
|
124
|
+
when = rendered_at or datetime.now(timezone.utc)
|
|
125
|
+
date_line = when.strftime("%Y-%m-%d")
|
|
126
|
+
|
|
127
|
+
streams = "·".join(sorted({s.stream.value for s in signatures}))
|
|
128
|
+
# Streams line only renders when more than one stream is cited - keeps
|
|
129
|
+
# single-adapter output (T0 git-only) matching the spec example verbatim.
|
|
130
|
+
header_lines = [f"Atlas / {handle_display} / {date_line}"]
|
|
131
|
+
if streams and len(signatures) > 1:
|
|
132
|
+
header_lines.append(f"streams: {streams}")
|
|
133
|
+
|
|
134
|
+
body_lines = [f"{OBSERVATION_PREFIX}{obs.line}" for obs in observations]
|
|
135
|
+
|
|
136
|
+
return "\n".join(header_lines + [""] + body_lines) + "\n"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Atlas signature schema - the derivative vector shape.
|
|
2
|
+
|
|
3
|
+
A Signature is the output of one adapter reading one substrate once. It carries:
|
|
4
|
+
* a 128-dim coefficient vector (padded with NaN for adapter-specific shorter
|
|
5
|
+
outputs - each adapter documents which slots it populates)
|
|
6
|
+
* provenance metadata (which stream, which extraction version, when)
|
|
7
|
+
* a content-free hash for server-crossing under depth-fork consent
|
|
8
|
+
|
|
9
|
+
Per spec §2 honesty clause: the hash is NOT a pseudonymisation guarantee. It is
|
|
10
|
+
re-identifiable via membership inference at scale. Protection is legal (per-stream
|
|
11
|
+
Art-6(1)(a) consent + audit) + operational (no-cross-join policy) - not
|
|
12
|
+
architectural.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import enum
|
|
18
|
+
import hashlib
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from datetime import datetime, timezone
|
|
21
|
+
|
|
22
|
+
SIGNATURE_DIM: int = 128
|
|
23
|
+
SIGNATURE_SCHEMA_VERSION: int = 1
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SubstrateStream(str, enum.Enum):
|
|
27
|
+
"""Enumeration of shipped + parked substrate streams.
|
|
28
|
+
|
|
29
|
+
Each stream is gated independently for consent per D-IaI-1.5.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
GIT = "git"
|
|
33
|
+
SHELL = "shell"
|
|
34
|
+
VAULT = "vault"
|
|
35
|
+
WINDOWS = "windows"
|
|
36
|
+
DOWNLOADS = "downloads"
|
|
37
|
+
CALENDAR = "calendar"
|
|
38
|
+
NOTES = "notes"
|
|
39
|
+
RECENTS = "recents"
|
|
40
|
+
AESTHETIC = "aesthetic"
|
|
41
|
+
MUSIC = "music" # Phase 2 - Drew-gated E-tier firewall DR required
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ProvenanceClass(str, enum.Enum):
|
|
45
|
+
"""D-IaI-1.2 provenance classes - gate consent by (stream × provenance)."""
|
|
46
|
+
|
|
47
|
+
ACTIVE = "active" # user-initiated read (`alter atlas read`)
|
|
48
|
+
PASSIVE_LOCAL = "passive_local" # daemon subscription, LOCAL-ONLY
|
|
49
|
+
PASSIVE_AGGREGATE = "passive_aggregate" # k>=1000 population observation
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class SignatureCoefficient:
|
|
54
|
+
"""One named slot in the 128-dim vector.
|
|
55
|
+
|
|
56
|
+
Adapters declare which coefficients they populate; unpopulated slots stay
|
|
57
|
+
NaN. The name is canonical across versions - schema bumps add coefficients,
|
|
58
|
+
never rename.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
slot: int
|
|
62
|
+
name: str
|
|
63
|
+
value: float
|
|
64
|
+
unit: str | None = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class Signature:
|
|
69
|
+
"""One adapter's read of one substrate at one moment."""
|
|
70
|
+
|
|
71
|
+
stream: SubstrateStream
|
|
72
|
+
provenance: ProvenanceClass
|
|
73
|
+
extracted_at: datetime
|
|
74
|
+
extraction_version: int
|
|
75
|
+
coefficients: tuple[SignatureCoefficient, ...]
|
|
76
|
+
schema_version: int = SIGNATURE_SCHEMA_VERSION
|
|
77
|
+
|
|
78
|
+
def to_vector(self) -> list[float]:
|
|
79
|
+
"""Dense 128-dim vector with NaN for unpopulated slots."""
|
|
80
|
+
vec = [float("nan")] * SIGNATURE_DIM
|
|
81
|
+
for coeff in self.coefficients:
|
|
82
|
+
if 0 <= coeff.slot < SIGNATURE_DIM:
|
|
83
|
+
vec[coeff.slot] = coeff.value
|
|
84
|
+
return vec
|
|
85
|
+
|
|
86
|
+
def content_free_hash(self) -> str:
|
|
87
|
+
"""BLAKE3-ish stable hash over (stream, version, sorted coefficients).
|
|
88
|
+
|
|
89
|
+
Uses sha256 for stdlib availability; BLAKE3 is a post-launch upgrade.
|
|
90
|
+
This hash is re-identifiable at scale (spec §2 honesty clause) - do
|
|
91
|
+
not treat as pseudonymisation.
|
|
92
|
+
"""
|
|
93
|
+
h = hashlib.sha256()
|
|
94
|
+
h.update(self.stream.value.encode())
|
|
95
|
+
h.update(self.schema_version.to_bytes(4, "big"))
|
|
96
|
+
h.update(self.extraction_version.to_bytes(4, "big"))
|
|
97
|
+
for coeff in sorted(self.coefficients, key=lambda c: c.slot):
|
|
98
|
+
h.update(coeff.slot.to_bytes(2, "big"))
|
|
99
|
+
h.update(coeff.name.encode())
|
|
100
|
+
# Fixed-width float repr so hash is stable across Python versions.
|
|
101
|
+
h.update(f"{coeff.value:.9e}".encode())
|
|
102
|
+
return h.hexdigest()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def utcnow() -> datetime:
|
|
106
|
+
return datetime.now(timezone.utc)
|