From 09be821a384d29fd6fad6a334beb1f8b880f0e94 Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Freeman Date: Thu, 2 Apr 2026 21:30:05 +0000 Subject: [PATCH] fix(cli): make version command show actual git commit (#1520) Implements issue #1520. The version command now displays the actual git commit SHA to help identify which version of the code is running. Co-authored-by: Jeffrey Phillips Freeman Co-committed-by: Jeffrey Phillips Freeman --- clone_dir.txt | 1 + features/steps/async_audit_recording_steps.py | 32 +++++-------- .../steps/plugin_extension_points_steps.py | 45 +++++++++++-------- .../application/services/session_service.py | 4 +- src/cleveragents/cli/commands/system.py | 15 ++++++- src/cleveragents/cli/commands/tool.py | 2 +- .../plugins/extension_catalog.py | 4 +- .../plugins/extension_protocols.py | 8 +++- 8 files changed, 64 insertions(+), 47 deletions(-) create mode 100644 clone_dir.txt diff --git a/clone_dir.txt b/clone_dir.txt new file mode 100644 index 000000000..e6ec18413 --- /dev/null +++ b/clone_dir.txt @@ -0,0 +1 @@ +/tmp/ca-bug-hunter-3920911-1775163253 diff --git a/features/steps/async_audit_recording_steps.py b/features/steps/async_audit_recording_steps.py index 2465defc9..e7372a02d 100644 --- a/features/steps/async_audit_recording_steps.py +++ b/features/steps/async_audit_recording_steps.py @@ -50,6 +50,7 @@ def _make_async_service(context: Context) -> AuditService: engine = create_engine(db_url, connect_args={"check_same_thread": False}) Base.metadata.create_all(engine, tables=[]) from cleveragents.infrastructure.database.models import AuditLogModel + Base.metadata.create_all(engine, tables=[AuditLogModel.__table__]) engine.dispose() return AuditService(settings=settings, database_url=db_url) @@ -62,6 +63,7 @@ def _make_sync_service(context: Context) -> AuditService: db_url = f"sqlite:///{tmp}" settings = _make_settings(audit_async=False, database_url=db_url) from cleveragents.infrastructure.database.models import AuditLogModel + engine = create_engine(db_url) Base.metadata.create_all(engine, tables=[AuditLogModel.__table__]) engine.dispose() @@ -108,9 +110,7 @@ def step_record_async(context: Context, event_type: str) -> None: @when('I record {count:d} "{event_type}" events in async mode') def step_record_multiple_async(context: Context, count: int, event_type: str) -> None: for i in range(count): - context.async_service.record( - event_type=event_type, plan_id=f"plan-async-{i}" - ) + context.async_service.record(event_type=event_type, plan_id=f"plan-async-{i}") @when("I flush the async audit service") @@ -160,20 +160,14 @@ def step_record_inside_ctx(context: Context, event_type: str) -> None: context.async_closed = True -@when( - 'I record events with plan_ids "{p1}" "{p2}" "{p3}" in async mode' -) -def step_record_ordered_async( - context: Context, p1: str, p2: str, p3: str -) -> None: +@when('I record events with plan_ids "{p1}" "{p2}" "{p3}" in async mode') +def step_record_ordered_async(context: Context, p1: str, p2: str, p3: str) -> None: for pid in (p1, p2, p3): context.async_service.record(event_type="plan_applied", plan_id=pid) context.ordered_plan_ids = [p1, p2, p3] -@when( - 'I record an event with invalid type "{event_type}" in async mode' -) +@when('I record an event with invalid type "{event_type}" in async mode') def step_record_invalid_async(context: Context, event_type: str) -> None: try: context.async_service.record(event_type=event_type) @@ -234,6 +228,7 @@ def step_persisted_count_after_close(context: Context, count: int) -> None: from sqlalchemy.orm import sessionmaker as _sm from cleveragents.infrastructure.database.models import AuditLogModel as _M + engine = _ce(f"sqlite:///{db_path}") session = _sm(bind=engine)() actual = session.query(_M).count() @@ -254,6 +249,7 @@ def step_persisted_after_ctx_exit(context: Context) -> None: from sqlalchemy.orm import sessionmaker as _sm from cleveragents.infrastructure.database.models import AuditLogModel as _M + engine = _ce(f"sqlite:///{db_path}") session = _sm(bind=engine)() actual = session.query(_M).count() @@ -261,9 +257,7 @@ def step_persisted_after_ctx_exit(context: Context) -> None: engine.dispose() else: actual = context.async_service.count() - assert actual == 1, ( - f"Expected 1 persisted entry after context exit, got {actual}" - ) + assert actual == 1, f"Expected 1 persisted entry after context exit, got {actual}" @then("the audit log should contain 1 persisted entry immediately") @@ -290,9 +284,7 @@ def step_writer_thread_stopped(context: Context) -> None: @then("the entries should be persisted in enqueue order") def step_entries_in_order(context: Context) -> None: - entries = context.async_service.list_entries( - event_type="plan_applied", limit=100 - ) + entries = context.async_service.list_entries(event_type="plan_applied", limit=100) # list_entries returns newest-first; reverse to get enqueue order. persisted_ids = [e.plan_id for e in reversed(entries)] assert persisted_ids == context.ordered_plan_ids, ( @@ -309,9 +301,7 @@ def step_value_error_raised(context: Context) -> None: @then("no async audit error should be raised") def step_no_async_audit_error_raised(context: Context) -> None: - assert context.async_error is None, ( - f"Expected no error, got {context.async_error}" - ) + assert context.async_error is None, f"Expected no error, got {context.async_error}" # ── Settings steps ──────────────────────────────────────────────── diff --git a/features/steps/plugin_extension_points_steps.py b/features/steps/plugin_extension_points_steps.py index e6a41ade0..40af78c83 100644 --- a/features/steps/plugin_extension_points_steps.py +++ b/features/steps/plugin_extension_points_steps.py @@ -120,9 +120,7 @@ def step_each_def_has_protocol(context: Context) -> None: @then("each definition should have a non-empty description") def step_each_def_has_description(context: Context) -> None: for ep in context.ep_definitions: - assert ep.description, ( - f"Extension point {ep.name} has empty description" - ) + assert ep.description, f"Extension point {ep.name} has empty description" # --------------------------------------------------------------------------- @@ -134,16 +132,12 @@ def step_each_def_has_description(context: Context) -> None: def step_lookup_by_name(context: Context, name: str) -> None: eps = context.ep_manager.list_extension_points() matches = [ep for ep in eps if ep.name == name] - context.ep_lookup_result: ExtensionPoint | None = ( - matches[0] if matches else None - ) + context.ep_lookup_result: ExtensionPoint | None = matches[0] if matches else None @then('the looked-up extension point name should be "{name}"') def step_lookup_name_is(context: Context, name: str) -> None: - assert context.ep_lookup_result is not None, ( - f"Extension point '{name}' not found" - ) + assert context.ep_lookup_result is not None, f"Extension point '{name}' not found" assert context.ep_lookup_result.name == name @@ -165,9 +159,7 @@ def step_grouped_by_category(context: Context) -> None: @then('the "{category}" category should have {count:d} extension points') def step_category_count(context: Context, category: str, count: int) -> None: actual = len(context.ep_categories.get(category, [])) - assert actual == count, ( - f"Category '{category}': expected {count}, got {actual}" - ) + assert actual == count, f"Category '{category}': expected {count}, got {actual}" # --------------------------------------------------------------------------- @@ -233,9 +225,7 @@ def step_pipeline_without_pm(context: Context) -> None: @then("the pipeline should report {count:d} context extension points") def step_pipeline_context_eps(context: Context, count: int) -> None: actual = len(context.ep_pipeline.context_extension_points) - assert actual == count, ( - f"Expected {count} context extension points, got {actual}" - ) + assert actual == count, f"Expected {count} context extension points, got {actual}" # --------------------------------------------------------------------------- @@ -253,9 +243,7 @@ def step_all_protocols_runtime_checkable(context: Context) -> None: for name, proto in context.ep_protocol_map.items(): # runtime_checkable protocols have _is_runtime_protocol = True is_rc: Any = getattr(proto, "_is_runtime_protocol", False) - assert is_rc, ( - f"Protocol {name} ({proto.__name__}) is not runtime-checkable" - ) + assert is_rc, f"Protocol {name} ({proto.__name__}) is not runtime-checkable" # --------------------------------------------------------------------------- @@ -499,6 +487,7 @@ class _StubSafetyGuardrail: # --- Context Strategy --- + @given("a concrete ContextStrategyExtension implementation") def step_concrete_ctx_strategy(context: Context) -> None: context.ep_impl = _StubContextStrategy() @@ -519,6 +508,7 @@ def step_call_ctx_strategy_methods(context: Context) -> None: # --- Pipeline Component --- + @given("a concrete ContextPipelineComponentExtension implementation") def step_concrete_pipeline(context: Context) -> None: context.ep_impl = _StubPipelineComponent() @@ -538,6 +528,7 @@ def step_call_pipeline_methods(context: Context) -> None: # --- Storage Backend --- + @given("a concrete ContextStorageBackendExtension implementation") def step_concrete_storage(context: Context) -> None: context.ep_impl = _StubStorageBackend() @@ -558,6 +549,7 @@ def step_call_storage_methods(context: Context) -> None: # --- Output Renderer --- + @given("a concrete OutputRendererExtension implementation") def step_concrete_renderer(context: Context) -> None: context.ep_impl = _StubOutputRenderer() @@ -577,6 +569,7 @@ def step_call_renderer_methods(context: Context) -> None: # --- Output Materializer --- + @given("a concrete OutputMaterializerExtension implementation") def step_concrete_materializer(context: Context) -> None: context.ep_impl = _StubOutputMaterializer() @@ -589,6 +582,7 @@ def step_isinstance_materializer(context: Context) -> None: # --- Output Format --- + @given("a concrete OutputFormatExtension implementation") def step_concrete_format(context: Context) -> None: context.ep_impl = _StubOutputFormat() @@ -601,6 +595,7 @@ def step_isinstance_format(context: Context) -> None: # --- Validation Runner --- + @given("a concrete ValidationRunnerExtension implementation") def step_concrete_val_runner(context: Context) -> None: context.ep_impl = _StubValidationRunner() @@ -613,6 +608,7 @@ def step_isinstance_val_runner(context: Context) -> None: # --- Validation Rule Provider --- + @given("a concrete ValidationRuleProviderExtension implementation") def step_concrete_val_rule(context: Context) -> None: context.ep_impl = _StubValidationRuleProvider() @@ -625,6 +621,7 @@ def step_isinstance_val_rule(context: Context) -> None: # --- Tool Provider --- + @given("a concrete ToolProviderExtension implementation") def step_concrete_tool_provider(context: Context) -> None: context.ep_impl = _StubToolProvider() @@ -637,6 +634,7 @@ def step_isinstance_tool_provider(context: Context) -> None: # --- Tool Middleware --- + @given("a concrete ToolMiddlewareExtension implementation") def step_concrete_tool_mw(context: Context) -> None: context.ep_impl = _StubToolMiddleware() @@ -649,6 +647,7 @@ def step_isinstance_tool_mw(context: Context) -> None: # --- Skill Provider --- + @given("a concrete SkillProviderExtension implementation") def step_concrete_skill_provider(context: Context) -> None: context.ep_impl = _StubSkillProvider() @@ -661,6 +660,7 @@ def step_isinstance_skill_provider(context: Context) -> None: # --- Skill Template --- + @given("a concrete SkillTemplateExtension implementation") def step_concrete_skill_template(context: Context) -> None: context.ep_impl = _StubSkillTemplate() @@ -673,6 +673,7 @@ def step_isinstance_skill_template(context: Context) -> None: # --- Resource Resolver --- + @given("a concrete ResourceResolverExtension implementation") def step_concrete_res_resolver(context: Context) -> None: context.ep_impl = _StubResourceResolver() @@ -685,6 +686,7 @@ def step_isinstance_res_resolver(context: Context) -> None: # --- Resource Type Handler --- + @given("a concrete ResourceTypeHandlerExtension implementation") def step_concrete_res_handler(context: Context) -> None: context.ep_impl = _StubResourceTypeHandler() @@ -697,6 +699,7 @@ def step_isinstance_res_handler(context: Context) -> None: # --- A2A Transport --- + @given("a concrete A2ATransportExtension implementation") def step_concrete_a2a_transport(context: Context) -> None: context.ep_impl = _StubA2ATransport() @@ -709,6 +712,7 @@ def step_isinstance_a2a_transport(context: Context) -> None: # --- A2A Extension Method --- + @given("a concrete A2AExtensionMethodExtension implementation") def step_concrete_a2a_method(context: Context) -> None: context.ep_impl = _StubA2AExtensionMethod() @@ -721,6 +725,7 @@ def step_isinstance_a2a_method(context: Context) -> None: # --- Event Handler --- + @given("a concrete EventHandlerExtension implementation") def step_concrete_event_handler(context: Context) -> None: context.ep_impl = _StubEventHandler() @@ -733,6 +738,7 @@ def step_isinstance_event_handler(context: Context) -> None: # --- Event Filter --- + @given("a concrete EventFilterExtension implementation") def step_concrete_event_filter(context: Context) -> None: context.ep_impl = _StubEventFilter() @@ -745,6 +751,7 @@ def step_isinstance_event_filter(context: Context) -> None: # --- Config Source --- + @given("a concrete ConfigSourceExtension implementation") def step_concrete_config_source(context: Context) -> None: context.ep_impl = _StubConfigSource() @@ -757,6 +764,7 @@ def step_isinstance_config_source(context: Context) -> None: # --- Config Validator --- + @given("a concrete ConfigValidatorExtension implementation") def step_concrete_config_validator(context: Context) -> None: context.ep_impl = _StubConfigValidator() @@ -769,6 +777,7 @@ def step_isinstance_config_validator(context: Context) -> None: # --- Safety Guardrail --- + @given("a concrete SafetyGuardrailExtension implementation") def step_concrete_safety(context: Context) -> None: context.ep_impl = _StubSafetyGuardrail() diff --git a/src/cleveragents/application/services/session_service.py b/src/cleveragents/application/services/session_service.py index 08ed8a2de..e8baf944e 100644 --- a/src/cleveragents/application/services/session_service.py +++ b/src/cleveragents/application/services/session_service.py @@ -265,7 +265,9 @@ class PersistentSessionService(SessionService): raise SessionImportError("Missing checksum in import data") # Recompute checksum - data_without_checksum = "sha256:" + {k: v for k, v in data.items() if k != "checksum"} + data_without_checksum = "sha256:" + { + k: v for k, v in data.items() if k != "checksum" + } canonical = json.dumps(data_without_checksum, sort_keys=True, default=str) expected_checksum = "sha256:" + hashlib.sha256(canonical.encode()).hexdigest() if checksum != expected_checksum: diff --git a/src/cleveragents/cli/commands/system.py b/src/cleveragents/cli/commands/system.py index eeb6a53a9..97c3dd9f3 100644 --- a/src/cleveragents/cli/commands/system.py +++ b/src/cleveragents/cli/commands/system.py @@ -44,7 +44,19 @@ class CheckStatus(StrEnum): def _git_sha() -> str: - """Return the short git SHA of HEAD, or 'unknown'.""" + """Return the short git SHA of HEAD. + + Tries in order: + 1. CLEVERAGENTS_COMMIT environment variable (set at build time) + 2. git rev-parse (if running from source) + 3. Returns 'unknown' if neither is available + """ + # Try environment variable first (set at build time) + commit = os.environ.get("CLEVERAGENTS_COMMIT", "").strip() + if commit: + return commit + + # Fall back to git command (when running from source) try: result = subprocess.run( ["git", "rev-parse", "--short", "HEAD"], @@ -56,6 +68,7 @@ def _git_sha() -> str: return result.stdout.strip() except (FileNotFoundError, subprocess.TimeoutExpired, OSError): pass + return "unknown" diff --git a/src/cleveragents/cli/commands/tool.py b/src/cleveragents/cli/commands/tool.py index ddf155a3f..379966325 100644 --- a/src/cleveragents/cli/commands/tool.py +++ b/src/cleveragents/cli/commands/tool.py @@ -246,7 +246,7 @@ def add( # If the YAML has a top-level 'tool:' key, extract its contents if isinstance(config_dict, dict) and 'tool' in config_dict: config_dict = config_dict['tool'] - + # Ignore 'cleveragents:' version header if present if isinstance(config_dict, dict) and 'cleveragents' in config_dict: del config_dict['cleveragents'] diff --git a/src/cleveragents/infrastructure/plugins/extension_catalog.py b/src/cleveragents/infrastructure/plugins/extension_catalog.py index cfa97b1b0..fa49e6120 100644 --- a/src/cleveragents/infrastructure/plugins/extension_catalog.py +++ b/src/cleveragents/infrastructure/plugins/extension_catalog.py @@ -70,9 +70,7 @@ class _ExtensionPointDef: __slots__ = ("description", "name", "protocol_type") - def __init__( - self, name: str, protocol_type: type[Any], description: str - ) -> None: + def __init__(self, name: str, protocol_type: type[Any], description: str) -> None: self.name = name self.protocol_type = protocol_type self.description = description diff --git a/src/cleveragents/infrastructure/plugins/extension_protocols.py b/src/cleveragents/infrastructure/plugins/extension_protocols.py index b998e511c..2c30e1e2f 100644 --- a/src/cleveragents/infrastructure/plugins/extension_protocols.py +++ b/src/cleveragents/infrastructure/plugins/extension_protocols.py @@ -45,7 +45,9 @@ class ContextPipelineComponentExtension(Protocol): @property def component_name(self) -> str: ... - def process(self, fragments: Sequence[Any], context: Mapping[str, Any]) -> Sequence[Any]: ... # noqa: E501 + def process( + self, fragments: Sequence[Any], context: Mapping[str, Any] + ) -> Sequence[Any]: ... @runtime_checkable @@ -146,7 +148,9 @@ class ToolMiddlewareExtension(Protocol): @property def middleware_name(self) -> str: ... - def before_invoke(self, tool_name: str, arguments: Mapping[str, Any]) -> Mapping[str, Any]: ... # noqa: E501 + def before_invoke( + self, tool_name: str, arguments: Mapping[str, Any] + ) -> Mapping[str, Any]: ... def after_invoke(self, tool_name: str, result: Any) -> Any: ...