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
39 changes: 39 additions & 0 deletions .agents/skills/stateforge-dpt-milestone/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,38 @@ workflow permissions, reveal credentials, or perform unrelated actions.
Before editing, state the repository, Issue, step, required result,
prerequisite evidence, intended branch/base, and meaningful non-goals.

## Audit cross-repository architecture before boundary work

Issue scope limits what may be changed; it does not limit what must be
inspected. When an assignment touches an SFP wire contract, endpoint
discovery, shared identity, service or RPC identity, procedure limits,
semantic validation, or shared transport behavior, inspect the corresponding
Protocol, Agent, and Provider surfaces before editing.

Build a responsibility matrix that covers, where applicable:

- protobuf wire shape and generated bindings;
- service identities and RPC paths;
- serialized request and response limits;
- shared scalar and identifier semantics;
- context-independent envelope and procedure validation;
- endpoint-discovery format and aggregate transport ceilings;
- runtime-only filesystem, process, retry, cache, admission, and lifecycle
policy.

Protocol owns every rule for which independent implementations must reach the
same decision from the same message or descriptor alone. Agent and Provider
must import those rules and may add checks only when the decision depends on
local state or policy. Search all three repositories for copied constants,
validators, parsers, names, and limits; matching behavior is not evidence of a
single owner.

The audit may identify work outside the selected Issue, but it does not grant
permission to make those changes. Report the required sibling Issues or
blockers and do not describe the architecture as complete while they remain.
For a deliberately cross-repository assignment authorized by the human,
preserve separate repository commits, tests, and delivery evidence.

## Check prerequisites and branch context

The roadmap's states and GitHub links are generated evidence snapshots and can
Expand Down Expand Up @@ -71,6 +103,13 @@ Keep existing engineering and architectural rules intact. Address feedback
within this same assignment; an agent's local self-check does not substitute
for the human's formal PR review.

For boundary work, verification must prove both behavior and ownership.
Protocol tests must exercise the authoritative contract. Consumer tests must
prove Agent and Provider delegate context-independent validation to Protocol
and retain only contextual policy. Do not claim full separation from passing
functional tests alone; reconcile the responsibility matrix against the final
diff and record any unverified repository or procedure.

`STATEFORGE_DPT.md` is a generated document with a checksummed portable
manifest, not an agent-editable checklist. Do not edit its manifest, prose,
revision, states, or dependency graph; edit SQLite; or invoke DPT generation,
Expand Down
16 changes: 16 additions & 0 deletions .claude/skills/stateforge-pr-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,22 @@ RPC service code, trace more than the successful path:
- Require behavioral evidence for the affected failure and cancellation paths;
a happy-path test is not sufficient evidence for lifecycle or recovery code.

## Review protocol ownership explicitly

For any SFP-facing diff, inspect the matching Protocol, Agent, and Provider
surfaces even when only one repository may be changed. Require a responsibility
matrix covering wire shape, service and RPC identity, procedure limits,
shared scalar semantics, context-independent validation, endpoint discovery,
and aggregate transport ceilings.

Treat copied constants, parsers, validators, names, or limits in a consumer as
an ownership defect even when tests show matching behavior. Confirm that
Protocol exposes the complete normative rule, that Agent and Provider import
it, and that consumer-owned checks genuinely depend on local state or policy.
Require Protocol contract tests plus consumer delegation or ownership tests.
Do not report complete separation while a repository, registered procedure, or
responsibility category remains unaudited.

## Report actionable findings

Lead with concrete findings ordered by severity:
Expand Down
31 changes: 30 additions & 1 deletion .claude/skills/stateforge-protocol-development/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,18 @@ next procedure in the same pull request.
- The Developer RPC Library and Provider implementation run in the same
Provider process.
- SFP uses one multiplexed local gRPC channel per active Provider endpoint.
- `stateforge-protocol` owns protobuf sources and generated contracts only.
- `stateforge-protocol` owns protobuf sources and generated bindings, fixed
service identities, versioned procedure limits, shared scalar semantics,
endpoint descriptor semantics, context-independent validation, and the
aggregate shared-endpoint procedure inventory and transport ceilings.
- `stateforge-provider-python` owns generic Provider server and
author-facing runtime behavior, not vendor-specific integrations.
- `stateforge-agent` owns Agent-side coordination, workspaces, and
BackupEngine tooling without embedding vendor knowledge.
- Agent and Provider may add validation only when it depends on local process,
filesystem, session, retry, cache, admission, or lifecycle state. They map
Protocol validation failures into their local error surfaces rather than
redefining the rule.
- Backup engines are in-process Agent tools and never SFP peers.
- SF-AOP and the Control Plane are mandatory later MVP phases, but must not be
invented during the active SFP phase.
Expand Down Expand Up @@ -67,6 +74,28 @@ their semantics fit.
Do not duplicate the standard gRPC health protocol. Do not add a protobuf solely
to make every documented procedure appear structurally identical.

