@@ -594,6 +594,13 @@ enum Commands {
594594 command : Option < ProfileCommands > ,
595595 } ,
596596
597+ /// Manage delegated identity credential records.
598+ #[ command( help_template = SUBCOMMAND_HELP_TEMPLATE ) ]
599+ DelegatedCredential {
600+ #[ command( subcommand) ]
601+ command : Option < DelegatedCredentialCommands > ,
602+ } ,
603+
597604 /// Manage workspaces.
598605 #[ command( alias = "ws" , after_help = WORKSPACE_EXAMPLES , help_template = SUBCOMMAND_HELP_TEMPLATE ) ]
599606 Workspace {
@@ -1288,6 +1295,62 @@ impl ProfileCommands {
12881295 }
12891296}
12901297
1298+ #[ derive( Subcommand , Debug ) ]
1299+ enum DelegatedCredentialCommands {
1300+ /// List delegated identity credentials.
1301+ #[ command( help_template = LEAF_HELP_TEMPLATE , next_help_heading = "FLAGS" ) ]
1302+ List {
1303+ /// Maximum number of credentials to return in this page.
1304+ #[ arg( long, default_value_t = 100 ) ]
1305+ page_size : i32 ,
1306+
1307+ /// Opaque continuation token from a previous page.
1308+ #[ arg( long, default_value = "" ) ]
1309+ page_token : String ,
1310+
1311+ /// Output only credential IDs.
1312+ #[ arg( long, conflicts_with = "output" ) ]
1313+ ids : bool ,
1314+
1315+ /// Output format.
1316+ #[ arg( short = 'o' , long = "output" , value_enum, default_value_t = OutputFormat :: Table , conflicts_with = "ids" ) ]
1317+ output : OutputFormat ,
1318+ } ,
1319+
1320+ /// Show delegated identity credential status.
1321+ #[ command( help_template = LEAF_HELP_TEMPLATE , next_help_heading = "FLAGS" ) ]
1322+ Status {
1323+ /// Delegated identity credential ID.
1324+ id : String ,
1325+
1326+ /// Output format.
1327+ #[ arg( short = 'o' , long = "output" , value_enum, default_value_t = OutputFormat :: Table ) ]
1328+ output : OutputFormat ,
1329+ } ,
1330+
1331+ /// Revoke a delegated identity credential.
1332+ #[ command( help_template = LEAF_HELP_TEMPLATE , next_help_heading = "FLAGS" ) ]
1333+ Revoke {
1334+ /// Delegated identity credential ID.
1335+ id : String ,
1336+
1337+ /// Expected resource version for compare-and-swap updates.
1338+ #[ arg( long = "resource-version" , default_value_t = 0 ) ]
1339+ resource_version : u64 ,
1340+ } ,
1341+
1342+ /// Delete a delegated identity credential record.
1343+ #[ command( help_template = LEAF_HELP_TEMPLATE , next_help_heading = "FLAGS" ) ]
1344+ Delete {
1345+ /// Delegated identity credential ID.
1346+ id : String ,
1347+
1348+ /// Expected resource version for compare-and-swap deletes.
1349+ #[ arg( long = "resource-version" , default_value_t = 0 ) ]
1350+ resource_version : u64 ,
1351+ } ,
1352+ }
1353+
12911354// -----------------------------------------------------------------------
12921355// Gateway commands (replaces the old `cluster` / `cluster admin` groups)
12931356// -----------------------------------------------------------------------
@@ -1528,6 +1591,12 @@ enum SandboxCommands {
15281591 #[ arg( long = "provider" ) ]
15291592 providers : Vec < String > ,
15301593
1594+ /// Allow the sandbox to use a separately authorized copy of your identity for a bounded duration.
1595+ /// Opens a browser authorization flow when no usable gateway-owned grant exists. Accepts
1596+ /// positive durations with m, h, or d suffixes, for example 30m, 8h, or 7d.
1597+ #[ arg( long = "use-my-identity-for" , value_name = "DURATION" ) ]
1598+ delegate_identity_for : Option < String > ,
1599+
15311600 /// Path to a custom sandbox policy YAML file.
15321601 /// Overrides the built-in default and the `OPENSHELL_SANDBOX_POLICY` env var.
15331602 #[ arg( long, value_hint = ValueHint :: FilePath ) ]
@@ -1845,6 +1914,41 @@ enum SandboxCommands {
18451914 /// Manage reusable sandbox workload templates.
18461915 #[ command( subcommand) ]
18471916 Template ( SandboxTemplateCommands ) ,
1917+
1918+ /// Manage sandbox delegated identity.
1919+ #[ command( subcommand, name = "delegated-identity" ) ]
1920+ DelegatedIdentity ( SandboxDelegatedIdentityCommands ) ,
1921+ }
1922+
1923+ #[ derive( Subcommand , Debug ) ]
1924+ enum SandboxDelegatedIdentityCommands {
1925+ /// Show delegated identity status for a sandbox.
1926+ #[ command( help_template = LEAF_HELP_TEMPLATE , next_help_heading = "FLAGS" ) ]
1927+ Status {
1928+ /// Sandbox name.
1929+ #[ arg( add = ArgValueCompleter :: new( completers:: complete_sandbox_names) ) ]
1930+ name : String ,
1931+ } ,
1932+
1933+ /// Withdraw delegated identity from a sandbox.
1934+ #[ command( help_template = LEAF_HELP_TEMPLATE , next_help_heading = "FLAGS" ) ]
1935+ Withdraw {
1936+ /// Sandbox name.
1937+ #[ arg( add = ArgValueCompleter :: new( completers:: complete_sandbox_names) ) ]
1938+ name : String ,
1939+ } ,
1940+
1941+ /// Set delegated identity expiry to now plus the requested duration.
1942+ #[ command( help_template = LEAF_HELP_TEMPLATE , next_help_heading = "FLAGS" ) ]
1943+ Extend {
1944+ /// Sandbox name.
1945+ #[ arg( add = ArgValueCompleter :: new( completers:: complete_sandbox_names) ) ]
1946+ name : String ,
1947+
1948+ /// New authorization window, for example 24h.
1949+ #[ arg( long = "for" , value_name = "DURATION" ) ]
1950+ duration : String ,
1951+ } ,
18481952}
18491953
18501954/// Common observation flags; the deadline starts after the mutation is saved.
@@ -3367,6 +3471,7 @@ async fn run_async() -> Result<()> {
33673471 memory,
33683472 driver_config_json,
33693473 providers,
3474+ delegate_identity_for,
33703475 policy,
33713476 forward,
33723477 expose,
@@ -3463,6 +3568,7 @@ async fn run_async() -> Result<()> {
34633568 driver_config_json : driver_config_json. as_deref ( ) ,
34643569 editor,
34653570 providers : & providers,
3571+ delegate_identity_for : delegate_identity_for. as_deref ( ) ,
34663572 policy : policy. as_deref ( ) ,
34673573 forward,
34683574 expose,
@@ -3813,6 +3919,37 @@ async fn run_async() -> Result<()> {
38133919 . await ?;
38143920 }
38153921 } ,
3922+ SandboxCommands :: DelegatedIdentity ( command) => match command {
3923+ SandboxDelegatedIdentityCommands :: Status { name } => {
3924+ run:: sandbox_delegated_identity_status (
3925+ endpoint,
3926+ & name,
3927+ & cli. workspace ,
3928+ & tls,
3929+ )
3930+ . await ?;
3931+ }
3932+ SandboxDelegatedIdentityCommands :: Withdraw { name } => {
3933+ run:: sandbox_delegated_identity_withdraw (
3934+ endpoint,
3935+ & name,
3936+ & cli. workspace ,
3937+ & tls,
3938+ )
3939+ . await ?;
3940+ }
3941+ SandboxDelegatedIdentityCommands :: Extend { name, duration } => {
3942+ run:: sandbox_delegated_identity_extend (
3943+ endpoint,
3944+ & ctx. name ,
3945+ & name,
3946+ & duration,
3947+ & cli. workspace ,
3948+ & tls,
3949+ )
3950+ . await ?;
3951+ }
3952+ } ,
38163953 }
38173954 }
38183955 }
@@ -4075,6 +4212,61 @@ async fn run_async() -> Result<()> {
40754212 apply_auth ( & mut tls, & ctx. name ) ?;
40764213 command. run ( endpoint, & cli. workspace , & tls) . await ?;
40774214 }
4215+ Some ( Commands :: DelegatedCredential {
4216+ command : Some ( command) ,
4217+ } ) => {
4218+ let ctx = resolve_gateway ( & cli. gateway , & cli. gateway_endpoint ) ?;
4219+ let endpoint = & ctx. endpoint ;
4220+ let mut tls = tls. with_gateway_name ( & ctx. name ) ;
4221+ apply_auth ( & mut tls, & ctx. name ) ?;
4222+
4223+ match command {
4224+ DelegatedCredentialCommands :: List {
4225+ page_size,
4226+ page_token,
4227+ ids,
4228+ output,
4229+ } => {
4230+ run:: delegated_identity_credential_list (
4231+ endpoint,
4232+ page_size,
4233+ & page_token,
4234+ ids,
4235+ output. as_str ( ) ,
4236+ & tls,
4237+ )
4238+ . await ?;
4239+ }
4240+ DelegatedCredentialCommands :: Status { id, output } => {
4241+ run:: delegated_identity_credential_status ( endpoint, & id, output. as_str ( ) , & tls)
4242+ . await ?;
4243+ }
4244+ DelegatedCredentialCommands :: Revoke {
4245+ id,
4246+ resource_version,
4247+ } => {
4248+ run:: delegated_identity_credential_revoke (
4249+ endpoint,
4250+ & id,
4251+ resource_version,
4252+ & tls,
4253+ )
4254+ . await ?;
4255+ }
4256+ DelegatedCredentialCommands :: Delete {
4257+ id,
4258+ resource_version,
4259+ } => {
4260+ run:: delegated_identity_credential_delete (
4261+ endpoint,
4262+ & id,
4263+ resource_version,
4264+ & tls,
4265+ )
4266+ . await ?;
4267+ }
4268+ }
4269+ }
40784270 Some ( Commands :: Term { theme } ) => {
40794271 let ctx = resolve_gateway ( & cli. gateway , & cli. gateway_endpoint ) ?;
40804272 let mut tls = tls. with_gateway_name ( & ctx. name ) ;
@@ -4214,6 +4406,13 @@ async fn run_async() -> Result<()> {
42144406 . print_help ( )
42154407 . expect ( "Failed to print help" ) ;
42164408 }
4409+ Some ( Commands :: DelegatedCredential { command : None } ) => {
4410+ Cli :: command ( )
4411+ . find_subcommand_mut ( "delegated-credential" )
4412+ . expect ( "delegated-credential subcommand exists" )
4413+ . print_help ( )
4414+ . expect ( "Failed to print help" ) ;
4415+ }
42174416 Some ( Commands :: Gateway { command : None } ) => {
42184417 Cli :: command ( )
42194418 . find_subcommand_mut ( "gateway" )
@@ -5170,6 +5369,76 @@ mod tests {
51705369 } ) if ids == [ "custom-api" , "custom-alt" ] ) ) ;
51715370 }
51725371
5372+ #[ test]
5373+ fn delegated_credential_commands_parse ( ) {
5374+ let list = Cli :: try_parse_from ( [
5375+ "openshell" ,
5376+ "delegated-credential" ,
5377+ "list" ,
5378+ "--page-size" ,
5379+ "25" ,
5380+ "--page-token" ,
5381+ "next-credential-page" ,
5382+ "--ids" ,
5383+ ] )
5384+ . expect ( "delegated credential list should parse" ) ;
5385+ match list. command {
5386+ Some ( Commands :: DelegatedCredential {
5387+ command :
5388+ Some ( DelegatedCredentialCommands :: List {
5389+ page_size,
5390+ page_token,
5391+ ids,
5392+ output,
5393+ } ) ,
5394+ } ) => {
5395+ assert_eq ! ( page_size, 25 ) ;
5396+ assert_eq ! ( page_token, "next-credential-page" ) ;
5397+ assert ! ( ids) ;
5398+ assert_eq ! ( output, OutputFormat :: Table ) ;
5399+ }
5400+ command => panic ! ( "unexpected delegated credential list command: {command:?}" ) ,
5401+ }
5402+
5403+ let status = Cli :: try_parse_from ( [
5404+ "openshell" ,
5405+ "delegated-credential" ,
5406+ "status" ,
5407+ "delegated-identity-123" ,
5408+ "-o" ,
5409+ "json" ,
5410+ ] )
5411+ . expect ( "delegated credential status should parse" ) ;
5412+ assert ! ( matches!(
5413+ status. command,
5414+ Some ( Commands :: DelegatedCredential {
5415+ command: Some ( DelegatedCredentialCommands :: Status {
5416+ id,
5417+ output: OutputFormat :: Json ,
5418+ } )
5419+ } ) if id == "delegated-identity-123"
5420+ ) ) ;
5421+
5422+ let revoke = Cli :: try_parse_from ( [
5423+ "openshell" ,
5424+ "delegated-credential" ,
5425+ "revoke" ,
5426+ "delegated-identity-123" ,
5427+ "--resource-version" ,
5428+ "7" ,
5429+ ] )
5430+ . expect ( "delegated credential revoke should parse" ) ;
5431+ assert ! ( matches!(
5432+ revoke. command,
5433+ Some ( Commands :: DelegatedCredential {
5434+ command: Some ( DelegatedCredentialCommands :: Revoke {
5435+ id,
5436+ resource_version: 7 ,
5437+ } )
5438+ } ) if id == "delegated-identity-123"
5439+ ) ) ;
5440+ }
5441+
51735442 #[ test]
51745443 fn profile_import_and_lint_require_exactly_one_source ( ) {
51755444 for verb in [ "import" , "lint" ] {
@@ -6363,6 +6632,31 @@ mod tests {
63636632 }
63646633 }
63656634
6635+ #[ test]
6636+ fn sandbox_create_use_my_identity_for_parses ( ) {
6637+ let cli = Cli :: try_parse_from ( [
6638+ "openshell" ,
6639+ "sandbox" ,
6640+ "create" ,
6641+ "--use-my-identity-for" ,
6642+ "8h" ,
6643+ ] )
6644+ . expect ( "sandbox create --use-my-identity-for should parse" ) ;
6645+ match cli. command {
6646+ Some ( Commands :: Sandbox {
6647+ command :
6648+ Some ( SandboxCommands :: Create {
6649+ delegate_identity_for,
6650+ ..
6651+ } ) ,
6652+ ..
6653+ } ) => {
6654+ assert_eq ! ( delegate_identity_for. as_deref( ) , Some ( "8h" ) ) ;
6655+ }
6656+ other => panic ! ( "expected SandboxCommands::Create, got: {other:?}" ) ,
6657+ }
6658+ }
6659+
63666660 /// `--approval-mode <bogus>` is rejected by clap's value parser, so the
63676661 /// CLI can't smuggle through a future-mode value that the gateway
63686662 /// doesn't yet know about.
0 commit comments