@@ -43,6 +43,20 @@ type sandboxDTO struct {
4343 Workdir string `json:"workdir,omitempty"`
4444}
4545
46+ type policyDTO struct {
47+ Name string `json:"name"`
48+ Label string `json:"label"`
49+ Description string `json:"description"`
50+ CapabilityProfile string `json:"capability_profile"`
51+ SidecarRequired bool `json:"sidecar_required"`
52+ IsDefault bool `json:"is_default"`
53+ Selectable bool `json:"selectable"`
54+ AssignmentSource string `json:"assignment_source"`
55+ NetworkEgressFQDNs []string `json:"network_egress_fqdns,omitempty"`
56+ CreatedAt time.Time `json:"created_at,omitzero"`
57+ UpdatedAt time.Time `json:"updated_at,omitzero"`
58+ }
59+
4660// fallbackWorkdir is the path the MCP/CLI assume when a sandbox DTO
4761// arrives without a workdir field (older sandboxd before the workdir
4862// contract shipped).
@@ -94,13 +108,14 @@ func newCellaCmd() *cobra.Command {
94108 cmd := & cobra.Command {
95109 Use : "cella" ,
96110 Aliases : []string {"sandbox" },
97- Short : "Manage cellas (create, list, rename , start, stop, delete, run)." ,
111+ Short : "Manage cellas (create, list, policy , start, stop, delete, run)." ,
98112 Long : `Manage Cella sandboxes — per-user compute environments at cella.latere.ai.
99113
100114Each cella is a PVC-backed workspace plus a Pod for compute. Tier
101115'ephemeral' auto-stops on idle and auto-deletes after a wall-clock
102116window; tier 'persistent' stays until you delete it.` ,
103117 Example : ` latere cella list
118+ latere cella policy list
104119 latere cella create --name dev --tier persistent --disk 10
105120 latere cella shell dev
106121 latere cella run dev -- python train.py
@@ -114,6 +129,7 @@ window; tier 'persistent' stays until you delete it.`,
114129 newCeStartCmd (),
115130 newCeStopCmd (),
116131 newCeDeleteCmd (),
132+ newCePolicyCmd (),
117133 newCeExecCmd (),
118134 newCeShellCmd (),
119135 newCeRunCmd (),
@@ -185,6 +201,7 @@ image and account defaults for disk, CPU, memory, idle timeout, and
185201policy. Use --tier persistent for a workspace that survives until
186202explicitly deleted.` ,
187203 Example : ` latere cella create
204+ latere cella policy list
188205 latere cella create --name dev --tier persistent --disk 10
189206 latere cella create --name gpu-test --cpu 2 --memory 8Gi
190207 latere cella create --name app --env NODE_ENV=development --credential github
@@ -301,6 +318,95 @@ cellas returned by the backend, including warm-pool cellas.`,
301318 return cmd
302319}
303320
321+ func newCePolicyCmd () * cobra.Command {
322+ var (
323+ apiURL string
324+ jsonF bool
325+ )
326+ cmd := & cobra.Command {
327+ Use : "policy" ,
328+ Aliases : []string {"policies" },
329+ Short : "List policy profiles available for new cellas." ,
330+ Long : `List Cella policy profiles visible to the current token.
331+
332+ Policies control runtime capabilities such as network shape, workspace
333+ layout, and whether Cella's credential sidecar is required. The default
334+ policy is used when 'latere cella create' is run without --policy.
335+
336+ Use a selectable policy with:
337+
338+ latere cella create --policy <name>
339+
340+ If create fails because the selected policy requires the sidecar, list
341+ policies and choose a selectable policy where SIDECAR is "no", or ask
342+ an admin to configure the sidecar client for your token.` ,
343+ Example : ` latere cella policy
344+ latere cella policy list
345+ latere cella policies --json
346+ latere cella create --policy restricted-network` ,
347+ RunE : func (cmd * cobra.Command , args []string ) error {
348+ return runPolicyList (cmd .Context (), apiURL , jsonF )
349+ },
350+ }
351+ f := cmd .Flags ()
352+ f .StringVar (& apiURL , "api-url" , "" , "override Cella API base URL" )
353+ f .BoolVar (& jsonF , "json" , false , "JSON output" )
354+
355+ list := & cobra.Command {
356+ Use : "list" ,
357+ Short : "List policy profiles available for new cellas." ,
358+ Long : cmd .Long ,
359+ Example : ` latere cella policy list
360+ latere cella policy list --json
361+ latere cella create --policy restricted-network` ,
362+ RunE : func (cmd * cobra.Command , args []string ) error {
363+ return runPolicyList (cmd .Context (), apiURL , jsonF )
364+ },
365+ }
366+ list .Flags ().StringVar (& apiURL , "api-url" , "" , "override Cella API base URL" )
367+ list .Flags ().BoolVar (& jsonF , "json" , false , "JSON output" )
368+ cmd .AddCommand (list )
369+ return cmd
370+ }
371+
372+ func runPolicyList (ctx context.Context , apiURL string , jsonF bool ) error {
373+ c , err := authedClient (apiURL )
374+ if err != nil {
375+ return err
376+ }
377+ var policies []policyDTO
378+ if err := c .GetJSON (ctx , "/v1/policies" , & policies ); err != nil {
379+ return err
380+ }
381+ if jsonF {
382+ return printJSON (policies )
383+ }
384+ if len (policies ) == 0 {
385+ fmt .Fprintln (os .Stdout , "No policy profiles are visible to this token." )
386+ fmt .Fprintln (os .Stdout , "Ask your Latere admin to assign a selectable policy, then retry `latere cella create --policy <name>`." )
387+ return nil
388+ }
389+ printPolicies (policies )
390+ return nil
391+ }
392+
393+ func printPolicies (policies []policyDTO ) {
394+ tw := tabwriter .NewWriter (os .Stdout , 0 , 2 , 2 , ' ' , 0 )
395+ _ , _ = fmt .Fprintln (tw , "NAME\t DEFAULT\t SELECTABLE\t SIDECAR\t CAPABILITY\t SOURCE\t DESCRIPTION" )
396+ for _ , p := range policies {
397+ _ , _ = fmt .Fprintf (tw , "%s\t %s\t %s\t %s\t %s\t %s\t %s\n " ,
398+ p .Name ,
399+ yesNo (p .IsDefault ),
400+ yesNo (p .Selectable ),
401+ yesNo (p .SidecarRequired ),
402+ defaultStr (p .CapabilityProfile , "-" ),
403+ defaultStr (p .AssignmentSource , "-" ),
404+ oneLine (defaultStr (p .Description , p .Label )),
405+ )
406+ }
407+ _ = tw .Flush ()
408+ }
409+
304410func newCeGetCmd () * cobra.Command {
305411 var apiURL string
306412 cmd := & cobra.Command {
@@ -1447,6 +1553,17 @@ func defaultStr(s, fallback string) string {
14471553 return s
14481554}
14491555
1556+ func yesNo (v bool ) string {
1557+ if v {
1558+ return "yes"
1559+ }
1560+ return "no"
1561+ }
1562+
1563+ func oneLine (s string ) string {
1564+ return strings .Join (strings .Fields (s ), " " )
1565+ }
1566+
14501567func humanAge (t time.Time ) string {
14511568 if t .IsZero () {
14521569 return "-"
0 commit comments