Skip to content

Commit a59b8d7

Browse files
emooreatxclaude
andauthored
Release 2.3.2: Mobile UX, Localization, and Test Automation (#655)
* Security fixes for SonarCloud blockers and version bump to 2.3.1 - Add _validate_config_path() to config.py to validate config paths are within allowed directories before file write operations - Sanitize all task_id, task.name, and defer_until values in scheduler service logs to prevent log injection (CWE-117) - Bump version to 2.3.1-stable (versionCode 77) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Document HTTPExceptions in FastAPI route responses - Add RESPONSES_400_404_500_503 and RESPONSES_400_500_503 combined patterns - Document 404 in cancel_scheduled_task endpoint - Use cleaner combined response patterns in scheduler and telemetry_export Fixes SonarCloud code smells about undocumented HTTPException status codes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add iOS Documents/ciris path to config allowlist iOS defaults to ~/Documents/ciris/.env when CIRIS_HOME is unset. The previous allowlist only included ~/ciris, blocking valid iOS installs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Patch _validate_config_path in LLM config tests Tests use tmp_path which isn't in the config path allowlist. Patch the validation function to pass through for test directories. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Remove unused exception variables in scheduler service When fixing log injection (CWE-117), removed {e} from log messages but left 'as e' in except clauses. Now using bare 'except Exception:'. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Remove f-string prefix from strings without replacements F-strings without {} are confusing and wasteful. Changed to regular strings. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix mobile credits display and add pre-flight credit check - Credits badge now shows daily free uses when paid/free credits are 0 - Added pre-flight credit check before sendMessage() to block send when no credits - Added user-friendly error message handling (no raw JSON shown to users) - Added credit renewal countdown (hours until midnight UTC) when exhausted - Added 7 new localized credit error strings in all 16 languages: - credits_no_credits, credits_no_credits_short, credits_purchase_required - credits_renews_at, credits_error_generic, credits_error_network, credits_error_server 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix SonarCloud scan action - use sonarcloud-github-action Switch from sonarqube-scan-action@v6 to sonarcloud-github-action@v5. The sonarqube-scan-action is for self-hosted SonarQube servers and was failing with 403 errors from binaries.sonarsource.com. Since we use SonarCloud, the sonarcloud-github-action is the correct choice. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix SonarCloud path injection by returning new validated Path SECURITY: The _validate_config_path() function now returns a NEW Path object constructed from validated components (allowed_parent / relative_path) instead of returning the original tainted path. This breaks the taint chain that SonarCloud tracks from user-controlled input to file operations. Before: _validate_config_path(path) returned the same tainted path After: _validate_config_path(path) returns Path(allowed_parent / relative_path) The caller now uses the returned validated path for file operations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix circular imports and improve code architecture - Extract Dialect and ConflictResolution enums to types.py to break dialect.py ↔ query_builder.py circular import - Move telemetry helper imports to top level in service.py (helpers.py only imports GraphTelemetryService under TYPE_CHECKING) - Extract startup logging utilities to startup_logging.py to break module_loader.py ↔ service_initializer.py circular import - Update all dependent imports to use new module locations Files added: - ciris_engine/logic/persistence/db/types.py - ciris_engine/logic/runtime/startup_logging.py 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Replace Dict[str, Any] with typed schemas in protocols (Phase 1) - authentication.py: verify_token_sync() now returns TokenVerification - scheduler.py: schedule_deferred_task() context param uses DeferralContext - wa_auth.py: create_oauth_wa() profile param uses OAuthUserProfile - authentication/service.py: Updated implementation to return TokenVerification Note: configurable.py handle_oauth_callback() intentionally kept as Dict[str, Any] because it's used for both OAuth tokens AND device auth status (different structures). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add typed schemas for LLM service state tracking Phase 2 of Dict[str, Any] cleanup - create typed schemas for internal data structures: - RetryState: Tracks retry count, previous errors, and request correlation - LLMRequestMetadata: Metadata for proxy requests with billing/tracing info - EndpointStats: Statistics for LLM endpoint monitoring Analysis found that adapter_configuration and discovery_service Dict[str, Any] usages are legitimate serialization edges (user input) and DI patterns (kwargs). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix mypy: restore ErrorContext type alias The ErrorContext type alias was accidentally removed during Phase 2 Dict[str, Any] cleanup. This is a legitimate use case for Dict[str, Any] since error context contains arbitrary debugging information for logging. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix test patches after startup_logging module extraction Update test patches to reference ciris_engine.logic.runtime.startup_logging instead of ciris_engine.logic.runtime.service_initializer for: - _services_started - _current_phase - _log_service_started These were moved to startup_logging.py to break the circular import between module_loader and service_initializer. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix language rotation not stopping after startup completes The rotation LaunchedEffect had a while(true) loop that never checked if startup completed. This caused the temporary rotation language to overwrite the user's preferred language (or English default) after resetToPersistedLanguage() was called. Fix: Add `phase` as a dependency and stop rotation when phase becomes READY or FIRST_RUN_SETUP. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix 4 failing tests: JWT verification + my_data timeouts 1. test_sync_token_verification: Updated to use TokenVerification object attributes instead of dict subscripts after return type changed from dict to Pydantic model. 2. test_my_data.py (3 tests): Fixed timeouts caused by: - Fixture name collision: renamed mock_runtime to my_data_mock_runtime to avoid conflict with conftest.py fixture - Signing key hang: added mock_signing_key_fallback fixture to prevent _compute_agent_id_hash_from_signer() from blocking while trying to initialize ciris_verify singleton in test environment Test suite: 10,303 passed, 47 skipped, 0 failed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add missing localization keys + location search API + lint tool - Add 58 missing mobile.settings_* localization keys visible in Settings screen - Fix CIRISApiClient: add HttpClient instances for searchLocations/getCountries - Add tools/check_kotlin_localizations.py to detect missing keys - Sync en.json to Android and Desktop localization directories 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Sync localization keys across all 16 languages - Remove extra keys not in en.json from am, es, fr, ja, tr, ur - Add missing startup/system/status sections to fr, ja - Sync all language files to mobile directories Test suite: 10,308 passed, 47 skipped 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add location search API to KMP protocol + desktop localizations - Add searchLocations() and getCountries() to CIRISApiClientProtocol - Add LocationResultData, LocationSearchResponse, CountryInfoData, CountriesResponse data models - Update SetupScreen, SetupState, SetupViewModel for location search - Sync desktop localization files with ur.json (Urdu) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add version display to BillingScreen + comprehensive help guide Features: - Add getAppVersion() and getAppBuildNumber() platform functions - Display version at bottom of BillingScreen (v2.3.1 (77)) - Create HELP_INDEX.md with complete UI/UX documentation: - 26 screens with purpose, features, and usage - Troubleshooting guide - Localization reference (16 languages) - Keyboard shortcuts Platform implementations: - Android: PackageManager with caching - iOS: NSBundle Info.plist - Desktop: JAR manifest with fallback constant 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix adapter wizard button visibility in setup wizard The "Configure Now" button wasn't appearing for adapters like Home Assistant that have interactive configuration (wizard) workflows during first-run setup. Root cause: Mobile app was incorrectly deriving requires_config from requiredEnvVars instead of the adapter's interactive_config manifest field. Changes: - Add requires_config and config_fields to backend AdapterConfig model - Add has_interactive_config to ModuleTypeInfo schema - Update adapter discovery to detect interactive_config from manifest - Update setup helpers to set requires_config from has_interactive_config - Update mobile API model with new fields - Update CIRISApiClient to use backend's requires_config value 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Complete localization (16 languages) + signing key preservation feature Localization: - Full UI translations for am, ar, de, es, fr, hi, it, ja, ko, pt, ru, sw, tr, ur, zh - Localized DMA prompts for all 16 languages - Localized conscience prompts for all 16 languages - ACCORD polyglot compression and summaries - Help guide translations - Credit strings and new mobile UI strings Signing Key Preservation (Data Management): - Add clearDataOnly() to EnvFileUpdater (Android/iOS/Desktop) - Soft reset preserves signing key for wallet access retention - New "Wipe Signing Key" danger option with severe warnings - Full translations for reset/wipe strings in all languages Wallet & Providers: - Add paymaster client for gas abstraction - Provider validation and audit improvements - Balance monitor enhancements Infrastructure: - Minimal dream processor refactor - Location search API for setup wizard - Attestation platform improvements - Streaming verification enhancements Testing & QA: - Mobile QA runner improvements - iOS physical device test support - Localization completeness tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix localization sync: add mobile.setup_include_location to all languages - Add setup_include_location key to mobile section in all 15 language files - Sync localization files to mobile/androidApp, mobile/desktopApp, and mobile/shared - Fixes test_localization_completeness and test_kotlin_localizations tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix SonarCloud code quality issues - partnership.py: Merge implicitly concatenated strings - location.py: Use Annotated type hints, remove redundant response_model - system_snapshot_helpers.py: Merge implicitly concatenated strings - dream_memorize_handler.py: Mark unused variables with _ - minimal_dream_processor.py: Re-raise CancelledError, use suppress() for intentional catch - hashes.py: Merge implicitly concatenated strings - llm.py: Define constant for duplicated literal "Total tokens used" 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add ISO 6709 coordinate storage and location utilities - Add location_latitude/longitude fields to SetupCompleteRequest (ISO 6709 decimal degrees) - Store coordinates in graph memory and .env during setup completion - Create location_utils.py with UserLocation dataclass and helper functions: - get_user_location(): Read location from env vars for tool context - get_location_for_context_enrichment(): Format for context enrichment - format_coordinates_for_trace(): Consent-gated trace metadata - Update CIRISAccord metrics to include lat/long in correlation metadata - Tools can now access user coordinates via environment variables when nav/weather enabled 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add user location support to weather and navigation adapters Weather adapter: - Use user location from setup as default (replaces separate config) - Add weather:my_location context enrichment tool (5min cache) - Dynamic tool descriptions show user's configured location Navigation adapter: - Store user location coordinates for routing - Add navigation:my_location context enrichment tool (1hr cache) - Add navigation:route_from_me tool for implicit start location - Tools only appear when user location is available from setup Both adapters now automatically consume location data set during setup wizard, making weather and navigation features work seamlessly without separate configuration. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix wallet attestation level and startup language rotation Wallet (x402_provider.py): - Fix attestation level showing 0/5 when hardware trust degraded - Now fetches actual attestation level BEFORE checking hardware trust - Passes real level to SpendingAuthority instead of hardcoding 0 Startup screen (StartupScreen.kt): - Always rotate languages during startup as visual effect - Removed hasExplicitLanguage condition that blocked rotation - Language indicator now shows based on phase, not language selection - Resets to persisted language when startup completes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add consolidator startup indication to mobile app Backend (tsdb_consolidation/service.py): - Add console output for mobile app: [CONSOLIDATOR] messages - Output when checking for missed windows, processing periods, complete Mobile (StartupViewModel.kt): - Parse [CONSOLIDATOR] messages from console output - Track consolidator status in StateFlow - Don't timeout while consolidator is actively running - Clear status 2 seconds after completion Mobile (StartupScreen.kt): - Display consolidator status with yellow indicator - Shows "Checking for missed windows...", "Processing period X...", etc. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix flaky TSDB edge creation tests for parallel execution Root cause: Tests were using global patching of get_db_connection which caused race conditions when multiple tests ran in parallel. Multiple tests patching the same global function simultaneously led to interference. Fix: - Replaced mock_db_connection fixture with test_db_path fixture using unique temporary files (pytest's tmp_path) for true test isolation - Tests now use separate SQLite database files per test - Close connections before calling EdgeManager methods that create their own connections to avoid WAL mode lock conflicts - Removed all global patching except for explicit retry behavior tests All 22 tests now pass reliably with -n 8 parallel execution. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add pre-commit hook to check localization sync New pre-commit hook that BLOCKS commits if localization files are out of sync with en.json. This prevents adding new English strings without corresponding translations in all 16 supported languages. Features: - Checks all 16 language files against en.json keys - Reports missing translations per language - Reports extra keys that should be removed - Only runs when localization/*.json files are modified - Clear error messages with fix instructions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add desktop scroll support and improve language selector UX - Add PlatformScrollbar component (expect/actual) for desktop scrollbars - Add scroll support to LoginScreen, StartupScreen, TelemetryScreen - Center and highlight language selector on login screen - Show "Interface + Agent" hint to clarify language affects reasoning - Fix z-order so language selector is clickable on desktop 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix desktop factory reset, consent partnership, FFI loading, and test automation Major fixes for the desktop wipe → setup wizard → agent flow: - Fix CIRISVerify FFI loading: platform-aware suffix ordering (.dylib before .so on macOS) with pip-installed package fallback. Symlink macOS dylib. - Fix factory reset on desktop: preserve signing keys during data wipe, delete databases in both ~/ciris/data/ and CWD/data/ (dev mode), detect git repo root for correct data directory resolution. - Fix .env location: remove CWD .env check (XDG best practices), use ~/ciris/.env as canonical location. Clear stale CIRIS_CONFIGURED env var when .env file is deleted. - Fix founding partnership consent node: use consent/{wa_id} pattern matching ConsentService lookups. Pass wa_cert.wa_id instead of username. - Fix config_migration partnership backfill: check consent/{wa_id} pattern. - Fix _get_agent_id: add signing key fallback + detailed logging for lens-identifier endpoint debugging. - Fix desktop app restart after wipe: don't call exitProcess(), use pythonRuntime.shutdown() + local-shutdown API, navigate to Startup screen. - Fix PythonRuntime.desktop: treat empty cognitive_state as healthy (not stuck). - Add /screenshot endpoint to test automation server (java.awt.Robot, gated behind CIRIS_TEST_MODE). - Add /mouse-click and /mouse-click-xy endpoints for real AWT mouse events (works with ExposedDropdownMenuBox and other Compose popups). - Add testableClickable to provider and model dropdowns in SetupScreen. - Add postLocalShutdown() to CIRISApiClient for server restart. - Add input_location_search testable tag and location_result_* tags. - Add SwiftCapture-based video recording to demo clip script. - Add E2E test script: tools/test_desktop_wipe_setup.sh. - Bump version to 2.3.2. Known issue: startup language rotation prevents E2E test from completing autonomously (app stuck on Startup screen cycling through languages). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix language rotation recomposition storm and consent node_id pattern - Fix language sync to backend: only sync when user explicitly selects a language (hasExplicitLanguageSelection), not during temporary login screen language rotation. Prevents API spam and recomposition. - Fix InteractViewModel pipeline label updates: skip temporary language rotations, only update on explicit selection or English default. - Fix founding partnership consent node_id: use consent/{wa_id} pattern matching ConsentService lookups (was consent/{username}). - Fix config_migration partnership backfill: check consent/{wa_id}. - Fix test script: export OPENROUTER_API_KEY, wait for Login screen. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix CIRIS_ prefix env var support for LLM service initialization The setup wizard writes CIRIS_OPENAI_API_KEY etc to .env, but the LLM service only checked unprefixed OPENAI_API_KEY. This caused the server to fail with "No API key found" after a clean setup. - Add _env() helper to check CIRIS_ prefix first, then unprefixed - Fix _detect_provider_from_env: check CIRIS_OPENAI_API_BASE - Fix _get_api_key_for_provider: check CIRIS_ prefixed keys - Fix main.py startup key check: include CIRIS_ prefixed vars - Fix service_initializer fallback: check CIRIS_OPENAI_API_KEY - Fix E2E test: clean data state before run, longer login timeout Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Bump version strings for iOS/Android and update Xcode project Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix wizard select step skip, document E2E UI automation workflow - Fix AdapterWizardDialog: send "skip" for optional select steps with no selection, preventing infinite re-fetch of options on cameras step. - Fix server-side select step handler: accept "skip" and empty selection for optional steps (step.optional=True), advancing to next step. - Update mobile/CLAUDE.md: document full E2E automation workflow, all test server endpoints, key testTags, platform differences (desktop HTTP server vs iOS XCTest vs Android Espresso). - Update tools/qa_runner/CLAUDE.md: document desktop E2E test script, Home Assistant adapter setup flow, platform notes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Replace login C with CIRIS signet, add first-run welcome message, fix iOS data wipe - Replace plain "C" text with CIRISSignet composable on login screen - Add first-run welcome message: "Welcome! Choose how to set up your AI assistant." Localized to all 16 languages (am, ar, de, en, es, fr, hi, it, ja, ko, pt, ru, sw, tr, ur, zh) - Show welcome message only on isFirstRun, returning users see normal tagline - Fix iOS EnvFileUpdater.clearDataOnly: preserve agent_signing* files during soft reset (same fix as desktop), prevents CIRISVerify key generation crash - Sync localization files across all 3 directories (master, iOS bundle, desktop resources) - Sync master localization to iOS app bundle (fixes missing _full keys on Data Management) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add cross-platform test automation server (iOS POSIX sockets) The test automation HTTP server is now available on iOS in addition to desktop. iOS uses a POSIX socket server (no Ktor dependency needed for K/N), providing the same endpoints: /health, /tree, /click, /input, /wait, /screen, /element/{tag}. Architecture: - TestAutomationState (commonMain): shared element registry, click handlers, screen tracking — used by all platforms - TestAutomationHandler (commonMain): pure handler logic for endpoints - TestServerModels (commonMain): serializable request/response models - IOSTestAutomationServer (iosMain): POSIX socket HTTP server - iOS TestAutomation actual: now functional (was no-op), delegates to TestAutomationState, tracks element positions via onGloballyPositioned - startTestAutomationServer() expect/actual: iOS starts POSIX server, desktop no-op (Main.kt handles), Android TODO Usage on iOS: xcrun devicectl device process launch -d DEVICE_ID \ --environment-variables '{"CIRIS_TEST_MODE":"true"}' ai.ciris.mobile iproxy 18091 8091 -u IDEVICE_ID & curl http://127.0.0.1:18091/health # {"status":"ok","testMode":true} curl http://127.0.0.1:18091/screen # {"screen":"Login"} Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix is_first_run content check, make login buttons automatable - Fix is_first_run: check .env file contents for CIRIS_CONFIGURED="true", not just file existence. Stub .env files (from failed wipe or manual push) no longer prevent first-run detection. - Make btn_local_login and btn_apple_signin testableClickable for automation - iOS E2E validated: wipe → setup wizard → OpenRouter LLM → HA adapter all driven via test automation server (POSIX sockets on port 8091) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Update docs: cross-platform test automation, iOS E2E workflow - mobile/CLAUDE.md: Document iOS POSIX socket test server, full iOS E2E workflow (launch → iproxy → drive UI → HA OAuth via Chrome → verify), iOS-specific gotchas (2s input delay, terminate-existing, callback forwarding), split endpoint tables into shared vs desktop-only - tools/qa_runner/CLAUDE.md: Add platform automation comparison table, iOS E2E commands, iOS-specific gotchas section - tools/CLAUDE.md: Add record_demo_clips.py and test_desktop_wipe_setup.sh to standalone scripts table, add iOS E2E and demo recording to workflows Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add 2.3.2 changelog entry Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Backfill 2.3.1 changelog entry 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add Android test automation HTTP server (Ktor CIO) - Add Ktor server dependencies to androidMain - Create AndroidTestAutomationServer with same endpoints as Desktop/iOS - Update TestAutomation.android.kt to track element positions - Enable test mode via /data/local/tmp/ciris_test_mode file - Start server in MainActivity.onCreate(), stop in onDestroy() - Remove broken macOS dylib symlink 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix duplicate user messages: widen dedup window to 30 seconds The optimistic local message add and the server-fetched message could have timestamps on different sides of a 5-second boundary, producing different dedup keys and showing the same message twice. Widen the dedup window from 5s/10s to 30s for both USER and AGENT messages. This prevents duplicates from the optimistic add + poll cycle while still allowing legitimately repeated messages sent 30+ seconds apart. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Merge conflict resolution for test_prompt_formatting.py * fix: robust CIRIS_HOME detection for Android/iOS settings persistence Add multi-strategy path detection for Android and iOS platforms that doesn't rely solely on CIRIS_HOME environment variable being set: Android (_get_android_ciris_home): 1. Check CIRIS_HOME env var if set 2. Probe known package paths (/data/data/ai.ciris.mobile{,.debug}/files/ciris) 3. Fallback to Path.home()/ciris (Chaquopy sets HOME correctly) iOS (_get_ios_ciris_home): 1. Check CIRIS_HOME env var if set 2. Probe Documents/ciris in iOS sandbox 3. Fallback with warning if path doesn't look like iOS This fixes settings not persisting when the Python backend wrote to a different path than where the mobile app expected to read config. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs: update CHANGELOG for 2.3.2 with CIRIS_HOME and dedup fixes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: update tests for 2.3.2 changes and add missing localization key Localization: - Add mobile.login_first_run_welcome key to all 15 language files - Sync localization files to mobile/androidApp and mobile/shared Test fixes for 2.3.2 behavioral changes: - first_run tests: Update for standardized ~/ciris/.env config path - founding_partnership tests: Update assertions for wa_id format (wa-{name}) - founding_partnership_migration tests: Fix helper to use correct consent node IDs - my_data tests: Mock signing key fallback to prevent test hangs All 10294 tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix P1/P2 issues from bot review P2: Fix location parsing order - parse in same order as setup writes (Country, Region, City instead of reversed order) P1: Add coordinate parsing error handling - guard against invalid float conversion with try/except and log warnings P1: Document smart-account sender issue - ERC-4337 paymaster sends assume deployed smart account but _evm_address is EOA. Added FIXME comment and Known Issues section in CHANGELOG for #656. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix test_first_run tests for 2.3.2 behavior Updated tests to reflect 2.3.2 changes: - CWD-based .env detection was removed for consistency - CIRIS_CONFIGURED env var alone is not sufficient; valid .env required - Use CIRIS_CONFIG_DIR for explicit dev/test override Tests now verify: - test_is_first_run_with_env_var_only: env var alone IS first run - test_is_first_run_cwd_env_not_auto_detected: CWD .env not detected - test_is_first_run_with_config_dir_override: explicit override works Also updated get_config_paths() docstring to match implementation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent c0d2348 commit a59b8d7

217 files changed

Lines changed: 36715 additions & 23525 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,42 @@ All notable changes to CIRIS Agent will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.3.2] - 2026-04-01
9+
10+
### Added
11+
12+
- **Cross-Platform Test Automation** - HTTP server (Desktop: Ktor CIO, iOS: POSIX sockets) on port 8091
13+
- **Shared Test Logic** - Test handler models and state in `commonMain` for all KMP targets
14+
- **Desktop Automation** - `/screenshot` endpoint (java.awt.Robot), `/mouse-click` for dropdowns
15+
- **Testable UI Elements** - `testableClickable` on provider/model dropdowns and login buttons
16+
- **Demo Recording** - SwiftCapture integration (`tools/record_demo_clips.py`)
17+
- **Desktop E2E Test** - Wipe-to-setup test script (`tools/test_desktop_wipe_setup.sh`)
18+
- **CIRIS Signet** - Login screen displays signet icon instead of plain "C" text
19+
- **First-Run Welcome** - Localized welcome message for 16 languages
20+
- **Desktop Restart API** - `postLocalShutdown()` for server restart after wipe
21+
22+
### Fixed
23+
24+
- **Factory Reset Keys** - Preserves signing keys (prevents CIRISVerify FFI crash on restart)
25+
- **Founding Partnership** - Uses `consent/{wa_id}` matching ConsentService lookups
26+
- **First Run Detection** - Checks `.env` contents for `CIRIS_CONFIGURED`, not just file existence
27+
- **CIRISVerify FFI** - Platform-aware suffix ordering (.dylib before .so on macOS)
28+
- **Config Path** - Standardized to `~/ciris/.env`, removed CWD-based path check
29+
- **Stale Env Vars** - `CIRIS_CONFIGURED` cleared when `.env` is deleted
30+
- **Language Rotation** - No longer triggers API sync or pipeline label recomposition
31+
- **Env Var Prefix** - `CIRIS_` prefix supported by LLM service, main.py, service_initializer
32+
- **Wizard Skip** - Select step accepts "skip" for optional steps (cameras)
33+
- **Desktop Wipe** - Server restart via local-shutdown API, repo root data dir detection
34+
- **Python Runtime** - Empty cognitive_state treated as healthy, not stuck
35+
- **CIRIS_HOME Detection** - Multi-strategy path probing for Android/iOS (fixes settings persistence)
36+
- **Message Dedup** - Duplicate user message deduplication window widened to 30 seconds
37+
- **Location Parsing** - Fixed parsing order to match setup serialization (Country, Region, City)
38+
- **Coordinate Parsing** - Added error handling for malformed latitude/longitude env values
39+
40+
### Known Issues
41+
42+
- **Wallet Paymaster** - ERC-4337 paymaster sends require deployed smart account; new users may see "account not deployed" errors until smart account factory integration is added (#656)
43+
844
## [2.3.1] - 2026-03-30
945

1046
### Added

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
**A type-safe, auditable AI agent framework with built-in ethical reasoning**
1919

20-
**BETA RELEASE 2.3.1-stable** | [Release Notes](CHANGELOG.md) | [Documentation Hub](docs/README.md)
20+
**BETA RELEASE 2.3.2-stable** | [Release Notes](CHANGELOG.md) | [Documentation Hub](docs/README.md)
2121

2222
CIRIS lets you run AI agents that explain their decisions, defer to humans when uncertain, and maintain complete audit trails. Currently powering Discord community moderation, designed to scale to healthcare and education.
2323

android/app/src/main/python/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
# Static version - updated at build time by the Android build process
99
# This avoids file-system hashing logic that doesn't work in the Android package
10-
__version__ = "android-2.3.1"
10+
__version__ = "android-2.3.2"
1111

1212

1313
def get_version() -> str:

ciris_adapters/ciris_accord_metrics/services.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -503,8 +503,18 @@ def __init__(
503503
# Coordinates in ISO 6709 decimal degrees format
504504
lat_str = os.environ.get("CIRIS_USER_LATITUDE", "") if env_share_location else ""
505505
lon_str = os.environ.get("CIRIS_USER_LONGITUDE", "") if env_share_location else ""
506-
self._user_latitude: Optional[float] = float(lat_str) if lat_str else None
507-
self._user_longitude: Optional[float] = float(lon_str) if lon_str else None
506+
self._user_latitude: Optional[float] = None
507+
self._user_longitude: Optional[float] = None
508+
if lat_str:
509+
try:
510+
self._user_latitude = float(lat_str)
511+
except ValueError:
512+
logger.warning("Invalid CIRIS_USER_LATITUDE value: %s", lat_str)
513+
if lon_str:
514+
try:
515+
self._user_longitude = float(lon_str)
516+
except ValueError:
517+
logger.warning("Invalid CIRIS_USER_LONGITUDE value: %s", lon_str)
508518
if self._share_location_in_traces and self._user_location:
509519
coords = f" ({self._user_latitude}, {self._user_longitude})" if self._user_latitude else ""
510520
logger.info(f" Location sharing enabled: {self._user_location}{coords}")

ciris_adapters/ciris_verify/ffi_bindings/client.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -305,13 +305,31 @@ def _find_binary(self, explicit_path: Optional[str]) -> Path:
305305
if path.exists():
306306
return path
307307

308-
# Also check relative to this module
308+
# Also check relative to this module (prefer platform-native suffix)
309309
module_dir = Path(__file__).parent
310-
for suffix in [".so", ".dylib", ".dll"]:
310+
platform_suffixes = {
311+
"Darwin": [".dylib", ".so"],
312+
"Linux": [".so", ".dylib"],
313+
"Windows": [".dll"],
314+
}
315+
suffixes = platform_suffixes.get(system, [".so", ".dylib", ".dll"])
316+
for suffix in suffixes:
311317
candidate = module_dir / f"libciris_verify_ffi{suffix}"
312318
if candidate.exists():
313319
return candidate
314320

321+
# Check pip-installed ciris_verify package as final fallback
322+
try:
323+
import ciris_verify as cv_pkg
324+
pkg_dir = Path(cv_pkg.__file__).parent
325+
for suffix in suffixes:
326+
candidate = pkg_dir / f"libciris_verify_ffi{suffix}"
327+
if candidate.exists():
328+
logger.info(f"[CIRISVerify] Using pip-installed library: {candidate}")
329+
return candidate
330+
except (ImportError, AttributeError):
331+
pass
332+
315333
raise BinaryNotFoundError(f"Searched: {paths}")
316334

317335
def _verify_binary_integrity(self) -> None:

ciris_adapters/wallet/providers/x402_provider.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -717,10 +717,18 @@ async def _send_usdc_via_paymaster(self, recipient: str, amount: Decimal) -> str
717717
)
718718

719719
# Create initial UserOperation (without gas estimates)
720+
# FIXME: P1 - Smart account deployment issue
721+
# The current implementation assumes self._evm_address is an already-deployed
722+
# ERC-4337 smart account, but it's actually just an EOA derived from CIRISVerify.
723+
# For new users, bundlers will reject with "account not deployed" because:
724+
# 1. sender should be the counterfactual smart account address (from factory)
725+
# 2. init_code should contain factory + init calldata for first deployment
726+
# Fix requires: smart account factory integration, address computation, and
727+
# tracking deployment state. See issue: CIRISAI/CIRISAgent#656
720728
user_op = UserOperation(
721729
sender=self._evm_address,
722730
nonce=hex(nonce),
723-
init_code="0x", # Account already deployed
731+
init_code="0x", # FIXME: Need init_code for undeployed accounts
724732
call_data="0x" + call_data.hex(),
725733
call_gas_limit=hex(200000), # Initial estimate
726734
verification_gas_limit=hex(100000),

ciris_engine/constants.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
from pathlib import Path
44

55
# Version information
6-
CIRIS_VERSION = "2.3.1-stable"
6+
CIRIS_VERSION = "2.3.2-stable"
77
ACCORD_VERSION = "1.2-Beta"
88
CIRIS_VERSION_MAJOR = 2
99
CIRIS_VERSION_MINOR = 3
10-
CIRIS_VERSION_PATCH = 1
10+
CIRIS_VERSION_PATCH = 2
1111
CIRIS_VERSION_BUILD = 0
1212
CIRIS_VERSION_STAGE = "stable"
1313
CIRIS_CODENAME = "Context Engineering" # Codename for this release

ciris_engine/logic/adapters/api/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
5252
"""Manage application lifecycle."""
5353
# Startup
5454
print("Starting CIRIS API...")
55+
# Note: Founding partnership reconciliation is handled by config_migration.py
56+
# during runtime initialization (PARTNERSHIP_MIGRATION step).
5557
yield
5658
# Shutdown
5759
print("Shutting down CIRIS API...")

ciris_engine/logic/adapters/api/routes/my_data.py

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,45 +175,84 @@ def _get_agent_id(request: Request) -> Optional[str]:
175175
"""Get the current agent ID from runtime.
176176
177177
Checks multiple paths since identity may be populated at different stages.
178+
The signing key (via CIRISVerify) is always available and provides a
179+
deterministic agent identifier, so this function should never return None.
178180
"""
179181
runtime = getattr(request.app.state, "runtime", None)
180182
if not runtime:
181-
logger.debug("_get_agent_id: No runtime in app.state")
183+
logger.error(
184+
"_get_agent_id: runtime MISSING from app.state! "
185+
f"app.state attrs: {[a for a in dir(request.app.state) if not a.startswith('_')]}"
186+
)
182187
return None
183188

184189
# Primary path: runtime.agent_identity.agent_id
185190
identity = getattr(runtime, "agent_identity", None)
186191
if identity and hasattr(identity, "agent_id"):
187192
agent_id = identity.agent_id
188193
if agent_id is not None:
194+
logger.debug(f"_get_agent_id: Found via runtime.agent_identity: {agent_id}")
189195
return str(agent_id)
196+
else:
197+
logger.debug("_get_agent_id: runtime.agent_identity exists but agent_id is None")
198+
else:
199+
logger.debug(
200+
f"_get_agent_id: runtime.agent_identity not available "
201+
f"(identity={identity}, type={type(identity).__name__ if identity else 'None'})"
202+
)
190203

191204
# Fallback: identity_manager.agent_identity
192205
identity_mgr = getattr(runtime, "identity_manager", None)
193206
if identity_mgr:
194207
mgr_identity = getattr(identity_mgr, "agent_identity", None)
195208
if mgr_identity and hasattr(mgr_identity, "agent_id") and mgr_identity.agent_id:
209+
logger.debug(f"_get_agent_id: Found via identity_manager: {mgr_identity.agent_id}")
196210
return str(mgr_identity.agent_id)
211+
else:
212+
logger.debug(
213+
f"_get_agent_id: identity_manager exists but no agent_id "
214+
f"(mgr_identity={mgr_identity is not None})"
215+
)
197216

198217
# Fallback: essential_config.agent_name (always set)
199218
essential = getattr(runtime, "essential_config", None)
200219
if essential:
201220
agent_name = getattr(essential, "agent_name", None)
202221
if agent_name:
203-
logger.debug(f"_get_agent_id: Using essential_config.agent_name={agent_name}")
222+
logger.info(f"_get_agent_id: Using essential_config.agent_name={agent_name}")
204223
return str(agent_name)
224+
else:
225+
logger.debug("_get_agent_id: essential_config exists but agent_name is None/empty")
226+
else:
227+
logger.debug("_get_agent_id: no essential_config on runtime")
205228

206229
# Legacy fallback
207230
legacy = getattr(runtime, "agent_id", None)
208231
if legacy:
232+
logger.debug(f"_get_agent_id: Using legacy runtime.agent_id={legacy}")
209233
return str(legacy)
210234

211-
logger.warning(
212-
"_get_agent_id: Could not determine agent_id. "
235+
# Signing key fallback: key_id is always available (deterministic from Ed25519 pubkey)
236+
try:
237+
from ciris_engine.logic.audit.signing_protocol import get_unified_signing_key
238+
239+
unified_key = get_unified_signing_key()
240+
if unified_key.has_key:
241+
key_id = unified_key.key_id
242+
logger.info(f"_get_agent_id: Using signing key key_id={key_id}")
243+
return key_id
244+
else:
245+
logger.warning("_get_agent_id: Signing key exists but has_key=False")
246+
except Exception as e:
247+
logger.error(f"_get_agent_id: Signing key fallback FAILED: {e}", exc_info=True)
248+
249+
logger.error(
250+
"_get_agent_id: ALL fallbacks exhausted! Could not determine agent_id. "
213251
f"runtime={type(runtime).__name__}, "
214252
f"has_identity={identity is not None}, "
215253
f"has_identity_mgr={identity_mgr is not None}, "
216-
f"has_essential={essential is not None}"
254+
f"has_essential={essential is not None}, "
255+
f"runtime_attrs={[a for a in dir(runtime) if not a.startswith('_') and 'agent' in a.lower()]}"
217256
)
218257
return None
219258

@@ -297,8 +336,13 @@ async def get_lens_identifier(
297336
298337
This is a self-service endpoint — no admin approval needed.
299338
"""
339+
logger.info("[lens-identifier] Request received, resolving agent_id...")
300340
agent_id = _get_agent_id(request)
301341
if not agent_id:
342+
logger.error(
343+
"[lens-identifier] agent_id could not be resolved! "
344+
"This should never happen — the signing key is always available."
345+
)
302346
raise HTTPException(
303347
status_code=status.HTTP_404_NOT_FOUND,
304348
detail="Agent identity not available. The agent may not be fully initialized.",

ciris_engine/logic/adapters/api/routes/setup/complete.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ async def _ensure_system_wa(auth_service: Any) -> None:
194194
logger.warning("⚠️ Could not create system WA - deferral handling may not work")
195195

196196

197-
def _create_founding_partnership(user_id: str) -> None:
197+
def _create_founding_partnership(wa_id: str) -> None:
198198
"""Create a default PARTNERED consent record for the setup user.
199199
200200
The user who completes the setup wizard has explicitly consented by
@@ -203,8 +203,11 @@ def _create_founding_partnership(user_id: str) -> None:
203203
("Your growth supports mine"). This is configured consistency, not
204204
bypassed safeguards (see COGNITIVE_STATE_BEHAVIORS FSD).
205205
206-
Creates a GraphNode with type=CONSENT and stream=PARTNERED, mirroring
207-
the pattern used by _handle_partnership_accept() in partnership.py.
206+
Creates a GraphNode with type=CONSENT using the consent/{wa_id}
207+
pattern that matches the ConsentService lookups.
208+
209+
Args:
210+
wa_id: The WA certificate ID (e.g., "wa-2026-04-01-337AE1")
208211
"""
209212
from ciris_engine.logic.persistence import add_graph_node
210213
from ciris_engine.logic.services.lifecycle.time.service import TimeService
@@ -214,7 +217,7 @@ def _create_founding_partnership(user_id: str) -> None:
214217
now = datetime.now(timezone.utc)
215218

216219
partnered_status = ConsentStatus(
217-
user_id=user_id,
220+
user_id=wa_id,
218221
stream=ConsentStream.PARTNERED,
219222
categories=[
220223
ConsentCategory.INTERACTION,
@@ -228,11 +231,16 @@ def _create_founding_partnership(user_id: str) -> None:
228231
attribution_count=0,
229232
)
230233

234+
# ConsentService stores nodes as consent/{user_id} where user_id = wa_id
235+
# The consent status API uses auth.user_id (bare wa_id from token)
236+
node_id = f"consent/{wa_id}"
237+
231238
node = GraphNode(
232-
id=f"consent/{user_id}",
239+
id=node_id,
233240
type=NodeType.CONSENT,
234241
scope=GraphScope.LOCAL,
235242
attributes={
243+
"user_id": f"user/{wa_id}",
236244
"stream": (
237245
partnered_status.stream.value if hasattr(partnered_status.stream, "value") else partnered_status.stream
238246
),
@@ -252,7 +260,9 @@ def _create_founding_partnership(user_id: str) -> None:
252260

253261
time_service = TimeService()
254262
add_graph_node(node, time_service, None)
255-
logger.info(f"✅ Founding partnership created for setup user: {user_id}")
263+
print(f"[SETUP_COMPLETE] ✅ Founding partnership created: {node_id} (PARTNERED)")
264+
logger.info(f"✅ Founding partnership created for setup user: {node_id}")
265+
256266

257267

258268
def _store_user_preferences(user_id: str, setup: SetupCompleteRequest) -> None:
@@ -377,15 +387,12 @@ async def _create_setup_users(setup: SetupCompleteRequest, auth_db_path: str) ->
377387

378388
# Create founding partnership for setup user — the user consented by
379389
# completing setup, the agent's consent is configured in its template
380-
# Use canonical user ID: for OAuth users it's "provider:external_id", for local users it's the username
381-
if setup.oauth_provider and setup.oauth_external_id:
382-
canonical_user_id = f"{setup.oauth_provider}:{setup.oauth_external_id}"
383-
else:
384-
canonical_user_id = setup.admin_username
385-
_create_founding_partnership(canonical_user_id)
390+
# Use wa_id as the consent node ID (matches consent_user/{wa_id} pattern
391+
# used by the consent status API and consent service)
392+
_create_founding_partnership(wa_cert.wa_id)
386393

387-
# Store user preferences (language & location) in graph memory
388-
_store_user_preferences(canonical_user_id, setup)
394+
# Store user preferences keyed by wa_id for consistency
395+
_store_user_preferences(wa_cert.wa_id, setup)
389396

390397
# Ensure system WA exists
391398
await _ensure_system_wa(auth_service)
@@ -760,7 +767,15 @@ async def complete_setup(setup: SetupCompleteRequest, request: Request) -> Succe
760767
logger.info(f"Using runtime audit database: {auth_db_path}")
761768

762769
# Create users immediately (don't wait for restart)
763-
await _create_setup_users(setup, auth_db_path)
770+
print(f"[SETUP_COMPLETE] Calling _create_setup_users(username={setup.admin_username}, db={auth_db_path})")
771+
try:
772+
await _create_setup_users(setup, auth_db_path)
773+
print("[SETUP_COMPLETE] _create_setup_users completed successfully")
774+
except Exception as user_err:
775+
print(f"[SETUP_COMPLETE] _create_setup_users FAILED: {user_err}")
776+
import traceback
777+
traceback.print_exc()
778+
raise
764779

765780
# Reload user cache in APIAuthService to pick up newly created users
766781
auth_service = getattr(request.app.state, "auth_service", None)

0 commit comments

Comments
 (0)