Skip to content

Commit 8ad69d4

Browse files
markturanskyAmbient Code Botclaude
authored
fix(control-plane): credential session unblock + project namespace cleanup + CLI credential verbs (#1181)
## Summary - **fix(control-plane):** Remove `ensureCredentialRoleBindings` from `kube_reconciler.go` — this was creating K8s `RoleBinding` objects referencing a non-existent `credential:token-reader` ClusterRole, blocking session provisioning. The runner authenticates via `BOT_TOKEN` (control-plane JWT injected as a secret), not K8s SA token, so the binding was vestigial and served no purpose. - **fix(control-plane):** `project_reconciler.go` `EventDeleted` now calls `DeprovisionNamespace` instead of logging "namespace retained for safety" (was a deliberate no-op that was never wired up). - **feat(cli):** Wire `credentials` into generic `acpctl get/delete/describe` verbs (was returning "unknown resource type"). - **feat(cli):** Add `kind: Credential` support to `acpctl apply`. - **feat(cli):** Add `-o json` to `acpctl agent start`. - **feat(cli):** Add `demo-github.sh` — end-to-end GitHub credential demo script alongside `demo-kind.sh`. ## Test plan - [ ] Start a session with a credential bound to an agent — should no longer fail with `clusterroles.rbac.authorization.k8s.io "credential:token-reader" not found` - [ ] Delete a project — namespace should be deprovisioned (previously retained indefinitely) - [ ] `acpctl get credentials` / `acpctl describe credential <id>` / `acpctl delete credential <id>` work as generic verbs - [ ] `acpctl apply` with a `kind: Credential` YAML creates/patches the credential - [ ] `acpctl agent start <agent> -o json` returns JSON session object - [ ] `./components/ambient-cli/demo-github.sh` runs end-to-end with a GitHub PAT 🤖 Generated with [Claude Code](https://claude.ai/code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Added JSON output format (`--output/-o`) for agent start command * Added credential management: create, list, view, and delete credentials via CLI * Included demo script for GitHub credential workflow * **Improvements** * Projects now automatically clean up their namespaces upon deletion <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Ambient Code Bot <bot@ambient-code.local> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 630e8d7 commit 8ad69d4

8 files changed

Lines changed: 573 additions & 55 deletions

File tree

components/ambient-cli/cmd/acpctl/agent/cmd.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,8 +349,9 @@ var deleteCmd = &cobra.Command{
349349
}
350350

351351
var agentStartArgs struct {
352-
projectID string
353-
prompt string
352+
projectID string
353+
prompt string
354+
outputFormat string
354355
}
355356

356357
var agentStartCmd = &cobra.Command{
@@ -389,6 +390,13 @@ var agentStartCmd = &cobra.Command{
389390
return fmt.Errorf("start agent: %w", err)
390391
}
391392

393+
if agentStartArgs.outputFormat == "json" {
394+
printer := output.NewPrinter(output.FormatJSON, cmd.OutOrStdout())
395+
if resp.Session != nil {
396+
return printer.PrintJSON(resp.Session)
397+
}
398+
return printer.PrintJSON(resp)
399+
}
392400
if resp.Session != nil {
393401
fmt.Fprintf(cmd.OutOrStdout(), "session/%s started (phase: %s)\n", resp.Session.ID, resp.Session.Phase)
394402
} else {
@@ -533,6 +541,7 @@ func init() {
533541

534542
agentStartCmd.Flags().StringVar(&agentStartArgs.projectID, "project-id", "", "Project ID (defaults to configured project)")
535543
agentStartCmd.Flags().StringVar(&agentStartArgs.prompt, "prompt", "", "Task prompt for this run")
544+
agentStartCmd.Flags().StringVarP(&agentStartArgs.outputFormat, "output", "o", "", "Output format: json")
536545

537546
ignitionCmd.Flags().StringVar(&ignitionArgs.projectID, "project-id", "", "Project ID (defaults to configured project)")
538547

components/ambient-cli/cmd/acpctl/apply/cmd.go

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ import (
2222

2323
var Cmd = &cobra.Command{
2424
Use: "apply",
25-
Short: "Apply declarative Project and Agent manifests",
26-
Long: `Apply Projects and Agents from YAML files or a Kustomize directory.
25+
Short: "Apply declarative Project, Agent, and Credential manifests",
26+
Long: `Apply Projects, Agents, and Credentials from YAML files or a Kustomize directory.
2727
2828
Mirrors kubectl apply semantics: resources are created if they do not exist,
2929
or patched if they do. Output reports created / configured / unchanged per resource.
3030
31-
Supported kinds: Project, Agent
31+
Supported kinds: Project, Agent, Credential
3232
3333
File format (one or more documents separated by ---):
3434
@@ -64,6 +64,16 @@ Examples:
6464
acpctl apply -k .ambient/teams/overlays/dev/
6565
acpctl apply -k .ambient/teams/overlays/prod/ --dry-run
6666
cat lead.yaml | acpctl apply -f -
67+
68+
Credential example:
69+
70+
kind: Credential
71+
name: my-gitlab-pat
72+
provider: gitlab
73+
token: $GITLAB_PAT
74+
url: https://gitlab.myco.com
75+
labels:
76+
team: platform
6777
`,
6878
RunE: run,
6979
}
@@ -93,6 +103,10 @@ type resource struct {
93103
Labels map[string]string `yaml:"labels"`
94104
Annotations map[string]string `yaml:"annotations"`
95105
Inbox []inboxSeed `yaml:"inbox"`
106+
Provider string `yaml:"provider"`
107+
Token string `yaml:"token"`
108+
URL string `yaml:"url"`
109+
Email string `yaml:"email"`
96110
}
97111

98112
type inboxSeed struct {
@@ -161,6 +175,8 @@ func run(cmd *cobra.Command, _ []string) error {
161175
result, err = applyProject(ctx, client, doc)
162176
case "agent":
163177
result, err = applyAgent(ctx, client, doc, projectName, factory)
178+
case "credential":
179+
result, err = applyCredential(ctx, client, doc)
164180
default:
165181
fmt.Fprintf(cmd.ErrOrStderr(), "warning: unknown kind %q — skipping\n", doc.Kind)
166182
continue
@@ -226,6 +242,82 @@ func applyProject(ctx context.Context, client *sdkclient.Client, doc resource) (
226242
return applyResult{Kind: "Project", Name: doc.Name, Status: "configured"}, nil
227243
}
228244

245+
func applyCredential(ctx context.Context, client *sdkclient.Client, doc resource) (applyResult, error) {
246+
existing, err := client.Credentials().Get(ctx, doc.Name)
247+
if err != nil {
248+
token := os.ExpandEnv(doc.Token)
249+
builder := sdktypes.NewCredentialBuilder().
250+
Name(doc.Name).
251+
Provider(doc.Provider)
252+
if token != "" {
253+
builder = builder.Token(token)
254+
}
255+
if doc.Description != "" {
256+
builder = builder.Description(doc.Description)
257+
}
258+
if doc.URL != "" {
259+
builder = builder.Url(doc.URL)
260+
}
261+
if doc.Email != "" {
262+
builder = builder.Email(doc.Email)
263+
}
264+
if len(doc.Labels) > 0 {
265+
builder = builder.Labels(marshalStringMap(doc.Labels))
266+
}
267+
if len(doc.Annotations) > 0 {
268+
builder = builder.Annotations(marshalStringMap(doc.Annotations))
269+
}
270+
cred, buildErr := builder.Build()
271+
if buildErr != nil {
272+
return applyResult{}, buildErr
273+
}
274+
if _, createErr := client.Credentials().Create(ctx, cred); createErr != nil {
275+
return applyResult{}, createErr
276+
}
277+
return applyResult{Kind: "Credential", Name: doc.Name, Status: "created"}, nil
278+
}
279+
280+
patch, changed := buildCredentialPatch(existing, doc)
281+
if !changed {
282+
return applyResult{Kind: "Credential", Name: doc.Name, Status: "unchanged"}, nil
283+
}
284+
if _, err = client.Credentials().Update(ctx, existing.ID, patch); err != nil {
285+
return applyResult{}, err
286+
}
287+
return applyResult{Kind: "Credential", Name: doc.Name, Status: "configured"}, nil
288+
}
289+
290+
func buildCredentialPatch(existing *sdktypes.Credential, doc resource) (map[string]any, bool) {
291+
changed := false
292+
patch := sdktypes.NewCredentialPatchBuilder()
293+
if doc.Description != "" && doc.Description != existing.Description {
294+
patch = patch.Description(doc.Description)
295+
changed = true
296+
}
297+
if doc.URL != "" {
298+
patch = patch.Url(doc.URL)
299+
changed = true
300+
}
301+
if doc.Email != "" {
302+
patch = patch.Email(doc.Email)
303+
changed = true
304+
}
305+
token := os.ExpandEnv(doc.Token)
306+
if token != "" {
307+
patch = patch.Token(token)
308+
changed = true
309+
}
310+
if len(doc.Labels) > 0 {
311+
patch = patch.Labels(marshalStringMap(doc.Labels))
312+
changed = true
313+
}
314+
if len(doc.Annotations) > 0 {
315+
patch = patch.Annotations(marshalStringMap(doc.Annotations))
316+
changed = true
317+
}
318+
return patch.Build(), changed
319+
}
320+
229321
func marshalStringMap(m map[string]string) string {
230322
if len(m) == 0 {
231323
return ""

components/ambient-cli/cmd/acpctl/delete/cmd.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ Valid resource types:
2626
session (aliases: sess)
2727
agent
2828
role
29-
role-binding (aliases: rolebinding, rb)`,
29+
role-binding (aliases: rolebinding, rb)
30+
credential (aliases: cred)`,
3031
Args: cobra.ExactArgs(2),
3132
RunE: run,
3233
}
@@ -107,7 +108,14 @@ func run(cmd *cobra.Command, cmdArgs []string) error {
107108
fmt.Fprintf(cmd.OutOrStdout(), "role-binding/%s deleted\n", name)
108109
return nil
109110

111+
case "credential", "credentials", "cred", "creds":
112+
if err := client.Credentials().Delete(ctx, name); err != nil {
113+
return fmt.Errorf("delete credential %q: %w", name, err)
114+
}
115+
fmt.Fprintf(cmd.OutOrStdout(), "credential/%s deleted\n", name)
116+
return nil
117+
110118
default:
111-
return fmt.Errorf("unknown or non-deletable resource type: %s\nDeletable types: project, project-settings, session, agent, role, role-binding", cmdArgs[0])
119+
return fmt.Errorf("unknown or non-deletable resource type: %s\nDeletable types: project, project-settings, session, agent, role, role-binding, credential", cmdArgs[0])
112120
}
113121
}

components/ambient-cli/cmd/acpctl/describe/cmd.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ Valid resource types:
2424
user (aliases: usr)
2525
agent (aliases: agents)
2626
role
27-
role-binding (aliases: rb)`,
27+
role-binding (aliases: rb)
28+
credential (aliases: cred)`,
2829
Args: cobra.ExactArgs(2),
2930
RunE: run,
3031
}
@@ -98,7 +99,14 @@ func run(cmd *cobra.Command, cmdArgs []string) error {
9899
}
99100
return printer.PrintJSON(rb)
100101

102+
case "credential", "credentials", "cred", "creds":
103+
cred, err := client.Credentials().Get(ctx, name)
104+
if err != nil {
105+
return fmt.Errorf("describe credential %q: %w", name, err)
106+
}
107+
return printer.PrintJSON(cred)
108+
101109
default:
102-
return fmt.Errorf("unknown resource type: %s\nValid types: session, project, project-settings, user, agent, role, role-binding", cmdArgs[0])
110+
return fmt.Errorf("unknown resource type: %s\nValid types: session, project, project-settings, user, agent, role, role-binding, credential", cmdArgs[0])
103111
}
104112
}

components/ambient-cli/cmd/acpctl/get/cmd.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Valid resource types:
3939
agents (aliases: agent)
4040
roles (aliases: role)
4141
role-bindings (aliases: role-binding, rb)
42+
credentials (aliases: credential, cred)
4243
`,
4344
Args: cobra.RangeArgs(1, 2),
4445
RunE: run,
@@ -135,8 +136,10 @@ func run(cmd *cobra.Command, cmdArgs []string) error {
135136
return getRoles(ctx, client, printer, name)
136137
case "role-bindings":
137138
return getRoleBindings(ctx, client, printer, name)
139+
case "credentials":
140+
return getCredentials(ctx, client, printer, name)
138141
default:
139-
return fmt.Errorf("unknown resource type: %s\nValid types: sessions, projects, project-agents, project-settings, users, agents, roles, role-bindings", cmdArgs[0])
142+
return fmt.Errorf("unknown resource type: %s\nValid types: sessions, projects, project-agents, project-settings, users, agents, roles, role-bindings, credentials", cmdArgs[0])
140143
}
141144
}
142145

@@ -158,6 +161,8 @@ func normalizeResource(r string) string {
158161
return "roles"
159162
case "role-binding", "role-bindings", "rolebinding", "rolebindings", "rb":
160163
return "role-bindings"
164+
case "credential", "credentials", "cred", "creds":
165+
return "credentials"
161166
default:
162167
return r
163168
}
@@ -510,6 +515,48 @@ func getRoleBindings(ctx context.Context, client *sdkclient.Client, printer *out
510515
return printRoleBindingTable(printer, list.Items)
511516
}
512517

518+
func getCredentials(ctx context.Context, client *sdkclient.Client, printer *output.Printer, name string) error {
519+
if name != "" {
520+
cred, err := client.Credentials().Get(ctx, name)
521+
if err != nil {
522+
return fmt.Errorf("get credential %q: %w", name, err)
523+
}
524+
if printer.Format() == output.FormatJSON {
525+
return printer.PrintJSON(cred)
526+
}
527+
return printCredentialTable(printer, []sdktypes.Credential{*cred})
528+
}
529+
opts := sdktypes.NewListOptions().Size(args.limit).Build()
530+
list, err := client.Credentials().List(ctx, opts)
531+
if err != nil {
532+
return fmt.Errorf("list credentials: %w", err)
533+
}
534+
if printer.Format() == output.FormatJSON {
535+
return printer.PrintJSON(list)
536+
}
537+
return printCredentialTable(printer, list.Items)
538+
}
539+
540+
func printCredentialTable(printer *output.Printer, credentials []sdktypes.Credential) error {
541+
columns := []output.Column{
542+
{Name: "ID", Width: 27},
543+
{Name: "NAME", Width: 24},
544+
{Name: "PROVIDER", Width: 12},
545+
{Name: "DESCRIPTION", Width: 32},
546+
{Name: "AGE", Width: 10},
547+
}
548+
table := output.NewTable(printer.Writer(), columns)
549+
table.WriteHeaders()
550+
for _, c := range credentials {
551+
age := ""
552+
if c.CreatedAt != nil {
553+
age = output.FormatAge(time.Since(*c.CreatedAt))
554+
}
555+
table.WriteRow(c.ID, c.Name, c.Provider, c.Description, age)
556+
}
557+
return nil
558+
}
559+
513560
func printRoleBindingTable(printer *output.Printer, rbs []sdktypes.RoleBinding) error {
514561
columns := []output.Column{
515562
{Name: "ID", Width: 27},

0 commit comments

Comments
 (0)