From 3eff819447a805f5310050e8d662a40e2a33deb1 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Tue, 1 Sep 2026 15:24:53 +0500 Subject: [PATCH] feat(agentjournal): a handoff is a goal another agent can pick up agent-runtime was a tested library with zero estate consumers, linked to GDS by gitlink and docs alone. Now the engine consumes it where its contract fits exactly: gds handoff checkpoints unfinished work for a next actor, and that is a Goal journal -- durable, revisioned, validated by agent-runtime itself, never a private dialect. Apply creates the goal (genesis create, then the store's own CAS update) and completes the checkpoint item with commit and file evidence; verify completes the verification item idempotently; both append handoff lifecycle events to a JSONL stream through the library's emitter. A journal failure is a finding beside a checkpoint that already exists, never a rollback of it. Claude-Session: https://claude.ai/code/session_01LsGid6U5RrQdFvJmvYdGCF --- .gds/bundle.lock.yaml | 10 +- .github/workflows/gds-ci.yml | 4 +- core/agentjournal/agentjournal.go | 171 +++++++++++++++++++++++++ core/agentjournal/agentjournal_test.go | 94 ++++++++++++++ core/app/handoff_workflow.go | 48 ++++++- go.mod | 3 +- go.sum | 18 +-- 7 files changed, 330 insertions(+), 18 deletions(-) create mode 100644 core/agentjournal/agentjournal.go create mode 100644 core/agentjournal/agentjournal_test.go diff --git a/.gds/bundle.lock.yaml b/.gds/bundle.lock.yaml index b1e86ce..135021a 100644 --- a/.gds/bundle.lock.yaml +++ b/.gds/bundle.lock.yaml @@ -5,14 +5,14 @@ bundle: version: "0.4.0-dev" release_sequence: 0 channel: "development" - source_tree_digest: "sha256:c7c04a600276303a2b4b63ccd2211fcb59eb94bfdcc6320e02eded9c0d2c2b10" - digest: "sha256:e753bacfdd5eedd658dbb63a82f9ce84a3b7efb2b14ae1fd55550fe040a003f5" + source_tree_digest: "sha256:376e0d4a5c11a6214ad70d8ceff8c01645fcf5a71fba332e94e4c2e6061e22a4" + digest: "sha256:681dfce12bea5d4826ba0516c9a1112da41e160abe68d213c27c241e07740821" projection: - input_digest: "sha256:ba8b4e3da061c68235be603206bcb1042ec9091b4477da9bfbe302273042b4bb" - output_digest: "sha256:8efad1c40b82dda2d01a4ca37756b38bfae9f66cce4ac0f4808b2913d918278b" + input_digest: "sha256:1de3673910b83ab471567d083633ec096f4436e0727d068f37daebbf3d6e9ff5" + output_digest: "sha256:0e9aa2aa74c34b912b8b93e21da1d759ba0552fc55174898f8517d92dc95719c" files: - path: ".gds/compiled-policy.json" digest: "sha256:78d09606bb4168d74bce1f50ab62b46a7ded34652c6b23af1badfd26dd060e94" - path: ".github/workflows/gds-ci.yml" - digest: "sha256:a2590f9aaec401c3e313fa639416e8a6b45ddfb96c3e3947a0b0b8c6a6e1dc57" + digest: "sha256:28368b2b4b0e55ac288e9282739caf06b884a6297747c82104dd5a39bada3fce" diff --git a/.github/workflows/gds-ci.yml b/.github/workflows/gds-ci.yml index f3c09d7..136fdaf 100644 --- a/.github/workflows/gds-ci.yml +++ b/.github/workflows/gds-ci.yml @@ -1,8 +1,8 @@ # GENERATED FILE - DO NOT EDIT DIRECTLY # generator: gds # bundle: 0.4.0-dev -# source-tree-digest: sha256:c7c04a600276303a2b4b63ccd2211fcb59eb94bfdcc6320e02eded9c0d2c2b10 -# input-digest: sha256:ba8b4e3da061c68235be603206bcb1042ec9091b4477da9bfbe302273042b4bb +# source-tree-digest: sha256:376e0d4a5c11a6214ad70d8ceff8c01645fcf5a71fba332e94e4c2e6061e22a4 +# input-digest: sha256:1de3673910b83ab471567d083633ec096f4436e0727d068f37daebbf3d6e9ff5 # output-digest: sha256:01fb4854784be9e4564bcc84e70786484b370879be5e5ab1dd49f8b73ea2dea4 # edit-source: # - .gds/repository.yaml diff --git a/core/agentjournal/agentjournal.go b/core/agentjournal/agentjournal.go new file mode 100644 index 0000000..2b70650 --- /dev/null +++ b/core/agentjournal/agentjournal.go @@ -0,0 +1,171 @@ +// Package agentjournal records a handoff as an agent-runtime goal. +// +// gds handoff checkpoints unfinished work for a next actor, and that is +// exactly the contract agent-runtime's Goal journal and handoff lifecycle +// events were written for: a durable, revisioned, vendor-neutral record of a +// goal another agent is expected to pick up. The journal lives beside the +// operation state, the lifecycle events append to a JSONL stream, and both +// use agent-runtime's own validation -- GDS adds identity and evidence, never +// a private dialect. +package agentjournal + +import ( + "context" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/NDDev-OpenNetwork/agent-runtime/goal" + "github.com/NDDev-OpenNetwork/agent-runtime/observability" +) + +const ( + // CheckpointItem is the acceptance item the apply completes: the + // checkpoint commit exists and is published. + CheckpointItem = "checkpoint-published" + // VerifiedItem is the acceptance item the verify completes: the handoff + // operation re-proved the checkpoint. + VerifiedItem = "handoff-verified" +) + +var invalidIDRunes = regexp.MustCompile(`[^a-z0-9._-]+`) + +// Recorder writes goal journals and lifecycle events under one directory. +type Recorder struct { + Directory string + Now func() time.Time +} + +func (r Recorder) now() time.Time { + if r.Now != nil { + return r.Now() + } + return time.Now().UTC() +} + +// GoalID derives a valid agent-runtime goal id from an operation id. +func GoalID(operationID string) string { + lowered := strings.ToLower(strings.TrimSpace(operationID)) + lowered = strings.ReplaceAll(lowered, "_", "-") + lowered = invalidIDRunes.ReplaceAllString(lowered, "-") + lowered = strings.Trim(lowered, "._-") + if lowered == "" { + lowered = "operation" + } + return "handoff." + lowered +} + +// JournalPath is where an operation's goal journal lives. +func (r Recorder) JournalPath(operationID string) string { + return filepath.Join(r.Directory, GoalID(operationID)+".json") +} + +func (r Recorder) eventsPath() string { + return filepath.Join(r.Directory, "handoff-events.jsonl") +} + +// RecordCheckpoint creates the goal journal for an applied handoff and marks +// the checkpoint item complete with its commit evidence, then appends the +// dispatched handoff event. +func (r Recorder) RecordCheckpoint( + ctx context.Context, + operationID, repositoryID, intent string, + files []string, + commitReference string, + sessionID string, +) error { + if err := os.MkdirAll(r.Directory, 0o700); err != nil { + return fmt.Errorf("create agent journal directory: %w", err) + } + now := r.now() + journal, err := goal.New(GoalID(operationID), intent, []goal.ChecklistItem{ + {ID: CheckpointItem, Acceptance: "the checkpoint commit exists on the published branch"}, + {ID: VerifiedItem, Acceptance: "gds handoff --verify re-proved the checkpoint"}, + }, []string{"integration", "cleanup"}, now) + if err != nil { + return fmt.Errorf("draft handoff goal: %w", err) + } + evidence := []goal.Evidence{{Type: goal.EvidenceCommit, Reference: commitReference, Result: "checkpoint published for " + repositoryID}} + for _, file := range files { + evidence = append(evidence, goal.Evidence{Type: goal.EvidenceFile, Reference: file, Result: "carried by the checkpoint"}) + } + store := goal.Store{Path: r.JournalPath(operationID)} + // The store insists on a genesis create followed by a CAS update -- the + // same discipline every other consumer gets, so GDS takes it too. + if err := store.Create(journal); err != nil { + return fmt.Errorf("store handoff goal: %w", err) + } + if _, err := store.Update(journal.Revision, func(stored *goal.Journal) error { + return stored.CompleteItem(CheckpointItem, evidence, now) + }); err != nil { + return fmt.Errorf("complete checkpoint item: %w", err) + } + return r.emit(ctx, operationID, sessionID, observability.HandoffStageDispatched) +} + +// RecordVerified marks the verification item complete on the stored journal +// and appends the completed handoff event. +func (r Recorder) RecordVerified( + ctx context.Context, + operationID string, + verificationReference string, + sessionID string, +) error { + store := goal.Store{Path: r.JournalPath(operationID)} + current, err := store.Load() + if err != nil { + return fmt.Errorf("load handoff goal: %w", err) + } + // A re-verify is idempotent: the journal already says it, and the event + // stream already carries it, so neither is repeated. + for _, item := range current.Goal.Acceptance { + if item.ID == VerifiedItem && item.Status == goal.ItemComplete { + return nil + } + } + now := r.now() + if _, err := store.Update(current.Revision, func(journal *goal.Journal) error { + return journal.CompleteItem(VerifiedItem, []goal.Evidence{{ + Type: goal.EvidenceCommand, Reference: verificationReference, + Result: "handoff verify succeeded", + }}, now) + }); err != nil { + return fmt.Errorf("complete verification item: %w", err) + } + return r.emit(ctx, operationID, sessionID, observability.HandoffStageCompleted) +} + +func (r Recorder) emit(ctx context.Context, operationID, sessionID string, stage observability.HandoffStage) error { + sink, err := observability.OpenJSONLSink(r.eventsPath(), observability.JSONLOptions{Name: "gds-handoff"}) + if err != nil { + return fmt.Errorf("open handoff event sink: %w", err) + } + defer sink.Close(ctx) + emitter, err := observability.NewEmitter( + observability.Runtime{ID: "gds", Version: "handoff-v1"}, + []observability.Sink{sink}, + observability.Options{Clock: r.now}, + ) + if err != nil { + return fmt.Errorf("build handoff event emitter: %w", err) + } + draft, err := observability.HandoffDraft( + GoalID(operationID), + observability.ActorWorker, observability.ActorWorker, stage, nil, nil, + observability.Context{ + CorrelationID: GoalID(operationID), + Actor: observability.Actor{Kind: observability.ActorWorker, ID: sessionID}, + Attempt: observability.AttemptInitial, + }, + ) + if err != nil { + return fmt.Errorf("draft handoff event: %w", err) + } + if _, _, err := emitter.Emit(ctx, draft); err != nil { + return fmt.Errorf("emit handoff event: %w", err) + } + return nil +} diff --git a/core/agentjournal/agentjournal_test.go b/core/agentjournal/agentjournal_test.go new file mode 100644 index 0000000..65b52d0 --- /dev/null +++ b/core/agentjournal/agentjournal_test.go @@ -0,0 +1,94 @@ +package agentjournal + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/NDDev-OpenNetwork/agent-runtime/goal" +) + +func fixedNow() time.Time { return time.Date(2026, 9, 1, 12, 0, 0, 0, time.UTC) } + +func TestGoalIDDerivesAValidRuntimeIdentity(t *testing.T) { + id := GoalID("op_01M0G5WWVAHQGK33XJ9M183MJ5") + if id != "handoff.op-01m0g5wwvahqgk33xj9m183mj5" { + t.Fatalf("id=%q", id) + } + if _, err := goal.New(id, "x", []goal.ChecklistItem{{ID: "a", Acceptance: "b"}}, nil, fixedNow()); err != nil { + t.Fatalf("derived id refused by agent-runtime: %v", err) + } +} + +func TestRecordCheckpointThenVerifiedCompletesTheGoalStory(t *testing.T) { + recorder := Recorder{Directory: filepath.Join(t.TempDir(), "agent-journals"), Now: fixedNow} + ctx := context.Background() + err := recorder.RecordCheckpoint(ctx, "op_TEST123", "device:example/repo", "checkpoint the refactor", []string{"a.go", "b.go"}, "operation:op_TEST123", "session-1") + if err != nil { + t.Fatal(err) + } + store := goal.Store{Path: recorder.JournalPath("op_TEST123")} + journal, err := store.Load() + if err != nil { + t.Fatal(err) + } + if err := journal.Validate(); err != nil { + t.Fatalf("stored journal does not validate under agent-runtime: %v", err) + } + byID := map[string]goal.ChecklistItem{} + for _, item := range journal.Goal.Acceptance { + byID[item.ID] = item + } + if byID[CheckpointItem].Status != goal.ItemComplete { + t.Fatal("checkpoint item is not complete after apply") + } + if byID[VerifiedItem].Status != goal.ItemPending { + t.Fatal("verification item must stay pending until verify") + } + // Files ride as evidence on the completed item. + if len(byID[CheckpointItem].Evidence) != 3 { + t.Fatalf("evidence=%+v", byID[CheckpointItem].Evidence) + } + + if err := recorder.RecordVerified(ctx, "op_TEST123", "gds handoff --verify op_TEST123", "session-2"); err != nil { + t.Fatal(err) + } + journal, err = store.Load() + if err != nil { + t.Fatal(err) + } + for _, item := range journal.Goal.Acceptance { + if item.ID == VerifiedItem && item.Status != goal.ItemComplete { + t.Fatal("verification item is not complete after verify") + } + } + // A second verify is idempotent, not a corruption. + if err := recorder.RecordVerified(ctx, "op_TEST123", "gds handoff --verify op_TEST123", "session-2"); err != nil { + t.Fatalf("re-verify must not fail: %v", err) + } + + // The lifecycle stream carries the dispatched and completed handoff events. + raw, err := os.ReadFile(filepath.Join(recorder.Directory, "handoff-events.jsonl")) + if err != nil { + t.Fatal(err) + } + lines := strings.Split(strings.TrimSpace(string(raw)), "\n") + if len(lines) < 2 { + t.Fatalf("events=%d, want at least dispatched and completed", len(lines)) + } + var first map[string]any + if err := json.Unmarshal([]byte(lines[0]), &first); err != nil { + t.Fatalf("event stream is not JSONL: %v", err) + } +} + +func TestRecordVerifiedWithoutACheckpointRefuses(t *testing.T) { + recorder := Recorder{Directory: t.TempDir(), Now: fixedNow} + if err := recorder.RecordVerified(context.Background(), "op_NOPE", "ref", "s"); err == nil { + t.Fatal("verify without a stored goal was accepted") + } +} diff --git a/core/app/handoff_workflow.go b/core/app/handoff_workflow.go index 17041bc..0520b25 100644 --- a/core/app/handoff_workflow.go +++ b/core/app/handoff_workflow.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "github.com/NDDev-OpenNetwork/github-device-sync/core/agentjournal" "github.com/NDDev-OpenNetwork/github-device-sync/core/canonicaljson" "github.com/NDDev-OpenNetwork/github-device-sync/core/compiler" "github.com/NDDev-OpenNetwork/github-device-sync/core/domain" @@ -205,7 +206,7 @@ func (services *Services) ApplyHandoff( if finding := validateOperationActor(options.DeviceID, options.SessionID); finding != nil { return domain.NewEnvelope("gds handoff apply", domain.ExitInput, nil, *finding) } - _, store, stateFinding := openOperationState(ctx, options.StatePath) + statePath, store, stateFinding := openOperationState(ctx, options.StatePath) if stateFinding != nil { return domain.NewEnvelope("gds handoff apply", domain.ExitInput, nil, *stateFinding) } @@ -251,9 +252,40 @@ func (services *Services) ApplyHandoff( envelope.Mutation.Attempted = result.MutationAttempted envelope.Mutation.Completed = result.MutationCompleted envelope.Scope["repository_id"] = plan.Scope.Repositories[0] + // The checkpoint is durable; now the handoff itself becomes a durable + // goal another agent can pick up. The journal is evidence beside the + // operation, so a failure to write it is a finding, never a rollback of + // a checkpoint that already exists. + recorder := agentjournal.Recorder{ + Directory: filepath.Join(filepath.Dir(statePath), "agent-journals"), + Now: services.Now, + } + if err := recorder.RecordCheckpoint( + ctx, result.OperationID, plan.Scope.Repositories[0], + handoffGoalIntent(plan), files, "operation:"+result.OperationID, + options.SessionID, + ); err != nil { + envelope.Findings = append(envelope.Findings, domain.Finding{ + Code: "GDS_HANDOFF_AGENT_JOURNAL_NOT_RECORDED", Severity: domain.SeverityMedium, + Message: "The checkpoint exists, but its agent-runtime goal journal was not recorded: " + err.Error(), + }) + } else { + envelope.Scope["agent_journal"] = recorder.JournalPath(result.OperationID) + } return envelope } +// handoffGoalIntent names the goal after the plan's own checkpoint message, +// falling back to a stable phrase when the plan carries none. +func handoffGoalIntent(plan operations.Plan) string { + for _, step := range plan.Steps { + if message, ok := step.Parameters["message"].(string); ok && strings.TrimSpace(message) != "" { + return message + } + } + return "carry the checkpointed work to completion" +} + func (services *Services) VerifyHandoff( ctx context.Context, operationID string, @@ -265,7 +297,7 @@ func (services *Services) VerifyHandoff( if finding := validateOperationActor(options.DeviceID, options.SessionID); finding != nil { return domain.NewEnvelope("gds handoff verify", domain.ExitInput, nil, *finding) } - _, store, stateFinding := openOperationState(ctx, options.StatePath) + statePath, store, stateFinding := openOperationState(ctx, options.StatePath) if stateFinding != nil { return domain.NewEnvelope("gds handoff verify", domain.ExitInput, nil, *stateFinding) } @@ -301,6 +333,18 @@ func (services *Services) VerifyHandoff( envelope := domain.Success("gds handoff verify", result) envelope.OperationID = operationID envelope.Scope["repository_id"] = plan.Scope.Repositories[0] + recorder := agentjournal.Recorder{ + Directory: filepath.Join(filepath.Dir(statePath), "agent-journals"), + Now: services.Now, + } + if err := recorder.RecordVerified(ctx, operationID, "gds handoff --verify "+operationID, options.SessionID); err != nil { + envelope.Findings = append(envelope.Findings, domain.Finding{ + Code: "GDS_HANDOFF_AGENT_JOURNAL_NOT_RECORDED", Severity: domain.SeverityMedium, + Message: "The verification succeeded, but the agent-runtime goal journal was not advanced: " + err.Error(), + }) + } else { + envelope.Scope["agent_journal"] = recorder.JournalPath(operationID) + } return envelope } diff --git a/go.mod b/go.mod index a99b79b..42e683e 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/NDDev-OpenNetwork/github-device-sync go 1.26.7 require ( + github.com/NDDev-OpenNetwork/agent-runtime v0.1.2-0.20260828080341-a0738060888d github.com/dlclark/regexp2 v1.12.0 github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 github.com/spf13/cobra v1.10.2 @@ -19,7 +20,7 @@ require ( github.com/ncruces/go-strftime v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/spf13/pflag v1.0.9 // indirect - golang.org/x/text v0.39.0 // indirect + golang.org/x/text v0.41.0 // indirect modernc.org/libc v1.74.4 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/go.sum b/go.sum index d1a246e..b1a4eb3 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/NDDev-OpenNetwork/agent-runtime v0.1.2-0.20260828080341-a0738060888d h1:L77kZXzJtNVLHFJoLyEXOwjQOUCpcyU+cdMIhDMebZI= +github.com/NDDev-OpenNetwork/agent-runtime v0.1.2-0.20260828080341-a0738060888d/go.mod h1:4W4dEgssqaWVQcxErXWuH2B9jb8tKj9O08x0TRwYikw= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= @@ -27,16 +29,16 @@ github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4= go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= +golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= -golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= -golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= -golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= +golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI= modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=