# schemas/lib/ops_contract.ncl — Ops contract (ADR-037) # NATS JetStream subject namespaces, JWT signed command structure, # stream configuration, and workspace ops contract definition. let OpsType = std.contract.custom ( fun label => fun value => let valid = ["deploy", "scale", "restart", "secret_update", "drain"] in if std.array.any (fun x => x == value) valid then 'Ok value else 'Error { message = "Invalid op_type '%{value}'.\nValid values: deploy | scale | restart | secret_update | drain" } ) in let _StreamRetention = [| 'WorkQueue, 'Limits, 'Interest |] in let _ScopeEntry = { op_type | OpsType, target_pattern | String | doc "Glob pattern for allowed op targets (e.g., 'staging-*', 'vapora')", } in let _JwtClaims = { iss | String | doc "Signer identity: keeper-vm-primary | operator- | gh-actions-", sub | String | doc "Requesting principal: woodpecker-job- | manual-", aud | String | doc "Target workspace name", scopes | Array _ScopeEntry | doc "Allowed (op_type, target_pattern) tuples scoped to this signer", seq | Number | doc "Per-issuer monotonic counter — anti-replay", jti | String | doc "UUIDv4 idempotency key", expected_state_version | String | doc "Optimistic concurrency token — workspace state version this op read", exp | Number | doc "Unix timestamp: token expiry", nbf | Number | doc "Unix timestamp: token not-valid-before", } in let _StreamConfig = { name | String, subjects | Array String, retention | _StreamRetention | doc "JetStream retention policy" | default = 'WorkQueue, max_age_s | Number | doc "Message TTL in seconds", replicas | Number | doc "JetStream stream replica count" | default = 1, max_bytes | Number | doc "Max stream storage in bytes (-1 = unlimited)" | default = -1, } in let _OpsSubjects = { pending | String | doc "ops.pending..> — unsigned proposals from emitters", cmd | String | doc "ops.cmd..> — signed commands ready to apply", ack | String | doc "ops.ack..> — application result from ops-controller", audit | String | doc "ops.audit. — immutable audit stream", } in let _OpsStreams = { pending | _StreamConfig | doc "WorkQueue, 14d — buffers unsigned proposals", cmd | _StreamConfig | doc "WorkQueue, 24h — signed commands awaiting application", audit | _StreamConfig | doc "Limits, 90d, replicas=3 — immutable audit record", } in # Workspace-level ops contract — embed in workspace infra NCL as `ops_contract` let _OpsWorkspaceConfig = { workspace | String, subjects | _OpsSubjects, streams | _OpsStreams, authorized_signers | Array String | doc "Signer identity keys allowed to sign for this workspace" | default = [], } in { OpsType = OpsType, StreamRetention = _StreamRetention, ScopeEntry = _ScopeEntry, JwtClaims = _JwtClaims, StreamConfig = _StreamConfig, OpsSubjects = _OpsSubjects, OpsStreams = _OpsStreams, OpsWorkspaceConfig = _OpsWorkspaceConfig, # Constructs a full OpsWorkspaceConfig from a workspace name. # Stream names follow ADR-037 convention: OPS_{STREAM}_{workspace} # (workspace name is used verbatim; uppercase normalisation is ops-controller's concern). make_ops_config | not_exported = fun workspace => { workspace = workspace, subjects = { pending = "ops.pending.%{workspace}.>", cmd = "ops.cmd.%{workspace}.>", ack = "ops.ack.%{workspace}.>", audit = "ops.audit.%{workspace}", }, streams = { pending = { name = "OPS_PENDING_%{workspace}", subjects = ["ops.pending.%{workspace}.>"], retention = 'WorkQueue, max_age_s = 1209600, replicas = 1, max_bytes = -1, }, cmd = { name = "OPS_CMD_%{workspace}", subjects = ["ops.cmd.%{workspace}.>"], retention = 'WorkQueue, max_age_s = 86400, replicas = 1, max_bytes = -1, }, audit = { name = "OPS_AUDIT_%{workspace}", subjects = ["ops.audit.%{workspace}"], retention = 'Limits, max_age_s = 7776000, replicas = 3, max_bytes = -1, }, }, authorized_signers = [], }, }