Cargo Workspace & Single Binary CLI/API Scaffold #2

Closed
opened 2026-09-22 17:58:53 +00:00 by yuris · 0 comments
Owner

Summary

Scaffold the single Cargo package producing the life binary, providing foundational CLI entry points (life <command>), internal module boundaries, uniform JSON output envelopes, and the Axum HTTP service router scaffold as defined in the prospective architecture.

Background & Technical Rationale

The Life architecture intentionally avoids distributed microservices, TypeScript/Python sidecars, MCP protocol wrappers, or heavy generic agent frameworks. Instead, a single Rust binary serves two operational roles:

  1. Daemon mode (life serve): An authenticated HTTP service exposing /v1/* routes, hosting background workers, and mediating access to SQLite, Radicale (CalDAV), Hindsight, and Jev.
  2. Client CLI mode (life <command>): A thin, authenticated HTTPS client that communicates with life serve over the local network or unix domain socket, parsing command-line arguments and rendering structured JSON.

A uniform communication contract governs all CLI outputs and HTTP endpoints. The CLI must output exactly one JSON envelope on stdout when invoked with --json, while all diagnostic, trace, and logging output is routed strictly to stderr. Diagnostics must never contain authentication tokens or private item payloads.

The CLI chooses semantic exit codes (e.g., exit code 6 for pending/partial operations, exit code 4 for concurrency conflicts) matching the HTTP transport semantics defined in the technical specification.

Exact Technical References & Specifications

Process Model

From docs/technical/architecture.md#process-model:

"One Rust Cargo package produces life. life serve runs a centralized HTTP service; all other commands are thin authenticated HTTPS clients. OMP and Hermes invoke the same public CLI, not harness-internal helpers. A future web/mobile client may use the same API. No generic agent framework, MCP server or TypeScript/Python sidecar is needed."

flowchart LR
  O[OMP or Hermes skill] --> C[life CLI]
  C --> S[life serve: authenticated HTTP API]
  S --> R[Radicale: current tasks and events]
  S --> D[SQLite: timeline, proposals, operations, outbox]
  S --> H[Hindsight: semantic context]
  S --> J[TypeSafe/Jev: typed judgment]

Implementation Boundaries & Layout

From docs/technical/architecture.md#implementation-boundaries:

Proposed dependency Responsibility
Tokio Async network runtime, bounded tasks and shutdown
Axum Versioned service routes, body limits, authentication middleware
Clap Explicit CLI commands and validation
Serde Typed JSON contracts and schema-version checks
reqwest with rustls TLS HTTP transport; one reusable pooled client per configured backend
rusqlite SQLite on a dedicated blocking database worker, never blocking Tokio executors
tracing Structured redacted diagnostics and correlation IDs

Prospective layout:

Cargo.toml
Cargo.lock
src/
  main.rs
  cli.rs
  api.rs
  domain/
  calendar/
  history/
  hindsight.rs
  jev.rs
  config.rs
tests/

Transport and Common Contract

From docs/technical/cli-api.md#transport-and-common-contract:

"The service exposes JSON /v1 over HTTPS with bearer Life tokens. CLI --json emits exactly one envelope on stdout; diagnostics go to stderr and never contain tokens/private bodies. IDs are opaque and URL-path encoded, never raw remote hrefs or arbitrary fetch URLs. Timestamps are RFC3339 UTC unless a tagged date or explicit timezone field says otherwise. Reject unknown request fields and unsupported schema versions. Optional fields may be omitted; null means explicitly unknown where documented. PATCH omission preserves a property; null clears only nullable properties.

Every envelope has schema_version: 1, request_id, status, data, warnings and error. Status is ok, needs_clarification, partial or error. Warnings are objects with code, message and optional collection_id/item_id. Error is null or {code,message,retryable,operation_id?}. Success data is route-specific; failures may carry recoverable operation/step data. needs_clarification data contains questions and optional candidates; no calendar mutation has occurred. partial never means all requested work completed."

Exit Code to HTTP Mapping Table

From docs/technical/cli-api.md#transport-and-common-contract:

Exit Meaning HTTP mapping
0 Complete success 200/201
2 Invalid input/configuration 400/413/422
3 Clarification or approval required 200 with needs_clarification; 403 approval_required
4 Conflict 409/412
5 Dependency unavailable 503
6 Pending, partial or unknown outcome 202 or 200 with partial
1 Other failure, including authentication/authorization 401/403/500

Command-to-Route Mapping Table

From docs/technical/cli-api.md#command-to-route-table:

Command Method and route Scope
life agenda --period today|tomorrow|week GET /v1/agenda read
life tasks list GET /v1/tasks read
life item get ID GET /v1/items/{id} read
life task add --input - POST /v1/tasks write
life task update ID --etag ETAG --input - PATCH /v1/tasks/{id} write
life task complete ID --etag ETAG --operation-id KEY --input - POST /v1/tasks/{id}/complete write
life task reopen ID --etag ETAG --input - POST /v1/tasks/{id}/reopen write
life task cancel ID --etag ETAG --input - POST /v1/tasks/{id}/cancel write
life event add --input - POST /v1/events write
life event update ID --etag ETAG --input - PATCH /v1/events/{id} write
life event cancel ID --etag ETAG --input - POST /v1/events/{id}/cancel write
life capture --input - POST /v1/captures write
life resolve --text TEXT --kind task|event POST /v1/resolve read
life plan --period today|week --input - POST /v1/plans write: stores proposal only
life next --window 90m --energy medium POST /v1/next read
life replan --input - POST /v1/replans write: fact/proposal, not calendar
life review --period week POST /v1/reviews read
life context add --input - POST /v1/context write
life context search --input - POST /v1/context/search read
life timeline GET /v1/timeline read
life proposal get ID GET /v1/proposals/{id} read
life proposal apply ID --input - POST /v1/proposals/{id}/apply write
life proposal reject ID --input - POST /v1/proposals/{id}/reject write
life operation get ID GET /v1/operations/{id} read
life sync --input - POST /v1/sync admin
life doctor GET /health/live and GET /health/ready read for detailed readiness
life serve Local service process, no client route local operator

Formal Section Citations

  • docs/technical/architecture.md#process-model: Details the single-binary architecture and service interactions.
  • docs/technical/architecture.md#implementation-boundaries: Normative table for dependencies and prospective code organization.
  • docs/technical/cli-api.md#transport-and-common-contract: Defines JSON envelope attributes, stderr log routing, and the exit code mapping.
  • docs/technical/cli-api.md#command-to-route-table: Full mapping of CLI commands to Axum HTTP routes and auth scopes.
  • docs/technical/development.md#implementation-sequencing: Guides the scaffolding phase before domain and adapter expansion.

What to Build (Step-by-Step)

  1. Initialize Cargo.toml:

    • Single package life (edition 2021 or 2024).
    • Core dependencies:
      • tokio = { version = "1", features = ["full"] }
      • axum = { version = "0.7", features = ["macros"] }
      • clap = { version = "4", features = ["derive", "env"] }
      • serde = { version = "1.0", features = ["derive"] }
      • serde_json = "1.0"
      • tracing = "0.1"
      • tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
      • thiserror = "1.0"
      • uuid = { version = "1", features = ["v4"] }
  2. Setup Entry Point & Logging (src/main.rs):

    • Configure tracing_subscriber to send all logs to std::io::stderr.
    • Parse CLI arguments using clap.
    • Dispatch to either life serve daemon handler or CLI client dispatch.
    • Enforce process exit codes matching docs/technical/cli-api.md#transport-and-common-contract.
  3. Define CLI Commands & JSON Envelope (src/cli.rs):

    • Define Cli derive struct with subcommands: serve, doctor, agenda, tasks, item, task, event, capture, resolve, plan, next, replan, review, context, timeline, proposal, operation, sync.
    • Define the standard generic envelope struct:
      #[derive(Serialize, Deserialize, Debug)]
      pub struct ResponseEnvelope<T> {
          pub schema_version: u32, // must be 1
          pub request_id: String,
          pub status: ResponseStatus, // "ok", "needs_clarification", "partial", "error"
          pub data: Option<T>,
          pub warnings: Vec<WarningItem>,
          pub error: Option<ErrorItem>,
      }
      
    • Implement helper emit_json<T>(envelope: ResponseEnvelope<T>, exit_code: i32) which serializes to stdout followed by a single newline.
  4. Define Service Router Scaffold (src/api.rs):

    • Construct Axum Router binding:
      • GET /health/live
      • GET /health/ready
      • Placeholder stubs for /v1/* routes returning 501 Not Implemented wrapped in standard ResponseEnvelope.
    • Add middleware to attach request_id (X-Request-ID) and inject into tracing span.
  5. Establish Core Directory Module Skeletons:

    • src/domain/mod.rs: pure domain identities and value types.
    • src/calendar/mod.rs: CalDAV and iCalendar preservation logic.
    • src/history/mod.rs: SQLite worker, schema migrations, outbox.
    • src/config.rs: configuration models.
  6. Format & Linter Checks:

    • Ensure cargo check, cargo fmt --check, and cargo clippy --all-targets -- -D warnings run with zero errors.

Acceptance Criteria

  • cargo build produces the single executable target/debug/life.
  • life --help lists all prospective subcommands matching docs/technical/cli-api.md#command-to-route-table.
  • Running any unimplemented command with --json outputs a valid JSON envelope with schema_version: 1, request_id, status, data, warnings, error strictly on stdout.
  • Log output from tracing prints exclusively to stderr, leaving stdout pure JSON parseable by jq.
  • Exit codes from CLI execution conform to the normative exit table (e.g. invalid arguments exit 2, server stub error exits 1 or 5).
  • cargo fmt --check and cargo clippy --all-targets -- -D warnings pass cleanly.

Blocked by

  • #001: Locked Nix Flake DevShell for Clean-Host Toolchain
## Summary Scaffold the single Cargo package producing the `life` binary, providing foundational CLI entry points (`life <command>`), internal module boundaries, uniform JSON output envelopes, and the Axum HTTP service router scaffold as defined in the prospective architecture. ## Background & Technical Rationale The Life architecture intentionally avoids distributed microservices, TypeScript/Python sidecars, MCP protocol wrappers, or heavy generic agent frameworks. Instead, a single Rust binary serves two operational roles: 1. **Daemon mode (`life serve`)**: An authenticated HTTP service exposing `/v1/*` routes, hosting background workers, and mediating access to SQLite, Radicale (CalDAV), Hindsight, and Jev. 2. **Client CLI mode (`life <command>`)**: A thin, authenticated HTTPS client that communicates with `life serve` over the local network or unix domain socket, parsing command-line arguments and rendering structured JSON. A uniform communication contract governs all CLI outputs and HTTP endpoints. The CLI must output exactly one JSON envelope on `stdout` when invoked with `--json`, while all diagnostic, trace, and logging output is routed strictly to `stderr`. Diagnostics must never contain authentication tokens or private item payloads. The CLI chooses semantic exit codes (e.g., exit code 6 for pending/partial operations, exit code 4 for concurrency conflicts) matching the HTTP transport semantics defined in the technical specification. ## Exact Technical References & Specifications ### Process Model From `docs/technical/architecture.md#process-model`: > "One Rust Cargo package produces `life`. `life serve` runs a centralized HTTP service; all other commands are thin authenticated HTTPS clients. OMP and Hermes invoke the same public CLI, not harness-internal helpers. A future web/mobile client may use the same API. No generic agent framework, MCP server or TypeScript/Python sidecar is needed." ```mermaid flowchart LR O[OMP or Hermes skill] --> C[life CLI] C --> S[life serve: authenticated HTTP API] S --> R[Radicale: current tasks and events] S --> D[SQLite: timeline, proposals, operations, outbox] S --> H[Hindsight: semantic context] S --> J[TypeSafe/Jev: typed judgment] ``` ### Implementation Boundaries & Layout From `docs/technical/architecture.md#implementation-boundaries`: | Proposed dependency | Responsibility | |---|---| | Tokio | Async network runtime, bounded tasks and shutdown | | Axum | Versioned service routes, body limits, authentication middleware | | Clap | Explicit CLI commands and validation | | Serde | Typed JSON contracts and schema-version checks | | reqwest with rustls | TLS HTTP transport; one reusable pooled client per configured backend | | rusqlite | SQLite on a dedicated blocking database worker, never blocking Tokio executors | | tracing | Structured redacted diagnostics and correlation IDs | Prospective layout: ```text Cargo.toml Cargo.lock src/ main.rs cli.rs api.rs domain/ calendar/ history/ hindsight.rs jev.rs config.rs tests/ ``` ### Transport and Common Contract From `docs/technical/cli-api.md#transport-and-common-contract`: > "The service exposes JSON `/v1` over HTTPS with bearer Life tokens. CLI `--json` emits exactly one envelope on stdout; diagnostics go to stderr and never contain tokens/private bodies. IDs are opaque and URL-path encoded, never raw remote hrefs or arbitrary fetch URLs. Timestamps are RFC3339 UTC unless a tagged date or explicit timezone field says otherwise. Reject unknown request fields and unsupported schema versions. Optional fields may be omitted; null means explicitly unknown where documented. PATCH omission preserves a property; null clears only nullable properties. > > Every envelope has `schema_version: 1`, `request_id`, `status`, `data`, `warnings` and `error`. Status is `ok`, `needs_clarification`, `partial` or `error`. Warnings are objects with `code`, `message` and optional `collection_id`/`item_id`. Error is null or `{code,message,retryable,operation_id?}`. Success data is route-specific; failures may carry recoverable operation/step data. `needs_clarification` data contains `questions` and optional `candidates`; no calendar mutation has occurred. `partial` never means all requested work completed." ### Exit Code to HTTP Mapping Table From `docs/technical/cli-api.md#transport-and-common-contract`: | Exit | Meaning | HTTP mapping | |---|---|---| | 0 | Complete success | 200/201 | | 2 | Invalid input/configuration | 400/413/422 | | 3 | Clarification or approval required | 200 with needs_clarification; 403 approval_required | | 4 | Conflict | 409/412 | | 5 | Dependency unavailable | 503 | | 6 | Pending, partial or unknown outcome | 202 or 200 with partial | | 1 | Other failure, including authentication/authorization | 401/403/500 | ### Command-to-Route Mapping Table From `docs/technical/cli-api.md#command-to-route-table`: | Command | Method and route | Scope | |---|---|---| | `life agenda --period today\|tomorrow\|week` | GET /v1/agenda | read | | `life tasks list` | GET /v1/tasks | read | | `life item get ID` | GET /v1/items/{id} | read | | `life task add --input -` | POST /v1/tasks | write | | `life task update ID --etag ETAG --input -` | PATCH /v1/tasks/{id} | write | | `life task complete ID --etag ETAG --operation-id KEY --input -` | POST /v1/tasks/{id}/complete | write | | `life task reopen ID --etag ETAG --input -` | POST /v1/tasks/{id}/reopen | write | | `life task cancel ID --etag ETAG --input -` | POST /v1/tasks/{id}/cancel | write | | `life event add --input -` | POST /v1/events | write | | `life event update ID --etag ETAG --input -` | PATCH /v1/events/{id} | write | | `life event cancel ID --etag ETAG --input -` | POST /v1/events/{id}/cancel | write | | `life capture --input -` | POST /v1/captures | write | | `life resolve --text TEXT --kind task\|event` | POST /v1/resolve | read | | `life plan --period today\|week --input -` | POST /v1/plans | write: stores proposal only | | `life next --window 90m --energy medium` | POST /v1/next | read | | `life replan --input -` | POST /v1/replans | write: fact/proposal, not calendar | | `life review --period week` | POST /v1/reviews | read | | `life context add --input -` | POST /v1/context | write | | `life context search --input -` | POST /v1/context/search | read | | `life timeline` | GET /v1/timeline | read | | `life proposal get ID` | GET /v1/proposals/{id} | read | | `life proposal apply ID --input -` | POST /v1/proposals/{id}/apply | write | | `life proposal reject ID --input -` | POST /v1/proposals/{id}/reject | write | | `life operation get ID` | GET /v1/operations/{id} | read | | `life sync --input -` | POST /v1/sync | admin | | `life doctor` | GET /health/live and GET /health/ready | read for detailed readiness | | `life serve` | Local service process, no client route | local operator | ### Formal Section Citations - `docs/technical/architecture.md#process-model`: Details the single-binary architecture and service interactions. - `docs/technical/architecture.md#implementation-boundaries`: Normative table for dependencies and prospective code organization. - `docs/technical/cli-api.md#transport-and-common-contract`: Defines JSON envelope attributes, stderr log routing, and the exit code mapping. - `docs/technical/cli-api.md#command-to-route-table`: Full mapping of CLI commands to Axum HTTP routes and auth scopes. - `docs/technical/development.md#implementation-sequencing`: Guides the scaffolding phase before domain and adapter expansion. ## What to Build (Step-by-Step) 1. **Initialize `Cargo.toml`**: - Single package `life` (edition 2021 or 2024). - Core dependencies: - `tokio = { version = "1", features = ["full"] }` - `axum = { version = "0.7", features = ["macros"] }` - `clap = { version = "4", features = ["derive", "env"] }` - `serde = { version = "1.0", features = ["derive"] }` - `serde_json = "1.0"` - `tracing = "0.1"` - `tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }` - `thiserror = "1.0"` - `uuid = { version = "1", features = ["v4"] }` 2. **Setup Entry Point & Logging (`src/main.rs`)**: - Configure `tracing_subscriber` to send all logs to `std::io::stderr`. - Parse CLI arguments using `clap`. - Dispatch to either `life serve` daemon handler or CLI client dispatch. - Enforce process exit codes matching `docs/technical/cli-api.md#transport-and-common-contract`. 3. **Define CLI Commands & JSON Envelope (`src/cli.rs`)**: - Define `Cli` derive struct with subcommands: `serve`, `doctor`, `agenda`, `tasks`, `item`, `task`, `event`, `capture`, `resolve`, `plan`, `next`, `replan`, `review`, `context`, `timeline`, `proposal`, `operation`, `sync`. - Define the standard generic envelope struct: ```rust #[derive(Serialize, Deserialize, Debug)] pub struct ResponseEnvelope<T> { pub schema_version: u32, // must be 1 pub request_id: String, pub status: ResponseStatus, // "ok", "needs_clarification", "partial", "error" pub data: Option<T>, pub warnings: Vec<WarningItem>, pub error: Option<ErrorItem>, } ``` - Implement helper `emit_json<T>(envelope: ResponseEnvelope<T>, exit_code: i32)` which serializes to `stdout` followed by a single newline. 4. **Define Service Router Scaffold (`src/api.rs`)**: - Construct Axum `Router` binding: - `GET /health/live` - `GET /health/ready` - Placeholder stubs for `/v1/*` routes returning `501 Not Implemented` wrapped in standard `ResponseEnvelope`. - Add middleware to attach `request_id` (`X-Request-ID`) and inject into tracing span. 5. **Establish Core Directory Module Skeletons**: - `src/domain/mod.rs`: pure domain identities and value types. - `src/calendar/mod.rs`: CalDAV and iCalendar preservation logic. - `src/history/mod.rs`: SQLite worker, schema migrations, outbox. - `src/config.rs`: configuration models. 6. **Format & Linter Checks**: - Ensure `cargo check`, `cargo fmt --check`, and `cargo clippy --all-targets -- -D warnings` run with zero errors. ## Acceptance Criteria - [x] `cargo build` produces the single executable `target/debug/life`. - [x] `life --help` lists all prospective subcommands matching `docs/technical/cli-api.md#command-to-route-table`. - [x] Running any unimplemented command with `--json` outputs a valid JSON envelope with `schema_version: 1`, `request_id`, `status`, `data`, `warnings`, `error` strictly on `stdout`. - [x] Log output from `tracing` prints exclusively to `stderr`, leaving `stdout` pure JSON parseable by `jq`. - [x] Exit codes from CLI execution conform to the normative exit table (e.g. invalid arguments exit 2, server stub error exits 1 or 5). - [x] `cargo fmt --check` and `cargo clippy --all-targets -- -D warnings` pass cleanly. ## Blocked by - #001: Locked Nix Flake DevShell for Clean-Host Toolchain
yuris closed this issue 2026-09-22 18:23:48 +00:00
yuris changed title from Issue #002: Cargo Workspace & Single Binary CLI/API Scaffold to Cargo Workspace & Single Binary CLI/API Scaffold 2026-09-22 20:02:41 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
yuris/life-service#2
No description provided.