brainiac-cli 0.16.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.
- brain/__init__.py +39 -0
- brain/__main__.py +14 -0
- brain/_assets/AGENTS.md +564 -0
- brain/_assets/overlay/template/brand/brand-guide.md +24 -0
- brain/_assets/overlay/template/keywords/glossary.md +15 -0
- brain/_assets/overlay/template/people/roster.md +14 -0
- brain/_assets/overlay/template/voice/voice-profile.md +34 -0
- brain/_assets/routines/manifest.json +257 -0
- brain/_assets/scripts/brain-brief-mac.plist +59 -0
- brain/_assets/scripts/brain-brief.sh +74 -0
- brain/_assets/scripts/brain-synthesis-mac.plist +48 -0
- brain/_assets/scripts/brain-synthesis.sh +214 -0
- brain/_assets/scripts/install-brief-mac.sh +152 -0
- brain/_assets/scripts/install-brief-windows.ps1 +97 -0
- brain/_assets/scripts/register_tasks.py +386 -0
- brain/_assets/templates/company.md +21 -0
- brain/_assets/templates/concept.md +27 -0
- brain/_assets/templates/daily.md +20 -0
- brain/_assets/templates/decision.md +42 -0
- brain/_assets/templates/meeting.md +33 -0
- brain/_assets/templates/person.md +20 -0
- brain/_assets/templates/project.md +23 -0
- brain/_assets/templates/state-moc.md +40 -0
- brain/_version.py +12 -0
- brain/anchor.py +121 -0
- brain/audit.py +422 -0
- brain/backup.py +210 -0
- brain/brief.py +417 -0
- brain/capture.py +117 -0
- brain/chunk.py +249 -0
- brain/classification.py +134 -0
- brain/cli.py +1906 -0
- brain/config.py +368 -0
- brain/connect.py +362 -0
- brain/context.py +108 -0
- brain/core.py +3018 -0
- brain/doctor.py +1161 -0
- brain/egress.py +148 -0
- brain/embed.py +857 -0
- brain/encryption.py +217 -0
- brain/frontmatter.py +102 -0
- brain/golden_probe.py +678 -0
- brain/graph.py +369 -0
- brain/graphify.py +352 -0
- brain/index.py +1576 -0
- brain/ingest/__init__.py +19 -0
- brain/ingest/handlers/__init__.py +43 -0
- brain/ingest/handlers/base.py +95 -0
- brain/ingest/handlers/docx.py +78 -0
- brain/ingest/handlers/email.py +228 -0
- brain/ingest/handlers/html.py +142 -0
- brain/ingest/handlers/image.py +91 -0
- brain/ingest/handlers/pdf.py +99 -0
- brain/ingest/handlers/pptx.py +69 -0
- brain/ingest/handlers/tables.py +41 -0
- brain/ingest/handlers/text.py +43 -0
- brain/ingest/handlers/xlsx.py +100 -0
- brain/ingest/handlers/zip.py +163 -0
- brain/ingest/pipeline.py +839 -0
- brain/ingest/transcript.py +158 -0
- brain/init.py +870 -0
- brain/maintenance.py +2266 -0
- brain/mcp_adapter.py +217 -0
- brain/multihop.py +232 -0
- brain/notes.py +195 -0
- brain/overlay.py +183 -0
- brain/projection.py +79 -0
- brain/rerank.py +425 -0
- brain/snapshot.py +231 -0
- brain/update.py +743 -0
- brain/vectors.py +225 -0
- brainiac_cli-0.16.0.dist-info/METADATA +306 -0
- brainiac_cli-0.16.0.dist-info/RECORD +77 -0
- brainiac_cli-0.16.0.dist-info/WHEEL +5 -0
- brainiac_cli-0.16.0.dist-info/entry_points.txt +4 -0
- brainiac_cli-0.16.0.dist-info/licenses/LICENSE +202 -0
- brainiac_cli-0.16.0.dist-info/top_level.txt +1 -0
brain/encryption.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""DORMANT conditional at-rest encryption module (SEC-02).
|
|
2
|
+
|
|
3
|
+
Design v5 §2 walked back "encrypt the sensitive zone = load-bearing": on a
|
|
4
|
+
single-user machine with FileVault/BitLocker on, broad app-level encryption
|
|
5
|
+
above FDE is over-engineering. So this module exists but is **OFF by default**.
|
|
6
|
+
It switches on ONLY for a documented trigger (the flip-list), the most important
|
|
7
|
+
being an **off-device backup/sync** of a decrypted-readable copy — which is the
|
|
8
|
+
one place encryption genuinely matters (used by brain.backup, SEC-03).
|
|
9
|
+
|
|
10
|
+
Primitive: AES-256-GCM (authenticated). Key custody mirrors the audit chain —
|
|
11
|
+
OS secret store / injected env, **NO file fallback** — and uses a *separate* key
|
|
12
|
+
from the signing key (encryption != signing). Fail-closed: if encryption is
|
|
13
|
+
requested but no key resolves, the operation refuses rather than writing
|
|
14
|
+
plaintext.
|
|
15
|
+
|
|
16
|
+
from_env: $BRAIN_ENCRYPTION_KEY (base64 of 32 raw bytes)
|
|
17
|
+
from_cmd: $BRAIN_ENCRYPTION_KEY_CMD (stdout = base64 key)
|
|
18
|
+
keychain: macOS `security` / Windows Credential Manager (keyring)
|
|
19
|
+
(NO bare-file fallback — by design.)
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import base64
|
|
24
|
+
import os
|
|
25
|
+
import shutil
|
|
26
|
+
import subprocess
|
|
27
|
+
import sys
|
|
28
|
+
from typing import Optional
|
|
29
|
+
|
|
30
|
+
MAGIC = b"BRAINENCv1" # token header so a wrong-format blob fails loudly
|
|
31
|
+
NONCE_LEN = 12 # AES-GCM standard nonce
|
|
32
|
+
KEY_LEN = 32 # AES-256
|
|
33
|
+
KEYCHAIN_SERVICE_DEFAULT = "profile-a-brain-encryption-key"
|
|
34
|
+
|
|
35
|
+
# The flip-list (design v5 §2). Encryption is OFF unless one of these holds.
|
|
36
|
+
FLIP_LIST: tuple[str, ...] = (
|
|
37
|
+
"off-device backup or cloud sync of a decrypted-readable copy "
|
|
38
|
+
"(iCloud/OneDrive/Dropbox/network share)",
|
|
39
|
+
"genuinely regulated data (PCI mandated; MNPI/PII under a flagged regime)",
|
|
40
|
+
"shared / multi-user machine (OS permissions are the weak point)",
|
|
41
|
+
"a cyber team contractually mandates app-level encryption at rest",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class EncryptionError(RuntimeError):
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class EncryptionKeyUnavailable(EncryptionError):
|
|
50
|
+
"""No encryption key resolved from any OS-secret-store path. Fail closed."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class EncryptionDisabled(EncryptionError):
|
|
54
|
+
"""Encryption is OFF (default) and was invoked without an explicit override."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def is_enabled() -> bool:
|
|
58
|
+
"""True iff at-rest app-encryption has been flipped ON for this install.
|
|
59
|
+
|
|
60
|
+
OFF by default. Enable with ``BRAIN_ENCRYPTION=on`` (or 1/true/yes). The
|
|
61
|
+
backup path may still FORCE encryption regardless of this flag, because an
|
|
62
|
+
off-device backup is the one place encryption matters even when the at-rest
|
|
63
|
+
baseline stays FDE-only.
|
|
64
|
+
"""
|
|
65
|
+
return os.environ.get("BRAIN_ENCRYPTION", "").strip().lower() in {"on", "1", "true", "yes"}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _require_crypto():
|
|
69
|
+
try:
|
|
70
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM # noqa: F401
|
|
71
|
+
except ImportError as exc: # pragma: no cover
|
|
72
|
+
raise EncryptionError(
|
|
73
|
+
"'cryptography' is required for the encryption module "
|
|
74
|
+
"(pip install 'brainiac-cli[audit]')"
|
|
75
|
+
) from exc
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _key_from_keychain(service: str, account: str) -> Optional[bytes]:
|
|
79
|
+
if sys.platform != "darwin" or not shutil.which("security"):
|
|
80
|
+
return None
|
|
81
|
+
try:
|
|
82
|
+
out = subprocess.run(
|
|
83
|
+
["security", "find-generic-password", "-s", service, "-a", account, "-w"],
|
|
84
|
+
capture_output=True, timeout=10,
|
|
85
|
+
)
|
|
86
|
+
except (OSError, subprocess.SubprocessError):
|
|
87
|
+
return None
|
|
88
|
+
if out.returncode != 0:
|
|
89
|
+
return None
|
|
90
|
+
return out.stdout.strip()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _key_from_credential_manager(service: str, account: str) -> Optional[bytes]:
|
|
94
|
+
if not sys.platform.startswith("win"):
|
|
95
|
+
return None
|
|
96
|
+
try:
|
|
97
|
+
import keyring # type: ignore
|
|
98
|
+
|
|
99
|
+
secret = keyring.get_password(service, account)
|
|
100
|
+
return secret.encode("utf-8") if secret else None
|
|
101
|
+
except Exception:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _decode_key(raw: bytes, source: str) -> bytes:
|
|
106
|
+
"""Accept a base64-encoded 32-byte key (or raw 32 bytes). Fail closed on a
|
|
107
|
+
wrong-length key — never derive/pad silently."""
|
|
108
|
+
raw = raw.strip()
|
|
109
|
+
if len(raw) == KEY_LEN:
|
|
110
|
+
return raw
|
|
111
|
+
try:
|
|
112
|
+
decoded = base64.b64decode(raw, validate=True)
|
|
113
|
+
except Exception as exc:
|
|
114
|
+
raise EncryptionKeyUnavailable(
|
|
115
|
+
f"encryption key [{source}] is not base64 nor {KEY_LEN} raw bytes: {exc}"
|
|
116
|
+
) from exc
|
|
117
|
+
if len(decoded) != KEY_LEN:
|
|
118
|
+
raise EncryptionKeyUnavailable(
|
|
119
|
+
f"encryption key [{source}] decodes to {len(decoded)} bytes; need {KEY_LEN} (AES-256)"
|
|
120
|
+
)
|
|
121
|
+
return decoded
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def resolve_encryption_key() -> tuple[bytes, str]:
|
|
125
|
+
"""Resolve the 32-byte AES key from the OS secret store / env. Fail closed.
|
|
126
|
+
|
|
127
|
+
Returns (key_bytes, source_label). Raises EncryptionKeyUnavailable if nothing
|
|
128
|
+
resolves — there is intentionally NO bare-key-file fallback.
|
|
129
|
+
"""
|
|
130
|
+
service = os.environ.get("BRAIN_ENCRYPTION_KEYCHAIN_SERVICE", KEYCHAIN_SERVICE_DEFAULT)
|
|
131
|
+
account = (
|
|
132
|
+
os.environ.get("BRAIN_ENCRYPTION_KEYCHAIN_ACCOUNT")
|
|
133
|
+
or os.environ.get("USER") or os.environ.get("USERNAME") or "default"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
env_key = os.environ.get("BRAIN_ENCRYPTION_KEY")
|
|
137
|
+
if env_key:
|
|
138
|
+
return _decode_key(env_key.encode("utf-8"), "env:BRAIN_ENCRYPTION_KEY"), "env:BRAIN_ENCRYPTION_KEY"
|
|
139
|
+
|
|
140
|
+
cmd = os.environ.get("BRAIN_ENCRYPTION_KEY_CMD")
|
|
141
|
+
if cmd:
|
|
142
|
+
try:
|
|
143
|
+
# `cmd` is an OPERATOR-controlled env var (BRAIN_ENCRYPTION_KEY_CMD),
|
|
144
|
+
# not untrusted input -- mirrors audit._pem_from_cmd's BRAIN_AUDIT_
|
|
145
|
+
# KEY_CMD rationale. shell=True is intentional so custody backends
|
|
146
|
+
# can use a pipeline (e.g. `age -d -i id key.pem.age`); anyone able
|
|
147
|
+
# to set this env var already has code execution, so shell=True adds
|
|
148
|
+
# no attack surface. nosec B602 - see docs/SECURITY_NOTES.md.
|
|
149
|
+
out = subprocess.run(cmd, shell=True, capture_output=True, timeout=20) # noqa: S602
|
|
150
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
151
|
+
raise EncryptionKeyUnavailable(f"BRAIN_ENCRYPTION_KEY_CMD failed: {exc}") from exc
|
|
152
|
+
if out.returncode != 0 or not out.stdout.strip():
|
|
153
|
+
raise EncryptionKeyUnavailable(f"BRAIN_ENCRYPTION_KEY_CMD failed (exit {out.returncode})")
|
|
154
|
+
return _decode_key(out.stdout, "env:BRAIN_ENCRYPTION_KEY_CMD"), "env:BRAIN_ENCRYPTION_KEY_CMD"
|
|
155
|
+
|
|
156
|
+
kc = _key_from_keychain(service, account)
|
|
157
|
+
if kc:
|
|
158
|
+
return _decode_key(kc, f"keychain:{service}"), f"keychain:{service}"
|
|
159
|
+
|
|
160
|
+
cm = _key_from_credential_manager(service, account)
|
|
161
|
+
if cm:
|
|
162
|
+
return _decode_key(cm, f"credential-manager:{service}"), f"credential-manager:{service}"
|
|
163
|
+
|
|
164
|
+
raise EncryptionKeyUnavailable(
|
|
165
|
+
"No encryption key resolved. Tried, in order: $BRAIN_ENCRYPTION_KEY, "
|
|
166
|
+
"$BRAIN_ENCRYPTION_KEY_CMD, macOS Keychain, Windows Credential Manager. "
|
|
167
|
+
"There is NO file fallback by design — encryption fails closed. Store a "
|
|
168
|
+
"base64 32-byte key in the OS secret store (or inject it for unattended runs)."
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def generate_key_b64() -> str:
|
|
173
|
+
"""Mint a fresh base64-encoded AES-256 key (for setup/tests)."""
|
|
174
|
+
return base64.b64encode(os.urandom(KEY_LEN)).decode("ascii")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def encrypt_bytes(data: bytes, *, force: bool = False) -> bytes:
|
|
178
|
+
"""Encrypt ``data`` to a self-describing token (MAGIC || nonce || ciphertext).
|
|
179
|
+
|
|
180
|
+
Refuses unless encryption is enabled (``is_enabled()``) OR ``force=True`` (the
|
|
181
|
+
backup path forces it). Fails closed if no key resolves.
|
|
182
|
+
"""
|
|
183
|
+
if not force and not is_enabled():
|
|
184
|
+
raise EncryptionDisabled(
|
|
185
|
+
"at-rest encryption is OFF (default). Set BRAIN_ENCRYPTION=on to enable, "
|
|
186
|
+
"or call with force=True (the off-device backup path). Flip-list: "
|
|
187
|
+
+ "; ".join(FLIP_LIST)
|
|
188
|
+
)
|
|
189
|
+
_require_crypto()
|
|
190
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
191
|
+
|
|
192
|
+
key, _src = resolve_encryption_key()
|
|
193
|
+
nonce = os.urandom(NONCE_LEN)
|
|
194
|
+
ct = AESGCM(key).encrypt(nonce, data, MAGIC)
|
|
195
|
+
return MAGIC + nonce + ct
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def decrypt_bytes(token: bytes) -> bytes:
|
|
199
|
+
"""Decrypt a token produced by :func:`encrypt_bytes`. Authenticated — a
|
|
200
|
+
tampered token (or wrong key) raises EncryptionError, never returns garbage."""
|
|
201
|
+
_require_crypto()
|
|
202
|
+
from cryptography.exceptions import InvalidTag
|
|
203
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
204
|
+
|
|
205
|
+
if not token.startswith(MAGIC):
|
|
206
|
+
raise EncryptionError("not a brain encryption token (bad magic header)")
|
|
207
|
+
body = token[len(MAGIC):]
|
|
208
|
+
if len(body) < NONCE_LEN + 16:
|
|
209
|
+
raise EncryptionError("encryption token too short / truncated")
|
|
210
|
+
nonce, ct = body[:NONCE_LEN], body[NONCE_LEN:]
|
|
211
|
+
key, _src = resolve_encryption_key()
|
|
212
|
+
try:
|
|
213
|
+
return AESGCM(key).decrypt(nonce, ct, MAGIC)
|
|
214
|
+
except InvalidTag as exc:
|
|
215
|
+
raise EncryptionError(
|
|
216
|
+
"decryption failed: authentication tag mismatch (wrong key or tampered token)"
|
|
217
|
+
) from exc
|
brain/frontmatter.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""YAML frontmatter parse/serialise — stdlib-first, PyYAML if available.
|
|
2
|
+
|
|
3
|
+
Mirrors the conventions validator (tools/validate.py) so the engine and the
|
|
4
|
+
validator never disagree on note shape. Runs on a bare system python3.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def split(text: str) -> tuple[str, str] | None:
|
|
12
|
+
"""Return (frontmatter_block, body) or None if no leading frontmatter."""
|
|
13
|
+
if not text.startswith("---"):
|
|
14
|
+
return None
|
|
15
|
+
parts = text.split("---", 2)
|
|
16
|
+
if len(parts) < 3:
|
|
17
|
+
return None
|
|
18
|
+
return parts[1], parts[2]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse(block: str) -> dict[str, Any]:
|
|
22
|
+
"""Parse a frontmatter block. PyYAML if importable, else a flat mini-parser."""
|
|
23
|
+
try:
|
|
24
|
+
import yaml # type: ignore
|
|
25
|
+
|
|
26
|
+
data = yaml.safe_load(block)
|
|
27
|
+
return data if isinstance(data, dict) else {}
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
30
|
+
data: dict[str, Any] = {}
|
|
31
|
+
for line in block.splitlines():
|
|
32
|
+
if not line.strip() or line.lstrip().startswith("#"):
|
|
33
|
+
continue
|
|
34
|
+
if ":" not in line or line[0] in " \t-":
|
|
35
|
+
continue
|
|
36
|
+
key, _, val = line.partition(":")
|
|
37
|
+
key, val = key.strip(), val.strip()
|
|
38
|
+
if val.startswith("[") and val.endswith("]"):
|
|
39
|
+
inner = val[1:-1].strip()
|
|
40
|
+
data[key] = [x.strip().strip("'\"") for x in inner.split(",") if x.strip()]
|
|
41
|
+
else:
|
|
42
|
+
data[key] = _strip_inline_comment(val).strip("'\"")
|
|
43
|
+
return data
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _strip_inline_comment(val: str) -> str:
|
|
47
|
+
"""Strip a trailing unquoted `` # comment`` from a scalar value.
|
|
48
|
+
|
|
49
|
+
A quoted value keeps a literal ``#`` (``'#internal'`` is a value, not a
|
|
50
|
+
comment); YAML requires the ``#`` be preceded by whitespace to start a
|
|
51
|
+
comment, so ``val#x`` (no space) is left alone too."""
|
|
52
|
+
val = val.strip()
|
|
53
|
+
if val[:1] in "'\"":
|
|
54
|
+
return val
|
|
55
|
+
idx = val.find(" #")
|
|
56
|
+
if idx == -1:
|
|
57
|
+
return val
|
|
58
|
+
return val[:idx].rstrip()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def parse_text(text: str) -> tuple[dict[str, Any], str]:
|
|
62
|
+
"""Convenience: full note text -> (meta, body). Empty meta if none."""
|
|
63
|
+
fm = split(text)
|
|
64
|
+
if fm is None:
|
|
65
|
+
return {}, text
|
|
66
|
+
return parse(fm[0]), fm[1]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _scalar(v: Any) -> str:
|
|
70
|
+
if isinstance(v, bool):
|
|
71
|
+
return "true" if v else "false"
|
|
72
|
+
return str(v)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def set_keys(text: str, updates: dict[str, Any]) -> str:
|
|
76
|
+
"""Return ``text`` with each ``updates`` key set in the frontmatter block —
|
|
77
|
+
replacing an existing ``key: ...`` line in place, appending new keys at the
|
|
78
|
+
end of the block otherwise. Body and every other line are untouched.
|
|
79
|
+
|
|
80
|
+
Line-based (mirrors the flat mini-parser above), not a full YAML re-dump —
|
|
81
|
+
the values this is used for (``brain supersede``'s bitemporal keys) are all
|
|
82
|
+
bare scalars, so preserving the rest of the block byte-for-byte matters more
|
|
83
|
+
than a general YAML writer would buy us.
|
|
84
|
+
"""
|
|
85
|
+
fm = split(text)
|
|
86
|
+
if fm is None:
|
|
87
|
+
raise ValueError("set_keys: text has no frontmatter block")
|
|
88
|
+
block, body = fm
|
|
89
|
+
remaining = dict(updates)
|
|
90
|
+
out_lines: list[str] = []
|
|
91
|
+
for line in block.splitlines():
|
|
92
|
+
stripped = line.strip()
|
|
93
|
+
if stripped and ":" in stripped and stripped[0] not in " \t-#":
|
|
94
|
+
key = stripped.split(":", 1)[0].strip()
|
|
95
|
+
if key in remaining:
|
|
96
|
+
out_lines.append(f"{key}: {_scalar(remaining.pop(key))}")
|
|
97
|
+
continue
|
|
98
|
+
out_lines.append(line)
|
|
99
|
+
for key, val in remaining.items():
|
|
100
|
+
out_lines.append(f"{key}: {_scalar(val)}")
|
|
101
|
+
new_block = "\n".join(ln for ln in out_lines if ln.strip() != "") + "\n"
|
|
102
|
+
return f"---\n{new_block}---{body}"
|