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
30 changes: 30 additions & 0 deletions .github/tests/boatstack_test_support.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Shared strict projections for Boatstack repository tests."""

from __future__ import annotations

from collections.abc import Mapping


def prescription_cli_arguments(
prescription: Mapping[str, object], correlation: str
) -> tuple[str, ...]:
"""Project one complete current prescription into raw apply CLI arguments."""
arguments = [
"--correlation",
correlation,
"--prescription-id",
str(prescription["id"]),
"--expected-state-revision",
str(prescription["expected_state_revision"]),
"--expected-program-fingerprint",
str(prescription["expected_program_fingerprint"]),
"--expected-snapshot-fingerprint",
str(prescription["expected_snapshot_fingerprint"]),
"--authority-fingerprint",
str(prescription["authority_fingerprint"]),
]
for capability in prescription["required_capabilities"]:
arguments.extend(("--required-capability", str(capability)))
for capability in prescription["effective_capabilities"]:
arguments.extend(("--effective-capability", str(capability)))
return tuple(arguments)
8 changes: 3 additions & 5 deletions .github/tests/test_detached_supervision.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import unittest
from pathlib import Path

from boatstack_test_support import prescription_cli_arguments


REPO = Path(__file__).resolve().parents[2]
RUNTIME = REPO / "boatstack"
Expand Down Expand Up @@ -100,11 +102,7 @@ def apply_prescribed(
correlation = resolved["snapshot"]["invocation"]["correlation_id"]
return self.helper_json(
"apply", "--transition", transition, *args,
"--correlation", correlation,
"--prescription-id", prescription["id"],
"--expected-state-revision", prescription["expected_state_revision"],
"--expected-program-fingerprint", prescription["expected_program_fingerprint"],
"--expected-snapshot-fingerprint", prescription["expected_snapshot_fingerprint"],
*prescription_cli_arguments(prescription, correlation),
cwd=cwd,
)

Expand Down
46 changes: 46 additions & 0 deletions .github/tests/test_prescription_projection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Contract tests for the shared raw-CLI prescription projection."""

from __future__ import annotations

import unittest

from boatstack_test_support import prescription_cli_arguments


class PrescriptionProjectionContract(unittest.TestCase):
def setUp(self) -> None:
self.prescription = {
"id": "prx-example",
"expected_state_revision": 41,
"expected_program_fingerprint": "a" * 64,
"expected_snapshot_fingerprint": "snapshot-example",
"authority_fingerprint": "auth-example",
"required_capabilities": ["command.execute", "repository.write"],
"effective_capabilities": ["command.execute", "repository.write"],
}

def test_projection_preserves_complete_current_identity(self) -> None:
self.assertEqual(
prescription_cli_arguments(self.prescription, "correlation-example"),
(
"--correlation", "correlation-example",
"--prescription-id", "prx-example",
"--expected-state-revision", "41",
"--expected-program-fingerprint", "a" * 64,
"--expected-snapshot-fingerprint", "snapshot-example",
"--authority-fingerprint", "auth-example",
"--required-capability", "command.execute",
"--required-capability", "repository.write",
"--effective-capability", "command.execute",
"--effective-capability", "repository.write",
),
)

def test_missing_current_identity_fails_closed(self) -> None:
del self.prescription["authority_fingerprint"]
with self.assertRaises(KeyError):
prescription_cli_arguments(self.prescription, "correlation-example")


if __name__ == "__main__":
unittest.main()
19 changes: 11 additions & 8 deletions .github/tests/test_repository_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import xml.etree.ElementTree as ET
from pathlib import Path

from boatstack_test_support import prescription_cli_arguments


