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.
Files changed (77) hide show
  1. brain/__init__.py +39 -0
  2. brain/__main__.py +14 -0
  3. brain/_assets/AGENTS.md +564 -0
  4. brain/_assets/overlay/template/brand/brand-guide.md +24 -0
  5. brain/_assets/overlay/template/keywords/glossary.md +15 -0
  6. brain/_assets/overlay/template/people/roster.md +14 -0
  7. brain/_assets/overlay/template/voice/voice-profile.md +34 -0
  8. brain/_assets/routines/manifest.json +257 -0
  9. brain/_assets/scripts/brain-brief-mac.plist +59 -0
  10. brain/_assets/scripts/brain-brief.sh +74 -0
  11. brain/_assets/scripts/brain-synthesis-mac.plist +48 -0
  12. brain/_assets/scripts/brain-synthesis.sh +214 -0
  13. brain/_assets/scripts/install-brief-mac.sh +152 -0
  14. brain/_assets/scripts/install-brief-windows.ps1 +97 -0
  15. brain/_assets/scripts/register_tasks.py +386 -0
  16. brain/_assets/templates/company.md +21 -0
  17. brain/_assets/templates/concept.md +27 -0
  18. brain/_assets/templates/daily.md +20 -0
  19. brain/_assets/templates/decision.md +42 -0
  20. brain/_assets/templates/meeting.md +33 -0
  21. brain/_assets/templates/person.md +20 -0
  22. brain/_assets/templates/project.md +23 -0
  23. brain/_assets/templates/state-moc.md +40 -0
  24. brain/_version.py +12 -0
  25. brain/anchor.py +121 -0
  26. brain/audit.py +422 -0
  27. brain/backup.py +210 -0
  28. brain/brief.py +417 -0
  29. brain/capture.py +117 -0
  30. brain/chunk.py +249 -0
  31. brain/classification.py +134 -0
  32. brain/cli.py +1906 -0
  33. brain/config.py +368 -0
  34. brain/connect.py +362 -0
  35. brain/context.py +108 -0
  36. brain/core.py +3018 -0
  37. brain/doctor.py +1161 -0
  38. brain/egress.py +148 -0
  39. brain/embed.py +857 -0
  40. brain/encryption.py +217 -0
  41. brain/frontmatter.py +102 -0
  42. brain/golden_probe.py +678 -0
  43. brain/graph.py +369 -0
  44. brain/graphify.py +352 -0
  45. brain/index.py +1576 -0
  46. brain/ingest/__init__.py +19 -0
  47. brain/ingest/handlers/__init__.py +43 -0
  48. brain/ingest/handlers/base.py +95 -0
  49. brain/ingest/handlers/docx.py +78 -0
  50. brain/ingest/handlers/email.py +228 -0
  51. brain/ingest/handlers/html.py +142 -0
  52. brain/ingest/handlers/image.py +91 -0
  53. brain/ingest/handlers/pdf.py +99 -0
  54. brain/ingest/handlers/pptx.py +69 -0
  55. brain/ingest/handlers/tables.py +41 -0
  56. brain/ingest/handlers/text.py +43 -0
  57. brain/ingest/handlers/xlsx.py +100 -0
  58. brain/ingest/handlers/zip.py +163 -0
  59. brain/ingest/pipeline.py +839 -0
  60. brain/ingest/transcript.py +158 -0
  61. brain/init.py +870 -0
  62. brain/maintenance.py +2266 -0
  63. brain/mcp_adapter.py +217 -0
  64. brain/multihop.py +232 -0
  65. brain/notes.py +195 -0
  66. brain/overlay.py +183 -0
  67. brain/projection.py +79 -0
  68. brain/rerank.py +425 -0
  69. brain/snapshot.py +231 -0
  70. brain/update.py +743 -0
  71. brain/vectors.py +225 -0
  72. brainiac_cli-0.16.0.dist-info/METADATA +306 -0
  73. brainiac_cli-0.16.0.dist-info/RECORD +77 -0
  74. brainiac_cli-0.16.0.dist-info/WHEEL +5 -0
  75. brainiac_cli-0.16.0.dist-info/entry_points.txt +4 -0
  76. brainiac_cli-0.16.0.dist-info/licenses/LICENSE +202 -0
  77. 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}"