haru-cli 0.1.4__tar.gz → 0.1.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. {haru_cli-0.1.4 → haru_cli-0.1.6}/CHANGELOG.md +48 -0
  2. {haru_cli-0.1.4 → haru_cli-0.1.6}/PKG-INFO +36 -3
  3. {haru_cli-0.1.4 → haru_cli-0.1.6}/README.md +35 -2
  4. {haru_cli-0.1.4 → haru_cli-0.1.6}/docs/configuration.md +9 -0
  5. haru_cli-0.1.6/docs/troubleshooting.md +136 -0
  6. {haru_cli-0.1.4 → haru_cli-0.1.6}/pyproject.toml +16 -3
  7. haru_cli-0.1.6/src/haru/agents/__init__.py +1 -0
  8. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/agents/factory.py +14 -8
  9. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/auth/identity.py +102 -25
  10. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/auth/session.py +81 -42
  11. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/auth/sso.py +6 -3
  12. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/cli.py +10 -1
  13. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/commands/chat.py +9 -1
  14. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/commands/config.py +8 -0
  15. haru_cli-0.1.6/src/haru/commands/doctor.py +161 -0
  16. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/commands/login.py +8 -2
  17. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/commands/run.py +8 -2
  18. haru_cli-0.1.6/src/haru/commands/streaming.py +83 -0
  19. haru_cli-0.1.6/src/haru/diagnostics/__init__.py +6 -0
  20. haru_cli-0.1.6/src/haru/diagnostics/checks.py +336 -0
  21. haru_cli-0.1.6/src/haru/diagnostics/matrix.py +143 -0
  22. haru_cli-0.1.6/src/haru/logging_setup.py +118 -0
  23. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/models/bedrock.py +8 -3
  24. haru_cli-0.1.6/src/haru/models/errors.py +134 -0
  25. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/observability/telemetry.py +2 -2
  26. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/sessions/manager.py +11 -4
  27. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/tools/mcp.py +12 -6
  28. {haru_cli-0.1.4 → haru_cli-0.1.6}/tests/unit/test_agent_factory.py +9 -9
  29. {haru_cli-0.1.4 → haru_cli-0.1.6}/tests/unit/test_bedrock.py +6 -6
  30. haru_cli-0.1.6/tests/unit/test_diagnostics.py +338 -0
  31. haru_cli-0.1.6/tests/unit/test_doctor.py +172 -0
  32. {haru_cli-0.1.4 → haru_cli-0.1.6}/tests/unit/test_guardrails.py +2 -2
  33. {haru_cli-0.1.4 → haru_cli-0.1.6}/tests/unit/test_identity.py +87 -4
  34. haru_cli-0.1.6/tests/unit/test_import_time.py +25 -0
  35. haru_cli-0.1.6/tests/unit/test_logging_setup.py +123 -0
  36. {haru_cli-0.1.4 → haru_cli-0.1.6}/tests/unit/test_mcp.py +7 -7
  37. haru_cli-0.1.6/tests/unit/test_model_errors.py +84 -0
  38. {haru_cli-0.1.4 → haru_cli-0.1.6}/tests/unit/test_session.py +26 -3
  39. {haru_cli-0.1.4 → haru_cli-0.1.6}/tests/unit/test_telemetry.py +5 -5
  40. {haru_cli-0.1.4 → haru_cli-0.1.6}/uv.lock +1 -1
  41. haru_cli-0.1.4/src/haru/agents/__init__.py +0 -18
  42. haru_cli-0.1.4/src/haru/commands/streaming.py +0 -34
  43. {haru_cli-0.1.4 → haru_cli-0.1.6}/.github/workflows/ci.yml +0 -0
  44. {haru_cli-0.1.4 → haru_cli-0.1.6}/.github/workflows/release.yml +0 -0
  45. {haru_cli-0.1.4 → haru_cli-0.1.6}/.gitignore +0 -0
  46. {haru_cli-0.1.4 → haru_cli-0.1.6}/.kiro/steering/coding-standards.md +0 -0
  47. {haru_cli-0.1.4 → haru_cli-0.1.6}/.kiro/steering/git-workflow.md +0 -0
  48. {haru_cli-0.1.4 → haru_cli-0.1.6}/.kiro/steering/product.md +0 -0
  49. {haru_cli-0.1.4 → haru_cli-0.1.6}/.kiro/steering/security.md +0 -0
  50. {haru_cli-0.1.4 → haru_cli-0.1.6}/.kiro/steering/structure.md +0 -0
  51. {haru_cli-0.1.4 → haru_cli-0.1.6}/.kiro/steering/tech.md +0 -0
  52. {haru_cli-0.1.4 → haru_cli-0.1.6}/.kiro/steering/testing.md +0 -0
  53. {haru_cli-0.1.4 → haru_cli-0.1.6}/.pre-commit-config.yaml +0 -0
  54. {haru_cli-0.1.4 → haru_cli-0.1.6}/.python-version +0 -0
  55. {haru_cli-0.1.4 → haru_cli-0.1.6}/CLAUDE.md +0 -0
  56. {haru_cli-0.1.4 → haru_cli-0.1.6}/LICENSE +0 -0
  57. {haru_cli-0.1.4 → haru_cli-0.1.6}/config/agents.yaml +0 -0
  58. {haru_cli-0.1.4 → haru_cli-0.1.6}/config/guardrails.yaml +0 -0
  59. {haru_cli-0.1.4 → haru_cli-0.1.6}/config/haru.yaml +0 -0
  60. {haru_cli-0.1.4 → haru_cli-0.1.6}/config/logging.yaml +0 -0
  61. {haru_cli-0.1.4 → haru_cli-0.1.6}/config/mcp.yaml +0 -0
  62. {haru_cli-0.1.4 → haru_cli-0.1.6}/config/models.yaml +0 -0
  63. {haru_cli-0.1.4 → haru_cli-0.1.6}/config/prompts/researcher.md +0 -0
  64. {haru_cli-0.1.4 → haru_cli-0.1.6}/config/prompts/supervisor.md +0 -0
  65. {haru_cli-0.1.4 → haru_cli-0.1.6}/config/prompts/writer.md +0 -0
  66. {haru_cli-0.1.4 → haru_cli-0.1.6}/renovate.json +0 -0
  67. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/__init__.py +0 -0
  68. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/__main__.py +0 -0
  69. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/agents/orchestration.py +0 -0
  70. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/auth/__init__.py +0 -0
  71. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/auth/cache.py +0 -0
  72. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/auth/pkce.py +0 -0
  73. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/commands/__init__.py +0 -0
  74. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/commands/agents.py +0 -0
  75. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/commands/session.py +0 -0
  76. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/config/__init__.py +0 -0
  77. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/config/loader.py +0 -0
  78. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/config/schema.py +0 -0
  79. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/config/templates/__init__.py +0 -0
  80. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/config/templates/agents.yaml +0 -0
  81. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/config/templates/haru.yaml +0 -0
  82. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/config/templates/logging.yaml +0 -0
  83. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/config/templates/mcp.yaml +0 -0
  84. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/config/templates/models.yaml +0 -0
  85. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/config/templates/prompts/researcher.md +0 -0
  86. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/config/templates/prompts/supervisor.md +0 -0
  87. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/config/templates/prompts/writer.md +0 -0
  88. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/errors.py +0 -0
  89. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/models/__init__.py +0 -0
  90. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/observability/__init__.py +0 -0
  91. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/observability/guardrails.py +0 -0
  92. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/sessions/__init__.py +0 -0
  93. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/steering/__init__.py +0 -0
  94. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/steering/prompts.py +0 -0
  95. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/tools/__init__.py +0 -0
  96. {haru_cli-0.1.4 → haru_cli-0.1.6}/src/haru/tools/registry.py +0 -0
  97. {haru_cli-0.1.4 → haru_cli-0.1.6}/tests/conftest.py +0 -0
  98. {haru_cli-0.1.4 → haru_cli-0.1.6}/tests/integration/test_packaging.py +0 -0
  99. {haru_cli-0.1.4 → haru_cli-0.1.6}/tests/unit/test_cache.py +0 -0
  100. {haru_cli-0.1.4 → haru_cli-0.1.6}/tests/unit/test_cli.py +0 -0
  101. {haru_cli-0.1.4 → haru_cli-0.1.6}/tests/unit/test_config_command.py +0 -0
  102. {haru_cli-0.1.4 → haru_cli-0.1.6}/tests/unit/test_config_loader.py +0 -0
  103. {haru_cli-0.1.4 → haru_cli-0.1.6}/tests/unit/test_main.py +0 -0
  104. {haru_cli-0.1.4 → haru_cli-0.1.6}/tests/unit/test_orchestration.py +0 -0
  105. {haru_cli-0.1.4 → haru_cli-0.1.6}/tests/unit/test_pkce.py +0 -0
  106. {haru_cli-0.1.4 → haru_cli-0.1.6}/tests/unit/test_prompts.py +0 -0
  107. {haru_cli-0.1.4 → haru_cli-0.1.6}/tests/unit/test_sessions.py +0 -0
  108. {haru_cli-0.1.4 → haru_cli-0.1.6}/tests/unit/test_sso.py +0 -0