REPO = Path(__file__).resolve().parents[2]
RUNTIME = REPO / "boatstack"
Expand Down Expand Up @@ -97,11 +99,7 @@ def apply_prescribed(
correlation = resolved["snapshot"]["invocation"]["correlation_id"]
applied = self.run_command(
binary, "apply", "--transition", transition, *args,
"--correlation", correlation,
"--prescription-id", prescription["id"],
"--expected-state-revision", prescription["expected_state_revision"],
"--expected-program-fingerprint", prescription["expected_program_fingerprint"],
"--expected-snapshot-fingerprint", prescription["expected_snapshot_fingerprint"],
*prescription_cli_arguments(prescription, correlation),
cwd=cwd, env=env,
)
return json.loads(applied.stdout)
Expand Down Expand Up @@ -566,11 +564,16 @@ def test_offline_installer_initializes_updates_and_guards_through_kernel(self) -
self.assertEqual(pin["version"], "v0.7.contract-new")
self.assertEqual(pin["sha256"], hashlib.sha256(self.helper.read_bytes()).hexdigest())
self.assertNotIn("path", pin)
events = self.run_command(
events = [json.loads(line) for line in self.run_command(
launcher, "events", "--repo", repository, "--format", "jsonl", env=env
).stdout.splitlines()
transitions = {json.loads(line)["transition_id"] for line in events}
).stdout.splitlines()]
transitions = {event["transition_id"] for event in events}
self.assertTrue({"installation.initialize", "engagement.begin", "installation.reconcile-update"}.issubset(transitions))
for event in events:
self.assertTrue(event["authority_fingerprint"])
self.assertTrue(event["required_capabilities"])
self.assertTrue(event["granted_capabilities"])
self.assertTrue(event["exercised_capabilities"])

def test_program_changing_update_is_explicit_atomic_and_dormant_safe(self) -> None:
# control-law: accepted-program-delta-atomically-pins-runtime-and-program
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ program-qualified transition identity used by complete user-facing Flows.
The [prescription transaction boundary](docs/architecture/prescription-transactions.md)
defines the exact durable-state and executable-program compare-and-swap contract
between resolution and effects.
The [capability and authority boundary](docs/architecture/capability-authority-boundary.md)
defines how programs narrow effect surfaces without granting themselves
authority, including the explicit arbitrary-command frontier.

## Install

Expand Down
29 changes: 28 additions & 1 deletion boatstack/cmd/boatstack-helper/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ type commandOptions struct {
expectedStateRevision uint64
expectedProgramFingerprint string
expectedSnapshotFingerprint string
authorityFingerprint string
requiredCapabilities stringList
effectiveCapabilities stringList
idempotencyKey string
humanActor string
repositoryPolicy bool
Expand Down Expand Up @@ -252,6 +255,9 @@ func parseOptions(command string, arguments []string, transition catalog.Transit
flags.Uint64Var(&options.expectedStateRevision, "expected-state-revision", 0, "exact durable state revision observed during resolution")
flags.StringVar(&options.expectedProgramFingerprint, "expected-program-fingerprint", "", "exact executable control-program fingerprint observed during resolution")
flags.StringVar(&options.expectedSnapshotFingerprint, "expected-snapshot-fingerprint", "", "exact admission-relevant snapshot fingerprint observed during resolution")
flags.StringVar(&options.authorityFingerprint, "authority-fingerprint", "", "exact authority projection fingerprint from resolution")
flags.Var(&options.requiredCapabilities, "required-capability", "required capability from resolution (repeatable)")
flags.Var(&options.effectiveCapabilities, "effective-capability", "effective capability from resolution (repeatable)")
flags.StringVar(&options.idempotencyKey, "idempotency-key", "", "exact prior admission idempotency key for safe replay")
flags.StringVar(&options.humanActor, "human", "", "explicit command-scoped human authority actor")
flags.BoolVar(&options.repositoryPolicy, "repository-authority", false, "derive repository-policy authority from the V2 project configuration")
Expand Down Expand Up @@ -448,6 +454,14 @@ func buildRequest(operation surfaces.Operation, options commandOptions) (surface
if err != nil {
return surfaces.Request{}, err
}
requiredCapabilities, err := parseCapabilities("--required-capability", options.requiredCapabilities)
if err != nil {
return surfaces.Request{}, err
}
effectiveCapabilities, err := parseCapabilities("--effective-capability", options.effectiveCapabilities)
if err != nil {
return surfaces.Request{}, err
}
flowID := options.flowID
if flowID == "" && goal.ID != "" {
flowID = "flow-" + goal.ID
Expand All @@ -460,11 +474,24 @@ func buildRequest(operation surfaces.Operation, options commandOptions) (surface
FlowID: flowID, Goal: goal, TransitionID: catalog.TransitionID(options.transitionID), Authority: authority, Parameters: parameters,
Prescription: protocol.Prescription{SchemaVersion: protocol.PrescriptionSchemaVersion, ID: options.prescriptionID,
TransitionID: catalog.TransitionID(options.transitionID), ExpectedStateRevision: options.expectedStateRevision,
ExpectedProgramFingerprint: options.expectedProgramFingerprint, ExpectedSnapshotFingerprint: options.expectedSnapshotFingerprint},
ExpectedProgramFingerprint: options.expectedProgramFingerprint, ExpectedSnapshotFingerprint: options.expectedSnapshotFingerprint,
AuthorityFingerprint: options.authorityFingerprint, RequiredCapabilities: requiredCapabilities, EffectiveCapabilities: effectiveCapabilities},
RepositoryAuthority: options.repositoryPolicy, IdempotencyKey: options.idempotencyKey, Command: options.command,
}, nil
}

func parseCapabilities(field string, values []string) ([]catalog.Capability, error) {
capabilities := make([]catalog.Capability, len(values))
for index, value := range values {
capabilities[index] = catalog.Capability(value)
}
normalized, err := catalog.NormalizeCapabilities(field, capabilities)
if err != nil {
return nil, err
}
return normalized, nil
}

func parseParameters(values []string) (protocol.Parameters, error) {
parameters := make(protocol.Parameters, 0, len(values))
for _, value := range values {
Expand Down
32 changes: 32 additions & 0 deletions boatstack/cmd/boatstack-helper/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import (
"os"
"path/filepath"
"testing"
"time"

"github.com/operatorstack/boatstack/boatstack/internal/buildinfo"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
"github.com/operatorstack/boatstack/boatstack/internal/surfaces"
"github.com/operatorstack/boatstack/boatstack/internal/testprogram"
)
Expand Down Expand Up @@ -73,3 +76,32 @@ func TestBuildRevisionPrefersReleaseEmbeddedSourceCommit(t *testing.T) {
t.Fatalf("build revision = %q", got)
}
}

func TestTransitionReceiptCannotBeLoadedAsAuthority(t *testing.T) {
// control-law: historical execution evidence is not a reusable authority token
path := filepath.Join(t.TempDir(), "transition-receipt.json")
if err := os.WriteFile(path, []byte(`{"schema_version":5,"id":"trc-old"}`), 0o600); err != nil {
t.Fatal(err)
}
_, err := loadAuthority(commandOptions{authorityReceipts: stringList{path}}, "correlation", model.Goal{}, time.Now().UTC())
if err == nil {
t.Fatal("transition receipt was accepted as authority")
}
}

func TestRawCLIReconstructsExactCapabilityPrescription(t *testing.T) {
options := commandOptions{
transitionID: "installation.update", host: "cli", repository: ".", prescriptionID: "prx-test",
expectedStateRevision: 7, expectedProgramFingerprint: hash([]byte("program")), expectedSnapshotFingerprint: hash([]byte("snapshot")),
authorityFingerprint: "auth-test",
requiredCapabilities: stringList{string(catalog.CapabilityRepositoryWrite), string(catalog.CapabilityCommandExecute)},
effectiveCapabilities: stringList{string(catalog.CapabilityRepositoryWrite), string(catalog.CapabilityCommandExecute)},
}
request, err := buildRequest(surfaces.OperationApply, options)
if err != nil {
t.Fatal(err)
}
if request.Prescription.AuthorityFingerprint != options.authorityFingerprint || len(request.Prescription.RequiredCapabilities) != 2 || len(request.Prescription.EffectiveCapabilities) != 2 {
t.Fatalf("CLI lost capability prescription: %#v", request.Prescription)
}
}
Loading