From 334e0f0d232c2baa138fefe272ba266d5b6e4348 Mon Sep 17 00:00:00 2001 From: mohiit1502 Date: Fri, 21 Aug 2026 18:45:09 +0530 Subject: [PATCH] =?UTF-8?q?feat:=20harden=20IAM=20config=20=E2=80=94=20har?= =?UTF-8?q?dcode=20bundle=20constants,=20simplify=20login=20UX?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IAM issuer, client ID, and loopback redirect URI are now bundle constants. Users no longer configure IAM — `agos login` works with zero setup. `agos config` now only exposes AGOS_CLI_API_URL (optional, for local dev). Removed --issuer / --client-id / --host / --port from login help output. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- cli/agos.py | 126 ++++++++++++++++++++------------------------------ cli/manual.py | 18 +++++--- 2 files changed, 60 insertions(+), 84 deletions(-) diff --git a/cli/agos.py b/cli/agos.py index 092d935..ab044f6 100644 --- a/cli/agos.py +++ b/cli/agos.py @@ -64,20 +64,22 @@ _AUTH_DIR = Path.home() / '.agos' _AUTH_FILE = _AUTH_DIR / 'auth.json' _CONFIG_FILE = _AUTH_DIR / 'config.json' -# Keys the CLI recognizes and their descriptions +# ── Hardcoded bundle constants ───────────────────────────────────────────────── +# IAM and client details are fixed for the CLI bundle — users never configure +# these. The loopback redirect URI is the RFC 8252 standard for native/CLI apps. +_BUNDLE_IAM_ISSUER = 'https://iam.armco.dev' +_BUNDLE_IAM_CLIENT_ID = 'client_60250edc6189418ab3d7ee4d1577c0e7' +_BUNDLE_IAM_CALLBACK_HOST = '127.0.0.1' +_BUNDLE_IAM_CALLBACK_PORT = 8976 +_BUNDLE_IAM_REDIRECT_URI = f'http://{_BUNDLE_IAM_CALLBACK_HOST}:{_BUNDLE_IAM_CALLBACK_PORT}/callback' +_BUNDLE_API_URL_PROD = 'https://agos.armco.dev' +_BUNDLE_API_URL_LOCAL = 'http://localhost:2000' +# ────────────────────────────────────────────────────────────────────────────── + +# Keys the user can configure (IAM is not user-configurable — it's baked in) _CONFIG_SCHEMA: dict[str, dict] = { - 'AGOS_CLI_IAM_ISSUER': { - 'description': 'Armco IAM issuer URL (e.g. https://iam.armco.dev)', - 'required': True, - 'env_aliases': ['VITE_IAM_ISSUER', 'IAM_ISSUER'], - }, - 'AGOS_CLI_IAM_CLIENT_ID': { - 'description': 'OAuth client ID for the native/CLI app', - 'required': True, - 'env_aliases': ['VITE_IAM_DESKTOP_CLIENT_ID', 'VITE_IAM_CLIENT_ID'], - }, 'AGOS_CLI_API_URL': { - 'description': 'AGOS API base URL (default: http://localhost:2000)', + 'description': f'AGOS API base URL (prod: {_BUNDLE_API_URL_PROD} | local dev: {_BUNDLE_API_URL_LOCAL})', 'required': False, 'env_aliases': ['AGOS_API_URL', 'VITE_API_URL'], }, @@ -111,6 +113,11 @@ def _config_effective_value(key: str) -> str | None: return None +def _resolve_api_url_from_config() -> str: + """Return the API URL: stored config > env alias > prod default.""" + return _config_effective_value('AGOS_CLI_API_URL') or _BUNDLE_API_URL_PROD + + def _utc_now_iso() -> str: return datetime.now(timezone.utc).isoformat() @@ -307,21 +314,6 @@ def cli(): @cli.command() -@click.option('--issuer', default=None, help='Armco IAM issuer URL') -@click.option('--client-id', default=None, help='IAM OAuth client ID for the CLI/native app') -@click.option( - '--host', - default='127.0.0.1', - show_default=True, - help='Loopback host for the local callback listener', -) -@click.option( - '--port', - default=8976, - show_default=True, - type=int, - help='Loopback port for the local callback listener', -) @click.option( '--timeout', 'timeout_seconds', @@ -330,32 +322,21 @@ def cli(): type=int, help='Seconds to wait for the browser login callback', ) -@click.option('--scopes', default=None, help='Space or comma separated OAuth scopes') -def login(issuer, client_id, host, port, timeout_seconds, scopes): - """Login with Armco IAM for Agos CLI.""" +# Hidden dev-only overrides — not shown in help +@click.option('--issuer', default=None, hidden=True) +@click.option('--client-id', default=None, hidden=True) +@click.option('--host', default=None, hidden=True) +@click.option('--port', default=None, type=int, hidden=True) +def login(timeout_seconds, issuer, client_id, host, port): + """Login to Agos via Armco IAM (opens browser for authentication).""" correlation_id = f'cli_login_{uuid.uuid4().hex[:12]}' - resolved_issuer = issuer or _config_effective_value('AGOS_CLI_IAM_ISSUER') or _first_env('VITE_IAM_ISSUER', 'IAM_ISSUER') - resolved_client_id = client_id or _config_effective_value('AGOS_CLI_IAM_CLIENT_ID') or _first_env('VITE_IAM_CLIENT_ID') - resolved_scopes = _parse_scopes(scopes or _first_env('AGOS_CLI_IAM_SCOPES')) - redirect_uri = f'http://{host}:{port}/callback' - - if not resolved_issuer: - raise click.ClickException( - 'Missing IAM issuer. Run `agos config` to set it up, ' - 'or set AGOS_CLI_IAM_ISSUER / VITE_IAM_ISSUER, or pass --issuer.' - ) - - if not resolved_client_id: - raise click.ClickException( - 'Missing CLI IAM client ID. Run `agos config` to set it up, ' - 'or set AGOS_CLI_IAM_CLIENT_ID / VITE_IAM_DESKTOP_CLIENT_ID, or pass --client-id.' - ) - - if os.getenv('AGOS_CLI_IAM_CLIENT_ID', '').strip() == '' and os.getenv('VITE_IAM_CLIENT_ID', '').strip(): - console.print( - '[yellow]Using VITE_IAM_CLIENT_ID for CLI login. ' - 'Ensure IAM allows the loopback redirect URI for this client.[/yellow]' - ) + # Bundle constants — users never need to configure these + resolved_issuer = issuer or _BUNDLE_IAM_ISSUER + resolved_client_id = client_id or _BUNDLE_IAM_CLIENT_ID + resolved_host = host or _BUNDLE_IAM_CALLBACK_HOST + resolved_port = port or _BUNDLE_IAM_CALLBACK_PORT + resolved_scopes = _parse_scopes(_first_env('AGOS_CLI_IAM_SCOPES')) + redirect_uri = f'http://{resolved_host}:{resolved_port}/callback' _emit_log( 'INFO', @@ -376,8 +357,8 @@ def login(issuer, client_id, host, port, timeout_seconds, scopes): code_verifier = _pkce_verifier() code_challenge = _pkce_challenge(code_verifier) callback_event, callback_payload, thread = _start_callback_server( - host=host, - port=port, + host=resolved_host, + port=resolved_port, expected_state=state, timeout_seconds=timeout_seconds, correlation_id=correlation_id, @@ -474,8 +455,9 @@ def login(issuer, client_id, host, port, timeout_seconds, scopes): @click.option('--list', 'list_all', is_flag=True, default=False, help='Show current stored config.') @click.option('--unset', default=None, metavar='KEY', help='Remove a stored config key.') def config_command(assignment, list_all, unset): - """Configure CLI settings (IAM issuer, client ID, API URL, etc.). + """Configure CLI settings (API URL, etc.). + IAM auth details are bundled — you do not need to configure them. Run without arguments for interactive setup. Pass KEY=VALUE pairs to set specific values. Use --list to view current config or --unset KEY to remove a value. @@ -483,8 +465,7 @@ def config_command(assignment, list_all, unset): \b Examples: agos config - agos config AGOS_CLI_IAM_ISSUER=https://iam.armco.dev - agos config AGOS_CLI_IAM_CLIENT_ID=client_abc123 + agos config AGOS_CLI_API_URL=https://agos.armco.dev agos config --list agos config --unset AGOS_CLI_API_URL """ @@ -529,7 +510,7 @@ def config_command(assignment, list_all, unset): for pair in assignment: if '=' not in pair: raise click.ClickException( - f'Invalid format: `{pair}`. Use KEY=VALUE (e.g. AGOS_CLI_IAM_ISSUER=https://iam.armco.dev)' + f'Invalid format: `{pair}`. Use KEY=VALUE (e.g. AGOS_CLI_API_URL=https://agos.armco.dev)' ) key, _, value = pair.partition('=') key = key.strip() @@ -541,22 +522,22 @@ def config_command(assignment, list_all, unset): console.print(f'[green]Set[/green] {key} = {cfg[key]}') return - # Interactive mode — check required keys and prompt for any that are missing - console.print('[bold blue]Agos CLI Configuration Setup[/bold blue]') + # Interactive mode + console.print('[bold blue]Agos CLI Configuration[/bold blue]') console.print( - f'[dim]Settings are saved to {_CONFIG_FILE}[/dim]\n' - '[dim]Press Enter to keep an existing value.[/dim]\n' + f'[dim]Settings saved to {_CONFIG_FILE}[/dim]\n' + f'[dim]IAM auth is pre-configured (issuer: {_BUNDLE_IAM_ISSUER})[/dim]\n' + '[dim]Press Enter to keep an existing value, or clear it to reset to default.[/dim]\n' ) changed = False for key, meta in _CONFIG_SCHEMA.items(): effective = _config_effective_value(key) stored = cfg.get(key, '') - required_label = '[red]*required[/red]' if meta['required'] else '[dim]optional[/dim]' + required_label = '[dim]optional[/dim]' prompt_default = stored or '' - # Show current effective source if not stored directly if effective and not stored: console.print( - f'[dim] {key} is currently satisfied by an environment variable/alias: {effective}[/dim]' + f'[dim] {key} is currently set via environment: {effective}[/dim]' ) display_default = f' [dim](current: {stored})[/dim]' if stored else '' console.print(f'\n[cyan]{key}[/cyan] {required_label}{display_default}') @@ -567,7 +548,6 @@ def config_command(assignment, list_all, unset): cfg[key] = new_value changed = True elif not new_value and stored: - # User cleared the value del cfg[key] changed = True @@ -577,18 +557,10 @@ def config_command(assignment, list_all, unset): else: console.print('\n[dim]No changes.[/dim]') - # Final summary console.print() - all_ok = all( - _config_effective_value(k) - for k, m in _CONFIG_SCHEMA.items() - if m['required'] - ) - if all_ok: - console.print('[bold green]✓ All required settings are configured. Run `agos login` to authenticate.[/bold green]') - else: - missing = [k for k, m in _CONFIG_SCHEMA.items() if m['required'] and not _config_effective_value(k)] - console.print(f'[yellow]⚠ Still missing required keys: {", ".join(missing)}[/yellow]') + api_url = _resolve_api_url_from_config() + console.print(f'[bold green]✓ Ready. API target: {api_url}[/bold green]') + console.print('[dim]Run `agos login` to authenticate.[/dim]') @cli.group() diff --git a/cli/manual.py b/cli/manual.py index a4b95e5..92d123a 100644 --- a/cli/manual.py +++ b/cli/manual.py @@ -28,11 +28,14 @@ _MANUAL: Dict[str, Dict[str, object]] = { ], }, 'login': { - 'summary': 'Authenticate the CLI with Armco IAM and cache tokens in `~/.agos/auth.json`.', + 'summary': ( + 'Authenticate the CLI with Armco IAM via a browser-based PKCE flow. ' + 'IAM provider details are pre-configured — just run `agos login`. ' + 'Tokens are cached in ~/.agos/auth.json.' + ), 'examples': [ 'agos login', - 'agos login --issuer https://iam.armco.dev --client-id client_abc123', - 'agos login --port 8976 --timeout 240', + 'agos login --timeout 240', ], }, 'whoami': { @@ -57,20 +60,21 @@ _MANUAL: Dict[str, Dict[str, object]] = { }, 'config': { 'summary': ( - 'Configure CLI settings (IAM issuer, client ID, API URL) stored in ~/.agos/config.json. ' + 'Configure CLI settings stored in ~/.agos/config.json. ' + 'IAM auth details are pre-configured in the bundle — users only need to ' + 'set AGOS_CLI_API_URL if targeting a non-default API endpoint. ' 'Run without arguments for an interactive guided setup. ' 'Pass KEY=VALUE pairs to set individual keys non-interactively.' ), 'examples': [ 'agos config', - 'agos config AGOS_CLI_IAM_ISSUER=https://iam.armco.dev', - 'agos config AGOS_CLI_IAM_CLIENT_ID=client_abc123', + 'agos config AGOS_CLI_API_URL=https://agos.armco.dev', 'agos config --list', 'agos config --unset AGOS_CLI_API_URL', ], 'notes': [ 'Config is stored at ~/.agos/config.json (mode 0600).', - 'Values set here override .env files but are overridden by explicit --flags.', + 'IAM issuer and client ID are hardcoded in the bundle — do not set them here.', 'After configuring, run `agos login` to authenticate.', ], },