algo-cli-runtime 0.14.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 (237) hide show
  1. algo_cli/__init__.py +3 -0
  2. algo_cli/__main__.py +7 -0
  3. algo_cli/_internal/__init__.py +12 -0
  4. algo_cli/_internal/policy_chain.py +259 -0
  5. algo_cli/action_registry.py +1047 -0
  6. algo_cli/agent_blocks.py +550 -0
  7. algo_cli/agent_pipeline.py +1457 -0
  8. algo_cli/agent_threads.py +308 -0
  9. algo_cli/animations.py +316 -0
  10. algo_cli/cache_admission.py +209 -0
  11. algo_cli/capability_mask.py +66 -0
  12. algo_cli/chat_protocol.py +116 -0
  13. algo_cli/chatgpt_auth.py +510 -0
  14. algo_cli/chatgpt_client.py +657 -0
  15. algo_cli/code_rag.py +479 -0
  16. algo_cli/config.py +651 -0
  17. algo_cli/context_budget.py +679 -0
  18. algo_cli/credential_helpers.py +315 -0
  19. algo_cli/deliberation.py +29 -0
  20. algo_cli/display.py +1470 -0
  21. algo_cli/evals/__init__.py +21 -0
  22. algo_cli/evals/algorithm_effectiveness.py +560 -0
  23. algo_cli/evals/competitive_harness_rating.py +702 -0
  24. algo_cli/evals/cot_quality.py +220 -0
  25. algo_cli/evals/harness_retrieval_benchmark.py +401 -0
  26. algo_cli/evals/performance_regression.py +136 -0
  27. algo_cli/evals/scorecard_grading.py +308 -0
  28. algo_cli/evals/session_distribution.py +84 -0
  29. algo_cli/execution_guardrails.py +806 -0
  30. algo_cli/extensions_manifest.py +84 -0
  31. algo_cli/git_evidence.py +227 -0
  32. algo_cli/google_workspace.py +407 -0
  33. algo_cli/google_workspace_auth.py +523 -0
  34. algo_cli/harness.py +2587 -0
  35. algo_cli/identity.py +557 -0
  36. algo_cli/index_compute_lab.py +228 -0
  37. algo_cli/inference_harness.py +70 -0
  38. algo_cli/intelligence/__init__.py +1103 -0
  39. algo_cli/intelligence/acrobat_config.py +307 -0
  40. algo_cli/intelligence/acrobat_manifests.py +338 -0
  41. algo_cli/intelligence/acrobat_models.py +195 -0
  42. algo_cli/intelligence/acrobat_pipeline.py +295 -0
  43. algo_cli/intelligence/acrobat_runtime.py +302 -0
  44. algo_cli/intelligence/acrobat_security.py +261 -0
  45. algo_cli/intelligence/acrobat_workflows.py +226 -0
  46. algo_cli/intelligence/actionability.py +165 -0
  47. algo_cli/intelligence/adversarial_audit.py +136 -0
  48. algo_cli/intelligence/agent_arena.py +92 -0
  49. algo_cli/intelligence/agent_benchmark.py +236 -0
  50. algo_cli/intelligence/agent_runtime.py +171 -0
  51. algo_cli/intelligence/agents_as_tools.py +70 -0
  52. algo_cli/intelligence/artifact_binding.py +80 -0
  53. algo_cli/intelligence/autonomous_engineer.py +1976 -0
  54. algo_cli/intelligence/backpressure.py +99 -0
  55. algo_cli/intelligence/bloom_filter.py +186 -0
  56. algo_cli/intelligence/bonferroni.py +66 -0
  57. algo_cli/intelligence/boundary_compaction.py +98 -0
  58. algo_cli/intelligence/catalog_verifier.py +172 -0
  59. algo_cli/intelligence/cavecrew.py +118 -0
  60. algo_cli/intelligence/changelog.py +176 -0
  61. algo_cli/intelligence/checkpoint_resume.py +92 -0
  62. algo_cli/intelligence/circuit_breaker.py +88 -0
  63. algo_cli/intelligence/clarification_gate.py +101 -0
  64. algo_cli/intelligence/code_graph.py +180 -0
  65. algo_cli/intelligence/coderank.py +97 -0
  66. algo_cli/intelligence/consistent_hash.py +150 -0
  67. algo_cli/intelligence/consortium_synthesis.py +139 -0
  68. algo_cli/intelligence/construction/__init__.py +241 -0
  69. algo_cli/intelligence/construction/common.py +273 -0
  70. algo_cli/intelligence/construction/documents.py +496 -0
  71. algo_cli/intelligence/construction/labor_units.py +1395 -0
  72. algo_cli/intelligence/construction/payments.py +470 -0
  73. algo_cli/intelligence/construction/risk.py +784 -0
  74. algo_cli/intelligence/content_extractor.py +132 -0
  75. algo_cli/intelligence/context_adaptive.py +102 -0
  76. algo_cli/intelligence/context_ops.py +95 -0
  77. algo_cli/intelligence/count_min.py +145 -0
  78. algo_cli/intelligence/cow_state.py +103 -0
  79. algo_cli/intelligence/critic_loop.py +119 -0
  80. algo_cli/intelligence/cross_source.py +113 -0
  81. algo_cli/intelligence/daemon_mode.py +99 -0
  82. algo_cli/intelligence/dag_orchestration.py +151 -0
  83. algo_cli/intelligence/deep_research.py +155 -0
  84. algo_cli/intelligence/degenerate_detector.py +78 -0
  85. algo_cli/intelligence/delta_report.py +92 -0
  86. algo_cli/intelligence/discovery_event_log.py +92 -0
  87. algo_cli/intelligence/document_ingest.py +298 -0
  88. algo_cli/intelligence/dual_layer_validate.py +151 -0
  89. algo_cli/intelligence/echo_fidelity.py +73 -0
  90. algo_cli/intelligence/ema_tuning.py +104 -0
  91. algo_cli/intelligence/event_log.py +92 -0
  92. algo_cli/intelligence/evidence_graph.py +114 -0
  93. algo_cli/intelligence/extension_host.py +162 -0
  94. algo_cli/intelligence/extension_manifest.py +115 -0
  95. algo_cli/intelligence/falsification_suite.py +178 -0
  96. algo_cli/intelligence/finance/__init__.py +169 -0
  97. algo_cli/intelligence/finance/anomalies.py +135 -0
  98. algo_cli/intelligence/finance/ap_ar.py +351 -0
  99. algo_cli/intelligence/finance/cash.py +162 -0
  100. algo_cli/intelligence/finance/close.py +332 -0
  101. algo_cli/intelligence/finance/common.py +244 -0
  102. algo_cli/intelligence/finance/construction.py +135 -0
  103. algo_cli/intelligence/finance/controls.py +172 -0
  104. algo_cli/intelligence/finance/evidence.py +119 -0
  105. algo_cli/intelligence/finance/exceptions.py +157 -0
  106. algo_cli/intelligence/finance/reconciliations.py +254 -0
  107. algo_cli/intelligence/finance/revenue.py +109 -0
  108. algo_cli/intelligence/finance/tax.py +74 -0
  109. algo_cli/intelligence/finance/workpapers.py +111 -0
  110. algo_cli/intelligence/finding_record.py +120 -0
  111. algo_cli/intelligence/flow_dag.py +267 -0
  112. algo_cli/intelligence/gatherer.py +223 -0
  113. algo_cli/intelligence/golden_master.py +98 -0
  114. algo_cli/intelligence/graph_rag.py +195 -0
  115. algo_cli/intelligence/group_chat.py +143 -0
  116. algo_cli/intelligence/hash_dedup.py +145 -0
  117. algo_cli/intelligence/hyperloglog.py +128 -0
  118. algo_cli/intelligence/incremental_index.py +316 -0
  119. algo_cli/intelligence/index_store.py +16 -0
  120. algo_cli/intelligence/iteration_plan.py +133 -0
  121. algo_cli/intelligence/kernel_plugins.py +167 -0
  122. algo_cli/intelligence/lesson_catalog.py +135 -0
  123. algo_cli/intelligence/llm_fallback.py +169 -0
  124. algo_cli/intelligence/log2_histogram.py +267 -0
  125. algo_cli/intelligence/lsp_integration.py +147 -0
  126. algo_cli/intelligence/memory_evolution.py +117 -0
  127. algo_cli/intelligence/minhash_lsh.py +182 -0
  128. algo_cli/intelligence/multi_model_score.py +174 -0
  129. algo_cli/intelligence/multi_tier_grade.py +211 -0
  130. algo_cli/intelligence/negative_controls.py +113 -0
  131. algo_cli/intelligence/numeric_clamp.py +63 -0
  132. algo_cli/intelligence/occ_editor.py +66 -0
  133. algo_cli/intelligence/output_normalize.py +112 -0
  134. algo_cli/intelligence/parallel_delegation.py +98 -0
  135. algo_cli/intelligence/parallel_fanout.py +104 -0
  136. algo_cli/intelligence/permission_modes.py +105 -0
  137. algo_cli/intelligence/pre_push_gate.py +68 -0
  138. algo_cli/intelligence/prefetch.py +171 -0
  139. algo_cli/intelligence/process_framework.py +217 -0
  140. algo_cli/intelligence/project_graph.py +387 -0
  141. algo_cli/intelligence/query_expansion.py +146 -0
  142. algo_cli/intelligence/ralph_loop.py +117 -0
  143. algo_cli/intelligence/rate_limiter.py +153 -0
  144. algo_cli/intelligence/refactor_transaction.py +94 -0
  145. algo_cli/intelligence/research_workspace.py +108 -0
  146. algo_cli/intelligence/retraction_ledger.py +72 -0
  147. algo_cli/intelligence/saga_pattern.py +88 -0
  148. algo_cli/intelligence/session_fork.py +100 -0
  149. algo_cli/intelligence/shadow_editor.py +67 -0
  150. algo_cli/intelligence/shell_session.py +213 -0
  151. algo_cli/intelligence/source_registry.py +143 -0
  152. algo_cli/intelligence/spawn_scales.py +99 -0
  153. algo_cli/intelligence/stat_stability.py +104 -0
  154. algo_cli/intelligence/structural_validator.py +148 -0
  155. algo_cli/intelligence/subagent_spawner.py +111 -0
  156. algo_cli/intelligence/symmetric_verify.py +70 -0
  157. algo_cli/intelligence/task_classifier.py +129 -0
  158. algo_cli/intelligence/team_execution.py +122 -0
  159. algo_cli/intelligence/tiered_access.py +121 -0
  160. algo_cli/intelligence/utility_registry.py +159 -0
  161. algo_cli/intuition_engine.py +560 -0
  162. algo_cli/intuition_injector.py +82 -0
  163. algo_cli/kernels/__init__.py +5 -0
  164. algo_cli/kernels/manifest.py +763 -0
  165. algo_cli/main.py +3903 -0
  166. algo_cli/memory_candidates.py +541 -0
  167. algo_cli/memory_echo_veil.py +394 -0
  168. algo_cli/memory_runtime.py +112 -0
  169. algo_cli/model_info.py +548 -0
  170. algo_cli/model_profile.py +160 -0
  171. algo_cli/model_routing.py +74 -0
  172. algo_cli/oneshot.py +331 -0
  173. algo_cli/perf_telemetry.py +389 -0
  174. algo_cli/plugins.py +245 -0
  175. algo_cli/private_event_store.py +654 -0
  176. algo_cli/quantization/__init__.py +24 -0
  177. algo_cli/quantization/lloyd_max.py +98 -0
  178. algo_cli/quantization/turbo_quant.py +308 -0
  179. algo_cli/reasoning/__init__.py +46 -0
  180. algo_cli/reasoning/combinatorial.py +356 -0
  181. algo_cli/reasoning/graph_of_thought.py +297 -0
  182. algo_cli/reasoning/mcts.py +220 -0
  183. algo_cli/reasoning/neuro_symbolic.py +250 -0
  184. algo_cli/reasoning/react.py +246 -0
  185. algo_cli/reasoning/reflexion.py +225 -0
  186. algo_cli/reasoning/tree_of_thought.py +241 -0
  187. algo_cli/reasoning_bridge.py +150 -0
  188. algo_cli/reconciliation.py +284 -0
  189. algo_cli/reflex.py +385 -0
  190. algo_cli/resources/docs/ALGO.md +13958 -0
  191. algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
  192. algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
  193. algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
  194. algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
  195. algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
  196. algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
  197. algo_cli/resources/docs/main-split-map.md +35 -0
  198. algo_cli/resources/docs/privacy-and-context.md +48 -0
  199. algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
  200. algo_cli/resources/skills/README.md +26 -0
  201. algo_cli/resources/skills/algo-cli.md +59 -0
  202. algo_cli/resources/skills/edit-file-precision.md +49 -0
  203. algo_cli/resources/skills/harness-search-first.md +47 -0
  204. algo_cli/resources/skills/memory-recall-ritual.md +51 -0
  205. algo_cli/resources/skills/qol-algorithms.md +224 -0
  206. algo_cli/resources/skills/smart-error-recovery.md +56 -0
  207. algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
  208. algo_cli/retrieval_algorithms.py +127 -0
  209. algo_cli/runtime_qos.py +236 -0
  210. algo_cli/runtime_services.py +320 -0
  211. algo_cli/session_commands.py +95 -0
  212. algo_cli/session_mode.py +113 -0
  213. algo_cli/skills.py +430 -0
  214. algo_cli/slash_dispatch.py +1265 -0
  215. algo_cli/small_context.py +206 -0
  216. algo_cli/spawn_budget.py +89 -0
  217. algo_cli/task_ledger.py +84 -0
  218. algo_cli/task_router.py +197 -0
  219. algo_cli/tool_context.py +94 -0
  220. algo_cli/tool_contract.py +99 -0
  221. algo_cli/tool_policy.py +357 -0
  222. algo_cli/tool_runtime.py +647 -0
  223. algo_cli/tools.py +3056 -0
  224. algo_cli/url_scheme.py +174 -0
  225. algo_cli/verify.py +154 -0
  226. algo_cli/version_manifest.py +178 -0
  227. algo_cli/vision_screenshot_verify.py +76 -0
  228. algo_cli/workspace_resolver.py +68 -0
  229. algo_cli/x_account.py +209 -0
  230. algo_cli/xai_auth.py +374 -0
  231. algo_cli/xai_client.py +600 -0
  232. algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
  233. algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
  234. algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
  235. algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
  236. algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
  237. ollama_cli/__init__.py +67 -0