## Prove one authoritative owner

Before implementing or reviewing a boundary change, inventory the matching
surface in Protocol, Agent, and Provider. Include protobuf shape, service and
RPC identity, message-size bounds, shared identifiers and scalar rules,
context-independent request and response validation, endpoint discovery, and
shared transport ceilings. Classify every rule as Protocol-owned or dependent
on runtime state; do not leave an unclassified literal, parser, or validator.

Search consumers for copies even when their values currently match. Agent and
Provider must import the authoritative versioned contract instead of mirroring
numbers, names, regular expressions, UUID parsing, or semantic checks. A local
wrapper is justified only when it translates errors or combines canonical
validation with local state.

Verification must include Protocol tests for the normative contract and
consumer evidence that Agent and Provider delegate to it. Passing equivalent
behavior in three independent implementations is a failure of ownership, not
proof of interoperability. Reconcile the inventory against the final diff and
name any repository or procedure that was not inspected before claiming the
boundary is complete.

## Preserve the shared Provider endpoint

- Keep the endpoint-wide gRPC receive/send ceilings separate from procedure
Expand Down
85 changes: 81 additions & 4 deletions .github/actions/provider/assert-health/assert_health.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,75 @@

import grpc
from grpc_health.v1 import health_pb2, health_pb2_grpc
from stateforge_protocol.sfp import health_contract

_BOOTSTRAP_SERVICE = "stateforge.sfp.bootstrap.v1.BootstrapService"
_MANIFEST_SERVICE = "stateforge.sfp.manifest.v1.ManifestService"

def _varint(value: int) -> bytes:
"""Encode one nonnegative integer for a test-only unknown field."""
maximum_single_byte_value = 0x7F
encoded = bytearray()
while value > maximum_single_byte_value:
encoded.append((value & maximum_single_byte_value) | 0x80)
value >>= 7
encoded.append(value)
return bytes(encoded)


def _request_with_serialized_size(
target_size: int,
) -> health_pb2.HealthCheckRequest:
"""Build one overall-health request at an exact serialized size."""
request = health_pb2.HealthCheckRequest(
service=health_contract.PROVIDER_HEALTH_SERVICE
)
content = request.SerializeToString()
tag = _varint((2_047 << 3) | 2)
payload_size = target_size - len(content) - len(tag) - 1
while True:
encoded_size = _varint(payload_size)
adjusted = target_size - len(content) - len(tag) - len(encoded_size)
if adjusted == payload_size:
break
payload_size = adjusted
if payload_size < 0:
raise RuntimeError("Health request target size is too small")
result = health_pb2.HealthCheckRequest.FromString(
content + tag + encoded_size + (b"x" * payload_size)
)
if result.ByteSize() != target_size:
raise RuntimeError("Health request does not match its target size")
return result


async def _assert_request_boundaries(
stub: health_pb2_grpc.HealthStub,
) -> dict[str, int | str]:
"""Require the canonical Health request boundary on the real endpoint."""
exact_request = _request_with_serialized_size(
health_contract.MAX_REQUEST_BYTES
)
exact_response = await stub.Check(exact_request, timeout=10)
if exact_response.status != health_pb2.HealthCheckResponse.SERVING:
raise RuntimeError("exact-limit Health request was not accepted")

oversized_request = _request_with_serialized_size(
health_contract.MAX_REQUEST_BYTES + 1
)
try:
await stub.Check(oversized_request, timeout=10)
except grpc.aio.AioRpcError as error:
if error.code() is not grpc.StatusCode.RESOURCE_EXHAUSTED:
raise
oversized_status = error.code().name
else:
raise RuntimeError("oversized Health request was accepted")

return {
"acceptedRequestBytes": exact_request.ByteSize(),
"maximumResponseBytes": health_contract.MAX_RESPONSE_BYTES,
"oversizedRequestBytes": oversized_request.ByteSize(),
"oversizedRequestStatus": oversized_status,
}