@@ -9,6 +9,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ### Added
11
11
 
12
+ - `haru doctor`: diagnoses configuration, SSO token, identity provenance,
13
+ role credentials, caller identity, region divergence, Bedrock reachability,
14
+ inference-profile availability, and guardrail sanity, with per-check
15
+ remediation and `--json` output. `--invoke` makes one real (billable)
16
+ Bedrock call for a definitive answer.
17
+ - `haru doctor --all-roles`: probes every account and role you are assigned
18
+ and prints a matrix of which combinations can actually reach Bedrock -
19
+ answering "which IAM role do I need?" empirically.
20
+ - Global `--debug` flag and real logging: `logging.yaml` (level/format/file)
21
+ is now honoured instead of being dead config. AWS SDK logging rises to INFO
22
+ under `--debug` (operations and error codes, never bodies or headers), and a
23
+ redaction filter masks credential-shaped values.
24
+ - `docs/troubleshooting.md`: the IAM policy to hand an AWS administrator,
25
+ including the easily-missed foundation-model ARNs that cross-region
26
+ inference profiles require, and why a Kiro/Amazon Q role will not work.
27
+
28
+ ### Fixed
29
+
30
+ - Bedrock failures no longer surface as raw botocore tracebacks. AccessDenied,
31
+ ResourceNotFound, Validation, Throttling, and expired-credential errors are
32
+ translated into actionable messages naming the model, region, role, and the
33
+ exact IAM action required. A denial mid-chat now returns to the prompt
34
+ instead of ending the session.
35
+
36
+ ### Fixed
37
+
38
+ - Stale configuration pins no longer produce a broken sign-in: `haru login`
39
+ validates a pinned `role_name`/`account_id` against your actual Identity
40
+ Center assignments, warns, and falls back to the chooser when they do not
41
+ match (previously a leftover `role_name` from an older `config init` was
42
+ persisted unchecked and every later command failed).
43
+ - Role rejections are reported honestly: `AWS rejected role 'X' in account
44
+ 'Y' … the role may not be assigned to you` instead of the misleading
45
+ "SSO credentials rejected; run 'haru login'", which is now reserved for
46
+ genuinely invalid tokens.
47
+
48
+ ### Changed
49
+
50
+ - CLI startup is roughly 6x faster (~0.6s to ~0.1s for `haru --version`):
51
+ strands, mcp, boto3, botocore, and opentelemetry are imported lazily at
52
+ their call sites, so `--help`, `--version`, `config`, `agents`, and
53
+ `session list` never load the agent/AWS stack. A guard test fails the
54
+ build if an eager import creeps back in.
55
+ - `haru login` and `haru config show` report where the account and role
56
+ came from (login selection, pinned in config, or an environment variable).
57
+
58
+ ### Added
59
+
12
60
  - The login callback now serves a proper local result page (dark themed,
13
61
  approval/denial card): "Request approved" on success, "Request denied"