@@ -0,0 +1,302 @@
1
+ """B163, B164, B165, B175: Acrobat-derived runtime patterns.
2
+
3
+ - B163: Deterministic Variant / Experiment Bucketing
4
+ - B164: Broker / Sidecar Process Boundary
5
+ - B165: Chunked Web Resource Loading
6
+ - B175: Script Bytecode Compilation Cache
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass, field
12
+ from typing import Any, Callable, Protocol
13
+ import hashlib
14
+ import time
15
+
16
+
17
+ # ── B163: Deterministic Variant / Experiment Bucketing ────────────────
18
+
19
+
20
+ @dataclass
21
+ class VariantRange:
22
+ """A numeric range mapping to a variant."""
23
+ start: int # inclusive
24
+ end: int # inclusive
25
+ variant: str
26
+
27
+
28
+ @dataclass
29
+ class ExperimentDefinition:
30
+ """An experiment with variant ranges (B163)."""
31
+ experiment_id: str
32
+ ranges: list[VariantRange] = field(default_factory=list)
33
+ control_variant: str = "control"
34
+
35
+ def assign(self, subject_id: str) -> str:
36
+ """Deterministically assign a variant based on subject_id hash."""
37
+ bucket = self._hash_bucket(subject_id)
38
+ for r in self.ranges:
39
+ if r.start <= bucket <= r.end:
40
+ return r.variant
41
+ return self.control_variant
42
+
43
+ def _hash_bucket(self, subject_id: str) -> int:
44
+ """Stable hash to bucket 1-1000."""
45
+ h = hashlib.sha256(f"{self.experiment_id}:{subject_id}".encode()).hexdigest()
46
+ return int(h[:8], 16) % 1000 + 1
47
+
48
+
49
+ class ExperimentRegistry:
50
+ """Registry of experiments (B163)."""
51
+
52
+ def __init__(self) -> None:
53
+ self._experiments: dict[str, ExperimentDefinition] = {}
54
+
55
+ def register(self, experiment: ExperimentDefinition) -> None:
56
+ self._experiments[experiment.experiment_id] = experiment
57
+
58
+ def assign(self, experiment_id: str, subject_id: str) -> str:
59
+ """Assign a variant for the given experiment and subject."""
60
+ exp = self._experiments.get(experiment_id)
61
+ if not exp:
62
+ return "control"
63
+ return exp.assign(subject_id)
64
+
65
+ def all_experiments(self) -> list[str]:
66
+ return list(self._experiments.keys())
67
+
68
+
69
+ # ── B164: Broker / Sidecar Process Boundary ───────────────────────────
70
+
71
+
72
+ @dataclass
73
+ class SidecarRequest:
74
+ """A request to a sidecar worker (B164)."""
75
+ command: str
76
+ params: dict[str, Any] = field(default_factory=dict)
77
+ timeout_seconds: float = 30.0
78
+ permission_envelope: dict[str, bool] = field(default_factory=dict)
79
+
80
+
81
+ @dataclass
82
+ class SidecarResult:
83
+ """Result from a sidecar worker (B164)."""
84
+ success: bool = True
85
+ output: Any = None
86
+ error: str = ""
87
+ timed_out: bool = False
88
+ crashed: bool = False
89
+ duration_ms: float = 0.0
90
+
91
+
92
+ class SidecarWorker(Protocol):
93
+ """Protocol for sidecar workers (B164)."""
94
+ def execute(self, request: SidecarRequest) -> SidecarResult: ...
95
+
96
+
97
+ class InProcessSidecar:
98
+ """A simple in-process sidecar for testing (B164)."""
99
+
100
+ def __init__(self, handlers: dict[str, Callable[[dict], Any]] | None = None) -> None:
101
+ self._handlers = handlers or {}
102
+
103
+ def register(self, command: str, handler: Callable[[dict], Any]) -> None:
104
+ self._handlers[command] = handler
105
+
106
+ def execute(self, request: SidecarRequest) -> SidecarResult:
107
+ start = time.monotonic()
108
+ handler = self._handlers.get(request.command)
109
+ if not handler:
110
+ return SidecarResult(
111
+ success=False,
112
+ error=f"unknown command: {request.command}",
113
+ duration_ms=(time.monotonic() - start) * 1000,
114
+ )
115
+ # Check permission envelope
116
+ if request.permission_envelope.get("external_send") and not request.permission_envelope.get("approved"):
117
+ return SidecarResult(
118
+ success=False,
119
+ error="external_send not approved",
120
+ duration_ms=(time.monotonic() - start) * 1000,
121
+ )
122
+ try:
123
+ output = handler(request.params)
124
+ return SidecarResult(
125
+ success=True,
126
+ output=output,
127
+ duration_ms=(time.monotonic() - start) * 1000,
128
+ )
129
+ except Exception as e:
130
+ return SidecarResult(
131
+ success=False,
132
+ error=str(e),
133
+ crashed=True,
134
+ duration_ms=(time.monotonic() - start) * 1000,
135
+ )
136
+
137
+
138
+ class BrokerManager:
139
+ """Manages broker/sidecar process boundaries (B164)."""
140
+
141
+ def __init__(self) -> None:
142
+ self._workers: dict[str, SidecarWorker] = {}
143
+
144
+ def register_worker(self, name: str, worker: SidecarWorker) -> None:
145
+ self._workers[name] = worker
146
+
147
+ def dispatch(self, worker_name: str, request: SidecarRequest) -> SidecarResult:
148
+ """Dispatch a request to a named worker."""
149
+ worker = self._workers.get(worker_name)
150
+ if not worker:
151
+ return SidecarResult(success=False, error=f"unknown worker: {worker_name}")
152
+ try:
153
+ return worker.execute(request)
154
+ except Exception as e:
155
+ # Worker crash does not kill the broker
156
+ return SidecarResult(success=False, error=str(e), crashed=True)
157
+
158
+ def available_workers(self) -> list[str]:
159
+ return list(self._workers.keys())
160
+
161
+
162
+ # ── B165: Chunked Web Resource Loading ──────────────────────────────
163
+
164
+
165
+ @dataclass
166
+ class ResourceChunk:
167
+ """A chunk of a web resource (B165)."""
168
+ chunk_id: str
169
+ version: str = ""
170
+ size_bytes: int = 0
171
+ optional: bool = True
172
+
173
+
174
+ @dataclass
175
+ class ChunkLoadResult:
176
+ """Result of loading a chunk."""
177
+ chunk_id: str
178
+ loaded: bool = True
179
+ from_cache: bool = False
180
+ error: str = ""
181
+ load_time_ms: float = 0.0
182
+
183
+
184
+ class ChunkedResourceLoader:
185
+ """Lazy-loads optional feature chunks (B165)."""
186
+
187
+ def __init__(self) -> None:
188
+ self._chunks: dict[str, ResourceChunk] = {}
189
+ self._cache: dict[str, Any] = {}
190
+ self._loaded: set[str] = set()
191
+
192
+ def register_chunk(self, chunk: ResourceChunk) -> None:
193
+ self._chunks[chunk.chunk_id] = chunk
194
+
195
+ def load(self, chunk_id: str, loader: Callable[[], Any] | None = None) -> ChunkLoadResult:
196
+ """Load a chunk by ID, using the provided loader function."""
197
+ start = time.monotonic()
198
+ chunk = self._chunks.get(chunk_id)
199
+ if not chunk:
200
+ return ChunkLoadResult(chunk_id=chunk_id, loaded=False, error=f"unknown chunk: {chunk_id}")
201
+ if chunk_id in self._cache:
202
+ return ChunkLoadResult(
203
+ chunk_id=chunk_id, loaded=True, from_cache=True,
204
+ load_time_ms=(time.monotonic() - start) * 1000,
205
+ )
206
+ if loader:
207
+ try:
208
+ self._cache[chunk_id] = loader()
209
+ self._loaded.add(chunk_id)
210
+ return ChunkLoadResult(
211
+ chunk_id=chunk_id, loaded=True,
212
+ load_time_ms=(time.monotonic() - start) * 1000,
213
+ )
214
+ except Exception as e:
215
+ return ChunkLoadResult(
216
+ chunk_id=chunk_id, loaded=False, error=str(e),
217
+ load_time_ms=(time.monotonic() - start) * 1000,
218
+ )
219
+ return ChunkLoadResult(
220
+ chunk_id=chunk_id, loaded=False, error="no loader provided",
221
+ load_time_ms=(time.monotonic() - start) * 1000,
222
+ )
223
+
224
+ def is_loaded(self, chunk_id: str) -> bool:
225
+ return chunk_id in self._loaded
226
+
227
+ def invalidate(self, chunk_id: str) -> bool:
228
+ """Invalidate a cached chunk."""
229
+ if chunk_id in self._cache:
230
+ del self._cache[chunk_id]
231
+ self._loaded.discard(chunk_id)
232
+ return True
233
+ return False
234
+
235
+ def all_chunks(self) -> list[str]:
236
+ return list(self._chunks.keys())
237
+
238
+
239
+ # ── B175: Script Bytecode Compilation Cache ──────────────────────────
240
+
241
+
242
+ @dataclass
243
+ class CacheEntry:
244
+ """A compiled script cache entry (B175)."""
245
+ source_path: str
246
+ cache_path: str
247
+ source_mtime: float
248
+ platform: str
249
+
250
+
251
+ class ScriptBytecodeCache:
252
+ """Caches compiled scripts to avoid re-parsing on every startup (B175)."""
253
+
254
+ def __init__(self, platform: str = "win") -> None:
255
+ self._platform = platform
256
+ self._entries: dict[str, CacheEntry] = {}
257
+ self._compile_fn: Callable[[str], bytes] | None = None
258
+
259
+ def set_compiler(self, compile_fn: Callable[[str], bytes]) -> None:
260
+ self._compile_fn = compile_fn
261
+
262
+ def get_or_compile(self, source_path: str, source_mtime: float) -> tuple[bytes, bool]:
263
+ """Get cached bytecode or compile from source.
264
+ Returns (bytecode, from_cache).
265
+ """
266
+ entry = self._entries.get(source_path)
267
+ if entry and entry.source_mtime >= source_mtime and entry.platform == self._platform:
268
+ # Cache hit
269
+ return b"", True # In real impl, would read cache file
270
+
271
+ # Cache miss - compile
272
+ if not self._compile_fn:
273
+ raise RuntimeError("no compiler set")
274
+ bytecode = self._compile_fn(source_path)
275
+ self._entries[source_path] = CacheEntry(
276
+ source_path=source_path,
277
+ cache_path=f"{source_path}.cache",
278
+ source_mtime=source_mtime,
279
+ platform=self._platform,
280
+ )
281
+ return bytecode, False
282
+
283
+ def invalidate(self, source_path: str) -> bool:
284
+ """Invalidate a cache entry."""
285
+ if source_path in self._entries:
286
+ del self._entries[source_path]
287
+ return True
288
+ return False
289
+
290
+ def is_stale(self, source_path: str, source_mtime: float) -> bool:
291
+ """Check if a cache entry is stale."""
292
+ entry = self._entries.get(source_path)
293
+ if not entry:
294
+ return True
295
+ return entry.source_mtime < source_mtime or entry.platform != self._platform
296
+
297
+ def cache_stats(self) -> dict[str, int]:
298
+ """Return cache statistics."""
299
+ return {
300
+ "entries": len(self._entries),
301
+ "platform": hash(self._platform), # just for testing
302
+ }
@@ -0,0 +1,261 @@
1
+ """B159, B161, B173: Acrobat-derived security patterns.
2
+
3
+ - B159: Search/Redaction Pattern Packs
4
+ - B161: Pre-Model Disqualification Rules
5
+ - B173: Native Messaging Host Allowlist
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Any
12
+ import re
13
+
14
+
15
+ # ── B159: Search/Redaction Pattern Packs ───────────────────────────────
16
+
17
+
18
+ @dataclass
19
+ class RedactionPattern:
20
+ """A single redaction pattern (B159)."""
21
+ display_name: str
22
+ regex: str
23
+ examples: str = ""
24
+ category: str = "general"
25
+ severity: str = "medium" # "low", "medium", "high"
26
+
27
+
28
+ @dataclass
29
+ class RedactionCode:
30
+ """A legal/business reason code for redaction (B159)."""
31
+ name: str
32
+ description: str = ""
33
+
34
+
35
+ @dataclass
36
+ class RedactionMatch:
37
+ """A matched span for redaction."""
38
+ pattern_name: str
39
+ span: tuple[int, int]
40
+ text: str
41
+ code: str = ""
42
+ locale: str = "ENU"
43
+
44
+
45
+ @dataclass
46
+ class RedactionPatternPack:
47
+ """A locale-specific pack of redaction patterns (B159)."""
48
+ locale: str = "ENU"
49
+ patterns: list[RedactionPattern] = field(default_factory=list)
50
+ codes: list[RedactionCode] = field(default_factory=list)
51
+
52
+ def scan(self, text: str, enabled_categories: set[str] | None = None) -> list[RedactionMatch]:
53
+ """Scan text for matches against all patterns."""
54
+ matches: list[RedactionMatch] = []
55
+ for pattern in self.patterns:
56
+ if enabled_categories and pattern.category not in enabled_categories:
57
+ continue
58
+ try:
59
+ for m in re.finditer(pattern.regex, text):
60
+ matches.append(RedactionMatch(
61
+ pattern_name=pattern.display_name,
62
+ span=(m.start(), m.end()),
63
+ text=m.group(),
64
+ locale=self.locale,
65
+ ))
66
+ except re.error:
67
+ continue
68
+ return matches
69
+
70
+
71
+ class RedactionPackRegistry:
72
+ """Registry of locale-specific redaction packs (B159)."""
73
+
74
+ def __init__(self) -> None:
75
+ self._packs: dict[str, RedactionPatternPack] = {}
76
+
77
+ def register(self, pack: RedactionPatternPack) -> None:
78
+ self._packs[pack.locale] = pack
79
+
80
+ def get(self, locale: str) -> RedactionPatternPack | None:
81
+ # Try exact locale
82
+ if locale in self._packs:
83
+ return self._packs[locale]
84
+ # Try language prefix
85
+ lang = locale.split("_")[0].upper()
86
+ if lang in self._packs:
87
+ return self._packs[lang]
88
+ # Fall back to ENU
89
+ return self._packs.get("ENU")
90
+
91
+ def scan(self, text: str, locale: str = "ENU", enabled_categories: set[str] | None = None) -> list[RedactionMatch]:
92
+ """Scan text using the best available locale pack."""
93
+ pack = self.get(locale)
94
+ if not pack:
95
+ return []
96
+ return pack.scan(text, enabled_categories)
97
+
98
+ def available_locales(self) -> list[str]:
99
+ return list(self._packs.keys())
100
+
101
+
102
+ # Default patterns (ENU)
103
+ def default_enu_pack() -> RedactionPatternPack:
104
+ """Create a default ENU redaction pack with common PII patterns."""
105
+ return RedactionPatternPack(
106
+ locale="ENU",
107
+ patterns=[
108
+ RedactionPattern(
109
+ display_name="Phone Numbers",
110
+ regex=r"\b\d{3}[-.\s]\d{3}[-.\s]\d{4}\b|\b\d{3}[-.\s]\d{4}\b",
111
+ examples="555-123-4567, 555-1212",
112
+ category="phone",
113
+ severity="medium",
114
+ ),
115
+ RedactionPattern(
116
+ display_name="Credit Cards",
117
+ regex=r"(\b)(((\d{4}(-|\s|\.|_)?){3}(\d{3,4})))",
118
+ examples="1234-5678-9012-3456",
119
+ category="credit_card",
120
+ severity="high",
121
+ ),
122
+ RedactionPattern(
123
+ display_name="Social Security Numbers",
124
+ regex=r"(\b)((\d{3}(-|\s|\.|_)\d{2}(-|\s|\.|_)\d{4})|(\d{9}))(\b)",
125
+ examples="123-45-6789",
126
+ category="ssn",
127
+ severity="high",
128
+ ),
129
+ RedactionPattern(
130
+ display_name="Email Addresses",
131
+ regex=r"([a-zA-Z0-9_])([a-zA-Z0-9_\-\.])*@([a-zA-Z0-9\-])+\.([a-zA-Z\.]+)",
132
+ examples="user@example.com",
133
+ category="email",
134
+ severity="medium",
135
+ ),
136
+ ],
137
+ codes=[
138
+ RedactionCode(name="(b) (1) (A)", description="FOIA exemption"),
139
+ RedactionCode(name="(b) (6)", description="Personal privacy"),
140
+ ],
141
+ )
142
+
143
+
144
+ # ── B161: Pre-Model Disqualification Rules ─────────────────────────────
145
+
146
+
147
+ @dataclass
148
+ class DisqualificationRule:
149
+ """A deterministic disqualification rule (B161)."""
150
+ rule_id: str
151
+ tag_type: str # "table", "artifact", "toc", "any", etc.
152
+ tag_subtype: str = ""
153
+ nested_in_tag: str = ""
154
+ action: str = "disqualify" # "disqualify" or "allow"
155
+ conditions: list[dict[str, Any]] = field(default_factory=list)
156
+
157
+
158
+ @dataclass
159
+ class DisqualificationResult:
160
+ """Result of checking disqualification rules."""
161
+ disqualified: bool
162
+ rule_id: str = ""
163
+ reason: str = ""
164
+
165
+
166
+ class DisqualificationEngine:
167
+ """Checks candidates against deterministic rules before model calls (B161)."""
168
+
169
+ def __init__(self) -> None:
170
+ self._rules: list[DisqualificationRule] = []
171
+
172
+ def add_rule(self, rule: DisqualificationRule) -> None:
173
+ self._rules.append(rule)
174
+
175
+ def check(self, candidate: dict[str, Any]) -> DisqualificationResult:
176
+ """Check a candidate against all rules.
177
+
178
+ Candidate should have: tag_type, tag_subtype, nested_in (dict with tag_type).
179
+ """
180
+ for rule in self._rules:
181
+ if rule.tag_type != "any" and rule.tag_type != candidate.get("tag_type"):
182
+ continue
183
+ if rule.tag_subtype and rule.tag_subtype != candidate.get("tag_subtype"):
184
+ continue
185
+ if rule.nested_in_tag:
186
+ nested = candidate.get("nested_in", {})
187
+ if nested.get("tag_type") != rule.nested_in_tag:
188
+ continue
189
+ if rule.action == "disqualify":
190
+ return DisqualificationResult(
191
+ disqualified=True,
192
+ rule_id=rule.rule_id,
193
+ reason=f"disqualified by rule: {rule.rule_id}",
194
+ )
195
+ return DisqualificationResult(disqualified=False)
196
+
197
+ @property
198
+ def rule_count(self) -> int:
199
+ return len(self._rules)
200
+
201
+
202
+ def default_disqual_controls() -> list[DisqualificationRule]:
203
+ """Default disqualification rules from Acrobat's disqual-controls.json."""
204
+ return [
205
+ DisqualificationRule(
206
+ rule_id="table_inside_list",
207
+ tag_type="table",
208
+ nested_in_tag="list",
209
+ ),
210
+ DisqualificationRule(
211
+ rule_id="rule_watermark",
212
+ tag_type="artifact",
213
+ tag_subtype="watermark",
214
+ ),
215
+ DisqualificationRule(
216
+ rule_id="toc",
217
+ tag_type="toc",
218
+ ),
219
+ DisqualificationRule(
220
+ rule_id="rule_True_MC",
221
+ tag_type="any",
222
+ ),
223
+ ]
224
+
225
+
226
+ # ── B173: Native Messaging Host Allowlist ─────────────────────────────
227
+
228
+
229
+ @dataclass
230
+ class NativeMessagingHostManifest:
231
+ """A native messaging host manifest (B173)."""
232
+ name: str
233
+ description: str = ""
234
+ path: str = ""
235
+ host_type: str = "stdio"
236
+ allowed_origins: list[str] = field(default_factory=list)
237
+
238
+
239
+ class NativeMessagingAllowlist:
240
+ """Validates incoming messages against an allowlist of origins (B173)."""
241
+
242
+ def __init__(self) -> None:
243
+ self._hosts: dict[str, NativeMessagingHostManifest] = {}
244
+
245
+ def register(self, manifest: NativeMessagingHostManifest) -> None:
246
+ self._hosts[manifest.name] = manifest
247
+
248
+ def is_allowed(self, host_name: str, origin: str) -> bool:
249
+ """Check if an origin is allowed for a host."""
250
+ host = self._hosts.get(host_name)
251
+ if not host:
252
+ return False
253
+ if not host.allowed_origins:
254
+ return False # empty allowlist rejects all
255
+ return origin in host.allowed_origins
256
+
257
+ def get_host(self, name: str) -> NativeMessagingHostManifest | None:
258
+ return self._hosts.get(name)
259
+
260
+ def all_hosts(self) -> list[str]:
261
+ return list(self._hosts.keys())