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
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -640,9 +640,20 @@ IDs, labels, annotations, workload identity, endpoints, and unknown fields. Any
partial, duplicate-key, identity/window-mismatched, oversized, or invalid-total response is rejected
as a whole and emits zero facts; invalid rows are never filtered into a partial success. A successful
empty allocation map also emits zero facts, while missing OpenCost coverage is never estimated. This
library path adds no OpenCost client, service discovery, credentials, persistence,
billing, optimization, mutation, fleet/team rollup, currency conversion, freshness objective, or
GPU-utilization claim. The current CLI and Hub do not fetch or display these facts yet.
package can preserve each successful projection in a per-scope snapshot, including an empty fact
set, and combine snapshots for one exact window into a deterministic workspace USD total. The
caller supplies the complete expected-scope set; output names every expected, reported,
successful-empty, and missing scope, and missing scopes never contribute synthetic zero cost.
Every fact is revalidated before all component and total amounts are summed with exact decimal
arithmetic. A rollup carries the source window end only when at least one scope reported and selects
no stale threshold.

The rollup is an offline workspace computation core, not a live Hub feature. This library path adds
no OpenCost client, port-forward, service or ingress discovery, arbitrary endpoint, credentials,
Kubernetes Service-proxy RBAC, OCM transport, persistence, runtime wiring, per-team attribution,
UI/API, billing, optimization, mutation, currency conversion, freshness objective, or
GPU-utilization claim. The current CLI and Hub do not fetch, persist, roll up, or display these
facts yet.

Every verdict includes its rule, exact cited signals, confidence state, missing lenses, and an
advisory command or PR change for the operator to inspect and run. The brain performs no I/O and
Expand Down
12 changes: 12 additions & 0 deletions docs/EPICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3174,6 +3174,18 @@ flowchart TD

**Key risk / guardrail.** A partial rollup read as complete. Guardrail: coverage is always shown.