14
62
  with the reason on error, missing parameters, or a state mismatch —
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haru-cli
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: Secure, scriptable CLI for Amazon Bedrock Claude models via the AWS Strands Agents SDK.
5
5
  Author: haru-cli maintainers
6
6
  License-Expression: Apache-2.0
@@ -56,6 +56,8 @@ uv sync
56
56
 
57
57
  | Command | Purpose |
58
58
  | -------------------------------- | ------------------------------------------------------------- |
59
+ | `haru doctor` | Diagnose config, sign-in, and Bedrock permissions |
60
+ | `haru doctor --all-roles` | Find which account+role can actually reach Bedrock |
59
61
  | `haru config init` | Create a starter configuration (interactive) |
60
62
  | `haru config show` | Show the resolved configuration (no secrets) |
61
63
  | `haru login` | Browser sign-in to IAM Identity Center (OAuth 2.0 + PKCE) |
@@ -68,7 +70,9 @@ uv sync
68
70
  | `haru --version` | Print the installed version |
69
71
 
70
72
  Configuration is resolved from `--config`, then `$HARU_CONFIG`, then
71
- `./config/haru.yaml`, then `~/.config/haru/haru.yaml`.
73
+ `./config/haru.yaml`, then `~/.config/haru/haru.yaml`. `haru --debug <command>`
74
+ turns on verbose logging, including the AWS API operations and error codes
75
+ behind a failure (never request bodies or credentials).
72
76
 
73
77
  Inside `haru chat`, slash commands switch targets mid-session:
74
78
 
@@ -93,7 +97,15 @@ haru chat --agent supervisor --session-id demo
93
97
  AWS accounts and permission-set roles are discovered automatically (like
94
98
  `aws configure sso`), auto-selected when there is exactly one, prompted
95
99
  otherwise, and remembered for later sessions. Pin `auth.sso.account_id` /
