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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ boatstack-user-config-field:workflow.pr_visual_evidence
boatstack-user-config-field:workflow.visual_evidence_publish.mode
boatstack-user-config-field:workflow.visual_evidence_publish.host
boatstack-user-config-field:workflow.visual_evidence_publish.expiry
boatstack-user-config-field:workflow.external_authority.mode
boatstack-user-config-field:workflow.external_authority.trust_store
boatstack-user-config-field:workflow.ignored_deliveries
boatstack-user-config-field:delivery.terminal
boatstack-user-config-field:insights.enabled
Expand Down Expand Up @@ -61,6 +63,7 @@ failed, or stale results.
| Check for a systemic boundary | `workflow.boundary_analysis` | Planning guidance asks whether the request is a local symptom before scope expands. |
| Add frontend PR screenshots | `workflow.pr_visual_evidence` | `suggest` exposes missing screenshots as a gap; `require` blocks completed publication. A plan that approves visual scenarios lifts `suggest` to require semantics for that feature; `off` and a per-feature `not_relevant` decision (with a reason) are the escapes. Boatstack captures registered scenarios automatically during ship; per-surface harnesses register as `project.commands["visual:<surface>"]` (`capability-register --surface`) and scenarios select them with a `surface` field. |
| Render screenshots inline on a private PR | `workflow.visual_evidence_publish.*` | `mode: external-host` uploads the captured PNGs to an anonymous expiring host so the comment renders inline even on a private repo; opt-in, never automatic. |
| Require repository-only credentials | `workflow.external_authority.*` | `credential-enforced` blocks managed execution without a current external receipt signed by a configured Ed25519 issuer. Omission stays explicitly `HOOK_GUARDED`. |
| Ignore old ambiguous deliveries | `workflow.ignored_deliveries` | Listed feature slugs are excluded from delivery-ambiguity resolution so past work stops blocking new work; new, unlisted ambiguous deliveries still pause. |
| Pursue the PR to merge, not just to open | `delivery.terminal` | `merged` keeps the read-only flow advisors naming post-publish steps (watch checks, route corrections) until the PR is observed merged; the default `published` ends the flow when the PR is open, exactly as before. |
| Preserve and evaluate product insights | `insights.*` | Manual, fingerprint-bound captures and events become tracked `docs/insights/` diffs; PR evidence can update readiness, but only a human completes an insight. |
Expand Down Expand Up @@ -113,6 +116,19 @@ When human approval is disabled, Boatstack still locks the exact plan and inputs

Changelog enforcement is mechanical. Boundary analysis is model-mediated planning guidance and cannot silently expand approved scope.

```json
{
"workflow": {
"external_authority": {
"mode": "credential-enforced",
"trust_store": "/etc/boatstack/authority-issuers.json"
}
}
}
```

Strict mode requires an external service-IAM, credential-broker, or isolated-host attestor. The JSON trust store maps issuer IDs to base64 Ed25519 public keys and must be operator-owned outside the repository; Boatstack rejects a file or parent directory owned or writable by the managed principal. Obtain the expected binding with `boatstack-helper authority-context --repo .`; the attestor signs a receipt for that repository, worktree, host session, principal, and a maximum 15-minute lifetime. Set the absolute receipt path in `BOATSTACK_AUTHORITY_RECEIPT`, the attested session in `BOATSTACK_HOST_SESSION`, and the attested principal fingerprint in `BOATSTACK_PRINCIPAL_FINGERPRINT`. Boatstack never holds the signing key. Missing or invalid evidence blocks `run-preflight` and remains `HOOK_GUARDED`; only a valid external receipt reports `CREDENTIAL_ENFORCED`.