**Current bounded slice (F13.2a, #284).** `internal/connector/opencost` preserves every successful
F13.1a projection in a per-scope snapshot, including a complete empty allocation set, and computes
one deterministic workspace USD total for an exact caller-bound window. The caller supplies the
unique expected cluster set; output separately names expected, reported, successful-empty, and
missing scopes, so missing OpenCost coverage never becomes zero cost or a complete rollup. Every
fact is revalidated against workspace, cluster, namespace, window, currency, lens, provenance,
canonical payload, and native identity before all monetary components and totals are summed with
exact decimal arithmetic. The rollup uses the window end as observation time only when at least one
scope reported. It adds no live transport, endpoint, credential, persistence, Hub/runtime wiring,
team/label attribution, UI, stale threshold, conversion, billing, optimization, GPU-efficiency
inference, or write path, and therefore does not complete F13.2.

### F13.3 — GPU cost columns (DCGM)

**What it is.** GPU cost/utilization columns in the fleet cost view where DCGM metrics exist —
Expand Down
12 changes: 12 additions & 0 deletions docs/SITH-NOTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -3087,6 +3087,18 @@ flowchart TD

**Key risk / guardrail.** A partial rollup read as complete. Guardrail: coverage is always shown.

**Current bounded slice (F13.2a, #284).** `internal/connector/opencost` preserves every successful
F13.1a projection in a per-scope snapshot, including a complete empty allocation set, and computes
one deterministic workspace USD total for an exact caller-bound window. The caller supplies the
unique expected cluster set; output separately names expected, reported, successful-empty, and
missing scopes, so missing OpenCost coverage never becomes zero cost or a complete rollup. Every
fact is revalidated against workspace, cluster, namespace, window, currency, lens, provenance,
canonical payload, and native identity before all monetary components and totals are summed with
exact decimal arithmetic. The rollup uses the window end as observation time only when at least one
scope reported. It adds no live transport, endpoint, credential, persistence, Hub/runtime wiring,
team/label attribution, UI, stale threshold, conversion, billing, optimization, GPU-efficiency
inference, or write path, and therefore does not complete F13.2.

### F13.3 — GPU cost columns (DCGM)

**What it is.** GPU cost/utilization columns in the fleet cost view where DCGM metrics exist —
Expand Down
90 changes: 90 additions & 0 deletions docs/adr/0012-opencost-coverage-aware-workspace-rollup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# ADR 0012: Coverage-aware workspace rollup for OpenCost cost facts

**Status:** Accepted
**Date:** 2026-07-18
**Decision owners:** E13 / F13.2a ([#284](https://github.com/ArdurAI/sith/issues/284))

## Context

ADR 0011 and issue 282 define an exact-decimal USD projector for one already-authorized OpenCost
namespace-allocation response. A successful response may contain an empty allocation map and
correctly emit zero facts. A later fleet rollup cannot infer from zero facts whether the cluster
reported an empty result or never reported at all. Treating both cases as zero cost would violate
E13's central coverage guardrail.

The live access path is also unresolved. OpenCost documents its API on port 9003 through an
operator-run Kubernetes port-forward and notes that deployments may expose a Service or Ingress.
Sith has local, Hub, and security-held OCM environments, but no accepted contract assigns OpenCost
endpoint discovery, authentication, TLS, or credential forwarding to one of them. Those choices
must not leak into the normalization or aggregation core.

Per-team grouping is similarly premature: the normalized cost fact deliberately discards labels,
and Sith does not yet have a canonical team-attribution identity for a namespace. Guessing from a
workload or arbitrary label would create unstable cross-tenant accounting semantics.

## Decision

1. `ProjectNamespaceCostSnapshot` wraps a successful F13.1a projection in a value-only envelope
containing its exact workspace, cluster scope, UTC window, trusted USD unit, and facts. Presence
of the snapshot is the reporting signal; an empty fact slice is a successful empty report.
2. `RollupWorkspaceCosts` accepts one explicit expected-scope set plus at most one successful
snapshot per reporting scope. Expected scopes are bounded, unique, and caller-authoritative.
3. Every snapshot must match the requested workspace, exact UTC window, and USD unit. Every fact is
revalidated against the closed cost taxonomy, TELEMETRY lens, cluster/namespace entity,
OpenCost provenance and protocol, canonical payload bytes, native SHA-256 identity, and source
observation time.
4. Invalid, duplicate, foreign, stale-marked, oversized, or ambiguous input aborts the entire
operation and returns no partial rollup. Duplicate namespaces within one cluster are rejected.
5. All fifteen component, adjustment, and total values are parsed as exact rational decimals and
summed independently. Output uses canonical five-decimal strings; binary floating point is not
used.
6. Coverage separately names expected, reported, successful-empty, and missing scopes. A missing
scope contributes no fact and no synthetic zero. `complete` is true only when every expected
scope has a successful snapshot.
7. The rollup preserves the allocation-window end as `observed_at` when at least one scope
reported. With no report, `observed_at` is absent. No collection time or stale objective is
invented.
8. The computation is bounded to 256 scopes, 1,024 facts per scope, 4,096 facts total, 8 MiB of
normalized payload, a 256 KiB result, and an aggregate magnitude derived from the per-fact cost
bound.

## Consequences

- A workspace total can never silently present partial OpenCost coverage as complete.
- Successful empty reports remain distinguishable from unavailable OpenCost without retaining raw
responses or adding a sentinel fact.
- The result retains aggregate amounts, coverage metadata (expected, reported, successful-empty,
and missing categories plus `complete`), and optional `observed_at` only. Namespace names,
provider IDs, labels, annotations, workload identity, endpoints, credentials, and unknown source
fields do not survive.
- Historical evidence remains tied to its source window, allowing F13.4 to select a freshness
objective later without retroactively changing fact semantics.
- This is an offline workspace computation core. It does not provide the live F13.1 adapter,
persistence, Hub/runtime composition, an API or UI, team rollups, or F13.2 completion.
- Runtime expense is bounded local CPU and memory. The slice creates no cloud resource, network
call, storage, telemetry-volume, egress, or recurring-service cost.

## Alternatives considered

- **Treat zero facts as zero cost:** rejected because it conflates successful empty coverage with a
missing cluster.
- **Emit a synthetic zero-cost fact:** rejected because a sentinel would look like observed
namespace cost and contaminate the fact model.
- **Accept arbitrary OpenCost URLs and credentials in the core:** held because this requires an
explicit SSRF, redirect, TLS, endpoint-provenance, and credential-forwarding decision.
- **Use the Kubernetes Service proxy:** held because it adds `services/proxy` RBAC and does not
solve the security-held OCM transport.
- **Group by an arbitrary team label now:** rejected because no canonical, tenant-scoped team
identity exists and F13.1a intentionally discards labels.
- **Use `float64`:** rejected because deterministic fleet totals require exact decimal behavior.
- **Stamp collection time or choose a stale threshold:** rejected because rereading historical
evidence must not make it fresh, and the objective belongs to F13.4.

## Primary references

- [OpenCost allocation API](https://opencost.io/docs/integrations/api/)
- [OpenCost installation and access](https://opencost.io/docs/installation/install/)
- [OpenCost v1.120.2](https://github.com/opencost/opencost/releases/tag/v1.120.2)
- [ADR 0011](0011-opencost-namespace-cost-facts.md)
- [F13.1a issue 282](https://github.com/ArdurAI/sith/issues/282)
- [E13 transport escalation](https://github.com/ArdurAI/sith/issues/31#issuecomment-5013914477)
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ decision rests on an external fact, that fact is web-verified and cited (see als
| [0009](0009-release-supply-chain.md) | Reproducible and identity-bound release supply chain | Accepted |
| [0010](0010-native-local-desktop-shell.md) | Native local desktop shell | Accepted |
| [0011](0011-opencost-namespace-cost-facts.md) | Exact-decimal USD boundary for OpenCost namespace cost facts | Accepted |
| [0012](0012-opencost-coverage-aware-workspace-rollup.md) | Coverage-aware workspace rollup for OpenCost cost facts | Accepted |

Planning ADRs remain **Proposed** until their implementation lane accepts or rejects them.
Implementation-specific ADRs may be **Accepted** when the corresponding shipped slice provides
Expand Down
24 changes: 24 additions & 0 deletions internal/connector/opencost/boundary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,37 +36,56 @@ var allowedProductionImports = map[string]bool{

var allowedProductionFiles = map[string]string{
"project.go": "bc5a1b2919a50cd0213deb2881c25a3fd43beed790c9d3cbfeb9e147cb72dc96",
"rollup.go": "1dccb66e42dde4be827d2b2ef3a8cfea624dbce374934fd9e8340849cc3ba093",
}

var allowedProductionDeclarations = map[string]bool{
"func:ProjectNamespaceCosts": true,
"func:ProjectNamespaceCostSnapshot": true,
"func:RollupWorkspaceCosts": true,
"func:buildFact": true,
"func:consumeUniqueJSON": true,
"func:decodeOptionalField": true,
"func:matchingDelimiter": true,
"func:namespaceCostNativeID": true,
"func:newCostAccumulator": true,
"func:objectFields": true,
"func:observationCostValue": true,
"func:parseCanonicalCost": true,
"func:parseCanonicalTime": true,
"func:parseCostAmount": true,
"func:rejectCaseAliases": true,
"func:rejectDuplicateJSON": true,
"func:sortedScopeKeys": true,
"func:validCostLiteral": true,
"func:validateAllocation": true,
"func:validateAllocationWindow": true,
"func:validateCanonicalTime": true,
"func:validateNamespaceCostFact": true,
"func:validateNamespaceCostSnapshot": true,
"func:validateProjection": true,
"func:validateResponse": true,
"func:validateText": true,
"func:validateWorkspaceRollupRequest": true,
"method:allocationProperties.UnmarshalJSON": true,
"method:allocationRecord.UnmarshalJSON": true,
"method:allocationResponse.UnmarshalJSON": true,
"method:allocationWindow.UnmarshalJSON": true,
"method:costAccumulator.add": true,
"method:costAccumulator.amounts": true,
"type:AllocationQuery": true,
"type:CostAmounts": true,
"type:NamespaceCostSnapshot": true,
"type:Projection": true,
"type:WorkspaceCostCoverage": true,
"type:WorkspaceCostRollup": true,
"type:WorkspaceRollupRequest": true,
"type:allocationProperties": true,
"type:allocationRecord": true,
"type:allocationResponse": true,
"type:allocationWindow": true,
"type:costField": true,
"type:costAccumulator": true,
"type:namespaceCostObservation": true,
"value:Kind": true,
"value:ProtocolVersion": true,
Expand All @@ -83,6 +102,11 @@ var allowedProductionDeclarations = map[string]bool{
"value:maxJSONDepth": true,
"value:maxQueryWindow": true,
"value:maxResponseBytes": true,
"value:maxRollupCostUnits": true,
"value:maxRollupFacts": true,
"value:maxRollupInputBytes": true,
"value:maxRollupPayloadBytes": true,
"value:maxRollupScopes": true,
}

func TestProjectorHasNoIOCredentialPersistenceOrMutationSeam(t *testing.T) {
Expand Down
Loading