Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions docs/architecture/contracts/api-payload-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -468,12 +468,14 @@ interface RuntimeNodeAttachRequest {
sessionId: SessionId;
participantId: ParticipantId;
nodeId: NodeId;
clientVersion: EventEnvelopeVersion; // semver "MAJOR.MINOR" (ADR-018 §Decision #1); validated against sessions.min_client_version — below-floor daemons are admitted read-only, not ejected (ADR-018 §Decision #4 / Plan-003 I-003-1). Comparison is semver-aware (MAJOR.MINOR), not lexicographic.
capabilities: Record<string, unknown>;
healthState: "online" | "degraded";
}
interface RuntimeNodeAttachResponse {
attachmentId: string;
state: NodeState;
state: NodeState; // liveness axis (registering|online|degraded|offline|revoked) — UNCHANGED
readOnly: boolean; // permission axis, orthogonal to state — true iff clientVersion is below the session floor (DERIVED from stored client_version vs sessions.min_client_version, never a NodeState value). A node may be online + readOnly.
attachedAt: string;
}

Expand All @@ -482,7 +484,7 @@ interface RuntimeNodeHeartbeatRequest {
nodeId: NodeId;
healthState: "online" | "degraded";
}
// Response: 204 No Content
// Response: null — over tRPC, HTTP 200 with { result: { data: null } } (resolver returns null, not a 204); over JSON-RPC, result: null (RuntimeNodeHeartbeatResponseSchema = z.null())

