phase 1 foundations: api types, store, crypto, auth
Lands the bottom three layers of Phase 1: P1-08 internal/api: protocol_version + envelope + every WS message shape from spec.md §6.2 (Hello, Heartbeat, Job*, Schedule*, etc). Wire-format tests pin the JSON shape so a rename here breaks tests instead of silently breaking the agent. P1-02 + P1-03 internal/store: SQLite via modernc.org/sqlite, embed.FS + a tiny version table for hand-rolled migrations. 0001_initial.sql covers every table from spec.md §5 plus enrollment_tokens and host_schedule_version. Typed accessors for users / sessions / enrollment / audit. WAL + foreign_keys + busy_timeout on by default. P1-06 internal/crypto: XChaCha20-Poly1305 AEAD wrapper with per-message random nonce. Key file lifecycle (generate + refuse-to-overwrite, load with size validation). Optional additionalData binds ciphertext to the row that owns it. P1-04 internal/auth (partial — passwords + tokens; sessions middleware lands with the HTTP handlers): argon2id following RFC 9106 (64 MiB / t=3 / p=4 / 32B), constant-time verify. HashToken stores SHA-256 of session/agent/enrollment tokens so a stolen DB doesn't hand over credentials. Build floor moves to Go 1.25 (modernc.org/sqlite v1.50+ requires it); CI + Dockerfile + README updated. Markdown lint diagnostics on tasks.md cleared. All packages tested. ~70 new tests pass in <1s. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// MessageType enumerates every kind of envelope that can flow over
|
||||
// the agent ↔ server WebSocket. Keeping these as string constants
|
||||
// (not iota ints) makes traffic readable in logs and packet captures.
|
||||
type MessageType string
|
||||
|
||||
// Agent → server message types.
|
||||
const (
|
||||
MsgHello MessageType = "hello"
|
||||
MsgHeartbeat MessageType = "heartbeat"
|
||||
MsgJobStarted MessageType = "job.started"
|
||||
MsgJobProgress MessageType = "job.progress"
|
||||
MsgJobFinished MessageType = "job.finished"
|
||||
MsgSnapshotsRpt MessageType = "snapshots.report"
|
||||
MsgRepoStats MessageType = "repo.stats"
|
||||
MsgLogStream MessageType = "log.stream"
|
||||
MsgScheduleAck MessageType = "schedule.ack"
|
||||
MsgCommandResult MessageType = "command.result" // ack for command.run
|
||||
MsgError MessageType = "error"
|
||||
)
|
||||
|
||||
// Server → agent message types.
|
||||
const (
|
||||
MsgCommandRun MessageType = "command.run"
|
||||
MsgCommandCancel MessageType = "command.cancel"
|
||||
MsgScheduleSet MessageType = "schedule.set"
|
||||
MsgConfigUpdate MessageType = "config.update"
|
||||
MsgAgentUpdateAvail MessageType = "agent.update.available"
|
||||
)
|
||||
|
||||
// Envelope is the framing for every WS message in either direction.
|
||||
// Payload is parsed into the concrete struct chosen by Type.
|
||||
//
|
||||
// ID is set on RPC-style messages (command.run / command.result) so
|
||||
// responses can be correlated. For one-shot pushes (heartbeat,
|
||||
// job.progress) it is empty.
|
||||
type Envelope struct {
|
||||
Type MessageType `json:"type"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Payload json.RawMessage `json:"payload,omitempty"`
|
||||
}
|
||||
|
||||
// Marshal builds an envelope from a concrete payload struct.
|
||||
func Marshal(t MessageType, id string, payload any) (Envelope, error) {
|
||||
if payload == nil {
|
||||
return Envelope{Type: t, ID: id}, nil
|
||||
}
|
||||
raw, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return Envelope{}, fmt.Errorf("marshal %s payload: %w", t, err)
|
||||
}
|
||||
return Envelope{Type: t, ID: id, Payload: raw}, nil
|
||||
}
|
||||
|
||||
// UnmarshalPayload decodes the envelope's payload into v.
|
||||
func (e Envelope) UnmarshalPayload(v any) error {
|
||||
if len(e.Payload) == 0 {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(e.Payload, v)
|
||||
}
|
||||
|
||||
// ErrorCode enumerates error reasons surfaced over the wire.
|
||||
// These are stable identifiers; client code may switch on them.
|
||||
type ErrorCode string
|
||||
|
||||
const (
|
||||
ErrProtocolTooOld ErrorCode = "protocol_too_old"
|
||||
ErrProtocolTooNew ErrorCode = "protocol_too_new"
|
||||
ErrUnauthorized ErrorCode = "unauthorized"
|
||||
ErrBadRequest ErrorCode = "bad_request"
|
||||
ErrInternal ErrorCode = "internal"
|
||||
)
|
||||
|
||||
// ErrorPayload is the body of an `error` envelope.
|
||||
type ErrorPayload struct {
|
||||
Code ErrorCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
HelpURL string `json:"help_url,omitempty"`
|
||||
}
|
||||
Reference in New Issue
Block a user