Skip to content

feat(core): scaffold agentd-core service#1058

Merged
geoffjay merged 41 commits into
mainfrom
issue-212
Apr 16, 2026
Merged

feat(core): scaffold agentd-core service#1058
geoffjay merged 41 commits into
mainfrom
issue-212

Conversation

@geoffjay
Copy link
Copy Markdown
Owner

@geoffjay geoffjay commented Apr 8, 2026

Adds the crates/core workspace member implementing the central auth/API gateway scaffold described in issue #212.

  • Binary agentd-core listening on port 17007 (configurable via AGENTD_PORT)
  • GET /health endpoint returning {"status":"ok","service":"core"}
  • GET /metrics Prometheus endpoint with service_info gauge
  • Graceful Ctrl+C shutdown
  • Shared middleware: structured tracing, CORS, Prometheus metrics layer
  • Two integration tests covering health status and JSON body

Closes #212

@geoffjay geoffjay added the review-agent Used to invoke a review by an agent tracking this label label Apr 8, 2026
Copy link
Copy Markdown
Owner Author

@geoffjay geoffjay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: feat(core): scaffold agentd-core service

Clean scaffold that correctly follows the established service patterns across the workspace. No issues found.


Stack context

issue-212 branches directly off main with no needs restack marker — the branch is current. No parent PR. This is the v0.3.3 critical path entry point; landing it unblocks #240 → #241 → #237 → {#238, #239} → #242 and ultimately resolves the architectural placement question on PR #1057.


Correctness checks

Bind address (127.0.0.1): Consistent with the majority of services in the workspace — orchestrator, communicate, memory, notify, wrap, and index all bind to 127.0.0.1. The pattern is correct. ✅

uuid pinned version: uuid is not in the workspace [dependencies] table, so uuid = { version = "1.11", features = ["v4", "serde"] } is the correct form. Matches orchestrator, notify, memory, and every other crate that uses uuid. ✅

tower-http: Uses workspace = true correctly since it is a workspace dependency. ✅

Port 17007: The doc comment correctly explains the divergence from the issue spec (17010 was already taken by communicate). Good transparency. ✅


Structure

lib.rs + main.rs split: Correct Rust binary+library crate pattern. api.rs is compiled as part of the library (for tests) and independently in the binary. The #[cfg(test)] tests in api.rs run under the library compilation unit, which is the right setup. ✅

init_metrics(): The .expect("failed to install metrics recorder") is acceptable — metrics recorder installation is a fatal startup error in every other service that uses the same pattern. ✅

Metrics in main.rs rather than api::create_router(): Correct separation — the metrics endpoint requires PrometheusHandle state that's initialized at the binary level, not something create_router() should own. Tests correctly cover /health without needing a metrics recorder. ✅

Middleware order (metrics_layer → trace_layer → cors_layer): Consistent with orchestrator and other services. ✅

Graceful shutdown: axum::serve(...).with_graceful_shutdown(shutdown_signal()).into_future().await? is the correct axum 0.7 pattern. ✅


Tests

Both tests use tower::ServiceExt::oneshot — the idiomatic axum handler test approach:

  • health_returns_200: verifies status code ✅
  • health_returns_json_body: verifies both status and service fields ✅

Coverage is appropriate for a scaffold PR. ✅


Summary

This PR delivers exactly what #212 specified: health endpoint, Prometheus metrics, graceful shutdown, shared middleware, two integration tests. The implementation is consistent with the rest of the workspace. Approving.

geoffjay and others added 12 commits April 15, 2026 14:09
…metheus metrics

Adds the crates/core workspace member: binary agentd-core listening on
port 17007 (AGENTD_PORT), GET /health endpoint, Prometheus /metrics,
graceful Ctrl+C shutdown, and shared middleware (trace, CORS, metrics).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…apper

Sets up the structural scaffold that all core entity issues depend on:
- crates/core/src/entity/{mod,user,organization,membership,session}.rs — minimal
  DeriveEntityModel stubs for the four core tables
- crates/core/src/migration/mod.rs — empty Migrator ready for future migrations
- crates/core/src/storage.rs — Storage wrapping DatabaseConnection, runs
  Migrator::up on construction
- Adds sea-orm, sea-orm-migration, and async-trait to Cargo.toml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the stub entity/user.rs with the full DeriveEntityModel
implementation matching the users table schema from the initial migration:
id, email (unique), password_hash, display_name, role, timestamps.

Declares has_many relations to memberships and sessions, with Related<T>
impls for both. To satisfy the has_many bound (R: Related<Self>), the
membership and session stubs gain a user_id column and belongs_to User
relation — the minimum required for compilation before #238 and #239 fill
in their full schemas.

Adds tempfile dev-dependency for the test helper.

Test: insert a user via ActiveModel and retrieve it by primary key.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds crates/core/src/migration/m20260305_000001_create_core_tables.rs
creating four tables in a single migration:
- users (id, email unique, password_hash, display_name, role, timestamps)
- organizations (id, name unique, slug unique, timestamps)
- memberships (id, user_id, organization_id, role, timestamps) with
  composite unique index on (user_id, organization_id)
- sessions (id, user_id, token_hash, expires_at, created_at) with
  indexes on token_hash and expires_at

All tables use if_not_exists; down() drops in reverse dependency order.
Registers the migration in Migrator and calls Storage::new() at startup
to apply pending migrations before the HTTP server begins accepting requests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…many relations

Replaces entity stubs with full DeriveEntityModel implementations:

