clm-kernel 0.0.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.
- clm/__init__.py +230 -0
- clm/__main__.py +25 -0
- clm/cross_cutting/__init__.py +50 -0
- clm/cross_cutting/_test_runner.py +36 -0
- clm/cross_cutting/async_utils.py +25 -0
- clm/cross_cutting/cel_builder.py +123 -0
- clm/cross_cutting/cel_converter.py +697 -0
- clm/cross_cutting/cel_engine.py +252 -0
- clm/cross_cutting/cel_ops.py +72 -0
- clm/cross_cutting/cel_ops_db.py +157 -0
- clm/cross_cutting/cel_ops_files.py +99 -0
- clm/cross_cutting/cel_ops_state.py +71 -0
- clm/cross_cutting/cel_ops_strings.py +64 -0
- clm/cross_cutting/cel_resolver.py +63 -0
- clm/cross_cutting/cel_slices.py +56 -0
- clm/cross_cutting/cel_store.py +330 -0
- clm/cross_cutting/clm_logger.py +140 -0
- clm/cross_cutting/config/__init__.py +155 -0
- clm/cross_cutting/config/config_constants.py +392 -0
- clm/cross_cutting/config/env_parameters.py +90 -0
- clm/cross_cutting/config/logging.py +378 -0
- clm/cross_cutting/config/settings.py +504 -0
- clm/cross_cutting/context.py +5 -0
- clm/cross_cutting/domain_models.py +55 -0
- clm/cross_cutting/domain_types.py +61 -0
- clm/cross_cutting/effects.py +67 -0
- clm/cross_cutting/errors.py +153 -0
- clm/cross_cutting/governance.py +331 -0
- clm/cross_cutting/guards.py +41 -0
- clm/cross_cutting/mcard.py +321 -0
- clm/cross_cutting/native.py +185 -0
- clm/cross_cutting/observability.py +289 -0
- clm/cross_cutting/pcard.py +434 -0
- clm/cross_cutting/registry.py +71 -0
- clm/cross_cutting/resources.py +84 -0
- clm/cross_cutting/secrets.py +44 -0
- clm/cross_cutting/subprocess_utils.py +118 -0
- clm/cross_cutting/telemetry.py +164 -0
- clm/cross_cutting/testing/__init__.py +8 -0
- clm/cross_cutting/testing/comparator.py +187 -0
- clm/cross_cutting/testing/datasets.py +87 -0
- clm/cross_cutting/timeutil.py +20 -0
- clm/cross_cutting/types/new_type_check.py +0 -0
- clm/cross_cutting/utils/__init__.py +1 -0
- clm/cross_cutting/utils/url_safety.py +50 -0
- clm/cross_cutting/vcard.py +560 -0
- clm/cross_cutting/writer.py +118 -0
- clm/layer0/README.md +50 -0
- clm/layer0/__init__.py +223 -0
- clm/layer0/algebra.py +60 -0
- clm/layer0/algebra_baldwin.py +133 -0
- clm/layer0/algebra_baldwin_generators.py +36 -0
- clm/layer0/algebra_equivalence.py +262 -0
- clm/layer0/algebra_ingest.py +306 -0
- clm/layer0/algebra_manifest_io.py +156 -0
- clm/layer0/assembler.py +481 -0
- clm/layer0/cel/__init__.py +23 -0
- clm/layer0/cel_eval.py +491 -0
- clm/layer0/codec.py +106 -0
- clm/layer0/context.py +122 -0
- clm/layer0/db.py +160 -0
- clm/layer0/exceptions.py +73 -0
- clm/layer0/fibration.py +106 -0
- clm/layer0/gates.py +50 -0
- clm/layer0/hash.py +80 -0
- clm/layer0/loader.py +826 -0
- clm/layer0/math_utils.py +163 -0
- clm/layer0/mcard.py +96 -0
- clm/layer0/model/__init__.py +0 -0
- clm/layer0/model/action.py +223 -0
- clm/layer0/model/card.py +280 -0
- clm/layer0/model/card_triad.py +427 -0
- clm/layer0/model/compatibility.py +60 -0
- clm/layer0/model/dictionary.py +46 -0
- clm/layer0/model/dots.py +331 -0
- clm/layer0/model/event_producer.py +88 -0
- clm/layer0/model/g_time.py +89 -0
- clm/layer0/model/handle.py +163 -0
- clm/layer0/model/hash/__init__.py +6 -0
- clm/layer0/model/hash/algorithms/__init__.py +3 -0
- clm/layer0/model/hash/algorithms/custom_hash.py +7 -0
- clm/layer0/model/hash/algorithms/local_sha256.py +178 -0
- clm/layer0/model/hash/constants.py +53 -0
- clm/layer0/model/hash/enums.py +60 -0
- clm/layer0/model/hash/validator.py +307 -0
- clm/layer0/model/interpreter.py +275 -0
- clm/layer0/model/pagination.py +45 -0
- clm/layer0/model/pcard.py +716 -0
- clm/layer0/model/schema.py +44 -0
- clm/layer0/model/utils/__init__.py +1 -0
- clm/layer0/model/utils/content_analyzer.py +152 -0
- clm/layer0/model/validators/__init__.py +1 -0
- clm/layer0/model/validators/base_validator.py +44 -0
- clm/layer0/model/validators/binary_validator.py +66 -0
- clm/layer0/model/validators/text_validator.py +82 -0
- clm/layer0/model/validators/validation_registry.py +49 -0
- clm/layer0/model/vcard.py +823 -0
- clm/layer0/model/vcard_ext/__init__.py +39 -0
- clm/layer0/model/vcard_ext/core.py +60 -0
- clm/layer0/model/vcard_ext/network.py +23 -0
- clm/layer0/model/vcard_ext/observability.py +63 -0
- clm/layer0/model/vcard_ext/storage.py +60 -0
- clm/layer0/model/vcard_ext/vendors.py +211 -0
- clm/layer0/model/vcard_sandwich.py +119 -0
- clm/layer0/model/vcard_vocabulary.py +463 -0
- clm/layer0/model/workflow.py +274 -0
- clm/layer0/narrative.py +431 -0
- clm/layer0/ontology/collection.py +125 -0
- clm/layer0/parser.py +429 -0
- clm/layer0/relations.py +73 -0
- clm/layer0/rule_evaluator.py +251 -0
- clm/layer0/schema/__init__.py +38 -0
- clm/layer0/schema/ddl.py +97 -0
- clm/layer0/templates/abstract.yaml +99 -0
- clm/layer0/templates/balanced.yaml +253 -0
- clm/layer0/templates/concrete.yaml +172 -0
- clm/layer0/type_registry.py +223 -0
- clm/layer0/types.py +204 -0
- clm/layer0/utils.py +886 -0
- clm/layer0/verifier_core.py +262 -0
- clm/layer1/__init__.py +31 -0
- clm/layer1/action_dispatcher.py +275 -0
- clm/layer1/adapter_registry.py +233 -0
- clm/layer1/algebra_baldwin_generators.py +192 -0
- clm/layer1/arrow_compiler.py +525 -0
- clm/layer1/batch_processor.py +152 -0
- clm/layer1/cel/__init__.py +27 -0
- clm/layer1/cel/builder.py +173 -0
- clm/layer1/engine.py +759 -0
- clm/layer1/engine_async.py +89 -0
- clm/layer1/eval_callback.py +49 -0
- clm/layer1/gate_composer.py +229 -0
- clm/layer1/hoare_sandwich.py +298 -0
- clm/layer1/identity_guard.py +348 -0
- clm/layer1/matrix.py +138 -0
- clm/layer1/neural_runtime.py +221 -0
- clm/layer1/operations/__init__.py +123 -0
- clm/layer1/operations/augment.py +156 -0
- clm/layer1/operations/base.py +109 -0
- clm/layer1/operations/builtins.py +338 -0
- clm/layer1/operations/dispatch.py +144 -0
- clm/layer1/operations/exclude.py +107 -0
- clm/layer1/operations/handle.py +197 -0
- clm/layer1/operations/identity.py +139 -0
- clm/layer1/operations/invert.py +66 -0
- clm/layer1/operations/loader.py +116 -0
- clm/layer1/operations/manipulators/__init__.py +23 -0
- clm/layer1/operations/manipulators/document.py +211 -0
- clm/layer1/operations/manipulators/media.py +134 -0
- clm/layer1/operations/manipulators/structured.py +159 -0
- clm/layer1/operations/manipulators/tabular.py +334 -0
- clm/layer1/operations/port.py +65 -0
- clm/layer1/operations/services.py +529 -0
- clm/layer1/operations/split.py +117 -0
- clm/layer1/operations/substitute.py +75 -0
- clm/layer1/petri.py +1052 -0
- clm/layer1/pocketflow_orchestrator.py +408 -0
- clm/layer1/pocketflow_scheduler.py +519 -0
- clm/layer1/protocols.py +197 -0
- clm/layer1/runtime_adapter.py +718 -0
- clm/layer1/runtimes/__init__.py +71 -0
- clm/layer1/runtimes/_generated_assertions.py +125 -0
- clm/layer1/runtimes/_generated_types.py +42 -0
- clm/layer1/runtimes/base.py +265 -0
- clm/layer1/runtimes/binary.py +101 -0
- clm/layer1/runtimes/factory.py +203 -0
- clm/layer1/runtimes/javascript.py +321 -0
- clm/layer1/runtimes/javascript_runtime.js +33 -0
- clm/layer1/runtimes/js_scripts/isomorphism.mjs +22 -0
- clm/layer1/runtimes/js_scripts/loader.mjs +52 -0
- clm/layer1/runtimes/lambda_calc.py +126 -0
- clm/layer1/runtimes/python.py +288 -0
- clm/layer1/runtimes/python_runtime.py +41 -0
- clm/layer1/runtimes/script.py +114 -0
- clm/layer1/sandbox.py +334 -0
- clm/layer1/savepoint_guard.py +136 -0
- clm/layer1/sparse_net.py +122 -0
- clm/layer1/spec_tester.py +165 -0
- clm/layer1/store.py +295 -0
- clm/layer1/test_runner.py +498 -0
- clm/layer1/type_inspector.py +166 -0
- clm/layer1/verifier.py +772 -0
- clm/layer1/vm.py +91 -0
- clm/layer2/__init__.py +78 -0
- clm/layer2/card_collection.py +678 -0
- clm/layer2/classifier.py +479 -0
- clm/layer2/engine/__init__.py +1 -0
- clm/layer2/engine/abstract_sql_engine.py +320 -0
- clm/layer2/engine/base.py +193 -0
- clm/layer2/engine/duckdb_engine.py +576 -0
- clm/layer2/engine/sqlite_engine.py +518 -0
- clm/layer2/fiber/__init__.py +31 -0
- clm/layer2/fiber/context.py +89 -0
- clm/layer2/fiber/disposable.py +163 -0
- clm/layer2/fiber/fiber.py +125 -0
- clm/layer2/fiber/hoare.py +191 -0
- clm/layer2/file_io.py +376 -0
- clm/layer2/file_registrar.py +77 -0
- clm/layer2/ingest.py +202 -0
- clm/layer2/lineage.py +322 -0
- clm/layer2/mcard_fs.py +262 -0
- clm/layer2/merkle.py +11 -0
- clm/layer2/mime.py +244 -0
- clm/layer2/mime_detector.py +245 -0
- clm/layer2/private_collection.py +118 -0
- clm/layer2/shared_store.py +11 -0
- clm/layer2/storage/__init__.py +190 -0
- clm/layer2/storage/db_bitemporal.py +141 -0
- clm/layer2/storage/db_driver.py +331 -0
- clm/layer2/storage/db_hyperlinks.py +214 -0
- clm/layer2/storage/db_pool.py +139 -0
- clm/layer2/storage/db_repository.py +229 -0
- clm/layer2/storage/savepoint.py +78 -0
- clm/layer2/storage/sqlite_wal.py +74 -0
- clm/layer2/tridb.py +245 -0
- clm/layer2/vcard_receipt.py +71 -0
- clm/layer2/vfs.py +17 -0
- clm/layer3/__init__.py +141 -0
- clm/layer3/agency/__init__.py +19 -0
- clm/layer3/agency/config.py +220 -0
- clm/layer3/agency/providers/__init__.py +15 -0
- clm/layer3/agency/providers/base.py +91 -0
- clm/layer3/agency/providers/mlc_llm.py +169 -0
- clm/layer3/agency/providers/ollama.py +203 -0
- clm/layer3/agency/router.py +71 -0
- clm/layer3/agency/runtime.py +524 -0
- clm/layer3/gateway.py +353 -0
- clm/layer3/open_interpreter_adapter.py +159 -0
- clm/layer3/rag/__init__.py +70 -0
- clm/layer3/rag/cli.py +228 -0
- clm/layer3/rag/config.py +153 -0
- clm/layer3/rag/embeddings/__init__.py +16 -0
- clm/layer3/rag/embeddings/base.py +79 -0
- clm/layer3/rag/embeddings/ollama.py +211 -0
- clm/layer3/rag/embeddings/vision.py +290 -0
- clm/layer3/rag/engine.py +276 -0
- clm/layer3/rag/graph/__init__.py +22 -0
- clm/layer3/rag/graph/community.py +198 -0
- clm/layer3/rag/graph/engine.py +433 -0
- clm/layer3/rag/graph/extractor.py +255 -0
- clm/layer3/rag/graph/schema.py +115 -0
- clm/layer3/rag/graph/store.py +641 -0
- clm/layer3/rag/indexer.py +333 -0
- clm/layer3/rag/llm_providers.json +52 -0
- clm/layer3/rag/semantic_versioning.py +313 -0
- clm/layer3/rag/vector/__init__.py +28 -0
- clm/layer3/rag/vector/handle_vector_store.py +749 -0
- clm/layer3/rag/vector/schema.py +151 -0
- clm/layer3/rag/vector/store.py +632 -0
- clm/layer3/reticulum/__init__.py +55 -0
- clm/layer3/reticulum/bridge.py +74 -0
- clm/layer3/reticulum/daemon.py +88 -0
- clm/layer3/reticulum/discovery.py +164 -0
- clm/layer3/reticulum/identity.py +107 -0
- clm/layer3/reticulum/media.py +148 -0
- clm/layer3/reticulum/transport.py +133 -0
- clm/layer3/satori/__init__.py +209 -0
- clm/layer3/satori/alpha_conversion.py +168 -0
- clm/layer3/satori/beta_reduction.py +359 -0
- clm/layer3/satori/continuation.py +38 -0
- clm/layer3/satori/eta_conversion.py +205 -0
- clm/layer3/satori/free_variables.py +163 -0
- clm/layer3/satori/io_effects.py +370 -0
- clm/layer3/satori/lambda_runtime.py +726 -0
- clm/layer3/satori/lambda_term.py +230 -0
- clm/layer3/satori/sheaf_audit.py +261 -0
- clm/layer3/satori/speech_act.py +279 -0
- clm/layer4/README.md +9 -0
- clm/layer4/__init__.py +67 -0
- clm/layer4/_sovereign_executor.py +580 -0
- clm/layer4/action_commands.py +719 -0
- clm/layer4/api.py +236 -0
- clm/layer4/bootstrap/__init__.py +13 -0
- clm/layer4/bootstrap/bootstrapper.py +118 -0
- clm/layer4/bootstrap/cleanup.py +44 -0
- clm/layer4/bootstrap/clm_hooks.py +49 -0
- clm/layer4/bootstrap/genesis.py +77 -0
- clm/layer4/bootstrap/injection.py +125 -0
- clm/layer4/cli.py +549 -0
- clm/layer4/collection_manager.py +553 -0
- clm/layer4/commands/__init__.py +81 -0
- clm/layer4/commands/action_cmd.py +42 -0
- clm/layer4/commands/check_cmd.py +82 -0
- clm/layer4/commands/envelope.py +73 -0
- clm/layer4/commands/evaluate_cmd.py +224 -0
- clm/layer4/commands/mcard_cmd.py +24 -0
- clm/layer4/commands/petri_cmd.py +92 -0
- clm/layer4/commands/tridb_cmd.py +319 -0
- clm/layer4/commands/version_cmd.py +27 -0
- clm/layer4/diff_commands.py +167 -0
- clm/layer4/eoa_sponsor.py +232 -0
- clm/layer4/events.py +69 -0
- clm/layer4/flux_gateway.py +133 -0
- clm/layer4/gateway.py +91 -0
- clm/layer4/loader.py +307 -0
- clm/layer4/mcard_commands.py +166 -0
- clm/layer4/membrane.py +273 -0
- clm/layer4/navigation.py +50 -0
- clm/layer4/protocol/protocol_entry_python.py +108 -0
- clm/layer4/protocol/python_sync.py +126 -0
- clm/layer4/runner.py +150 -0
- clm/layer4/satori.py +262 -0
- clm/layer4/server/python_sync_client.py +150 -0
- clm/layer4/server/python_sync_server.py +139 -0
- clm/layer4/server/service_api.py +315 -0
- clm/layer4/server/start_servers.py +107 -0
- clm/layer4/server/stop_servers.py +24 -0
- clm/layer4/simulated_optimization_fix.py +0 -0
- clm/layer4/storage.py +463 -0
- clm/layer4/tridb_admin.py +268 -0
- clm/layer4/websocket/test_ws_client.py +104 -0
- clm/layer4/websocket/test_ws_server.py +138 -0
- clm/layer4/websocket/ws_server_local.py +160 -0
- clm/layer5/__init__.py +51 -0
- clm/layer5/async_batch_writer.py +133 -0
- clm/layer5/learning.py +276 -0
- clm/layer5/telemetry_hook.py +107 -0
- clm/layer5/type_lattice.py +238 -0
- clm/py.typed +0 -0
- clm/schemas/__init__.py +652 -0
- clm/schemas/abstract_face_schema.yaml +77 -0
- clm/schemas/cel_schema.yaml +416 -0
- clm/schemas/lifecycle/__init__.py +45 -0
- clm/schemas/lifecycle/models.py +123 -0
- clm/schemas/lifecycle/service.py +364 -0
- clm/schemas/mcard_schema.sql +192 -0
- clm/schemas/mcard_vector_schema.sql +283 -0
- clm/schemas/mlp-context-v1.json +91 -0
- clm_kernel-0.0.0.dist-info/METADATA +121 -0
- clm_kernel-0.0.0.dist-info/RECORD +333 -0
- clm_kernel-0.0.0.dist-info/WHEEL +4 -0
- clm_kernel-0.0.0.dist-info/entry_points.txt +3 -0
- clm_kernel-0.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Supervised Subprocess Execution and Process Group Guards (Sprint 178 / Sprint 384 Quarantine).
|
|
2
|
+
|
|
3
|
+
[QUARANTINE - HOST-SIDE ADAPTER TOOLING ONLY]
|
|
4
|
+
---------------------------------------------
|
|
5
|
+
WARNING: This module is strictly quarantined for host-side tooling, external runtime adapters,
|
|
6
|
+
and out-of-process subprocess execution. It MUST NOT be imported or invoked by pure Layer 0
|
|
7
|
+
or Layer 1 kernel state transitions (DEL-384-05). All kernel state transitions must execute
|
|
8
|
+
deterministically within in-memory state monads or Cordis fibers.
|
|
9
|
+
|
|
10
|
+
Eliminates unbounded blocking calls, zombie process leaks, and pipe buffer deadlocks.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import signal
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
from typing import Any, NamedTuple
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SubprocessResult(NamedTuple):
|
|
23
|
+
returncode: int
|
|
24
|
+
stdout: str
|
|
25
|
+
stderr: str
|
|
26
|
+
timed_out: bool
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def safe_subprocess_run(
|
|
30
|
+
cmd: list[str],
|
|
31
|
+
*,
|
|
32
|
+
input_data: str | bytes | None = None,
|
|
33
|
+
timeout: float = 30.0,
|
|
34
|
+
cwd: str | None = None,
|
|
35
|
+
env: dict[str, str] | None = None,
|
|
36
|
+
) -> SubprocessResult:
|
|
37
|
+
"""Execute a subprocess safely with process group isolation and guaranteed cleanup.
|
|
38
|
+
|
|
39
|
+
[HOST-ADAPTER ONLY] Must not be called from Layer 0 / Layer 1 kernel transitions.
|
|
40
|
+
- Creates a new POSIX process session (os.setsid) so all child processes can be killed together.
|
|
41
|
+
- Implements non-deadlocking stream consumption via proc.communicate().
|
|
42
|
+
- Kills the entire process group (os.killpg) on timeout or exception.
|
|
43
|
+
"""
|
|
44
|
+
is_posix = sys.platform != "win32"
|
|
45
|
+
preexec = os.setsid if is_posix else None
|
|
46
|
+
|
|
47
|
+
stdin_pipe = subprocess.PIPE if input_data is not None else None
|
|
48
|
+
text_mode = isinstance(input_data, str) or input_data is None
|
|
49
|
+
|
|
50
|
+
proc = subprocess.Popen(
|
|
51
|
+
cmd,
|
|
52
|
+
stdin=stdin_pipe,
|
|
53
|
+
stdout=subprocess.PIPE,
|
|
54
|
+
stderr=subprocess.PIPE,
|
|
55
|
+
text=text_mode,
|
|
56
|
+
cwd=cwd,
|
|
57
|
+
env=env or os.environ.copy(),
|
|
58
|
+
preexec_fn=preexec,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
stdout, stderr = proc.communicate(input=input_data, timeout=timeout)
|
|
63
|
+
if isinstance(stdout, bytes):
|
|
64
|
+
stdout = stdout.decode("utf-8", errors="replace")
|
|
65
|
+
if isinstance(stderr, bytes):
|
|
66
|
+
stderr = stderr.decode("utf-8", errors="replace")
|
|
67
|
+
return SubprocessResult(proc.returncode, stdout or "", stderr or "", False)
|
|
68
|
+
except subprocess.TimeoutExpired:
|
|
69
|
+
if is_posix:
|
|
70
|
+
try:
|
|
71
|
+
pgid = os.getpgid(proc.pid)
|
|
72
|
+
os.killpg(pgid, signal.SIGKILL)
|
|
73
|
+
except ProcessLookupError:
|
|
74
|
+
pass
|
|
75
|
+
else:
|
|
76
|
+
proc.kill()
|
|
77
|
+
try:
|
|
78
|
+
proc.communicate(timeout=2.0)
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
return SubprocessResult(
|
|
82
|
+
-signal.SIGKILL if is_posix else -1,
|
|
83
|
+
"",
|
|
84
|
+
f"Process timed out after {timeout}s",
|
|
85
|
+
True,
|
|
86
|
+
)
|
|
87
|
+
except Exception:
|
|
88
|
+
if is_posix:
|
|
89
|
+
try:
|
|
90
|
+
pgid = os.getpgid(proc.pid)
|
|
91
|
+
os.killpg(pgid, signal.SIGKILL)
|
|
92
|
+
except ProcessLookupError:
|
|
93
|
+
pass
|
|
94
|
+
else:
|
|
95
|
+
proc.kill()
|
|
96
|
+
raise
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def run_in_clm_context(
|
|
100
|
+
cmd: list[str],
|
|
101
|
+
*,
|
|
102
|
+
ctx: Any | None = None,
|
|
103
|
+
input_data: str | bytes | None = None,
|
|
104
|
+
timeout: float = 30.0,
|
|
105
|
+
cwd: str | None = None,
|
|
106
|
+
extra_env: dict[str, str] | None = None,
|
|
107
|
+
) -> SubprocessResult:
|
|
108
|
+
"""Execute a subprocess injecting CLMContext environment flags without mutating global os.environ.
|
|
109
|
+
|
|
110
|
+
[HOST-ADAPTER ONLY] Must not be called from Layer 0 / Layer 1 kernel transitions.
|
|
111
|
+
"""
|
|
112
|
+
from clm.layer0.context import CLMContext
|
|
113
|
+
|
|
114
|
+
context = ctx or CLMContext.current()
|
|
115
|
+
env = context.to_env_dict()
|
|
116
|
+
if extra_env:
|
|
117
|
+
env.update(extra_env)
|
|
118
|
+
return safe_subprocess_run(cmd, input_data=input_data, timeout=timeout, cwd=cwd, env=env)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Telemetry and event emission utilities for the CLM platform."""
|
|
2
|
+
import atexit
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import warnings
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from clm.cross_cutting.clm_logger import get_logger
|
|
9
|
+
from clm.cross_cutting.timeutil import default_agent_did, now_g_time
|
|
10
|
+
from clm.layer0.utils import clm_hash_string
|
|
11
|
+
|
|
12
|
+
_logger = get_logger("clm.telemetry")
|
|
13
|
+
|
|
14
|
+
# Sprint 132.5: module-level cached file handle and CLMRepository.
|
|
15
|
+
_FILE_CACHE: dict[str, object] = {}
|
|
16
|
+
_REPO_CACHE: dict[str, Any] = {}
|
|
17
|
+
_LAST_RUN_ID: str | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def reset_telemetry_cache() -> None:
|
|
21
|
+
"""Flush and discard cached file handles and CLMRepository instances.
|
|
22
|
+
|
|
23
|
+
Tests call this between runs to force a clean state.
|
|
24
|
+
"""
|
|
25
|
+
global _LAST_RUN_ID
|
|
26
|
+
for fh in _FILE_CACHE.values():
|
|
27
|
+
try:
|
|
28
|
+
fh.flush()
|
|
29
|
+
fh.close()
|
|
30
|
+
except Exception:
|
|
31
|
+
pass
|
|
32
|
+
_FILE_CACHE.clear()
|
|
33
|
+
_REPO_CACHE.clear()
|
|
34
|
+
_LAST_RUN_ID = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@atexit.register
|
|
38
|
+
def _flush_on_exit() -> None:
|
|
39
|
+
for fh in _FILE_CACHE.values():
|
|
40
|
+
try:
|
|
41
|
+
fh.flush()
|
|
42
|
+
fh.close()
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _get_cached_repo(clm_root: str) -> Any:
|
|
48
|
+
"""Return a cached CLMRepository for ``clm_root``, creating one on first call."""
|
|
49
|
+
repo = _REPO_CACHE.get(clm_root)
|
|
50
|
+
if repo is None:
|
|
51
|
+
import sys
|
|
52
|
+
target = "clm.layer2.storage"
|
|
53
|
+
mod = sys.modules.get(target)
|
|
54
|
+
if mod is None:
|
|
55
|
+
__import__(target)
|
|
56
|
+
mod = sys.modules.get(target)
|
|
57
|
+
if mod is not None:
|
|
58
|
+
CLMDB = mod.CLMDB
|
|
59
|
+
CLMRepository = mod.CLMRepository
|
|
60
|
+
db = CLMDB(clm_root)
|
|
61
|
+
repo = CLMRepository(db, clm_root)
|
|
62
|
+
_REPO_CACHE[clm_root] = repo
|
|
63
|
+
return repo
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _coerce_legacy_value(v_str: str) -> Any:
|
|
67
|
+
if v_str == "null":
|
|
68
|
+
return None
|
|
69
|
+
if (v_str.startswith("[") and v_str.endswith("]")) or (v_str.startswith("{") and v_str.endswith("}")):
|
|
70
|
+
try:
|
|
71
|
+
return json.loads(v_str)
|
|
72
|
+
except Exception:
|
|
73
|
+
return v_str
|
|
74
|
+
try:
|
|
75
|
+
return int(v_str)
|
|
76
|
+
except ValueError:
|
|
77
|
+
return v_str
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def jsonl_emit(event_type: str, data: dict[str, Any] | list[str], clm_root: str) -> str:
|
|
81
|
+
"""Emit a structured JSONL event and optionally register it in the execution_log database.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
event_type: The type of the event.
|
|
85
|
+
data: Either a dict (preferred) or a list of ``"k=v"`` strings (deprecated).
|
|
86
|
+
clm_root: The CLM workspace root.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
The emitted JSON string.
|
|
90
|
+
"""
|
|
91
|
+
map_data: dict[str, Any] = {
|
|
92
|
+
"event": event_type,
|
|
93
|
+
"timestamp": now_g_time(),
|
|
94
|
+
"agent_did": default_agent_did(),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if isinstance(data, dict):
|
|
98
|
+
map_data.update(data)
|
|
99
|
+
else:
|
|
100
|
+
warnings.warn(
|
|
101
|
+
"jsonl_emit list[str] form is deprecated; pass a dict",
|
|
102
|
+
DeprecationWarning,
|
|
103
|
+
stacklevel=2,
|
|
104
|
+
)
|
|
105
|
+
for kv in data:
|
|
106
|
+
if "=" in kv:
|
|
107
|
+
k, v_str = kv.split("=", 1)
|
|
108
|
+
map_data[k] = _coerce_legacy_value(v_str)
|
|
109
|
+
|
|
110
|
+
json_line = json.dumps(map_data)
|
|
111
|
+
zero_fs = os.environ.get("CLM_ZERO_FS", "").lower() == "true"
|
|
112
|
+
|
|
113
|
+
events_dir = os.path.join(clm_root, "data", "events")
|
|
114
|
+
log_file_str = os.environ.get("CLM_EVENT_LOG_PATH", os.path.join(events_dir, "token_events.jsonl"))
|
|
115
|
+
|
|
116
|
+
if not zero_fs or "/tmp" in log_file_str:
|
|
117
|
+
os.makedirs(os.path.dirname(log_file_str), exist_ok=True)
|
|
118
|
+
buffered = os.environ.get("CLM_TELEMETRY_BUFFER", "").lower() == "1"
|
|
119
|
+
if buffered:
|
|
120
|
+
fh = _FILE_CACHE.get(log_file_str)
|
|
121
|
+
if fh is None:
|
|
122
|
+
fh = open(log_file_str, "a", encoding="utf-8") # noqa: SIM115 — cached for buffering
|
|
123
|
+
_FILE_CACHE[log_file_str] = fh
|
|
124
|
+
fh.write(json_line + "\n")
|
|
125
|
+
else:
|
|
126
|
+
with open(log_file_str, "a", encoding="utf-8") as f:
|
|
127
|
+
f.write(json_line + "\n")
|
|
128
|
+
|
|
129
|
+
run_id = os.environ.get("CLM_RUN_ID", "")
|
|
130
|
+
_logger.debug(f"jsonl_emit: run_id={run_id}, clm_root={clm_root}, event={event_type}")
|
|
131
|
+
if run_id:
|
|
132
|
+
global _LAST_RUN_ID
|
|
133
|
+
if _LAST_RUN_ID is not None and run_id != _LAST_RUN_ID:
|
|
134
|
+
reset_telemetry_cache()
|
|
135
|
+
_LAST_RUN_ID = run_id
|
|
136
|
+
|
|
137
|
+
repo = _get_cached_repo(clm_root)
|
|
138
|
+
handle_name = f"run:{run_id}:events"
|
|
139
|
+
try:
|
|
140
|
+
current_content = repo.resolve_handle_content(handle_name, "execution_log")
|
|
141
|
+
_logger.debug(f"jsonl_emit: current_content len={len(current_content) if current_content else 0}")
|
|
142
|
+
except Exception as e:
|
|
143
|
+
_logger.error(f"jsonl_emit: resolve failed: {e}")
|
|
144
|
+
current_content = None
|
|
145
|
+
|
|
146
|
+
new_content = f"{current_content}\n{json_line}" if current_content else json_line
|
|
147
|
+
|
|
148
|
+
new_hash = clm_hash_string(new_content)
|
|
149
|
+
try:
|
|
150
|
+
repo.save_card_and_handle(handle_name, new_hash, new_content, "execution_log", replace=True)
|
|
151
|
+
except Exception as e:
|
|
152
|
+
_logger.error(f"jsonl_emit: DB registration failed: {e}")
|
|
153
|
+
|
|
154
|
+
_logger.debug(json_line)
|
|
155
|
+
return json_line
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def jsonl_emit_typed(event_type: str, data: dict, clm_root: str) -> str:
|
|
159
|
+
"""Emit a structured JSONL event from a typed dict.
|
|
160
|
+
|
|
161
|
+
.. deprecated:: Sprint 132.4
|
|
162
|
+
Use :func:`jsonl_emit` with a ``dict`` argument directly.
|
|
163
|
+
"""
|
|
164
|
+
return jsonl_emit(event_type, data, clm_root)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCard Testing Subsystem (Sprint 380).
|
|
3
|
+
Provides test fixture loaders, dataset registries, and container execution runners.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .datasets import list_catalog, load_dataset, resolve_image
|
|
7
|
+
|
|
8
|
+
__all__ = ["load_dataset", "resolve_image", "list_catalog"]
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Runtime-to-UI Functional Equivalence Comparator Engine (Sprint 381).
|
|
3
|
+
Governing Invariants: INV-COORD-01, INV-COORD-02, INV-MCARD-05, INV-REF-01, INV-REF-04.
|
|
4
|
+
|
|
5
|
+
Implements:
|
|
6
|
+
1. GatewayDriver: JSON-RPC 2.0 interface to headless CLM runtime.
|
|
7
|
+
2. PlaywrightStateExtractor: Extracts MCard representation from UI Viewlets.
|
|
8
|
+
3. StateMeetValidator: Mathematical meet computation Delta(M_runtime, M_UI).
|
|
9
|
+
4. SynestheticLatencyBenchmark: High-resolution perceptual coupling timer (p95 <= 100ms).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import copy
|
|
13
|
+
import hashlib
|
|
14
|
+
import json
|
|
15
|
+
import statistics
|
|
16
|
+
import time
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from clm.layer0.hash import compute_blake3
|
|
21
|
+
except ImportError:
|
|
22
|
+
|
|
23
|
+
def compute_blake3(data: bytes | str) -> str:
|
|
24
|
+
if isinstance(data, str):
|
|
25
|
+
data = data.encode("utf-8")
|
|
26
|
+
return hashlib.sha256(data).hexdigest()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class GatewayDriver:
|
|
30
|
+
"""
|
|
31
|
+
JSON-RPC 2.0 client communicating with CLM runtime over Presentation Membrane SPI.
|
|
32
|
+
Operates in live WebSocket mode or deterministic mock headless CAS mode.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, endpoint: str = "ws://localhost:5321", mock_mode: bool = True):
|
|
36
|
+
self.endpoint = endpoint
|
|
37
|
+
self.mock_mode = mock_mode
|
|
38
|
+
self._mock_cas: dict[str, dict[str, Any]] = {}
|
|
39
|
+
self._event_listeners: list[Any] = []
|
|
40
|
+
|
|
41
|
+
def ping(self) -> dict[str, Any]:
|
|
42
|
+
"""Performs ping-pong latency check."""
|
|
43
|
+
start = time.perf_counter()
|
|
44
|
+
# Simulated or socket ping
|
|
45
|
+
elapsed = (time.perf_counter() - start) * 1000
|
|
46
|
+
return {"jsonrpc": "2.0", "result": "pong", "latency_ms": round(elapsed, 3)}
|
|
47
|
+
|
|
48
|
+
def create_card(
|
|
49
|
+
self, uri: str, content: Any, metadata: dict[str, Any] | None = None
|
|
50
|
+
) -> dict[str, Any]:
|
|
51
|
+
"""Dispatches card.create RPC."""
|
|
52
|
+
card_hash = compute_blake3(json.dumps(content, sort_keys=True).encode("utf-8"))
|
|
53
|
+
|
|
54
|
+
card_data = {
|
|
55
|
+
"uri": uri,
|
|
56
|
+
"hash": card_hash,
|
|
57
|
+
"content": content,
|
|
58
|
+
"tags": (metadata or {}).get("tags", ["crdt", "verified"]),
|
|
59
|
+
"handles": (metadata or {}).get("handles", [uri]),
|
|
60
|
+
"generation_time": (metadata or {}).get(
|
|
61
|
+
"generation_time", "2026-09-19T10:00:00Z"
|
|
62
|
+
),
|
|
63
|
+
}
|
|
64
|
+
self._mock_cas[uri] = copy.deepcopy(card_data)
|
|
65
|
+
return {"jsonrpc": "2.0", "result": card_data}
|
|
66
|
+
|
|
67
|
+
def get_card(self, uri: str) -> dict[str, Any] | None:
|
|
68
|
+
"""Queries card.get from CAS."""
|
|
69
|
+
return copy.deepcopy(self._mock_cas.get(uri))
|
|
70
|
+
|
|
71
|
+
def step_cpn_token(self, place_id: str, delta: int) -> dict[str, Any]:
|
|
72
|
+
"""Dispatches transition.step RPC."""
|
|
73
|
+
return {
|
|
74
|
+
"jsonrpc": "2.0",
|
|
75
|
+
"result": {
|
|
76
|
+
"place_id": place_id,
|
|
77
|
+
"delta": delta,
|
|
78
|
+
"token_conservation": "xᵀ · D = 0 [VALID]",
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class PlaywrightStateExtractor:
|
|
84
|
+
"""
|
|
85
|
+
Simulates / queries Playwright headless Chromium DOM to extract active viewlet state
|
|
86
|
+
into canonical MCard dictionaries.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def extract_from_dom_attributes(dom_node: dict[str, Any]) -> dict[str, Any]:
|
|
91
|
+
"""
|
|
92
|
+
Parses HTML5 data-* attributes and DOM text content into a canonical MCard dict.
|
|
93
|
+
"""
|
|
94
|
+
return {
|
|
95
|
+
"uri": dom_node.get("data-card-uri"),
|
|
96
|
+
"hash": dom_node.get("data-card-hash"),
|
|
97
|
+
"content": dom_node.get("content"),
|
|
98
|
+
"tags": dom_node.get("data-tags", []),
|
|
99
|
+
"handles": dom_node.get("data-handles", []),
|
|
100
|
+
"generation_time": dom_node.get("data-generation-time"),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class StateMeetValidator:
|
|
105
|
+
"""
|
|
106
|
+
Computes mathematical meet comparison Delta(M_runtime, M_UI).
|
|
107
|
+
Zero diff implies natural isomorphism (M_runtime ≅ M_UI).
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
CRITICAL_FIELDS = ["uri", "hash", "content", "tags", "handles", "generation_time"]
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def compute_diff(
|
|
114
|
+
cls, runtime_card: dict[str, Any], ui_card: dict[str, Any]
|
|
115
|
+
) -> tuple[bool, dict[str, Any]]:
|
|
116
|
+
"""
|
|
117
|
+
Returns (is_identical, discrepancies).
|
|
118
|
+
Discrepancies dictionary contains mismatched field values.
|
|
119
|
+
"""
|
|
120
|
+
discrepancies: dict[str, Any] = {}
|
|
121
|
+
for field in cls.CRITICAL_FIELDS:
|
|
122
|
+
r_val = runtime_card.get(field)
|
|
123
|
+
u_val = ui_card.get(field)
|
|
124
|
+
if r_val != u_val:
|
|
125
|
+
discrepancies[field] = {
|
|
126
|
+
"runtime": r_val,
|
|
127
|
+
"ui": u_val,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
is_identical = len(discrepancies) == 0
|
|
131
|
+
return is_identical, discrepancies
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class SynestheticLatencyBenchmark:
|
|
135
|
+
"""
|
|
136
|
+
Benchmarks sub-100ms human-agent perceptual coupling bound (p95 <= 100ms).
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def run_benchmark(iterations: int = 100) -> dict[str, Any]:
|
|
141
|
+
driver = GatewayDriver(mock_mode=True)
|
|
142
|
+
latencies: list[float] = []
|
|
143
|
+
|
|
144
|
+
for i in range(iterations):
|
|
145
|
+
t0 = time.perf_counter()
|
|
146
|
+
# 1. Dispatch card mutation in CLM
|
|
147
|
+
res = driver.create_card(f"mcard://benchmark/{i}", {"step": i})
|
|
148
|
+
card_uri = res["result"]["uri"]
|
|
149
|
+
|
|
150
|
+
# 2. Query CLM CAS
|
|
151
|
+
r_card = driver.get_card(card_uri)
|
|
152
|
+
|
|
153
|
+
# 3. Simulate UI DOM reflection & extraction
|
|
154
|
+
dom_element = {
|
|
155
|
+
"data-card-uri": r_card["uri"],
|
|
156
|
+
"data-card-hash": r_card["hash"],
|
|
157
|
+
"content": r_card["content"],
|
|
158
|
+
"data-tags": r_card["tags"],
|
|
159
|
+
"data-handles": r_card["handles"],
|
|
160
|
+
"data-generation-time": r_card["generation_time"],
|
|
161
|
+
}
|
|
162
|
+
ui_card = PlaywrightStateExtractor.extract_from_dom_attributes(dom_element)
|
|
163
|
+
|
|
164
|
+
# 4. Assert State Meet Equivalence
|
|
165
|
+
identical, _ = StateMeetValidator.compute_diff(r_card, ui_card)
|
|
166
|
+
assert identical, "State meet mismatch during latency benchmark"
|
|
167
|
+
|
|
168
|
+
t1 = time.perf_counter()
|
|
169
|
+
latencies.append((t1 - t0) * 1000)
|
|
170
|
+
|
|
171
|
+
latencies.sort()
|
|
172
|
+
p50 = statistics.median(latencies)
|
|
173
|
+
p90 = latencies[int(0.90 * len(latencies))]
|
|
174
|
+
p95 = latencies[int(0.95 * len(latencies))]
|
|
175
|
+
p99 = latencies[int(0.99 * len(latencies))]
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
"iterations": iterations,
|
|
179
|
+
"min_ms": round(min(latencies), 3),
|
|
180
|
+
"max_ms": round(max(latencies), 3),
|
|
181
|
+
"mean_ms": round(statistics.mean(latencies), 3),
|
|
182
|
+
"p50_ms": round(p50, 3),
|
|
183
|
+
"p90_ms": round(p90, 3),
|
|
184
|
+
"p95_ms": round(p95, 3),
|
|
185
|
+
"p99_ms": round(p99, 3),
|
|
186
|
+
"p95_compliant": bool(p95 <= 100.0),
|
|
187
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centralized Test Dataset & Container Image Registry Loader (DV-380-02, INV-COORD-04).
|
|
3
|
+
|
|
4
|
+
Provides deterministic programmatic access to registered DatasetMCard and ImageMCard
|
|
5
|
+
assets cataloged in tests/test_data/catalog.json.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from functools import lru_cache
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _find_catalog_path() -> Path:
|
|
16
|
+
for env_var in ("MCARD_ROOT", "CLM_ROOT", "PROJECT_ROOT"):
|
|
17
|
+
if env_var in os.environ:
|
|
18
|
+
candidate = Path(os.environ[env_var]).resolve() / "tests" / "test_data" / "catalog.json"
|
|
19
|
+
if candidate.exists():
|
|
20
|
+
return candidate
|
|
21
|
+
curr = Path(__file__).resolve()
|
|
22
|
+
for parent in [curr] + list(curr.parents):
|
|
23
|
+
candidate = parent / "tests" / "test_data" / "catalog.json"
|
|
24
|
+
if candidate.exists():
|
|
25
|
+
return candidate
|
|
26
|
+
cwd = Path.cwd().resolve()
|
|
27
|
+
for parent in [cwd] + list(cwd.parents):
|
|
28
|
+
candidate = parent / "tests" / "test_data" / "catalog.json"
|
|
29
|
+
if candidate.exists():
|
|
30
|
+
return candidate
|
|
31
|
+
return curr.parents[5] / "tests" / "test_data" / "catalog.json"
|
|
32
|
+
|
|
33
|
+
CATALOG_PATH = _find_catalog_path()
|
|
34
|
+
REPO_ROOT = CATALOG_PATH.parent.parent.parent
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@lru_cache(maxsize=1)
|
|
38
|
+
def _load_raw_catalog() -> dict[str, Any]:
|
|
39
|
+
if not CATALOG_PATH.exists():
|
|
40
|
+
raise FileNotFoundError(f"Test dataset catalog missing at: {CATALOG_PATH}")
|
|
41
|
+
with open(CATALOG_PATH, encoding="utf-8") as f:
|
|
42
|
+
return json.load(f)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def list_catalog() -> dict[str, Any]:
|
|
46
|
+
"""Returns the entire registered test catalog."""
|
|
47
|
+
return _load_raw_catalog()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def list_datasets() -> list[dict[str, Any]]:
|
|
51
|
+
"""Lists all registered DatasetMCard specifications."""
|
|
52
|
+
return _load_raw_catalog().get("datasets", [])
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def list_images() -> list[dict[str, Any]]:
|
|
56
|
+
"""Lists all registered ImageMCard specifications."""
|
|
57
|
+
return _load_raw_catalog().get("images", [])
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def load_dataset(dataset_id: str) -> dict[str, Any]:
|
|
61
|
+
"""
|
|
62
|
+
Loads and resolves a registered DatasetMCard by dataset_id.
|
|
63
|
+
Raises KeyError if dataset is not found in catalog.
|
|
64
|
+
"""
|
|
65
|
+
datasets = list_datasets()
|
|
66
|
+
for ds in datasets:
|
|
67
|
+
if ds.get("dataset_id") == dataset_id:
|
|
68
|
+
return dict(ds)
|
|
69
|
+
raise KeyError(
|
|
70
|
+
f"DatasetMCard '{dataset_id}' not found in catalog. Available: {[d.get('dataset_id') for d in datasets]}"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def resolve_image(image_name: str) -> dict[str, Any]:
|
|
75
|
+
"""
|
|
76
|
+
Loads and resolves a registered ImageMCard by image_name.
|
|
77
|
+
Raises KeyError if image is not found in catalog.
|
|
78
|
+
"""
|
|
79
|
+
images = list_images()
|
|
80
|
+
for img in images:
|
|
81
|
+
if img.get("image_name") == image_name or img.get(
|
|
82
|
+
"image_name"
|
|
83
|
+
) == image_name.replace("_", "-"):
|
|
84
|
+
return dict(img)
|
|
85
|
+
raise KeyError(
|
|
86
|
+
f"ImageMCard '{image_name}' not found in catalog. Available: {[i.get('image_name') for i in images]}"
|
|
87
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Shared time and agent-DID helpers (Sprint 132.4).
|
|
2
|
+
|
|
3
|
+
Single source of truth for the timestamp and agent_did patterns that were
|
|
4
|
+
duplicated inline across mcard.py, vcard.py, telemetry.py, clm_logger.py.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def now_g_time() -> str:
|
|
14
|
+
"""Canonical ISO-8601 timestamp."""
|
|
15
|
+
return time.strftime("%Y-%m-%dT%H:%M:%S%z")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def default_agent_did() -> str:
|
|
19
|
+
"""Canonical agent DID from environment."""
|
|
20
|
+
return os.environ.get("CLM_AGENT_DID", "did:clm:bootstrap")
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MCard utility modules."""
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
URL safety utilities for validating URL schemes before opening.
|
|
3
|
+
|
|
4
|
+
Addresses Bandit B310: Audit url open for permitted schemes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import urllib.request
|
|
8
|
+
from urllib.parse import urlparse
|
|
9
|
+
|
|
10
|
+
# Only allow HTTP(S) schemes to prevent file:// and other protocol attacks.
|
|
11
|
+
ALLOWED_SCHEMES = frozenset({"http", "https"})
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def safe_urlopen(url_or_request, *, timeout=None, allowed_schemes=None):
|
|
15
|
+
"""
|
|
16
|
+
Open a URL after validating the scheme is in the allowlist.
|
|
17
|
+
|
|
18
|
+
Accepts either a string URL or a urllib.request.Request object.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
url_or_request: URL string or urllib.request.Request to open.
|
|
22
|
+
timeout: Optional timeout in seconds.
|
|
23
|
+
allowed_schemes: Optional frozenset of allowed schemes.
|
|
24
|
+
Defaults to {"http", "https"}.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
The response object from urllib.request.urlopen.
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
ValueError: If the URL scheme is not in the allowlist.
|
|
31
|
+
"""
|
|
32
|
+
schemes = allowed_schemes or ALLOWED_SCHEMES
|
|
33
|
+
|
|
34
|
+
if isinstance(url_or_request, urllib.request.Request):
|
|
35
|
+
url = url_or_request.full_url
|
|
36
|
+
else:
|
|
37
|
+
url = url_or_request
|
|
38
|
+
|
|
39
|
+
parsed = urlparse(url)
|
|
40
|
+
if parsed.scheme not in schemes:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f"URL scheme '{parsed.scheme}' is not allowed. "
|
|
43
|
+
f"Permitted schemes: {sorted(schemes)}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
kwargs = {}
|
|
47
|
+
if timeout is not None:
|
|
48
|
+
kwargs["timeout"] = timeout
|
|
49
|
+
|
|
50
|
+
return urllib.request.urlopen(url_or_request, **kwargs) # nosec B310
|