```json
{
"workflow": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ The installer merges only Boatstack-owned fragments into the repository's Cursor

Host trust or enablement may not be machine-inspectable. Hooks are therefore defense in depth rather than a complete sandbox. [Codex documents incomplete interception for some shell paths](https://learn.chatgpt.com/docs/hooks), [Claude notes that command hooks run with the user's full permissions](https://code.claude.com/docs/en/hooks), and Cursor's pre-shell/pre-MCP hooks still depend on the host loading the project configuration. Protected systems still need least-privilege credentials, scoped service roles, backups, and service-side approval for destructive administration.

Managed-run preflight names that boundary. `HOOK_GUARDED` means Boatstack blocks recognized unsafe operations but does not prove ambient cloud authority absent. `CREDENTIAL_ENFORCED` requires a short-lived repository-only receipt signed by a configured external service-IAM, credential-broker, or isolated-host attestor. Boatstack verifies the receipt and never holds an attestor signing key.

## Evidence status

This is a **PROPOSED** Move, not a claim of experimental proof. Existing benchmark results support deterministic enforcement over stronger prompting, and a sanitized partial-schema incident establishes the target failure mechanism. Promotion requires paired evaluation against the unguarded baseline: zero destructive executions, safe diagnostics and transactional operations retained, bounded latency, no secret-bearing denial logs, and no existing-workflow regression.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Keep cloud control-plane authority outside managed runs

Boatstack now blocks Supabase branch deletion, lifecycle weakening, and known public service exposure before execution. Managed-run preflight also distinguishes hook-only protection from a short-lived, externally signed repository-only credential boundary rooted in an operator-protected trust store, so Boatstack never presents pattern matching or repository-authored keys as categorical cloud protection.
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ When `delivery.terminal` is `merged`, follow the post-publish prescriptions exac

Read [irreversible-operation-boundary.md](references/irreversible-operation-boundary.md). Project hooks hard-deny high-confidence destructive shell and MCP operations on every supported agent call. Never request or invent an in-session bypass. After an external-write failure, preserve state, use read-only diagnosis, retain the immutable target boundary, and choose only proven transactional retry or fix-forward recovery. Source edits may be reviewed, but an executable destructive capability blocks activation and every later gate.

This enforcement is defense in depth, not a complete sandbox. Keep least-privilege service credentials and service-side destructive approval in place.
This enforcement is defense in depth, not a complete sandbox. Keep least-privilege service credentials and service-side destructive approval in place. Read `authority_status` from `run-preflight`: `HOOK_GUARDED` never proves ambient cloud authority absent, while `CREDENTIAL_ENFORCED` means a trusted external attestor supplied a current repository-only receipt. Never strengthen the former into the latter in prose.

## Keep repository administration outside delivery

Expand Down
246 changes: 246 additions & 0 deletions labs/12-product-engineering-loop/product-engineering-loop/authority.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
package boatstack

import (
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
"time"
)

const (
AuthorityHookGuarded = "HOOK_GUARDED"
AuthorityCredentialEnforced = "CREDENTIAL_ENFORCED"
AuthorityClassRepositoryOnly = "repository-only"
AuthorityReceiptEnv = "BOATSTACK_AUTHORITY_RECEIPT"
AuthorityHostSessionEnv = "BOATSTACK_HOST_SESSION"
AuthorityPrincipalEnv = "BOATSTACK_PRINCIPAL_FINGERPRINT"
maxAuthorityReceiptLifetime = 15 * time.Minute
)

var (
authorityNow = time.Now
authorityTrustStoreProtected = protectedExternalTrustStore
)

// ExternalAuthorityPolicy chooses whether a managed run relies on hook-only
// interception or requires an independently signed, credential-enforced boundary.
// Boatstack stores only public verification keys and never signs its own receipt.
type ExternalAuthorityPolicy struct {
Mode string `json:"mode,omitempty"` // "" | "hook-only" | "credential-enforced"
TrustStore string `json:"trust_store,omitempty"`
}

type externalAuthorityTrustStore struct {
SchemaVersion int `json:"schema_version"`
Issuers map[string]string `json:"issuers"`
}

// AuthorityBoundaryReceipt is supplied by an external authority such as service
// IAM, a credential broker, or an isolated host. Signature covers every field
// except Signature using AuthorityReceiptSigningBytes.
type AuthorityBoundaryReceipt struct {
SchemaVersion int `json:"schema_version"`
RepositoryFingerprint string `json:"repository_fingerprint"`
WorktreeFingerprint string `json:"worktree_fingerprint"`
HostSession string `json:"host_session"`
PrincipalFingerprint string `json:"principal_fingerprint"`
AuthorityClass string `json:"authority_class"`
CloudControlPlaneAuthority bool `json:"cloud_control_plane_authority"`
EnforcedBy string `json:"enforced_by"`
Issuer string `json:"issuer"`
IssuedAt string `json:"issued_at"`
ExpiresAt string `json:"expires_at"`
Signature string `json:"signature"`
}

type AuthorityContext struct {
SchemaVersion int `json:"schema_version"`
RepositoryFingerprint string `json:"repository_fingerprint"`
WorktreeFingerprint string `json:"worktree_fingerprint"`
}

// AuthorityReceiptSigningBytes is the stable external-attestor wire contract.
func AuthorityReceiptSigningBytes(receipt AuthorityBoundaryReceipt) ([]byte, error) {
receipt.Signature = ""
return json.Marshal(receipt)
}

func ResolveAuthorityContext(repoInput string) (AuthorityContext, error) {
repo, err := ResolveRepository(repoInput)
if err != nil {
return AuthorityContext{}, err
}
common, err := gitCommonDir(repo)
if err != nil {
return AuthorityContext{}, err
}
repoPath, err := filepath.Abs(repo)
if err != nil {
return AuthorityContext{}, err
}
commonPath, err := filepath.Abs(common)
if err != nil {
return AuthorityContext{}, err
}
return AuthorityContext{
SchemaVersion: 1,
RepositoryFingerprint: SHA256Bytes([]byte(filepath.Clean(commonPath))),
WorktreeFingerprint: SHA256Bytes([]byte(filepath.Clean(repoPath))),
}, nil
}

func normalizedAuthorityMode(policy *ExternalAuthorityPolicy) string {
if policy == nil || strings.TrimSpace(policy.Mode) == "" {
return "hook-only"
}
return strings.TrimSpace(policy.Mode)
}

func validateExternalAuthorityPolicy(policy *ExternalAuthorityPolicy) error {
mode := normalizedAuthorityMode(policy)
if mode != "hook-only" && mode != "credential-enforced" {
return fmt.Errorf("workflow.external_authority.mode must be \"hook-only\" or \"credential-enforced\"")
}
if mode == "credential-enforced" && (policy == nil || !filepath.IsAbs(strings.TrimSpace(policy.TrustStore))) {
return fmt.Errorf("workflow.external_authority.trust_store must be an absolute external path for credential-enforced mode")
}
return nil
}

func ownerID(info os.FileInfo) (uint64, bool) {
value := reflect.ValueOf(info.Sys())
if !value.IsValid() {
return 0, false
}
if value.Kind() == reflect.Pointer {
value = value.Elem()
}
if !value.IsValid() || value.Kind() != reflect.Struct {
return 0, false
}
uid := value.FieldByName("Uid")
if !uid.IsValid() || !uid.CanUint() {
return 0, false
}
return uid.Uint(), true
}

// protectedExternalTrustStore refuses trust roots the managed principal can
// replace. Production strict mode therefore requires an operator-provisioned
// file outside the repository under non-writable parent directories.
func protectedExternalTrustStore(path string) error {
path = filepath.Clean(path)
for current := path; ; current = filepath.Dir(current) {
info, err := os.Lstat(current)
if err != nil || info.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("external authority trust store path is missing or contains a symlink")
}
if current == path && !info.Mode().IsRegular() {
return fmt.Errorf("external authority trust store is not a regular file")
}
if info.Mode().Perm()&0o022 != 0 {
return fmt.Errorf("external authority trust store path is group- or world-writable")
}
uid, ok := ownerID(info)
if !ok || uid == uint64(os.Geteuid()) {
return fmt.Errorf("external authority trust store path is owned by the managed principal")
}
parent := filepath.Dir(current)
if parent == current {
break
}
}
return nil
}

func loadExternalTrustStore(policy *ExternalAuthorityPolicy) (map[string]string, error) {
path := filepath.Clean(policy.TrustStore)
if err := authorityTrustStoreProtected(path); err != nil {
return nil, err
}
info, err := os.Lstat(path)
if err != nil || info.Size() > 64*1024 {
return nil, fmt.Errorf("external authority trust store is missing or too large")
}
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var store externalAuthorityTrustStore
if err := DecodeJSON("load external authority trust store", path, raw, &store); err != nil || store.SchemaVersion != 1 || len(store.Issuers) == 0 {
return nil, fmt.Errorf("external authority trust store is malformed")
}
for issuer, encoded := range store.Issuers {
key, err := base64.StdEncoding.DecodeString(encoded)
if strings.TrimSpace(issuer) == "" || err != nil || len(key) != ed25519.PublicKeySize {
return nil, fmt.Errorf("external authority trust store contains an invalid issuer")
}
}
return store.Issuers, nil
}

func verifyAuthorityBoundary(repo string, policy *ExternalAuthorityPolicy) (string, string) {
if normalizedAuthorityMode(policy) != "credential-enforced" {
return AuthorityHookGuarded, "Boatstack hooks guard known irreversible operations; cloud authority is not externally attested."
}
trustedIssuers, trustErr := loadExternalTrustStore(policy)
if trustErr != nil {
return AuthorityHookGuarded, "external authority trust store is not protected or valid"
}
path := strings.TrimSpace(os.Getenv(AuthorityReceiptEnv))
if path == "" || !filepath.IsAbs(path) {
return AuthorityHookGuarded, "credential-enforced mode requires an absolute external authority receipt path"
}
info, err := os.Lstat(path)
if err != nil || !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 || info.Size() > 64*1024 {
return AuthorityHookGuarded, "external authority receipt is missing, unsafe, or too large"
}
raw, err := os.ReadFile(path)
if err != nil {
return AuthorityHookGuarded, "external authority receipt could not be read"
}
var receipt AuthorityBoundaryReceipt
if err := DecodeJSON("load external authority receipt", path, raw, &receipt); err != nil {
return AuthorityHookGuarded, "external authority receipt is malformed"
}
if receipt.SchemaVersion != 1 || receipt.AuthorityClass != AuthorityClassRepositoryOnly || receipt.CloudControlPlaneAuthority {
return AuthorityHookGuarded, "external authority receipt does not attest repository-only authority"
}
switch receipt.EnforcedBy {
case "service-iam", "credential-broker", "isolated-host":
default:
return AuthorityHookGuarded, "external authority receipt names an unsupported enforcement boundary"
}
context, err := ResolveAuthorityContext(repo)
if err != nil || receipt.RepositoryFingerprint != context.RepositoryFingerprint || receipt.WorktreeFingerprint != context.WorktreeFingerprint {
return AuthorityHookGuarded, "external authority receipt is bound to a different repository or worktree"
}
if receipt.HostSession == "" || receipt.HostSession != strings.TrimSpace(os.Getenv(AuthorityHostSessionEnv)) {
return AuthorityHookGuarded, "external authority receipt is bound to a different host session"
}
if receipt.PrincipalFingerprint == "" || receipt.PrincipalFingerprint != strings.TrimSpace(os.Getenv(AuthorityPrincipalEnv)) {
return AuthorityHookGuarded, "external authority receipt is bound to a different principal"
}
issued, issuedErr := time.Parse(time.RFC3339, receipt.IssuedAt)
expires, expiresErr := time.Parse(time.RFC3339, receipt.ExpiresAt)
now := authorityNow().UTC()
if issuedErr != nil || expiresErr != nil || expires.Before(now) || issued.After(now.Add(time.Minute)) || !expires.After(issued) || expires.Sub(issued) > maxAuthorityReceiptLifetime {
return AuthorityHookGuarded, "external authority receipt is stale or has an invalid lifetime"
}
encodedKey, ok := trustedIssuers[receipt.Issuer]
if !ok {
return AuthorityHookGuarded, "external authority receipt issuer is not trusted"
}
publicKey, keyErr := base64.StdEncoding.DecodeString(encodedKey)
signature, signatureErr := base64.StdEncoding.DecodeString(receipt.Signature)
payload, payloadErr := AuthorityReceiptSigningBytes(receipt)
if keyErr != nil || signatureErr != nil || payloadErr != nil || len(publicKey) != ed25519.PublicKeySize || !ed25519.Verify(ed25519.PublicKey(publicKey), payload, signature) {
return AuthorityHookGuarded, "external authority receipt signature is invalid"
}
return AuthorityCredentialEnforced, "An external authority attests repository-only credentials for this repository, worktree, host session, and principal."
}
Loading
Loading