Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
288d275
feat(specs): credential sidecar isolation architecture
May 20, 2026
1cc2dc6
feat(control-plane): add per-credential sidecar injection
May 21, 2026
f1f070a
feat(runner): SSE sidecar transport for credential MCP servers
May 21, 2026
b018062
feat(runner): MCP-tool-only git push prompts in sidecar mode
May 21, 2026
0ab4da9
feat: credential sidecar Dockerfiles and build targets
May 21, 2026
e373777
test(control-plane): credential sidecar injection unit tests
May 21, 2026
a29f2b1
feat: credential sidecar token refresh via CP token exchange
May 21, 2026
eb027eb
chore: cleanup built artifacts and gitignore
May 21, 2026
0a96c00
fix: credential sidecar security hardening and end-to-end validation
May 21, 2026
469ca40
fix: migration ordering for role_bindings and pr-test standard OpenSh…
May 21, 2026
8ed5d4d
fix: add AMBIENT_API_TOKEN to api-server in install-standard.sh
May 21, 2026
d0ecdc6
feat: support ANTHROPIC_API_KEY in install-standard.sh
May 21, 2026
fa13c83
feat: add Vertex AI support to install-standard.sh
May 21, 2026
0e29da7
fix: make credential project_id optional in API schema
May 21, 2026
68bea15
feat: add --scope-id shorthand flag for role-binding creation
May 21, 2026
0c7e419
fix: add project-scoped credential routes and fix RBAC path resolution
May 21, 2026
5ef445f
fix: increase SSE scanner buffer to 1MB to prevent stream errors
May 21, 2026
37fefc9
fix: credential sidecar security hardening and end-to-end validation
May 22, 2026
6d05767
fix: address review bot feedback on credential sidecar entrypoint and…
May 22, 2026
9184faf
fix: use PtrString for *string ProjectId field in credential integrat…
May 22, 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,5 @@ hack/

# Personal exports
*.csv
components/credential-sidecars/entrypoint/credential-entrypoint
components/credential-sidecars/entrypoint/entrypoint
35 changes: 35 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
.PHONY: setup-minio minio-console minio-logs minio-status
.PHONY: validate-makefile lint-makefile check-shell makefile-health benchmark benchmark-ci
.PHONY: _create-operator-config _auto-port-forward _show-access-info _kind-load-images
.PHONY: build-credential-sidecars build-credential-github build-credential-jira build-credential-k8s build-credential-google

# Default target
.DEFAULT_GOAL := help
Expand Down Expand Up @@ -67,6 +68,10 @@ STATE_SYNC_IMAGE ?= vteam_state_sync:$(IMAGE_TAG)
PUBLIC_API_IMAGE ?= vteam_public_api:$(IMAGE_TAG)
API_SERVER_IMAGE ?= vteam_api_server:$(IMAGE_TAG)
OBSERVABILITY_DASHBOARD_IMAGE ?= vteam_observability_dashboard:$(IMAGE_TAG)
GITHUB_MCP_IMAGE ?= vteam_credential_github:$(IMAGE_TAG)
JIRA_MCP_IMAGE ?= vteam_credential_jira:$(IMAGE_TAG)
K8S_MCP_IMAGE ?= vteam_credential_k8s:$(IMAGE_TAG)
GOOGLE_MCP_IMAGE ?= vteam_credential_google:$(IMAGE_TAG)

# kind-local overlay always references localhost/vteam_* images.
# Podman produces this prefix natively; for Docker we tag before loading.
Expand Down Expand Up @@ -221,6 +226,36 @@ build-observability-dashboard: ## Build observability dashboard image
-t $(OBSERVABILITY_DASHBOARD_IMAGE) .
@echo "$(COLOR_GREEN)✓$(COLOR_RESET) Observability dashboard built: $(OBSERVABILITY_DASHBOARD_IMAGE)"

build-credential-sidecars: build-credential-github build-credential-jira build-credential-k8s build-credential-google ## Build all credential sidecar images

build-credential-github: ## Build GitHub credential sidecar image
@echo "$(COLOR_BLUE)▶$(COLOR_RESET) Building GitHub credential sidecar with $(CONTAINER_ENGINE)..."
@$(CONTAINER_ENGINE) build $(PLATFORM_FLAG) $(BUILD_FLAGS) \
-f components/credential-sidecars/github/Dockerfile \
-t $(GITHUB_MCP_IMAGE) .
@echo "$(COLOR_GREEN)✓$(COLOR_RESET) GitHub credential sidecar built: $(GITHUB_MCP_IMAGE)"

build-credential-jira: ## Build Jira credential sidecar image
@echo "$(COLOR_BLUE)▶$(COLOR_RESET) Building Jira credential sidecar with $(CONTAINER_ENGINE)..."
@$(CONTAINER_ENGINE) build $(PLATFORM_FLAG) $(BUILD_FLAGS) \
-f components/credential-sidecars/jira/Dockerfile \
-t $(JIRA_MCP_IMAGE) .
@echo "$(COLOR_GREEN)✓$(COLOR_RESET) Jira credential sidecar built: $(JIRA_MCP_IMAGE)"

build-credential-k8s: ## Build K8s credential sidecar image
@echo "$(COLOR_BLUE)▶$(COLOR_RESET) Building K8s credential sidecar with $(CONTAINER_ENGINE)..."
@$(CONTAINER_ENGINE) build $(PLATFORM_FLAG) $(BUILD_FLAGS) \
-f components/credential-sidecars/k8s/Dockerfile \
-t $(K8S_MCP_IMAGE) .
@echo "$(COLOR_GREEN)✓$(COLOR_RESET) K8s credential sidecar built: $(K8S_MCP_IMAGE)"

build-credential-google: ## Build Google credential sidecar image
@echo "$(COLOR_BLUE)▶$(COLOR_RESET) Building Google credential sidecar with $(CONTAINER_ENGINE)..."
@$(CONTAINER_ENGINE) build $(PLATFORM_FLAG) $(BUILD_FLAGS) \
-f components/credential-sidecars/google/Dockerfile \
-t $(GOOGLE_MCP_IMAGE) .
@echo "$(COLOR_GREEN)✓$(COLOR_RESET) Google credential sidecar built: $(GOOGLE_MCP_IMAGE)"

Comment on lines +229 to +258
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Declare new credential-sidecar targets as .PHONY.

The targets added at Lines 228, 230, 237, 243, and 249 are missing from .PHONY (Lines 1-13). If same-named files appear, recipes may not run.

Suggested patch
-.PHONY: _create-operator-config _auto-port-forward _show-access-info _kind-load-images
+.PHONY: _create-operator-config _auto-port-forward _show-access-info _kind-load-images
+.PHONY: build-credential-sidecars build-credential-github build-credential-jira build-credential-k8s build-credential-google
🧰 Tools
🪛 checkmake (0.3.2)

[warning] 228-228: Target "build-credential-sidecars" should be declared PHONY.

(phonydeclared)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Makefile` around lines 228 - 254, Add the new Makefile targets to the .PHONY
declaration so their recipes always run: include build-credential-sidecars,
build-credential-github, build-credential-jira, build-credential-k8s, and
build-credential-google in the existing .PHONY list (update the .PHONY line near
the top where other phony targets are declared) to prevent name collisions with
files of the same names.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Credential sidecar build targets are currently orphaned from primary workflows.

At Line 228 you add sidecar build targets, but build-all (Line 170), push-all (Line 299), and _kind-load-images (Line 1263) still omit these images. This means make kind-up LOCAL_IMAGES=true and make push-all can skip sidecar images even when sidecars are required.

Suggested patch
-build-all: build-frontend build-backend build-operator build-runner build-state-sync build-public-api build-api-server build-observability-dashboard ## Build all container images
+build-all: build-frontend build-backend build-operator build-runner build-state-sync build-public-api build-api-server build-observability-dashboard build-credential-sidecars ## Build all container images
@@
-	`@for` image in $(FRONTEND_IMAGE) $(BACKEND_IMAGE) $(OPERATOR_IMAGE) $(RUNNER_IMAGE) $(STATE_SYNC_IMAGE) $(PUBLIC_API_IMAGE) $(API_SERVER_IMAGE) $(OBSERVABILITY_DASHBOARD_IMAGE); do \
+	`@for` image in $(FRONTEND_IMAGE) $(BACKEND_IMAGE) $(OPERATOR_IMAGE) $(RUNNER_IMAGE) $(STATE_SYNC_IMAGE) $(PUBLIC_API_IMAGE) $(API_SERVER_IMAGE) $(OBSERVABILITY_DASHBOARD_IMAGE) $(GITHUB_MCP_IMAGE) $(JIRA_MCP_IMAGE) $(K8S_MCP_IMAGE) $(GOOGLE_MCP_IMAGE); do \
@@
-	`@for` img in $(BACKEND_IMAGE) $(FRONTEND_IMAGE) $(OPERATOR_IMAGE) $(RUNNER_IMAGE) $(STATE_SYNC_IMAGE) $(PUBLIC_API_IMAGE) $(API_SERVER_IMAGE) $(OBSERVABILITY_DASHBOARD_IMAGE); do \
+	`@for` img in $(BACKEND_IMAGE) $(FRONTEND_IMAGE) $(OPERATOR_IMAGE) $(RUNNER_IMAGE) $(STATE_SYNC_IMAGE) $(PUBLIC_API_IMAGE) $(API_SERVER_IMAGE) $(OBSERVABILITY_DASHBOARD_IMAGE) $(GITHUB_MCP_IMAGE) $(JIRA_MCP_IMAGE) $(K8S_MCP_IMAGE) $(GOOGLE_MCP_IMAGE); do \
🧰 Tools
🪛 checkmake (0.3.2)

[warning] 228-228: Target "build-credential-sidecars" should be declared PHONY.

(phonydeclared)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Makefile` around lines 228 - 254, The new credential sidecar targets
(build-credential-github, build-credential-jira, build-credential-k8s,
build-credential-google) are not wired into the main flows; update the primary
meta targets build-all, push-all and the _kind-load-images target to include
these sidecar targets (or include a sidecar variable list) so that running make
build-all, make push-all, or make _kind-load-images (and make kind-up
LOCAL_IMAGES=true) will build/push/load the credential sidecars; reference the
targets by name (build-credential-github, build-credential-jira,
build-credential-k8s, build-credential-google) when adding them as dependencies
or appending them to the appropriate image lists used by build-all, push-all and
_kind-load-images.

build-cli: ## Build acpctl CLI binary
@echo "$(COLOR_BLUE)▶$(COLOR_RESET) Building acpctl CLI..."
@cd components/ambient-cli && make build
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4963,7 +4963,6 @@ components:
type: string
required:
- name
- project_id
- provider
type: object
example:
Expand All @@ -4975,7 +4974,6 @@ components:
token: token
labels: labels
updated_at: 2000-01-23T04:56:07.000+00:00
project_id: project_id
provider: github
name: name
id: id
Expand Down
35 changes: 21 additions & 14 deletions components/ambient-api-server/pkg/api/openapi/model_credential.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion components/ambient-api-server/pkg/rbac/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,10 @@ func pathToResource(path string) string {
for i, p := range parts {
if p == "v1" && i+1 < len(parts) {
seg := parts[i+1]
return strings.ReplaceAll(strings.TrimSuffix(seg, "s"), "_", "_")
if seg == "projects" && i+3 < len(parts) {
seg = parts[i+3]
}
return strings.TrimSuffix(seg, "s")
}
}
return "unknown"
Expand Down
58 changes: 58 additions & 0 deletions components/ambient-api-server/pkg/rbac/middleware_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package rbac

import (
"net/http"
"testing"
)

func TestPathToResource(t *testing.T) {
tests := []struct {
path string
want string
}{
{"/api/ambient/v1/credentials", "credential"},
{"/api/ambient/v1/credentials/abc123", "credential"},
{"/api/ambient/v1/credentials/abc123/token", "credential"},
{"/api/ambient/v1/projects/prtest/credentials/abc123/token", "credential"},
{"/api/ambient/v1/projects/prtest/credentials", "credential"},
{"/api/ambient/v1/projects", "project"},
{"/api/ambient/v1/projects/prtest", "project"},
{"/api/ambient/v1/sessions", "session"},
{"/api/ambient/v1/role_bindings", "role_binding"},
{"/api/ambient/v1/roles", "role"},
{"/foo/bar", "unknown"},
}
for _, tt := range tests {
t.Run(tt.path, func(t *testing.T) {
got := pathToResource(tt.path)
if got != tt.want {
t.Errorf("pathToResource(%q) = %q, want %q", tt.path, got, tt.want)
}
})
}
}

func TestPathToAction(t *testing.T) {
tests := []struct {
method string
path string
want string
}{
{http.MethodGet, "/api/ambient/v1/credentials/abc123/token", "fetch_token"},
{http.MethodGet, "/api/ambient/v1/projects/prtest/credentials/abc123/token", "fetch_token"},
{http.MethodGet, "/api/ambient/v1/credentials", "read"},
{http.MethodPost, "/api/ambient/v1/credentials", "create"},
{http.MethodPatch, "/api/ambient/v1/credentials/abc123", "update"},
{http.MethodDelete, "/api/ambient/v1/credentials/abc123", "delete"},
{http.MethodGet, "/api/ambient/v1/agents/abc123/start", "start"},
{http.MethodGet, "/api/ambient/v1/agents/abc123/stop", "stop"},
}
for _, tt := range tests {
t.Run(tt.method+" "+tt.path, func(t *testing.T) {
got := pathToAction(tt.method, tt.path)
if got != tt.want {
t.Errorf("pathToAction(%q, %q) = %q, want %q", tt.method, tt.path, got, tt.want)
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func TestCredentialPost(t *testing.T) {
ctx := h.NewAuthenticatedContext(account)

credentialInput := openapi.Credential{
ProjectId: testProjectID,
ProjectId: openapi.PtrString(testProjectID),
Name: "test-name",
Description: openapi.PtrString("test-description"),
Provider: "test-provider",
Expand Down
10 changes: 10 additions & 0 deletions components/ambient-api-server/plugins/credentials/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ func init() {
credentialsRouter.HandleFunc("/{cred_id}/token", credentialHandler.GetToken).Methods(http.MethodGet)
credentialsRouter.Use(authMiddleware.AuthenticateAccountJWT)
credentialsRouter.Use(authzMiddleware.AuthorizeApi)

projectCredRouter := apiV1Router.PathPrefix("/projects").Subrouter()
projectCredRouter.HandleFunc("/{id}/credentials", credentialHandler.List).Methods(http.MethodGet)
projectCredRouter.HandleFunc("/{id}/credentials", credentialHandler.Create).Methods(http.MethodPost)
projectCredRouter.HandleFunc("/{id}/credentials/{cred_id}", credentialHandler.Get).Methods(http.MethodGet)
projectCredRouter.HandleFunc("/{id}/credentials/{cred_id}", credentialHandler.Patch).Methods(http.MethodPatch)
projectCredRouter.HandleFunc("/{id}/credentials/{cred_id}", credentialHandler.Delete).Methods(http.MethodDelete)
projectCredRouter.HandleFunc("/{id}/credentials/{cred_id}/token", credentialHandler.GetToken).Methods(http.MethodGet)
projectCredRouter.Use(authMiddleware.AuthenticateAccountJWT)
projectCredRouter.Use(authzMiddleware.AuthorizeApi)
})

pkgserver.RegisterController("Credentials", func(manager *controllers.KindControllerManager, services pkgserver.ServicesInterface) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ func migration() *gormigrate.Migration {
type Project struct {
db.Model
Name string `gorm:"uniqueIndex;not null"`
DisplayName *string
Description *string
Labels *string
Annotations *string
Expand Down
17 changes: 17 additions & 0 deletions components/ambient-cli/cmd/acpctl/create/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ var createArgs struct {
bindAgentID string
bindSessionID string
bindCredID string
scopeID string
}

func init() {
Expand All @@ -80,6 +81,7 @@ func init() {
Cmd.Flags().StringVar(&createArgs.bindAgentID, "agent-id-fk", "", "Agent FK for role-binding")
Cmd.Flags().StringVar(&createArgs.bindSessionID, "session-id-fk", "", "Session FK for role-binding")
Cmd.Flags().StringVar(&createArgs.bindCredID, "credential-id-fk", "", "Credential FK for role-binding")
Cmd.Flags().StringVar(&createArgs.scopeID, "scope-id", "", "Scope target ID for role-binding (shorthand for --{scope}-id-fk)")
}

func run(cmd *cobra.Command, cmdArgs []string) error {
Expand Down Expand Up @@ -299,6 +301,21 @@ func createRoleBinding(cmd *cobra.Command, ctx context.Context, client *sdkclien
return fmt.Errorf("--scope is required")
}

if createArgs.scopeID != "" {
switch createArgs.scope {
case "project":
createArgs.bindProjectID = createArgs.scopeID
case "agent":
createArgs.bindAgentID = createArgs.scopeID
case "session":
createArgs.bindSessionID = createArgs.scopeID
case "credential":
createArgs.bindCredID = createArgs.scopeID
default:
return fmt.Errorf("--scope-id not supported for scope %q; use the explicit FK flag", createArgs.scope)
}
}

builder := sdktypes.NewRoleBindingBuilder().
RoleID(createArgs.roleID).
Scope(createArgs.scope)
Expand Down
29 changes: 29 additions & 0 deletions components/ambient-cli/cmd/acpctl/create/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,35 @@ func TestCreateRoleBinding_Success(t *testing.T) {
}
}

func TestCreateRoleBinding_ScopeID(t *testing.T) {
srv := testhelper.NewServer(t)
srv.Handle("/api/ambient/v1/role_bindings", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
credID := "cred-1"
srv.RespondJSON(t, w, http.StatusCreated, &types.RoleBinding{
ObjectReference: types.ObjectReference{ID: "rb-scope"},
RoleID: "r-1",
Scope: "credential",
CredentialID: &credID,
})
})

testhelper.Configure(t, srv.URL)
result := testhelper.Run(t, Cmd, "role-binding",
"--role-id", "r-1",
"--scope", "credential",
"--scope-id", "cred-1",
)
if result.Err != nil {
t.Fatalf("unexpected error: %v\nstdout: %s\nstderr: %s", result.Err, result.Stdout, result.Stderr)
}
if !strings.Contains(result.Stdout, "role-binding/rb-scope") {
t.Errorf("expected 'role-binding/rb-scope created', got: %s", result.Stdout)
}
}

func TestCreateRoleBinding_MissingScope(t *testing.T) {
srv := testhelper.NewServer(t)
testhelper.Configure(t, srv.URL)
Expand Down
1 change: 1 addition & 0 deletions components/ambient-cli/cmd/acpctl/session/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func runEvents(cmd *cobra.Command, args []string) error {
fmt.Fprintf(cmd.OutOrStdout(), "Streaming events for session %s (Ctrl+C to stop)...\n\n", sessionID)

scanner := bufio.NewScanner(stream)
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "data: ") {
Expand Down
1 change: 1 addition & 0 deletions components/ambient-cli/cmd/acpctl/session/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,7 @@ func streamMessagesContinuous(cmd *cobra.Command, client *sdkclient.Client, sess

func renderSSEStream(stream io.Reader, out io.Writer, jsonMode, exitOnRunFinished bool) error {
scanner := bufio.NewScanner(stream)
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
var reasoningBuf strings.Builder
var inText bool
for scanner.Scan() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,18 @@ func runKubeMode(ctx context.Context, cfg *config.ControlPlaneConfig) error {
RunnerImageNamespace: cfg.RunnerImageNamespace,
MCPImage: cfg.MCPImage,
MCPAPIServerURL: cfg.MCPAPIServerURL,
GitHubMCPImage: cfg.GitHubMCPImage,
JiraMCPImage: cfg.JiraMCPImage,
K8sMCPImage: cfg.K8sMCPImage,
GoogleMCPImage: cfg.GoogleMCPImage,
RunnerLogLevel: cfg.RunnerLogLevel,
CPRuntimeNamespace: cfg.CPRuntimeNamespace,
CPTokenURL: cfg.CPTokenURL,
CPTokenPublicKey: string(kp.PublicKeyPEM),
HTTPProxy: cfg.HTTPProxy,
HTTPSProxy: cfg.HTTPSProxy,
NoProxy: cfg.NoProxy,
ImagePullSecret: cfg.ImagePullSecret,
}

conn, err := grpc.NewClient(cfg.GRPCServerAddr, grpc.WithTransportCredentials(grpcCredentials(cfg.GRPCUseTLS)))
Expand Down
Loading
Loading