Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
78db529
feat(workstation): escalating shell gate with command-name extraction
mindburnlabs Jul 24, 2026
8a8e6cf
feat(cli): watch subcommand + workstation gate command surface
mindburnlabs Jul 24, 2026
b170070
fix(shellgate): bind approvals and close unsafe paths
mindburnlabs Jul 29, 2026
ca2d0ad
Merge remote-tracking branch 'origin/main' into tui-shell-gate
mindburnlabs Jul 29, 2026
a5899cd
fix(cli): retain terminal watch dependency
mindburnlabs Jul 29, 2026
8ea11b4
fix(cli): complete terminal watch module graph
mindburnlabs Jul 29, 2026
5ecbc2b
fix(shellgate): preserve outer wrapper flag semantics
mindburnlabs Jul 29, 2026
91489f2
Merge remote-tracking branch 'origin/main' into tui-shell-gate
mindburnlabs Jul 29, 2026
5f94d49
Merge remote-tracking branch 'origin/main' into tui-shell-gate
mindburnlabs Jul 29, 2026
d173c73
Merge remote-tracking branch 'origin/main' into tui-shell-gate
mindburnlabs Jul 29, 2026
671b815
fix(workstation): close shell gate permit findings
mindburnlabs Jul 29, 2026
2de64cb
fix(workstation): seal approval and terminal boundaries
mindburnlabs Jul 29, 2026
6b76e8b
Merge remote-tracking branch 'origin/main' into tui-shell-gate
mindburnlabs Jul 29, 2026
7e69c77
chore(boundary): refresh merged protected manifest
mindburnlabs Jul 29, 2026
5eb0d87
fix(workstation): separate approval authority
mindburnlabs Jul 29, 2026
7fc84e9
fix(workstation): close approval race and dynamic redirects
mindburnlabs Jul 29, 2026
3badfee
fix(workstation): close permit security findings
mindburnlabs Jul 30, 2026
4ffa7e1
Merge branch 'main' into tui-shell-gate
mindburnlabs Aug 1, 2026
2e37b7c
Merge branch 'main' into tui-shell-gate
mindburnlabs Aug 1, 2026
a5722cd
Merge branch 'main' into tui-shell-gate
mindburnlabs Aug 1, 2026
4ac41e4
Merge branch 'main' into tui-shell-gate
mindburnlabs Aug 1, 2026
5e66e18
Merge branch 'main' into tui-shell-gate
mindburnlabs Aug 1, 2026
5633950
Merge branch 'main' into tui-shell-gate
mindburnlabs Aug 1, 2026
af60df1
Merge branch 'main' into tui-shell-gate
mindburnlabs Aug 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion api/openapi/helm.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3647,7 +3647,7 @@ paths:
tags: [identity]
summary: Create an approval ceremony
security:
- AdminBearerAuth: []
- ServiceBearerAuth: []
requestBody:
content:
application/json:
Expand Down Expand Up @@ -6147,6 +6147,7 @@ components:
timelock_until: { type: string, format: date-time }
expires_at: { type: string, format: date-time }
break_glass: { type: boolean }
binding_hash: { type: string }
reason: { type: string }
receipt_id: { type: string }
ceremony_hash: { type: string }
Expand Down
7 changes: 6 additions & 1 deletion core/cmd/helm-ai-kernel/boundary_surface_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,11 @@ func runApprovalsCreate(args []string, registry *boundarypkg.SurfaceRegistry, st
fmt.Fprintln(stderr, "Error: --subject and --action are required")
return 2
}
approvalID, err := contracts.NewSurfaceID("approval")
if err != nil {
fmt.Fprintf(stderr, "Error: %v\n", err)
return 1
}
now := time.Now().UTC()
var timelock time.Time
if *timelockMs > 0 {
Expand All @@ -459,7 +464,7 @@ func runApprovalsCreate(args []string, registry *boundarypkg.SurfaceRegistry, st
expiresAt = now.Add(time.Duration(*expiresInMs) * time.Millisecond)
}
approval, err := registry.PutApproval(contracts.ApprovalCeremony{
ApprovalID: contracts.SurfaceID("approval", *subject+"-"+*action),
ApprovalID: approvalID,
Subject: *subject,
Action: *action,
State: contracts.ApprovalCeremonyPending,
Expand Down
59 changes: 56 additions & 3 deletions core/cmd/helm-ai-kernel/contract_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
mcppkg "github.com/Mindburn-Labs/helm-ai-kernel/core/pkg/mcp"
helmotel "github.com/Mindburn-Labs/helm-ai-kernel/core/pkg/otel"
runtimesandbox "github.com/Mindburn-Labs/helm-ai-kernel/core/pkg/runtime/sandbox"
"github.com/Mindburn-Labs/helm-ai-kernel/core/pkg/workstation"
)

const (
Expand Down Expand Up @@ -1130,7 +1131,7 @@ func registerContractRoutes(mux *http.ServeMux, svc *Services) {
writeContractJSON(w, http.StatusOK, snapshot)
}))

mux.HandleFunc("/api/v1/approvals", protectRuntimeHandler(RouteAuthAdmin, func(w http.ResponseWriter, r *http.Request) {
mux.HandleFunc("/api/v1/approvals", protectApprovalCollectionHandler(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
writeContractJSON(w, http.StatusOK, surfaces.ListApprovals())
Expand All @@ -1142,6 +1143,7 @@ func registerContractRoutes(mux *http.ServeMux, svc *Services) {
RequestedBy string `json:"requested_by"`
Approvers []string `json:"approvers"`
Quorum int `json:"quorum"`
BindingHash string `json:"binding_hash"`
TimelockMs int64 `json:"timelock_ms"`
ExpiresInMs int64 `json:"expires_in_ms"`
Reason string `json:"reason"`
Expand All @@ -1153,7 +1155,12 @@ func registerContractRoutes(mux *http.ServeMux, svc *Services) {
return
}
if req.ApprovalID == "" {
req.ApprovalID = contracts.SurfaceID("approval", req.Subject+"-"+req.Action)
var err error
req.ApprovalID, err = contracts.NewSurfaceID("approval")
if err != nil {
api.WriteInternal(w, err)
return
}
}
now := time.Now().UTC()
var timelock time.Time
Expand All @@ -1172,6 +1179,7 @@ func registerContractRoutes(mux *http.ServeMux, svc *Services) {
RequestedBy: req.RequestedBy,
Approvers: req.Approvers,
Quorum: req.Quorum,
BindingHash: req.BindingHash,
TimelockUntil: timelock,
ExpiresAt: expires,
BreakGlass: req.BreakGlass,
Expand All @@ -1190,7 +1198,7 @@ func registerContractRoutes(mux *http.ServeMux, svc *Services) {
}
}))

mux.HandleFunc("/api/v1/approvals/", protectRuntimeHandler(RouteAuthAdmin, func(w http.ResponseWriter, r *http.Request) {
mux.HandleFunc("/api/v1/approvals/", protectApprovalItemHandler(func(w http.ResponseWriter, r *http.Request) {
suffix := strings.TrimPrefix(r.URL.Path, "/api/v1/approvals/")
approvalID, action, ok := strings.Cut(suffix, "/")
if !ok || approvalID == "" {
Expand All @@ -1201,6 +1209,51 @@ func registerContractRoutes(mux *http.ServeMux, svc *Services) {
api.WriteMethodNotAllowed(w)
return
}
if action == "consume" {
var req struct {
BindingHash string `json:"binding_hash"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || strings.TrimSpace(req.BindingHash) == "" {
api.WriteBadRequest(w, "binding_hash is required")
return
}
var matched *contracts.ApprovalCeremony
for _, approval := range surfaces.ListApprovals() {
if approval.ApprovalID == approvalID {
copy := approval
matched = &copy
break
}
}
if matched == nil {
api.WriteNotFound(w, "approval not found")
return
}
if matched.State != contracts.ApprovalCeremonyAllowed ||
matched.Subject != workstation.ShellGateApprovalSubject ||
matched.Action != workstation.ShellGateApprovalAction ||
matched.BindingHash != req.BindingHash {
api.WriteBadRequest(w, "approval is not an approved shell command with this binding")
return
}
if !matched.ExpiresAt.IsZero() && !time.Now().Before(matched.ExpiresAt) {
api.WriteBadRequest(w, "approval is expired")
return
}
approval, err := surfaces.TransitionApproval(
approvalID,
contracts.ApprovalCeremonyRevoked,
servicePrincipalID,
"",
"consumed by workstation shell gate",
)
if err != nil {
api.WriteBadRequest(w, err.Error())
return
}
writeContractJSON(w, http.StatusOK, approval)
return
}
if action == "webauthn/challenge" {
var req struct {
Method string `json:"method"`
Expand Down
90 changes: 89 additions & 1 deletion core/cmd/helm-ai-kernel/contract_routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"sort"
"strings"
"testing"
Expand Down Expand Up @@ -356,7 +357,7 @@ func TestApprovalRoutesSupportWebAuthnChallengeAssertion(t *testing.T) {
registerContractRoutes(mux, svc)

createReq := httptest.NewRequest(http.MethodPost, "/api/v1/approvals", strings.NewReader(`{"approval_id":"approval-webauthn","subject":"mcp:srv","action":"mcp.approve","requested_by":"agent:test","quorum":1}`))
authorizeTestRequest(createReq)
authorizeServiceTestRequest(createReq)
createRec := httptest.NewRecorder()
mux.ServeHTTP(createRec, createReq)
if createRec.Code != http.StatusCreated {
Expand Down Expand Up @@ -395,6 +396,88 @@ func TestApprovalRoutesSupportWebAuthnChallengeAssertion(t *testing.T) {
}
}

func TestApprovalRoutesSplitRequestApprovalAndConsumptionAuthority(t *testing.T) {
svc, cleanup := newContractRouteTestServices(t)
defer cleanup()
mux := http.NewServeMux()
registerContractRoutes(mux, svc)

payload := `{"subject":"shell_command","action":"shell_operate","requested_by":"agent.local","quorum":1,"binding_hash":"sha256:exact-command","reason":"shellgate-binding=sha256:exact-command"}`
adminCreate := httptest.NewRequest(http.MethodPost, approvalAPIBasePath, strings.NewReader(payload))
authorizeTestRequest(adminCreate)
adminCreateRec := httptest.NewRecorder()
mux.ServeHTTP(adminCreateRec, adminCreate)
if adminCreateRec.Code != http.StatusUnauthorized {
t.Fatalf("admin credential created requester ceremony: status=%d body=%s", adminCreateRec.Code, adminCreateRec.Body.String())
}

ids := make([]string, 0, 2)
for i := 0; i < 2; i++ {
req := httptest.NewRequest(http.MethodPost, approvalAPIBasePath, strings.NewReader(payload))
authorizeServiceTestRequest(req)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusCreated {
t.Fatalf("service create %d status=%d body=%s", i, rec.Code, rec.Body.String())
}
var approval contracts.ApprovalCeremony
if err := json.NewDecoder(rec.Body).Decode(&approval); err != nil {
t.Fatal(err)
}
ids = append(ids, approval.ApprovalID)
}
if ids[0] == ids[1] {
t.Fatalf("missing approval ids collided: %q", ids[0])
}
duplicatePayload := strings.Replace(payload, `"subject":"shell_command"`, `"approval_id":"`+ids[0]+`","subject":"shell_command"`, 1)
duplicate := httptest.NewRequest(http.MethodPost, approvalAPIBasePath, strings.NewReader(duplicatePayload))
authorizeServiceTestRequest(duplicate)
duplicateRec := httptest.NewRecorder()
mux.ServeHTTP(duplicateRec, duplicate)
if duplicateRec.Code != http.StatusBadRequest {
t.Fatalf("explicit duplicate overwrote ceremony: status=%d body=%s", duplicateRec.Code, duplicateRec.Body.String())
}

serviceApprove := httptest.NewRequest(http.MethodPost, approvalAPIBasePath+"/"+ids[0]+"/approve", strings.NewReader(`{"actor":"operator.cli"}`))
authorizeServiceTestRequest(serviceApprove)
serviceApproveRec := httptest.NewRecorder()
mux.ServeHTTP(serviceApproveRec, serviceApprove)
if serviceApproveRec.Code != http.StatusUnauthorized {
t.Fatalf("request credential approved ceremony: status=%d body=%s", serviceApproveRec.Code, serviceApproveRec.Body.String())
}

adminApprove := httptest.NewRequest(http.MethodPost, approvalAPIBasePath+"/"+ids[0]+"/approve", strings.NewReader(`{"actor":"operator.cli"}`))
authorizeTestRequest(adminApprove)
adminApproveRec := httptest.NewRecorder()
mux.ServeHTTP(adminApproveRec, adminApprove)
if adminApproveRec.Code != http.StatusOK {
t.Fatalf("admin approve status=%d body=%s", adminApproveRec.Code, adminApproveRec.Body.String())
}

wrongConsume := httptest.NewRequest(http.MethodPost, approvalAPIBasePath+"/"+ids[0]+"/consume", strings.NewReader(`{"binding_hash":"sha256:other-command"}`))
authorizeServiceTestRequest(wrongConsume)
wrongConsumeRec := httptest.NewRecorder()
mux.ServeHTTP(wrongConsumeRec, wrongConsume)
if wrongConsumeRec.Code != http.StatusBadRequest {
t.Fatalf("wrong binding consume status=%d body=%s", wrongConsumeRec.Code, wrongConsumeRec.Body.String())
}

consume := httptest.NewRequest(http.MethodPost, approvalAPIBasePath+"/"+ids[0]+"/consume", strings.NewReader(`{"binding_hash":"sha256:exact-command"}`))
authorizeServiceTestRequest(consume)
consumeRec := httptest.NewRecorder()
mux.ServeHTTP(consumeRec, consume)
if consumeRec.Code != http.StatusOK {
t.Fatalf("exact binding consume status=%d body=%s", consumeRec.Code, consumeRec.Body.String())
}
replayConsume := httptest.NewRequest(http.MethodPost, approvalAPIBasePath+"/"+ids[0]+"/consume", strings.NewReader(`{"binding_hash":"sha256:exact-command"}`))
authorizeServiceTestRequest(replayConsume)
replayConsumeRec := httptest.NewRecorder()
mux.ServeHTTP(replayConsumeRec, replayConsume)
if replayConsumeRec.Code != http.StatusBadRequest {
t.Fatalf("approval consumed twice: status=%d body=%s", replayConsumeRec.Code, replayConsumeRec.Body.String())
}
}

func TestReplayVerifyDetectsReceiptChainBreakWithValidManifest(t *testing.T) {
svc, cleanup := newContractRouteTestServices(t)
defer cleanup()
Expand Down Expand Up @@ -524,6 +607,7 @@ func TestReceiptListReturnsCursorPagination(t *testing.T) {
func newContractRouteTestServices(t *testing.T) (*Services, func()) {
t.Helper()
t.Setenv("HELM_ADMIN_API_KEY", testAdminAPIKey)
t.Setenv(serviceAPIKeyEnv, testAdminAPIKey+"-service")
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -593,6 +677,10 @@ func authorizeTestRequest(req *http.Request) {
req.Header.Set(principalHeader, "system-admin")
}

func authorizeServiceTestRequest(req *http.Request) {
req.Header.Set("Authorization", "Bearer "+os.Getenv(serviceAPIKeyEnv))
}

type overflowReceiptStore struct {
captureReceiptStore
}
Expand Down
24 changes: 24 additions & 0 deletions core/cmd/helm-ai-kernel/route_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,30 @@ func protectRuntimeHandler(auth RouteAuth, handler http.HandlerFunc) http.Handle
}
}

func protectApprovalCollectionHandler(handler http.HandlerFunc) http.HandlerFunc {
admin := requireRuntimeAdmin(handler)
service := requireRuntimeService(handler)
return func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
service(w, r)
return
}
admin(w, r)
}
}

func protectApprovalItemHandler(handler http.HandlerFunc) http.HandlerFunc {
admin := requireRuntimeAdmin(handler)
service := requireRuntimeService(handler)
return func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/consume") {
service(w, r)
return
}
admin(w, r)
}
}

func requireRuntimeAdmin(handler http.HandlerFunc) http.HandlerFunc {
adminKey := os.Getenv(helmauth.AdminAPIKeyEnv)
return func(w http.ResponseWriter, r *http.Request) {
Expand Down
3 changes: 2 additions & 1 deletion core/cmd/helm-ai-kernel/route_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@ func RuntimeRouteSpecs() []RuntimeRouteSpec {
{Method: http.MethodGet, Path: "/api/v1/authz/snapshots", MuxPattern: "/api/v1/authz/snapshots", Auth: RouteAuthAdmin, RateLimit: RouteRateAdmin, ContractStatus: RouteContractPublic, OperationID: "listAuthzSnapshots", Owner: "core/cmd/helm-ai-kernel"},
{Method: http.MethodGet, Path: "/api/v1/authz/snapshots/{snapshot_id}", MuxPattern: "/api/v1/authz/snapshots/", Auth: RouteAuthAdmin, RateLimit: RouteRateAdmin, ContractStatus: RouteContractPublic, OperationID: "getAuthzSnapshot", Owner: "core/cmd/helm-ai-kernel"},
{Method: http.MethodGet, Path: "/api/v1/approvals", MuxPattern: "/api/v1/approvals", Auth: RouteAuthAdmin, RateLimit: RouteRateAdmin, ContractStatus: RouteContractPublic, OperationID: "listApprovalCeremonies", Owner: "core/cmd/helm-ai-kernel"},
{Method: http.MethodPost, Path: "/api/v1/approvals", MuxPattern: "/api/v1/approvals", Auth: RouteAuthAdmin, RateLimit: RouteRateAdmin, ContractStatus: RouteContractPublic, OperationID: "createApprovalCeremony", Owner: "core/cmd/helm-ai-kernel"},
{Method: http.MethodPost, Path: "/api/v1/approvals", MuxPattern: "/api/v1/approvals", Auth: RouteAuthService, RateLimit: RouteRateAdmin, ContractStatus: RouteContractPublic, OperationID: "createApprovalCeremony", Owner: "core/cmd/helm-ai-kernel"},
{Method: http.MethodPost, Path: "/api/v1/approvals/{approval_id}/consume", MuxPattern: "/api/v1/approvals/", Auth: RouteAuthService, RateLimit: RouteRateAdmin, ContractStatus: RouteContractInternal, OperationID: "consumeShellApprovalCeremony", Owner: "core/cmd/helm-ai-kernel"},
{Method: http.MethodPost, Path: "/api/v1/approvals/{approval_id}/webauthn/challenge", MuxPattern: "/api/v1/approvals/", Auth: RouteAuthAdmin, RateLimit: RouteRateAdmin, ContractStatus: RouteContractPublic, OperationID: "createApprovalWebAuthnChallenge", Owner: "core/cmd/helm-ai-kernel"},
{Method: http.MethodPost, Path: "/api/v1/approvals/{approval_id}/webauthn/assert", MuxPattern: "/api/v1/approvals/", Auth: RouteAuthAdmin, RateLimit: RouteRateAdmin, ContractStatus: RouteContractPublic, OperationID: "assertApprovalWebAuthnChallenge", Owner: "core/cmd/helm-ai-kernel"},
{Method: http.MethodPost, Path: "/api/v1/approvals/{approval_id}/{action}", MuxPattern: "/api/v1/approvals/", Auth: RouteAuthAdmin, RateLimit: RouteRateAdmin, ContractStatus: RouteContractPublic, OperationID: "transitionApprovalCeremony", Owner: "core/cmd/helm-ai-kernel"},
Expand Down
Loading
Loading