organization.rs — id, name (unique), slug (unique), timestamps; has_many
Memberships; Related<user::Entity> via membership junction (via() pattern).

membership.rs — id, user_id, organization_id, role, timestamps; belongs_to
User and Organization; Related impls for both.

user.rs — adds Related<organization::Entity> with via() completing the
bidirectional many-to-many between User and Organization through Membership.

Four tests in organization.rs:
- test_org_insert_and_find: basic CRUD
- test_membership_links_user_and_org: junction row creation
- test_find_users_with_related_organizations: find_with_related(Org) from
  user side — one user, two orgs via two memberships
- test_find_org_with_related_users: find_with_related(User) from org side —
  one org, two member users

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the stub entity/session.rs with the full DeriveEntityModel
implementation matching the sessions table schema: id, user_id,
token_hash, expires_at, created_at.

Declares belongs_to User relation and Related<user::Entity> impl.

Three tests covering the acceptance criteria:
- test_session_creation: insert via ActiveModel, retrieve by PK
- test_session_validation_with_related_user: find non-expired session by
  token_hash using find_also_related(user::Entity) for single-query auth
- test_expired_session_cleanup: delete_many with ExpiresAt.lt filter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
geoffjay and others added 14 commits April 15, 2026 14:10
…nd tests

OrganizationStorage (organization_storage.rs):
- create, get_by_id, get_by_slug, list (ordered by name), update, delete
- 8 unit tests: CRUD round-trips, unique name + slug constraints, not-found paths

MembershipStorage (membership_storage.rs):
- add_member, remove_member, list_members(org_id), list_user_organizations(user_id),
  get_membership, update_role
- list_user_organizations uses inner_join on organization::Entity filtered by user_id
- 8 unit tests: add/get, remove, list in both directions, update_role, duplicate rejection

Storage updated with organizations() and memberships() accessors sharing the
DatabaseConnection. Both storage types declared in lib.rs as public modules.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds crates/core/tests/entity_relations.rs with 9 tests covering all
acceptance criteria:

- test_crud_user: insert/find/update/delete via ActiveModel + delete_by_id
- test_user_has_many_memberships: find_related(membership::Entity) from a
  loaded user model
- test_many_to_many_user_organizations_from_user_side: find_with_related
  traversal user → organizations through membership junction
- test_many_to_many_organization_users_from_org_side: find_with_related
  traversal org → users through membership junction
- test_session_belongs_to_user: find_also_related(user::Entity) for
  single-query session validation with eager-loaded user
- test_unique_email_constraint: duplicate email insert returns Err
- test_unique_membership_constraint: duplicate (user_id, org_id) returns Err
- test_migration_idempotent: Migrator::up twice does not error
- test_migration_down_and_up: full rollback + re-apply yields empty tables

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… migration

Adds UserStorage with full CRUD, argon2id password hashing, get_by_username/email,
set_active_organization, and paginated listing. Adds migration to extend the users
table with nullable username (unique) and active_organization_id columns. Updates
all entity helpers and integration tests to include the new fields.
…oint

Adds TenantContext extractor (session + active org required, 403 if no
active org) and PUT /users/me/active-organization endpoint with membership
validation. Also adds api/users.rs module with 4 endpoint tests.
Implements POST /auth/register, POST /auth/login, POST /auth/logout, and
GET /auth/me. Adds SessionStorage with 256-bit random token generation,
expiry cleanup, and AuthUser extractor middleware. Registration creates a
default personal organization and sets it as the active organization.
Adds `agent auth register/login/logout/status` and `agent org list/create/switch`
commands communicating with the agentd-core service (port 17007). Session token
stored at ~/.config/agentd/session with 0600 permissions. Adds `dirs` dependency
for cross-platform config path resolution. Registers core service in status check.
Adds GET/PUT /api/v1/users/me, PUT /api/v1/users/me/password,
GET /api/v1/users/me/organizations, and full CRUD for organizations
with member management (add, remove, list). Enforces owner-only mutations,
last-owner protection, and clears active_organization_id on org deletion.
Adds update_email, update_password, and clear_active_organization_for_org
to UserStorage. 21 new endpoint tests covering auth and authorization checks.
Implements HTTP reverse proxy in crates/core that forwards /api/v1/{service}/*
requests to downstream agentd services with X-Tenant-ID and X-Request-ID header
injection. Includes concurrent health aggregation at /api/v1/health.
geoffjay added 13 commits April 15, 2026 17:27
feat(core): add API gateway proxy for downstream service routing
feat(cli): add auth and org commands for core service
feat(core): add user and organization management API endpoints
feat(core): add TenantContext middleware and active organization endpoint
feat(core): add authentication endpoints with SQLite-backed sessions
feat(core): add UserStorage with argon2 password hashing and username migration
feat(core): add OrganizationStorage and MembershipStorage
feat(core): add SeaORM integration tests for entity relations
feat(core): define Organization and Membership entities with many-to-many relations
feat(core): define SeaORM Session entity with belongs_to User
feat(core): define SeaORM User entity with has_many relations
feat(core): implement initial sea-orm migration for core service schema
feat(core): add entity module stubs, migration runner, and Storage wrapper
@geoffjay geoffjay merged commit b943964 into main Apr 16, 2026
8 of 12 checks passed
@geoffjay geoffjay deleted the issue-212 branch April 16, 2026 00:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-queue Approved by reviewer, queued for merge by conductor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scaffold the core service crate

1 participant