From 4529ed3a47a79b95997d5fd54f380807665826d6 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sun, 5 Jul 2026 03:02:58 -0400 Subject: [PATCH] feat: add signal trust object backend --- README.md | 9 ++ internal/contracts/signal.pb.go | 13 +- internal/contracts/signal.proto | 1 + internal/trust_store.go | 120 +++++++++++++- internal/trust_store_test.go | 176 +++++++++++++++++++++ proto/signal.pb.go | 13 +- proto/signal.proto | 1 + scripts/run-pipeline-tests.sh | 1 + testdata/pipelines/signal-trust-reset.yaml | 4 +- 9 files changed, 332 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e617146..929cafa 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,15 @@ composition tests. The `local_file` backend requires reloads after restart, rejects corrupt snapshots, and appends JSONL audit rows for persistent trust decisions. `production_policy: true` rejects `local_file` unless `allow_production_local_file: true` is explicitly set by the host. The +`object_store` backend is a local object-store emulator for scenarios and host +integration tests that need durable object-shaped trust custody before a live +cloud adapter is approved. It requires `allow_object_store_backend: true` and +`storage_path`, writes one checksum-protected trust snapshot object under a +hash-derived key for the `store_ref`, reloads after restart, and rejects +tampered snapshots or same-store objects found under the wrong key. It is +always rejected under `production_policy: true`; production deployments should +use the host-managed HTTP backend or a future approved storage adapter with its +own credential, audit, and abuse-control boundary. The `http` backend delegates trust snapshot custody to a host-managed service. It requires `endpoint_url`, rejects URL userinfo, rejects plain `http://` unless `allow_insecure_http: true` is set for local loopback fixtures, optionally sends diff --git a/internal/contracts/signal.pb.go b/internal/contracts/signal.pb.go index 5f3dbdf..395c5de 100644 --- a/internal/contracts/signal.pb.go +++ b/internal/contracts/signal.pb.go @@ -656,6 +656,7 @@ type SignalTrustStoreConfig struct { AuthHeaderEnv string `protobuf:"bytes,11,opt,name=auth_header_env,json=authHeaderEnv,proto3" json:"auth_header_env,omitempty"` RequestTimeoutMs uint32 `protobuf:"varint,12,opt,name=request_timeout_ms,json=requestTimeoutMs,proto3" json:"request_timeout_ms,omitempty"` AllowInsecureHttp bool `protobuf:"varint,13,opt,name=allow_insecure_http,json=allowInsecureHttp,proto3" json:"allow_insecure_http,omitempty"` + AllowObjectStoreBackend bool `protobuf:"varint,14,opt,name=allow_object_store_backend,json=allowObjectStoreBackend,proto3" json:"allow_object_store_backend,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -781,6 +782,13 @@ func (x *SignalTrustStoreConfig) GetAllowInsecureHttp() bool { return false } +func (x *SignalTrustStoreConfig) GetAllowObjectStoreBackend() bool { + if x != nil { + return x.AllowObjectStoreBackend + } + return false +} + type SignalTrustObserveConfig struct { state protoimpl.MessageState `protogen:"open.v1"` StoreRef string `protobuf:"bytes,1,opt,name=store_ref,json=storeRef,proto3" json:"store_ref,omitempty"` @@ -13638,7 +13646,7 @@ const file_internal_contracts_signal_proto_rawDesc = "" + "\adisplay\x18\x02 \x01(\tR\adisplay\x12#\n" + "\rscannable_hex\x18\x03 \x01(\tR\fscannableHex\x12'\n" + "\x0fmatched_display\x18\x04 \x01(\bR\x0ematchedDisplay\x12+\n" + - "\x11matched_scannable\x18\x05 \x01(\bR\x10matchedScannable\"\xa2\x04\n" + + "\x11matched_scannable\x18\x05 \x01(\bR\x10matchedScannable\"\xdf\x04\n" + "\x16SignalTrustStoreConfig\x12\x1b\n" + "\tstore_ref\x18\x01 \x01(\tR\bstoreRef\x12\x18\n" + "\abackend\x18\x02 \x01(\tR\abackend\x12!\n" + @@ -13654,7 +13662,8 @@ const file_internal_contracts_signal_proto_rawDesc = "" + " \x01(\tR\x0eauthHeaderName\x12&\n" + "\x0fauth_header_env\x18\v \x01(\tR\rauthHeaderEnv\x12,\n" + "\x12request_timeout_ms\x18\f \x01(\rR\x10requestTimeoutMs\x12.\n" + - "\x13allow_insecure_http\x18\r \x01(\bR\x11allowInsecureHttp\"\xea\x01\n" + + "\x13allow_insecure_http\x18\r \x01(\bR\x11allowInsecureHttp\x12;\n" + + "\x1aallow_object_store_backend\x18\x0e \x01(\bR\x17allowObjectStoreBackend\"\xea\x01\n" + "\x18SignalTrustObserveConfig\x12\x1b\n" + "\tstore_ref\x18\x01 \x01(\tR\bstoreRef\x12\x16\n" + "\x06policy\x18\x02 \x01(\tR\x06policy\x12\x18\n" + diff --git a/internal/contracts/signal.proto b/internal/contracts/signal.proto index 180d64b..5f9f9b6 100644 --- a/internal/contracts/signal.proto +++ b/internal/contracts/signal.proto @@ -82,6 +82,7 @@ message SignalTrustStoreConfig { string auth_header_env = 11; uint32 request_timeout_ms = 12; bool allow_insecure_http = 13; + bool allow_object_store_backend = 14; } message SignalTrustObserveConfig { diff --git a/internal/trust_store.go b/internal/trust_store.go index b481ac2..fbedf01 100644 --- a/internal/trust_store.go +++ b/internal/trust_store.go @@ -24,6 +24,7 @@ const ( signalTrustBackendMemory = "memory" signalTrustBackendLocalFile = "local_file" signalTrustBackendHTTP = "http" + signalTrustBackendObject = "object_store" signalTrustSchemaVersion = 1 defaultSignalTrustHTTPTimeout = 5 * time.Second @@ -84,6 +85,13 @@ type signalTrustSnapshot struct { State signalTrustSnapshotStateV1 `json:"state"` } +type signalTrustObject struct { + SchemaVersion int `json:"schema_version"` + Backend string `json:"backend"` + StoreRef string `json:"store_ref"` + Snapshot *signalTrustSnapshot `json:"snapshot"` +} + type signalTrustSnapshotStateV1 struct { Records map[string]*signalTrustRecord `json:"records"` Events []*signalTrustEvent `json:"events,omitempty"` @@ -174,6 +182,16 @@ func (m *signalTrustStoreModule) Init() error { if cfg.GetProductionPolicy() && !cfg.GetAllowProductionLocalFile() { return fmt.Errorf("signal trust store: production policy rejects local_file backend") } + case signalTrustBackendObject: + if !cfg.GetAllowObjectStoreBackend() { + return fmt.Errorf("signal trust store: object_store backend requires allow_object_store_backend") + } + if cfg.GetStoragePath() == "" { + return fmt.Errorf("signal trust store: storage_path is required") + } + if cfg.GetProductionPolicy() { + return fmt.Errorf("signal trust store: production policy rejects object_store backend") + } case signalTrustBackendHTTP: parsed, client, err := signalTrustHTTPConfig(cfg) if err != nil { @@ -208,7 +226,7 @@ func (m *signalTrustStoreModule) Init() error { records: map[string]*signalTrustRecord{}, events: []*signalTrustEvent{}, } - if backend == signalTrustBackendLocalFile { + if backend == signalTrustBackendLocalFile || backend == signalTrustBackendObject { if err := store.load(); err != nil { return err } @@ -457,6 +475,8 @@ func (s *signalTrustStore) load() error { switch s.backend { case signalTrustBackendLocalFile: return s.loadLocalFile() + case signalTrustBackendObject: + return s.loadObjectStore() case signalTrustBackendHTTP: return s.loadHTTP(context.Background()) default: @@ -482,6 +502,68 @@ func (s *signalTrustStore) loadLocalFile() error { return nil } +func (s *signalTrustStore) loadObjectStore() error { + root := filepath.Join(s.path, "trust") + if err := os.MkdirAll(root, 0o700); err != nil { + return fmt.Errorf("signal trust store: create object store dir: %w", err) + } + var loaded bool + err := filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error { + if err != nil { + return err + } + if entry.IsDir() || entry.Name() != "snapshot.json" { + return nil + } + object, err := s.readObjectStoreSnapshot(path) + if err != nil { + return err + } + if object.StoreRef != s.ref { + return nil + } + if loaded { + return fmt.Errorf("signal trust store: object store contains duplicate snapshot for %q", safeSignalTrustRefForError(s.ref)) + } + if path != s.objectStoreSnapshotPath() { + return fmt.Errorf("signal trust store: object key mismatch for %q", safeSignalTrustRefForError(s.ref)) + } + if object.Snapshot == nil { + return fmt.Errorf("signal trust store: object snapshot is required") + } + if err := s.applySnapshot(*object.Snapshot); err != nil { + return err + } + loaded = true + return nil + }) + if err != nil { + return fmt.Errorf("signal trust store: read object store: %w", err) + } + return nil +} + +func (s *signalTrustStore) readObjectStoreSnapshot(path string) (*signalTrustObject, error) { + raw, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("signal trust store: read object %q: %w", filepath.Base(path), err) + } + var object signalTrustObject + if err := json.Unmarshal(raw, &object); err != nil { + return nil, fmt.Errorf("signal trust store: decode object %q: %w", filepath.Base(path), err) + } + if object.SchemaVersion != signalTrustSchemaVersion { + return nil, fmt.Errorf("signal trust store: unsupported object version %d", object.SchemaVersion) + } + if object.Backend != signalTrustBackendObject { + return nil, fmt.Errorf("signal trust store: object backend=%q, want %q", object.Backend, signalTrustBackendObject) + } + if object.StoreRef == "" { + return nil, fmt.Errorf("signal trust store: object store_ref is required") + } + return &object, nil +} + func (s *signalTrustStore) applySnapshot(snapshot signalTrustSnapshot) error { if snapshot.SchemaVersion != signalTrustSchemaVersion { return fmt.Errorf("signal trust store: unsupported storage version %d", snapshot.SchemaVersion) @@ -511,6 +593,8 @@ func (s *signalTrustStore) persistLocked() error { switch s.backend { case signalTrustBackendLocalFile: return s.persistLocalFileLocked() + case signalTrustBackendObject: + return s.persistObjectStoreLocked() case signalTrustBackendHTTP: return s.persistHTTPLocked(context.Background()) default: @@ -557,6 +641,40 @@ func (s *signalTrustStore) persistLocalFileLocked() error { return nil } +func (s *signalTrustStore) persistObjectStoreLocked() error { + snapshot, err := s.snapshotLocked() + if err != nil { + return err + } + object := signalTrustObject{ + SchemaVersion: signalTrustSchemaVersion, + Backend: signalTrustBackendObject, + StoreRef: s.ref, + Snapshot: snapshot, + } + raw, err := json.MarshalIndent(object, "", " ") + if err != nil { + return fmt.Errorf("signal trust store: encode object: %w", err) + } + objectPath := s.objectStoreSnapshotPath() + if err := os.MkdirAll(filepath.Dir(objectPath), 0o700); err != nil { + return fmt.Errorf("signal trust store: create object dir: %w", err) + } + tmp := objectPath + ".tmp" + if err := os.WriteFile(tmp, raw, 0o600); err != nil { + return fmt.Errorf("signal trust store: write object temp: %w", err) + } + if err := os.Rename(tmp, objectPath); err != nil { + return fmt.Errorf("signal trust store: replace object: %w", err) + } + return nil +} + +func (s *signalTrustStore) objectStoreSnapshotPath() string { + sum := sha256.Sum256([]byte(s.ref)) + return filepath.Join(s.path, "trust", hex.EncodeToString(sum[:]), "snapshot.json") +} + func (s *signalTrustStore) auditLocked(entry signalTrustAuditEntry) error { event := s.appendEventLocked(entry) if err := s.persistLocked(); err != nil { diff --git a/internal/trust_store_test.go b/internal/trust_store_test.go index fb90788..5b207f4 100644 --- a/internal/trust_store_test.go +++ b/internal/trust_store_test.go @@ -257,6 +257,161 @@ func TestSignalTrustStoreRejectsCorruptLocalFileAndProductionLocalFile(t *testin } } +func TestSignalTrustStoreObjectBackendRejectsUnsafeConfig(t *testing.T) { + resetSignalTrustStoresForTest(t) + storeRoot := filepath.Join(t.TempDir(), "objects") + + for _, tc := range []struct { + name string + cfg *contracts.SignalTrustStoreConfig + }{ + { + name: "requires allow flag", + cfg: &contracts.SignalTrustStoreConfig{ + StoreRef: "trust-main", + Backend: "object_store", + StoragePath: storeRoot, + }, + }, + { + name: "requires storage path", + cfg: &contracts.SignalTrustStoreConfig{ + StoreRef: "trust-main", + Backend: "object_store", + AllowObjectStoreBackend: true, + }, + }, + { + name: "production rejects local object store", + cfg: &contracts.SignalTrustStoreConfig{ + StoreRef: "trust-main", + Backend: "object_store", + StoragePath: storeRoot, + AllowObjectStoreBackend: true, + ProductionPolicy: true, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + module := newSignalTrustStoreModule("trust-main", tc.cfg) + if err := module.Init(); err == nil { + t.Fatal("unsafe object trust store config succeeded") + } + }) + } +} + +func TestSignalTrustStoreObjectBackendPersistsReloadsAndRejectsChangedKey(t *testing.T) { + resetSignalTrustStoresForTest(t) + storeRoot := filepath.Join(t.TempDir(), "objects") + initTrustStoreForTest(t, "trust-main", &contracts.SignalTrustStoreConfig{ + StoreRef: "trust-main", + Backend: "object_store", + StoragePath: storeRoot, + AllowObjectStoreBackend: true, + }) + + first := observeTrustForTest(t, "trust-main", "pair://tenant/user-a/user-b", trustTestRemoteKey, "trust_on_first_use") + if first.Output.GetStatus() != "new_trust" { + t.Fatalf("first status = %q", first.Output.GetStatus()) + } + objects := signalTrustObjectStoreFilesForTest(t, storeRoot) + if len(objects) != 1 { + t.Fatalf("object store wrote %d objects, want 1: %v", len(objects), objects) + } + if strings.Contains(objects[0], "trust-main") { + t.Fatalf("object key leaked raw store ref: %s", objects[0]) + } + + resetSignalTrustStoresForTest(t) + initTrustStoreForTest(t, "trust-main", &contracts.SignalTrustStoreConfig{ + StoreRef: "trust-main", + Backend: "object_store", + StoragePath: storeRoot, + AllowObjectStoreBackend: true, + }) + second := observeTrustForTest(t, "trust-main", "pair://tenant/user-a/user-b", trustTestRemoteKey, "require_existing") + if second.Output.GetStatus() != "trusted" || second.Output.GetRecordRef() != first.Output.GetRecordRef() { + t.Fatalf("reload status/ref = %q/%q, want trusted/%q", second.Output.GetStatus(), second.Output.GetRecordRef(), first.Output.GetRecordRef()) + } + + _, err := ExecuteSignalTrustObserve(t.Context(), sdk.TypedStepRequest[*contracts.SignalTrustObserveConfig, *contracts.SignalTrustObserveInput]{ + Config: &contracts.SignalTrustObserveConfig{StoreRef: "trust-main", Policy: "require_existing", Version: 1, Iterations: 5200}, + Input: trustObserveInput("pair://tenant/user-a/user-b", trustTestChangedRemote, "reason://changed"), + }) + if err == nil || !strings.Contains(err.Error(), "changed") { + t.Fatalf("expected changed trust error after object reload, got %v", err) + } +} + +func TestSignalTrustStoreObjectBackendRejectsTamperAndWrongObjectKey(t *testing.T) { + resetSignalTrustStoresForTest(t) + storeRoot := filepath.Join(t.TempDir(), "objects") + initTrustStoreForTest(t, "trust-main", &contracts.SignalTrustStoreConfig{ + StoreRef: "trust-main", + Backend: "object_store", + StoragePath: storeRoot, + AllowObjectStoreBackend: true, + }) + observeTrustForTest(t, "trust-main", "pair://tenant/user-a/user-b", trustTestRemoteKey, "trust_on_first_use") + objects := signalTrustObjectStoreFilesForTest(t, storeRoot) + if len(objects) != 1 { + t.Fatalf("object store wrote %d objects, want 1: %v", len(objects), objects) + } + + raw, err := os.ReadFile(objects[0]) + if err != nil { + t.Fatalf("read trust object: %v", err) + } + tampered := bytes.Replace(raw, []byte("pair://tenant/user-a/user-b"), []byte("pair://tenant/user-x/user-y"), 1) + if bytes.Equal(tampered, raw) { + t.Fatal("test fixture did not tamper trust object") + } + if err := os.WriteFile(objects[0], tampered, 0o600); err != nil { + t.Fatalf("write tampered trust object: %v", err) + } + resetSignalTrustStoresForTest(t) + module := newSignalTrustStoreModule("trust-main", &contracts.SignalTrustStoreConfig{ + StoreRef: "trust-main", + Backend: "object_store", + StoragePath: storeRoot, + AllowObjectStoreBackend: true, + }) + if err := module.Init(); err == nil || !strings.Contains(err.Error(), "checksum") { + t.Fatalf("expected checksum tamper error, got %v", err) + } + + cleanRoot := filepath.Join(t.TempDir(), "objects-clean") + initTrustStoreForTest(t, "trust-main", &contracts.SignalTrustStoreConfig{ + StoreRef: "trust-main", + Backend: "object_store", + StoragePath: cleanRoot, + AllowObjectStoreBackend: true, + }) + observeTrustForTest(t, "trust-main", "pair://tenant/user-a/user-b", trustTestRemoteKey, "trust_on_first_use") + cleanObjects := signalTrustObjectStoreFilesForTest(t, cleanRoot) + if len(cleanObjects) != 1 { + t.Fatalf("clean object store wrote %d objects, want 1: %v", len(cleanObjects), cleanObjects) + } + wrongPath := filepath.Join(cleanRoot, "trust", strings.Repeat("0", 64), "snapshot.json") + if err := os.MkdirAll(filepath.Dir(wrongPath), 0o700); err != nil { + t.Fatalf("create wrong object dir: %v", err) + } + if err := os.Rename(cleanObjects[0], wrongPath); err != nil { + t.Fatalf("move trust object to wrong key: %v", err) + } + resetSignalTrustStoresForTest(t) + wrongKey := newSignalTrustStoreModule("trust-main", &contracts.SignalTrustStoreConfig{ + StoreRef: "trust-main", + Backend: "object_store", + StoragePath: cleanRoot, + AllowObjectStoreBackend: true, + }) + if err := wrongKey.Init(); err == nil || !strings.Contains(err.Error(), "object key") { + t.Fatalf("expected object key mismatch error, got %v", err) + } +} + func TestSignalTrustStoreAuditsPersistentDecisions(t *testing.T) { resetSignalTrustStoresForTest(t) dir := t.TempDir() @@ -689,3 +844,24 @@ func quoteJSON(s string) string { } return strings.TrimSpace(buf.String()) } + +func signalTrustObjectStoreFilesForTest(t *testing.T, root string) []string { + t.Helper() + var objects []string + err := filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error { + if err != nil { + return err + } + if entry.IsDir() { + return nil + } + if strings.HasSuffix(entry.Name(), ".json") { + objects = append(objects, path) + } + return nil + }) + if err != nil { + t.Fatalf("walk object store: %v", err) + } + return objects +} diff --git a/proto/signal.pb.go b/proto/signal.pb.go index b70b11c..8c126f2 100644 --- a/proto/signal.pb.go +++ b/proto/signal.pb.go @@ -656,6 +656,7 @@ type SignalTrustStoreConfig struct { AuthHeaderEnv string `protobuf:"bytes,11,opt,name=auth_header_env,json=authHeaderEnv,proto3" json:"auth_header_env,omitempty"` RequestTimeoutMs uint32 `protobuf:"varint,12,opt,name=request_timeout_ms,json=requestTimeoutMs,proto3" json:"request_timeout_ms,omitempty"` AllowInsecureHttp bool `protobuf:"varint,13,opt,name=allow_insecure_http,json=allowInsecureHttp,proto3" json:"allow_insecure_http,omitempty"` + AllowObjectStoreBackend bool `protobuf:"varint,14,opt,name=allow_object_store_backend,json=allowObjectStoreBackend,proto3" json:"allow_object_store_backend,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -781,6 +782,13 @@ func (x *SignalTrustStoreConfig) GetAllowInsecureHttp() bool { return false } +func (x *SignalTrustStoreConfig) GetAllowObjectStoreBackend() bool { + if x != nil { + return x.AllowObjectStoreBackend + } + return false +} + type SignalTrustObserveConfig struct { state protoimpl.MessageState `protogen:"open.v1"` StoreRef string `protobuf:"bytes,1,opt,name=store_ref,json=storeRef,proto3" json:"store_ref,omitempty"` @@ -10826,7 +10834,7 @@ const file_proto_signal_proto_rawDesc = "" + "\adisplay\x18\x02 \x01(\tR\adisplay\x12#\n" + "\rscannable_hex\x18\x03 \x01(\tR\fscannableHex\x12'\n" + "\x0fmatched_display\x18\x04 \x01(\bR\x0ematchedDisplay\x12+\n" + - "\x11matched_scannable\x18\x05 \x01(\bR\x10matchedScannable\"\xa2\x04\n" + + "\x11matched_scannable\x18\x05 \x01(\bR\x10matchedScannable\"\xdf\x04\n" + "\x16SignalTrustStoreConfig\x12\x1b\n" + "\tstore_ref\x18\x01 \x01(\tR\bstoreRef\x12\x18\n" + "\abackend\x18\x02 \x01(\tR\abackend\x12!\n" + @@ -10842,7 +10850,8 @@ const file_proto_signal_proto_rawDesc = "" + " \x01(\tR\x0eauthHeaderName\x12&\n" + "\x0fauth_header_env\x18\v \x01(\tR\rauthHeaderEnv\x12,\n" + "\x12request_timeout_ms\x18\f \x01(\rR\x10requestTimeoutMs\x12.\n" + - "\x13allow_insecure_http\x18\r \x01(\bR\x11allowInsecureHttp\"\xea\x01\n" + + "\x13allow_insecure_http\x18\r \x01(\bR\x11allowInsecureHttp\x12;\n" + + "\x1aallow_object_store_backend\x18\x0e \x01(\bR\x17allowObjectStoreBackend\"\xea\x01\n" + "\x18SignalTrustObserveConfig\x12\x1b\n" + "\tstore_ref\x18\x01 \x01(\tR\bstoreRef\x12\x16\n" + "\x06policy\x18\x02 \x01(\tR\x06policy\x12\x18\n" + diff --git a/proto/signal.proto b/proto/signal.proto index a51d3d5..6d774bc 100644 --- a/proto/signal.proto +++ b/proto/signal.proto @@ -82,6 +82,7 @@ message SignalTrustStoreConfig { string auth_header_env = 11; uint32 request_timeout_ms = 12; bool allow_insecure_http = 13; + bool allow_object_store_backend = 14; } message SignalTrustObserveConfig { diff --git a/scripts/run-pipeline-tests.sh b/scripts/run-pipeline-tests.sh index 1f09fee..70d5b46 100755 --- a/scripts/run-pipeline-tests.sh +++ b/scripts/run-pipeline-tests.sh @@ -47,6 +47,7 @@ fi rm -rf "$PLUGIN_DIR/$PLUGIN_NAME" mkdir -p "$PLUGIN_DIR/$PLUGIN_NAME" +rm -rf "$ROOT/.wfctl/trust-reset-objects" (cd "$ROOT" && GOWORK=off go build \ -ldflags "-X github.com/GoCodeAlone/workflow-plugin-signal/internal.Version=${VERSION:-0.0.0}" \ diff --git a/testdata/pipelines/signal-trust-reset.yaml b/testdata/pipelines/signal-trust-reset.yaml index 313a02d..7ceeca4 100644 --- a/testdata/pipelines/signal-trust-reset.yaml +++ b/testdata/pipelines/signal-trust-reset.yaml @@ -28,7 +28,9 @@ modules: type: signal.trust_store config: store_ref: trust_store - backend: memory + backend: object_store + storage_path: .wfctl/trust-reset-objects + allow_object_store_backend: true pipelines: trust_reset: