feat: execute continuity satisfaction through commit
CI / test (push) Successful in 11s

This commit is contained in:
m
2026-08-16 16:19:23 +03:00
parent fe0b93739f
commit 4a165b5238
25 changed files with 1249 additions and 28 deletions
+133
View File
@@ -0,0 +1,133 @@
package main
import (
"context"
"crypto/subtle"
"encoding/json"
"errors"
"net/http"
"strings"
"git.cin.su/m/Assistant/adapters/postgres"
commitcontract "git.cin.su/m/Assistant/contracts/commit"
commitdomain "git.cin.su/m/Assistant/modules/commit/domain"
"git.cin.su/m/Assistant/modules/commit/ports"
)
type executionRepository interface {
LoadAuthorized(context.Context, commitcontract.AuthorizedActionID) (commitcontract.AuthorizedAction, error)
LoadLatestAttempt(context.Context, commitcontract.AuthorizedActionID) (commitcontract.ActionAttempt, error)
BeginAttempt(context.Context, commitcontract.ActionAttempt) error
FinishAttempt(context.Context, commitcontract.ActionAttempt) error
}
type attemptRequest struct {
AttemptID commitcontract.ActionAttemptID
}
func registerExecutionRoutes(mux *http.ServeMux, repository executionRepository, executor ports.Executor, reconciler ports.Reconciler, token string) {
mux.HandleFunc("POST /v1/internal/authorized-actions/{id}/execute", requireCommitToken(token, func(w http.ResponseWriter, r *http.Request) {
action, err := repository.LoadAuthorized(r.Context(), commitcontract.AuthorizedActionID(r.PathValue("id")))
if errors.Is(err, postgres.ErrAuthorizedActionNotFound) {
http.Error(w, "authorized action not found", http.StatusNotFound)
return
}
if err != nil {
http.Error(w, "authorized action unavailable", http.StatusInternalServerError)
return
}
if _, err = repository.LoadLatestAttempt(r.Context(), action.ID); err == nil {
http.Error(w, "action already has an execution attempt", http.StatusConflict)
return
} else if !errors.Is(err, postgres.ErrActionAttemptNotFound) {
http.Error(w, "action attempt unavailable", http.StatusInternalServerError)
return
}
request, ok := decodeAttemptRequest(w, r)
if !ok {
return
}
begun := commitcontract.ActionAttempt{ID: request.AttemptID, AuthorizedID: action.ID, IntentID: action.Proposal.IntentID, State: commitcontract.ActionExecuting}
if err = repository.BeginAttempt(r.Context(), begun); err != nil {
http.Error(w, "begin execution attempt", http.StatusConflict)
return
}
attempt, executeErr := commitdomain.Execute(action, request.AttemptID, executor)
if err = repository.FinishAttempt(r.Context(), attempt); err != nil {
http.Error(w, "finish execution attempt", http.StatusInternalServerError)
return
}
writeAttempt(w, attempt, executeErr)
}))
mux.HandleFunc("POST /v1/internal/authorized-actions/{id}/reconcile", requireCommitToken(token, func(w http.ResponseWriter, r *http.Request) {
action, err := repository.LoadAuthorized(r.Context(), commitcontract.AuthorizedActionID(r.PathValue("id")))
if errors.Is(err, postgres.ErrAuthorizedActionNotFound) {
http.Error(w, "authorized action not found", http.StatusNotFound)
return
}
if err != nil {
http.Error(w, "authorized action unavailable", http.StatusInternalServerError)
return
}
previous, err := repository.LoadLatestAttempt(r.Context(), action.ID)
if err != nil {
http.Error(w, "unknown execution attempt not found", http.StatusConflict)
return
}
if previous.State == commitcontract.ActionExecuting {
previous.State = commitcontract.ActionUnknownOutcome
if err = repository.FinishAttempt(r.Context(), previous); err != nil {
http.Error(w, "recover interrupted attempt", http.StatusInternalServerError)
return
}
}
if previous.State != commitcontract.ActionUnknownOutcome {
http.Error(w, "action does not require reconciliation", http.StatusConflict)
return
}
request, ok := decodeAttemptRequest(w, r)
if !ok {
return
}
begun := commitcontract.ActionAttempt{ID: request.AttemptID, AuthorizedID: action.ID, IntentID: action.Proposal.IntentID, State: commitcontract.ActionExecuting}
if err = repository.BeginAttempt(r.Context(), begun); err != nil {
http.Error(w, "begin reconciliation attempt", http.StatusConflict)
return
}
attempt, reconcileErr := commitdomain.Reconcile(action, previous, request.AttemptID, reconciler)
if err = repository.FinishAttempt(r.Context(), attempt); err != nil {
http.Error(w, "finish reconciliation attempt", http.StatusInternalServerError)
return
}
writeAttempt(w, attempt, reconcileErr)
}))
}
func decodeAttemptRequest(w http.ResponseWriter, r *http.Request) (attemptRequest, bool) {
var request attemptRequest
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 8<<10)).Decode(&request); err != nil || request.AttemptID == "" {
http.Error(w, "invalid attempt request", http.StatusBadRequest)
return attemptRequest{}, false
}
return request, true
}
func writeAttempt(w http.ResponseWriter, attempt commitcontract.ActionAttempt, operationErr error) {
w.Header().Set("Content-Type", "application/json")
if errors.Is(operationErr, commitdomain.ErrUnknownOutcome) {
w.WriteHeader(http.StatusAccepted)
}
_ = json.NewEncoder(w).Encode(attempt)
}
func requireCommitToken(expected string, next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
provided := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
if expected == "" || len(provided) != len(expected) || subtle.ConstantTimeCompare([]byte(provided), []byte(expected)) != 1 {
http.Error(w, "commit executor denied", http.StatusUnauthorized)
return
}
next(w, r)
}
}
+73
View File
@@ -0,0 +1,73 @@
package main
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.cin.su/m/Assistant/adapters/postgres"
commitcontract "git.cin.su/m/Assistant/contracts/commit"
"git.cin.su/m/Assistant/modules/commit/ports"
)
type fakeExecutionRepository struct {
action commitcontract.AuthorizedAction
attempts []commitcontract.ActionAttempt
}
func (f *fakeExecutionRepository) LoadAuthorized(context.Context, commitcontract.AuthorizedActionID) (commitcontract.AuthorizedAction, error) {
return f.action, nil
}
func (f *fakeExecutionRepository) LoadLatestAttempt(context.Context, commitcontract.AuthorizedActionID) (commitcontract.ActionAttempt, error) {
if len(f.attempts) == 0 {
return commitcontract.ActionAttempt{}, postgres.ErrActionAttemptNotFound
}
return f.attempts[len(f.attempts)-1], nil
}
func (f *fakeExecutionRepository) BeginAttempt(_ context.Context, attempt commitcontract.ActionAttempt) error {
f.attempts = append(f.attempts, attempt)
return nil
}
func (f *fakeExecutionRepository) FinishAttempt(_ context.Context, attempt commitcontract.ActionAttempt) error {
if len(f.attempts) == 0 || f.attempts[len(f.attempts)-1].ID != attempt.ID {
return errors.New("attempt not begun")
}
f.attempts[len(f.attempts)-1] = attempt
return nil
}
type knownExecutor struct{ calls int }
func (e *knownExecutor) Execute(commitcontract.AuthorizedAction) ports.ExecutionResult {
e.calls++
return ports.ExecutionResult{Known: true, Succeeded: true}
}
func (e *knownExecutor) Reconcile(commitcontract.AuthorizedAction) ports.ExecutionResult {
return ports.ExecutionResult{Known: true, Succeeded: true}
}
func TestExecutionIsDurablyBegunAndCannotBeBlindlyRepeated(t *testing.T) {
repository := &fakeExecutionRepository{action: commitcontract.AuthorizedAction{ID: "authorized-1", State: commitcontract.ActionAuthorized, Proposal: commitcontract.ActionProposal{IntentID: "intent-1"}}}
executor := &knownExecutor{}
mux := http.NewServeMux()
registerExecutionRoutes(mux, repository, executor, executor, "secret")
request := httptest.NewRequest(http.MethodPost, "/v1/internal/authorized-actions/authorized-1/execute", strings.NewReader(`{"AttemptID":"attempt-1"}`))
request.Header.Set("Authorization", "Bearer secret")
result := httptest.NewRecorder()
mux.ServeHTTP(result, request)
if result.Code != http.StatusOK || executor.calls != 1 || repository.attempts[0].State != commitcontract.ActionSucceeded {
t.Fatalf("status=%d calls=%d attempts=%#v", result.Code, executor.calls, repository.attempts)
}
repeated := httptest.NewRequest(http.MethodPost, "/v1/internal/authorized-actions/authorized-1/execute", strings.NewReader(`{"AttemptID":"attempt-2"}`))
repeated.Header.Set("Authorization", "Bearer secret")
repeatedResult := httptest.NewRecorder()
mux.ServeHTTP(repeatedResult, repeated)
if repeatedResult.Code != http.StatusConflict || executor.calls != 1 {
t.Fatalf("repeat status=%d calls=%d", repeatedResult.Code, executor.calls)
}
}
+19 -1
View File
@@ -8,7 +8,9 @@ import (
"net/http"
"net/url"
"os"
"time"
"git.cin.su/m/Assistant/adapters/continuityexecutor"
"git.cin.su/m/Assistant/adapters/postgres"
"git.cin.su/m/Assistant/contracts/authority"
commitcontract "git.cin.su/m/Assistant/contracts/commit"
@@ -52,8 +54,17 @@ func main() {
if coreURL == "" {
log.Fatal("CORE_RUNTIME_URL is required")
}
executorToken := os.Getenv("COMMIT_EXECUTOR_TOKEN")
if executorToken == "" {
log.Fatal("COMMIT_EXECUTOR_TOKEN is required")
}
executor, err := continuityexecutor.New(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), executorToken, &http.Client{Timeout: 5 * time.Second})
if err != nil {
log.Fatal(err)
}
mux := http.NewServeMux()
mux.Handle("/healthz", health.Handler{Runtime: "commit-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
registerExecutionRoutes(mux, repository, executor, executor, executorToken)
mux.HandleFunc("POST /v1/authorized-actions", func(w http.ResponseWriter, r *http.Request) {
var request authorizationRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
@@ -80,7 +91,7 @@ func main() {
http.Error(w, "work target is stale or unavailable", http.StatusConflict)
return
}
action, err := commitdomain.Authorize(request.Proposal, commitdomain.AuthorizationContext{Binding: binding, Scope: scope, Grant: grant}, authorityValidator{}, request.AuthorizedID)
action, err := commitdomain.Authorize(request.Proposal, commitdomain.AuthorizationContext{Binding: binding, Scope: scope, Grant: grant}, authorityValidator{}, request.AuthorizedID, time.Now().UnixMilli())
if err != nil {
http.Error(w, "authorization denied", http.StatusConflict)
return
@@ -113,6 +124,13 @@ func main() {
}
}
func envOr(name, fallback string) string {
if value := os.Getenv(name); value != "" {
return value
}
return fallback
}
func loadJSON[T any](ctx context.Context, endpoint string) (T, error) {
var value T
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)