This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
)
|
||||
|
||||
const tokenPath = "/run/secrets/acceptance_harness_service_token"
|
||||
|
||||
type client struct {
|
||||
base string
|
||||
token string
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
func main() {
|
||||
base := strings.TrimRight(os.Getenv("CORE_RUNTIME_URL"), "/")
|
||||
if base == "" {
|
||||
base = "http://core-runtime-candidate:8080"
|
||||
}
|
||||
token, err := os.ReadFile(tokenPath)
|
||||
if err != nil {
|
||||
fail("acceptance credential unavailable")
|
||||
}
|
||||
c := client{base: base, token: strings.TrimSpace(string(token)), http: &http.Client{Timeout: 90 * time.Second}}
|
||||
if strings.TrimSpace(c.token) == "" {
|
||||
fail("acceptance credential unavailable")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
var issued struct {
|
||||
Token string
|
||||
Session identity.CreatorSession
|
||||
}
|
||||
if err := c.post(ctx, "/v1/acceptance/sessions", nil, &issued); err != nil {
|
||||
fail("acceptance authentication failed")
|
||||
}
|
||||
if issued.Token == "" || issued.Session.PrincipalKind != identity.PrincipalKindService {
|
||||
fail("acceptance principal was not authenticated as service")
|
||||
}
|
||||
c.token = issued.Token
|
||||
var first interaction.ChatMessageResponse
|
||||
if err := c.post(ctx, "/v1/creator/chat/messages", interaction.ChatMessageRequest{Message: "synthetic acceptance statement", CorrelationID: "acceptance-harness", SyntheticAcceptance: true, SyntheticScopeID: "synthetic-acceptance-scope"}, &first); err != nil {
|
||||
fail("synthetic interaction failed")
|
||||
}
|
||||
var corrected interaction.ChatMessageResponse
|
||||
if err := c.post(ctx, "/v1/creator/chat/messages", interaction.ChatMessageRequest{ConversationID: first.ConversationID, Message: "synthetic acceptance correction", CorrelationID: "acceptance-harness-correction", CorrectsUtteranceID: first.UtteranceID, SyntheticAcceptance: true, SyntheticScopeID: "synthetic-acceptance-scope"}, &corrected); err != nil {
|
||||
fail("synthetic correction failed")
|
||||
}
|
||||
var summary struct {
|
||||
LearningSignals int `json:"learning_signals"`
|
||||
ImprovementNeeds int `json:"improvement_needs"`
|
||||
LearningLedger int `json:"learning_ledger"`
|
||||
}
|
||||
if err := c.get(ctx, "/v1/acceptance/self/summary", &summary); err != nil {
|
||||
fail("acceptance SELF summary unavailable")
|
||||
}
|
||||
fmt.Printf("acceptance_pass learning_signals=%d improvement_needs=%d learning_ledger=%d\n", summary.LearningSignals, summary.ImprovementNeeds, summary.LearningLedger)
|
||||
}
|
||||
|
||||
func (c client) post(ctx context.Context, path string, body any, out any) error {
|
||||
return c.request(ctx, http.MethodPost, path, body, out)
|
||||
}
|
||||
func (c client) get(ctx context.Context, path string, out any) error {
|
||||
return c.request(ctx, http.MethodGet, path, nil, out)
|
||||
}
|
||||
func (c client) request(ctx context.Context, method, path string, body any, out any) error {
|
||||
var reader io.Reader
|
||||
if body != nil {
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reader = bytes.NewReader(data)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, c.base+path, reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return errors.New("acceptance request denied")
|
||||
}
|
||||
if out != nil && json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(out) != nil {
|
||||
return errors.New("invalid acceptance response")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func fail(message string) { fmt.Fprintln(os.Stderr, message); os.Exit(1) }
|
||||
@@ -47,6 +47,7 @@ const (
|
||||
epistemicsDBPasswordPath = "/run/secrets/epistemics_db_password"
|
||||
interactionDBPasswordPath = "/run/secrets/interaction_db_password"
|
||||
selfDBPasswordPath = "/run/secrets/self_db_password"
|
||||
acceptanceHarnessTokenPath = "/run/secrets/acceptance_harness_service_token"
|
||||
controlPlaneServiceTokenFile = "control_plane_service_token"
|
||||
)
|
||||
|
||||
@@ -162,6 +163,18 @@ func main() {
|
||||
if err := bootstrapCreatorAccount(context.Background(), identityRepository, creatorUsername, creatorPasswordPath); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
acceptanceServiceToken := ""
|
||||
var acceptancePrincipal identity.ServicePrincipal
|
||||
if os.Getenv("ACCEPTANCE_HARNESS_SERVICE_CREDENTIAL_REF") != "" {
|
||||
acceptanceServiceToken, err = readCredentialRefSecretPurpose("ACCEPTANCE_HARNESS_SERVICE_TOKEN", "ACCEPTANCE_HARNESS_SERVICE_CREDENTIAL_REF", acceptanceHarnessTokenPath, secretcontracts.SecretPurposeServiceAuthentication)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
acceptancePrincipal, err = identityRepository.LoadServicePrincipal(context.Background(), identity.PrincipalID("domovoy-acceptance-harness-principal"))
|
||||
if err != nil || !acceptancePrincipal.Active {
|
||||
log.Fatal("acceptance service principal is unavailable")
|
||||
}
|
||||
}
|
||||
interactionPassword, err := readCredentialRefSecret("INTERACTION_POSTGRES_PASSWORD", "INTERACTION_POSTGRES_CREDENTIAL_REF", interactionDBPasswordPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -230,12 +243,32 @@ func main() {
|
||||
mux := http.NewServeMux()
|
||||
authenticateCreator := func(w http.ResponseWriter, r *http.Request) (identity.CreatorSession, bool) {
|
||||
session, err := identitydomain.ValidateCreatorSession(sessionKey, bearerToken(r), time.Now())
|
||||
if err != nil {
|
||||
if err != nil || (session.PrincipalKind != "" && session.PrincipalKind != identity.PrincipalKindHuman) {
|
||||
http.Error(w, "creator session denied", http.StatusUnauthorized)
|
||||
return identity.CreatorSession{}, false
|
||||
}
|
||||
return session, true
|
||||
}
|
||||
authenticateAcceptance := func(w http.ResponseWriter, r *http.Request) (identity.CreatorSession, bool) {
|
||||
if acceptanceServiceToken == "" || acceptancePrincipal.Kind != identity.PrincipalKindService || !acceptancePrincipal.Active || !identitydomain.BootstrapMatches(acceptanceServiceToken, bearerToken(r)) {
|
||||
http.Error(w, "acceptance authentication denied", http.StatusUnauthorized)
|
||||
return identity.CreatorSession{}, false
|
||||
}
|
||||
_, session, err := identitydomain.IssueServiceSession(sessionKey, acceptancePrincipal, time.Now(), 30*time.Minute)
|
||||
if err != nil {
|
||||
http.Error(w, "acceptance session unavailable", http.StatusServiceUnavailable)
|
||||
return identity.CreatorSession{}, false
|
||||
}
|
||||
return session, true
|
||||
}
|
||||
authenticateAcceptanceSession := func(w http.ResponseWriter, r *http.Request) (identity.CreatorSession, bool) {
|
||||
session, err := identitydomain.ValidateCreatorSession(sessionKey, bearerToken(r), time.Now())
|
||||
if err != nil || session.PrincipalKind != identity.PrincipalKindService || session.PrincipalID != acceptancePrincipal.PrincipalID || !acceptancePrincipal.Active {
|
||||
http.Error(w, "acceptance session denied", http.StatusUnauthorized)
|
||||
return identity.CreatorSession{}, false
|
||||
}
|
||||
return session, true
|
||||
}
|
||||
resolveFocus := func(r *http.Request) (situation.FocusBinding, error) {
|
||||
runtime := situation.RuntimeBinding{SessionID: situation.SessionID(r.URL.Query().Get("session_id")), Workspace: situation.WorkspaceID(r.URL.Query().Get("workspace_id"))}
|
||||
bindings, err := repository.ByRuntime(r.Context(), runtime)
|
||||
@@ -286,6 +319,35 @@ func main() {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/sessions", creatorDatabaseSessionHandler(bootstrapToken, creatorUsername, identityRepository, sessionKey, time.Now))
|
||||
mux.HandleFunc("POST /v1/acceptance/sessions", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := authenticateAcceptance(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
token, issued, err := identitydomain.IssueServiceSession(sessionKey, acceptancePrincipal, time.Now(), 30*time.Minute)
|
||||
if err != nil {
|
||||
http.Error(w, "acceptance session unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
_ = session
|
||||
writeCreatorSession(w, token, issued)
|
||||
})
|
||||
mux.HandleFunc("GET /v1/acceptance/self/summary", func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := authenticateAcceptanceSession(w, r); !ok {
|
||||
return
|
||||
}
|
||||
if selfRepository == nil {
|
||||
http.Error(w, "SELF unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
summary, err := selfRepository.AcceptanceSummary(r.Context(), string(acceptancePrincipal.PrincipalID))
|
||||
if err != nil {
|
||||
http.Error(w, "SELF summary unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(summary)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/account/password", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := authenticateCreator(w, r)
|
||||
if !ok {
|
||||
@@ -596,15 +658,21 @@ func main() {
|
||||
}{Experiences: experiences, Reflections: reflections, Skills: skills})
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/chat/messages", func(w http.ResponseWriter, r *http.Request) {
|
||||
var request interaction.ChatMessageRequest
|
||||
body, readErr := io.ReadAll(http.MaxBytesReader(w, r.Body, interactiondomain.MaxHumanUtteranceBytes+1024))
|
||||
if readErr != nil || json.Unmarshal(body, &request) != nil {
|
||||
http.Error(w, "invalid chat message", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
session, ok := authenticateCreator(w, r)
|
||||
if !ok && request.SyntheticAcceptance {
|
||||
session, ok = authenticateAcceptanceSession(w, r)
|
||||
}
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request interaction.ChatMessageRequest
|
||||
decoder := json.NewDecoder(io.LimitReader(r.Body, interactiondomain.MaxHumanUtteranceBytes+1024))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&request); err != nil || decoder.Decode(&struct{}{}) != io.EOF {
|
||||
http.Error(w, "invalid chat message", http.StatusBadRequest)
|
||||
if session.PrincipalKind == identity.PrincipalKindService && (!request.SyntheticAcceptance || request.SyntheticScopeID != acceptancePrincipal.SyntheticScope) {
|
||||
http.Error(w, "synthetic acceptance scope denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
|
||||
func main() {
|
||||
reason := flag.String("reason", "", "rotation reason: owner-requested-rotation or exposure-detected")
|
||||
credential := flag.String("credential", "control-plane", "credential purpose: control-plane or acceptance-harness")
|
||||
flag.Parse()
|
||||
if flag.NArg() != 1 {
|
||||
fail("usage: secret-control-plane [ensure|rotate]")
|
||||
@@ -26,6 +27,9 @@ func main() {
|
||||
fail("control-plane credential store unavailable")
|
||||
}
|
||||
spec := platformsecrets.ControlPlaneServiceTokenSpec()
|
||||
if strings.TrimSpace(*credential) == "acceptance-harness" {
|
||||
spec = platformsecrets.AcceptanceHarnessServiceTokenSpec()
|
||||
}
|
||||
switch strings.TrimSpace(flag.Arg(0)) {
|
||||
case "ensure":
|
||||
result, err := store.Ensure(spec)
|
||||
|
||||
Reference in New Issue
Block a user