Commit 3004583
feat(ownership): centralized ownership model with typed identities, DB-backed pairing, and OwnershipCache (#1898)
* feat(ownership): add OwnerId, Identity, UserRole, can_act_on types
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix(ownership): private OwnerId field, ResourceScope serde derives, fix doc comment
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* refactor(tenant): replace SystemScope::db() escape hatch with typed workspace_for_user(), fix stale variable names
- Add SystemScope::workspace_for_user() that wraps Workspace::new_with_db
- Remove SystemScope::db() which exposed the raw Arc<dyn Database>
- Update 3 callers (routine_engine.rs x2, heartbeat.rs x1) to use the new method
- Fix stale comment: "admin context" -> "system context" in SystemScope
- Rename `admin` bindings to `system` in agent_loop.rs for clarity
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix(tenant): rename stale admin binding to system_store in heartbeat.rs
* refactor(tenant): TenantScope/TenantCtx carry Identity, add with_identity() constructor and bridge new()
- TenantScope: replace `user_id: String` field with `identity: Identity`; add `with_identity()` preferred constructor; keep `new(user_id, db)` as Member-role bridge; add `identity()` accessor; all internal method bodies use `identity.owner_id.as_str()` in place of `&self.user_id`
- TenantCtx: replace `user_id: String` field with `identity: Identity`; update constructor signature; add `identity()` accessor; `user_id()` delegates to `identity.owner_id.as_str()`; cost/rate methods updated accordingly
- agent_loop: split `tenant_ctx(&str)` into bridge + new `tenant_ctx_with_identity(Identity)` which holds the full body; bridge delegates to avoid duplication
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* feat(db): add V16 tool scope, V17 channel_identities, V18 pairing_requests migrations
- PostgreSQL: V16__tool_scope.sql adds scope column to wasm_tools/dynamic_tools
- PostgreSQL: V17__channel_identities.sql creates channel identity resolution table
- PostgreSQL: V18__pairing_requests.sql creates pairing request table replacing file-based store
- libSQL SCHEMA: adds scope column to wasm_tools/dynamic_tools, channel_identities, pairing_requests tables
- libSQL INCREMENTAL_MIGRATIONS: versions 17-19 for existing databases
- IDEMPOTENT_ADD_COLUMN_MIGRATIONS: handles fresh-install/upgrade dual path for scope columns
- Runner updated to check ALL idempotent columns per version before skipping SQL
- Test: test_ownership_model_tables_created verifies all new tables/columns exist after migrations
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix(db): use correct RFC3339 timestamp default in libSQL, document version sequence offset
Replace datetime('now') with strftime('%Y-%m-%dT%H:%M:%fZ', 'now') in the
channel_identities and pairing_requests table definitions (both in SCHEMA and
INCREMENTAL_MIGRATIONS) to match the project-standard RFC 3339 timestamp format
with millisecond precision. Also add a comment clarifying that libSQL incremental
migration version numbers are independent from PostgreSQL VN migration numbers.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* feat(ownership): bootstrap_ownership(), migrate_default_owner, V19 FK migration, replace hardcoded 'default' user IDs
- Add V19__ownership_fk.sql (programmatic-only, not in auto-migration sweep)
- Add `migrate_default_owner` to Database trait + both PgBackend and LibSqlBackend
- Add `get_or_create_user` default method to UserStore trait
- Add `bootstrap_ownership()` to app.rs, called in init_database() after connect_with_handles
- Replace hardcoded "default" owner_id in cli/config.rs, cli/mcp.rs, cli/mod.rs, orchestrator/mod.rs
- Add TODO(ownership) comments in llm/session.rs and tools/mcp/client.rs for deferred constructors
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix(ownership): atomic get_or_create_user, transactional migrate_default_owner, V19 FK inline constant, fix remaining 'default' user IDs
- Delete migrations/V19__ownership_fk.sql so refinery no longer auto-applies FK constraints before bootstrap_ownership runs; add OWNERSHIP_FK_SQL constant with TODO for future programmatic application
- Remove racy SELECT+INSERT default in UserStore::get_or_create_user; both PostgreSQL (ON CONFLICT DO NOTHING) and libSQL (INSERT OR IGNORE) now use atomic upserts
- Wrap migrate_default_owner in explicit transactions on both backends for atomicity
- Make bootstrap_ownership failure fatal (propagate error instead of warn-and-continue)
- Fix mcp auth/test --user: change from default_value="default" to Option<String> resolved from configured owner_id
- Replace hardcoded "default" user IDs in channels/wasm/setup.rs with config.owner_id
- Replace "default" sentinel in OrchestratorState test helper with "<unset>" to make the test-only nature explicit
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix(ownership): remove default user_id from create_job(), change sentinel strings to <unset>
- Gate ContextManager::create_job() behind #[cfg(test)]; production code must
use create_job_for_user() with an explicit user_id to prevent DB rows with
user_id = 'default' being silently created on the production write path.
- Change the placeholder user_id in McpClient::new(), new_with_name(), and
new_with_config() from "default" to "<unset>" so accidental secrets/settings
lookups surface immediately rather than silently touching the wrong DB partition.
- Same sentinel change for SessionManager::new() and new_async() in session.rs;
these are overwritten by attach_store() at startup with the real owner_id.
- Update tests that asserted the old "default" sentinel to expect "<unset>", and
switch test_list_jobs_tool / test_job_status_tool to create_job_for_user("default")
to keep ownership alignment with JobContext::default().
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* feat(db): add ChannelPairingStore sub-trait with resolve_channel_identity, upsert/approve pairing, PostgreSQL + libSQL implementations
Adds PairingRequestRecord, ChannelPairingStore trait (5 methods), and
generate_pairing_code() to src/db/mod.rs; implements for PgBackend in
postgres.rs and LibSqlBackend in libsql/pairing.rs; wires ChannelPairingStore
into the Database supertrait bound; all 6 libSQL unit tests pass.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix(db): atomic libSQL approve_pairing with BEGIN IMMEDIATE, add case-insensitive/expired/double-approve tests
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* feat(ownership): add OwnershipCache for zero-DB-read identity resolution on warm path
Converts src/ownership.rs to src/ownership/ module directory and adds
src/ownership/cache.rs with a write-through in-process cache mapping
(channel, external_id) -> Identity. Wired as Arc<OwnershipCache> on
AppComponents for Task 8 pairing integration. All 7 cache unit tests pass.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* test(e2e): add ownership model E2E tests and extend pairing tests for DB-backed store
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix(e2e): remove unused asyncio import, add fallback assertion in test_pairing_response_structure
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* test(tenant): unit tests for TenantScope::with_identity and AdminScope construction
Adds 5 focused unit tests verifying TenantScope::with_identity stores the
full Identity (owner_id + role), TenantScope::new creates a Member-role
identity, and AdminScope::new returns Some for Admin and None for Member.
Uses LibSqlBackend::new_memory() as the test DB stub.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix(ownership): recover from RwLock poison instead of expect() in OwnershipCache
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* test(ownership): integration tests for bootstrap, tenant isolation, and ChannelPairingStore
Adds tests/ownership_integration.rs covering migrate_default_owner idempotency,
TenantScope per-user setting isolation (including Admin role bypass check),
and the full ChannelPairingStore lifecycle (upsert, approve, remove, multi-channel isolation).
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix(test): remove duplicate pairing tests and flaky random-code assertion from integration suite
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* feat(pairing): rewrite PairingStore to DB-backed async with OwnershipCache
Replaces the file-based pairing store (~/.ironclaw/*-pairing.json,
*-allowFrom.json) with a DB-backed async implementation that delegates
to ChannelPairingStore and writes through to OwnershipCache on reads.
- PairingStore::new(db, cache) uses the DB; new_noop() for test/no-DB
- resolve_identity() cache-first lookup via OwnershipCache
- approve(code, owner_id) removes channel arg (DB looks up by code)
- All WASM host functions updated: pairing_upsert_request uses block_in_place,
pairing-is-allowed renamed to pairing-resolve-identity returning Option<String>,
pairing-read-allow-from deprecated (returns empty list)
- Signal channel receives PairingStore via new(config, db) constructor
- Web gateway pairing handlers read from state.store (DB) directly
- extensions.rs derive_activation_status drops PairingStore dependency;
derives status from extension.active and owner_binding flag instead
- All test call sites updated to use new_noop()
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix(pairing): add missing pairing_store field to all GatewayState initializers, fix disk-full post-edit compile
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* feat(channels): remove owner_id from IncomingMessage, user_id is the canonical resolved OwnerId
`owner_id` on `IncomingMessage` was always a duplicate of `user_id` —
both fields held the same value at every call site. Remove the field and
`with_owner_id()` builder, update the four WASM-wrapper and HTTP test
assertions to use `user_id`, and drop the redundant struct literal field
in the routine_engine test helper.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix(channels): remove stale owner_id param from make_message test helper
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* test(e2e): add browser/Playwright tests for ownership model — auth screen, chat UI, owner login
Adds five Playwright-based browser tests to the ownership model E2E suite
verifying the web UI experience: authenticated owner sees chat input, unauthenticated
browser sees auth screen, owner can send a message and receive a response, settings
tab renders without errors, and basic page structure is correct after login.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* feat(settings): migrate channel credentials from plaintext settings to encrypted secrets store
Moves nearai.session_token from the plaintext DB settings table to the
AES-256-GCM encrypted secrets store (key: nearai_session_token).
- SessionManager gains an `attach_secrets()` method that wires in the
secrets store; `save_session` writes to it when available and
`load_session_from_secrets` is called preferentially over settings
- `migrate_session_credential()` runs idempotently on each startup in
`init_secrets()`, reading the JSON session from settings, writing it
to secrets, then deleting the plaintext copy
- Wizard's `persist_session_to_db` now writes to secrets first, falling
back to plaintext settings only when secrets store is unavailable
- Plaintext settings path is preserved as fallback for installs without
a secrets store (no master key configured)
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix(settings): settings fallback only when no secrets store, verify decryption before deleting plaintext
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix(ownership): ROLLBACK in libSQL migrate_default_owner, shared OwnershipCache across channels, add dynamic_tools to migration, fix doc comment
- libSQL migrate_default_owner: wrap UPDATE loop in async closure + match to emit ROLLBACK on any mid-transaction failure (mirroring approve_pairing pattern)
- Both backends: add dynamic_tools to the migrate_default_owner table list so agent-built tools are migrated on first pairing
- setup_wasm_channels: accept Arc<OwnershipCache> parameter instead of allocating a fresh cache, share the AppComponents cache
- SignalChannel::new: accept Arc<OwnershipCache> parameter and pass it to PairingStore instead of allocating a new cache
- PairingStore: fix module-level and struct-level doc comments to accurately describe lazy cache population after approve()
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix(web): use can_act_on for authorization in job/routine handlers instead of raw string comparisons
Replace 12 raw `user_id != user.user_id` / `user_id == user.user_id` string comparisons
in jobs.rs and 4 in routines.rs with calls through the canonical `can_act_on` function
from `crate::ownership`, which is the spec-mandated authorization mechanism.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* chore: include remaining modified files in ownership model branch
* fix: add pairing_store field to test GatewayState initializers, update PairingStore API calls in integration tests
Add missing `pairing_store: None` to all GatewayState struct initializers
in test files. Migrate old file-based PairingStore API calls
(PairingStore::new(), PairingStore::with_base_dir()) to the new DB-backed
API (PairingStore::new_noop()). Rewrite pairing_integration.rs to use
LibSqlBackend with the new async DB-backed PairingStore API.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* chore: cargo fmt
* fix(pairing): truly no-op PairingStore noop mode, ensure owner user in CLI, fix signal safety comments
- PairingStore::upsert_request now returns a dummy record in noop mode instead of
erroring, and approve silently succeeds (matching the doc promise of "writes
are silently discarded").
- PairingStore::approve now accepts a channel parameter, matching the updated
DB trait signature and propagated to all call sites (CLI, web server, tests).
- CLI run_pairing_command ensures the owner user row exists before approval to
satisfy the FK constraint on channel_identities.owner_id.
- Signal channel block_in_place safety comments corrected from "WASM channel
callbacks" to "Signal channel message processing".
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix(pairing): thread channel through approve_pairing, add created flag, retry on code collision, remove redundant indexes
Addresses PR review comments:
- approve_pairing validates code belongs to the given channel
- PairingRequestRecord.created replaces timing heuristic
- upsert retries on UNIQUE violation (up to 3 attempts)
- redundant indexes removed (UNIQUE creates implicit index)
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix(ownership): migrate api_tokens, serialize PG approvals, propagate resolved owner_id
Addresses PR review P1/P2 regressions:
- api_tokens included in migrate_default_owner (both backends)
- PostgreSQL approve_pairing uses FOR UPDATE to prevent concurrent approvals
- Signal resolve_sender_identity returns owner_id, set as IncomingMessage.user_id
with raw phone number preserved as sender_id for reply routing
- Feishu uses resolved owner_id from pairing_resolve_identity in emitted message
- PairingStore noop mode logs warning when pairing admission is impossible
[skip-regression-check]
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix(pr-review): sanitize DB errors in pairing handlers, fix doc comments, add TODO for derive_activation_status
- Pairing list/approve handlers no longer leak DB error details to clients
- NotFound errors return user-friendly 'Invalid or expired pairing code' message
- Module doc in pairing/store.rs corrected (remove -> evict, no insert method)
- wit_compat.rs stub comment corrected to match actual Val shape
- TODO added for derive_activation_status has_paired approximation
* fix(pr-review): propagate libSQL query errors in approve_pairing, round-trip validate session credential migration, fix test doc comment
- libSQL approve_pairing: .ok().flatten() replaced with .map_err() to propagate DB errors
- migrate_session_credential: round-trip compares decrypted secret against plaintext before deleting
- ownership_integration.rs: doc comment corrected to match actual test coverage
* fix(pairing): store meta, wrap upserts in transactions, case-insensitive role/channel, log Signal DB errors, use auth role in handlers
- Store meta JSONB/TEXT column in pairing_requests (PG migration V18, libSQL schema + incremental migration 19)
- Wrap upsert_pairing_request in transactions (PG: client.transaction(), libSQL: BEGIN IMMEDIATE/COMMIT/ROLLBACK)
- Case-insensitive role parsing: eq_ignore_ascii_case("admin") in both backends
- Case-insensitive channel matching in approve_pairing: LOWER(channel) = LOWER($2)
- Log DB errors in Signal resolve_sender_identity instead of silently discarding
- Use auth role from UserIdentity in web handlers (jobs.rs, routines.rs) via identity_from_auth helper
- Fix variable shadowing: rename `let channel` to `let req_channel` in libsql approve_pairing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(security): add auth to pairing list, cache eviction on deactivate, runtime assert in Signal, remove default fallback, warn on noop pairing codes
Addresses zmanian's review:
- #1: pairing_list_handler requires AuthenticatedUser
- #2: OwnershipCache.evict_user() evicts all entries for a user on suspension
- #3: debug_assert! for multi-thread runtime in Signal block_in_place
- #9: Noop PairingStore warns when generating unredeemable codes
- #10: cli/mcp.rs default fallback replaced with <unset>
* fix(pairing): consistent LOWER() channel matching in resolve_channel_identity, fix wizard doc comment, fix E2E test assertion for ActionResponse convention
* fix(pairing): apply LOWER() consistently across all ChannelPairingStore queries (upsert, list_pending, remove)
All channel matching now uses LOWER() in both PostgreSQL and libSQL backends:
- upsert_pairing_request: WHERE LOWER(channel) = LOWER($1)
- list_pending_pairings: WHERE LOWER(channel) = LOWER($1)
- remove_channel_identity: WHERE LOWER(channel) = LOWER($1)
Previously only resolve_channel_identity and approve_pairing used LOWER(),
causing inconsistent matching when channel names differed by case.
* fix(pairing): unify code challenge flow and harden web pairing
* test: harden pairing review follow-ups
* fix: guard wasm pairing callbacks by runtime flavor
* fix(pairing): normalize channel keys and serialize pg upserts
* chore(web): clean up ownership review follow-ups
* Preserve WASM pairing allowlist compatibility
---------
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>1 parent 994f960 commit 3004583
84 files changed
Lines changed: 4894 additions & 1609 deletions
File tree
- channels-src
- discord/src
- feishu/src
- slack/src
- telegram/src
- whatsapp/src
- docs
- migrations
- src
- agent
- channels
- wasm
- web
- handlers
- static
- i18n
- tests
- cli
- context
- db
- libsql
- extensions
- llm
- orchestrator
- ownership
- pairing
- setup
- tools
- builtin
- mcp
- worker
- tests
- e2e
- scenarios
- support
- wit
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1152 | 1152 | | |
1153 | 1153 | | |
1154 | 1154 | | |
1155 | | - | |
1156 | | - | |
| 1155 | + | |
| 1156 | + | |
1157 | 1157 | | |
1158 | 1158 | | |
1159 | 1159 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
517 | 517 | | |
518 | 518 | | |
519 | 519 | | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
520 | 525 | | |
521 | 526 | | |
522 | 527 | | |
523 | 528 | | |
524 | 529 | | |
525 | | - | |
526 | | - | |
527 | | - | |
528 | | - | |
529 | | - | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
530 | 537 | | |
531 | 538 | | |
532 | 539 | | |
| |||
586 | 593 | | |
587 | 594 | | |
588 | 595 | | |
589 | | - | |
| 596 | + | |
590 | 597 | | |
591 | 598 | | |
592 | 599 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
757 | 757 | | |
758 | 758 | | |
759 | 759 | | |
760 | | - | |
761 | | - | |
| 760 | + | |
| 761 | + | |
762 | 762 | | |
763 | 763 | | |
764 | 764 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1634 | 1634 | | |
1635 | 1635 | | |
1636 | 1636 | | |
1637 | | - | |
1638 | | - | |
| 1637 | + | |
| 1638 | + | |
1639 | 1639 | | |
1640 | 1640 | | |
1641 | 1641 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
910 | 910 | | |
911 | 911 | | |
912 | 912 | | |
913 | | - | |
914 | | - | |
| 913 | + | |
| 914 | + | |
915 | 915 | | |
916 | 916 | | |
917 | 917 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
60 | | - | |
61 | | - | |
62 | | - | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
63 | 64 | | |
64 | 65 | | |
65 | 66 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
247 | 247 | | |
248 | 248 | | |
249 | 249 | | |
250 | | - | |
| 250 | + | |
251 | 251 | | |
252 | 252 | | |
253 | 253 | | |
| |||
363 | 363 | | |
364 | 364 | | |
365 | 365 | | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
366 | 382 | | |
367 | 383 | | |
368 | | - | |
369 | | - | |
370 | | - | |
371 | | - | |
372 | | - | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
373 | 388 | | |
374 | 389 | | |
375 | 390 | | |
| |||
394 | 409 | | |
395 | 410 | | |
396 | 411 | | |
397 | | - | |
| 412 | + | |
398 | 413 | | |
399 | 414 | | |
400 | 415 | | |
401 | 416 | | |
402 | 417 | | |
403 | 418 | | |
404 | 419 | | |
405 | | - | |
| 420 | + | |
406 | 421 | | |
407 | 422 | | |
408 | 423 | | |
409 | | - | |
| 424 | + | |
410 | 425 | | |
411 | 426 | | |
412 | 427 | | |
413 | | - | |
| 428 | + | |
414 | 429 | | |
415 | 430 | | |
416 | 431 | | |
| |||
507 | 522 | | |
508 | 523 | | |
509 | 524 | | |
510 | | - | |
511 | | - | |
| 525 | + | |
| 526 | + | |
512 | 527 | | |
513 | 528 | | |
514 | 529 | | |
| |||
704 | 719 | | |
705 | 720 | | |
706 | 721 | | |
707 | | - | |
| 722 | + | |
708 | 723 | | |
709 | 724 | | |
710 | 725 | | |
711 | 726 | | |
712 | 727 | | |
713 | | - | |
| 728 | + | |
714 | 729 | | |
715 | 730 | | |
716 | 731 | | |
| |||
723 | 738 | | |
724 | 739 | | |
725 | 740 | | |
726 | | - | |
| 741 | + | |
727 | 742 | | |
728 | 743 | | |
729 | 744 | | |
| |||
747 | 762 | | |
748 | 763 | | |
749 | 764 | | |
750 | | - | |
| 765 | + | |
751 | 766 | | |
752 | 767 | | |
753 | 768 | | |
| |||
0 commit comments