mirror of
https://github.com/open-gitagent/langship.sh.git
synced 2026-08-03 07:21:04 +02:00
feat: implement environment management features
- Added environment creation and editing pages with forms for name and description. - Integrated environment listing with options to edit and delete environments. - Updated agent detail page to manage environments followed by agents. - Enhanced API to support environment operations including listing, creating, updating, and deleting environments. - Refactored related components and state management for improved clarity and functionality.
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
restate "github.com/restatedev/sdk-go"
|
||||
@@ -17,6 +18,13 @@ import (
|
||||
// It pauses the workflow using a Restate Awakeable and blocks until an external
|
||||
// caller resolves it via POST /api/executions/{id}/resume.
|
||||
//
|
||||
// Node params (all optional):
|
||||
// - method "ui" (default) | "quorum" | "auto" — "auto" emits on output 0
|
||||
// immediately without pausing. quorum N>1 is surfaced in the reviewer
|
||||
// context; strict N-approver enforcement is a follow-up.
|
||||
// - minApprovers — for method "quorum".
|
||||
// - timeoutSeconds — reject automatically after that long.
|
||||
//
|
||||
// Two outputs:
|
||||
// - Output 0: approved — items flow with human-supplied data merged in
|
||||
// - Output 1: rejected — items flow with rejection_reason field
|
||||
@@ -39,6 +47,27 @@ func (e *ApprovalExecutor) Execute(
|
||||
inputItems = []models.Item{{}}
|
||||
}
|
||||
|
||||
// Resolve the effective approval policy from node params.
|
||||
method := strParam(node.Parameters, "method", "")
|
||||
if method == "" {
|
||||
method = "ui"
|
||||
}
|
||||
minApprovers := intParam(node.Parameters, "minApprovers", 1)
|
||||
timeoutSeconds := intParam(node.Parameters, "timeoutSeconds", 0)
|
||||
|
||||
// Auto method: don't pause at all — emit straight to the approved output.
|
||||
if method == "auto" {
|
||||
slog.InfoContext(ctx, "approval_auto", slog.String("node", node.Name))
|
||||
var out []models.Item
|
||||
for _, item := range inputItems {
|
||||
m := copyItem(item)
|
||||
m["approved"] = true
|
||||
m["approval_method"] = "auto"
|
||||
out = append(out, m)
|
||||
}
|
||||
return map[int][]models.Item{0: out}, nil
|
||||
}
|
||||
|
||||
// Approval requires Restate for durable blocking.
|
||||
raw := durability.RestateCtxFromContext(ctx)
|
||||
rctx, ok := raw.(restate.WorkflowContext)
|
||||
@@ -69,6 +98,13 @@ func (e *ApprovalExecutor) Execute(
|
||||
if reason, ok := resolved["reason"].(string); ok && reason != "" {
|
||||
approvalCtx["reason"] = reason
|
||||
}
|
||||
approvalCtx["method"] = method
|
||||
if method == "quorum" {
|
||||
approvalCtx["minApprovers"] = minApprovers
|
||||
}
|
||||
if timeoutSeconds > 0 {
|
||||
approvalCtx["timeoutSeconds"] = timeoutSeconds
|
||||
}
|
||||
if len(inputItems) == 1 {
|
||||
approvalCtx["inputs"] = map[string]any(inputItems[0])
|
||||
} else if len(inputItems) > 1 {
|
||||
@@ -111,10 +147,29 @@ func (e *ApprovalExecutor) Execute(
|
||||
slog.String("awakeable_id", awakeableID),
|
||||
)
|
||||
|
||||
// Block on the Awakeable. Durable across crashes / restarts.
|
||||
approvalData, err := awakeable.Result()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("approval node %q: %w", node.Name, err)
|
||||
// Block on the Awakeable. Durable across crashes / restarts. If a
|
||||
// timeout is set, race it against a durable Restate timer — whichever
|
||||
// fires first wins; on timeout we route to the rejected output.
|
||||
var approvalData map[string]any
|
||||
timedOut := false
|
||||
if timeoutSeconds > 0 {
|
||||
selector := restate.Select(rctx, awakeable, restate.After(rctx, time.Duration(timeoutSeconds)*time.Second))
|
||||
switch winner := selector.Select(); winner {
|
||||
case awakeable:
|
||||
d, err := awakeable.Result()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("approval node %q: %w", node.Name, err)
|
||||
}
|
||||
approvalData = d
|
||||
default:
|
||||
timedOut = true
|
||||
}
|
||||
} else {
|
||||
d, err := awakeable.Result()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("approval node %q: %w", node.Name, err)
|
||||
}
|
||||
approvalData = d
|
||||
}
|
||||
|
||||
// Clear pending markers now that we've resumed.
|
||||
@@ -122,6 +177,19 @@ func (e *ApprovalExecutor) Execute(
|
||||
restate.Clear(rctx, "pending_approval_id")
|
||||
restate.Clear(rctx, "pending_approval_context")
|
||||
|
||||
if timedOut {
|
||||
slog.InfoContext(ctx, "approval_timed_out",
|
||||
slog.String("node", node.Name), slog.Int("timeout_seconds", timeoutSeconds))
|
||||
var rejected []models.Item
|
||||
for _, item := range inputItems {
|
||||
r := copyItem(item)
|
||||
r["approved"] = false
|
||||
r["rejection_reason"] = fmt.Sprintf("approval timed out after %ds", timeoutSeconds)
|
||||
rejected = append(rejected, r)
|
||||
}
|
||||
return map[int][]models.Item{1: rejected}, nil
|
||||
}
|
||||
|
||||
slog.InfoContext(ctx, "workflow_resumed",
|
||||
slog.String("node", node.Name),
|
||||
)
|
||||
|
||||
+24
-18
@@ -44,16 +44,6 @@ func (e *DeployExecutor) Execute(ctx context.Context, node models.NodeDef, input
|
||||
}
|
||||
logger := engine.NodeLoggerFromContext(ctx)
|
||||
|
||||
target := strings.ToLower(strParam(node.Parameters, "target", "agentcore"))
|
||||
switch target {
|
||||
case "agentcore":
|
||||
// supported below
|
||||
case "k8s", "kubernetes", "vertex", "":
|
||||
return nil, fmt.Errorf("deploy: target %q not yet implemented (only \"agentcore\" today)", target)
|
||||
default:
|
||||
return nil, fmt.Errorf("deploy: unknown target %q", target)
|
||||
}
|
||||
|
||||
trigger := firstItem(inputs)
|
||||
agentID, _ := trigger["agentId"].(string)
|
||||
if agentID == "" {
|
||||
@@ -63,18 +53,33 @@ func (e *DeployExecutor) Execute(ctx context.Context, node models.NodeDef, input
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("deploy: load agent %q: %w", agentID, err)
|
||||
}
|
||||
credName := strParam(node.Parameters, "credentialName", "aws")
|
||||
// envName is purely informational here (recorded in the summary).
|
||||
envName, _ := trigger["environment"].(string)
|
||||
|
||||
// target: node param → "agentcore".
|
||||
target := strings.ToLower(strParam(node.Parameters, "target", ""))
|
||||
if target == "" {
|
||||
target = "agentcore"
|
||||
}
|
||||
switch target {
|
||||
case "agentcore":
|
||||
// supported below
|
||||
case "k8s", "kubernetes", "vertex":
|
||||
return nil, fmt.Errorf("deploy: target %q not yet implemented (only \"agentcore\" today)", target)
|
||||
default:
|
||||
return nil, fmt.Errorf("deploy: unknown target %q", target)
|
||||
}
|
||||
|
||||
// credentialName: node param → "aws".
|
||||
credName := strParam(node.Parameters, "credentialName", "")
|
||||
if credName == "" {
|
||||
credName = "aws"
|
||||
}
|
||||
cred, credScope, err := e.lookupCredential(ctx, a, credName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("deploy: %w", err)
|
||||
}
|
||||
logger.Log(fmt.Sprintf("[deploy] using %s credential %q", credScope, credName))
|
||||
if cred.Type != storage.CredentialAWS {
|
||||
return nil, fmt.Errorf("deploy: credential %q is type %q; target=agentcore needs an aws credential", credName, cred.Type)
|
||||
}
|
||||
if cred.AwsRegion == "" || cred.AwsAccountID == "" || cred.AwsCrossAccountRoleArn == "" {
|
||||
return nil, fmt.Errorf("deploy: aws credential %q is missing region / accountId / crossAccountRoleArn", credName)
|
||||
}
|
||||
logger.Log(fmt.Sprintf("[deploy] target=%s credential=%s (%s)", target, credName, credScope))
|
||||
|
||||
image := resolveDeployImage(node.Parameters, inputs)
|
||||
if image == "" {
|
||||
@@ -143,6 +148,7 @@ func (e *DeployExecutor) Execute(ctx context.Context, node models.NodeDef, input
|
||||
|
||||
summary := map[string]any{
|
||||
"target": "agentcore",
|
||||
"environment": envName,
|
||||
"agentId": agentID,
|
||||
"agentName": a.Name,
|
||||
"credentialName": cred.Name,
|
||||
|
||||
Reference in New Issue
Block a user