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
18 changes: 11 additions & 7 deletions docs/EPICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3048,26 +3048,30 @@ Guardrail: the taxonomy is closed; "re-skin a tool" is not an expressible connec

### F12.3 — Versioning and one-canonical-connector policy

**What it is.** A minor-additive protocol contract and a rule of one canonical connector per
tool — the Terraform discipline that prevents the Backstage redundancy/abandonment failure.
**What it is.** A structured, explicitly negotiated framework wire contract, a separate opaque
adapter/evidence contract version, and a rule of one canonical connector per tool — the Terraform
discipline that prevents the Backstage redundancy/abandonment failure.

**How it works.**
1. Major protocol versions delineate compatibility; minor versions are strictly additive.
2. The registry admits **one** canonical connector per target tool, with declared ownership.
3. Breaking a connector's contract is a major-version, reviewed change — never a silent minor bump.
1. Endpoints advertise exact structured `{major, minor}` framework wire versions; the highest
exact common version is selected. No common major or no explicitly common minor fails closed.
2. Adapter/evidence contract versions remain opaque provenance and never drive wire compatibility.
3. The registry admits **one** canonical connector per target tool, with declared ownership.
4. Breaking the framework contract is a major-version, reviewed change — never a silent minor bump.

```mermaid
flowchart TD
A["Connector change"] --> B{"Breaking?"}
B -- "no" --> C["Minor: additive, compatible"]
B -- "no" --> C["Minor: additive and explicitly advertised"]
B -- "yes" --> D["Major: reviewed compatibility break"]
E["New connector for tool T"] --> F{"Canonical connector for T exists?"}
F -- "yes" --> G["Improve the canonical one (no duplicate)"]
F -- "no" --> H["Register as canonical, with owner"]
```

**Acceptance criteria.**
- Minor protocol changes are additive; breaks require a major version and review.
- Wire versions and opaque adapter versions are separate; only explicitly shared wire versions
negotiate, additive minors remain reviewed, and breaks require a major version and review.
- The registry holds one canonical connector per tool with a named owner.

**Key risk / guardrail.** Overlapping half-maintained connectors (the Backstage marketplace).
Expand Down
18 changes: 11 additions & 7 deletions docs/SITH-NOTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -2961,26 +2961,30 @@ Guardrail: the taxonomy is closed; "re-skin a tool" is not an expressible connec

### F12.3 — Versioning and one-canonical-connector policy

**What it is.** A minor-additive protocol contract and a rule of one canonical connector per
tool — the Terraform discipline that prevents the Backstage redundancy/abandonment failure.
**What it is.** A structured, explicitly negotiated framework wire contract, a separate opaque
adapter/evidence contract version, and a rule of one canonical connector per tool — the Terraform
discipline that prevents the Backstage redundancy/abandonment failure.

**How it works.**
1. Major protocol versions delineate compatibility; minor versions are strictly additive.
2. The registry admits **one** canonical connector per target tool, with declared ownership.
3. Breaking a connector's contract is a major-version, reviewed change — never a silent minor bump.
1. Endpoints advertise exact structured `{major, minor}` framework wire versions; the highest
exact common version is selected. No common major or no explicitly common minor fails closed.
2. Adapter/evidence contract versions remain opaque provenance and never drive wire compatibility.
3. The registry admits **one** canonical connector per target tool, with declared ownership.
4. Breaking the framework contract is a major-version, reviewed change — never a silent minor bump.

```mermaid
flowchart TD
A["Connector change"] --> B{"Breaking?"}
B -- "no" --> C["Minor: additive, compatible"]
B -- "no" --> C["Minor: additive and explicitly advertised"]
B -- "yes" --> D["Major: reviewed compatibility break"]
E["New connector for tool T"] --> F{"Canonical connector for T exists?"}
F -- "yes" --> G["Improve the canonical one (no duplicate)"]
F -- "no" --> H["Register as canonical, with owner"]
```

**Acceptance criteria.**
- Minor protocol changes are additive; breaks require a major version and review.
- Wire versions and opaque adapter versions are separate; only explicitly shared wire versions
negotiate, additive minors remain reviewed, and breaks require a major version and review.
- The registry holds one canonical connector per tool with a named owner.

**Key risk / guardrail.** Overlapping half-maintained connectors (the Backstage marketplace).
Expand Down
76 changes: 76 additions & 0 deletions docs/adr/0014-connector-wire-adapter-version-split.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# ADR 0014: Split connector wire compatibility from adapter provenance