96
- `role_name` in configuration only if you want to skip the discovery.
100
+ `role_name` in configuration only if you want to skip the discovery — pins
101
+ are validated against your actual assignments, and a stale pin falls back to
102
+ the chooser with a warning rather than failing later.
103
+
104
+ The consent screen you see in the browser ("Allow haru-cli to access your
105
+ data?") is rendered by AWS IAM Identity Center, and its wording reflects the
106
+ scopes the client requests. haru requests only `sso:account:access`, so AWS
107
+ describes exactly that: the ability to assume roles you have been assigned.
108
+ Once you approve, haru serves a local result page confirming the outcome.
97
109
 
98
110
  Authentication uses the same PKCE authorization-code flow as `aws sso login`,
99
111
  caching tokens in the botocore-compatible schema under `~/.aws/sso/cache`
@@ -139,6 +151,27 @@ Declarative YAML under `config/` — models, agents, orchestration
139
151
  logging, and OpenTelemetry. No secrets in YAML: values reference environment
140
152
  variables with `${env:VAR}`. See [docs/configuration.md](docs/configuration.md).
141
153
 
154
+ ## Troubleshooting
155
+
156
+ If `haru login` succeeds but `haru chat` fails, the role you signed in with
157
+ almost certainly lacks Bedrock permissions. Signing in and being allowed to
158
+ call Bedrock are separate things: haru assumes a role in **your** AWS account
159
+ and calls Bedrock there, so that role needs `bedrock:InvokeModel` and
160
+ `bedrock:InvokeModelWithResponseStream`.
161
+
162
+ Kiro and Amazon Q working is not evidence that haru will — they send your SSO
163
+ token to the managed Amazon Q service and never call Bedrock in your account,
164
+ so there is no Kiro role to copy.
165
+
166
+ ```bash
167
+ haru doctor # what is configured, signed in, and permitted
168
+ haru doctor --all-roles # which of your assigned roles can reach Bedrock
169
+ haru doctor --invoke # definitive check (makes one billable call)
170
+ ```
171
+
172
+ See [docs/troubleshooting.md](docs/troubleshooting.md) for the IAM policy to
173
+ hand your AWS administrator.
174
+
142
175
  ## Development
143
176
 
144
177
  All quality gates must pass before a change is considered done:
@@ -28,6 +28,8 @@ uv sync
28
28
 
29
29
  | Command | Purpose |
30
30
  | -------------------------------- | ------------------------------------------------------------- |
31
+ | `haru doctor` | Diagnose config, sign-in, and Bedrock permissions |
32
+ | `haru doctor --all-roles` | Find which account+role can actually reach Bedrock |
31
33
  | `haru config init` | Create a starter configuration (interactive) |
32
34
  | `haru config show` | Show the resolved configuration (no secrets) |
33
35
  | `haru login` | Browser sign-in to IAM Identity Center (OAuth 2.0 + PKCE) |
@@ -40,7 +42,9 @@ uv sync
40
42
  | `haru --version` | Print the installed version |
41
43
 
42
44
  Configuration is resolved from `--config`, then `$HARU_CONFIG`, then
43
- `./config/haru.yaml`, then `~/.config/haru/haru.yaml`.
45
+ `./config/haru.yaml`, then `~/.config/haru/haru.yaml`. `haru --debug <command>`
46
+ turns on verbose logging, including the AWS API operations and error codes
47
+ behind a failure (never request bodies or credentials).
44
48
 
45
49
  Inside `haru chat`, slash commands switch targets mid-session:
46
50
 
@@ -65,7 +69,15 @@ haru chat --agent supervisor --session-id demo
65
69
  AWS accounts and permission-set roles are discovered automatically (like
66
70
  `aws configure sso`), auto-selected when there is exactly one, prompted
67
71
  otherwise, and remembered for later sessions. Pin `auth.sso.account_id` /
68
- `role_name` in configuration only if you want to skip the discovery.
72
+ `role_name` in configuration only if you want to skip the discovery — pins
73
+ are validated against your actual assignments, and a stale pin falls back to
74
+ the chooser with a warning rather than failing later.
75
+
76
+ The consent screen you see in the browser ("Allow haru-cli to access your
77
+ data?") is rendered by AWS IAM Identity Center, and its wording reflects the
78
+ scopes the client requests. haru requests only `sso:account:access`, so AWS
79
+ describes exactly that: the ability to assume roles you have been assigned.
80
+ Once you approve, haru serves a local result page confirming the outcome.
69
81
 
70
82
  Authentication uses the same PKCE authorization-code flow as `aws sso login`,
71
83
  caching tokens in the botocore-compatible schema under `~/.aws/sso/cache`
@@ -111,6 +123,27 @@ Declarative YAML under `config/` — models, agents, orchestration
111
123
  logging, and OpenTelemetry. No secrets in YAML: values reference environment
112
124
  variables with `${env:VAR}`. See [docs/configuration.md](docs/configuration.md).
113
125
 
126
+ ## Troubleshooting
127
+
128
+ If `haru login` succeeds but `haru chat` fails, the role you signed in with
129
+ almost certainly lacks Bedrock permissions. Signing in and being allowed to
130
+ call Bedrock are separate things: haru assumes a role in **your** AWS account
131
+ and calls Bedrock there, so that role needs `bedrock:InvokeModel` and
132
+ `bedrock:InvokeModelWithResponseStream`.
133
+
134
+ Kiro and Amazon Q working is not evidence that haru will — they send your SSO
135
+ token to the managed Amazon Q service and never call Bedrock in your account,
136
+ so there is no Kiro role to copy.
137
+
138
+ ```bash
139
+ haru doctor # what is configured, signed in, and permitted
140
+ haru doctor --all-roles # which of your assigned roles can reach Bedrock
141
+ haru doctor --invoke # definitive check (makes one billable call)
142
+ ```
143
+
144
+ See [docs/troubleshooting.md](docs/troubleshooting.md) for the IAM policy to
145
+ hand your AWS administrator.
146
+
114
147
  ## Development
115
148
 
116
149
  All quality gates must pass before a change is considered done:
@@ -50,6 +50,15 @@ configuration; `top_k` and `seed` travel via
50
50
  non-default sampling values (HTTP 400), and no Claude model honours `seed` —
51
51
  use these fields with models that accept them (for example Haiku 4.5).
52
52
 
53
+ ## Logging
54
+
55
+ `logging.yaml` is honoured: `level` (DEBUG/INFO/WARNING/ERROR), `format`
56
+ (`json` or `text`), and `file` (a path also writes a 0600 log file). The
57
+ global `--debug` flag overrides the level and additionally raises AWS SDK
58
+ logging to INFO - operation names, endpoints, retries, and error codes, never
59
+ request bodies or headers. A redaction filter masks credential-shaped values
60
+ in all log output.
61
+
53
62
  ## Sessions
54
63
 
55
64
  Conversation persistence is configured in the base file:
@@ -0,0 +1,136 @@
1
+ # Troubleshooting
2
+
3
+ Start with `haru doctor`. It checks configuration, sign-in, and Bedrock
4
+ permissions, and prints what to fix.
5
+
6
+ ```bash
7
+ haru doctor # configuration, token, identity, Bedrock reachability
8
+ haru doctor --invoke # definitive: makes one real (billable) Bedrock call
9
+ haru doctor --all-roles # probe every account and role you are assigned
10
+ haru doctor --json # machine-readable, for a support ticket
11
+ ```
12
+
13
+ ## "`haru login` works but `haru chat` fails"
14
+
15
+ This is the common case, and it is usually not a bug. **Signing in and being
16
+ allowed to call Bedrock are two different permissions.**
17
+
18
+ haru's path is: browser SSO → `sso:GetRoleCredentials` → an assumed role in
19
+ **your own AWS account** → `bedrock-runtime` Converse. Signing in proves only
20
+ that IAM Identity Center assigned you a role; calling Bedrock additionally
21
+ requires that role to hold `bedrock:InvokeModel` and
22
+ `bedrock:InvokeModelWithResponseStream`, and that Bedrock model access is
23
+ enabled for the Claude models in that account and region.
24
+
25
+ ### Kiro or Amazon Q working tells you nothing about haru
26
+
27
+ Kiro CLI and Amazon Q authenticate through the same Identity Center, then send
28
+ the SSO token as a bearer credential to the **managed Amazon Q service**. They
29
+ never call Bedrock in your account, so they need no `bedrock:*` IAM
30
+ permissions at all — [Kiro's IAM documentation](https://kiro.dev/docs/cli/enterprise/iam/)
31
+ lists `sso:*`, `user-subscriptions:*`, and `q:*` actions and no
32
+ `bedrock:InvokeModel`.
33
+
34
+ Consequently an Amazon Q permission set (names like `AmazonQUsers`) is
35
+ **expected to fail** with haru, and there is no Kiro role to copy. Run
36
+ `haru doctor --all-roles` to find which of your assigned roles can actually
37
+ reach Bedrock:
38
+
39
+ ```
40
+ ACCOUNT NAME ROLE CREDS BEDROCK INVOKE
41
+ 881490127383 amazonq-prod AmazonQUsers ok denied denied
42
+ 111122223333 ml-sandbox BedrockDeveloper ok ok ok
43
+ ```
44
+
45
+ Then re-run `haru login` and choose the working combination, or pin it with
46
+ `auth.sso.account_id` and `auth.sso.role_name`.
47
+
48
+ ## Policy to give your AWS administrator
49
+
50
+ Attach to the permission set you sign in with. **Both invoke statements are
51
+ required.** haru defaults to `us.` cross-region inference profiles, so Bedrock
52
+ authorizes against the underlying foundation-model ARN in *every region the
53
+ profile routes to* — a policy granting only the inference-profile ARN produces
54
+ an `AccessDeniedException` naming a foundation-model ARN you never configured.
55
+
56
+ ```json
57
+ {
58
+ "Version": "2012-10-17",
59
+ "Statement": [
60
+ {
61
+ "Sid": "InvokeViaInferenceProfile",
62
+ "Effect": "Allow",
63
+ "Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
64
+ "Resource": "arn:aws:bedrock:us-east-1:ACCOUNT_ID:inference-profile/us.anthropic.claude-*"
65
+ },
66
+ {
67
+ "Sid": "InvokeUnderlyingFoundationModelsInEveryRoutedRegion",
68
+ "Effect": "Allow",
69
+ "Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
70
+ "Resource": [
71
+ "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-*",
72
+ "arn:aws:bedrock:us-east-2::foundation-model/anthropic.claude-*",
73
+ "arn:aws:bedrock:us-west-2::foundation-model/anthropic.claude-*"
74
+ ]
75
+ },
76
+ {
77
+ "Sid": "GuardrailsWhenConfigured",
78
+ "Effect": "Allow",
79
+ "Action": "bedrock:ApplyGuardrail",
80
+ "Resource": "arn:aws:bedrock:us-east-1:ACCOUNT_ID:guardrail/*"
81
+ },
82
+ {
83
+ "Sid": "Diagnostics",
84
+ "Effect": "Allow",
85
+ "Action": ["bedrock:ListFoundationModels", "bedrock:GetInferenceProfile"],
86
+ "Resource": "*"
87
+ }
88
+ ]
89
+ }
90
+ ```
91
+
92
+ There is **no `bedrock:Converse` IAM action** — Converse is authorized by
93
+ `bedrock:InvokeModel` and ConverseStream by
94
+ `bedrock:InvokeModelWithResponseStream`. A policy naming `bedrock:Converse`
95
+ grants nothing.
96
+
97
+ `sts:GetCallerIdentity` and the `sso:*` calls need no policy; they are
98
+ authorized by the service itself. That is why `haru doctor` can show a valid
99
+ assumed-role ARN while Bedrock is still denied.
100
+
101
+ ## Model access is separate from IAM
102
+
103
+ Even with the policy above, Bedrock refuses models that are not enabled for
104
+ the account. In the AWS console: **Bedrock → Model access → Manage model
105
+ access**, enable the Anthropic models, and confirm the region matches
106
+ `auth.bedrock_region` (or the model entry's `region`, which wins).
107
+
108
+ ## Debug logging
109
+
110
+ ```bash
111
+ haru --debug chat
112
+ ```
113
+
114
+ Raises haru's own logging to DEBUG and AWS SDK logging to INFO, showing the
115
+ API operations, endpoints, retries, and error codes behind a failure. Request
116
+ and response bodies and headers are never logged, and a redaction filter masks
117
+ credential-shaped values as defence in depth.
118
+
119
+ Set persistent logging in `logging.yaml`:
120
+
121
+ ```yaml
122
+ logging:
123
+ level: INFO # DEBUG, INFO, WARNING, ERROR
124
+ format: json # json or text
125
+ file: null # a path here writes a 0600 log file as well as stderr
126
+ ```
127
+
128
+ ## Other failures
129
+
130
+ | Symptom | Cause and fix |
131
+ | --- | --- |
132
+ | `No configuration found` | Run `haru config init`, or pass `--config`, or set `$HARU_CONFIG`. |
133
+ | `No cached SSO token` | Run `haru login`. |
134
+ | `AWS rejected role 'X' in account 'Y'` | The role is not assigned to you, or a stale pin is in your config. Run `haru doctor --all-roles`. |
135
+ | `Guardrails are enabled but no guardrail_id` | Set `guardrails.guardrail_id`, or `enabled: false`. Guardrails fail closed by design. |
136
+ | Bedrock rejects sampling parameters | Claude 5-series and Opus 4.7+ reject non-default `temperature`/`top_p`/`top_k`. Leave them unset for those models. |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "haru-cli"
3
- version = "0.1.4"
3
+ version = "0.1.6"
4
4
  description = "Secure, scriptable CLI for Amazon Bedrock Claude models via the AWS Strands Agents SDK."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -61,8 +61,21 @@ select = ["E", "F", "I", "N", "UP", "B", "SIM", "PL", "RUF", "S"]
61
61
 
62
62
  [tool.ruff.lint.per-file-ignores]
63
63
  # S101: pytest uses bare asserts. S105/S106/S107: tests exercise secret
64
- # handling with deliberately fake credential strings.
65
- "tests/**" = ["S101", "PLR2004", "S105", "S106", "S107"]
64
+ # handling with deliberately fake credential strings. S603: the import-time
65
+ # guard runs the interpreter in a subprocess with a fixed argv.
66
+ "tests/**" = ["S101", "PLR2004", "S105", "S106", "S107", "S603"]
67
+ # PLC0415: heavy third-party imports (strands, mcp, boto3, opentelemetry)
68
+ # are deliberately deferred to call sites so `haru --help` stays fast; the
69
+ # guard test tests/unit/test_import_time.py enforces this.
70
+ "src/haru/agents/factory.py" = ["PLC0415"]
71
+ "src/haru/auth/*" = ["PLC0415"]
72
+ "src/haru/commands/doctor.py" = ["PLC0415"]
73
+ "src/haru/commands/streaming.py" = ["PLC0415"]
74
+ "src/haru/diagnostics/*" = ["PLC0415"]
75
+ "src/haru/models/bedrock.py" = ["PLC0415"]
76
+ "src/haru/observability/telemetry.py" = ["PLC0415"]
77
+ "src/haru/sessions/manager.py" = ["PLC0415"]
78
+ "src/haru/tools/mcp.py" = ["PLC0415"]
66
79
 
67
80
  [tool.ruff.lint.isort]
68
81
  known-first-party = ["haru"]
@@ -0,0 +1 @@
1
+ """Agent factories and multi-agent orchestration."""
@@ -5,15 +5,9 @@ tools, and MCP tools. Multi-agent orchestration composes these in
5
5
  ``haru.agents.orchestration``.
6
6
  """
7
7
 
8
- from collections.abc import Mapping
9
- from pathlib import Path
10
- from typing import Any
8
+ from __future__ import annotations
11
9
 
12
- import boto3
13
- from strands import Agent
14
- from strands.models import BedrockModel
15
- from strands.session.session_manager import SessionManager
16
- from strands.tools.mcp import MCPClient
10
+ from typing import TYPE_CHECKING, Any
17
11
 
18
12
  from haru.config.schema import HaruConfig, SamplingConfig
19
13
  from haru.errors import ConfigError
@@ -21,6 +15,16 @@ from haru.models.bedrock import build_model, get_model_config, merge_sampling
21
15
  from haru.steering.prompts import DEFAULT_PROMPTS_ROOT, load_prompts, resolve_prompt
22
16
  from haru.tools.mcp import collect_tools
23
17
 
18
+ if TYPE_CHECKING:
19
+ from collections.abc import Mapping
20
+ from pathlib import Path
21
+
22
+ import boto3
23
+ from strands import Agent
24
+ from strands.models import BedrockModel
25
+ from strands.session.session_manager import SessionManager
26
+ from strands.tools.mcp import MCPClient
27
+
24
28
 
25
29
  def build_agent( # noqa: PLR0913 - keyword-only wiring points, all optional
26
30
  config: HaruConfig,
@@ -41,6 +45,8 @@ def build_agent( # noqa: PLR0913 - keyword-only wiring points, all optional
41
45
  model entry). Raises ConfigError for unknown agents, prompt references,
42
46
  tools, or an invalid override.
43
47
  """
48
+ from strands import Agent
49
+
44
50
  if agent_name is None:
45
51
  model_cfg = get_model_config(config, model_name)
46
52
  model = build_model(model_cfg, session, guardrails=config.guardrails, sampling=sampling)
@@ -4,8 +4,14 @@ Kiro-style browser sign-in needs no upfront AWS knowledge: after the token
4
4
  exchange, the accessible accounts and permission-set roles are listed from
5
5
  IAM Identity Center (as ``aws configure sso`` does), auto-selected when there
6
6
  is exactly one, and persisted next to the token cache for later sessions.
7
+
8
+ Configuration pins (``account_id``/``role_name``) are validated against the
9
+ user's actual assignments; a stale pin produces a warning and falls back to
10
+ the chooser instead of persisting a broken identity.
7
11
  """
8
12
 
13
+ from __future__ import annotations
14
+
9
15
  import json
10
16
  import os
11
17
  from collections.abc import Callable
@@ -13,9 +19,6 @@ from dataclasses import dataclass
13
19
  from pathlib import Path
14
20
  from typing import Any
15
21
 
16
- import boto3
17
- from botocore.exceptions import ClientError
18
-
19
22
  from haru.auth.cache import cache_path
20
23
  from haru.auth.sso import SsoToken
21
24
  from haru.config.schema import SsoConfig
@@ -24,6 +27,12 @@ from haru.errors import AuthError
24
27
  Chooser = Callable[[str, list[str]], int]
25
28
  """Callback picking one option: (question, labels) -> chosen index."""
26
29
 
30
+ Notify = Callable[[str], None]
31
+ """Callback for user-facing warnings during selection."""
32
+
33
+ SOURCE_SELECTED = "login selection"
34
+ SOURCE_CONFIG = "pinned in config"
35
+
27
36
 
28
37
  @dataclass(frozen=True)
29
38
  class SelectedIdentity:
@@ -32,6 +41,8 @@ class SelectedIdentity:
32
41
  account_id: str
33
42
  role_name: str
34
43
  account_name: str | None = None
44
+ account_source: str = SOURCE_SELECTED
45
+ role_source: str = SOURCE_SELECTED
35
46
 
36
47
 
37
48
  def identity_path(start_url: str, cache_dir: Path | None = None) -> Path:
@@ -72,57 +83,121 @@ def read_identity(start_url: str, cache_dir: Path | None = None) -> SelectedIden
72
83
  return None
73
84
 
74
85
 
75
- def select_identity(
86
+ def effective_identity(
87
+ sso_cfg: SsoConfig, cache_dir: Path | None = None
88
+ ) -> tuple[str | None, str, str | None, str]:
89
+ """Resolve the effective account and role with their provenance.
90
+
91
+ Returns ``(account_id, account_source, role_name, role_source)``;
92
+ values are None (source ``"unset"``) when nothing provides them.
93
+ Precedence per field: config pin, then ``account_id_env``, then the
94
+ identity persisted at login.
95
+ """
96
+ stored = read_identity(sso_cfg.start_url, cache_dir)
97
+
98
+ if sso_cfg.account_id is not None:
99
+ account, account_source = sso_cfg.account_id, SOURCE_CONFIG
100
+ elif sso_cfg.account_id_env is not None and os.environ.get(sso_cfg.account_id_env):
101
+ account = os.environ[sso_cfg.account_id_env]
102
+ account_source = f"from ${sso_cfg.account_id_env}"
103
+ elif stored is not None:
104
+ account, account_source = stored.account_id, SOURCE_SELECTED
105
+ else:
106
+ account, account_source = None, "unset"
107
+
108
+ if sso_cfg.role_name is not None:
109
+ role, role_source = sso_cfg.role_name, SOURCE_CONFIG
110
+ elif stored is not None:
111
+ role, role_source = stored.role_name, SOURCE_SELECTED
112
+ else:
113
+ role, role_source = None, "unset"
114
+
115
+ return account, account_source, role, role_source
116
+
117
+
118
+ def select_identity( # noqa: PLR0913 - keyword-only wiring points, all optional
76
119
  sso_cfg: SsoConfig,
77
120
  token: SsoToken,
78
121
  *,
79
122
  sso_client: Any | None = None,
80
123
  cache_dir: Path | None = None,
81
124
  chooser: Chooser | None = None,
125
+ notify: Notify | None = None,
82
126
  ) -> SelectedIdentity:
83
127
  """Resolve the account and role for this sign-in and persist the choice.
84
128
 
85
- Configuration pins win (``account_id``/``role_name``, or the account id
86
- from ``account_id_env``); anything unpinned is discovered from Identity
87
- Center. With multiple choices and no ``chooser``, raises AuthError.
129
+ Configuration pins are honoured only when they match the user's actual
130
+ assignments; a stale pin triggers a ``notify`` warning and falls back to
131
+ discovery. With multiple choices and no ``chooser``, raises AuthError.
88
132
  """
89
133
  if sso_client is None:
90
- sso_client = boto3.Session().client("sso", region_name=sso_cfg.sso_region)
134
+ import boto3
91
135
 
92
- account_id = _pinned_account(sso_cfg)
93
- account_name: str | None = None
94
- if account_id is None:
95
- accounts = _list_accounts(sso_client, token.access_token)
136
+ sso_client = boto3.Session().client("sso", region_name=sso_cfg.sso_region)
137
+ warn = notify if notify is not None else _ignore_warning
138
+
139
+ accounts = list_accounts(sso_client, token.access_token)
140
+ pinned_account, pin_source = _pinned_account(sso_cfg)
141
+ account_source = SOURCE_SELECTED
142
+ chosen = next((account for account in accounts if account["accountId"] == pinned_account), None)
143
+ if chosen is not None:
144
+ account_source = pin_source
145
+ else:
146
+ if pinned_account is not None:
147
+ warn(
148
+ f"Warning: configured account {pinned_account} is not accessible with"
149
+ " this sign-in; choose from your accessible accounts."
150
+ )
96
151
  chosen = _choose(
97
152
  "Choose the AWS account to use",
98
153
  accounts,
99
154
  lambda account: f"{account['accountId']} {account.get('accountName', '')}".strip(),
100
155
  chooser,
101
156
  )
102
- account_id = chosen["accountId"]
103
- account_name = chosen.get("accountName")
104
-
105
- role_name = sso_cfg.role_name
106
- if role_name is None:
107
- roles = _list_roles(sso_client, token.access_token, account_id)
157
+ account_id: str = chosen["accountId"]
158
+ account_name: str | None = chosen.get("accountName")
159
+
160
+ roles = list_roles(sso_client, token.access_token, account_id)
161
+ role_source = SOURCE_SELECTED
162
+ if sso_cfg.role_name is not None and sso_cfg.role_name in roles:
163
+ role_name = sso_cfg.role_name
164
+ role_source = SOURCE_CONFIG
165
+ else:
166
+ if sso_cfg.role_name is not None:
167
+ warn(
168
+ f"Warning: configured role {sso_cfg.role_name!r} is not assigned to you"
169
+ f" in account {account_id}; choose from your assigned roles."
170
+ )
108
171
  role_name = _choose(f"Choose the role for account {account_id}", roles, str, chooser)
109
172
 
110
173
  identity = SelectedIdentity(
111
- account_id=account_id, role_name=role_name, account_name=account_name
174
+ account_id=account_id,
175
+ role_name=role_name,
176
+ account_name=account_name,
177
+ account_source=account_source,
178
+ role_source=role_source,
112
179
  )
113
180
  write_identity(identity, sso_cfg.start_url, cache_dir)
114
181
  return identity
115
182
 
116
183
 
117
- def _pinned_account(sso_cfg: SsoConfig) -> str | None:
184
+ def _ignore_warning(message: str) -> None:
185
+ """Default notify callback: drop the warning (non-interactive callers)."""
186
+
187
+
188
+ def _pinned_account(sso_cfg: SsoConfig) -> tuple[str | None, str]:
118
189
  if sso_cfg.account_id is not None:
119
- return sso_cfg.account_id
190
+ return sso_cfg.account_id, SOURCE_CONFIG
120
191
  if sso_cfg.account_id_env is not None:
121
- return os.environ.get(sso_cfg.account_id_env)
122
- return None
192
+ value = os.environ.get(sso_cfg.account_id_env)
193
+ if value:
194
+ return value, f"from ${sso_cfg.account_id_env}"
195
+ return None, ""
123
196
 
124
197
 
125
- def _list_accounts(sso_client: Any, access_token: str) -> list[dict[str, Any]]:
198
+ def list_accounts(sso_client: Any, access_token: str) -> list[dict[str, Any]]:
199
+ from botocore.exceptions import ClientError
200
+
126
201
  try:
127
202
  paginator = sso_client.get_paginator("list_accounts")
128
203
  accounts = [
@@ -137,7 +212,9 @@ def _list_accounts(sso_client: Any, access_token: str) -> list[dict[str, Any]]:
137
212
  return sorted(accounts, key=lambda account: str(account["accountId"]))
138
213
 
139
214
 
140
- def _list_roles(sso_client: Any, access_token: str, account_id: str) -> list[str]:
215
+ def list_roles(sso_client: Any, access_token: str, account_id: str) -> list[str]:
216
+ from botocore.exceptions import ClientError
217
+
141
218
  try:
142
219
  paginator = sso_client.get_paginator("list_account_roles")
143
220
  roles = [