// RuntimeNodeCapabilityUpdate
interface RuntimeNodeCapabilityUpdateRequest {
Expand All @@ -501,9 +503,24 @@ interface RuntimeNodeDetachRequest {
nodeId: NodeId;
reason?: string;
}
// Response: 204 No Content
// Response: null — over tRPC, HTTP 200 with { result: { data: null } } (resolver returns null, not a 204); over JSON-RPC, result: null (RuntimeNodeDetachResponseSchema = z.null())
```

### Runtime-Node Method-Name Registry (Tier 3)

Plan-003's runtime-node operations are exposed as four methods. Method-name strings are `dotted-lowercase` per the canonical `METHOD_NAME_FORMAT` ratified in §Tier 1 (cont.): Plan-007 above (the `register(method, …)` guard at the regex constant) — the same convention shared across Plan-007's JSON-RPC daemon IPC (mechanically regex-enforced at register time) and Plan-008's tRPC control-plane procedures, so the SDK call-site shape (`client.runtimenode.attach({ … })`) is symmetric across transports. Plan-003 registers these handlers under the Plan-007-partial daemon IPC substrate, and the attach/heartbeat calls also cross the Plan-008 control-plane transport (per [Plan-003 §Dependencies](../../plans/003-runtime-node-attach.md)).

The `runtimenode` namespace token is the concatenated domain noun — distinct from the `runtime_node.*` **event** taxonomy (the 7 lifecycle events in [Spec-006 §Runtime Node Lifecycle](../../specs/006-session-event-taxonomy-and-audit-log.md)). The underscore `runtime_node.*` form is a valid _event_ name but is **rejected** as a _method_ name by `METHOD_NAME_FORMAT` (no underscores). `runtimenode.capabilityupdate` is the system's first multi-word procedure: it uses the 2-segment lowercase run-on form to match the uniform single-verb arity of the `session.*` surface (the regex also permits a 3-segment `noun.sub.verb` form, reserved for a future nested-router need).

| Method | Procedure type | Request schema | Response schema |
| --- | --- | --- | --- |
| `runtimenode.attach` | `mutation` | `RuntimeNodeAttachRequest` | `RuntimeNodeAttachResponse` |
| `runtimenode.heartbeat` | `mutation` | `RuntimeNodeHeartbeatRequest` | `null` — HTTP 200 `{ result: { data: null } }` (tRPC) / `result: null` (JSON-RPC); `RuntimeNodeHeartbeatResponseSchema` (`z.null()`) |
| `runtimenode.capabilityupdate` | `mutation` | `RuntimeNodeCapabilityUpdateRequest` | `RuntimeNodeCapabilityUpdateResponse` |
| `runtimenode.detach` | `mutation` | `RuntimeNodeDetachRequest` | `null` — HTTP 200 `{ result: { data: null } }` (tRPC) / `result: null` (JSON-RPC); `RuntimeNodeDetachResponseSchema` (`z.null()`) |

All four are `mutation`s (state-changing, non-idempotent) per the tRPC procedure-type convention in §Tier 1 (cont.): Plan-008 above. The request/response shapes are the interfaces defined directly above; the canonical Zod schemas live in `packages/contracts/` per the §Source-of-Truth Policy. `heartbeat` and `detach` carry a `null` response payload, not an empty `204` body: their resolvers return `null`, which tRPC serializes as an ordinary HTTP 200 success envelope `{ result: { data: null } }` (the control-plane router uses the default transformer, so there is no `data.json` wrapper). This matters because the SDK's `parseTrpcResult` calls `response.json()` on every 2xx response — a `204` with an empty body would throw `SyntaxError`, whereas `{ result: { data: null } }` parses cleanly and `z.null()` validates the extracted `null`. Over the JSON-RPC daemon transport — where JSON-RPC 2.0 requires a `result` member on success — they return `result: null`. Both transports are validated by the canonical `RuntimeNodeHeartbeatResponseSchema` / `RuntimeNodeDetachResponseSchema` (`z.null()`), so the SDK's `JsonRpcClient.call` (daemon) and the tRPC client both have a concrete result schema to pass (Plan-003 T1.3 / T4.1).

---

## Tier 4: Plans 005, 006, 007 (Task 4.5)
Expand Down
38 changes: 31 additions & 7 deletions docs/architecture/cross-plan-dependencies.md

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions docs/architecture/schemas/shared-postgres-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ CREATE TABLE runtime_node_attachments (
participant_id UUID NOT NULL REFERENCES participants(id),
node_id TEXT NOT NULL, -- daemon-assigned node identifier
capabilities JSONB NOT NULL DEFAULT '{}', -- declared capabilities
client_version TEXT NOT NULL, -- daemon semver "MAJOR.MINOR" at attach; floor-compared vs sessions.min_client_version (ADR-018 §Decision #4) — makes the read-only verdict auditable + roster-displayable
state TEXT NOT NULL DEFAULT 'registering'
CHECK(state IN ('registering', 'online', 'degraded', 'offline', 'revoked')),
attached_at TIMESTAMPTZ NOT NULL DEFAULT now()
Expand All @@ -198,6 +199,13 @@ CREATE TABLE runtime_node_attachments (
CREATE INDEX idx_node_attachments_session ON runtime_node_attachments(session_id);
CREATE INDEX idx_node_attachments_participant ON runtime_node_attachments(participant_id);
CREATE UNIQUE INDEX idx_node_attachments_node ON runtime_node_attachments(node_id, session_id);
-- One-active-session enforcement (Plan-003 I-003-5; Spec-003 line 114 — "one active session at a time in v1"):
-- a node has at most one attachment in an active state across all sessions. The partial UNIQUE constrains
-- only active-state rows, so an inactive ('offline' or 'revoked') row does not block a later (re)attach at
-- the index level. Reattach eligibility is then a T3.2 application decision: an 'offline' row is reactivated
-- on reconnect, while a 'revoked' row is refused — revocation is terminal (Plan-003 T3.2/P10).
CREATE UNIQUE INDEX idx_node_attachments_active ON runtime_node_attachments(node_id)
WHERE state IN ('registering', 'online', 'degraded');

-- Owner: Plan-003
CREATE TABLE runtime_node_presence (
Expand Down
11 changes: 11 additions & 0 deletions docs/backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,17 @@ The items below were surfaced by the [plan-readiness-audit Tier 1](./operations/
- Tracked-by: ADR-010 acceptance criteria for `packages/crypto-paseto/` (RFC conformance gating release) — current AC is satisfied by success-vector parity; this BL hardens to spec-mandated adversarial coverage.
- Revisit Trigger: Plan-025 Tier 7 remainder begins (failure vectors should land before the Fastify relay-node ships); OR a downstream consumer plan (Plan-002 Phase 2, Plan-018) surfaces a tamper class that wasn't covered by the handwritten negative cases; OR PASETO spec maintainers publish an updated `4-F-*` vector set.

### BL-131: Plan-003 Phase-5 runtime-node renderer automated component / E2E coverage

- Status: `blocked` (until the Plan-023 renderer test harness ships)
- Priority: `P3`
- Owner: `unassigned`
- References: [Plan-003 Phase 5 (T5.1–T5.4)](./plans/003-runtime-node-attach.md) + §Verification renderer-smoke step, [Plan-003 CP-003-3](./plans/003-runtime-node-attach.md#cp-003-3--plan-023-owns-the-windowsidekicks-preload-bridge-the-phase-5-renderer-projects-over) (renderer projects over the Spec-023 `window.sidekicks` bridge), [ADR-023](./decisions/023-v1-ci-cd-and-release-automation.md) (the Plan-023-owned `test:renderer` CI surface), [Plan-023](./plans/023-desktop-shell-and-renderer.md) (renderer test harness + Tier 8 IPC dispatcher), [BL-123](#bl-123-wire-coverage-tooling--pick-v11-numerical-bar) (sibling criterion-gated V1.1 coverage deferral)
- Summary: Plan-003 Phase 5 ships three renderer view components under `apps/desktop/src/renderer/src/runtime-node-attach/` (`NodeRoster.tsx`, `AttachFlow.tsx` + `CapabilityDeclaration.tsx`, `MixedVersionStatus.tsx`) as a thin projection over the `window.sidekicks` preload bridge (CP-003-3). At Tier 3 their acceptance rests on the T5.4 manual two-client attach smoke — the load-bearing floor / attach / membership semantics (I-003-1, I-003-3) are already proven by the Phase 1–4 automated suite (C1–C6, D1–D6, P1–P8, I1–I3), so the renderer surface is a projection check, not a semantics gate. Automated renderer component tests + the two-client E2E that replaces the manual smoke are deferred to V1.1 because the Plan-023 renderer test harness (RTL/jsdom component-test infra + `window.sidekicks` mock; the Tier 8 IPC dispatcher for cross-client E2E) is not available until Plan-023's Tier 8 remainder ships. Deferral follows the criterion-gated discipline (BL-123 precedent): the test infra anchors in the Plan-023-owned harness rather than a hand-rolled per-plan mock that would drift from the canonical bridge shape.
- Exit Criteria: (a) Plan-023 ships the renderer test harness (component-test infra + `window.sidekicks` mock surface; Tier 8 IPC dispatcher for two-client E2E); (b) automated component tests for `NodeRoster` / `AttachFlow` / `CapabilityDeclaration` / `MixedVersionStatus` assert bridge-only data access (no `node:*`/`electron` imports), the three render states, and below-floor read-only surfacing of `VERSION_FLOOR_EXCEEDED`; (c) an automated two-client attach E2E replaces the T5.4 manual smoke (verifies admit-not-eject + detach-leaves-membership-intact through the renderer); (d) the "automated coverage backfilled per BL-131" notes in Plan-003 T5.1–T5.4 + §Verification are resolved.
- Tracked-by: Plan-023 (renderer test harness + Tier 8 IPC dispatcher).
- Revisit Trigger: Plan-023 Tier 8 ships the renderer test harness / IPC dispatcher; OR V1.1 planning starts; OR a runtime-node renderer regression surfaces that automated component / E2E coverage would have caught.

---

_Closed items live in [Backlog Archive](./archive/backlog-archive.md)._
Loading
Loading