- **Status:** Accepted
- **Date:** 2026-07-19
- **Issue:** [#288](https://github.com/ArdurAI/sith/issues/288)
- **Parent:** [#30](https://github.com/ArdurAI/sith/issues/30)

## Context

E12 requires an out-of-process, typed, versioned connector framework. The existing internal
connector descriptor has one string field, `ProtocolV`, but landed code uses that field for two
different domains:

- the original source-adapter spec describes one connector-wide semver where minor changes are
additive and major changes are reviewed; and
- adapters and projectors use opaque evidence or behavior identifiers such as `1.0.0`,
`alerts/v1`, `search/ecs-v1`, and `gitops-open-pr/2026-03-10`.

Those opaque values are important provenance, but they cannot truthfully drive a future gRPC
compatibility handshake. Repurposing them would require an unrelated migration of existing facts,
fixtures, and persisted evidence before transport exists.

Protocol Buffers allows additive fields as a wire-safe change, but wire safety is narrower than
application-semantic compatibility. It also forbids reusing field numbers and strongly discourages
field-type changes. Sith therefore needs an explicit compatibility policy before it commits a
protobuf service definition.

## Decision

1. Add a structured `WireVersion{Major, Minor}` framework domain. Major zero is invalid.
2. Connector descriptors advertise an explicit set of 1 through 32 supported wire versions. The
registry rejects malformed, duplicate, or oversized offers and stores a deterministic sorted
copy.
3. `NegotiateWireVersion` chooses the highest exact version advertised by both endpoints.
4. No common major returns a distinct major-mismatch error. A common major without an explicitly
shared minor returns a distinct unsupported-minor error. Same major alone never implies support.
5. Rename connector-descriptor `ProtocolV` to opaque `AdapterVersion`. This value continues to
identify the adapter's evidence and behavior contract and is not parsed as semver.
6. Preserve `fleet.Provenance.ProtocolV` and its serialized `protocol_version` field. Existing
evidence, persisted facts, and projector protocol identifiers do not migrate in this slice.
7. The initial framework offer is `{major: 1, minor: 0}`.
8. Keep protobuf, generated code, gRPC dependencies, subprocess launch, IPC authentication,
credentials, networking, persistence, and execution out of this slice.

## Consequences

- The framework can evolve transport independently from every adapter's native/evidence contract.
- Compatibility is deterministic and fail-closed; an opaque provenance value can no longer be
mistaken for a transport version.
- Version negotiation has a fixed 32-entry-per-endpoint allocation and comparison bound.
- A connector adding minor version 1 must explicitly keep minor version 0 in its offer if it still
supports it. This is more verbose than range inference but prevents accidental semantic claims.
- Registry descriptor JSON changes before any public out-of-process SDK exists. The package is
internal and current descriptor consumers are migrated in the same change.
- Existing fleet evidence JSON and database rows remain stable.
- This adds bounded in-memory comparison only. It adds no process, listener, permission, cloud
resource, telemetry cardinality, or recurring cost.
- Future protobuf work must preserve field numbers, reserve removed fields, avoid required fields,
and independently define authenticated local IPC, deadlines, health, and bounded restart policy.

## Alternatives considered

- **Repurpose `ProtocolV` as strict semver and migrate all evidence identifiers:** rejected as a
broad breaking migration before transport exists.
- **Use opaque adapter identifiers directly for the wire handshake:** rejected because major/minor
compatibility would be undefined and untestable.
- **Infer support for every lower minor in the same major:** rejected because protobuf wire safety
does not prove application-semantic support.
- **Delay the split until protobuf lands:** rejected because schema work without an accepted
version-domain boundary would bake the ambiguity into the wire contract.

## Primary sources

- [Protocol Buffers proto3 language and compatibility guide](https://protobuf.dev/programming-guides/proto3/)
- [Protocol Buffers best practices](https://protobuf.dev/best-practices/dos-donts/)
- [gRPC core concepts](https://grpc.io/docs/what-is-grpc/core-concepts/)
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ decision rests on an external fact, that fact is web-verified and cited (see als
| [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 |
| [0013](0013-dcgm-gpu-utilization-facts.md) | Privacy-minimized DCGM GPU utilization facts | Accepted |
| [0014](0014-connector-wire-adapter-version-split.md) | Split connector wire compatibility from adapter provenance | 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
59 changes: 40 additions & 19 deletions docs/specs/F2.1-source-adapter-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ type Evidence struct {
// of re-skinning it (E12/F12.2), and lets audit trace any fact to its origin.
type Provenance struct {
Adapter string `json:"adapter"` // Connector.Kind()
ProtocolV string `json:"protocol_version"` // connector protocol version that produced it
ProtocolV string `json:"protocol_version"` // opaque adapter/evidence contract; not E12 wire compatibility
NativeID string `json:"native_id,omitempty"` // the source's own id (UID, series, object id)
DeepLink string `json:"deep_link,omitempty"` // URL into the tool's own UI (brokered read-through)
Collector string `json:"collector,omitempty"` // read-session / worker id, for debugging
Expand Down Expand Up @@ -313,12 +313,18 @@ type Connector interface {

// Descriptor pins a connector into E12's closed taxonomy and versioning discipline.
type Descriptor struct {
Kind string `json:"kind"`
ConnKind ConnectorKind `json:"connector_kind"` // read-adapter | brokered-read-through | typed-action (F12.2)
ProtocolV string `json:"protocol_version"` // semver; minor is additive, major is a reviewed break (F12.3)
Owner string `json:"owner"` // named owner — one canonical connector per tool
Capabilities []Capability `json:"capabilities"`
Verbs []string `json:"verbs,omitempty"` // closed-vocab verbs this adapter hosts (typed-action only)
Kind string `json:"kind"` // canonical target-tool identity; one connector per tool
ConnKind ConnectorKind `json:"connector_kind"` // read-adapter | brokered-read-through | typed-action (F12.2)
WireVersions []WireVersion `json:"wire_versions"` // exact framework versions explicitly supported
AdapterVersion string `json:"adapter_version"` // opaque evidence/behavior contract; not transport semver
Owner string `json:"owner"` // named owner — one canonical connector per tool
Capabilities []Capability `json:"capabilities"`
Verbs []string `json:"verbs,omitempty"` // closed-vocab verbs this adapter hosts (typed-action only)
}

type WireVersion struct {
Major uint32 `json:"major"` // must be greater than zero
Minor uint32 `json:"minor"`
}

// ConnectorKind is E12/F12.2's closed taxonomy — nothing outside these three exists.
Expand All @@ -331,6 +337,16 @@ const (
)
```

`Kind` is the canonical target-tool identity used by registry uniqueness; a second connector for
the same tool is rejected even if it declares a different connector taxonomy. `WireVersions` and
`AdapterVersion` are separate domains. Every wire version must have `Major > 0`. The framework
negotiates the highest exact `{major, minor}` value advertised by both endpoints. Each offer is
bounded to 32 versions. It rejects malformed, duplicate, or oversized offers, no common major, and
a common major without an explicitly shared minor. Protobuf wire-safe
additions do not by themselves prove application-semantic compatibility, so Sith never infers
support for an unadvertised minor. Existing fleet evidence keeps its serialized
`protocol_version` field as opaque adapter/evidence provenance.

### 3.2 The read verbs — `Reader` (discover · read · query)

Every source implements at least `Reader`. These are the day-0 verbs, and the only verbs the
Expand Down Expand Up @@ -549,13 +565,15 @@ type Registry struct { /* unexported: map[string]entry + mutex */ }
func NewRegistry() *Registry

// Register builds the connector, then FAIL-SAFE checks:
// 1. Descriptor.Kind is non-empty and not already registered (one canonical per tool).
// 1. Descriptor.Kind is the non-empty canonical target-tool identity and is not already registered.
// 2. Descriptor.ConnKind is one of the three (F12.2) — else refused.
// 3. Every declared Capability maps to an interface the concrete type satisfies
// 3. WireVersions has 1..32 entries, every Major is > 0, values are unique and canonicalized;
// AdapterVersion is non-empty.
// 4. Every declared Capability maps to an interface the concrete type satisfies
// (e.g. CapExecute => value asserts to Executor). A declared-but-unimplemented
// capability is a registration error. A capability implemented but not declared
// is ignored (declaration is the source of truth; you opt in explicitly).
// 4. typed-action connectors declare their closed-vocab Verbs; read/brokered do not.
// 5. typed-action connectors declare their closed-vocab Verbs; read/brokered do not.
// Any failure returns an error and registers nothing.
func (r *Registry) Register(f Factory) error

Expand Down Expand Up @@ -607,11 +625,12 @@ connector executions).

```go
Descriptor{
Kind: "local-kubeconfig",
ConnKind: KindReadAdapter,
ProtocolV: "1.0.0",
Owner: "sith-core",
Capabilities: []Capability{CapDiscover, CapRead, CapQuery},
Kind: "local-kubeconfig",
ConnKind: KindReadAdapter,
WireVersions: []WireVersion{{Major: 1, Minor: 0}},
AdapterVersion: "1.0.0",
Owner: "sith-core",
Capabilities: []Capability{CapDiscover, CapRead, CapQuery},
// no Verbs — read adapter
}
```
Expand Down Expand Up @@ -801,7 +820,7 @@ Consequences:

---

## 9. Open decisions (recorded, not blocking)
## 9. Decisions and open questions (recorded, not blocking)

1. **`Observed` typing — `json.RawMessage` vs. a typed union.** Chosen: `json.RawMessage`, decoded
by `FactKind`, to keep `fleet` dependency-free and additive. Revisit if a typed `oneof`/generics
Expand All @@ -814,9 +833,11 @@ Consequences:
3. **Reachability probe cost.** `/version` vs. `SelfSubjectAccessReview` vs. lazy (probe on first
real read). Leaning: cheap `/version` at `Discover`, lazy upgrade on `Read`/`Query`. *Tunable;
not contract-affecting.*
4. **`ProtocolV` granularity.** One protocol version for the whole contract vs. per-verb. Chosen:
one per connector (F12.3 semver), simplest that satisfies "minor is additive". *Revisit at E12
when gRPC wire format lands.*
4. **Framework wire vs. adapter provenance (resolved for E12 in #288).** Chosen: an explicit list
of structured framework `{major, minor}` versions plus a separate opaque adapter contract
version. Negotiate only exact values both endpoints advertise; reject major mismatch and
same-major/no-common-minor cases. The legacy fleet-evidence `protocol_version` field remains
opaque provenance and is never used for transport compatibility.
5. **Where `Workspace` is stamped in local mode.** A single implicit local workspace constant
vs. threading E1's type down. Leaning: a `fleet.LocalWorkspace` constant so the type is present
from day 0 and the hub swap is mechanical. *Confirm against E1's final Workspace type.*
Expand Down
3 changes: 2 additions & 1 deletion internal/cli/cached_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ func (*cacheReader) Capabilities() []connector.Capability {

func (reader *cacheReader) Descriptor() connector.Descriptor {
return connector.Descriptor{
Kind: reader.Kind(), ConnKind: connector.KindReadAdapter, ProtocolV: "1.0.0", Owner: "test",
Kind: reader.Kind(), ConnKind: connector.KindReadAdapter,
WireVersions: []connector.WireVersion{connector.CurrentWireVersion()}, AdapterVersion: "1.0.0", Owner: "test",
Capabilities: reader.Capabilities(),
}
}
Expand Down
18 changes: 11 additions & 7 deletions internal/connector/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,17 @@ type Connector interface {

// Descriptor is static registry, taxonomy, ownership, and version metadata.
type Descriptor struct {
Kind string `json:"kind"`
ConnKind ConnectorKind `json:"connector_kind"`
ProtocolV string `json:"protocol_version"`
Owner string `json:"owner"`
Capabilities []Capability `json:"capabilities"`
Verbs []intent.Verb `json:"verbs,omitempty"`
ArgSchemas map[intent.Verb]json.RawMessage `json:"arg_schemas,omitempty"`
// Kind is the canonical target-tool identity; only one connector may own it.
Kind string `json:"kind"`
ConnKind ConnectorKind `json:"connector_kind"`
// WireVersions explicitly lists framework transport versions this connector can speak.
WireVersions []WireVersion `json:"wire_versions"`
// AdapterVersion is an opaque evidence and behavior contract identifier, not transport semver.
AdapterVersion string `json:"adapter_version"`
Owner string `json:"owner"`
Capabilities []Capability `json:"capabilities"`
Verbs []intent.Verb `json:"verbs,omitempty"`
ArgSchemas map[intent.Verb]json.RawMessage `json:"arg_schemas,omitempty"`
}

// ConnectorKind is the closed integration taxonomy.
Expand Down
3 changes: 2 additions & 1 deletion internal/connector/github/action_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@ func (*OpenPRPlanner) Capabilities() []connector.Capability {
// Descriptor binds gitops.open-pr to its exact handler-owned argument schema.
func (*OpenPRPlanner) Descriptor() connector.Descriptor {
return connector.Descriptor{
Kind: Kind, ConnKind: connector.KindTypedAction, ProtocolV: openPRProtocolVersion, Owner: "sith",
Kind: Kind, ConnKind: connector.KindTypedAction,
WireVersions: []connector.WireVersion{connector.CurrentWireVersion()}, AdapterVersion: openPRProtocolVersion, Owner: "sith",
Capabilities: []connector.Capability{connector.CapPlan},
Verbs: []intent.Verb{intent.VerbGitOpsOpenPR},
ArgSchemas: map[intent.Verb]json.RawMessage{intent.VerbGitOpsOpenPR: json.RawMessage(openPRSchema)},
Expand Down
Loading