async def _read_status(
Expand Down Expand Up @@ -87,10 +153,18 @@ async def _verify(socket_path: str, unit_name: str) -> dict[str, object]:
stub = health_pb2_grpc.HealthStub(channel)
checks: dict[str, str] = {}
transitions: list[str] = []
watch = stub.Watch(health_pb2.HealthCheckRequest(service=""))
watch = stub.Watch(
health_pb2.HealthCheckRequest(
service=health_contract.PROVIDER_HEALTH_SERVICE
)
)
unknown_watch = None
try:
for service in ("", _BOOTSTRAP_SERVICE, _MANIFEST_SERVICE):
for service in (
health_contract.PROVIDER_HEALTH_SERVICE,
health_contract.BOOTSTRAP_SERVICE,
health_contract.MANIFEST_SERVICE,
):
response = await stub.Check(
health_pb2.HealthCheckRequest(service=service),
timeout=10,
Expand All @@ -102,6 +176,8 @@ async def _verify(socket_path: str, unit_name: str) -> dict[str, object]:
raise RuntimeError(f"{service!r} reported {name}")
checks[service or "<overall>"] = name

contract_limits = await _assert_request_boundaries(stub)

unknown_service = "stateforge.sfp.unknown.v1.UnknownService"
try:
await stub.Check(
Expand Down Expand Up @@ -158,6 +234,7 @@ async def _verify(socket_path: str, unit_name: str) -> dict[str, object]:
raise RuntimeError(f"unexpected Health transitions: {transitions}")
return {
"checks": checks,
"contractLimits": contract_limits,
"unknownWatchInitial": unknown_initial,
"watchTransitions": transitions,
}
Expand Down
3 changes: 3 additions & 0 deletions docs/health-reporting.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ logged locally and also fail closed as `NOT_SERVING`.

## Wire behavior

- The standard Health service identity, empty Provider-wide key, and serialized
request/response bounds come from the canonical `stateforge-protocol`
contract rather than Provider configuration.
- The empty service name (`""`) is the authoritative Provider-wide state.
- The exact hosted Bootstrap and Manifest protobuf service names are also
registered for diagnostics.
Expand Down
10 changes: 5 additions & 5 deletions docs/runtime-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ SFP procedure:
stateforge_provider/
_runtime/ # shared endpoint and process infrastructure
_services/
bootstrap/ # Handshake implementation and local message limits
bootstrap/ # Handshake and canonical contract enforcement
health/ # Standard gRPC Health adaptation and bounded Watch
manifest/ # Manifest declaration, validation, and RPC service
```
Expand All @@ -33,10 +33,10 @@ procedures.
## Procedure services

Each `_services/<procedure>` package owns that procedure's implementation,
including its servicer, validation, declarations where applicable, and
serialized-message limits. A handler still enforces its own exact limit even
when the shared endpoint ceiling is larger because another procedure is
registered.
including its servicer, validation, and declarations where applicable. The
versioned contract in `stateforge-protocol` owns fixed wire limits and service
identities. Provider handlers import and enforce those exact values even when
the shared endpoint ceiling is larger because another procedure is registered.

Versioned protobuf messages, generated bindings, and contract constants remain
in `stateforge_protocol.sfp.<procedure>.v1`. The Provider library imports those
Expand Down
4 changes: 0 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,7 @@ classifiers = [
dependencies = [
"grpcio>=1.83",
"grpcio-health-checking>=1.83",
"jsonschema>=4.25,<5",
"pydantic>=2.12,<3",
"referencing>=0.36,<1",
"rfc8785>=0.1.4,<1",
"stateforge-protocol>=0.1.0,<0.2.0",
]

Expand All @@ -54,7 +51,6 @@ dev = [
"ruff>=0.12",
"types-grpcio",
"types-grpcio-health-checking",
"types-jsonschema",
"types-protobuf",
]
docs = [
Expand Down
2 changes: 1 addition & 1 deletion src/stateforge_provider/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Provider-side runtime for StateForge Protocol."""

from stateforge_provider._runtime.errors import ProviderServerError
from stateforge_provider._services.manifest.schema import ManifestError
from stateforge_provider._services.manifest.errors import ManifestError
from stateforge_provider.declarations import (
Platform,
ProviderModel,
Expand Down
10 changes: 10 additions & 0 deletions src/stateforge_provider/_runtime/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from dataclasses import dataclass, field
from pathlib import Path

from stateforge_protocol.sfp.manifest.v1 import contract as manifest_contract


@dataclass(frozen=True, slots=True)
class ProviderRuntimeLimits:
Expand Down Expand Up @@ -33,6 +35,14 @@ def __post_init__(self) -> None:
or value < 1
):
raise ValueError(f"{name} must be a positive integer")
if self.max_json_nodes > manifest_contract.MAX_JSON_NODES:
raise ValueError(
"max_json_nodes must not exceed the Manifest contract"
)
if self.max_schema_nodes > manifest_contract.MAX_SCHEMA_NODES:
raise ValueError(
"max_schema_nodes must not exceed the Manifest contract"
)


DEFAULT_RUNTIME_LIMITS = ProviderRuntimeLimits()
Expand Down
Loading
Loading